@cm.reduce (guvectorize)¶
@cm.reduce maps to numba.guvectorize: a generalized ufunc that loops
over named core dimensions and writes into an output array Numba allocates
for you. It is the tool for row-wise and matrix kernels where the loop over
the data is the whole point but the layout is regular.
import numpy as np
import cmenpy as cm
@cm.reduce
def clip(row: "Array[float, 'n']",
low: "Scalar[float]",
up: "Scalar[float]",
out: "Array[float, 'n']"):
for i in range(row.shape[0]):
out[i] = min(max(row[i], low), up)
clip(np.array([[-5.0, 0.5, 5.0], [10.0, -10.0, 1.0]]), -1.0, 1.0)
# -> array([[-1. , 0.5, 1. ], [ 1. , -1. , 1. ]])
The last parameter is the output: it is allocated by Numba and never passed in by the caller.
Three forms¶
| Form | When |
|---|---|
| Typed annotations (preferred) | layout inferred from dimension labels; last parameter is the output |
cm.Contract |
you want explicit control over core types and layout |
Legacy (signatures, layout) |
porting existing guvectorize code |
1. Annotation-inferred (preferred)¶
Every parameter must be annotated with a fully typed spec, and the dimension labels define the layout:
@cm.reduce
def matmul(a: "Array[float, 'm,n']",
b: "Array[float, 'n,k']",
out: "Array[float, 'm,k']"):
for i in range(a.shape[0]):
for j in range(b.shape[1]):
acc = 0.0
for k in range(a.shape[1]):
acc += a[i, k] * b[k, j]
out[i, j] = acc
Here n is shared between a and b (a core dimension that must match),
while m and k are free — Numba loops over them and allocates the result.
You can add an explicit layout= alongside annotations:
@cm.reduce(layout="(n),()->(n)")
def scale(row: "Array[float, 1]", factor: "Scalar[float]",
out: "Array[float, 1]"):
...
2. cm.Contract¶
Contract[[inputs...], output] encodes the same information without
annotations, with shorthands (see specs):
@cm.reduce(cm.Contract[[float, float, ..., ..., ...], float])
def combine(a, b, s1, s2, s3, out):
for i in range(a.shape[0]):
out[i] = a[i] + b[i] * (s1 + s2 + s3)
# layout: (n),(n),(),(),()->(n)
- A plain dtype (
float) meansArray[dtype, "n"]— a 1-D array. ...means a scalar.Array[dtype]without a rank gets a private dimension (k0, …), shared with nothing.
3. Legacy (signatures, layout)¶
The classic Numba form, still accepted — and the way to declare multiple signatures:
@cm.reduce(
[
[cm.Array[float, 1], cm.Scalar[float], cm.Array[float, 1]],
[cm.Array[np.float32, 1], cm.Scalar[float], cm.Array[np.float32, 1]],
],
"(n),()->(n)",
)
def scale(row, factor, out):
for i in range(row.shape[0]):
out[i] = row[i] * factor
Layout rules¶
- Every parameter is fully typed — a bare
cm.Arrayraises. - Dimension labels are matched by name across all arguments, including the output.
- Every output dimension must appear in some input.
- The output cannot be a scalar — a gufunc always writes into an array.
- Scalars are annotated
cm.Scalar[dtype]and appear as()in the layout.
Options¶
| Option | Effect |
|---|---|
fastmath=True |
Numba fastmath |
parallel=True |
target="parallel" — parallel over the free dimensions |
| any other kwarg | forwarded verbatim (cache=True, …) |
For a model method, the flags can also come from the model (Compilation options).
Choosing between reduce, vectorize and precompile¶
| You are writing… | Use |
|---|---|
| scalar → scalar, broadcast | @cm.vectorize |
| a loop over core dimensions with a matching output layout | @cm.reduce |
| anything irregular: branches over rows, multiple outputs, early exits | @cm.precompile |
a whole-block objective (pop, ndim) -> (pop,) |
@cm.precompile + cm.vectorized_target |
Introspection¶
@cm.reduce kernels compile at decoration time when fully typed, so
signature errors surface at import. See
Troubleshooting for the common TypeErrors.