← All chapters

11 · Content revision 5

Multiply rows by columns, then reuse batches

Multiply checked 2-D and batched tensors with scalar Rust loops, including inner-dimension checks, batch broadcasting, and transpose flags.

Predict one row-by-column product

Treat two rows as two token positions with three features. The activation matrix AA has shape [2,3]:

[[1, 2, 3],
 [4, 5, 6]]

The projection weight WW has shape [3,2]:

[[1, 2],
 [0, 1],
 [2, 0]]

Predict the result shape before multiplying any values. The shared inner extents are both 33. The two outer extents survive, so the result has shape [2,2].

Now predict cell C1,0C_{1,0}. Select row 11 from AA and column 00 from WW, pair the entries at the same inner coordinate, and add from k=0k=0 upward:

C1,0=41+50+62=4+0+12=16.\begin{aligned} C_{1,0} &= 4\cdot1 + 5\cdot0 + 6\cdot2 \\ &= 4 + 0 + 12 \\ &= 16. \end{aligned}

Apply the same rule to all four row-column pairs:

The output CC has shape [2,2] and values:

[[ 7,  4],
 [16, 13]]

For the later batched prediction, left batch zero is this original AA, while left batch one is:

[[0, 1, 2],
 [2, 1, 0]]

The right operand has shape [1,3,2] and stores the original WW exactly once. Its singleton leading axis maps both output batches to right batch zero.

This is not elementwise multiplication: AA and WW do not even have the same shape. One output cell consumes a complete row and a complete column, and the shared inner axis disappears.

Contract one shared inner dimension

The chapter’s matrix formula is:

Cij=k=0K1AikBkjC_{ij}=\sum_{k=0}^{K-1} A_{ik}B_{kj}

For the worked example, the general right operand is the projection weight: B=WB=W.

Fix one output row ii and column jj. Initialize a running sum to 00. For each kk from zero through K1K-1, multiply AikA_{ik} by BkjB_{kj} and add that product. The loop order is part of this reference implementation because floating-point addition is not generally associative.

For batched shapes [,M,K][\ldots,M,K] and [,K,N][\ldots,K,N], the formula runs independently at every compatible leading batch coordinate. Batch coordinates are omitted from the notation so the newly introduced row-column contraction stays visible. Only the leading batch axes broadcast; the two KK extents must be equal.

Name every matrix index and extent

SymbolOperational meaning
CijC_{ij}The output value at row ii and column jj.
AikA_{ik}The left input value at output row ii and contracted position kk.
BkjB_{kj}The right input value at contracted position kk and output column jj.
iiThe zero-based output-row index.
jjThe zero-based output-column index.
kkThe zero-based coordinate traversed along both shared inner axes.
KKThe equal inner extent and therefore the number of scalar multiplications whose products are summed into one cell.

The output keeps MM rows from the effective left matrix and NN columns from the effective right matrix. A transpose flag logically swaps only an operand’s last two axes. Thus stored W𝖳W^{\mathsf T} shape [2,3] can act as logical shape [3,2] when transpose_right=true; no values need to be copied.

Rank-one promotion is intentionally absent. A vector must be represented with an explicit row or column axis so the output-shape rule has no hidden case.

From one fixed context vector to matrices of positions

Bengio et al.’s feed-forward neural language model looks up learned vectors for a fixed number of context words, concatenates them into one fixed-length context vector, and computes next-word scores with learned matrix-vector transforms. It shares features beyond count tables, but each prediction still uses that bounded context instead of masked self-attention across a sequence of positions.

The earlier model is Bengio et al., A Neural Probabilistic Language Model. Bengio et al. store learned word features in a matrix, concatenate the fixed context-word vectors, and compute next-word scores with successive learned matrix-vector transformations and a nonlinear hidden layer. In the paper, that calculation is y=b+Wx+Utanh(d+Hx)y=b+Wx+U\tanh(d+Hx), with learned word features stored in a V×m|V|\times m matrix CC.

That model is already part of LLM history: learned word features and shared neural parameters replace separate count-table rows. The bounded contrast here is its organization around one concatenated fixed-length context vector. The paper does not specify this course’s batched tensor API, stride rules, or error types.

The later sources are Vaswani et al., Attention Is All You Need and Radford et al., Language Models are Unsupervised Multitask Learners. Vaswani et al. pack queries, keys, and values into matrices, define attention through scaled query-key products followed by softmax and value weighting, and use learned query, key, value, and output projections plus two linear transforms in each position-wise feed-forward network. The GPT-2 report uses a Transformer-based architecture for autoregressive language models and scales its four model sizes from 12 to 48 layers, model widths 768 to 1600, and a 1024-token context. In the Transformer’s notation, the attention mapping is softmax(QK𝖳/dk)V\operatorname{softmax}(QK^{\mathsf T}/\sqrt{d_k})V.

Checked matrix multiplication is the reusable contraction behind learned projections, attention scores, and attention-weighted values on the road to a modern decoder. This course’s batched broadcasting, transpose flags, strided traversal, storage policy, zero-size rules, and explicit errors are local correctness decisions, not designs attributed to the papers.

The Rust contrast makes that historical step concrete without pretending to implement any cited model. fixed_context_projection calculates one three-feature context vector with a shape-specific loop. The general tensor path applies the same row-column rule to multiple token rows and leading batches.

Compare one fixed-context weighted sum with the contraction later repeated across token rows rust/demos/ch11-matrix-multiplication/src/lib.rs#fixed-width-projection
/// Projects one fixed-width context representation with scalar operations.
///
/// This shape-specific baseline makes the older matrix-vector calculation
/// explicit; it is not presented as the exact equation of any cited model.
pub fn fixed_context_projection(context: [f64; 3], weights: [[f64; 2]; 3]) -> [f64; 2] {
    let mut output = [0.0; 2];
    for column in 0..2 {
        for inner in 0..3 {
            output[column] += context[inner] * weights[inner][column];
        }
    }
    output
}

Check shapes before the scalar loops

The error type keeps each rejected invariant visible. The API checks left rank, right rank, effective inner dimensions after transpose flags, batch axes from left to right, the complete output layout, and then output allocation. A request that violates several rules therefore has one stable first error.

Keep every checked matrix-product rejection in a distinct error variant rust/crates/llm-from-scratch/src/tensor/matmul.rs#matmul-errors
/// A rejected matrix product, output layout, allocation, or converted view operation.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MatmulError {
    /// An owned output layout violates the tensor storage invariant.
    Tensor(TensorError),
    /// A tensor-view error was converted into the matrix-multiplication error type.
    View(TensorViewError),
    /// Matrix multiplication does not promote a left vector in this chapter.
    LeftRankTooSmall { rank: usize },
    /// Matrix multiplication does not promote a right vector in this chapter.
    RightRankTooSmall { rank: usize },
    /// The effective final left axis and penultimate right axis differ.
    InnerDimensionMismatch { left: usize, right: usize },
    /// Two trailing-aligned batch dimensions are neither equal nor singleton.
    IncompatibleBatch {
        axis: usize,
        left_dimension: usize,
        right_dimension: usize,
    },
    /// The checked output shape is valid, but its value buffer cannot be reserved.
    OutputAllocationFailed { elements: usize },
}

impl fmt::Display for MatmulError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Tensor(error) => error.fmt(formatter),
            Self::View(error) => error.fmt(formatter),
            Self::LeftRankTooSmall { rank } => {
                write!(
                    formatter,
                    "left matmul input must have rank at least 2, got {rank}"
                )
            }
            Self::RightRankTooSmall { rank } => {
                write!(
                    formatter,
                    "right matmul input must have rank at least 2, got {rank}"
                )
            }
            Self::InnerDimensionMismatch { left, right } => write!(
                formatter,
                "matmul inner dimensions do not match: left size {left}, right size {right}"
            ),
            Self::IncompatibleBatch {
                axis,
                left_dimension,
                right_dimension,
            } => write!(
                formatter,
                "cannot broadcast batch axis {axis}: left size {left_dimension}, right size {right_dimension}"
            ),
            Self::OutputAllocationFailed { elements } => write!(
                formatter,
                "cannot allocate output buffer for {elements} f64 values"
            ),
        }
    }
}

impl Error for MatmulError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            Self::Tensor(error) => Some(error),
            Self::View(error) => Some(error),
            _ => None,
        }
    }
}

impl From<TensorError> for MatmulError {
    fn from(error: TensorError) -> Self {
        Self::Tensor(error)
    }
}

impl From<TensorViewError> for MatmulError {
    fn from(error: TensorViewError) -> Self {
        Self::View(error)
    }
}

Batch planning right-aligns only axes before the last two matrix axes. Missing leading axes act as one, and a singleton input batch coordinate maps to zero. The planner validates the complete output shape rather than the batch prefix in isolation. That detail admits a valid empty result such as the input-shape pair [usize::MAX,2,0,3] and [1,2,3,1]: the zero row extent makes the full output empty before any input is read.

TensorView::get remains the public operation for a coordinate supplied by a caller. Because that coordinate may have the wrong rank or an out-of-bounds index, get must validate every call. Matrix multiplication starts from a different boundary: both views are already valid, and the planner has already checked the effective matrix dimensions, batch compatibility, and complete output layout. The scalar loop can therefore reuse one checked offset plan instead of reconstructing and revalidating two coordinates for every product.

For one output cell, each operand needs a starting storage offset before kk changes. This chapter calls that starting position the cell-base offset. The contracted-axis stride then moves from the k=0k=0 value to the k=1k=1 value, and so on.

For output shape [...,M,N], each operand receives one effective stride for every output axis. A leading batch axis whose input extent is greater than one keeps its input stride; a missing or size-one batch axis receives stride 0 so another output batch reuses the same input batch. On the left, the output-row axis keeps the effective row stride and the output-column axis receives 0. On the right, the output-row axis receives 0 and the output-column axis keeps the effective column stride. The planner separately stores the left and right strides for advancing along the shared inner axis.

The worked matrices make this plan concrete. Contiguous AA has strides [3,1], and contiguous WW has strides [2,1]. Across output coordinates [0,0], [0,1], [1,0], [1,1], the left cell-base strides [3,0] produce offsets [0,0,3,3]; the right cell-base strides [0,1] produce offsets [0,1,0,1]. Cell C1,0C_{1,0} therefore starts at left offset 3 and right offset 0. Advancing by the contracted-axis strides 1 and 2 yields the three offset pairs (3,0), (4,2), and (5,4), which read 414\cdot1, 505\cdot0, and 626\cdot2 in ascending kk order.

For the batched shapes [2,2,3] and [1,3,2], the corresponding cell-base strides are [6,3,0] on the left and [0,0,1] on the right. The first 0 in the right plan is the singleton batch axis: output batches 0 and 1 both start from right batch 0. A transpose flag swaps the roles of an operand’s last two extents and strides; it does not move the stored values.

The two cell-base cursors are checked once and then emit offsets in batch-major, row-major, column-major order. Within each cell, the loop reads the left value, reads the right value, multiplies them, and adds to the running sum while kk increases. Reads still use ordinary safe bounds-checked slice indexing. No coordinate vector is allocated inside the contraction, and no implicit materialization occurs for a sliced or transposed view: that view simply supplies different base offsets and strides.

Plan checked cell-base and inner-axis strides once, then accumulate by storage offset rust/crates/llm-from-scratch/src/tensor/matmul.rs#checked-matmul
#[derive(Debug)]
struct MatmulPlan {
    /// The owned result's logical row-major shape and element count.
    output_shape: Vec<usize>,
    output_len: usize,
    /// The number of products accumulated into each output cell.
    inner: usize,
    /// One source-storage movement per output batch, row, and column axis.
    left_cell_strides: Vec<usize>,
    right_cell_strides: Vec<usize>,
    /// Source-storage movement when the contracted index increases by one.
    left_inner_stride: usize,
    right_inner_stride: usize,
}

#[derive(Clone, Copy, Debug)]
struct EffectiveMatrixLayout {
    rows: usize,
    columns: usize,
    row_stride: usize,
    column_stride: usize,
}

/// Multiplies two rank-two or batched tensor views as stored.
pub fn matmul(left: &TensorView<'_>, right: &TensorView<'_>) -> Result<Tensor, MatmulError> {
    matmul_with_transpose(left, right, false, false)
}

/// Multiplies two tensor views after optional logical final-axis transposes.
///
/// Inputs must have rank at least two. Only axes before the final two matrix
/// axes broadcast, using trailing alignment. The effective inner dimensions
/// must match exactly. The scalar contraction visits `k` in ascending order and
/// reads through offsets established by one checked strided plan per operand.
pub fn matmul_with_transpose(
    left: &TensorView<'_>,
    right: &TensorView<'_>,
    transpose_left: bool,
    transpose_right: bool,
) -> Result<Tensor, MatmulError> {
    let plan = MatmulPlan::new(left, right, transpose_left, transpose_right)?;
    let mut values = output_buffer(plan.output_len)?;
    if plan.inner == 0 {
        values.resize(plan.output_len, 0.0);
        return Tensor::from_vec(plan.output_shape, values).map_err(Into::into);
    }

    let left_cell_offsets = left
        .projected_offsets(&plan.output_shape, &plan.left_cell_strides, plan.output_len)
        .expect("a checked matmul plan retains valid left cell offsets");
    let right_cell_offsets = right
        .projected_offsets(
            &plan.output_shape,
            &plan.right_cell_strides,
            plan.output_len,
        )
        .expect("a checked matmul plan retains valid right cell offsets");

    for (left_cell_offset, right_cell_offset) in left_cell_offsets.zip(right_cell_offsets) {
        let mut sum = 0.0;
        let mut left_offset = left_cell_offset;
        let mut right_offset = right_cell_offset;
        for inner_index in 0..plan.inner {
            let left_value = left.value_at_storage_offset(left_offset);
            let right_value = right.value_at_storage_offset(right_offset);
            sum += left_value * right_value;

            if inner_index + 1 < plan.inner {
                left_offset = left_offset
                    .checked_add(plan.left_inner_stride)
                    .expect("a checked matmul plan cannot overflow along the left inner axis");
                right_offset = right_offset
                    .checked_add(plan.right_inner_stride)
                    .expect("a checked matmul plan cannot overflow along the right inner axis");
            }
        }
        values.push(sum);
    }

    Tensor::from_vec(plan.output_shape, values).map_err(Into::into)
}

impl MatmulPlan {
    fn new(
        left: &TensorView<'_>,
        right: &TensorView<'_>,
        transpose_left: bool,
        transpose_right: bool,
    ) -> Result<Self, MatmulError> {
        if left.rank() < 2 {
            return Err(MatmulError::LeftRankTooSmall { rank: left.rank() });
        }
        if right.rank() < 2 {
            return Err(MatmulError::RightRankTooSmall { rank: right.rank() });
        }

        let left_matrix = effective_matrix_layout(left, transpose_left);
        let right_matrix = effective_matrix_layout(right, transpose_right);
        let rows = left_matrix.rows;
        let inner = left_matrix.columns;
        let right_inner = right_matrix.rows;
        let columns = right_matrix.columns;
        if inner != right_inner {
            return Err(MatmulError::InnerDimensionMismatch {
                left: inner,
                right: right_inner,
            });
        }

        let left_batch_shape = &left.shape()[..left.rank() - 2];
        let right_batch_shape = &right.shape()[..right.rank() - 2];
        let batch_shape = broadcast_batch_shape(left_batch_shape, right_batch_shape)?;
        let mut output_shape = batch_shape.clone();
        output_shape.extend([rows, columns]);
        let (_, output_len) = checked_row_major_layout(&output_shape)?;

        let batch_rank = batch_shape.len();
        let mut left_cell_strides = batch_effective_strides(left, batch_rank);
        left_cell_strides.extend([left_matrix.row_stride, 0]);
        let mut right_cell_strides = batch_effective_strides(right, batch_rank);
        right_cell_strides.extend([0, right_matrix.column_stride]);

        Ok(Self {
            output_shape,
            output_len,
            inner,
            left_cell_strides,
            right_cell_strides,
            left_inner_stride: left_matrix.column_stride,
            right_inner_stride: right_matrix.row_stride,
        })
    }
}

fn effective_matrix_layout(input: &TensorView<'_>, transposed: bool) -> EffectiveMatrixLayout {
    let matrix_axis = input.rank() - 2;
    let stored = EffectiveMatrixLayout {
        rows: input.shape()[matrix_axis],
        columns: input.shape()[matrix_axis + 1],
        row_stride: input.strides()[matrix_axis],
        column_stride: input.strides()[matrix_axis + 1],
    };
    if transposed {
        EffectiveMatrixLayout {
            rows: stored.columns,
            columns: stored.rows,
            row_stride: stored.column_stride,
            column_stride: stored.row_stride,
        }
    } else {
        stored
    }
}

fn broadcast_batch_shape(left: &[usize], right: &[usize]) -> Result<Vec<usize>, MatmulError> {
    let output_rank = left.len().max(right.len());
    let left_padding = output_rank - left.len();
    let right_padding = output_rank - right.len();
    let mut output = Vec::with_capacity(output_rank);

    for axis in 0..output_rank {
        let left_dimension = left
            .get(axis.wrapping_sub(left_padding))
            .copied()
            .unwrap_or(1);
        let right_dimension = right
            .get(axis.wrapping_sub(right_padding))
            .copied()
            .unwrap_or(1);
        let dimension = if left_dimension == right_dimension {
            left_dimension
        } else if left_dimension == 1 {
            right_dimension
        } else if right_dimension == 1 {
            left_dimension
        } else {
            return Err(MatmulError::IncompatibleBatch {
                axis,
                left_dimension,
                right_dimension,
            });
        };
        output.push(dimension);
    }
    Ok(output)
}

fn batch_effective_strides(input: &TensorView<'_>, output_batch_rank: usize) -> Vec<usize> {
    let input_batch_rank = input.rank() - 2;
    let padding = output_batch_rank - input_batch_rank;
    (0..output_batch_rank)
        .map(|output_axis| {
            if output_axis < padding {
                return 0;
            }

            let input_axis = output_axis - padding;
            if input.shape()[input_axis] == 1 {
                0
            } else {
                input.strides()[input_axis]
            }
        })
        .collect()
}

If a batch, row, or column extent is zero, the result is empty and performs no reads. If only K=0K=0, output cells still exist but neither input has a readable inner-axis value. After reserving the output, the implementation writes one positive 0.0 per cell without constructing an offset cursor or reading either input. Contracted extents 00 and 11 do not broadcast and are rejected as an inner mismatch.

The small helper constructs the 2-D product, the stored right transpose, and two left batches sharing one weight batch. Applying the shape-specific function to each row gives the same four values as the rank-generic matrix product.

Build one 2-D product, logical right transpose, and shared-weight batched product rust/demos/ch11-matrix-multiplication/src/lib.rs#tiny-matmul-example
/// Multiplies the same values as 2-D matrices, a logical transpose, and batches.
pub fn tiny_matrix_multiplication_example() -> Result<TinyMatrixMultiplicationExample, MatmulError>
{
    let token_rows = Tensor::from_vec(TOKEN_SHAPE.to_vec(), TOKEN_VALUES.to_vec())?;
    let weights = Tensor::from_vec(WEIGHT_SHAPE.to_vec(), WEIGHT_VALUES.to_vec())?;
    let product = matmul(&token_rows.view(), &weights.view())?;

    let stored_transpose = Tensor::from_vec(
        STORED_TRANSPOSE_SHAPE.to_vec(),
        STORED_TRANSPOSE_VALUES.to_vec(),
    )?;
    let transpose_product =
        matmul_with_transpose(&token_rows.view(), &stored_transpose.view(), false, true)?;

    let batched_token_rows =
        Tensor::from_vec(BATCHED_TOKEN_SHAPE.to_vec(), BATCHED_TOKEN_VALUES.to_vec())?;
    let batched_weights = Tensor::from_vec(BATCHED_WEIGHT_SHAPE.to_vec(), WEIGHT_VALUES.to_vec())?;
    let batched_product = matmul(&batched_token_rows.view(), &batched_weights.view())?;

    Ok(TinyMatrixMultiplicationExample {
        token_rows,
        weights,
        product,
        stored_transpose,
        transpose_product,
        batched_token_rows,
        batched_weights,
        batched_product,
    })
}

The learner program also shows a zero-inner contraction and the exact rank, inner-dimension, and batch-dimension errors.

Run 2-D, transpose, batch, zero-inner, and error examples rust/demos/ch11-matrix-multiplication/src/main.rs#learner-matrix-multiplication-output
    let example = tiny_matrix_multiplication_example()?;

    let zero_inner_left = Tensor::from_vec(vec![2, 0], vec![])?;
    let zero_inner_right = Tensor::from_vec(vec![0, 2], vec![])?;
    let zero_inner = matmul(&zero_inner_left.view(), &zero_inner_right.view())?;

    let wrong_inner = Tensor::from_vec(vec![4, 2], vec![0.0; 8])?;
    let inner_error = matmul(&example.token_rows.view(), &wrong_inner.view()).unwrap_err();

    let wrong_batch = Tensor::from_vec(vec![3, 3, 2], vec![0.0; 18])?;
    let batch_error = matmul(&example.batched_token_rows.view(), &wrong_batch.view()).unwrap_err();

    let rank_one = Tensor::from_vec(vec![3], vec![1.0, 2.0, 3.0])?;
    let rank_error =
        matmul_with_transpose(&rank_one.view(), &example.weights.view(), false, false).unwrap_err();

Integer-valued examples can be compared exactly. A decimal dot product instead uses absolute tolerance 1e-12, because decimal fractions are not generally exact binary floating-point values. The cancellation example also makes the ascending-kk accumulation order observable. No library implements matrix multiplication for this chapter.

Trace one output cell, then add a batch axis

Read the figure in four passes:

  1. Follow the solid row marker across AA and the dotted column marker down WW. Their intersection is the double-bordered C1,0C_{1,0} cell.
  2. Traverse the three double-bordered contraction cards in kk order. Verify products 4.0, 0.0, and 12.0, then running totals 4.0, 4.0, 16.0.
  3. Compare stored right shape [2,3] with logical shape [3,2], then notice that both output batches map to right batch zero.
  4. Inspect the dashed errors. Inner sizes 3 and 4 fail before a scalar loop; matching matrix axes with batch sizes 2 and 3 reach the batch error.

Follow one row-by-column contraction, then reuse one weight batch

Compare three matrices, accumulate one selected output cell in contracted-index order, and inspect logical transposition, batch reuse, and rejected shapes.

Left activation shape
[2, 3]
Projection weight shape
[3, 2]
Product shape
[2, 2]

Select one left row and one right column

The solid arrow and left border select one row of the left activation. The downward arrow and dotted border select one column of the projection weight. Their intersection becomes the double-bordered focused output cell.

AA — Shape [2, 3]
Row Column 0Column 1Column 2
0 1.02.03.0
Selected left row: 1 4.05.06.0
×\times
WW — Shape [3, 2]
Row Selected right column: Column 0 Column 1
0 1.02.0
1 0.01.0
2 2.00.0
==
CC — Shape [2, 2]
Row Column 0Column 1
0 7.0 4.0
1 Focused output cell: 16.0 13.0

Accumulate three products in contracted-index order

Each double-bordered card is one contracted-index step; the running total reaches the focused output only after all three products.

  1. Product accumulated along the inner dimension: Term k=0k=0

    4.01.0=4.04.0\cdot1.0=4.0

    Product
    4.0
    Running total
    4.0
  2. Product accumulated along the inner dimension: Term k=1k=1

    5.00.0=0.05.0\cdot0.0=0.0

    Product
    0.0
    Running total
    4.0
  3. Product accumulated along the inner dimension: Term k=2k=2

    6.02.0=12.06.0\cdot2.0=12.0

    Product
    12.0
    Running total
    16.0

Reuse logical weights without copying values

A transpose flag changes the logical final axes without materializing, while the curved arrow marks the single right batch reused for both output batches.

Stored shape
[2, 3]
Logical shape
[3, 2]
Output shape
[2, 2]
Output values
[7.0, 4.0, 16.0, 13.0]
  1. Shared weight batch reused by broadcasting: Output batch 0
    Left batch
    0
    Right batch
    0
    Values
    [7.0, 4.0, 16.0, 13.0]
  2. Shared weight batch reused by broadcasting: Output batch 1
    Left batch
    1
    Right batch
    0
    Values
    [4.0, 1.0, 2.0, 5.0]

Reject mismatched inner and batch dimensions

Dashed cards keep the exact dimensions rejected before allocation or scalar multiplication begins.

  1. Rejected matrix product: inner-dimension-mismatch The two inner dimensions must be equal. 343\ne4
  2. Rejected matrix product: incompatible-batch Leading batch dimensions must be equal or one of them must be singleton. Batch axis 0: 232\ne3

Read the focused output as a complete proof: one selected row and one selected column supply exactly three products, and their ordered running totals end at 1616. The batch cards then show that only the leading batch axis reuses the weight matrix. The two rejected examples distinguish equality of the contracted inner extents from broadcasting of leading batch axes.

Predict products before running Rust

Write effective shapes before calculating any values.

  1. Predict the output shape and all four values for the example matrix AA with shape [2,3] multiplied by WW with shape [3,2].
  2. Recompute C1,0C_{1,0} in ascending kk order. Which axis disappears, and why?
  3. Store W𝖳W^{\mathsf T} as shape [2,3]. Which flag restores logical shape [3,2], and what output remains unchanged?
  4. For left shape [2,2,3] and right shape [1,3,2], map output batch 1 to both input batch coordinates and predict its four values.
  5. Predict the product of shapes [2,0] and [0,2]. Why is the result not an error?
  6. Name the first error for a rank-one left input, inner sizes 3 and 4, and compatible matrix axes with leading batch sizes 2 and 3.
  7. For contiguous AA strides [3,1] and WW strides [2,1], write both cell-base stride plans and the three source-offset pairs used by C1,0C_{1,0}. Why can a sliced non-contiguous view use the same algorithm without a copy?
  8. Misconception check: is matrix multiplication elementwise multiplication followed by one global sum? Identify the distinct row, column, and inner indices.
Check the eight matrix-multiplication predictions
  1. The equal inner extents disappear. The outer extents form shape [2,2], and the values are [7,4,16,13] in row-major order.
  2. 414\cdot1 gives running total 44; 505\cdot0 leaves it 44; 626\cdot2 raises it to 1616. Axis kk disappears because all three matching positions contribute to one cell.
  3. Set transpose_right=true. Stored [2,3] is interpreted as logical [3,2], and the output remains shape [2,2] with [7,4,16,13].
  4. Output batch 1 maps to left batch 1 and right batch 0. Its product is shape [2,2] with values [4,1,2,5].
  5. Shape [2,2] contains four positive-zero values. Each cell exists, but K=0K=0 supplies no terms to a running sum initialized as 0.0.
  6. The errors are LeftRankTooSmall { rank: 1 }, then InnerDimensionMismatch { left: 3, right: 4 }, then IncompatibleBatch { axis: 0, left_dimension: 2, right_dimension: 3 }.
  7. The left cell-base plan is [3,0], and the right plan is [0,1]. C1,0C_{1,0} starts at offsets 3 and 0; contracted strides 1 and 2 produce (3,0), (4,2), and (5,4). A sliced view supplies its own checked base offset and strides to the same plan, so only the newly owned contiguous output is allocated.
  8. No. Output row ii selects one left row, output column jj selects one right column, and inner index kk pairs and sums their shared positions. Other row-column pairs produce other cells rather than joining one global sum.

Prepare learned projections and attention

The cumulative tensor core can now multiply rank-two or batched strided views, require equal contracted dimensions, broadcast only leading batch axes, and interpret optional final-axis transposes without copying. Checked shape and allocation failures occur before scalar evaluation, while successful results own predictable contiguous storage.

Public lookup still validates every coordinate supplied by a caller. Inside a validated matrix product, checked cell-base cursors plus two contracted-axis strides express the same batch, row, column, and kk choices without rebuilding coordinates for every scalar product.

This is the numerical contraction behind later learned projections, QK𝖳QK^{\mathsf T} attention scores, attention-weighted values, feed-forward layers, and their gradients. It is not yet a learned layer or attention mechanism: there are no parameters, bias, masking, scaling, normalization, or training graph here.

Chapter 12 next turns arbitrary matrix outputs into stable probabilities and log-probabilities. That chapter will subtract a maximum before exponentiation so large logits do not overflow and will keep the selected normalization axis explicit for vocabulary and attention distributions.