> ## Documentation Index
> Fetch the complete documentation index at: https://docs.superfloat.si/llms.txt
> Use this file to discover all available pages before exploring further.

# Matrix multiply

> a @ b, with NumPy's shape rules, built from the same fma chain as dot so every output is reproducible.

## The rule

`a @ b` (or `sf.matmul(a, b)`) computes each output as `dot` of a row of `a` and a column of `b`: a sequential chain of `fma` from k = 0, saturating at each step, in the width of the data. Every output equals the 1-D `sf.dot` of that row and column bit for bit, which is tested.

```python theme={null}
import sf

A = sf.array([[0.5, 0.25], [0.1, 0.2]], dtype=sf.sf16)
B = sf.array([[0.5, 0.0], [0.25, 0.5]], dtype=sf.sf16)
A @ B
# sfarray([[0.3125    , 0.125     ],
#          [0.09997559, 0.1000061 ]], dtype=sf16)
```

Take the top-left output: 0.5 × 0.5 = 0.25 (exact), then fma(0.25, 0.25, 0.25) = 0.0625 + 0.25 = 0.3125. The bottom-left shows truncation at work: 0.1 is 3277 codes, 3277 × 16384 / 32768 = 1638.5 truncates to 1638, plus 0.2 × 0.25 (6554 × 8192 / 32768 = 1638.5, truncates to 1638) gives 3276 codes = 0.09997559, not 0.1.

## Shapes

NumPy's rules: 1-D operands are promoted and the extra dimension dropped, leading dimensions broadcast as a batch. A mismatch raises `SFShapeError` on the host before anything runs:

```python theme={null}
x = sf.array([0.1, -0.25, 0.5], dtype=sf.sf16)
A @ x
# SFShapeError: matmul: shapes (2, 2) and (3,) are not aligned (2 != 3)
```

## Matmul plus bias

`sf.addmm(a, b, d)` is `a @ b` followed by one saturating add of `d`. It is a separate function because "the chain, then one add" is what the hardware's `mat_fma` instruction is believed to do, and it is what the PyTorch linear layer uses for the bias.

```python theme={null}
d = sf.array([[0.1, 0.1], [0.1, 0.1]], dtype=sf.sf16)
sf.addmm(A, B, d)
# sfarray([[0.4125061 , 0.2250061 ],
#          [0.19998169, 0.20001221]], dtype=sf16)
```

## On the device

With device arrays, `@` is a heavy operation: it goes through the pre-flight gate, runs on the target, and returns a status.

```python theme={null}
h = sf.asarray(A, device="atreides")
w = sf.asarray(B, device="atreides")
y = h @ w
y.status         # SFStatus(saturated=0, div0=0, invalid=0, exact=True)
```

The `exact=True` comes from the range bound: the host computed that no partial sum of any output could exceed the maximum, so the launch was marked safe and no scan was needed. [The device path →](/arrays/device)

## Can it saturate?

A partial sum can pass the edge even if the final value would not, and once it saturates it stays there. The host's bound is conservative: it uses the L1 norms of the rows and columns, which is tighter than the spec's `K × max|A| × max|B|` and never looser. When the bound cannot prove safety, the probe samples real outputs; when it can prove every output saturates, the operation is refused before launch by default.

```python theme={null}
big = sf.asarray(np.full((4, 4), 0.9), dtype=sf.sf16, device="atreides")
big @ big
# SFPreflightWarning: pre-flight: matmul on sf16 is suspected to saturate on every element
#   (golden-vector probe: every probed output saturates); launching anyway
```

That was a warning rather than a refusal because the probe saw it, not the bound: a sample is a suspicion, never a proof. `sf.config(suspect="raise")` refuses on a suspicion; `suspect="verify"` replays the operation on the host and refuses only on proof.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.