diff --git a/.githooks/prepare-commit-msg b/.githooks/prepare-commit-msg
deleted file mode 100755
index fa51f80df..000000000
--- a/.githooks/prepare-commit-msg
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/sh
-grep -qs "^Signed-off-by: " "$1" || {
- name=$(git config user.name)
- email=$(git config user.email)
-
- if [ -n "$name" ] && [ -n "$email" ]; then
- echo "" >> "$1"
- echo "Signed-off-by: $name <$email>" >> "$1"
- fi
-}
\ No newline at end of file
diff --git a/.github/actions/stamp-version/action.yml b/.github/actions/stamp-version/action.yml
new file mode 100644
index 000000000..2a40301c2
--- /dev/null
+++ b/.github/actions/stamp-version/action.yml
@@ -0,0 +1,24 @@
+name: Stamp version
+description: Resolve the release version and write python/cactus/_version.py
+runs:
+ using: composite
+ steps:
+ - shell: bash
+ env:
+ TAG: ${{ github.event.release.tag_name }}
+ INPUT_VERSION: ${{ github.event.inputs.version }}
+ run: |
+ if [ -n "$TAG" ]; then
+ raw="${TAG#v}"
+ elif [ -n "$INPUT_VERSION" ]; then
+ raw="${INPUT_VERSION#v}"
+ else
+ raw=$(sed 's/^v//' CACTUS_VERSION)
+ fi
+ version=$(RAW="$raw" python3 -c 'import os; r=os.environ["RAW"]; print(r + ".0" if r.count(".") == 1 else r)')
+ if [ -z "$version" ]; then
+ echo "::error::No version source available (TAG, INPUT_VERSION, CACTUS_VERSION all empty)"
+ exit 1
+ fi
+ printf '__version__ = "%s"\n' "$version" > python/cactus/_version.py
+ echo "Stamped version: $version"
diff --git a/.github/assemble-docs.sh b/.github/assemble-docs.sh
new file mode 100755
index 000000000..5c329452d
--- /dev/null
+++ b/.github/assemble-docs.sh
@@ -0,0 +1,223 @@
+#!/usr/bin/env bash
+# Assembles site_docs/ from scattered source files and rewrites internal links.
+# Used by the release.yml workflow.
+set -euo pipefail
+
+DOCS_VERSION="${1:-}"
+
+if sed --version >/dev/null 2>&1; then
+ sedi() { sed -i "$@"; }
+else
+ sedi() { sed -i '' "$@"; }
+fi
+
+rm -rf site_docs
+mkdir -p site_docs/docs site_docs/python site_docs/apple site_docs/android \
+ site_docs/flutter site_docs/rust site_docs/swift site_docs/kotlin \
+ site_docs/react-native site_docs/blog site_docs/assets
+
+cp -r assets/* site_docs/assets/
+
+echo "docs.cactuscompute.com" > site_docs/CNAME
+
+mkdir -p site_docs/stylesheets site_docs/javascripts
+cp .github/docs-overrides/stylesheets/custom.css site_docs/stylesheets/custom.css
+cp .github/docs-overrides/javascripts/mathjax.js site_docs/javascripts/mathjax.js
+
+[ -f CONTRIBUTING.md ] && cp CONTRIBUTING.md site_docs/CONTRIBUTING.md
+[ -f DCO.md ] && cp DCO.md site_docs/DCO.md
+
+cp docs/*.md site_docs/docs/
+
+cp python/README.md site_docs/python/README.md
+cp apple/README.md site_docs/apple/README.md
+cp android/README.md site_docs/android/README.md
+cp bindings/flutter/README.md site_docs/flutter/README.md
+cp bindings/swift/README.md site_docs/swift/README.md
+cp bindings/kotlin/README.md site_docs/kotlin/README.md
+cp bindings/react-native/README.md site_docs/react-native/README.md
+cp bindings/rust/README.md site_docs/rust/README.md
+
+if [ -d blog ] && ls blog/*.md >/dev/null 2>&1; then
+ cp blog/*.md site_docs/blog/
+fi
+
+{
+ echo '---'
+ echo 'title: "Cactus"'
+ echo 'description: "Energy-efficient AI inference engine for phones, wearables, Macs, and ARM devices."'
+ echo '---'
+ echo ''
+ cat README.md
+} > site_docs/index.md
+
+sedi 's/^# Cactus$//' site_docs/index.md
+
+sedi 's|(/docs/cactus_engine\.md)|(docs/cactus_engine.md)|g' site_docs/index.md
+sedi 's|(/docs/cactus_graph\.md)|(docs/cactus_graph.md)|g' site_docs/index.md
+sedi 's|(/docs/cactus_index\.md)|(docs/cactus_index.md)|g' site_docs/index.md
+sedi 's|(/docs/cactus_kernels\.md)|(docs/cactus_kernels.md)|g' site_docs/index.md
+sedi 's|(/docs/cactus_quants\.md)|(docs/cactus_quants.md)|g' site_docs/index.md
+sedi 's|(/docs/cactus_transpiler\.md)|(docs/cactus_transpiler.md)|g' site_docs/index.md
+sedi 's|(/docs/cactus_hybrid\.md)|(docs/cactus_hybrid.md)|g' site_docs/index.md
+sedi 's|(/docs/finetuning\.md)|(docs/finetuning.md)|g' site_docs/index.md
+sedi 's|(/docs/compatibility\.md)|(docs/compatibility.md)|g' site_docs/index.md
+sedi 's|(/docs/quickstart\.md)|(docs/quickstart.md)|g' site_docs/index.md
+sedi 's|(/docs/choose-bindings\.md)|(docs/choose-bindings.md)|g' site_docs/index.md
+sedi 's|(/CONTRIBUTING\.md)|(CONTRIBUTING.md)|g' site_docs/index.md
+sedi 's|(/bindings/swift/)|(swift/README.md)|g' site_docs/index.md
+sedi 's|(/bindings/kotlin/)|(kotlin/README.md)|g' site_docs/index.md
+sedi 's|(/bindings/python/)|(python/README.md)|g' site_docs/index.md
+sedi 's|(/bindings/react-native/)|(react-native/README.md)|g' site_docs/index.md
+sedi 's|(/bindings/flutter/)|(flutter/README.md)|g' site_docs/index.md
+sedi 's|(/bindings/rust/)|(rust/README.md)|g' site_docs/index.md
+sedi 's|(/python/)|(python/README.md)|g' site_docs/index.md
+sedi 's|(/apple/)|(apple/README.md)|g' site_docs/index.md
+sedi 's|(/android/)|(android/README.md)|g' site_docs/index.md
+sedi 's|(/blog/hybrid_transcription\.md)|(blog/hybrid_transcription.md)|g' site_docs/index.md
+sedi 's|(/blog/lfm2_24b_a2b\.md)|(blog/lfm2_24b_a2b.md)|g' site_docs/index.md
+sedi 's|(/blog/parakeet\.md)|(blog/parakeet.md)|g' site_docs/index.md
+sedi 's|(/blog/lfm2\.5_350m\.md)|(blog/lfm2.5_350m.md)|g' site_docs/index.md
+sedi 's|(/blog/gemma4\.md)|(blog/gemma4.md)|g' site_docs/index.md
+sedi 's|(/blog/turboquant-h\.md)|(blog/turboquant-h.md)|g' site_docs/index.md
+
+for f in site_docs/docs/*.md; do
+ sedi 's|(/docs/cactus_engine\.md)|(cactus_engine.md)|g' "$f"
+ sedi 's|(/docs/cactus_graph\.md)|(cactus_graph.md)|g' "$f"
+ sedi 's|(/docs/cactus_index\.md)|(cactus_index.md)|g' "$f"
+ sedi 's|(/docs/cactus_kernels\.md)|(cactus_kernels.md)|g' "$f"
+ sedi 's|(/docs/cactus_quants\.md)|(cactus_quants.md)|g' "$f"
+ sedi 's|(/docs/cactus_transpiler\.md)|(cactus_transpiler.md)|g' "$f"
+ sedi 's|(/docs/cactus_hybrid\.md)|(cactus_hybrid.md)|g' "$f"
+ sedi 's|(/docs/finetuning\.md)|(finetuning.md)|g' "$f"
+ sedi 's|(/docs/compatibility\.md)|(compatibility.md)|g' "$f"
+ sedi 's|(/docs/quickstart\.md)|(quickstart.md)|g' "$f"
+ sedi 's|(/docs/choose-bindings\.md)|(choose-bindings.md)|g' "$f"
+ sedi 's|(/docs/index\.md)|(../index.md)|g' "$f"
+ sedi 's|(/blog/hybrid_transcription\.md)|(../blog/hybrid_transcription.md)|g' "$f"
+ sedi 's|(/blog/lfm2_24b_a2b\.md)|(../blog/lfm2_24b_a2b.md)|g' "$f"
+ sedi 's|(/blog/parakeet\.md)|(../blog/parakeet.md)|g' "$f"
+ sedi 's|(/blog/lfm2\.5_350m\.md)|(../blog/lfm2.5_350m.md)|g' "$f"
+ sedi 's|(/blog/gemma4\.md)|(../blog/gemma4.md)|g' "$f"
+ sedi 's|(/blog/turboquant-h\.md)|(../blog/turboquant-h.md)|g' "$f"
+ sedi 's|(/CONTRIBUTING\.md)|(../CONTRIBUTING.md)|g' "$f"
+ sedi 's|(/bindings/swift/)|(../swift/README.md)|g' "$f"
+ sedi 's|(/bindings/kotlin/)|(../kotlin/README.md)|g' "$f"
+ sedi 's|(/bindings/python/)|(../python/README.md)|g' "$f"
+ sedi 's|(/bindings/react-native/)|(../react-native/README.md)|g' "$f"
+ sedi 's|(/bindings/flutter/)|(../flutter/README.md)|g' "$f"
+ sedi 's|(/bindings/rust/)|(../rust/README.md)|g' "$f"
+ sedi 's|(/python/)|(../python/README.md)|g' "$f"
+ sedi 's|(/apple/)|(../apple/README.md)|g' "$f"
+ sedi 's|(/android/)|(../android/README.md)|g' "$f"
+done
+
+for f in site_docs/python/README.md site_docs/apple/README.md site_docs/android/README.md site_docs/flutter/README.md site_docs/swift/README.md site_docs/kotlin/README.md site_docs/react-native/README.md site_docs/rust/README.md; do
+ sedi 's|(/docs/cactus_engine\.md)|(../docs/cactus_engine.md)|g' "$f"
+ sedi 's|(/docs/cactus_graph\.md)|(../docs/cactus_graph.md)|g' "$f"
+ sedi 's|(/docs/cactus_index\.md)|(../docs/cactus_index.md)|g' "$f"
+ sedi 's|(/docs/cactus_kernels\.md)|(../docs/cactus_kernels.md)|g' "$f"
+ sedi 's|(/docs/cactus_quants\.md)|(../docs/cactus_quants.md)|g' "$f"
+ sedi 's|(/docs/cactus_transpiler\.md)|(../docs/cactus_transpiler.md)|g' "$f"
+ sedi 's|(/docs/cactus_hybrid\.md)|(../docs/cactus_hybrid.md)|g' "$f"
+ sedi 's|(/docs/finetuning\.md)|(../docs/finetuning.md)|g' "$f"
+ sedi 's|(/docs/compatibility\.md)|(../docs/compatibility.md)|g' "$f"
+ sedi 's|(/docs/quickstart\.md)|(../docs/quickstart.md)|g' "$f"
+ sedi 's|(/docs/choose-bindings\.md)|(../docs/choose-bindings.md)|g' "$f"
+ sedi 's|(/docs/index\.md)|(../index.md)|g' "$f"
+ sedi 's|(/blog/hybrid_transcription\.md)|(../blog/hybrid_transcription.md)|g' "$f"
+ sedi 's|(/blog/lfm2_24b_a2b\.md)|(../blog/lfm2_24b_a2b.md)|g' "$f"
+ sedi 's|(/blog/parakeet\.md)|(../blog/parakeet.md)|g' "$f"
+ sedi 's|(/blog/lfm2\.5_350m\.md)|(../blog/lfm2.5_350m.md)|g' "$f"
+ sedi 's|(/blog/gemma4\.md)|(../blog/gemma4.md)|g' "$f"
+ sedi 's|(/blog/turboquant-h\.md)|(../blog/turboquant-h.md)|g' "$f"
+ sedi 's|(/CONTRIBUTING\.md)|(../CONTRIBUTING.md)|g' "$f"
+ sedi 's|(/bindings/swift/)|(../swift/README.md)|g' "$f"
+ sedi 's|(/bindings/kotlin/)|(../kotlin/README.md)|g' "$f"
+ sedi 's|(/bindings/python/)|(../python/README.md)|g' "$f"
+ sedi 's|(/bindings/react-native/)|(../react-native/README.md)|g' "$f"
+ sedi 's|(/bindings/flutter/)|(../flutter/README.md)|g' "$f"
+ sedi 's|(/bindings/rust/)|(../rust/README.md)|g' "$f"
+ sedi 's|(/python/)|(../python/README.md)|g' "$f"
+ sedi 's|(/apple/)|(../apple/README.md)|g' "$f"
+ sedi 's|(/android/)|(../android/README.md)|g' "$f"
+ sedi 's|(\.\.\/README\.md)|(../index.md)|g' "$f"
+done
+
+if ls site_docs/blog/*.md >/dev/null 2>&1; then
+ for f in site_docs/blog/*.md; do
+ sedi 's|(/docs/cactus_engine\.md)|(../docs/cactus_engine.md)|g' "$f"
+ sedi 's|(/docs/cactus_graph\.md)|(../docs/cactus_graph.md)|g' "$f"
+ sedi 's|(/docs/cactus_index\.md)|(../docs/cactus_index.md)|g' "$f"
+ sedi 's|(/docs/cactus_kernels\.md)|(../docs/cactus_kernels.md)|g' "$f"
+ sedi 's|(/docs/cactus_quants\.md)|(../docs/cactus_quants.md)|g' "$f"
+ sedi 's|(/docs/cactus_transpiler\.md)|(../docs/cactus_transpiler.md)|g' "$f"
+ sedi 's|(/docs/cactus_hybrid\.md)|(../docs/cactus_hybrid.md)|g' "$f"
+ sedi 's|(/docs/finetuning\.md)|(../docs/finetuning.md)|g' "$f"
+ sedi 's|(/docs/compatibility\.md)|(../docs/compatibility.md)|g' "$f"
+ sedi 's|(/docs/quickstart\.md)|(../docs/quickstart.md)|g' "$f"
+ sedi 's|(/docs/choose-bindings\.md)|(../docs/choose-bindings.md)|g' "$f"
+ sedi 's|(/docs/index\.md)|(../index.md)|g' "$f"
+ sedi 's|(/CONTRIBUTING\.md)|(../CONTRIBUTING.md)|g' "$f"
+ sedi 's|(/bindings/swift/)|(../swift/README.md)|g' "$f"
+ sedi 's|(/bindings/kotlin/)|(../kotlin/README.md)|g' "$f"
+ sedi 's|(/bindings/python/)|(../python/README.md)|g' "$f"
+ sedi 's|(/bindings/react-native/)|(../react-native/README.md)|g' "$f"
+ sedi 's|(/bindings/flutter/)|(../flutter/README.md)|g' "$f"
+ sedi 's|(/bindings/rust/)|(../rust/README.md)|g' "$f"
+ sedi 's|(/python/)|(../python/README.md)|g' "$f"
+ sedi 's|(/apple/)|(../apple/README.md)|g' "$f"
+ sedi 's|(/android/)|(../android/README.md)|g' "$f"
+ sedi 's|(/blog/hybrid_transcription\.md)|(hybrid_transcription.md)|g' "$f"
+ sedi 's|(/blog/lfm2_24b_a2b\.md)|(lfm2_24b_a2b.md)|g' "$f"
+ sedi 's|(/blog/parakeet\.md)|(parakeet.md)|g' "$f"
+ sedi 's|(/blog/lfm2\.5_350m\.md)|(lfm2.5_350m.md)|g' "$f"
+ sedi 's|(/blog/gemma4\.md)|(gemma4.md)|g' "$f"
+ sedi 's|(/blog/turboquant-h\.md)|(turboquant-h.md)|g' "$f"
+ done
+fi
+
+if [ -f site_docs/CONTRIBUTING.md ]; then
+ sedi 's|(/docs/cactus_engine\.md)|(docs/cactus_engine.md)|g' site_docs/CONTRIBUTING.md
+ sedi 's|(/docs/cactus_graph\.md)|(docs/cactus_graph.md)|g' site_docs/CONTRIBUTING.md
+ sedi 's|(/docs/cactus_index\.md)|(docs/cactus_index.md)|g' site_docs/CONTRIBUTING.md
+ sedi 's|(/docs/cactus_kernels\.md)|(docs/cactus_kernels.md)|g' site_docs/CONTRIBUTING.md
+ sedi 's|(/docs/cactus_quants\.md)|(docs/cactus_quants.md)|g' site_docs/CONTRIBUTING.md
+ sedi 's|(/docs/cactus_transpiler\.md)|(docs/cactus_transpiler.md)|g' site_docs/CONTRIBUTING.md
+ sedi 's|(/docs/cactus_hybrid\.md)|(docs/cactus_hybrid.md)|g' site_docs/CONTRIBUTING.md
+ sedi 's|(/docs/index\.md)|(index.md)|g' site_docs/CONTRIBUTING.md
+fi
+
+if [ -n "$DOCS_VERSION" ]; then
+ {
+ echo "!!! note \"Version ${DOCS_VERSION}\""
+ echo " You're viewing docs for **${DOCS_VERSION}**. If you are cloning the repository, make sure to check out this release: \`git checkout ${DOCS_VERSION}\`"
+ echo ""
+ cat site_docs/docs/quickstart.md
+ } > site_docs/docs/quickstart.tmp && mv site_docs/docs/quickstart.tmp site_docs/docs/quickstart.md
+fi
+
+for nav_path in \
+ "python/README.md" \
+ "swift/README.md" \
+ "kotlin/README.md" \
+ "flutter/README.md" \
+ "react-native/README.md" \
+ "rust/README.md" \
+ "blog/README.md" \
+ "blog/hybrid_transcription.md" \
+ "blog/lfm2_24b_a2b.md" \
+ "blog/parakeet.md" \
+ "blog/lfm2.5_350m.md" \
+ "blog/gemma4.md" \
+ "blog/turboquant-h.md" \
+ "CONTRIBUTING.md" \
+ "docs/compatibility.md"; do
+ if [ ! -f "site_docs/$nav_path" ]; then
+ grep -vF "$nav_path" mkdocs.yml > mkdocs.yml.tmp && mv mkdocs.yml.tmp mkdocs.yml
+ fi
+done
+
+if ! ls site_docs/blog/*.md >/dev/null 2>&1; then
+ grep -v "^ - Blog:" mkdocs.yml > mkdocs.yml.tmp && mv mkdocs.yml.tmp mkdocs.yml
+fi
diff --git a/.github/docs-overrides/javascripts/mathjax.js b/.github/docs-overrides/javascripts/mathjax.js
new file mode 100644
index 000000000..80e81ba59
--- /dev/null
+++ b/.github/docs-overrides/javascripts/mathjax.js
@@ -0,0 +1,12 @@
+window.MathJax = {
+ tex: {
+ inlineMath: [["\\(", "\\)"]],
+ displayMath: [["\\[", "\\]"]],
+ processEscapes: true,
+ processEnvironments: true
+ },
+ options: {
+ ignoreHtmlClass: ".*|",
+ processHtmlClass: "arithmatex"
+ }
+};
diff --git a/.github/docs-overrides/main.html b/.github/docs-overrides/main.html
new file mode 100644
index 000000000..0af326afb
--- /dev/null
+++ b/.github/docs-overrides/main.html
@@ -0,0 +1,8 @@
+{% extends "base.html" %}
+
+{% block outdated %}
+ You're not viewing the latest version.
+
+ Click here to go to latest.
+
+{% endblock %}
diff --git a/.github/docs-overrides/stylesheets/custom.css b/.github/docs-overrides/stylesheets/custom.css
new file mode 100644
index 000000000..5ce45acc6
--- /dev/null
+++ b/.github/docs-overrides/stylesheets/custom.css
@@ -0,0 +1,50 @@
+@font-face {
+ font-family: 'Geist';
+ src: url('https://cdn.jsdelivr.net/npm/geist@1.3.0/dist/fonts/geist-sans/Geist-Regular.woff2') format('woff2');
+ font-weight: 400;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'Geist';
+ src: url('https://cdn.jsdelivr.net/npm/geist@1.3.0/dist/fonts/geist-sans/Geist-Medium.woff2') format('woff2');
+ font-weight: 500;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'Geist';
+ src: url('https://cdn.jsdelivr.net/npm/geist@1.3.0/dist/fonts/geist-sans/Geist-Bold.woff2') format('woff2');
+ font-weight: 700;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'Geist Mono';
+ src: url('https://cdn.jsdelivr.net/npm/geist@1.3.0/dist/fonts/geist-mono/GeistMono-Regular.woff2') format('woff2');
+ font-weight: 400;
+ font-display: swap;
+}
+
+:root {
+ --md-text-font: 'Geist', -apple-system, BlinkMacSystemFont, sans-serif;
+ --md-code-font: 'Geist Mono', 'SFMono-Regular', 'Menlo', monospace;
+}
+
+[data-md-color-scheme="slate"] {
+ --md-default-bg-color: #000000;
+ --md-footer-bg-color: #000000;
+ --md-footer-bg-color--dark: #000000;
+}
+
+[data-md-color-scheme="slate"] .md-header,
+[data-md-color-scheme="slate"] .md-tabs,
+[data-md-color-scheme="slate"] .md-sidebar {
+ background-color: #000000;
+}
+
+.md-banner--warning {
+ background-color: #d4a017;
+ color: #000000;
+}
+
+.md-banner--warning a {
+ color: #000000;
+}
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 000000000..e05a224e4
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,46 @@
+name: Build
+
+on:
+ pull_request:
+ paths:
+ - cactus-engine/**
+ - cactus-graph/**
+ - cactus-kernels/**
+ - apple/**
+ - android/**
+ - .github/workflows/build.yml
+ push:
+ branches: [main]
+ paths:
+ - cactus-engine/**
+ - cactus-graph/**
+ - cactus-kernels/**
+ - apple/**
+ - android/**
+ - .github/workflows/build.yml
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ apple:
+ runs-on: macos-15
+ timeout-minutes: 30
+ steps:
+ - uses: actions/checkout@v4
+ - run: bash cactus-engine/build.sh
+ - run: BUILD_STATIC=true BUILD_XCFRAMEWORK=false bash apple/build.sh
+ - run: bash android/build.sh
+
+ linux:
+ runs-on: ubuntu-24.04-arm
+ timeout-minutes: 15
+ steps:
+ - uses: actions/checkout@v4
+ - run: sudo apt-get update && sudo apt-get install -y cmake build-essential libcurl4-openssl-dev
+ - run: bash cactus-engine/build.sh
diff --git a/.github/workflows/cpp.yml b/.github/workflows/cpp.yml
new file mode 100644
index 000000000..a75ad5312
--- /dev/null
+++ b/.github/workflows/cpp.yml
@@ -0,0 +1,42 @@
+name: Unit Tests
+
+on:
+ pull_request:
+ paths:
+ - cactus-kernels/**
+ - cactus-graph/**
+ - .github/workflows/cpp.yml
+ push:
+ branches: [main]
+ paths:
+ - cactus-kernels/**
+ - cactus-graph/**
+ - .github/workflows/cpp.yml
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ unit-tests:
+ timeout-minutes: 30
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - os: macos-15
+ deps: brew install ccache ninja
+ - os: ubuntu-24.04-arm
+ deps: sudo apt-get update && sudo apt-get install -y cmake build-essential libcurl4-openssl-dev ccache ninja-build
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v4
+ - run: ${{ matrix.deps }}
+ - name: Run kernel tests
+ run: bash cactus-kernels/test.sh
+ - name: Run graph tests
+ run: bash cactus-graph/test.sh
diff --git a/.github/workflows/dco.yml b/.github/workflows/dco.yml
index 1d12852dd..fdbf6d45c 100644
--- a/.github/workflows/dco.yml
+++ b/.github/workflows/dco.yml
@@ -4,17 +4,35 @@ on:
pull_request:
types: [opened, synchronize, reopened]
+permissions:
+ contents: read
+
+concurrency:
+ group: dco-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
jobs:
dco-check:
runs-on: ubuntu-latest
+ timeout-minutes: 5
name: DCO Check
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check DCO
- uses: dco-action/check@v1
- with:
- token: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
+ run: |
+ set -euo pipefail
+
+ missing=0
+ while read -r commit; do
+ author="$(git show -s --format='%an <%ae>' "$commit")"
+ if ! git show -s --format='%B' "$commit" | grep -Fqi "Signed-off-by: $author"; then
+ echo "::error::Commit $commit is missing Signed-off-by: $author"
+ missing=1
+ fi
+ done < <(git rev-list --no-merges "${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}")
+
+ exit "$missing"
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 000000000..5b76c753e
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,74 @@
+name: Deploy Docs
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - docs/**
+ - blog/**
+ - assets/**
+ - mkdocs.yml
+ - CACTUS_VERSION
+ - CONTRIBUTING.md
+ - DCO.md
+ - .github/assemble-docs.sh
+ - .github/docs-overrides/**
+ - .github/workflows/docs.yml
+ - python/README.md
+ - apple/README.md
+ - android/README.md
+ - bindings/flutter/README.md
+ - bindings/kotlin/README.md
+ - bindings/react-native/README.md
+ - bindings/rust/README.md
+ - bindings/swift/README.md
+ - README.md
+ workflow_call:
+ inputs:
+ version:
+ type: string
+ required: false
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: docs-deploy
+ cancel-in-progress: false
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ fetch-tags: true
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+
+ - name: Install dependencies
+ run: pip install "mkdocs==1.6.1" "mkdocs-material==9.7.5" "mike==2.1.4"
+
+ - name: Build and deploy docs
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ TAG="${{ inputs.version || '' }}"
+ [ -z "$TAG" ] && TAG=$(cat CACTUS_VERSION | tr -d '[:space:]')
+ bash .github/assemble-docs.sh "$TAG"
+ mike deploy "$TAG" latest --update-aliases --push
+ mike set-default latest --push
+
+ git checkout gh-pages
+ echo "docs.cactuscompute.com" > CNAME
+ git add CNAME
+ git commit -m "Preserve CNAME for custom domain" --allow-empty || true
+ git push origin gh-pages
+ git checkout -
diff --git a/.github/workflows/inference.yml b/.github/workflows/inference.yml
new file mode 100644
index 000000000..d8cac3705
--- /dev/null
+++ b/.github/workflows/inference.yml
@@ -0,0 +1,112 @@
+name: Inference Tests
+
+on:
+ pull_request_review:
+ types: [submitted]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ actions: read
+
+concurrency:
+ group: inference-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ gate:
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ outputs:
+ approved: ${{ steps.check.outputs.approved }}
+ steps:
+ - id: check
+ run: |
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
+ echo "approved=true" >> $GITHUB_OUTPUT
+ elif [ "${{ github.event.review.state }}" = "approved" ]; then
+ echo "approved=true" >> $GITHUB_OUTPUT
+ else
+ echo "approved=false" >> $GITHUB_OUTPUT
+ fi
+
+ runner-check:
+ needs: gate
+ if: needs.gate.outputs.approved == 'true'
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ outputs:
+ available: ${{ steps.check.outputs.available }}
+ steps:
+ - id: check
+ env:
+ GH_TOKEN: ${{ github.token }}
+ REPO: ${{ github.repository }}
+ run: |
+ set -euo pipefail
+ status=$(curl -sS -L -w "%{http_code}" -o runners.json \
+ -H "Authorization: Bearer ${GH_TOKEN}" \
+ -H "Accept: application/vnd.github+json" \
+ -H "X-GitHub-Api-Version: 2022-11-28" \
+ "https://api.github.com/repos/${REPO}/actions/runners?per_page=100" || true)
+ if [ "$status" != "200" ]; then
+ echo "available=false" >> "$GITHUB_OUTPUT"
+ echo "::notice::Could not inspect self-hosted runners (HTTP $status); skipping inference tests instead of queueing."
+ exit 0
+ fi
+ count=$(jq '[.runners[] | select(.status == "online" and .busy == false) | select(([.labels[].name] | contains(["self-hosted", "linux", "arm64"])))] | length' runners.json)
+ if [ "$count" -gt 0 ]; then
+ echo "available=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "available=false" >> "$GITHUB_OUTPUT"
+ echo "::notice::No idle self-hosted linux arm64 runner is available; skipping inference tests."
+ fi
+
+ build:
+ needs: [gate, runner-check]
+ if: needs.gate.outputs.approved == 'true' && needs.runner-check.outputs.available == 'true'
+ runs-on: [self-hosted, linux, arm64]
+ timeout-minutes: 60
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ clean: false
+
+ - name: Setup Python environment
+ run: |
+ if [ ! -f venv/bin/activate ]; then
+ source ./setup
+ fi
+
+ - name: Build libcactus + engine tests
+ run: |
+ bash cactus-engine/build.sh
+ cd cactus-engine/tests
+ rm -rf build && mkdir -p build && cd build
+ cmake .. -DCMAKE_BUILD_TYPE=Release
+ make -j$(nproc)
+
+ model-tests:
+ needs: build
+ runs-on: [self-hosted, linux, arm64]
+ timeout-minutes: 60
+ env:
+ CACTUS_NO_CLOUD_TELE: "1"
+ strategy:
+ fail-fast: false
+ matrix:
+ model:
+ - LiquidAI/LFM2-VL-450M
+ - openai/whisper-small
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ clean: false
+
+ - name: Run tests for ${{ matrix.model }}
+ env:
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
+ run: |
+ source venv/bin/activate
+ MODEL="${{ github.event.inputs.model || matrix.model }}"
+ python3 -m cactus test --component engine --model "$MODEL"
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
deleted file mode 100644
index c3b8fe8d2..000000000
--- a/.github/workflows/publish.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-name: Publish Models
-
-on:
- push:
- tags:
- - 'v*'
-
-jobs:
- publish:
- runs-on: macos-latest
- steps:
- - uses: actions/checkout@v4
-
- - uses: actions/checkout@v4
- with:
- repository: cactus-compute/cactus-pro
- token: ${{ secrets.CACTUS_PRO_TOKEN }}
- path: cactus-pro
-
- - uses: actions/setup-python@v5
- with:
- python-version: '3.12'
-
- - run: pip install -r tools/requirements.txt
-
- - run: |
- cd tools
- python -m src.publish_to_hf \
- --version ${GITHUB_REF#refs/tags/} \
- --org Cactus-Compute \
- --precision MIXED \
- --bits 8
- env:
- HF_TOKEN: ${{ secrets.HF_TOKEN }}
diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml
new file mode 100644
index 000000000..9cf97fcd3
--- /dev/null
+++ b/.github/workflows/pypi.yml
@@ -0,0 +1,161 @@
+name: PyPI Publish
+
+on:
+ workflow_call:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "Version to publish (e.g. 2.0.0). Leave empty to read from CACTUS_VERSION."
+ required: false
+ type: string
+
+permissions:
+ contents: read
+
+jobs:
+ wheels-macos:
+ runs-on: macos-26
+ timeout-minutes: 45
+ env:
+ MACOSX_DEPLOYMENT_TARGET: "14.0"
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+
+ - uses: ./.github/actions/stamp-version
+
+ - name: Install SDL2 (live microphone transcription)
+ run: brew install sdl2
+
+ - name: Build native library
+ run: |
+ pip install -e python/
+ cactus build
+ mkdir -p python/cactus/bindings/lib
+ cp cactus-engine/build/libcactus_engine.dylib python/cactus/bindings/lib/
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "22"
+
+ - name: Bundle coding agent into the wheel
+ run: |
+ (cd cactus-code && npm install --no-audit --no-fund)
+ bash cactus-code/scripts/bundle-for-pypi.sh
+ rm -rf python/cactus/code/packages/tui/native/win32
+ rm -rf python/cactus/code/packages/tui/native/darwin/prebuilds/darwin-x64
+
+ - name: Build and repair wheel
+ working-directory: python
+ run: |
+ pip install build delocate wheel
+ python -m build --wheel -C="--build-option=--plat-name=macosx_14_0_arm64"
+ env -u MACOSX_DEPLOYMENT_TARGET delocate-wheel --require-archs arm64 -w wheelhouse dist/*.whl
+ whl="$(ls "$PWD"/wheelhouse/*.whl)"
+ work="$(mktemp -d)"
+ python -m wheel unpack "$whl" -d "$work"
+ pkg="$(ls -d "$work"/*/)"
+ cp "$(brew --prefix sdl3)/lib/libSDL3.0.dylib" "${pkg}cactus/.dylibs/libSDL3.dylib"
+ chmod +w "${pkg}cactus/.dylibs/libSDL3.dylib"
+ for lib in "${pkg}cactus/.dylibs/libSDL2-2.0.0.dylib" "${pkg}cactus/.dylibs/libSDL3.dylib"; do
+ vtool -set-build-version macos 14.0 14.0 -replace -output "$lib" "$lib"
+ codesign -f -s - "$lib"
+ done
+ sed -i '' -E 's/macosx_[0-9]+_[0-9]+_arm64/macosx_14_0_arm64/' "$(ls -d "${pkg}"*.dist-info)/WHEEL"
+ rm "$whl"
+ python -m wheel pack "$pkg" -d "$PWD/wheelhouse"
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: wheel-macos-arm64
+ path: python/wheelhouse/*.whl
+
+ wheels-linux:
+ runs-on: ubuntu-24.04-arm
+ timeout-minutes: 45
+ container:
+ image: quay.io/pypa/manylinux_2_28_aarch64
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Provision toolchain
+ run: |
+ dnf install -y epel-release
+ dnf install -y libcurl-devel openssl-devel SDL2-devel pkgconfig
+ echo "/opt/python/cp312-cp312/bin" >> "$GITHUB_PATH"
+
+ - uses: ./.github/actions/stamp-version
+
+ - name: Build native library
+ run: |
+ pip install -e python/
+ cactus build
+ mkdir -p python/cactus/bindings/lib
+ cp cactus-engine/build/libcactus_engine.so python/cactus/bindings/lib/
+
+ - name: Install Node.js 22
+ run: |
+ curl -fsSL https://nodejs.org/dist/v22.21.0/node-v22.21.0-linux-arm64.tar.xz -o /tmp/node.tar.xz
+ tar -xJf /tmp/node.tar.xz -C /opt
+ echo "/opt/node-v22.21.0-linux-arm64/bin" >> "$GITHUB_PATH"
+
+ - name: Bundle coding agent into the wheel
+ run: |
+ (cd cactus-code && npm install --no-audit --no-fund)
+ bash cactus-code/scripts/bundle-for-pypi.sh
+ rm -rf python/cactus/code/packages/tui/native
+
+ - name: Build and repair wheel
+ working-directory: python
+ run: |
+ pip install build auditwheel
+ python -m build --wheel -C="--build-option=--plat-name=linux_aarch64"
+ auditwheel repair dist/*.whl -w wheelhouse
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: wheel-linux-aarch64
+ path: python/wheelhouse/*.whl
+
+ sdist:
+ runs-on: ubuntu-24.04
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+
+ - uses: ./.github/actions/stamp-version
+
+ - name: Build sdist
+ working-directory: python
+ run: |
+ pip install build
+ python -m build --sdist
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: sdist
+ path: python/dist/*.tar.gz
+
+ publish:
+ needs: [wheels-macos, wheels-linux, sdist]
+ runs-on: ubuntu-24.04
+ timeout-minutes: 15
+ permissions:
+ id-token: write
+ steps:
+ - uses: actions/download-artifact@v4
+ with:
+ path: dist
+ merge-multiple: true
+
+ - uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ packages-dir: dist
+ attestations: false
diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml
new file mode 100644
index 000000000..4b15e6de8
--- /dev/null
+++ b/.github/workflows/python.yml
@@ -0,0 +1,47 @@
+name: Python Tests
+
+on:
+ pull_request:
+ paths:
+ - "python/**"
+ - ".github/workflows/python.yml"
+ push:
+ branches: [main]
+ paths:
+ - "python/**"
+ - ".github/workflows/python.yml"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ test:
+ runs-on: ubuntu-24.04-arm
+ timeout-minutes: 30
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install build tools
+ run: sudo apt-get update && sudo apt-get install -y cmake build-essential libcurl4-openssl-dev
+
+ - name: Build libcactus
+ run: bash cactus-engine/build.sh
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+
+ - name: Install dependencies
+ run: pip install -e "python/[dev,convert]"
+
+ - name: Run tests
+ run: |
+ cd python
+ pytest tests/ -v \
+ --ignore=tests/test_model.py \
+ --ignore=tests/test_server_live.py
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 000000000..c79543413
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,117 @@
+name: Release
+
+on:
+ release:
+ types: [published]
+
+permissions:
+ contents: read
+
+concurrency:
+ group: release-${{ github.ref }}
+ cancel-in-progress: false
+
+jobs:
+ pypi:
+ permissions:
+ id-token: write
+ contents: read
+ uses: ./.github/workflows/pypi.yml
+
+ homebrew:
+ needs: pypi
+ runs-on: ubuntu-24.04
+ timeout-minutes: 15
+ env:
+ PACKAGE: cactus-compute
+ TAP: cactus-compute/homebrew-cactus
+ TAG: ${{ github.event.release.tag_name }}
+ steps:
+ - name: Resolve version
+ id: ver
+ run: |
+ raw="${TAG#v}"
+ version=$(python3 -c "raw='${raw}'; print(raw + '.0' if raw.count('.') == 1 else raw)")
+ echo "version=$version" >> "$GITHUB_OUTPUT"
+
+ - name: Resolve sdist URL and SHA256 from PyPI
+ id: pypi
+ env:
+ VERSION: ${{ steps.ver.outputs.version }}
+ run: |
+ for i in $(seq 1 10); do
+ if json=$(curl -fsSL "https://pypi.org/pypi/${PACKAGE}/${VERSION}/json"); then
+ break
+ fi
+ echo "Waiting for PyPI metadata (attempt $i)..."
+ sleep 30
+ done
+ [ -n "$json" ] || { echo "PyPI metadata not available for ${PACKAGE} ${VERSION}"; exit 1; }
+ pick() { echo "$json" | jq -er '.urls[] | select(.packagetype=="sdist") | .'"$1"; }
+ {
+ echo "sdist_url=$(pick url)"
+ echo "sdist_sha=$(pick digests.sha256)"
+ } >> "$GITHUB_OUTPUT"
+
+ - name: Update tap formula
+ env:
+ HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
+ VERSION: ${{ steps.ver.outputs.version }}
+ SDIST_URL: ${{ steps.pypi.outputs.sdist_url }}
+ SDIST_SHA: ${{ steps.pypi.outputs.sdist_sha }}
+ run: |
+ git clone "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/${TAP}.git" tap
+ mkdir -p tap/Formula
+ cat > tap/Formula/cactus.rb <= 22 on PATH.
+ cactus convert (torch) is opt-in, same as pip's [convert] extra:
+ #{Formula["python@3.12"].opt_bin}/python3.12 -m pip --python=#{libexec}/bin/python install "${PACKAGE}[convert]==#{version}"
+ EOS
+ end
+
+ test do
+ assert_equal "cactus #{version}\n", shell_output("#{bin}/cactus --version")
+ assert_match "libcactus_engine",
+ Dir[libexec/"lib/python*/site-packages/cactus/bindings/lib/*"].join
+ end
+ end
+ EOF
+ cd tap
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git add Formula/cactus.rb
+ git commit -m "Update cactus to ${TAG}"
+ git push origin main
+
+ docs:
+ permissions:
+ contents: write
+ uses: ./.github/workflows/docs.yml
+ with:
+ version: ${{ github.event.release.tag_name }}
diff --git a/.gitignore b/.gitignore
index 64b09d249..b2a93af05 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,27 +1,32 @@
-build
+**/build/
+*.a
+*.so
+*.dylib
+*.bin
+*.cg
+
+!cactus-engine/libs/curl/**/*.a
+!android/mbedtls/**/*.a
+
+apple/build-*/
+apple/*.xcframework
+
.claude
.vscode
.DS_Store
-*.gguf
-weights/
-stage/
-.cache/
-android/build/
-android/libs/
-android/arm64-v8a/
-*.so
-
-apple/build/
-apple/build-static/
-apple/*.xcframework
-apple/libcactus.a
-*.a
-!libs/libcactus_pro.a
+__pycache__/
+*.pyc
+*.egg-info
+.pytest_cache/
venv
-__pycache__
-*.egg-info
-profile.txt
-*.dylib
-*.bin
\ No newline at end of file
+weights/
+transpiled/
+python/dist/
+python/cactus/bin/
+python/cactus/bindings/lib/
+site/
+site_docs/
+
+python/cactus/code/
diff --git a/CACTUS_VERSION b/CACTUS_VERSION
index 3e1ad720b..0ac852dde 100644
--- a/CACTUS_VERSION
+++ b/CACTUS_VERSION
@@ -1 +1 @@
-1.5.0
\ No newline at end of file
+v2.0.1
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 78e1b8b74..5174d4485 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,198 +1,54 @@
-# Contributing to Cactus
-
-### Code Style
-
-- **C++ Standard**: Use C++20 features where appropriate
-- **Formatting**: Follow the existing code style in the project
-- **ARM NEON**: When writing SIMD code, ensure proper alignment and use appropriate intrinsics
-- **Comments**: Add comments for complex algorithms, especially in kernel implementations
-
-### Performance Considerations
-
-Cactus is a high-performance inference library optimized for ARM processors. When contributing:
-
-1. **Benchmark Your Changes**: Test performance impact, especially for kernel functions
-2. **Memory Efficiency**: Minimize memory allocations in hot paths
-3. **SIMD Optimization**: Use ARM NEON intrinsics where beneficial
-4. **Cache Awareness**: Consider cache line sizes and memory access patterns
-
-### Testing
-
-Before submitting a PR:
+---
+title: "Contributing to Cactus"
+description: "Guidelines for contributing to Cactus, the on-device AI inference engine. Covers code style, PR process, testing, and benchmarking."
+keywords: ["contributing", "open source", "pull request", "code style", "C++20"]
+---
-1. Ensure all existing tests pass
-2. Add tests for new functionality
-3. Test on ARM hardware if possible (Apple Silicon, Raspberry Pi, etc.)
-4. Verify quantized operations maintain acceptable accuracy
+# Contributing to Cactus
-### Pull Request Process
+Thank you for your interest in contributing to Cactus! This document covers the guidelines and process for making contributions.
-1. **Fork** the repository and create your branch from `main`
-2. **Make your changes** following the guidelines above
-3. **Sign-off all commits** using DCO
-4. **Update documentation** if you change APIs
-5. **Open a Pull Request** with a clear title and description
+## Code Guidelines
-#### PR Description Template
+- **C++ Standard**: Use C++20 features where appropriate.
+- **Formatting**: Follow the existing code style in the project, one header per folder.
+- **Comments**: Avoid comments, make your code read like plain english.
+- **AI-Generated Code**: Do not blindly PR AI slop, this codebase is very complex, they miss details.
+- **Update docs**: Please update docs when necessary, be intuitive and straightforward.
+- **Keep It Simple**: Do not go beyond the scope of the GH issue, avoid bloated PRs, keep codes lean.
+- **Benchmark Your Changes**: Test performance impact, Cactus is performance-critical.
+- **Test everything**: A PR that fails to build is the biggest red flag, means it was not tested.
-```markdown
-## Description
-Brief description of what this PR does
+## Pull Request Process
-## Type of Change
-- [ ] Bug fix
-- [ ] New feature
-- [ ] Performance improvement
-- [ ] Documentation update
+1. Fork the repository and create a branch from `main`.
+2. Make your changes, keeping the scope focused on the relevant GitHub issue.
+3. Run `cactus test` to verify your changes build and pass all tests.
+4. Update documentation if your changes affect the public API or user-facing behavior.
+5. Submit a pull request with a clear description of what you changed and why.
## Testing
-- [ ] Tests pass locally
-- [ ] Tested on ARM hardware
-- [ ] Benchmarked performance impact
-
-## Checklist
-- [ ] All commits are signed-off (DCO)
-- [ ] Code follows project style
-- [ ] Comments added where necessary
-- [ ] Documentation updated if needed
-```
-
-### Reporting Issues
-
-When reporting issues, please include:
-
-1. System information (OS, CPU architecture, ARM variant)
-2. Cactus version or commit hash
-3. Minimal code to reproduce the issue
-4. Expected vs actual behavior
-5. Any relevant logs or error messages
-
-### Areas of Contribution
-
-We especially welcome contributions in these areas:
-
-- **Kernel Optimizations**: SIMD implementations for ARM architectures
-- **Quantization**: Improved quantization techniques (INT8, INT4)
-- **Model Support**: Support for additional model architectures
-- **NPU Integration**: Apple Neural Engine and other NPU backends
-- **Documentation**: Tutorials, examples, and API documentation
-- **Testing**: Test coverage and benchmarking infrastructure
-
-### Development Setup
-
-```bash
-# Clone the repository
-git clone https://github.com/yourusername/cactus.git
-cd cactus
-
-# Setup the environment (installs dependencies and activates venv)
-./setup
-```
-
-You can run these codes directly on M-series Macbooks since they are ARM-based.
-Vanilla M3 CPU-only can run Qwen3-600m-INT8 at 60-70 toks/sec.
-
-### Running Tests
-
-```bash
-cactus test # Run unit tests and benchmarks
-cactus test --model # Use a specific model for tests
-cactus test --ios # Run tests on connected iPhone
-cactus test --android # Run tests on connected Android device
-```
-
-### Building
-
-```bash
-cactus build # Build for ARM chips (libcactus.a)
-cactus build --apple # Build for Apple platforms (iOS/macOS)
-cactus build --android # Build for Android
-```
-
-### Downloading Models
```bash
-cactus download # Download and convert model weights
-cactus run # Download, build, and run playground
-```
+# Run all tests
+cactus test
-### Python FFI for Researchers
+# Run tests on a connected iOS device
+cactus test --ios
-Cactus provides Python bindings for quick scripting and research. After setup:
+# Run tests on a connected Android device
+cactus test --android
-```bash
-cactus build
-cactus download LiquidAI/LFM2-VL-450M
-cactus download openai/whisper-small
-cd python && python example.py
+# Test a specific model
+cactus test --model LiquidAI/LFM2-VL-450M
```
-**Available functions:**
+## Developer Certificate of Origin
-```python
-from cactus import (
- cactus_init, # Load a model
- cactus_complete, # Text/VLM completion
- cactus_transcribe, # Audio transcription (Whisper)
- cactus_embed, # Text embeddings
- cactus_image_embed, # Image embeddings
- cactus_audio_embed, # Audio embeddings
- cactus_reset, # Reset model state
- cactus_destroy # Free model memory
-)
-```
-
-**Quick example:**
-
-```python
-import json
-from cactus import cactus_init, cactus_complete, cactus_destroy
-
-# Load model
-model = cactus_init("../weights/lfm2-vl-450m")
-
-# Text completion
-messages = json.dumps([{"role": "user", "content": "What is 2+2?"}])
-response = cactus_complete(model, messages)
-print(json.loads(response)["response"])
-
-# VLM - describe image
-messages = json.dumps([{"role": "user", "content": "Describe this image", "images": ["path/to/image.png"]}])
-response = cactus_complete(model, messages)
-
-cactus_destroy(model)
-```
+All contributions must comply with the [Developer Certificate of Origin (DCO)](DCO.md). By submitting a contribution, you certify that you have the right to do so under the project's open source license.
-**Whisper transcription:**
-
-```python
-from cactus import cactus_init, cactus_transcribe, cactus_destroy
-
-whisper = cactus_init("../weights/whisper-small")
-prompt = "<|startoftranscript|><|en|><|transcribe|><|notimestamps|>"
-response = cactus_transcribe(whisper, "audio.wav", prompt=prompt)
-
-cactus_destroy(whisper)
-```
-
-See `python/example.py` for a complete working example.
-
-### Questions?
-
-If you have questions about contributing, feel free to:
-
-1. Open an issue for discussion
-2. Check existing issues and PRs
-3. Review the codebase documentation
-
-## License
-
-By contributing to Cactus, you agree that your contributions will be licensed under the same license as the project (check LICENSE file).
-
-## Enforcement
-
-Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainers. All complaints will be reviewed and investigated promptly and fairly.
-
----
+## See Also
-Thank you for contributing to Cactus!
\ No newline at end of file
+- [Cactus Engine API](/docs/cactus_engine.md) — C API reference
+- [Cactus Graph API](/docs/cactus_graph.md) — Computational graph API reference
+- [Cactus Index API](/docs/cactus_index.md) — Vector database API reference
diff --git a/README.md b/README.md
index 95d00c444..00d2c928e 100644
--- a/README.md
+++ b/README.md
@@ -1,63 +1,52 @@
+# Cactus
+
-Cross-platform & energy-efficient kernels, runtime and AI inference engine for mobile devices.
+[![Docs][docs-shield]][docs-url]
+[![Website][website-shield]][website-url]
+[![GitHub][github-shield]][github-url]
+[![HuggingFace][hf-shield]][hf-url]
+[![Reddit][reddit-shield]][reddit-url]
+[![Blog][blog-shield]][blog-url]
+
+A hybrid edge-cloud AI engine for mobile devices & wearables.
```
┌─────────────────┐
-│ Cactus FFI │ ←── OpenAI compatible C API for integration (tools, RAG, cloud handoff)
-└─────────────────┘
+│ Cactus Engine │ ←── OpenAI-compatible APIs for text, speech, and vision.
+└─────────────────┘
│
┌─────────────────┐
-│ Cactus Engine │ ←── High-level transformer engine (NPU support, INT4/INT8/FP16/MIXED)
-└─────────────────┘
+│ Cactus Graph │ ←── Zero-copy computation graph
+└─────────────────┘
│
┌─────────────────┐
-│ Cactus Models │ ←── Implements SOTA models using Cactus Graphs
-└─────────────────┘
+│ Cactus Kernels │ ←── CPU/GPU kernels for (Apple, Samsung, Pixel, etc.)
+└─────────────────┘
│
-┌─────────────────┐
-│ Cactus Graph │ ←── Unified zero-copy computation graph (think NumPy for mobile)
-└─────────────────┘
+┌─────────────────┐
+│ Cactus Quants │ ←── Custom rotation-based quantization technique
+└─────────────────┘
│
┌─────────────────┐
-│ Cactus Kernels │ ←── Low-level ARM-specific SIMD operations (think CUDA for mobile)
+│Cactus Transpiler│ ←── Transpiles custom PyTorch model to Cactus.
└─────────────────┘
```
-# Cactus Graph & Kernel
-```cpp
-#include cactus.h
-
-CactusGraph graph;
-auto a = graph.input({2, 3}, Precision::FP16);
-auto b = graph.input({3, 4}, Precision::INT8);
-
-auto x1 = graph.matmul(a, b, false);
-auto x2 = graph.transpose(x1);
-auto result = graph.matmul(b, x2, true);
-
-float a_data[6] = {1.1f, 2.3f, 3.4f, 4.2f, 5.7f, 6.8f};
-float b_data[12] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
-
-graph.set_input(a, a_data, Precision::FP16);
-graph.set_input(b, b_data, Precision::INT8);
-
-graph.execute();
-void* output_data = graph.get_output(result);
+## Quick Demo (Mac)
-graph.hard_reset();
+- Step 1: `brew install cactus-compute/cactus/cactus`
+- Step 2: `cactus run`
-```
+## Cactus Engine
-# Cactus Engine & FFI
```cpp
-#include cactus.h
-
-cactus_set_pro_key(""); // email founders@cactuscompute.com for optional key
+#include "cactus_engine.h"
cactus_model_t model = cactus_init(
- "path/to/weight/folder", // section to generate weigths below
- "txt/or/md/file/or/dir/with/many", // nullptr if none, cactus does automatic fast RAG
+ "path/to/weight/folder",
+ "path to txt or dir of txts for auto-rag",
+ false
);
const char* messages = R"([
@@ -72,116 +61,283 @@ const char* options = R"({
char response[4096];
int result = cactus_complete(
- model, // model handle from cactus_init
- messages, // JSON array of chat messages
- response, // buffer to store response JSON
- sizeof(response), // size of response buffer
- options, // optional: generation options (nullptr for defaults)
- nullptr, // optional: tools JSON for function calling
- nullptr, // optional: streaming callback fn(token, id, user_data)
- nullptr // optional: user data passed to callback
+ model, // model handle
+ messages, // JSON chat messages
+ response, // response buffer
+ sizeof(response), // buffer size
+ options, // generation options
+ nullptr, // tools JSON
+ nullptr, // streaming callback
+ nullptr, // user data
+ nullptr, // pcm audio buffer
+ 0 // pcm buffer size
);
```
-Example response from Gemma3-270m
+Example response from Gemma4-E2B
```json
{
- "success": true, // when successfully generated locally
- "error": null, // returns specific errors if success = false
- "cloud_handoff": false, // true when model is unconfident, simply route to cloud
- "response": "Hi there!", // null when error is not null or cloud_handoff = true
- "function_calls": [], // parsed to [{"name":"set_alarm","arguments":{"hour":"10","minute":"0"}}]
- "confidence": 0.8193, // how confident the model is with its response
- "time_to_first_token_ms": 45.23, // latency (time to first token)
- "total_time_ms": 163.67, // total execution time
- "prefill_tps": 1621.89, // prefill tokens per second
- "decode_tps": 168.42, // decode tokens per second
- "ram_usage_mb": 245.67, // current process RAM usage in MB
+ "success": true, // generation succeeded
+ "error": null, // error details if failed
+ "cloud_handoff": false, // true if cloud model used
+ "response": "Hi there!",
+ "function_calls": [], // parsed tool calls
+ "segments": [], // transcription segments (empty for chat)
+ "confidence": 0.8193, // model confidence
+ "confidence_threshold": 0.7, // resolved handoff threshold (model-dependent)
+ "time_to_first_token_ms": 45.23,
+ "total_time_ms": 163.67,
+ "prefill_tps": 1621.89,
+ "decode_tps": 168.42,
+ "ram_usage_mb": 245.67,
"prefill_tokens": 28,
"decode_tokens": 50,
"total_tokens": 78
}
```
-# Performance
-
-- **Models:** LFM2-VL-450m & Whisper-Small
-- **Precision:** Cactus smartly blends INT4, INT8 and F16 for all weights.
-- **Decode** = toks/sec, **P/D** = prefill/decode, **VLM** = 256×256 image, **STT** = 30s audio
-- **Cactus Pro**: Uses NPU for realtime and large context (Apple for now), scores are marked with *
-
-| Device | Short Decode | 4k-P/D | VLM-TTFT | VLM-Dec | STT-TTFT | STT-Dec |
-|--------|--------|--------|----------|---------|----------|---------|
-| Mac M4 Pro | 170 | 989/150 | 0.2s/0.1s* | 168 | 1.0s/0.2s* | 92 |
-| Mac M3 Pro | 140 | 890/123 | 0.3s/0.1s* | 149 | 1.5s/0.4s* | 81 |
-| iPad/Mac M4 | 134 | 603/106 | 0.3s/0.1s* | 129 | 1.8s0.3s* | 70 |
-| iPad/Mac M3 | 117 | 525/93 | 0.4s/0.1s* | 111 | 2.8s/0.7s* | 61 |
-| iPhone 17 Pro | 126 | 428/84 | 0.5s/0.1s* | 120 | 3.0s/0.6s* | 80 |
-| iPhone 16 Pro | 106 | 380/81 | 0.6s/0.2s* | 101 | 4.3s/0.7s* | 75 |
-| iPhone 15 Pro | 90 | 330/75 | 0.7s/0.3s* | 92 | 4.5s/0.8s* | 70 |
-| Galaxy S25 Ultra | 80 | 355/52 | 0.7s | 70 | 3.6s/- | 32 |
-| Nothing 3 | 56 | 320/46 | 0.8s | 54 | 4.5s | 55 |
-| Pixel 6a | 25 | 108/24 | 2.3s | 25 | 9.6 | 15 |
-| Raspberry Pi 5 | 20 | 292/18 | 1.7s | 23 | 15s | 16 |
-
-
-# Supported models
-
-- Cactus smartly and compactly blends INT4, INT8 and F16 for all weights.
-- You could still quantize everything with one precision, but mixed is optimal
-
-| Model | Zipped Size | Completion | Tools | Vision | Embed | Speech | Pro |
-|-------|------------------|------------|-------|--------|-------|--------|-----|
-| google/gemma-3-270m-it | 252MB | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ |
-| google/functiongemma-270m-it | 252MB | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ |
-| openai/whisper-small | 283MB | ✗ | ✗ | ✗ | ✓ | ✓ | Apple |
-| LiquidAI/LFM2-350M | 244MB | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ |
-| LiquidAI/LFM2-VL-450M | 448MB | ✓ | ✗ | ✓ | ✓ | ✗ | Apple |
-| nomic-ai/nomic-embed-text-v2-moe | 451MB | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ |
-| Qwen/Qwen3-0.6B | 514MB | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ |
-| Qwen/Qwen3-Embedding-0.6B | 514MB | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ |
-| LiquidAI/LFM2-700M | 498MB | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ |
-| google/gemma-3-1b-it | 642MB | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ |
-| LiquidAI/LFM2.5-1.2B-Instruct | 474MB | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ |
-| LiquidAI/LFM2-1.2B-RAG | 474MB | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ |
-| LiquidAI/LFM2-1.2B-Tool | 474MB | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ |
-| openai/whisper-medium | 658MB | ✗ | ✗ | ✗ | ✓ | ✓ | Apple |
-| LiquidAI/LFM2.5-VL-1.6B | 954MB | ✓ | ✗ | ✓ | ✓ | ✗ | Apple |
-| Qwen/Qwen3-1.7B | 749MB | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ |
-
-# Using this repo on a Mac
-
-```bash
-git clone https://github.com/cactus-compute/cactus && cd cactus && source ./setup
+## Cactus Graph
+
+```cpp
+#include "cactus_graph.h"
+
+CactusGraph graph;
+auto a = graph.input({2, 3}, Precision::FP16);
+auto b = graph.input({3, 4}, Precision::INT8);
+
+auto x1 = graph.matmul(a, b, false);
+auto x2 = graph.transpose(x1);
+auto result = graph.matmul(b, x2, true);
+
+float a_data[6] = {1.1f, 2.3f, 3.4f, 4.2f, 5.7f, 6.8f};
+float b_data[12] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
+
+graph.set_input(a, a_data, Precision::FP16);
+graph.set_input(b, b_data, Precision::INT8);
+
+graph.execute();
+void* output_data = graph.get_output(result);
+
+graph.hard_reset();
+```
+
+## Inference Speed
+
+- LLM: Gemma-4-E2B-CQ4 (1k-context prefill / decode for 100 tokens)
+- VLM: Gemma-4-E2B-CQ4 (256px image encode time / decode)
+- Transcribe: Parakeet-TDT-0.6B-CQ4 (20s audio end-to-end transcribe time)
+- 1k-Context RAM: peak MB during the LLM benchmark
+- No speculative decode or MTP, pure decode
+
+Command: `cactus benchmark` [optional `--ios` or `--android`]
+
+| Device | LLM | VLM | Transcribe | RAM |
+|--------|-----|-----|------------|---------------|
+| Mac M5 Max | 2964tps / 154tps | 0.09s / 168tps | 0.15s | 1348MB |
+| Mac M4 Pro | 1947tps / 97tps | 0.25s / 108tps | 0.28s | 1225MB |
+| Mac M3 Pro | 1294tps / 64tps | 0.40s / 72tps | 0.37s | 735MB |
+| iPad/Vision Pro M5 | 1336tps / 71tps | 0.25s / 80tps | 0.27s | 703MB |
+| iPhone 17 Pro | 729tps / 37tps | 0.5s / 39tps | 0.51s | 644MB |
+| iPhone 15 Pro | 511tps / 25tps | 1.16s / 27tps | 1.40s | 635MB |
+
+N/B: With 1k-context prefill and decode for 100 runs on M5 Max
+- `LFM2.5-VL-1.6B` = 289toks/sec
+- `Qwen3-1.7B` = 155toks/sec
+- `LFM2.5-VL-450m` = 472toks/sec, image encodes in 43ms
+- `LFM22.5-VL-230m` = 555toks/sec
+
+## Output Quality
+
+- Gemma-4-E2B-it accuracy across bit widths, averaged over 3 seeds.
+- CQ3.26 and CQ2.54 are mixed-precision, CQ2/CQ3/CQ4 are uniformly quantized.
+- Full results in [docs/cactus_quants.md](/docs/cactus_quants.md):
+
+| Task | F16 (Original) | CQ4 | CQ3.26 | CQ2.54 | CQ2 |
+|-----------|-----|-----|--------|--------|-----|
+| ARC-E | 73.80 | 73.73 | 74.20 | 68.20 | 50.80 |
+| ARC-C | 56.47 | 52.47 | 51.53 | 37.20 | 24.73 |
+| HellaSwag | 46.93 | 47.07 | 45.20 | 40.73 | 35.87 |
+| WinoGrande | 61.00 | 61.13 | 59.60 | 60.13 | 51.27 |
+| MMLU | 62.33 | 59.45 | 57.63 | 47.19 | 33.18 |
+| GPQA | 34.34 | 34.34 | 31.82 | 30.81 | 23.23 |
+| GSM8K | 73.67 | 71.20 | 66.20 | 22.00 | 0.40 |
+| HumanEval | 54.88 | 57.11 | 53.66 | 15.24 | 1.02 |
+| BFCL Simple | 92.00 | 92.42 | 91.50 | 82.25 | 18.75 |
+| BFCL Multi | 89.00 | 88.33 | 89.00 | 52.50 | 13.67 |
+| BFCL Parallel | 84.00 | 83.67 | 82.50 | 30.00 | 3.33 |
+| BFCL Parallel-Multi | 78.00 | 83.33 | 82.00 | 37.00 | 1.33 |
+
+## Supported Models
+
+- Any HuggingFace model can be converted using `cactus convert [HF-Name]`, though experimental.
+- Liquid, Gemma. whisper. parakeet and Qwen model families are especially tested.
+- Some models have been pre-uploaded [here](https://huggingface.co/Cactus-Compute), just run `cactus download [HF-Name]`.
+- `cactus run [HF-Name]` albeit first downloads or convert the model if not found.
+
+## Learn More
+
+| Reference | Language | Description |
+|-----------|----------|-------------|
+| [Cactus Engine](/docs/cactus_engine.md) | C | Chat completion, streaming, tool calling, transcription, embeddings, RAG, vision, vector index, cloud handoff |
+| [Cactus Graph](/docs/cactus_graph.md) | C++ | Tensor operations, matrix multiplication, attention, normalization, activation functions |
+| [Cactus Kernels](/docs/cactus_kernels.md) | C++ | ARM NEON SIMD kernels for matmul, attention, convolution, quantization, DSP, image processing |
+| [Cactus Quants](/docs/cactus_quants.md) | C++ | Rotation-and-codebook quantization from 4-bit to 1-bit for all weight tensors |
+| [Cactus Hybrid](/docs/cactus_hybrid.md) | C/Python | Route hard queries to the cloud automatically based on local model confidence |
+| [Cactus Transpiler](/docs/cactus_transpiler.md) | Python | Convert any PyTorch model to a Cactus runtime graph for on-device inference |
+| [Python Package](/python/) | Python | Python package and CLI |
+
+## Bindings
+
+- [Swift](/bindings/swift/)
+- [Kotlin](/bindings/kotlin/)
+- [Flutter](/bindings/flutter/)
+- [React Native](/bindings/react-native/)
+- [Python](/bindings/python/)
+- [Rust](/bindings/rust/)
+
+## Using this repo
+
+```
+┌────────────────────────────────────────────────────────────────────────────────┐
+│ │
+│ Step 0: if on Linux (Ubuntu/Debian) │
+│ sudo apt-get install python3.12 python3.12-venv python3-pip cmake │
+│ build-essential libcurl4-openssl-dev │
+│ │
+│ Step 1: clone and setup │
+│ git clone https://github.com/cactus-compute/cactus && cd cactus │
+│ source ./setup │
+│ │
+│ Step 2: use the commands │
+│────────────────────────────────────────────────────────────────────────────────│
+│ │
+│ cactus auth manage cloud API key │
+│ --status show key status │
+│ --clear remove saved key │
+│ │
+│ cactus run [model|path] run a model (downloads if needed) │
+│ --bits 1|2|3|4|2.54|3.26 CQ quantization (default: 4) │
+│ --backend cpu|metal inference backend (default: auto) │
+│ --image image file for VLM inference │
+│ --audio audio file for audio chat │
+│ --system system prompt │
+│ --prompt send prompt immediately │
+│ --thinking enable thinking/reasoning mode │
+│ --token HuggingFace token (gated models) │
+│ --reconvert force local rebuild from source │
+│ │
+│ cactus transcribe [model] live microphone transcription with a model│
+│ --file audio file to transcribe (WAV) │
+│ --language language code (default: en) │
+│ --bits 1|2|3|4|2.54|3.26 CQ quantization (default: 4) │
+│ --token HuggingFace token (gated models) │
+│ --reconvert force local rebuild from source │
+│ │
+│ cactus download [model] get a bundle (prebuilt, else build) │
+│ --bits 1|2|3|4|2.54|3.26 CQ quantization (default: 4) │
+│ --token HuggingFace token (gated models) │
+│ --reconvert force local rebuild from source │
+│ │
+│ cactus convert [dir] HuggingFace -> runnable cactus bundle │
+│ (CQ weights + runtime graph) │
+│ --bits 1|2|3|4 CQ quantization (default: 4) │
+│ --token HuggingFace token (gated models) │
+│ --reconvert force local rebuild from source │
+│ --lora merge a LoRA adapter before converting │
+│ --weights-only stop after CQ weights (skip the graph) │
+│ --artifact-dir bundle output (default: weights/) │
+│ │
+│ cactus serve [model] OpenAI-compatible local HTTP server │
+│ --host bind address (default: 127.0.0.1) │
+│ --port port (default: 8080) │
+│ --bits 1|2|3|4|2.54|3.26 CQ quantization (default: 4) │
+│ --backend cpu|metal inference backend (default: auto) │
+│ --token HuggingFace token (gated models) │
+│ --reconvert force local rebuild from source │
+│ --no-cloud-handoff disable automatic cloud handoff │
+│ --confidence-threshold <0..1> handoff to cloud below this confidence │
+│ --cloud-timeout-ms max wait for cloud handoff │
+│ │
+│ cactus code run the AI coding agent (TUI / print) │
+│ --serve-model auto-start a server with this model │
+│ --bits 1|2|3|4|2.54|3.26 CQ quantization (default: 4) │
+│ --backend cpu|metal inference backend (default: auto) │
+│ --token HuggingFace token (gated models) │
+│ --reconvert force local rebuild from source │
+│ --host server address (default: 127.0.0.1) │
+│ --port server port (default: 8080) │
+│ --no-serve require a running server (no auto-start) │
+│ --no-cloud-handoff disable automatic cloud handoff │
+│ --confidence-threshold <0..1> handoff to cloud below this confidence │
+│ --cloud-timeout-ms max wait for cloud handoff │
+│ -- pass remaining args to the agent │
+│ │
+│ cactus list list downloaded models │
+│ │
+│ cactus build build cactus libraries │
+│ --apple Apple (iOS/macOS) │
+│ --android Android │
+│ --python shared lib for Python FFI │
+│ │
+│ cactus test run the test suite │
+│ --component kernels | graph | engine | all │
+│ (default: all) │
+│ --model default: google/gemma-4-E2B-it │
+│ --transcription-model default: nvidia/parakeet-tdt-0.6b-v3 │
+│ --bits 1|2|3|4|2.54|3.26 CQ quantization (default: 4) │
+│ --backend cpu|metal inference backend (default: auto) │
+│ --token HuggingFace token (gated models) │
+│ --reconvert force local rebuild of test models │
+│ --suite run a single test suite by name │
+│ (resolved across components, │
+│ e.g. llm → engine) │
+│ --list list components and suites │
+│ --ios run on connected iPhone │
+│ --android run on connected Android │
+│ --enable-telemetry send cloud telemetry (off by default) │
+│ │
+│ cactus benchmark run the engine benchmark suite │
+│ --model default: google/gemma-4-E2B-it │
+│ --transcription-model default: nvidia/parakeet-tdt-0.6b-v3 │
+│ --bits 1|2|3|4|2.54|3.26 CQ quantization (default: 4) │
+│ --backend cpu|metal inference backend (default: auto) │
+│ --ios run on connected iPhone │
+│ --android run on connected Android │
+│ │
+│ cactus clean delete build artifacts, weights, venv │
+│ cactus --help show this help │
+│ │
+└────────────────────────────────────────────────────────────────────────────────┘
+```
+
+## Citation
+
+If you use Cactus in your research, please cite it as follows:
+
+```bibtex
+@software{cactus,
+ title = {Cactus: AI Inference Engine for Phones & Wearables},
+ author = {Ndubuaku, Henry and Cactus Team},
+ url = {https://github.com/cactus-compute/cactus},
+ year = {2025}
+}
```
-- `[model]` is a HuggingFace name from the table above (default: `google/gemma-3-270m-it`)
-- Common flags: `--precision INT4|INT8|FP16` (default: INT4), `--token `
-- Always run `source ./setup` in any new terminal.
-
-| Command | Description |
-|---------|-------------|
-| `cactus run [model]` | Opens playground (auto downloads model) |
-| `cactus download [model]` | Downloads model to `./weights` |
-| `cactus convert [model] [dir]` | Converts model, supports LoRA merging (`--lora `) |
-| `cactus build` | Builds for ARM (`--apple` or `--android`) |
-| `cactus test` | Runs tests (`--ios` / `--android`, `--model [name/path]`) |
-| `cactus clean` | Removes build artifacts |
-| `cactus --help` | Shows all commands and flags |
-
-# Using in your apps
-
-- [Python for Mac](/python/)
-- [React Native SDK](https://github.com/cactus-compute/cactus-react-native)
-- [Swift Multiplatform SDK](https://github.com/mhayes853/swift-cactus)
-- [Kotlin Multiplatform SDK](https://github.com/cactus-compute/cactus-kotlin)
-- [Flutter SDK](https://github.com/cactus-compute/cactus-flutter)
-- [Rust SDK](https://github.com/mrsarac/cactus-rs)
-
-# Try demo apps
-
-- [iOS Demo](https://apps.apple.com/gb/app/cactus-chat/id6744444212)
-- [Android Demo](https://play.google.com/store/apps/details?id=com.rshemetsubuser.myapp)
-
-# Maintaining Organisations
-1. [Cactus Compute, Inc](https://cactuscompute.com/)
-2. [UCLA's BruinAI](https://bruinai.org/)
\ No newline at end of file
+**N/B:** Scroll all the way up and click the shields link for resources!
+
+[docs-shield]: https://img.shields.io/badge/Docs-555?style=for-the-badge&logo=readthedocs&logoColor=white
+[docs-url]: https://cactus-compute.github.io/cactus/
+
+[website-shield]: https://img.shields.io/badge/Website-555?style=for-the-badge&logo=safari&logoColor=white
+[website-url]: https://cactuscompute.com/
+
+[github-shield]: https://img.shields.io/badge/GitHub-555?style=for-the-badge&logo=github&logoColor=white
+[github-url]: https://github.com/cactus-compute/cactus
+
+[hf-shield]: https://img.shields.io/badge/HuggingFace-555?style=for-the-badge&logo=huggingface&logoColor=white
+[hf-url]: https://huggingface.co/Cactus-Compute
+
+[reddit-shield]: https://img.shields.io/badge/Reddit-555?style=for-the-badge&logo=reddit&logoColor=white
+[reddit-url]: https://www.reddit.com/r/cactuscompute/
+
+[blog-shield]: https://img.shields.io/badge/Blog-555?style=for-the-badge&logo=hashnode&logoColor=white
+[blog-url]: https://cactuscompute.com/blog
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 000000000..bb3ce4b31
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,28 @@
+---
+title: "Security Policy"
+description: "How to report security vulnerabilities in Cactus, the on-device AI inference engine."
+keywords: ["security", "vulnerability", "responsible disclosure"]
+---
+
+# Security Policy
+
+## Reporting a Vulnerability
+
+If you discover a security vulnerability in Cactus, please report it responsibly. **Do not open a public GitHub issue for security vulnerabilities.**
+
+Instead, please email **security@cactuscompute.com** with:
+
+1. A description of the vulnerability.
+2. Steps to reproduce the issue.
+3. The potential impact.
+4. Any suggested fixes (optional).
+
+We will acknowledge receipt within 48 hours and aim to provide a fix or mitigation plan within 7 days.
+
+## Scope
+
+This policy covers the Cactus inference engine, its bindings (Python, Swift, Kotlin, Flutter, Rust, React Native), and the official model weights hosted on [HuggingFace](https://huggingface.co/Cactus-Compute).
+
+## Supported Versions
+
+Security fixes are applied to the latest release. We recommend always running the latest version of Cactus.
diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt
index d2260ddca..c62281b1c 100644
--- a/android/CMakeLists.txt
+++ b/android/CMakeLists.txt
@@ -1,141 +1,43 @@
cmake_minimum_required(VERSION 3.10)
-project(Cactus LANGUAGES CXX)
+project(CactusAndroid LANGUAGES CXX)
-set(CMAKE_CXX_STANDARD 20)
-set(CMAKE_CXX_STANDARD_REQUIRED True)
-
-set(SOURCE_DIR ${CMAKE_SOURCE_DIR}/../cactus)
+set(CMAKE_BUILD_TYPE Release CACHE STRING "")
+set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
-if(NOT CMAKE_BUILD_TYPE)
- set(CMAKE_BUILD_TYPE Release)
- message(STATUS "CMAKE_BUILD_TYPE was not set, defaulting to Release.")
+if(NOT ${ANDROID_ABI} STREQUAL "arm64-v8a")
+ message(FATAL_ERROR "Unsupported Android ABI: ${ANDROID_ABI}. Cactus only supports arm64-v8a.")
endif()
-file(GLOB ENGINE_SOURCES "${SOURCE_DIR}/engine/*.cpp")
-file(GLOB GRAPH_SOURCES "${SOURCE_DIR}/graph/*.cpp")
-file(GLOB KERNEL_SOURCES "${SOURCE_DIR}/kernel/*.cpp")
-file(GLOB MODEL_SOURCES "${SOURCE_DIR}/models/*.cpp")
-file(GLOB FFI_SOURCES "${SOURCE_DIR}/ffi/*.cpp")
-file(GLOB NPU_SOURCES "${SOURCE_DIR}/npu/*.cpp")
-
-set(HEADER_FILES
- ${SOURCE_DIR}/cactus.h
- ${SOURCE_DIR}/engine/engine.h
- ${SOURCE_DIR}/graph/graph.h
- ${SOURCE_DIR}/kernel/kernel.h
- ${SOURCE_DIR}/kernel/kernel_utils.h
- ${SOURCE_DIR}/ffi/cactus_ffi.h
- ${SOURCE_DIR}/ffi/cactus_utils.h
- ${SOURCE_DIR}/models/model.h
- ${SOURCE_DIR}/npu/npu.h
-)
-
-set(COMMON_SOURCES
- ${KERNEL_SOURCES}
- ${GRAPH_SOURCES}
- ${ENGINE_SOURCES}
- ${FFI_SOURCES}
- ${MODEL_SOURCES}
- ${NPU_SOURCES}
-)
-
-if(NOT COMMON_SOURCES)
- message(FATAL_ERROR "No source files found! Check SOURCE_DIR: ${SOURCE_DIR}")
+if(NOT TARGET cactus_engine)
+ add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../cactus-engine ${CMAKE_CURRENT_BINARY_DIR}/cactus-engine)
endif()
-find_library(LOG_LIB log)
-
-add_library(
- cactus
- SHARED
- ${COMMON_SOURCES}
- ${CMAKE_SOURCE_DIR}/cactus_jni.cpp
-)
-
-add_library(
- cactus_static
- STATIC
- ${COMMON_SOURCES}
-)
-
-set(COMMON_INCLUDE_DIRS
- ${SOURCE_DIR}
- ${SOURCE_DIR}/engine
- ${SOURCE_DIR}/graph
- ${SOURCE_DIR}/kernel
- ${SOURCE_DIR}/ffi
- ${SOURCE_DIR}/models
- ${SOURCE_DIR}/npu
-)
-
-target_include_directories(cactus PRIVATE ${COMMON_INCLUDE_DIRS})
-target_include_directories(cactus_static PRIVATE ${COMMON_INCLUDE_DIRS})
+set(_DEPS cactus_engine cactus_graph cactus_kernels)
-target_link_libraries(cactus ${LOG_LIB} android)
+add_library(cactus_jni SHARED ${CMAKE_CURRENT_SOURCE_DIR}/cactus_jni.cpp)
-set(COMMON_COMPILE_DEFINITIONS
- PLATFORM_CPU_ONLY=1
+set_target_properties(cactus_jni PROPERTIES
+ OUTPUT_NAME cactus_engine
+ CXX_STANDARD 20
+ CXX_STANDARD_REQUIRED ON
)
-target_compile_definitions(cactus PRIVATE ${COMMON_COMPILE_DEFINITIONS})
-target_compile_definitions(cactus_static PRIVATE ${COMMON_COMPILE_DEFINITIONS})
-
-set(COMMON_COMPILE_OPTIONS
- -pthread
- -Wall
- -Wextra
- -pedantic
- -Wno-c++20-designator
- -Wno-missing-field-initializers
- -fomit-frame-pointer
- -funroll-loops
- -ftree-vectorize
+target_include_directories(cactus_jni PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}/../cactus-engine
+ ${CMAKE_CURRENT_SOURCE_DIR}/../cactus-engine/src
)
-target_compile_options(cactus PRIVATE ${COMMON_COMPILE_OPTIONS})
-target_compile_options(cactus_static PRIVATE ${COMMON_COMPILE_OPTIONS})
-
-if (${CMAKE_BUILD_TYPE} STREQUAL "Debug")
- target_compile_options(cactus PRIVATE -DCACTUS_ANDROID_ENABLE_LOGGING)
- target_compile_options(cactus_static PRIVATE -DCACTUS_ANDROID_ENABLE_LOGGING)
-endif ()
+target_link_libraries(cactus_jni PRIVATE ${_DEPS})
-set(OPTIMIZATION_OPTIONS
- -O3
- -DNDEBUG
- -ffunction-sections
- -fdata-sections
-)
-
-target_compile_options(cactus PRIVATE ${OPTIMIZATION_OPTIONS})
-target_compile_options(cactus_static PRIVATE ${OPTIMIZATION_OPTIONS})
-
-target_compile_options(cactus PRIVATE -fvisibility=hidden -fvisibility-inlines-hidden)
-
-if (${ANDROID_ABI} STREQUAL "arm64-v8a")
- set(ARM_OPTIONS
- -march=armv8.2-a+fp16+simd+dotprod
- )
- set(ARM_DEFINITIONS
- __ARM_NEON=1
- __ARM_FEATURE_FP16_VECTOR_ARITHMETIC=1
- __ARM_FEATURE_DOTPROD=1
- )
-
- target_compile_options(cactus PRIVATE ${ARM_OPTIONS})
- target_compile_options(cactus_static PRIVATE ${ARM_OPTIONS})
-
- target_compile_definitions(cactus PRIVATE ${ARM_DEFINITIONS})
- target_compile_definitions(cactus_static PRIVATE ${ARM_DEFINITIONS})
-else()
- message(FATAL_ERROR "Unsupported Android ABI: ${ANDROID_ABI}. Cactus only supports arm64-v8a due to ARM NEON requirements.")
+set(_CACTUS_VERSION_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../CACTUS_VERSION")
+if(EXISTS "${_CACTUS_VERSION_FILE}")
+ file(READ "${_CACTUS_VERSION_FILE}" _CACTUS_VERSION_CONTENT)
+ string(STRIP "${_CACTUS_VERSION_CONTENT}" CACTUS_VERSION_STR)
+ target_compile_definitions(cactus_jni PRIVATE CACTUS_COMPILE_TIME_VERSION="${CACTUS_VERSION_STR}")
endif()
-target_link_options(cactus PRIVATE -Wl,--gc-sections)
-target_link_options(cactus PRIVATE -Wl,--exclude-libs,ALL)
-target_link_options(cactus PRIVATE -flto)
-target_link_options(cactus PRIVATE -Wl,-z,max-page-size=16384)
+if(CMAKE_BUILD_TYPE STREQUAL "Debug")
+ target_compile_definitions(cactus_jni PRIVATE CACTUS_ANDROID_ENABLE_LOGGING)
+endif()
-set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
-set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
-set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
+target_link_options(cactus_jni PRIVATE -Wl,-z,max-page-size=16384)
diff --git a/android/Cactus.android.kt b/android/Cactus.android.kt
deleted file mode 100644
index 3a6e302ca..000000000
--- a/android/Cactus.android.kt
+++ /dev/null
@@ -1,387 +0,0 @@
-package com.cactus
-
-import org.json.JSONArray
-import org.json.JSONObject
-
-actual class Cactus private constructor(private var handle: Long) : AutoCloseable {
-
- actual companion object {
- init {
- System.loadLibrary("cactus")
- }
-
- actual fun create(modelPath: String, corpusDir: String?): Cactus {
- val handle = nativeInit(modelPath, corpusDir)
- if (handle == 0L) {
- throw CactusException(nativeGetLastError().ifEmpty { "Failed to initialize model" })
- }
- return Cactus(handle)
- }
-
- actual fun setTelemetryToken(token: String) = nativeSetTelemetryToken(token)
- actual fun setProKey(key: String) = nativeSetProKey(key)
-
- @JvmStatic
- private external fun nativeInit(modelPath: String, corpusDir: String?): Long
- @JvmStatic
- private external fun nativeGetLastError(): String
- @JvmStatic
- private external fun nativeSetTelemetryToken(token: String)
- @JvmStatic
- private external fun nativeSetProKey(key: String)
- }
-
- actual fun complete(prompt: String, options: CompletionOptions): CompletionResult {
- return complete(listOf(Message.user(prompt)), options, null, null)
- }
-
- actual fun complete(
- messages: List,
- options: CompletionOptions,
- tools: List