Metadata-Version: 2.4
Name: simdpunk
Version: 0.1.0.dev1
Summary: Python access to simdpunk, a header-only quantized-tensor kernel library
License-Expression: AGPL-3.0-or-later
License-File: LICENSE
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: C++
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: POSIX :: Linux
Project-URL: Homepage, https://codeberg.org/simdpunk/simdpunk
Project-URL: Source, https://codeberg.org/simdpunk/simdpunk
Project-URL: Issues, https://codeberg.org/simdpunk/simdpunk/issues
Requires-Python: >=3.9
Requires-Dist: numpy>=1.21
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Description-Content-Type: text/markdown

# simdpunk from Python — a minimal ctypes wrapper

One compiled C ABI and a thin Python layer: numpy is the only runtime dependency.

```sh
pip install simdpunk                               # a manylinux wheel: x86_64, and aarch64 where the release has one
python -m simdpunk.mnist --data DIR                # the MNIST example; DIR holds the four MNIST files (see below)
```

| file | what it is |
|---|---|
| `simdpunk_c.cpp` | a C ABI over the registry: the registry as text, each operand's extent, one predicate (`arm_accepts_arrangement`), and **one call** — every arm is `void fn(const call_desc&, void* const*)`, so one entry point reaches every row of the registry |
| `CMakeLists.txt` | builds it as `_simdpunk.so` against the library's own INTERFACE target; OpenMP for the `openmp` executor, no `-march` (the ISA arms choose at run time) |
| `simdpunk/__init__.py` | the Python side: `call`, `describe`, `accepts`, `arm_of`, `REGISTRY` |
| `simdpunk/mnist.py` | the MNIST example on it — `mnist_train --path plan`'s step, node by node |
| `tests/` | the smoke tests every wheel is tested with |
| `ci/build-wheel.sh` | the CI's one step, inside a manylinux image: sdist, wheel from it, `auditwheel repair`, tests on the installed wheel |
| `../pyproject.toml` | the package: scikit-build-core drives the CMake project; the wheel is `py3-none-<platform>` (no Python.h, so one build serves every Python 3); `[tool.cibuildwheel]` is the GitHub mirror's |

**From a checkout:** `pip install .` builds and installs it; `python3 build.py` builds
`simdpunk/_simdpunk.so` in place (the same CMake project) for `PYTHONPATH=python`.
Either needs CMake ≥ 3.20 and a C++23 compiler (built and tested here with g++ 15.2).

## The API

```python
import numpy as np, simdpunk as sp

sp.describe("gemm.fp32_x_fp32.nt")     # operands by slot, shape parameters, arms (* = default)
C = np.empty((M, N), np.float32)
sp.call("gemm.fp32_x_fp32.nt", {"in.a": A, "in.b": B, "out.c": C}, m=M, n=N, k=K)
```

- **Operands by slot name, shape by parameter name** — the family's own vocabulary
  (`sp.describe`). The shape is a prefix of the declared list and the rest derive, as in a direct
  C++ call. A parameter skipped before the last one given is a `TypeError`: a spelled 0 is a zero
  stride, not "derive it".
- **Checked before any kernel runs:** every operand present, fp32/u8/int32 operands of the right
  dtype, C-contiguous, and every dense operand at least the extent the family declares at this
  shape (`extents.h`). A short buffer is a `ValueError`, not a heap overrun.
- `arm=` names an arm (default: the family's `default_arm`). `arrangement={"in.codes": ("rowmajor", 0)}`
  states an arranged operand's layout — the u8 GEMM refuses an unstated one.
  `recipe=` is the emission (`absmean`, `absmax_kbit`); `exec=` is `serial` or `openmp`.
- A Python number passed for an input slot is a one-element array of the slot's format. An operand
  updated in place (Adam's `m`, `v`, `w`) is the same array under its `in.` and `out.` slots.
- A precondition only the arm checks is still the arm's FATAL, and it ends the process, as it
  would in C++.

## What `mnist.py` does

Each `sp.call` is one node of the training step (`trace_step`) or the evaluation (`trace_eval`) of
the library's C++ MNIST example, on the arm that node runs: 784 → hidden → 10 (the head padded to 64
columns), ternary weights emitted from fp32 masters at the head of every step, layer 1 an exact
uint8 × ternary GEMM on the pixel bytes, layer 2 an fp32 GEMM on fake-quantised int8 rows, softmax
cross-entropy, Adam. The initial weights and the shuffle come from numpy, not `std::mt19937`, and
evaluation reads the rowmajor codes where the example's eval plan reads the pack, so a run follows
that example's steps but not its bits.

`DIR` holds the four uncompressed MNIST files: `train-images-idx3-ubyte`, `train-labels-idx1-ubyte`,
`t10k-images-idx3-ubyte` and `t10k-labels-idx1-ubyte`. Nothing is downloaded for you.

- **`--check`** holds every GEMM of the first step to numpy float64 on the same inputs (the run
  fails beyond 1e-5 of the largest value); the wheel's smoke tests run it.
- **Accuracy**: the defaults (3 epochs, 256 hidden units) reach about 97.5 % test accuracy on MNIST,
  measured with this release's scalar arms.
- `--model FILE` starts from a checkpoint (format 2) that the C++ `mnist_train` wrote.

## What it does not wrap

The `l1` planner — graph, lowering, `validate`, `verify`, the arena and `staged_plan`. Here Python
calls the arms one at a time, the way the example's direct path does, and a plan's checks are not
made. Also not wrapped: the program tier, the `state::qparam` facets, the pack builders (host code,
not families) and the fixture reader. Each would be one more small function in `simdpunk_c.cpp`.

## License

`AGPL-3.0-or-later` — the [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.html),
version 3 or any later version; the sdist and every wheel carry the text (`LICENSE`), and the metadata says
so (`License-Expression`, PEP 639).

Linux only: the wheels are glibc (musllinux is skipped), and macOS and Windows are not built. The
library's ISA arms are GCC/Clang target attributes, and the `openmp` executor wants libgomp.
