rindow/rindow-math-matrix
Rindow Math Matrix is a PHP library for scientific matrix operations, providing N-dimensional arrays, broadcasting, BLAS, linear algebra, and machine learning functions. It integrates C/C++ libraries like OpenBLAS and CLBlast via FFI and supports GPU acceleration through OpenCL.
rindow/rindow-math-matrix is essentially "NumPy for PHP": an N-dimensional array object plus broadcasting, BLAS bindings, linear algebra, and random number generation. It is the numerical foundation that rindow/rindow-neuralnetworks sits on, so it is proven in a real ML stack even though the package itself has only 13 stars and effectively no independent dependents.
Minimum setup:
composer require rindow/rindow-math-matrix
composer require rindow/rindow-math-matrix-matlibffi
The second line is not optional if you care about speed. Without a plugin driver the package runs in pure-PHP "Basic" mode, which is correct but roughly an order of magnitude slower. Then install the prebuilt binaries (Rindow-Matlib / OpenBLAS / CLBlast) linked from the rindow-math-matrix-matlibffi repo. PHP 8.1–8.4 are supported; 2.2.0 adds PHP 8.5 test coverage. Legacy PHP 7.x/8.0 users must stay on the 1.1.x line.
Verify the driver actually loaded before writing any code:
vendor/bin/rindow-math-matrix -v
The -v flag was added in 2.0.4 specifically to show the boot log when the driver fails to load and the service level silently drops to Basic. Bookmark that command — it is your first debugging step for every performance complaint.
Where to look first: the docs live at rindow.github.io/mathematics, not in the repository, so do not judge the package by its README. Inside the code, start at Rindow\Math\Matrix\MatrixOperator (the developer-facing facade) and drop into Rindow\Math\Matrix\LinearAlgebra only when you need buffer-level control. The release notes in release/ are the de-facto API changelog and are worth skimming for whichever functions you plan to use.
First real use case: replacing nested PHP loops for batch scoring or feature transforms. The canonical sample is:
use Rindow\Math\Matrix\MatrixOperator;
$mo = new MatrixOperator();
$a = $mo->array([[1,2],[3,4]]);
$b = $mo->array([[2,3],[4,5]]);
echo $mo->toString($mo->cross($a,$b), indent:true)."\n";
That is enough to prove the install; a batch dot-product over 10k rows is enough to prove the value.
Two layers, pick deliberately. MatrixOperator is the ergonomic wrapper (array(), toString(), cross(), zeros()) and is what you want 90% of the time. LinearAlgebra is the low-level service operating on NDArray objects with explicit dtypes and output buffers. Mixing them casually is the most common source of confusion — decide per class whether you are in "friendly" or "buffer" land.
Pin your dtypes explicitly. Since 2.0, array() no longer auto-detects type; the default is float32. Any code doing money-ish, scientific, or integer-index work must pass dtype: NDArray::float64 or int32. Treat an unqualified array() call as a code smell in review.
Broadcast instead of looping. The package supports NumPy-style broadcasting; a row-wise normalization should be $x / $mean on a [N,1] operand, not a foreach. This is where the speedup and the readability both come from.
Reuse output buffers in hot loops. BLAS-style functions accept an output buffer (gemm, matmul). In an inference or training loop, allocate once outside the loop and pass the same NDArray in. This avoids the per-iteration allocation that otherwise dominates PHP-level runtime.
Reproducibility. Use setSeed() (added in 2.2.0) plus randInt()/randomUniform()/randomNormal()/randomCategorical(). Seed in your service constructor, not in a test helper, so training runs are reproducible by config rather than by accident.
Shape gymnastics. expandDims/squeeze for rank fixes, gatherb/masking/where for selection, einsum for arbitrary contractions and einsum4p1 when you specifically have 4+1 dimensional attention-shaped tensors and need the fast path. slice/stack/concat/split cover the recurrent-network style plumbing.
GPU when the laptop has no NVIDIA. laAccelerated('clblast', ['deviceType'=>'GPU']) routes through OpenCL and works on Intel/AMD iGPUs. Wrap driver selection behind a config value (math.driver) so CI and local dev can force Basic mode and stay deterministic.
Laravel integration. Bind MatrixOperator in a service provider, but instantiate per queued job rather than sharing a singleton across long-lived workers — the object carries a LinearAlgebra instance with driver state and RNG state. Put heavy computation in jobs, never in the request cycle; a 2000×2000 gemm is still a request-killer even accelerated.
Persistence caution. The serialization format changed in 2.0 (class names replaced by keywords). Do not let cached/serialized NDArrays cross a major-version upgrade; re-derive them or store raw arrays instead.
The silent downgrade to Basic mode is the number one trap. Nothing throws — you just get slow code. Every deployment should assert the service level at boot, and vendor/bin/rindow-math-matrix -v is the diagnostic. Also check ffi.enable in php.ini, since 2.0 made FFI the default access path and a disabled FFI quietly costs you everything.
macOS is degraded. Because of an issue in rindow-math-buffer-ffi, macOS currently only supports PHP-only Basic mode. If your team is on MacBooks, plan for Linux or WSL for anything performance-sensitive, and expect your Mac devs to see different timings than CI.
Fractional indexing semantics changed. Version 1 treats [0,4] as elements 0 through 4; version 2 treats [0,5] as the equivalent. Legacy NDArrayPhp::$rangeStyle / RANGE_STYLE_1 exist, but the clean fix is to use the explicit R(0,5) helper so intent is unambiguous regardless of global state.
Device selection options are mutually exclusive. 'device' => '0,1' means platform 0, device 1; 'deviceType' => 'GPU' means the first GPU. Passing both is invalid (clarified in 1.2.4 after a typo in the original option names).
gather()/reduceGather() warn rather than throw since 1.2.2, which was a deliberate workaround but means malformed indices can produce garbage silently. Validate index ranges yourself before calling.
Complex numbers are BLAS-only. You cannot rely on complex support across the general function surface, and as a consequence amax/amin return the absolute value when complex types are in play. Do not use them as a drop-in for "find the max coefficient".
Preview functions can vanish. topk and gathernd are explicitly marked preview; keep them behind a thin wrapper so a minor-version bump cannot ripple through your codebase.
Zero-dimensional arrays as scalars in offsetSet only works from 2.2.0. If you support older pins, guard that pattern.
Debug by differential testing. For anything non-trivial, generate the expected result in NumPy once, commit it as a fixture, and assert with isclose(). toString()/shapeToString()/dtypeToString() (2.2.0) make failure messages readable; isfinite/isinf/isnan catch the NaN propagation that PHP's loose typing otherwise hides.
Abandoned old-extension path. The rindow_openblas/rindow_opencl/rindow_clblast PHP extensions still exist but are explicitly "not actively updated." Build new work on the FFI plugin; treat the extension as a fallback for hosts where FFI is unavailable.
Vendor risk is real. BSD-3-Clause is friendly, but the bus factor is roughly one maintainer, the docs live off-repo, and the API has broken compatibility across 1.x → 2.x (default dtype, range syntax, serialization). Isolate the library behind a small internal interface — a MatrixOps contract with a handful of methods — so a future swap to the extension driver, a different library, or an out-of-process Python service is a contained change rather than a rewrite.
How can I help you explore Laravel packages today?