Skip to content

Tessero Unreleased

Unreleased, upcoming changes on main.

Added

  • Opt-in GPU-matching floating-point profile (csrc/src/fpenv.c, declared in tessero.h; feature fp_flush_profile). NVIDIA GPUs flush subnormal values to zero and expose no preserve mode, and NaN results can carry different bit patterns on different processors. tsr_fpenv_flush_enter / tsr_fpenv_flush_leave switch the calling thread to flush-to-zero for a scope and restore its previous state exactly; tsr_flush_subnormal_f32 and tsr_canon_nan_f32 normalise arrays in place with integer bit operations; tsr_nan_first_f32 finds the first NaN, with f64 twins of the three array helpers. The flush scope is for f32 work only: NVIDIA GPUs keep f64 subnormals, so f64 inside a scope would diverge. Additive only: no existing function, default, golden or self-check changes, and nothing runs unless called.

  • Reproducible-f32 transcendental primitives (tsr_repro_rsqrt_f32, tsr_repro_exp2_f32, tsr_repro_exp_f32, tsr_repro_erf_f32; csrc/src/repro_f32.c, declared in csrc/include/tessero.h). A new, original contribution, NOT part of the ported NumPy/SciPy architecture. NumPy/SciPy transcendentals are accuracy-first and defer to the system libm/vendor math, which is not bit-reproducible across hardware, drivers, or SIMD width. These are reproducible by construction - built only from correctly-rounded f32 operations, giving one defined f32 bit-value on every SIMD width, thread count, and CPU ISA (and, via a mirrored GPU shader, the GPU). Proven bit-identical across scalar/AVX2/AVX-512/NEON/SVE, 1 vs 4 threads, and x86 == ARM (csrc/repro/ARCH_MATRIX.md); four frozen goldens + canonical spec in csrc/repro/GOLDENS.md. Minimax coefficients are from Cephes (public domain) and Abramowitz & Stegun 7.1.26; the reproducible construction and the resulting bit-identity are the contribution. The ~1,845 parity-gated NumPy/SciPy functions are unaffected.

  • Reproducible recip and tanh primitives (tsr_repro_recip_f32, tsr_repro_tanh_f32), with goldens f7f9ec19 and 5b43f3d6 in csrc/repro/GOLDENS.md.

  • Reproducible composites (tsr_repro_{sigmoid,gelu_tanh,softmax,rmsnorm,layernorm}_f32): one definition of each composition, built on the primitives. Goldens confirmed on two independent hosts, each scalar == AVX2 == 4-thread.

  • Reproducible backward composites (gradients of gelu_tanh, erf, sigmoid, tanh, rmsnorm, layernorm, and the norm gamma gradient) in the single header csrc/include/tessero_repro_f32.h. Goldens recorded in csrc/repro/GOLDENS.md, approved on two hosts.

  • Talario's GPU kernels mirror these definitions and match them bit for bit on twelve NVIDIA GPU models across four generations, Ampere to Blackwell (attested at 1024 x 1024 by Talario); other GPUs are not yet attested.

  • NumPy/SciPy parity program (prompts/numpy-scipy-parity-program.md, ADR 0011, ADR 0012). Coverage is measured, not asserted. tools/parity/census.py lists every public symbol of NumPy 2.4.4 and SciPy 1.17.1, and tools/parity/report.py classifies each one from the code and the recorded fixture runs (docs/project/numpy-scipy-coverage.md).

    • Kernel function registry (ADR 0011). One C table of functions (ufunc, gufunc, distribution, routine, random) interpreted at run time by both bindings. The extension registers Tessero\Ext\Special, Stats, Np and Random\Generator from it at MINIT. The FFI façades Tessero\Special, Stats, Np and the Generator methods are generated from it (tools/parity/gen-facades.php).
    • scipy.special: every real-valued ufunc, from the code SciPy itself runs (xsf, Boost.Math and SciPy's glue, vendored with their licences; see THIRD_PARTY_NOTICES.md).
    • scipy.stats: the continuous and discrete distributions, on a port of rv_continuous/rv_discrete (csrc/cxx/stats_dist.*: the public-method semantics and the generic QUADPACK/brentq/bisection fallbacks, with SciPy's QUADPACK and brentq vendored). Also descriptive statistics, correlation and hypothesis tests. A new class Tessero\Stats\Distribution (and Tessero\Ext\Distribution) provides pdf...isf, stats, moment, entropy, interval and rvs.
    • NumPy statistics, sets and searching (Tessero\Np, Tessero\Ext\Np): median, quantile/percentile (all methods), nan-reductions, histogram/bincount/digitize, cov/corrcoef, unique and set operations, partition, lexsort, searchsorted, correlate/convolve.
    • NumPy array construction and manipulation (Tessero\Np, Tessero\Ext\Np, ADR 0013): the split and stack families, concatenate, tile/repeat/resize, flip/roll/rot90, axis moves, broadcasting, append/insert/delete, diag/tri*/*_indices, *_like, take/compress/nonzero, index conversions, meshgrid, select/choose, pad (all modes). Also the numeric routines: outer/inner/ kron/cross/tensordot, diff/gradient/trapezoid/unwrap, interp, logspace/geomspace/ vander, the poly* functions, the window functions, i0/sinc, nan_to_num, and the in-place writers put, putmask, place, copyto, fill_diagonal and put_along_axis. That is 118 functions, verified by NumPy fixtures on both backends. Integer results are exact above 2^53, including unique, the set operations and quantile's discrete methods, which now keep the input's integer or bool dtype as NumPy does.
    • numpy.random.Generator distributions from NumPy's own distributions.c on the kernel PCG64. Streams are bit-identical to NumPy on both backends. The extension gains a stateful Tessero\Ext\Random\Generator (defaultRng, integers, permutation, shuffle, getState/setState).
    • Fixtures generated by NumPy/SciPy for every registry function, replayed on both backends by tools/parity/run-tests.php. They are generated in a pinned numerical environment (tools/parity/gen-fixtures.sh, ADR 0012), and every file records that environment.
    • Deviation rules (docs/project/reference-deviations.md, with counts generated from the fixtures). These cover inputs where SciPy crashes or never returns, rows where SciPy's own value is undefined (found with AddressSanitizer or by history dependence), SciPy exceptions on invalid parameters where its contract says NaN, and a vectorised result that disagrees with SciPy's element-by-element one.
    • tools/parity/claims-check.php fails the build on coverage language or percentages that the generated pages do not back. CI runs the fixtures on both backends.
    • Coverage of real-world usage (docs/project/usage-weighted-coverage.md). tools/parity/usage.py counts how often the library code of 18 widely used scientific Python packages calls each NumPy and SciPy function, and report.py weights the verified coverage by those counts and lists the most-called functions that are not yet verified on both backends.
  • Beta tracking.

    • The NumPy/SciPy coverage inventory (docs/project/numpy-scipy-coverage.md, 169 rows) is generated from the public API by tools/api-dump.php and tools/gap_inventory.py, so it cannot claim a function that does not exist.
    • A gap analysis interprets the inventory.
    • An open-issue list: 50 issues with acceptance criteria, kept in .github/beta-issues.json. tools/beta-issues.php renders it and imports it into GitHub with gh.
    • A live-testing checklist for Windows, macOS, arm64/musl, FPM, Laravel and long soaks.
  • Backend parity (ADR 0010, docs/architecture/adr/0010-shared-kernels-and-vector-math.md):

    • The loop ufuncs moved from the extension into libtessero (csrc/src/ufunc.c, tsr_ufunc_*). The FFI package gains Tessero\Math, with the same 74 functions, signatures, out: semantics and results as Tessero\Ext\Math.
    • Memory mapping and .npy headers moved into libtessero (csrc/src/mmap.c: tsr_mmap_plan/open/flush/close, tsr_npy_header, tsr_npy_write_header).
    • The FFI package gains NDArray::memmap(), openMemmap(), load($path, $mmapMode), save(), flush(), isMemmap(), isReadonly(), isWritable(), filename(), and Tessero::info()['memory_mapped']. It checks open_basedir and refuses stream wrappers, like the extension.
    • The extension gains .npy I/O: NDArray::load($path, ?$mmapMode), $a->save($path) (byte-identical to numpy.save) and NDArray::openMemmap(), with Fortran-order files mapped with Fortran strides.
    • The extension reduces over a list of axes with keepdims: sum prod min max any all mean var std, and argmin/argmax with keepdims. Results are bit-identical to the FFI package.
    • Laravel: Tessero::memmap(), load() and openMemmap() on either backend.
  • Hardening:

    • libFuzzer targets for the slice parser, the .npy header parser, memory-map planning and the JSON writer (csrc/fuzz/, make -C csrc fuzz), with seed corpora and oracles.
    • A standalone mutation driver for machines without libFuzzer (make fuzz-standalone).
    • Property-based tests (tests/Unit/PropertyTest.php).
    • A cross-backend byte-parity test (tests/Ext/BackendParityTest.php).
    • A soak harness (tools/soak.php).
    • A nightly workflow: 30 min of fuzzing per target, 5.5 h soaks per backend, property tests ×50.
    • docs/operations/soak-testing.md, the memory-safety review, a Safeguards table in the memory model, and crash-debugging steps (core dumps, gdb, ASan builds of both bindings) in troubleshooting.
    • tests/Unit/MemorySafetyTest.php and ext/tests/014-memory-safety.phpt.
  • Performance:

    • Vectorised exp/log for float64 and float32 (csrc/src/vmath.h: branch-free, fdlibm algorithms, < 1 ulp). exp on 1M values went from 9.3 ms to 1.2 ms.
    • Real-input FFT (tsr_rfft, tsr_irfft: half-length transform); Fft::rfft and irfft use it.
    • sort uses an MSD + LSD hybrid radix sort with parallel buckets: 1M float64 went from 53 ms to 30 ms.
  • Bench cases log 1M and rfft 2^20 real. .npy fixtures written by NumPy (tests/fixtures/generate_npy.py).

  • Error code -9 TSR_EIO for operating-system I/O errors.

Earlier in this cycle

  • Universal functions Tessero\Ext\Math: 74 element-wise functions with NumPy broadcasting, type resolution and out: arrays.
    • Kernel family: bit-identical to the NDArray methods.
    • Loop family: cbrt exp2 trunc arcsinh arccosh arctanh degrees radians erf erfc gamma lgamma signbit fmax fmin copysign nextafter logaddexp logaddexp2 fmod.
    • Checked against NumPy/SciPy (64 cases).
  • Memory-mapped arrays NDArray::memmap($file, $mode, $shape, $dtype, $offset): the modes of numpy.memmap, shape inference, flush() / flush(sync: true), and the mapping released after the last view.
  • ADR-0009, guide pages Universal functions and Memory-mapped arrays.

Security

From the memory-safety review (docs/project/memory-safety-review.md):

  • Element-count overflow (both backends, and C reshape). A shape whose element count wraps around 64 bits could describe a small allocation as a huge array. Such shapes could come from fromBytes, from unserialize() of a cached or queued payload, from zeros/empty, or from reshape. Negative dimensions with a positive product were also accepted when unserializing. All sizes are now overflow-checked before allocating.
  • FFI Generator::shuffle on a read-only memory map wrote through PROT_READ pages and crashed the process. It now throws.
  • Preload header in the shared temp directory was written under a predictable name and reused if present. It is now created exclusively (0600) and deleted after loading.
  • Library version check. The FFI package refuses a libtessero binary from a different version instead of risking mismatched layouts.
  • Released memory is never passed to C. This covers flush() after a mapping is closed and LAPACK pointers during PHP's shutdown destructor pass.

Fixed

  • Registry routine errors now use Tessero's exception classes. On both backends, an index error from a registry routine is IndexError (IndexException in the extension), and a dtype or type error is DTypeError (DTypeException); these used to be PHP's ValueError. FFI messages name the method in camelCase, as the extension's do (Np::putAlongAxis). A float given where an integer is required is a type error, as in NumPy.

  • Np::unique, the set operations, quantile and 0-d integer results lost precision above 2^53. They are exact now.

  • Np::searchsorted accepted a sorted array that is not 1-D. It now raises, as NumPy does.

  • Np::bincount rejected a PHP list of floats. It now accepts one and converts it with int(), as NumPy does for a Python list.

  • Reference values depended on the generating machine. On AVX-512 CPUs, NumPy's float64 exp/log/power/expm1/log1p/sinh/arcsin/cbrt differ from the C library by an ulp on some inputs. All fixtures, including the older parity.json and ufunc.json, are now generated with those dispatch targets disabled (ADR 0012). Hand-set tolerances that only absorbed the difference were removed: random draws are compared exactly, and cov/corrcoef/correlate/convolve use the default tolerance.

  • Math::cbrt returns the C library's value, as NumPy does outside AVX-512 (cbrt(27) = 3.0000000000000004). The Newton correction that matched AVX-512 NumPy was removed.

  • Memory safety of the new numerical code (an independent verification and a sanitizer fuzz pass over all 539 registry functions, about 370 000 calls under AddressSanitizer and UBSan):

    • Negative trimming proportions read out of bounds, and could crash PHP (trim_mean, trimboth, trim1, and levene/fligner with center='trimmed').
    • lmoment with a huge order read out of bounds.
    • cumulative_sum/cumulative_prod with include_initial other than 0/1 returned uninitialised memory.
    • bincount with ±inf indexed memory wildly.
    • C++ exceptions (allocation failures, oversized requests) terminated the PHP process instead of raising. Every C++ entry point is now guarded, including callbacks from the vendored QUADPACK and brentq.
    • Parameters that size an array now raise MemoryError beyond the memory budget, or the machine's RAM when none is set.
    • NaN integer parameters, oversized axis and bin values, and integer overflow in fisher_exact and cramervonmises_2samp are now handled; every float→int conversion is defined.
    • tests/Unit/AdversarialInputTest.php pins every case on both backends.
  • Where SciPy itself crashes, Tessero returns NaN; where SciPy raises, Tessero raises. Mathieu functions for |q| > 2·10⁷ or m > 3·10⁸; hyp2f1 for |c| ≥ 2⁵³ on its recurrence; hyp1f1 with |b| ≥ 2⁵² on its integer-b path (these three crash SciPy). levy_stable with α = 1 far in the tail raises (SciPy: RecursionError). A Boost overflow raises (SciPy: OverflowError), for example in ncf.pdf and ncf.isf.

  • Integer inputs follow NumPy's int64 arithmetic. ptp, nansum, nanprod, nanmin, nanmax, the cumulative functions, partition, correlate and convolve are exact above 2⁵³ and wrap on overflow as NumPy does. correlate/convolve return NumPy's integer dtype.

  • NumPy input validation: bincount rejects float arrays and a non-integer minlength, ptp rejects booleans, and average checks weight shapes (_weights_are_valid).

  • Vendored SciPy code on inputs where SciPy itself fails (THIRD_PARTY_NOTICES.md). The spheroidal wave functions' work arrays are sized for their loops (SciPy overruns them). qstar no longer reads before its array for m = 0. cdflib's non-central t sum (nctdtridf, nctdtrinc) stops only when it provably cannot terminate, instead of looping forever.

  • sort()/argsort() ignored kernel errors (both bindings). When the memory budget ran out while the kernel allocated sort scratch space, the result came back unsorted (or with zeroed indices) instead of throwing. Both bindings now check every return code and throw MemoryError or MemoryException. The FFI sparse constructors and products (tsr_dense_to_csr, tsr_csr_matvec, tsr_csr_matmat) are now checked too. Regression tests are in MemorySafetyTest and 014-memory-safety.phpt (finding F-9 in the memory-safety review).

  • Slice parser, found by fuzzing:

    • A blank item ("0, ,1") read past its comma, an out-of-bounds read in C. Blanks are now bounded, and "1 , 2" (blanks around commas) parses as it should; it was wrongly rejected before.
    • Steps near 2^62 overflowed the element-count and stride arithmetic, in C and in the FFI package's PHP slicer.
  • FFI NDArray binary and unary operations with out: wrote straight into out even when out partially overlapped an input (a shifted view of the same buffer), so they read already-overwritten values. They now go through a temporary, as the extension does.

  • clone of an FFI array now copies the data (it shared the buffer), matching the extension. Writing a clone no longer changes the original or a memory map.

  • Multi-axis mean/var/std in the FFI package now merge the axes in ascending order whatever order they are listed in, so results do not depend on the listing order.

  • mean, var and std compute exactly as NumPy does (two-pass pairwise) on both backends; cbrt corrects glibc's 1-ulp error on exact cubes.

Verification record (reference machine: Linux x86-64, 2 cores, AVX-512, PHP 8.4.21, GCC 13, NumPy 2.4.4)

  • C: 270 checks, optimised, ASan + UBSan, Clang without OpenMP.
  • Fuzzing: about 13 million mutated inputs across the four targets under ASan + UBSan (standalone driver), clean after the two slice-parser fixes. libFuzzer itself could not be installed here (package downloads blocked); CI runs it.
  • FFI suites: 706 tests in all three JIT modes (JIT off, function, tracing).
  • Extension: 14 phpt files and 505 parity/ufunc cases, also under ASan + UBSan. Unit, extension and backend-parity suites together: 1 214 pass, 26 FFI-only skipped. Preload end-to-end passes.
  • Laravel bridge: 5 tests on both backends. Documentation examples: 59 checks with both backends.
  • Soak (tools/soak.php): 30 minutes per backend, running concurrently.
    • Extension, JIT off: 785 608 iterations.
    • FFI package, tracing JIT: 475 522 iterations.
    • Result: native and mapped bytes back at 0 after every iteration; heap slope 0; RSS slope +13.7 / -5.2 KiB/min (thresholds 16 / 64); no descriptor growth. PASS.
    • An earlier 11-minute run of both backends was also clean.
  • Not run: the Windows memory-map path (csrc/src/mmap.c, Win32 API, never compiled), the CI matrix, 24-72 h soaks.

Edit this page on GitHub