diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5ff4c3958..e05a224e4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,7 +3,6 @@ name: Build on: pull_request: paths: - - cactus/** - cactus-engine/** - cactus-graph/** - cactus-kernels/** @@ -13,7 +12,6 @@ on: push: branches: [main] paths: - - cactus/** - cactus-engine/** - cactus-graph/** - cactus-kernels/** @@ -25,13 +23,17 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: apple: runs-on: macos-15 - timeout-minutes: 15 + timeout-minutes: 30 steps: - uses: actions/checkout@v4 - - run: bash cactus/build.sh + - run: bash cactus-engine/build.sh - run: BUILD_STATIC=true BUILD_XCFRAMEWORK=false bash apple/build.sh - run: bash android/build.sh @@ -41,4 +43,4 @@ jobs: steps: - uses: actions/checkout@v4 - run: sudo apt-get update && sudo apt-get install -y cmake build-essential libcurl4-openssl-dev - - run: bash cactus/build.sh + - run: bash cactus-engine/build.sh diff --git a/.github/workflows/cpp.yml b/.github/workflows/cpp.yml index 636bb764b..a75ad5312 100644 --- a/.github/workflows/cpp.yml +++ b/.github/workflows/cpp.yml @@ -1,23 +1,29 @@ +name: Unit Tests + on: pull_request: paths: - - cactus/** - - tests/** + - cactus-kernels/** + - cactus-graph/** - .github/workflows/cpp.yml push: branches: [main] paths: - - cactus/** - - tests/** + - cactus-kernels/** + - cactus-graph/** - .github/workflows/cpp.yml workflow_dispatch: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: - test: - timeout-minutes: 45 + unit-tests: + timeout-minutes: 30 strategy: fail-fast: false matrix: @@ -30,17 +36,7 @@ jobs: steps: - uses: actions/checkout@v4 - run: ${{ matrix.deps }} - - uses: actions/cache@v4 - with: - path: ~/.ccache - key: ccache-${{ matrix.os }}-${{ github.ref_name }}-${{ hashFiles('cactus/CMakeLists.txt', 'tests/CMakeLists.txt', '.github/workflows/cpp.yml') }} - restore-keys: | - ccache-${{ matrix.os }}-${{ github.ref_name }}- - ccache-${{ matrix.os }}- - - run: ccache --zero-stats || true - - run: cmake -G Ninja -S cactus -B cactus/build -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER_LAUNCHER=ccache - - run: cmake --build cactus/build --target cactus_ffi --parallel 4 - - run: cmake -G Ninja -S tests -B tests/build-ci -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER_LAUNCHER=ccache - - run: cmake --build tests/build-ci --target test_stt --parallel 4 - - run: ./tests/build-ci/test_stt - - run: ccache --show-stats || true + - 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 ed73abd3e..fdbf6d45c 100644 --- a/.github/workflows/dco.yml +++ b/.github/workflows/dco.yml @@ -4,9 +4,17 @@ 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 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index fdd9e45fa..b084bbd51 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -28,9 +28,17 @@ on: required: false workflow_dispatch: +permissions: + contents: read + +concurrency: + group: docs-${{ github.ref }} + cancel-in-progress: false + jobs: deploy: runs-on: ubuntu-latest + timeout-minutes: 15 permissions: contents: write steps: @@ -41,7 +49,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: "3.x" + python-version: "3.12" - name: Install dependencies run: pip install "mkdocs==1.6.1" "mkdocs-material==9.7.5" "mike==2.1.4" diff --git a/.github/workflows/inference.yml b/.github/workflows/inference.yml index 47c49cb40..10241bbfa 100644 --- a/.github/workflows/inference.yml +++ b/.github/workflows/inference.yml @@ -8,9 +8,14 @@ on: permissions: contents: 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: @@ -28,6 +33,7 @@ jobs: needs: gate if: needs.gate.outputs.approved == 'true' runs-on: [self-hosted, linux, arm64] + timeout-minutes: 60 steps: - uses: actions/checkout@v4 with: @@ -39,77 +45,35 @@ jobs: source ./setup fi - - name: Convert models - env: - HF_TOKEN: ${{ secrets.HF_TOKEN }} - run: | - source venv/bin/activate - convert() { - local id=$1 dir=$2 bits=$3 - [ -f "weights/$dir/config.txt" ] && return - python3 -m cactus convert "$id" --bits "$bits" ${HF_TOKEN:+--token "$HF_TOKEN"} || true - } - convert "Qwen/Qwen3-0.6B" "qwen3-0.6b" "4" - convert "google/gemma-3-270m-it" "gemma-3-270m-it" "4" - convert "openai/whisper-small" "whisper-small" "4" - convert "nvidia/parakeet-tdt-0.6b-v3" "parakeet-tdt-0.6b-v3" "4" - - - name: Build + - name: Build libcactus + engine tests run: | - bash cactus/build.sh + 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) - unit-tests: - needs: build - runs-on: [self-hosted, linux, arm64] - env: - CACTUS_INDEX_PATH: /tmp/cactus_test_index - CACTUS_NO_CLOUD_TELE: "1" - steps: - - uses: actions/checkout@v4 - with: - clean: false - - name: Run unit tests - run: | - mkdir -p "$CACTUS_INDEX_PATH" - cd cactus-engine/tests/build - ./test_index - ./test_model_loading - ./test_telemetry - - - name: Run kernel tests - run: bash cactus-kernels/test.sh - - - name: Run graph tests - run: bash cactus-graph/test.sh - - inference-tests: + model-tests: needs: build runs-on: [self-hosted, linux, arm64] + timeout-minutes: 60 env: - CACTUS_TEST_ASSETS: ${{ github.workspace }}/cactus-engine/tests/assets CACTUS_NO_CLOUD_TELE: "1" strategy: fail-fast: false matrix: - include: - - test: test_llm - model_dir: qwen3-0.6b - - test: test_llm - model_dir: gemma-3-270m-it - - test: test_stt - model_dir: whisper-small - - test: test_stt - model_dir: parakeet-tdt-0.6b-v3 + model: + - LiquidAI/LFM2-VL-450M + - openai/whisper-small steps: - uses: actions/checkout@v4 with: clean: false - - name: Run ${{ matrix.test }} (${{ matrix.model_dir }}) + + - name: Run tests for ${{ matrix.model }} env: - CACTUS_TEST_MODEL: ${{ github.workspace }}/weights/${{ matrix.model_dir }} - CACTUS_TEST_TRANSCRIBE_MODEL: ${{ github.workspace }}/weights/${{ matrix.model_dir }} - run: ./cactus-engine/tests/build/${{ matrix.test }} + 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_to_hf.yml b/.github/workflows/publish_to_hf.yml deleted file mode 100644 index 5100a4535..000000000 --- a/.github/workflows/publish_to_hf.yml +++ /dev/null @@ -1,153 +0,0 @@ -name: Publish to Hugging Face - -on: - release: - types: [published] - workflow_dispatch: - inputs: - model: - description: Model to publish (leave empty to publish all) - required: false - type: string - version: - description: Version tag - required: true - type: string - org: - description: Organization - required: false - default: Cactus-Compute - type: string - int4: - description: Export INT4 - required: false - type: boolean - default: false - int8: - description: Export INT8 - required: false - type: boolean - default: false - fp16: - description: Export FP16 - required: false - type: boolean - default: false - apple: - description: Export Apple weights - required: false - type: boolean - default: false - pipeline_tag: - description: Pipeline tag (e.g. text-generation) - required: false - type: string - tags: - description: Comma-separated tags (e.g. completion,tools) - required: false - type: string - description: - description: Model description - required: false - type: string - branch: - description: Branch override (uses version tag if empty) - required: false - type: string - -jobs: - publish: - runs-on: macos-latest - steps: - - name: Get version - id: version - run: | - if [ "${{ github.event_name }}" == "release" ]; then - echo "tag=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT - else - echo "tag=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT - fi - - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.inputs.branch || steps.version.outputs.tag }} - - - 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' - - - name: Install dependencies - run: pip install -r python/requirements.txt - - - name: Publish models - run: | - cd python - ORG="${{ github.event.inputs.org || 'Cactus-Compute' }}" - VERSION="${{ steps.version.outputs.tag }}" - - publish_model() { - local model=$1 int4=$2 int8=$3 fp16=$4 apple=$5 pipeline_tag=$6 tags=$7 description="${8:-}" - local FLAGS=(--version "$VERSION" --org "$ORG" --model "$model") - [ "$int4" == "true" ] && FLAGS+=(--int4) - [ "$int8" == "true" ] && FLAGS+=(--int8) - [ "$fp16" == "true" ] && FLAGS+=(--fp16) - [ "$apple" == "true" ] && FLAGS+=(--apple) - [ -n "$pipeline_tag" ] && FLAGS+=(--pipeline-tag "$pipeline_tag") - [ -n "$tags" ] && FLAGS+=(--tags "$tags") - set +e - python -m src.publish_to_hf --task export_model "${FLAGS[@]}" --description "$description" - local rc=$? - set -e - return $rc - } - - if [ -n "${{ github.event.inputs.model }}" ]; then - publish_model \ - "${{ github.event.inputs.model }}" \ - "${{ github.event.inputs.int4 }}" \ - "${{ github.event.inputs.int8 }}" \ - "${{ github.event.inputs.fp16 }}" \ - "${{ github.event.inputs.apple }}" \ - "${{ github.event.inputs.pipeline_tag }}" \ - "${{ github.event.inputs.tags }}" \ - "${{ github.event.inputs.description }}" - else - FAILED_MODELS=() - while read -r config; do - model=$(echo "$config" | jq -r '.model') - int4=$(echo "$config" | jq -r '.int4') - int8=$(echo "$config" | jq -r '.int8') - fp16=$(echo "$config" | jq -r '.fp16') - apple=$(echo "$config" | jq -r '.apple') - pipeline_tag=$(echo "$config" | jq -r '.pipeline_tag // ""') - tags=$(echo "$config" | jq -r 'if .tags then .tags | join(",") else "" end') - description=$(echo "$config" | jq -r '.description // ""') - if ! publish_model "$model" "$int4" "$int8" "$fp16" "$apple" "$pipeline_tag" "$tags" "$description"; then - echo "::error::Failed to publish $model" - FAILED_MODELS+=("$model") - fi - done < <(jq -c '.[]' "$GITHUB_WORKSPACE/models.json") - - if [ ${#FAILED_MODELS[@]} -gt 0 ]; then - echo "::error::Failed models: ${FAILED_MODELS[*]}" - exit 1 - fi - fi - env: - HF_TOKEN: ${{ secrets.HF_TOKEN }} - HF_HUB_DISABLE_XET: "1" - - - name: Update organization README - if: "!github.event.inputs.model" - run: | - cd python - ORG="${{ github.event.inputs.org || 'Cactus-Compute' }}" - python -m src.publish_to_hf --task update_org_readme --org "$ORG" - env: - HF_TOKEN: ${{ secrets.HF_TOKEN }} diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index e7c3c0c51..5b62c06ee 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -9,9 +9,13 @@ on: required: false type: string +permissions: + contents: read + jobs: wheels-macos: runs-on: macos-26 + timeout-minutes: 45 env: MACOSX_DEPLOYMENT_TARGET: "14.0" steps: @@ -28,7 +32,7 @@ jobs: pip install -e python/ cactus build mkdir -p python/cactus/bindings/lib - cp cactus/build/libcactus.dylib python/cactus/bindings/lib/ + cp cactus-engine/build/libcactus_engine.dylib python/cactus/bindings/lib/ - name: Build and repair wheel working-directory: python @@ -44,6 +48,7 @@ jobs: wheels-linux: runs-on: ubuntu-24.04-arm + timeout-minutes: 45 container: image: quay.io/pypa/manylinux_2_28_aarch64 steps: @@ -61,7 +66,7 @@ jobs: pip install -e python/ cactus build mkdir -p python/cactus/bindings/lib - cp cactus/build/libcactus.so python/cactus/bindings/lib/ + cp cactus-engine/build/libcactus_engine.so python/cactus/bindings/lib/ - name: Build and repair wheel working-directory: python @@ -77,6 +82,7 @@ jobs: sdist: runs-on: ubuntu-24.04 + timeout-minutes: 10 steps: - uses: actions/checkout@v4 @@ -100,6 +106,7 @@ jobs: publish: needs: [wheels-macos, wheels-linux, sdist] runs-on: ubuntu-24.04 + timeout-minutes: 15 permissions: id-token: write steps: diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 5e0672ca4..ab063e29e 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -10,10 +10,19 @@ on: 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 @@ -21,7 +30,7 @@ jobs: run: sudo apt-get update && sudo apt-get install -y cmake build-essential libcurl4-openssl-dev - name: Build libcactus - run: bash cactus/build.sh + run: bash cactus-engine/build.sh - uses: actions/setup-python@v5 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 217ccac37..325e2db38 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,9 @@ on: release: types: [published] +permissions: + contents: read + concurrency: group: release-${{ github.ref }} cancel-in-progress: false @@ -18,6 +21,7 @@ jobs: homebrew: needs: pypi runs-on: ubuntu-24.04 + timeout-minutes: 15 env: PACKAGE: cactus-compute TAP: cactus-compute/homebrew-cactus @@ -65,6 +69,8 @@ jobs: license "Cactus Compute License" include Language::Python::Virtualenv depends_on "python@3.12" + depends_on macos: :ventura + depends_on arch: :arm64 livecheck do url "https://pypi.org/pypi/${PACKAGE}/json" diff --git a/.github/workflows/verify-pr.yml b/.github/workflows/verify-pr.yml deleted file mode 100644 index 015bc07cd..000000000 --- a/.github/workflows/verify-pr.yml +++ /dev/null @@ -1,225 +0,0 @@ -name: PR Verification -# SETUP -# 1. System packages: -# sudo apt-get install -y build-essential libcurl4-openssl-dev cmake libicu-dev -# 2. Python 3.12 via Homebrew (deadsnakes PPA doesn't support Ubuntu 25.10): -# # Install Homebrew -# NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - -# # Install Python 3.12 -# eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" -# brew install python@3.12 - -# # Make it available system-wide -# sudo ln -sf /home/linuxbrew/.linuxbrew/bin/python3.12 /usr/local/bin/python3.12 - -# GitHub Actions Runner Installation (already done) - -# mkdir -p ~/actions-runner && cd ~/actions-runner -# curl -sO -L https://github.com/actions/runner/releases/download/v2.331.0/actions-runner-linux-arm64-2.331.0.tar.gz -# tar xzf actions-runner-linux-arm64-2.331.0.tar.gz -# sudo ./bin/installdependencies.sh - -# Configure Runner for a Repo (repeat this for each repo) - -# 1. Remove old runner config (if switching repos): -# cd ~/actions-runner -# sudo ./svc.sh stop -# sudo ./svc.sh uninstall -# ./config.sh remove --token -# 1. Get the remove token from the old repo: Settings → Actions → Runners → click runner → Remove. -# 2. Get new token: Go to the target repo (e.g. cactus-compute/cactus) → Settings → Actions → Runners → New self-hosted runner → Linux / ARM64. Copy the token from the ./config.sh command -# shown. -# 3. Configure: -# cd ~/actions-runner -# ./config.sh --url https://github.com/cactus-compute/cactus --token -# 3. Accept defaults for runner group, name, and labels (it auto-detects self-hosted, linux, ARM64). -# 4. Start as service: -# sudo ./svc.sh install -# sudo ./svc.sh start -# 5. Add repo secret: Go to the target repo → Settings → Secrets and variables → Actions → New repository secret → Name: HF_TOKEN, Value: -# 6. Verify: The runner should appear as "Online" in the repo's Settings → Actions → Runners. Trigger the workflow from Actions → PR Verification → Run workflow. - -on: - pull_request_review: - types: [submitted] - workflow_dispatch: - -jobs: - gate: - runs-on: ubuntu-latest - 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 - - build: - needs: gate - if: needs.gate.outputs.approved == 'true' - runs-on: [self-hosted, linux, arm64] - steps: - - uses: actions/checkout@v4 - with: - clean: false - - - name: Check for Python changes - id: check_python - env: - GH_TOKEN: ${{ github.token }} - run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "python_changed=false" >> $GITHUB_OUTPUT - else - CHANGED=$(gh pr diff ${{ github.event.pull_request.number }} --name-only | grep -cE '^python/(src/|requirements\.txt)' || true) - echo "python_changed=$([[ $CHANGED -gt 0 ]] && echo true || echo false)" >> $GITHUB_OUTPUT - fi - - - name: Ensure venv exists - run: | - if [ ! -f venv/bin/activate ]; then - echo "No venv found, running setup..." - source ./setup - fi - - - name: Rebuild venv if Python sources changed - if: steps.check_python.outputs.python_changed == 'true' - run: | - deactivate || true - rm -rf venv - source ./setup - - - name: Purge cached weights if converters changed - if: steps.check_python.outputs.python_changed == 'true' - run: rm -rf weights/ - - - name: Convert models if weights missing - env: - HF_TOKEN: ${{ secrets.HF_TOKEN }} - run: | - source venv/bin/activate - convert_if_missing() { - local model_id=$1 output_dir=$2 precision=$3 - if [ ! -f "weights/$output_dir/config.txt" ]; then - echo "Converting $model_id -> weights/$output_dir" - python3 -m src.cli convert "$model_id" "weights/$output_dir" --precision "$precision" ${HF_TOKEN:+--token "$HF_TOKEN"} || echo "WARNING: conversion of $model_id failed (exit $?), checking fallback directory" - local default_dir="weights/$(echo "$model_id" | sed 's|.*/||' | tr '[:upper:]' '[:lower:]')" - if [ ! -f "weights/$output_dir/config.txt" ] && [ -f "$default_dir/config.txt" ]; then - mv "$default_dir" "weights/$output_dir" - fi - fi - } - convert_if_missing "Qwen/Qwen3-0.6B" "qwen3-0.6b" "INT8" - convert_if_missing "nomic-ai/nomic-embed-text-v2-moe" "nomic-embed-text-v2-moe" "INT8" - convert_if_missing "LiquidAI/LFM2.5-350M" "lfm2-350m" "INT8" - convert_if_missing "LiquidAI/LFM2-8B-A1B" "lfm2-8b-a1b" "INT8" - convert_if_missing "google/gemma-3-270m-it" "gemma-3-270m-it" "INT8" - convert_if_missing "openai/whisper-small" "whisper-small" "INT8" - convert_if_missing "UsefulSensors/moonshine-base" "moonshine-base" "INT8" - convert_if_missing "LiquidAI/LFM2-VL-450M" "lfm2-vl-450m-int8" "INT8" - convert_if_missing "LiquidAI/LFM2-VL-450M" "lfm2-vl-450m-int4" "INT4" - convert_if_missing "LiquidAI/LFM2-VL-450M" "lfm2-vl-450m-fp16" "FP16" - - - name: Build library - run: bash cactus/build.sh - - - name: Build tests - run: | - cd tests - rm -rf build && mkdir -p build && cd build - cmake .. -DCMAKE_RULE_MESSAGES=OFF -DCMAKE_VERBOSE_MAKEFILE=OFF - make -j$(nproc) - - unit-tests: - needs: build - runs-on: [self-hosted, linux, arm64] - steps: - - uses: actions/checkout@v4 - with: - clean: false - - name: Run unit tests - env: - CACTUS_INDEX_PATH: ${{ runner.temp }}/cactus_index - CACTUS_NO_CLOUD_TELE: "1" - run: | - mkdir -p "$CACTUS_INDEX_PATH" - FAILED=0 - for test in test_graph test_kernel test_kv_cache test_index test_model_loading; do - echo "=== Running $test ===" - if ! ./tests/build/$test; then - FAILED=$((FAILED + 1)) - fi - done - if [ $FAILED -gt 0 ]; then exit 1; fi - - exhaustive-tests: - needs: build - runs-on: [self-hosted, linux, arm64] - strategy: - fail-fast: false - matrix: - include: - - family: qwen - model_dir: qwen3-0.6b - env_var: CACTUS_TEST_MODEL - precision: INT8 - - family: nomic - model_dir: nomic-embed-text-v2-moe - env_var: CACTUS_TEST_MODEL - precision: INT8 - - family: lfm2 - model_dir: lfm2-350m - env_var: CACTUS_TEST_MODEL - precision: INT8 - - family: lfm2moe - model_dir: lfm2-8b-a1b - env_var: CACTUS_TEST_MODEL - precision: INT8 - - family: gemma - model_dir: gemma-3-270m-it - env_var: CACTUS_TEST_MODEL - precision: INT8 - - family: whisper - model_dir: whisper-small - env_var: CACTUS_TEST_TRANSCRIBE_MODEL - precision: INT8 - - family: moonshine - model_dir: moonshine-base - env_var: CACTUS_TEST_TRANSCRIBE_MODEL - precision: INT8 - - family: lfm2vl - model_dir: lfm2-vl-450m-int8 - env_var: CACTUS_TEST_MODEL - precision: INT8 - - family: lfm2vl - model_dir: lfm2-vl-450m-int4 - env_var: CACTUS_TEST_MODEL - precision: INT4 - - family: lfm2vl - model_dir: lfm2-vl-450m-fp16 - env_var: CACTUS_TEST_MODEL - precision: FP16 - steps: - - uses: actions/checkout@v4 - with: - clean: false - - name: Run exhaustive test - env: - CACTUS_NO_CLOUD_TELE: "1" - run: | - export ${{ matrix.env_var }}="${{ github.workspace }}/weights/${{ matrix.model_dir }}" - export CACTUS_TEST_ASSETS="${{ github.workspace }}/tests/assets" - export CACTUS_TEST_GOLDEN_FILE="${{ github.workspace }}/tests/golden/golden.json" - export CACTUS_TEST_GOLDEN_FAMILY="${{ matrix.family }}" - export CACTUS_TEST_GOLDEN_PRECISION="${{ matrix.precision }}" - if [ "${{ matrix.family }}" = "whisper" ] || [ "${{ matrix.family }}" = "moonshine" ]; then - export CACTUS_TEST_VAD_MODEL="${{ github.workspace }}/weights/silero-vad" - fi - ./tests/build/test_exhaustive diff --git a/.gitignore b/.gitignore index bdd0d1517..743d97d84 100644 --- a/.gitignore +++ b/.gitignore @@ -1,26 +1,15 @@ **/build/ -**/build_inc/ *.a *.so *.dylib *.bin -*.gguf -*.npy *.cg !cactus-engine/libs/curl/**/*.a -!libs/curl/**/*.a !android/mbedtls/**/*.a apple/build-*/ apple/*.xcframework -apple/libcactus*.a -android/libcactus.so -android/libcactus.a -android/libs/ -android/arm64-v8a/ -bindings/flutter/libcactus.so -bindings/flutter/*.xcframework .claude .vscode @@ -29,24 +18,13 @@ bindings/flutter/*.xcframework __pycache__/ *.pyc *.egg-info +.pytest_cache/ venv weights/ -stage/ +transpiled/ python/dist/ python/cactus/bin/ python/cactus/bindings/lib/ -.cache/ -tmp site/ site_docs/ - -notes.txt -profile.txt -*_profile.txt -tests/plan.md -tests/assets/test_wav_info.txt -vision_activations.json -gemma-4-eap-extras/ -.githooks/ -ruy diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e69de29bb..000000000 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bfff37112..36b5fab1e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ Thank you for your interest in contributing to Cactus! This document covers the 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. Run `cactus test --performance` if your changes affect performance-critical paths. +4. Run `cactus test --suite performance` if your changes affect performance-critical paths. 5. Update documentation if your changes affect the public API or user-facing behavior. 6. Submit a pull request with a clear description of what you changed and why. @@ -41,10 +41,10 @@ cactus test --ios cactus test --android # Run performance tests -cactus test --performance +cactus test --suite performance # Test a specific model -cactus test --model LiquidAI/LFM2.5-1.2B-Instruct +cactus test --model LiquidAI/LFM2-VL-450M ``` ## Developer Certificate of Origin diff --git a/README.md b/README.md index 11a119144..979c43976 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ A low-latency AI engine for mobile devices & wearables. ## Cactus Engine ```cpp -#include "cactus.h" +#include "cactus_engine.h" cactus_model_t model = cactus_init( "path/to/weight/folder", @@ -102,7 +102,7 @@ Example response from Gemma3-270m ## Cactus Graph ```cpp -#include "cactus.h" +#include "cactus_graph.h" CactusGraph graph; auto a = graph.input({2, 3}, Precision::FP16); @@ -227,67 +227,84 @@ cactus build # default static lib ## Using this repo ``` -┌──────────────────────────────────────────────────────────────────────────────┐ -│ │ -│ Step 0: if on Linux (Ubuntu/Debian) │ -│ sudo apt-get install python3 python3-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] chat playground (gemma-4-E2B-it) │ -│ --image image file for VLM inference │ -│ --audio audio file (WAV) for audio chat │ -│ --system system prompt │ -│ --prompt send prompt immediately │ -│ --thinking enable thinking/reasoning mode │ -│ --token HF token (gated models) │ -│ --reconvert force reconversion from source │ -│ │ -│ cactus transcribe [model] speech-to-text (parakeet-tdt-0.6b-v3) │ -│ --file audio file to transcribe (required) │ -│ --language language code (default: en) │ -│ --token HF token (gated models) │ -│ --reconvert force reconversion from source │ -│ │ -│ cactus download fetch pre-converted CQ from Cactus-Compute │ -│ --bits 1|2|3|4 CQ quantization (default: 4) │ -│ --token HuggingFace API token │ -│ │ -│ cactus convert [dir] convert model to CQ format │ -│ (pre-converted if available, else │ -│ built from source) │ -│ --bits 1|2|3|4 CQ quantization (default: 4) │ -│ --token HuggingFace API token │ -│ --reconvert force build from source │ -│ │ -│ cactus build build for ARM → build/libcactus.a │ -│ --apple Apple (iOS/macOS) │ -│ --android Android │ -│ --python shared lib for Python FFI │ -│ │ -│ cactus test run unit tests and benchmarks │ -│ --model default: google/gemma-4-E2B-it │ -│ --suite run a specific suite (llm, vlm, stt, │ -│ embed, rag, graph, index, kernel, etc.) │ -│ --token HuggingFace API token │ -│ --reconvert force reconversion from source │ -│ --ios run on connected iPhone │ -│ --android run on connected Android │ -│ │ -│ cactus clean remove all build artifacts │ -│ cactus --help show all commands and flags │ -│ │ -└──────────────────────────────────────────────────────────────────────────────┘ +┌────────────────────────────────────────────────────────────────────────────────┐ +│ │ +│ Step 0: if on Linux (Ubuntu/Debian) │ +│ sudo apt-get install python3 python3-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 run a model (downloads if needed) │ +│ --bits 1|2|3|4 CQ quantization (default: 4) │ +│ --platform cpu|apple target accelerator (default: cpu) │ +│ --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 convert+transpile fallback │ +│ │ +│ cactus transcribe [model] transcribe audio with a model │ +│ --file audio file to transcribe (required) │ +│ --language language code (default: en) │ +│ --token HuggingFace token (gated models) │ +│ --reconvert force reconversion from source │ +│ │ +│ cactus download download a pre-built bundle │ +│ --bits 1|2|3|4 CQ quantization (default: 4) │ +│ --platform cpu|apple target accelerator (default: cpu) │ +│ --token HuggingFace token │ +│ │ +│ cactus convert [dir] convert HuggingFace weights to CQ │ +│ --bits 1|2|3|4 CQ quantization (default: 4) │ +│ --token HuggingFace token │ +│ --reconvert force build from source │ +│ --lora merge a LoRA adapter before converting │ +│ │ +│ cactus transpile build a runnable bundle from CQ weights │ +│ --weights-dir path to CQ weights (default: lookup) │ +│ --task force task type (default: auto) │ +│ --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) │ +│ │ +│ cactus list list local converted weights and bundles │ +│ │ +│ 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: LiquidAI/LFM2-VL-450M │ +│ --transcription-model default: openai/whisper-base │ +│ --suite run a single test suite (resolved │ +│ across components; e.g. performance │ +│ → kernels + graph, 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 clean delete build artifacts │ +│ cactus --help show this help │ +│ │ +└────────────────────────────────────────────────────────────────────────────────┘ ``` ## Maintaining Organisations diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index db6c1f472..bf1165792 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -8,13 +8,8 @@ if(NOT ${ANDROID_ABI} STREQUAL "arm64-v8a") message(FATAL_ERROR "Unsupported Android ABI: ${ANDROID_ABI}. Cactus only supports arm64-v8a.") endif() -set(CACTUS_MBEDTLS_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/mbedtls/${ANDROID_ABI}") -if(NOT EXISTS "${CACTUS_MBEDTLS_ROOT}") - set(CACTUS_MBEDTLS_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../libs/mbedtls/android/${ANDROID_ABI}") -endif() - if(NOT TARGET cactus_engine) - add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../cactus ${CMAKE_CURRENT_BINARY_DIR}/cactus) + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../cactus-engine ${CMAKE_CURRENT_BINARY_DIR}/cactus-engine) endif() set(_DEPS cactus_engine cactus_graph cactus_kernels) @@ -22,31 +17,18 @@ set(_DEPS cactus_engine cactus_graph cactus_kernels) add_library(cactus_jni SHARED ${CMAKE_CURRENT_SOURCE_DIR}/cactus_jni.cpp) set_target_properties(cactus_jni PROPERTIES - OUTPUT_NAME cactus + OUTPUT_NAME cactus_engine CXX_STANDARD 20 CXX_STANDARD_REQUIRED ON ) target_include_directories(cactus_jni PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/../cactus ${CMAKE_CURRENT_SOURCE_DIR}/../cactus-engine ${CMAKE_CURRENT_SOURCE_DIR}/../cactus-engine/src ) target_link_libraries(cactus_jni PRIVATE ${_DEPS}) -find_library(LOG_LIB log) -target_link_libraries(cactus_jni PRIVATE ${LOG_LIB} android) - -set(_MBEDTLS "${CACTUS_MBEDTLS_ROOT}/lib/libmbedtls.a") -set(_MBEDX509 "${CACTUS_MBEDTLS_ROOT}/lib/libmbedx509.a") -set(_MBEDCRYPTO "${CACTUS_MBEDTLS_ROOT}/lib/libmbedcrypto.a") -if(EXISTS "${_MBEDTLS}" AND EXISTS "${_MBEDX509}" AND EXISTS "${_MBEDCRYPTO}") - target_link_libraries(cactus_jni PRIVATE "${_MBEDTLS}" "${_MBEDX509}" "${_MBEDCRYPTO}") -endif() - -target_compile_definitions(cactus_jni PRIVATE PLATFORM_CPU_ONLY=1) - set(_CACTUS_VERSION_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../CACTUS_VERSION") if(EXISTS "${_CACTUS_VERSION_FILE}") file(READ "${_CACTUS_VERSION_FILE}" _CACTUS_VERSION_CONTENT) diff --git a/android/README.md b/android/README.md index 2d8051dfc..fa73fa740 100644 --- a/android/README.md +++ b/android/README.md @@ -1,6 +1,6 @@ # Android Build -Builds `libcactus` for Android (arm64-v8a). +Builds `libcactus_engine` for Android (arm64-v8a). ## Usage @@ -16,8 +16,8 @@ bash android/build.sh ## Output -- `libcactus.so` — Shared library (JNI, for Android apps) -- `libcactus.a` — Static library (for native test binaries) +- `libcactus_engine.so` — Shared library (JNI, for Android apps) +- `libcactus_engine.a` — Static library (for native test binaries) ## Options diff --git a/android/build.sh b/android/build.sh index 664c02423..b139e6230 100755 --- a/android/build.sh +++ b/android/build.sh @@ -1,4 +1,5 @@ -#!/bin/bash -e +#!/bin/bash +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" @@ -9,22 +10,22 @@ CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE:-Release} BUILD_DIR="$ANDROID_DIR/build" CACTUS_CURL_ROOT=${CACTUS_CURL_ROOT:-"$PROJECT_ROOT/cactus-engine/libs/curl"} -if [ -z "$ANDROID_NDK_HOME" ]; then - if [ -n "$ANDROID_HOME" ]; then +if [ -z "${ANDROID_NDK_HOME:-}" ]; then + if [ -n "${ANDROID_HOME:-}" ]; then ANDROID_NDK_HOME=$(ls -d "$ANDROID_HOME/ndk/"* 2>/dev/null | sort -V | tail -1) fi - if [ -z "$ANDROID_NDK_HOME" ] && [ -d "$HOME/Library/Android/sdk" ]; then + if [ -z "${ANDROID_NDK_HOME:-}" ] && [ -d "$HOME/Library/Android/sdk" ]; then ANDROID_NDK_HOME=$(ls -d "$HOME/Library/Android/sdk/ndk/"* 2>/dev/null | sort -V | tail -1) fi - if [ -z "$ANDROID_NDK_HOME" ] && [ -d "/opt/homebrew/Caskroom/android-ndk" ]; then + if [ -z "${ANDROID_NDK_HOME:-}" ] && [ -d "/opt/homebrew/Caskroom/android-ndk" ]; then ANDROID_NDK_HOME=$(ls -d /opt/homebrew/Caskroom/android-ndk/*/AndroidNDK*.app/Contents/NDK 2>/dev/null | sort -V | tail -1) fi - if [ -z "$ANDROID_NDK_HOME" ] && [ -d "/opt/homebrew/share/android-ndk" ]; then + if [ -z "${ANDROID_NDK_HOME:-}" ] && [ -d "/opt/homebrew/share/android-ndk" ]; then ANDROID_NDK_HOME="/opt/homebrew/share/android-ndk" fi fi -if [ -z "$ANDROID_NDK_HOME" ] || [ ! -d "$ANDROID_NDK_HOME" ]; then +if [ -z "${ANDROID_NDK_HOME:-}" ] || [ ! -d "$ANDROID_NDK_HOME" ]; then echo "Error: Android NDK not found." echo "Set ANDROID_NDK_HOME or install NDK via Android SDK Manager" exit 1 @@ -58,14 +59,14 @@ cmake -DCMAKE_TOOLCHAIN_FILE="$CMAKE_TOOLCHAIN_FILE" \ cmake --build "$BUILD_DIR" --config "$CMAKE_BUILD_TYPE" -j "$n_cpu" >/dev/null -cp "$BUILD_DIR/lib/libcactus.so" "$ANDROID_DIR/" 2>/dev/null || \ - cp "$BUILD_DIR/libcactus.so" "$ANDROID_DIR" 2>/dev/null || \ - { echo "Error: Could not find libcactus.so"; exit 1; } +cp "$BUILD_DIR/lib/libcactus_engine.so" "$ANDROID_DIR/" 2>/dev/null || \ + cp "$BUILD_DIR/libcactus_engine.so" "$ANDROID_DIR" 2>/dev/null || \ + { echo "Error: Could not find libcactus_engine.so"; exit 1; } -cp "$BUILD_DIR/lib/libcactus.a" "$ANDROID_DIR/libcactus.a" 2>/dev/null || \ - cp "$BUILD_DIR/libcactus.a" "$ANDROID_DIR/libcactus.a" 2>/dev/null || \ - { echo "Warning: Could not find libcactus.a"; } +cp "$BUILD_DIR/lib/libcactus_engine.a" "$ANDROID_DIR/libcactus_engine.a" 2>/dev/null || \ + cp "$BUILD_DIR/libcactus_engine.a" "$ANDROID_DIR/libcactus_engine.a" 2>/dev/null || \ + { echo "Warning: Could not find libcactus_engine.a"; } echo "Build complete!" -echo "Shared library location: $ANDROID_DIR/libcactus.so" -echo "Static library location: $ANDROID_DIR/libcactus.a" +echo "Shared library location: $ANDROID_DIR/libcactus_engine.so" +echo "Static library location: $ANDROID_DIR/libcactus_engine.a" diff --git a/android/cactus_jni.cpp b/android/cactus_jni.cpp index 771c93f45..90a8be6d5 100644 --- a/android/cactus_jni.cpp +++ b/android/cactus_jni.cpp @@ -23,12 +23,13 @@ static void token_callback_bridge(const char* token, uint32_t token_id, void* us bool attached = false; jint status = ctx->jvm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6); if (status == JNI_EDETACHED) { - ctx->jvm->AttachCurrentThread(&env, nullptr); + if (ctx->jvm->AttachCurrentThread(&env, nullptr) != JNI_OK) return; attached = true; } - jstring jtoken = env->NewStringUTF(token); + if (!env) return; + jstring jtoken = token ? env->NewStringUTF(token) : nullptr; env->CallVoidMethod(ctx->callback, ctx->method, jtoken, static_cast(token_id)); - env->DeleteLocalRef(jtoken); + if (jtoken) env->DeleteLocalRef(jtoken); if (attached) ctx->jvm->DetachCurrentThread(); } @@ -38,14 +39,15 @@ static void log_callback_bridge(int level, const char* component, const char* me bool attached = false; jint status = ctx->jvm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6); if (status == JNI_EDETACHED) { - ctx->jvm->AttachCurrentThread(&env, nullptr); + if (ctx->jvm->AttachCurrentThread(&env, nullptr) != JNI_OK) return; attached = true; } - jstring jcomponent = env->NewStringUTF(component); - jstring jmessage = env->NewStringUTF(message); + if (!env) return; + jstring jcomponent = component ? env->NewStringUTF(component) : nullptr; + jstring jmessage = message ? env->NewStringUTF(message) : nullptr; env->CallVoidMethod(ctx->callback, ctx->method, static_cast(level), jcomponent, jmessage); - env->DeleteLocalRef(jcomponent); - env->DeleteLocalRef(jmessage); + if (jcomponent) env->DeleteLocalRef(jcomponent); + if (jmessage) env->DeleteLocalRef(jmessage); if (attached) ctx->jvm->DetachCurrentThread(); } @@ -610,6 +612,11 @@ Java_com_cactus_CactusJNI_nativeLogSetCallback(JNIEnv* env, jobject, jobject cal env->GetJavaVM(&jvm); jclass cls = env->GetObjectClass(callback); jmethodID method = env->GetMethodID(cls, "onLog", "(ILjava/lang/String;Ljava/lang/String;)V"); + if (!method) { + env->ExceptionClear(); + cactus_log_set_callback(nullptr, nullptr); + return; + } g_log_callback_ctx = new LogCallbackContext{jvm, env->NewGlobalRef(callback), method}; cactus_log_set_callback(log_callback_bridge, g_log_callback_ctx); } @@ -628,6 +635,10 @@ Java_com_cactus_CactusJNI_nativeSetTelemetryEnvironment(JNIEnv* env, jobject, JNIEXPORT void JNICALL Java_com_cactus_CactusJNI_nativeSetAppId(JNIEnv* env, jobject, jstring appId) { + if (!appId) { + cactus_set_app_id(nullptr); + return; + } const char* id = env->GetStringUTFChars(appId, nullptr); cactus_set_app_id(id); env->ReleaseStringUTFChars(appId, id); diff --git a/apple/CMakeLists.txt b/apple/CMakeLists.txt index 510dfa64a..85fde7328 100644 --- a/apple/CMakeLists.txt +++ b/apple/CMakeLists.txt @@ -7,7 +7,7 @@ set(CMAKE_BUILD_TYPE Release CACHE STRING "") set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) if(NOT TARGET cactus_engine) - add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../cactus ${CMAKE_CURRENT_BINARY_DIR}/cactus) + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../cactus-engine ${CMAKE_CURRENT_BINARY_DIR}/cactus-engine) endif() set(_DEPS cactus_engine cactus_graph cactus_kernels) diff --git a/apple/README.md b/apple/README.md index e2a5cc710..98fe929e3 100644 --- a/apple/README.md +++ b/apple/README.md @@ -1,6 +1,6 @@ # Apple Build -Builds `libcactus` for iOS and macOS. +Builds `libcactus_engine` for iOS and macOS. ## Usage @@ -16,8 +16,8 @@ bash apple/build.sh ## Output -- `libcactus-device.a` — iOS device (arm64) -- `libcactus-simulator.a` — iOS simulator (arm64) +- `libcactus_engine-device.a` — iOS device (arm64) +- `libcactus_engine-simulator.a` — iOS simulator (arm64) - `cactus-ios.xcframework/` — iOS XCFramework (device + simulator) - `cactus-macos.xcframework/` — macOS XCFramework (arm64) diff --git a/apple/build.sh b/apple/build.sh index 7a0e36650..69b2ebe75 100755 --- a/apple/build.sh +++ b/apple/build.sh @@ -1,4 +1,5 @@ -#!/bin/bash -e +#!/bin/bash +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" @@ -69,8 +70,8 @@ function build_static_library() { cmake --build "$BUILD_DIR" --config "$CMAKE_BUILD_TYPE" -j "$n_cpu" >/dev/null mkdir -p "$APPLE_DIR" - cp "$BUILD_DIR/libcactus.a" "$APPLE_DIR/libcactus-device.a" - echo "Device static library built: $APPLE_DIR/libcactus-device.a" + cp "$BUILD_DIR/libcactus_engine.a" "$APPLE_DIR/libcactus_engine-device.a" + echo "Device static library built: $APPLE_DIR/libcactus_engine-device.a" echo "Building static library for iOS simulator..." BUILD_DIR_SIM="$APPLE_DIR/build-static-simulator" @@ -95,8 +96,8 @@ function build_static_library() { cmake --build "$BUILD_DIR_SIM" --config "$CMAKE_BUILD_TYPE" -j "$n_cpu" >/dev/null - cp "$BUILD_DIR_SIM/libcactus.a" "$APPLE_DIR/libcactus-simulator.a" - echo "Simulator static library built: $APPLE_DIR/libcactus-simulator.a" + cp "$BUILD_DIR_SIM/libcactus_engine.a" "$APPLE_DIR/libcactus_engine-simulator.a" + echo "Simulator static library built: $APPLE_DIR/libcactus_engine-simulator.a" } function build_framework() { @@ -205,8 +206,8 @@ echo "Total time: $((t1 - t0)) seconds" if [ "$BUILD_STATIC" = "true" ]; then rm -rf "$APPLE_DIR/build-static-device" "$APPLE_DIR/build-static-simulator" "$APPLE_DIR/build-static-macos" echo "Static libraries:" - echo " Device: $APPLE_DIR/libcactus-device.a" - echo " Simulator: $APPLE_DIR/libcactus-simulator.a" + echo " Device: $APPLE_DIR/libcactus_engine-device.a" + echo " Simulator: $APPLE_DIR/libcactus_engine-simulator.a" fi if [ "$BUILD_XCFRAMEWORK" = "true" ]; then diff --git a/bindings/flutter/README.md b/bindings/flutter/README.md index 128478f34..077219048 100644 --- a/bindings/flutter/README.md +++ b/bindings/flutter/README.md @@ -12,9 +12,9 @@ cactus build --android -1. Copy `libcactus.so` to `jniLibs/arm64-v8a/` (Android) -2. Add `cactus-ios.xcframework` to your Xcode project (iOS) -3. Add `cactus-macos.xcframework` to your Xcode project (macOS) +1. Copy `android/libcactus_engine.so` to your app's `jniLibs/arm64-v8a/` +2. Add `apple/cactus-ios.xcframework` to your Xcode project (iOS) +3. Add `apple/cactus-macos.xcframework` to your Xcode project (macOS) 4. Copy `cactus.dart` into your Dart source tree 5. Add `ffi` to `pubspec.yaml` @@ -30,6 +30,7 @@ final modelPath = '/path/to/model'.toNativeUtf8(); final model = cactusInit(modelPath, nullptr, false); calloc.free(modelPath); +const messagesJson = '[{"role":"user","content":"Hello"}]'; final msgs = messagesJson.toNativeUtf8(); final buf = calloc(65536); cactusComplete(model, msgs, buf.cast(), 65536, nullptr, nullptr, nullptr, nullptr, nullptr, 0); diff --git a/bindings/flutter/cactus.dart b/bindings/flutter/cactus.dart index 11ba4f1e8..566580002 100644 --- a/bindings/flutter/cactus.dart +++ b/bindings/flutter/cactus.dart @@ -4,7 +4,7 @@ import 'dart:io'; import 'package:ffi/ffi.dart'; final DynamicLibrary _lib = (Platform.isAndroid || Platform.isLinux) - ? DynamicLibrary.open('libcactus.so') + ? DynamicLibrary.open('libcactus_engine.so') : DynamicLibrary.process(); typedef CactusModelT = Pointer; diff --git a/bindings/kotlin/Cactus.kt b/bindings/kotlin/Cactus.kt index 28e38a773..afc4e2178 100644 --- a/bindings/kotlin/Cactus.kt +++ b/bindings/kotlin/Cactus.kt @@ -2,7 +2,7 @@ package com.cactus object CactusJNI { init { - System.loadLibrary("cactus") + System.loadLibrary("cactus_engine") } @JvmStatic external fun nativeInit(modelPath: String, corpusDir: String?, cacheIndex: Boolean): Long diff --git a/bindings/kotlin/README.md b/bindings/kotlin/README.md index b1c4b57e6..a31c1cf55 100644 --- a/bindings/kotlin/README.md +++ b/bindings/kotlin/README.md @@ -2,7 +2,7 @@ JNI bridge to `cactus_engine.h` for Android, with KMP support for iOS. -The JNI bridge itself (`android/cactus_jni.cpp`, compiled into `libcactus.so`) is JVM-language-agnostic. Java consumers can use it by writing an equivalent `CactusJNI.java` with `native` declarations matching the signatures in `Cactus.kt` — same library, same `Java_com_cactus_CactusJNI_*` symbols. +The JNI bridge itself (`android/cactus_jni.cpp`, compiled into `libcactus_engine.so`) is JVM-language-agnostic. Java consumers can use it by writing an equivalent `CactusJNI.java` with `native` declarations matching the signatures in `Cactus.kt` — same library, same `Java_com_cactus_CactusJNI_*` symbols. ## Android Integration @@ -13,7 +13,7 @@ cactus build --android -1. Copy `android/libcactus.so` to `app/src/main/jniLibs/arm64-v8a/` +1. Copy `android/libcactus_engine.so` to `app/src/main/jniLibs/arm64-v8a/` 2. Copy `Cactus.kt` and `CactusCallbacks.kt` to your Kotlin source tree @@ -42,8 +42,8 @@ iosMain/ Cactus.ios.kt actual via cinterop kotlin { androidTarget() - listOf(iosArm64(), iosSimulatorArm64()).forEach { - it.compilations.getByName("main") { + listOf(iosArm64(), iosSimulatorArm64()).forEach { target -> + target.compilations.getByName("main") { cinterops { val cactus by creating { defFile("src/nativeInterop/cinterop/cactus.def") @@ -51,8 +51,9 @@ kotlin { } } } - it.binaries.framework { - linkerOpts("-L/path/to/cactus/apple", "-lcactus-device") + val libSuffix = if (target.name == "iosSimulatorArm64") "simulator" else "device" + target.binaries.framework { + linkerOpts("-L/path/to/cactus/apple", "-lcactus_engine-$libSuffix") } } } diff --git a/bindings/python/README.md b/bindings/python/README.md index 913d5a0de..f166b9a63 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -1,20 +1,10 @@ # Python Bindings -ctypes FFI bindings to `cactus_engine.h`. The package source lives at [`/python/`](/python/). +ctypes FFI to `cactus_engine.h`. The binding module lives at +[`/python/cactus/bindings/cactus.py`](/python/cactus/bindings/cactus.py). -## Integration +It loads `libcactus_engine.{so,dylib}` from `/python/cactus/bin/`, populated by +`cactus build --python`. -```bash -cactus build --python -pip install -e python/ -``` - -## Usage - -```python -from cactus.bindings import cactus - -model = cactus.cactus_init("/path/to/model", None, False) -response = cactus.cactus_complete(model, messages_json, None, None, None) -cactus.cactus_destroy(model) -``` +For installation, the high-level API, the CLI, the server, and the transpiler, +see [`/python/README.md`](/python/). diff --git a/bindings/react-native/README.md b/bindings/react-native/README.md index 71e43c8a5..b333d88b3 100644 --- a/bindings/react-native/README.md +++ b/bindings/react-native/README.md @@ -29,7 +29,7 @@ cactus build --android import Cactus from './index'; const handle = await Cactus.init('/path/to/model', null, false); -const result = await Cactus.complete(handle, messagesJson, null, null, null); +const result = await Cactus.complete(handle, messagesJson, null, null, null, false); await Cactus.destroy(handle); ``` diff --git a/bindings/rust/README.md b/bindings/rust/README.md index 69a5a10fe..a1b3f8768 100644 --- a/bindings/rust/README.md +++ b/bindings/rust/README.md @@ -10,13 +10,12 @@ cactus build ``` - -1. Copy `cactus.rs` into your project -2. Link against `libcactus.a` in your `build.rs`: +Copy [`cactus.rs`](cactus.rs) into your project (it carries its own `#[link]` +attributes) and point Cargo at the build directory: ```rust -println!("cargo:rustc-link-lib=static=cactus"); -println!("cargo:rustc-link-search=native=/path/to/cactus/cactus/build"); +// build.rs +println!("cargo:rustc-link-search=native=/path/to/cactus/cactus-engine/build"); ``` ## Usage @@ -27,11 +26,24 @@ use std::os::raw::c_char; mod cactus; -unsafe { - let path = CString::new("/path/to/model").unwrap(); - let model = cactus::cactus_init(path.as_ptr(), std::ptr::null(), false); - let mut buf = vec![0i8; 65536]; - cactus::cactus_complete(model, msgs.as_ptr(), buf.as_mut_ptr() as *mut c_char, buf.len(), std::ptr::null(), std::ptr::null(), None, std::ptr::null_mut(), std::ptr::null(), 0); - cactus::cactus_destroy(model); +fn main() { + unsafe { + let path = CString::new("/path/to/model").unwrap(); + let model = cactus::cactus_init(path.as_ptr(), std::ptr::null(), false); + + let messages = CString::new(r#"[{"role":"user","content":"Hello"}]"#).unwrap(); + let mut response = vec![0u8; 65536]; + cactus::cactus_complete( + model, + messages.as_ptr(), + response.as_mut_ptr() as *mut c_char, + response.len(), + std::ptr::null(), std::ptr::null(), + None, std::ptr::null_mut(), + std::ptr::null(), 0, + ); + + cactus::cactus_destroy(model); + } } ``` diff --git a/bindings/rust/cactus.rs b/bindings/rust/cactus.rs index ba4a4b406..d30adfa13 100644 --- a/bindings/rust/cactus.rs +++ b/bindings/rust/cactus.rs @@ -5,6 +5,14 @@ pub type CactusIndexT = *mut c_void; pub type CactusTokenCallback = Option; pub type CactusLogCallback = Option; +#[cfg_attr(target_os = "macos", link(name = "Accelerate", kind = "framework"))] +#[cfg_attr(target_os = "macos", link(name = "CoreML", kind = "framework"))] +#[cfg_attr(target_os = "macos", link(name = "Foundation", kind = "framework"))] +#[cfg_attr(target_os = "macos", link(name = "Security", kind = "framework"))] +#[cfg_attr(target_os = "macos", link(name = "SystemConfiguration", kind = "framework"))] +#[cfg_attr(target_os = "macos", link(name = "CFNetwork", kind = "framework"))] +#[cfg_attr(target_os = "macos", link(name = "curl"))] +#[link(name = "cactus_engine", kind = "static")] unsafe extern "C" { pub fn cactus_init(model_path: *const c_char, corpus_dir: *const c_char, cache_index: bool) -> CactusModelT; pub fn cactus_destroy(model: CactusModelT); diff --git a/bindings/swift/README.md b/bindings/swift/README.md index 592daec0a..94d826240 100644 --- a/bindings/swift/README.md +++ b/bindings/swift/README.md @@ -13,7 +13,7 @@ cactus build --apple **XCFramework**: drag `apple/cactus-ios.xcframework` (or `apple/cactus-macos.xcframework`) into Xcode (Embed & Sign). The framework bundles `cactus_engine.h`, so `import cactus` works directly. -**Static library**: link `apple/libcactus-device.a` (or `apple/libcactus-simulator.a`), copy `bindings/swift/module.modulemap` into your project, and add `cactus-engine/` to Header Search Paths so the module map can find `cactus_engine.h`. +**Static library**: link `apple/libcactus_engine-device.a` (or `apple/libcactus_engine-simulator.a`), copy `bindings/swift/module.modulemap` into your project, and add `cactus-engine/` to Header Search Paths so the module map can find `cactus_engine.h`. ## Usage diff --git a/blog/gemma4.md b/blog/gemma4.md index cf76fc011..389e743e8 100644 --- a/blog/gemma4.md +++ b/blog/gemma4.md @@ -130,12 +130,12 @@ Cactus supports React Native, Flutter, Swift, Kotlin, Python, Rust, and C++. Pic | Flutter | [cactus-flutter](https://cactus.dev/flutter/) | Dart FFI bindings | | Swift (iOS/macOS) | [cactus-swift](https://cactus.dev/swift/) | XCFramework with NPU support | | Kotlin (Android) | [cactus-kotlin](https://cactus.dev/kotlin/) | JNI + Kotlin Multiplatform | -| Python | [cactus-ai](https://cactus.dev/python/) | `pip install cactus-ai` | -| Rust | [Rust bindings](https://cactus.dev/rust/) | Copy `cactus.rs`, link `libcactus.a` | -| C++ | [cactus.h](https://cactus.dev/docs/cactus_engine/) | Single header, link `libcactus.a` | +| Python | [cactus-compute](https://cactus.dev/python/) | `pip install cactus-compute` | +| Rust | [Rust bindings](https://cactus.dev/rust/) | Copy `cactus.rs`, link `libcactus_engine.a` | +| C++ | [cactus.h](https://cactus.dev/docs/cactus_engine/) | Single header, link `libcactus_engine.a` | Full quickstart with code examples for every binding: [cactus.dev/docs/quickstart](https://cactus.dev/docs/quickstart/) -Pre-quantized weights for Gemma 4 and 30+ other models: [huggingface.co/cactus-compute](https://huggingface.co/cactus-compute) +Pre-quantized weights for Gemma 4 and 30+ other models: [huggingface.co/Cactus-Compute](https://huggingface.co/Cactus-Compute) If you're building something with on-device multimodal AI (voice agents, VR interfaces, local-first apps) we want to hear about it. Open an issue on [GitHub](https://github.com/cactus-compute/cactus), or just start building. diff --git a/blog/hybrid_transcription.md b/blog/hybrid_transcription.md index bff5ba35f..87dc15806 100644 --- a/blog/hybrid_transcription.md +++ b/blog/hybrid_transcription.md @@ -47,7 +47,7 @@ As a Mac user, you can pull down the CLI and test the latency and accuracy local ```bash brew install cactus-compute/cactus/cactus -cactus transcribe +cactus transcribe --file path/to/audio.wav ``` ## Built for the Edge, from the Ground Up diff --git a/blog/lfm2.5_350m.md b/blog/lfm2.5_350m.md index 75ca391c7..9f2e40750 100644 --- a/blog/lfm2.5_350m.md +++ b/blog/lfm2.5_350m.md @@ -65,7 +65,7 @@ Both lock you into a vendor-specific format that may not support your model arch CPU is the universal target. Every ARM device, flagship, budget, wearable, Raspberry Pi, and most recent ones support DOTPROD or I8MM extensions. A single well-optimized CPU codepath works everywhere, the main challenge is energy-inefficiency compared to NPU. -ARM is also closing this gap. The progression from NEON to DOTPROD to I8MM to SME2 reflects a deliberate push to bring matrix-processing efficiency onto the CPU itself. Each generation narrows the energy and throughput gap with dedicated NPUs. SME2, available on Apple's M4 and upcoming ARMv9.2 cores, adds streaming matrix operations that approach NPU-class efficiency for INT8 workloads while remaining fully programmable. Cactus already compiles SME2 codepaths where available. The bet is that general-purpose CPU silicon will continue absorbing the capabilities that once justified a separate accelerator. +ARM is also closing this gap. The progression from NEON to DOTPROD to I8MM to SME2 reflects a deliberate push to bring matrix-processing efficiency onto the CPU itself. Each generation narrows the energy and throughput gap with dedicated NPUs. SME2, landing on upcoming ARMv9.2-A cores, adds streaming matrix operations that approach NPU-class efficiency for INT8 workloads while remaining fully programmable. The bet is that general-purpose CPU silicon will continue absorbing the capabilities that once justified a separate accelerator. ## Why Single Core for Decode @@ -186,7 +186,7 @@ The choice often comes down to small models that fit everywhere but are too weak Cactus exists because we took on this specific slice of the problem; I spent the last few years building foundation models and inference for tiny devices. The team's approach is systematic: profile every major ARM variant, write dedicated kernel paths for each ISA extension tier (NEON, DOTPROD, I8MM, SME2), design the model format around the worst-case memory behavior (Android's mmap limitations), and test on real production devices, not emulators. -This it not-trivial; battling with chip manufactures with different brand visions, model developers chasing benchmarks over useability and more. +This is not trivial; battling with chip manufacturers with different brand visions, model developers chasing benchmarks over usability and more. The result is that businesses shipping with Cactus don't need to worry about inference failing silently on a budget Samsung, or a model that's accurate on benchmarks but too large for half their install base. The kernel, the format, and the quantization strategy are all designed so that the same model file works correctly across the full device spectrum, with predictable RAM, predictable speed, and predictable quality. diff --git a/blog/parakeet.md b/blog/parakeet.md index ec3a29b16..e62b52a37 100644 --- a/blog/parakeet.md +++ b/blog/parakeet.md @@ -158,17 +158,11 @@ cactus download nvidia/parakeet-ctc-1.1b For integrating Parakeet into your own applications, use the Python FFI bindings directly: ```python -import json from cactus import cactus_init, cactus_transcribe, cactus_destroy model = cactus_init("weights/parakeet-ctc-1.1b", None, False) -result = json.loads( - cactus_transcribe(model, "/path/to/audio.wav", None, None, None, None) -) - -if not result["success"]: - raise RuntimeError(result["error"]) +result = cactus_transcribe(model, "/path/to/audio.wav") print("\n\nFinal transcript:") print(result["response"]) @@ -179,7 +173,7 @@ cactus_destroy(model) ### 5. Use the [C API](/docs/cactus_engine.md) -The C API is the base layer all other bindings build on. Link against `libcactus` and include the FFI header: +The C API is the base layer all other bindings build on. Link against `libcactus_engine` and include the FFI header: ```c #include "cactus_engine.h" @@ -205,7 +199,7 @@ int main() { ### 6. Use the [Rust Binding](/bindings/rust/) -Copy `cactus.rs` into your project (see [the README](/bindings/rust/)), link `libcactus.a` from `cactus build`, and call the FFI bindings directly: +Copy `cactus.rs` into your project (see [the README](/bindings/rust/)), link `libcactus_engine.a` from `cactus build`, and call the FFI bindings directly: ```rust use std::ffi::CString; @@ -250,20 +244,9 @@ The Swift binding exposes top-level functions that map directly to the C FFI: import Foundation let model = try cactusInit("weights/parakeet-ctc-1.1b", nil, false) - -// File-based transcription let resultJson = try cactusTranscribe(model, "/path/to/audio.wav", nil, nil, nil, nil) print(resultJson) -// Streaming transcription -let stream = try cactusStreamTranscribeStart(model, "{\"min_chunk_size\": 16000}") -// Feed PCM chunks from your audio source (16-bit, 16kHz, mono) -let partial = try cactusStreamTranscribeProcess(stream, audioChunk) -print("Partial: \(partial)") - -let finalResult = try cactusStreamTranscribeStop(stream) -print("Final: \(finalResult)") - cactusDestroy(model) ``` @@ -275,20 +258,9 @@ The Kotlin binding exposes top-level functions that map directly to the C FFI: import com.cactus.* val model = cactusInit("weights/parakeet-ctc-1.1b", null, false) - -// File-based transcription val resultJson = cactusTranscribe(model, "/path/to/audio.wav", null, null, null, null) println(resultJson) -// Streaming transcription -val stream = cactusStreamTranscribeStart(model, "{\"min_chunk_size\": 16000}") -// Feed PCM chunks from your audio source (16-bit, 16kHz, mono) -val partial = cactusStreamTranscribeProcess(stream, audioChunk) -println("Partial: $partial") - -val finalResult = cactusStreamTranscribeStop(stream) -println("Final: $finalResult") - cactusDestroy(model) ``` @@ -300,19 +272,9 @@ The Flutter binding brings Cactus transcription to iOS, macOS, and Android: import 'cactus.dart'; final model = cactusInit('weights/parakeet-ctc-1.1b', null, false); - -// File-based transcription final resultJson = cactusTranscribe(model, '/path/to/audio.wav', null, null, null, null); print(resultJson); -// Streaming transcription -final stream = cactusStreamTranscribeStart(model, '{"min_chunk_size": 16000}'); -final partial = cactusStreamTranscribeProcess(stream, audioChunk); -print('Partial: $partial'); - -final finalResult = cactusStreamTranscribeStop(stream); -print('Final: $finalResult'); - cactusDestroy(model); ``` diff --git a/cactus-engine/CMakeLists.txt b/cactus-engine/CMakeLists.txt index d240d748c..1755844cf 100644 --- a/cactus-engine/CMakeLists.txt +++ b/cactus-engine/CMakeLists.txt @@ -8,41 +8,8 @@ if(NOT TARGET cactus_graph) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../cactus-graph ${CMAKE_CURRENT_BINARY_DIR}/cactus-graph) endif() -# --- libcurl --- set(CACTUS_CURL_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/libs/curl" CACHE PATH "Path to vendored libcurl") -set(CACTUS_CURL_INCLUDE_DIR "${CACTUS_CURL_ROOT}/include") -if(APPLE) - if(CMAKE_SYSTEM_NAME STREQUAL "iOS") - string(TOLOWER "${CMAKE_OSX_SYSROOT}" _sysroot_lower) - if(_sysroot_lower MATCHES "simulator") - set(CACTUS_CURL_LIBRARY "${CACTUS_CURL_ROOT}/ios/simulator/libcurl.a") - else() - set(CACTUS_CURL_LIBRARY "${CACTUS_CURL_ROOT}/ios/device/libcurl.a") - endif() - else() - set(CACTUS_CURL_LIBRARY "${CACTUS_CURL_ROOT}/macos/libcurl.a") - endif() - if(EXISTS "${CACTUS_CURL_INCLUDE_DIR}/curl/curl.h" AND EXISTS "${CACTUS_CURL_LIBRARY}") - set(CACTUS_HAS_CURL TRUE) - else() - set(CACTUS_HAS_CURL FALSE) - message(WARNING "Vendored libcurl not found at ${CACTUS_CURL_ROOT}; cloud features disabled") - endif() -elseif(ANDROID) - set(CACTUS_CURL_LIBRARY "${CACTUS_CURL_ROOT}/android/${ANDROID_ABI}/libcurl.a") - if(EXISTS "${CACTUS_CURL_INCLUDE_DIR}/curl/curl.h" AND EXISTS "${CACTUS_CURL_LIBRARY}") - set(CACTUS_HAS_CURL TRUE) - else() - set(CACTUS_HAS_CURL FALSE) - message(WARNING "Vendored libcurl not found for Android; cloud features disabled") - endif() -else() - find_package(CURL QUIET) - set(CACTUS_HAS_CURL ${CURL_FOUND}) -endif() - -# --- Sources --- set(ENGINE_SOURCES src/bpe.cpp src/sp.cpp @@ -53,7 +20,6 @@ set(ENGINE_SOURCES src/index.cpp src/index_ffi.cpp src/log.cpp - src/graph_ffi.cpp src/rag.cpp src/telemetry.cpp src/telemetry_impl.cpp @@ -73,52 +39,70 @@ else() endif() add_library(cactus_engine STATIC ${ENGINE_SOURCES}) - set_target_properties(cactus_engine PROPERTIES POSITION_INDEPENDENT_CODE ON + OUTPUT_NAME cactus_engine_core ) target_include_directories(cactus_engine PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/libs - ${CMAKE_CURRENT_SOURCE_DIR}/../cactus-kernels/src - ${CMAKE_CURRENT_SOURCE_DIR}/../cactus-kernels/libs ) target_link_libraries(cactus_engine PUBLIC cactus_graph) -# --- curl linking --- -if(CACTUS_HAS_CURL) - if(APPLE OR ANDROID) - target_link_libraries(cactus_engine PRIVATE "${CACTUS_CURL_LIBRARY}") - target_include_directories(cactus_engine PRIVATE "${CACTUS_CURL_INCLUDE_DIR}") +if(APPLE) + target_link_libraries(cactus_engine PUBLIC + "-framework CoreML" "-framework Foundation" + "-framework Security" "-framework SystemConfiguration" "-framework CFNetwork" + ) + + if(CMAKE_SYSTEM_NAME STREQUAL "iOS") + string(TOLOWER "${CMAKE_OSX_SYSROOT}" _sysroot) + if(_sysroot MATCHES "simulator") + set(_curl "${CACTUS_CURL_ROOT}/ios/simulator/libcurl.a") + else() + set(_curl "${CACTUS_CURL_ROOT}/ios/device/libcurl.a") + endif() else() - target_link_libraries(cactus_engine PRIVATE ${CURL_LIBRARIES}) - target_include_directories(cactus_engine PRIVATE ${CURL_INCLUDE_DIRS}) + set(_curl "${CACTUS_CURL_ROOT}/macos/libcurl.a") endif() - target_compile_definitions(cactus_engine PRIVATE CACTUS_USE_CURL=1) -endif() - -# --- Apple frameworks --- -if(APPLE) - find_library(COREML_FRAMEWORK CoreML REQUIRED) - find_library(FOUNDATION_FRAMEWORK Foundation REQUIRED) - find_library(SECURITY_FRAMEWORK Security REQUIRED) - find_library(SYSTEMCONFIGURATION_FRAMEWORK SystemConfiguration REQUIRED) - find_library(CFNETWORK_FRAMEWORK CFNetwork REQUIRED) + if(EXISTS "${_curl}") + target_link_libraries(cactus_engine PRIVATE "${_curl}") + target_include_directories(cactus_engine PRIVATE "${CACTUS_CURL_ROOT}/include") + target_compile_definitions(cactus_engine PRIVATE CACTUS_USE_CURL=1) + endif() +elseif(ANDROID) + set(_mbedtls ${CMAKE_CURRENT_SOURCE_DIR}/../android/mbedtls/${ANDROID_ABI}/lib) target_link_libraries(cactus_engine PUBLIC - ${COREML_FRAMEWORK} ${FOUNDATION_FRAMEWORK} - ${SECURITY_FRAMEWORK} ${SYSTEMCONFIGURATION_FRAMEWORK} ${CFNETWORK_FRAMEWORK} + ${_mbedtls}/libmbedtls.a + ${_mbedtls}/libmbedx509.a + ${_mbedtls}/libmbedcrypto.a + log + android ) + + set(_curl "${CACTUS_CURL_ROOT}/android/${ANDROID_ABI}/libcurl.a") + if(EXISTS "${_curl}") + target_link_libraries(cactus_engine PRIVATE "${_curl}") + target_include_directories(cactus_engine PRIVATE "${CACTUS_CURL_ROOT}/include") + target_compile_definitions(cactus_engine PRIVATE CACTUS_USE_CURL=1) + endif() +else() + find_package(CURL QUIET) + if(CURL_FOUND) + target_link_libraries(cactus_engine PRIVATE ${CURL_LIBRARIES}) + target_include_directories(cactus_engine PRIVATE ${CURL_INCLUDE_DIRS}) + target_compile_definitions(cactus_engine PRIVATE CACTUS_USE_CURL=1) + endif() endif() -# --- Version --- -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_engine PRIVATE CACTUS_COMPILE_TIME_VERSION="${CACTUS_VERSION_STR}") +set(_version_file "${CMAKE_CURRENT_SOURCE_DIR}/../CACTUS_VERSION") +if(EXISTS "${_version_file}") + file(READ "${_version_file}" _version) + string(STRIP "${_version}" _version) + target_compile_definitions(cactus_engine PRIVATE CACTUS_COMPILE_TIME_VERSION="${_version}") endif() target_compile_options(cactus_engine PRIVATE @@ -126,3 +110,59 @@ target_compile_options(cactus_engine PRIVATE $<$:-Wno-c99-extensions> $<$:-Wno-pedantic> ) + +set(_CACTUS_ENGINE_BUNDLE "${CMAKE_BINARY_DIR}/libcactus_engine.a") +set(_CACTUS_ENGINE_ARCHIVES + $ + $ + $ +) +set(_CACTUS_ENGINE_DEPS cactus_engine cactus_graph cactus_kernels) + +if(APPLE) + add_custom_target(cactus_engine_bundle ALL + COMMAND libtool -static -o ${_CACTUS_ENGINE_BUNDLE} ${_CACTUS_ENGINE_ARCHIVES} + DEPENDS ${_CACTUS_ENGINE_DEPS} + BYPRODUCTS ${_CACTUS_ENGINE_BUNDLE} + ) +else() + file(GENERATE OUTPUT ${CMAKE_BINARY_DIR}/cactus_engine_bundle.mri CONTENT + "create ${_CACTUS_ENGINE_BUNDLE}\naddlib $\naddlib $\naddlib $\nsave\nend\n" + ) + add_custom_target(cactus_engine_bundle ALL + COMMAND ${CMAKE_AR} -M < ${CMAKE_BINARY_DIR}/cactus_engine_bundle.mri + DEPENDS ${_CACTUS_ENGINE_DEPS} + BYPRODUCTS ${_CACTUS_ENGINE_BUNDLE} + ) +endif() + +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + file(WRITE ${CMAKE_BINARY_DIR}/cactus_engine_shared_stub.cpp "") + add_library(cactus_engine_shared SHARED ${CMAKE_BINARY_DIR}/cactus_engine_shared_stub.cpp) + set_target_properties(cactus_engine_shared PROPERTIES OUTPUT_NAME cactus_engine) + add_dependencies(cactus_engine_shared ${_CACTUS_ENGINE_DEPS}) + + if(APPLE) + target_link_options(cactus_engine_shared PRIVATE + -Wl,-force_load,$ + -Wl,-force_load,$ + -Wl,-force_load,$ + ) + set(_curl "${CACTUS_CURL_ROOT}/macos/libcurl.a") + if(EXISTS "${_curl}") + target_link_libraries(cactus_engine_shared PRIVATE "${_curl}") + endif() + target_link_libraries(cactus_engine_shared PRIVATE + "-framework Accelerate" "-framework CoreML" "-framework Foundation" + "-framework Security" "-framework SystemConfiguration" "-framework CFNetwork" + ) + else() + target_link_options(cactus_engine_shared PRIVATE + -Wl,--whole-archive ${_CACTUS_ENGINE_ARCHIVES} -Wl,--no-whole-archive + ) + find_package(CURL QUIET) + if(CURL_FOUND) + target_link_libraries(cactus_engine_shared PRIVATE ${CURL_LIBRARIES}) + endif() + endif() +endif() diff --git a/cactus-engine/build.sh b/cactus-engine/build.sh new file mode 100755 index 000000000..68177f692 --- /dev/null +++ b/cactus-engine/build.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -e + +cd "$(dirname "$0")" + +echo "Building cactus-engine..." + +mkdir -p build +cd build + +cmake .. -DCMAKE_RULE_MESSAGES=OFF -DCMAKE_VERBOSE_MAKEFILE=OFF > /dev/null +make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) + +echo "cactus-engine built successfully!" +echo " Static: $(pwd)/libcactus_engine.a" +echo " Shared: $(pwd)/libcactus_engine.$([ "$(uname)" = Darwin ] && echo dylib || echo so)" diff --git a/cactus-engine/cactus_engine.h b/cactus-engine/cactus_engine.h index 7f2422b2a..41af950da 100644 --- a/cactus-engine/cactus_engine.h +++ b/cactus-engine/cactus_engine.h @@ -1,6 +1,8 @@ #ifndef CACTUS_FFI_H #define CACTUS_FFI_H +#include "cactus_graph.h" + #include #include #include @@ -190,253 +192,6 @@ CACTUS_FFI_EXPORT void cactus_set_app_id(const char* app_id); CACTUS_FFI_EXPORT void cactus_telemetry_flush(void); CACTUS_FFI_EXPORT void cactus_telemetry_shutdown(void); -// cactus graph export -typedef void* cactus_graph_t; -typedef uint64_t cactus_node_t; - -typedef struct { - int32_t precision; - size_t rank; - size_t shape[8]; - size_t num_elements; - size_t byte_size; -} cactus_tensor_info_t; - -CACTUS_FFI_EXPORT cactus_graph_t cactus_graph_create(void); -CACTUS_FFI_EXPORT void cactus_graph_destroy(cactus_graph_t graph); -CACTUS_FFI_EXPORT int cactus_graph_hard_reset(cactus_graph_t graph); - -CACTUS_FFI_EXPORT int cactus_graph_save(cactus_graph_t graph, const char* filename); -CACTUS_FFI_EXPORT cactus_graph_t cactus_graph_load(const char* filename); - -CACTUS_FFI_EXPORT int cactus_graph_input( - cactus_graph_t graph, const size_t* shape, size_t rank, int32_t precision, -cactus_node_t* out_node); - -CACTUS_FFI_EXPORT int cactus_graph_set_input( - cactus_graph_t graph, cactus_node_t node, const void* data, int32_t -precision); -CACTUS_FFI_EXPORT int cactus_graph_set_external_input( - cactus_graph_t graph, cactus_node_t node, void* data, int32_t precision); -CACTUS_FFI_EXPORT int cactus_graph_mark_embedded_input( - cactus_graph_t graph, cactus_node_t node); - -CACTUS_FFI_EXPORT int cactus_graph_precision_cast( - cactus_graph_t graph, cactus_node_t input, int32_t target_precision, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_add(cactus_graph_t graph, cactus_node_t a, -cactus_node_t b, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_add_clipped(cactus_graph_t graph, cactus_node_t a, -cactus_node_t b, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_subtract(cactus_graph_t graph, cactus_node_t -a, cactus_node_t b, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_multiply(cactus_graph_t graph, cactus_node_t -a, cactus_node_t b, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_divide(cactus_graph_t graph, cactus_node_t -a, cactus_node_t b, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_not_equal(cactus_graph_t graph, cactus_node_t -a, cactus_node_t b, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_scalar_add(cactus_graph_t graph, cactus_node_t x, float value, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_scalar_subtract(cactus_graph_t graph, cactus_node_t x, float value, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_scalar_multiply(cactus_graph_t graph, cactus_node_t x, float value, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_scalar_divide(cactus_graph_t graph, cactus_node_t x, float value, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_scalar_not_equal(cactus_graph_t graph, cactus_node_t x, float value, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_scalar_exp(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_scalar_sqrt(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_scalar_cos(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_scalar_sin(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_scalar_log(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_abs(cactus_graph_t graph, cactus_node_t x, -cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_pow(cactus_graph_t graph, cactus_node_t x, -float exponent, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_view( - cactus_graph_t graph, cactus_node_t x, const size_t* shape, size_t rank, -cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_flatten( - cactus_graph_t graph, cactus_node_t x, int32_t start_dim, int32_t end_dim, -cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_reshape( - cactus_graph_t graph, cactus_node_t x, const size_t* shape, size_t rank, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_transpose( - cactus_graph_t graph, cactus_node_t x, int32_t backend, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_transpose_n( - cactus_graph_t graph, cactus_node_t x, const size_t* permutation, size_t rank, int32_t backend, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_slice( - cactus_graph_t graph, cactus_node_t x, int32_t axis, size_t start, size_t length, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_index( - cactus_graph_t graph, cactus_node_t x, size_t index_value, int32_t dim, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_sum(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_mean(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_variance(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_min(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_max(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_cumsum(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_concat( - cactus_graph_t graph, cactus_node_t a, cactus_node_t b, int32_t axis, -cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_cat( - cactus_graph_t graph, const cactus_node_t* nodes, size_t count, int32_t -axis, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_matmul( - cactus_graph_t graph, cactus_node_t a, cactus_node_t b, bool pretransposed_rhs, int32_t backend, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_gather( - cactus_graph_t graph, cactus_node_t tensor, cactus_node_t indices, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_embedding_from_tensor( - cactus_graph_t graph, cactus_node_t embedding_tensor, cactus_node_t indices, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_embedding_from_file( - cactus_graph_t graph, const char* filename, cactus_node_t indices, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_mmap_embeddings( - cactus_graph_t graph, const char* filename, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_mmap_weights( - cactus_graph_t graph, const char* filename, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_bind_mmap_weights( - cactus_graph_t graph, cactus_node_t node, const char* filename); -CACTUS_FFI_EXPORT int cactus_graph_bilinear_interpolation( - cactus_graph_t graph, cactus_node_t pos_embeds, size_t dst_height, size_t dst_width, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_release_weight_pages(cactus_graph_t graph, cactus_node_t node); -CACTUS_FFI_EXPORT int cactus_graph_prefetch_weight_pages(cactus_graph_t graph, cactus_node_t node); -CACTUS_FFI_EXPORT int cactus_graph_release_all_weight_pages(cactus_graph_t graph); - -CACTUS_FFI_EXPORT int cactus_graph_relu(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_silu(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_gelu(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_gelu_erf(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_sigmoid(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_tanh(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_glu(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_layernorm( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, cactus_node_t bias, float epsilon, bool has_bias, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_groupnorm( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, cactus_node_t bias, size_t num_groups, float epsilon, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_batchnorm( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, cactus_node_t bias, cactus_node_t running_mean, cactus_node_t running_var, int32_t axis, float epsilon, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_topk(cactus_graph_t graph, cactus_node_t input, size_t k, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_rms_norm( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, float epsilon, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_rope( - cactus_graph_t graph, cactus_node_t input, float theta, size_t position_offset, int32_t backend, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_rope_gptj( - cactus_graph_t graph, cactus_node_t input, float theta, size_t position_offset, size_t rot_dim, int32_t backend, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_softmax(cactus_graph_t graph, cactus_node_t input, int32_t axis, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_attention( - cactus_graph_t graph, cactus_node_t query, cactus_node_t key, cactus_node_t value, float scale, bool is_causal, size_t position_offset, size_t window_size, int32_t backend, bool use_mask, cactus_node_t mask, bool additive_mask, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_rel_pos_bias( - cactus_graph_t graph, cactus_node_t query, cactus_node_t relative_key, float scale, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_attention_int8_hybrid( - cactus_graph_t graph, cactus_node_t query, cactus_node_t key_new, cactus_node_t value_new, float scale, size_t position_offset, - const int8_t* cached_keys, const int8_t* cached_values, const float* k_scales, const float* v_scales, - size_t cache_len, size_t num_kv_heads, size_t head_dim, size_t window_size, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_kv_cache_state( - cactus_graph_t graph, size_t max_seq_len, size_t num_kv_heads, size_t head_dim, size_t window_size, size_t sink_size, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_kv_cache_append( - cactus_graph_t graph, cactus_node_t new_kv, cactus_node_t cache_state, size_t window_size, size_t sink_size, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_attention_cached( - cactus_graph_t graph, cactus_node_t query, cactus_node_t key_new, cactus_node_t value_new, - cactus_node_t k_cache_state, cactus_node_t v_cache_state, - float scale, size_t position_offset, size_t window_size, size_t v_head_dim, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_conv_cache_state( - cactus_graph_t graph, size_t window_size, size_t hidden_dim, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_conv_cache_append( - cactus_graph_t graph, cactus_node_t new_data, cactus_node_t cache_state, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_conv_cache_initialize( - cactus_graph_t graph, cactus_node_t rows, cactus_node_t cache_state, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_recurrent_cache_state( - cactus_graph_t graph, const size_t* shape, size_t shape_len, int precision, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_recurrent_cache_write( - cactus_graph_t graph, cactus_node_t new_value, cactus_node_t cache_input, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_rfft( - cactus_graph_t graph, cactus_node_t input, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_irfft( - cactus_graph_t graph, cactus_node_t input, size_t output_length, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_mel_filter_bank( - cactus_graph_t graph, size_t num_frequency_bins, size_t num_mel_filters, - float min_frequency, float max_frequency, size_t sampling_rate, - int norm_type, int scale_type, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_spectrogram( - cactus_graph_t graph, cactus_node_t waveform, cactus_node_t mel_filters, - size_t frame_length, size_t hop_length, size_t fft_length, - float power, bool center, int pad_mode, - float mel_floor, int log_mel_mode, - float dither, float preemphasis, bool remove_dc_offset, - cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_image_preprocess( - cactus_graph_t graph, cactus_node_t pixel_input, - int src_width, int src_height, int target_width, int target_height, - int patch_size, int channels, float rescale_factor, - const float* mean, const float* std_dev, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_conv1d_causal( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, size_t kernel_size, size_t dilation, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_conv1d_k3( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, size_t stride, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_conv1d_k7s3( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, cactus_node_t bias, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_conv1d( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, bool has_bias, cactus_node_t bias, size_t stride, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_conv1d_same_depthwise_k9( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, bool has_bias, cactus_node_t bias, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_conv1d_pointwise( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, bool has_bias, cactus_node_t bias, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_conv2d_k3s2p1( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, bool has_bias, cactus_node_t bias, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_conv2d_depthwise_k3s2p1( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, bool has_bias, cactus_node_t bias, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_conv2d_pointwise_1x1( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, bool has_bias, cactus_node_t bias, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_lstm_cell( - cactus_graph_t graph, cactus_node_t input, cactus_node_t h_prev, cactus_node_t c_prev, cactus_node_t weight_ih, cactus_node_t weight_hh, cactus_node_t bias_ih, cactus_node_t bias_hh, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_gated_deltanet_decode( - cactus_graph_t graph, cactus_node_t query, cactus_node_t key, cactus_node_t value, cactus_node_t gate_log, cactus_node_t beta, cactus_node_t initial_state, float scale, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_gated_deltanet_prefill( - cactus_graph_t graph, cactus_node_t query, cactus_node_t key, cactus_node_t value, cactus_node_t gate_log, cactus_node_t beta, cactus_node_t initial_state, size_t chunk_size, float scale, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_stft( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, size_t stride, size_t num_fft_bins, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_altup_predict( - cactus_graph_t graph, cactus_node_t coefs, const cactus_node_t* streams, size_t num_streams, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_altup_correct( - cactus_graph_t graph, cactus_node_t coefs, cactus_node_t innovation, const cactus_node_t* predictions, size_t num_predictions, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_gaussian_topk( - cactus_graph_t graph, cactus_node_t input, float ppf, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_moe_layer_gated( - cactus_graph_t graph, cactus_node_t hidden, cactus_node_t routing_probs, cactus_node_t topk_indices, - const cactus_node_t* w1_weights, const cactus_node_t* w3_weights, const cactus_node_t* w2_weights, - size_t num_experts, size_t num_experts_per_tok, bool normalize_routing, float epsilon, float routed_scaling_factor, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_moe_layer_ungated( - cactus_graph_t graph, cactus_node_t hidden, cactus_node_t routing_probs, cactus_node_t topk_indices, - const cactus_node_t* w1_weights, const cactus_node_t* w2_weights, - size_t num_experts, size_t num_experts_per_tok, bool normalize_routing, float epsilon, float routed_scaling_factor, int32_t activation, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_sample( - cactus_graph_t graph, cactus_node_t logits, float temperature, float top_p, size_t top_k, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_scatter_topk( - cactus_graph_t graph, cactus_node_t indices, cactus_node_t values, size_t num_classes, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_persistent( - cactus_graph_t graph, cactus_node_t source_node, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_is_populated( - cactus_graph_t graph, cactus_node_t persistent_node, int32_t* out_is_populated); -CACTUS_FFI_EXPORT int cactus_graph_invalidate_persistent( - cactus_graph_t graph, cactus_node_t persistent_node); - -CACTUS_FFI_EXPORT int cactus_graph_execute(cactus_graph_t graph); -CACTUS_FFI_EXPORT int cactus_graph_get_output_ptr(cactus_graph_t graph, -cactus_node_t node, void** out_ptr); -CACTUS_FFI_EXPORT int cactus_graph_get_output_info(cactus_graph_t graph, -cactus_node_t node, cactus_tensor_info_t* out_info); - #ifdef __cplusplus } #endif diff --git a/cactus-engine/src/init.cpp b/cactus-engine/src/init.cpp index f435f2133..14f0b353e 100644 --- a/cactus-engine/src/init.cpp +++ b/cactus-engine/src/init.cpp @@ -335,8 +335,6 @@ static bool load_corpus_index(CactusModelHandle* handle, const std::string& corp } } -std::string last_error_message; - bool matches_stop_sequence(const std::vector& generated_tokens, const std::vector>& stop_sequences) { for (const auto& stop_seq : stop_sequences) { @@ -351,10 +349,6 @@ bool matches_stop_sequence(const std::vector& generated_tokens, extern "C" { -const char* cactus_get_last_error() { - return last_error_message.c_str(); -} - cactus_model_t cactus_init(const char* model_path, const char* corpus_dir, bool cache_index) { constexpr size_t DEFAULT_CONTEXT_SIZE = 512; // matches default sliding window size @@ -375,7 +369,7 @@ cactus_model_t cactus_init(const char* model_path, const char* corpus_dir, bool try { auto* handle = new CactusModelHandle(); - handle->model = create_model(model_path); + handle->model = create_model(model_path_str); handle->model_name = model_name; if (!handle->model) { diff --git a/cactus-engine/test.sh b/cactus-engine/test.sh index 2769baca9..a5da20aa6 100755 --- a/cactus-engine/test.sh +++ b/cactus-engine/test.sh @@ -1,6 +1,5 @@ #!/bin/bash set -e - cd "$(dirname "$0")" PROJECT_ROOT="$(pwd)/.." @@ -8,89 +7,78 @@ ASSETS_DIR="$(pwd)/tests/assets" IOS_MODE=false ANDROID_MODE=false -ONLY_EXEC="" -MODEL_ARG="" +SUITE="" while [[ $# -gt 0 ]]; do case $1 in - --ios) - IOS_MODE=true - shift - ;; - --android) - ANDROID_MODE=true - shift - ;; - --only) - [ -z "${2:-}" ] && echo "Error: --only requires an argument" && exit 1 - ONLY_EXEC="$2" - shift 2 - ;; - --model) - [ -z "${2:-}" ] && echo "Error: --model requires an argument" && exit 1 - MODEL_ARG="$2" - shift 2 - ;; - *) - shift - ;; + --ios) IOS_MODE=true; shift ;; + --android) ANDROID_MODE=true; shift ;; + --suite) SUITE="${2:?--suite needs an argument}"; shift 2 ;; + --model) CACTUS_TEST_MODEL="${2:?--model needs an argument}"; shift 2 ;; + --transcription-model) CACTUS_TEST_TRANSCRIPTION_MODEL="${2:?--transcription-model needs an argument}"; shift 2 ;; + *) echo "Unknown arg: $1" >&2; exit 2 ;; esac done -MODEL_DIR="${CACTUS_TEST_MODEL:-${MODEL_ARG:-$PROJECT_ROOT/weights/LFM2-VL-450M}}" +resolve_bundle_dir() { + local model="$1" label="$2" + if [ -z "$model" ]; then + echo "Error: --$label is required. Run engine tests via the CLI (cactus test ...) which sets it." >&2 + exit 2 + fi + local dir + if [[ "$model" == /* || "$model" == ./* ]]; then + dir="$model" + else + dir="$PROJECT_ROOT/weights/$(basename "$model" | tr '[:upper:]' '[:lower:]')" + fi + if [ ! -f "$dir/components/manifest.json" ]; then + echo "Bundle missing at $dir. Run: cactus transpile $model" >&2 + exit 1 + fi + echo "$dir" +} + +BUNDLE_DIR="$(resolve_bundle_dir "$CACTUS_TEST_MODEL" "model")" +TRANSCRIPTION_BUNDLE_DIR="$(resolve_bundle_dir "$CACTUS_TEST_TRANSCRIPTION_MODEL" "transcription-model")" if [ "$IOS_MODE" = true ]; then - export CACTUS_TEST_ONLY="$ONLY_EXEC" - exec "$(pwd)/tests/ios/run.sh" "$MODEL_DIR" + export CACTUS_TEST_MODEL="$BUNDLE_DIR" CACTUS_TEST_TRANSCRIPTION_MODEL="$TRANSCRIPTION_BUNDLE_DIR" CACTUS_TEST_SUITE="$SUITE" + exec "$(pwd)/tests/ios/run.sh" fi - if [ "$ANDROID_MODE" = true ]; then - export CACTUS_TEST_ONLY="$ONLY_EXEC" - exec "$(pwd)/tests/android/run.sh" "$MODEL_DIR" -fi - -if [ ! -d "$MODEL_DIR" ]; then - echo "Model weights not found at $MODEL_DIR" - echo "Set CACTUS_TEST_MODEL or download weights first." - exit 1 + export CACTUS_TEST_MODEL="$BUNDLE_DIR" CACTUS_TEST_TRANSCRIPTION_MODEL="$TRANSCRIPTION_BUNDLE_DIR" CACTUS_TEST_SUITE="$SUITE" + exec "$(pwd)/tests/android/run.sh" fi -echo "Building and testing cactus-engine..." -echo "Model: $MODEL_DIR" - -cd "$PROJECT_ROOT/cactus" -rm -rf build -mkdir -p build -cd build -cmake .. -DCMAKE_RULE_MESSAGES=OFF -DCMAKE_VERBOSE_MAKEFILE=OFF > /dev/null 2>&1 -make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) +echo "Model: $CACTUS_TEST_MODEL" +echo "Bundle: $BUNDLE_DIR" +echo "Transcription model: $CACTUS_TEST_TRANSCRIPTION_MODEL" +echo "Transcription bundle: $TRANSCRIPTION_BUNDLE_DIR" cd "$PROJECT_ROOT/cactus-engine/tests" -rm -rf build -mkdir -p build -cd build -cmake .. -DCMAKE_RULE_MESSAGES=OFF -DCMAKE_VERBOSE_MAKEFILE=OFF > /dev/null 2>&1 -make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) +rm -rf build && mkdir build && cd build +cmake .. -DCMAKE_RULE_MESSAGES=OFF -DCMAKE_VERBOSE_MAKEFILE=OFF > /dev/null +make -j"$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)" -echo "" -export CACTUS_TEST_MODEL="$MODEL_DIR" +export CACTUS_TEST_MODEL="$BUNDLE_DIR" +export CACTUS_TEST_TRANSCRIPTION_MODEL="$TRANSCRIPTION_BUNDLE_DIR" export CACTUS_TEST_ASSETS="$ASSETS_DIR" export CACTUS_INDEX_PATH="$ASSETS_DIR" FAILED=0 - -if [ -n "$ONLY_EXEC" ]; then - target="./test_$ONLY_EXEC" +if [ -n "$SUITE" ]; then + target="./test_$SUITE" if [ -x "$target" ]; then - ./"$target" || FAILED=1 + "$target" || FAILED=1 else - echo "Test not found: $target" + echo "Test not found: $target" >&2 FAILED=1 fi else - for test_bin in test_*; do - [ -x "$test_bin" ] && ./"$test_bin" || FAILED=1 + for t in ./test_*; do + [ -x "$t" ] || continue + "$t" || FAILED=1 done fi - exit $FAILED diff --git a/cactus-engine/tests/CMakeLists.txt b/cactus-engine/tests/CMakeLists.txt index ebd22f639..804fa319c 100644 --- a/cactus-engine/tests/CMakeLists.txt +++ b/cactus-engine/tests/CMakeLists.txt @@ -2,76 +2,29 @@ cmake_minimum_required(VERSION 3.10) project(CactusEngineTests LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED True) - -set(CACTUS_ENGINE_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/..) -set(CACTUS_PROJECT_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../..) +set(CMAKE_CXX_STANDARD_REQUIRED ON) if(APPLE) set(CMAKE_OSX_ARCHITECTURES "arm64") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -arch arm64 -pthread -Wall -Wextra -pedantic -O3") - set(CACTUS_CURL_ROOT "${CACTUS_ENGINE_ROOT}/libs/curl") - set(CACTUS_CURL_INCLUDE_DIR "${CACTUS_CURL_ROOT}/include") - set(CACTUS_CURL_LIBRARY_MACOS "${CACTUS_CURL_ROOT}/macos/libcurl.a") - if(NOT EXISTS "${CACTUS_CURL_LIBRARY_MACOS}") - unset(CACTUS_CURL_LIBRARY_MACOS) - find_library(CACTUS_CURL_LIBRARY_MACOS NAMES curl) - endif() -else() - find_package(CURL) endif() -set(CACTUS_LIB "${CACTUS_PROJECT_ROOT}/cactus/build/libcactus.a") -if(NOT EXISTS "${CACTUS_LIB}") - message(FATAL_ERROR "Missing ${CACTUS_LIB}; build the top-level cactus target first") +if(NOT TARGET cactus_engine) + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/.. ${CMAKE_CURRENT_BINARY_DIR}/cactus-engine) endif() -if(NOT APPLE) - find_library(MMAN_LIB mman) -else() - find_library(COREML_FRAMEWORK CoreML REQUIRED) - find_library(FOUNDATION_FRAMEWORK Foundation REQUIRED) - find_library(ACCELERATE_FRAMEWORK Accelerate REQUIRED) - find_library(SECURITY_FRAMEWORK Security REQUIRED) - find_library(SYSTEMCONFIGURATION_FRAMEWORK SystemConfiguration REQUIRED) - find_library(CFNETWORK_FRAMEWORK CFNetwork REQUIRED) -endif() - -function(cactus_link_target target_name) - target_link_libraries(${target_name} PRIVATE ${CACTUS_LIB}) - target_include_directories(${target_name} PRIVATE - ${CACTUS_ENGINE_ROOT} - ${CACTUS_PROJECT_ROOT}/cactus-graph - ${CACTUS_PROJECT_ROOT}/cactus-kernels - ${CACTUS_PROJECT_ROOT}/cactus-kernels/src - ) - if(MMAN_LIB) - target_link_libraries(${target_name} PRIVATE ${MMAN_LIB}) - endif() - if(APPLE) - target_link_libraries(${target_name} PRIVATE - ${ACCELERATE_FRAMEWORK} ${COREML_FRAMEWORK} ${FOUNDATION_FRAMEWORK} - ${SECURITY_FRAMEWORK} ${SYSTEMCONFIGURATION_FRAMEWORK} ${CFNETWORK_FRAMEWORK} - ) - if(CACTUS_CURL_LIBRARY_MACOS) - target_link_libraries(${target_name} PRIVATE ${CACTUS_CURL_LIBRARY_MACOS}) - target_include_directories(${target_name} PRIVATE ${CACTUS_CURL_INCLUDE_DIR}) - endif() - elseif(CURL_FOUND) - target_link_libraries(${target_name} PRIVATE ${CURL_LIBRARIES}) - target_include_directories(${target_name} PRIVATE ${CURL_INCLUDE_DIRS}) - endif() -endfunction() - -file(GLOB TEST_SOURCES CONFIGURE_DEPENDS "test_*.cpp") +file(GLOB TEST_SOURCES "test_*.cpp") list(REMOVE_ITEM TEST_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/test_utils.cpp") foreach(TEST_FILE ${TEST_SOURCES}) get_filename_component(TEST_NAME ${TEST_FILE} NAME_WE) add_executable(${TEST_NAME} ${TEST_FILE} test_utils.cpp) - cactus_link_target(${TEST_NAME}) + target_link_libraries(${TEST_NAME} PRIVATE cactus_engine) endforeach() +if(TARGET test_curl AND EXISTS "${CACTUS_CURL_ROOT}/include/curl/curl.h") + target_include_directories(test_curl PRIVATE "${CACTUS_CURL_ROOT}/include") +endif() + find_package(SDL2 QUIET CONFIG) if(NOT SDL2_FOUND) find_library(SDL2_LIBRARY NAMES SDL2 PATHS /opt/homebrew/lib /usr/local/lib) @@ -81,15 +34,15 @@ if(NOT SDL2_FOUND) endif() endif() if(SDL2_FOUND) - add_executable(chat chat.cpp test_utils.cpp) - cactus_link_target(chat) + add_executable(run run.cpp test_utils.cpp) + target_link_libraries(run PRIVATE cactus_engine) if(TARGET SDL2::SDL2) - target_link_libraries(chat PRIVATE SDL2::SDL2) + target_link_libraries(run PRIVATE SDL2::SDL2) else() - target_link_libraries(chat PRIVATE ${SDL2_LIBRARY}) - target_include_directories(chat PRIVATE ${SDL2_INCLUDE_DIR}) + target_link_libraries(run PRIVATE ${SDL2_LIBRARY}) + target_include_directories(run PRIVATE ${SDL2_INCLUDE_DIR}) endif() endif() -add_executable(asr asr.cpp) -cactus_link_target(asr) +add_executable(transcribe transcribe.cpp) +target_link_libraries(transcribe PRIVATE cactus_engine) diff --git a/cactus-engine/tests/android/CMakeLists.txt b/cactus-engine/tests/android/CMakeLists.txt index a1764f8d8..9ff1fc7c2 100644 --- a/cactus-engine/tests/android/CMakeLists.txt +++ b/cactus-engine/tests/android/CMakeLists.txt @@ -2,81 +2,22 @@ cmake_minimum_required(VERSION 3.10) project(CactusAndroidTests LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED True) -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread -Wall -Wextra -pedantic -O3") +set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(PROJECT_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../../..) -set(CACTUS_ENGINE_DIR ${PROJECT_ROOT}/cactus-engine) -set(CACTUS_GRAPH_DIR ${PROJECT_ROOT}/cactus-graph) -set(CACTUS_KERNELS_DIR ${PROJECT_ROOT}/cactus-kernels) - -set(ANDROID_BUILD_DIR ${PROJECT_ROOT}/android/build) -set(CACTUS_LIB ${ANDROID_BUILD_DIR}/cactus/libcactus.a) - -set(CACTUS_CURL_ROOT "$ENV{CACTUS_CURL_ROOT}") -if(NOT CACTUS_CURL_ROOT) - set(CACTUS_CURL_ROOT ${CACTUS_ENGINE_DIR}/libs/curl) -endif() -set(CACTUS_CURL_INCLUDE_DIR "${CACTUS_CURL_ROOT}/include") -set(CACTUS_CURL_LIBRARY "${CACTUS_CURL_ROOT}/android/arm64-v8a/libcurl.a") - -set(CACTUS_MBEDTLS_ROOT ${PROJECT_ROOT}/android/mbedtls/arm64-v8a) -if(NOT EXISTS "${CACTUS_MBEDTLS_ROOT}") - set(CACTUS_MBEDTLS_ROOT ${PROJECT_ROOT}/libs/mbedtls/android/arm64-v8a) -endif() -set(CACTUS_MBEDTLS_CRYPTO_LIBRARY "${CACTUS_MBEDTLS_ROOT}/lib/libmbedcrypto.a") -set(CACTUS_MBEDTLS_TLS_LIBRARY "${CACTUS_MBEDTLS_ROOT}/lib/libmbedtls.a") -set(CACTUS_MBEDTLS_X509_LIBRARY "${CACTUS_MBEDTLS_ROOT}/lib/libmbedx509.a") - -if(NOT EXISTS ${CACTUS_LIB}) - message(FATAL_ERROR "Cactus library not found at: ${CACTUS_LIB}") +if(NOT TARGET cactus_engine) + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../.. ${CMAKE_CURRENT_BINARY_DIR}/cactus-engine) endif() -message(STATUS "Using Cactus library: ${CACTUS_LIB}") -message(STATUS "Using vendored curl root: ${CACTUS_CURL_ROOT}") - set(TESTS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/..) file(GLOB TEST_SOURCES "${TESTS_DIR}/test_*.cpp") list(REMOVE_ITEM TEST_SOURCES "${TESTS_DIR}/test_utils.cpp") -function(link_vendored_curl_if_present target_name) - if(CACTUS_CURL_ROOT AND EXISTS "${CACTUS_CURL_INCLUDE_DIR}/curl/curl.h" AND EXISTS "${CACTUS_CURL_LIBRARY}") - if(NOT EXISTS "${CACTUS_MBEDTLS_CRYPTO_LIBRARY}" OR - NOT EXISTS "${CACTUS_MBEDTLS_TLS_LIBRARY}" OR - NOT EXISTS "${CACTUS_MBEDTLS_X509_LIBRARY}") - message(FATAL_ERROR - "Vendored Android libcurl is present but mbedTLS static libs are missing.\n" - "Expected:\n" - " ${CACTUS_MBEDTLS_CRYPTO_LIBRARY}\n" - " ${CACTUS_MBEDTLS_TLS_LIBRARY}\n" - " ${CACTUS_MBEDTLS_X509_LIBRARY}") - endif() - target_include_directories(${target_name} PRIVATE "${CACTUS_CURL_INCLUDE_DIR}") - target_link_libraries(${target_name} PRIVATE - "${CACTUS_CURL_LIBRARY}" - "${CACTUS_MBEDTLS_TLS_LIBRARY}" - "${CACTUS_MBEDTLS_X509_LIBRARY}" - "${CACTUS_MBEDTLS_CRYPTO_LIBRARY}" - ) - message(STATUS "Target ${target_name} will link vendored libcurl: ${CACTUS_CURL_LIBRARY}") - endif() -endfunction() - -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) - foreach(TEST_FILE ${TEST_SOURCES}) get_filename_component(TEST_NAME ${TEST_FILE} NAME_WE) - add_executable(${TEST_NAME} - ${TEST_FILE} - ${TESTS_DIR}/test_utils.cpp - ) - target_link_libraries(${TEST_NAME} PRIVATE ${CACTUS_LIB}) - link_vendored_curl_if_present(${TEST_NAME}) - target_include_directories(${TEST_NAME} PRIVATE - ${CACTUS_ENGINE_DIR} - ${CACTUS_GRAPH_DIR} - ${CACTUS_KERNELS_DIR} - ${CACTUS_KERNELS_DIR}/src - ) - message(STATUS "Added Android test: ${TEST_NAME}") + add_executable(${TEST_NAME} ${TEST_FILE} ${TESTS_DIR}/test_utils.cpp) + target_link_libraries(${TEST_NAME} PRIVATE cactus_engine) endforeach() + +if(TARGET test_curl AND EXISTS "${CACTUS_CURL_ROOT}/include/curl/curl.h") + target_include_directories(test_curl PRIVATE "${CACTUS_CURL_ROOT}/include") +endif() diff --git a/cactus-engine/tests/android/run.sh b/cactus-engine/tests/android/run.sh index db94a4cb2..1d2e142e6 100755 --- a/cactus-engine/tests/android/run.sh +++ b/cactus-engine/tests/android/run.sh @@ -5,12 +5,12 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" CACTUS_CURL_ROOT="${CACTUS_CURL_ROOT:-$PROJECT_ROOT/cactus-engine/libs/curl}" export CACTUS_CURL_ROOT -MODEL_NAME="$1" - -if [ -z "$MODEL_NAME" ]; then - echo "Usage: run.sh " - exit 1 -fi +for var in CACTUS_TEST_MODEL CACTUS_TEST_TRANSCRIPTION_MODEL; do + if [ -z "${!var:-}" ] || [ ! -d "${!var}" ]; then + echo "Error: $var must point to a prepared bundle (got: '${!var:-}')" >&2 + exit 1 + fi +done echo "Running Cactus tests on Android..." echo "============================" @@ -182,9 +182,6 @@ if ! adb -s "$DEVICE_ID" shell echo "test" &>/dev/null; then exit 1 fi -echo "" -echo "Step 2: Building Cactus static library for Android..." - if [ -z "$ANDROID_NDK_HOME" ]; then if [ -n "$ANDROID_HOME" ]; then ANDROID_NDK_HOME=$(ls -d "$ANDROID_HOME/ndk/"* 2>/dev/null | sort -V | tail -1) @@ -201,27 +198,15 @@ fi cmake_toolchain="$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake" android_platform=${ANDROID_PLATFORM:-android-21} -android_lib_build_dir="$PROJECT_ROOT/android/build" n_jobs=$(nproc 2>/dev/null || sysctl -n hw.logicalcpu 2>/dev/null || echo 4) -cmake -DCMAKE_TOOLCHAIN_FILE="$cmake_toolchain" \ - -DANDROID_ABI="arm64-v8a" \ - -DANDROID_PLATFORM="$android_platform" \ - -DCMAKE_BUILD_TYPE=Release \ - -DCACTUS_CURL_ROOT="$CACTUS_CURL_ROOT" \ - -S "$PROJECT_ROOT/android" \ - -B "$android_lib_build_dir" >/dev/null - -if ! cmake --build "$android_lib_build_dir" --target cactus -j "$n_jobs" >/dev/null; then - echo "Failed to build Cactus static library" - exit 1 -fi +echo "" +echo "Step 2: Building Cactus library for Android..." -if [ ! -f "$android_lib_build_dir/cactus/libcactus.a" ]; then - echo "Static library not found at $android_lib_build_dir/cactus/libcactus.a" +if ! "$PROJECT_ROOT/android/build.sh"; then + echo "Failed to build Cactus library" exit 1 fi -echo "Static library built: $android_lib_build_dir/cactus/libcactus.a" echo "" echo "Step 3: Building Android tests..." @@ -237,6 +222,7 @@ if ! cmake -S "$android_test_dir" -B "$android_build_dir" \ -DANDROID_ABI="arm64-v8a" \ -DANDROID_PLATFORM="$android_platform" \ -DCMAKE_BUILD_TYPE=Release \ + -DCACTUS_CURL_ROOT="$CACTUS_CURL_ROOT" \ -DCMAKE_RULE_MESSAGES=OFF \ -DCMAKE_VERBOSE_MAKEFILE=OFF; then echo "Failed to configure tests" @@ -259,13 +245,13 @@ fi test_executables=() for test_exe in "${all_test_executables[@]}"; do test_name=$(basename "$test_exe" | sed 's/^test_//') - if [ -z "${CACTUS_TEST_ONLY:-}" ] || [ "$test_name" = "$CACTUS_TEST_ONLY" ]; then + if [ -z "${CACTUS_TEST_SUITE:-}" ] || [ "$test_name" = "$CACTUS_TEST_SUITE" ]; then test_executables+=("$test_exe") fi done if [ ${#test_executables[@]} -eq 0 ]; then - echo "No test executables match filter: $CACTUS_TEST_ONLY" + echo "No test executables match filter: $CACTUS_TEST_SUITE" exit 1 fi @@ -274,36 +260,53 @@ echo "Found ${#test_executables[@]} test executable(s)" echo "" echo "Step 4: Deploying to device..." -model_src="$MODEL_NAME" -model_dir=$(basename "$model_src") +model_dir=$(basename "$CACTUS_TEST_MODEL") +transcription_dir=$(basename "$CACTUS_TEST_TRANSCRIPTION_MODEL") assets_src="$PROJECT_ROOT/cactus-engine/tests/assets" device_test_dir="/data/local/tmp/cactus_tests" device_model_dir="/data/local/tmp/cactus_models" device_assets_dir="/data/local/tmp/cactus_assets" +adb -s "$DEVICE_ID" shell "rm -rf $device_test_dir $device_model_dir $device_assets_dir" adb -s "$DEVICE_ID" shell "mkdir -p $device_test_dir $device_model_dir $device_assets_dir" echo "Pushing model weights..." -adb -s "$DEVICE_ID" push "$model_src" "$device_model_dir/" +if ! adb -s "$DEVICE_ID" push "$CACTUS_TEST_MODEL" "$device_model_dir/" >/dev/null; then + echo "Failed to push model weights" + exit 1 +fi + +echo "Pushing transcription model..." +if ! adb -s "$DEVICE_ID" push "$CACTUS_TEST_TRANSCRIPTION_MODEL" "$device_model_dir/" >/dev/null; then + echo "Failed to push transcription model" + exit 1 +fi if [ -d "$assets_src" ]; then echo "Pushing test assets..." - adb -s "$DEVICE_ID" push "$assets_src" "$device_assets_dir/" + if ! adb -s "$DEVICE_ID" push "$assets_src" "$device_assets_dir/" >/dev/null; then + echo "Failed to push test assets" + exit 1 + fi fi echo "Pushing test executables..." for test_exe in "${test_executables[@]}"; do test_name=$(basename "$test_exe") - adb -s "$DEVICE_ID" push "$test_exe" "$device_test_dir/" + if ! adb -s "$DEVICE_ID" push "$test_exe" "$device_test_dir/" >/dev/null; then + echo "Failed to push $test_name" + exit 1 + fi adb -s "$DEVICE_ID" shell "chmod +x $device_test_dir/$test_name" done echo "" echo "Step 5: Running tests..." echo "------------------------" -echo "Using model path: $device_model_dir/$model_dir" -echo "Using assets path: $device_assets_dir/assets" +echo "Using model path: $device_model_dir/$model_dir" +echo "Using transcription model path: $device_model_dir/$transcription_dir" +echo "Using assets path: $device_assets_dir/assets" FAILED=0 for test_exe in "${test_executables[@]}"; do @@ -313,6 +316,7 @@ for test_exe in "${test_executables[@]}"; do if ! adb -s "$DEVICE_ID" shell "cd $device_test_dir && \ export CACTUS_TEST_MODEL=$device_model_dir/$model_dir && \ + export CACTUS_TEST_TRANSCRIPTION_MODEL=$device_model_dir/$transcription_dir && \ export CACTUS_TEST_ASSETS=$device_assets_dir/assets && \ export CACTUS_INDEX_PATH=$device_assets_dir/assets && \ export CACTUS_NO_CLOUD_TELE=${CACTUS_NO_CLOUD_TELE:-1} && \ diff --git a/cactus-engine/tests/ios/configure_xcode.rb b/cactus-engine/tests/ios/configure_xcode.rb index 0490e8e47..6a2c01c7f 100755 --- a/cactus-engine/tests/ios/configure_xcode.rb +++ b/cactus-engine/tests/ios/configure_xcode.rb @@ -21,7 +21,7 @@ def generate_app_delegate(output_path, test_files) extern_declarations = test_names.map { |name| "extern int #{name}_main();" }.join("\n") test_calls = test_names.map { |name| filter_name = name.sub(/^test_/, '') - " if (should_run(\"#{filter_name}\")) #{name}_main();" + " if (should_run(\"#{filter_name}\")) failed |= (#{name}_main() != 0);" }.join("\n") app_delegate_content = <<~OBJC @@ -29,7 +29,7 @@ def generate_app_delegate(output_path, test_files) #import "AppDelegate.h" #import #import -#include "cactus.h" +#include "cactus_engine.h" #include #{extern_declarations} @@ -73,6 +73,7 @@ def generate_app_delegate(output_path, test_files) NSString *bundlePath = [[NSBundle mainBundle] bundlePath]; [self copyFromBundle:bundlePath toDocuments:getenv("CACTUS_TEST_MODEL")]; + [self copyFromBundle:bundlePath toDocuments:getenv("CACTUS_TEST_TRANSCRIPTION_MODEL")]; [self copyFromBundle:bundlePath toDocuments:getenv("CACTUS_TEST_ASSETS")]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @@ -82,9 +83,12 @@ def generate_app_delegate(output_path, test_files) return filter.empty() || filter == name; }; + int failed = 0; #{test_calls} - exit(0); + FILE* f = fopen("cactus_test.exitcode", "w"); + if (f) { fprintf(f, "%d\\n", failed); fclose(f); } + exit(failed); }); return YES; @@ -163,13 +167,12 @@ def generate_app_delegate(output_path, test_files) apple_dir = File.join(project_root, 'apple') static_lib_path = device_type == 'simulator' ? - File.join(apple_dir, 'libcactus-simulator.a') : - File.join(apple_dir, 'libcactus-device.a') + File.join(apple_dir, 'libcactus_engine-simulator.a') : + File.join(apple_dir, 'libcactus_engine-device.a') fail_with("Static library not found at: #{static_lib_path}") unless File.exist?(static_lib_path) puts "Using static library: #{static_lib_path}" -cactus_dir = File.join(project_root, 'cactus') cactus_engine_dir = File.join(project_root, 'cactus-engine') cactus_graph_dir = File.join(project_root, 'cactus-graph') cactus_kernels_dir = File.join(project_root, 'cactus-kernels') @@ -203,7 +206,7 @@ def generate_app_delegate(output_path, test_files) target.build_configurations.each do |config| config.build_settings['HEADER_SEARCH_PATHS'] ||= ['$(inherited)'] - [tests_root, cactus_dir, cactus_engine_dir, cactus_graph_dir, cactus_kernels_dir, File.join(cactus_kernels_dir, 'src')].each do |path| + [tests_root, cactus_engine_dir, cactus_graph_dir, cactus_kernels_dir, File.join(cactus_kernels_dir, 'src')].each do |path| config.build_settings['HEADER_SEARCH_PATHS'] << path unless config.build_settings['HEADER_SEARCH_PATHS'].include?(path) end if curl_root && !curl_root.empty? diff --git a/cactus-engine/tests/ios/run.sh b/cactus-engine/tests/ios/run.sh index e9ddd05f9..4125d5d1e 100755 --- a/cactus-engine/tests/ios/run.sh +++ b/cactus-engine/tests/ios/run.sh @@ -5,12 +5,12 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" CACTUS_CURL_ROOT="${CACTUS_CURL_ROOT:-$PROJECT_ROOT/cactus-engine/libs/curl}" export CACTUS_CURL_ROOT -MODEL_NAME="$1" - -if [ -z "$MODEL_NAME" ]; then - echo "Usage: run.sh " - exit 1 -fi +for var in CACTUS_TEST_MODEL CACTUS_TEST_TRANSCRIPTION_MODEL; do + if [ -z "${!var:-}" ] || [ ! -d "${!var}" ]; then + echo "Error: $var must point to a prepared bundle (got: '${!var:-}')" >&2 + exit 1 + fi +done echo "Running Cactus tests on iOS..." echo "============================" @@ -309,19 +309,21 @@ fi echo "" echo "Step 5: Bundling model weights and assets..." -model_src="$MODEL_NAME" -model_dir=$(basename "$model_src") +model_dir=$(basename "$CACTUS_TEST_MODEL") +transcription_dir=$(basename "$CACTUS_TEST_TRANSCRIPTION_MODEL") assets_src="$PROJECT_ROOT/cactus-engine/tests/assets" -if [ ! -d "$model_src" ]; then - echo "Error: model weights not found at $model_src" +echo "Copying model weights to app bundle..." +rm -rf "$app_path/$model_dir" +if ! cp -R "$CACTUS_TEST_MODEL" "$app_path/"; then + echo "Error: Could not copy model weights from $CACTUS_TEST_MODEL" exit 1 fi -echo "Copying model weights to app bundle..." -rm -rf "$app_path/$model_dir" -if ! cp -R "$model_src" "$app_path/"; then - echo "Error: Could not copy model weights from $model_src" +echo "Copying transcription model to app bundle..." +rm -rf "$app_path/$transcription_dir" +if ! cp -R "$CACTUS_TEST_TRANSCRIPTION_MODEL" "$app_path/"; then + echo "Error: Could not copy transcription model from $CACTUS_TEST_TRANSCRIPTION_MODEL" exit 1 fi @@ -349,18 +351,30 @@ if [ "$device_type" = "simulator" ]; then fi echo "Launching tests..." - echo "Using model path: $model_dir" - echo "Using assets path: assets" + echo "Using model path: $model_dir" + echo "Using transcription model path: $transcription_dir" + echo "Using assets path: assets" sim_env=( "SIMCTL_CHILD_CACTUS_TEST_MODEL=$model_dir" + "SIMCTL_CHILD_CACTUS_TEST_TRANSCRIPTION_MODEL=$transcription_dir" "SIMCTL_CHILD_CACTUS_TEST_ASSETS=assets" "SIMCTL_CHILD_CACTUS_INDEX_PATH=assets" "SIMCTL_CHILD_CACTUS_NO_CLOUD_TELE=${CACTUS_NO_CLOUD_TELE:-1}" - "SIMCTL_CHILD_CACTUS_TEST_ONLY=${CACTUS_TEST_ONLY:-}" + "SIMCTL_CHILD_CACTUS_TEST_ONLY=${CACTUS_TEST_SUITE:-}" ) - env "${sim_env[@]}" xcrun simctl launch --console-pty "$device_uuid" "$bundle_id" + env "${sim_env[@]}" xcrun simctl launch --console-pty --terminate-running-process "$device_uuid" "$bundle_id" + + data_container=$(xcrun simctl get_app_container "$device_uuid" "$bundle_id" data 2>/dev/null || true) + exitcode_file="$data_container/Documents/cactus_test.exitcode" + if [ -n "$data_container" ] && [ -f "$exitcode_file" ]; then + SUITE_EXIT=$(tr -d '[:space:]' < "$exitcode_file") + [[ "$SUITE_EXIT" =~ ^[0-9]+$ ]] || SUITE_EXIT=1 + else + echo "Could not retrieve exit-code marker from simulator" + SUITE_EXIT=1 + fi else echo "Installing on: $device_name" @@ -375,19 +389,22 @@ else echo "Launching tests..." echo "(Logs will be fetched from device after completion)" - echo "Using model path: $model_dir" - echo "Using assets path: assets" + echo "Using model path: $model_dir" + echo "Using transcription model path: $transcription_dir" + echo "Using assets path: assets" device_env=( "DEVICECTL_CHILD_CACTUS_TEST_MODEL=$model_dir" + "DEVICECTL_CHILD_CACTUS_TEST_TRANSCRIPTION_MODEL=$transcription_dir" "DEVICECTL_CHILD_CACTUS_TEST_ASSETS=assets" "DEVICECTL_CHILD_CACTUS_INDEX_PATH=assets" "DEVICECTL_CHILD_CACTUS_NO_CLOUD_TELE=${CACTUS_NO_CLOUD_TELE:-1}" - "DEVICECTL_CHILD_CACTUS_TEST_ONLY=${CACTUS_TEST_ONLY:-}" + "DEVICECTL_CHILD_CACTUS_TEST_ONLY=${CACTUS_TEST_SUITE:-}" ) env "${device_env[@]}" \ - xcrun devicectl device process launch --device "$device_uuid" "$bundle_id" 2>&1 || true + xcrun devicectl device process launch --device "$device_uuid" "$bundle_id" 2>&1 + SUITE_EXIT=$? echo "Waiting for tests to complete..." max_wait=300 @@ -415,6 +432,11 @@ else --domain-type appDataContainer \ --source Documents/cactus_test.log \ --destination "$log_dir/cactus_test.log" 2>/dev/null || true + xcrun devicectl device copy from --device "$device_uuid" \ + --domain-identifier "$bundle_id" \ + --domain-type appDataContainer \ + --source Documents/cactus_test.exitcode \ + --destination "$log_dir/exitcode" 2>/dev/null || true if [ -f "$log_dir/cactus_test.log" ]; then echo "=== Device Test Output ===" @@ -423,8 +445,21 @@ else else echo "Could not retrieve test logs from device" fi + + if [ -f "$log_dir/exitcode" ]; then + SUITE_EXIT=$(tr -d '[:space:]' < "$log_dir/exitcode") + [[ "$SUITE_EXIT" =~ ^[0-9]+$ ]] || SUITE_EXIT=1 + else + echo "Could not retrieve exit-code marker from device" + SUITE_EXIT=1 + fi rm -rf "$log_dir" fi echo "" -echo "Tests complete." +if [ "${SUITE_EXIT:-1}" -eq 0 ]; then + echo "All tests passed." +else + echo "Some tests failed." +fi +exit "${SUITE_EXIT:-1}" diff --git a/cactus-engine/tests/chat.cpp b/cactus-engine/tests/run.cpp similarity index 99% rename from cactus-engine/tests/chat.cpp rename to cactus-engine/tests/run.cpp index b578d5b16..7fcdcb0fb 100644 --- a/cactus-engine/tests/chat.cpp +++ b/cactus-engine/tests/run.cpp @@ -585,7 +585,7 @@ int main(int argc, char** argv) { } input = record_prompt.empty() ? "Transcribe or respond to this audio." : record_prompt; #else - std::cerr << "Recording requires SDL2, but this chat binary was built without SDL2.\n"; + std::cerr << "Recording requires SDL2, but this binary was built without SDL2.\n"; continue; #endif } diff --git a/tests/test_curl.cpp b/cactus-engine/tests/test_curl.cpp similarity index 58% rename from tests/test_curl.cpp rename to cactus-engine/tests/test_curl.cpp index 222216204..e73296c1c 100644 --- a/tests/test_curl.cpp +++ b/cactus-engine/tests/test_curl.cpp @@ -6,40 +6,24 @@ #include #include -#if __has_include() #include -#define CACTUS_TEST_HAS_CURL 1 -#else -#define CACTUS_TEST_HAS_CURL 0 -#endif bool test_curl_version_info() { -#if !CACTUS_TEST_HAS_CURL - return false; -#else curl_version_info_data* info = curl_version_info(CURLVERSION_NOW); if (!info) return false; if (!info->version || std::string(info->version).empty()) return false; if (!info->host || std::string(info->host).empty()) return false; return true; -#endif } bool test_curl_easy_init() { -#if !CACTUS_TEST_HAS_CURL - return false; -#else CURL* handle = curl_easy_init(); if (!handle) return false; curl_easy_cleanup(handle); return true; -#endif } bool test_curl_url_api() { -#if !CACTUS_TEST_HAS_CURL - return false; -#else CURL* handle = curl_easy_init(); if (!handle) return false; @@ -51,39 +35,10 @@ bool test_curl_url_api() { curl_easy_cleanup(handle); return ok; -#endif -} - -bool test_curl_http_request() { -#if !CACTUS_TEST_HAS_CURL - return false; -#else - CURL* handle = curl_easy_init(); - if (!handle) return false; - - bool ok = true; - ok = ok && (curl_easy_setopt(handle, CURLOPT_URL, "https://example.com/") == CURLE_OK); - ok = ok && (curl_easy_setopt(handle, CURLOPT_NOBODY, 1L) == CURLE_OK); - ok = ok && (curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1L) == CURLE_OK); - ok = ok && (curl_easy_setopt(handle, CURLOPT_TIMEOUT_MS, 3000L) == CURLE_OK); - ok = ok && (curl_easy_setopt(handle, CURLOPT_CONNECTTIMEOUT_MS, 2000L) == CURLE_OK); - if (!ok) { - curl_easy_cleanup(handle); - return false; - } - - CURLcode rc = curl_easy_perform(handle); - long http_code = 0; - curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &http_code); - curl_easy_cleanup(handle); - - return (rc == CURLE_OK) && (http_code >= 100); -#endif } struct CurlHttpCheck { bool pass = false; - bool skip = false; std::string reason; long http_code = 0; std::string body_preview; @@ -96,22 +51,7 @@ static size_t curl_write_to_string(char* ptr, size_t size, size_t nmemb, void* u return size * nmemb; } -static bool curl_supports_protocol(const char* name) { -#if !CACTUS_TEST_HAS_CURL - (void)name; - return false; -#else - curl_version_info_data* info = curl_version_info(CURLVERSION_NOW); - if (!info || !info->protocols) return false; - for (const char* const* p = info->protocols; *p; ++p) { - if (std::string(*p) == name) return true; - } - return false; -#endif -} - static void configure_curl_tls(CURL* handle) { -#if CACTUS_TEST_HAS_CURL if (!handle) return; curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(handle, CURLOPT_SSL_VERIFYHOST, 2L); @@ -127,21 +67,11 @@ static void configure_curl_tls(CURL* handle) { curl_easy_setopt(handle, CURLOPT_CAPATH, "/system/etc/security/cacerts"); } #endif -#else - (void)handle; -#endif } CurlHttpCheck run_curl_http_request_check() { -#if !CACTUS_TEST_HAS_CURL - return {false, true, "curl/curl.h not available in include path"}; -#else - if (!curl_supports_protocol("https")) { - return {false, false, "libcurl built without HTTPS protocol support", 0, ""}; - } - CURL* handle = curl_easy_init(); - if (!handle) return {false, false, "curl_easy_init returned null", 0, ""}; + if (!handle) return {false, "curl_easy_init returned null", 0, ""}; bool opts_ok = true; std::string body; @@ -156,7 +86,7 @@ CurlHttpCheck run_curl_http_request_check() { configure_curl_tls(handle); if (!opts_ok) { curl_easy_cleanup(handle); - return {false, false, "failed to configure curl options", 0, ""}; + return {false, "failed to configure curl options", 0, ""}; } CURLcode rc = curl_easy_perform(handle); @@ -167,28 +97,21 @@ CurlHttpCheck run_curl_http_request_check() { if (rc == CURLE_OK && http_code >= 200 && http_code < 400) { if (body.find("badssl.com") != std::string::npos) { - return {true, false, "", http_code, body_preview}; + return {true, "", http_code, body_preview}; } - std::ostringstream oss; - oss << "https request succeeded but response body did not contain expected marker"; - return {false, false, oss.str(), http_code, body_preview}; + return {false, "https request succeeded but response body did not contain expected marker", + http_code, body_preview}; } std::ostringstream oss; oss << "request failed rc=" << static_cast(rc) << " (" << curl_easy_strerror(rc) << "), http_code=" << http_code; - return {false, false, oss.str(), http_code, body_preview}; -#endif + return {false, oss.str(), http_code, body_preview}; } int main() { TestUtils::TestRunner runner("Curl Tests"); -#if !CACTUS_TEST_HAS_CURL - runner.log_skip("curl_headers", "curl/curl.h not available in include path"); - runner.print_summary(); - return 0; -#else CURLcode init_rc = curl_global_init(CURL_GLOBAL_DEFAULT); if (init_rc != CURLE_OK) { runner.run_test("global_init", false); @@ -200,23 +123,18 @@ int main() { runner.run_test("easy_init", test_curl_easy_init()); runner.run_test("url_api", test_curl_url_api()); CurlHttpCheck http_check = run_curl_http_request_check(); - if (http_check.skip) { - runner.log_skip("http_request", http_check.reason); - } else { - runner.run_test("http_request", http_check.pass); - if (http_check.http_code > 0) { - std::cout << " http_code: " << http_check.http_code << "\n"; - } - if (!http_check.body_preview.empty()) { - std::cout << " body_preview: " << http_check.body_preview << "\n"; - } - if (!http_check.pass && !http_check.reason.empty()) { - std::cout << " reason: " << http_check.reason << "\n"; - } + runner.run_test("http_request", http_check.pass); + if (http_check.http_code > 0) { + std::cout << " http_code: " << http_check.http_code << "\n"; + } + if (!http_check.body_preview.empty()) { + std::cout << " body_preview: " << http_check.body_preview << "\n"; + } + if (!http_check.pass && !http_check.reason.empty()) { + std::cout << " reason: " << http_check.reason << "\n"; } curl_global_cleanup(); runner.print_summary(); return runner.all_passed() ? 0 : 1; -#endif } diff --git a/cactus-engine/tests/test_embed.cpp b/cactus-engine/tests/test_embed.cpp index 6c19e6e0d..41c913659 100644 --- a/cactus-engine/tests/test_embed.cpp +++ b/cactus-engine/tests/test_embed.cpp @@ -5,7 +5,7 @@ using namespace EngineTestUtils; static const char* g_model_path = std::getenv("CACTUS_TEST_MODEL"); -static const char* g_transcribe_model_path = std::getenv("CACTUS_TEST_TRANSCRIBE_MODEL"); +static const char* g_transcription_model_path = std::getenv("CACTUS_TEST_TRANSCRIPTION_MODEL"); static const char* g_assets_path = std::getenv("CACTUS_TEST_ASSETS"); bool test_embeddings() { @@ -18,16 +18,21 @@ bool test_embeddings() { const char* texts[] = {"My name is Henry Ndubuaku", "Your name is Henry Ndubuaku"}; std::vector emb1(2048), emb2(2048); - size_t dim1, dim2; + size_t dim1 = 0, dim2 = 0; Timer t1; - cactus_embed(model, texts[0], emb1.data(), emb1.size() * sizeof(float), &dim1, true); + int rc1 = cactus_embed(model, texts[0], emb1.data(), emb1.size() * sizeof(float), &dim1, true); double time1 = t1.elapsed_ms(); Timer t2; - cactus_embed(model, texts[1], emb2.data(), emb2.size() * sizeof(float), &dim2, true); + int rc2 = cactus_embed(model, texts[1], emb2.data(), emb2.size() * sizeof(float), &dim2, true); double time2 = t2.elapsed_ms(); + if (rc1 < 0 || rc2 < 0 || dim1 == 0 || dim1 != dim2 || dim1 > emb1.size()) { + cactus_destroy(model); + return false; + } + float similarity = 0; for (size_t i = 0; i < dim1; ++i) { similarity += emb1[i] * emb2[i]; @@ -48,21 +53,13 @@ static bool test_image_embeddings() { << "║ IMAGE EMBEDDING TEST ║\n" << "╚══════════════════════════════════════════╝\n"; - if (!g_model_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_MODEL not set\n"; - return true; - } - std::string image_path = std::string(g_assets_path) + "/test_monkey.png"; const size_t buffer_size = 1024 * 1024 * 4; std::vector embeddings(buffer_size / sizeof(float)); size_t embedding_dim = 0; cactus_model_t model = cactus_init(g_model_path, nullptr, false); - if (!model) { - std::cout << "⊘ SKIP │ Model doesn't support image embeddings\n"; - return true; - } + if (!model) return false; Timer t; int result = cactus_image_embed(model, image_path.c_str(), embeddings.data(), buffer_size, &embedding_dim); @@ -70,11 +67,6 @@ static bool test_image_embeddings() { cactus_destroy(model); - if (result == -1) { - std::cout << "⊘ SKIP │ Model doesn't support image embeddings\n"; - return true; - } - std::cout << "├─ Embedding dim: " << embedding_dim << "\n" << "└─ Time: " << std::fixed << std::setprecision(2) << elapsed << "ms" << std::endl; @@ -86,20 +78,12 @@ static bool test_audio_embeddings() { << "║ AUDIO EMBEDDING TEST ║\n" << "╚══════════════════════════════════════════╝\n"; - if (!g_transcribe_model_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_TRANSCRIBE_MODEL not set\n"; - return true; - } - const size_t buffer_size = 1024 * 1024; std::vector embeddings(buffer_size / sizeof(float)); size_t embedding_dim = 0; - cactus_model_t model = cactus_init(g_transcribe_model_path, nullptr, false); - if (!model) { - std::cout << "⊘ SKIP │ Failed to init audio model\n"; - return true; - } + cactus_model_t model = cactus_init(g_transcription_model_path, nullptr, false); + if (!model) return false; std::string audio_path = std::string(g_assets_path) + "/test.wav"; Timer t; @@ -108,11 +92,6 @@ static bool test_audio_embeddings() { cactus_destroy(model); - if (result == -1) { - std::cout << "⊘ SKIP │ Model doesn't support audio embeddings\n"; - return true; - } - std::cout << "├─ Embedding dim: " << embedding_dim << "\n" << "└─ Time: " << std::fixed << std::setprecision(2) << elapsed << "ms" << std::endl; diff --git a/cactus-engine/tests/test_llm.cpp b/cactus-engine/tests/test_llm.cpp index ebbb097ac..56382474d 100644 --- a/cactus-engine/tests/test_llm.cpp +++ b/cactus-engine/tests/test_llm.cpp @@ -561,19 +561,15 @@ bool test_1k_context() { int main() { TestUtils::TestRunner runner("LLM Tests"); - const char* only = std::getenv("CACTUS_TEST_ONLY"); - auto should_run = [&](const char* name) { - return only == nullptr || std::string(only) == name; - }; - if (should_run("1k_context")) runner.run_test("1k_context", test_1k_context()); - if (should_run("streaming")) runner.run_test("streaming", test_streaming()); - if (should_run("prefill")) runner.run_test("prefill", test_prefill()); - if (should_run("prefill_idempotent_reuse")) runner.run_test("prefill_idempotent_reuse", test_prefill_idempotent_reuse()); - if (should_run("prefill_prefix_extension_reuse")) runner.run_test("prefill_prefix_extension_reuse", test_prefill_prefix_extension_reuse()); - if (should_run("prefill_invalidated_on_message_change")) runner.run_test("prefill_invalidated_on_message_change", test_prefill_invalidated_on_message_change()); - if (should_run("tool_calls")) runner.run_test("tool_calls", test_tool_call()); - if (should_run("tool_multiple_tool_call_invocations")) runner.run_test("tool_multiple_tool_call_invocations", test_multiple_tool_call_invocations()); - if (should_run("tool_calls_with_three_tools")) runner.run_test("tool_calls_with_three_tools", test_tool_call_with_three_tools()); + runner.run_test("1k_context", test_1k_context()); + runner.run_test("streaming", test_streaming()); + runner.run_test("prefill", test_prefill()); + runner.run_test("prefill_idempotent_reuse", test_prefill_idempotent_reuse()); + runner.run_test("prefill_prefix_extension_reuse", test_prefill_prefix_extension_reuse()); + runner.run_test("prefill_invalidated_on_message_change", test_prefill_invalidated_on_message_change()); + runner.run_test("tool_calls", test_tool_call()); + runner.run_test("tool_multiple_tool_call_invocations", test_multiple_tool_call_invocations()); + runner.run_test("tool_calls_with_three_tools", test_tool_call_with_three_tools()); runner.print_summary(); return runner.all_passed() ? 0 : 1; } diff --git a/cactus-engine/tests/test_rag.cpp b/cactus-engine/tests/test_rag.cpp index 5aad3b6fc..14d2c0398 100644 --- a/cactus-engine/tests/test_rag.cpp +++ b/cactus-engine/tests/test_rag.cpp @@ -1,7 +1,6 @@ #include "test_utils.h" #include #include -#include #include #include @@ -37,44 +36,8 @@ bool test_rag() { << "║ RAG TEST ║\n" << "╚══════════════════════════════════════════╝\n"; - if (!g_model_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_MODEL not set\n"; - return true; - } - - if (!g_assets_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_ASSETS not set\n"; - return true; - } - std::string corpus_dir = std::string(g_assets_path) + "/rag_corpus"; - DIR* dir = opendir(corpus_dir.c_str()); - if (!dir) { - std::cout << "⊘ SKIP │ RAG corpus directory not found at " << corpus_dir << "\n"; - return true; - } - - bool has_corpus_files = false; - struct dirent* entry; - while ((entry = readdir(dir)) != nullptr) { - std::string name = entry->d_name; - if (name.size() > 4 && name.substr(name.size() - 4) == ".txt") { - has_corpus_files = true; - break; - } - if (name.size() > 3 && name.substr(name.size() - 3) == ".md") { - has_corpus_files = true; - break; - } - } - closedir(dir); - - if (!has_corpus_files) { - std::cout << "⊘ SKIP │ No .txt or .md files found in " << corpus_dir << "\n"; - return true; - } - std::cout << "├─ Corpus dir: " << corpus_dir << "\n"; std::cout << "├─ Initializing model with RAG...\n"; diff --git a/cactus-engine/tests/test_stt.cpp b/cactus-engine/tests/test_stt.cpp index 55f9b2ca0..56b9fdd73 100644 --- a/cactus-engine/tests/test_stt.cpp +++ b/cactus-engine/tests/test_stt.cpp @@ -6,7 +6,7 @@ using namespace EngineTestUtils; -static const char* g_model_path = std::getenv("CACTUS_TEST_MODEL"); +static const char* g_transcription_model_path = std::getenv("CACTUS_TEST_TRANSCRIPTION_MODEL"); static const char* g_assets_path = std::getenv("CACTUS_TEST_ASSETS"); bool test_transcription() { @@ -14,16 +14,7 @@ bool test_transcription() { << "║ TRANSCRIPTION TEST ║\n" << "╚══════════════════════════════════════════╝\n"; - if (!g_model_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_MODEL not set\n"; - return true; - } - if (!g_assets_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_ASSETS not set\n"; - return true; - } - - cactus_model_t model = cactus_init(g_model_path, nullptr, false); + cactus_model_t model = cactus_init(g_transcription_model_path, nullptr, false); if (!model) { std::cerr << "[✗] Failed to initialize model\n"; return false; @@ -67,16 +58,7 @@ bool test_transcription_pcm() { << "║ TRANSCRIPTION PCM TEST ║\n" << "╚══════════════════════════════════════════╝\n"; - if (!g_model_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_MODEL not set\n"; - return true; - } - if (!g_assets_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_ASSETS not set\n"; - return true; - } - - cactus_model_t model = cactus_init(g_model_path, nullptr, false); + cactus_model_t model = cactus_init(g_transcription_model_path, nullptr, false); if (!model) { std::cerr << "[✗] Failed to initialize model\n"; return false; diff --git a/cactus-engine/tests/test_telemetry.cpp b/cactus-engine/tests/test_telemetry.cpp index 42f18414e..632f55779 100644 --- a/cactus-engine/tests/test_telemetry.cpp +++ b/cactus-engine/tests/test_telemetry.cpp @@ -48,12 +48,6 @@ std::string random_project_id() { return std::string(buf); } -enum class CloudTelemetryTestResult { - Passed, - Failed, - Skipped, -}; - bool test_record_many_then_flush() { const std::string cache_dir = make_temp_dir("cactus_record_many_flush"); @@ -156,6 +150,12 @@ bool test_record_and_flush_race_no_deadlock() { return all_completed && event_count == expected_event_count; } +enum class CloudTelemetryTestResult { + Passed, + Failed, + Skipped, +}; + CloudTelemetryTestResult test_cloud_upload_record_then_flush() { const char* no_cloud_tele = std::getenv("CACTUS_NO_CLOUD_TELE"); if (no_cloud_tele && no_cloud_tele[0] != '\0') { diff --git a/cactus-engine/tests/test_vlm.cpp b/cactus-engine/tests/test_vlm.cpp index 77a49717b..e8aeea925 100644 --- a/cactus-engine/tests/test_vlm.cpp +++ b/cactus-engine/tests/test_vlm.cpp @@ -1,5 +1,4 @@ #include "test_utils.h" -#include #include #include @@ -15,16 +14,6 @@ static const char* g_options = R"({ })"; bool test_vlm_multiturn() { - std::string model_path_str(g_model_path ? g_model_path : ""); - - std::string vision_file = model_path_str + "/vision_patch_embedder_input_proj.weights"; - std::ifstream vf(vision_file); - if (!vf.good()) { - std::cout << "Skipping VLM multi-turn test: vision weights not found." << std::endl; - return true; - } - vf.close(); - std::cout << "\n╔══════════════════════════════════════════╗\n" << "║ VLM MULTI-TURN TEST ║\n" << "╚══════════════════════════════════════════╝\n"; @@ -107,35 +96,8 @@ bool test_prefill_invalidated_on_message_change_vlm() { << "║" << std::setw(42) << std::left << " PREFILL INVALIDATION (VLM) TEST" << "║\n" << "╚══════════════════════════════════════════╝\n"; - std::string model_path_str(g_model_path ? g_model_path : ""); - std::string vision_file = model_path_str + "/vision_patch_embedder_input_proj.weights"; - std::ifstream vf(vision_file); - if (!vf.good()) { - std::cout << "⊘ SKIP │ Vision weights not found\n"; - return true; - } - vf.close(); - - if (!g_assets_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_ASSETS not set\n"; - return true; - } - std::string prefill_img_path = std::string(g_assets_path) + "/test_monkey.png"; - std::ifstream imgf(prefill_img_path); - if (!imgf.good()) { - std::cout << "⊘ SKIP │ test_monkey.png not found\n"; - return true; - } - imgf.close(); - std::string complete_img_path = std::string(g_assets_path) + "/test_thing.png"; - std::ifstream imgf2(complete_img_path); - if (!imgf2.good()) { - std::cout << "⊘ SKIP │ test_thing.png not found\n"; - return true; - } - imgf2.close(); cactus_model_t model = cactus_init(g_model_path, nullptr, false); if (!model) { @@ -197,35 +159,8 @@ bool test_prefill_with_images() { << "║" << std::setw(42) << std::left << " PREFILL WITH IMAGES TEST" << "║\n" << "╚══════════════════════════════════════════╝\n"; - std::string model_path_str(g_model_path ? g_model_path : ""); - std::string vision_file = model_path_str + "/vision_patch_embedder_input_proj.weights"; - std::ifstream vf(vision_file); - if (!vf.good()) { - std::cout << "⊘ SKIP │ Vision weights not found\n"; - return true; - } - vf.close(); - - if (!g_assets_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_ASSETS not set\n"; - return true; - } - std::string prefill_img_path = std::string(g_assets_path) + "/test_monkey.png"; - std::ifstream imgf(prefill_img_path); - if (!imgf.good()) { - std::cout << "⊘ SKIP │ test_monkey.png not found\n"; - return true; - } - imgf.close(); - std::string extension_img_path = std::string(g_assets_path) + "/test_thing.png"; - std::ifstream imgf2(extension_img_path); - if (!imgf2.good()) { - std::cout << "⊘ SKIP │ test_thing.png not found\n"; - return true; - } - imgf2.close(); cactus_model_t model = cactus_init(g_model_path, nullptr, false); if (!model) { @@ -302,35 +237,8 @@ bool test_prefill_prefix_extension_reuse_vlm() { << "║" << std::setw(42) << std::left << " PREFILL PREFIX EXTENSION (VLM)" << "║\n" << "╚══════════════════════════════════════════╝\n"; - std::string model_path_str(g_model_path ? g_model_path : ""); - std::string vision_file = model_path_str + "/vision_patch_embedder_input_proj.weights"; - std::ifstream vf(vision_file); - if (!vf.good()) { - std::cout << "⊘ SKIP │ Vision weights not found\n"; - return true; - } - vf.close(); - - if (!g_assets_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_ASSETS not set\n"; - return true; - } - std::string base_img_path = std::string(g_assets_path) + "/test_monkey.png"; - std::ifstream imgf(base_img_path); - if (!imgf.good()) { - std::cout << "⊘ SKIP │ test_monkey.png not found\n"; - return true; - } - imgf.close(); - std::string extension_img_path = std::string(g_assets_path) + "/test_thing.png"; - std::ifstream imgf2(extension_img_path); - if (!imgf2.good()) { - std::cout << "⊘ SKIP │ test_thing.png not found\n"; - return true; - } - imgf2.close(); cactus_model_t model = cactus_init(g_model_path, nullptr, false); if (!model) { diff --git a/cactus-engine/tests/asr.cpp b/cactus-engine/tests/transcribe.cpp similarity index 100% rename from cactus-engine/tests/asr.cpp rename to cactus-engine/tests/transcribe.cpp diff --git a/cactus-graph/CMakeLists.txt b/cactus-graph/CMakeLists.txt index e99787137..c82b62a02 100644 --- a/cactus-graph/CMakeLists.txt +++ b/cactus-graph/CMakeLists.txt @@ -23,6 +23,8 @@ add_library(cactus_graph STATIC src/ops_cache.cpp src/ops_dsp.cpp src/ops_image.cpp + src/graph_ffi.cpp + src/last_error.cpp ) set_target_properties(cactus_graph PROPERTIES POSITION_INDEPENDENT_CODE ON) @@ -40,7 +42,7 @@ target_compile_options(cactus_graph PRIVATE $<$:-Wno-pedantic> ) -if(BUILD_TESTING OR CACTUS_BUILD_TESTS) +if((BUILD_TESTING OR CACTUS_BUILD_TESTS) AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) file(GLOB TEST_SOURCES "tests/test_*.cpp") foreach(TEST_SRC ${TEST_SOURCES}) get_filename_component(TEST_NAME ${TEST_SRC} NAME_WE) diff --git a/cactus-graph/cactus_graph.h b/cactus-graph/cactus_graph.h index ffa0851cb..3d7db196e 100644 --- a/cactus-graph/cactus_graph.h +++ b/cactus-graph/cactus_graph.h @@ -806,4 +806,268 @@ namespace GraphFile { }; } +#if __GNUC__ >= 4 + #define CACTUS_FFI_EXPORT __attribute__((visibility("default"))) +#else + #define CACTUS_FFI_EXPORT +#endif + +extern std::string last_error_message; + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void* cactus_graph_t; +typedef uint64_t cactus_node_t; + +typedef struct { + int32_t precision; + size_t rank; + size_t shape[8]; + size_t num_elements; + size_t byte_size; +} cactus_tensor_info_t; + +CACTUS_FFI_EXPORT const char* cactus_get_last_error(void); + +CACTUS_FFI_EXPORT cactus_graph_t cactus_graph_create(void); +CACTUS_FFI_EXPORT void cactus_graph_destroy(cactus_graph_t graph); +CACTUS_FFI_EXPORT int cactus_graph_hard_reset(cactus_graph_t graph); + +CACTUS_FFI_EXPORT int cactus_graph_save(cactus_graph_t graph, const char* filename); +CACTUS_FFI_EXPORT cactus_graph_t cactus_graph_load(const char* filename); + +CACTUS_FFI_EXPORT int cactus_graph_input( + cactus_graph_t graph, const size_t* shape, size_t rank, int32_t precision, +cactus_node_t* out_node); + +CACTUS_FFI_EXPORT int cactus_graph_set_input( + cactus_graph_t graph, cactus_node_t node, const void* data, int32_t +precision); +CACTUS_FFI_EXPORT int cactus_graph_set_external_input( + cactus_graph_t graph, cactus_node_t node, void* data, int32_t precision); +CACTUS_FFI_EXPORT int cactus_graph_mark_embedded_input( + cactus_graph_t graph, cactus_node_t node); + +CACTUS_FFI_EXPORT int cactus_graph_precision_cast( + cactus_graph_t graph, cactus_node_t input, int32_t target_precision, cactus_node_t* out); + +CACTUS_FFI_EXPORT int cactus_graph_add(cactus_graph_t graph, cactus_node_t a, +cactus_node_t b, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_add_clipped(cactus_graph_t graph, cactus_node_t a, +cactus_node_t b, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_subtract(cactus_graph_t graph, cactus_node_t +a, cactus_node_t b, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_multiply(cactus_graph_t graph, cactus_node_t +a, cactus_node_t b, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_divide(cactus_graph_t graph, cactus_node_t +a, cactus_node_t b, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_not_equal(cactus_graph_t graph, cactus_node_t +a, cactus_node_t b, cactus_node_t* out); + +CACTUS_FFI_EXPORT int cactus_graph_scalar_add(cactus_graph_t graph, cactus_node_t x, float value, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_scalar_subtract(cactus_graph_t graph, cactus_node_t x, float value, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_scalar_multiply(cactus_graph_t graph, cactus_node_t x, float value, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_scalar_divide(cactus_graph_t graph, cactus_node_t x, float value, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_scalar_not_equal(cactus_graph_t graph, cactus_node_t x, float value, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_scalar_exp(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_scalar_sqrt(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_scalar_cos(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_scalar_sin(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_scalar_log(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); + +CACTUS_FFI_EXPORT int cactus_graph_abs(cactus_graph_t graph, cactus_node_t x, +cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_pow(cactus_graph_t graph, cactus_node_t x, +float exponent, cactus_node_t* out); + +CACTUS_FFI_EXPORT int cactus_graph_view( + cactus_graph_t graph, cactus_node_t x, const size_t* shape, size_t rank, +cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_flatten( + cactus_graph_t graph, cactus_node_t x, int32_t start_dim, int32_t end_dim, +cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_reshape( + cactus_graph_t graph, cactus_node_t x, const size_t* shape, size_t rank, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_transpose( + cactus_graph_t graph, cactus_node_t x, int32_t backend, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_transpose_n( + cactus_graph_t graph, cactus_node_t x, const size_t* permutation, size_t rank, int32_t backend, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_slice( + cactus_graph_t graph, cactus_node_t x, int32_t axis, size_t start, size_t length, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_index( + cactus_graph_t graph, cactus_node_t x, size_t index_value, int32_t dim, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_sum(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_mean(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_variance(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_min(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_max(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_cumsum(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out); + +CACTUS_FFI_EXPORT int cactus_graph_concat( + cactus_graph_t graph, cactus_node_t a, cactus_node_t b, int32_t axis, +cactus_node_t* out); + +CACTUS_FFI_EXPORT int cactus_graph_cat( + cactus_graph_t graph, const cactus_node_t* nodes, size_t count, int32_t +axis, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_matmul( + cactus_graph_t graph, cactus_node_t a, cactus_node_t b, bool pretransposed_rhs, int32_t backend, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_gather( + cactus_graph_t graph, cactus_node_t tensor, cactus_node_t indices, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_embedding_from_tensor( + cactus_graph_t graph, cactus_node_t embedding_tensor, cactus_node_t indices, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_embedding_from_file( + cactus_graph_t graph, const char* filename, cactus_node_t indices, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_mmap_embeddings( + cactus_graph_t graph, const char* filename, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_mmap_weights( + cactus_graph_t graph, const char* filename, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_bind_mmap_weights( + cactus_graph_t graph, cactus_node_t node, const char* filename); +CACTUS_FFI_EXPORT int cactus_graph_bilinear_interpolation( + cactus_graph_t graph, cactus_node_t pos_embeds, size_t dst_height, size_t dst_width, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_release_weight_pages(cactus_graph_t graph, cactus_node_t node); +CACTUS_FFI_EXPORT int cactus_graph_prefetch_weight_pages(cactus_graph_t graph, cactus_node_t node); +CACTUS_FFI_EXPORT int cactus_graph_release_all_weight_pages(cactus_graph_t graph); + +CACTUS_FFI_EXPORT int cactus_graph_relu(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_silu(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_gelu(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_gelu_erf(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_sigmoid(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_tanh(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_glu(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out); + +CACTUS_FFI_EXPORT int cactus_graph_layernorm( + cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, cactus_node_t bias, float epsilon, bool has_bias, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_groupnorm( + cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, cactus_node_t bias, size_t num_groups, float epsilon, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_batchnorm( + cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, cactus_node_t bias, cactus_node_t running_mean, cactus_node_t running_var, int32_t axis, float epsilon, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_topk(cactus_graph_t graph, cactus_node_t input, size_t k, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_rms_norm( + cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, float epsilon, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_rope( + cactus_graph_t graph, cactus_node_t input, float theta, size_t position_offset, int32_t backend, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_rope_gptj( + cactus_graph_t graph, cactus_node_t input, float theta, size_t position_offset, size_t rot_dim, int32_t backend, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_softmax(cactus_graph_t graph, cactus_node_t input, int32_t axis, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_attention( + cactus_graph_t graph, cactus_node_t query, cactus_node_t key, cactus_node_t value, float scale, bool is_causal, size_t position_offset, size_t window_size, int32_t backend, bool use_mask, cactus_node_t mask, bool additive_mask, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_rel_pos_bias( + cactus_graph_t graph, cactus_node_t query, cactus_node_t relative_key, float scale, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_attention_int8_hybrid( + cactus_graph_t graph, cactus_node_t query, cactus_node_t key_new, cactus_node_t value_new, float scale, size_t position_offset, + const int8_t* cached_keys, const int8_t* cached_values, const float* k_scales, const float* v_scales, + size_t cache_len, size_t num_kv_heads, size_t head_dim, size_t window_size, cactus_node_t* out); + +CACTUS_FFI_EXPORT int cactus_graph_kv_cache_state( + cactus_graph_t graph, size_t max_seq_len, size_t num_kv_heads, size_t head_dim, size_t window_size, size_t sink_size, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_kv_cache_append( + cactus_graph_t graph, cactus_node_t new_kv, cactus_node_t cache_state, size_t window_size, size_t sink_size, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_attention_cached( + cactus_graph_t graph, cactus_node_t query, cactus_node_t key_new, cactus_node_t value_new, + cactus_node_t k_cache_state, cactus_node_t v_cache_state, + float scale, size_t position_offset, size_t window_size, size_t v_head_dim, cactus_node_t* out); + +CACTUS_FFI_EXPORT int cactus_graph_conv_cache_state( + cactus_graph_t graph, size_t window_size, size_t hidden_dim, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_conv_cache_append( + cactus_graph_t graph, cactus_node_t new_data, cactus_node_t cache_state, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_conv_cache_initialize( + cactus_graph_t graph, cactus_node_t rows, cactus_node_t cache_state, cactus_node_t* out); + +CACTUS_FFI_EXPORT int cactus_graph_recurrent_cache_state( + cactus_graph_t graph, const size_t* shape, size_t shape_len, int precision, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_recurrent_cache_write( + cactus_graph_t graph, cactus_node_t new_value, cactus_node_t cache_input, cactus_node_t* out); + +CACTUS_FFI_EXPORT int cactus_graph_rfft( + cactus_graph_t graph, cactus_node_t input, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_irfft( + cactus_graph_t graph, cactus_node_t input, size_t output_length, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_mel_filter_bank( + cactus_graph_t graph, size_t num_frequency_bins, size_t num_mel_filters, + float min_frequency, float max_frequency, size_t sampling_rate, + int norm_type, int scale_type, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_spectrogram( + cactus_graph_t graph, cactus_node_t waveform, cactus_node_t mel_filters, + size_t frame_length, size_t hop_length, size_t fft_length, + float power, bool center, int pad_mode, + float mel_floor, int log_mel_mode, + float dither, float preemphasis, bool remove_dc_offset, + cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_image_preprocess( + cactus_graph_t graph, cactus_node_t pixel_input, + int src_width, int src_height, int target_width, int target_height, + int patch_size, int channels, float rescale_factor, + const float* mean, const float* std_dev, cactus_node_t* out); + +CACTUS_FFI_EXPORT int cactus_graph_conv1d_causal( + cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, size_t kernel_size, size_t dilation, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_conv1d_k3( + cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, size_t stride, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_conv1d_k7s3( + cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, cactus_node_t bias, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_conv1d( + cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, bool has_bias, cactus_node_t bias, size_t stride, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_conv1d_same_depthwise_k9( + cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, bool has_bias, cactus_node_t bias, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_conv1d_pointwise( + cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, bool has_bias, cactus_node_t bias, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_conv2d_k3s2p1( + cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, bool has_bias, cactus_node_t bias, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_conv2d_depthwise_k3s2p1( + cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, bool has_bias, cactus_node_t bias, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_conv2d_pointwise_1x1( + cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, bool has_bias, cactus_node_t bias, cactus_node_t* out); + +CACTUS_FFI_EXPORT int cactus_graph_lstm_cell( + cactus_graph_t graph, cactus_node_t input, cactus_node_t h_prev, cactus_node_t c_prev, cactus_node_t weight_ih, cactus_node_t weight_hh, cactus_node_t bias_ih, cactus_node_t bias_hh, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_gated_deltanet_decode( + cactus_graph_t graph, cactus_node_t query, cactus_node_t key, cactus_node_t value, cactus_node_t gate_log, cactus_node_t beta, cactus_node_t initial_state, float scale, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_gated_deltanet_prefill( + cactus_graph_t graph, cactus_node_t query, cactus_node_t key, cactus_node_t value, cactus_node_t gate_log, cactus_node_t beta, cactus_node_t initial_state, size_t chunk_size, float scale, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_stft( + cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, size_t stride, size_t num_fft_bins, cactus_node_t* out); + +CACTUS_FFI_EXPORT int cactus_graph_altup_predict( + cactus_graph_t graph, cactus_node_t coefs, const cactus_node_t* streams, size_t num_streams, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_altup_correct( + cactus_graph_t graph, cactus_node_t coefs, cactus_node_t innovation, const cactus_node_t* predictions, size_t num_predictions, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_gaussian_topk( + cactus_graph_t graph, cactus_node_t input, float ppf, cactus_node_t* out); + +CACTUS_FFI_EXPORT int cactus_graph_moe_layer_gated( + cactus_graph_t graph, cactus_node_t hidden, cactus_node_t routing_probs, cactus_node_t topk_indices, + const cactus_node_t* w1_weights, const cactus_node_t* w3_weights, const cactus_node_t* w2_weights, + size_t num_experts, size_t num_experts_per_tok, bool normalize_routing, float epsilon, float routed_scaling_factor, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_moe_layer_ungated( + cactus_graph_t graph, cactus_node_t hidden, cactus_node_t routing_probs, cactus_node_t topk_indices, + const cactus_node_t* w1_weights, const cactus_node_t* w2_weights, + size_t num_experts, size_t num_experts_per_tok, bool normalize_routing, float epsilon, float routed_scaling_factor, int32_t activation, cactus_node_t* out); + +CACTUS_FFI_EXPORT int cactus_graph_sample( + cactus_graph_t graph, cactus_node_t logits, float temperature, float top_p, size_t top_k, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_scatter_topk( + cactus_graph_t graph, cactus_node_t indices, cactus_node_t values, size_t num_classes, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_persistent( + cactus_graph_t graph, cactus_node_t source_node, cactus_node_t* out); +CACTUS_FFI_EXPORT int cactus_graph_is_populated( + cactus_graph_t graph, cactus_node_t persistent_node, int32_t* out_is_populated); +CACTUS_FFI_EXPORT int cactus_graph_invalidate_persistent( + cactus_graph_t graph, cactus_node_t persistent_node); + +CACTUS_FFI_EXPORT int cactus_graph_execute(cactus_graph_t graph); +CACTUS_FFI_EXPORT int cactus_graph_get_output_ptr(cactus_graph_t graph, +cactus_node_t node, void** out_ptr); +CACTUS_FFI_EXPORT int cactus_graph_get_output_info(cactus_graph_t graph, +cactus_node_t node, cactus_tensor_info_t* out_info); + +#ifdef __cplusplus +} +#endif + #endif diff --git a/cactus-graph/src/core.cpp b/cactus-graph/src/core.cpp index 7aff9cf49..1ad864bb9 100644 --- a/cactus-graph/src/core.cpp +++ b/cactus-graph/src/core.cpp @@ -87,7 +87,16 @@ BufferDesc::BufferDesc(BufferDesc&& other) noexcept num_groups(other.num_groups), activation_scales_data(other.activation_scales_data), owned_activation_scales(std::move(other.owned_activation_scales)), - num_rows_for_activation_scales(other.num_rows_for_activation_scales) { + num_rows_for_activation_scales(other.num_rows_for_activation_scales), + cq_codebook(other.cq_codebook), + cq_input_scale(other.cq_input_scale), + cq_input_scale_recip(other.cq_input_scale_recip), + cq_norms(other.cq_norms), + cq_left_signs(other.cq_left_signs), + cq_right_signs(other.cq_right_signs), + cq_permutation(other.cq_permutation), + cq_rotation(other.cq_rotation), + cq_flags(other.cq_flags) { other.total_size = 0; other.byte_size = 0; other.external_data = nullptr; @@ -96,6 +105,15 @@ BufferDesc::BufferDesc(BufferDesc&& other) noexcept other.num_groups = 0; other.activation_scales_data = nullptr; other.num_rows_for_activation_scales = 0; + other.cq_codebook = nullptr; + other.cq_input_scale = nullptr; + other.cq_input_scale_recip = nullptr; + other.cq_norms = nullptr; + other.cq_left_signs = nullptr; + other.cq_right_signs = nullptr; + other.cq_permutation = nullptr; + other.cq_rotation = nullptr; + other.cq_flags = 0; } BufferDesc& BufferDesc::operator=(BufferDesc&& other) noexcept { @@ -116,6 +134,15 @@ BufferDesc& BufferDesc::operator=(BufferDesc&& other) noexcept { activation_scales_data = other.activation_scales_data; owned_activation_scales = std::move(other.owned_activation_scales); num_rows_for_activation_scales = other.num_rows_for_activation_scales; + cq_codebook = other.cq_codebook; + cq_input_scale = other.cq_input_scale; + cq_input_scale_recip = other.cq_input_scale_recip; + cq_norms = other.cq_norms; + cq_left_signs = other.cq_left_signs; + cq_right_signs = other.cq_right_signs; + cq_permutation = other.cq_permutation; + cq_rotation = other.cq_rotation; + cq_flags = other.cq_flags; other.total_size = 0; other.byte_size = 0; @@ -125,6 +152,15 @@ BufferDesc& BufferDesc::operator=(BufferDesc&& other) noexcept { other.num_groups = 0; other.activation_scales_data = nullptr; other.num_rows_for_activation_scales = 0; + other.cq_codebook = nullptr; + other.cq_input_scale = nullptr; + other.cq_input_scale_recip = nullptr; + other.cq_norms = nullptr; + other.cq_left_signs = nullptr; + other.cq_right_signs = nullptr; + other.cq_permutation = nullptr; + other.cq_rotation = nullptr; + other.cq_flags = 0; } return *this; } diff --git a/cactus-engine/src/graph_ffi.cpp b/cactus-graph/src/graph_ffi.cpp similarity index 99% rename from cactus-engine/src/graph_ffi.cpp rename to cactus-graph/src/graph_ffi.cpp index 4d9b78c47..02861ab63 100644 --- a/cactus-engine/src/graph_ffi.cpp +++ b/cactus-graph/src/graph_ffi.cpp @@ -1,11 +1,7 @@ -#include "../cactus_engine.h" -#include "utils.h" -#include "cactus_graph.h" +#include "../cactus_graph.h" #include #include -using namespace cactus::ffi; - struct GraphHandle { CactusGraph graph; }; diff --git a/cactus-graph/src/last_error.cpp b/cactus-graph/src/last_error.cpp new file mode 100644 index 000000000..3810e9af2 --- /dev/null +++ b/cactus-graph/src/last_error.cpp @@ -0,0 +1,8 @@ +#include "../cactus_graph.h" +#include + +std::string last_error_message; + +extern "C" const char* cactus_get_last_error() { + return last_error_message.c_str(); +} diff --git a/cactus-graph/test.sh b/cactus-graph/test.sh index 1dc01db14..4bf78b6d8 100755 --- a/cactus-graph/test.sh +++ b/cactus-graph/test.sh @@ -3,19 +3,38 @@ set -e cd "$(dirname "$0")" +SUITE="" +while [[ $# -gt 0 ]]; do + case $1 in + --suite) SUITE="${2:?--suite needs an argument}"; shift 2 ;; + *) echo "Unknown arg: $1" >&2; exit 2 ;; + esac +done + echo "Building and testing cactus-graph..." rm -rf build mkdir -p build cd build -cmake .. -DCACTUS_BUILD_TESTS=ON -DCMAKE_RULE_MESSAGES=OFF -DCMAKE_VERBOSE_MAKEFILE=OFF > /dev/null 2>&1 +cmake .. -DCACTUS_BUILD_TESTS=ON -DCMAKE_RULE_MESSAGES=OFF -DCMAKE_VERBOSE_MAKEFILE=OFF > /dev/null make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) echo "" FAILED=0 -for test_bin in test_*; do - [ -x "$test_bin" ] && ./"$test_bin" || FAILED=1 -done +if [ -n "$SUITE" ]; then + target="./test_${SUITE}" + if [ -x "$target" ]; then + "$target" || FAILED=1 + else + echo "Test not found: $target" >&2 + FAILED=1 + fi +else + for t in ./test_*; do + [ -x "$t" ] || continue + "$t" || FAILED=1 + done +fi exit $FAILED diff --git a/cactus-kernels/CMakeLists.txt b/cactus-kernels/CMakeLists.txt index 5312512da..bd0f5ff03 100644 --- a/cactus-kernels/CMakeLists.txt +++ b/cactus-kernels/CMakeLists.txt @@ -27,20 +27,13 @@ set_target_properties(cactus_kernels PROPERTIES POSITION_INDEPENDENT_CODE ON) target_include_directories(cactus_kernels PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/src - PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/libs + ${CMAKE_CURRENT_SOURCE_DIR}/libs ) target_compile_options(cactus_kernels PUBLIC -march=armv8.2-a+fp16+simd+dotprod+i8mm -pthread -O3 ) -target_compile_definitions(cactus_kernels PUBLIC - __ARM_NEON=1 - __ARM_FEATURE_FP16_VECTOR_ARITHMETIC=1 - __ARM_FEATURE_DOTPROD=1 - __ARM_FEATURE_MATMUL_INT8=1 -) - target_compile_options(cactus_kernels PRIVATE -Wall -Wextra -pedantic -Wno-missing-field-initializers $<$:-Wno-c99-extensions> @@ -50,20 +43,11 @@ target_compile_options(cactus_kernels PRIVATE if(APPLE) set(CMAKE_OSX_ARCHITECTURES "arm64") target_compile_definitions(cactus_kernels PUBLIC ACCELERATE_NEW_LAPACK) - find_library(ACCELERATE_FRAMEWORK Accelerate REQUIRED) - target_link_libraries(cactus_kernels PUBLIC ${ACCELERATE_FRAMEWORK}) + target_link_libraries(cactus_kernels PUBLIC "-framework Accelerate") endif() -if(BUILD_TESTING OR CACTUS_BUILD_TESTS) - set(TEST_SOURCES - tests/test_elementwise.cpp - tests/test_matmul.cpp - tests/test_attention.cpp - tests/test_reduce.cpp - tests/test_conv.cpp - tests/test_quant.cpp - tests/test_dsp.cpp - ) +if((BUILD_TESTING OR CACTUS_BUILD_TESTS) AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + file(GLOB TEST_SOURCES "tests/test_*.cpp") foreach(TEST_SRC ${TEST_SOURCES}) get_filename_component(TEST_NAME ${TEST_SRC} NAME_WE) add_executable(${TEST_NAME} ${TEST_SRC}) diff --git a/cactus-kernels/test.sh b/cactus-kernels/test.sh index df2d247d7..9c6d1ca01 100755 --- a/cactus-kernels/test.sh +++ b/cactus-kernels/test.sh @@ -3,19 +3,38 @@ set -e cd "$(dirname "$0")" +SUITE="" +while [[ $# -gt 0 ]]; do + case $1 in + --suite) SUITE="${2:?--suite needs an argument}"; shift 2 ;; + *) echo "Unknown arg: $1" >&2; exit 2 ;; + esac +done + echo "Building and testing cactus-kernels..." rm -rf build mkdir -p build cd build -cmake .. -DCACTUS_BUILD_TESTS=ON -DCMAKE_RULE_MESSAGES=OFF -DCMAKE_VERBOSE_MAKEFILE=OFF > /dev/null 2>&1 +cmake .. -DCACTUS_BUILD_TESTS=ON -DCMAKE_RULE_MESSAGES=OFF -DCMAKE_VERBOSE_MAKEFILE=OFF > /dev/null make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) echo "" FAILED=0 -for test_bin in test_*; do - [ -x "$test_bin" ] && ./"$test_bin" || FAILED=1 -done +if [ -n "$SUITE" ]; then + target="./test_${SUITE}" + if [ -x "$target" ]; then + "$target" || FAILED=1 + else + echo "Test not found: $target" >&2 + FAILED=1 + fi +else + for t in ./test_*; do + [ -x "$t" ] || continue + "$t" || FAILED=1 + done +fi exit $FAILED diff --git a/cactus-kernels/tests/test_elementwise.cpp b/cactus-kernels/tests/test_elementwise.cpp index e6f08e8f4..bae21e4e8 100644 --- a/cactus-kernels/tests/test_elementwise.cpp +++ b/cactus-kernels/tests/test_elementwise.cpp @@ -94,6 +94,21 @@ bool test_transpose_2d_f16() { return true; } +bool test_fast_tanh_f32x4() { + constexpr float TOL = 1e-5f; + for (int i = -10000; i <= 10000; ++i) { + float x = 0.001f * static_cast(i); + float32x4_t r = fast_tanh_f32x4(vdupq_n_f32(x)); + float lanes[4]; + vst1q_f32(lanes, r); + float want = std::tanh(x); + for (int k = 0; k < 4; ++k) { + if (std::fabs(lanes[k] - want) > TOL) return false; + } + } + return true; +} + bool run_benchmarks() { auto bench_binary = [](const char* label, void(*fn)(const __fp16*, const __fp16*, __fp16*, size_t)) { const size_t n = 1024 * 1024; @@ -168,6 +183,7 @@ int main() { runner.run_test("add_clipped_f16", test_add_clipped_f16()); runner.run_test("scalar_ops_f16", test_scalar_ops_f16()); runner.run_test("transpose_2d_f16", test_transpose_2d_f16()); + runner.run_test("fast_tanh_f32x4", test_fast_tanh_f32x4()); runner.print_benchmarks_header(); runner.run_bench("benchmarks", run_benchmarks()); runner.print_summary(); diff --git a/cactus/CMakeLists.txt b/cactus/CMakeLists.txt deleted file mode 100644 index 41167424c..000000000 --- a/cactus/CMakeLists.txt +++ /dev/null @@ -1,57 +0,0 @@ -cmake_minimum_required(VERSION 3.10) -project(Cactus LANGUAGES CXX) - -set(CMAKE_BUILD_TYPE Release CACHE STRING "") -set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) - -if(NOT TARGET cactus_engine) - add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../cactus-engine ${CMAKE_CURRENT_BINARY_DIR}/cactus-engine) -endif() - -set(_ARCHIVES $ $ $) -set(_DEPS cactus_engine cactus_graph cactus_kernels) - -if(APPLE) - add_custom_target(cactus ALL - COMMAND libtool -static -o ${CMAKE_BINARY_DIR}/libcactus.a ${_ARCHIVES} - DEPENDS ${_DEPS} - ) -else() - file(GENERATE OUTPUT ${CMAKE_BINARY_DIR}/combine.mri CONTENT - "create ${CMAKE_BINARY_DIR}/libcactus.a\naddlib $\naddlib $\naddlib $\nsave\nend\n" - ) - add_custom_target(cactus ALL - COMMAND ${CMAKE_AR} -M < ${CMAKE_BINARY_DIR}/combine.mri - DEPENDS ${_DEPS} - ) -endif() - -if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) -file(WRITE ${CMAKE_BINARY_DIR}/stub.cpp "") -add_library(cactus_shared SHARED ${CMAKE_BINARY_DIR}/stub.cpp) -set_target_properties(cactus_shared PROPERTIES OUTPUT_NAME cactus) -add_dependencies(cactus_shared ${_DEPS}) - -if(APPLE) - target_link_options(cactus_shared PRIVATE - -Wl,-force_load,$ - -Wl,-force_load,$ - -Wl,-force_load,$ - ) - set(_CURL "${CACTUS_CURL_ROOT}/macos/libcurl.a") - if(EXISTS "${_CURL}") - target_link_libraries(cactus_shared PRIVATE "${_CURL}") - endif() - target_link_libraries(cactus_shared PRIVATE - "-framework Accelerate" "-framework CoreML" "-framework Foundation" - "-framework Security" "-framework SystemConfiguration" "-framework CFNetwork" - ) -else() - target_link_options(cactus_shared PRIVATE -Wl,--whole-archive ${_ARCHIVES} -Wl,--no-whole-archive) - find_package(CURL QUIET) - if(CURL_FOUND) - target_link_libraries(cactus_shared PRIVATE ${CURL_LIBRARIES}) - endif() -endif() -endif() diff --git a/cactus/build.sh b/cactus/build.sh deleted file mode 100755 index cd913a9d4..000000000 --- a/cactus/build.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -set -e - -missing=() -command -v cmake &> /dev/null || missing+=("cmake") -command -v make &> /dev/null || missing+=("make") -command -v g++ &> /dev/null || command -v clang++ &> /dev/null || missing+=("g++") - -if [ ${#missing[@]} -gt 0 ]; then - echo "Error: Missing required build tools: ${missing[*]}" - echo "" - if [[ "$OSTYPE" == "linux-gnu"* ]]; then - echo "Install with: sudo apt-get install cmake build-essential" - elif [[ "$OSTYPE" == "darwin"* ]]; then - echo "Install with: xcode-select --install && brew install cmake" - fi - exit 1 -fi - -cd "$(dirname "$0")" - -echo "Building Cactus library..." - -LOCK_DIR="${TMPDIR:-/tmp}/cactus-build.lock" -while ! mkdir "$LOCK_DIR" 2>/dev/null; do - if [ -f "$LOCK_DIR/pid" ] && ! kill -0 "$(cat "$LOCK_DIR/pid" 2>/dev/null)" 2>/dev/null; then - rm -rf "$LOCK_DIR" - continue - fi - sleep 2 -done -echo "$$" > "$LOCK_DIR/pid" -trap 'rm -rf "$LOCK_DIR"' EXIT - -mkdir -p build -cd build - -cmake .. -DCMAKE_RULE_MESSAGES=OFF -DCMAKE_VERBOSE_MAKEFILE=OFF > /dev/null 2>&1 -make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) - -echo "Cactus library built successfully!" -echo " Static: $(pwd)/libcactus.a" -echo " Shared: $(pwd)/libcactus.$([ "$(uname)" = Darwin ] && echo dylib || echo so)" diff --git a/cactus/cactus.h b/cactus/cactus.h deleted file mode 100644 index 8d5032c5c..000000000 --- a/cactus/cactus.h +++ /dev/null @@ -1,8 +0,0 @@ -#ifndef CACTUS_H -#define CACTUS_H - -#include "cactus_kernels.h" -#include "cactus_graph.h" -#include "cactus_engine.h" - -#endif // CACTUS_H diff --git a/cactus/engine/engine.h b/cactus/engine/engine.h deleted file mode 100644 index afebfeb07..000000000 --- a/cactus/engine/engine.h +++ /dev/null @@ -1,1104 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "../graph/graph.h" - -#ifdef __clang__ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wc99-extensions" -#pragma clang diagnostic ignored "-Wunused-parameter" -#elif defined(__GNUC__) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wpedantic" -#pragma GCC diagnostic ignored "-Wunused-parameter" -#endif - -extern "C" { - #include "../../libs/stb/stb_image.h" - #include "../../libs/stb/stb_image_resize2.h" -} - -#ifdef __clang__ -#pragma clang diagnostic pop -#elif defined(__GNUC__) -#pragma GCC diagnostic pop -#endif - -class CactusGraph; - -namespace cactus { -namespace npu { - class NPUPrefill; -} -namespace engine { - -class Siglip2Preprocessor; - -struct Config { - uint32_t vocab_size = 151936; - uint32_t bos_token_id = 151643; - uint32_t eos_token_id = 151645; - uint32_t num_layers = 28; - uint32_t hidden_dim = 1024; - uint32_t ffn_intermediate_dim = 3072; - uint32_t attention_heads = 16; - uint32_t attention_kv_heads = 8; - uint32_t attention_head_dim = 128; - float layer_norm_eps = 1e-6f; - float rope_theta = 1000000.0f; - uint32_t num_experts = 0; - uint32_t num_shared_experts = 0; - uint32_t num_top_experts = 0; - uint32_t moe_every_n_layers = 0; - uint32_t moe_intermediate_dim = 0; - uint32_t num_dense_layers = 0; - uint32_t num_experts_per_tok = 0; - bool norm_topk_prob = false; - bool use_expert_bias = false; - float routed_scaling_factor = 1.0f; - bool tie_word_embeddings = true; - - uint32_t vision_hidden_dim = 0; - uint32_t vision_num_layers = 0; - uint32_t vision_attention_heads = 0; - uint32_t vision_image_size = 0; - uint32_t vision_patch_size = 0; - uint32_t vision_num_channels = 3; - uint32_t vision_embed_dim = 0; - uint32_t visual_tokens_per_img = 0; - bool use_pixel_shuffle = false; - uint32_t pixel_shuffle_factor = 1; - bool use_image_tokens = false; - uint32_t image_token_id = 0; - bool use_layout_tags = false; - uint32_t image_seq_len = 64; - - uint32_t global_image_size = 2048; - uint32_t max_tile_size = 512; - float rescale_factor = 0.00392156862745098f; - float image_mean = 0.5f; - float image_std = 0.5f; - - uint32_t downsample_factor = 2; - uint32_t min_tiles = 2; - uint32_t max_tiles = 10; - bool use_thumbnail = true; - uint32_t min_image_tokens = 64; - uint32_t max_image_tokens = 256; - uint32_t max_num_patches = 1024; - uint32_t tile_size = 512; - float max_pixels_tolerance = 2.0f; - bool do_image_splitting = true; - bool encoder_act_gelu = false; - bool decoder_act_gelu = false; - uint32_t num_encoder_layers = 0; - uint32_t num_decoder_layers = 0; - float partial_rotary_factor = 0.0f; - uint32_t pad_token_id = 0; - uint32_t conv_kernel_size = 0; - uint32_t subsampling_conv_kernel_size = 0; - uint32_t subsampling_conv_stride = 0; - uint32_t subsampling_conv_channels = 0; - uint32_t subsampling_factor = 0; - uint32_t num_mel_bins = 80; - std::string encoder_hidden_act = "silu"; - uint32_t linear_num_key_heads = 0; - uint32_t linear_key_head_dim = 0; - uint32_t linear_num_value_heads = 0; - uint32_t linear_value_head_dim = 0; - uint32_t linear_q_proj_dim = 0; - uint32_t linear_k_proj_dim = 0; - uint32_t linear_v_proj_dim = 0; - - uint32_t kv_lora_rank = 0; - uint32_t q_lora_rank = 0; - uint32_t qk_head_dim = 0; - uint32_t qk_nope_head_dim = 0; - uint32_t qk_rope_head_dim = 0; - uint32_t v_head_dim = 0; - uint32_t rope_interleave = 0; - bool attention_bias = false; - float rope_scaling_factor = 1.0f; - float rope_mscale_all_dim = 0.0f; - - enum class ModelType {QWEN = 0, GEMMA = 1, NOMIC = 3, LFM2 = 5, SIGLIP2 = 6, WHISPER = 7, MOONSHINE = 8, SILERO_VAD = 9, PARAKEET = 10, QWEN3P5 = 11, PARAKEET_TDT = 12, GEMMA3N = 13, YOUTU = 14, GEMMA4 = 15, PYANNOTE = 16, WESPEAKER = 17, NEEDLE = 18}; - uint32_t predictor_hidden_dim = 0; - uint32_t predictor_num_layers = 0; - uint32_t tdt_joint_dim = 0; - uint32_t tdt_num_durations = 0; - uint32_t tdt_blank_id = 0; - std::vector tdt_durations; - - ModelType model_type = ModelType::QWEN; - - enum class ModelVariant {DEFAULT = 0, VLM = 1, EXTRACT = 2, RAG = 3}; - ModelVariant model_variant = ModelVariant::DEFAULT; - - enum class Activation {GELU = 0, SILU = 1}; - Activation activation = Activation::SILU; - - enum class Backend {CPU = 0, NPU = 1}; - Backend default_backend = Backend::CPU; - - enum class Precision {INT8 = 0, FP16 = 1, FP32 = 2}; - Precision precision = Precision::FP32; - - float default_temperature = 0.6f; - float default_top_p = 0.95f; - size_t default_top_k = 20; - float default_max_tps = -1.0f; - float default_cloud_handoff_threshold = 0.0f; - size_t default_rolling_entropy_window = 10; - - std::vector layer_types; - size_t conv_L_cache = 0; - - uint32_t altup_num_inputs = 4; - uint32_t laurel_rank = 64; - uint32_t hidden_size_per_layer_input = 256; - uint32_t num_kv_shared_layers = 0; - uint32_t sliding_window = 512; - float rope_local_base_freq = 10000.0f; - float final_logit_softcapping = 0.0f; - float global_partial_rotary_factor = 1.0f; - uint32_t expert_intermediate_size = 0; - uint32_t global_head_dim = 0; - uint32_t num_global_kv_heads = 0; - bool attention_k_eq_v = false; - bool enable_moe_block = false; - std::vector activation_sparsity_ppf; - - uint32_t vision_head_dim = 64; - uint32_t vision_kv_heads = 12; - uint32_t vision_intermediate_size = 3072; - uint32_t vision_position_embedding_size = 10240; - uint32_t vision_pooling_kernel_size = 3; - uint32_t vision_default_output_length = 280; - float vision_rope_theta = 100.0f; - - uint32_t audio_hidden_dim = 0; - uint32_t audio_num_layers = 0; - uint32_t audio_num_heads = 0; - uint32_t audio_head_dim = 0; - uint32_t audio_input_feat_size = 128; - uint32_t audio_conf_conv_kernel_size = 5; - uint32_t audio_chunk_size = 12; - uint32_t audio_context_left = 13; - uint32_t audio_context_right = 0; - float audio_logit_cap = 50.0f; - float audio_residual_weight = 0.5f; - uint32_t audio_output_proj_dims = 0; - uint32_t audio_vocab_size = 128; - uint32_t audio_vocab_offset = 0; - uint32_t audio_soft_tokens = 188; - uint32_t audio_sscp_conv0_channels = 128; - uint32_t audio_sscp_conv1_channels = 32; - float audio_sscp_conv_eps = 1e-3f; - float audio_rms_norm_eps = 1e-6f; - uint32_t audio_fft_length = 1024; - uint32_t audio_token_id = 0; - bool audio_fft_overdrive = false; - uint32_t channel_open_token_id = 100; - uint32_t channel_close_token_id = 101; - - static bool is_gemma_family(ModelType t) { - return t == ModelType::GEMMA || t == ModelType::GEMMA3N || t == ModelType::GEMMA4; - } - - bool from_json(const std::string& json_path); - std::string to_json() const; -}; - - - -struct MergeRule { - std::string first; - std::string second; - std::string merged; - uint32_t priority; - - MergeRule(const std::string& f, const std::string& s, const std::string& m, uint32_t p) - : first(f), second(s), merged(m), priority(p) {} -}; - - -struct ToolCallInfo { - std::string name; - std::string arguments; -}; - -struct ChatMessage { - std::string role; - std::string content; - std::string name; - std::vector images; - std::vector audio; - size_t audio_soft_token_count = 0; - std::vector tool_calls; -}; - -inline std::string format_needle_query_text(const std::vector& messages) { - std::string system_text; - std::string user_query; - - for (const auto& msg : messages) { - if (msg.role == "system") { - if (!system_text.empty()) { - system_text += "\n"; - } - system_text += msg.content; - } else if (msg.role == "user") { - user_query = msg.content; - } - } - - if (user_query.empty() && !messages.empty()) { - user_query = messages.back().content; - } - if (system_text.empty()) { - return user_query; - } - if (user_query.empty()) { - return system_text; - } - return system_text + "\n\n" + user_query; -} - -struct ToolConstraintSpec { - std::string name; - std::vector parameter_names; - std::vector required_parameter_names; -}; - -struct TokenizerRuntimeConfig { - enum class TokenizerType { UNKNOWN, BPE, SENTENCEPIECE }; - enum class VocabFormat { UNKNOWN, ID_TAB_TOKEN, LINE_TOKEN }; - enum class Normalizer { NONE, METASPACE, BYTE_LEVEL }; - enum class Decoder { NONE, REPLACE_METASPACE, BYTE_LEVEL }; - - TokenizerType tokenizer_type = TokenizerType::UNKNOWN; - VocabFormat vocab_format = VocabFormat::UNKNOWN; - Normalizer normalizer = Normalizer::NONE; - Decoder decoder = Decoder::NONE; - bool byte_fallback = false; - bool has_chat_template = false; -}; - -TokenizerRuntimeConfig load_tokenizer_runtime_config(const std::string& config_file); -void load_special_tokens_map(const std::string& config_file, std::unordered_map& special_tokens); -std::vector split_with_special_tokens(const std::string& text, const std::unordered_map& special_tokens); - -inline std::string extract_json_string(const std::string& json, size_t& pos) { - std::string value; - while (pos < json.size() && json[pos] != '"') { - if (json[pos] == '\\' && pos + 1 < json.size()) { - pos++; - if (json[pos] == 'n') value += '\n'; - else if (json[pos] == 't') value += '\t'; - else if (json[pos] == 'r') value += '\r'; - else if (json[pos] == '"') value += '"'; - else if (json[pos] == '\\') value += '\\'; - else value += json[pos]; - } else { - value += json[pos]; - } - pos++; - } - if (pos < json.size()) pos++; - return value; -} - -class Tokenizer { -public: - virtual ~Tokenizer() = default; - - virtual std::vector encode(const std::string& text) const = 0; - virtual std::string decode(const std::vector& tokens) const = 0; - - virtual std::vector apply_chat_template(const std::vector& messages, bool add_generation_prompt = true) const; - virtual std::string format_chat_prompt(const std::vector& messages, bool add_generation_prompt = true, const std::string& tools_json = "", bool enable_thinking_if_supported = false) const; - - virtual uint32_t get_vocab_size() const = 0; - virtual uint32_t get_unk_token() const = 0; - virtual uint32_t get_bos_token() const = 0; - virtual uint32_t get_eos_token() const = 0; - virtual bool has_chat_template() const { return has_chat_template_; } - std::string get_default_stop_sequence() const; - - virtual bool load_vocabulary_with_config(const std::string& vocab_file, const std::string& merges_file, const std::string& config_file) = 0; - - uint32_t get_image_token_id() const { return image_token_id_; } - uint32_t get_fake_token_id() const { return fake_token_id_; } - uint32_t get_global_img_token_id() const { return global_img_token_id_; } - -protected: - enum class ModelType { UNKNOWN, QWEN, QWEN3P5, GEMMA, GEMMA4, LFM2, BERT, WHISPER, PARAKEET, YOUTU, NEEDLE}; - ModelType model_type_ = ModelType::UNKNOWN; - enum class ModelVariant { DEFAULT, VLM, EXTRACT, RAG}; - ModelVariant model_variant_ = ModelVariant::DEFAULT; - bool has_chat_template_ = false; - std::string chat_template_; - - uint32_t image_token_id_ = 396; - uint32_t fake_token_id_ = 49189; - uint32_t global_img_token_id_ = 49152; - - - uint32_t vision_patch_size_ = 16; - uint32_t vision_pooling_kernel_size_ = 3; - uint32_t vision_default_output_length_ = 280; - uint32_t vision_image_size_ = 768; - TokenizerRuntimeConfig runtime_config_; - - void detect_model_type(const std::string& config_path); - void load_chat_template(const std::string& template_file); - std::string format_qwen_style(const std::vector& messages, bool add_generation_prompt, const std::string& tools_json, bool enable_thinking_if_supported = false) const; - std::string format_gemma_style(const std::vector& messages, bool add_generation_prompt, const std::string& tools_json) const; - std::string format_gemma4_style(const std::vector& messages, bool add_generation_prompt, const std::string& tools_json, bool enable_thinking_if_supported = false) const; - std::string format_lfm2_style(const std::vector& messages, bool add_generation_prompt, const std::string& tools_json) const; - std::string format_lfm2_vl_style(const std::vector& messages, bool add_generation_prompt, const std::string& tools_json) const; - std::string format_needle_style(const std::vector& messages, bool add_generation_prompt, const std::string& tools_json) const; - std::string format_youtu_style(const std::vector& messages, bool add_generation_prompt, const std::string& tools_json) const; -}; - -class BPETokenizer : public Tokenizer { -public: - BPETokenizer(); - ~BPETokenizer(); - - bool load_vocabulary_mmap(const std::string& vocab_file, const std::string& merges_file); - bool load_vocabulary_with_config(const std::string& vocab_file, const std::string& merges_file, const std::string& config_file) override; - - std::vector encode(const std::string& text) const override; - std::string decode(const std::vector& tokens) const override; - - uint32_t get_vocab_size() const override { return vocab_size_; } - uint32_t get_unk_token() const override { return unk_token_id_; } - uint32_t get_bos_token() const override { return bos_token_id_; } - uint32_t get_eos_token() const override { return eos_token_id_; } - -private: - std::unordered_map token_to_id_; - std::vector id_to_token_; - std::vector merge_rules_; - std::unordered_map merge_map_; - - uint32_t vocab_size_; - uint32_t unk_token_id_; - uint32_t bos_token_id_; - uint32_t eos_token_id_; - - void* vocab_mmap_ptr_; - size_t vocab_mmap_size_; - - void* merges_mmap_ptr_; - size_t merges_mmap_size_; - - std::vector apply_bpe(const std::vector& tokens) const; - std::pair find_best_merge_fast(const std::vector& tokens) const; - - std::string bytes_to_unicode(const std::string& text) const; - std::string unicode_to_bytes(const std::string& text) const; - std::vector byte_level_split(const std::string& text) const; - std::vector utf8_split(const std::string& text) const; - - void cleanup_mmap(); - -private: - mutable std::unordered_map byte_to_unicode_; - mutable std::unordered_map unicode_to_byte_; - void init_byte_mappings() const; - - std::unordered_map special_tokens_; - std::vector split_with_special_tokens(const std::string& text) const; - void load_special_tokens(const std::string& config_file); -}; - -class SPTokenizer : public Tokenizer { -public: - SPTokenizer(); - ~SPTokenizer(); - - bool load_vocabulary_with_config(const std::string& vocab_file, const std::string& merges_file, const std::string& config_file) override; - - std::vector encode(const std::string& text) const override; - std::string decode(const std::vector& tokens) const override; - - uint32_t get_vocab_size() const override { return vocab_size_; } - uint32_t get_unk_token() const override { return unk_token_id_; } - uint32_t get_bos_token() const override { return bos_token_id_; } - uint32_t get_eos_token() const override { return eos_token_id_; } - -private: - struct TrieNode { - std::unordered_map> children; - int32_t token_id = -1; - float score = 0.0f; - }; - - std::unique_ptr trie_root_; - std::unordered_map token_to_id_; - std::vector id_to_token_; - std::vector token_scores_; - - uint32_t vocab_size_; - uint32_t unk_token_id_; - uint32_t bos_token_id_; - uint32_t eos_token_id_; - uint32_t pad_token_id_; - - bool sp_bpe_mode_ = false; - bool sp_add_dummy_prefix_ = false; - bool sp_byte_fallback_ = false; - - void* vocab_mmap_ptr_; - size_t vocab_mmap_size_; - - void build_trie(); - std::vector> tokenize_with_trie(const std::string& text) const; - std::vector tokenize_with_bpe(const std::string& text) const; - std::string preprocess_text(const std::string& text) const; - std::string postprocess_text(const std::string& text) const; - std::vector split_by_unicode_spaces(const std::string& text) const; - - void cleanup_mmap(); - - std::unordered_map special_tokens_; - std::vector split_with_special_tokens(const std::string& text) const; - void load_special_tokens(const std::string& config_file); -}; - -class ConvCache { -public: - struct CircularView { - const void* ptr1; - size_t len1; - const void* ptr2; - size_t len2; - size_t total_len; - }; - - void init(size_t layers, size_t hidden_dim, size_t window_len, Precision model_precision); - CircularView get_window(size_t layer) const; - void update(CactusGraph* gb, size_t layer, const size_t latest_token); - void reset(); - - bool is_empty() const { return num_layers == 0; } - - size_t num_layers = 0; - size_t hidden_size = 0; - size_t window_size = 0; - Precision precision = Precision::FP32; - size_t element_size = 4; - -private: - struct LayerState { - std::vector data; - size_t head = 0; - size_t count = 0; - }; - - std::vector layer_states; -}; - -struct KVCache { - static constexpr size_t DEFAULT_WINDOW_SIZE = 1024; - static constexpr size_t DEFAULT_SINK_SIZE = 4; - - struct LayerCache { - std::vector keys; - std::vector values; - std::vector key_scales; - std::vector value_scales; - size_t head_dim = 0; - size_t kv_heads = 0; - }; - - std::vector layer_caches; - - size_t window_size = DEFAULT_WINDOW_SIZE; - size_t sink_size = DEFAULT_SINK_SIZE; - size_t current_seq_len = 0; - size_t total_seq_len = 0; - size_t max_seq_len = 2048; - size_t num_layers = 0; - Precision precision; - size_t element_size = 4; - - void set_window_size(size_t window, size_t sink = DEFAULT_SINK_SIZE); - size_t get_effective_seq_len() const { return current_seq_len; } - size_t get_total_seq_len() const { return total_seq_len; } - size_t get_layer_head_dim(size_t layer_idx) const { return layer_caches[layer_idx].head_dim; } - size_t get_layer_kv_heads(size_t layer_idx) const { return layer_caches[layer_idx].kv_heads; } - - void init(size_t num_layers, size_t max_seq, const std::vector& layer_dims, const std::vector& layer_kv_heads, Precision model_precision); - void reset(); - void update_from_graph(CactusGraph* gb, const std::vector& k_nodes, - const std::vector& v_nodes, size_t seq_len, - size_t num_layers); - - void update_from_npu(size_t layer_idx, const __fp16* k_data, const __fp16* v_data, - size_t num_tokens, size_t kv_heads, size_t head_dim); - - bool is_empty() const { return current_seq_len == 0; } - void* get_key_ptr(size_t layer); - void* get_value_ptr(size_t layer); - - struct CircularView { - const void* ptr1; - const void* ptr2; - size_t len1; - size_t len2; - size_t total_len; - }; - - CircularView get_key_view(size_t layer); - CircularView get_value_view(size_t layer); - - const int8_t* get_keys_int8(size_t layer) const; - const int8_t* get_values_int8(size_t layer) const; - const float* get_key_scales(size_t layer) const; - const float* get_value_scales(size_t layer) const; - - void remove_token_range(size_t start, size_t count); - void compact_to_windows(const std::vector& target_windows); -}; - -class ToolCallConstrainer { -public: - enum class State { - DONE, - - QWEN_START, - QWEN_EXPECT_OPEN_BRACE, - QWEN_EXPECT_NAME_KEY, - QWEN_EXPECT_NAME_COLON, - QWEN_EXPECT_NAME_VALUE, - QWEN_EXPECT_COMMA, - QWEN_EXPECT_ARGS_KEY, - QWEN_EXPECT_ARGS_COLON, - QWEN_IN_ARGUMENTS, - QWEN_EXPECT_CLOSE_BRACE, - QWEN_EXPECT_END, - - NEEDLE_START, - - LFM_START, - LFM_EXPECT_BRACKET, - LFM_IN_FUNC_NAME, - LFM_EXPECT_PAREN, - LFM_IN_ARGUMENTS, - LFM_EXPECT_BRACKET_CLOSE, - LFM_EXPECT_END, - - GEMMA_START, - GEMMA_EXPECT_CALL, - GEMMA_IN_FUNC_NAME, - GEMMA_EXPECT_BRACE, - GEMMA_IN_ARGUMENTS, - GEMMA_EXPECT_END - }; - - void init(Config::ModelType model_type, - const std::vector& tools, - Tokenizer* tokenizer); - - const std::unordered_map& get_bias() const { return current_bias_; } - - void update(uint32_t token_id, const std::string& decoded_text); - - void reset(); - - bool is_active() const { return active_; } - -private: - bool active_ = false; - State state_ = State::QWEN_START; - Config::ModelType model_type_ = Config::ModelType::QWEN; - Tokenizer* tokenizer_ = nullptr; - - bool is_gemma_family() const { return Config::is_gemma_family(model_type_); } - bool is_needle() const { return model_type_ == Config::ModelType::NEEDLE; } - - enum class NeedleJsonState { - FREE, - IN_NAME, - IN_ARG_KEY, - }; - - struct NeedleTrieNode { - std::unordered_map> children; - bool is_terminal = false; - }; - - std::vector tool_specs_; - std::vector function_names_; - std::string generated_text_; - int brace_depth_ = 0; - - std::string call_start_tag_; - std::string call_end_tag_; - - std::unordered_set qwen_tool_call_start_tokens_; - std::unordered_set qwen_tool_call_end_tokens_; - std::unordered_set open_brace_tokens_; - std::unordered_set close_brace_tokens_; - std::unordered_set colon_tokens_; - std::unordered_set comma_tokens_; - std::unordered_set name_key_tokens_; - std::unordered_set args_key_tokens_; - std::unordered_set quote_tokens_; - std::unordered_set backtick_tokens_; - std::unordered_set all_func_name_tokens_; - std::unordered_map> func_name_sequences_; - NeedleJsonState needle_json_state_ = NeedleJsonState::FREE; - std::string needle_buffer_; - std::string needle_constrained_buf_; - std::string needle_current_function_; - bool needle_in_arguments_ = false; - int needle_arguments_depth_ = 0; - int needle_nesting_depth_ = 0; - bool needle_in_string_value_ = false; - bool needle_prev_char_escape_ = false; - std::unique_ptr needle_name_trie_; - std::unordered_map> needle_param_tries_; - std::vector needle_token_strings_; - std::unordered_map> needle_token_index_; - - std::unordered_set tool_start_tokens_; - std::unordered_set tool_end_tokens_; - std::unordered_set bracket_open_tokens_; - std::unordered_set bracket_close_tokens_; - std::unordered_set paren_open_tokens_; - std::unordered_set paren_close_tokens_; - std::unordered_set equals_tokens_; - - std::string lfm_current_function_; - std::string lfm_args_buffer_; - std::unordered_set lfm_seen_arg_keys_; - std::unordered_map> lfm_required_params_; - std::unordered_map> lfm_all_params_; - - std::unordered_set gemma_call_start_tokens_; - std::unordered_set gemma_call_end_tokens_; - std::unordered_set gemma_response_start_tokens_; - std::unordered_set gemma_call_prefix_tokens_; - std::unordered_set escape_tokens_; - - std::unordered_map current_bias_; - - void compute_bias(); - void tokenize_grammar_elements(); - void add_tokens_for_string(const std::string& str, std::unordered_set& token_set); - void add_tokens_for_prefix_string(const std::string& prefix, std::unordered_set& token_set); - void add_tokens_containing(char needle, std::unordered_set& token_set); - void tokenize_function_names(bool quote_names); - void init_common_tokens(); - void init_needle_constraints(); - void reset_needle_constraints(); - void feed_needle_text(const std::string& text); - void feed_needle_char(char ch); - bool needle_at_arg_key_start() const; - bool needle_is_value_string_start() const; - void needle_insert_word(NeedleTrieNode* root, const std::string& word); - const NeedleTrieNode* needle_get_trie_node(const NeedleTrieNode* root, const std::string& prefix) const; - bool needle_check_token_valid(const std::string& token_text, const NeedleTrieNode* trie_node) const; -}; - -class Model { -public: - struct DebugNode { - uint32_t layer_idx; - std::string name; - size_t node_id; - }; - - Model(); - explicit Model(const Config& config); - virtual ~Model(); - - const Config& get_config() const { return config_; } - Tokenizer* get_tokenizer() const { return tokenizer_.get(); } - const std::vector& get_debug_nodes() const; - - virtual bool init(const std::string& model_folder, size_t context_size, const std::string& system_prompt = "", bool do_warmup = true); - - virtual bool init(CactusGraph* external_graph, const std::string& model_folder, size_t context_size, - const std::string& system_prompt = "", bool do_warmup = true); - - virtual uint32_t decode(const std::vector& tokens, float temperature = -1.0f, float top_p = -1.0f, - size_t top_k = 0, const std::string& profile_file = "", float* out_entropy = nullptr, - float min_p = 0.15f, float repetition_penalty = 1.1f); - - virtual void prefill(const std::vector& tokens, size_t chunk_size = 256, const std::string& profile_file = ""); - - virtual void prefill_with_images(const std::vector& tokens, const std::vector& image_paths, - const std::string& profile_file = ""); - - virtual uint32_t decode_with_images(const std::vector& tokens, const std::vector& image_paths, - float temperature = -1.0f, float top_p = -1.0f, - size_t top_k = 0, const std::string& profile_file = "", float* out_entropy = nullptr, - float min_p = 0.15f, float repetition_penalty = 1.1f); - - virtual uint32_t decode_with_audio(const std::vector& tokens, const std::vector& audio_features, float temperature = 0.0f, float top_p = 0.0f, - size_t top_k = 0, const std::string& profile_file = "", float* out_entropy = nullptr, - float min_p = 0.15f, float repetition_penalty = 1.1f, - float* out_token_time_start = nullptr, float* out_token_time_end = nullptr); - - std::vector get_embeddings(const std::vector& tokens, bool pooled = true, bool normalize = false, const std::string& profile_file = ""); - - virtual std::vector get_image_embeddings(const std::string& image_path); - - virtual std::vector get_audio_embeddings(const std::vector& audio_features); - - virtual void reset_cache() { kv_cache_.reset(); token_history_.clear(); } - void record_sampled_token(uint32_t token) { - if (token_history_.size() >= MAX_TOKEN_HISTORY) { - token_history_.erase(token_history_.begin(), token_history_.begin() + (MAX_TOKEN_HISTORY / 2)); - } - token_history_.push_back(token); - } - - double score_tokens_window_logprob(const std::vector& tokens, size_t start, size_t end, size_t context, size_t* tokens_scored); - - - - void set_cache_window(size_t window_size, size_t sink_size = 4) { kv_cache_.set_window_size(window_size, sink_size); } - size_t get_cache_size() const { return kv_cache_.current_seq_len; } - - bool load_npu_prefill(const std::string& model_path); - bool has_npu_prefill() const; - size_t get_prefill_chunk_size() const; - - virtual void remove_thinking_tokens(const std::vector>& ranges); - virtual void compact_kv_cache() {} - - virtual void set_tool_constraints(const std::vector& tools); - virtual void clear_tool_constraints(); - virtual void update_tool_constraints(uint32_t token_id); - - void* graph_handle_; - - void set_vocab_bias(const std::unordered_map& bias) { - vocab_bias_ = bias; - } - - void clear_vocab_bias() { - vocab_bias_.clear(); - } - - bool has_vocab_bias() const { - return !vocab_bias_.empty(); - } - - const std::unordered_map& get_vocab_bias() const { - return vocab_bias_; - } - -protected: - size_t sample_token(CactusGraph* gb, size_t logits_node_id, float temperature, float top_p, size_t top_k, - float min_p, float repetition_penalty, - const std::unordered_map* extra_bias = nullptr) const; - - static void compute_entropy(CactusGraph* gb, size_t logits_node_id, float* out_entropy); - - virtual size_t forward(const std::vector& tokens, bool use_cache = false) = 0; - - virtual size_t forward(const std::vector& audio_features, const std::vector& tokens, bool use_cache = false); - - virtual void load_weights_to_graph(CactusGraph* gb) = 0; - - virtual size_t build_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0) = 0; - - virtual size_t build_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, - ComputeBackend backend) const = 0; - virtual size_t build_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0) = 0; - void update_kv_cache(CactusGraph* gb, size_t seq_len); - virtual std::vector get_kv_layer_dims() const { - return std::vector(config_.num_layers, config_.attention_head_dim); - } - virtual std::vector get_kv_layer_heads() const { - return std::vector(config_.num_layers, config_.attention_kv_heads); - } - virtual void post_init() {} - virtual void post_execute_updates(CactusGraph*, size_t) {} - Config config_; - std::unique_ptr tokenizer_; - - bool initialized_; - float attention_scale_; - -protected: - KVCache kv_cache_; - std::vector cache_k_output_nodes_; - std::vector cache_v_output_nodes_; - - std::string embedding_file_path_; - size_t embedding_node_id_; - std::string model_folder_path_; - size_t output_weight_node_id_; - - mutable std::vector debug_nodes_; - - void capture_debug_node(uint32_t layer_idx, const std::string& name, size_t node_id) const; - void clear_debug_nodes(); - - bool init_internal(CactusGraph* gb, const std::string& model_folder, size_t context_size, - const std::string& system_prompt, bool do_warmup); - bool owns_graph_; - - std::unique_ptr npu_prefill_; - void prefill_npu(const std::vector& tokens); - virtual std::vector<__fp16> get_token_embeddings(const std::vector& tokens); - - static constexpr size_t MAX_TOKEN_HISTORY = 128; - ToolCallConstrainer tool_constrainer_; - std::vector token_history_; - -private: - std::unordered_map vocab_bias_; -}; - -std::unique_ptr create_model(const std::string& model_folder); - -class Siglip2Preprocessor { -public: - struct Config { - int patch_size = 16; - int downsample_factor = 2; - int min_tiles = 2; - int max_tiles = 10; - bool use_thumbnail = true; - int min_image_tokens = 64; - int max_image_tokens = 256; - int max_num_patches = 1024; - int tile_size = 512; - float max_pixels_tolerance = 2.0f; - bool do_resize = true; - bool do_rescale = true; - bool do_normalize = true; - bool do_convert_rgb = true; - bool do_image_splitting = true; - float rescale_factor = 1.0f / 255.0f; - float image_mean[3] = {0.5f, 0.5f, 0.5f}; - float image_std[3] = {0.5f, 0.5f, 0.5f}; - }; - - struct PreprocessedImage { - std::vector pixel_values; - std::vector pixel_attention_mask; - std::vector> spatial_shapes; - std::vector pixel_values_shape; - std::vector pixel_attention_mask_shape; - std::vector spatial_shapes_shape; - int num_patches_height; - int num_patches_width; - int actual_num_patches; - int num_tiles; - int patch_dim; - int max_patches_per_tile; - - int image_rows; - int image_cols; - int image_height; - int image_width; - int tokens_per_tile; - int thumbnail_tokens; - - ~PreprocessedImage(); - }; - - struct SpatialShapeResult { - std::vector> shapes; - int grid_rows; - int grid_cols; - }; - - explicit Siglip2Preprocessor(const Config& config); - Siglip2Preprocessor(); - ~Siglip2Preprocessor(); - - PreprocessedImage preprocess_from_file(const std::string& image_path); - PreprocessedImage preprocess_from_memory(const unsigned char* img_data, int width, int height, int channels); - SpatialShapeResult compute_spatial_shapes(int height, int width); - -private: - Config config_; - - std::pair compute_pixel_limits() const; - std::vector convert_to_rgb(const unsigned char* img_data, int width, int height, int channels); - std::pair smart_resize(int height, int width); - bool is_image_too_large(int height, int width); - std::pair get_grid_layout(int height, int width); - std::pair find_closest_aspect_ratio(float aspect_ratio, int width, int height); - std::vector resize_image(const unsigned char* img_data, int src_width, int src_height, - int dst_width, int dst_height, int channels); - std::vector normalize_image(const float* img_data, int width, int height, int channels); - std::vector> convert_image_to_patches( - const std::vector& image, int width, int height, int channels, int patch_size); - PreprocessedImage pad_patches(const std::vector>& tile_patches, - const std::vector>& spatial_shapes, - int patch_dim, - int max_patches_per_tile); - int round_by_factor(int number, int factor); -}; - -class AudioProcessor { -public: - struct SpectrogramConfig { - size_t n_fft = 400; - size_t hop_length = 160; - size_t frame_length = 400; - float power = 2.0f; - bool center = true; - const char* pad_mode = "reflect"; - bool onesided = true; - float dither = 0.0f; - float mel_floor = 1e-10f; - const char* log_mel = nullptr; - float reference = 1.0f; - float min_value = 1e-10f; - bool remove_dc_offset = false; - float preemphasis = 0.0f; - bool hann_periodic = true; - float window_a0 = 0.5f; - size_t fft_override = 0; - bool mel_floor_additive = false; - }; - - AudioProcessor(); - ~AudioProcessor(); - - void init_mel_filters(size_t num_frequency_bins, size_t num_mel_filters, - float min_freq, float max_freq, size_t sampling_rate, - const char* norm = "slaney", const char* mel_scale = "slaney"); - - std::vector compute_spectrogram( - const std::vector& waveform, - const SpectrogramConfig& config); - - static std::vector compute_irfft( - const std::vector& complex_input, - size_t n, - const char* norm = "backward"); - - const std::vector& get_mel_filters() const { return mel_filters_; } - - size_t get_num_mel_filters() const { return num_mel_filters_; } - size_t get_num_frequency_bins() const { return num_frequency_bins_; } - -private: - std::vector mel_filters_; - size_t num_frequency_bins_; - size_t num_mel_filters_; -}; - -namespace index { - constexpr uint32_t MAGIC = 0x43414354; - constexpr uint32_t VERSION = 1; - - struct Document { - int id; - std::vector embedding; - std::string content; - std::string metadata; - }; - - struct QueryResult { - int doc_id; - float score; - - QueryResult(int doc_id, float score) : doc_id(doc_id), score(score) {} - }; - - struct QueryOptions { - size_t top_k = 10; - float score_threshold = -1.0f; - }; - - class Index { - public: - Index(const std::string& index_path, const std::string& data_path, size_t embedding_dim); - ~Index(); - - Index(const Index&) = delete; - Index& operator=(const Index&) = delete; - Index(Index&&) = delete; - Index& operator=(Index&&) = delete; - - void add_documents(const std::vector& documents); - void delete_documents(const std::vector& doc_ids); - std::vector get_documents(const std::vector& doc_ids); - std::vector> query(const std::vector>& embeddings, const QueryOptions& options); - void compact(); - - private: - struct IndexHeader { - uint32_t magic; - uint32_t version; - uint32_t embedding_dim; - uint32_t num_documents; - }; - - struct IndexEntry { - int32_t doc_id; - uint64_t data_offset; - uint8_t flags; // bit 0: tombstone - - const __fp16* embedding() const { - return reinterpret_cast(this + 1); - } - - static size_t size(size_t embedding_dim) { - return sizeof(IndexEntry) + embedding_dim * sizeof(__fp16); - } - }; - - struct DataHeader { - uint32_t magic; - uint32_t version; - }; - - struct DataEntry { - uint16_t content_len; - uint16_t metadata_len; - - const char* content() const { - return reinterpret_cast(this + 1); - } - - const char* metadata() const { - return content() + content_len; - } - }; - - void parse_index_header(); - void parse_data_header(); - void build_doc_id_map(); - void validate_documents(const std::vector& documents); - void validate_doc_ids(const std::vector& doc_ids); - ssize_t write_full(int fd, const void* buf, size_t count); - - std::unordered_map doc_id_map_; - - std::string index_path_, data_path_; - size_t embedding_dim_; - size_t index_entry_size_; - uint32_t num_documents_; - - int index_fd_, data_fd_; - void *mapped_index_, *mapped_data_; - size_t index_file_size_, data_file_size_; - }; -} // namespace index - -} -} diff --git a/cactus/engine/engine_bpe.cpp b/cactus/engine/engine_bpe.cpp deleted file mode 100644 index 58785735d..000000000 --- a/cactus/engine/engine_bpe.cpp +++ /dev/null @@ -1,566 +0,0 @@ -#include "engine.h" -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cactus { -namespace engine { - -namespace { -constexpr const char* kMetaspace = "\xE2\x96\x81"; -} // namespace - -BPETokenizer::BPETokenizer() - : vocab_size_(0), unk_token_id_(0), bos_token_id_(1), eos_token_id_(2), - vocab_mmap_ptr_(nullptr), vocab_mmap_size_(0), - merges_mmap_ptr_(nullptr), merges_mmap_size_(0) { - has_chat_template_ = false; -} - -BPETokenizer::~BPETokenizer() { - cleanup_mmap(); -} - -void BPETokenizer::cleanup_mmap() { - if (vocab_mmap_ptr_ && vocab_mmap_ptr_ != MAP_FAILED) { - munmap(vocab_mmap_ptr_, vocab_mmap_size_); - vocab_mmap_ptr_ = nullptr; - } - if (merges_mmap_ptr_ && merges_mmap_ptr_ != MAP_FAILED) { - munmap(merges_mmap_ptr_, merges_mmap_size_); - merges_mmap_ptr_ = nullptr; - } -} - -bool BPETokenizer::load_vocabulary_mmap(const std::string& vocab_file, const std::string& merges_file) { - int vocab_fd = open(vocab_file.c_str(), O_RDONLY); - if (vocab_fd == -1) return false; - - struct stat vocab_stat; - if (fstat(vocab_fd, &vocab_stat) == -1) { - close(vocab_fd); - return false; - } - - vocab_mmap_size_ = vocab_stat.st_size; - vocab_mmap_ptr_ = mmap(nullptr, vocab_mmap_size_, PROT_READ, MAP_PRIVATE, vocab_fd, 0); - close(vocab_fd); - - if (vocab_mmap_ptr_ == MAP_FAILED) return false; - - std::string vocab_content(static_cast(vocab_mmap_ptr_), vocab_mmap_size_); - std::istringstream vocab_stream(vocab_content); - - auto rtrim_cr = [](std::string& s) { - if (!s.empty() && s.back() == '\r') s.pop_back(); - }; - - std::string line; - token_to_id_.clear(); - id_to_token_.clear(); - special_tokens_.clear(); - const bool use_id_tab_vocab = - runtime_config_.vocab_format == TokenizerRuntimeConfig::VocabFormat::ID_TAB_TOKEN; - - if (use_id_tab_vocab) { - while (std::getline(vocab_stream, line)) { - rtrim_cr(line); - - std::string token; - uint32_t id = UINT32_MAX; - - std::istringstream iss(line); - if (iss >> id) { - if (std::getline(iss, token) && !token.empty() && token[0] == '\t') { - token = token.substr(1); - } - - if (token.empty()) { - auto last_pos = vocab_stream.tellg(); - while (std::getline(vocab_stream, line)) { - rtrim_cr(line); - if (!line.empty()) { - break; - } - token += '\n'; - last_pos = vocab_stream.tellg(); - } - vocab_stream.seekg(last_pos); - } - } - - if (!token.empty() && id != UINT32_MAX) { - token_to_id_[token] = id; - if (id >= id_to_token_.size()) { - id_to_token_.resize(id + 1); - } - id_to_token_[id] = token; - } - } - vocab_size_ = static_cast(id_to_token_.size()); - } else { - uint32_t id = 0; - while (std::getline(vocab_stream, line)) { - rtrim_cr(line); - token_to_id_[line] = id; - id_to_token_.push_back(line); - ++id; - } - vocab_size_ = id; - } - - int merges_fd = open(merges_file.c_str(), O_RDONLY); - if (merges_fd == -1) return false; - - struct stat merges_stat; - if (fstat(merges_fd, &merges_stat) == -1) { - close(merges_fd); - return false; - } - - merges_mmap_size_ = merges_stat.st_size; - merges_mmap_ptr_ = mmap(nullptr, merges_mmap_size_, PROT_READ, MAP_PRIVATE, merges_fd, 0); - close(merges_fd); - - if (merges_mmap_ptr_ == MAP_FAILED) return false; - - std::string merges_content(static_cast(merges_mmap_ptr_), merges_mmap_size_); - std::istringstream merges_stream(merges_content); - - merge_rules_.clear(); - uint32_t priority = 0; - - while (std::getline(merges_stream, line)) { - rtrim_cr(line); - if (line.empty() || line[0] == '#') continue; - - std::istringstream iss(line); - std::string first, second; - if (iss >> first >> second) { - rtrim_cr(first); - rtrim_cr(second); - - std::string merged = first + second; - merge_rules_.emplace_back(first, second, merged, priority); - - std::string key = first + "\x00" + second; - auto it = merge_map_.find(key); - if (it == merge_map_.end() || priority < it->second) { - merge_map_[key] = priority; - } - priority++; - } - } - - auto is_whitespace_only = [](const std::string& s) { - if (s.empty()) return false; - for (char c : s) { - if (c != ' ' && c != '\n' && c != '\t' && c != '\r') return false; - } - return true; - }; - std::vector ws_tokens; - for (const auto& tok : id_to_token_) { - if (tok.size() >= 2 && is_whitespace_only(tok)) ws_tokens.push_back(tok); - } - std::sort(ws_tokens.begin(), ws_tokens.end(), - [](const std::string& a, const std::string& b) { return a.size() < b.size(); }); - for (const auto& tok : ws_tokens) { - for (size_t i = 1; i < tok.size(); i++) { - std::string first = tok.substr(0, i); - std::string second = tok.substr(i); - if (!token_to_id_.count(first) || !token_to_id_.count(second)) continue; - std::string key = first + "\x00" + second; - if (merge_map_.find(key) == merge_map_.end()) { - merge_rules_.emplace_back(first, second, tok, priority); - merge_map_[key] = priority; - priority++; - } - break; - } - } - - std::sort(merge_rules_.begin(), merge_rules_.end(), - [](const MergeRule& a, const MergeRule& b) { - return a.priority < b.priority; - }); - - return true; -} - -bool BPETokenizer::load_vocabulary_with_config(const std::string& vocab_file, const std::string& merges_file, const std::string& config_file) { - runtime_config_ = load_tokenizer_runtime_config(config_file); - if (!load_vocabulary_mmap(vocab_file, merges_file)) { - return false; - } - - std::ifstream config_stream(config_file); - if (!config_stream.is_open()) { - return true; - } - - std::string line; - while (std::getline(config_stream, line)) { - if (line.empty() || line[0] == '#') continue; - - size_t eq_pos = line.find('='); - if (eq_pos == std::string::npos) continue; - - std::string key = line.substr(0, eq_pos); - std::string value = line.substr(eq_pos + 1); - - key.erase(0, key.find_first_not_of(" \t")); - key.erase(key.find_last_not_of(" \t") + 1); - value.erase(0, value.find_first_not_of(" \t")); - value.erase(value.find_last_not_of(" \t") + 1); - - if (key == "eos_token_id") { - eos_token_id_ = std::stoul(value); - } else if (key == "pad_token_id") { - if (unk_token_id_ == 0) { - unk_token_id_ = std::stoul(value); - } - } else if (key == "unk_token_id" && value != "null") { - unk_token_id_ = std::stoul(value); - } else if (key == "bos_token_id" && value != "null") { - bos_token_id_ = std::stoul(value); - } else if (key == "vocab_size") { - if (std::stoul(value) != vocab_size_) { - } - } - } - - std::string dir = config_file.substr(0, config_file.find_last_of("/\\")); - std::string special_tokens_path = dir + "/special_tokens.json"; - load_special_tokens(special_tokens_path); - - std::string template_path = dir + "/chat_template.jinja2"; - load_chat_template(template_path); - - std::string config_path = dir + "/config.txt"; - detect_model_type(config_path); - - return true; -} - -void BPETokenizer::load_special_tokens(const std::string& config_file) { - load_special_tokens_map(config_file, special_tokens_); -} - -std::vector BPETokenizer::split_with_special_tokens(const std::string& text) const { - return cactus::engine::split_with_special_tokens(text, special_tokens_); -} - -void BPETokenizer::init_byte_mappings() const { - if (!byte_to_unicode_.empty()) return; - - std::vector bytes; - - for (int i = 33; i <= 126; ++i) { - bytes.push_back(i); - } - - - for (int i = 161; i <= 255; ++i) { - bytes.push_back(i); - } - - std::vector remaining_bytes; - for (int i = 0; i <= 32; ++i) remaining_bytes.push_back(i); - remaining_bytes.push_back(127); - for (int i = 128; i <= 160; ++i) remaining_bytes.push_back(i); - - int unicode_start = 256; - for (int byte : remaining_bytes) { - bytes.push_back(byte); - } - - for (size_t i = 0; i < bytes.size(); ++i) { - uint8_t byte = static_cast(bytes[i]); - - if (byte >= 33 && byte <= 126) { - std::string unicode_char(1, static_cast(byte)); - byte_to_unicode_[byte] = unicode_char; - unicode_to_byte_[unicode_char] = byte; - } else if (byte >= 161 && byte <= 255) { - std::string unicode_char; - unicode_char += static_cast(0xC0 | (byte >> 6)); - unicode_char += static_cast(0x80 | (byte & 0x3F)); - byte_to_unicode_[byte] = unicode_char; - unicode_to_byte_[unicode_char] = byte; - } else { - int unicode_point = unicode_start++; - std::string unicode_char; - if (unicode_point < 0x800) { - unicode_char += static_cast(0xC0 | (unicode_point >> 6)); - unicode_char += static_cast(0x80 | (unicode_point & 0x3F)); - } else { - unicode_char += static_cast(0xE0 | (unicode_point >> 12)); - unicode_char += static_cast(0x80 | ((unicode_point >> 6) & 0x3F)); - unicode_char += static_cast(0x80 | (unicode_point & 0x3F)); - } - byte_to_unicode_[byte] = unicode_char; - unicode_to_byte_[unicode_char] = byte; - } - } -} - -std::string BPETokenizer::bytes_to_unicode(const std::string& text) const { - init_byte_mappings(); - - std::string result; - for (uint8_t byte : text) { - result += byte_to_unicode_.at(byte); - } - return result; -} - -std::string BPETokenizer::unicode_to_bytes(const std::string& text) const { - init_byte_mappings(); - - std::string result; - size_t i = 0; - while (i < text.length()) { - std::string unicode_char; - - if ((text[i] & 0x80) == 0) { - unicode_char = text.substr(i, 1); - i += 1; - } else if ((text[i] & 0xE0) == 0xC0) { - unicode_char = text.substr(i, 2); - i += 2; - } else if ((text[i] & 0xF0) == 0xE0) { - unicode_char = text.substr(i, 3); - i += 3; - } else { - unicode_char = text.substr(i, 1); - i += 1; - } - - auto it = unicode_to_byte_.find(unicode_char); - if (it != unicode_to_byte_.end()) { - result += static_cast(it->second); - } else if (unicode_char.size() == 1 && (unsigned char)unicode_char[0] < 128) { - result += unicode_char[0]; - } else { - result += '?'; - } - } - return result; -} - -std::vector BPETokenizer::byte_level_split(const std::string& text) const { - std::string unicode_text = bytes_to_unicode(text); - - std::vector chars; - size_t i = 0; - while (i < unicode_text.length()) { - size_t char_len = 1; - - if ((unicode_text[i] & 0x80) == 0) { - char_len = 1; - } else if ((unicode_text[i] & 0xE0) == 0xC0) { - char_len = 2; - } else if ((unicode_text[i] & 0xF0) == 0xE0) { - char_len = 3; - } else if ((unicode_text[i] & 0xF8) == 0xF0) { - char_len = 4; - } - - if (i + char_len <= unicode_text.length()) { - chars.push_back(unicode_text.substr(i, char_len)); - } - i += char_len; - } - - return chars; -} - -std::vector BPETokenizer::utf8_split(const std::string& text) const { - std::vector chars; - size_t i = 0; - while (i < text.length()) { - size_t char_len = 1; - const unsigned char byte = static_cast(text[i]); - - if ((byte & 0x80) == 0) { - char_len = 1; - } else if ((byte & 0xE0) == 0xC0) { - char_len = 2; - } else if ((byte & 0xF0) == 0xE0) { - char_len = 3; - } else if ((byte & 0xF8) == 0xF0) { - char_len = 4; - } - - if (i + char_len <= text.length()) { - chars.push_back(text.substr(i, char_len)); - } - i += char_len; - } - - return chars; -} - - -std::pair BPETokenizer::find_best_merge_fast(const std::vector& tokens) const { - int best_pos = -1; - uint32_t best_priority = UINT32_MAX; - - for (size_t i = 0; i < tokens.size() - 1; ++i) { - std::string key = tokens[i] + "\x00" + tokens[i + 1]; - auto it = merge_map_.find(key); - if (it != merge_map_.end()) { - if (it->second < best_priority) { - best_priority = it->second; - best_pos = static_cast(i); - } - } - } - - return {best_pos, best_priority}; -} - -std::vector BPETokenizer::apply_bpe(const std::vector& tokens) const { - if (tokens.size() <= 1) return tokens; - - std::vector current_tokens = tokens; - - - while (true) { - auto [merge_pos, priority] = find_best_merge_fast(current_tokens); - if (merge_pos == -1) break; - - - std::vector new_tokens; - new_tokens.reserve(current_tokens.size() - 1); - - for (int i = 0; i < static_cast(current_tokens.size()); ++i) { - if (i == merge_pos) { - std::string merged = current_tokens[i] + current_tokens[i + 1]; - new_tokens.push_back(merged); - i++; - } else { - new_tokens.push_back(current_tokens[i]); - } - } - current_tokens = std::move(new_tokens); - } - - return current_tokens; -} - -std::vector BPETokenizer::encode(const std::string& text) const { - if (text.empty()) return {}; - - - auto text_segments = split_with_special_tokens(text); - - - std::vector token_ids; - - for (const auto& segment : text_segments) { - auto special_it = special_tokens_.find(segment); - if (special_it != special_tokens_.end()) { - token_ids.push_back(special_it->second); - } else { - std::string normalized_segment = segment; - std::vector chars; - if (runtime_config_.normalizer == TokenizerRuntimeConfig::Normalizer::METASPACE) { - size_t pos = 0; - while ((pos = normalized_segment.find(' ', pos)) != std::string::npos) { - normalized_segment.replace(pos, 1, kMetaspace); - pos += 3; - } - chars = utf8_split(normalized_segment); - } else { - chars = byte_level_split(segment); - } - auto bpe_tokens = apply_bpe(chars); - - for (const auto& token : bpe_tokens) { - auto it = token_to_id_.find(token); - if (it != token_to_id_.end()) { - token_ids.push_back(it->second); - } else { - if (!runtime_config_.byte_fallback) { - token_ids.push_back(unk_token_id_); - continue; - } - - std::vector fallback_ids; - fallback_ids.reserve(token.size()); - for (unsigned char byte : token) { - char fallback_token[7]; - std::snprintf(fallback_token, sizeof(fallback_token), "<0x%02X>", byte); - auto fallback_it = token_to_id_.find(fallback_token); - if (fallback_it == token_to_id_.end()) { - fallback_ids.clear(); - break; - } - fallback_ids.push_back(fallback_it->second); - } - - if (fallback_ids.empty()) { - token_ids.push_back(unk_token_id_); - } else { - token_ids.insert(token_ids.end(), fallback_ids.begin(), fallback_ids.end()); - } - } - } - } - } - - return token_ids; -} - -std::string BPETokenizer::decode(const std::vector& tokens) const { - if (runtime_config_.decoder == TokenizerRuntimeConfig::Decoder::REPLACE_METASPACE) { - std::string result; - result.reserve(tokens.size() * 4); - - for (uint32_t token_id : tokens) { - if (token_id >= id_to_token_.size()) continue; - result += id_to_token_[token_id]; - } - - size_t pos = 0; - while ((pos = result.find(kMetaspace, pos)) != std::string::npos) { - result.replace(pos, 3, " "); - pos += 1; - } - return result; - } - - std::string unicode_result; - unicode_result.reserve(tokens.size() * 4); - - for (uint32_t token_id : tokens) { - if (token_id >= id_to_token_.size()) continue; - const std::string& tok = id_to_token_[token_id]; - - size_t pos = 0; - while (pos < tok.size()) { - if (pos + 3 <= tok.size() && - (unsigned char)tok[pos] == 0xE2 && - (unsigned char)tok[pos+1] == 0x96 && - (unsigned char)tok[pos+2] == 0x81) { - unicode_result.push_back(' '); - pos += 3; - } else { - unicode_result.push_back(tok[pos++]); - } - } - } - - return unicode_to_bytes(unicode_result); -} - - -} -} diff --git a/cactus/engine/engine_cache.cpp b/cactus/engine/engine_cache.cpp deleted file mode 100644 index 77a7381b4..000000000 --- a/cactus/engine/engine_cache.cpp +++ /dev/null @@ -1,691 +0,0 @@ -#include "engine.h" -#include "../graph/graph.h" -#include "../kernel/kernel.h" -#include -#include -#include - -namespace cactus { -namespace engine { - -void KVCache::init(size_t layers, size_t max_seq, const std::vector& layer_dims, const std::vector& layer_kv_heads, Precision model_precision) { - num_layers = layers; - max_seq_len = max_seq; - precision = model_precision; - element_size = PrecisionTraits::size_of(precision); - - layer_caches.resize(num_layers); - for (size_t i = 0; i < num_layers; i++) { - layer_caches[i].head_dim = layer_dims[i]; - layer_caches[i].kv_heads = layer_kv_heads[i]; - } - - current_seq_len = 0; - total_seq_len = 0; -} - -void KVCache::set_window_size(size_t window, size_t sink) { - window_size = window; - sink_size = sink; - - if (window_size > 0) { - for (size_t layer_idx = 0; layer_idx < num_layers; layer_idx++) { - size_t head_dim = get_layer_head_dim(layer_idx); - if (head_dim == 0) continue; - size_t layer_kv = get_layer_kv_heads(layer_idx); - size_t cache_bytes = window_size * layer_kv * head_dim * element_size; - size_t num_groups = (head_dim + KV_QUANT_GROUP_SIZE - 1) / KV_QUANT_GROUP_SIZE; - size_t num_scales = window_size * layer_kv * num_groups; - auto& cache = layer_caches[layer_idx]; - - cache.keys.resize(cache_bytes); - cache.values.resize(cache_bytes); - std::memset(cache.keys.data(), 0, cache_bytes); - std::memset(cache.values.data(), 0, cache_bytes); - - cache.key_scales.resize(num_scales); - cache.value_scales.resize(num_scales); - std::fill(cache.key_scales.begin(), cache.key_scales.end(), 1.0f); - std::fill(cache.value_scales.begin(), cache.value_scales.end(), 1.0f); - } - } -} - -void KVCache::reset() { - current_seq_len = 0; - total_seq_len = 0; -} - -void* KVCache::get_key_ptr(size_t layer) { - if (current_seq_len == 0 || layer >= num_layers) return nullptr; - return layer_caches[layer].keys.data(); -} - -void* KVCache::get_value_ptr(size_t layer) { - if (current_seq_len == 0 || layer >= num_layers) return nullptr; - return layer_caches[layer].values.data(); -} - -KVCache::CircularView KVCache::get_key_view(size_t layer) { - CircularView view; - if (layer >= num_layers || current_seq_len == 0) { - view.ptr1 = nullptr; - view.ptr2 = nullptr; - view.len1 = 0; - view.len2 = 0; - view.total_len = 0; - return view; - } - - view.ptr1 = layer_caches[layer].keys.data(); - view.ptr2 = nullptr; - view.len1 = current_seq_len; - view.len2 = 0; - view.total_len = current_seq_len; - return view; -} - -KVCache::CircularView KVCache::get_value_view(size_t layer) { - CircularView view; - if (layer >= num_layers || current_seq_len == 0) { - view.ptr1 = nullptr; - view.ptr2 = nullptr; - view.len1 = 0; - view.len2 = 0; - view.total_len = 0; - return view; - } - - view.ptr1 = layer_caches[layer].values.data(); - view.ptr2 = nullptr; - view.len1 = current_seq_len; - view.len2 = 0; - view.total_len = current_seq_len; - return view; -} - -void KVCache::update_from_graph(CactusGraph* gb, const std::vector& k_nodes, - const std::vector& v_nodes, size_t seq_len, - size_t layers) { - size_t old_seq_len = current_seq_len; - size_t new_total_len = old_seq_len + seq_len; - - total_seq_len += seq_len; - - size_t effective_seq_len; - bool use_sliding_window = (window_size > 0 && new_total_len > window_size); - - if (use_sliding_window) { - effective_seq_len = window_size; - } else { - effective_seq_len = new_total_len; - } - - bool any_layer_updated = false; - - for (size_t layer_idx = 0; layer_idx < layers; layer_idx++) { - if (k_nodes[layer_idx] == 0 || v_nodes[layer_idx] == 0) - continue; - - auto& cache = layer_caches[layer_idx]; - - void* k_output = gb->get_output(k_nodes[layer_idx]); - void* v_output = gb->get_output(v_nodes[layer_idx]); - - if (k_output && v_output) { - const auto& k_buffer = gb->get_output_buffer(k_nodes[layer_idx]); - const auto& v_buffer = gb->get_output_buffer(v_nodes[layer_idx]); - - size_t dim = get_layer_head_dim(layer_idx); - size_t kv_heads = get_layer_kv_heads(layer_idx); - size_t elements_per_token = kv_heads * dim; - size_t num_groups = (dim + KV_QUANT_GROUP_SIZE - 1) / KV_QUANT_GROUP_SIZE; - size_t scales_per_token = kv_heads * num_groups; - size_t bytes_per_token = elements_per_token * element_size; - - size_t expected_elements = new_total_len * elements_per_token; - - if (k_buffer.total_size == expected_elements && v_buffer.total_size == expected_elements) { - any_layer_updated = true; - - if (!use_sliding_window) { - size_t total_bytes = new_total_len * bytes_per_token; - cache.keys.resize(total_bytes); - cache.values.resize(total_bytes); - - if (precision == Precision::INT8) { - size_t num_scales = new_total_len * scales_per_token; - cache.key_scales.resize(num_scales); - cache.value_scales.resize(num_scales); - - cactus_quantize_kv_fp16_to_int8( - static_cast(k_output), - reinterpret_cast(cache.keys.data()), - cache.key_scales.data(), - new_total_len, kv_heads, dim); - - cactus_quantize_kv_fp16_to_int8( - static_cast(v_output), - reinterpret_cast(cache.values.data()), - cache.value_scales.data(), - new_total_len, kv_heads, dim); - } else { - std::memcpy(cache.keys.data(), k_output, total_bytes); - std::memcpy(cache.values.data(), v_output, total_bytes); - } - } else { - size_t cache_bytes = window_size * bytes_per_token; - size_t remaining_window = window_size - sink_size; - size_t skip_tokens = new_total_len - window_size; - - bool first_slide = (cache.keys.size() != cache_bytes); - - if (first_slide) { - cache.keys.resize(cache_bytes); - cache.values.resize(cache_bytes); - } - - if (precision == Precision::INT8) { - size_t num_scales = window_size * scales_per_token; - if (first_slide) { - cache.key_scales.resize(num_scales); - cache.value_scales.resize(num_scales); - } - - const __fp16* k_fp16 = static_cast(k_output); - const __fp16* v_fp16 = static_cast(v_output); - - if (first_slide) { - cactus_quantize_kv_fp16_to_int8( - k_fp16, - reinterpret_cast(cache.keys.data()), - cache.key_scales.data(), - sink_size, kv_heads, dim); - - cactus_quantize_kv_fp16_to_int8( - v_fp16, - reinterpret_cast(cache.values.data()), - cache.value_scales.data(), - sink_size, kv_heads, dim); - } - - size_t src_offset = (sink_size + skip_tokens) * elements_per_token; - size_t dst_offset = sink_size * elements_per_token; - size_t scale_dst_offset = sink_size * scales_per_token; - - cactus_quantize_kv_fp16_to_int8( - k_fp16 + src_offset, - reinterpret_cast(cache.keys.data()) + dst_offset, - cache.key_scales.data() + scale_dst_offset, - remaining_window, kv_heads, dim); - - cactus_quantize_kv_fp16_to_int8( - v_fp16 + src_offset, - reinterpret_cast(cache.values.data()) + dst_offset, - cache.value_scales.data() + scale_dst_offset, - remaining_window, kv_heads, dim); - } else { - if (first_slide) { - size_t sink_bytes = sink_size * bytes_per_token; - std::memcpy(cache.keys.data(), k_output, sink_bytes); - std::memcpy(cache.values.data(), v_output, sink_bytes); - } - - const uint8_t* k_src = static_cast(k_output) + - (sink_size + skip_tokens) * bytes_per_token; - const uint8_t* v_src = static_cast(v_output) + - (sink_size + skip_tokens) * bytes_per_token; - size_t sink_bytes = sink_size * bytes_per_token; - size_t recent_bytes = remaining_window * bytes_per_token; - - std::memcpy(cache.keys.data() + sink_bytes, k_src, recent_bytes); - std::memcpy(cache.values.data() + sink_bytes, v_src, recent_bytes); - } - } - } - else if (seq_len * elements_per_token == k_buffer.total_size && - seq_len * elements_per_token == v_buffer.total_size) { - any_layer_updated = true; - - if (!use_sliding_window) { - size_t old_bytes = old_seq_len * bytes_per_token; - size_t new_bytes = seq_len * bytes_per_token; - size_t total_bytes = old_bytes + new_bytes; - cache.keys.resize(total_bytes); - cache.values.resize(total_bytes); - - if (precision == Precision::INT8) { - size_t num_scales = (old_seq_len + seq_len) * scales_per_token; - cache.key_scales.resize(num_scales); - cache.value_scales.resize(num_scales); - - size_t dst_offset = old_seq_len * elements_per_token; - size_t scale_offset = old_seq_len * scales_per_token; - - cactus_quantize_kv_fp16_to_int8( - static_cast(k_output), - reinterpret_cast(cache.keys.data()) + dst_offset, - cache.key_scales.data() + scale_offset, - seq_len, kv_heads, dim); - - cactus_quantize_kv_fp16_to_int8( - static_cast(v_output), - reinterpret_cast(cache.values.data()) + dst_offset, - cache.value_scales.data() + scale_offset, - seq_len, kv_heads, dim); - } else { - std::memcpy(cache.keys.data() + old_bytes, k_output, new_bytes); - std::memcpy(cache.values.data() + old_bytes, v_output, new_bytes); - } - } else { - size_t cache_bytes = window_size * bytes_per_token; - - if (cache.keys.size() != cache_bytes) { - cache.keys.resize(cache_bytes); - cache.values.resize(cache_bytes); - } - - size_t tokens_to_shift = window_size - sink_size - seq_len; - size_t sink_bytes = sink_size * bytes_per_token; - - if (tokens_to_shift > 0 && old_seq_len > sink_size) { - size_t shift_src = old_seq_len - tokens_to_shift; - if (shift_src > sink_size) { - size_t shift_bytes = tokens_to_shift * bytes_per_token; - - std::memmove(cache.keys.data() + sink_bytes, - cache.keys.data() + shift_src * bytes_per_token, - shift_bytes); - std::memmove(cache.values.data() + sink_bytes, - cache.values.data() + shift_src * bytes_per_token, - shift_bytes); - - if (precision == Precision::INT8) { - size_t sink_scale_offset = sink_size * scales_per_token; - size_t shift_src_scale_offset = shift_src * scales_per_token; - size_t shift_scale_count = tokens_to_shift * scales_per_token; - - std::memmove(cache.key_scales.data() + sink_scale_offset, - cache.key_scales.data() + shift_src_scale_offset, - shift_scale_count * sizeof(float)); - std::memmove(cache.value_scales.data() + sink_scale_offset, - cache.value_scales.data() + shift_src_scale_offset, - shift_scale_count * sizeof(float)); - } - } - } - - size_t append_token_offset = window_size - seq_len; - size_t append_bytes_offset = append_token_offset * bytes_per_token; - size_t new_bytes = seq_len * bytes_per_token; - - if (precision == Precision::INT8) { - size_t append_offset = append_token_offset * elements_per_token; - size_t scale_offset = append_token_offset * scales_per_token; - - cactus_quantize_kv_fp16_to_int8( - static_cast(k_output), - reinterpret_cast(cache.keys.data()) + append_offset, - cache.key_scales.data() + scale_offset, - seq_len, kv_heads, dim); - - cactus_quantize_kv_fp16_to_int8( - static_cast(v_output), - reinterpret_cast(cache.values.data()) + append_offset, - cache.value_scales.data() + scale_offset, - seq_len, kv_heads, dim); - } else { - std::memcpy(cache.keys.data() + append_bytes_offset, k_output, new_bytes); - std::memcpy(cache.values.data() + append_bytes_offset, v_output, new_bytes); - } - } - } - } - } - - if (any_layer_updated) { - current_seq_len = effective_seq_len; - } -} - -void KVCache::update_from_npu(size_t layer_idx, const __fp16* k_data, const __fp16* v_data, - size_t num_tokens, size_t kv_heads, size_t dim) { - if (layer_idx >= num_layers || !k_data || !v_data || num_tokens == 0) { - return; - } - - auto& cache = layer_caches[layer_idx]; - size_t old_seq_len = current_seq_len; - size_t new_total_len = old_seq_len + num_tokens; - size_t elements_per_token = kv_heads * dim; - size_t bytes_per_token = elements_per_token * element_size; - size_t num_groups = (dim + KV_QUANT_GROUP_SIZE - 1) / KV_QUANT_GROUP_SIZE; - size_t scales_per_token = kv_heads * num_groups; - - if (layer_idx == 0) { - total_seq_len += num_tokens; - } - - bool use_sliding_window = (window_size > 0 && new_total_len > window_size); - size_t effective_seq_len = use_sliding_window ? window_size : new_total_len; - - if (!use_sliding_window) { - size_t total_bytes = new_total_len * bytes_per_token; - cache.keys.resize(total_bytes); - cache.values.resize(total_bytes); - - size_t num_scales = new_total_len * scales_per_token; - cache.key_scales.resize(num_scales); - cache.value_scales.resize(num_scales); - - size_t dst_offset = old_seq_len * elements_per_token; - size_t scale_offset = old_seq_len * scales_per_token; - - cactus_quantize_kv_fp16_to_int8( - k_data, - reinterpret_cast(cache.keys.data()) + dst_offset, - cache.key_scales.data() + scale_offset, - num_tokens, kv_heads, dim); - - cactus_quantize_kv_fp16_to_int8( - v_data, - reinterpret_cast(cache.values.data()) + dst_offset, - cache.value_scales.data() + scale_offset, - num_tokens, kv_heads, dim); - } else { - size_t cache_bytes = window_size * bytes_per_token; - - if (cache.keys.size() != cache_bytes) { - cache.keys.resize(cache_bytes); - cache.values.resize(cache_bytes); - - size_t num_scales = window_size * scales_per_token; - cache.key_scales.resize(num_scales); - cache.value_scales.resize(num_scales); - } - - size_t remaining_window = window_size - sink_size; - - if (num_tokens >= remaining_window) { - size_t skip_tokens = num_tokens - remaining_window; - size_t dst_offset = sink_size * elements_per_token; - size_t scale_offset = sink_size * scales_per_token; - - cactus_quantize_kv_fp16_to_int8( - k_data + skip_tokens * elements_per_token, - reinterpret_cast(cache.keys.data()) + dst_offset, - cache.key_scales.data() + scale_offset, - remaining_window, kv_heads, dim); - - cactus_quantize_kv_fp16_to_int8( - v_data + skip_tokens * elements_per_token, - reinterpret_cast(cache.values.data()) + dst_offset, - cache.value_scales.data() + scale_offset, - remaining_window, kv_heads, dim); - } else { - size_t tokens_to_shift = remaining_window - num_tokens; - - if (tokens_to_shift > 0 && old_seq_len > sink_size) { - size_t shift_src = old_seq_len - tokens_to_shift; - if (shift_src > sink_size) { - size_t sink_offset = sink_size * elements_per_token; - size_t shift_src_offset = shift_src * elements_per_token; - size_t shift_bytes = tokens_to_shift * bytes_per_token; - - std::memmove(cache.keys.data() + sink_offset, - cache.keys.data() + shift_src_offset, - shift_bytes); - std::memmove(cache.values.data() + sink_offset, - cache.values.data() + shift_src_offset, - shift_bytes); - - size_t sink_scale_offset = sink_size * scales_per_token; - size_t shift_src_scale_offset = shift_src * scales_per_token; - size_t shift_scale_count = tokens_to_shift * scales_per_token; - - std::memmove(cache.key_scales.data() + sink_scale_offset, - cache.key_scales.data() + shift_src_scale_offset, - shift_scale_count * sizeof(float)); - std::memmove(cache.value_scales.data() + sink_scale_offset, - cache.value_scales.data() + shift_src_scale_offset, - shift_scale_count * sizeof(float)); - } - } - - size_t append_token_offset = window_size - num_tokens; - size_t append_offset = append_token_offset * elements_per_token; - size_t scale_offset = append_token_offset * scales_per_token; - - cactus_quantize_kv_fp16_to_int8( - k_data, - reinterpret_cast(cache.keys.data()) + append_offset, - cache.key_scales.data() + scale_offset, - num_tokens, kv_heads, dim); - - cactus_quantize_kv_fp16_to_int8( - v_data, - reinterpret_cast(cache.values.data()) + append_offset, - cache.value_scales.data() + scale_offset, - num_tokens, kv_heads, dim); - } - } - - if (layer_idx == num_layers - 1) { - current_seq_len = effective_seq_len; - } -} - -void KVCache::remove_token_range(size_t start, size_t count) { - if (count == 0 || start >= current_seq_len || start + count > current_seq_len) return; - - size_t tail_tokens = current_seq_len - start - count; - - auto erase_bytes = [](std::vector& buf, size_t offset, size_t remove, size_t tail) { - std::memmove(buf.data() + offset, buf.data() + offset + remove, tail); - buf.resize(buf.size() - remove); - }; - - auto erase_floats = [](std::vector& buf, size_t offset, size_t remove, size_t tail) { - std::memmove(buf.data() + offset, buf.data() + offset + remove, tail * sizeof(float)); - buf.resize(buf.size() - remove); - }; - - for (size_t i = 0; i < layer_caches.size(); i++) { - size_t dim = get_layer_head_dim(i); - if (dim == 0) continue; - auto& layer = layer_caches[i]; - size_t num_kv_heads = get_layer_kv_heads(i); - size_t bytes_per_tok = num_kv_heads * dim * element_size; - - erase_bytes(layer.keys, start * bytes_per_tok, count * bytes_per_tok, tail_tokens * bytes_per_tok); - erase_bytes(layer.values, start * bytes_per_tok, count * bytes_per_tok, tail_tokens * bytes_per_tok); - - if (precision == Precision::INT8 && !layer.key_scales.empty()) { - size_t scaler_per_tok = num_kv_heads * dim / KV_QUANT_GROUP_SIZE; - erase_floats(layer.key_scales, start * scaler_per_tok, count * scaler_per_tok, tail_tokens * scaler_per_tok); - erase_floats(layer.value_scales, start * scaler_per_tok, count * scaler_per_tok, tail_tokens * scaler_per_tok); - } - } - - current_seq_len -= count; - total_seq_len -= count; -} - -void KVCache::compact_to_windows(const std::vector& target_windows) { - for (size_t i = 0; i < layer_caches.size(); i++) { - size_t dim = get_layer_head_dim(i); - if (dim == 0) continue; - size_t target = (i < target_windows.size()) ? target_windows[i] : 0; - if (target == 0) continue; - - auto& layer = layer_caches[i]; - size_t num_kv_heads = get_layer_kv_heads(i); - size_t layer_tokens = layer.keys.size() / (num_kv_heads * dim * element_size); - if (layer_tokens <= target) continue; - - size_t bytes_per_token = num_kv_heads * dim * element_size; - size_t keep_bytes = target * bytes_per_token; - size_t discard_bytes = (layer_tokens - target) * bytes_per_token; - - std::memmove(layer.keys.data(), layer.keys.data() + discard_bytes, keep_bytes); - layer.keys.resize(keep_bytes); - std::memmove(layer.values.data(), layer.values.data() + discard_bytes, keep_bytes); - layer.values.resize(keep_bytes); - - if (precision == Precision::INT8 && !layer.key_scales.empty()) { - size_t num_groups = (dim + KV_QUANT_GROUP_SIZE - 1) / KV_QUANT_GROUP_SIZE; - size_t scales_per_token = num_kv_heads * num_groups; - size_t keep_scales = target * scales_per_token; - size_t discard_scales = (layer_tokens - target) * scales_per_token; - - std::memmove(layer.key_scales.data(), layer.key_scales.data() + discard_scales, keep_scales * sizeof(float)); - layer.key_scales.resize(keep_scales); - std::memmove(layer.value_scales.data(), layer.value_scales.data() + discard_scales, keep_scales * sizeof(float)); - layer.value_scales.resize(keep_scales); - } - } -} - -const int8_t* KVCache::get_keys_int8(size_t layer) const { - if (layer >= num_layers || current_seq_len == 0) { - return nullptr; - } - return reinterpret_cast(layer_caches[layer].keys.data()); -} - -const int8_t* KVCache::get_values_int8(size_t layer) const { - if (layer >= num_layers || current_seq_len == 0) { - return nullptr; - } - return reinterpret_cast(layer_caches[layer].values.data()); -} - -const float* KVCache::get_key_scales(size_t layer) const { - if (layer >= num_layers || current_seq_len == 0) { - return nullptr; - } - return layer_caches[layer].key_scales.data(); -} - -const float* KVCache::get_value_scales(size_t layer) const { - if (layer >= num_layers || current_seq_len == 0) { - return nullptr; - } - return layer_caches[layer].value_scales.data(); -} - -void ConvCache::init(size_t layers, size_t hidden_dim, size_t window_len, Precision model_precision) { - num_layers = layers; - hidden_size = hidden_dim; - window_size = window_len; - precision = model_precision; - element_size = PrecisionTraits::size_of(precision); - - size_t state_bytes = window_size * hidden_size * element_size; - layer_states.resize(num_layers); - for (auto& state : layer_states) { - state.data.resize(state_bytes); - std::memset(state.data.data(), 0, state_bytes); - state.head = 0; - state.count = 0; - } -} - -ConvCache::CircularView ConvCache::get_window(size_t layer) const { - CircularView view; - if (layer >= num_layers) { - view.ptr1 = nullptr; - view.len1 = 0; - view.ptr2 = nullptr; - view.len2 = 0; - view.total_len = 0; - return view; - } - - const auto& state = layer_states[layer]; - if (state.count == 0) { - view.ptr1 = nullptr; - view.len1 = 0; - view.ptr2 = nullptr; - view.len2 = 0; - view.total_len = 0; - return view; - } - - size_t stride = hidden_size * element_size; - - if (state.count < window_size) { - view.ptr1 = state.data.data(); - view.len1 = state.count; - view.ptr2 = nullptr; - view.len2 = 0; - view.total_len = state.count; - return view; - } - - view.ptr1 = state.data.data(); - view.len1 = state.head; - view.ptr2 = state.data.data() + state.head * stride; - view.len2 = window_size - state.head; - view.total_len = window_size; - return view; -} - -void ConvCache::update(CactusGraph* gb, size_t layer, const size_t bx_node) { - if (layer >= num_layers || !bx_node || window_size == 0 || hidden_size == 0) { - return; - } - - auto& state = layer_states[layer]; - const void* output_ptr = gb->get_output(bx_node); - if (!output_ptr) { - return; - } - - const auto& buffer = gb->get_output_buffer(bx_node); - const size_t stride_bytes = hidden_size * element_size; - - size_t rows = 1; - if (!buffer.shape.empty()) { - if (buffer.shape.size() == 1) { - rows = 1; - } else { - rows = buffer.shape[0]; - } - } - - if (buffer.total_size > 0 && hidden_size > 0) { - size_t inferred = buffer.total_size / hidden_size; - if (inferred > 0) { - rows = inferred; - } - } - - if (rows == 0) { - return; - } - - size_t copy_rows = std::min(rows, window_size); - size_t start_row = rows > window_size ? rows - window_size : 0; - - const uint8_t* src = static_cast(output_ptr) + start_row * stride_bytes; - - for (size_t i = 0; i < copy_rows; ++i) { - std::memcpy(state.data.data() + state.head * stride_bytes, src + i * stride_bytes, stride_bytes); - state.head = (state.head + 1) % window_size; - if (state.count < window_size) { - ++state.count; - } - } -} - -void ConvCache::reset() { - for (auto& state : layer_states) { - std::fill(state.data.begin(), state.data.end(), 0); - state.head = 0; - state.count = 0; - } -} - -} -} \ No newline at end of file diff --git a/cactus/engine/engine_constraints.cpp b/cactus/engine/engine_constraints.cpp deleted file mode 100644 index cb03cef2f..000000000 --- a/cactus/engine/engine_constraints.cpp +++ /dev/null @@ -1,1120 +0,0 @@ -#include "engine.h" - -namespace cactus { -namespace engine { - -constexpr float FORCE_BIAS = 500.0f; -constexpr float BLOCK_BIAS = -500.0f; -constexpr float NEEDLE_BLOCK_BIAS = -1e9f; - -void ToolCallConstrainer::add_tokens_for_string(const std::string& str, std::unordered_set& token_set) { - if (!tokenizer_) return; - auto tokens = tokenizer_->encode(str); - for (uint32_t t : tokens) { - token_set.insert(t); - } -} - -void ToolCallConstrainer::add_tokens_for_prefix_string(const std::string& prefix, std::unordered_set& token_set) { - if (!tokenizer_) return; - const uint32_t vocab = tokenizer_->get_vocab_size(); - for (uint32_t t = 0; t < vocab; t++) { - if (tokenizer_->decode({t}).rfind(prefix, 0) == 0) { - token_set.insert(t); - } - } -} - -void ToolCallConstrainer::tokenize_function_names(bool quote_names) { - all_func_name_tokens_.clear(); - func_name_sequences_.clear(); - - for (const auto& name : function_names_) { - std::string name_to_encode = quote_names ? ("\"" + name + "\"") : name; - auto tokens = tokenizer_->encode(name_to_encode); - func_name_sequences_[name] = tokens; - for (uint32_t t : tokens) { - all_func_name_tokens_.insert(t); - } - if (quote_names) { - auto unquoted_tokens = tokenizer_->encode(name); - for (uint32_t t : unquoted_tokens) { - all_func_name_tokens_.insert(t); - } - } - } -} - -void ToolCallConstrainer::init_common_tokens() { - backtick_tokens_.clear(); - add_tokens_for_string("`", backtick_tokens_); - add_tokens_for_string("``", backtick_tokens_); - add_tokens_for_string("```", backtick_tokens_); - add_tokens_for_string("````", backtick_tokens_); - add_tokens_for_string("```json", backtick_tokens_); - add_tokens_for_string("```JSON", backtick_tokens_); - add_tokens_for_string("``` json", backtick_tokens_); - add_tokens_for_string("```\n", backtick_tokens_); - add_tokens_for_string("` ", backtick_tokens_); -} - -void ToolCallConstrainer::needle_insert_word(NeedleTrieNode* root, const std::string& word) { - if (!root) return; - - NeedleTrieNode* node = root; - for (char ch : word) { - auto& child = node->children[ch]; - if (!child) { - child = std::make_unique(); - } - node = child.get(); - } - node->is_terminal = true; -} - -const ToolCallConstrainer::NeedleTrieNode* ToolCallConstrainer::needle_get_trie_node( - const NeedleTrieNode* root, - const std::string& prefix -) const { - const NeedleTrieNode* node = root; - for (char ch : prefix) { - if (!node) return nullptr; - auto it = node->children.find(ch); - if (it == node->children.end()) { - return nullptr; - } - node = it->second.get(); - } - return node; -} - -bool ToolCallConstrainer::needle_check_token_valid(const std::string& token_text, - const NeedleTrieNode* trie_node) const { - const NeedleTrieNode* node = trie_node; - if (!node) return false; - - for (char ch : token_text) { - if (ch == '"') { - return node->is_terminal; - } - auto it = node->children.find(ch); - if (it == node->children.end()) { - return false; - } - node = it->second.get(); - } - return true; -} - -void ToolCallConstrainer::reset_needle_constraints() { - needle_json_state_ = NeedleJsonState::FREE; - needle_buffer_.clear(); - needle_constrained_buf_.clear(); - needle_current_function_.clear(); - needle_in_arguments_ = false; - needle_arguments_depth_ = 0; - needle_nesting_depth_ = 0; - needle_in_string_value_ = false; - needle_prev_char_escape_ = false; -} - -bool ToolCallConstrainer::needle_at_arg_key_start() const { - if (needle_buffer_.size() < 2) return false; - return needle_buffer_.compare(needle_buffer_.size() - 2, 2, "{\"") == 0 || - needle_buffer_.compare(needle_buffer_.size() - 2, 2, ",\"") == 0; -} - -bool ToolCallConstrainer::needle_is_value_string_start() const { - if (needle_buffer_.empty()) return false; - for (size_t i = needle_buffer_.size() - 1; i-- > 0;) { - char ch = needle_buffer_[i]; - if (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') { - continue; - } - return ch == ':'; - } - return false; -} - -void ToolCallConstrainer::feed_needle_char(char ch) { - if (needle_json_state_ == NeedleJsonState::IN_NAME || - needle_json_state_ == NeedleJsonState::IN_ARG_KEY) { - if (ch == '"') { - if (needle_json_state_ == NeedleJsonState::IN_NAME) { - needle_current_function_ = needle_constrained_buf_; - } - needle_constrained_buf_.clear(); - needle_json_state_ = NeedleJsonState::FREE; - } else { - needle_constrained_buf_.push_back(ch); - } - needle_buffer_.push_back(ch); - return; - } - - needle_buffer_.push_back(ch); - - if (needle_in_string_value_) { - if (needle_prev_char_escape_) { - needle_prev_char_escape_ = false; - return; - } - if (ch == '\\') { - needle_prev_char_escape_ = true; - return; - } - if (ch == '"') { - needle_in_string_value_ = false; - } - return; - } - - if (ch == '{' || ch == '[') { - needle_nesting_depth_++; - } else if (ch == '}' || ch == ']') { - needle_nesting_depth_ = std::max(0, needle_nesting_depth_ - 1); - if (ch == '}' && needle_in_arguments_ && needle_nesting_depth_ < needle_arguments_depth_) { - needle_in_arguments_ = false; - } - } - - if (needle_buffer_.size() >= 8 && - needle_buffer_.compare(needle_buffer_.size() - 8, 8, "\"name\":\"") == 0) { - needle_json_state_ = NeedleJsonState::IN_NAME; - needle_constrained_buf_.clear(); - return; - } - - if (needle_buffer_.size() >= 13 && - needle_buffer_.compare(needle_buffer_.size() - 13, 13, "\"arguments\":{") == 0) { - needle_in_arguments_ = true; - needle_arguments_depth_ = needle_nesting_depth_; - return; - } - - if (needle_in_arguments_ && - needle_nesting_depth_ == needle_arguments_depth_ && - needle_at_arg_key_start()) { - needle_json_state_ = NeedleJsonState::IN_ARG_KEY; - needle_constrained_buf_.clear(); - return; - } - - if (ch == '"' && needle_is_value_string_start()) { - needle_in_string_value_ = true; - } -} - -void ToolCallConstrainer::feed_needle_text(const std::string& text) { - for (char ch : text) { - feed_needle_char(ch); - } -} - -void ToolCallConstrainer::init_needle_constraints() { - reset_needle_constraints(); - - needle_name_trie_ = std::make_unique(); - needle_param_tries_.clear(); - for (const auto& tool : tool_specs_) { - if (!tool.name.empty()) { - needle_insert_word(needle_name_trie_.get(), tool.name); - } - - auto param_root = std::make_unique(); - for (const auto& param_name : tool.parameter_names) { - if (!param_name.empty()) { - needle_insert_word(param_root.get(), param_name); - } - } - needle_param_tries_[tool.name] = std::move(param_root); - } - - const uint32_t vocab_size = tokenizer_ ? tokenizer_->get_vocab_size() : 0; - if (needle_token_strings_.size() != vocab_size) { - needle_token_strings_.assign(vocab_size, ""); - needle_token_index_.clear(); - - const uint32_t eos = tokenizer_->get_eos_token(); - const uint32_t bos = tokenizer_->get_bos_token(); - const uint32_t unk = tokenizer_->get_unk_token(); - constexpr uint32_t pad = 0; - - for (uint32_t token_id = 0; token_id < vocab_size; ++token_id) { - if (token_id == pad || token_id == eos || token_id == bos || token_id == unk) { - continue; - } - std::string decoded = tokenizer_->decode({token_id}); - needle_token_strings_[token_id] = decoded; - if (decoded.empty()) continue; - needle_token_index_[decoded.front()].push_back(token_id); - } - } -} - -void ToolCallConstrainer::tokenize_grammar_elements() { - if (!tokenizer_) return; - - // Clear all token sets - open_brace_tokens_.clear(); - close_brace_tokens_.clear(); - colon_tokens_.clear(); - comma_tokens_.clear(); - name_key_tokens_.clear(); - args_key_tokens_.clear(); - quote_tokens_.clear(); - tool_start_tokens_.clear(); - tool_end_tokens_.clear(); - bracket_open_tokens_.clear(); - bracket_close_tokens_.clear(); - paren_open_tokens_.clear(); - paren_close_tokens_.clear(); - equals_tokens_.clear(); - - init_common_tokens(); - - if (model_type_ == Config::ModelType::LFM2) { - add_tokens_for_string("<|tool_call_start|>", tool_start_tokens_); - add_tokens_for_string("<|tool_call_end|>", tool_end_tokens_); - add_tokens_for_string("[", bracket_open_tokens_); - add_tokens_for_string("]", bracket_close_tokens_); - add_tokens_for_string("(", paren_open_tokens_); - add_tokens_for_string(")", paren_close_tokens_); - add_tokens_for_string("=", equals_tokens_); - add_tokens_for_string(",", comma_tokens_); - add_tokens_for_string("\"", quote_tokens_); - - tokenize_function_names(false); - add_tokens_for_prefix_string("(", paren_open_tokens_); - } else if (is_gemma_family()) { - gemma_call_start_tokens_.clear(); - gemma_call_end_tokens_.clear(); - gemma_call_prefix_tokens_.clear(); - escape_tokens_.clear(); - gemma_response_start_tokens_.clear(); - - add_tokens_for_string(call_start_tag_, gemma_call_start_tokens_); - add_tokens_for_string(call_end_tag_, gemma_call_end_tokens_); - if (model_type_ == Config::ModelType::GEMMA4) { - add_tokens_for_string("<|tool_response>", gemma_response_start_tokens_); - } else { - add_tokens_for_string("", gemma_response_start_tokens_); - add_tokens_for_string("", escape_tokens_); - } - add_tokens_for_string("call:", gemma_call_prefix_tokens_); - - add_tokens_for_string("{", open_brace_tokens_); - add_tokens_for_string("}", close_brace_tokens_); - add_tokens_for_string(":", colon_tokens_); - add_tokens_for_string(",", comma_tokens_); - - tokenize_function_names(false); - } else if (is_needle()) { - qwen_tool_call_start_tokens_.clear(); - add_tokens_for_string("", qwen_tool_call_start_tokens_); - } else { - qwen_tool_call_start_tokens_.clear(); - qwen_tool_call_end_tokens_.clear(); - - add_tokens_for_string("", qwen_tool_call_start_tokens_); - add_tokens_for_string("", qwen_tool_call_end_tokens_); - - add_tokens_for_string("{", open_brace_tokens_); - add_tokens_for_string("}", close_brace_tokens_); - add_tokens_for_string(":", colon_tokens_); - add_tokens_for_string(",", comma_tokens_); - add_tokens_for_string("\"", quote_tokens_); - - add_tokens_for_string("name", name_key_tokens_); - - add_tokens_for_string("arguments", args_key_tokens_); - - tokenize_function_names(true); - } -} - -void ToolCallConstrainer::init(Config::ModelType model_type, - const std::vector& tools, - Tokenizer* tokenizer) { - model_type_ = model_type; - tool_specs_ = tools; - function_names_.clear(); - function_names_.reserve(tool_specs_.size()); - for (const auto& tool : tool_specs_) { - function_names_.push_back(tool.name); - } - tokenizer_ = tokenizer; - generated_text_.clear(); - brace_depth_ = 0; - active_ = !function_names_.empty() && tokenizer != nullptr; - - if (model_type_ == Config::ModelType::LFM2) { - state_ = State::LFM_START; - lfm_current_function_.clear(); - lfm_args_buffer_.clear(); - lfm_seen_arg_keys_.clear(); - lfm_required_params_.clear(); - lfm_all_params_.clear(); - for (const auto& tool : tool_specs_) { - lfm_required_params_[tool.name] = tool.required_parameter_names; - lfm_all_params_[tool.name] = tool.parameter_names; - } - } else if (is_gemma_family()) { - state_ = State::GEMMA_START; - if (model_type_ == Config::ModelType::GEMMA4) { - call_start_tag_ = "<|tool_call>"; - call_end_tag_ = ""; - } else { - call_start_tag_ = ""; - call_end_tag_ = ""; - } - } else if (is_needle()) { - state_ = State::NEEDLE_START; - } else { - state_ = State::QWEN_START; - } - - if (!active_) { - return; - } - - tokenize_grammar_elements(); - if (is_needle()) { - init_needle_constraints(); - } - compute_bias(); -} - -void ToolCallConstrainer::update(uint32_t /*token_id*/, const std::string& decoded_text) { - if (!active_) return; - - generated_text_ += decoded_text; - - if (model_type_ == Config::ModelType::LFM2) { - switch (state_) { - case State::LFM_START: - if (generated_text_.find("<|tool_call_start|>") != std::string::npos) { - state_ = State::LFM_EXPECT_BRACKET; - generated_text_.clear(); - } - break; - - case State::LFM_EXPECT_BRACKET: - if (generated_text_.find("[") != std::string::npos) { - state_ = State::LFM_IN_FUNC_NAME; - generated_text_.clear(); - } - break; - - case State::LFM_IN_FUNC_NAME: - for (const auto& name : function_names_) { - if (generated_text_.find(name) != std::string::npos) { - lfm_current_function_ = name; - state_ = State::LFM_EXPECT_PAREN; - generated_text_.clear(); - break; - } - } - break; - - case State::LFM_EXPECT_PAREN: - if (generated_text_.find("(") != std::string::npos) { - state_ = State::LFM_IN_ARGUMENTS; - lfm_args_buffer_.clear(); - lfm_seen_arg_keys_.clear(); - generated_text_.clear(); - } - break; - - case State::LFM_IN_ARGUMENTS: { - lfm_args_buffer_ += decoded_text; - size_t eq_pos = 0; - while ((eq_pos = lfm_args_buffer_.find('=', eq_pos)) != std::string::npos) { - size_t key_end = eq_pos; - size_t key_start = key_end; - while (key_start > 0 && !std::isspace(static_cast(lfm_args_buffer_[key_start - 1])) && - lfm_args_buffer_[key_start - 1] != ',' && lfm_args_buffer_[key_start - 1] != '(') { - key_start--; - } - if (key_start < key_end) { - std::string key = lfm_args_buffer_.substr(key_start, key_end - key_start); - lfm_seen_arg_keys_.insert(key); - } - eq_pos++; - } - if (decoded_text.find(")") != std::string::npos) { - state_ = State::LFM_EXPECT_BRACKET_CLOSE; - generated_text_.clear(); - } else { - generated_text_.clear(); - } - break; - } - - case State::LFM_EXPECT_BRACKET_CLOSE: - if (generated_text_.find("]") != std::string::npos) { - state_ = State::LFM_EXPECT_END; - generated_text_.clear(); - } - break; - - case State::LFM_EXPECT_END: - if (generated_text_.find("<|tool_call_end|>") != std::string::npos) { - state_ = State::DONE; - generated_text_.clear(); - } - break; - - default: - break; - } - } else if (is_gemma_family()) { - switch (state_) { - case State::GEMMA_START: - if (generated_text_.find(call_start_tag_) != std::string::npos) { - state_ = State::GEMMA_EXPECT_CALL; - generated_text_.clear(); - } - break; - - case State::GEMMA_EXPECT_CALL: - if (generated_text_.find("call:") != std::string::npos) { - state_ = State::GEMMA_IN_FUNC_NAME; - generated_text_.clear(); - } - break; - - case State::GEMMA_IN_FUNC_NAME: - for (const auto& name : function_names_) { - if (generated_text_.find(name) != std::string::npos) { - state_ = State::GEMMA_EXPECT_BRACE; - generated_text_.clear(); - break; - } - } - break; - - case State::GEMMA_EXPECT_BRACE: - if (generated_text_.find("{") != std::string::npos) { - state_ = State::GEMMA_IN_ARGUMENTS; - brace_depth_ = 1; - generated_text_.clear(); - } - break; - - case State::GEMMA_IN_ARGUMENTS: - generated_text_.clear(); - for (char c : decoded_text) { - if (c == '{') brace_depth_++; - else if (c == '}') { - brace_depth_--; - if (brace_depth_ == 0) { - state_ = State::GEMMA_EXPECT_END; - break; - } - } - } - break; - - case State::GEMMA_EXPECT_END: - if (generated_text_.find(call_end_tag_) != std::string::npos) { - state_ = State::DONE; - generated_text_.clear(); - } - break; - - default: - break; - } - } else if (is_needle()) { - switch (state_) { - case State::NEEDLE_START: - if (generated_text_.find("") != std::string::npos) { - state_ = State::DONE; - generated_text_.clear(); - } - break; - - case State::DONE: - feed_needle_text(decoded_text); - generated_text_.clear(); - break; - - default: - break; - } - } else { - switch (state_) { - case State::QWEN_START: - if (generated_text_.find("") != std::string::npos) { - state_ = State::QWEN_EXPECT_OPEN_BRACE; - generated_text_.clear(); - } - break; - - case State::QWEN_EXPECT_OPEN_BRACE: { - size_t pos = generated_text_.find("{"); - if (pos != std::string::npos) { - state_ = State::QWEN_EXPECT_NAME_KEY; - generated_text_ = generated_text_.substr(pos + 1); - } - break; - } - - case State::QWEN_EXPECT_NAME_KEY: - if (generated_text_.find("name\"") != std::string::npos) { - state_ = State::QWEN_EXPECT_NAME_COLON; - generated_text_.clear(); - } - break; - - case State::QWEN_EXPECT_NAME_COLON: - if (generated_text_.find(":") != std::string::npos) { - state_ = State::QWEN_EXPECT_NAME_VALUE; - generated_text_.clear(); - } - break; - - case State::QWEN_EXPECT_NAME_VALUE: - for (const auto& name : function_names_) { - if (generated_text_.find(name + "\"") != std::string::npos) { - state_ = State::QWEN_EXPECT_COMMA; - generated_text_.clear(); - break; - } - } - break; - - case State::QWEN_EXPECT_COMMA: - if (generated_text_.find(",") != std::string::npos) { - state_ = State::QWEN_EXPECT_ARGS_KEY; - generated_text_.clear(); - } else if (generated_text_.find("}") != std::string::npos) { - state_ = State::QWEN_EXPECT_END; - generated_text_.clear(); - } - break; - - case State::QWEN_EXPECT_ARGS_KEY: - if (generated_text_.find("arguments\"") != std::string::npos) { - state_ = State::QWEN_EXPECT_ARGS_COLON; - generated_text_.clear(); - } - break; - - case State::QWEN_EXPECT_ARGS_COLON: - if (generated_text_.find(":") != std::string::npos) { - state_ = State::QWEN_IN_ARGUMENTS; - brace_depth_ = 0; - generated_text_.clear(); - } - break; - - case State::QWEN_IN_ARGUMENTS: - generated_text_.clear(); - for (char c : decoded_text) { - if (c == '{') brace_depth_++; - else if (c == '}') { - if (brace_depth_ > 0) { - brace_depth_--; - } else { - state_ = State::QWEN_EXPECT_CLOSE_BRACE; - generated_text_.clear(); - break; - } - } - } - break; - - case State::QWEN_EXPECT_CLOSE_BRACE: - if (generated_text_.find("}") != std::string::npos) { - state_ = State::QWEN_EXPECT_END; - generated_text_.clear(); - } - break; - - case State::QWEN_EXPECT_END: - if (generated_text_.find("") != std::string::npos) { - state_ = State::DONE; - generated_text_.clear(); - } - break; - - case State::DONE: - if (generated_text_.find("") != std::string::npos) { - state_ = State::QWEN_EXPECT_OPEN_BRACE; - generated_text_.clear(); - } - break; - - default: - break; - } - } - - compute_bias(); -} - -void ToolCallConstrainer::compute_bias() { - current_bias_.clear(); - - if (!active_) return; - - if (!is_needle()) { - for (uint32_t t : backtick_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - } - - if (model_type_ == Config::ModelType::LFM2) { - switch (state_) { - case State::LFM_START: - for (uint32_t t : tool_start_tokens_) { - current_bias_[t] = FORCE_BIAS; - } - for (uint32_t t : bracket_open_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - for (uint32_t t : paren_open_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - break; - - case State::LFM_EXPECT_BRACKET: - for (uint32_t t : bracket_open_tokens_) { - current_bias_[t] = FORCE_BIAS; - } - for (uint32_t t : paren_open_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - for (uint32_t t : bracket_close_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - break; - - case State::LFM_IN_FUNC_NAME: - for (uint32_t t : all_func_name_tokens_) { - current_bias_[t] = FORCE_BIAS; - } - for (uint32_t t : bracket_close_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - for (uint32_t t : paren_open_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - for (uint32_t t : paren_close_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - for (uint32_t t : equals_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - break; - - case State::LFM_EXPECT_PAREN: - for (uint32_t t : paren_open_tokens_) { - current_bias_[t] = FORCE_BIAS; - } - for (uint32_t t : bracket_close_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - for (uint32_t t : equals_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - break; - - case State::LFM_IN_ARGUMENTS: { - for (uint32_t t : equals_tokens_) { - current_bias_[t] = 10.0f; - } - for (uint32_t t : comma_tokens_) { - current_bias_[t] = 8.0f; - } - for (uint32_t t : quote_tokens_) { - current_bias_[t] = 5.0f; - } - for (uint32_t t : paren_close_tokens_) { - current_bias_[t] = 3.0f; - } - for (uint32_t t : bracket_close_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - for (uint32_t t : tool_end_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - auto req_it = lfm_required_params_.find(lfm_current_function_); - if (req_it != lfm_required_params_.end()) { - bool has_all_required = true; - for (const auto& req : req_it->second) { - if (!lfm_seen_arg_keys_.count(req)) { - has_all_required = false; - break; - } - } - if (!has_all_required) { - for (uint32_t t : paren_close_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - } - } - break; - } - - case State::LFM_EXPECT_BRACKET_CLOSE: - for (uint32_t t : bracket_close_tokens_) { - current_bias_[t] = FORCE_BIAS; - } - for (uint32_t t : paren_open_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - for (uint32_t t : equals_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - break; - - case State::LFM_EXPECT_END: - for (uint32_t t : tool_end_tokens_) { - current_bias_[t] = FORCE_BIAS; - } - for (uint32_t t : bracket_open_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - for (uint32_t t : paren_open_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - break; - - default: - break; - } - } else if (is_gemma_family()) { - for (uint32_t t : gemma_response_start_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - - switch (state_) { - case State::GEMMA_START: - for (uint32_t t : gemma_call_start_tokens_) { - current_bias_[t] = FORCE_BIAS; - } - for (uint32_t t : open_brace_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - for (uint32_t t : close_brace_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - break; - - case State::GEMMA_EXPECT_CALL: - for (uint32_t t : gemma_call_prefix_tokens_) { - current_bias_[t] = FORCE_BIAS; - } - for (uint32_t t : open_brace_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - for (uint32_t t : gemma_call_end_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - break; - - case State::GEMMA_IN_FUNC_NAME: - for (uint32_t t : all_func_name_tokens_) { - current_bias_[t] = FORCE_BIAS; - } - for (uint32_t t : close_brace_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - for (uint32_t t : gemma_call_end_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - break; - - case State::GEMMA_EXPECT_BRACE: - for (uint32_t t : open_brace_tokens_) { - current_bias_[t] = FORCE_BIAS; - } - for (uint32_t t : gemma_call_end_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - break; - - case State::GEMMA_IN_ARGUMENTS: - for (uint32_t t : colon_tokens_) { - current_bias_[t] = 10.0f; - } - for (uint32_t t : comma_tokens_) { - current_bias_[t] = 8.0f; - } - for (uint32_t t : escape_tokens_) { - current_bias_[t] = 5.0f; - } - for (uint32_t t : close_brace_tokens_) { - current_bias_[t] = 3.0f; - } - for (uint32_t t : open_brace_tokens_) { - current_bias_[t] = 3.0f; - } - for (uint32_t t : gemma_call_end_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - break; - - case State::GEMMA_EXPECT_END: - for (uint32_t t : gemma_call_end_tokens_) { - current_bias_[t] = FORCE_BIAS; - } - for (uint32_t t : open_brace_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - for (uint32_t t : gemma_call_start_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - break; - - default: - break; - } - } else if (is_needle()) { - switch (state_) { - case State::NEEDLE_START: - for (uint32_t t : qwen_tool_call_start_tokens_) { - current_bias_[t] = FORCE_BIAS; - } - if (tokenizer_) { - current_bias_[tokenizer_->get_eos_token()] = FORCE_BIAS; - } - break; - - default: - if (needle_json_state_ == NeedleJsonState::FREE) { - break; - } - - const NeedleTrieNode* trie_node = nullptr; - if (needle_json_state_ == NeedleJsonState::IN_NAME) { - trie_node = needle_get_trie_node(needle_name_trie_.get(), needle_constrained_buf_); - } else if (needle_json_state_ == NeedleJsonState::IN_ARG_KEY) { - auto it = needle_param_tries_.find(needle_current_function_); - if (it != needle_param_tries_.end()) { - trie_node = needle_get_trie_node(it->second.get(), needle_constrained_buf_); - } - } - - if (!trie_node) { - break; - } - - { - std::vector valid_tokens(needle_token_strings_.size(), false); - bool has_valid = false; - auto mark_valid_tokens = [&](char first_char) { - auto idx_it = needle_token_index_.find(first_char); - if (idx_it == needle_token_index_.end()) { - return; - } - for (uint32_t token_id : idx_it->second) { - if (!valid_tokens[token_id] && - needle_check_token_valid(needle_token_strings_[token_id], trie_node)) { - valid_tokens[token_id] = true; - has_valid = true; - } - } - }; - - for (const auto& [first_char, _] : trie_node->children) { - mark_valid_tokens(first_char); - } - if (trie_node->is_terminal) { - mark_valid_tokens('"'); - } - - if (!has_valid) { - break; - } - - for (size_t token_id = 0; token_id < valid_tokens.size(); ++token_id) { - if (!valid_tokens[token_id]) { - current_bias_[static_cast(token_id)] = NEEDLE_BLOCK_BIAS; - } - } - } - break; - } - } else { - switch (state_) { - case State::QWEN_START: - for (uint32_t t : qwen_tool_call_start_tokens_) { - current_bias_[t] = FORCE_BIAS; - } - for (uint32_t t : open_brace_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - for (uint32_t t : close_brace_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - break; - - case State::QWEN_EXPECT_OPEN_BRACE: - for (uint32_t t : open_brace_tokens_) { - current_bias_[t] = FORCE_BIAS; - } - for (uint32_t t : qwen_tool_call_end_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - break; - - case State::QWEN_EXPECT_NAME_KEY: { - bool has_name = generated_text_.find("name") != std::string::npos; - bool has_quote = generated_text_.find("\"") != std::string::npos; - if (has_name) { - for (uint32_t t : quote_tokens_) { current_bias_[t] = FORCE_BIAS; } - } else if (has_quote) { - for (uint32_t t : name_key_tokens_) { current_bias_[t] = FORCE_BIAS; } - for (uint32_t t : quote_tokens_) { current_bias_[t] = BLOCK_BIAS; } - } else { - for (uint32_t t : all_func_name_tokens_) { current_bias_[t] = BLOCK_BIAS; } - for (uint32_t t : args_key_tokens_) { current_bias_[t] = BLOCK_BIAS; } - for (uint32_t t : quote_tokens_) { current_bias_[t] = FORCE_BIAS; } - } - break; - } - - case State::QWEN_EXPECT_NAME_COLON: - for (uint32_t t : colon_tokens_) { - current_bias_[t] = FORCE_BIAS; - } - break; - - case State::QWEN_EXPECT_NAME_VALUE: { - bool name_complete = false; - for (const auto& name : function_names_) { - if (generated_text_.find(name) != std::string::npos) { name_complete = true; break; } - } - bool has_open_quote = generated_text_.find("\"") != std::string::npos; - if (name_complete) { - for (uint32_t t : quote_tokens_) { current_bias_[t] = FORCE_BIAS; } - } else if (has_open_quote) { - for (uint32_t t : all_func_name_tokens_) { current_bias_[t] = FORCE_BIAS; } - for (uint32_t t : quote_tokens_) { current_bias_[t] = 5.0f; } - } else { - for (uint32_t t : quote_tokens_) { current_bias_[t] = FORCE_BIAS; } - } - break; - } - - case State::QWEN_EXPECT_COMMA: - for (uint32_t t : comma_tokens_) { - current_bias_[t] = FORCE_BIAS; - } - for (uint32_t t : close_brace_tokens_) { - current_bias_[t] = 5.0f; - } - break; - - case State::QWEN_EXPECT_ARGS_KEY: { - bool has_args = generated_text_.find("arguments") != std::string::npos; - bool has_quote = generated_text_.find("\"") != std::string::npos; - if (has_args) { - for (uint32_t t : quote_tokens_) { current_bias_[t] = FORCE_BIAS; } - } else if (has_quote) { - for (uint32_t t : args_key_tokens_) { current_bias_[t] = FORCE_BIAS; } - for (uint32_t t : quote_tokens_) { current_bias_[t] = BLOCK_BIAS; } - } else { - for (uint32_t t : quote_tokens_) { current_bias_[t] = FORCE_BIAS; } - } - break; - } - - case State::QWEN_EXPECT_ARGS_COLON: - for (uint32_t t : colon_tokens_) { - current_bias_[t] = FORCE_BIAS; - } - break; - - case State::QWEN_IN_ARGUMENTS: - for (uint32_t t : open_brace_tokens_) { - current_bias_[t] = 3.0f; - } - for (uint32_t t : close_brace_tokens_) { - current_bias_[t] = 3.0f; - } - for (uint32_t t : colon_tokens_) { - current_bias_[t] = 2.0f; - } - for (uint32_t t : comma_tokens_) { - current_bias_[t] = 2.0f; - } - for (uint32_t t : quote_tokens_) { - current_bias_[t] = 2.0f; - } - for (uint32_t t : qwen_tool_call_end_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - break; - - case State::QWEN_EXPECT_CLOSE_BRACE: - for (uint32_t t : close_brace_tokens_) { - current_bias_[t] = FORCE_BIAS; - } - break; - - case State::QWEN_EXPECT_END: - for (uint32_t t : qwen_tool_call_end_tokens_) { - current_bias_[t] = FORCE_BIAS; - } - for (uint32_t t : open_brace_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - for (uint32_t t : qwen_tool_call_start_tokens_) { - current_bias_[t] = BLOCK_BIAS; - } - break; - - default: - break; - } - } -} - -void ToolCallConstrainer::reset() { - generated_text_.clear(); - current_bias_.clear(); - brace_depth_ = 0; - reset_needle_constraints(); - - if (model_type_ == Config::ModelType::LFM2) { - state_ = State::LFM_START; - lfm_current_function_.clear(); - lfm_args_buffer_.clear(); - lfm_seen_arg_keys_.clear(); - } else if (is_gemma_family()) { - state_ = State::GEMMA_START; - } else if (is_needle()) { - state_ = State::NEEDLE_START; - } else { - state_ = State::QWEN_START; - } - - if (active_) { - compute_bias(); - } -} - - -void Model::set_tool_constraints(const std::vector& tools) { - tool_constrainer_.init(config_.model_type, tools, tokenizer_.get()); -} - -void Model::clear_tool_constraints() { - tool_constrainer_.reset(); - tool_constrainer_.init(config_.model_type, {}, tokenizer_.get()); -} - -void Model::update_tool_constraints(uint32_t token_id) { - if (tool_constrainer_.is_active() && tokenizer_) { - std::string decoded = tokenizer_->decode({token_id}); - tool_constrainer_.update(token_id, decoded); - } -} - -} // namespace engine -} // namespace cactus diff --git a/cactus/engine/engine_image.cpp b/cactus/engine/engine_image.cpp deleted file mode 100644 index cf066e99e..000000000 --- a/cactus/engine/engine_image.cpp +++ /dev/null @@ -1,600 +0,0 @@ -#define STB_IMAGE_IMPLEMENTATION -#define STB_IMAGE_RESIZE_IMPLEMENTATION - -#define STBI_NO_BMP -#define STBI_NO_PSD -#define STBI_NO_HDR -#define STBI_NO_PIC -#define STBI_NO_PNM -#define STBI_NO_TGA - -#include "engine.h" -#include -#include -#include -#include -#include -#include -#include - -namespace cactus { -namespace engine { - -Siglip2Preprocessor::PreprocessedImage::~PreprocessedImage() { - pixel_values.clear(); - pixel_attention_mask.clear(); -} - -Siglip2Preprocessor::Siglip2Preprocessor(const Config& config) - : config_(config) {} - -Siglip2Preprocessor::Siglip2Preprocessor() : config_() {} - -Siglip2Preprocessor::~Siglip2Preprocessor() = default; - -std::pair Siglip2Preprocessor::compute_pixel_limits() const { - int64_t min_pixels = static_cast(config_.min_image_tokens) * - config_.patch_size * config_.patch_size * - config_.downsample_factor * config_.downsample_factor; - int64_t max_pixels = static_cast(config_.max_image_tokens) * - config_.patch_size * config_.patch_size * - config_.downsample_factor * config_.downsample_factor; - return {min_pixels, max_pixels}; -} - -int Siglip2Preprocessor::round_by_factor(int number, int factor) { - if (factor == 0) { - return number; - } - double scaled = static_cast(number) / static_cast(factor); - long long rounded = std::llround(scaled); - - return static_cast(rounded * factor); -} - -std::pair Siglip2Preprocessor::smart_resize(int height, int width) { - const int total_factor = config_.patch_size * config_.downsample_factor; - auto [min_pixels, max_pixels] = compute_pixel_limits(); - - int h_bar = std::max(total_factor, round_by_factor(height, total_factor)); - int w_bar = std::max(total_factor, round_by_factor(width, total_factor)); - - - if (static_cast(h_bar) * static_cast(w_bar) > max_pixels) { - double beta = std::sqrt((static_cast(height) * static_cast(width)) / - static_cast(max_pixels)); - h_bar = std::max(total_factor, - static_cast(std::floor(height / beta / total_factor)) * total_factor); - w_bar = std::max(total_factor, - static_cast(std::floor(width / beta / total_factor)) * total_factor); - } else if (static_cast(h_bar) * static_cast(w_bar) < min_pixels) { - double beta = std::sqrt(static_cast(min_pixels) / - (static_cast(height) * static_cast(width))); - h_bar = std::max(total_factor, - static_cast(std::ceil(height * beta / total_factor)) * total_factor); - w_bar = std::max(total_factor, - static_cast(std::ceil(width * beta / total_factor)) * total_factor); - } - - return {w_bar, h_bar}; -} - - -bool Siglip2Preprocessor::is_image_too_large(int height, int width) { - const int total_factor = config_.patch_size * config_.downsample_factor; - const int h_bar = std::max(config_.patch_size, round_by_factor(height, total_factor)); - const int w_bar = std::max(config_.patch_size, round_by_factor(width, total_factor)); - - const int64_t pixels = static_cast(h_bar) * static_cast(w_bar); - auto [min_pixels, max_pixels] = compute_pixel_limits(); - const double max_pixels_with_tolerance = static_cast(max_pixels) * config_.max_pixels_tolerance; - - bool result = static_cast(pixels) > max_pixels_with_tolerance; - - return result; -} - - -std::pair Siglip2Preprocessor::find_closest_aspect_ratio(float aspect_ratio, int width, int height) { - float best_ratio_diff = std::numeric_limits::infinity(); - std::pair best_ratio = {1, 1}; - int area = width * height; - - std::vector> target_ratios; - for (int n = config_.min_tiles; n <= config_.max_tiles; ++n) { - for (int w = 1; w <= n; ++w) { - for (int h = 1; h <= n; ++h) { - int total_tiles = w * h; - if (total_tiles >= config_.min_tiles && total_tiles <= config_.max_tiles) { - target_ratios.push_back({w, h}); - } - } - } - } - - std::sort(target_ratios.begin(), target_ratios.end()); - target_ratios.erase(std::unique(target_ratios.begin(), target_ratios.end()), target_ratios.end()); - std::sort(target_ratios.begin(), target_ratios.end(), [](const auto& a, const auto& b) { - return (a.first * a.second) < (b.first * b.second); - }); - - for (const auto& ratio : target_ratios) { - float target_aspect_ratio = static_cast(ratio.first) / ratio.second; - float ratio_diff = std::abs(aspect_ratio - target_aspect_ratio); - - if (ratio_diff < best_ratio_diff) { - best_ratio_diff = ratio_diff; - best_ratio = ratio; - } else if (ratio_diff == best_ratio_diff) { - int target_area = config_.tile_size * config_.tile_size * ratio.first * ratio.second; - if (area > 0.5f * target_area) { - best_ratio = ratio; - } - } - } - - - return best_ratio; -} - -std::pair Siglip2Preprocessor::get_grid_layout(int height, int width) { - float aspect_ratio = (float)width / (float)height; - auto [grid_width, grid_height] = find_closest_aspect_ratio(aspect_ratio, width, height); - - int target_width = config_.tile_size * grid_width; - int target_height = config_.tile_size * grid_height; - - return {target_width, target_height}; -} - -Siglip2Preprocessor::SpatialShapeResult Siglip2Preprocessor::compute_spatial_shapes(int height, int width) { - if (height <= 0 || width <= 0) { - throw std::runtime_error("Image dimensions must be positive"); - } - - if (config_.patch_size <= 0) { - throw std::runtime_error("Patch size must be positive"); - } - - const int patch = config_.patch_size; - auto [resized_width, resized_height] = smart_resize(height, width); - const bool should_split = config_.do_image_splitting && is_image_too_large(height, width); - - - SpatialShapeResult result; - result.grid_rows = 1; - result.grid_cols = 1; - - if (should_split) { - if (config_.tile_size % patch != 0) { - throw std::runtime_error("Tile size must be divisible by patch size"); - } - - auto [grid_target_width, grid_target_height] = get_grid_layout(height, width); - result.grid_cols = grid_target_width / config_.tile_size; - result.grid_rows = grid_target_height / config_.tile_size; - - - const int patches_per_tile_side = config_.tile_size / patch; - const auto tile_shape = std::make_pair(patches_per_tile_side, patches_per_tile_side); - - result.shapes.reserve(static_cast(result.grid_rows) * result.grid_cols + 1); - for (int row = 0; row < result.grid_rows; ++row) { - for (int col = 0; col < result.grid_cols; ++col) { - result.shapes.push_back(tile_shape); - - } - } - - if (config_.use_thumbnail && result.grid_rows * result.grid_cols != 1) { - if (resized_height % patch != 0 || resized_width % patch != 0) { - throw std::runtime_error("Resized thumbnail dimensions must be divisible by patch size"); - } - result.shapes.emplace_back(resized_height / patch, resized_width / patch); - - } - } else { - int target_width = resized_width; - int target_height = resized_height; - if (!config_.do_resize) { - target_width = width; - target_height = height; - } - - if (target_height % patch != 0 || target_width % patch != 0) { - throw std::runtime_error("Target dimensions must be divisible by patch size"); - } - - result.shapes.emplace_back(target_height / patch, target_width / patch); - - } - - return result; -} - - -Siglip2Preprocessor::PreprocessedImage Siglip2Preprocessor::preprocess_from_file(const std::string& image_path) { - int width, height, channels; - unsigned char* img_data = stbi_load(image_path.c_str(), &width, &height, &channels, 0); - - if (!img_data) { - throw std::runtime_error("Failed to load image: " + image_path + " - " + std::string(stbi_failure_reason())); - } - - PreprocessedImage result; - result = preprocess_from_memory(img_data, width, height, channels); - - stbi_image_free(img_data); - return result; -} - -Siglip2Preprocessor::PreprocessedImage Siglip2Preprocessor::preprocess_from_memory( - const unsigned char* img_data, int width, int height, int channels) { - if (!img_data) { - throw std::runtime_error("Invalid image data pointer"); - } - - const int expected_channels = 3; - std::vector rgb_data; - const unsigned char* source_data = img_data; - int source_channels = channels; - - if (config_.do_convert_rgb && channels != expected_channels) { - rgb_data = convert_to_rgb(img_data, width, height, channels); - source_data = rgb_data.data(); - source_channels = expected_channels; - - } - - if (source_channels != expected_channels) { - throw std::runtime_error("Image must have 3 channels (RGB)"); - } - - const int patch = config_.patch_size; - const int downsample = config_.downsample_factor; - const int patch_dim = patch * patch * expected_channels; - - if (patch <= 0) { - throw std::runtime_error("Patch size must be positive"); - } - if (config_.tile_size % patch != 0) { - throw std::runtime_error("Tile size must be divisible by patch size"); - } - - auto [resized_width, resized_height] = smart_resize(height, width); - - - const int patches_per_tile_side = config_.tile_size / patch; - const int tile_patch_count = patches_per_tile_side * patches_per_tile_side; - const int thumbnail_patch_cap = config_.max_image_tokens * downsample * downsample; - int max_patches_per_tile = config_.max_num_patches; - max_patches_per_tile = std::max(max_patches_per_tile, tile_patch_count); - max_patches_per_tile = std::max(max_patches_per_tile, thumbnail_patch_cap); - - const bool allow_splitting = config_.do_image_splitting; - const bool should_split = allow_splitting && is_image_too_large(height, width); - - - size_t expected_tiles = should_split ? static_cast(config_.max_tiles) : 1; - if (should_split && config_.use_thumbnail) { - expected_tiles += 1; - } - - std::vector> tile_patches; - tile_patches.reserve(expected_tiles); - std::vector> spatial_shapes; - spatial_shapes.reserve(expected_tiles); - - auto normalize_and_patchify = [&](const float* data_ptr, int img_width, int img_height) { - if (img_height % patch != 0 || img_width % patch != 0) { - throw std::runtime_error("Image dimensions must be divisible by patch size"); - } - std::vector normalized = normalize_image(data_ptr, img_width, img_height, expected_channels); - auto patches = convert_image_to_patches(normalized, img_width, img_height, expected_channels, patch); - std::vector flattened(patches.size() * patch_dim); - for (size_t idx = 0; idx < patches.size(); ++idx) { - std::copy(patches[idx].begin(), patches[idx].end(), flattened.begin() + idx * patch_dim); - } - tile_patches.push_back(std::move(flattened)); - spatial_shapes.emplace_back(img_height / patch, img_width / patch); - }; - - int grid_rows = 1; - int grid_cols = 1; - bool thumbnail_added = false; - - if (should_split) { - auto [grid_target_width, grid_target_height] = get_grid_layout(height, width); - grid_cols = grid_target_width / config_.tile_size; - grid_rows = grid_target_height / config_.tile_size; - - std::vector resized_grid = resize_image( - source_data, width, height, grid_target_width, grid_target_height, expected_channels); - - std::vector tile_buffer( - static_cast(config_.tile_size) * config_.tile_size * expected_channels); - - for (int row = 0; row < grid_rows; ++row) { - for (int col = 0; col < grid_cols; ++col) { - for (int y = 0; y < config_.tile_size; ++y) { - const float* src_row = resized_grid.data() + - ((row * config_.tile_size + y) * grid_target_width + col * config_.tile_size) * - expected_channels; - float* dst_row = tile_buffer.data() + (y * config_.tile_size) * expected_channels; - std::copy_n(src_row, - static_cast(config_.tile_size) * expected_channels, - dst_row); - } - normalize_and_patchify(tile_buffer.data(), config_.tile_size, config_.tile_size); - - } - } - - if (config_.use_thumbnail && grid_rows * grid_cols != 1) { - std::vector thumbnail_bytes = resize_image( - source_data, width, height, resized_width, resized_height, expected_channels); - normalize_and_patchify(thumbnail_bytes.data(), resized_width, resized_height); - thumbnail_added = true; - - } - } else { - int target_width = resized_width; - int target_height = resized_height; - - const bool needs_resize = config_.do_resize && (width != target_width || height != target_height); - - std::vector resized_image; - if (needs_resize) { - resized_image = resize_image(source_data, width, height, target_width, target_height, expected_channels); - normalize_and_patchify(resized_image.data(), target_width, target_height); - - } else { - resized_image.resize(static_cast(width) * height * expected_channels); - for (size_t idx = 0; idx < resized_image.size(); ++idx) { - resized_image[idx] = static_cast(source_data[idx]); - } - normalize_and_patchify(resized_image.data(), width, height); - target_width = width; - target_height = height; - resized_width = target_width; - resized_height = target_height; - - } - - grid_rows = 1; - grid_cols = 1; - } - - PreprocessedImage result = pad_patches(tile_patches, spatial_shapes, patch_dim, max_patches_per_tile); - - - result.image_rows = grid_rows; - result.image_cols = grid_cols; - result.image_width = resized_width; - result.image_height = resized_height; - - auto compute_tokens = [&](int patches_h, int patches_w) -> int { - int tokens_h = (patches_h + downsample - 1) / downsample; - int tokens_w = (patches_w + downsample - 1) / downsample; - return tokens_h * tokens_w; - }; - - result.tokens_per_tile = spatial_shapes.empty() - ? 0 - : compute_tokens(spatial_shapes.front().first, spatial_shapes.front().second); - - - if (thumbnail_added && !spatial_shapes.empty()) { - const auto& thumb_shape = spatial_shapes.back(); - result.thumbnail_tokens = compute_tokens(thumb_shape.first, thumb_shape.second); - - } else { - result.thumbnail_tokens = 0; - } - - return result; -} - -std::vector Siglip2Preprocessor::convert_to_rgb( - const unsigned char* img_data, int width, int height, int channels) { - - std::vector rgb_data(width * height * 3); - - if (channels == 1) { - for (int i = 0; i < width * height; ++i) { - rgb_data[i * 3 + 0] = img_data[i]; - rgb_data[i * 3 + 1] = img_data[i]; - rgb_data[i * 3 + 2] = img_data[i]; - } - } else if (channels == 4) { - for (int i = 0; i < width * height; ++i) { - rgb_data[i * 3 + 0] = img_data[i * 4 + 0]; - rgb_data[i * 3 + 1] = img_data[i * 4 + 1]; - rgb_data[i * 3 + 2] = img_data[i * 4 + 2]; - } - } else if (channels == 2) { - for (int i = 0; i < width * height; ++i) { - rgb_data[i * 3 + 0] = img_data[i * 2 + 0]; - rgb_data[i * 3 + 1] = img_data[i * 2 + 0]; - rgb_data[i * 3 + 2] = img_data[i * 2 + 0]; - } - } else { - throw std::runtime_error("Unsupported number of channels: " + std::to_string(channels)); - } - - return rgb_data; -} - -std::vector Siglip2Preprocessor::resize_image( - const unsigned char* img_data, int src_width, int src_height, - int dst_width, int dst_height, int channels) { - - const size_t src_elements = static_cast(src_width) * src_height * channels; - std::vector src_float(src_elements); - for (size_t idx = 0; idx < src_elements; ++idx) { - src_float[idx] = static_cast(img_data[idx]); - } - - - std::vector resized_data(static_cast(dst_width) * dst_height * channels); - - stbir_pixel_layout layout = (channels == 1) ? STBIR_1CHANNEL : - (channels == 3) ? STBIR_RGB : STBIR_RGBA; - - float* result = stbir_resize_float_linear( - src_float.data(), src_width, src_height, 0, - resized_data.data(), dst_width, dst_height, 0, - layout - ); - - if (!result) { - throw std::runtime_error("Failed to resize image"); - } - - - return resized_data; -} - -std::vector Siglip2Preprocessor::normalize_image( - const float* img_data, int width, int height, int channels) { - - size_t total_pixels = width * height * channels; - std::vector normalized(total_pixels); - - for (size_t i = 0; i < static_cast(width * height); ++i) { - for (int c = 0; c < channels; ++c) { - size_t idx = i * channels + c; - float pixel = img_data[idx]; - - if (config_.do_rescale) { - pixel *= config_.rescale_factor; - } - - if (config_.do_normalize) { - pixel = (pixel - config_.image_mean[c]) / config_.image_std[c]; - } - - normalized[idx] = pixel; - } - } - - - return normalized; -} - -std::vector> Siglip2Preprocessor::convert_image_to_patches( - const std::vector& image, int width, int height, int channels, int patch_size) { - - int num_patches_height = height / patch_size; - int num_patches_width = width / patch_size; - int num_patches = num_patches_height * num_patches_width; - int patch_elements = patch_size * patch_size * channels; - - std::vector> patches(num_patches, std::vector(patch_elements)); - - for (int ph = 0; ph < num_patches_height; ++ph) { - for (int pw = 0; pw < num_patches_width; ++pw) { - int patch_idx = ph * num_patches_width + pw; - - for (int y = 0; y < patch_size; ++y) { - for (int x = 0; x < patch_size; ++x) { - int img_y = ph * patch_size + y; - int img_x = pw * patch_size + x; - int img_idx = (img_y * width + img_x) * channels; - int patch_offset = (y * patch_size + x) * channels; - - for (int c = 0; c < channels; ++c) { - patches[patch_idx][patch_offset + c] = image[img_idx + c]; - } - } - } - } - } - - return patches; -} - -Siglip2Preprocessor::PreprocessedImage Siglip2Preprocessor::pad_patches( - const std::vector>& tile_patches, - const std::vector>& spatial_shapes, - int patch_dim, - int max_patches_per_tile) { - - if (tile_patches.size() != spatial_shapes.size()) { - throw std::runtime_error("Mismatch between tile data and spatial shapes"); - } - - PreprocessedImage result; - - const int num_tiles = static_cast(tile_patches.size()); - result.num_tiles = num_tiles; - result.patch_dim = patch_dim; - result.max_patches_per_tile = max_patches_per_tile; - result.spatial_shapes = spatial_shapes; - result.actual_num_patches = 0; - - const size_t total_values = static_cast(num_tiles) * max_patches_per_tile * patch_dim; - result.pixel_values.assign(total_values, 0.0f); - result.pixel_attention_mask.assign(static_cast(num_tiles) * max_patches_per_tile, 0); - - - for (int tile_idx = 0; tile_idx < num_tiles; ++tile_idx) { - const auto& [patches_h, patches_w] = spatial_shapes[tile_idx]; - const int actual_patches = patches_h * patches_w; - - if (actual_patches > max_patches_per_tile) { - throw std::runtime_error("Actual patches exceed max_patches_per_tile"); - } - - const auto& flattened = tile_patches[tile_idx]; - const size_t expected_size = static_cast(actual_patches) * patch_dim; - if (flattened.size() != expected_size) { - throw std::runtime_error("Tile patch data has unexpected size"); - } - - float* destination = result.pixel_values.data() + - static_cast(tile_idx) * max_patches_per_tile * patch_dim; - std::memcpy(destination, flattened.data(), expected_size * sizeof(float)); - - - int mask_offset = tile_idx * max_patches_per_tile; - for (int p = 0; p < actual_patches; ++p) { - result.pixel_attention_mask[mask_offset + p] = 1; - } - - result.actual_num_patches += actual_patches; - } - - if (!spatial_shapes.empty()) { - result.num_patches_height = spatial_shapes.front().first; - result.num_patches_width = spatial_shapes.front().second; - } else { - result.num_patches_height = 0; - result.num_patches_width = 0; - } - - result.pixel_values_shape = { - static_cast(num_tiles), - static_cast(max_patches_per_tile), - static_cast(patch_dim) - }; - - - result.pixel_attention_mask_shape = { - static_cast(num_tiles), - static_cast(max_patches_per_tile) - }; - - result.spatial_shapes_shape = { - static_cast(num_tiles), - static_cast(2) - }; - - - return result; -} - -} // namespace engine -} // namespace cactus - - diff --git a/cactus/engine/engine_index.cpp b/cactus/engine/engine_index.cpp deleted file mode 100644 index 13d08920a..000000000 --- a/cactus/engine/engine_index.cpp +++ /dev/null @@ -1,780 +0,0 @@ -#include "engine.h" -#include "kernel/kernel.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cactus { -namespace engine { -namespace index { - - __fp16 dot_product(const __fp16 *a, const __fp16 *b, size_t dim) { - __fp16 result; - cactus_matmul_f16(a, b, &result, 1, dim, 1); - return result; - } - - void normalize(__fp16 *v, size_t dim) { - __fp16 x = dot_product(v, v, dim); - if (x < 1e-6f) { - throw std::runtime_error("Cannot normalize zero vector"); - } - cactus_scalar_op_f16(&x, &x, 1, 0, ScalarOpType::SQRT); - cactus_scalar_op_f16(v, v, dim, x, ScalarOpType::DIVIDE); - } - - Index::Index(const std::string& index_path, const std::string& data_path, size_t embedding_dim): - index_path_(index_path), data_path_(data_path), embedding_dim_(embedding_dim), - index_entry_size_(IndexEntry::size(embedding_dim)), num_documents_(0), - index_fd_(-1), data_fd_(-1), - mapped_index_(nullptr), mapped_data_(nullptr) { - - bool index_exists = (access(index_path.c_str(), F_OK) == 0); - bool data_exists = (access(data_path.c_str(), F_OK) == 0); - - if (index_exists != data_exists) { - throw std::runtime_error("Index and data files must both exist or both not exist"); - } - - index_fd_ = open(index_path.c_str(), O_RDWR | O_CREAT, 0644); - if (index_fd_ < 0) { - throw std::runtime_error("Cannot open index file: " + index_path); - } - - data_fd_ = open(data_path.c_str(), O_RDWR | O_CREAT, 0644); - if (data_fd_ < 0) { - close(index_fd_); - throw std::runtime_error("Cannot open data file: " + data_path); - } - - auto cleanup_and_throw = [this](const std::string& msg) { - close(index_fd_); - close(data_fd_); - throw std::runtime_error(msg); - }; - - struct stat index_st, data_st; - if (fstat(index_fd_, &index_st)) { - cleanup_and_throw("Cannot get index file size: " + index_path); - } - index_file_size_ = index_st.st_size; - - if (fstat(data_fd_, &data_st)) { - cleanup_and_throw("Cannot get data file size: " + data_path); - } - data_file_size_ = data_st.st_size; - - if (!index_exists) { - index_file_size_ = sizeof(IndexHeader); - data_file_size_ = sizeof(DataHeader); - - if (ftruncate(index_fd_, index_file_size_) != 0) { - cleanup_and_throw("Failed to resize index file"); - } - - if (ftruncate(data_fd_, data_file_size_) != 0) { - cleanup_and_throw("Failed to resize data file"); - } - } - - mapped_index_ = mmap(nullptr, index_file_size_, PROT_READ | PROT_WRITE, MAP_SHARED, index_fd_, 0); - if (mapped_index_ == MAP_FAILED) { - cleanup_and_throw("Cannot map file: " + index_path); - } - - mapped_data_ = mmap(nullptr, data_file_size_, PROT_READ | PROT_WRITE, MAP_SHARED, data_fd_, 0); - if (mapped_data_ == MAP_FAILED) { - munmap(mapped_index_, index_file_size_); - close(index_fd_); - close(data_fd_); - throw std::runtime_error("Cannot map file: " + data_path); - } - - if (!index_exists) { - IndexHeader index_header = { - MAGIC, - VERSION, - static_cast(embedding_dim), - 0 - }; - memcpy(mapped_index_, &index_header, sizeof(IndexHeader)); - - DataHeader data_header = { MAGIC, VERSION }; - memcpy(mapped_data_, &data_header, sizeof(DataHeader)); - - if (msync(mapped_index_, index_file_size_, MS_SYNC) != 0) { - cleanup_and_throw("Failed to sync index file"); - } - - if (msync(mapped_data_, data_file_size_, MS_SYNC) != 0) { - cleanup_and_throw("Failed to sync data file"); - } - } - - parse_index_header(); - parse_data_header(); - - build_doc_id_map(); - } - - Index::~Index() { - if (mapped_index_ != nullptr && mapped_index_ != MAP_FAILED) { - madvise(mapped_index_, index_file_size_, MADV_DONTNEED); - munmap(mapped_index_, index_file_size_); - } - if (mapped_data_ != nullptr && mapped_data_ != MAP_FAILED) { - madvise(mapped_data_, data_file_size_, MADV_DONTNEED); - munmap(mapped_data_, data_file_size_); - } - if (index_fd_ != -1) { - close(index_fd_); - } - if (data_fd_ != -1) { - close(data_fd_); - } - } - - void Index::add_documents(const std::vector& documents) { - validate_documents(documents); - - size_t added_data_size = 0; - for (const auto& doc : documents) { - added_data_size += sizeof(DataEntry) + doc.content.size() + doc.metadata.size(); - } - - size_t new_index_size = index_file_size_ + documents.size() * index_entry_size_; - size_t new_data_size = data_file_size_ + added_data_size; - - if (ftruncate(index_fd_, new_index_size) != 0) { - throw std::runtime_error("Failed to resize index file"); - } - - if (ftruncate(data_fd_, new_data_size) != 0) { - throw std::runtime_error("Failed to resize data file"); - } - - munmap(mapped_index_, index_file_size_); - munmap(mapped_data_, data_file_size_); - - mapped_index_ = mmap(nullptr, new_index_size, PROT_READ | PROT_WRITE, MAP_SHARED, index_fd_, 0); - if (mapped_index_ == MAP_FAILED) { - throw std::runtime_error("Failed to remap index file"); - } - - mapped_data_ = mmap(nullptr, new_data_size, PROT_READ | PROT_WRITE, MAP_SHARED, data_fd_, 0); - if (mapped_data_ == MAP_FAILED) { - munmap(mapped_index_, new_index_size); - throw std::runtime_error("Failed to remap data file"); - } - - size_t old_index_size = index_file_size_; - size_t old_data_size = data_file_size_; - - index_file_size_ = new_index_size; - data_file_size_ = new_data_size; - - char* index_write_pos = static_cast(mapped_index_) + old_index_size; - char* data_write_pos = static_cast(mapped_data_) + old_data_size; - - uint64_t data_offset = old_data_size; - doc_id_map_.reserve(doc_id_map_.size() + documents.size()); - - for (size_t i = 0; i < documents.size(); ++i) { - const auto& doc = documents[i]; - - DataEntry data_entry{ - static_cast(doc.content.size()), - static_cast(doc.metadata.size()) - }; - - memcpy(data_write_pos, &data_entry, sizeof(DataEntry)); - data_write_pos += sizeof(DataEntry); - memcpy(data_write_pos, doc.content.data(), doc.content.size()); - data_write_pos += doc.content.size(); - memcpy(data_write_pos, doc.metadata.data(), doc.metadata.size()); - data_write_pos += doc.metadata.size(); - - IndexEntry index_entry{doc.id, data_offset, 0}; - memcpy(index_write_pos, &index_entry, sizeof(IndexEntry)); - index_write_pos += sizeof(IndexEntry); - - std::vector<__fp16> normalized_embedding(embedding_dim_); - cactus_fp32_to_fp16(doc.embedding.data(), normalized_embedding.data(), embedding_dim_); - normalize(normalized_embedding.data(), embedding_dim_); - - size_t embedding_bytes = embedding_dim_ * sizeof(__fp16); - memcpy(index_write_pos, normalized_embedding.data(), embedding_bytes); - index_write_pos += embedding_bytes; - - doc_id_map_[doc.id] = num_documents_ + static_cast(i); - data_offset += sizeof(DataEntry) + doc.content.size() + doc.metadata.size(); - } - - num_documents_ += static_cast(documents.size()); - IndexHeader* header = reinterpret_cast(mapped_index_); - header->num_documents = num_documents_; - - if (msync(mapped_data_, data_file_size_, MS_SYNC) != 0) { - throw std::runtime_error("Failed to sync data file to disk"); - } - - if (msync(mapped_index_, index_file_size_, MS_SYNC) != 0) { - throw std::runtime_error("Failed to sync index file to disk"); - } - } - - void Index::delete_documents(const std::vector& doc_ids) { - validate_doc_ids(doc_ids); - - char* index_ptr = static_cast(mapped_index_); - char* entries = index_ptr + sizeof(IndexHeader); - - if (num_documents_ > 0) { - size_t last_entry_offset = (num_documents_ - 1) * index_entry_size_; - if (sizeof(IndexHeader) + last_entry_offset + index_entry_size_ > index_file_size_) { - throw std::runtime_error("File corrupted: index entry extends beyond file size"); - } - } - - for (int doc_id : doc_ids) { - uint32_t i = doc_id_map_.at(doc_id); - IndexEntry& entry = *reinterpret_cast(entries + i * index_entry_size_); - - entry.flags |= 0x1; - doc_id_map_.erase(doc_id); - } - - if (msync(mapped_index_, index_file_size_, MS_SYNC) != 0) { - throw std::runtime_error("Failed to sync index file to disk"); - } - } - - std::vector Index::get_documents(const std::vector& doc_ids) { - validate_doc_ids(doc_ids); - - const char* index_ptr = static_cast(mapped_index_); - const char* entries = index_ptr + sizeof(IndexHeader); - const char* data_ptr = static_cast(mapped_data_); - - if (num_documents_ > 0) { - size_t last_entry_offset = (num_documents_ - 1) * index_entry_size_; - if (sizeof(IndexHeader) + last_entry_offset + index_entry_size_ > index_file_size_) { - throw std::runtime_error("File corrupted: index entry extends beyond file size"); - } - } - - std::vector results; - results.reserve(doc_ids.size()); - - for (int doc_id : doc_ids) { - uint32_t i = doc_id_map_.at(doc_id); - const IndexEntry& entry = *reinterpret_cast(entries + i * index_entry_size_); - - if (static_cast(entry.data_offset) + sizeof(DataEntry) > data_file_size_) { - throw std::runtime_error("File corrupted: data entry extends beyond file size"); - } - - const DataEntry* data_entry = reinterpret_cast(data_ptr + entry.data_offset); - size_t data_entry_size = sizeof(DataEntry) + data_entry->content_len + data_entry->metadata_len; - - if (static_cast(entry.data_offset) + data_entry_size > data_file_size_) { - throw std::runtime_error("File corrupted: data entry extends beyond file size"); - } - - std::vector embedding_f32(embedding_dim_); - cactus_fp16_to_fp32(entry.embedding(), embedding_f32.data(), embedding_dim_); - - results.emplace_back( - doc_id, - std::move(embedding_f32), - std::string(data_entry->content(), data_entry->content_len), - std::string(data_entry->metadata(), data_entry->metadata_len) - ); - } - - return results; - } - - std::vector> Index::query(const std::vector>& embeddings, const QueryOptions& options) { - if (embeddings.empty()) { - return {}; - } - - if (options.top_k == 0) { - return std::vector>(embeddings.size()); - } - - if (num_documents_ > 0) { - size_t last_entry_offset = (num_documents_ - 1) * index_entry_size_; - if (sizeof(IndexHeader) + last_entry_offset + index_entry_size_ > index_file_size_) { - throw std::runtime_error("File corrupted: index entry extends beyond file size"); - } - } - - std::vector> normalized_embeddings; - normalized_embeddings.reserve(embeddings.size()); - - for (const auto& embedding : embeddings) { - std::vector<__fp16> normalized_embedding(embedding_dim_); - cactus_fp32_to_fp16(embedding.data(), normalized_embedding.data(), embedding_dim_); - normalize(normalized_embedding.data(), embedding_dim_); - normalized_embeddings.emplace_back(std::move(normalized_embedding)); - } - - std::vector> all_results; - all_results.reserve(embeddings.size()); - - const char* index_ptr = static_cast(mapped_index_); - const char* entries = index_ptr + sizeof(IndexHeader); - - auto cmp = [](const QueryResult& a, const QueryResult& b) { - return a.score > b.score; - }; - - for (const auto& normalized_embedding : normalized_embeddings) { - std::priority_queue, decltype(cmp)> top_results(cmp); - - for (uint32_t i = 0; i < num_documents_; ++i) { - const IndexEntry& entry = *reinterpret_cast(entries + i * index_entry_size_); - - if (entry.flags & 0x1) { - continue; - } - - float score = static_cast( - dot_product(normalized_embedding.data(), entry.embedding(), embedding_dim_) - ); - - if (score < options.score_threshold) { - continue; - } - - if (top_results.size() < options.top_k) { - top_results.emplace(entry.doc_id, score); - } else if (score > top_results.top().score) { - top_results.pop(); - top_results.emplace(entry.doc_id, score); - } - } - - std::vector results; - results.reserve(top_results.size()); - - while(!top_results.empty()) { - results.emplace_back(top_results.top()); - top_results.pop(); - } - std::reverse(results.begin(), results.end()); - - all_results.emplace_back(std::move(results)); - } - - return all_results; - } - - void Index::compact() { - std::string temp_index_path = index_path_ + ".tmp"; - std::string temp_data_path = data_path_ + ".tmp"; - - const char* index_ptr = static_cast(mapped_index_); - const char* entries = index_ptr + sizeof(IndexHeader); - const char* data_ptr = static_cast(mapped_data_); - - uint32_t compacted_count = static_cast(doc_id_map_.size()); - - if (num_documents_ > 0) { - size_t last_entry_offset = (num_documents_ - 1) * index_entry_size_; - if (sizeof(IndexHeader) + last_entry_offset + index_entry_size_ > index_file_size_) { - throw std::runtime_error("Compaction failed: File corrupted: index entry extends beyond file size"); - } - } - - off_t new_data_offset = sizeof(DataHeader); - size_t new_data_size = sizeof(DataHeader); - - for (const auto& [doc_id, index] : doc_id_map_) { - const IndexEntry& entry = *reinterpret_cast(entries + index * index_entry_size_); - - if (static_cast(entry.data_offset) + sizeof(DataEntry) > data_file_size_) { - throw std::runtime_error("Compaction failed: File corrupted: data entry extends beyond file size"); - } - - const DataEntry* data_entry = reinterpret_cast(data_ptr + entry.data_offset); - uint32_t data_entry_size = sizeof(DataEntry) + data_entry->content_len + data_entry->metadata_len; - - if (static_cast(entry.data_offset) + data_entry_size > data_file_size_) { - throw std::runtime_error("Compaction failed: File corrupted: data entry extends beyond file size"); - } - - new_data_size += data_entry_size; - } - - size_t new_index_size = sizeof(IndexHeader) + compacted_count * index_entry_size_; - - int temp_index_fd = open(temp_index_path.c_str(), O_RDWR | O_CREAT | O_TRUNC, 0644); - if (temp_index_fd < 0) { - throw std::runtime_error("Cannot create temporary index file: " + temp_index_path); - } - - int temp_data_fd = open(temp_data_path.c_str(), O_RDWR | O_CREAT | O_TRUNC, 0644); - if (temp_data_fd < 0) { - close(temp_index_fd); - unlink(temp_index_path.c_str()); - throw std::runtime_error("Cannot create temporary data file: " + temp_data_path); - } - - auto cleanup_and_throw = [&temp_index_fd, &temp_data_fd, &temp_index_path, &temp_data_path](const std::string& msg) { - close(temp_index_fd); - close(temp_data_fd); - unlink(temp_index_path.c_str()); - unlink(temp_data_path.c_str()); - throw std::runtime_error(msg); - }; - - if (ftruncate(temp_index_fd, new_index_size) != 0) { - cleanup_and_throw("Failed to resize temporary index file"); - } - - if (ftruncate(temp_data_fd, new_data_size) != 0) { - cleanup_and_throw("Failed to resize temporary data file"); - } - - void* temp_index_map = mmap(nullptr, new_index_size, PROT_READ | PROT_WRITE, MAP_SHARED, temp_index_fd, 0); - if (temp_index_map == MAP_FAILED) { - cleanup_and_throw("Cannot map temporary index file"); - } - - void* temp_data_map = mmap(nullptr, new_data_size, PROT_READ | PROT_WRITE, MAP_SHARED, temp_data_fd, 0); - if (temp_data_map == MAP_FAILED) { - munmap(temp_index_map, new_index_size); - cleanup_and_throw("Cannot map temporary data file"); - } - - IndexHeader new_header = { - MAGIC, - VERSION, - static_cast(embedding_dim_), - compacted_count - }; - - DataHeader new_data_header = { - MAGIC, - VERSION - }; - - memcpy(temp_index_map, &new_header, sizeof(IndexHeader)); - memcpy(temp_data_map, &new_data_header, sizeof(DataHeader)); - - char* temp_index_ptr = static_cast(temp_index_map); - char* temp_data_ptr = static_cast(temp_data_map); - char* index_write_pos = temp_index_ptr + sizeof(IndexHeader); - char* data_write_pos = temp_data_ptr + sizeof(DataHeader); - - std::unordered_map new_doc_id_map; - new_doc_id_map.reserve(compacted_count); - - uint32_t new_index = 0; - new_data_offset = sizeof(DataHeader); - - for (const auto& [doc_id, index] : doc_id_map_) { - const IndexEntry& entry = *reinterpret_cast(entries + index * index_entry_size_); - const DataEntry* data_entry = reinterpret_cast(data_ptr + entry.data_offset); - uint32_t data_entry_size = sizeof(DataEntry) + data_entry->content_len + data_entry->metadata_len; - - memcpy(data_write_pos, data_entry, data_entry_size); - data_write_pos += data_entry_size; - - IndexEntry new_entry = {entry.doc_id, static_cast(new_data_offset), 0}; - - memcpy(index_write_pos, &new_entry, sizeof(IndexEntry)); - index_write_pos += sizeof(IndexEntry); - - size_t embedding_bytes = embedding_dim_ * sizeof(__fp16); - memcpy(index_write_pos, entry.embedding(), embedding_bytes); - index_write_pos += embedding_bytes; - - new_doc_id_map[doc_id] = new_index; - ++new_index; - - if (static_cast(data_entry_size) > std::numeric_limits::max() - new_data_offset) { - munmap(temp_index_map, new_index_size); - munmap(temp_data_map, new_data_size); - cleanup_and_throw("Data offset overflow during compaction"); - } - new_data_offset += data_entry_size; - } - - if (msync(temp_data_map, new_data_size, MS_SYNC) != 0) { - munmap(temp_index_map, new_index_size); - munmap(temp_data_map, new_data_size); - cleanup_and_throw("Failed to sync temporary data file"); - } - - if (msync(temp_index_map, new_index_size, MS_SYNC) != 0) { - munmap(temp_index_map, new_index_size); - munmap(temp_data_map, new_data_size); - cleanup_and_throw("Failed to sync temporary index file"); - } - - munmap(temp_index_map, new_index_size); - munmap(temp_data_map, new_data_size); - - std::string backup_index = index_path_ + ".backup"; - std::string backup_data = data_path_ + ".backup"; - - if (access(backup_index.c_str(), F_OK) == 0 || access(backup_data.c_str(), F_OK) == 0) { - cleanup_and_throw("Backup files already exist, previous compaction may have failed"); - } - - close(temp_index_fd); - close(temp_data_fd); - - munmap(mapped_index_, index_file_size_); - munmap(mapped_data_, data_file_size_); - close(index_fd_); - close(data_fd_); - - if (rename(index_path_.c_str(), backup_index.c_str()) != 0) { - cleanup_and_throw("Failed to backup index file"); - } - if (rename(data_path_.c_str(), backup_data.c_str()) != 0) { - rename(backup_index.c_str(), index_path_.c_str()); - cleanup_and_throw("Failed to backup data file"); - } - - if (rename(temp_data_path.c_str(), data_path_.c_str()) != 0) { - rename(backup_data.c_str(), data_path_.c_str()); - rename(backup_index.c_str(), index_path_.c_str()); - cleanup_and_throw("Failed to rename data file"); - } - - if (rename(temp_index_path.c_str(), index_path_.c_str()) != 0) { - unlink(data_path_.c_str()); - rename(backup_data.c_str(), data_path_.c_str()); - rename(backup_index.c_str(), index_path_.c_str()); - cleanup_and_throw("Failed to rename index file"); - } - - auto sync_dir = [](const std::string& file_path) { - size_t last_slash = file_path.rfind('/'); - std::string dir_path = (last_slash != std::string::npos) ? file_path.substr(0, last_slash) : "."; - int dir_fd = open(dir_path.c_str(), O_RDONLY); - if (dir_fd < 0) { - throw std::runtime_error("Cannot open directory for fsync: " + dir_path); - } - if (fsync(dir_fd) != 0) { - close(dir_fd); - throw std::runtime_error("Failed to fsync directory: " + dir_path); - } - close(dir_fd); - }; - - sync_dir(index_path_); - sync_dir(data_path_); - - unlink(backup_index.c_str()); - unlink(backup_data.c_str()); - - int new_index_fd = open(index_path_.c_str(), O_RDWR); - if (new_index_fd < 0) { - throw std::runtime_error("Cannot open new index file: " + index_path_); - } - - int new_data_fd = open(data_path_.c_str(), O_RDWR); - if (new_data_fd < 0) { - close(new_index_fd); - throw std::runtime_error("Cannot open new data file: " + data_path_); - } - - auto cleanup_fds = [&new_index_fd, &new_data_fd](const std::string& msg) { - close(new_index_fd); - close(new_data_fd); - throw std::runtime_error(msg); - }; - - struct stat index_st, data_st; - if (fstat(new_index_fd, &index_st)) { - cleanup_fds("Cannot get index file size: " + index_path_); - } - index_file_size_ = index_st.st_size; - - if (fstat(new_data_fd, &data_st)) { - cleanup_fds("Cannot get data file size: " + data_path_); - } - data_file_size_ = data_st.st_size; - - mapped_index_ = mmap(nullptr, index_file_size_, PROT_READ | PROT_WRITE, MAP_SHARED, new_index_fd, 0); - if (mapped_index_ == MAP_FAILED) { - cleanup_fds("Cannot map file: " + index_path_); - } - - mapped_data_ = mmap(nullptr, data_file_size_, PROT_READ | PROT_WRITE, MAP_SHARED, new_data_fd, 0); - if (mapped_data_ == MAP_FAILED) { - munmap(mapped_index_, index_file_size_); - close(new_index_fd); - close(new_data_fd); - throw std::runtime_error("Cannot map file: " + data_path_); - } - - index_fd_ = new_index_fd; - data_fd_ = new_data_fd; - - num_documents_ = compacted_count; - doc_id_map_ = std::move(new_doc_id_map); - } - - void Index::parse_index_header() { - const char* index_ptr = static_cast(mapped_index_); - size_t offset = 0; - - if (index_file_size_ < sizeof(IndexHeader)) { - throw std::runtime_error("Index file too small: insufficient data for header"); - } - - IndexHeader header; - header.magic = *reinterpret_cast(index_ptr + offset); - offset += sizeof(header.magic); - - if (header.magic != MAGIC) { - throw std::runtime_error("Invalid index file magic number"); - } - - header.version = *reinterpret_cast(index_ptr + offset); - offset += sizeof(header.version); - - if (header.version != VERSION) { - throw std::runtime_error("Index file version mismatch"); - } - - header.embedding_dim = *reinterpret_cast(index_ptr + offset); - offset += sizeof(header.embedding_dim); - - if (static_cast(header.embedding_dim) != embedding_dim_) { - throw std::runtime_error("Embedding dimension mismatch"); - } - - num_documents_ = *reinterpret_cast(index_ptr + offset); - } - - void Index::parse_data_header() { - const char* data_ptr = static_cast(mapped_data_); - size_t offset = 0; - - if (data_file_size_ < sizeof(DataHeader)) { - throw std::runtime_error("Data file too small: insufficient data for header"); - } - - DataHeader header; - header.magic = *reinterpret_cast(data_ptr + offset); - offset += sizeof(header.magic); - - if (header.magic != MAGIC) { - throw std::runtime_error("Invalid data file magic number"); - } - - header.version = *reinterpret_cast(data_ptr + offset); - offset += sizeof(header.version); - - if (header.version != VERSION) { - throw std::runtime_error("Data file version mismatch"); - } - } - - void Index::build_doc_id_map() { - const char* index_ptr = static_cast(mapped_index_); - const char* entries = index_ptr + sizeof(IndexHeader); - - if (num_documents_ > 0) { - size_t last_entry_offset = (num_documents_ - 1) * index_entry_size_; - if (sizeof(IndexHeader) + last_entry_offset + index_entry_size_ > index_file_size_) { - throw std::runtime_error("File corrupted: index entry extends beyond file size"); - } - } - - doc_id_map_.reserve(static_cast(num_documents_)); - - for (uint32_t i = 0; i < num_documents_; ++i) { - const IndexEntry& entry = *reinterpret_cast(entries + i * index_entry_size_); - - if (entry.flags & 0x1) { - continue; - } - - doc_id_map_[entry.doc_id] = i; - } - } - - void Index::validate_documents(const std::vector& documents) { - if (documents.empty()) { - throw std::runtime_error("Documents vector is empty"); - } - - std::unordered_set seen_ids; - for (const auto& doc : documents) { - if (doc.content.size() > 65535) { - throw std::runtime_error("Document content too long (max 65535 bytes) for ID: " + std::to_string(doc.id)); - } - - if (doc.metadata.size() > 65535) { - throw std::runtime_error("Document metadata too long (max 65535 bytes) for ID: " + std::to_string(doc.id)); - } - - if (doc.embedding.size() != embedding_dim_) { - throw std::runtime_error("Document embedding dimension mismatch for ID: " + std::to_string(doc.id)); - } - - if (seen_ids.find(doc.id) != seen_ids.end()) { - throw std::runtime_error("Duplicate document ID in input: " + std::to_string(doc.id)); - } - seen_ids.insert(doc.id); - - if (doc_id_map_.find(doc.id) != doc_id_map_.end()) { - throw std::runtime_error("Document ID already exists: " + std::to_string(doc.id)); - } - } - } - - void Index::validate_doc_ids(const std::vector& doc_ids) { - if (doc_ids.empty()) { - throw std::runtime_error("Document IDs vector is empty"); - } - - std::unordered_set seen_ids; - for (int doc_id : doc_ids) { - if (seen_ids.find(doc_id) != seen_ids.end()) { - throw std::runtime_error("Duplicate document ID in input: " + std::to_string(doc_id)); - } - seen_ids.insert(doc_id); - - if (doc_id_map_.find(doc_id) == doc_id_map_.end()) { - throw std::runtime_error("Document ID not found: " + std::to_string(doc_id)); - } - } - } - - ssize_t Index::write_full(int fd, const void* buf, size_t count) { - size_t written = 0; - const char* ptr = static_cast(buf); - while (written < count) { - ssize_t n = write(fd, ptr + written, count - written); - if (n < 0) { - if (errno == EINTR) continue; - return -1; - } - if (n == 0) return written; - written += n; - } - return written; - } - -} // namespace index -} // namespace engine -} // namespace cactus diff --git a/cactus/engine/engine_model.cpp b/cactus/engine/engine_model.cpp deleted file mode 100644 index da6b253db..000000000 --- a/cactus/engine/engine_model.cpp +++ /dev/null @@ -1,1102 +0,0 @@ -#include "engine.h" -#include "../models/model.h" -#include "../graph/graph.h" -#include "../npu/npu.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cactus { -namespace engine { - - -Model::Model() - : graph_handle_(nullptr), - config_(), - tokenizer_(nullptr), - initialized_(false), - attention_scale_(0.0f), - output_weight_node_id_(0), - owns_graph_(false) { -} - -Model::Model(const Config& config) - : graph_handle_(nullptr), - config_(config), - tokenizer_(nullptr), - initialized_(false), - attention_scale_(0.0f), - output_weight_node_id_(0), - owns_graph_(false) { -} - -Model::~Model() { - if (graph_handle_ && owns_graph_) { - delete static_cast(graph_handle_); - } -} - -bool Model::init(const std::string& model_folder, size_t context_size, const std::string& system_prompt, bool do_warmup) { - if (initialized_) { - return true; - } - auto* gb = new CactusGraph(); - graph_handle_ = gb; - owns_graph_ = true; - embedding_file_path_ = model_folder + "/token_embeddings.weights"; - return init_internal(gb, model_folder, context_size, system_prompt, do_warmup); -} - -bool Model::init(CactusGraph* external_graph, const std::string& model_folder, size_t context_size, - const std::string& system_prompt, bool do_warmup) { - if (!external_graph) { - throw std::invalid_argument("External graph pointer must not be null"); - } - if (initialized_) { - graph_handle_ = external_graph; - owns_graph_ = false; - return true; - } - - owns_graph_ = false; - graph_handle_ = external_graph; - return init_internal(external_graph, model_folder, context_size, system_prompt, do_warmup); -} - -bool Model::init_internal(CactusGraph* gb, const std::string& model_folder, size_t context_size, - const std::string& system_prompt, bool do_warmup) { - - CACTUS_LOG_DEBUG("model", "Initializing model from: " << model_folder); - model_folder_path_ = model_folder; - std::string config_path = model_folder + "/config.txt"; - - if (!config_.from_json(config_path)) { - CACTUS_LOG_ERROR("model", "Model initialization failed - config not loaded from: " << model_folder); - return false; - } - - std::string vocab_file = model_folder + "/vocab.txt"; - std::string merges_file = model_folder + "/merges.txt"; - std::string tokenizer_config_file = model_folder + "/tokenizer_config.txt"; - TokenizerRuntimeConfig tokenizer_runtime_config = load_tokenizer_runtime_config(tokenizer_config_file); - - std::ifstream merges_check(merges_file); - bool has_merges = false; - if (merges_check.is_open()) { - std::string line; - int line_count = 0; - while (std::getline(merges_check, line) && line_count < 10) { - if (!line.empty() && line[0] != '#') { - has_merges = true; - break; - } - line_count++; - } - merges_check.close(); - } - - if (tokenizer_runtime_config.tokenizer_type == TokenizerRuntimeConfig::TokenizerType::BPE || - (tokenizer_runtime_config.tokenizer_type == TokenizerRuntimeConfig::TokenizerType::UNKNOWN && has_merges)) { - tokenizer_ = std::make_unique(); - } else { - tokenizer_ = std::make_unique(); - } - - if (!tokenizer_->load_vocabulary_with_config(vocab_file, merges_file, tokenizer_config_file)) { - return false; - } - - graph_handle_ = gb; - - if(config_.model_type == Config::ModelType::WHISPER){ - embedding_file_path_ = model_folder+"/decoder_token_embeddings.weights"; - } - else{ - embedding_file_path_ = model_folder + "/token_embeddings.weights"; - } - - load_weights_to_graph(gb); - - if (config_.model_type == Config::ModelType::GEMMA3N || config_.model_type == Config::ModelType::GEMMA4) { - attention_scale_ = 1.0f; - } else if (config_.model_type == Config::ModelType::GEMMA) { - attention_scale_ = 1.0f / std::sqrt(256.0f); - } else { - attention_scale_ = 1.0f / std::sqrt(static_cast(config_.attention_head_dim)); - } - - Precision cache_precision = (config_.model_type == Config::ModelType::WHISPER || - config_.model_type == Config::ModelType::MOONSHINE || - config_.model_type == Config::ModelType::PARAKEET || - config_.model_type == Config::ModelType::PARAKEET_TDT || - config_.model_type == Config::ModelType::NEEDLE) - ? Precision::FP16 - : Precision::INT8; - kv_cache_.init(config_.num_layers, context_size, get_kv_layer_dims(), get_kv_layer_heads(), cache_precision); - - size_t window_size = std::min(context_size, size_t(512)); - size_t sink_size = 4; - const char* env_window = std::getenv("CACTUS_KV_WINDOW_SIZE"); - const char* env_sink = std::getenv("CACTUS_KV_SINK_SIZE"); - if (env_window) { - window_size = std::stoul(env_window); - } - if (env_sink) { - sink_size = std::stoul(env_sink); - } - kv_cache_.set_window_size(window_size, sink_size); - cache_k_output_nodes_.resize(config_.num_layers); - cache_v_output_nodes_.resize(config_.num_layers); - - post_init(); - - initialized_ = true; - - if (do_warmup && - config_.model_type != Config::ModelType::WHISPER && - config_.model_type != Config::ModelType::MOONSHINE && - config_.model_type != Config::ModelType::PARAKEET && - config_.model_type != Config::ModelType::PARAKEET_TDT && - config_.model_type != Config::ModelType::NEEDLE) { - std::string warmup_text = system_prompt.empty() ? "Hello" : system_prompt; - auto warmup_tokens = tokenizer_->encode(warmup_text); - if (config_.model_type == Config::ModelType::GEMMA4) { - warmup_tokens = {2}; - } - forward(warmup_tokens); - auto* gb = static_cast(graph_handle_); - gb->execute(); - } - - reset_cache(); - return true; -} - -size_t Model::forward(const std::vector& /*mel_bins*/, const std::vector& tokens, bool use_cache){ - return forward(tokens, use_cache); -} - -void Model::prefill(const std::vector& tokens, size_t chunk_size, const std::string& profile_file) { - if (tokens.empty()) { - return; - } - - if (has_npu_prefill()) { - size_t npu_chunk_size = static_cast(npu_prefill_->get_chunk_size()); - if (tokens.size() > npu_chunk_size) { - prefill_npu(tokens); - return; - } - } - - auto* gb = static_cast(graph_handle_); - - auto process_chunk = [&](const std::vector& chunk) { - forward(chunk, true); - gb->execute(profile_file); - post_execute_updates(gb, chunk.size()); - update_kv_cache(gb, chunk.size()); - }; - - if (tokens.size() <= chunk_size) { - process_chunk(tokens); - return; - } - - size_t num_full_chunks = (tokens.size() - 1) / chunk_size; - - for (size_t chunk_idx = 0; chunk_idx < num_full_chunks; ++chunk_idx) { - size_t start = chunk_idx * chunk_size; - size_t end = start + chunk_size; - std::vector chunk(tokens.begin() + start, tokens.begin() + end); - if (chunk_idx == 1) { - gb->set_prefill_mode(true); - } - process_chunk(chunk); - } - - gb->set_prefill_mode(false); - size_t final_start = num_full_chunks * chunk_size; - std::vector final_chunk(tokens.begin() + final_start, tokens.end()); - process_chunk(final_chunk); -} - -void Model::prefill_with_images(const std::vector& tokens, const std::vector& image_paths, - const std::string& profile_file) { - (void)image_paths; - prefill(tokens, get_prefill_chunk_size(), profile_file); -} - -uint32_t Model::decode(const std::vector& tokens, float temperature, float top_p, - size_t top_k, const std::string& profile_file, float* out_entropy, - float min_p, float repetition_penalty) { - - if (temperature < 0) { - temperature = config_.default_temperature; - } - if (top_p < 0) { - top_p = config_.default_top_p; - } - if (top_k == 0) { - top_k = config_.default_top_k; - } - auto final_hidden = forward(tokens, true); - - auto* gb = static_cast(graph_handle_); - auto backend = config_.default_backend == Config::Backend::CPU - ? ComputeBackend::CPU - : ComputeBackend::NPU; - - auto last_hidden = gb->index(final_hidden, tokens.size() - 1, 0); - const auto& last_hidden_buf = gb->get_output_buffer(last_hidden); - size_t hidden_dim = last_hidden_buf.shape[0]; - last_hidden = gb->reshape(last_hidden, {1, hidden_dim}); - - auto logits_node_id = gb->matmul(last_hidden, output_weight_node_id_, true, backend); - - if (config_.final_logit_softcapping > 0.0f) { - float inv_cap = 1.0f / config_.final_logit_softcapping; - logits_node_id = gb->scalar_multiply(logits_node_id, inv_cap); - logits_node_id = gb->tanh(logits_node_id); - logits_node_id = gb->scalar_multiply(logits_node_id, config_.final_logit_softcapping); - } - auto sampled_token_id = sample_token(gb, logits_node_id, temperature, top_p, top_k, min_p, repetition_penalty); - - gb->execute(profile_file); - - compute_entropy(gb, logits_node_id, out_entropy); - - post_execute_updates(gb, tokens.size()); - update_kv_cache(gb, tokens.size()); - - auto* output_ptr = gb->get_output(sampled_token_id); - uint32_t result_token = *static_cast(output_ptr); - record_sampled_token(result_token); - return result_token; -} - -size_t Model::sample_token(CactusGraph* gb, size_t logits_node_id, float temperature, float top_p, size_t top_k, - float min_p, float repetition_penalty, - const std::unordered_map* extra_bias) const { - auto combined_bias = tool_constrainer_.get_bias(); - for (const auto& [token_id, boost] : vocab_bias_) { - combined_bias[token_id] += boost; - } - if (extra_bias) { - for (const auto& [token_id, boost] : *extra_bias) { - combined_bias[token_id] += boost; - } - } - if (!token_history_.empty() && repetition_penalty > 1.0f && std::isfinite(repetition_penalty)) { - float log_penalty = std::log(repetition_penalty); - for (uint32_t tok : token_history_) { - combined_bias[tok] -= log_penalty; - } - } - return gb->sample_with_options(logits_node_id, temperature, top_p, min_p, 1.0f, top_k, combined_bias); -} - -void Model::compute_entropy(CactusGraph* gb, size_t logits_node_id, float* out_entropy) { - if (!out_entropy) return; - - const auto& logits_buf = gb->get_output_buffer(logits_node_id); - void* logits_ptr = gb->get_output(logits_node_id); - size_t vocab_size = logits_buf.shape.back(); - size_t seq_len = 1; - if (logits_buf.shape.size() >= 2) - seq_len = logits_buf.shape[logits_buf.shape.size() - 2]; - size_t row_offset = (seq_len > 0 ? (seq_len - 1) * vocab_size : 0); - - std::vector logits(vocab_size); - if (logits_buf.precision == Precision::FP32) { - float* src = static_cast(logits_ptr) + row_offset; - std::copy(src, src + vocab_size, logits.begin()); - } else if (logits_buf.precision == Precision::FP16) { - __fp16* src = static_cast<__fp16*>(logits_ptr) + row_offset; - Quantization::fp16_to_fp32(src, logits.data(), vocab_size); - } else { - int8_t* src = static_cast(logits_ptr) + row_offset; - Quantization::int8_to_fp32(src, logits.data(), vocab_size, 1.0f); - } - - float max_logit = *std::max_element(logits.begin(), logits.end()); - double sum_exp = 0.0; - for (size_t i = 0; i < vocab_size; ++i) - sum_exp += std::exp(static_cast(logits[i] - max_logit)); - double log_sum_exp = static_cast(max_logit) + std::log(sum_exp); - - double entropy = 0.0; - for (size_t i = 0; i < vocab_size; ++i) { - double log_prob = static_cast(logits[i]) - log_sum_exp; - double prob = std::exp(log_prob); - if (prob > 1e-10) - entropy -= prob * log_prob; - } - - double max_entropy = std::log(static_cast(vocab_size)); - *out_entropy = static_cast(entropy / max_entropy); -} - -uint32_t Model::decode_with_audio(const std::vector& tokens, const std::vector& /*mel_bins*/, float temperature, float top_p, size_t top_k, const std::string& profile_file, float* out_entropy, - float min_p, float repetition_penalty, - float* /*out_token_time_start*/, float* /*out_token_time_end*/){ - return decode(tokens, temperature, top_p, top_k, profile_file, out_entropy, min_p, repetition_penalty); -} - -uint32_t Model::decode_with_images(const std::vector& tokens, const std::vector& image_paths, - float temperature, float top_p, size_t top_k, const std::string& profile_file, float* out_entropy, - float min_p, float repetition_penalty) { - (void)image_paths; - return decode(tokens, temperature, top_p, top_k, profile_file, out_entropy, min_p, repetition_penalty); -} - -std::vector Model::get_image_embeddings(const std::string& /*image_path*/) { - throw std::runtime_error("Image embeddings not supported for this model type"); -} - -std::vector Model::get_audio_embeddings(const std::vector& /*mel_bins*/) { - throw std::runtime_error("Audio embeddings not supported for this model type"); -} - -void Model::update_kv_cache(CactusGraph* gb, size_t seq_len) { - kv_cache_.update_from_graph(gb, cache_k_output_nodes_, cache_v_output_nodes_, - seq_len, config_.num_layers); -} - -void Model::remove_thinking_tokens(const std::vector>& ranges) { - for (auto it = ranges.rbegin(); it != ranges.rend(); ++it) - kv_cache_.remove_token_range(it->first, it->second); -} - -std::vector Model::get_embeddings(const std::vector& tokens, bool pooled, bool normalize, const std::string& profile_file) { - std::vector embeddings; - auto final_hidden = forward(tokens); - - auto* gb = static_cast(graph_handle_); - auto* output_ptr = gb->get_output(final_hidden); - const auto& output_buffer = gb->get_output_buffer(final_hidden); - - if (pooled) { - auto pooled_hidden = gb->mean(final_hidden, 0); - - if (!profile_file.empty()) { - gb->execute(profile_file); - } else { - gb->execute(); - } - post_execute_updates(gb, tokens.size()); - auto* pooled_ptr = gb->get_output(pooled_hidden); - const auto& pooled_buffer = gb->get_output_buffer(pooled_hidden); - - size_t hidden_dim = pooled_buffer.total_size; - embeddings.resize(hidden_dim); - - if (pooled_buffer.precision == Precision::FP32) { - float* pooled_data = static_cast(pooled_ptr); - std::copy(pooled_data, pooled_data + hidden_dim, embeddings.begin()); - } else if (pooled_buffer.precision == Precision::FP16) { - __fp16* pooled_data = static_cast<__fp16*>(pooled_ptr); - Quantization::fp16_to_fp32(pooled_data, embeddings.data(), hidden_dim); - } else if (pooled_buffer.precision == Precision::INT8) { - int8_t* pooled_data = static_cast(pooled_ptr); - Quantization::int8_to_fp32(pooled_data, embeddings.data(), hidden_dim, 1.0f); - } - } else { - if (!profile_file.empty()) { - gb->execute(profile_file); - } else { - gb->execute(); - } - post_execute_updates(gb, tokens.size()); - - size_t total_size = output_buffer.total_size; - embeddings.resize(total_size); - - if (output_buffer.precision == Precision::FP32) { - float* hidden_states = static_cast(output_ptr); - std::copy(hidden_states, hidden_states + total_size, embeddings.begin()); - } else if (output_buffer.precision == Precision::FP16) { - __fp16* hidden_states = static_cast<__fp16*>(output_ptr); - for (size_t i = 0; i < total_size; i++) { - embeddings[i] = static_cast(hidden_states[i]); - } - } else if (output_buffer.precision == Precision::INT8) { - int8_t* hidden_states = static_cast(output_ptr); - for (size_t i = 0; i < total_size; i++) { - embeddings[i] = static_cast(hidden_states[i]); - } - } - } - - if (normalize && !embeddings.empty()) { - float norm_sq = 0.0f; - for (float v : embeddings) { - norm_sq += v * v; - } - float norm = std::sqrt(norm_sq); - if (norm > 1e-12f) { - float inv_norm = 1.0f / norm; - for (float& v : embeddings) { - v *= inv_norm; - } - } - } - - kv_cache_.reset(); - - return embeddings; -} - -bool Config::from_json(const std::string& config_path) { - std::ifstream file(config_path); - if (!file) { - CACTUS_LOG_ERROR("config", "Failed to open config file: " << config_path); - return false; - } - - std::string line; - while (std::getline(file, line)) { - if (line.empty() || line[0] == '#') continue; - - size_t eq_pos = line.find('='); - if (eq_pos == std::string::npos) continue; - - std::string key = line.substr(0, eq_pos); - std::string value = line.substr(eq_pos + 1); - - key.erase(0, key.find_first_not_of(" \t")); - key.erase(key.find_last_not_of(" \t") + 1); - value.erase(0, value.find_first_not_of(" \t")); - value.erase(value.find_last_not_of(" \t") + 1); - - if (key == "vocab_size") vocab_size = static_cast(std::stoul(value)); - else if (key == "bos_token_id") bos_token_id = static_cast(std::stoul(value)); - else if (key == "eos_token_id") eos_token_id = static_cast(std::stoul(value)); - else if (key == "num_layers") num_layers = static_cast(std::stoul(value)); - else if (key == "hidden_dim") hidden_dim = static_cast(std::stoul(value)); - else if (key == "ffn_intermediate_dim") ffn_intermediate_dim = static_cast(std::stoul(value)); - else if (key == "attention_heads") attention_heads = static_cast(std::stoul(value)); - else if (key == "attention_kv_heads") attention_kv_heads = static_cast(std::stoul(value)); - else if (key == "attention_head_dim") attention_head_dim = static_cast(std::stoul(value)); - else if (key == "layer_norm_eps") layer_norm_eps = std::stof(value); - else if (key == "rope_theta") rope_theta = std::stof(value); - else if (key == "num_experts") num_experts = static_cast(std::stoul(value)); - else if (key == "num_shared_experts") num_shared_experts = static_cast(std::stoul(value)); - else if (key == "num_top_experts") num_top_experts = static_cast(std::stoul(value)); - else if (key == "moe_every_n_layers") moe_every_n_layers = static_cast(std::stoul(value)); - else if (key == "moe_intermediate_dim" || key == "moe_intermediate_size") moe_intermediate_dim = static_cast(std::stoul(value)); - else if (key == "num_dense_layers") num_dense_layers = static_cast(std::stoul(value)); - else if (key == "num_experts_per_tok") num_experts_per_tok = static_cast(std::stoul(value)); - else if (key == "norm_topk_prob") norm_topk_prob = (value == "true" || value == "1"); - else if (key == "use_expert_bias") use_expert_bias = (value == "true" || value == "1"); - else if (key == "routed_scaling_factor") routed_scaling_factor = std::stof(value); - else if (key == "tie_word_embeddings") tie_word_embeddings = (value == "true" || value == "1"); - else if (key == "vision_hidden_dim" || key == "vision_hidden_size") vision_hidden_dim = static_cast(std::stoul(value)); - else if (key == "vision_num_layers") vision_num_layers = static_cast(std::stoul(value)); - else if (key == "vision_attention_heads") vision_attention_heads = static_cast(std::stoul(value)); - else if (key == "vision_image_size") vision_image_size = static_cast(std::stoul(value)); - else if (key == "vision_patch_size") vision_patch_size = static_cast(std::stoul(value)); - else if (key == "vision_num_channels") vision_num_channels = static_cast(std::stoul(value)); - else if (key == "vision_embed_dim") vision_embed_dim = static_cast(std::stoul(value)); - else if (key == "visual_tokens_per_img") visual_tokens_per_img = static_cast(std::stoul(value)); - else if (key == "use_pixel_shuffle") use_pixel_shuffle = (value == "true" || value == "1"); - else if (key == "pixel_shuffle_factor") pixel_shuffle_factor = static_cast(std::stoul(value)); - else if (key == "use_image_tokens") use_image_tokens = (value == "true" || value == "1"); - else if (key == "image_token_id") image_token_id = static_cast(std::stoul(value)); - else if (key == "use_layout_tags") use_layout_tags = (value == "true" || value == "1"); - else if (key == "image_seq_len") image_seq_len = static_cast(std::stoul(value)); - else if (key == "global_image_size") global_image_size = static_cast(std::stoul(value)); - else if (key == "max_tile_size") max_tile_size = static_cast(std::stoul(value)); - else if (key == "rescale_factor") rescale_factor = std::stof(value); - else if (key == "image_mean") image_mean = std::stof(value); - else if (key == "image_std") image_std = std::stof(value); - else if (key == "downsample_factor") downsample_factor = static_cast(std::stoul(value)); - else if (key == "min_tiles") min_tiles = static_cast(std::stoul(value)); - else if (key == "max_tiles") max_tiles = static_cast(std::stoul(value)); - else if (key == "use_thumbnail") use_thumbnail = (value == "true" || value == "1"); - else if (key == "min_image_tokens") min_image_tokens = static_cast(std::stoul(value)); - else if (key == "max_image_tokens") max_image_tokens = static_cast(std::stoul(value)); - else if (key == "tile_size") tile_size = static_cast(std::stoul(value)); - else if (key == "max_pixels_tolerance") max_pixels_tolerance = std::stof(value); - else if (key == "do_image_splitting") do_image_splitting = (value == "true" || value == "1"); - else if (key == "precision") { - if (value == "INT8") precision = Precision::INT8; - else if (value == "FP16") precision = Precision::FP16; - else precision = Precision::FP32; - } - else if (key == "model_type") { - std::string model_type_value = value; - std::transform(model_type_value.begin(), model_type_value.end(), model_type_value.begin(), ::tolower); - if (value == "gemma" || value == "GEMMA") model_type = ModelType::GEMMA; - else if (value == "lfm2" || value == "LFM2" || value == "lfm2_moe" || value == "LFM2_MOE") model_type = ModelType::LFM2; - else if (value == "bert" || value == "BERT") model_type = ModelType::NOMIC; - else if (value == "whisper" || value == "WHISPER") model_type = ModelType::WHISPER; - else if (value == "moonshine" || value == "MOONSHINE") model_type = ModelType::MOONSHINE; - else if (value == "silero_vad" || value == "SILERO_VAD") model_type = ModelType::SILERO_VAD; - else if (value == "parakeet" || value == "PARAKEET") model_type = ModelType::PARAKEET; - else if (model_type_value.rfind("qwen3_5", 0) == 0) model_type = ModelType::QWEN3P5; - else if (value == "parakeet_tdt" || value == "PARAKEET_TDT") model_type = ModelType::PARAKEET_TDT; - else if (value == "gemma3n" || value == "GEMMA3N") model_type = ModelType::GEMMA3N; - else if (value == "needle" || value == "NEEDLE") model_type = ModelType::NEEDLE; - else if (value == "gemma4" || value == "GEMMA4" || value == "tinyllama" || value == "TINYLLAMA") model_type = ModelType::GEMMA4; - else if (value == "youtu" || value == "YOUTU") model_type = ModelType::YOUTU; - else if (value == "pyannote" || value == "PYANNOTE") model_type = ModelType::PYANNOTE; - else if (value == "wespeaker" || value == "WESPEAKER") model_type = ModelType::WESPEAKER; - else model_type = ModelType::QWEN; - } - else if (key == "model_variant") { - std::string v = value; - std::transform(v.begin(), v.end(), v.begin(), ::tolower); - if (v == "vlm") model_variant = ModelVariant::VLM; - else if (v == "extract") model_variant = ModelVariant::EXTRACT; - else if (v == "rag") model_variant = ModelVariant::RAG; - else model_variant = ModelVariant::DEFAULT; - } - else if (key == "conv_L_cache") conv_L_cache = static_cast(std::stoul(value)); - else if (key == "layer_types") { - layer_types.clear(); - std::string sanitized; - sanitized.reserve(value.size()); - for (char c : value) { - if (c == '[' || c == ']' || c == '\'' || c == '"') { - continue; - } - sanitized.push_back(c); - } - std::stringstream ss(sanitized); - std::string item; - while (std::getline(ss, item, ',')) { - if (!item.empty()) { - item.erase(0, item.find_first_not_of(" \t")); - item.erase(item.find_last_not_of(" \t") + 1); - if (!item.empty()) layer_types.push_back(item); - } - } - } - else if (key == "enc_hidden_act") encoder_act_gelu = (value == "gelu"); - else if (key == "dec_hidden_act") decoder_act_gelu = (value == "gelu"); - else if (key == "num_encoder_layers") num_encoder_layers = static_cast(std::stoul(value)); - else if (key == "num_decoder_layers") num_decoder_layers = static_cast(std::stoul(value)); - else if (key == "partial_rotary_factor") partial_rotary_factor = std::stof(value); - else if (key == "pad_token_id") pad_token_id = static_cast(std::stoul(value)); - else if (key == "conv_kernel_size") conv_kernel_size = static_cast(std::stoul(value)); - else if (key == "subsampling_conv_kernel_size") subsampling_conv_kernel_size = static_cast(std::stoul(value)); - else if (key == "subsampling_conv_stride") subsampling_conv_stride = static_cast(std::stoul(value)); - else if (key == "subsampling_conv_channels") subsampling_conv_channels = static_cast(std::stoul(value)); - else if (key == "subsampling_factor") subsampling_factor = static_cast(std::stoul(value)); - else if (key == "num_mel_bins") num_mel_bins = static_cast(std::stoul(value)); - else if (key == "encoder_hidden_act") encoder_hidden_act = value; - else if (key == "linear_num_key_heads") linear_num_key_heads = static_cast(std::stoul(value)); - else if (key == "linear_key_head_dim") linear_key_head_dim = static_cast(std::stoul(value)); - else if (key == "linear_num_value_heads") linear_num_value_heads = static_cast(std::stoul(value)); - else if (key == "linear_value_head_dim") linear_value_head_dim = static_cast(std::stoul(value)); - else if (key == "linear_q_proj_dim") linear_q_proj_dim = static_cast(std::stoul(value)); - else if (key == "kv_lora_rank") kv_lora_rank = static_cast(std::stoul(value)); - else if (key == "q_lora_rank") q_lora_rank = static_cast(std::stoul(value)); - else if (key == "qk_head_dim") qk_head_dim = static_cast(std::stoul(value)); - else if (key == "qk_nope_head_dim") qk_nope_head_dim = static_cast(std::stoul(value)); - else if (key == "qk_rope_head_dim") qk_rope_head_dim = static_cast(std::stoul(value)); - else if (key == "v_head_dim") v_head_dim = static_cast(std::stoul(value)); - else if (key == "rope_interleave") rope_interleave = (value == "true" || value == "1"); - else if (key == "attention_bias") attention_bias = (value == "true" || value == "1"); - else if (key == "rope_scaling_factor") rope_scaling_factor = std::stof(value); - else if (key == "rope_mscale_all_dim") rope_mscale_all_dim = std::stof(value); - else if (key == "linear_k_proj_dim") linear_k_proj_dim = static_cast(std::stoul(value)); - else if (key == "linear_v_proj_dim") linear_v_proj_dim = static_cast(std::stoul(value)); - else if (key == "predictor_hidden_dim") predictor_hidden_dim = static_cast(std::stoul(value)); - else if (key == "predictor_num_layers") predictor_num_layers = static_cast(std::stoul(value)); - else if (key == "tdt_joint_dim") tdt_joint_dim = static_cast(std::stoul(value)); - else if (key == "tdt_num_durations") tdt_num_durations = static_cast(std::stoul(value)); - else if (key == "tdt_blank_id") tdt_blank_id = static_cast(std::stoul(value)); - else if (key == "tdt_durations") { - tdt_durations.clear(); - std::stringstream ss(value); - std::string item; - while (std::getline(ss, item, ',')) { - size_t first = item.find_first_not_of(" \t"); - if (first == std::string::npos) continue; - size_t last = item.find_last_not_of(" \t"); - item = item.substr(first, last - first + 1); - tdt_durations.push_back(static_cast(std::stoul(item))); - } - } - else if (key == "altup_num_inputs") altup_num_inputs = static_cast(std::stoul(value)); - else if (key == "laurel_rank") laurel_rank = static_cast(std::stoul(value)); - else if (key == "hidden_size_per_layer_input") hidden_size_per_layer_input = static_cast(std::stoul(value)); - else if (key == "num_kv_shared_layers") num_kv_shared_layers = static_cast(std::stoul(value)); - else if (key == "sliding_window") sliding_window = static_cast(std::stoul(value)); - else if (key == "rope_local_base_freq") rope_local_base_freq = std::stof(value); - else if (key == "final_logit_softcapping") final_logit_softcapping = std::stof(value); - else if (key == "global_partial_rotary_factor") global_partial_rotary_factor = std::stof(value); - else if (key == "expert_intermediate_size") expert_intermediate_size = static_cast(std::stoul(value)); - else if (key == "global_head_dim") global_head_dim = static_cast(std::stoul(value)); - else if (key == "num_global_kv_heads" || key == "num_global_key_value_heads") num_global_kv_heads = static_cast(std::stoul(value)); - else if (key == "attention_k_eq_v") attention_k_eq_v = (value == "true" || value == "1"); - else if (key == "enable_moe_block") enable_moe_block = (value == "true" || value == "1"); - else if (key == "vision_head_dim") vision_head_dim = static_cast(std::stoul(value)); - else if (key == "vision_kv_heads") vision_kv_heads = static_cast(std::stoul(value)); - else if (key == "vision_intermediate_size") vision_intermediate_size = static_cast(std::stoul(value)); - else if (key == "vision_position_embedding_size") vision_position_embedding_size = static_cast(std::stoul(value)); - else if (key == "vision_pooling_kernel_size") vision_pooling_kernel_size = static_cast(std::stoul(value)); - else if (key == "vision_default_output_length") vision_default_output_length = static_cast(std::stoul(value)); - else if (key == "vision_rope_theta") vision_rope_theta = std::stof(value); - else if (key == "audio_hidden_dim") audio_hidden_dim = static_cast(std::stoul(value)); - else if (key == "audio_num_layers") audio_num_layers = static_cast(std::stoul(value)); - else if (key == "audio_num_heads") audio_num_heads = static_cast(std::stoul(value)); - else if (key == "audio_head_dim") audio_head_dim = static_cast(std::stoul(value)); - else if (key == "audio_input_feat_size") audio_input_feat_size = static_cast(std::stoul(value)); - else if (key == "audio_conf_conv_kernel_size") audio_conf_conv_kernel_size = static_cast(std::stoul(value)); - else if (key == "audio_chunk_size") audio_chunk_size = static_cast(std::stoul(value)); - else if (key == "audio_context_left") audio_context_left = static_cast(std::stoul(value)); - else if (key == "audio_context_right") audio_context_right = static_cast(std::stoul(value)); - else if (key == "audio_logit_cap") audio_logit_cap = std::stof(value); - else if (key == "audio_residual_weight") audio_residual_weight = std::stof(value); - else if (key == "audio_output_proj_dims") audio_output_proj_dims = static_cast(std::stoul(value)); - else if (key == "audio_vocab_size") audio_vocab_size = static_cast(std::stoul(value)); - else if (key == "audio_vocab_offset") audio_vocab_offset = static_cast(std::stoul(value)); - else if (key == "audio_soft_tokens") audio_soft_tokens = static_cast(std::stoul(value)); - else if (key == "audio_sscp_conv0_channels") audio_sscp_conv0_channels = static_cast(std::stoul(value)); - else if (key == "audio_sscp_conv1_channels") audio_sscp_conv1_channels = static_cast(std::stoul(value)); - else if (key == "audio_sscp_conv_eps") audio_sscp_conv_eps = std::stof(value); - else if (key == "audio_rms_norm_eps") audio_rms_norm_eps = std::stof(value); - else if (key == "audio_fft_length") audio_fft_length = static_cast(std::stoul(value)); - else if (key == "audio_fft_overdrive") { - audio_fft_overdrive = (value == "true" || value == "1"); - audio_fft_length = audio_fft_overdrive ? 1024u : 512u; - } - else if (key == "audio_token_id") audio_token_id = static_cast(std::stoul(value)); - else if (key == "channel_open_token_id") channel_open_token_id = static_cast(std::stoul(value)); - else if (key == "channel_close_token_id") channel_close_token_id = static_cast(std::stoul(value)); - else if (key == "activation_sparsity_ppf") { - activation_sparsity_ppf.clear(); - std::stringstream ss(value); - std::string item; - while (std::getline(ss, item, ',')) { - size_t first = item.find_first_not_of(" \t"); - if (first == std::string::npos) continue; - size_t last = item.find_last_not_of(" \t"); - item = item.substr(first, last - first + 1); - activation_sparsity_ppf.push_back(std::stof(item)); - } - } - } - - if (is_gemma_family(model_type)) { - default_temperature = 1.0f; - default_top_p = 0.95f; - default_top_k = 64; - if (model_type == ModelType::GEMMA4) { - default_cloud_handoff_threshold = 0.92f; - default_rolling_entropy_window = 16; - } - } else if (model_type == ModelType::LFM2) { - default_temperature = 0.3f; - default_top_p = 0.95f; - default_top_k = 20; - } else if (model_type == ModelType::QWEN) { - default_temperature = 0.6f; - default_top_p = 0.95f; - default_top_k = 20; - } else if (model_type == ModelType::QWEN3P5) { - default_temperature = 0.7f; - default_top_p = 0.8f; - default_top_k = 20; - } else if (model_type == ModelType::WHISPER) { - default_temperature = 0.0f; - default_top_p = 0.0f; - default_top_k = 0; - default_cloud_handoff_threshold = 0.4f; - } else if (model_type == ModelType::MOONSHINE) { - default_temperature = 0.0f; - default_top_p = 0.0f; - default_top_k = 0; - default_max_tps = 6.5f; - default_cloud_handoff_threshold = 0.35f; - } else if (model_type == ModelType::PARAKEET) { - default_temperature = 0.0f; - default_top_p = 0.0f; - default_top_k = 0; - default_max_tps = 8.0f; - default_cloud_handoff_threshold = 0.35f; - } else if (model_type == ModelType::PARAKEET_TDT) { - default_temperature = 0.0f; - default_top_p = 0.0f; - default_top_k = 0; - default_max_tps = 8.0f; - default_cloud_handoff_threshold = 0.35f; - } else if (model_type == ModelType::NEEDLE) { - default_temperature = 0.0f; - default_top_p = 0.0f; - default_top_k = 0; - } else if (model_type == ModelType::YOUTU) { - default_temperature = 1.0f; - default_top_p = 0.95f; - default_top_k = 20; - } - - return true; -} - -std::string Config::to_json() const { - return "{}"; -} - -std::unique_ptr create_model(const std::string& model_folder) { - CACTUS_LOG_DEBUG("model", "Creating model from: " << model_folder); - Config config; - std::string config_path = model_folder + "/config.txt"; - - if (!config.from_json(config_path)) { - CACTUS_LOG_ERROR("model", "Failed to create model - cannot load config from: " << model_folder); - return nullptr; - } - - const bool has_vision_support = - config.use_image_tokens || - config.vision_num_layers > 0 || - config.vision_embed_dim > 0 || - config.vision_hidden_dim > 0 || - config.visual_tokens_per_img > 0; - - if (config.model_type == Config::ModelType::LFM2 && has_vision_support) { - return std::make_unique(config); - } - - const bool has_audio_support = - config.audio_num_layers > 0 || - config.audio_hidden_dim > 0; - - if (config.model_type == Config::ModelType::GEMMA4 && (has_vision_support || has_audio_support)) { - return std::make_unique(config); - } - - switch (config.model_type) { - case Config::ModelType::QWEN: - return std::make_unique(config); - case Config::ModelType::QWEN3P5: - return std::make_unique(config); - case Config::ModelType::GEMMA: - return std::make_unique(config); - case Config::ModelType::GEMMA3N: - return std::make_unique(config); - case Config::ModelType::LFM2: - if (config.num_experts > 0 && config.moe_intermediate_dim > 0 && config.num_experts_per_tok > 0) { - return std::make_unique(config); - } - return std::make_unique(config); - case Config::ModelType::NOMIC: - return std::make_unique(config); - case Config::ModelType::WHISPER: - return std::make_unique(config); - case Config::ModelType::MOONSHINE: - return std::make_unique(config); - case Config::ModelType::SILERO_VAD: - return std::make_unique(config); - case Config::ModelType::PARAKEET: - return std::make_unique(config); - case Config::ModelType::PARAKEET_TDT: - return std::make_unique(config); - case Config::ModelType::NEEDLE: - return std::make_unique(config); - case Config::ModelType::GEMMA4: - return std::make_unique(config); - case Config::ModelType::YOUTU: - return std::make_unique(config); - case Config::ModelType::PYANNOTE: - return std::make_unique(config); - case Config::ModelType::WESPEAKER: - return std::make_unique(config); - default: - return std::make_unique(config); - } -} - -void Model::capture_debug_node(uint32_t layer_idx, const std::string& name, size_t node_id) const { - auto* graph = static_cast(graph_handle_); - if (!graph) { - return; - } - graph->capture_debug_node(layer_idx, name, node_id); -} - -void Model::clear_debug_nodes() { - auto* graph = static_cast(graph_handle_); - if (!graph) { - return; - } - graph->clear_debug_nodes(); -} - -const std::vector& Model::get_debug_nodes() const { - auto* graph = static_cast(graph_handle_); - debug_nodes_.clear(); - if (!graph) { - return debug_nodes_; - } - - const auto& entries = graph->get_debug_nodes(); - debug_nodes_.reserve(entries.size()); - for (const auto& entry : entries) { - debug_nodes_.push_back({entry.layer_idx, entry.name, entry.node_id}); - } - return debug_nodes_; -} - -bool Model::load_npu_prefill(const std::string& model_path) { - CACTUS_LOG_DEBUG("npu", "Attempting to load NPU prefill from: " << model_path); - - npu_prefill_ = npu::create_prefill(); - if (!npu_prefill_) { - CACTUS_LOG_DEBUG("npu", "NPU prefill creation failed (not supported on this device)"); - return false; - } - - bool loaded = npu_prefill_->load(model_path); - if (loaded) { - CACTUS_LOG_INFO("npu", "NPU prefill loaded successfully from: " << model_path); - } else { - CACTUS_LOG_DEBUG("npu", "NPU prefill model not found at: " << model_path); - } - return loaded; -} - -bool Model::has_npu_prefill() const { - return npu_prefill_ && npu_prefill_->is_available(); -} - -size_t Model::get_prefill_chunk_size() const { - if (has_npu_prefill()) { - return static_cast(npu_prefill_->get_chunk_size()); - } - return 256; // default chunk size -} - -std::vector<__fp16> Model::get_token_embeddings(const std::vector& tokens) { - auto* gb = static_cast(graph_handle_); - if (!gb || tokens.empty()) { - return {}; - } - - gb->soft_reset(); - - size_t tok_input = gb->input({tokens.size()}, Precision::FP32); - std::vector tok_f(tokens.size()); - for (size_t i = 0; i < tokens.size(); i++) { - tok_f[i] = static_cast(tokens[i]); - } - gb->set_input(tok_input, tok_f.data(), Precision::FP32); - - size_t embedding_node = gb->embedding(embedding_node_id_, tok_input); - - gb->execute(); - - const auto& emb_buf = gb->get_output_buffer(embedding_node); - void* emb_ptr = gb->get_output(embedding_node); - - size_t num_tokens = tokens.size(); - size_t hidden_dim = config_.hidden_dim; - std::vector<__fp16> embeddings(num_tokens * hidden_dim); - - if (emb_buf.precision == Precision::FP16) { - __fp16* src = static_cast<__fp16*>(emb_ptr); - std::copy(src, src + num_tokens * hidden_dim, embeddings.begin()); - } else if (emb_buf.precision == Precision::FP32) { - float* src = static_cast(emb_ptr); - for (size_t i = 0; i < num_tokens * hidden_dim; i++) { - embeddings[i] = static_cast<__fp16>(src[i]); - } - } else if (emb_buf.precision == Precision::INT8) { - int8_t* src = static_cast(emb_ptr); - for (size_t i = 0; i < num_tokens * hidden_dim; i++) { - embeddings[i] = static_cast<__fp16>(src[i]); - } - } - - return embeddings; -} - -void Model::prefill_npu(const std::vector& tokens) { - if (!npu_prefill_ || !npu_prefill_->is_available()) { - throw std::runtime_error("NPU prefill not available"); - } - - const int chunk_size = npu_prefill_->get_chunk_size(); - const int hidden_dim = npu_prefill_->get_hidden_dim(); - const int num_layers = npu_prefill_->get_num_layers(); - const int fallback_num_kv_heads = npu_prefill_->get_num_kv_heads(); - const int fallback_head_dim = npu_prefill_->get_head_dim(); - - const std::vector layer_dims = get_kv_layer_dims(); - const std::vector layer_heads = get_kv_layer_heads(); - const int layers_to_update = std::min(num_layers, static_cast(config_.num_layers)); - - std::vector<__fp16> all_embeddings = get_token_embeddings(tokens); - if (all_embeddings.empty()) { - throw std::runtime_error("Failed to get token embeddings for NPU prefill"); - } - - if (Config::is_gemma_family(config_.model_type)) { - float scale = std::sqrt(static_cast(hidden_dim)); - for (size_t i = 0; i < all_embeddings.size(); i++) { - all_embeddings[i] = __fp16(static_cast(all_embeddings[i]) * scale); - } - } - - size_t num_tokens = tokens.size(); - size_t num_chunks = (num_tokens + chunk_size - 1) / chunk_size; - - for (size_t c = 0; c < num_chunks; c++) { - size_t start = c * chunk_size; - size_t actual_tokens = std::min(static_cast(chunk_size), num_tokens - start); - - std::vector<__fp16> chunk_embeddings(chunk_size * hidden_dim, __fp16(0)); - std::copy(all_embeddings.begin() + start * hidden_dim, - all_embeddings.begin() + (start + actual_tokens) * hidden_dim, - chunk_embeddings.begin()); - - int position_offset = static_cast(start); - - npu::NPUPrefillDirectResult direct_result = npu_prefill_->prefill_chunk_direct(chunk_embeddings, position_offset); - - if (direct_result.valid) { - for (int layer_idx = 0; layer_idx < layers_to_update; layer_idx++) { - const auto& k_ref = direct_result.k_caches[layer_idx]; - const auto& v_ref = direct_result.v_caches[layer_idx]; - - if (k_ref.data && v_ref.data) { - size_t layer_kv_heads = layer_idx < static_cast(layer_heads.size()) - ? layer_heads[layer_idx] - : static_cast(fallback_num_kv_heads); - size_t layer_head_dim = layer_idx < static_cast(layer_dims.size()) - ? layer_dims[layer_idx] - : static_cast(fallback_head_dim); - - size_t expected = static_cast(chunk_size) * layer_kv_heads * layer_head_dim; - if (expected > 0 && (k_ref.count < expected || v_ref.count < expected)) { - CACTUS_LOG_WARN( - "npu", - "NPU prefill cache output too small for layer " << layer_idx - << " (expected>=" << expected - << ", got k=" << k_ref.count << ", v=" << v_ref.count << "); skipping layer"); - continue; - } - - kv_cache_.update_from_npu(layer_idx, k_ref.data, v_ref.data, - actual_tokens, layer_kv_heads, layer_head_dim); - } - } - } - } -} - -double Model::score_tokens_window_logprob( - const std::vector& tokens, - size_t start, - size_t end, - size_t context, - size_t* tokens_scored -) { - if (tokens_scored) - *tokens_scored = 0; - - if (tokens.empty()) - return 0.0; - - if (end > tokens.size()) - end = tokens.size(); - - if (start >= end) - return 0.0; - - if (start == 0) - start = 1; - - if (start >= end) - return 0.0; - - const size_t target_len = end - start; - const size_t ctx_begin = (start > context) ? (start - context) : 0; - - if (end < 2) return 0.0; - const size_t input_end = end - 1; - - if (input_end <= ctx_begin) - return 0.0; - - std::vector input_tokens(tokens.begin() + ctx_begin,tokens.begin() + input_end); - - if (tokens_scored) - *tokens_scored = target_len; - - reset_cache(); - - auto* gb = static_cast(graph_handle_); - const auto backend = (config_.default_backend == Config::Backend::CPU) ? ComputeBackend::CPU : ComputeBackend::NPU; - - const size_t hidden_node = forward(input_tokens, /*use_cache=*/false); - const auto& hidden_buf = gb->get_output_buffer(hidden_node); - - if (hidden_buf.shape.size() != 2) { - throw std::runtime_error("Expected hidden to be rank-2 [L, hidden_dim]"); - } - - - const size_t first_pos = start - ctx_begin - 1; - const size_t hidden_slice = gb->slice(hidden_node, /*axis=*/0, first_pos, target_len); - bool transpose_w = true; - const size_t logits_node = gb->matmul(hidden_slice, output_weight_node_id_, transpose_w, backend); - gb->execute(); - - const auto& logits_buf = gb->get_output_buffer(logits_node); - if (logits_buf.shape.size() != 2) - throw std::runtime_error("Expected logits to be rank-2 [T, vocab]"); - - const size_t T = logits_buf.shape[0]; - const size_t vocab_size = logits_buf.shape[1]; - - if (T != target_len) - throw std::runtime_error("Logits T dimension does not match target_len"); - - void* logits_ptr = gb->get_output(logits_node); - std::vector row(vocab_size); - double total_logprob = 0.0; - - for (size_t i = 0; i < target_len; ++i) { - const uint32_t y = tokens[start + i]; - if (y >= vocab_size) - throw std::runtime_error("Target token out of vocab range"); - - if (logits_buf.precision == Precision::FP32) { - const float* src = static_cast(logits_ptr) + i * vocab_size; - std::memcpy(row.data(), src, vocab_size * sizeof(float)); - } - else if (logits_buf.precision == Precision::FP16) { - const __fp16* src = static_cast(logits_ptr) + i * vocab_size; - Quantization::fp16_to_fp32(const_cast<__fp16*>(src), row.data(), vocab_size); - } - else { - const int8_t* src = static_cast(logits_ptr) + i * vocab_size; - Quantization::int8_to_fp32(const_cast(src), row.data(), vocab_size, 1.0f); - } - - float max_logit = *std::max_element(row.begin(), row.end()); - double sum = 0.0; - - for (size_t j = 0; j < vocab_size; ++j) - sum += std::exp(double(row[j] - max_logit)); - - const double lse = double(max_logit) + std::log(sum); - total_logprob += double(row[y]) - lse; - } - - return total_logprob; -} -} -} diff --git a/cactus/engine/engine_signals.cpp b/cactus/engine/engine_signals.cpp deleted file mode 100644 index 38468b4c0..000000000 --- a/cactus/engine/engine_signals.cpp +++ /dev/null @@ -1,775 +0,0 @@ -#include "engine.h" -#include "../kernel/kernel_utils.h" -#include -#include -#include -#include -#include -#include -#include - -#ifdef __APPLE__ -#include -#endif - -#ifndef M_PI -#define M_PI 3.14159265358979323846 -#endif - -namespace cactus { -namespace engine { - -static void to_db( - float* spectrogram, - size_t size, - float reference, - float min_value, - const float* db_range, - float multiplier) -{ - if (reference <= 0.0f) { - throw std::invalid_argument("reference must be greater than zero"); - } - if (min_value <= 0.0f) { - throw std::invalid_argument("min_value must be greater than zero"); - } - - reference = std::max(min_value, reference); - const float log_ref = std::log10(reference); - - CactusThreading::parallel_for(size, CactusThreading::Thresholds::ALL_REDUCE, [&](size_t start, size_t end) { - for (size_t i = start; i < end; i++) { - float value = std::max(min_value, spectrogram[i]); - spectrogram[i] = multiplier * (std::log10(value) - log_ref); - } - }); - - if (db_range != nullptr) { - if (*db_range <= 0.0f) { - throw std::invalid_argument("db_range must be greater than zero"); - } - - float max_db = CactusThreading::parallel_reduce, float, std::function>( - size, CactusThreading::Thresholds::ALL_REDUCE, - [&](size_t start, size_t end) { - float local_max = -std::numeric_limits::infinity(); - for (size_t i = start; i < end; i++) { - local_max = std::max(local_max, spectrogram[i]); - } - return local_max; - }, - -std::numeric_limits::infinity(), - [](float a, float b) { return std::max(a, b); } - ); - - float min_db = max_db - *db_range; - CactusThreading::parallel_for(size, CactusThreading::Thresholds::ALL_REDUCE, [&](size_t start, size_t end) { - for (size_t i = start; i < end; i++) { - spectrogram[i] = std::max(min_db, spectrogram[i]); - } - }); - } -} - -static size_t bit_reverse(size_t x, size_t log2n) { - size_t result = 0; - for (size_t i = 0; i < log2n; i++) { - result = (result << 1) | (x & 1); - x >>= 1; - } - return result; -} - -static bool is_power_of_two(size_t n) { - return n != 0 && (n & (n - 1)) == 0; -} - -static size_t log2_power_of_two(size_t n) { - size_t log2n = 0; - for (size_t temp = n; temp > 1; temp >>= 1) { - log2n++; - } - return log2n; -} - -static void fft_radix2(float* re, float* im, size_t n, bool inverse) { - if (!is_power_of_two(n)) return; - - const size_t log2n = log2_power_of_two(n); - - for (size_t i = 0; i < n; i++) { - size_t j = bit_reverse(i, log2n); - if (i < j) { - std::swap(re[i], re[j]); - std::swap(im[i], im[j]); - } - } - - for (size_t s = 1; s <= log2n; s++) { - size_t m = size_t{1} << s; - size_t m2 = m >> 1; - float w_re = 1.0f; - float w_im = 0.0f; - const float angle = static_cast(M_PI) / static_cast(m2); - const float wm_re = std::cos(angle); - const float wm_im = inverse ? std::sin(angle) : -std::sin(angle); - - for (size_t j = 0; j < m2; j++) { - for (size_t k = j; k < n; k += m) { - size_t k_m2 = k + m2; - float t_re = w_re * re[k_m2] - w_im * im[k_m2]; - float t_im = w_re * im[k_m2] + w_im * re[k_m2]; - float u_re = re[k]; - float u_im = im[k]; - re[k] = u_re + t_re; - im[k] = u_im + t_im; - re[k_m2] = u_re - t_re; - im[k_m2] = u_im - t_im; - } - float new_w_re = w_re * wm_re - w_im * wm_im; - float new_w_im = w_re * wm_im + w_im * wm_re; - w_re = new_w_re; - w_im = new_w_im; - } - } -} - -enum class FFTNorm { - BACKWARD, - FORWARD, - ORTHO -}; - -static FFTNorm parse_fft_norm(const char* norm) { - if (!norm || std::strcmp(norm, "backward") == 0) { - return FFTNorm::BACKWARD; - } - if (std::strcmp(norm, "forward") == 0) { - return FFTNorm::FORWARD; - } - if (std::strcmp(norm, "ortho") == 0) { - return FFTNorm::ORTHO; - } - throw std::invalid_argument("norm must be one of {\"backward\",\"forward\",\"ortho\"}"); -} - -static size_t next_power_of_two(size_t n) { - size_t padded_n = 1; - while (padded_n < n) { - padded_n <<= 1; - } - return padded_n; -} - -static float fft_norm_factor(size_t n, FFTNorm norm_mode, bool inverse) { - if (norm_mode == FFTNorm::ORTHO) { - return 1.0f / std::sqrt(static_cast(n)); - } - if (norm_mode == FFTNorm::FORWARD) { - return inverse ? 1.0f : (1.0f / static_cast(n)); - } - return inverse ? (1.0f / static_cast(n)) : 1.0f; -} - -static void ensure_irfft_scratch(std::vector& re, std::vector& im, size_t n) { - if (re.size() < n) { - re.resize(n); - } - if (im.size() < n) { - im.resize(n); - } -} - -static void idft_r_f32_1d_dft( - const float* input, - float* output, - const size_t n, - const float norm_factor) { - const size_t in_len = n / 2 + 1; - const float two_pi_over_n = (2.0f * static_cast(M_PI)) / static_cast(n); - for (size_t t = 0; t < n; ++t) { - float sum = input[0]; - for (size_t k = 1; k < in_len; ++k) { - const float re = input[k * 2]; - const float im = input[k * 2 + 1]; - const float angle = two_pi_over_n * static_cast(k * t); - const float c = std::cos(angle); - const float s = std::sin(angle); - const bool self_conjugate = (k * 2 == n); - if (self_conjugate) { - sum += re * c; - } else { - sum += 2.0f * (re * c - im * s); - } - } - output[t] = sum * norm_factor; - } -} - -static void rfft_f32_1d(const float* input, float* output, const size_t n, const char* norm) { - const size_t out_len = n / 2 + 1; - const FFTNorm norm_mode = parse_fft_norm(norm); - const float norm_factor = fft_norm_factor(n, norm_mode, false); - - if (n == 1) { - output[0] = input[0] * norm_factor; - output[1] = 0.0f; - return; - } - -#ifdef __APPLE__ - { - size_t log2n = 0; - size_t padded_n = 1; - while (padded_n < n) { - padded_n <<= 1; - log2n++; - } - - FFTSetup fft_setup = vDSP_create_fftsetup(log2n, FFT_RADIX2); - if (fft_setup) { - std::vector buffer(padded_n, 0.0f); - std::copy(input, input + n, buffer.begin()); - - DSPSplitComplex split; - std::vector real_part(padded_n / 2); - std::vector imag_part(padded_n / 2); - split.realp = real_part.data(); - split.imagp = imag_part.data(); - - vDSP_ctoz(reinterpret_cast(buffer.data()), 2, &split, 1, padded_n / 2); - - vDSP_fft_zrip(fft_setup, &split, 1, log2n, FFT_FORWARD); - - float scale = 0.5f * norm_factor; - - output[0] = split.realp[0] * scale * 2.0f; - output[1] = 0.0f; - - if (out_len > 1) { - size_t nyquist_idx = padded_n / 2; - if (nyquist_idx < out_len) { - output[nyquist_idx * 2] = split.imagp[0] * scale * 2.0f; - output[nyquist_idx * 2 + 1] = 0.0f; - } - } - - for (size_t i = 1; i < out_len && i < padded_n / 2; i++) { - output[i * 2] = split.realp[i] * scale * 2.0f; - output[i * 2 + 1] = split.imagp[i] * scale * 2.0f; - } - - vDSP_destroy_fftsetup(fft_setup); - return; - } - } -#endif - - const size_t padded_n = next_power_of_two(n); - - std::vector re(padded_n, 0.0f), im(padded_n, 0.0f); - std::copy(input, input + n, re.begin()); - - fft_radix2(re.data(), im.data(), padded_n, false); - - for (size_t i = 0; i < out_len; i++) { - output[i * 2] = re[i] * norm_factor; - output[i * 2 + 1] = im[i] * norm_factor; - } -} - -static void idft_r_f32_1d(const float* input, float* output, const size_t n, const char* norm) { - const size_t in_len = n / 2 + 1; - const FFTNorm norm_mode = parse_fft_norm(norm); - const float norm_factor = fft_norm_factor(n, norm_mode, true); - - if (n == 1) { - output[0] = input[0] * norm_factor; - return; - } - - if (!is_power_of_two(n)) { - idft_r_f32_1d_dft(input, output, n, norm_factor); - return; - } - - const size_t padded_n = next_power_of_two(n); - const size_t padded_half_plus_one = padded_n / 2 + 1; - thread_local std::vector re_scratch; - thread_local std::vector im_scratch; - ensure_irfft_scratch(re_scratch, im_scratch, padded_n); - std::fill(re_scratch.begin(), re_scratch.begin() + padded_n, 0.0f); - std::fill(im_scratch.begin(), im_scratch.begin() + padded_n, 0.0f); - float* re = re_scratch.data(); - float* im = im_scratch.data(); - for (size_t i = 0; i < in_len; ++i) { - re[i] = input[i * 2]; - im[i] = input[i * 2 + 1]; - } - - im[0] = 0.0f; - const bool has_padded_nyquist_bin = in_len == padded_half_plus_one; - if (has_padded_nyquist_bin) { - im[padded_n / 2] = 0.0f; - } - - const size_t mirror_limit = has_padded_nyquist_bin ? (in_len - 1) : in_len; - for (size_t i = 1; i < mirror_limit; ++i) { - const size_t mirrored = padded_n - i; - re[mirrored] = re[i]; - im[mirrored] = -im[i]; - } - - fft_radix2(re, im, padded_n, true); - - for (size_t i = 0; i < n; ++i) { - output[i] = re[i] * norm_factor; - } -} - -static float hertz_to_mel(float freq, const char* mel_scale) { - if (std::strcmp(mel_scale, "htk") == 0) { - return 2595.0f * std::log10(1.0f + (freq / 700.0f)); - } else if (std::strcmp(mel_scale, "kaldi") == 0) { - return 1127.0f * std::log(1.0f + (freq / 700.0f)); - } - - const float min_log_hertz = 1000.0f; - const float min_log_mel = 15.0f; - const float logstep = 27.0f / std::log(6.4f); - float mels = 3.0f * freq / 200.0f; - - if (freq >= min_log_hertz) { - mels = min_log_mel + std::log(freq / min_log_hertz) * logstep; - } - - return mels; -} - -static float mel_to_hertz(float mels, const char* mel_scale) { - if (std::strcmp(mel_scale, "htk") == 0) { - return 700.0f * (std::pow(10.0f, mels / 2595.0f) - 1.0f); - } else if (std::strcmp(mel_scale, "kaldi") == 0) { - return 700.0f * (std::exp(mels / 1127.0f) - 1.0f); - } - - const float min_log_hertz = 1000.0f; - const float min_log_mel = 15.0f; - const float logstep = std::log(6.4f) / 27.0f; - float freq = 200.0f * mels / 3.0f; - - if (mels >= min_log_mel) { - freq = min_log_hertz * std::exp(logstep * (mels - min_log_mel)); - } - - return freq; -} - -static void generate_mel_filter_bank( - float* mel_filters, - const int num_frequency_bins, - const int num_mel_filters, - const float min_frequency, - const float max_frequency, - const int sampling_rate, - const char* norm, - const char* mel_scale, - const bool triangularize_in_mel_space) -{ - if (norm != nullptr && std::strcmp(norm, "slaney") != 0) { - throw std::invalid_argument("norm must be one of None or \"slaney\""); - } - - if (std::strcmp(mel_scale, "htk") != 0 && std::strcmp(mel_scale, "kaldi") != 0 && std::strcmp(mel_scale, "slaney") != 0) { - throw std::invalid_argument("mel_scale should be one of \"htk\", \"slaney\" or \"kaldi\"."); - } - - if (num_frequency_bins < 2) { - throw std::invalid_argument( - "Require num_frequency_bins: " + std::to_string(num_frequency_bins) + " >= 2"); - } - - if (min_frequency > max_frequency) { - throw std::invalid_argument( - "Require min_frequency: " + std::to_string(min_frequency) + - " <= max_frequency: " + std::to_string(max_frequency)); - } - - const float mel_min = hertz_to_mel(min_frequency, mel_scale); - const float mel_max = hertz_to_mel(max_frequency, mel_scale); - - std::vector mel_freqs(num_mel_filters + 2); - for (int i = 0; i < num_mel_filters + 2; i++) { - mel_freqs[i] = mel_min + (mel_max - mel_min) * i / (num_mel_filters + 1); - } - - std::vector filter_freqs(num_mel_filters + 2); - for (int i = 0; i < num_mel_filters + 2; i++) { - filter_freqs[i] = mel_to_hertz(mel_freqs[i], mel_scale); - } - - std::vector fft_freqs(num_frequency_bins); - if (triangularize_in_mel_space) { - float fft_bin_width = static_cast(sampling_rate) / ((num_frequency_bins - 1) * 2); - for (int i = 0; i < num_frequency_bins; i++) { - fft_freqs[i] = hertz_to_mel(fft_bin_width * i, mel_scale); - } - filter_freqs = mel_freqs; - } else { - for (int i = 0; i < num_frequency_bins; i++) { - fft_freqs[i] = (static_cast(sampling_rate) / 2.0f) * i / (num_frequency_bins - 1); - } - } - - for (int i = 0; i < num_mel_filters; i++) { - float left_edge = filter_freqs[i]; - float center = filter_freqs[i + 1]; - float right_edge = filter_freqs[i + 2]; - - for (int j = 0; j < num_frequency_bins; j++) { - float freq = fft_freqs[j]; - float down_slope = (freq - left_edge) / (center - left_edge); - float up_slope = (right_edge - freq) / (right_edge - center); - - mel_filters[i * num_frequency_bins + j] = std::max(0.0f, std::min(down_slope, up_slope)); - } - } - - if (norm != nullptr && std::strcmp(norm, "slaney") == 0) { - for (int i = 0; i < num_mel_filters; i++) { - float enorm = 2.0f / (filter_freqs[i + 2] - filter_freqs[i]); - for (int j = 0; j < num_frequency_bins; j++) { - mel_filters[i * num_frequency_bins + j] *= enorm; - } - } - } -} - -static void compute_spectrogram_f32( - const float* waveform, - size_t waveform_length, - const float* window, - size_t window_length, - size_t frame_length, - size_t hop_length, - const size_t* fft_length, - float* spectrogram, - float power, - bool center, - const char* pad_mode, - bool onesided [[maybe_unused]], - float dither, - const float* preemphasis, - const float* mel_filters, - size_t mel_filters_size, - float mel_floor, - const char* log_mel, - float reference, - float min_value, - const float* db_range, - bool remove_dc_offset) -{ - size_t actual_fft_length; - if (fft_length == nullptr) { - actual_fft_length = frame_length; - } else { - actual_fft_length = *fft_length; - } - - if (frame_length > actual_fft_length) { - throw std::invalid_argument( - "frame_length (" + std::to_string(frame_length) + - ") may not be larger than fft_length (" + - std::to_string(actual_fft_length) + ")"); - } - - std::vector hann_window; - const float* actual_window = window; - - if (window == nullptr) { - size_t length = frame_length + 1; - hann_window.resize(frame_length); - for (size_t i = 0; i < frame_length; i++) { - hann_window[i] = 0.5f * (1.0f - std::cos(2.0f * static_cast(M_PI) * i / (length - 1))); - } - actual_window = hann_window.data(); - } else if (window_length != frame_length) { - throw std::invalid_argument( - "Length of the window (" + std::to_string(window_length) + - ") must equal frame_length (" + std::to_string(frame_length) + ")"); - } - - if (hop_length <= 0) { - throw std::invalid_argument("hop_length must be greater than zero"); - } - - if (power == 0.0f && mel_filters != nullptr) { - throw std::invalid_argument( - "You have provided `mel_filters` but `power` is `None`. " - "Mel spectrogram computation is not yet supported for complex-valued spectrogram. " - "Specify `power` to fix this issue."); - } - - std::vector padded_waveform; - const float* input_waveform = waveform; - size_t input_length = waveform_length; - - if (center) { - size_t pad_length = frame_length / 2; - size_t padded_length = waveform_length + 2 * pad_length; - padded_waveform.assign(padded_length, 0.0f); - - if (std::strcmp(pad_mode, "reflect") == 0) { - for (size_t i = 0; i < pad_length; i++) { - padded_waveform[i] = waveform[pad_length - i]; - } - - std::copy(waveform, waveform + waveform_length, padded_waveform.data() + pad_length); - - for (size_t i = 0; i < pad_length; i++) { - padded_waveform[pad_length + waveform_length + i] = waveform[waveform_length - 2 - i]; - } - } else if (std::strcmp(pad_mode, "constant") == 0) { - std::copy(waveform, waveform + waveform_length, padded_waveform.data() + pad_length); - } else { - throw std::invalid_argument("Unsupported pad_mode: " + std::string(pad_mode)); - } - - input_waveform = padded_waveform.data(); - input_length = padded_length; - } - - const size_t num_frames = 1 + (input_length - frame_length) / hop_length; - const size_t num_frequency_bins = (actual_fft_length / 2) + 1; - - std::vector buffer(actual_fft_length); - std::vector raw_complex_frequencies(num_frequency_bins * 2); - - const size_t num_mel_bins = mel_filters != nullptr ? mel_filters_size / num_frequency_bins : 0; - const size_t spectrogram_bins = mel_filters != nullptr ? num_mel_bins : num_frequency_bins; - - std::vector temp_spectrogram(num_frames * num_frequency_bins); - - CactusThreading::parallel_for(num_frames, CactusThreading::Thresholds::SCALAR_EXPENSIVE, [&](size_t start_frame, size_t end_frame) { - std::vector local_buffer(actual_fft_length); - std::vector local_complex_frequencies(num_frequency_bins * 2); - - for (size_t frame_idx = start_frame; frame_idx < end_frame; frame_idx++) { - size_t timestep = frame_idx * hop_length; - std::fill(local_buffer.begin(), local_buffer.end(), 0.0f); - - size_t available_length = std::min(frame_length, input_length - timestep); - std::copy(input_waveform + timestep, input_waveform + timestep + available_length, local_buffer.data()); - - if (dither != 0.0f) { - thread_local std::mt19937 rng(std::random_device{}()); - std::normal_distribution dist(0.0f, 1.0f); - for (size_t i = 0; i < frame_length; i++) { - local_buffer[i] += dither * dist(rng); - } - } - - if (remove_dc_offset) { - float mean = 0.0f; - for (size_t i = 0; i < frame_length; i++) { - mean += local_buffer[i]; - } - mean /= static_cast(frame_length); - - for (size_t i = 0; i < frame_length; i++) { - local_buffer[i] -= mean; - } - } - - if (preemphasis != nullptr) { - float preemph_coef = *preemphasis; - for (size_t i = frame_length - 1; i > 0; i--) { - local_buffer[i] -= preemph_coef * local_buffer[i - 1]; - } - local_buffer[0] *= (1.0f - preemph_coef); - } - - for (size_t i = 0; i < frame_length; i++) { - local_buffer[i] *= actual_window[i]; - } - - rfft_f32_1d(local_buffer.data(), local_complex_frequencies.data(), actual_fft_length, "backward"); - - for (size_t i = 0; i < num_frequency_bins; i++) { - float real = local_complex_frequencies[i * 2]; - float imag = local_complex_frequencies[i * 2 + 1]; - float magnitude = std::hypot(real, imag); - temp_spectrogram[frame_idx * num_frequency_bins + i] = std::pow(magnitude, power); - } - } - }); - - if (mel_filters != nullptr) { - CactusThreading::parallel_for_2d(num_mel_bins, num_frames, CactusThreading::Thresholds::AXIS_REDUCE, [&](size_t m, size_t t) { - float sum = 0.0f; - for (size_t f = 0; f < num_frequency_bins; f++) { - sum += mel_filters[m * num_frequency_bins + f] * temp_spectrogram[t * num_frequency_bins + f]; - } - spectrogram[m * num_frames + t] = std::max(mel_floor, sum); - }); - } else { - CactusThreading::parallel_for_2d(num_frames, num_frequency_bins, CactusThreading::Thresholds::AXIS_REDUCE, [&](size_t t, size_t f) { - spectrogram[f * num_frames + t] = temp_spectrogram[t * num_frequency_bins + f]; - }); - } - - if (power != 0.0f && log_mel != nullptr) { - const size_t total_elements = spectrogram_bins * num_frames; - - if (std::strcmp(log_mel, "log") == 0) { - CactusThreading::parallel_for(total_elements, CactusThreading::Thresholds::ALL_REDUCE, [&](size_t start, size_t end) { - for (size_t i = start; i < end; i++) { - spectrogram[i] = std::log(spectrogram[i]); - } - }); - } else if (std::strcmp(log_mel, "log10") == 0) { - CactusThreading::parallel_for(total_elements, CactusThreading::Thresholds::ALL_REDUCE, [&](size_t start, size_t end) { - for (size_t i = start; i < end; i++) { - spectrogram[i] = std::log10(spectrogram[i]); - } - }); - } else if (std::strcmp(log_mel, "dB") == 0) { - if (power == 1.0f) { - to_db(spectrogram, total_elements, reference, min_value, db_range, 20.0f); - } else if (power == 2.0f) { - to_db(spectrogram, total_elements, reference, min_value, db_range, 10.0f); - } else { - throw std::invalid_argument( - "Cannot use log_mel option 'dB' with power " + std::to_string(power)); - } - } else { - throw std::invalid_argument("Unknown log_mel option: " + std::string(log_mel)); - } - } -} - -AudioProcessor::AudioProcessor() - : num_frequency_bins_(0), num_mel_filters_(0) {} - -AudioProcessor::~AudioProcessor() = default; - -void AudioProcessor::init_mel_filters(size_t num_frequency_bins, - size_t num_mel_filters, - float min_freq, - float max_freq, - size_t sampling_rate, - const char* norm, - const char* mel_scale) { - num_frequency_bins_ = num_frequency_bins; - num_mel_filters_ = num_mel_filters; - mel_filters_.resize(num_mel_filters * num_frequency_bins); - - generate_mel_filter_bank( - mel_filters_.data(), - num_frequency_bins, - num_mel_filters, - min_freq, - max_freq, - sampling_rate, - norm, - mel_scale, - false - ); -} - -std::vector AudioProcessor::compute_spectrogram( - const std::vector& waveform, - const SpectrogramConfig& config) { - - if (mel_filters_.empty()) { - throw std::runtime_error("Mel filters not initialized. Call init_mel_filters() first."); - } - - const size_t n_samples = waveform.size(); - const size_t fft_size = config.fft_override > 0 ? config.fft_override : config.n_fft; - const size_t analysis_frame_length = config.n_fft; - const size_t window_length = std::min(config.frame_length, analysis_frame_length); - const size_t pad_length = config.center ? analysis_frame_length / 2 : 0; - const size_t padded_length = n_samples + 2 * pad_length; - if (analysis_frame_length == 0 || padded_length < analysis_frame_length || config.hop_length == 0) { - return {}; - } - const size_t num_frames = 1 + (padded_length - analysis_frame_length) / config.hop_length; - - std::vector output(num_mel_filters_ * num_frames); - std::vector window(analysis_frame_length, 0.0f); - if (window_length == 1) { - window[analysis_frame_length / 2] = 1.0f; - } else if (window_length > 1) { - const float denom = config.hann_periodic - ? static_cast(window_length) - : static_cast(window_length - 1); - const float a0 = config.window_a0; - const size_t left_pad = (analysis_frame_length - window_length) / 2; - for (size_t i = 0; i < window_length; ++i) { - const float w = a0 - (1.0f - a0) * std::cos(2.0f * static_cast(M_PI) * static_cast(i) / denom); - window[left_pad + i] = w; - } - } - - float effective_floor = config.mel_floor_additive ? 0.0f : config.mel_floor; - const char* effective_log = config.mel_floor_additive ? nullptr : config.log_mel; - - compute_spectrogram_f32( - waveform.data(), - waveform.size(), - window.data(), - window.size(), - analysis_frame_length, - config.hop_length, - &fft_size, - output.data(), - config.power, - config.center, - config.pad_mode, - config.onesided, - config.dither, - config.preemphasis != 0.0f ? &config.preemphasis : nullptr, - mel_filters_.data(), - mel_filters_.size(), - effective_floor, - effective_log, - config.reference, - config.min_value, - nullptr, - config.remove_dc_offset - ); - - if (config.mel_floor_additive) { - for (size_t i = 0; i < output.size(); i++) { -#ifdef __APPLE__ - // vDSP FFT returns 2x standard DFT magnitudes, so raw mel - // values are 2x too large. Halve before adding floor and log. - output[i] = std::log(output[i] * 0.5f + config.mel_floor); -#else - output[i] = std::log(output[i] + config.mel_floor); -#endif - } - } - - return output; -} - -std::vector AudioProcessor::compute_irfft( - const std::vector& complex_input, - size_t n, - const char* norm) { - if (n == 0) { - throw std::invalid_argument("compute_irfft: n must be greater than 0"); - } - const size_t expected_in_len = (n / 2 + 1) * 2; - if (complex_input.size() != expected_in_len) { - throw std::invalid_argument("compute_irfft: complex_input must contain exactly (n/2+1)*2 values"); - } - std::vector output(n); - idft_r_f32_1d(complex_input.data(), output.data(), n, norm); - return output; -} - -} -} diff --git a/cactus/engine/engine_sp.cpp b/cactus/engine/engine_sp.cpp deleted file mode 100644 index a80754747..000000000 --- a/cactus/engine/engine_sp.cpp +++ /dev/null @@ -1,507 +0,0 @@ -#include "engine.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cactus { -namespace engine { - -SPTokenizer::SPTokenizer() - : trie_root_(std::make_unique()), - vocab_size_(0), - unk_token_id_(3), - bos_token_id_(2), - eos_token_id_(1), - pad_token_id_(0), - vocab_mmap_ptr_(nullptr), - vocab_mmap_size_(0) { - has_chat_template_ = false; -} - -SPTokenizer::~SPTokenizer() { - cleanup_mmap(); -} - -void SPTokenizer::cleanup_mmap() { - if (vocab_mmap_ptr_ && vocab_mmap_ptr_ != MAP_FAILED) { - munmap(vocab_mmap_ptr_, vocab_mmap_size_); - vocab_mmap_ptr_ = nullptr; - } -} - -bool SPTokenizer::load_vocabulary_with_config(const std::string& vocab_file, const std::string& /*merges_file*/, const std::string& config_file) { - runtime_config_ = load_tokenizer_runtime_config(config_file); - std::string config_path = config_file.substr(0, config_file.find_last_of("/\\")) + "/config.txt"; - detect_model_type(config_path); - - std::ifstream vocab_stream(vocab_file); - if (!vocab_stream.is_open()) return false; - - token_to_id_.clear(); - id_to_token_.clear(); - token_scores_.clear(); - - std::string first_line; - std::getline(vocab_stream, first_line); - vocab_stream.seekg(0); - - bool is_id_token_format = false; - if (!first_line.empty()) { - is_id_token_format = (std::isdigit(first_line[0]) && - first_line.find('\t') != std::string::npos); - } - - if (is_id_token_format) { - std::string line = ""; - while (std::getline(vocab_stream, line)) { - std::string token = ""; - uint32_t id = UINT32_MAX; - - std::istringstream iss(line); - if (iss >> id) { - if (std::getline(iss, token)) { - if (!token.empty() && token[0] == '\t') { - token = token.substr(1); - } - } - - if (token.empty()) { - auto last_pos = vocab_stream.tellg(); - while (std::getline(vocab_stream, line)) { - if (!line.empty()) { - break; - } - token += '\n'; - last_pos = vocab_stream.tellg(); - } - vocab_stream.seekg(last_pos); - } - } - - if (!token.empty() && id != UINT32_MAX) { - float score = -static_cast(id); - size_t tab_in_token = token.find('\t'); - if (tab_in_token != std::string::npos) { - std::string score_str = token.substr(tab_in_token + 1); - if (!score_str.empty()) score = std::stof(score_str); - token = token.substr(0, tab_in_token); - } - token_to_id_[token] = id; - if (id >= id_to_token_.size()) { - id_to_token_.resize(id + 1); - token_scores_.resize(id + 1, 0.0f); - } - id_to_token_[id] = token; - token_scores_[id] = score; - } - } - vocab_size_ = id_to_token_.size(); - } else { - std::string line; - uint32_t id = 0; - - vocab_stream.seekg(0); - while (std::getline(vocab_stream, line)) { - token_to_id_[line] = id; - id_to_token_.push_back(line); - token_scores_.push_back(0.0f); - id++; - } - vocab_size_ = id; - } - - vocab_stream.close(); - - build_trie(); - - std::ifstream config_stream(config_file); - if (config_stream.is_open()) { - std::string config_line; - while (std::getline(config_stream, config_line)) { - if (config_line.empty() || config_line[0] == '#') continue; - - size_t eq_pos = config_line.find('='); - if (eq_pos == std::string::npos) continue; - - std::string key = config_line.substr(0, eq_pos); - std::string value = config_line.substr(eq_pos + 1); - - key.erase(0, key.find_first_not_of(" \t")); - key.erase(key.find_last_not_of(" \t") + 1); - value.erase(0, value.find_first_not_of(" \t")); - value.erase(value.find_last_not_of(" \t") + 1); - - if (key == "eos_token_id") { - eos_token_id_ = std::stoul(value); - } else if (key == "pad_token_id") { - pad_token_id_ = std::stoul(value); - } else if (key == "unk_token_id") { - unk_token_id_ = std::stoul(value); - } else if (key == "bos_token_id") { - bos_token_id_ = std::stoul(value); - } else if (key == "sp_model_type") { - sp_bpe_mode_ = (value == "bpe" || value == "BPE"); - } else if (key == "sp_add_dummy_prefix") { - sp_add_dummy_prefix_ = (value == "true" || value == "1"); - } else if (key == "sp_byte_fallback") { - sp_byte_fallback_ = (value == "true" || value == "1"); - } - } - } - - std::string special_tokens_path = config_file.substr(0, config_file.find_last_of("/\\")) + "/special_tokens.json"; - load_special_tokens(special_tokens_path); - - std::string template_path = config_file.substr(0, config_file.find_last_of("/\\")) + "/chat_template.jinja2"; - load_chat_template(template_path); - - return true; -} - -void SPTokenizer::build_trie() { - for (uint32_t id = 0; id < id_to_token_.size(); ++id) { - const std::string& token = id_to_token_[id]; - if (token.empty()) continue; - - std::u32string u32_token; - size_t pos = 0; - while (pos < token.length()) { - char32_t codepoint = 0; - unsigned char byte = token[pos]; - - if (byte < 0x80) { - codepoint = byte; - pos++; - } else if ((byte & 0xE0) == 0xC0) { - if (pos + 1 < token.length()) { - codepoint = ((byte & 0x1F) << 6) | (token[pos + 1] & 0x3F); - pos += 2; - } else break; - } else if ((byte & 0xF0) == 0xE0) { - if (pos + 2 < token.length()) { - codepoint = ((byte & 0x0F) << 12) | - ((token[pos + 1] & 0x3F) << 6) | - (token[pos + 2] & 0x3F); - pos += 3; - } else break; - } else if ((byte & 0xF8) == 0xF0) { - if (pos + 3 < token.length()) { - codepoint = ((byte & 0x07) << 18) | - ((token[pos + 1] & 0x3F) << 12) | - ((token[pos + 2] & 0x3F) << 6) | - (token[pos + 3] & 0x3F); - pos += 4; - } else break; - } else { - pos++; - continue; - } - - u32_token.push_back(codepoint); - } - - if (u32_token.empty()) continue; - - TrieNode* current = trie_root_.get(); - for (char32_t ch : u32_token) { - if (current->children.find(ch) == current->children.end()) { - current->children[ch] = std::make_unique(); - } - current = current->children[ch].get(); - } - current->token_id = static_cast(id); - current->score = token_scores_[id]; - } -} - -std::string SPTokenizer::preprocess_text(const std::string& text) const { - if (text.empty()) return text; - - if (sp_bpe_mode_) { - std::string processed; - if (sp_add_dummy_prefix_) processed += "\xE2\x96\x81"; - for (char c : text) { - if (c == ' ') processed += "\xE2\x96\x81"; - else processed += c; - } - return processed; - } - - std::string processed = ""; - if (model_type_ == ModelType::BERT) { - processed = "▁"; - } - - for (size_t i = text.find_first_not_of(" "); i < text.length(); i++) { - char c = text[i]; - if (c == ' ') { - processed += "▁"; - } else { - processed += c; - } - } - - return processed; -} - -std::string SPTokenizer::postprocess_text(const std::string& text) const { - std::string result; - size_t i = 0; - while (i < text.length()) { - if (i + 2 < text.length() && - static_cast(text[i]) == 0xE2 && - static_cast(text[i+1]) == 0x96 && - static_cast(text[i+2]) == 0x81) { - result += ' '; - i += 3; - } else { - result += text[i]; - i++; - } - } - if (!result.empty() && result[0] == ' ') { - result = result.substr(1); - } - return result; -} - -std::vector> SPTokenizer::tokenize_with_trie(const std::string& text) const { - std::vector> result; - - std::u32string u32_text; - size_t pos = 0; - while (pos < text.length()) { - char32_t codepoint = 0; - unsigned char byte = text[pos]; - - if (byte < 0x80) { - codepoint = byte; - pos++; - } else if ((byte & 0xE0) == 0xC0) { - if (pos + 1 < text.length()) { - codepoint = ((byte & 0x1F) << 6) | (text[pos + 1] & 0x3F); - pos += 2; - } else break; - } else if ((byte & 0xF0) == 0xE0) { - if (pos + 2 < text.length()) { - codepoint = ((byte & 0x0F) << 12) | - ((text[pos + 1] & 0x3F) << 6) | - (text[pos + 2] & 0x3F); - pos += 3; - } else break; - } else if ((byte & 0xF8) == 0xF0) { - if (pos + 3 < text.length()) { - codepoint = ((byte & 0x07) << 18) | - ((text[pos + 1] & 0x3F) << 12) | - ((text[pos + 2] & 0x3F) << 6) | - (text[pos + 3] & 0x3F); - pos += 4; - } else break; - } else { - pos++; - continue; - } - - u32_text.push_back(codepoint); - } - - if (u32_text.empty()) { - result.push_back({text, unk_token_id_}); - return result; - } - - pos = 0; - while (pos < u32_text.length()) { - TrieNode* current = trie_root_.get(); - size_t best_match_len = 0; - int32_t best_token_id = -1; - - for (size_t len = 0; pos + len < u32_text.length(); ++len) { - char32_t ch = u32_text[pos + len]; - if (current->children.find(ch) == current->children.end()) { - break; - } - current = current->children[ch].get(); - if (current->token_id >= 0) { - best_match_len = len + 1; - best_token_id = current->token_id; - } - } - - if (best_match_len > 0) { - std::u32string u32_token = u32_text.substr(pos, best_match_len); - - std::string token; - for (char32_t cp : u32_token) { - if (cp < 0x80) { - token.push_back(static_cast(cp)); - } else if (cp < 0x800) { - token.push_back(static_cast(0xC0 | (cp >> 6))); - token.push_back(static_cast(0x80 | (cp & 0x3F))); - } else if (cp < 0x10000) { - token.push_back(static_cast(0xE0 | (cp >> 12))); - token.push_back(static_cast(0x80 | ((cp >> 6) & 0x3F))); - token.push_back(static_cast(0x80 | (cp & 0x3F))); - } else { - token.push_back(static_cast(0xF0 | (cp >> 18))); - token.push_back(static_cast(0x80 | ((cp >> 12) & 0x3F))); - token.push_back(static_cast(0x80 | ((cp >> 6) & 0x3F))); - token.push_back(static_cast(0x80 | (cp & 0x3F))); - } - } - result.push_back({token, static_cast(best_token_id)}); - pos += best_match_len; - } else { - char32_t cp = u32_text[pos]; - std::string char_str; - if (cp < 0x80) { - char_str.push_back(static_cast(cp)); - } else if (cp < 0x800) { - char_str.push_back(static_cast(0xC0 | (cp >> 6))); - char_str.push_back(static_cast(0x80 | (cp & 0x3F))); - } else if (cp < 0x10000) { - char_str.push_back(static_cast(0xE0 | (cp >> 12))); - char_str.push_back(static_cast(0x80 | ((cp >> 6) & 0x3F))); - char_str.push_back(static_cast(0x80 | (cp & 0x3F))); - } else { - char_str.push_back(static_cast(0xF0 | (cp >> 18))); - char_str.push_back(static_cast(0x80 | ((cp >> 12) & 0x3F))); - char_str.push_back(static_cast(0x80 | ((cp >> 6) & 0x3F))); - char_str.push_back(static_cast(0x80 | (cp & 0x3F))); - } - result.push_back({char_str, unk_token_id_}); - pos++; - } - } - - return result; -} - -std::vector SPTokenizer::tokenize_with_bpe(const std::string& text) const { - std::vector symbols; - for (size_t i = 0; i < text.size(); ) { - size_t char_len = 1; - unsigned char byte = static_cast(text[i]); - if ((byte & 0xE0) == 0xC0) char_len = 2; - else if ((byte & 0xF0) == 0xE0) char_len = 3; - else if ((byte & 0xF8) == 0xF0) char_len = 4; - if (i + char_len <= text.size()) symbols.push_back(text.substr(i, char_len)); - i += char_len; - } - - while (symbols.size() > 1) { - int best_index = -1; - float best_score = -std::numeric_limits::infinity(); - for (size_t i = 0; i + 1 < symbols.size(); ++i) { - auto it = token_to_id_.find(symbols[i] + symbols[i + 1]); - if (it == token_to_id_.end()) continue; - float score = it->second < token_scores_.size() - ? token_scores_[it->second] - : -static_cast(it->second); - if (score > best_score) { best_score = score; best_index = static_cast(i); } - } - if (best_index < 0) break; - symbols[best_index] += symbols[best_index + 1]; - symbols.erase(symbols.begin() + best_index + 1); - } - - std::vector result; - for (const auto& symbol : symbols) { - auto it = token_to_id_.find(symbol); - if (it != token_to_id_.end()) { result.push_back(it->second); continue; } - if (sp_byte_fallback_) { - for (unsigned char b : symbol) { - char buf[7]; std::snprintf(buf, sizeof(buf), "<0x%02X>", b); - auto byte_it = token_to_id_.find(buf); - result.push_back(byte_it != token_to_id_.end() ? byte_it->second : unk_token_id_); - } - continue; - } - result.push_back(unk_token_id_); - } - return result; -} - -std::vector SPTokenizer::split_with_special_tokens(const std::string& text) const { - return cactus::engine::split_with_special_tokens(text, special_tokens_); -} - -std::vector SPTokenizer::encode(const std::string& text) const { - if (text.empty()) return {}; - - auto text_segments = split_with_special_tokens(text); - std::vector token_ids; - - for (const auto& segment : text_segments) { - auto special_it = special_tokens_.find(segment); - if (special_it != special_tokens_.end()) { - token_ids.push_back(special_it->second); - } else { - std::string processed = preprocess_text(segment); - if (processed.empty()) continue; - - if (sp_bpe_mode_) { - auto ids = tokenize_with_bpe(processed); - token_ids.insert(token_ids.end(), ids.begin(), ids.end()); - } else { - auto token_pairs = tokenize_with_trie(processed); - for (const auto& [token, id] : token_pairs) { - token_ids.push_back(id); - } - } - } - } - - return token_ids; -} - -std::string SPTokenizer::decode(const std::vector& tokens) const { - if (tokens.size() == 1) { - if (tokens[0] >= id_to_token_.size()) return {}; - const std::string& piece = id_to_token_[tokens[0]]; - unsigned int byte_val; - if (piece.size() == 6 && std::sscanf(piece.c_str(), "<0x%02X>", &byte_val) == 1) { - return std::string(1, static_cast(byte_val)); - } - std::string result; - for (size_t i = 0; i < piece.length(); ) { - if (i + 2 < piece.length() && - static_cast(piece[i]) == 0xE2 && - static_cast(piece[i+1]) == 0x96 && - static_cast(piece[i+2]) == 0x81) { - result += ' '; - i += 3; - } else { - result += piece[i++]; - } - } - return result; - } - - std::string raw; - for (uint32_t token_id : tokens) { - if (token_id >= id_to_token_.size()) continue; - const std::string& piece = id_to_token_[token_id]; - unsigned int byte_val; - if (piece.size() == 6 && std::sscanf(piece.c_str(), "<0x%02X>", &byte_val) == 1) { - raw.push_back(static_cast(byte_val)); - } else { - raw += piece; - } - } - return postprocess_text(raw); -} - -void SPTokenizer::load_special_tokens(const std::string& config_file) { - load_special_tokens_map(config_file, special_tokens_); -} - -} // namespace engine -} // namespace cactus diff --git a/cactus/engine/engine_tokenizer.cpp b/cactus/engine/engine_tokenizer.cpp deleted file mode 100644 index d0749714f..000000000 --- a/cactus/engine/engine_tokenizer.cpp +++ /dev/null @@ -1,1018 +0,0 @@ -#include "engine.h" -#include -#include -#include -#include -#include - -extern "C" { - #include "../../libs/stb/stb_image.h" -} - -namespace cactus { -namespace engine { - -namespace { - -std::string trim_copy(const std::string& value) { - size_t start = value.find_first_not_of(" \t\r\n"); - if (start == std::string::npos) return ""; - size_t end = value.find_last_not_of(" \t\r\n"); - return value.substr(start, end - start + 1); -} - -TokenizerRuntimeConfig::TokenizerType parse_tokenizer_type(const std::string& value) { - if (value == "bpe") return TokenizerRuntimeConfig::TokenizerType::BPE; - if (value == "sentencepiece") return TokenizerRuntimeConfig::TokenizerType::SENTENCEPIECE; - return TokenizerRuntimeConfig::TokenizerType::UNKNOWN; -} - -TokenizerRuntimeConfig::VocabFormat parse_vocab_format(const std::string& value) { - if (value == "id_tab_token") return TokenizerRuntimeConfig::VocabFormat::ID_TAB_TOKEN; - if (value == "line_token") return TokenizerRuntimeConfig::VocabFormat::LINE_TOKEN; - return TokenizerRuntimeConfig::VocabFormat::UNKNOWN; -} - -TokenizerRuntimeConfig::Normalizer parse_normalizer(const std::string& value) { - if (value == "metaspace") return TokenizerRuntimeConfig::Normalizer::METASPACE; - if (value == "byte_level") return TokenizerRuntimeConfig::Normalizer::BYTE_LEVEL; - return TokenizerRuntimeConfig::Normalizer::NONE; -} - -TokenizerRuntimeConfig::Decoder parse_decoder(const std::string& value) { - if (value == "replace_metaspace") return TokenizerRuntimeConfig::Decoder::REPLACE_METASPACE; - if (value == "byte_level") return TokenizerRuntimeConfig::Decoder::BYTE_LEVEL; - return TokenizerRuntimeConfig::Decoder::NONE; -} - -void skip_json_whitespace(const std::string& json, size_t& pos) { - while (pos < json.size() && std::isspace(static_cast(json[pos]))) { - ++pos; - } -} - -bool extract_added_token_object(const std::string& json, size_t& pos, std::string& out_object) { - skip_json_whitespace(json, pos); - if (pos >= json.size() || json[pos] != '{') { - return false; - } - - size_t start = pos; - size_t depth = 0; - bool in_string = false; - bool escaped = false; - - while (pos < json.size()) { - char c = json[pos++]; - if (escaped) { - escaped = false; - continue; - } - if (c == '\\') { - escaped = true; - continue; - } - if (c == '"') { - in_string = !in_string; - continue; - } - if (in_string) { - continue; - } - if (c == '{') { - ++depth; - } else if (c == '}') { - if (depth == 0) { - return false; - } - --depth; - if (depth == 0) { - out_object = json.substr(start, pos - start); - return true; - } - } - } - - return false; -} - -bool parse_added_token_entry(const std::string& object, std::string& token_content, uint32_t& token_id, - bool& is_special) { - token_content.clear(); - token_id = 0; - is_special = false; - - size_t id_key = object.find("\"id\""); - if (id_key == std::string::npos) { - return false; - } - size_t id_colon = object.find(':', id_key); - if (id_colon == std::string::npos) { - return false; - } - size_t id_pos = id_colon + 1; - skip_json_whitespace(object, id_pos); - size_t id_end = id_pos; - while (id_end < object.size() && std::isdigit(static_cast(object[id_end]))) { - ++id_end; - } - if (id_end == id_pos) { - return false; - } - token_id = static_cast(std::stoul(object.substr(id_pos, id_end - id_pos))); - - size_t content_key = object.find("\"content\""); - if (content_key == std::string::npos) { - return false; - } - size_t content_colon = object.find(':', content_key); - if (content_colon == std::string::npos) { - return false; - } - size_t content_pos = object.find('"', content_colon + 1); - if (content_pos == std::string::npos) { - return false; - } - ++content_pos; - token_content = extract_json_string(object, content_pos); - - size_t special_key = object.find("\"special\""); - if (special_key != std::string::npos) { - size_t special_colon = object.find(':', special_key); - if (special_colon != std::string::npos) { - size_t special_pos = special_colon + 1; - skip_json_whitespace(object, special_pos); - is_special = object.compare(special_pos, 4, "true") == 0; - } - } - - return true; -} - -void load_tokenizer_json_added_special_tokens( - const std::string& tokenizer_json_path, - std::unordered_map& special_tokens) { - std::ifstream file(tokenizer_json_path); - if (!file.is_open()) { - return; - } - - std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - size_t pos = content.find("\"added_tokens\""); - if (pos == std::string::npos) { - return; - } - - pos = content.find('[', pos); - if (pos == std::string::npos) { - return; - } - ++pos; - - while (pos < content.size()) { - skip_json_whitespace(content, pos); - if (pos >= content.size() || content[pos] == ']') { - break; - } - - std::string object; - if (!extract_added_token_object(content, pos, object)) { - ++pos; - continue; - } - - std::string token_content; - uint32_t token_id = 0; - bool is_special = false; - if (parse_added_token_entry(object, token_content, token_id, is_special) && is_special) { - special_tokens[token_content] = token_id; - } - - skip_json_whitespace(content, pos); - if (pos < content.size() && content[pos] == ',') { - ++pos; - } - } -} - -} // namespace - -TokenizerRuntimeConfig load_tokenizer_runtime_config(const std::string& config_file) { - TokenizerRuntimeConfig config; - - std::ifstream file(config_file); - if (!file.is_open()) { - return config; - } - - std::string line; - while (std::getline(file, line)) { - if (line.empty() || line[0] == '#') continue; - - size_t eq_pos = line.find('='); - if (eq_pos == std::string::npos) continue; - - const std::string key = trim_copy(line.substr(0, eq_pos)); - const std::string value = trim_copy(line.substr(eq_pos + 1)); - - if (key == "tokenizer_type") { - config.tokenizer_type = parse_tokenizer_type(value); - } else if (key == "vocab_format") { - config.vocab_format = parse_vocab_format(value); - } else if (key == "normalizer") { - config.normalizer = parse_normalizer(value); - } else if (key == "decoder") { - config.decoder = parse_decoder(value); - } else if (key == "byte_fallback") { - config.byte_fallback = (value == "true" || value == "1"); - } else if (key == "has_chat_template") { - config.has_chat_template = (value == "true" || value == "1"); - } - } - - return config; -} - -void load_special_tokens_map(const std::string& config_file, std::unordered_map& special_tokens) { - special_tokens.clear(); - - std::ifstream file(config_file); - if (file.is_open()) { - std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - - size_t pos = content.find("\"special_tokens\""); - if (pos != std::string::npos) { - pos = content.find("{", pos); - if (pos != std::string::npos) { - size_t end_pos = content.find("}", pos); - if (end_pos != std::string::npos) { - std::string special_tokens_section = content.substr(pos + 1, end_pos - pos - 1); - std::istringstream iss(special_tokens_section); - std::string line; - - while (std::getline(iss, line)) { - size_t colon_pos = line.find(":"); - if (colon_pos == std::string::npos) continue; - - std::string id_part = line.substr(0, colon_pos); - std::string token_part = line.substr(colon_pos + 1); - - size_t id_start = id_part.find("\""); - size_t id_end = id_part.find("\"", id_start + 1); - if (id_start == std::string::npos || id_end == std::string::npos) continue; - - uint32_t token_id = - static_cast(std::stoul(id_part.substr(id_start + 1, id_end - id_start - 1))); - - size_t token_start = token_part.find("\""); - if (token_start == std::string::npos) continue; - size_t value_pos = token_start + 1; - std::string token_content = extract_json_string(token_part, value_pos); - special_tokens[token_content] = token_id; - } - } - } - } - } - - size_t slash_pos = config_file.find_last_of("/\\"); - std::string dir = (slash_pos == std::string::npos) ? "." : config_file.substr(0, slash_pos); - load_tokenizer_json_added_special_tokens(dir + "/tokenizer.json", special_tokens); -} - -std::vector split_with_special_tokens(const std::string& text, - const std::unordered_map& special_tokens) { - std::vector result; - size_t start = 0; - while (start < text.size()) { - size_t best_match_pos = text.size(); - size_t best_match_len = 0; - std::string best_special_token; - - for (const auto& [special_token, token_id] : special_tokens) { - size_t pos = text.find(special_token, start); - if (pos != std::string::npos && - (pos < best_match_pos || (pos == best_match_pos && special_token.length() > best_match_len))) { - best_match_pos = pos; - best_match_len = special_token.length(); - best_special_token = special_token; - } - } - - if (best_match_pos < text.size()) { - if (best_match_pos > start) - result.push_back(text.substr(start, best_match_pos - start)); - result.push_back(best_special_token); - start = best_match_pos + best_match_len; - } else { - if (start < text.size()) - result.push_back(text.substr(start)); - break; - } - } - return result; -} - -void Tokenizer::load_chat_template(const std::string& template_file) { - std::ifstream file(template_file); - if (!file.is_open()) { - has_chat_template_ = false; - return; - } - chat_template_ = std::string((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - has_chat_template_ = !chat_template_.empty(); -} - -void Tokenizer::detect_model_type(const std::string& config_path) { - std::ifstream file(config_path); - if (!file.is_open()) { - model_type_ = ModelType::UNKNOWN; - return; - } - - std::string line; - while (std::getline(file, line)) { - size_t pos = line.find("model_type"); - if (pos != std::string::npos) { - std::transform(line.begin(), line.end(), line.begin(), ::tolower); - - if (line.find("qwen3_5") != std::string::npos) { - model_type_ = ModelType::QWEN3P5; - break; - } else if (line.find("qwen") != std::string::npos) { - model_type_ = ModelType::QWEN; - break; - } else if (line.find("gemma4") != std::string::npos || line.find("tinyllama") != std::string::npos) { - model_type_ = ModelType::GEMMA4; - break; - } else if (line.find("gemma") != std::string::npos) { - model_type_ = ModelType::GEMMA; - break; - } else if(line.find("lfm2") != std::string::npos) { - model_type_ = ModelType::LFM2; - } else if (line.find("bert") != std::string::npos) { - model_type_ = ModelType::BERT; - break; - } else if (line.find("whisper") != std::string::npos) { - model_type_ = ModelType::WHISPER; - break; - } else if (line.find("parakeet") != std::string::npos) { - model_type_ = ModelType::PARAKEET; - break; - } else if (line.find("needle") != std::string::npos) { - model_type_ = ModelType::NEEDLE; - break; - } else if (line.find("youtu") != std::string::npos) { - model_type_ = ModelType::YOUTU; - break; - } else { - model_type_ = ModelType::UNKNOWN; - } - } - } - file.clear(); - file.seekg(0); - - while (std::getline(file, line)) { - size_t pos2 = line.find("model_variant"); - if (pos2 != std::string::npos) { - std::transform(line.begin(), line.end(), line.begin(), ::tolower); - - if (line.find("vlm") != std::string::npos) { - model_variant_ = ModelVariant::VLM; - break; - } else if (line.find("extract") != std::string::npos) { - model_variant_ = ModelVariant::EXTRACT; - break; - } else if (line.find("rag") != std::string::npos) { - model_variant_ = ModelVariant::RAG; - break; - } else { - model_variant_ = ModelVariant::DEFAULT; - } - } - } - - file.clear(); - file.seekg(0); - while (std::getline(file, line)) { - auto parse_uint = [&](const std::string& key, uint32_t& out) { - size_t p = line.find(key + "="); - if (p != std::string::npos) { - out = static_cast(std::stoul(line.substr(p + key.size() + 1))); - } - }; - parse_uint("vision_patch_size", vision_patch_size_); - parse_uint("vision_pooling_kernel_size", vision_pooling_kernel_size_); - parse_uint("vision_default_output_length", vision_default_output_length_); - parse_uint("vision_image_size", vision_image_size_); - } - - file.close(); -} - -std::string Tokenizer::get_default_stop_sequence() const { - switch (model_type_) { - case ModelType::GEMMA: - return ""; - case ModelType::GEMMA4: - return ""; - case ModelType::QWEN: - case ModelType::QWEN3P5: - case ModelType::LFM2: - return "<|im_end|>"; - case ModelType::NEEDLE: - return ""; - default: - return "<|im_end|>"; - } -} - -std::vector Tokenizer::apply_chat_template(const std::vector& messages, bool add_generation_prompt) const { - std::string formatted_prompt = format_chat_prompt(messages, add_generation_prompt); - return encode(formatted_prompt); -} - -std::string Tokenizer::format_chat_prompt(const std::vector& messages, bool add_generation_prompt, - const std::string& tools_json, bool enable_thinking_if_supported) const { - bool has_images = false; - for (const auto& msg : messages) { - if (!msg.images.empty()) { - has_images = true; - break; - } - } - if (model_type_ == ModelType::LFM2 && has_images) { - return format_lfm2_vl_style(messages, add_generation_prompt, tools_json); - } - - switch (model_type_) { - case ModelType::QWEN: - case ModelType::QWEN3P5: - return format_qwen_style(messages, add_generation_prompt, tools_json, enable_thinking_if_supported); - case ModelType::GEMMA: - return format_gemma_style(messages, add_generation_prompt, tools_json); - case ModelType::GEMMA4: - return format_gemma4_style(messages, add_generation_prompt, tools_json, enable_thinking_if_supported); - case ModelType::LFM2: - return format_lfm2_style(messages, add_generation_prompt, tools_json); - case ModelType::NEEDLE: - return format_needle_style(messages, add_generation_prompt, tools_json); - case ModelType::YOUTU: - return format_youtu_style(messages, add_generation_prompt, tools_json); - default: - return format_qwen_style(messages, add_generation_prompt, tools_json); - } -} - -std::string Tokenizer::format_needle_style(const std::vector& messages, - bool /*add_generation_prompt*/, - const std::string& tools_json) const { - std::string serialized_tools = tools_json.empty() ? "[]" : tools_json; - return format_needle_query_text(messages) + "" + serialized_tools + ""; -} - -std::string Tokenizer::format_qwen_style(const std::vector& messages, bool add_generation_prompt, const std::string& tools_json, bool enable_thinking_if_supported) const { - std::string result; - size_t start_idx = 0; - - if (!tools_json.empty()) { - result += "<|im_start|>system\n"; - if (!messages.empty() && messages[0].role == "system") { - result += messages[0].content + "\n\n"; - start_idx = 1; - } - result += "# Tools\n\n"; - result += "You may call one or more functions to assist with the user query.\n\n"; - result += "You are provided with function signatures within XML tags:\n"; - result += ""; - result += tools_json; - result += "\n\n\n"; - result += "For each function call, return a json object with function name and arguments within XML tags:\n"; - result += "\n"; - result += "{\"name\": , \"arguments\": }\n"; - result += ""; - result += "<|im_end|>\n"; - } else { - if (!messages.empty() && messages[0].role == "system") { - result += "<|im_start|>system\n" + messages[0].content + "<|im_end|>\n"; - start_idx = 1; - } - } - - size_t last_query_index = messages.empty() ? 0 : messages.size() - 1; - for (size_t idx = messages.size(); idx > 0; idx--) { - const auto& m = messages[idx - 1]; - if (m.role == "user") { - bool is_tool_response = m.content.size() >= 31 && - m.content.substr(0, 15) == "" && - m.content.substr(m.content.size() - 16) == ""; - if (!is_tool_response) { - last_query_index = idx - 1; - break; - } - } - } - - for (size_t i = start_idx; i < messages.size(); i++) { - const auto& msg = messages[i]; - if (msg.role == "user" || (msg.role == "system" && i > 0)) { - result += "<|im_start|>" + msg.role + "\n" + msg.content + "<|im_end|>\n"; - } else if (msg.role == "assistant") { - if (i > last_query_index && i == messages.size() - 1) { - result += "<|im_start|>assistant\n\n\n\n\n"; - std::string content = msg.content; - size_t lstrip = content.find_first_not_of('\n'); - if (lstrip != std::string::npos) { - result += content.substr(lstrip); - } - } else { - result += "<|im_start|>assistant\n" + msg.content; - } - if (!msg.tool_calls.empty()) { - for (size_t j = 0; j < msg.tool_calls.size(); j++) { - if ((j == 0 && !msg.content.empty()) || j > 0) { - result += "\n"; - } - result += "\n{\"name\": \"" + msg.tool_calls[j].name - + "\", \"arguments\": " + msg.tool_calls[j].arguments - + "}\n"; - } - } - result += "<|im_end|>\n"; - } else if (msg.role == "tool") { - bool first_tool = (i == 0 || messages[i - 1].role != "tool"); - bool last_tool = (i == messages.size() - 1 || messages[i + 1].role != "tool"); - if (first_tool) { - result += "<|im_start|>user"; - } - result += "\n\n" + msg.content + "\n"; - if (last_tool) { - result += "<|im_end|>\n"; - } - } - } - - if (add_generation_prompt) { - result += "<|im_start|>assistant\n"; - if (!enable_thinking_if_supported) { - result += "\n\n\n\n"; - } - } - - return result; -} - -std::string Tokenizer::format_lfm2_style(const std::vector& messages, - bool add_generation_prompt, - const std::string& tools_json) const -{ - std::string result = "<|startoftext|>"; - - std::string sys_content; - bool has_system_msg = false; - for (const auto& msg : messages) { - if (msg.role == "system") { - sys_content = msg.content; - has_system_msg = true; - break; - } - } - - if (!tools_json.empty()) { - if (!sys_content.empty()) { - sys_content += "\n"; - } - sys_content += "List of tools: <|tool_list_start|>["; - if (!tools_json.empty()) { - sys_content += "\n"; - sys_content += tools_json; - sys_content += "\n"; - } - sys_content += "]<|tool_list_end|>"; - sys_content += "\n\nWhen you need to call a tool, use this exact format:\n"; - sys_content += "<|tool_call_start|>[function_name(arg1=\"value1\", arg2=\"value2\")]<|tool_call_end|>\n"; - sys_content += "You can call multiple tools by using multiple tool call blocks."; - } - - if (!sys_content.empty()) { - result += "<|im_start|>system\n"; - result += sys_content; - result += "<|im_end|>\n"; - } - - for (const auto& msg : messages) { - if (msg.role == "system" && has_system_msg) { - has_system_msg = false; - continue; - } - result += "<|im_start|>" + msg.role + "\n"; - if (msg.role == "tool") { - result += "<|tool_response_start|>"; - result += msg.content; - result += "<|tool_response_end|>"; - } else { - result += msg.content; - if (msg.role == "assistant" && !msg.tool_calls.empty()) { - for (const auto& tc : msg.tool_calls) { - result += "<|tool_call_start|>[{\"name\": \"" + tc.name - + "\", \"arguments\": " + tc.arguments + "}]<|tool_call_end|>"; - } - } - } - result += "<|im_end|>\n"; - } - - if (add_generation_prompt) { - result += "<|im_start|>assistant\n"; - } - - return result; -} - -static std::string format_tool_call_for_prompt(const std::string& name, const std::string& args_json, bool use_pipe_tags) { - const char* tag_start = use_pipe_tags ? "<|tool_call>" : ""; - const char* tag_end = use_pipe_tags ? "" : ""; - - std::string formatted_args = "{"; - if (!args_json.empty()) { - std::map args; - size_t pos = 0; - while (pos < args_json.length() && args_json[pos] != '{') pos++; - if (pos < args_json.length()) pos++; - - while (pos < args_json.length()) { - while (pos < args_json.length() && std::isspace(static_cast(args_json[pos]))) pos++; - if (pos >= args_json.length() || args_json[pos] == '}') break; - if (args_json[pos] == ',') { pos++; continue; } - if (args_json[pos] != '"') break; - pos++; - - std::string key; - while (pos < args_json.length() && args_json[pos] != '"') { - if (args_json[pos] == '\\' && pos + 1 < args_json.length()) { pos++; } - key += args_json[pos++]; - } - if (pos < args_json.length()) pos++; - - while (pos < args_json.length() && (std::isspace(static_cast(args_json[pos])) || args_json[pos] == ':')) pos++; - - std::string value; - if (pos < args_json.length() && args_json[pos] == '"') { - pos++; - std::string str_val; - while (pos < args_json.length() && args_json[pos] != '"') { - if (args_json[pos] == '\\' && pos + 1 < args_json.length()) { - pos++; - if (args_json[pos] == 'n') str_val += '\n'; - else if (args_json[pos] == 't') str_val += '\t'; - else str_val += args_json[pos]; - } else { - str_val += args_json[pos]; - } - pos++; - } - if (pos < args_json.length()) pos++; - value = "" + str_val + ""; - } else { - size_t val_start = pos; - while (pos < args_json.length() && args_json[pos] != ',' && args_json[pos] != '}') pos++; - value = args_json.substr(val_start, pos - val_start); - while (!value.empty() && std::isspace(static_cast(value.back()))) value.pop_back(); - } - - if (!key.empty()) args[key] = value; - } - - bool first = true; - for (const auto& [k, v] : args) { - if (!first) formatted_args += ","; - first = false; - formatted_args += k + ":" + v; - } - } - formatted_args += "}"; - - return std::string(tag_start) + "call:" + name + formatted_args + tag_end; -} - -std::string Tokenizer::format_gemma4_style(const std::vector& messages, bool add_generation_prompt, - const std::string& tools_json, bool enable_thinking_if_supported) const { - std::string result = ""; - - std::string sys_content; - size_t first_msg = 0; - if (!messages.empty() && (messages[0].role == "system" || messages[0].role == "developer")) { - sys_content = messages[0].content; - first_msg = 1; - } - - if (enable_thinking_if_supported || !sys_content.empty() || !tools_json.empty()) { - result += "<|turn>system\n"; - if (enable_thinking_if_supported) { - result += "<|think|>"; - } - result += sys_content; - result += tools_json; - result += "\n"; - } - - auto strip_channel = [](const std::string& text) -> std::string { - const std::string open_tag = "<|channel>"; - const std::string close_tag = ""; - std::string out; - size_t pos = 0; - while (pos < text.size()) { - size_t open_pos = text.find(open_tag, pos); - if (open_pos == std::string::npos) { - out += text.substr(pos); - break; - } - out += text.substr(pos, open_pos - pos); - size_t close_pos = text.find(close_tag, open_pos + open_tag.size()); - if (close_pos == std::string::npos) { - break; - } - pos = close_pos + close_tag.size(); - } - return out; - }; - - auto compute_soft_tokens = [&](const std::string& image_path) -> size_t { - int w = 0, h = 0, c = 0; - unsigned char* data = stbi_load(image_path.c_str(), &w, &h, &c, 3); - if (!data) return 0; - stbi_image_free(data); - - uint32_t p = vision_patch_size_; - uint32_t k = vision_pooling_kernel_size_; - uint32_t side = k * p; - uint32_t max_patches = vision_default_output_length_ * k * k; - float factor = std::sqrt(static_cast(max_patches) * p * p / - (static_cast(h) * w)); - int th = static_cast(std::floor(factor * h / side)) * side; - int tw = static_cast(std::floor(factor * w / side)) * side; - if (th == 0) th = side; - if (tw == 0) tw = side; - return static_cast((th / p / k) * (tw / p / k)); - }; - - for (size_t i = first_msg; i < messages.size(); i++) { - const auto& msg = messages[i]; - std::string role = (msg.role == "assistant") ? "model" : msg.role; - result += "<|turn>" + role + "\n"; - if (role == "model") { - result += strip_channel(msg.content); - if (!msg.tool_calls.empty()) { - for (const auto& tc : msg.tool_calls) { - result += format_tool_call_for_prompt(tc.name, tc.arguments, true); - } - } - } else { - for (const auto& image_path : msg.images) { - size_t n = compute_soft_tokens(image_path); - if (n > 0) { - result += "\n\n<|image>"; - for (size_t j = 0; j < n; j++) - result += "<|image|>"; - result += "\n\n"; - } - } - result += msg.content; - if (msg.audio_soft_token_count > 0) { - result += "<|audio>"; - for (size_t j = 0; j < msg.audio_soft_token_count; j++) - result += "<|audio|>"; - result += ""; - } - } - result += "\n"; - } - - if (add_generation_prompt) { - result += "<|turn>model\n"; - } - - return result; -} - -std::string Tokenizer::format_lfm2_vl_style( - const std::vector& messages, - bool add_generation_prompt, - const std::string& tools_json) const -{ - if (!tools_json.empty()) { - return "ERROR: Tool calls are not supported for LFM2-VL models"; - } - - std::string result = "<|startoftext|>"; - - for (const auto& msg : messages) { - result += "<|im_start|>" + msg.role + "\n"; - for (const auto& image_path : msg.images) { - int width = 0, height = 0, channels = 0; - unsigned char* img_data = stbi_load(image_path.c_str(), &width, &height, &channels, 0); - - if (img_data) { - Siglip2Preprocessor preprocessor; - auto shape_result = preprocessor.compute_spatial_shapes(height, width); - int downsample_factor = 2; - bool use_thumbnail = true; - int grid_rows = shape_result.grid_rows; - int grid_cols = shape_result.grid_cols; - int num_tiles = grid_rows * grid_cols; - result += "<|image_start|>"; - - if (num_tiles > 1) { - for (int tile_idx = 0; tile_idx < num_tiles; ++tile_idx) { - int row = tile_idx / grid_cols; - int col = tile_idx % grid_cols; - - result += "<|img_row_" + std::to_string(row + 1) + "_col_" + std::to_string(col + 1) + "|>"; - auto [tile_height, tile_width] = shape_result.shapes[tile_idx]; - int tile_tokens = (tile_height * tile_width) / (downsample_factor * downsample_factor); - - for (int t = 0; t < tile_tokens; ++t) { - result += ""; - } - } - if (use_thumbnail && static_cast(num_tiles) < shape_result.shapes.size()) { - result += "<|img_thumbnail|>"; - - auto [thumb_height, thumb_width] = shape_result.shapes[num_tiles]; - int thumbnail_tokens = (thumb_height * thumb_width) / (downsample_factor * downsample_factor); - - for (int t = 0; t < thumbnail_tokens; ++t) { - result += ""; - } - } - } else if (num_tiles == 1) { - auto [thumb_height, thumb_width] = shape_result.shapes[0]; - int thumbnail_tokens = (thumb_height * thumb_width) / (downsample_factor * downsample_factor); - - for (int t = 0; t < thumbnail_tokens; ++t) { - result += ""; - } - } - - result += "<|image_end|>"; - - stbi_image_free(img_data); - } else { - result += ""; - } - } - result += msg.content; - - result += "<|im_end|>\n"; - } - - if (add_generation_prompt) { - result += "<|im_start|>assistant\n"; - } - - return result; -} - - -std::string Tokenizer::format_gemma_style(const std::vector& messages, bool add_generation_prompt, const std::string& tools_json) const { - auto trim = [](const std::string& s) -> std::string { - size_t start = 0; - while (start < s.size() && std::isspace(static_cast(s[start]))) ++start; - size_t end = s.size(); - while (end > start && std::isspace(static_cast(s[end - 1]))) --end; - return s.substr(start, end - start); - }; - - std::string result = ""; - - std::string system_content; - size_t start_idx = 0; - - if (!messages.empty() && (messages[0].role == "system" || messages[0].role == "developer")) { - system_content = messages[0].content; - start_idx = 1; - } - - if (!tools_json.empty() || !system_content.empty()) { - result += "developer\n"; - if (!system_content.empty()) { - result += trim(system_content); - } - if (!tools_json.empty()) { - result += tools_json; - } - result += "\n"; - } - - std::string prev_message_type; - - for (size_t i = start_idx; i < messages.size(); i++) { - const auto& msg = messages[i]; - - if (msg.role == "tool") { - std::string func_name = msg.name.empty() ? "tool" : msg.name; - result += "response:" + func_name + "{" + msg.content + "}"; - prev_message_type = "tool_response"; - - } else if (msg.role == "user") { - if (prev_message_type != "tool_response") { - result += "user\n"; - } - result += trim(msg.content); - result += "\n"; - prev_message_type = "content"; - - } else if (msg.role == "assistant" || msg.role == "model") { - if (prev_message_type != "tool_response") { - result += "model\n"; - } - if (!msg.content.empty()) { - result += trim(msg.content); - } - if (!msg.tool_calls.empty()) { - for (const auto& tc : msg.tool_calls) { - result += format_tool_call_for_prompt(tc.name, tc.arguments, false); - } - if (i == messages.size() - 1) { - result += ""; - } - prev_message_type = "tool_call"; - } else { - result += "\n"; - prev_message_type = "content"; - } - } - } - - if (add_generation_prompt) { - if (prev_message_type != "tool_response" && prev_message_type != "tool_call") { - result += "model\n"; - } - } - - return result; -} - -std::string Tokenizer::format_youtu_style(const std::vector& messages, bool add_generation_prompt, const std::string& tools_json) const { - std::string result = "<|begin_of_text|>"; - - std::string system_block; - for (const auto& msg : messages) { - if (msg.role == "system") { system_block = msg.content; break; } - } - - if (!tools_json.empty()) { - std::string tool_desc = - "<|begin_of_tool_description|>Tool calling capabilities.\n" - "You may call one or more functions to assist with the user query. " - "You have the following functions available:\n" - "```json\n" + tools_json + "\n```\n" - "For tool call returns, you MUST use the following format:\n" - "{\"name\": \"function-name\", \"arguments\": {\"param1\": \"value1\", \"param2\": \"value2\"}}\n" - "<|end_of_tool_description|>"; - system_block = system_block.empty() ? tool_desc : system_block + "\n\n" + tool_desc; - } - - result += system_block; - - bool is_last_user = false; - bool is_tool = false; - bool is_output_first = true; - - for (const auto& msg : messages) { - if (msg.role == "system") { - continue; - } else if (msg.role == "user") { - is_last_user = true; is_tool = false; - result += "<|User|>" + msg.content; - } else if (msg.role == "tool") { - is_tool = true; is_last_user = false; - if (is_output_first) { - result += "<|User|>" + msg.content + ""; - is_output_first = false; - } else { - result += "\n" + msg.content + ""; - } - } else if (msg.role == "assistant" || msg.role == "model") { - is_last_user = false; is_tool = false; is_output_first = true; - result += "<|Assistant|>" + msg.content; - if (!msg.tool_calls.empty()) { - for (const auto& tc : msg.tool_calls) { - result += "{\"name\": \"" + tc.name - + "\", \"arguments\": " + tc.arguments + "}"; - } - } - result += "<|end_of_text|>"; - } - } - - if (add_generation_prompt && (is_last_user || is_tool)) { - result += "<|Assistant|>"; - } - - return result; -} - -} // namespace engine -} // namespace cactus diff --git a/cactus/ffi/cactus_cloud.cpp b/cactus/ffi/cactus_cloud.cpp deleted file mode 100644 index 93114cefc..000000000 --- a/cactus/ffi/cactus_cloud.cpp +++ /dev/null @@ -1,462 +0,0 @@ -#include "cactus_cloud.h" -#include "telemetry/telemetry.h" - -#include -#include -#include -#include -#include -#include -#include - -#ifdef CACTUS_USE_CURL -#include -#endif - -namespace cactus { -namespace ffi { - -namespace { - -std::string compact_error_detail(std::string detail) { - detail = trim_string(detail); - if (detail.empty()) return {}; - - for (char& c : detail) { - if (c == '\n' || c == '\r' || c == '\t') c = ' '; - } - while (detail.find(" ") != std::string::npos) { - detail.erase(detail.find(" "), 1); - } - - constexpr size_t kMaxLen = 160; - if (detail.size() > kMaxLen) { - detail = detail.substr(0, kMaxLen) + "..."; - } - return detail; -} - -} // namespace - -std::string resolve_cloud_api_key(const char* cloud_key_param) { - const char* env_cloud = std::getenv("CACTUS_CLOUD_KEY"); - const char* env_cloud_legacy = std::getenv("CACTUS_CLOUD_API_KEY"); // keeping this cuz Matthew Hayes had some code reliant on Cactus_cloud_api_key - std::string resolved_cloud_key; - bool key_from_param_or_env = false; - - if (cloud_key_param && *cloud_key_param) { - resolved_cloud_key = cloud_key_param; - key_from_param_or_env = true; - } else if (env_cloud && *env_cloud) { - resolved_cloud_key = env_cloud; - key_from_param_or_env = true; - } else if (env_cloud_legacy && *env_cloud_legacy) { - resolved_cloud_key = env_cloud_legacy; - key_from_param_or_env = true; - } else { - resolved_cloud_key = cactus::telemetry::loadCachedCloudApiKey(); - } - - resolved_cloud_key = trim_string(resolved_cloud_key); - - const bool should_cache = - !resolved_cloud_key.empty() && - key_from_param_or_env; - if (should_cache) { - cactus::telemetry::cacheCloudApiKey(resolved_cloud_key.c_str()); - } - - return resolved_cloud_key; -} - -namespace { - -#ifdef CACTUS_USE_CURL -static std::atomic g_warned_missing_cloud_api_key{false}; - -static void apply_curl_tls_trust(CURL* curl) { - if (!curl) return; - const char* ca_bundle = std::getenv("CACTUS_CA_BUNDLE"); - if (ca_bundle && ca_bundle[0] != '\0') { - curl_easy_setopt(curl, CURLOPT_CAINFO, ca_bundle); - } -#if defined(__ANDROID__) - const char* ca_path = std::getenv("CACTUS_CA_PATH"); - if (ca_path && ca_path[0] != '\0') { - curl_easy_setopt(curl, CURLOPT_CAPATH, ca_path); - } else { - curl_easy_setopt(curl, CURLOPT_CAPATH, "/system/etc/security/cacerts"); - } -#endif -} - -static size_t curl_write_cb(void* ptr, size_t size, size_t nmemb, void* userdata) { - auto* s = static_cast(userdata); - s->append(static_cast(ptr), size * nmemb); - return size * nmemb; -} - -static bool read_file_bytes(const std::string& path, std::vector& out) { - std::ifstream in(path, std::ios::binary); - if (!in.is_open()) return false; - in.seekg(0, std::ios::end); - std::streamsize size = in.tellg(); - if (size <= 0) return false; - in.seekg(0, std::ios::beg); - - out.resize(static_cast(size)); - if (!in.read(reinterpret_cast(out.data()), size)) return false; - return true; -} - -static std::string infer_mime_type(const std::string& path) { - auto dot = path.find_last_of('.'); - if (dot == std::string::npos) return "image/jpeg"; - std::string ext = path.substr(dot + 1); - std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); - - if (ext == "png") return "image/png"; - if (ext == "webp") return "image/webp"; - if (ext == "gif") return "image/gif"; - return "image/jpeg"; -} - -static std::string build_cloud_text_prompt(const CloudCompletionRequest& request) { - std::ostringstream oss; - oss << "You are continuing an existing assistant conversation.\\n"; - oss << "Output contract:\\n"; - oss << "1) Never include role prefixes like 'assistant:'.\\n"; - oss << "2) Never include markdown/code fences/backticks.\\n"; - oss << "3) Return only the final assistant answer text unless a tool call is required.\\n"; - oss << "4) If a tool call is required, return ONLY JSON with this exact shape:\\n"; - oss << "[{\"name\":\"tool_name\",\"arguments\":{\"arg\":\"value\"}}]\\n"; - oss << "5) Do not include any prose before or after that JSON tool-call output.\\n"; - oss << "\\nConversation:\\n"; - for (const auto& m : request.messages) { - oss << "[" << m.role << "] " << m.content << "\\n"; - } - - if (!request.tools.empty()) { - oss << "\\nAvailable tools JSON (use only these tool names and arguments):\\n"; - oss << serialize_tools_json(request.tools) << "\\n"; - oss << "If tools are relevant, prefer the strict JSON tool-call output contract above.\\n"; - } - - if (!request.local_output.empty()) { - oss << "\\nLocal model draft (useful fallback reference, may be low confidence):\\n"; - oss << request.local_output << "\\n"; - } - - return oss.str(); -} - -static std::string call_cloud_endpoint(const std::string& url, - const std::string& payload, - long timeout_ms, - const char* cloud_key_param, - std::string& err_out) { - std::string api_key = resolve_cloud_api_key(cloud_key_param); - if (api_key.empty()) { - if (!g_warned_missing_cloud_api_key.exchange(true)) { - CACTUS_LOG_WARN("cloud_handoff", "No cloud key found (cloud_key param, CACTUS_CLOUD_KEY, or CACTUS_CLOUD_API_KEY env); cloud handoff will fall back to local output"); - } - err_out = "missing_api_key"; - return {}; - } - - CURL* curl = curl_easy_init(); - if (!curl) { - err_out = "curl_init_failed"; - return {}; - } - - std::string response_body; - struct curl_slist* headers = nullptr; - headers = curl_slist_append(headers, ("X-API-Key: " + api_key).c_str()); - headers = curl_slist_append(headers, "Content-Type: application/json"); - - const char* extra_hdrs = std::getenv("CACTUS_CLOUD_HEADERS"); - if (extra_hdrs && extra_hdrs[0] != '\0') { - std::istringstream stream(extra_hdrs); - std::string pair; - while (std::getline(stream, pair, ',')) { - auto eq = pair.find('='); - if (eq != std::string::npos && eq > 0) { - std::string header = pair.substr(0, eq) + ": " + pair.substr(eq + 1); - headers = curl_slist_append(headers, header.c_str()); - } - } - } - - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload.c_str()); - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast(payload.size())); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_body); - curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout_ms); - - if (!env_flag_enabled("CACTUS_CLOUD_STRICT_SSL")) { - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); - } else { - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); - apply_curl_tls_trust(curl); - } - - CURLcode res = curl_easy_perform(curl); - long http_status = 0; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_status); - curl_slist_free_all(headers); - curl_easy_cleanup(curl); - - if (res != CURLE_OK) { - err_out = curl_easy_strerror(res); - return {}; - } - - if (http_status >= 400) { - std::string detail = json_string_field(response_body, "error"); - if (detail.empty()) detail = json_string_field(response_body, "message"); - if (detail.empty()) detail = json_string_field(response_body, "detail"); - detail = compact_error_detail(detail); - - err_out = "http_" + std::to_string(http_status); - if (!detail.empty()) { - err_out += ":" + detail; - } - return {}; - } - - return response_body; -} -#endif - -} // namespace - -std::string cloud_base64_encode(const uint8_t* data, size_t len) { - static const char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - std::string out; - out.reserve(((len + 2) / 3) * 4); - for (size_t i = 0; i < len; i += 3) { - uint32_t n = static_cast(data[i]) << 16; - if (i + 1 < len) n |= static_cast(data[i + 1]) << 8; - if (i + 2 < len) n |= static_cast(data[i + 2]); - out += table[(n >> 18) & 0x3F]; - out += table[(n >> 12) & 0x3F]; - out += (i + 1 < len) ? table[(n >> 6) & 0x3F] : '='; - out += (i + 2 < len) ? table[n & 0x3F] : '='; - } - return out; -} - -std::vector cloud_build_wav(const uint8_t* pcm, size_t pcm_bytes) { - constexpr uint32_t sample_rate = 16000; - constexpr uint16_t channels = 1; - constexpr uint16_t bits = 16; - const uint32_t byte_rate = sample_rate * channels * bits / 8; - const uint16_t block_align = channels * bits / 8; - const uint32_t data_size = static_cast(pcm_bytes); - const uint32_t file_size = 36 + data_size; - - std::vector wav(44 + pcm_bytes); - auto w16 = [&](size_t off, uint16_t v) { - wav[off] = v & 0xFF; - wav[off + 1] = v >> 8; - }; - auto w32 = [&](size_t off, uint32_t v) { - wav[off] = v & 0xFF; - wav[off + 1] = (v >> 8) & 0xFF; - wav[off + 2] = (v >> 16) & 0xFF; - wav[off + 3] = (v >> 24) & 0xFF; - }; - - std::memcpy(wav.data(), "RIFF", 4); - w32(4, file_size); - std::memcpy(wav.data() + 8, "WAVE", 4); - std::memcpy(wav.data() + 12, "fmt ", 4); - w32(16, 16); - w16(20, 1); - w16(22, channels); - w32(24, sample_rate); - w32(28, byte_rate); - w16(32, block_align); - w16(34, bits); - std::memcpy(wav.data() + 36, "data", 4); - w32(40, data_size); - std::memcpy(wav.data() + 44, pcm, pcm_bytes); - return wav; -} - -CloudResponse cloud_transcribe_request(const std::string& audio_b64, - const std::string& fallback_text, - long timeout_seconds, - const char* cloud_key) { -#ifdef CACTUS_USE_CURL - std::string base = env_or_default("CACTUS_CLOUD_API_BASE", "https://104.198.76.3/api/v1"); - std::string endpoint = base + "/transcribe"; - - std::string payload = "{\"audio\":\"" + audio_b64 + "\",\"mime_type\":\"audio/wav\",\"language\":\"en-US\"}"; - - std::string err; - std::string body = call_cloud_endpoint(endpoint, payload, timeout_seconds * 1000L, cloud_key, err); - if (body.empty()) { - return {fallback_text, "", false, err.empty() ? "request_failed" : err}; - } - - std::string transcript = json_string_field(body, "transcript"); - if (transcript.empty()) { - transcript = json_string_field(body, "text"); - } - if (transcript.empty()) { - transcript = json_string_field(body, "response"); - } - if (transcript.empty()) { - transcript = json_string_field(body, "analysis"); - } - if (transcript.empty()) { - std::string detail = json_string_field(body, "error"); - if (detail.empty()) detail = json_string_field(body, "message"); - if (detail.empty()) detail = json_string_field(body, "detail"); - detail = compact_error_detail(detail); - if (!detail.empty()) { - return {fallback_text, "", false, "missing_transcript:" + detail}; - } - return {fallback_text, "", false, "missing_transcript"}; - } - - std::string api_key_hash = json_string_field(body, "api_key_hash"); - return {transcript, api_key_hash, true, ""}; -#else - (void)audio_b64; - (void)timeout_seconds; - return {fallback_text, "", false, "curl_not_enabled"}; -#endif -} - -CloudCompletionResult cloud_complete_request(const CloudCompletionRequest& request, - long timeout_ms) { -#ifdef CACTUS_USE_CURL - std::string base = env_or_default("CACTUS_CLOUD_API_BASE", "https://104.198.76.3/api/v1"); - std::string model = env_or_default("CACTUS_CLOUD_MODEL", "gemini-2.5-flash"); - - std::string endpoint; - std::string payload; - - std::string img_b64; - std::string img_mime; - if (request.has_images) { - for (const auto& message : request.messages) { - for (const auto& image : message.images) { - if (!image.empty()) { - std::vector img_bytes; - if (read_file_bytes(image, img_bytes)) { - img_b64 = cloud_base64_encode(img_bytes.data(), img_bytes.size()); - img_mime = infer_mime_type(image); - } - break; - } - } - if (!img_b64.empty()) break; - } - } - - std::string audio_b64; - if (request.has_audio && !request.audio_pcm.empty()) { - auto wav = cloud_build_wav(request.audio_pcm.data(), request.audio_pcm.size()); - audio_b64 = cloud_base64_encode(wav.data(), wav.size()); - } - - std::string text_prompt = build_cloud_text_prompt(request); - bool use_omni = !audio_b64.empty() || (request.has_images && request.has_audio); - - if (use_omni) { - endpoint = base + "/omni"; - payload = "{"; - payload += "\"text\":\"" + escape_json_string(text_prompt) + "\""; - if (!img_b64.empty()) { - payload += ",\"image\":\"" + img_b64 + "\""; - payload += ",\"image_mime_type\":\"" + img_mime + "\""; - } - if (!audio_b64.empty()) { - payload += ",\"audio\":\"" + audio_b64 + "\""; - payload += ",\"audio_mime_type\":\"audio/wav\""; - } - payload += ",\"model\":\"" + escape_json_string(model) + "\""; - payload += ",\"language\":\"en-US\""; - payload += "}"; - } else if (request.has_images && !img_b64.empty()) { - endpoint = base + "/vlm"; - payload = "{" - "\"image\":\"" + img_b64 + "\"," - "\"mime_type\":\"" + img_mime + "\"," - "\"prompt\":\"" + escape_json_string(text_prompt) + "\"," - "\"language\":\"en-US\"," - "\"model\":\"" + escape_json_string(model) + "\"" - "}"; - } else { - endpoint = base + "/text"; - payload = "{" - "\"text\":\"" + escape_json_string(text_prompt) + "\"," - "\"language\":\"en-US\"," - "\"model\":\"" + escape_json_string(model) + "\"" - "}"; - } - - std::string err; - const char* cloud_key = request.cloud_key.empty() ? nullptr : request.cloud_key.c_str(); - std::string body = call_cloud_endpoint(endpoint, payload, timeout_ms, cloud_key, err); - if (body.empty()) { - return {false, false, "", {}, err.empty() ? "request_failed" : err}; - } - - std::vector function_calls; - std::string calls_json = json_array_field(body, "function_calls"); - if (calls_json != "[]") { - function_calls = split_json_array(calls_json); - } - - std::string response = json_string_field(body, "text"); - if (response.empty()) { - response = json_string_field(body, "analysis"); - } - - if (!response.empty() && function_calls.empty()) { - std::string regular_response; - parse_function_calls_from_response(response, regular_response, function_calls); - response = regular_response; - } - - if (!response.empty() && function_calls.empty()) { - auto first = response.find_first_not_of(" \t\n\r"); - auto last = response.find_last_not_of(" \t\n\r"); - if (first != std::string::npos && last != std::string::npos) { - std::string trimmed = response.substr(first, last - first + 1); - if (!trimmed.empty() && trimmed.front() == '[' && trimmed.back() == ']' && - trimmed.find("\"name\"") != std::string::npos) { - function_calls = split_json_array(trimmed); - response.clear(); - } - } - } - - if (response.empty() && function_calls.empty()) { - return {false, false, "", {}, "missing_text"}; - } - - CloudCompletionResult out; - out.ok = true; - out.used_cloud = true; - out.response = response; - out.function_calls = std::move(function_calls); - return out; -#else - (void)request; - (void)timeout_ms; - return {false, false, "", {}, "curl_not_enabled"}; -#endif -} - -} // namespace ffi -} // namespace cactus diff --git a/cactus/ffi/cactus_cloud.h b/cactus/ffi/cactus_cloud.h deleted file mode 100644 index 6911d9a91..000000000 --- a/cactus/ffi/cactus_cloud.h +++ /dev/null @@ -1,50 +0,0 @@ -#ifndef CACTUS_CLOUD_H -#define CACTUS_CLOUD_H - -#include "cactus_utils.h" -#include -#include - -namespace cactus { -namespace ffi { - -struct CloudResponse { - std::string transcript; - std::string api_key_hash; - bool used_cloud = false; - std::string error; -}; - -struct CloudCompletionRequest { - std::vector messages; - std::vector tools; - std::string local_output; - std::vector local_function_calls; - bool has_images = false; - bool has_audio = false; - std::vector audio_pcm; - std::string cloud_key; -}; - -struct CloudCompletionResult { - bool ok = false; - bool used_cloud = false; - std::string response; - std::vector function_calls; - std::string error; -}; - -std::string cloud_base64_encode(const uint8_t* data, size_t len); -std::vector cloud_build_wav(const uint8_t* pcm, size_t pcm_bytes); -std::string resolve_cloud_api_key(const char* cloud_key_param); -CloudResponse cloud_transcribe_request(const std::string& audio_b64, - const std::string& fallback_text, - long timeout_seconds = 15L, - const char* cloud_key = nullptr); -CloudCompletionResult cloud_complete_request(const CloudCompletionRequest& request, - long timeout_ms); - -} // namespace ffi -} // namespace cactus - -#endif // CACTUS_CLOUD_H diff --git a/cactus/ffi/cactus_complete.cpp b/cactus/ffi/cactus_complete.cpp deleted file mode 100644 index 461e8b8ff..000000000 --- a/cactus/ffi/cactus_complete.cpp +++ /dev/null @@ -1,1371 +0,0 @@ -#include "cactus_ffi.h" -#include "cactus_cloud.h" -#include "cactus_utils.h" -#include "needle_tools.h" -#include "telemetry/telemetry.h" -#include "../../libs/audio/wav.h" -#include -#include -#include -#include -#include -#include -#include - -using namespace cactus::engine; -using namespace cactus::ffi; - -static constexpr size_t DEFAULT_ROLLING_ENTROPY_WINDOW = 10; - -namespace { - -std::vector> extract_schema_property_types(const std::string& schema); -std::vector extract_schema_required(const std::string& schema); - -std::string extract_last_user_query(const std::vector& messages) { - for (auto it = messages.rbegin(); it != messages.rend(); ++it) { - if (it->role == "user") { - return it->content; - } - } - return {}; -} - -std::vector encode_needle_tools_suffix(Tokenizer* tokenizer, const std::string& formatted_tools) { - auto prefix = tokenizer->encode(""); - auto body = tokenizer->encode(formatted_tools); - std::vector tokens; - tokens.reserve(prefix.size() + body.size() + 1); - tokens.insert(tokens.end(), prefix.begin(), prefix.end()); - tokens.insert(tokens.end(), body.begin(), body.end()); - tokens.push_back(tokenizer->get_eos_token()); - return tokens; -} - -void inject_rag_context(CactusModelHandle* handle, std::vector& messages) { - if (!handle->corpus_index) return; - - std::string query = extract_last_user_query(messages); - if (query.empty()) return; - - std::string rag_context = retrieve_rag_context(handle, query); - if (rag_context.empty()) return; - - if (!messages.empty() && messages[0].role == "system") { - messages[0].content = rag_context + messages[0].content; - } else { - ChatMessage system_msg; - system_msg.role = "system"; - system_msg.content = rag_context + "Answer the user's question using ONLY the context above. Do not use any prior knowledge. If the answer cannot be found in the context, respond with \"I don't have enough information to answer that.\""; - messages.insert(messages.begin(), system_msg); - } -} - -std::vector build_tool_constraint_specs(const std::vector& tools) { - std::vector specs; - specs.reserve(tools.size()); - - for (const auto& tool : tools) { - ToolConstraintSpec spec; - spec.name = tool.name; - - auto schema_it = tool.parameters.find("schema"); - if (schema_it != tool.parameters.end()) { - auto properties = extract_schema_property_types(schema_it->second); - spec.parameter_names.reserve(properties.size()); - for (const auto& [name, _] : properties) { - spec.parameter_names.push_back(name); - } - spec.required_parameter_names = extract_schema_required(schema_it->second); - } - - specs.push_back(std::move(spec)); - } - - return specs; -} - -void strip_thinking_from_cache(CactusModelHandle* handle, - const std::vector& generated_tokens, - size_t prompt_len) { - const auto& cfg = handle->model->get_config(); - uint32_t open_id = cfg.channel_open_token_id; - uint32_t close_id = cfg.channel_close_token_id; - auto ranges = find_channel_token_ranges(generated_tokens, prompt_len, - open_id, close_id); - if (ranges.empty()) return; - - handle->model->remove_thinking_tokens(ranges); - for (auto it = ranges.rbegin(); it != ranges.rend(); ++it) { - auto start = handle->processed_tokens.begin() + it->first; - handle->processed_tokens.erase(start, start + it->second); - } -} - -void setup_tool_constraints(CactusModelHandle* handle, const std::vector& tools, - bool force_tools, float& temperature, - const std::unordered_map& needle_name_map = {}) { - if (!force_tools || tools.empty()) return; - - auto specs = build_tool_constraint_specs(tools); - - if (!needle_name_map.empty()) { - for (auto& spec : specs) { - std::string snake = needle::to_snake_case(spec.name); - spec.name = snake; - } - } - - handle->model->set_tool_constraints(specs); - - if (temperature == 0.0f) { - temperature = 0.01f; - } -} - -size_t find_json_block_end(const std::string& json, size_t start) { - if (start >= json.size() || json[start] != '{') { - return std::string::npos; - } - - int depth = 1; - bool in_string = false; - bool escaped = false; - size_t pos = start + 1; - while (pos < json.size() && depth > 0) { - char c = json[pos]; - if (in_string) { - if (escaped) { - escaped = false; - } else if (c == '\\') { - escaped = true; - } else if (c == '"') { - in_string = false; - } - } else { - if (c == '"') { - in_string = true; - } else if (c == '{') { - depth++; - } else if (c == '}') { - depth--; - } - } - ++pos; - } - - return depth == 0 ? pos : std::string::npos; -} - -std::string extract_json_object_field(const std::string& json, const std::string& key) { - std::string pattern = "\"" + key + "\":"; - size_t key_pos = json.find(pattern); - if (key_pos == std::string::npos) { - return {}; - } - - size_t object_start = json.find('{', key_pos + pattern.size()); - if (object_start == std::string::npos) { - return {}; - } - - size_t object_end = find_json_block_end(json, object_start); - if (object_end == std::string::npos) { - return {}; - } - - return json.substr(object_start, object_end - object_start); -} - -std::vector> extract_schema_property_types(const std::string& schema) { - std::vector> properties; - std::string properties_object = extract_json_object_field(schema, "properties"); - if (properties_object.empty() || properties_object.size() < 2) { - return properties; - } - - size_t pos = 1; - while (pos + 1 < properties_object.size()) { - size_t key_start = properties_object.find('"', pos); - if (key_start == std::string::npos || key_start + 1 >= properties_object.size()) { - break; - } - size_t key_end = properties_object.find('"', key_start + 1); - if (key_end == std::string::npos) { - break; - } - - std::string name = properties_object.substr(key_start + 1, key_end - key_start - 1); - size_t value_start = properties_object.find('{', key_end); - if (value_start == std::string::npos) { - break; - } - size_t value_end = find_json_block_end(properties_object, value_start); - if (value_end == std::string::npos) { - break; - } - - std::string value = properties_object.substr(value_start, value_end - value_start); - std::string type = "string"; - std::string type_pattern = "\"type\":\""; - size_t type_pos = value.find(type_pattern); - if (type_pos != std::string::npos) { - size_t type_start = type_pos + type_pattern.size(); - size_t type_end = value.find('"', type_start); - if (type_end != std::string::npos) { - type = value.substr(type_start, type_end - type_start); - } - } else if (value.find("\"enum\"") != std::string::npos) { - type = "string"; - } else if (value.find("\"properties\"") != std::string::npos) { - type = "object"; - } - - properties.emplace_back(std::move(name), std::move(type)); - pos = value_end; - } - - return properties; -} - -std::vector extract_schema_required(const std::string& schema) { - std::vector required; - std::string key = "\"required\""; - size_t key_pos = schema.find(key); - if (key_pos == std::string::npos) return required; - size_t arr_start = schema.find('[', key_pos + key.size()); - if (arr_start == std::string::npos) return required; - size_t arr_end = schema.find(']', arr_start); - if (arr_end == std::string::npos) return required; - size_t pos = arr_start + 1; - while (pos < arr_end) { - size_t qs = schema.find('"', pos); - if (qs == std::string::npos || qs >= arr_end) break; - size_t qe = schema.find('"', qs + 1); - if (qe == std::string::npos || qe > arr_end) break; - required.push_back(schema.substr(qs + 1, qe - qs - 1)); - pos = qe + 1; - } - return required; -} - -std::string serialize_needle_tools(const std::vector& tools, - const char* raw_tools_json, - std::unordered_map& name_map) { - name_map.clear(); - - if (raw_tools_json && std::strlen(raw_tools_json) > 2) { - std::string raw(raw_tools_json); - - bool is_needle_format = (raw.find("\"parameters\"") != std::string::npos && - raw.find("\"schema\"") == std::string::npos && - raw.find("\"type\":\"function\"") == std::string::npos); - - if (is_needle_format) { - std::string result; - result.reserve(raw.size()); - const std::string name_key = "\"name\":"; - size_t pos = 0; - - while (pos < raw.size()) { - size_t found = raw.find(name_key, pos); - if (found == std::string::npos) { - result.append(raw, pos, raw.size() - pos); - break; - } - result.append(raw, pos, found + name_key.size() - pos); - pos = found + name_key.size(); - - while (pos < raw.size() && (raw[pos] == ' ' || raw[pos] == '\t' || - raw[pos] == '\n' || raw[pos] == '\r')) { - result += raw[pos]; - pos++; - } - if (pos >= raw.size() || raw[pos] != '"') continue; - result += '"'; - pos++; - - size_t name_start = pos; - bool escaped = false; - while (pos < raw.size()) { - if (escaped) { escaped = false; pos++; continue; } - if (raw[pos] == '\\') { escaped = true; pos++; continue; } - if (raw[pos] == '"') break; - pos++; - } - std::string orig_name = raw.substr(name_start, pos - name_start); - std::string snake_name = needle::to_snake_case(orig_name); - name_map[snake_name] = orig_name; - - result += snake_name; - } - - std::string compact; - compact.reserve(result.size()); - bool in_str = false; - bool esc = false; - size_t ri = 0; - while (ri < result.size()) { - unsigned char ch = static_cast(result[ri]); - if (in_str) { - if (esc) { - compact += static_cast(ch); - esc = false; - } else if (ch == '\\') { - compact += '\\'; - esc = true; - } else if (ch == '"') { - compact += '"'; - in_str = false; - } else if (ch >= 0x80) { - uint32_t cp = 0; - int extra = 0; - if ((ch & 0xE0) == 0xC0) { cp = ch & 0x1F; extra = 1; } - else if ((ch & 0xF0) == 0xE0) { cp = ch & 0x0F; extra = 2; } - else if ((ch & 0xF8) == 0xF0) { cp = ch & 0x07; extra = 3; } - else { compact += static_cast(ch); ri++; continue; } - for (int e = 0; e < extra && ri + 1 < result.size(); e++) { - ri++; - cp = (cp << 6) | (static_cast(result[ri]) & 0x3F); - } - if (cp <= 0xFFFF) { - char buf[8]; - std::snprintf(buf, sizeof(buf), "\\u%04x", cp); - compact += buf; - } else { - cp -= 0x10000; - char buf[14]; - std::snprintf(buf, sizeof(buf), "\\u%04x\\u%04x", - 0xD800 + (cp >> 10), 0xDC00 + (cp & 0x3FF)); - compact += buf; - } - } else { - compact += static_cast(ch); - } - } else { - if (ch == '"') { in_str = true; compact += '"'; } - else if (ch != ' ' && ch != '\t' && ch != '\n' && ch != '\r') { - compact += static_cast(ch); - } - } - ri++; - } - - return compact; - } - } - - if (tools.empty()) { - return "[]"; - } - - std::ostringstream oss; - oss << "["; - for (size_t i = 0; i < tools.size(); ++i) { - if (i > 0) { - oss << ","; - } - - std::string snake_name = needle::to_snake_case(tools[i].name); - name_map[snake_name] = tools[i].name; - - oss << "{\"name\":\"" << escape_json_string(snake_name) << "\""; - if (!tools[i].description.empty()) { - oss << ",\"description\":\"" << escape_json_string(tools[i].description) << "\""; - } - - oss << ",\"parameters\":"; - auto schema_it = tools[i].parameters.find("schema"); - if (schema_it == tools[i].parameters.end()) { - oss << "{}"; - } else { - auto properties = extract_schema_property_types(schema_it->second); - auto required = extract_schema_required(schema_it->second); - std::unordered_set required_set(required.begin(), required.end()); - - if (properties.empty()) { - oss << "{}"; - } else { - std::string properties_object = extract_json_object_field(schema_it->second, "properties"); - - oss << "{"; - for (size_t p = 0; p < properties.size(); ++p) { - if (p > 0) { - oss << ","; - } - const std::string& param_name = properties[p].first; - const std::string& param_type = properties[p].second; - bool is_required = required_set.count(param_name) > 0; - - std::string description; - std::string prop_obj = extract_json_object_field(properties_object, param_name); - if (!prop_obj.empty()) { - std::string desc_pattern = "\"description\":\""; - size_t desc_pos = prop_obj.find(desc_pattern); - if (desc_pos != std::string::npos) { - size_t desc_start = desc_pos + desc_pattern.size(); - size_t desc_end = desc_start; - bool escaped = false; - while (desc_end < prop_obj.size()) { - if (escaped) { escaped = false; desc_end++; continue; } - if (prop_obj[desc_end] == '\\') { escaped = true; desc_end++; continue; } - if (prop_obj[desc_end] == '"') break; - desc_end++; - } - description = prop_obj.substr(desc_start, desc_end - desc_start); - } - } - - oss << "\"" << escape_json_string(param_name) << "\":{"; - oss << "\"type\":\"" << escape_json_string(param_type) << "\""; - if (!description.empty()) { - oss << ",\"description\":\"" << description << "\""; - } - oss << ",\"required\":" << (is_required ? "true" : "false"); - oss << "}"; - } - oss << "}"; - } - } - - oss << "}"; - } - oss << "]"; - return oss.str(); -} - -std::vector> build_stop_sequences( - Tokenizer* tokenizer, - const std::vector& stop_sequences, - Config::ModelType model_type, - bool has_tools -) { - std::vector> stop_token_sequences; - stop_token_sequences.push_back({tokenizer->get_eos_token()}); - - std::vector sequences = stop_sequences; - if (sequences.empty()) { - std::string default_stop = tokenizer->get_default_stop_sequence(); - if (!default_stop.empty()) { - sequences.push_back(default_stop); - } - } - for (const auto& stop_seq : sequences) { - stop_token_sequences.push_back(tokenizer->encode(stop_seq)); - } - - if ((model_type == Config::ModelType::GEMMA || model_type == Config::ModelType::GEMMA3N) && has_tools) { - stop_token_sequences.push_back(tokenizer->encode("")); - stop_token_sequences.push_back(tokenizer->encode("")); - } - - if (model_type == Config::ModelType::GEMMA4) { - stop_token_sequences.push_back(tokenizer->encode("")); - if (has_tools) { - stop_token_sequences.push_back(tokenizer->encode("")); - stop_token_sequences.push_back(tokenizer->encode("<|tool_response>")); - } - } - - return stop_token_sequences; -} - -void trim_stop_suffix(std::vector& generated_tokens, - const std::vector>& stop_token_sequences, - bool include_stop_sequences) { - if (include_stop_sequences) return; - for (const auto& stop_seq : stop_token_sequences) { - if (stop_seq.empty()) continue; - if (generated_tokens.size() >= stop_seq.size() && - std::equal(stop_seq.rbegin(), stop_seq.rend(), generated_tokens.rbegin())) { - generated_tokens.resize(generated_tokens.size() - stop_seq.size()); - break; - } - } -} - -void reset_cache(CactusModelHandle* handle) { - handle->model->reset_cache(); - handle->processed_tokens.clear(); - handle->processed_images.clear(); - handle->user_audio_counts.clear(); -} - -struct PrefillResult { - std::vector remaining_tokens; - size_t prefilled_count = 0; - bool was_prefix = false; - bool was_exact_match = false; -}; - -struct EntropyState { - std::vector window; - float window_sum = 0.0f; - float total_sum = 0.0f; - size_t total_count = 0; - bool spike_handoff = false; - size_t window_size = DEFAULT_ROLLING_ENTROPY_WINDOW; - - void add(float entropy) { - window.push_back(entropy); - window_sum += entropy; - total_sum += entropy; - total_count++; - - if (window.size() > window_size) { - window_sum -= window.front(); - window.erase(window.begin()); - } - } - - float rolling_confidence() const { - return 1.0f - (window_sum / window.size()); - } - - float mean_confidence() const { - return 1.0f - (total_sum / static_cast(total_count)); - } -}; - -struct PreparedPrompt { - InferenceOptions options; - Config::ModelType model_type = Config::ModelType::QWEN; - std::vector image_paths; - std::vector audio_paths; - std::vector messages; - std::vector tools; - std::vector tokens; - size_t context_token_count = 0; - std::vector> images; - - std::vector audio_features; - size_t audio_num_frames = 0; - - std::unordered_map needle_name_map; - - bool has_images() const { - return std::any_of(images.begin(), images.end(), - [](const auto& msg_imgs) { return !msg_imgs.empty(); }); - } - - bool has_audio() const { - return !audio_features.empty(); - } -}; - -CactusModelHandle::ProcessedImage image_signature(const std::string& image_path) { - std::filesystem::path normalized_path(image_path); - std::error_code ec; - - auto absolute_path = std::filesystem::absolute(normalized_path, ec); - if (!ec) { - normalized_path = absolute_path; - } - - CactusModelHandle::ProcessedImage image; - image.path = normalized_path.string(); - - ec.clear(); - auto status = std::filesystem::status(normalized_path, ec); - if (!ec && std::filesystem::is_regular_file(status)) { - std::error_code time_ec; - auto mtime = std::filesystem::last_write_time(normalized_path, time_ec); - if (!time_ec) { - image.last_modified_timestamp = static_cast(mtime.time_since_epoch().count()); - } - } - - return image; -} - -std::vector> images_from_message(const std::vector& messages) { - std::vector> message_signatures; - message_signatures.reserve(messages.size()); - - for (const auto& message : messages) { - std::vector image_signatures; - image_signatures.reserve(message.images.size()); - for (const auto& image_path : message.images) { - image_signatures.push_back(image_signature(image_path)); - } - message_signatures.push_back(std::move(image_signatures)); - } - - return message_signatures; -} - -bool image_context_prefix_matches( - const std::vector>& prefix, - const std::vector>& full -) { - return prefix.size() <= full.size() && - std::equal(prefix.begin(), prefix.end(), full.begin()); -} - -bool prompt_context_matches( - const CactusModelHandle* handle, - const PreparedPrompt& prompt -) { - if (handle->processed_tokens.empty()) { - return false; - } - if (handle->model && - handle->model->get_config().model_type == Config::ModelType::NEEDLE && - prompt.tokens.size() != handle->processed_tokens.size() + 1) { - return false; - } - if (prompt.context_token_count < handle->processed_tokens.size()) { - return false; - } - if (!std::equal(handle->processed_tokens.begin(), handle->processed_tokens.end(), prompt.tokens.begin())) { - return false; - } - if (prompt.has_images()) { - return image_context_prefix_matches(handle->processed_images, prompt.images); - } - return !prompt.has_images(); -} - -PreparedPrompt prepare_prompt( - CactusModelHandle* handle, - const char* messages_json, - const char* options_json, - const char* tools_json, - bool apply_tool_constraints, - bool add_generation_prompt, - const uint8_t* pcm_buffer = nullptr, - size_t pcm_buffer_size = 0 -) { - if (!handle || !handle->model) { - throw std::runtime_error("Invalid model handle"); - } - - PreparedPrompt prompt; - prompt.options = parse_inference_options_json(options_json ? options_json : ""); - prompt.messages = parse_messages_json(messages_json, prompt.image_paths, &prompt.audio_paths); - if (prompt.messages.empty()) { - throw std::runtime_error("No messages provided"); - } - - inject_rag_context(handle, prompt.messages); - - if (tools_json && std::strlen(tools_json) > 0) { - prompt.tools = parse_tools_json(tools_json); - } - - if (prompt.options.tool_rag_top_k > 0 && prompt.tools.size() > prompt.options.tool_rag_top_k) { - std::string query = extract_last_user_query(prompt.messages); - if (!query.empty()) { - prompt.tools = select_relevant_tools(handle, query, prompt.tools, prompt.options.tool_rag_top_k); - } - } - - if (prompt.model_type == Config::ModelType::NEEDLE && !prompt.tools.empty()) { - prompt.options.force_tools = true; - } - - if (apply_tool_constraints && prompt.model_type != Config::ModelType::NEEDLE) { - setup_tool_constraints(handle, prompt.tools, prompt.options.force_tools, prompt.options.temperature); - } - - auto* tokenizer = handle->model->get_tokenizer(); - if (!tokenizer) { - throw std::runtime_error("Tokenizer unavailable"); - } - - prompt.model_type = handle->model->get_config().model_type; - - if (prompt.options.confidence_threshold < 0.0f) { - float model_default = handle->model->get_config().default_cloud_handoff_threshold; - prompt.options.confidence_threshold = (model_default > 0.0f) ? model_default : 0.7f; - } - - if (prompt.model_type == Config::ModelType::GEMMA4) { - std::vector audio_samples; - if (pcm_buffer != nullptr && pcm_buffer_size > 1) { - auto waveform_fp32 = cactus::audio::pcm_buffer_to_float_samples(pcm_buffer, pcm_buffer_size); - audio_samples = resample_to_16k_fp32(waveform_fp32, 16000); - } else if (!prompt.audio_paths.empty()) { - for (auto it = prompt.messages.rbegin(); it != prompt.messages.rend(); ++it) { - if (!it->audio.empty()) { - const std::string& audio_path = it->audio.back(); - AudioFP32 wav = load_wav(audio_path); - audio_samples = resample_to_16k_fp32(wav.samples, wav.sample_rate); - break; - } - } - } - std::vector user_indices; - for (size_t i = 0; i < prompt.messages.size(); i++) { - if (prompt.messages[i].role == "user") user_indices.push_back(i); - } - auto& counts = handle->user_audio_counts; - for (size_t u = 0; u + 1 < user_indices.size(); u++) { - if (u < counts.size() && counts[u] > 0) { - prompt.messages[user_indices[u]].audio_soft_token_count = counts[u]; - } - } - if (!audio_samples.empty() && !user_indices.empty()) { - auto audio_prep = cactus::audio::preprocess_audio_for_gemma4(audio_samples, handle->model->get_config()); - prompt.audio_features = std::move(audio_prep.features); - prompt.audio_num_frames = audio_prep.num_frames; - size_t u = user_indices.size() - 1; - prompt.messages[user_indices[u]].audio_soft_token_count = audio_prep.num_soft_tokens; - if (counts.size() <= u) counts.resize(u + 1, 0); - counts[u] = audio_prep.num_soft_tokens; - } - } - - std::string formatted_tools; - if (Config::is_gemma_family(prompt.model_type)) { - formatted_tools = gemma::format_tools(prompt.tools, prompt.model_type == Config::ModelType::GEMMA4); - } else if (prompt.model_type == Config::ModelType::NEEDLE) { - formatted_tools = serialize_needle_tools(prompt.tools, tools_json, prompt.needle_name_map); - } else if (prompt.model_type == Config::ModelType::QWEN || prompt.model_type == Config::ModelType::QWEN3P5) { - formatted_tools = serialize_tools_for_template(prompt.tools); - } else { - formatted_tools = serialize_tools_json(prompt.tools); - } - - if (prompt.model_type == Config::ModelType::NEEDLE) { - if (apply_tool_constraints) { - setup_tool_constraints(handle, prompt.tools, prompt.options.force_tools, - prompt.options.temperature, prompt.needle_name_map); - } - - std::string query_text = cactus::engine::format_needle_query_text(prompt.messages); - prompt.tokens = tokenizer->encode(query_text); - auto suffix_tokens = encode_needle_tools_suffix(tokenizer, formatted_tools); - prompt.tokens.insert(prompt.tokens.end(), suffix_tokens.begin(), suffix_tokens.end()); - } else { - std::string full_prompt = tokenizer->format_chat_prompt( - prompt.messages, - add_generation_prompt, - formatted_tools, - prompt.options.enable_thinking_if_supported - ); - if (full_prompt.find("ERROR:") == 0) { - throw std::runtime_error(full_prompt.substr(6)); - } - prompt.tokens = tokenizer->encode(full_prompt); - } - prompt.context_token_count = prompt.tokens.size(); - prompt.images = images_from_message(prompt.messages); - return prompt; -} - -PrefillResult do_prefill( - CactusModelHandle* handle, - const PreparedPrompt& prompt, - const std::vector& target_tokens -) { - PrefillResult result = {}; - bool has_images = prompt.has_images(); - - result.was_prefix = prompt_context_matches(handle, prompt); - result.was_exact_match = result.was_prefix && - target_tokens.size() == handle->processed_tokens.size(); - - if (result.was_exact_match) { - return result; - } - - std::vector tokens_to_process; - if (!result.was_prefix) { - reset_cache(handle); - tokens_to_process = target_tokens; - } else { - tokens_to_process.assign( - target_tokens.begin() + handle->processed_tokens.size(), - target_tokens.end() - ); - } - - if (tokens_to_process.size() > 1) { - std::vector prefill_tokens(tokens_to_process.begin(), tokens_to_process.end() - 1); - result.prefilled_count = prefill_tokens.size(); - if (has_images) { - std::vector delta_image_paths; - if (result.was_prefix) { - size_t cached_image_count = 0; - for (const auto& msg_imgs : handle->processed_images) { - cached_image_count += msg_imgs.size(); - } - delta_image_paths.assign( - prompt.image_paths.begin() + cached_image_count, - prompt.image_paths.end() - ); - } else { - delta_image_paths = prompt.image_paths; - } - handle->model->prefill_with_images(prefill_tokens, delta_image_paths); - } else { - handle->model->prefill(prefill_tokens, handle->model->get_prefill_chunk_size()); - } - result.remaining_tokens = {tokens_to_process.back()}; - } else { - result.remaining_tokens = tokens_to_process; - } - - return result; -} - -uint32_t decode( - std::unique_ptr& model, - const std::vector& tokens, - const InferenceOptions& options, - float* out_entropy -) { - return model->decode(tokens, options.temperature, options.top_p, options.top_k, - "", out_entropy, options.min_p, options.repetition_penalty); -} - -uint32_t generate_first_token( - CactusModelHandle* handle, - const PrefillResult& prefill_result, - const PreparedPrompt& prompt, - float* first_token_entropy -) { - if (prefill_result.was_exact_match || prefill_result.remaining_tokens.empty()) { - if (handle->processed_tokens.empty()) { - throw std::runtime_error("Cannot generate from empty prompt"); - } - return decode(handle->model, {handle->processed_tokens.back()}, prompt.options, first_token_entropy); - } - return decode(handle->model, prefill_result.remaining_tokens, prompt.options, first_token_entropy); -} - -std::string construct_prefill_response_json( - bool success, - const std::string* error, - size_t prefill_tokens, - double prefill_tps, - double total_time_ms -) { - std::ostringstream json; - json << "{"; - json << "\"success\":" << (success ? "true" : "false") << ","; - if (error) { - json << "\"error\":\"" << escape_json_string(*error) << "\","; - } else { - json << "\"error\":null,"; - } - json << "\"prefill_tokens\":" << prefill_tokens << ","; - json << "\"prefill_tps\":" << std::fixed << std::setprecision(2) << prefill_tps << ","; - json << "\"total_time_ms\":" << std::fixed << std::setprecision(2) << total_time_ms << ","; - json << "\"ram_usage_mb\":" << std::fixed << std::setprecision(2) << get_ram_usage_mb(); - json << "}"; - return json.str(); -} - -} // anonymous namespace - -extern "C" { - -int cactus_complete( - cactus_model_t model, - const char* messages_json, - char* response_buffer, - size_t buffer_size, - const char* options_json, - const char* tools_json, - cactus_token_callback callback, - void* user_data, - const uint8_t* pcm_buffer, - size_t pcm_buffer_size -) { - if (!model) { - std::string error_msg = last_error_message.empty() ? - "Model not initialized. Check model path and files." : last_error_message; - CACTUS_LOG_ERROR("complete", error_msg); - handle_error_response(error_msg, response_buffer, buffer_size); - return -1; - } - - if (!messages_json || !response_buffer || buffer_size == 0) { - CACTUS_LOG_ERROR("complete", "Invalid parameters: messages_json, response_buffer, or buffer_size"); - handle_error_response("Invalid parameters", response_buffer, buffer_size); - return -1; - } - - try { - auto start_time = std::chrono::high_resolution_clock::now(); - - auto* handle = static_cast(model); - handle->should_stop = false; - auto* tokenizer = handle->model->get_tokenizer(); - auto prompt = prepare_prompt(handle, messages_json, options_json, tools_json, true, true, pcm_buffer, pcm_buffer_size); - - CACTUS_LOG_DEBUG("complete", "Prompt tokens: " << prompt.tokens.size() - << ", max_tokens: " << prompt.options.max_tokens); - - bool has_images = prompt.has_images(); - bool has_audio = prompt.has_audio(); - const bool has_gemma4_mixed_media = prompt.model_type == Config::ModelType::GEMMA4 && has_images && has_audio; - auto decode_gemma4_mixed_media = [&](const std::vector& tokens, float* out_entropy) -> uint32_t { - auto* gemma4_mm = dynamic_cast(handle->model.get()); - if (!gemma4_mm) { - throw std::runtime_error("Gemma4 mixed-media decode requested on non-Gemma4 multimodal model"); - } - return gemma4_mm->decode_with_media( - tokens, - prompt.image_paths, - prompt.audio_features, - prompt.options.temperature, prompt.options.top_p, prompt.options.top_k, - "", out_entropy, - prompt.options.min_p, prompt.options.repetition_penalty - ); - }; - - auto stop_token_sequences = build_stop_sequences(tokenizer, prompt.options.stop_sequences, prompt.model_type, !prompt.tools.empty()); - - std::vector generated_tokens; - double time_to_first_token = 0.0; - float first_token_entropy = 0.0f; - uint32_t next_token; - size_t prompt_tokens; - - if ((has_gemma4_mixed_media || has_audio) && !handle->processed_tokens.empty()) { - auto& cache = handle->processed_tokens; - size_t common = 0; - size_t limit = std::min(cache.size(), prompt.tokens.size()); - while (common < limit && cache[common] == prompt.tokens[common]) common++; - if (common < cache.size()) { - CACTUS_LOG_WARN("complete", "KV cache diverges from new prompt at position " << common - << "/" << cache.size() << "; trimming and re-prefilling the divergent suffix"); - size_t kv_len = handle->model->get_cache_size(); - if (kv_len > common) { - handle->model->remove_thinking_tokens({{common, kv_len - common}}); - } - cache.resize(common); - } - } - - if (has_gemma4_mixed_media) { - prompt_tokens = prompt.tokens.size(); - next_token = decode_gemma4_mixed_media(prompt.tokens, &first_token_entropy); - } else if (has_audio) { - prompt_tokens = prompt.tokens.size(); - next_token = handle->model->decode_with_audio( - prompt.tokens, prompt.audio_features, - prompt.options.temperature, prompt.options.top_p, prompt.options.top_k, - "", &first_token_entropy, - prompt.options.min_p, prompt.options.repetition_penalty); - } else { - auto prefill_result = do_prefill(handle, prompt, prompt.tokens); - prompt_tokens = prefill_result.prefilled_count + prefill_result.remaining_tokens.size(); - next_token = generate_first_token(handle, prefill_result, prompt, &first_token_entropy); - } - - handle->processed_tokens = prompt.tokens; - handle->processed_images = prompt.images; - - auto token_end = std::chrono::high_resolution_clock::now(); - time_to_first_token = std::chrono::duration_cast(token_end - start_time).count() / 1000.0; - - float confidence = 1.0f - first_token_entropy; - bool cloud_used = false; - std::string cloud_error; - std::future cloud_future; - bool cloud_future_started = false; - const bool cloud_eligible = prompt.options.auto_handoff && (!has_images || prompt.options.handoff_with_images); - - auto maybe_start_cloud_handoff = [&](const std::string& local_output_hint, - const std::vector& local_calls_hint) { - if (!cloud_eligible || cloud_future_started) { - return; - } - CloudCompletionRequest request; - request.messages = prompt.messages; - request.tools = prompt.tools; - request.local_output = local_output_hint; - request.local_function_calls = local_calls_hint; - request.has_images = has_images; - request.has_audio = has_audio; - if (has_audio && pcm_buffer != nullptr && pcm_buffer_size > 0) { - request.audio_pcm.assign(pcm_buffer, pcm_buffer + pcm_buffer_size); - } - request.cloud_key = resolve_cloud_api_key(nullptr); - - cloud_future_started = true; - cloud_future = std::async(std::launch::async, [request, &prompt]() { - return cloud_complete_request(request, static_cast(prompt.options.cloud_timeout_ms)); - }); - }; - - if (confidence < prompt.options.confidence_threshold) { - maybe_start_cloud_handoff("", {}); - } - - generated_tokens.push_back(next_token); - handle->processed_tokens.push_back(next_token); - - if (prompt.options.force_tools && !prompt.tools.empty()) { - handle->model->update_tool_constraints(next_token); - } - - EntropyState entropy; - { - size_t cfg_window = handle->model->get_config().default_rolling_entropy_window; - if (cfg_window > 0) entropy.window_size = cfg_window; - } - entropy.add(first_token_entropy); - - if (!matches_stop_sequence(generated_tokens, stop_token_sequences)) { - if (callback) { - std::string new_text = tokenizer->decode({next_token}); - callback(new_text.c_str(), next_token, user_data); - } - - for (size_t i = 1; i < prompt.options.max_tokens; i++) { - if (handle->should_stop) break; - - float token_entropy = 0.0f; - if (has_gemma4_mixed_media) { - next_token = decode_gemma4_mixed_media(handle->processed_tokens, &token_entropy); - } else if (has_audio) { - next_token = handle->model->decode_with_audio( - handle->processed_tokens, prompt.audio_features, - prompt.options.temperature, prompt.options.top_p, prompt.options.top_k, - "", &token_entropy, - prompt.options.min_p, prompt.options.repetition_penalty); - } else { - next_token = decode(handle->model, {next_token}, prompt.options, &token_entropy); - } - handle->processed_tokens.push_back(next_token); - generated_tokens.push_back(next_token); - - entropy.add(token_entropy); - - if (entropy.rolling_confidence() < prompt.options.confidence_threshold) { - entropy.spike_handoff = true; - maybe_start_cloud_handoff("", {}); - } - - if (prompt.options.force_tools && !prompt.tools.empty()) { - handle->model->update_tool_constraints(next_token); - } - - if (matches_stop_sequence(generated_tokens, stop_token_sequences)) { - trim_stop_suffix(generated_tokens, stop_token_sequences, prompt.options.include_stop_sequences); - break; - } - - if (callback) { - std::string new_text = tokenizer->decode({next_token}); - callback(new_text.c_str(), next_token, user_data); - } - } - } else { - trim_stop_suffix(generated_tokens, stop_token_sequences, prompt.options.include_stop_sequences); - } - - confidence = entropy.mean_confidence(); - - if (prompt.options.force_tools && !prompt.tools.empty()) { - handle->model->clear_tool_constraints(); - } - - if (prompt.model_type == Config::ModelType::GEMMA4 && prompt.options.enable_thinking_if_supported && !generated_tokens.empty()) { - strip_thinking_from_cache(handle, generated_tokens, prompt.tokens.size()); - } - - if (prompt.model_type == Config::ModelType::GEMMA4) { - handle->model->compact_kv_cache(); - } - - auto end_time = std::chrono::high_resolution_clock::now(); - double total_time_ms = std::chrono::duration_cast(end_time - start_time).count() / 1000.0; - - size_t completion_tokens = generated_tokens.size(); - double decode_time_ms = total_time_ms - time_to_first_token; - double prefill_tps = time_to_first_token > 0 ? (prompt_tokens * 1000.0) / time_to_first_token : 0.0; - double decode_tps = (completion_tokens > 1 && decode_time_ms > 0) ? ((completion_tokens - 1) * 1000.0) / decode_time_ms : 0.0; - - std::string response_text = tokenizer->decode(generated_tokens); - - std::string regular_response; - std::vector function_calls; - parse_function_calls_from_response(response_text, regular_response, function_calls); - - if (prompt.model_type == Config::ModelType::NEEDLE) { - needle::restore_tool_names(function_calls, prompt.needle_name_map); - } - - std::string thinking_text; - if (prompt.model_type == Config::ModelType::GEMMA4 || prompt.options.enable_thinking_if_supported) { - std::string stripped_content; - strip_thinking_block(regular_response, thinking_text, stripped_content); - regular_response = stripped_content; - if (!prompt.options.enable_thinking_if_supported) { - thinking_text.clear(); - } - } - - if (confidence < prompt.options.confidence_threshold) { - maybe_start_cloud_handoff(regular_response, function_calls); - } - - std::string local_completion = regular_response; - if (local_completion.empty() && function_calls.empty()) { - local_completion = response_text; - } - std::string primary_response = local_completion; - std::vector primary_function_calls = function_calls; - - if (cloud_future_started) { - auto status = cloud_future.wait_for(std::chrono::milliseconds(prompt.options.cloud_timeout_ms)); - if (status == std::future_status::ready) { - CloudCompletionResult cloud_result = cloud_future.get(); - if (cloud_result.ok && (!cloud_result.response.empty() || !cloud_result.function_calls.empty())) { - cloud_used = true; - if (!cloud_result.response.empty()) { - primary_response = cloud_result.response; - } - if (!cloud_result.function_calls.empty()) { - primary_function_calls = cloud_result.function_calls; - } - } else { - cloud_error = cloud_result.error.empty() ? "cloud completion failed" : cloud_result.error; - CACTUS_LOG_WARN("cloud_handoff", "Cloud completion failed, falling back to local output: " << cloud_error); - } - } else { - cloud_error = "timeout"; - CACTUS_LOG_WARN("cloud_handoff", "Cloud completion timed out, falling back to local output: " << cloud_error); - } - } - - const bool handoff_succeeded = cloud_used; - std::string result = construct_response_json(primary_response, primary_function_calls, time_to_first_token, - total_time_ms, prefill_tps, decode_tps, prompt_tokens, - completion_tokens, confidence, handoff_succeeded, - thinking_text); - - if (result.length() >= buffer_size) { - handle_error_response("Response buffer too small", response_buffer, buffer_size); - return -1; - } - - std::strcpy(response_buffer, result.c_str()); - - std::string function_calls_json = serialize_function_calls(primary_function_calls); - cactus::telemetry::CompletionMetrics metrics{}; - metrics.success = true; - metrics.cloud_handoff = handoff_succeeded; - metrics.ttft_ms = time_to_first_token; - metrics.prefill_tps = prefill_tps; - metrics.decode_tps = decode_tps; - metrics.response_time_ms = total_time_ms; - metrics.confidence = confidence; - metrics.ram_usage_mb = get_ram_usage_mb(); - metrics.prefill_tokens = prompt_tokens; - metrics.decode_tokens = completion_tokens; - metrics.error_message = nullptr; - metrics.function_calls_json = nullptr; - cactus::telemetry::recordCompletion(handle->model_name.c_str(), metrics); - - return static_cast(result.length()); - - } catch (const std::exception& e) { - CACTUS_LOG_ERROR("complete", "Exception: " << e.what()); - handle_error_response(e.what(), response_buffer, buffer_size); - - cactus::telemetry::CompletionMetrics metrics{}; - metrics.success = false; - metrics.cloud_handoff = false; - metrics.ttft_ms = 0.0; - metrics.prefill_tps = 0.0; - metrics.decode_tps = 0.0; - metrics.response_time_ms = 0.0; - metrics.confidence = 0.0; - metrics.ram_usage_mb = get_ram_usage_mb(); - metrics.prefill_tokens = 0; - metrics.decode_tokens = 0; - metrics.error_message = e.what(); - metrics.function_calls_json = nullptr; - auto* h = static_cast(model); - cactus::telemetry::recordCompletion(h ? h->model_name.c_str() : "unknown", metrics); - - return -1; - } catch (...) { - CACTUS_LOG_ERROR("complete", "Unknown exception during completion"); - handle_error_response("Unknown error during completion", response_buffer, buffer_size); - - cactus::telemetry::CompletionMetrics metrics{}; - metrics.success = false; - metrics.cloud_handoff = false; - metrics.ttft_ms = 0.0; - metrics.prefill_tps = 0.0; - metrics.decode_tps = 0.0; - metrics.response_time_ms = 0.0; - metrics.confidence = 0.0; - metrics.ram_usage_mb = get_ram_usage_mb(); - metrics.prefill_tokens = 0; - metrics.decode_tokens = 0; - metrics.error_message = "Unknown error during completion"; - metrics.function_calls_json = nullptr; - auto* h = static_cast(model); - cactus::telemetry::recordCompletion(h ? h->model_name.c_str() : "unknown", metrics); - - return -1; - } -} - -int cactus_prefill( - cactus_model_t model, - const char* messages_json, - char* response_buffer, - size_t buffer_size, - const char* options_json, - const char* tools_json, - const uint8_t* pcm_buffer, - size_t pcm_buffer_size -) { - if (!model) { - std::string error_msg = last_error_message.empty() - ? "Model not initialized. Check model path and files." - : last_error_message; - if (response_buffer && buffer_size > 0) { - std::string result = construct_prefill_response_json(false, &error_msg, 0, 0.0, 0.0); - if (result.size() < buffer_size) { - std::strcpy(response_buffer, result.c_str()); - } - } - return -1; - } - - if (!messages_json || !response_buffer || buffer_size == 0) { - std::string error_msg = "Invalid parameters"; - if (response_buffer && buffer_size > 0) { - std::string result = construct_prefill_response_json(false, &error_msg, 0, 0.0, 0.0); - if (result.size() < buffer_size) { - std::strcpy(response_buffer, result.c_str()); - } - } - return -1; - } - - try { - auto start_time = std::chrono::high_resolution_clock::now(); - - auto* handle = static_cast(model); - auto prompt = prepare_prompt(handle, messages_json, options_json, tools_json, false, false, pcm_buffer, pcm_buffer_size); - - std::vector context_tokens(prompt.tokens.begin(), prompt.tokens.begin() + prompt.context_token_count); - auto prefill_result = do_prefill(handle, prompt, context_tokens); - - if (!prefill_result.was_exact_match) { - handle->processed_tokens = context_tokens; - if (!handle->processed_tokens.empty()) { - handle->processed_tokens.pop_back(); - } - } - handle->processed_images = prompt.images; - - auto end_time = std::chrono::high_resolution_clock::now(); - double elapsed_ms = std::chrono::duration_cast(end_time - start_time).count() / 1000.0; - double prefill_tps = (prefill_result.prefilled_count > 0 && elapsed_ms > 0.0) - ? (static_cast(prefill_result.prefilled_count) * 1000.0) / elapsed_ms - : 0.0; - - std::string result = construct_prefill_response_json(true, nullptr, prefill_result.prefilled_count, prefill_tps, elapsed_ms); - if (result.size() >= buffer_size) { - std::string error_msg = "Response buffer too small"; - std::string error_json = construct_prefill_response_json(false, &error_msg, 0, 0.0, 0.0); - if (error_json.size() < buffer_size) { - std::strcpy(response_buffer, error_json.c_str()); - } - return -1; - } - - std::strcpy(response_buffer, result.c_str()); - return static_cast(result.size()); - } catch (const std::exception& e) { - std::string error_msg = e.what(); - std::string result = construct_prefill_response_json(false, &error_msg, 0, 0.0, 0.0); - if (result.size() < buffer_size) { - std::strcpy(response_buffer, result.c_str()); - } - return -1; - } catch (...) { - std::string error_msg = "Unknown error during prefill"; - std::string result = construct_prefill_response_json(false, &error_msg, 0, 0.0, 0.0); - if (result.size() < buffer_size) { - std::strcpy(response_buffer, result.c_str()); - } - return -1; - } -} - -int cactus_tokenize( - cactus_model_t model, - const char* text, - uint32_t* token_buffer, - size_t token_buffer_len, - size_t* out_token_len -) { - if (!model || !text || !out_token_len) return -1; - - try { - auto* handle = static_cast(model); - auto* tokenizer = handle->model->get_tokenizer(); - - std::vector toks = tokenizer->encode(std::string(text)); - *out_token_len = toks.size(); - - if (!token_buffer || token_buffer_len == 0) return 0; - if (token_buffer_len < toks.size()) return -2; - - std::memcpy(token_buffer, toks.data(), toks.size() * sizeof(uint32_t)); - return 0; - } catch (...) { - return -1; - } -} - -int cactus_score_window( - cactus_model_t model, - const uint32_t* tokens, - size_t token_len, - size_t start, - size_t end, - size_t context, - char* response_buffer, - size_t buffer_size -) { - if (!model || !tokens || token_len == 0 || !response_buffer || buffer_size == 0) { - handle_error_response("Invalid parameters", response_buffer, buffer_size); - return -1; - } - - try { - auto* handle = static_cast(model); - - std::vector vec(tokens, tokens + token_len); - - size_t scored = 0; - double logprob = handle->model->score_tokens_window_logprob(vec, start, end, context, &scored); - - std::ostringstream oss; - oss << "{" - << "\"success\":true," - << "\"logprob\":" << std::setprecision(10) << logprob << "," - << "\"tokens\":" << scored - << "}"; - - std::string result = oss.str(); - if (result.size() >= buffer_size) { - handle_error_response("Response buffer too small", response_buffer, buffer_size); - return -1; - } - - std::strcpy(response_buffer, result.c_str()); - return (int)result.size(); - - } catch (const std::exception& e) { - handle_error_response(e.what(), response_buffer, buffer_size); - return -1; - } -} - -} diff --git a/cactus/ffi/cactus_diarize.cpp b/cactus/ffi/cactus_diarize.cpp deleted file mode 100644 index 358c7aa37..000000000 --- a/cactus/ffi/cactus_diarize.cpp +++ /dev/null @@ -1,200 +0,0 @@ -#include "cactus_ffi.h" -#include "cactus_utils.h" -#include "../models/model.h" -#include "../../libs/audio/wav.h" -#include -#include -#include - -using namespace cactus::engine; -using namespace cactus::ffi; - -static constexpr int DIARIZE_MAX_SPEAKERS = 3; - -struct DiarizeOptions { - size_t step_samples = 16000; - float threshold = -1.0f; - int num_speakers = -1; - int min_speakers = -1; - int max_speakers = -1; - bool raw_powerset = false; -}; - -static DiarizeOptions parse_diarize_options(const std::string& json) { - DiarizeOptions opts; - if (json.empty()) return opts; - - size_t pos; - - pos = json.find("\"step_ms\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(json[pos])) pos++; - int v = std::stoi(json.substr(pos)); - if (v > 0) opts.step_samples = static_cast(v) * 16; - } - - pos = json.find("\"threshold\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(json[pos])) pos++; - opts.threshold = std::stof(json.substr(pos)); - } - - pos = json.find("\"num_speakers\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(json[pos])) pos++; - opts.num_speakers = std::stoi(json.substr(pos)); - } - - pos = json.find("\"min_speakers\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(json[pos])) pos++; - opts.min_speakers = std::stoi(json.substr(pos)); - } - - pos = json.find("\"max_speakers\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(json[pos])) pos++; - opts.max_speakers = std::stoi(json.substr(pos)); - } - - pos = json.find("\"raw_powerset\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(json[pos])) pos++; - opts.raw_powerset = (json.substr(pos, 4) == "true"); - } - - return opts; -} - -static void apply_speaker_filter(std::vector& scores, size_t total_frames, const DiarizeOptions& opts) { - int keep = DIARIZE_MAX_SPEAKERS; - if (opts.num_speakers > 0) keep = std::min(opts.num_speakers, DIARIZE_MAX_SPEAKERS); - if (opts.max_speakers > 0) keep = std::min(keep, opts.max_speakers); - if (opts.min_speakers > 0) keep = std::max(keep, std::min(opts.min_speakers, DIARIZE_MAX_SPEAKERS)); - if (keep >= DIARIZE_MAX_SPEAKERS) return; - - float activity[DIARIZE_MAX_SPEAKERS] = {}; - for (size_t f = 0; f < total_frames; ++f) - for (int s = 0; s < DIARIZE_MAX_SPEAKERS; ++s) - activity[s] += scores[f * DIARIZE_MAX_SPEAKERS + s]; - - int order[DIARIZE_MAX_SPEAKERS] = {0, 1, 2}; - std::sort(order, order + DIARIZE_MAX_SPEAKERS, [&](int a, int b) { return activity[a] > activity[b]; }); - - bool active[DIARIZE_MAX_SPEAKERS] = {}; - for (int i = 0; i < keep; ++i) active[order[i]] = true; - - for (size_t f = 0; f < total_frames; ++f) - for (int s = 0; s < DIARIZE_MAX_SPEAKERS; ++s) - if (!active[s]) scores[f * DIARIZE_MAX_SPEAKERS + s] = 0.0f; -} - -static void apply_threshold(std::vector& scores, float threshold) { - for (float& v : scores) - v = (v >= threshold) ? v : 0.0f; -} - -extern "C" { - -int cactus_diarize( - cactus_model_t model, - const char* audio_file_path, - char* response_buffer, - size_t buffer_size, - const char* options_json, - const uint8_t* pcm_buffer, - size_t pcm_buffer_size -) { - if (validate_audio_params("diarize", model, response_buffer, buffer_size, audio_file_path, pcm_buffer, pcm_buffer_size) != 0) - return -1; - - try { - auto start_time = std::chrono::high_resolution_clock::now(); - auto* handle = static_cast(model); - auto* pyannote = dynamic_cast(handle->model.get()); - if (!pyannote) { - CACTUS_LOG_ERROR("diarize", "Model is not a PyAnnote diarization model"); - handle_error_response("Model is not a PyAnnote diarization model", response_buffer, buffer_size); - return -1; - } - - std::vector audio; - if (audio_file_path == nullptr) { - audio = pcm_to_float(pcm_buffer, pcm_buffer_size); - } else { - AudioFP32 wav_audio = load_wav(audio_file_path); - audio = resample_to_16k_fp32(wav_audio.samples, wav_audio.sample_rate); - } - - if (audio.empty()) { - last_error_message = "Failed to load audio or audio is empty"; - CACTUS_LOG_ERROR("diarize", last_error_message); - handle_error_response(last_error_message, response_buffer, buffer_size); - return -1; - } - - const DiarizeOptions opts = parse_diarize_options(options_json ? options_json : ""); - auto scores = pyannote->diarize(audio.data(), audio.size(), opts.step_samples, opts.raw_powerset); - audio = {}; - - const int num_channels = opts.raw_powerset ? 7 : DIARIZE_MAX_SPEAKERS; - const size_t total_frames = scores.size() / num_channels; - - if (!opts.raw_powerset) { - if (opts.num_speakers > 0 || opts.min_speakers > 0 || opts.max_speakers > 0) - apply_speaker_filter(scores, total_frames, opts); - - if (opts.threshold >= 0.0f) - apply_threshold(scores, opts.threshold); - } - - auto end_time = std::chrono::high_resolution_clock::now(); - double total_time_ms = std::chrono::duration_cast(end_time - start_time).count() / 1000.0; - - std::ostringstream json; - json << "{"; - json << "\"success\":true,"; - json << "\"error\":null,"; - json << "\"num_speakers\":" << num_channels << ","; - json << "\"scores\":["; - json << std::fixed << std::setprecision(6); - for (size_t i = 0; i < scores.size(); ++i) { - if (i > 0) json << ","; - json << scores[i]; - } - json << "],"; - json << "\"total_time_ms\":" << std::fixed << std::setprecision(2) << total_time_ms << ","; - json << "\"ram_usage_mb\":" << std::fixed << std::setprecision(2) << get_ram_usage_mb(); - json << "}"; - - std::string response = json.str(); - if (response.length() >= buffer_size) { - last_error_message = "Response buffer too small"; - CACTUS_LOG_ERROR("diarize", last_error_message); - handle_error_response(last_error_message, response_buffer, buffer_size); - return -1; - } - - std::strcpy(response_buffer, response.c_str()); - return static_cast(response.length()); - - } catch (const std::exception& e) { - last_error_message = "Exception during diarization: " + std::string(e.what()); - CACTUS_LOG_ERROR("diarize", last_error_message); - handle_error_response(last_error_message, response_buffer, buffer_size); - return -1; - } catch (...) { - last_error_message = "Unknown exception during diarization"; - CACTUS_LOG_ERROR("diarize", last_error_message); - handle_error_response(last_error_message, response_buffer, buffer_size); - return -1; - } -} - -} diff --git a/cactus/ffi/cactus_embed.cpp b/cactus/ffi/cactus_embed.cpp deleted file mode 100644 index 2ce85f0ca..000000000 --- a/cactus/ffi/cactus_embed.cpp +++ /dev/null @@ -1,192 +0,0 @@ -#include "cactus_ffi.h" -#include "cactus_utils.h" -#include "../../libs/audio/wav.h" -#include -#include -#include - -using namespace cactus::engine; -using namespace cactus::ffi; -using cactus::audio::WHISPER_SAMPLE_RATE; -using cactus::audio::apply_preemphasis; -using cactus::audio::get_parakeet_spectrogram_config; -using cactus::audio::get_whisper_spectrogram_config; -using cactus::audio::init_whisper_mel_filters; -using cactus::audio::normalize_parakeet_log_mel; -using cactus::audio::normalize_whisper_mel; -using cactus::audio::trim_mel_frames; - -static std::vector compute_mel_from_wav(const std::string& wav_path, bool is_parakeet, size_t mel_bins) { - AudioFP32 audio = load_wav(wav_path); - std::vector waveform_16k = resample_to_16k_fp32(audio.samples, audio.sample_rate); - - auto cfg = is_parakeet ? get_parakeet_spectrogram_config() : get_whisper_spectrogram_config(); - const size_t num_mel_filters = std::max(1, mel_bins); - - AudioProcessor ap; - if (is_parakeet) { - ap.init_mel_filters(cfg.n_fft / 2 + 1, num_mel_filters, 0.0f, 8000.0f, WHISPER_SAMPLE_RATE); - } else { - init_whisper_mel_filters(ap, cfg, num_mel_filters); - } - const size_t waveform_samples = waveform_16k.size(); - if (is_parakeet) { - apply_preemphasis(waveform_16k, 0.97f); - } - std::vector mel = ap.compute_spectrogram(waveform_16k, cfg); - - if (mel.empty()) return mel; - if (is_parakeet) { - normalize_parakeet_log_mel(mel, num_mel_filters); - size_t valid_frames = waveform_samples / cfg.hop_length; - if (valid_frames == 0) valid_frames = 1; - trim_mel_frames(mel, num_mel_filters, valid_frames); - return mel; - } - - const bool is_v3 = num_mel_filters > 80; - return normalize_whisper_mel(mel, num_mel_filters, is_v3); -} - -extern "C" { - -int cactus_embed( - cactus_model_t model, - const char* text, - float* embeddings_buffer, - size_t buffer_size, - size_t* embedding_dim, - bool normalize -) { - if (!model || !text || !embeddings_buffer || buffer_size == 0) { - CACTUS_LOG_ERROR("embed", "Invalid parameters for text embedding"); - return -1; - } - - try { - auto* handle = static_cast(model); - auto* tokenizer = handle->model->get_tokenizer(); - - std::vector tokens = tokenizer->encode(text); - if (tokens.empty()) { - CACTUS_LOG_ERROR("embed", "Tokenization produced empty result"); - return -1; - } - - std::vector embeddings = handle->model->get_embeddings(tokens, true, normalize); - if (embeddings.size() * sizeof(float) > buffer_size) { - CACTUS_LOG_ERROR("embed", "Buffer too small: need " << embeddings.size() * sizeof(float) << " bytes, got " << buffer_size); - return -2; - } - - std::memcpy(embeddings_buffer, embeddings.data(), embeddings.size() * sizeof(float)); - if (embedding_dim) *embedding_dim = embeddings.size(); - - return static_cast(embeddings.size()); - - } catch (const std::exception& e) { - last_error_message = e.what(); - CACTUS_LOG_ERROR("embed", "Exception: " << e.what()); - return -1; - } catch (...) { - last_error_message = "Unknown error during embedding"; - CACTUS_LOG_ERROR("embed", last_error_message); - return -1; - } -} - -int cactus_image_embed( - cactus_model_t model, - const char* image_path, - float* embeddings_buffer, - size_t buffer_size, - size_t* embedding_dim -) { - if (!model || !image_path || !embeddings_buffer || buffer_size == 0) { - CACTUS_LOG_ERROR("image_embed", "Invalid parameters for image embedding"); - return -1; - } - - try { - auto* handle = static_cast(model); - - CACTUS_LOG_DEBUG("image_embed", "Processing image: " << image_path); - std::vector embeddings = handle->model->get_image_embeddings(image_path); - if (embeddings.empty()) { - CACTUS_LOG_ERROR("image_embed", "Image embedding returned empty result"); - return -1; - } - if (embeddings.size() * sizeof(float) > buffer_size) { - CACTUS_LOG_ERROR("image_embed", "Buffer too small: need " << embeddings.size() * sizeof(float) << " bytes"); - return -2; - } - - std::memcpy(embeddings_buffer, embeddings.data(), embeddings.size() * sizeof(float)); - if (embedding_dim) *embedding_dim = embeddings.size(); - - return static_cast(embeddings.size()); - - } catch (const std::exception& e) { - last_error_message = e.what(); - CACTUS_LOG_ERROR("image_embed", "Exception: " << e.what()); - return -1; - } catch (...) { - last_error_message = "Unknown error during image embedding"; - CACTUS_LOG_ERROR("image_embed", last_error_message); - return -1; - } -} - -int cactus_audio_embed( - cactus_model_t model, - const char* audio_path, - float* embeddings_buffer, - size_t buffer_size, - size_t* embedding_dim -) { - if (!model || !audio_path || !embeddings_buffer || buffer_size == 0) { - CACTUS_LOG_ERROR("audio_embed", "Invalid parameters for audio embedding"); - return -1; - } - - try { - auto* handle = static_cast(model); - - CACTUS_LOG_DEBUG("audio_embed", "Processing audio: " << audio_path); - const bool is_parakeet = - handle->model->get_config().model_type == cactus::engine::Config::ModelType::PARAKEET || - handle->model->get_config().model_type == cactus::engine::Config::ModelType::PARAKEET_TDT; - auto mel_bins = compute_mel_from_wav(audio_path, is_parakeet, handle->model->get_config().num_mel_bins); - if (mel_bins.empty()) { - last_error_message = "Failed to compute mel spectrogram"; - CACTUS_LOG_ERROR("audio_embed", last_error_message << " for: " << audio_path); - return -1; - } - - std::vector embeddings = handle->model->get_audio_embeddings(mel_bins); - if (embeddings.empty()) { - CACTUS_LOG_ERROR("audio_embed", "Audio embedding returned empty result"); - return -1; - } - if (embeddings.size() * sizeof(float) > buffer_size) { - CACTUS_LOG_ERROR("audio_embed", "Buffer too small: need " << embeddings.size() * sizeof(float) << " bytes"); - return -2; - } - - std::memcpy(embeddings_buffer, embeddings.data(), embeddings.size() * sizeof(float)); - if (embedding_dim) *embedding_dim = embeddings.size(); - - return static_cast(embeddings.size()); - - } catch (const std::exception& e) { - last_error_message = e.what(); - CACTUS_LOG_ERROR("audio_embed", "Exception: " << e.what()); - return -1; - } catch (...) { - last_error_message = "Unknown error during audio embedding"; - CACTUS_LOG_ERROR("audio_embed", last_error_message); - return -1; - } -} - -} diff --git a/cactus/ffi/cactus_embed_speaker.cpp b/cactus/ffi/cactus_embed_speaker.cpp deleted file mode 100644 index ddb42707e..000000000 --- a/cactus/ffi/cactus_embed_speaker.cpp +++ /dev/null @@ -1,125 +0,0 @@ -#include "cactus_ffi.h" -#include "cactus_utils.h" -#include "../models/model.h" -#include "../../libs/audio/wav.h" -#include -#include - -using namespace cactus::engine; -using namespace cactus::ffi; -using cactus::audio::get_wespeaker_spectrogram_config; - -extern "C" { - -int cactus_embed_speaker( - cactus_model_t model, - const char* audio_file_path, - char* response_buffer, - size_t buffer_size, - const char* options_json, - const uint8_t* pcm_buffer, - size_t pcm_buffer_size, - const float* mask_weights, - size_t mask_num_frames -) { - (void)options_json; - if (validate_audio_params("embed_speaker", model, response_buffer, buffer_size, audio_file_path, pcm_buffer, pcm_buffer_size) != 0) - return -1; - - try { - auto start_time = std::chrono::high_resolution_clock::now(); - auto* handle = static_cast(model); - auto* wespeaker = dynamic_cast(handle->model.get()); - if (!wespeaker) { - CACTUS_LOG_ERROR("embed_speaker", "Model is not a WeSpeaker embedding model"); - handle_error_response("Model is not a WeSpeaker embedding model", response_buffer, buffer_size); - return -1; - } - - std::vector audio; - if (audio_file_path == nullptr) { - audio = pcm_to_float(pcm_buffer, pcm_buffer_size); - } else { - AudioFP32 wav_audio = load_wav(audio_file_path); - audio = resample_to_16k_fp32(wav_audio.samples, wav_audio.sample_rate); - } - - if (audio.empty()) { - last_error_message = "Failed to load audio or audio is empty"; - CACTUS_LOG_ERROR("embed_speaker", last_error_message); - handle_error_response(last_error_message, response_buffer, buffer_size); - return -1; - } - - static const auto s_cfg = get_wespeaker_spectrogram_config(); - static AudioProcessor s_ap = []() { - AudioProcessor p; - p.init_mel_filters(get_wespeaker_spectrogram_config().n_fft / 2 + 1, - 80, 20.0f, 8000.0f, 16000, nullptr, "htk"); - return p; - }(); - - std::vector mel = s_ap.compute_spectrogram(audio, s_cfg); - audio = {}; - - static constexpr size_t NUM_MEL_BINS = 80; - const size_t num_frames = mel.size() / NUM_MEL_BINS; - - for (size_t m = 0; m < NUM_MEL_BINS; ++m) { - float* row = mel.data() + m * num_frames; - float sum = 0.0f; - for (size_t t = 0; t < num_frames; ++t) sum += row[t]; - const float mean = sum / static_cast(num_frames); - for (size_t t = 0; t < num_frames; ++t) row[t] -= mean; - } - - std::vector embedding; - if (mask_weights && mask_num_frames > 0) { - embedding = wespeaker->embed(mel.data(), mel.size(), mask_weights, mask_num_frames); - } else { - embedding = wespeaker->embed(mel.data(), mel.size()); - } - - auto end_time = std::chrono::high_resolution_clock::now(); - double total_time_ms = std::chrono::duration_cast(end_time - start_time).count() / 1000.0; - - std::ostringstream json; - json << "{"; - json << "\"success\":true,"; - json << "\"error\":null,"; - json << "\"embedding\":["; - json << std::fixed << std::setprecision(6); - for (size_t i = 0; i < embedding.size(); ++i) { - if (i > 0) json << ","; - json << embedding[i]; - } - json << "],"; - json << "\"total_time_ms\":" << std::fixed << std::setprecision(2) << total_time_ms << ","; - json << "\"ram_usage_mb\":" << std::fixed << std::setprecision(2) << get_ram_usage_mb(); - json << "}"; - - std::string response = json.str(); - if (response.length() >= buffer_size) { - last_error_message = "Response buffer too small"; - CACTUS_LOG_ERROR("embed_speaker", last_error_message); - handle_error_response(last_error_message, response_buffer, buffer_size); - return -1; - } - - std::strcpy(response_buffer, response.c_str()); - return static_cast(response.length()); - - } catch (const std::exception& e) { - last_error_message = "Exception during speaker embedding: " + std::string(e.what()); - CACTUS_LOG_ERROR("embed_speaker", last_error_message); - handle_error_response(last_error_message, response_buffer, buffer_size); - return -1; - } catch (...) { - last_error_message = "Unknown exception during speaker embedding"; - CACTUS_LOG_ERROR("embed_speaker", last_error_message); - handle_error_response(last_error_message, response_buffer, buffer_size); - return -1; - } -} - -} diff --git a/cactus/ffi/cactus_ffi.h b/cactus/ffi/cactus_ffi.h deleted file mode 100644 index 163e1805c..000000000 --- a/cactus/ffi/cactus_ffi.h +++ /dev/null @@ -1,454 +0,0 @@ -#ifndef CACTUS_FFI_H -#define CACTUS_FFI_H - -#include -#include -#include - -#if __GNUC__ >= 4 - #define CACTUS_FFI_EXPORT __attribute__((visibility("default"))) - #define CACTUS_FFI_LOCAL __attribute__((visibility("hidden"))) -#else - #define CACTUS_FFI_EXPORT - #define CACTUS_FFI_LOCAL -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -typedef void* cactus_model_t; -typedef void* cactus_index_t; -typedef void* cactus_stream_transcribe_t; - -typedef void (*cactus_token_callback)(const char* token, uint32_t token_id, void* user_data); - -CACTUS_FFI_EXPORT cactus_model_t cactus_init( - const char* model_path, - const char* corpus_dir, // optional: NULL if no RAG corpus - bool cache_index // false = always rebuild index, true = load cached if available -); - -CACTUS_FFI_EXPORT void cactus_destroy(cactus_model_t model); -CACTUS_FFI_EXPORT void cactus_reset(cactus_model_t model); -CACTUS_FFI_EXPORT void cactus_stop(cactus_model_t model); - -CACTUS_FFI_EXPORT int cactus_complete( - cactus_model_t model, - const char* messages_json, - char* response_buffer, - size_t buffer_size, - const char* options_json, // optional - const char* tools_json, // optional - cactus_token_callback callback, // optional - void* user_data, // optional - const uint8_t* pcm_buffer, // optional: NULL when not used - size_t pcm_buffer_size // optional: 0 when not used -); - -CACTUS_FFI_EXPORT int cactus_prefill( - cactus_model_t model, - const char* messages_json, - char* response_buffer, - size_t buffer_size, - const char* options_json, // optional - const char* tools_json, // optional - const uint8_t* pcm_buffer, // optional: NULL when not used - size_t pcm_buffer_size // optional: 0 when not used -); - -CACTUS_FFI_EXPORT int cactus_tokenize( - cactus_model_t model, - const char* text, - uint32_t* token_buffer, - size_t token_buffer_len, - size_t* out_token_len -); - -CACTUS_FFI_EXPORT int cactus_score_window( - cactus_model_t model, - const uint32_t* tokens, - size_t token_len, - size_t start, - size_t end, - size_t context, - char* response_buffer, - size_t buffer_size -); - -CACTUS_FFI_EXPORT int cactus_transcribe( - cactus_model_t model, - const char* audio_file_path, // NULL if using pcm_buffer - const char* prompt, - char* response_buffer, - size_t buffer_size, - const char* options_json, // optional - cactus_token_callback callback, // optional - void* user_data, // optional - const uint8_t* pcm_buffer, // NULL if using audio_file_path - size_t pcm_buffer_size -); - -CACTUS_FFI_EXPORT int cactus_detect_language( - cactus_model_t model, - const char* audio_file_path, // NULL if using pcm_buffer - char* response_buffer, - size_t buffer_size, - const char* options_json, // optional - const uint8_t* pcm_buffer, // NULL if using audio_file_path - size_t pcm_buffer_size -); - -CACTUS_FFI_EXPORT cactus_stream_transcribe_t cactus_stream_transcribe_start( - cactus_model_t model, - const char* options_json // optional -); - -CACTUS_FFI_EXPORT int cactus_stream_transcribe_process( - cactus_stream_transcribe_t stream, - const uint8_t* pcm_buffer, - size_t pcm_buffer_size, - char* response_buffer, - size_t buffer_size -); - -CACTUS_FFI_EXPORT int cactus_stream_transcribe_stop( - cactus_stream_transcribe_t stream, - char* response_buffer, - size_t buffer_size -); - -CACTUS_FFI_EXPORT int cactus_embed( - cactus_model_t model, - const char* text, - float* embeddings_buffer, - size_t buffer_size, - size_t* embedding_dim, - bool normalize -); - -CACTUS_FFI_EXPORT int cactus_image_embed( - cactus_model_t model, - const char* image_path, - float* embeddings_buffer, - size_t buffer_size, - size_t* embedding_dim -); - -CACTUS_FFI_EXPORT int cactus_audio_embed( - cactus_model_t model, - const char* audio_path, - float* embeddings_buffer, - size_t buffer_size, - size_t* embedding_dim -); - -CACTUS_FFI_EXPORT int cactus_vad( - cactus_model_t model, - const char* audio_file_path, - char* response_buffer, - size_t buffer_size, - const char* options_json, - const uint8_t* pcm_buffer, - size_t pcm_buffer_size -); - -CACTUS_FFI_EXPORT int cactus_diarize( - cactus_model_t model, - const char* audio_file_path, - char* response_buffer, - size_t buffer_size, - const char* options_json, - const uint8_t* pcm_buffer, - size_t pcm_buffer_size -); - -CACTUS_FFI_EXPORT int cactus_embed_speaker( - cactus_model_t model, - const char* audio_file_path, - char* response_buffer, - size_t buffer_size, - const char* options_json, - const uint8_t* pcm_buffer, - size_t pcm_buffer_size, - const float* mask_weights, - size_t mask_num_frames -); - -CACTUS_FFI_EXPORT int cactus_rag_query( - cactus_model_t model, - const char* query, - char* response_buffer, - size_t buffer_size, - size_t top_k -); - -CACTUS_FFI_EXPORT cactus_index_t cactus_index_init( - const char* index_dir, - size_t embedding_dim -); - -CACTUS_FFI_EXPORT int cactus_index_add( - cactus_index_t index, - const int* ids, - const char** documents, - const char** metadatas, // optional: can be NULL - const float** embeddings, - size_t count, - size_t embedding_dim -); - -CACTUS_FFI_EXPORT int cactus_index_delete( - cactus_index_t index, - const int* ids, - size_t ids_count -); - -CACTUS_FFI_EXPORT int cactus_index_get( - cactus_index_t index, - const int* ids, - size_t ids_count, - char** document_buffers, - size_t* document_buffer_sizes, - char** metadata_buffers, - size_t* metadata_buffer_sizes, - float** embedding_buffers, - size_t* embedding_buffer_sizes -); - -CACTUS_FFI_EXPORT int cactus_index_query( - cactus_index_t index, - const float** embeddings, - size_t embeddings_count, - size_t embedding_dim, - const char* options_json, // optional - int** id_buffers, - size_t* id_buffer_sizes, - float** score_buffers, - size_t* score_buffer_sizes -); - -CACTUS_FFI_EXPORT int cactus_index_compact(cactus_index_t index); -CACTUS_FFI_EXPORT void cactus_index_destroy(cactus_index_t index); - -CACTUS_FFI_EXPORT const char* cactus_get_last_error(void); - -// level: 0=DEBUG, 1=INFO, 2=WARN (default), 3=ERROR, 4=NONE -CACTUS_FFI_EXPORT void cactus_log_set_level(int level); - -typedef void (*cactus_log_callback_t)(int level, const char* component, const char* message, void* user_data); -CACTUS_FFI_EXPORT void cactus_log_set_callback(cactus_log_callback_t callback, void* user_data); - -CACTUS_FFI_EXPORT void cactus_set_telemetry_environment(const char* framework, const char* cache_location, const char* version); -CACTUS_FFI_EXPORT void cactus_set_app_id(const char* app_id); -CACTUS_FFI_EXPORT void cactus_telemetry_flush(void); -CACTUS_FFI_EXPORT void cactus_telemetry_shutdown(void); - -// cactus graph export -typedef void* cactus_graph_t; -typedef uint64_t cactus_node_t; - -typedef struct { - int32_t precision; - size_t rank; - size_t shape[8]; - size_t num_elements; - size_t byte_size; -} cactus_tensor_info_t; - -CACTUS_FFI_EXPORT cactus_graph_t cactus_graph_create(void); -CACTUS_FFI_EXPORT void cactus_graph_destroy(cactus_graph_t graph); -CACTUS_FFI_EXPORT int cactus_graph_hard_reset(cactus_graph_t graph); - -CACTUS_FFI_EXPORT int cactus_graph_save(cactus_graph_t graph, const char* filename); -CACTUS_FFI_EXPORT cactus_graph_t cactus_graph_load(const char* filename); - -CACTUS_FFI_EXPORT int cactus_graph_input( - cactus_graph_t graph, const size_t* shape, size_t rank, int32_t precision, -cactus_node_t* out_node); - -CACTUS_FFI_EXPORT int cactus_graph_set_input( - cactus_graph_t graph, cactus_node_t node, const void* data, int32_t -precision); -CACTUS_FFI_EXPORT int cactus_graph_set_external_input( - cactus_graph_t graph, cactus_node_t node, void* data, int32_t precision); - -CACTUS_FFI_EXPORT int cactus_graph_precision_cast( - cactus_graph_t graph, cactus_node_t input, int32_t target_precision, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_quantize_activations( - cactus_graph_t graph, cactus_node_t input, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_add(cactus_graph_t graph, cactus_node_t a, -cactus_node_t b, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_add_clipped(cactus_graph_t graph, cactus_node_t a, -cactus_node_t b, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_subtract(cactus_graph_t graph, cactus_node_t -a, cactus_node_t b, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_multiply(cactus_graph_t graph, cactus_node_t -a, cactus_node_t b, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_divide(cactus_graph_t graph, cactus_node_t -a, cactus_node_t b, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_scalar_add(cactus_graph_t graph, cactus_node_t x, float value, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_scalar_subtract(cactus_graph_t graph, cactus_node_t x, float value, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_scalar_multiply(cactus_graph_t graph, cactus_node_t x, float value, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_scalar_divide(cactus_graph_t graph, cactus_node_t x, float value, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_scalar_exp(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_scalar_sqrt(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_scalar_cos(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_scalar_sin(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_scalar_log(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_abs(cactus_graph_t graph, cactus_node_t x, -cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_pow(cactus_graph_t graph, cactus_node_t x, -float exponent, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_view( - cactus_graph_t graph, cactus_node_t x, const size_t* shape, size_t rank, -cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_flatten( - cactus_graph_t graph, cactus_node_t x, int32_t start_dim, int32_t end_dim, -cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_reshape( - cactus_graph_t graph, cactus_node_t x, const size_t* shape, size_t rank, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_transpose( - cactus_graph_t graph, cactus_node_t x, int32_t backend, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_transpose_n( - cactus_graph_t graph, cactus_node_t x, const size_t* permutation, size_t rank, int32_t backend, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_slice( - cactus_graph_t graph, cactus_node_t x, int32_t axis, size_t start, size_t length, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_index( - cactus_graph_t graph, cactus_node_t x, size_t index_value, int32_t dim, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_sum(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_mean(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_variance(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_min(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_max(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_concat( - cactus_graph_t graph, cactus_node_t a, cactus_node_t b, int32_t axis, -cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_cat( - cactus_graph_t graph, const cactus_node_t* nodes, size_t count, int32_t -axis, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_matmul( - cactus_graph_t graph, cactus_node_t a, cactus_node_t b, bool pretransposed_rhs, int32_t backend, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_gather( - cactus_graph_t graph, cactus_node_t tensor, cactus_node_t indices, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_embedding_from_tensor( - cactus_graph_t graph, cactus_node_t embedding_tensor, cactus_node_t indices, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_embedding_from_file( - cactus_graph_t graph, const char* filename, cactus_node_t indices, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_mmap_embeddings( - cactus_graph_t graph, const char* filename, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_mmap_weights( - cactus_graph_t graph, const char* filename, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_bilinear_interpolation( - cactus_graph_t graph, cactus_node_t pos_embeds, size_t dst_height, size_t dst_width, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_set_grouped_scales( - cactus_graph_t graph, cactus_node_t node, size_t group_size, size_t num_groups, void* scales_ptr); -CACTUS_FFI_EXPORT int cactus_graph_set_interleaved( - cactus_graph_t graph, cactus_node_t node, bool interleaved, size_t original_n); -CACTUS_FFI_EXPORT int cactus_graph_release_weight_pages(cactus_graph_t graph, cactus_node_t node); -CACTUS_FFI_EXPORT int cactus_graph_prefetch_weight_pages(cactus_graph_t graph, cactus_node_t node); -CACTUS_FFI_EXPORT int cactus_graph_release_all_weight_pages(cactus_graph_t graph); - -CACTUS_FFI_EXPORT int cactus_graph_relu(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_silu(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_gelu(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_gelu_erf(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_sigmoid(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_tanh(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_glu(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_layernorm( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, cactus_node_t bias, float epsilon, bool has_bias, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_groupnorm( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, cactus_node_t bias, size_t num_groups, float epsilon, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_batchnorm( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, cactus_node_t bias, cactus_node_t running_mean, cactus_node_t running_var, int32_t axis, float epsilon, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_topk(cactus_graph_t graph, cactus_node_t input, size_t k, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_rms_norm( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, float epsilon, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_rope( - cactus_graph_t graph, cactus_node_t input, float theta, size_t position_offset, int32_t backend, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_rope_gptj( - cactus_graph_t graph, cactus_node_t input, float theta, size_t position_offset, size_t rot_dim, int32_t backend, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_softmax(cactus_graph_t graph, cactus_node_t input, int32_t axis, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_attention( - cactus_graph_t graph, cactus_node_t query, cactus_node_t key, cactus_node_t value, float scale, bool is_causal, size_t position_offset, size_t window_size, int32_t backend, bool use_mask, cactus_node_t mask, bool additive_mask, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_rel_pos_bias( - cactus_graph_t graph, cactus_node_t query, cactus_node_t relative_key, float scale, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_attention_int8_hybrid( - cactus_graph_t graph, cactus_node_t query, cactus_node_t key_new, cactus_node_t value_new, float scale, size_t position_offset, - const int8_t* cached_keys, const int8_t* cached_values, const float* k_scales, const float* v_scales, - size_t cache_len, size_t num_kv_heads, size_t head_dim, size_t window_size, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_conv1d_causal( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, size_t kernel_size, size_t dilation, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_conv1d_k3( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, size_t stride, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_conv1d_k7s3( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, cactus_node_t bias, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_conv1d( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, bool has_bias, cactus_node_t bias, size_t stride, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_conv1d_same_depthwise_k9( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, bool has_bias, cactus_node_t bias, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_conv1d_pointwise( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, bool has_bias, cactus_node_t bias, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_conv2d_k3s2p1( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, bool has_bias, cactus_node_t bias, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_conv2d_depthwise_k3s2p1( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, bool has_bias, cactus_node_t bias, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_conv2d_pointwise_1x1( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, bool has_bias, cactus_node_t bias, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_lstm_cell( - cactus_graph_t graph, cactus_node_t input, cactus_node_t h_prev, cactus_node_t c_prev, cactus_node_t weight_ih, cactus_node_t weight_hh, cactus_node_t bias_ih, cactus_node_t bias_hh, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_gated_deltanet_decode( - cactus_graph_t graph, cactus_node_t query, cactus_node_t key, cactus_node_t value, cactus_node_t gate_log, cactus_node_t beta, cactus_node_t initial_state, float scale, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_gated_deltanet_prefill( - cactus_graph_t graph, cactus_node_t query, cactus_node_t key, cactus_node_t value, cactus_node_t gate_log, cactus_node_t beta, cactus_node_t initial_state, size_t chunk_size, float scale, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_stft( - cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, size_t stride, size_t num_fft_bins, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_altup_predict( - cactus_graph_t graph, cactus_node_t coefs, const cactus_node_t* streams, size_t num_streams, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_altup_correct( - cactus_graph_t graph, cactus_node_t coefs, cactus_node_t innovation, const cactus_node_t* predictions, size_t num_predictions, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_gaussian_topk( - cactus_graph_t graph, cactus_node_t input, float ppf, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_moe_layer_gated( - cactus_graph_t graph, cactus_node_t hidden, cactus_node_t routing_probs, cactus_node_t topk_indices, - const cactus_node_t* w1_weights, const cactus_node_t* w3_weights, const cactus_node_t* w2_weights, - size_t num_experts, size_t num_experts_per_tok, bool normalize_routing, float epsilon, float routed_scaling_factor, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_moe_layer_ungated( - cactus_graph_t graph, cactus_node_t hidden, cactus_node_t routing_probs, cactus_node_t topk_indices, - const cactus_node_t* w1_weights, const cactus_node_t* w2_weights, - size_t num_experts, size_t num_experts_per_tok, bool normalize_routing, float epsilon, float routed_scaling_factor, int32_t activation, cactus_node_t* out); - -CACTUS_FFI_EXPORT int cactus_graph_sample( - cactus_graph_t graph, cactus_node_t logits, float temperature, float top_p, size_t top_k, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_scatter_topk( - cactus_graph_t graph, cactus_node_t indices, cactus_node_t values, size_t num_classes, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_persistent( - cactus_graph_t graph, cactus_node_t source_node, cactus_node_t* out); -CACTUS_FFI_EXPORT int cactus_graph_is_populated( - cactus_graph_t graph, cactus_node_t persistent_node, int32_t* out_is_populated); -CACTUS_FFI_EXPORT int cactus_graph_invalidate_persistent( - cactus_graph_t graph, cactus_node_t persistent_node); - -CACTUS_FFI_EXPORT int cactus_graph_execute(cactus_graph_t graph); -CACTUS_FFI_EXPORT int cactus_graph_get_output_ptr(cactus_graph_t graph, -cactus_node_t node, void** out_ptr); -CACTUS_FFI_EXPORT int cactus_graph_get_output_info(cactus_graph_t graph, -cactus_node_t node, cactus_tensor_info_t* out_info); - -#ifdef __cplusplus -} -#endif - -#endif // CACTUS_FFI_H diff --git a/cactus/ffi/cactus_graph.cpp b/cactus/ffi/cactus_graph.cpp deleted file mode 100644 index f0aa77f37..000000000 --- a/cactus/ffi/cactus_graph.cpp +++ /dev/null @@ -1,1257 +0,0 @@ -#include "cactus_ffi.h" -#include "cactus_utils.h" -#include "../graph/graph.h" -#include -#include - -using namespace cactus::ffi; - -struct GraphHandle { - CactusGraph graph; -}; - -static GraphHandle* as_graph(cactus_graph_t g) { - return reinterpret_cast(g); -} - -static int fail_invalid(const char* msg) { - last_error_message = msg; - return -1; -} - -#define GRAPH_WRAP_UNARY(fn_name, method_name) \ -int fn_name(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out) { \ - if (!graph || !out) return fail_invalid("Invalid args to " #fn_name); \ - try { \ - *out = static_cast(as_graph(graph)->graph.method_name(static_cast(x))); \ - return 0; \ - } catch (const std::exception& e) { \ - last_error_message = e.what(); \ - return -1; \ - } \ -} - -#define GRAPH_WRAP_BINARY(fn_name, method_name) \ -int fn_name(cactus_graph_t graph, cactus_node_t a, cactus_node_t b, cactus_node_t* out) { \ - if (!graph || !out) return fail_invalid("Invalid args to " #fn_name); \ - try { \ - *out = static_cast(as_graph(graph)->graph.method_name(static_cast(a), static_cast(b))); \ - return 0; \ - } catch (const std::exception& e) { \ - last_error_message = e.what(); \ - return -1; \ - } \ -} - -extern "C" { - -cactus_graph_t cactus_graph_create(void) { - try { - return reinterpret_cast(new GraphHandle()); - } catch (const std::exception& e) { - last_error_message = e.what(); - return nullptr; - } catch (...) { - last_error_message = "Unknown error creating graph"; - return nullptr; - } -} - -void cactus_graph_destroy(cactus_graph_t graph) { - delete as_graph(graph); -} - -int cactus_graph_hard_reset(cactus_graph_t graph) { - if (!graph) { - last_error_message = "Invalid args to cactus_graph_hard_reset"; - return -1; - } - try { - as_graph(graph)->graph.hard_reset(); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_save(cactus_graph_t graph, const char* filename) { - if (!graph || !filename) { - last_error_message = "Invalid args to cactus_graph_save"; - return -1; - } - try { - as_graph(graph)->graph.save(filename); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - - -cactus_graph_t cactus_graph_load(const char* filename) { - if (!filename) { - last_error_message = "Invalid args to cactus_graph_load"; - return nullptr; - } - try { - auto handle = std::make_unique(); - handle->graph = CactusGraph::load(std::string(filename)); - return reinterpret_cast(handle.release()); - } catch (const std::exception& e) { - last_error_message = e.what(); - return nullptr; - } catch (...) { - last_error_message = "Unknown error loading graph"; - return nullptr; - } -} - -int cactus_graph_input(cactus_graph_t graph, const size_t* shape, size_t rank, int32_t precision, cactus_node_t* out_node) { - if (!graph || !shape || rank == 0 || !out_node) { - last_error_message = "Invalid args to cactus_graph_input"; - return -1; - } - try { - std::vector s(shape, shape + rank); - auto id = as_graph(graph)->graph.input(s, static_cast(precision)); - *out_node = static_cast(id); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_set_input(cactus_graph_t graph, cactus_node_t node, const void* data, int32_t precision) { - if (!graph || !data) { - last_error_message = "Invalid args to cactus_graph_set_input"; - return -1; - } - try { - as_graph(graph)->graph.set_input(static_cast(node), data, static_cast(precision)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_set_external_input(cactus_graph_t graph, cactus_node_t node, void* data, int32_t precision) { - if (!graph || !data) { - return fail_invalid("Invalid args to cactus_graph_set_external_input"); - } - try { - as_graph(graph)->graph.set_external_input(static_cast(node), data, static_cast(precision)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_precision_cast(cactus_graph_t graph, cactus_node_t input, int32_t target_precision, cactus_node_t* out) { - if (!graph || !out) { - return fail_invalid("Invalid args to cactus_graph_precision_cast"); - } - try { - *out = static_cast(as_graph(graph)->graph.precision_cast(static_cast(input), static_cast(target_precision))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_quantize_activations(cactus_graph_t graph, cactus_node_t input, cactus_node_t* out) { - if (!graph || !out) { - return fail_invalid("Invalid args to cactus_graph_quantize_activations"); - } - try { - *out = static_cast(as_graph(graph)->graph.quantize_activations(static_cast(input))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_add(cactus_graph_t graph, cactus_node_t a, cactus_node_t b, cactus_node_t* out) { - if (!graph || !out) { - last_error_message = "Invalid args to cactus_graph_add"; - return -1; - } - try { - *out = static_cast(as_graph(graph)->graph.add(static_cast(a), static_cast(b))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_add_clipped(cactus_graph_t graph, cactus_node_t a, cactus_node_t b, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_add_clipped"); - try { - *out = static_cast(as_graph(graph)->graph.add_clipped(static_cast(a), static_cast(b))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_subtract(cactus_graph_t graph, cactus_node_t a, cactus_node_t b, cactus_node_t* out) { - if (!graph || !out) { - last_error_message = "Invalid args to cactus_graph_subtract"; - return -1; - } - try { - *out = static_cast(as_graph(graph)->graph.subtract(static_cast(a), static_cast(b))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_multiply(cactus_graph_t graph, cactus_node_t a, cactus_node_t b, cactus_node_t* out) { - if (!graph || !out) { - last_error_message = "Invalid args to cactus_graph_multiply"; - return -1; - } - try { - *out = static_cast(as_graph(graph)->graph.multiply(static_cast(a), static_cast(b))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_divide(cactus_graph_t graph, cactus_node_t a, cactus_node_t b, cactus_node_t* out) { - if (!graph || !out) { - last_error_message = "Invalid args to cactus_graph_divide"; - return -1; - } - try { - *out = static_cast(as_graph(graph)->graph.divide(static_cast(a), static_cast(b))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_scalar_add(cactus_graph_t graph, cactus_node_t x, float value, cactus_node_t *out) { - if (!graph || !out) { - last_error_message = "Invalid args to cactus_graph_scalar_add"; - return -1; - } - try { - *out = static_cast(as_graph(graph)->graph.scalar_add(static_cast(x), value)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_scalar_subtract(cactus_graph_t graph, cactus_node_t x, float value, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_scalar_subtract"); - try { - *out = static_cast(as_graph(graph)->graph.scalar_subtract(static_cast(x), value)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_scalar_multiply(cactus_graph_t graph, cactus_node_t x, float value, cactus_node_t *out) { - if (!graph || !out) { - last_error_message = "Invalid args to cactus_graph_scalar_multiply"; - return -1; - } - try { - *out = static_cast(as_graph(graph)->graph.scalar_multiply(static_cast(x), value)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_scalar_divide(cactus_graph_t graph, cactus_node_t x, float value, cactus_node_t *out) { - if (!graph || !out) { - last_error_message = "Invalid args to cactus_graph_scalar_divide"; - return -1; - } - try { - *out = static_cast(as_graph(graph)->graph.scalar_divide(static_cast(x), value)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_scalar_exp(cactus_graph_t graph, cactus_node_t x, cactus_node_t *out) { - if (!graph || !out) { - last_error_message = "Invalid args to cactus_graph_scalar_exp"; - return -1; - } - try { - *out = static_cast(as_graph(graph)->graph.scalar_exp(static_cast(x))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_scalar_sqrt(cactus_graph_t graph, cactus_node_t x, cactus_node_t *out) { - if (!graph || !out) { - last_error_message = "Invalid args to cactus_graph_scalar_sqrt"; - return -1; - } - try { - *out = static_cast(as_graph(graph)->graph.scalar_sqrt(static_cast(x))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_scalar_cos(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_scalar_cos"); - try { - *out = static_cast(as_graph(graph)->graph.scalar_cos(static_cast(x))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_scalar_sin(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_scalar_sin"); - try { - *out = static_cast(as_graph(graph)->graph.scalar_sin(static_cast(x))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_scalar_log(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_scalar_log"); - try { - *out = static_cast(as_graph(graph)->graph.scalar_log(static_cast(x))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_abs(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out) { - if (!graph || !out) { - last_error_message = "Invalid args to cactus_graph_abs"; - return -1; - } - try { - *out = static_cast(as_graph(graph)->graph.abs(static_cast(x))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_pow(cactus_graph_t graph, cactus_node_t x, float exponent, cactus_node_t* out) { - if (!graph || !out) { - last_error_message = "Invalid args to cactus_graph_pow"; - return -1; - } - try { - *out = static_cast(as_graph(graph)->graph.pow(static_cast(x), exponent)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_view(cactus_graph_t graph, cactus_node_t x, const size_t* shape, size_t rank, cactus_node_t* out) { - if (!graph || !shape || rank == 0 || !out) { - last_error_message = "Invalid args to cactus_graph_view"; - return -1; - } - try { - std::vector s(shape, shape + rank); - *out = static_cast(as_graph(graph)->graph.view(static_cast(x), s)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_flatten(cactus_graph_t graph, cactus_node_t x, int32_t start_dim, int32_t end_dim, cactus_node_t* out) { - if (!graph || !out) { - last_error_message = "Invalid args to cactus_graph_flatten"; - return -1; - } - try { - *out = static_cast(as_graph(graph)->graph.flatten(static_cast(x), start_dim, end_dim)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_reshape(cactus_graph_t graph, cactus_node_t x, const size_t* shape, size_t rank, cactus_node_t* out) { - if (!graph || !shape || rank == 0 || !out) return fail_invalid("Invalid args to cactus_graph_reshape"); - try { - std::vector s(shape, shape + rank); - *out = static_cast(as_graph(graph)->graph.reshape(static_cast(x), s)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_transpose(cactus_graph_t graph, cactus_node_t x, int32_t backend, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_transpose"); - try { - *out = static_cast(as_graph(graph)->graph.transpose(static_cast(x), static_cast(backend))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_transpose_n(cactus_graph_t graph, cactus_node_t x, const size_t* permutation, size_t rank, int32_t backend, cactus_node_t* out) { - if (!graph || !permutation || rank == 0 || !out) return fail_invalid("Invalid args to cactus_graph_transpose_n"); - try { - std::vector p(permutation, permutation + rank); - *out = static_cast(as_graph(graph)->graph.transposeN(static_cast(x), p, static_cast(backend))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_slice(cactus_graph_t graph, cactus_node_t x, int32_t axis, size_t start, size_t length, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_slice"); - try { - *out = static_cast(as_graph(graph)->graph.slice(static_cast(x), axis, start, length)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_index(cactus_graph_t graph, cactus_node_t x, size_t index_value, int32_t dim, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_index"); - try { - *out = static_cast(as_graph(graph)->graph.index(static_cast(x), index_value, dim)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_sum(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_sum"); - try { - *out = static_cast(as_graph(graph)->graph.sum(static_cast(x), axis)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_mean(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_mean"); - try { - *out = static_cast(as_graph(graph)->graph.mean(static_cast(x), axis)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_variance(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_variance"); - try { - *out = static_cast(as_graph(graph)->graph.variance(static_cast(x), axis)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_min(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_min"); - try { - *out = static_cast(as_graph(graph)->graph.min(static_cast(x), axis)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_max(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_max"); - try { - *out = static_cast(as_graph(graph)->graph.max(static_cast(x), axis)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_concat(cactus_graph_t graph, cactus_node_t a, cactus_node_t b, int32_t axis, cactus_node_t* out) { - if (!graph || !out) { - last_error_message = "Invalid args to cactus_graph_concat"; - return -1; - } - try { - *out = static_cast(as_graph(graph)->graph.concat(static_cast(a), static_cast(b), axis)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_cat(cactus_graph_t graph, const cactus_node_t* nodes, size_t count, int32_t axis, cactus_node_t* out) { - if (!graph || !nodes || !out || count == 0) { - last_error_message = "Invalid args to cactus_graph_cat"; - return -1; - } - try { - std::vector ns; - for (size_t i = 0; i < count; ++i) { - ns.push_back(static_cast(nodes[i])); - } - *out = static_cast(as_graph(graph)->graph.cat(ns, axis)); - return 0; - - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_matmul(cactus_graph_t graph, cactus_node_t a, cactus_node_t b, bool pretransposed_rhs, int32_t backend, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_matmul"); - try { - *out = static_cast(as_graph(graph)->graph.matmul(static_cast(a), static_cast(b), pretransposed_rhs, static_cast(backend))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_gather(cactus_graph_t graph, cactus_node_t tensor, cactus_node_t indices, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_gather"); - try { - *out = static_cast(as_graph(graph)->graph.gather(static_cast(tensor), static_cast(indices))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_embedding_from_tensor(cactus_graph_t graph, cactus_node_t embedding_tensor, cactus_node_t indices, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_embedding_from_tensor"); - try { - *out = static_cast(as_graph(graph)->graph.embedding(static_cast(embedding_tensor), static_cast(indices))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_embedding_from_file(cactus_graph_t graph, const char* filename, cactus_node_t indices, cactus_node_t* out) { - if (!graph || !filename || !out) return fail_invalid("Invalid args to cactus_graph_embedding_from_file"); - try { - *out = static_cast(as_graph(graph)->graph.embedding(std::string(filename), static_cast(indices))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_mmap_embeddings(cactus_graph_t graph, const char* filename, cactus_node_t* out) { - if (!graph || !filename || !out) return fail_invalid("Invalid args to cactus_graph_mmap_embeddings"); - try { - *out = static_cast(as_graph(graph)->graph.mmap_embeddings(std::string(filename))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_mmap_weights(cactus_graph_t graph, const char* filename, cactus_node_t* out) { - if (!graph || !filename || !out) return fail_invalid("Invalid args to cactus_graph_mmap_weights"); - try { - *out = static_cast(as_graph(graph)->graph.mmap_weights(std::string(filename))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_bilinear_interpolation(cactus_graph_t graph, cactus_node_t pos_embeds, size_t dst_height, size_t dst_width, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_bilinear_interpolation"); - try { - *out = static_cast(as_graph(graph)->graph.bilinear_interpolation(static_cast(pos_embeds), dst_height, dst_width)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_set_grouped_scales(cactus_graph_t graph, cactus_node_t node, size_t group_size, size_t num_groups, void* scales_ptr) { - if (!graph || !scales_ptr) return fail_invalid("Invalid args to cactus_graph_set_grouped_scales"); - try { - as_graph(graph)->graph.set_grouped_scales(static_cast(node), group_size, num_groups, scales_ptr); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_set_interleaved(cactus_graph_t graph, cactus_node_t node, bool interleaved, size_t original_n) { - if (!graph) return fail_invalid("Invalid args to cactus_graph_set_interleaved"); - try { - as_graph(graph)->graph.set_interleaved(static_cast(node), interleaved, original_n); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_release_weight_pages(cactus_graph_t graph, cactus_node_t node) { - if (!graph) return fail_invalid("Invalid args to cactus_graph_release_weight_pages"); - try { - as_graph(graph)->graph.release_weight_pages(static_cast(node)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_prefetch_weight_pages(cactus_graph_t graph, cactus_node_t node) { - if (!graph) return fail_invalid("Invalid args to cactus_graph_prefetch_weight_pages"); - try { - as_graph(graph)->graph.prefetch_weight_pages(static_cast(node)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_release_all_weight_pages(cactus_graph_t graph) { - if (!graph) return fail_invalid("Invalid args to cactus_graph_release_all_weight_pages"); - try { - as_graph(graph)->graph.release_all_weight_pages(); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_relu(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_relu"); - try { - *out = static_cast(as_graph(graph)->graph.relu(static_cast(x))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_silu(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_silu"); - try { - *out = static_cast(as_graph(graph)->graph.silu(static_cast(x))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_gelu(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_gelu"); - try { - *out = static_cast(as_graph(graph)->graph.gelu(static_cast(x))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_gelu_erf(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_gelu_erf"); - try { - *out = static_cast(as_graph(graph)->graph.gelu_erf(static_cast(x))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_sigmoid(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_sigmoid"); - try { - *out = static_cast(as_graph(graph)->graph.sigmoid(static_cast(x))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_tanh(cactus_graph_t graph, cactus_node_t x, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_tanh"); - try { - *out = static_cast(as_graph(graph)->graph.tanh(static_cast(x))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_glu(cactus_graph_t graph, cactus_node_t x, int32_t axis, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_glu"); - try { - *out = static_cast(as_graph(graph)->graph.glu(static_cast(x), axis)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_layernorm(cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, cactus_node_t bias, float epsilon, bool has_bias, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_layernorm"); - try { - if (has_bias) { - *out = static_cast(as_graph(graph)->graph.layernorm(static_cast(input), static_cast(weight), static_cast(bias), epsilon)); - } else { - *out = static_cast(as_graph(graph)->graph.layernorm(static_cast(input), static_cast(weight), epsilon)); - } - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_groupnorm(cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, cactus_node_t bias, size_t num_groups, float epsilon, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_groupnorm"); - try { - *out = static_cast(as_graph(graph)->graph.groupnorm(static_cast(input), static_cast(weight), static_cast(bias), num_groups, epsilon)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_batchnorm(cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, cactus_node_t bias, cactus_node_t running_mean, cactus_node_t running_var, int32_t axis, float epsilon, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_batchnorm"); - try { - *out = static_cast(as_graph(graph)->graph.batchnorm(static_cast(input), static_cast(weight), static_cast(bias), static_cast(running_mean), static_cast(running_var), axis, epsilon)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_topk(cactus_graph_t graph, cactus_node_t input, size_t k, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_topk"); - try { - *out = static_cast(as_graph(graph)->graph.topk(static_cast(input), k)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_rms_norm(cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, float epsilon, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_rms_norm"); - try { - *out = static_cast(as_graph(graph)->graph.rms_norm(static_cast(input), static_cast(weight), epsilon)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_rope(cactus_graph_t graph, cactus_node_t input, float theta, size_t position_offset, int32_t backend, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_rope"); - try { - *out = static_cast(as_graph(graph)->graph.rope(static_cast(input), theta, position_offset, static_cast(backend))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_rope_gptj(cactus_graph_t graph, cactus_node_t input, float theta, size_t position_offset, size_t rot_dim, int32_t backend, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_rope_gptj"); - try { - *out = static_cast(as_graph(graph)->graph.rope_gptj(static_cast(input), theta, position_offset, rot_dim, static_cast(backend))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_softmax(cactus_graph_t graph, cactus_node_t input, int32_t axis, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_softmax"); - try { - *out = static_cast(as_graph(graph)->graph.softmax(static_cast(input), axis)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_attention(cactus_graph_t graph, cactus_node_t query, cactus_node_t key, cactus_node_t value, float scale, bool is_causal, size_t position_offset, size_t window_size, int32_t backend, bool use_mask, cactus_node_t mask, bool additive_mask, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_attention"); - try { - if (use_mask) { - *out = static_cast(as_graph(graph)->graph.attention_masked(static_cast(query), static_cast(key), static_cast(value), static_cast(mask), scale, is_causal, static_cast(backend), additive_mask, position_offset, window_size)); - } else if (window_size > 0 || position_offset > 0) { - *out = static_cast(as_graph(graph)->graph.attention(static_cast(query), static_cast(key), static_cast(value), scale, position_offset, window_size, static_cast(backend))); - } else { - *out = static_cast(as_graph(graph)->graph.attention(static_cast(query), static_cast(key), static_cast(value), scale, is_causal, static_cast(backend))); - } - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_rel_pos_bias(cactus_graph_t graph, cactus_node_t query, cactus_node_t relative_key, float scale, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_rel_pos_bias"); - try { - *out = static_cast(as_graph(graph)->graph.rel_pos_bias(static_cast(query), static_cast(relative_key), scale)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_attention_int8_hybrid(cactus_graph_t graph, cactus_node_t query, cactus_node_t key_new, cactus_node_t value_new, float scale, size_t position_offset, - const int8_t* cached_keys, const int8_t* cached_values, const float* k_scales, const float* v_scales, - size_t cache_len, size_t num_kv_heads, size_t head_dim, size_t window_size, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_attention_int8_hybrid"); - try { - *out = static_cast(as_graph(graph)->graph.attention_int8_hybrid(static_cast(query), static_cast(key_new), static_cast(value_new), scale, position_offset, cached_keys, cached_values, k_scales, v_scales, cache_len, num_kv_heads, head_dim, window_size)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_conv1d_causal(cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, size_t kernel_size, size_t dilation, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_conv1d_causal"); - try { - *out = static_cast(as_graph(graph)->graph.conv1d_causal(static_cast(input), static_cast(weight), kernel_size, dilation)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_conv1d_k3(cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, size_t stride, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_conv1d_k3"); - try { - *out = static_cast(as_graph(graph)->graph.conv1d_k3(static_cast(input), static_cast(weight), stride)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_conv1d_k7s3(cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, cactus_node_t bias, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_conv1d_k7s3"); - try { - *out = static_cast(as_graph(graph)->graph.conv1d_k7s3(static_cast(input), static_cast(weight), static_cast(bias))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_conv1d(cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, bool has_bias, cactus_node_t bias, size_t stride, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_conv1d"); - try { - if (has_bias) { - *out = static_cast(as_graph(graph)->graph.conv1d(static_cast(input), static_cast(weight), static_cast(bias), stride)); - } else { - *out = static_cast(as_graph(graph)->graph.conv1d(static_cast(input), static_cast(weight), stride)); - } - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_conv1d_same_depthwise_k9(cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, bool has_bias, cactus_node_t bias, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_conv1d_same_depthwise_k9"); - try { - if (has_bias) { - *out = static_cast(as_graph(graph)->graph.conv1d_same_depthwise_k9(static_cast(input), static_cast(weight), static_cast(bias))); - } else { - *out = static_cast(as_graph(graph)->graph.conv1d_same_depthwise_k9(static_cast(input), static_cast(weight))); - } - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_conv1d_pointwise(cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, bool has_bias, cactus_node_t bias, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_conv1d_pointwise"); - try { - if (has_bias) { - *out = static_cast(as_graph(graph)->graph.conv1d_pointwise(static_cast(input), static_cast(weight), static_cast(bias))); - } else { - *out = static_cast(as_graph(graph)->graph.conv1d_pointwise(static_cast(input), static_cast(weight))); - } - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_conv2d_k3s2p1(cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, bool has_bias, cactus_node_t bias, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_conv2d_k3s2p1"); - try { - if (has_bias) { - *out = static_cast(as_graph(graph)->graph.conv2d_k3s2p1(static_cast(input), static_cast(weight), static_cast(bias))); - } else { - *out = static_cast(as_graph(graph)->graph.conv2d_k3s2p1(static_cast(input), static_cast(weight))); - } - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_conv2d_depthwise_k3s2p1(cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, bool has_bias, cactus_node_t bias, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_conv2d_depthwise_k3s2p1"); - try { - if (has_bias) { - *out = static_cast(as_graph(graph)->graph.conv2d_depthwise_k3s2p1(static_cast(input), static_cast(weight), static_cast(bias))); - } else { - *out = static_cast(as_graph(graph)->graph.conv2d_depthwise_k3s2p1(static_cast(input), static_cast(weight))); - } - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_conv2d_pointwise_1x1(cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, bool has_bias, cactus_node_t bias, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_conv2d_pointwise_1x1"); - try { - if (has_bias) { - *out = static_cast(as_graph(graph)->graph.conv2d_pointwise_1x1(static_cast(input), static_cast(weight), static_cast(bias))); - } else { - *out = static_cast(as_graph(graph)->graph.conv2d_pointwise_1x1(static_cast(input), static_cast(weight))); - } - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_lstm_cell(cactus_graph_t graph, cactus_node_t input, cactus_node_t h_prev, cactus_node_t c_prev, cactus_node_t weight_ih, cactus_node_t weight_hh, cactus_node_t bias_ih, cactus_node_t bias_hh, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_lstm_cell"); - try { - *out = static_cast(as_graph(graph)->graph.lstm_cell(static_cast(input), static_cast(h_prev), static_cast(c_prev), static_cast(weight_ih), static_cast(weight_hh), static_cast(bias_ih), static_cast(bias_hh))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_gated_deltanet_decode(cactus_graph_t graph, cactus_node_t query, cactus_node_t key, cactus_node_t value, cactus_node_t gate_log, cactus_node_t beta, cactus_node_t initial_state, float scale, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_gated_deltanet_decode"); - try { - *out = static_cast(as_graph(graph)->graph.gated_deltanet_decode(static_cast(query), static_cast(key), static_cast(value), static_cast(gate_log), static_cast(beta), static_cast(initial_state), scale)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_gated_deltanet_prefill(cactus_graph_t graph, cactus_node_t query, cactus_node_t key, cactus_node_t value, cactus_node_t gate_log, cactus_node_t beta, cactus_node_t initial_state, size_t chunk_size, float scale, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_gated_deltanet_prefill"); - try { - *out = static_cast(as_graph(graph)->graph.gated_deltanet_prefill(static_cast(query), static_cast(key), static_cast(value), static_cast(gate_log), static_cast(beta), static_cast(initial_state), chunk_size, scale)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_stft(cactus_graph_t graph, cactus_node_t input, cactus_node_t weight, size_t stride, size_t num_fft_bins, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_stft"); - try { - *out = static_cast(as_graph(graph)->graph.stft(static_cast(input), static_cast(weight), stride, num_fft_bins)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_altup_predict(cactus_graph_t graph, cactus_node_t coefs, const cactus_node_t* streams, size_t num_streams, cactus_node_t* out) { - if (!graph || !streams || num_streams == 0 || !out) return fail_invalid("Invalid args to cactus_graph_altup_predict"); - try { - std::vector s(num_streams); - for (size_t i = 0; i < num_streams; ++i) s[i] = static_cast(streams[i]); - *out = static_cast(as_graph(graph)->graph.altup_predict(static_cast(coefs), s.data(), num_streams)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_altup_correct(cactus_graph_t graph, cactus_node_t coefs, cactus_node_t innovation, const cactus_node_t* predictions, size_t num_predictions, cactus_node_t* out) { - if (!graph || !predictions || num_predictions == 0 || !out) return fail_invalid("Invalid args to cactus_graph_altup_correct"); - try { - std::vector p(num_predictions); - for (size_t i = 0; i < num_predictions; ++i) p[i] = static_cast(predictions[i]); - *out = static_cast(as_graph(graph)->graph.altup_correct(static_cast(coefs), static_cast(innovation), p.data(), num_predictions)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_gaussian_topk(cactus_graph_t graph, cactus_node_t input, float ppf, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_gaussian_topk"); - try { - *out = static_cast(as_graph(graph)->graph.gaussian_topk(static_cast(input), ppf)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_moe_layer_gated(cactus_graph_t graph, cactus_node_t hidden, cactus_node_t routing_probs, cactus_node_t topk_indices, - const cactus_node_t* w1_weights, const cactus_node_t* w3_weights, const cactus_node_t* w2_weights, - size_t num_experts, size_t num_experts_per_tok, bool normalize_routing, float epsilon, float routed_scaling_factor, cactus_node_t* out) { - if (!graph || !w1_weights || !w3_weights || !w2_weights || !out) return fail_invalid("Invalid args to cactus_graph_moe_layer_gated"); - try { - std::vector w1(num_experts), w3(num_experts), w2(num_experts); - for (size_t i = 0; i < num_experts; ++i) { - w1[i] = static_cast(w1_weights[i]); - w3[i] = static_cast(w3_weights[i]); - w2[i] = static_cast(w2_weights[i]); - } - *out = static_cast(as_graph(graph)->graph.moe_layer(static_cast(hidden), static_cast(routing_probs), static_cast(topk_indices), w1, w3, w2, num_experts, num_experts_per_tok, normalize_routing, epsilon, routed_scaling_factor)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_moe_layer_ungated(cactus_graph_t graph, cactus_node_t hidden, cactus_node_t routing_probs, cactus_node_t topk_indices, - const cactus_node_t* w1_weights, const cactus_node_t* w2_weights, - size_t num_experts, size_t num_experts_per_tok, bool normalize_routing, float epsilon, float routed_scaling_factor, int32_t activation, cactus_node_t* out) { - if (!graph || !w1_weights || !w2_weights || !out) return fail_invalid("Invalid args to cactus_graph_moe_layer_ungated"); - try { - std::vector w1(num_experts), w2(num_experts); - for (size_t i = 0; i < num_experts; ++i) { - w1[i] = static_cast(w1_weights[i]); - w2[i] = static_cast(w2_weights[i]); - } - *out = static_cast(as_graph(graph)->graph.moe_layer(static_cast(hidden), static_cast(routing_probs), static_cast(topk_indices), w1, w2, num_experts, num_experts_per_tok, normalize_routing, epsilon, routed_scaling_factor, static_cast(activation))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_sample(cactus_graph_t graph, cactus_node_t logits, float temperature, float top_p, size_t top_k, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_sample"); - try { - std::unordered_map empty_bias; - *out = static_cast(as_graph(graph)->graph.sample(static_cast(logits), temperature, top_p, top_k, empty_bias)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_scatter_topk(cactus_graph_t graph, cactus_node_t indices, cactus_node_t values, size_t num_classes, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_scatter_topk"); - try { - *out = static_cast(as_graph(graph)->graph.scatter_topk(static_cast(indices), static_cast(values), num_classes)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_persistent(cactus_graph_t graph, cactus_node_t source_node, cactus_node_t* out) { - if (!graph || !out) return fail_invalid("Invalid args to cactus_graph_persistent"); - try { - *out = static_cast(as_graph(graph)->graph.persistent(static_cast(source_node))); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_is_populated(cactus_graph_t graph, cactus_node_t persistent_node, int32_t* out_is_populated) { - if (!graph || !out_is_populated) return fail_invalid("Invalid args to cactus_graph_is_populated"); - try { - *out_is_populated = as_graph(graph)->graph.is_populated(static_cast(persistent_node)) ? 1 : 0; - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_invalidate_persistent(cactus_graph_t graph, cactus_node_t persistent_node) { - if (!graph) return fail_invalid("Invalid args to cactus_graph_invalidate_persistent"); - try { - as_graph(graph)->graph.invalidate_persistent(static_cast(persistent_node)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_execute(cactus_graph_t graph) { - if (!graph) { - last_error_message = "Graph is null"; - return -1; - } - try { - as_graph(graph)->graph.execute(); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_get_output_ptr(cactus_graph_t graph, cactus_node_t node, void** out_ptr) { - if (!graph || !out_ptr) { - last_error_message = "Invalid args to cactus_graph_get_output_ptr"; - return -1; - } - try { - *out_ptr = as_graph(graph)->graph.get_output(static_cast(node)); - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} - -int cactus_graph_get_output_info(cactus_graph_t graph, cactus_node_t node, cactus_tensor_info_t* out_info) { - if (!graph || !out_info) { - last_error_message = "Invalid args to cactus_graph_get_output_info"; - return -1; - } - try { - const auto& buf = as_graph(graph)->graph.get_output_buffer(static_cast(node)); - out_info->precision = static_cast(buf.precision); - out_info->rank = buf.shape.size(); - if (out_info->rank > 8) { - last_error_message = "Rank exceeds cactus_tensor_info_t shape capacity"; - return -1; - } - for (size_t i = 0; i < out_info->rank; ++i) { - out_info->shape[i] = buf.shape[i]; - } - for (size_t i = out_info->rank; i < 8; ++i) { - out_info->shape[i] = 0; - } - out_info->num_elements = buf.total_size; - out_info->byte_size = buf.byte_size; - return 0; - } catch (const std::exception& e) { - last_error_message = e.what(); - return -1; - } -} -} diff --git a/cactus/ffi/cactus_index.cpp b/cactus/ffi/cactus_index.cpp deleted file mode 100644 index a81f62377..000000000 --- a/cactus/ffi/cactus_index.cpp +++ /dev/null @@ -1,458 +0,0 @@ -#include "cactus_ffi.h" -#include "cactus_utils.h" -#include - -using namespace cactus::ffi; - -struct CactusIndexHandle { - std::unique_ptr index; -}; - -static cactus::engine::index::QueryOptions parse_query_options_json(const std::string& json) { - cactus::engine::index::QueryOptions options; - - if (json.empty()) return options; - - size_t pos = json.find("\"top_k\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(json[pos])) pos++; - options.top_k = std::stoul(json.substr(pos)); - } - - pos = json.find("\"score_threshold\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(json[pos])) pos++; - options.score_threshold = std::stof(json.substr(pos)); - } - - return options; -} - -extern "C" { - -cactus_index_t cactus_index_init(const char* index_dir, size_t embedding_dim) { - if (!index_dir) { - last_error_message = "Index directory path cannot be null"; - CACTUS_LOG_ERROR("index_init", last_error_message); - return nullptr; - } - - if (embedding_dim == 0) { - last_error_message = "Embedding dimension must be greater than 0"; - CACTUS_LOG_ERROR("index_init", last_error_message); - return nullptr; - } - - std::string dir_path(index_dir); - if (dir_path.empty()) { - last_error_message = "Index directory path cannot be empty"; - CACTUS_LOG_ERROR("index_init", last_error_message); - return nullptr; - } - - std::string index_path_str = dir_path + "/index.bin"; - std::string data_path_str = dir_path + "/data.bin"; - - CACTUS_LOG_INFO("index_init", "Initializing index in directory: " << dir_path << ", dim: " << embedding_dim); - - try { - auto* handle = new CactusIndexHandle(); - handle->index = std::make_unique( - index_path_str, - data_path_str, - embedding_dim - ); - - if (!handle->index) { - last_error_message = "Failed to create index instance"; - CACTUS_LOG_ERROR("index_init", last_error_message); - - delete handle; - return nullptr; - } - - CACTUS_LOG_INFO("index_init", "Index initialized successfully"); - return handle; - - } catch (const std::exception& e) { - last_error_message = "Exception during index init: " + std::string(e.what()); - CACTUS_LOG_ERROR("index_init", last_error_message); - return nullptr; - } catch (...) { - last_error_message = "Unknown exception during index initialization"; - CACTUS_LOG_ERROR("index_init", last_error_message); - return nullptr; - } -} - -int cactus_index_add( - cactus_index_t index, - const int* ids, - const char** documents, - const char** metadatas, - const float** embeddings, - size_t count, - size_t embedding_dim -) { - if (!index) { - last_error_message = "Index not initialized"; - CACTUS_LOG_ERROR("index_add", last_error_message); - return -1; - } - - if (!ids || !documents || !embeddings) { - last_error_message = "Invalid parameters: ids, documents, or embeddings is null"; - CACTUS_LOG_ERROR("index_add", last_error_message); - return -1; - } - - if (count == 0) { - last_error_message = "Invalid parameter: count must be greater than 0"; - CACTUS_LOG_ERROR("index_add", last_error_message); - return -1; - } - - if (embedding_dim == 0) { - last_error_message = "Invalid parameter: embedding_dim must be greater than 0"; - CACTUS_LOG_ERROR("index_add", last_error_message); - return -1; - } - - try { - auto* handle = static_cast(index); - - std::vector docs; - docs.reserve(count); - - for (size_t i = 0; i < count; ++i) { - if (!embeddings[i]) { - last_error_message = "Embedding at index " + std::to_string(i) + " is null"; - CACTUS_LOG_ERROR("index_add", last_error_message); - return -1; - } - - docs.emplace_back( - ids[i], - std::vector(embeddings[i], embeddings[i] + embedding_dim), - documents[i] ? std::string(documents[i]) : "", - metadatas && metadatas[i] ? std::string(metadatas[i]) : "" - ); - } - - CACTUS_LOG_INFO("index_add", "Adding " << docs.size() << " documents"); - handle->index->add_documents(docs); - - return 0; - - } catch (const std::exception& e) { - last_error_message = std::string(e.what()); - CACTUS_LOG_ERROR("index_add", "Exception: " << e.what()); - return -1; - } catch (...) { - last_error_message = "Unknown error during add"; - CACTUS_LOG_ERROR("index_add", last_error_message); - return -1; - } -} - -int cactus_index_delete( - cactus_index_t index, - const int* ids, - size_t ids_count -) { - if (!index) { - last_error_message = "Index not initialized"; - CACTUS_LOG_ERROR("index_delete", last_error_message); - return -1; - } - - if (!ids || ids_count == 0) { - last_error_message = "Invalid parameters"; - CACTUS_LOG_ERROR("index_delete", last_error_message); - return -1; - } - - try { - auto* handle = static_cast(index); - - std::vector doc_ids(ids, ids + ids_count); - - CACTUS_LOG_INFO("index_delete", "Deleting " << doc_ids.size() << " documents"); - handle->index->delete_documents(doc_ids); - - return 0; - - } catch (const std::exception& e) { - last_error_message = std::string(e.what()); - CACTUS_LOG_ERROR("index_delete", "Exception: " << e.what()); - return -1; - } catch (...) { - last_error_message = "Unknown error during delete"; - CACTUS_LOG_ERROR("index_delete", last_error_message); - return -1; - } -} - -int cactus_index_get( - cactus_index_t index, - const int* ids, - size_t ids_count, - char** document_buffers, - size_t* document_buffer_sizes, - char** metadata_buffers, - size_t* metadata_buffer_sizes, - float** embedding_buffers, - size_t* embedding_buffer_sizes -) { - if (!index) { - last_error_message = "Index not initialized"; - CACTUS_LOG_ERROR("index_get", last_error_message); - return -1; - } - - if (!ids) { - last_error_message = "Invalid parameter: ids is null"; - CACTUS_LOG_ERROR("index_get", last_error_message); - return -1; - } - - if (ids_count == 0) { - last_error_message = "Invalid parameter: ids_count must be greater than 0"; - CACTUS_LOG_ERROR("index_get", last_error_message); - return -1; - } - - if (document_buffers && !document_buffer_sizes) { - last_error_message = "Invalid parameters: document_buffers provided but document_buffer_sizes is null"; - CACTUS_LOG_ERROR("index_get", last_error_message); - return -1; - } - - if (metadata_buffers && !metadata_buffer_sizes) { - last_error_message = "Invalid parameters: metadata_buffers provided but metadata_buffer_sizes is null"; - CACTUS_LOG_ERROR("index_get", last_error_message); - return -1; - } - - if (embedding_buffers && !embedding_buffer_sizes) { - last_error_message = "Invalid parameters: embedding_buffers provided but embedding_buffer_sizes is null"; - CACTUS_LOG_ERROR("index_get", last_error_message); - return -1; - } - - try { - auto* handle = static_cast(index); - - std::vector doc_ids(ids, ids + ids_count); - - CACTUS_LOG_INFO("index_get", "Getting " << doc_ids.size() << " documents"); - std::vector docs = handle->index->get_documents(doc_ids); - - for (size_t i = 0; i < docs.size(); ++i) { - if (document_buffers) { - size_t document_size = docs[i].content.size() + 1; - if (document_buffer_sizes[i] < document_size) { - last_error_message = "Document buffer too small at index " + std::to_string(i) + - " (required: " + std::to_string(document_size) + - ", provided: " + std::to_string(document_buffer_sizes[i]) + ")"; - CACTUS_LOG_ERROR("index_get", last_error_message); - return -1; - } - } - - if (metadata_buffers) { - size_t metadata_size = docs[i].metadata.size() + 1; - if (metadata_buffer_sizes[i] < metadata_size) { - last_error_message = "Metadata buffer too small at index " + std::to_string(i) + - " (required: " + std::to_string(metadata_size) + - ", provided: " + std::to_string(metadata_buffer_sizes[i]) + ")"; - CACTUS_LOG_ERROR("index_get", last_error_message); - return -1; - } - } - - if (embedding_buffers) { - size_t embedding_size = docs[i].embedding.size(); - if (embedding_buffer_sizes[i] < embedding_size) { - last_error_message = "Embedding buffer too small at index " + std::to_string(i) + - " (required: " + std::to_string(embedding_size) + - ", provided: " + std::to_string(embedding_buffer_sizes[i]) + ")"; - CACTUS_LOG_ERROR("index_get", last_error_message); - return -1; - } - } - } - - for (size_t i = 0; i < docs.size(); ++i) { - if (document_buffers) { - size_t document_size = docs[i].content.size() + 1; - std::memcpy(document_buffers[i], docs[i].content.c_str(), document_size); - document_buffer_sizes[i] = document_size; - } - - if (metadata_buffers) { - size_t metadata_size = docs[i].metadata.size() + 1; - std::memcpy(metadata_buffers[i], docs[i].metadata.c_str(), metadata_size); - metadata_buffer_sizes[i] = metadata_size; - } - - if (embedding_buffers) { - size_t embedding_size = docs[i].embedding.size(); - std::memcpy(embedding_buffers[i], docs[i].embedding.data(), embedding_size * sizeof(float)); - embedding_buffer_sizes[i] = embedding_size; - } - } - - return 0; - - } catch (const std::exception& e) { - last_error_message = std::string(e.what()); - CACTUS_LOG_ERROR("index_get", "Exception: " << e.what()); - return -1; - } catch (...) { - last_error_message = "Unknown error during get"; - CACTUS_LOG_ERROR("index_get", last_error_message); - return -1; - } -} - -int cactus_index_query( - cactus_index_t index, - const float** embeddings, - size_t embeddings_count, - size_t embedding_dim, - const char* options_json, - int** id_buffers, - size_t* id_buffer_sizes, - float** score_buffers, - size_t* score_buffer_sizes -) { - if (!index) { - last_error_message = "Index not initialized"; - CACTUS_LOG_ERROR("index_query", last_error_message); - return -1; - } - - if (!embeddings) { - last_error_message = "Invalid parameter: embeddings is null"; - CACTUS_LOG_ERROR("index_query", last_error_message); - return -1; - } - - if (!id_buffers || !score_buffers) { - last_error_message = "Invalid parameters: id_buffers or score_buffers is null"; - CACTUS_LOG_ERROR("index_query", last_error_message); - return -1; - } - - if (!id_buffer_sizes || !score_buffer_sizes) { - last_error_message = "Invalid parameters: buffer size arrays cannot be null"; - CACTUS_LOG_ERROR("index_query", last_error_message); - return -1; - } - - if (embeddings_count == 0) { - last_error_message = "Invalid parameter: embeddings_count must be greater than 0"; - CACTUS_LOG_ERROR("index_query", last_error_message); - return -1; - } - - if (embedding_dim == 0) { - last_error_message = "Invalid parameter: embedding_dim must be greater than 0"; - CACTUS_LOG_ERROR("index_query", last_error_message); - return -1; - } - - try { - auto* handle = static_cast(index); - - std::vector> embeddings_vec; - embeddings_vec.reserve(embeddings_count); - - for (size_t i = 0; i < embeddings_count; ++i) { - if (!embeddings[i]) { - last_error_message = "Embedding at index " + std::to_string(i) + " is null"; - CACTUS_LOG_ERROR("index_query", last_error_message); - return -1; - } - embeddings_vec.emplace_back(embeddings[i], embeddings[i] + embedding_dim); - } - - cactus::engine::index::QueryOptions options; - if (options_json && std::strlen(options_json) > 0) { - options = parse_query_options_json(options_json); - } - - CACTUS_LOG_DEBUG("index_query", "Querying with " << embeddings_count << " queries, dim: " << embedding_dim << ", top_k: " << options.top_k); - std::vector> results = handle->index->query(embeddings_vec, options); - - for (size_t i = 0; i < results.size(); ++i) { - size_t result_count = results[i].size(); - if (id_buffer_sizes[i] < result_count || score_buffer_sizes[i] < result_count) { - last_error_message = "Result buffer too small at query index " + std::to_string(i) + - " (required: " + std::to_string(result_count) + - ", id_buffer: " + std::to_string(id_buffer_sizes[i]) + - ", score_buffer: " + std::to_string(score_buffer_sizes[i]) + ")"; - CACTUS_LOG_ERROR("index_query", last_error_message); - return -1; - } - } - - for (size_t i = 0; i < results.size(); ++i) { - size_t result_count = results[i].size(); - - for (size_t j = 0; j < result_count; ++j) { - id_buffers[i][j] = results[i][j].doc_id; - score_buffers[i][j] = results[i][j].score; - } - - id_buffer_sizes[i] = result_count; - score_buffer_sizes[i] = result_count; - } - - return 0; - - } catch (const std::exception& e) { - last_error_message = std::string(e.what()); - CACTUS_LOG_ERROR("index_query", "Exception: " << e.what()); - return -1; - } catch (...) { - last_error_message = "Unknown error during query"; - CACTUS_LOG_ERROR("index_query", last_error_message); - return -1; - } -} - -int cactus_index_compact(cactus_index_t index) { - if (!index) { - last_error_message = "Index not initialized"; - CACTUS_LOG_ERROR("index_compact", last_error_message); - return -1; - } - - try { - auto* handle = static_cast(index); - - CACTUS_LOG_INFO("index_compact", "Compacting index"); - handle->index->compact(); - - return 0; - - } catch (const std::exception& e) { - last_error_message = std::string(e.what()); - CACTUS_LOG_ERROR("index_compact", "Exception: " << e.what()); - return -1; - } catch (...) { - last_error_message = "Unknown error during compact"; - CACTUS_LOG_ERROR("index_compact", last_error_message); - return -1; - } -} - -void cactus_index_destroy(cactus_index_t index) { - if (index) delete static_cast(index); -} - -} diff --git a/cactus/ffi/cactus_init.cpp b/cactus/ffi/cactus_init.cpp deleted file mode 100644 index d3e4fd122..000000000 --- a/cactus/ffi/cactus_init.cpp +++ /dev/null @@ -1,485 +0,0 @@ -#include "cactus_ffi.h" -#include "cactus_utils.h" -#include "telemetry/telemetry.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace cactus::engine; -using namespace cactus::ffi; - -static constexpr size_t RAG_MAX_CHUNK_TOKENS = 128; -static constexpr size_t RAG_MIN_CHUNK_TOKENS = 24; -static constexpr size_t RAG_CHUNK_OVERLAP = 32; - -static void apply_no_cloud_telemetry_env() { - if (cactus::ffi::env_flag_enabled("CACTUS_NO_CLOUD_TELE")) { - cactus::telemetry::setCloudDisabled(true); - } -} - -static time_t get_file_mtime(const std::string& path) { - struct stat st; - if (stat(path.c_str(), &st) == 0) { - return st.st_mtime; - } - return 0; -} - -static bool corpus_is_stale(const std::string& corpus_dir) { - std::string index_path = corpus_dir + "/index.bin"; - time_t index_mtime = get_file_mtime(index_path); - if (index_mtime == 0) return true; - - DIR* dir = opendir(corpus_dir.c_str()); - if (!dir) return true; - - struct dirent* entry; - while ((entry = readdir(dir)) != nullptr) { - std::string name = entry->d_name; - if (name == "." || name == "..") continue; - if (name == "index.bin" || name == "data.bin") continue; - - bool is_corpus_file = false; - if (name.size() > 4 && name.substr(name.size() - 4) == ".txt") is_corpus_file = true; - if (name.size() > 3 && name.substr(name.size() - 3) == ".md") is_corpus_file = true; - - if (is_corpus_file) { - std::string full_path = corpus_dir + "/" + name; - time_t file_mtime = get_file_mtime(full_path); - if (file_mtime > index_mtime) { - closedir(dir); - CACTUS_LOG_INFO("init", "Corpus file " << name << " is newer than index, rebuilding"); - return true; - } - } - } - closedir(dir); - return false; -} - -static std::string read_file_contents(const std::string& path) { - std::ifstream file(path); - if (!file.is_open()) return ""; - std::stringstream buffer; - buffer << file.rdbuf(); - return buffer.str(); -} - -static std::vector scan_corpus_files(const std::string& corpus_dir) { - std::vector files; - DIR* dir = opendir(corpus_dir.c_str()); - if (!dir) return files; - - struct dirent* entry; - while ((entry = readdir(dir)) != nullptr) { - std::string name = entry->d_name; - if (name == "." || name == "..") continue; - if (name == "index.bin" || name == "data.bin") continue; - - std::string full_path = corpus_dir + "/" + name; - struct stat st; - if (stat(full_path.c_str(), &st) == 0 && S_ISREG(st.st_mode)) { - if (name.size() > 4 && (name.substr(name.size() - 4) == ".txt" || name.substr(name.size() - 3) == ".md")) { - files.push_back(full_path); - } - } - } - closedir(dir); - return files; -} - -static std::vector split_into_paragraphs(const std::string& content) { - std::vector paragraphs; - std::string current; - - size_t i = 0; - while (i < content.size()) { - // Check for markdown header - if (content[i] == '#' && (i == 0 || content[i-1] == '\n')) { - if (!current.empty()) { - paragraphs.push_back(current); - current.clear(); - } - // Include the header line - while (i < content.size() && content[i] != '\n') { - current += content[i++]; - } - if (i < content.size()) current += content[i++]; - continue; - } - - // Check for double newline (paragraph break) - if (content[i] == '\n' && i + 1 < content.size() && content[i+1] == '\n') { - current += content[i]; - if (!current.empty() && current != "\n") { - paragraphs.push_back(current); - current.clear(); - } - i++; - // Skip multiple blank lines - while (i < content.size() && content[i] == '\n') i++; - continue; - } - - current += content[i++]; - } - - if (!current.empty()) { - paragraphs.push_back(current); - } - - return paragraphs; -} - -static std::vector> chunk_corpus( - const std::vector& file_paths, - Tokenizer* tokenizer -) { - std::vector> chunks; - - for (const auto& path : file_paths) { - std::string content = read_file_contents(path); - if (content.empty()) continue; - - std::string filename = path; - size_t last_slash = path.find_last_of("/\\"); - if (last_slash != std::string::npos) { - filename = path.substr(last_slash + 1); - } - - auto paragraphs = split_into_paragraphs(content); - - std::string current_chunk; - size_t current_tokens = 0; - - for (const auto& para : paragraphs) { - std::vector para_tokens = tokenizer->encode(para); - size_t para_token_count = para_tokens.size(); - - // If paragraph alone is too large, split it by tokens - if (para_token_count > RAG_MAX_CHUNK_TOKENS) { - // Flush current chunk first - if (!current_chunk.empty()) { - chunks.emplace_back(current_chunk, filename); - current_chunk.clear(); - current_tokens = 0; - } - - // Split large paragraph by sentences or fixed tokens - size_t stride = RAG_MAX_CHUNK_TOKENS - RAG_CHUNK_OVERLAP; - for (size_t i = 0; i < para_tokens.size(); i += stride) { - size_t end = std::min(i + RAG_MAX_CHUNK_TOKENS, para_tokens.size()); - std::vector chunk_tokens(para_tokens.begin() + i, para_tokens.begin() + end); - std::string chunk_text = tokenizer->decode(chunk_tokens); - chunks.emplace_back(chunk_text, filename); - if (end >= para_tokens.size()) break; - } - continue; - } - - // Would adding this paragraph exceed max? - if (current_tokens + para_token_count > RAG_MAX_CHUNK_TOKENS && !current_chunk.empty()) { - chunks.emplace_back(current_chunk, filename); - current_chunk.clear(); - current_tokens = 0; - } - - // Add paragraph to current chunk - if (!current_chunk.empty()) current_chunk += "\n"; - current_chunk += para; - current_tokens += para_token_count; - } - - // Don't forget the last chunk - if (!current_chunk.empty() && current_tokens >= RAG_MIN_CHUNK_TOKENS) { - chunks.emplace_back(current_chunk, filename); - } else if (!current_chunk.empty() && !chunks.empty()) { - // Append small remaining chunk to previous - chunks.back().first += "\n" + current_chunk; - } else if (!current_chunk.empty()) { - chunks.emplace_back(current_chunk, filename); - } - } - - return chunks; -} - -static bool build_corpus_index(CactusModelHandle* handle, const std::string& corpus_dir) { - CACTUS_LOG_INFO("init", "Building corpus index from: " << corpus_dir); - - auto* tokenizer = handle->model->get_tokenizer(); - if (!tokenizer) { - CACTUS_LOG_ERROR("init", "No tokenizer available for corpus indexing"); - return false; - } - - auto file_paths = scan_corpus_files(corpus_dir); - if (file_paths.empty()) { - CACTUS_LOG_WARN("init", "No .txt or .md files found in corpus directory"); - return false; - } - - CACTUS_LOG_INFO("init", "Found " << file_paths.size() << " corpus files"); - - auto chunks = chunk_corpus(file_paths, tokenizer); - if (chunks.empty()) { - CACTUS_LOG_WARN("init", "No chunks generated from corpus"); - return false; - } - - CACTUS_LOG_INFO("init", "Generated " << chunks.size() << " chunks from corpus"); - - std::vector test_tokens = tokenizer->encode("test"); - std::vector test_embedding = handle->model->get_embeddings(test_tokens, true, true); - if (test_embedding.empty()) { - CACTUS_LOG_ERROR("init", "Failed to get embedding dimension"); - return false; - } - size_t embedding_dim = test_embedding.size(); - handle->corpus_embedding_dim = embedding_dim; - - CACTUS_LOG_INFO("init", "Embedding dimension: " << embedding_dim); - - std::string index_path = corpus_dir + "/index.bin"; - std::string data_path = corpus_dir + "/data.bin"; - - std::remove(index_path.c_str()); - std::remove(data_path.c_str()); - - try { - handle->corpus_index = std::make_unique(index_path, data_path, embedding_dim); - } catch (const std::exception& e) { - CACTUS_LOG_ERROR("init", "Failed to create index: " << e.what()); - return false; - } - - std::vector docs; - docs.reserve(chunks.size()); - - for (size_t i = 0; i < chunks.size(); ++i) { - const auto& [chunk_text, source_file] = chunks[i]; - - std::vector tokens = tokenizer->encode(chunk_text); - std::vector embedding = handle->model->get_embeddings(tokens, true, true); - - if (embedding.size() != embedding_dim) { - CACTUS_LOG_WARN("init", "Skipping chunk " << i << " - embedding dimension mismatch"); - continue; - } - - docs.push_back(index::Document{ - static_cast(i), - std::move(embedding), - chunk_text, - source_file - }); - - if ((i + 1) % 50 == 0) { - CACTUS_LOG_INFO("init", "Embedded " << (i + 1) << "/" << chunks.size() << " chunks"); - } - } - - if (docs.empty()) { - CACTUS_LOG_ERROR("init", "No documents to add to index"); - return false; - } - - try { - handle->corpus_index->add_documents(docs); - } catch (const std::exception& e) { - CACTUS_LOG_ERROR("init", "Failed to add documents to index: " << e.what()); - return false; - } - - CACTUS_LOG_INFO("init", "Corpus index built successfully with " << docs.size() << " chunks"); - return true; -} - -static bool load_corpus_index(CactusModelHandle* handle, const std::string& corpus_dir) { - std::string index_path = corpus_dir + "/index.bin"; - std::string data_path = corpus_dir + "/data.bin"; - - struct stat st; - if (stat(index_path.c_str(), &st) != 0 || stat(data_path.c_str(), &st) != 0) { - return false; - } - - if (corpus_is_stale(corpus_dir)) { - return false; - } - - auto* tokenizer = handle->model->get_tokenizer(); - std::vector test_tokens = tokenizer->encode("test"); - std::vector test_embedding = handle->model->get_embeddings(test_tokens, true, true); - if (test_embedding.empty()) { - CACTUS_LOG_ERROR("init", "Failed to get embedding dimension for index loading"); - return false; - } - size_t embedding_dim = test_embedding.size(); - handle->corpus_embedding_dim = embedding_dim; - - try { - handle->corpus_index = std::make_unique(index_path, data_path, embedding_dim); - CACTUS_LOG_INFO("init", "Loaded existing corpus index from: " << corpus_dir); - return true; - } catch (const std::exception& e) { - CACTUS_LOG_WARN("init", "Failed to load existing index: " << e.what()); - return false; - } -} - -std::string last_error_message; - -bool matches_stop_sequence(const std::vector& generated_tokens, - const std::vector>& stop_sequences) { - for (const auto& stop_seq : stop_sequences) { - if (stop_seq.empty()) continue; - if (generated_tokens.size() >= stop_seq.size()) { - if (std::equal(stop_seq.rbegin(), stop_seq.rend(), generated_tokens.rbegin())) - return true; - } - } - return false; -} - -extern "C" { - -const char* cactus_get_last_error() { - return last_error_message.c_str(); -} - -cactus_model_t cactus_init(const char* model_path, const char* corpus_dir, bool cache_index) { - constexpr size_t DEFAULT_CONTEXT_SIZE = 512; // matches default sliding window size - - std::string model_path_str = model_path ? std::string(model_path) : "unknown"; - - std::string model_name = model_path_str; - size_t last_slash = model_path_str.find_last_of("/\\"); - if (last_slash != std::string::npos) { - model_name = model_path_str.substr(last_slash + 1); - } - - CACTUS_LOG_INFO("init", "Loading model: " << model_name << " from " << model_path_str); - - apply_no_cloud_telemetry_env(); - cactus::telemetry::init(nullptr, model_path_str.c_str(), nullptr); - - auto __cactus_init_start = std::chrono::steady_clock::now(); - - try { - auto* handle = new CactusModelHandle(); - handle->model = create_model(model_path); - handle->model_name = model_name; - - if (!handle->model) { - last_error_message = "Failed to create model - check config.txt exists at: " + model_path_str; - CACTUS_LOG_ERROR("init", last_error_message); - { - auto __cactus_init_err_dur = std::chrono::duration_cast(std::chrono::steady_clock::now() - __cactus_init_start).count(); - cactus::telemetry::recordInit(model_name.c_str(), false, static_cast(__cactus_init_err_dur), last_error_message.c_str()); - } - delete handle; - return nullptr; - } - - if (!handle->model->init(model_path, DEFAULT_CONTEXT_SIZE)) { - last_error_message = "Failed to initialize model - check weight files at: " + model_path_str; - CACTUS_LOG_ERROR("init", last_error_message); - { - auto __cactus_init_err_dur = std::chrono::duration_cast(std::chrono::steady_clock::now() - __cactus_init_start).count(); - cactus::telemetry::recordInit(model_name.c_str(), false, static_cast(__cactus_init_err_dur), last_error_message.c_str()); - } - delete handle; - return nullptr; - } - - auto model_type = handle->model->get_config().model_type; - if (model_type == Config::ModelType::WHISPER || - model_type == Config::ModelType::MOONSHINE || - model_type == Config::ModelType::PARAKEET || - model_type == Config::ModelType::PARAKEET_TDT) { - std::string vad_path = model_path_str + "/vad"; - handle->vad_model = create_model(vad_path); - if (!handle->vad_model) { - last_error_message = "Failed to create VAD model - check VAD weights at: " + vad_path; - CACTUS_LOG_ERROR("init", last_error_message); - delete handle; - return nullptr; - } - if (!handle->vad_model->init(vad_path, 0, "", false)) { - last_error_message = "Failed to initialize VAD model - check VAD weight files at: " + vad_path; - CACTUS_LOG_ERROR("init", last_error_message); - delete handle; - return nullptr; - } - } - - if (corpus_dir != nullptr && strlen(corpus_dir) > 0) { - handle->corpus_dir = std::string(corpus_dir); - - bool loaded = false; - if (cache_index) { - loaded = load_corpus_index(handle, handle->corpus_dir); - } - - if (!loaded) { - CACTUS_LOG_INFO("init", (cache_index ? "No existing index found, building new corpus index" : "Building fresh corpus index (caching disabled)")); - if (!build_corpus_index(handle, handle->corpus_dir)) { - CACTUS_LOG_WARN("init", "Failed to build corpus index - RAG disabled"); - } - } - } - - CACTUS_LOG_INFO("init", "Model loaded successfully: " << model_name); - { - auto __cactus_init_ok_dur = std::chrono::duration_cast(std::chrono::steady_clock::now() - __cactus_init_start).count(); - cactus::telemetry::recordInit(model_name.c_str(), true, static_cast(__cactus_init_ok_dur), ""); - } - - return handle; - } catch (const std::exception& e) { - last_error_message = "Exception during init: " + std::string(e.what()); - CACTUS_LOG_ERROR("init", last_error_message); - { - auto __cactus_init_err_dur = std::chrono::duration_cast(std::chrono::steady_clock::now() - __cactus_init_start).count(); - cactus::telemetry::recordInit(model_name.c_str(), false, static_cast(__cactus_init_err_dur), last_error_message.c_str()); - } - return nullptr; - } catch (...) { - last_error_message = "Unknown exception during model initialization"; - CACTUS_LOG_ERROR("init", last_error_message); - { - auto __cactus_init_err_dur = std::chrono::duration_cast(std::chrono::steady_clock::now() - __cactus_init_start).count(); - cactus::telemetry::recordInit(model_name.c_str(), false, static_cast(__cactus_init_err_dur), last_error_message.c_str()); - } - return nullptr; - } -} - -void cactus_destroy(cactus_model_t model) { - if (model) delete static_cast(model); -} - -void cactus_reset(cactus_model_t model) { - if (!model) return; - auto* handle = static_cast(model); - handle->model->reset_cache(); - handle->processed_tokens.clear(); - handle->processed_images.clear(); - handle->user_audio_counts.clear(); -} - -void cactus_stop(cactus_model_t model) { - if (!model) return; - auto* handle = static_cast(model); - handle->should_stop = true; -} - -} diff --git a/cactus/ffi/cactus_log.cpp b/cactus/ffi/cactus_log.cpp deleted file mode 100644 index d067f9adf..000000000 --- a/cactus/ffi/cactus_log.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "cactus_ffi.h" -#include "graph/graph.h" - -extern "C" { - -void cactus_log_set_level(int level) { - cactus::Logger::instance().set_level(static_cast(level)); -} - -void cactus_log_set_callback(cactus_log_callback_t callback, void* user_data) { - if (callback) { - cactus::Logger::instance().set_callback( - [callback, user_data](cactus::LogLevel level, - const std::string& component, - const std::string& message) { - callback(static_cast(level), component.c_str(), message.c_str(), user_data); - }); - } else { - cactus::Logger::instance().set_callback(nullptr); - } -} - -} diff --git a/cactus/ffi/cactus_rag.cpp b/cactus/ffi/cactus_rag.cpp deleted file mode 100644 index a0765f787..000000000 --- a/cactus/ffi/cactus_rag.cpp +++ /dev/null @@ -1,459 +0,0 @@ -#include "cactus_ffi.h" -#include "cactus_utils.h" -#include -#include -#include -#include -#include -#include -#include - -using namespace cactus::engine; -using namespace cactus::ffi; - -static constexpr size_t RAG_TOP_K = 5; -static constexpr size_t RAG_CANDIDATE_K = 20; -static constexpr float RRF_K = 60.0f; -static constexpr float RRF_EMB_WEIGHT = 0.8f; -static constexpr float RRF_BM25_WEIGHT = 0.2f; -static constexpr float BM25_K1 = 1.5f; -static constexpr float BM25_B = 0.75f; - -static std::vector> compute_rrf_scores( - const std::vector>& emb_ranked, - const std::vector>& bm25_ranked, - size_t num_items -) { - std::vector> emb_sorted = emb_ranked; - std::vector> bm25_sorted = bm25_ranked; - - std::sort(emb_sorted.begin(), emb_sorted.end(), - [](const auto& a, const auto& b) { return a.first > b.first; }); - std::sort(bm25_sorted.begin(), bm25_sorted.end(), - [](const auto& a, const auto& b) { return a.first > b.first; }); - - std::unordered_map emb_rank_map, bm25_rank_map; - for (size_t r = 0; r < emb_sorted.size(); ++r) { - emb_rank_map[emb_sorted[r].second] = r + 1; - } - for (size_t r = 0; r < bm25_sorted.size(); ++r) { - bm25_rank_map[bm25_sorted[r].second] = r + 1; - } - - std::vector> rrf_scored; - rrf_scored.reserve(num_items); - for (size_t i = 0; i < num_items; ++i) { - float rrf = RRF_EMB_WEIGHT / (RRF_K + emb_rank_map[i]) + - RRF_BM25_WEIGHT / (RRF_K + bm25_rank_map[i]); - rrf_scored.emplace_back(rrf, i); - } - - std::sort(rrf_scored.begin(), rrf_scored.end(), - [](const auto& a, const auto& b) { return a.first > b.first; }); - - return rrf_scored; -} - -static std::vector tokenize_words(const std::string& text) { - std::vector words; - std::string current; - for (char c : text) { - if (std::isalnum(static_cast(c))) { - current += std::tolower(static_cast(c)); - } else if (!current.empty()) { - if (current.length() > 2) { - words.push_back(current); - } - current.clear(); - } - } - if (!current.empty() && current.length() > 2) { - words.push_back(current); - } - return words; -} - -static float compute_bm25_score( - const std::vector& query_words, - const std::string& doc_content, - float avg_doc_len, - const std::unordered_map& doc_freqs, - int total_docs -) { - auto doc_words = tokenize_words(doc_content); - float doc_len = static_cast(doc_words.size()); - - std::unordered_map tf; - for (const auto& w : doc_words) { - tf[w]++; - } - - float score = 0.0f; - std::unordered_set query_set(query_words.begin(), query_words.end()); - - for (const auto& term : query_set) { - auto tf_it = tf.find(term); - if (tf_it == tf.end()) continue; - - float term_freq = static_cast(tf_it->second); - float df = 1.0f; - auto df_it = doc_freqs.find(term); - if (df_it != doc_freqs.end()) { - df = static_cast(df_it->second); - } - - float idf = std::log((total_docs - df + 0.5f) / (df + 0.5f) + 1.0f); - - float tf_component = (term_freq * (BM25_K1 + 1.0f)) / - (term_freq + BM25_K1 * (1.0f - BM25_B + BM25_B * (doc_len / avg_doc_len))); - - score += idf * tf_component; - } - - return score; -} - -std::string retrieve_rag_context(CactusModelHandle* handle, const std::string& query) { - if (!handle->corpus_index || handle->corpus_embedding_dim == 0) { - return ""; - } - - auto* tokenizer = handle->model->get_tokenizer(); - if (!tokenizer) return ""; - - std::vector query_tokens = tokenizer->encode(query); - if (query_tokens.empty()) return ""; - - std::vector query_embedding = handle->model->get_embeddings(query_tokens, true, true); - if (query_embedding.size() != handle->corpus_embedding_dim) { - CACTUS_LOG_WARN("rag", "Query embedding dimension mismatch"); - return ""; - } - - index::QueryOptions options; - options.top_k = RAG_CANDIDATE_K; - options.score_threshold = 0.0f; - - std::vector> query_embeddings = {query_embedding}; - - try { - auto results = handle->corpus_index->query(query_embeddings, options); - if (results.empty() || results[0].empty()) { - return ""; - } - - std::vector doc_ids; - std::vector embedding_scores; - for (const auto& result : results[0]) { - doc_ids.push_back(result.doc_id); - embedding_scores.push_back(result.score); - } - - auto docs = handle->corpus_index->get_documents(doc_ids); - if (docs.empty()) { - return ""; - } - - auto query_words = tokenize_words(query); - - float total_len = 0.0f; - std::unordered_map doc_freqs; - for (const auto& doc : docs) { - auto words = tokenize_words(doc.content); - total_len += words.size(); - std::unordered_set unique_words(words.begin(), words.end()); - for (const auto& w : unique_words) { - doc_freqs[w]++; - } - } - float avg_doc_len = total_len / docs.size(); - - std::vector> emb_ranked; - std::vector> bm25_ranked; - for (size_t i = 0; i < docs.size(); ++i) { - emb_ranked.emplace_back(embedding_scores[i], i); - float bm25 = compute_bm25_score( - query_words, docs[i].content, avg_doc_len, doc_freqs, docs.size() - ); - bm25_ranked.emplace_back(bm25, i); - } - - auto rrf_scored = compute_rrf_scores(emb_ranked, bm25_ranked, docs.size()); - - std::string context = "[Retrieved Context - Use ONLY this information to answer. If the answer is not in the context, say \"I don't have enough information to answer that.\"]\n"; - size_t count = std::min(RAG_TOP_K, rrf_scored.size()); - for (size_t i = 0; i < count; ++i) { - size_t idx = rrf_scored[i].second; - context += "---\n"; - context += docs[idx].content; - if (!docs[idx].metadata.empty()) { - context += "\n(Source: " + docs[idx].metadata + ")"; - } - context += "\n"; - } - context += "---\n\n"; - - CACTUS_LOG_DEBUG("rag", "Retrieved " << count << " RAG chunks (hybrid BM25+embedding)"); - return context; - - } catch (const std::exception& e) { - CACTUS_LOG_WARN("rag", "RAG retrieval failed: " << e.what()); - return ""; - } -} - -static std::string tool_to_text(const ToolFunction& tool) { - return tool.name + " " + tool.description; -} - -static float cosine_similarity(const std::vector& a, const std::vector& b) { - if (a.size() != b.size() || a.empty()) return 0.0f; - - float dot = 0.0f, norm_a = 0.0f, norm_b = 0.0f; - for (size_t i = 0; i < a.size(); ++i) { - dot += a[i] * b[i]; - norm_a += a[i] * a[i]; - norm_b += b[i] * b[i]; - } - - float denom = std::sqrt(norm_a) * std::sqrt(norm_b); - return denom > 0.0f ? dot / denom : 0.0f; -} - -std::vector select_relevant_tools( - CactusModelHandle* handle, - const std::string& query, - const std::vector& all_tools, - size_t top_k -) { - using cactus::ffi::ToolFunction; - if (!handle || all_tools.empty() || top_k == 0) return all_tools; - - // If we have fewer tools than top_k, just return all - if (all_tools.size() <= top_k) return all_tools; - - auto* tokenizer = handle->model->get_tokenizer(); - if (!tokenizer) { - CACTUS_LOG_WARN("tool_rag", "No tokenizer available, returning all tools"); - return all_tools; - } - - // Build tool texts and embeddings if not cached or tool set changed - bool need_recompute = (handle->tool_texts.size() != all_tools.size()); - if (!need_recompute) { - for (size_t i = 0; i < all_tools.size(); ++i) { - if (tool_to_text(all_tools[i]) != handle->tool_texts[i]) { - need_recompute = true; - break; - } - } - } - - if (need_recompute) { - CACTUS_LOG_DEBUG("tool_rag", "Computing embeddings for " << all_tools.size() << " tools"); - handle->tool_texts.clear(); - handle->tool_embeddings.clear(); - handle->tool_texts.reserve(all_tools.size()); - handle->tool_embeddings.reserve(all_tools.size()); - - for (const auto& tool : all_tools) { - std::string text = tool_to_text(tool); - handle->tool_texts.push_back(text); - - std::vector tokens = tokenizer->encode(text); - if (!tokens.empty()) { - std::vector emb = handle->model->get_embeddings(tokens, true, true); - handle->tool_embeddings.push_back(std::move(emb)); - } else { - handle->tool_embeddings.push_back({}); - } - } - } - - // Embed the query - std::vector query_tokens = tokenizer->encode(query); - if (query_tokens.empty()) { - CACTUS_LOG_WARN("tool_rag", "Empty query, returning all tools"); - return all_tools; - } - - std::vector query_embedding = handle->model->get_embeddings(query_tokens, true, true); - if (query_embedding.empty()) { - CACTUS_LOG_WARN("tool_rag", "Failed to get query embedding, returning all tools"); - return all_tools; - } - - auto query_words = tokenize_words(query); - - float total_len = 0.0f; - std::unordered_map doc_freqs; - for (const auto& text : handle->tool_texts) { - auto words = tokenize_words(text); - total_len += words.size(); - std::unordered_set unique_words(words.begin(), words.end()); - for (const auto& w : unique_words) { - doc_freqs[w]++; - } - } - float avg_doc_len = total_len / handle->tool_texts.size(); - - std::vector> emb_ranked; - for (size_t i = 0; i < handle->tool_embeddings.size(); ++i) { - float sim = cosine_similarity(query_embedding, handle->tool_embeddings[i]); - emb_ranked.emplace_back(sim, i); - } - - std::vector> bm25_ranked; - for (size_t i = 0; i < handle->tool_texts.size(); ++i) { - float bm25 = compute_bm25_score( - query_words, handle->tool_texts[i], avg_doc_len, doc_freqs, handle->tool_texts.size() - ); - bm25_ranked.emplace_back(bm25, i); - } - - auto rrf_scored = compute_rrf_scores(emb_ranked, bm25_ranked, all_tools.size()); - - // Select top-k tools - std::vector selected; - selected.reserve(top_k); - for (size_t i = 0; i < top_k && i < rrf_scored.size(); ++i) { - size_t idx = rrf_scored[i].second; - selected.push_back(all_tools[idx]); - CACTUS_LOG_DEBUG("tool_rag", "Selected tool: " << all_tools[idx].name << " (RRF: " << rrf_scored[i].first << ")"); - } - - CACTUS_LOG_INFO("tool_rag", "Selected " << selected.size() << " of " << all_tools.size() << " tools using hybrid ranking"); - return selected; -} - -extern "C" { - -int cactus_rag_query( - cactus_model_t model, - const char* query, - char* response_buffer, - size_t buffer_size, - size_t top_k -) { - if (!model || !query || !response_buffer || buffer_size == 0) { - return -1; - } - - auto* handle = static_cast(model); - - if (!handle->corpus_index || handle->corpus_embedding_dim == 0) { - std::strcpy(response_buffer, "{\"chunks\":[],\"error\":\"No corpus index loaded\"}"); - return 0; - } - - try { - auto* tokenizer = handle->model->get_tokenizer(); - if (!tokenizer) { - std::strcpy(response_buffer, "{\"chunks\":[],\"error\":\"No tokenizer\"}"); - return 0; - } - - std::vector query_tokens = tokenizer->encode(query); - if (query_tokens.empty()) { - std::strcpy(response_buffer, "{\"chunks\":[],\"error\":\"Empty query\"}"); - return 0; - } - - std::vector query_embedding = handle->model->get_embeddings(query_tokens, true, true); - if (query_embedding.size() != handle->corpus_embedding_dim) { - std::strcpy(response_buffer, "{\"chunks\":[],\"error\":\"Embedding dimension mismatch\"}"); - return 0; - } - - index::QueryOptions options; - options.top_k = RAG_CANDIDATE_K; - options.score_threshold = 0.0f; - - std::vector> query_embeddings = {query_embedding}; - auto results = handle->corpus_index->query(query_embeddings, options); - - if (results.empty() || results[0].empty()) { - std::strcpy(response_buffer, "{\"chunks\":[]}"); - return 0; - } - - std::vector doc_ids; - std::vector embedding_scores; - for (const auto& result : results[0]) { - doc_ids.push_back(result.doc_id); - embedding_scores.push_back(result.score); - } - - auto docs = handle->corpus_index->get_documents(doc_ids); - - auto query_words = tokenize_words(query); - - float total_len = 0.0f; - std::unordered_map doc_freqs; - for (const auto& doc : docs) { - auto words = tokenize_words(doc.content); - total_len += words.size(); - std::unordered_set unique_words(words.begin(), words.end()); - for (const auto& w : unique_words) { - doc_freqs[w]++; - } - } - float avg_doc_len = docs.size() > 0 ? total_len / docs.size() : 1.0f; - - std::vector> emb_ranked; - std::vector> bm25_ranked; - for (size_t i = 0; i < docs.size(); ++i) { - emb_ranked.emplace_back(embedding_scores[i], i); - float bm25 = compute_bm25_score( - query_words, docs[i].content, avg_doc_len, doc_freqs, docs.size() - ); - bm25_ranked.emplace_back(bm25, i); - } - - auto rrf_scored = compute_rrf_scores(emb_ranked, bm25_ranked, docs.size()); - - size_t result_count = std::min(top_k > 0 ? top_k : RAG_TOP_K, rrf_scored.size()); - - std::ostringstream oss; - oss << "{\"chunks\":["; - for (size_t i = 0; i < result_count; ++i) { - size_t idx = rrf_scored[i].second; - float final_score = rrf_scored[i].first; - - if (i > 0) oss << ","; - oss << "{\"score\":" << std::setprecision(4) << final_score - << ",\"source\":\"" << docs[idx].metadata << "\"" - << ",\"content\":\""; - for (char c : docs[idx].content) { - switch (c) { - case '"': oss << "\\\""; break; - case '\\': oss << "\\\\"; break; - case '\n': oss << "\\n"; break; - case '\r': oss << "\\r"; break; - case '\t': oss << "\\t"; break; - default: oss << c; - } - } - oss << "\"}"; - } - oss << "]}"; - - std::string result = oss.str(); - if (result.size() >= buffer_size) { - std::strcpy(response_buffer, "{\"chunks\":[],\"error\":\"Buffer too small\"}"); - return -1; - } - - std::strcpy(response_buffer, result.c_str()); - return static_cast(result.size()); - - } catch (const std::exception& e) { - std::string error = "{\"chunks\":[],\"error\":\"" + std::string(e.what()) + "\"}"; - if (error.size() < buffer_size) { - std::strcpy(response_buffer, error.c_str()); - } - return -1; - } -} - -} diff --git a/cactus/ffi/cactus_stream.cpp b/cactus/ffi/cactus_stream.cpp deleted file mode 100644 index 40dc6d6fa..000000000 --- a/cactus/ffi/cactus_stream.cpp +++ /dev/null @@ -1,2704 +0,0 @@ -#include "cactus_ffi.h" -#include "cactus_cloud.h" -#include "cactus_utils.h" -#include "telemetry/telemetry.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace cactus::ffi; -using cactus::audio::WHISPER_SAMPLE_RATE; -using cactus::audio::apply_preemphasis; -using cactus::audio::get_parakeet_spectrogram_config; -using cactus::audio::normalize_parakeet_log_mel; -using cactus::audio::trim_mel_frames; - -double json_number(const std::string& json, const std::string& key) { - std::string pattern = "\"" + key + "\":"; - size_t pos = json.find(pattern); - if (pos == std::string::npos) return 0.0; - size_t start = pos + pattern.size(); - while (start < json.size() && (json[start] == ' ' || json[start] == '\t')) ++start; - size_t end = start; - while (end < json.size() && std::string(",}] \t\n\r").find(json[end]) == std::string::npos) ++end; - try { return std::stod(json.substr(start, end - start)); } - catch (...) { return 0.0; } -} - -std::string json_string(const std::string& json, const std::string& key) { - std::string pattern = "\"" + key + "\":"; - size_t pos = json.find(pattern); - if (pos == std::string::npos) return {}; - size_t start = pos + pattern.size(); - - while (start < json.size() && (json[start] == ' ' || json[start] == '\t')) ++start; - if (start >= json.size() || json[start] != '"') return {}; - size_t q1 = start; - size_t q2 = json.find('"', q1 + 1); - if (q2 == std::string::npos) return {}; - return json.substr(q1 + 1, q2 - q1 - 1); -} - -inline std::string escape_json(const std::string& s) { - return escape_json_string(s); -} - -bool json_bool(const std::string& json, const std::string& key) { - std::string pattern = "\"" + key + "\":"; - size_t pos = json.find(pattern); - if (pos == std::string::npos) return false; - size_t start = pos + pattern.size(); - while (start < json.size() && (json[start] == ' ' || json[start] == '\t')) ++start; - if (start + 4 <= json.size() && json.substr(start, 4) == "true") return true; - return false; -} - -static std::string ensure_json_bool_option(const std::string& json, - const std::string& key, - bool value) { - const std::string field = "\"" + key + "\""; - if (json.find(field) != std::string::npos) return json; - - const std::string bool_str = value ? "true" : "false"; - if (json.empty()) { - return "{\"" + key + "\":" + bool_str + "}"; - } - - std::string out = json; - const size_t close = out.rfind('}'); - const size_t open = out.find('{'); - if (open == std::string::npos || close == std::string::npos || close <= open) { - return json; - } - - size_t content_start = open + 1; - while (content_start < close && std::isspace(static_cast(out[content_start]))) { - ++content_start; - } - const bool has_entries = content_start < close; - const std::string insertion = (has_entries ? "," : "") + field + ":" + bool_str; - out.insert(close, insertion); - return out; -} - -std::string json_array(const std::string& json, const std::string& key) { - std::string pattern = "\"" + key + "\":"; - size_t pos = json.find(pattern); - if (pos == std::string::npos) return "[]"; - size_t start = pos + pattern.size(); - while (start < json.size() && (json[start] == ' ' || json[start] == '\t')) ++start; - if (start >= json.size() || json[start] != '[') return "[]"; - int depth = 1; - size_t end = start + 1; - while (end < json.size() && depth > 0) { - if (json[end] == '[') depth++; - else if (json[end] == ']') depth--; - end++; - } - return json.substr(start, end - start); -} - -static std::string suppress_unwanted_text(const std::string& text) { - static const std::regex pattern(R"(\([^)]*\)|\[[^\]]*\]|\.\.\.)"); - std::string result = std::regex_replace(text, pattern, ""); - - size_t start = result.find_first_not_of(" \t\n\r"); - if (start == std::string::npos) return ""; - size_t end = result.find_last_not_of(" \t\n\r"); - return result.substr(start, end - start + 1); -} - -static bool is_pcm_chunk_silent(const uint8_t* pcm_buffer, size_t pcm_buffer_size, float rms_threshold) { - if (!pcm_buffer || pcm_buffer_size < sizeof(int16_t)) return true; - - const size_t sample_count = pcm_buffer_size / sizeof(int16_t); - const auto* samples = reinterpret_cast(pcm_buffer); - double sum_sq = 0.0; - for (size_t i = 0; i < sample_count; ++i) { - const double normalized = static_cast(samples[i]) / 32768.0; - sum_sq += normalized * normalized; - } - - const double rms = std::sqrt(sum_sq / static_cast(sample_count)); - return rms < static_cast(rms_threshold); -} - -static void parse_stream_transcribe_init_options(const std::string& json, - double& confirmation_threshold, - size_t& min_chunk_size, - bool& telemetry_enabled, - std::string& language) { - confirmation_threshold = 0.99; - min_chunk_size = 32000; - telemetry_enabled = true; - language = "en"; - - if (json.empty()) return; - - size_t pos = json.find("\"confirmation_threshold\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - confirmation_threshold = std::stod(json.substr(pos)); - } - - pos = json.find("\"min_chunk_size\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - min_chunk_size = static_cast(std::stod(json.substr(pos))); - } - - pos = json.find("\"telemetry_enabled\""); - if (pos != std::string::npos) { - telemetry_enabled = json_bool(json, "telemetry_enabled"); - } - - language = json_string(json, "language"); - if (language.empty()) language = "en"; -} - -static bool build_parakeet_features_from_pcm( - const uint8_t* pcm_buffer, - size_t pcm_buffer_size, - const cactus::engine::Model* model, - std::vector& out_features) { - if (!pcm_buffer || pcm_buffer_size < sizeof(int16_t) || !model) { - return false; - } - - std::vector waveform = cactus::audio::pcm_buffer_to_float_samples( - pcm_buffer, pcm_buffer_size); - if (waveform.empty()) { - return false; - } - - const auto cfg = get_parakeet_spectrogram_config(); - const size_t mel_bins = std::max( - 1, static_cast(model->get_config().num_mel_bins)); - cactus::engine::AudioProcessor processor; - processor.init_mel_filters( - cfg.n_fft / 2 + 1, - mel_bins, - 0.0f, - 8000.0f, - WHISPER_SAMPLE_RATE); - - const size_t waveform_samples = waveform.size(); - apply_preemphasis(waveform, 0.97f); - out_features = processor.compute_spectrogram(waveform, cfg); - normalize_parakeet_log_mel(out_features, mel_bins); - size_t valid_frames = waveform_samples / cfg.hop_length; - if (valid_frames == 0) valid_frames = 1; - trim_mel_frames(out_features, mel_bins, valid_frames); - return !out_features.empty(); -} - -static size_t parakeet_tdt_pcm_bytes_to_encoder_frames( - const cactus::engine::Model* model, - size_t pcm_bytes) { - if (!model) return 0; - const auto cfg = get_parakeet_spectrogram_config(); - const double sample_count = static_cast(pcm_bytes) / 2.0; - const double feature_frames = sample_count / static_cast(cfg.hop_length); - const double subsampling = std::max(1, model->get_config().subsampling_factor); - return static_cast(std::llround(feature_frames / subsampling)); -} - -struct CactusStreamTranscribeHandle { - CactusModelHandle* model_handle; - - struct CactusStreamTranscribeOptions { - double confirmation_threshold; - size_t min_chunk_size; - std::string language; - } options; - std::string transcribe_options_json; - bool has_custom_vocabulary_bias = false; - std::vector custom_vocabulary; - - std::vector audio_buffer; - std::string previous_parakeet_pending; - size_t previous_parakeet_audio_buffer_size = 0; - size_t previous_parakeet_pending_ticks = 0; - size_t parakeet_silence_run_bytes = 0; - size_t parakeet_speech_run_bytes = 0; - bool parakeet_resume_guard_active = true; - std::string parakeet_committed_text; - size_t parakeet_chunk_cursor_bytes = 0; - float parakeet_committed_until_sec = 0.0f; - bool parakeet_onset_active = false; - size_t parakeet_onset_start_bytes = 0; - bool parakeet_tdt_chunked_stream = false; - struct ParakeetTdtDecodeContext { - std::vector audio_samples; - size_t samples_decoded_up_to = 0; - float audio_time_offset_sec = 0.0f; - cactus::engine::ParakeetTDTModel::ChunkStreamState decoder_state; - cactus::engine::AudioProcessor audio_processor; - size_t mel_bins = 0; - bool initialized = false; - } parakeet_tdt_decode_context; - bool parakeet_tdt_chunked_blocked_until_pause = false; - cactus::engine::ParakeetTDTModel::ChunkStreamState parakeet_tdt_decoder_state; - std::string parakeet_tdt_pending_raw; - struct ParakeetTdtResumeCheckpoint { - size_t end_bytes = 0; - cactus::engine::ParakeetTDTModel::ChunkStreamState state; - }; - std::deque parakeet_tdt_resume_checkpoints; - - std::vector previous_segments; - bool previous_cloud_handoff = false; - float stream_audio_offset_sec = 0.0f; - uint64_t next_cloud_job_id = 1; - - struct CloudJob { - uint64_t id; - std::future result; - }; - std::vector pending_cloud_jobs; - std::vector> completed_cloud_results; - - char transcribe_response_buffer[65536]; - - std::chrono::steady_clock::time_point stream_start; - bool stream_first_token_seen; - double stream_first_token_ms; - int stream_total_tokens; - - std::chrono::steady_clock::time_point stream_session_start; - bool stream_session_first_token_seen; - double stream_session_first_token_ms; - int stream_cumulative_tokens; - double last_stream_decode_tps = 0.0; - double last_stream_decode_tokens = 0.0; - double last_stream_total_time_ms = 0.0; - double last_stream_raw_decoder_tps = 0.0; -}; - -static std::vector parse_segments(const std::string& transcribe_json); - -struct StreamWindowDecodeResult { - std::string raw_json; - std::string response; - std::vector segments; - double decode_tokens = 0.0; - bool cloud_handoff = false; -}; - -static double pcm_bytes_to_seconds(size_t pcm_bytes) { - return static_cast(pcm_bytes) / (16000.0 * 2.0); -} - -static size_t seconds_to_pcm_bytes(double seconds) { - if (seconds <= 0.0) return 0; - const size_t raw = static_cast(std::llround(seconds * 16000.0 * 2.0)); - return raw - (raw % sizeof(int16_t)); -} - -static std::string join_segments_by_end_time( - const std::vector& segments, - float offset_sec, - float min_end_exclusive_sec, - float max_end_inclusive_sec, - float* out_max_emitted_end_sec = nullptr) { - std::string joined; - float max_end_sec = min_end_exclusive_sec; - for (const auto& seg : segments) { - const float abs_end_sec = offset_sec + seg.end; - if (abs_end_sec <= min_end_exclusive_sec || abs_end_sec > max_end_inclusive_sec) { - continue; - } - if (!joined.empty()) joined += ' '; - joined += seg.text; - if (abs_end_sec > max_end_sec) { - max_end_sec = abs_end_sec; - } - } - - if (out_max_emitted_end_sec) { - *out_max_emitted_end_sec = max_end_sec; - } - return suppress_unwanted_text(joined); -} - -static bool run_stream_window_transcribe( - CactusStreamTranscribeHandle* handle, - const char* transcribe_prompt, - const std::string& effective_transcribe_options_json, - const uint8_t* decode_pcm, - size_t decode_pcm_size, - StreamWindowDecodeResult& out) { - if (!handle || !handle->model_handle || !decode_pcm || decode_pcm_size == 0) { - return false; - } - - const auto model_type = handle->model_handle->model->get_config().model_type; - const bool use_model_stream_mode = - model_type != cactus::engine::Config::ModelType::PARAKEET_TDT; - cactus::telemetry::setStreamMode(use_model_stream_mode); - const int result = cactus_transcribe( - handle->model_handle, - nullptr, - transcribe_prompt, - handle->transcribe_response_buffer, - sizeof(handle->transcribe_response_buffer), - effective_transcribe_options_json.empty() ? nullptr : effective_transcribe_options_json.c_str(), - nullptr, - nullptr, - decode_pcm, - decode_pcm_size); - cactus::telemetry::setStreamMode(false); - - if (result < 0) { - return false; - } - - out.raw_json.assign(handle->transcribe_response_buffer); - out.response = suppress_unwanted_text(json_string(out.raw_json, "response")); - out.segments = parse_segments(out.raw_json); - out.decode_tokens = std::max(0.0, json_number(out.raw_json, "decode_tokens")); - out.cloud_handoff = json_bool(out.raw_json, "cloud_handoff"); - handle->last_stream_decode_tps = std::max(0.0, json_number(out.raw_json, "decode_tps")); - handle->last_stream_decode_tokens = out.decode_tokens; - handle->last_stream_total_time_ms = std::max(0.0, json_number(out.raw_json, "total_time_ms")); - handle->last_stream_raw_decoder_tps = std::max(0.0, json_number(out.raw_json, "raw_decoder_tps")); - return true; -} - -static bool run_parakeet_tdt_chunk_decode( - CactusStreamTranscribeHandle* handle, - size_t window_start_bytes, - size_t window_end_bytes, - size_t chunk_start_bytes, - size_t chunk_end_bytes, - cactus::engine::ParakeetTDTModel::ChunkStreamResult& out, - cactus::engine::ParakeetTDTModel::ChunkStreamState& out_state) { - if (!handle || !handle->model_handle || !handle->model_handle->model) { - return false; - } - if (chunk_end_bytes <= chunk_start_bytes || - window_end_bytes <= window_start_bytes || - window_end_bytes > handle->audio_buffer.size() || - chunk_end_bytes > window_end_bytes || - window_start_bytes > chunk_start_bytes) { - return false; - } - - auto* model = static_cast( - handle->model_handle->model.get()); - size_t replay_start_bytes = window_start_bytes; - cactus::engine::ParakeetTDTModel::ChunkStreamState base_state; - bool found_checkpoint = false; - for (auto it = handle->parakeet_tdt_resume_checkpoints.rbegin(); - it != handle->parakeet_tdt_resume_checkpoints.rend(); - ++it) { - if (it->end_bytes <= window_start_bytes) { - replay_start_bytes = std::max(window_start_bytes, it->end_bytes); - base_state = it->state; - found_checkpoint = true; - break; - } - } - if (!found_checkpoint) { - for (auto it = handle->parakeet_tdt_resume_checkpoints.rbegin(); - it != handle->parakeet_tdt_resume_checkpoints.rend(); - ++it) { - if (it->end_bytes <= chunk_start_bytes) { - replay_start_bytes = std::max(window_start_bytes, it->end_bytes); - base_state = it->state; - found_checkpoint = true; - break; - } - } - } - if (!found_checkpoint) { - replay_start_bytes = window_start_bytes; - base_state = {}; - } - - std::vector audio_features; - if (!build_parakeet_features_from_pcm( - handle->audio_buffer.data() + window_start_bytes, - window_end_bytes - window_start_bytes, - model, - audio_features)) { - return false; - } - - const size_t replay_start_frame = parakeet_tdt_pcm_bytes_to_encoder_frames( - model, replay_start_bytes - window_start_bytes); - const size_t start_frame = parakeet_tdt_pcm_bytes_to_encoder_frames( - model, chunk_start_bytes - window_start_bytes); - size_t end_frame = parakeet_tdt_pcm_bytes_to_encoder_frames( - model, chunk_end_bytes - window_start_bytes); - if (end_frame <= start_frame) { - end_frame = start_frame + 1; - } - - const auto decode_start = std::chrono::steady_clock::now(); - std::lock_guard lock(handle->model_handle->model_mutex); - cactus::telemetry::setStreamMode(true); - try { - out_state = base_state; - out = model->decode_chunk_stream( - audio_features, - replay_start_frame, - start_frame, - end_frame, - out_state); - } catch (...) { - cactus::telemetry::setStreamMode(false); - throw; - } - cactus::telemetry::setStreamMode(false); - const auto decode_end = std::chrono::steady_clock::now(); - const double decode_ms = std::chrono::duration_cast( - decode_end - decode_start).count() / 1000.0; - handle->last_stream_total_time_ms = decode_ms; - handle->last_stream_decode_tokens = static_cast(out.token_count); - handle->last_stream_decode_tps = - (out.token_count > 1 && decode_ms > 0.0) - ? ((static_cast(out.token_count) - 1.0) * 1000.0) / decode_ms - : 0.0; - handle->last_stream_raw_decoder_tps = out.raw_decoder_tps; - return true; -} - -static void reset_parakeet_tdt_chunk_stream(CactusStreamTranscribeHandle* handle) { - if (!handle) return; - handle->parakeet_tdt_decoder_state = {}; - handle->parakeet_tdt_pending_raw.clear(); - handle->parakeet_tdt_resume_checkpoints.clear(); -} - -static std::vector> collect_parakeet_word_spans(const std::string& text); - -static std::string strip_unwanted_text_preserving_edges(const std::string& text) { - static const std::regex pattern(R"(\([^)]*\)|\[[^\]]*\]|\.\.\.)"); - return std::regex_replace(text, pattern, ""); -} - -static void parakeet_append_monotonic_text(std::string& dst, const std::string& piece) { - const std::string normalized = suppress_unwanted_text(piece); - if (normalized.empty()) return; - if (dst.empty()) { - dst = normalized; - return; - } - - const unsigned char first = static_cast(normalized.front()); - const bool attach_without_space = - std::ispunct(first) && normalized.front() != '"' && - normalized.front() != '\'' && normalized.front() != '('; - if (!std::isspace(static_cast(dst.back())) && !attach_without_space) { - dst += " "; - } - dst += normalized; -} - -static std::vector parse_segments(const std::string& transcribe_json) { - std::vector segs; - std::string arr = json_array(transcribe_json, "segments"); - size_t i = 1; - while (i < arr.size() - 1) { - while (i < arr.size() && (arr[i] == ',' || std::isspace((unsigned char)arr[i]))) ++i; - if (i >= arr.size() - 1 || arr[i] != '{') break; - int depth = 1; - size_t obj_start = i++; - while (i < arr.size() && depth > 0) { - if (arr[i] == '{') ++depth; - else if (arr[i] == '}') --depth; - ++i; - } - std::string obj = arr.substr(obj_start, i - obj_start); - segs.emplace_back( - static_cast(json_number(obj, "start")), - static_cast(json_number(obj, "end")), - json_string(obj, "text") - ); - } - return segs; -} - -static bool is_parakeet_word_char(unsigned char ch) { - return std::isalnum(ch) || ch == '\''; -} - -static std::vector> collect_parakeet_word_spans(const std::string& text) { - std::vector> spans; - bool in_word = false; - size_t word_start = 0; - - for (size_t i = 0; i < text.size(); ++i) { - const unsigned char ch = static_cast(text[i]); - if (is_parakeet_word_char(ch)) { - if (!in_word) { - in_word = true; - word_start = i; - } - } else if (in_word) { - spans.emplace_back(word_start, i); - in_word = false; - } - } - - if (in_word) { - spans.emplace_back(word_start, text.size()); - } - return spans; -} - -static std::string parakeet_word_from_span( - const std::string& text, - const std::pair& span) { - if (span.second <= span.first || span.second > text.size()) return ""; - return text.substr(span.first, span.second - span.first); -} - -static bool parakeet_words_fuzzy_equal(const std::string& a, const std::string& b) { - if (a.empty() || b.empty()) return false; - const size_t dist = levenshtein_ci(a, b); - if (dist == 0) return true; - const size_t max_len = std::max(a.size(), b.size()); - return max_len >= 5 && dist <= 1; -} - -static size_t parakeet_common_prefix_words(const std::string& left, const std::string& right) { - const auto left_spans = collect_parakeet_word_spans(left); - const auto right_spans = collect_parakeet_word_spans(right); - const size_t max_words = std::min(left_spans.size(), right_spans.size()); - size_t common = 0; - for (; common < max_words; ++common) { - const std::string left_word = parakeet_word_from_span(left, left_spans[common]); - const std::string right_word = parakeet_word_from_span(right, right_spans[common]); - if (!parakeet_words_fuzzy_equal(left_word, right_word)) { - break; - } - } - return common; -} - -static std::string parakeet_take_first_words(const std::string& text, size_t words) { - if (text.empty() || words == 0) return ""; - const auto spans = collect_parakeet_word_spans(text); - if (spans.empty()) return ""; - if (words >= spans.size()) return suppress_unwanted_text(text); - const size_t end_idx = spans[words].first; - return suppress_unwanted_text(text.substr(0, end_idx)); -} - -static std::string parakeet_drop_first_words(const std::string& text, size_t words) { - if (text.empty()) return ""; - const auto spans = collect_parakeet_word_spans(text); - if (spans.empty()) return suppress_unwanted_text(text); - if (words >= spans.size()) return ""; - const size_t start_idx = spans[words].first; - return suppress_unwanted_text(text.substr(start_idx)); -} - -static size_t parakeet_word_count(const std::string& text) { - return collect_parakeet_word_spans(text).size(); -} - -static size_t parakeet_overlap_suffix_prefix_words( - const std::string& left, - const std::string& right, - size_t max_words = 64) { - const auto left_spans = collect_parakeet_word_spans(left); - const auto right_spans = collect_parakeet_word_spans(right); - if (left_spans.empty() || right_spans.empty()) return 0; - - const size_t max_overlap = std::min({left_spans.size(), right_spans.size(), max_words}); - size_t best = 0; - for (size_t k = 1; k <= max_overlap; ++k) { - bool matched = true; - for (size_t i = 0; i < k; ++i) { - const auto& left_span = left_spans[left_spans.size() - k + i]; - const auto& right_span = right_spans[i]; - const std::string left_word = parakeet_word_from_span(left, left_span); - const std::string right_word = parakeet_word_from_span(right, right_span); - if (!parakeet_words_fuzzy_equal(left_word, right_word)) { - matched = false; - break; - } - } - if (matched) best = k; - } - return best; -} - -static std::string parakeet_strip_recent_aligned_overlap( - const std::string& committed_text, - const std::string& hypothesis, - size_t max_recent_words = 24) { - const std::string candidate = suppress_unwanted_text(hypothesis); - if (candidate.empty() || committed_text.empty()) return candidate; - - const auto committed_spans = collect_parakeet_word_spans(committed_text); - if (committed_spans.empty()) return candidate; - - const size_t recent_words = std::min(committed_spans.size(), max_recent_words); - const size_t recent_start = committed_spans[committed_spans.size() - recent_words].first; - const std::string recent_tail = suppress_unwanted_text(committed_text.substr(recent_start)); - const auto recent_spans = collect_parakeet_word_spans(recent_tail); - const auto candidate_spans = collect_parakeet_word_spans(candidate); - const size_t recent_count = recent_spans.size(); - const size_t candidate_count = candidate_spans.size(); - if (recent_count == 0 || candidate_count == 0) return candidate; - - size_t best_run = 0; - size_t best_candidate_start = candidate_count; - size_t best_candidate_end = 0; - size_t best_recent_start = recent_count; - - for (size_t i = 0; i < recent_count; ++i) { - for (size_t j = 0; j < candidate_count; ++j) { - size_t run = 0; - while (i + run < recent_count && j + run < candidate_count) { - const std::string recent_word = parakeet_word_from_span( - recent_tail, recent_spans[i + run]); - const std::string candidate_word = parakeet_word_from_span( - candidate, candidate_spans[j + run]); - if (!parakeet_words_fuzzy_equal(recent_word, candidate_word)) { - break; - } - ++run; - } - - const size_t candidate_end = j + run; - const bool better_run = run > best_run; - const bool better_end = run == best_run && candidate_end > best_candidate_end; - const bool better_start = run == best_run && - candidate_end == best_candidate_end && - j < best_candidate_start; - if (better_run || better_end || better_start) { - best_run = run; - best_candidate_start = j; - best_candidate_end = candidate_end; - best_recent_start = i; - } - } - } - - if (best_run == 0 || best_candidate_end == 0) return candidate; - - const size_t min_overlap_words = candidate_count <= 5 ? 2 : 3; - if (best_run < min_overlap_words) return candidate; - - if (best_candidate_start > 1) return candidate; - if (best_candidate_start == 1) { - const std::string lead_word = parakeet_word_from_span(candidate, candidate_spans[0]); - const std::string overlap_word = parakeet_word_from_span(candidate, candidate_spans[1]); - const std::string recent_word = parakeet_word_from_span( - recent_tail, recent_spans[best_recent_start]); - if (!parakeet_words_fuzzy_equal(lead_word, overlap_word) && - !parakeet_words_fuzzy_equal(lead_word, recent_word)) { - return candidate; - } - } - - return parakeet_drop_first_words(candidate, best_candidate_end); -} - -static std::string parakeet_strip_recent_committed_prefix( - const std::string& committed_text, - const std::string& hypothesis); - -static std::string parakeet_emit_delta( - const std::string& committed_text, - const std::string& newly_confirmed) { - std::string candidate = parakeet_strip_recent_committed_prefix( - committed_text, newly_confirmed); - if (candidate.empty()) return ""; - if (committed_text.empty()) return candidate; - - const size_t overlap_words = parakeet_overlap_suffix_prefix_words(committed_text, candidate); - if (overlap_words > 0) { - return parakeet_drop_first_words(candidate, overlap_words); - } - - return candidate; -} - -static std::string parakeet_strip_recent_committed_prefix( - const std::string& committed_text, - const std::string& hypothesis) { - const std::string candidate = suppress_unwanted_text(hypothesis); - if (candidate.empty() || committed_text.empty()) return candidate; - - const auto committed_spans = collect_parakeet_word_spans(committed_text); - if (committed_spans.empty()) return candidate; - - const size_t recent_words = std::min(committed_spans.size(), 24); - const size_t recent_start = committed_spans[committed_spans.size() - recent_words].first; - const std::string recent_tail = suppress_unwanted_text(committed_text.substr(recent_start)); - const size_t candidate_words = parakeet_word_count(candidate); - const size_t overlap_words = parakeet_overlap_suffix_prefix_words(recent_tail, candidate, recent_words); - - // Be conservative for tiny hypotheses, but strip even a single repeated - // boundary word when the new hypothesis is a longer continuation. - const size_t required_overlap = candidate_words >= 5 ? 1 : 2; - if (overlap_words >= required_overlap) { - return parakeet_drop_first_words(candidate, overlap_words); - } - - const std::string aligned_trimmed = parakeet_strip_recent_aligned_overlap( - committed_text, candidate, recent_words); - if (aligned_trimmed != candidate) { - return aligned_trimmed; - } - - const auto recent_spans = collect_parakeet_word_spans(recent_tail); - const auto candidate_spans = collect_parakeet_word_spans(candidate); - const size_t recent_count = recent_spans.size(); - const size_t candidate_count = candidate_spans.size(); - if (recent_count == 0 || candidate_count < 4) return candidate; - - std::vector> dp( - recent_count + 1, std::vector(candidate_count + 1, 0)); - for (size_t i = 1; i <= recent_count; ++i) { - for (size_t j = 1; j <= candidate_count; ++j) { - const std::string recent_word = parakeet_word_from_span(recent_tail, recent_spans[i - 1]); - const std::string candidate_word = parakeet_word_from_span(candidate, candidate_spans[j - 1]); - if (parakeet_words_fuzzy_equal(recent_word, candidate_word)) { - dp[i][j] = dp[i - 1][j - 1] + 1; - } else { - dp[i][j] = std::max(dp[i - 1][j], dp[i][j - 1]); - } - } - } - - const size_t lcs_words = dp[recent_count][candidate_count]; - if (lcs_words < 3 || lcs_words * 2 < candidate_count) return candidate; - - size_t i = recent_count; - size_t j = candidate_count; - size_t last_matched_candidate_word = 0; - bool found_match = false; - bool captured_last_match = false; - while (i > 0 && j > 0) { - const std::string recent_word = parakeet_word_from_span(recent_tail, recent_spans[i - 1]); - const std::string candidate_word = parakeet_word_from_span(candidate, candidate_spans[j - 1]); - if (parakeet_words_fuzzy_equal(recent_word, candidate_word)) { - if (!captured_last_match) { - last_matched_candidate_word = j; - captured_last_match = true; - } - found_match = true; - --i; - --j; - } else if (dp[i - 1][j] >= dp[i][j - 1]) { - --i; - } else { - --j; - } - } - - if (!found_match || last_matched_candidate_word >= candidate_count) return candidate; - return parakeet_drop_first_words(candidate, last_matched_candidate_word); - - return candidate; -} - -static std::string serialize_segments_with_offset( - const std::vector& segs, float offset_sec) { - std::ostringstream out; - out << "["; - for (size_t i = 0; i < segs.size(); ++i) { - if (i > 0) out << ","; - out << std::fixed << std::setprecision(3) - << "{\"start\":" << (segs[i].start + offset_sec) - << ",\"end\":" << (segs[i].end + offset_sec) - << ",\"text\":\"" << escape_json(segs[i].text) << "\"}"; - } - out << "]"; - return out.str(); -} - -static std::string build_stream_response( - const std::string& raw_json_str, - const std::string& error_msg, - const std::string& confirmed, - const std::string& pending, - const std::string& segments_json, - bool cloud_handoff, - double buffer_duration_ms, - uint64_t cloud_job_id, - uint64_t cloud_result_job_id, - const CloudResponse& cloud_result, - double override_time_to_first_token_ms = -1.0, - double override_total_time_ms = -1.0, - double override_decode_tps = -1.0, - double override_decode_tokens = -1.0, - double override_total_tokens = -1.0, - double override_raw_decoder_tps = -1.0 -) { - std::string function_calls = json_array(raw_json_str, "function_calls"); - double confidence = json_number(raw_json_str, "confidence"); - double time_to_first_token_ms = json_number(raw_json_str, "time_to_first_token_ms"); - double total_time_ms = json_number(raw_json_str, "total_time_ms"); - double prefill_tps = json_number(raw_json_str, "prefill_tps"); - double decode_tps = json_number(raw_json_str, "decode_tps"); - double ram_usage_mb = json_number(raw_json_str, "ram_usage_mb"); - double prefill_tokens = json_number(raw_json_str, "prefill_tokens"); - double decode_tokens = json_number(raw_json_str, "decode_tokens"); - double total_tokens = json_number(raw_json_str, "total_tokens"); - double raw_decoder_tps = json_number(raw_json_str, "raw_decoder_tps"); - if (override_time_to_first_token_ms >= 0.0) { - time_to_first_token_ms = override_time_to_first_token_ms; - } - if (override_total_time_ms >= 0.0) { - total_time_ms = override_total_time_ms; - } - if (override_decode_tps >= 0.0) { - decode_tps = override_decode_tps; - } - if (override_decode_tokens >= 0.0) { - decode_tokens = override_decode_tokens; - } - if (override_total_tokens >= 0.0) { - total_tokens = override_total_tokens; - } - if (override_raw_decoder_tps >= 0.0) { - raw_decoder_tps = override_raw_decoder_tps; - } - std::string effective_confirmed = confirmed; - if (cloud_result.used_cloud && !cloud_result.transcript.empty()) { - effective_confirmed = cloud_result.transcript; - } - - std::ostringstream json_builder; - json_builder << "{"; - json_builder << "\"success\":true,"; - json_builder << "\"buffer_duration_ms\":" << buffer_duration_ms << ","; - json_builder << "\"error\":" << (error_msg.empty() ? "null" : "\"" + escape_json(error_msg) + "\"") << ","; - json_builder << "\"cloud_handoff\":" << (cloud_handoff ? "true" : "false") << ","; - json_builder << "\"cloud_job_id\":" << cloud_job_id << ","; - json_builder << "\"cloud_result_job_id\":" << cloud_result_job_id << ","; - json_builder << "\"cloud_result\":\"" << escape_json(cloud_result.transcript) << "\","; - json_builder << "\"cloud_result_used_cloud\":" << (cloud_result.used_cloud ? "true" : "false") << ","; - json_builder << "\"cloud_result_error\":"; - if (cloud_result.error.empty()) { - json_builder << "null,"; - } else { - json_builder << "\"" << escape_json(cloud_result.error) << "\","; - } - json_builder << "\"cloud_result_source\":\"" << (cloud_result.used_cloud ? "cloud" : "fallback") << "\","; - json_builder << "\"confirmed_local\":\"" << escape_json(confirmed) << "\","; - json_builder << "\"confirmed\":\"" << escape_json(effective_confirmed) << "\","; - json_builder << "\"pending\":\"" << escape_json(pending) << "\","; - json_builder << "\"segments\":" << segments_json << ","; - json_builder << "\"function_calls\":" << function_calls << ","; - json_builder << "\"confidence\":" << confidence << ","; - json_builder << "\"time_to_first_token_ms\":" << time_to_first_token_ms << ","; - json_builder << "\"total_time_ms\":" << total_time_ms << ","; - json_builder << "\"prefill_tps\":" << prefill_tps << ","; - json_builder << "\"decode_tps\":" << decode_tps << ","; - json_builder << "\"ram_usage_mb\":" << ram_usage_mb << ","; - json_builder << "\"prefill_tokens\":" << prefill_tokens << ","; - json_builder << "\"decode_tokens\":" << decode_tokens << ","; - json_builder << "\"total_tokens\":" << total_tokens << ","; - json_builder << "\"raw_decoder_tps\":" << raw_decoder_tps; - json_builder << "}"; - return json_builder.str(); -} - -extern "C" { - -cactus_stream_transcribe_t cactus_stream_transcribe_start(cactus_model_t model, const char* options_json) { - if (!model) { - last_error_message = "Model not initialized. Check model path and files."; - CACTUS_LOG_ERROR("stream_transcribe_start", last_error_message); - return nullptr; - } - - try { - auto* model_handle = static_cast(model); - if (!model_handle->model) { - last_error_message = "Invalid model handle."; - CACTUS_LOG_ERROR("stream_transcribe_start", last_error_message); - return nullptr; - } - - auto* stream_handle = new CactusStreamTranscribeHandle(); - stream_handle->model_handle = model_handle; - stream_handle->transcribe_response_buffer[0] = '\0'; - - auto session_start_time = std::chrono::steady_clock::now(); - stream_handle->stream_start = session_start_time; - stream_handle->stream_first_token_seen = false; - stream_handle->stream_first_token_ms = 0.0; - stream_handle->stream_total_tokens = 0; - - stream_handle->stream_session_start = session_start_time; - stream_handle->stream_session_first_token_seen = false; - stream_handle->stream_session_first_token_ms = 0.0; - stream_handle->stream_cumulative_tokens = 0; - - double confirmation_threshold; - size_t min_chunk_size; - bool telemetry_enabled; - std::string language; - parse_stream_transcribe_init_options( - options_json ? options_json : "", - confirmation_threshold, - min_chunk_size, - telemetry_enabled, - language - ); - - stream_handle->options = { confirmation_threshold, min_chunk_size, language }; - stream_handle->transcribe_options_json = options_json ? options_json : ""; - stream_handle->parakeet_tdt_chunked_stream = - model_handle->model->get_config().model_type == - cactus::engine::Config::ModelType::PARAKEET_TDT; - { - float vocabulary_boost = 5.0f; - parse_custom_vocabulary_options(stream_handle->transcribe_options_json, - stream_handle->custom_vocabulary, vocabulary_boost); - auto vocab_bias = build_custom_vocabulary_bias( - model_handle->model->get_tokenizer(), - stream_handle->custom_vocabulary, - vocabulary_boost - ); - stream_handle->has_custom_vocabulary_bias = !vocab_bias.empty(); - if (stream_handle->has_custom_vocabulary_bias) { - model_handle->model->set_vocab_bias(vocab_bias); - } - } - - if (stream_handle->parakeet_tdt_chunked_stream) { - auto& ctx = stream_handle->parakeet_tdt_decode_context; - ctx.mel_bins = std::max( - 1, static_cast(model_handle->model->get_config().num_mel_bins)); - auto cfg = get_parakeet_spectrogram_config(); - ctx.audio_processor.init_mel_filters( - cfg.n_fft / 2 + 1, - ctx.mel_bins, - 0.0f, - 8000.0f, - WHISPER_SAMPLE_RATE); - ctx.initialized = true; - } - - CACTUS_LOG_INFO("stream_transcribe_start", - "Stream transcription initialized for model: " << model_handle->model_name); - - return stream_handle; - } catch (const std::exception& e) { - last_error_message = "Exception during stream_transcribe_start: " + std::string(e.what()); - CACTUS_LOG_ERROR("stream_transcribe_start", last_error_message); - return nullptr; - } catch (...) { - last_error_message = "Unknown exception during stream transcription initialization"; - CACTUS_LOG_ERROR("stream_transcribe_start", last_error_message); - return nullptr; - } -} - -int cactus_stream_transcribe_process( - cactus_stream_transcribe_t stream, - const uint8_t* pcm_buffer, - size_t pcm_buffer_size, - char* response_buffer, - size_t buffer_size -) { - if (!stream) { - last_error_message = "Stream not initialized."; - CACTUS_LOG_ERROR("stream_transcribe_process", last_error_message); - cactus::telemetry::recordStreamTranscription(nullptr, false, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0, last_error_message.c_str()); - return -1; - } - - if (!pcm_buffer || pcm_buffer_size == 0) { - last_error_message = "Invalid parameters: pcm_buffer or pcm_buffer_size"; - CACTUS_LOG_ERROR("stream_transcribe_process", last_error_message); - cactus::telemetry::recordStreamTranscription(nullptr, false, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0, last_error_message.c_str()); - return -1; - } - - if (!response_buffer || buffer_size == 0) { - last_error_message = "Invalid parameters: response_buffer or buffer_size"; - CACTUS_LOG_ERROR("stream_transcribe_process", last_error_message); - cactus::telemetry::recordStreamTranscription(nullptr, false, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0, last_error_message.c_str()); - return -1; - } - - try { - auto* handle = static_cast(stream); - const auto model_type = handle->model_handle->model->get_config().model_type; - bool is_moonshine = model_type == cactus::engine::Config::ModelType::MOONSHINE; - bool is_parakeet_tdt = - model_type == cactus::engine::Config::ModelType::PARAKEET_TDT; - bool is_parakeet_ctc = - model_type == cactus::engine::Config::ModelType::PARAKEET; - bool is_parakeet = - is_parakeet_ctc || is_parakeet_tdt; - bool is_gemma4 = model_type == cactus::engine::Config::ModelType::GEMMA4; - - if (is_parakeet_tdt && - handle->parakeet_tdt_chunked_stream && - handle->parakeet_tdt_decode_context.initialized) { - auto& ctx = handle->parakeet_tdt_decode_context; - auto* tdt_model = static_cast( - handle->model_handle->model.get()); - auto new_samples = cactus::audio::pcm_buffer_to_float_samples( - pcm_buffer, pcm_buffer_size); - ctx.audio_samples.insert(ctx.audio_samples.end(), new_samples.begin(), new_samples.end()); - - constexpr size_t kTdtLeftContextSamples = 8 * 16000; - constexpr size_t kTdtRightContextSamples = 16000; - constexpr size_t kTdtChunkSamples = 16000; - constexpr size_t kTdtColdStartSamples = 6 * 16000; - - const size_t total_samples = ctx.audio_samples.size(); - const size_t decodable_up_to = total_samples > kTdtRightContextSamples - ? total_samples - kTdtRightContextSamples - : 0; - const size_t min_chunk_samples = ctx.samples_decoded_up_to == 0 - ? kTdtColdStartSamples - : kTdtChunkSamples; - - if (decodable_up_to <= ctx.samples_decoded_up_to || - decodable_up_to - ctx.samples_decoded_up_to < min_chunk_samples) { - const std::string json_response = build_stream_response( - "{}", - "", - "", - handle->previous_parakeet_pending, - "[]", - false, - 0.0, - 0, - 0, - CloudResponse{}, - handle->stream_first_token_seen ? handle->stream_first_token_ms : 0.0, - handle->last_stream_total_time_ms, - handle->last_stream_decode_tps, - handle->last_stream_decode_tokens, - handle->last_stream_decode_tokens, - handle->last_stream_raw_decoder_tps); - if (json_response.length() >= buffer_size) { - handle_error_response("Response buffer too small", response_buffer, buffer_size); - return -1; - } - std::strcpy(response_buffer, json_response.c_str()); - return static_cast(json_response.length()); - } - - const size_t window_start_sample = ctx.samples_decoded_up_to > kTdtLeftContextSamples - ? ctx.samples_decoded_up_to - kTdtLeftContextSamples - : 0; - const size_t window_end_sample = std::min( - total_samples, decodable_up_to + kTdtRightContextSamples); - std::vector window_audio( - ctx.audio_samples.begin() + window_start_sample, - ctx.audio_samples.begin() + window_end_sample); - - auto cfg = get_parakeet_spectrogram_config(); - const size_t waveform_samples = window_audio.size(); - apply_preemphasis(window_audio, 0.97f); - window_audio = ctx.audio_processor.compute_spectrogram(window_audio, cfg); - normalize_parakeet_log_mel(window_audio, ctx.mel_bins); - size_t valid_frames = waveform_samples / cfg.hop_length; - if (valid_frames == 0) valid_frames = 1; - trim_mel_frames(window_audio, ctx.mel_bins, valid_frames); - - const uint32_t subsampling = - std::max(1, handle->model_handle->model->get_config().subsampling_factor); - const size_t samples_per_enc_frame = cfg.hop_length * subsampling; - const size_t context_samples = ctx.samples_decoded_up_to - window_start_sample; - const size_t decode_start_frame = context_samples / samples_per_enc_frame; - const size_t new_samples_count = decodable_up_to - ctx.samples_decoded_up_to; - const size_t decode_end_frame = - decode_start_frame + (new_samples_count / samples_per_enc_frame); - - if (decode_end_frame <= decode_start_frame) { - const std::string json_response = build_stream_response( - "{}", - "", - "", - handle->previous_parakeet_pending, - "[]", - false, - 0.0, - 0, - 0, - CloudResponse{}, - handle->stream_first_token_seen ? handle->stream_first_token_ms : 0.0, - handle->last_stream_total_time_ms, - handle->last_stream_decode_tps, - handle->last_stream_decode_tokens, - handle->last_stream_decode_tokens, - handle->last_stream_raw_decoder_tps); - if (json_response.length() >= buffer_size) { - handle_error_response("Response buffer too small", response_buffer, buffer_size); - return -1; - } - std::strcpy(response_buffer, json_response.c_str()); - return static_cast(json_response.length()); - } - - cactus::engine::ParakeetTDTModel::ChunkStreamResult chunk_decode; - { - const auto decode_start = std::chrono::steady_clock::now(); - std::lock_guard lock(handle->model_handle->model_mutex); - cactus::telemetry::setStreamMode(true); - try { - chunk_decode = tdt_model->decode_chunk_stream( - window_audio, - decode_start_frame, - decode_start_frame, - decode_end_frame, - ctx.decoder_state); - cactus_reset(handle->model_handle); - } catch (...) { - cactus::telemetry::setStreamMode(false); - throw; - } - cactus::telemetry::setStreamMode(false); - const auto decode_end = std::chrono::steady_clock::now(); - const double decode_ms = - std::chrono::duration_cast( - decode_end - decode_start).count() / 1000.0; - handle->last_stream_total_time_ms = decode_ms; - handle->last_stream_decode_tokens = static_cast(chunk_decode.token_count); - handle->last_stream_decode_tps = - (chunk_decode.token_count > 1 && decode_ms > 0.0) - ? ((static_cast(chunk_decode.token_count) - 1.0) * 1000.0) / decode_ms - : 0.0; - handle->last_stream_raw_decoder_tps = chunk_decode.raw_decoder_tps; - } - - const float window_offset_sec = ctx.audio_time_offset_sec + - static_cast(window_start_sample) / static_cast(WHISPER_SAMPLE_RATE); - std::vector confirmed_segments; - if (!chunk_decode.confirmed_text.empty()) { - confirmed_segments.emplace_back( - window_offset_sec + chunk_decode.start_sec, - window_offset_sec + chunk_decode.confirmed_end_sec, - suppress_unwanted_text(chunk_decode.confirmed_text)); - } - std::string confirmed_text = suppress_unwanted_text(chunk_decode.confirmed_text); - std::string pending_text = suppress_unwanted_text(chunk_decode.pending_text); - if (!confirmed_text.empty() && !handle->custom_vocabulary.empty()) { - apply_vocabulary_spelling_correction(confirmed_text, handle->custom_vocabulary); - } - if (!pending_text.empty() && !handle->custom_vocabulary.empty()) { - apply_vocabulary_spelling_correction(pending_text, handle->custom_vocabulary); - } - - if (!confirmed_text.empty()) { - if (chunk_decode.token_count > 0) { - handle->stream_total_tokens += static_cast(chunk_decode.token_count); - handle->stream_cumulative_tokens += static_cast(chunk_decode.token_count); - } - if (!handle->stream_first_token_seen) { - auto now = std::chrono::steady_clock::now(); - handle->stream_first_token_ms = - std::chrono::duration_cast( - now - handle->stream_start).count(); - handle->stream_first_token_seen = true; - } - if (!handle->stream_session_first_token_seen) { - auto now = std::chrono::steady_clock::now(); - handle->stream_session_first_token_ms = - std::chrono::duration_cast( - now - handle->stream_session_start).count(); - handle->stream_session_first_token_seen = true; - } - parakeet_append_monotonic_text(handle->parakeet_committed_text, confirmed_text); - } - - handle->previous_parakeet_pending = pending_text; - handle->previous_parakeet_pending_ticks = pending_text.empty() ? 0 : 1; - - if (chunk_decode.confirmed_token_count > 0) { - const size_t confirmed_samples_in_window = static_cast( - chunk_decode.confirmed_end_sec * WHISPER_SAMPLE_RATE); - ctx.samples_decoded_up_to = window_start_sample + confirmed_samples_in_window; - } else if (chunk_decode.token_count == 0) { - ctx.samples_decoded_up_to = decodable_up_to; - } - - if (ctx.samples_decoded_up_to > kTdtLeftContextSamples * 2) { - const size_t trim_samples = ctx.samples_decoded_up_to - kTdtLeftContextSamples; - ctx.audio_samples.erase( - ctx.audio_samples.begin(), - ctx.audio_samples.begin() + trim_samples); - ctx.samples_decoded_up_to -= trim_samples; - ctx.audio_time_offset_sec += static_cast(trim_samples) / 16000.0f; - } - - handle->parakeet_committed_until_sec = std::max( - handle->parakeet_committed_until_sec, - window_offset_sec + - (chunk_decode.confirmed_token_count > 0 ? chunk_decode.confirmed_end_sec : 0.0f)); - - const std::string json_response = build_stream_response( - "{}", - "", - confirmed_text, - pending_text, - serialize_segments_with_offset(confirmed_segments, 0.0f), - false, - 0.0, - 0, - 0, - CloudResponse{}, - handle->stream_first_token_seen ? handle->stream_first_token_ms : 0.0, - handle->last_stream_total_time_ms, - handle->last_stream_decode_tps, - handle->last_stream_decode_tokens, - handle->last_stream_decode_tokens, - handle->last_stream_raw_decoder_tps); - if (json_response.length() >= buffer_size) { - handle_error_response("Response buffer too small", response_buffer, buffer_size); - return -1; - } - std::strcpy(response_buffer, json_response.c_str()); - return static_cast(json_response.length()); - } - - handle->audio_buffer.insert( - handle->audio_buffer.end(), - pcm_buffer, - pcm_buffer + pcm_buffer_size - ); - CACTUS_LOG_DEBUG("stream_transcribe_process", - "Inserted " << pcm_buffer_size << " bytes, buffer size: " << handle->audio_buffer.size()); - - if (handle->audio_buffer.size() < handle->options.min_chunk_size * sizeof(int16_t)) { - std::string json_response = "{\"success\":true,\"confirmed\":\"\",\"pending\":\"\"}"; - - if (json_response.length() >= buffer_size) { - last_error_message = "Response buffer too small"; - CACTUS_LOG_ERROR("stream_transcribe_process", last_error_message); - handle_error_response(last_error_message, response_buffer, buffer_size); - cactus::telemetry::recordStreamTranscription(nullptr, false, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0, last_error_message.c_str()); - return -1; - } - - std::strcpy(response_buffer, json_response.c_str()); - return static_cast(json_response.length()); - } - - std::string whisper_prompt = "<|startoftranscript|><|" + handle->options.language + "|><|transcribe|><|notimestamps|>"; - const char* transcribe_prompt = - is_gemma4 ? "Transcribe the audio." : - (is_moonshine || is_parakeet) ? "" : - whisper_prompt.c_str(); - std::string effective_transcribe_options_json = handle->transcribe_options_json; - if (is_parakeet && !is_parakeet_tdt) { - effective_transcribe_options_json = ensure_json_bool_option( - effective_transcribe_options_json, "use_vad", false); - } - - if (is_parakeet_tdt) { - constexpr double kParakeetTdtLeftContextSec = 2.0; - constexpr double kParakeetTdtChunkSec = 0.25; - constexpr double kParakeetTdtRightContextSec = 1.0; - constexpr size_t kParakeetTdtMaxChunksPerProcess = 3; - - const size_t left_context_bytes = seconds_to_pcm_bytes(kParakeetTdtLeftContextSec); - const size_t chunk_bytes = std::max( - seconds_to_pcm_bytes(kParakeetTdtChunkSec), - handle->options.min_chunk_size * sizeof(int16_t)); - const size_t right_context_bytes = seconds_to_pcm_bytes(kParakeetTdtRightContextSec); - const std::string tdt_vad_options_json = ensure_json_bool_option( - handle->transcribe_options_json, "use_vad", true); - const std::string tdt_no_vad_options_json = ensure_json_bool_option( - handle->transcribe_options_json, "use_vad", false); - - if (handle->parakeet_tdt_chunked_stream && - !handle->parakeet_tdt_chunked_blocked_until_pause && - !handle->parakeet_committed_text.empty()) { - const size_t encoder_frame_bytes = - get_parakeet_spectrogram_config().hop_length * - std::max(1, handle->model_handle->model->get_config().subsampling_factor) * - sizeof(int16_t); - std::string confirmed; - size_t confirmed_audio_bytes = 0; - size_t processed_chunks = 0; - bool chunked_stalled_on_speech = false; - - while (processed_chunks < kParakeetTdtMaxChunksPerProcess) { - const bool chunked_warmup = - handle->parakeet_committed_text.empty() && - handle->parakeet_tdt_pending_raw.empty(); - const size_t available_chunk_bytes = - handle->audio_buffer.size() > handle->parakeet_chunk_cursor_bytes + right_context_bytes - ? handle->audio_buffer.size() - handle->parakeet_chunk_cursor_bytes - right_context_bytes - : 0; - const size_t chunk_decode_bytes = std::max( - encoder_frame_bytes, - ((chunked_warmup - ? available_chunk_bytes - : seconds_to_pcm_bytes(0.96)) / - encoder_frame_bytes) * encoder_frame_bytes); - if (handle->parakeet_chunk_cursor_bytes + chunk_decode_bytes + right_context_bytes > - handle->audio_buffer.size()) { - break; - } - - const size_t chunk_start_bytes = handle->parakeet_chunk_cursor_bytes; - const size_t chunk_end_bytes = chunk_start_bytes + chunk_decode_bytes; - const size_t window_start_bytes = chunk_start_bytes > left_context_bytes - ? chunk_start_bytes - left_context_bytes - : 0; - const size_t window_end_bytes = chunk_end_bytes + right_context_bytes; - - cactus::engine::ParakeetTDTModel::ChunkStreamResult chunk_decode; - cactus::engine::ParakeetTDTModel::ChunkStreamState chunk_state; - if (!run_parakeet_tdt_chunk_decode( - handle, - window_start_bytes, - window_end_bytes, - chunk_start_bytes, - chunk_end_bytes, - chunk_decode, - chunk_state)) { - last_error_message = "Chunked TDT decode failed."; - CACTUS_LOG_ERROR("stream_transcribe_process", last_error_message); - handle_error_response(last_error_message, response_buffer, buffer_size); - cactus::telemetry::recordStreamTranscription( - handle->model_handle ? handle->model_handle->model_name.c_str() : nullptr, - false, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0, - last_error_message.c_str()); - return -1; - } - - const bool chunk_silent = is_pcm_chunk_silent( - handle->audio_buffer.data() + chunk_start_bytes, - chunk_end_bytes - chunk_start_bytes, - 0.012f); - const std::string raw_chunk_text = - strip_unwanted_text_preserving_edges(chunk_decode.text); - const std::string raw_confirmed_text = - strip_unwanted_text_preserving_edges(chunk_decode.confirmed_text); - const std::string raw_pending_text = - strip_unwanted_text_preserving_edges(chunk_decode.pending_text); - - if (raw_chunk_text.empty() && !chunk_silent) { - chunked_stalled_on_speech = true; - break; - } - - if (!raw_chunk_text.empty()) { - std::string confirmed_delta = suppress_unwanted_text(raw_confirmed_text); - std::string pending_raw = raw_pending_text; - if (!confirmed_delta.empty() && !handle->custom_vocabulary.empty()) { - apply_vocabulary_spelling_correction(confirmed_delta, handle->custom_vocabulary); - } - handle->parakeet_tdt_pending_raw = pending_raw; - if (!confirmed_delta.empty()) { - if (chunk_decode.token_count > 0) { - handle->stream_total_tokens += static_cast(chunk_decode.token_count); - handle->stream_cumulative_tokens += static_cast(chunk_decode.token_count); - } - - if (!handle->stream_first_token_seen) { - auto now = std::chrono::steady_clock::now(); - handle->stream_first_token_ms = - std::chrono::duration_cast( - now - handle->stream_start).count(); - handle->stream_first_token_seen = true; - } - if (!handle->stream_session_first_token_seen) { - auto now = std::chrono::steady_clock::now(); - handle->stream_session_first_token_ms = - std::chrono::duration_cast( - now - handle->stream_session_start).count(); - handle->stream_session_first_token_seen = true; - } - - parakeet_append_monotonic_text( - handle->parakeet_committed_text, - confirmed_delta); - parakeet_append_monotonic_text(confirmed, confirmed_delta); - const size_t confirmed_boundary_bytes = std::min( - chunk_end_bytes, - std::max( - chunk_start_bytes, - window_start_bytes + seconds_to_pcm_bytes(chunk_decode.confirmed_end_sec))); - if (confirmed_boundary_bytes > chunk_start_bytes) { - confirmed_audio_bytes += confirmed_boundary_bytes - chunk_start_bytes; - } - } - } - - handle->parakeet_chunk_cursor_bytes = chunk_end_bytes; - handle->parakeet_tdt_decoder_state = chunk_state; - const size_t checkpoint_end_bytes = std::min( - chunk_end_bytes, - std::max( - window_start_bytes, - window_start_bytes + seconds_to_pcm_bytes(chunk_decode.resume_end_sec))); - while (!handle->parakeet_tdt_resume_checkpoints.empty() && - handle->parakeet_tdt_resume_checkpoints.back().end_bytes >= checkpoint_end_bytes) { - handle->parakeet_tdt_resume_checkpoints.pop_back(); - } - if (handle->parakeet_tdt_resume_checkpoints.empty() || - handle->parakeet_tdt_resume_checkpoints.back().end_bytes < checkpoint_end_bytes) { - handle->parakeet_tdt_resume_checkpoints.push_back({checkpoint_end_bytes, chunk_state}); - } - while (handle->parakeet_tdt_resume_checkpoints.size() > 16) { - handle->parakeet_tdt_resume_checkpoints.pop_front(); - } - handle->parakeet_committed_until_sec = std::max( - handle->parakeet_committed_until_sec, - static_cast( - handle->stream_audio_offset_sec + - pcm_bytes_to_seconds(checkpoint_end_bytes))); - ++processed_chunks; - } - - const bool fallback_to_legacy_tdt = - chunked_stalled_on_speech && - confirmed.empty() && - handle->parakeet_tdt_pending_raw.empty(); - if (fallback_to_legacy_tdt) { - const bool bootstrap_reset = - handle->parakeet_committed_text.empty() && - handle->previous_parakeet_pending.empty(); - reset_parakeet_tdt_chunk_stream(handle); - handle->parakeet_tdt_chunked_blocked_until_pause = true; - if (bootstrap_reset) { - handle->parakeet_chunk_cursor_bytes = 0; - } - } - - if (!fallback_to_legacy_tdt && - handle->parakeet_chunk_cursor_bytes > left_context_bytes) { - const size_t trim_bytes = - handle->parakeet_chunk_cursor_bytes - left_context_bytes; - handle->stream_audio_offset_sec += static_cast( - pcm_bytes_to_seconds(trim_bytes)); - handle->audio_buffer.erase( - handle->audio_buffer.begin(), - handle->audio_buffer.begin() + trim_bytes); - handle->parakeet_chunk_cursor_bytes -= trim_bytes; - while (!handle->parakeet_tdt_resume_checkpoints.empty() && - handle->parakeet_tdt_resume_checkpoints.front().end_bytes <= trim_bytes) { - handle->parakeet_tdt_resume_checkpoints.pop_front(); - } - for (auto& checkpoint : handle->parakeet_tdt_resume_checkpoints) { - checkpoint.end_bytes -= trim_bytes; - } - } - - if (!fallback_to_legacy_tdt) { - std::string pending_output; - std::string raw_json = "{}"; - if (!handle->parakeet_tdt_pending_raw.empty()) { - pending_output = suppress_unwanted_text(handle->parakeet_tdt_pending_raw); - if (!pending_output.empty() && !handle->custom_vocabulary.empty()) { - apply_vocabulary_spelling_correction( - pending_output, handle->custom_vocabulary); - } - } - if (handle->audio_buffer.size() > handle->parakeet_chunk_cursor_bytes) { - const size_t preview_start_bytes = handle->parakeet_chunk_cursor_bytes > left_context_bytes - ? handle->parakeet_chunk_cursor_bytes - left_context_bytes - : 0; - StreamWindowDecodeResult preview_decode; - if (run_stream_window_transcribe( - handle, - transcribe_prompt, - tdt_no_vad_options_json, - handle->audio_buffer.data() + preview_start_bytes, - handle->audio_buffer.size() - preview_start_bytes, - preview_decode)) { - raw_json = preview_decode.raw_json; - if (pending_output.empty()) { - pending_output = parakeet_strip_recent_committed_prefix( - handle->parakeet_committed_text, - preview_decode.response); - if (!pending_output.empty() && !handle->custom_vocabulary.empty()) { - apply_vocabulary_spelling_correction( - pending_output, handle->custom_vocabulary); - } - } - } - } - - handle->previous_parakeet_pending = pending_output; - handle->previous_parakeet_pending_ticks = pending_output.empty() ? 0 : 1; - handle->previous_parakeet_audio_buffer_size = handle->audio_buffer.size(); - handle->previous_segments.clear(); - handle->previous_cloud_handoff = false; - handle->parakeet_onset_active = false; - handle->parakeet_resume_guard_active = false; - - CloudResponse empty_cloud_result; - const std::string json_response = build_stream_response( - raw_json, - "", - confirmed, - pending_output, - "[]", - false, - pcm_bytes_to_seconds(confirmed_audio_bytes) * 1000.0, - 0, - 0, - empty_cloud_result, - handle->stream_first_token_seen ? handle->stream_first_token_ms : 0.0, - raw_json == "{}" ? handle->last_stream_total_time_ms : -1.0, - raw_json == "{}" ? handle->last_stream_decode_tps : -1.0, - raw_json == "{}" ? handle->last_stream_decode_tokens : -1.0, - raw_json == "{}" ? handle->last_stream_decode_tokens : -1.0, - raw_json == "{}" ? handle->last_stream_raw_decoder_tps : -1.0); - if (json_response.length() >= buffer_size) { - last_error_message = "Response buffer too small"; - CACTUS_LOG_ERROR("stream_transcribe_process", last_error_message); - handle_error_response(last_error_message, response_buffer, buffer_size); - cactus::telemetry::recordStreamTranscription(nullptr, false, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0, last_error_message.c_str()); - return -1; - } - - std::strcpy(response_buffer, json_response.c_str()); - return static_cast(json_response.length()); - } - } - - auto decode_window = [&](size_t window_start_bytes, - size_t window_end_bytes, - StreamWindowDecodeResult& out, - float& out_offset_sec) -> bool { - if (window_end_bytes <= window_start_bytes || window_end_bytes > handle->audio_buffer.size()) { - return false; - } - out_offset_sec = static_cast( - handle->stream_audio_offset_sec + pcm_bytes_to_seconds(window_start_bytes)); - StreamWindowDecodeResult vad_decode; - const bool vad_ok = run_stream_window_transcribe( - handle, - transcribe_prompt, - tdt_vad_options_json, - handle->audio_buffer.data() + window_start_bytes, - window_end_bytes - window_start_bytes, - vad_decode); - if (vad_ok && (!vad_decode.response.empty() || !vad_decode.segments.empty())) { - out = std::move(vad_decode); - return true; - } - - return run_stream_window_transcribe( - handle, - transcribe_prompt, - tdt_no_vad_options_json, - handle->audio_buffer.data() + window_start_bytes, - window_end_bytes - window_start_bytes, - out); - }; - - constexpr size_t kParakeetHoldbackWords = 1; - constexpr size_t kParakeetSilenceFlushBytes = 16000; // ~0.5s silence - constexpr size_t kParakeetStableFlushWords = 10; - constexpr size_t kParakeetStableFlushTicks = 2; - constexpr size_t kParakeetResumeGuardBytes = 24000; // ~0.75s speech - constexpr size_t kParakeetOnsetLeadSilenceBytes = 16000; // ~0.5s lead-in - constexpr size_t kParakeetOnsetMinConfirmWords = 6; - - const bool chunk_silent = is_pcm_chunk_silent(pcm_buffer, pcm_buffer_size, 0.012f); - const size_t prior_silence_run_bytes = handle->parakeet_silence_run_bytes; - const bool speech_resumed = - !chunk_silent && prior_silence_run_bytes >= kParakeetSilenceFlushBytes; - const size_t speech_chunk_start_bytes = - handle->audio_buffer.size() >= pcm_buffer_size - ? handle->audio_buffer.size() - pcm_buffer_size - : 0; - - if (chunk_silent) { - handle->parakeet_silence_run_bytes += pcm_buffer_size; - handle->parakeet_speech_run_bytes = 0; - if (handle->parakeet_silence_run_bytes >= kParakeetSilenceFlushBytes) { - handle->parakeet_resume_guard_active = true; - handle->parakeet_onset_active = false; - reset_parakeet_tdt_chunk_stream(handle); - const size_t retained_silence_bytes = std::min( - handle->parakeet_silence_run_bytes, kParakeetOnsetLeadSilenceBytes); - handle->parakeet_onset_start_bytes = - handle->audio_buffer.size() > retained_silence_bytes - ? handle->audio_buffer.size() - retained_silence_bytes - : 0; - } - } else { - if (!handle->parakeet_onset_active && - handle->parakeet_committed_text.empty() && - handle->previous_parakeet_pending.empty()) { - handle->parakeet_onset_active = true; - handle->parakeet_onset_start_bytes = 0; - } - if (speech_resumed) { - handle->parakeet_resume_guard_active = true; - handle->parakeet_speech_run_bytes = 0; - handle->parakeet_onset_active = true; - const size_t retained_silence_bytes = std::min( - prior_silence_run_bytes, kParakeetOnsetLeadSilenceBytes); - handle->parakeet_onset_start_bytes = - speech_chunk_start_bytes > retained_silence_bytes - ? speech_chunk_start_bytes - retained_silence_bytes - : 0; - } - handle->parakeet_silence_run_bytes = 0; - if (handle->parakeet_resume_guard_active) { - handle->parakeet_speech_run_bytes += pcm_buffer_size; - if (handle->parakeet_speech_run_bytes >= kParakeetResumeGuardBytes) { - handle->parakeet_resume_guard_active = false; - handle->parakeet_speech_run_bytes = 0; - } - } - } - - const bool detected_pause = - handle->parakeet_silence_run_bytes >= kParakeetSilenceFlushBytes; - const bool should_hold_cursor = handle->parakeet_onset_active; - - size_t processed_chunks = 0; - if (!should_hold_cursor) { - while (handle->parakeet_chunk_cursor_bytes + chunk_bytes + right_context_bytes <= - handle->audio_buffer.size() && - processed_chunks < kParakeetTdtMaxChunksPerProcess) { - handle->parakeet_chunk_cursor_bytes += chunk_bytes; - ++processed_chunks; - } - } - - if (!handle->parakeet_onset_active && - handle->parakeet_chunk_cursor_bytes > left_context_bytes) { - const size_t trim_bytes = - handle->parakeet_chunk_cursor_bytes - left_context_bytes; - handle->stream_audio_offset_sec += static_cast( - pcm_bytes_to_seconds(trim_bytes)); - handle->audio_buffer.erase( - handle->audio_buffer.begin(), - handle->audio_buffer.begin() + trim_bytes); - handle->parakeet_chunk_cursor_bytes -= trim_bytes; - } - - const bool onset_mode_active = handle->parakeet_onset_active; - const size_t preview_start_bytes = onset_mode_active - ? std::min(handle->parakeet_onset_start_bytes, handle->audio_buffer.size()) - : (handle->parakeet_chunk_cursor_bytes > left_context_bytes - ? handle->parakeet_chunk_cursor_bytes - left_context_bytes - : 0); - const size_t preview_end_bytes = handle->audio_buffer.size(); - - StreamWindowDecodeResult response_decode; - float response_segments_offset_sec = 0.0f; - if (!decode_window(preview_start_bytes, preview_end_bytes, response_decode, response_segments_offset_sec)) { - last_error_message = "Transcription failed in stream process."; - CACTUS_LOG_ERROR("stream_transcribe_process", last_error_message); - handle_error_response(last_error_message, response_buffer, buffer_size); - cactus::telemetry::recordStreamTranscription( - handle->model_handle ? handle->model_handle->model_name.c_str() : nullptr, - false, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0, - last_error_message.c_str()); - return -1; - } - - std::string confirmed; - const std::string previous_pending = handle->previous_parakeet_pending; - std::string pending_tail = previous_pending; - const std::string normalized_response = parakeet_strip_recent_committed_prefix( - handle->parakeet_committed_text, response_decode.response); - if (!normalized_response.empty()) { - if (previous_pending.empty() || (onset_mode_active && handle->parakeet_resume_guard_active)) { - pending_tail = normalized_response; - } else { - const size_t stable_words = parakeet_common_prefix_words( - previous_pending, normalized_response); - const size_t confirm_words = - stable_words > kParakeetHoldbackWords - ? stable_words - kParakeetHoldbackWords - : 0; - confirmed = parakeet_take_first_words(normalized_response, confirm_words); - pending_tail = confirm_words > 0 - ? parakeet_drop_first_words(normalized_response, confirm_words) - : normalized_response; - } - } - - const bool onset_commit_too_small = - onset_mode_active && - !confirmed.empty() && - !detected_pause && - parakeet_word_count(confirmed) < kParakeetOnsetMinConfirmWords; - if (onset_commit_too_small) { - confirmed.clear(); - pending_tail = normalized_response; - } - - const bool pending_equivalent = - pending_tail == previous_pending || - (!pending_tail.empty() && - !previous_pending.empty() && - levenshtein_ci(pending_tail, previous_pending) <= 1); - const bool pending_changed = !pending_equivalent; - if (pending_changed) { - handle->previous_parakeet_audio_buffer_size = handle->audio_buffer.size(); - } - - const bool stable_short_pending = - !pending_tail.empty() && - !handle->parakeet_resume_guard_active && - pending_equivalent && - handle->previous_parakeet_pending_ticks >= kParakeetStableFlushTicks && - parakeet_word_count(pending_tail) <= kParakeetStableFlushWords; - if ((detected_pause || stable_short_pending) && !pending_tail.empty()) { - if (!confirmed.empty()) confirmed += " "; - confirmed += pending_tail; - pending_tail.clear(); - } - - double buffer_duration_ms = 0.0; - bool cloud_handoff_triggered = false; - uint64_t cloud_job_id = 0; - uint64_t cloud_result_job_id = 0; - CloudResponse cloud_result; - - if (!confirmed.empty()) { - if (!handle->custom_vocabulary.empty()) { - apply_vocabulary_spelling_correction(confirmed, handle->custom_vocabulary); - } - - std::string confirmed_delta = parakeet_emit_delta( - handle->parakeet_committed_text, confirmed); - if (!confirmed_delta.empty()) { - if (response_decode.decode_tokens > 0.0) { - handle->stream_total_tokens += static_cast(std::round(response_decode.decode_tokens)); - handle->stream_cumulative_tokens += static_cast(std::round(response_decode.decode_tokens)); - } - - if (!handle->stream_first_token_seen) { - auto now = std::chrono::steady_clock::now(); - handle->stream_first_token_ms = - std::chrono::duration_cast( - now - handle->stream_start).count(); - handle->stream_first_token_seen = true; - } - - if (!handle->stream_session_first_token_seen) { - auto now = std::chrono::steady_clock::now(); - handle->stream_session_first_token_ms = - std::chrono::duration_cast( - now - handle->stream_session_start).count(); - handle->stream_session_first_token_seen = true; - } - - if (!handle->parakeet_committed_text.empty()) { - handle->parakeet_committed_text += " "; - } - handle->parakeet_committed_text += confirmed_delta; - confirmed = confirmed_delta; - if (onset_mode_active) { - handle->parakeet_onset_active = false; - handle->parakeet_onset_start_bytes = 0; - handle->parakeet_chunk_cursor_bytes = - handle->audio_buffer.size() > right_context_bytes - ? handle->audio_buffer.size() - right_context_bytes - : 0; - } - buffer_duration_ms = - static_cast(processed_chunks) * kParakeetTdtChunkSec * 1000.0; - } else { - confirmed.clear(); - } - } - - if (pending_tail.empty()) { - handle->previous_parakeet_pending_ticks = 0; - } else if (pending_equivalent) { - handle->previous_parakeet_pending_ticks = std::min( - handle->previous_parakeet_pending_ticks + 1, 1000000); - } else { - handle->previous_parakeet_pending_ticks = 1; - } - handle->previous_parakeet_pending = pending_tail; - if (pending_tail.empty()) { - handle->previous_parakeet_audio_buffer_size = handle->audio_buffer.size(); - } - - std::vector current_segments = response_decode.segments; - const float segments_offset_sec = response_segments_offset_sec; - const std::string json_str = response_decode.raw_json; - const std::string pending_output = pending_tail; - handle->previous_cloud_handoff = false; - - for (auto it = handle->pending_cloud_jobs.begin(); it != handle->pending_cloud_jobs.end(); ) { - if (it->result.wait_for(std::chrono::milliseconds(0)) == std::future_status::ready) { - CloudResponse job_result = it->result.get(); - CACTUS_LOG_INFO( - "cloud_handoff", - "Completed stream cloud job id=" << it->id - << " used_cloud=" << (job_result.used_cloud ? "true" : "false") - << " error=" << (job_result.error.empty() ? "none" : job_result.error) - << " transcript_chars=" << job_result.transcript.size()); - handle->completed_cloud_results.push_back({it->id, std::move(job_result)}); - it = handle->pending_cloud_jobs.erase(it); - } else { - ++it; - } - } - - if (!handle->completed_cloud_results.empty()) { - cloud_result_job_id = handle->completed_cloud_results.front().first; - cloud_result = handle->completed_cloud_results.front().second; - handle->completed_cloud_results.erase(handle->completed_cloud_results.begin()); - - if (!cloud_result.api_key_hash.empty()) { - cactus::telemetry::setCloudKey(cloud_result.api_key_hash.c_str()); - } - } - - constexpr int STREAM_TOKENS_CAP = 20000; - constexpr double STREAM_DURATION_CAP_MS = 600000.0; - auto now = std::chrono::steady_clock::now(); - double elapsed_ms = - std::chrono::duration_cast( - now - handle->stream_start).count(); - if (handle->stream_total_tokens >= STREAM_TOKENS_CAP || - elapsed_ms >= STREAM_DURATION_CAP_MS) { - double period_tps = (elapsed_ms > 0.0) - ? (static_cast(handle->stream_total_tokens) * 1000.0) / elapsed_ms - : 0.0; - - double cumulative_elapsed_ms = - std::chrono::duration_cast( - now - handle->stream_session_start).count(); - double cumulative_tps = (cumulative_elapsed_ms > 0.0) - ? (static_cast(handle->stream_cumulative_tokens) * 1000.0) / - cumulative_elapsed_ms - : 0.0; - - cactus::telemetry::recordStreamTranscription( - handle->model_handle->model_name.c_str(), - true, - handle->stream_first_token_ms, - period_tps, - elapsed_ms, - handle->stream_total_tokens, - handle->stream_session_first_token_ms, - cumulative_tps, - cumulative_elapsed_ms, - handle->stream_cumulative_tokens, - "" - ); - - handle->stream_start = std::chrono::steady_clock::now(); - handle->stream_first_token_seen = false; - handle->stream_first_token_ms = 0.0; - handle->stream_total_tokens = 0; - } - - std::string json_response = build_stream_response( - json_str, - json_string(json_str, "error"), - confirmed, - pending_output, - serialize_segments_with_offset(current_segments, segments_offset_sec), - cloud_handoff_triggered, - buffer_duration_ms, - cloud_job_id, - cloud_result_job_id, - cloud_result - ); - - if (json_response.length() >= buffer_size) { - last_error_message = "Response buffer too small"; - CACTUS_LOG_ERROR("stream_transcribe_process", last_error_message); - handle_error_response(last_error_message, response_buffer, buffer_size); - cactus::telemetry::recordStreamTranscription( - handle->model_handle ? handle->model_handle->model_name.c_str() : nullptr, - false, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0, - last_error_message.c_str()); - return -1; - } - - std::strcpy(response_buffer, json_response.c_str()); - return static_cast(json_response.length()); - } - - const uint8_t* decode_pcm = handle->audio_buffer.data(); - size_t decode_pcm_size = handle->audio_buffer.size(); - if (is_parakeet_tdt) { - constexpr size_t kParakeetDecodeWindowBytes = 160000; // ~5.0s - if (decode_pcm_size > kParakeetDecodeWindowBytes) { - decode_pcm += (decode_pcm_size - kParakeetDecodeWindowBytes); - decode_pcm_size = kParakeetDecodeWindowBytes; - } - } - - cactus::telemetry::setStreamMode(true); - int result = cactus_transcribe( - handle->model_handle, - nullptr, - transcribe_prompt, - handle->transcribe_response_buffer, - sizeof(handle->transcribe_response_buffer), - effective_transcribe_options_json.empty() ? nullptr : effective_transcribe_options_json.c_str(), - nullptr, - nullptr, - decode_pcm, - decode_pcm_size); - cactus::telemetry::setStreamMode(false); - if (result < 0) { - last_error_message = "Transcription failed in stream process."; - CACTUS_LOG_ERROR("stream_transcribe_process", last_error_message); - handle_error_response(last_error_message, response_buffer, buffer_size); - cactus::telemetry::recordStreamTranscription(handle->model_handle ? handle->model_handle->model_name.c_str() : nullptr, false, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0, last_error_message.c_str()); - return -1; - } - - std::string json_str(handle->transcribe_response_buffer); - std::string response = suppress_unwanted_text(json_string(json_str, "response")); - - cactus_reset(handle->model_handle); - - auto current_segments = parse_segments(json_str); - const float segments_offset_sec = static_cast(handle->stream_audio_offset_sec); - - std::string confirmed; - double buffer_duration_ms = 0.0; - bool cloud_handoff_triggered = false; - uint64_t cloud_job_id = 0; - uint64_t cloud_result_job_id = 0; - CloudResponse cloud_result; - double chunk_decode_tokens = json_number(json_str, "decode_tokens"); - if (chunk_decode_tokens < 0.0) { - chunk_decode_tokens = 0.0; - } - - if (is_parakeet_tdt) { - constexpr size_t kParakeetHoldbackWords = 1; - constexpr size_t kParakeetTailBytes = 64000; // ~2.0s left context - constexpr size_t kParakeetSilenceFlushBytes = 16000; // ~0.5s silence - constexpr size_t kParakeetStableFlushWords = 4; - constexpr size_t kParakeetStableFlushTicks = 2; - constexpr size_t kParakeetMediumTrimWords = 3; - constexpr size_t kParakeetAggressiveTrimWords = 6; - - const std::string previous_pending = handle->previous_parakeet_pending; - std::string pending_tail = previous_pending; - const bool chunk_silent = is_pcm_chunk_silent(pcm_buffer, pcm_buffer_size, 0.012f); - if (chunk_silent) { - handle->parakeet_silence_run_bytes += pcm_buffer_size; - } else { - handle->parakeet_silence_run_bytes = 0; - } - if (!response.empty()) { - const std::string normalized_response = parakeet_strip_recent_committed_prefix( - handle->parakeet_committed_text, response); - if (previous_pending.empty()) { - pending_tail = normalized_response; - } else { - const size_t stable_words = parakeet_common_prefix_words(previous_pending, normalized_response); - size_t confirm_words = 0; - if (stable_words > kParakeetHoldbackWords) { - confirm_words = stable_words - kParakeetHoldbackWords; - } - - confirmed = parakeet_take_first_words(normalized_response, confirm_words); - if (confirm_words > 0) { - pending_tail = parakeet_drop_first_words(normalized_response, confirm_words); - } else { - pending_tail = normalized_response; - } - } - } - - const bool pending_equivalent = - pending_tail == previous_pending || - (!pending_tail.empty() && - !previous_pending.empty() && - levenshtein_ci(pending_tail, previous_pending) <= 1); - const bool pending_changed = !pending_equivalent; - if (pending_changed) { - handle->previous_parakeet_audio_buffer_size = handle->audio_buffer.size(); - } - - const bool detected_pause = - handle->parakeet_silence_run_bytes >= kParakeetSilenceFlushBytes; - const bool stable_short_pending = - !pending_tail.empty() && - pending_equivalent && - handle->previous_parakeet_pending_ticks >= kParakeetStableFlushTicks && - parakeet_word_count(pending_tail) <= kParakeetStableFlushWords && - handle->audio_buffer.size() >= - (handle->previous_parakeet_audio_buffer_size + (kParakeetSilenceFlushBytes / 2)); - if ((detected_pause || stable_short_pending) && !pending_tail.empty()) { - if (!confirmed.empty()) confirmed += " "; - confirmed += pending_tail; - pending_tail.clear(); - } - - if (!confirmed.empty()) { - if (!handle->custom_vocabulary.empty()) { - apply_vocabulary_spelling_correction(confirmed, handle->custom_vocabulary); - } - - std::string confirmed_delta = parakeet_emit_delta( - handle->parakeet_committed_text, confirmed); - - const size_t max_trim = handle->audio_buffer.size() > kParakeetTailBytes - ? handle->audio_buffer.size() - kParakeetTailBytes - : 0; - const size_t confirmed_words = parakeet_word_count(confirmed); - const size_t hop_bytes = std::clamp( - pcm_buffer_size, static_cast(4000), static_cast(16000)); - size_t confirmed_bytes = std::min(hop_bytes, max_trim); - if (confirmed_words >= kParakeetAggressiveTrimWords || pending_tail.empty()) { - confirmed_bytes = max_trim; - } else if (confirmed_words >= kParakeetMediumTrimWords) { - confirmed_bytes = std::min(max_trim, hop_bytes * 2); - } - buffer_duration_ms = (confirmed_bytes / 2.0) / 16000.0 * 1000.0; - - if (!confirmed_delta.empty()) { - if (chunk_decode_tokens > 0.0) { - handle->stream_total_tokens += static_cast(std::round(chunk_decode_tokens)); - handle->stream_cumulative_tokens += static_cast(std::round(chunk_decode_tokens)); - } - - if (!handle->stream_first_token_seen) { - auto now = std::chrono::steady_clock::now(); - handle->stream_first_token_ms = std::chrono::duration_cast(now - handle->stream_start).count(); - handle->stream_first_token_seen = true; - } - - if (!handle->stream_session_first_token_seen) { - auto now = std::chrono::steady_clock::now(); - handle->stream_session_first_token_ms = std::chrono::duration_cast(now - handle->stream_session_start).count(); - handle->stream_session_first_token_seen = true; - } - - if (!handle->parakeet_committed_text.empty()) { - handle->parakeet_committed_text += " "; - } - handle->parakeet_committed_text += confirmed_delta; - } - - if (confirmed_bytes > 0) { - if (handle->previous_cloud_handoff && !confirmed.empty()) { - cloud_handoff_triggered = true; - std::vector confirmed_audio( - handle->audio_buffer.begin(), - handle->audio_buffer.begin() + confirmed_bytes - ); - auto wav = cloud_build_wav(confirmed_audio.data(), confirmed_audio.size()); - std::string b64 = cloud_base64_encode(wav.data(), wav.size()); - cloud_job_id = handle->next_cloud_job_id++; - CACTUS_LOG_INFO( - "cloud_handoff", - "Queued stream cloud job id=" << cloud_job_id - << " confirmed_chars=" << confirmed.size() - << " audio_bytes=" << confirmed_audio.size()); - handle->pending_cloud_jobs.push_back({ - cloud_job_id, - std::async(std::launch::async, cloud_transcribe_request, b64, confirmed, 15L, nullptr) - }); - } - - handle->stream_audio_offset_sec += static_cast(confirmed_bytes) / (16000.0f * 2.0f); - handle->audio_buffer.erase( - handle->audio_buffer.begin(), - handle->audio_buffer.begin() + confirmed_bytes - ); - } - - confirmed = confirmed_delta; - handle->previous_cloud_handoff = false; - handle->previous_parakeet_audio_buffer_size = handle->audio_buffer.size(); - } else { - handle->previous_cloud_handoff = json_bool(json_str, "cloud_handoff"); - } - - if (pending_tail.empty()) { - handle->previous_parakeet_pending_ticks = 0; - } else if (pending_equivalent) { - handle->previous_parakeet_pending_ticks = std::min( - handle->previous_parakeet_pending_ticks + 1, 1000000); - } else { - handle->previous_parakeet_pending_ticks = 1; - } - handle->previous_parakeet_pending = pending_tail; - if (pending_tail.empty()) { - handle->previous_parakeet_audio_buffer_size = handle->audio_buffer.size(); - } - handle->previous_segments = current_segments; - } else { - size_t confirmed_segments = 0; - float confirmed_end_sec = 0.0f; - for (size_t i = 0; i < std::min(handle->previous_segments.size(), current_segments.size()); ++i) { - if (handle->previous_segments[i].text != current_segments[i].text) { - break; - } - confirmed_segments = i + 1; - confirmed_end_sec = std::min(handle->previous_segments[i].end, current_segments[i].end); - } - - if (confirmed_segments > 0 && confirmed_end_sec > 0.0f) { - buffer_duration_ms = confirmed_end_sec * 1000.0; - - for (size_t i = 0; i < confirmed_segments; ++i) { - if (!confirmed.empty()) confirmed += ' '; - confirmed += handle->previous_segments[i].text; - } - - if (!handle->custom_vocabulary.empty()) { - apply_vocabulary_spelling_correction(confirmed, handle->custom_vocabulary); - } - - if (chunk_decode_tokens > 0.0) { - handle->stream_total_tokens += static_cast(std::round(chunk_decode_tokens)); - handle->stream_cumulative_tokens += static_cast(std::round(chunk_decode_tokens)); - } - - if (!handle->stream_first_token_seen) { - auto now = std::chrono::steady_clock::now(); - handle->stream_first_token_ms = std::chrono::duration_cast(now - handle->stream_start).count(); - handle->stream_first_token_seen = true; - } - - if (!handle->stream_session_first_token_seen) { - auto now = std::chrono::steady_clock::now(); - handle->stream_session_first_token_ms = std::chrono::duration_cast(now - handle->stream_session_start).count(); - handle->stream_session_first_token_seen = true; - } - - const size_t confirmed_bytes = std::min( - static_cast(confirmed_end_sec * 16000.0) * 2, - handle->audio_buffer.size() - ); - - if (handle->previous_cloud_handoff && !confirmed.empty()) { - cloud_handoff_triggered = true; - std::vector confirmed_audio( - handle->audio_buffer.begin(), - handle->audio_buffer.begin() + confirmed_bytes - ); - auto wav = cloud_build_wav(confirmed_audio.data(), confirmed_audio.size()); - std::string b64 = cloud_base64_encode(wav.data(), wav.size()); - cloud_job_id = handle->next_cloud_job_id++; - CACTUS_LOG_INFO( - "cloud_handoff", - "Queued stream cloud job id=" << cloud_job_id - << " confirmed_chars=" << confirmed.size() - << " audio_bytes=" << confirmed_audio.size()); - handle->pending_cloud_jobs.push_back({ - cloud_job_id, - std::async(std::launch::async, cloud_transcribe_request, b64, confirmed, 15L, nullptr) - }); - } - - handle->stream_audio_offset_sec += confirmed_end_sec; - handle->audio_buffer.erase( - handle->audio_buffer.begin(), - handle->audio_buffer.begin() + confirmed_bytes - ); - constexpr float kSegmentEpsilonSec = 0.02f; - size_t current_tail_start = 0; - while (current_tail_start < current_segments.size() && - current_segments[current_tail_start].end <= confirmed_end_sec + kSegmentEpsilonSec) { - ++current_tail_start; - } - handle->previous_segments = std::vector( - current_segments.begin() + current_tail_start, current_segments.end()); - handle->previous_cloud_handoff = false; - } else { - handle->previous_segments = current_segments; - handle->previous_cloud_handoff = json_bool(json_str, "cloud_handoff"); - } - } - - for (auto it = handle->pending_cloud_jobs.begin(); it != handle->pending_cloud_jobs.end(); ) { - if (it->result.wait_for(std::chrono::milliseconds(0)) == std::future_status::ready) { - CloudResponse job_result = it->result.get(); - CACTUS_LOG_INFO( - "cloud_handoff", - "Completed stream cloud job id=" << it->id - << " used_cloud=" << (job_result.used_cloud ? "true" : "false") - << " error=" << (job_result.error.empty() ? "none" : job_result.error) - << " transcript_chars=" << job_result.transcript.size()); - handle->completed_cloud_results.push_back({it->id, std::move(job_result)}); - it = handle->pending_cloud_jobs.erase(it); - } else { - ++it; - } - } - - if (!handle->completed_cloud_results.empty()) { - cloud_result_job_id = handle->completed_cloud_results.front().first; - cloud_result = handle->completed_cloud_results.front().second; - handle->completed_cloud_results.erase(handle->completed_cloud_results.begin()); - - if (!cloud_result.api_key_hash.empty()) { - cactus::telemetry::setCloudKey(cloud_result.api_key_hash.c_str()); - } - } - - constexpr int STREAM_TOKENS_CAP = 20000; - constexpr double STREAM_DURATION_CAP_MS = 600000.0; - auto now = std::chrono::steady_clock::now(); - double elapsed_ms = std::chrono::duration_cast(now - handle->stream_start).count(); - if (handle->stream_total_tokens >= STREAM_TOKENS_CAP || elapsed_ms >= STREAM_DURATION_CAP_MS) { - double period_tps = (elapsed_ms > 0.0) ? (static_cast(handle->stream_total_tokens) * 1000.0) / elapsed_ms : 0.0; - - double cumulative_elapsed_ms = std::chrono::duration_cast(now - handle->stream_session_start).count(); - double cumulative_tps = (cumulative_elapsed_ms > 0.0) ? (static_cast(handle->stream_cumulative_tokens) * 1000.0) / cumulative_elapsed_ms : 0.0; - - cactus::telemetry::recordStreamTranscription( - handle->model_handle->model_name.c_str(), - true, - handle->stream_first_token_ms, - period_tps, - elapsed_ms, - handle->stream_total_tokens, - handle->stream_session_first_token_ms, - cumulative_tps, - cumulative_elapsed_ms, - handle->stream_cumulative_tokens, - "" - ); - - handle->stream_start = std::chrono::steady_clock::now(); - handle->stream_first_token_seen = false; - handle->stream_first_token_ms = 0.0; - handle->stream_total_tokens = 0; - } - - std::string pending_output = response; - if (is_parakeet_tdt && !handle->previous_parakeet_pending.empty()) { - pending_output = handle->previous_parakeet_pending; - } - - std::string json_response = build_stream_response( - json_str, - json_string(json_str, "error"), - confirmed, - pending_output, - serialize_segments_with_offset(current_segments, segments_offset_sec), - cloud_handoff_triggered, - buffer_duration_ms, - cloud_job_id, - cloud_result_job_id, - cloud_result - ); - - if (json_response.length() >= buffer_size) { - last_error_message = "Response buffer too small"; - CACTUS_LOG_ERROR("stream_transcribe_process", last_error_message); - handle_error_response(last_error_message, response_buffer, buffer_size); - cactus::telemetry::recordStreamTranscription(handle->model_handle ? handle->model_handle->model_name.c_str() : nullptr, false, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0, last_error_message.c_str()); - return -1; - } - - std::strcpy(response_buffer, json_response.c_str()); - return static_cast(json_response.length()); - } catch (const std::exception& e) { - last_error_message = "Exception during stream_transcribe_process: " + std::string(e.what()); - CACTUS_LOG_ERROR("stream_transcribe_process", last_error_message); - handle_error_response(e.what(), response_buffer, buffer_size); - cactus::telemetry::recordStreamTranscription(nullptr, false, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0, e.what()); - return -1; - } catch (...) { - last_error_message = "Unknown exception during stream transcription processing"; - CACTUS_LOG_ERROR("stream_transcribe_process", last_error_message); - handle_error_response("Unknown error during stream processing", response_buffer, buffer_size); - cactus::telemetry::recordStreamTranscription(nullptr, false, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0, "Unknown error during stream processing"); - return -1; - } -} - -int cactus_stream_transcribe_stop( - cactus_stream_transcribe_t stream, - char* response_buffer, - size_t buffer_size -) { - if (!stream) { - last_error_message = "Stream not initialized."; - CACTUS_LOG_ERROR("stream_transcribe_stop", last_error_message); - return -1; - } - - auto* handle = static_cast(stream); - auto clear_stream_vocab_bias = [&]() { - if (handle->has_custom_vocabulary_bias && - handle->model_handle && - handle->model_handle->model) { - handle->model_handle->model->clear_vocab_bias(); - } - }; - - if (!response_buffer || buffer_size == 0) { - clear_stream_vocab_bias(); - delete handle; - return 0; - } - - try { - const auto model_type = handle->model_handle->model->get_config().model_type; - bool is_moonshine = model_type == cactus::engine::Config::ModelType::MOONSHINE; - bool is_parakeet_tdt = - model_type == cactus::engine::Config::ModelType::PARAKEET_TDT; - bool is_parakeet_ctc = - model_type == cactus::engine::Config::ModelType::PARAKEET; - bool is_parakeet = - is_parakeet_ctc || is_parakeet_tdt; - bool is_tinyllama = model_type == cactus::engine::Config::ModelType::GEMMA4; - - std::string final_confirmed; - if (is_parakeet_tdt) { - final_confirmed = handle->parakeet_committed_text; - if (!(handle->parakeet_tdt_chunked_stream && handle->parakeet_tdt_decode_context.initialized) && - !handle->previous_parakeet_pending.empty()) { - const std::string normalized_pending = parakeet_strip_recent_committed_prefix( - final_confirmed, handle->previous_parakeet_pending); - const std::string pending_delta = parakeet_emit_delta( - final_confirmed, normalized_pending); - if (!pending_delta.empty()) { - if (!final_confirmed.empty()) final_confirmed += " "; - final_confirmed += pending_delta; - } - } - } else { - for (const auto& seg : handle->previous_segments) { - if (!final_confirmed.empty()) final_confirmed += ' '; - final_confirmed += seg.text; - } - } - - if (is_parakeet_tdt && - handle->parakeet_tdt_chunked_stream && - handle->parakeet_tdt_decode_context.initialized) { - auto& ctx = handle->parakeet_tdt_decode_context; - auto* tdt_model = static_cast( - handle->model_handle->model.get()); - const size_t total_samples = ctx.audio_samples.size(); - if (total_samples > ctx.samples_decoded_up_to) { - constexpr size_t kTdtLeftContextSamples = 8 * 16000; - const size_t window_start_sample = ctx.samples_decoded_up_to > kTdtLeftContextSamples - ? ctx.samples_decoded_up_to - kTdtLeftContextSamples - : 0; - std::vector window_audio( - ctx.audio_samples.begin() + window_start_sample, - ctx.audio_samples.end()); - - auto cfg = get_parakeet_spectrogram_config(); - const size_t waveform_samples = window_audio.size(); - apply_preemphasis(window_audio, 0.97f); - window_audio = ctx.audio_processor.compute_spectrogram(window_audio, cfg); - normalize_parakeet_log_mel(window_audio, ctx.mel_bins); - size_t valid_frames = waveform_samples / cfg.hop_length; - if (valid_frames == 0) valid_frames = 1; - trim_mel_frames(window_audio, ctx.mel_bins, valid_frames); - - const uint32_t subsampling = - std::max(1, handle->model_handle->model->get_config().subsampling_factor); - const size_t samples_per_enc_frame = cfg.hop_length * subsampling; - const size_t context_samples = ctx.samples_decoded_up_to - window_start_sample; - const size_t decode_start_frame = context_samples / samples_per_enc_frame; - const size_t total_window_samples = total_samples - window_start_sample; - const size_t decode_end_frame = total_window_samples / samples_per_enc_frame; - - if (decode_end_frame > decode_start_frame) { - cactus::engine::ParakeetTDTModel::ChunkStreamResult chunk_decode; - { - std::lock_guard lock(handle->model_handle->model_mutex); - cactus::telemetry::setStreamMode(true); - try { - chunk_decode = tdt_model->decode_chunk_stream( - window_audio, - decode_start_frame, - decode_start_frame, - decode_end_frame, - ctx.decoder_state); - cactus_reset(handle->model_handle); - } catch (...) { - cactus::telemetry::setStreamMode(false); - throw; - } - cactus::telemetry::setStreamMode(false); - } - - std::string confirmed_flush = suppress_unwanted_text(chunk_decode.confirmed_text); - std::string pending_flush = suppress_unwanted_text(chunk_decode.pending_text); - if (!confirmed_flush.empty() && !handle->custom_vocabulary.empty()) { - apply_vocabulary_spelling_correction(confirmed_flush, handle->custom_vocabulary); - } - if (!pending_flush.empty() && !handle->custom_vocabulary.empty()) { - apply_vocabulary_spelling_correction(pending_flush, handle->custom_vocabulary); - } - parakeet_append_monotonic_text(final_confirmed, confirmed_flush); - parakeet_append_monotonic_text(final_confirmed, pending_flush); - } - } else if (!handle->previous_parakeet_pending.empty()) { - parakeet_append_monotonic_text(final_confirmed, handle->previous_parakeet_pending); - } - } - - if (!handle->audio_buffer.empty()) { - std::string whisper_prompt = "<|startoftranscript|><|" + handle->options.language + "|><|transcribe|><|notimestamps|>"; - const char* transcribe_prompt = - is_tinyllama ? "Transcribe the audio." : - (is_moonshine || is_parakeet) ? "" : - whisper_prompt.c_str(); - std::string effective_transcribe_options_json = handle->transcribe_options_json; - if (is_parakeet && !is_parakeet_tdt) { - effective_transcribe_options_json = ensure_json_bool_option( - effective_transcribe_options_json, "use_vad", false); - } - - if (is_parakeet_tdt) { - const bool use_chunked_stop = - handle->parakeet_tdt_chunked_stream && - !handle->parakeet_tdt_chunked_blocked_until_pause && - (handle->parakeet_tdt_decoder_state.initialized || - !handle->parakeet_tdt_resume_checkpoints.empty() || - !handle->parakeet_tdt_pending_raw.empty()); - if (use_chunked_stop) { - final_confirmed = handle->parakeet_committed_text; - constexpr double kParakeetTdtLeftContextSec = 2.0; - const size_t left_context_bytes = seconds_to_pcm_bytes(kParakeetTdtLeftContextSec); - if (handle->parakeet_chunk_cursor_bytes < handle->audio_buffer.size()) { - const size_t chunk_start_bytes = handle->parakeet_chunk_cursor_bytes; - const size_t chunk_end_bytes = handle->audio_buffer.size(); - const size_t window_start_bytes = chunk_start_bytes > left_context_bytes - ? chunk_start_bytes - left_context_bytes - : 0; - cactus::engine::ParakeetTDTModel::ChunkStreamResult chunk_decode; - cactus::engine::ParakeetTDTModel::ChunkStreamState chunk_state; - if (run_parakeet_tdt_chunk_decode( - handle, - window_start_bytes, - chunk_end_bytes, - chunk_start_bytes, - chunk_end_bytes, - chunk_decode, - chunk_state)) { - std::string confirmed_chunk = suppress_unwanted_text(chunk_decode.confirmed_text); - std::string pending_chunk = suppress_unwanted_text(chunk_decode.pending_text); - if (!confirmed_chunk.empty() && !handle->custom_vocabulary.empty()) { - apply_vocabulary_spelling_correction(confirmed_chunk, handle->custom_vocabulary); - } - if (!pending_chunk.empty() && !handle->custom_vocabulary.empty()) { - apply_vocabulary_spelling_correction(pending_chunk, handle->custom_vocabulary); - } - parakeet_append_monotonic_text(final_confirmed, confirmed_chunk); - handle->parakeet_tdt_pending_raw = - strip_unwanted_text_preserving_edges(chunk_decode.pending_text); - handle->parakeet_chunk_cursor_bytes = chunk_end_bytes; - handle->parakeet_tdt_decoder_state = chunk_state; - handle->parakeet_committed_until_sec = std::max( - handle->parakeet_committed_until_sec, - static_cast( - handle->stream_audio_offset_sec + - pcm_bytes_to_seconds( - std::min( - chunk_end_bytes, - std::max( - window_start_bytes, - window_start_bytes + seconds_to_pcm_bytes( - chunk_decode.resume_end_sec)))))); - } - } else if (!handle->previous_parakeet_pending.empty()) { - const std::string normalized_pending = parakeet_strip_recent_committed_prefix( - final_confirmed, handle->previous_parakeet_pending); - const std::string pending_delta = parakeet_emit_delta( - final_confirmed, normalized_pending); - if (!pending_delta.empty()) { - if (!final_confirmed.empty()) final_confirmed += " "; - final_confirmed += pending_delta; - } - } - if (!handle->parakeet_tdt_pending_raw.empty()) { - std::string pending_flush = suppress_unwanted_text( - handle->parakeet_tdt_pending_raw); - if (!pending_flush.empty() && !handle->custom_vocabulary.empty()) { - apply_vocabulary_spelling_correction( - pending_flush, handle->custom_vocabulary); - } - parakeet_append_monotonic_text(final_confirmed, pending_flush); - handle->parakeet_tdt_pending_raw.clear(); - } - handle->parakeet_committed_text = final_confirmed; - } else { - constexpr double kParakeetTdtLeftContextSec = 2.0; - constexpr float kParakeetTdtCommitEpsilonSec = 0.03f; - const size_t left_context_bytes = seconds_to_pcm_bytes(kParakeetTdtLeftContextSec); - const size_t flush_start_bytes = handle->parakeet_onset_active - ? std::min(handle->parakeet_onset_start_bytes, handle->audio_buffer.size()) - : (handle->parakeet_chunk_cursor_bytes > left_context_bytes - ? handle->parakeet_chunk_cursor_bytes - left_context_bytes - : 0); - const std::string tdt_vad_options_json = ensure_json_bool_option( - handle->transcribe_options_json, "use_vad", true); - const std::string tdt_no_vad_options_json = ensure_json_bool_option( - handle->transcribe_options_json, "use_vad", false); - - StreamWindowDecodeResult flush_decode; - bool flush_ok = run_stream_window_transcribe( - handle, - transcribe_prompt, - tdt_vad_options_json, - handle->audio_buffer.data() + flush_start_bytes, - handle->audio_buffer.size() - flush_start_bytes, - flush_decode); - if (flush_ok && flush_decode.response.empty() && flush_decode.segments.empty()) { - flush_ok = run_stream_window_transcribe( - handle, - transcribe_prompt, - tdt_no_vad_options_json, - handle->audio_buffer.data() + flush_start_bytes, - handle->audio_buffer.size() - flush_start_bytes, - flush_decode); - } - if (flush_ok) { - const float flush_offset_sec = static_cast( - handle->stream_audio_offset_sec + pcm_bytes_to_seconds(flush_start_bytes)); - float flushed_until_sec = handle->parakeet_committed_until_sec; - std::string flushed_text = join_segments_by_end_time( - flush_decode.segments, - flush_offset_sec, - handle->parakeet_committed_until_sec + kParakeetTdtCommitEpsilonSec, - std::numeric_limits::max(), - &flushed_until_sec); - if (flushed_text.empty()) { - flushed_text = parakeet_strip_recent_committed_prefix( - final_confirmed, flush_decode.response); - } - const std::string effective_flush_delta = - parakeet_emit_delta(final_confirmed, flushed_text); - if (!effective_flush_delta.empty()) { - if (!final_confirmed.empty()) final_confirmed += " "; - final_confirmed += effective_flush_delta; - } - if (flushed_until_sec > handle->parakeet_committed_until_sec) { - handle->parakeet_committed_until_sec = flushed_until_sec; - } - } else { - CACTUS_LOG_WARN( - "stream_transcribe_stop", - "Final flush transcription failed; returning previously confirmed transcript."); - } - } - } else { - cactus::telemetry::setStreamMode(true); - const int flush_result = cactus_transcribe( - handle->model_handle, - nullptr, - transcribe_prompt, - handle->transcribe_response_buffer, - sizeof(handle->transcribe_response_buffer), - effective_transcribe_options_json.empty() ? nullptr : effective_transcribe_options_json.c_str(), - nullptr, - nullptr, - handle->audio_buffer.data(), - handle->audio_buffer.size()); - cactus::telemetry::setStreamMode(false); - - cactus_reset(handle->model_handle); - - if (flush_result >= 0) { - std::string flush_json(handle->transcribe_response_buffer); - std::string flushed_text = suppress_unwanted_text(json_string(flush_json, "response")); - if (flushed_text.empty()) { - auto flush_segments = parse_segments(flush_json); - for (const auto& seg : flush_segments) { - if (!flushed_text.empty()) flushed_text += ' '; - flushed_text += seg.text; - } - } - if (!flushed_text.empty()) { - if (is_parakeet_tdt) { - const std::string normalized_flush = parakeet_strip_recent_committed_prefix( - final_confirmed, flushed_text); - const std::string effective_flush_delta = - parakeet_emit_delta(final_confirmed, normalized_flush); - if (!effective_flush_delta.empty()) { - if (!final_confirmed.empty()) final_confirmed += " "; - final_confirmed += effective_flush_delta; - } - } else { - final_confirmed = flushed_text; - } - } - } else { - CACTUS_LOG_WARN( - "stream_transcribe_stop", - "Final flush transcription failed; returning previously confirmed segments."); - } - } - } - - if (!handle->custom_vocabulary.empty()) { - apply_vocabulary_spelling_correction(final_confirmed, handle->custom_vocabulary); - } - - std::string json_response = "{\"success\":true,\"confirmed\":\"" + - escape_json(final_confirmed) + "\"}"; - - auto now = std::chrono::steady_clock::now(); - double elapsed_ms = std::chrono::duration_cast(now - handle->stream_start).count(); - double period_tps = (elapsed_ms > 0.0) ? (static_cast(handle->stream_total_tokens) * 1000.0) / elapsed_ms : 0.0; - - double cumulative_elapsed_ms = std::chrono::duration_cast(now - handle->stream_session_start).count(); - double cumulative_tps = (cumulative_elapsed_ms > 0.0) ? (static_cast(handle->stream_cumulative_tokens) * 1000.0) / cumulative_elapsed_ms : 0.0; - - cactus::telemetry::recordStreamTranscription( - handle->model_handle->model_name.c_str(), - true, - handle->stream_first_token_ms, - period_tps, - elapsed_ms, - handle->stream_total_tokens, - handle->stream_session_first_token_ms, - cumulative_tps, - cumulative_elapsed_ms, - handle->stream_cumulative_tokens, - "" - ); - - if (json_response.length() >= buffer_size) { - last_error_message = "Response buffer too small"; - CACTUS_LOG_ERROR("stream_transcribe_stop", last_error_message); - handle_error_response(last_error_message, response_buffer, buffer_size); - cactus::telemetry::recordStreamTranscription(handle->model_handle ? handle->model_handle->model_name.c_str() : nullptr, false, handle->stream_first_token_ms, 0.0, 0.0, handle->stream_total_tokens, handle->stream_session_first_token_ms, 0.0, 0.0, handle->stream_cumulative_tokens, last_error_message.c_str()); - clear_stream_vocab_bias(); - delete handle; - return -1; - } - - std::strcpy(response_buffer, json_response.c_str()); - clear_stream_vocab_bias(); - delete handle; - return static_cast(json_response.length()); - } catch (const std::exception& e) { - last_error_message = "Exception during stream_transcribe_stop: " + std::string(e.what()); - CACTUS_LOG_ERROR("stream_transcribe_stop", last_error_message); - handle_error_response(e.what(), response_buffer, buffer_size); - cactus::telemetry::recordStreamTranscription(handle->model_handle ? handle->model_handle->model_name.c_str() : nullptr, false, handle->stream_first_token_ms, 0.0, 0.0, handle->stream_total_tokens, handle->stream_session_first_token_ms, 0.0, 0.0, handle->stream_cumulative_tokens, e.what()); - clear_stream_vocab_bias(); - delete handle; - return -1; - } catch (...) { - last_error_message = "Unknown exception during stream transcription stop"; - CACTUS_LOG_ERROR("stream_transcribe_stop", last_error_message); - handle_error_response("Unknown error during stream stop", response_buffer, buffer_size); - cactus::telemetry::recordStreamTranscription(handle->model_handle ? handle->model_handle->model_name.c_str() : nullptr, false, handle->stream_first_token_ms, 0.0, 0.0, handle->stream_total_tokens, handle->stream_session_first_token_ms, 0.0, 0.0, handle->stream_cumulative_tokens, "Unknown error during stream stop"); - clear_stream_vocab_bias(); - delete handle; - return -1; - } -} - -} diff --git a/cactus/ffi/cactus_telemetry.cpp b/cactus/ffi/cactus_telemetry.cpp deleted file mode 100644 index 1844f0e22..000000000 --- a/cactus/ffi/cactus_telemetry.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "cactus_ffi.h" -#include "telemetry/telemetry.h" - -extern "C" { - -void cactus_set_telemetry_environment(const char* framework, const char* cache_location, const char* version) { - cactus::telemetry::setTelemetryEnvironment(framework, cache_location, version); -} - -void cactus_set_app_id(const char* app_id) { - cactus::telemetry::setAppId(app_id); -} - -void cactus_telemetry_flush(void) { - cactus::telemetry::flush(); -} - -void cactus_telemetry_shutdown(void) { - cactus::telemetry::shutdown(); -} - -} diff --git a/cactus/ffi/cactus_transcribe.cpp b/cactus/ffi/cactus_transcribe.cpp deleted file mode 100644 index 539c0f155..000000000 --- a/cactus/ffi/cactus_transcribe.cpp +++ /dev/null @@ -1,829 +0,0 @@ -#include "cactus_ffi.h" -#include "cactus_utils.h" -#include "../models/model.h" -#include "telemetry/telemetry.h" -#include "../../libs/audio/wav.h" -#include -#include -#include -#include -#include -#include -#include - -using namespace cactus::engine; -using namespace cactus::ffi; -using cactus::audio::WHISPER_TARGET_FRAMES; -using cactus::audio::WHISPER_SAMPLE_RATE; -using cactus::audio::apply_preemphasis; -using cactus::audio::get_parakeet_spectrogram_config; -using cactus::audio::get_whisper_spectrogram_config; -using cactus::audio::init_whisper_mel_filters; -using cactus::audio::normalize_parakeet_log_mel; -using cactus::audio::normalize_whisper_mel; -using cactus::audio::trim_mel_frames; -using cactus::audio::get_htk_spectrogram_config; -using cactus::audio::get_gemma4_audio_spectrogram_config; -using cactus::audio::transpose_mel_to_frame_major; - -static constexpr size_t WHISPER_MAX_DECODER_POSITIONS = 448; -static constexpr size_t MAX_CHUNK_SAMPLES = WHISPER_SAMPLE_RATE * 30; -static constexpr size_t MAX_CONTEXT_WORDS = 64; - -static std::string extract_whisper_language_code(std::string token_text) { - const size_t open = token_text.find("<|"); - if (open == std::string::npos) return ""; - - const size_t close = token_text.find("|>", open + 2); - if (close == std::string::npos || close <= open + 2) return ""; - - std::string inner = token_text.substr(open + 2, close - (open + 2)); - std::transform(inner.begin(), inner.end(), inner.begin(), [](unsigned char c) { - return static_cast(std::tolower(c)); - }); - - static const std::vector non_language_tokens = { - "startoftranscript", "transcribe", "translate", "notimestamps", - "endoftext", "nospeech", "nocaptions", "sot", "sot_prev", "sot_lm" - }; - for (const auto& reserved : non_language_tokens) { - if (inner == reserved) return ""; - } - - if (inner.size() < 2 || inner.size() > 8) return ""; - for (char c : inner) { - const bool ok = (c >= 'a' && c <= 'z') || c == '-'; - if (!ok) return ""; - } - return inner; -} - -static bool is_terminal_transcription_piece(const std::string& piece) { - return piece == "<|endoftext|>" || - piece == "<|endoftranscript|>" || - piece == "" || - piece == ""; -} - -extern "C" { - -int cactus_transcribe( - cactus_model_t model, - const char* audio_file_path, - const char* prompt, - char* response_buffer, - size_t buffer_size, - const char* options_json, - cactus_token_callback callback, - void* user_data, - const uint8_t* pcm_buffer, - size_t pcm_buffer_size -) { - if (validate_audio_params("transcribe", model, response_buffer, buffer_size, audio_file_path, pcm_buffer, pcm_buffer_size) != 0) - return -1; - - try { - auto start_time = std::chrono::high_resolution_clock::now(); - auto* handle = static_cast(model); - std::lock_guard lock(handle->model_mutex); - handle->should_stop = false; - - float cloud_handoff_threshold = handle->model->get_config().default_cloud_handoff_threshold; - const std::string opts = options_json ? options_json : ""; - InferenceOptions options = parse_inference_options_json(opts); - { - size_t pos = opts.find("\"cloud_handoff_threshold\""); - if (pos != std::string::npos) { - pos = opts.find(':', pos); - if (pos != std::string::npos) { - ++pos; - while (pos < opts.size() && std::isspace(static_cast(opts[pos]))) ++pos; - try { - cloud_handoff_threshold = std::stof(opts.c_str() + pos); - } catch (...) {} - } - } - } - - const char* force_handoff_env = std::getenv("CACTUS_FORCE_HANDOFF"); - if (force_handoff_env && force_handoff_env[0] == '1' && force_handoff_env[1] == '\0') { - cloud_handoff_threshold = 0.0001f; - } - - const bool request_has_custom_vocabulary_options = - opts.find("\"custom_vocabulary\"") != std::string::npos || - opts.find("\"vocabulary_boost\"") != std::string::npos; - const bool apply_request_scoped_vocabulary_bias = - request_has_custom_vocabulary_options && !handle->model->has_vocab_bias(); - - struct ScopedVocabularyBiasReset { - Model* model; - bool clear_on_exit; - ~ScopedVocabularyBiasReset() { - if (clear_on_exit && model) { - model->clear_vocab_bias(); - } - } - } scoped_vocabulary_bias_reset{ - handle->model.get(), - apply_request_scoped_vocabulary_bias - }; - std::vector custom_vocabulary; - float vocabulary_boost_unused = 5.0f; - parse_custom_vocabulary_options(opts, custom_vocabulary, vocabulary_boost_unused); - - if (apply_request_scoped_vocabulary_bias) { - apply_custom_vocabulary_options(handle->model.get(), opts); - } - - if (request_has_custom_vocabulary_options && - options.temperature == 0.0f && options.top_p <= 0.0f && options.top_k == 0) { - // Keep deterministic decoding while ensuring bias is applied in sampling. - options.top_k = 1; - } - - bool is_whisper = handle->model->get_config().model_type == cactus::engine::Config::ModelType::WHISPER; - bool is_moonshine = handle->model->get_config().model_type == cactus::engine::Config::ModelType::MOONSHINE; - bool is_parakeet_tdt = handle->model->get_config().model_type == cactus::engine::Config::ModelType::PARAKEET_TDT; - bool is_parakeet = - handle->model->get_config().model_type == cactus::engine::Config::ModelType::PARAKEET || - handle->model->get_config().model_type == cactus::engine::Config::ModelType::PARAKEET_TDT; - bool is_gemma4 = handle->model->get_config().model_type == cactus::engine::Config::ModelType::GEMMA4; - - std::vector audio_samples; - if (audio_file_path == nullptr) { - auto waveform_fp32 = cactus::audio::pcm_buffer_to_float_samples(pcm_buffer, pcm_buffer_size); - audio_samples = resample_to_16k_fp32(waveform_fp32, WHISPER_SAMPLE_RATE); - } else { - AudioFP32 audio = load_wav(audio_file_path); - audio_samples = resample_to_16k_fp32(audio.samples, audio.sample_rate); - } - - if (opts.find("\"max_tokens\"") == std::string::npos) { - const float audio_length_sec = static_cast(audio_samples.size()) / static_cast(WHISPER_SAMPLE_RATE); - const float tps = is_parakeet ? 30.0f : (is_gemma4 ? 30.0f : 20.0f); - const size_t estimated = static_cast(audio_length_sec * tps); - options.max_tokens = std::max(estimated, 100); - } - - if (is_gemma4) { - if (audio_samples.empty()) { - handle_error_response("No audio input provided", response_buffer, buffer_size); - cactus::telemetry::recordTranscription(handle->model_name.c_str(), false, 0.0, 0.0, 0.0, 0, 0.0, "No audio input"); - return -1; - } - - const auto& model_config = handle->model->get_config(); - uint32_t audio_token_id = model_config.audio_token_id; - if (audio_token_id == 0) { - CACTUS_LOG_WARN("transcribe", "audio_token_id not set in config, using default 258881"); - audio_token_id = 258881; - } - - auto audio_prep = cactus::audio::preprocess_audio_for_gemma4(audio_samples, model_config); - std::vector& audio_features = audio_prep.features; - size_t num_soft_tokens = audio_prep.num_soft_tokens; - - auto* tokenizer = handle->model->get_tokenizer(); - if (!tokenizer) { - CACTUS_LOG_ERROR("transcribe", "Tokenizer unavailable"); - handle_error_response("Tokenizer unavailable", response_buffer, buffer_size); - cactus::telemetry::recordTranscription(handle->model_name.c_str(), false, 0.0, 0.0, 0.0, 0, 0.0, "Tokenizer unavailable"); - return -1; - } - - std::string task_text = (prompt[0] != '\0') ? std::string(prompt) : "Transcribe the audio."; - auto prefix_tokens = tokenizer->encode("<|turn>user\n" + task_text + "<|audio>"); - auto suffix_tokens = tokenizer->encode("\n<|turn>model\n"); - - std::vector tokens; - tokens.reserve(prefix_tokens.size() + num_soft_tokens + suffix_tokens.size()); - tokens.insert(tokens.end(), prefix_tokens.begin(), prefix_tokens.end()); - for (size_t j = 0; j < num_soft_tokens; j++) - tokens.push_back(audio_token_id); - tokens.insert(tokens.end(), suffix_tokens.begin(), suffix_tokens.end()); - - std::vector> stop_token_sequences = {{ tokenizer->get_eos_token() }}; - auto append_stop = [&](const char* stop_text) { - std::vector seq = tokenizer->encode(stop_text); - if (!seq.empty()) - stop_token_sequences.push_back(std::move(seq)); - }; - append_stop(""); - append_stop(""); - append_stop(""); - - const size_t prompt_token_count = tokens.size(); - double time_to_first_token = 0.0; - size_t completion_tokens = 0; - std::string final_text; - float total_entropy_sum = 0.0f; - float max_token_entropy_norm = 0.0f; - std::vector generated_tokens; - generated_tokens.reserve(options.max_tokens); - - for (size_t i = 0; i < options.max_tokens; ++i) { - if (handle->should_stop) break; - - float token_entropy = 0.0f; - uint32_t next_token = handle->model->decode_with_audio( - tokens, audio_features, - options.temperature, options.top_p, options.top_k, - "", &token_entropy, - options.min_p, options.repetition_penalty - ); - - if (completion_tokens == 0) [[unlikely]] { - auto t_first = std::chrono::high_resolution_clock::now(); - time_to_first_token = std::chrono::duration_cast(t_first - start_time).count() / 1000.0; - } - - total_entropy_sum += token_entropy; - if (token_entropy > max_token_entropy_norm) max_token_entropy_norm = token_entropy; - - generated_tokens.emplace_back(next_token); - if (matches_stop_sequence(generated_tokens, stop_token_sequences)) - break; - - std::string piece = tokenizer->decode({ next_token }); - tokens.emplace_back(next_token); - completion_tokens++; - final_text += piece; - if (callback) callback(piece.c_str(), next_token, user_data); - } - - cactus_reset(model); - - float mean_entropy = completion_tokens > 0 ? total_entropy_sum / static_cast(completion_tokens) : 0.0f; - float confidence = 1.0f - mean_entropy; - - auto end_time = std::chrono::high_resolution_clock::now(); - double total_time_ms = std::chrono::duration_cast(end_time - start_time).count() / 1000.0; - double prefill_tps = time_to_first_token > 0 ? (prompt_token_count * 1000.0) / time_to_first_token : 0.0; - double decode_time_ms = std::max(0.0, total_time_ms - time_to_first_token); - double decode_tps = (completion_tokens > 1 && decode_time_ms > 0.0) ? ((completion_tokens - 1) * 1000.0) / decode_time_ms : 0.0; - - if (!final_text.empty() && final_text[0] == ' ') - final_text.erase(0, 1); - - // Strip thinking channel content: <|channel>thought... - { - auto chan_start = final_text.find("<|channel>"); - auto chan_end = final_text.find(""); - if (chan_start != std::string::npos && chan_end != std::string::npos && chan_end > chan_start) { - size_t after = chan_end + std::string("").length(); - final_text = final_text.substr(after); - if (!final_text.empty() && final_text[0] == ' ') - final_text.erase(0, 1); - } - } - - const bool cloud_handoff = !final_text.empty() && final_text.length() > 5 && - cloud_handoff_threshold > 0.0f && max_token_entropy_norm > cloud_handoff_threshold; - - std::string json = construct_response_json(final_text, {}, time_to_first_token, total_time_ms, prefill_tps, decode_tps, prompt_token_count, completion_tokens, confidence, cloud_handoff); - - if (json.size() >= buffer_size) { - handle_error_response("Response buffer too small", response_buffer, buffer_size); - cactus::telemetry::recordTranscription(handle->model_name.c_str(), false, 0.0, 0.0, 0.0, 0, 0.0, "Response buffer too small"); - return -1; - } - - cactus::telemetry::recordTranscription(handle->model_name.c_str(), true, time_to_first_token, decode_tps, total_time_ms, static_cast(completion_tokens), get_ram_usage_mb(), ""); - std::strcpy(response_buffer, json.c_str()); - return static_cast(json.size()); - } - - auto to_sec = [](size_t samples) { - return static_cast(samples) / static_cast(WHISPER_SAMPLE_RATE); - }; - - struct AudioChunk { - std::vector audio; - float start_sec; - float end_sec; - std::vector> anchors; - }; - std::vector audio_chunks; - - if (options.use_vad) { - auto* vad = static_cast(handle->vad_model.get()); - auto vad_segments = vad->get_speech_timestamps(audio_samples, {}); - audio_chunks.reserve(vad_segments.size()); - - std::vector current; - std::vector> current_anchors; - size_t chunk_start_sample = 0; - size_t chunk_end_sample = 0; - float concat_cursor = 0.0f; - for (const auto& seg : vad_segments) { - size_t end = std::min(seg.end, audio_samples.size()); - if (current.size() + (end - seg.start) > MAX_CHUNK_SAMPLES) { - audio_chunks.emplace_back(std::move(current), to_sec(chunk_start_sample), to_sec(chunk_end_sample), std::move(current_anchors)); - current.clear(); - current_anchors.clear(); - concat_cursor = 0.0f; - } - if (current.empty()) chunk_start_sample = seg.start; - chunk_end_sample = end; - current_anchors.emplace_back(concat_cursor, to_sec(seg.start)); - concat_cursor += to_sec(end - seg.start); - current.insert( - current.end(), - audio_samples.begin() + seg.start, - audio_samples.begin() + end - ); - } - - if (!current.empty()) { - audio_chunks.emplace_back(std::move(current), to_sec(chunk_start_sample), to_sec(chunk_end_sample), std::move(current_anchors)); - } - - if (audio_chunks.empty()) { - CACTUS_LOG_DEBUG("transcribe", "VAD detected only silence, returning empty transcription"); - auto vad_end_time = std::chrono::high_resolution_clock::now(); - double vad_total_time_ms = std::chrono::duration_cast(vad_end_time - start_time).count() / 1000.0; - std::string json = construct_response_json("", {}, 0.0, vad_total_time_ms, 0.0, 0.0, 0, 0, 1.0f); - if (json.size() >= buffer_size) { - handle_error_response("Response buffer too small", response_buffer, buffer_size); - cactus::telemetry::recordTranscription(handle->model_name.c_str(), false, 0.0, 0.0, 0.0, 0, 0.0, "Response buffer too small"); - return -1; - } - cactus::telemetry::recordTranscription(handle->model_name.c_str(), true, 0.0, 0.0, vad_total_time_ms, 0, get_ram_usage_mb(), ""); - std::strcpy(response_buffer, json.c_str()); - return static_cast(json.size()); - } - } else { - audio_chunks.reserve((audio_samples.size() + MAX_CHUNK_SAMPLES - 1) / MAX_CHUNK_SAMPLES); - for (size_t start = 0; start < audio_samples.size(); start += MAX_CHUNK_SAMPLES) { - size_t end = std::min(start + MAX_CHUNK_SAMPLES, audio_samples.size()); - audio_chunks.emplace_back(std::vector(audio_samples.begin() + start, audio_samples.begin() + end), to_sec(start), to_sec(end)); - } - } - - auto cfg = is_parakeet ? get_parakeet_spectrogram_config() : get_whisper_spectrogram_config(); - size_t mel_bins = std::max(1, static_cast(handle->model->get_config().num_mel_bins)); - const bool is_whisper_v3 = is_whisper && mel_bins > 80; - AudioProcessor ap; - if (is_parakeet) { - ap.init_mel_filters(cfg.n_fft / 2 + 1, mel_bins, 0.0f, 8000.0f, WHISPER_SAMPLE_RATE); - } else if (!is_moonshine) { - init_whisper_mel_filters(ap, cfg, mel_bins); - } - - auto* tokenizer = handle->model->get_tokenizer(); - if (!tokenizer) { - CACTUS_LOG_ERROR("transcribe", "Tokenizer unavailable"); - handle_error_response("Tokenizer unavailable", response_buffer, buffer_size); - cactus::telemetry::recordTranscription(handle->model_name.c_str(), false, 0.0, 0.0, 0.0, 0, 0.0, "Tokenizer unavailable"); - return -1; - } - - std::vector initial_tokens = tokenizer->encode( - prompt && prompt[0] != '\0' - ? std::string(prompt) - : (is_whisper ? "<|startoftranscript|>" : std::string()) - ); - if (initial_tokens.empty() && !is_moonshine && !is_parakeet) { - CACTUS_LOG_ERROR("transcribe", "Decoder input tokens empty after encoding prompt"); - handle_error_response("Decoder input tokens empty", response_buffer, buffer_size); - cactus::telemetry::recordTranscription(handle->model_name.c_str(), false, 0.0, 0.0, 0.0, 0, 0.0, "Decoder input tokens empty"); - return -1; - } - - float max_tps = handle->model->get_config().default_max_tps; - if (max_tps < 0) max_tps = 100; - - std::vector> stop_token_sequences = {{ tokenizer->get_eos_token() }}; - auto append_exact_stop_sequence = [&](const char* stop_text) { - std::vector seq = tokenizer->encode(stop_text); - if (!seq.empty() && tokenizer->decode(seq) == stop_text) { - stop_token_sequences.push_back(std::move(seq)); - } - }; - append_exact_stop_sequence("<|endoftext|>"); - append_exact_stop_sequence("<|endoftranscript|>"); - append_exact_stop_sequence(""); - append_exact_stop_sequence(""); - - double time_to_first_token = 0.0; - size_t completion_tokens = 0; - std::string final_text; - float total_entropy_sum = 0.0f; - float max_token_entropy_norm = 0.0f; - - const std::regex whisper_timestamp_re(R"(<\|(\d+(?:\.\d+)?)\|>)"); - std::vector segments; - - auto sop = tokenizer->encode("<|startofprev|>"); - auto sot = tokenizer->encode("<|startoftranscript|>"); - auto zero = tokenizer->encode("<|0.00|>"); - auto notimestamps = tokenizer->encode("<|notimestamps|>"); - const bool has_notimestamps = !notimestamps.empty() && - std::search(initial_tokens.begin(), initial_tokens.end(), - notimestamps.begin(), notimestamps.end()) != initial_tokens.end(); - auto sot_it = std::search(initial_tokens.begin(), initial_tokens.end(), sot.begin(), sot.end()); - const auto sot_begin = sot_it != initial_tokens.end() ? sot_it : initial_tokens.begin(); - - for (auto& audio_chunk : audio_chunks) { - if (handle->should_stop || completion_tokens >= options.max_tokens) break; - - std::vector chunk_audio = std::move(audio_chunk.audio); - const float audio_chunk_length_sec = static_cast(chunk_audio.size()) / static_cast(WHISPER_SAMPLE_RATE); - - std::vector tokens; - if (final_text.empty() || is_parakeet || is_moonshine) { - tokens = initial_tokens; - } else { - size_t word_count = 0, pos = final_text.size(); - while (pos > 0 && word_count < MAX_CONTEXT_WORDS) { - while (pos > 0 && std::isspace((unsigned char)final_text[pos - 1])) --pos; - while (pos > 0 && !std::isspace((unsigned char)final_text[pos - 1])) --pos; - ++word_count; - } - tokens = sop; - auto ctx = tokenizer->encode(final_text.substr(pos)); - tokens.insert(tokens.end(), ctx.begin(), ctx.end()); - tokens.insert(tokens.end(), sot_begin, initial_tokens.end()); - } - - if (is_whisper && !has_notimestamps && !zero.empty() && (tokens.empty() || tokens.back() != zero.back())) { - tokens.insert(tokens.end(), zero.begin(), zero.end()); - } - - if (!is_moonshine) { - if (is_parakeet) { - size_t waveform_samples = chunk_audio.size(); - apply_preemphasis(chunk_audio, 0.97f); - chunk_audio = ap.compute_spectrogram(chunk_audio, cfg); - normalize_parakeet_log_mel(chunk_audio, mel_bins); - size_t valid_frames = waveform_samples / cfg.hop_length; - if (valid_frames == 0) valid_frames = 1; - trim_mel_frames(chunk_audio, mel_bins, valid_frames); - } else { - std::vector mel = ap.compute_spectrogram(chunk_audio, cfg); - chunk_audio = normalize_whisper_mel(mel, mel_bins, is_whisper_v3); - } - } - - if (chunk_audio.empty()) { - CACTUS_LOG_DEBUG("transcribe", "Chunk audio features empty, skipping"); - continue; - } - - CACTUS_LOG_DEBUG("transcribe", "Chunk audio features size: " << chunk_audio.size()); - - size_t chunk_max_tokens = options.max_tokens - completion_tokens; - if (!is_parakeet) { - size_t max_allowed = tokens.size() < WHISPER_MAX_DECODER_POSITIONS ? - WHISPER_MAX_DECODER_POSITIONS - tokens.size() : 0; - if (chunk_max_tokens > max_allowed) chunk_max_tokens = max_allowed; - } - size_t max_tps_tokens = std::max(1, static_cast(audio_chunk_length_sec * max_tps)); - if (chunk_max_tokens > max_tps_tokens) chunk_max_tokens = max_tps_tokens; - - tokens.reserve(tokens.size() + chunk_max_tokens); - std::vector generated_tokens; - generated_tokens.reserve(chunk_max_tokens); - - float segment_start = audio_chunk.start_sec; - float segment_end = audio_chunk.end_sec; - float orig_offset = audio_chunk.start_sec; - size_t anchor_idx = 0; - std::string segment_text; - - for (size_t i = 0; i < chunk_max_tokens; ++i) { - if (handle->should_stop) break; - - float token_entropy = 0.0f; - float tok_time_start = 0.0f; - float tok_time_end = 0.0f; - uint32_t next_token = handle->model->decode_with_audio( - tokens, - chunk_audio, - options.temperature, - options.top_p, - options.top_k, - "", - &token_entropy, - options.min_p, - options.repetition_penalty, - is_parakeet_tdt ? &tok_time_start : nullptr, - is_parakeet_tdt ? &tok_time_end : nullptr - ); - - if (completion_tokens == 0) [[unlikely]] { - auto t_first = std::chrono::high_resolution_clock::now(); - time_to_first_token = std::chrono::duration_cast(t_first - start_time).count() / 1000.0; - } - - total_entropy_sum += token_entropy; - if (token_entropy > max_token_entropy_norm) max_token_entropy_norm = token_entropy; - - generated_tokens.emplace_back(next_token); - if (matches_stop_sequence(generated_tokens, stop_token_sequences)) { - break; - } - - std::string piece = tokenizer->decode({ next_token }); - if (is_terminal_transcription_piece(piece)) { - break; - } - tokens.emplace_back(next_token); - completion_tokens++; - - std::smatch timestamp_match; - if (is_parakeet_tdt) { - while (anchor_idx + 1 < audio_chunk.anchors.size() && audio_chunk.anchors[anchor_idx + 1].first <= tok_time_end) { - ++anchor_idx; - orig_offset = audio_chunk.anchors[anchor_idx].second - audio_chunk.anchors[anchor_idx].first; - } - const bool new_word = !piece.empty() && piece[0] == ' '; - if (new_word && !segment_text.empty()) { - final_text += segment_text; - segments.emplace_back( - segment_start, - segment_end, - segment_text[0] == ' ' - ? (segment_text.erase(0, 1), std::move(segment_text)) - : std::move(segment_text) - ); - segment_text.clear(); - } - if (segment_text.empty()) segment_start = tok_time_start + orig_offset; - segment_text += piece; - segment_end = tok_time_end + orig_offset; - if (callback) callback(piece.c_str(), next_token, user_data); - continue; - } else if (std::regex_match(piece, timestamp_match, whisper_timestamp_re)) { - const float ts_sec = std::stof(timestamp_match[1].str()); - while (anchor_idx + 1 < audio_chunk.anchors.size() && audio_chunk.anchors[anchor_idx + 1].first <= ts_sec) { - ++anchor_idx; - orig_offset = audio_chunk.anchors[anchor_idx].second - audio_chunk.anchors[anchor_idx].first; - } - segment_end = std::min(ts_sec + orig_offset, audio_chunk.end_sec); - if (!segment_text.empty()) { - final_text += segment_text; - segments.emplace_back( - segment_start, - segment_end, - segment_text[0] == ' ' - ? (segment_text.erase(0, 1), std::move(segment_text)) - : std::move(segment_text) - ); - segment_text.clear(); - } - segment_start = segment_end; - continue; - } - - segment_text += piece; - if (callback) callback(piece.c_str(), next_token, user_data); - } - - if (!segment_text.empty()) { - final_text += segment_text; - segments.emplace_back( - segment_start, - segment_end, - segment_text[0] == ' ' - ? (segment_text.erase(0, 1), std::move(segment_text)) - : std::move(segment_text) - ); - } - - cactus_reset(model); - } - - float mean_entropy = completion_tokens > 0 ? total_entropy_sum / static_cast(completion_tokens) : 0.0f; - float confidence = 1.0f - mean_entropy; - - auto end_time = std::chrono::high_resolution_clock::now(); - double total_time_ms = std::chrono::duration_cast(end_time - start_time).count() / 1000.0; - size_t prompt_tokens = initial_tokens.size(); - double prefill_tps = time_to_first_token > 0 ? (prompt_tokens * 1000.0) / time_to_first_token : 0.0; - double decode_time_ms = std::max(0.0, total_time_ms - time_to_first_token); - double decode_tps = (completion_tokens > 1 && decode_time_ms > 0.0) ? ((completion_tokens - 1) * 1000.0) / decode_time_ms : 0.0; - - if (!final_text.empty() && final_text[0] == ' ') final_text.erase(0, 1); - - if (!custom_vocabulary.empty()) { - apply_vocabulary_spelling_correction(final_text, custom_vocabulary); - for (auto& seg : segments) { - apply_vocabulary_spelling_correction(seg.text, custom_vocabulary); - } - } - - const bool cloud_handoff = !final_text.empty() && final_text.length() > 5 && - cloud_handoff_threshold > 0.0f && max_token_entropy_norm > cloud_handoff_threshold; - - std::string json = construct_response_json(final_text, {}, time_to_first_token, total_time_ms, prefill_tps, decode_tps, prompt_tokens, completion_tokens, confidence, cloud_handoff, "", segments); - - if (json.size() >= buffer_size) { - handle_error_response("Response buffer too small", response_buffer, buffer_size); - cactus::telemetry::recordTranscription(handle->model_name.c_str(), false, 0.0, 0.0, 0.0, 0, 0.0, "Response buffer too small"); - return -1; - } - - cactus::telemetry::recordTranscription(handle->model_name.c_str(), true, time_to_first_token, decode_tps, total_time_ms, static_cast(completion_tokens), get_ram_usage_mb(), ""); - - std::strcpy(response_buffer, json.c_str()); - - return static_cast(json.size()); - } - catch (const std::exception& e) { - CACTUS_LOG_ERROR("transcribe", "Exception: " << e.what()); - handle_error_response(e.what(), response_buffer, buffer_size); - cactus::telemetry::recordTranscription(model ? static_cast(model)->model_name.c_str() : nullptr, false, 0.0, 0.0, 0.0, 0, 0.0, e.what()); - return -1; - } - catch (...) { - CACTUS_LOG_ERROR("transcribe", "Unknown exception during transcription"); - handle_error_response("Unknown error in transcribe", response_buffer, buffer_size); - cactus::telemetry::recordTranscription(model ? static_cast(model)->model_name.c_str() : nullptr, false, 0.0, 0.0, 0.0, 0, 0.0, "Unknown error in transcribe"); - return -1; - } -} - -int cactus_detect_language( - cactus_model_t model, - const char* audio_file_path, - char* response_buffer, - size_t buffer_size, - const char* options_json, - const uint8_t* pcm_buffer, - size_t pcm_buffer_size -) { - if (!model) { - std::string error_msg = last_error_message.empty() ? "Model not initialized." : last_error_message; - CACTUS_LOG_ERROR("detect_language", error_msg); - handle_error_response(error_msg, response_buffer, buffer_size); - return -1; - } - if (!response_buffer || buffer_size == 0) { - CACTUS_LOG_ERROR("detect_language", "Invalid parameters: response_buffer or buffer_size"); - handle_error_response("Invalid parameters", response_buffer, buffer_size); - return -1; - } - if (!audio_file_path && (!pcm_buffer || pcm_buffer_size == 0)) { - CACTUS_LOG_ERROR("detect_language", "No audio input provided"); - handle_error_response("Either audio_file_path or pcm_buffer must be provided", response_buffer, buffer_size); - return -1; - } - if (audio_file_path && pcm_buffer && pcm_buffer_size > 0) { - CACTUS_LOG_ERROR("detect_language", "Both audio_file_path and pcm_buffer provided"); - handle_error_response("Cannot provide both audio_file_path and pcm_buffer", response_buffer, buffer_size); - return -1; - } - if (pcm_buffer && pcm_buffer_size > 0 && (pcm_buffer_size < 2 || pcm_buffer_size % 2 != 0)) { - CACTUS_LOG_ERROR("detect_language", "Invalid pcm_buffer_size: " << pcm_buffer_size); - handle_error_response("pcm_buffer_size must be even and at least 2 bytes", response_buffer, buffer_size); - return -1; - } - - try { - auto start_time = std::chrono::high_resolution_clock::now(); - auto* handle = static_cast(model); - std::lock_guard lock(handle->model_mutex); - handle->should_stop = false; - - if (handle->model->get_config().model_type != cactus::engine::Config::ModelType::WHISPER) { - handle_error_response("Language detection currently requires a Whisper model", response_buffer, buffer_size); - return -1; - } - - InferenceOptions options = parse_inference_options_json(options_json ? options_json : ""); - - std::vector audio_buffer; - if (audio_file_path == nullptr) { - const int16_t* pcm_samples = reinterpret_cast(pcm_buffer); - size_t num_samples = pcm_buffer_size / 2; - - std::vector waveform_fp32(num_samples); - for (size_t i = 0; i < num_samples; i++) { - waveform_fp32[i] = static_cast(pcm_samples[i]) / 32768.0f; - } - audio_buffer = resample_to_16k_fp32(waveform_fp32, WHISPER_SAMPLE_RATE); - } else { - AudioFP32 audio = load_wav(audio_file_path); - audio_buffer = resample_to_16k_fp32(audio.samples, audio.sample_rate); - } - - if (options.use_vad && handle->vad_model) { - auto* vad = static_cast(handle->vad_model.get()); - auto segments = vad->get_speech_timestamps(audio_buffer, {}); - - std::vector speech_audio; - for (const auto& segment : segments) { - speech_audio.insert( - speech_audio.end(), - audio_buffer.begin() + segment.start, - audio_buffer.begin() + std::min(segment.end, audio_buffer.size()) - ); - } - audio_buffer = std::move(speech_audio); - } - - if (audio_buffer.empty()) { - handle_error_response("No speech detected in audio input", response_buffer, buffer_size); - return -1; - } - - AudioProcessor ap; - auto cfg = get_whisper_spectrogram_config(); - const size_t mel_bins = std::max(1, static_cast(handle->model->get_config().num_mel_bins)); - init_whisper_mel_filters(ap, cfg, mel_bins); - std::vector mel = ap.compute_spectrogram(audio_buffer, cfg); - std::vector features = normalize_whisper_mel(mel, mel_bins, mel_bins > 80); - if (features.empty()) { - handle_error_response("Computed audio features are empty", response_buffer, buffer_size); - return -1; - } - - auto* tokenizer = handle->model->get_tokenizer(); - if (!tokenizer) { - handle_error_response("Tokenizer unavailable", response_buffer, buffer_size); - return -1; - } - - std::vector decode_tokens = tokenizer->encode("<|startoftranscript|>"); - if (decode_tokens.empty()) { - handle_error_response("Failed to encode Whisper start token", response_buffer, buffer_size); - return -1; - } - - handle->model->reset_cache(); - float entropy = 1.0f; - uint32_t token_id = 0; - std::string token_text; - std::string language; - - // Whisper may emit one or more control tokens before the language token. - // Decode a few initial tokens and select the first valid language token. - constexpr size_t kMaxLanguageProbeSteps = 4; - for (size_t step = 0; step < kMaxLanguageProbeSteps; ++step) { - float step_entropy = 1.0f; - const uint32_t step_token_id = handle->model->decode_with_audio( - decode_tokens, features, 0.0f, 0.0f, 0, "", &step_entropy, - 0.0f, 1.0f - ); - const std::string step_token_text = tokenizer->decode({step_token_id}); - const std::string step_language = extract_whisper_language_code(step_token_text); - - token_id = step_token_id; - token_text = step_token_text; - entropy = step_entropy; - - if (!step_language.empty()) { - language = step_language; - break; - } - - decode_tokens.push_back(step_token_id); - } - handle->model->reset_cache(); - - if (language.empty()) { - language = "unknown"; - } - - float confidence = 1.0f - entropy; - if (confidence < 0.0f) confidence = 0.0f; - if (confidence > 1.0f) confidence = 1.0f; - - auto end_time = std::chrono::high_resolution_clock::now(); - double total_time_ms = std::chrono::duration_cast(end_time - start_time).count() / 1000.0; - - std::ostringstream json; - json << "{"; - json << "\"success\":true,"; - json << "\"error\":null,"; - json << "\"language\":\"" << escape_json_string(language) << "\","; - json << "\"language_token\":\"" << escape_json_string(token_text) << "\","; - json << "\"token_id\":" << token_id << ","; - json << "\"confidence\":" << std::fixed << std::setprecision(4) << confidence << ","; - json << "\"entropy\":" << std::fixed << std::setprecision(4) << entropy << ","; - json << "\"total_time_ms\":" << std::fixed << std::setprecision(2) << total_time_ms << ","; - json << "\"ram_usage_mb\":" << std::fixed << std::setprecision(2) << get_ram_usage_mb(); - json << "}"; - - std::string response = json.str(); - if (response.size() >= buffer_size) { - handle_error_response("Response buffer too small", response_buffer, buffer_size); - return -1; - } - - std::strcpy(response_buffer, response.c_str()); - return static_cast(response.size()); - } catch (const std::exception& e) { - CACTUS_LOG_ERROR("detect_language", "Exception: " << e.what()); - handle_error_response(e.what(), response_buffer, buffer_size); - return -1; - } catch (...) { - CACTUS_LOG_ERROR("detect_language", "Unknown exception during language detection"); - handle_error_response("Unknown error in detect_language", response_buffer, buffer_size); - return -1; - } -} - -} diff --git a/cactus/ffi/cactus_utils.h b/cactus/ffi/cactus_utils.h deleted file mode 100644 index aad98d83c..000000000 --- a/cactus/ffi/cactus_utils.h +++ /dev/null @@ -1,1869 +0,0 @@ -#ifndef CACTUS_UTILS_H -#define CACTUS_UTILS_H - -#include "../engine/engine.h" -#include "../models/model.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef __APPLE__ -#include -#include -#elif defined(_WIN32) -#include -#include -#elif defined(__linux__) || defined(__ANDROID__) -#include -#endif - -inline size_t get_memory_footprint_bytes() { -#ifdef __APPLE__ - task_vm_info_data_t vm_info; - mach_msg_type_number_t count = TASK_VM_INFO_COUNT; - if (task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&vm_info, &count) == KERN_SUCCESS) - return vm_info.phys_footprint; - -#elif defined(_WIN32) - PROCESS_MEMORY_COUNTERS_EX pmc; - if (GetProcessMemoryInfo(GetCurrentProcess(), (PROCESS_MEMORY_COUNTERS*)&pmc, sizeof(pmc))) - return pmc.PrivateUsage; - -#elif defined(__linux__) || defined(__ANDROID__) - std::ifstream statm("/proc/self/statm"); - if (statm.is_open()) { - size_t size, resident; - statm >> size >> resident; - return resident * sysconf(_SC_PAGESIZE); - } -#endif - return 0; -} - -inline double get_ram_usage_mb() { - return get_memory_footprint_bytes() / (1024.0 * 1024.0); -} - -struct CactusModelHandle { - std::unique_ptr model; - std::unique_ptr vad_model; - std::atomic should_stop; - std::vector processed_tokens; - struct ProcessedImage { - std::string path; - long long last_modified_timestamp = 0; - - bool operator==(const ProcessedImage& other) const { - return path == other.path && last_modified_timestamp == other.last_modified_timestamp; - } - }; - - std::vector> processed_images; - std::vector user_audio_counts; - std::mutex model_mutex; - std::string model_name; - std::unique_ptr corpus_index; - std::string corpus_dir; - size_t corpus_embedding_dim = 0; - std::vector> tool_embeddings; - std::vector tool_texts; - - CactusModelHandle() : should_stop(false) {} -}; - -extern std::string last_error_message; - -bool matches_stop_sequence(const std::vector& generated_tokens, - const std::vector>& stop_sequences); - -std::string retrieve_rag_context(CactusModelHandle* handle, const std::string& query); - -namespace cactus { -namespace audio { - -static constexpr size_t WHISPER_TARGET_FRAMES = 3000; -static constexpr int WHISPER_SAMPLE_RATE = 16000; - -inline cactus::engine::AudioProcessor::SpectrogramConfig get_whisper_spectrogram_config() { - cactus::engine::AudioProcessor::SpectrogramConfig cfg{}; - cfg.n_fft = 400; - cfg.frame_length = 400; - cfg.hop_length = 160; - cfg.power = 2.0f; - cfg.center = true; - cfg.pad_mode = "reflect"; - cfg.onesided = true; - cfg.dither = 0.0f; - cfg.mel_floor = 1e-10f; - cfg.log_mel = "log10"; - cfg.reference = 1.0f; - cfg.min_value = 1e-10f; - cfg.remove_dc_offset = true; - return cfg; -} - -inline cactus::engine::AudioProcessor::SpectrogramConfig get_parakeet_spectrogram_config() { - cactus::engine::AudioProcessor::SpectrogramConfig cfg{}; - cfg.n_fft = 512; - cfg.frame_length = 400; - cfg.hop_length = 160; - cfg.power = 2.0f; - cfg.center = true; - cfg.pad_mode = "constant"; - cfg.onesided = true; - cfg.dither = 0.0f; - cfg.mel_floor = 5.960464477539063e-08f; // 2^-24 guard value used by HF Parakeet. - cfg.log_mel = "log"; - cfg.reference = 1.0f; - cfg.min_value = 1e-10f; - cfg.remove_dc_offset = false; - cfg.hann_periodic = false; - return cfg; -} - -inline cactus::engine::AudioProcessor::SpectrogramConfig get_htk_spectrogram_config() { - cactus::engine::AudioProcessor::SpectrogramConfig cfg{}; - cfg.n_fft = 321; - cfg.frame_length = 320; - cfg.fft_override = 1024; - cfg.hop_length = 160; - cfg.power = 1.0f; - cfg.center = false; - cfg.pad_mode = "constant"; - cfg.onesided = true; - cfg.dither = 0.0f; - cfg.mel_floor = 0.001f; - cfg.log_mel = "log"; - cfg.reference = 1.0f; - cfg.min_value = 0.001f; - cfg.remove_dc_offset = false; - cfg.hann_periodic = true; - return cfg; -} - -inline cactus::engine::AudioProcessor::SpectrogramConfig get_gemma4_audio_spectrogram_config( - const cactus::engine::Config& model_config) { - auto cfg = get_htk_spectrogram_config(); - cfg.fft_override = model_config.audio_fft_length; - cfg.mel_floor_additive = true; - return cfg; -} - -inline cactus::engine::AudioProcessor::SpectrogramConfig get_wespeaker_spectrogram_config() { - cactus::engine::AudioProcessor::SpectrogramConfig cfg{}; - cfg.n_fft = 512; - cfg.frame_length = 400; - cfg.hop_length = 160; - cfg.power = 2.0f; - cfg.center = false; - cfg.pad_mode = "constant"; - cfg.onesided = true; - cfg.dither = 0.0f; - cfg.mel_floor = 1.1754944e-38f; - cfg.log_mel = "log"; - cfg.reference = 1.0f; - cfg.min_value = 1.1754944e-38f; - cfg.remove_dc_offset = true; - cfg.preemphasis = 0.97f; - cfg.hann_periodic = false; - cfg.window_a0 = 0.54f; - return cfg; -} - -// Whisper v1/v2: 80 mel bins, HTK. Whisper v3: 128 mel bins, Slaney, 512-FFT, no DC removal. -inline void init_whisper_mel_filters(cactus::engine::AudioProcessor& ap, - cactus::engine::AudioProcessor::SpectrogramConfig& cfg, - size_t mel_bins) { - const size_t num_mel_filters = std::max(1, mel_bins); - const bool is_v3 = mel_bins > 80; - if (is_v3) { - cfg.fft_override = 512; - cfg.remove_dc_offset = false; - } - const size_t fft_len = cfg.fft_override > 0 ? cfg.fft_override : cfg.n_fft; - const size_t num_frequency_bins = fft_len / 2 + 1; - if (is_v3) { - ap.init_mel_filters(num_frequency_bins, num_mel_filters, 0.0f, 8000.0f, - WHISPER_SAMPLE_RATE, "slaney", "slaney"); - } else { - ap.init_mel_filters(num_frequency_bins, num_mel_filters, 0.0f, 8000.0f, - WHISPER_SAMPLE_RATE); - } -} - -// use_mel_floor_padding=true pads short audio with the normalized mel floor (required for v3). -inline std::vector normalize_whisper_mel(std::vector& mel, size_t n_mels, - bool use_mel_floor_padding = false) { - if (mel.empty() || n_mels == 0) return mel; - size_t n_frames = mel.size() / n_mels; - - float max_val = -std::numeric_limits::infinity(); - for (float v : mel) if (v > max_val) max_val = v; - - float min_allowed = max_val - 8.0f; - for (float& v : mel) { - if (v < min_allowed) v = min_allowed; - v = (v + 4.0f) * 0.25f; - } - - if (n_frames != WHISPER_TARGET_FRAMES) { - float pad_val = use_mel_floor_padding ? (min_allowed + 4.0f) * 0.25f : 0.0f; - std::vector fixed(n_mels * WHISPER_TARGET_FRAMES, pad_val); - size_t copy_frames = std::min(n_frames, WHISPER_TARGET_FRAMES); - for (size_t m = 0; m < n_mels; ++m) { - const float* src = &mel[m * n_frames]; - float* dst = &fixed[m * WHISPER_TARGET_FRAMES]; - std::copy(src, src + copy_frames, dst); - } - return fixed; - } - return std::move(mel); -} - -inline std::vector transpose_mel_to_frame_major(const std::vector& mel, - size_t num_mels, size_t num_frames) { - std::vector transposed(num_frames * num_mels); - for (size_t m = 0; m < num_mels; m++) { - for (size_t t = 0; t < num_frames; t++) { - transposed[t * num_mels + m] = mel[m * num_frames + t]; - } - } - return transposed; -} - -inline void apply_preemphasis(std::vector& waveform, float coefficient = 0.97f) { - if (waveform.size() < 2 || coefficient == 0.0f) { - return; - } - for (size_t i = waveform.size() - 1; i > 0; --i) { - waveform[i] -= coefficient * waveform[i - 1]; - } -} - -inline void normalize_parakeet_log_mel(std::vector& mel, size_t num_mels, float epsilon = 1e-5f) { - if (mel.empty() || num_mels == 0 || (mel.size() % num_mels) != 0) { - return; - } - const size_t num_frames = mel.size() / num_mels; - if (num_frames == 0) { - return; - } - - for (size_t m = 0; m < num_mels; ++m) { - const size_t base = m * num_frames; - float mean = 0.0f; - for (size_t t = 0; t < num_frames; ++t) { - mean += mel[base + t]; - } - mean /= static_cast(num_frames); - - float variance = 0.0f; - for (size_t t = 0; t < num_frames; ++t) { - const float d = mel[base + t] - mean; - variance += d * d; - } - const float denom = static_cast(std::max(1, num_frames - 1)); - const float inv_std = 1.0f / std::sqrt((variance / denom) + epsilon); - for (size_t t = 0; t < num_frames; ++t) { - mel[base + t] = (mel[base + t] - mean) * inv_std; - } - } -} - -inline void trim_mel_frames(std::vector& mel, size_t num_mels, size_t valid_frames) { - if (mel.empty() || num_mels == 0 || (mel.size() % num_mels) != 0) { - return; - } - size_t total_frames = mel.size() / num_mels; - if (valid_frames == 0 || valid_frames >= total_frames) { - return; - } - std::vector trimmed(num_mels * valid_frames); - for (size_t m = 0; m < num_mels; ++m) { - const float* src = &mel[m * total_frames]; - float* dst = &trimmed[m * valid_frames]; - std::copy(src, src + valid_frames, dst); - } - mel.swap(trimmed); -} - -struct AudioPreprocessResult { - std::vector features; - size_t num_frames = 0; - size_t num_soft_tokens = 0; -}; - -inline AudioPreprocessResult preprocess_audio_for_gemma4( - std::vector audio_samples, - const cactus::engine::Config& model_config -) { - AudioPreprocessResult result; - if (audio_samples.empty()) return result; - - size_t pad_amt = 320 - (audio_samples.size() % 320); - if (pad_amt < 320) - audio_samples.resize(audio_samples.size() + pad_amt, 0.0f); - - size_t mel_bins = model_config.audio_input_feat_size; - auto cfg = get_gemma4_audio_spectrogram_config(model_config); - - size_t semicausal_pad = cfg.frame_length / 2; - audio_samples.insert(audio_samples.begin(), semicausal_pad, 0.0f); - - cactus::engine::AudioProcessor ap; - size_t fft_for_mel = cfg.fft_override > 0 ? cfg.fft_override : cfg.n_fft; - ap.init_mel_filters(fft_for_mel / 2 + 1, mel_bins, 0.0f, 8000.0f, 16000, - nullptr, "htk"); - std::vector mel = ap.compute_spectrogram(audio_samples, cfg); - - result.num_frames = mel.size() / mel_bins; - result.features = transpose_mel_to_frame_major(mel, mel_bins, result.num_frames); - - size_t after_stage1 = (result.num_frames + 1) / 2; - result.num_soft_tokens = (after_stage1 + 1) / 2; - - return result; -} - -inline std::vector pcm_buffer_to_float_samples( - const uint8_t* pcm_buffer, size_t pcm_buffer_size -) { - const int16_t* pcm_samples = reinterpret_cast(pcm_buffer); - size_t num_samples = pcm_buffer_size / 2; - std::vector waveform_fp32(num_samples); - constexpr float inv_32768 = 1.0f / 32768.0f; - for (size_t i = 0; i < num_samples; i++) - waveform_fp32[i] = static_cast(pcm_samples[i]) * inv_32768; - return waveform_fp32; -} - -} // namespace audio -} // namespace cactus - -namespace cactus { -namespace ffi { - -inline bool env_flag_enabled(const char* key) { - const char* value = std::getenv(key); - return value && value[0] != '\0' && !(value[0] == '0' && value[1] == '\0'); -} - -inline std::string generateUUID() { -#ifdef __APPLE__ - uuid_t uuid; - uuid_generate_random(uuid); - char uuid_str[37]; - uuid_unparse_lower(uuid, uuid_str); - return std::string(uuid_str); -#else - static std::random_device rd; - static std::mt19937 gen(rd()); - static std::uniform_int_distribution<> dis(0, 15); - static std::uniform_int_distribution<> dis2(8, 11); - - std::stringstream ss; - ss << std::hex; - for (int i = 0; i < 8; i++) ss << dis(gen); - ss << "-"; - for (int i = 0; i < 4; i++) ss << dis(gen); - ss << "-4"; - for (int i = 0; i < 3; i++) ss << dis(gen); - ss << "-"; - ss << dis2(gen); - for (int i = 0; i < 3; i++) ss << dis(gen); - ss << "-"; - for (int i = 0; i < 12; i++) ss << dis(gen); - return ss.str(); -#endif -} - -struct ToolFunction { - std::string name; - std::string description; - std::unordered_map parameters; -}; - -struct InferenceOptions { - float temperature = 0.0f; - float top_p = 0.0f; - float min_p = 0.15f; - float repetition_penalty = 1.1f; - float confidence_threshold = -1.0f; - size_t top_k = 0; - size_t max_tokens = 100; - size_t tool_rag_top_k = 2; - size_t cloud_timeout_ms = 15000; - std::vector stop_sequences; - bool force_tools = false; - bool include_stop_sequences = false; - bool use_vad = true; - bool telemetry_enabled = true; - bool auto_handoff = true; - bool handoff_with_images = true; - bool enable_thinking_if_supported = false; -}; - -} // namespace ffi -} // namespace cactus - -std::vector select_relevant_tools( - CactusModelHandle* handle, - const std::string& query, - const std::vector& all_tools, - size_t top_k); - -#include "gemma_tools.h" - -namespace cactus { -namespace ffi { - -inline std::string escape_json_string(const std::string& s) { - std::ostringstream o; - for (char c : s) { - if (c == '"') o << "\\\""; - else if (c == '\n') o << "\\n"; - else if (c == '\r') o << "\\r"; - else if (c == '\t') o << "\\t"; - else if (c == '\\') o << "\\\\"; - else o << c; - } - return o.str(); -} - - -inline std::string trim_string(const std::string& s) { - size_t start = 0; - while (start < s.size() && std::isspace(static_cast(s[start]))) ++start; - size_t end = s.size(); - while (end > start && std::isspace(static_cast(s[end - 1]))) --end; - return s.substr(start, end - start); -} - -inline size_t find_matching_delimiter(const std::string& s, size_t pos, char open, char close) { - int depth = 1; - pos++; - while (pos < s.length() && depth > 0) { - if (s[pos] == open) depth++; - else if (s[pos] == close) depth--; - else if (s[pos] == '"') { - pos++; - while (pos < s.length() && s[pos] != '"') { - if (s[pos] == '\\') pos++; - pos++; - } - } - pos++; - } - return pos; -} - -inline std::string env_or_default(const char* key, const char* fallback) { - const char* v = std::getenv(key); - if (v && v[0] != '\0') return std::string(v); - return std::string(fallback); -} - -inline std::string json_string_field(const std::string& json, const std::string& key) { - std::string pattern = "\"" + key + "\":"; - size_t pos = json.find(pattern); - if (pos == std::string::npos) return {}; - - size_t i = pos + pattern.size(); - while (i < json.size() && std::isspace(static_cast(json[i]))) i++; - if (i >= json.size() || json[i] != '"') return {}; - ++i; - - std::string out; - out.reserve(128); - while (i < json.size()) { - char c = json[i++]; - if (c == '"') return out; - if (c == '\\' && i < json.size()) { - char e = json[i++]; - switch (e) { - case '"': out.push_back('"'); break; - case '\\': out.push_back('\\'); break; - case '/': out.push_back('/'); break; - case 'b': out.push_back('\b'); break; - case 'f': out.push_back('\f'); break; - case 'n': out.push_back('\n'); break; - case 'r': out.push_back('\r'); break; - case 't': out.push_back('\t'); break; - default: out.push_back(e); break; - } - continue; - } - out.push_back(c); - } - return {}; -} - -inline std::string json_array_field(const std::string& json, const std::string& key) { - std::string pattern = "\"" + key + "\":"; - size_t pos = json.find(pattern); - if (pos == std::string::npos) return "[]"; - size_t start = pos + pattern.size(); - while (start < json.size() && std::isspace(static_cast(json[start]))) ++start; - if (start >= json.size() || json[start] != '[') return "[]"; - - int depth = 1; - size_t end = start + 1; - while (end < json.size() && depth > 0) { - if (json[end] == '[') depth++; - else if (json[end] == ']') depth--; - end++; - } - return json.substr(start, end - start); -} - -inline std::vector split_json_array(const std::string& array_json) { - std::vector out; - if (array_json.size() < 2 || array_json.front() != '[' || array_json.back() != ']') return out; - - size_t i = 1; - while (i + 1 < array_json.size()) { - while (i + 1 < array_json.size() && - (std::isspace(static_cast(array_json[i])) || array_json[i] == ',')) i++; - if (i + 1 >= array_json.size() || array_json[i] != '{') break; - - size_t start = i; - int depth = 0; - bool in_str = false; - bool esc = false; - for (; i < array_json.size(); ++i) { - char c = array_json[i]; - if (in_str) { - if (esc) esc = false; - else if (c == '\\') esc = true; - else if (c == '"') in_str = false; - continue; - } - if (c == '"') { in_str = true; continue; } - if (c == '{') depth++; - if (c == '}') { - depth--; - if (depth == 0) { - out.push_back(array_json.substr(start, i - start + 1)); - i++; - break; - } - } - } - } - return out; -} - -inline std::string serialize_tools_json(const std::vector& tools) { - if (tools.empty()) return ""; - std::ostringstream oss; - oss << "["; - for (size_t i = 0; i < tools.size(); ++i) { - if (i > 0) oss << ","; - oss << "{\"type\":\"function\",\"function\":{"; - oss << "\"name\":\"" << escape_json_string(tools[i].name) << "\","; - oss << "\"description\":\"" << escape_json_string(tools[i].description) << "\""; - auto it = tools[i].parameters.find("schema"); - if (it != tools[i].parameters.end()) { - oss << ",\"parameters\":" << it->second; - } - oss << "}}"; - } - oss << "]"; - return oss.str(); -} - -namespace json_sorted { - -inline void skip_ws(const std::string& s, size_t& p) { - while (p < s.size() && std::isspace(static_cast(s[p]))) p++; -} - -inline std::string parse_string(const std::string& s, size_t& p) { - std::string r = "\""; - p++; - while (p < s.size()) { - if (s[p] == '\\') { - r += s[p++]; - if (p < s.size()) r += s[p++]; - } else if (s[p] == '"') { - r += '"'; - p++; - return r; - } else { - r += s[p++]; - } - } - return r; -} - -inline std::string parse_value(const std::string& s, size_t& p); - -inline std::string parse_object(const std::string& s, size_t& p) { - p++; - std::map entries; - skip_ws(s, p); - while (p < s.size() && s[p] != '}') { - if (s[p] == ',') { p++; skip_ws(s, p); continue; } - std::string key = parse_string(s, p); - skip_ws(s, p); - if (p < s.size() && s[p] == ':') p++; - skip_ws(s, p); - std::string val = parse_value(s, p); - entries[key] = val; - skip_ws(s, p); - } - if (p < s.size()) p++; - std::string r = "{"; - bool first = true; - for (const auto& kv : entries) { - if (!first) r += ", "; - r += kv.first + ": " + kv.second; - first = false; - } - r += "}"; - return r; -} - -inline std::string parse_array(const std::string& s, size_t& p) { - p++; - std::vector items; - skip_ws(s, p); - while (p < s.size() && s[p] != ']') { - if (s[p] == ',') { p++; skip_ws(s, p); continue; } - items.push_back(parse_value(s, p)); - skip_ws(s, p); - } - if (p < s.size()) p++; - std::string r = "["; - for (size_t i = 0; i < items.size(); i++) { - if (i > 0) r += ", "; - r += items[i]; - } - r += "]"; - return r; -} - -inline std::string parse_value(const std::string& s, size_t& p) { - skip_ws(s, p); - if (p >= s.size()) return ""; - if (s[p] == '"') return parse_string(s, p); - if (s[p] == '{') return parse_object(s, p); - if (s[p] == '[') return parse_array(s, p); - size_t start = p; - while (p < s.size() && s[p] != ',' && s[p] != '}' && s[p] != ']' && !std::isspace(static_cast(s[p]))) p++; - return s.substr(start, p - start); -} - -inline std::string reformat(const std::string& json) { - size_t p = 0; - return parse_value(json, p); -} - -} // namespace json_sorted - -inline std::string serialize_tools_for_template(const std::vector& tools) { - if (tools.empty()) return ""; - std::string result; - for (const auto& tool : tools) { - std::map func_fields; - func_fields["\"description\""] = "\"" + escape_json_string(tool.description) + "\""; - func_fields["\"name\""] = "\"" + escape_json_string(tool.name) + "\""; - auto it = tool.parameters.find("schema"); - if (it != tool.parameters.end()) { - func_fields["\"parameters\""] = json_sorted::reformat(it->second); - } - std::string func_json = "{"; - bool first = true; - for (const auto& kv : func_fields) { - if (!first) func_json += ", "; - func_json += kv.first + ": " + kv.second; - first = false; - } - func_json += "}"; - result += "\n{\"function\": " + func_json + ", \"type\": \"function\"}"; - } - return result; -} - -inline void handle_error_response(const std::string& error_message, char* response_buffer, size_t buffer_size) { - std::ostringstream json; - json << "{"; - json << "\"success\":false,"; - json << "\"error\":\"" << escape_json_string(error_message) << "\","; - json << "\"cloud_handoff\":false,"; - json << "\"response\":null,"; - json << "\"function_calls\":[],"; - json << "\"confidence\":0.0,"; - json << "\"time_to_first_token_ms\":0.0,"; - json << "\"total_time_ms\":0.0,"; - json << "\"prefill_tps\":0.0,"; - json << "\"decode_tps\":0.0,"; - json << "\"ram_usage_mb\":" << std::fixed << std::setprecision(2) << get_ram_usage_mb() << ","; - json << "\"prefill_tokens\":0,"; - json << "\"decode_tokens\":0,"; - json << "\"total_tokens\":0"; - json << "}"; - std::string error_json = json.str(); - if (response_buffer && error_json.length() < buffer_size) { - std::strcpy(response_buffer, error_json.c_str()); - } -} - -inline std::vector parse_messages_json(const std::string& json, - std::vector& out_image_paths, - std::vector* out_audio_paths = nullptr) { - std::vector messages; - out_image_paths.clear(); - if (out_audio_paths) out_audio_paths->clear(); - - size_t pos = json.find('['); - if (pos == std::string::npos) { - throw std::runtime_error("Invalid JSON: expected array"); - } - - pos = json.find('{', pos); - while (pos != std::string::npos) { - cactus::engine::ChatMessage msg; - - size_t obj_start = pos; - int brace_count = 1; - size_t obj_end = obj_start + 1; - while (obj_end < json.length() && brace_count > 0) { - if (json[obj_end] == '{') brace_count++; - else if (json[obj_end] == '}') brace_count--; - obj_end++; - } - - size_t role_pos = json.find("\"role\"", pos); - if (role_pos == std::string::npos || role_pos >= obj_end) break; - - size_t role_start = json.find('"', role_pos + 6) + 1; - size_t role_end = json.find('"', role_start); - msg.role = json.substr(role_start, role_end - role_start); - - size_t content_pos = json.find("\"content\"", role_end); - if (content_pos != std::string::npos && content_pos < obj_end) { - size_t content_start = json.find('"', content_pos + 9) + 1; - size_t content_end = content_start; - - while (content_end < json.length()) { - content_end = json.find('"', content_end); - if (content_end == std::string::npos) break; - if (json[content_end - 1] != '\\') break; - content_end++; - } - - msg.content = json.substr(content_start, content_end - content_start); - - size_t escape_pos = 0; - while ((escape_pos = msg.content.find("\\n", escape_pos)) != std::string::npos) { - msg.content.replace(escape_pos, 2, "\n"); - escape_pos += 1; - } - escape_pos = 0; - while ((escape_pos = msg.content.find("\\\"", escape_pos)) != std::string::npos) { - msg.content.replace(escape_pos, 2, "\""); - escape_pos += 1; - } - } - - auto parse_path_array = [&](const char* key, std::vector& dest, - std::vector* out_paths) { - size_t key_pos = json.find(key, pos); - if (key_pos == std::string::npos || key_pos >= obj_end) return; - size_t array_start = json.find('[', key_pos); - if (array_start == std::string::npos || array_start >= obj_end) return; - size_t array_end = json.find(']', array_start); - if (array_end == std::string::npos || array_end >= obj_end) return; - size_t cur = array_start; - while (true) { - cur = json.find('"', cur + 1); - if (cur == std::string::npos || cur >= array_end) break; - size_t str_start = cur + 1; - size_t str_end = json.find('"', str_start); - if (str_end == std::string::npos || str_end > array_end) break; - std::string path = std::filesystem::absolute( - std::filesystem::path(json.substr(str_start, str_end - str_start))).string(); - dest.push_back(path); - if (out_paths) out_paths->push_back(path); - cur = str_end; - } - }; - - parse_path_array("\"images\"", msg.images, &out_image_paths); - parse_path_array("\"audio\"", msg.audio, out_audio_paths); - - if (msg.role == "tool") { - size_t name_pos = json.find("\"name\"", obj_start); - if (name_pos != std::string::npos && name_pos < obj_end) { - size_t name_quote = json.find('"', name_pos + 6); - if (name_quote != std::string::npos && name_quote < obj_end) { - size_t name_start = name_quote + 1; - size_t name_end = json.find('"', name_start); - if (name_end != std::string::npos && name_end < obj_end) { - msg.name = json.substr(name_start, name_end - name_start); - } - } - } - } - - size_t tool_calls_pos = json.find("\"tool_calls\"", obj_start); - if (tool_calls_pos != std::string::npos && tool_calls_pos < obj_end) { - size_t tool_calls_arr_start = json.find('[', tool_calls_pos); - if (tool_calls_arr_start != std::string::npos && tool_calls_arr_start < obj_end) { - size_t tool_calls_arr_end = find_matching_delimiter(json, tool_calls_arr_start, '[', ']'); - - size_t search_pos = tool_calls_arr_start; - while (true) { - size_t func_pos = json.find("\"function\"", search_pos); - if (func_pos == std::string::npos || func_pos >= tool_calls_arr_end) break; - - size_t func_obj_start = json.find('{', func_pos + 10); - if (func_obj_start == std::string::npos || func_obj_start >= tool_calls_arr_end) break; - - size_t func_obj_end = find_matching_delimiter(json, func_obj_start, '{', '}'); - - cactus::engine::ToolCallInfo tool_call; - - size_t fn_name_pos = json.find("\"name\"", func_obj_start); - if (fn_name_pos != std::string::npos && fn_name_pos < func_obj_end) { - size_t fn_name_quote = json.find('"', fn_name_pos + 6); - if (fn_name_quote != std::string::npos && fn_name_quote < func_obj_end) { - size_t fn_name_start = fn_name_quote + 1; - size_t fn_name_end = json.find('"', fn_name_start); - if (fn_name_end != std::string::npos && fn_name_end < func_obj_end) { - tool_call.name = json.substr(fn_name_start, fn_name_end - fn_name_start); - } - } - } - - size_t args_pos = json.find("\"arguments\"", func_obj_start); - if (args_pos != std::string::npos && args_pos < func_obj_end) { - size_t colon_pos = json.find(':', args_pos + 11); - if (colon_pos != std::string::npos && colon_pos < func_obj_end) { - size_t args_start = colon_pos + 1; - while (args_start < json.length() && std::isspace(static_cast(json[args_start]))) args_start++; - - if (args_start < func_obj_end && json[args_start] == '{') { - size_t args_end = find_matching_delimiter(json, args_start, '{', '}'); - tool_call.arguments = json.substr(args_start, args_end - args_start); - } else if (args_start < func_obj_end && json[args_start] == '"') { - size_t str_start = args_start + 1; - size_t str_end = str_start; - while (str_end < json.length() && json[str_end] != '"') { - if (json[str_end] == '\\') str_end++; - str_end++; - } - tool_call.arguments = json.substr(str_start, str_end - str_start); - } - } - } - - if (!tool_call.name.empty()) { - msg.tool_calls.push_back(tool_call); - } - search_pos = func_obj_end; - } - } - } - - messages.push_back(msg); - - pos = json.find('{', obj_end); - } - - return messages; -} - -inline std::vector parse_tools_json(const std::string& json) { - std::vector tools; - - if (json.empty()) return tools; - - size_t pos = json.find('['); - if (pos == std::string::npos) return tools; - - pos = json.find("\"function\"", pos); - while (pos != std::string::npos) { - ToolFunction tool; - size_t next_search = pos + 1; - - size_t name_pos = json.find("\"name\"", pos); - if (name_pos != std::string::npos) { - size_t name_start = json.find('"', name_pos + 6) + 1; - size_t name_end = json.find('"', name_start); - tool.name = json.substr(name_start, name_end - name_start); - } - - size_t desc_pos = json.find("\"description\"", pos); - if (desc_pos != std::string::npos) { - size_t desc_start = json.find('"', desc_pos + 13) + 1; - size_t desc_end = json.find('"', desc_start); - tool.description = json.substr(desc_start, desc_end - desc_start); - } - - size_t params_pos = json.find("\"parameters\"", pos); - if (params_pos != std::string::npos) { - size_t params_start = json.find('{', params_pos); - if (params_start != std::string::npos) { - int brace_count = 1; - size_t params_end = params_start + 1; - while (params_end < json.length() && brace_count > 0) { - if (json[params_end] == '{') brace_count++; - else if (json[params_end] == '}') brace_count--; - params_end++; - } - tool.parameters["schema"] = json.substr(params_start, params_end - params_start); - next_search = params_end; - } - } - - if (!tool.name.empty()) { - tools.push_back(tool); - } - - pos = json.find("\"function\"", next_search); - } - - return tools; -} - -inline bool try_parse_json_float(const std::string& json, const std::string& key, float& out_value) { - std::string pattern = "\"" + key + "\":"; - size_t pos = json.find(pattern); - if (pos == std::string::npos) return false; - - size_t start = pos + pattern.size(); - while (start < json.size() && std::isspace(static_cast(json[start]))) ++start; - - size_t end = start; - while (end < json.size() && std::string(",}] \t\n\r").find(json[end]) == std::string::npos) ++end; - - try { - out_value = std::stof(json.substr(start, end - start)); - return true; - } catch (...) { - return false; - } -} - -inline std::vector parse_json_string_array_field(const std::string& json, const std::string& key) { - std::vector out; - std::string pattern = "\"" + key + "\":"; - size_t pos = json.find(pattern); - if (pos == std::string::npos) return out; - - size_t start = pos + pattern.size(); - while (start < json.size() && std::isspace(static_cast(json[start]))) ++start; - if (start >= json.size() || json[start] != '[') return out; - - int depth = 1; - bool in_string = false; - bool escaped = false; - size_t end = start + 1; - - while (end < json.size() && depth > 0) { - char c = json[end]; - if (in_string) { - if (escaped) escaped = false; - else if (c == '\\') escaped = true; - else if (c == '"') in_string = false; - } else { - if (c == '"') in_string = true; - else if (c == '[') depth++; - else if (c == ']') depth--; - } - ++end; - } - - if (depth != 0) return out; - const std::string array_json = json.substr(start, end - start); - if (array_json.size() < 2 || array_json.front() != '[' || array_json.back() != ']') return out; - - size_t i = 1; - while (i + 1 < array_json.size()) { - while (i + 1 < array_json.size() && - (std::isspace(static_cast(array_json[i])) || array_json[i] == ',')) { - ++i; - } - if (i + 1 >= array_json.size() || array_json[i] == ']') break; - if (array_json[i] != '"') break; - - ++i; - std::string value; - bool escaped = false; - while (i < array_json.size()) { - char c = array_json[i++]; - if (escaped) { - switch (c) { - case '"': value.push_back('"'); break; - case '\\': value.push_back('\\'); break; - case '/': value.push_back('/'); break; - case 'b': value.push_back('\b'); break; - case 'f': value.push_back('\f'); break; - case 'n': value.push_back('\n'); break; - case 'r': value.push_back('\r'); break; - case 't': value.push_back('\t'); break; - default: value.push_back(c); break; - } - escaped = false; - continue; - } - if (c == '\\') { - escaped = true; - continue; - } - if (c == '"') { - out.push_back(value); - break; - } - value.push_back(c); - } - } - - return out; -} - -inline void parse_custom_vocabulary_options(const std::string& json, - std::vector& custom_vocabulary, - float& vocabulary_boost) { - custom_vocabulary.clear(); - vocabulary_boost = 5.0f; - if (json.empty()) return; - - float parsed_boost = vocabulary_boost; - if (try_parse_json_float(json, "vocabulary_boost", parsed_boost)) { - vocabulary_boost = std::clamp(parsed_boost, 0.0f, 20.0f); - } - - custom_vocabulary = parse_json_string_array_field(json, "custom_vocabulary"); -} - -inline std::unordered_map build_token_bias_map(const std::vector>& tokenized_entries, - float vocabulary_boost) { - std::unordered_map vocab_bias; - const float clamped_boost = std::clamp(vocabulary_boost, 0.0f, 20.0f); - if (clamped_boost == 0.0f) return vocab_bias; - - for (const auto& token_ids : tokenized_entries) { - for (uint32_t token_id : token_ids) { - float& entry = vocab_bias[token_id]; - if (entry < clamped_boost) { - entry = clamped_boost; - } - } - } - - return vocab_bias; -} - -inline std::unordered_map build_custom_vocabulary_bias(cactus::engine::Tokenizer* tokenizer, - const std::vector& custom_vocabulary, - float vocabulary_boost) { - if (!tokenizer || custom_vocabulary.empty()) return {}; - std::vector> tokenized_entries; - tokenized_entries.reserve(custom_vocabulary.size()); - - for (const auto& word : custom_vocabulary) { - if (word.empty()) continue; - tokenized_entries.push_back(tokenizer->encode(word)); - } - - return build_token_bias_map(tokenized_entries, vocabulary_boost); -} - -inline void apply_custom_vocabulary_options(cactus::engine::Model* model, const std::string& json) { - if (!model) return; - - std::vector custom_vocabulary; - float vocabulary_boost = 5.0f; - parse_custom_vocabulary_options(json, custom_vocabulary, vocabulary_boost); - model->set_vocab_bias(build_custom_vocabulary_bias(model->get_tokenizer(), custom_vocabulary, vocabulary_boost)); -} - -inline size_t levenshtein_ci(const std::string& a, const std::string& b) { - const size_t m = a.size(), n = b.size(); - std::vector prev(n + 1), curr(n + 1); - for (size_t j = 0; j <= n; ++j) prev[j] = j; - for (size_t i = 1; i <= m; ++i) { - curr[0] = i; - for (size_t j = 1; j <= n; ++j) { - const bool match = std::tolower(static_cast(a[i - 1])) == - std::tolower(static_cast(b[j - 1])); - curr[j] = std::min({prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + (match ? 0 : 1)}); - } - std::swap(prev, curr); - } - return prev[n]; -} - -inline std::string collapse_spaces(const std::string& s) { - std::string out; - out.reserve(s.size()); - for (char c : s) { - if (c != ' ') out += c; - } - return out; -} - -inline void apply_vocabulary_spelling_correction( - std::string& text, - const std::vector& custom_vocabulary) -{ - if (custom_vocabulary.empty() || text.empty()) return; - - struct VocabEntry { - const std::string* original; - std::string collapsed; - }; - std::vector vocab_entries; - vocab_entries.reserve(custom_vocabulary.size()); - for (const auto& v : custom_vocabulary) { - vocab_entries.push_back({&v, collapse_spaces(v)}); - } - - struct Token { std::string text; bool is_word; }; - std::vector tokens; - size_t pos = 0; - while (pos < text.size()) { - if (std::isalnum(static_cast(text[pos])) || - text[pos] == '\'' || text[pos] == '-') { - size_t start = pos; - while (pos < text.size() && (std::isalnum(static_cast(text[pos])) || - text[pos] == '\'' || text[pos] == '-')) { - ++pos; - } - tokens.push_back({text.substr(start, pos - start), true}); - } else { - size_t start = pos; - while (pos < text.size() && !std::isalnum(static_cast(text[pos])) && - text[pos] != '\'' && text[pos] != '-') { - ++pos; - } - tokens.push_back({text.substr(start, pos - start), false}); - } - } - - std::vector word_indices; - for (size_t i = 0; i < tokens.size(); ++i) { - if (tokens[i].is_word) word_indices.push_back(i); - } - - std::vector consumed(tokens.size(), false); - - auto strip_suffix = [](const std::string& word) -> std::pair { - if (word.size() >= 3 && word.substr(word.size() - 2) == "'s") { - return {word.substr(0, word.size() - 2), "'s"}; - } - if (word.size() >= 3 && word.substr(word.size() - 2) == "'t") { - return {word.substr(0, word.size() - 2), "'t"}; - } - if (word.size() >= 4 && word.back() == 's' && - word[word.size() - 2] != 's' && // avoid stripping from "boss", "class" - std::isalpha(static_cast(word[word.size() - 2]))) { - return {word.substr(0, word.size() - 1), "s"}; - } - return {word, ""}; - }; - - size_t wi = 0; - while (wi < word_indices.size()) { - size_t best_dist = std::numeric_limits::max(); - const std::string* best_match = nullptr; - size_t best_window = 0; - size_t best_first_token = 0; - size_t best_last_token = 0; - std::string best_suffix; - - for (size_t window = std::min(3, word_indices.size() - wi); window >= 1; --window) { - std::string window_collapsed; - const size_t first_tok = word_indices[wi]; - const size_t last_tok = word_indices[wi + window - 1]; - for (size_t w = 0; w < window; ++w) { - window_collapsed += tokens[word_indices[wi + w]].text; - } - - if (window == 1 && window_collapsed.size() < 3) break; - - auto [stem, suffix] = strip_suffix(window_collapsed); - const std::string* candidates[] = {&window_collapsed, &stem}; - const std::string suffixes[] = {"", suffix}; - const size_t num_candidates = suffix.empty() ? 1 : 2; - - for (size_t ci = 0; ci < num_candidates; ++ci) { - const std::string& candidate = *candidates[ci]; - if (candidate.empty()) continue; - - for (const auto& entry : vocab_entries) { - const size_t wlen = candidate.size(); - const size_t vlen = entry.collapsed.size(); - - const size_t len_diff = wlen > vlen ? wlen - vlen : vlen - wlen; - const size_t max_dist = std::max(1, std::min(wlen, vlen) / 3); - if (len_diff > max_dist) continue; - - const size_t dist = levenshtein_ci(candidate, entry.collapsed); - - // For single-edit corrections, require first char match to prevent - // false positives like "vortex" → "Cortex". - if (dist == 1 && window == 1) { - const bool first_char_match = - std::tolower(static_cast(candidate[0])) == - std::tolower(static_cast(entry.collapsed[0])); - if (!first_char_match) continue; - } - - if (dist <= max_dist && dist < best_dist) { - best_dist = dist; - best_match = entry.original; - best_window = window; - best_first_token = first_tok; - best_last_token = last_tok; - best_suffix = suffixes[ci]; - } - } - } - - if (best_dist == 0) break; - } - - // Allow dist==0 for multi-word merges where word boundaries changed. - const bool should_replace = best_match && - best_dist != std::numeric_limits::max() && - (best_dist > 0 || best_window > 1); - - if (should_replace) { - tokens[best_first_token].text = *best_match + best_suffix; - for (size_t t = best_first_token + 1; t <= best_last_token; ++t) { - consumed[t] = true; - } - for (size_t t = best_first_token + 1; t <= best_last_token; ++t) { - if (t > 0) consumed[t - 1] = consumed[t - 1] || !tokens[t - 1].is_word; - } - wi += best_window; - } else { - ++wi; - } - } - - std::string result; - result.reserve(text.size()); - for (size_t i = 0; i < tokens.size(); ++i) { - if (!consumed[i]) { - result += tokens[i].text; - } - } - - text = std::move(result); -} - -inline InferenceOptions parse_inference_options_json(const std::string& json) { - InferenceOptions options; - - if (json.empty()) return options; - - size_t pos = json.find("\"temperature\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - options.temperature = std::stof(json.substr(pos)); - } - - pos = json.find("\"top_p\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - options.top_p = std::stof(json.substr(pos)); - } - - float parsed_min_p = options.min_p; - if (try_parse_json_float(json, "min_p", parsed_min_p)) { - options.min_p = std::clamp(parsed_min_p, 0.0f, 1.0f); - } - - float parsed_rep_penalty = options.repetition_penalty; - if (try_parse_json_float(json, "repetition_penalty", parsed_rep_penalty)) { - if (std::isfinite(parsed_rep_penalty) && parsed_rep_penalty > 0.0f) { - options.repetition_penalty = parsed_rep_penalty; - } - } - - pos = json.find("\"top_k\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - options.top_k = std::stoul(json.substr(pos)); - } - - pos = json.find("\"max_tokens\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - options.max_tokens = std::stoul(json.substr(pos)); - } - - pos = json.find("\"force_tools\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(static_cast(json[pos]))) pos++; - options.force_tools = (json.substr(pos, 4) == "true"); - } - - pos = json.find("\"tool_rag_top_k\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - options.tool_rag_top_k = std::stoul(json.substr(pos)); - } - - pos = json.find("\"confidence_threshold\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - options.confidence_threshold = std::stof(json.substr(pos)); - } - - pos = json.find("\"include_stop_sequences\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(static_cast(json[pos]))) pos++; - options.include_stop_sequences = (json.substr(pos, 4) == "true"); - } - - pos = json.find("\"use_vad\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(static_cast(json[pos]))) pos++; - options.use_vad = (json.substr(pos, 4) == "true"); - } - - pos = json.find("\"telemetry_enabled\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(static_cast(json[pos]))) pos++; - options.telemetry_enabled = (json.substr(pos, 4) == "true"); - } - - pos = json.find("\"auto_handoff\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(static_cast(json[pos]))) pos++; - options.auto_handoff = (json.substr(pos, 4) == "true"); - } - - pos = json.find("\"cloud_timeout_ms\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - options.cloud_timeout_ms = std::stoul(json.substr(pos)); - } - - pos = json.find("\"handoff_with_images\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(static_cast(json[pos]))) pos++; - options.handoff_with_images = (json.substr(pos, 4) == "true"); - } - - pos = json.find("\"enable_thinking_if_supported\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(static_cast(json[pos]))) pos++; - options.enable_thinking_if_supported = (json.substr(pos, 4) == "true"); - } - - pos = json.find("\"stop_sequences\""); - if (pos != std::string::npos) { - pos = json.find('[', pos); - if (pos != std::string::npos) { - size_t end_pos = json.find(']', pos); - size_t seq_pos = json.find('"', pos); - - while (seq_pos != std::string::npos && seq_pos < end_pos) { - size_t seq_start = seq_pos + 1; - size_t seq_end = json.find('"', seq_start); - if (seq_end != std::string::npos) { - options.stop_sequences.push_back(json.substr(seq_start, seq_end - seq_start)); - } - seq_pos = json.find('"', seq_end + 1); - } - } - } - - return options; -} - -static inline std::string trim_lfm2_slice(const std::string& value, size_t begin, size_t end) { - return trim_string(value.substr(begin, end - begin)); -} - -static inline void append_lfm2_call(const std::string& entry, - std::vector& function_calls) { - if (entry.empty()) return; - - std::string trimmed_entry = trim_lfm2_slice(entry, 0, entry.size()); - if (trimmed_entry.empty()) return; - - size_t paren_pos = trimmed_entry.find('('); - if (paren_pos == std::string::npos) return; - - std::string func_name = trim_lfm2_slice(trimmed_entry, 0, paren_pos); - std::string args_str = trim_lfm2_slice(trimmed_entry, paren_pos + 1, trimmed_entry.size()); - - while (!args_str.empty() && (args_str.back() == ')' || args_str.back() == ']')) { - args_str.pop_back(); - } - args_str = trim_lfm2_slice(args_str, 0, args_str.size()); - - std::string json_call = "{\"name\":\"" + func_name + "\",\"arguments\":{"; - - size_t arg_pos = 0; - bool first_arg = true; - while (arg_pos < args_str.length()) { - while (arg_pos < args_str.length() && std::isspace(static_cast(args_str[arg_pos]))) { - arg_pos++; - } - - size_t eq_pos = args_str.find('=', arg_pos); - if (eq_pos == std::string::npos) break; - - std::string arg_name = args_str.substr(arg_pos, eq_pos - arg_pos); - - size_t val_start = eq_pos + 1; - size_t val_end = val_start; - bool quoted = false; - - if (val_start < args_str.length() && args_str[val_start] == '"') { - quoted = true; - val_start++; - val_end = args_str.find('"', val_start); - if (val_end == std::string::npos) break; - } else { - int depth = 0; - for (val_end = val_start; val_end < args_str.length(); val_end++) { - char c = args_str[val_end]; - if (c == '[' || c == '{') depth++; - else if (c == ']' || c == '}') depth--; - else if (c == ',' && depth == 0) break; - } - } - - std::string arg_value = args_str.substr(val_start, val_end - val_start); - - if (!quoted) { - if (arg_value == "True") arg_value = "true"; - else if (arg_value == "False") arg_value = "false"; - else if (arg_value == "None") arg_value = "null"; - } - - if (!first_arg) json_call += ","; - json_call += "\"" + arg_name + "\":"; - json_call += quoted ? ("\"" + arg_value + "\"") : arg_value; - first_arg = false; - - arg_pos = args_str.find(',', val_end); - if (arg_pos != std::string::npos) { - arg_pos++; - } else { - break; - } - } - - json_call += "}}"; - function_calls.push_back(json_call); -} - -inline void parse_function_calls_from_response(const std::string& response_text, - std::string& regular_response, - std::vector& function_calls) { - regular_response = response_text; - function_calls.clear(); - - gemma::parse_function_calls(regular_response, function_calls); - - const std::string QWEN_TOOL_START = ""; - const std::string QWEN_TOOL_END = ""; - size_t qwen_start_pos = 0; - - while ((qwen_start_pos = regular_response.find(QWEN_TOOL_START, qwen_start_pos)) != std::string::npos) { - size_t content_start = qwen_start_pos + QWEN_TOOL_START.length(); - size_t qwen_end_pos = regular_response.find(QWEN_TOOL_END, content_start); - - size_t erase_end; - std::string json_content; - - if (qwen_end_pos != std::string::npos) { - json_content = regular_response.substr(content_start, qwen_end_pos - content_start); - erase_end = qwen_end_pos + QWEN_TOOL_END.length(); - } else { - json_content = regular_response.substr(content_start); - erase_end = regular_response.length(); - } - - size_t first = json_content.find_first_not_of(" \t\n\r"); - size_t last = json_content.find_last_not_of(" \t\n\r"); - if (first != std::string::npos && last != std::string::npos) { - json_content = json_content.substr(first, last - first + 1); - } - - if (json_content.size() > 2 && json_content.find("\"name\"") != std::string::npos) { - // Unwrap array wrapper if present: [{"name":...}] -> {"name":...} - if (json_content[0] == '[') { - size_t obj_start = json_content.find('{'); - size_t obj_end = json_content.rfind('}'); - if (obj_start != std::string::npos && obj_end != std::string::npos && obj_end > obj_start) { - json_content = json_content.substr(obj_start, obj_end - obj_start + 1); - } - } - if (json_content[0] == '{') { - size_t depth = 0; - bool in_string = false; - bool escaped = false; - size_t end_pos = 0; - for (size_t c = 0; c < json_content.size(); c++) { - char ch = json_content[c]; - if (escaped) { escaped = false; continue; } - if (ch == '\\' && in_string) { escaped = true; continue; } - if (ch == '"') { in_string = !in_string; continue; } - if (!in_string) { - if (ch == '{') depth++; - else if (ch == '}' && --depth == 0) { end_pos = c + 1; break; } - } - } - if (end_pos > 0) { - function_calls.push_back(json_content.substr(0, end_pos)); - } - } - } - - regular_response.erase(qwen_start_pos, erase_end - qwen_start_pos); - } - - const std::string TOOL_CALL_START = "<|tool_call_start|>"; - const std::string TOOL_CALL_END = "<|tool_call_end|>"; - size_t lfm2_start_pos = 0; - - while ((lfm2_start_pos = regular_response.find(TOOL_CALL_START, lfm2_start_pos)) != std::string::npos) { - size_t content_start = lfm2_start_pos + TOOL_CALL_START.length(); - size_t tool_end_pos = regular_response.find(TOOL_CALL_END, content_start); - - if (tool_end_pos != std::string::npos) { - std::string tool_content = regular_response.substr(content_start, tool_end_pos - content_start); - std::string content = tool_content; - size_t trim_start = 0; - while (trim_start < content.size() && std::isspace(static_cast(content[trim_start]))) { - trim_start++; - } - - if (trim_start < content.size()) { - size_t trim_end = content.size() - 1; - while (trim_end > trim_start && std::isspace(static_cast(content[trim_end]))) { - trim_end--; - } - content = content.substr(trim_start, trim_end - trim_start + 1); - } else { - content.clear(); - } - - if (!content.empty() && content.front() == '[' && content.back() == ']') { - std::string inner = content.substr(1, content.size() - 2); - - size_t inner_first = inner.find_first_not_of(" \t\n\r"); - if (inner_first != std::string::npos && inner[inner_first] == '{') { - size_t pos = inner_first; - while (pos < inner.size()) { - if (inner[pos] == '{') { - int brace_depth = 1; - size_t obj_start = pos; - pos++; - while (pos < inner.size() && brace_depth > 0) { - if (inner[pos] == '{') brace_depth++; - else if (inner[pos] == '}') brace_depth--; - pos++; - } - if (brace_depth == 0) { - std::string json_obj = inner.substr(obj_start, pos - obj_start); - if (json_obj.find("\"name\"") != std::string::npos) { - function_calls.push_back(json_obj); - } - } - } else { - pos++; - } - } - } else { - size_t start = 0; - int paren_depth = 0; - - for (size_t i = 0; i < inner.size(); ++i) { - char c = inner[i]; - if (c == '(') { - paren_depth++; - } else if (c == ')' && paren_depth > 0) { - paren_depth--; - } else if (c == ',' && paren_depth == 0) { - append_lfm2_call(inner.substr(start, i - start), function_calls); - start = i + 1; - } - } - - if (start < inner.size()) { - append_lfm2_call(inner.substr(start), function_calls); - } - } - } else if (!content.empty()) { - append_lfm2_call(content, function_calls); - } - - regular_response.erase(lfm2_start_pos, tool_end_pos + TOOL_CALL_END.length() - lfm2_start_pos); - } else { - break; - } - } - - const char* FUNCTION_CALL_MARKER = "\"function_call\""; - size_t search_pos = 0; - const size_t text_len = regular_response.length(); - - while (search_pos < text_len) { - size_t marker_pos = regular_response.find(FUNCTION_CALL_MARKER, search_pos); - if (marker_pos == std::string::npos) break; - - size_t json_start = regular_response.find('{', marker_pos); - if (json_start == std::string::npos) break; - - int brace_count = 1; - size_t json_end = json_start + 1; - while (json_end < text_len && brace_count > 0) { - char c = regular_response[json_end]; - brace_count += (c == '{') - (c == '}'); - json_end++; - } - - if (brace_count == 0) { - function_calls.push_back(regular_response.substr(json_start, json_end - json_start)); - regular_response = regular_response.substr(0, marker_pos); - size_t last_bracket = regular_response.rfind('{'); - if(last_bracket != std::string::npos) { - regular_response = regular_response.substr(0, last_bracket); - } - } - search_pos = json_end; - } -} - -inline std::vector> find_channel_token_ranges( - const std::vector& tokens, size_t offset, - uint32_t channel_open_id, uint32_t channel_close_id) { - std::vector> ranges; - size_t pos = 0; - while (pos < tokens.size()) { - if (tokens[pos] != channel_open_id) { - pos++; - continue; - } - - size_t block_start = pos; - pos++; - while (pos < tokens.size() && tokens[pos] != channel_close_id) { - pos++; - } - if (pos < tokens.size()) { - pos++; - } - ranges.push_back({offset + block_start, pos - block_start}); - } - return ranges; -} - -inline void strip_tag_blocks(std::string& text, std::string& extracted, - const std::string& open_tag, const std::string& close_tag) { - std::string result; - size_t pos = 0; - - size_t first_close = text.find(close_tag); - size_t first_open = text.find(open_tag); - if (first_close != std::string::npos && - (first_open == std::string::npos || first_close < first_open)) { - extracted += text.substr(0, first_close); - pos = first_close + close_tag.size(); - } - - while (pos < text.size()) { - size_t open_pos = text.find(open_tag, pos); - if (open_pos == std::string::npos) { - result += text.substr(pos); - break; - } - result += text.substr(pos, open_pos - pos); - size_t content_start = open_pos + open_tag.size(); - size_t close_pos = text.find(close_tag, content_start); - if (close_pos == std::string::npos) { - if (!extracted.empty()) extracted += "\n"; - extracted += text.substr(content_start); - break; - } - if (!extracted.empty()) extracted += "\n"; - extracted += text.substr(content_start, close_pos - content_start); - pos = close_pos + close_tag.size(); - } - text = result; -} - -inline void strip_thinking_block(const std::string& input, std::string& thinking, std::string& content) { - thinking.clear(); - content = input; - - auto trim = [](std::string& s) { - size_t first = s.find_first_not_of(" \t\n\r"); - size_t last = s.find_last_not_of(" \t\n\r"); - if (first != std::string::npos && last != std::string::npos) - s = s.substr(first, last - first + 1); - else - s.clear(); - }; - - if (content.find("<|channel>") != std::string::npos || content.find("") != std::string::npos) { - strip_tag_blocks(content, thinking, "<|channel>", ""); - } else if (content.find("") != std::string::npos || content.find("") != std::string::npos) { - strip_tag_blocks(content, thinking, "", ""); - } else { - return; - } - - trim(thinking); - trim(content); -} - -struct TranscriptSegment { - float start; - float end; - std::string text; -}; - -inline std::string construct_response_json(const std::string& regular_response, - const std::vector& function_calls, - double time_to_first_token, - double total_time_ms, - double prefill_tps, - double decode_tps, - size_t prompt_tokens, - size_t completion_tokens, - float confidence = 0.0f, - bool cloud_handoff = false, - const std::string& thinking = "", - const std::vector& segments = {}) { - std::ostringstream json; - json << "{"; - json << "\"success\":true,"; - json << "\"error\":null,"; - json << "\"cloud_handoff\":" << (cloud_handoff ? "true" : "false") << ","; - json << "\"response\":\"" << escape_json_string(regular_response) << "\","; - if (!thinking.empty()) { - json << "\"thinking\":\"" << escape_json_string(thinking) << "\","; - } - json << "\"function_calls\":["; - for (size_t i = 0; i < function_calls.size(); ++i) { - if (i > 0) json << ","; - json << function_calls[i]; - } - json << "],"; - json << "\"segments\":["; - for (size_t i = 0; i < segments.size(); ++i) { - if (i > 0) json << ","; - json << "{\"start\":" << std::fixed << std::setprecision(3) << segments[i].start - << ",\"end\":" << std::fixed << std::setprecision(3) << segments[i].end - << ",\"text\":\"" << escape_json_string(segments[i].text) << "\"}"; - } - json << "],"; - json << "\"confidence\":" << std::fixed << std::setprecision(4) << confidence << ","; - json << "\"time_to_first_token_ms\":" << std::fixed << std::setprecision(2) << time_to_first_token << ","; - json << "\"total_time_ms\":" << std::fixed << std::setprecision(2) << total_time_ms << ","; - json << "\"prefill_tps\":" << std::fixed << std::setprecision(2) << prefill_tps << ","; - json << "\"decode_tps\":" << std::fixed << std::setprecision(2) << decode_tps << ","; - json << "\"ram_usage_mb\":" << std::fixed << std::setprecision(2) << get_ram_usage_mb() << ","; - json << "\"prefill_tokens\":" << prompt_tokens << ","; - json << "\"decode_tokens\":" << completion_tokens << ","; - json << "\"total_tokens\":" << (prompt_tokens + completion_tokens); - json << "}"; - return json.str(); -} - -inline std::string serialize_function_calls(const std::vector& calls) { - if (calls.empty()) return "[]"; - std::ostringstream oss; - oss << "["; - for (size_t i = 0; i < calls.size(); ++i) { - if (i > 0) oss << ","; - oss << calls[i]; - } - oss << "]"; - return oss.str(); -} - -inline int validate_audio_params( - const char* component, - void* model, - char* response_buffer, size_t buffer_size, - const char* audio_file_path, - const uint8_t* pcm_buffer, size_t pcm_buffer_size) { - if (!model) { - std::string err = last_error_message.empty() ? "Model not initialized." : last_error_message; - CACTUS_LOG_ERROR(component, err); - handle_error_response(err, response_buffer, buffer_size); - return -1; - } - if (!response_buffer || buffer_size == 0) { - CACTUS_LOG_ERROR(component, "Invalid parameters: response_buffer or buffer_size"); - handle_error_response("Invalid parameters", response_buffer, buffer_size); - return -1; - } - if (!audio_file_path && (!pcm_buffer || pcm_buffer_size == 0)) { - CACTUS_LOG_ERROR(component, "No audio input provided"); - handle_error_response("Either audio_file_path or pcm_buffer must be provided", response_buffer, buffer_size); - return -1; - } - if (audio_file_path && pcm_buffer && pcm_buffer_size > 0) { - CACTUS_LOG_ERROR(component, "Both audio_file_path and pcm_buffer provided"); - handle_error_response("Cannot provide both audio_file_path and pcm_buffer", response_buffer, buffer_size); - return -1; - } - if (pcm_buffer && pcm_buffer_size > 0 && (pcm_buffer_size < 2 || pcm_buffer_size % 2 != 0)) { - CACTUS_LOG_ERROR(component, "Invalid pcm_buffer_size"); - handle_error_response("pcm_buffer_size must be even and at least 2 bytes", response_buffer, buffer_size); - return -1; - } - return 0; -} - -inline std::vector pcm_to_float(const uint8_t* pcm_buffer, size_t pcm_buffer_size) { - const int16_t* samples = reinterpret_cast(pcm_buffer); - size_t n = pcm_buffer_size / 2; - std::vector out(n); - for (size_t i = 0; i < n; ++i) - out[i] = static_cast(samples[i]) / 32768.0f; - return out; -} - -} // namespace ffi -} // namespace cactus - -#ifdef __cplusplus -extern "C" { -#endif - -const char* cactus_get_last_error(); - -#ifdef __cplusplus -} -#endif - -#endif // CACTUS_UTILS_H diff --git a/cactus/ffi/cactus_vad.cpp b/cactus/ffi/cactus_vad.cpp deleted file mode 100644 index 10b2e99c3..000000000 --- a/cactus/ffi/cactus_vad.cpp +++ /dev/null @@ -1,176 +0,0 @@ -#include "cactus_ffi.h" -#include "cactus_utils.h" -#include "../models/model.h" -#include "../../libs/audio/wav.h" -#include -#include - -using namespace cactus::engine; -using namespace cactus::ffi; - - -static SileroVADModel::SpeechTimestampsOptions parse_speech_timestamps_options(const std::string& json) { - SileroVADModel::SpeechTimestampsOptions options; - - if (json.empty()) return options; - - size_t pos = json.find("\"threshold\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(json[pos])) pos++; - options.threshold = std::stof(json.substr(pos)); - } - - pos = json.find("\"neg_threshold\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(json[pos])) pos++; - options.neg_threshold = std::stof(json.substr(pos)); - } - - pos = json.find("\"min_speech_duration_ms\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(json[pos])) pos++; - options.min_speech_duration_ms = std::stoi(json.substr(pos)); - } - - pos = json.find("\"max_speech_duration_s\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(json[pos])) pos++; - options.max_speech_duration_s = std::stof(json.substr(pos)); - } - - pos = json.find("\"min_silence_duration_ms\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(json[pos])) pos++; - options.min_silence_duration_ms = std::stoi(json.substr(pos)); - } - - pos = json.find("\"speech_pad_ms\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(json[pos])) pos++; - options.speech_pad_ms = std::stoi(json.substr(pos)); - } - - pos = json.find("\"window_size_samples\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(json[pos])) pos++; - options.window_size_samples = std::stoi(json.substr(pos)); - } - - pos = json.find("\"min_silence_at_max_speech\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(json[pos])) pos++; - options.min_silence_at_max_speech = std::stoi(json.substr(pos)); - } - - pos = json.find("\"use_max_poss_sil_at_max_speech\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(json[pos])) pos++; - std::string value = json.substr(pos, 5); - options.use_max_poss_sil_at_max_speech = (value.find("true") != std::string::npos); - } - - pos = json.find("\"sampling_rate\""); - if (pos != std::string::npos) { - pos = json.find(':', pos) + 1; - while (pos < json.length() && std::isspace(json[pos])) pos++; - options.sampling_rate = std::stoi(json.substr(pos)); - } - - return options; -} - - -extern "C" { - -int cactus_vad( - cactus_model_t model, - const char* audio_file_path, - char* response_buffer, - size_t buffer_size, - const char* options_json, - const uint8_t* pcm_buffer, - size_t pcm_buffer_size -) { - if (validate_audio_params("vad", model, response_buffer, buffer_size, audio_file_path, pcm_buffer, pcm_buffer_size) != 0) - return -1; - - try { - auto start_time = std::chrono::high_resolution_clock::now(); - auto* handle = static_cast(model); - auto* vad_model = static_cast(handle->model.get()); - - SileroVADModel::SpeechTimestampsOptions options = parse_speech_timestamps_options(options_json ? options_json : ""); - - std::vector audio; - if (audio_file_path == nullptr) { - audio = pcm_to_float(pcm_buffer, pcm_buffer_size); - } else { - AudioFP32 wav_audio = load_wav(audio_file_path); - audio = resample_to_16k_fp32(wav_audio.samples, wav_audio.sample_rate); - } - - if (audio.empty()) { - last_error_message = "Failed to load audio or audio is empty"; - CACTUS_LOG_ERROR("vad", last_error_message); - handle_error_response(last_error_message, response_buffer, buffer_size); - return -1; - } - - auto segments = vad_model->get_speech_timestamps(audio, options); - - auto end_time = std::chrono::high_resolution_clock::now(); - double total_time_ms = std::chrono::duration_cast(end_time - start_time).count() / 1000.0; - - std::ostringstream json; - json << "{"; - json << "\"success\":true,"; - json << "\"error\":null,"; - json << "\"segments\":["; - - for (size_t i = 0; i < segments.size(); ++i) { - if (i > 0) json << ","; - json << "{"; - json << "\"start\":" << segments[i].start << ","; - json << "\"end\":" << segments[i].end; - json << "}"; - } - - json << "],"; - json << "\"total_time_ms\":" << std::fixed << std::setprecision(2) << total_time_ms << ","; - json << "\"ram_usage_mb\":" << std::fixed << std::setprecision(2) << get_ram_usage_mb(); - json << "}"; - - std::string response = json.str(); - if (response.length() >= buffer_size) { - last_error_message = "Response buffer too small"; - CACTUS_LOG_ERROR("vad", last_error_message); - handle_error_response(last_error_message, response_buffer, buffer_size); - return -1; - } - - std::strcpy(response_buffer, response.c_str()); - return static_cast(response.length()); - - } catch (const std::exception& e) { - last_error_message = "Exception during VAD processing: " + std::string(e.what()); - CACTUS_LOG_ERROR("vad", last_error_message); - handle_error_response(last_error_message, response_buffer, buffer_size); - return -1; - } catch (...) { - last_error_message = "Unknown exception during VAD processing"; - CACTUS_LOG_ERROR("vad", last_error_message); - handle_error_response(last_error_message, response_buffer, buffer_size); - return -1; - } -} - -} diff --git a/cactus/ffi/gemma_tools.h b/cactus/ffi/gemma_tools.h deleted file mode 100644 index f0f9fe26d..000000000 --- a/cactus/ffi/gemma_tools.h +++ /dev/null @@ -1,576 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -namespace gemma { - -inline std::string to_upper(const std::string& s) { - std::string result = s; - for (auto& c : result) c = std::toupper(c); - return result; -} - -inline std::string escape(const std::string& s) { - return "" + s + ""; -} - -inline void skip_whitespace(const std::string& json, size_t& pos) { - while (pos < json.length() && std::isspace(json[pos])) pos++; -} - -inline std::string extract_json_string(const std::string& json, size_t& pos) { - std::string value; - while (pos < json.length() && json[pos] != '"') { - if (json[pos] == '\\' && pos + 1 < json.length()) { - pos++; - if (json[pos] == 'n') value += '\n'; - else if (json[pos] == 't') value += '\t'; - else if (json[pos] == 'r') value += '\r'; - else if (json[pos] == '"') value += '"'; - else if (json[pos] == '\\') value += '\\'; - else value += json[pos]; - } else { - value += json[pos]; - } - pos++; - } - if (pos < json.length()) pos++; - return value; -} - -std::string format_argument(const std::string& json, size_t& pos, bool escape_keys); -std::string format_parameters(const std::string& properties_json, const std::string& /*required_json*/); - -inline std::string format_argument(const std::string& json, size_t& pos, bool escape_keys = true) { - skip_whitespace(json, pos); - if (pos >= json.length()) return ""; - - char c = json[pos]; - - if (c == '"') { - pos++; - std::string value = extract_json_string(json, pos); - return escape(value); - } else if (c == '{') { - std::string result = "{"; - pos++; - bool first = true; - - while (pos < json.length()) { - skip_whitespace(json, pos); - if (pos >= json.length() || json[pos] == '}') { pos++; break; } - if (json[pos] == ',') { pos++; continue; } - - if (json[pos] != '"') break; - pos++; - std::string key = extract_json_string(json, pos); - - skip_whitespace(json, pos); - if (pos < json.length() && json[pos] == ':') pos++; - - std::string value = format_argument(json, pos, escape_keys); - - if (!first) result += ","; - first = false; - if (escape_keys) { - result += escape(key) + ":" + value; - } else { - result += key + ":" + value; - } - } - result += "}"; - return result; - } else if (c == '[') { - std::string result = "["; - pos++; - bool first = true; - - while (pos < json.length()) { - skip_whitespace(json, pos); - if (pos >= json.length() || json[pos] == ']') { pos++; break; } - if (json[pos] == ',') { pos++; continue; } - - std::string value = format_argument(json, pos, escape_keys); - - if (!first) result += ","; - first = false; - result += value; - } - result += "]"; - return result; - } else if (json.compare(pos, 4, "true") == 0) { - pos += 4; - return "true"; - } else if (json.compare(pos, 5, "false") == 0) { - pos += 5; - return "false"; - } else if (json.compare(pos, 4, "null") == 0) { - pos += 4; - return "null"; - } else { - size_t start = pos; - while (pos < json.length() && (std::isdigit(json[pos]) || json[pos] == '.' || - json[pos] == '-' || json[pos] == '+' || json[pos] == 'e' || json[pos] == 'E')) { - pos++; - } - return json.substr(start, pos - start); - } -} - -inline std::map parse_json_object_raw(const std::string& json, size_t& pos) { - std::map result; - skip_whitespace(json, pos); - if (pos >= json.length() || json[pos] != '{') return result; - pos++; - - while (pos < json.length()) { - skip_whitespace(json, pos); - if (pos >= json.length() || json[pos] == '}') { pos++; break; } - if (json[pos] == ',') { pos++; continue; } - - if (json[pos] != '"') break; - pos++; - std::string key = extract_json_string(json, pos); - - skip_whitespace(json, pos); - if (pos < json.length() && json[pos] == ':') pos++; - skip_whitespace(json, pos); - - size_t value_start = pos; - if (json[pos] == '"') { - pos++; - while (pos < json.length() && json[pos] != '"') { - if (json[pos] == '\\') pos++; - pos++; - } - pos++; - } else if (json[pos] == '{') { - int depth = 1; - pos++; - while (pos < json.length() && depth > 0) { - if (json[pos] == '{') depth++; - else if (json[pos] == '}') depth--; - else if (json[pos] == '"') { - pos++; - while (pos < json.length() && json[pos] != '"') { - if (json[pos] == '\\') pos++; - pos++; - } - } - pos++; - } - } else if (json[pos] == '[') { - int depth = 1; - pos++; - while (pos < json.length() && depth > 0) { - if (json[pos] == '[') depth++; - else if (json[pos] == ']') depth--; - else if (json[pos] == '"') { - pos++; - while (pos < json.length() && json[pos] != '"') { - if (json[pos] == '\\') pos++; - pos++; - } - } - pos++; - } - } else { - while (pos < json.length() && json[pos] != ',' && json[pos] != '}') pos++; - } - result[key] = json.substr(value_start, pos - value_start); - } - return result; -} - -inline std::string get_json_string_value(const std::string& json, size_t pos) { - skip_whitespace(json, pos); - if (pos < json.length() && json[pos] == '"') { - pos++; - return extract_json_string(json, pos); - } - return ""; -} - -inline std::string format_parameters(const std::string& properties_json, const std::string& /*required_json*/) { - static const std::set standard_keys = {"description", "type", "properties", "required", "nullable"}; - - size_t pos = 0; - auto properties = parse_json_object_raw(properties_json, pos); - - std::string result; - bool first = true; - - for (const auto& [key, value_json] : properties) { - if (standard_keys.count(key)) continue; - - if (!first) result += ","; - first = false; - - size_t prop_pos = 0; - auto prop_obj = parse_json_object_raw(value_json, prop_pos); - - result += key + ":{"; - - if (prop_obj.count("description")) { - std::string desc = get_json_string_value(prop_obj["description"], 0); - result += "description:" + escape(desc); - } - - std::string type_val; - if (prop_obj.count("type")) { - type_val = get_json_string_value(prop_obj["type"], 0); - } - - if (to_upper(type_val) == "STRING") { - if (prop_obj.count("enum")) { - size_t enum_pos = 0; - std::string enum_formatted = format_argument(prop_obj["enum"], enum_pos, true); - result += ",enum:" + enum_formatted; - } - } else if (to_upper(type_val) == "OBJECT") { - if (prop_obj.count("properties")) { - std::string nested_required; - if (prop_obj.count("required")) { - nested_required = prop_obj["required"]; - } - result += ",properties:{" + format_parameters(prop_obj["properties"], nested_required) + "}"; - } - if (prop_obj.count("required")) { - std::string req_items; - size_t req_pos = 0; - skip_whitespace(prop_obj["required"], req_pos); - if (req_pos < prop_obj["required"].length() && prop_obj["required"][req_pos] == '[') { - req_pos++; - bool req_first = true; - while (req_pos < prop_obj["required"].length()) { - skip_whitespace(prop_obj["required"], req_pos); - if (prop_obj["required"][req_pos] == ']') break; - if (prop_obj["required"][req_pos] == ',') { req_pos++; continue; } - if (prop_obj["required"][req_pos] == '"') { - req_pos++; - std::string req_item = extract_json_string(prop_obj["required"], req_pos); - if (!req_first) req_items += ","; - req_first = false; - req_items += escape(req_item); - } - } - } - if (!req_items.empty()) { - result += ",required:[" + req_items + "]"; - } - } - } else if (to_upper(type_val) == "ARRAY") { - if (prop_obj.count("items")) { - result += ",items:{"; - size_t items_pos = 0; - auto items_obj = parse_json_object_raw(prop_obj["items"], items_pos); - bool items_first = true; - - for (const auto& [item_key, item_value] : items_obj) { - if (!items_first) result += ","; - items_first = false; - - if (item_key == "properties") { - std::string items_required; - if (items_obj.count("required")) { - items_required = items_obj["required"]; - } - result += "properties:{" + format_parameters(item_value, items_required) + "}"; - } else if (item_key == "required") { - result += "required:["; - size_t req_pos = 0; - skip_whitespace(item_value, req_pos); - if (req_pos < item_value.length() && item_value[req_pos] == '[') { - req_pos++; - bool req_first = true; - while (req_pos < item_value.length()) { - skip_whitespace(item_value, req_pos); - if (item_value[req_pos] == ']') break; - if (item_value[req_pos] == ',') { req_pos++; continue; } - if (item_value[req_pos] == '"') { - req_pos++; - std::string req_item = extract_json_string(item_value, req_pos); - if (!req_first) result += ","; - req_first = false; - result += escape(req_item); - } - } - } - result += "]"; - } else if (item_key == "type") { - std::string item_type = get_json_string_value(item_value, 0); - result += "type:" + escape(to_upper(item_type)); - } else { - size_t val_pos = 0; - result += item_key + ":" + format_argument(item_value, val_pos, true); - } - } - result += "}"; - } - } - - if (!type_val.empty()) { - result += ",type:" + escape(to_upper(type_val)); - } - - result += "}"; - } - - return result; -} - -inline std::string format_function_declaration(const std::string& name, - const std::string& description, - const std::string& params_json) { - std::string result = "declaration:" + name + "{"; - result += "description:" + escape(description); - - if (!params_json.empty()) { - result += ",parameters:{"; - - size_t pos = 0; - auto params = parse_json_object_raw(params_json, pos); - - if (params.count("properties")) { - std::string required_json; - if (params.count("required")) { - required_json = params["required"]; - } - result += "properties:{" + format_parameters(params["properties"], required_json) + "}"; - } - - if (params.count("required")) { - std::string req_items; - size_t req_pos = 0; - skip_whitespace(params["required"], req_pos); - if (req_pos < params["required"].length() && params["required"][req_pos] == '[') { - req_pos++; - bool first = true; - while (req_pos < params["required"].length()) { - skip_whitespace(params["required"], req_pos); - if (params["required"][req_pos] == ']') break; - if (params["required"][req_pos] == ',') { req_pos++; continue; } - if (params["required"][req_pos] == '"') { - req_pos++; - std::string item = extract_json_string(params["required"], req_pos); - if (!first) req_items += ","; - first = false; - req_items += escape(item); - } - } - } - if (!req_items.empty()) { - result += ",required:[" + req_items + "]"; - } - } - - if (params.count("type")) { - std::string type_val = get_json_string_value(params["type"], 0); - result += ",type:" + escape(to_upper(type_val)); - } - - result += "}"; - } - - result += "}"; - return result; -} - -template -inline std::string format_tools(const std::vector& tools, bool use_pipe_tags = false) { - if (tools.empty()) return ""; - - const char* decl_start = use_pipe_tags ? "<|tool>" : ""; - const char* decl_end = use_pipe_tags ? "" : ""; - - std::string result; - for (const auto& tool : tools) { - result += decl_start; - std::string params_json; - auto it = tool.parameters.find("schema"); - if (it != tool.parameters.end()) { - params_json = it->second; - } - - result += format_function_declaration(tool.name, tool.description, params_json); - result += decl_end; - } - return result; -} - - -inline size_t match_quote_tag(const std::string& s, size_t pos) { - if (s.compare(pos, 8, "") == 0) return 8; - if (s.compare(pos, 5, "<|\"|>") == 0) return 5; - return 0; -} - -inline size_t find_quote_tag(const std::string& s, size_t pos) { - size_t e = s.find("", pos); - size_t t = s.find("<|\"|>", pos); - if (e == std::string::npos) return t; - if (t == std::string::npos) return e; - return std::min(e, t); -} - -inline std::string unescape(const std::string& s) { - const std::string ESCAPE_TAG = ""; - std::string result = s; - size_t pos = 0; - while ((pos = result.find(ESCAPE_TAG, pos)) != std::string::npos) { - result.erase(pos, ESCAPE_TAG.length()); - } - return result; -} - -inline std::string args_to_json(const std::string& args_content) { - std::string result = "{"; - size_t pos = 0; - bool first = true; - - if (!args_content.empty() && args_content[0] == '{') pos = 1; - - while (pos < args_content.length()) { - while (pos < args_content.length() && std::isspace(args_content[pos])) pos++; - if (pos >= args_content.length() || args_content[pos] == '}') break; - if (args_content[pos] == ',') { pos++; continue; } - - size_t key_start = pos; - while (pos < args_content.length() && args_content[pos] != ':') pos++; - std::string key = args_content.substr(key_start, pos - key_start); - if (pos < args_content.length()) pos++; - - std::string value; - while (pos < args_content.length() && std::isspace(args_content[pos])) pos++; - - if (pos < args_content.length()) { - size_t qtag_len = match_quote_tag(args_content, pos); - if (qtag_len > 0) { - pos += qtag_len; - size_t val_end = find_quote_tag(args_content, pos); - if (val_end != std::string::npos) { - value = "\"" + args_content.substr(pos, val_end - pos) + "\""; - pos = val_end + match_quote_tag(args_content, val_end); - } - } else if (args_content[pos] == '{') { - int depth = 1; - size_t start = pos; - pos++; - while (pos < args_content.length() && depth > 0) { - if (args_content[pos] == '{') depth++; - else if (args_content[pos] == '}') depth--; - pos++; - } - value = args_to_json(args_content.substr(start, pos - start)); - } else if (args_content[pos] == '[') { - int depth = 1; - size_t start = pos; - pos++; - while (pos < args_content.length() && depth > 0) { - if (args_content[pos] == '[') depth++; - else if (args_content[pos] == ']') depth--; - pos++; - } - std::string arr_content = args_content.substr(start + 1, pos - start - 2); - value = "["; - size_t arr_pos = 0; - bool first_item = true; - while (arr_pos < arr_content.length()) { - while (arr_pos < arr_content.length() && (std::isspace(arr_content[arr_pos]) || arr_content[arr_pos] == ',')) arr_pos++; - if (arr_pos >= arr_content.length()) break; - - if (!first_item) value += ","; - first_item = false; - - size_t aq_len = match_quote_tag(arr_content, arr_pos); - if (aq_len > 0) { - arr_pos += aq_len; - size_t end = find_quote_tag(arr_content, arr_pos); - if (end != std::string::npos) { - value += "\"" + arr_content.substr(arr_pos, end - arr_pos) + "\""; - arr_pos = end + match_quote_tag(arr_content, end); - } - } else { - size_t end = arr_content.find_first_of(",]", arr_pos); - if (end == std::string::npos) end = arr_content.length(); - value += arr_content.substr(arr_pos, end - arr_pos); - arr_pos = end; - } - } - value += "]"; - } else { - size_t val_start = pos; - while (pos < args_content.length() && args_content[pos] != ',' && args_content[pos] != '}') { - pos++; - } - value = args_content.substr(val_start, pos - val_start); - while (!value.empty() && std::isspace(value.back())) value.pop_back(); - } - } - - if (!first) result += ","; - first = false; - result += "\"" + key + "\":" + value; - } - - result += "}"; - return result; -} - -inline void parse_function_calls(std::string& response, std::vector& function_calls) { - - const std::string CALL_START = (response.find("<|tool_call>") != std::string::npos) - ? "<|tool_call>" : ""; - const std::string CALL_END = (CALL_START == "<|tool_call>") - ? "" : ""; - size_t pos = 0; - - while ((pos = response.find(CALL_START, pos)) != std::string::npos) { - size_t content_start = pos + CALL_START.length(); - size_t call_end_pos = response.find(CALL_END, content_start); - - size_t content_end = (call_end_pos != std::string::npos) ? call_end_pos : response.length(); - std::string call_content = response.substr(content_start, content_end - content_start); - - if (call_content.compare(0, 5, "call:") == 0) { - size_t brace_pos = call_content.find('{'); - - if (brace_pos == std::string::npos) { - size_t sep_pos = call_content.find_first_of(", ", 5); - if (sep_pos != std::string::npos) { - std::string func_name = call_content.substr(5, sep_pos - 5); - size_t args_start = sep_pos + 1; - while (args_start < call_content.length() && - (call_content[args_start] == ' ' || call_content[args_start] == ',')) { - args_start++; - } - std::string args_content = "{" + call_content.substr(args_start); - if (args_content.back() != '}') args_content += "}"; - - std::string args_json = args_to_json(args_content); - std::string json_call = "{\"name\":\"" + func_name + "\",\"arguments\":" + args_json + "}"; - function_calls.push_back(json_call); - } - } else { - std::string func_name = call_content.substr(5, brace_pos - 5); - std::string args_content = call_content.substr(brace_pos); - if (args_content.back() != '}') args_content += "}"; - - std::string args_json = args_to_json(args_content); - std::string json_call = "{\"name\":\"" + func_name + "\",\"arguments\":" + args_json + "}"; - function_calls.push_back(json_call); - } - } - - size_t erase_end = (call_end_pos != std::string::npos) ? - call_end_pos + CALL_END.length() : response.length(); - response.erase(pos, erase_end - pos); - } -} - -} // namespace gemma \ No newline at end of file diff --git a/cactus/ffi/needle_tools.h b/cactus/ffi/needle_tools.h deleted file mode 100644 index 4330cff51..000000000 --- a/cactus/ffi/needle_tools.h +++ /dev/null @@ -1,87 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -namespace needle { - -inline std::string to_snake_case(const std::string& name) { - std::string s; - s.reserve(name.size() * 2); - for (char c : name) { - if (std::isalnum(static_cast(c)) || c == '_') { - s += c; - } else { - if (s.empty() || s.back() != '_') s += '_'; - } - } - - std::string s2; - s2.reserve(s.size() * 2); - for (size_t i = 0; i < s.size(); i++) { - char c = s[i]; - if (i > 0 && std::isupper(static_cast(c))) { - char prev = s[i - 1]; - if (std::islower(static_cast(prev)) || std::isdigit(static_cast(prev))) { - s2 += '_'; - } - } - s2 += c; - } - - std::string s3; - s3.reserve(s2.size() * 2); - for (size_t i = 0; i < s2.size(); i++) { - s3 += s2[i]; - if (i + 1 < s2.size() && - std::isupper(static_cast(s2[i])) && - std::isupper(static_cast(s2[i + 1]))) { - if (i + 2 < s2.size() && std::islower(static_cast(s2[i + 2]))) { - s3 += '_'; - } - } - } - - std::string result; - result.reserve(s3.size()); - bool prev_underscore = false; - for (char c : s3) { - if (c == '_') { - if (!prev_underscore) result += '_'; - prev_underscore = true; - } else { - result += static_cast(std::tolower(static_cast(c))); - prev_underscore = false; - } - } - - size_t start = result.find_first_not_of('_'); - if (start == std::string::npos) return result; - size_t end = result.find_last_not_of('_'); - return result.substr(start, end - start + 1); -} - -inline void restore_tool_names(std::vector& function_calls, - const std::unordered_map& name_map) { - if (name_map.empty()) return; - for (auto& call : function_calls) { - for (const auto& [snake, orig] : name_map) { - std::string from = "\"name\":\"" + snake + "\""; - size_t pos = call.find(from); - if (pos == std::string::npos) { - from = "\"name\": \"" + snake + "\""; - pos = call.find(from); - } - if (pos != std::string::npos) { - std::string to = from.substr(0, from.size() - snake.size() - 1) + orig + "\""; - call.replace(pos, from.size(), to); - break; - } - } - } -} - -} // namespace needle diff --git a/cactus/graph/graph.h b/cactus/graph/graph.h deleted file mode 100644 index b43617758..000000000 --- a/cactus/graph/graph.h +++ /dev/null @@ -1,779 +0,0 @@ -#ifndef GRAPH_H -#define GRAPH_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cactus { - -enum class LogLevel { - DEBUG = 0, - INFO = 1, - WARN = 2, - ERROR = 3, - NONE = 4 -}; - -class Logger { -public: - static Logger& instance() { - static Logger logger; - return logger; - } - - void set_level(LogLevel level) { min_level_ = level; } - LogLevel get_level() const { return min_level_; } - - void set_callback(std::function cb) { - std::lock_guard lock(mutex_); - callback_ = cb; - } - - void log(LogLevel level, const std::string& component, const std::string& message) { - if (level < min_level_) return; - - std::lock_guard lock(mutex_); - - if (callback_) { - callback_(level, component, message); - } else { - std::cerr << "[" << level_string(level) << "] [" << component << "] " << message << std::endl; - } - - if (level == LogLevel::ERROR) { - last_error_ = "[" + component + "] " + message; - } - } - - const std::string& last_error() const { return last_error_; } - void clear_error() { last_error_.clear(); } - -private: - Logger() : min_level_(LogLevel::WARN) {} - - static const char* level_string(LogLevel level) { - switch (level) { - case LogLevel::DEBUG: return "DEBUG"; - case LogLevel::INFO: return "INFO"; - case LogLevel::WARN: return "WARN"; - case LogLevel::ERROR: return "ERROR"; - default: return "?"; - } - } - - LogLevel min_level_; - std::mutex mutex_; - std::string last_error_; - std::function callback_; -}; - -} // namespace cactus - -#define CACTUS_LOG(level, component, msg) \ - do { \ - if (static_cast(level) >= static_cast(cactus::Logger::instance().get_level())) { \ - std::ostringstream _cactus_log_ss; \ - _cactus_log_ss << msg; \ - cactus::Logger::instance().log(level, component, _cactus_log_ss.str()); \ - } \ - } while(0) - -#define CACTUS_LOG_DEBUG(component, msg) CACTUS_LOG(cactus::LogLevel::DEBUG, component, msg) -#define CACTUS_LOG_INFO(component, msg) CACTUS_LOG(cactus::LogLevel::INFO, component, msg) -#define CACTUS_LOG_WARN(component, msg) CACTUS_LOG(cactus::LogLevel::WARN, component, msg) -#define CACTUS_LOG_ERROR(component, msg) CACTUS_LOG(cactus::LogLevel::ERROR, component, msg) - -namespace GraphFile { - class MappedFile; - struct SerializedGraph; -} - -enum class Precision { - INT8, - FP16, - FP32, - INT4 -}; - -enum class ComputeBackend { - CPU, - NPU -}; - -enum class Activation { - SILU, - GELU, - GELU_ERF, - RELU, - SIGMOID, - TANH -}; - -enum class OpType { - INPUT, PRECISION_CAST, - ADD, ADD_CLIPPED, SUBTRACT, MULTIPLY, DIVIDE, - ABS, POW, FLATTEN, VIEW, - MATMUL, TRANSPOSE, RESHAPE, SLICE, GATHER, EMBEDDING, - BILINEAR_INTERPOLATION, - SUM, MEAN, VARIANCE, MIN, MAX, - RMS_NORM, ROPE, ROPE_GPTJ, SOFTMAX, ATTENTION, ATTENTION_INT8_HYBRID, REL_POS_BIAS, CONV1D_CAUSAL, CONV1D_K3, CONV1D_K7S3, CONV1D, CONV1D_SAME_DEPTHWISE_K9, CONV1D_POINTWISE, CONV2D_K3S2P1, CONV2D_DEPTHWISE_K3S2P1, CONV2D_POINTWISE_1X1, GLU, BATCHNORM, - SCALAR_ADD, SCALAR_SUBTRACT, SCALAR_MULTIPLY, SCALAR_DIVIDE, SCALAR_EXP, SCALAR_SQRT, SCALAR_COS, SCALAR_SIN, SCALAR_LOG, - RELU, SILU, GELU, GELU_ERF, SIGMOID, TANH, - SAMPLE, CONCAT, CAT, - SCATTER_TOPK, - TOPK, LAYERNORM, GROUPNORM, - MOE_LAYER, - INDEX, - PERSISTENT, - QUANTIZE_ACTIVATIONS, - LSTM_CELL, - GATED_DELTANET_DECODE, - GATED_DELTANET_PREFILL, - STFT, - ALTUP_PREDICT, - ALTUP_CORRECT, - GAUSSIAN_TOPK, - MAXPOOL1D, - BILSTM_SEQUENCE, - LEAKY_RELU, - CONV2D_K3S1P1, - STATS_POOL, - WEIGHTED_STATS_POOL, - DENSE_MLP_INT4_FUSED -}; - -struct PrecisionTraits { - static constexpr size_t size_of(Precision prec) { - switch (prec) { - case Precision::INT8: return 1; - case Precision::FP16: return 2; - case Precision::FP32: return 4; - case Precision::INT4: return 1; - } - return 1; - } - - static constexpr size_t packed_size_of(Precision prec, size_t count) { - switch (prec) { - case Precision::INT4: return (count + 1) / 2; - default: return count * size_of(prec); - } - } - - static size_t byte_offset_of(Precision prec, size_t element_offset) { - switch (prec) { - case Precision::INT4: - assert(element_offset % 32 == 0 && "INT4 byte offset must be group-aligned (multiple of 32)"); - return element_offset / 2; - default: return element_offset * size_of(prec); - } - } - - static constexpr bool is_integer(Precision prec) { - switch (prec) { - case Precision::INT8: return true; - case Precision::INT4: return true; - case Precision::FP16: return false; - case Precision::FP32: return false; - } - return true; - } - - static constexpr bool is_floating_point(Precision prec) { - switch (prec) { - case Precision::INT8: return false; - case Precision::INT4: return false; - case Precision::FP16: return true; - case Precision::FP32: return true; - } - return false; - } -}; - -namespace Quantization { - void int8_to_fp32(const int8_t* src, float* dst, size_t count, float scale = 1.0f); - void fp32_to_int8(const float* src, int8_t* dst, size_t count, float scale = 1.0f); - void fp16_to_fp32(const __fp16* src, float* dst, size_t count); - void fp32_to_fp16(const float* src, __fp16* dst, size_t count); - void int8_to_fp16(const int8_t* src, __fp16* dst, size_t count, float scale = 1.0f); - void fp16_to_int8(const __fp16* src, int8_t* dst, size_t count, float scale = 1.0f); -} - -struct TensorConfig { - Precision default_precision = Precision::INT8; - Precision compute_precision = Precision::INT8; - Precision output_precision = Precision::INT8; - bool auto_mixed_precision = false; - - static TensorConfig& global(); -}; - -struct BroadcastInfo { - std::vector output_shape; - bool needs_broadcasting; - - static BroadcastInfo compute(const std::vector& lhs, const std::vector& rhs); -}; - -class BufferPool; - -struct BufferDesc { - std::vector shape; - size_t total_size; - size_t byte_size; - std::unique_ptr data; - void* external_data; - char* pooled_data; - Precision precision; - - size_t group_size = 0; - size_t num_groups = 0; - void* scales_data = nullptr; - std::unique_ptr owned_scales; - - bool is_interleaved = false; - size_t original_N = 0; - - void* activation_scales_data = nullptr; - std::unique_ptr owned_activation_scales; - size_t num_rows_for_activation_scales = 0; - - BufferDesc(); - BufferDesc(const std::vector& s, Precision prec = Precision::INT8); - ~BufferDesc(); - - BufferDesc(BufferDesc&& other) noexcept; - BufferDesc& operator=(BufferDesc&& other) noexcept; - - BufferDesc(const BufferDesc&) = delete; - BufferDesc& operator=(const BufferDesc&) = delete; - - void* get_data(); - const void* get_data() const; - - template - T* data_as() { return static_cast(get_data()); } - - template - const T* data_as() const { return static_cast(get_data()); } - - const __fp16* scales_as_fp16() const { - return reinterpret_cast(scales_data); - } - - bool is_grouped_int8() const { - return precision == Precision::INT8 && group_size > 0; - } - - bool is_grouped_int4() const { - return precision == Precision::INT4 && group_size > 0; - } - - void set_grouped_scales(size_t gs, size_t ng, void* scales_ptr) { - group_size = gs; - num_groups = ng; - scales_data = scales_ptr; - } - - void set_interleaved(bool interleaved, size_t orig_n) { - is_interleaved = interleaved; - original_N = orig_n; - } - - bool has_activation_scales() const { - return activation_scales_data != nullptr && num_rows_for_activation_scales > 0; - } - const float* activation_scales_as_float() const { - return reinterpret_cast(activation_scales_data); - } - float* activation_scales_as_float() { - return reinterpret_cast(activation_scales_data); - } - void allocate_activation_scales(size_t num_rows) { - num_rows_for_activation_scales = num_rows; - owned_activation_scales = std::make_unique(num_rows * sizeof(float)); - activation_scales_data = owned_activation_scales.get(); - } - void set_activation_scales(void* scales_ptr, size_t num_rows) { - activation_scales_data = scales_ptr; - num_rows_for_activation_scales = num_rows; - } - - void allocate(); - void allocate_from_pool(BufferPool& pool); - void release_to_pool(BufferPool& pool); - void set_external(void* ptr); -}; - -struct OpParams { - float scalar = 0.0f; - float scale = 1.0f; - float theta = 10000.0f; - float epsilon = 1e-6f; - int axis = -1; - bool pretransposed_rhs = false; - size_t position_offset = 0; - size_t slice_start = 0; - size_t slice_length = 0; - size_t window_size = 0; - bool is_causal = true; - bool attention_mask_is_additive = false; - float logit_cap = 0.0f; - std::vector new_shape; - std::vector permutation; - Precision output_precision = Precision::INT8; - BroadcastInfo broadcast_info; - ComputeBackend backend = ComputeBackend::CPU; - - size_t dilation = 1; - size_t stride = 1; - float temperature = 1.0f; - float top_p = 1.0f; - float min_p = 0.15f; - float repetition_penalty = 1.1f; - size_t top_k = 0; - size_t random_seed = 0; - - size_t index_value = 0; - size_t num_classes = 0; - size_t num_groups = 0; - size_t dst_height = 0; - size_t dst_width = 0; - bool align_corners = true; - bool normalize_routing = false; - size_t num_experts = 0; - size_t num_experts_per_tok = 0; - bool moe_gated = true; - Activation activation = Activation::SILU; - - std::vector bias_values; - std::vector bias_indices; - - const int8_t* cached_keys_int8 = nullptr; - const int8_t* cached_values_int8 = nullptr; - const float* cached_k_scales = nullptr; - const float* cached_v_scales = nullptr; - size_t cache_seq_len = 0; - size_t num_kv_heads = 0; - size_t head_dim = 0; - size_t num_fft_bins = 0; - size_t chunk_size = 0; - size_t num_altup_inputs = 0; - size_t v_head_dim = 0; - size_t kernel_size = 0; -}; - -struct GraphNode { - size_t id; - OpType op_type; - std::vector input_ids; - BufferDesc output_buffer; - OpParams params; - - GraphNode(size_t node_id, OpType type); -}; - -using nodes_vector = std::vector>; -using node_index_map_t = std::unordered_map; - -inline const BufferDesc& get_input(const GraphNode& node, size_t idx, - const nodes_vector& nodes, - const node_index_map_t& node_index_map) { - return nodes[node_index_map.at(node.input_ids[idx])]->output_buffer; -} - -struct AxisDims { - size_t outer, axis_size, inner; - static AxisDims from_shape(const std::vector& shape, size_t axis) { - AxisDims d; - d.outer = 1; - for (size_t i = 0; i < axis; i++) d.outer *= shape[i]; - d.axis_size = shape[axis]; - d.inner = 1; - for (size_t i = axis + 1; i < shape.size(); i++) d.inner *= shape[i]; - return d; - } -}; - -template -void dispatch_binary_op(OpType op, const T* lhs, const T* rhs, T* output, size_t count); - -template -void dispatch_unary_op(OpType op, const T* input, T* output, size_t count, float param = 0.0f); - -void compute_node_optimized(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); -void compute_matmul_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); -void compute_transpose_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); -void compute_reduce_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); -void compute_fused_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); -void compute_reshape_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); -void compute_precision_cast_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); -void compute_sample_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); -void compute_scatter_topk_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); -void compute_topk_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); -void compute_layernorm_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); -void compute_groupnorm_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); -void compute_persistent_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); -void compute_dense_mlp_int4_fused_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); -void compute_index_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); -void compute_lstm_cell_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); -void compute_gated_deltanet_decode_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); -void compute_gated_deltanet_prefill_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); -void compute_altup_predict_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); -void compute_altup_correct_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); -void compute_maxpool1d_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); -void compute_bilstm_sequence_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); -void compute_conv2d_k3s1p1_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); -void compute_stats_pool_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); -void compute_weighted_stats_pool_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map); - -void shrink_thread_local_buffers(); -class BufferPool { -public: - BufferPool() = default; - ~BufferPool() = default; - - BufferPool(const BufferPool&) = delete; - BufferPool& operator=(const BufferPool&) = delete; - BufferPool(BufferPool&&) noexcept = default; - BufferPool& operator=(BufferPool&&) noexcept = default; - - char* acquire(size_t byte_size); - void release(char* ptr, size_t byte_size); - void clear(); - - size_t active_bytes() const { return active_bytes_; } - size_t pool_bytes() const { return pool_bytes_; } - size_t peak_bytes() const { return peak_bytes_; } - -private: - std::unordered_map>> free_buffers_; - size_t active_bytes_ = 0; - size_t pool_bytes_ = 0; - size_t peak_bytes_ = 0; - - size_t round_up_size(size_t size) const; -}; - -namespace ValidationUtils { - void validate_tensor_dims(const std::vector& shape, size_t required_dims, const std::string& op_name); - void validate_precision(Precision actual, Precision required, const std::string& op_name); - void validate_input_count(size_t actual, size_t required, const std::string& op_name); -} - - -class CactusGraph { -public: - CactusGraph(); - ~CactusGraph() = default; - - CactusGraph(const CactusGraph&) = delete; - CactusGraph& operator=(const CactusGraph&) = delete; - CactusGraph(CactusGraph&&) noexcept = default; - CactusGraph& operator=(CactusGraph&&) noexcept = default; - - struct DebugNodeEntry { - uint32_t layer_idx; - std::string name; - size_t node_id; - }; - - void save(const std::string& path); - static CactusGraph load(const std::string& path); - - size_t input(const std::vector& shape, Precision precision = Precision::INT8); - size_t precision_cast(size_t input, Precision target_precision); - size_t quantize_activations(size_t input); - - size_t add(size_t input1, size_t input2); - size_t add_clipped(size_t input1, size_t input2); - size_t subtract(size_t input1, size_t input2); - size_t multiply(size_t input1, size_t input2); - size_t divide(size_t input1, size_t input2); - - size_t scalar_add(size_t input, float value); - size_t scalar_subtract(size_t input, float value); - size_t scalar_multiply(size_t input, float value); - size_t scalar_divide(size_t input, float value); - size_t scalar_exp(size_t input); - size_t scalar_sqrt(size_t input); - size_t scalar_cos(size_t input); - size_t scalar_sin(size_t input); - size_t scalar_log(size_t input); - - size_t relu(size_t input); - size_t silu(size_t input); - size_t gelu(size_t input); - size_t gelu_erf(size_t input); - size_t sigmoid(size_t input); - size_t tanh(size_t input); - size_t glu(size_t input, int axis = -1); - - size_t abs(size_t input); - size_t pow(size_t input, float exponent); - size_t view(size_t input, const std::vector& new_shape); - size_t flatten(size_t input, int start_dim = 0, int end_dim = -1); - - size_t matmul(size_t input1, size_t input2, bool pretransposed_rhs = false, ComputeBackend backend = ComputeBackend::CPU); - size_t transpose(size_t input, ComputeBackend backend = ComputeBackend::CPU); - size_t transposeN(size_t input, const std::vector& permutation, ComputeBackend backend = ComputeBackend::CPU); - size_t reshape(size_t input, const std::vector& new_shape); - size_t slice(size_t input, int axis, size_t start, size_t length); - size_t index(size_t input, size_t index_value, int dim); - - size_t sum(size_t input, int axis); - size_t mean(size_t input, int axis); - size_t variance(size_t input, int axis); - size_t min(size_t input, int axis); - size_t max(size_t input, int axis); - - size_t gather(size_t embeddings, size_t indices); - size_t mmap_embeddings(const std::string& filename); - size_t mmap_weights(const std::string& filename); - void set_grouped_scales(size_t node_id, size_t group_size, size_t num_groups, void* scales_ptr); - void set_interleaved(size_t node_id, bool interleaved, size_t original_N); - - void release_weight_pages(size_t node_id); - void prefetch_weight_pages(size_t node_id); - void release_all_weight_pages(); - size_t embedding(const std::string& filename, size_t indices); - size_t embedding(size_t embedding_tensor, size_t indices); - size_t bilinear_interpolation(size_t pos_embeds, size_t dst_height, size_t dst_width, bool align_corners = true); - - size_t layernorm(size_t input, size_t weight, size_t bias, float epsilon = 1e-5f); - size_t layernorm(size_t input, size_t weight, float epsilon = 1e-5f); // No bias version - size_t groupnorm(size_t input, size_t weight, size_t bias, size_t num_groups = 32, float epsilon = 1e-5f); - size_t batchnorm(size_t input, size_t weight, size_t bias, size_t running_mean, size_t running_var, int axis = 1, float epsilon = 1e-5f); - size_t topk(size_t input, size_t k); - size_t dense_mlp_int4_fused(size_t hidden, size_t gate_weight, - size_t up_weight, size_t down_weight); - size_t moe_layer(size_t hidden, - size_t routing_probs, - size_t topk_indices, - const std::vector& w1_weights, - const std::vector& w3_weights, - const std::vector& w2_weights, - size_t num_experts, - size_t num_experts_per_tok, - bool normalize_routing, - float epsilon, - float routed_scaling_factor, - Activation activation = Activation::SILU, - size_t per_expert_scale = 0); - size_t moe_layer(size_t hidden, - size_t routing_probs, - size_t topk_indices, - const std::vector& w1_weights, - const std::vector& w2_weights, - size_t num_experts, - size_t num_experts_per_tok, - bool normalize_routing, - float epsilon, - float routed_scaling_factor, - Activation activation); - size_t rms_norm(size_t input, size_t weight, float epsilon = 1e-5f); - size_t rope(size_t input, float theta, size_t position_offset = 0, ComputeBackend backend = ComputeBackend::CPU); - size_t rope_gptj(size_t input, float theta, size_t position_offset = 0, size_t rot_dim = 0, ComputeBackend backend = ComputeBackend::CPU); - size_t softmax(size_t input, int axis = -1); - size_t attention(size_t query, size_t key, size_t value, float scale, bool is_causal = true, ComputeBackend backend = ComputeBackend::CPU); - size_t attention(size_t query, size_t key, size_t value, float scale, size_t position_offset, ComputeBackend backend = ComputeBackend::CPU); - size_t attention(size_t query, size_t key, size_t value, float scale, size_t position_offset, size_t window_size, ComputeBackend backend = ComputeBackend::CPU); - size_t attention_masked(size_t query, size_t key, size_t value, size_t mask, float scale, - bool is_causal = true, ComputeBackend backend = ComputeBackend::CPU, - bool additive_mask = false, size_t position_offset = 0, size_t window_size = 0, - float logit_cap = 0.0f); - size_t rel_pos_bias(size_t query, size_t relative_key, float scale); - - size_t attention_int8_hybrid(size_t query, size_t key_new, size_t value_new, float scale, size_t position_offset, - const int8_t* cached_keys, const int8_t* cached_values, - const float* k_scales, const float* v_scales, - size_t cache_len, size_t num_kv_heads, size_t head_dim, - size_t window_size = 0, size_t v_head_dim = 0); - - size_t conv1d_causal(size_t input, size_t weight, size_t kernel_size, size_t dilation = 1); - size_t conv1d_k3(size_t input, size_t weight, size_t stride); - size_t conv1d_k7s3(size_t input, size_t weight, size_t bias); - size_t conv1d(size_t input, size_t weight, size_t stride); - size_t conv1d(size_t input, size_t weight, size_t bias, size_t stride); - size_t conv1d_same_depthwise_k9(size_t input, size_t weight); - size_t conv1d_same_depthwise_k9(size_t input, size_t weight, size_t bias); - size_t conv1d_pointwise(size_t input, size_t weight); - size_t conv1d_pointwise(size_t input, size_t weight, size_t bias); - size_t conv2d_k3s2p1(size_t input, size_t weight); - size_t conv2d_k3s2p1(size_t input, size_t weight, size_t bias); - size_t conv2d_depthwise_k3s2p1(size_t input, size_t weight); - size_t conv2d_depthwise_k3s2p1(size_t input, size_t weight, size_t bias); - size_t conv2d_pointwise_1x1(size_t input, size_t weight); - size_t conv2d_pointwise_1x1(size_t input, size_t weight, size_t bias); - - size_t lstm_cell(size_t input, size_t h_prev, size_t c_prev, size_t weight_ih, size_t weight_hh, size_t bias_ih, size_t bias_hh); - size_t gated_deltanet_decode(size_t query, size_t key, size_t value, size_t gate_log, size_t beta, - size_t initial_state, float scale = 0.0f); - size_t gated_deltanet_prefill(size_t query, size_t key, size_t value, size_t gate_log, size_t beta, - size_t initial_state, size_t chunk_size = 64, float scale = 0.0f); - size_t stft(size_t input, size_t weight, size_t stride, size_t num_fft_bins); - - size_t altup_predict(size_t coefs, const size_t* streams, size_t num_streams); - size_t altup_correct(size_t coefs, size_t innovation, const size_t* predictions, size_t num_predictions); - - size_t gaussian_topk(size_t input, float ppf); - - size_t maxpool1d(size_t input, size_t kernel_size, size_t stride); - size_t leaky_relu(size_t input, float negative_slope = 0.01f); - size_t bilstm_sequence(size_t input, size_t w_ih_fwd, size_t w_hh_fwd, size_t b_ih_fwd, size_t b_hh_fwd, - size_t w_ih_bwd, size_t w_hh_bwd, size_t b_ih_bwd, size_t b_hh_bwd); - size_t conv2d_k3s1p1(size_t input, size_t weight); - size_t conv2d_k3s1p1(size_t input, size_t weight, size_t bias); - size_t stats_pool(size_t input); - size_t weighted_stats_pool(size_t input, size_t weights); - - size_t sample(size_t logits, float temperature = 0.6f, float top_p = 0.95f, size_t top_k = 20, - const std::unordered_map& logit_bias = {}); - size_t sample_with_options(size_t logits, float temperature, float top_p, float min_p, float repetition_penalty, - size_t top_k, const std::unordered_map& logit_bias = {}); - - size_t concat(size_t input1, size_t input2, int axis = 0); - size_t cat(const std::vector& inputs, int axis); - size_t scatter_topk(size_t indices, size_t values, size_t num_classes); - - void set_input(size_t node_id, const void* data, Precision precision); - void set_external_input(size_t node_id, void* data, Precision precision); - void* get_output(size_t node_id); - - void execute(const std::string& profile_file = ""); - void hard_reset(); - void soft_reset(); - void soft_reset_keep_pool(); - void set_prefill_mode(bool enabled) { prefill_mode_ = enabled; } - - void register_debug_node(uint32_t layer_idx, const std::string& name, size_t node_id); - void capture_debug_node(uint32_t layer_idx, const std::string& name, size_t node_id); - const std::vector& get_debug_nodes() const; - void clear_debug_nodes(); - - size_t add_node(OpType op_type, const std::vector& inputs, const std::vector& output_shape, const OpParams& params = {}); - const BufferDesc& get_output_buffer(size_t node_id) const; - void allocate_buffers(); - size_t get_node_count() const; - - size_t persistent(size_t source_node); - bool is_populated(size_t persistent_node_id) const; - void invalidate_persistent(size_t persistent_node_id); - - std::vector> nodes_; - std::unordered_map node_index_map_; - -private: - static CactusGraph from_serialized(const GraphFile::SerializedGraph& serialized); - size_t next_node_id_; - std::vector> mapped_files_; - std::unordered_map weight_cache_; - std::unordered_map node_to_mapped_file_; - std::vector debug_nodes_; - BufferPool buffer_pool_; - bool prefill_mode_ = false; - - std::unordered_set persistent_node_ids_; - std::unordered_set populated_node_ids_; -}; - - -namespace GraphFile { - struct LoadedNode { - size_t node_id; - std::vector shape; - Precision precision; - size_t byte_size; - }; - - struct GraphHeader { - uint32_t magic; - uint32_t version; - uint32_t node_count; - uint32_t flags = 0; - }; - - struct NodeEntry { - uint32_t index; // serialized node index 0..n-1 - OpType op_type; - std::vector inputs; - std::vector output_shape; - Precision precision; - OpParams params; - }; - - struct SerializedGraph { - GraphHeader header; - std::vector nodes; - std::vector graph_inputs; // IDs of serialized inputs - std::vector graph_outputs; // IDs of serialized outputs - }; - - SerializedGraph load_graph(const std::string& filename); - void save_graph(const CactusGraph& graph, const std::string& filename); - - void save_node(CactusGraph& graph, size_t node_id, const std::string& filename); - - class MappedFile { - public: - MappedFile(const std::string& filename); - ~MappedFile(); - - MappedFile(const MappedFile&) = delete; - MappedFile& operator=(const MappedFile&) = delete; - MappedFile(MappedFile&& other) noexcept; - MappedFile& operator=(MappedFile&& other) noexcept; - - const std::vector& shape() const; - Precision precision() const; - size_t byte_size() const; - - size_t group_size() const { return group_size_; } - size_t num_groups() const { return num_groups_; } - const void* scales_data() const; - - bool is_interleaved() const { return is_interleaved_; } - size_t original_N() const { return original_N_; } - - void* data(); - const void* data() const; - - template - const T* typed_data() const; - - void release_pages(); - void prefetch_pages(); - - private: - int fd_; - void* mapped_data_; - size_t file_size_, data_offset_; - std::vector shape_; - Precision precision_; - size_t byte_size_; - size_t group_size_ = 0; - size_t num_groups_ = 0; - size_t scales_offset_ = 0; - size_t scales_bytes_ = 0; - uint32_t alignment_ = 32; - - bool is_interleaved_ = false; - size_t original_N_ = 0; - - void parse_header(); - void apply_madvise_hints(); - }; -} - -#endif diff --git a/cactus/graph/graph_builder.cpp b/cactus/graph/graph_builder.cpp deleted file mode 100644 index ca7b57edc..000000000 --- a/cactus/graph/graph_builder.cpp +++ /dev/null @@ -1,1707 +0,0 @@ -#include "graph.h" -#include -#include -#include -#include - -size_t CactusGraph::input(const std::vector& shape, Precision precision) { - return add_node(OpType::INPUT, {}, shape, {.output_precision = precision}); -} - -size_t CactusGraph::add(size_t input1, size_t input2) { - const auto& lhs_buffer = get_output_buffer(input1); - const auto& rhs_buffer = get_output_buffer(input2); - BroadcastInfo broadcast_info = BroadcastInfo::compute(lhs_buffer.shape, rhs_buffer.shape); - OpParams params{.broadcast_info = broadcast_info}; - - return add_node(OpType::ADD, {input1, input2}, broadcast_info.output_shape, params); -} - -size_t CactusGraph::add_clipped(size_t input1, size_t input2) { - const auto& lhs_buffer = get_output_buffer(input1); - const auto& rhs_buffer = get_output_buffer(input2); - - BroadcastInfo broadcast_info = BroadcastInfo::compute(lhs_buffer.shape, rhs_buffer.shape); - OpParams params{.broadcast_info = broadcast_info}; - - return add_node(OpType::ADD_CLIPPED, {input1, input2}, broadcast_info.output_shape, params); -} - -size_t CactusGraph::subtract(size_t input1, size_t input2) { - const auto& lhs_buffer = get_output_buffer(input1); - const auto& rhs_buffer = get_output_buffer(input2); - - BroadcastInfo broadcast_info = BroadcastInfo::compute(lhs_buffer.shape, rhs_buffer.shape); - OpParams params{.broadcast_info = broadcast_info}; - - return add_node(OpType::SUBTRACT, {input1, input2}, broadcast_info.output_shape, params); -} - -size_t CactusGraph::multiply(size_t input1, size_t input2) { - const auto& lhs_buffer = get_output_buffer(input1); - const auto& rhs_buffer = get_output_buffer(input2); - - BroadcastInfo broadcast_info = BroadcastInfo::compute(lhs_buffer.shape, rhs_buffer.shape); - OpParams params{.broadcast_info = broadcast_info}; - - return add_node(OpType::MULTIPLY, {input1, input2}, broadcast_info.output_shape, params); -} - -size_t CactusGraph::divide(size_t input1, size_t input2) { - const auto& lhs_buffer = get_output_buffer(input1); - const auto& rhs_buffer = get_output_buffer(input2); - - BroadcastInfo broadcast_info = BroadcastInfo::compute(lhs_buffer.shape, rhs_buffer.shape); - OpParams params{.broadcast_info = broadcast_info}; - - return add_node(OpType::DIVIDE, {input1, input2}, broadcast_info.output_shape, params); -} - -size_t CactusGraph::abs(size_t input) { - const auto& input_buffer = get_output_buffer(input); - OpParams params{.output_precision = input_buffer.precision}; - return add_node(OpType::ABS, {input}, input_buffer.shape, params); -} - -size_t CactusGraph::pow(size_t input, float exponent) { - const auto& input_buffer = get_output_buffer(input); - OpParams params{.scalar = exponent, .output_precision = input_buffer.precision}; - return add_node(OpType::POW, {input}, input_buffer.shape, params); -} - -size_t CactusGraph::view(size_t input, const std::vector& new_shape) { - const auto& input_buffer = get_output_buffer(input); - - size_t input_elements = 1; - for (size_t dim : input_buffer.shape) { - input_elements *= dim; - } - - size_t new_elements = 1; - for (size_t dim : new_shape) { - new_elements *= dim; - } - - if (input_elements != new_elements) { - throw std::runtime_error("View operation requires total number of elements to remain the same"); - } - - OpParams params{.new_shape = new_shape}; - return add_node(OpType::VIEW, {input}, new_shape, params); -} - -size_t CactusGraph::flatten(size_t input, int start_dim, int end_dim) { - const auto& input_buffer = get_output_buffer(input); - const auto& shape = input_buffer.shape; - size_t rank = shape.size(); - - if (start_dim < 0) start_dim += rank; - if (end_dim < 0) end_dim += rank; - - if (start_dim < 0 || static_cast(start_dim) >= rank || - end_dim < 0 || static_cast(end_dim) >= rank || - start_dim > end_dim) { - throw std::runtime_error("Invalid start_dim or end_dim for flatten operation"); - } - - std::vector output_shape; - - for (int i = 0; i < start_dim; ++i) { - output_shape.push_back(shape[i]); - } - - size_t flattened_dim = 1; - for (int i = start_dim; i <= end_dim; ++i) { - flattened_dim *= shape[i]; - } - output_shape.push_back(flattened_dim); - - for (size_t i = end_dim + 1; i < rank; ++i) { - output_shape.push_back(shape[i]); - } - - OpParams params{.new_shape = output_shape}; - return add_node(OpType::FLATTEN, {input}, output_shape, params); -} - -size_t CactusGraph::matmul(size_t input1, size_t input2, bool pretransposed_rhs, ComputeBackend backend) { - const auto& lhs_buffer = get_output_buffer(input1); - const auto& rhs_buffer = get_output_buffer(input2); - - if (lhs_buffer.shape.size() != 2 || rhs_buffer.shape.size() != 2) { - throw std::invalid_argument("Matrix multiplication requires 2D tensors"); - } - - size_t M = lhs_buffer.shape[0]; - size_t K = lhs_buffer.shape[1]; - size_t rhs_K = pretransposed_rhs ? rhs_buffer.shape[1] : rhs_buffer.shape[0]; - - // For interleaved weights, use original_N (shape stores padded N) - size_t N; - if (rhs_buffer.is_interleaved && rhs_buffer.original_N > 0) { - N = rhs_buffer.original_N; - } else { - N = pretransposed_rhs ? rhs_buffer.shape[0] : rhs_buffer.shape[1]; - } - - if (K != rhs_K) { - std::cout << "Matrix dimensions incompatible for multiplication: " - << "lhs=[" << M << "," << K << "] rhs=[" << rhs_buffer.shape[0] << "," << rhs_buffer.shape[1] << "]" - << " pretransposed=" << pretransposed_rhs - << " (K=" << K << " != rhs_K=" << rhs_K << ")" << std::endl; - throw std::invalid_argument("Matrix dimensions incompatible for multiplication"); - } - - std::vector output_shape = {M, N}; - OpParams params{.pretransposed_rhs = pretransposed_rhs, .backend = backend}; - return add_node(OpType::MATMUL, {input1, input2}, output_shape, params); -} - -size_t CactusGraph::transpose(size_t input, ComputeBackend backend) { - const auto& input_buffer = get_output_buffer(input); - std::vector output_shape = input_buffer.shape; - - if (output_shape.size() >= 2) { - std::swap(output_shape[output_shape.size()-2], output_shape[output_shape.size()-1]); - } - - std::vector permutation; - for (size_t i = 0; i < input_buffer.shape.size(); ++i) { - permutation.push_back(i); - } - if (permutation.size() >= 2) { - std::swap(permutation[permutation.size()-2], permutation[permutation.size()-1]); - } - - OpParams params{.permutation = permutation, .backend = backend}; - return add_node(OpType::TRANSPOSE, {input}, output_shape, params); -} - -size_t CactusGraph::transposeN(size_t input, const std::vector& permutation, ComputeBackend backend) { - const auto& input_buffer = get_output_buffer(input); - if (permutation.size() != input_buffer.shape.size()) { - throw std::runtime_error("transposeN permutation size must match tensor rank"); - } - std::vector output_shape(permutation.size()); - for (size_t i = 0; i < permutation.size(); ++i) { - size_t p = permutation[i]; - if (p >= input_buffer.shape.size()) { - throw std::runtime_error("transposeN permutation index out of range"); - } - output_shape[i] = input_buffer.shape[p]; - } - OpParams params{.permutation = permutation, .backend = backend}; - return add_node(OpType::TRANSPOSE, {input}, output_shape, params); -} - -size_t CactusGraph::reshape(size_t input, const std::vector& new_shape) { - OpParams params{.new_shape = new_shape}; - return add_node(OpType::RESHAPE, {input}, new_shape, params); -} - -size_t CactusGraph::index(size_t input, size_t index_value, int dim) { - const auto& input_buffer = get_output_buffer(input); - const auto& shape = input_buffer.shape; - - if (shape.empty()) { - throw std::invalid_argument("Cannot index a scalar tensor"); - } - - int actual_dim = dim; - if (actual_dim < 0) { - actual_dim += static_cast(shape.size()); - } - - if (actual_dim < 0 || static_cast(actual_dim) >= shape.size()) { - throw std::invalid_argument("Index dimension out of bounds"); - } - - if (index_value >= shape[actual_dim]) { - throw std::invalid_argument("Index value " + std::to_string(index_value) + - " out of bounds for dimension " + std::to_string(actual_dim) + - " with size " + std::to_string(shape[actual_dim])); - } - - std::vector output_shape; - for (size_t i = 0; i < shape.size(); ++i) { - if (static_cast(i) != actual_dim) { - output_shape.push_back(shape[i]); - } - } - - if (output_shape.empty()) { - output_shape = {1}; - } - - OpParams params{.axis = actual_dim, .output_precision = input_buffer.precision, .index_value = index_value}; - return add_node(OpType::INDEX, {input}, output_shape, params); -} - -size_t CactusGraph::sum(size_t input, int axis) { - const auto& input_buffer = get_output_buffer(input); - std::vector output_shape; - - if (axis == -1) { - output_shape = {1}; - } else { - output_shape = input_buffer.shape; - output_shape.erase(output_shape.begin() + axis); - if (output_shape.empty()) { - output_shape = {1}; - } - } - - OpParams params{.axis = axis, .output_precision = input_buffer.precision}; - return add_node(OpType::SUM, {input}, output_shape, params); -} - -size_t CactusGraph::mean(size_t input, int axis) { - const auto& input_buffer = get_output_buffer(input); - std::vector output_shape; - - if (axis == -1) { - output_shape = {1}; - } else { - output_shape = input_buffer.shape; - output_shape.erase(output_shape.begin() + axis); - if (output_shape.empty()) { - output_shape = {1}; - } - } - - OpParams params{.axis = axis, .output_precision = input_buffer.precision}; - return add_node(OpType::MEAN, {input}, output_shape, params); -} - -size_t CactusGraph::variance(size_t input, int axis) { - const auto& input_buffer = get_output_buffer(input); - std::vector output_shape; - - if (axis == -1) { - output_shape = {1}; - } else { - output_shape = input_buffer.shape; - output_shape.erase(output_shape.begin() + axis); - if (output_shape.empty()) { - output_shape = {1}; - } - } - - OpParams params{.axis = axis, .output_precision = input_buffer.precision}; - return add_node(OpType::VARIANCE, {input}, output_shape, params); -} - -size_t CactusGraph::min(size_t input, int axis) { - const auto& input_buffer = get_output_buffer(input); - std::vector output_shape; - - if (axis == -1) { - output_shape = {1}; - } else { - output_shape = input_buffer.shape; - output_shape.erase(output_shape.begin() + axis); - if (output_shape.empty()) { - output_shape = {1}; - } - } - - OpParams params{.axis = axis, .output_precision = input_buffer.precision}; - return add_node(OpType::MIN, {input}, output_shape, params); -} - -size_t CactusGraph::max(size_t input, int axis) { - const auto& input_buffer = get_output_buffer(input); - std::vector output_shape; - - if (axis == -1) { - output_shape = {1}; - } else { - output_shape = input_buffer.shape; - output_shape.erase(output_shape.begin() + axis); - if (output_shape.empty()) { - output_shape = {1}; - } - } - - OpParams params{.axis = axis, .output_precision = input_buffer.precision}; - return add_node(OpType::MAX, {input}, output_shape, params); -} - -size_t CactusGraph::rms_norm(size_t input, size_t weight, float epsilon) { - OpParams params{.epsilon = epsilon}; - return add_node(OpType::RMS_NORM, {input, weight}, {}, params); -} - -size_t CactusGraph::rope(size_t input, float theta, size_t position_offset, ComputeBackend backend) { - OpParams params{.theta = theta, .position_offset = position_offset, .backend = backend}; - return add_node(OpType::ROPE, {input}, {}, params); -} - -size_t CactusGraph::softmax(size_t input, int axis) { - OpParams params{.axis = axis}; - return add_node(OpType::SOFTMAX, {input}, {}, params); -} - -size_t CactusGraph::topk(size_t input, size_t k) { - const auto& input_buffer = get_output_buffer(input); - - if (input_buffer.shape.empty()) { - throw std::runtime_error("TopK requires non-empty input tensor"); - } - - std::vector output_shape = {2, input_buffer.shape[0], k}; - OpParams params{.output_precision = Precision::FP32, .top_k = k}; - - return add_node(OpType::TOPK, {input}, output_shape, params); -} - -size_t CactusGraph::dense_mlp_int4_fused(size_t hidden, size_t gate_weight, - size_t up_weight, size_t down_weight) { - const auto& hidden_buf = get_output_buffer(hidden); - if (hidden_buf.shape.size() < 2) { - throw std::runtime_error("dense_mlp_int4_fused requires hidden of rank >= 2"); - } - const auto& down_buf = get_output_buffer(down_weight); - size_t hidden_dim; - if (down_buf.is_interleaved && down_buf.original_N > 0) { - hidden_dim = down_buf.original_N; - } else { - hidden_dim = down_buf.shape[down_buf.shape.size() - 2]; - } - - std::vector output_shape = hidden_buf.shape; - output_shape[output_shape.size() - 1] = hidden_dim; - - OpParams params; - params.output_precision = Precision::FP16; - - return add_node(OpType::DENSE_MLP_INT4_FUSED, - {hidden, gate_weight, up_weight, down_weight}, - output_shape, params); -} - -size_t CactusGraph::moe_layer(size_t hidden, - size_t routing_probs, - size_t topk_indices, - const std::vector& w1_weights, - const std::vector& w3_weights, - const std::vector& w2_weights, - size_t num_experts, - size_t num_experts_per_tok, - bool normalize_routing, - float epsilon, - float routed_scaling_factor, - Activation activation, - size_t per_expert_scale) { - const auto& hidden_buffer = get_output_buffer(hidden); - const auto& routing_buffer = get_output_buffer(routing_probs); - const auto& topk_buffer = get_output_buffer(topk_indices); - - if (hidden_buffer.shape.size() != 2) { - throw std::runtime_error("moe_layer expects [tokens, hidden_dim] for hidden"); - } - if (routing_buffer.shape.size() != 2 || topk_buffer.shape.size() != 2) { - throw std::runtime_error("moe_layer expects 2D routing_probs and topk_indices"); - } - if (routing_buffer.shape[0] != hidden_buffer.shape[0] || topk_buffer.shape[0] != hidden_buffer.shape[0]) { - throw std::runtime_error("moe_layer token dimension mismatch across inputs"); - } - if (w1_weights.size() != num_experts || w3_weights.size() != num_experts || w2_weights.size() != num_experts) { - throw std::runtime_error("moe_layer expects num_experts weight tensors for each of w1, w3, w2"); - } - - std::vector input_ids; - input_ids.reserve(3 + 3 * num_experts + 1); - input_ids.push_back(hidden); - input_ids.push_back(routing_probs); - input_ids.push_back(topk_indices); - for (size_t i = 0; i < num_experts; ++i) input_ids.push_back(w1_weights[i]); - for (size_t i = 0; i < num_experts; ++i) input_ids.push_back(w3_weights[i]); - for (size_t i = 0; i < num_experts; ++i) input_ids.push_back(w2_weights[i]); - if (per_expert_scale != 0) input_ids.push_back(per_expert_scale); - - OpParams params; - params.num_experts = num_experts; - params.num_experts_per_tok = num_experts_per_tok; - params.normalize_routing = normalize_routing; - params.epsilon = epsilon; - params.scalar = routed_scaling_factor; - params.output_precision = hidden_buffer.precision; - params.activation = activation; - params.moe_gated = true; - - return add_node(OpType::MOE_LAYER, input_ids, hidden_buffer.shape, params); -} - -size_t CactusGraph::moe_layer(size_t hidden, - size_t routing_probs, - size_t topk_indices, - const std::vector& w1_weights, - const std::vector& w2_weights, - size_t num_experts, - size_t num_experts_per_tok, - bool normalize_routing, - float epsilon, - float routed_scaling_factor, - Activation activation) { - const auto& hidden_buffer = get_output_buffer(hidden); - const auto& routing_buffer = get_output_buffer(routing_probs); - const auto& topk_buffer = get_output_buffer(topk_indices); - - if (hidden_buffer.shape.size() != 2) { - throw std::runtime_error("moe_layer expects [tokens, hidden_dim] for hidden"); - } - if (routing_buffer.shape.size() != 2 || topk_buffer.shape.size() != 2) { - throw std::runtime_error("moe_layer expects 2D routing_probs and topk_indices"); - } - if (routing_buffer.shape[0] != hidden_buffer.shape[0] || topk_buffer.shape[0] != hidden_buffer.shape[0]) { - throw std::runtime_error("moe_layer token dimension mismatch across inputs"); - } - if (w1_weights.size() != num_experts || w2_weights.size() != num_experts) { - throw std::runtime_error("moe_layer expects num_experts weight tensors for each of w1, w2"); - } - - std::vector input_ids; - input_ids.reserve(3 + 2 * num_experts); - input_ids.push_back(hidden); - input_ids.push_back(routing_probs); - input_ids.push_back(topk_indices); - for (size_t i = 0; i < num_experts; ++i) input_ids.push_back(w1_weights[i]); - for (size_t i = 0; i < num_experts; ++i) input_ids.push_back(w2_weights[i]); - - OpParams params; - params.num_experts = num_experts; - params.num_experts_per_tok = num_experts_per_tok; - params.normalize_routing = normalize_routing; - params.epsilon = epsilon; - params.scalar = routed_scaling_factor; - params.output_precision = hidden_buffer.precision; - params.moe_gated = false; - params.activation = activation; - - return add_node(OpType::MOE_LAYER, input_ids, hidden_buffer.shape, params); -} - -size_t CactusGraph::layernorm(size_t input, size_t weight, size_t bias, float epsilon) { - OpParams params{.epsilon = epsilon}; - return add_node(OpType::LAYERNORM, {input, weight, bias}, {}, params); -} - -size_t CactusGraph::layernorm(size_t input, size_t weight, float epsilon) { - OpParams params{.epsilon = epsilon}; - return add_node(OpType::LAYERNORM, {input, weight}, {}, params); -} - -size_t CactusGraph::groupnorm(size_t input, size_t weight, size_t bias, size_t num_groups, float epsilon) { - OpParams params{.epsilon = epsilon, .num_groups = num_groups}; - return add_node(OpType::GROUPNORM, {input, weight, bias}, {}, params); -} - -size_t CactusGraph::batchnorm(size_t input, size_t weight, size_t bias, size_t running_mean, size_t running_var, int axis, float epsilon) { - const auto& xin = get_output_buffer(input); - const auto& w = get_output_buffer(weight); - const auto& b = get_output_buffer(bias); - const auto& rm = get_output_buffer(running_mean); - const auto& rv = get_output_buffer(running_var); - - if (xin.shape.empty()) { - throw std::runtime_error("batchnorm expects non-scalar input"); - } - - int actual_axis = axis; - if (actual_axis < 0) { - actual_axis += static_cast(xin.shape.size()); - } - if (actual_axis < 0 || static_cast(actual_axis) >= xin.shape.size()) { - throw std::runtime_error("batchnorm axis out of range"); - } - - const size_t C = xin.shape[static_cast(actual_axis)]; - if (w.total_size != C || b.total_size != C || rm.total_size != C || rv.total_size != C) { - throw std::runtime_error("batchnorm parameter size mismatch"); - } - - OpParams params{.epsilon = epsilon, .axis = actual_axis}; - params.output_precision = xin.precision; - return add_node(OpType::BATCHNORM, {input, weight, bias, running_mean, running_var}, xin.shape, params); -} - -size_t CactusGraph::attention(size_t query, size_t key, size_t value, float scale, bool is_causal, ComputeBackend backend) { - OpParams params{.scale = scale, .is_causal = is_causal, .backend = backend}; - const auto& qs = get_output_buffer(query).shape; - const auto& vs = get_output_buffer(value).shape; - return add_node(OpType::ATTENTION, {query, key, value}, {qs[0], qs[1], qs[2], vs[3]}, params); -} - -size_t CactusGraph::attention(size_t query, size_t key, size_t value, float scale, size_t position_offset, ComputeBackend backend) { - OpParams params{.scale = scale, .position_offset = position_offset, .backend = backend}; - const auto& qs = get_output_buffer(query).shape; - const auto& vs = get_output_buffer(value).shape; - return add_node(OpType::ATTENTION, {query, key, value}, {qs[0], qs[1], qs[2], vs[3]}, params); -} - -size_t CactusGraph::attention(size_t query, size_t key, size_t value, float scale, size_t position_offset, size_t window_size, ComputeBackend backend) { - OpParams params{.scale = scale, .position_offset = position_offset, .window_size = window_size, .backend = backend}; - const auto& qs = get_output_buffer(query).shape; - const auto& vs = get_output_buffer(value).shape; - return add_node(OpType::ATTENTION, {query, key, value}, {qs[0], qs[1], qs[2], vs[3]}, params); -} - -size_t CactusGraph::attention_masked(size_t query, size_t key, size_t value, size_t mask, float scale, - bool is_causal, ComputeBackend backend, bool additive_mask, - size_t position_offset, size_t window_size, float logit_cap) { - OpParams params{ - .scale = scale, - .position_offset = position_offset, - .window_size = window_size, - .is_causal = is_causal, - .backend = backend - }; - params.attention_mask_is_additive = additive_mask; - params.logit_cap = logit_cap; - const auto& qs = get_output_buffer(query).shape; - const auto& vs = get_output_buffer(value).shape; - return add_node(OpType::ATTENTION, {query, key, value, mask}, {qs[0], qs[1], qs[2], vs[3]}, params); -} - -size_t CactusGraph::rel_pos_bias(size_t query, size_t relative_key, float scale) { - const auto& q = get_output_buffer(query); - const auto& r = get_output_buffer(relative_key); - - if (q.shape.size() != 4) { - throw std::runtime_error("rel_pos_bias expects query [B, T, H, D]"); - } - if (r.shape.size() != 4) { - throw std::runtime_error("rel_pos_bias expects relative_key [B, R, H, D]"); - } - - const size_t B = q.shape[0]; - const size_t T = q.shape[1]; - const size_t H = q.shape[2]; - const size_t D = q.shape[3]; - const size_t R = r.shape[1]; - - if (r.shape[0] != 1 && r.shape[0] != B) { - throw std::runtime_error("rel_pos_bias relative_key batch must be 1 or match query batch"); - } - if (r.shape[2] != H || r.shape[3] != D) { - throw std::runtime_error("rel_pos_bias expects matching [H, D] between query and relative_key"); - } - if (R < (2 * T - 1)) { - throw std::runtime_error("rel_pos_bias requires relative_key length >= 2*T-1"); - } - - OpParams params{.scale = scale, .output_precision = q.precision}; - return add_node(OpType::REL_POS_BIAS, {query, relative_key}, {B, H, T, T}, params); -} - -size_t CactusGraph::attention_int8_hybrid(size_t query, size_t key_new, size_t value_new, float scale, size_t position_offset, - const int8_t* cached_keys, const int8_t* cached_values, - const float* k_scales, const float* v_scales, - size_t cache_len, size_t num_kv_heads, size_t head_dim, - size_t window_size, size_t v_head_dim) { - OpParams params; - params.scale = scale; - params.position_offset = position_offset; - params.window_size = window_size; - params.cached_keys_int8 = cached_keys; - params.cached_values_int8 = cached_values; - params.cached_k_scales = k_scales; - params.cached_v_scales = v_scales; - params.cache_seq_len = cache_len; - params.num_kv_heads = num_kv_heads; - params.head_dim = head_dim; - params.v_head_dim = v_head_dim; - std::vector out_shape; - if (v_head_dim != 0 && v_head_dim != head_dim) { - const auto& q_buf = get_output_buffer(query); - out_shape = {q_buf.shape[0], q_buf.shape[1], q_buf.shape[2], v_head_dim}; - } - return add_node(OpType::ATTENTION_INT8_HYBRID, {query, key_new, value_new}, out_shape, params); -} - -size_t CactusGraph::conv1d_causal(size_t input, size_t weight, size_t, size_t dilation) { - OpParams params{.dilation = dilation}; - return add_node(OpType::CONV1D_CAUSAL, {input, weight}, {}, params); -} - -size_t CactusGraph::conv1d_k3(size_t input, size_t weight, size_t stride) { - const auto& xin = get_output_buffer(input); - const auto& w = get_output_buffer(weight); - - if (xin.shape.size() != 3) throw std::runtime_error("conv1d_k3 expects N,C,L"); - if (w.shape.size() != 3) throw std::runtime_error("weight must be [C_out,C_in,3]"); - if (w.shape[1] != xin.shape[1]) throw std::runtime_error("C_in mismatch in conv1d_k3"); - if (w.shape[2] != 3) throw std::runtime_error("K=3 expected in conv1d_k3"); - - const size_t N = xin.shape[0]; - const size_t L = xin.shape[2]; - const size_t C_out= w.shape[0]; - const size_t K = w.shape[2]; - - const size_t pad = 1; - const size_t L_out = (L + 2 * pad - K) / stride + 1; - - OpParams params{}; - params.stride = stride; - params.output_precision = xin.precision; - - std::vector out_shape{N, C_out, L_out}; - return add_node(OpType::CONV1D_K3, {input, weight}, out_shape, params); -} - -size_t CactusGraph::conv1d_k7s3(size_t input, size_t weight, size_t bias) { - const auto& xin = get_output_buffer(input); - const auto& w = get_output_buffer(weight); - const auto& b = get_output_buffer(bias); - - if (xin.shape.size() != 3) throw std::runtime_error("conv1d_k7s3 expects N,C,L"); - if (w.shape.size() != 3) throw std::runtime_error("weight must be [C_in, 7, C_out]"); - if (w.shape[0] != xin.shape[1]) throw std::runtime_error("C_in mismatch in conv1d_k7s3"); - if (w.shape[1] != 7) throw std::runtime_error("K=7 expected in conv1d_k7s3"); - - size_t C_out = w.shape[2]; - if (b.total_size != C_out) throw std::runtime_error("Bias size mismatch"); - - const size_t N = xin.shape[0]; - const size_t L = xin.shape[2]; - const size_t K = 7; - const size_t stride = 3; - - const size_t L_out = (L < K) ? 0 : (L - K) / stride + 1; - - OpParams params{}; - params.stride = stride; - params.output_precision = xin.precision; - - std::vector out_shape{N, C_out, L_out}; - return add_node(OpType::CONV1D_K7S3, {input, weight, bias}, out_shape, params); -} - -size_t CactusGraph::conv1d(size_t input, size_t weight, size_t stride) { - const auto& xin = get_output_buffer(input); - const auto& w = get_output_buffer(weight); - - if (xin.shape.size() != 3) throw std::runtime_error("conv1d expects N,C,L"); - if (w.shape.size() != 3) throw std::runtime_error("conv1d weight expects [C_out, C_in, K]"); - - size_t N = xin.shape[0]; - size_t C_out = w.shape[0]; - size_t L = xin.shape[2]; - size_t K = w.shape[2]; - size_t L_out = (L - K) / stride + 1; - - OpParams params{.stride = stride}; - return add_node(OpType::CONV1D, {input, weight}, {N, C_out, L_out}, params); -} - -size_t CactusGraph::conv1d(size_t input, size_t weight, size_t bias, size_t stride) { - const auto& xin = get_output_buffer(input); - const auto& w = get_output_buffer(weight); - const auto& b = get_output_buffer(bias); - - if (xin.shape.size() != 3) throw std::runtime_error("conv1d expects N,C,L"); - if (w.shape.size() != 3) throw std::runtime_error("conv1d weight expects [C_out, C_in, K]"); - - size_t N = xin.shape[0]; - size_t C_out = w.shape[0]; - size_t L = xin.shape[2]; - size_t K = w.shape[2]; - size_t L_out = (L - K) / stride + 1; - if (b.total_size != C_out) throw std::runtime_error("conv1d bias size mismatch"); - - OpParams params{.output_precision = xin.precision, .stride = stride}; - return add_node(OpType::CONV1D, {input, weight, bias}, {N, C_out, L_out}, params); -} - -size_t CactusGraph::conv1d_same_depthwise_k9(size_t input, size_t weight) { - const auto& xin = get_output_buffer(input); - const auto& w = get_output_buffer(weight); - - if (xin.shape.size() != 3) { - throw std::runtime_error("conv1d_same_depthwise_k9 expects input [N, L, C]"); - } - - const size_t N = xin.shape[0]; - const size_t L = xin.shape[1]; - const size_t C = xin.shape[2]; - - if (w.shape.size() == 2) { - if (w.shape[0] != C || w.shape[1] != 9) { - throw std::runtime_error("conv1d_same_depthwise_k9 weight must be [C, 9]"); - } - } else if (w.shape.size() == 3) { - if (w.shape[0] != C || w.shape[1] != 1 || w.shape[2] != 9) { - throw std::runtime_error("conv1d_same_depthwise_k9 weight must be [C, 1, 9]"); - } - } else { - throw std::runtime_error("conv1d_same_depthwise_k9 weight must be rank 2 or 3"); - } - - OpParams params{}; - params.output_precision = xin.precision; - return add_node(OpType::CONV1D_SAME_DEPTHWISE_K9, {input, weight}, {N, L, C}, params); -} - -size_t CactusGraph::conv1d_same_depthwise_k9(size_t input, size_t weight, size_t bias) { - const auto& xin = get_output_buffer(input); - const auto& w = get_output_buffer(weight); - const auto& b = get_output_buffer(bias); - - if (xin.shape.size() != 3) { - throw std::runtime_error("conv1d_same_depthwise_k9 expects input [N, L, C]"); - } - - const size_t N = xin.shape[0]; - const size_t L = xin.shape[1]; - const size_t C = xin.shape[2]; - - if (w.shape.size() == 2) { - if (w.shape[0] != C || w.shape[1] != 9) { - throw std::runtime_error("conv1d_same_depthwise_k9 weight must be [C, 9]"); - } - } else if (w.shape.size() == 3) { - if (w.shape[0] != C || w.shape[1] != 1 || w.shape[2] != 9) { - throw std::runtime_error("conv1d_same_depthwise_k9 weight must be [C, 1, 9]"); - } - } else { - throw std::runtime_error("conv1d_same_depthwise_k9 weight must be rank 2 or 3"); - } - - if (b.total_size != C) { - throw std::runtime_error("conv1d_same_depthwise_k9 bias size mismatch"); - } - - OpParams params{}; - params.output_precision = xin.precision; - return add_node(OpType::CONV1D_SAME_DEPTHWISE_K9, {input, weight, bias}, {N, L, C}, params); -} - -size_t CactusGraph::conv1d_pointwise(size_t input, size_t weight) { - const auto& xin = get_output_buffer(input); - const auto& w = get_output_buffer(weight); - - if (xin.shape.size() != 3) { - throw std::runtime_error("conv1d_pointwise expects input [N, L, C_in]"); - } - - const size_t N = xin.shape[0]; - const size_t L = xin.shape[1]; - const size_t C_in = xin.shape[2]; - size_t C_out = 0; - - if (w.shape.size() == 2) { - C_out = w.shape[0]; - if (w.shape[1] != C_in) { - throw std::runtime_error("conv1d_pointwise weight must be [C_out, C_in]"); - } - } else if (w.shape.size() == 3) { - C_out = w.shape[0]; - if (w.shape[1] != C_in || w.shape[2] != 1) { - throw std::runtime_error("conv1d_pointwise weight must be [C_out, C_in, 1]"); - } - } else { - throw std::runtime_error("conv1d_pointwise weight must be rank 2 or 3"); - } - - OpParams params{}; - params.output_precision = xin.precision; - return add_node(OpType::CONV1D_POINTWISE, {input, weight}, {N, L, C_out}, params); -} - -size_t CactusGraph::conv1d_pointwise(size_t input, size_t weight, size_t bias) { - const auto& xin = get_output_buffer(input); - const auto& w = get_output_buffer(weight); - const auto& b = get_output_buffer(bias); - - if (xin.shape.size() != 3) { - throw std::runtime_error("conv1d_pointwise expects input [N, L, C_in]"); - } - - const size_t C_in = xin.shape[2]; - size_t C_out = 0; - - if (w.shape.size() == 2) { - C_out = w.shape[0]; - if (w.shape[1] != C_in) { - throw std::runtime_error("conv1d_pointwise weight must be [C_out, C_in]"); - } - } else if (w.shape.size() == 3) { - C_out = w.shape[0]; - if (w.shape[1] != C_in || w.shape[2] != 1) { - throw std::runtime_error("conv1d_pointwise weight must be [C_out, C_in, 1]"); - } - } else { - throw std::runtime_error("conv1d_pointwise weight must be rank 2 or 3"); - } - - if (b.total_size != C_out) { - throw std::runtime_error("conv1d_pointwise bias size mismatch"); - } - - OpParams params{}; - params.output_precision = xin.precision; - return add_node(OpType::CONV1D_POINTWISE, {input, weight, bias}, {xin.shape[0], xin.shape[1], C_out}, params); -} - -size_t CactusGraph::conv2d_k3s2p1(size_t input, size_t weight) { - const auto& xin = get_output_buffer(input); - const auto& w = get_output_buffer(weight); - - if (xin.shape.size() != 4) { - throw std::runtime_error("conv2d_k3s2p1 expects input [N, C_in, H, W]"); - } - if (w.shape.size() != 4) { - throw std::runtime_error("conv2d_k3s2p1 weight must be [C_out, C_in, 3, 3]"); - } - - const size_t N = xin.shape[0]; - const size_t C_in = xin.shape[1]; - const size_t H = xin.shape[2]; - const size_t W = xin.shape[3]; - const size_t C_out = w.shape[0]; - - if (w.shape[1] != C_in || w.shape[2] != 3 || w.shape[3] != 3) { - throw std::runtime_error("conv2d_k3s2p1 weight must match [C_out, C_in, 3, 3]"); - } - if (H == 0 || W == 0) { - throw std::runtime_error("conv2d_k3s2p1 input spatial dimensions must be > 0"); - } - - const size_t H_out = (H - 1) / 2 + 1; - const size_t W_out = (W - 1) / 2 + 1; - - OpParams params{}; - params.output_precision = xin.precision; - return add_node(OpType::CONV2D_K3S2P1, {input, weight}, {N, C_out, H_out, W_out}, params); -} - -size_t CactusGraph::conv2d_k3s2p1(size_t input, size_t weight, size_t bias) { - const auto& xin = get_output_buffer(input); - const auto& w = get_output_buffer(weight); - const auto& b = get_output_buffer(bias); - - if (xin.shape.size() != 4) { - throw std::runtime_error("conv2d_k3s2p1 expects input [N, C_in, H, W]"); - } - - const size_t C_in = xin.shape[1]; - const size_t H = xin.shape[2]; - const size_t W = xin.shape[3]; - const size_t C_out = w.shape[0]; - - if (w.shape.size() != 4 || w.shape[1] != C_in || w.shape[2] != 3 || w.shape[3] != 3) { - throw std::runtime_error("conv2d_k3s2p1 weight must be [C_out, C_in, 3, 3]"); - } - if (b.total_size != C_out) { - throw std::runtime_error("conv2d_k3s2p1 bias size mismatch"); - } - if (H == 0 || W == 0) { - throw std::runtime_error("conv2d_k3s2p1 input spatial dimensions must be > 0"); - } - - const size_t N = xin.shape[0]; - const size_t H_out = (H - 1) / 2 + 1; - const size_t W_out = (W - 1) / 2 + 1; - - OpParams params{}; - params.output_precision = xin.precision; - return add_node(OpType::CONV2D_K3S2P1, {input, weight, bias}, {N, C_out, H_out, W_out}, params); -} - -size_t CactusGraph::conv2d_depthwise_k3s2p1(size_t input, size_t weight) { - const auto& xin = get_output_buffer(input); - const auto& w = get_output_buffer(weight); - - if (xin.shape.size() != 4) { - throw std::runtime_error("conv2d_depthwise_k3s2p1 expects input [N, C, H, W]"); - } - - const size_t N = xin.shape[0]; - const size_t C = xin.shape[1]; - const size_t H = xin.shape[2]; - const size_t W = xin.shape[3]; - - if (w.shape.size() == 3) { - if (w.shape[0] != C || w.shape[1] != 3 || w.shape[2] != 3) { - throw std::runtime_error("conv2d_depthwise_k3s2p1 weight must be [C, 3, 3]"); - } - } else if (w.shape.size() == 4) { - if (w.shape[0] != C || w.shape[1] != 1 || w.shape[2] != 3 || w.shape[3] != 3) { - throw std::runtime_error("conv2d_depthwise_k3s2p1 weight must be [C, 1, 3, 3]"); - } - } else { - throw std::runtime_error("conv2d_depthwise_k3s2p1 weight must be rank 3 or 4"); - } - - if (H == 0 || W == 0) { - throw std::runtime_error("conv2d_depthwise_k3s2p1 input spatial dimensions must be > 0"); - } - - const size_t H_out = (H - 1) / 2 + 1; - const size_t W_out = (W - 1) / 2 + 1; - OpParams params{}; - params.output_precision = xin.precision; - return add_node(OpType::CONV2D_DEPTHWISE_K3S2P1, {input, weight}, {N, C, H_out, W_out}, params); -} - -size_t CactusGraph::conv2d_depthwise_k3s2p1(size_t input, size_t weight, size_t bias) { - const auto& xin = get_output_buffer(input); - const auto& w = get_output_buffer(weight); - const auto& b = get_output_buffer(bias); - - if (xin.shape.size() != 4) { - throw std::runtime_error("conv2d_depthwise_k3s2p1 expects input [N, C, H, W]"); - } - - const size_t C = xin.shape[1]; - const size_t H = xin.shape[2]; - const size_t W = xin.shape[3]; - - if (w.shape.size() == 3) { - if (w.shape[0] != C || w.shape[1] != 3 || w.shape[2] != 3) { - throw std::runtime_error("conv2d_depthwise_k3s2p1 weight must be [C, 3, 3]"); - } - } else if (w.shape.size() == 4) { - if (w.shape[0] != C || w.shape[1] != 1 || w.shape[2] != 3 || w.shape[3] != 3) { - throw std::runtime_error("conv2d_depthwise_k3s2p1 weight must be [C, 1, 3, 3]"); - } - } else { - throw std::runtime_error("conv2d_depthwise_k3s2p1 weight must be rank 3 or 4"); - } - - if (b.total_size != C) { - throw std::runtime_error("conv2d_depthwise_k3s2p1 bias size mismatch"); - } - if (H == 0 || W == 0) { - throw std::runtime_error("conv2d_depthwise_k3s2p1 input spatial dimensions must be > 0"); - } - - const size_t N = xin.shape[0]; - const size_t H_out = (H - 1) / 2 + 1; - const size_t W_out = (W - 1) / 2 + 1; - - OpParams params{}; - params.output_precision = xin.precision; - return add_node(OpType::CONV2D_DEPTHWISE_K3S2P1, {input, weight, bias}, {N, C, H_out, W_out}, params); -} - -size_t CactusGraph::conv2d_pointwise_1x1(size_t input, size_t weight) { - const auto& xin = get_output_buffer(input); - const auto& w = get_output_buffer(weight); - - if (xin.shape.size() != 4) { - throw std::runtime_error("conv2d_pointwise_1x1 expects input [N, C_in, H, W]"); - } - - const size_t N = xin.shape[0]; - const size_t C_in = xin.shape[1]; - const size_t H = xin.shape[2]; - const size_t W = xin.shape[3]; - size_t C_out = 0; - - if (w.shape.size() == 2) { - C_out = w.shape[0]; - if (w.shape[1] != C_in) { - throw std::runtime_error("conv2d_pointwise_1x1 weight must be [C_out, C_in]"); - } - } else if (w.shape.size() == 4) { - C_out = w.shape[0]; - if (w.shape[1] != C_in || w.shape[2] != 1 || w.shape[3] != 1) { - throw std::runtime_error("conv2d_pointwise_1x1 weight must be [C_out, C_in, 1, 1]"); - } - } else { - throw std::runtime_error("conv2d_pointwise_1x1 weight must be rank 2 or 4"); - } - - if (H == 0 || W == 0) { - throw std::runtime_error("conv2d_pointwise_1x1 input spatial dimensions must be > 0"); - } - - OpParams params{}; - params.output_precision = xin.precision; - return add_node(OpType::CONV2D_POINTWISE_1X1, {input, weight}, {N, C_out, H, W}, params); -} - -size_t CactusGraph::conv2d_pointwise_1x1(size_t input, size_t weight, size_t bias) { - const auto& xin = get_output_buffer(input); - const auto& w = get_output_buffer(weight); - const auto& b = get_output_buffer(bias); - - if (xin.shape.size() != 4) { - throw std::runtime_error("conv2d_pointwise_1x1 expects input [N, C_in, H, W]"); - } - - const size_t C_in = xin.shape[1]; - const size_t H = xin.shape[2]; - const size_t W = xin.shape[3]; - size_t C_out = 0; - - if (w.shape.size() == 2) { - C_out = w.shape[0]; - if (w.shape[1] != C_in) { - throw std::runtime_error("conv2d_pointwise_1x1 weight must be [C_out, C_in]"); - } - } else if (w.shape.size() == 4) { - C_out = w.shape[0]; - if (w.shape[1] != C_in || w.shape[2] != 1 || w.shape[3] != 1) { - throw std::runtime_error("conv2d_pointwise_1x1 weight must be [C_out, C_in, 1, 1]"); - } - } else { - throw std::runtime_error("conv2d_pointwise_1x1 weight must be rank 2 or 4"); - } - - if (b.total_size != C_out) { - throw std::runtime_error("conv2d_pointwise_1x1 bias size mismatch"); - } - if (H == 0 || W == 0) { - throw std::runtime_error("conv2d_pointwise_1x1 input spatial dimensions must be > 0"); - } - - OpParams params{}; - params.output_precision = xin.precision; - return add_node(OpType::CONV2D_POINTWISE_1X1, {input, weight, bias}, {xin.shape[0], C_out, H, W}, params); -} - -size_t CactusGraph::lstm_cell(size_t input, size_t h_prev, size_t c_prev, size_t weight_ih, size_t weight_hh, size_t bias_ih, size_t bias_hh) { - const auto& h_buffer = get_output_buffer(h_prev); - std::vector output_shape = {h_buffer.shape[0], h_buffer.shape[1], 2}; - return add_node(OpType::LSTM_CELL, {input, h_prev, c_prev, weight_ih, weight_hh, bias_ih, bias_hh}, output_shape, {}); -} - -size_t CactusGraph::gated_deltanet_decode(size_t query, size_t key, size_t value, size_t gate_log, size_t beta, - size_t initial_state, float scale) { - const auto& q = get_output_buffer(query); - const auto& k = get_output_buffer(key); - const auto& v = get_output_buffer(value); - const auto& g = get_output_buffer(gate_log); - const auto& b = get_output_buffer(beta); - const auto& s = get_output_buffer(initial_state); - - if (q.shape.size() != 4 || k.shape.size() != 4 || v.shape.size() != 4) { - throw std::runtime_error("gated_deltanet_decode expects query/key/value rank 4 [B, T, H, D]"); - } - if (g.shape.size() != 3 || b.shape.size() != 3) { - throw std::runtime_error("gated_deltanet_decode expects gate_log/beta rank 3 [B, T, H]"); - } - if (s.shape.size() != 4) { - throw std::runtime_error("gated_deltanet_decode expects initial_state rank 4 [B, K, H, V]"); - } - - const size_t B = q.shape[0]; - const size_t T = q.shape[1]; - const size_t Hq = q.shape[2]; - const size_t K = q.shape[3]; - const size_t Hv = v.shape[2]; - const size_t V = v.shape[3]; - if (T != 1) { - throw std::runtime_error("gated_deltanet_decode expects sequence length T=1"); - } - auto is_supported_precision = [](Precision p) { - return p == Precision::FP16 || p == Precision::FP32; - }; - if (!is_supported_precision(q.precision) || !is_supported_precision(k.precision) || - !is_supported_precision(v.precision) || !is_supported_precision(g.precision) || - !is_supported_precision(b.precision) || !is_supported_precision(s.precision)) { - throw std::runtime_error("gated_deltanet_decode requires FP16/FP32 inputs"); - } - - float op_scale = scale; - if (op_scale == 0.0f) { - op_scale = 1.0f / std::sqrt(static_cast(K)); - } - - OpParams params; - params.scale = op_scale; - params.num_kv_heads = Hq; - params.output_precision = Precision::FP16; - return add_node(OpType::GATED_DELTANET_DECODE, - {query, key, value, gate_log, beta, initial_state}, - {B, static_cast(1 + K), Hv, V}, - params); -} - -size_t CactusGraph::gated_deltanet_prefill(size_t query, size_t key, size_t value, size_t gate_log, size_t beta, - size_t initial_state, size_t chunk_size, float scale) { - const auto& q = get_output_buffer(query); - const auto& k = get_output_buffer(key); - const auto& v = get_output_buffer(value); - const auto& g = get_output_buffer(gate_log); - const auto& b = get_output_buffer(beta); - const auto& s = get_output_buffer(initial_state); - - if (q.shape.size() != 4 || k.shape.size() != 4 || v.shape.size() != 4) { - throw std::runtime_error("gated_deltanet_prefill expects query/key/value rank 4 [B, T, H, D]"); - } - if (g.shape.size() != 3 || b.shape.size() != 3) { - throw std::runtime_error("gated_deltanet_prefill expects gate_log/beta rank 3 [B, T, H]"); - } - if (s.shape.size() != 4) { - throw std::runtime_error("gated_deltanet_prefill expects initial_state rank 4 [B, K, H, V]"); - } - - const size_t B = q.shape[0]; - const size_t T = q.shape[1]; - const size_t Hq = q.shape[2]; - const size_t K = q.shape[3]; - const size_t Hv = v.shape[2]; - const size_t V = v.shape[3]; - auto is_supported_precision = [](Precision p) { - return p == Precision::FP16 || p == Precision::FP32; - }; - if (!is_supported_precision(q.precision) || !is_supported_precision(k.precision) || - !is_supported_precision(v.precision) || !is_supported_precision(g.precision) || - !is_supported_precision(b.precision) || !is_supported_precision(s.precision)) { - throw std::runtime_error("gated_deltanet_prefill requires FP16/FP32 inputs"); - } - - if (chunk_size == 0) { - chunk_size = 64; - } - - float op_scale = scale; - if (op_scale == 0.0f) { - op_scale = 1.0f / std::sqrt(static_cast(K)); - } - - OpParams params; - params.scale = op_scale; - params.chunk_size = chunk_size; - params.num_kv_heads = Hq; - params.output_precision = Precision::FP16; - return add_node(OpType::GATED_DELTANET_PREFILL, - {query, key, value, gate_log, beta, initial_state}, - {B, T + K, Hv, V}, - params); -} - -size_t CactusGraph::stft(size_t input, size_t weight, size_t stride, size_t num_fft_bins) { - const auto& xin = get_output_buffer(input); - const auto& w = get_output_buffer(weight); - - if (xin.shape.size() != 3) throw std::runtime_error("stft expects N,C,L input"); - if (w.shape.size() != 3) throw std::runtime_error("stft weight expects [C_out, C_in, K]"); - - size_t N = xin.shape[0]; - size_t L = xin.shape[2]; - size_t K = w.shape[2]; - size_t L_out = (L - K) / stride + 1; - - OpParams params{}; - params.stride = stride; - params.num_fft_bins = num_fft_bins; - - return add_node(OpType::STFT, {input, weight}, {N, 2 * num_fft_bins, L_out}, params); -} - -size_t CactusGraph::concat(size_t input1, size_t input2, int axis) { - const auto& buffer1 = get_output_buffer(input1); - const auto& buffer2 = get_output_buffer(input2); - - if (buffer1.shape.size() != buffer2.shape.size()) { - throw std::runtime_error("Concat requires inputs with same number of dimensions"); - } - - std::vector output_shape = buffer1.shape; - size_t ndims = output_shape.size(); - - if (axis < 0) axis += ndims; - if (axis < 0 || static_cast(axis) >= ndims) { - throw std::runtime_error("Invalid axis for concat operation"); - } - - for (size_t i = 0; i < ndims; ++i) { - if (i != static_cast(axis) && buffer1.shape[i] != buffer2.shape[i]) { - throw std::runtime_error("Concat inputs must have same shape except on concat axis"); - } - } - - output_shape[axis] = buffer1.shape[axis] + buffer2.shape[axis]; - - OpParams params; - params.axis = axis; - return add_node(OpType::CONCAT, {input1, input2}, output_shape, params); -} - -size_t CactusGraph::cat(const std::vector& inputs, int axis) { - if (inputs.empty()) { - throw std::runtime_error("Cat requires at least one input"); - } - - const auto& first_buffer = get_output_buffer(inputs[0]); - std::vector output_shape = first_buffer.shape; - size_t ndims = output_shape.size(); - - if (axis < 0) axis += ndims; - if (axis < 0 || static_cast(axis) >= ndims) { - throw std::runtime_error("Invalid axis for cat operation"); - } - - for (size_t i = 1; i < inputs.size(); ++i) { - const auto& buffer = get_output_buffer(inputs[i]); - if (buffer.shape.size() != ndims) { - throw std::runtime_error("All inputs to cat must have same number of dimensions"); - } - for (size_t d = 0; d < ndims; ++d) { - if (d != static_cast(axis) && buffer.shape[d] != output_shape[d]) { - throw std::runtime_error("All inputs to cat must have same shape except on cat axis"); - } - } - output_shape[axis] += buffer.shape[axis]; - } - - OpParams params; - params.axis = axis; - return add_node(OpType::CAT, inputs, output_shape, params); -} - -size_t CactusGraph::scatter_topk(size_t indices, size_t values, size_t num_classes) { - const auto& indices_buffer = get_output_buffer(indices); - const auto& values_buffer = get_output_buffer(values); - - if (indices_buffer.shape != values_buffer.shape) { - throw std::runtime_error("ScatterTopK requires indices and values with identical shapes"); - } - if (indices_buffer.shape.size() != 2) { - throw std::runtime_error("ScatterTopK currently supports 2D tensors [batch, top_k]"); - } - if (indices_buffer.precision != Precision::FP32 || values_buffer.precision != Precision::FP32) { - throw std::runtime_error("ScatterTopK expects FP32 indices and values"); - } - - std::vector output_shape = {num_classes, indices_buffer.shape[0]}; - OpParams params{.output_precision = Precision::FP32, .num_classes = num_classes}; - return add_node(OpType::SCATTER_TOPK, {indices, values}, output_shape, params); -} - -size_t CactusGraph::sample(size_t logits, float temperature, float top_p, size_t top_k, - const std::unordered_map& logit_bias) { - return this->sample_with_options(logits, temperature, top_p, 0.15f, 1.1f, top_k, logit_bias); -} - -size_t CactusGraph::sample_with_options(size_t logits, float temperature, float top_p, - float min_p, float repetition_penalty, size_t top_k, - const std::unordered_map& logit_bias) { - const auto& logits_buffer = get_output_buffer(logits); - - if (logits_buffer.shape.empty()) { - throw std::runtime_error("Sample requires non-empty logits tensor"); - } - - OpParams params; - params.temperature = temperature; - params.top_p = top_p; - params.min_p = min_p; - params.repetition_penalty = repetition_penalty; - params.top_k = top_k; - params.random_seed = std::chrono::high_resolution_clock::now().time_since_epoch().count(); - params.output_precision = Precision::FP32; - - if (!logit_bias.empty()) { - params.bias_indices.reserve(logit_bias.size()); - params.bias_values.reserve(logit_bias.size()); - for (const auto& [idx, val] : logit_bias) { - params.bias_indices.push_back(idx); - params.bias_values.push_back(val); - } - } - - std::vector output_shape = {1}; - return add_node(OpType::SAMPLE, {logits}, output_shape, params); -} - -size_t CactusGraph::scalar_add(size_t input, float value) { - OpParams params{}; - params.scalar = value; - params.output_precision = get_output_buffer(input).precision; - return add_node(OpType::SCALAR_ADD, {input}, {}, params); -} - -size_t CactusGraph::scalar_subtract(size_t input, float value) { - OpParams params{}; - params.scalar = value; - params.output_precision = get_output_buffer(input).precision; - return add_node(OpType::SCALAR_SUBTRACT, {input}, {}, params); -} - -size_t CactusGraph::scalar_multiply(size_t input, float value) { - OpParams params{}; - params.scalar = value; - params.output_precision = get_output_buffer(input).precision; - return add_node(OpType::SCALAR_MULTIPLY, {input}, {}, params); -} - -size_t CactusGraph::scalar_divide(size_t input, float value) { - OpParams params{}; - params.scalar = value; - params.output_precision = get_output_buffer(input).precision; - return add_node(OpType::SCALAR_DIVIDE, {input}, {}, params); -} - -size_t CactusGraph::scalar_exp(size_t input) { - return add_node(OpType::SCALAR_EXP, {input}, {}); -} - -size_t CactusGraph::scalar_sqrt(size_t input) { - return add_node(OpType::SCALAR_SQRT, {input}, {}); -} - -size_t CactusGraph::scalar_cos(size_t input) { - return add_node(OpType::SCALAR_COS, {input}, {}); -} - -size_t CactusGraph::scalar_sin(size_t input) { - return add_node(OpType::SCALAR_SIN, {input}, {}); -} - -size_t CactusGraph::scalar_log(size_t input) { - return add_node(OpType::SCALAR_LOG, {input}, {}); -} - -size_t CactusGraph::relu(size_t input) { - return add_node(OpType::RELU, {input}, {}); -} - -size_t CactusGraph::silu(size_t input) { - return add_node(OpType::SILU, {input}, {}); -} - -size_t CactusGraph::gelu(size_t input) { - return add_node(OpType::GELU, {input}, {}); -} - -size_t CactusGraph::gelu_erf(size_t input) { - return add_node(OpType::GELU_ERF, {input}, {}); -} - -size_t CactusGraph::sigmoid(size_t input) { - return add_node(OpType::SIGMOID, {input}, {}); -} - -size_t CactusGraph::tanh(size_t input) { - return add_node(OpType::TANH, {input}, {}); -} - -size_t CactusGraph::glu(size_t input, int axis) { - const auto& xin = get_output_buffer(input); - if (xin.shape.empty()) { - throw std::runtime_error("glu expects non-scalar input"); - } - - int actual_axis = axis; - if (actual_axis < 0) { - actual_axis += static_cast(xin.shape.size()); - } - if (actual_axis < 0 || static_cast(actual_axis) >= xin.shape.size()) { - throw std::runtime_error("glu axis out of range"); - } - - std::vector out_shape = xin.shape; - const size_t axis_size = out_shape[static_cast(actual_axis)]; - if ((axis_size % 2) != 0) { - throw std::runtime_error("glu split dimension must be even"); - } - out_shape[static_cast(actual_axis)] = axis_size / 2; - - OpParams params{}; - params.axis = actual_axis; - params.output_precision = xin.precision; - return add_node(OpType::GLU, {input}, out_shape, params); -} - -size_t CactusGraph::rope_gptj(size_t input, float theta, size_t position_offset, size_t rot_dim, ComputeBackend backend) { - OpParams params; - params.theta = theta; - params.position_offset = position_offset; - params.scalar = static_cast(rot_dim); - params.backend = backend; - return add_node(OpType::ROPE_GPTJ, {input}, {}, params); -} - -size_t CactusGraph::gather(size_t tensor, size_t indices) { - const auto& tensor_buffer = get_output_buffer(tensor); - const auto& idx_shape = get_output_buffer(indices).shape; - - if (tensor_buffer.shape.empty()) { - throw std::runtime_error("Cannot gather from scalar tensor"); - } - - std::vector output_shape = idx_shape; - for (size_t i = 1; i < tensor_buffer.shape.size(); i++) { - output_shape.push_back(tensor_buffer.shape[i]); - } - - OpParams params; - params.output_precision = tensor_buffer.precision; - - return add_node(OpType::GATHER, {tensor, indices}, output_shape, params); -} - -size_t CactusGraph::slice(size_t input, int axis, size_t start, size_t length) { - const auto& input_buffer = get_output_buffer(input); - if (input_buffer.shape.empty()) { - throw std::runtime_error("Cannot slice a scalar tensor"); - } - - size_t axis_index = static_cast(axis); - size_t axis_size = input_buffer.shape[axis_index]; - - if (start + length > axis_size) { - throw std::runtime_error("Slice range extends beyond axis size"); - } - - std::vector output_shape = input_buffer.shape; - output_shape[axis_index] = length; - - OpParams params; - params.axis = axis_index; - params.slice_start = start; - params.slice_length = length; - params.output_precision = input_buffer.precision; - - return add_node(OpType::SLICE, {input}, output_shape, params); -} - -size_t CactusGraph::embedding(size_t embedding_tensor, size_t indices) { - const auto& emb_buffer = get_output_buffer(embedding_tensor); - const auto& idx_shape = get_output_buffer(indices).shape; - - if (emb_buffer.shape.size() != 2) { - std::cerr << "Error: Embedding tensor " << embedding_tensor << " has invalid shape: ["; - for(auto d : emb_buffer.shape) std::cerr << d << ","; - std::cerr << "]. OpType=" << (int)nodes_[node_index_map_[embedding_tensor]]->op_type - << " ExtData=" << emb_buffer.external_data << std::endl; - throw std::runtime_error("Embedding tensor must be 2D [vocab_size, hidden_dim]"); - } - - std::vector output_shape = idx_shape; - output_shape.push_back(emb_buffer.shape[1]); - - OpParams params; - params.output_precision = Precision::FP16; - return add_node(OpType::EMBEDDING, {embedding_tensor, indices}, output_shape, params); -} - -size_t CactusGraph::bilinear_interpolation(size_t pos_embeds, size_t dst_height, size_t dst_width, bool align_corners) { - const auto& pos_embeds_buffer = get_output_buffer(pos_embeds); - size_t embed_dim = pos_embeds_buffer.shape[1]; - std::vector output_shape = {dst_height * dst_width, embed_dim}; - - OpParams params; - params.dst_height = dst_height; - params.dst_width = dst_width; - params.align_corners = align_corners; - params.output_precision = Precision::FP16; - - return add_node(OpType::BILINEAR_INTERPOLATION, {pos_embeds}, output_shape, params); -} - -size_t CactusGraph::precision_cast(size_t input, Precision target_precision) { - OpParams params{}; - params.output_precision = target_precision; - return add_node(OpType::PRECISION_CAST, {input}, {}, params); -} - -size_t CactusGraph::quantize_activations(size_t input) { - const auto& input_buffer = get_output_buffer(input); - - if (input_buffer.precision != Precision::FP16) { - throw std::invalid_argument("quantize_activations requires FP16 input"); - } - - OpParams params{}; - params.output_precision = Precision::INT8; - return add_node(OpType::QUANTIZE_ACTIVATIONS, {input}, input_buffer.shape, params); -} - -size_t CactusGraph::add_node(OpType op_type, const std::vector& inputs, const std::vector& output_shape, const OpParams& params) { - auto node = std::make_unique(next_node_id_, op_type); - node->input_ids = inputs; - node->params = params; - - std::vector result_shape = output_shape; - if (result_shape.empty() && !inputs.empty()) { - result_shape = nodes_[node_index_map_[inputs[0]]]->output_buffer.shape; - } - - Precision result_precision = params.output_precision; - if (op_type == OpType::PRECISION_CAST) { - result_precision = params.output_precision; - } else if (result_precision == Precision::INT8 && !inputs.empty()) { - result_precision = nodes_[node_index_map_[inputs[0]]]->output_buffer.precision; - } - - node->output_buffer = BufferDesc(result_shape, result_precision); - - size_t node_id = next_node_id_++; - node_index_map_[node_id] = nodes_.size(); - nodes_.push_back(std::move(node)); - - return node_id; -} - -const BufferDesc& CactusGraph::get_output_buffer(size_t node_id) const { - return nodes_[node_index_map_.at(node_id)]->output_buffer; -} - -size_t CactusGraph::persistent(size_t source_node) { - const auto& source_buffer = get_output_buffer(source_node); - OpParams params; - params.output_precision = source_buffer.precision; - size_t node_id = add_node(OpType::PERSISTENT, {source_node}, source_buffer.shape, params); - persistent_node_ids_.insert(node_id); - return node_id; -} - -bool CactusGraph::is_populated(size_t persistent_node_id) const { - return populated_node_ids_.count(persistent_node_id) > 0; -} - -void CactusGraph::invalidate_persistent(size_t persistent_node_id) { - populated_node_ids_.erase(persistent_node_id); - persistent_node_ids_.erase(persistent_node_id); -} - -size_t CactusGraph::altup_predict(size_t coefs, const size_t* streams, size_t num_streams) { - const auto& stream0_buf = get_output_buffer(streams[0]); - - size_t seq_len = stream0_buf.shape[0]; - size_t hidden_dim = stream0_buf.shape[1]; - - std::vector input_ids = {coefs}; - for (size_t i = 0; i < num_streams; i++) - input_ids.push_back(streams[i]); - - OpParams params; - params.num_altup_inputs = num_streams; - return add_node(OpType::ALTUP_PREDICT, input_ids, {num_streams * seq_len, hidden_dim}, params); -} - -size_t CactusGraph::altup_correct(size_t coefs, size_t innovation, const size_t* predictions, size_t num_predictions) { - const auto& pred0_buf = get_output_buffer(predictions[0]); - - size_t seq_len = pred0_buf.shape[0]; - size_t hidden_dim = pred0_buf.shape[1]; - - std::vector input_ids = {coefs, innovation}; - for (size_t i = 0; i < num_predictions; i++) - input_ids.push_back(predictions[i]); - - OpParams params; - params.num_altup_inputs = num_predictions; - return add_node(OpType::ALTUP_CORRECT, input_ids, {num_predictions * seq_len, hidden_dim}, params); -} - -size_t CactusGraph::gaussian_topk(size_t input, float ppf) { - const auto& in_buf = get_output_buffer(input); - OpParams params; - params.scalar = ppf; - return add_node(OpType::GAUSSIAN_TOPK, {input}, in_buf.shape, params); -} - -size_t CactusGraph::leaky_relu(size_t input, float negative_slope) { - const auto& in_buf = get_output_buffer(input); - OpParams params; - params.scalar = negative_slope; - return add_node(OpType::LEAKY_RELU, {input}, in_buf.shape, params); -} - -size_t CactusGraph::bilstm_sequence(size_t input, size_t w_ih_fwd, size_t w_hh_fwd, size_t b_ih_fwd, size_t b_hh_fwd, - size_t w_ih_bwd, size_t w_hh_bwd, size_t b_ih_bwd, size_t b_hh_bwd) { - const auto& in_buf = get_output_buffer(input); - const auto& w_ih = get_output_buffer(w_ih_fwd); - size_t batch = in_buf.shape[0]; - size_t seq_len = in_buf.shape[1]; - size_t hidden_size = w_ih.shape[0] / 4; - return add_node(OpType::BILSTM_SEQUENCE, - {input, w_ih_fwd, w_hh_fwd, b_ih_fwd, b_hh_fwd, w_ih_bwd, w_hh_bwd, b_ih_bwd, b_hh_bwd}, - {batch, seq_len, 2 * hidden_size}, {}); -} - -size_t CactusGraph::maxpool1d(size_t input, size_t kernel_size, size_t stride) { - const auto& in_buf = get_output_buffer(input); - size_t batch = in_buf.shape[0]; - size_t channels = in_buf.shape[1]; - size_t input_length = in_buf.shape[2]; - size_t output_length = (input_length - kernel_size) / stride + 1; - - OpParams params; - params.kernel_size = kernel_size; - params.stride = stride; - return add_node(OpType::MAXPOOL1D, {input}, {batch, channels, output_length}, params); -} - -size_t CactusGraph::conv2d_k3s1p1(size_t input, size_t weight) { - const auto& xin = get_output_buffer(input); - const auto& w = get_output_buffer(weight); - - if (xin.shape.size() != 4) { - throw std::runtime_error("conv2d_k3s1p1 expects input [N, C_in, H, W]"); - } - if (w.shape.size() != 4) { - throw std::runtime_error("conv2d_k3s1p1 weight must be [C_out, C_in, 3, 3]"); - } - - const size_t N = xin.shape[0]; - const size_t C_in = xin.shape[1]; - const size_t H = xin.shape[2]; - const size_t W = xin.shape[3]; - const size_t C_out = w.shape[0]; - - if (w.shape[1] != C_in || w.shape[2] != 3 || w.shape[3] != 3) { - throw std::runtime_error("conv2d_k3s1p1 weight must match [C_out, C_in, 3, 3]"); - } - if (H == 0 || W == 0) { - throw std::runtime_error("conv2d_k3s1p1 input spatial dimensions must be > 0"); - } - - OpParams params{}; - params.output_precision = xin.precision; - return add_node(OpType::CONV2D_K3S1P1, {input, weight}, {N, C_out, H, W}, params); -} - -size_t CactusGraph::conv2d_k3s1p1(size_t input, size_t weight, size_t bias) { - const auto& xin = get_output_buffer(input); - const auto& w = get_output_buffer(weight); - const auto& b = get_output_buffer(bias); - - if (xin.shape.size() != 4) { - throw std::runtime_error("conv2d_k3s1p1 expects input [N, C_in, H, W]"); - } - - const size_t N = xin.shape[0]; - const size_t C_in = xin.shape[1]; - const size_t H = xin.shape[2]; - const size_t W = xin.shape[3]; - - if (w.shape.size() != 4 || w.shape[1] != C_in || w.shape[2] != 3 || w.shape[3] != 3) { - throw std::runtime_error("conv2d_k3s1p1 weight must be [C_out, C_in, 3, 3]"); - } - const size_t C_out = w.shape[0]; - if (b.total_size != C_out) { - throw std::runtime_error("conv2d_k3s1p1 bias size mismatch"); - } - if (H == 0 || W == 0) { - throw std::runtime_error("conv2d_k3s1p1 input spatial dimensions must be > 0"); - } - - OpParams params{}; - params.output_precision = xin.precision; - return add_node(OpType::CONV2D_K3S1P1, {input, weight, bias}, {N, C_out, H, W}, params); -} - -size_t CactusGraph::stats_pool(size_t input) { - const auto& xin = get_output_buffer(input); - size_t batch = xin.shape[0]; - size_t features = 1; - for (size_t i = 1; i < xin.shape.size() - 1; ++i) features *= xin.shape[i]; - return add_node(OpType::STATS_POOL, {input}, {batch, features * 2}); -} - -size_t CactusGraph::weighted_stats_pool(size_t input, size_t weights) { - const auto& xin = get_output_buffer(input); - size_t batch = xin.shape[0]; - size_t features = 1; - for (size_t i = 1; i < xin.shape.size() - 1; ++i) features *= xin.shape[i]; - return add_node(OpType::WEIGHTED_STATS_POOL, {input, weights}, {batch, features * 2}); -} diff --git a/cactus/graph/graph_core.cpp b/cactus/graph/graph_core.cpp deleted file mode 100644 index f418da72b..000000000 --- a/cactus/graph/graph_core.cpp +++ /dev/null @@ -1,265 +0,0 @@ -#include "graph.h" -#include -#include -#include - -size_t BufferPool::round_up_size(size_t size) const { - constexpr size_t ALIGNMENT = 64; - constexpr size_t MIN_BUCKET = 1024; - - if (size < MIN_BUCKET) return MIN_BUCKET; - - size_t aligned = (size + ALIGNMENT - 1) & ~(ALIGNMENT - 1); - - size_t bucket = MIN_BUCKET; - while (bucket < aligned) { - bucket *= 2; - } - return bucket; -} - -char* BufferPool::acquire(size_t byte_size) { - if (byte_size == 0) return nullptr; - - size_t bucket_size = round_up_size(byte_size); - - auto it = free_buffers_.find(bucket_size); - if (it != free_buffers_.end() && !it->second.empty()) { - auto buffer = std::move(it->second.back()); - it->second.pop_back(); - pool_bytes_ -= bucket_size; - active_bytes_ += bucket_size; - if (active_bytes_ > peak_bytes_) { - peak_bytes_ = active_bytes_; - } - return buffer.release(); - } - - active_bytes_ += bucket_size; - if (active_bytes_ > peak_bytes_) { - peak_bytes_ = active_bytes_; - } - return new char[bucket_size]; -} - -void BufferPool::release(char* ptr, size_t byte_size) { - if (!ptr || byte_size == 0) return; - - size_t bucket_size = round_up_size(byte_size); - active_bytes_ -= bucket_size; - pool_bytes_ += bucket_size; - - free_buffers_[bucket_size].push_back(std::unique_ptr(ptr)); -} - -void BufferPool::clear() { - free_buffers_.clear(); - pool_bytes_ = 0; -} - -BufferDesc::BufferDesc() - : total_size(0), byte_size(0), external_data(nullptr), pooled_data(nullptr), - precision(Precision::FP16) {} - -BufferDesc::BufferDesc(const std::vector& s, Precision prec) - : shape(s), external_data(nullptr), pooled_data(nullptr), precision(prec) { - total_size = 1; - for (size_t dim : shape) total_size *= dim; - byte_size = PrecisionTraits::packed_size_of(prec, total_size); -} - -BufferDesc::~BufferDesc() { - if (pooled_data) { - delete[] pooled_data; - pooled_data = nullptr; - } -} - -BufferDesc::BufferDesc(BufferDesc&& other) noexcept - : shape(std::move(other.shape)), - total_size(other.total_size), - byte_size(other.byte_size), - data(std::move(other.data)), - external_data(other.external_data), - pooled_data(other.pooled_data), - precision(other.precision), - group_size(other.group_size), - num_groups(other.num_groups), - scales_data(other.scales_data), - owned_scales(std::move(other.owned_scales)), - is_interleaved(other.is_interleaved), - original_N(other.original_N), - activation_scales_data(other.activation_scales_data), - owned_activation_scales(std::move(other.owned_activation_scales)), - num_rows_for_activation_scales(other.num_rows_for_activation_scales) { - other.total_size = 0; - other.byte_size = 0; - other.external_data = nullptr; - other.pooled_data = nullptr; - other.group_size = 0; - other.num_groups = 0; - other.scales_data = nullptr; - other.is_interleaved = false; - other.original_N = 0; - other.activation_scales_data = nullptr; - other.num_rows_for_activation_scales = 0; -} - -BufferDesc& BufferDesc::operator=(BufferDesc&& other) noexcept { - if (this != &other) { - if (pooled_data) { - delete[] pooled_data; - } - - shape = std::move(other.shape); - total_size = other.total_size; - byte_size = other.byte_size; - data = std::move(other.data); - external_data = other.external_data; - pooled_data = other.pooled_data; - precision = other.precision; - group_size = other.group_size; - num_groups = other.num_groups; - scales_data = other.scales_data; - owned_scales = std::move(other.owned_scales); - is_interleaved = other.is_interleaved; - original_N = other.original_N; - activation_scales_data = other.activation_scales_data; - owned_activation_scales = std::move(other.owned_activation_scales); - num_rows_for_activation_scales = other.num_rows_for_activation_scales; - - other.total_size = 0; - other.byte_size = 0; - other.external_data = nullptr; - other.pooled_data = nullptr; - other.group_size = 0; - other.num_groups = 0; - other.scales_data = nullptr; - other.is_interleaved = false; - other.original_N = 0; - other.activation_scales_data = nullptr; - other.num_rows_for_activation_scales = 0; - } - return *this; -} - -void* BufferDesc::get_data() { - if (external_data) return external_data; - if (pooled_data) return pooled_data; - return data.get(); -} - -const void* BufferDesc::get_data() const { - if (external_data) return external_data; - if (pooled_data) return pooled_data; - return data.get(); -} - -void BufferDesc::allocate() { - if (!data && !external_data && !pooled_data) { - data = std::make_unique(byte_size); - } -} - -void BufferDesc::allocate_from_pool(BufferPool& pool) { - if (!data && !external_data && !pooled_data && byte_size > 0) { - pooled_data = pool.acquire(byte_size); - } -} - -void BufferDesc::release_to_pool(BufferPool& pool) { - if (pooled_data && byte_size > 0) { - pool.release(pooled_data, byte_size); - pooled_data = nullptr; - } -} - -void BufferDesc::set_external(void* ptr) { - external_data = ptr; - data.reset(); - pooled_data = nullptr; -} - -// GraphNode implementation -GraphNode::GraphNode(size_t node_id, OpType type) : id(node_id), op_type(type) {} - -// TensorConfig implementation -TensorConfig& TensorConfig::global() { - static TensorConfig instance; - return instance; -} - -// BroadcastInfo implementation -BroadcastInfo BroadcastInfo::compute(const std::vector& lhs, const std::vector& rhs) { - BroadcastInfo info; - size_t max_dims = std::max(lhs.size(), rhs.size()); - info.output_shape.resize(max_dims); - - for (size_t i = 0; i < max_dims; ++i) { - size_t lhs_dim = i < lhs.size() ? lhs[lhs.size() - 1 - i] : 1; - size_t rhs_dim = i < rhs.size() ? rhs[rhs.size() - 1 - i] : 1; - - if (lhs_dim != rhs_dim && lhs_dim != 1 && rhs_dim != 1) { - throw std::invalid_argument("Shapes are not compatible for broadcasting"); - } - - info.output_shape[max_dims - 1 - i] = std::max(lhs_dim, rhs_dim); - } - - info.needs_broadcasting = (lhs != info.output_shape || rhs != info.output_shape); - return info; -} - -namespace ValidationUtils { - void validate_tensor_dims(const std::vector& shape, size_t required_dims, const std::string& op_name) { - if (shape.size() != required_dims) { - throw std::runtime_error(op_name + " requires " + std::to_string(required_dims) + - "D tensor, got " + std::to_string(shape.size()) + "D tensor"); - } - } - - void validate_precision(Precision actual, Precision required, const std::string& op_name) { - if (actual != required) { - std::string actual_str = (actual == Precision::INT8) ? "INT8" : "FP32"; - std::string required_str = (required == Precision::INT8) ? "INT8" : "FP32"; - throw std::runtime_error(op_name + " requires " + required_str + " precision, got " + actual_str); - } - } - - void validate_input_count(size_t actual, size_t required, const std::string& op_name) { - if (actual < required) { - throw std::runtime_error(op_name + " requires " + std::to_string(required) + - " inputs, got " + std::to_string(actual) + " inputs"); - } - } -} - -CactusGraph::CactusGraph() : next_node_id_(1) {} - -size_t CactusGraph::get_node_count() const { - return nodes_.size(); -} - -void CactusGraph::register_debug_node(uint32_t layer_idx, const std::string& name, size_t node_id) { - debug_nodes_.push_back({layer_idx, name, node_id}); -} - -void CactusGraph::capture_debug_node(uint32_t layer_idx, const std::string& name, size_t node_id) { - register_debug_node(layer_idx, name, node_id); -} - -const std::vector& CactusGraph::get_debug_nodes() const { - return debug_nodes_; -} - -void CactusGraph::clear_debug_nodes() { - debug_nodes_.clear(); -} - -void CactusGraph::allocate_buffers() { - for (auto& node : nodes_) { - if (node->op_type != OpType::INPUT) { - node->output_buffer.allocate(); - } - } -} diff --git a/cactus/graph/graph_execute.cpp b/cactus/graph/graph_execute.cpp deleted file mode 100644 index f8bbc11a5..000000000 --- a/cactus/graph/graph_execute.cpp +++ /dev/null @@ -1,806 +0,0 @@ -#include "graph.h" -#include "../kernel/kernel_utils.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using ComputeFn = void(*)(GraphNode&, const nodes_vector&, const node_index_map_t&); - -#define DECLARE_COMPUTE(name) \ - extern void name(GraphNode&, const nodes_vector&, const node_index_map_t&) - -DECLARE_COMPUTE(compute_binary_op_node); -DECLARE_COMPUTE(compute_unary_op_node); -DECLARE_COMPUTE(compute_activation_node); -DECLARE_COMPUTE(compute_reduce_node); -DECLARE_COMPUTE(compute_reshape_node); -DECLARE_COMPUTE(compute_precision_cast_node); -DECLARE_COMPUTE(compute_matmul_node); -DECLARE_COMPUTE(compute_rms_norm_node); -DECLARE_COMPUTE(compute_rope_node); -DECLARE_COMPUTE(compute_softmax_node); -DECLARE_COMPUTE(compute_attention_node); -DECLARE_COMPUTE(compute_attention_int8_hybrid_node); -DECLARE_COMPUTE(compute_rel_pos_bias_node); -DECLARE_COMPUTE(compute_layernorm_node); -DECLARE_COMPUTE(compute_conv1d_causal_node); -DECLARE_COMPUTE(compute_conv1d_k3_node); -DECLARE_COMPUTE(compute_conv1d_k7s3_node); -DECLARE_COMPUTE(compute_conv1d_node); -DECLARE_COMPUTE(compute_conv1d_same_depthwise_k9_node); -DECLARE_COMPUTE(compute_conv1d_pointwise_node); -DECLARE_COMPUTE(compute_conv2d_k3s2p1_node); -DECLARE_COMPUTE(compute_conv2d_depthwise_k3s2p1_node); -DECLARE_COMPUTE(compute_conv2d_pointwise_1x1_node); -DECLARE_COMPUTE(compute_glu_node); -DECLARE_COMPUTE(compute_batchnorm_node); -DECLARE_COMPUTE(compute_groupnorm_node); -DECLARE_COMPUTE(compute_rope_gptj_node); -DECLARE_COMPUTE(compute_lstm_cell_node); -DECLARE_COMPUTE(compute_gated_deltanet_decode_node); -DECLARE_COMPUTE(compute_gated_deltanet_prefill_node); -DECLARE_COMPUTE(compute_stft_node); -DECLARE_COMPUTE(compute_altup_predict_node); -DECLARE_COMPUTE(compute_altup_correct_node); -DECLARE_COMPUTE(compute_gaussian_topk_node); -DECLARE_COMPUTE(compute_maxpool1d_node); -DECLARE_COMPUTE(compute_bilstm_sequence_node); -DECLARE_COMPUTE(compute_conv2d_k3s1p1_node); -DECLARE_COMPUTE(compute_stats_pool_node); -DECLARE_COMPUTE(compute_transpose_node); -DECLARE_COMPUTE(compute_gather_node); -DECLARE_COMPUTE(compute_slice_node); -DECLARE_COMPUTE(compute_embedding_node); -DECLARE_COMPUTE(compute_concat_node); -DECLARE_COMPUTE(compute_cat_node); -DECLARE_COMPUTE(compute_index_node); -DECLARE_COMPUTE(compute_bilinear_interpolation_node); -DECLARE_COMPUTE(compute_sample_node); -DECLARE_COMPUTE(compute_topk_node); -DECLARE_COMPUTE(compute_scatter_topk_node); -DECLARE_COMPUTE(compute_moe_layer_node); -DECLARE_COMPUTE(compute_dense_mlp_int4_fused_node); -DECLARE_COMPUTE(compute_persistent_node); -DECLARE_COMPUTE(compute_quantize_activations_node); -extern void shrink_thread_local_buffers(); -#undef DECLARE_COMPUTE - -static const std::unordered_map dispatch_table = { - {OpType::ADD, compute_binary_op_node}, - {OpType::ADD_CLIPPED, compute_binary_op_node}, - {OpType::SUBTRACT, compute_binary_op_node}, - {OpType::MULTIPLY, compute_binary_op_node}, - {OpType::DIVIDE, compute_binary_op_node}, - {OpType::SCALAR_ADD, compute_unary_op_node}, - {OpType::SCALAR_SUBTRACT, compute_unary_op_node}, - {OpType::SCALAR_MULTIPLY, compute_unary_op_node}, - {OpType::SCALAR_DIVIDE, compute_unary_op_node}, - {OpType::SCALAR_EXP, compute_unary_op_node}, - {OpType::SCALAR_SQRT, compute_unary_op_node}, - {OpType::SCALAR_COS, compute_unary_op_node}, - {OpType::SCALAR_SIN, compute_unary_op_node}, - {OpType::SCALAR_LOG, compute_unary_op_node}, - {OpType::ABS, compute_unary_op_node}, - {OpType::POW, compute_unary_op_node}, - {OpType::RELU, compute_activation_node}, - {OpType::SILU, compute_activation_node}, - {OpType::GELU, compute_activation_node}, - {OpType::GELU_ERF, compute_activation_node}, - {OpType::SIGMOID, compute_activation_node}, - {OpType::TANH, compute_activation_node}, - {OpType::LEAKY_RELU, compute_activation_node}, - {OpType::SUM, compute_reduce_node}, - {OpType::MEAN, compute_reduce_node}, - {OpType::VARIANCE, compute_reduce_node}, - {OpType::MIN, compute_reduce_node}, - {OpType::MAX, compute_reduce_node}, - {OpType::FLATTEN, compute_reshape_node}, - {OpType::VIEW, compute_reshape_node}, - {OpType::RESHAPE, compute_reshape_node}, - {OpType::PRECISION_CAST, compute_precision_cast_node}, - {OpType::MATMUL, compute_matmul_node}, - {OpType::RMS_NORM, compute_rms_norm_node}, - {OpType::LAYERNORM, compute_layernorm_node}, - {OpType::GROUPNORM, compute_groupnorm_node}, - {OpType::BATCHNORM, compute_batchnorm_node}, - {OpType::ROPE, compute_rope_node}, - {OpType::ROPE_GPTJ, compute_rope_gptj_node}, - {OpType::SOFTMAX, compute_softmax_node}, - {OpType::ATTENTION, compute_attention_node}, - {OpType::ATTENTION_INT8_HYBRID, compute_attention_int8_hybrid_node}, - {OpType::REL_POS_BIAS, compute_rel_pos_bias_node}, - {OpType::CONV1D_CAUSAL, compute_conv1d_causal_node}, - {OpType::CONV1D_K3, compute_conv1d_k3_node}, - {OpType::CONV1D_K7S3, compute_conv1d_k7s3_node}, - {OpType::CONV1D, compute_conv1d_node}, - {OpType::CONV1D_SAME_DEPTHWISE_K9, compute_conv1d_same_depthwise_k9_node}, - {OpType::CONV1D_POINTWISE, compute_conv1d_pointwise_node}, - {OpType::CONV2D_K3S2P1, compute_conv2d_k3s2p1_node}, - {OpType::CONV2D_DEPTHWISE_K3S2P1, compute_conv2d_depthwise_k3s2p1_node}, - {OpType::CONV2D_POINTWISE_1X1, compute_conv2d_pointwise_1x1_node}, - {OpType::CONV2D_K3S1P1, compute_conv2d_k3s1p1_node}, - {OpType::GLU, compute_glu_node}, - {OpType::TRANSPOSE, compute_transpose_node}, - {OpType::GATHER, compute_gather_node}, - {OpType::SLICE, compute_slice_node}, - {OpType::EMBEDDING, compute_embedding_node}, - {OpType::CONCAT, compute_concat_node}, - {OpType::CAT, compute_cat_node}, - {OpType::INDEX, compute_index_node}, - {OpType::BILINEAR_INTERPOLATION, compute_bilinear_interpolation_node}, - {OpType::SAMPLE, compute_sample_node}, - {OpType::TOPK, compute_topk_node}, - {OpType::SCATTER_TOPK, compute_scatter_topk_node}, - {OpType::MOE_LAYER, compute_moe_layer_node}, - {OpType::DENSE_MLP_INT4_FUSED, compute_dense_mlp_int4_fused_node}, - {OpType::PERSISTENT, compute_persistent_node}, - {OpType::QUANTIZE_ACTIVATIONS, compute_quantize_activations_node}, - {OpType::LSTM_CELL, compute_lstm_cell_node}, - {OpType::GATED_DELTANET_DECODE, compute_gated_deltanet_decode_node}, - {OpType::GATED_DELTANET_PREFILL, compute_gated_deltanet_prefill_node}, - {OpType::STFT, compute_stft_node}, - {OpType::ALTUP_PREDICT, compute_altup_predict_node}, - {OpType::ALTUP_CORRECT, compute_altup_correct_node}, - {OpType::GAUSSIAN_TOPK, compute_gaussian_topk_node}, - {OpType::MAXPOOL1D, compute_maxpool1d_node}, - {OpType::BILSTM_SEQUENCE, compute_bilstm_sequence_node}, - {OpType::STATS_POOL, compute_stats_pool_node}, - {OpType::WEIGHTED_STATS_POOL, compute_weighted_stats_pool_node}, -}; - -static const char* op_type_names[] = { - "INPUT", "PRECISION_CAST", - "ADD", "ADD_CLIPPED", "SUBTRACT", "MULTIPLY", "DIVIDE", - "MATMUL", "TRANSPOSE", "RESHAPE", "SLICE", "GATHER", "EMBEDDING", "VIEW", "FLATTEN", - "BILINEAR_INTERPOLATION", - "SUM", "MEAN", "VARIANCE", "MIN", "MAX", - "RMS_NORM", "ROPE", "ROPE_GPTJ", "SOFTMAX", "ATTENTION", "ATTENTION_INT8_HYBRID", "REL_POS_BIAS", "CONV1D_CAUSAL", "CONV1D_K3", "CONV1D_K7S3", "CONV1D", "CONV1D_SAME_DEPTHWISE_K9", "CONV1D_POINTWISE", "CONV2D_K3S2P1", "CONV2D_DEPTHWISE_K3S2P1", "CONV2D_POINTWISE_1X1", "GLU", "BATCHNORM", - "SCALAR_ADD", "SCALAR_SUBTRACT", "SCALAR_MULTIPLY", "SCALAR_DIVIDE", - "SCALAR_EXP", "SCALAR_SQRT", "SCALAR_COS", "SCALAR_SIN", "SCALAR_LOG", - "ABS", "POW", - "RELU", "SILU", "GELU", "GELU_ERF", "SIGMOID", "TANH", - "SAMPLE", "CONCAT", - "SCATTER_TOPK", - "TOPK", "LAYERNORM", "GROUPNORM", - "MOE_LAYER", - "INDEX", - "PERSISTENT", - "QUANTIZE_ACTIVATIONS", - "LSTM_CELL", - "GATED_DELTANET_DECODE", - "GATED_DELTANET_PREFILL", - "STFT", - "ALTUP_PREDICT", - "ALTUP_CORRECT", - "GAUSSIAN_TOPK", - "MAXPOOL1D", - "BILSTM_SEQUENCE", - "LEAKY_RELU", - "CONV2D_K3S1P1", - "STATS_POOL", - "WEIGHTED_STATS_POOL", - "DENSE_MLP_INT4_FUSED" -}; - -static const char* get_op_name(OpType op) { - return op_type_names[static_cast(op)]; -} - -void compute_node_optimized(GraphNode& node, const nodes_vector& nodes, const node_index_map_t& node_index_map) { - if (node.op_type == OpType::INPUT) return; - - auto it = dispatch_table.find(node.op_type); - if (it != dispatch_table.end()) { - it->second(node, nodes, node_index_map); - } else { - throw std::runtime_error("Unknown operation type: " + std::to_string(static_cast(node.op_type))); - } -} - -void CactusGraph::set_input(size_t node_id, const void* data, Precision) { - auto it = node_index_map_.find(node_id); - if (it == node_index_map_.end()) { - throw std::out_of_range("Unknown input node id: " + std::to_string(node_id)); - } - - auto& node = *nodes_[it->second]; - if (node.op_type != OpType::INPUT) { - throw std::invalid_argument("Can only set data on input nodes"); - } - - if (!node.output_buffer.data && !node.output_buffer.external_data) { - node.output_buffer.allocate(); - } - - std::memcpy(node.output_buffer.get_data(), data, node.output_buffer.byte_size); -} - -void CactusGraph::set_external_input(size_t node_id, void* data, Precision) { - auto it = node_index_map_.find(node_id); - if (it == node_index_map_.end()) { - throw std::out_of_range("Unknown input node id: " + std::to_string(node_id)); - } - - auto& node = *nodes_[it->second]; - if (node.op_type != OpType::INPUT) { - throw std::invalid_argument("Can only set data on input nodes"); - } - - node.output_buffer.set_external(data); -} - -void* CactusGraph::get_output(size_t node_id) { - auto it = node_index_map_.find(node_id); - if (it == node_index_map_.end()) { - throw std::out_of_range("Unknown output node id: " + std::to_string(node_id)); - } - - auto& buffer = nodes_[it->second]->output_buffer; - if (!buffer.get_data()) { - buffer.allocate(); - } - return buffer.get_data(); -} - -void CactusGraph::execute(const std::string& profile_file) { - std::vector last_use(nodes_.size(), 0); - for (size_t i = 0; i < nodes_.size(); ++i) { - for (size_t input_id : nodes_[i]->input_ids) { - auto it = node_index_map_.find(input_id); - if (it != node_index_map_.end()) { - last_use[it->second] = std::max(last_use[it->second], i); - } - } - } - - BufferPool& pool = buffer_pool_; - - auto get_env_int = [](const char* name, int fallback) -> int { - const char* val = std::getenv(name); - return val ? std::atoi(val) : fallback; - }; - - auto get_env_str = [](const char* name) -> std::string { - const char* val = std::getenv(name); - return val ? std::string(val) : std::string(); - }; - - bool capture_to_stdout = get_env_int("CACTUS_CAPTURE_STDOUT", 0) != 0; - std::string capture_file_path = get_env_str("CACTUS_CAPTURE_FILE"); - bool capture_requested = get_env_int("CACTUS_CAPTURE_ENABLE", 0) != 0; - std::string capture_dir = get_env_str("CACTUS_CAPTURE_DIR"); - - if (!capture_requested) { - capture_requested = capture_to_stdout || !capture_file_path.empty() || !capture_dir.empty(); - } else if (capture_file_path.empty() && !capture_to_stdout && capture_dir.empty()) { - capture_to_stdout = true; - } - - size_t capture_preview_count = static_cast(get_env_int("CACTUS_CAPTURE_PREVIEW_COUNT", 8)); - size_t capture_max_elements = static_cast(get_env_int("CACTUS_CAPTURE_MAX_ELEMENTS", 65536)); - - std::string env_profile = get_env_str("CACTUS_PROFILE_FILE"); - if (env_profile.empty()) env_profile = get_env_str("CACTUS_PROFILE"); - - std::string target_profile = profile_file; - if (target_profile.empty() && !env_profile.empty()) { - target_profile = env_profile; - } - - bool enable_profiling = !target_profile.empty(); - bool to_stdout = (target_profile == "stdout" || target_profile == "-"); - - std::ofstream profile_out; - std::ostream* out = &std::cout; - - if (enable_profiling && !to_stdout) { - profile_out.open(target_profile, std::ios::app); - if (profile_out.is_open()) { - out = &profile_out; - } - } - - auto total_start = std::chrono::high_resolution_clock::now(); - - if (enable_profiling) { - *out << "=== Graph Execution Profile ===" << std::endl; - *out << std::left << std::setw(24) << "Operation" - << std::setw(12) << "Time (ms)" - << std::setw(20) << "Output Shape" - << "Backend" << std::endl; - *out << std::string(72, '-') << std::endl; - } - - for (size_t node_idx = 0; node_idx < nodes_.size(); ++node_idx) { - auto& node = nodes_[node_idx]; - - if (node->op_type != OpType::INPUT) { - node->output_buffer.allocate_from_pool(pool); - } - - if (enable_profiling && node->op_type != OpType::INPUT) { - auto start = std::chrono::high_resolution_clock::now(); - - compute_node_optimized(*node, nodes_, node_index_map_); - - if (node->op_type == OpType::PERSISTENT) { - populated_node_ids_.insert(node->id); - } - - auto end = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration_cast(end - start); - double ms = duration.count() / 1000.0; - - std::string shape_str = "["; - for (size_t i = 0; i < node->output_buffer.shape.size(); ++i) { - if (i > 0) shape_str += ","; - shape_str += std::to_string(node->output_buffer.shape[i]); - } - shape_str += "]"; - - std::string values_str = ""; - if (node->output_buffer.get_data()) { - size_t num_values = std::min(size_t(5), node->output_buffer.total_size); - values_str = " values=["; - - if (node->output_buffer.precision == Precision::FP32) { - if (node->op_type == OpType::SAMPLE) { - uint32_t* uint32_data = reinterpret_cast(node->output_buffer.get_data()); - for (size_t i = 0; i < num_values; ++i) { - if (i > 0) values_str += ","; - values_str += std::to_string(uint32_data[i]); - } - } else { - float* float_data = reinterpret_cast(node->output_buffer.get_data()); - for (size_t i = 0; i < num_values; ++i) { - if (i > 0) values_str += ","; - values_str += std::to_string(float_data[i]).substr(0, 6); - } - } - } else if (node->output_buffer.precision == Precision::FP16) { - __fp16* fp16_data = reinterpret_cast<__fp16*>(node->output_buffer.get_data()); - for (size_t i = 0; i < num_values; ++i) { - if (i > 0) values_str += ","; - values_str += std::to_string(static_cast(fp16_data[i])).substr(0, 6); - } - } else if (node->output_buffer.precision == Precision::INT8) { - int8_t* int8_data = reinterpret_cast(node->output_buffer.get_data()); - for (size_t i = 0; i < num_values; ++i) { - if (i > 0) values_str += ","; - values_str += std::to_string(static_cast(int8_data[i])); - } - } - - if (node->output_buffer.total_size > 5) { - values_str += ",..."; - } - values_str += "]"; - } - - std::string weights_str = ""; - if ((node->op_type == OpType::RMS_NORM || node->op_type == OpType::MATMUL || - node->op_type == OpType::GATHER || node->op_type == OpType::EMBEDDING || - node->op_type == OpType::ATTENTION || node->op_type == OpType::CONCAT) && - node->input_ids.size() >= 2) { - const auto& weight_node = nodes_[node_index_map_.at(node->input_ids[1])]; - if (weight_node->output_buffer.get_data()) { - size_t num_values = std::min(size_t(5), weight_node->output_buffer.total_size); - weights_str = " weights=["; - - if (weight_node->output_buffer.precision == Precision::FP32) { - const float* float_data = weight_node->output_buffer.data_as(); - for (size_t i = 0; i < num_values; ++i) { - if (i > 0) weights_str += ","; - weights_str += std::to_string(float_data[i]).substr(0, 6); - } - } else if (weight_node->output_buffer.precision == Precision::FP16) { - const __fp16* fp16_data = weight_node->output_buffer.data_as<__fp16>(); - for (size_t i = 0; i < num_values; ++i) { - if (i > 0) weights_str += ","; - weights_str += std::to_string(static_cast(fp16_data[i])).substr(0, 6); - } - } else if (weight_node->output_buffer.precision == Precision::INT8) { - const int8_t* int8_data = weight_node->output_buffer.data_as(); - for (size_t i = 0; i < num_values; ++i) { - if (i > 0) weights_str += ","; - weights_str += std::to_string(static_cast(int8_data[i])); - } - } else if (weight_node->output_buffer.precision == Precision::INT4) { - const uint8_t* packed = weight_node->output_buffer.data_as(); - int8x16_t high, low; - unpack_int4_as_int8x16x2(packed, high, low); - int8_t low_lanes[16], high_lanes[16]; - vst1q_s8(low_lanes, low); - vst1q_s8(high_lanes, high); - - for (size_t i = 0; i < num_values; ++i) { - if (i > 0) weights_str += ","; - int8_t val = (i < 16) ? low_lanes[i] : high_lanes[i - 16]; - weights_str += std::to_string(static_cast(val)); - } - } - - if (weight_node->output_buffer.total_size > 5) { - weights_str += ",..."; - } - weights_str += "]"; - } - } - - *out << std::left << std::setw(24) << get_op_name(node->op_type) - << std::setw(12) << std::fixed << std::setprecision(3) << ms - << std::setw(20) << shape_str - << values_str << weights_str << std::endl; - } else { - compute_node_optimized(*node, nodes_, node_index_map_); - - if (node->op_type == OpType::PERSISTENT) { - populated_node_ids_.insert(node->id); - } - } - } - - std::unique_ptr capture_file_stream; - std::vector capture_outputs; - - if (capture_requested) { - if (capture_to_stdout) { - capture_outputs.push_back(&std::cout); - } - - if (!capture_file_path.empty()) { - std::filesystem::path capture_path(capture_file_path); - if (capture_path.has_parent_path()) { - std::error_code ec; - std::filesystem::create_directories(capture_path.parent_path(), ec); - } - - auto stream_ptr = std::make_unique(capture_path, std::ios::out | std::ios::app); - if (stream_ptr->is_open()) { - capture_outputs.push_back(stream_ptr.get()); - capture_file_stream = std::move(stream_ptr); - } else { - std::cerr << "Failed to open capture file: " << capture_path << std::endl; - } - } - - if (!capture_dir.empty()) { - std::filesystem::path dir_path(capture_dir); - std::error_code ec; - std::filesystem::create_directories(dir_path, ec); - } - - if (capture_outputs.empty() && capture_dir.empty()) { - capture_requested = false; - } - } - - if (capture_requested) { - auto precision_to_string = [](Precision p) -> const char* { - switch (p) { - case Precision::FP32: return "FP32"; - case Precision::FP16: return "FP16"; - case Precision::INT8: return "INT8"; - default: return "UNKNOWN"; - } - }; - - auto format_double = [](double value) { - std::ostringstream oss; - oss << std::fixed << std::setprecision(6) << value; - return oss.str(); - }; - - auto now = std::chrono::system_clock::now(); - std::time_t now_time = std::chrono::system_clock::to_time_t(now); - std::tm time_info{}; -#if defined(_WIN32) - localtime_s(&time_info, &now_time); -#else - localtime_r(&now_time, &time_info); -#endif - - auto write_header = [&](std::ostream& stream) { - stream << "=== Graph Debug Capture ===" << std::endl; - stream << "Timestamp: " << std::put_time(&time_info, "%Y-%m-%d %H:%M:%S") << std::endl; - stream << "Captured nodes: " << debug_nodes_.size() << std::endl; - stream << std::string(60, '-') << std::endl; - }; - - auto write_separator = [](std::ostream& stream) { - stream << std::string(60, '-') << std::endl; - }; - - if (debug_nodes_.empty()) { - for (auto* stream : capture_outputs) { - write_header(*stream); - *stream << "No debug nodes registered on this graph." << std::endl; - write_separator(*stream); - stream->flush(); - } - } else { - for (auto* stream : capture_outputs) { - write_header(*stream); - } - - for (const auto& entry : debug_nodes_) { - auto node_it = node_index_map_.find(entry.node_id); - const GraphNode* node_ptr = nullptr; - if (node_it != node_index_map_.end()) { - node_ptr = nodes_[node_it->second].get(); - } - - if (!node_ptr) { - for (auto* stream : capture_outputs) { - *stream << "Layer " << entry.layer_idx << " - " << entry.name - << " (node " << entry.node_id << ")" << std::endl; - *stream << " Data: " << std::endl; - write_separator(*stream); - } - continue; - } - - const BufferDesc& buffer = node_ptr->output_buffer; - const void* data_ptr = buffer.get_data(); - size_t total_size = buffer.total_size; - - std::ostringstream shape_ss; - shape_ss << "["; - for (size_t i = 0; i < buffer.shape.size(); ++i) { - if (i > 0) { - shape_ss << ","; - } - shape_ss << buffer.shape[i]; - } - shape_ss << "]"; - std::string shape_str = shape_ss.str(); - - bool has_data = data_ptr != nullptr && total_size > 0; - size_t elements_to_process = total_size; - bool truncated = false; - if (has_data && elements_to_process > capture_max_elements && capture_max_elements > 0) { - elements_to_process = capture_max_elements; - truncated = true; - } - - std::vector preview_values; - if (capture_preview_count > 0) { - preview_values.reserve(std::min(capture_preview_count, elements_to_process)); - } - - double min_val = std::numeric_limits::infinity(); - double max_val = -std::numeric_limits::infinity(); - long double sum = 0.0L; - long double sum_sq = 0.0L; - - if (has_data && elements_to_process > 0) { - auto accumulate = [&](float value, size_t index) { - double v = static_cast(value); - min_val = std::min(min_val, v); - max_val = std::max(max_val, v); - sum += static_cast(value); - sum_sq += static_cast(value) * static_cast(value); - if (capture_preview_count > 0 && index < capture_preview_count) { - preview_values.push_back(value); - } - }; - - if (buffer.precision == Precision::FP32) { - const float* typed = static_cast(data_ptr); - for (size_t i = 0; i < elements_to_process; ++i) { - accumulate(typed[i], i); - } - } else if (buffer.precision == Precision::FP16) { - const __fp16* typed = reinterpret_cast(data_ptr); - for (size_t i = 0; i < elements_to_process; ++i) { - accumulate(static_cast(typed[i]), i); - } - } else if (buffer.precision == Precision::INT8) { - const int8_t* typed = reinterpret_cast(data_ptr); - for (size_t i = 0; i < elements_to_process; ++i) { - accumulate(static_cast(typed[i]), i); - } - } else if (buffer.precision == Precision::INT4) { - assert(elements_to_process % 32 == 0 && "INT4 precision capture requires element count to be multiple of 32"); - const uint8_t* packed_ptr = reinterpret_cast(data_ptr); - for (size_t i = 0; i < elements_to_process; i+=32) { - int8x16_t high, low; - unpack_int4_as_int8x16x2(packed_ptr + i / 2, high, low); - int8_t high_lanes[16], low_lanes[16]; - vst1q_s8(high_lanes, high); - vst1q_s8(low_lanes, low); - - for (size_t j = 0; j < 16; ++j) { - accumulate(static_cast(low_lanes[j]), i + j); - } - for (size_t j = 0; j < 16; ++j) { - accumulate(static_cast(high_lanes[j]), i + 16 + j); - } - } - } else { - has_data = false; - } - } else { - has_data = false; - } - - if (!capture_dir.empty() && has_data) { - std::string safe_name = entry.name; - std::string filename = capture_dir + "/" + safe_name + ".bin"; - std::ofstream bin_file(filename, std::ios::binary); - if (bin_file.is_open()) { - size_t bytes_to_write = buffer.byte_size; - if (truncated) { - bytes_to_write = PrecisionTraits::packed_size_of(buffer.precision, elements_to_process); - } - bin_file.write(reinterpret_cast(data_ptr), bytes_to_write); - } - } - - size_t processed_count = has_data ? elements_to_process : 0; - long double mean_ld = processed_count > 0 ? sum / processed_count : 0.0L; - long double variance_ld = processed_count > 0 ? (sum_sq / processed_count) - (mean_ld * mean_ld) : 0.0L; - if (variance_ld < 0.0L) { - variance_ld = 0.0L; - } - double mean_val = static_cast(mean_ld); - double stddev_val = processed_count > 0 ? std::sqrt(static_cast(variance_ld)) : 0.0; - - std::ostringstream preview_ss; - if (capture_preview_count > 0 && !preview_values.empty()) { - preview_ss << "["; - for (size_t i = 0; i < preview_values.size(); ++i) { - if (i > 0) { - preview_ss << ", "; - } - preview_ss << format_double(static_cast(preview_values[i])); - } - if (processed_count > preview_values.size()) { - if (!preview_values.empty()) { - preview_ss << ", ..."; - } else { - preview_ss << "..."; - } - } - preview_ss << "]"; - } - - for (auto* stream : capture_outputs) { - *stream << "Layer " << entry.layer_idx << " - " << entry.name - << " (node " << entry.node_id << ")" << std::endl; - *stream << " Shape: " << shape_str << " Precision: " << precision_to_string(buffer.precision) << std::endl; - if (!has_data) { - *stream << " Data: " << std::endl; - } else { - *stream << " Stats: min=" << format_double(min_val) - << " max=" << format_double(max_val) - << " mean=" << format_double(mean_val) - << " std=" << format_double(stddev_val) << std::endl; - if (truncated || processed_count < total_size) { - *stream << " Note: stats computed on first " << processed_count - << " of " << total_size << " values" << std::endl; - } - if (capture_preview_count > 0 && !preview_values.empty()) { - *stream << " Preview: " << preview_ss.str() << std::endl; - } - } - write_separator(*stream); - } - } - - for (auto* stream : capture_outputs) { - stream->flush(); - } - } - } - - if (enable_profiling) { - auto total_end = std::chrono::high_resolution_clock::now(); - auto total_duration = std::chrono::duration_cast(total_end - total_start); - double total_ms = total_duration.count() / 1000.0; - - *out << std::string(72, '-') << std::endl; - *out << "Total execution time: " << std::fixed << std::setprecision(3) << total_ms << " ms" << std::endl; - *out << "================================" << std::endl; - - if (profile_out.is_open()) { - profile_out.close(); - } - } -} - -void CactusGraph::hard_reset() { - nodes_.clear(); - node_index_map_.clear(); - mapped_files_.clear(); - weight_cache_.clear(); - next_node_id_ = 1; - debug_nodes_.clear(); - buffer_pool_.clear(); -} - -void CactusGraph::soft_reset() { - std::set cached_node_ids; - for (const auto& cache_entry : weight_cache_) { - cached_node_ids.insert(cache_entry.second); - } - - for (size_t pid : persistent_node_ids_) { - cached_node_ids.insert(pid); - } - - size_t max_preserved_id = 0; - for (const auto& node : nodes_) { - if ((node->op_type == OpType::INPUT && node->output_buffer.external_data) || - cached_node_ids.count(node->id)) { - max_preserved_id = std::max(max_preserved_id, node->id); - } - } - - auto preserved_nodes = std::move(nodes_); - auto preserved_index_map = std::move(node_index_map_); - - nodes_.clear(); - node_index_map_.clear(); - - for (auto& node : preserved_nodes) { - if ((node->op_type == OpType::INPUT && node->output_buffer.external_data) || - cached_node_ids.count(node->id)) { - size_t index = nodes_.size(); - node_index_map_[node->id] = index; - nodes_.push_back(std::move(node)); - } - } - - next_node_id_ = max_preserved_id + 1; - debug_nodes_.clear(); - if (!prefill_mode_) { - buffer_pool_.clear(); - shrink_thread_local_buffers(); - } -} - -void CactusGraph::soft_reset_keep_pool() { - std::set cached_node_ids; - for (const auto& cache_entry : weight_cache_) { - cached_node_ids.insert(cache_entry.second); - } - - for (size_t pid : persistent_node_ids_) { - cached_node_ids.insert(pid); - } - - size_t max_preserved_id = 0; - for (const auto& node : nodes_) { - if ((node->op_type == OpType::INPUT && node->output_buffer.external_data) || - cached_node_ids.count(node->id)) { - max_preserved_id = std::max(max_preserved_id, node->id); - } - } - - auto preserved_nodes = std::move(nodes_); - - nodes_.clear(); - node_index_map_.clear(); - - for (auto& node : preserved_nodes) { - if ((node->op_type == OpType::INPUT && node->output_buffer.external_data) || - cached_node_ids.count(node->id)) { - size_t index = nodes_.size(); - node_index_map_[node->id] = index; - nodes_.push_back(std::move(node)); - } - } - - next_node_id_ = max_preserved_id + 1; - debug_nodes_.clear(); -} diff --git a/cactus/graph/graph_io.cpp b/cactus/graph/graph_io.cpp deleted file mode 100644 index 8eded5e94..000000000 --- a/cactus/graph/graph_io.cpp +++ /dev/null @@ -1,808 +0,0 @@ -#include "graph.h" -#include "graph_param_io.h" -#include -#include -#include -#include -#include -#include -#include - -namespace { - constexpr uint32_t fourcc(char a, char b, char c, char d) { - return static_cast(static_cast(a)) | - (static_cast(static_cast(b)) << 8) | - (static_cast(static_cast(c)) << 16) | - (static_cast(static_cast(d)) << 24); - } - - constexpr uint32_t CACTUS_MAGIC = 0x54434143; - constexpr uint32_t CACTUS_GRAPH_MAGIC = fourcc('C', 'G', 'R', 'F'); - constexpr uint32_t FLAG_HAS_SCALES = 1 << 0; - constexpr uint32_t FLAG_INTERLEAVED = 1 << 3; - constexpr size_t HEADER_SIZE = 84; - - inline size_t align_offset(size_t offset, size_t alignment) { - size_t remainder = offset % alignment; - if (remainder == 0) return offset; - return offset + (alignment - remainder); - } - - inline void write_u32(std::ostream& out, uint32_t v) { - out.write(reinterpret_cast(&v), sizeof(v)); - } - - inline void write_u64(std::ostream& out, uint64_t v) { - out.write(reinterpret_cast(&v), sizeof(v)); - } - - void write_size_vector(std::ostream& out, const std::vector& values) { - uint32_t size = static_cast(values.size()); - write_u32(out, size); - for (size_t v : values) { - write_u64(out, static_cast(v)); - } - } - - void write_u32_vector(std::ostream& out, const std::vector& values) { - uint32_t size = static_cast(values.size()); - write_u32(out, size); - for (uint32_t v : values) { - write_u32(out, v); - } - } - - std::vector compute_leaf_outputs(const GraphFile::SerializedGraph& sg) { - std::vector referenced(sg.nodes.size(), false); - - for (const auto& node : sg.nodes) { - for (uint32_t input_idx : node.inputs) { - if (input_idx >= referenced.size()) { - throw std::runtime_error("Graph save failed: input index out of range"); - } - referenced[input_idx] = true; - } - } - - std::vector outputs; - outputs.reserve(sg.nodes.size()); - - for (uint32_t i = 0; i < sg.nodes.size(); ++i) { - if (!referenced[i]) { - outputs.push_back(i); - } - } - - return outputs; - } - - void write_serialized_graph(std::ostream& out, const GraphFile::SerializedGraph& sg) { - write_u32(out, sg.header.magic); - write_u32(out, sg.header.version); - write_u32(out, sg.header.node_count); - write_u32(out, sg.header.flags); - - write_u32_vector(out, sg.graph_inputs); - write_u32_vector(out, sg.graph_outputs); - - for (const auto& node : sg.nodes) { - write_u32(out, node.index); - write_u32(out, static_cast(node.op_type)); - write_u32_vector(out, node.inputs); - write_size_vector(out, node.output_shape); - write_u32(out, static_cast(node.precision)); - GraphParamIO::write_op_params(out, node.op_type, node.params); - } - - if (!out) { - throw std::runtime_error("Error writing serialized graph"); - } - } - - // read helpers - uint32_t read_u32(std::istream& in) { - uint32_t v = 0; - in.read(reinterpret_cast(&v), sizeof(v)); - if (!in) { - throw std::runtime_error("Unexpected EOF while reading uint32"); - } - return v; - } - - uint64_t read_u64(std::istream& in) { - uint64_t v = 0; - in.read(reinterpret_cast(&v), sizeof(v)); - if (!in) { - throw std::runtime_error("Unexpected EOF while reading uint64"); - } - return v; - } - - std::vector read_u32_vector(std::istream& in) { - uint32_t count = read_u32(in); - std::vector values; - values.reserve(count); - for (uint32_t i = 0; i < count; ++i) { - values.push_back(read_u32(in)); - } - return values; - } - - std::vector read_size_vector(std::istream& in) { - uint32_t count = read_u32(in); - std::vector values; - values.reserve(count); - for (uint32_t i = 0; i < count; ++i) { - values.push_back(static_cast(read_u64(in))); - } - return values; - } - - bool is_binary_broadcast_op(OpType op_type) { - switch (op_type) { - case OpType::ADD: - case OpType::ADD_CLIPPED: - case OpType::SUBTRACT: - case OpType::MULTIPLY: - case OpType::DIVIDE: - return true; - default: - return false; - } - } - - void populate_derived_params(CactusGraph& graph, - const GraphFile::NodeEntry& node_entry, - const std::vector& runtime_inputs, - OpParams& params) { - if (is_binary_broadcast_op(node_entry.op_type)) { - if (runtime_inputs.size() != 2) { - throw std::runtime_error("Graph file corrupted: binary op missing inputs"); - } - const auto& lhs = graph.get_output_buffer(runtime_inputs[0]); - const auto& rhs = graph.get_output_buffer(runtime_inputs[1]); - params.broadcast_info = BroadcastInfo::compute(lhs.shape, rhs.shape); - } - } - - GraphFile::GraphHeader read_graph_header(std::istream& in) { - GraphFile::GraphHeader header; - header.magic = read_u32(in); - header.version = read_u32(in); - header.node_count = read_u32(in); - header.flags = read_u32(in); - - if (header.magic != CACTUS_GRAPH_MAGIC) { - throw std::runtime_error("Invalid graph file: bad magic"); - } - if (header.version != 4) { - throw std::runtime_error("Unsupported graph file version: " + - std::to_string(header.version)); - } - - return header; - } - - GraphFile::NodeEntry read_node_entry(std::istream& in) { - GraphFile::NodeEntry node; - node.index = read_u32(in); - uint32_t op_type_val = read_u32(in); - if (op_type_val > static_cast(OpType::STATS_POOL)) { - throw std::runtime_error("Graph file corrupted: invalid op type"); - } - node.op_type = static_cast(op_type_val); - node.inputs = read_u32_vector(in); - node.output_shape = read_size_vector(in); - uint32_t precision_val = read_u32(in); - if (precision_val > static_cast(Precision::INT4)) { - throw std::runtime_error("Graph file corrupted: invalid precision"); - } - node.precision = static_cast(precision_val); - GraphParamIO::read_op_params(in, node.op_type, node.params); - return node; - } - - -} // namespace - -void CactusGraph::save(const std::string& path) { - GraphFile::save_graph(*this, path); -} - -CactusGraph CactusGraph::from_serialized(const GraphFile::SerializedGraph& sg) { - CactusGraph graph; - std::vector runtime_ids; - runtime_ids.reserve(sg.nodes.size()); - - if (sg.nodes.size() != sg.header.node_count) { - throw std::runtime_error("Graph file corrupted: node count mismatch"); - } - - for (size_t i = 0; i < sg.nodes.size(); ++i) { - const auto& node_entry = sg.nodes[i]; - - if (node_entry.index != i) { - throw std::runtime_error("Graph file corrupted: node indices must be dense and ordered"); - } - std::vector runtime_inputs; - runtime_inputs.reserve(node_entry.inputs.size()); - - for (uint32_t serialized_input_idx : node_entry.inputs) { - if (serialized_input_idx >= runtime_ids.size()) { - throw std::runtime_error( - "Graph file corrupted: input refers to a node that has not been reconstructed yet" - ); - } - runtime_inputs.push_back(runtime_ids[serialized_input_idx]); - } - - size_t new_node_id = 0; - - if (node_entry.op_type == OpType::INPUT) { - new_node_id = graph.input(node_entry.output_shape, node_entry.precision); - } - else { - OpParams params = node_entry.params; - params.output_precision = node_entry.precision; - populate_derived_params(graph, node_entry, runtime_inputs, params); - new_node_id = graph.add_node(node_entry.op_type, runtime_inputs, node_entry.output_shape, params); - - if (node_entry.op_type == OpType::PERSISTENT) { - graph.persistent_node_ids_.insert(new_node_id); - } - } - runtime_ids.push_back(new_node_id); - } - return graph; -} - -CactusGraph CactusGraph::load(const std::string& path) { - GraphFile::SerializedGraph sg = GraphFile::load_graph(path); - return from_serialized(sg); -} - -size_t CactusGraph::mmap_embeddings(const std::string& filename) { - auto mapped_file = std::make_unique(filename); - - const auto& shape = mapped_file->shape(); - if (shape.size() != 2) { - throw std::runtime_error("Memory-mapped embeddings must be 2D [vocab_size, embedding_dim]"); - } - - Precision precision = mapped_file->precision(); - - size_t node_id = input(shape, precision); - set_external_input(node_id, const_cast(mapped_file->data()), precision); - - if (PrecisionTraits::is_integer(precision) && mapped_file->group_size() > 0) { - set_grouped_scales(node_id, mapped_file->group_size(), mapped_file->num_groups(), - const_cast(mapped_file->scales_data())); - - if (mapped_file->is_interleaved()) { - auto& buffer = nodes_[node_index_map_.at(node_id)]->output_buffer; - buffer.set_interleaved(true, mapped_file->original_N()); - } - } - - size_t file_idx = mapped_files_.size(); - mapped_files_.push_back(std::move(mapped_file)); - node_to_mapped_file_[node_id] = file_idx; - weight_cache_[filename] = node_id; - return node_id; -} - -size_t CactusGraph::mmap_weights(const std::string& filename) { - auto it = weight_cache_.find(filename); - if (it != weight_cache_.end()) { - return it->second; - } - - auto mapped_file = std::make_unique(filename); - - const auto& shape = mapped_file->shape(); - Precision precision = mapped_file->precision(); - - size_t node_id = input(shape, precision); - set_external_input(node_id, const_cast(mapped_file->data()), precision); - - if (PrecisionTraits::is_integer(precision) && mapped_file->group_size() > 0) { - set_grouped_scales(node_id, mapped_file->group_size(), mapped_file->num_groups(), - const_cast(mapped_file->scales_data())); - - if (mapped_file->is_interleaved()) { - auto& buffer = nodes_[node_index_map_.at(node_id)]->output_buffer; - buffer.set_interleaved(true, mapped_file->original_N()); - } - } - - size_t file_idx = mapped_files_.size(); - mapped_files_.push_back(std::move(mapped_file)); - node_to_mapped_file_[node_id] = file_idx; - weight_cache_[filename] = node_id; - return node_id; -} - -void CactusGraph::release_weight_pages(size_t node_id) { - auto it = node_to_mapped_file_.find(node_id); - if (it != node_to_mapped_file_.end() && it->second < mapped_files_.size()) { - mapped_files_[it->second]->release_pages(); - } -} - -void CactusGraph::prefetch_weight_pages(size_t node_id) { - auto it = node_to_mapped_file_.find(node_id); - if (it != node_to_mapped_file_.end() && it->second < mapped_files_.size()) { - mapped_files_[it->second]->prefetch_pages(); - } -} - -void CactusGraph::release_all_weight_pages() { - for (auto& mf : mapped_files_) { - if (mf) mf->release_pages(); - } -} - -void CactusGraph::set_grouped_scales(size_t node_id, size_t group_size, size_t num_groups, void* scales_ptr) { - auto it = node_index_map_.find(node_id); - if (it != node_index_map_.end()) { - nodes_[it->second]->output_buffer.set_grouped_scales(group_size, num_groups, scales_ptr); - } -} - -void CactusGraph::set_interleaved(size_t node_id, bool interleaved, size_t original_N) { - auto it = node_index_map_.find(node_id); - if (it != node_index_map_.end()) { - nodes_[it->second]->output_buffer.set_interleaved(interleaved, original_N); - } -} - -size_t CactusGraph::embedding(const std::string& filename, size_t indices) { - auto mapped_file = std::make_unique(filename); - - const auto& shape = mapped_file->shape(); - if (shape.size() != 2) { - throw std::runtime_error("Embedding file must contain 2D tensor [vocab_size, hidden_dim]"); - } - - Precision precision = mapped_file->precision(); - size_t embeddings_node = input(shape, precision); - set_external_input(embeddings_node, const_cast(mapped_file->data()), precision); - - if (PrecisionTraits::is_integer(precision) && mapped_file->group_size() > 0) { - set_grouped_scales(embeddings_node, mapped_file->group_size(), mapped_file->num_groups(), - const_cast(mapped_file->scales_data())); - - if (mapped_file->is_interleaved()) { - auto& buffer = nodes_[node_index_map_.at(embeddings_node)]->output_buffer; - buffer.set_interleaved(true, mapped_file->original_N()); - } - } - - mapped_files_.push_back(std::move(mapped_file)); - - const auto& idx_shape = get_output_buffer(indices).shape; - std::vector output_shape = idx_shape; - output_shape.push_back(shape[1]); - - OpParams params; - params.output_precision = Precision::FP16; - return add_node(OpType::EMBEDDING, {embeddings_node, indices}, output_shape, params); -} - - -namespace GraphFile { - -void save_graph(const CactusGraph& graph, - const std::string& filename) { - - std::ofstream out(filename, std::ios::binary); - if (!out) { - throw std::runtime_error("Cannot open file for writing: " + filename); - } - - std::unordered_map id_to_index; - id_to_index.reserve(graph.nodes_.size()); - - for (uint32_t i = 0; i < graph.nodes_.size(); ++i) { - id_to_index[graph.nodes_[i]->id] = i; - } - - SerializedGraph sg; - sg.header.magic = CACTUS_GRAPH_MAGIC; - sg.header.version = 4; - sg.header.node_count = static_cast(graph.nodes_.size()); - sg.header.flags = 0; - - sg.nodes.reserve(graph.nodes_.size()); - - for (uint32_t i = 0; i < graph.nodes_.size(); ++i) { - const auto& node = graph.nodes_[i]; - - NodeEntry entry; - entry.index = i; - entry.op_type = node->op_type; - entry.output_shape = node->output_buffer.shape; - entry.precision = node->output_buffer.precision; - entry.params = node->params; - - entry.inputs.reserve(node->input_ids.size()); - for (size_t input_id : node->input_ids) { - auto it = id_to_index.find(input_id); - if (it == id_to_index.end()) { - throw std::runtime_error("Graph save failed: missing input id mapping"); - } - entry.inputs.push_back(it->second); - } - - if (node->op_type == OpType::INPUT) { - sg.graph_inputs.push_back(entry.index); - } - - sg.nodes.push_back(std::move(entry)); - } - - sg.graph_outputs = compute_leaf_outputs(sg); - - write_serialized_graph(out, sg); - - if (!out) { - throw std::runtime_error("Error writing graph data to file: " + filename); - } -} - -SerializedGraph load_graph(const std::string& filename) { - std::ifstream in(filename, std::ios::binary); - if (!in) { - throw std::runtime_error("Cannot open file for reading: " + filename); - } - SerializedGraph sg; - sg.header = read_graph_header(in); - sg.graph_inputs = read_u32_vector(in); - sg.graph_outputs = read_u32_vector(in); - sg.nodes.reserve(sg.header.node_count); - for (uint32_t i = 0; i < sg.header.node_count; ++i) { - sg.nodes.push_back(read_node_entry(in)); - } - - if (sg.nodes.size() != sg.header.node_count) { - throw std::runtime_error("Graph file corrupted: node count mismatch"); - } - - for (uint32_t i = 0; i < sg.nodes.size(); ++i) { - const auto& node = sg.nodes[i]; - - if (node.index != i) { - throw std::runtime_error("Graph file corrupted: node indices must be dense and ordered"); - } - - for (uint32_t input_idx : node.inputs) { - if (input_idx >= sg.nodes.size()) { - throw std::runtime_error("Graph file corrupted: input index out of range"); - } - } - } - - for (uint32_t input_idx : sg.graph_inputs) { - if (input_idx >= sg.nodes.size()) { - throw std::runtime_error("Graph file corrupted: graph input index out of range"); - } - } - - for (uint32_t output_idx : sg.graph_outputs) { - if (output_idx >= sg.nodes.size()) { - throw std::runtime_error("Graph file corrupted: graph output index out of range"); - } - } - - return sg; -} - -void save_node(CactusGraph& graph, size_t node_id, const std::string& filename) { - graph.execute(); - void* data = graph.get_output(node_id); - - const auto& buffer = graph.get_output_buffer(node_id); - const auto& shape = buffer.shape; - Precision precision = buffer.precision; - - std::ofstream file(filename, std::ios::binary); - if (!file) { - throw std::runtime_error("Cannot open file for writing: " + filename); - } - - size_t total_elements = 1; - for (size_t dim : shape) { - total_elements *= dim; - } - - size_t byte_size = PrecisionTraits::packed_size_of(precision, total_elements); - - bool has_scales = PrecisionTraits::is_integer(precision) && buffer.group_size > 0 && buffer.scales_data; - size_t N = shape.size() >= 1 ? shape[0] : 1; - size_t scales_bytes = has_scales ? (N * buffer.num_groups * sizeof(__fp16)) : 0; - - uint32_t ndim = static_cast(shape.size()); - uint32_t flags = has_scales ? FLAG_HAS_SCALES : 0; - if (buffer.is_interleaved) { - flags |= FLAG_INTERLEAVED; - } - uint32_t alignment = 32; - - uint32_t magic = CACTUS_MAGIC; - file.write(reinterpret_cast(&magic), sizeof(magic)); - file.write(reinterpret_cast(&flags), sizeof(flags)); - file.write(reinterpret_cast(&alignment), sizeof(alignment)); - file.write(reinterpret_cast(&ndim), sizeof(ndim)); - - for (uint32_t i = 0; i < 4; i++) { - uint64_t dim_val = (i < shape.size()) ? static_cast(shape[i]) : 0; - file.write(reinterpret_cast(&dim_val), sizeof(dim_val)); - } - - uint32_t prec_val = static_cast(precision); - file.write(reinterpret_cast(&prec_val), sizeof(prec_val)); - - uint64_t data_bytes = static_cast(byte_size); - uint64_t scales_bytes_val = static_cast(scales_bytes); - file.write(reinterpret_cast(&data_bytes), sizeof(data_bytes)); - file.write(reinterpret_cast(&scales_bytes_val), sizeof(scales_bytes_val)); - - uint32_t group_size = has_scales ? static_cast(buffer.group_size) : 0; - uint32_t num_groups = has_scales ? static_cast(buffer.num_groups) : 0; - file.write(reinterpret_cast(&group_size), sizeof(group_size)); - file.write(reinterpret_cast(&num_groups), sizeof(num_groups)); - - uint64_t original_N = buffer.is_interleaved ? buffer.original_N : N; - file.write(reinterpret_cast(&original_N), sizeof(original_N)); - - size_t header_end = HEADER_SIZE; - size_t aligned_header = align_offset(header_end, alignment); - size_t header_padding = aligned_header - header_end; - for (size_t i = 0; i < header_padding; i++) { - char zero = 0; - file.write(&zero, 1); - } - - if (has_scales) { - file.write(static_cast(buffer.scales_data), scales_bytes); - - size_t scales_end = aligned_header + scales_bytes; - size_t data_start = align_offset(scales_end, alignment); - size_t scales_padding = data_start - scales_end; - for (size_t i = 0; i < scales_padding; i++) { - char zero = 0; - file.write(&zero, 1); - } - } - - file.write(static_cast(data), byte_size); - - if (!file) { - throw std::runtime_error("Error writing node data to file: " + filename); - } -} - -// MappedFile implementation - -MappedFile::MappedFile(const std::string& filename) - : fd_(-1), mapped_data_(nullptr), file_size_(0), data_offset_(0) { - fd_ = open(filename.c_str(), O_RDONLY); - if (fd_ == -1) { - throw std::runtime_error("Cannot open file for mapping: " + filename); - } - - struct stat st; - if (fstat(fd_, &st) == -1) { - close(fd_); - throw std::runtime_error("Cannot get file size: " + filename); - } - file_size_ = static_cast(st.st_size); - - mapped_data_ = mmap(nullptr, file_size_, PROT_READ, MAP_SHARED, fd_, 0); - if (mapped_data_ == MAP_FAILED) { - close(fd_); - throw std::runtime_error("Cannot map file: " + filename); - } - - close(fd_); - fd_ = -1; - - parse_header(); - apply_madvise_hints(); -} - -MappedFile::~MappedFile() { - if (mapped_data_ != nullptr && mapped_data_ != MAP_FAILED) { - madvise(mapped_data_, file_size_, MADV_DONTNEED); - munmap(mapped_data_, file_size_); - mapped_data_ = nullptr; - } - if (fd_ != -1) { - close(fd_); - fd_ = -1; - } -} - -MappedFile::MappedFile(MappedFile&& other) noexcept - : fd_(other.fd_), mapped_data_(other.mapped_data_), file_size_(other.file_size_), - data_offset_(other.data_offset_), shape_(std::move(other.shape_)), - precision_(other.precision_), byte_size_(other.byte_size_), - group_size_(other.group_size_), num_groups_(other.num_groups_), - scales_offset_(other.scales_offset_), scales_bytes_(other.scales_bytes_), - alignment_(other.alignment_), - is_interleaved_(other.is_interleaved_), - original_N_(other.original_N_) { - other.fd_ = -1; - other.mapped_data_ = nullptr; - other.file_size_ = 0; - other.is_interleaved_ = false; - other.original_N_ = 0; -} - -MappedFile& MappedFile::operator=(MappedFile&& other) noexcept { - if (this != &other) { - if (mapped_data_ != nullptr && mapped_data_ != MAP_FAILED) { - munmap(mapped_data_, file_size_); - } - if (fd_ != -1) { - close(fd_); - } - - fd_ = other.fd_; - mapped_data_ = other.mapped_data_; - file_size_ = other.file_size_; - data_offset_ = other.data_offset_; - shape_ = std::move(other.shape_); - precision_ = other.precision_; - byte_size_ = other.byte_size_; - group_size_ = other.group_size_; - num_groups_ = other.num_groups_; - scales_offset_ = other.scales_offset_; - scales_bytes_ = other.scales_bytes_; - alignment_ = other.alignment_; - is_interleaved_ = other.is_interleaved_; - original_N_ = other.original_N_; - other.fd_ = -1; - other.mapped_data_ = nullptr; - other.file_size_ = 0; - other.is_interleaved_ = false; - other.original_N_ = 0; - } - return *this; -} - -const std::vector& MappedFile::shape() const { - return shape_; -} - -Precision MappedFile::precision() const { - return precision_; -} - -size_t MappedFile::byte_size() const { - return byte_size_; -} - -const void* MappedFile::scales_data() const { - return static_cast(mapped_data_) + scales_offset_; -} - -void* MappedFile::data() { - return static_cast(mapped_data_) + data_offset_; -} - -const void* MappedFile::data() const { - return static_cast(mapped_data_) + data_offset_; -} - -template -const T* MappedFile::typed_data() const { - return static_cast(data()); -} - -void MappedFile::parse_header() { - if (file_size_ < HEADER_SIZE) { - throw std::runtime_error("File too small: insufficient data for header"); - } - - const char* ptr = static_cast(mapped_data_); - size_t offset = 0; - - uint32_t magic = *reinterpret_cast(ptr + offset); - offset += sizeof(uint32_t); - if (magic != CACTUS_MAGIC) { - throw std::runtime_error("Invalid tensor file: missing CACT magic number"); - } - - uint32_t flags = *reinterpret_cast(ptr + offset); - offset += sizeof(uint32_t); - is_interleaved_ = (flags & FLAG_INTERLEAVED) != 0; - - alignment_ = *reinterpret_cast(ptr + offset); - offset += sizeof(uint32_t); - if (alignment_ == 0) alignment_ = 1; - - uint32_t ndim = *reinterpret_cast(ptr + offset); - offset += sizeof(uint32_t); - - shape_.clear(); - for (uint32_t i = 0; i < 4; i++) { - uint64_t dim_val = *reinterpret_cast(ptr + offset); - offset += sizeof(uint64_t); - if (i < ndim && dim_val > 0) { - shape_.push_back(static_cast(dim_val)); - } - } - - uint32_t prec_val = *reinterpret_cast(ptr + offset); - precision_ = static_cast(prec_val); - offset += sizeof(uint32_t); - - byte_size_ = *reinterpret_cast(ptr + offset); - offset += sizeof(uint64_t); - - scales_bytes_ = *reinterpret_cast(ptr + offset); - offset += sizeof(uint64_t); - - group_size_ = *reinterpret_cast(ptr + offset); - offset += sizeof(uint32_t); - - num_groups_ = *reinterpret_cast(ptr + offset); - offset += sizeof(uint32_t); - - original_N_ = *reinterpret_cast(ptr + offset); - offset += sizeof(uint64_t); - - size_t aligned_header = align_offset(HEADER_SIZE, alignment_); - - if (scales_bytes_ > 0) { - scales_offset_ = aligned_header; - size_t scales_end = scales_offset_ + scales_bytes_; - data_offset_ = align_offset(scales_end, alignment_); - } else { - scales_offset_ = 0; - data_offset_ = aligned_header; - } - - if (data_offset_ + byte_size_ > file_size_) { - throw std::runtime_error("File corrupted: data extends beyond file size"); - } - -} - -void MappedFile::apply_madvise_hints() { - if (scales_bytes_ > 0 && scales_offset_ > 0) { - madvise(static_cast(mapped_data_) + scales_offset_, scales_bytes_, MADV_WILLNEED); - } - - madvise(static_cast(mapped_data_) + data_offset_, byte_size_, MADV_SEQUENTIAL); - - if (byte_size_ > 1024 * 1024) { - madvise(static_cast(mapped_data_) + data_offset_, byte_size_, MADV_WILLNEED); - } -} - -void MappedFile::release_pages() { - if (mapped_data_ == nullptr || mapped_data_ == MAP_FAILED) return; - - if (scales_bytes_ > 0 && scales_offset_ > 0) { - madvise(static_cast(mapped_data_) + scales_offset_, scales_bytes_, MADV_DONTNEED); - } - madvise(static_cast(mapped_data_) + data_offset_, byte_size_, MADV_DONTNEED); -} - -void MappedFile::prefetch_pages() { - if (mapped_data_ == nullptr || mapped_data_ == MAP_FAILED) return; - - if (scales_bytes_ > 0 && scales_offset_ > 0) { - madvise(static_cast(mapped_data_) + scales_offset_, scales_bytes_, MADV_WILLNEED); - } - madvise(static_cast(mapped_data_) + data_offset_, byte_size_, MADV_WILLNEED); -} - -template const int8_t* MappedFile::typed_data() const; -template const float* MappedFile::typed_data() const; -template const uint16_t* MappedFile::typed_data() const; -template const uint8_t* MappedFile::typed_data() const; - -} // namespace GraphFile diff --git a/cactus/graph/graph_ops_math.cpp b/cactus/graph/graph_ops_math.cpp deleted file mode 100644 index 811da3511..000000000 --- a/cactus/graph/graph_ops_math.cpp +++ /dev/null @@ -1,348 +0,0 @@ -#include "graph.h" -#include "../kernel/kernel.h" -#include -#include - -namespace Quantization { - void int8_to_fp32(const int8_t* src, float* dst, size_t count, float scale) { - cactus_int8_to_fp32(src, dst, count, scale); - } - - void fp32_to_int8(const float* src, int8_t* dst, size_t count, float scale) { - cactus_fp32_to_int8(src, dst, count, scale); - } - - void fp16_to_fp32(const __fp16* src, float* dst, size_t count) { - cactus_fp16_to_fp32(src, dst, count); - } - - void fp32_to_fp16(const float* src, __fp16* dst, size_t count) { - cactus_fp32_to_fp16(src, dst, count); - } - - void int8_to_fp16(const int8_t* src, __fp16* dst, size_t count, float scale) { - cactus_int8_to_fp16(src, dst, count, scale); - } - - void fp16_to_int8(const __fp16* src, int8_t* dst, size_t count, float scale) { - cactus_fp16_to_int8(src, dst, count, scale); - } -} - -static std::vector compute_strides(const std::vector& shape, const std::vector& target_shape) { - std::vector strides(target_shape.size()); - - size_t shape_offset = target_shape.size() - shape.size(); - - for (size_t i = 0; i < target_shape.size(); ++i) { - if (i < shape_offset) { - strides[i] = 0; - } else { - size_t dim_idx = i - shape_offset; - if (shape[dim_idx] == 1) { - strides[i] = 0; - } else { - strides[i] = 1; - for (size_t j = dim_idx + 1; j < shape.size(); ++j) { - strides[i] *= shape[j]; - } - } - } - } - - return strides; -} - -void dispatch_binary_op_f16(OpType op, const __fp16* lhs, const __fp16* rhs, __fp16* output, size_t count) { - switch (op) { - case OpType::ADD: - cactus_add_f16(lhs, rhs, output, count); - break; - case OpType::ADD_CLIPPED: - cactus_add_f16_clipped(lhs, rhs, output, count); - break; - case OpType::SUBTRACT: - cactus_subtract_f16(lhs, rhs, output, count); - break; - case OpType::MULTIPLY: - cactus_multiply_f16(lhs, rhs, output, count); - break; - case OpType::DIVIDE: - cactus_divide_f16(lhs, rhs, output, count); - break; - default: - break; - } -} - -void dispatch_unary_op_f16(OpType op, const __fp16* input, __fp16* output, size_t count, float param) { - ScalarOpType scalar_op; - switch (op) { - case OpType::SCALAR_ADD: scalar_op = ScalarOpType::ADD; break; - case OpType::SCALAR_SUBTRACT: scalar_op = ScalarOpType::SUBTRACT; break; - case OpType::SCALAR_MULTIPLY: scalar_op = ScalarOpType::MULTIPLY; break; - case OpType::SCALAR_DIVIDE: scalar_op = ScalarOpType::DIVIDE; break; - case OpType::SCALAR_EXP: scalar_op = ScalarOpType::EXP; break; - case OpType::SCALAR_SQRT: scalar_op = ScalarOpType::SQRT; break; - case OpType::SCALAR_COS: scalar_op = ScalarOpType::COS; break; - case OpType::SCALAR_SIN: scalar_op = ScalarOpType::SIN; break; - case OpType::SCALAR_LOG: scalar_op = ScalarOpType::LOG; break; - case OpType::ABS: scalar_op = ScalarOpType::ABS; break; - case OpType::POW: scalar_op = ScalarOpType::POW; break; - default: return; - } - - cactus_scalar_op_f16(input, output, count, param, scalar_op); -} - -void compute_binary_op_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - const auto& lhs = get_input(node, 0, nodes, node_index_map); - const auto& rhs = get_input(node, 1, nodes, node_index_map); - - if (lhs.precision != Precision::FP16) { - throw std::runtime_error("Binary operations only support FP16 precision (got " + std::to_string(static_cast(lhs.precision)) + ")"); - } - - if (node.params.broadcast_info.needs_broadcasting) { - std::vector lhs_strides = compute_strides(lhs.shape, node.params.broadcast_info.output_shape); - std::vector rhs_strides = compute_strides(rhs.shape, node.params.broadcast_info.output_shape); - - switch (node.op_type) { - case OpType::ADD: - case OpType::ADD_CLIPPED: - cactus_add_broadcast_f16(lhs.data_as<__fp16>(), rhs.data_as<__fp16>(), - node.output_buffer.data_as<__fp16>(), - lhs_strides.data(), rhs_strides.data(), - node.params.broadcast_info.output_shape.data(), - node.params.broadcast_info.output_shape.size()); - break; - case OpType::SUBTRACT: - cactus_subtract_broadcast_f16(lhs.data_as<__fp16>(), rhs.data_as<__fp16>(), - node.output_buffer.data_as<__fp16>(), - lhs_strides.data(), rhs_strides.data(), - node.params.broadcast_info.output_shape.data(), - node.params.broadcast_info.output_shape.size()); - break; - case OpType::MULTIPLY: - cactus_multiply_broadcast_f16(lhs.data_as<__fp16>(), rhs.data_as<__fp16>(), - node.output_buffer.data_as<__fp16>(), - lhs_strides.data(), rhs_strides.data(), - node.params.broadcast_info.output_shape.data(), - node.params.broadcast_info.output_shape.size()); - break; - case OpType::DIVIDE: - cactus_divide_broadcast_f16(lhs.data_as<__fp16>(), rhs.data_as<__fp16>(), - node.output_buffer.data_as<__fp16>(), - lhs_strides.data(), rhs_strides.data(), - node.params.broadcast_info.output_shape.data(), - node.params.broadcast_info.output_shape.size()); - break; - default: break; - } - } else { - dispatch_binary_op_f16(node.op_type, lhs.data_as<__fp16>(), - rhs.data_as<__fp16>(), node.output_buffer.data_as<__fp16>(), - node.output_buffer.total_size); - } -} - -void compute_unary_op_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - const auto& input = get_input(node, 0, nodes, node_index_map); - - if (input.precision != Precision::FP16) { - throw std::runtime_error("Scalar operations only support FP16 precision"); - } - - dispatch_unary_op_f16(node.op_type, input.data_as<__fp16>(), - node.output_buffer.data_as<__fp16>(), - node.output_buffer.total_size, node.params.scalar); -} - -void compute_activation_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - const auto& input = get_input(node, 0, nodes, node_index_map); - - if (input.precision != Precision::FP16) { - throw std::runtime_error("Activation operations only support FP16 precision"); - } - - switch (node.op_type) { - case OpType::RELU: - cactus_relu_f16(input.data_as<__fp16>(), - node.output_buffer.data_as<__fp16>(), - node.output_buffer.total_size); - break; - case OpType::SILU: - cactus_silu_f16(input.data_as<__fp16>(), - node.output_buffer.data_as<__fp16>(), - node.output_buffer.total_size); - break; - case OpType::GELU: - cactus_gelu_f16(input.data_as<__fp16>(), - node.output_buffer.data_as<__fp16>(), - node.output_buffer.total_size); - break; - case OpType::GELU_ERF: - cactus_gelu_f16_erf(input.data_as<__fp16>(), - node.output_buffer.data_as<__fp16>(), - node.output_buffer.total_size); - break; - case OpType::SIGMOID: - cactus_sigmoid_f16(input.data_as<__fp16>(), - node.output_buffer.data_as<__fp16>(), - node.output_buffer.total_size); - break; - case OpType::TANH: - cactus_tanh_f16(input.data_as<__fp16>(), - node.output_buffer.data_as<__fp16>(), - node.output_buffer.total_size); - break; - case OpType::LEAKY_RELU: - cactus_leaky_relu_f16(input.data_as<__fp16>(), - node.output_buffer.data_as<__fp16>(), - node.output_buffer.total_size, node.params.scalar); - break; - default: - break; - } -} - -void compute_reduce_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - const auto& input_buffer = get_input(node, 0, nodes, node_index_map); - int axis = node.params.axis; - - if (input_buffer.precision != Precision::FP16) { - throw std::runtime_error("Reduction operations only support FP16 precision"); - } - - if (axis == -1) { - switch (node.op_type) { - case OpType::SUM: { - double result = cactus_sum_all_f16(input_buffer.data_as<__fp16>(), input_buffer.total_size); - node.output_buffer.data_as<__fp16>()[0] = static_cast<__fp16>(result); - break; - } - case OpType::MEAN: { - double result = cactus_mean_all_f16(input_buffer.data_as<__fp16>(), input_buffer.total_size); - node.output_buffer.data_as<__fp16>()[0] = static_cast<__fp16>(result); - break; - } - case OpType::VARIANCE: { - double result = cactus_variance_all_f16(input_buffer.data_as<__fp16>(), input_buffer.total_size); - node.output_buffer.data_as<__fp16>()[0] = static_cast<__fp16>(result); - break; - } - case OpType::MIN: { - __fp16 result = cactus_min_all_f16(input_buffer.data_as<__fp16>(), input_buffer.total_size); - node.output_buffer.data_as<__fp16>()[0] = result; - break; - } - case OpType::MAX: { - __fp16 result = cactus_max_all_f16(input_buffer.data_as<__fp16>(), input_buffer.total_size); - node.output_buffer.data_as<__fp16>()[0] = result; - break; - } - default: break; - } - } else { - auto dims = AxisDims::from_shape(input_buffer.shape, static_cast(axis)); - - switch (node.op_type) { - case OpType::SUM: - cactus_sum_axis_f16(input_buffer.data_as<__fp16>(), node.output_buffer.data_as<__fp16>(), - dims.outer, dims.axis_size, dims.inner); - break; - case OpType::MEAN: - cactus_mean_axis_f16(input_buffer.data_as<__fp16>(), node.output_buffer.data_as<__fp16>(), - dims.outer, dims.axis_size, dims.inner); - break; - case OpType::VARIANCE: - cactus_variance_axis_f16(input_buffer.data_as<__fp16>(), node.output_buffer.data_as<__fp16>(), - dims.outer, dims.axis_size, dims.inner); - break; - case OpType::MIN: - cactus_min_axis_f16(input_buffer.data_as<__fp16>(), node.output_buffer.data_as<__fp16>(), - dims.outer, dims.axis_size, dims.inner); - break; - case OpType::MAX: - cactus_max_axis_f16(input_buffer.data_as<__fp16>(), node.output_buffer.data_as<__fp16>(), - dims.outer, dims.axis_size, dims.inner); - break; - default: break; - } - } -} - -void compute_reshape_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - const auto& input_buffer = get_input(node, 0, nodes, node_index_map); - - size_t input_total_elements = input_buffer.total_size; - size_t output_total_elements = node.output_buffer.total_size; - - if (input_total_elements != output_total_elements) { - throw std::runtime_error("Reshape operation: input elements (" + std::to_string(input_total_elements) + - ") must match output elements (" + std::to_string(output_total_elements) + ")"); - } - - std::memcpy(node.output_buffer.get_data(), input_buffer.get_data(), input_buffer.byte_size); -} - -template -static void dequant_grouped_int8(const int8_t* src, OutT* dst, const __fp16* scales, - const std::vector& shape, size_t group_size) { - if (shape.size() == 2) { - size_t N = shape[0], K = shape[1]; - size_t num_groups = K / group_size; - for (size_t row = 0; row < N; ++row) { - for (size_t col = 0; col < K; ++col) { - size_t idx = row * K + col; - float scale = static_cast(scales[row * num_groups + col / group_size]); - dst[idx] = static_cast(static_cast(src[idx]) * scale); - } - } - } else if (shape.size() == 1) { - size_t K = shape[0]; - for (size_t col = 0; col < K; ++col) { - float scale = static_cast(scales[col / group_size]); - dst[col] = static_cast(static_cast(src[col]) * scale); - } - } -} - -void compute_precision_cast_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - const auto& input_buf = get_input(node, 0, nodes, node_index_map); - - if (input_buf.precision == node.output_buffer.precision) { - std::memcpy(node.output_buffer.get_data(), input_buf.get_data(), input_buf.byte_size); - return; - } - - size_t count = input_buf.total_size; - - if (input_buf.precision == Precision::INT8 && node.output_buffer.precision == Precision::FP32) { - if (input_buf.is_grouped_int8()) { - dequant_grouped_int8(input_buf.data_as(), node.output_buffer.data_as(), - input_buf.scales_as_fp16(), input_buf.shape, input_buf.group_size); - } else { - Quantization::int8_to_fp32(input_buf.data_as(), node.output_buffer.data_as(), count, 1.0f); - } - } else if (input_buf.precision == Precision::FP32 && node.output_buffer.precision == Precision::INT8) { - Quantization::fp32_to_int8(input_buf.data_as(), node.output_buffer.data_as(), count, 1.0f); - } else if (input_buf.precision == Precision::FP16 && node.output_buffer.precision == Precision::FP32) { - Quantization::fp16_to_fp32(input_buf.data_as<__fp16>(), node.output_buffer.data_as(), count); - } else if (input_buf.precision == Precision::FP32 && node.output_buffer.precision == Precision::FP16) { - Quantization::fp32_to_fp16(input_buf.data_as(), node.output_buffer.data_as<__fp16>(), count); - } else if (input_buf.precision == Precision::INT8 && node.output_buffer.precision == Precision::FP16) { - if (input_buf.is_grouped_int8()) { - dequant_grouped_int8<__fp16>(input_buf.data_as(), node.output_buffer.data_as<__fp16>(), - input_buf.scales_as_fp16(), input_buf.shape, input_buf.group_size); - } else { - Quantization::int8_to_fp16(input_buf.data_as(), node.output_buffer.data_as<__fp16>(), count, 1.0f); - } - } else if (input_buf.precision == Precision::FP16 && node.output_buffer.precision == Precision::INT8) { - Quantization::fp16_to_int8(input_buf.data_as<__fp16>(), node.output_buffer.data_as(), count, 1.0f); - } else { - throw std::runtime_error("Unsupported precision conversion from " + - std::to_string(static_cast(input_buf.precision)) + - " to " + std::to_string(static_cast(node.output_buffer.precision))); - } -} diff --git a/cactus/graph/graph_ops_nn.cpp b/cactus/graph/graph_ops_nn.cpp deleted file mode 100644 index ae6f92b0e..000000000 --- a/cactus/graph/graph_ops_nn.cpp +++ /dev/null @@ -1,2532 +0,0 @@ -#include "graph.h" -#include "../kernel/kernel.h" -#include "../kernel/kernel_utils.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace { - thread_local std::vector<__fp16> transpose_buffer_fp16; - thread_local std::vector quant_activation_buffer; - thread_local std::vector quant_scales_buffer; - - thread_local const __fp16* cached_quant_src = nullptr; - thread_local size_t cached_quant_M = 0; - thread_local size_t cached_quant_K = 0; - - void ensure_transpose_buffer_fp16(size_t required_size) { - if (transpose_buffer_fp16.size() < required_size) { - transpose_buffer_fp16.resize(required_size); - } - } - - void ensure_quant_buffers(size_t M, size_t K) { - size_t required_data = M * K; - if (quant_activation_buffer.size() < required_data) { - quant_activation_buffer.resize(required_data); - } - if (quant_scales_buffer.size() < M) { - quant_scales_buffer.resize(M); - } - } - - void quantize_activations_fp16_to_int8(const __fp16* src, int8_t* dst, float* scales, size_t M, size_t K) { - if (src == cached_quant_src && M == cached_quant_M && K == cached_quant_K) { - return; - } - - constexpr size_t PARALLEL_THRESHOLD = 16; - - if (M >= PARALLEL_THRESHOLD) { - CactusThreading::parallel_for(M, CactusThreading::Thresholds::ELEMENT_WISE, - [src, dst, scales, K](size_t m_start, size_t m_end) { - for (size_t m = m_start; m < m_end; m++) { - float max_abs = cactus_fp16_max_abs(src + m * K, K); - float scale = max_abs / 127.0f; - if (scale < 1e-10f) scale = 1e-10f; - scales[m] = scale; - cactus_fp16_to_int8(src + m * K, dst + m * K, K, scale); - } - }); - } else { - for (size_t m = 0; m < M; m++) { - float max_abs = cactus_fp16_max_abs(src + m * K, K); - float scale = max_abs / 127.0f; - if (scale < 1e-10f) scale = 1e-10f; - scales[m] = scale; - cactus_fp16_to_int8(src + m * K, dst + m * K, K, scale); - } - } - - cached_quant_src = src; - cached_quant_M = M; - cached_quant_K = K; - } - - const __fp16* as_fp16_ptr(const BufferDesc& buffer, std::vector<__fp16>& scratch) { - if (buffer.precision == Precision::FP16) { - return buffer.data_as<__fp16>(); - } - if (buffer.precision == Precision::FP32) { - scratch.resize(buffer.total_size); - cactus_fp32_to_fp16(buffer.data_as(), scratch.data(), buffer.total_size); - return scratch.data(); - } - throw std::runtime_error("GATED_DELTANET unsupported precision (expected FP16/FP32)"); - } - - void validate_gated_deltanet_inputs( - const BufferDesc& q, - const BufferDesc& k, - const BufferDesc& v, - const BufferDesc& g, - const BufferDesc& b, - const BufferDesc& s) { - auto is_supported_precision = [](Precision p) { - return p == Precision::FP16 || p == Precision::FP32; - }; - if (!is_supported_precision(q.precision) || !is_supported_precision(k.precision) || - !is_supported_precision(v.precision) || !is_supported_precision(g.precision) || - !is_supported_precision(b.precision) || !is_supported_precision(s.precision)) { - throw std::runtime_error("GATED_DELTANET requires FP16/FP32 inputs"); - } - - if (q.shape.size() != 4 || k.shape.size() != 4 || v.shape.size() != 4) { - throw std::runtime_error("GATED_DELTANET expects query/key/value rank 4 [B, T, H, D]"); - } - if (g.shape.size() != 3 || b.shape.size() != 3) { - throw std::runtime_error("GATED_DELTANET expects gate_log/beta rank 3 [B, T, H]"); - } - if (s.shape.size() != 4) { - throw std::runtime_error("GATED_DELTANET expects state rank 4 [B, K, H, V]"); - } - - const size_t B = q.shape[0]; - const size_t T = q.shape[1]; - const size_t Hq = q.shape[2]; - const size_t K = q.shape[3]; - - if (k.shape[0] != B || k.shape[1] != T || k.shape[2] != Hq || k.shape[3] != K) { - throw std::runtime_error("GATED_DELTANET query/key shape mismatch"); - } - if (v.shape[0] != B || v.shape[1] != T) { - throw std::runtime_error("GATED_DELTANET value shape mismatch"); - } - const size_t Hv = v.shape[2]; - if (g.shape[0] != B || g.shape[1] != T || g.shape[2] != Hv || - b.shape[0] != B || b.shape[1] != T || b.shape[2] != Hv) { - throw std::runtime_error("GATED_DELTANET gate_log/beta shape mismatch"); - } - if (Hq == 0 || Hv == 0 || (Hv % Hq) != 0) { - throw std::runtime_error("GATED_DELTANET expects value heads divisible by q/k heads"); - } - const size_t V = v.shape[3]; - if (s.shape[0] != B || s.shape[1] != K || s.shape[2] != Hv || s.shape[3] != V) { - throw std::runtime_error("GATED_DELTANET state shape mismatch"); - } - } - - -} - -void shrink_thread_local_buffers() { - std::vector<__fp16>().swap(transpose_buffer_fp16); - std::vector().swap(quant_activation_buffer); - std::vector().swap(quant_scales_buffer); - cached_quant_src = nullptr; - cached_quant_M = 0; - cached_quant_K = 0; -} - -void compute_quantize_activations_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - const auto& input_buffer = get_input(node, 0, nodes, node_index_map); - const auto& shape = input_buffer.shape; - - if (input_buffer.precision != Precision::FP16) { - throw std::runtime_error("QUANTIZE_ACTIVATIONS requires FP16 input"); - } - - if (shape.size() < 2) { - throw std::runtime_error("QUANTIZE_ACTIVATIONS requires at least 2D tensor"); - } - - size_t K = shape.back(); - size_t M = 1; - for (size_t i = 0; i < shape.size() - 1; ++i) { - M *= shape[i]; - } - - if (!node.output_buffer.has_activation_scales() || - node.output_buffer.num_rows_for_activation_scales != M) { - node.output_buffer.allocate_activation_scales(M); - } - - const __fp16* src = input_buffer.data_as<__fp16>(); - int8_t* dst = node.output_buffer.data_as(); - float* scales = node.output_buffer.activation_scales_as_float(); - - constexpr size_t PARALLEL_THRESHOLD = 16; - - if (M >= PARALLEL_THRESHOLD) { - CactusThreading::parallel_for(M, CactusThreading::Thresholds::ELEMENT_WISE, - [src, dst, scales, K](size_t m_start, size_t m_end) { - for (size_t m = m_start; m < m_end; m++) { - float max_abs = cactus_fp16_max_abs(src + m * K, K); - float scale = max_abs / 127.0f; - if (scale < 1e-10f) scale = 1e-10f; - scales[m] = scale; - cactus_fp16_to_int8(src + m * K, dst + m * K, K, scale); - } - }); - } else { - for (size_t m = 0; m < M; m++) { - float max_abs = cactus_fp16_max_abs(src + m * K, K); - float scale = max_abs / 127.0f; - if (scale < 1e-10f) scale = 1e-10f; - scales[m] = scale; - cactus_fp16_to_int8(src + m * K, dst + m * K, K, scale); - } - } -} - -void compute_matmul_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - const auto& lhs_buffer = get_input(node, 0, nodes, node_index_map); - const auto& rhs_buffer = get_input(node, 1, nodes, node_index_map); - const auto& lhs_shape = lhs_buffer.shape; - const auto& rhs_shape = rhs_buffer.shape; - - size_t M = lhs_shape[lhs_shape.size() - 2]; - size_t K = lhs_shape[lhs_shape.size() - 1]; - size_t N; - if (rhs_buffer.is_interleaved && rhs_buffer.original_N > 0) { - N = rhs_buffer.original_N; - } else { - N = node.params.pretransposed_rhs ? - rhs_shape[rhs_shape.size() - 2] : rhs_shape[rhs_shape.size() - 1]; - } - - bool pretransposed_rhs = node.params.pretransposed_rhs; - - ComputeBackend backend = node.params.backend; - - if (backend == ComputeBackend::NPU) { - throw std::runtime_error("NPU matrix multiplication not yet implemented"); - } - - const bool lhs_is_prequantized_int8 = (lhs_buffer.precision == Precision::INT8 && - lhs_buffer.has_activation_scales()); - - if (PrecisionTraits::is_integer(rhs_buffer.precision) && rhs_buffer.group_size > 0) { - const int8_t* rhs = rhs_buffer.data_as(); - const __fp16* rhs_scales = rhs_buffer.scales_as_fp16(); - __fp16* output = node.output_buffer.data_as<__fp16>(); - - if (!pretransposed_rhs) { - throw std::runtime_error("Group-wise quantized matmul requires pretransposed weights"); - } - - const int8_t* lhs_int8; - const float* lhs_scales; - - if (lhs_is_prequantized_int8) { - lhs_int8 = lhs_buffer.data_as(); - lhs_scales = lhs_buffer.activation_scales_as_float(); - } else if (lhs_buffer.precision == Precision::FP16) { - ensure_quant_buffers(M, K); - cached_quant_src = nullptr; - quantize_activations_fp16_to_int8(lhs_buffer.data_as<__fp16>(), quant_activation_buffer.data(), - quant_scales_buffer.data(), M, K); - lhs_int8 = quant_activation_buffer.data(); - lhs_scales = quant_scales_buffer.data(); - } else { - throw std::runtime_error("Quantized matmul requires INT8 (pre-quantized) or FP16 activations"); - } - - cactus_matmul_integer(rhs_buffer.precision, - lhs_int8, lhs_scales, - rhs, rhs_scales, output, - M, K, N, rhs_buffer.group_size); - } else { - if (lhs_buffer.precision != Precision::FP16) { - throw std::runtime_error("FP16 matmul requires FP16 activations (got precision " + std::to_string(static_cast(lhs_buffer.precision)) + ")"); - } - - const __fp16* lhs = lhs_buffer.data_as<__fp16>(); - const __fp16* rhs = rhs_buffer.data_as<__fp16>(); - __fp16* output = node.output_buffer.data_as<__fp16>(); - - if (pretransposed_rhs) { - cactus_matmul_f16(lhs, rhs, output, M, K, N); - } else { - size_t transpose_size = rhs_shape[0] * rhs_shape[1]; - ensure_transpose_buffer_fp16(transpose_size); - - cactus_transpose_2d_f16(rhs, transpose_buffer_fp16.data(), - rhs_shape[0], rhs_shape[1], 0, rhs_shape[0]); - cactus_matmul_f16(lhs, transpose_buffer_fp16.data(), output, M, K, N); - } - } -} - -namespace { - thread_local std::vector<__fp16> moe_compact_hidden_buf; - thread_local std::vector<__fp16> moe_gate_buf; - thread_local std::vector<__fp16> moe_up_buf; - thread_local std::vector<__fp16> moe_expert_out_buf; - thread_local std::vector moe_lhs_q_buf; - thread_local std::vector moe_lhs_scales_buf; - thread_local std::vector moe_expert_offsets_buf; - thread_local std::vector moe_expert_tokens_buf; - thread_local std::vector moe_routing_denom_buf; - - void ensure_moe_buffers(size_t max_tokens, size_t hidden_dim, size_t intermediate_dim, - size_t num_experts, size_t top_k) { - size_t hidden_size = max_tokens * hidden_dim; - size_t inter_size = max_tokens * intermediate_dim; - if (moe_compact_hidden_buf.size() < hidden_size) moe_compact_hidden_buf.resize(hidden_size); - if (moe_gate_buf.size() < inter_size) moe_gate_buf.resize(inter_size); - if (moe_up_buf.size() < inter_size) moe_up_buf.resize(inter_size); - if (moe_expert_out_buf.size() < hidden_size) moe_expert_out_buf.resize(hidden_size); - size_t max_k = std::max(hidden_dim, intermediate_dim); - size_t quant_size = max_tokens * max_k; - if (moe_lhs_q_buf.size() < quant_size) moe_lhs_q_buf.resize(quant_size); - if (moe_lhs_scales_buf.size() < max_tokens) moe_lhs_scales_buf.resize(max_tokens); - size_t total_assignments = max_tokens * top_k; - if (moe_expert_offsets_buf.size() < num_experts + 1) moe_expert_offsets_buf.resize(num_experts + 1); - if (moe_expert_tokens_buf.size() < total_assignments) moe_expert_tokens_buf.resize(total_assignments); - if (moe_routing_denom_buf.size() < max_tokens) moe_routing_denom_buf.resize(max_tokens); - } - - void moe_matmul(const __fp16* lhs, - size_t M, - size_t K, - const BufferDesc& rhs_buffer, - __fp16* output, - size_t N, - bool lhs_prequantized = false) { - if (rhs_buffer.precision == Precision::FP16) { - cactus_matmul_f16(lhs, rhs_buffer.data_as<__fp16>(), output, M, K, N); - return; - } - - if (PrecisionTraits::is_integer(rhs_buffer.precision) && rhs_buffer.group_size > 0) { - int8_t* lhs_q = moe_lhs_q_buf.data(); - float* lhs_scales = moe_lhs_scales_buf.data(); - if (!lhs_prequantized) { - for (size_t row = 0; row < M; ++row) { - float scale = cactus_fp16_max_abs(lhs + row * K, K) / 127.0f; - if (scale < 1e-10f) scale = 1e-10f; - lhs_scales[row] = scale; - cactus_fp16_to_int8(lhs + row * K, lhs_q + row * K, K, scale); - } - } - cactus_matmul_integer(rhs_buffer.precision, - lhs_q, lhs_scales, - rhs_buffer.data_as(), - rhs_buffer.scales_as_fp16(), - output, M, K, N, rhs_buffer.group_size); - return; - } - - throw std::runtime_error("moe_layer only supports FP16 or grouped INT4/INT8 expert weights"); - } -} - -void compute_moe_layer_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - const size_t num_experts = node.params.num_experts; - const size_t top_k = node.params.num_experts_per_tok; - const bool normalize_routing = node.params.normalize_routing; - const float eps = node.params.epsilon; - const float routed_scaling_factor = node.params.scalar; - const bool gated = node.params.moe_gated; - const Activation activation = node.params.activation; - const size_t base_inputs = gated ? (3 + 3 * num_experts) : (3 + 2 * num_experts); - bool has_per_expert_scale = node.input_ids.size() == base_inputs + 1; - if (node.input_ids.size() != base_inputs && node.input_ids.size() != base_inputs + 1) { - throw std::runtime_error("moe_layer expects " + std::to_string(base_inputs) + " or " + std::to_string(base_inputs + 1) + " inputs, got " + std::to_string(node.input_ids.size())); - } - - const auto& hidden_buffer = get_input(node, 0, nodes, node_index_map); - const auto& routing_buffer = get_input(node, 1, nodes, node_index_map); - const auto& topk_idx_buffer = get_input(node, 2, nodes, node_index_map); - - if (hidden_buffer.precision != Precision::FP16 || node.output_buffer.precision != Precision::FP16) { - throw std::runtime_error("moe_layer expects FP16 hidden/output"); - } - if (topk_idx_buffer.precision != Precision::FP32) { - throw std::runtime_error("moe_layer expects FP32 topk indices"); - } - - const __fp16* expert_scales_fp16 = nullptr; - if (has_per_expert_scale) { - const auto& scale_buffer = get_input(node, base_inputs, nodes, node_index_map); - if (scale_buffer.precision != Precision::FP16) { - throw std::runtime_error("moe_layer expects FP16 per_expert_scale"); - } - expert_scales_fp16 = scale_buffer.data_as<__fp16>(); - } - - const size_t token_count = hidden_buffer.shape[0]; - const size_t hidden_dim = hidden_buffer.shape[1]; - const size_t total_num_experts = routing_buffer.shape[1]; - - const auto& w1_0_buffer = get_input(node, 3, nodes, node_index_map); - const size_t expert_intermediate_dim = w1_0_buffer.shape[0]; - - const auto* hidden = hidden_buffer.data_as<__fp16>(); - auto* output = node.output_buffer.data_as<__fp16>(); - const auto* topk_idx = topk_idx_buffer.data_as(); - const auto* routing_fp16 = routing_buffer.precision == Precision::FP16 ? routing_buffer.data_as<__fp16>() : nullptr; - const auto* routing_fp32 = routing_buffer.precision == Precision::FP32 ? routing_buffer.data_as() : nullptr; - - auto routing_prob = [&](size_t tok, size_t exp) -> float { - const size_t offset = tok * total_num_experts + exp; - if (routing_fp16) return static_cast(routing_fp16[offset]); - return routing_fp32[offset]; - }; - - ensure_moe_buffers(token_count, hidden_dim, expert_intermediate_dim, num_experts, top_k); - - size_t* expert_offsets = moe_expert_offsets_buf.data(); - size_t* expert_tokens_flat = moe_expert_tokens_buf.data(); - - std::memset(expert_offsets, 0, (num_experts + 1) * sizeof(size_t)); - for (size_t tok = 0; tok < token_count; ++tok) { - for (size_t k = 0; k < top_k; ++k) { - float raw_idx = topk_idx[tok * top_k + k]; - if (!std::isfinite(raw_idx)) { - throw std::runtime_error("moe_layer got non-finite expert index"); - } - size_t idx = static_cast(raw_idx + 0.5f); - if (idx >= num_experts) { - throw std::runtime_error("moe_layer got expert index out of range"); - } - expert_offsets[idx + 1]++; - } - } - - for (size_t e = 0; e < num_experts; ++e) { - expert_offsets[e + 1] += expert_offsets[e]; - } - - thread_local std::vector moe_write_cursors; - if (moe_write_cursors.size() < num_experts) moe_write_cursors.resize(num_experts); - std::memcpy(moe_write_cursors.data(), expert_offsets, num_experts * sizeof(size_t)); - - for (size_t tok = 0; tok < token_count; ++tok) { - for (size_t k = 0; k < top_k; ++k) { - size_t idx = static_cast(topk_idx[tok * top_k + k] + 0.5f); - expert_tokens_flat[moe_write_cursors[idx]++] = tok; - } - } - - float* routing_denom = moe_routing_denom_buf.data(); - if (normalize_routing) { - for (size_t tok = 0; tok < token_count; ++tok) { - float sum_probs = 0.0f; - for (size_t k = 0; k < top_k; ++k) { - size_t idx = static_cast(topk_idx[tok * top_k + k] + 0.5f); - sum_probs += routing_prob(tok, idx); - } - routing_denom[tok] = sum_probs + eps; - } - } - - std::memset(output, 0, token_count * hidden_dim * sizeof(__fp16)); - - for (size_t expert_idx = 0; expert_idx < num_experts; ++expert_idx) { - const size_t start = expert_offsets[expert_idx]; - const size_t end = expert_offsets[expert_idx + 1]; - if (start == end) continue; - - const size_t selected_count = end - start; - const size_t* selected_tokens = expert_tokens_flat + start; - - const auto& w1_buffer = get_input(node, 3 + expert_idx, nodes, node_index_map); - const auto& w2_buffer = gated - ? get_input(node, 3 + 2 * num_experts + expert_idx, nodes, node_index_map) - : get_input(node, 3 + num_experts + expert_idx, nodes, node_index_map); - - __fp16* compact_hidden = moe_compact_hidden_buf.data(); - for (size_t i = 0; i < selected_count; ++i) { - std::memcpy(compact_hidden + i * hidden_dim, - hidden + selected_tokens[i] * hidden_dim, - hidden_dim * sizeof(__fp16)); - } - - __fp16* gate = moe_gate_buf.data(); - __fp16* up = moe_up_buf.data(); - __fp16* expert_out = moe_expert_out_buf.data(); - - moe_matmul(compact_hidden, selected_count, hidden_dim, w1_buffer, gate, expert_intermediate_dim); - const bool w1_was_int8 = w1_buffer.is_grouped_int8(); - - switch (activation) { - case Activation::GELU: - cactus_gelu_f16(gate, gate, selected_count * expert_intermediate_dim); - break; - case Activation::GELU_ERF: - cactus_gelu_f16_erf(gate, gate, selected_count * expert_intermediate_dim); - break; - case Activation::RELU: - cactus_relu_f16(gate, gate, selected_count * expert_intermediate_dim); - break; - case Activation::SILU: - default: - cactus_silu_f16(gate, gate, selected_count * expert_intermediate_dim); - break; - } - - if (gated) { - const auto& w3_buffer = get_input(node, 3 + num_experts + expert_idx, nodes, node_index_map); - moe_matmul(compact_hidden, selected_count, hidden_dim, w3_buffer, up, expert_intermediate_dim, w1_was_int8); - cactus_multiply_f16(gate, up, gate, selected_count * expert_intermediate_dim); - } - - moe_matmul(gate, selected_count, expert_intermediate_dim, w2_buffer, expert_out, hidden_dim); - - for (size_t i = 0; i < selected_count; ++i) { - const size_t tok = selected_tokens[i]; - float expert_prob = routing_prob(tok, expert_idx); - if (expert_prob <= 0.0f) continue; - - float route_weight = expert_prob; - if (normalize_routing) { - route_weight = expert_prob / routing_denom[tok]; - } - route_weight *= routed_scaling_factor; - if (expert_scales_fp16) { - route_weight *= static_cast(expert_scales_fp16[expert_idx]); - } - - auto* out_row = output + tok * hidden_dim; - const auto* expert_row = expert_out + i * hidden_dim; - cactus_add_scaled_f16(out_row, expert_row, out_row, hidden_dim, route_weight); - } - } -} - -namespace { - struct DenseFusedScratch { - std::vector<__fp16> gate_tile; - std::vector<__fp16> up_tile; - }; - - // Inline to skip cactus_fp16_to_int8's parallel_for orchestration, which - // dispatches ~5 threads at d_ffn=12288 and dominates the SIMD work. - static inline void fp16_to_int8_inplace(const __fp16* src, int8_t* dst, size_t n, float scale) { - const float inv_scale = 1.0f / scale; - const float32x4_t inv_vec = vdupq_n_f32(inv_scale); - const float32x4_t min_vec = vdupq_n_f32(-128.0f); - const float32x4_t max_vec = vdupq_n_f32(127.0f); - size_t i = 0; - for (; i + 8 <= n; i += 8) { - float16x8_t in = vld1q_f16(src + i); - float32x4_t lo = vmulq_f32(vcvt_f32_f16(vget_low_f16(in)), inv_vec); - float32x4_t hi = vmulq_f32(vcvt_f32_f16(vget_high_f16(in)), inv_vec); - lo = vmaxq_f32(vminq_f32(lo, max_vec), min_vec); - hi = vmaxq_f32(vminq_f32(hi, max_vec), min_vec); - int16x8_t s = vcombine_s16(vqmovn_s32(vcvtnq_s32_f32(lo)), - vqmovn_s32(vcvtnq_s32_f32(hi))); - vst1_s8(dst + i, vqmovn_s16(s)); - } - for (; i < n; ++i) { - float v = static_cast(src[i]) * inv_scale; - if (v > 127.0f) v = 127.0f; else if (v < -128.0f) v = -128.0f; - dst[i] = static_cast(std::lround(v)); - } - } -} - -void compute_dense_mlp_int4_fused_node( - GraphNode& node, - const std::vector>& nodes, - const std::unordered_map& node_index_map) { - const auto& hidden_buf = get_input(node, 0, nodes, node_index_map); - const auto& gate_buf = get_input(node, 1, nodes, node_index_map); - const auto& up_buf = get_input(node, 2, nodes, node_index_map); - const auto& down_buf = get_input(node, 3, nodes, node_index_map); - - if (hidden_buf.precision != Precision::FP16) { - throw std::runtime_error("dense_mlp_int4_fused: hidden must be FP16"); - } - if (gate_buf.precision != Precision::INT4 || gate_buf.group_size == 0 || - up_buf.precision != Precision::INT4 || up_buf.group_size == 0 || - down_buf.precision != Precision::INT4 || down_buf.group_size == 0) { - throw std::runtime_error("dense_mlp_int4_fused: weights must be INT4 group-quantized"); - } - const size_t group_size = gate_buf.group_size; - if (up_buf.group_size != group_size || down_buf.group_size != group_size) { - throw std::runtime_error("dense_mlp_int4_fused: gate/up/down group_size must match"); - } - - const auto& shape = hidden_buf.shape; - const size_t hidden_dim_in = shape.back(); - size_t M = 1; - for (size_t i = 0; i + 1 < shape.size(); ++i) M *= shape[i]; - - const size_t d_ffn = gate_buf.is_interleaved ? gate_buf.original_N : gate_buf.shape[0]; - const size_t hidden_dim = down_buf.is_interleaved ? down_buf.original_N : down_buf.shape[0]; - if (hidden_dim_in != hidden_dim) { - throw std::runtime_error("dense_mlp_int4_fused: hidden dim mismatch with down weight"); - } - if (d_ffn % 4 != 0 || d_ffn % group_size != 0 || hidden_dim % 4 != 0) { - throw std::runtime_error("dense_mlp_int4_fused: d_ffn must be a multiple of 4 and of group_size, hidden_dim a multiple of 4"); - } - const size_t blocks_d_ffn = d_ffn / 4; - const size_t blocks_hidden = hidden_dim / 4; - - constexpr size_t tile_n_blocks = 32; - constexpr size_t tile_rows = tile_n_blocks * 4; - if (tile_rows % group_size != 0) { - throw std::runtime_error("dense_mlp_int4_fused: tile_rows must be a multiple of group_size"); - } - - const __fp16* hidden_fp16 = hidden_buf.data_as<__fp16>(); - __fp16* output = node.output_buffer.data_as<__fp16>(); - - const int8_t* gate_w = gate_buf.data_as(); - const __fp16* gate_s = gate_buf.scales_as_fp16(); - const int8_t* up_w = up_buf.data_as(); - const __fp16* up_s = up_buf.scales_as_fp16(); - const int8_t* down_w = down_buf.data_as(); - const __fp16* down_s = down_buf.scales_as_fp16(); - - thread_local std::vector dense_x_int8; - thread_local std::vector dense_x_scales; - thread_local std::vector<__fp16> dense_h_fp16; - thread_local std::vector dense_h_int8; - if (dense_x_int8.size() < M * hidden_dim) dense_x_int8.resize(M * hidden_dim); - if (dense_x_scales.size() < M) dense_x_scales.resize(M); - if (dense_h_fp16.size() < d_ffn) dense_h_fp16.resize(d_ffn); - if (dense_h_int8.size() < d_ffn) dense_h_int8.resize(d_ffn); - - auto& pool = CactusThreading::get_thread_pool(); - const size_t num_workers = pool.num_workers(); - std::vector dense_fused_scratch(num_workers); - for (auto& sc : dense_fused_scratch) { - sc.gate_tile.resize(tile_rows); - sc.up_tile.resize(tile_rows); - } - - for (size_t t = 0; t < M; ++t) { - const __fp16* x = hidden_fp16 + t * hidden_dim; - float maxabs = cactus_fp16_max_abs(x, hidden_dim); - float xs = std::max(maxabs / 127.0f, 1e-10f); - dense_x_scales[t] = xs; - cactus_fp16_to_int8(x, dense_x_int8.data() + t * hidden_dim, hidden_dim, xs); - } - - const size_t tiles_d_ffn = (blocks_d_ffn + tile_n_blocks - 1) / tile_n_blocks; - const size_t tiles_hidden = (blocks_hidden + tile_n_blocks - 1) / tile_n_blocks; - const size_t num_threads_p1 = std::min(num_workers, tiles_d_ffn); - const size_t num_threads_p2 = std::min(num_workers, tiles_hidden); - - for (size_t t = 0; t < M; ++t) { - const int8_t* x_int8 = dense_x_int8.data() + t * hidden_dim; - const float x_scale = dense_x_scales[t]; - __fp16* y = output + t * hidden_dim; - __fp16* h_full = dense_h_fp16.data(); - int8_t* h_int8 = dense_h_int8.data(); - - std::atomic p1_next{0}; - std::atomic p2_next{0}; - - auto run = [&](size_t nt, auto&& task) { - if (nt <= 1) { task(0); return; } - pool.enqueue_n_threads(nt - 1, nt - 1, [&](size_t s, size_t e) { - for (size_t w = s; w < e; ++w) task(w + 1); - }); - task(0); - pool.wait_all(); - }; - - run(num_threads_p1, [&](size_t worker_id) { - auto& sc = dense_fused_scratch[worker_id]; - __fp16* gate_tile_buf = sc.gate_tile.data(); - __fp16* up_tile_buf = sc.up_tile.data(); - while (true) { - size_t tile_id = p1_next.fetch_add(1, std::memory_order_relaxed); - if (tile_id >= tiles_d_ffn) break; - const size_t blk_start = tile_id * tile_n_blocks; - const size_t blk_end = std::min(blk_start + tile_n_blocks, blocks_d_ffn); - const size_t k_lo = blk_start * 4; - const size_t k_count = (blk_end - blk_start) * 4; - - cactus_gemv_int4_block_range( - x_int8, x_scale, gate_w, gate_s, gate_tile_buf - k_lo, - hidden_dim, d_ffn, group_size, blk_start, blk_end); - cactus_gelu_f16(gate_tile_buf, gate_tile_buf, k_count); - cactus_gemv_int4_block_range( - x_int8, x_scale, up_w, up_s, up_tile_buf - k_lo, - hidden_dim, d_ffn, group_size, blk_start, blk_end); - cactus_multiply_f16(gate_tile_buf, up_tile_buf, h_full + k_lo, k_count); - } - }); - - const float h_maxabs = cactus_fp16_max_abs(h_full, d_ffn); - const float h_scale = std::max(h_maxabs / 127.0f, 1e-10f); - fp16_to_int8_inplace(h_full, h_int8, d_ffn, h_scale); - - run(num_threads_p2, [&](size_t /*worker_id*/) { - while (true) { - size_t tile_id = p2_next.fetch_add(1, std::memory_order_relaxed); - if (tile_id >= tiles_hidden) break; - const size_t blk_start = tile_id * tile_n_blocks; - const size_t blk_end = std::min(blk_start + tile_n_blocks, blocks_hidden); - cactus_gemv_int4_block_range( - h_int8, h_scale, down_w, down_s, y, - d_ffn, hidden_dim, group_size, blk_start, blk_end); - } - }); - } -} - -void compute_rms_norm_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - const auto& input_buffer = get_input(node, 0, nodes, node_index_map); - const auto& weight_buffer = get_input(node, 1, nodes, node_index_map); - - if (input_buffer.shape.size() != 2) { - throw std::runtime_error("RMS normalization requires 2D input tensor [batch_size, dims], got " + - std::to_string(input_buffer.shape.size()) + "D tensor"); - } - - size_t batch_size = input_buffer.shape[0]; - size_t dims = input_buffer.shape[1]; - - if (input_buffer.precision != Precision::FP16) { - throw std::runtime_error("RMS normalization only supports FP16 precision"); - } - - cactus_rms_norm_f16(input_buffer.data_as<__fp16>(), weight_buffer.data_as<__fp16>(), - node.output_buffer.data_as<__fp16>(), batch_size, dims, node.params.epsilon); -} - -void compute_rope_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - if (node.params.backend == ComputeBackend::NPU) { - throw std::runtime_error("NPU RoPE operation not yet implemented"); - } - - const auto& input_buffer = get_input(node, 0, nodes, node_index_map); - const auto& shape = input_buffer.shape; - - if (shape.size() < 4) { - throw std::runtime_error("RoPE operation requires 4D tensor with shape [batch, seq_len, num_heads, head_dim], got " + - std::to_string(shape.size()) + "D tensor"); - } - - if (input_buffer.precision != Precision::FP16 || node.output_buffer.precision != Precision::FP16) { - throw std::runtime_error("RoPE operation only supports FP16 precision"); - } - - size_t batch_size = shape[0]; - size_t seq_len = shape[1]; - size_t num_heads = shape[2]; - size_t head_dim = shape[3]; - - cactus_rope_f16(input_buffer.data_as<__fp16>(), node.output_buffer.data_as<__fp16>(), - batch_size, seq_len, num_heads, head_dim, node.params.position_offset, node.params.theta); -} - -void compute_softmax_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - const auto& input_buffer = get_input(node, 0, nodes, node_index_map); - const auto& shape = input_buffer.shape; - - if (shape.size() < 2) { - throw std::runtime_error("Softmax operation requires at least 2D tensor, got " + - std::to_string(shape.size()) + "D tensor"); - } - - if (input_buffer.precision != Precision::FP16) { - throw std::runtime_error("Softmax operation only supports FP16 precision"); - } - - size_t batch_size = 1; - for (size_t i = 0; i < shape.size() - 1; i++) { - batch_size *= shape[i]; - } - size_t vocab_size = shape[shape.size() - 1]; - - cactus_softmax_f16(input_buffer.data_as<__fp16>(), node.output_buffer.data_as<__fp16>(), - batch_size, 1, vocab_size); -} - -void compute_rel_pos_bias_node(GraphNode& node, const std::vector>& nodes, - const std::unordered_map& node_index_map) { - if (node.input_ids.size() != 2) { - throw std::runtime_error("REL_POS_BIAS requires 2 inputs (query, relative_key)"); - } - - const auto& q_buffer = get_input(node, 0, nodes, node_index_map); - const auto& r_buffer = get_input(node, 1, nodes, node_index_map); - auto& y_buffer = node.output_buffer; - - if (q_buffer.shape.size() != 4) { - throw std::runtime_error("REL_POS_BIAS query must be [B, T, H, D]"); - } - if (r_buffer.shape.size() != 4) { - throw std::runtime_error("REL_POS_BIAS relative_key must be [B, R, H, D]"); - } - if (q_buffer.precision != Precision::FP16 || r_buffer.precision != Precision::FP16) { - throw std::runtime_error("REL_POS_BIAS currently only supports FP16 tensors"); - } - - const size_t B = q_buffer.shape[0]; - const size_t T = q_buffer.shape[1]; - const size_t H = q_buffer.shape[2]; - const size_t D = q_buffer.shape[3]; - const size_t Rb = r_buffer.shape[0]; - const size_t R = r_buffer.shape[1]; - - if (Rb != 1 && Rb != B) { - throw std::runtime_error("REL_POS_BIAS relative_key batch must be 1 or match query batch"); - } - if (r_buffer.shape[2] != H || r_buffer.shape[3] != D) { - throw std::runtime_error("REL_POS_BIAS expects matching [H, D] between query and relative_key"); - } - if (R < (2 * T - 1)) { - throw std::runtime_error("REL_POS_BIAS requires relative_key length >= 2*T-1"); - } - - const __fp16* q = q_buffer.data_as<__fp16>(); - const __fp16* r = r_buffer.data_as<__fp16>(); - __fp16* y = y_buffer.data_as<__fp16>(); - - const float scale = node.params.scale; - - const size_t q_batch_stride = T * H * D; - const size_t r_batch_stride = R * H * D; - const size_t y_batch_stride = H * T * T; - const size_t q_head_stride = D; - const size_t r_head_stride = D; - const size_t q_time_stride = H * D; - const size_t r_time_stride = H * D; - - CactusThreading::parallel_for(B * H * T, CactusThreading::Thresholds::ATTENTION, - [&](size_t start_idx, size_t end_idx) { - for (size_t work_idx = start_idx; work_idx < end_idx; ++work_idx) { - const size_t b = work_idx / (H * T); - const size_t rem = work_idx % (H * T); - const size_t h = rem / T; - const size_t t = rem % T; - - const size_t rb = (Rb == 1) ? 0 : b; - const __fp16* q_vec = q + b * q_batch_stride + t * q_time_stride + h * q_head_stride; - const __fp16* r_base = r + rb * r_batch_stride + h * r_head_stride; - __fp16* y_row = y + b * y_batch_stride + h * (T * T) + t * T; - - for (size_t j = 0; j < T; ++j) { - const size_t rel_idx = (T - 1) - t + j; - const __fp16* r_vec = r_base + rel_idx * r_time_stride; - - float acc = 0.0f; - for (size_t d = 0; d < D; ++d) { - acc += static_cast(q_vec[d]) * static_cast(r_vec[d]); - } - y_row[j] = static_cast<__fp16>(acc * scale); - } - } - }); -} - -void compute_attention_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - if (node.params.backend == ComputeBackend::NPU) { - throw std::runtime_error("NPU attention operation not yet implemented"); - } - - if (node.input_ids.size() < 3 || node.input_ids.size() > 4) { - throw std::runtime_error("Attention operation requires 3 or 4 inputs (query, key, value[, mask]), got " + - std::to_string(node.input_ids.size()) + " inputs"); - } - - const auto& query_buffer = get_input(node, 0, nodes, node_index_map); - const auto& key_buffer = get_input(node, 1, nodes, node_index_map); - const auto& value_buffer = get_input(node, 2, nodes, node_index_map); - const BufferDesc* mask_buffer = nullptr; - if (node.input_ids.size() == 4) { - mask_buffer = &get_input(node, 3, nodes, node_index_map); - } - const auto& q_shape = query_buffer.shape; - const auto& k_shape = key_buffer.shape; - - if (q_shape.size() < 4) { - throw std::runtime_error("Attention operation requires 4D tensors [batch, seq_len, num_heads, head_dim], got " + - std::to_string(q_shape.size()) + "D tensor"); - } - - if (query_buffer.precision != Precision::FP16) { - throw std::runtime_error("Attention operation only supports FP16 precision"); - } - - size_t batch_size = q_shape[0]; - size_t seq_len = q_shape[1]; - size_t num_q_heads = q_shape[2]; - size_t head_dim = q_shape[3]; - size_t num_kv_heads = k_shape[2]; - size_t kv_seq_len = key_buffer.shape[1]; - size_t v_head_dim = value_buffer.shape[3]; - bool mask_per_head = false; - const __fp16* mask_ptr = nullptr; - - if (mask_buffer) { - if (mask_buffer->precision != Precision::FP16) { - throw std::runtime_error("Attention mask tensor must be FP16"); - } - - if (mask_buffer->shape.size() == 3) { - if (mask_buffer->shape[0] != batch_size || - mask_buffer->shape[1] != seq_len || - mask_buffer->shape[2] != kv_seq_len) { - throw std::runtime_error("Attention mask [B, T, S] shape mismatch"); - } - mask_per_head = false; - } else if (mask_buffer->shape.size() == 4) { - if (mask_buffer->shape[0] != batch_size || - mask_buffer->shape[1] != num_q_heads || - mask_buffer->shape[2] != seq_len || - mask_buffer->shape[3] != kv_seq_len) { - throw std::runtime_error("Attention mask [B, H, T, S] shape mismatch"); - } - mask_per_head = true; - } else { - throw std::runtime_error("Attention mask must be rank 3 or 4"); - } - - mask_ptr = mask_buffer->data_as<__fp16>(); - } - - cactus_attention_f16(query_buffer.data_as<__fp16>(), key_buffer.data_as<__fp16>(), - value_buffer.data_as<__fp16>(), node.output_buffer.data_as<__fp16>(), - batch_size, seq_len, kv_seq_len, num_q_heads, num_kv_heads, head_dim, node.params.scale, mask_ptr, - node.params.position_offset, node.params.window_size, node.params.is_causal, - node.params.attention_mask_is_additive, mask_per_head, v_head_dim, node.params.logit_cap); -} - -void compute_attention_int8_hybrid_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - const auto& query_buffer = get_input(node, 0, nodes, node_index_map); - const auto& key_new_buffer = get_input(node, 1, nodes, node_index_map); - const auto& value_new_buffer = get_input(node, 2, nodes, node_index_map); - const auto& q_shape = query_buffer.shape; - - if (q_shape.size() < 4) { - throw std::runtime_error("ATTENTION_INT8_HYBRID requires 4D query tensor"); - } - - size_t batch_size = q_shape[0]; - size_t seq_len = q_shape[1]; - size_t num_q_heads = q_shape[2]; - size_t head_dim = node.params.head_dim; - size_t v_head_dim = node.params.v_head_dim; - size_t num_kv_heads = node.params.num_kv_heads; - size_t cache_len = node.params.cache_seq_len; - size_t new_len = key_new_buffer.shape[1]; - - cactus_attention_hybrid_int8_fp16( - query_buffer.data_as<__fp16>(), - node.params.cached_keys_int8, - node.params.cached_values_int8, - node.params.cached_k_scales, - node.params.cached_v_scales, - key_new_buffer.data_as<__fp16>(), - value_new_buffer.data_as<__fp16>(), - node.output_buffer.data_as<__fp16>(), - batch_size, seq_len, cache_len, new_len, - num_q_heads, num_kv_heads, head_dim, - node.params.scale, node.params.position_offset, true, - node.params.window_size, KV_QUANT_GROUP_SIZE, v_head_dim - ); -} - -void compute_layernorm_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - const auto& input_buffer = get_input(node, 0, nodes, node_index_map); - const auto& weight_buffer = get_input(node, 1, nodes, node_index_map); - bool has_bias = node.input_ids.size() > 2; - float epsilon = node.params.epsilon; - - if (input_buffer.shape.empty()) { - throw std::runtime_error("LayerNorm requires non-empty input tensor"); - } - - size_t feature_size = input_buffer.shape.back(); - size_t batch_size = input_buffer.total_size / feature_size; - - if (weight_buffer.total_size != feature_size) { - throw std::runtime_error("LayerNorm weight size mismatch with input feature dimension"); - } - - using BufferDesc = std::remove_reference_t; - const BufferDesc* bias_buffer_ptr = nullptr; - if (has_bias) { - const auto& bias_buffer = get_input(node, 2, nodes, node_index_map); - if (bias_buffer.total_size != feature_size) { - throw std::runtime_error("LayerNorm bias size mismatch with input feature dimension"); - } - bias_buffer_ptr = &bias_buffer; - } - - if (input_buffer.precision == Precision::FP16 && - weight_buffer.precision == Precision::FP16 && - node.output_buffer.precision == Precision::FP16 && - (!has_bias || bias_buffer_ptr->precision == Precision::FP16)) { - cactus_layer_norm_f16( - input_buffer.data_as<__fp16>(), - weight_buffer.data_as<__fp16>(), - has_bias ? bias_buffer_ptr->data_as<__fp16>() : nullptr, - node.output_buffer.data_as<__fp16>(), - batch_size, - feature_size, - epsilon); - return; - } - - std::vector input_float(input_buffer.total_size); - std::vector weight_float(feature_size); - std::vector bias_float(feature_size, 0.0f); - - if (input_buffer.precision == Precision::INT8) { - throw std::runtime_error("LayerNorm currently does not support INT8 input"); - } else if (input_buffer.precision == Precision::FP16) { - const __fp16* input_fp16 = input_buffer.data_as<__fp16>(); - for (size_t i = 0; i < input_buffer.total_size; ++i) { - input_float[i] = static_cast(input_fp16[i]); - } - } else { - std::memcpy(input_float.data(), input_buffer.data_as(), input_buffer.total_size * sizeof(float)); - } - - if (weight_buffer.precision == Precision::INT8) { - throw std::runtime_error("LayerNorm currently does not support INT8 weight"); - } else if (weight_buffer.precision == Precision::FP16) { - const __fp16* weight_fp16 = weight_buffer.data_as<__fp16>(); - for (size_t i = 0; i < feature_size; ++i) { - weight_float[i] = static_cast(weight_fp16[i]); - } - } else { - std::memcpy(weight_float.data(), weight_buffer.data_as(), feature_size * sizeof(float)); - } - - if (has_bias) { - const auto& bias_buffer = *bias_buffer_ptr; - if (bias_buffer.precision == Precision::INT8) { - throw std::runtime_error("LayerNorm currently does not support INT8 bias"); - } else if (bias_buffer.precision == Precision::FP16) { - const __fp16* bias_fp16 = bias_buffer.data_as<__fp16>(); - for (size_t i = 0; i < feature_size; ++i) { - bias_float[i] = static_cast(bias_fp16[i]); - } - } else { - std::memcpy(bias_float.data(), bias_buffer.data_as(), feature_size * sizeof(float)); - } - } - - std::vector output_float(input_buffer.total_size); - for (size_t b = 0; b < batch_size; ++b) { - const float* input_row = input_float.data() + b * feature_size; - float* output_row = output_float.data() + b * feature_size; - - float mean = 0.0f; - for (size_t i = 0; i < feature_size; ++i) { - mean += input_row[i]; - } - mean /= feature_size; - - float variance = 0.0f; - for (size_t i = 0; i < feature_size; ++i) { - float diff = input_row[i] - mean; - variance += diff * diff; - } - variance /= feature_size; - - float std_inv = 1.0f / std::sqrt(variance + epsilon); - for (size_t i = 0; i < feature_size; ++i) { - output_row[i] = (input_row[i] - mean) * std_inv * weight_float[i] + bias_float[i]; - } - } - - if (node.output_buffer.precision == Precision::INT8) { - throw std::runtime_error("LayerNorm currently does not support INT8 output"); - } else if (node.output_buffer.precision == Precision::FP16) { - __fp16* output_fp16 = node.output_buffer.data_as<__fp16>(); - for (size_t i = 0; i < input_buffer.total_size; ++i) { - output_fp16[i] = static_cast<__fp16>(output_float[i]); - } - } else { - std::memcpy(node.output_buffer.data_as(), output_float.data(), input_buffer.total_size * sizeof(float)); - } -} - -void compute_conv1d_causal_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - if (node.params.backend == ComputeBackend::NPU) { - throw std::runtime_error("NPU causal convolution operation not yet implemented"); - } - - const auto& X = get_input(node, 0, nodes, node_index_map); - const auto& W = get_input(node, 1, nodes, node_index_map); - auto& Y = node.output_buffer; - - if (X.shape.size() != 3) { - throw std::runtime_error("Causal conv requires 3D input [batch, seq_len, in_channels]"); - } - if (W.shape.size() != 3) { - throw std::runtime_error("Weight must be 3D"); - } - - const size_t N = X.shape[0]; - const size_t L = X.shape[1]; - const size_t C_in = X.shape[2]; - const size_t W0 = W.shape[0]; - const size_t W1 = W.shape[1]; - const size_t W2 = W.shape[2]; - const size_t dil = node.params.dilation; - if (dil < 1) throw std::runtime_error("dilation must be >= 1"); - - size_t M = 1; - size_t C_out = 0; - const bool standard_layout = (W1 == 1); - const bool transposed_layout = (W2 == 1); - if ((!standard_layout && !transposed_layout) || (W0 % C_in != 0)) { - throw std::runtime_error("Only depthwise causal convolution is supported currently"); - } - const size_t K = standard_layout ? W2 : W1; - M = W0 / C_in; - C_out = C_in * M; - - Y.shape = { N, L, C_out }; - Y.precision = X.precision; - - auto transpose_depthwise_weights_fp16 = [&](const __fp16* src) { - std::vector<__fp16> transposed(W0 * K); - for (size_t oc = 0; oc < W0; ++oc) { - for (size_t k = 0; k < K; ++k) { - transposed[oc * K + k] = src[(oc * W1 + k) * W2]; - } - } - return transposed; - }; - - if (W.precision == Precision::INT8) { - const size_t W_size = W0 * W1 * W2; - const int8_t* W_int8 = W.data_as(); - - std::vector<__fp16> W_fp16(W_size); - - if (W.is_grouped_int8()) { - const __fp16* scales = W.scales_as_fp16(); - const size_t K_total = W1 * K; - const size_t group_size = W.group_size; - const size_t num_groups = K_total / group_size; - - for (size_t row = 0; row < W0; ++row) { - for (size_t col = 0; col < K_total; ++col) { - size_t idx = row * K_total + col; - size_t group_idx = col / group_size; - float scale = static_cast(scales[row * num_groups + group_idx]); - W_fp16[idx] = static_cast<__fp16>(W_int8[idx] * scale); - } - } - } else { - for (size_t i = 0; i < W_size; ++i) { - W_fp16[i] = static_cast<__fp16>(W_int8[i]); - } - } - - if (transposed_layout && !standard_layout) { - auto fixed = transpose_depthwise_weights_fp16(W_fp16.data()); - cactus_conv1d_causal_depthwise_f16( - X.data_as<__fp16>(), fixed.data(), Y.data_as<__fp16>(), - N, L, C_in, K, dil); - } else { - cactus_conv1d_causal_depthwise_f16( - X.data_as<__fp16>(), W_fp16.data(), Y.data_as<__fp16>(), - N, L, C_in, K, dil); - } - } else if (W.precision == Precision::FP16) { - if (transposed_layout && !standard_layout) { - auto fixed = transpose_depthwise_weights_fp16(W.data_as<__fp16>()); - cactus_conv1d_causal_depthwise_f16( - X.data_as<__fp16>(), fixed.data(), Y.data_as<__fp16>(), - N, L, C_in, K, dil); - } else { - cactus_conv1d_causal_depthwise_f16( - X.data_as<__fp16>(), W.data_as<__fp16>(), Y.data_as<__fp16>(), - N, L, C_in, K, dil); - } - } else { - throw std::runtime_error("Depthwise causal conv supports INT8/FP16 weights"); - } -} - -void compute_conv1d_k3_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - if (node.params.backend == ComputeBackend::NPU) { - throw std::runtime_error("NPU causal convolution operation not yet implemented"); - } - - const auto& X = get_input(node, 0, nodes, node_index_map); - const auto& W = get_input(node, 1, nodes, node_index_map); - auto& Y = node.output_buffer; - - if (X.shape.size() != 3) - throw std::runtime_error("Conv requires 3D input [N, C_in, L]!"); - - if (W.shape.size() != 3) - throw std::runtime_error("Weight must be [C_out, C_in, 3]!"); - - const size_t N = X.shape[0]; - const size_t C_in = X.shape[1]; - const size_t L = X.shape[2]; - - const size_t C_out = W.shape[0]; - const size_t K = W.shape[2]; - const size_t stride = node.params.stride; - - if (K != 3) - throw std::runtime_error("Conv1d_k3 only supports K=3!"); - - size_t L_out = ((L - 1) / stride) + 1; - Y.shape = { N, C_out, L_out }; - Y.precision = X.precision; - - if (X.precision != Precision::FP16) { - throw std::runtime_error("Conv1d_k3 only supports FP16 activations"); - } - - if (W.precision == Precision::INT8) { - const size_t W_size = C_out * C_in * K; - const int8_t* W_int8 = W.data_as(); - - std::vector<__fp16> W_fp16(W_size); - - if (W.is_grouped_int8()) { - const __fp16* scales = W.scales_as_fp16(); - const size_t K_total = C_in * K; - const size_t group_size = W.group_size; - const size_t num_groups = K_total / group_size; - - for (size_t row = 0; row < C_out; ++row) { - for (size_t col = 0; col < K_total; ++col) { - size_t idx = row * K_total + col; - size_t group_idx = col / group_size; - float scale = static_cast(scales[row * num_groups + group_idx]); - W_fp16[idx] = static_cast<__fp16>(W_int8[idx] * scale); - } - } - } else { - for (size_t i = 0; i < W_size; ++i) { - W_fp16[i] = static_cast<__fp16>(W_int8[i]); - } - } - - cactus_conv1d_f16_k3( - X.data_as<__fp16>(), - W_fp16.data(), - Y.data_as<__fp16>(), - N, L, C_in, C_out, stride - ); - } else if (W.precision == Precision::FP16) { - cactus_conv1d_f16_k3( - X.data_as<__fp16>(), - W.data_as<__fp16>(), - Y.data_as<__fp16>(), - N, L, C_in, C_out, stride - ); - } else { - throw std::runtime_error("Conv1d_k3 only supports FP16 and INT8 weights"); - } -} - -void compute_conv1d_node(GraphNode& node, const std::vector>& nodes, - const std::unordered_map& node_index_map) { - const auto& X = get_input(node, 0, nodes, node_index_map); - const auto& W = get_input(node, 1, nodes, node_index_map); - const BufferDesc* B = nullptr; - if (node.input_ids.size() >= 3) { - B = &get_input(node, 2, nodes, node_index_map); - } - - auto& Y = node.output_buffer; - - if (X.shape.size() != 3) { - throw std::runtime_error("conv1d expects input [N, C_in, L]"); - } - if (W.shape.size() != 3) { - throw std::runtime_error("conv1d weight must be [C_out, C_in, K]"); - } - - const size_t N = X.shape[0]; - const size_t C_in = X.shape[1]; - const size_t L = X.shape[2]; - const size_t C_out = W.shape[0]; - const size_t K = W.shape[2]; - const size_t stride = node.params.stride; - - if (W.shape[1] != C_in) { - throw std::runtime_error("conv1d weight C_in mismatch"); - } - - if (X.precision != Precision::FP16 || W.precision != Precision::FP16) { - throw std::runtime_error("Conv1d only supports FP16"); - } - - const __fp16* bias_ptr = nullptr; - std::vector<__fp16> bias_fp16; - if (B) { - if (B->total_size != C_out) { - throw std::runtime_error("conv1d bias size mismatch"); - } - if (B->precision == Precision::FP16) { - bias_ptr = B->data_as<__fp16>(); - } else if (B->precision == Precision::FP32) { - bias_fp16.resize(C_out); - cactus_fp32_to_fp16(B->data_as(), bias_fp16.data(), C_out); - bias_ptr = bias_fp16.data(); - } else { - throw std::runtime_error("conv1d bias only supports FP16/FP32"); - } - } - - cactus_conv1d_f16(X.data_as<__fp16>(), W.data_as<__fp16>(), bias_ptr, - Y.data_as<__fp16>(), N, L, C_in, C_out, K, stride); -} - -void compute_conv1d_same_depthwise_k9_node(GraphNode& node, const std::vector>& nodes, - const std::unordered_map& node_index_map) { - if (node.params.backend == ComputeBackend::NPU) { - throw std::runtime_error("NPU conv1d_same_depthwise_k9 operation not yet implemented"); - } - - const auto& X = get_input(node, 0, nodes, node_index_map); - const auto& W = get_input(node, 1, nodes, node_index_map); - const BufferDesc* B = nullptr; - if (node.input_ids.size() >= 3) { - B = &get_input(node, 2, nodes, node_index_map); - } - auto& Y = node.output_buffer; - - if (X.shape.size() != 3) { - throw std::runtime_error("conv1d_same_depthwise_k9 expects input [N, L, C]"); - } - if (X.precision != Precision::FP16) { - throw std::runtime_error("conv1d_same_depthwise_k9 only supports FP16 activations"); - } - - const size_t N = X.shape[0]; - const size_t L = X.shape[1]; - const size_t C = X.shape[2]; - const size_t K = 9; - - if (W.shape.size() == 2) { - if (W.shape[0] != C || W.shape[1] != K) { - throw std::runtime_error("conv1d_same_depthwise_k9 weight must be [C, 9]"); - } - } else if (W.shape.size() == 3) { - if (W.shape[0] != C || W.shape[1] != 1 || W.shape[2] != K) { - throw std::runtime_error("conv1d_same_depthwise_k9 weight must be [C, 1, 9]"); - } - } else { - throw std::runtime_error("conv1d_same_depthwise_k9 weight must be rank 2 or 3"); - } - - Y.shape = {N, L, C}; - Y.precision = Precision::FP16; - - const __fp16* bias_ptr = nullptr; - std::vector<__fp16> bias_fp16; - if (B) { - if (B->total_size != C) { - throw std::runtime_error("conv1d_same_depthwise_k9 bias size mismatch"); - } - if (B->precision == Precision::FP16) { - bias_ptr = B->data_as<__fp16>(); - } else if (B->precision == Precision::FP32) { - bias_fp16.resize(C); - cactus_fp32_to_fp16(B->data_as(), bias_fp16.data(), C); - bias_ptr = bias_fp16.data(); - } else { - throw std::runtime_error("conv1d_same_depthwise_k9 bias only supports FP16/FP32"); - } - } - - if (W.precision == Precision::FP16) { - cactus_conv1d_same_depthwise_f16_k9( - X.data_as<__fp16>(), - W.data_as<__fp16>(), - bias_ptr, - Y.data_as<__fp16>(), - N, L, C - ); - return; - } - - if (W.precision == Precision::INT8) { - const size_t W_size = C * K; - const int8_t* W_int8 = W.data_as(); - std::vector<__fp16> W_fp16(W_size); - - if (W.is_grouped_int8()) { - const __fp16* scales = W.scales_as_fp16(); - const size_t K_total = K; - const size_t group_size = W.group_size; - if (group_size == 0 || (K_total % group_size) != 0 || scales == nullptr) { - throw std::runtime_error("Grouped INT8 conv1d_same_depthwise_k9 requires valid per-group scales"); - } - - const size_t num_groups = K_total / group_size; - for (size_t row = 0; row < C; ++row) { - for (size_t col = 0; col < K_total; ++col) { - size_t idx = row * K_total + col; - size_t group_idx = col / group_size; - float scale = static_cast(scales[row * num_groups + group_idx]); - W_fp16[idx] = static_cast<__fp16>(W_int8[idx] * scale); - } - } - } else { - for (size_t i = 0; i < W_size; ++i) { - W_fp16[i] = static_cast<__fp16>(W_int8[i]); - } - } - - cactus_conv1d_same_depthwise_f16_k9( - X.data_as<__fp16>(), - W_fp16.data(), - bias_ptr, - Y.data_as<__fp16>(), - N, L, C - ); - return; - } - - throw std::runtime_error("conv1d_same_depthwise_k9 only supports FP16/INT8 weights"); -} - -void compute_conv2d_k3s2p1_node(GraphNode& node, const std::vector>& nodes, - const std::unordered_map& node_index_map) { - if (node.params.backend == ComputeBackend::NPU) { - throw std::runtime_error("NPU conv2d_k3s2p1 operation not yet implemented"); - } - - const auto& X = get_input(node, 0, nodes, node_index_map); - const auto& W = get_input(node, 1, nodes, node_index_map); - const BufferDesc* B = nullptr; - if (node.input_ids.size() >= 3) { - B = &get_input(node, 2, nodes, node_index_map); - } - auto& Y = node.output_buffer; - - if (X.shape.size() != 4) { - throw std::runtime_error("conv2d_k3s2p1 expects input [N, C_in, H, W]"); - } - if (X.precision != Precision::FP16) { - throw std::runtime_error("conv2d_k3s2p1 only supports FP16 activations"); - } - - const size_t N = X.shape[0]; - const size_t C_in = X.shape[1]; - const size_t H = X.shape[2]; - const size_t W_in = X.shape[3]; - const size_t C_out = Y.shape[1]; - - if (H == 0 || W_in == 0) { - throw std::runtime_error("conv2d_k3s2p1 input spatial dimensions must be > 0"); - } - - const __fp16* bias_ptr = nullptr; - std::vector<__fp16> bias_fp16; - if (B) { - if (B->precision == Precision::FP16) { - bias_ptr = B->data_as<__fp16>(); - } else if (B->precision == Precision::FP32) { - bias_fp16.resize(C_out); - cactus_fp32_to_fp16(B->data_as(), bias_fp16.data(), C_out); - bias_ptr = bias_fp16.data(); - } else { - throw std::runtime_error("conv2d_k3s2p1 bias only supports FP16/FP32"); - } - } - - if (W.precision == Precision::FP16) { - if (W.shape.size() != 4) { - throw std::runtime_error("conv2d_k3s2p1 FP16 weight must be [C_out, C_in, 3, 3]"); - } - cactus_conv2d_f16_k3s2p1_nchw( - X.data_as<__fp16>(), - W.data_as<__fp16>(), - bias_ptr, - Y.data_as<__fp16>(), - N, C_in, H, W_in, C_out - ); - return; - } - - if (W.precision == Precision::INT8) { - const size_t K_total = C_in * 9; - const size_t W_size = C_out * K_total; - const int8_t* W_int8 = W.data_as(); - std::vector<__fp16> W_fp16(W_size); - - if (W.is_grouped_int8()) { - const __fp16* scales = W.scales_as_fp16(); - const size_t group_size = W.group_size; - if (group_size == 0 || (K_total % group_size) != 0 || scales == nullptr) { - throw std::runtime_error("Grouped INT8 conv2d_k3s2p1 requires valid per-group scales"); - } - - const size_t num_groups = K_total / group_size; - for (size_t row = 0; row < C_out; ++row) { - for (size_t col = 0; col < K_total; ++col) { - size_t idx = row * K_total + col; - size_t group_idx = col / group_size; - float scale = static_cast(scales[row * num_groups + group_idx]); - W_fp16[idx] = static_cast<__fp16>(W_int8[idx] * scale); - } - } - } else { - for (size_t i = 0; i < W_size; ++i) { - W_fp16[i] = static_cast<__fp16>(W_int8[i]); - } - } - - cactus_conv2d_f16_k3s2p1_nchw( - X.data_as<__fp16>(), - W_fp16.data(), - bias_ptr, - Y.data_as<__fp16>(), - N, C_in, H, W_in, C_out - ); - return; - } - - throw std::runtime_error("conv2d_k3s2p1 only supports FP16/INT8 weights"); -} - -void compute_conv2d_depthwise_k3s2p1_node(GraphNode& node, const std::vector>& nodes, - const std::unordered_map& node_index_map) { - if (node.params.backend == ComputeBackend::NPU) { - throw std::runtime_error("NPU conv2d_depthwise_k3s2p1 operation not yet implemented"); - } - - const auto& X = get_input(node, 0, nodes, node_index_map); - const auto& W = get_input(node, 1, nodes, node_index_map); - const BufferDesc* B = nullptr; - if (node.input_ids.size() >= 3) { - B = &get_input(node, 2, nodes, node_index_map); - } - auto& Y = node.output_buffer; - - if (X.shape.size() != 4) { - throw std::runtime_error("conv2d_depthwise_k3s2p1 expects input [N, C, H, W]"); - } - if (X.precision != Precision::FP16) { - throw std::runtime_error("conv2d_depthwise_k3s2p1 only supports FP16 activations"); - } - - const size_t N = X.shape[0]; - const size_t C = X.shape[1]; - const size_t H = X.shape[2]; - const size_t W_in = X.shape[3]; - if (H == 0 || W_in == 0) { - throw std::runtime_error("conv2d_depthwise_k3s2p1 input spatial dimensions must be > 0"); - } - - if (W.shape.size() == 3) { - if (W.shape[0] != C || W.shape[1] != 3 || W.shape[2] != 3) { - throw std::runtime_error("conv2d_depthwise_k3s2p1 weight must be [C, 3, 3]"); - } - } else if (W.shape.size() == 4) { - if (W.shape[0] != C || W.shape[1] != 1 || W.shape[2] != 3 || W.shape[3] != 3) { - throw std::runtime_error("conv2d_depthwise_k3s2p1 weight must be [C, 1, 3, 3]"); - } - } else { - throw std::runtime_error("conv2d_depthwise_k3s2p1 weight must be rank 3 or 4"); - } - - const size_t H_out = (H - 1) / 2 + 1; - const size_t W_out = (W_in - 1) / 2 + 1; - Y.shape = {N, C, H_out, W_out}; - Y.precision = Precision::FP16; - - const __fp16* bias_ptr = nullptr; - std::vector<__fp16> bias_fp16; - if (B) { - if (B->total_size != C) { - throw std::runtime_error("conv2d_depthwise_k3s2p1 bias size mismatch"); - } - if (B->precision == Precision::FP16) { - bias_ptr = B->data_as<__fp16>(); - } else if (B->precision == Precision::FP32) { - bias_fp16.resize(C); - cactus_fp32_to_fp16(B->data_as(), bias_fp16.data(), C); - bias_ptr = bias_fp16.data(); - } else { - throw std::runtime_error("conv2d_depthwise_k3s2p1 bias only supports FP16/FP32"); - } - } - - if (W.precision == Precision::FP16) { - cactus_conv2d_depthwise_f16_k3s2p1_nchw( - X.data_as<__fp16>(), - W.data_as<__fp16>(), - bias_ptr, - Y.data_as<__fp16>(), - N, C, H, W_in - ); - return; - } - - if (W.precision == Precision::INT8) { - const size_t K_total = 9; - const size_t W_size = C * K_total; - const int8_t* W_int8 = W.data_as(); - std::vector<__fp16> W_fp16(W_size); - - if (W.is_grouped_int8()) { - const __fp16* scales = W.scales_as_fp16(); - const size_t group_size = W.group_size; - if (group_size == 0 || (K_total % group_size) != 0 || scales == nullptr) { - throw std::runtime_error("Grouped INT8 conv2d_depthwise_k3s2p1 requires valid per-group scales"); - } - - const size_t num_groups = K_total / group_size; - for (size_t row = 0; row < C; ++row) { - for (size_t col = 0; col < K_total; ++col) { - size_t idx = row * K_total + col; - size_t group_idx = col / group_size; - float scale = static_cast(scales[row * num_groups + group_idx]); - W_fp16[idx] = static_cast<__fp16>(W_int8[idx] * scale); - } - } - } else { - for (size_t i = 0; i < W_size; ++i) { - W_fp16[i] = static_cast<__fp16>(W_int8[i]); - } - } - - cactus_conv2d_depthwise_f16_k3s2p1_nchw( - X.data_as<__fp16>(), - W_fp16.data(), - bias_ptr, - Y.data_as<__fp16>(), - N, C, H, W_in - ); - return; - } - - throw std::runtime_error("conv2d_depthwise_k3s2p1 only supports FP16/INT8 weights"); -} - -void compute_conv2d_pointwise_1x1_node(GraphNode& node, const std::vector>& nodes, - const std::unordered_map& node_index_map) { - if (node.params.backend == ComputeBackend::NPU) { - throw std::runtime_error("NPU conv2d_pointwise_1x1 operation not yet implemented"); - } - - const auto& X = get_input(node, 0, nodes, node_index_map); - const auto& W = get_input(node, 1, nodes, node_index_map); - const BufferDesc* B = nullptr; - if (node.input_ids.size() >= 3) { - B = &get_input(node, 2, nodes, node_index_map); - } - auto& Y = node.output_buffer; - - if (X.shape.size() != 4) { - throw std::runtime_error("conv2d_pointwise_1x1 expects input [N, C_in, H, W]"); - } - if (X.precision != Precision::FP16) { - throw std::runtime_error("conv2d_pointwise_1x1 only supports FP16 activations"); - } - - const size_t N = X.shape[0]; - const size_t C_in = X.shape[1]; - const size_t H = X.shape[2]; - const size_t W_in = X.shape[3]; - if (H == 0 || W_in == 0) { - throw std::runtime_error("conv2d_pointwise_1x1 input spatial dimensions must be > 0"); - } - - size_t C_out = 0; - if (W.shape.size() == 2) { - C_out = W.shape[0]; - if (W.shape[1] != C_in) { - throw std::runtime_error("conv2d_pointwise_1x1 weight must be [C_out, C_in]"); - } - } else if (W.shape.size() == 4) { - C_out = W.shape[0]; - if (W.shape[1] != C_in || W.shape[2] != 1 || W.shape[3] != 1) { - throw std::runtime_error("conv2d_pointwise_1x1 weight must be [C_out, C_in, 1, 1]"); - } - } else { - throw std::runtime_error("conv2d_pointwise_1x1 weight must be rank 2 or 4"); - } - - Y.shape = {N, C_out, H, W_in}; - Y.precision = Precision::FP16; - - const __fp16* bias_ptr = nullptr; - std::vector<__fp16> bias_fp16; - if (B) { - if (B->total_size != C_out) { - throw std::runtime_error("conv2d_pointwise_1x1 bias size mismatch"); - } - if (B->precision == Precision::FP16) { - bias_ptr = B->data_as<__fp16>(); - } else if (B->precision == Precision::FP32) { - bias_fp16.resize(C_out); - cactus_fp32_to_fp16(B->data_as(), bias_fp16.data(), C_out); - bias_ptr = bias_fp16.data(); - } else { - throw std::runtime_error("conv2d_pointwise_1x1 bias only supports FP16/FP32"); - } - } - - if (W.precision == Precision::FP16) { - cactus_conv2d_pointwise_f16_1x1_nchw_gemm( - X.data_as<__fp16>(), - W.data_as<__fp16>(), - bias_ptr, - Y.data_as<__fp16>(), - N, C_in, H, W_in, C_out - ); - return; - } - - if (W.precision == Precision::INT8) { - const size_t K_total = C_in; - const size_t W_size = C_out * K_total; - const int8_t* W_int8 = W.data_as(); - std::vector<__fp16> W_fp16(W_size); - - if (W.is_grouped_int8()) { - const __fp16* scales = W.scales_as_fp16(); - const size_t group_size = W.group_size; - if (group_size == 0 || (K_total % group_size) != 0 || scales == nullptr) { - throw std::runtime_error("Grouped INT8 conv2d_pointwise_1x1 requires valid per-group scales"); - } - - const size_t num_groups = K_total / group_size; - for (size_t row = 0; row < C_out; ++row) { - for (size_t col = 0; col < K_total; ++col) { - size_t idx = row * K_total + col; - size_t group_idx = col / group_size; - float scale = static_cast(scales[row * num_groups + group_idx]); - W_fp16[idx] = static_cast<__fp16>(W_int8[idx] * scale); - } - } - } else { - for (size_t i = 0; i < W_size; ++i) { - W_fp16[i] = static_cast<__fp16>(W_int8[i]); - } - } - - cactus_conv2d_pointwise_f16_1x1_nchw_gemm( - X.data_as<__fp16>(), - W_fp16.data(), - bias_ptr, - Y.data_as<__fp16>(), - N, C_in, H, W_in, C_out - ); - return; - } - - throw std::runtime_error("conv2d_pointwise_1x1 only supports FP16/INT8 weights"); -} - -void compute_conv1d_pointwise_node(GraphNode& node, const std::vector>& nodes, - const std::unordered_map& node_index_map) { - if (node.params.backend == ComputeBackend::NPU) { - throw std::runtime_error("NPU conv1d_pointwise operation not yet implemented"); - } - - const auto& X = get_input(node, 0, nodes, node_index_map); - const auto& W = get_input(node, 1, nodes, node_index_map); - const BufferDesc* B = nullptr; - if (node.input_ids.size() >= 3) { - B = &get_input(node, 2, nodes, node_index_map); - } - auto& Y = node.output_buffer; - - if (X.shape.size() != 3) { - throw std::runtime_error("conv1d_pointwise expects input [N, L, C_in]"); - } - if (X.precision != Precision::FP16) { - throw std::runtime_error("conv1d_pointwise only supports FP16 activations"); - } - - const size_t N = X.shape[0]; - const size_t L = X.shape[1]; - const size_t C_in = X.shape[2]; - - size_t C_out = 0; - if (W.shape.size() == 2) { - C_out = W.shape[0]; - if (W.shape[1] != C_in) { - throw std::runtime_error("conv1d_pointwise weight must be [C_out, C_in]"); - } - } else if (W.shape.size() == 3) { - C_out = W.shape[0]; - if (W.shape[1] != C_in || W.shape[2] != 1) { - throw std::runtime_error("conv1d_pointwise weight must be [C_out, C_in, 1]"); - } - } else { - throw std::runtime_error("conv1d_pointwise weight must be rank 2 or 3"); - } - - Y.shape = {N, L, C_out}; - Y.precision = Precision::FP16; - - const __fp16* bias_ptr = nullptr; - std::vector<__fp16> bias_fp16; - if (B) { - if (B->total_size != C_out) { - throw std::runtime_error("conv1d_pointwise bias size mismatch"); - } - if (B->precision == Precision::FP16) { - bias_ptr = B->data_as<__fp16>(); - } else if (B->precision == Precision::FP32) { - bias_fp16.resize(C_out); - cactus_fp32_to_fp16(B->data_as(), bias_fp16.data(), C_out); - bias_ptr = bias_fp16.data(); - } else { - throw std::runtime_error("conv1d_pointwise bias only supports FP16/FP32"); - } - } - - if (W.precision == Precision::FP16) { - cactus_conv1d_pointwise_f16_gemm( - X.data_as<__fp16>(), - W.data_as<__fp16>(), - bias_ptr, - Y.data_as<__fp16>(), - N, L, C_in, C_out - ); - return; - } - - if (W.precision == Precision::INT8) { - const size_t K_total = C_in; - const size_t W_size = C_out * K_total; - const int8_t* W_int8 = W.data_as(); - std::vector<__fp16> W_fp16(W_size); - - if (W.is_grouped_int8()) { - const __fp16* scales = W.scales_as_fp16(); - const size_t group_size = W.group_size; - if (group_size == 0 || (K_total % group_size) != 0 || scales == nullptr) { - throw std::runtime_error("Grouped INT8 conv1d_pointwise requires valid per-group scales"); - } - - const size_t num_groups = K_total / group_size; - for (size_t row = 0; row < C_out; ++row) { - for (size_t col = 0; col < K_total; ++col) { - size_t idx = row * K_total + col; - size_t group_idx = col / group_size; - float scale = static_cast(scales[row * num_groups + group_idx]); - W_fp16[idx] = static_cast<__fp16>(W_int8[idx] * scale); - } - } - } else { - for (size_t i = 0; i < W_size; ++i) { - W_fp16[i] = static_cast<__fp16>(W_int8[i]); - } - } - - cactus_conv1d_pointwise_f16_gemm( - X.data_as<__fp16>(), - W_fp16.data(), - bias_ptr, - Y.data_as<__fp16>(), - N, L, C_in, C_out - ); - return; - } - - throw std::runtime_error("conv1d_pointwise only supports FP16/INT8 weights"); -} - -void compute_glu_node(GraphNode& node, const std::vector>& nodes, - const std::unordered_map& node_index_map) { - const auto& X = get_input(node, 0, nodes, node_index_map); - auto& Y = node.output_buffer; - - if (X.shape.empty()) { - throw std::runtime_error("GLU expects non-scalar input"); - } - - int axis = node.params.axis; - if (axis < 0) axis += static_cast(X.shape.size()); - if (axis < 0 || static_cast(axis) >= X.shape.size()) { - throw std::runtime_error("GLU axis out of range"); - } - - const size_t axis_size = X.shape[static_cast(axis)]; - if ((axis_size % 2) != 0) { - throw std::runtime_error("GLU split dimension must be even"); - } - const size_t split = axis_size / 2; - - size_t outer = 1; - for (int i = 0; i < axis; ++i) { - outer *= X.shape[static_cast(i)]; - } - size_t inner = 1; - for (size_t i = static_cast(axis) + 1; i < X.shape.size(); ++i) { - inner *= X.shape[i]; - } - - std::vector out_shape = X.shape; - out_shape[static_cast(axis)] = split; - Y.shape = out_shape; - Y.precision = X.precision; - - if (X.precision == Precision::FP16) { - cactus_glu_f16(X.data_as<__fp16>(), Y.data_as<__fp16>(), outer, split, inner); - return; - } - - if (X.precision == Precision::FP32) { - cactus_glu_f32(X.data_as(), Y.data_as(), outer, split, inner); - return; - } - - throw std::runtime_error("GLU only supports FP16/FP32"); -} - -void compute_batchnorm_node(GraphNode& node, const std::vector>& nodes, - const std::unordered_map& node_index_map) { - if (node.input_ids.size() != 5) { - throw std::runtime_error("BatchNorm expects 5 inputs: input, weight, bias, running_mean, running_var"); - } - - const auto& X = get_input(node, 0, nodes, node_index_map); - const auto& W = get_input(node, 1, nodes, node_index_map); - const auto& B = get_input(node, 2, nodes, node_index_map); - const auto& RM = get_input(node, 3, nodes, node_index_map); - const auto& RV = get_input(node, 4, nodes, node_index_map); - auto& Y = node.output_buffer; - - if (X.shape.empty()) { - throw std::runtime_error("BatchNorm expects non-scalar input"); - } - - int axis = node.params.axis; - if (axis < 0) axis += static_cast(X.shape.size()); - if (axis < 0 || static_cast(axis) >= X.shape.size()) { - throw std::runtime_error("BatchNorm axis out of range"); - } - - const size_t C = X.shape[static_cast(axis)]; - if (W.total_size != C || B.total_size != C || RM.total_size != C || RV.total_size != C) { - throw std::runtime_error("BatchNorm parameter size mismatch"); - } - - auto load_1d_float = [C](const BufferDesc& buf, const char* name) -> std::vector { - if (buf.total_size != C) { - throw std::runtime_error(std::string("BatchNorm parameter size mismatch for ") + name); - } - std::vector out(C); - if (buf.precision == Precision::FP16) { - const __fp16* p = buf.data_as<__fp16>(); - for (size_t i = 0; i < C; ++i) out[i] = static_cast(p[i]); - } else if (buf.precision == Precision::FP32) { - std::memcpy(out.data(), buf.data_as(), C * sizeof(float)); - } else { - throw std::runtime_error(std::string("BatchNorm parameter ") + name + " must be FP16 or FP32"); - } - return out; - }; - - const std::vector gamma = load_1d_float(W, "weight"); - const std::vector beta = load_1d_float(B, "bias"); - const std::vector mean = load_1d_float(RM, "running_mean"); - const std::vector var = load_1d_float(RV, "running_var"); - - size_t outer = 1; - for (int i = 0; i < axis; ++i) { - outer *= X.shape[static_cast(i)]; - } - size_t inner = 1; - for (size_t i = static_cast(axis) + 1; i < X.shape.size(); ++i) { - inner *= X.shape[i]; - } - - Y.shape = X.shape; - Y.precision = X.precision; - - if (X.precision == Precision::FP16) { - cactus_batchnorm_f16( - X.data_as<__fp16>(), - gamma.data(), - beta.data(), - mean.data(), - var.data(), - Y.data_as<__fp16>(), - outer, - C, - inner, - node.params.epsilon - ); - return; - } - - if (X.precision == Precision::FP32) { - cactus_batchnorm_f32( - X.data_as(), - gamma.data(), - beta.data(), - mean.data(), - var.data(), - Y.data_as(), - outer, - C, - inner, - node.params.epsilon - ); - return; - } - - throw std::runtime_error("BatchNorm only supports FP16/FP32 activations"); -} - -void compute_stft_node(GraphNode& node, const std::vector>& nodes, - const std::unordered_map& node_index_map) { - const auto& X = get_input(node, 0, nodes, node_index_map); - const auto& W = get_input(node, 1, nodes, node_index_map); - auto& Y = node.output_buffer; - - const size_t N = X.shape[0]; - const size_t C_in = X.shape[1]; - const size_t L = X.shape[2]; - const size_t C_out = W.shape[0]; - const size_t K = W.shape[2]; - const size_t stride = node.params.stride; - const size_t num_fft_bins = node.params.num_fft_bins; - - if (X.precision != Precision::FP16 || W.precision != Precision::FP16) { - throw std::runtime_error("stft only supports FP16"); - } - - cactus_stft_f16(X.data_as<__fp16>(), W.data_as<__fp16>(), - Y.data_as<__fp16>(), N, L, C_in, C_out, K, stride, num_fft_bins); -} - -void compute_conv1d_k7s3_node(GraphNode& node, const std::vector>& nodes, - const std::unordered_map& node_index_map) { - const auto& X = get_input(node, 0, nodes, node_index_map); - const auto& W = get_input(node, 1, nodes, node_index_map); - const BufferDesc* B = nullptr; - if (node.input_ids.size() >= 3) { - B = &get_input(node, 2, nodes, node_index_map); - } - - auto& Y = node.output_buffer; - - const size_t N = X.shape[0]; - const size_t C_in = X.shape[1]; - const size_t L = X.shape[2]; - - if (W.shape.size() != 3) throw std::runtime_error("Weight must be 3D"); - const size_t C_in_W = W.shape[0]; - const size_t K = W.shape[1]; - const size_t C_out = W.shape[2]; - const size_t stride = node.params.stride; - - if (C_in != C_in_W) throw std::runtime_error("Channel mismatch in conv1d_k7s3"); - if (K != 7 || stride != 3) throw std::runtime_error("conv1d_k7s3 requires K=7, stride=3"); - - if (X.precision != Precision::FP16 || W.precision != Precision::FP16) { - throw std::runtime_error("Conv1d specialized only supports FP16"); - } - - size_t L_out = (L < 7) ? 0 : (L - 7) / 3 + 1; - Y.shape = {N, C_out, L_out}; - Y.precision = Precision::FP16; - - const __fp16* bias_ptr = nullptr; - std::vector<__fp16> bias_fp16; - if (B) { - if (B->total_size != C_out) { - throw std::runtime_error("conv1d_k7s3 bias size mismatch"); - } - if (B->precision == Precision::FP16) { - bias_ptr = B->data_as<__fp16>(); - } else if (B->precision == Precision::FP32) { - bias_fp16.resize(C_out); - cactus_fp32_to_fp16(B->data_as(), bias_fp16.data(), C_out); - bias_ptr = bias_fp16.data(); - } else { - throw std::runtime_error("conv1d_k7s3 bias only supports FP16/FP32"); - } - } - - cactus_conv1d_f16_k7s3_oc8( - X.data_as<__fp16>(), - W.data_as<__fp16>(), - bias_ptr, - Y.data_as<__fp16>(), - N, L, C_in, C_out - ); -} - -void compute_gated_deltanet_decode_node(GraphNode& node, const std::vector>& nodes, - const std::unordered_map& node_index_map) { - if (node.input_ids.size() != 6) { - throw std::runtime_error("GATED_DELTANET_DECODE expects 6 inputs"); - } - - const auto& q = get_input(node, 0, nodes, node_index_map); - const auto& k = get_input(node, 1, nodes, node_index_map); - const auto& v = get_input(node, 2, nodes, node_index_map); - const auto& g = get_input(node, 3, nodes, node_index_map); - const auto& b = get_input(node, 4, nodes, node_index_map); - const auto& s = get_input(node, 5, nodes, node_index_map); - - validate_gated_deltanet_inputs(q, k, v, g, b, s); - if (q.shape[1] != 1) { - throw std::runtime_error("GATED_DELTANET_DECODE expects T=1"); - } - - const size_t B = q.shape[0]; - const size_t Hq = q.shape[2]; - const size_t K = q.shape[3]; - const size_t Hv = v.shape[2]; - const size_t V = v.shape[3]; - const size_t qk_heads_from_params = node.params.num_kv_heads; - if (qk_heads_from_params != 0 && qk_heads_from_params != Hq) { - throw std::runtime_error("GATED_DELTANET_DECODE num_qk_heads param mismatch"); - } - - std::vector<__fp16> q_cast; - std::vector<__fp16> k_cast; - std::vector<__fp16> v_cast; - std::vector<__fp16> g_cast; - std::vector<__fp16> b_cast; - std::vector<__fp16> s_cast; - const __fp16* q_data = as_fp16_ptr(q, q_cast); - const __fp16* k_data = as_fp16_ptr(k, k_cast); - const __fp16* v_data = as_fp16_ptr(v, v_cast); - const __fp16* g_data = as_fp16_ptr(g, g_cast); - const __fp16* b_data = as_fp16_ptr(b, b_cast); - const __fp16* s_data = as_fp16_ptr(s, s_cast); - __fp16* out = node.output_buffer.data_as<__fp16>(); - - cactus_gated_deltanet_decode_f16( - q_data, k_data, v_data, g_data, b_data, s_data, out, - B, Hq, Hv, K, V, node.params.scale); -} - -void compute_gated_deltanet_prefill_node(GraphNode& node, const std::vector>& nodes, - const std::unordered_map& node_index_map) { - if (node.input_ids.size() != 6) { - throw std::runtime_error("GATED_DELTANET_PREFILL expects 6 inputs"); - } - - const auto& q = get_input(node, 0, nodes, node_index_map); - const auto& k = get_input(node, 1, nodes, node_index_map); - const auto& v = get_input(node, 2, nodes, node_index_map); - const auto& g = get_input(node, 3, nodes, node_index_map); - const auto& b = get_input(node, 4, nodes, node_index_map); - const auto& s = get_input(node, 5, nodes, node_index_map); - - validate_gated_deltanet_inputs(q, k, v, g, b, s); - - const size_t B = q.shape[0]; - const size_t T = q.shape[1]; - const size_t Hq = q.shape[2]; - const size_t K = q.shape[3]; - const size_t Hv = v.shape[2]; - const size_t V = v.shape[3]; - const size_t qk_heads_from_params = node.params.num_kv_heads; - if (qk_heads_from_params != 0 && qk_heads_from_params != Hq) { - throw std::runtime_error("GATED_DELTANET_PREFILL num_qk_heads param mismatch"); - } - - std::vector<__fp16> q_cast; - std::vector<__fp16> k_cast; - std::vector<__fp16> v_cast; - std::vector<__fp16> g_cast; - std::vector<__fp16> b_cast; - std::vector<__fp16> s_cast; - const __fp16* q_data = as_fp16_ptr(q, q_cast); - const __fp16* k_data = as_fp16_ptr(k, k_cast); - const __fp16* v_data = as_fp16_ptr(v, v_cast); - const __fp16* g_data = as_fp16_ptr(g, g_cast); - const __fp16* b_data = as_fp16_ptr(b, b_cast); - const __fp16* s_data = as_fp16_ptr(s, s_cast); - __fp16* out = node.output_buffer.data_as<__fp16>(); - - cactus_gated_deltanet_prefill_f16( - q_data, k_data, v_data, g_data, b_data, s_data, out, - B, T, Hq, Hv, K, V, node.params.chunk_size, node.params.scale); -} - -void compute_rope_gptj_node(GraphNode& node, const std::vector>& nodes, - const std::unordered_map& node_index_map) { - const auto& input_buffer = get_input(node, 0, nodes, node_index_map); - const auto& shape = input_buffer.shape; - - size_t batch_size = shape[0]; - size_t seq_len = shape[1]; - size_t num_heads = shape[2]; - size_t head_dim = shape[3]; - size_t rot_dim = static_cast(node.params.scalar); - - cactus_gpt_j_rope_f16(input_buffer.data_as<__fp16>(), node.output_buffer.data_as<__fp16>(), - batch_size, seq_len, num_heads, head_dim, rot_dim, - node.params.position_offset, node.params.theta); -} - -void compute_groupnorm_node(GraphNode& node, const std::vector>& nodes, - const std::unordered_map& node_index_map) { - const auto& input = get_input(node, 0, nodes, node_index_map); - const auto& weight = get_input(node, 1, nodes, node_index_map); - const auto& bias = get_input(node, 2, nodes, node_index_map); - float epsilon = node.params.epsilon; - - size_t batch_size = input.shape[0]; - size_t channels = input.shape[1]; - size_t spatial_size = 1; - for (size_t i = 2; i < input.shape.size(); ++i) spatial_size *= input.shape[i]; - - size_t num_groups = node.params.num_groups; - if (num_groups == 0) num_groups = 32; - - if (channels % num_groups != 0) { - throw std::runtime_error("GroupNorm: channels must be divisible by num_groups"); - } - - size_t channels_per_group = channels / num_groups; - - const __fp16* src = input.data_as<__fp16>(); - const __fp16* w = weight.data_as<__fp16>(); - const __fp16* b = bias.data_as<__fp16>(); - __fp16* dst = node.output_buffer.data_as<__fp16>(); - - for (size_t n = 0; n < batch_size; ++n) { - for (size_t g = 0; g < num_groups; ++g) { - float sum = 0.0f, sum_sq = 0.0f; - size_t count = 0; - - for (size_t c = 0; c < channels_per_group; ++c) { - size_t ch = g * channels_per_group + c; - for (size_t s = 0; s < spatial_size; ++s) { - size_t idx = n * channels * spatial_size + ch * spatial_size + s; - float val = static_cast(src[idx]); - sum += val; - sum_sq += val * val; - count++; - } - } - - float mean = sum / count; - float var = (sum_sq / count) - (mean * mean); - float inv_std = 1.0f / std::sqrt(var + epsilon); - - for (size_t c = 0; c < channels_per_group; ++c) { - size_t ch = g * channels_per_group + c; - float wt = static_cast(w[ch]); - float bi = static_cast(b[ch]); - - for (size_t s = 0; s < spatial_size; ++s) { - size_t idx = n * channels * spatial_size + ch * spatial_size + s; - float val = static_cast(src[idx]); - dst[idx] = static_cast<__fp16>((val - mean) * inv_std * wt + bi); - } - } - } - } -} - -void compute_lstm_cell_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - const auto& input_buffer = get_input(node, 0, nodes, node_index_map); - const auto& h_prev_buffer = get_input(node, 1, nodes, node_index_map); - const auto& c_prev_buffer = get_input(node, 2, nodes, node_index_map); - const auto& weight_ih_buffer = get_input(node, 3, nodes, node_index_map); - const auto& weight_hh_buffer = get_input(node, 4, nodes, node_index_map); - const auto& bias_ih_buffer = get_input(node, 5, nodes, node_index_map); - const auto& bias_hh_buffer = get_input(node, 6, nodes, node_index_map); - - if (input_buffer.precision != Precision::FP16 || h_prev_buffer.precision != Precision::FP16 || - c_prev_buffer.precision != Precision::FP16 || weight_ih_buffer.precision != Precision::FP16 || - weight_hh_buffer.precision != Precision::FP16 || bias_ih_buffer.precision != Precision::FP16 || - bias_hh_buffer.precision != Precision::FP16) { - throw std::runtime_error("LSTM cell requires all inputs to be FP16"); - } - - if (input_buffer.shape.size() != 2 || h_prev_buffer.shape.size() != 2 || c_prev_buffer.shape.size() != 2) { - throw std::runtime_error("LSTM cell input/state shapes must be 2D [batch, features]"); - } - - const size_t batch_size = input_buffer.shape[0]; - const size_t input_size = input_buffer.shape[1]; - const size_t hidden_size = h_prev_buffer.shape[1]; - - const __fp16* x_input = input_buffer.data_as<__fp16>(); - const __fp16* h_prev = h_prev_buffer.data_as<__fp16>(); - const __fp16* c_prev = c_prev_buffer.data_as<__fp16>(); - const __fp16* weight_ih = weight_ih_buffer.data_as<__fp16>(); - const __fp16* weight_hh = weight_hh_buffer.data_as<__fp16>(); - const __fp16* bias_ih = bias_ih_buffer.data_as<__fp16>(); - const __fp16* bias_hh = bias_hh_buffer.data_as<__fp16>(); - - node.output_buffer.shape = {batch_size, hidden_size, 2}; - node.output_buffer.total_size = batch_size * hidden_size * 2; - node.output_buffer.precision = Precision::FP16; - node.output_buffer.allocate(); - - std::vector<__fp16> h_new_temp(batch_size * hidden_size); - std::vector<__fp16> c_new_temp(batch_size * hidden_size); - - cactus_lstm_cell_f16( - x_input, h_prev, c_prev, - weight_ih, weight_hh, - bias_ih, bias_hh, - h_new_temp.data(), c_new_temp.data(), - batch_size, input_size, hidden_size - ); - - __fp16* output = node.output_buffer.data_as<__fp16>(); - for (size_t b = 0; b < batch_size; ++b) { - for (size_t i = 0; i < hidden_size; ++i) { - const size_t idx = b * hidden_size + i; - output[b * hidden_size * 2 + i * 2] = h_new_temp[idx]; - output[b * hidden_size * 2 + i * 2 + 1] = c_new_temp[idx]; - } - } -} - -void compute_altup_predict_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - size_t n = node.params.num_altup_inputs; - const auto& coefs_buf = get_input(node, 0, nodes, node_index_map); - - std::vector stream_ptrs(n); - for (size_t i = 0; i < n; i++) { - stream_ptrs[i] = get_input(node, 1 + i, nodes, node_index_map).data_as<__fp16>(); - } - - const auto& stream0_buf = get_input(node, 1, nodes, node_index_map); - size_t seq_len = stream0_buf.shape[0]; - size_t hidden_dim = stream0_buf.shape[1]; - - cactus_altup_predict_f16( - coefs_buf.data_as<__fp16>(), - stream_ptrs.data(), - node.output_buffer.data_as<__fp16>(), - n, seq_len, hidden_dim); -} - -void compute_gaussian_topk_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - const auto& input_buf = get_input(node, 0, nodes, node_index_map); - const __fp16* input = input_buf.data_as<__fp16>(); - __fp16* output = node.output_buffer.data_as<__fp16>(); - - size_t rows = input_buf.shape[0]; - size_t cols = input_buf.shape[1]; - float ppf = node.params.scalar; - - cactus_gaussian_topk_f16(input, output, rows, cols, ppf); -} - -void compute_altup_correct_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - size_t n = node.params.num_altup_inputs; - const auto& coefs_buf = get_input(node, 0, nodes, node_index_map); - const auto& innov_buf = get_input(node, 1, nodes, node_index_map); - - std::vector pred_ptrs(n); - for (size_t i = 0; i < n; i++) { - pred_ptrs[i] = get_input(node, 2 + i, nodes, node_index_map).data_as<__fp16>(); - } - - size_t seq_len = innov_buf.shape[0]; - size_t hidden_dim = innov_buf.shape[1]; - - cactus_altup_correct_f16( - coefs_buf.data_as<__fp16>(), - innov_buf.data_as<__fp16>(), - pred_ptrs.data(), - node.output_buffer.data_as<__fp16>(), - n, seq_len, hidden_dim); -} - -void compute_bilstm_sequence_node(GraphNode& node, const std::vector>& nodes, - const std::unordered_map& node_index_map) { - const auto& input = get_input(node, 0, nodes, node_index_map); - const auto& w_ih_fwd = get_input(node, 1, nodes, node_index_map); - const auto& w_hh_fwd = get_input(node, 2, nodes, node_index_map); - const auto& b_ih_fwd = get_input(node, 3, nodes, node_index_map); - const auto& b_hh_fwd = get_input(node, 4, nodes, node_index_map); - const auto& w_ih_bwd = get_input(node, 5, nodes, node_index_map); - const auto& w_hh_bwd = get_input(node, 6, nodes, node_index_map); - const auto& b_ih_bwd = get_input(node, 7, nodes, node_index_map); - const auto& b_hh_bwd = get_input(node, 8, nodes, node_index_map); - - size_t batch_size = input.shape[0]; - size_t seq_len = input.shape[1]; - size_t input_size = input.shape[2]; - size_t hidden_size = w_ih_fwd.shape[0] / 4; - - cactus_bilstm_sequence_f16( - input.data_as<__fp16>(), - w_ih_fwd.data_as<__fp16>(), w_hh_fwd.data_as<__fp16>(), - b_ih_fwd.data_as<__fp16>(), b_hh_fwd.data_as<__fp16>(), - w_ih_bwd.data_as<__fp16>(), w_hh_bwd.data_as<__fp16>(), - b_ih_bwd.data_as<__fp16>(), b_hh_bwd.data_as<__fp16>(), - node.output_buffer.data_as<__fp16>(), - batch_size, seq_len, input_size, hidden_size); -} - -void compute_maxpool1d_node(GraphNode& node, const std::vector>& nodes, - const std::unordered_map& node_index_map) { - const auto& input = get_input(node, 0, nodes, node_index_map); - - size_t batch_size = input.shape[0]; - size_t channels = input.shape[1]; - size_t input_length = input.shape[2]; - size_t kernel_size = node.params.kernel_size; - size_t stride = node.params.stride; - - cactus_maxpool1d_f16( - input.data_as<__fp16>(), - node.output_buffer.data_as<__fp16>(), - batch_size, channels, input_length, - kernel_size, stride); -} - -void compute_conv2d_k3s1p1_node(GraphNode& node, const std::vector>& nodes, - const std::unordered_map& node_index_map) { - const auto& X = get_input(node, 0, nodes, node_index_map); - const auto& W = get_input(node, 1, nodes, node_index_map); - const BufferDesc* B = nullptr; - if (node.input_ids.size() >= 3) { - B = &get_input(node, 2, nodes, node_index_map); - } - auto& Y = node.output_buffer; - - if (X.shape.size() != 4) { - throw std::runtime_error("conv2d_k3s1p1 expects input [N, C_in, H, W]"); - } - if (X.precision != Precision::FP16) { - throw std::runtime_error("conv2d_k3s1p1 only supports FP16 activations"); - } - - const size_t N = X.shape[0]; - const size_t C_in = X.shape[1]; - const size_t H = X.shape[2]; - const size_t W_in = X.shape[3]; - const size_t C_out = Y.shape[1]; - - if (H == 0 || W_in == 0) { - throw std::runtime_error("conv2d_k3s1p1 input spatial dimensions must be > 0"); - } - - const __fp16* bias_ptr = nullptr; - std::vector<__fp16> bias_fp16; - if (B) { - if (B->precision == Precision::FP16) { - bias_ptr = B->data_as<__fp16>(); - } else if (B->precision == Precision::FP32) { - bias_fp16.resize(C_out); - cactus_fp32_to_fp16(B->data_as(), bias_fp16.data(), C_out); - bias_ptr = bias_fp16.data(); - } else { - throw std::runtime_error("conv2d_k3s1p1 bias only supports FP16/FP32"); - } - } - - if (W.precision == Precision::FP16) { - if (W.shape.size() != 4) { - throw std::runtime_error("conv2d_k3s1p1 FP16 weight must be [C_out, C_in, 3, 3]"); - } - cactus_conv2d_f16_k3s1p1_nchw( - X.data_as<__fp16>(), W.data_as<__fp16>(), bias_ptr, - Y.data_as<__fp16>(), - N, C_in, H, W_in, C_out); - return; - } - - throw std::runtime_error("conv2d_k3s1p1 only supports FP16 weights"); -} - -void compute_stats_pool_node(GraphNode& node, const std::vector>& nodes, - const std::unordered_map& node_index_map) { - const auto& input = get_input(node, 0, nodes, node_index_map); - const __fp16* src = input.data_as<__fp16>(); - __fp16* dst = node.output_buffer.data_as<__fp16>(); - - size_t batch = input.shape[0]; - size_t total_per_batch = input.total_size / batch; - size_t T = input.shape.back(); - size_t features = total_per_batch / T; - - for (size_t b = 0; b < batch; ++b) { - const __fp16* batch_src = src + b * total_per_batch; - __fp16* batch_dst = dst + b * features * 2; - - for (size_t f = 0; f < features; ++f) { - float sum = 0.0f, sum_sq = 0.0f; - for (size_t t = 0; t < T; ++t) { - float v = static_cast(batch_src[f * T + t]); - sum += v; - sum_sq += v * v; - } - float mean = sum / static_cast(T); - float var = T > 1 ? (sum_sq - static_cast(T) * mean * mean) / static_cast(T - 1) : 0.0f; - float std_val = sqrtf(fmaxf(var, 0.0f)); - batch_dst[f] = static_cast<__fp16>(mean); - batch_dst[features + f] = static_cast<__fp16>(std_val); - } - } -} - -void compute_weighted_stats_pool_node(GraphNode& node, const std::vector>& nodes, - const std::unordered_map& node_index_map) { - const auto& input = get_input(node, 0, nodes, node_index_map); - const auto& weight_buf = get_input(node, 1, nodes, node_index_map); - const __fp16* src = input.data_as<__fp16>(); - const float* weights = weight_buf.data_as(); - __fp16* dst = node.output_buffer.data_as<__fp16>(); - - size_t batch = input.shape[0]; - size_t total_per_batch = input.total_size / batch; - size_t T = input.shape.back(); - size_t features = total_per_batch / T; - - constexpr float eps = 1e-8f; - - for (size_t b = 0; b < batch; ++b) { - const __fp16* batch_src = src + b * total_per_batch; - const float* batch_w = weights + b * T; - __fp16* batch_dst = dst + b * features * 2; - - float v1 = 0.0f, v2 = 0.0f; - for (size_t t = 0; t < T; ++t) { - float w = batch_w[t]; - v1 += w; - v2 += w * w; - } - float v1_safe = v1 + eps; - float var_denom = v1_safe - v2 / v1_safe + eps; - - for (size_t f = 0; f < features; ++f) { - float wsum = 0.0f; - for (size_t t = 0; t < T; ++t) { - wsum += static_cast(batch_src[f * T + t]) * batch_w[t]; - } - float mean = wsum / v1_safe; - - float wvar = 0.0f; - for (size_t t = 0; t < T; ++t) { - float dx = static_cast(batch_src[f * T + t]) - mean; - wvar += batch_w[t] * dx * dx; - } - float std_val = sqrtf(fmaxf(wvar / var_denom, 0.0f)); - - batch_dst[f] = static_cast<__fp16>(mean); - batch_dst[features + f] = static_cast<__fp16>(std_val); - } - } -} - diff --git a/cactus/graph/graph_ops_sample.cpp b/cactus/graph/graph_ops_sample.cpp deleted file mode 100644 index 26f6b621f..000000000 --- a/cactus/graph/graph_ops_sample.cpp +++ /dev/null @@ -1,134 +0,0 @@ -#include "graph.h" -#include "../kernel/kernel.h" -#include -#include -#include -#include - -void compute_sample_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - const auto& logits_buffer = get_input(node, 0, nodes, node_index_map); - - float temperature = node.params.temperature; - float top_p = node.params.top_p; - float min_p = node.params.min_p; - float repetition_penalty = node.params.repetition_penalty; - size_t top_k = node.params.top_k; - size_t random_seed = node.params.random_seed; - - const float* bias_values = node.params.bias_values.empty() ? nullptr : node.params.bias_values.data(); - const uint32_t* bias_indices = node.params.bias_indices.empty() ? nullptr : node.params.bias_indices.data(); - size_t bias_count = node.params.bias_values.size(); - - if (logits_buffer.shape.size() != 2) { - throw std::runtime_error("Sample expects 2D logits tensor [seq_len, vocab_size]"); - } - - size_t seq_len = logits_buffer.shape[0]; - size_t vocab_size = logits_buffer.shape[1]; - size_t last_token_offset = (seq_len - 1) * vocab_size; - - if (logits_buffer.precision == Precision::FP16) { - const __fp16* logits_fp16 = logits_buffer.data_as<__fp16>(); - cactus_sample_f16_ex(logits_fp16 + last_token_offset, node.output_buffer.data_as(), - vocab_size, temperature, top_p, min_p, repetition_penalty, top_k, random_seed, - bias_values, bias_indices, bias_count); - } else { - const float* logits_fp32 = logits_buffer.data_as(); - cactus_sample_f32_ex(logits_fp32 + last_token_offset, node.output_buffer.data_as(), - vocab_size, temperature, top_p, min_p, repetition_penalty, top_k, random_seed, - bias_values, bias_indices, bias_count); - } -} - -void compute_topk_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - const auto& input_buffer = get_input(node, 0, nodes, node_index_map); - if (input_buffer.shape.size() != 2) { - throw std::runtime_error("TopK currently only supports 2D tensors [batch, features]"); - } - - size_t k = node.params.top_k; - size_t batch_size = input_buffer.shape[0]; - size_t feature_size = input_buffer.shape[1]; - size_t block_size = batch_size * k; - - std::vector input_float(input_buffer.total_size); - if (input_buffer.precision == Precision::INT8) { - throw std::runtime_error("TopK currently does not support INT8 input"); - } else if (input_buffer.precision == Precision::FP16) { - const __fp16* input_fp16 = input_buffer.data_as<__fp16>(); - for (size_t i = 0; i < input_buffer.total_size; ++i) { - input_float[i] = static_cast(input_fp16[i]); - } - } else { - const float* input_fp32 = input_buffer.data_as(); - std::memcpy(input_float.data(), input_fp32, input_buffer.total_size * sizeof(float)); - } - - float* output = node.output_buffer.data_as(); - - for (size_t b = 0; b < batch_size; ++b) { - const float* row = input_float.data() + b * feature_size; - - std::vector> indexed_values(feature_size); - for (size_t i = 0; i < feature_size; ++i) { - indexed_values[i] = {i, row[i]}; - } - - std::partial_sort(indexed_values.begin(), - indexed_values.begin() + k, - indexed_values.end(), - [](const auto& a, const auto& b) { return a.second > b.second; }); - - float* idx_out_row = output + b * k; - float* val_out_row = output + block_size + b * k; - for (size_t i = 0; i < k; ++i) { - idx_out_row[i] = static_cast(indexed_values[i].first); - val_out_row[i] = indexed_values[i].second; - } - } -} - -void compute_scatter_topk_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - const auto& indices_buffer = get_input(node, 0, nodes, node_index_map); - const auto& values_buffer = get_input(node, 1, nodes, node_index_map); - - if (indices_buffer.shape != values_buffer.shape) { - throw std::runtime_error("ScatterTopK requires indices and values with identical shapes"); - } - if (indices_buffer.shape.size() != 2) { - throw std::runtime_error("ScatterTopK currently supports 2D tensors"); - } - - size_t batch_size = indices_buffer.shape[0]; - size_t top_k = indices_buffer.shape[1]; - size_t num_classes = node.params.num_classes; - - if (num_classes == 0) { - throw std::runtime_error("ScatterTopK requires num_classes > 0"); - } - - float* output = node.output_buffer.data_as(); - std::fill(output, output + num_classes * batch_size, 0.0f); - - if (indices_buffer.precision != Precision::FP32 || values_buffer.precision != Precision::FP32) { - throw std::runtime_error("ScatterTopK currently expects FP32 inputs"); - } - - const float* indices_data = indices_buffer.data_as(); - const float* values_data = values_buffer.data_as(); - - for (size_t b = 0; b < batch_size; ++b) { - for (size_t k = 0; k < top_k; ++k) { - float raw_index = indices_data[b * top_k + k]; - if (!std::isfinite(raw_index)) { - throw std::runtime_error("ScatterTopK index is not finite"); - } - size_t expert_index = static_cast(raw_index + 0.5f); - if (expert_index >= num_classes) { - throw std::runtime_error("ScatterTopK index out of range"); - } - float weight = values_data[b * top_k + k]; - output[expert_index * batch_size + b] = weight; - } - } -} diff --git a/cactus/graph/graph_ops_tensor.cpp b/cactus/graph/graph_ops_tensor.cpp deleted file mode 100644 index 8ccdcd49d..000000000 --- a/cactus/graph/graph_ops_tensor.cpp +++ /dev/null @@ -1,470 +0,0 @@ -#include "graph.h" -#include "../kernel/kernel.h" -#include "../kernel/kernel_utils.h" -#include -#include -#include - -void compute_transpose_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - if (node.params.backend == ComputeBackend::NPU) { - throw std::runtime_error("NPU transpose operation not yet implemented"); - } - - const auto& input_buffer = get_input(node, 0, nodes, node_index_map); - - if (input_buffer.precision != Precision::FP16) { - throw std::runtime_error("Transpose only supports FP16 precision"); - } - - const auto& permutation = node.params.permutation; - - const __fp16* input = input_buffer.data_as<__fp16>(); - __fp16* output = node.output_buffer.data_as<__fp16>(); - cactus_transpose_f16(input, output, input_buffer.shape.data(), permutation.data(), permutation.size(), 0, input_buffer.total_size); -} - -void compute_gather_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - const auto& tensor_buffer = get_input(node, 0, nodes, node_index_map); - const auto& indices_buffer = get_input(node, 1, nodes, node_index_map); - - size_t first_dim = tensor_buffer.shape[0]; - size_t element_size = 1; - for (size_t i = 1; i < tensor_buffer.shape.size(); i++) { - element_size *= tensor_buffer.shape[i]; - } - - size_t num_indices = indices_buffer.total_size; - size_t bytes_per_element = PrecisionTraits::packed_size_of(tensor_buffer.precision, element_size); - - if (PrecisionTraits::is_integer(tensor_buffer.precision)) { - const char* tensor_data = static_cast(tensor_buffer.get_data()); - char* output = static_cast(node.output_buffer.get_data()); - Precision prec = tensor_buffer.precision; - - const bool is_grouped = tensor_buffer.group_size > 0; - __fp16* gathered_scales = nullptr; - const __fp16* src_scales = nullptr; - size_t num_groups = 0; - - if (is_grouped) { - num_groups = tensor_buffer.num_groups; - src_scales = tensor_buffer.scales_as_fp16(); - size_t scales_bytes = num_indices * num_groups * sizeof(__fp16); - node.output_buffer.owned_scales = std::make_unique(scales_bytes); - gathered_scales = reinterpret_cast<__fp16*>(node.output_buffer.owned_scales.get()); - } - - const int8_t* indices = indices_buffer.data_as(); - for (size_t i = 0; i < num_indices; i++) { - size_t idx = static_cast(indices[i]); - if (idx >= first_dim) { - throw std::runtime_error("Gather index " + std::to_string(idx) + " out of bounds for dimension " + std::to_string(first_dim)); - } - std::memcpy(output + PrecisionTraits::byte_offset_of(prec, i * element_size), - tensor_data + PrecisionTraits::byte_offset_of(prec, idx * element_size), - bytes_per_element); - if (is_grouped) { - for (size_t g = 0; g < num_groups; g++) { - gathered_scales[i * num_groups + g] = src_scales[idx * num_groups + g]; - } - } - } - - if (is_grouped) { - node.output_buffer.group_size = tensor_buffer.group_size; - node.output_buffer.num_groups = num_groups; - node.output_buffer.scales_data = gathered_scales; - } - } else if (tensor_buffer.precision == Precision::FP16) { - const __fp16* tensor_data = tensor_buffer.data_as<__fp16>(); - __fp16* output = node.output_buffer.data_as<__fp16>(); - - if (indices_buffer.precision == Precision::INT8) { - const int8_t* indices = indices_buffer.data_as(); - for (size_t i = 0; i < num_indices; i++) { - size_t idx = static_cast(indices[i]); - if (idx >= first_dim) { - throw std::runtime_error("Gather index " + std::to_string(idx) + " out of bounds for dimension " + std::to_string(first_dim)); - } - std::memcpy(output + i * element_size, tensor_data + idx * element_size, bytes_per_element); - } - } else { - const float* indices = indices_buffer.data_as(); - for (size_t i = 0; i < num_indices; i++) { - size_t idx = static_cast(indices[i]); - if (idx >= first_dim) { - throw std::runtime_error("Gather index " + std::to_string(idx) + " out of bounds for dimension " + std::to_string(first_dim)); - } - std::memcpy(output + i * element_size, tensor_data + idx * element_size, bytes_per_element); - } - } - } else { - const float* tensor_data = tensor_buffer.data_as(); - float* output = node.output_buffer.data_as(); - - if (indices_buffer.precision == Precision::INT8) { - const int8_t* indices = indices_buffer.data_as(); - for (size_t i = 0; i < num_indices; i++) { - size_t idx = static_cast(indices[i]); - if (idx >= first_dim) { - throw std::runtime_error("Gather index " + std::to_string(idx) + " out of bounds for dimension " + std::to_string(first_dim)); - } - std::memcpy(output + i * element_size, tensor_data + idx * element_size, bytes_per_element); - } - } else { - const float* indices = indices_buffer.data_as(); - for (size_t i = 0; i < num_indices; i++) { - size_t idx = static_cast(indices[i]); - if (idx >= first_dim) { - throw std::runtime_error("Gather index " + std::to_string(idx) + " out of bounds for dimension " + std::to_string(first_dim)); - } - std::memcpy(output + i * element_size, tensor_data + idx * element_size, bytes_per_element); - } - } - } -} - -void compute_slice_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - auto* input_node = nodes[node_index_map.at(node.input_ids[0])].get(); - auto& input_buffer = input_node->output_buffer; - - const size_t axis_index = static_cast(node.params.axis); - - const size_t axis_size = input_buffer.shape[axis_index]; - const size_t slice_start = node.params.slice_start; - size_t slice_length = node.params.slice_length; - - if (slice_length == 0) { - slice_length = axis_size - slice_start; - } - - if (axis_index == 0) { - auto dims0 = AxisDims::from_shape(input_buffer.shape, 0); - - auto* base_ptr = static_cast(input_buffer.get_data()); - if (!base_ptr) { - throw std::runtime_error("Slice input buffer is not available"); - } - - const size_t byte_offset = PrecisionTraits::byte_offset_of(input_buffer.precision, slice_start * dims0.inner); - - node.output_buffer.set_external(base_ptr + byte_offset); - node.output_buffer.precision = input_buffer.precision; - - if (input_buffer.is_grouped_int8()) { - size_t num_groups = input_buffer.num_groups; - size_t scales_bytes = slice_length * num_groups * sizeof(__fp16); - node.output_buffer.owned_scales = std::make_unique(scales_bytes); - __fp16* sliced_scales = reinterpret_cast<__fp16*>(node.output_buffer.owned_scales.get()); - const __fp16* input_scales = input_buffer.scales_as_fp16(); - - for (size_t i = 0; i < slice_length; i++) { - for (size_t g = 0; g < num_groups; g++) { - sliced_scales[i * num_groups + g] = input_scales[(slice_start + i) * num_groups + g]; - } - } - - node.output_buffer.group_size = input_buffer.group_size; - node.output_buffer.num_groups = num_groups; - node.output_buffer.scales_data = sliced_scales; - } - return; - } - - const char* input_ptr = static_cast(input_buffer.get_data()); - if (!input_ptr) { - throw std::runtime_error("Slice input buffer is not available"); - } - - auto dims = AxisDims::from_shape(input_buffer.shape, axis_index); - - node.output_buffer.external_data = nullptr; - node.output_buffer.allocate(); - node.output_buffer.precision = input_buffer.precision; - - auto* output_ptr = static_cast(node.output_buffer.get_data()); - if (!output_ptr) { - throw std::runtime_error("Slice output buffer could not be allocated"); - } - - const size_t copy_block_elements = slice_length * dims.inner; - const size_t axis_stride_elements = axis_size * dims.inner; - const size_t copy_block_bytes = PrecisionTraits::byte_offset_of(input_buffer.precision, copy_block_elements); - const size_t axis_stride_bytes = PrecisionTraits::byte_offset_of(input_buffer.precision, axis_stride_elements); - - for (size_t outer = 0; outer < dims.outer; ++outer) { - const char* src = input_ptr + outer * axis_stride_bytes + PrecisionTraits::byte_offset_of(input_buffer.precision, slice_start * dims.inner); - char* dst = output_ptr + outer * copy_block_bytes; - std::memcpy(dst, src, copy_block_bytes); - } -} - -void compute_embedding_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - const auto& embeddings_buffer = get_input(node, 0, nodes, node_index_map); - const auto& indices_buffer = get_input(node, 1, nodes, node_index_map); - - size_t hidden_dim = embeddings_buffer.shape[1]; - size_t num_indices = indices_buffer.total_size; - size_t vocab_size = embeddings_buffer.original_N > 0 - ? embeddings_buffer.original_N - : embeddings_buffer.shape[0]; - - std::vector indices_float; - const float* indices_ptr; - if (indices_buffer.precision == Precision::FP32) { - indices_ptr = indices_buffer.data_as(); - } else { - indices_float.resize(num_indices); - const int8_t* int_indices = indices_buffer.data_as(); - for (size_t i = 0; i < num_indices; i++) { - indices_float[i] = static_cast(int_indices[i]); - } - indices_ptr = indices_float.data(); - } - - __fp16* output = node.output_buffer.data_as<__fp16>(); - - Precision emb_prec = embeddings_buffer.precision; - if (PrecisionTraits::is_integer(emb_prec) && embeddings_buffer.group_size > 0) { - const int8_t* embeddings = embeddings_buffer.data_as(); - const __fp16* scales = embeddings_buffer.scales_as_fp16(); - size_t group_size = embeddings_buffer.group_size; - size_t num_groups = embeddings_buffer.num_groups; - - static const uint8_t gather_indices[4][16] = { - {0, 1, 2, 3, 16, 17, 18, 19, 32, 33, 34, 35, 48, 49, 50, 51}, // lane 0 - {4, 5, 6, 7, 20, 21, 22, 23, 36, 37, 38, 39, 52, 53, 54, 55}, // lane 1 - {8, 9, 10, 11, 24, 25, 26, 27, 40, 41, 42, 43, 56, 57, 58, 59}, // lane 2 - {12, 13, 14, 15, 28, 29, 30, 31, 44, 45, 46, 47, 60, 61, 62, 63} // lane 3 - }; - - auto load_table = [emb_prec](const int8_t* base) -> int8x16x4_t { - if (emb_prec == Precision::INT4) { - const uint8_t* ubase = reinterpret_cast(base); - int8x16_t low_a, high_a, low_b, high_b; - unpack_int4_as_int8x16x2(ubase, high_a, low_a); - unpack_int4_as_int8x16x2(ubase + 16, high_b, low_b); - return {low_a, high_a, low_b, high_b}; - } - return vld1q_s8_x4(base); - }; - - for (size_t i = 0; i < num_indices; i++) { - size_t idx = static_cast(indices_ptr[i]); - if (idx >= vocab_size) { - throw std::runtime_error("Embedding index out of bounds: " + std::to_string(idx) + " >= " + std::to_string(vocab_size)); - } - - size_t block = idx / 4; - size_t lane = idx % 4; - __fp16* out_row = output + i * hidden_dim; - - uint8x16_t indices_vec = vld1q_u8(gather_indices[lane]); - - for (size_t g = 0; g < num_groups; g++) { - float scale = (float)scales[(block * num_groups + g) * 4 + lane]; - float32x4_t scale_vec = vdupq_n_f32(scale); - - size_t k_start = g * group_size; - size_t k_end = std::min(k_start + group_size, hidden_dim); - - const int8_t* group_base = embeddings + PrecisionTraits::byte_offset_of(emb_prec, (block * hidden_dim + k_start) * 4); - - size_t k = k_start; - for (; k + 16 <= k_end; k += 16) { - const int8_t* chunk_base = group_base + PrecisionTraits::byte_offset_of(emb_prec, (k - k_start) * 4); - int8x16x4_t table = load_table(chunk_base); - - int8x16_t values = vqtbl4q_s8(table, indices_vec); - - int16x8_t lo16 = vmovl_s8(vget_low_s8(values)); - int16x8_t hi16 = vmovl_s8(vget_high_s8(values)); - - float32x4_t f0 = vmulq_f32(vcvtq_f32_s32(vmovl_s16(vget_low_s16(lo16))), scale_vec); - float32x4_t f1 = vmulq_f32(vcvtq_f32_s32(vmovl_s16(vget_high_s16(lo16))), scale_vec); - float32x4_t f2 = vmulq_f32(vcvtq_f32_s32(vmovl_s16(vget_low_s16(hi16))), scale_vec); - float32x4_t f3 = vmulq_f32(vcvtq_f32_s32(vmovl_s16(vget_high_s16(hi16))), scale_vec); - - vst1q_f16(out_row + k, vcombine_f16(vcvt_f16_f32(f0), vcvt_f16_f32(f1))); - vst1q_f16(out_row + k + 8, vcombine_f16(vcvt_f16_f32(f2), vcvt_f16_f32(f3))); - } - - if (k < k_end) - assert (embeddings_buffer.precision != Precision::INT4 && "grouped INT4 embeddings must have hidden_dim that is a multiple of 32"); - for (; k < k_end; k++) { - size_t k_group = k / 4; - size_t k_within = k % 4; - int8_t val = embeddings[(block * (hidden_dim / 4) + k_group) * 16 + lane * 4 + k_within]; - out_row[k] = static_cast<__fp16>(val * scale); - } - } - } - } else if (embeddings_buffer.precision == Precision::FP16) { - const __fp16* embeddings = embeddings_buffer.data_as<__fp16>(); - for (size_t i = 0; i < num_indices; i++) { - size_t idx = static_cast(indices_ptr[i]); - if (idx >= vocab_size) { - throw std::runtime_error("Embedding index out of bounds: " + std::to_string(idx) + " >= " + std::to_string(vocab_size)); - } - std::memcpy(output + i * hidden_dim, embeddings + idx * hidden_dim, hidden_dim * sizeof(__fp16)); - } - } else { - throw std::runtime_error("Embedding requires interleaved grouped INT4/INT8 or FP16"); - } -} - -void compute_concat_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - const auto& input1_buffer = get_input(node, 0, nodes, node_index_map); - const auto& input2_buffer = get_input(node, 1, nodes, node_index_map); - - std::vector shape1 = input1_buffer.shape; - std::vector shape2 = input2_buffer.shape; - std::vector output_shape = node.output_buffer.shape; - - if (input1_buffer.precision != Precision::FP16) { - throw std::runtime_error("Concat operation only supports FP16 precision"); - } - cactus_concat_f16(input1_buffer.data_as<__fp16>(), input2_buffer.data_as<__fp16>(), - node.output_buffer.data_as<__fp16>(), - shape1.data(), shape2.data(), output_shape.data(), - shape1.size(), node.params.axis); -} - -void compute_cat_node( - GraphNode& node, - const std::vector>& nodes, - const std::unordered_map& node_index_map -) { - if (node.params.axis < 0) { - throw std::runtime_error("Cat operation does not support negative axis"); - } - if (node.input_ids.size() < 2) { - throw std::runtime_error("Cat operation requires at least 2 input tensors"); - } - - const auto& first_buffer = get_input(node, 0, nodes, node_index_map); - - if (first_buffer.precision != Precision::FP16) { - throw std::runtime_error("Cat operation only supports FP16 precision"); - } - - std::vector input_data_ptrs(node.input_ids.size()); - std::vector input_shape_ptrs(node.input_ids.size()); - - for (size_t i = 0; i < node.input_ids.size(); i++) { - const auto& buffer = get_input(node, i, nodes, node_index_map); - - if (buffer.precision != Precision::FP16) { - throw std::runtime_error("Cat operation only supports FP16 precision"); - } - - input_data_ptrs[i] = buffer.data_as<__fp16>(); - input_shape_ptrs[i] = buffer.shape.data(); - } - - cactus_cat_f16(input_data_ptrs.data(), - node.output_buffer.data_as<__fp16>(), - input_shape_ptrs.data(), - node.output_buffer.shape.data(), - node.input_ids.size(), - node.output_buffer.shape.size(), - node.params.axis); -} - -void compute_index_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - const auto& input_buffer = get_input(node, 0, nodes, node_index_map); - const auto& input_shape = input_buffer.shape; - - int dim = node.params.axis; - size_t index_value = node.params.index_value; - - const char* input_data = static_cast(input_buffer.get_data()); - char* output_data = static_cast(node.output_buffer.get_data()); - - if (dim == 0) { - size_t slice_size = input_buffer.total_size / input_shape[0]; - size_t offset_bytes = PrecisionTraits::byte_offset_of(input_buffer.precision, index_value * slice_size); - node.output_buffer.set_external(const_cast(input_data) + offset_bytes); - return; - } - - std::vector input_strides(input_shape.size()); - input_strides[input_shape.size() - 1] = 1; - for (int i = static_cast(input_shape.size()) - 2; i >= 0; --i) { - input_strides[i] = input_strides[i + 1] * input_shape[i + 1]; - } - - size_t slice_size = input_strides[dim]; - size_t outer_size = input_buffer.total_size / input_strides[dim - 1]; - size_t dim_stride = input_strides[dim]; - size_t block_size = dim_stride * input_shape[dim]; - - size_t output_idx = 0; - for (size_t outer_idx = 0; outer_idx < outer_size; ++outer_idx) { - size_t input_base = outer_idx * block_size + index_value * dim_stride; - - char* output_offset_bytes = output_data + PrecisionTraits::byte_offset_of(input_buffer.precision, output_idx); - const char* input_offset_bytes = input_data + PrecisionTraits::byte_offset_of(input_buffer.precision, input_base); - size_t length = PrecisionTraits::byte_offset_of(input_buffer.precision, slice_size); - std::memcpy(output_offset_bytes, input_offset_bytes, length); - - output_idx += slice_size; - } -} - -void compute_bilinear_interpolation_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - const auto& pos_embeds_buffer = get_input(node, 0, nodes, node_index_map); - - size_t total_pos_embeds = pos_embeds_buffer.shape[0]; - size_t embed_dim = pos_embeds_buffer.shape[1]; - - size_t src_height = static_cast(std::sqrt(total_pos_embeds)); - size_t src_width = src_height; - - size_t dst_height = node.params.dst_height; - size_t dst_width = node.params.dst_width; - bool align_corners = node.params.align_corners; - - __fp16* output = node.output_buffer.data_as<__fp16>(); - - if (pos_embeds_buffer.precision == Precision::FP16) { - const __fp16* input = pos_embeds_buffer.data_as<__fp16>(); - cactus_bilinear_interpolation_f16(input, output, src_height, src_width, embed_dim, - dst_height, dst_width, align_corners); - } - else if (pos_embeds_buffer.precision == Precision::INT8) { - std::vector<__fp16> input_fp16(total_pos_embeds * embed_dim); - cactus_int8_to_fp16(pos_embeds_buffer.data_as(), input_fp16.data(), - total_pos_embeds * embed_dim); - cactus_bilinear_interpolation_f16(input_fp16.data(), output, src_height, src_width, embed_dim, - dst_height, dst_width, align_corners); - } - else { - throw std::runtime_error("BILINEAR_INTERPOLATION only supports INT8 and FP16 input precision"); - } -} - -void compute_persistent_node(GraphNode& node, const std::vector>& nodes, const std::unordered_map& node_index_map) { - if (node.input_ids.empty()) { - return; - } - - auto it = node_index_map.find(node.input_ids[0]); - - if (it != node_index_map.end()) { - const auto& input_buffer = nodes[it->second]->output_buffer; - - if (!node.output_buffer.get_data()) { - node.output_buffer.allocate(); - } - - std::memcpy(node.output_buffer.get_data(), - input_buffer.get_data(), - input_buffer.byte_size); - } else { - if (node.output_buffer.get_data()) { - return; - } - throw std::runtime_error("PERSISTENT node input not found and not populated - this should not happen"); - } -} diff --git a/cactus/graph/graph_param_io.cpp b/cactus/graph/graph_param_io.cpp deleted file mode 100644 index 6d49c9ad6..000000000 --- a/cactus/graph/graph_param_io.cpp +++ /dev/null @@ -1,380 +0,0 @@ -#include "graph_param_io.h" - -#include -#include -#include -#include - -namespace { - -void write_u32(std::ostream& out, uint32_t v) { - out.write(reinterpret_cast(&v), sizeof(v)); -} - -void write_u64(std::ostream& out, uint64_t v) { - out.write(reinterpret_cast(&v), sizeof(v)); -} - -void write_i32(std::ostream& out, int32_t v) { - out.write(reinterpret_cast(&v), sizeof(v)); -} - -void write_f32(std::ostream& out, float v) { - out.write(reinterpret_cast(&v), sizeof(v)); -} - -uint32_t read_u32(std::istream& in) { - uint32_t v = 0; - in.read(reinterpret_cast(&v), sizeof(v)); - if (!in) throw std::runtime_error("Unexpected EOF while reading uint32"); - return v; -} - -uint64_t read_u64(std::istream& in) { - uint64_t v = 0; - in.read(reinterpret_cast(&v), sizeof(v)); - if (!in) throw std::runtime_error("Unexpected EOF while reading uint64"); - return v; -} - -int32_t read_i32(std::istream& in) { - int32_t v = 0; - in.read(reinterpret_cast(&v), sizeof(v)); - if (!in) throw std::runtime_error("Unexpected EOF while reading int32"); - return v; -} - -float read_f32(std::istream& in) { - float v = 0.0f; - in.read(reinterpret_cast(&v), sizeof(v)); - if (!in) throw std::runtime_error("Unexpected EOF while reading float"); - return v; -} - -void write_size_vector(std::ostream& out, const std::vector& values) { - write_u32(out, static_cast(values.size())); - for (size_t v : values) write_u64(out, static_cast(v)); -} - -std::vector read_size_vector(std::istream& in) { - uint32_t count = read_u32(in); - std::vector values; - values.reserve(count); - for (uint32_t i = 0; i < count; ++i) values.push_back(static_cast(read_u64(in))); - return values; -} - -void write_u32_vector(std::ostream& out, const std::vector& values) { - write_u32(out, static_cast(values.size())); - for (uint32_t v : values) write_u32(out, v); -} - -std::vector read_u32_vector(std::istream& in) { - uint32_t count = read_u32(in); - std::vector values; - values.reserve(count); - for (uint32_t i = 0; i < count; ++i) values.push_back(read_u32(in)); - return values; -} - -void write_f32_vector(std::ostream& out, const std::vector& values) { - write_u32(out, static_cast(values.size())); - for (float v : values) write_f32(out, v); -} - -std::vector read_f32_vector(std::istream& in) { - uint32_t count = read_u32(in); - std::vector values; - values.reserve(count); - for (uint32_t i = 0; i < count; ++i) values.push_back(read_f32(in)); - return values; -} - -enum class ParamField : uint32_t { - Scalar = 1, - Axis, - NewShape, - PretransposedRhs, - Backend, - SliceStart, - SliceLength, - Epsilon, - NumGroups, - IndexValue, - Permutation, - Scale, - Theta, - PositionOffset, - WindowSize, - IsCausal, - AttentionMaskIsAdditive, - LogitCap, - TopK, - Temperature, - TopP, - MinP, - RepetitionPenalty, - RandomSeed, - BiasIndices, - BiasValues, - NormalizeRouting, - NumExperts, - NumExpertsPerTok, - MoeGated, - Activation, - NumKvHeads, - ChunkSize, - DstHeight, - DstWidth, - AlignCorners, - NumClasses, - NumAltupInputs, - KernelSize, - Stride, - Dilation, - NumFftBins, - CacheSeqLen, - HeadDim, - VHeadDim, - CachedKeysInt8Ptr, - CachedValuesInt8Ptr, - CachedKScalesPtr, - CachedVScalesPtr, -}; - -enum class FieldPersistence { - Persistent, - RuntimeOnly, - Derived, -}; - -struct FieldSpec { - ParamField field; - FieldPersistence persistence; -}; - -using Schema = std::vector; - -const Schema& op_schema(OpType op_type) { - static const Schema kEmpty{}; - static const std::unordered_map kSchemas = { - {OpType::POW, {{ParamField::Scalar, FieldPersistence::Persistent}}}, - {OpType::SCALAR_ADD, {{ParamField::Scalar, FieldPersistence::Persistent}}}, - {OpType::SCALAR_SUBTRACT, {{ParamField::Scalar, FieldPersistence::Persistent}}}, - {OpType::SCALAR_MULTIPLY, {{ParamField::Scalar, FieldPersistence::Persistent}}}, - {OpType::SCALAR_DIVIDE, {{ParamField::Scalar, FieldPersistence::Persistent}}}, - {OpType::SOFTMAX, {{ParamField::Axis, FieldPersistence::Persistent}}}, - {OpType::SUM, {{ParamField::Axis, FieldPersistence::Persistent}}}, - {OpType::MEAN, {{ParamField::Axis, FieldPersistence::Persistent}}}, - {OpType::VARIANCE, {{ParamField::Axis, FieldPersistence::Persistent}}}, - {OpType::MIN, {{ParamField::Axis, FieldPersistence::Persistent}}}, - {OpType::MAX, {{ParamField::Axis, FieldPersistence::Persistent}}}, - {OpType::INDEX, {{ParamField::Axis, FieldPersistence::Persistent}, {ParamField::IndexValue, FieldPersistence::Persistent}}}, - {OpType::CONCAT, {{ParamField::Axis, FieldPersistence::Persistent}}}, - {OpType::CAT, {{ParamField::Axis, FieldPersistence::Persistent}}}, - {OpType::VIEW, {{ParamField::NewShape, FieldPersistence::Persistent}}}, - {OpType::RESHAPE, {{ParamField::NewShape, FieldPersistence::Persistent}}}, - {OpType::FLATTEN, {{ParamField::NewShape, FieldPersistence::Persistent}}}, - {OpType::MATMUL, {{ParamField::PretransposedRhs, FieldPersistence::Persistent}, {ParamField::Backend, FieldPersistence::Persistent}}}, - {OpType::TRANSPOSE, {{ParamField::Permutation, FieldPersistence::Persistent}, {ParamField::Backend, FieldPersistence::Persistent}}}, - {OpType::SLICE, {{ParamField::Axis, FieldPersistence::Persistent}, {ParamField::SliceStart, FieldPersistence::Persistent}, {ParamField::SliceLength, FieldPersistence::Persistent}}}, - {OpType::RMS_NORM, {{ParamField::Epsilon, FieldPersistence::Persistent}}}, - {OpType::LAYERNORM, {{ParamField::Epsilon, FieldPersistence::Persistent}}}, - {OpType::GROUPNORM, {{ParamField::Epsilon, FieldPersistence::Persistent}, {ParamField::NumGroups, FieldPersistence::Persistent}}}, - {OpType::BATCHNORM, {{ParamField::Epsilon, FieldPersistence::Persistent}, {ParamField::Axis, FieldPersistence::Persistent}}}, - {OpType::ROPE, {{ParamField::Theta, FieldPersistence::Persistent}, {ParamField::PositionOffset, FieldPersistence::Persistent}, {ParamField::Backend, FieldPersistence::Persistent}}}, - {OpType::ROPE_GPTJ, {{ParamField::Theta, FieldPersistence::Persistent}, {ParamField::PositionOffset, FieldPersistence::Persistent}, {ParamField::Scalar, FieldPersistence::Persistent}, {ParamField::Backend, FieldPersistence::Persistent}}}, - {OpType::TOPK, {{ParamField::TopK, FieldPersistence::Persistent}}}, - {OpType::ATTENTION, {{ParamField::Scale, FieldPersistence::Persistent}, {ParamField::PositionOffset, FieldPersistence::Persistent}, {ParamField::WindowSize, FieldPersistence::Persistent}, {ParamField::IsCausal, FieldPersistence::Persistent}, {ParamField::AttentionMaskIsAdditive, FieldPersistence::Persistent}, {ParamField::LogitCap, FieldPersistence::Persistent}, {ParamField::Backend, FieldPersistence::Persistent}}}, - {OpType::REL_POS_BIAS, {{ParamField::Scale, FieldPersistence::Persistent}}}, - {OpType::ATTENTION_INT8_HYBRID, { - {ParamField::Scale, FieldPersistence::Persistent}, - {ParamField::PositionOffset, FieldPersistence::Persistent}, - {ParamField::WindowSize, FieldPersistence::Persistent}, - {ParamField::NumKvHeads, FieldPersistence::Persistent}, - {ParamField::CacheSeqLen, FieldPersistence::Persistent}, - {ParamField::HeadDim, FieldPersistence::Persistent}, - {ParamField::VHeadDim, FieldPersistence::Persistent}, - {ParamField::CachedKeysInt8Ptr, FieldPersistence::RuntimeOnly}, - {ParamField::CachedValuesInt8Ptr, FieldPersistence::RuntimeOnly}, - {ParamField::CachedKScalesPtr, FieldPersistence::RuntimeOnly}, - {ParamField::CachedVScalesPtr, FieldPersistence::RuntimeOnly}, - }}, - {OpType::MOE_LAYER, {{ParamField::Scalar, FieldPersistence::Persistent}, {ParamField::Epsilon, FieldPersistence::Persistent}, {ParamField::NormalizeRouting, FieldPersistence::Persistent}, {ParamField::NumExperts, FieldPersistence::Persistent}, {ParamField::NumExpertsPerTok, FieldPersistence::Persistent}, {ParamField::MoeGated, FieldPersistence::Persistent}, {ParamField::Activation, FieldPersistence::Persistent}}}, - {OpType::GATED_DELTANET_DECODE, {{ParamField::Scale, FieldPersistence::Persistent}, {ParamField::NumKvHeads, FieldPersistence::Persistent}}}, - {OpType::GATED_DELTANET_PREFILL, {{ParamField::Scale, FieldPersistence::Persistent}, {ParamField::NumKvHeads, FieldPersistence::Persistent}, {ParamField::ChunkSize, FieldPersistence::Persistent}}}, - {OpType::STFT, {{ParamField::Stride, FieldPersistence::Persistent}, {ParamField::NumFftBins, FieldPersistence::Persistent}}}, - {OpType::BILINEAR_INTERPOLATION, {{ParamField::DstHeight, FieldPersistence::Persistent}, {ParamField::DstWidth, FieldPersistence::Persistent}, {ParamField::AlignCorners, FieldPersistence::Persistent}}}, - {OpType::SCATTER_TOPK, {{ParamField::NumClasses, FieldPersistence::Persistent}}}, - {OpType::ALTUP_PREDICT, {{ParamField::NumAltupInputs, FieldPersistence::Persistent}}}, - {OpType::ALTUP_CORRECT, {{ParamField::NumAltupInputs, FieldPersistence::Persistent}}}, - {OpType::MAXPOOL1D, {{ParamField::KernelSize, FieldPersistence::Persistent}, {ParamField::Stride, FieldPersistence::Persistent}}}, - {OpType::CONV1D_CAUSAL, {{ParamField::Dilation, FieldPersistence::Persistent}}}, - {OpType::CONV1D_K3, {{ParamField::Stride, FieldPersistence::Persistent}}}, - {OpType::CONV1D_K7S3, {{ParamField::Stride, FieldPersistence::Persistent}}}, - {OpType::CONV1D, {{ParamField::Stride, FieldPersistence::Persistent}}}, - {OpType::SAMPLE, {{ParamField::Temperature, FieldPersistence::Persistent}, {ParamField::TopP, FieldPersistence::Persistent}, {ParamField::MinP, FieldPersistence::Persistent}, {ParamField::RepetitionPenalty, FieldPersistence::Persistent}, {ParamField::TopK, FieldPersistence::Persistent}, {ParamField::RandomSeed, FieldPersistence::Persistent}, {ParamField::BiasIndices, FieldPersistence::Persistent}, {ParamField::BiasValues, FieldPersistence::Persistent}}}, - }; - - auto it = kSchemas.find(op_type); - return it == kSchemas.end() ? kEmpty : it->second; -} - -void write_field(std::ostream& out, ParamField field, const OpParams& params) { - switch (field) { - case ParamField::Scalar: write_f32(out, params.scalar); break; - case ParamField::Axis: write_i32(out, static_cast(params.axis)); break; - case ParamField::NewShape: write_size_vector(out, params.new_shape); break; - case ParamField::PretransposedRhs: write_u32(out, params.pretransposed_rhs ? 1u : 0u); break; - case ParamField::Backend: write_u32(out, static_cast(params.backend)); break; - case ParamField::SliceStart: write_u64(out, static_cast(params.slice_start)); break; - case ParamField::SliceLength: write_u64(out, static_cast(params.slice_length)); break; - case ParamField::Epsilon: write_f32(out, params.epsilon); break; - case ParamField::NumGroups: write_u64(out, static_cast(params.num_groups)); break; - case ParamField::IndexValue: write_u64(out, static_cast(params.index_value)); break; - case ParamField::Permutation: write_size_vector(out, params.permutation); break; - case ParamField::Scale: write_f32(out, params.scale); break; - case ParamField::Theta: write_f32(out, params.theta); break; - case ParamField::PositionOffset: write_u64(out, static_cast(params.position_offset)); break; - case ParamField::WindowSize: write_u64(out, static_cast(params.window_size)); break; - case ParamField::IsCausal: write_u32(out, params.is_causal ? 1u : 0u); break; - case ParamField::AttentionMaskIsAdditive: write_u32(out, params.attention_mask_is_additive ? 1u : 0u); break; - case ParamField::LogitCap: write_f32(out, params.logit_cap); break; - case ParamField::TopK: write_u64(out, static_cast(params.top_k)); break; - case ParamField::Temperature: write_f32(out, params.temperature); break; - case ParamField::TopP: write_f32(out, params.top_p); break; - case ParamField::MinP: write_f32(out, params.min_p); break; - case ParamField::RepetitionPenalty: write_f32(out, params.repetition_penalty); break; - case ParamField::RandomSeed: write_u64(out, static_cast(params.random_seed)); break; - case ParamField::BiasIndices: write_u32_vector(out, params.bias_indices); break; - case ParamField::BiasValues: write_f32_vector(out, params.bias_values); break; - case ParamField::NormalizeRouting: write_u32(out, params.normalize_routing ? 1u : 0u); break; - case ParamField::NumExperts: write_u64(out, static_cast(params.num_experts)); break; - case ParamField::NumExpertsPerTok: write_u64(out, static_cast(params.num_experts_per_tok)); break; - case ParamField::MoeGated: write_u32(out, params.moe_gated ? 1u : 0u); break; - case ParamField::Activation: write_u32(out, static_cast(params.activation)); break; - case ParamField::NumKvHeads: write_u64(out, static_cast(params.num_kv_heads)); break; - case ParamField::ChunkSize: write_u64(out, static_cast(params.chunk_size)); break; - case ParamField::DstHeight: write_u64(out, static_cast(params.dst_height)); break; - case ParamField::DstWidth: write_u64(out, static_cast(params.dst_width)); break; - case ParamField::AlignCorners: write_u32(out, params.align_corners ? 1u : 0u); break; - case ParamField::NumClasses: write_u64(out, static_cast(params.num_classes)); break; - case ParamField::NumAltupInputs: write_u64(out, static_cast(params.num_altup_inputs)); break; - case ParamField::KernelSize: write_u64(out, static_cast(params.kernel_size)); break; - case ParamField::Stride: write_u64(out, static_cast(params.stride)); break; - case ParamField::Dilation: write_u64(out, static_cast(params.dilation)); break; - case ParamField::NumFftBins: write_u64(out, static_cast(params.num_fft_bins)); break; - case ParamField::CacheSeqLen: write_u64(out, static_cast(params.cache_seq_len)); break; - case ParamField::HeadDim: write_u64(out, static_cast(params.head_dim)); break; - case ParamField::VHeadDim: write_u64(out, static_cast(params.v_head_dim)); break; - case ParamField::CachedKeysInt8Ptr: - case ParamField::CachedValuesInt8Ptr: - case ParamField::CachedKScalesPtr: - case ParamField::CachedVScalesPtr: - throw std::runtime_error("Attempted to serialize runtime-only pointer field"); - } -} - -void read_field(std::istream& in, ParamField field, OpParams& params) { - switch (field) { - case ParamField::Scalar: params.scalar = read_f32(in); break; - case ParamField::Axis: params.axis = static_cast(read_i32(in)); break; - case ParamField::NewShape: params.new_shape = read_size_vector(in); break; - case ParamField::PretransposedRhs: params.pretransposed_rhs = (read_u32(in) != 0); break; - case ParamField::Backend: { - uint32_t backend_val = read_u32(in); - if (backend_val > static_cast(ComputeBackend::NPU)) { - throw std::runtime_error("Graph file corrupted: invalid backend"); - } - params.backend = static_cast(backend_val); - break; - } - case ParamField::SliceStart: params.slice_start = static_cast(read_u64(in)); break; - case ParamField::SliceLength: params.slice_length = static_cast(read_u64(in)); break; - case ParamField::Epsilon: params.epsilon = read_f32(in); break; - case ParamField::NumGroups: params.num_groups = static_cast(read_u64(in)); break; - case ParamField::IndexValue: params.index_value = static_cast(read_u64(in)); break; - case ParamField::Permutation: params.permutation = read_size_vector(in); break; - case ParamField::Scale: params.scale = read_f32(in); break; - case ParamField::Theta: params.theta = read_f32(in); break; - case ParamField::PositionOffset: params.position_offset = static_cast(read_u64(in)); break; - case ParamField::WindowSize: params.window_size = static_cast(read_u64(in)); break; - case ParamField::IsCausal: params.is_causal = (read_u32(in) != 0); break; - case ParamField::AttentionMaskIsAdditive: params.attention_mask_is_additive = (read_u32(in) != 0); break; - case ParamField::LogitCap: params.logit_cap = read_f32(in); break; - case ParamField::TopK: params.top_k = static_cast(read_u64(in)); break; - case ParamField::Temperature: params.temperature = read_f32(in); break; - case ParamField::TopP: params.top_p = read_f32(in); break; - case ParamField::MinP: params.min_p = read_f32(in); break; - case ParamField::RepetitionPenalty: params.repetition_penalty = read_f32(in); break; - case ParamField::RandomSeed: params.random_seed = static_cast(read_u64(in)); break; - case ParamField::BiasIndices: params.bias_indices = read_u32_vector(in); break; - case ParamField::BiasValues: params.bias_values = read_f32_vector(in); break; - case ParamField::NormalizeRouting: params.normalize_routing = (read_u32(in) != 0); break; - case ParamField::NumExperts: params.num_experts = static_cast(read_u64(in)); break; - case ParamField::NumExpertsPerTok: params.num_experts_per_tok = static_cast(read_u64(in)); break; - case ParamField::MoeGated: params.moe_gated = (read_u32(in) != 0); break; - case ParamField::Activation: params.activation = static_cast(read_u32(in)); break; - case ParamField::NumKvHeads: params.num_kv_heads = static_cast(read_u64(in)); break; - case ParamField::ChunkSize: params.chunk_size = static_cast(read_u64(in)); break; - case ParamField::DstHeight: params.dst_height = static_cast(read_u64(in)); break; - case ParamField::DstWidth: params.dst_width = static_cast(read_u64(in)); break; - case ParamField::AlignCorners: params.align_corners = (read_u32(in) != 0); break; - case ParamField::NumClasses: params.num_classes = static_cast(read_u64(in)); break; - case ParamField::NumAltupInputs: params.num_altup_inputs = static_cast(read_u64(in)); break; - case ParamField::KernelSize: params.kernel_size = static_cast(read_u64(in)); break; - case ParamField::Stride: params.stride = static_cast(read_u64(in)); break; - case ParamField::Dilation: params.dilation = static_cast(read_u64(in)); break; - case ParamField::NumFftBins: params.num_fft_bins = static_cast(read_u64(in)); break; - case ParamField::CacheSeqLen: params.cache_seq_len = static_cast(read_u64(in)); break; - case ParamField::HeadDim: params.head_dim = static_cast(read_u64(in)); break; - case ParamField::VHeadDim: params.v_head_dim = static_cast(read_u64(in)); break; - case ParamField::CachedKeysInt8Ptr: - case ParamField::CachedValuesInt8Ptr: - case ParamField::CachedKScalesPtr: - case ParamField::CachedVScalesPtr: - throw std::runtime_error("Graph file corrupted: runtime-only field serialized"); - } -} - -} // namespace - -namespace GraphParamIO { - -void write_op_params(std::ostream& out, OpType op_type, const OpParams& params) { - const auto& schema = op_schema(op_type); - std::vector fields; - fields.reserve(schema.size()); - for (const auto& spec : schema) { - if (spec.persistence == FieldPersistence::Persistent) { - fields.push_back(spec.field); - } - } - - write_u32(out, static_cast(fields.size())); - for (ParamField field : fields) { - write_u32(out, static_cast(field)); - write_field(out, field, params); - } -} - -void read_op_params(std::istream& in, OpType op_type, OpParams& params) { - uint32_t field_count = read_u32(in); - const auto& schema = op_schema(op_type); - std::unordered_set allowed; - for (const auto& spec : schema) { - if (spec.persistence == FieldPersistence::Persistent) { - allowed.insert(static_cast(spec.field)); - } - } - - for (uint32_t i = 0; i < field_count; ++i) { - uint32_t raw_field = read_u32(in); - if (allowed.count(raw_field) == 0) { - throw std::runtime_error("Graph file corrupted: field not allowed for op"); - } - read_field(in, static_cast(raw_field), params); - } -} - -} // namespace GraphParamIO diff --git a/cactus/graph/graph_param_io.h b/cactus/graph/graph_param_io.h deleted file mode 100644 index 0362ea8c5..000000000 --- a/cactus/graph/graph_param_io.h +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once - -#include "graph.h" -#include - -namespace GraphParamIO { - -void write_op_params(std::ostream& out, OpType op_type, const OpParams& params); -void read_op_params(std::istream& in, OpType op_type, OpParams& params); - -} // namespace GraphParamIO diff --git a/cactus/kernel/kernel.h b/cactus/kernel/kernel.h deleted file mode 100644 index 13f4a00be..000000000 --- a/cactus/kernel/kernel.h +++ /dev/null @@ -1,475 +0,0 @@ -#ifndef KERNEL_H -#define KERNEL_H - -#include -#include - -enum class Precision; - -enum class ScalarOpType { - ADD, - SUBTRACT, - MULTIPLY, - DIVIDE, - ABS, - EXP, - POW, - SQRT, - COS, - SIN, - LOG -}; - -constexpr size_t KV_QUANT_GROUP_SIZE = 32; - -void cactus_add_f16(const __fp16* a, const __fp16* b, __fp16* output, size_t num_elements); -void cactus_add_f16_clipped(const __fp16* a, const __fp16* b, __fp16* output, size_t num_elements); -void cactus_subtract_f16(const __fp16* a, const __fp16* b, __fp16* output, size_t num_elements); -void cactus_multiply_f16(const __fp16* a, const __fp16* b, __fp16* output, size_t num_elements); -void cactus_add_scaled_f16(const __fp16* base, const __fp16* src, __fp16* output, size_t num_elements, float scale); -void cactus_divide_f16(const __fp16* a, const __fp16* b, __fp16* output, size_t num_elements); - -void cactus_add_broadcast_f16(const __fp16* a, const __fp16* b, __fp16* output, - const size_t* a_strides, const size_t* b_strides, - const size_t* output_shape, size_t ndim); -void cactus_subtract_broadcast_f16(const __fp16* a, const __fp16* b, __fp16* output, - const size_t* a_strides, const size_t* b_strides, - const size_t* output_shape, size_t ndim); -void cactus_multiply_broadcast_f16(const __fp16* a, const __fp16* b, __fp16* output, - const size_t* a_strides, const size_t* b_strides, - const size_t* output_shape, size_t ndim); -void cactus_divide_broadcast_f16(const __fp16* a, const __fp16* b, __fp16* output, - const size_t* a_strides, const size_t* b_strides, - const size_t* output_shape, size_t ndim); - -void cactus_scalar_op_f16(const __fp16* input, __fp16* output, size_t num_elements, float scalar_value, ScalarOpType op_type); - -void cactus_gemv_int8(const int8_t* A, float A_scale, - const int8_t* B, const __fp16* B_scales, - __fp16* C, size_t K, size_t N, size_t group_size); - -void cactus_gemm_int8(const int8_t* A, const float* A_scales, - const int8_t* B, const __fp16* B_scales, - __fp16* C, size_t M, size_t K, size_t N, size_t group_size); - -void cactus_matmul_int8(const int8_t* A, const float* A_scales, - const int8_t* B, const __fp16* B_scales, - __fp16* C, size_t M, size_t K, size_t N, size_t group_size); - -void cactus_gemv_int8_i8mm(const int8_t* A, float A_scale, - const int8_t* B, const __fp16* B_scales, - __fp16* C, size_t K, size_t N, size_t group_size); - -void cactus_gemm_int8_i8mm(const int8_t* A, const float* A_scales, - const int8_t* B, const __fp16* B_scales, - __fp16* C, size_t M, size_t K, size_t N, size_t group_size); - -void cactus_gemv_int4(const int8_t* A, float A_scale, - const int8_t* B_packed, const __fp16* B_scales, - __fp16* C, size_t K, size_t N, size_t group_size); - -void cactus_gemv_int4_block_range(const int8_t* A, float A_scale, - const int8_t* B_packed, const __fp16* B_scales, - __fp16* C, size_t K, size_t N, size_t group_size, - size_t block_start, size_t block_end); - -void cactus_gemm_int4(const int8_t* A, const float* A_scales, - const int8_t* B_packed, const __fp16* B_scales, - __fp16* C, size_t M, size_t K, size_t N, size_t group_size); - -void cactus_matmul_int4(const int8_t* A, const float* A_scales, - const int8_t* B_packed, const __fp16* B_scales, - __fp16* C, size_t M, size_t K, size_t N, size_t group_size); - -void cactus_matmul_integer(Precision precision, - const int8_t* A, const float* A_scales, - const int8_t* B, const __fp16* B_scales, - __fp16* C, size_t M, size_t K, size_t N, size_t group_size); - -void cactus_matmul_f16(const __fp16* a, const __fp16* b_transposed, __fp16* c, - size_t M, size_t K, size_t N); - -void cactus_transpose_2d_f16(const __fp16* source, __fp16* destination, - size_t num_rows, size_t num_cols, size_t start_row, size_t end_row); -void cactus_transpose_f16(const __fp16* source, __fp16* destination, const size_t* shape, - const size_t* permutation, size_t ndim, size_t start_idx, size_t end_idx); - -double cactus_sum_all_f16(const __fp16* data, size_t num_elements); -void cactus_sum_axis_f16(const __fp16* input, __fp16* output, size_t outer_size, size_t axis_size, size_t inner_size); - -double cactus_mean_all_f16(const __fp16* data, size_t num_elements); -void cactus_mean_axis_f16(const __fp16* input, __fp16* output, size_t outer_size, size_t axis_size, size_t inner_size); - -double cactus_variance_all_f16(const __fp16* data, size_t num_elements); -void cactus_variance_axis_f16(const __fp16* input, __fp16* output, size_t outer_size, size_t axis_size, size_t inner_size); - -__fp16 cactus_min_all_f16(const __fp16* data, size_t num_elements); -void cactus_min_axis_f16(const __fp16* input, __fp16* output, size_t outer_size, size_t axis_size, size_t inner_size); - -__fp16 cactus_max_all_f16(const __fp16* data, size_t num_elements); -void cactus_max_axis_f16(const __fp16* input, __fp16* output, size_t outer_size, size_t axis_size, size_t inner_size); - -void cactus_rms_norm_f16(const __fp16* input, const __fp16* weight, __fp16* output, - size_t batch_size, size_t dims, float eps); - -void cactus_layer_norm_f16(const __fp16* input, const __fp16* weight, const __fp16* bias, - __fp16* output, size_t batch_size, size_t dims, float eps); - -void cactus_rope_f16(const __fp16* input, __fp16* output, size_t batch_size, size_t seq_len, - size_t num_heads, size_t head_dim, size_t start_pos, float theta); - -void cactus_gpt_j_rope_f16(const __fp16* input, __fp16* output, size_t batch_size, size_t seq_len, - size_t num_heads, size_t head_dim, size_t rot_dim, size_t start_pos, float theta); - -void cactus_softmax_f16(const __fp16* input, __fp16* output, size_t batch_size, - size_t seq_len, size_t vocab_size); - -void cactus_relu_f16(const __fp16* input, __fp16* output, size_t num_elements); - -void cactus_leaky_relu_f16(const __fp16* input, __fp16* output, size_t num_elements, float negative_slope); - -void cactus_silu_f16(const __fp16* input, __fp16* output, size_t num_elements); - -void cactus_gelu_f16(const __fp16* input, __fp16* output, size_t num_elements); - -void cactus_gelu_f16_erf(const __fp16* input, __fp16* output, size_t num_elements); - -void cactus_sigmoid_f16(const __fp16* input, __fp16* output, size_t num_elements); - -void cactus_tanh_f16(const __fp16* input, __fp16* output, size_t num_elements); - -void cactus_glu_f16( - const __fp16* input, - __fp16* output, - size_t outer_size, - size_t split_size, - size_t inner_size -); - -void cactus_glu_f32( - const float* input, - float* output, - size_t outer_size, - size_t split_size, - size_t inner_size -); - -void cactus_batchnorm_f16( - const __fp16* input, - const float* weight, - const float* bias, - const float* running_mean, - const float* running_var, - __fp16* output, - size_t outer_size, - size_t channels, - size_t inner_size, - float epsilon -); - -void cactus_batchnorm_f32( - const float* input, - const float* weight, - const float* bias, - const float* running_mean, - const float* running_var, - float* output, - size_t outer_size, - size_t channels, - size_t inner_size, - float epsilon -); - -void cactus_attention_f16(const __fp16* queries, const __fp16* keys, const __fp16* values, __fp16* output, - size_t batch_size, size_t seq_len, size_t kv_seq_len, size_t num_q_heads, size_t num_kv_heads, - size_t head_dim, float scale, const __fp16* mask, size_t position_offset = 0, size_t window_size = 0, - bool is_causal = true, bool mask_is_additive = false, bool mask_per_head = false, - size_t v_head_dim = 0, float logit_cap = 0.0f); - -void cactus_attention_hybrid_int8_fp16( - const __fp16* queries, - const int8_t* keys_cached, - const int8_t* values_cached, - const float* k_scales, - const float* v_scales, - const __fp16* keys_new, - const __fp16* values_new, - __fp16* output, - size_t batch_size, size_t seq_len, size_t cache_len, size_t new_len, - size_t num_q_heads, size_t num_kv_heads, size_t head_dim, - float scale, size_t position_offset = 0, bool is_causal = true, size_t window_size = 0, - size_t group_size = KV_QUANT_GROUP_SIZE, size_t v_head_dim = 0); - -void cactus_gated_deltanet_decode_f16( - const __fp16* q_data, - const __fp16* k_data, - const __fp16* v_data, - const __fp16* g_data, - const __fp16* b_data, - const __fp16* s_data, - __fp16* out, - size_t B, - size_t Hq, - size_t Hv, - size_t K, - size_t V, - float scale); - -void cactus_gated_deltanet_prefill_f16( - const __fp16* q_data, - const __fp16* k_data, - const __fp16* v_data, - const __fp16* g_data, - const __fp16* b_data, - const __fp16* s_data, - __fp16* out, - size_t B, - size_t T, - size_t Hq, - size_t Hv, - size_t K, - size_t V, - size_t requested_chunk_size, - float scale); - -void cactus_conv1d_causal_depthwise_f16( - const __fp16* input, - const __fp16* weight, - __fp16* output, - size_t N, - size_t L, - size_t C, - size_t K, - size_t dilation); - -void cactus_conv1d_f16_k3( - const __fp16* input, - const __fp16* weight, - __fp16* output, - size_t N, - size_t L, - size_t C_in, - size_t C_out, - size_t stride -); - -void cactus_conv1d_f16( - const __fp16* input, - const __fp16* weight, - const __fp16* bias, - __fp16* output, - size_t N, - size_t L, - size_t C_in, - size_t C_out, - size_t K, - size_t stride -); - -void cactus_stft_f16( - const __fp16* input, - const __fp16* weight, - __fp16* output, - size_t N, size_t L, - size_t C_in, size_t C_out, - size_t K, size_t stride, - size_t num_fft_bins -); - -void cactus_conv1d_f16_k7s3_oc8( - const __fp16* input, - const __fp16* Wpack, - const __fp16* bias, - __fp16* output, - size_t N, - size_t L, - size_t C_in, - size_t C_out -); - -void cactus_conv1d_same_depthwise_f16_k9( - const __fp16* input, - const __fp16* weight, - const __fp16* bias, - __fp16* output, - size_t N, - size_t L, - size_t C -); - -void cactus_conv2d_f16_k3s1p1_nchw( - const __fp16* input, - const __fp16* weight, - const __fp16* bias, - __fp16* output, - size_t N, - size_t C_in, - size_t H, - size_t W, - size_t C_out -); - -void cactus_conv2d_f16_k3s2p1_nchw( - const __fp16* input, - const __fp16* weight, - const __fp16* bias, - __fp16* output, - size_t N, - size_t C_in, - size_t H, - size_t W, - size_t C_out -); - -void cactus_conv2d_depthwise_f16_k3s2p1_nchw( - const __fp16* input, - const __fp16* weight, - const __fp16* bias, - __fp16* output, - size_t N, - size_t C, - size_t H, - size_t W -); - -void cactus_conv2d_pointwise_f16_1x1_nchw_gemm( - const __fp16* input, - const __fp16* weight, - const __fp16* bias, - __fp16* output, - size_t N, - size_t C_in, - size_t H, - size_t W, - size_t C_out -); - -void cactus_conv1d_pointwise_f16_gemm( - const __fp16* input, - const __fp16* weight, - const __fp16* bias, - __fp16* output, - size_t N, - size_t L, - size_t C_in, - size_t C_out -); - -void cactus_bilinear_interpolation_f16(const __fp16* input, __fp16* output, size_t src_height, size_t src_width, size_t embed_dim, - size_t dst_height, size_t dst_width, bool align_corners = true); - -void cactus_sample_f32(const float* logits, uint32_t* output, size_t vocab_size, - float temperature, float top_p, size_t top_k, size_t random_seed, - const float* bias_values = nullptr, const uint32_t* bias_indices = nullptr, - size_t bias_count = 0); -void cactus_sample_f16(const __fp16* logits, uint32_t* output, size_t vocab_size, - float temperature, float top_p, size_t top_k, size_t random_seed, - const float* bias_values = nullptr, const uint32_t* bias_indices = nullptr, - size_t bias_count = 0); - -void cactus_sample_f32_ex(const float* logits, uint32_t* output, size_t vocab_size, - float temperature, float top_p, float min_p, float repetition_penalty, - size_t top_k, size_t random_seed, - const float* bias_values = nullptr, const uint32_t* bias_indices = nullptr, - size_t bias_count = 0); -void cactus_sample_f16_ex(const __fp16* logits, uint32_t* output, size_t vocab_size, - float temperature, float top_p, float min_p, float repetition_penalty, - size_t top_k, size_t random_seed, - const float* bias_values = nullptr, const uint32_t* bias_indices = nullptr, - size_t bias_count = 0); - -void cactus_concat_f16(const __fp16* input1, const __fp16* input2, __fp16* output, - const size_t* shape1, const size_t* shape2, const size_t* output_shape, - size_t ndims, int axis); -void cactus_cat_f16(const __fp16** inputs, __fp16* output, const size_t** input_shapes, - const size_t* output_shape, size_t num_inputs, size_t rank, int axis); - -void cactus_int8_to_fp32(const int8_t* src, float* dst, size_t count, float scale = 1.0f); -void cactus_fp32_to_int8(const float* src, int8_t* dst, size_t count, float scale = 1.0f); -void cactus_fp16_to_fp32(const __fp16* src, float* dst, size_t count); -void cactus_fp32_to_fp16(const float* src, __fp16* dst, size_t count); -void cactus_int8_to_fp16(const int8_t* src, __fp16* dst, size_t count, float scale = 1.0f); -void cactus_fp16_to_int8(const __fp16* src, int8_t* dst, size_t count, float scale = 1.0f); -float cactus_fp16_max_abs(const __fp16* src, size_t count); - -void cactus_quantize_kv_fp16_to_int8( - const __fp16* src, - int8_t* dst, - float* scales, - size_t seq_len, size_t kv_heads, size_t head_dim, - size_t group_size = KV_QUANT_GROUP_SIZE); - -inline size_t kv_scales_count(size_t seq_len, size_t kv_heads, size_t head_dim, size_t group_size = KV_QUANT_GROUP_SIZE) { - size_t num_groups = (head_dim + group_size - 1) / group_size; - return seq_len * kv_heads * num_groups; -} - -void cactus_unpack_int4_to_int8(const uint8_t* packed, int8_t* unpacked, size_t unpacked_count); - -void cactus_gaussian_topk_f16( - const __fp16* input, - __fp16* output, - size_t rows, - size_t cols, - float ppf); - -void cactus_altup_predict_f16( - const __fp16* coefs, - const __fp16* const* streams, - __fp16* output, - size_t n, - size_t seq_len, - size_t hidden_dim); - -void cactus_altup_correct_f16( - const __fp16* coefs, - const __fp16* innovation, - const __fp16* const* predictions, - __fp16* output, - size_t n, - size_t seq_len, - size_t hidden_dim); - -void cactus_lstm_cell_f16( - const __fp16* x_input, - const __fp16* h_prev, - const __fp16* c_prev, - const __fp16* weight_ih, - const __fp16* weight_hh, - const __fp16* bias_ih, - const __fp16* bias_hh, - __fp16* h_new, - __fp16* c_new, - size_t batch_size, - size_t input_size, - size_t hidden_size -); - -void cactus_bilstm_sequence_f16( - const __fp16* input, - const __fp16* weight_ih_fwd, - const __fp16* weight_hh_fwd, - const __fp16* bias_ih_fwd, - const __fp16* bias_hh_fwd, - const __fp16* weight_ih_bwd, - const __fp16* weight_hh_bwd, - const __fp16* bias_ih_bwd, - const __fp16* bias_hh_bwd, - __fp16* output, - size_t batch_size, - size_t seq_len, - size_t input_size, - size_t hidden_size -); - -void cactus_maxpool1d_f16( - const __fp16* input, - __fp16* output, - size_t batch_size, - size_t channels, - size_t input_length, - size_t kernel_size, - size_t stride -); - -#endif diff --git a/cactus/kernel/kernel_altup.cpp b/cactus/kernel/kernel_altup.cpp deleted file mode 100644 index f15bf9030..000000000 --- a/cactus/kernel/kernel_altup.cpp +++ /dev/null @@ -1,180 +0,0 @@ -#include "kernel.h" -#include -#include -#include -#include - -void cactus_gaussian_topk_f16( - const __fp16* input, - __fp16* output, - size_t rows, - size_t cols, - float ppf -) { - static constexpr float MAX_SAFE_ABS = 240.0f; - - for (size_t r = 0; r < rows; r++) { - const __fp16* row_in = input + r * cols; - __fp16* row_out = output + r * cols; - - // Pass 1: compute mean and max absolute deviation - float32x4_t sum_vec = vdupq_n_f32(0.0f); - size_t d = 0; - for (; d + 8 <= cols; d += 8) { - float16x8_t v = vld1q_f16(row_in + d); - sum_vec = vaddq_f32(sum_vec, vcvt_f32_f16(vget_low_f16(v))); - sum_vec = vaddq_f32(sum_vec, vcvt_f32_f16(vget_high_f16(v))); - } - float sum = vaddvq_f32(sum_vec); - for (; d < cols; d++) { - sum += static_cast(row_in[d]); - } - float mu = sum / static_cast(cols); - - float32x4_t mu_vec = vdupq_n_f32(mu); - float32x4_t max_abs_vec = vdupq_n_f32(0.0f); - d = 0; - for (; d + 8 <= cols; d += 8) { - float16x8_t v = vld1q_f16(row_in + d); - float32x4_t lo = vsubq_f32(vcvt_f32_f16(vget_low_f16(v)), mu_vec); - float32x4_t hi = vsubq_f32(vcvt_f32_f16(vget_high_f16(v)), mu_vec); - max_abs_vec = vmaxq_f32(max_abs_vec, vabsq_f32(lo)); - max_abs_vec = vmaxq_f32(max_abs_vec, vabsq_f32(hi)); - } - float max_abs = vmaxvq_f32(max_abs_vec); - for (; d < cols; d++) { - float diff = static_cast(row_in[d]) - mu; - float a = diff < 0.0f ? -diff : diff; - if (a > max_abs) max_abs = a; - } - - // Safe scaling: scale = max(max_abs / 240, 1) - float scale = max_abs * (1.0f / MAX_SAFE_ABS); - if (scale < 1.0f) scale = 1.0f; - float inv_scale = 1.0f / scale; - - // Pass 2: compute variance of scaled deviations - float32x4_t inv_scale_vec = vdupq_n_f32(inv_scale); - float32x4_t var_sum_vec = vdupq_n_f32(0.0f); - d = 0; - for (; d + 8 <= cols; d += 8) { - float16x8_t v = vld1q_f16(row_in + d); - float32x4_t lo = vmulq_f32(vsubq_f32(vcvt_f32_f16(vget_low_f16(v)), mu_vec), inv_scale_vec); - float32x4_t hi = vmulq_f32(vsubq_f32(vcvt_f32_f16(vget_high_f16(v)), mu_vec), inv_scale_vec); - var_sum_vec = vfmaq_f32(var_sum_vec, lo, lo); - var_sum_vec = vfmaq_f32(var_sum_vec, hi, hi); - } - float var_sum = vaddvq_f32(var_sum_vec); - for (; d < cols; d++) { - float ds = (static_cast(row_in[d]) - mu) * inv_scale; - var_sum += ds * ds; - } - float variance = var_sum / static_cast(cols); - float sigma = sqrtf(variance) * scale; - - // cutoff = mu + ppf * sigma, output = relu(input - cutoff) - float cutoff = mu + ppf * sigma; - float32x4_t cutoff_vec = vdupq_n_f32(cutoff); - float32x4_t zero_vec = vdupq_n_f32(0.0f); - d = 0; - for (; d + 8 <= cols; d += 8) { - float16x8_t v = vld1q_f16(row_in + d); - float32x4_t lo = vmaxq_f32(vsubq_f32(vcvt_f32_f16(vget_low_f16(v)), cutoff_vec), zero_vec); - float32x4_t hi = vmaxq_f32(vsubq_f32(vcvt_f32_f16(vget_high_f16(v)), cutoff_vec), zero_vec); - vst1q_f16(row_out + d, vcombine_f16(vcvt_f16_f32(lo), vcvt_f16_f32(hi))); - } - for (; d < cols; d++) { - float val = static_cast(row_in[d]) - cutoff; - row_out[d] = static_cast<__fp16>(val > 0.0f ? val : 0.0f); - } - } -} - -void cactus_altup_predict_f16( - const __fp16* coefs, - const __fp16* const* streams, - __fp16* output, - size_t n, - size_t seq_len, - size_t hidden_dim -) { - if (n > 8) throw std::runtime_error("cactus_altup_predict_f16 expects n <= 8"); - const size_t coef_stride = n * n; - - for (size_t i = 0; i < n; i++) { - for (size_t s = 0; s < seq_len; s++) { - const __fp16* coef_row = coefs + s * coef_stride + i * n; - __fp16* out_row = output + (i * seq_len + s) * hidden_dim; - const __fp16* src_i = streams[i] + s * hidden_dim; - - float c[8]; - for (size_t j = 0; j < n; j++) { - c[j] = static_cast(coef_row[j]); - } - - size_t d = 0; - for (; d + 8 <= hidden_dim; d += 8) { - float32x4_t acc_lo = vcvt_f32_f16(vget_low_f16(vld1q_f16(src_i + d))); - float32x4_t acc_hi = vcvt_f32_f16(vget_high_f16(vld1q_f16(src_i + d))); - - for (size_t j = 0; j < n; j++) { - float16x8_t sj = vld1q_f16(streams[j] + s * hidden_dim + d); - float32x4_t cj = vdupq_n_f32(c[j]); - acc_lo = vfmaq_f32(acc_lo, vcvt_f32_f16(vget_low_f16(sj)), cj); - acc_hi = vfmaq_f32(acc_hi, vcvt_f32_f16(vget_high_f16(sj)), cj); - } - - vst1q_f16(out_row + d, vcombine_f16(vcvt_f16_f32(acc_lo), vcvt_f16_f32(acc_hi))); - } - for (; d < hidden_dim; d++) { - float acc = static_cast(src_i[d]); - for (size_t j = 0; j < n; j++) { - acc += c[j] * static_cast(streams[j][s * hidden_dim + d]); - } - out_row[d] = static_cast<__fp16>(acc); - } - } - } -} - -void cactus_altup_correct_f16( - const __fp16* coefs, - const __fp16* innovation, - const __fp16* const* predictions, - __fp16* output, - size_t n, - size_t seq_len, - size_t hidden_dim -) { - for (size_t i = 0; i < n; i++) { - for (size_t s = 0; s < seq_len; s++) { - float ci = static_cast(coefs[s * n + i]); - float32x4_t ci_vec = vdupq_n_f32(ci); - - const __fp16* pred_row = predictions[i] + s * hidden_dim; - const __fp16* innov_row = innovation + s * hidden_dim; - __fp16* out_row = output + (i * seq_len + s) * hidden_dim; - - size_t d = 0; - for (; d + 8 <= hidden_dim; d += 8) { - float16x8_t pred = vld1q_f16(pred_row + d); - float16x8_t innov = vld1q_f16(innov_row + d); - - float32x4_t p_lo = vcvt_f32_f16(vget_low_f16(pred)); - float32x4_t p_hi = vcvt_f32_f16(vget_high_f16(pred)); - float32x4_t i_lo = vcvt_f32_f16(vget_low_f16(innov)); - float32x4_t i_hi = vcvt_f32_f16(vget_high_f16(innov)); - - float32x4_t out_lo = vfmaq_f32(p_lo, i_lo, ci_vec); - float32x4_t out_hi = vfmaq_f32(p_hi, i_hi, ci_vec); - - vst1q_f16(out_row + d, vcombine_f16(vcvt_f16_f32(out_lo), vcvt_f16_f32(out_hi))); - } - for (; d < hidden_dim; d++) { - out_row[d] = static_cast<__fp16>( - static_cast(pred_row[d]) + ci * static_cast(innov_row[d]) - ); - } - } - } -} diff --git a/cactus/kernel/kernel_attention.cpp b/cactus/kernel/kernel_attention.cpp deleted file mode 100644 index ad4dd13a4..000000000 --- a/cactus/kernel/kernel_attention.cpp +++ /dev/null @@ -1,1351 +0,0 @@ -#include "kernel.h" -#include "kernel_utils.h" -#include -#include -#include -#include -#include -#include -#include - -#ifdef __APPLE__ -#include -#endif - -#ifdef __APPLE__ -static void cactus_attention_f16_accelerate( - const __fp16* queries, - const __fp16* keys, - const __fp16* values, - __fp16* output, - size_t batch_size, - size_t seq_len, - size_t kv_seq_len, - size_t num_q_heads, - size_t num_kv_heads, - size_t head_dim, - size_t v_head_dim, - float scale, - size_t position_offset, - bool is_causal -) { - constexpr size_t BLOCK_SIZE = 64; - - const size_t group_size = num_q_heads / num_kv_heads; - const size_t q_batch_stride = seq_len * num_q_heads * head_dim; - const size_t k_batch_stride = kv_seq_len * num_kv_heads * head_dim; - const size_t v_batch_stride = kv_seq_len * num_kv_heads * v_head_dim; - const size_t o_batch_stride = seq_len * num_q_heads * v_head_dim; - const size_t q_seq_stride = num_q_heads * head_dim; - const size_t k_seq_stride = num_kv_heads * head_dim; - const size_t v_seq_stride = num_kv_heads * v_head_dim; - const size_t o_seq_stride = num_q_heads * v_head_dim; - - static constexpr CactusThreading::ParallelConfig ATTENTION_BATCHED{1, 1}; - CactusThreading::parallel_for(batch_size * num_q_heads, ATTENTION_BATCHED, - [&](size_t start, size_t end) { - - std::vector Q_f32(seq_len * head_dim); - std::vector K_f32(BLOCK_SIZE * head_dim); - std::vector V_f32(BLOCK_SIZE * v_head_dim); - std::vector scores(seq_len * BLOCK_SIZE); - std::vector acc(seq_len * v_head_dim); - std::vector row_max(seq_len); - std::vector row_sum(seq_len); - - for (size_t work = start; work < end; ++work) { - const size_t batch = work / num_q_heads; - const size_t q_head = work % num_q_heads; - const size_t kv_head = q_head / group_size; - - for (size_t q = 0; q < seq_len; ++q) { - const __fp16* q_src = queries + batch*q_batch_stride + q*q_seq_stride + q_head*head_dim; - float* q_dst = Q_f32.data() + q * head_dim; - for (size_t d = 0; d < head_dim; d += 8) { - float16x8_t v = vld1q_f16(q_src + d); - vst1q_f32(q_dst + d, vcvt_f32_f16(vget_low_f16(v))); - vst1q_f32(q_dst + d + 4, vcvt_f32_f16(vget_high_f16(v))); - } - } - - std::fill(row_max.begin(), row_max.begin() + seq_len, -INFINITY); - std::fill(row_sum.begin(), row_sum.begin() + seq_len, 0.0f); - memset(acc.data(), 0, seq_len * v_head_dim * sizeof(float)); - - for (size_t kv0 = 0; kv0 < kv_seq_len; kv0 += BLOCK_SIZE) { - const size_t block_len = std::min(BLOCK_SIZE, kv_seq_len - kv0); - - size_t q_start = 0; - size_t active_rows = seq_len; - if (is_causal) { - if (kv0 > position_offset) { - q_start = kv0 - position_offset; - } - if (q_start >= seq_len) continue; - active_rows = seq_len - q_start; - } - - for (size_t i = 0; i < block_len; ++i) { - const __fp16* k_src = keys + batch*k_batch_stride + (kv0+i)*k_seq_stride + kv_head*head_dim; - float* k_dst = K_f32.data() + i * head_dim; - for (size_t d = 0; d < head_dim; d += 8) { - float16x8_t v = vld1q_f16(k_src + d); - vst1q_f32(k_dst + d, vcvt_f32_f16(vget_low_f16(v))); - vst1q_f32(k_dst + d + 4, vcvt_f32_f16(vget_high_f16(v))); - } - } - - cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasTrans, - (int)active_rows, (int)block_len, (int)head_dim, - scale, - Q_f32.data() + q_start * head_dim, (int)head_dim, - K_f32.data(), (int)head_dim, - 0.0f, - scores.data(), (int)block_len); - - for (size_t r = 0; r < active_rows; ++r) { - const size_t q_pos = q_start + r; - const size_t abs_q = position_offset + q_pos; - float* s_row = scores.data() + r * block_len; - - size_t valid_len = block_len; - if (is_causal) { - if (abs_q < kv0) { - memset(s_row, 0, block_len * sizeof(float)); - continue; - } - valid_len = std::min(block_len, abs_q - kv0 + 1); - } - - float32x4_t vmax = vdupq_n_f32(-INFINITY); - size_t j = 0; - for (; j + 4 <= valid_len; j += 4) { - vmax = vmaxq_f32(vmax, vld1q_f32(s_row + j)); - } - float block_max = vmaxvq_f32(vmax); - for (; j < valid_len; ++j) { - block_max = std::max(block_max, s_row[j]); - } - - float prev_max = row_max[q_pos]; - float new_max = std::max(prev_max, block_max); - float scale_old = expf(prev_max - new_max); - - if (prev_max != -INFINITY) { - float* acc_row = acc.data() + q_pos * v_head_dim; - float32x4_t sv = vdupq_n_f32(scale_old); - for (size_t d = 0; d < v_head_dim; d += 4) { - float32x4_t a = vld1q_f32(acc_row + d); - vst1q_f32(acc_row + d, vmulq_f32(a, sv)); - } - } - row_sum[q_pos] = row_sum[q_pos] * scale_old; - row_max[q_pos] = new_max; - - float32x4_t vsum = vdupq_n_f32(0.0f); - float32x4_t vnmax = vdupq_n_f32(new_max); - float32x4_t log2e = vdupq_n_f32(1.442695f); - j = 0; - for (; j + 4 <= valid_len; j += 4) { - float32x4_t x = vmulq_f32(vsubq_f32(vld1q_f32(s_row + j), vnmax), log2e); - float32x4_t x_floor = vrndmq_f32(x); - int32x4_t xi = vcvtq_s32_f32(x_floor); - float32x4_t xf = vsubq_f32(x, x_floor); - float32x4_t t = vfmaq_n_f32(vdupq_n_f32(0.2246932f), xf, 0.0789673f); - t = vfmaq_f32(vdupq_n_f32(0.6963248f), t, xf); - float32x4_t y = vfmaq_f32(vdupq_n_f32(0.9999003f), t, xf); - xi = vshlq_n_s32(vaddq_s32(xi, vdupq_n_s32(127)), 23); - y = vmulq_f32(y, vreinterpretq_f32_s32(xi)); - uint32x4_t underflow = vcltq_f32(x, vdupq_n_f32(-126.0f)); - y = vbslq_f32(underflow, vdupq_n_f32(0.0f), y); - vst1q_f32(s_row + j, y); - vsum = vaddq_f32(vsum, y); - } - float block_sum = vaddvq_f32(vsum); - for (; j < valid_len; ++j) { - s_row[j] = expf(s_row[j] - new_max); - block_sum += s_row[j]; - } - if (valid_len < block_len) { - memset(s_row + valid_len, 0, (block_len - valid_len) * sizeof(float)); - } - row_sum[q_pos] += block_sum; - } - - for (size_t i = 0; i < block_len; ++i) { - const __fp16* v_src = values + batch*v_batch_stride + (kv0+i)*v_seq_stride + kv_head*v_head_dim; - float* v_dst = V_f32.data() + i * v_head_dim; - for (size_t d = 0; d < v_head_dim; d += 8) { - float16x8_t v = vld1q_f16(v_src + d); - vst1q_f32(v_dst + d, vcvt_f32_f16(vget_low_f16(v))); - vst1q_f32(v_dst + d + 4, vcvt_f32_f16(vget_high_f16(v))); - } - } - - cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans, - (int)active_rows, (int)v_head_dim, (int)block_len, - 1.0f, - scores.data(), (int)block_len, - V_f32.data(), (int)v_head_dim, - 1.0f, - acc.data() + q_start * v_head_dim, (int)v_head_dim); - } - - for (size_t q = 0; q < seq_len; ++q) { - __fp16* o = output + batch*o_batch_stride + q*o_seq_stride + q_head*v_head_dim; - float sum = row_sum[q]; - if (sum == 0.0f) { - memset(o, 0, v_head_dim * sizeof(__fp16)); - continue; - } - float inv = 1.0f / sum; - float32x4_t invv = vdupq_n_f32(inv); - float* acc_row = acc.data() + q * v_head_dim; - for (size_t d = 0; d < v_head_dim; d += 8) { - float32x4_t a0 = vmulq_f32(vld1q_f32(acc_row + d), invv); - float32x4_t a1 = vmulq_f32(vld1q_f32(acc_row + d + 4), invv); - vst1q_f16(o + d, vcombine_f16(vcvt_f16_f32(a0), vcvt_f16_f32(a1))); - } - } - } - }); -} -#endif - -static inline void cactus_attention_f16_fast( - const __fp16* queries, - const __fp16* keys, - const __fp16* values, - __fp16* output, - size_t batch_size, - size_t seq_len, - size_t kv_seq_len, - size_t num_q_heads, - size_t num_kv_heads, - size_t head_dim, - float scale, - size_t position_offset, - bool is_causal, - size_t window_size, - size_t v_head_dim -) { - constexpr size_t BLOCK_SIZE = 32; - const size_t qk_nblocks = head_dim / 8; - const size_t v_nblocks = v_head_dim / 8; - -#ifdef __APPLE__ - if (seq_len >= 64 && window_size == 0) { - cactus_attention_f16_accelerate( - queries, keys, values, output, - batch_size, seq_len, kv_seq_len, - num_q_heads, num_kv_heads, head_dim, v_head_dim, - scale, position_offset, is_causal - ); - return; - } -#endif - - const size_t group_size = num_q_heads / num_kv_heads; - const size_t q_batch_stride = seq_len * num_q_heads * head_dim; - const size_t kv_batch_stride = kv_seq_len * num_kv_heads * head_dim; - const size_t v_batch_stride = kv_seq_len * num_kv_heads * v_head_dim; - const size_t o_batch_stride = seq_len * num_q_heads * v_head_dim; - const size_t q_seq_stride = num_q_heads * head_dim; - const size_t kv_seq_stride = num_kv_heads * head_dim; - const size_t v_seq_stride = num_kv_heads * v_head_dim; - const size_t o_seq_stride = num_q_heads * v_head_dim; - - CactusThreading::parallel_for(batch_size * num_q_heads * seq_len, CactusThreading::Thresholds::ATTENTION, - [&](size_t start, size_t end) { - - float block_scores[BLOCK_SIZE]; - std::vector acc_lo(v_nblocks), acc_hi(v_nblocks); - - for (size_t work = start; work < end; ++work) { - const size_t batch = work / (num_q_heads * seq_len); - const size_t rem = work % (num_q_heads * seq_len); - const size_t q_head = rem / seq_len; - const size_t q_pos = rem % seq_len; - const size_t kv_head = q_head / group_size; - - const __fp16* q = queries + batch*q_batch_stride + q_pos*q_seq_stride + q_head*head_dim; - __fp16* o = output + batch*o_batch_stride + q_pos*o_seq_stride + q_head*v_head_dim; - - for (size_t i = 0; i < v_nblocks; i++) { - acc_lo[i] = vdupq_n_f32(0.f); - acc_hi[i] = vdupq_n_f32(0.f); - } - - float running_max = -INFINITY; - float running_sum = 0.f; - - const size_t abs_q = position_offset + q_pos; - size_t kv_end = is_causal ? std::min(kv_seq_len, abs_q + 1) : kv_seq_len; - size_t kv_start = (window_size > 0 && abs_q > window_size) ? abs_q - window_size : 0; - - for (size_t kv0 = kv_start; kv0 < kv_end; kv0 += BLOCK_SIZE) { - const size_t kv1 = std::min(kv0 + BLOCK_SIZE, kv_end); - float block_max = -INFINITY; - - for (size_t i = kv0; i < kv1; i++) { - float32x4_t s0 = vdupq_n_f32(0.f); - float32x4_t s1 = vdupq_n_f32(0.f); - - const __fp16* k = keys + batch*kv_batch_stride + i*kv_seq_stride + kv_head*head_dim; - - for (size_t d = 0; d < qk_nblocks; d++) { - float16x8_t qv = vld1q_f16(q + d*8); - float16x8_t kv = vld1q_f16(k + d*8); - - float32x4_t ql = vcvt_f32_f16(vget_low_f16(qv)); - float32x4_t qh = vcvt_f32_f16(vget_high_f16(qv)); - float32x4_t kl = vcvt_f32_f16(vget_low_f16(kv)); - float32x4_t kh = vcvt_f32_f16(vget_high_f16(kv)); - - s0 = vfmaq_f32(s0, ql, kl); - s1 = vfmaq_f32(s1, qh, kh); - } - - float score = vaddvq_f32(vaddq_f32(s0, s1)) * scale; - block_scores[i - kv0] = score; - block_max = std::max(block_max, score); - } - - float current_block_scale = 1.0f; - if (block_max > running_max) { - float scale_correction = expf(running_max - block_max); - running_sum *= scale_correction; - - for (size_t d = 0; d < v_nblocks; d++) { - acc_lo[d] = vmulq_n_f32(acc_lo[d], scale_correction); - acc_hi[d] = vmulq_n_f32(acc_hi[d], scale_correction); - } - running_max = block_max; - } else { - current_block_scale = expf(block_max - running_max); - } - - float block_sum = 0.f; - for (size_t i = 0; i < kv1 - kv0; i++) { - block_scores[i] = expf(block_scores[i] - block_max); - block_sum += block_scores[i]; - } - - for (size_t i = 0; i < kv1 - kv0; i++) { - const float attn_weight = block_scores[i] * current_block_scale; - if (attn_weight == 0.f) continue; - - const __fp16* v = values + batch*v_batch_stride + (kv0+i)*v_seq_stride + kv_head*v_head_dim; - float32x4_t wv = vdupq_n_f32(attn_weight); - - for (size_t d = 0; d < v_nblocks; d++) { - float16x8_t vv = vld1q_f16(v + d*8); - acc_lo[d] = vfmaq_f32(acc_lo[d], vcvt_f32_f16(vget_low_f16(vv)), wv); - acc_hi[d] = vfmaq_f32(acc_hi[d], vcvt_f32_f16(vget_high_f16(vv)), wv); - } - } - - running_sum += block_sum * current_block_scale; - } - - if (running_sum == 0.f) { - memset(o, 0, v_head_dim * sizeof(__fp16)); - continue; - } - - float inv = 1.f / running_sum; - float32x4_t invv = vdupq_n_f32(inv); - - for (size_t d = 0; d < v_nblocks; d++) { - float16x8_t out = vcombine_f16( - vcvt_f16_f32(vmulq_f32(acc_lo[d], invv)), - vcvt_f16_f32(vmulq_f32(acc_hi[d], invv)) - ); - vst1q_f16(o + d*8, out); - } - } - }); -} - -void cactus_attention_f16( - const __fp16* queries, - const __fp16* keys, - const __fp16* values, - __fp16* output, - size_t batch_size, - size_t seq_len, - size_t kv_seq_len, - size_t num_q_heads, - size_t num_kv_heads, - size_t head_dim, - float scale, - const __fp16* mask, - size_t position_offset, - size_t window_size, - bool is_causal, - bool mask_is_additive, - bool mask_per_head, - size_t v_head_dim, - float logit_cap -) { - if (v_head_dim == 0) v_head_dim = head_dim; - if (scale == 0.0f) { - scale = 1.0f / sqrtf(static_cast(head_dim)); - } - - if (mask == nullptr && head_dim % 8 == 0 && v_head_dim % 8 == 0 && logit_cap == 0.0f) { - cactus_attention_f16_fast( - queries, keys, values, output, - batch_size, seq_len, kv_seq_len, - num_q_heads, num_kv_heads, head_dim, - scale, position_offset, is_causal, window_size, v_head_dim - ); - return; - } - - constexpr size_t VECTOR_WIDTH = 8; - constexpr size_t BLOCK_SIZE = 32; - const size_t head_dim_aligned = (head_dim / VECTOR_WIDTH) * VECTOR_WIDTH; - const size_t v_head_dim_aligned = (v_head_dim / VECTOR_WIDTH) * VECTOR_WIDTH; - - const size_t group_size = num_q_heads / num_kv_heads; - - const size_t q_batch_stride = seq_len * num_q_heads * head_dim; - const size_t kv_batch_stride = kv_seq_len * num_kv_heads * head_dim; - const size_t v_batch_stride = kv_seq_len * num_kv_heads * v_head_dim; - const size_t o_batch_stride = seq_len * num_q_heads * v_head_dim; - const size_t q_seq_stride = num_q_heads * head_dim; - const size_t kv_seq_stride = num_kv_heads * head_dim; - const size_t v_seq_stride = num_kv_heads * v_head_dim; - const size_t o_seq_stride = num_q_heads * v_head_dim; - const size_t mask_batch_stride = mask - ? (mask_per_head ? (num_q_heads * seq_len * kv_seq_len) : (seq_len * kv_seq_len)) - : 0; - - CactusThreading::parallel_for(batch_size * num_q_heads * seq_len, CactusThreading::Thresholds::ATTENTION, - [=](size_t start_idx, size_t end_idx) { - std::vector block_scores(BLOCK_SIZE); - std::vector output_accum_low(v_head_dim_aligned / VECTOR_WIDTH * 2); - std::vector output_accum_high(v_head_dim_aligned / VECTOR_WIDTH * 2); - - const size_t v_tail_dims = v_head_dim - v_head_dim_aligned; - std::vector output_accum_tail(v_tail_dims, 0.0f); - - const float NEG_INF = -std::numeric_limits::infinity(); - const size_t used_vec_blocks = v_head_dim_aligned / VECTOR_WIDTH; - - for (size_t work_idx = start_idx; work_idx < end_idx; ++work_idx) { - const size_t batch_idx = work_idx / (num_q_heads * seq_len); - const size_t remainder = work_idx % (num_q_heads * seq_len); - const size_t q_head_idx = remainder / seq_len; - const size_t q_pos = remainder % seq_len; - - const size_t kv_head_idx = q_head_idx / group_size; - - const __fp16* Q_base = queries + batch_idx * q_batch_stride; - const __fp16* K_base = keys + batch_idx * kv_batch_stride; - const __fp16* V_base = values + batch_idx * v_batch_stride; - __fp16* O_base = output + batch_idx * o_batch_stride; - const __fp16* M = mask ? (mask + batch_idx * mask_batch_stride) : nullptr; - const __fp16* q_vec = Q_base + q_pos * q_seq_stride + q_head_idx * head_dim; - __fp16* o_vec = O_base + q_pos * o_seq_stride + q_head_idx * v_head_dim; - - float running_max = -std::numeric_limits::infinity(); - float running_sum = 0.0f; - - for (size_t i = 0; i < output_accum_low.size(); ++i) { - output_accum_low[i] = vdupq_n_f32(0.0f); - output_accum_high[i] = vdupq_n_f32(0.0f); - } - for (size_t i = 0; i < v_tail_dims; ++i) { - output_accum_tail[i] = 0.0f; - } - - const bool is_decode = (q_pos == seq_len - 1) && seq_len > 1; - const size_t absolute_q_pos = position_offset + q_pos; - - size_t kv_start = 0; - size_t kv_end = kv_seq_len; - - if (window_size > 0 && window_size < kv_seq_len) { - if (absolute_q_pos > window_size) { - kv_start = absolute_q_pos - window_size; - } - if (is_causal) { - kv_end = std::min(kv_end, absolute_q_pos + 1); - } - } else if (is_causal) { - kv_end = std::min(kv_end, absolute_q_pos + 1); - } - - for (size_t kv_block_start = kv_start; kv_block_start < kv_end; kv_block_start += BLOCK_SIZE) { - const size_t kv_block_end = std::min(kv_block_start + BLOCK_SIZE, kv_end); - const size_t block_size = kv_block_end - kv_block_start; - - float block_max = -std::numeric_limits::infinity(); - - if (!is_decode && is_causal && kv_block_start > absolute_q_pos) { - for (size_t kv_idx = 0; kv_idx < block_size; ++kv_idx) { - block_scores[kv_idx] = NEG_INF; - } - continue; - } - - for (size_t kv_idx = 0; kv_idx < block_size; ++kv_idx) { - const size_t kv_pos = kv_block_start + kv_idx; - - if (!is_decode && is_causal && kv_pos > absolute_q_pos) { - block_scores[kv_idx] = NEG_INF; - continue; - } - - const __fp16* k_vec = K_base + kv_pos * kv_seq_stride + kv_head_idx * head_dim; - - if (kv_idx + 1 < block_size) { - const __fp16* next_k_vec = K_base + (kv_pos + 1) * kv_seq_stride + kv_head_idx * head_dim; - __builtin_prefetch(next_k_vec, 0, 1); - } - - float32x4_t score_accum_low = vdupq_n_f32(0.0f); - float32x4_t score_accum_high = vdupq_n_f32(0.0f); - - for (size_t dim_block = 0; dim_block < head_dim_aligned; dim_block += VECTOR_WIDTH) { - float16x8_t q_vec_f16 = vld1q_f16(&q_vec[dim_block]); - float16x8_t k_vec_f16 = vld1q_f16(&k_vec[dim_block]); - - float32x4_t q_low = vcvt_f32_f16(vget_low_f16(q_vec_f16)); - float32x4_t q_high = vcvt_f32_f16(vget_high_f16(q_vec_f16)); - float32x4_t k_low = vcvt_f32_f16(vget_low_f16(k_vec_f16)); - float32x4_t k_high = vcvt_f32_f16(vget_high_f16(k_vec_f16)); - - score_accum_low = vfmaq_f32(score_accum_low, q_low, k_low); - score_accum_high = vfmaq_f32(score_accum_high, q_high, k_high); - } - - float score = vaddvq_f32(vaddq_f32(score_accum_low, score_accum_high)); - - for (size_t dim = head_dim_aligned; dim < head_dim; ++dim) { - score += static_cast(q_vec[dim]) * static_cast(k_vec[dim]); - } - - score *= scale; - - size_t absolute_q_pos = position_offset + q_pos; - - if (is_causal && kv_pos > absolute_q_pos) { - score = NEG_INF; - } - else if (window_size > 0 && kv_pos < absolute_q_pos && (absolute_q_pos - kv_pos) > window_size) { - score = NEG_INF; - } - else if (M) { - const size_t mask_index = mask_per_head - ? ((q_head_idx * seq_len + q_pos) * kv_seq_len + kv_pos) - : (q_pos * kv_seq_len + kv_pos); - const float mask_value = static_cast(M[mask_index]); - if (mask_is_additive) { - if (!std::isfinite(mask_value)) { - score = NEG_INF; - } else { - score += mask_value; - } - } else if (mask_value == 0.0f) { - score = NEG_INF; - } - } - - if (logit_cap > 0.0f && std::isfinite(score)) { - score = logit_cap * tanhf(score / logit_cap); - } - - block_scores[kv_idx] = score; - block_max = std::max(block_max, score); - } - - float current_block_scale = 1.0f; - - if (block_max > NEG_INF) { - if (block_max > running_max) { - float scale_correction = expf(running_max - block_max); - running_sum *= scale_correction; - - for (size_t i = 0; i < used_vec_blocks; ++i) { - output_accum_low[i] = vmulq_n_f32(output_accum_low[i], scale_correction); - output_accum_high[i] = vmulq_n_f32(output_accum_high[i], scale_correction); - } - for (size_t i = 0; i < v_tail_dims; ++i) { - output_accum_tail[i] *= scale_correction; - } - running_max = block_max; - } else { - current_block_scale = expf(block_max - running_max); - } - } - - float block_sum = 0.0f; - const size_t vec_size = (block_size / 4) * 4; - - for (size_t kv_idx = 0; kv_idx < vec_size; kv_idx += 4) { - float32x4_t scores = vld1q_f32(&block_scores[kv_idx]); - uint32x4_t inf_mask = vceqq_f32(scores, vdupq_n_f32(NEG_INF)); - - float32x4_t x = vsubq_f32(scores, vdupq_n_f32(block_max)); - x = vmulq_n_f32(x, 1.442695f); - float32x4_t x_floor = vrndmq_f32(x); - int32x4_t xi = vcvtq_s32_f32(x_floor); - float32x4_t xf = vsubq_f32(x, x_floor); - - float32x4_t t = vfmaq_n_f32(vdupq_n_f32(0.2246932f), xf, 0.0789673f); - t = vfmaq_f32(vdupq_n_f32(0.6963248f), t, xf); - float32x4_t y = vfmaq_f32(vdupq_n_f32(0.9999003f), t, xf); - - xi = vaddq_s32(xi, vdupq_n_s32(127)); - xi = vshlq_n_s32(xi, 23); - y = vmulq_f32(y, vreinterpretq_f32_s32(xi)); - - uint32x4_t underflow_mask = vcltq_f32(x, vdupq_n_f32(-126.0f)); - uint32x4_t zero_mask = vorrq_u32(inf_mask, underflow_mask); - y = vbslq_f32(zero_mask, vdupq_n_f32(0.0f), y); - - vst1q_f32(&block_scores[kv_idx], y); - block_sum += vaddvq_f32(y); - } - - for (size_t kv_idx = vec_size; kv_idx < block_size; ++kv_idx) { - if (block_scores[kv_idx] != NEG_INF) { - block_scores[kv_idx] = expf(block_scores[kv_idx] - block_max); - block_sum += block_scores[kv_idx]; - } else { - block_scores[kv_idx] = 0.0f; - } - } - - for (size_t kv_idx = 0; kv_idx < block_size; ++kv_idx) { - const float attn_weight = block_scores[kv_idx] * current_block_scale; - if (attn_weight == 0.0f) continue; - - const size_t kv_pos = kv_block_start + kv_idx; - const __fp16* v_vec = V_base + kv_pos * v_seq_stride + kv_head_idx * v_head_dim; - - const float32x4_t weight_vec = vdupq_n_f32(attn_weight); - - for (size_t dim_block = 0; dim_block < v_head_dim_aligned; dim_block += VECTOR_WIDTH) { - float16x8_t v_vec_f16 = vld1q_f16(&v_vec[dim_block]); - float32x4_t v_low = vcvt_f32_f16(vget_low_f16(v_vec_f16)); - float32x4_t v_high = vcvt_f32_f16(vget_high_f16(v_vec_f16)); - - size_t idx = dim_block / VECTOR_WIDTH; - output_accum_low[idx] = vfmaq_f32(output_accum_low[idx], v_low, weight_vec); - output_accum_high[idx] = vfmaq_f32(output_accum_high[idx], v_high, weight_vec); - } - - for (size_t dim = v_head_dim_aligned; dim < v_head_dim; ++dim) { - float val = attn_weight * static_cast(v_vec[dim]); - output_accum_tail[dim - v_head_dim_aligned] += val; - } - } - - running_sum += block_sum * current_block_scale; - } - - if (running_sum > 0.0f) { - const float inv_sum = 1.0f / running_sum; - const float32x4_t inv_sum_vec = vdupq_n_f32(inv_sum); - - for (size_t dim_block = 0; dim_block < v_head_dim_aligned; dim_block += VECTOR_WIDTH) { - size_t idx = dim_block / VECTOR_WIDTH; - float32x4_t final_low = vmulq_f32(output_accum_low[idx], inv_sum_vec); - float32x4_t final_high = vmulq_f32(output_accum_high[idx], inv_sum_vec); - - float16x4_t low_f16 = vcvt_f16_f32(final_low); - float16x4_t high_f16 = vcvt_f16_f32(final_high); - float16x8_t combined = vcombine_f16(low_f16, high_f16); - - vst1q_f16(&o_vec[dim_block], combined); - } - - for (size_t dim = v_head_dim_aligned; dim < v_head_dim; ++dim) { - o_vec[dim] = static_cast<__fp16>(output_accum_tail[dim - v_head_dim_aligned] * inv_sum); - } - } else { - for (size_t dim = 0; dim < v_head_dim; ++dim) { - o_vec[dim] = static_cast<__fp16>(0.0f); - } - } - } - }); -} - -void cactus_attention_hybrid_int8_fp16( - const __fp16* queries, - const int8_t* keys_cached, - const int8_t* values_cached, - const float* k_scales, - const float* v_scales, - const __fp16* keys_new, - const __fp16* values_new, - __fp16* output, - size_t batch_size, - size_t seq_len, - size_t cache_len, - size_t new_len, - size_t num_q_heads, - size_t num_kv_heads, - size_t head_dim, - float scale, - size_t position_offset, - bool is_causal, - size_t window_size, - size_t quant_group_size, - size_t v_head_dim -) { - if (v_head_dim == 0) v_head_dim = head_dim; - if (scale == 0.0f) { - scale = 1.0f / sqrtf(static_cast(head_dim)); - } - - const size_t kv_seq_len = cache_len + new_len; - - constexpr size_t VECTOR_WIDTH = 8; - constexpr size_t BLOCK_SIZE = 32; - const size_t head_dim_aligned = (head_dim / VECTOR_WIDTH) * VECTOR_WIDTH; - const size_t v_head_dim_aligned = (v_head_dim / VECTOR_WIDTH) * VECTOR_WIDTH; - - const size_t gqa_group_size = num_q_heads / num_kv_heads; - const size_t num_quant_groups_k = (head_dim + quant_group_size - 1) / quant_group_size; - const size_t num_quant_groups_v = (v_head_dim + quant_group_size - 1) / quant_group_size; - - const size_t q_batch_stride = seq_len * num_q_heads * head_dim; - const size_t k_cached_batch_stride = cache_len * num_kv_heads * head_dim; - const size_t v_cached_batch_stride = cache_len * num_kv_heads * v_head_dim; - const size_t k_new_batch_stride = new_len * num_kv_heads * head_dim; - const size_t v_new_batch_stride = new_len * num_kv_heads * v_head_dim; - const size_t o_batch_stride = seq_len * num_q_heads * v_head_dim; - const size_t q_seq_stride = num_q_heads * head_dim; - const size_t k_seq_stride = num_kv_heads * head_dim; - const size_t v_seq_stride = num_kv_heads * v_head_dim; - const size_t o_seq_stride = num_q_heads * v_head_dim; - - CactusThreading::parallel_for(batch_size * num_q_heads * seq_len, CactusThreading::Thresholds::ATTENTION, - [=](size_t start_idx, size_t end_idx) { - std::vector block_scores(BLOCK_SIZE); - std::vector output_accum_low(v_head_dim_aligned / VECTOR_WIDTH * 2); - std::vector output_accum_high(v_head_dim_aligned / VECTOR_WIDTH * 2); - - for (size_t work_idx = start_idx; work_idx < end_idx; ++work_idx) { - const size_t batch_idx = work_idx / (num_q_heads * seq_len); - const size_t remainder = work_idx % (num_q_heads * seq_len); - const size_t q_head_idx = remainder / seq_len; - const size_t q_pos = remainder % seq_len; - - const size_t kv_head_idx = q_head_idx / gqa_group_size; - - const __fp16* Q_base = queries + batch_idx * q_batch_stride; - const int8_t* K_cached_base = keys_cached + batch_idx * k_cached_batch_stride; - const int8_t* V_cached_base = values_cached + batch_idx * v_cached_batch_stride; - const __fp16* K_new_base = keys_new + batch_idx * k_new_batch_stride; - const __fp16* V_new_base = values_new + batch_idx * v_new_batch_stride; - __fp16* O_base = output + batch_idx * o_batch_stride; - - const __fp16* q_vec = Q_base + q_pos * q_seq_stride + q_head_idx * head_dim; - __fp16* o_vec = O_base + q_pos * o_seq_stride + q_head_idx * v_head_dim; - - float running_max = -std::numeric_limits::infinity(); - float running_sum = 0.0f; - - for (size_t i = 0; i < output_accum_low.size(); ++i) { - output_accum_low[i] = vdupq_n_f32(0.0f); - output_accum_high[i] = vdupq_n_f32(0.0f); - } - - const size_t absolute_q_pos = position_offset + q_pos; - size_t kv_end = is_causal ? std::min(kv_seq_len, cache_len + q_pos + 1) : kv_seq_len; - - size_t kv_start = 0; - if (window_size > 0 && absolute_q_pos > window_size) { - kv_start = absolute_q_pos - window_size; - } - - constexpr size_t SINK_SIZE = 4; - const size_t cache_abs_offset = (position_offset >= cache_len) ? (position_offset - cache_len) : 0; - - const size_t kv_block_start0 = (window_size > 0 && kv_start > 0) ? 0 - : (kv_start / BLOCK_SIZE) * BLOCK_SIZE; - - for (size_t kv_block_start = kv_block_start0; kv_block_start < kv_end; kv_block_start += BLOCK_SIZE) { - const size_t kv_block_end = std::min(kv_block_start + BLOCK_SIZE, kv_end); - const size_t block_size = kv_block_end - kv_block_start; - - float block_max = -std::numeric_limits::infinity(); - - for (size_t kv_idx = 0; kv_idx < block_size; ++kv_idx) { - const size_t kv_pos = kv_block_start + kv_idx; - - bool window_masked = false; - if (window_size > 0 && kv_start > 0) { - if (kv_pos < cache_len) { - if (cache_abs_offset == 0 || kv_pos >= SINK_SIZE) { - window_masked = (cache_abs_offset + kv_pos < kv_start); - } - } else { - window_masked = (kv_pos + cache_abs_offset < kv_start); - } - } - - if ((is_causal && kv_pos > absolute_q_pos) || window_masked) { - block_scores[kv_idx] = -std::numeric_limits::infinity(); - continue; - } - - float32x4_t score_accum_low = vdupq_n_f32(0.0f); - float32x4_t score_accum_high = vdupq_n_f32(0.0f); - - if (kv_pos < cache_len) { - const int8_t* k_vec = K_cached_base + kv_pos * k_seq_stride + kv_head_idx * head_dim; - const float* k_scale_base = k_scales + (kv_pos * num_kv_heads + kv_head_idx) * num_quant_groups_k; - - for (size_t quant_group = 0; quant_group < num_quant_groups_k; quant_group++) { - const size_t dim_base = quant_group * quant_group_size; - const float k_scale = k_scale_base[quant_group]; - const float32x4_t k_scale_vec = vdupq_n_f32(k_scale); - - #pragma unroll - for (size_t i = 0; i < 4; i++) { - const size_t dim_block = dim_base + i * VECTOR_WIDTH; - if (dim_block >= head_dim_aligned) break; - - float16x8_t q_vec_f16 = vld1q_f16(&q_vec[dim_block]); - float32x4_t q_low = vcvt_f32_f16(vget_low_f16(q_vec_f16)); - float32x4_t q_high = vcvt_f32_f16(vget_high_f16(q_vec_f16)); - - int8x8_t k_vec_i8 = vld1_s8(&k_vec[dim_block]); - int16x8_t k_vec_i16 = vmovl_s8(k_vec_i8); - float32x4_t k_low = vmulq_f32(vcvtq_f32_s32(vmovl_s16(vget_low_s16(k_vec_i16))), k_scale_vec); - float32x4_t k_high = vmulq_f32(vcvtq_f32_s32(vmovl_s16(vget_high_s16(k_vec_i16))), k_scale_vec); - - score_accum_low = vfmaq_f32(score_accum_low, q_low, k_low); - score_accum_high = vfmaq_f32(score_accum_high, q_high, k_high); - } - } - } else { - const size_t new_pos = kv_pos - cache_len; - const __fp16* k_vec = K_new_base + new_pos * k_seq_stride + kv_head_idx * head_dim; - - for (size_t dim_block = 0; dim_block < head_dim_aligned; dim_block += VECTOR_WIDTH) { - float16x8_t q_vec_f16 = vld1q_f16(&q_vec[dim_block]); - float16x8_t k_vec_f16 = vld1q_f16(&k_vec[dim_block]); - - float32x4_t q_low = vcvt_f32_f16(vget_low_f16(q_vec_f16)); - float32x4_t q_high = vcvt_f32_f16(vget_high_f16(q_vec_f16)); - float32x4_t k_low = vcvt_f32_f16(vget_low_f16(k_vec_f16)); - float32x4_t k_high = vcvt_f32_f16(vget_high_f16(k_vec_f16)); - - score_accum_low = vfmaq_f32(score_accum_low, q_low, k_low); - score_accum_high = vfmaq_f32(score_accum_high, q_high, k_high); - } - } - - float score = vaddvq_f32(vaddq_f32(score_accum_low, score_accum_high)) * scale; - block_scores[kv_idx] = score; - block_max = std::max(block_max, score); - } - - float current_block_scale = 1.0f; - if (block_max > -std::numeric_limits::infinity()) { - if (block_max > running_max) { - float scale_correction = expf(running_max - block_max); - running_sum *= scale_correction; - - for (size_t i = 0; i < output_accum_low.size() / 2; ++i) { - output_accum_low[i] = vmulq_n_f32(output_accum_low[i], scale_correction); - output_accum_high[i] = vmulq_n_f32(output_accum_high[i], scale_correction); - } - running_max = block_max; - } else { - current_block_scale = expf(block_max - running_max); - } - } - - float block_sum = 0.0f; - for (size_t kv_idx = 0; kv_idx < block_size; ++kv_idx) { - if (block_scores[kv_idx] != -std::numeric_limits::infinity()) { - block_scores[kv_idx] = expf(block_scores[kv_idx] - block_max); - block_sum += block_scores[kv_idx]; - } else { - block_scores[kv_idx] = 0.0f; - } - } - - for (size_t kv_idx = 0; kv_idx < block_size; ++kv_idx) { - const float attn_weight = block_scores[kv_idx] * current_block_scale; - if (attn_weight == 0.0f) continue; - - const size_t kv_pos = kv_block_start + kv_idx; - const float32x4_t weight_vec = vdupq_n_f32(attn_weight); - - if (kv_pos < cache_len) { - const int8_t* v_vec = V_cached_base + kv_pos * v_seq_stride + kv_head_idx * v_head_dim; - const float* v_scale_base = v_scales + (kv_pos * num_kv_heads + kv_head_idx) * num_quant_groups_v; - - for (size_t quant_group = 0; quant_group < num_quant_groups_v; quant_group++) { - const size_t dim_base = quant_group * quant_group_size; - const float v_scale = v_scale_base[quant_group]; - const float32x4_t v_scale_vec = vdupq_n_f32(v_scale); - - #pragma unroll - for (size_t i = 0; i < 4; i++) { - const size_t dim_block = dim_base + i * VECTOR_WIDTH; - if (dim_block >= v_head_dim_aligned) break; - - int8x8_t v_vec_i8 = vld1_s8(&v_vec[dim_block]); - int16x8_t v_vec_i16 = vmovl_s8(v_vec_i8); - float32x4_t v_low = vmulq_f32(vcvtq_f32_s32(vmovl_s16(vget_low_s16(v_vec_i16))), v_scale_vec); - float32x4_t v_high = vmulq_f32(vcvtq_f32_s32(vmovl_s16(vget_high_s16(v_vec_i16))), v_scale_vec); - - size_t idx = dim_block / VECTOR_WIDTH; - output_accum_low[idx] = vfmaq_f32(output_accum_low[idx], v_low, weight_vec); - output_accum_high[idx] = vfmaq_f32(output_accum_high[idx], v_high, weight_vec); - } - } - } else { - const size_t new_pos = kv_pos - cache_len; - const __fp16* v_vec = V_new_base + new_pos * v_seq_stride + kv_head_idx * v_head_dim; - - for (size_t dim_block = 0; dim_block < v_head_dim_aligned; dim_block += VECTOR_WIDTH) { - float16x8_t v_vec_f16 = vld1q_f16(&v_vec[dim_block]); - float32x4_t v_low = vcvt_f32_f16(vget_low_f16(v_vec_f16)); - float32x4_t v_high = vcvt_f32_f16(vget_high_f16(v_vec_f16)); - - size_t idx = dim_block / VECTOR_WIDTH; - output_accum_low[idx] = vfmaq_f32(output_accum_low[idx], v_low, weight_vec); - output_accum_high[idx] = vfmaq_f32(output_accum_high[idx], v_high, weight_vec); - } - } - } - - running_sum += block_sum * current_block_scale; - } - - if (running_sum > 0.0f) { - const float inv_sum = 1.0f / running_sum; - const float32x4_t inv_sum_vec = vdupq_n_f32(inv_sum); - - for (size_t dim_block = 0; dim_block < v_head_dim_aligned; dim_block += VECTOR_WIDTH) { - size_t idx = dim_block / VECTOR_WIDTH; - float32x4_t final_low = vmulq_f32(output_accum_low[idx], inv_sum_vec); - float32x4_t final_high = vmulq_f32(output_accum_high[idx], inv_sum_vec); - - float16x4_t low_f16 = vcvt_f16_f32(final_low); - float16x4_t high_f16 = vcvt_f16_f32(final_high); - float16x8_t combined = vcombine_f16(low_f16, high_f16); - - vst1q_f16(&o_vec[dim_block], combined); - } - } else { - for (size_t dim = 0; dim < v_head_dim; ++dim) { - o_vec[dim] = static_cast<__fp16>(0.0f); - } - } - } - }); -} - -void cactus_rms_norm_f16( - const __fp16* input, - const __fp16* weight, - __fp16* output, - size_t batch_size, - size_t dims, - float eps -) { - constexpr size_t SIMD_WIDTH = 8; - constexpr size_t UNROLL_FACTOR = 2; - constexpr size_t TILE_SIZE = SIMD_WIDTH * UNROLL_FACTOR; - - for (size_t b = 0; b < batch_size; ++b) { - const __fp16* input_row = input + b * dims; - __fp16* output_row = output + b * dims; - - float32x4_t sum_squares_vec[UNROLL_FACTOR * 2]; - for (size_t u = 0; u < UNROLL_FACTOR * 2; u++) { - sum_squares_vec[u] = vdupq_n_f32(0.0f); - } - - size_t i = 0; - const size_t tile_end = (dims >= TILE_SIZE) ? dims - TILE_SIZE + 1 : 0; - - for (; i < tile_end; i += TILE_SIZE) { - for (size_t u = 0; u < UNROLL_FACTOR; u++) { - float16x8_t input_vec = vld1q_f16(&input_row[i + u * SIMD_WIDTH]); - float32x4_t input_low = vcvt_f32_f16(vget_low_f16(input_vec)); - float32x4_t input_high = vcvt_f32_f16(vget_high_f16(input_vec)); - sum_squares_vec[u * 2] = vfmaq_f32(sum_squares_vec[u * 2], input_low, input_low); - sum_squares_vec[u * 2 + 1] = vfmaq_f32(sum_squares_vec[u * 2 + 1], input_high, input_high); - } - } - - const size_t simd_end = (dims >= SIMD_WIDTH) ? dims - SIMD_WIDTH + 1 : 0; - for (; i < simd_end; i += SIMD_WIDTH) { - float16x8_t input_vec = vld1q_f16(&input_row[i]); - float32x4_t input_low = vcvt_f32_f16(vget_low_f16(input_vec)); - float32x4_t input_high = vcvt_f32_f16(vget_high_f16(input_vec)); - sum_squares_vec[0] = vfmaq_f32(sum_squares_vec[0], input_low, input_low); - sum_squares_vec[1] = vfmaq_f32(sum_squares_vec[1], input_high, input_high); - } - - float32x4_t total_sum = sum_squares_vec[0]; - for (size_t u = 1; u < UNROLL_FACTOR * 2; u++) { - total_sum = vaddq_f32(total_sum, sum_squares_vec[u]); - } - float sum_squares = vaddvq_f32(total_sum); - - for (; i < dims; ++i) { - float val = static_cast(input_row[i]); - sum_squares += val * val; - } - - float rms = sqrtf(sum_squares / static_cast(dims) + eps); - float inv_rms = 1.0f / rms; - float16x8_t inv_rms_vec = vdupq_n_f16(static_cast<__fp16>(inv_rms)); - - i = 0; - for (; i < tile_end; i += TILE_SIZE) { - for (size_t u = 0; u < UNROLL_FACTOR; u++) { - float16x8_t input_vec = vld1q_f16(&input_row[i + u * SIMD_WIDTH]); - float16x8_t weight_vec = vld1q_f16(&weight[i + u * SIMD_WIDTH]); - float16x8_t norm_vec = vmulq_f16(vmulq_f16(input_vec, inv_rms_vec), weight_vec); - vst1q_f16(&output_row[i + u * SIMD_WIDTH], norm_vec); - } - } - - for (; i < simd_end; i += SIMD_WIDTH) { - float16x8_t input_vec = vld1q_f16(&input_row[i]); - float16x8_t weight_vec = vld1q_f16(&weight[i]); - float16x8_t norm_vec = vmulq_f16(vmulq_f16(input_vec, inv_rms_vec), weight_vec); - vst1q_f16(&output_row[i], norm_vec); - } - - for (; i < dims; ++i) { - output_row[i] = static_cast<__fp16>(static_cast(input_row[i]) * inv_rms * static_cast(weight[i])); - } - } -} - -void cactus_layer_norm_f16( - const __fp16* input, - const __fp16* weight, - const __fp16* bias, - __fp16* output, - size_t batch_size, - size_t dims, - float eps -) { - constexpr size_t SIMD_WIDTH = 8; - constexpr size_t UNROLL_FACTOR = 3; - constexpr size_t TILE_SIZE = SIMD_WIDTH * UNROLL_FACTOR; - - const size_t tile_end = (dims >= TILE_SIZE) ? dims - TILE_SIZE + 1 : 0; - const size_t simd_end = (dims >= SIMD_WIDTH) ? dims - SIMD_WIDTH + 1 : 0; - - for (size_t b = 0; b < batch_size; ++b) { - const __fp16* input_row = input + b * dims; - __fp16* output_row = output + b * dims; - - float32x4_t sum_input_vec[UNROLL_FACTOR * 2]; - float32x4_t sum_squares_vec[UNROLL_FACTOR * 2]; - for (size_t u = 0; u < UNROLL_FACTOR * 2; u++) { - sum_input_vec[u] = vdupq_n_f32(0.0f); - sum_squares_vec[u] = vdupq_n_f32(0.0f); - } - - size_t i = 0; - - for (; i < tile_end; i += TILE_SIZE) { - for (size_t u = 0; u < UNROLL_FACTOR; u++) { - float16x8_t input_vec = vld1q_f16(&input_row[i + u * SIMD_WIDTH]); - float32x4_t input_low = vcvt_f32_f16(vget_low_f16(input_vec)); - float32x4_t input_high = vcvt_f32_f16(vget_high_f16(input_vec)); - - sum_input_vec[u * 2] = vaddq_f32(sum_input_vec[u * 2], input_low); - sum_input_vec[u * 2 + 1] = vaddq_f32(sum_input_vec[u * 2 + 1], input_high); - - sum_squares_vec[u * 2] = vfmaq_f32(sum_squares_vec[u * 2], input_low, input_low); - sum_squares_vec[u * 2 + 1] = vfmaq_f32(sum_squares_vec[u * 2 + 1], input_high, input_high); - } - } - - for (; i < simd_end; i += SIMD_WIDTH) { - float16x8_t input_vec = vld1q_f16(&input_row[i]); - float32x4_t input_low = vcvt_f32_f16(vget_low_f16(input_vec)); - float32x4_t input_high = vcvt_f32_f16(vget_high_f16(input_vec)); - sum_input_vec[0] = vaddq_f32(sum_input_vec[0], input_low); - sum_input_vec[1] = vaddq_f32(sum_input_vec[1], input_high); - sum_squares_vec[0] = vfmaq_f32(sum_squares_vec[0], input_low, input_low); - sum_squares_vec[1] = vfmaq_f32(sum_squares_vec[1], input_high, input_high); - } - - float32x4_t total_sum_inputs = sum_input_vec[0]; - float32x4_t total_sum_squares = sum_squares_vec[0]; - for (size_t u = 1; u < UNROLL_FACTOR * 2; u++) { - total_sum_inputs = vaddq_f32(total_sum_inputs, sum_input_vec[u]); - total_sum_squares = vaddq_f32(total_sum_squares, sum_squares_vec[u]); - } - - float sum_inputs = vaddvq_f32(total_sum_inputs); - float sum_squares = vaddvq_f32(total_sum_squares); - for (; i < dims; ++i) { - float val = static_cast(input_row[i]); - sum_inputs += val; - sum_squares += val * val; - } - - float mean = sum_inputs / static_cast(dims); - float mean_squares = sum_squares / static_cast(dims); - float variance = mean_squares - mean * mean; - if (variance < 0.0f) variance = 0.0f; - float inv_std = 1.0f / sqrtf(variance + eps); - - float16x8_t mean_vec = vdupq_n_f16(static_cast<__fp16>(mean)); - float16x8_t inv_std_vec = vdupq_n_f16(static_cast<__fp16>(inv_std)); - - i = 0; - if (bias) { - for (; i < tile_end; i += TILE_SIZE) { - for (size_t u = 0; u < UNROLL_FACTOR; u++) { - float16x8_t input_vec = vld1q_f16(&input_row[i + u * SIMD_WIDTH]); - float16x8_t weight_vec = vld1q_f16(&weight[i + u * SIMD_WIDTH]); - float16x8_t bias_vec = vld1q_f16(&bias[i + u * SIMD_WIDTH]); - float16x8_t out_vec = vmulq_f16(vmulq_f16(vsubq_f16(input_vec, mean_vec), inv_std_vec), weight_vec); - out_vec = vaddq_f16(out_vec, bias_vec); - vst1q_f16(&output_row[i + u * SIMD_WIDTH], out_vec); - } - } - - for (; i < simd_end; i += SIMD_WIDTH) { - float16x8_t input_vec = vld1q_f16(&input_row[i]); - float16x8_t weight_vec = vld1q_f16(&weight[i]); - float16x8_t bias_vec = vld1q_f16(&bias[i]); - float16x8_t out_vec = vmulq_f16(vmulq_f16(vsubq_f16(input_vec, mean_vec), inv_std_vec), weight_vec); - out_vec = vaddq_f16(out_vec, bias_vec); - vst1q_f16(&output_row[i], out_vec); - } - - for (; i < dims; ++i) { - output_row[i] = static_cast<__fp16>((static_cast(input_row[i]) - mean) * inv_std * static_cast(weight[i]) + static_cast(bias[i])); - } - } else { - for (; i < tile_end; i += TILE_SIZE) { - for (size_t u = 0; u < UNROLL_FACTOR; u++) { - float16x8_t input_vec = vld1q_f16(&input_row[i + u * SIMD_WIDTH]); - float16x8_t weight_vec = vld1q_f16(&weight[i + u * SIMD_WIDTH]); - float16x8_t out_vec = vmulq_f16(vmulq_f16(vsubq_f16(input_vec, mean_vec), inv_std_vec), weight_vec); - vst1q_f16(&output_row[i + u * SIMD_WIDTH], out_vec); - } - } - - for (; i < simd_end; i += SIMD_WIDTH) { - float16x8_t input_vec = vld1q_f16(&input_row[i]); - float16x8_t weight_vec = vld1q_f16(&weight[i]); - float16x8_t out_vec = vmulq_f16(vmulq_f16(vsubq_f16(input_vec, mean_vec), inv_std_vec), weight_vec); - vst1q_f16(&output_row[i], out_vec); - } - - for (; i < dims; ++i) { - output_row[i] = static_cast<__fp16>((static_cast(input_row[i]) - mean) * inv_std * static_cast(weight[i])); - } - } - } -} - -namespace CactusRoPEF16 { - -struct RoPECacheF16 { - std::vector<__fp16> cos_table; - std::vector<__fp16> sin_table; - size_t max_seq_len; - size_t head_dim; - float theta; - bool initialized; - - RoPECacheF16() : max_seq_len(0), head_dim(0), theta(0.0f), initialized(false) {} -}; - -static thread_local RoPECacheF16 rope_cache_f16; - -void precompute_rope_tables_f16(size_t seq_len, size_t head_dim, float theta) { - if (rope_cache_f16.initialized && - rope_cache_f16.max_seq_len >= seq_len && - rope_cache_f16.head_dim == head_dim && - rope_cache_f16.theta == theta) { - return; - } - - const size_t half_dim = head_dim / 2; - const size_t table_size = seq_len * half_dim; - - rope_cache_f16.cos_table.resize(table_size); - rope_cache_f16.sin_table.resize(table_size); - - for (size_t pos = 0; pos < seq_len; ++pos) { - const float pos_float = static_cast(pos); - for (size_t i = 0; i < half_dim; ++i) { - const float freq = 1.0f / powf(theta, (2.0f * i) / head_dim); - const float angle = pos_float * freq; - - const size_t idx = pos * half_dim + i; - rope_cache_f16.cos_table[idx] = static_cast<__fp16>(cosf(angle)); - rope_cache_f16.sin_table[idx] = static_cast<__fp16>(sinf(angle)); - } - } - - rope_cache_f16.max_seq_len = seq_len; - rope_cache_f16.head_dim = head_dim; - rope_cache_f16.theta = theta; - rope_cache_f16.initialized = true; -} - -} - -void cactus_rope_f16( - const __fp16* input, - __fp16* output, - size_t batch_size, - size_t seq_len, - size_t num_heads, - size_t head_dim, - size_t start_pos, - float theta -) { - const size_t half_dim = head_dim / 2; - - CactusRoPEF16::precompute_rope_tables_f16(seq_len + start_pos, head_dim, theta); - - const __fp16* cos_cache = CactusRoPEF16::rope_cache_f16.cos_table.data() + start_pos * half_dim; - const __fp16* sin_cache = CactusRoPEF16::rope_cache_f16.sin_table.data() + start_pos * half_dim; - - CactusThreading::parallel_for(batch_size * seq_len, CactusThreading::Thresholds::SCALAR_EXPENSIVE, - [&](size_t start_idx, size_t end_idx) { - for (size_t idx = start_idx; idx < end_idx; ++idx) { - const size_t batch_idx = idx / seq_len; - const size_t seq_idx = idx % seq_len; - - for (size_t head_idx = 0; head_idx < num_heads; ++head_idx) { - const size_t offset = ((batch_idx * seq_len + seq_idx) * num_heads + head_idx) * head_dim; - const __fp16* input_ptr = input + offset; - __fp16* output_ptr = output + offset; - - const __fp16* cos_ptr = cos_cache + seq_idx * half_dim; - const __fp16* sin_ptr = sin_cache + seq_idx * half_dim; - - constexpr size_t SIMD_WIDTH = 8; - const size_t vectorized_half_dim = (half_dim / SIMD_WIDTH) * SIMD_WIDTH; - - for (size_t i = 0; i < vectorized_half_dim; i += SIMD_WIDTH) { - float16x8_t cos_vec = vld1q_f16(&cos_ptr[i]); - float16x8_t sin_vec = vld1q_f16(&sin_ptr[i]); - - float16x8_t x_first_half = vld1q_f16(&input_ptr[i]); - float16x8_t x_second_half = vld1q_f16(&input_ptr[i + half_dim]); - - float16x8_t first_result = vfmsq_f16(vmulq_f16(x_first_half, cos_vec), x_second_half, sin_vec); - float16x8_t second_result = vfmaq_f16(vmulq_f16(x_second_half, cos_vec), x_first_half, sin_vec); - - vst1q_f16(&output_ptr[i], first_result); - vst1q_f16(&output_ptr[i + half_dim], second_result); - } - - for (size_t i = vectorized_half_dim; i < half_dim; ++i) { - const __fp16 cos_val = cos_ptr[i]; - const __fp16 sin_val = sin_ptr[i]; - - const __fp16 x_first_half = input_ptr[i]; - const __fp16 x_second_half = input_ptr[i + half_dim]; - - output_ptr[i] = x_first_half * cos_val - x_second_half * sin_val; - - output_ptr[i + half_dim] = x_second_half * cos_val + x_first_half * sin_val; - } - } - } - }); -} - -void cactus_gpt_j_rope_f16( - const __fp16* input, - __fp16* output, - size_t batch_size, - size_t seq_len, - size_t num_heads, - size_t head_dim, - size_t rot_dim, - size_t start_pos, - float theta -) { - const size_t half_rot_dim = rot_dim / 2; - - CactusRoPEF16::precompute_rope_tables_f16(seq_len + start_pos, rot_dim, theta); - - const __fp16* cos_cache = CactusRoPEF16::rope_cache_f16.cos_table.data() + start_pos * half_rot_dim; - const __fp16* sin_cache = CactusRoPEF16::rope_cache_f16.sin_table.data() + start_pos * half_rot_dim; - - CactusThreading::parallel_for(batch_size * seq_len, CactusThreading::Thresholds::SCALAR_EXPENSIVE, - [&](size_t start_idx, size_t end_idx) { - for (size_t idx = start_idx; idx < end_idx; ++idx) { - const size_t batch_idx = idx / seq_len; - const size_t seq_idx = idx % seq_len; - - for (size_t head_idx = 0; head_idx < num_heads; ++head_idx) { - const size_t offset = ((batch_idx * seq_len + seq_idx) * num_heads + head_idx) * head_dim; - const __fp16* input_ptr = input + offset; - __fp16* output_ptr = output + offset; - - const __fp16* cos_ptr = cos_cache + seq_idx * half_rot_dim; - const __fp16* sin_ptr = sin_cache + seq_idx * half_rot_dim; - - constexpr size_t SIMD_WIDTH = 8; - const size_t vectorized_half_rot_dim = (half_rot_dim / SIMD_WIDTH) * SIMD_WIDTH; - - for (size_t i = 0; i < vectorized_half_rot_dim; i += SIMD_WIDTH) { - float16x8_t cos_vec = vld1q_f16(&cos_ptr[i]); - float16x8_t sin_vec = vld1q_f16(&sin_ptr[i]); - - float16x8x2_t x_vec = vld2q_f16(&input_ptr[2*i]); - float16x8_t x_first_half = x_vec.val[0]; - float16x8_t x_second_half = x_vec.val[1]; - - float16x8_t first_result = vfmsq_f16(vmulq_f16(x_first_half, cos_vec), x_second_half, sin_vec); - float16x8_t second_result = vfmaq_f16(vmulq_f16(x_second_half, cos_vec), x_first_half, sin_vec); - - float16x8x2_t t; - t.val[0] = first_result; - t.val[1] = second_result; - vst2q_f16(&output_ptr[2*i], t); - } - - for (size_t i = vectorized_half_rot_dim; i < half_rot_dim; ++i) { - const __fp16 cos_val = cos_ptr[i]; - const __fp16 sin_val = sin_ptr[i]; - - const __fp16 x_first_half = input_ptr[2*i]; - const __fp16 x_second_half = input_ptr[2*i + 1]; - - output_ptr[2*i] = x_first_half * cos_val - x_second_half * sin_val; - - output_ptr[2*i + 1] = x_second_half * cos_val + x_first_half * sin_val; - } - - constexpr size_t TAIL_SIMD_WIDTH = 8; - size_t copy_idx = rot_dim; - const size_t copy_end_vec = (head_dim / TAIL_SIMD_WIDTH) * TAIL_SIMD_WIDTH; - - for (; copy_idx + TAIL_SIMD_WIDTH <= copy_end_vec; copy_idx += TAIL_SIMD_WIDTH) { - float16x8_t v = vld1q_f16(&input_ptr[copy_idx]); - vst1q_f16(&output_ptr[copy_idx], v); - } - for (; copy_idx < head_dim; ++copy_idx) { - output_ptr[copy_idx] = input_ptr[copy_idx]; - } - } - } - }); -} diff --git a/cactus/kernel/kernel_bilstm.cpp b/cactus/kernel/kernel_bilstm.cpp deleted file mode 100644 index 2b4ea2321..000000000 --- a/cactus/kernel/kernel_bilstm.cpp +++ /dev/null @@ -1,370 +0,0 @@ -#include "kernel.h" -#include "kernel_utils.h" -#include -#include -#include -#include - -#ifdef __APPLE__ -#include -#endif - -static void apply_lstm_gates_f32( - const float* __restrict gates, - float* __restrict c, - float* __restrict h, - size_t hidden_size -) { - const float32x4_t one = vdupq_n_f32(1.0f); - size_t i = 0; - - for (; i + 4 <= hidden_size; i += 4) { - float32x4_t i_raw = vld1q_f32(gates + i); - float32x4_t f_raw = vld1q_f32(gates + hidden_size + i); - float32x4_t g_raw = vld1q_f32(gates + 2 * hidden_size + i); - float32x4_t o_raw = vld1q_f32(gates + 3 * hidden_size + i); - - float32x4_t i_act = vdivq_f32(one, vaddq_f32(one, fast_exp_f32x4(vnegq_f32(i_raw)))); - float32x4_t f_act = vdivq_f32(one, vaddq_f32(one, fast_exp_f32x4(vnegq_f32(f_raw)))); - float32x4_t g_act = fast_tanh_f32x4(g_raw); - float32x4_t o_act = vdivq_f32(one, vaddq_f32(one, fast_exp_f32x4(vnegq_f32(o_raw)))); - - float32x4_t c_prev = vld1q_f32(c + i); - float32x4_t c_new = vfmaq_f32(vmulq_f32(f_act, c_prev), i_act, g_act); - vst1q_f32(c + i, c_new); - - vst1q_f32(h + i, vmulq_f32(o_act, fast_tanh_f32x4(c_new))); - } - - for (; i < hidden_size; ++i) { - float ig = 1.0f / (1.0f + expf(-gates[i])); - float fg = 1.0f / (1.0f + expf(-gates[hidden_size + i])); - float gg = tanhf(gates[2 * hidden_size + i]); - float og = 1.0f / (1.0f + expf(-gates[3 * hidden_size + i])); - float cv = fg * c[i] + ig * gg; - c[i] = cv; - h[i] = og * tanhf(cv); - } -} - -#ifndef __APPLE__ -static void apply_lstm_gates_f16( - const __fp16* __restrict gates, - __fp16* __restrict c, - __fp16* __restrict h, - size_t hidden_size -) { - constexpr size_t W = 8; - const size_t simd_end = (hidden_size / W) * W; - const float32x4_t one = vdupq_n_f32(1.0f); - - for (size_t i = 0; i < simd_end; i += W) { - float16x8_t i_raw = vld1q_f16(gates + i); - float16x8_t f_raw = vld1q_f16(gates + hidden_size + i); - float16x8_t g_raw = vld1q_f16(gates + 2 * hidden_size + i); - float16x8_t o_raw = vld1q_f16(gates + 3 * hidden_size + i); - - float32x4_t il = vcvt_f32_f16(vget_low_f16(i_raw)); - float32x4_t ih = vcvt_f32_f16(vget_high_f16(i_raw)); - float16x8_t i_act = vcombine_f16( - vcvt_f16_f32(vdivq_f32(one, vaddq_f32(one, fast_exp_f32x4(vnegq_f32(il))))), - vcvt_f16_f32(vdivq_f32(one, vaddq_f32(one, fast_exp_f32x4(vnegq_f32(ih)))))); - - float32x4_t fl = vcvt_f32_f16(vget_low_f16(f_raw)); - float32x4_t fh = vcvt_f32_f16(vget_high_f16(f_raw)); - float16x8_t f_act = vcombine_f16( - vcvt_f16_f32(vdivq_f32(one, vaddq_f32(one, fast_exp_f32x4(vnegq_f32(fl))))), - vcvt_f16_f32(vdivq_f32(one, vaddq_f32(one, fast_exp_f32x4(vnegq_f32(fh)))))); - - float32x4_t gl = vcvt_f32_f16(vget_low_f16(g_raw)); - float32x4_t gh = vcvt_f32_f16(vget_high_f16(g_raw)); - float16x8_t g_act = vcombine_f16( - vcvt_f16_f32(fast_tanh_f32x4(gl)), - vcvt_f16_f32(fast_tanh_f32x4(gh))); - - float32x4_t ol = vcvt_f32_f16(vget_low_f16(o_raw)); - float32x4_t oh = vcvt_f32_f16(vget_high_f16(o_raw)); - float16x8_t o_act = vcombine_f16( - vcvt_f16_f32(vdivq_f32(one, vaddq_f32(one, fast_exp_f32x4(vnegq_f32(ol))))), - vcvt_f16_f32(vdivq_f32(one, vaddq_f32(one, fast_exp_f32x4(vnegq_f32(oh)))))); - - float16x8_t c_prev = vld1q_f16(c + i); - float16x8_t c_new = vfmaq_f16(vmulq_f16(f_act, c_prev), i_act, g_act); - vst1q_f16(c + i, c_new); - - float32x4_t cl = vcvt_f32_f16(vget_low_f16(c_new)); - float32x4_t ch = vcvt_f32_f16(vget_high_f16(c_new)); - float16x8_t c_tanh = vcombine_f16( - vcvt_f16_f32(fast_tanh_f32x4(cl)), - vcvt_f16_f32(fast_tanh_f32x4(ch))); - vst1q_f16(h + i, vmulq_f16(o_act, c_tanh)); - } - - for (size_t i = simd_end; i < hidden_size; ++i) { - float ig = 1.0f / (1.0f + expf(-static_cast(gates[i]))); - float fg = 1.0f / (1.0f + expf(-static_cast(gates[hidden_size + i]))); - float gg = tanhf(static_cast(gates[2 * hidden_size + i])); - float og = 1.0f / (1.0f + expf(-static_cast(gates[3 * hidden_size + i]))); - float cv = fg * static_cast(c[i]) + ig * gg; - c[i] = static_cast<__fp16>(cv); - h[i] = static_cast<__fp16>(og * tanhf(cv)); - } -} - -static inline float hsum_f16x8_f32(float16x8_t v) { - float16x4_t lo = vget_low_f16(v); - float16x4_t hi = vget_high_f16(v); - float16x4_t s4 = vadd_f16(lo, hi); - float16x4_t s2 = vadd_f16(s4, vext_f16(s4, s4, 2)); - float16x4_t s1 = vadd_f16(s2, vext_f16(s2, s2, 1)); - return static_cast(vget_lane_f16(s1, 0)); -} - -static void lstm_gemv_f16_neon( - const __fp16* __restrict x, - const __fp16* __restrict W, - const __fp16* __restrict bias, - __fp16* __restrict out, - size_t K, size_t N -) { - const size_t K16 = (K / 16) * 16; - const size_t K8 = (K / 8) * 8; - size_t n = 0; - for (; n + 4 <= N; n += 4) { - const __fp16* w0 = W + n * K; - const __fp16* w1 = W + (n + 1) * K; - const __fp16* w2 = W + (n + 2) * K; - const __fp16* w3 = W + (n + 3) * K; - float16x8_t a0 = vdupq_n_f16(0); - float16x8_t a1 = vdupq_n_f16(0); - float16x8_t a2 = vdupq_n_f16(0); - float16x8_t a3 = vdupq_n_f16(0); - for (size_t k = 0; k < K16; k += 16) { - float16x8_t xlo = vld1q_f16(x + k); - float16x8_t xhi = vld1q_f16(x + k + 8); - a0 = vfmaq_f16(a0, xlo, vld1q_f16(w0 + k)); - a0 = vfmaq_f16(a0, xhi, vld1q_f16(w0 + k + 8)); - a1 = vfmaq_f16(a1, xlo, vld1q_f16(w1 + k)); - a1 = vfmaq_f16(a1, xhi, vld1q_f16(w1 + k + 8)); - a2 = vfmaq_f16(a2, xlo, vld1q_f16(w2 + k)); - a2 = vfmaq_f16(a2, xhi, vld1q_f16(w2 + k + 8)); - a3 = vfmaq_f16(a3, xlo, vld1q_f16(w3 + k)); - a3 = vfmaq_f16(a3, xhi, vld1q_f16(w3 + k + 8)); - } - for (size_t k = K16; k < K8; k += 8) { - float16x8_t xv = vld1q_f16(x + k); - a0 = vfmaq_f16(a0, xv, vld1q_f16(w0 + k)); - a1 = vfmaq_f16(a1, xv, vld1q_f16(w1 + k)); - a2 = vfmaq_f16(a2, xv, vld1q_f16(w2 + k)); - a3 = vfmaq_f16(a3, xv, vld1q_f16(w3 + k)); - } - float s0 = hsum_f16x8_f32(a0); - float s1 = hsum_f16x8_f32(a1); - float s2 = hsum_f16x8_f32(a2); - float s3 = hsum_f16x8_f32(a3); - for (size_t k = K8; k < K; ++k) { - float xv = static_cast(x[k]); - s0 += xv * static_cast(w0[k]); - s1 += xv * static_cast(w1[k]); - s2 += xv * static_cast(w2[k]); - s3 += xv * static_cast(w3[k]); - } - out[n] = static_cast<__fp16>(s0 + static_cast(bias[n])); - out[n + 1] = static_cast<__fp16>(s1 + static_cast(bias[n + 1])); - out[n + 2] = static_cast<__fp16>(s2 + static_cast(bias[n + 2])); - out[n + 3] = static_cast<__fp16>(s3 + static_cast(bias[n + 3])); - } - for (; n < N; ++n) { - const __fp16* w = W + n * K; - float16x8_t acc = vdupq_n_f16(0); - for (size_t k = 0; k < K8; k += 8) - acc = vfmaq_f16(acc, vld1q_f16(x + k), vld1q_f16(w + k)); - float s = hsum_f16x8_f32(acc); - for (size_t k = K8; k < K; ++k) - s += static_cast(x[k]) * static_cast(w[k]); - out[n] = static_cast<__fp16>(s + static_cast(bias[n])); - } -} -#endif - -static inline void fp16_to_fp32_neon(const __fp16* src, float* dst, size_t n) { - size_t i = 0; - for (; i + 8 <= n; i += 8) { - float16x8_t v = vld1q_f16(src + i); - vst1q_f32(dst + i, vcvt_f32_f16(vget_low_f16(v))); - vst1q_f32(dst + i + 4, vcvt_f32_f16(vget_high_f16(v))); - } - for (; i < n; ++i) - dst[i] = static_cast(src[i]); -} - -static inline void fp32_to_fp16_neon(const float* src, __fp16* dst, size_t n) { - size_t i = 0; - for (; i + 8 <= n; i += 8) { - float32x4_t lo = vld1q_f32(src + i); - float32x4_t hi = vld1q_f32(src + i + 4); - vst1q_f16(dst + i, vcombine_f16(vcvt_f16_f32(lo), vcvt_f16_f32(hi))); - } - for (; i < n; ++i) - dst[i] = static_cast<__fp16>(src[i]); -} - -void cactus_bilstm_sequence_f16( - const __fp16* input, - const __fp16* weight_ih_fwd, - const __fp16* weight_hh_fwd, - const __fp16* bias_ih_fwd, - const __fp16* bias_hh_fwd, - const __fp16* weight_ih_bwd, - const __fp16* weight_hh_bwd, - const __fp16* bias_ih_bwd, - const __fp16* bias_hh_bwd, - __fp16* output, - size_t batch_size, - size_t seq_len, - size_t input_size, - size_t hidden_size -) { - const size_t gate_size = 4 * hidden_size; - const size_t combined_K = input_size + hidden_size; - const size_t output_size = 2 * hidden_size; - - std::vector bias_fwd_f32(gate_size); - std::vector bias_bwd_f32(gate_size); - for (size_t g = 0; g < gate_size; ++g) { - bias_fwd_f32[g] = static_cast(bias_ih_fwd[g]) + static_cast(bias_hh_fwd[g]); - bias_bwd_f32[g] = static_cast(bias_ih_bwd[g]) + static_cast(bias_hh_bwd[g]); - } - -#ifdef __APPLE__ - std::vector W_fwd_f32(gate_size * combined_K); - std::vector W_bwd_f32(gate_size * combined_K); - for (size_t g = 0; g < gate_size; ++g) { - fp16_to_fp32_neon(weight_ih_fwd + g * input_size, - W_fwd_f32.data() + g * combined_K, input_size); - fp16_to_fp32_neon(weight_hh_fwd + g * hidden_size, - W_fwd_f32.data() + g * combined_K + input_size, hidden_size); - fp16_to_fp32_neon(weight_ih_bwd + g * input_size, - W_bwd_f32.data() + g * combined_K, input_size); - fp16_to_fp32_neon(weight_hh_bwd + g * hidden_size, - W_bwd_f32.data() + g * combined_K + input_size, hidden_size); - } - - for (size_t b = 0; b < batch_size; ++b) { - const __fp16* batch_in = input + b * seq_len * input_size; - __fp16* batch_out = output + b * seq_len * output_size; - - auto& pool = CactusThreading::get_thread_pool(); - auto bwd_future = pool.enqueue([&, batch_in, batch_out]() { - std::vector xh(combined_K); - std::vector gates(gate_size); - std::vector h(hidden_size, 0.0f); - std::vector c(hidden_size, 0.0f); - - for (size_t idx = 0; idx < seq_len; ++idx) { - const size_t t = seq_len - 1 - idx; - fp16_to_fp32_neon(batch_in + t * input_size, xh.data(), input_size); - memcpy(xh.data() + input_size, h.data(), hidden_size * sizeof(float)); - - memcpy(gates.data(), bias_bwd_f32.data(), gate_size * sizeof(float)); - cblas_sgemv(CblasRowMajor, CblasNoTrans, - static_cast(gate_size), static_cast(combined_K), - 1.0f, W_bwd_f32.data(), static_cast(combined_K), - xh.data(), 1, 1.0f, gates.data(), 1); - - apply_lstm_gates_f32(gates.data(), c.data(), h.data(), hidden_size); - - fp32_to_fp16_neon(h.data(), batch_out + t * output_size + hidden_size, hidden_size); - } - }); - - { - std::vector xh(combined_K); - std::vector gates(gate_size); - std::vector h(hidden_size, 0.0f); - std::vector c(hidden_size, 0.0f); - - for (size_t t = 0; t < seq_len; ++t) { - fp16_to_fp32_neon(batch_in + t * input_size, xh.data(), input_size); - memcpy(xh.data() + input_size, h.data(), hidden_size * sizeof(float)); - - memcpy(gates.data(), bias_fwd_f32.data(), gate_size * sizeof(float)); - cblas_sgemv(CblasRowMajor, CblasNoTrans, - static_cast(gate_size), static_cast(combined_K), - 1.0f, W_fwd_f32.data(), static_cast(combined_K), - xh.data(), 1, 1.0f, gates.data(), 1); - - apply_lstm_gates_f32(gates.data(), c.data(), h.data(), hidden_size); - - fp32_to_fp16_neon(h.data(), batch_out + t * output_size, hidden_size); - } - } - - bwd_future.get(); - } - -#else - std::vector<__fp16> W_fwd_f16(gate_size * combined_K); - std::vector<__fp16> W_bwd_f16(gate_size * combined_K); - for (size_t g = 0; g < gate_size; ++g) { - memcpy(W_fwd_f16.data() + g * combined_K, - weight_ih_fwd + g * input_size, input_size * sizeof(__fp16)); - memcpy(W_fwd_f16.data() + g * combined_K + input_size, - weight_hh_fwd + g * hidden_size, hidden_size * sizeof(__fp16)); - memcpy(W_bwd_f16.data() + g * combined_K, - weight_ih_bwd + g * input_size, input_size * sizeof(__fp16)); - memcpy(W_bwd_f16.data() + g * combined_K + input_size, - weight_hh_bwd + g * hidden_size, hidden_size * sizeof(__fp16)); - } - - std::vector<__fp16> bias_fwd_f16(gate_size); - std::vector<__fp16> bias_bwd_f16(gate_size); - for (size_t g = 0; g < gate_size; ++g) { - bias_fwd_f16[g] = static_cast<__fp16>(bias_fwd_f32[g]); - bias_bwd_f16[g] = static_cast<__fp16>(bias_bwd_f32[g]); - } - - for (size_t b = 0; b < batch_size; ++b) { - const __fp16* batch_in = input + b * seq_len * input_size; - __fp16* batch_out = output + b * seq_len * output_size; - - auto& pool = CactusThreading::get_thread_pool(); - auto bwd_future = pool.enqueue([&, batch_in, batch_out]() { - std::vector<__fp16> xh(combined_K); - std::vector<__fp16> gates(gate_size); - std::vector<__fp16> h(hidden_size, static_cast<__fp16>(0)); - std::vector<__fp16> c(hidden_size, static_cast<__fp16>(0)); - - for (size_t idx = 0; idx < seq_len; ++idx) { - const size_t t = seq_len - 1 - idx; - memcpy(xh.data(), batch_in + t * input_size, input_size * sizeof(__fp16)); - memcpy(xh.data() + input_size, h.data(), hidden_size * sizeof(__fp16)); - lstm_gemv_f16_neon(xh.data(), W_bwd_f16.data(), bias_bwd_f16.data(), - gates.data(), combined_K, gate_size); - apply_lstm_gates_f16(gates.data(), c.data(), h.data(), hidden_size); - memcpy(batch_out + t * output_size + hidden_size, - h.data(), hidden_size * sizeof(__fp16)); - } - }); - - { - std::vector<__fp16> xh(combined_K); - std::vector<__fp16> gates(gate_size); - std::vector<__fp16> h(hidden_size, static_cast<__fp16>(0)); - std::vector<__fp16> c(hidden_size, static_cast<__fp16>(0)); - - for (size_t t = 0; t < seq_len; ++t) { - memcpy(xh.data(), batch_in + t * input_size, input_size * sizeof(__fp16)); - memcpy(xh.data() + input_size, h.data(), hidden_size * sizeof(__fp16)); - lstm_gemv_f16_neon(xh.data(), W_fwd_f16.data(), bias_fwd_f16.data(), - gates.data(), combined_K, gate_size); - apply_lstm_gates_f16(gates.data(), c.data(), h.data(), hidden_size); - memcpy(batch_out + t * output_size, - h.data(), hidden_size * sizeof(__fp16)); - } - } - - bwd_future.get(); - } -#endif -} diff --git a/cactus/kernel/kernel_blas.cpp b/cactus/kernel/kernel_blas.cpp deleted file mode 100644 index 2d6f9ba72..000000000 --- a/cactus/kernel/kernel_blas.cpp +++ /dev/null @@ -1,591 +0,0 @@ -#include "kernel.h" -#include "kernel_utils.h" -#include -#include -#include -#include -#include - -static inline size_t compute_linear_index(const size_t* coords, const size_t* strides, size_t ndim) { - size_t index = 0; - for (size_t i = 0; i < ndim; ++i) { - index += coords[i] * strides[i]; - } - return index; -} - -static inline void increment_coords(size_t* coords, const size_t* shape, size_t ndim) { - for (int i = ndim - 1; i >= 0; --i) { - coords[i]++; - if (coords[i] < shape[i]) { - break; - } - coords[i] = 0; - } -} - -enum class BroadcastOp { ADD, SUB, MUL, DIV }; - -template -static inline float16x8_t broadcast_op_vec(float16x8_t a, float16x8_t b) { - if constexpr (Op == BroadcastOp::ADD) return vaddq_f16(a, b); - else if constexpr (Op == BroadcastOp::SUB) return vsubq_f16(a, b); - else if constexpr (Op == BroadcastOp::MUL) return vmulq_f16(a, b); - else return vdivq_f16(a, b); -} - -template -static inline __fp16 broadcast_op_scalar(__fp16 a, __fp16 b) { - if constexpr (Op == BroadcastOp::ADD) return a + b; - else if constexpr (Op == BroadcastOp::SUB) return a - b; - else if constexpr (Op == BroadcastOp::MUL) return a * b; - else return a / b; -} - -template -static void broadcast_op_optimized(const __fp16* a, const __fp16* b, __fp16* output, - const size_t* a_strides, const size_t* b_strides, - const size_t* output_shape, size_t ndim) { - size_t total_elements = 1; - for (size_t i = 0; i < ndim; ++i) { - total_elements *= output_shape[i]; - } - - if (total_elements == 0) return; - - size_t inner_size = output_shape[ndim - 1]; - bool a_inner_contiguous = (a_strides[ndim - 1] == 1) || (a_strides[ndim - 1] == 0); - bool b_inner_contiguous = (b_strides[ndim - 1] == 1) || (b_strides[ndim - 1] == 0); - bool a_inner_broadcast = (a_strides[ndim - 1] == 0); - bool b_inner_broadcast = (b_strides[ndim - 1] == 0); - - size_t outer_size = total_elements / inner_size; - - if (a_inner_contiguous && b_inner_contiguous && inner_size >= 8) { - CactusThreading::parallel_for(outer_size, CactusThreading::Thresholds::ELEMENT_WISE, - [&](size_t start_outer, size_t end_outer) { - std::vector coords(ndim, 0); - - size_t tmp = start_outer; - for (int i = ndim - 2; i >= 0; --i) { - coords[i] = tmp % output_shape[i]; - tmp /= output_shape[i]; - } - - for (size_t outer_idx = start_outer; outer_idx < end_outer; ++outer_idx) { - size_t a_base = 0, b_base = 0; - for (size_t i = 0; i < ndim - 1; ++i) { - a_base += coords[i] * a_strides[i]; - b_base += coords[i] * b_strides[i]; - } - - __fp16* out_ptr = output + outer_idx * inner_size; - const size_t vec_end = (inner_size / 8) * 8; - - const bool use_stream = total_elements >= STREAMING_STORE_THRESHOLD; - - if (a_inner_broadcast && b_inner_broadcast) { - __fp16 result = broadcast_op_scalar(a[a_base], b[b_base]); - float16x8_t result_vec = vdupq_n_f16(result); - if (use_stream) { - for (size_t i = 0; i < vec_end; i += 8) { - stream_store_f16x8(out_ptr + i, result_vec); - } - } else { - for (size_t i = 0; i < vec_end; i += 8) { - vst1q_f16(out_ptr + i, result_vec); - } - } - for (size_t i = vec_end; i < inner_size; ++i) { - out_ptr[i] = result; - } - } else if (a_inner_broadcast) { - float16x8_t a_vec = vdupq_n_f16(a[a_base]); - const __fp16* b_ptr = b + b_base; - if (use_stream) { - for (size_t i = 0; i < vec_end; i += 8) { - float16x8_t b_vec = vld1q_f16(b_ptr + i); - stream_store_f16x8(out_ptr + i, broadcast_op_vec(a_vec, b_vec)); - } - } else { - for (size_t i = 0; i < vec_end; i += 8) { - float16x8_t b_vec = vld1q_f16(b_ptr + i); - vst1q_f16(out_ptr + i, broadcast_op_vec(a_vec, b_vec)); - } - } - for (size_t i = vec_end; i < inner_size; ++i) { - out_ptr[i] = broadcast_op_scalar(a[a_base], b_ptr[i]); - } - } else if (b_inner_broadcast) { - const __fp16* a_ptr = a + a_base; - float16x8_t b_vec = vdupq_n_f16(b[b_base]); - if (use_stream) { - for (size_t i = 0; i < vec_end; i += 8) { - float16x8_t a_vec = vld1q_f16(a_ptr + i); - stream_store_f16x8(out_ptr + i, broadcast_op_vec(a_vec, b_vec)); - } - } else { - for (size_t i = 0; i < vec_end; i += 8) { - float16x8_t a_vec = vld1q_f16(a_ptr + i); - vst1q_f16(out_ptr + i, broadcast_op_vec(a_vec, b_vec)); - } - } - for (size_t i = vec_end; i < inner_size; ++i) { - out_ptr[i] = broadcast_op_scalar(a_ptr[i], b[b_base]); - } - } else { - const __fp16* a_ptr = a + a_base; - const __fp16* b_ptr = b + b_base; - if (use_stream) { - for (size_t i = 0; i < vec_end; i += 8) { - float16x8_t a_vec = vld1q_f16(a_ptr + i); - float16x8_t b_vec = vld1q_f16(b_ptr + i); - stream_store_f16x8(out_ptr + i, broadcast_op_vec(a_vec, b_vec)); - } - } else { - for (size_t i = 0; i < vec_end; i += 8) { - float16x8_t a_vec = vld1q_f16(a_ptr + i); - float16x8_t b_vec = vld1q_f16(b_ptr + i); - vst1q_f16(out_ptr + i, broadcast_op_vec(a_vec, b_vec)); - } - } - for (size_t i = vec_end; i < inner_size; ++i) { - out_ptr[i] = broadcast_op_scalar(a_ptr[i], b_ptr[i]); - } - } - - for (int i = ndim - 2; i >= 0; --i) { - coords[i]++; - if (coords[i] < output_shape[i]) break; - coords[i] = 0; - } - } - }); - } else { - CactusThreading::parallel_for(total_elements, CactusThreading::Thresholds::ELEMENT_WISE, - [&](size_t start_idx, size_t end_idx) { - std::vector coords(ndim); - - size_t tmp = start_idx; - for (int i = ndim - 1; i >= 0; --i) { - coords[i] = tmp % output_shape[i]; - tmp /= output_shape[i]; - } - - for (size_t linear_idx = start_idx; linear_idx < end_idx; ++linear_idx) { - size_t a_idx = compute_linear_index(coords.data(), a_strides, ndim); - size_t b_idx = compute_linear_index(coords.data(), b_strides, ndim); - - output[linear_idx] = broadcast_op_scalar(a[a_idx], b[b_idx]); - - increment_coords(coords.data(), output_shape, ndim); - } - }); - } -} - -void cactus_add_f16(const __fp16* a, const __fp16* b, __fp16* output, size_t num_elements) { - const bool use_streaming = num_elements >= STREAMING_STORE_THRESHOLD; - CactusThreading::parallel_for(num_elements, CactusThreading::Thresholds::ELEMENT_WISE, - [&](size_t start_idx, size_t end_idx) { - constexpr size_t SIMD_WIDTH = 8; - constexpr size_t UNROLL = 4; - const size_t unrolled_end = start_idx + ((end_idx - start_idx) / (SIMD_WIDTH * UNROLL)) * (SIMD_WIDTH * UNROLL); - const size_t vectorized_end = start_idx + ((end_idx - start_idx) / SIMD_WIDTH) * SIMD_WIDTH; - - if (use_streaming) { - for (size_t i = start_idx; i < unrolled_end; i += SIMD_WIDTH * UNROLL) { - __builtin_prefetch(&a[i + 256], 0, 0); - __builtin_prefetch(&b[i + 256], 0, 0); - float16x8_t v0 = vaddq_f16(vld1q_f16(&a[i]), vld1q_f16(&b[i])); - float16x8_t v1 = vaddq_f16(vld1q_f16(&a[i + 8]), vld1q_f16(&b[i + 8])); - float16x8_t v2 = vaddq_f16(vld1q_f16(&a[i + 16]), vld1q_f16(&b[i + 16])); - float16x8_t v3 = vaddq_f16(vld1q_f16(&a[i + 24]), vld1q_f16(&b[i + 24])); - stream_store_f16x8(&output[i], v0); - stream_store_f16x8(&output[i + 8], v1); - stream_store_f16x8(&output[i + 16], v2); - stream_store_f16x8(&output[i + 24], v3); - } - for (size_t i = unrolled_end; i < vectorized_end; i += SIMD_WIDTH) { - stream_store_f16x8(&output[i], vaddq_f16(vld1q_f16(&a[i]), vld1q_f16(&b[i]))); - } - } else { - for (size_t i = start_idx; i < vectorized_end; i += SIMD_WIDTH) { - vst1q_f16(&output[i], vaddq_f16(vld1q_f16(&a[i]), vld1q_f16(&b[i]))); - } - } - - for (size_t i = vectorized_end; i < end_idx; ++i) { - output[i] = a[i] + b[i]; - } - }); -} - -void cactus_add_f16_clipped(const __fp16* a, const __fp16* b, __fp16* output, size_t num_elements) { - CactusThreading::parallel_for(num_elements, CactusThreading::Thresholds::ELEMENT_WISE, - [&](size_t start_idx, size_t end_idx) { - constexpr size_t SIMD_WIDTH = 8; - const size_t vectorized_end = start_idx + ((end_idx - start_idx) / SIMD_WIDTH) * SIMD_WIDTH; - - constexpr float FP16_MAX = 65500.0f; - const float32x4_t max_val = vdupq_n_f32(FP16_MAX); - const float32x4_t min_val = vdupq_n_f32(-FP16_MAX); - - for (size_t i = start_idx; i < vectorized_end; i += SIMD_WIDTH) { - float16x8_t a_vec = vld1q_f16(&a[i]); - float16x8_t b_vec = vld1q_f16(&b[i]); - - float32x4_t a_low = vcvt_f32_f16(vget_low_f16(a_vec)); - float32x4_t a_high = vcvt_f32_f16(vget_high_f16(a_vec)); - float32x4_t b_low = vcvt_f32_f16(vget_low_f16(b_vec)); - float32x4_t b_high = vcvt_f32_f16(vget_high_f16(b_vec)); - - float32x4_t result_low = vaddq_f32(a_low, b_low); - float32x4_t result_high = vaddq_f32(a_high, b_high); - - result_low = vminq_f32(vmaxq_f32(result_low, min_val), max_val); - result_high = vminq_f32(vmaxq_f32(result_high, min_val), max_val); - - float16x4_t result_low_f16 = vcvt_f16_f32(result_low); - float16x4_t result_high_f16 = vcvt_f16_f32(result_high); - float16x8_t result_vec = vcombine_f16(result_low_f16, result_high_f16); - - vst1q_f16(&output[i], result_vec); - } - - for (size_t i = vectorized_end; i < end_idx; ++i) { - float result = static_cast(a[i]) + static_cast(b[i]); - result = std::fmin(std::fmax(result, -FP16_MAX), FP16_MAX); - output[i] = static_cast<__fp16>(result); - } - }); -} - -void cactus_subtract_f16(const __fp16* a, const __fp16* b, __fp16* output, size_t num_elements) { - const bool use_streaming = num_elements >= STREAMING_STORE_THRESHOLD; - CactusThreading::parallel_for(num_elements, CactusThreading::Thresholds::ELEMENT_WISE, - [&](size_t start_idx, size_t end_idx) { - constexpr size_t SIMD_WIDTH = 8; - const size_t vectorized_end = start_idx + ((end_idx - start_idx) / SIMD_WIDTH) * SIMD_WIDTH; - - if (use_streaming) { - for (size_t i = start_idx; i < vectorized_end; i += SIMD_WIDTH) { - float16x8_t a_vec = vld1q_f16(&a[i]); - float16x8_t b_vec = vld1q_f16(&b[i]); - stream_store_f16x8(&output[i], vsubq_f16(a_vec, b_vec)); - } - } else { - for (size_t i = start_idx; i < vectorized_end; i += SIMD_WIDTH) { - float16x8_t a_vec = vld1q_f16(&a[i]); - float16x8_t b_vec = vld1q_f16(&b[i]); - vst1q_f16(&output[i], vsubq_f16(a_vec, b_vec)); - } - } - - for (size_t i = vectorized_end; i < end_idx; ++i) { - output[i] = a[i] - b[i]; - } - }); -} - -void cactus_multiply_f16(const __fp16* a, const __fp16* b, __fp16* output, size_t num_elements) { - const bool use_streaming = num_elements >= STREAMING_STORE_THRESHOLD; - CactusThreading::parallel_for(num_elements, CactusThreading::Thresholds::ELEMENT_WISE, - [&](size_t start_idx, size_t end_idx) { - constexpr size_t SIMD_WIDTH = 8; - const size_t vectorized_end = start_idx + ((end_idx - start_idx) / SIMD_WIDTH) * SIMD_WIDTH; - - if (use_streaming) { - for (size_t i = start_idx; i < vectorized_end; i += SIMD_WIDTH) { - float16x8_t a_vec = vld1q_f16(&a[i]); - float16x8_t b_vec = vld1q_f16(&b[i]); - stream_store_f16x8(&output[i], vmulq_f16(a_vec, b_vec)); - } - } else { - for (size_t i = start_idx; i < vectorized_end; i += SIMD_WIDTH) { - float16x8_t a_vec = vld1q_f16(&a[i]); - float16x8_t b_vec = vld1q_f16(&b[i]); - vst1q_f16(&output[i], vmulq_f16(a_vec, b_vec)); - } - } - - for (size_t i = vectorized_end; i < end_idx; ++i) { - output[i] = a[i] * b[i]; - } - }); -} - -void cactus_add_scaled_f16(const __fp16* base, const __fp16* src, __fp16* output, size_t num_elements, float scale) { - constexpr size_t SIMD_WIDTH = 8; - const float32x4_t vscale = vdupq_n_f32(scale); - const size_t vec_end = (num_elements / SIMD_WIDTH) * SIMD_WIDTH; - - for (size_t i = 0; i < vec_end; i += SIMD_WIDTH) { - float16x8_t base_vec = vld1q_f16(base + i); - float16x8_t src_vec = vld1q_f16(src + i); - - float32x4_t base_lo = vcvt_f32_f16(vget_low_f16(base_vec)); - float32x4_t base_hi = vcvt_f32_f16(vget_high_f16(base_vec)); - float32x4_t src_lo = vcvt_f32_f16(vget_low_f16(src_vec)); - float32x4_t src_hi = vcvt_f32_f16(vget_high_f16(src_vec)); - - float32x4_t result_lo = vfmaq_f32(base_lo, src_lo, vscale); - float32x4_t result_hi = vfmaq_f32(base_hi, src_hi, vscale); - - vst1q_f16(output + i, vcombine_f16(vcvt_f16_f32(result_lo), vcvt_f16_f32(result_hi))); - } - for (size_t i = vec_end; i < num_elements; ++i) { - output[i] = static_cast<__fp16>(static_cast(base[i]) - + static_cast(src[i]) * scale); - } -} - -void cactus_divide_f16(const __fp16* a, const __fp16* b, __fp16* output, size_t num_elements) { - const bool use_streaming = num_elements >= STREAMING_STORE_THRESHOLD; - CactusThreading::parallel_for(num_elements, CactusThreading::Thresholds::ELEMENT_WISE, - [&](size_t start_idx, size_t end_idx) { - constexpr size_t SIMD_WIDTH = 8; - const size_t vectorized_end = start_idx + ((end_idx - start_idx) / SIMD_WIDTH) * SIMD_WIDTH; - - if (use_streaming) { - for (size_t i = start_idx; i < vectorized_end; i += SIMD_WIDTH) { - float16x8_t a_vec = vld1q_f16(&a[i]); - float16x8_t b_vec = vld1q_f16(&b[i]); - stream_store_f16x8(&output[i], vdivq_f16(a_vec, b_vec)); - } - } else { - for (size_t i = start_idx; i < vectorized_end; i += SIMD_WIDTH) { - float16x8_t a_vec = vld1q_f16(&a[i]); - float16x8_t b_vec = vld1q_f16(&b[i]); - vst1q_f16(&output[i], vdivq_f16(a_vec, b_vec)); - } - } - - for (size_t i = vectorized_end; i < end_idx; ++i) { - output[i] = a[i] / b[i]; - } - }); -} - -void cactus_add_broadcast_f16(const __fp16* a, const __fp16* b, __fp16* output, - const size_t* a_strides, const size_t* b_strides, - const size_t* output_shape, size_t ndim) { - broadcast_op_optimized(a, b, output, a_strides, b_strides, output_shape, ndim); -} - -void cactus_subtract_broadcast_f16(const __fp16* a, const __fp16* b, __fp16* output, - const size_t* a_strides, const size_t* b_strides, - const size_t* output_shape, size_t ndim) { - broadcast_op_optimized(a, b, output, a_strides, b_strides, output_shape, ndim); -} - -void cactus_multiply_broadcast_f16(const __fp16* a, const __fp16* b, __fp16* output, - const size_t* a_strides, const size_t* b_strides, - const size_t* output_shape, size_t ndim) { - broadcast_op_optimized(a, b, output, a_strides, b_strides, output_shape, ndim); -} - -void cactus_divide_broadcast_f16(const __fp16* a, const __fp16* b, __fp16* output, - const size_t* a_strides, const size_t* b_strides, - const size_t* output_shape, size_t ndim) { - broadcast_op_optimized(a, b, output, a_strides, b_strides, output_shape, ndim); -} - -void cactus_concat_f16(const __fp16* input1, const __fp16* input2, __fp16* output, - const size_t* shape1, const size_t* shape2, const size_t* output_shape, - size_t ndims, int axis) { - if (axis < 0) axis += ndims; - - size_t outer_size = 1; - for (size_t i = 0; i < static_cast(axis); ++i) { - outer_size *= output_shape[i]; - } - - size_t inner_size = 1; - for (size_t i = axis + 1; i < ndims; ++i) { - inner_size *= output_shape[i]; - } - - size_t axis_size1 = shape1[axis]; - size_t axis_size2 = shape2[axis]; - - size_t input1_stride = axis_size1 * inner_size; - size_t input2_stride = axis_size2 * inner_size; - size_t output_stride = (axis_size1 + axis_size2) * inner_size; - - CactusThreading::parallel_for(outer_size, CactusThreading::Thresholds::ELEMENT_WISE, - [&](size_t start, size_t end) { - for (size_t outer = start; outer < end; ++outer) { - const __fp16* in1_ptr = input1 + outer * input1_stride; - const __fp16* in2_ptr = input2 + outer * input2_stride; - __fp16* out_ptr = output + outer * output_stride; - - size_t copy_size1 = axis_size1 * inner_size; - std::memcpy(out_ptr, in1_ptr, copy_size1 * sizeof(__fp16)); - - size_t copy_size2 = axis_size2 * inner_size; - std::memcpy(out_ptr + copy_size1, in2_ptr, copy_size2 * sizeof(__fp16)); - } - }); -} - -void cactus_cat_f16(const __fp16** inputs, __fp16* output, const size_t** input_shapes, - const size_t* output_shape, size_t num_inputs, size_t rank, int axis) { - if (axis < 0) axis += rank; - - size_t outer_size = 1; - for (size_t i = 0; i < static_cast(axis); ++i) { - outer_size *= output_shape[i]; - } - - size_t inner_size = 1; - for (size_t i = axis + 1; i < rank; ++i) { - inner_size *= output_shape[i]; - } - - CactusThreading::parallel_for(outer_size, CactusThreading::Thresholds::ELEMENT_WISE, - [&](size_t start, size_t end) { - for (size_t outer = start; outer < end; ++outer) { - __fp16* out_ptr = output + outer * inner_size * output_shape[axis]; - size_t offset = 0; - - for (size_t input_idx = 0; input_idx < num_inputs; ++input_idx) { - const __fp16* in_ptr = inputs[input_idx] + outer * inner_size * input_shapes[input_idx][axis]; - size_t copy_size = input_shapes[input_idx][axis] * inner_size; - std::memcpy(out_ptr + offset, in_ptr, copy_size * sizeof(__fp16)); - offset += copy_size; - } - } - }); -} - -void cactus_transpose_2d_f16(const __fp16* source, __fp16* destination, size_t num_rows, size_t num_cols, size_t start_row, size_t end_row) { - constexpr size_t TILE_SIZE = 32; - constexpr size_t VECTOR_WIDTH = 8; - - for (size_t row_tile_start = start_row; row_tile_start < end_row; row_tile_start += TILE_SIZE) { - const size_t row_tile_end = std::min(row_tile_start + TILE_SIZE, end_row); - - for (size_t col_tile_start = 0; col_tile_start < num_cols; col_tile_start += TILE_SIZE) { - const size_t col_tile_end = std::min(col_tile_start + TILE_SIZE, num_cols); - - for (size_t row_block = row_tile_start; row_block < row_tile_end; row_block += VECTOR_WIDTH) { - const size_t row_block_end = std::min(row_block + VECTOR_WIDTH, row_tile_end); - - for (size_t col_block = col_tile_start; col_block < col_tile_end; col_block += VECTOR_WIDTH) { - const size_t col_block_end = std::min(col_block + VECTOR_WIDTH, col_tile_end); - - if (row_block_end - row_block >= 8 && col_block_end - col_block >= 8) { - float16x8_t rows[8]; - for (int i = 0; i < 8; i++) { - if (row_block + i < row_block_end) { - rows[i] = vld1q_f16(&source[(row_block + i) * num_cols + col_block]); - } else { - rows[i] = vdupq_n_f16(0.0f); - } - } - - float16x8x2_t r01 = vtrnq_f16(rows[0], rows[1]); - float16x8x2_t r23 = vtrnq_f16(rows[2], rows[3]); - float16x8x2_t r45 = vtrnq_f16(rows[4], rows[5]); - float16x8x2_t r67 = vtrnq_f16(rows[6], rows[7]); - - float32x4x2_t r0123_lo = vtrnq_f32( - vreinterpretq_f32_f16(r01.val[0]), - vreinterpretq_f32_f16(r23.val[0])); - float32x4x2_t r0123_hi = vtrnq_f32( - vreinterpretq_f32_f16(r01.val[1]), - vreinterpretq_f32_f16(r23.val[1])); - float32x4x2_t r4567_lo = vtrnq_f32( - vreinterpretq_f32_f16(r45.val[0]), - vreinterpretq_f32_f16(r67.val[0])); - float32x4x2_t r4567_hi = vtrnq_f32( - vreinterpretq_f32_f16(r45.val[1]), - vreinterpretq_f32_f16(r67.val[1])); - - float16x8_t col0 = vreinterpretq_f16_f32(vcombine_f32( - vget_low_f32(r0123_lo.val[0]), vget_low_f32(r4567_lo.val[0]))); - float16x8_t col1 = vreinterpretq_f16_f32(vcombine_f32( - vget_low_f32(r0123_hi.val[0]), vget_low_f32(r4567_hi.val[0]))); - float16x8_t col2 = vreinterpretq_f16_f32(vcombine_f32( - vget_low_f32(r0123_lo.val[1]), vget_low_f32(r4567_lo.val[1]))); - float16x8_t col3 = vreinterpretq_f16_f32(vcombine_f32( - vget_low_f32(r0123_hi.val[1]), vget_low_f32(r4567_hi.val[1]))); - float16x8_t col4 = vreinterpretq_f16_f32(vcombine_f32( - vget_high_f32(r0123_lo.val[0]), vget_high_f32(r4567_lo.val[0]))); - float16x8_t col5 = vreinterpretq_f16_f32(vcombine_f32( - vget_high_f32(r0123_hi.val[0]), vget_high_f32(r4567_hi.val[0]))); - float16x8_t col6 = vreinterpretq_f16_f32(vcombine_f32( - vget_high_f32(r0123_lo.val[1]), vget_high_f32(r4567_lo.val[1]))); - float16x8_t col7 = vreinterpretq_f16_f32(vcombine_f32( - vget_high_f32(r0123_hi.val[1]), vget_high_f32(r4567_hi.val[1]))); - - float16x8_t cols[8] = {col0, col1, col2, col3, col4, col5, col6, col7}; - for (int c = 0; c < 8 && col_block + c < col_block_end; c++) { - if (col_block + c < num_cols) { - if (row_block_end - row_block >= 8) { - vst1q_f16(&destination[(col_block + c) * num_rows + row_block], cols[c]); - } else { - __fp16 temp[8]; - vst1q_f16(temp, cols[c]); - for (size_t i = 0; i < row_block_end - row_block; ++i) { - destination[(col_block + c) * num_rows + row_block + i] = temp[i]; - } - } - } - } - } else { - for (size_t row = row_block; row < row_block_end; row++) { - for (size_t col = col_block; col < col_block_end; col++) { - destination[col * num_rows + row] = source[row * num_cols + col]; - } - } - } - } - } - } - } -} - -void cactus_transpose_f16(const __fp16* source, __fp16* destination, const size_t* shape, const size_t* permutation, size_t ndim, size_t start_idx, size_t end_idx) { - if (ndim == 2 && permutation[0] == 1 && permutation[1] == 0) { - size_t num_rows = shape[0]; - size_t num_cols = shape[1]; - - constexpr size_t THRESHOLD = 8192; - constexpr size_t TILE_ROWS = 32; - if (num_rows * num_cols >= THRESHOLD) { - const size_t num_row_blocks = (num_rows + TILE_ROWS - 1) / TILE_ROWS; - - CactusThreading::parallel_for(num_row_blocks, CactusThreading::Thresholds::SCALAR_EXPENSIVE, - [=](size_t start_block, size_t end_block) { - for (size_t block_idx = start_block; block_idx < end_block; ++block_idx) { - size_t start_row = block_idx * TILE_ROWS; - size_t end_row = std::min(start_row + TILE_ROWS, num_rows); - - cactus_transpose_2d_f16(source, destination, num_rows, num_cols, start_row, end_row); - } - }); - } else { - cactus_transpose_2d_f16(source, destination, num_rows, num_cols, 0, num_rows); - } - } else { - for (size_t idx = start_idx; idx < end_idx; ++idx) { - size_t src_idx = 0; - size_t tmp_idx = idx; - - for (size_t i = 0; i < ndim; ++i) { - size_t coord = tmp_idx % shape[permutation[ndim - 1 - i]]; - tmp_idx /= shape[permutation[ndim - 1 - i]]; - - size_t stride = 1; - for (size_t j = permutation[ndim - 1 - i] + 1; j < ndim; ++j) { - stride *= shape[j]; - } - src_idx += coord * stride; - } - - destination[idx] = source[src_idx]; - } - } -} diff --git a/cactus/kernel/kernel_conv.cpp b/cactus/kernel/kernel_conv.cpp deleted file mode 100644 index 7e7dbe5f1..000000000 --- a/cactus/kernel/kernel_conv.cpp +++ /dev/null @@ -1,1581 +0,0 @@ -#include "kernel.h" -#include "kernel_utils.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef __APPLE__ -#include -#endif - -constexpr size_t T_TILE_F16 = 2; -constexpr size_t ACCELERATE_K_THRESHOLD = 32; -constexpr size_t ACCELERATE_L_THRESHOLD = 128; - -#ifdef __APPLE__ -static void conv1d_causal_depthwise_f16_accelerate( - const __fp16* input, - const __fp16* weight, - __fp16* output, - size_t N, size_t L, size_t C, size_t K, size_t dilation) -{ - const size_t in_bs = L * C; - const size_t out_bs = L * C; - CactusThreading::parallel_for_2d(N, C, CactusThreading::Thresholds::ATTENTION, [&](size_t n, size_t c) { - const __fp16* Xb = input + n * in_bs; - __fp16* Yb = output + n * out_bs; - const __fp16* Wc = weight + c * K; - - if (dilation == 1) { - - size_t i = 0; - for(; i + 7 < L; i += 8){ - float16x8_t acc = vdupq_n_f16(0.0f); - - for (size_t k = 0; k < K; ++k) { - float16x8_t input_vec = vld1q_f16(&Xb[(i + k) * C + c]); - float16x8_t weight_vec = vdupq_n_f16(Wc[k]); - acc = vfmaq_f16(acc, input_vec, weight_vec); - } - vst1q_f16(&Yb[i * C + c], acc); - } - - for (; i < L; ++i) { - float acc = 0.0f; - for (size_t k = 0; k < K; ++k) { - acc += float(Xb[(i + k) * C + c]) * float(Wc[k]); - } - Yb[i * C + c] = (__fp16)acc; - } - } else { - - for (size_t i = 0; i < L; ++i) { - float acc = 0.0f; - - for (size_t k = 0; k < K; ++k) { - size_t idx = i + k * dilation; - acc += float(Xb[idx * C + c]) * float(Wc[k]); - } - - Yb[i * C + c] = (__fp16)acc; - } - } - }); -} -#endif - -void cactus_conv1d_causal_depthwise_f16( - const __fp16* input, - const __fp16* weight, - __fp16* output, - size_t N, size_t L, size_t C, size_t K, size_t dilation) -{ -#ifdef __APPLE__ - if (K >= ACCELERATE_K_THRESHOLD && L >= ACCELERATE_L_THRESHOLD) { - conv1d_causal_depthwise_f16_accelerate(input, weight, output, N, L, C, K, dilation); - return; - } -#endif - - const size_t in_bs = L * C; - const size_t out_bs = L * C; - - CactusThreading::parallel_for_2d(N, C, CactusThreading::Thresholds::ATTENTION, [&](size_t n, size_t c) { - const __fp16* Xb = input + n * in_bs; - __fp16* Yb = output + n * out_bs; - - std::vector wrev(K); - const __fp16* Wc = weight + c * K; - for (size_t k = 0; k < K; ++k) wrev[k] = (float)Wc[K - 1 - k]; - - for (size_t t0 = 0; t0 < L; t0 += T_TILE_F16) { - const size_t t1 = std::min(t0 + 1, L - 1); - - float32x4_t vacc0 = vdupq_n_f32(0.f); - float32x4_t vacc1 = vdupq_n_f32(0.f); - - size_t k = 0; - for (; k + 8 <= K; k += 8) { - - float x0_0=0, x1_0=0, x2_0=0, x3_0=0; - float x0_1=0, x1_1=0, x2_1=0, x3_1=0; - { - ptrdiff_t a0=(ptrdiff_t)t0-(ptrdiff_t)((k+0)*dilation); - ptrdiff_t a1=(ptrdiff_t)t0-(ptrdiff_t)((k+1)*dilation); - ptrdiff_t a2=(ptrdiff_t)t0-(ptrdiff_t)((k+2)*dilation); - ptrdiff_t a3=(ptrdiff_t)t0-(ptrdiff_t)((k+3)*dilation); - if (a0>=0) x0_0 = (float)Xb[(size_t)a0*C + c]; - if (a1>=0) x1_0 = (float)Xb[(size_t)a1*C + c]; - if (a2>=0) x2_0 = (float)Xb[(size_t)a2*C + c]; - if (a3>=0) x3_0 = (float)Xb[(size_t)a3*C + c]; - - ptrdiff_t b0=(ptrdiff_t)t1-(ptrdiff_t)((k+0)*dilation); - ptrdiff_t b1=(ptrdiff_t)t1-(ptrdiff_t)((k+1)*dilation); - ptrdiff_t b2=(ptrdiff_t)t1-(ptrdiff_t)((k+2)*dilation); - ptrdiff_t b3=(ptrdiff_t)t1-(ptrdiff_t)((k+3)*dilation); - if (b0>=0) x0_1 = (float)Xb[(size_t)b0*C + c]; - if (b1>=0) x1_1 = (float)Xb[(size_t)b1*C + c]; - if (b2>=0) x2_1 = (float)Xb[(size_t)b2*C + c]; - if (b3>=0) x3_1 = (float)Xb[(size_t)b3*C + c]; - } - float32x4_t xv0 = {x0_0,x1_0,x2_0,x3_0}; - float32x4_t yv0 = {x0_1,x1_1,x2_1,x3_1}; - float32x4_t wv0 = {wrev[k+0],wrev[k+1],wrev[k+2],wrev[k+3]}; - vacc0 = vfmaq_f32(vacc0, xv0, wv0); - vacc1 = vfmaq_f32(vacc1, yv0, wv0); - - float a0_0=0, a1_0=0, a2_0=0, a3_0=0; - float a0_1=0, a1_1=0, a2_1=0, a3_1=0; - { - ptrdiff_t a0i=(ptrdiff_t)t0-(ptrdiff_t)((k+4)*dilation); - ptrdiff_t a1i=(ptrdiff_t)t0-(ptrdiff_t)((k+5)*dilation); - ptrdiff_t a2i=(ptrdiff_t)t0-(ptrdiff_t)((k+6)*dilation); - ptrdiff_t a3i=(ptrdiff_t)t0-(ptrdiff_t)((k+7)*dilation); - if (a0i>=0) a0_0 = (float)Xb[(size_t)a0i*C + c]; - if (a1i>=0) a1_0 = (float)Xb[(size_t)a1i*C + c]; - if (a2i>=0) a2_0 = (float)Xb[(size_t)a2i*C + c]; - if (a3i>=0) a3_0 = (float)Xb[(size_t)a3i*C + c]; - - ptrdiff_t b0i=(ptrdiff_t)t1-(ptrdiff_t)((k+4)*dilation); - ptrdiff_t b1i=(ptrdiff_t)t1-(ptrdiff_t)((k+5)*dilation); - ptrdiff_t b2i=(ptrdiff_t)t1-(ptrdiff_t)((k+6)*dilation); - ptrdiff_t b3i=(ptrdiff_t)t1-(ptrdiff_t)((k+7)*dilation); - if (b0i>=0) a0_1 = (float)Xb[(size_t)b0i*C + c]; - if (b1i>=0) a1_1 = (float)Xb[(size_t)b1i*C + c]; - if (b2i>=0) a2_1 = (float)Xb[(size_t)b2i*C + c]; - if (b3i>=0) a3_1 = (float)Xb[(size_t)b3i*C + c]; - } - float32x4_t xv1 = {a0_0,a1_0,a2_0,a3_0}; - float32x4_t yv1 = {a0_1,a1_1,a2_1,a3_1}; - float32x4_t wv1 = {wrev[k+4],wrev[k+5],wrev[k+6],wrev[k+7]}; - vacc0 = vfmaq_f32(vacc0, xv1, wv1); - vacc1 = vfmaq_f32(vacc1, yv1, wv1); - } - - float acc0 = vaddvq_f32(vacc0); - float acc1 = vaddvq_f32(vacc1); - - for (; k < K; ++k) { - ptrdiff_t a=(ptrdiff_t)t0-(ptrdiff_t)(k*dilation); - if (a>=0) acc0 += wrev[k] * (float)Xb[(size_t)a*C + c]; - ptrdiff_t b=(ptrdiff_t)t1-(ptrdiff_t)(k*dilation); - if (b>=0) acc1 += wrev[k] * (float)Xb[(size_t)b*C + c]; - } - - Yb[t0*C + c] = (__fp16)acc0; - if (t0 + 1 < L) Yb[t1*C + c] = (__fp16)acc1; - } - }); -} - -void cactus_conv1d_f16_k3( - const __fp16* input, - const __fp16* weight, - __fp16* output, - size_t N, size_t L, - size_t C_in, size_t C_out, - size_t stride -){ - const size_t out_len = ((L - 1) / stride) + 1; - const size_t in_bs = C_in * L; - const size_t out_bs = C_out * out_len; - - const size_t total_compute = N * C_out * out_len * C_in * 3; - CactusThreading::ParallelConfig config = (total_compute < 100000) - ? CactusThreading::ParallelConfig{SIZE_MAX, SIZE_MAX} - : CactusThreading::Thresholds::ATTENTION; - - CactusThreading::parallel_for_2d(N, C_out, config, [&](size_t n, size_t oc) { - const __fp16* Xb = input + n * in_bs; - __fp16* Yoc = output + n * out_bs + oc * out_len; - const __fp16* Woc = weight + oc * (C_in * 3); - - for (size_t out_idx = 0; out_idx < out_len; out_idx += 2) { - const size_t out_t0 = out_idx; - const bool have_t1 = (out_idx + 1) < out_len; - const size_t out_t1 = have_t1 ? (out_idx + 1) : out_idx; - - const size_t t0 = out_t0 * stride; - const size_t t1 = have_t1 ? (out_t1 * stride) : t0; - - float32x4_t acc0 = vdupq_n_f32(0.f); - float32x4_t acc1 = vdupq_n_f32(0.f); - - size_t ic = 0; - for (; ic + 16 <= C_in; ic += 16) { - for (size_t u = 0; u < 16; ++u) { - const __fp16* Xc = Xb + (ic + u) * L; - const __fp16* Wc = Woc + (ic + u) * 3; - - const float16x8_t wv = { - Wc[0], Wc[1], Wc[2], (__fp16)0, - Wc[0], Wc[1], Wc[2], (__fp16)0 - }; - - const ptrdiff_t tm0 = (ptrdiff_t)t0 - 1; - const ptrdiff_t tp0 = (ptrdiff_t)t0 + 1; - const ptrdiff_t tm1 = (ptrdiff_t)t1 - 1; - const ptrdiff_t tp1 = (ptrdiff_t)t1 + 1; - - const __fp16 x0m = (tm0 >= 0) ? Xc[tm0] : (__fp16)0; - const __fp16 x00 = Xc[t0]; - const __fp16 x0p = (tp0 < (ptrdiff_t)L) ? Xc[tp0] : (__fp16)0; - - __fp16 x1m = 0, x10 = 0, x1p = 0; - if (have_t1) { - x1m = (tm1 >= 0) ? Xc[tm1] : (__fp16)0; - x10 = Xc[t1]; - x1p = (tp1 < (ptrdiff_t)L) ? Xc[tp1] : (__fp16)0; - } - - const float16x8_t xv = { - x0m, x00, x0p, (__fp16)0, - x1m, x10, x1p, (__fp16)0 - }; - - const float16x4_t xv0_h = vget_low_f16(xv); - const float16x4_t wv0_h = vget_low_f16(wv); - acc0 = vfmaq_f32(acc0, vcvt_f32_f16(xv0_h), vcvt_f32_f16(wv0_h)); - - if (have_t1) { - const float16x4_t xv1_h = vget_high_f16(xv); - const float16x4_t wv1_h = vget_high_f16(wv); - acc1 = vfmaq_f32(acc1, vcvt_f32_f16(xv1_h), vcvt_f32_f16(wv1_h)); - } - } - } - - for (; ic < C_in; ++ic) { - const __fp16* Xc = Xb + ic * L; - const __fp16* Wc = Woc + ic * 3; - - const float16x8_t wv = { - Wc[0], Wc[1], Wc[2], (__fp16)0, - Wc[0], Wc[1], Wc[2], (__fp16)0 - }; - - const ptrdiff_t tm0 = (ptrdiff_t)t0 - 1; - const ptrdiff_t tp0 = (ptrdiff_t)t0 + 1; - const ptrdiff_t tm1 = (ptrdiff_t)t1 - 1; - const ptrdiff_t tp1 = (ptrdiff_t)t1 + 1; - - const __fp16 x0m = (tm0 >= 0) ? Xc[tm0] : (__fp16)0; - const __fp16 x00 = Xc[t0]; - const __fp16 x0p = (tp0 < (ptrdiff_t)L) ? Xc[tp0] : (__fp16)0; - - __fp16 x1m = 0, x10 = 0, x1p = 0; - if (have_t1) { - x1m = (tm1 >= 0) ? Xc[tm1] : (__fp16)0; - x10 = Xc[t1]; - x1p = (tp1 < (ptrdiff_t)L) ? Xc[tp1] : (__fp16)0; - } - - const float16x8_t xv = { - x0m, x00, x0p, (__fp16)0, - x1m, x10, x1p, (__fp16)0 - }; - - const float16x4_t xv0_h = vget_low_f16(xv); - const float16x4_t wv0_h = vget_low_f16(wv); - acc0 = vfmaq_f32(acc0, vcvt_f32_f16(xv0_h), vcvt_f32_f16(wv0_h)); - - if (have_t1) { - const float16x4_t xv1_h = vget_high_f16(xv); - const float16x4_t wv1_h = vget_high_f16(wv); - acc1 = vfmaq_f32(acc1, vcvt_f32_f16(xv1_h), vcvt_f32_f16(wv1_h)); - } - } - - float32x2_t s0 = vadd_f32(vget_low_f32(acc0), vget_high_f32(acc0)); - float sum0 = vget_lane_f32(s0, 0) + vget_lane_f32(s0, 1); - Yoc[out_t0] = (__fp16)sum0; - - if (have_t1) { - float32x2_t s1 = vadd_f32(vget_low_f32(acc1), vget_high_f32(acc1)); - float sum1 = vget_lane_f32(s1, 0) + vget_lane_f32(s1, 1); - Yoc[out_t1] = (__fp16)sum1; - } - } - }); -} - -#ifdef __APPLE__ -static void conv1d_f16_accelerate( - const __fp16* input, - const __fp16* weight, - const __fp16* bias, - __fp16* output, - size_t N, size_t L, - size_t C_in, size_t C_out, - size_t K, - size_t stride -){ - const size_t out_len = ((L - K) / stride) + 1; - const size_t in_bs = C_in * L; - const size_t out_bs = C_out * out_len; - - const size_t total_compute = N * C_out * out_len * C_in * K; - CactusThreading::ParallelConfig config = (total_compute < 100000) - ? CactusThreading::ParallelConfig{SIZE_MAX, SIZE_MAX} - : CactusThreading::Thresholds::ATTENTION; - - CactusThreading::parallel_for_2d( - N, C_out, config, - [&](size_t n, size_t oc) { - - const __fp16* Xb = input + n * in_bs; - __fp16* Yoc = output + n * out_bs + oc * out_len; - const __fp16* Woc = weight + oc * (C_in * K); - const float b = bias ? (float)bias[oc] : 0.f; - - std::vector out_f32(out_len, b); - std::vector input_f32(L); - std::vector weight_f32(K); - - for (size_t ic = 0; ic < C_in; ++ic) { - const __fp16* Xc = Xb + ic * L; - const __fp16* Wc = Woc + ic * K; - - for (size_t i = 0; i < L; ++i) input_f32[i] = (float)Xc[i]; - for (size_t k = 0; k < K; ++k) weight_f32[k] = (float)Wc[k]; - - if (stride == 1) { - std::vector conv_out(out_len); - vDSP_conv(input_f32.data(), 1, weight_f32.data(), 1, - conv_out.data(), 1, out_len, K); - vDSP_vadd(out_f32.data(), 1, conv_out.data(), 1, - out_f32.data(), 1, out_len); - } else { - std::vector full_conv(L - K + 1); - vDSP_conv(input_f32.data(), 1, weight_f32.data(), 1, - full_conv.data(), 1, L - K + 1, K); - for (size_t out_t = 0; out_t < out_len; ++out_t) { - out_f32[out_t] += full_conv[out_t * stride]; - } - } - } - - for (size_t out_t = 0; out_t < out_len; ++out_t) { - Yoc[out_t] = (__fp16)out_f32[out_t]; - } - }); -} -#endif - -static void conv1d_f16_neon( - const __fp16* input, - const __fp16* weight, - const __fp16* bias, - __fp16* output, - size_t N, size_t L, - size_t C_in, size_t C_out, - size_t K, - size_t stride -){ - const size_t out_len = ((L - K) / stride) + 1; - const size_t in_bs = C_in * L; - const size_t out_bs = C_out * out_len; - - const size_t total_compute = N * C_out * out_len * C_in * K; - CactusThreading::ParallelConfig config = (total_compute < 100000) - ? CactusThreading::ParallelConfig{SIZE_MAX, SIZE_MAX} - : CactusThreading::Thresholds::ATTENTION; - - CactusThreading::parallel_for_2d( - N, C_out, config, - [&](size_t n, size_t oc) { - - const __fp16* Xb = input + n * in_bs; - __fp16* Yoc = output + n * out_bs + oc * out_len; - const __fp16* Woc = weight + oc * (C_in * K); - const float b = bias ? (float)bias[oc] : 0.f; - - for (size_t out_t = 0; out_t < out_len; ++out_t) { - const size_t t = out_t * stride; - float sum = b; - - for (size_t ic = 0; ic < C_in; ++ic) { - const __fp16* Xc = Xb + ic * L + t; - const __fp16* Wc = Woc + ic * K; - - float32x4_t acc0 = vdupq_n_f32(0.f); - float32x4_t acc1 = vdupq_n_f32(0.f); - - size_t k = 0; - - for (; k + 8 <= K; k += 8) { - const float16x8_t xv = vld1q_f16(Xc + k); - const float16x8_t wv = vld1q_f16(Wc + k); - - acc0 = vfmaq_f32(acc0, - vcvt_f32_f16(vget_low_f16(xv)), - vcvt_f32_f16(vget_low_f16(wv))); - acc1 = vfmaq_f32(acc1, - vcvt_f32_f16(vget_high_f16(xv)), - vcvt_f32_f16(vget_high_f16(wv))); - } - - float32x2_t s1 = vadd_f32(vget_low_f32(acc0), vget_high_f32(acc0)); - sum += vget_lane_f32(s1, 0) + vget_lane_f32(s1, 1); - - float32x2_t s2 = vadd_f32(vget_low_f32(acc1), vget_high_f32(acc1)); - sum += vget_lane_f32(s2, 0) + vget_lane_f32(s2, 1); - - for (; k < K; ++k) { - sum += (float)Xc[k] * (float)Wc[k]; - } - } - - Yoc[out_t] = (__fp16)sum; - } - }); -} - -#ifdef __APPLE__ -static void conv1d_f16_gemm( - const __fp16* input, - const __fp16* weight, - const __fp16* bias, - __fp16* output, - size_t N, size_t L, - size_t C_in, size_t C_out, - size_t K, - size_t stride -) { - const size_t out_len = (L - K) / stride + 1; - const size_t col_K = C_in * K; - - std::vector W_f32(C_out * col_K); - for (size_t i = 0; i < C_out * col_K; ++i) - W_f32[i] = static_cast(weight[i]); - - std::vector bias_f32; - if (bias) { - bias_f32.resize(C_out); - for (size_t i = 0; i < C_out; ++i) - bias_f32[i] = static_cast(bias[i]); - } - - std::vector col(col_K * out_len); - std::vector Y_f32(C_out * out_len); - - for (size_t n = 0; n < N; ++n) { - const __fp16* Xn = input + n * C_in * L; - __fp16* Yn = output + n * C_out * out_len; - - if (stride == 1) { - for (size_t ic = 0; ic < C_in; ++ic) { - const __fp16* Xc = Xn + ic * L; - for (size_t k = 0; k < K; ++k) { - float* dst = col.data() + (ic * K + k) * out_len; - const __fp16* src = Xc + k; - size_t t = 0; - for (; t + 8 <= out_len; t += 8) { - float16x8_t v = vld1q_f16(src + t); - vst1q_f32(dst + t, vcvt_f32_f16(vget_low_f16(v))); - vst1q_f32(dst + t + 4, vcvt_f32_f16(vget_high_f16(v))); - } - for (; t < out_len; ++t) - dst[t] = static_cast(src[t]); - } - } - } else { - for (size_t ic = 0; ic < C_in; ++ic) { - const __fp16* Xc = Xn + ic * L; - for (size_t k = 0; k < K; ++k) { - float* dst = col.data() + (ic * K + k) * out_len; - for (size_t t = 0; t < out_len; ++t) - dst[t] = static_cast(Xc[t * stride + k]); - } - } - } - - cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans, - static_cast(C_out), static_cast(out_len), static_cast(col_K), - 1.0f, W_f32.data(), static_cast(col_K), - col.data(), static_cast(out_len), - 0.0f, Y_f32.data(), static_cast(out_len)); - - if (bias) { - for (size_t oc = 0; oc < C_out; ++oc) { - float b = bias_f32[oc]; - const float* src = Y_f32.data() + oc * out_len; - __fp16* dst = Yn + oc * out_len; - size_t t = 0; - for (; t + 8 <= out_len; t += 8) { - float32x4_t v0 = vaddq_f32(vld1q_f32(src + t), vdupq_n_f32(b)); - float32x4_t v1 = vaddq_f32(vld1q_f32(src + t + 4), vdupq_n_f32(b)); - vst1q_f16(dst + t, vcombine_f16(vcvt_f16_f32(v0), vcvt_f16_f32(v1))); - } - for (; t < out_len; ++t) - dst[t] = static_cast<__fp16>(src[t] + b); - } - } else { - for (size_t oc = 0; oc < C_out; ++oc) { - const float* src = Y_f32.data() + oc * out_len; - __fp16* dst = Yn + oc * out_len; - size_t t = 0; - for (; t + 8 <= out_len; t += 8) { - float32x4_t v0 = vld1q_f32(src + t); - float32x4_t v1 = vld1q_f32(src + t + 4); - vst1q_f16(dst + t, vcombine_f16(vcvt_f16_f32(v0), vcvt_f16_f32(v1))); - } - for (; t < out_len; ++t) - dst[t] = static_cast<__fp16>(src[t]); - } - } - } -} -#endif - -void cactus_conv1d_f16( - const __fp16* input, - const __fp16* weight, - const __fp16* bias, - __fp16* output, - size_t N, size_t L, - size_t C_in, size_t C_out, - size_t K, - size_t stride -){ -#ifdef __APPLE__ - if (stride > 1 || K < ACCELERATE_K_THRESHOLD) { - conv1d_f16_gemm(input, weight, bias, output, N, L, C_in, C_out, K, stride); - return; - } - if (K >= ACCELERATE_K_THRESHOLD && L >= ACCELERATE_L_THRESHOLD) { - conv1d_f16_accelerate(input, weight, bias, output, N, L, C_in, C_out, K, stride); - return; - } -#endif - conv1d_f16_neon(input, weight, bias, output, N, L, C_in, C_out, K, stride); -} - -inline void conv1d_k7s3_oc8_t4( - const __fp16* Xb, - const __fp16* Wpack, - const __fp16* bias, - __fp16* Yb, - size_t L, - size_t out_len, - size_t C_in, - size_t C_out, - size_t out_t, - size_t oc0 -){ - float32x4_t acc[4][2]; - float32x4_t b0 = vdupq_n_f32(0.f); - float32x4_t b1 = vdupq_n_f32(0.f); - if (bias) { - float16x8_t bv = vld1q_f16(bias + oc0); - b0 = vcvt_f32_f16(vget_low_f16(bv)); - b1 = vcvt_f32_f16(vget_high_f16(bv)); - } - - for (int j = 0; j < 4; ++j) { - acc[j][0] = b0; - acc[j][1] = b1; - } - - const size_t t_base = out_t * 3; - - for (size_t ic = 0; ic < C_in; ++ic) { - const __fp16* Wic = Wpack + (ic * 7) * C_out + oc0; - const __fp16* Xic = Xb + ic * L + t_base; - - for (int k = 0; k < 7; ++k) { - float16x8_t w_half = vld1q_f16(Wic + k * C_out); - float32x4_t w0 = vcvt_f32_f16(vget_low_f16(w_half)); - float32x4_t w1 = vcvt_f32_f16(vget_high_f16(w_half)); - for (int j = 0; j < 4; ++j) { - float x_val = (float)Xic[j * 3 + k]; - float32x4_t xv = vdupq_n_f32(x_val); - - acc[j][0] = vfmaq_f32(acc[j][0], w0, xv); - acc[j][1] = vfmaq_f32(acc[j][1], w1, xv); - } - } - } - - float tmp0[4], tmp1[4]; - for(int j=0; j<4; ++j) { - vst1q_f32(tmp0, acc[j][0]); - vst1q_f32(tmp1, acc[j][1]); - - for(int i=0; i<4; ++i) { - Yb[(oc0 + i) * out_len + out_t + j] = (__fp16)tmp0[i]; - } - for(int i=0; i<4; ++i) { - Yb[(oc0 + 4 + i) * out_len + out_t + j] = (__fp16)tmp1[i]; - } - } -} - -inline void conv1d_k7s3_oc8_scalar( - const __fp16* Xb, - const __fp16* Wpack, - const __fp16* bias, - __fp16* Yb, - size_t L, - size_t out_len, - size_t C_in, - size_t C_out, - size_t out_t, - size_t oc0 -){ - float32x4_t acc0 = vdupq_n_f32(0.f); - float32x4_t acc1 = vdupq_n_f32(0.f); - - if (bias) { - float16x8_t bv = vld1q_f16(bias + oc0); - acc0 = vcvt_f32_f16(vget_low_f16(bv)); - acc1 = vcvt_f32_f16(vget_high_f16(bv)); - } - - const size_t t_base = out_t * 3; - - for (size_t ic = 0; ic < C_in; ++ic) { - const __fp16* Wic = Wpack + (ic * 7) * C_out + oc0; - const __fp16* Xic = Xb + ic * L + t_base; - for (int k = 0; k < 7; ++k) { - float x_val = (float)Xic[k]; - float32x4_t xv = vdupq_n_f32(x_val); - - float16x8_t w_half = vld1q_f16(Wic + k * C_out); - acc0 = vfmaq_f32(acc0, vcvt_f32_f16(vget_low_f16(w_half)), xv); - acc1 = vfmaq_f32(acc1, vcvt_f32_f16(vget_high_f16(w_half)), xv); - } - } - - float tmp[8]; - vst1q_f32(tmp, acc0); - vst1q_f32(tmp+4, acc1); - - for(int i=0; i<8; ++i) { - if (oc0 + i < C_out) { - Yb[(oc0 + i) * out_len + out_t] = (__fp16)tmp[i]; - } - } -} - -void cactus_conv1d_f16_k7s3_oc8( - const __fp16* input, - const __fp16* Wpack, - const __fp16* bias, - __fp16* output, - size_t N, - size_t L, - size_t C_in, - size_t C_out) -{ - if (L < 7) return; - size_t out_len = (L - 7) / 3 + 1; - size_t num_oc_blocks = (C_out + 7) / 8; - - CactusThreading::parallel_for_2d(N, num_oc_blocks, CactusThreading::Thresholds::SCALAR_EXPENSIVE, [=](size_t n, size_t ob) { - size_t oc0 = ob * 8; - const __fp16* Xb = input + n * (C_in * L); - __fp16* Yb = output + n * (C_out * out_len); - - size_t out_t = 0; - for (; out_t + 4 <= out_len; out_t += 4) { - conv1d_k7s3_oc8_t4(Xb, Wpack, bias, Yb, L, out_len, C_in, C_out, out_t, oc0); - } - - for (; out_t < out_len; ++out_t) { - conv1d_k7s3_oc8_scalar(Xb, Wpack, bias, Yb, L, out_len, C_in, C_out, out_t, oc0); - } - }); -} - - - -void cactus_bilinear_interpolation_f16(const __fp16* input, __fp16* output, size_t src_height, size_t src_width, size_t embed_dim, - size_t dst_height, size_t dst_width, bool align_corners) -{ - float scale_h, scale_w; - if (align_corners) { - scale_h = (src_height > 1 && dst_height > 1) - ? static_cast(src_height - 1) / static_cast(dst_height - 1) - : 0.0f; - scale_w = (src_width > 1 && dst_width > 1) - ? static_cast(src_width - 1) / static_cast(dst_width - 1) - : 0.0f; - } else { - scale_h = (dst_height > 0) - ? static_cast(src_height) / static_cast(dst_height) - : 0.0f; - scale_w = (dst_width > 0) - ? static_cast(src_width) / static_cast(dst_width) - : 0.0f; - } - - for (size_t dst_y = 0; dst_y < dst_height; ++dst_y) { - for (size_t dst_x = 0; dst_x < dst_width; ++dst_x) { - float src_y_float, src_x_float; - if (align_corners) { - src_y_float = static_cast(dst_y) * scale_h; - src_x_float = static_cast(dst_x) * scale_w; - } else { - src_y_float = (static_cast(dst_y) + 0.5f) * scale_h - 0.5f; - src_x_float = (static_cast(dst_x) + 0.5f) * scale_w - 0.5f; - if (src_y_float < 0.0f) src_y_float = 0.0f; - if (src_x_float < 0.0f) src_x_float = 0.0f; - const float max_y = static_cast(src_height) - 1.0f; - const float max_x = static_cast(src_width) - 1.0f; - if (src_y_float > max_y) src_y_float = max_y; - if (src_x_float > max_x) src_x_float = max_x; - } - - int y0 = static_cast(std::floor(src_y_float)); - int x0 = static_cast(std::floor(src_x_float)); - - int y1 = ((y0 + 1) < static_cast(src_height)) ? (y0 + 1) : (static_cast(src_height) - 1); - int x1 = ((x0 + 1) < static_cast(src_width)) ? (x0 + 1) : (static_cast(src_width) - 1); - - float dy = src_y_float - y0; - float dx = src_x_float - x0; - - float w00 = (1.0f - dx) * (1.0f - dy); - float w01 = dx * (1.0f - dy); - float w10 = (1.0f - dx) * dy; - float w11 = dx * dy; - - size_t idx00 = (y0 * static_cast(src_width) + x0) * static_cast(embed_dim); - size_t idx01 = (y0 * static_cast(src_width) + x1) * static_cast(embed_dim); - size_t idx10 = (y1 * static_cast(src_width) + x0) * static_cast(embed_dim); - size_t idx11 = (y1 * static_cast(src_width) + x1) * static_cast(embed_dim); - - size_t out_idx = (dst_y * dst_width + dst_x) * embed_dim; - - size_t d = 0; - for(; d + 8 <= embed_dim; d += 8){ - float16x8_t v00 = vld1q_f16(input + idx00 + d); - float16x8_t v01 = vld1q_f16(input + idx01 + d); - float16x8_t v10 = vld1q_f16(input + idx10 + d); - float16x8_t v11 = vld1q_f16(input + idx11 + d); - - float16x8_t result = vfmaq_f16( - vfmaq_f16( - vfmaq_f16(vdupq_n_f16(0.0f), v00, vdupq_n_f16(w00)), - v01, vdupq_n_f16(w01)), - v10, vdupq_n_f16(w10)); - result = vfmaq_f16(result, v11, vdupq_n_f16(w11)); - - vst1q_f16(output + out_idx + d, result); - } - - for (; d < embed_dim; ++d) { - output[out_idx + d] = - input[idx00 + d] * w00 + - input[idx01 + d] * w01 + - input[idx10 + d] * w10 + - input[idx11 + d] * w11; - } - } - } -} - -void cactus_stft_f16( - const __fp16* input, - const __fp16* weight, - __fp16* output, - size_t N, size_t L, - size_t C_in, size_t /*C_out*/, - size_t K, size_t stride, - size_t num_fft_bins -) { - const size_t out_len = ((L - K) / stride) + 1; - const size_t in_bs = C_in * L; - const size_t out_bs = 2 * num_fft_bins * out_len; - - for (size_t n = 0; n < N; ++n) { - const __fp16* Xb = input + n * in_bs; - - for (size_t bin = 0; bin < num_fft_bins; ++bin) { - const __fp16* Wr = weight + bin * (C_in * K); - const __fp16* Wi = weight + (bin + num_fft_bins) * (C_in * K); - - for (size_t out_t = 0; out_t < out_len; ++out_t) { - const size_t t = out_t * stride; - float sum_real = 0.0f; - float sum_imag = 0.0f; - - for (size_t ic = 0; ic < C_in; ++ic) { - const __fp16* Xc = Xb + ic * L + t; - const __fp16* Wrc = Wr + ic * K; - const __fp16* Wic = Wi + ic * K; - - float32x4_t acc_r0 = vdupq_n_f32(0.f); - float32x4_t acc_r1 = vdupq_n_f32(0.f); - float32x4_t acc_i0 = vdupq_n_f32(0.f); - float32x4_t acc_i1 = vdupq_n_f32(0.f); - - size_t k = 0; - for (; k + 8 <= K; k += 8) { - const float16x8_t xv = vld1q_f16(Xc + k); - const float16x8_t wr = vld1q_f16(Wrc + k); - const float16x8_t wi = vld1q_f16(Wic + k); - - acc_r0 = vfmaq_f32(acc_r0, vcvt_f32_f16(vget_low_f16(xv)), vcvt_f32_f16(vget_low_f16(wr))); - acc_r1 = vfmaq_f32(acc_r1, vcvt_f32_f16(vget_high_f16(xv)), vcvt_f32_f16(vget_high_f16(wr))); - acc_i0 = vfmaq_f32(acc_i0, vcvt_f32_f16(vget_low_f16(xv)), vcvt_f32_f16(vget_low_f16(wi))); - acc_i1 = vfmaq_f32(acc_i1, vcvt_f32_f16(vget_high_f16(xv)), vcvt_f32_f16(vget_high_f16(wi))); - } - - sum_real += vaddvq_f32(acc_r0) + vaddvq_f32(acc_r1); - sum_imag += vaddvq_f32(acc_i0) + vaddvq_f32(acc_i1); - - for (; k < K; ++k) { - float x = (float)Xc[k]; - sum_real += x * (float)Wrc[k]; - sum_imag += x * (float)Wic[k]; - } - } - - output[n * out_bs + bin * out_len + out_t] = (__fp16)sum_real; - output[n * out_bs + (bin + num_fft_bins) * out_len + out_t] = (__fp16)sum_imag; - } - } - } -} - -void cactus_conv1d_same_depthwise_f16_k9( - const __fp16* input, - const __fp16* weight, - const __fp16* bias, - __fp16* output, - size_t N, size_t L, size_t C) -{ - constexpr int K = 9; - - const size_t bs = L * C; - - CactusThreading::parallel_for_2d( - N, C, CactusThreading::Thresholds::ATTENTION, - [&](size_t n, size_t c) { - - const __fp16* Xb = input + n * bs; - __fp16* Yb = output + n * bs; - - const __fp16* Wc = weight + c * K; - const float b = bias ? (float)bias[c] : 0.0f; - - const float w0 = (float)Wc[0]; - const float w1 = (float)Wc[1]; - const float w2 = (float)Wc[2]; - const float w3 = (float)Wc[3]; - const float w4 = (float)Wc[4]; - const float w5 = (float)Wc[5]; - const float w6 = (float)Wc[6]; - const float w7 = (float)Wc[7]; - const float w8 = (float)Wc[8]; - - const float32x4_t wv0 = {w0, w1, w2, w3}; - const float32x4_t wv1 = {w4, w5, w6, w7}; - - for (size_t t0 = 0; t0 < L; t0 += T_TILE_F16) { - const bool have_t1 = (t0 + 1 < L); - const size_t t1 = have_t1 ? (t0 + 1) : t0; - - float x0_0=0, x1_0=0, x2_0=0, x3_0=0; - float x4_0=0, x5_0=0, x6_0=0, x7_0=0; - float x8_0=0; - - { - const ptrdiff_t i0 = (ptrdiff_t)t0 - 4; - const ptrdiff_t i1 = (ptrdiff_t)t0 - 3; - const ptrdiff_t i2 = (ptrdiff_t)t0 - 2; - const ptrdiff_t i3 = (ptrdiff_t)t0 - 1; - if (i0 >= 0 && i0 < (ptrdiff_t)L) x0_0 = (float)Xb[(size_t)i0 * C + c]; - if (i1 >= 0 && i1 < (ptrdiff_t)L) x1_0 = (float)Xb[(size_t)i1 * C + c]; - if (i2 >= 0 && i2 < (ptrdiff_t)L) x2_0 = (float)Xb[(size_t)i2 * C + c]; - if (i3 >= 0 && i3 < (ptrdiff_t)L) x3_0 = (float)Xb[(size_t)i3 * C + c]; - - const ptrdiff_t j4 = (ptrdiff_t)t0 + 0; - const ptrdiff_t j5 = (ptrdiff_t)t0 + 1; - const ptrdiff_t j6 = (ptrdiff_t)t0 + 2; - const ptrdiff_t j7 = (ptrdiff_t)t0 + 3; - const ptrdiff_t j8 = (ptrdiff_t)t0 + 4; - if (j4 >= 0 && j4 < (ptrdiff_t)L) x4_0 = (float)Xb[(size_t)j4 * C + c]; - if (j5 >= 0 && j5 < (ptrdiff_t)L) x5_0 = (float)Xb[(size_t)j5 * C + c]; - if (j6 >= 0 && j6 < (ptrdiff_t)L) x6_0 = (float)Xb[(size_t)j6 * C + c]; - if (j7 >= 0 && j7 < (ptrdiff_t)L) x7_0 = (float)Xb[(size_t)j7 * C + c]; - if (j8 >= 0 && j8 < (ptrdiff_t)L) x8_0 = (float)Xb[(size_t)j8 * C + c]; - } - - float32x4_t acc0v = vdupq_n_f32(0.f); - const float32x4_t xv0_0 = {x0_0, x1_0, x2_0, x3_0}; - const float32x4_t xv1_0 = {x4_0, x5_0, x6_0, x7_0}; - acc0v = vfmaq_f32(acc0v, xv0_0, wv0); - acc0v = vfmaq_f32(acc0v, xv1_0, wv1); - float acc0 = vaddvq_f32(acc0v) + (w8 * x8_0) + b; - - float acc1 = 0.f; - if (have_t1) { - float x0_1=0, x1_1=0, x2_1=0, x3_1=0; - float x4_1=0, x5_1=0, x6_1=0, x7_1=0; - float x8_1=0; - - { - const ptrdiff_t i0 = (ptrdiff_t)t1 - 4; - const ptrdiff_t i1 = (ptrdiff_t)t1 - 3; - const ptrdiff_t i2 = (ptrdiff_t)t1 - 2; - const ptrdiff_t i3 = (ptrdiff_t)t1 - 1; - if (i0 >= 0 && i0 < (ptrdiff_t)L) x0_1 = (float)Xb[(size_t)i0 * C + c]; - if (i1 >= 0 && i1 < (ptrdiff_t)L) x1_1 = (float)Xb[(size_t)i1 * C + c]; - if (i2 >= 0 && i2 < (ptrdiff_t)L) x2_1 = (float)Xb[(size_t)i2 * C + c]; - if (i3 >= 0 && i3 < (ptrdiff_t)L) x3_1 = (float)Xb[(size_t)i3 * C + c]; - - const ptrdiff_t j4 = (ptrdiff_t)t1 + 0; - const ptrdiff_t j5 = (ptrdiff_t)t1 + 1; - const ptrdiff_t j6 = (ptrdiff_t)t1 + 2; - const ptrdiff_t j7 = (ptrdiff_t)t1 + 3; - const ptrdiff_t j8 = (ptrdiff_t)t1 + 4; - if (j4 >= 0 && j4 < (ptrdiff_t)L) x4_1 = (float)Xb[(size_t)j4 * C + c]; - if (j5 >= 0 && j5 < (ptrdiff_t)L) x5_1 = (float)Xb[(size_t)j5 * C + c]; - if (j6 >= 0 && j6 < (ptrdiff_t)L) x6_1 = (float)Xb[(size_t)j6 * C + c]; - if (j7 >= 0 && j7 < (ptrdiff_t)L) x7_1 = (float)Xb[(size_t)j7 * C + c]; - if (j8 >= 0 && j8 < (ptrdiff_t)L) x8_1 = (float)Xb[(size_t)j8 * C + c]; - } - - float32x4_t acc1v = vdupq_n_f32(0.f); - const float32x4_t xv0_1 = {x0_1, x1_1, x2_1, x3_1}; - const float32x4_t xv1_1 = {x4_1, x5_1, x6_1, x7_1}; - acc1v = vfmaq_f32(acc1v, xv0_1, wv0); - acc1v = vfmaq_f32(acc1v, xv1_1, wv1); - acc1 = vaddvq_f32(acc1v) + (w8 * x8_1) + b; - } - - Yb[t0 * C + c] = (__fp16)acc0; - if (have_t1) Yb[t1 * C + c] = (__fp16)acc1; - } - }); -} - - -void cactus_conv2d_f16_k3s2p1_nchw( - const __fp16* input, - const __fp16* weight, - const __fp16* bias, - __fp16* output, - size_t N, - size_t C_in, size_t H, size_t W, - size_t C_out -){ - if (H + 2 < 3 || W + 2 < 3) return; - const size_t H_out = (H - 1) / 2 + 1; - const size_t W_out = (W - 1) / 2 + 1; - -#ifdef __APPLE__ - { - const size_t spatial = H_out * W_out; - const size_t col_K = C_in * 9; - std::vector W_f32(C_out * col_K); - for (size_t oc = 0; oc < C_out; ++oc) - for (size_t ic = 0; ic < C_in; ++ic) - for (size_t kh = 0; kh < 3; ++kh) - for (size_t kw = 0; kw < 3; ++kw) - W_f32[oc * col_K + ic * 9 + kh * 3 + kw] = - static_cast(weight[((oc * C_in + ic) * 3 + kh) * 3 + kw]); - - std::vector bias_f32(C_out, 0.0f); - if (bias) - for (size_t i = 0; i < C_out; ++i) - bias_f32[i] = static_cast(bias[i]); - - std::vector col(col_K * spatial); - std::vector Y_f32(C_out * spatial); - - for (size_t n = 0; n < N; ++n) { - const __fp16* Xn = input + n * C_in * H * W; - __fp16* Yn = output + n * C_out * spatial; - - for (size_t ic = 0; ic < C_in; ++ic) { - for (size_t kh = 0; kh < 3; ++kh) { - for (size_t kw = 0; kw < 3; ++kw) { - float* dst = col.data() + (ic * 9 + kh * 3 + kw) * spatial; - for (size_t oh = 0; oh < H_out; ++oh) { - ptrdiff_t ih = static_cast(oh) * 2 + static_cast(kh) - 1; - float* dst_row = dst + oh * W_out; - if (ih < 0 || ih >= static_cast(H)) { - memset(dst_row, 0, W_out * sizeof(float)); - continue; - } - const __fp16* src_row = Xn + ic * H * W + static_cast(ih) * W; - const ptrdiff_t iw_offset = static_cast(kw) - 1; - for (size_t ow = 0; ow < W_out; ++ow) { - ptrdiff_t iw = static_cast(ow) * 2 + iw_offset; - dst_row[ow] = (iw < 0 || iw >= static_cast(W)) - ? 0.0f : static_cast(src_row[iw]); - } - } - } - } - } - - cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans, - static_cast(C_out), static_cast(spatial), static_cast(col_K), - 1.0f, W_f32.data(), static_cast(col_K), - col.data(), static_cast(spatial), - 0.0f, Y_f32.data(), static_cast(spatial)); - - for (size_t oc = 0; oc < C_out; ++oc) { - const float b = bias_f32[oc]; - const float* src = Y_f32.data() + oc * spatial; - __fp16* dst = Yn + oc * spatial; - size_t i = 0; - for (; i + 8 <= spatial; i += 8) { - float32x4_t v0 = vaddq_f32(vld1q_f32(src + i), vdupq_n_f32(b)); - float32x4_t v1 = vaddq_f32(vld1q_f32(src + i + 4), vdupq_n_f32(b)); - vst1q_f16(dst + i, vcombine_f16(vcvt_f16_f32(v0), vcvt_f16_f32(v1))); - } - for (; i < spatial; ++i) - dst[i] = static_cast<__fp16>(src[i] + b); - } - } - return; - } -#endif - - auto in_idx = [&](size_t n, size_t ic, size_t ih, size_t iw) -> size_t { - return ((n * C_in + ic) * H + ih) * W + iw; - }; - auto w_idx = [&](size_t oc, size_t ic, size_t kh, size_t kw) -> size_t { - return (((oc * C_in + ic) * 3 + kh) * 3 + kw); - }; - auto out_idx = [&](size_t n, size_t oc, size_t oh, size_t ow) -> size_t { - return ((n * C_out + oc) * H_out + oh) * W_out + ow; - }; - - constexpr size_t OW_VEC = 4; - - const size_t total_compute = N * C_out * H_out * W_out * C_in * 9; - CactusThreading::ParallelConfig cfg = - (total_compute < 100000) ? CactusThreading::ParallelConfig{SIZE_MAX, SIZE_MAX} - : CactusThreading::Thresholds::ATTENTION; - - CactusThreading::parallel_for_2d(N, C_out, cfg, [&](size_t n, size_t oc) { - const float b0 = bias ? (float)bias[oc] : 0.0f; - - for (size_t oh = 0; oh < H_out; ++oh) { - const ptrdiff_t ih0 = (ptrdiff_t)oh * 2 - 1; - - const bool h_interior = (ih0 >= 0) && ((size_t)(ih0 + 2) < H); - - for (size_t ow = 0; ow < W_out; ) { - const size_t rem = W_out - ow; - - const bool do_vec = (rem >= OW_VEC); - const bool w_interior_vec = - do_vec && - (ow >= 1) && - (((ow + (OW_VEC - 1)) * 2 + 1) < W); - - float32x4_t vacc = vdupq_n_f32(b0); - - if (h_interior && w_interior_vec) { - for (size_t ic = 0; ic < C_in; ++ic) { - - const __fp16* row0 = input + in_idx(n, ic, (size_t)(ih0 + 0), 0); - const __fp16* row1 = input + in_idx(n, ic, (size_t)(ih0 + 1), 0); - const __fp16* row2 = input + in_idx(n, ic, (size_t)(ih0 + 2), 0); - const ptrdiff_t iw_base0 = (ptrdiff_t)ow * 2 - 1; - { - const float w00 = (float)weight[w_idx(oc, ic, 0, 0)]; - const float w01 = (float)weight[w_idx(oc, ic, 0, 1)]; - const float w02 = (float)weight[w_idx(oc, ic, 0, 2)]; - - float x0 = (float)row0[(size_t)(iw_base0 + 0)]; - float x1 = (float)row0[(size_t)(iw_base0 + 2)]; - float x2 = (float)row0[(size_t)(iw_base0 + 4)]; - float x3 = (float)row0[(size_t)(iw_base0 + 6)]; - vacc = vfmaq_f32(vacc, (float32x4_t){x0,x1,x2,x3}, vdupq_n_f32(w00)); - - x0 = (float)row0[(size_t)(iw_base0 + 1)]; - x1 = (float)row0[(size_t)(iw_base0 + 3)]; - x2 = (float)row0[(size_t)(iw_base0 + 5)]; - x3 = (float)row0[(size_t)(iw_base0 + 7)]; - vacc = vfmaq_f32(vacc, (float32x4_t){x0,x1,x2,x3}, vdupq_n_f32(w01)); - - x0 = (float)row0[(size_t)(iw_base0 + 2)]; - x1 = (float)row0[(size_t)(iw_base0 + 4)]; - x2 = (float)row0[(size_t)(iw_base0 + 6)]; - x3 = (float)row0[(size_t)(iw_base0 + 8)]; - vacc = vfmaq_f32(vacc, (float32x4_t){x0,x1,x2,x3}, vdupq_n_f32(w02)); - } - - { - const float w10 = (float)weight[w_idx(oc, ic, 1, 0)]; - const float w11 = (float)weight[w_idx(oc, ic, 1, 1)]; - const float w12 = (float)weight[w_idx(oc, ic, 1, 2)]; - const ptrdiff_t iw_base0 = (ptrdiff_t)ow * 2 - 1; - - float x0 = (float)row1[(size_t)(iw_base0 + 0)]; - float x1 = (float)row1[(size_t)(iw_base0 + 2)]; - float x2 = (float)row1[(size_t)(iw_base0 + 4)]; - float x3 = (float)row1[(size_t)(iw_base0 + 6)]; - vacc = vfmaq_f32(vacc, (float32x4_t){x0,x1,x2,x3}, vdupq_n_f32(w10)); - - x0 = (float)row1[(size_t)(iw_base0 + 1)]; - x1 = (float)row1[(size_t)(iw_base0 + 3)]; - x2 = (float)row1[(size_t)(iw_base0 + 5)]; - x3 = (float)row1[(size_t)(iw_base0 + 7)]; - vacc = vfmaq_f32(vacc, (float32x4_t){x0,x1,x2,x3}, vdupq_n_f32(w11)); - - x0 = (float)row1[(size_t)(iw_base0 + 2)]; - x1 = (float)row1[(size_t)(iw_base0 + 4)]; - x2 = (float)row1[(size_t)(iw_base0 + 6)]; - x3 = (float)row1[(size_t)(iw_base0 + 8)]; - vacc = vfmaq_f32(vacc, (float32x4_t){x0,x1,x2,x3}, vdupq_n_f32(w12)); - } - - { - const float w20 = (float)weight[w_idx(oc, ic, 2, 0)]; - const float w21 = (float)weight[w_idx(oc, ic, 2, 1)]; - const float w22 = (float)weight[w_idx(oc, ic, 2, 2)]; - const ptrdiff_t iw_base0 = (ptrdiff_t)ow * 2 - 1; - - float x0 = (float)row2[(size_t)(iw_base0 + 0)]; - float x1 = (float)row2[(size_t)(iw_base0 + 2)]; - float x2 = (float)row2[(size_t)(iw_base0 + 4)]; - float x3 = (float)row2[(size_t)(iw_base0 + 6)]; - vacc = vfmaq_f32(vacc, (float32x4_t){x0,x1,x2,x3}, vdupq_n_f32(w20)); - - x0 = (float)row2[(size_t)(iw_base0 + 1)]; - x1 = (float)row2[(size_t)(iw_base0 + 3)]; - x2 = (float)row2[(size_t)(iw_base0 + 5)]; - x3 = (float)row2[(size_t)(iw_base0 + 7)]; - vacc = vfmaq_f32(vacc, (float32x4_t){x0,x1,x2,x3}, vdupq_n_f32(w21)); - - x0 = (float)row2[(size_t)(iw_base0 + 2)]; - x1 = (float)row2[(size_t)(iw_base0 + 4)]; - x2 = (float)row2[(size_t)(iw_base0 + 6)]; - x3 = (float)row2[(size_t)(iw_base0 + 8)]; - vacc = vfmaq_f32(vacc, (float32x4_t){x0,x1,x2,x3}, vdupq_n_f32(w22)); - } - } - - __fp16* Y = output + out_idx(n, oc, oh, ow); - const float16x4_t yv = vcvt_f16_f32(vacc); - vst1_f16(Y, yv); - - ow += OW_VEC; - continue; - } - - const size_t tile = do_vec ? OW_VEC : 1; - float acc_s[OW_VEC] = {b0, b0, b0, b0}; - - for (size_t ic = 0; ic < C_in; ++ic) { - for (size_t kh = 0; kh < 3; ++kh) { - const ptrdiff_t ih = ih0 + (ptrdiff_t)kh; - if (ih < 0 || ih >= (ptrdiff_t)H) continue; - - const __fp16* row = input + in_idx(n, ic, (size_t)ih, 0); - - for (size_t kw = 0; kw < 3; ++kw) { - const float wv = (float)weight[w_idx(oc, ic, kh, kw)]; - for (size_t t = 0; t < tile; ++t) { - const size_t ow_t = ow + t; - const ptrdiff_t iw = (ptrdiff_t)ow_t * 2 - 1 + (ptrdiff_t)kw; - if (iw < 0 || iw >= (ptrdiff_t)W) continue; - acc_s[t] += (float)row[(size_t)iw] * wv; - } - } - } - } - - __fp16* Y = output + out_idx(n, oc, oh, ow); - for (size_t t = 0; t < tile; ++t) { - Y[t] = (__fp16)acc_s[t]; - } - ow += tile; - } - } - }); -} - -void cactus_conv2d_depthwise_f16_k3s2p1_nchw( - const __fp16* input, - const __fp16* weight, - const __fp16* bias, - __fp16* output, - size_t N, - size_t C, - size_t H, - size_t W -){ - if (H + 2 < 3 || W + 2 < 3) return; - const size_t H_out = (H - 1) / 2 + 1; - const size_t W_out = (W - 1) / 2 + 1; - - auto in_idx = [&](size_t n, size_t c, size_t ih, size_t iw) -> size_t { - return ((n * C + c) * H + ih) * W + iw; - }; - auto out_idx = [&](size_t n, size_t c, size_t oh, size_t ow) -> size_t { - return ((n * C + c) * H_out + oh) * W_out + ow; - }; - - constexpr size_t OW_VEC = 4; - const size_t total_compute = N * C * H_out * W_out * 9; - CactusThreading::ParallelConfig cfg = - (total_compute < 100000) ? CactusThreading::ParallelConfig{SIZE_MAX, SIZE_MAX} - : CactusThreading::Thresholds::ATTENTION; - - CactusThreading::parallel_for_2d(N, C, cfg, [&](size_t n, size_t c) { - const __fp16* Wc = weight + c * 9; - const float w00 = (float)Wc[0], w01 = (float)Wc[1], w02 = (float)Wc[2]; - const float w10 = (float)Wc[3], w11 = (float)Wc[4], w12 = (float)Wc[5]; - const float w20 = (float)Wc[6], w21 = (float)Wc[7], w22 = (float)Wc[8]; - const float b0 = bias ? (float)bias[c] : 0.0f; - - for (size_t oh = 0; oh < H_out; ++oh) { - const ptrdiff_t ih0 = (ptrdiff_t)oh * 2 - 1; - const bool h_interior = (ih0 >= 0) && ((size_t)(ih0 + 2) < H); - - for (size_t ow = 0; ow < W_out; ) { - const size_t rem = W_out - ow; - const bool do_vec = (rem >= OW_VEC); - const bool w_interior_vec = - do_vec && - (ow >= 1) && - (((ow + (OW_VEC - 1)) * 2 + 1) < W); - - if (h_interior && w_interior_vec) { - const __fp16* row0 = input + in_idx(n, c, (size_t)(ih0 + 0), 0); - const __fp16* row1 = input + in_idx(n, c, (size_t)(ih0 + 1), 0); - const __fp16* row2 = input + in_idx(n, c, (size_t)(ih0 + 2), 0); - const ptrdiff_t iw_base0 = (ptrdiff_t)ow * 2 - 1; - - float32x4_t vacc = vdupq_n_f32(b0); - float x0, x1, x2, x3; - - x0 = (float)row0[(size_t)(iw_base0 + 0)]; - x1 = (float)row0[(size_t)(iw_base0 + 2)]; - x2 = (float)row0[(size_t)(iw_base0 + 4)]; - x3 = (float)row0[(size_t)(iw_base0 + 6)]; - vacc = vfmaq_f32(vacc, (float32x4_t){x0, x1, x2, x3}, vdupq_n_f32(w00)); - - x0 = (float)row0[(size_t)(iw_base0 + 1)]; - x1 = (float)row0[(size_t)(iw_base0 + 3)]; - x2 = (float)row0[(size_t)(iw_base0 + 5)]; - x3 = (float)row0[(size_t)(iw_base0 + 7)]; - vacc = vfmaq_f32(vacc, (float32x4_t){x0, x1, x2, x3}, vdupq_n_f32(w01)); - - x0 = (float)row0[(size_t)(iw_base0 + 2)]; - x1 = (float)row0[(size_t)(iw_base0 + 4)]; - x2 = (float)row0[(size_t)(iw_base0 + 6)]; - x3 = (float)row0[(size_t)(iw_base0 + 8)]; - vacc = vfmaq_f32(vacc, (float32x4_t){x0, x1, x2, x3}, vdupq_n_f32(w02)); - - x0 = (float)row1[(size_t)(iw_base0 + 0)]; - x1 = (float)row1[(size_t)(iw_base0 + 2)]; - x2 = (float)row1[(size_t)(iw_base0 + 4)]; - x3 = (float)row1[(size_t)(iw_base0 + 6)]; - vacc = vfmaq_f32(vacc, (float32x4_t){x0, x1, x2, x3}, vdupq_n_f32(w10)); - - x0 = (float)row1[(size_t)(iw_base0 + 1)]; - x1 = (float)row1[(size_t)(iw_base0 + 3)]; - x2 = (float)row1[(size_t)(iw_base0 + 5)]; - x3 = (float)row1[(size_t)(iw_base0 + 7)]; - vacc = vfmaq_f32(vacc, (float32x4_t){x0, x1, x2, x3}, vdupq_n_f32(w11)); - - x0 = (float)row1[(size_t)(iw_base0 + 2)]; - x1 = (float)row1[(size_t)(iw_base0 + 4)]; - x2 = (float)row1[(size_t)(iw_base0 + 6)]; - x3 = (float)row1[(size_t)(iw_base0 + 8)]; - vacc = vfmaq_f32(vacc, (float32x4_t){x0, x1, x2, x3}, vdupq_n_f32(w12)); - - x0 = (float)row2[(size_t)(iw_base0 + 0)]; - x1 = (float)row2[(size_t)(iw_base0 + 2)]; - x2 = (float)row2[(size_t)(iw_base0 + 4)]; - x3 = (float)row2[(size_t)(iw_base0 + 6)]; - vacc = vfmaq_f32(vacc, (float32x4_t){x0, x1, x2, x3}, vdupq_n_f32(w20)); - - x0 = (float)row2[(size_t)(iw_base0 + 1)]; - x1 = (float)row2[(size_t)(iw_base0 + 3)]; - x2 = (float)row2[(size_t)(iw_base0 + 5)]; - x3 = (float)row2[(size_t)(iw_base0 + 7)]; - vacc = vfmaq_f32(vacc, (float32x4_t){x0, x1, x2, x3}, vdupq_n_f32(w21)); - - x0 = (float)row2[(size_t)(iw_base0 + 2)]; - x1 = (float)row2[(size_t)(iw_base0 + 4)]; - x2 = (float)row2[(size_t)(iw_base0 + 6)]; - x3 = (float)row2[(size_t)(iw_base0 + 8)]; - vacc = vfmaq_f32(vacc, (float32x4_t){x0, x1, x2, x3}, vdupq_n_f32(w22)); - - __fp16* Y = output + out_idx(n, c, oh, ow); - vst1_f16(Y, vcvt_f16_f32(vacc)); - ow += OW_VEC; - continue; - } - - const size_t tile = do_vec ? OW_VEC : 1; - float acc_s[OW_VEC] = {b0, b0, b0, b0}; - for (size_t t = 0; t < tile; ++t) { - const size_t ow_t = ow + t; - for (size_t kh = 0; kh < 3; ++kh) { - const ptrdiff_t ih = ih0 + (ptrdiff_t)kh; - if (ih < 0 || ih >= (ptrdiff_t)H) continue; - - const __fp16* row = input + in_idx(n, c, (size_t)ih, 0); - for (size_t kw = 0; kw < 3; ++kw) { - const ptrdiff_t iw = (ptrdiff_t)ow_t * 2 - 1 + (ptrdiff_t)kw; - if (iw < 0 || iw >= (ptrdiff_t)W) continue; - const float wv = (float)Wc[kh * 3 + kw]; - acc_s[t] += (float)row[(size_t)iw] * wv; - } - } - } - - __fp16* Y = output + out_idx(n, c, oh, ow); - for (size_t t = 0; t < tile; ++t) { - Y[t] = (__fp16)acc_s[t]; - } - ow += tile; - } - } - }); -} - -void cactus_conv2d_pointwise_f16_1x1_nchw_gemm( - const __fp16* input, - const __fp16* weight, - const __fp16* bias, - __fp16* output, - size_t N, - size_t C_in, - size_t H, - size_t W, - size_t C_out -){ - const size_t HW = H * W; - if (HW == 0 || N == 0) return; - - CactusThreading::parallel_for( - N, - CactusThreading::Thresholds::ATTENTION, - [&](size_t n_start, size_t n_end) { - std::vector<__fp16> packed_in(HW * C_in); - std::vector<__fp16> packed_out(HW * C_out); - - for (size_t n = n_start; n < n_end; ++n) { - const __fp16* Xn = input + n * C_in * HW; - __fp16* Yn = output + n * C_out * HW; - - for (size_t hw = 0; hw < HW; ++hw) { - const size_t h = hw / W; - const size_t w = hw - h * W; - __fp16* row = packed_in.data() + hw * C_in; - for (size_t ic = 0; ic < C_in; ++ic) { - row[ic] = Xn[(ic * H + h) * W + w]; - } - } - - cactus_matmul_f16( - packed_in.data(), - weight, - packed_out.data(), - HW, - C_in, - C_out - ); - - for (size_t hw = 0; hw < HW; ++hw) { - const size_t h = hw / W; - const size_t w = hw - h * W; - const __fp16* row = packed_out.data() + hw * C_out; - for (size_t oc = 0; oc < C_out; ++oc) { - float outv = (float)row[oc]; - if (bias) outv += (float)bias[oc]; - Yn[(oc * H + h) * W + w] = (__fp16)outv; - } - } - } - }); -} - -void cactus_conv1d_pointwise_f16_gemm( - const __fp16* input, - const __fp16* weight, - const __fp16* bias, - __fp16* output, - size_t N, - size_t L, - size_t C_in, - size_t C_out -){ - const size_t M = N * L; - if (M == 0) return; - - cactus_matmul_f16(input, weight, output, M, C_in, C_out); - - if (bias) { - for (size_t m = 0; m < M; ++m) { - __fp16* row = output + m * C_out; - for (size_t oc = 0; oc < C_out; ++oc) { - row[oc] = (__fp16)((float)row[oc] + (float)bias[oc]); - } - } - } -} - -void cactus_conv2d_f16_k3s1p1_nchw( - const __fp16* input, - const __fp16* weight, - const __fp16* bias, - __fp16* output, - size_t N, - size_t C_in, size_t H, size_t W, - size_t C_out -) { - const size_t H_out = H; - const size_t W_out = W; - -#ifdef __APPLE__ - const size_t col_K = C_in * 9; - std::vector W_f32(C_out * col_K); - for (size_t oc = 0; oc < C_out; ++oc) { - for (size_t ic = 0; ic < C_in; ++ic) { - for (size_t kh = 0; kh < 3; ++kh) { - for (size_t kw = 0; kw < 3; ++kw) { - W_f32[oc * col_K + ic * 9 + kh * 3 + kw] = - static_cast(weight[((oc * C_in + ic) * 3 + kh) * 3 + kw]); - } - } - } - } - - std::vector bias_f32(C_out, 0.0f); - if (bias) { - for (size_t i = 0; i < C_out; ++i) - bias_f32[i] = static_cast(bias[i]); - } - - std::vector col(col_K * H_out * W_out); - std::vector Y_f32(C_out * H_out * W_out); - - for (size_t n = 0; n < N; ++n) { - const __fp16* Xn = input + n * C_in * H * W; - __fp16* Yn = output + n * C_out * H_out * W_out; - - for (size_t ic = 0; ic < C_in; ++ic) { - for (size_t kh = 0; kh < 3; ++kh) { - for (size_t kw = 0; kw < 3; ++kw) { - float* dst = col.data() + (ic * 9 + kh * 3 + kw) * H_out * W_out; - for (size_t oh = 0; oh < H_out; ++oh) { - ptrdiff_t ih = static_cast(oh) + static_cast(kh) - 1; - float* dst_row = dst + oh * W_out; - if (ih < 0 || ih >= static_cast(H)) { - memset(dst_row, 0, W_out * sizeof(float)); - continue; - } - const __fp16* src_row = Xn + ic * H * W + static_cast(ih) * W; - const ptrdiff_t iw_offset = static_cast(kw) - 1; - size_t ow_start = 0, ow_end = W_out; - if (iw_offset < 0) { dst_row[0] = 0.0f; ow_start = 1; } - if (iw_offset > 0) { dst_row[W_out - 1] = 0.0f; ow_end = W_out - 1; } - size_t ow = ow_start; - for (; ow + 8 <= ow_end; ow += 8) { - float16x8_t v = vld1q_f16(src_row + ow + iw_offset); - vst1q_f32(dst_row + ow, vcvt_f32_f16(vget_low_f16(v))); - vst1q_f32(dst_row + ow + 4, vcvt_f32_f16(vget_high_f16(v))); - } - for (; ow < ow_end; ++ow) - dst_row[ow] = static_cast(src_row[ow + iw_offset]); - } - } - } - } - - cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans, - static_cast(C_out), static_cast(H_out * W_out), static_cast(col_K), - 1.0f, W_f32.data(), static_cast(col_K), - col.data(), static_cast(H_out * W_out), - 0.0f, Y_f32.data(), static_cast(H_out * W_out)); - - for (size_t oc = 0; oc < C_out; ++oc) { - float b = bias_f32[oc]; - const float* src = Y_f32.data() + oc * H_out * W_out; - __fp16* dst = Yn + oc * H_out * W_out; - size_t i = 0; - for (; i + 8 <= H_out * W_out; i += 8) { - float32x4_t v0 = vaddq_f32(vld1q_f32(src + i), vdupq_n_f32(b)); - float32x4_t v1 = vaddq_f32(vld1q_f32(src + i + 4), vdupq_n_f32(b)); - vst1q_f16(dst + i, vcombine_f16(vcvt_f16_f32(v0), vcvt_f16_f32(v1))); - } - for (; i < H_out * W_out; ++i) - dst[i] = static_cast<__fp16>(src[i] + b); - } - } - -#else - const size_t total_compute = N * C_out * H_out * W_out * C_in * 9; - CactusThreading::ParallelConfig cfg = - (total_compute < 100000) ? CactusThreading::ParallelConfig{SIZE_MAX, SIZE_MAX} - : CactusThreading::Thresholds::ATTENTION; - - CactusThreading::parallel_for_2d(N, C_out, cfg, [&](size_t n, size_t oc) { - const float b0 = bias ? static_cast(bias[oc]) : 0.0f; - for (size_t oh = 0; oh < H_out; ++oh) { - for (size_t ow = 0; ow < W_out; ++ow) { - float acc = b0; - for (size_t ic = 0; ic < C_in; ++ic) { - for (size_t kh = 0; kh < 3; ++kh) { - for (size_t kw = 0; kw < 3; ++kw) { - const ptrdiff_t ih = static_cast(oh) + kh - 1; - const ptrdiff_t iw = static_cast(ow) + kw - 1; - if (ih >= 0 && ih < static_cast(H) && - iw >= 0 && iw < static_cast(W)) { - acc += static_cast(input[((n * C_in + ic) * H + ih) * W + iw]) * - static_cast(weight[((oc * C_in + ic) * 3 + kh) * 3 + kw]); - } - } - } - } - output[((n * C_out + oc) * H_out + oh) * W_out + ow] = static_cast<__fp16>(acc); - } - } - }); -#endif -} - -void cactus_maxpool1d_f16( - const __fp16* input, - __fp16* output, - size_t batch_size, - size_t channels, - size_t input_length, - size_t kernel_size, - size_t stride -) { - const size_t output_length = (input_length - kernel_size) / stride + 1; - - CactusThreading::parallel_for(batch_size * channels, CactusThreading::Thresholds::ELEMENT_WISE, - [&](size_t start, size_t end) { - for (size_t bc = start; bc < end; ++bc) { - const size_t b = bc / channels; - const size_t c = bc % channels; - - const __fp16* src = input + b * channels * input_length + c * input_length; - __fp16* dst = output + b * channels * output_length + c * output_length; - - for (size_t i = 0; i < output_length; ++i) { - const size_t in_start = i * stride; - float max_val = -std::numeric_limits::infinity(); - for (size_t k = 0; k < kernel_size; ++k) { - float val = static_cast(src[in_start + k]); - if (val > max_val) max_val = val; - } - dst[i] = static_cast<__fp16>(max_val); - } - } - }); -} diff --git a/cactus/kernel/kernel_deltanet.cpp b/cactus/kernel/kernel_deltanet.cpp deleted file mode 100644 index 4972bc49b..000000000 --- a/cactus/kernel/kernel_deltanet.cpp +++ /dev/null @@ -1,677 +0,0 @@ -#include "kernel.h" -#include "kernel_utils.h" - -#include -#include -#include -#include -#include - -namespace { - -inline float safe_exp_gate(float gate_log) { - const float clamped = std::max(-20.0f, std::min(6.0f, gate_log)); - return std::exp(clamped); -} - -inline void fold_state_scale(std::vector& state, float factor) { - const float32x4_t f4 = vdupq_n_f32(factor); - size_t i = 0; - for (; i + 8 <= state.size(); i += 8) { - vst1q_f32(&state[i], vmulq_f32(vld1q_f32(&state[i]), f4)); - vst1q_f32(&state[i + 4], vmulq_f32(vld1q_f32(&state[i + 4]), f4)); - } - for (; i < state.size(); ++i) state[i] *= factor; -} - -struct GatedDeltaChunkScratch { - std::vector state; - std::vector m_chunk; - std::vector q_chunk; - std::vector k_chunk; - std::vector v_chunk; - std::vector g_chunk; - std::vector beta_chunk; - std::vector n0_proj; - std::vector delta_chunk; - std::vector gram; - std::vector p_prev; - std::vector p_curr; - std::vector inv_p_curr; - std::vector coeff; - std::vector out_acc; - - void ensure(size_t k_dim, size_t v_dim, size_t c_max) { - const size_t kv = k_dim * v_dim; - if (state.size() < kv) state.resize(kv); - if (m_chunk.size() < kv) m_chunk.resize(kv); - if (q_chunk.size() < c_max * k_dim) q_chunk.resize(c_max * k_dim); - if (k_chunk.size() < c_max * k_dim) k_chunk.resize(c_max * k_dim); - if (v_chunk.size() < c_max * v_dim) v_chunk.resize(c_max * v_dim); - if (g_chunk.size() < c_max) g_chunk.resize(c_max); - if (beta_chunk.size() < c_max) beta_chunk.resize(c_max); - if (n0_proj.size() < c_max * v_dim) n0_proj.resize(c_max * v_dim); - if (delta_chunk.size() < c_max * v_dim) delta_chunk.resize(c_max * v_dim); - if (gram.size() < c_max * c_max) gram.resize(c_max * c_max); - if (p_prev.size() < c_max) p_prev.resize(c_max); - if (p_curr.size() < c_max) p_curr.resize(c_max); - if (inv_p_curr.size() < c_max) inv_p_curr.resize(c_max); - if (coeff.size() < c_max * c_max) coeff.resize(c_max * c_max); - if (out_acc.size() < v_dim) out_acc.resize(v_dim); - } -}; - -thread_local GatedDeltaChunkScratch g_gated_deltanet_chunk_scratch; - -inline size_t tuned_gated_deltanet_chunk_size(size_t requested_chunk, size_t k_dim, size_t v_dim) { - size_t chunk = requested_chunk == 0 ? 64 : requested_chunk; - - const char* env_chunk = std::getenv("CACTUS_GATED_DELTANET_CHUNK_SIZE"); - if (env_chunk != nullptr) { - long parsed = std::strtol(env_chunk, nullptr, 10); - if (parsed > 1) { - chunk = static_cast(parsed); - } - } else { - (void)k_dim; - (void)v_dim; - chunk = std::min(chunk, 16); - } - - return std::max(2, chunk); -} - -void gated_deltanet_step( - const __fp16* q_ptr, - const __fp16* k_ptr, - const __fp16* v_ptr, - float gate_log, - float beta, - float scale, - size_t k_dim, - size_t v_dim, - std::vector& state, - float& state_scale, - std::vector& proj, - std::vector& delta, - __fp16* out_ptr) { - const float gate_log_safe = std::isfinite(gate_log) ? gate_log : -20.0f; - const float beta_safe = std::isfinite(beta) ? std::min(1.0f, std::max(0.0f, beta)) : 0.0f; - - if (proj.size() != v_dim) { - proj.assign(v_dim, 0.0f); - } - if (delta.size() != v_dim) { - delta.assign(v_dim, 0.0f); - } - - const float prev_scale = state_scale; - std::fill(proj.begin(), proj.end(), 0.0f); - - for (size_t kd = 0; kd < k_dim; ++kd) { - if (kd + 2 < k_dim) __builtin_prefetch(state.data() + (kd + 2) * v_dim, 0, 3); - const float k_val = static_cast(k_ptr[kd]); - const float* state_row = state.data() + kd * v_dim; - size_t vd = 0; - const float32x4_t k4 = vdupq_n_f32(k_val); - for (; vd + 4 <= v_dim; vd += 4) { - float32x4_t p = vld1q_f32(proj.data() + vd); - float32x4_t s = vld1q_f32(state_row + vd); - p = vfmaq_f32(p, s, k4); - vst1q_f32(proj.data() + vd, p); - } - for (; vd < v_dim; ++vd) { - proj[vd] += state_row[vd] * k_val; - } - } - { - size_t vd = 0; - const float32x4_t scale4 = vdupq_n_f32(prev_scale); - for (; vd + 4 <= v_dim; vd += 4) { - float32x4_t p = vld1q_f32(proj.data() + vd); - p = vmulq_f32(p, scale4); - vst1q_f32(proj.data() + vd, p); - } - for (; vd < v_dim; ++vd) { - proj[vd] *= prev_scale; - } - } - - for (size_t vd = 0; vd < v_dim; ++vd) { - const float v_val = static_cast(v_ptr[vd]); - delta[vd] = (v_val - proj[vd]) * beta_safe; - } - - state_scale = prev_scale * safe_exp_gate(gate_log_safe); - if (!std::isfinite(state_scale) || - std::fabs(state_scale) < 1e-8f || - std::fabs(state_scale) > 1e8f) { - fold_state_scale(state, state_scale); - state_scale = 1.0f; - } - const float inv_scale = 1.0f / state_scale; - - for (size_t kd = 0; kd < k_dim; ++kd) { - if (kd + 2 < k_dim) __builtin_prefetch(state.data() + (kd + 2) * v_dim, 1, 3); - const float k_scaled = static_cast(k_ptr[kd]) * inv_scale; - float* state_row = state.data() + kd * v_dim; - size_t vd = 0; - const float32x4_t k4 = vdupq_n_f32(k_scaled); - for (; vd + 4 <= v_dim; vd += 4) { - float32x4_t s = vld1q_f32(state_row + vd); - float32x4_t d = vld1q_f32(delta.data() + vd); - s = vfmaq_f32(s, d, k4); - vst1q_f32(state_row + vd, s); - } - for (; vd < v_dim; ++vd) { - state_row[vd] += k_scaled * delta[vd]; - } - for (size_t vd = 0; vd < v_dim; ++vd) { - if (!std::isfinite(state_row[vd])) { - state_row[vd] = 0.0f; - } - } - } - - std::fill(proj.begin(), proj.end(), 0.0f); - for (size_t kd = 0; kd < k_dim; ++kd) { - if (kd + 2 < k_dim) __builtin_prefetch(state.data() + (kd + 2) * v_dim, 0, 3); - const float q_val = static_cast(q_ptr[kd]); - const float* state_row = state.data() + kd * v_dim; - size_t vd = 0; - const float32x4_t q4 = vdupq_n_f32(q_val); - for (; vd + 4 <= v_dim; vd += 4) { - float32x4_t o = vld1q_f32(proj.data() + vd); - float32x4_t s = vld1q_f32(state_row + vd); - o = vfmaq_f32(o, s, q4); - vst1q_f32(proj.data() + vd, o); - } - for (; vd < v_dim; ++vd) { - proj[vd] += state_row[vd] * q_val; - } - } - - const float out_scale = state_scale * scale; - for (size_t vd = 0; vd < v_dim; ++vd) { - float acc = proj[vd]; - if (!std::isfinite(acc)) { - acc = 0.0f; - } - out_ptr[vd] = static_cast<__fp16>(acc * out_scale); - } -} - -void gated_deltanet_prefill_old_f16( - const __fp16* q_data, - const __fp16* k_data, - const __fp16* v_data, - const __fp16* g_data, - const __fp16* b_data, - const __fp16* s_data, - __fp16* out, - size_t B, - size_t T, - size_t Hq, - size_t Hv, - size_t K, - size_t V, - float scale) { - const size_t out_seq = T + K; - const size_t qk_repeat = Hv / Hq; - - CactusThreading::parallel_for(B * Hv, CactusThreading::Thresholds::SCALAR_EXPENSIVE, - [&](size_t bh_start, size_t bh_end) { - std::vector state(K * V); - std::vector proj(V); - std::vector delta(V); - - for (size_t bh = bh_start; bh < bh_end; ++bh) { - const size_t batch = bh / Hv; - const size_t v_head = bh % Hv; - const size_t qk_head = v_head / qk_repeat; - - for (size_t kd = 0; kd < K; ++kd) { - for (size_t vd = 0; vd < V; ++vd) { - const size_t s_idx = (((batch * K + kd) * Hv + v_head) * V + vd); - state[kd * V + vd] = static_cast(s_data[s_idx]); - } - } - - float state_scale = 1.0f; - for (size_t t = 0; t < T; ++t) { - const size_t qkv_k_base = (((batch * T + t) * Hq + qk_head) * K); - const size_t qkv_v_base = (((batch * T + t) * Hv + v_head) * V); - const size_t gb_idx = ((batch * T + t) * Hv + v_head); - - gated_deltanet_step( - q_data + qkv_k_base, - k_data + qkv_k_base, - v_data + qkv_v_base, - static_cast(g_data[gb_idx]), - static_cast(b_data[gb_idx]), - scale, - K, - V, - state, - state_scale, - proj, - delta, - out + (((batch * out_seq + t) * Hv + v_head) * V)); - } - - fold_state_scale(state, state_scale); - for (size_t kd = 0; kd < K; ++kd) { - for (size_t vd = 0; vd < V; ++vd) { - const size_t out_idx = (((batch * out_seq + (T + kd)) * Hv + v_head) * V + vd); - out[out_idx] = static_cast<__fp16>(state[kd * V + vd]); - } - } - } - }); -} - -void gated_deltanet_prefill_chunked_f16( - const __fp16* q_data, - const __fp16* k_data, - const __fp16* v_data, - const __fp16* g_data, - const __fp16* b_data, - const __fp16* s_data, - __fp16* out, - size_t B, - size_t T, - size_t Hq, - size_t Hv, - size_t K, - size_t V, - size_t chunk_size, - float scale) { - const size_t out_seq = T + K; - const size_t qk_repeat = Hv / Hq; - constexpr float kMinAbs = 1e-12f; - - CactusThreading::parallel_for(B * Hv, CactusThreading::Thresholds::SCALAR_EXPENSIVE, - [&](size_t bh_start, size_t bh_end) { - auto& ws = g_gated_deltanet_chunk_scratch; - ws.ensure(K, V, chunk_size); - - float* state = ws.state.data(); - float* m_chunk = ws.m_chunk.data(); - float* q_chunk = ws.q_chunk.data(); - float* k_chunk = ws.k_chunk.data(); - float* v_chunk = ws.v_chunk.data(); - float* g_chunk = ws.g_chunk.data(); - float* beta_chunk = ws.beta_chunk.data(); - float* n0_proj = ws.n0_proj.data(); - float* delta_chunk = ws.delta_chunk.data(); - float* gram = ws.gram.data(); - float* p_prev = ws.p_prev.data(); - float* p_curr = ws.p_curr.data(); - float* inv_p_curr = ws.inv_p_curr.data(); - float* coeff = ws.coeff.data(); - float* out_acc = ws.out_acc.data(); - - for (size_t bh = bh_start; bh < bh_end; ++bh) { - const size_t batch = bh / Hv; - const size_t v_head = bh % Hv; - const size_t qk_head = v_head / qk_repeat; - - for (size_t kd = 0; kd < K; ++kd) { - const size_t s_idx = (((batch * K + kd) * Hv + v_head) * V); - cactus_fp16_to_fp32(s_data + s_idx, state + kd * V, V); - } - - for (size_t t0 = 0; t0 < T; t0 += chunk_size) { - const size_t C = std::min(chunk_size, T - t0); - const size_t CV = C * V; - const size_t CC = C * C; - - std::fill(n0_proj, n0_proj + CV, 0.0f); - std::fill(delta_chunk, delta_chunk + CV, 0.0f); - std::fill(gram, gram + CC, 0.0f); - std::fill(coeff, coeff + CC, 0.0f); - std::fill(m_chunk, m_chunk + (K * V), 0.0f); - - // Fused fp16→fp32 conversion + n0_proj: k_chunk stays in L1 - for (size_t ct = 0; ct < C; ++ct) { - const size_t t = t0 + ct; - const size_t qkv_k_base = (((batch * T + t) * Hq + qk_head) * K); - const size_t qkv_v_base = (((batch * T + t) * Hv + v_head) * V); - const size_t gb_idx = ((batch * T + t) * Hv + v_head); - - // Inline fp16→fp32 for q - { - const __fp16* src = q_data + qkv_k_base; - float* dst = q_chunk + ct * K; - size_t i = 0; - for (; i + 8 <= K; i += 8) { - float16x8_t in = vld1q_f16(src + i); - vst1q_f32(dst + i, vcvt_f32_f16(vget_low_f16(in))); - vst1q_f32(dst + i + 4, vcvt_f32_f16(vget_high_f16(in))); - } - for (; i < K; ++i) dst[i] = static_cast(src[i]); - } - - // Inline fp16→fp32 for k - { - const __fp16* src = k_data + qkv_k_base; - float* dst = k_chunk + ct * K; - size_t i = 0; - for (; i + 8 <= K; i += 8) { - float16x8_t in = vld1q_f16(src + i); - vst1q_f32(dst + i, vcvt_f32_f16(vget_low_f16(in))); - vst1q_f32(dst + i + 4, vcvt_f32_f16(vget_high_f16(in))); - } - for (; i < K; ++i) dst[i] = static_cast(src[i]); - } - - // Inline fp16→fp32 for v - { - const __fp16* src = v_data + qkv_v_base; - float* dst = v_chunk + ct * V; - size_t i = 0; - for (; i + 8 <= V; i += 8) { - float16x8_t in = vld1q_f16(src + i); - vst1q_f32(dst + i, vcvt_f32_f16(vget_low_f16(in))); - vst1q_f32(dst + i + 4, vcvt_f32_f16(vget_high_f16(in))); - } - for (; i < V; ++i) dst[i] = static_cast(src[i]); - } - - const float gate_log = static_cast(g_data[gb_idx]); - const float beta = static_cast(b_data[gb_idx]); - g_chunk[ct] = safe_exp_gate(std::isfinite(gate_log) ? gate_log : -20.0f); - beta_chunk[ct] = std::isfinite(beta) ? std::min(1.0f, std::max(0.0f, beta)) : 0.0f; - - // n0_proj fused here so k_chunk[ct] is still in L1 - const float* k_row = k_chunk + ct * K; - float* p_row = n0_proj + ct * V; - for (size_t kd = 0; kd < K; ++kd) { - const float kval = k_row[kd]; - const float* s_row = state + kd * V; - size_t vd = 0; - const float32x4_t kv4 = vdupq_n_f32(kval); - for (; vd + 8 <= V; vd += 8) { - float32x4_t p4 = vld1q_f32(p_row + vd); - const float32x4_t slo = vld1q_f32(s_row + vd); - const float32x4_t shi = vld1q_f32(s_row + vd + 4); - p4 = vfmaq_f32(p4, slo, kv4); - vst1q_f32(p_row + vd, p4); - p4 = vld1q_f32(p_row + vd + 4); - p4 = vfmaq_f32(p4, shi, kv4); - vst1q_f32(p_row + vd + 4, p4); - } - for (; vd < V; ++vd) { - p_row[vd] += kval * s_row[vd]; - } - } - } - - for (size_t t = 0; t < C; ++t) { - const float* kt = k_chunk + t * K; - for (size_t j = 0; j < t; ++j) { - const float* kj = k_chunk + j * K; - float dot = 0.0f; - size_t kd = 0; - float32x4_t acc4 = vdupq_n_f32(0.0f); - for (; kd + 8 <= K; kd += 8) { - const float32x4_t al = vld1q_f32(kt + kd); - const float32x4_t ah = vld1q_f32(kt + kd + 4); - const float32x4_t bl = vld1q_f32(kj + kd); - const float32x4_t bh = vld1q_f32(kj + kd + 4); - acc4 = vfmaq_f32(acc4, al, bl); - acc4 = vfmaq_f32(acc4, ah, bh); - } - dot += vaddvq_f32(acc4); - for (; kd < K; ++kd) { - dot += kt[kd] * kj[kd]; - } - gram[t * C + j] = dot; - } - } - - float running_p = 1.0f; - for (size_t t = 0; t < C; ++t) { - p_prev[t] = running_p; - running_p *= g_chunk[t]; - p_curr[t] = running_p; - inv_p_curr[t] = (std::fabs(running_p) > kMinAbs) ? (1.0f / running_p) : 0.0f; - } - - for (size_t t = 0; t < C; ++t) { - const float pp = p_prev[t]; - float* coeff_row = coeff + t * C; - for (size_t j = 0; j < t; ++j) { - const float inv_pc = inv_p_curr[j]; - coeff_row[j] = inv_pc == 0.0f ? 0.0f : (pp * inv_pc * gram[t * C + j]); - } - } - - for (size_t t = 0; t < C; ++t) { - const float pp = p_prev[t]; - const float bt = beta_chunk[t]; - const float* n0_row = n0_proj + t * V; - const float* v_row = v_chunk + t * V; - float* d_row = delta_chunk + t * V; - - size_t vd = 0; - const float32x4_t pp4 = vdupq_n_f32(pp); - const float32x4_t bt4 = vdupq_n_f32(bt); - for (; vd + 8 <= V; vd += 8) { - float32x4_t acc_lo = vmulq_f32(vld1q_f32(n0_row + vd), pp4); - float32x4_t acc_hi = vmulq_f32(vld1q_f32(n0_row + vd + 4), pp4); - for (size_t j = 0; j < t; ++j) { - const float c = coeff[t * C + j]; - const float* dj = delta_chunk + j * V + vd; - acc_lo = vmlaq_n_f32(acc_lo, vld1q_f32(dj), c); - acc_hi = vmlaq_n_f32(acc_hi, vld1q_f32(dj + 4), c); - } - vst1q_f32(d_row + vd, vmulq_f32(vsubq_f32(vld1q_f32(v_row + vd), acc_lo), bt4)); - vst1q_f32(d_row + vd + 4, vmulq_f32(vsubq_f32(vld1q_f32(v_row + vd + 4), acc_hi), bt4)); - } - for (; vd < V; ++vd) { - float acc = pp * n0_row[vd]; - for (size_t j = 0; j < t; ++j) { - acc += coeff[t * C + j] * delta_chunk[j * V + vd]; - } - d_row[vd] = bt * (v_row[vd] - acc); - } - } - - running_p = 1.0f; - for (size_t t = 0; t < C; ++t) { - const float gt = g_chunk[t]; - const float* k_row = k_chunk + t * K; - const float* q_row = q_chunk + t * K; - const float* d_row = delta_chunk + t * V; - - running_p *= gt; - const float p_run = running_p; - - for (size_t kd = 0; kd < K; ++kd) { - const float kval = k_row[kd]; - float* m_row = m_chunk + kd * V; - size_t vd = 0; - const float32x4_t gt4 = vdupq_n_f32(gt); - const float32x4_t kv4 = vdupq_n_f32(kval); - for (; vd + 8 <= V; vd += 8) { - float32x4_t ml = vld1q_f32(m_row + vd); - float32x4_t mh = vld1q_f32(m_row + vd + 4); - const float32x4_t dl = vld1q_f32(d_row + vd); - const float32x4_t dh = vld1q_f32(d_row + vd + 4); - ml = vmulq_f32(ml, gt4); - mh = vmulq_f32(mh, gt4); - ml = vfmaq_f32(ml, dl, kv4); - mh = vfmaq_f32(mh, dh, kv4); - vst1q_f32(m_row + vd, ml); - vst1q_f32(m_row + vd + 4, mh); - } - for (; vd < V; ++vd) { - m_row[vd] = gt * m_row[vd] + kval * d_row[vd]; - } - } - - const size_t out_base = (((batch * out_seq + (t0 + t)) * Hv + v_head) * V); - std::fill(out_acc, out_acc + V, 0.0f); - for (size_t kd = 0; kd < K; ++kd) { - const float qval = q_row[kd]; - const float* s_row = state + kd * V; - const float* m_row = m_chunk + kd * V; - size_t vd = 0; - const float32x4_t q4 = vdupq_n_f32(qval); - const float32x4_t p4 = vdupq_n_f32(p_run); - for (; vd + 8 <= V; vd += 8) { - float32x4_t acc4 = vld1q_f32(out_acc + vd); - float32x4_t acc4h = vld1q_f32(out_acc + vd + 4); - const float32x4_t sl = vld1q_f32(s_row + vd); - const float32x4_t sh = vld1q_f32(s_row + vd + 4); - const float32x4_t ml = vld1q_f32(m_row + vd); - const float32x4_t mh = vld1q_f32(m_row + vd + 4); - const float32x4_t nl = vfmaq_f32(ml, sl, p4); - const float32x4_t nh = vfmaq_f32(mh, sh, p4); - acc4 = vfmaq_f32(acc4, nl, q4); - acc4h = vfmaq_f32(acc4h, nh, q4); - vst1q_f32(out_acc + vd, acc4); - vst1q_f32(out_acc + vd + 4, acc4h); - } - for (; vd < V; ++vd) { - out_acc[vd] += qval * (p_run * s_row[vd] + m_row[vd]); - } - } - for (size_t vd = 0; vd < V; ++vd) { - out[out_base + vd] = static_cast<__fp16>(out_acc[vd] * scale); - } - } - - const float p_end = running_p; - for (size_t kd = 0; kd < K; ++kd) { - float* s_row = state + kd * V; - const float* m_row = m_chunk + kd * V; - size_t vd = 0; - const float32x4_t p4 = vdupq_n_f32(p_end); - for (; vd + 8 <= V; vd += 8) { - float32x4_t sl = vld1q_f32(s_row + vd); - float32x4_t sh = vld1q_f32(s_row + vd + 4); - const float32x4_t ml = vld1q_f32(m_row + vd); - const float32x4_t mh = vld1q_f32(m_row + vd + 4); - sl = vfmaq_f32(ml, sl, p4); - sh = vfmaq_f32(mh, sh, p4); - vst1q_f32(s_row + vd, sl); - vst1q_f32(s_row + vd + 4, sh); - } - for (; vd < V; ++vd) { - s_row[vd] = p_end * s_row[vd] + m_row[vd]; - } - } - } - - for (size_t kd = 0; kd < K; ++kd) { - const size_t out_idx = (((batch * out_seq + (T + kd)) * Hv + v_head) * V); - cactus_fp32_to_fp16(state + kd * V, out + out_idx, V); - } - } - }); -} - -} // namespace - -void cactus_gated_deltanet_decode_f16( - const __fp16* q_data, - const __fp16* k_data, - const __fp16* v_data, - const __fp16* g_data, - const __fp16* b_data, - const __fp16* s_data, - __fp16* out, - size_t B, - size_t Hq, - size_t Hv, - size_t K, - size_t V, - float scale) { - const size_t qk_repeat = Hv / Hq; - const size_t out_seq = 1 + K; - - CactusThreading::parallel_for(B * Hv, CactusThreading::Thresholds::SCALAR_EXPENSIVE, - [&](size_t bh_start, size_t bh_end) { - std::vector state(K * V); - std::vector proj(V); - std::vector delta(V); - - for (size_t bh = bh_start; bh < bh_end; ++bh) { - const size_t batch = bh / Hv; - const size_t v_head = bh % Hv; - const size_t qk_head = v_head / qk_repeat; - - for (size_t kd = 0; kd < K; ++kd) { - for (size_t vd = 0; vd < V; ++vd) { - const size_t s_idx = (((batch * K + kd) * Hv + v_head) * V + vd); - state[kd * V + vd] = static_cast(s_data[s_idx]); - } - } - - float state_scale = 1.0f; - const size_t qk_base = ((batch * Hq + qk_head) * K); - const size_t v_base = ((batch * Hv + v_head) * V); - const float gate_log = static_cast(g_data[batch * Hv + v_head]); - const float beta = static_cast(b_data[batch * Hv + v_head]); - - gated_deltanet_step( - q_data + qk_base, - k_data + qk_base, - v_data + v_base, - gate_log, - beta, - scale, - K, - V, - state, - state_scale, - proj, - delta, - out + (((batch * out_seq) * Hv + v_head) * V)); - - fold_state_scale(state, state_scale); - for (size_t kd = 0; kd < K; ++kd) { - for (size_t vd = 0; vd < V; ++vd) { - const size_t out_idx = (((batch * out_seq + (1 + kd)) * Hv + v_head) * V + vd); - out[out_idx] = static_cast<__fp16>(state[kd * V + vd]); - } - } - } - }); -} - -void cactus_gated_deltanet_prefill_f16( - const __fp16* q_data, - const __fp16* k_data, - const __fp16* v_data, - const __fp16* g_data, - const __fp16* b_data, - const __fp16* s_data, - __fp16* out, - size_t B, - size_t T, - size_t Hq, - size_t Hv, - size_t K, - size_t V, - size_t requested_chunk_size, - float scale) { - const char* force_old = std::getenv("CACTUS_GATED_DELTANET_PREFILL_OLD"); - if (force_old != nullptr && std::atoi(force_old) != 0) { - gated_deltanet_prefill_old_f16(q_data, k_data, v_data, g_data, b_data, s_data, out, - B, T, Hq, Hv, K, V, scale); - return; - } - - if (requested_chunk_size <= 1) { - gated_deltanet_prefill_old_f16(q_data, k_data, v_data, g_data, b_data, s_data, out, - B, T, Hq, Hv, K, V, scale); - return; - } - - const size_t chunk_size = tuned_gated_deltanet_chunk_size(requested_chunk_size, K, V); - gated_deltanet_prefill_chunked_f16(q_data, k_data, v_data, g_data, b_data, s_data, out, - B, T, Hq, Hv, K, V, chunk_size, scale); -} diff --git a/cactus/kernel/kernel_i8mm.cpp b/cactus/kernel/kernel_i8mm.cpp deleted file mode 100644 index c8c976173..000000000 --- a/cactus/kernel/kernel_i8mm.cpp +++ /dev/null @@ -1,189 +0,0 @@ -#include - -#ifndef __ARM_FEATURE_MATMUL_INT8 -#error "kernel_i8mm.cpp must be compiled with I8MM enabled (e.g. -march=armv8.2-a+fp16+simd+dotprod+i8mm)" -#endif - -#include "kernel.h" -#include "kernel_utils.h" -#include -#include - -void cactus_gemv_int8_i8mm( - const int8_t* A, - const float A_scale, - const int8_t* B, - const __fp16* B_scales, - __fp16* C, - size_t K, size_t N, - size_t group_size -) { - if (K == 0 || N == 0) return; - - const size_t num_groups = K / group_size; - const size_t N_blocks = (N + 3) / 4; - - auto process_blocks = [=](size_t block_start, size_t block_end) { - for (size_t n_block = block_start; n_block < block_end; ++n_block) { - const size_t n_start = n_block * 4; - const size_t actual_n = std::min(size_t(4), N - n_start); - - float32x4_t running_sum = vdupq_n_f32(0.0f); - - for (size_t g = 0; g < num_groups; g++) { - const size_t k_base = g * group_size; - const int8_t* a_ptr = A + k_base; - const int8_t* b_ptr = B + (n_block * K + k_base) * 4; - - __builtin_prefetch(b_ptr + group_size * 8, 0, 3); - - int32x4_t acc_01 = vdupq_n_s32(0); - int32x4_t acc_23 = vdupq_n_s32(0); - - for (size_t k_off = 0; k_off < group_size; k_off += 8) { - int32x4_t chunk0 = vreinterpretq_s32_s8(vld1q_s8(b_ptr + k_off * 4)); - int32x4_t chunk1 = vreinterpretq_s32_s8(vld1q_s8(b_ptr + k_off * 4 + 16)); - - int8x16_t b01 = vreinterpretq_s8_s32(vzip1q_s32(chunk0, chunk1)); - int8x16_t b23 = vreinterpretq_s8_s32(vzip2q_s32(chunk0, chunk1)); - - int8x8_t a8 = vld1_s8(a_ptr + k_off); - int8x16_t a_dup = vcombine_s8(a8, a8); - - acc_01 = vmmlaq_s32(acc_01, a_dup, b01); - acc_23 = vmmlaq_s32(acc_23, a_dup, b23); - } - - float32x2_t r01 = vcvt_f32_s32(vget_low_s32(acc_01)); - float32x2_t r23 = vcvt_f32_s32(vget_low_s32(acc_23)); - float32x4_t group_result = vcombine_f32(r01, r23); - - const __fp16* scale_ptr = B_scales + (n_block * num_groups + g) * 4; - float32x4_t scales = vcvt_f32_f16(vld1_f16(scale_ptr)); - running_sum = vmlaq_f32(running_sum, group_result, scales); - } - - float32x4_t result = vmulq_n_f32(running_sum, A_scale); - float16x4_t result_f16 = vcvt_f16_f32(result); - - if (actual_n == 4) { - vst1_f16(C + n_start, result_f16); - } else { - for (size_t ni = 0; ni < actual_n; ni++) { - C[n_start + ni] = vget_lane_f16(result_f16, 0); - result_f16 = vext_f16(result_f16, result_f16, 1); - } - } - } - }; - - auto& pool = CactusThreading::get_thread_pool(); - size_t num_threads = CactusThreading::GemmThreading::get_gemv_threads(N_blocks, pool.num_workers()); - num_threads = std::min(num_threads, N_blocks); - - if (num_threads <= 1) { - process_blocks(0, N_blocks); - } else { - pool.enqueue_n_threads(N_blocks, num_threads, process_blocks); - pool.wait_all(); - } -} - -void cactus_gemm_int8_i8mm( - const int8_t* A, - const float* A_scales, - const int8_t* B, - const __fp16* B_scales, - __fp16* C, - size_t M, size_t K, size_t N, - size_t group_size -) { - if (M == 0 || K == 0 || N == 0) return; - - const size_t num_groups = K / group_size; - const size_t N_blocks = (N + 3) / 4; - const size_t M_pairs = (M + 1) / 2; - const size_t total_tiles = M_pairs * N_blocks; - - CactusThreading::parallel_gemm_tiles(M, total_tiles, - [=](size_t tile_start, size_t tile_end) { - for (size_t tile_idx = tile_start; tile_idx < tile_end; ++tile_idx) { - const size_t m_pair = tile_idx / N_blocks; - const size_t n_block = tile_idx % N_blocks; - const size_t m0 = m_pair * 2; - const size_t m1 = std::min(m0 + 1, M - 1); - const size_t n_start = n_block * 4; - const size_t actual_n = std::min(size_t(4), N - n_start); - - const int8_t* a_row0 = A + m0 * K; - const int8_t* a_row1 = A + m1 * K; - - float32x4_t sum_row0 = vdupq_n_f32(0.0f); - float32x4_t sum_row1 = vdupq_n_f32(0.0f); - - for (size_t g = 0; g < num_groups; g++) { - const size_t k_base = g * group_size; - const int8_t* b_ptr = B + (n_block * K + k_base) * 4; - - __builtin_prefetch(b_ptr + group_size * 8, 0, 3); - - int32x4_t acc_01 = vdupq_n_s32(0); - int32x4_t acc_23 = vdupq_n_s32(0); - - for (size_t k_off = 0; k_off < group_size; k_off += 8) { - int32x4_t chunk0 = vreinterpretq_s32_s8(vld1q_s8(b_ptr + k_off * 4)); - int32x4_t chunk1 = vreinterpretq_s32_s8(vld1q_s8(b_ptr + k_off * 4 + 16)); - - int8x16_t b01 = vreinterpretq_s8_s32(vzip1q_s32(chunk0, chunk1)); - int8x16_t b23 = vreinterpretq_s8_s32(vzip2q_s32(chunk0, chunk1)); - - int8x8_t a0_8 = vld1_s8(a_row0 + k_base + k_off); - int8x8_t a1_8 = vld1_s8(a_row1 + k_base + k_off); - int8x16_t a_2rows = vcombine_s8(a0_8, a1_8); - - acc_01 = vmmlaq_s32(acc_01, a_2rows, b01); - acc_23 = vmmlaq_s32(acc_23, a_2rows, b23); - } - - const __fp16* scale_ptr = B_scales + (n_block * num_groups + g) * 4; - float32x4_t scales = vcvt_f32_f16(vld1_f16(scale_ptr)); - - float32x2_t r0_01 = vcvt_f32_s32(vget_low_s32(acc_01)); - float32x2_t r0_23 = vcvt_f32_s32(vget_low_s32(acc_23)); - float32x4_t group_row0 = vcombine_f32(r0_01, r0_23); - sum_row0 = vmlaq_f32(sum_row0, group_row0, scales); - - float32x2_t r1_01 = vcvt_f32_s32(vget_high_s32(acc_01)); - float32x2_t r1_23 = vcvt_f32_s32(vget_high_s32(acc_23)); - float32x4_t group_row1 = vcombine_f32(r1_01, r1_23); - sum_row1 = vmlaq_f32(sum_row1, group_row1, scales); - } - - { - float32x4_t result = vmulq_n_f32(sum_row0, A_scales[m0]); - float16x4_t result_f16 = vcvt_f16_f32(result); - if (actual_n == 4) { - vst1_f16(C + m0 * N + n_start, result_f16); - } else { - for (size_t ni = 0; ni < actual_n; ni++) { - C[m0 * N + n_start + ni] = vget_lane_f16(result_f16, 0); - result_f16 = vext_f16(result_f16, result_f16, 1); - } - } - } - - if (m0 + 1 < M) { - float32x4_t result = vmulq_n_f32(sum_row1, A_scales[m1]); - float16x4_t result_f16 = vcvt_f16_f32(result); - if (actual_n == 4) { - vst1_f16(C + m1 * N + n_start, result_f16); - } else { - for (size_t ni = 0; ni < actual_n; ni++) { - C[m1 * N + n_start + ni] = vget_lane_f16(result_f16, 0); - result_f16 = vext_f16(result_f16, result_f16, 1); - } - } - } - } - }); -} diff --git a/cactus/kernel/kernel_lstm.cpp b/cactus/kernel/kernel_lstm.cpp deleted file mode 100644 index 84d0077a1..000000000 --- a/cactus/kernel/kernel_lstm.cpp +++ /dev/null @@ -1,117 +0,0 @@ -#include "kernel.h" -#include "kernel_utils.h" -#include -#include -#include - -void cactus_lstm_cell_f16( - const __fp16* x_input, - const __fp16* h_prev, - const __fp16* c_prev, - const __fp16* weight_ih, - const __fp16* weight_hh, - const __fp16* bias_ih, - const __fp16* bias_hh, - __fp16* h_new, - __fp16* c_new, - size_t batch_size, - size_t input_size, - size_t hidden_size -) { - constexpr size_t SIMD_WIDTH = 8; - const size_t gate_size = 4 * hidden_size; - - std::vector<__fp16> gates_ih(batch_size * gate_size); - std::vector<__fp16> gates_hh(batch_size * gate_size); - - cactus_matmul_f16(x_input, weight_ih, gates_ih.data(), batch_size, input_size, gate_size); - cactus_matmul_f16(h_prev, weight_hh, gates_hh.data(), batch_size, hidden_size, gate_size); - - const size_t simd_end = (hidden_size / SIMD_WIDTH) * SIMD_WIDTH; - - CactusThreading::parallel_for(batch_size, CactusThreading::Thresholds::SCALAR_EXPENSIVE, - [&](size_t batch_start, size_t batch_end) { - const float32x4_t one = vdupq_n_f32(1.0f); - - for (size_t b = batch_start; b < batch_end; ++b) { - const size_t gate_offset = b * gate_size; - const size_t hidden_offset = b * hidden_size; - - for (size_t h = 0; h < simd_end; h += SIMD_WIDTH) { - float16x8_t i_gate = vaddq_f16(vaddq_f16(vld1q_f16(&gates_ih[gate_offset + h]), - vld1q_f16(&gates_hh[gate_offset + h])), - vaddq_f16(vld1q_f16(&bias_ih[h]), - vld1q_f16(&bias_hh[h]))); - - float16x8_t f_gate = vaddq_f16(vaddq_f16(vld1q_f16(&gates_ih[gate_offset + hidden_size + h]), - vld1q_f16(&gates_hh[gate_offset + hidden_size + h])), - vaddq_f16(vld1q_f16(&bias_ih[hidden_size + h]), - vld1q_f16(&bias_hh[hidden_size + h]))); - - float16x8_t g_gate = vaddq_f16(vaddq_f16(vld1q_f16(&gates_ih[gate_offset + 2 * hidden_size + h]), - vld1q_f16(&gates_hh[gate_offset + 2 * hidden_size + h])), - vaddq_f16(vld1q_f16(&bias_ih[2 * hidden_size + h]), - vld1q_f16(&bias_hh[2 * hidden_size + h]))); - - float16x8_t o_gate = vaddq_f16(vaddq_f16(vld1q_f16(&gates_ih[gate_offset + 3 * hidden_size + h]), - vld1q_f16(&gates_hh[gate_offset + 3 * hidden_size + h])), - vaddq_f16(vld1q_f16(&bias_ih[3 * hidden_size + h]), - vld1q_f16(&bias_hh[3 * hidden_size + h]))); - - float32x4_t i_low = vcvt_f32_f16(vget_low_f16(i_gate)); - float32x4_t i_high = vcvt_f32_f16(vget_high_f16(i_gate)); - float16x8_t i_act = vcombine_f16(vcvt_f16_f32(vdivq_f32(one, vaddq_f32(one, fast_exp_f32x4(vnegq_f32(i_low))))), - vcvt_f16_f32(vdivq_f32(one, vaddq_f32(one, fast_exp_f32x4(vnegq_f32(i_high)))))); - - float32x4_t f_low = vcvt_f32_f16(vget_low_f16(f_gate)); - float32x4_t f_high = vcvt_f32_f16(vget_high_f16(f_gate)); - float16x8_t f_act = vcombine_f16(vcvt_f16_f32(vdivq_f32(one, vaddq_f32(one, fast_exp_f32x4(vnegq_f32(f_low))))), - vcvt_f16_f32(vdivq_f32(one, vaddq_f32(one, fast_exp_f32x4(vnegq_f32(f_high)))))); - - float32x4_t g_low = vcvt_f32_f16(vget_low_f16(g_gate)); - float32x4_t g_high = vcvt_f32_f16(vget_high_f16(g_gate)); - float16x8_t g_act = vcombine_f16(vcvt_f16_f32(fast_tanh_f32x4(g_low)), - vcvt_f16_f32(fast_tanh_f32x4(g_high))); - - float32x4_t o_low = vcvt_f32_f16(vget_low_f16(o_gate)); - float32x4_t o_high = vcvt_f32_f16(vget_high_f16(o_gate)); - float16x8_t o_act = vcombine_f16(vcvt_f16_f32(vdivq_f32(one, vaddq_f32(one, fast_exp_f32x4(vnegq_f32(o_low))))), - vcvt_f16_f32(vdivq_f32(one, vaddq_f32(one, fast_exp_f32x4(vnegq_f32(o_high)))))); - - float16x8_t c_prev_vec = vld1q_f16(&c_prev[hidden_offset + h]); - float16x8_t c_update = vfmaq_f16(vmulq_f16(f_act, c_prev_vec), i_act, g_act); - vst1q_f16(&c_new[hidden_offset + h], c_update); - - float32x4_t c_low = vcvt_f32_f16(vget_low_f16(c_update)); - float32x4_t c_high = vcvt_f32_f16(vget_high_f16(c_update)); - float16x8_t c_tanh = vcombine_f16(vcvt_f16_f32(fast_tanh_f32x4(c_low)), - vcvt_f16_f32(fast_tanh_f32x4(c_high))); - vst1q_f16(&h_new[hidden_offset + h], vmulq_f16(o_act, c_tanh)); - } - - for (size_t h = simd_end; h < hidden_size; ++h) { - float i_gate_val = static_cast(gates_ih[gate_offset + h] + - gates_hh[gate_offset + h] + - bias_ih[h] + bias_hh[h]); - float f_gate_val = static_cast(gates_ih[gate_offset + hidden_size + h] + - gates_hh[gate_offset + hidden_size + h] + - bias_ih[hidden_size + h] + bias_hh[hidden_size + h]); - float g_gate_val = static_cast(gates_ih[gate_offset + 2 * hidden_size + h] + - gates_hh[gate_offset + 2 * hidden_size + h] + - bias_ih[2 * hidden_size + h] + bias_hh[2 * hidden_size + h]); - float o_gate_val = static_cast(gates_ih[gate_offset + 3 * hidden_size + h] + - gates_hh[gate_offset + 3 * hidden_size + h] + - bias_ih[3 * hidden_size + h] + bias_hh[3 * hidden_size + h]); - - float i_act = 1.0f / (1.0f + expf(-i_gate_val)); - float f_act = 1.0f / (1.0f + expf(-f_gate_val)); - float g_act = tanhf(g_gate_val); - float o_act = 1.0f / (1.0f + expf(-o_gate_val)); - - float c_val = f_act * static_cast(c_prev[hidden_offset + h]) + i_act * g_act; - c_new[hidden_offset + h] = static_cast<__fp16>(c_val); - h_new[hidden_offset + h] = static_cast<__fp16>(o_act * tanhf(c_val)); - } - } - }); -} diff --git a/cactus/kernel/kernel_matmul.cpp b/cactus/kernel/kernel_matmul.cpp deleted file mode 100644 index 929663aa2..000000000 --- a/cactus/kernel/kernel_matmul.cpp +++ /dev/null @@ -1,863 +0,0 @@ -#include "kernel.h" -#include "kernel_utils.h" -#include "../graph/graph.h" -#include -#include -#include -#include -#include - -#ifdef __APPLE__ -#include -constexpr size_t ACCELERATE_M_THRESHOLD = 4; -constexpr size_t ACCELERATE_K_THRESHOLD = 256; -#endif - -// Do NOT Remove: Uncomment for testing on various paths -// ----- -// TEMPORARY: Force fallback path for testing on DOTPROD devices -// #undef __ARM_FEATURE_DOTPROD - -#if defined(__ARM_FEATURE_DOTPROD) - #define CACTUS_DOTQ_LANE(acc, b, a, lane) vdotq_laneq_s32(acc, b, a, lane) -#else - static inline int32x4_t cactus_dotq_with_pattern(int32x4_t acc, int8x16_t b, int8x8_t a_pattern) { - int8x8_t b_lo = vget_low_s8(b); - int8x8_t b_hi = vget_high_s8(b); - - int16x8_t prod_lo = vmull_s8(b_lo, a_pattern); - int16x8_t prod_hi = vmull_s8(b_hi, a_pattern); - - int32x4_t sum_lo = vpaddlq_s16(prod_lo); - int32x4_t sum_hi = vpaddlq_s16(prod_hi); - - int32x2_t final_lo = vpadd_s32(vget_low_s32(sum_lo), vget_high_s32(sum_lo)); - int32x2_t final_hi = vpadd_s32(vget_low_s32(sum_hi), vget_high_s32(sum_hi)); - - return vaddq_s32(acc, vcombine_s32(final_lo, final_hi)); - } - - static inline int32x4_t cactus_dotq_lane0(int32x4_t acc, int8x16_t b, int8x16_t a) { - int8x8_t a_lo = vget_low_s8(a); - int8x8_t a_pattern = vreinterpret_s8_s32(vdup_lane_s32(vreinterpret_s32_s8(a_lo), 0)); - return cactus_dotq_with_pattern(acc, b, a_pattern); - } - - static inline int32x4_t cactus_dotq_lane1(int32x4_t acc, int8x16_t b, int8x16_t a) { - int8x8_t a_lo = vget_low_s8(a); - int8x8_t a_pattern = vreinterpret_s8_s32(vdup_lane_s32(vreinterpret_s32_s8(a_lo), 1)); - return cactus_dotq_with_pattern(acc, b, a_pattern); - } - - static inline int32x4_t cactus_dotq_lane2(int32x4_t acc, int8x16_t b, int8x16_t a) { - int8x8_t a_hi = vget_high_s8(a); - int8x8_t a_pattern = vreinterpret_s8_s32(vdup_lane_s32(vreinterpret_s32_s8(a_hi), 0)); - return cactus_dotq_with_pattern(acc, b, a_pattern); - } - - static inline int32x4_t cactus_dotq_lane3(int32x4_t acc, int8x16_t b, int8x16_t a) { - int8x8_t a_hi = vget_high_s8(a); - int8x8_t a_pattern = vreinterpret_s8_s32(vdup_lane_s32(vreinterpret_s32_s8(a_hi), 1)); - return cactus_dotq_with_pattern(acc, b, a_pattern); - } - - #define CACTUS_DOTQ_LANE(acc, b, a, lane) cactus_dotq_lane##lane(acc, b, a) -#endif - -static inline __fp16 hsum_f16x8(float16x8_t v) { - float16x4_t lo = vget_low_f16(v); - float16x4_t hi = vget_high_f16(v); - float16x4_t sum4 = vadd_f16(lo, hi); - float16x4_t sum2 = vadd_f16(sum4, vext_f16(sum4, sum4, 2)); - float16x4_t sum1 = vadd_f16(sum2, vext_f16(sum2, sum2, 1)); - return vget_lane_f16(sum1, 0); -} - -static void cactus_matmul_f16_worker( - const __fp16* a, - const __fp16* b_transposed, - __fp16* c, - size_t /*M*/, - size_t K, - size_t N, - size_t start_row, - size_t end_row -) { - constexpr size_t TILE_M = 4; - constexpr size_t TILE_N = 4; - const size_t K16 = (K / 16) * 16; - const size_t K8 = (K / 8) * 8; - - for (size_t row_block = start_row; row_block < end_row; row_block += TILE_M) { - const size_t m_end = std::min(row_block + TILE_M, end_row); - - for (size_t col_block = 0; col_block < N; col_block += TILE_N) { - const size_t n_end = std::min(col_block + TILE_N, N); - - float16x8_t acc[TILE_M][TILE_N]; - for (size_t m = 0; m < TILE_M; ++m) - for (size_t n = 0; n < TILE_N; ++n) - acc[m][n] = vdupq_n_f16(0); - - for (size_t k = 0; k < K16; k += 16) { - float16x8_t a0_lo = (row_block < m_end) ? vld1q_f16(a + row_block * K + k) : vdupq_n_f16(0); - float16x8_t a0_hi = (row_block < m_end) ? vld1q_f16(a + row_block * K + k + 8) : vdupq_n_f16(0); - float16x8_t a1_lo = (row_block + 1 < m_end) ? vld1q_f16(a + (row_block + 1) * K + k) : vdupq_n_f16(0); - float16x8_t a1_hi = (row_block + 1 < m_end) ? vld1q_f16(a + (row_block + 1) * K + k + 8) : vdupq_n_f16(0); - float16x8_t a2_lo = (row_block + 2 < m_end) ? vld1q_f16(a + (row_block + 2) * K + k) : vdupq_n_f16(0); - float16x8_t a2_hi = (row_block + 2 < m_end) ? vld1q_f16(a + (row_block + 2) * K + k + 8) : vdupq_n_f16(0); - float16x8_t a3_lo = (row_block + 3 < m_end) ? vld1q_f16(a + (row_block + 3) * K + k) : vdupq_n_f16(0); - float16x8_t a3_hi = (row_block + 3 < m_end) ? vld1q_f16(a + (row_block + 3) * K + k + 8) : vdupq_n_f16(0); - - for (size_t ni = 0; ni < TILE_N && col_block + ni < n_end; ++ni) { - float16x8_t b_lo = vld1q_f16(b_transposed + (col_block + ni) * K + k); - float16x8_t b_hi = vld1q_f16(b_transposed + (col_block + ni) * K + k + 8); - - acc[0][ni] = vfmaq_f16(acc[0][ni], a0_lo, b_lo); - acc[0][ni] = vfmaq_f16(acc[0][ni], a0_hi, b_hi); - acc[1][ni] = vfmaq_f16(acc[1][ni], a1_lo, b_lo); - acc[1][ni] = vfmaq_f16(acc[1][ni], a1_hi, b_hi); - acc[2][ni] = vfmaq_f16(acc[2][ni], a2_lo, b_lo); - acc[2][ni] = vfmaq_f16(acc[2][ni], a2_hi, b_hi); - acc[3][ni] = vfmaq_f16(acc[3][ni], a3_lo, b_lo); - acc[3][ni] = vfmaq_f16(acc[3][ni], a3_hi, b_hi); - } - } - - for (size_t k = K16; k < K8; k += 8) { - float16x8_t a0_v = (row_block < m_end) ? vld1q_f16(a + row_block * K + k) : vdupq_n_f16(0); - float16x8_t a1_v = (row_block + 1 < m_end) ? vld1q_f16(a + (row_block + 1) * K + k) : vdupq_n_f16(0); - float16x8_t a2_v = (row_block + 2 < m_end) ? vld1q_f16(a + (row_block + 2) * K + k) : vdupq_n_f16(0); - float16x8_t a3_v = (row_block + 3 < m_end) ? vld1q_f16(a + (row_block + 3) * K + k) : vdupq_n_f16(0); - - for (size_t ni = 0; ni < TILE_N && col_block + ni < n_end; ++ni) { - float16x8_t b_v = vld1q_f16(b_transposed + (col_block + ni) * K + k); - acc[0][ni] = vfmaq_f16(acc[0][ni], a0_v, b_v); - acc[1][ni] = vfmaq_f16(acc[1][ni], a1_v, b_v); - acc[2][ni] = vfmaq_f16(acc[2][ni], a2_v, b_v); - acc[3][ni] = vfmaq_f16(acc[3][ni], a3_v, b_v); - } - } - - for (size_t k = K8; k < K; ++k) { - for (size_t mi = 0; mi < TILE_M && row_block + mi < m_end; ++mi) { - __fp16 av = a[(row_block + mi) * K + k]; - for (size_t ni = 0; ni < TILE_N && col_block + ni < n_end; ++ni) { - __fp16 bv = b_transposed[(col_block + ni) * K + k]; - acc[mi][ni] = vsetq_lane_f16(vgetq_lane_f16(acc[mi][ni], 0) + av * bv, acc[mi][ni], 0); - } - } - } - - for (size_t mi = 0; mi < TILE_M && row_block + mi < m_end; ++mi) { - for (size_t ni = 0; ni < TILE_N && col_block + ni < n_end; ++ni) { - c[(row_block + mi) * N + col_block + ni] = hsum_f16x8(acc[mi][ni]); - } - } - } - } -} - -void cactus_matmul_f16( - const __fp16* a, - const __fp16* b_transposed, - __fp16* c, - size_t M, - size_t K, - size_t N -) { - -#ifdef __APPLE__ - if (K >= ACCELERATE_K_THRESHOLD && M >= ACCELERATE_M_THRESHOLD) { - const size_t a_len = M * K; - const size_t b_len = N * K; - const size_t c_len = M * N; - - std::vector A_f32(a_len); - std::vector BT_f32(b_len); - std::vector C_f32(c_len); - - for (size_t i = 0; i < a_len; i++) A_f32[i] = (float)a[i]; - for (size_t i = 0; i < b_len; i++) BT_f32[i] = (float)b_transposed[i]; - - cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasTrans, - (int)M, (int)N, (int)K, - 1.0f, A_f32.data(), (int)K, - BT_f32.data(), (int)K, - 0.0f, C_f32.data(), (int)N); - - for (size_t i = 0; i < c_len; i++) { - float v = C_f32[i]; - if (v > 65504.f) v = 65504.f; - else if (v < -65504.f) v = -65504.f; - c[i] = (__fp16)v; - } - return; - } -#endif - - constexpr size_t TILE_M = 4; - const size_t num_row_blocks = (M + TILE_M - 1) / TILE_M; - - CactusThreading::parallel_for(num_row_blocks, CactusThreading::Thresholds::SCALAR_EXPENSIVE, - [=](size_t start_block, size_t end_block) { - for (size_t block_idx = start_block; block_idx < end_block; ++block_idx) { - size_t start_row = block_idx * TILE_M; - size_t end_row = std::min(start_row + TILE_M, M); - - cactus_matmul_f16_worker( - a, b_transposed, c, - M, K, N, - start_row, end_row - ); - - } - }); -} - - -void cactus_gemv_int8( - const int8_t* A, - const float A_scale, - const int8_t* B, - const __fp16* B_scales, - __fp16* C, - size_t K, size_t N, - size_t group_size -) { - if (K == 0 || N == 0) return; - - const size_t num_groups = K / group_size; - const size_t N_blocks = (N + 3) / 4; - - auto process_blocks = [=](size_t block_start, size_t block_end) { - for (size_t n_block = block_start; n_block < block_end; ++n_block) { - const size_t n_start = n_block * 4; - const size_t actual_n = std::min(size_t(4), N - n_start); - - float32x4_t running_sum = vdupq_n_f32(0.0f); - - size_t g = 0; - for (; g + 1 < num_groups; g += 2) { - const size_t k_base0 = g * group_size; - const size_t k_base1 = (g + 1) * group_size; - - const int8_t* a_ptr0 = A + k_base0; - const int8_t* a_ptr1 = A + k_base1; - const int8_t* b_base0 = B + (n_block * K + k_base0) * 4; - const int8_t* b_base1 = B + (n_block * K + k_base1) * 4; - - __builtin_prefetch(b_base0 + group_size * 8, 0, 3); - - int32x4_t acc0 = vdupq_n_s32(0); - int32x4_t acc1 = vdupq_n_s32(0); - - { - int8x16_t a_vec = vld1q_s8(a_ptr0); - int8x16_t b0 = vld1q_s8(b_base0); - int8x16_t b1 = vld1q_s8(b_base0 + 16); - int8x16_t b2 = vld1q_s8(b_base0 + 32); - int8x16_t b3 = vld1q_s8(b_base0 + 48); - - acc0 = CACTUS_DOTQ_LANE(acc0, b0, a_vec, 0); - acc0 = CACTUS_DOTQ_LANE(acc0, b1, a_vec, 1); - acc0 = CACTUS_DOTQ_LANE(acc0, b2, a_vec, 2); - acc0 = CACTUS_DOTQ_LANE(acc0, b3, a_vec, 3); - - a_vec = vld1q_s8(a_ptr0 + 16); - b0 = vld1q_s8(b_base0 + 64); - b1 = vld1q_s8(b_base0 + 80); - b2 = vld1q_s8(b_base0 + 96); - b3 = vld1q_s8(b_base0 + 112); - - acc0 = CACTUS_DOTQ_LANE(acc0, b0, a_vec, 0); - acc0 = CACTUS_DOTQ_LANE(acc0, b1, a_vec, 1); - acc0 = CACTUS_DOTQ_LANE(acc0, b2, a_vec, 2); - acc0 = CACTUS_DOTQ_LANE(acc0, b3, a_vec, 3); - } - - { - int8x16_t a_vec = vld1q_s8(a_ptr1); - int8x16_t b0 = vld1q_s8(b_base1); - int8x16_t b1 = vld1q_s8(b_base1 + 16); - int8x16_t b2 = vld1q_s8(b_base1 + 32); - int8x16_t b3 = vld1q_s8(b_base1 + 48); - - acc1 = CACTUS_DOTQ_LANE(acc1, b0, a_vec, 0); - acc1 = CACTUS_DOTQ_LANE(acc1, b1, a_vec, 1); - acc1 = CACTUS_DOTQ_LANE(acc1, b2, a_vec, 2); - acc1 = CACTUS_DOTQ_LANE(acc1, b3, a_vec, 3); - - a_vec = vld1q_s8(a_ptr1 + 16); - b0 = vld1q_s8(b_base1 + 64); - b1 = vld1q_s8(b_base1 + 80); - b2 = vld1q_s8(b_base1 + 96); - b3 = vld1q_s8(b_base1 + 112); - - acc1 = CACTUS_DOTQ_LANE(acc1, b0, a_vec, 0); - acc1 = CACTUS_DOTQ_LANE(acc1, b1, a_vec, 1); - acc1 = CACTUS_DOTQ_LANE(acc1, b2, a_vec, 2); - acc1 = CACTUS_DOTQ_LANE(acc1, b3, a_vec, 3); - } - - const __fp16* scale_ptr0 = B_scales + (n_block * num_groups + g) * 4; - const __fp16* scale_ptr1 = B_scales + (n_block * num_groups + g + 1) * 4; - - float16x4_t scales0_f16 = vld1_f16(scale_ptr0); - float16x4_t scales1_f16 = vld1_f16(scale_ptr1); - float32x4_t scales0 = vcvt_f32_f16(scales0_f16); - float32x4_t scales1 = vcvt_f32_f16(scales1_f16); - - running_sum = vmlaq_f32(running_sum, vcvtq_f32_s32(acc0), scales0); - running_sum = vmlaq_f32(running_sum, vcvtq_f32_s32(acc1), scales1); - } - - for (; g < num_groups; g++) { - const size_t k_base = g * group_size; - const int8_t* a_ptr = A + k_base; - const int8_t* b_base = B + (n_block * K + k_base) * 4; - - int32x4_t acc = vdupq_n_s32(0); - - int8x16_t a_vec = vld1q_s8(a_ptr); - int8x16_t b0 = vld1q_s8(b_base); - int8x16_t b1 = vld1q_s8(b_base + 16); - int8x16_t b2 = vld1q_s8(b_base + 32); - int8x16_t b3 = vld1q_s8(b_base + 48); - - acc = CACTUS_DOTQ_LANE(acc, b0, a_vec, 0); - acc = CACTUS_DOTQ_LANE(acc, b1, a_vec, 1); - acc = CACTUS_DOTQ_LANE(acc, b2, a_vec, 2); - acc = CACTUS_DOTQ_LANE(acc, b3, a_vec, 3); - - a_vec = vld1q_s8(a_ptr + 16); - b0 = vld1q_s8(b_base + 64); - b1 = vld1q_s8(b_base + 80); - b2 = vld1q_s8(b_base + 96); - b3 = vld1q_s8(b_base + 112); - - acc = CACTUS_DOTQ_LANE(acc, b0, a_vec, 0); - acc = CACTUS_DOTQ_LANE(acc, b1, a_vec, 1); - acc = CACTUS_DOTQ_LANE(acc, b2, a_vec, 2); - acc = CACTUS_DOTQ_LANE(acc, b3, a_vec, 3); - - const __fp16* scale_ptr = B_scales + (n_block * num_groups + g) * 4; - float16x4_t scales_f16 = vld1_f16(scale_ptr); - float32x4_t scales = vcvt_f32_f16(scales_f16); - - running_sum = vmlaq_f32(running_sum, vcvtq_f32_s32(acc), scales); - } - - float32x4_t result = vmulq_n_f32(running_sum, A_scale); - float16x4_t result_f16 = vcvt_f16_f32(result); - - if (actual_n == 4) { - vst1_f16(C + n_start, result_f16); - } else { - for (size_t ni = 0; ni < actual_n; ni++) { - C[n_start + ni] = vget_lane_f16(result_f16, 0); - result_f16 = vext_f16(result_f16, result_f16, 1); - } - } - } - }; - - auto& pool = CactusThreading::get_thread_pool(); - size_t num_threads = CactusThreading::GemmThreading::get_gemv_threads(N_blocks, pool.num_workers()); - num_threads = std::min(num_threads, N_blocks); - - if (num_threads <= 1) { - process_blocks(0, N_blocks); - } else { - pool.enqueue_n_threads(N_blocks, num_threads, process_blocks); - pool.wait_all(); - } -} - -void cactus_gemm_int8( - const int8_t* A, - const float* A_scales, - const int8_t* B, - const __fp16* B_scales, - __fp16* C, - size_t M, size_t K, size_t N, - size_t group_size -) { - if (M == 0 || K == 0 || N == 0) return; - - constexpr size_t TILE_M = 8; - constexpr size_t TILE_N = 4; - - const size_t num_groups = K / group_size; - const size_t N_blocks = (N + TILE_N - 1) / TILE_N; - const size_t num_row_tiles = (M + TILE_M - 1) / TILE_M; - const size_t total_tiles = num_row_tiles * N_blocks; - - CactusThreading::parallel_gemm_tiles(M, total_tiles, - [=](size_t tile_start, size_t tile_end) { - for (size_t tile_idx = tile_start; tile_idx < tile_end; ++tile_idx) { - const size_t tile_row = tile_idx / N_blocks; - const size_t n_block = tile_idx % N_blocks; - const size_t m_start = tile_row * TILE_M; - const size_t m_end = std::min(m_start + TILE_M, M); - const size_t n_start = n_block * TILE_N; - const size_t n_end = std::min(n_start + TILE_N, N); - const size_t actual_m = m_end - m_start; - const size_t actual_n = n_end - n_start; - - const int8_t* a_rows[TILE_M]; - for (size_t mi = 0; mi < TILE_M; mi++) { - size_t row = m_start + (mi < actual_m ? mi : actual_m - 1); - a_rows[mi] = A + row * K; - } - - float32x4_t running_sum[TILE_M]; - for (size_t mi = 0; mi < TILE_M; mi++) { - running_sum[mi] = vdupq_n_f32(0.0f); - } - - for (size_t g = 0; g < num_groups; g++) { - const size_t k_base = g * group_size; - const int8_t* b_base = B + (n_block * K + k_base) * 4; - - __builtin_prefetch(b_base + group_size * 4, 0, 3); - - int8x16_t b00 = vld1q_s8(b_base); - int8x16_t b01 = vld1q_s8(b_base + 16); - int8x16_t b02 = vld1q_s8(b_base + 32); - int8x16_t b03 = vld1q_s8(b_base + 48); - - int8x16_t b10 = vld1q_s8(b_base + 64); - int8x16_t b11 = vld1q_s8(b_base + 80); - int8x16_t b12 = vld1q_s8(b_base + 96); - int8x16_t b13 = vld1q_s8(b_base + 112); - - const __fp16* scale_ptr = B_scales + (n_block * num_groups + g) * 4; - float16x4_t scales_f16 = vld1_f16(scale_ptr); - float32x4_t scales = vcvt_f32_f16(scales_f16); - - #define CACTUS_GEMM_ROW(ROW) do { \ - const int8_t* a_ptr_##ROW = a_rows[ROW] + k_base; \ - int32x4_t acc_##ROW = vdupq_n_s32(0); \ - int8x16_t a_lo_##ROW = vld1q_s8(a_ptr_##ROW); \ - acc_##ROW = CACTUS_DOTQ_LANE(acc_##ROW, b00, a_lo_##ROW, 0); \ - acc_##ROW = CACTUS_DOTQ_LANE(acc_##ROW, b01, a_lo_##ROW, 1); \ - acc_##ROW = CACTUS_DOTQ_LANE(acc_##ROW, b02, a_lo_##ROW, 2); \ - acc_##ROW = CACTUS_DOTQ_LANE(acc_##ROW, b03, a_lo_##ROW, 3); \ - int8x16_t a_hi_##ROW = vld1q_s8(a_ptr_##ROW + 16); \ - acc_##ROW = CACTUS_DOTQ_LANE(acc_##ROW, b10, a_hi_##ROW, 0); \ - acc_##ROW = CACTUS_DOTQ_LANE(acc_##ROW, b11, a_hi_##ROW, 1); \ - acc_##ROW = CACTUS_DOTQ_LANE(acc_##ROW, b12, a_hi_##ROW, 2); \ - acc_##ROW = CACTUS_DOTQ_LANE(acc_##ROW, b13, a_hi_##ROW, 3); \ - running_sum[ROW] = vmlaq_f32(running_sum[ROW], vcvtq_f32_s32(acc_##ROW), scales); \ - } while(0) - - CACTUS_GEMM_ROW(0); - CACTUS_GEMM_ROW(1); - CACTUS_GEMM_ROW(2); - CACTUS_GEMM_ROW(3); - CACTUS_GEMM_ROW(4); - CACTUS_GEMM_ROW(5); - CACTUS_GEMM_ROW(6); - CACTUS_GEMM_ROW(7); - #undef CACTUS_GEMM_ROW - } - - for (size_t mi = 0; mi < actual_m; mi++) { - const float a_scale = A_scales[m_start + mi]; - float32x4_t result = vmulq_n_f32(running_sum[mi], a_scale); - float16x4_t result_f16 = vcvt_f16_f32(result); - - if (actual_n == 4) { - vst1_f16(C + (m_start + mi) * N + n_start, result_f16); - } else { - for (size_t ni = 0; ni < actual_n; ni++) { - C[(m_start + mi) * N + n_start + ni] = vget_lane_f16(result_f16, 0); - result_f16 = vext_f16(result_f16, result_f16, 1); - } - } - } - } - }); -} - -void cactus_matmul_int8( - const int8_t* A, - const float* A_scales, - const int8_t* B, - const __fp16* B_scales, - __fp16* C, - size_t M, size_t K, size_t N, - size_t group_size -) { - if (M == 0 || K == 0 || N == 0) return; - -#if defined(CACTUS_COMPILE_I8MM) - if (cpu_has_i8mm()) { - if (M == 1) { - cactus_gemv_int8_i8mm(A, A_scales[0], B, B_scales, C, K, N, group_size); - } else { - cactus_gemm_int8_i8mm(A, A_scales, B, B_scales, C, M, K, N, group_size); - } - return; - } -#endif - - if (M == 1) { - cactus_gemv_int8(A, A_scales[0], B, B_scales, C, K, N, group_size); - } else { - cactus_gemm_int8(A, A_scales, B, B_scales, C, M, K, N, group_size); - } -} - -void cactus_gemv_int4( - const int8_t* A, - const float A_scale, - const int8_t* B_packed_raw, - const __fp16* B_scales, - __fp16* C, - size_t K, size_t N, - size_t group_size -) { - const uint8_t* B_packed = reinterpret_cast(B_packed_raw); - if (K == 0 || N == 0) return; - - const size_t num_groups = K / group_size; - const size_t N_blocks = (N + 3) / 4; - - auto process_blocks = [=](size_t block_start, size_t block_end) { - size_t n_block = block_start; - - for (; n_block + 1 < block_end; n_block += 2) { - float32x4_t sum_a = vdupq_n_f32(0.0f); - float32x4_t sum_b = vdupq_n_f32(0.0f); - - for (size_t g = 0; g < num_groups; g++) { - const size_t k_base = g * group_size; - const int8_t* a_ptr = A + k_base; - const uint8_t* ba = B_packed + (n_block * K + k_base) * 2; - const uint8_t* bb = B_packed + ((n_block + 1) * K + k_base) * 2; - - int32x4_t acc_a = vdupq_n_s32(0); - int32x4_t acc_b = vdupq_n_s32(0); - - int8x16_t a_lo = vld1q_s8(a_ptr); - int8x16_t a_hi = vld1q_s8(a_ptr + 16); - - { - int8x16_t b0, b1, b2, b3; - unpack_int4_as_int8x16x2(ba, b1, b0); - unpack_int4_as_int8x16x2(ba + 16, b3, b2); - acc_a = CACTUS_DOTQ_LANE(acc_a, b0, a_lo, 0); - acc_a = CACTUS_DOTQ_LANE(acc_a, b1, a_lo, 1); - acc_a = CACTUS_DOTQ_LANE(acc_a, b2, a_lo, 2); - acc_a = CACTUS_DOTQ_LANE(acc_a, b3, a_lo, 3); - unpack_int4_as_int8x16x2(ba + 32, b1, b0); - unpack_int4_as_int8x16x2(ba + 48, b3, b2); - acc_a = CACTUS_DOTQ_LANE(acc_a, b0, a_hi, 0); - acc_a = CACTUS_DOTQ_LANE(acc_a, b1, a_hi, 1); - acc_a = CACTUS_DOTQ_LANE(acc_a, b2, a_hi, 2); - acc_a = CACTUS_DOTQ_LANE(acc_a, b3, a_hi, 3); - } - { - int8x16_t b0, b1, b2, b3; - unpack_int4_as_int8x16x2(bb, b1, b0); - unpack_int4_as_int8x16x2(bb + 16, b3, b2); - acc_b = CACTUS_DOTQ_LANE(acc_b, b0, a_lo, 0); - acc_b = CACTUS_DOTQ_LANE(acc_b, b1, a_lo, 1); - acc_b = CACTUS_DOTQ_LANE(acc_b, b2, a_lo, 2); - acc_b = CACTUS_DOTQ_LANE(acc_b, b3, a_lo, 3); - unpack_int4_as_int8x16x2(bb + 32, b1, b0); - unpack_int4_as_int8x16x2(bb + 48, b3, b2); - acc_b = CACTUS_DOTQ_LANE(acc_b, b0, a_hi, 0); - acc_b = CACTUS_DOTQ_LANE(acc_b, b1, a_hi, 1); - acc_b = CACTUS_DOTQ_LANE(acc_b, b2, a_hi, 2); - acc_b = CACTUS_DOTQ_LANE(acc_b, b3, a_hi, 3); - } - - const __fp16* spa = B_scales + (n_block * num_groups + g) * 4; - const __fp16* spb = B_scales + ((n_block + 1) * num_groups + g) * 4; - float32x4_t sa = vcvt_f32_f16(vld1_f16(spa)); - float32x4_t sb = vcvt_f32_f16(vld1_f16(spb)); - sum_a = vmlaq_f32(sum_a, vcvtq_f32_s32(acc_a), sa); - sum_b = vmlaq_f32(sum_b, vcvtq_f32_s32(acc_b), sb); - } - - vst1_f16(C + n_block * 4, vcvt_f16_f32(vmulq_n_f32(sum_a, A_scale))); - vst1_f16(C + (n_block + 1) * 4, vcvt_f16_f32(vmulq_n_f32(sum_b, A_scale))); - } - - for (; n_block < block_end; ++n_block) { - const size_t n_start = n_block * 4; - const size_t actual_n = std::min(size_t(4), N - n_start); - float32x4_t running_sum = vdupq_n_f32(0.0f); - - for (size_t g = 0; g < num_groups; g++) { - const size_t k_base = g * group_size; - const int8_t* a_ptr = A + k_base; - const uint8_t* b_base = B_packed + (n_block * K + k_base) * 2; - - int32x4_t acc = vdupq_n_s32(0); - int8x16_t a_lo = vld1q_s8(a_ptr); - int8x16_t a_hi = vld1q_s8(a_ptr + 16); - - int8x16_t b0, b1, b2, b3; - unpack_int4_as_int8x16x2(b_base, b1, b0); - unpack_int4_as_int8x16x2(b_base + 16, b3, b2); - acc = CACTUS_DOTQ_LANE(acc, b0, a_lo, 0); - acc = CACTUS_DOTQ_LANE(acc, b1, a_lo, 1); - acc = CACTUS_DOTQ_LANE(acc, b2, a_lo, 2); - acc = CACTUS_DOTQ_LANE(acc, b3, a_lo, 3); - unpack_int4_as_int8x16x2(b_base + 32, b1, b0); - unpack_int4_as_int8x16x2(b_base + 48, b3, b2); - acc = CACTUS_DOTQ_LANE(acc, b0, a_hi, 0); - acc = CACTUS_DOTQ_LANE(acc, b1, a_hi, 1); - acc = CACTUS_DOTQ_LANE(acc, b2, a_hi, 2); - acc = CACTUS_DOTQ_LANE(acc, b3, a_hi, 3); - - float32x4_t scales = vcvt_f32_f16(vld1_f16(B_scales + (n_block * num_groups + g) * 4)); - running_sum = vmlaq_f32(running_sum, vcvtq_f32_s32(acc), scales); - } - - float32x4_t result = vmulq_n_f32(running_sum, A_scale); - float16x4_t result_f16 = vcvt_f16_f32(result); - if (actual_n == 4) { - vst1_f16(C + n_start, result_f16); - } else { - for (size_t ni = 0; ni < actual_n; ni++) { - C[n_start + ni] = vget_lane_f16(result_f16, 0); - result_f16 = vext_f16(result_f16, result_f16, 1); - } - } - } - }; - - auto& pool = CactusThreading::get_thread_pool(); - size_t num_threads = CactusThreading::GemmThreading::get_gemv_threads(N_blocks, pool.num_workers()); - num_threads = std::min(num_threads, N_blocks); - - if (num_threads <= 1) { - process_blocks(0, N_blocks); - } else { - pool.enqueue_n_threads(N_blocks, num_threads, process_blocks); - pool.wait_all(); - } -} - -void cactus_gemv_int4_block_range( - const int8_t* A, float A_scale, - const int8_t* B_packed_raw, const __fp16* B_scales, - __fp16* C, - size_t K, size_t N, size_t group_size, - size_t block_start, size_t block_end -) { - const uint8_t* B_packed = reinterpret_cast(B_packed_raw); - if (K == 0 || N == 0 || block_end <= block_start) return; - const size_t num_groups = K / group_size; - - size_t n_block = block_start; - for (; n_block + 1 < block_end; n_block += 2) { - const size_t n_start_a = n_block * 4; - const size_t n_start_b = (n_block + 1) * 4; - float32x4_t sum_a = vdupq_n_f32(0.0f); - float32x4_t sum_b = vdupq_n_f32(0.0f); - - for (size_t g = 0; g < num_groups; ++g) { - const size_t k_base = g * group_size; - const int8_t* a_ptr = A + k_base; - const uint8_t* ba = B_packed + (n_block * K + k_base) * 2; - const uint8_t* bb = B_packed + ((n_block + 1) * K + k_base) * 2; - - int32x4_t acc_a = vdupq_n_s32(0); - int32x4_t acc_b = vdupq_n_s32(0); - int8x16_t a_lo = vld1q_s8(a_ptr); - int8x16_t a_hi = vld1q_s8(a_ptr + 16); - { - int8x16_t b0, b1, b2, b3; - unpack_int4_as_int8x16x2(ba, b1, b0); - unpack_int4_as_int8x16x2(ba + 16, b3, b2); - acc_a = CACTUS_DOTQ_LANE(acc_a, b0, a_lo, 0); - acc_a = CACTUS_DOTQ_LANE(acc_a, b1, a_lo, 1); - acc_a = CACTUS_DOTQ_LANE(acc_a, b2, a_lo, 2); - acc_a = CACTUS_DOTQ_LANE(acc_a, b3, a_lo, 3); - unpack_int4_as_int8x16x2(ba + 32, b1, b0); - unpack_int4_as_int8x16x2(ba + 48, b3, b2); - acc_a = CACTUS_DOTQ_LANE(acc_a, b0, a_hi, 0); - acc_a = CACTUS_DOTQ_LANE(acc_a, b1, a_hi, 1); - acc_a = CACTUS_DOTQ_LANE(acc_a, b2, a_hi, 2); - acc_a = CACTUS_DOTQ_LANE(acc_a, b3, a_hi, 3); - } - { - int8x16_t b0, b1, b2, b3; - unpack_int4_as_int8x16x2(bb, b1, b0); - unpack_int4_as_int8x16x2(bb + 16, b3, b2); - acc_b = CACTUS_DOTQ_LANE(acc_b, b0, a_lo, 0); - acc_b = CACTUS_DOTQ_LANE(acc_b, b1, a_lo, 1); - acc_b = CACTUS_DOTQ_LANE(acc_b, b2, a_lo, 2); - acc_b = CACTUS_DOTQ_LANE(acc_b, b3, a_lo, 3); - unpack_int4_as_int8x16x2(bb + 32, b1, b0); - unpack_int4_as_int8x16x2(bb + 48, b3, b2); - acc_b = CACTUS_DOTQ_LANE(acc_b, b0, a_hi, 0); - acc_b = CACTUS_DOTQ_LANE(acc_b, b1, a_hi, 1); - acc_b = CACTUS_DOTQ_LANE(acc_b, b2, a_hi, 2); - acc_b = CACTUS_DOTQ_LANE(acc_b, b3, a_hi, 3); - } - - const __fp16* spa = B_scales + (n_block * num_groups + g) * 4; - const __fp16* spb = B_scales + ((n_block + 1) * num_groups + g) * 4; - float32x4_t sa = vcvt_f32_f16(vld1_f16(spa)); - float32x4_t sb = vcvt_f32_f16(vld1_f16(spb)); - sum_a = vmlaq_f32(sum_a, vcvtq_f32_s32(acc_a), sa); - sum_b = vmlaq_f32(sum_b, vcvtq_f32_s32(acc_b), sb); - } - vst1_f16(C + n_start_a, vcvt_f16_f32(vmulq_n_f32(sum_a, A_scale))); - vst1_f16(C + n_start_b, vcvt_f16_f32(vmulq_n_f32(sum_b, A_scale))); - } - - for (; n_block < block_end; ++n_block) { - const size_t n_start = n_block * 4; - float32x4_t running_sum = vdupq_n_f32(0.0f); - for (size_t g = 0; g < num_groups; ++g) { - const size_t k_base = g * group_size; - const int8_t* a_ptr = A + k_base; - const uint8_t* b_base = B_packed + (n_block * K + k_base) * 2; - int32x4_t acc = vdupq_n_s32(0); - int8x16_t a_lo = vld1q_s8(a_ptr); - int8x16_t a_hi = vld1q_s8(a_ptr + 16); - int8x16_t b0, b1, b2, b3; - unpack_int4_as_int8x16x2(b_base, b1, b0); - unpack_int4_as_int8x16x2(b_base + 16, b3, b2); - acc = CACTUS_DOTQ_LANE(acc, b0, a_lo, 0); - acc = CACTUS_DOTQ_LANE(acc, b1, a_lo, 1); - acc = CACTUS_DOTQ_LANE(acc, b2, a_lo, 2); - acc = CACTUS_DOTQ_LANE(acc, b3, a_lo, 3); - unpack_int4_as_int8x16x2(b_base + 32, b1, b0); - unpack_int4_as_int8x16x2(b_base + 48, b3, b2); - acc = CACTUS_DOTQ_LANE(acc, b0, a_hi, 0); - acc = CACTUS_DOTQ_LANE(acc, b1, a_hi, 1); - acc = CACTUS_DOTQ_LANE(acc, b2, a_hi, 2); - acc = CACTUS_DOTQ_LANE(acc, b3, a_hi, 3); - float32x4_t scales = vcvt_f32_f16(vld1_f16(B_scales + (n_block * num_groups + g) * 4)); - running_sum = vmlaq_f32(running_sum, vcvtq_f32_s32(acc), scales); - } - vst1_f16(C + n_start, vcvt_f16_f32(vmulq_n_f32(running_sum, A_scale))); - } -} - -void cactus_gemm_int4( - const int8_t* A, - const float* A_scales, - const int8_t* B_packed_raw, - const __fp16* B_scales, - __fp16* C, - size_t M, size_t K, size_t N, - size_t group_size -) { - if (M == 0 || K == 0 || N == 0) return; - - const uint8_t* B_packed = reinterpret_cast(B_packed_raw); - - constexpr size_t TILE_M = 4; - constexpr size_t TILE_N = 4; - - const size_t num_groups = K / group_size; - const size_t N_blocks = (N + TILE_N - 1) / TILE_N; - const size_t num_row_tiles = (M + TILE_M - 1) / TILE_M; - const size_t total_tiles = num_row_tiles * N_blocks; - - CactusThreading::parallel_gemm_tiles(M, total_tiles, - [=](size_t tile_start, size_t tile_end) { - for (size_t tile_idx = tile_start; tile_idx < tile_end; ++tile_idx) { - const size_t tile_row = tile_idx / N_blocks; - const size_t n_block = tile_idx % N_blocks; - const size_t m_start = tile_row * TILE_M; - const size_t m_end = std::min(m_start + TILE_M, M); - const size_t n_start = n_block * TILE_N; - const size_t n_end = std::min(n_start + TILE_N, N); - const size_t actual_m = m_end - m_start; - const size_t actual_n = n_end - n_start; - - float32x4_t running_sum[TILE_M] = { - vdupq_n_f32(0.0f), vdupq_n_f32(0.0f), - vdupq_n_f32(0.0f), vdupq_n_f32(0.0f) - }; - - for (size_t g = 0; g < num_groups; g++) { - const size_t k_base = g * group_size; - const uint8_t* b_base = B_packed + (n_block * K + k_base) * 2; - - __builtin_prefetch(b_base + group_size * 2, 0, 3); - - int8x16_t b00, b01, b02, b03; - int8x16_t b10, b11, b12, b13; - - unpack_int4_as_int8x16x2(b_base, b01, b00); - unpack_int4_as_int8x16x2(b_base + 16, b03, b02); - unpack_int4_as_int8x16x2(b_base + 32, b11, b10); - unpack_int4_as_int8x16x2(b_base + 48, b13, b12); - - const __fp16* scale_ptr = B_scales + (n_block * num_groups + g) * 4; - float16x4_t scales_f16 = vld1_f16(scale_ptr); - float32x4_t scales = vcvt_f32_f16(scales_f16); - - for (size_t mi = 0; mi < actual_m; mi++) { - const int8_t* a_ptr = A + (m_start + mi) * K + k_base; - - int32x4_t acc = vdupq_n_s32(0); - - int8x16_t a_vec = vld1q_s8(a_ptr); - acc = CACTUS_DOTQ_LANE(acc, b00, a_vec, 0); - acc = CACTUS_DOTQ_LANE(acc, b01, a_vec, 1); - acc = CACTUS_DOTQ_LANE(acc, b02, a_vec, 2); - acc = CACTUS_DOTQ_LANE(acc, b03, a_vec, 3); - - a_vec = vld1q_s8(a_ptr + 16); - acc = CACTUS_DOTQ_LANE(acc, b10, a_vec, 0); - acc = CACTUS_DOTQ_LANE(acc, b11, a_vec, 1); - acc = CACTUS_DOTQ_LANE(acc, b12, a_vec, 2); - acc = CACTUS_DOTQ_LANE(acc, b13, a_vec, 3); - - running_sum[mi] = vmlaq_f32(running_sum[mi], vcvtq_f32_s32(acc), scales); - } - } - - for (size_t mi = 0; mi < actual_m; mi++) { - const float a_scale = A_scales[m_start + mi]; - float32x4_t result = vmulq_n_f32(running_sum[mi], a_scale); - float16x4_t result_f16 = vcvt_f16_f32(result); - - if (actual_n == 4) { - vst1_f16(C + (m_start + mi) * N + n_start, result_f16); - } else { - for (size_t ni = 0; ni < actual_n; ni++) { - C[(m_start + mi) * N + n_start + ni] = vget_lane_f16(result_f16, 0); - result_f16 = vext_f16(result_f16, result_f16, 1); - } - } - } - } - }); -} - -void cactus_matmul_int4(const int8_t* A, const float* A_scales, - const int8_t* B_packed, const __fp16* B_scales, - __fp16* C, size_t M, size_t K, size_t N, size_t group_size) { - if (M == 0 || K == 0 || N == 0) return; - - if (M == 1) { - cactus_gemv_int4(A, A_scales[0], B_packed, B_scales, C, K, N, group_size); - } else { - cactus_gemm_int4(A, A_scales, B_packed, B_scales, C, M, K, N, group_size); - } -} - -void cactus_matmul_integer(Precision precision, - const int8_t* A, const float* A_scales, - const int8_t* B, const __fp16* B_scales, - __fp16* C, size_t M, size_t K, size_t N, size_t group_size) { - if (precision == Precision::INT4) { - cactus_matmul_int4(A, A_scales, B, B_scales, C, M, K, N, group_size); - } else { - cactus_matmul_int8(A, A_scales, B, B_scales, C, M, K, N, group_size); - } -} diff --git a/cactus/kernel/kernel_nn.cpp b/cactus/kernel/kernel_nn.cpp deleted file mode 100644 index 0bb29863d..000000000 --- a/cactus/kernel/kernel_nn.cpp +++ /dev/null @@ -1,924 +0,0 @@ -#include "kernel.h" -#include "kernel_utils.h" -#include -#include -#include -#include -#include -#include -#include - -void cactus_relu_f16(const __fp16* input, __fp16* output, size_t num_elements) { - for (size_t i = 0; i < num_elements; ++i) { - __fp16 x = input[i]; - output[i] = x > static_cast<__fp16>(0) ? x : static_cast<__fp16>(0); - } -} - -void cactus_leaky_relu_f16(const __fp16* input, __fp16* output, size_t num_elements, float negative_slope) { - for (size_t i = 0; i < num_elements; ++i) { - __fp16 x = input[i]; - output[i] = x > static_cast<__fp16>(0) ? x : static_cast<__fp16>(static_cast(x) * negative_slope); - } -} - -void cactus_silu_f16(const __fp16* input, __fp16* output, size_t num_elements) { - CactusThreading::parallel_for(num_elements, CactusThreading::Thresholds::SCALAR_EXPENSIVE, - [&](size_t start_idx, size_t end_idx) { - constexpr size_t SIMD_WIDTH = 8; - const size_t vectorized_end = start_idx + ((end_idx - start_idx) / SIMD_WIDTH) * SIMD_WIDTH; - const float32x4_t one_f32 = vdupq_n_f32(1.0f); - - for (size_t i = start_idx; i < vectorized_end; i += SIMD_WIDTH) { - float16x8_t x = vld1q_f16(&input[i]); - - float32x4_t x_low = vcvt_f32_f16(vget_low_f16(x)); - float32x4_t x_high = vcvt_f32_f16(vget_high_f16(x)); - - float32x4_t exp_low = fast_exp_f32x4(vnegq_f32(x_low)); - float32x4_t exp_high = fast_exp_f32x4(vnegq_f32(x_high)); - - float32x4_t sigmoid_low = vdivq_f32(one_f32, vaddq_f32(one_f32, exp_low)); - float32x4_t sigmoid_high = vdivq_f32(one_f32, vaddq_f32(one_f32, exp_high)); - - float32x4_t silu_low = vmulq_f32(x_low, sigmoid_low); - float32x4_t silu_high = vmulq_f32(x_high, sigmoid_high); - - float16x8_t silu = vcombine_f16(vcvt_f16_f32(silu_low), vcvt_f16_f32(silu_high)); - vst1q_f16(&output[i], silu); - } - - for (size_t i = vectorized_end; i < end_idx; ++i) { - float x_f32 = static_cast(input[i]); - float sigmoid = 1.0f / (1.0f + expf(-x_f32)); - output[i] = static_cast<__fp16>(x_f32 * sigmoid); - } - }); -} - -void cactus_gelu_f16(const __fp16* input, __fp16* output, size_t num_elements) { - const float sqrt_2_over_pi = 0.7978845608028654f; - const float coeff = 0.044715f; - - CactusThreading::parallel_for(num_elements, CactusThreading::Thresholds::SCALAR_EXPENSIVE, - [&](size_t start_idx, size_t end_idx) { - constexpr size_t SIMD_WIDTH = 8; - const size_t vectorized_end = start_idx + ((end_idx - start_idx) / SIMD_WIDTH) * SIMD_WIDTH; - - const float32x4_t half = vdupq_n_f32(0.5f); - const float32x4_t one = vdupq_n_f32(1.0f); - const float32x4_t sqrt_2_pi_vec = vdupq_n_f32(sqrt_2_over_pi); - const float32x4_t coeff_vec = vdupq_n_f32(coeff); - - for (size_t i = start_idx; i < vectorized_end; i += SIMD_WIDTH) { - float16x8_t x_f16 = vld1q_f16(&input[i]); - - float32x4_t x_low = vcvt_f32_f16(vget_low_f16(x_f16)); - float32x4_t x_high = vcvt_f32_f16(vget_high_f16(x_f16)); - - float32x4_t x_cubed_low = vmulq_f32(vmulq_f32(x_low, x_low), x_low); - float32x4_t x_cubed_high = vmulq_f32(vmulq_f32(x_high, x_high), x_high); - - float32x4_t inner_low = vfmaq_f32(x_low, coeff_vec, x_cubed_low); - float32x4_t inner_high = vfmaq_f32(x_high, coeff_vec, x_cubed_high); - inner_low = vmulq_f32(sqrt_2_pi_vec, inner_low); - inner_high = vmulq_f32(sqrt_2_pi_vec, inner_high); - - float32x4_t tanh_low = fast_tanh_f32x4(inner_low); - float32x4_t tanh_high = fast_tanh_f32x4(inner_high); - - float32x4_t gelu_low = vmulq_f32(vmulq_f32(half, x_low), vaddq_f32(one, tanh_low)); - float32x4_t gelu_high = vmulq_f32(vmulq_f32(half, x_high), vaddq_f32(one, tanh_high)); - - float16x8_t gelu_f16 = vcombine_f16(vcvt_f16_f32(gelu_low), vcvt_f16_f32(gelu_high)); - vst1q_f16(&output[i], gelu_f16); - } - - for (size_t i = vectorized_end; i < end_idx; ++i) { - float x = static_cast(input[i]); - float inner = sqrt_2_over_pi * (x + coeff * x * x * x); - float gelu = 0.5f * x * (1.0f + tanhf(inner)); - output[i] = static_cast<__fp16>(gelu); - } - }); -} - -void cactus_gelu_f16_erf(const __fp16* input, __fp16* output, size_t num_elements) -{ - const float inv_sqrt2 = 0.70710678118654752440f; - - CactusThreading::parallel_for( - num_elements, - CactusThreading::Thresholds::SCALAR_EXPENSIVE, - [&](size_t start_idx, size_t end_idx) { - - constexpr size_t SIMD = 8; - size_t vec_end = start_idx + ((end_idx - start_idx) / SIMD) * SIMD; - - for (size_t i = start_idx; i < vec_end; i += SIMD) { - - float16x8_t xh = vld1q_f16(&input[i]); - - float32x4_t x0 = vcvt_f32_f16(vget_low_f16(xh)); - float32x4_t x1 = vcvt_f32_f16(vget_high_f16(xh)); - - float32x4_t arg0 = vmulq_n_f32(x0, inv_sqrt2); - float32x4_t arg1 = vmulq_n_f32(x1, inv_sqrt2); - - float arg0_s[4], arg1_s[4]; - float erf0_s[4], erf1_s[4]; - vst1q_f32(arg0_s, arg0); - vst1q_f32(arg1_s, arg1); - - for (int j = 0; j < 4; j++) { - erf0_s[j] = erff(arg0_s[j]); - erf1_s[j] = erff(arg1_s[j]); - } - - float32x4_t erf0 = vld1q_f32(erf0_s); - float32x4_t erf1 = vld1q_f32(erf1_s); - - float32x4_t t0 = vaddq_f32(vdupq_n_f32(1.0f), erf0); - float32x4_t t1 = vaddq_f32(vdupq_n_f32(1.0f), erf1); - - float32x4_t gelu0 = vmulq_f32(vmulq_n_f32(x0, 0.5f), t0); - float32x4_t gelu1 = vmulq_f32(vmulq_n_f32(x1, 0.5f), t1); - - float16x8_t gelu_h = vcombine_f16( - vcvt_f16_f32(gelu0), - vcvt_f16_f32(gelu1) - ); - - vst1q_f16(&output[i], gelu_h); - } - - for (size_t i = vec_end; i < end_idx; i++) { - float x = (float)input[i]; - float arg = x * inv_sqrt2; - float res = 0.5f * x * (1.0f + erff(arg)); - output[i] = (__fp16)res; - } - } - ); -} - -void cactus_sigmoid_f16(const __fp16* input, __fp16* output, size_t num_elements) { - CactusThreading::parallel_for(num_elements, CactusThreading::Thresholds::SCALAR_EXPENSIVE, - [&](size_t start_idx, size_t end_idx) { - constexpr size_t SIMD_WIDTH = 8; - const size_t vectorized_end = start_idx + ((end_idx - start_idx) / SIMD_WIDTH) * SIMD_WIDTH; - const float32x4_t one_f32 = vdupq_n_f32(1.0f); - - for (size_t i = start_idx; i < vectorized_end; i += SIMD_WIDTH) { - float16x8_t x = vld1q_f16(&input[i]); - - float32x4_t x_low = vcvt_f32_f16(vget_low_f16(x)); - float32x4_t x_high = vcvt_f32_f16(vget_high_f16(x)); - - float32x4_t exp_low = fast_exp_f32x4(vnegq_f32(x_low)); - float32x4_t exp_high = fast_exp_f32x4(vnegq_f32(x_high)); - - float32x4_t sigmoid_low = vdivq_f32(one_f32, vaddq_f32(one_f32, exp_low)); - float32x4_t sigmoid_high = vdivq_f32(one_f32, vaddq_f32(one_f32, exp_high)); - - float16x8_t sigmoid = vcombine_f16(vcvt_f16_f32(sigmoid_low), vcvt_f16_f32(sigmoid_high)); - vst1q_f16(&output[i], sigmoid); - } - - for (size_t i = vectorized_end; i < end_idx; ++i) { - float x_f32 = static_cast(input[i]); - float sigmoid = 1.0f / (1.0f + expf(-x_f32)); - output[i] = static_cast<__fp16>(sigmoid); - } - }); -} - -void cactus_tanh_f16(const __fp16* input, __fp16* output, size_t num_elements) { - CactusThreading::parallel_for(num_elements, CactusThreading::Thresholds::SCALAR_EXPENSIVE, - [&](size_t start_idx, size_t end_idx) { - constexpr size_t SIMD_WIDTH = 8; - const size_t vectorized_end = start_idx + ((end_idx - start_idx) / SIMD_WIDTH) * SIMD_WIDTH; - - for (size_t i = start_idx; i < vectorized_end; i += SIMD_WIDTH) { - float16x8_t x_f16 = vld1q_f16(&input[i]); - - float32x4_t x_low = vcvt_f32_f16(vget_low_f16(x_f16)); - float32x4_t x_high = vcvt_f32_f16(vget_high_f16(x_f16)); - - float32x4_t tanh_low = fast_tanh_f32x4(x_low); - float32x4_t tanh_high = fast_tanh_f32x4(x_high); - - float16x8_t tanh_f16 = vcombine_f16(vcvt_f16_f32(tanh_low), vcvt_f16_f32(tanh_high)); - vst1q_f16(&output[i], tanh_f16); - } - - for (size_t i = vectorized_end; i < end_idx; ++i) { - float x = static_cast(input[i]); - output[i] = static_cast<__fp16>(std::tanh(x)); - } - }); -} - -void cactus_glu_f16( - const __fp16* input, - __fp16* output, - size_t outer_size, - size_t split_size, - size_t inner_size -) { - if (outer_size == 0 || split_size == 0 || inner_size == 0) return; - - const size_t axis_size = split_size * 2; - const size_t rows = outer_size * split_size; - - CactusThreading::parallel_for(rows, CactusThreading::Thresholds::ELEMENT_WISE, - [&](size_t row_start, size_t row_end) { - const float32x4_t one = vdupq_n_f32(1.0f); - - for (size_t row = row_start; row < row_end; ++row) { - const size_t outer_idx = row / split_size; - const size_t split_idx = row - outer_idx * split_size; - const size_t in_base = outer_idx * axis_size * inner_size; - const size_t out_base = outer_idx * split_size * inner_size; - - const __fp16* a = input + in_base + split_idx * inner_size; - const __fp16* b = input + in_base + (split_idx + split_size) * inner_size; - __fp16* y = output + out_base + split_idx * inner_size; - - size_t i = 0; - for (; i + 8 <= inner_size; i += 8) { - const float16x8_t av = vld1q_f16(a + i); - const float16x8_t bv = vld1q_f16(b + i); - - const float32x4_t a_lo = vcvt_f32_f16(vget_low_f16(av)); - const float32x4_t a_hi = vcvt_f32_f16(vget_high_f16(av)); - const float32x4_t b_lo = vcvt_f32_f16(vget_low_f16(bv)); - const float32x4_t b_hi = vcvt_f32_f16(vget_high_f16(bv)); - - const float32x4_t exp_lo = fast_exp_f32x4(vnegq_f32(b_lo)); - const float32x4_t exp_hi = fast_exp_f32x4(vnegq_f32(b_hi)); - const float32x4_t gate_lo = vdivq_f32(one, vaddq_f32(one, exp_lo)); - const float32x4_t gate_hi = vdivq_f32(one, vaddq_f32(one, exp_hi)); - - const float32x4_t y_lo = vmulq_f32(a_lo, gate_lo); - const float32x4_t y_hi = vmulq_f32(a_hi, gate_hi); - vst1q_f16(y + i, vcombine_f16(vcvt_f16_f32(y_lo), vcvt_f16_f32(y_hi))); - } - - for (; i < inner_size; ++i) { - const float gate = 1.0f / (1.0f + expf(-static_cast(b[i]))); - y[i] = static_cast<__fp16>(static_cast(a[i]) * gate); - } - } - }); -} - -void cactus_glu_f32( - const float* input, - float* output, - size_t outer_size, - size_t split_size, - size_t inner_size -) { - if (outer_size == 0 || split_size == 0 || inner_size == 0) return; - - const size_t axis_size = split_size * 2; - const size_t rows = outer_size * split_size; - - CactusThreading::parallel_for(rows, CactusThreading::Thresholds::ELEMENT_WISE, - [&](size_t row_start, size_t row_end) { - const float32x4_t one = vdupq_n_f32(1.0f); - - for (size_t row = row_start; row < row_end; ++row) { - const size_t outer_idx = row / split_size; - const size_t split_idx = row - outer_idx * split_size; - const size_t in_base = outer_idx * axis_size * inner_size; - const size_t out_base = outer_idx * split_size * inner_size; - - const float* a = input + in_base + split_idx * inner_size; - const float* b = input + in_base + (split_idx + split_size) * inner_size; - float* y = output + out_base + split_idx * inner_size; - - size_t i = 0; - for (; i + 4 <= inner_size; i += 4) { - const float32x4_t av = vld1q_f32(a + i); - const float32x4_t bv = vld1q_f32(b + i); - const float32x4_t expv = fast_exp_f32x4(vnegq_f32(bv)); - const float32x4_t gate = vdivq_f32(one, vaddq_f32(one, expv)); - vst1q_f32(y + i, vmulq_f32(av, gate)); - } - - for (; i < inner_size; ++i) { - const float gate = 1.0f / (1.0f + expf(-b[i])); - y[i] = a[i] * gate; - } - } - }); -} - -void cactus_batchnorm_f16( - const __fp16* input, - const float* weight, - const float* bias, - const float* running_mean, - const float* running_var, - __fp16* output, - size_t outer_size, - size_t channels, - size_t inner_size, - float epsilon -) { - if (outer_size == 0 || channels == 0 || inner_size == 0) return; - - std::vector scale(channels); - std::vector shift(channels); - for (size_t c = 0; c < channels; ++c) { - const float inv_std = 1.0f / std::sqrt(running_var[c] + epsilon); - scale[c] = weight[c] * inv_std; - shift[c] = bias[c] - running_mean[c] * scale[c]; - } - - const size_t rows = outer_size * channels; - CactusThreading::parallel_for(rows, CactusThreading::Thresholds::ELEMENT_WISE, - [&](size_t row_start, size_t row_end) { - for (size_t row = row_start; row < row_end; ++row) { - const size_t c = row % channels; - const float32x4_t scv = vdupq_n_f32(scale[c]); - const float32x4_t shv = vdupq_n_f32(shift[c]); - - const __fp16* x = input + row * inner_size; - __fp16* y = output + row * inner_size; - - size_t i = 0; - for (; i + 8 <= inner_size; i += 8) { - const float16x8_t xv = vld1q_f16(x + i); - float32x4_t lo = vcvt_f32_f16(vget_low_f16(xv)); - float32x4_t hi = vcvt_f32_f16(vget_high_f16(xv)); - lo = vfmaq_f32(shv, lo, scv); - hi = vfmaq_f32(shv, hi, scv); - const float16x8_t yv = vcombine_f16(vcvt_f16_f32(lo), vcvt_f16_f32(hi)); - vst1q_f16(y + i, yv); - } - - for (; i < inner_size; ++i) { - y[i] = static_cast<__fp16>(static_cast(x[i]) * scale[c] + shift[c]); - } - } - }); -} - -void cactus_batchnorm_f32( - const float* input, - const float* weight, - const float* bias, - const float* running_mean, - const float* running_var, - float* output, - size_t outer_size, - size_t channels, - size_t inner_size, - float epsilon -) { - if (outer_size == 0 || channels == 0 || inner_size == 0) return; - - std::vector scale(channels); - std::vector shift(channels); - for (size_t c = 0; c < channels; ++c) { - const float inv_std = 1.0f / std::sqrt(running_var[c] + epsilon); - scale[c] = weight[c] * inv_std; - shift[c] = bias[c] - running_mean[c] * scale[c]; - } - - const size_t rows = outer_size * channels; - CactusThreading::parallel_for(rows, CactusThreading::Thresholds::ELEMENT_WISE, - [&](size_t row_start, size_t row_end) { - for (size_t row = row_start; row < row_end; ++row) { - const size_t c = row % channels; - const float32x4_t scv = vdupq_n_f32(scale[c]); - const float32x4_t shv = vdupq_n_f32(shift[c]); - - const float* x = input + row * inner_size; - float* y = output + row * inner_size; - - size_t i = 0; - for (; i + 4 <= inner_size; i += 4) { - const float32x4_t xv = vld1q_f32(x + i); - vst1q_f32(y + i, vfmaq_f32(shv, xv, scv)); - } - - for (; i < inner_size; ++i) { - y[i] = x[i] * scale[c] + shift[c]; - } - } - }); -} - -void kernel_softmax_f16_single(const __fp16* input, __fp16* output, size_t vocab_size) { - - constexpr size_t SIMD_WIDTH = 8; - constexpr size_t UNROLL_FACTOR = 4; - constexpr size_t VECTORIZED_WIDTH = SIMD_WIDTH * UNROLL_FACTOR; - const size_t vocab_vectorized = (vocab_size / VECTORIZED_WIDTH) * VECTORIZED_WIDTH; - - float32x4_t max_vec[UNROLL_FACTOR * 2]; - for (size_t u = 0; u < UNROLL_FACTOR * 2; u++) { - max_vec[u] = vdupq_n_f32(-std::numeric_limits::infinity()); - } - - for (size_t i = 0; i < vocab_vectorized; i += VECTORIZED_WIDTH) { - for (size_t u = 0; u < UNROLL_FACTOR; u++) { - float16x8_t x_vec_f16 = vld1q_f16(&input[i + u * SIMD_WIDTH]); - float32x4_t x_low = vcvt_f32_f16(vget_low_f16(x_vec_f16)); - float32x4_t x_high = vcvt_f32_f16(vget_high_f16(x_vec_f16)); - max_vec[u * 2] = vmaxq_f32(max_vec[u * 2], x_low); - max_vec[u * 2 + 1] = vmaxq_f32(max_vec[u * 2 + 1], x_high); - } - } - - float32x4_t final_max = max_vec[0]; - for (size_t u = 1; u < UNROLL_FACTOR * 2; u++) { - final_max = vmaxq_f32(final_max, max_vec[u]); - } - - float max_val = vmaxvq_f32(final_max); - for (size_t i = vocab_vectorized; i < vocab_size; ++i) { - max_val = std::max(max_val, static_cast(input[i])); - } - - const float32x4_t max_broadcast = vdupq_n_f32(max_val); - - float32x4_t sum_vec[UNROLL_FACTOR * 2]; - for (size_t u = 0; u < UNROLL_FACTOR * 2; u++) { - sum_vec[u] = vdupq_n_f32(0.0f); - } - - for (size_t i = 0; i < vocab_vectorized; i += VECTORIZED_WIDTH) { - for (size_t u = 0; u < UNROLL_FACTOR; u++) { - float16x8_t x_vec_f16 = vld1q_f16(&input[i + u * SIMD_WIDTH]); - - float32x4_t x_low = vcvt_f32_f16(vget_low_f16(x_vec_f16)); - float32x4_t x_high = vcvt_f32_f16(vget_high_f16(x_vec_f16)); - - float32x4_t exp_low = fast_exp_f32x4(vsubq_f32(x_low, max_broadcast)); - float32x4_t exp_high = fast_exp_f32x4(vsubq_f32(x_high, max_broadcast)); - - float16x8_t exp_f16 = vcombine_f16(vcvt_f16_f32(exp_low), vcvt_f16_f32(exp_high)); - vst1q_f16(&output[i + u * SIMD_WIDTH], exp_f16); - - sum_vec[u * 2] = vaddq_f32(sum_vec[u * 2], exp_low); - sum_vec[u * 2 + 1] = vaddq_f32(sum_vec[u * 2 + 1], exp_high); - } - } - - float32x4_t final_sum = sum_vec[0]; - for (size_t u = 1; u < UNROLL_FACTOR * 2; u++) { - final_sum = vaddq_f32(final_sum, sum_vec[u]); - } - - float sum = vaddvq_f32(final_sum); - for (size_t i = vocab_vectorized; i < vocab_size; ++i) { - float exp_val = expf(static_cast(input[i]) - max_val); - output[i] = static_cast<__fp16>(exp_val); - sum += exp_val; - } - - const float inv_sum = 1.0f / sum; - const float16x8_t inv_sum_vec_f16 = vdupq_n_f16(static_cast<__fp16>(inv_sum)); - - for (size_t i = 0; i < vocab_vectorized; i += VECTORIZED_WIDTH) { - for (size_t u = 0; u < UNROLL_FACTOR; u++) { - float16x8_t exp_vec = vld1q_f16(&output[i + u * SIMD_WIDTH]); - float16x8_t result = vmulq_f16(exp_vec, inv_sum_vec_f16); - vst1q_f16(&output[i + u * SIMD_WIDTH], result); - } - } - - for (size_t i = vocab_vectorized; i < vocab_size; ++i) { - output[i] = static_cast<__fp16>(static_cast(output[i]) * inv_sum); - } -} - -void cactus_softmax_f16( - const __fp16* input, - __fp16* output, - size_t batch_size, - size_t seq_len, - size_t vocab_size -) { - CactusThreading::parallel_for(batch_size * seq_len, CactusThreading::Thresholds::SCALAR_EXPENSIVE, - [&](size_t start_idx, size_t end_idx) { - for (size_t idx = start_idx; idx < end_idx; ++idx) { - const size_t offset = idx * vocab_size; - kernel_softmax_f16_single(input + offset, output + offset, vocab_size); - } - }); -} - -void cactus_sample_f32(const float* logits, uint32_t* output, size_t vocab_size, - float temperature, float top_p, size_t top_k, size_t random_seed, - const float* bias_values, const uint32_t* bias_indices, - size_t bias_count) { - cactus_sample_f32_ex(logits, output, vocab_size, - temperature, top_p, 0.15f, 1.1f, - top_k, random_seed, - bias_values, bias_indices, bias_count); -} - -void cactus_sample_f32_ex(const float* logits, uint32_t* output, size_t vocab_size, - float temperature, float top_p, float min_p, float repetition_penalty, - size_t top_k, size_t random_seed, - const float* bias_values, const uint32_t* bias_indices, - size_t bias_count) { - if (vocab_size == 0) { - output[0] = 0; - return; - } - - const bool has_bias = bias_values && bias_indices && bias_count > 0; - - std::vector filtered_logits(vocab_size); - std::memcpy(filtered_logits.data(), logits, vocab_size * sizeof(float)); - - if (has_bias) { - for (size_t i = 0; i < bias_count; ++i) { - uint32_t idx = bias_indices[i]; - if (idx < vocab_size) { - filtered_logits[idx] += bias_values[i]; - } - } - } - - if (temperature == 0.0f) { - auto it = std::max_element(filtered_logits.begin(), filtered_logits.end()); - output[0] = static_cast(std::distance(filtered_logits.begin(), it)); - return; - } - - if (temperature > 0) { - for (size_t i = 0; i < vocab_size; ++i) { - filtered_logits[i] /= temperature; - } - } - - (void)repetition_penalty; - - if (top_k > 0) { - std::vector> logit_pairs; - logit_pairs.reserve(vocab_size); - for (size_t i = 0; i < vocab_size; ++i) { - logit_pairs.emplace_back(filtered_logits[i], i); - } - std::sort(logit_pairs.begin(), logit_pairs.end(), - [](const auto& a, const auto& b) { return a.first > b.first; }); - - if (top_k < vocab_size) { - float kth_value = logit_pairs[top_k - 1].first; - for (size_t i = 0; i < vocab_size; ++i) { - if (filtered_logits[i] < kth_value) { - filtered_logits[i] = -std::numeric_limits::infinity(); - } - } - } - } - - if (min_p > 0.0f) { - float max_logit = *std::max_element(filtered_logits.begin(), filtered_logits.end()); - if (!std::isinf(max_logit)) { - std::vector temp_probs(vocab_size); - float sum = 0.0f; - for (size_t i = 0; i < vocab_size; ++i) { - if (!std::isinf(filtered_logits[i])) { - temp_probs[i] = std::exp(filtered_logits[i] - max_logit); - sum += temp_probs[i]; - } else { - temp_probs[i] = 0.0f; - } - } - - if (sum > 0.0f) { - for (size_t i = 0; i < vocab_size; ++i) { - temp_probs[i] /= sum; - } - - float max_prob = *std::max_element(temp_probs.begin(), temp_probs.end()); - float threshold = max_prob * min_p; - - for (size_t i = 0; i < vocab_size; ++i) { - if (temp_probs[i] < threshold) { - filtered_logits[i] = -std::numeric_limits::infinity(); - } - } - } - } - } - - if (top_p > 0.0f && top_p < 1.0f) { - std::vector> sorted_logits; - sorted_logits.reserve(vocab_size); - for (size_t i = 0; i < vocab_size; ++i) { - if (!std::isinf(filtered_logits[i])) { - sorted_logits.emplace_back(filtered_logits[i], i); - } - } - std::sort(sorted_logits.begin(), sorted_logits.end(), - [](const auto& a, const auto& b) { return a.first > b.first; }); - - float max_logit = sorted_logits.empty() ? 0.0f : sorted_logits[0].first; - std::vector temp_probs; - temp_probs.reserve(sorted_logits.size()); - float sum = 0.0f; - for (const auto& pair : sorted_logits) { - float prob = std::exp(pair.first - max_logit); - temp_probs.push_back(prob); - sum += prob; - } - - for (float& prob : temp_probs) { - prob /= sum; - } - - float cumulative_prob = 0.0f; - std::vector indices_to_remove(sorted_logits.size(), false); - for (size_t i = 0; i < sorted_logits.size(); ++i) { - cumulative_prob += temp_probs[i]; - if (cumulative_prob > top_p) { - indices_to_remove[i] = true; - } - } - - if (!indices_to_remove.empty()) { - for (size_t i = 1; i < indices_to_remove.size(); ++i) { - indices_to_remove[i] = indices_to_remove[i-1] || indices_to_remove[i]; - } - indices_to_remove[0] = false; - } - - for (size_t i = 0; i < sorted_logits.size(); ++i) { - if (indices_to_remove[i]) { - filtered_logits[sorted_logits[i].second] = -std::numeric_limits::infinity(); - } - } - } - - float max_logit = *std::max_element(filtered_logits.begin(), filtered_logits.end()); - if (std::isinf(max_logit)) { - output[0] = 0; - return; - } - - std::vector probs(vocab_size); - float sum = 0.0f; - for (size_t i = 0; i < vocab_size; ++i) { - if (std::isinf(filtered_logits[i])) { - probs[i] = 0.0f; - } else { - probs[i] = std::exp(filtered_logits[i] - max_logit); - sum += probs[i]; - } - } - - if (sum == 0.0f) { - output[0] = 0; - return; - } - - for (size_t i = 0; i < vocab_size; ++i) { - probs[i] /= sum; - } - - uint32_t actual_seed = (random_seed == 0) ? std::random_device{}() : random_seed; - std::mt19937 gen(actual_seed); - std::uniform_real_distribution dist(0.0f, 1.0f); - float sample = dist(gen); - - float cumulative = 0.0f; - for (size_t i = 0; i < vocab_size; ++i) { - cumulative += probs[i]; - if (cumulative >= sample) { - output[0] = static_cast(i); - return; - } - } - - for (size_t i = vocab_size; i > 0; --i) { - if (probs[i-1] > 0.0f) { - output[0] = static_cast(i-1); - return; - } - } - - output[0] = 0; -} - -void cactus_sample_f16(const __fp16* logits, uint32_t* output, size_t vocab_size, - float temperature, float top_p, size_t top_k, size_t random_seed, - const float* bias_values, const uint32_t* bias_indices, - size_t bias_count) { - cactus_sample_f16_ex(logits, output, vocab_size, - temperature, top_p, 0.15f, 1.1f, - top_k, random_seed, - bias_values, bias_indices, bias_count); -} - -void cactus_sample_f16_ex(const __fp16* logits, uint32_t* output, size_t vocab_size, - float temperature, float top_p, float min_p, float repetition_penalty, - size_t top_k, size_t random_seed, - const float* bias_values, const uint32_t* bias_indices, - size_t bias_count) { - if (vocab_size == 0) { - output[0] = 0; - return; - } - - const bool has_bias = bias_values && bias_indices && bias_count > 0; - - std::vector<__fp16> filtered_logits(vocab_size); - std::memcpy(filtered_logits.data(), logits, vocab_size * sizeof(__fp16)); - - if (has_bias) { - for (size_t i = 0; i < bias_count; ++i) { - uint32_t idx = bias_indices[i]; - if (idx < vocab_size) { - filtered_logits[idx] = static_cast<__fp16>(static_cast(filtered_logits[idx]) + bias_values[i]); - } - } - } - - if (temperature == 0.0f) { - auto it = std::max_element(filtered_logits.begin(), filtered_logits.end()); - output[0] = static_cast(std::distance(filtered_logits.begin(), it)); - return; - } - - if (temperature > 0) { - __fp16 inv_temp = static_cast<__fp16>(1.0f / temperature); - float16x8_t inv_temp_vec = vdupq_n_f16(inv_temp); - size_t i = 0; - for (; i + 8 <= vocab_size; i += 8) { - float16x8_t logits_vec = vld1q_f16(&filtered_logits[i]); - float16x8_t scaled = vmulq_f16(logits_vec, inv_temp_vec); - vst1q_f16(&filtered_logits[i], scaled); - } - for (; i < vocab_size; ++i) { - filtered_logits[i] = filtered_logits[i] * inv_temp; - } - } - - (void)repetition_penalty; - - if (top_k > 0) { - std::vector> logit_pairs; - logit_pairs.reserve(vocab_size); - for (size_t i = 0; i < vocab_size; ++i) { - logit_pairs.emplace_back(filtered_logits[i], i); - } - std::partial_sort(logit_pairs.begin(), - logit_pairs.begin() + std::min(top_k, vocab_size), - logit_pairs.end(), - [](const auto& a, const auto& b) { return a.first > b.first; }); - - if (top_k < vocab_size) { - __fp16 kth_value = logit_pairs[top_k - 1].first; - __fp16 neg_inf = static_cast<__fp16>(-std::numeric_limits::infinity()); - for (size_t i = 0; i < vocab_size; ++i) { - if (filtered_logits[i] < kth_value) { - filtered_logits[i] = neg_inf; - } - } - } - } - - if (min_p > 0.0f) { - float max_logit = -std::numeric_limits::infinity(); - for (size_t i = 0; i < vocab_size; ++i) { - max_logit = std::max(max_logit, static_cast(filtered_logits[i])); - } - if (!std::isinf(max_logit)) { - std::vector temp_probs(vocab_size); - float sum = 0.0f; - for (size_t i = 0; i < vocab_size; ++i) { - float v = static_cast(filtered_logits[i]); - if (!std::isinf(v)) { - temp_probs[i] = std::exp(v - max_logit); - sum += temp_probs[i]; - } else { - temp_probs[i] = 0.0f; - } - } - if (sum > 0.0f) { - for (float& p : temp_probs) p /= sum; - float max_prob = *std::max_element(temp_probs.begin(), temp_probs.end()); - float threshold = max_prob * min_p; - __fp16 neg_inf = static_cast<__fp16>(-std::numeric_limits::infinity()); - for (size_t i = 0; i < vocab_size; ++i) { - if (temp_probs[i] < threshold) { - filtered_logits[i] = neg_inf; - } - } - } - } - } - - if (top_p > 0.0f && top_p < 1.0f) { - std::vector> sorted_logits; - sorted_logits.reserve(vocab_size); - __fp16 neg_inf = static_cast<__fp16>(-std::numeric_limits::infinity()); - for (size_t i = 0; i < vocab_size; ++i) { - if (filtered_logits[i] != neg_inf) { - sorted_logits.emplace_back(filtered_logits[i], i); - } - } - std::sort(sorted_logits.begin(), sorted_logits.end(), - [](const auto& a, const auto& b) { return a.first > b.first; }); - - __fp16 max_logit = sorted_logits.empty() ? static_cast<__fp16>(0.0f) : sorted_logits[0].first; - std::vector temp_probs; - temp_probs.reserve(sorted_logits.size()); - float sum = 0.0f; - for (const auto& pair : sorted_logits) { - float prob = std::exp(static_cast(pair.first - max_logit)); - temp_probs.push_back(prob); - sum += prob; - } - - for (float& prob : temp_probs) { - prob /= sum; - } - - float cumulative_prob = 0.0f; - std::vector indices_to_remove(sorted_logits.size(), false); - bool threshold_reached = false; - for (size_t i = 0; i < sorted_logits.size(); ++i) { - cumulative_prob += temp_probs[i]; - if (cumulative_prob > top_p && i > 0) { - threshold_reached = true; - } - if (threshold_reached) { - indices_to_remove[i] = true; - } - } - - if (!indices_to_remove.empty()) { - for (size_t i = 1; i < indices_to_remove.size(); ++i) { - indices_to_remove[i] = indices_to_remove[i-1] || indices_to_remove[i]; - } - indices_to_remove[0] = false; - } - - for (size_t i = 0; i < sorted_logits.size(); ++i) { - if (indices_to_remove[i]) { - filtered_logits[sorted_logits[i].second] = neg_inf; - } - } - } - - __fp16 max_logit = *std::max_element(filtered_logits.begin(), filtered_logits.end()); - __fp16 neg_inf = static_cast<__fp16>(-std::numeric_limits::infinity()); - if (max_logit == neg_inf) { - output[0] = 0; - return; - } - - std::vector probs(vocab_size); - float sum = 0.0f; - for (size_t i = 0; i < vocab_size; ++i) { - if (filtered_logits[i] == neg_inf) { - probs[i] = 0.0f; - } else { - probs[i] = std::exp(static_cast(filtered_logits[i] - max_logit)); - sum += probs[i]; - } - } - - if (sum == 0.0f) { - output[0] = 0; - return; - } - - for (size_t i = 0; i < vocab_size; ++i) { - probs[i] /= sum; - } - - uint32_t actual_seed = (random_seed == 0) ? std::random_device{}() : random_seed; - std::mt19937 gen(actual_seed); - std::uniform_real_distribution dist(0.0f, 1.0f); - float sample = dist(gen); - - float cumulative = 0.0f; - for (size_t i = 0; i < vocab_size; ++i) { - cumulative += probs[i]; - if (cumulative >= sample) { - output[0] = static_cast(i); - return; - } - } - - for (size_t i = vocab_size; i > 0; --i) { - if (probs[i-1] > 0.0f) { - output[0] = static_cast(i-1); - return; - } - } - - output[0] = 0; -} diff --git a/cactus/kernel/kernel_quants.cpp b/cactus/kernel/kernel_quants.cpp deleted file mode 100644 index d3553603b..000000000 --- a/cactus/kernel/kernel_quants.cpp +++ /dev/null @@ -1,312 +0,0 @@ -#include "kernel.h" -#include "kernel_utils.h" -#include -#include -#include -#include - -void cactus_int8_to_fp32(const int8_t* src, float* dst, size_t count, float scale) { - CactusThreading::parallel_for(count, CactusThreading::Thresholds::ELEMENT_WISE, - [src, dst, scale](size_t start, size_t end) { - const size_t simd_end = start + ((end - start) / 16) * 16; - float32x4_t scale_vec = vdupq_n_f32(scale); - - for (size_t i = start; i < simd_end; i += 16) { - int8x16_t input = vld1q_s8(&src[i]); - - int16x8_t low = vmovl_s8(vget_low_s8(input)); - int16x8_t high = vmovl_s8(vget_high_s8(input)); - - int32x4_t low_low = vmovl_s16(vget_low_s16(low)); - int32x4_t low_high = vmovl_s16(vget_high_s16(low)); - int32x4_t high_low = vmovl_s16(vget_low_s16(high)); - int32x4_t high_high = vmovl_s16(vget_high_s16(high)); - - float32x4_t f_low_low = vcvtq_f32_s32(low_low); - float32x4_t f_low_high = vcvtq_f32_s32(low_high); - float32x4_t f_high_low = vcvtq_f32_s32(high_low); - float32x4_t f_high_high = vcvtq_f32_s32(high_high); - - vst1q_f32(&dst[i], vmulq_f32(f_low_low, scale_vec)); - vst1q_f32(&dst[i + 4], vmulq_f32(f_low_high, scale_vec)); - vst1q_f32(&dst[i + 8], vmulq_f32(f_high_low, scale_vec)); - vst1q_f32(&dst[i + 12], vmulq_f32(f_high_high, scale_vec)); - } - - for (size_t i = simd_end; i < end; ++i) { - dst[i] = static_cast(src[i]) * scale; - } - }); -} - -void cactus_fp32_to_int8(const float* src, int8_t* dst, size_t count, float scale) { - const float inv_scale = 1.0f / scale; - - CactusThreading::parallel_for(count, CactusThreading::Thresholds::ELEMENT_WISE, - [src, dst, inv_scale](size_t start, size_t end) { - const size_t simd_end = start + ((end - start) / 16) * 16; - float32x4_t inv_scale_vec = vdupq_n_f32(inv_scale); - float32x4_t min_vec = vdupq_n_f32(-128.0f); - float32x4_t max_vec = vdupq_n_f32(127.0f); - - for (size_t i = start; i < simd_end; i += 16) { - float32x4_t input_0 = vld1q_f32(&src[i]); - float32x4_t input_1 = vld1q_f32(&src[i + 4]); - float32x4_t input_2 = vld1q_f32(&src[i + 8]); - float32x4_t input_3 = vld1q_f32(&src[i + 12]); - - float32x4_t scaled_0 = vmulq_f32(input_0, inv_scale_vec); - float32x4_t scaled_1 = vmulq_f32(input_1, inv_scale_vec); - float32x4_t scaled_2 = vmulq_f32(input_2, inv_scale_vec); - float32x4_t scaled_3 = vmulq_f32(input_3, inv_scale_vec); - - scaled_0 = vmaxq_f32(vminq_f32(scaled_0, max_vec), min_vec); - scaled_1 = vmaxq_f32(vminq_f32(scaled_1, max_vec), min_vec); - scaled_2 = vmaxq_f32(vminq_f32(scaled_2, max_vec), min_vec); - scaled_3 = vmaxq_f32(vminq_f32(scaled_3, max_vec), min_vec); - - int32x4_t int_0 = vcvtnq_s32_f32(scaled_0); - int32x4_t int_1 = vcvtnq_s32_f32(scaled_1); - int32x4_t int_2 = vcvtnq_s32_f32(scaled_2); - int32x4_t int_3 = vcvtnq_s32_f32(scaled_3); - - int16x8_t int16_low = vcombine_s16(vqmovn_s32(int_0), vqmovn_s32(int_1)); - int16x8_t int16_high = vcombine_s16(vqmovn_s32(int_2), vqmovn_s32(int_3)); - - int8x16_t result = vcombine_s8(vqmovn_s16(int16_low), vqmovn_s16(int16_high)); - vst1q_s8(&dst[i], result); - } - - for (size_t i = simd_end; i < end; ++i) { - float quantized = src[i] * inv_scale; - dst[i] = static_cast(std::round(std::max(-128.0f, std::min(127.0f, quantized)))); - } - }); -} - -void cactus_fp16_to_fp32(const __fp16* src, float* dst, size_t count) { - CactusThreading::parallel_for(count, CactusThreading::Thresholds::ELEMENT_WISE, - [src, dst](size_t start, size_t end) { - const size_t simd_end = start + ((end - start) / 8) * 8; - - for (size_t i = start; i < simd_end; i += 8) { - float16x8_t input = vld1q_f16(&src[i]); - - float32x4_t output_low = vcvt_f32_f16(vget_low_f16(input)); - float32x4_t output_high = vcvt_f32_f16(vget_high_f16(input)); - - vst1q_f32(&dst[i], output_low); - vst1q_f32(&dst[i + 4], output_high); - } - - for (size_t i = simd_end; i < end; ++i) { - dst[i] = static_cast(src[i]); - } - }); -} - -void cactus_fp32_to_fp16(const float* src, __fp16* dst, size_t count) { - CactusThreading::parallel_for(count, CactusThreading::Thresholds::ELEMENT_WISE, - [src, dst](size_t start, size_t end) { - const size_t simd_end = start + ((end - start) / 8) * 8; - - for (size_t i = start; i < simd_end; i += 8) { - float32x4_t input_low = vld1q_f32(&src[i]); - float32x4_t input_high = vld1q_f32(&src[i + 4]); - - float16x4_t output_low = vcvt_f16_f32(input_low); - float16x4_t output_high = vcvt_f16_f32(input_high); - - float16x8_t output = vcombine_f16(output_low, output_high); - vst1q_f16(&dst[i], output); - } - - for (size_t i = simd_end; i < end; ++i) { - dst[i] = static_cast<__fp16>(src[i]); - } - }); -} - -void cactus_int8_to_fp16(const int8_t* src, __fp16* dst, size_t count, float scale) { - CactusThreading::parallel_for(count, CactusThreading::Thresholds::ELEMENT_WISE, - [src, dst, scale](size_t start, size_t end) { - const size_t simd_end = start + ((end - start) / 8) * 8; - float32x4_t scale_vec = vdupq_n_f32(scale); - - for (size_t i = start; i < simd_end; i += 8) { - int8x8_t input = vld1_s8(&src[i]); - - int16x8_t int16 = vmovl_s8(input); - int32x4_t int32_low = vmovl_s16(vget_low_s16(int16)); - int32x4_t int32_high = vmovl_s16(vget_high_s16(int16)); - - float32x4_t float_low = vcvtq_f32_s32(int32_low); - float32x4_t float_high = vcvtq_f32_s32(int32_high); - - float_low = vmulq_f32(float_low, scale_vec); - float_high = vmulq_f32(float_high, scale_vec); - - float16x8_t output = vcombine_f16(vcvt_f16_f32(float_low), vcvt_f16_f32(float_high)); - vst1q_f16(&dst[i], output); - } - - for (size_t i = simd_end; i < end; ++i) { - dst[i] = static_cast<__fp16>(static_cast(src[i]) * scale); - } - }); -} - -void cactus_fp16_to_int8(const __fp16* src, int8_t* dst, size_t count, float scale) { - const float inv_scale = 1.0f / scale; - - CactusThreading::parallel_for(count, CactusThreading::Thresholds::ELEMENT_WISE, - [src, dst, inv_scale](size_t start, size_t end) { - const size_t simd_end = start + ((end - start) / 8) * 8; - float32x4_t inv_scale_vec = vdupq_n_f32(inv_scale); - float32x4_t min_vec = vdupq_n_f32(-128.0f); - float32x4_t max_vec = vdupq_n_f32(127.0f); - - for (size_t i = start; i < simd_end; i += 8) { - float16x8_t input = vld1q_f16(&src[i]); - - float32x4_t input_low = vcvt_f32_f16(vget_low_f16(input)); - float32x4_t input_high = vcvt_f32_f16(vget_high_f16(input)); - - float32x4_t scaled_low = vmulq_f32(input_low, inv_scale_vec); - float32x4_t scaled_high = vmulq_f32(input_high, inv_scale_vec); - - scaled_low = vmaxq_f32(vminq_f32(scaled_low, max_vec), min_vec); - scaled_high = vmaxq_f32(vminq_f32(scaled_high, max_vec), min_vec); - - int32x4_t int_low = vcvtnq_s32_f32(scaled_low); - int32x4_t int_high = vcvtnq_s32_f32(scaled_high); - - int16x8_t int16_combined = vcombine_s16(vqmovn_s32(int_low), vqmovn_s32(int_high)); - int8x8_t result = vqmovn_s16(int16_combined); - - vst1_s8(&dst[i], result); - } - - for (size_t i = simd_end; i < end; ++i) { - float quantized = static_cast(src[i]) * inv_scale; - dst[i] = static_cast(std::round(std::max(-128.0f, std::min(127.0f, quantized)))); - } - }); -} - -float cactus_fp16_max_abs(const __fp16* src, size_t count) { - float32x4_t abs_max_vec = vdupq_n_f32(0.0f); - const size_t simd_end = (count / 8) * 8; - - for (size_t i = 0; i < simd_end; i += 8) { - float16x8_t input = vld1q_f16(&src[i]); - - float32x4_t input_low = vcvt_f32_f16(vget_low_f16(input)); - float32x4_t input_high = vcvt_f32_f16(vget_high_f16(input)); - - float32x4_t abs_low = vabsq_f32(input_low); - float32x4_t abs_high = vabsq_f32(input_high); - - abs_max_vec = vmaxq_f32(abs_max_vec, abs_low); - abs_max_vec = vmaxq_f32(abs_max_vec, abs_high); - } - - float max_abs = vmaxvq_f32(abs_max_vec); - - for (size_t i = simd_end; i < count; ++i) { - float abs_val = std::abs(static_cast(src[i])); - max_abs = std::max(max_abs, abs_val); - } - - return max_abs; -} - -static inline float quantize_group_fp16_to_int8(const __fp16* src, int8_t* dst, size_t count) { - - float32x4_t max_vec = vdupq_n_f32(0.0f); - size_t k = 0; - - for (; k + 8 <= count; k += 8) { - float16x8_t vals = vld1q_f16(src + k); - max_vec = vmaxq_f32(max_vec, vabsq_f32(vcvt_f32_f16(vget_low_f16(vals)))); - max_vec = vmaxq_f32(max_vec, vabsq_f32(vcvt_f32_f16(vget_high_f16(vals)))); - } - - float max_abs = vmaxvq_f32(max_vec); - for (; k < count; k++) { - float val = std::abs(static_cast(src[k])); - if (val > max_abs) max_abs = val; - } - - float scale = max_abs / 127.0f; - if (scale < 1e-10f) scale = 1e-10f; - float inv_scale = 1.0f / scale; - float32x4_t inv_scale_vec = vdupq_n_f32(inv_scale); - - k = 0; - for (; k + 8 <= count; k += 8) { - float16x8_t vals = vld1q_f16(src + k); - int32x4_t i0 = vcvtnq_s32_f32(vmulq_f32(vcvt_f32_f16(vget_low_f16(vals)), inv_scale_vec)); - int32x4_t i1 = vcvtnq_s32_f32(vmulq_f32(vcvt_f32_f16(vget_high_f16(vals)), inv_scale_vec)); - int16x4_t s0 = vqmovn_s32(i0); - int16x4_t s1 = vqmovn_s32(i1); - int8x8_t result = vqmovn_s16(vcombine_s16(s0, s1)); - vst1_s8(dst + k, result); - } - - for (; k < count; k++) { - float val = static_cast(src[k]) * inv_scale; - int32_t q = static_cast(roundf(val)); - q = std::max(-128, std::min(127, q)); - dst[k] = static_cast(q); - } - - return scale; -} - -void cactus_quantize_kv_fp16_to_int8( - const __fp16* src, - int8_t* dst, - float* scales, - size_t seq_len, size_t kv_heads, size_t head_dim, - size_t group_size -) { - const size_t num_groups = (head_dim + group_size - 1) / group_size; - - CactusThreading::parallel_for(seq_len * kv_heads, CactusThreading::Thresholds::ELEMENT_WISE, - [src, dst, scales, head_dim, group_size, num_groups](size_t start, size_t end) { - for (size_t idx = start; idx < end; idx++) { - const __fp16* head_src = src + idx * head_dim; - int8_t* head_dst = dst + idx * head_dim; - float* head_scales = scales + idx * num_groups; - - for (size_t g = 0; g < num_groups; g++) { - size_t group_start = g * group_size; - size_t group_count = std::min(group_size, head_dim - group_start); - head_scales[g] = quantize_group_fp16_to_int8( - head_src + group_start, - head_dst + group_start, - group_count - ); - } - } - }); -} - -void cactus_unpack_int4_to_int8(const uint8_t* packed, int8_t* unpacked, size_t unpacked_count) { - assert (unpacked_count % 32 == 0 && "Unpacked count must be a multiple of 32 for int4 unpacking"); - const size_t num_groups = unpacked_count / 32; - - CactusThreading::parallel_for(num_groups, CactusThreading::Thresholds::ELEMENT_WISE, - [packed, unpacked](size_t start, size_t end) { - for (size_t group = start; group < end; ++group) { - const uint8_t* src = &packed[group * 16]; - int8_t* dst = &unpacked[group * 32]; - - int8x16_t high_signed, low_signed; - unpack_int4_as_int8x16x2(src, high_signed, low_signed); - vst1q_s8(dst, low_signed); - vst1q_s8(dst + 16, high_signed); - } - }); -} diff --git a/cactus/kernel/kernel_reduce.cpp b/cactus/kernel/kernel_reduce.cpp deleted file mode 100644 index e0f3d19e8..000000000 --- a/cactus/kernel/kernel_reduce.cpp +++ /dev/null @@ -1,327 +0,0 @@ -#include "kernel.h" -#include "kernel_utils.h" -#include -#include - -template -static void axis_reduce_f32_impl(const __fp16* input, __fp16* output, - size_t outer_size, size_t axis_size, size_t inner_size, - FinalizeFn finalize) { - if (inner_size == 1) { - CactusThreading::parallel_for(outer_size, CactusThreading::Thresholds::AXIS_REDUCE, - [&](size_t start_outer, size_t end_outer) { - constexpr size_t W = SIMD_F16_WIDTH; - const size_t vec_axis = simd_align(axis_size); - - for (size_t outer = start_outer; outer < end_outer; ++outer) { - const __fp16* row = input + outer * axis_size; - float32x4_t sum_lo = vdupq_n_f32(0.0f); - float32x4_t sum_hi = vdupq_n_f32(0.0f); - - for (size_t a = 0; a < vec_axis; a += W) { - float32x4_t lo, hi; - f16x8_split_f32(vld1q_f16(row + a), lo, hi); - sum_lo = vaddq_f32(sum_lo, lo); - sum_hi = vaddq_f32(sum_hi, hi); - } - - float total = vaddvq_f32(vaddq_f32(sum_lo, sum_hi)); - for (size_t a = vec_axis; a < axis_size; ++a) { - total += static_cast(row[a]); - } - - output[outer] = static_cast<__fp16>(finalize(total, axis_size)); - } - }); - return; - } - - CactusThreading::parallel_for_2d(outer_size, inner_size, CactusThreading::Thresholds::AXIS_REDUCE, - [&](size_t outer, size_t inner) { - constexpr size_t W = SIMD_F16_WIDTH; - const size_t vec_axis = simd_align(axis_size); - - float32x4_t sum_lo = vdupq_n_f32(0.0f); - float32x4_t sum_hi = vdupq_n_f32(0.0f); - - for (size_t a = 0; a < vec_axis; a += W) { - __fp16 values[W]; - for (size_t j = 0; j < W; j++) { - values[j] = input[outer * axis_size * inner_size + (a + j) * inner_size + inner]; - } - float32x4_t lo, hi; - f16x8_split_f32(vld1q_f16(values), lo, hi); - sum_lo = vaddq_f32(sum_lo, lo); - sum_hi = vaddq_f32(sum_hi, hi); - } - - float total = vaddvq_f32(vaddq_f32(sum_lo, sum_hi)); - - for (size_t a = vec_axis; a < axis_size; a++) { - total += static_cast(input[outer * axis_size * inner_size + a * inner_size + inner]); - } - - output[outer * inner_size + inner] = static_cast<__fp16>(finalize(total, axis_size)); - }); -} - -template -static void axis_reduce_f16_impl(const __fp16* input, __fp16* output, - size_t outer_size, size_t axis_size, size_t inner_size, - float16x8_t init_vec, __fp16 init_scalar, - SimdReduceOp simd_reduce, ScalarReduceOp scalar_reduce) { - if (inner_size == 1) { - CactusThreading::parallel_for(outer_size, CactusThreading::Thresholds::AXIS_REDUCE, - [&](size_t start_outer, size_t end_outer) { - constexpr size_t W = SIMD_F16_WIDTH; - const size_t vec_axis = simd_align(axis_size); - - for (size_t outer = start_outer; outer < end_outer; ++outer) { - const __fp16* row = input + outer * axis_size; - float16x8_t acc = init_vec; - - for (size_t a = 0; a < vec_axis; a += W) { - acc = simd_reduce(acc, vld1q_f16(row + a)); - } - - __fp16 result = init_scalar; - __fp16 arr[W]; - vst1q_f16(arr, acc); - for (size_t j = 0; j < W; ++j) { - result = scalar_reduce(result, arr[j]); - } - - for (size_t a = vec_axis; a < axis_size; ++a) { - result = scalar_reduce(result, row[a]); - } - - output[outer] = result; - } - }); - return; - } - - CactusThreading::parallel_for_2d(outer_size, inner_size, CactusThreading::Thresholds::AXIS_REDUCE, - [&](size_t outer, size_t inner) { - constexpr size_t W = SIMD_F16_WIDTH; - const size_t vec_axis = simd_align(axis_size); - float16x8_t acc = init_vec; - - for (size_t a = 0; a < vec_axis; a += W) { - __fp16 values[W]; - for (size_t j = 0; j < W; j++) { - values[j] = input[outer * axis_size * inner_size + (a + j) * inner_size + inner]; - } - acc = simd_reduce(acc, vld1q_f16(values)); - } - - __fp16 result = init_scalar; - __fp16 arr[8]; - vst1q_f16(arr, acc); - for (int j = 0; j < 8; j++) { - result = scalar_reduce(result, arr[j]); - } - - for (size_t a = vec_axis; a < axis_size; a++) { - result = scalar_reduce(result, input[outer * axis_size * inner_size + a * inner_size + inner]); - } - - output[outer * inner_size + inner] = result; - }); -} - -double cactus_sum_all_f16(const __fp16* data, size_t num_elements) { - return CactusThreading::parallel_reduce( - num_elements, CactusThreading::Thresholds::ALL_REDUCE, - [&](size_t start, size_t end) -> double { - const size_t vec_end = start + simd_align(end - start); - float32x4_t sum_lo = vdupq_n_f32(0.0f); - float32x4_t sum_hi = vdupq_n_f32(0.0f); - - for (size_t i = start; i < vec_end; i += SIMD_F16_WIDTH) { - float32x4_t lo, hi; - f16x8_split_f32(vld1q_f16(&data[i]), lo, hi); - sum_lo = vaddq_f32(sum_lo, lo); - sum_hi = vaddq_f32(sum_hi, hi); - } - - double s = static_cast(vaddvq_f32(vaddq_f32(sum_lo, sum_hi))); - for (size_t i = vec_end; i < end; ++i) s += static_cast(data[i]); - return s; - }, - 0.0, [](double a, double b) { return a + b; } - ); -} - -void cactus_sum_axis_f16(const __fp16* input, __fp16* output, size_t outer_size, size_t axis_size, size_t inner_size) { - axis_reduce_f32_impl(input, output, outer_size, axis_size, inner_size, - [](float total, size_t) { return total; }); -} - -double cactus_mean_all_f16(const __fp16* data, size_t num_elements) { - return cactus_sum_all_f16(data, num_elements) / static_cast(num_elements); -} - -void cactus_mean_axis_f16(const __fp16* input, __fp16* output, size_t outer_size, size_t axis_size, size_t inner_size) { - axis_reduce_f32_impl(input, output, outer_size, axis_size, inner_size, - [](float total, size_t axis_size) { return total / static_cast(axis_size); }); -} - -struct VarianceState { - double sum; - double sum_sq; - VarianceState() : sum(0.0), sum_sq(0.0) {} - VarianceState(double s, double sq) : sum(s), sum_sq(sq) {} -}; - -double cactus_variance_all_f16(const __fp16* data, size_t num_elements) { - VarianceState result = CactusThreading::parallel_reduce( - num_elements, CactusThreading::Thresholds::ALL_REDUCE, - [&](size_t start, size_t end) -> VarianceState { - const size_t vec_end = start + simd_align(end - start); - - float32x4_t sum_lo = vdupq_n_f32(0.0f), sum_hi = vdupq_n_f32(0.0f); - float32x4_t sq_lo = vdupq_n_f32(0.0f), sq_hi = vdupq_n_f32(0.0f); - - for (size_t i = start; i < vec_end; i += SIMD_F16_WIDTH) { - float32x4_t lo, hi; - f16x8_split_f32(vld1q_f16(&data[i]), lo, hi); - sum_lo = vaddq_f32(sum_lo, lo); - sum_hi = vaddq_f32(sum_hi, hi); - sq_lo = vfmaq_f32(sq_lo, lo, lo); - sq_hi = vfmaq_f32(sq_hi, hi, hi); - } - - double sum = static_cast(vaddvq_f32(vaddq_f32(sum_lo, sum_hi))); - double sum_sq = static_cast(vaddvq_f32(vaddq_f32(sq_lo, sq_hi))); - - for (size_t i = vec_end; i < end; ++i) { - double x = static_cast(data[i]); - sum += x; - sum_sq += x * x; - } - return VarianceState(sum, sum_sq); - }, - VarianceState(), - [](const VarianceState& a, const VarianceState& b) { - return VarianceState(a.sum + b.sum, a.sum_sq + b.sum_sq); - } - ); - - double mean = result.sum / static_cast(num_elements); - double mean_sq = result.sum_sq / static_cast(num_elements); - return mean_sq - mean * mean; -} - -void cactus_variance_axis_f16(const __fp16* input, __fp16* output, size_t outer_size, size_t axis_size, size_t inner_size) { - if (inner_size == 1) { - CactusThreading::parallel_for(outer_size, CactusThreading::Thresholds::AXIS_REDUCE, - [&](size_t start_outer, size_t end_outer) { - constexpr size_t W = SIMD_F16_WIDTH; - const size_t vec_axis = simd_align(axis_size); - - for (size_t outer = start_outer; outer < end_outer; ++outer) { - const __fp16* row = input + outer * axis_size; - float32x4_t sum_lo = vdupq_n_f32(0.0f); - float32x4_t sum_hi = vdupq_n_f32(0.0f); - float32x4_t sq_lo = vdupq_n_f32(0.0f); - float32x4_t sq_hi = vdupq_n_f32(0.0f); - - for (size_t a = 0; a < vec_axis; a += W) { - float32x4_t lo, hi; - f16x8_split_f32(vld1q_f16(row + a), lo, hi); - sum_lo = vaddq_f32(sum_lo, lo); - sum_hi = vaddq_f32(sum_hi, hi); - sq_lo = vfmaq_f32(sq_lo, lo, lo); - sq_hi = vfmaq_f32(sq_hi, hi, hi); - } - - float sum = vaddvq_f32(vaddq_f32(sum_lo, sum_hi)); - float sum_sq = vaddvq_f32(vaddq_f32(sq_lo, sq_hi)); - for (size_t a = vec_axis; a < axis_size; ++a) { - float x = static_cast(row[a]); - sum += x; - sum_sq += x * x; - } - - float mean = sum / static_cast(axis_size); - float mean_sq = sum_sq / static_cast(axis_size); - output[outer] = static_cast<__fp16>(mean_sq - mean * mean); - } - }); - return; - } - - CactusThreading::parallel_for_2d(outer_size, inner_size, CactusThreading::Thresholds::AXIS_REDUCE, - [&](size_t outer, size_t inner) { - float sum = 0.0f, sum_sq = 0.0f; - for (size_t a = 0; a < axis_size; a++) { - float x = static_cast(input[outer * axis_size * inner_size + a * inner_size + inner]); - sum += x; - sum_sq += x * x; - } - float mean = sum / static_cast(axis_size); - float mean_sq = sum_sq / static_cast(axis_size); - output[outer * inner_size + inner] = static_cast<__fp16>(mean_sq - mean * mean); - }); -} - -__fp16 cactus_min_all_f16(const __fp16* data, size_t num_elements) { - return CactusThreading::parallel_reduce( - num_elements, CactusThreading::Thresholds::ALL_REDUCE, - [&](size_t start, size_t end) -> __fp16 { - const size_t vec_end = start + simd_align(end - start); - float16x8_t acc = vdupq_n_f16(static_cast<__fp16>(65504.0f)); - - for (size_t i = start; i < vec_end; i += SIMD_F16_WIDTH) { - acc = vminq_f16(acc, vld1q_f16(&data[i])); - } - - __fp16 result = static_cast<__fp16>(65504.0f); - __fp16 arr[8]; - vst1q_f16(arr, acc); - for (int j = 0; j < 8; j++) result = std::min(result, arr[j]); - for (size_t i = vec_end; i < end; ++i) result = std::min(result, data[i]); - return result; - }, - static_cast<__fp16>(65504.0f), - [](__fp16 a, __fp16 b) { return std::min(a, b); } - ); -} - -void cactus_min_axis_f16(const __fp16* input, __fp16* output, size_t outer_size, size_t axis_size, size_t inner_size) { - axis_reduce_f16_impl(input, output, outer_size, axis_size, inner_size, - vdupq_n_f16(static_cast<__fp16>(65504.0f)), static_cast<__fp16>(65504.0f), - [](float16x8_t a, float16x8_t b) { return vminq_f16(a, b); }, - [](__fp16 a, __fp16 b) { return std::min(a, b); }); -} - -__fp16 cactus_max_all_f16(const __fp16* data, size_t num_elements) { - return CactusThreading::parallel_reduce( - num_elements, CactusThreading::Thresholds::ALL_REDUCE, - [&](size_t start, size_t end) -> __fp16 { - const size_t vec_end = start + simd_align(end - start); - float16x8_t acc = vdupq_n_f16(static_cast<__fp16>(-65504.0f)); - - for (size_t i = start; i < vec_end; i += SIMD_F16_WIDTH) { - acc = vmaxq_f16(acc, vld1q_f16(&data[i])); - } - - __fp16 result = static_cast<__fp16>(-65504.0f); - __fp16 arr[8]; - vst1q_f16(arr, acc); - for (int j = 0; j < 8; j++) result = std::max(result, arr[j]); - for (size_t i = vec_end; i < end; ++i) result = std::max(result, data[i]); - return result; - }, - static_cast<__fp16>(-65504.0f), - [](__fp16 a, __fp16 b) { return std::max(a, b); } - ); -} - -void cactus_max_axis_f16(const __fp16* input, __fp16* output, size_t outer_size, size_t axis_size, size_t inner_size) { - axis_reduce_f16_impl(input, output, outer_size, axis_size, inner_size, - vdupq_n_f16(static_cast<__fp16>(-65504.0f)), static_cast<__fp16>(-65504.0f), - [](float16x8_t a, float16x8_t b) { return vmaxq_f16(a, b); }, - [](__fp16 a, __fp16 b) { return std::max(a, b); }); -} diff --git a/cactus/kernel/kernel_scalar.cpp b/cactus/kernel/kernel_scalar.cpp deleted file mode 100644 index a35fea26e..000000000 --- a/cactus/kernel/kernel_scalar.cpp +++ /dev/null @@ -1,98 +0,0 @@ -#include "kernel.h" -#include "kernel_utils.h" -#include -#include - -void cactus_scalar_op_f16(const __fp16* input, __fp16* output, size_t num_elements, float scalar_value, ScalarOpType op_type) { - const __fp16 scalar_f16 = static_cast<__fp16>(scalar_value); - const bool use_streaming = num_elements >= STREAMING_STORE_THRESHOLD; - - switch (op_type) { - case ScalarOpType::ADD: { - const float16x8_t sv = vdupq_n_f16(scalar_f16); - elementwise_op_f16(input, output, num_elements, use_streaming, - CactusThreading::Thresholds::SCALAR_BASIC, - [sv](float16x8_t v) { return vaddq_f16(v, sv); }, - [scalar_f16](__fp16 v) { return static_cast<__fp16>(v + scalar_f16); }); - break; - } - case ScalarOpType::SUBTRACT: { - const float16x8_t sv = vdupq_n_f16(scalar_f16); - elementwise_op_f16(input, output, num_elements, use_streaming, - CactusThreading::Thresholds::SCALAR_BASIC, - [sv](float16x8_t v) { return vsubq_f16(v, sv); }, - [scalar_f16](__fp16 v) { return static_cast<__fp16>(v - scalar_f16); }); - break; - } - case ScalarOpType::MULTIPLY: { - const float16x8_t sv = vdupq_n_f16(scalar_f16); - elementwise_op_f16(input, output, num_elements, use_streaming, - CactusThreading::Thresholds::SCALAR_BASIC, - [sv](float16x8_t v) { return vmulq_f16(v, sv); }, - [scalar_f16](__fp16 v) { return static_cast<__fp16>(v * scalar_f16); }); - break; - } - case ScalarOpType::DIVIDE: { - const float16x8_t sv = vdupq_n_f16(scalar_f16); - elementwise_op_f16(input, output, num_elements, use_streaming, - CactusThreading::Thresholds::SCALAR_BASIC, - [sv](float16x8_t v) { return vdivq_f16(v, sv); }, - [scalar_f16](__fp16 v) { return static_cast<__fp16>(v / scalar_f16); }); - break; - } - case ScalarOpType::ABS: - elementwise_op_f16(input, output, num_elements, use_streaming, - CactusThreading::Thresholds::SCALAR_BASIC, - [](float16x8_t v) { return vabsq_f16(v); }, - [](__fp16 v) { return static_cast<__fp16>(std::abs(static_cast(v))); }); - break; - - case ScalarOpType::EXP: - elementwise_op_f16(input, output, num_elements, use_streaming, - CactusThreading::Thresholds::SCALAR_EXPENSIVE, - [](float16x8_t v) { return apply_f32_op_on_f16x8(v, fast_exp_f32x4); }, - [](__fp16 v) { return static_cast<__fp16>(std::exp(static_cast(v))); }, 2); - break; - - case ScalarOpType::SQRT: - elementwise_op_f16(input, output, num_elements, use_streaming, - CactusThreading::Thresholds::SCALAR_EXPENSIVE, - [](float16x8_t v) { return apply_f32_op_on_f16x8(v, vsqrtq_f32); }, - [](__fp16 v) { return static_cast<__fp16>(std::sqrt(static_cast(v))); }, 2); - break; - - case ScalarOpType::POW: - CactusThreading::parallel_for(num_elements, - CactusThreading::Thresholds::SCALAR_EXPENSIVE, - [&](size_t start, size_t end) { - for (size_t i = start; i < end; ++i) { - output[i] = static_cast<__fp16>( - std::pow(static_cast(input[i]), static_cast(scalar_f16))); - } - }); - break; - - case ScalarOpType::COS: - case ScalarOpType::SIN: - CactusThreading::parallel_for(num_elements, - CactusThreading::Thresholds::SCALAR_EXPENSIVE, - [&](size_t start, size_t end) { - for (size_t i = start; i < end; ++i) { - float val = static_cast(input[i]); - float result = (op_type == ScalarOpType::COS) ? std::cos(val) : std::sin(val); - output[i] = static_cast<__fp16>(result); - } - }); - break; - - case ScalarOpType::LOG: - CactusThreading::parallel_for(num_elements, - CactusThreading::Thresholds::SCALAR_EXPENSIVE, - [&](size_t start, size_t end) { - for (size_t i = start; i < end; ++i) { - output[i] = static_cast<__fp16>(std::log(static_cast(input[i]))); - } - }); - break; - } -} diff --git a/cactus/kernel/kernel_utils.h b/cactus/kernel/kernel_utils.h deleted file mode 100644 index b20215818..000000000 --- a/cactus/kernel/kernel_utils.h +++ /dev/null @@ -1,681 +0,0 @@ -#ifndef KERNEL_UTILS_H -#define KERNEL_UTILS_H - -#include -#if defined(__APPLE__) -#include -#include -#endif -#if defined(__ANDROID__) -#include -#include -#include -#include -#endif -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -constexpr size_t NEON_VECTOR_SIZE = 16; -constexpr size_t STREAMING_STORE_THRESHOLD = 32768; - -inline void stream_store_f16x8(__fp16* dst, float16x8_t val) { -#if defined(__aarch64__) - float16x4_t lo = vget_low_f16(val); - float16x4_t hi = vget_high_f16(val); - __asm__ __volatile__( - "stnp %d0, %d1, [%2]" - : - : "w"(lo), "w"(hi), "r"(dst) - : "memory" - ); -#else - vst1q_f16(dst, val); -#endif -} - -inline bool cpu_has_i8mm() { -#if defined(__aarch64__) - static std::once_flag once; - static bool has = false; - - std::call_once(once, []() { -#if defined(__APPLE__) - int ret = 0; - size_t size = sizeof(ret); - if (sysctlbyname("hw.optional.arm.FEAT_I8MM", &ret, &size, nullptr, 0) == 0) { - has = (ret == 1); - } -#elif defined(__ANDROID__) - unsigned long hwcap2 = getauxval(AT_HWCAP2); - #ifndef HWCAP2_I8MM - #define HWCAP2_I8MM (1 << 13) - #endif - has = (hwcap2 & HWCAP2_I8MM) != 0; -#endif - }); - - return has; -#else - return false; -#endif -} - -inline float32x4_t fast_exp_f32x4(float32x4_t x) { - const float32x4_t log2e = vdupq_n_f32(1.4426950408889634f); - const float32x4_t ln2 = vdupq_n_f32(0.6931471805599453f); - - const float32x4_t c0 = vdupq_n_f32(1.0f); - const float32x4_t c1 = vdupq_n_f32(0.6931471805599453f); - const float32x4_t c2 = vdupq_n_f32(0.2402265069591007f); - const float32x4_t c3 = vdupq_n_f32(0.05550410866482158f); - const float32x4_t c4 = vdupq_n_f32(0.009618129842071803f); - - x = vmaxq_f32(x, vdupq_n_f32(-87.0f)); - x = vminq_f32(x, vdupq_n_f32(87.0f)); - - float32x4_t z = vmulq_f32(x, log2e); - - int32x4_t zi = vcvtq_s32_f32(z); - float32x4_t zf = vsubq_f32(z, vcvtq_f32_s32(zi)); - - uint32x4_t neg_mask = vcltq_f32(zf, vdupq_n_f32(0.0f)); - zi = vsubq_s32(zi, vandq_s32(vreinterpretq_s32_u32(neg_mask), vdupq_n_s32(1))); - zf = vaddq_f32(zf, vreinterpretq_f32_u32(vandq_u32(neg_mask, vreinterpretq_u32_f32(vdupq_n_f32(1.0f))))); - - float32x4_t zf_ln2 = vmulq_f32(zf, ln2); - float32x4_t p = c4; - p = vfmaq_f32(c3, p, zf_ln2); - p = vfmaq_f32(c2, p, zf_ln2); - p = vfmaq_f32(c1, p, zf_ln2); - p = vfmaq_f32(c0, p, zf_ln2); - - int32x4_t exp_bits = vshlq_n_s32(vaddq_s32(zi, vdupq_n_s32(127)), 23); - float32x4_t scale = vreinterpretq_f32_s32(exp_bits); - - return vmulq_f32(p, scale); -} - -// Cephes-style 13/6 rational tanh approximation (same coefficients as Eigen). -// Constants are stored as static splatted arrays so the compiler emits a single -// pc-relative `ldr q` per load. -alignas(16) inline constexpr float kFastTanhAlpha[7][4] = { - { 4.89352455891786e-03f, 4.89352455891786e-03f, 4.89352455891786e-03f, 4.89352455891786e-03f }, - { 6.37261928875436e-04f, 6.37261928875436e-04f, 6.37261928875436e-04f, 6.37261928875436e-04f }, - { 1.48572235717979e-05f, 1.48572235717979e-05f, 1.48572235717979e-05f, 1.48572235717979e-05f }, - { 5.12229709037114e-08f, 5.12229709037114e-08f, 5.12229709037114e-08f, 5.12229709037114e-08f }, - {-8.60467152213735e-11f,-8.60467152213735e-11f,-8.60467152213735e-11f,-8.60467152213735e-11f }, - { 2.00018790482477e-13f, 2.00018790482477e-13f, 2.00018790482477e-13f, 2.00018790482477e-13f }, - {-2.76076847742355e-16f,-2.76076847742355e-16f,-2.76076847742355e-16f,-2.76076847742355e-16f }, -}; -alignas(16) inline constexpr float kFastTanhBeta[4][4] = { - { 4.89352518554385e-03f, 4.89352518554385e-03f, 4.89352518554385e-03f, 4.89352518554385e-03f }, - { 2.26843463243900e-03f, 2.26843463243900e-03f, 2.26843463243900e-03f, 2.26843463243900e-03f }, - { 1.18534705686654e-04f, 1.18534705686654e-04f, 1.18534705686654e-04f, 1.18534705686654e-04f }, - { 1.19825839466702e-06f, 1.19825839466702e-06f, 1.19825839466702e-06f, 1.19825839466702e-06f }, -}; -alignas(16) inline constexpr float kFastTanhClampHi[4] = { 9.0f, 9.0f, 9.0f, 9.0f }; -alignas(16) inline constexpr float kFastTanhClampLo[4] = {-9.0f,-9.0f,-9.0f,-9.0f }; - -inline float32x4_t fast_tanh_f32x4(float32x4_t x) { - x = vmaxq_f32(vld1q_f32(kFastTanhClampLo), vminq_f32(vld1q_f32(kFastTanhClampHi), x)); - float32x4_t x2 = vmulq_f32(x, x); - float32x4_t p = vfmaq_f32(vld1q_f32(kFastTanhAlpha[5]), vld1q_f32(kFastTanhAlpha[6]), x2); - p = vfmaq_f32(vld1q_f32(kFastTanhAlpha[4]), p, x2); - p = vfmaq_f32(vld1q_f32(kFastTanhAlpha[3]), p, x2); - p = vfmaq_f32(vld1q_f32(kFastTanhAlpha[2]), p, x2); - p = vfmaq_f32(vld1q_f32(kFastTanhAlpha[1]), p, x2); - p = vfmaq_f32(vld1q_f32(kFastTanhAlpha[0]), p, x2); - p = vmulq_f32(p, x); - float32x4_t q = vfmaq_f32(vld1q_f32(kFastTanhBeta[2]), vld1q_f32(kFastTanhBeta[3]), x2); - q = vfmaq_f32(vld1q_f32(kFastTanhBeta[1]), q, x2); - q = vfmaq_f32(vld1q_f32(kFastTanhBeta[0]), q, x2); - return vdivq_f32(p, q); -} - -constexpr size_t SIMD_F16_WIDTH = 8; - -inline size_t simd_align(size_t count, size_t width = SIMD_F16_WIDTH) { - return (count / width) * width; -} - -inline void f16x8_split_f32(float16x8_t v, float32x4_t& lo, float32x4_t& hi) { - lo = vcvt_f32_f16(vget_low_f16(v)); - hi = vcvt_f32_f16(vget_high_f16(v)); -} - -inline float16x8_t f32_merge_f16(float32x4_t lo, float32x4_t hi) { - return vcombine_f16(vcvt_f16_f32(lo), vcvt_f16_f32(hi)); -} - -inline float32x4_t fast_sigmoid_f32x4(float32x4_t x) { - const float32x4_t one = vdupq_n_f32(1.0f); - return vdivq_f32(one, vaddq_f32(one, fast_exp_f32x4(vnegq_f32(x)))); -} - -template -inline float16x8_t apply_f32_op_on_f16x8(float16x8_t v, F32x4Op op) { - float32x4_t lo, hi; - f16x8_split_f32(v, lo, hi); - return f32_merge_f16(op(lo), op(hi)); -} - -inline void unpack_int4_as_int8x16x2(const uint8_t* ptr, int8x16_t& high_decoded, int8x16_t& low_decoded) { - int8x16_t packed = vreinterpretq_s8_u8(vld1q_u8(ptr)); - high_decoded = vshrq_n_s8(packed, 4); - low_decoded = vshrq_n_s8(vshlq_n_s8(packed, 4), 4); -} - -namespace CactusThreading { - -#if defined(__ANDROID__) - struct CoreTopology { - std::vector performance_cores; - std::vector all_cores; - - static CoreTopology& get() { - static CoreTopology topo = detect(); - return topo; - } - - private: - static int read_sysfs_int(const char* path) { - std::ifstream f(path); - if (!f.is_open()) return -1; - int val = -1; - f >> val; - return val; - } - - static CoreTopology detect() { - CoreTopology topo; - constexpr int MAX_CPUS = 16; - std::vector> core_caps; - - for (int i = 0; i < MAX_CPUS; ++i) { - char path[128]; - - snprintf(path, sizeof(path), - "/sys/devices/system/cpu/cpu%d/cpu_capacity", i); - int cap = read_sysfs_int(path); - if (cap > 0) { - core_caps.push_back({i, cap}); - topo.all_cores.push_back(i); - continue; - } - - snprintf(path, sizeof(path), - "/sys/devices/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", i); - int freq = read_sysfs_int(path); - if (freq > 0) { - core_caps.push_back({i, freq}); - topo.all_cores.push_back(i); - } - } - - if (core_caps.empty()) return topo; - - int max_cap = 0; - for (auto& [id, cap] : core_caps) { - max_cap = std::max(max_cap, cap); - } - - int threshold = static_cast(max_cap * 0.70); - for (auto& [id, cap] : core_caps) { - if (cap >= threshold) { - topo.performance_cores.push_back(id); - } - } - - return topo; - } - }; - - inline bool pin_current_thread_to_cores(const std::vector& cores) { - if (cores.empty()) return false; - cpu_set_t mask; - CPU_ZERO(&mask); - for (int core : cores) { - CPU_SET(core, &mask); - } - return sched_setaffinity(0, sizeof(mask), &mask) == 0; - } -#endif - - class ThreadPool { - private: - static constexpr size_t MAX_WORKERS = 16; - - std::vector workers; - std::deque> tasks; - - std::mutex mutex; - std::condition_variable work_available; - std::condition_variable work_done; - - bool stop{false}; - std::atomic pending_tasks{0}; - size_t num_workers_; - - void worker_thread() { - while (true) { - std::function task; - { - std::unique_lock lock(mutex); - work_available.wait(lock, [this] { - return stop || !tasks.empty(); - }); - - if (stop && tasks.empty()) { - return; - } - - task = std::move(tasks.front()); - tasks.pop_front(); - } - - task(); - - if (pending_tasks.fetch_sub(1, std::memory_order_acq_rel) == 1) { - std::lock_guard lock(mutex); - work_done.notify_one(); - } - } - } - - public: - explicit ThreadPool(size_t num_threads = std::thread::hardware_concurrency()) - : stop(false), pending_tasks(0) { - num_workers_ = std::min(num_threads, MAX_WORKERS); - if (num_workers_ == 0) num_workers_ = 1; - -#if defined(__ANDROID__) - auto& topo = CoreTopology::get(); - if (!topo.performance_cores.empty()) { - num_workers_ = std::min(num_workers_, topo.performance_cores.size()); - } -#endif - - workers.reserve(num_workers_); - for (size_t i = 0; i < num_workers_; ++i) { - workers.emplace_back([this]() { -#if defined(__ANDROID__) - auto& perf = CoreTopology::get().performance_cores; - if (!perf.empty()) { - pin_current_thread_to_cores(perf); - } -#endif - worker_thread(); - }); - } - } - - ~ThreadPool() { - { - std::lock_guard lock(mutex); - stop = true; - } - work_available.notify_all(); - for (auto& worker : workers) { - if (worker.joinable()) { - worker.join(); - } - } - } - - template - auto enqueue(F&& f) -> std::future { - using return_type = decltype(f()); - - auto task = std::make_shared>( - std::forward(f) - ); - - std::future res = task->get_future(); - - { - std::lock_guard lock(mutex); - pending_tasks.fetch_add(1, std::memory_order_relaxed); - tasks.emplace_back([task](){ (*task)(); }); - } - work_available.notify_one(); - - return res; - } - - template - void enqueue_batch(size_t total_work, F task_func) { - if (total_work == 0) return; - - const size_t num_tasks = std::min(num_workers_, total_work); - const size_t per_worker = total_work / num_tasks; - const size_t remainder = total_work % num_tasks; - - { - std::lock_guard lock(mutex); - pending_tasks.fetch_add(num_tasks, std::memory_order_relaxed); - - for (size_t w = 0; w < num_tasks; ++w) { - size_t start = w * per_worker + std::min(w, remainder); - size_t end = start + per_worker + (w < remainder ? 1 : 0); - tasks.emplace_back([=]() { task_func(start, end); }); - } - } - work_available.notify_all(); - } - - void wait_all() { - std::unique_lock lock(mutex); - work_done.wait(lock, [this] { - return pending_tasks.load(std::memory_order_acquire) == 0; - }); - } - - template - void enqueue_n_threads(size_t total_work, size_t num_threads, F task_func) { - if (total_work == 0 || num_threads == 0) return; - - num_threads = std::min(num_threads, std::min(num_workers_, total_work)); - const size_t per_thread = total_work / num_threads; - const size_t remainder = total_work % num_threads; - - { - std::lock_guard lock(mutex); - pending_tasks.fetch_add(num_threads, std::memory_order_relaxed); - - for (size_t t = 0; t < num_threads; ++t) { - size_t start = t * per_thread + std::min(t, remainder); - size_t end = start + per_thread + (t < remainder ? 1 : 0); - tasks.emplace_back([=]() { task_func(start, end); }); - } - } - work_available.notify_all(); - } - - size_t num_workers() const { return num_workers_; } - }; - - inline ThreadPool& get_thread_pool() { - static ThreadPool pool; - return pool; - } - - struct ParallelConfig { - size_t min_work_gate; - size_t work_per_thread; - - constexpr ParallelConfig(size_t gate, size_t per_thread) - : min_work_gate(gate), work_per_thread(per_thread) {} - }; - - inline size_t get_optimal_thread_count(size_t total_work, ParallelConfig config) { - if (total_work < config.min_work_gate) return 1; - - size_t pool_size = get_thread_pool().num_workers(); - size_t num_threads = (total_work + config.work_per_thread - 1) / config.work_per_thread; - return std::min(pool_size, std::max(static_cast(1), num_threads)); - } - - struct Thresholds { - #if defined(__ANDROID__) - static constexpr ParallelConfig ATTENTION{64, 32}; - static constexpr ParallelConfig ELEMENT_WISE{5000, 2500}; - static constexpr ParallelConfig AXIS_REDUCE{1000, 500}; - static constexpr ParallelConfig ALL_REDUCE{10000, 5000}; - static constexpr ParallelConfig SCALAR_BASIC{30000, 15000}; - static constexpr ParallelConfig SCALAR_EXPENSIVE{10000, 5000}; - #else // Apple - static constexpr ParallelConfig ATTENTION{32, 16}; - static constexpr ParallelConfig ELEMENT_WISE{5000, 2500}; - static constexpr ParallelConfig AXIS_REDUCE{1000, 500}; - static constexpr ParallelConfig ALL_REDUCE{10000, 5000}; - static constexpr ParallelConfig SCALAR_BASIC{5000, 2500}; - static constexpr ParallelConfig SCALAR_EXPENSIVE{2500, 1250}; - #endif - }; - - struct GemmThreading { - #if defined(__ANDROID__) - static size_t get_num_threads(size_t M, size_t pool_size) { - if (M <= 1) return 1; - return pool_size; - } - static size_t get_gemv_threads(size_t /*N_blocks*/, size_t /*pool_size*/) { - return 1; - } - #elif defined(__APPLE__) && TARGET_OS_IPHONE - static constexpr size_t GEMV_MIN_N_BLOCKS = 512; - static size_t get_num_threads(size_t M, size_t pool_size) { - if (M <= 1) return std::min(pool_size, static_cast(2)); - return pool_size; - } - static size_t get_gemv_threads(size_t N_blocks, size_t pool_size) { - if (N_blocks < GEMV_MIN_N_BLOCKS) return 1; - return std::min(pool_size, static_cast(3)); - } - #else - static constexpr size_t GEMV_MIN_N_BLOCKS = 256; - static size_t get_num_threads(size_t M, size_t pool_size) { - if (M <= 1) return std::min(pool_size, static_cast(4)); - return pool_size; - } - static size_t get_gemv_threads(size_t N_blocks, size_t pool_size) { - if (N_blocks < GEMV_MIN_N_BLOCKS) return 1; - if (N_blocks < 512) return std::min(pool_size, static_cast(2)); - return std::min(pool_size, static_cast(5)); - } - #endif - }; - - inline size_t& get_gemm_thread_override() { - static size_t override_threads = 0; - return override_threads; - } - - inline void set_gemm_threads(size_t num_threads) { - get_gemm_thread_override() = num_threads; - } - - inline void reset_gemm_threads() { - get_gemm_thread_override() = 0; - } - - class TaskHandle { - private: - std::vector> futures_; - bool auto_wait_; - - public: - TaskHandle(bool auto_wait = true) : auto_wait_(auto_wait) {} - - ~TaskHandle() { - if (auto_wait_) { - wait(); - } - } - - TaskHandle(TaskHandle&&) = default; - TaskHandle& operator=(TaskHandle&&) = default; - TaskHandle(const TaskHandle&) = delete; - TaskHandle& operator=(const TaskHandle&) = delete; - - void add_future(std::future&& f) { - futures_.push_back(std::move(f)); - } - - void wait() { - for (auto& f : futures_) { - if (f.valid()) { - f.wait(); - } - } - futures_.clear(); - } - - bool is_ready() const { - for (const auto& f : futures_) { - if (f.valid() && f.wait_for(std::chrono::seconds(0)) != std::future_status::ready) { - return false; - } - } - return true; - } - - size_t task_count() const { return futures_.size(); } - }; - - template - TaskHandle parallel_for(size_t total_work, ParallelConfig config, WorkFunc work_func, bool wait = true) { - const size_t num_threads = get_optimal_thread_count(total_work, config); - TaskHandle handle(!wait); - - if (num_threads == 1) { - if (wait) { - work_func(0, total_work); - return handle; - } - auto& pool = get_thread_pool(); - handle.add_future(pool.enqueue([work_func, total_work]() { - work_func(0, total_work); - })); - return handle; - } - - auto& pool = get_thread_pool(); - const size_t work_per_thread = total_work / num_threads; - - for (size_t t = 0; t < num_threads; ++t) { - handle.add_future(pool.enqueue([work_func, t, num_threads, work_per_thread, total_work]() { - const size_t start_idx = t * work_per_thread; - const size_t end_idx = (t == num_threads - 1) ? total_work : (t + 1) * work_per_thread; - work_func(start_idx, end_idx); - })); - } - - if (wait) { - handle.wait(); - } - return handle; - } - - template - void parallel_for_2d(size_t outer_size, size_t inner_size, ParallelConfig config, WorkFunc work_func) { - const size_t total_work = outer_size * inner_size; - parallel_for(total_work, config, [&](size_t start_idx, size_t end_idx) { - for (size_t work_idx = start_idx; work_idx < end_idx; ++work_idx) { - const size_t outer = work_idx / inner_size; - const size_t inner = work_idx % inner_size; - work_func(outer, inner); - } - }); - } - - template - ResultType parallel_reduce(size_t total_work, ParallelConfig config, - WorkFunc work_func, ResultType init_value, CombineFunc combine_func) { - const size_t num_threads = get_optimal_thread_count(total_work, config); - - if (num_threads == 1) { - return work_func(0, total_work); - } - - auto& pool = get_thread_pool(); - std::vector> futures; - std::vector partial_results(num_threads, init_value); - const size_t work_per_thread = total_work / num_threads; - - for (size_t t = 0; t < num_threads; ++t) { - futures.push_back(pool.enqueue([work_func, t, num_threads, work_per_thread, total_work]() -> ResultType { - const size_t start_idx = t * work_per_thread; - const size_t end_idx = (t == num_threads - 1) ? total_work : (t + 1) * work_per_thread; - return work_func(start_idx, end_idx); - })); - } - - ResultType result = init_value; - for (auto& future : futures) { - result = combine_func(result, future.get()); - } - return result; - } - - template - void parallel_gemm_tiles(size_t M, size_t total_tiles, WorkFunc work_func) { - auto& pool = get_thread_pool(); - - size_t override = get_gemm_thread_override(); - size_t num_threads = (override > 0) ? override : GemmThreading::get_num_threads(M, pool.num_workers()); - num_threads = std::min(num_threads, total_tiles); - - if (num_threads <= 1) { - work_func(0, total_tiles); - return; - } - - pool.enqueue_n_threads(total_tiles, num_threads, work_func); - pool.wait_all(); - } - -} - -template -void elementwise_op_f16(const __fp16* input, __fp16* output, size_t num_elements, - bool use_streaming, CactusThreading::ParallelConfig config, - SimdOp simd_op, ScalarOp scalar_op, size_t unroll = 4) { - CactusThreading::parallel_for(num_elements, config, - [&](size_t start, size_t end) { - const size_t n = end - start; - const size_t vec_end = start + simd_align(n); - - if (use_streaming && unroll >= 4) { - const size_t unrolled_end = start + simd_align(n, SIMD_F16_WIDTH * 4); - for (size_t i = start; i < unrolled_end; i += SIMD_F16_WIDTH * 4) { - __builtin_prefetch(&input[i + 256], 0, 0); - float16x8_t v0 = simd_op(vld1q_f16(&input[i])); - float16x8_t v1 = simd_op(vld1q_f16(&input[i + 8])); - float16x8_t v2 = simd_op(vld1q_f16(&input[i + 16])); - float16x8_t v3 = simd_op(vld1q_f16(&input[i + 24])); - stream_store_f16x8(&output[i], v0); - stream_store_f16x8(&output[i + 8], v1); - stream_store_f16x8(&output[i + 16], v2); - stream_store_f16x8(&output[i + 24], v3); - } - for (size_t i = unrolled_end; i < vec_end; i += SIMD_F16_WIDTH) { - stream_store_f16x8(&output[i], simd_op(vld1q_f16(&input[i]))); - } - } else if (use_streaming && unroll >= 2) { - const size_t unrolled_end = start + simd_align(n, SIMD_F16_WIDTH * 2); - for (size_t i = start; i < unrolled_end; i += SIMD_F16_WIDTH * 2) { - __builtin_prefetch(&input[i + 128], 0, 0); - float16x8_t v0 = simd_op(vld1q_f16(&input[i])); - float16x8_t v1 = simd_op(vld1q_f16(&input[i + 8])); - stream_store_f16x8(&output[i], v0); - stream_store_f16x8(&output[i + 8], v1); - } - for (size_t i = unrolled_end; i < vec_end; i += SIMD_F16_WIDTH) { - stream_store_f16x8(&output[i], simd_op(vld1q_f16(&input[i]))); - } - } else { - for (size_t i = start; i < vec_end; i += SIMD_F16_WIDTH) { - vst1q_f16(&output[i], simd_op(vld1q_f16(&input[i]))); - } - } - for (size_t i = vec_end; i < end; ++i) { - output[i] = scalar_op(input[i]); - } - }); -} - -#endif // KERNEL_UTILS_H diff --git a/cactus/models/gemma4/model_gemma4.cpp b/cactus/models/gemma4/model_gemma4.cpp deleted file mode 100644 index c99917878..000000000 --- a/cactus/models/gemma4/model_gemma4.cpp +++ /dev/null @@ -1,624 +0,0 @@ -#include "model_gemma4.h" -#include "../../graph/graph.h" -#include -#include -#include -#include - -namespace cactus { -namespace engine { - -Gemma4Model::Gemma4Model() : Model() {} - -Gemma4Model::Gemma4Model(const Config& config) : Model(config) { - weight_nodes_.layers.resize(config.num_layers); -} - -bool Gemma4Model::is_global_layer(uint32_t idx) const { - return config_.layer_types[idx] == "global"; -} - -std::vector Gemma4Model::get_kv_layer_dims() const { - uint32_t n = config_.num_layers; - - std::vector dims(n); - for (uint32_t i = 0; i < n; i++) { - if (i >= first_shared_layer_) { - dims[i] = 0; - } else if (is_global_layer(i)) { - dims[i] = config_.global_head_dim > 0 ? config_.global_head_dim : config_.attention_head_dim * 2; - } else { - dims[i] = config_.attention_head_dim; - } - } - return dims; -} - -std::vector Gemma4Model::get_kv_layer_heads() const { - uint32_t n = config_.num_layers; - - std::vector heads(n); - for (uint32_t i = 0; i < n; i++) { - if (i >= first_shared_layer_) { - heads[i] = 0; - } else if (is_global_layer(i) && config_.num_global_kv_heads > 0) { - heads[i] = config_.num_global_kv_heads; - } else { - heads[i] = config_.attention_kv_heads; - } - } - return heads; -} - -void Gemma4Model::compact_kv_cache() { - uint32_t n = config_.num_layers; - - std::vector target_windows(n, 0); - for (uint32_t i = 0; i < n; i++) { - if (i >= first_shared_layer_) continue; - if (!is_global_layer(i)) - target_windows[i] = config_.sliding_window; - } - kv_cache_.compact_to_windows(target_windows); -} - -void Gemma4Model::post_init() { - uint32_t n = config_.num_layers; - - kv_cache_.set_window_size(0, 0); - - kv_share_map_.resize(n, -1); - shared_k_nodes_.resize(n, 0); - shared_v_nodes_.resize(n, 0); - - for (uint32_t i = first_shared_layer_; i < n; i++) { - bool is_global = is_global_layer(i); - for (int j = static_cast(first_shared_layer_) - 1; j >= 0; j--) { - if (is_global_layer(j) == is_global) { - kv_share_map_[i] = j; - break; - } - } - } -} - -void Gemma4Model::load_weights_to_graph(CactusGraph* gb) { - uint32_t n = config_.num_layers; - uint32_t num_shared = config_.num_kv_shared_layers; - first_shared_layer_ = (n > num_shared) ? n - num_shared : n; - - embedding_node_id_ = gb->mmap_embeddings(embedding_file_path_); - weight_nodes_.output_norm_weight = gb->mmap_weights(model_folder_path_ + "/output_norm.weights"); - if (config_.tie_word_embeddings) { - weight_nodes_.output_weight = embedding_node_id_; - output_weight_node_id_ = embedding_node_id_; - } else { - weight_nodes_.output_weight = gb->mmap_weights(model_folder_path_ + "/output_weight.weights"); - output_weight_node_id_ = weight_nodes_.output_weight; - } - - bool has_pli = config_.hidden_size_per_layer_input > 0; - if (has_pli) { - weight_nodes_.embed_tokens_per_layer = gb->mmap_embeddings(model_folder_path_ + "/embed_tokens_per_layer.weights"); - weight_nodes_.per_layer_model_proj = gb->mmap_weights(model_folder_path_ + "/per_layer_model_proj.weights"); - weight_nodes_.per_layer_proj_norm = gb->mmap_weights(model_folder_path_ + "/per_layer_proj_norm.weights"); - } - - bool has_moe = config_.enable_moe_block; - - for (uint32_t i = 0; i < config_.num_layers; i++) { - auto& layer = weight_nodes_.layers[i]; - std::string prefix = model_folder_path_ + "/layer_" + std::to_string(i) + "_"; - bool is_shared = (i >= first_shared_layer_); - - layer.attn_q_weight = gb->mmap_weights(prefix + "attn_q.weights"); - layer.attn_k_weight = is_shared ? 0 : gb->mmap_weights(prefix + "attn_k.weights"); - bool k_eq_v = config_.attention_k_eq_v && is_global_layer(i); - layer.attn_v_weight = is_shared ? 0 : (k_eq_v ? layer.attn_k_weight : gb->mmap_weights(prefix + "attn_v.weights")); - layer.attn_output_weight = gb->mmap_weights(prefix + "attn_output.weights"); - layer.input_layernorm_weight = gb->mmap_weights(prefix + "input_norm.weights"); - layer.attn_q_norm_weight = gb->mmap_weights(prefix + "attn_q_norm.weights"); - layer.attn_k_norm_weight = is_shared ? 0 : gb->mmap_weights(prefix + "attn_k_norm.weights"); - layer.ffn_gate_weight = gb->mmap_weights(prefix + "ffn_gate.weights"); - layer.ffn_up_weight = gb->mmap_weights(prefix + "ffn_up.weights"); - layer.ffn_down_weight = gb->mmap_weights(prefix + "ffn_down.weights"); - layer.post_attention_layernorm_weight = gb->mmap_weights(prefix + "post_attn_norm.weights"); - layer.pre_feedforward_layernorm_weight = gb->mmap_weights(prefix + "pre_ffn_norm.weights"); - layer.post_feedforward_layernorm_weight = gb->mmap_weights(prefix + "post_ffn_norm.weights"); - if (has_pli) { - layer.per_layer_gate = gb->mmap_weights(prefix + "per_layer_gate.weights"); - layer.per_layer_proj = gb->mmap_weights(prefix + "per_layer_proj.weights"); - layer.post_per_layer_norm = gb->mmap_weights(prefix + "post_per_layer_norm.weights"); - } - layer.layer_scalar = std::filesystem::exists(prefix + "layer_scalar.weights") ? gb->mmap_weights(prefix + "layer_scalar.weights") : 0; - - if (has_moe) { - uint32_t num_experts = config_.num_experts; - uint32_t hidden = config_.hidden_dim; - uint32_t expert_dim = config_.expert_intermediate_size > 0 ? config_.expert_intermediate_size : config_.ffn_intermediate_dim; - layer.moe_w1_experts.resize(num_experts); - layer.moe_w3_experts.resize(num_experts); - layer.moe_w2_experts.resize(num_experts); - - auto w1_packed = gb->mmap_weights(prefix + "moe_gate_proj.weights"); - auto w3_packed = gb->mmap_weights(prefix + "moe_up_proj.weights"); - auto w2_packed = gb->mmap_weights(prefix + "moe_down_proj.weights"); - - const auto& w1_buf = gb->get_output_buffer(w1_packed); - const auto& w3_buf = gb->get_output_buffer(w3_packed); - const auto& w2_buf = gb->get_output_buffer(w2_packed); - - auto setup_experts = [&](const BufferDesc& buf, - std::vector& expert_nodes, - size_t out_dim, size_t in_dim) { - auto* base = static_cast(const_cast(buf.get_data())); - Precision prec = buf.precision; - bool is_quantized = PrecisionTraits::is_integer(prec) && buf.group_size > 0; - size_t K = in_dim; - if (is_quantized && K % buf.group_size != 0) - K = ((K + buf.group_size - 1) / buf.group_size) * buf.group_size; - size_t expert_data_bytes = PrecisionTraits::packed_size_of(prec, out_dim * K); - size_t groups_per_expert = is_quantized ? (out_dim * K + buf.group_size - 1) / buf.group_size : 0; - size_t expert_scales_bytes = groups_per_expert * sizeof(__fp16); - - char* scales_base = is_quantized ? static_cast(const_cast(buf.scales_data)) : nullptr; - - for (uint32_t e = 0; e < num_experts; e++) { - expert_nodes[e] = gb->input({out_dim, K}, prec); - gb->set_external_input(expert_nodes[e], base + e * expert_data_bytes, prec); - if (is_quantized) { - gb->set_grouped_scales(expert_nodes[e], buf.group_size, groups_per_expert, - scales_base + e * expert_scales_bytes); - if (buf.is_interleaved) - gb->set_interleaved(expert_nodes[e], true, out_dim); - } - } - }; - - setup_experts(w1_buf, layer.moe_w1_experts, expert_dim, hidden); - setup_experts(w3_buf, layer.moe_w3_experts, expert_dim, hidden); - setup_experts(w2_buf, layer.moe_w2_experts, hidden, expert_dim); - - layer.router_proj = gb->mmap_weights(prefix + "router_proj.weights"); - layer.router_scale = gb->mmap_weights(prefix + "router_scale.weights"); - layer.moe_per_expert_scale = gb->mmap_weights(prefix + "moe_per_expert_scale.weights"); - layer.post_ffn_norm_1 = gb->mmap_weights(prefix + "post_ffn_norm_1.weights"); - layer.pre_ffn_norm_2 = gb->mmap_weights(prefix + "pre_ffn_norm_2.weights"); - layer.post_ffn_norm_2 = gb->mmap_weights(prefix + "post_ffn_norm_2.weights"); - } - } - - size_t sliding_head_dim = config_.attention_head_dim; - size_t global_head_dim = config_.global_head_dim > 0 ? config_.global_head_dim : config_.attention_head_dim * 2; - v_norm_ones_weight_.assign(std::max(sliding_head_dim, global_head_dim), static_cast<__fp16>(1.0f)); - v_norm_ones_node_ = gb->input({sliding_head_dim}, Precision::FP16); - gb->set_external_input(v_norm_ones_node_, v_norm_ones_weight_.data(), Precision::FP16); - v_norm_ones_global_node_ = gb->input({global_head_dim}, Precision::FP16); - gb->set_external_input(v_norm_ones_global_node_, v_norm_ones_weight_.data(), Precision::FP16); - - if (npu::is_npu_available()) { - std::string npu_prefill_path = model_folder_path_ + "/model.mlpackage"; - if (std::filesystem::exists(npu_prefill_path)) { - if (!load_npu_prefill(npu_prefill_path) || !has_npu_prefill()) { - CACTUS_LOG_WARN("npu", "[gemma4] found model.mlpackage but failed to enable NPU prefill; using CPU prefill"); - } - } else { - CACTUS_LOG_WARN("npu", "[gemma4] model.mlpackage not found; using CPU prefill"); - } - } else { - CACTUS_LOG_WARN("npu", "[gemma4] NPU backend unavailable on this device; using CPU prefill"); - } -} - -size_t Gemma4Model::build_per_layer_input(CactusGraph* gb, size_t hidden, size_t pli_combined, uint32_t layer_idx, - ComputeBackend backend) const { - const auto& layer = weight_nodes_.layers[layer_idx]; - uint32_t pli_dim = config_.hidden_size_per_layer_input; - - auto gate = gb->gelu(gb->matmul(hidden, layer.per_layer_gate, true, backend)); - auto pli_slice = gb->slice(pli_combined, 1, layer_idx * pli_dim, pli_dim); - auto gated = gb->multiply(gate, pli_slice); - auto pli_proj = gb->matmul(gated, layer.per_layer_proj, true, backend); - auto pli_normed = gb->rms_norm(pli_proj, layer.post_per_layer_norm, config_.layer_norm_eps); - - return gb->add(hidden, pli_normed); -} - -size_t Gemma4Model::apply_partial_rope(CactusGraph* gb, size_t tensor, size_t head_dim, size_t rot_dim, - float rope_freq, size_t position_offset) { - if (rot_dim < head_dim) { - size_t half_dim = head_dim / 2; - size_t half_rot = rot_dim / 2; - size_t pass_len = half_dim - half_rot; - float adjusted_theta = std::pow(rope_freq, static_cast(rot_dim) / static_cast(head_dim)); - - auto left_rot = gb->slice(tensor, 3, 0, half_rot); - auto left_pass = gb->slice(tensor, 3, half_rot, pass_len); - auto right_rot = gb->slice(tensor, 3, half_dim, half_rot); - auto right_pass = gb->slice(tensor, 3, half_dim + half_rot, pass_len); - - auto rotated = gb->rope(gb->concat(left_rot, right_rot, 3), adjusted_theta, position_offset); - auto rotated_left = gb->slice(rotated, 3, 0, half_rot); - auto rotated_right = gb->slice(rotated, 3, half_rot, half_rot); - - auto new_left = gb->concat(rotated_left, left_pass, 3); - auto new_right = gb->concat(rotated_right, right_pass, 3); - return gb->concat(new_left, new_right, 3); - } - return gb->rope(tensor, rope_freq, position_offset); -} - -size_t Gemma4Model::build_attention(CactusGraph* gb, size_t input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache, size_t position_offset) { - const auto& layer = weight_nodes_.layers[layer_idx]; - size_t seq_len = gb->get_output_buffer(input).shape[0]; - int share_src = (layer_idx < kv_share_map_.size()) ? kv_share_map_[layer_idx] : -1; - - bool is_global = is_global_layer(layer_idx); - - size_t head_dim = is_global ? (config_.global_head_dim > 0 ? config_.global_head_dim : config_.attention_head_dim * 2) : config_.attention_head_dim; - size_t num_heads = config_.attention_heads; - size_t kv_heads = is_global && config_.num_global_kv_heads > 0 ? config_.num_global_kv_heads : config_.attention_kv_heads; - float rope_freq = is_global ? config_.rope_theta : config_.rope_local_base_freq; - size_t window = is_global ? 0 : config_.sliding_window; - size_t rot_dim = static_cast(head_dim * (is_global ? config_.global_partial_rotary_factor : 1.0f)); - - auto q = gb->matmul(input, layer.attn_q_weight, true, backend); - q = gb->reshape(q, {seq_len * num_heads, head_dim}); - q = gb->rms_norm(q, layer.attn_q_norm_weight, config_.layer_norm_eps); - q = gb->reshape(q, {1, seq_len, num_heads, head_dim}); - - size_t q4 = apply_partial_rope(gb, q, head_dim, rot_dim, rope_freq, position_offset); - - size_t k4, v4; - if (share_src >= 0 && shared_k_nodes_[share_src] != 0) { - k4 = shared_k_nodes_[share_src]; - v4 = shared_v_nodes_[share_src]; - } else { - auto k = gb->matmul(input, layer.attn_k_weight, true, backend); - k = gb->reshape(k, {seq_len * kv_heads, head_dim}); - k = gb->rms_norm(k, layer.attn_k_norm_weight, config_.layer_norm_eps); - k = gb->reshape(k, {1, seq_len, kv_heads, head_dim}); - - k4 = apply_partial_rope(gb, k, head_dim, rot_dim, rope_freq, position_offset); - - auto v_proj = gb->matmul(input, layer.attn_v_weight, true, backend); - size_t v_ones = is_global ? v_norm_ones_global_node_ : v_norm_ones_node_; - auto v = gb->rms_norm(gb->reshape(v_proj, {seq_len * kv_heads, head_dim}), v_ones, config_.layer_norm_eps); - v4 = gb->reshape(v, {1, seq_len, kv_heads, head_dim}); - - shared_k_nodes_[layer_idx] = k4; - shared_v_nodes_[layer_idx] = v4; - } - - if (use_cache && share_src < 0) { - cache_k_output_nodes_[layer_idx] = k4; - cache_v_output_nodes_[layer_idx] = v4; - } - - size_t cache_src = (share_src >= 0) ? static_cast(share_src) : layer_idx; - size_t attn; - if (use_cache && !kv_cache_.is_empty()) { - attn = gb->attention_int8_hybrid(q4, k4, v4, attention_scale_, position_offset, - kv_cache_.get_keys_int8(cache_src), kv_cache_.get_values_int8(cache_src), - kv_cache_.get_key_scales(cache_src), kv_cache_.get_value_scales(cache_src), - kv_cache_.current_seq_len, kv_heads, head_dim, window); - } else { - attn = gb->attention(q4, k4, v4, attention_scale_, position_offset, window); - } - - return gb->matmul(gb->reshape(attn, {seq_len, num_heads * head_dim}), layer.attn_output_weight, true, backend); -} - -size_t Gemma4Model::build_mlp(CactusGraph* gb, size_t input, uint32_t layer_idx, - ComputeBackend backend) const { - const auto& layer = weight_nodes_.layers[layer_idx]; - static const bool dense_mlp_fused_disabled = []() { - const char* env = std::getenv("CACTUS_DISABLE_DENSE_MLP_FUSED"); - return env && env[0] && env[0] != '0'; - }(); - if (!dense_mlp_fused_disabled) { - const auto& gate_buf = gb->get_output_buffer(layer.ffn_gate_weight); - const auto& up_buf = gb->get_output_buffer(layer.ffn_up_weight); - const auto& down_buf = gb->get_output_buffer(layer.ffn_down_weight); - if (gate_buf.precision == Precision::INT4 && gate_buf.group_size > 0 && - up_buf.precision == Precision::INT4 && up_buf.group_size > 0 && - down_buf.precision == Precision::INT4 && down_buf.group_size > 0) { - return gb->dense_mlp_int4_fused(input, - layer.ffn_gate_weight, - layer.ffn_up_weight, - layer.ffn_down_weight); - } - } - auto gate = gb->gelu(gb->matmul(input, layer.ffn_gate_weight, true, backend)); - auto up = gb->matmul(input, layer.ffn_up_weight, true, backend); - return gb->matmul(gb->multiply(gate, up), layer.ffn_down_weight, true, backend); -} - -size_t Gemma4Model::build_moe(CactusGraph* gb, size_t input, uint32_t layer_idx, - ComputeBackend backend) const { - const auto& layer = weight_nodes_.layers[layer_idx]; - - auto router_logits = gb->matmul(input, layer.router_proj, true, backend); - auto topk_result = gb->topk(router_logits, config_.num_experts_per_tok); - auto topk_indices = gb->index(topk_result, 0, 0); - auto routing_probs = gb->softmax(router_logits); - - return gb->moe_layer(input, routing_probs, topk_indices, - layer.moe_w1_experts, layer.moe_w3_experts, layer.moe_w2_experts, - config_.num_experts, config_.num_experts_per_tok, - true, 1e-6f, 1.0f, Activation::GELU, - layer.moe_per_expert_scale); -} - -size_t Gemma4Model::build_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache, size_t position_offset) { - const auto& layer = weight_nodes_.layers[layer_idx]; - - auto normed = gb->rms_norm(hidden, layer.input_layernorm_weight, config_.layer_norm_eps); - auto attn_raw = build_attention(gb, normed, layer_idx, backend, use_cache, position_offset); - auto attn = gb->rms_norm(attn_raw, layer.post_attention_layernorm_weight, config_.layer_norm_eps); - auto residual = gb->add(hidden, attn); - - if (config_.enable_moe_block) { - auto h1 = gb->rms_norm(residual, layer.pre_feedforward_layernorm_weight, config_.layer_norm_eps); - h1 = build_mlp(gb, h1, layer_idx, backend); - h1 = gb->rms_norm(h1, layer.post_ffn_norm_1, config_.layer_norm_eps); - - auto h2 = gb->multiply(residual, layer.router_scale); - h2 = gb->rms_norm(h2, layer.pre_ffn_norm_2, config_.layer_norm_eps); - h2 = build_moe(gb, h2, layer_idx, backend); - h2 = gb->rms_norm(h2, layer.post_ffn_norm_2, config_.layer_norm_eps); - - auto combined = gb->rms_norm(gb->add(h1, h2), layer.post_feedforward_layernorm_weight, config_.layer_norm_eps); - return gb->add(residual, combined); - } - - auto pre_mlp = gb->rms_norm(residual, layer.pre_feedforward_layernorm_weight, config_.layer_norm_eps); - auto mlp_raw = build_mlp(gb, pre_mlp, layer_idx, backend); - auto mlp = gb->rms_norm(mlp_raw, layer.post_feedforward_layernorm_weight, config_.layer_norm_eps); - - return gb->add(residual, mlp); -} - -size_t Gemma4Model::apply_transformer_layer(CactusGraph* gb, size_t hidden, size_t pli, uint32_t layer_idx, - ComputeBackend backend, bool use_cache, size_t pos_offset) { - hidden = build_transformer_block(gb, hidden, layer_idx, backend, use_cache, pos_offset); - if (config_.hidden_size_per_layer_input > 0) - hidden = build_per_layer_input(gb, hidden, pli, layer_idx, backend); - if (weight_nodes_.layers[layer_idx].layer_scalar != 0) - hidden = gb->multiply(hidden, weight_nodes_.layers[layer_idx].layer_scalar); - return hidden; -} - -size_t Gemma4Model::build_pli_combined(CactusGraph* gb, size_t hidden, size_t pli_embed, - size_t seq_len, ComputeBackend backend) { - uint32_t num_layers = config_.num_layers; - uint32_t pli_dim = config_.hidden_size_per_layer_input; - - auto pli_proj = gb->scalar_multiply(gb->matmul(hidden, weight_nodes_.per_layer_model_proj, true, backend), - 1.0f / std::sqrt(static_cast(config_.hidden_dim))); - pli_proj = gb->reshape(pli_proj, {seq_len * num_layers, pli_dim}); - pli_proj = gb->rms_norm(pli_proj, weight_nodes_.per_layer_proj_norm, config_.layer_norm_eps); - pli_proj = gb->reshape(pli_proj, {seq_len, num_layers * pli_dim}); - return gb->scalar_multiply(gb->add(pli_proj, pli_embed), 1.0f / std::sqrt(2.0f)); -} - -std::pair Gemma4Model::build_preamble_and_embed(CactusGraph* gb, size_t seq_len, ComputeBackend backend, - size_t& token_input, size_t& pli_input) { - uint32_t pli_dim = config_.hidden_size_per_layer_input; - - token_input = gb->input({seq_len}, Precision::FP32); - auto hidden = gb->scalar_multiply(gb->embedding(embedding_node_id_, token_input), - std::sqrt(static_cast(config_.hidden_dim))); - - pli_input = gb->input({seq_len}, Precision::FP32); - auto pli_embed = gb->scalar_multiply(gb->embedding(weight_nodes_.embed_tokens_per_layer, pli_input), - std::sqrt(static_cast(pli_dim))); - auto pli_combined = build_pli_combined(gb, hidden, pli_embed, seq_len, backend); - - return {hidden, pli_combined}; -} - -void Gemma4Model::set_token_inputs(CactusGraph* gb, size_t token_input, size_t pli_input, - const std::vector& tokens) { - std::vector input_data(tokens.size()); - for (size_t i = 0; i < tokens.size(); i++) - input_data[i] = static_cast(tokens[i]); - gb->set_input(token_input, input_data.data(), Precision::FP32); - gb->set_input(pli_input, input_data.data(), Precision::FP32); -} - -size_t Gemma4Model::forward_from_embeddings(CactusGraph* gb, size_t hidden, const std::vector& pli_tokens, - size_t seq_len, ComputeBackend backend, bool use_cache) { - return forward_from_embeddings(gb, hidden, hidden, pli_tokens, seq_len, backend, use_cache); -} - -size_t Gemma4Model::build_pli_combined_from_tokens(CactusGraph* gb, size_t hidden, - const std::vector& pli_tokens, - size_t seq_len, ComputeBackend backend) { - if (config_.hidden_size_per_layer_input == 0) - return 0; - - uint32_t pli_dim = config_.hidden_size_per_layer_input; - - auto pli_input = gb->input({seq_len}, Precision::FP32); - auto pli_embed = gb->scalar_multiply(gb->embedding(weight_nodes_.embed_tokens_per_layer, pli_input), - std::sqrt(static_cast(pli_dim))); - auto pli_combined = build_pli_combined(gb, hidden, pli_embed, seq_len, backend); - - std::vector pli_data(pli_tokens.size()); - for (size_t i = 0; i < pli_tokens.size(); i++) - pli_data[i] = static_cast(pli_tokens[i]); - gb->set_input(pli_input, pli_data.data(), Precision::FP32); - - return pli_combined; -} - -size_t Gemma4Model::forward_from_embeddings(CactusGraph* gb, size_t hidden, size_t pli_hidden_source, - const std::vector& pli_tokens, size_t seq_len, - ComputeBackend backend, bool use_cache) { - size_t pos_offset = use_cache ? kv_cache_.get_total_seq_len() : 0; - - std::fill(shared_k_nodes_.begin(), shared_k_nodes_.end(), 0); - std::fill(shared_v_nodes_.begin(), shared_v_nodes_.end(), 0); - - if (config_.hidden_size_per_layer_input == 0) { - for (uint32_t i = 0; i < config_.num_layers; i++) - hidden = apply_transformer_layer(gb, hidden, 0, i, backend, use_cache, pos_offset); - return gb->rms_norm(hidden, weight_nodes_.output_norm_weight, config_.layer_norm_eps); - } - - auto pli_combined = build_pli_combined_from_tokens(gb, pli_hidden_source, pli_tokens, seq_len, backend); - - for (uint32_t i = 0; i < config_.num_layers; i++) - hidden = apply_transformer_layer(gb, hidden, pli_combined, i, backend, use_cache, pos_offset); - - return gb->rms_norm(hidden, weight_nodes_.output_norm_weight, config_.layer_norm_eps); -} - -size_t Gemma4Model::forward(const std::vector& tokens, bool use_cache) { - if (!initialized_ || !graph_handle_) - throw std::runtime_error("Model not initialized - call init() first"); - if (tokens.empty()) - throw std::runtime_error("Token sequence cannot be empty"); - - auto* gb = static_cast(graph_handle_); - gb->soft_reset(); - - std::fill(shared_k_nodes_.begin(), shared_k_nodes_.end(), 0); - std::fill(shared_v_nodes_.begin(), shared_v_nodes_.end(), 0); - - size_t pos_offset = use_cache ? kv_cache_.get_total_seq_len() : 0; - auto backend = config_.default_backend == Config::Backend::CPU ? ComputeBackend::CPU : ComputeBackend::NPU; - - if (config_.hidden_size_per_layer_input == 0) { - auto token_input = gb->input({tokens.size()}, Precision::FP32); - auto hidden = gb->scalar_multiply(gb->embedding(embedding_node_id_, token_input), - std::sqrt(static_cast(config_.hidden_dim))); - - for (uint32_t i = 0; i < config_.num_layers; i++) - hidden = apply_transformer_layer(gb, hidden, 0, i, backend, use_cache, pos_offset); - - std::vector input_data(tokens.size()); - for (size_t i = 0; i < tokens.size(); i++) - input_data[i] = static_cast(tokens[i]); - gb->set_input(token_input, input_data.data(), Precision::FP32); - return gb->rms_norm(hidden, weight_nodes_.output_norm_weight, config_.layer_norm_eps); - } - - size_t token_input, pli_input; - auto hidden_pli = build_preamble_and_embed(gb, tokens.size(), backend, token_input, pli_input); - size_t hidden = hidden_pli.first, pli = hidden_pli.second; - - for (uint32_t i = 0; i < config_.num_layers; i++) - hidden = apply_transformer_layer(gb, hidden, pli, i, backend, use_cache, pos_offset); - - set_token_inputs(gb, token_input, pli_input, tokens); - return gb->rms_norm(hidden, weight_nodes_.output_norm_weight, config_.layer_norm_eps); -} - -size_t Gemma4Model::forward_split(const std::vector& tokens, bool use_cache) { - auto* gb = static_cast(graph_handle_); - gb->soft_reset(); - - std::fill(shared_k_nodes_.begin(), shared_k_nodes_.end(), 0); - std::fill(shared_v_nodes_.begin(), shared_v_nodes_.end(), 0); - - size_t seq_len = tokens.size(); - size_t pos_offset = use_cache ? kv_cache_.get_total_seq_len() : 0; - auto backend = config_.default_backend == Config::Backend::CPU ? ComputeBackend::CPU : ComputeBackend::NPU; - - if (config_.hidden_size_per_layer_input == 0) { - auto token_input = gb->input({seq_len}, Precision::FP32); - auto hidden = gb->scalar_multiply(gb->embedding(embedding_node_id_, token_input), - std::sqrt(static_cast(config_.hidden_dim))); - - for (uint32_t i = 0; i < first_shared_layer_; i++) - hidden = apply_transformer_layer(gb, hidden, 0, i, backend, use_cache, pos_offset); - - hidden = gb->index(hidden, seq_len - 1, 0); - hidden = gb->reshape(hidden, {1, config_.hidden_dim}); - - size_t shared_pos_offset = pos_offset + seq_len - 1; - for (uint32_t i = first_shared_layer_; i < config_.num_layers; i++) - hidden = apply_transformer_layer(gb, hidden, 0, i, backend, use_cache, shared_pos_offset); - - std::vector input_data(tokens.size()); - for (size_t i = 0; i < tokens.size(); i++) - input_data[i] = static_cast(tokens[i]); - gb->set_input(token_input, input_data.data(), Precision::FP32); - return gb->rms_norm(hidden, weight_nodes_.output_norm_weight, config_.layer_norm_eps); - } - - size_t token_input, pli_input; - auto hidden_pli = build_preamble_and_embed(gb, seq_len, backend, token_input, pli_input); - size_t hidden = hidden_pli.first, pli = hidden_pli.second; - - for (uint32_t i = 0; i < first_shared_layer_; i++) - hidden = apply_transformer_layer(gb, hidden, pli, i, backend, use_cache, pos_offset); - - hidden = gb->index(hidden, seq_len - 1, 0); - hidden = gb->reshape(hidden, {1, config_.hidden_dim}); - auto pli_last = gb->index(pli, seq_len - 1, 0); - pli_last = gb->reshape(pli_last, {1, config_.num_layers * config_.hidden_size_per_layer_input}); - - size_t shared_pos_offset = pos_offset + seq_len - 1; - for (uint32_t i = first_shared_layer_; i < config_.num_layers; i++) - hidden = apply_transformer_layer(gb, hidden, pli_last, i, backend, use_cache, shared_pos_offset); - - set_token_inputs(gb, token_input, pli_input, tokens); - return gb->rms_norm(hidden, weight_nodes_.output_norm_weight, config_.layer_norm_eps); -} - - -void Gemma4Model::prefill(const std::vector& tokens, size_t chunk_size, const std::string& profile_file) { - if (tokens.empty()) - return; - - if (has_npu_prefill()) { - size_t npu_chunk_size = static_cast(npu_prefill_->get_chunk_size()); - if (tokens.size() > npu_chunk_size) { - Model::prefill(tokens, chunk_size, profile_file); - return; - } - } - - static constexpr size_t SPLIT_PREFILL_MIN_TOKENS = 32; - bool use_split = config_.num_kv_shared_layers > 0 - && tokens.size() >= SPLIT_PREFILL_MIN_TOKENS - && !std::getenv("CACTUS_DISABLE_SPLIT_PREFILL"); - - if (!use_split) { - Model::prefill(tokens, chunk_size, profile_file); - return; - } - - auto* gb = static_cast(graph_handle_); - auto process_chunk = [&](const std::vector& chunk) { - forward_split(chunk, true); - gb->execute(profile_file); - update_kv_cache(gb, chunk.size()); - }; - - if (tokens.size() <= chunk_size) { - process_chunk(tokens); - return; - } - - size_t num_full_chunks = (tokens.size() - 1) / chunk_size; - for (size_t i = 0; i < num_full_chunks; ++i) { - size_t start = i * chunk_size; - std::vector chunk(tokens.begin() + start, tokens.begin() + start + chunk_size); - if (i == 1) - gb->set_prefill_mode(true); - process_chunk(chunk); - } - - gb->set_prefill_mode(false); - size_t final_start = num_full_chunks * chunk_size; - process_chunk(std::vector(tokens.begin() + final_start, tokens.end())); -} - -} -} diff --git a/cactus/models/gemma4/model_gemma4.h b/cactus/models/gemma4/model_gemma4.h deleted file mode 100644 index ce157decd..000000000 --- a/cactus/models/gemma4/model_gemma4.h +++ /dev/null @@ -1,413 +0,0 @@ -#pragma once - -#include "../model.h" - -bool test_gemma4_vision(bool expect_npu); -bool test_gemma4_audio(bool expect_npu); - -namespace cactus { -namespace engine { - -class Gemma4Model : public Model { - friend class Gemma4MmModel; -public: - Gemma4Model(); - explicit Gemma4Model(const Config& config); - ~Gemma4Model() override = default; - -protected: - size_t build_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0) override; - - size_t build_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, - ComputeBackend backend) const override; - - size_t build_moe(CactusGraph* gb, size_t input, uint32_t layer_idx, - ComputeBackend backend) const; - - size_t build_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0) override; - - size_t forward(const std::vector& tokens, bool use_cache = false) override; - void prefill(const std::vector& tokens, size_t chunk_size = 256, const std::string& profile_file = "") override; - void load_weights_to_graph(CactusGraph* gb) override; - void post_init() override; - std::vector get_kv_layer_dims() const override; - std::vector get_kv_layer_heads() const override; - void compact_kv_cache() override; - - size_t forward_from_embeddings(CactusGraph* gb, size_t hidden, const std::vector& pli_tokens, - size_t seq_len, ComputeBackend backend, bool use_cache); - size_t forward_from_embeddings(CactusGraph* gb, size_t hidden, size_t pli_hidden_source, - const std::vector& pli_tokens, size_t seq_len, - ComputeBackend backend, bool use_cache); - size_t build_pli_combined_from_tokens(CactusGraph* gb, size_t hidden, - const std::vector& pli_tokens, - size_t seq_len, ComputeBackend backend); - -private: - size_t forward_split(const std::vector& tokens, bool use_cache); - - std::pair build_preamble_and_embed(CactusGraph* gb, size_t seq_len, ComputeBackend backend, - size_t& token_input, size_t& pli_input); - - void set_token_inputs(CactusGraph* gb, size_t token_input, size_t pli_input, - const std::vector& tokens); - - size_t build_pli_combined(CactusGraph* gb, size_t hidden, size_t pli_embed, - size_t seq_len, ComputeBackend backend); - - size_t build_per_layer_input(CactusGraph* gb, size_t hidden, size_t pli_combined, uint32_t layer_idx, - ComputeBackend backend) const; - - bool is_global_layer(uint32_t idx) const; - size_t apply_partial_rope(CactusGraph* gb, size_t tensor, size_t head_dim, size_t rot_dim, - float rope_freq, size_t position_offset); - size_t apply_transformer_layer(CactusGraph* gb, size_t hidden, size_t pli, uint32_t layer_idx, - ComputeBackend backend, bool use_cache, size_t pos_offset); - - struct WeightNodeIDs { - size_t output_weight; - size_t output_norm_weight; - - size_t embed_tokens_per_layer; - size_t per_layer_model_proj; - size_t per_layer_proj_norm; - - struct LayerWeights { - size_t attn_q_weight; - size_t attn_k_weight; - size_t attn_v_weight; - size_t attn_output_weight; - size_t input_layernorm_weight; - size_t attn_q_norm_weight; - size_t attn_k_norm_weight; - size_t pre_feedforward_layernorm_weight; - size_t post_feedforward_layernorm_weight; - size_t ffn_gate_weight; - size_t ffn_up_weight; - size_t ffn_down_weight; - size_t post_attention_layernorm_weight; - size_t per_layer_gate; - size_t per_layer_proj; - size_t post_per_layer_norm; - size_t layer_scalar; - - std::vector moe_w1_experts; - std::vector moe_w3_experts; - std::vector moe_w2_experts; - size_t moe_per_expert_scale = 0; - size_t router_proj = 0; - size_t router_scale = 0; - size_t post_ffn_norm_1 = 0; - size_t pre_ffn_norm_2 = 0; - size_t post_ffn_norm_2 = 0; - }; - - std::vector layers; - } weight_nodes_; - - uint32_t first_shared_layer_ = 0; - std::vector kv_share_map_; - std::vector shared_k_nodes_; - std::vector shared_v_nodes_; - - std::vector<__fp16> v_norm_ones_weight_; - size_t v_norm_ones_node_ = 0; - size_t v_norm_ones_global_node_ = 0; -}; - -class Gemma4VisionModel : public Model { - friend class Gemma4MmModel; -public: - struct PreprocessedImage { - std::vector pixel_values; - size_t height; - size_t width; - size_t patch_height; - size_t patch_width; - size_t num_patches; - }; - - Gemma4VisionModel(); - explicit Gemma4VisionModel(const Config& config); - ~Gemma4VisionModel() override = default; - - PreprocessedImage preprocess_image(const std::string& image_path); - size_t forward_vision(CactusGraph* gb, const PreprocessedImage& img, ComputeBackend backend); - size_t build_vision_projector(CactusGraph* gb, size_t vision_features, ComputeBackend backend); - -protected: - size_t forward(const std::vector&, bool) override { - throw std::runtime_error("Gemma4VisionModel: use forward_vision() instead"); - } - size_t build_attention(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) override { - throw std::runtime_error("Gemma4VisionModel: build_attention unused"); - } - size_t build_mlp(CactusGraph*, size_t, uint32_t, ComputeBackend) const override { - throw std::runtime_error("Gemma4VisionModel: build_mlp unused"); - } - size_t build_transformer_block(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) override { - throw std::runtime_error("Gemma4VisionModel: build_transformer_block unused"); - } - void load_weights_to_graph(CactusGraph* gb) override; - -private: - size_t build_vision_patch_embedding(CactusGraph* gb, const PreprocessedImage& img, ComputeBackend backend); - size_t build_vision_attention(CactusGraph* gb, size_t input, uint32_t layer_idx, - size_t cos_node, size_t sin_node, - size_t attn_mask_node, ComputeBackend backend); - size_t build_vision_mlp(CactusGraph* gb, size_t input, uint32_t layer_idx, ComputeBackend backend); - size_t build_vision_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - size_t cos_node, size_t sin_node, - size_t attn_mask_node, ComputeBackend backend); - size_t build_vision_pooler(CactusGraph* gb, size_t hidden, const PreprocessedImage& img, ComputeBackend backend); - std::pair build_2d_rope_nodes(CactusGraph* gb, const PreprocessedImage& img, size_t max_patches); - size_t build_padding_mask(CactusGraph* gb, size_t num_real, size_t max_patches); - - struct VisionWeightNodes { - size_t patch_input_proj = 0; - size_t position_table = 0; - - struct LayerWeights { - size_t attn_q_weight = 0; - size_t attn_k_weight = 0; - size_t attn_v_weight = 0; - size_t attn_output_weight = 0; - size_t attn_q_norm = 0; - size_t attn_k_norm = 0; - size_t input_layernorm = 0; - size_t post_attention_layernorm = 0; - size_t pre_feedforward_layernorm = 0; - size_t post_feedforward_layernorm = 0; - size_t mlp_gate_proj = 0; - size_t mlp_up_proj = 0; - size_t mlp_down_proj = 0; - size_t layer_scalar = 0; - }; - - std::vector layers; - size_t embed_vision_proj = 0; - size_t post_proj_norm = 0; - } vision_weights_; - - std::vector<__fp16> vision_v_norm_ones_; - size_t vision_v_norm_ones_node_ = 0; - - std::unique_ptr npu_encoder_; - bool use_npu_encoder_ = false; - bool disable_npu_ = false; - - friend bool ::test_gemma4_vision(bool); -}; - -class Gemma4AudioModel : public Model { - friend class Gemma4MmModel; -public: - Gemma4AudioModel(); - explicit Gemma4AudioModel(const Config& config); - ~Gemma4AudioModel() override = default; - - struct ConformerContext { - size_t timing_fp16 = 0; - size_t front_pad = 0; - size_t back_pad = 0; - size_t seq_len = 0; - }; - - size_t forward_audio(CactusGraph* gb, const std::vector& mel_features, - size_t num_frames, ComputeBackend backend); - - size_t build_audio_projector(CactusGraph* gb, size_t audio_features, ComputeBackend backend); - - size_t build_sscp(CactusGraph* gb, const std::vector& mel_features, - size_t num_frames, ComputeBackend backend); - - ConformerContext build_conformer_context(CactusGraph* gb, size_t sscp_output); - - size_t build_conformer_ffw(CactusGraph* gb, size_t input, uint32_t layer_idx, - bool is_end, ComputeBackend backend); - size_t build_conformer_attention(CactusGraph* gb, size_t input, uint32_t layer_idx, - const ConformerContext& ctx, ComputeBackend backend); - size_t build_conformer_lconv1d(CactusGraph* gb, size_t input, uint32_t layer_idx, - ComputeBackend backend); - size_t build_conformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - const ConformerContext& ctx, ComputeBackend backend); - - struct AudioWeightNodes { - size_t sscp_conv0_weight = 0; - size_t sscp_conv0_norm = 0; - size_t sscp_conv1_weight = 0; - size_t sscp_conv1_norm = 0; - size_t sscp_input_proj = 0; - - struct ClipBounds { - float in_min = -1e10f, in_max = 1e10f; - float out_min = -1e10f, out_max = 1e10f; - }; - - struct ConformerLayerWeights { - size_t ffw_start_1 = 0, ffw_start_2 = 0; - ClipBounds ffw_start_1_clip, ffw_start_2_clip; - size_t ffw_start_pre_norm = 0, ffw_start_post_norm = 0; - size_t attn_q = 0, attn_k = 0, attn_v = 0; - ClipBounds attn_q_clip, attn_k_clip, attn_v_clip; - size_t attn_per_dim_scale = 0; - size_t attn_rel_pos_proj = 0; - size_t attn_post = 0; - ClipBounds attn_post_clip; - size_t attn_pre_norm = 0, attn_post_norm = 0; - size_t lconv_start = 0, lconv_depthwise = 0, lconv_end = 0; - ClipBounds lconv_start_clip, lconv_end_clip; - size_t lconv_pre_norm = 0, lconv_conv_norm = 0; - size_t ffw_end_1 = 0, ffw_end_2 = 0; - ClipBounds ffw_end_1_clip, ffw_end_2_clip; - size_t ffw_end_pre_norm = 0, ffw_end_post_norm = 0; - size_t block_norm = 0; - }; - std::vector layers; - - size_t output_proj = 0; - size_t output_proj_bias = 0; - - size_t embed_audio_proj = 0; - } audio_weights_; - - std::vector<__fp16> audio_proj_norm_ones_; - size_t audio_proj_norm_ones_node_ = 0; - - std::unique_ptr npu_encoder_; - bool use_npu_encoder_ = false; - bool disable_npu_ = false; - std::vector<__fp16> npu_audio_input_scratch_; - std::vector<__fp16> npu_audio_output_scratch_; - std::vector<__fp16> npu_audio_reorder_scratch_; - - friend bool ::test_gemma4_audio(bool); - -protected: - size_t forward(const std::vector&, bool) override { - throw std::runtime_error("Gemma4AudioModel: use forward_audio() instead"); - } - size_t build_attention(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) override { - throw std::runtime_error("Gemma4AudioModel: build_attention unused"); - } - size_t build_mlp(CactusGraph*, size_t, uint32_t, ComputeBackend) const override { - throw std::runtime_error("Gemma4AudioModel: build_mlp unused"); - } - size_t build_transformer_block(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) override { - throw std::runtime_error("Gemma4AudioModel: build_transformer_block unused"); - } - void load_weights_to_graph(CactusGraph* gb) override; -}; - -class Gemma4MmModel : public Model { -public: - Gemma4MmModel(); - explicit Gemma4MmModel(const Config& config); - ~Gemma4MmModel() override = default; - - bool init(const std::string& model_folder, size_t context_size, - const std::string& system_prompt = "", bool do_warmup = true) override; - - size_t forward(const std::vector& tokens, bool use_cache = false) override; - - uint32_t decode(const std::vector& tokens, - float temperature = -1.0f, float top_p = -1.0f, size_t top_k = 0, - const std::string& profile_file = "", float* out_entropy = nullptr, - float min_p = 0.15f, float repetition_penalty = 1.1f) override; - - void prefill(const std::vector& tokens, size_t chunk_size = 256, - const std::string& profile_file = "") override; - - void prefill_with_images(const std::vector& tokens, const std::vector& image_paths, - const std::string& profile_file = "") override; - - uint32_t decode_with_images( - const std::vector& tokens, - const std::vector& image_paths, - float temperature = -1.0f, float top_p = -1.0f, size_t top_k = 0, - const std::string& profile_file = "", float* out_entropy = nullptr, - float min_p = 0.15f, float repetition_penalty = 1.1f) override; - - uint32_t decode_with_audio( - const std::vector& tokens, - const std::vector& audio_features, - float temperature = 0.0f, float top_p = 0.0f, size_t top_k = 0, - const std::string& profile_file = "", float* out_entropy = nullptr, - float min_p = 0.15f, float repetition_penalty = 1.1f, - float* out_token_time_start = nullptr, float* out_token_time_end = nullptr) override; - - uint32_t decode_with_media( - const std::vector& tokens, - const std::vector& image_paths, - const std::vector& audio_features, - float temperature = -1.0f, float top_p = -1.0f, size_t top_k = 0, - const std::string& profile_file = "", float* out_entropy = nullptr, - float min_p = 0.15f, float repetition_penalty = 1.1f); - - void reset_cache() override; - std::vector get_image_embeddings(const std::string& image_path) override; - std::vector get_audio_embeddings(const std::vector& audio_features) override; - void compact_kv_cache() override; - void remove_thinking_tokens(const std::vector>& ranges) override; - - void set_tool_constraints(const std::vector& tools) override; - void clear_tool_constraints() override; - void update_tool_constraints(uint32_t token_id) override; - -protected: - size_t build_attention(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) override; - size_t build_mlp(CactusGraph*, size_t, uint32_t, ComputeBackend) const override; - size_t build_transformer_block(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) override; - void load_weights_to_graph(CactusGraph* gb) override; - -private: - struct ForwardResult { - size_t final_hidden_node; - size_t seq_len; - }; - - ForwardResult forward_multimodal(CactusGraph* gb, const std::vector& tokens, - const std::vector& image_paths, - const std::vector* audio_features, - size_t audio_num_frames, - ComputeBackend backend, bool use_cache); - - uint32_t decode_multimodal(const std::vector& tokens, - const std::vector& image_paths, - const std::vector* audio_features, - size_t audio_num_frames, - float temperature, float top_p, size_t top_k, - const std::string& profile_file, float* out_entropy, - float min_p, float repetition_penalty); - -public: - struct MultimodalInputs { - size_t hidden_node = 0; - size_t pli_hidden_source_node = 0; - std::vector pli_tokens; - size_t seq_len = 0; - }; - - const Gemma4VisionModel& vision_encoder() const { return vision_encoder_; } - Gemma4VisionModel& vision_encoder() { return vision_encoder_; } - const Gemma4AudioModel& audio_encoder() const { return audio_encoder_; } - Gemma4AudioModel& audio_encoder() { return audio_encoder_; } - const Gemma4Model& language_model() const { return language_model_; } - Gemma4Model& language_model() { return language_model_; } - MultimodalInputs build_multimodal_inputs( - CactusGraph* gb, const std::vector& tokens, - const std::vector& image_paths, - const std::vector* audio_features, - size_t audio_num_frames, - ComputeBackend backend); - -private: - Gemma4VisionModel vision_encoder_; - Gemma4AudioModel audio_encoder_; - Gemma4Model language_model_; -}; - -} -} diff --git a/cactus/models/gemma4/model_gemma4_audio.cpp b/cactus/models/gemma4/model_gemma4_audio.cpp deleted file mode 100644 index 9d7e7cab8..000000000 --- a/cactus/models/gemma4/model_gemma4_audio.cpp +++ /dev/null @@ -1,781 +0,0 @@ -#include "model_gemma4.h" -#include "../../graph/graph.h" -#include -#include -#include -#include -#include - -namespace cactus { -namespace engine { - -static const float INV_LN2 = 1.0f / std::log(2.0f); -static const float GEMMA4_AUDIO_K_SCALE = std::log(1.0f + std::exp(1.0f)) / std::log(2.0f); -static const char* GEMMA4_AUDIO_NPU_INPUT_NAME = "x"; - -static size_t shape_elements(const std::vector& shape) { - if (shape.empty()) return 0; - size_t total = 1; - for (int d : shape) { - if (d <= 0) return 0; - total *= static_cast(d); - } - return total; -} - -static size_t infer_npu_audio_max_frames(const std::vector& shape, size_t mel_bins) { - if (shape.size() == 2) { - size_t s0 = static_cast(shape[0]); - size_t s1 = static_cast(shape[1]); - if (s1 == mel_bins) return s0; - if (s0 == mel_bins) return s1; - } else if (shape.size() == 3) { - size_t s0 = static_cast(shape[0]); - size_t s1 = static_cast(shape[1]); - size_t s2 = static_cast(shape[2]); - if (s0 == 1 && s2 == mel_bins) return s1; // [1, T, F] - if (s0 == 1 && s1 == mel_bins) return s2; // [1, F, T] - if (s1 == mel_bins && s2 == 1) return s0; // [T, F, 1] - if (s0 == mel_bins && s2 == 1) return s1; // [F, T, 1] - } else if (shape.size() == 4) { - size_t s0 = static_cast(shape[0]); - size_t s1 = static_cast(shape[1]); - size_t s2 = static_cast(shape[2]); - size_t s3 = static_cast(shape[3]); - if (s0 == 1 && s1 == 1 && s3 == mel_bins) return s2; // [1, 1, T, F] - if (s0 == 1 && s1 == 1 && s2 == mel_bins) return s3; // [1, 1, F, T] - if (s0 == 1 && s1 == mel_bins && s3 == 1) return s2; // [1, F, T, 1] - if (s0 == 1 && s2 == mel_bins && s3 == 1) return s1; // [1, T, F, 1] - } - return 0; -} - -static bool pack_gemma4_audio_for_npu(const std::vector& mel_features, - size_t frames, - size_t mel_bins, - const std::vector& input_shape, - std::vector<__fp16>& packed) { - if (input_shape.empty()) return false; - const size_t total = shape_elements(input_shape); - if (total == 0) return false; - packed.resize(total); - std::fill(packed.begin(), packed.end(), static_cast<__fp16>(0.0f)); - - auto tm = [&](size_t t, size_t m) -> __fp16 { - return static_cast<__fp16>(mel_features[t * mel_bins + m]); - }; - - if (input_shape.size() == 4) { - const size_t s0 = static_cast(input_shape[0]); - const size_t s1 = static_cast(input_shape[1]); - const size_t s2 = static_cast(input_shape[2]); - const size_t s3 = static_cast(input_shape[3]); - if (s0 != 1) return false; - - if (s1 == 1 && s2 >= frames && s3 == mel_bins) { - for (size_t t = 0; t < frames; ++t) - for (size_t m = 0; m < mel_bins; ++m) - packed[(t * s3) + m] = tm(t, m); - return true; - } - if (s1 == 1 && s2 == mel_bins && s3 >= frames) { - for (size_t t = 0; t < frames; ++t) - for (size_t m = 0; m < mel_bins; ++m) - packed[(m * s3) + t] = tm(t, m); - return true; - } - if (s1 >= frames && s2 == mel_bins && s3 == 1) { - for (size_t t = 0; t < frames; ++t) - for (size_t m = 0; m < mel_bins; ++m) - packed[((t * s2 + m) * s3)] = tm(t, m); - return true; - } - if (s1 == mel_bins && s2 >= frames && s3 == 1) { - for (size_t t = 0; t < frames; ++t) - for (size_t m = 0; m < mel_bins; ++m) - packed[((m * s2 + t) * s3)] = tm(t, m); - return true; - } - return false; - } - - if (input_shape.size() == 3) { - const size_t s0 = static_cast(input_shape[0]); - const size_t s1 = static_cast(input_shape[1]); - const size_t s2 = static_cast(input_shape[2]); - - if (s0 == 1 && s1 >= frames && s2 == mel_bins) { - for (size_t t = 0; t < frames; ++t) - for (size_t m = 0; m < mel_bins; ++m) - packed[t * s2 + m] = tm(t, m); - return true; - } - if (s0 == 1 && s1 == mel_bins && s2 >= frames) { - for (size_t t = 0; t < frames; ++t) - for (size_t m = 0; m < mel_bins; ++m) - packed[m * s2 + t] = tm(t, m); - return true; - } - if (s0 >= frames && s1 == mel_bins && s2 == 1) { - for (size_t t = 0; t < frames; ++t) - for (size_t m = 0; m < mel_bins; ++m) - packed[(t * s1 + m) * s2] = tm(t, m); - return true; - } - if (s0 == mel_bins && s1 >= frames && s2 == 1) { - for (size_t t = 0; t < frames; ++t) - for (size_t m = 0; m < mel_bins; ++m) - packed[(m * s1 + t) * s2] = tm(t, m); - return true; - } - return false; - } - - if (input_shape.size() == 2) { - const size_t s0 = static_cast(input_shape[0]); - const size_t s1 = static_cast(input_shape[1]); - - if (s0 >= frames && s1 == mel_bins) { - for (size_t t = 0; t < frames; ++t) - for (size_t m = 0; m < mel_bins; ++m) - packed[t * s1 + m] = tm(t, m); - return true; - } - if (s0 == mel_bins && s1 >= frames) { - for (size_t t = 0; t < frames; ++t) - for (size_t m = 0; m < mel_bins; ++m) - packed[m * s1 + t] = tm(t, m); - return true; - } - return false; - } - - return false; -} - -struct NPUAudioOutputLayout { - std::vector dims; - size_t hidden_axis = SIZE_MAX; - size_t hidden_dim = 0; - size_t time_steps = 0; -}; - -static bool infer_npu_audio_output_layout(const std::vector& output_shape, - size_t elements_written, - size_t expected_hidden_dim, - NPUAudioOutputLayout& layout) { - if (elements_written == 0) return false; - - layout = NPUAudioOutputLayout{}; - for (int d : output_shape) { - if (d > 0) layout.dims.push_back(static_cast(d)); - } - - if (layout.dims.empty()) { - if (expected_hidden_dim == 0 || (elements_written % expected_hidden_dim) != 0) { - return false; - } - layout.dims = {elements_written / expected_hidden_dim, expected_hidden_dim}; - } - - const size_t rank = layout.dims.size(); - size_t hidden_axis = SIZE_MAX; - if (expected_hidden_dim > 0) { - if (layout.dims[rank - 1] == expected_hidden_dim) { - hidden_axis = rank - 1; - } else if (rank >= 2 && layout.dims[rank - 2] == expected_hidden_dim) { - hidden_axis = rank - 2; - } else { - for (size_t i = 0; i < rank; ++i) { - if (layout.dims[i] == expected_hidden_dim) { - hidden_axis = i; - break; - } - } - } - } - - if (hidden_axis == SIZE_MAX) { - hidden_axis = rank - 1; - } - - const size_t hidden_dim = layout.dims[hidden_axis]; - if (hidden_dim == 0 || (elements_written % hidden_dim) != 0) { - return false; - } - - layout.hidden_axis = hidden_axis; - layout.hidden_dim = hidden_dim; - layout.time_steps = elements_written / hidden_dim; - return layout.time_steps > 0; -} - -static bool materialize_npu_audio_time_major(const __fp16* src, - const NPUAudioOutputLayout& layout, - size_t time_steps, - std::vector<__fp16>& dst) { - if (!src || layout.dims.empty() || layout.hidden_dim == 0 || layout.time_steps == 0) { - return false; - } - if (layout.hidden_axis >= layout.dims.size() || time_steps == 0 || time_steps > layout.time_steps) { - return false; - } - - const size_t rank = layout.dims.size(); - if (layout.hidden_axis == rank - 1) { - // Already [time, hidden] in row-major flattening. - return false; - } - - std::vector strides(rank, 1); - for (size_t i = rank; i-- > 1;) { - strides[i - 1] = strides[i] * layout.dims[i]; - } - - std::vector non_hidden_axes; - non_hidden_axes.reserve(rank - 1); - for (size_t i = 0; i < rank; ++i) { - if (i != layout.hidden_axis) non_hidden_axes.push_back(i); - } - - dst.assign(time_steps * layout.hidden_dim, static_cast<__fp16>(0.0f)); - - for (size_t t = 0; t < time_steps; ++t) { - size_t rem = t; - size_t base_offset = 0; - for (size_t i = non_hidden_axes.size(); i-- > 0;) { - size_t axis = non_hidden_axes[i]; - size_t dim = layout.dims[axis]; - size_t coord = rem % dim; - rem /= dim; - base_offset += coord * strides[axis]; - } - - for (size_t h = 0; h < layout.hidden_dim; ++h) { - size_t src_index = base_offset + h * strides[layout.hidden_axis]; - dst[t * layout.hidden_dim + h] = src[src_index]; - } - } - - return true; -} - -static size_t graph_clamp(CactusGraph* gb, size_t x, float lo, float hi) { - if (lo >= hi || std::isinf(lo) || std::isinf(hi)) return x; - x = gb->scalar_add(x, -lo); - x = gb->relu(x); - x = gb->scalar_add(x, lo); - x = gb->scalar_multiply(gb->scalar_add(x, -hi), -1.0f); - x = gb->relu(x); - x = gb->scalar_add(gb->scalar_multiply(x, -1.0f), hi); - return x; -} - -static size_t clipped_matmul(CactusGraph* gb, size_t input, size_t weight, - float in_min, float in_max, float out_min, float out_max, - ComputeBackend backend) { - size_t clamped_in = graph_clamp(gb, input, in_min, in_max); - size_t result = gb->matmul(clamped_in, weight, true, backend); - return graph_clamp(gb, result, out_min, out_max); -} - -static float read_scalar_weight(CactusGraph* gb, const std::string& path) { - size_t node = gb->mmap_weights(path); - const auto& buf = gb->get_output_buffer(node); - if (buf.precision == Precision::FP16) - return static_cast(*buf.data_as<__fp16>()); - if (buf.precision == Precision::FP32) - return *buf.data_as(); - return 0.0f; -} - -static Gemma4AudioModel::AudioWeightNodes::ClipBounds load_clip_bounds(CactusGraph* gb, const std::string& prefix) { - Gemma4AudioModel::AudioWeightNodes::ClipBounds cb{0, 0, 0, 0}; - std::string in_min_path = prefix + "_input_min.weights"; - if (!std::filesystem::exists(in_min_path)) - return cb; - cb.in_min = read_scalar_weight(gb, in_min_path); - cb.in_max = read_scalar_weight(gb, prefix + "_input_max.weights"); - cb.out_min = read_scalar_weight(gb, prefix + "_output_min.weights"); - cb.out_max = read_scalar_weight(gb, prefix + "_output_max.weights"); - return cb; -} - -static void get_conv2d_output_hw(const CactusGraph* gb, size_t node, size_t& h, size_t& w) { - const auto& buf = gb->get_output_buffer(node); - h = buf.shape[2]; - w = buf.shape[3]; -} - -Gemma4AudioModel::Gemma4AudioModel() : Model() {} -Gemma4AudioModel::Gemma4AudioModel(const Config& config) : Model(config) {} - -void Gemma4AudioModel::load_weights_to_graph(CactusGraph* gb) { - auto resolve = [&](const std::string& name) -> std::string { - return model_folder_path_ + "/" + name; - }; - auto resolve_existing = [&](const std::string& preferred, - const std::string& legacy = std::string()) -> std::string { - std::string preferred_path = resolve(preferred); - if (std::filesystem::exists(preferred_path)) { - return preferred_path; - } - if (!legacy.empty()) { - std::string legacy_path = resolve(legacy); - if (std::filesystem::exists(legacy_path)) { - return legacy_path; - } - } - return preferred_path; - }; - auto load_weight = [&](const std::string& preferred, - const std::string& legacy = std::string()) -> size_t { - return gb->mmap_weights(resolve_existing(preferred, legacy)); - }; - auto load_clip_from_weight = [&](const std::string& preferred, - const std::string& legacy = std::string()) - -> Gemma4AudioModel::AudioWeightNodes::ClipBounds { - std::string weight_path = resolve_existing(preferred, legacy); - static const std::string suffix = ".weights"; - if (weight_path.size() > suffix.size() && - weight_path.compare(weight_path.size() - suffix.size(), suffix.size(), suffix) == 0) { - return load_clip_bounds(gb, weight_path.substr(0, weight_path.size() - suffix.size())); - } - return Gemma4AudioModel::AudioWeightNodes::ClipBounds{0, 0, 0, 0}; - }; - - if (!disable_npu_ && npu::is_npu_available()) { - std::string npu_path = model_folder_path_ + "/audio_encoder.mlpackage"; - if (std::filesystem::exists(npu_path)) { - npu_encoder_ = npu::create_encoder(); - if (npu_encoder_ && npu_encoder_->load(npu_path)) { - use_npu_encoder_ = true; - std::vector input_shape = npu_encoder_->get_input_shape(); - npu_encoder_->preallocate(input_shape, GEMMA4_AUDIO_NPU_INPUT_NAME, ""); - } else { - use_npu_encoder_ = false; - npu_encoder_.reset(); - CACTUS_LOG_WARN("npu", "[gemma4-audio] found audio_encoder.mlpackage but failed to enable NPU audio encoder; using CPU"); - } - } else { - CACTUS_LOG_WARN("npu", "[gemma4-audio] audio_encoder.mlpackage not found; using CPU audio encoder"); - } - } else if (!disable_npu_) { - CACTUS_LOG_WARN("npu", "[gemma4-audio] NPU backend unavailable on this device; using CPU audio encoder"); - } - - // Keep CPU audio weights available even when the NPU encoder is enabled. - // The runtime may still fall back to the graph path for unsupported inputs. - { - audio_weights_.sscp_conv0_weight = load_weight( - "audio_subsample_conv_projection_conv_0_conv.weights", - "audio_subsample_conv_projection_layer0_conv.weights"); - audio_weights_.sscp_conv0_norm = load_weight( - "audio_subsample_conv_projection_conv_0_norm.weights", - "audio_subsample_conv_projection_layer0_norm.weights"); - audio_weights_.sscp_conv1_weight = load_weight( - "audio_subsample_conv_projection_conv_1_conv.weights", - "audio_subsample_conv_projection_layer1_conv.weights"); - audio_weights_.sscp_conv1_norm = load_weight( - "audio_subsample_conv_projection_conv_1_norm.weights", - "audio_subsample_conv_projection_layer1_norm.weights"); - audio_weights_.sscp_input_proj = load_weight( - "audio_subsample_conv_projection_input_proj.weights", - "audio_subsample_conv_projection_input_proj_linear.weights"); - - audio_weights_.layers.resize(config_.audio_num_layers); - for (uint32_t i = 0; i < config_.audio_num_layers; i++) { - auto& layer = audio_weights_.layers[i]; - - auto layer_weight = [&](const std::string& preferred_suffix, - const std::string& legacy_suffix = std::string()) -> size_t { - std::string legacy = legacy_suffix.empty() ? preferred_suffix : legacy_suffix; - return load_weight( - "audio_conformer_" + std::to_string(i) + "_" + preferred_suffix, - "audio_layers_" + std::to_string(i) + "_" + legacy); - }; - auto layer_clip = [&](const std::string& preferred_suffix, - const std::string& legacy_suffix = std::string()) - -> Gemma4AudioModel::AudioWeightNodes::ClipBounds { - std::string legacy = legacy_suffix.empty() ? preferred_suffix : legacy_suffix; - return load_clip_from_weight( - "audio_conformer_" + std::to_string(i) + "_" + preferred_suffix, - "audio_layers_" + std::to_string(i) + "_" + legacy); - }; - - layer.ffw_start_1 = layer_weight("ffw_layer_start_ffw_layer_1.weights", "feed_forward1_ffw_layer_1.weights"); - layer.ffw_start_2 = layer_weight("ffw_layer_start_ffw_layer_2.weights", "feed_forward1_ffw_layer_2.weights"); - layer.ffw_start_1_clip = layer_clip("ffw_layer_start_ffw_layer_1.weights", "feed_forward1_ffw_layer_1.weights"); - layer.ffw_start_2_clip = layer_clip("ffw_layer_start_ffw_layer_2.weights", "feed_forward1_ffw_layer_2.weights"); - layer.ffw_start_pre_norm = layer_weight("ffw_layer_start_pre_layer_norm.weights", "feed_forward1_pre_layer_norm.weights"); - layer.ffw_start_post_norm = layer_weight("ffw_layer_start_post_layer_norm.weights", "feed_forward1_post_layer_norm.weights"); - - layer.attn_q = layer_weight("attention_attn_q_proj.weights", "self_attn_q_proj.weights"); - layer.attn_k = layer_weight("attention_attn_k_proj.weights", "self_attn_k_proj.weights"); - layer.attn_v = layer_weight("attention_attn_v_proj.weights", "self_attn_v_proj.weights"); - layer.attn_q_clip = layer_clip("attention_attn_q_proj.weights", "self_attn_q_proj.weights"); - layer.attn_k_clip = layer_clip("attention_attn_k_proj.weights", "self_attn_k_proj.weights"); - layer.attn_v_clip = layer_clip("attention_attn_v_proj.weights", "self_attn_v_proj.weights"); - layer.attn_per_dim_scale = layer_weight("attention_attn_per_dim_scale.weights", "self_attn_per_dim_scale.weights"); - layer.attn_rel_pos_proj = layer_weight( - "attention_attn_relative_position_embedding_pos_proj.weights", - "self_attn_relative_k_proj.weights"); - layer.attn_post = layer_weight("attention_post.weights", "self_attn_post.weights"); - layer.attn_post_clip = layer_clip("attention_post.weights", "self_attn_post.weights"); - layer.attn_pre_norm = layer_weight("attention_pre_attn_norm.weights", "norm_pre_attn.weights"); - layer.attn_post_norm = layer_weight("attention_post_norm.weights", "norm_post_attn.weights"); - - layer.lconv_start = layer_weight("lconv1d_linear_start.weights"); - layer.lconv_depthwise = layer_weight("lconv1d_depthwise_conv1d.weights"); - layer.lconv_end = layer_weight("lconv1d_linear_end.weights"); - layer.lconv_start_clip = layer_clip("lconv1d_linear_start.weights"); - layer.lconv_end_clip = layer_clip("lconv1d_linear_end.weights"); - layer.lconv_pre_norm = layer_weight("lconv1d_pre_layer_norm.weights"); - layer.lconv_conv_norm = layer_weight("lconv1d_conv_norm.weights"); - - layer.ffw_end_1 = layer_weight("ffw_layer_end_ffw_layer_1.weights", "feed_forward2_ffw_layer_1.weights"); - layer.ffw_end_2 = layer_weight("ffw_layer_end_ffw_layer_2.weights", "feed_forward2_ffw_layer_2.weights"); - layer.ffw_end_1_clip = layer_clip("ffw_layer_end_ffw_layer_1.weights", "feed_forward2_ffw_layer_1.weights"); - layer.ffw_end_2_clip = layer_clip("ffw_layer_end_ffw_layer_2.weights", "feed_forward2_ffw_layer_2.weights"); - layer.ffw_end_pre_norm = layer_weight("ffw_layer_end_pre_layer_norm.weights", "feed_forward2_pre_layer_norm.weights"); - layer.ffw_end_post_norm = layer_weight("ffw_layer_end_post_layer_norm.weights", "feed_forward2_post_layer_norm.weights"); - - layer.block_norm = layer_weight("norm.weights", "norm_out.weights"); - } - - audio_weights_.output_proj = load_weight("audio_output_proj.weights"); - audio_weights_.output_proj_bias = load_weight("audio_output_proj.bias"); - } - audio_weights_.embed_audio_proj = load_weight("embed_audio_proj.weights"); - - size_t proj_out_dim = config_.audio_output_proj_dims > 0 - ? static_cast(config_.audio_output_proj_dims) - : static_cast(config_.audio_hidden_dim); - audio_proj_norm_ones_.assign(proj_out_dim, static_cast<__fp16>(1.0f)); - audio_proj_norm_ones_node_ = gb->input({proj_out_dim}, Precision::FP16); - gb->set_external_input(audio_proj_norm_ones_node_, audio_proj_norm_ones_.data(), Precision::FP16); - - output_weight_node_id_ = 0; -} - -size_t Gemma4AudioModel::build_sscp(CactusGraph* gb, const std::vector& mel_features, - size_t num_frames, ComputeBackend backend) { - size_t mel_bins = config_.audio_input_feat_size; - size_t conv0_ch = config_.audio_sscp_conv0_channels; - size_t conv1_ch = config_.audio_sscp_conv1_channels; - - size_t mel_input = gb->input({num_frames, mel_bins}, Precision::FP32); - gb->set_input(mel_input, mel_features.data(), Precision::FP32); - size_t mel_fp16 = gb->precision_cast(mel_input, Precision::FP16); - - size_t x = gb->reshape(mel_fp16, {1, 1, num_frames, mel_bins}); - - x = gb->conv2d_k3s2p1(x, audio_weights_.sscp_conv0_weight); - size_t h1, w1; - get_conv2d_output_hw(gb, x, h1, w1); - - x = gb->reshape(x, {conv0_ch, h1, w1}); - x = gb->transposeN(x, {1, 2, 0}); - x = gb->reshape(x, {h1 * w1, conv0_ch}); - x = gb->layernorm(x, audio_weights_.sscp_conv0_norm, config_.audio_sscp_conv_eps); - x = gb->reshape(x, {h1, w1, conv0_ch}); - x = gb->transposeN(x, {2, 0, 1}); - x = gb->relu(x); - x = gb->reshape(x, {1, conv0_ch, h1, w1}); - - x = gb->conv2d_k3s2p1(x, audio_weights_.sscp_conv1_weight); - size_t h2, w2; - get_conv2d_output_hw(gb, x, h2, w2); - - x = gb->reshape(x, {conv1_ch, h2, w2}); - x = gb->transposeN(x, {1, 2, 0}); - x = gb->reshape(x, {h2 * w2, conv1_ch}); - x = gb->layernorm(x, audio_weights_.sscp_conv1_norm, config_.audio_sscp_conv_eps); - x = gb->reshape(x, {h2, w2, conv1_ch}); - x = gb->transposeN(x, {2, 0, 1}); - x = gb->relu(x); - - x = gb->transposeN(x, {1, 2, 0}); - x = gb->reshape(x, {h2, w2 * conv1_ch}); - - return gb->matmul(x, audio_weights_.sscp_input_proj, true, backend); -} - -static std::vector compute_timing_signal(size_t num_positions, size_t hidden_dim, size_t max_past) { - size_t num_timescales = hidden_dim / 2; - float log_ts_inc = std::log(1.0e4f) / std::max(static_cast(num_timescales) - 1, 1); - - std::vector signal(num_positions * hidden_dim); - for (size_t p = 0; p < num_positions; p++) { - float pos = static_cast(max_past) - static_cast(p); - for (size_t i = 0; i < num_timescales; i++) { - float scaled = pos * std::exp(-static_cast(i) * log_ts_inc); - signal[p * hidden_dim + i] = std::sin(scaled); - signal[p * hidden_dim + num_timescales + i] = std::cos(scaled); - } - } - return signal; -} - -static size_t create_zero_pad_node(CactusGraph* gb, size_t time_len, size_t num_heads, size_t head_dim) { - if (time_len == 0) return 0; - std::vector<__fp16> zeros(time_len * num_heads * head_dim, static_cast<__fp16>(0.0f)); - size_t node = gb->input({time_len, num_heads, head_dim}, Precision::FP16); - gb->set_input(node, zeros.data(), Precision::FP16); - return node; -} - -Gemma4AudioModel::ConformerContext Gemma4AudioModel::build_conformer_context( - CactusGraph* gb, size_t sscp_output) { - - size_t num_heads = config_.audio_num_heads; - size_t head_dim = config_.audio_head_dim; - size_t hidden_dim = num_heads * head_dim; - size_t max_past = config_.audio_context_left > 0 ? config_.audio_context_left - 1 : 0; - size_t num_positions = max_past + config_.audio_context_right + 1; - size_t seq_len = gb->get_output_buffer(sscp_output).shape[0]; - - auto timing_signal = compute_timing_signal(num_positions, hidden_dim, max_past); - size_t timing_input = gb->input({num_positions, hidden_dim}, Precision::FP32); - gb->set_input(timing_input, timing_signal.data(), Precision::FP32); - size_t timing_fp16 = gb->precision_cast(timing_input, Precision::FP16); - - size_t front_pad_len = (seq_len >= num_positions) ? seq_len - num_positions : 0; - size_t back_pad_len = seq_len > 0 ? seq_len - 1 : 0; - - return { - timing_fp16, - create_zero_pad_node(gb, front_pad_len, num_heads, head_dim), - create_zero_pad_node(gb, back_pad_len, num_heads, head_dim), - seq_len - }; -} - -size_t Gemma4AudioModel::build_conformer_ffw(CactusGraph* gb, size_t input, uint32_t layer_idx, - bool is_end, ComputeBackend backend) { - const auto& layer = audio_weights_.layers[layer_idx]; - float residual_weight = config_.audio_residual_weight; - float eps = config_.audio_rms_norm_eps; - - size_t residual = input; - - size_t pre_norm = is_end ? layer.ffw_end_pre_norm : layer.ffw_start_pre_norm; - size_t post_norm = is_end ? layer.ffw_end_post_norm : layer.ffw_start_post_norm; - size_t w1 = is_end ? layer.ffw_end_1 : layer.ffw_start_1; - size_t w2 = is_end ? layer.ffw_end_2 : layer.ffw_start_2; - - auto& c1 = is_end ? layer.ffw_end_1_clip : layer.ffw_start_1_clip; - auto& c2 = is_end ? layer.ffw_end_2_clip : layer.ffw_start_2_clip; - - size_t x = gb->rms_norm(input, pre_norm, eps); - - x = clipped_matmul(gb, x, w1, c1.in_min, c1.in_max, c1.out_min, c1.out_max, backend); - x = gb->silu(x); - x = clipped_matmul(gb, x, w2, c2.in_min, c2.in_max, c2.out_min, c2.out_max, backend); - - x = gb->rms_norm(x, post_norm, eps); - - x = gb->scalar_multiply(x, residual_weight); - return gb->add(residual, x); -} - -size_t Gemma4AudioModel::build_conformer_attention(CactusGraph* gb, size_t input, uint32_t layer_idx, - const ConformerContext& ctx, ComputeBackend backend) { - const auto& layer = audio_weights_.layers[layer_idx]; - float eps = config_.audio_rms_norm_eps; - size_t num_heads = config_.audio_num_heads; - size_t head_dim = config_.audio_head_dim; - size_t hidden_dim = num_heads * head_dim; - float logit_cap = config_.audio_logit_cap; - size_t max_past = config_.audio_context_left > 0 ? config_.audio_context_left - 1 : 0; - size_t num_positions = max_past + config_.audio_context_right + 1; - size_t seq_len = ctx.seq_len; - - size_t residual = input; - - size_t x = gb->rms_norm(input, layer.attn_pre_norm, eps); - - size_t q = clipped_matmul(gb, x, layer.attn_q, layer.attn_q_clip.in_min, layer.attn_q_clip.in_max, layer.attn_q_clip.out_min, layer.attn_q_clip.out_max, backend); - size_t k = clipped_matmul(gb, x, layer.attn_k, layer.attn_k_clip.in_min, layer.attn_k_clip.in_max, layer.attn_k_clip.out_min, layer.attn_k_clip.out_max, backend); - size_t v = clipped_matmul(gb, x, layer.attn_v, layer.attn_v_clip.in_min, layer.attn_v_clip.in_max, layer.attn_v_clip.out_min, layer.attn_v_clip.out_max, backend); - - float q_scale = (1.0f / std::sqrt(static_cast(head_dim))) * INV_LN2; - size_t q_dim_scale_fp16 = gb->precision_cast(layer.attn_per_dim_scale, Precision::FP16); - size_t q_dim_scale = gb->scalar_exp(q_dim_scale_fp16); - q_dim_scale = gb->scalar_add(q_dim_scale, 1.0f); - q_dim_scale = gb->scalar_log(q_dim_scale); - q_dim_scale = gb->scalar_multiply(q_dim_scale, q_scale); - - size_t q_flat = gb->reshape(q, {seq_len * num_heads, head_dim}); - q_flat = gb->multiply(q_flat, q_dim_scale); - q = gb->reshape(q_flat, {seq_len, hidden_dim}); - - k = gb->scalar_multiply(k, GEMMA4_AUDIO_K_SCALE); - - size_t sin_emb = gb->matmul(ctx.timing_fp16, layer.attn_rel_pos_proj, true, backend); - size_t sin_emb_3d = gb->reshape(sin_emb, {num_positions, num_heads, head_dim}); - size_t R = 2 * seq_len - 1; - - size_t rel_key; - if (seq_len >= num_positions) { - rel_key = (ctx.front_pad != 0) ? gb->concat(ctx.front_pad, sin_emb_3d, 0) : sin_emb_3d; - if (ctx.back_pad != 0) - rel_key = gb->concat(rel_key, ctx.back_pad, 0); - } else { - size_t start_idx = num_positions - seq_len; - size_t sin_emb_sliced = gb->slice(sin_emb_3d, 0, start_idx, seq_len); - rel_key = (ctx.back_pad != 0) ? gb->concat(sin_emb_sliced, ctx.back_pad, 0) : sin_emb_sliced; - } - - size_t rel_key_4d = gb->reshape(rel_key, {1, R, num_heads, head_dim}); - size_t q4 = gb->reshape(q, {1, seq_len, num_heads, head_dim}); - size_t rel_bias = gb->rel_pos_bias(q4, rel_key_4d, 1.0f); - - size_t k4 = gb->reshape(k, {1, seq_len, num_heads, head_dim}); - size_t v4 = gb->reshape(v, {1, seq_len, num_heads, head_dim}); - - size_t attn = gb->attention_masked(q4, k4, v4, rel_bias, 1.0f, - true, backend, true, 0, max_past, logit_cap); - - size_t attn_out = gb->reshape(attn, {seq_len, hidden_dim}); - - size_t out = clipped_matmul(gb, attn_out, layer.attn_post, layer.attn_post_clip.in_min, layer.attn_post_clip.in_max, layer.attn_post_clip.out_min, layer.attn_post_clip.out_max, backend); - out = gb->rms_norm(out, layer.attn_post_norm, eps); - - return gb->add(residual, out); -} - -size_t Gemma4AudioModel::build_conformer_lconv1d(CactusGraph* gb, size_t input, uint32_t layer_idx, - ComputeBackend backend) { - const auto& layer = audio_weights_.layers[layer_idx]; - float eps = config_.audio_rms_norm_eps; - size_t kernel_size = config_.audio_conf_conv_kernel_size; - size_t hidden_dim = config_.audio_hidden_dim; - - size_t residual = input; - - size_t x = gb->rms_norm(input, layer.lconv_pre_norm, eps); - - x = clipped_matmul(gb, x, layer.lconv_start, layer.lconv_start_clip.in_min, layer.lconv_start_clip.in_max, layer.lconv_start_clip.out_min, layer.lconv_start_clip.out_max, backend); - - x = gb->glu(x, -1); - - const auto& x_buf = gb->get_output_buffer(x); - size_t seq_len = x_buf.shape[0]; - x = gb->reshape(x, {1, seq_len, hidden_dim}); - - x = gb->conv1d_causal(x, layer.lconv_depthwise, kernel_size); - - x = gb->reshape(x, {seq_len, hidden_dim}); - - x = gb->rms_norm(x, layer.lconv_conv_norm, eps); - x = gb->silu(x); - - x = clipped_matmul(gb, x, layer.lconv_end, layer.lconv_end_clip.in_min, layer.lconv_end_clip.in_max, layer.lconv_end_clip.out_min, layer.lconv_end_clip.out_max, backend); - - return gb->add(x, residual); -} - -size_t Gemma4AudioModel::build_conformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - const ConformerContext& ctx, ComputeBackend backend) { - hidden = build_conformer_ffw(gb, hidden, layer_idx, false, backend); - hidden = build_conformer_attention(gb, hidden, layer_idx, ctx, backend); - hidden = build_conformer_lconv1d(gb, hidden, layer_idx, backend); - hidden = build_conformer_ffw(gb, hidden, layer_idx, true, backend); - return gb->rms_norm(hidden, audio_weights_.layers[layer_idx].block_norm, config_.audio_rms_norm_eps); -} - -size_t Gemma4AudioModel::forward_audio(CactusGraph* gb, const std::vector& mel_features, - size_t num_frames, ComputeBackend backend) { - if (use_npu_encoder_ && npu_encoder_ && npu_encoder_->is_available()) { - std::vector npu_input_shape = npu_encoder_->get_input_shape(); - size_t mel_bins = static_cast(config_.audio_input_feat_size); - size_t max_npu_frames = infer_npu_audio_max_frames(npu_input_shape, mel_bins); - size_t copy_frames = max_npu_frames > 0 ? std::min(num_frames, max_npu_frames) : num_frames; - size_t input_values = copy_frames * mel_bins; - - if (mel_features.size() < input_values) { - CACTUS_LOG_WARN("npu", "[gemma4-audio] insufficient mel input values; falling back to CPU audio encoder"); - } else if (!pack_gemma4_audio_for_npu(mel_features, copy_frames, mel_bins, npu_input_shape, - npu_audio_input_scratch_)) { - CACTUS_LOG_WARN("npu", "[gemma4-audio] unsupported NPU input shape; falling back to CPU audio encoder"); - } else { - size_t t1 = (copy_frames + 1) / 2; - size_t t2 = (t1 + 1) / 2; - size_t expected_out_dim = config_.audio_output_proj_dims > 0 - ? static_cast(config_.audio_output_proj_dims) - : static_cast(config_.audio_hidden_dim); - - std::vector out_shape = npu_encoder_->get_output_shape(); - size_t out_elements = npu_encoder_->get_output_buffer_size(); - if (out_elements == 0) out_elements = shape_elements(out_shape); - if (out_elements == 0) out_elements = std::max(1, t2 * expected_out_dim); - - if (npu_audio_output_scratch_.size() < out_elements) - npu_audio_output_scratch_.resize(out_elements); - - size_t written = npu_encoder_->encode( - npu_audio_input_scratch_.data(), npu_audio_output_scratch_.data(), npu_input_shape, - GEMMA4_AUDIO_NPU_INPUT_NAME, ""); - - NPUAudioOutputLayout out_layout; - if (written > 0 && - infer_npu_audio_output_layout(out_shape, written, expected_out_dim, out_layout) && - out_layout.hidden_dim == expected_out_dim) { - const __fp16* src = npu_audio_output_scratch_.data(); - __fp16* cached_output = npu_encoder_->get_output_buffer(); - size_t cached_count = npu_encoder_->get_output_buffer_size(); - size_t required = out_layout.time_steps * out_layout.hidden_dim; - if (cached_output != nullptr && cached_count >= required) { - src = cached_output; - } - - size_t actual_time = std::min(t2, out_layout.time_steps); - if (actual_time > 0) { - const __fp16* final_src = src; - if (out_layout.hidden_axis != (out_layout.dims.size() - 1)) { - if (!materialize_npu_audio_time_major(src, out_layout, actual_time, - npu_audio_reorder_scratch_)) { - CACTUS_LOG_WARN("npu", "[gemma4-audio] failed to reorder NPU output layout; falling back to CPU audio encoder"); - } else { - final_src = npu_audio_reorder_scratch_.data(); - } - } - - if (final_src != nullptr) { - size_t hidden = gb->input({actual_time, out_layout.hidden_dim}, Precision::FP16); - gb->set_input(hidden, final_src, Precision::FP16); - return hidden; - } - } - } - - if (written > 0 && out_layout.hidden_dim != expected_out_dim) { - CACTUS_LOG_WARN("npu", "[gemma4-audio] NPU output dim mismatch; falling back to CPU audio encoder"); - } else { - CACTUS_LOG_WARN("npu", "audio encode failed, falling back to CPU"); - } - } - } - - size_t hidden = build_sscp(gb, mel_features, num_frames, backend); - ConformerContext ctx = build_conformer_context(gb, hidden); - for (uint32_t i = 0; i < config_.audio_num_layers; i++) - hidden = build_conformer_block(gb, hidden, i, ctx, backend); - - if (config_.audio_output_proj_dims > 0) { - hidden = gb->matmul(hidden, audio_weights_.output_proj, true, backend); - hidden = gb->add(hidden, audio_weights_.output_proj_bias); - } - - return hidden; -} - -size_t Gemma4AudioModel::build_audio_projector(CactusGraph* gb, size_t audio_features, ComputeBackend backend) { - size_t normed = gb->rms_norm(audio_features, audio_proj_norm_ones_node_, config_.audio_rms_norm_eps); - size_t projected = gb->matmul(normed, audio_weights_.embed_audio_proj, true, backend); - return gb->scalar_multiply(projected, 1.0f / 16.0f); -} - -} -} diff --git a/cactus/models/gemma4/model_gemma4_mm.cpp b/cactus/models/gemma4/model_gemma4_mm.cpp deleted file mode 100644 index ab825dfb8..000000000 --- a/cactus/models/gemma4/model_gemma4_mm.cpp +++ /dev/null @@ -1,473 +0,0 @@ -#include "model_gemma4.h" -#include "../../graph/graph.h" -#include -#include -#include - -namespace cactus { -namespace engine { - -Gemma4MmModel::Gemma4MmModel() : Model() { - config_.model_type = Config::ModelType::GEMMA4; -} - -Gemma4MmModel::Gemma4MmModel(const Config& config) - : Model(config), vision_encoder_(config), audio_encoder_(config), language_model_(config) {} - -bool Gemma4MmModel::init(const std::string& model_folder, size_t context_size, - const std::string& system_prompt, bool do_warmup) { - if (!Model::init(model_folder, context_size, system_prompt, false)) - return false; - - auto* shared_graph = static_cast(graph_handle_); - if (!shared_graph) - throw std::runtime_error("Shared graph was not initialized for Gemma4MmModel"); - - bool has_vision = config_.vision_num_layers > 0 || config_.vision_embed_dim > 0; - bool has_audio = config_.audio_num_layers > 0 || config_.audio_hidden_dim > 0; - - if (has_vision) { - if (!vision_encoder_.init(shared_graph, model_folder, context_size, "", false)) - throw std::runtime_error("Failed to initialize vision encoder"); - } - - if (has_audio) { - if (!audio_encoder_.init(shared_graph, model_folder, context_size, "", false)) - throw std::runtime_error("Failed to initialize audio encoder"); - } - - if (!language_model_.init(shared_graph, model_folder, context_size, system_prompt, false)) - throw std::runtime_error("Failed to initialize language model"); - - output_weight_node_id_ = language_model_.output_weight_node_id_; - - if (do_warmup) { - std::vector warmup_tokens = {2}; - language_model_.forward(warmup_tokens); - auto* gb2 = static_cast(language_model_.graph_handle_); - gb2->execute(); - language_model_.reset_cache(); - } - - return true; -} - -void Gemma4MmModel::reset_cache() { - Model::reset_cache(); - language_model_.reset_cache(); -} - -void Gemma4MmModel::compact_kv_cache() { - language_model_.compact_kv_cache(); -} - -void Gemma4MmModel::remove_thinking_tokens(const std::vector>& ranges) { - language_model_.remove_thinking_tokens(ranges); -} - -void Gemma4MmModel::set_tool_constraints(const std::vector& tools) { - language_model_.set_tool_constraints(tools); -} - -void Gemma4MmModel::clear_tool_constraints() { - language_model_.clear_tool_constraints(); -} - -void Gemma4MmModel::update_tool_constraints(uint32_t token_id) { - language_model_.update_tool_constraints(token_id); -} - -void Gemma4MmModel::load_weights_to_graph(CactusGraph*) { - output_weight_node_id_ = 0; -} - -Gemma4MmModel::ForwardResult Gemma4MmModel::forward_multimodal( - CactusGraph* gb, const std::vector& tokens, - const std::vector& image_paths, - const std::vector* audio_features, - size_t audio_num_frames, - ComputeBackend backend, bool use_cache) { - - auto inputs = build_multimodal_inputs( - gb, tokens, image_paths, audio_features, audio_num_frames, backend); - - size_t final_hidden = language_model_.forward_from_embeddings( - gb, - inputs.hidden_node, - inputs.pli_hidden_source_node, - inputs.pli_tokens, - inputs.seq_len, - backend, - use_cache); - - return ForwardResult{final_hidden, inputs.seq_len}; -} - -Gemma4MmModel::MultimodalInputs Gemma4MmModel::build_multimodal_inputs( - CactusGraph* gb, const std::vector& tokens, - const std::vector& image_paths, - const std::vector* audio_features, - size_t audio_num_frames, - ComputeBackend backend) { - - size_t vision_soft_node = 0; - size_t num_vision_soft_tokens = 0; - size_t audio_soft_node = 0; - size_t num_audio_soft_tokens = 0; - - if (!image_paths.empty()) { - auto preprocessed = vision_encoder_.preprocess_image(image_paths[0]); - size_t vision_output = vision_encoder_.forward_vision(gb, preprocessed, backend); - vision_soft_node = vision_encoder_.build_vision_projector(gb, vision_output, backend); - uint32_t k = config_.vision_pooling_kernel_size; - num_vision_soft_tokens = (preprocessed.patch_width / k) * (preprocessed.patch_height / k); - } - - if (audio_features && !audio_features->empty()) { - size_t audio_output = audio_encoder_.forward_audio(gb, *audio_features, audio_num_frames, backend); - audio_soft_node = audio_encoder_.build_audio_projector(gb, audio_output, backend); - const auto& audio_buf = gb->get_output_buffer(audio_soft_node); - num_audio_soft_tokens = audio_buf.shape[0]; - } - - uint32_t image_token_id = config_.image_token_id; - uint32_t audio_token_id = config_.audio_token_id; - uint32_t pad_token_id = config_.pad_token_id; - - std::vector sequence_nodes; - std::vector current_text; - std::vector pli_tokens; - size_t total_seq_len = 0; - size_t vision_offset = 0; - size_t audio_offset = 0; - - auto flush_text = [&]() { - if (current_text.empty()) return; - size_t seg_len = current_text.size(); - size_t input_node = gb->input({seg_len}, Precision::FP32); - - auto hidden = gb->scalar_multiply( - gb->embedding(language_model_.embedding_node_id_, input_node), - std::sqrt(static_cast(config_.hidden_dim))); - - std::vector input_data(seg_len); - for (size_t i = 0; i < seg_len; i++) - input_data[i] = static_cast(current_text[i]); - gb->set_input(input_node, input_data.data(), Precision::FP32); - - sequence_nodes.push_back(hidden); - for (auto t : current_text) - pli_tokens.push_back(t); - total_seq_len += seg_len; - current_text.clear(); - }; - - auto append_soft_region = [&](size_t soft_node, size_t& soft_offset, size_t total_soft_tokens, - size_t placeholder_count) { - size_t to_insert = std::min(placeholder_count, total_soft_tokens - soft_offset); - if (to_insert > 0) { - sequence_nodes.push_back(gb->slice(soft_node, 0, soft_offset, to_insert)); - for (size_t j = 0; j < to_insert; j++) - pli_tokens.push_back(pad_token_id); - total_seq_len += to_insert; - soft_offset += to_insert; - } - }; - - auto flush_vision_region = [&](size_t placeholder_count) { - append_soft_region(vision_soft_node, vision_offset, num_vision_soft_tokens, placeholder_count); - }; - - auto flush_audio_region = [&](size_t placeholder_count) { - append_soft_region(audio_soft_node, audio_offset, num_audio_soft_tokens, placeholder_count); - }; - - bool in_image_region = false; - bool in_audio_region = false; - size_t region_count = 0; - - for (size_t i = 0; i < tokens.size(); i++) { - uint32_t tok = tokens[i]; - bool is_vision_token = (tok == image_token_id && image_token_id != 0); - bool is_audio_token = (tok == audio_token_id && audio_token_id != 0); - - if (is_vision_token) { - if (in_audio_region) { - flush_audio_region(region_count); - in_audio_region = false; - } - if (!in_image_region) { - flush_text(); - in_image_region = true; - region_count = 0; - } - region_count++; - } else if (is_audio_token) { - if (in_image_region) { - flush_vision_region(region_count); - in_image_region = false; - } - if (!in_audio_region) { - flush_text(); - in_audio_region = true; - region_count = 0; - } - region_count++; - } else { - if (in_image_region) { - flush_vision_region(region_count); - in_image_region = false; - } - if (in_audio_region) { - flush_audio_region(region_count); - in_audio_region = false; - } - current_text.push_back(tok); - } - } - - if (in_image_region) - flush_vision_region(region_count); - if (in_audio_region) - flush_audio_region(region_count); - flush_text(); - - if (sequence_nodes.empty()) - throw std::runtime_error("No embedding nodes built"); - - size_t merged = sequence_nodes[0]; - for (size_t i = 1; i < sequence_nodes.size(); i++) - merged = gb->concat(merged, sequence_nodes[i], 0); - - return MultimodalInputs{ - .hidden_node = merged, - .pli_hidden_source_node = merged, - .pli_tokens = std::move(pli_tokens), - .seq_len = total_seq_len, - }; -} - -uint32_t Gemma4MmModel::decode_multimodal( - const std::vector& tokens, - const std::vector& image_paths, - const std::vector* audio_features, - size_t audio_num_frames, - float temperature, float top_p, size_t top_k, - const std::string& profile_file, float* out_entropy, - float min_p, float repetition_penalty) { - - if (!initialized_ || !graph_handle_) - throw std::runtime_error("Model not initialized - call init() first"); - - bool has_media = !image_paths.empty() || (audio_features && !audio_features->empty()); - - if (!has_media) { - return language_model_.decode(tokens, temperature, top_p, top_k, profile_file, out_entropy, min_p, repetition_penalty); - } - - if (temperature < 0) temperature = config_.default_temperature; - if (top_p < 0) top_p = config_.default_top_p; - if (top_k == 0) top_k = config_.default_top_k; - - auto* gb = static_cast(graph_handle_); - gb->soft_reset(); - auto backend = config_.default_backend == Config::Backend::CPU ? ComputeBackend::CPU : ComputeBackend::NPU; - - size_t cached_len = language_model_.kv_cache_.current_seq_len; - if (cached_len >= tokens.size()) { - reset_cache(); - cached_len = 0; - } - std::vector forward_tokens = cached_len == 0 - ? tokens - : std::vector(tokens.end() - (tokens.size() - cached_len), tokens.end()); - - bool delta_has_media = std::any_of(forward_tokens.begin(), forward_tokens.end(), [&](uint32_t t) { - return (config_.image_token_id != 0 && t == config_.image_token_id) || - (config_.audio_token_id != 0 && t == config_.audio_token_id); - }); - - size_t final_hidden_node = 0; - size_t seq_len_for_updates = 0; - if (delta_has_media) { - auto result = forward_multimodal(gb, forward_tokens, image_paths, audio_features, - audio_num_frames, backend, true); - final_hidden_node = result.final_hidden_node; - seq_len_for_updates = result.seq_len; - } else { - final_hidden_node = language_model_.forward(forward_tokens, true); - seq_len_for_updates = forward_tokens.size(); - } - - auto last_hidden = gb->index(final_hidden_node, seq_len_for_updates - 1, 0); - const auto& last_buf = gb->get_output_buffer(last_hidden); - last_hidden = gb->reshape(last_hidden, {1, last_buf.shape[0]}); - - auto logits_node = gb->matmul(last_hidden, language_model_.output_weight_node_id_, true, backend); - - if (config_.final_logit_softcapping > 0.0f) { - float inv_cap = 1.0f / config_.final_logit_softcapping; - logits_node = gb->scalar_multiply(logits_node, inv_cap); - logits_node = gb->tanh(logits_node); - logits_node = gb->scalar_multiply(logits_node, config_.final_logit_softcapping); - } - - size_t sampled_token = - language_model_.sample_token(gb, logits_node, temperature, top_p, top_k, min_p, repetition_penalty, nullptr); - - if (!profile_file.empty()) - gb->execute(profile_file); - else - gb->execute(); - - compute_entropy(gb, logits_node, out_entropy); - - language_model_.post_execute_updates(gb, seq_len_for_updates); - language_model_.update_kv_cache(gb, seq_len_for_updates); - - auto* output_ptr = gb->get_output(sampled_token); - uint32_t result_token = *static_cast(output_ptr); - language_model_.record_sampled_token(result_token); - return result_token; -} - -size_t Gemma4MmModel::forward(const std::vector& tokens, bool use_cache) { - return language_model_.forward(tokens, use_cache); -} - -uint32_t Gemma4MmModel::decode(const std::vector& tokens, - float temperature, float top_p, size_t top_k, - const std::string& profile_file, float* out_entropy, - float min_p, float repetition_penalty) { - if (!initialized_ || !graph_handle_) - throw std::runtime_error("Model not initialized - call init() first"); - return language_model_.decode(tokens, temperature, top_p, top_k, profile_file, out_entropy, min_p, repetition_penalty); -} - -void Gemma4MmModel::prefill(const std::vector& tokens, size_t chunk_size, - const std::string& profile_file) { - if (!initialized_ || !graph_handle_) - throw std::runtime_error("Model not initialized - call init() first"); - language_model_.prefill(tokens, chunk_size, profile_file); -} - -void Gemma4MmModel::prefill_with_images(const std::vector& tokens, - const std::vector& image_paths, - const std::string& profile_file) { - if (!initialized_ || !graph_handle_) - throw std::runtime_error("Model not initialized - call init() first"); - - if (image_paths.empty()) { - prefill(tokens, get_prefill_chunk_size(), profile_file); - return; - } - - auto* gb = static_cast(graph_handle_); - gb->soft_reset(); - auto backend = config_.default_backend == Config::Backend::CPU ? ComputeBackend::CPU : ComputeBackend::NPU; - - auto result = forward_multimodal(gb, tokens, image_paths, nullptr, 0, backend, true); - - if (!profile_file.empty()) - gb->execute(profile_file); - else - gb->execute(); - - language_model_.post_execute_updates(gb, result.seq_len); - language_model_.update_kv_cache(gb, result.seq_len); -} - -uint32_t Gemma4MmModel::decode_with_images( - const std::vector& tokens, const std::vector& image_paths, - float temperature, float top_p, size_t top_k, - const std::string& profile_file, float* out_entropy, - float min_p, float repetition_penalty) { - return decode_multimodal(tokens, image_paths, nullptr, 0, - temperature, top_p, top_k, profile_file, out_entropy, - min_p, repetition_penalty); -} - -uint32_t Gemma4MmModel::decode_with_audio( - const std::vector& tokens, const std::vector& audio_features, - float temperature, float top_p, size_t top_k, - const std::string& profile_file, float* out_entropy, - float min_p, float repetition_penalty, - float* /*out_token_time_start*/, float* /*out_token_time_end*/) { - size_t num_frames = audio_features.size() / config_.audio_input_feat_size; - std::vector empty_images; - return decode_multimodal(tokens, empty_images, &audio_features, num_frames, - temperature, top_p, top_k, profile_file, out_entropy, - min_p, repetition_penalty); -} - -uint32_t Gemma4MmModel::decode_with_media( - const std::vector& tokens, - const std::vector& image_paths, - const std::vector& audio_features, - float temperature, float top_p, size_t top_k, - const std::string& profile_file, float* out_entropy, - float min_p, float repetition_penalty) { - size_t num_frames = audio_features.size() / config_.audio_input_feat_size; - return decode_multimodal(tokens, image_paths, &audio_features, num_frames, - temperature, top_p, top_k, profile_file, out_entropy, - min_p, repetition_penalty); -} - -std::vector Gemma4MmModel::get_image_embeddings(const std::string& image_path) { - if (!initialized_ || !graph_handle_) - throw std::runtime_error("Model not initialized - call init() first"); - auto* gb = static_cast(graph_handle_); - gb->soft_reset(); - auto backend = config_.default_backend == Config::Backend::CPU ? ComputeBackend::CPU : ComputeBackend::NPU; - - auto preprocessed = vision_encoder_.preprocess_image(image_path); - size_t vision_output = vision_encoder_.forward_vision(gb, preprocessed, backend); - size_t projected = vision_encoder_.build_vision_projector(gb, vision_output, backend); - - gb->execute(); - - const auto& buf = gb->get_output_buffer(projected); - size_t total = buf.total_size; - std::vector embedding(total); - const __fp16* fp16_data = buf.data_as<__fp16>(); - for (size_t i = 0; i < total; i++) - embedding[i] = static_cast(fp16_data[i]); - return embedding; -} - -std::vector Gemma4MmModel::get_audio_embeddings(const std::vector& audio_features) { - if (!initialized_ || !graph_handle_) - throw std::runtime_error("Model not initialized - call init() first"); - auto* gb = static_cast(graph_handle_); - gb->soft_reset(); - auto backend = config_.default_backend == Config::Backend::CPU ? ComputeBackend::CPU : ComputeBackend::NPU; - - size_t num_frames = audio_features.size() / config_.audio_input_feat_size; - size_t audio_output = audio_encoder_.forward_audio(gb, audio_features, num_frames, backend); - size_t projected = audio_encoder_.build_audio_projector(gb, audio_output, backend); - - gb->execute(); - - const auto& buf = gb->get_output_buffer(projected); - size_t total = buf.total_size; - std::vector embedding(total); - const __fp16* fp16_data = buf.data_as<__fp16>(); - for (size_t i = 0; i < total; i++) - embedding[i] = static_cast(fp16_data[i]); - return embedding; -} - -size_t Gemma4MmModel::build_attention(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) { - throw std::runtime_error("build_attention should not be called directly on Gemma4MmModel"); -} - -size_t Gemma4MmModel::build_mlp(CactusGraph*, size_t, uint32_t, ComputeBackend) const { - throw std::runtime_error("build_mlp should not be called directly on Gemma4MmModel"); -} - -size_t Gemma4MmModel::build_transformer_block(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) { - throw std::runtime_error("build_transformer_block should not be called directly on Gemma4MmModel"); -} - -} -} diff --git a/cactus/models/gemma4/model_gemma4_vision.cpp b/cactus/models/gemma4/model_gemma4_vision.cpp deleted file mode 100644 index 33043db1a..000000000 --- a/cactus/models/gemma4/model_gemma4_vision.cpp +++ /dev/null @@ -1,565 +0,0 @@ -#include "model_gemma4.h" -#include "../../graph/graph.h" -#include -#include -#include -#include -#include -#include - -extern "C" { - #include "../../../libs/stb/stb_image.h" - #include "../../../libs/stb/stb_image_resize2.h" -} - -namespace cactus { -namespace engine { - -static std::pair, std::vector> compute_2d_rope_tables( - const Gemma4VisionModel::PreprocessedImage& img, size_t max_patches, size_t head_dim, float theta) { - - size_t half_dim = head_dim / 2; - size_t freq_per_dim = half_dim / 2; - - std::vector cos_table(max_patches * head_dim, 1.0f); - std::vector sin_table(max_patches * head_dim, 0.0f); - - for (size_t py = 0; py < img.patch_height; py++) { - for (size_t px = 0; px < img.patch_width; px++) { - size_t patch_idx = py * img.patch_width + px; - - for (size_t i = 0; i < freq_per_dim; i++) { - float inv_freq = 1.0f / std::pow(theta, 2.0f * i / static_cast(half_dim)); - - float x_angle = static_cast(px) * inv_freq; - cos_table[patch_idx * head_dim + i] = std::cos(x_angle); - cos_table[patch_idx * head_dim + freq_per_dim + i] = std::cos(x_angle); - sin_table[patch_idx * head_dim + i] = std::sin(x_angle); - sin_table[patch_idx * head_dim + freq_per_dim + i] = std::sin(x_angle); - - float y_angle = static_cast(py) * inv_freq; - cos_table[patch_idx * head_dim + half_dim + i] = std::cos(y_angle); - cos_table[patch_idx * head_dim + half_dim + freq_per_dim + i] = std::cos(y_angle); - sin_table[patch_idx * head_dim + half_dim + i] = std::sin(y_angle); - sin_table[patch_idx * head_dim + half_dim + freq_per_dim + i] = std::sin(y_angle); - } - } - } - - return {cos_table, sin_table}; -} - -static std::pair, std::vector<__fp16>> compute_npu_rope_tables( - const Gemma4VisionModel::PreprocessedImage& img, size_t max_patches, size_t head_dim, float theta) { - - auto [cos_f32, sin_f32] = compute_2d_rope_tables(img, max_patches, head_dim, theta); - - std::vector<__fp16> cos_full(max_patches * head_dim); - std::vector<__fp16> sin_signed(max_patches * head_dim); - - for (size_t p = 0; p < max_patches; p++) { - size_t base = p * head_dim; - for (size_t d = 0; d < head_dim; d++) { - cos_full[base + d] = static_cast<__fp16>(cos_f32[base + d]); - sin_signed[base + d] = static_cast<__fp16>(sin_f32[base + d]); - } - } - - return {cos_full, sin_signed}; -} - -Gemma4VisionModel::Gemma4VisionModel() : Model() {} -Gemma4VisionModel::Gemma4VisionModel(const Config& config) : Model(config) {} - -void Gemma4VisionModel::load_weights_to_graph(CactusGraph* gb) { - auto resolve = [&](const std::string& name) -> std::string { - return model_folder_path_ + "/" + name; - }; - - if (!disable_npu_ && npu::is_npu_available()) { - std::string npu_path = model_folder_path_ + "/vision_encoder.mlpackage"; - if (std::filesystem::exists(npu_path)) { - npu_encoder_ = npu::create_encoder(); - if (npu_encoder_ && npu_encoder_->load(npu_path)) { - use_npu_encoder_ = true; - std::vector input_shape = npu_encoder_->get_input_shape(); - if (input_shape.empty()) { - input_shape = {static_cast(config_.vision_default_output_length * - config_.vision_pooling_kernel_size * config_.vision_pooling_kernel_size), - static_cast(config_.vision_embed_dim)}; - } - npu_encoder_->preallocate(input_shape, "hidden_states", "output"); - } else { - use_npu_encoder_ = false; - npu_encoder_.reset(); - CACTUS_LOG_WARN("npu", "[gemma4-vision] found vision_encoder.mlpackage but failed to enable NPU vision encoder; using CPU"); - } - } else { - CACTUS_LOG_WARN("npu", "[gemma4-vision] vision_encoder.mlpackage not found; using CPU vision encoder"); - } - } else if (!disable_npu_) { - CACTUS_LOG_WARN("npu", "[gemma4-vision] NPU backend unavailable on this device; using CPU vision encoder"); - } - - vision_weights_.patch_input_proj = gb->mmap_weights(resolve("vision_patch_embedder_input_proj.weights")); - vision_weights_.position_table = gb->mmap_weights(resolve("vision_patch_embedder_position_embedding_table.weights")); - - if (!use_npu_encoder_) { - vision_weights_.layers.resize(config_.vision_num_layers); - for (uint32_t i = 0; i < config_.vision_num_layers; i++) { - auto& layer = vision_weights_.layers[i]; - std::string prefix = resolve("vision_encoder_layers_" + std::to_string(i) + "_"); - - layer.attn_q_weight = gb->mmap_weights(prefix + "self_attn_q_proj.weights"); - layer.attn_k_weight = gb->mmap_weights(prefix + "self_attn_k_proj.weights"); - layer.attn_v_weight = gb->mmap_weights(prefix + "self_attn_v_proj.weights"); - layer.attn_output_weight = gb->mmap_weights(prefix + "self_attn_o_proj.weights"); - layer.attn_q_norm = gb->mmap_weights(prefix + "self_attn_q_norm.weights"); - layer.attn_k_norm = gb->mmap_weights(prefix + "self_attn_k_norm.weights"); - layer.input_layernorm = gb->mmap_weights(prefix + "input_layernorm.weights"); - layer.post_attention_layernorm = gb->mmap_weights(prefix + "post_attention_layernorm.weights"); - layer.pre_feedforward_layernorm = gb->mmap_weights(prefix + "pre_feedforward_layernorm.weights"); - layer.post_feedforward_layernorm = gb->mmap_weights(prefix + "post_feedforward_layernorm.weights"); - layer.mlp_gate_proj = gb->mmap_weights(prefix + "mlp_gate_proj.weights"); - layer.mlp_up_proj = gb->mmap_weights(prefix + "mlp_up_proj.weights"); - layer.mlp_down_proj = gb->mmap_weights(prefix + "mlp_down_proj.weights"); - layer.layer_scalar = std::filesystem::exists(prefix + "layer_scalar.weights") ? gb->mmap_weights(prefix + "layer_scalar.weights") : 0; - } - } - - vision_weights_.embed_vision_proj = gb->mmap_weights(resolve("embed_vision_proj.weights")); - - size_t vision_head_dim = config_.vision_head_dim; - vision_v_norm_ones_.assign(vision_head_dim, static_cast<__fp16>(1.0f)); - vision_v_norm_ones_node_ = gb->input({vision_head_dim}, Precision::FP16); - gb->set_external_input(vision_v_norm_ones_node_, vision_v_norm_ones_.data(), Precision::FP16); - - vision_weights_.post_proj_norm = gb->mmap_weights(resolve("embed_vision_post_proj_norm.weights")); - - output_weight_node_id_ = 0; -} - -Gemma4VisionModel::PreprocessedImage Gemma4VisionModel::preprocess_image(const std::string& image_path) { - int w, h, channels; - unsigned char* data = stbi_load(image_path.c_str(), &w, &h, &channels, 3); - if (!data) - throw std::runtime_error("Failed to load image: " + image_path); - - uint32_t patch_size = config_.vision_patch_size; - uint32_t pooling_k = config_.vision_pooling_kernel_size; - uint32_t max_patches = config_.vision_default_output_length * pooling_k * pooling_k; - uint32_t side_mult = pooling_k * patch_size; - - float total_px = static_cast(h) * static_cast(w); - float target_px = static_cast(max_patches) * static_cast(patch_size * patch_size); - float factor = std::sqrt(target_px / total_px); - - int target_h = static_cast(std::floor(factor * h / side_mult)) * side_mult; - int target_w = static_cast(std::floor(factor * w / side_mult)) * side_mult; - - if (target_h == 0) target_h = side_mult; - if (target_w == 0) target_w = side_mult; - - std::vector resized_data; - unsigned char* src = data; - if (target_h != h || target_w != w) { - resized_data.resize(target_h * target_w * 3); - stbir_resize_uint8_linear(data, w, h, 0, resized_data.data(), target_w, target_h, 0, - static_cast(3)); - src = resized_data.data(); - } - - size_t ph = target_h / patch_size; - size_t pw = target_w / patch_size; - size_t num_patches = ph * pw; - - std::vector pixels(3 * target_h * target_w); - for (int y = 0; y < target_h; y++) { - for (int x = 0; x < target_w; x++) { - size_t src_idx = (y * target_w + x) * 3; - for (int c = 0; c < 3; c++) { - pixels[c * target_h * target_w + y * target_w + x] = - static_cast(src[src_idx + c]) * config_.rescale_factor; - } - } - } - - stbi_image_free(data); - - return PreprocessedImage{ - std::move(pixels), - static_cast(target_h), - static_cast(target_w), - ph, pw, num_patches - }; -} - -size_t Gemma4VisionModel::build_vision_patch_embedding(CactusGraph* gb, const PreprocessedImage& img, - ComputeBackend backend) { - uint32_t patch_size = config_.vision_patch_size; - size_t patch_dim = 3 * patch_size * patch_size; - size_t num_patches = img.num_patches; - size_t hidden_size = config_.vision_embed_dim; - size_t pos_embed_size = config_.vision_position_embedding_size; - - size_t pixel_input = gb->input({3, img.height, img.width}, Precision::FP32); - gb->set_input(pixel_input, img.pixel_values.data(), Precision::FP32); - - size_t pixel_fp16 = gb->precision_cast(pixel_input, Precision::FP16); - size_t reshaped = gb->reshape(pixel_fp16, {3, img.patch_height, patch_size, img.patch_width, patch_size}); - size_t permuted = gb->transposeN(reshaped, {1, 3, 2, 4, 0}); - size_t patches = gb->reshape(permuted, {num_patches, patch_dim}); - - float mean = config_.image_mean; - float norm_std = config_.image_std; - patches = gb->scalar_multiply(gb->scalar_add(patches, -mean), 1.0f / norm_std); - - size_t projected = gb->matmul(patches, vision_weights_.patch_input_proj, true, backend); - - size_t pos_table = gb->reshape(vision_weights_.position_table, {2, pos_embed_size, hidden_size}); - - std::vector x_positions(num_patches); - std::vector y_positions(num_patches); - for (size_t py = 0; py < img.patch_height; py++) { - for (size_t px = 0; px < img.patch_width; px++) { - size_t idx = py * img.patch_width + px; - x_positions[idx] = static_cast(px); - y_positions[idx] = static_cast(py); - } - } - - size_t x_pos_input = gb->input({num_patches}, Precision::FP32); - gb->set_input(x_pos_input, x_positions.data(), Precision::FP32); - size_t y_pos_input = gb->input({num_patches}, Precision::FP32); - gb->set_input(y_pos_input, y_positions.data(), Precision::FP32); - - size_t x_table = gb->slice(pos_table, 0, 0, 1); - x_table = gb->reshape(x_table, {pos_embed_size, hidden_size}); - size_t y_table = gb->slice(pos_table, 0, 1, 1); - y_table = gb->reshape(y_table, {pos_embed_size, hidden_size}); - - size_t x_embed = gb->embedding(x_table, x_pos_input); - size_t y_embed = gb->embedding(y_table, y_pos_input); - size_t pos_embed = gb->add(x_embed, y_embed); - - size_t pos_embed_fp16 = gb->precision_cast(pos_embed, Precision::FP16); - - return gb->add(projected, pos_embed_fp16); -} - -std::pair Gemma4VisionModel::build_2d_rope_nodes( - CactusGraph* gb, const PreprocessedImage& img, size_t max_patches) { - - size_t head_dim = config_.vision_head_dim; - auto rope_tables = compute_2d_rope_tables(img, max_patches, head_dim, config_.vision_rope_theta); - auto& cos_table = rope_tables.first, sin_table = rope_tables.second; - - size_t cos_input = gb->input({max_patches, head_dim}, Precision::FP32); - gb->set_input(cos_input, cos_table.data(), Precision::FP32); - size_t cos_node = gb->precision_cast(cos_input, Precision::FP16); - cos_node = gb->reshape(cos_node, {1, max_patches, 1, head_dim}); - - size_t sin_input = gb->input({max_patches, head_dim}, Precision::FP32); - gb->set_input(sin_input, sin_table.data(), Precision::FP32); - size_t sin_node = gb->precision_cast(sin_input, Precision::FP16); - sin_node = gb->reshape(sin_node, {1, max_patches, 1, head_dim}); - - return {cos_node, sin_node}; -} - -size_t Gemma4VisionModel::build_padding_mask(CactusGraph* gb, size_t num_real, size_t max_patches) { - if (num_real >= max_patches) - return 0; - - std::vector mask(max_patches * max_patches, 0.0f); - for (size_t q = 0; q < num_real; q++) - for (size_t k = 0; k < num_real; k++) - mask[q * max_patches + k] = 1.0f; - size_t mask_input = gb->input({max_patches, max_patches}, Precision::FP32); - gb->set_input(mask_input, mask.data(), Precision::FP32); - size_t mask_node = gb->precision_cast(mask_input, Precision::FP16); - return gb->reshape(mask_node, {1, max_patches, max_patches}); -} - -size_t Gemma4VisionModel::build_vision_attention(CactusGraph* gb, size_t input, uint32_t layer_idx, - size_t cos_node, size_t sin_node, - size_t attn_mask_node, ComputeBackend backend) { - const auto& layer = vision_weights_.layers[layer_idx]; - size_t head_dim = config_.vision_head_dim; - size_t num_heads = config_.vision_attention_heads; - size_t kv_heads = config_.vision_kv_heads; - - const auto& input_buf = gb->get_output_buffer(input); - size_t seq_len = input_buf.shape[0]; - - auto q = gb->matmul(input, layer.attn_q_weight, true, backend); - q = gb->reshape(q, {seq_len * num_heads, head_dim}); - q = gb->rms_norm(q, layer.attn_q_norm, config_.layer_norm_eps); - q = gb->reshape(q, {1, seq_len, num_heads, head_dim}); - - auto k = gb->matmul(input, layer.attn_k_weight, true, backend); - k = gb->reshape(k, {seq_len * kv_heads, head_dim}); - k = gb->rms_norm(k, layer.attn_k_norm, config_.layer_norm_eps); - k = gb->reshape(k, {1, seq_len, kv_heads, head_dim}); - - auto v_proj = gb->matmul(input, layer.attn_v_weight, true, backend); - auto v = gb->rms_norm(gb->reshape(v_proj, {seq_len * kv_heads, head_dim}), - vision_v_norm_ones_node_, config_.layer_norm_eps); - v = gb->reshape(v, {1, seq_len, kv_heads, head_dim}); - - size_t half_dim = head_dim / 2; - - auto apply_2d_rope = [&](size_t tensor) -> size_t { - auto first_half = gb->slice(tensor, 3, 0, half_dim); - auto second_half = gb->slice(tensor, 3, half_dim, half_dim); - - auto rot_first_a = gb->slice(first_half, 3, 0, half_dim / 2); - auto rot_first_b = gb->slice(first_half, 3, half_dim / 2, half_dim / 2); - auto neg_b = gb->scalar_multiply(rot_first_b, -1.0f); - - size_t cos_x = gb->slice(cos_node, 3, 0, half_dim); - size_t sin_x = gb->slice(sin_node, 3, 0, half_dim); - - auto first_rotated = gb->add( - gb->multiply(first_half, cos_x), - gb->multiply(gb->concat(neg_b, rot_first_a, 3), sin_x)); - - auto rot_second_a = gb->slice(second_half, 3, 0, half_dim / 2); - auto rot_second_b = gb->slice(second_half, 3, half_dim / 2, half_dim / 2); - auto neg_sb = gb->scalar_multiply(rot_second_b, -1.0f); - - size_t cos_y = gb->slice(cos_node, 3, half_dim, half_dim); - size_t sin_y = gb->slice(sin_node, 3, half_dim, half_dim); - - auto second_rotated = gb->add( - gb->multiply(second_half, cos_y), - gb->multiply(gb->concat(neg_sb, rot_second_a, 3), sin_y)); - - return gb->concat(first_rotated, second_rotated, 3); - }; - - auto q_rot = apply_2d_rope(q); - auto k_rot = apply_2d_rope(k); - - size_t attn; - if (attn_mask_node != 0) - attn = gb->attention_masked(q_rot, k_rot, v, attn_mask_node, 1.f, false, backend); - else - attn = gb->attention(q_rot, k_rot, v, 1.f, false, backend); - - return gb->matmul(gb->reshape(attn, {seq_len, num_heads * head_dim}), - layer.attn_output_weight, true, backend); -} - -size_t Gemma4VisionModel::build_vision_mlp(CactusGraph* gb, size_t input, uint32_t layer_idx, - ComputeBackend backend) { - const auto& layer = vision_weights_.layers[layer_idx]; - auto gate = gb->gelu(gb->matmul(input, layer.mlp_gate_proj, true, backend)); - auto up = gb->matmul(input, layer.mlp_up_proj, true, backend); - return gb->matmul(gb->multiply(gate, up), layer.mlp_down_proj, true, backend); -} - -size_t Gemma4VisionModel::build_vision_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - size_t cos_node, size_t sin_node, - size_t attn_mask_node, ComputeBackend backend) { - const auto& layer = vision_weights_.layers[layer_idx]; - - auto normed = gb->rms_norm(hidden, layer.input_layernorm, config_.layer_norm_eps); - auto attn_raw = build_vision_attention(gb, normed, layer_idx, cos_node, sin_node, attn_mask_node, backend); - auto attn = gb->rms_norm(attn_raw, layer.post_attention_layernorm, config_.layer_norm_eps); - auto residual = gb->add(hidden, attn); - - auto pre_mlp = gb->rms_norm(residual, layer.pre_feedforward_layernorm, config_.layer_norm_eps); - auto mlp_raw = build_vision_mlp(gb, pre_mlp, layer_idx, backend); - auto mlp = gb->rms_norm(mlp_raw, layer.post_feedforward_layernorm, config_.layer_norm_eps); - - auto out = gb->add(residual, mlp); - - if (layer.layer_scalar != 0) - out = gb->multiply(out, layer.layer_scalar); - - return out; -} - -size_t Gemma4VisionModel::build_vision_pooler(CactusGraph* gb, size_t hidden, const PreprocessedImage& img, - ComputeBackend backend) { - size_t k = config_.vision_pooling_kernel_size; - size_t output_length = config_.vision_default_output_length; - size_t num_patches = img.num_patches; - size_t k_squared = k * k; - - if (num_patches == output_length) - return hidden; - - size_t max_x = img.patch_width; - size_t max_y = img.patch_height; - - std::vector pool_weights(num_patches * output_length, 0.0f); - std::vector valid_bins(output_length, false); - for (size_t py = 0; py < max_y; py++) { - for (size_t px = 0; px < max_x; px++) { - size_t patch_idx = py * max_x + px; - size_t kx = px / k; - size_t ky = py / k; - size_t kernel_idx = kx + (max_x / k) * ky; - if (kernel_idx < output_length) { - pool_weights[kernel_idx * num_patches + patch_idx] = 1.0f / static_cast(k_squared); - valid_bins[kernel_idx] = true; - } - } - } - - size_t weights_node = gb->input({output_length, num_patches}, Precision::FP32); - gb->set_input(weights_node, pool_weights.data(), Precision::FP32); - size_t weights_fp16 = gb->precision_cast(weights_node, Precision::FP16); - - size_t pooled = gb->matmul(weights_fp16, hidden, false, backend); - - size_t valid_count = 0; - for (bool v : valid_bins) if (v) valid_count++; - - if (valid_count == output_length) - return pooled; - - std::vector valid_slices; - for (size_t i = 0; i < output_length; i++) { - if (valid_bins[i]) { - auto slice = gb->slice(pooled, 0, i, 1); - valid_slices.push_back(slice); - } - } - - size_t stripped = valid_slices[0]; - for (size_t i = 1; i < valid_slices.size(); i++) - stripped = gb->concat(stripped, valid_slices[i], 0); - - return stripped; -} - -size_t Gemma4VisionModel::forward_vision(CactusGraph* gb, const PreprocessedImage& img, ComputeBackend backend) { - size_t hidden = build_vision_patch_embedding(gb, img, backend); - - size_t num_real = img.num_patches; - size_t hidden_size = config_.vision_embed_dim; - uint32_t pooling_k = config_.vision_pooling_kernel_size; - size_t max_patches = config_.vision_default_output_length * pooling_k * pooling_k; - bool can_use_npu_path = use_npu_encoder_ && npu_encoder_ && npu_encoder_->is_available(); - if (can_use_npu_path) { - std::vector npu_input_shape = npu_encoder_->get_input_shape(); - if (npu_input_shape.size() >= 2) { - size_t a = static_cast(npu_input_shape[0]); - size_t b = static_cast(npu_input_shape[1]); - if (b == hidden_size) { - max_patches = a; - } else if (a == hidden_size) { - max_patches = b; - } else if (a > 0) { - max_patches = a; - } - } - } - if (num_real > max_patches) { - if (can_use_npu_path) { - CACTUS_LOG_WARN("npu", "[gemma4-vision] image has more patches than NPU encoder supports; falling back to CPU vision encoder"); - can_use_npu_path = false; - max_patches = config_.vision_default_output_length * pooling_k * pooling_k; - } - } - if (!can_use_npu_path) { - max_patches = num_real; - } - size_t num_padding = max_patches - num_real; - - if (num_padding > 0) { - std::vector<__fp16> zeros(num_padding * hidden_size, static_cast<__fp16>(0.0f)); - size_t pad_node = gb->input({num_padding, hidden_size}, Precision::FP16); - gb->set_input(pad_node, zeros.data(), Precision::FP16); - hidden = gb->concat(hidden, pad_node, 0); - } - - auto [cos_node, sin_node] = build_2d_rope_nodes(gb, img, max_patches); - size_t attn_mask_node = build_padding_mask(gb, num_real, max_patches); - - if (can_use_npu_path) { - gb->execute(); - - const auto& h_buf = gb->get_output_buffer(hidden); - if (h_buf.precision != Precision::FP16) - throw std::runtime_error("[gemma4-vision] expected FP16 hidden output for NPU path"); - std::vector<__fp16> hidden_fp16(max_patches * hidden_size); - memcpy(hidden_fp16.data(), h_buf.data_as<__fp16>(), max_patches * hidden_size * sizeof(__fp16)); - - size_t head_dim = config_.vision_head_dim; - auto [cos_fp16, sin_fp16] = compute_npu_rope_tables(img, max_patches, head_dim, config_.vision_rope_theta); - - std::vector<__fp16> additive_mask(max_patches * max_patches, static_cast<__fp16>(0.0f)); - if (num_padding > 0) { - for (size_t q = 0; q < max_patches; q++) - for (size_t k = 0; k < max_patches; k++) - if (q >= num_real || k >= num_real) - additive_mask[q * max_patches + k] = static_cast<__fp16>(-65504.0f); - } - - std::vector inputs = { - {"hidden_states", hidden_fp16.data(), {static_cast(max_patches), static_cast(hidden_size)}}, - {"cos_full", cos_fp16.data(), {static_cast(max_patches), 1, static_cast(head_dim)}}, - {"sin_signed", sin_fp16.data(), {static_cast(max_patches), 1, static_cast(head_dim)}}, - {"attention_mask", additive_mask.data(), {1, static_cast(max_patches), static_cast(max_patches)}}, - }; - - std::vector out_shape = npu_encoder_->get_output_shape(); - size_t out_elements = 1; - for (int d : out_shape) out_elements *= d; - size_t required = max_patches * hidden_size; - if (out_elements < required) - out_elements = required; - - std::vector<__fp16> npu_output(out_elements); - size_t written = npu_encoder_->encode_multimodal_input(inputs, npu_output.data(), "output"); - - if (written >= required) { - hidden = gb->input({max_patches, hidden_size}, Precision::FP16); - gb->set_input(hidden, npu_output.data(), Precision::FP16); - } else { - CACTUS_LOG_WARN("npu", "[gemma4-vision] encode_multimodal_input returned insufficient output, falling back to CPU"); - for (uint32_t i = 0; i < config_.vision_num_layers; i++) - hidden = build_vision_transformer_block(gb, hidden, i, cos_node, sin_node, attn_mask_node, backend); - } - } else { - for (uint32_t i = 0; i < config_.vision_num_layers; i++) { - auto [cn, sn] = build_2d_rope_nodes(gb, img, num_real); - hidden = build_vision_transformer_block(gb, hidden, i, cn, sn, 0, backend); - gb->execute(); - - const auto& out_buf = gb->get_output_buffer(hidden); - std::vector tmp(out_buf.byte_size); - std::memcpy(tmp.data(), out_buf.get_data(), out_buf.byte_size); - auto shape = out_buf.shape; - auto prec = out_buf.precision; - - const auto& lw = vision_weights_.layers[i]; - for (size_t wn : {lw.attn_q_weight, lw.attn_k_weight, lw.attn_v_weight, - lw.attn_output_weight, lw.mlp_gate_proj, lw.mlp_up_proj, lw.mlp_down_proj}) - gb->release_weight_pages(wn); - - gb->soft_reset_keep_pool(); - hidden = gb->input(shape, prec); - gb->set_input(hidden, tmp.data(), prec); - } - gb->release_weight_pages(vision_weights_.patch_input_proj); - gb->release_weight_pages(vision_weights_.position_table); - } - - if (num_padding > 0) - hidden = gb->slice(hidden, 0, 0, num_real); - - return build_vision_pooler(gb, hidden, img, backend); -} - -size_t Gemma4VisionModel::build_vision_projector(CactusGraph* gb, size_t vision_features, ComputeBackend backend) { - size_t projected = gb->matmul(vision_features, vision_weights_.embed_vision_proj, true, backend); - return gb->rms_norm(projected, vision_weights_.post_proj_norm, config_.layer_norm_eps); -} - -} -} diff --git a/cactus/models/model.h b/cactus/models/model.h deleted file mode 100644 index 2cb17a0e4..000000000 --- a/cactus/models/model.h +++ /dev/null @@ -1,1775 +0,0 @@ -#pragma once - -#include "../engine/engine.h" -#include "../npu/npu.h" - -namespace cactus { -namespace engine { - - - -class QwenModel : public Model { -public: - QwenModel(); - explicit QwenModel(const Config& config); - ~QwenModel() override = default; - -protected: - size_t build_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0) override; - - size_t build_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, - ComputeBackend backend) const override; - - size_t build_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0) override; - - size_t forward(const std::vector& tokens, bool use_cache = false) override; - void load_weights_to_graph(CactusGraph* gb) override; - -private: - struct WeightNodeIDs { - size_t output_weight; - size_t output_norm_weight; - - struct LayerWeights { - size_t attn_q_weight; - size_t attn_k_weight; - size_t attn_v_weight; - size_t attn_output_weight; - size_t input_layernorm_weight; - size_t attn_q_norm_weight; - size_t attn_k_norm_weight; - size_t pre_feedforward_layernorm_weight; - size_t post_feedforward_layernorm_weight; - size_t ffn_gate_weight; - size_t ffn_up_weight; - size_t ffn_down_weight; - size_t post_attention_layernorm_weight; - }; - - std::vector layers; - } weight_nodes_; -}; - -class Qwen3p5Model : public Model { -public: - Qwen3p5Model(); - explicit Qwen3p5Model(const Config& config); - ~Qwen3p5Model() override = default; - -protected: - size_t build_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0) override; - - size_t build_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, - ComputeBackend backend) const override; - - size_t build_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0) override; - - size_t forward(const std::vector& tokens, bool use_cache = false) override; - void load_weights_to_graph(CactusGraph* gb) override; - void post_init() override; - void post_execute_updates(CactusGraph* gb, size_t seq_len) override; - void reset_cache() override; - -private: - size_t build_gated_deltanet(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache, size_t position_offset); - - struct WeightNodeIDs { - size_t output_weight = 0; - size_t output_norm_weight = 0; - - enum class LayerType : uint8_t { ATTENTION, DELTANET }; - - struct LayerWeights { - size_t attn_q_weight = 0; - size_t attn_k_weight = 0; - size_t attn_v_weight = 0; - size_t attn_output_weight = 0; - size_t input_layernorm_weight = 0; - size_t attn_q_norm_weight = 0; - size_t attn_k_norm_weight = 0; - size_t deltanet_qkv_weight = 0; - size_t deltanet_gate_weight = 0; - size_t deltanet_beta_weight = 0; - size_t deltanet_gate_bias = 0; - size_t deltanet_beta_bias = 0; - size_t deltanet_z_weight = 0; - size_t deltanet_conv_weight = 0; - size_t ffn_gate_weight = 0; - size_t ffn_up_weight = 0; - size_t ffn_down_weight = 0; - size_t post_attention_layernorm_weight = 0; - }; - - struct LayerEntry { - LayerType type = LayerType::ATTENTION; - LayerWeights weights; - }; - - std::vector layers; - } weight_nodes_; - - ConvCache conv_cache_; - std::vector conv_cache_state_nodes_; - bool last_forward_used_cache_ = false; - size_t deltanet_total_seq_len_ = 0; - size_t deltanet_heads_ = 0; - size_t deltanet_key_dim_ = 0; - size_t deltanet_value_dim_ = 0; - size_t deltanet_state_flat_dim_ = 0; - size_t deltanet_mixed_dim_ = 0; - size_t deltanet_conv_history_len_ = 0; - size_t deltanet_conv_flat_dim_ = 0; - size_t deltanet_cache_row_dim_ = 0; -}; - - - -class GemmaModel : public Model { -public: - GemmaModel(); - explicit GemmaModel(const Config& config); - ~GemmaModel() override = default; - -protected: - size_t build_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0) override; - - size_t build_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, - ComputeBackend backend) const override; - - size_t build_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0) override; - - size_t forward(const std::vector& tokens, bool use_cache = false) override; - void load_weights_to_graph(CactusGraph* gb) override; - void post_init() override; - -private: - struct WeightNodeIDs { - size_t output_weight; - size_t output_norm_weight; - - struct LayerWeights { - size_t attn_q_weight; - size_t attn_k_weight; - size_t attn_v_weight; - size_t attn_output_weight; - size_t input_layernorm_weight; - size_t attn_q_norm_weight; - size_t attn_k_norm_weight; - size_t pre_feedforward_layernorm_weight; - size_t post_feedforward_layernorm_weight; - size_t ffn_gate_weight; - size_t ffn_up_weight; - size_t ffn_down_weight; - size_t post_attention_layernorm_weight; - }; - - std::vector layers; - } weight_nodes_; -}; - -class GemmaModel3n : public Model { -public: - GemmaModel3n(); - explicit GemmaModel3n(const Config& config); - ~GemmaModel3n() override = default; - -protected: - size_t build_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0) override; - - size_t build_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, - ComputeBackend backend) const override; - - size_t build_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0) override; - - size_t forward(const std::vector& tokens, bool use_cache = false) override; - void prefill(const std::vector& tokens, size_t chunk_size = 256, const std::string& profile_file = "") override; - void load_weights_to_graph(CactusGraph* gb) override; - void post_init() override; - std::vector get_kv_layer_dims() const override; - -private: - size_t forward_split(const std::vector& tokens, bool use_cache); - - size_t build_preamble(CactusGraph* gb, size_t seq_len, ComputeBackend backend, - size_t& token_input, size_t& pli_input, size_t* streams); - void build_layer(CactusGraph* gb, uint32_t layer_idx, ComputeBackend backend, - bool use_cache, size_t pos_offset, size_t pli, size_t* streams); - size_t build_output_head(CactusGraph* gb, size_t* streams, ComputeBackend backend); - void set_token_inputs(CactusGraph* gb, size_t token_input, size_t pli_input, - const std::vector& tokens); - - size_t build_laurel(CactusGraph* gb, size_t normed_input, uint32_t layer_idx, ComputeBackend backend) const; - size_t build_gaussian_topk(CactusGraph* gb, size_t input, float ppf) const; - size_t build_rms_norm_no_weight(CactusGraph* gb, size_t input, size_t num_rows, size_t row_dim) const; - size_t build_magnitude_normalize(CactusGraph* gb, size_t reference, size_t target) const; - - size_t build_altup_router_modalities(CactusGraph* gb, size_t stream0, uint32_t layer_idx, ComputeBackend backend) const; - void build_altup_predict(CactusGraph* gb, size_t modalities, uint32_t layer_idx, - const size_t* streams, size_t* predictions) const; - void build_altup_correct(CactusGraph* gb, size_t activated, size_t modalities, uint32_t layer_idx, - ComputeBackend backend, const size_t* predictions, size_t* corrected) const; - void build_per_layer_input(CactusGraph* gb, size_t pli_combined, uint32_t layer_idx, - ComputeBackend backend, size_t* streams) const; - - struct WeightNodeIDs { - size_t output_weight; - size_t output_norm_weight; - - size_t altup_proj_weights[3]; - size_t altup_unembed_proj_weights[3]; - size_t embed_tokens_per_layer; - size_t per_layer_model_proj; - size_t per_layer_proj_norm; - - struct LayerWeights { - size_t attn_q_weight; - size_t attn_k_weight; - size_t attn_v_weight; - size_t attn_output_weight; - size_t input_layernorm_weight; - size_t attn_q_norm_weight; - size_t attn_k_norm_weight; - size_t pre_feedforward_layernorm_weight; - size_t post_feedforward_layernorm_weight; - size_t ffn_gate_weight; - size_t ffn_up_weight; - size_t ffn_down_weight; - size_t post_attention_layernorm_weight; - - size_t altup_router_norm; - size_t altup_prediction_coefs; - size_t altup_correction_coefs; - size_t altup_correct_output_scale; - size_t altup_modality_router; - size_t laurel_left; - size_t laurel_right; - size_t laurel_norm; - size_t per_layer_gate; - size_t per_layer_proj; - size_t post_per_layer_norm; - }; - - std::vector layers; - } weight_nodes_; - - std::vector kv_share_map_; - std::vector shared_k_nodes_; - std::vector shared_v_nodes_; - - std::vector<__fp16> v_norm_ones_weight_; - size_t v_norm_ones_node_ = 0; -}; - -class Siglip2VisionModel : public Model { - friend class Lfm2VlModel; - -public: - struct VisionEmbeddingResult { - size_t combined_embeddings; - std::vector tile_embeddings; - }; - - Siglip2VisionModel(); - explicit Siglip2VisionModel(const Config& cfg); - ~Siglip2VisionModel() override = default; - virtual size_t forward_vision(const Siglip2Preprocessor::PreprocessedImage& preprocessed_image); - virtual size_t forward_vision(CactusGraph* gb, - const Siglip2Preprocessor::PreprocessedImage& preprocessed_image, - ComputeBackend backend); - std::vector get_image_embedding(const std::string& image_path); - Siglip2Preprocessor& get_preprocessor() { return preprocessor_; } - const Siglip2Preprocessor& get_preprocessor() const { return preprocessor_; } - -protected: - VisionEmbeddingResult build_vision_embeddings(CactusGraph* gb, - const Siglip2Preprocessor::PreprocessedImage& preprocessed_image, - ComputeBackend backend); - - size_t build_vision_transformer_layer(CactusGraph* gb, size_t hidden_states, uint32_t layer_idx, - ComputeBackend backend); - - size_t build_vision_attention(CactusGraph* gb, size_t hidden_states, uint32_t layer_idx, - ComputeBackend backend); - - size_t build_vision_mlp(CactusGraph* gb, size_t hidden_states, uint32_t layer_idx, - ComputeBackend backend); - void ensure_cpu_vision_weights_loaded(CactusGraph* gb); - - void load_weights_to_graph(CactusGraph* gb) override; - size_t forward(const std::vector& tokens, bool use_cache = false) override; - size_t build_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0) override; - size_t build_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, - ComputeBackend backend) const override; - size_t build_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0) override; - -protected: - struct VisionWeightNodeIDs { - size_t patch_embedding_weight; - size_t patch_embedding_bias; - size_t position_embedding; - size_t post_layernorm_weight; - size_t post_layernorm_bias; - - struct VisionLayerWeights { - size_t attn_q_weight; - size_t attn_k_weight; - size_t attn_v_weight; - size_t attn_output_weight; - size_t attn_q_bias; - size_t attn_k_bias; - size_t attn_v_bias; - size_t attn_output_bias; - size_t layer_norm1_weight; - size_t layer_norm1_bias; - size_t layer_norm2_weight; - size_t layer_norm2_bias; - size_t mlp_fc1_weight; - size_t mlp_fc1_bias; - size_t mlp_fc2_weight; - size_t mlp_fc2_bias; - }; - - std::vector vision_layers; - } vision_weight_nodes_; - - Siglip2Preprocessor preprocessor_; - - std::unique_ptr npu_encoder_; - bool use_npu_encoder_ = false; - bool cpu_vision_weights_loaded_ = false; -}; - - -class LFM2Model : public Model { - friend class Lfm2VlModel; - -public: - LFM2Model(); - explicit LFM2Model(const Config& config); - ~LFM2Model() override = default; - - bool is_cache_empty() const; - - bool init(const std::string& model_folder, size_t context_size, const std::string& system_prompt = "", bool do_warmup = true) override; - bool init(CactusGraph* external_graph, const std::string& model_folder, size_t context_size, - const std::string& system_prompt = "", bool do_warmup = true) override; - -protected: - using Model::forward; - size_t build_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0) override; - - size_t build_conv1d(CactusGraph* gb, size_t input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache); - - size_t build_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, - ComputeBackend backend) const override; - - size_t build_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0) override; - - size_t forward(const std::vector& tokens, bool use_cache = false) override; - size_t forward(CactusGraph* gb, const std::vector& tokens, ComputeBackend backend, bool use_cache = false); - size_t forward(CactusGraph* gb, size_t input_embeddings, size_t seq_len, ComputeBackend backend, bool use_cache = false); - void post_init() override; - void post_execute_updates(CactusGraph* gb, size_t seq_len) override; - void reset_cache() override; - void load_weights_to_graph(CactusGraph* gb) override; - -private: - - struct WeightNodeIDs { - size_t output_weight; - size_t output_norm_weight; - - struct LayerWeights { - size_t attn_q_weight; - size_t attn_k_weight; - size_t attn_v_weight; - size_t attn_output_weight; - size_t attn_q_norm_weight; - size_t attn_k_norm_weight; - - size_t conv_depthwise_weight; - size_t conv_in_proj_weight; - size_t conv_out_proj_weight; - - size_t input_layernorm_weight; - size_t post_attention_layernorm_weight; - size_t ffn_gate_weight; - size_t ffn_up_weight; - size_t ffn_down_weight; - }; - - enum class LayerType : uint8_t { ATTENTION, CONV }; - - struct LayerEntry { - LayerType type; - LayerWeights weights; - }; - - std::vector layers; - } weight_nodes_; - - ConvCache conv_cache_; - std::vector conv_cache_bx_nodes_; - bool last_forward_used_cache_ = false; -}; - - -class LFM2MoEModel : public Model { -public: - LFM2MoEModel(); - explicit LFM2MoEModel(const Config& config); - ~LFM2MoEModel() override = default; - - bool is_cache_empty() const; - - bool init(const std::string& model_folder, size_t context_size, const std::string& system_prompt = "", bool do_warmup = true) override; - bool init(CactusGraph* external_graph, const std::string& model_folder, size_t context_size, - const std::string& system_prompt = "", bool do_warmup = true) override; - -protected: - using Model::forward; - size_t build_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0) override; - - size_t build_conv1d(CactusGraph* gb, size_t input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache); - - size_t build_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, - ComputeBackend backend) const override; - - size_t build_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0) override; - - size_t forward(const std::vector& tokens, bool use_cache = false) override; - size_t forward(CactusGraph* gb, const std::vector& tokens, ComputeBackend backend, bool use_cache = false); - size_t forward(CactusGraph* gb, size_t input_embeddings, size_t seq_len, ComputeBackend backend, bool use_cache = false); - void post_init() override; - void post_execute_updates(CactusGraph* gb, size_t seq_len) override; - void reset_cache() override; - void load_weights_to_graph(CactusGraph* gb) override; - -private: - bool is_dense_layer(uint32_t layer_idx) const; - - struct WeightNodeIDs { - size_t output_weight; - size_t output_norm_weight; - - struct ExpertWeights { - size_t w1_weight = 0; - size_t w3_weight = 0; - size_t w2_weight = 0; - }; - - struct LayerWeights { - size_t attn_q_weight = 0; - size_t attn_k_weight = 0; - size_t attn_v_weight = 0; - size_t attn_output_weight = 0; - size_t attn_q_norm_weight = 0; - size_t attn_k_norm_weight = 0; - - size_t conv_depthwise_weight = 0; - size_t conv_in_proj_weight = 0; - size_t conv_out_proj_weight = 0; - - size_t input_layernorm_weight = 0; - size_t post_attention_layernorm_weight = 0; - size_t ffn_gate_weight = 0; - size_t ffn_up_weight = 0; - size_t ffn_down_weight = 0; - - size_t moe_router_weight = 0; - size_t moe_expert_bias = 0; - std::vector moe_experts; - }; - - enum class LayerType : uint8_t { ATTENTION, CONV }; - - struct LayerEntry { - LayerType type; - LayerWeights weights; - }; - - std::vector layers; - } weight_nodes_; - - ConvCache conv_cache_; - std::vector conv_cache_bx_nodes_; - bool last_forward_used_cache_ = false; -}; - - -class NomicModel : public Model { -public: - NomicModel(); - explicit NomicModel(const Config& config); - ~NomicModel() override = default; - -protected: - size_t build_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0) override; - - size_t build_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, - ComputeBackend backend) const override; - - size_t build_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0) override; - - size_t forward(const std::vector& tokens, bool use_cache = false) override; - - void load_weights_to_graph(CactusGraph* gb) override; - -private: - size_t build_standard_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, - ComputeBackend backend) const; - - size_t build_moe_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, - ComputeBackend backend) const; - - struct WeightNodeIDs { - size_t embedding_layernorm_weight; - size_t embedding_layernorm_bias; - - struct LayerWeights { - size_t attn_q_weight; - size_t attn_k_weight; - size_t attn_v_weight; - size_t attn_q_bias; - size_t attn_k_bias; - size_t attn_v_bias; - size_t attn_output_weight; - size_t attn_output_bias; - size_t ffn_up_weight; - size_t ffn_up_bias; - size_t ffn_norm_1_weight; - size_t ffn_norm_1_bias; - size_t ffn_down_weight; - size_t ffn_down_bias; - size_t ffn_norm_2_weight; - size_t ffn_norm_2_bias; - size_t mlp_router_layer_weight; - size_t mlp_experts_bias; - std::vector mlp_experts_mlp1_weight; - std::vector mlp_experts_mlp2_weight; - }; - - std::vector layers; - } weight_nodes_; -}; - -class WhisperModel : public Model { -public: - WhisperModel(); - explicit WhisperModel(const Config& config); - ~WhisperModel() override = default; - -protected: - size_t build_attention(CactusGraph*, size_t, uint32_t,ComputeBackend, bool, size_t) override { - throw std::runtime_error("Whisper: build_attention unused"); - } - - size_t build_mlp(CactusGraph*, size_t, uint32_t, ComputeBackend) const override { - throw std::runtime_error("Whisper: build_mlp unused"); - } - - size_t build_transformer_block(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) override { - throw std::runtime_error("Whisper: build_transformer_block unused"); - } - - size_t forward(const std::vector& /*tokens*/, bool /*use_cache*/ = false) override { - throw std::runtime_error("Whisper requires mel+token forward()."); - } - - size_t forward(const std::vector& audio_features, const std::vector& tokens, bool use_cache = false) override; - - void run_encoder(const std::vector& audio_features); - void reset_graph_side_cache_nodes(); - - size_t run_decoder_step(const std::vector& tokens, bool use_cache, bool last_token_only); - - void load_weights_to_graph(CactusGraph* gb) override; - - size_t build_encoder_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0); - - size_t build_decoder_self_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0); - - size_t build_encoder_self_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0); - - size_t build_encoder_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, - ComputeBackend backend); - - size_t build_decoder_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, - ComputeBackend backend) const; - - size_t build_encoder_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0); - - size_t build_decoder_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0); - - size_t build_conv1d(CactusGraph* gb, size_t input); - - uint32_t decode_with_audio(const std::vector& tokens, const std::vector& audio_features, - float temperature = 0.0f, float top_p = 0.0f, size_t top_k = 0, const std::string& profile_file = "", float* out_entropy = nullptr, - float min_p = 0.15f, float repetition_penalty = 1.1f, - float* out_token_time_start = nullptr, float* out_token_time_end = nullptr) override; - - std::vector get_audio_embeddings(const std::vector& audio_features) override; - - void reset_cache() override; - -private: - struct WeightNodeIDs { - size_t output_weight; - size_t output_norm_weight; - - size_t decoder_norm_weight; - size_t decoder_norm_bias; - size_t decoder_position_embeddings_weight; - - size_t encoder_position_embeddings; - size_t encoder_conv1_weight; - size_t encoder_conv1_bias; - size_t encoder_conv2_weight; - size_t encoder_conv2_bias; - size_t encoder_conv3_weight; - size_t encoder_conv3_bias; - size_t encoder_norm_weight; - size_t encoder_norm_bias; - - size_t encoder_output; - - struct LayerWeights { - size_t decoder_output_norm_bias; - size_t decoder_output_norm_weight; - size_t decoder_position_embeddings_weight; - size_t decoder_token_embeddings_weight; - - size_t decoder_encoder_attn_q_weight; - size_t decoder_encoder_attn_k_weight; - size_t decoder_encoder_attn_v_weight; - size_t decoder_encoder_attn_q_bias; - size_t decoder_encoder_attn_v_bias; - size_t decoder_encoder_attn_output_weight; - size_t decoder_encoder_attn_output_bias; - - size_t decoder_post_encoder_layernorm_weight; - size_t decoder_post_encoder_layernorm_bias; - - size_t decoder_ffn1_weight; - size_t decoder_ffn1_bias; - size_t decoder_ffn2_weight; - size_t decoder_ffn2_bias; - - size_t decoder_post_ffn_layernorm_weight; - size_t decoder_post_ffn_layernorm_bias; - - size_t decoder_self_attn_q_weight; - size_t decoder_self_attn_k_weight; - size_t decoder_self_attn_v_weight; - size_t decoder_self_attn_q_bias; - size_t decoder_self_attn_v_bias; - size_t decoder_self_attn_output_weight; - size_t decoder_self_attn_output_bias; - - size_t decoder_post_attn_layernorm_weight; - size_t decoder_post_attn_layernorm_bias; - - //Encoder layers - size_t encoder_ffn1_weight; - size_t encoder_ffn1_bias; - size_t encoder_ffn2_weight; - size_t encoder_ffn2_bias; - - size_t encoder_post_ffn_layernorm_weight; - size_t encoder_post_ffn_layernorm_bias; - - size_t encoder_self_attn_q_weight; - size_t encoder_self_attn_k_weight; - size_t encoder_self_attn_v_weight; - size_t encoder_self_attn_q_bias; - size_t encoder_self_attn_v_bias; - size_t encoder_self_attn_output_weight; - size_t encoder_self_attn_output_bias; - - size_t encoder_post_attn_layernorm_weight; - size_t encoder_post_attn_layernorm_bias; - }; - - std::vector layers; - } weight_nodes_; - - bool encoder_ready_ = false; - size_t last_new_tokens_; - size_t last_conv1_node_ = 0; - size_t last_conv2_node_ = 0; - size_t last_encoder_post_norm_node_ = 0; - size_t last_enc_plus_pos_node_ = 0; - size_t encoder_transformer_block_0 = 0; - size_t encoder_pre_gelu = 0; - size_t encoder_post_gelu = 0; - - std::vector encoder_block_out_nodes_; - - size_t encoder_output_persistent_ = 0; - - std::vector suppress_tokens_ = { - 1, - 2, - 7, - 8, - 9, - 10, - 14, - 25, - 26, - 27, - 28, - 29, - 31, - 58, - 59, - 60, - 61, - 62, - 63, - 90, - 91, - 92, - 93, - 359, - 503, - 522, - 542, - 873, - 893, - 902, - 918, - 922, - 931, - 1350, - 1853, - 1982, - 2460, - 2627, - 3246, - 3253, - 3268, - 3536, - 3846, - 3961, - 4183, - 4667, - 6585, - 6647, - 7273, - 9061, - 9383, - 10428, - 10929, - 11938, - 12033, - 12331, - 12562, - 13793, - 14157, - 14635, - 15265, - 15618, - 16553, - 16604, - 18362, - 18956, - 20075, - 21675, - 22520, - 26130, - 26161, - 26435, - 28279, - 29464, - 31650, - 32302, - 32470, - 36865, - 42863, - 47425, - 49870, - 50254, - 50258, - 50358, - 50359, - 50360, - 50361, - 50362 - }; - - std::vector begin_suppress_tokens_ = { - 220, - 50257 - }; - - bool first_decode_step_ = true; - - std::unordered_map suppress_bias_; - std::unordered_map suppress_bias_first_step_; - - std::vector encoder_k_persistent_; - std::vector encoder_v_persistent_; - - std::unique_ptr npu_encoder_; - bool use_npu_encoder_ = false; - - uint32_t enc_layers_ = 0; - uint32_t dec_layers_ = 0; -}; - - -class MoonshineModel : public Model { -public: - MoonshineModel(); - explicit MoonshineModel(const Config& config); - ~MoonshineModel() override = default; - -protected: - size_t build_attention(CactusGraph*, size_t, uint32_t,ComputeBackend, bool, size_t) override { - throw std::runtime_error("Moonshine: build_attention unused"); - } - - size_t build_mlp(CactusGraph*, size_t, uint32_t, ComputeBackend) const override { - throw std::runtime_error("Moonshine: build_mlp unused"); - } - - size_t build_transformer_block(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) override { - throw std::runtime_error("Moonshine: build_transformer_block unused"); - } - - size_t forward(const std::vector& /*tokens*/, bool /*use_cache*/ = false) override { - throw std::runtime_error("Moonshine requires mel+token forward()."); - } - - size_t forward(const std::vector& audio_features, const std::vector& tokens, bool use_cache = false) override; - - size_t build_encoder(CactusGraph* gb, const std::vector& audio_features); - - void reset_graph_side_cache_nodes(); - - size_t build_decoder(const std::vector& tokens, bool use_cache, bool last_token_only); - - void load_weights_to_graph(CactusGraph* gb) override; - - size_t build_decoder_cross_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0); - - size_t build_decoder_self_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0); - - size_t build_encoder_self_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0); - - size_t build_encoder_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, - ComputeBackend backend); - - size_t build_decoder_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, - ComputeBackend backend) const; - - size_t build_encoder_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0); - - size_t build_decoder_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0); - - size_t build_conv1d(CactusGraph* gb, size_t input); - size_t build_audio_preprocessor(CactusGraph* gb, size_t input); - - uint32_t decode_with_audio(const std::vector& tokens, const std::vector& audio_features, - float temperature = 0.0f, float top_p = 0.0f, size_t top_k = 0, const std::string& profile_file = "", float* out_entropy = nullptr, - float min_p = 0.15f, float repetition_penalty = 1.1f, - float* out_token_time_start = nullptr, float* out_token_time_end = nullptr) override; - - std::vector get_audio_embeddings(const std::vector& audio_features) override; - - void reset_cache() override; - -private: - struct WeightNodeIDs { - size_t output_weight; - - size_t decoder_norm_weight; - - size_t encoder_conv1_weight; - size_t encoder_conv2_weight; - size_t encoder_conv2_bias; - size_t encoder_conv3_weight; - size_t encoder_conv3_bias; - size_t encoder_norm_weight; - size_t encoder_norm_bias; - size_t encoder_layer_norm_weight; - - - struct LayerWeights { - //Decoder layers - - size_t decoder_encoder_attn_q_weight; - size_t decoder_encoder_attn_k_weight; - size_t decoder_encoder_attn_v_weight; - size_t decoder_encoder_attn_output_weight; - - size_t decoder_post_encoder_layernorm_weight; - - size_t decoder_ffn1_weight; - size_t decoder_ffn1_bias; - size_t decoder_ffn_gate_weight; - size_t decoder_ffn_gate_bias; - size_t decoder_ffn_up_weight; - size_t decoder_ffn_up_bias; - size_t decoder_ffn2_weight; - size_t decoder_ffn2_bias; - - size_t decoder_post_ffn_layernorm_weight; - - size_t decoder_self_attn_q_weight; - size_t decoder_self_attn_k_weight; - size_t decoder_self_attn_v_weight; - size_t decoder_self_attn_output_weight; - - size_t decoder_post_attn_layernorm_weight; - - //Encoder layers - size_t encoder_ffn1_weight; - size_t encoder_ffn1_bias; - size_t encoder_ffn_gate_weight; - size_t encoder_ffn_gate_bias; - size_t encoder_ffn_up_weight; - size_t encoder_ffn_up_bias; - size_t encoder_ffn2_weight; - size_t encoder_ffn2_bias; - - size_t encoder_post_ffn_layernorm_weight; - - size_t encoder_self_attn_q_weight; - size_t encoder_self_attn_k_weight; - size_t encoder_self_attn_v_weight; - size_t encoder_self_attn_output_weight; - - size_t encoder_post_attn_layernorm_weight; - }; - - std::vector encoder_layers; - std::vector decoder_layers; - } weight_nodes_; - - bool encoder_ready_ = false; - size_t last_new_tokens_; - std::vector encoder_output_host_; - std::vector encoder_output_shape_; - size_t last_conv1_node_ = 0; - size_t last_conv2_node_ = 0; - size_t last_encoder_post_norm_node_ = 0; - size_t last_enc_plus_pos_node_ = 0; - size_t encoder_transformer_block_0 = 0; - size_t encoder_pre_gelu = 0; - size_t encoder_post_gelu = 0; - - std::vector encoder_block_out_nodes_; - std::vector encoder_output_bytes_; - Precision encoder_output_precision_ = Precision::FP32; - - bool first_decode_step_ = true; - - std::vector encoder_k_persistent_; - std::vector encoder_v_persistent_; - - Precision encoder_kv_precision_ = Precision::FP32; - bool encoder_kv_ready_ = false; - - std::unique_ptr npu_encoder_; - bool use_npu_encoder_ = false; - -}; - -class NeedleModel : public Model { -public: - NeedleModel(); - explicit NeedleModel(const Config& config); - ~NeedleModel() override = default; - -protected: - size_t build_attention(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) override { - throw std::runtime_error("Needle: build_attention unused"); - } - - size_t build_mlp(CactusGraph*, size_t, uint32_t, ComputeBackend) const override { - throw std::runtime_error("Needle: build_mlp unused"); - } - - size_t build_transformer_block(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) override { - throw std::runtime_error("Needle: build_transformer_block unused"); - } - - size_t forward(const std::vector& tokens, bool use_cache = false) override; - void prefill(const std::vector& tokens, size_t chunk_size = 256, const std::string& profile_file = "") override; - uint32_t decode(const std::vector& tokens, float temperature = -1.0f, float top_p = -1.0f, - size_t top_k = 0, const std::string& profile_file = "", float* out_entropy = nullptr, - float min_p = 0.15f, float repetition_penalty = 1.1f) override; - void load_weights_to_graph(CactusGraph* gb) override; - void reset_cache() override; - - void run_encoder(const std::vector& encoder_tokens); - void reset_graph_side_cache_nodes(); - size_t run_decoder_step(const std::vector& tokens, bool use_cache, bool last_token_only); - - size_t build_encoder_self_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0); - size_t build_decoder_self_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0); - size_t build_decoder_cross_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0); - size_t build_encoder_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0); - size_t build_decoder_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0); - -private: - struct WeightNodeIDs { - size_t output_weight = 0; - size_t encoder_norm_weight = 0; - size_t decoder_norm_weight = 0; - - struct EncoderLayerWeights { - size_t attn_gate_weight = 0; - size_t attn_q_weight = 0; - size_t attn_k_weight = 0; - size_t attn_v_weight = 0; - size_t attn_output_weight = 0; - size_t input_norm_weight = 0; - size_t attn_q_norm_weight = 0; - size_t attn_k_norm_weight = 0; - }; - - struct DecoderLayerWeights { - size_t self_attn_gate_weight = 0; - size_t cross_attn_gate_weight = 0; - size_t self_attn_q_weight = 0; - size_t self_attn_k_weight = 0; - size_t self_attn_v_weight = 0; - size_t self_attn_output_weight = 0; - size_t self_attn_q_norm_weight = 0; - size_t self_attn_k_norm_weight = 0; - - size_t encoder_attn_q_weight = 0; - size_t encoder_attn_k_weight = 0; - size_t encoder_attn_v_weight = 0; - size_t encoder_attn_output_weight = 0; - size_t encoder_attn_q_norm_weight = 0; - size_t encoder_attn_k_norm_weight = 0; - - size_t input_norm_weight = 0; - size_t post_attn_norm_weight = 0; - }; - - std::vector encoder_layers; - std::vector decoder_layers; - } weight_nodes_; - - bool encoder_ready_ = false; - size_t last_encoder_post_norm_node_ = 0; - std::vector encoder_k_persistent_; - std::vector encoder_v_persistent_; -}; - -class ParakeetModel : public Model { -public: - ParakeetModel(); - explicit ParakeetModel(const Config& config); - ~ParakeetModel() override = default; - -protected: - size_t build_attention(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) override { - throw std::runtime_error("Parakeet: build_attention unused"); - } - - size_t build_mlp(CactusGraph*, size_t, uint32_t, ComputeBackend) const override { - throw std::runtime_error("Parakeet: build_mlp unused"); - } - - size_t build_transformer_block(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) override { - throw std::runtime_error("Parakeet: build_transformer_block unused"); - } - - size_t forward(const std::vector& /*tokens*/, bool /*use_cache*/ = false) override { - throw std::runtime_error("Parakeet requires audio feature forward()."); - } - - size_t forward(const std::vector& audio_features, const std::vector& tokens, bool use_cache = false) override; - void load_weights_to_graph(CactusGraph* gb) override; - uint32_t decode_with_audio(const std::vector& tokens, const std::vector& audio_features, - float temperature = 0.0f, float top_p = 0.0f, size_t top_k = 0, - const std::string& profile_file = "", float* out_entropy = nullptr, - float min_p = 0.15f, float repetition_penalty = 1.1f, - float* out_token_time_start = nullptr, float* out_token_time_end = nullptr) override; - std::vector get_audio_embeddings(const std::vector& audio_features) override; - void reset_cache() override; - -private: - size_t build_encoder(CactusGraph* gb, const std::vector& audio_features); - size_t build_ctc_logits(CactusGraph* gb, size_t hidden_states); - size_t build_subsampling(CactusGraph* gb, const std::vector& audio_features); - size_t build_relative_position_embeddings(CactusGraph* gb, size_t seq_len); - size_t build_self_attention(CactusGraph* gb, size_t hidden, size_t position_embeddings, uint32_t layer_idx, ComputeBackend backend); - size_t build_feed_forward(CactusGraph* gb, size_t hidden, uint32_t layer_idx, bool second_ff, ComputeBackend backend); - size_t build_convolution_module(CactusGraph* gb, size_t hidden, uint32_t layer_idx, ComputeBackend backend); - size_t build_encoder_block(CactusGraph* gb, size_t hidden, size_t position_embeddings, uint32_t layer_idx, ComputeBackend backend); - std::vector greedy_decode_tokens(CactusGraph* gb, size_t logits_node) const; - - struct WeightNodeIDs { - size_t ctc_head_weight = 0; - size_t ctc_head_bias = 0; - - size_t subsampling_conv0_weight = 0; - size_t subsampling_conv0_bias = 0; - size_t subsampling_depthwise1_weight = 0; - size_t subsampling_depthwise1_bias = 0; - size_t subsampling_pointwise1_weight = 0; - size_t subsampling_pointwise1_bias = 0; - size_t subsampling_depthwise2_weight = 0; - size_t subsampling_depthwise2_bias = 0; - size_t subsampling_pointwise2_weight = 0; - size_t subsampling_pointwise2_bias = 0; - size_t subsampling_linear_weight = 0; - size_t subsampling_linear_bias = 0; - - struct LayerWeights { - size_t ff1_linear1_weight = 0; - size_t ff1_linear1_bias = 0; - size_t ff1_linear2_weight = 0; - size_t ff1_linear2_bias = 0; - - size_t ff2_linear1_weight = 0; - size_t ff2_linear1_bias = 0; - size_t ff2_linear2_weight = 0; - size_t ff2_linear2_bias = 0; - - size_t self_attn_q_weight = 0; - size_t self_attn_q_bias = 0; - size_t self_attn_k_weight = 0; - size_t self_attn_k_bias = 0; - size_t self_attn_v_weight = 0; - size_t self_attn_v_bias = 0; - size_t self_attn_output_weight = 0; - size_t self_attn_output_bias = 0; - size_t self_attn_relative_k_weight = 0; - size_t self_attn_bias_u = 0; - size_t self_attn_bias_v = 0; - - size_t norm_ff1_weight = 0; - size_t norm_ff1_bias = 0; - size_t norm_self_attn_weight = 0; - size_t norm_self_attn_bias = 0; - size_t norm_conv_weight = 0; - size_t norm_conv_bias = 0; - size_t norm_ff2_weight = 0; - size_t norm_ff2_bias = 0; - size_t norm_out_weight = 0; - size_t norm_out_bias = 0; - - size_t conv_pointwise1_weight = 0; - size_t conv_pointwise1_bias = 0; - size_t conv_depthwise_weight = 0; - size_t conv_depthwise_bias = 0; - size_t conv_pointwise2_weight = 0; - size_t conv_pointwise2_bias = 0; - size_t conv_batchnorm_weight = 0; - size_t conv_batchnorm_bias = 0; - size_t conv_batchnorm_running_mean = 0; - size_t conv_batchnorm_running_var = 0; - }; - - std::vector layers; - } weight_nodes_; - - bool ctc_tokens_ready_ = false; - size_t ctc_emit_index_ = 0; - std::vector ctc_tokens_; - size_t last_input_token_count_ = 0; - - std::unique_ptr npu_encoder_; - bool use_npu_encoder_ = false; - bool has_cpu_encoder_weights_ = false; -}; - -class ParakeetTDTModel : public Model { -public: - ParakeetTDTModel(); - explicit ParakeetTDTModel(const Config& config); - ~ParakeetTDTModel() override = default; - - struct TDTToken { uint32_t id; float time_start; float time_end; }; - - struct ChunkStreamState { - bool initialized = false; - uint32_t last_token = 0; - std::vector> h; - std::vector> c; - }; - - struct ChunkStreamResult { - std::string text; - std::string confirmed_text; - std::string pending_text; - size_t token_count = 0; - size_t confirmed_token_count = 0; - double raw_decoder_tps = 0.0; - double raw_decoder_time_ms = 0.0; - float start_sec = 0.0f; - float confirmed_end_sec = 0.0f; - float resume_end_sec = 0.0f; - float end_sec = 0.0f; - }; - - ChunkStreamResult decode_chunk_stream( - const std::vector& audio_features, - size_t replay_start_frame, - size_t start_frame, - size_t end_frame, - ChunkStreamState& state); - -protected: - size_t build_attention(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) override { - throw std::runtime_error("ParakeetTDT: build_attention unused"); - } - - size_t build_mlp(CactusGraph*, size_t, uint32_t, ComputeBackend) const override { - throw std::runtime_error("ParakeetTDT: build_mlp unused"); - } - - size_t build_transformer_block(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) override { - throw std::runtime_error("ParakeetTDT: build_transformer_block unused"); - } - - size_t forward(const std::vector& /*tokens*/, bool /*use_cache*/ = false) override { - throw std::runtime_error("ParakeetTDT requires audio feature forward()."); - } - - size_t forward(const std::vector& audio_features, const std::vector& tokens, bool use_cache = false) override; - void load_weights_to_graph(CactusGraph* gb) override; - uint32_t decode_with_audio(const std::vector& tokens, const std::vector& audio_features, - float temperature = 0.0f, float top_p = 0.0f, size_t top_k = 0, - const std::string& profile_file = "", float* out_entropy = nullptr, - float min_p = 0.15f, float repetition_penalty = 1.1f, - float* out_token_time_start = nullptr, float* out_token_time_end = nullptr) override; - std::vector get_audio_embeddings(const std::vector& audio_features) override; - void reset_cache() override; - -private: - size_t build_encoder(CactusGraph* gb, const std::vector& audio_features); - size_t build_subsampling(CactusGraph* gb, const std::vector& audio_features); - size_t build_relative_position_embeddings(CactusGraph* gb, size_t seq_len); - size_t build_self_attention(CactusGraph* gb, size_t hidden, size_t position_embeddings, uint32_t layer_idx, ComputeBackend backend); - size_t build_feed_forward(CactusGraph* gb, size_t hidden, uint32_t layer_idx, bool second_ff, ComputeBackend backend); - size_t build_convolution_module(CactusGraph* gb, size_t hidden, uint32_t layer_idx, ComputeBackend backend); - size_t build_encoder_block(CactusGraph* gb, size_t hidden, size_t position_embeddings, uint32_t layer_idx, ComputeBackend backend); - std::vector decode_tdt_tokens_with_state( - CactusGraph* gb, - size_t encoder_hidden_node, - size_t replay_start_frame, - size_t start_frame, - size_t end_frame, - ChunkStreamState* stream_state, - size_t* out_confirmed_count = nullptr, - double* out_raw_decoder_time_ms = nullptr) const; - std::vector greedy_decode_tdt_tokens(CactusGraph* gb, size_t encoder_hidden_node) const; - - struct WeightNodeIDs { - size_t subsampling_conv0_weight = 0; - size_t subsampling_conv0_bias = 0; - size_t subsampling_depthwise1_weight = 0; - size_t subsampling_depthwise1_bias = 0; - size_t subsampling_pointwise1_weight = 0; - size_t subsampling_pointwise1_bias = 0; - size_t subsampling_depthwise2_weight = 0; - size_t subsampling_depthwise2_bias = 0; - size_t subsampling_pointwise2_weight = 0; - size_t subsampling_pointwise2_bias = 0; - size_t subsampling_linear_weight = 0; - size_t subsampling_linear_bias = 0; - - struct LayerWeights { - size_t ff1_linear1_weight = 0; - size_t ff1_linear1_bias = 0; - size_t ff1_linear2_weight = 0; - size_t ff1_linear2_bias = 0; - - size_t ff2_linear1_weight = 0; - size_t ff2_linear1_bias = 0; - size_t ff2_linear2_weight = 0; - size_t ff2_linear2_bias = 0; - - size_t self_attn_q_weight = 0; - size_t self_attn_q_bias = 0; - size_t self_attn_k_weight = 0; - size_t self_attn_k_bias = 0; - size_t self_attn_v_weight = 0; - size_t self_attn_v_bias = 0; - size_t self_attn_output_weight = 0; - size_t self_attn_output_bias = 0; - size_t self_attn_relative_k_weight = 0; - size_t self_attn_bias_u = 0; - size_t self_attn_bias_v = 0; - - size_t norm_ff1_weight = 0; - size_t norm_ff1_bias = 0; - size_t norm_self_attn_weight = 0; - size_t norm_self_attn_bias = 0; - size_t norm_conv_weight = 0; - size_t norm_conv_bias = 0; - size_t norm_ff2_weight = 0; - size_t norm_ff2_bias = 0; - size_t norm_out_weight = 0; - size_t norm_out_bias = 0; - - size_t conv_pointwise1_weight = 0; - size_t conv_pointwise1_bias = 0; - size_t conv_depthwise_weight = 0; - size_t conv_depthwise_bias = 0; - size_t conv_pointwise2_weight = 0; - size_t conv_pointwise2_bias = 0; - size_t conv_batchnorm_weight = 0; - size_t conv_batchnorm_bias = 0; - size_t conv_batchnorm_running_mean = 0; - size_t conv_batchnorm_running_var = 0; - }; - - struct PredictorLayerWeights { - size_t weight_ih = 0; - size_t weight_hh = 0; - size_t bias = 0; - }; - - size_t predictor_embed = 0; - std::vector predictor_layers; - size_t joint_enc_weight = 0; - size_t joint_enc_bias = 0; - size_t joint_pred_weight = 0; - size_t joint_pred_bias = 0; - size_t joint_out_weight = 0; - size_t joint_out_bias = 0; - - std::vector layers; - } weight_nodes_; - - bool tdt_tokens_ready_ = false; - size_t tdt_emit_index_ = 0; - std::vector tdt_tokens_; - size_t last_input_token_count_ = 0; - - std::unique_ptr npu_encoder_; - bool use_npu_encoder_ = false; - bool has_cpu_encoder_weights_ = false; -}; - - -class Lfm2VlModel : public Model { -public: - Lfm2VlModel(); - explicit Lfm2VlModel(const Config& config); - ~Lfm2VlModel() override = default; - - bool init(const std::string& model_folder, size_t context_size, const std::string& system_prompt = "", bool do_warmup = true) override; - size_t forward(const std::vector& tokens, bool use_cache = false) override; - - uint32_t decode(const std::vector& tokens, - float temperature = -1.0f, - float top_p = -1.0f, - size_t top_k = 0, - const std::string& profile_file = "", - float* out_entropy = nullptr, - float min_p = 0.15f, - float repetition_penalty = 1.1f) override; - - void prefill(const std::vector& tokens, size_t chunk_size = 256, const std::string& profile_file = "") override; - - void prefill_with_images(const std::vector& tokens, const std::vector& image_paths, - const std::string& profile_file = "") override; - - uint32_t decode_with_images( - const std::vector& tokens, - const std::vector& image_paths, - float temperature = -1.0f, - float top_p = -1.0f, - size_t top_k = 0, - const std::string& profile_file = "", - float* out_entropy = nullptr, - float min_p = 0.15f, - float repetition_penalty = 1.1f) override; - - void reset_cache() override; - std::vector get_image_embeddings(const std::string& image_path) override; - -protected: - size_t build_attention(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) override; - size_t build_mlp(CactusGraph*, size_t, uint32_t, ComputeBackend) const override; - size_t build_transformer_block(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) override; - - void load_weights_to_graph(CactusGraph* gb) override; - -private: - struct ProjectedTileFeature { - size_t node_id; - size_t token_count; - }; - - struct TextEmbeddingInput { - size_t input_node; - std::vector tokens; - }; - - struct MergedEmbeddingResult { - size_t node_id; - size_t seq_len; - }; - - struct ForwardImageResult { - size_t final_hidden_node; - size_t seq_len; - }; - std::vector get_image_features( - CactusGraph* gb, - const Siglip2Preprocessor::PreprocessedImage& preprocessed_image, - ComputeBackend backend); - - ForwardImageResult forward_images( - CactusGraph* gb, - const std::vector& tokens, - const std::vector& image_paths, - ComputeBackend backend, - bool use_cache); - size_t build_multimodal_projector( - CactusGraph* gb, - size_t image_features, - size_t tile_h, - size_t tile_w, - ComputeBackend backend); - size_t pixel_unshuffle(CactusGraph* gb, size_t hidden_states, size_t height, size_t width, size_t channels); - MergedEmbeddingResult merge_image_text_embeddings( - CactusGraph* gb, - const std::vector& tokens, - const std::vector>& image_embedding_nodes, - std::vector& text_embedding_inputs); - Siglip2VisionModel vision_tower_; - LFM2Model language_model_; - Siglip2Preprocessor preprocessor_; - struct ProjectorWeights { - size_t layer_norm_weight; - size_t layer_norm_bias; - size_t linear_1_weight; - size_t linear_1_bias; - size_t linear_2_weight; - size_t linear_2_bias; - } projector_weights_; - - bool vision_weights_loaded_ = false; - bool language_weights_loaded_ = false; - - bool image_prefill_completed_ = false; - size_t last_token_count_ = 0; -}; - -class SileroVADModel : public Model { -public: - static constexpr size_t CONTEXT_SIZE = 64; - static constexpr size_t CHUNK_SIZE = 512; - static constexpr size_t REFLECT_PAD_SIZE = 64; - static constexpr size_t HIDDEN_SIZE = 128; - static constexpr size_t GATE_SIZE = 512; - - SileroVADModel(); - explicit SileroVADModel(const Config& config); - ~SileroVADModel() override; - - bool init(const std::string& model_folder, size_t context_size = 0, - const std::string& system_prompt = "", bool do_warmup = false) override; - - struct SpeechTimestamp { - size_t start; - size_t end; - }; - - struct SpeechTimestampsOptions { - float threshold = 0.5f; - float neg_threshold = 0.0f; - int min_speech_duration_ms = 250; - float max_speech_duration_s = std::numeric_limits::infinity(); - int min_silence_duration_ms = 100; - int speech_pad_ms = 30; - int window_size_samples = 512; - int min_silence_at_max_speech = 98; - bool use_max_poss_sil_at_max_speech = true; - int sampling_rate = 16000; - }; - - float process_chunk(const std::vector& audio_chunk); - void reset_states(); - std::vector get_speech_timestamps(const std::vector& audio, const SpeechTimestampsOptions& options); - -protected: - size_t build_attention(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) override { - throw std::runtime_error("SileroVAD: build_attention unused"); - } - - size_t build_mlp(CactusGraph*, size_t, uint32_t, ComputeBackend) const override { - throw std::runtime_error("SileroVAD: build_mlp unused"); - } - - size_t build_transformer_block(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) override { - throw std::runtime_error("SileroVAD: build_transformer_block unused"); - } - - size_t forward(const std::vector&, bool = false) override { - throw std::runtime_error("SileroVAD: use process_chunk() instead"); - } - - size_t forward(const std::vector& audio_features, const std::vector& tokens, bool use_cache = false) override; - - void load_weights_to_graph(CactusGraph* gb) override; - -private: - void build_graph(); - - struct VADGraphNodes { - size_t input; - size_t h_prev; - size_t c_prev; - size_t h_new; - size_t c_new; - size_t output; - size_t lstm_output; - size_t encoder_output; - } graph_nodes_; - - struct VADWeightNodes { - size_t stft_basis; - struct EncoderBlock { - size_t conv_weight; - size_t conv_bias; - }; - std::vector encoder_blocks; - size_t lstm_weight_ih; - size_t lstm_weight_hh; - size_t lstm_bias_ih; - size_t lstm_bias_hh; - size_t output_conv_weight; - size_t output_conv_bias; - } weight_nodes_; - - struct VADState { - std::vector<__fp16> h; - std::vector<__fp16> c; - std::vector context; - std::vector input_buf; - std::vector<__fp16> input_fp16; - } state_; - - CactusGraph graph_; - std::string weights_path_; -}; - -class YoutuModel : public Model { -public: - YoutuModel(); - explicit YoutuModel(const Config& config); - ~YoutuModel() override = default; - - void reset_cache() override; - -protected: - size_t build_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0) override; - - size_t build_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, - ComputeBackend backend) const override; - - size_t build_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache = false, size_t position_offset = 0) override; - - size_t forward(const std::vector& tokens, bool use_cache = false) override; - void load_weights_to_graph(CactusGraph* gb) override; - std::vector get_kv_layer_dims() const override; - void post_init() override; - void post_execute_updates(CactusGraph* gb, size_t seq_len) override; - -private: - KVCache v_cache_; - std::vector cache_v_nodes_; - - struct WeightNodeIDs { - size_t output_weight = 0; - size_t output_norm_weight = 0; - - struct LayerWeights { - size_t attn_q_weight = 0; - size_t attn_q_a_weight = 0; - size_t attn_q_a_norm_weight = 0; - size_t attn_q_b_weight = 0; - size_t attn_kv_a_weight = 0; - size_t attn_kv_a_norm_weight = 0; - size_t attn_kv_b_weight = 0; - size_t attn_output_weight = 0; - size_t input_layernorm_weight = 0; - size_t post_attention_layernorm_weight = 0; - size_t ffn_gate_weight = 0; - size_t ffn_up_weight = 0; - size_t ffn_down_weight = 0; - size_t attn_q_a_bias = 0; - size_t attn_kv_a_bias = 0; - size_t attn_output_bias = 0; - }; - - std::vector layers; - } weight_nodes_; -}; - -class PyAnnoteModel : public Model { -public: - PyAnnoteModel(); - explicit PyAnnoteModel(const Config& config); - ~PyAnnoteModel() override = default; - - bool init(const std::string& model_folder, size_t context_size = 0, - const std::string& system_prompt = "", bool do_warmup = false) override; - - std::vector diarize(const float* pcm_f32, size_t num_samples, size_t step_samples = 16000, bool raw_powerset = false); - -protected: - size_t build_attention(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) override { - throw std::runtime_error("PyAnnote: build_attention unused"); - } - - size_t build_mlp(CactusGraph*, size_t, uint32_t, ComputeBackend) const override { - throw std::runtime_error("PyAnnote: build_mlp unused"); - } - - size_t build_transformer_block(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) override { - throw std::runtime_error("PyAnnote: build_transformer_block unused"); - } - - size_t forward(const std::vector&, bool = false) override { - throw std::runtime_error("PyAnnote: use diarize() instead"); - } - - void load_weights_to_graph(CactusGraph* gb) override; - -private: - void build_graph(); - - struct LSTMLayerWeights { - size_t w_ih_fwd, w_hh_fwd, b_ih_fwd, b_hh_fwd; - size_t w_ih_bwd, w_hh_bwd, b_ih_bwd, b_hh_bwd; - }; - - struct WeightNodeIDs { - size_t sinc_filters; - size_t wav_norm_weight, wav_norm_bias; - size_t norm0_weight, norm0_bias; - size_t conv1_weight, conv1_bias; - size_t norm1_weight, norm1_bias; - size_t conv2_weight, conv2_bias; - size_t norm2_weight, norm2_bias; - LSTMLayerWeights lstm_layers[4]; - size_t linear0_weight, linear0_bias; - size_t linear1_weight, linear1_bias; - size_t classifier_weight, classifier_bias; - } weight_nodes_; - - CactusGraph graph_; - size_t audio_input_ = 0; - size_t output_node_ = 0; - std::vector hamming_; - std::vector<__fp16> chunk_buf_; -}; - -class WeSpeakerModel : public Model { -public: - WeSpeakerModel(); - explicit WeSpeakerModel(const Config& config); - ~WeSpeakerModel() override = default; - - bool init(const std::string& model_folder, size_t context_size = 0, - const std::string& system_prompt = "", bool do_warmup = false) override; - - std::vector embed(const float* fbank_features, size_t num_features); - std::vector embed(const float* fbank_features, size_t num_features, - const float* mask_weights, size_t mask_num_frames); - -protected: - size_t build_attention(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) override { - throw std::runtime_error("WeSpeaker: build_attention unused"); - } - - size_t build_mlp(CactusGraph*, size_t, uint32_t, ComputeBackend) const override { - throw std::runtime_error("WeSpeaker: build_mlp unused"); - } - - size_t build_transformer_block(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) override { - throw std::runtime_error("WeSpeaker: build_transformer_block unused"); - } - - size_t forward(const std::vector&, bool = false) override { - throw std::runtime_error("WeSpeaker: use embed() instead"); - } - - void load_weights_to_graph(CactusGraph* gb) override; - -private: - void build_graph(size_t num_frames); - void build_graph_masked(size_t num_frames); - - struct ResBlockWeights { - size_t conv1_w, conv1_b; - size_t conv2_w, conv2_b; - size_t shortcut_conv_w, shortcut_conv_b; - bool has_shortcut = false; - }; - - ResBlockWeights load_resblock(CactusGraph* gb, const std::string& prefix, bool has_shortcut); - size_t build_resblock(CactusGraph* gb, size_t x, const ResBlockWeights& rb, bool stride2); - size_t build_resnet_body(CactusGraph* gb, size_t x); - - struct WeightNodeIDs { - size_t conv1_w, conv1_b; - std::vector layer1, layer2, layer3, layer4; - size_t seg1_w, seg1_b; - } weight_nodes_; - - CactusGraph graph_; - size_t audio_input_ = 0; - size_t mask_input_ = 0; - size_t output_node_ = 0; - size_t current_num_frames_ = 0; - bool current_masked_ = false; - std::vector<__fp16> input_buf_; - std::vector mask_buf_; -}; - -} -} - -#include "gemma4/model_gemma4.h" diff --git a/cactus/models/model_gemma.cpp b/cactus/models/model_gemma.cpp deleted file mode 100644 index a128c8905..000000000 --- a/cactus/models/model_gemma.cpp +++ /dev/null @@ -1,192 +0,0 @@ -#include "model.h" -#include "../graph/graph.h" -#include "../npu/npu.h" -#include -#include - - -namespace cactus { -namespace engine { - -GemmaModel::GemmaModel() : Model() {} - -GemmaModel::GemmaModel(const Config& config) : Model(config) { - weight_nodes_.layers.resize(config.num_layers); -} - -void GemmaModel::post_init() { - kv_cache_.set_window_size(0, 0); -} - -void GemmaModel::load_weights_to_graph(CactusGraph* gb) { - embedding_node_id_ = gb->mmap_embeddings(embedding_file_path_); - weight_nodes_.output_norm_weight = gb->mmap_weights(model_folder_path_ + "/output_norm.weights"); - - if (config_.tie_word_embeddings) { - weight_nodes_.output_weight = embedding_node_id_; - output_weight_node_id_ = embedding_node_id_; - } else { - weight_nodes_.output_weight = gb->mmap_weights(model_folder_path_ + "/output_weight.weights"); - output_weight_node_id_ = weight_nodes_.output_weight; - } - - for (uint32_t i = 0; i < config_.num_layers; i++) { - auto& layer = weight_nodes_.layers[i]; - std::string layer_prefix = model_folder_path_ + "/layer_" + std::to_string(i) + "_"; - layer.attn_q_weight = gb->mmap_weights(layer_prefix + "attn_q.weights"); - layer.attn_k_weight = gb->mmap_weights(layer_prefix + "attn_k.weights"); - layer.attn_v_weight = gb->mmap_weights(layer_prefix + "attn_v.weights"); - layer.attn_output_weight = gb->mmap_weights(layer_prefix + "attn_output.weights"); - layer.input_layernorm_weight = gb->mmap_weights(layer_prefix + "input_norm.weights"); - layer.attn_q_norm_weight = gb->mmap_weights(layer_prefix + "attn_q_norm.weights"); - layer.attn_k_norm_weight = gb->mmap_weights(layer_prefix + "attn_k_norm.weights"); - layer.ffn_gate_weight = gb->mmap_weights(layer_prefix + "ffn_gate.weights"); - layer.ffn_up_weight = gb->mmap_weights(layer_prefix + "ffn_up.weights"); - layer.ffn_down_weight = gb->mmap_weights(layer_prefix + "ffn_down.weights"); - layer.post_attention_layernorm_weight = gb->mmap_weights(layer_prefix + "post_attn_norm.weights"); - layer.pre_feedforward_layernorm_weight = gb->mmap_weights(layer_prefix + "pre_ffn_norm.weights"); - layer.post_feedforward_layernorm_weight = gb->mmap_weights(layer_prefix + "post_ffn_norm.weights"); - } - - if (npu::is_npu_available()) { - std::string npu_prefill_path = model_folder_path_ + "/model.mlpackage"; - load_npu_prefill(npu_prefill_path); - } -} - -size_t GemmaModel::build_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache, size_t position_offset) { - const auto& layer = weight_nodes_.layers[layer_idx]; - - auto q_proj = gb->matmul(normalized_input, layer.attn_q_weight, true, backend); - auto k_proj = gb->matmul(normalized_input, layer.attn_k_weight, true, backend); - auto v_proj = gb->matmul(normalized_input, layer.attn_v_weight, true, backend); - - const auto& q_shape = gb->get_output_buffer(q_proj).shape; - size_t batch_seq = q_shape[0]; - size_t num_heads = config_.attention_heads; - size_t head_dim = config_.attention_head_dim; - q_proj = gb->reshape(q_proj, {batch_seq * num_heads, head_dim}); - q_proj = gb->rms_norm(q_proj, layer.attn_q_norm_weight, config_.layer_norm_eps); - q_proj = gb->reshape(q_proj, {batch_seq, num_heads * head_dim}); - - size_t num_kv_heads = config_.attention_kv_heads; - k_proj = gb->reshape(k_proj, {batch_seq * num_kv_heads, head_dim}); - k_proj = gb->rms_norm(k_proj, layer.attn_k_norm_weight, config_.layer_norm_eps); - k_proj = gb->reshape(k_proj, {batch_seq, num_kv_heads * head_dim}); - - size_t seq_len = batch_seq; - - auto q_proj_4d = gb->reshape(q_proj, {1, seq_len, config_.attention_heads, config_.attention_head_dim}); - auto k_proj_4d = gb->reshape(k_proj, {1, seq_len, config_.attention_kv_heads, config_.attention_head_dim}); - auto v_proj_4d = gb->reshape(v_proj, {1, seq_len, config_.attention_kv_heads, config_.attention_head_dim}); - - bool is_global_attention = ((layer_idx + 1) % 6) == 0; - - if (config_.rope_theta > 0) { - float rope_freq = is_global_attention ? 1000000.0f : 10000.0f; - - q_proj_4d = gb->rope(q_proj_4d, rope_freq, position_offset); - k_proj_4d = gb->rope(k_proj_4d, rope_freq, position_offset); - } - - if (use_cache) { - cache_k_output_nodes_[layer_idx] = k_proj_4d; - cache_v_output_nodes_[layer_idx] = v_proj_4d; - } - - size_t attn_output_4d; - size_t window_size = is_global_attention ? 0 : 512; - - if (use_cache && !kv_cache_.is_empty()) { - attn_output_4d = gb->attention_int8_hybrid( - q_proj_4d, k_proj_4d, v_proj_4d, - attention_scale_, position_offset, - kv_cache_.get_keys_int8(layer_idx), - kv_cache_.get_values_int8(layer_idx), - kv_cache_.get_key_scales(layer_idx), - kv_cache_.get_value_scales(layer_idx), - kv_cache_.current_seq_len, num_kv_heads, head_dim, window_size - ); - } else { - attn_output_4d = gb->attention(q_proj_4d, k_proj_4d, v_proj_4d, attention_scale_, position_offset, window_size); - } - auto attn_output = gb->reshape(attn_output_4d, {seq_len, config_.attention_head_dim * config_.attention_heads}); - return gb->matmul(attn_output, layer.attn_output_weight, true, backend); -} - - -size_t GemmaModel::build_mlp(CactusGraph* gb, size_t input, uint32_t layer_idx, - ComputeBackend backend) const { - const auto& layer = weight_nodes_.layers[layer_idx]; - - size_t gate_output = gb->matmul(input, layer.ffn_gate_weight, true, backend); - size_t up_output = gb->matmul(input, layer.ffn_up_weight, true, backend); - size_t gate_gelu = gb->gelu(gate_output); - size_t gated = gb->multiply(gate_gelu, up_output); - return gb->matmul(gated, layer.ffn_down_weight, true, backend); -} - -size_t GemmaModel::build_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache, size_t position_offset) { - const auto& layer = weight_nodes_.layers[layer_idx]; - - auto normalized_input = gb->rms_norm(hidden, layer.input_layernorm_weight, config_.layer_norm_eps); - auto attn_output = build_attention(gb, normalized_input, layer_idx, backend, use_cache, position_offset); - - auto normalized_attn = gb->rms_norm(attn_output, layer.post_attention_layernorm_weight, config_.layer_norm_eps); - - auto after_attention = gb->add_clipped(hidden, normalized_attn); - - auto pre_mlp_norm = gb->rms_norm(after_attention, layer.pre_feedforward_layernorm_weight, config_.layer_norm_eps); - auto mlp_output = build_mlp(gb, pre_mlp_norm, layer_idx, backend); - - auto normalized_mlp = gb->rms_norm(mlp_output, layer.post_feedforward_layernorm_weight, config_.layer_norm_eps); - - return gb->add_clipped(after_attention, normalized_mlp); -} - - -size_t GemmaModel::forward(const std::vector& tokens, bool use_cache) { - if (!initialized_ || !graph_handle_) { - throw std::runtime_error("Model not initialized - call init() first"); - } - - if (tokens.empty()) { - throw std::runtime_error("Token sequence cannot be empty"); - } - - auto* gb = static_cast(graph_handle_); - gb->soft_reset(); - - auto seq_len = static_cast(tokens.size()); - - size_t position_offset = use_cache ? kv_cache_.get_total_seq_len() : 0; - - auto backend = config_.default_backend == Config::Backend::CPU - ? ComputeBackend::CPU - : ComputeBackend::NPU; - - auto input_node_id = gb->input({seq_len}, Precision::FP32); - auto hidden = gb->embedding(embedding_node_id_, input_node_id); - - float embed_scale = std::sqrt(static_cast(config_.hidden_dim)); - hidden = gb->scalar_multiply(hidden, embed_scale); - - for (uint32_t layer_idx = 0; layer_idx < config_.num_layers; layer_idx++) { - hidden = build_transformer_block(gb, hidden, layer_idx, backend, use_cache, position_offset); - } - - auto final_hidden = gb->rms_norm(hidden, weight_nodes_.output_norm_weight, config_.layer_norm_eps); - - std::vector input_data(seq_len); - for (size_t i = 0; i < seq_len; i++) { - input_data[i] = static_cast(tokens[i]); - } - gb->set_input(input_node_id, input_data.data(), Precision::FP32); - - return final_hidden; -} - -} -} \ No newline at end of file diff --git a/cactus/models/model_gemma3n.cpp b/cactus/models/model_gemma3n.cpp deleted file mode 100644 index 65a3b8173..000000000 --- a/cactus/models/model_gemma3n.cpp +++ /dev/null @@ -1,475 +0,0 @@ -#include "model.h" -#include "../graph/graph.h" -#include -#include -#include -#include - -namespace cactus { -namespace engine { - -static const float RSQRT2 = 1.0f / std::sqrt(2.0f); - -GemmaModel3n::GemmaModel3n() : Model() {} - -GemmaModel3n::GemmaModel3n(const Config& config) : Model(config) { - weight_nodes_.layers.resize(config.num_layers); -} - -std::vector GemmaModel3n::get_kv_layer_dims() const { - uint32_t n = config_.num_layers; - uint32_t num_shared = config_.num_kv_shared_layers; - uint32_t first_shared = (n > num_shared) ? n - num_shared : n; - - std::vector dims(n); - for (uint32_t i = 0; i < n; i++) { - dims[i] = (i >= first_shared) ? 0 : config_.attention_head_dim; - } - return dims; -} - -void GemmaModel3n::post_init() { - kv_cache_.set_window_size(0, 0); - - uint32_t n = config_.num_layers; - uint32_t num_shared = config_.num_kv_shared_layers; - uint32_t first_shared = (n > num_shared) ? n - num_shared : n; - - kv_share_map_.resize(n, -1); - shared_k_nodes_.resize(n, 0); - shared_v_nodes_.resize(n, 0); - - auto is_global_layer = [&](uint32_t idx) -> bool { - if (!config_.layer_types.empty() && idx < config_.layer_types.size()) { - const auto& lt = config_.layer_types[idx]; - return (lt == "global" || lt == "full" || lt == "full_attention"); - } - return (idx % 5) == 4; - }; - - for (uint32_t i = first_shared; i < n; i++) { - bool is_global = is_global_layer(i); - for (int j = static_cast(first_shared) - 1; j >= 0; j--) { - if (is_global_layer(j) == is_global) { - kv_share_map_[i] = j; - break; - } - } - } -} - -void GemmaModel3n::load_weights_to_graph(CactusGraph* gb) { - assert(config_.altup_num_inputs == 4 && "WeightNodeIDs altup arrays assume exactly 4 AltUp streams"); - embedding_node_id_ = gb->mmap_embeddings(embedding_file_path_); - weight_nodes_.output_norm_weight = gb->mmap_weights(model_folder_path_ + "/output_norm.weights"); - if (config_.tie_word_embeddings) { - weight_nodes_.output_weight = embedding_node_id_; - output_weight_node_id_ = embedding_node_id_; - } else { - weight_nodes_.output_weight = gb->mmap_weights(model_folder_path_ + "/output_weight.weights"); - output_weight_node_id_ = weight_nodes_.output_weight; - } - - for (int i = 0; i < 3; i++) { - auto idx = std::to_string(i); - weight_nodes_.altup_proj_weights[i] = gb->mmap_weights(model_folder_path_ + "/altup_proj_" + idx + ".weights"); - weight_nodes_.altup_unembed_proj_weights[i] = gb->mmap_weights(model_folder_path_ + "/altup_unembed_proj_" + idx + ".weights"); - } - - weight_nodes_.embed_tokens_per_layer = gb->mmap_embeddings(model_folder_path_ + "/embed_tokens_per_layer.weights"); - weight_nodes_.per_layer_model_proj = gb->mmap_weights(model_folder_path_ + "/per_layer_model_proj.weights"); - weight_nodes_.per_layer_proj_norm = gb->mmap_weights(model_folder_path_ + "/per_layer_proj_norm.weights"); - - uint32_t num_shared = config_.num_kv_shared_layers; - uint32_t first_shared = (config_.num_layers > num_shared) ? config_.num_layers - num_shared : config_.num_layers; - - for (uint32_t i = 0; i < config_.num_layers; i++) { - auto& layer = weight_nodes_.layers[i]; - std::string prefix = model_folder_path_ + "/layer_" + std::to_string(i) + "_"; - bool is_shared = (i >= first_shared); - - layer.attn_q_weight = gb->mmap_weights(prefix + "attn_q.weights"); - layer.attn_k_weight = is_shared ? 0 : gb->mmap_weights(prefix + "attn_k.weights"); - layer.attn_v_weight = is_shared ? 0 : gb->mmap_weights(prefix + "attn_v.weights"); - layer.attn_output_weight = gb->mmap_weights(prefix + "attn_output.weights"); - layer.input_layernorm_weight = gb->mmap_weights(prefix + "input_norm.weights"); - layer.attn_q_norm_weight = gb->mmap_weights(prefix + "attn_q_norm.weights"); - layer.attn_k_norm_weight = is_shared ? 0 : gb->mmap_weights(prefix + "attn_k_norm.weights"); - layer.ffn_gate_weight = gb->mmap_weights(prefix + "ffn_gate.weights"); - layer.ffn_up_weight = gb->mmap_weights(prefix + "ffn_up.weights"); - layer.ffn_down_weight = gb->mmap_weights(prefix + "ffn_down.weights"); - layer.post_attention_layernorm_weight = gb->mmap_weights(prefix + "post_attn_norm.weights"); - layer.pre_feedforward_layernorm_weight = gb->mmap_weights(prefix + "pre_ffn_norm.weights"); - layer.post_feedforward_layernorm_weight = gb->mmap_weights(prefix + "post_ffn_norm.weights"); - - layer.altup_router_norm = gb->mmap_weights(prefix + "altup_router_norm.weights"); - layer.altup_prediction_coefs = gb->mmap_weights(prefix + "altup_prediction_coefs.weights"); - layer.altup_correction_coefs = gb->mmap_weights(prefix + "altup_correction_coefs.weights"); - layer.altup_correct_output_scale = gb->mmap_weights(prefix + "altup_correct_output_scale.weights"); - layer.altup_modality_router = gb->mmap_weights(prefix + "altup_modality_router.weights"); - layer.laurel_left = gb->mmap_weights(prefix + "laurel_left.weights"); - layer.laurel_right = gb->mmap_weights(prefix + "laurel_right.weights"); - layer.laurel_norm = gb->mmap_weights(prefix + "laurel_norm.weights"); - layer.per_layer_gate = gb->mmap_weights(prefix + "per_layer_gate.weights"); - layer.per_layer_proj = gb->mmap_weights(prefix + "per_layer_proj.weights"); - layer.post_per_layer_norm = gb->mmap_weights(prefix + "post_per_layer_norm.weights"); - } - - size_t head_dim = config_.attention_head_dim; - v_norm_ones_weight_.assign(head_dim, static_cast<__fp16>(1.0f)); - v_norm_ones_node_ = gb->input({head_dim}, Precision::FP16); - gb->set_external_input(v_norm_ones_node_, v_norm_ones_weight_.data(), Precision::FP16); -} - - -size_t GemmaModel3n::build_rms_norm_no_weight(CactusGraph* gb, size_t input, size_t num_rows, size_t row_dim) const { - auto flat = gb->reshape(input, {num_rows, row_dim}); - return gb->rms_norm(flat, v_norm_ones_node_, config_.layer_norm_eps); -} - -size_t GemmaModel3n::build_magnitude_normalize(CactusGraph* gb, size_t reference, size_t target) const { - size_t rows = gb->get_output_buffer(reference).shape[0]; - auto ref_sq = gb->reshape(gb->mean(gb->multiply(reference, reference), 1), {rows, 1}); - auto tgt_sq = gb->reshape(gb->mean(gb->multiply(target, target), 1), {rows, 1}); - auto ref_mag = gb->scalar_sqrt(ref_sq); - auto tgt_mag = gb->scalar_sqrt(gb->scalar_add(tgt_sq, 1e-5f)); - auto ratio = gb->divide(ref_mag, tgt_mag); - return gb->multiply(target, ratio); -} - -size_t GemmaModel3n::build_gaussian_topk(CactusGraph* gb, size_t input, float ppf) const { - return gb->gaussian_topk(input, ppf); -} - -size_t GemmaModel3n::build_laurel(CactusGraph* gb, size_t normed_input, uint32_t layer_idx, - ComputeBackend backend) const { - const auto& layer = weight_nodes_.layers[layer_idx]; - auto x = gb->matmul(normed_input, layer.laurel_left, true, backend); - x = gb->matmul(x, layer.laurel_right, true, backend); - return gb->add(normed_input, gb->rms_norm(x, layer.laurel_norm, config_.layer_norm_eps)); -} - -size_t GemmaModel3n::build_altup_router_modalities(CactusGraph* gb, size_t stream0, uint32_t layer_idx, - ComputeBackend backend) const { - const auto& layer = weight_nodes_.layers[layer_idx]; - auto x = gb->rms_norm(stream0, layer.altup_router_norm, config_.layer_norm_eps); - x = gb->scalar_multiply(x, 1.0f / static_cast(config_.hidden_dim)); - return gb->tanh(gb->matmul(x, layer.altup_modality_router, true, backend)); -} - -void GemmaModel3n::build_altup_predict(CactusGraph* gb, size_t modalities, uint32_t layer_idx, - const size_t* streams, size_t* predictions) const { - uint32_t n = config_.altup_num_inputs; - auto coefs = gb->matmul(modalities, weight_nodes_.layers[layer_idx].altup_prediction_coefs, true, ComputeBackend::CPU); - - size_t seq_len = gb->get_output_buffer(streams[0]).shape[0]; - auto fused = gb->altup_predict(coefs, streams, n); - for (uint32_t i = 0; i < n; i++) { - predictions[i] = gb->slice(fused, 0, i * seq_len, seq_len); - } -} - -void GemmaModel3n::build_altup_correct(CactusGraph* gb, size_t activated, size_t modalities, uint32_t layer_idx, - ComputeBackend backend, const size_t* predictions, size_t* corrected) const { - uint32_t n = config_.altup_num_inputs; - auto coefs = gb->scalar_add(gb->matmul(modalities, weight_nodes_.layers[layer_idx].altup_correction_coefs, true, backend), 1.0f); - auto innovation = gb->subtract(activated, predictions[0]); - - size_t seq_len = gb->get_output_buffer(predictions[0]).shape[0]; - auto fused = gb->altup_correct(coefs, innovation, predictions, n); - for (uint32_t i = 0; i < n; i++) { - corrected[i] = gb->slice(fused, 0, i * seq_len, seq_len); - } -} - -void GemmaModel3n::build_per_layer_input(CactusGraph* gb, size_t pli_combined, uint32_t layer_idx, - ComputeBackend backend, size_t* streams) const { - const auto& layer = weight_nodes_.layers[layer_idx]; - uint32_t pli_dim = config_.hidden_size_per_layer_input; - - auto gated = gb->multiply(streams[0], layer.altup_correct_output_scale); - gated = gb->gelu(gb->matmul(gated, layer.per_layer_gate, true, backend)); - - auto pli = gb->multiply(gated, gb->slice(pli_combined, 1, layer_idx * pli_dim, pli_dim)); - pli = gb->rms_norm(gb->matmul(pli, layer.per_layer_proj, true, backend), layer.post_per_layer_norm, config_.layer_norm_eps); - - for (uint32_t i = 1; i < config_.altup_num_inputs; i++) - streams[i] = gb->add(streams[i], pli); -} - - -size_t GemmaModel3n::build_attention(CactusGraph* gb, size_t input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache, size_t position_offset) { - const auto& layer = weight_nodes_.layers[layer_idx]; - size_t seq_len = gb->get_output_buffer(input).shape[0]; - size_t num_heads = config_.attention_heads; - size_t kv_heads = config_.attention_kv_heads; - size_t head_dim = config_.attention_head_dim; - int share_src = (layer_idx < kv_share_map_.size()) ? kv_share_map_[layer_idx] : -1; - - bool is_global = false; - if (!config_.layer_types.empty() && layer_idx < config_.layer_types.size()) { - const auto& lt = config_.layer_types[layer_idx]; - is_global = (lt == "global" || lt == "full" || lt == "full_attention"); - } else { - is_global = (layer_idx % 5) == 4; - } - float rope_freq = is_global ? config_.rope_theta : config_.rope_local_base_freq; - size_t window = is_global ? 0 : config_.sliding_window; - - auto q = gb->matmul(input, layer.attn_q_weight, true, backend); - q = gb->reshape(q, {seq_len * num_heads, head_dim}); - q = gb->rms_norm(q, layer.attn_q_norm_weight, config_.layer_norm_eps); - q = gb->reshape(q, {1, seq_len, num_heads, head_dim}); - auto q4 = gb->rope(q, rope_freq, position_offset); - - size_t k4, v4; - if (share_src >= 0 && shared_k_nodes_[share_src] != 0) { - k4 = shared_k_nodes_[share_src]; - v4 = shared_v_nodes_[share_src]; - } else { - auto k = gb->matmul(input, layer.attn_k_weight, true, backend); - k = gb->reshape(k, {seq_len * kv_heads, head_dim}); - k = gb->rms_norm(k, layer.attn_k_norm_weight, config_.layer_norm_eps); - k = gb->reshape(k, {1, seq_len, kv_heads, head_dim}); - k4 = gb->rope(k, rope_freq, position_offset); - - auto v_proj = gb->matmul(input, layer.attn_v_weight, true, backend); - auto v = build_rms_norm_no_weight(gb, v_proj, seq_len * kv_heads, head_dim); - v4 = gb->reshape(v, {1, seq_len, kv_heads, head_dim}); - - shared_k_nodes_[layer_idx] = k4; - shared_v_nodes_[layer_idx] = v4; - } - - if (use_cache && share_src < 0) { - cache_k_output_nodes_[layer_idx] = k4; - cache_v_output_nodes_[layer_idx] = v4; - } - - size_t cache_src = (share_src >= 0) ? static_cast(share_src) : layer_idx; - size_t attn; - if (use_cache && !kv_cache_.is_empty()) { - attn = gb->attention_int8_hybrid( - q4, k4, v4, attention_scale_, position_offset, - kv_cache_.get_keys_int8(cache_src), kv_cache_.get_values_int8(cache_src), - kv_cache_.get_key_scales(cache_src), kv_cache_.get_value_scales(cache_src), - kv_cache_.current_seq_len, kv_heads, head_dim, window); - } else { - attn = gb->attention(q4, k4, v4, attention_scale_, position_offset, window); - } - - auto o_proj = gb->matmul(gb->reshape(attn, {seq_len, num_heads * head_dim}), layer.attn_output_weight, true, backend); - return o_proj; -} - - -size_t GemmaModel3n::build_mlp(CactusGraph* gb, size_t input, uint32_t layer_idx, - ComputeBackend backend) const { - const auto& layer = weight_nodes_.layers[layer_idx]; - - auto gate = gb->matmul(input, layer.ffn_gate_weight, true, backend); - auto up = gb->matmul(input, layer.ffn_up_weight, true, backend); - - if (layer_idx < config_.activation_sparsity_ppf.size() && config_.activation_sparsity_ppf[layer_idx] > 0.0f) - gate = build_gaussian_topk(gb, gate, config_.activation_sparsity_ppf[layer_idx]); - - auto gate_activated = gb->gelu(gate); - auto activated = gb->multiply(gate_activated, up); - return gb->matmul(activated, layer.ffn_down_weight, true, backend); -} - - -size_t GemmaModel3n::build_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache, size_t position_offset) { - const auto& layer = weight_nodes_.layers[layer_idx]; - auto normed = gb->rms_norm(hidden, layer.input_layernorm_weight, config_.layer_norm_eps); - - auto laurel = build_laurel(gb, normed, layer_idx, backend); - auto attn_raw = build_attention(gb, normed, layer_idx, backend, use_cache, position_offset); - auto attn = gb->rms_norm(attn_raw, layer.post_attention_layernorm_weight, config_.layer_norm_eps); - auto combined = gb->add(gb->add(hidden, attn), laurel); - auto residual = gb->scalar_multiply(combined, RSQRT2); - - auto pre_mlp = gb->rms_norm(residual, layer.pre_feedforward_layernorm_weight, config_.layer_norm_eps); - auto mlp_raw = build_mlp(gb, pre_mlp, layer_idx, backend); - auto mlp = gb->rms_norm(mlp_raw, layer.post_feedforward_layernorm_weight, config_.layer_norm_eps); - - auto block_out = gb->add(residual, mlp); - return block_out; -} - - -size_t GemmaModel3n::build_preamble(CactusGraph* gb, size_t seq_len, ComputeBackend backend, - size_t& token_input, size_t& pli_input, size_t* streams) { - uint32_t num_layers = config_.num_layers; - uint32_t pli_dim = config_.hidden_size_per_layer_input; - uint32_t num_altup = config_.altup_num_inputs; - - token_input = gb->input({seq_len}, Precision::FP32); - auto x = gb->scalar_multiply(gb->embedding(embedding_node_id_, token_input), - std::sqrt(static_cast(config_.hidden_dim))); - - pli_input = gb->input({seq_len}, Precision::FP32); - auto pli_embed = gb->scalar_multiply(gb->embedding(weight_nodes_.embed_tokens_per_layer, pli_input), - std::sqrt(static_cast(pli_dim))); - auto pli_proj = gb->scalar_multiply(gb->matmul(x, weight_nodes_.per_layer_model_proj, true, backend), - 1.0f / std::sqrt(static_cast(config_.hidden_dim))); - pli_proj = gb->reshape(pli_proj, {seq_len * num_layers, pli_dim}); - pli_proj = gb->rms_norm(pli_proj, weight_nodes_.per_layer_proj_norm, config_.layer_norm_eps); - pli_proj = gb->reshape(pli_proj, {seq_len, num_layers * pli_dim}); - auto pli_combined = gb->scalar_multiply(gb->add(pli_proj, pli_embed), RSQRT2); - - streams[0] = x; - for (uint32_t i = 1; i < num_altup; i++) { - streams[i] = build_magnitude_normalize(gb, x, gb->matmul(x, weight_nodes_.altup_proj_weights[i - 1], true, backend)); - } - - return pli_combined; -} - -void GemmaModel3n::build_layer(CactusGraph* gb, uint32_t layer_idx, ComputeBackend backend, - bool use_cache, size_t pos_offset, size_t pli, size_t* streams) { - auto modalities = build_altup_router_modalities(gb, streams[0], layer_idx, backend); - - size_t predictions[4]; - build_altup_predict(gb, modalities, layer_idx, streams, predictions); - - auto activated = build_transformer_block(gb, predictions[0], layer_idx, backend, use_cache, pos_offset); - - auto modalities_post = build_altup_router_modalities(gb, activated, layer_idx, backend); - - build_altup_correct(gb, activated, modalities_post, layer_idx, backend, predictions, streams); - - if (config_.hidden_size_per_layer_input > 0) { - build_per_layer_input(gb, pli, layer_idx, backend, streams); - } -} - -size_t GemmaModel3n::build_output_head(CactusGraph* gb, size_t* streams, ComputeBackend backend) { - uint32_t num_altup = config_.altup_num_inputs; - - for (uint32_t i = 1; i < num_altup; i++) { - streams[i] = gb->matmul(streams[i], weight_nodes_.altup_unembed_proj_weights[i - 1], true, backend); - streams[i] = build_magnitude_normalize(gb, streams[0], streams[i]); - } - - auto hidden = streams[0]; - for (uint32_t i = 1; i < num_altup; i++) - hidden = gb->add(hidden, streams[i]); - hidden = gb->scalar_multiply(hidden, 1.0f / static_cast(num_altup)); - - auto final_normed = gb->rms_norm(hidden, weight_nodes_.output_norm_weight, config_.layer_norm_eps); - return final_normed; -} - -void GemmaModel3n::set_token_inputs(CactusGraph* gb, size_t token_input, size_t pli_input, - const std::vector& tokens) { - std::vector input_data(tokens.size()); - for (size_t i = 0; i < tokens.size(); i++) - input_data[i] = static_cast(tokens[i]); - gb->set_input(token_input, input_data.data(), Precision::FP32); - gb->set_input(pli_input, input_data.data(), Precision::FP32); -} - - -size_t GemmaModel3n::forward(const std::vector& tokens, bool use_cache) { - if (!initialized_ || !graph_handle_) - throw std::runtime_error("Model not initialized - call init() first"); - if (tokens.empty()) - throw std::runtime_error("Token sequence cannot be empty"); - - auto* gb = static_cast(graph_handle_); - gb->soft_reset(); - - size_t pos_offset = use_cache ? kv_cache_.get_total_seq_len() : 0; - auto backend = config_.default_backend == Config::Backend::CPU ? ComputeBackend::CPU : ComputeBackend::NPU; - - size_t token_input, pli_input, streams[4]; - auto pli = build_preamble(gb, tokens.size(), backend, token_input, pli_input, streams); - - for (uint32_t i = 0; i < config_.num_layers; i++) - build_layer(gb, i, backend, use_cache, pos_offset, pli, streams); - - set_token_inputs(gb, token_input, pli_input, tokens); - return build_output_head(gb, streams, backend); -} - - -size_t GemmaModel3n::forward_split(const std::vector& tokens, bool use_cache) { - auto* gb = static_cast(graph_handle_); - gb->soft_reset(); - - size_t seq_len = tokens.size(); - size_t pos_offset = use_cache ? kv_cache_.get_total_seq_len() : 0; - auto backend = config_.default_backend == Config::Backend::CPU ? ComputeBackend::CPU : ComputeBackend::NPU; - uint32_t num_layers = config_.num_layers; - uint32_t first_shared = num_layers - config_.num_kv_shared_layers; - - size_t token_input, pli_input, streams[4]; - auto pli = build_preamble(gb, seq_len, backend, token_input, pli_input, streams); - - for (uint32_t i = 0; i < first_shared; i++) - build_layer(gb, i, backend, use_cache, pos_offset, pli, streams); - - size_t hidden_dim = config_.hidden_dim; - for (uint32_t i = 0; i < config_.altup_num_inputs; i++) { - streams[i] = gb->index(streams[i], seq_len - 1, 0); - streams[i] = gb->reshape(streams[i], {1, hidden_dim}); - } - - auto pli_last = gb->index(pli, seq_len - 1, 0); - pli_last = gb->reshape(pli_last, {1, num_layers * config_.hidden_size_per_layer_input}); - - for (uint32_t i = first_shared; i < num_layers; i++) - build_layer(gb, i, backend, use_cache, pos_offset, pli_last, streams); - - set_token_inputs(gb, token_input, pli_input, tokens); - return build_output_head(gb, streams, backend); -} - - -void GemmaModel3n::prefill(const std::vector& tokens, size_t chunk_size, const std::string& profile_file) { - if (tokens.empty()) - return; - - static constexpr size_t SPLIT_PREFILL_MIN_TOKENS = 32; - bool use_split = config_.num_kv_shared_layers > 0 - && tokens.size() >= SPLIT_PREFILL_MIN_TOKENS - && !std::getenv("CACTUS_DISABLE_SPLIT_PREFILL"); - - if (!use_split) { - Model::prefill(tokens, chunk_size, profile_file); - return; - } - - auto* gb = static_cast(graph_handle_); - - auto process_chunk = [&](const std::vector& chunk) { - forward_split(chunk, true); - gb->execute(profile_file); - update_kv_cache(gb, chunk.size()); - }; - - if (tokens.size() <= chunk_size) { - process_chunk(tokens); - return; - } - - size_t num_full_chunks = (tokens.size() - 1) / chunk_size; - for (size_t i = 0; i < num_full_chunks; ++i) { - size_t start = i * chunk_size; - std::vector chunk(tokens.begin() + start, tokens.begin() + start + chunk_size); - if (i == 1) - gb->set_prefill_mode(true); - process_chunk(chunk); - } - - gb->set_prefill_mode(false); - size_t final_start = num_full_chunks * chunk_size; - std::vector final_chunk(tokens.begin() + final_start, tokens.end()); - process_chunk(final_chunk); -} - -} -} diff --git a/cactus/models/model_lfm2.cpp b/cactus/models/model_lfm2.cpp deleted file mode 100644 index 12d10cad9..000000000 --- a/cactus/models/model_lfm2.cpp +++ /dev/null @@ -1,389 +0,0 @@ -#include "model.h" -#include "../graph/graph.h" -#include -#include -#include -#include -#include -#include -#include -namespace cactus { -namespace engine { -LFM2Model::LFM2Model() : Model() { - weight_nodes_.layers.resize(config_.num_layers); - conv_cache_bx_nodes_.assign(config_.num_layers, 0); -} -LFM2Model::LFM2Model(const Config& config) : Model(config) { - weight_nodes_.layers.resize(config_.num_layers); - conv_cache_bx_nodes_.assign(config_.num_layers, 0); -} -void LFM2Model::post_init() { - if (config_.conv_L_cache > 0) { - Precision cache_precision = Precision::FP16; - size_t conv_window = config_.conv_L_cache > 0 ? config_.conv_L_cache - 1 : 0; - conv_cache_.init(config_.num_layers, config_.hidden_dim, conv_window, cache_precision); - - } - last_forward_used_cache_ = false; - -} -void LFM2Model::reset_cache() { - Model::reset_cache(); - - if (conv_cache_.window_size > 0) { - conv_cache_.reset(); - - } -} -bool LFM2Model::is_cache_empty() const { - return kv_cache_.is_empty(); -} -bool LFM2Model::init(const std::string& model_folder, size_t context_size, const std::string& system_prompt, bool do_warmup) { - if (!Model::init(model_folder, context_size, system_prompt, do_warmup)) { - return false; - } - - if (weight_nodes_.layers.size() != config_.num_layers) { - weight_nodes_.layers.resize(config_.num_layers); - - } - conv_cache_bx_nodes_.assign(config_.num_layers, 0); - - return true; -} -bool LFM2Model::init(CactusGraph* external_graph, const std::string& model_folder, size_t context_size, - const std::string& system_prompt, bool do_warmup) { - if (!Model::init(external_graph, model_folder, context_size, system_prompt, do_warmup)) { - return false; - } - - if (weight_nodes_.layers.size() != config_.num_layers) { - weight_nodes_.layers.resize(config_.num_layers); - - } - conv_cache_bx_nodes_.assign(config_.num_layers, 0); - - return true; -} -void LFM2Model::load_weights_to_graph(CactusGraph* gb) { - if (weight_nodes_.layers.size() != config_.num_layers) { - weight_nodes_.layers.resize(config_.num_layers); - } - if (conv_cache_bx_nodes_.size() != config_.num_layers) { - conv_cache_bx_nodes_.assign(config_.num_layers, 0); - } - embedding_node_id_ = gb->mmap_embeddings(embedding_file_path_); - weight_nodes_.output_norm_weight = gb->mmap_weights(model_folder_path_ + "/output_norm.weights"); - - if (config_.tie_word_embeddings) { - weight_nodes_.output_weight = embedding_node_id_; - output_weight_node_id_ = embedding_node_id_; - - } else { - weight_nodes_.output_weight = gb->mmap_weights(model_folder_path_ + "/output_weight.weights"); - output_weight_node_id_ = weight_nodes_.output_weight; - - } - for (uint32_t i = 0; i < config_.num_layers; i++) { - auto& layer_entry = weight_nodes_.layers[i]; - auto& layer = layer_entry.weights; - std::string layer_prefix = model_folder_path_ + "/layer_" + std::to_string(i) + "_"; - - bool is_conv_layer = false; - if (i < config_.layer_types.size()) { - std::string layer_type = config_.layer_types[i]; - is_conv_layer = (layer_type == "conv" || layer_type == "CONV"); - - } - if (is_conv_layer) { - layer_entry.type = WeightNodeIDs::LayerType::CONV; - layer.conv_in_proj_weight = gb->mmap_weights(layer_prefix + "conv_in_proj.weights"); - layer.conv_out_proj_weight = gb->mmap_weights(layer_prefix + "conv_out_proj.weights"); - layer.conv_depthwise_weight = gb->mmap_weights(layer_prefix + "conv_depthwise.weights"); - - } else { - layer_entry.type = WeightNodeIDs::LayerType::ATTENTION; - layer.attn_q_weight = gb->mmap_weights(layer_prefix + "attn_q.weights"); - layer.attn_k_weight = gb->mmap_weights(layer_prefix + "attn_k.weights"); - layer.attn_v_weight = gb->mmap_weights(layer_prefix + "attn_v.weights"); - layer.attn_output_weight = gb->mmap_weights(layer_prefix + "attn_output.weights"); - layer.attn_q_norm_weight = gb->mmap_weights(layer_prefix + "attn_q_norm.weights"); - layer.attn_k_norm_weight = gb->mmap_weights(layer_prefix + "attn_k_norm.weights"); - - } - layer.input_layernorm_weight = gb->mmap_weights(layer_prefix + "input_norm.weights"); - layer.post_attention_layernorm_weight = gb->mmap_weights(layer_prefix + "post_attn_norm.weights"); - layer.ffn_gate_weight = gb->mmap_weights(layer_prefix + "ffn_gate.weights"); - layer.ffn_up_weight = gb->mmap_weights(layer_prefix + "ffn_up.weights"); - layer.ffn_down_weight = gb->mmap_weights(layer_prefix + "ffn_down.weights"); - - } - -} -size_t LFM2Model::build_conv1d(CactusGraph* gb, size_t input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache) { - const auto& layer_entry = weight_nodes_.layers[layer_idx]; - const auto& layer = layer_entry.weights; - size_t in_proj = gb->matmul(input, layer.conv_in_proj_weight, true, backend); - const auto& in_proj_buf = gb->get_output_buffer(in_proj); - if (in_proj_buf.shape.size() != 2 || (in_proj_buf.shape[1] % 3) != 0) { - throw std::runtime_error("Conv in_proj output must be [L, 3*C]"); - } - - const size_t L = in_proj_buf.shape[0]; - const size_t C = in_proj_buf.shape[1] / 3; - - size_t triplet = gb->reshape(in_proj, {L, static_cast(3), C}); - size_t B = gb->slice(triplet, 1, 0, 1); - size_t Cg = gb->slice(triplet, 1, 1, 1); - size_t X = gb->slice(triplet, 1, 2, 1); - - B = gb->reshape(B, {L, C}); - - Cg = gb->reshape(Cg, {L, C}); - - X = gb->reshape(X, {L, C}); - size_t Bx = gb->multiply(B, X); - if (use_cache) { - conv_cache_bx_nodes_[layer_idx] = Bx; - - } else { - conv_cache_bx_nodes_[layer_idx] = 0; - - } - const auto& wbuf = gb->get_output_buffer(layer.conv_depthwise_weight); - size_t K = wbuf.shape.back(); - - size_t conv_w = layer.conv_depthwise_weight; - if (wbuf.shape.size() == 2) { - K = wbuf.shape[1]; - conv_w = gb->reshape(conv_w, {wbuf.shape[0], static_cast(1), K}); - } else if (wbuf.shape.size() != 3){ - throw std::runtime_error("Unexpected depthwise weight rank"); - } - K = wbuf.shape.back(); - - size_t conv_input_lc = Bx; - if (use_cache && conv_cache_.window_size > 0) { - auto view = conv_cache_.get_window(layer_idx); - std::vector segments; - if (view.len2 > 0) { - size_t left_node = gb->input({view.len2, C}, conv_cache_.precision); - gb->set_external_input(left_node, const_cast(view.ptr2), conv_cache_.precision); - segments.push_back(left_node); - - } - if (view.len1 > 0) { - size_t right_node = gb->input({view.len1, C}, conv_cache_.precision); - gb->set_external_input(right_node, const_cast(view.ptr1), conv_cache_.precision); - segments.push_back(right_node); - - } - if (!segments.empty()) { - conv_input_lc = segments[0]; - for (size_t idx = 1; idx < segments.size(); ++idx) { - conv_input_lc = gb->concat(conv_input_lc, segments[idx], 0); - - } - conv_input_lc = gb->concat(conv_input_lc, Bx, 0); - - } - } - - const auto& conv_input_buf = gb->get_output_buffer(conv_input_lc); - size_t total_len = conv_input_buf.shape[0]; - - size_t x_nlc = gb->reshape(conv_input_lc, {static_cast(1), total_len, C}); - const size_t dilation = 1; - - size_t y_nlc = gb->conv1d_causal(x_nlc, conv_w, K, dilation); - size_t start = total_len > L ? total_len - L : 0; - size_t y_slice = gb->slice(y_nlc, 1, start, L); - - size_t y_lc = gb->reshape(y_slice, {L, C}); - size_t gated = gb->multiply(Cg, y_lc); - size_t projected = gb->matmul(gated, layer.conv_out_proj_weight, true, backend); - return projected; -} - -size_t LFM2Model::build_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache, size_t position_offset) { - const auto& layer_entry = weight_nodes_.layers[layer_idx]; - const auto& layer = layer_entry.weights; - auto q_proj_linear = gb->matmul(normalized_input, layer.attn_q_weight, true, backend); - auto k_proj_linear = gb->matmul(normalized_input, layer.attn_k_weight, true, backend); - auto v_proj_linear = gb->matmul(normalized_input, layer.attn_v_weight, true, backend); - const auto& q_shape = gb->get_output_buffer(q_proj_linear).shape; - size_t batch_seq = q_shape[0]; - size_t num_heads = config_.attention_heads; - size_t head_dim = config_.attention_head_dim; - - auto q_proj_reshaped = gb->reshape(q_proj_linear, {batch_seq * num_heads, head_dim}); - - auto q_proj_norm = gb->rms_norm(q_proj_reshaped, layer.attn_q_norm_weight, config_.layer_norm_eps); - - auto q_proj = gb->reshape(q_proj_norm, {batch_seq, num_heads * head_dim}); - size_t num_kv_heads = config_.attention_kv_heads; - auto k_proj_reshaped = gb->reshape(k_proj_linear, {batch_seq * num_kv_heads, head_dim}); - - auto k_proj_norm = gb->rms_norm(k_proj_reshaped, layer.attn_k_norm_weight, config_.layer_norm_eps); - - auto k_proj = gb->reshape(k_proj_norm, {batch_seq, num_kv_heads * head_dim}); - size_t seq_len = batch_seq; - - auto q_proj_4d = gb->reshape(q_proj, {1, seq_len, config_.attention_heads, config_.attention_head_dim}); - auto k_proj_4d = gb->reshape(k_proj, {1, seq_len, config_.attention_kv_heads, config_.attention_head_dim}); - auto v_proj_4d = gb->reshape(v_proj_linear, {1, seq_len, config_.attention_kv_heads, config_.attention_head_dim}); - - if (config_.rope_theta > 0) { - q_proj_4d = gb->rope(q_proj_4d, config_.rope_theta, position_offset); - k_proj_4d = gb->rope(k_proj_4d, config_.rope_theta, position_offset); - } - if (use_cache) { - cache_k_output_nodes_[layer_idx] = k_proj_4d; - cache_v_output_nodes_[layer_idx] = v_proj_4d; - } - - size_t attn_output_4d; - - if (use_cache && !kv_cache_.is_empty()) { - attn_output_4d = gb->attention_int8_hybrid( - q_proj_4d, k_proj_4d, v_proj_4d, - attention_scale_, position_offset, - kv_cache_.get_keys_int8(layer_idx), - kv_cache_.get_values_int8(layer_idx), - kv_cache_.get_key_scales(layer_idx), - kv_cache_.get_value_scales(layer_idx), - kv_cache_.current_seq_len, num_kv_heads, head_dim - ); - } else { - attn_output_4d = gb->attention(q_proj_4d, k_proj_4d, v_proj_4d, attention_scale_, position_offset); - } - auto attn_output = gb->reshape(attn_output_4d, {seq_len, config_.attention_head_dim * config_.attention_heads}); - auto projected = gb->matmul(attn_output, layer.attn_output_weight, true, backend); - return projected; -} - -size_t LFM2Model::build_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, ComputeBackend backend) const { - const auto& layer_entry = weight_nodes_.layers[layer_idx]; - const auto& layer = layer_entry.weights; - auto gate = gb->matmul(normalized_h, layer.ffn_gate_weight, true, backend); - auto up = gb->matmul(normalized_h, layer.ffn_up_weight, true, backend); - auto activated = gb->multiply(gb->silu(gate), up); - - auto down = gb->matmul(activated, layer.ffn_down_weight, true, backend); - - return down; -} - -size_t LFM2Model::build_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache, size_t position_offset) { - const auto& layer_entry = weight_nodes_.layers[layer_idx]; - const auto& layer = layer_entry.weights; - auto normalized_input = gb->rms_norm(hidden, layer.input_layernorm_weight, config_.layer_norm_eps); - size_t block_output; - if (layer_entry.type == WeightNodeIDs::LayerType::CONV) { - block_output = build_conv1d(gb, normalized_input, layer_idx, backend, use_cache); - } else { - if (layer_idx < conv_cache_bx_nodes_.size()) { - conv_cache_bx_nodes_[layer_idx] = 0; - } - block_output = build_attention(gb, normalized_input, layer_idx, backend, use_cache, position_offset); - } - - auto after_block = gb->add(hidden, block_output); - auto normalized_after_block = gb->rms_norm(after_block, layer.post_attention_layernorm_weight, config_.layer_norm_eps); - auto mlp_output = build_mlp(gb, normalized_after_block, layer_idx, backend); - auto block_result = gb->add(after_block, mlp_output); - return block_result; -} - -size_t LFM2Model::forward(CactusGraph* gb, size_t input_embeddings, size_t seq_len, - ComputeBackend backend, bool use_cache) { - if (seq_len == 0) { - throw std::runtime_error("Sequence length must be greater than zero"); - } - - if (conv_cache_bx_nodes_.size() != config_.num_layers) { - conv_cache_bx_nodes_.assign(config_.num_layers, 0); - - } - std::fill(conv_cache_bx_nodes_.begin(), conv_cache_bx_nodes_.end(), 0); - - last_forward_used_cache_ = use_cache; - - if (!use_cache && conv_cache_.window_size > 0) { - conv_cache_.reset(); - - } - size_t position_offset = use_cache ? kv_cache_.get_total_seq_len() : 0; - - size_t hidden = input_embeddings; - - for (uint32_t layer_idx = 0; layer_idx < config_.num_layers; layer_idx++) { - hidden = build_transformer_block(gb, hidden, layer_idx, backend, use_cache, position_offset); - - } - - auto final_hidden = gb->rms_norm(hidden, weight_nodes_.output_norm_weight, config_.layer_norm_eps); - - return final_hidden; -} -size_t LFM2Model::forward(CactusGraph* gb, const std::vector& tokens, - ComputeBackend backend, bool use_cache) { - if (tokens.empty()) { - throw std::runtime_error("Token sequence cannot be empty"); - } - auto seq_len = static_cast(tokens.size()); - auto input_node_id = gb->input({seq_len}, Precision::FP32); - auto hidden = gb->embedding(embedding_node_id_, input_node_id); - auto final_hidden = forward(gb, hidden, seq_len, backend, use_cache); - - std::vector input_data(seq_len); - for (size_t i = 0; i < seq_len; i++) { - input_data[i] = static_cast(tokens[i]); - } - gb->set_input(input_node_id, input_data.data(), Precision::FP32); - - return final_hidden; -} -size_t LFM2Model::forward(const std::vector& tokens, bool use_cache) { - if (!initialized_ || !graph_handle_) { - throw std::runtime_error("Model not initialized - call init() first"); - } - - auto* gb = static_cast(graph_handle_); - gb->soft_reset(); - - auto backend = config_.default_backend == Config::Backend::CPU - ? ComputeBackend::CPU - : ComputeBackend::NPU; - return forward(gb, tokens, backend, use_cache); -} -void LFM2Model::post_execute_updates(CactusGraph* gb, size_t /*seq_len*/) { - if (conv_cache_bx_nodes_.empty()) { - return; - } - - if (!last_forward_used_cache_ || conv_cache_.window_size == 0) { - std::fill(conv_cache_bx_nodes_.begin(), conv_cache_bx_nodes_.end(), 0); - - return; - } - size_t layer_count = std::min(conv_cache_bx_nodes_.size(), weight_nodes_.layers.size()); - for (size_t layer_idx = 0; layer_idx < layer_count; ++layer_idx) { - if (weight_nodes_.layers[layer_idx].type != WeightNodeIDs::LayerType::CONV) { - conv_cache_bx_nodes_[layer_idx] = 0; - continue; - } - size_t bx_node = conv_cache_bx_nodes_[layer_idx]; - if (bx_node != 0) { - conv_cache_.update(gb, layer_idx, bx_node); - } - conv_cache_bx_nodes_[layer_idx] = 0; - } - last_forward_used_cache_ = false; - -} -} -} \ No newline at end of file diff --git a/cactus/models/model_lfm2moe.cpp b/cactus/models/model_lfm2moe.cpp deleted file mode 100644 index e867531bb..000000000 --- a/cactus/models/model_lfm2moe.cpp +++ /dev/null @@ -1,456 +0,0 @@ -#include "model.h" -#include "../graph/graph.h" -#include -#include -#include -#include -#include -#include - -namespace cactus { -namespace engine { - -LFM2MoEModel::LFM2MoEModel() : Model() { - weight_nodes_.layers.resize(config_.num_layers); - conv_cache_bx_nodes_.assign(config_.num_layers, 0); -} -LFM2MoEModel::LFM2MoEModel(const Config& config) : Model(config) { - weight_nodes_.layers.resize(config_.num_layers); - conv_cache_bx_nodes_.assign(config_.num_layers, 0); -} -void LFM2MoEModel::post_init() { - if (config_.num_dense_layers > config_.num_layers) { - throw std::runtime_error("num_dense_layers cannot be greater than num_layers for LFM2MoEModel"); - } - if (config_.num_experts == 0 || config_.num_experts_per_tok == 0 || config_.moe_intermediate_dim == 0) { - throw std::runtime_error("LFM2MoEModel requires num_experts, num_experts_per_tok, and moe_intermediate_dim to be non-zero"); - } - if (!config_.use_expert_bias || !config_.norm_topk_prob) { - throw std::runtime_error("LFM2MoEModel currently requires use_expert_bias=true and norm_topk_prob=true"); - } - if (config_.num_experts_per_tok > config_.num_experts) { - throw std::runtime_error("num_experts_per_tok cannot be greater than num_experts"); - } - - if (config_.conv_L_cache > 0) { - Precision cache_precision = Precision::FP16; - size_t conv_window = config_.conv_L_cache > 0 ? config_.conv_L_cache - 1 : 0; - conv_cache_.init(config_.num_layers, config_.hidden_dim, conv_window, cache_precision); - - } - last_forward_used_cache_ = false; - -} - -bool LFM2MoEModel::is_dense_layer(uint32_t layer_idx) const { - return layer_idx < config_.num_dense_layers; -} -void LFM2MoEModel::reset_cache() { - Model::reset_cache(); - - if (conv_cache_.window_size > 0) { - conv_cache_.reset(); - - } -} -bool LFM2MoEModel::is_cache_empty() const { - return kv_cache_.is_empty(); -} -bool LFM2MoEModel::init(const std::string& model_folder, size_t context_size, const std::string& system_prompt, bool do_warmup) { - if (!Model::init(model_folder, context_size, system_prompt, do_warmup)) { - return false; - } - - if (weight_nodes_.layers.size() != config_.num_layers) { - weight_nodes_.layers.resize(config_.num_layers); - - } - conv_cache_bx_nodes_.assign(config_.num_layers, 0); - - return true; -} -bool LFM2MoEModel::init(CactusGraph* external_graph, const std::string& model_folder, size_t context_size, - const std::string& system_prompt, bool do_warmup) { - if (!Model::init(external_graph, model_folder, context_size, system_prompt, do_warmup)) { - return false; - } - - if (weight_nodes_.layers.size() != config_.num_layers) { - weight_nodes_.layers.resize(config_.num_layers); - - } - conv_cache_bx_nodes_.assign(config_.num_layers, 0); - - return true; -} -void LFM2MoEModel::load_weights_to_graph(CactusGraph* gb) { - if (weight_nodes_.layers.size() != config_.num_layers) { - weight_nodes_.layers.resize(config_.num_layers); - } - if (conv_cache_bx_nodes_.size() != config_.num_layers) { - conv_cache_bx_nodes_.assign(config_.num_layers, 0); - } - auto mmap_or_throw = [&](const std::string& path, bool embedding) -> size_t { - size_t node_id = embedding ? gb->mmap_embeddings(path) : gb->mmap_weights(path); - return node_id; - }; - - embedding_node_id_ = mmap_or_throw(embedding_file_path_, true); - weight_nodes_.output_norm_weight = mmap_or_throw(model_folder_path_ + "/output_norm.weights", false); - - if (config_.tie_word_embeddings) { - weight_nodes_.output_weight = embedding_node_id_; - output_weight_node_id_ = embedding_node_id_; - - } else { - weight_nodes_.output_weight = mmap_or_throw(model_folder_path_ + "/output_weight.weights", false); - output_weight_node_id_ = weight_nodes_.output_weight; - - } - for (uint32_t i = 0; i < config_.num_layers; i++) { - auto& layer_entry = weight_nodes_.layers[i]; - auto& layer = layer_entry.weights; - std::string layer_prefix = model_folder_path_ + "/layer_" + std::to_string(i) + "_"; - - bool is_conv_layer = false; - if (i < config_.layer_types.size()) { - std::string layer_type = config_.layer_types[i]; - is_conv_layer = (layer_type == "conv" || layer_type == "CONV"); - - } - if (is_conv_layer) { - layer_entry.type = WeightNodeIDs::LayerType::CONV; - layer.conv_in_proj_weight = mmap_or_throw(layer_prefix + "conv_in_proj.weights", false); - layer.conv_out_proj_weight = mmap_or_throw(layer_prefix + "conv_out_proj.weights", false); - layer.conv_depthwise_weight = mmap_or_throw(layer_prefix + "conv_depthwise.weights", false); - - } else { - layer_entry.type = WeightNodeIDs::LayerType::ATTENTION; - layer.attn_q_weight = mmap_or_throw(layer_prefix + "attn_q.weights", false); - layer.attn_k_weight = mmap_or_throw(layer_prefix + "attn_k.weights", false); - layer.attn_v_weight = mmap_or_throw(layer_prefix + "attn_v.weights", false); - layer.attn_output_weight = mmap_or_throw(layer_prefix + "attn_output.weights", false); - layer.attn_q_norm_weight = mmap_or_throw(layer_prefix + "attn_q_norm.weights", false); - layer.attn_k_norm_weight = mmap_or_throw(layer_prefix + "attn_k_norm.weights", false); - - } - layer.input_layernorm_weight = mmap_or_throw(layer_prefix + "input_norm.weights", false); - layer.post_attention_layernorm_weight = mmap_or_throw(layer_prefix + "post_attn_norm.weights", false); - if (is_dense_layer(i)) { - layer.ffn_gate_weight = mmap_or_throw(layer_prefix + "ffn_gate.weights", false); - layer.ffn_up_weight = mmap_or_throw(layer_prefix + "ffn_up.weights", false); - layer.ffn_down_weight = mmap_or_throw(layer_prefix + "ffn_down.weights", false); - } else { - layer.moe_router_weight = mmap_or_throw(layer_prefix + "moe_router.weights", false); - layer.moe_expert_bias = mmap_or_throw(layer_prefix + "moe_expert_bias.weights", false); - layer.moe_experts.resize(config_.num_experts); - for (uint32_t expert_idx = 0; expert_idx < config_.num_experts; ++expert_idx) { - auto& expert = layer.moe_experts[expert_idx]; - std::string expert_prefix = layer_prefix + "moe_expert_" + std::to_string(expert_idx) + "_"; - expert.w1_weight = mmap_or_throw(expert_prefix + "w1.weights", false); - expert.w3_weight = mmap_or_throw(expert_prefix + "w3.weights", false); - expert.w2_weight = mmap_or_throw(expert_prefix + "w2.weights", false); - } - } - - } - -} -size_t LFM2MoEModel::build_conv1d(CactusGraph* gb, size_t input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache) { - const auto& layer_entry = weight_nodes_.layers[layer_idx]; - const auto& layer = layer_entry.weights; - size_t in_proj = gb->matmul(input, layer.conv_in_proj_weight, true, backend); - const auto& in_proj_buf = gb->get_output_buffer(in_proj); - if (in_proj_buf.shape.size() != 2 || (in_proj_buf.shape[1] % 3) != 0) { - throw std::runtime_error("Conv in_proj output must be [L, 3*C]"); - } - - const size_t L = in_proj_buf.shape[0]; - const size_t C = in_proj_buf.shape[1] / 3; - - size_t triplet = gb->reshape(in_proj, {L, static_cast(3), C}); - size_t B = gb->slice(triplet, 1, 0, 1); - size_t Cg = gb->slice(triplet, 1, 1, 1); - size_t X = gb->slice(triplet, 1, 2, 1); - - B = gb->reshape(B, {L, C}); - - Cg = gb->reshape(Cg, {L, C}); - - X = gb->reshape(X, {L, C}); - size_t Bx = gb->multiply(B, X); - if (use_cache) { - conv_cache_bx_nodes_[layer_idx] = Bx; - - } else { - conv_cache_bx_nodes_[layer_idx] = 0; - - } - const auto& wbuf = gb->get_output_buffer(layer.conv_depthwise_weight); - size_t K = wbuf.shape.back(); - - size_t conv_w = layer.conv_depthwise_weight; - if (wbuf.shape.size() == 2) { - K = wbuf.shape[1]; - conv_w = gb->reshape(conv_w, {wbuf.shape[0], static_cast(1), K}); - } else if (wbuf.shape.size() != 3){ - throw std::runtime_error("Unexpected depthwise weight rank"); - } - K = wbuf.shape.back(); - - size_t conv_input_lc = Bx; - if (use_cache && conv_cache_.window_size > 0) { - auto view = conv_cache_.get_window(layer_idx); - std::vector segments; - if (view.len2 > 0) { - size_t left_node = gb->input({view.len2, C}, conv_cache_.precision); - gb->set_external_input(left_node, const_cast(view.ptr2), conv_cache_.precision); - segments.push_back(left_node); - - } - if (view.len1 > 0) { - size_t right_node = gb->input({view.len1, C}, conv_cache_.precision); - gb->set_external_input(right_node, const_cast(view.ptr1), conv_cache_.precision); - segments.push_back(right_node); - - } - if (!segments.empty()) { - conv_input_lc = segments[0]; - for (size_t idx = 1; idx < segments.size(); ++idx) { - conv_input_lc = gb->concat(conv_input_lc, segments[idx], 0); - - } - conv_input_lc = gb->concat(conv_input_lc, Bx, 0); - - } - } - - const auto& conv_input_buf = gb->get_output_buffer(conv_input_lc); - size_t total_len = conv_input_buf.shape[0]; - - size_t x_nlc = gb->reshape(conv_input_lc, {static_cast(1), total_len, C}); - const size_t dilation = 1; - - size_t y_nlc = gb->conv1d_causal(x_nlc, conv_w, K, dilation); - size_t start = total_len > L ? total_len - L : 0; - size_t y_slice = gb->slice(y_nlc, 1, start, L); - - size_t y_lc = gb->reshape(y_slice, {L, C}); - size_t gated = gb->multiply(Cg, y_lc); - size_t projected = gb->matmul(gated, layer.conv_out_proj_weight, true, backend); - return projected; -} - -size_t LFM2MoEModel::build_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache, size_t position_offset) { - const auto& layer_entry = weight_nodes_.layers[layer_idx]; - const auto& layer = layer_entry.weights; - auto q_proj_linear = gb->matmul(normalized_input, layer.attn_q_weight, true, backend); - auto k_proj_linear = gb->matmul(normalized_input, layer.attn_k_weight, true, backend); - auto v_proj_linear = gb->matmul(normalized_input, layer.attn_v_weight, true, backend); - const auto& q_shape = gb->get_output_buffer(q_proj_linear).shape; - size_t batch_seq = q_shape[0]; - size_t num_heads = config_.attention_heads; - size_t head_dim = config_.attention_head_dim; - - auto q_proj_reshaped = gb->reshape(q_proj_linear, {batch_seq * num_heads, head_dim}); - - auto q_proj_norm = gb->rms_norm(q_proj_reshaped, layer.attn_q_norm_weight, config_.layer_norm_eps); - - auto q_proj = gb->reshape(q_proj_norm, {batch_seq, num_heads * head_dim}); - size_t num_kv_heads = config_.attention_kv_heads; - auto k_proj_reshaped = gb->reshape(k_proj_linear, {batch_seq * num_kv_heads, head_dim}); - - auto k_proj_norm = gb->rms_norm(k_proj_reshaped, layer.attn_k_norm_weight, config_.layer_norm_eps); - - auto k_proj = gb->reshape(k_proj_norm, {batch_seq, num_kv_heads * head_dim}); - size_t seq_len = batch_seq; - - auto q_proj_4d = gb->reshape(q_proj, {1, seq_len, config_.attention_heads, config_.attention_head_dim}); - auto k_proj_4d = gb->reshape(k_proj, {1, seq_len, config_.attention_kv_heads, config_.attention_head_dim}); - auto v_proj_4d = gb->reshape(v_proj_linear, {1, seq_len, config_.attention_kv_heads, config_.attention_head_dim}); - - if (config_.rope_theta > 0) { - q_proj_4d = gb->rope(q_proj_4d, config_.rope_theta, position_offset); - k_proj_4d = gb->rope(k_proj_4d, config_.rope_theta, position_offset); - } - if (use_cache) { - cache_k_output_nodes_[layer_idx] = k_proj_4d; - cache_v_output_nodes_[layer_idx] = v_proj_4d; - } - - size_t attn_output_4d; - - if (use_cache && !kv_cache_.is_empty()) { - attn_output_4d = gb->attention_int8_hybrid( - q_proj_4d, k_proj_4d, v_proj_4d, - attention_scale_, position_offset, - kv_cache_.get_keys_int8(layer_idx), - kv_cache_.get_values_int8(layer_idx), - kv_cache_.get_key_scales(layer_idx), - kv_cache_.get_value_scales(layer_idx), - kv_cache_.current_seq_len, num_kv_heads, head_dim - ); - } else { - attn_output_4d = gb->attention(q_proj_4d, k_proj_4d, v_proj_4d, attention_scale_, position_offset); - } - auto attn_output = gb->reshape(attn_output_4d, {seq_len, config_.attention_head_dim * config_.attention_heads}); - auto projected = gb->matmul(attn_output, layer.attn_output_weight, true, backend); - return projected; -} - -size_t LFM2MoEModel::build_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, ComputeBackend backend) const { - const auto& layer_entry = weight_nodes_.layers[layer_idx]; - const auto& layer = layer_entry.weights; - if (is_dense_layer(layer_idx)) { - auto gate = gb->matmul(normalized_h, layer.ffn_gate_weight, true, backend); - auto up = gb->matmul(normalized_h, layer.ffn_up_weight, true, backend); - auto activated = gb->multiply(gb->silu(gate), up); - auto down = gb->matmul(activated, layer.ffn_down_weight, true, backend); - return down; - } - - auto router_logits = gb->matmul(normalized_h, layer.moe_router_weight, true, backend); - auto routing_probs = gb->sigmoid(router_logits); - auto scores_for_routing = gb->add(routing_probs, layer.moe_expert_bias); - auto topk_indices_and_values = gb->topk(scores_for_routing, config_.num_experts_per_tok); - auto topk_indices = gb->index(topk_indices_and_values, 0, 0); - - std::vector w1_weights, w3_weights, w2_weights; - w1_weights.reserve(config_.num_experts); - w3_weights.reserve(config_.num_experts); - w2_weights.reserve(config_.num_experts); - for (uint32_t expert_idx = 0; expert_idx < config_.num_experts; ++expert_idx) { - const auto& expert = layer.moe_experts[expert_idx]; - w1_weights.push_back(expert.w1_weight); - w3_weights.push_back(expert.w3_weight); - w2_weights.push_back(expert.w2_weight); - } - - return gb->moe_layer( - normalized_h, - routing_probs, - topk_indices, - w1_weights, - w3_weights, - w2_weights, - config_.num_experts, - config_.num_experts_per_tok, - config_.norm_topk_prob, - 1e-6f, - config_.routed_scaling_factor - ); -} - -size_t LFM2MoEModel::build_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache, size_t position_offset) { - const auto& layer_entry = weight_nodes_.layers[layer_idx]; - const auto& layer = layer_entry.weights; - auto normalized_input = gb->rms_norm(hidden, layer.input_layernorm_weight, config_.layer_norm_eps); - size_t block_output; - if (layer_entry.type == WeightNodeIDs::LayerType::CONV) { - block_output = build_conv1d(gb, normalized_input, layer_idx, backend, use_cache); - } else { - if (layer_idx < conv_cache_bx_nodes_.size()) { - conv_cache_bx_nodes_[layer_idx] = 0; - } - block_output = build_attention(gb, normalized_input, layer_idx, backend, use_cache, position_offset); - } - - auto after_block = gb->add(hidden, block_output); - auto normalized_after_block = gb->rms_norm(after_block, layer.post_attention_layernorm_weight, config_.layer_norm_eps); - auto mlp_output = build_mlp(gb, normalized_after_block, layer_idx, backend); - auto block_result = gb->add(after_block, mlp_output); - return block_result; -} - -size_t LFM2MoEModel::forward(CactusGraph* gb, size_t input_embeddings, size_t seq_len, - ComputeBackend backend, bool use_cache) { - if (seq_len == 0) { - throw std::runtime_error("Sequence length must be greater than zero"); - } - - if (conv_cache_bx_nodes_.size() != config_.num_layers) { - conv_cache_bx_nodes_.assign(config_.num_layers, 0); - - } - std::fill(conv_cache_bx_nodes_.begin(), conv_cache_bx_nodes_.end(), 0); - - last_forward_used_cache_ = use_cache; - - if (!use_cache && conv_cache_.window_size > 0) { - conv_cache_.reset(); - - } - size_t position_offset = use_cache ? kv_cache_.get_total_seq_len() : 0; - - size_t hidden = input_embeddings; - - for (uint32_t layer_idx = 0; layer_idx < config_.num_layers; layer_idx++) { - hidden = build_transformer_block(gb, hidden, layer_idx, backend, use_cache, position_offset); - - } - - auto final_hidden = gb->rms_norm(hidden, weight_nodes_.output_norm_weight, config_.layer_norm_eps); - - return final_hidden; -} -size_t LFM2MoEModel::forward(CactusGraph* gb, const std::vector& tokens, - ComputeBackend backend, bool use_cache) { - if (tokens.empty()) { - throw std::runtime_error("Token sequence cannot be empty"); - } - auto seq_len = static_cast(tokens.size()); - auto input_node_id = gb->input({seq_len}, Precision::FP32); - auto hidden = gb->embedding(embedding_node_id_, input_node_id); - auto final_hidden = forward(gb, hidden, seq_len, backend, use_cache); - - std::vector input_data(seq_len); - for (size_t i = 0; i < seq_len; i++) { - input_data[i] = static_cast(tokens[i]); - } - gb->set_input(input_node_id, input_data.data(), Precision::FP32); - - return final_hidden; -} -size_t LFM2MoEModel::forward(const std::vector& tokens, bool use_cache) { - if (!initialized_ || !graph_handle_) { - throw std::runtime_error("Model not initialized - call init() first"); - } - - auto* gb = static_cast(graph_handle_); - gb->soft_reset(); - - auto backend = config_.default_backend == Config::Backend::CPU - ? ComputeBackend::CPU - : ComputeBackend::NPU; - return forward(gb, tokens, backend, use_cache); -} -void LFM2MoEModel::post_execute_updates(CactusGraph* gb, size_t /*seq_len*/) { - if (conv_cache_bx_nodes_.empty()) { - return; - } - - if (!last_forward_used_cache_ || conv_cache_.window_size == 0) { - std::fill(conv_cache_bx_nodes_.begin(), conv_cache_bx_nodes_.end(), 0); - - return; - } - size_t layer_count = std::min(conv_cache_bx_nodes_.size(), weight_nodes_.layers.size()); - for (size_t layer_idx = 0; layer_idx < layer_count; ++layer_idx) { - if (weight_nodes_.layers[layer_idx].type != WeightNodeIDs::LayerType::CONV) { - conv_cache_bx_nodes_[layer_idx] = 0; - continue; - } - size_t bx_node = conv_cache_bx_nodes_[layer_idx]; - if (bx_node != 0) { - conv_cache_.update(gb, layer_idx, bx_node); - } - conv_cache_bx_nodes_[layer_idx] = 0; - } - last_forward_used_cache_ = false; - -} -} -} diff --git a/cactus/models/model_lfm2vl.cpp b/cactus/models/model_lfm2vl.cpp deleted file mode 100644 index c4357ad0b..000000000 --- a/cactus/models/model_lfm2vl.cpp +++ /dev/null @@ -1,607 +0,0 @@ -#include "model.h" -#include "../graph/graph.h" -#include -#include -#include -#include -#include - -namespace cactus { -namespace engine { - -Lfm2VlModel::Lfm2VlModel() : Model() { - config_.model_type = Config::ModelType::LFM2; -} - -Lfm2VlModel::Lfm2VlModel(const Config& config) - : Model(config), - vision_tower_(config), - language_model_(config) { - Siglip2Preprocessor::Config preprocessor_config; - preprocessor_config.patch_size = static_cast(config.vision_patch_size); - preprocessor_config.downsample_factor = static_cast(config.downsample_factor); - preprocessor_config.min_tiles = static_cast(config.min_tiles); - preprocessor_config.max_tiles = static_cast(config.max_tiles); - preprocessor_config.use_thumbnail = config.use_thumbnail; - preprocessor_config.min_image_tokens = static_cast(config.min_image_tokens); - preprocessor_config.max_image_tokens = static_cast(config.max_image_tokens); - preprocessor_config.max_num_patches = static_cast(config.max_num_patches); - preprocessor_config.tile_size = static_cast(config.tile_size); - preprocessor_config.max_pixels_tolerance = config.max_pixels_tolerance; - preprocessor_config.do_resize = true; - preprocessor_config.do_rescale = true; - preprocessor_config.do_normalize = true; - preprocessor_config.do_convert_rgb = true; - preprocessor_config.do_image_splitting = config.do_image_splitting; - preprocessor_config.rescale_factor = config.rescale_factor; - preprocessor_config.image_mean[0] = config.image_mean; - preprocessor_config.image_mean[1] = config.image_mean; - preprocessor_config.image_mean[2] = config.image_mean; - preprocessor_config.image_std[0] = config.image_std; - preprocessor_config.image_std[1] = config.image_std; - preprocessor_config.image_std[2] = config.image_std; - - preprocessor_ = Siglip2Preprocessor(preprocessor_config); -} - -bool Lfm2VlModel::init(const std::string& model_folder, size_t context_size, const std::string& system_prompt, bool do_warmup) { - - if (!Model::init(model_folder, context_size, system_prompt, false)) { - return false; - } - auto* shared_graph = static_cast(graph_handle_); - if (!shared_graph) { - throw std::runtime_error("Shared graph was not initialized for Lfm2VlModel"); - } - std::string vision_folder = model_folder; - if (!vision_tower_.init(shared_graph, vision_folder, context_size, "", false)) { - throw std::runtime_error("Failed to initialize vision tower"); - } - - vision_weights_loaded_ = true; - if (!language_model_.init(shared_graph, model_folder, context_size, system_prompt, false)) { - throw std::runtime_error("Failed to initialize language model"); - } - - language_model_.output_weight_node_id_ = output_weight_node_id_; - - language_weights_loaded_ = true; - - if (do_warmup) { - std::string warmup_text = system_prompt.empty() ? "Hello" : system_prompt; - auto warmup_tokens = tokenizer_->encode(warmup_text); - language_model_.decode(warmup_tokens, config_.default_temperature, config_.default_top_p, config_.default_top_k, ""); - language_model_.reset_cache(); - } - - return true; -} - -void Lfm2VlModel::reset_cache() { - Model::reset_cache(); - language_model_.reset_cache(); - image_prefill_completed_ = false; - last_token_count_ = 0; -} - -void Lfm2VlModel::load_weights_to_graph(CactusGraph* gb) { - namespace fs = std::filesystem; - fs::path base(model_folder_path_); - - auto resolve_weight = [&](const std::string& primary, const std::string& fallback = "") -> std::string { - fs::path primary_path = base / primary; - if (fs::exists(primary_path)) { - return primary_path.string(); - } - if (!fallback.empty()) { - fs::path fallback_path = base / fallback; - if (fs::exists(fallback_path)) { - return fallback_path.string(); - } - } - return primary_path.string(); - }; - - try { - projector_weights_.layer_norm_weight = gb->mmap_weights(resolve_weight("projector_layer_norm.weights")); - projector_weights_.layer_norm_bias = gb->mmap_weights(resolve_weight("projector_layer_norm.bias.weights")); - } catch (const std::exception&) { - projector_weights_.layer_norm_weight = 0; - projector_weights_.layer_norm_bias = 0; - } - - projector_weights_.linear_1_weight = gb->mmap_weights(resolve_weight("projector_linear_1.weights", "projector_linear1.weights")); - projector_weights_.linear_1_bias = gb->mmap_weights(resolve_weight("projector_linear_1.bias.weights", "projector_linear1.bias.weights")); - projector_weights_.linear_2_weight = gb->mmap_weights(resolve_weight("projector_linear_2.weights", "projector_linear2.weights")); - projector_weights_.linear_2_bias = gb->mmap_weights(resolve_weight("projector_linear_2.bias.weights", "projector_linear2.bias.weights")); - output_weight_node_id_ = gb->mmap_weights(resolve_weight("token_embeddings.weights")); -} - -size_t Lfm2VlModel::pixel_unshuffle(CactusGraph* gb, size_t hidden_states, - size_t height, size_t width, size_t channels) { - - const size_t factor = config_.downsample_factor; - const size_t new_height = height / factor; - const size_t new_width = width / factor; - size_t step1 = gb->reshape(hidden_states, {1, height, new_width, channels * factor}); - step1 = gb->transposeN(step1, {0, 2, 1, 3}); - size_t step2 = gb->reshape(step1, {1, new_width, new_height, channels * factor * factor}); - size_t result = gb->transposeN(step2, {0, 2, 1, 3}); - return result; -} - -size_t Lfm2VlModel::build_multimodal_projector(CactusGraph* gb, size_t image_features, - size_t tile_h, size_t tile_w, ComputeBackend backend) { - const size_t vision_hidden = config_.vision_embed_dim; - - const auto& input_buf = gb->get_output_buffer(image_features); - size_t image_features_fp16 = (input_buf.precision == Precision::FP16) - ? image_features - : gb->precision_cast(image_features, Precision::FP16); - - size_t unshuffled = pixel_unshuffle(gb, image_features_fp16, tile_h, tile_w, vision_hidden); - const size_t factor = config_.downsample_factor; - const size_t new_h = tile_h / factor; - const size_t new_w = tile_w / factor; - const size_t in_channels = vision_hidden * factor * factor; - const size_t seq_len = new_h * new_w; - size_t flattened = gb->reshape(unshuffled, {seq_len, in_channels}); - size_t normalized = flattened; - if (projector_weights_.layer_norm_weight != 0 && projector_weights_.layer_norm_bias != 0) { - normalized = gb->layernorm(flattened, projector_weights_.layer_norm_weight, - projector_weights_.layer_norm_bias, config_.layer_norm_eps); - } - size_t hidden = gb->matmul(normalized, projector_weights_.linear_1_weight, true, backend); - hidden = gb->add(hidden, projector_weights_.linear_1_bias); - hidden = gb->gelu(hidden); - size_t output = gb->matmul(hidden, projector_weights_.linear_2_weight, true, backend); - output = gb->add(output, projector_weights_.linear_2_bias); - return output; -} - -std::vector Lfm2VlModel::get_image_embeddings(const std::string& image_path) { - return vision_tower_.get_image_embedding(image_path); -} - -std::vector Lfm2VlModel::get_image_features( - CactusGraph* gb, - const Siglip2Preprocessor::PreprocessedImage& preprocessed_image, - ComputeBackend backend) { - - size_t vision_output = vision_tower_.forward_vision(gb, preprocessed_image, backend); - std::vector projected_features; - projected_features.reserve(preprocessed_image.spatial_shapes.size()); - - size_t offset = 0; - for (size_t tile_idx = 0; tile_idx < preprocessed_image.spatial_shapes.size(); ++tile_idx) { - const auto& shape = preprocessed_image.spatial_shapes[tile_idx]; - const size_t tile_h = shape.first; - const size_t tile_w = shape.second; - const size_t tile_tokens = tile_h * tile_w; - const size_t factor = config_.downsample_factor; - if (factor == 0) { - throw std::runtime_error("Downsample factor must be greater than zero"); - } - if (tile_h % factor != 0 || tile_w % factor != 0) { - throw std::runtime_error("Tile dimensions must be divisible by downsample factor"); - } - const size_t new_h = tile_h / factor; - const size_t new_w = tile_w / factor; - const size_t projected_tokens = new_h * new_w; - - size_t tile_features = gb->slice(vision_output, 0, offset, tile_tokens); - offset += tile_tokens; - - size_t reshaped = gb->reshape(tile_features, {1, tile_h, tile_w, config_.vision_embed_dim}); - size_t projected = build_multimodal_projector(gb, reshaped, tile_h, tile_w, backend); - ProjectedTileFeature feature{}; - feature.node_id = projected; - feature.token_count = projected_tokens; - projected_features.push_back(feature); - - } - - return projected_features; -} - -Lfm2VlModel::MergedEmbeddingResult Lfm2VlModel::merge_image_text_embeddings( - CactusGraph* gb, - const std::vector& tokens, - const std::vector>& image_embedding_nodes, - std::vector& text_embedding_inputs) { - - text_embedding_inputs.clear(); - - Tokenizer* tokenizer = language_model_.get_tokenizer(); - if (!tokenizer) { - throw std::runtime_error("Tokenizer must be initialized before merging embeddings"); - } - - const uint32_t image_token_id = tokenizer->get_image_token_id(); - - auto get_token_id = [tokenizer](const std::string& token_text) -> uint32_t { - auto encoded = tokenizer->encode(token_text); - if (encoded.size() != 1) { - - throw std::runtime_error("Expected single token encoding for " + token_text); - } - return encoded[0]; - }; - - const uint32_t image_start_id = get_token_id("<|image_start|>"); - const uint32_t image_end_id = get_token_id("<|image_end|>"); - - std::vector sequence_nodes; - sequence_nodes.reserve(tokens.size() + image_embedding_nodes.size()); - - std::vector current_segment; - current_segment.reserve(tokens.size()); - - size_t total_seq_len = 0; - - auto flush_segment = [&](void) { - if (current_segment.empty()) { - - return; - } - - const size_t segment_len = current_segment.size(); - TextEmbeddingInput segment; - segment.tokens.swap(current_segment); - segment.input_node = gb->input({segment.tokens.size()}, Precision::FP32); - - const auto& embedding_buffer = gb->get_output_buffer(language_model_.embedding_node_id_); - - for (size_t i = 0; i < embedding_buffer.shape.size(); ++i) { - (void)i; - } - - size_t embedding_node = gb->embedding(language_model_.embedding_node_id_, segment.input_node); - - text_embedding_inputs.push_back(std::move(segment)); - - sequence_nodes.push_back(embedding_node); - - total_seq_len += segment_len; - - current_segment.clear(); - - }; - - size_t token_index = 0; - size_t image_index = 0; - - while (token_index < tokens.size()) { - uint32_t token_id = tokens[token_index]; - - if (token_id == image_start_id) { - flush_segment(); - - if (image_index >= image_embedding_nodes.size()) { - - throw std::runtime_error("Encountered <|image_start|> without corresponding image features"); - } - - current_segment.push_back(token_id); - ++token_index; - - const auto& tiles = image_embedding_nodes[image_index]; - size_t tile_index = 0; - while (token_index < tokens.size()) { - uint32_t inner_token = tokens[token_index]; - if (inner_token == image_token_id) { - flush_segment(); - - if (tile_index >= tiles.size()) { - - throw std::runtime_error("More placeholders than projected tile features"); - } - - const auto& tile = tiles[tile_index++]; - sequence_nodes.push_back(tile.node_id); - - total_seq_len += tile.token_count; - - for (size_t count = 0; count < tile.token_count; ++count) { - if (token_index >= tokens.size()) { - throw std::runtime_error("Insufficient tokens for projected features"); - } - if (tokens[token_index] != image_token_id) { - throw std::runtime_error("Unexpected token encountered within image feature span"); - } - ++token_index; - - } - continue; - } - - current_segment.push_back(inner_token); - - ++token_index; - - if (inner_token == image_end_id) { - flush_segment(); - break; - } - } - - if (tile_index != tiles.size()) { - if (tile_index < tiles.size()) { - for (size_t remaining = tile_index; remaining < tiles.size(); ++remaining) { - (void)tiles[remaining]; - } - } - throw std::runtime_error("Unused projected tile features remain after processing image block"); - } - - ++image_index; - - } else { - current_segment.push_back(token_id); - - ++token_index; - - } - } - - flush_segment(); - if (image_index != image_embedding_nodes.size()) { - throw std::runtime_error("Not all image features were consumed while merging embeddings"); - } - - if (sequence_nodes.empty()) { - throw std::runtime_error("Failed to build embedding sequence from provided tokens"); - } - - size_t merged = sequence_nodes[0]; - for (size_t idx = 1; idx < sequence_nodes.size(); ++idx) { - merged = gb->concat(merged, sequence_nodes[idx], 0); - - } - - return MergedEmbeddingResult{merged, total_seq_len}; -} - -size_t Lfm2VlModel::build_attention(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) { - throw std::runtime_error("build_attention should not be called directly on Lfm2VlModel"); -} - -size_t Lfm2VlModel::build_mlp(CactusGraph*, size_t, uint32_t, ComputeBackend) const { - throw std::runtime_error("build_mlp should not be called directly on Lfm2VlModel"); -} - -size_t Lfm2VlModel::build_transformer_block(CactusGraph*, size_t, uint32_t, ComputeBackend, bool, size_t) { - throw std::runtime_error("build_transformer_block should not be called directly on Lfm2VlModel"); -} - -size_t Lfm2VlModel::forward(const std::vector& tokens, bool use_cache) { - if (!initialized_ || !graph_handle_) { - throw std::runtime_error("Model not initialized - call init() first"); - } - - auto* gb = static_cast(graph_handle_); - gb->soft_reset(); - - auto backend = config_.default_backend == Config::Backend::CPU - ? ComputeBackend::CPU - : ComputeBackend::NPU; - - size_t final_hidden = language_model_.forward(gb, tokens, backend, use_cache); - - return final_hidden; -} - -uint32_t Lfm2VlModel::decode(const std::vector& tokens, - float temperature, - float top_p, - size_t top_k, - const std::string& profile_file, - float* out_entropy, - float min_p, - float repetition_penalty) { - if (!initialized_ || !graph_handle_) { - throw std::runtime_error("Model not initialized - call init() first"); - } - - if (temperature < 0) { - temperature = config_.default_temperature; - } - if (top_p < 0) { - top_p = config_.default_top_p; - } - if (top_k == 0) { - top_k = config_.default_top_k; - } - - image_prefill_completed_ = false; - last_token_count_ = tokens.size(); - - return language_model_.decode(tokens, temperature, top_p, top_k, profile_file, out_entropy, min_p, repetition_penalty); -} - -void Lfm2VlModel::prefill(const std::vector& tokens, size_t chunk_size, const std::string& profile_file) { - if (!initialized_ || !graph_handle_) { - throw std::runtime_error("Model not initialized - call init() first"); - } - - image_prefill_completed_ = false; - last_token_count_ = tokens.size(); - - language_model_.prefill(tokens, chunk_size, profile_file); -} - -void Lfm2VlModel::prefill_with_images(const std::vector& tokens, const std::vector& image_paths, - const std::string& profile_file) { - if (!initialized_ || !graph_handle_) { - throw std::runtime_error("Model not initialized - call init() first"); - } - if (tokens.empty()) { - return; - } - if (image_paths.empty()) { - prefill(tokens, get_prefill_chunk_size(), profile_file); - return; - } - - auto* gb = static_cast(graph_handle_); - gb->soft_reset(); - auto backend = config_.default_backend == Config::Backend::CPU - ? ComputeBackend::CPU - : ComputeBackend::NPU; - - auto forward_result = forward_images(gb, tokens, image_paths, backend, true); - gb->execute(profile_file); - - language_model_.post_execute_updates(gb, forward_result.seq_len); - language_model_.update_kv_cache(gb, forward_result.seq_len); - - image_prefill_completed_ = true; - last_token_count_ = tokens.size(); -} - -Lfm2VlModel::ForwardImageResult Lfm2VlModel::forward_images( - CactusGraph* gb, - const std::vector& tokens, - const std::vector& image_paths, - ComputeBackend backend, - bool use_cache) { - if (!gb) { - throw std::runtime_error("Graph must be initialized before forwarding"); - } - if (tokens.empty()) { - throw std::runtime_error("Token sequence cannot be empty"); - } - - std::vector> all_image_embeddings; - all_image_embeddings.reserve(image_paths.size()); - for (const auto& image_path : image_paths) { - auto preprocessed = preprocessor_.preprocess_from_file(image_path); - - auto image_features = get_image_features(gb, preprocessed, backend); - - all_image_embeddings.push_back(std::move(image_features)); - } - - std::vector text_embedding_inputs; - text_embedding_inputs.reserve(tokens.size() / 4 + 1); - - auto merged_embeddings = merge_image_text_embeddings(gb, tokens, all_image_embeddings, text_embedding_inputs); - if (merged_embeddings.seq_len == 0) { - throw std::runtime_error("Merged embedding sequence length cannot be zero"); - } - - for (const auto& embedding_input : text_embedding_inputs) { - if (embedding_input.tokens.empty()) { - continue; - } - - std::vector segment_data(embedding_input.tokens.size()); - for (size_t i = 0; i < embedding_input.tokens.size(); ++i) { - segment_data[i] = static_cast(embedding_input.tokens[i]); - } - gb->set_input(embedding_input.input_node, segment_data.data(), Precision::FP32); - } - size_t final_hidden = language_model_.forward(gb, merged_embeddings.node_id, merged_embeddings.seq_len, backend, use_cache); - return ForwardImageResult{final_hidden, merged_embeddings.seq_len}; -} - -uint32_t Lfm2VlModel::decode_with_images( - const std::vector& tokens, - const std::vector& image_paths, - float temperature, - float top_p, - size_t top_k, - const std::string& profile_file, - float* out_entropy, - float min_p, - float repetition_penalty) { - - if (!initialized_ || !graph_handle_) { - throw std::runtime_error("Model not initialized - call init() first"); - } - - if (image_paths.empty()) { - - image_prefill_completed_ = false; - last_token_count_ = tokens.size(); - return language_model_.decode(tokens, temperature, top_p, top_k, profile_file, out_entropy, min_p, repetition_penalty); - } - - if (temperature < 0) { - temperature = config_.default_temperature; - } - if (top_p < 0) { - top_p = config_.default_top_p; - } - if (top_k == 0) { - top_k = config_.default_top_k; - } - - auto* gb = static_cast(graph_handle_); - gb->soft_reset(); - auto backend = config_.default_backend == Config::Backend::CPU - ? ComputeBackend::CPU - : ComputeBackend::NPU; - bool cache_empty = language_model_.is_cache_empty(); - bool need_prefill = cache_empty || !image_prefill_completed_; - - if (!need_prefill && tokens.size() <= last_token_count_) { - - reset_cache(); - need_prefill = true; - } - - size_t seq_len_for_updates = 0; - size_t final_hidden_node = 0; - - if (need_prefill) { - auto forward_result = forward_images(gb, tokens, image_paths, backend, true); - - final_hidden_node = forward_result.final_hidden_node; - seq_len_for_updates = forward_result.seq_len; - image_prefill_completed_ = true; - last_token_count_ = tokens.size(); - } else { - size_t delta = tokens.size() - last_token_count_; - if (delta > tokens.size()) { - delta = tokens.size(); - } - if (delta == 0) { - if (tokens.empty()) { - throw std::runtime_error("Token sequence cannot be empty for cached decode step"); - } - delta = 1; - - } - std::vector incremental_tokens(tokens.end() - delta, tokens.end()); - - final_hidden_node = language_model_.forward(gb, incremental_tokens, backend, true); - - seq_len_for_updates = incremental_tokens.size(); - last_token_count_ = tokens.size(); - } - - auto logits_node_id = gb->matmul(final_hidden_node, language_model_.output_weight_node_id_, true, backend); - size_t sampled_token_id = - language_model_.sample_token(gb, logits_node_id, temperature, top_p, top_k, min_p, repetition_penalty, nullptr); - if (!profile_file.empty()) { - gb->execute(profile_file); - - } else { - gb->execute(); - - } - - compute_entropy(gb, logits_node_id, out_entropy); - - language_model_.post_execute_updates(gb, seq_len_for_updates); - language_model_.update_kv_cache(gb, seq_len_for_updates); - - auto* output_ptr = gb->get_output(sampled_token_id); - uint32_t result = *static_cast(output_ptr); - language_model_.record_sampled_token(result); - return result; -} - -} -} diff --git a/cactus/models/model_moonshine.cpp b/cactus/models/model_moonshine.cpp deleted file mode 100644 index fe03f5983..000000000 --- a/cactus/models/model_moonshine.cpp +++ /dev/null @@ -1,596 +0,0 @@ -#include "model.h" -#include "../graph/graph.h" -#include "../npu/npu.h" -#include "../kernel/kernel.h" -#include -#include -#include -#include -#include -#include -namespace cactus { -namespace engine { - -static size_t moonshine_downsampled_len(size_t audio_len) { - if (audio_len < 127) { - return 0; - } - size_t l1 = (audio_len - 127) / 64 + 1; - if (l1 < 7) { - return 0; - } - size_t l2 = (l1 - 7) / 3 + 1; - if (l2 < 3) { - return 0; - } - size_t l3 = (l2 - 1) / 2 + 1; - return l3; -} - -MoonshineModel::MoonshineModel() : Model() {} -MoonshineModel::MoonshineModel(const Config& config) : Model(config) { - weight_nodes_.encoder_layers.resize(config.num_encoder_layers); - weight_nodes_.decoder_layers.resize(config.num_decoder_layers); - float hd = static_cast(config.attention_head_dim); - if (hd <= 0.0f) { - hd = 64.0f; - } - attention_scale_ = 1.0f / std::sqrt(hd); - encoder_block_out_nodes_.resize(config.num_encoder_layers, 0); - encoder_k_persistent_.assign(config.num_decoder_layers, 0); - encoder_v_persistent_.assign(config.num_decoder_layers, 0); -} -void MoonshineModel::load_weights_to_graph(CactusGraph* gb) { - embedding_node_id_ = gb->mmap_embeddings(embedding_file_path_); - weight_nodes_.decoder_norm_weight = gb->mmap_weights(model_folder_path_ + "/output_norm.weights"); - output_weight_node_id_ = gb->mmap_weights(model_folder_path_ + "/output_weight.weights"); - if (output_weight_node_id_ == 0) { - output_weight_node_id_ = gb->mmap_weights(model_folder_path_ + "/output_layer.weights"); - } - weight_nodes_.output_weight = output_weight_node_id_; - if (npu::is_npu_available()) { - std::string npu_encoder_path = model_folder_path_ + "/model.mlpackage"; - npu_encoder_ = npu::create_encoder(); - if (npu_encoder_ && npu_encoder_->load(npu_encoder_path)) { - use_npu_encoder_ = true; - std::vector input_shape = npu_encoder_->get_input_shape(); - if (!input_shape.empty()) { - npu_encoder_->preallocate(input_shape, "x", ""); - } - } - else { - use_npu_encoder_ = false; - npu_encoder_.reset(); - } - } - if (!use_npu_encoder_) { - weight_nodes_.encoder_conv1_weight = gb->mmap_weights(model_folder_path_ + "/encoder_conv1_weight.weights"); - weight_nodes_.encoder_conv2_weight = gb->mmap_weights(model_folder_path_ + "/encoder_conv2_weight.weights"); - weight_nodes_.encoder_conv2_bias = gb->mmap_weights(model_folder_path_ + "/encoder_conv2_bias.bias"); - weight_nodes_.encoder_conv3_weight = gb->mmap_weights(model_folder_path_ + "/encoder_conv3_weight.weights"); - weight_nodes_.encoder_conv3_bias = gb->mmap_weights(model_folder_path_ + "/encoder_conv3_bias.bias"); - weight_nodes_.encoder_norm_weight = gb->mmap_weights(model_folder_path_ + "/encoder_norm_weight.weights"); - weight_nodes_.encoder_norm_bias = gb->mmap_weights(model_folder_path_ + "/encoder_norm_bias.bias"); - weight_nodes_.encoder_layer_norm_weight = gb->mmap_weights(model_folder_path_ + "/encoder_layer_norm_weight.weights"); - } - for (uint32_t i = 0; i < config_.num_decoder_layers; i++) { - auto& layer = weight_nodes_.decoder_layers[i]; - std::string layer_prefix = model_folder_path_ + "/layer_" + std::to_string(i) + "_"; - layer.decoder_encoder_attn_k_weight = gb->mmap_weights(layer_prefix + "encoder_attn_k.weights"); - layer.decoder_encoder_attn_q_weight = gb->mmap_weights(layer_prefix + "encoder_attn_q.weights"); - layer.decoder_encoder_attn_v_weight = gb->mmap_weights(layer_prefix + "encoder_attn_v.weights"); - layer.decoder_encoder_attn_output_weight = gb->mmap_weights(layer_prefix + "encoder_attn_output.weights"); - layer.decoder_post_encoder_layernorm_weight = gb->mmap_weights(layer_prefix + "post_attn_norm.weights"); - if (config_.decoder_act_gelu) { - layer.decoder_ffn1_weight = gb->mmap_weights(layer_prefix + "mlp_fc1.weights"); - layer.decoder_ffn1_bias = gb->mmap_weights(layer_prefix + "mlp_fc1.bias"); - } - else { - layer.decoder_ffn_gate_weight = gb->mmap_weights(layer_prefix + "ffn_gate.weights"); - layer.decoder_ffn_gate_bias = gb->mmap_weights(layer_prefix + "ffn_gate.bias"); - layer.decoder_ffn_up_weight = gb->mmap_weights(layer_prefix + "ffn_up.weights"); - layer.decoder_ffn_up_bias = gb->mmap_weights(layer_prefix + "ffn_up.bias"); - } - layer.decoder_ffn2_weight = gb->mmap_weights(layer_prefix + "mlp_fc2.weights"); - layer.decoder_ffn2_bias = gb->mmap_weights(layer_prefix + "mlp_fc2.bias"); - layer.decoder_post_ffn_layernorm_weight = gb->mmap_weights(layer_prefix + "final_norm.weights"); - layer.decoder_self_attn_k_weight = gb->mmap_weights(layer_prefix + "attn_k.weights"); - layer.decoder_self_attn_q_weight = gb->mmap_weights(layer_prefix + "attn_q.weights"); - layer.decoder_self_attn_v_weight = gb->mmap_weights(layer_prefix + "attn_v.weights"); - layer.decoder_self_attn_output_weight = gb->mmap_weights(layer_prefix + "attn_output.weights"); - layer.decoder_post_attn_layernorm_weight = gb->mmap_weights(layer_prefix + "input_norm.weights"); - } - if (!use_npu_encoder_) { - for (uint32_t i = 0; i < config_.num_encoder_layers; i++) { - auto& layer = weight_nodes_.encoder_layers[i]; - std::string layer_prefix = model_folder_path_ + "/encoder_layer_" + std::to_string(i) + "_"; - if (config_.encoder_act_gelu) { - layer.encoder_ffn1_weight = gb->mmap_weights(layer_prefix + "mlp_fc1.weights"); - layer.encoder_ffn1_bias = gb->mmap_weights(layer_prefix + "mlp_fc1.bias"); - } - else { - layer.encoder_ffn_gate_weight = gb->mmap_weights(layer_prefix + "ffn_gate.weights"); - layer.encoder_ffn_gate_bias = gb->mmap_weights(layer_prefix + "ffn_gate.bias"); - layer.encoder_ffn_up_weight = gb->mmap_weights(layer_prefix + "ffn_up.weights"); - layer.encoder_ffn_up_bias = gb->mmap_weights(layer_prefix + "ffn_up.bias"); - } - layer.encoder_ffn2_weight = gb->mmap_weights(layer_prefix + "mlp_fc2.weights"); - layer.encoder_ffn2_bias = gb->mmap_weights(layer_prefix + "mlp_fc2.bias"); - layer.encoder_post_ffn_layernorm_weight = gb->mmap_weights(layer_prefix + "post_attn_norm.weights"); - layer.encoder_self_attn_k_weight = gb->mmap_weights(layer_prefix + "attn_k.weights"); - layer.encoder_self_attn_q_weight = gb->mmap_weights(layer_prefix + "attn_q.weights"); - layer.encoder_self_attn_v_weight = gb->mmap_weights(layer_prefix + "attn_v.weights"); - layer.encoder_self_attn_output_weight = gb->mmap_weights(layer_prefix + "attn_output.weights"); - layer.encoder_post_attn_layernorm_weight = gb->mmap_weights(layer_prefix + "input_norm.weights"); - } - } -} -static size_t build_encoder_mlp_gelu(CactusGraph* gb, size_t input, size_t w1, size_t b1, size_t w2, size_t b2, ComputeBackend backend, uint32_t /*layer_idx*/) { - auto ffn1_weight = gb->matmul(input, w1, true, backend); - auto ffn1_bias = gb->add(ffn1_weight, b1); - auto ffn1_act = gb->gelu(ffn1_bias); - auto ffn2_weight = gb->matmul(ffn1_act, w2, true, backend); - auto ffn2_bias = gb->add(ffn2_weight, b2); - return ffn2_bias; -} -static size_t build_decoder_mlp_silu(CactusGraph* gb, size_t input, size_t w_gate, size_t b_gate, size_t w_up, size_t b_up, size_t w2, size_t b2, ComputeBackend backend, uint32_t /*layer_idx*/) { - auto gate_weight = gb->matmul(input, w_gate, true, backend); - auto gate_bias = gb->add(gate_weight, b_gate); - auto gate_act = gb->silu(gate_bias); - auto up_weight = gb->matmul(input, w_up, true, backend); - auto up_bias = gb->add(up_weight, b_up); - auto intermediate = gb->multiply(gate_act, up_bias); - auto output_weight = gb->matmul(intermediate, w2, true, backend); - auto output_bias = gb->add(output_weight, b2); - return output_bias; -} -size_t MoonshineModel::build_decoder_cross_attention(CactusGraph* gb, size_t input, uint32_t layer_idx, ComputeBackend backend, bool /*use_cache*/, size_t /*position_offset*/){ - const auto& layer = weight_nodes_.decoder_layers[layer_idx]; - size_t q = gb->matmul(input, layer.decoder_encoder_attn_q_weight, true, backend); - const auto& q_buf = gb->get_output_buffer(q); - if (q_buf.shape.size() != 2) { - throw std::runtime_error("encoder cross-attn: q must be [T_dec, D]"); - } - size_t T_dec = q_buf.shape[0]; - size_t q_heads = config_.attention_heads; - size_t kv_heads = config_.attention_kv_heads; - size_t head_dim = config_.attention_head_dim; - q = gb->reshape(q, {1, T_dec, q_heads, head_dim}); - size_t k_4d = 0; - size_t v_4d = 0; - bool is_pop = (encoder_k_persistent_[layer_idx] != 0 && gb->is_populated(encoder_k_persistent_[layer_idx])); - std::string prefix = "model.decoder.layers." + std::to_string(layer_idx) + ".encoder_attn."; - if (is_pop) { - k_4d = encoder_k_persistent_[layer_idx]; - v_4d = encoder_v_persistent_[layer_idx]; - } - else { - size_t enc_norm = last_encoder_post_norm_node_; - size_t k = gb->matmul(enc_norm, layer.decoder_encoder_attn_k_weight, true, backend); - size_t v = gb->matmul(enc_norm, layer.decoder_encoder_attn_v_weight, true, backend); - const auto& k_buf = gb->get_output_buffer(k); - if (k_buf.shape.size() != 2) { - throw std::runtime_error("encoder cross-attn: k must be [T_enc, D]"); - } - size_t T_enc = k_buf.shape[0]; - k_4d = gb->reshape(k, {1, T_enc, kv_heads, head_dim}); - v_4d = gb->reshape(v, {1, T_enc, kv_heads, head_dim}); - if (encoder_k_persistent_[layer_idx] == 0) { - encoder_k_persistent_[layer_idx] = gb->persistent(k_4d); - encoder_v_persistent_[layer_idx] = gb->persistent(v_4d); - } - k_4d = encoder_k_persistent_[layer_idx]; - v_4d = encoder_v_persistent_[layer_idx]; - } - size_t attn = gb->attention(q, k_4d, v_4d, attention_scale_, false); - attn = gb->reshape(attn, {T_dec, q_heads * head_dim}); - size_t output = gb->matmul(attn, layer.decoder_encoder_attn_output_weight, true, backend); - return output; -} -void MoonshineModel::reset_graph_side_cache_nodes() { - cache_k_output_nodes_.assign(config_.num_decoder_layers, 0); - cache_v_output_nodes_.assign(config_.num_decoder_layers, 0); -} -void MoonshineModel::reset_cache() { - Model::reset_cache(); - encoder_ready_ = false; - encoder_kv_ready_ = false; - first_decode_step_ = true; - encoder_output_host_.clear(); - auto* gb = static_cast(graph_handle_); - if (gb) { - if (last_encoder_post_norm_node_ != 0) { - gb->invalidate_persistent(last_encoder_post_norm_node_); - last_encoder_post_norm_node_ = 0; - } - for (size_t i = 0; i < encoder_k_persistent_.size(); ++i) { - if (encoder_k_persistent_[i] != 0) { - gb->invalidate_persistent(encoder_k_persistent_[i]); - encoder_k_persistent_[i] = 0; - } - if (encoder_v_persistent_[i] != 0) { - gb->invalidate_persistent(encoder_v_persistent_[i]); - encoder_v_persistent_[i] = 0; - } - } - } -} -size_t MoonshineModel::build_decoder_self_attention(CactusGraph* gb, size_t input, uint32_t layer_idx, ComputeBackend backend, bool use_cache, size_t position_offset){ - const auto& layer = weight_nodes_.decoder_layers[layer_idx]; - size_t q_proj = gb->matmul(input, layer.decoder_self_attn_q_weight, true, backend); - size_t k_proj = gb->matmul(input, layer.decoder_self_attn_k_weight, true, backend); - size_t v_proj = gb->matmul(input, layer.decoder_self_attn_v_weight, true, backend); - std::string prefix = "model.decoder.layers." + std::to_string(layer_idx) + ".self_attn."; - const auto& q_shape = gb->get_output_buffer(q_proj).shape; - if (q_shape.size() != 2) { - throw std::runtime_error("decoder self-attn: q must be [T_new, D]"); - } - size_t seq_new = q_shape[0]; - size_t num_heads = config_.attention_heads; - size_t head_dim = config_.attention_head_dim; - size_t num_kv_heads = config_.attention_kv_heads; - size_t q_4d = gb->reshape(q_proj, {1, seq_new, num_heads, head_dim}); - size_t k_4d = gb->reshape(k_proj, {1, seq_new, num_kv_heads, head_dim}); - size_t v_4d = gb->reshape(v_proj, {1, seq_new, num_kv_heads, head_dim}); - size_t rot_dim = std::max(head_dim / 2, (size_t)32); - if (config_.rope_theta > 0) { - q_4d = gb->rope_gptj(q_4d, config_.rope_theta, position_offset, rot_dim); - k_4d = gb->rope_gptj(k_4d, config_.rope_theta, position_offset, rot_dim); - } - size_t final_k = k_4d; - size_t final_v = v_4d; - if (use_cache && !kv_cache_.is_empty()) { - auto k_view = kv_cache_.get_key_view(layer_idx); - auto v_view = kv_cache_.get_value_view(layer_idx); - if (!k_view.ptr1 || !v_view.ptr1) { - throw std::runtime_error("KV cache view is empty but kv_cache_.is_empty()==false"); - } - size_t cache_len = kv_cache_.current_seq_len; - size_t cache_k_node = gb->input( - {1, cache_len, num_kv_heads, head_dim} -, - kv_cache_.precision - ); - size_t cache_v_node = gb->input( - {1, cache_len, num_kv_heads, head_dim} -, - kv_cache_.precision - ); - if (k_view.ptr2 == nullptr && v_view.ptr2 == nullptr) { - gb->set_external_input(cache_k_node, const_cast(k_view.ptr1), kv_cache_.precision); - gb->set_external_input(cache_v_node, const_cast(v_view.ptr1), kv_cache_.precision); - } - else { - gb->set_external_input(cache_k_node, kv_cache_.get_key_ptr(layer_idx), kv_cache_.precision); - gb->set_external_input(cache_v_node, kv_cache_.get_value_ptr(layer_idx), kv_cache_.precision); - } - final_k = gb->concat(cache_k_node, k_4d, 1); - final_v = gb->concat(cache_v_node, v_4d, 1); - } - if (use_cache) { - cache_k_output_nodes_[layer_idx] = final_k; - cache_v_output_nodes_[layer_idx] = final_v; - } - else { - cache_k_output_nodes_[layer_idx] = k_4d; - cache_v_output_nodes_[layer_idx] = v_4d; - } - auto attn_out_4d = gb->attention(q_4d, final_k, final_v, attention_scale_, position_offset); - auto attn_out = gb->reshape(attn_out_4d, {seq_new, num_heads * head_dim}); - auto output = gb->matmul(attn_out, layer.decoder_self_attn_output_weight, true, backend); - return output; -} -size_t MoonshineModel::build_encoder_self_attention(CactusGraph* gb, size_t input, uint32_t layer_idx, ComputeBackend backend, bool use_cache, size_t /*position_offset*/){ - const auto& layer = weight_nodes_.encoder_layers[layer_idx]; - if(use_cache) - throw std::runtime_error("The encoder attention layers are not auto-regressive, and thus don't use KV caching!"); - auto q_proj = gb->matmul(input, layer.encoder_self_attn_q_weight, true, backend); - auto v_proj = gb->matmul(input, layer.encoder_self_attn_v_weight, true, backend); - auto k_proj = gb->matmul(input, layer.encoder_self_attn_k_weight, true, backend); - std::string prefix = "model.encoder.layers." + std::to_string(layer_idx) + ".self_attn."; - size_t seq_len = gb->get_output_buffer(q_proj).shape[0]; - size_t num_heads = config_.attention_heads; - size_t head_dim = config_.attention_head_dim; - auto q = gb->reshape(q_proj, {1, seq_len, num_heads, head_dim}); - auto k = gb->reshape(k_proj, {1, seq_len, num_heads, head_dim}); - auto v = gb->reshape(v_proj, {1, seq_len, num_heads, head_dim}); - size_t rot_dim = static_cast(head_dim * config_.partial_rotary_factor); - if (rot_dim % 2 != 0) throw std::runtime_error("rot_dim must be even"); - if (config_.rope_theta > 0) { - q = gb->rope_gptj(q, config_.rope_theta, 0, rot_dim); - k = gb->rope_gptj(k, config_.rope_theta, 0, rot_dim); - } - auto attn = gb->attention(q, k, v, attention_scale_, false); - attn = gb->reshape(attn, {seq_len, num_heads * head_dim}); - auto output = gb->matmul(attn, layer.encoder_self_attn_output_weight, true, backend); - return output; -} -size_t MoonshineModel::build_audio_preprocessor(CactusGraph* gb, size_t input) -{ - size_t conv_input = input; - const auto& xbuf = gb->get_output_buffer(input); - if (xbuf.precision == Precision::INT8) { - conv_input = gb->precision_cast(input, Precision::FP16); - } - size_t conv1 = gb->conv1d(conv_input, weight_nodes_.encoder_conv1_weight, 64); - last_conv1_node_ = conv1; - size_t conv1_act = gb->tanh(conv1); - size_t group_norm = gb->groupnorm(conv1_act, weight_nodes_.encoder_norm_weight, weight_nodes_.encoder_norm_bias, 1); - - size_t conv2_with_bias = gb->conv1d_k7s3(group_norm, weight_nodes_.encoder_conv2_weight, weight_nodes_.encoder_conv2_bias); - size_t conv2_act = gb->gelu(conv2_with_bias); - size_t conv3 = gb->conv1d_k3(conv2_act, weight_nodes_.encoder_conv3_weight, 2); - auto bias3_shape = gb->get_output_buffer(weight_nodes_.encoder_conv3_bias).shape; - size_t C3 = bias3_shape[0]; - size_t bias3 = gb->reshape(weight_nodes_.encoder_conv3_bias, {1, C3, 1}); - size_t conv3_with_bias = gb->add(conv3, bias3); - size_t conv3_act = gb->gelu(conv3_with_bias); - const auto& buf = gb->get_output_buffer(conv3_act); - size_t conv_out_transposed; - if (buf.precision == Precision::FP16) { - conv_out_transposed = gb->transpose(conv3_act, ComputeBackend::CPU); - } - else { - size_t conv3_f16 = gb->precision_cast(conv3_act, Precision::FP16); - conv_out_transposed = gb->transpose(conv3_f16, ComputeBackend::CPU); - } - return conv_out_transposed; -} -size_t MoonshineModel::build_encoder_transformer_block( - CactusGraph* gb, - size_t hidden, - uint32_t layer_idx, - ComputeBackend backend, - bool use_cache, - size_t position_offset) -{ - const auto& layer = weight_nodes_.encoder_layers[layer_idx]; - size_t input_layernorm = gb->layernorm( - hidden, - layer.encoder_post_attn_layernorm_weight - ); - size_t self_attn_out = build_encoder_self_attention( - gb, input_layernorm, layer_idx, backend, use_cache, position_offset - ); - size_t post_attention_layernorm_input = gb->add(hidden, self_attn_out); - size_t post_attention_layernorm = gb->layernorm( - post_attention_layernorm_input, - layer.encoder_post_ffn_layernorm_weight - ); - std::string prefix = "model.encoder.layers." + std::to_string(layer_idx) + "."; - size_t mlp_out = build_encoder_mlp_gelu( - gb, post_attention_layernorm, layer.encoder_ffn1_weight, layer.encoder_ffn1_bias, - layer.encoder_ffn2_weight, layer.encoder_ffn2_bias, backend, layer_idx - ); - size_t out = gb->add(post_attention_layernorm_input, mlp_out); - if (layer_idx < encoder_block_out_nodes_.size()) { - encoder_block_out_nodes_[layer_idx] = out; - } - return out; -} -size_t MoonshineModel::build_decoder_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, ComputeBackend backend, bool use_cache, size_t position_offset){ - const auto& layer = weight_nodes_.decoder_layers[layer_idx]; - size_t input_layernorm = gb->layernorm(hidden, layer.decoder_post_attn_layernorm_weight); - size_t sa = build_decoder_self_attention(gb, input_layernorm, layer_idx, backend, use_cache, position_offset); - size_t x_post_sa = gb->add(hidden, sa); - size_t post_attention_layernorm = gb->layernorm(x_post_sa, layer.decoder_post_encoder_layernorm_weight); - size_t ca = build_decoder_cross_attention(gb, post_attention_layernorm, layer_idx, backend, use_cache, position_offset); - size_t x_post_ca = gb->add(x_post_sa, ca); - size_t final_layernorm = gb->layernorm(x_post_ca, layer.decoder_post_ffn_layernorm_weight); - size_t mlp_out = build_decoder_mlp_silu( - gb, final_layernorm, layer.decoder_ffn_gate_weight, layer.decoder_ffn_gate_bias, - layer.decoder_ffn_up_weight, layer.decoder_ffn_up_bias, - layer.decoder_ffn2_weight, layer.decoder_ffn2_bias, backend, layer_idx - ); - size_t x_post_ffn = gb->add(x_post_ca, mlp_out); - std::string prefix = "model.decoder.layers." + std::to_string(layer_idx) + "."; - return x_post_ffn; -} -size_t MoonshineModel::build_encoder(CactusGraph* gb, const std::vector& audio_features) -{ - if (!audio_features.empty()) { - float min_val = audio_features[0]; - float max_val = audio_features[0]; - double sum_val = 0.0; - for (float val : audio_features) { - if (val < min_val) min_val = val; - if (val > max_val) max_val = val; - sum_val += val; - } - (void)sum_val; - } - if (use_npu_encoder_ && npu_encoder_ && npu_encoder_->is_available()) { - std::vector input_shape = npu_encoder_->get_input_shape(); - if (input_shape.size() != 3) { - input_shape = {1, 1, static_cast(audio_features.size())}; - } - if (input_shape[1] != 1) { - throw std::runtime_error("Moonshine NPU encoder expects mono audio input"); - } - size_t expected_samples = static_cast(input_shape[2]); - std::vector<__fp16> audio_f16(expected_samples, 0); - size_t copy_samples = std::min(audio_features.size(), expected_samples); - if (copy_samples > 0) { - cactus_fp32_to_fp16(audio_features.data(), audio_f16.data(), copy_samples); - } - size_t T_enc = moonshine_downsampled_len(expected_samples); - size_t D_enc = static_cast(config_.hidden_dim); - if (T_enc == 0 || D_enc == 0) { - std::cout << "NPU encoder output has unexpected shape for input size " - << expected_samples << std::endl; - std::cout << "Falling back to CPU encoder path." << std::endl; - goto encoder_cpu_fallback; - } - - size_t total_elements = T_enc * D_enc; - std::vector<__fp16> npu_output(total_elements); - size_t elements_written = npu_encoder_->encode( - audio_f16.data(), - npu_output.data(), - input_shape, - "x", - "" - ); - if (elements_written > 0) { - size_t enc_output_node = gb->input({T_enc, D_enc}, Precision::FP16); - gb->set_input(enc_output_node, npu_output.data(), Precision::FP16); - size_t enc_output_persistent = gb->persistent(enc_output_node); - last_encoder_post_norm_node_ = enc_output_persistent; - return enc_output_persistent; - } - } -encoder_cpu_fallback: - auto backend = - (config_.default_backend == Config::Backend::CPU) - ? ComputeBackend::CPU - : ComputeBackend::NPU; - size_t audio_input = 0; - const std::vector* cpu_audio = &audio_features; - std::vector<__fp16> audio_f16(cpu_audio->size()); - if (!cpu_audio->empty()) { - cactus_fp32_to_fp16(cpu_audio->data(), audio_f16.data(), cpu_audio->size()); - } - size_t audio_length = cpu_audio->size(); - audio_input = gb->input({1, 1, audio_length}, Precision::FP16); - gb->set_input(audio_input, audio_f16.data(), Precision::FP16); - size_t conv2_transposed = build_audio_preprocessor(gb, audio_input); - const auto& conv_shape = gb->get_output_buffer(conv2_transposed).shape; - if (conv_shape.size() != 3 || conv_shape[0] != 1) - throw std::runtime_error("Conv2 transpose should be [1, T_enc, D]."); - size_t T_enc = conv_shape[1]; - size_t D_enc = conv_shape[2]; - size_t h = gb->reshape(conv2_transposed, {T_enc, D_enc}); - for (uint32_t i = 0; i < config_.num_encoder_layers; ++i){ - h = build_encoder_transformer_block(gb, h, i, backend, false, 0); - } - size_t h_norm = gb->layernorm( - h, - weight_nodes_.encoder_layer_norm_weight - ); - size_t h_norm_persistent = gb->persistent(h_norm); - last_encoder_post_norm_node_ = h_norm_persistent; - return h_norm_persistent; -} -size_t MoonshineModel::build_decoder(const std::vector& tokens, bool use_cache, bool last_token_only) { - auto* gb = static_cast(graph_handle_); - const size_t full_len = tokens.size(); - if (full_len == 0) { - throw std::runtime_error("Decoder token list cannot be empty."); - } - auto backend = config_.default_backend == Config::Backend::CPU - ? ComputeBackend::CPU - : ComputeBackend::NPU; - size_t start_idx = (use_cache && kv_cache_.current_seq_len > 0) ? full_len - 1 : 0; - size_t new_tokens = full_len - start_idx; - size_t position_offset = use_cache ? kv_cache_.current_seq_len : 0; - size_t tok_input = gb->input({new_tokens}, Precision::FP32); - std::vector tok_f(new_tokens); - for (size_t i = 0; i < new_tokens; i++) { - tok_f[i] = static_cast(tokens[start_idx + i]); - } - gb->set_input(tok_input, tok_f.data(), Precision::FP32); - size_t dec_hidden = gb->embedding(embedding_node_id_, tok_input); - for (uint32_t layer_idx = 0; layer_idx < config_.num_decoder_layers; ++layer_idx) { - dec_hidden = build_decoder_transformer_block( - gb, - dec_hidden, - layer_idx, - backend, - use_cache, - position_offset - ); - } - size_t dec_norm = gb->layernorm( - dec_hidden, - weight_nodes_.decoder_norm_weight - ); - size_t logits_input = dec_norm; - if (last_token_only) { - size_t row_index = new_tokens - 1; - logits_input = gb->slice(logits_input, 0, row_index, 1); - } - auto w_shape = gb->get_output_buffer(output_weight_node_id_).shape; - size_t logits = gb->matmul(logits_input, output_weight_node_id_, true, backend); - last_new_tokens_ = new_tokens; - return logits; -} -size_t MoonshineModel::forward(const std::vector& audio_features, const std::vector& tokens, bool use_cache) -{ - auto* gb = static_cast(graph_handle_); - gb->clear_debug_nodes(); - build_encoder(gb, audio_features); - size_t logits = build_decoder(tokens, use_cache, true); - return logits; -} -std::vector MoonshineModel::get_audio_embeddings(const std::vector& audio_features) { - auto* gb = static_cast(graph_handle_); - build_encoder(gb, audio_features); - size_t pooled = gb->mean(last_encoder_post_norm_node_, 0); - gb->execute(); - const auto& output_buf = gb->get_output_buffer(pooled); - size_t hidden_dim = output_buf.total_size; - std::vector embedding(hidden_dim); - void* output_data = gb->get_output(pooled); - const float* output_ptr = static_cast(output_data); - std::copy(output_ptr, output_ptr + hidden_dim, embedding.begin()); - reset_cache(); - return embedding; -} -uint32_t MoonshineModel::decode_with_audio( - const std::vector& tokens, - const std::vector& audio_features, - float temperature, - float top_p, - size_t top_k, - const std::string& profile_file, - float* out_entropy, - float min_p, - float repetition_penalty, - float* /*out_token_time_start*/, - float* /*out_token_time_end*/) -{ - if (!initialized_ || !graph_handle_) - throw std::runtime_error("Model not initialized - call init() first"); - if (audio_features.empty()) - throw std::runtime_error("Audio features cannot be empty in Moonshine decode_with_audio"); - auto* gb = static_cast(graph_handle_); - gb->clear_debug_nodes(); - bool cold_start = !encoder_ready_; - size_t logits_node = 0; - uint32_t bos = static_cast(get_tokenizer()->get_bos_token()); - std::vector full_tokens; - full_tokens.reserve(tokens.size() + 1); - full_tokens.push_back(bos); - full_tokens.insert(full_tokens.end(), tokens.begin(), tokens.end()); - if (cold_start) - { - gb->soft_reset(); - kv_cache_.reset(); - kv_cache_.current_seq_len = 0; - reset_graph_side_cache_nodes(); - encoder_kv_ready_ = false; - first_decode_step_ = true; - build_encoder(gb, audio_features); - logits_node = build_decoder(full_tokens, false, false); - encoder_ready_ = true; - } - else - { - gb->soft_reset(); - reset_graph_side_cache_nodes(); - std::vector last_token_vec = { tokens.back() }; - logits_node = build_decoder(last_token_vec, true, true); - } - size_t sampled_token_id = sample_token(gb, logits_node, temperature, top_p, top_k, min_p, repetition_penalty); - if (!profile_file.empty()) gb->execute(profile_file); - else gb->execute(); - compute_entropy(gb, logits_node, out_entropy); - post_execute_updates(gb, full_tokens.size()); - update_kv_cache(gb, last_new_tokens_); - auto* out_ptr = gb->get_output(sampled_token_id); - uint32_t sampled = *reinterpret_cast(out_ptr); - record_sampled_token(sampled); - return sampled; -} -} -} diff --git a/cactus/models/model_needle.cpp b/cactus/models/model_needle.cpp deleted file mode 100644 index e7220a97f..000000000 --- a/cactus/models/model_needle.cpp +++ /dev/null @@ -1,337 +0,0 @@ -#include "model.h" -#include "../graph/graph.h" -#include -#include - -namespace cactus { -namespace engine { - -namespace { - -size_t delta_rms_norm(CactusGraph* gb, size_t input, size_t weight, float epsilon) { - return gb->rms_norm(input, gb->scalar_add(weight, 1.0f), epsilon); -} - -size_t normalize_qk_proj(CactusGraph* gb, size_t proj, size_t norm_weight, - size_t seq_len, size_t num_heads, size_t head_dim, float epsilon) { - proj = gb->reshape(proj, {seq_len * num_heads, head_dim}); - proj = delta_rms_norm(gb, proj, norm_weight, epsilon); - return gb->reshape(proj, {seq_len, num_heads * head_dim}); -} - -size_t apply_residual_gate(CactusGraph* gb, size_t residual, size_t update, size_t gate_weight) { - return gb->add_clipped(residual, gb->multiply(update, gb->sigmoid(gate_weight))); -} - -size_t embed_tokens(CactusGraph* gb, size_t embedding_id, const std::vector& tokens, size_t hidden_dim) { - size_t n = tokens.size(); - size_t input_node = gb->input({n}, Precision::FP32); - std::vector data(n); - for (size_t i = 0; i < n; ++i) data[i] = static_cast(tokens[i]); - gb->set_input(input_node, data.data(), Precision::FP32); - return gb->scalar_multiply(gb->embedding(embedding_id, input_node), std::sqrt(static_cast(hidden_dim))); -} - -} // namespace - -NeedleModel::NeedleModel() : Model() {} - -NeedleModel::NeedleModel(const Config& config) : Model(config) { - weight_nodes_.encoder_layers.resize(config.num_encoder_layers); - weight_nodes_.decoder_layers.resize(config.num_decoder_layers); - float hd = static_cast(config.attention_head_dim); - attention_scale_ = 1.0f / std::sqrt(hd > 0.0f ? hd : 64.0f); - encoder_k_persistent_.assign(config.num_decoder_layers, 0); - encoder_v_persistent_.assign(config.num_decoder_layers, 0); -} - -void NeedleModel::load_weights_to_graph(CactusGraph* gb) { - embedding_node_id_ = gb->mmap_embeddings(embedding_file_path_); - weight_nodes_.encoder_norm_weight = gb->mmap_weights(model_folder_path_ + "/encoder_layer_norm_weight.weights"); - weight_nodes_.decoder_norm_weight = gb->mmap_weights(model_folder_path_ + "/output_norm.weights"); - - if (config_.tie_word_embeddings) { - weight_nodes_.output_weight = embedding_node_id_; - output_weight_node_id_ = embedding_node_id_; - } else { - weight_nodes_.output_weight = gb->mmap_weights(model_folder_path_ + "/output_weight.weights"); - output_weight_node_id_ = weight_nodes_.output_weight; - } - - for (uint32_t i = 0; i < config_.num_encoder_layers; ++i) { - auto& l = weight_nodes_.encoder_layers[i]; - std::string p = model_folder_path_ + "/encoder_layer_" + std::to_string(i) + "_"; - l.attn_gate_weight = gb->mmap_weights(p + "attn_gate.weights"); - l.input_norm_weight = gb->mmap_weights(p + "input_norm.weights"); - l.attn_q_weight = gb->mmap_weights(p + "attn_q.weights"); - l.attn_k_weight = gb->mmap_weights(p + "attn_k.weights"); - l.attn_v_weight = gb->mmap_weights(p + "attn_v.weights"); - l.attn_output_weight = gb->mmap_weights(p + "attn_output.weights"); - l.attn_q_norm_weight = gb->mmap_weights(p + "attn_q_norm.weights"); - l.attn_k_norm_weight = gb->mmap_weights(p + "attn_k_norm.weights"); - } - - for (uint32_t i = 0; i < config_.num_decoder_layers; ++i) { - auto& l = weight_nodes_.decoder_layers[i]; - std::string p = model_folder_path_ + "/layer_" + std::to_string(i) + "_"; - l.self_attn_gate_weight = gb->mmap_weights(p + "self_attn_gate.weights"); - l.cross_attn_gate_weight = gb->mmap_weights(p + "cross_attn_gate.weights"); - l.input_norm_weight = gb->mmap_weights(p + "input_norm.weights"); - l.post_attn_norm_weight = gb->mmap_weights(p + "post_attn_norm.weights"); - l.self_attn_q_weight = gb->mmap_weights(p + "attn_q.weights"); - l.self_attn_k_weight = gb->mmap_weights(p + "attn_k.weights"); - l.self_attn_v_weight = gb->mmap_weights(p + "attn_v.weights"); - l.self_attn_output_weight = gb->mmap_weights(p + "attn_output.weights"); - l.self_attn_q_norm_weight = gb->mmap_weights(p + "attn_q_norm.weights"); - l.self_attn_k_norm_weight = gb->mmap_weights(p + "attn_k_norm.weights"); - l.encoder_attn_q_weight = gb->mmap_weights(p + "encoder_attn_q.weights"); - l.encoder_attn_k_weight = gb->mmap_weights(p + "encoder_attn_k.weights"); - l.encoder_attn_v_weight = gb->mmap_weights(p + "encoder_attn_v.weights"); - l.encoder_attn_output_weight = gb->mmap_weights(p + "encoder_attn_output.weights"); - l.encoder_attn_q_norm_weight = gb->mmap_weights(p + "encoder_attn_q_norm.weights"); - l.encoder_attn_k_norm_weight = gb->mmap_weights(p + "encoder_attn_k_norm.weights"); - } -} - -void NeedleModel::reset_graph_side_cache_nodes() { - cache_k_output_nodes_.assign(config_.num_decoder_layers, 0); - cache_v_output_nodes_.assign(config_.num_decoder_layers, 0); -} - -void NeedleModel::reset_cache() { - Model::reset_cache(); - encoder_ready_ = false; - auto* gb = static_cast(graph_handle_); - if (!gb) { reset_graph_side_cache_nodes(); return; } - - if (last_encoder_post_norm_node_ != 0) { - gb->invalidate_persistent(last_encoder_post_norm_node_); - last_encoder_post_norm_node_ = 0; - } - for (size_t i = 0; i < encoder_k_persistent_.size(); ++i) { - if (encoder_k_persistent_[i] != 0) { gb->invalidate_persistent(encoder_k_persistent_[i]); encoder_k_persistent_[i] = 0; } - if (encoder_v_persistent_[i] != 0) { gb->invalidate_persistent(encoder_v_persistent_[i]); encoder_v_persistent_[i] = 0; } - } - reset_graph_side_cache_nodes(); -} - -size_t NeedleModel::build_encoder_self_attention(CactusGraph* gb, size_t input, - uint32_t layer_idx, ComputeBackend backend, - bool /*use_cache*/, size_t /*position_offset*/) { - const auto& l = weight_nodes_.encoder_layers[layer_idx]; - auto q = gb->matmul(input, l.attn_q_weight, true, backend); - auto k = gb->matmul(input, l.attn_k_weight, true, backend); - auto v = gb->matmul(input, l.attn_v_weight, true, backend); - - size_t seq = gb->get_output_buffer(q).shape[0]; - size_t nh = config_.attention_heads, nkv = config_.attention_kv_heads, hd = config_.attention_head_dim; - - q = normalize_qk_proj(gb, q, l.attn_q_norm_weight, seq, nh, hd, config_.layer_norm_eps); - k = normalize_qk_proj(gb, k, l.attn_k_norm_weight, seq, nkv, hd, config_.layer_norm_eps); - - auto q4 = gb->reshape(q, {1, seq, nh, hd}); - auto k4 = gb->reshape(k, {1, seq, nkv, hd}); - auto v4 = gb->reshape(v, {1, seq, nkv, hd}); - if (config_.rope_theta > 0) { q4 = gb->rope(q4, config_.rope_theta, 0); k4 = gb->rope(k4, config_.rope_theta, 0); } - - auto attn = gb->attention(q4, k4, v4, attention_scale_, false); - return gb->matmul(gb->reshape(attn, {seq, nh * hd}), l.attn_output_weight, true, backend); -} - -size_t NeedleModel::build_decoder_self_attention(CactusGraph* gb, size_t input, - uint32_t layer_idx, ComputeBackend backend, - bool use_cache, size_t position_offset) { - const auto& l = weight_nodes_.decoder_layers[layer_idx]; - auto q = gb->matmul(input, l.self_attn_q_weight, true, backend); - auto k = gb->matmul(input, l.self_attn_k_weight, true, backend); - auto v = gb->matmul(input, l.self_attn_v_weight, true, backend); - - size_t seq = gb->get_output_buffer(q).shape[0]; - size_t nh = config_.attention_heads, nkv = config_.attention_kv_heads, hd = config_.attention_head_dim; - - q = normalize_qk_proj(gb, q, l.self_attn_q_norm_weight, seq, nh, hd, config_.layer_norm_eps); - k = normalize_qk_proj(gb, k, l.self_attn_k_norm_weight, seq, nkv, hd, config_.layer_norm_eps); - - auto q4 = gb->reshape(q, {1, seq, nh, hd}); - auto k4 = gb->reshape(k, {1, seq, nkv, hd}); - auto v4 = gb->reshape(v, {1, seq, nkv, hd}); - if (config_.rope_theta > 0) { q4 = gb->rope(q4, config_.rope_theta, position_offset); k4 = gb->rope(k4, config_.rope_theta, position_offset); } - - size_t final_k = k4, final_v = v4; - if (use_cache && !kv_cache_.is_empty()) { - size_t cache_len = kv_cache_.current_seq_len; - size_t ck = gb->input({1, cache_len, nkv, hd}, kv_cache_.precision); - size_t cv = gb->input({1, cache_len, nkv, hd}, kv_cache_.precision); - auto kv = kv_cache_.get_key_view(layer_idx); - auto vv = kv_cache_.get_value_view(layer_idx); - if (!kv.ptr2 && !vv.ptr2) { - gb->set_external_input(ck, const_cast(kv.ptr1), kv_cache_.precision); - gb->set_external_input(cv, const_cast(vv.ptr1), kv_cache_.precision); - } else { - gb->set_external_input(ck, kv_cache_.get_key_ptr(layer_idx), kv_cache_.precision); - gb->set_external_input(cv, kv_cache_.get_value_ptr(layer_idx), kv_cache_.precision); - } - final_k = gb->concat(ck, k4, 1); - final_v = gb->concat(cv, v4, 1); - } - - cache_k_output_nodes_[layer_idx] = use_cache ? final_k : k4; - cache_v_output_nodes_[layer_idx] = use_cache ? final_v : v4; - - auto attn = gb->attention(q4, final_k, final_v, attention_scale_, position_offset); - return gb->matmul(gb->reshape(attn, {seq, nh * hd}), l.self_attn_output_weight, true, backend); -} - -size_t NeedleModel::build_decoder_cross_attention(CactusGraph* gb, size_t input, - uint32_t layer_idx, ComputeBackend backend, - bool /*use_cache*/, size_t /*position_offset*/) { - const auto& l = weight_nodes_.decoder_layers[layer_idx]; - size_t nh = config_.attention_heads, nkv = config_.attention_kv_heads, hd = config_.attention_head_dim; - - auto q = gb->matmul(input, l.encoder_attn_q_weight, true, backend); - size_t seq_dec = gb->get_output_buffer(q).shape[0]; - q = normalize_qk_proj(gb, q, l.encoder_attn_q_norm_weight, seq_dec, nh, hd, config_.layer_norm_eps); - auto q4 = gb->reshape(q, {1, seq_dec, nh, hd}); - - size_t k4 = 0, v4 = 0; - bool cached = encoder_k_persistent_[layer_idx] != 0 && gb->is_populated(encoder_k_persistent_[layer_idx]); - if (cached) { - k4 = encoder_k_persistent_[layer_idx]; - v4 = encoder_v_persistent_[layer_idx]; - } else { - auto kp = gb->matmul(last_encoder_post_norm_node_, l.encoder_attn_k_weight, true, backend); - auto vp = gb->matmul(last_encoder_post_norm_node_, l.encoder_attn_v_weight, true, backend); - size_t seq_enc = gb->get_output_buffer(kp).shape[0]; - kp = normalize_qk_proj(gb, kp, l.encoder_attn_k_norm_weight, seq_enc, nkv, hd, config_.layer_norm_eps); - k4 = gb->reshape(kp, {1, seq_enc, nkv, hd}); - v4 = gb->reshape(vp, {1, seq_enc, nkv, hd}); - if (encoder_k_persistent_[layer_idx] == 0) { - encoder_k_persistent_[layer_idx] = gb->persistent(k4); - encoder_v_persistent_[layer_idx] = gb->persistent(v4); - } - k4 = encoder_k_persistent_[layer_idx]; - v4 = encoder_v_persistent_[layer_idx]; - } - - auto attn = gb->attention(q4, k4, v4, attention_scale_, false); - return gb->matmul(gb->reshape(attn, {seq_dec, nh * hd}), l.encoder_attn_output_weight, true, backend); -} - -size_t NeedleModel::build_encoder_transformer_block(CactusGraph* gb, size_t hidden, - uint32_t layer_idx, ComputeBackend backend, - bool use_cache, size_t position_offset) { - const auto& l = weight_nodes_.encoder_layers[layer_idx]; - auto normed = delta_rms_norm(gb, hidden, l.input_norm_weight, config_.layer_norm_eps); - auto attn = build_encoder_self_attention(gb, normed, layer_idx, backend, use_cache, position_offset); - return apply_residual_gate(gb, hidden, attn, l.attn_gate_weight); -} - -size_t NeedleModel::build_decoder_transformer_block(CactusGraph* gb, size_t hidden, - uint32_t layer_idx, ComputeBackend backend, - bool use_cache, size_t position_offset) { - const auto& l = weight_nodes_.decoder_layers[layer_idx]; - auto normed = delta_rms_norm(gb, hidden, l.input_norm_weight, config_.layer_norm_eps); - auto self_out = build_decoder_self_attention(gb, normed, layer_idx, backend, use_cache, position_offset); - auto after_self = apply_residual_gate(gb, hidden, self_out, l.self_attn_gate_weight); - auto cross_in = delta_rms_norm(gb, after_self, l.post_attn_norm_weight, config_.layer_norm_eps); - auto cross_out = build_decoder_cross_attention(gb, cross_in, layer_idx, backend, use_cache, position_offset); - return apply_residual_gate(gb, after_self, cross_out, l.cross_attn_gate_weight); -} - -void NeedleModel::run_encoder(const std::vector& encoder_tokens) { - auto* gb = static_cast(graph_handle_); - auto backend = config_.default_backend == Config::Backend::CPU ? ComputeBackend::CPU : ComputeBackend::NPU; - auto hidden = embed_tokens(gb, embedding_node_id_, encoder_tokens, config_.hidden_dim); - - for (uint32_t i = 0; i < config_.num_encoder_layers; ++i) - hidden = build_encoder_transformer_block(gb, hidden, i, backend, false, 0); - - last_encoder_post_norm_node_ = gb->persistent( - delta_rms_norm(gb, hidden, weight_nodes_.encoder_norm_weight, config_.layer_norm_eps)); -} - -size_t NeedleModel::run_decoder_step(const std::vector& tokens, bool use_cache, bool last_token_only) { - auto* gb = static_cast(graph_handle_); - auto backend = config_.default_backend == Config::Backend::CPU ? ComputeBackend::CPU : ComputeBackend::NPU; - size_t pos_offset = use_cache ? kv_cache_.current_seq_len : 0; - - auto hidden = embed_tokens(gb, embedding_node_id_, tokens, config_.hidden_dim); - for (uint32_t i = 0; i < config_.num_decoder_layers; ++i) - hidden = build_decoder_transformer_block(gb, hidden, i, backend, use_cache, pos_offset); - - auto norm = delta_rms_norm(gb, hidden, weight_nodes_.decoder_norm_weight, config_.layer_norm_eps); - if (last_token_only) norm = gb->slice(norm, 0, tokens.size() - 1, 1); - return gb->matmul(norm, output_weight_node_id_, true, backend); -} - -size_t NeedleModel::forward(const std::vector& tokens, bool use_cache) { - auto* gb = static_cast(graph_handle_); - gb->soft_reset(); - reset_graph_side_cache_nodes(); - - if (!encoder_ready_ || !use_cache) { - std::vector enc(tokens.begin(), tokens.end() - 1); - std::vector seed(tokens.end() - 1, tokens.end()); - if (!use_cache) { reset_cache(); gb->soft_reset(); } - run_encoder(enc); - encoder_ready_ = true; - return run_decoder_step(seed, false, false); - } - return run_decoder_step(tokens, true, tokens.size() == 1); -} - -void NeedleModel::prefill(const std::vector& tokens, size_t /*chunk_size*/, const std::string& profile_file) { - if (tokens.empty()) return; - reset_cache(); - auto* gb = static_cast(graph_handle_); - gb->soft_reset(); - run_encoder(tokens); - if (!profile_file.empty()) gb->execute(profile_file); else gb->execute(); - encoder_ready_ = true; -} - -uint32_t NeedleModel::decode(const std::vector& tokens, - float temperature, float top_p, size_t top_k, - const std::string& profile_file, float* out_entropy, - float /*min_p*/, float /*repetition_penalty*/) { - if (temperature < 0) temperature = config_.default_temperature; - if (top_p < 0) top_p = config_.default_top_p; - if (top_k == 0) top_k = config_.default_top_k; - - auto* gb = static_cast(graph_handle_); - gb->soft_reset(); - reset_graph_side_cache_nodes(); - - bool cold_start = kv_cache_.is_empty(); - std::vector cold_tokens; - const std::vector* dec_tokens = &tokens; - if (!encoder_ready_) { - reset_cache(); gb->soft_reset(); - std::vector enc(tokens.begin(), tokens.end() - 1); - cold_tokens.assign(tokens.end() - 1, tokens.end()); - run_encoder(enc); - encoder_ready_ = true; - dec_tokens = &cold_tokens; - cold_start = true; - } - - size_t logits = cold_start - ? run_decoder_step(*dec_tokens, false, false) - : run_decoder_step(tokens, true, tokens.size() == 1); - - if (config_.final_logit_softcapping > 0.0f) { - float inv = 1.0f / config_.final_logit_softcapping; - logits = gb->scalar_multiply(gb->tanh(gb->scalar_multiply(logits, inv)), config_.final_logit_softcapping); - } - - auto sampled = sample_token(gb, logits, temperature, top_p, top_k, 0.0f, 1.0f); - if (!profile_file.empty()) gb->execute(profile_file); else gb->execute(); - - compute_entropy(gb, logits, out_entropy); - post_execute_updates(gb, tokens.size()); - update_kv_cache(gb, dec_tokens->size()); - return *static_cast(gb->get_output(sampled)); -} - -} // namespace engine -} // namespace cactus diff --git a/cactus/models/model_nomic.cpp b/cactus/models/model_nomic.cpp deleted file mode 100644 index a9477a865..000000000 --- a/cactus/models/model_nomic.cpp +++ /dev/null @@ -1,201 +0,0 @@ -#include "model.h" -#include "../graph/graph.h" -#include - - -namespace cactus { -namespace engine { - -NomicModel::NomicModel() : Model() {} - -NomicModel::NomicModel(const Config& config) : Model(config) { - weight_nodes_.layers.resize(config.num_layers); -} - -void NomicModel::load_weights_to_graph(CactusGraph* gb) { - embedding_node_id_ = gb->mmap_embeddings(embedding_file_path_); - weight_nodes_.embedding_layernorm_weight = gb->mmap_weights(model_folder_path_ + "/embedding_layernorm.weight"); - weight_nodes_.embedding_layernorm_bias = gb->mmap_weights(model_folder_path_ + "/embedding_layernorm.bias"); - - for (uint32_t i = 0; i < config_.num_layers; i++) { - auto& layer = weight_nodes_.layers[i]; - std::string layer_prefix = model_folder_path_ + "/layer_" + std::to_string(i) + "_"; - layer.attn_q_weight = gb->mmap_weights(layer_prefix + "attn_q.weights"); - layer.attn_k_weight = gb->mmap_weights(layer_prefix + "attn_k.weights"); - layer.attn_v_weight = gb->mmap_weights(layer_prefix + "attn_v.weights"); - layer.attn_q_bias = gb->mmap_weights(layer_prefix + "attn_q.bias"); - layer.attn_k_bias = gb->mmap_weights(layer_prefix + "attn_k.bias"); - layer.attn_v_bias = gb->mmap_weights(layer_prefix + "attn_v.bias"); - - layer.attn_output_weight = gb->mmap_weights(layer_prefix + "attn_output.weights"); - layer.attn_output_bias = gb->mmap_weights(layer_prefix + "attn_output.bias"); - layer.ffn_norm_1_weight = gb->mmap_weights(layer_prefix + "norm1.weights"); - layer.ffn_norm_1_bias = gb->mmap_weights(layer_prefix + "norm1.bias"); - layer.ffn_norm_2_weight = gb->mmap_weights(layer_prefix + "norm2.weights"); - layer.ffn_norm_2_bias = gb->mmap_weights(layer_prefix + "norm2.bias"); - - if ((i + 1) % config_.moe_every_n_layers != 0) { - layer.ffn_up_weight = gb->mmap_weights(layer_prefix + "mlp_fc1.weights"); - layer.ffn_up_bias = gb->mmap_weights(layer_prefix + "mlp_fc1.bias"); - layer.ffn_down_weight = gb->mmap_weights(layer_prefix + "mlp_fc2.weights"); - layer.ffn_down_bias = gb->mmap_weights(layer_prefix + "mlp_fc2.bias"); - - } else { - layer.mlp_router_layer_weight = gb->mmap_weights(layer_prefix + "mlp_router.layer.weights"); - layer.mlp_experts_bias = gb->mmap_weights(layer_prefix + "mlp_experts.bias"); - - for (uint32_t j = 0; j < config_.num_experts; j++) { - layer.mlp_experts_mlp1_weight.push_back(gb->mmap_weights(layer_prefix + "mlp_expert_" + std::to_string(j) + ".mlp1.weights")); - layer.mlp_experts_mlp2_weight.push_back(gb->mmap_weights(layer_prefix + "mlp_expert_" + std::to_string(j) + ".mlp2.weights")); - } - } - } -} - -size_t NomicModel::build_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache, size_t position_offset) { - if (use_cache) { - throw std::runtime_error("NomicModel does not support generation, it's an encoder model"); - } - (void)position_offset; - - const auto& layer = weight_nodes_.layers[layer_idx]; - - auto q_proj = gb->matmul(normalized_input, layer.attn_q_weight, true, backend); - q_proj = gb->add(q_proj, layer.attn_q_bias); - - auto k_proj = gb->matmul(normalized_input, layer.attn_k_weight, true, backend); - k_proj = gb->add(k_proj, layer.attn_k_bias); - - auto v_proj = gb->matmul(normalized_input, layer.attn_v_weight, true, backend); - v_proj = gb->add(v_proj, layer.attn_v_bias); - - const auto& q_shape = gb->get_output_buffer(q_proj).shape; - const size_t seq_len = q_shape[0]; - const size_t num_heads = config_.attention_heads; - const size_t head_dim = config_.attention_head_dim; - - if (num_heads == 0 || head_dim == 0) { - throw std::runtime_error("Invalid attention head configuration for Nomic model"); - } - - auto reshape_to_heads = [&](size_t tensor) { - return gb->reshape(tensor, {1, seq_len, num_heads, head_dim}); - }; - - auto q_proj_4d = reshape_to_heads(q_proj); - auto k_proj_4d = reshape_to_heads(k_proj); - auto v_proj_4d = reshape_to_heads(v_proj); - - if (config_.rope_theta > 0) { - q_proj_4d = gb->rope(q_proj_4d, config_.rope_theta, 0); - k_proj_4d = gb->rope(k_proj_4d, config_.rope_theta, 0); - } - - auto attn_output_4d = gb->attention(q_proj_4d, k_proj_4d, v_proj_4d, attention_scale_, false); - auto attn_output = gb->reshape(attn_output_4d, {seq_len, num_heads * head_dim}); - - auto output = gb->matmul(attn_output, layer.attn_output_weight, true, backend); - output = gb->add(output, layer.attn_output_bias); - return output; -} - -size_t NomicModel::build_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, - ComputeBackend backend) const { - if ((layer_idx + 1) % config_.moe_every_n_layers != 0) { - return build_standard_mlp(gb, normalized_h, layer_idx, backend); - } else { - return build_moe_mlp(gb, normalized_h, layer_idx, backend); - } -} - -size_t NomicModel::build_standard_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, - ComputeBackend backend) const { - const auto& layer = weight_nodes_.layers[layer_idx]; - auto hidden = gb->matmul(normalized_h, layer.ffn_up_weight, true, backend); - hidden = gb->add(hidden, layer.ffn_up_bias); - hidden = gb->gelu(hidden); - hidden = gb->matmul(hidden, layer.ffn_down_weight, true, backend); - hidden = gb->add(hidden, layer.ffn_down_bias); - return hidden; -} - -size_t NomicModel::build_moe_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, - ComputeBackend backend) const { - const auto& layer = weight_nodes_.layers[layer_idx]; - const size_t num_experts = config_.num_experts != 0 - ? config_.num_experts - : gb->get_output_buffer(layer.mlp_router_layer_weight).shape[0]; - - auto gate_weights = gb->matmul(normalized_h, layer.mlp_router_layer_weight, true, backend); - auto gate_probs = gb->softmax(gate_weights); - auto topk_result = gb->topk(gate_probs, config_.num_top_experts); - auto topk_idx = gb->index(topk_result, 0, 0); - - std::vector w1_weights, w2_weights; - for (size_t e = 0; e < num_experts; ++e) { - w1_weights.push_back(layer.mlp_experts_mlp1_weight[e]); - w2_weights.push_back(layer.mlp_experts_mlp2_weight[e]); - } - - auto moe_out = gb->moe_layer( - normalized_h, gate_probs, topk_idx, - w1_weights, w2_weights, - num_experts, config_.num_top_experts, - false, - 1e-6f, - 1.0f, - Activation::GELU - ); - - return gb->add(moe_out, layer.mlp_experts_bias); -} - -size_t NomicModel::build_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache, size_t position_offset) { - if (use_cache) { - throw std::runtime_error("NomicModel does not support generation, it's an encoder model"); - } - (void)position_offset; - const auto& layer = weight_nodes_.layers[layer_idx]; - auto attn_output = build_attention(gb, hidden, layer_idx, backend); - auto residual = gb->add(hidden, attn_output); - auto normalized_residual = gb->layernorm(residual, layer.ffn_norm_1_weight, layer.ffn_norm_1_bias, config_.layer_norm_eps); - auto mlp_output = build_mlp(gb, normalized_residual, layer_idx, backend); - auto final_residual = gb->add(normalized_residual, mlp_output); - auto normalized_final_residual = gb->layernorm(final_residual, layer.ffn_norm_2_weight, layer.ffn_norm_2_bias, config_.layer_norm_eps); - return normalized_final_residual; -} - -size_t NomicModel::forward(const std::vector& tokens, bool use_cache) { - if (use_cache) { - throw std::runtime_error("NomicModel does not support generation, it's an encoder model"); - } - auto* gb = static_cast(graph_handle_); - gb->soft_reset(); - - size_t seq_len = static_cast(tokens.size()); - auto backend = config_.default_backend == Config::Backend::CPU - ? ComputeBackend::CPU - : ComputeBackend::NPU; - - size_t input_node_id = gb->input({seq_len}, Precision::FP32); - std::vector input_data(seq_len); - for (size_t i = 0; i < seq_len; i++) { - input_data[i] = static_cast(tokens[i]); - } - gb->set_input(input_node_id, input_data.data(), Precision::FP32); - - size_t hidden = gb->embedding(embedding_node_id_, input_node_id); - - hidden = gb->layernorm(hidden, weight_nodes_.embedding_layernorm_weight, weight_nodes_.embedding_layernorm_bias, config_.layer_norm_eps); - - for (uint32_t layer_idx = 0; layer_idx < config_.num_layers; layer_idx++) { - hidden = build_transformer_block(gb, hidden, layer_idx, backend); - } - - return hidden; -} - -} -} diff --git a/cactus/models/model_parakeet.cpp b/cactus/models/model_parakeet.cpp deleted file mode 100644 index 7aee878b3..000000000 --- a/cactus/models/model_parakeet.cpp +++ /dev/null @@ -1,860 +0,0 @@ -#include "model.h" -#include "../graph/graph.h" -#include "../npu/npu.h" -#include "../kernel/kernel.h" -#include -#include -#include -#include -#include -#include -#include - -size_t shape_elements(const std::vector& shape) { - if (shape.empty()) return 0; - size_t total = 1; - for (int d : shape) { - if (d <= 0) return 0; - total *= static_cast(d); - } - return total; -} - -bool pack_parakeet_features_for_npu( - const std::vector<__fp16>& time_major_f16, - size_t frames, - size_t num_mels, - const std::vector& input_shape, - std::vector<__fp16>& packed) -{ - if (input_shape.empty()) return false; - const size_t total = shape_elements(input_shape); - if (total == 0) return false; - packed.assign(total, static_cast<__fp16>(0.0f)); - - auto tm = [&](size_t t, size_t m) -> __fp16 { - return time_major_f16[t * num_mels + m]; - }; - - if (input_shape.size() == 4) { - const size_t s0 = static_cast(input_shape[0]); - const size_t s1 = static_cast(input_shape[1]); - const size_t s2 = static_cast(input_shape[2]); - const size_t s3 = static_cast(input_shape[3]); - if (s0 != 1) return false; - - if (s1 == 1 && s2 >= frames && s3 == num_mels) { - for (size_t t = 0; t < frames; ++t) { - for (size_t m = 0; m < num_mels; ++m) { - packed[(t * s3) + m] = tm(t, m); - } - } - return true; - } - if (s1 >= frames && s2 == num_mels && s3 == 1) { - for (size_t t = 0; t < frames; ++t) { - for (size_t m = 0; m < num_mels; ++m) { - packed[((t * s2 + m) * s3)] = tm(t, m); - } - } - return true; - } - if (s1 == num_mels && s2 >= frames && s3 == 1) { - for (size_t t = 0; t < frames; ++t) { - for (size_t m = 0; m < num_mels; ++m) { - packed[((m * s2 + t) * s3)] = tm(t, m); - } - } - return true; - } - if (s1 == 1 && s2 == num_mels && s3 >= frames) { - for (size_t t = 0; t < frames; ++t) { - for (size_t m = 0; m < num_mels; ++m) { - packed[(m * s3) + t] = tm(t, m); - } - } - return true; - } - return false; - } - - if (input_shape.size() == 3) { - const size_t s0 = static_cast(input_shape[0]); - const size_t s1 = static_cast(input_shape[1]); - const size_t s2 = static_cast(input_shape[2]); - - if (s0 == 1 && s1 >= frames && s2 == num_mels) { - for (size_t t = 0; t < frames; ++t) { - for (size_t m = 0; m < num_mels; ++m) { - packed[t * s2 + m] = tm(t, m); - } - } - return true; - } - if (s0 == 1 && s1 == num_mels && s2 >= frames) { - for (size_t t = 0; t < frames; ++t) { - for (size_t m = 0; m < num_mels; ++m) { - packed[m * s2 + t] = tm(t, m); - } - } - return true; - } - if (s0 >= frames && s1 == num_mels && s2 == 1) { - for (size_t t = 0; t < frames; ++t) { - for (size_t m = 0; m < num_mels; ++m) { - packed[(t * s1 + m) * s2] = tm(t, m); - } - } - return true; - } - if (s0 == num_mels && s1 >= frames && s2 == 1) { - for (size_t t = 0; t < frames; ++t) { - for (size_t m = 0; m < num_mels; ++m) { - packed[(m * s1 + t) * s2] = tm(t, m); - } - } - return true; - } - return false; - } - - if (input_shape.size() == 2) { - const size_t s0 = static_cast(input_shape[0]); - const size_t s1 = static_cast(input_shape[1]); - - if (s0 >= frames && s1 == num_mels) { - for (size_t t = 0; t < frames; ++t) { - for (size_t m = 0; m < num_mels; ++m) { - packed[t * s1 + m] = tm(t, m); - } - } - return true; - } - if (s0 == num_mels && s1 >= frames) { - for (size_t t = 0; t < frames; ++t) { - for (size_t m = 0; m < num_mels; ++m) { - packed[m * s1 + t] = tm(t, m); - } - } - return true; - } - return false; - } - - return false; -} - -bool infer_npu_encoder_output_shape( - const std::vector& output_shape, - size_t elements_written, - size_t fallback_hidden_dim, - size_t& time_steps, - size_t& hidden_dim) -{ - if (elements_written == 0) return false; - - std::vector dims; - dims.reserve(output_shape.size()); - for (int d : output_shape) { - if (d > 0) dims.push_back(static_cast(d)); - } - - if (dims.size() >= 2) { - time_steps = dims[dims.size() - 2]; - hidden_dim = dims[dims.size() - 1]; - } else if (dims.size() == 1) { - hidden_dim = dims[0]; - if (hidden_dim == 0 || (elements_written % hidden_dim) != 0) return false; - time_steps = elements_written / hidden_dim; - } else { - hidden_dim = fallback_hidden_dim; - if (hidden_dim == 0 || (elements_written % hidden_dim) != 0) return false; - time_steps = elements_written / hidden_dim; - } - - if (time_steps == 0 || hidden_dim == 0) return false; - if (time_steps * hidden_dim > elements_written) return false; - return true; -} - -namespace cactus { -namespace engine { - -ParakeetModel::ParakeetModel() : Model() {} - -ParakeetModel::ParakeetModel(const Config& config) : Model(config) { - weight_nodes_.layers.resize(config.num_layers); - - float hd = static_cast(config.attention_head_dim); - if (hd <= 0.0f) { - hd = 64.0f; - } - attention_scale_ = 1.0f / std::sqrt(hd); - - // Legacy converted Parakeet configs inherited generic rope_theta defaults. - // Keep RoPE disabled unless explicitly set to a non-default value. - if (std::fabs(config_.rope_theta - 10000.0f) < 1e-3f || - std::fabs(config_.rope_theta - 1000000.0f) < 1e-3f) { - config_.rope_theta = 0.0f; - } -} - -void ParakeetModel::load_weights_to_graph(CactusGraph* gb) { - weight_nodes_.subsampling_conv0_weight = gb->mmap_weights(model_folder_path_ + "/subsampling_conv0_weight.weights"); - weight_nodes_.subsampling_conv0_bias = gb->mmap_weights(model_folder_path_ + "/subsampling_conv0_bias.bias"); - weight_nodes_.subsampling_depthwise1_weight = gb->mmap_weights(model_folder_path_ + "/subsampling_depthwise1_weight.weights"); - weight_nodes_.subsampling_depthwise1_bias = gb->mmap_weights(model_folder_path_ + "/subsampling_depthwise1_bias.bias"); - weight_nodes_.subsampling_pointwise1_weight = gb->mmap_weights(model_folder_path_ + "/subsampling_pointwise1_weight.weights"); - weight_nodes_.subsampling_pointwise1_bias = gb->mmap_weights(model_folder_path_ + "/subsampling_pointwise1_bias.bias"); - weight_nodes_.subsampling_depthwise2_weight = gb->mmap_weights(model_folder_path_ + "/subsampling_depthwise2_weight.weights"); - weight_nodes_.subsampling_depthwise2_bias = gb->mmap_weights(model_folder_path_ + "/subsampling_depthwise2_bias.bias"); - weight_nodes_.subsampling_pointwise2_weight = gb->mmap_weights(model_folder_path_ + "/subsampling_pointwise2_weight.weights"); - weight_nodes_.subsampling_pointwise2_bias = gb->mmap_weights(model_folder_path_ + "/subsampling_pointwise2_bias.bias"); - weight_nodes_.subsampling_linear_weight = gb->mmap_weights(model_folder_path_ + "/subsampling_linear_weight.weights"); - weight_nodes_.subsampling_linear_bias = gb->mmap_weights(model_folder_path_ + "/subsampling_linear_bias.bias"); - - weight_nodes_.ctc_head_weight = gb->mmap_weights(model_folder_path_ + "/ctc_head_weight.weights"); - weight_nodes_.ctc_head_bias = gb->mmap_weights(model_folder_path_ + "/ctc_head_bias.bias"); - - if (npu::is_npu_available()) { - std::string npu_encoder_path = model_folder_path_ + "/model.mlpackage"; - npu_encoder_ = npu::create_encoder(); - if (npu_encoder_ && npu_encoder_->load(npu_encoder_path)) { - use_npu_encoder_ = true; - std::vector input_shape = npu_encoder_->get_input_shape(); - if (!input_shape.empty()) { - npu_encoder_->preallocate(input_shape, "x", ""); - } - } else { - use_npu_encoder_ = false; - npu_encoder_.reset(); - } - } - - const std::filesystem::path model_path(model_folder_path_); - has_cpu_encoder_weights_ = - std::filesystem::exists(model_path / "subsampling_conv0_weight.weights") && - std::filesystem::exists(model_path / "subsampling_linear_weight.weights"); - - if (has_cpu_encoder_weights_) { - weight_nodes_.subsampling_conv0_weight = gb->mmap_weights(model_folder_path_ + "/subsampling_conv0_weight.weights"); - weight_nodes_.subsampling_conv0_bias = gb->mmap_weights(model_folder_path_ + "/subsampling_conv0_bias.bias"); - weight_nodes_.subsampling_depthwise1_weight = gb->mmap_weights(model_folder_path_ + "/subsampling_depthwise1_weight.weights"); - weight_nodes_.subsampling_depthwise1_bias = gb->mmap_weights(model_folder_path_ + "/subsampling_depthwise1_bias.bias"); - weight_nodes_.subsampling_pointwise1_weight = gb->mmap_weights(model_folder_path_ + "/subsampling_pointwise1_weight.weights"); - weight_nodes_.subsampling_pointwise1_bias = gb->mmap_weights(model_folder_path_ + "/subsampling_pointwise1_bias.bias"); - weight_nodes_.subsampling_depthwise2_weight = gb->mmap_weights(model_folder_path_ + "/subsampling_depthwise2_weight.weights"); - weight_nodes_.subsampling_depthwise2_bias = gb->mmap_weights(model_folder_path_ + "/subsampling_depthwise2_bias.bias"); - weight_nodes_.subsampling_pointwise2_weight = gb->mmap_weights(model_folder_path_ + "/subsampling_pointwise2_weight.weights"); - weight_nodes_.subsampling_pointwise2_bias = gb->mmap_weights(model_folder_path_ + "/subsampling_pointwise2_bias.bias"); - weight_nodes_.subsampling_linear_weight = gb->mmap_weights(model_folder_path_ + "/subsampling_linear_weight.weights"); - weight_nodes_.subsampling_linear_bias = gb->mmap_weights(model_folder_path_ + "/subsampling_linear_bias.bias"); - } - - for (uint32_t i = 0; i < config_.num_layers; ++i) { - auto& layer = weight_nodes_.layers[i]; - std::string layer_prefix = model_folder_path_ + "/layer_" + std::to_string(i) + "_"; - - layer.ff1_linear1_weight = gb->mmap_weights(layer_prefix + "ff1_linear1.weights"); - layer.ff1_linear1_bias = gb->mmap_weights(layer_prefix + "ff1_linear1.bias"); - layer.ff1_linear2_weight = gb->mmap_weights(layer_prefix + "ff1_linear2.weights"); - layer.ff1_linear2_bias = gb->mmap_weights(layer_prefix + "ff1_linear2.bias"); - - layer.ff2_linear1_weight = gb->mmap_weights(layer_prefix + "ff2_linear1.weights"); - layer.ff2_linear1_bias = gb->mmap_weights(layer_prefix + "ff2_linear1.bias"); - layer.ff2_linear2_weight = gb->mmap_weights(layer_prefix + "ff2_linear2.weights"); - layer.ff2_linear2_bias = gb->mmap_weights(layer_prefix + "ff2_linear2.bias"); - - layer.self_attn_q_weight = gb->mmap_weights(layer_prefix + "self_attn_q.weights"); - layer.self_attn_q_bias = gb->mmap_weights(layer_prefix + "self_attn_q.bias"); - layer.self_attn_k_weight = gb->mmap_weights(layer_prefix + "self_attn_k.weights"); - layer.self_attn_k_bias = gb->mmap_weights(layer_prefix + "self_attn_k.bias"); - layer.self_attn_v_weight = gb->mmap_weights(layer_prefix + "self_attn_v.weights"); - layer.self_attn_v_bias = gb->mmap_weights(layer_prefix + "self_attn_v.bias"); - layer.self_attn_output_weight = gb->mmap_weights(layer_prefix + "self_attn_output.weights"); - layer.self_attn_output_bias = gb->mmap_weights(layer_prefix + "self_attn_output.bias"); - layer.self_attn_relative_k_weight = gb->mmap_weights(layer_prefix + "self_attn_relative_k.weights"); - layer.self_attn_bias_u = gb->mmap_weights(layer_prefix + "self_attn_bias_u.weights"); - layer.self_attn_bias_v = gb->mmap_weights(layer_prefix + "self_attn_bias_v.weights"); - - layer.norm_ff1_weight = gb->mmap_weights(layer_prefix + "norm_ff1.weights"); - layer.norm_ff1_bias = gb->mmap_weights(layer_prefix + "norm_ff1.bias"); - layer.norm_self_attn_weight = gb->mmap_weights(layer_prefix + "norm_self_attn.weights"); - layer.norm_self_attn_bias = gb->mmap_weights(layer_prefix + "norm_self_attn.bias"); - layer.norm_conv_weight = gb->mmap_weights(layer_prefix + "norm_conv.weights"); - layer.norm_conv_bias = gb->mmap_weights(layer_prefix + "norm_conv.bias"); - layer.norm_ff2_weight = gb->mmap_weights(layer_prefix + "norm_ff2.weights"); - layer.norm_ff2_bias = gb->mmap_weights(layer_prefix + "norm_ff2.bias"); - layer.norm_out_weight = gb->mmap_weights(layer_prefix + "norm_out.weights"); - layer.norm_out_bias = gb->mmap_weights(layer_prefix + "norm_out.bias"); - - layer.conv_pointwise1_weight = gb->mmap_weights(layer_prefix + "conv_pointwise1.weights"); - layer.conv_pointwise1_bias = gb->mmap_weights(layer_prefix + "conv_pointwise1.bias"); - layer.conv_depthwise_weight = gb->mmap_weights(layer_prefix + "conv_depthwise.weights"); - layer.conv_depthwise_bias = gb->mmap_weights(layer_prefix + "conv_depthwise.bias"); - layer.conv_pointwise2_weight = gb->mmap_weights(layer_prefix + "conv_pointwise2.weights"); - layer.conv_pointwise2_bias = gb->mmap_weights(layer_prefix + "conv_pointwise2.bias"); - layer.conv_batchnorm_weight = gb->mmap_weights(layer_prefix + "conv_batchnorm_weight.weights"); - layer.conv_batchnorm_bias = gb->mmap_weights(layer_prefix + "conv_batchnorm_bias.bias"); - layer.conv_batchnorm_running_mean = gb->mmap_weights(layer_prefix + "conv_batchnorm_running_mean.weights"); - layer.conv_batchnorm_running_var = gb->mmap_weights(layer_prefix + "conv_batchnorm_running_var.weights"); - } -} - -size_t ParakeetModel::build_subsampling(CactusGraph* gb, const std::vector& audio_features) { - const size_t num_mels = std::max(1, static_cast(config_.num_mel_bins)); - if (audio_features.empty() || (audio_features.size() % num_mels) != 0) { - throw std::runtime_error("Parakeet expects audio_features with shape [num_mels, num_frames]"); - } - - const size_t frames = audio_features.size() / num_mels; - std::vector time_major(frames * num_mels); - for (size_t m = 0; m < num_mels; ++m) { - const float* src = &audio_features[m * frames]; - for (size_t t = 0; t < frames; ++t) { - time_major[t * num_mels + m] = src[t]; - } - } - - std::vector<__fp16> features_f16(time_major.size()); - cactus_fp32_to_fp16(time_major.data(), features_f16.data(), time_major.size()); - - size_t x = gb->input({1, 1, frames, num_mels}, Precision::FP16); - gb->set_input(x, features_f16.data(), Precision::FP16); - - x = gb->conv2d_k3s2p1(x, weight_nodes_.subsampling_conv0_weight, weight_nodes_.subsampling_conv0_bias); - x = gb->relu(x); - - x = gb->conv2d_depthwise_k3s2p1(x, weight_nodes_.subsampling_depthwise1_weight, weight_nodes_.subsampling_depthwise1_bias); - x = gb->conv2d_pointwise_1x1(x, weight_nodes_.subsampling_pointwise1_weight, weight_nodes_.subsampling_pointwise1_bias); - x = gb->relu(x); - - x = gb->conv2d_depthwise_k3s2p1(x, weight_nodes_.subsampling_depthwise2_weight, weight_nodes_.subsampling_depthwise2_bias); - x = gb->conv2d_pointwise_1x1(x, weight_nodes_.subsampling_pointwise2_weight, weight_nodes_.subsampling_pointwise2_bias); - x = gb->relu(x); - - const auto& conv_shape = gb->get_output_buffer(x).shape; - if (conv_shape.size() != 4 || conv_shape[0] != 1) { - throw std::runtime_error("Parakeet subsampling produced invalid shape"); - } - - const size_t C = conv_shape[1]; - const size_t T = conv_shape[2]; - const size_t W = conv_shape[3]; - - size_t t_major = gb->transposeN(x, {0, 2, 1, 3}, ComputeBackend::CPU); - size_t flattened = gb->reshape(t_major, {T, C * W}); - size_t projected = gb->matmul(flattened, weight_nodes_.subsampling_linear_weight, true, ComputeBackend::CPU); - projected = gb->add(projected, weight_nodes_.subsampling_linear_bias); - return projected; -} - -size_t ParakeetModel::build_relative_position_embeddings(CactusGraph* gb, size_t seq_len) { - const size_t hidden_dim = std::max(1, static_cast(config_.hidden_dim)); - const size_t half_dim = hidden_dim / 2; - const size_t rel_len = 2 * seq_len - 1; - - std::vector pos_embed(rel_len * hidden_dim, 0.0f); - for (size_t p = 0; p < rel_len; ++p) { - const int rel_pos = static_cast(seq_len - 1) - static_cast(p); - for (size_t i = 0; i < half_dim; ++i) { - const float exponent = static_cast(2 * i) / static_cast(hidden_dim); - const float inv_freq = 1.0f / std::pow(10000.0f, exponent); - const float angle = static_cast(rel_pos) * inv_freq; - pos_embed[p * hidden_dim + 2 * i] = std::sin(angle); - if (2 * i + 1 < hidden_dim) { - pos_embed[p * hidden_dim + 2 * i + 1] = std::cos(angle); - } - } - } - - std::vector<__fp16> pos_embed_f16(pos_embed.size()); - cactus_fp32_to_fp16(pos_embed.data(), pos_embed_f16.data(), pos_embed.size()); - - size_t pos_node = gb->input({rel_len, hidden_dim}, Precision::FP16); - gb->set_input(pos_node, pos_embed_f16.data(), Precision::FP16); - return pos_node; -} - -size_t ParakeetModel::build_self_attention(CactusGraph* gb, size_t hidden, size_t position_embeddings, - uint32_t layer_idx, ComputeBackend backend) { - const auto& layer = weight_nodes_.layers[layer_idx]; - - size_t q = gb->matmul(hidden, layer.self_attn_q_weight, true, backend); - q = gb->add(q, layer.self_attn_q_bias); - size_t k = gb->matmul(hidden, layer.self_attn_k_weight, true, backend); - k = gb->add(k, layer.self_attn_k_bias); - size_t v = gb->matmul(hidden, layer.self_attn_v_weight, true, backend); - v = gb->add(v, layer.self_attn_v_bias); - - const auto& q_shape = gb->get_output_buffer(q).shape; - if (q_shape.size() != 2) { - throw std::runtime_error("Parakeet self-attention expects [T, D]"); - } - - const size_t T = q_shape[0]; - const size_t q_heads = std::max(1, static_cast(config_.attention_heads)); - const size_t kv_heads = std::max(1, static_cast(config_.attention_kv_heads)); - const size_t head_dim = std::max(1, static_cast(config_.attention_head_dim)); - - size_t q4 = gb->reshape(q, {1, T, q_heads, head_dim}); - size_t k4 = gb->reshape(k, {1, T, kv_heads, head_dim}); - size_t v4 = gb->reshape(v, {1, T, kv_heads, head_dim}); - - size_t bias_u = gb->reshape(layer.self_attn_bias_u, {1, static_cast(1), q_heads, head_dim}); - size_t bias_v = gb->reshape(layer.self_attn_bias_v, {1, static_cast(1), q_heads, head_dim}); - size_t q_u4 = gb->add(q4, bias_u); - size_t q_v4 = gb->add(q4, bias_v); - - size_t rel_k_flat = gb->matmul(position_embeddings, layer.self_attn_relative_k_weight, true, backend); - size_t rel_k4 = gb->reshape(rel_k_flat, {1, 2 * T - 1, q_heads, head_dim}); - size_t rel_bias = gb->rel_pos_bias(q_v4, rel_k4, attention_scale_); - - size_t attn = gb->attention_masked(q_u4, k4, v4, rel_bias, attention_scale_, false, backend, true); - attn = gb->reshape(attn, {T, q_heads * head_dim}); - - size_t out = gb->matmul(attn, layer.self_attn_output_weight, true, backend); - out = gb->add(out, layer.self_attn_output_bias); - return out; -} - -size_t ParakeetModel::build_feed_forward(CactusGraph* gb, size_t hidden, uint32_t layer_idx, bool second_ff, ComputeBackend backend) { - const auto& layer = weight_nodes_.layers[layer_idx]; - - size_t w1 = second_ff ? layer.ff2_linear1_weight : layer.ff1_linear1_weight; - size_t b1 = second_ff ? layer.ff2_linear1_bias : layer.ff1_linear1_bias; - size_t w2 = second_ff ? layer.ff2_linear2_weight : layer.ff1_linear2_weight; - size_t b2 = second_ff ? layer.ff2_linear2_bias : layer.ff1_linear2_bias; - - size_t x = gb->matmul(hidden, w1, true, backend); - x = gb->add(x, b1); - - std::string act = config_.encoder_hidden_act; - std::transform(act.begin(), act.end(), act.begin(), ::tolower); - if (act.find("gelu") != std::string::npos) { - x = gb->gelu(x); - } else if (act == "relu") { - x = gb->relu(x); - } else { - x = gb->silu(x); - } - - x = gb->matmul(x, w2, true, backend); - x = gb->add(x, b2); - return x; -} - -size_t ParakeetModel::build_convolution_module(CactusGraph* gb, size_t hidden, uint32_t layer_idx, ComputeBackend backend) { - (void)backend; - const auto& layer = weight_nodes_.layers[layer_idx]; - const auto& hidden_shape = gb->get_output_buffer(hidden).shape; - if (hidden_shape.size() != 2) { - throw std::runtime_error("Parakeet convolution module expects [T, D]"); - } - - const size_t T = hidden_shape[0]; - const size_t D = hidden_shape[1]; - - size_t x = gb->reshape(hidden, {1, T, D}); - x = gb->conv1d_pointwise(x, layer.conv_pointwise1_weight, layer.conv_pointwise1_bias); - x = gb->glu(x, -1); - - x = gb->conv1d_same_depthwise_k9(x, layer.conv_depthwise_weight, layer.conv_depthwise_bias); - - x = gb->batchnorm( - x, - layer.conv_batchnorm_weight, - layer.conv_batchnorm_bias, - layer.conv_batchnorm_running_mean, - layer.conv_batchnorm_running_var, - 2, - 1e-5f - ); - - std::string act = config_.encoder_hidden_act; - std::transform(act.begin(), act.end(), act.begin(), ::tolower); - if (act.find("gelu") != std::string::npos) { - x = gb->gelu(x); - } else if (act == "relu") { - x = gb->relu(x); - } else { - x = gb->silu(x); - } - - x = gb->conv1d_pointwise(x, layer.conv_pointwise2_weight, layer.conv_pointwise2_bias); - - x = gb->reshape(x, {T, D}); - return x; -} - -size_t ParakeetModel::build_encoder_block(CactusGraph* gb, size_t hidden, size_t position_embeddings, - uint32_t layer_idx, ComputeBackend backend) { - const auto& layer = weight_nodes_.layers[layer_idx]; - - size_t ff1_in = gb->layernorm(hidden, layer.norm_ff1_weight, layer.norm_ff1_bias); - size_t ff1 = build_feed_forward(gb, ff1_in, layer_idx, false, backend); - ff1 = gb->scalar_multiply(ff1, 0.5f); - size_t x = gb->add(hidden, ff1); - - size_t attn_in = gb->layernorm(x, layer.norm_self_attn_weight, layer.norm_self_attn_bias); - size_t attn = build_self_attention(gb, attn_in, position_embeddings, layer_idx, backend); - x = gb->add(x, attn); - - size_t conv_in = gb->layernorm(x, layer.norm_conv_weight, layer.norm_conv_bias); - size_t conv = build_convolution_module(gb, conv_in, layer_idx, backend); - x = gb->add(x, conv); - - size_t ff2_in = gb->layernorm(x, layer.norm_ff2_weight, layer.norm_ff2_bias); - size_t ff2 = build_feed_forward(gb, ff2_in, layer_idx, true, backend); - ff2 = gb->scalar_multiply(ff2, 0.5f); - x = gb->add(x, ff2); - - x = gb->layernorm(x, layer.norm_out_weight, layer.norm_out_bias); - return x; -} - -size_t ParakeetModel::build_encoder(CactusGraph* gb, const std::vector& audio_features) { - const size_t num_mels = std::max(1, static_cast(config_.num_mel_bins)); - if (audio_features.empty() || (audio_features.size() % num_mels) != 0) { - throw std::runtime_error("Parakeet expects audio_features with shape [num_mels, num_frames]"); - } - const size_t frames = audio_features.size() / num_mels; - size_t expected_hidden_dim = std::max(1, static_cast(config_.hidden_dim)); - size_t expected_vocab_size = std::max(1, static_cast(config_.vocab_size)); - { - const auto& ctc_w_shape = gb->get_output_buffer(weight_nodes_.ctc_head_weight).shape; - if (ctc_w_shape.size() == 2) { - expected_vocab_size = ctc_w_shape[0]; - expected_hidden_dim = ctc_w_shape[1]; - } else if (ctc_w_shape.size() == 3) { - expected_vocab_size = ctc_w_shape[0]; - expected_hidden_dim = ctc_w_shape[1]; - } - } - - if (use_npu_encoder_ && npu_encoder_ && npu_encoder_->is_available()) { - std::vector time_major(frames * num_mels); - for (size_t m = 0; m < num_mels; ++m) { - const float* src = &audio_features[m * frames]; - for (size_t t = 0; t < frames; ++t) { - time_major[t * num_mels + m] = src[t]; - } - } - - std::vector<__fp16> time_major_f16(time_major.size()); - cactus_fp32_to_fp16(time_major.data(), time_major_f16.data(), time_major.size()); - - std::vector input_shape = npu_encoder_->get_input_shape(); - std::vector<__fp16> npu_input; - if (pack_parakeet_features_for_npu(time_major_f16, frames, num_mels, input_shape, npu_input)) { - npu_encoder_->preallocate(input_shape, "x", ""); - - std::vector output_shape = npu_encoder_->get_output_shape(); - size_t output_capacity = npu_encoder_->get_output_buffer_size(); - if (output_capacity == 0) { - output_capacity = shape_elements(output_shape); - } - if (output_capacity == 0) { - output_capacity = std::max( - 1, frames * std::max(1, static_cast(config_.hidden_dim))); - } - - std::vector<__fp16> npu_output(output_capacity); - const size_t elements_written = npu_encoder_->encode( - npu_input.data(), - npu_output.data(), - input_shape, - "x", - "" - ); - - size_t T_enc = 0; - size_t D_enc = 0; - if (elements_written > 0 && - infer_npu_encoder_output_shape(output_shape, elements_written, - static_cast(config_.hidden_dim), T_enc, D_enc)) { - const __fp16* src = npu_output.data(); - __fp16* cached_output = npu_encoder_->get_output_buffer(); - const size_t cached_count = npu_encoder_->get_output_buffer_size(); - const size_t required = T_enc * D_enc; - if (cached_output != nullptr && cached_count >= required) { - src = cached_output; - } - - // Accept encoder hidden output [T, hidden] or direct logits [T, vocab]. - if (D_enc == expected_hidden_dim || D_enc == expected_vocab_size) { - size_t enc_output = gb->input({T_enc, D_enc}, Precision::FP16); - gb->set_input(enc_output, src, Precision::FP16); - return enc_output; - } - } - } - } - - if (!has_cpu_encoder_weights_) { - throw std::runtime_error( - "Parakeet requires either CPU encoder weights or model.mlpackage encoder output."); - } - - ComputeBackend backend = ComputeBackend::CPU; - size_t hidden = build_subsampling(gb, audio_features); - const auto& hidden_shape = gb->get_output_buffer(hidden).shape; - if (hidden_shape.size() != 2) { - throw std::runtime_error("Parakeet encoder expects subsampling output [T, D]"); - } - size_t position_embeddings = build_relative_position_embeddings(gb, hidden_shape[0]); - for (uint32_t i = 0; i < config_.num_layers; ++i) { - hidden = build_encoder_block(gb, hidden, position_embeddings, i, backend); - } - return hidden; -} - -size_t ParakeetModel::build_ctc_logits(CactusGraph* gb, size_t hidden_states) { - const auto& hidden_shape = gb->get_output_buffer(hidden_states).shape; - if (hidden_shape.size() != 2) { - throw std::runtime_error("Parakeet CTC head expects hidden states [T, D]"); - } - const size_t T = hidden_shape[0]; - const size_t D = hidden_shape[1]; - - size_t hidden_nlc = gb->reshape(hidden_states, {1, T, D}); - const auto& ctc_w_shape = gb->get_output_buffer(weight_nodes_.ctc_head_weight).shape; - size_t vocab_size = ctc_w_shape.empty() ? config_.vocab_size : ctc_w_shape[0]; - size_t logits_nlc = gb->conv1d_pointwise(hidden_nlc, weight_nodes_.ctc_head_weight, weight_nodes_.ctc_head_bias); - - return gb->reshape(logits_nlc, {T, vocab_size}); -} - -size_t ParakeetModel::forward(const std::vector& audio_features, const std::vector& tokens, bool use_cache) { - (void)tokens; - (void)use_cache; - auto* gb = static_cast(graph_handle_); - gb->clear_debug_nodes(); - size_t encoder_out = build_encoder(gb, audio_features); - - const auto& enc_shape = gb->get_output_buffer(encoder_out).shape; - if (enc_shape.size() != 2) { - throw std::runtime_error("Parakeet encoder output must be rank-2 [T, D]"); - } - - size_t ctc_hidden_dim = std::max(1, static_cast(config_.hidden_dim)); - size_t ctc_vocab_size = std::max(1, static_cast(config_.vocab_size)); - const auto& ctc_w_shape = gb->get_output_buffer(weight_nodes_.ctc_head_weight).shape; - if (ctc_w_shape.size() == 2) { - ctc_vocab_size = ctc_w_shape[0]; - ctc_hidden_dim = ctc_w_shape[1]; - } else if (ctc_w_shape.size() == 3) { - ctc_vocab_size = ctc_w_shape[0]; - ctc_hidden_dim = ctc_w_shape[1]; - } - - // If the NPU graph already emits [T, vocab] logits, skip CPU CTC head. - if (enc_shape[1] == ctc_vocab_size && ctc_vocab_size != ctc_hidden_dim) { - return encoder_out; - } - - if (enc_shape[1] != ctc_hidden_dim) { - throw std::runtime_error( - "Parakeet encoder output dim mismatch: expected hidden dim " + - std::to_string(ctc_hidden_dim) + ", got " + std::to_string(enc_shape[1])); - } - - return build_ctc_logits(gb, encoder_out); -} - -std::vector ParakeetModel::greedy_decode_tokens(CactusGraph* gb, size_t logits_node) const { - const auto& logits_buf = gb->get_output_buffer(logits_node); - if (logits_buf.shape.size() != 2) { - throw std::runtime_error("Parakeet logits must be rank-2 [T, vocab]"); - } - - const size_t T = logits_buf.shape[0]; - const size_t vocab_size = logits_buf.shape[1]; - std::vector frame_ids(T, 0); - - if (logits_buf.precision == Precision::FP32) { - const float* src = logits_buf.data_as(); - for (size_t t = 0; t < T; ++t) { - const float* row = src + t * vocab_size; - size_t best_idx = 0; - float best_val = row[0]; - for (size_t v = 1; v < vocab_size; ++v) { - if (row[v] > best_val) { - best_val = row[v]; - best_idx = v; - } - } - frame_ids[t] = static_cast(best_idx); - } - } else if (logits_buf.precision == Precision::FP16) { - const __fp16* src = logits_buf.data_as<__fp16>(); - for (size_t t = 0; t < T; ++t) { - const __fp16* row = src + t * vocab_size; - size_t best_idx = 0; - float best_val = static_cast(row[0]); - for (size_t v = 1; v < vocab_size; ++v) { - const float val = static_cast(row[v]); - if (val > best_val) { - best_val = val; - best_idx = v; - } - } - frame_ids[t] = static_cast(best_idx); - } - } else { - const int8_t* src = logits_buf.data_as(); - for (size_t t = 0; t < T; ++t) { - const int8_t* row = src + t * vocab_size; - size_t best_idx = 0; - int best_val = static_cast(row[0]); - for (size_t v = 1; v < vocab_size; ++v) { - const int val = static_cast(row[v]); - if (val > best_val) { - best_val = val; - best_idx = v; - } - } - frame_ids[t] = static_cast(best_idx); - } - } - - const uint32_t blank_id = config_.pad_token_id > 0 ? config_.pad_token_id : static_cast(vocab_size - 1); - std::vector decoded; - decoded.reserve(frame_ids.size()); - - uint32_t prev = blank_id; - for (uint32_t id : frame_ids) { - if (id == blank_id) { - prev = blank_id; - continue; - } - if (id == prev) { - continue; - } - decoded.push_back(id); - prev = id; - } - - return decoded; -} - -uint32_t ParakeetModel::decode_with_audio( - const std::vector& tokens, - const std::vector& audio_features, - float temperature, - float top_p, - size_t top_k, - const std::string& profile_file, - float* out_entropy, - float min_p, - float repetition_penalty, - float* /*out_token_time_start*/, - float* /*out_token_time_end*/) -{ - (void)temperature; - (void)top_p; - (void)top_k; - (void)min_p; - (void)repetition_penalty; - - if (!initialized_ || !graph_handle_) { - throw std::runtime_error("Model not initialized - call init() first"); - } - if (audio_features.empty()) { - throw std::runtime_error("Audio features cannot be empty in Parakeet decode_with_audio"); - } - - auto* gb = static_cast(graph_handle_); - const bool new_request = !ctc_tokens_ready_ || tokens.empty() || tokens.size() < last_input_token_count_; - if (new_request) { - gb->soft_reset(); - size_t logits_node = forward(audio_features, tokens, false); - - if (!profile_file.empty()) { - gb->execute(profile_file); - } else { - gb->execute(); - } - - ctc_tokens_ = greedy_decode_tokens(gb, logits_node); - ctc_emit_index_ = 0; - ctc_tokens_ready_ = true; - } - - last_input_token_count_ = tokens.size(); - if (out_entropy) { - *out_entropy = 0.0f; - } - - if (ctc_emit_index_ < ctc_tokens_.size()) { - return ctc_tokens_[ctc_emit_index_++]; - } - return get_tokenizer()->get_eos_token(); -} - -std::vector ParakeetModel::get_audio_embeddings(const std::vector& audio_features) { - auto* gb = static_cast(graph_handle_); - gb->soft_reset(); - size_t hidden = build_encoder(gb, audio_features); - - const auto& hidden_shape = gb->get_output_buffer(hidden).shape; - size_t ctc_hidden_dim = std::max(1, static_cast(config_.hidden_dim)); - size_t ctc_vocab_size = std::max(1, static_cast(config_.vocab_size)); - const auto& ctc_w_shape = gb->get_output_buffer(weight_nodes_.ctc_head_weight).shape; - if (ctc_w_shape.size() == 2) { - ctc_vocab_size = ctc_w_shape[0]; - ctc_hidden_dim = ctc_w_shape[1]; - } else if (ctc_w_shape.size() == 3) { - ctc_vocab_size = ctc_w_shape[0]; - ctc_hidden_dim = ctc_w_shape[1]; - } - - // Embeddings require hidden states; if NPU returned logits, rerun CPU encoder. - if (hidden_shape.size() == 2 && - hidden_shape[1] == ctc_vocab_size && - ctc_vocab_size != ctc_hidden_dim && - use_npu_encoder_) { - if (!has_cpu_encoder_weights_) { - throw std::runtime_error( - "Parakeet audio embeddings require hidden-state encoder output; " - "CPU encoder fallback weights are not available."); - } - const bool prev_use_npu = use_npu_encoder_; - use_npu_encoder_ = false; - gb->soft_reset(); - hidden = build_encoder(gb, audio_features); - use_npu_encoder_ = prev_use_npu; - } - - size_t pooled = gb->mean(hidden, 0); - gb->execute(); - - const auto& output_buf = gb->get_output_buffer(pooled); - const size_t hidden_dim = output_buf.total_size; - std::vector embedding(hidden_dim); - - if (output_buf.precision == Precision::FP32) { - const float* src = output_buf.data_as(); - std::copy(src, src + hidden_dim, embedding.begin()); - } else if (output_buf.precision == Precision::FP16) { - const __fp16* src = output_buf.data_as<__fp16>(); - Quantization::fp16_to_fp32(src, embedding.data(), hidden_dim); - } else { - const int8_t* src = output_buf.data_as(); - Quantization::int8_to_fp32(src, embedding.data(), hidden_dim, 1.0f); - } - - reset_cache(); - return embedding; -} - -void ParakeetModel::reset_cache() { - Model::reset_cache(); - ctc_tokens_ready_ = false; - ctc_emit_index_ = 0; - ctc_tokens_.clear(); - last_input_token_count_ = 0; -} - -} // namespace engine -} // namespace cactus \ No newline at end of file diff --git a/cactus/models/model_parakeet_tdt.cpp b/cactus/models/model_parakeet_tdt.cpp deleted file mode 100644 index da0e827d4..000000000 --- a/cactus/models/model_parakeet_tdt.cpp +++ /dev/null @@ -1,1107 +0,0 @@ -#include "model.h" -#include "../graph/graph.h" -#include "../npu/npu.h" -#include "../kernel/kernel.h" -#include "../telemetry/telemetry.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -size_t shape_elements(const std::vector& shape); -bool pack_parakeet_features_for_npu( - const std::vector<__fp16>& time_major_f16, - size_t frames, - size_t num_mels, - const std::vector& input_shape, - std::vector<__fp16>& packed); - -namespace { - -constexpr uint32_t kMaxStreamDurationSkipFrames = 2; - -bool infer_npu_encoder_output_shape( - const std::vector& output_shape, - size_t elements_written, - size_t fallback_hidden_dim, - size_t& time_steps, - size_t& hidden_dim) -{ - if (elements_written == 0) return false; - - std::vector dims; - dims.reserve(output_shape.size()); - for (int d : output_shape) { - if (d > 0) dims.push_back(static_cast(d)); - } - - if (dims.size() >= 2) { - time_steps = dims[dims.size() - 2]; - hidden_dim = dims[dims.size() - 1]; - } else if (dims.size() == 1) { - hidden_dim = dims[0]; - if (hidden_dim == 0 || (elements_written % hidden_dim) != 0) return false; - time_steps = elements_written / hidden_dim; - } else { - hidden_dim = fallback_hidden_dim; - if (hidden_dim == 0 || (elements_written % hidden_dim) != 0) return false; - time_steps = elements_written / hidden_dim; - } - - if (time_steps == 0 || hidden_dim == 0) return false; - if (time_steps * hidden_dim > elements_written) return false; - return true; -} - -std::vector<__fp16> copy_buffer_to_fp16(const BufferDesc& buffer) { - std::vector<__fp16> out(buffer.total_size, static_cast<__fp16>(0.0f)); - if (buffer.total_size == 0) { - return out; - } - - if (buffer.precision == Precision::FP16) { - const __fp16* src = buffer.data_as<__fp16>(); - std::copy(src, src + buffer.total_size, out.begin()); - return out; - } - - if (buffer.precision == Precision::FP32) { - const float* src = buffer.data_as(); - cactus_fp32_to_fp16(src, out.data(), buffer.total_size); - return out; - } - - if (buffer.precision == Precision::INT8 && !buffer.is_interleaved) { - const int8_t* src = buffer.data_as(); - Quantization::int8_to_fp16(src, out.data(), buffer.total_size, 1.0f); - return out; - } - - throw std::runtime_error("Unsupported precision for FP16 conversion in ParakeetTDT"); -} - -size_t argmax_range(const BufferDesc& buffer, size_t offset, size_t length) { - if (length == 0 || (offset + length) > buffer.total_size) { - throw std::runtime_error("Invalid argmax range"); - } - - size_t best_idx = 0; - float best_val = -std::numeric_limits::infinity(); - - if (buffer.precision == Precision::FP16) { - const __fp16* src = buffer.data_as<__fp16>() + offset; - best_val = static_cast(src[0]); - for (size_t i = 1; i < length; ++i) { - float v = static_cast(src[i]); - if (v > best_val) { - best_val = v; - best_idx = i; - } - } - return best_idx; - } - - if (buffer.precision == Precision::FP32) { - const float* src = buffer.data_as() + offset; - best_val = src[0]; - for (size_t i = 1; i < length; ++i) { - float v = src[i]; - if (v > best_val) { - best_val = v; - best_idx = i; - } - } - return best_idx; - } - - if (buffer.precision == Precision::INT8) { - const int8_t* src = buffer.data_as() + offset; - best_val = static_cast(src[0]); - for (size_t i = 1; i < length; ++i) { - float v = static_cast(src[i]); - if (v > best_val) { - best_val = v; - best_idx = i; - } - } - return best_idx; - } - - throw std::runtime_error("Unsupported logits precision in argmax"); -} - -size_t argmax_range_with_bias(const BufferDesc& buffer, size_t offset, size_t length, - const std::unordered_map& bias) { - if (bias.empty()) { - return argmax_range(buffer, offset, length); - } - if (length == 0 || (offset + length) > buffer.total_size) { - throw std::runtime_error("Invalid argmax range"); - } - - size_t best_idx = 0; - float best_val = -std::numeric_limits::infinity(); - - if (buffer.precision == Precision::FP16) { - const __fp16* src = buffer.data_as<__fp16>() + offset; - for (size_t i = 0; i < length; ++i) { - float v = static_cast(src[i]); - auto it = bias.find(static_cast(i)); - if (it != bias.end()) v += it->second; - if (v > best_val) { best_val = v; best_idx = i; } - } - return best_idx; - } - - if (buffer.precision == Precision::FP32) { - const float* src = buffer.data_as() + offset; - for (size_t i = 0; i < length; ++i) { - float v = src[i]; - auto it = bias.find(static_cast(i)); - if (it != bias.end()) v += it->second; - if (v > best_val) { best_val = v; best_idx = i; } - } - return best_idx; - } - - if (buffer.precision == Precision::INT8) { - const int8_t* src = buffer.data_as() + offset; - for (size_t i = 0; i < length; ++i) { - float v = static_cast(src[i]); - auto it = bias.find(static_cast(i)); - if (it != bias.end()) v += it->second; - if (v > best_val) { best_val = v; best_idx = i; } - } - return best_idx; - } - - throw std::runtime_error("Unsupported logits precision in biased argmax"); -} - -} // namespace - -namespace cactus { -namespace engine { - -ParakeetTDTModel::ParakeetTDTModel() : Model() {} - -ParakeetTDTModel::ParakeetTDTModel(const Config& config) : Model(config) { - weight_nodes_.layers.resize(config.num_layers); - - float hd = static_cast(config.attention_head_dim); - if (hd <= 0.0f) { - hd = 64.0f; - } - attention_scale_ = 1.0f / std::sqrt(hd); - - if (std::fabs(config_.rope_theta - 10000.0f) < 1e-3f || - std::fabs(config_.rope_theta - 1000000.0f) < 1e-3f) { - config_.rope_theta = 0.0f; - } -} - -void ParakeetTDTModel::load_weights_to_graph(CactusGraph* gb) { - weight_nodes_.subsampling_conv0_weight = gb->mmap_weights(model_folder_path_ + "/subsampling_conv0_weight.weights"); - weight_nodes_.subsampling_conv0_bias = gb->mmap_weights(model_folder_path_ + "/subsampling_conv0_bias.bias"); - weight_nodes_.subsampling_depthwise1_weight = gb->mmap_weights(model_folder_path_ + "/subsampling_depthwise1_weight.weights"); - weight_nodes_.subsampling_depthwise1_bias = gb->mmap_weights(model_folder_path_ + "/subsampling_depthwise1_bias.bias"); - weight_nodes_.subsampling_pointwise1_weight = gb->mmap_weights(model_folder_path_ + "/subsampling_pointwise1_weight.weights"); - weight_nodes_.subsampling_pointwise1_bias = gb->mmap_weights(model_folder_path_ + "/subsampling_pointwise1_bias.bias"); - weight_nodes_.subsampling_depthwise2_weight = gb->mmap_weights(model_folder_path_ + "/subsampling_depthwise2_weight.weights"); - weight_nodes_.subsampling_depthwise2_bias = gb->mmap_weights(model_folder_path_ + "/subsampling_depthwise2_bias.bias"); - weight_nodes_.subsampling_pointwise2_weight = gb->mmap_weights(model_folder_path_ + "/subsampling_pointwise2_weight.weights"); - weight_nodes_.subsampling_pointwise2_bias = gb->mmap_weights(model_folder_path_ + "/subsampling_pointwise2_bias.bias"); - weight_nodes_.subsampling_linear_weight = gb->mmap_weights(model_folder_path_ + "/subsampling_linear_weight.weights"); - weight_nodes_.subsampling_linear_bias = gb->mmap_weights(model_folder_path_ + "/subsampling_linear_bias.bias"); - - weight_nodes_.predictor_embed = gb->mmap_weights(model_folder_path_ + "/tdt_predictor_embed.weights"); - - size_t predictor_layers = static_cast(config_.predictor_num_layers); - if (predictor_layers == 0) { - while (true) { - const std::string prefix = model_folder_path_ + "/tdt_predictor_lstm_" + std::to_string(predictor_layers); - if (!std::filesystem::exists(prefix + "_weight_ih.weights") || - !std::filesystem::exists(prefix + "_weight_hh.weights") || - !std::filesystem::exists(prefix + "_bias.weights")) { - break; - } - ++predictor_layers; - } - } - if (predictor_layers == 0) { - predictor_layers = 1; - } - - weight_nodes_.predictor_layers.resize(predictor_layers); - for (size_t i = 0; i < predictor_layers; ++i) { - auto& layer = weight_nodes_.predictor_layers[i]; - const std::string prefix = model_folder_path_ + "/tdt_predictor_lstm_" + std::to_string(i); - layer.weight_ih = gb->mmap_weights(prefix + "_weight_ih.weights"); - layer.weight_hh = gb->mmap_weights(prefix + "_weight_hh.weights"); - layer.bias = gb->mmap_weights(prefix + "_bias.weights"); - } - - weight_nodes_.joint_enc_weight = gb->mmap_weights(model_folder_path_ + "/tdt_joint_enc.weights"); - weight_nodes_.joint_enc_bias = gb->mmap_weights(model_folder_path_ + "/tdt_joint_enc.bias"); - weight_nodes_.joint_pred_weight = gb->mmap_weights(model_folder_path_ + "/tdt_joint_pred.weights"); - weight_nodes_.joint_pred_bias = gb->mmap_weights(model_folder_path_ + "/tdt_joint_pred.bias"); - weight_nodes_.joint_out_weight = gb->mmap_weights(model_folder_path_ + "/tdt_joint_out.weights"); - weight_nodes_.joint_out_bias = gb->mmap_weights(model_folder_path_ + "/tdt_joint_out.bias"); - - if (npu::is_npu_available()) { - std::string npu_encoder_path = model_folder_path_ + "/model.mlpackage"; - npu_encoder_ = npu::create_encoder(); - if (npu_encoder_ && npu_encoder_->load(npu_encoder_path)) { - use_npu_encoder_ = true; - std::vector input_shape = npu_encoder_->get_input_shape(); - if (!input_shape.empty()) { - npu_encoder_->preallocate(input_shape, "x", ""); - } - } else { - use_npu_encoder_ = false; - npu_encoder_.reset(); - } - } - - const std::filesystem::path model_path(model_folder_path_); - has_cpu_encoder_weights_ = - std::filesystem::exists(model_path / "subsampling_conv0_weight.weights") && - std::filesystem::exists(model_path / "subsampling_linear_weight.weights"); - - if (has_cpu_encoder_weights_) { - weight_nodes_.subsampling_conv0_weight = gb->mmap_weights(model_folder_path_ + "/subsampling_conv0_weight.weights"); - weight_nodes_.subsampling_conv0_bias = gb->mmap_weights(model_folder_path_ + "/subsampling_conv0_bias.bias"); - weight_nodes_.subsampling_depthwise1_weight = gb->mmap_weights(model_folder_path_ + "/subsampling_depthwise1_weight.weights"); - weight_nodes_.subsampling_depthwise1_bias = gb->mmap_weights(model_folder_path_ + "/subsampling_depthwise1_bias.bias"); - weight_nodes_.subsampling_pointwise1_weight = gb->mmap_weights(model_folder_path_ + "/subsampling_pointwise1_weight.weights"); - weight_nodes_.subsampling_pointwise1_bias = gb->mmap_weights(model_folder_path_ + "/subsampling_pointwise1_bias.bias"); - weight_nodes_.subsampling_depthwise2_weight = gb->mmap_weights(model_folder_path_ + "/subsampling_depthwise2_weight.weights"); - weight_nodes_.subsampling_depthwise2_bias = gb->mmap_weights(model_folder_path_ + "/subsampling_depthwise2_bias.bias"); - weight_nodes_.subsampling_pointwise2_weight = gb->mmap_weights(model_folder_path_ + "/subsampling_pointwise2_weight.weights"); - weight_nodes_.subsampling_pointwise2_bias = gb->mmap_weights(model_folder_path_ + "/subsampling_pointwise2_bias.bias"); - weight_nodes_.subsampling_linear_weight = gb->mmap_weights(model_folder_path_ + "/subsampling_linear_weight.weights"); - weight_nodes_.subsampling_linear_bias = gb->mmap_weights(model_folder_path_ + "/subsampling_linear_bias.bias"); - } - - for (uint32_t i = 0; i < config_.num_layers; ++i) { - auto& layer = weight_nodes_.layers[i]; - std::string layer_prefix = model_folder_path_ + "/layer_" + std::to_string(i) + "_"; - - layer.ff1_linear1_weight = gb->mmap_weights(layer_prefix + "ff1_linear1.weights"); - layer.ff1_linear1_bias = gb->mmap_weights(layer_prefix + "ff1_linear1.bias"); - layer.ff1_linear2_weight = gb->mmap_weights(layer_prefix + "ff1_linear2.weights"); - layer.ff1_linear2_bias = gb->mmap_weights(layer_prefix + "ff1_linear2.bias"); - - layer.ff2_linear1_weight = gb->mmap_weights(layer_prefix + "ff2_linear1.weights"); - layer.ff2_linear1_bias = gb->mmap_weights(layer_prefix + "ff2_linear1.bias"); - layer.ff2_linear2_weight = gb->mmap_weights(layer_prefix + "ff2_linear2.weights"); - layer.ff2_linear2_bias = gb->mmap_weights(layer_prefix + "ff2_linear2.bias"); - - layer.self_attn_q_weight = gb->mmap_weights(layer_prefix + "self_attn_q.weights"); - layer.self_attn_q_bias = gb->mmap_weights(layer_prefix + "self_attn_q.bias"); - layer.self_attn_k_weight = gb->mmap_weights(layer_prefix + "self_attn_k.weights"); - layer.self_attn_k_bias = gb->mmap_weights(layer_prefix + "self_attn_k.bias"); - layer.self_attn_v_weight = gb->mmap_weights(layer_prefix + "self_attn_v.weights"); - layer.self_attn_v_bias = gb->mmap_weights(layer_prefix + "self_attn_v.bias"); - layer.self_attn_output_weight = gb->mmap_weights(layer_prefix + "self_attn_output.weights"); - layer.self_attn_output_bias = gb->mmap_weights(layer_prefix + "self_attn_output.bias"); - layer.self_attn_relative_k_weight = gb->mmap_weights(layer_prefix + "self_attn_relative_k.weights"); - layer.self_attn_bias_u = gb->mmap_weights(layer_prefix + "self_attn_bias_u.weights"); - layer.self_attn_bias_v = gb->mmap_weights(layer_prefix + "self_attn_bias_v.weights"); - - layer.norm_ff1_weight = gb->mmap_weights(layer_prefix + "norm_ff1.weights"); - layer.norm_ff1_bias = gb->mmap_weights(layer_prefix + "norm_ff1.bias"); - layer.norm_self_attn_weight = gb->mmap_weights(layer_prefix + "norm_self_attn.weights"); - layer.norm_self_attn_bias = gb->mmap_weights(layer_prefix + "norm_self_attn.bias"); - layer.norm_conv_weight = gb->mmap_weights(layer_prefix + "norm_conv.weights"); - layer.norm_conv_bias = gb->mmap_weights(layer_prefix + "norm_conv.bias"); - layer.norm_ff2_weight = gb->mmap_weights(layer_prefix + "norm_ff2.weights"); - layer.norm_ff2_bias = gb->mmap_weights(layer_prefix + "norm_ff2.bias"); - layer.norm_out_weight = gb->mmap_weights(layer_prefix + "norm_out.weights"); - layer.norm_out_bias = gb->mmap_weights(layer_prefix + "norm_out.bias"); - - layer.conv_pointwise1_weight = gb->mmap_weights(layer_prefix + "conv_pointwise1.weights"); - layer.conv_pointwise1_bias = gb->mmap_weights(layer_prefix + "conv_pointwise1.bias"); - layer.conv_depthwise_weight = gb->mmap_weights(layer_prefix + "conv_depthwise.weights"); - layer.conv_depthwise_bias = gb->mmap_weights(layer_prefix + "conv_depthwise.bias"); - layer.conv_pointwise2_weight = gb->mmap_weights(layer_prefix + "conv_pointwise2.weights"); - layer.conv_pointwise2_bias = gb->mmap_weights(layer_prefix + "conv_pointwise2.bias"); - layer.conv_batchnorm_weight = gb->mmap_weights(layer_prefix + "conv_batchnorm_weight.weights"); - layer.conv_batchnorm_bias = gb->mmap_weights(layer_prefix + "conv_batchnorm_bias.bias"); - layer.conv_batchnorm_running_mean = gb->mmap_weights(layer_prefix + "conv_batchnorm_running_mean.weights"); - layer.conv_batchnorm_running_var = gb->mmap_weights(layer_prefix + "conv_batchnorm_running_var.weights"); - } -} - -size_t ParakeetTDTModel::build_subsampling(CactusGraph* gb, const std::vector& audio_features) { - const size_t num_mels = std::max(1, static_cast(config_.num_mel_bins)); - if (audio_features.empty() || (audio_features.size() % num_mels) != 0) { - throw std::runtime_error("ParakeetTDT expects audio_features with shape [num_mels, num_frames]"); - } - - const size_t frames = audio_features.size() / num_mels; - std::vector time_major(frames * num_mels); - for (size_t m = 0; m < num_mels; ++m) { - const float* src = &audio_features[m * frames]; - for (size_t t = 0; t < frames; ++t) { - time_major[t * num_mels + m] = src[t]; - } - } - - std::vector<__fp16> features_f16(time_major.size()); - cactus_fp32_to_fp16(time_major.data(), features_f16.data(), time_major.size()); - - size_t x = gb->input({1, 1, frames, num_mels}, Precision::FP16); - gb->set_input(x, features_f16.data(), Precision::FP16); - - x = gb->conv2d_k3s2p1(x, weight_nodes_.subsampling_conv0_weight, weight_nodes_.subsampling_conv0_bias); - x = gb->relu(x); - - x = gb->conv2d_depthwise_k3s2p1(x, weight_nodes_.subsampling_depthwise1_weight, weight_nodes_.subsampling_depthwise1_bias); - x = gb->conv2d_pointwise_1x1(x, weight_nodes_.subsampling_pointwise1_weight, weight_nodes_.subsampling_pointwise1_bias); - x = gb->relu(x); - - x = gb->conv2d_depthwise_k3s2p1(x, weight_nodes_.subsampling_depthwise2_weight, weight_nodes_.subsampling_depthwise2_bias); - x = gb->conv2d_pointwise_1x1(x, weight_nodes_.subsampling_pointwise2_weight, weight_nodes_.subsampling_pointwise2_bias); - x = gb->relu(x); - - const auto& conv_shape = gb->get_output_buffer(x).shape; - if (conv_shape.size() != 4 || conv_shape[0] != 1) { - throw std::runtime_error("ParakeetTDT subsampling produced invalid shape"); - } - - const size_t C = conv_shape[1]; - const size_t T = conv_shape[2]; - const size_t W = conv_shape[3]; - - size_t t_major = gb->transposeN(x, {0, 2, 1, 3}, ComputeBackend::CPU); - size_t flattened = gb->reshape(t_major, {T, C * W}); - size_t projected = gb->matmul(flattened, weight_nodes_.subsampling_linear_weight, true, ComputeBackend::CPU); - projected = gb->add(projected, weight_nodes_.subsampling_linear_bias); - return projected; -} - -size_t ParakeetTDTModel::build_relative_position_embeddings(CactusGraph* gb, size_t seq_len) { - const size_t hidden_dim = std::max(1, static_cast(config_.hidden_dim)); - const size_t half_dim = hidden_dim / 2; - const size_t rel_len = 2 * seq_len - 1; - - std::vector pos_embed(rel_len * hidden_dim, 0.0f); - for (size_t p = 0; p < rel_len; ++p) { - const int rel_pos = static_cast(seq_len - 1) - static_cast(p); - for (size_t i = 0; i < half_dim; ++i) { - const float exponent = static_cast(2 * i) / static_cast(hidden_dim); - const float inv_freq = 1.0f / std::pow(10000.0f, exponent); - const float angle = static_cast(rel_pos) * inv_freq; - pos_embed[p * hidden_dim + 2 * i] = std::sin(angle); - if (2 * i + 1 < hidden_dim) { - pos_embed[p * hidden_dim + 2 * i + 1] = std::cos(angle); - } - } - } - - std::vector<__fp16> pos_embed_f16(pos_embed.size()); - cactus_fp32_to_fp16(pos_embed.data(), pos_embed_f16.data(), pos_embed.size()); - - size_t pos_node = gb->input({rel_len, hidden_dim}, Precision::FP16); - gb->set_input(pos_node, pos_embed_f16.data(), Precision::FP16); - return pos_node; -} - -size_t ParakeetTDTModel::build_self_attention(CactusGraph* gb, size_t hidden, size_t position_embeddings, - uint32_t layer_idx, ComputeBackend backend) { - const auto& layer = weight_nodes_.layers[layer_idx]; - - size_t q = gb->matmul(hidden, layer.self_attn_q_weight, true, backend); - q = gb->add(q, layer.self_attn_q_bias); - size_t k = gb->matmul(hidden, layer.self_attn_k_weight, true, backend); - k = gb->add(k, layer.self_attn_k_bias); - size_t v = gb->matmul(hidden, layer.self_attn_v_weight, true, backend); - v = gb->add(v, layer.self_attn_v_bias); - - const auto& q_shape = gb->get_output_buffer(q).shape; - if (q_shape.size() != 2) { - throw std::runtime_error("ParakeetTDT self-attention expects [T, D]"); - } - - const size_t T = q_shape[0]; - const size_t q_heads = std::max(1, static_cast(config_.attention_heads)); - const size_t kv_heads = std::max(1, static_cast(config_.attention_kv_heads)); - const size_t head_dim = std::max(1, static_cast(config_.attention_head_dim)); - - size_t q4 = gb->reshape(q, {1, T, q_heads, head_dim}); - size_t k4 = gb->reshape(k, {1, T, kv_heads, head_dim}); - size_t v4 = gb->reshape(v, {1, T, kv_heads, head_dim}); - - size_t bias_u = gb->reshape(layer.self_attn_bias_u, {1, static_cast(1), q_heads, head_dim}); - size_t bias_v = gb->reshape(layer.self_attn_bias_v, {1, static_cast(1), q_heads, head_dim}); - size_t q_u4 = gb->add(q4, bias_u); - size_t q_v4 = gb->add(q4, bias_v); - - size_t rel_k_flat = gb->matmul(position_embeddings, layer.self_attn_relative_k_weight, true, backend); - size_t rel_k4 = gb->reshape(rel_k_flat, {1, 2 * T - 1, q_heads, head_dim}); - size_t rel_bias = gb->rel_pos_bias(q_v4, rel_k4, attention_scale_); - - size_t attn = gb->attention_masked(q_u4, k4, v4, rel_bias, attention_scale_, false, backend, true); - attn = gb->reshape(attn, {T, q_heads * head_dim}); - - size_t out = gb->matmul(attn, layer.self_attn_output_weight, true, backend); - out = gb->add(out, layer.self_attn_output_bias); - return out; -} - -size_t ParakeetTDTModel::build_feed_forward(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - bool second_ff, ComputeBackend backend) { - const auto& layer = weight_nodes_.layers[layer_idx]; - - size_t w1 = second_ff ? layer.ff2_linear1_weight : layer.ff1_linear1_weight; - size_t b1 = second_ff ? layer.ff2_linear1_bias : layer.ff1_linear1_bias; - size_t w2 = second_ff ? layer.ff2_linear2_weight : layer.ff1_linear2_weight; - size_t b2 = second_ff ? layer.ff2_linear2_bias : layer.ff1_linear2_bias; - - size_t x = gb->matmul(hidden, w1, true, backend); - x = gb->add(x, b1); - - std::string act = config_.encoder_hidden_act; - std::transform(act.begin(), act.end(), act.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); - if (act.find("gelu") != std::string::npos) { - x = gb->gelu(x); - } else if (act == "relu") { - x = gb->relu(x); - } else { - x = gb->silu(x); - } - - x = gb->matmul(x, w2, true, backend); - x = gb->add(x, b2); - return x; -} - -size_t ParakeetTDTModel::build_convolution_module(CactusGraph* gb, size_t hidden, uint32_t layer_idx, ComputeBackend backend) { - (void)backend; - const auto& layer = weight_nodes_.layers[layer_idx]; - const auto& hidden_shape = gb->get_output_buffer(hidden).shape; - if (hidden_shape.size() != 2) { - throw std::runtime_error("ParakeetTDT convolution module expects [T, D]"); - } - - const size_t T = hidden_shape[0]; - const size_t D = hidden_shape[1]; - - size_t x = gb->reshape(hidden, {1, T, D}); - x = gb->conv1d_pointwise(x, layer.conv_pointwise1_weight, layer.conv_pointwise1_bias); - x = gb->glu(x, -1); - - x = gb->conv1d_same_depthwise_k9(x, layer.conv_depthwise_weight, layer.conv_depthwise_bias); - - x = gb->batchnorm( - x, - layer.conv_batchnorm_weight, - layer.conv_batchnorm_bias, - layer.conv_batchnorm_running_mean, - layer.conv_batchnorm_running_var, - 2, - 1e-5f - ); - - std::string act = config_.encoder_hidden_act; - std::transform(act.begin(), act.end(), act.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); - if (act.find("gelu") != std::string::npos) { - x = gb->gelu(x); - } else if (act == "relu") { - x = gb->relu(x); - } else { - x = gb->silu(x); - } - - x = gb->conv1d_pointwise(x, layer.conv_pointwise2_weight, layer.conv_pointwise2_bias); - - x = gb->reshape(x, {T, D}); - return x; -} - -size_t ParakeetTDTModel::build_encoder_block(CactusGraph* gb, size_t hidden, size_t position_embeddings, - uint32_t layer_idx, ComputeBackend backend) { - const auto& layer = weight_nodes_.layers[layer_idx]; - - size_t ff1_in = gb->layernorm(hidden, layer.norm_ff1_weight, layer.norm_ff1_bias); - size_t ff1 = build_feed_forward(gb, ff1_in, layer_idx, false, backend); - ff1 = gb->scalar_multiply(ff1, 0.5f); - size_t x = gb->add(hidden, ff1); - - size_t attn_in = gb->layernorm(x, layer.norm_self_attn_weight, layer.norm_self_attn_bias); - size_t attn = build_self_attention(gb, attn_in, position_embeddings, layer_idx, backend); - x = gb->add(x, attn); - - size_t conv_in = gb->layernorm(x, layer.norm_conv_weight, layer.norm_conv_bias); - size_t conv = build_convolution_module(gb, conv_in, layer_idx, backend); - x = gb->add(x, conv); - - size_t ff2_in = gb->layernorm(x, layer.norm_ff2_weight, layer.norm_ff2_bias); - size_t ff2 = build_feed_forward(gb, ff2_in, layer_idx, true, backend); - ff2 = gb->scalar_multiply(ff2, 0.5f); - x = gb->add(x, ff2); - - x = gb->layernorm(x, layer.norm_out_weight, layer.norm_out_bias); - return x; -} - -size_t ParakeetTDTModel::build_encoder(CactusGraph* gb, const std::vector& audio_features) { - const size_t num_mels = std::max(1, static_cast(config_.num_mel_bins)); - if (audio_features.empty() || (audio_features.size() % num_mels) != 0) { - throw std::runtime_error("ParakeetTDT expects audio_features with shape [num_mels, num_frames]"); - } - - const size_t frames = audio_features.size() / num_mels; - - size_t expected_hidden_dim = std::max(1, static_cast(config_.hidden_dim)); - { - const auto& joint_enc_shape = gb->get_output_buffer(weight_nodes_.joint_enc_weight).shape; - if (joint_enc_shape.size() == 2) { - expected_hidden_dim = joint_enc_shape[1]; - } - } - - if (use_npu_encoder_ && npu_encoder_ && npu_encoder_->is_available()) { - std::vector time_major(frames * num_mels); - for (size_t m = 0; m < num_mels; ++m) { - const float* src = &audio_features[m * frames]; - for (size_t t = 0; t < frames; ++t) { - time_major[t * num_mels + m] = src[t]; - } - } - - std::vector<__fp16> time_major_f16(time_major.size()); - cactus_fp32_to_fp16(time_major.data(), time_major_f16.data(), time_major.size()); - - std::vector input_shape = npu_encoder_->get_input_shape(); - std::vector<__fp16> npu_input; - if (pack_parakeet_features_for_npu(time_major_f16, frames, num_mels, input_shape, npu_input)) { - npu_encoder_->preallocate(input_shape, "x", ""); - - std::vector output_shape = npu_encoder_->get_output_shape(); - size_t output_capacity = npu_encoder_->get_output_buffer_size(); - if (output_capacity == 0) { - output_capacity = shape_elements(output_shape); - } - if (output_capacity == 0) { - output_capacity = std::max(1, frames * expected_hidden_dim); - } - - std::vector<__fp16> npu_output(output_capacity); - const size_t elements_written = npu_encoder_->encode( - npu_input.data(), - npu_output.data(), - input_shape, - "x", - "" - ); - - size_t T_enc = 0; - size_t D_enc = 0; - if (elements_written > 0 && - infer_npu_encoder_output_shape(output_shape, elements_written, expected_hidden_dim, T_enc, D_enc)) { - const __fp16* src = npu_output.data(); - __fp16* cached_output = npu_encoder_->get_output_buffer(); - const size_t cached_count = npu_encoder_->get_output_buffer_size(); - const size_t required = T_enc * D_enc; - if (cached_output != nullptr && cached_count >= required) { - src = cached_output; - } - - if (D_enc == expected_hidden_dim) { - size_t enc_output = gb->input({T_enc, D_enc}, Precision::FP16); - gb->set_input(enc_output, src, Precision::FP16); - return enc_output; - } - } - } - } - - if (!has_cpu_encoder_weights_) { - throw std::runtime_error( - "Parakeet-TDT requires either CPU encoder weights or model.mlpackage encoder output."); - } - - ComputeBackend backend = ComputeBackend::CPU; - size_t hidden = build_subsampling(gb, audio_features); - const auto& hidden_shape = gb->get_output_buffer(hidden).shape; - if (hidden_shape.size() != 2) { - throw std::runtime_error("ParakeetTDT encoder expects subsampling output [T, D]"); - } - - size_t position_embeddings = build_relative_position_embeddings(gb, hidden_shape[0]); - for (uint32_t i = 0; i < config_.num_layers; ++i) { - hidden = build_encoder_block(gb, hidden, position_embeddings, i, backend); - } - - return hidden; -} - -size_t ParakeetTDTModel::forward(const std::vector& audio_features, - const std::vector& tokens, - bool use_cache) { - (void)tokens; - (void)use_cache; - - auto* gb = static_cast(graph_handle_); - gb->clear_debug_nodes(); - return build_encoder(gb, audio_features); -} - -std::vector ParakeetTDTModel::decode_tdt_tokens_with_state( - CactusGraph* gb, - size_t encoder_hidden_node, - size_t replay_start_frame, - size_t start_frame, - size_t end_frame, - ChunkStreamState* stream_state, - size_t* out_confirmed_count, - double* out_raw_decoder_time_ms) const { - const auto& enc_buf = gb->get_output_buffer(encoder_hidden_node); - if (enc_buf.shape.size() != 2) { - throw std::runtime_error("ParakeetTDT encoder output must be rank-2 [T, D]"); - } - - const size_t T = enc_buf.shape[0]; - const size_t D = enc_buf.shape[1]; - if (T == 0 || D == 0) { - return {}; - } - - std::vector<__fp16> encoder_fp16 = copy_buffer_to_fp16(enc_buf); - - gb->soft_reset(); - - size_t frame_in = gb->input({1, D}, Precision::FP16); - size_t token_idx = gb->input({1}, Precision::FP32); - - size_t pred = gb->embedding(weight_nodes_.predictor_embed, token_idx); - - const size_t predictor_layers = weight_nodes_.predictor_layers.size(); - std::vector h_prev_nodes; - std::vector c_prev_nodes; - std::vector h_new_nodes; - std::vector c_new_nodes; - std::vector bias_hh_zero_nodes; - std::vector hidden_sizes; - - h_prev_nodes.reserve(predictor_layers); - c_prev_nodes.reserve(predictor_layers); - h_new_nodes.reserve(predictor_layers); - c_new_nodes.reserve(predictor_layers); - bias_hh_zero_nodes.reserve(predictor_layers); - hidden_sizes.reserve(predictor_layers); - - for (size_t i = 0; i < predictor_layers; ++i) { - const auto w_ih = weight_nodes_.predictor_layers[i].weight_ih; - const auto w_hh = weight_nodes_.predictor_layers[i].weight_hh; - const auto b_ih = weight_nodes_.predictor_layers[i].bias; - - if (gb->get_output_buffer(w_ih).precision != Precision::FP16 || - gb->get_output_buffer(w_hh).precision != Precision::FP16 || - gb->get_output_buffer(b_ih).precision != Precision::FP16) { - throw std::runtime_error( - "ParakeetTDT predictor LSTM weights must be FP16. " - "Re-convert the model with updated converter."); - } - - const auto& w_ih_shape = gb->get_output_buffer(w_ih).shape; - if (w_ih_shape.size() != 2 || (w_ih_shape[0] % 4) != 0) { - throw std::runtime_error("ParakeetTDT predictor LSTM weight_ih must be [4*hidden, input]"); - } - const size_t hidden_size = w_ih_shape[0] / 4; - hidden_sizes.push_back(hidden_size); - - size_t h_prev = gb->input({1, hidden_size}, Precision::FP16); - size_t c_prev = gb->input({1, hidden_size}, Precision::FP16); - size_t b_hh_zero = gb->input({4 * hidden_size}, Precision::FP16); - - size_t lstm_out = gb->lstm_cell(pred, h_prev, c_prev, w_ih, w_hh, b_ih, b_hh_zero); - size_t h_new = gb->slice(lstm_out, 2, 0, 1); - size_t c_new = gb->slice(lstm_out, 2, 1, 1); - - h_new = gb->reshape(h_new, {1, hidden_size}); - c_new = gb->reshape(c_new, {1, hidden_size}); - - pred = h_new; - - h_prev_nodes.push_back(h_prev); - c_prev_nodes.push_back(c_prev); - h_new_nodes.push_back(h_new); - c_new_nodes.push_back(c_new); - bias_hh_zero_nodes.push_back(b_hh_zero); - } - - size_t enc_proj = gb->matmul(frame_in, weight_nodes_.joint_enc_weight, true, ComputeBackend::CPU); - enc_proj = gb->add(enc_proj, weight_nodes_.joint_enc_bias); - - size_t pred_proj = gb->matmul(pred, weight_nodes_.joint_pred_weight, true, ComputeBackend::CPU); - pred_proj = gb->add(pred_proj, weight_nodes_.joint_pred_bias); - - size_t joint = gb->add(enc_proj, pred_proj); - joint = gb->relu(joint); - - size_t logits = gb->matmul(joint, weight_nodes_.joint_out_weight, true, ComputeBackend::CPU); - logits = gb->add(logits, weight_nodes_.joint_out_bias); - - const auto& logits_shape = gb->get_output_buffer(logits).shape; - if (logits_shape.size() != 2 || logits_shape[0] != 1) { - throw std::runtime_error("ParakeetTDT joint output must be [1, classes]"); - } - - const size_t total_classes = logits_shape[1]; - std::vector durations = config_.tdt_durations; - size_t duration_classes = durations.size(); - if (duration_classes == 0) { - const size_t fallback = static_cast(config_.tdt_num_durations); - if (fallback > 0) { - durations.resize(fallback); - for (size_t i = 0; i < fallback; ++i) { - durations[i] = static_cast(i); - } - duration_classes = fallback; - } - } - - if (duration_classes == 0 || duration_classes >= total_classes) { - throw std::runtime_error("ParakeetTDT duration classes are invalid"); - } - - const size_t token_classes = total_classes - duration_classes; - - uint32_t blank_id = config_.tdt_blank_id; - if (blank_id >= token_classes) { - const auto& emb_shape = gb->get_output_buffer(weight_nodes_.predictor_embed).shape; - if (emb_shape.size() == 2 && emb_shape[0] > 0) { - const size_t inferred_blank = emb_shape[0] - 1; - blank_id = static_cast(std::min(inferred_blank, token_classes - 1)); - } else { - blank_id = static_cast(token_classes - 1); - } - } - - const size_t time_limit = std::min(end_frame, T); - const size_t emit_begin = std::min(start_frame, time_limit); - const size_t time_begin = std::min(replay_start_frame, emit_begin); - - std::vector> h_state(predictor_layers); - std::vector> c_state(predictor_layers); - std::vector> bias_hh_zero_state(predictor_layers); - const bool can_resume_stream_state = - stream_state && - stream_state->initialized && - stream_state->h.size() == predictor_layers && - stream_state->c.size() == predictor_layers; - - for (size_t i = 0; i < predictor_layers; ++i) { - const bool can_resume_layer = - can_resume_stream_state && - stream_state->h[i].size() == hidden_sizes[i] && - stream_state->c[i].size() == hidden_sizes[i]; - if (can_resume_layer) { - h_state[i] = stream_state->h[i]; - c_state[i] = stream_state->c[i]; - } else { - h_state[i].assign(hidden_sizes[i], static_cast<__fp16>(0.0f)); - c_state[i].assign(hidden_sizes[i], static_cast<__fp16>(0.0f)); - } - bias_hh_zero_state[i].assign(4 * hidden_sizes[i], static_cast<__fp16>(0.0f)); - gb->set_input(bias_hh_zero_nodes[i], bias_hh_zero_state[i].data(), Precision::FP16); - } - - constexpr float kHopSec = 160.0f / 16000.0f; - const float frame_sec = kHopSec * static_cast(config_.subsampling_factor); - auto* tokenizer = get_tokenizer(); - - std::vector output_tokens; - output_tokens.reserve(T * 2); - - uint32_t last_token = blank_id; - if (can_resume_stream_state && - stream_state->last_token < token_classes) { - last_token = stream_state->last_token; - } - constexpr size_t kMaxSymbolsPerStep = 10; - size_t time_idx = time_begin; - const bool is_stream_mode = cactus::telemetry::isStreamMode(); - std::vector> snap_h = h_state; - std::vector> snap_c = c_state; - uint32_t snap_last_token = last_token; - size_t confirmed_count = 0; - double raw_decoder_time_ms = 0.0; - - while (time_idx < time_limit) { - bool advanced = false; - size_t symbols_added = 0; - - while (symbols_added < kMaxSymbolsPerStep) { - const __fp16* frame_ptr = encoder_fp16.data() + time_idx * D; - gb->set_input(frame_in, frame_ptr, Precision::FP16); - - float token_value = static_cast(last_token); - gb->set_input(token_idx, &token_value, Precision::FP32); - - for (size_t i = 0; i < predictor_layers; ++i) { - gb->set_input(h_prev_nodes[i], h_state[i].data(), Precision::FP16); - gb->set_input(c_prev_nodes[i], c_state[i].data(), Precision::FP16); - } - - const auto decoder_step_start = std::chrono::steady_clock::now(); - gb->execute(); - const auto decoder_step_end = std::chrono::steady_clock::now(); - raw_decoder_time_ms += - std::chrono::duration_cast( - decoder_step_end - decoder_step_start).count() / 1000.0; - - const auto& logits_buf = gb->get_output_buffer(logits); - const auto& bias = get_vocab_bias(); - const size_t best_token = argmax_range_with_bias(logits_buf, 0, token_classes, bias); - const size_t best_duration_idx = argmax_range(logits_buf, token_classes, duration_classes); - const uint32_t predicted_skip = durations[best_duration_idx]; - uint32_t skip = predicted_skip; - if (is_stream_mode && skip > kMaxStreamDurationSkipFrames) { - skip = kMaxStreamDurationSkipFrames; - } - - if (best_token != blank_id) { - if (stream_state && time_idx >= emit_begin && tokenizer) { - std::string piece = tokenizer->decode({static_cast(best_token)}); - if (!piece.empty() && piece[0] == ' ' && !output_tokens.empty()) { - snap_h = h_state; - snap_c = c_state; - snap_last_token = last_token; - confirmed_count = output_tokens.size(); - } - } - if (time_idx >= emit_begin) { - output_tokens.push_back( - {static_cast(best_token), time_idx * frame_sec, (time_idx + skip) * frame_sec}); - } - last_token = static_cast(best_token); - - for (size_t i = 0; i < predictor_layers; ++i) { - const auto& h_buf = gb->get_output_buffer(h_new_nodes[i]); - const auto& c_buf = gb->get_output_buffer(c_new_nodes[i]); - const __fp16* h_ptr = h_buf.data_as<__fp16>(); - const __fp16* c_ptr = c_buf.data_as<__fp16>(); - std::copy(h_ptr, h_ptr + hidden_sizes[i], h_state[i].begin()); - std::copy(c_ptr, c_ptr + hidden_sizes[i], c_state[i].begin()); - } - } - - ++symbols_added; - - if (skip > 0) { - time_idx += skip; - advanced = true; - break; - } - - if (best_token == blank_id) { - ++time_idx; - advanced = true; - break; - } - } - - if (!advanced) { - ++time_idx; - } - } - - if (out_confirmed_count) { - *out_confirmed_count = confirmed_count; - } - if (out_raw_decoder_time_ms) { - *out_raw_decoder_time_ms = raw_decoder_time_ms; - } - - if (stream_state) { - stream_state->initialized = true; - stream_state->last_token = snap_last_token; - stream_state->h = std::move(snap_h); - stream_state->c = std::move(snap_c); - } - - return output_tokens; -} - -std::vector ParakeetTDTModel::greedy_decode_tdt_tokens( - CactusGraph* gb, - size_t encoder_hidden_node) const { - return decode_tdt_tokens_with_state( - gb, - encoder_hidden_node, - 0, - 0, - std::numeric_limits::max(), - nullptr); -} - -uint32_t ParakeetTDTModel::decode_with_audio( - const std::vector& tokens, - const std::vector& audio_features, - float temperature, - float top_p, - size_t top_k, - const std::string& profile_file, - float* out_entropy, - float min_p, - float repetition_penalty, - float* out_token_time_start, - float* out_token_time_end) -{ - (void)temperature; - (void)top_p; - (void)top_k; - (void)min_p; - (void)repetition_penalty; - - if (!initialized_ || !graph_handle_) { - throw std::runtime_error("Model not initialized - call init() first"); - } - if (audio_features.empty()) { - throw std::runtime_error("Audio features cannot be empty in ParakeetTDT decode_with_audio"); - } - - auto* gb = static_cast(graph_handle_); - - const bool new_request = !tdt_tokens_ready_ || tokens.empty() || tokens.size() < last_input_token_count_; - if (new_request) { - gb->soft_reset(); - size_t encoder_out = forward(audio_features, tokens, false); - - if (!profile_file.empty()) { - gb->execute(profile_file); - } else { - gb->execute(); - } - - tdt_tokens_ = greedy_decode_tdt_tokens(gb, encoder_out); - tdt_emit_index_ = 0; - tdt_tokens_ready_ = true; - } - - last_input_token_count_ = tokens.size(); - if (out_entropy) { - *out_entropy = 0.0f; - } - - if (tdt_emit_index_ < tdt_tokens_.size()) { - const auto& tok = tdt_tokens_[tdt_emit_index_++]; - if (out_token_time_start) *out_token_time_start = tok.time_start; - if (out_token_time_end) *out_token_time_end = tok.time_end; - return tok.id; - } - - return get_tokenizer()->get_eos_token(); -} - -ParakeetTDTModel::ChunkStreamResult ParakeetTDTModel::decode_chunk_stream( - const std::vector& audio_features, - size_t replay_start_frame, - size_t start_frame, - size_t end_frame, - ChunkStreamState& state) { - if (!initialized_ || !graph_handle_) { - throw std::runtime_error("Model not initialized - call init() first"); - } - if (audio_features.empty()) { - throw std::runtime_error("Audio features cannot be empty in ParakeetTDT decode_chunk_stream"); - } - - auto* gb = static_cast(graph_handle_); - - gb->soft_reset(); - size_t encoder_out = forward(audio_features, {}, false); - gb->execute(); - - ChunkStreamResult result; - double raw_decoder_time_ms = 0.0; - std::vector tokens = decode_tdt_tokens_with_state( - gb, encoder_out, replay_start_frame, start_frame, end_frame, &state, - &result.confirmed_token_count, &raw_decoder_time_ms); - - result.token_count = tokens.size(); - result.raw_decoder_time_ms = raw_decoder_time_ms; - result.raw_decoder_tps = - (result.token_count > 0 && raw_decoder_time_ms > 0.0) - ? (static_cast(result.token_count) * 1000.0) / raw_decoder_time_ms - : 0.0; - constexpr float kHopSec = 160.0f / 16000.0f; - const float frame_sec = kHopSec * static_cast(config_.subsampling_factor); - result.start_sec = start_frame * frame_sec; - result.confirmed_end_sec = start_frame * frame_sec; - result.resume_end_sec = replay_start_frame * frame_sec; - result.end_sec = start_frame * frame_sec; - if (!tokens.empty()) { - result.start_sec = tokens.front().time_start; - result.end_sec = tokens.back().time_end; - if (result.confirmed_token_count > 0 && - result.confirmed_token_count <= tokens.size()) { - result.confirmed_end_sec = tokens[result.confirmed_token_count - 1].time_end; - result.resume_end_sec = result.confirmed_end_sec; - } - } - - auto* tokenizer = get_tokenizer(); - if (!tokenizer) { - throw std::runtime_error("Tokenizer unavailable in ParakeetTDT decode_chunk_stream"); - } - for (size_t i = 0; i < tokens.size(); ++i) { - const std::string piece = tokenizer->decode({tokens[i].id}); - result.text += piece; - if (i < result.confirmed_token_count) { - result.confirmed_text += piece; - } else { - result.pending_text += piece; - } - } - return result; -} - -std::vector ParakeetTDTModel::get_audio_embeddings(const std::vector& audio_features) { - auto* gb = static_cast(graph_handle_); - gb->soft_reset(); - - size_t hidden = build_encoder(gb, audio_features); - size_t pooled = gb->mean(hidden, 0); - gb->execute(); - - const auto& output_buf = gb->get_output_buffer(pooled); - const size_t hidden_dim = output_buf.total_size; - std::vector embedding(hidden_dim); - - if (output_buf.precision == Precision::FP32) { - const float* src = output_buf.data_as(); - std::copy(src, src + hidden_dim, embedding.begin()); - } else if (output_buf.precision == Precision::FP16) { - const __fp16* src = output_buf.data_as<__fp16>(); - Quantization::fp16_to_fp32(src, embedding.data(), hidden_dim); - } else { - const int8_t* src = output_buf.data_as(); - Quantization::int8_to_fp32(src, embedding.data(), hidden_dim, 1.0f); - } - - reset_cache(); - return embedding; -} - -void ParakeetTDTModel::reset_cache() { - Model::reset_cache(); - tdt_tokens_ready_ = false; - tdt_emit_index_ = 0; - tdt_tokens_.clear(); - last_input_token_count_ = 0; -} - -} // namespace engine -} // namespace cactus diff --git a/cactus/models/model_pyannote.cpp b/cactus/models/model_pyannote.cpp deleted file mode 100644 index 44ac4c8c3..000000000 --- a/cactus/models/model_pyannote.cpp +++ /dev/null @@ -1,201 +0,0 @@ -#include "model.h" -#include "../graph/graph.h" -#include "../kernel/kernel.h" -#include -#include -#include - -namespace cactus { -namespace engine { - -static constexpr size_t WINDOW_SAMPLES = 160000; -static constexpr size_t FRAMES_PER_WINDOW = 589; -static constexpr int NUM_CLASSES = 7; -static constexpr int NUM_SPEAKERS = 3; - -PyAnnoteModel::PyAnnoteModel() : Model() {} -PyAnnoteModel::PyAnnoteModel(const Config& config) : Model(config) {} - -void PyAnnoteModel::load_weights_to_graph(CactusGraph* gb) { - const std::string& p = model_folder_path_; - - weight_nodes_.sinc_filters = gb->mmap_weights(p + "/sincnet_sinc_filters.weights"); - weight_nodes_.wav_norm_weight = gb->mmap_weights(p + "/sincnet_wav_norm_weight.weights"); - weight_nodes_.wav_norm_bias = gb->mmap_weights(p + "/sincnet_wav_norm_bias.weights"); - weight_nodes_.norm0_weight = gb->mmap_weights(p + "/sincnet_norm0_weight.weights"); - weight_nodes_.norm0_bias = gb->mmap_weights(p + "/sincnet_norm0_bias.weights"); - weight_nodes_.conv1_weight = gb->mmap_weights(p + "/sincnet_conv1_weight.weights"); - weight_nodes_.conv1_bias = gb->mmap_weights(p + "/sincnet_conv1_bias.weights"); - weight_nodes_.norm1_weight = gb->mmap_weights(p + "/sincnet_norm1_weight.weights"); - weight_nodes_.norm1_bias = gb->mmap_weights(p + "/sincnet_norm1_bias.weights"); - weight_nodes_.conv2_weight = gb->mmap_weights(p + "/sincnet_conv2_weight.weights"); - weight_nodes_.conv2_bias = gb->mmap_weights(p + "/sincnet_conv2_bias.weights"); - weight_nodes_.norm2_weight = gb->mmap_weights(p + "/sincnet_norm2_weight.weights"); - weight_nodes_.norm2_bias = gb->mmap_weights(p + "/sincnet_norm2_bias.weights"); - - for (int layer = 0; layer < 4; ++layer) { - auto& lw = weight_nodes_.lstm_layers[layer]; - std::string fwd = p + "/lstm_fwd_" + std::to_string(layer); - std::string bwd = p + "/lstm_bwd_" + std::to_string(layer); - lw.w_ih_fwd = gb->mmap_weights(fwd + "_weight_ih.weights"); - lw.w_hh_fwd = gb->mmap_weights(fwd + "_weight_hh.weights"); - lw.b_ih_fwd = gb->mmap_weights(fwd + "_bias_ih.weights"); - lw.b_hh_fwd = gb->mmap_weights(fwd + "_bias_hh.weights"); - lw.w_ih_bwd = gb->mmap_weights(bwd + "_weight_ih.weights"); - lw.w_hh_bwd = gb->mmap_weights(bwd + "_weight_hh.weights"); - lw.b_ih_bwd = gb->mmap_weights(bwd + "_bias_ih.weights"); - lw.b_hh_bwd = gb->mmap_weights(bwd + "_bias_hh.weights"); - } - - weight_nodes_.linear0_weight = gb->mmap_weights(p + "/linear_0_weight.weights"); - weight_nodes_.linear0_bias = gb->mmap_weights(p + "/linear_0_bias.weights"); - weight_nodes_.linear1_weight = gb->mmap_weights(p + "/linear_1_weight.weights"); - weight_nodes_.linear1_bias = gb->mmap_weights(p + "/linear_1_bias.weights"); - weight_nodes_.classifier_weight = gb->mmap_weights(p + "/classifier_weight.weights"); - weight_nodes_.classifier_bias = gb->mmap_weights(p + "/classifier_bias.weights"); -} - -void PyAnnoteModel::build_graph() { - audio_input_ = graph_.input({1, 1, 160000}, Precision::FP16); - - size_t x = graph_.groupnorm(audio_input_, weight_nodes_.wav_norm_weight, weight_nodes_.wav_norm_bias, 1, 1e-5f); - x = graph_.conv1d(x, weight_nodes_.sinc_filters, 10); - x = graph_.abs(x); - x = graph_.maxpool1d(x, 3, 3); - x = graph_.groupnorm(x, weight_nodes_.norm0_weight, weight_nodes_.norm0_bias, 80, 1e-5f); - x = graph_.leaky_relu(x, 0.01f); - - x = graph_.conv1d(x, weight_nodes_.conv1_weight, weight_nodes_.conv1_bias, 1); - x = graph_.maxpool1d(x, 3, 3); - x = graph_.groupnorm(x, weight_nodes_.norm1_weight, weight_nodes_.norm1_bias, 60, 1e-5f); - x = graph_.leaky_relu(x, 0.01f); - - x = graph_.conv1d(x, weight_nodes_.conv2_weight, weight_nodes_.conv2_bias, 1); - x = graph_.maxpool1d(x, 3, 3); - x = graph_.groupnorm(x, weight_nodes_.norm2_weight, weight_nodes_.norm2_bias, 60, 1e-5f); - x = graph_.leaky_relu(x, 0.01f); - - x = graph_.transposeN(x, {0, 2, 1}); - - for (int layer = 0; layer < 4; ++layer) { - auto& lw = weight_nodes_.lstm_layers[layer]; - x = graph_.bilstm_sequence(x, - lw.w_ih_fwd, lw.w_hh_fwd, lw.b_ih_fwd, lw.b_hh_fwd, - lw.w_ih_bwd, lw.w_hh_bwd, lw.b_ih_bwd, lw.b_hh_bwd); - } - - const auto& bilstm_shape = graph_.get_output_buffer(x).shape; - size_t T = bilstm_shape[1]; - x = graph_.reshape(x, {T, bilstm_shape[2]}); - - x = graph_.add(graph_.matmul(x, weight_nodes_.linear0_weight, true), weight_nodes_.linear0_bias); - x = graph_.leaky_relu(x, 0.01f); - x = graph_.add(graph_.matmul(x, weight_nodes_.linear1_weight, true), weight_nodes_.linear1_bias); - x = graph_.leaky_relu(x, 0.01f); - - x = graph_.add(graph_.matmul(x, weight_nodes_.classifier_weight, true), weight_nodes_.classifier_bias); - x = graph_.softmax(x, -1); - x = graph_.reshape(x, {1, T, 7}); - - output_node_ = x; -} - -bool PyAnnoteModel::init(const std::string& model_folder, size_t context_size, - const std::string& system_prompt, bool do_warmup) { - (void)context_size; - (void)system_prompt; - (void)do_warmup; - - if (initialized_) { - return true; - } - - model_folder_path_ = model_folder; - - try { - load_weights_to_graph(&graph_); - build_graph(); - - chunk_buf_.resize(WINDOW_SAMPLES); - - hamming_.resize(FRAMES_PER_WINDOW); - for (size_t i = 0; i < FRAMES_PER_WINDOW; ++i) - hamming_[i] = 0.54f - 0.46f * std::cos(2.0f * M_PI * i / (FRAMES_PER_WINDOW - 1)); - - initialized_ = true; - return true; - } catch (const std::exception& e) { - initialized_ = false; - return false; - } -} - -std::vector PyAnnoteModel::diarize(const float* pcm_f32, size_t num_samples, size_t step_samples, bool raw_powerset) { - if (!initialized_) throw std::runtime_error("PyAnnote model not initialized"); - - const size_t total_frames = std::max( - FRAMES_PER_WINDOW, - static_cast(std::round((double)num_samples * FRAMES_PER_WINDOW / WINDOW_SAMPLES)) - ); - - std::vector aggregated(total_frames * NUM_CLASSES, 0.0f); - std::vector weight_sum(total_frames, 0.0f); - - auto process_chunk = [&](size_t chunk_start) { - const size_t copy_len = std::min(WINDOW_SAMPLES, num_samples - chunk_start); - for (size_t i = 0; i < copy_len; ++i) - chunk_buf_[i] = static_cast<__fp16>(pcm_f32[chunk_start + i]); - std::fill(chunk_buf_.begin() + copy_len, chunk_buf_.end(), static_cast<__fp16>(0.0f)); - - graph_.set_input(audio_input_, chunk_buf_.data(), Precision::FP16); - graph_.execute(); - - const __fp16* chunk_scores = graph_.get_output_buffer(output_node_).data_as<__fp16>(); - const size_t frame_offset = static_cast(std::round((double)chunk_start * FRAMES_PER_WINDOW / WINDOW_SAMPLES)); - - for (size_t f = 0; f < FRAMES_PER_WINDOW; ++f) { - const size_t out_f = frame_offset + f; - if (out_f >= total_frames) break; - const float w = hamming_[f]; - weight_sum[out_f] += w; - for (int c = 0; c < NUM_CLASSES; ++c) - aggregated[out_f * NUM_CLASSES + c] += w * static_cast(chunk_scores[f * NUM_CLASSES + c]); - } - }; - - const size_t last_start = num_samples > WINDOW_SAMPLES ? num_samples - WINDOW_SAMPLES : 0; - bool last_processed = false; - for (size_t s = 0; s + WINDOW_SAMPLES <= num_samples; s += step_samples) { - if (s >= last_start) last_processed = true; - process_chunk(s); - } - if (!last_processed) - process_chunk(last_start); - - for (size_t f = 0; f < total_frames; ++f) { - if (weight_sum[f] > 0.0f) { - const float inv_w = 1.0f / weight_sum[f]; - float* agg = aggregated.data() + f * NUM_CLASSES; - for (int c = 0; c < NUM_CLASSES; ++c) - agg[c] *= inv_w; - } - } - - if (raw_powerset) { - return aggregated; - } - - std::vector speaker_probs(total_frames * NUM_SPEAKERS, 0.0f); - for (size_t f = 0; f < total_frames; ++f) { - const float* agg = aggregated.data() + f * NUM_CLASSES; - float* dst = speaker_probs.data() + f * NUM_SPEAKERS; - dst[0] = agg[1] + agg[4] + agg[5]; - dst[1] = agg[2] + agg[4] + agg[6]; - dst[2] = agg[3] + agg[5] + agg[6]; - } - - return speaker_probs; -} - -} -} diff --git a/cactus/models/model_qwen.cpp b/cactus/models/model_qwen.cpp deleted file mode 100644 index 601c05334..000000000 --- a/cactus/models/model_qwen.cpp +++ /dev/null @@ -1,171 +0,0 @@ -#include "model.h" -#include "../graph/graph.h" -#include "../npu/npu.h" -#include -#include - - -namespace cactus { -namespace engine { - -QwenModel::QwenModel() : Model() {} - -QwenModel::QwenModel(const Config& config) : Model(config) { - weight_nodes_.layers.resize(config.num_layers); -} - -void QwenModel::load_weights_to_graph(CactusGraph* gb) { - embedding_node_id_ = gb->mmap_embeddings(embedding_file_path_); - weight_nodes_.output_norm_weight = gb->mmap_weights(model_folder_path_ + "/output_norm.weights"); - - if (config_.tie_word_embeddings) { - weight_nodes_.output_weight = embedding_node_id_; - output_weight_node_id_ = embedding_node_id_; - } else { - weight_nodes_.output_weight = gb->mmap_weights(model_folder_path_ + "/output_weight.weights"); - output_weight_node_id_ = weight_nodes_.output_weight; - } - - for (uint32_t i = 0; i < config_.num_layers; i++) { - auto& layer = weight_nodes_.layers[i]; - std::string layer_prefix = model_folder_path_ + "/layer_" + std::to_string(i) + "_"; - layer.attn_q_weight = gb->mmap_weights(layer_prefix + "attn_q.weights"); - layer.attn_k_weight = gb->mmap_weights(layer_prefix + "attn_k.weights"); - layer.attn_v_weight = gb->mmap_weights(layer_prefix + "attn_v.weights"); - layer.attn_output_weight = gb->mmap_weights(layer_prefix + "attn_output.weights"); - layer.input_layernorm_weight = gb->mmap_weights(layer_prefix + "input_norm.weights"); - layer.attn_q_norm_weight = gb->mmap_weights(layer_prefix + "attn_q_norm.weights"); - layer.attn_k_norm_weight = gb->mmap_weights(layer_prefix + "attn_k_norm.weights"); - layer.ffn_gate_weight = gb->mmap_weights(layer_prefix + "ffn_gate.weights"); - layer.ffn_up_weight = gb->mmap_weights(layer_prefix + "ffn_up.weights"); - layer.ffn_down_weight = gb->mmap_weights(layer_prefix + "ffn_down.weights"); - layer.post_attention_layernorm_weight = gb->mmap_weights(layer_prefix + "post_attn_norm.weights"); - } - - if (npu::is_npu_available()) { - std::string npu_prefill_path = model_folder_path_ + "/model.mlpackage"; - load_npu_prefill(npu_prefill_path); - } -} - -size_t QwenModel::build_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache, size_t position_offset) { - const auto& layer = weight_nodes_.layers[layer_idx]; - - auto q_proj = gb->matmul(normalized_input, layer.attn_q_weight, true, backend); - auto k_proj = gb->matmul(normalized_input, layer.attn_k_weight, true, backend); - auto v_proj = gb->matmul(normalized_input, layer.attn_v_weight, true, backend); - - const auto& q_shape = gb->get_output_buffer(q_proj).shape; - size_t batch_seq = q_shape[0]; - size_t num_heads = config_.attention_heads; - size_t head_dim = config_.attention_head_dim; - q_proj = gb->reshape(q_proj, {batch_seq * num_heads, head_dim}); - q_proj = gb->rms_norm(q_proj, layer.attn_q_norm_weight, config_.layer_norm_eps); - q_proj = gb->reshape(q_proj, {batch_seq, num_heads * head_dim}); - - size_t num_kv_heads = config_.attention_kv_heads; - k_proj = gb->reshape(k_proj, {batch_seq * num_kv_heads, head_dim}); - k_proj = gb->rms_norm(k_proj, layer.attn_k_norm_weight, config_.layer_norm_eps); - k_proj = gb->reshape(k_proj, {batch_seq, num_kv_heads * head_dim}); - - size_t seq_len = batch_seq; - - auto q_proj_4d = gb->reshape(q_proj, {1, seq_len, config_.attention_heads, config_.attention_head_dim}); - auto k_proj_4d = gb->reshape(k_proj, {1, seq_len, config_.attention_kv_heads, config_.attention_head_dim}); - auto v_proj_4d = gb->reshape(v_proj, {1, seq_len, config_.attention_kv_heads, config_.attention_head_dim}); - - if (config_.rope_theta > 0) { - q_proj_4d = gb->rope(q_proj_4d, config_.rope_theta, position_offset); - k_proj_4d = gb->rope(k_proj_4d, config_.rope_theta, position_offset); - } - - size_t attn_output_4d; - - if (use_cache) { - cache_k_output_nodes_[layer_idx] = k_proj_4d; - cache_v_output_nodes_[layer_idx] = v_proj_4d; - } - - if (use_cache && !kv_cache_.is_empty()) { - attn_output_4d = gb->attention_int8_hybrid( - q_proj_4d, k_proj_4d, v_proj_4d, - attention_scale_, position_offset, - kv_cache_.get_keys_int8(layer_idx), - kv_cache_.get_values_int8(layer_idx), - kv_cache_.get_key_scales(layer_idx), - kv_cache_.get_value_scales(layer_idx), - kv_cache_.current_seq_len, num_kv_heads, head_dim - ); - } else { - attn_output_4d = gb->attention(q_proj_4d, k_proj_4d, v_proj_4d, attention_scale_, position_offset); - } - - auto attn_output = gb->reshape(attn_output_4d, {seq_len, config_.attention_head_dim * config_.attention_heads}); - return gb->matmul(attn_output, layer.attn_output_weight, true, backend); -} - - -size_t QwenModel::build_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, - ComputeBackend backend) const { - const auto& layer = weight_nodes_.layers[layer_idx]; - size_t gate_output = gb->matmul(normalized_h, layer.ffn_gate_weight, true, backend); - size_t up_output = gb->matmul(normalized_h, layer.ffn_up_weight, true, backend); - size_t gate_silu = gb->silu(gate_output); - size_t gated = gb->multiply(gate_silu, up_output); - return gb->matmul(gated, layer.ffn_down_weight, true, backend); -} - - -size_t QwenModel::build_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache, size_t position_offset) { - const auto& layer = weight_nodes_.layers[layer_idx]; - auto normalized_input = gb->rms_norm(hidden, layer.input_layernorm_weight, config_.layer_norm_eps); - auto attn_output = build_attention(gb, normalized_input, layer_idx, backend, use_cache, position_offset); - auto after_attention = gb->add(hidden, attn_output); - auto normalized_after_attention = gb->rms_norm(after_attention, layer.post_attention_layernorm_weight, config_.layer_norm_eps); - auto mlp_output = build_mlp(gb, normalized_after_attention, layer_idx, backend); - return gb->add(after_attention, mlp_output); -} - - -size_t QwenModel::forward(const std::vector& tokens, bool use_cache) { - if (!initialized_ || !graph_handle_) { - throw std::runtime_error("Model not initialized - call init() first"); - } - - if (tokens.empty()) { - throw std::runtime_error("Token sequence cannot be empty"); - } - - auto* gb = static_cast(graph_handle_); - gb->soft_reset(); - - auto seq_len = static_cast(tokens.size()); - - size_t position_offset = use_cache ? kv_cache_.get_total_seq_len() : 0; - - auto backend = config_.default_backend == Config::Backend::CPU - ? ComputeBackend::CPU - : ComputeBackend::NPU; - - auto input_node_id = gb->input({seq_len}, Precision::FP32); - auto hidden = gb->embedding(embedding_node_id_, input_node_id); - - std::vector input_data(seq_len); - for (size_t i = 0; i < seq_len; i++) { - input_data[i] = static_cast(tokens[i]); - } - gb->set_input(input_node_id, input_data.data(), Precision::FP32); - - for (uint32_t layer_idx = 0; layer_idx < config_.num_layers; layer_idx++) { - hidden = build_transformer_block(gb, hidden, layer_idx, backend, use_cache, position_offset); - } - - auto final_hidden = gb->rms_norm(hidden, weight_nodes_.output_norm_weight, config_.layer_norm_eps); - - return final_hidden; -} - -} -} \ No newline at end of file diff --git a/cactus/models/model_qwen3p5.cpp b/cactus/models/model_qwen3p5.cpp deleted file mode 100644 index 0f438e1c0..000000000 --- a/cactus/models/model_qwen3p5.cpp +++ /dev/null @@ -1,771 +0,0 @@ -#include "model.h" -#include "../graph/graph.h" -#include "../npu/npu.h" -#include -#include -#include -#include -#include -#include -#include - -namespace cactus { -namespace engine { - -Qwen3p5Model::Qwen3p5Model() : Model() { - weight_nodes_.layers.resize(config_.num_layers); - conv_cache_state_nodes_.assign(config_.num_layers, 0); -} - -Qwen3p5Model::Qwen3p5Model(const Config& config) : Model(config) { - weight_nodes_.layers.resize(config.num_layers); - conv_cache_state_nodes_.assign(config.num_layers, 0); -} - -void Qwen3p5Model::post_init() { - bool has_deltanet_layer = false; - const WeightNodeIDs::LayerWeights* first_linear = nullptr; - for (const auto& layer : weight_nodes_.layers) { - if (layer.type == WeightNodeIDs::LayerType::DELTANET) { - has_deltanet_layer = true; - if (!first_linear) { - first_linear = &layer.weights; - } - break; - } - } - - deltanet_heads_ = static_cast(config_.attention_heads); - deltanet_key_dim_ = static_cast(config_.attention_head_dim); - deltanet_value_dim_ = static_cast(config_.attention_head_dim); - - if (has_deltanet_layer && - config_.linear_num_key_heads > 0 && - config_.linear_key_head_dim > 0) { - const size_t num_k_heads = static_cast(config_.linear_num_key_heads); - const size_t key_head_dim = static_cast(config_.linear_key_head_dim); - size_t num_v_heads = static_cast(config_.linear_num_value_heads > 0 - ? config_.linear_num_value_heads - : config_.linear_num_key_heads); - size_t value_head_dim = static_cast(config_.linear_value_head_dim); - - if (num_v_heads == 0) { - throw std::runtime_error("Qwen3p5Model invalid linear-attention value head count in config"); - } - if (value_head_dim == 0) { - if (config_.linear_v_proj_dim > 0 && (config_.linear_v_proj_dim % num_v_heads == 0)) { - value_head_dim = static_cast(config_.linear_v_proj_dim / num_v_heads); - } else { - throw std::runtime_error("Qwen3p5Model cannot derive linear-attention value head dim from config"); - } - } - if (config_.linear_q_proj_dim > 0 && - static_cast(config_.linear_q_proj_dim) != num_k_heads * key_head_dim) { - throw std::runtime_error("Qwen3p5Model linear-attention q projection dim mismatch with key-head topology"); - } - if (config_.linear_v_proj_dim > 0 && - static_cast(config_.linear_v_proj_dim) != num_v_heads * value_head_dim) { - throw std::runtime_error("Qwen3p5Model linear-attention v projection dim mismatch with value-head topology"); - } - - deltanet_heads_ = num_v_heads; - deltanet_key_dim_ = key_head_dim; - deltanet_value_dim_ = value_head_dim; - } else if (has_deltanet_layer) { - auto* gb = static_cast(graph_handle_); - if (!gb) { - throw std::runtime_error("Qwen3p5Model cannot infer linear-attention dims: graph unavailable"); - } - if (!first_linear) { - throw std::runtime_error("Qwen3p5Model expected deltanet layer weights for dim inference"); - } - - const auto& q_buf = gb->get_output_buffer(first_linear->attn_q_weight); - const auto& v_buf = gb->get_output_buffer(first_linear->attn_v_weight); - const auto& n_buf = gb->get_output_buffer(first_linear->attn_q_norm_weight); - if (q_buf.shape.size() < 2 || v_buf.shape.size() < 2 || n_buf.shape.empty()) { - throw std::runtime_error("Qwen3p5Model invalid linear-attention weight shapes"); - } - - const size_t q_proj_dim = q_buf.shape[0]; - const size_t v_proj_dim = v_buf.shape[0]; - const size_t inferred_key_dim = n_buf.shape[0]; - if (inferred_key_dim == 0 || q_proj_dim == 0 || v_proj_dim == 0) { - throw std::runtime_error("Qwen3p5Model failed to infer non-zero linear-attention dims"); - } - if (q_proj_dim % inferred_key_dim != 0) { - throw std::runtime_error("Qwen3p5Model q projection dim is not divisible by inferred key dim"); - } - - const size_t inferred_k_heads = q_proj_dim / inferred_key_dim; - const size_t inferred_v_heads = v_proj_dim / inferred_key_dim; - if (inferred_k_heads == 0 || inferred_v_heads == 0 || inferred_v_heads % inferred_k_heads != 0) { - throw std::runtime_error("Qwen3p5Model failed to infer compatible linear-attention head topology"); - } - - deltanet_heads_ = inferred_v_heads; - deltanet_key_dim_ = inferred_key_dim; - deltanet_value_dim_ = inferred_key_dim; - } - - if (deltanet_heads_ == 0 || deltanet_key_dim_ == 0 || deltanet_value_dim_ == 0) { - throw std::runtime_error("Qwen3p5Model requires non-zero deltanet heads/key/value dims"); - } - - deltanet_state_flat_dim_ = deltanet_heads_ * deltanet_key_dim_ * deltanet_value_dim_; - - deltanet_mixed_dim_ = 0; - if (config_.linear_q_proj_dim > 0 && config_.linear_v_proj_dim > 0) { - deltanet_mixed_dim_ = static_cast(2 * config_.linear_q_proj_dim + config_.linear_v_proj_dim); - } else if (has_deltanet_layer) { - auto* gb = static_cast(graph_handle_); - if (!gb || !first_linear) { - throw std::runtime_error("Qwen3p5Model cannot infer linear-attention mixed projection dim"); - } - const auto& q_buf = gb->get_output_buffer(first_linear->attn_q_weight); - const auto& v_buf = gb->get_output_buffer(first_linear->attn_v_weight); - if (q_buf.shape.size() < 2 || v_buf.shape.size() < 2) { - throw std::runtime_error("Qwen3p5Model invalid q/v projection shapes for mixed dim inference"); - } - deltanet_mixed_dim_ = (2 * q_buf.shape[0]) + v_buf.shape[0]; - } - - deltanet_conv_history_len_ = 0; - if (has_deltanet_layer) { - auto* gb = static_cast(graph_handle_); - if (!gb) { - throw std::runtime_error("Qwen3p5Model cannot infer linear-attention conv history: graph unavailable"); - } - for (const auto& layer : weight_nodes_.layers) { - if (layer.type != WeightNodeIDs::LayerType::DELTANET || layer.weights.deltanet_conv_weight == 0) { - continue; - } - const auto& conv_buf = gb->get_output_buffer(layer.weights.deltanet_conv_weight); - if (conv_buf.shape.empty()) { - continue; - } - const size_t kernel = conv_buf.shape.back(); - if (kernel > 1) { - deltanet_conv_history_len_ = std::max(deltanet_conv_history_len_, kernel - 1); - } - } - } - - if (deltanet_conv_history_len_ > 0 && deltanet_mixed_dim_ == 0) { - throw std::runtime_error("Qwen3p5Model linear-attention conv history requires non-zero mixed dim"); - } - deltanet_conv_flat_dim_ = deltanet_conv_history_len_ * deltanet_mixed_dim_; - deltanet_cache_row_dim_ = deltanet_state_flat_dim_ + deltanet_conv_flat_dim_; - if (deltanet_cache_row_dim_ == 0) { - throw std::runtime_error("Qwen3p5Model computed zero cache row dim"); - } - - conv_cache_.init(config_.num_layers, deltanet_cache_row_dim_, 1, Precision::FP32); - last_forward_used_cache_ = false; - deltanet_total_seq_len_ = 0; -} - -void Qwen3p5Model::reset_cache() { - Model::reset_cache(); - if (conv_cache_.window_size > 0) { - conv_cache_.reset(); - } - std::fill(conv_cache_state_nodes_.begin(), conv_cache_state_nodes_.end(), 0); - deltanet_total_seq_len_ = 0; - last_forward_used_cache_ = false; -} - -void Qwen3p5Model::post_execute_updates(CactusGraph* gb, size_t seq_len) { - if (!last_forward_used_cache_) { - std::fill(conv_cache_state_nodes_.begin(), conv_cache_state_nodes_.end(), 0); - return; - } - - const size_t layer_count = std::min(conv_cache_state_nodes_.size(), weight_nodes_.layers.size()); - for (size_t layer_idx = 0; layer_idx < layer_count; ++layer_idx) { - if (weight_nodes_.layers[layer_idx].type != WeightNodeIDs::LayerType::DELTANET) { - conv_cache_state_nodes_[layer_idx] = 0; - continue; - } - - const size_t state_node = conv_cache_state_nodes_[layer_idx]; - if (state_node != 0) { - conv_cache_.update(gb, layer_idx, state_node); - } - conv_cache_state_nodes_[layer_idx] = 0; - } - - deltanet_total_seq_len_ += seq_len; - last_forward_used_cache_ = false; -} - -void Qwen3p5Model::load_weights_to_graph(CactusGraph* gb) { - embedding_node_id_ = gb->mmap_embeddings(embedding_file_path_); - weight_nodes_.output_norm_weight = gb->mmap_weights(model_folder_path_ + "/output_norm.weights"); - - if (config_.tie_word_embeddings) { - weight_nodes_.output_weight = embedding_node_id_; - output_weight_node_id_ = embedding_node_id_; - } else { - weight_nodes_.output_weight = gb->mmap_weights(model_folder_path_ + "/output_weight.weights"); - output_weight_node_id_ = weight_nodes_.output_weight; - } - - auto choose_existing_weight = [&](const std::vector& candidates) -> std::string { - for (const auto& p : candidates) { - if (std::filesystem::exists(p)) { - return p; - } - } - return std::string(); - }; - - auto mmap_required = [&](const std::vector& candidates, const std::string& weight_name) -> size_t { - const std::string path = choose_existing_weight(candidates); - if (path.empty()) { - throw std::runtime_error("Qwen3p5Model missing required weight: " + weight_name); - } - return gb->mmap_weights(path); - }; - - for (uint32_t i = 0; i < config_.num_layers; i++) { - auto& layer_entry = weight_nodes_.layers[i]; - auto& layer = layer_entry.weights; - - std::string layer_prefix = model_folder_path_ + "/layer_" + std::to_string(i) + "_"; - bool is_deltanet = false; - if (i < config_.layer_types.size()) { - std::string lt = config_.layer_types[i]; - std::transform(lt.begin(), lt.end(), lt.begin(), ::tolower); - is_deltanet = (lt.find("deltanet") != std::string::npos || - lt.find("linear_attention") != std::string::npos || - lt.find("linear_attn") != std::string::npos); - } - layer_entry.type = is_deltanet ? WeightNodeIDs::LayerType::DELTANET : WeightNodeIDs::LayerType::ATTENTION; - - layer.attn_q_weight = mmap_required( - {layer_prefix + "attn_q.weights", layer_prefix + "linear_attn_q.weights"}, - "layer_" + std::to_string(i) + "_attn_q/linear_attn_q"); - layer.attn_k_weight = mmap_required( - {layer_prefix + "attn_k.weights", layer_prefix + "linear_attn_k.weights"}, - "layer_" + std::to_string(i) + "_attn_k/linear_attn_k"); - layer.attn_v_weight = mmap_required( - {layer_prefix + "attn_v.weights", layer_prefix + "linear_attn_v.weights"}, - "layer_" + std::to_string(i) + "_attn_v/linear_attn_v"); - layer.attn_output_weight = mmap_required( - {layer_prefix + "attn_output.weights", layer_prefix + "linear_attn_output.weights"}, - "layer_" + std::to_string(i) + "_attn_output/linear_attn_output"); - layer.input_layernorm_weight = mmap_required( - {layer_prefix + "input_norm.weights"}, - "layer_" + std::to_string(i) + "_input_norm"); - layer.attn_q_norm_weight = mmap_required( - {layer_prefix + "attn_q_norm.weights", layer_prefix + "linear_attn_norm.weights"}, - "layer_" + std::to_string(i) + "_attn_q_norm/linear_attn_norm"); - layer.attn_k_norm_weight = mmap_required( - {layer_prefix + "attn_k_norm.weights", layer_prefix + "linear_attn_norm.weights"}, - "layer_" + std::to_string(i) + "_attn_k_norm/linear_attn_norm"); - - if (is_deltanet) { - const std::string qkv_path = choose_existing_weight({ - layer_prefix + "linear_attn_qkv.weights" - }); - layer.deltanet_qkv_weight = qkv_path.empty() ? 0 : gb->mmap_weights(qkv_path); - - const std::string gate_path = choose_existing_weight({ - layer_prefix + "deltanet_gate.weights", - layer_prefix + "attn_gate.weights", - layer_prefix + "attn_f_gate.weights", - layer_prefix + "linear_attn_a.weights" - }); - const std::string beta_path = choose_existing_weight({ - layer_prefix + "deltanet_beta.weights", - layer_prefix + "attn_beta.weights", - layer_prefix + "attn_f_beta.weights", - layer_prefix + "linear_attn_b.weights" - }); - if (gate_path.empty() || beta_path.empty()) { - throw std::runtime_error( - "Qwen3p5Model deltanet layer missing gate/beta weights at layer " + std::to_string(i)); - } - layer.deltanet_gate_weight = gb->mmap_weights(gate_path); - layer.deltanet_beta_weight = gb->mmap_weights(beta_path); - - const std::string gate_bias_path = choose_existing_weight({ - layer_prefix + "linear_attn_A_log.weights" - }); - const std::string beta_bias_path = choose_existing_weight({ - layer_prefix + "linear_attn_dt_bias.weights" - }); - layer.deltanet_gate_bias = gate_bias_path.empty() ? 0 : gb->mmap_weights(gate_bias_path); - layer.deltanet_beta_bias = beta_bias_path.empty() ? 0 : gb->mmap_weights(beta_bias_path); - const std::string z_path = choose_existing_weight({ - layer_prefix + "linear_attn_z.weights" - }); - const std::string conv_path = choose_existing_weight({ - layer_prefix + "linear_attn_conv1d.weights" - }); - layer.deltanet_z_weight = z_path.empty() ? 0 : gb->mmap_weights(z_path); - layer.deltanet_conv_weight = conv_path.empty() ? 0 : gb->mmap_weights(conv_path); - } - - layer.ffn_gate_weight = mmap_required( - {layer_prefix + "ffn_gate.weights"}, - "layer_" + std::to_string(i) + "_ffn_gate"); - layer.ffn_up_weight = mmap_required( - {layer_prefix + "ffn_up.weights"}, - "layer_" + std::to_string(i) + "_ffn_up"); - layer.ffn_down_weight = mmap_required( - {layer_prefix + "ffn_down.weights"}, - "layer_" + std::to_string(i) + "_ffn_down"); - layer.post_attention_layernorm_weight = mmap_required( - {layer_prefix + "post_attn_norm.weights"}, - "layer_" + std::to_string(i) + "_post_attn_norm"); - } - - if (npu::is_npu_available()) { - std::string npu_prefill_path = model_folder_path_ + "/model.mlpackage"; - load_npu_prefill(npu_prefill_path); - } -} - -size_t Qwen3p5Model::build_gated_deltanet(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache, size_t position_offset) { - (void)position_offset; - const auto& layer_entry = weight_nodes_.layers[layer_idx]; - const auto& layer = layer_entry.weights; - - const auto& in_shape = gb->get_output_buffer(normalized_input).shape; - if (in_shape.size() < 2) { - throw std::runtime_error("Qwen3p5Model linear-attention expects rank-2 normalized input"); - } - const size_t seq_len = in_shape[0]; - - size_t q_proj = 0; - size_t k_proj = 0; - size_t v_proj = 0; - size_t mixed_qkv = 0; - size_t z_proj = layer.deltanet_z_weight ? gb->matmul(normalized_input, layer.deltanet_z_weight, true, backend) : 0; - - const size_t num_heads = deltanet_heads_; - const size_t key_dim = deltanet_key_dim_; - const size_t value_dim = deltanet_value_dim_; - const size_t v_proj_dim = num_heads * value_dim; - size_t qk_proj_dim = 0; - - if (layer.deltanet_qkv_weight != 0) { - mixed_qkv = gb->matmul(normalized_input, layer.deltanet_qkv_weight, true, backend); - const auto& mixed_shape = gb->get_output_buffer(mixed_qkv).shape; - if (mixed_shape.size() < 2 || mixed_shape[0] != seq_len) { - throw std::runtime_error("Qwen3p5Model invalid linear-attention joint qkv projection shape"); - } - if (mixed_shape[1] <= v_proj_dim || ((mixed_shape[1] - v_proj_dim) % 2) != 0) { - throw std::runtime_error("Qwen3p5Model invalid linear-attention joint qkv projection width"); - } - qk_proj_dim = (mixed_shape[1] - v_proj_dim) / 2; - } else { - q_proj = gb->matmul(normalized_input, layer.attn_q_weight, true, backend); - k_proj = gb->matmul(normalized_input, layer.attn_k_weight, true, backend); - v_proj = gb->matmul(normalized_input, layer.attn_v_weight, true, backend); - const auto& q_shape = gb->get_output_buffer(q_proj).shape; - if (q_shape.size() < 2 || q_shape[0] != seq_len) { - throw std::runtime_error("Qwen3p5Model invalid linear-attention q projection shape"); - } - qk_proj_dim = q_shape[1]; - size_t mixed_qk = gb->concat(q_proj, k_proj, 1); - mixed_qkv = gb->concat(mixed_qk, v_proj, 1); - } - - if (qk_proj_dim == 0 || qk_proj_dim % key_dim != 0 || v_proj_dim == 0) { - throw std::runtime_error("Qwen3p5Model invalid linear-attention projection dimensions"); - } - const size_t num_k_heads = qk_proj_dim / key_dim; - if (num_heads % num_k_heads != 0) { - throw std::runtime_error("Qwen3p5Model requires num_v_heads divisible by num_k_heads"); - } - - const size_t mixed_proj_dim = qk_proj_dim + qk_proj_dim + v_proj_dim; - if (deltanet_mixed_dim_ != 0 && mixed_proj_dim != deltanet_mixed_dim_) { - throw std::runtime_error("Qwen3p5Model linear-attention mixed projection dim mismatch"); - } - const size_t state_flat_dim = deltanet_state_flat_dim_; - size_t prev_state_flat = 0; - size_t prev_conv_flat = 0; - if (use_cache && conv_cache_.window_size > 0) { - const auto view = conv_cache_.get_window(layer_idx); - if (conv_cache_.window_size == 1 && view.total_len == 1) { - const uint8_t* cache_row = nullptr; - if (view.len2 == 1 && view.ptr2 != nullptr) { - cache_row = static_cast(view.ptr2); - } else if (view.len1 == 1 && view.ptr1 != nullptr) { - cache_row = static_cast(view.ptr1); - } - if (cache_row != nullptr) { - prev_state_flat = gb->input({1, state_flat_dim}, conv_cache_.precision); - gb->set_external_input(prev_state_flat, const_cast(static_cast(cache_row)), - conv_cache_.precision); - if (deltanet_conv_flat_dim_ > 0) { - const size_t state_bytes = state_flat_dim * conv_cache_.element_size; - prev_conv_flat = gb->input({1, deltanet_conv_flat_dim_}, conv_cache_.precision); - gb->set_external_input( - prev_conv_flat, - const_cast(static_cast(cache_row + state_bytes)), - conv_cache_.precision); - } - } - } - - if (prev_state_flat == 0) { - std::vector segments; - if (view.len2 > 0) { - size_t left_node = gb->input({view.len2, deltanet_cache_row_dim_}, conv_cache_.precision); - gb->set_external_input(left_node, const_cast(view.ptr2), conv_cache_.precision); - segments.push_back(left_node); - } - if (view.len1 > 0) { - size_t right_node = gb->input({view.len1, deltanet_cache_row_dim_}, conv_cache_.precision); - gb->set_external_input(right_node, const_cast(view.ptr1), conv_cache_.precision); - segments.push_back(right_node); - } - - if (!segments.empty()) { - size_t stacked = segments[0]; - for (size_t i = 1; i < segments.size(); ++i) { - stacked = gb->concat(stacked, segments[i], 0); - } - if (view.total_len > 1) { - stacked = gb->slice(stacked, 0, view.total_len - 1, 1); - } - prev_state_flat = gb->slice(stacked, 1, 0, state_flat_dim); - if (deltanet_conv_flat_dim_ > 0) { - prev_conv_flat = gb->slice(stacked, 1, state_flat_dim, deltanet_conv_flat_dim_); - } - } - } - } - - size_t conv_source_with_history = mixed_qkv; - if (layer.deltanet_conv_weight) { - const auto& conv_wbuf = gb->get_output_buffer(layer.deltanet_conv_weight); - const size_t kernel = conv_wbuf.shape.back(); - if (kernel > 1 && use_cache && deltanet_conv_history_len_ > 0) { - size_t history_2d = 0; - if (prev_conv_flat != 0) { - history_2d = gb->reshape(prev_conv_flat, {deltanet_conv_history_len_, mixed_proj_dim}); - history_2d = gb->precision_cast(history_2d, Precision::FP16); - } else { - history_2d = gb->input({deltanet_conv_history_len_, mixed_proj_dim}, Precision::FP16); - std::vector<__fp16> zeros(deltanet_conv_flat_dim_, static_cast<__fp16>(0.0f)); - gb->set_input(history_2d, zeros.data(), Precision::FP16); - } - conv_source_with_history = gb->concat(history_2d, mixed_qkv, 0); - } - - const size_t total_conv_len = gb->get_output_buffer(conv_source_with_history).shape[0]; - size_t conv_input = gb->reshape(conv_source_with_history, {1, total_conv_len, mixed_proj_dim}); - size_t conv_out = gb->conv1d_causal(conv_input, layer.deltanet_conv_weight, kernel, 1); - const size_t conv_start = total_conv_len > seq_len ? total_conv_len - seq_len : 0; - conv_out = gb->slice(conv_out, 1, conv_start, seq_len); - mixed_qkv = gb->reshape(gb->silu(conv_out), {seq_len, mixed_proj_dim}); - } - - q_proj = gb->slice(mixed_qkv, 1, 0, qk_proj_dim); - k_proj = gb->slice(mixed_qkv, 1, qk_proj_dim, qk_proj_dim); - v_proj = gb->slice(mixed_qkv, 1, qk_proj_dim + qk_proj_dim, v_proj_dim); - - size_t q_4d = gb->reshape(q_proj, {1, seq_len, num_k_heads, key_dim}); - size_t k_4d = gb->reshape(k_proj, {1, seq_len, num_k_heads, key_dim}); - size_t v_4d = gb->reshape(v_proj, {1, seq_len, num_heads, value_dim}); - - size_t q_norm = gb->sum(gb->multiply(q_4d, q_4d), 3); - q_norm = gb->scalar_sqrt(gb->scalar_add(q_norm, 1e-6f)); - q_norm = gb->reshape(q_norm, {1, seq_len, num_k_heads, 1}); - q_4d = gb->divide(q_4d, q_norm); - - size_t k_norm = gb->sum(gb->multiply(k_4d, k_4d), 3); - k_norm = gb->scalar_sqrt(gb->scalar_add(k_norm, 1e-6f)); - k_norm = gb->reshape(k_norm, {1, seq_len, num_k_heads, 1}); - k_4d = gb->divide(k_4d, k_norm); - - size_t a_logits = gb->matmul(normalized_input, layer.deltanet_gate_weight, true, backend); - size_t b_logits = gb->matmul(normalized_input, layer.deltanet_beta_weight, true, backend); - a_logits = gb->precision_cast(a_logits, Precision::FP16); - b_logits = gb->precision_cast(b_logits, Precision::FP16); - if (layer.deltanet_beta_bias) { - size_t dt_bias_vec = layer.deltanet_beta_bias; - const auto& dt_buf = gb->get_output_buffer(dt_bias_vec); - if (!dt_buf.shape.empty() && dt_buf.shape[0] > num_heads) { - dt_bias_vec = gb->slice(dt_bias_vec, 0, 0, num_heads); - } - size_t dt_bias_2d = gb->reshape(dt_bias_vec, {1, num_heads}); - dt_bias_2d = gb->precision_cast(dt_bias_2d, Precision::FP16); - a_logits = gb->add(a_logits, dt_bias_2d); - } - size_t a_softplus = gb->scalar_log(gb->scalar_add(gb->scalar_exp(a_logits), 1.0f)); - - size_t gate_log; - if (layer.deltanet_gate_bias) { - size_t a_log_vec = layer.deltanet_gate_bias; - const auto& a_log_buf = gb->get_output_buffer(a_log_vec); - if (!a_log_buf.shape.empty() && a_log_buf.shape[0] > num_heads) { - a_log_vec = gb->slice(a_log_vec, 0, 0, num_heads); - } - size_t a_log_2d = gb->reshape(a_log_vec, {1, num_heads}); - a_log_2d = gb->precision_cast(a_log_2d, Precision::FP16); - size_t neg_exp_a = gb->scalar_multiply(gb->scalar_exp(a_log_2d), -1.0f); - gate_log = gb->multiply(neg_exp_a, a_softplus); - } else { - gate_log = gb->scalar_multiply(a_softplus, -1.0f); - } - size_t beta = gb->sigmoid(b_logits); - - size_t gate_3d = gb->reshape(gate_log, {1, seq_len, num_heads}); - size_t beta_3d = gb->reshape(beta, {1, seq_len, num_heads}); - - size_t initial_state; - - if (prev_state_flat != 0) { - initial_state = gb->reshape(prev_state_flat, {1, key_dim, num_heads, value_dim}); - } else { - initial_state = gb->input({1, key_dim, num_heads, value_dim}, Precision::FP16); - std::vector<__fp16> zeros(state_flat_dim, static_cast<__fp16>(0.0f)); - gb->set_input(initial_state, zeros.data(), Precision::FP16); - } - - size_t deltanet_out; - if (use_cache && seq_len == 1) { - deltanet_out = gb->gated_deltanet_decode(q_4d, k_4d, v_4d, gate_3d, beta_3d, initial_state, 0.0f); - } else { - const size_t chunk_for_op = std::min(64, std::max(1, seq_len)); - deltanet_out = gb->gated_deltanet_prefill( - q_4d, k_4d, v_4d, gate_3d, beta_3d, initial_state, chunk_for_op, 0.0f); - } - - size_t y_4d = gb->slice(deltanet_out, 1, 0, seq_len); - size_t state_tail = gb->slice(deltanet_out, 1, seq_len, key_dim); - - if (use_cache) { - size_t packed_cache = gb->reshape(state_tail, {1, state_flat_dim}); - if (deltanet_conv_flat_dim_ > 0) { - size_t history_2d = 0; - if (layer.deltanet_conv_weight) { - const size_t total_history_len = gb->get_output_buffer(conv_source_with_history).shape[0]; - if (total_history_len >= deltanet_conv_history_len_) { - const size_t hist_start = total_history_len - deltanet_conv_history_len_; - history_2d = gb->slice(conv_source_with_history, 0, hist_start, deltanet_conv_history_len_); - } else { - size_t pad_rows = deltanet_conv_history_len_ - total_history_len; - size_t pad = gb->input({pad_rows, mixed_proj_dim}, Precision::FP16); - std::vector<__fp16> zeros(pad_rows * mixed_proj_dim, static_cast<__fp16>(0.0f)); - gb->set_input(pad, zeros.data(), Precision::FP16); - history_2d = gb->concat(pad, conv_source_with_history, 0); - } - } else if (prev_conv_flat != 0) { - history_2d = gb->reshape(prev_conv_flat, {deltanet_conv_history_len_, mixed_proj_dim}); - history_2d = gb->precision_cast(history_2d, Precision::FP16); - } else { - history_2d = gb->input({deltanet_conv_history_len_, mixed_proj_dim}, Precision::FP16); - std::vector<__fp16> zeros(deltanet_conv_flat_dim_, static_cast<__fp16>(0.0f)); - gb->set_input(history_2d, zeros.data(), Precision::FP16); - } - - size_t history_flat = gb->reshape(history_2d, {1, deltanet_conv_flat_dim_}); - packed_cache = gb->concat(packed_cache, history_flat, 1); - } - packed_cache = gb->precision_cast(packed_cache, conv_cache_.precision); - conv_cache_state_nodes_[layer_idx] = packed_cache; - cache_k_output_nodes_[layer_idx] = 0; - cache_v_output_nodes_[layer_idx] = 0; - } else { - conv_cache_state_nodes_[layer_idx] = 0; - } - - size_t y_2d = gb->reshape(y_4d, {seq_len * num_heads, value_dim}); - if (z_proj != 0) { - size_t z_3d = gb->reshape(z_proj, {seq_len, num_heads, value_dim}); - size_t z_2d = gb->reshape(z_3d, {seq_len * num_heads, value_dim}); - y_2d = gb->multiply(gb->rms_norm(y_2d, layer.attn_q_norm_weight, config_.layer_norm_eps), gb->silu(z_2d)); - } - y_2d = gb->reshape(y_2d, {seq_len, num_heads * value_dim}); - return gb->matmul(y_2d, layer.attn_output_weight, true, backend); -} - -size_t Qwen3p5Model::build_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache, size_t position_offset) { - const auto& layer_entry = weight_nodes_.layers[layer_idx]; - const auto& layer = layer_entry.weights; - - if (layer_entry.type == WeightNodeIDs::LayerType::DELTANET) { - return build_gated_deltanet(gb, normalized_input, layer_idx, backend, use_cache, position_offset); - } - - auto q_proj = gb->matmul(normalized_input, layer.attn_q_weight, true, backend); - auto k_proj = gb->matmul(normalized_input, layer.attn_k_weight, true, backend); - auto v_proj = gb->matmul(normalized_input, layer.attn_v_weight, true, backend); - - const auto& q_shape = gb->get_output_buffer(q_proj).shape; - size_t batch_seq = q_shape[0]; - size_t num_heads = config_.attention_heads; - size_t head_dim = config_.attention_head_dim; - const size_t q_target_dim = num_heads * head_dim; - - size_t q_gate = 0; - const size_t q_proj_dim = q_shape.size() > 1 ? q_shape[1] : 0; - if (q_proj_dim == 2 * q_target_dim) { - size_t q_packed = gb->reshape(q_proj, {batch_seq, num_heads, 2 * head_dim}); - q_gate = gb->slice(q_packed, 2, head_dim, head_dim); - q_proj = gb->slice(q_packed, 2, 0, head_dim); - q_gate = gb->reshape(q_gate, {batch_seq, q_target_dim}); - q_proj = gb->reshape(q_proj, {batch_seq, q_target_dim}); - } - - const size_t q_norm_weight = gb->scalar_add(layer.attn_q_norm_weight, 1.0f); - q_proj = gb->reshape(q_proj, {batch_seq * num_heads, head_dim}); - q_proj = gb->rms_norm(q_proj, q_norm_weight, config_.layer_norm_eps); - q_proj = gb->reshape(q_proj, {batch_seq, num_heads * head_dim}); - - size_t num_kv_heads = config_.attention_kv_heads; - const size_t k_norm_weight = gb->scalar_add(layer.attn_k_norm_weight, 1.0f); - k_proj = gb->reshape(k_proj, {batch_seq * num_kv_heads, head_dim}); - k_proj = gb->rms_norm(k_proj, k_norm_weight, config_.layer_norm_eps); - k_proj = gb->reshape(k_proj, {batch_seq, num_kv_heads * head_dim}); - - size_t seq_len = batch_seq; - - auto q_proj_4d = gb->reshape(q_proj, {1, seq_len, config_.attention_heads, config_.attention_head_dim}); - auto k_proj_4d = gb->reshape(k_proj, {1, seq_len, config_.attention_kv_heads, config_.attention_head_dim}); - auto v_proj_4d = gb->reshape(v_proj, {1, seq_len, config_.attention_kv_heads, config_.attention_head_dim}); - - if (config_.rope_theta > 0) { - float rotary_factor = config_.partial_rotary_factor; - if (rotary_factor <= 0.0f) { - rotary_factor = 0.25f; - } - size_t rot_dim = static_cast(static_cast(head_dim) * rotary_factor); - rot_dim = std::min(rot_dim, head_dim); - if ((rot_dim & 1u) != 0) { - rot_dim -= 1; - } - - auto apply_partial_rope = [&](size_t x_4d) -> size_t { - if (rot_dim == 0) { - return x_4d; - } - if (rot_dim >= head_dim) { - return gb->rope(x_4d, config_.rope_theta, position_offset); - } - size_t x_rot = gb->slice(x_4d, 3, 0, rot_dim); - size_t x_tail = gb->slice(x_4d, 3, rot_dim, head_dim - rot_dim); - x_rot = gb->rope(x_rot, config_.rope_theta, position_offset); - return gb->concat(x_rot, x_tail, 3); - }; - - q_proj_4d = apply_partial_rope(q_proj_4d); - k_proj_4d = apply_partial_rope(k_proj_4d); - } - - size_t attn_output_4d; - - if (use_cache) { - cache_k_output_nodes_[layer_idx] = k_proj_4d; - cache_v_output_nodes_[layer_idx] = v_proj_4d; - conv_cache_state_nodes_[layer_idx] = 0; - } - - if (use_cache && !kv_cache_.is_empty()) { - attn_output_4d = gb->attention_int8_hybrid( - q_proj_4d, k_proj_4d, v_proj_4d, - attention_scale_, position_offset, - kv_cache_.get_keys_int8(layer_idx), - kv_cache_.get_values_int8(layer_idx), - kv_cache_.get_key_scales(layer_idx), - kv_cache_.get_value_scales(layer_idx), - kv_cache_.current_seq_len, num_kv_heads, head_dim - ); - } else { - attn_output_4d = gb->attention(q_proj_4d, k_proj_4d, v_proj_4d, attention_scale_, position_offset); - } - - auto attn_output = gb->reshape(attn_output_4d, {seq_len, config_.attention_head_dim * config_.attention_heads}); - if (q_gate != 0) { - attn_output = gb->multiply(attn_output, gb->sigmoid(q_gate)); - } - return gb->matmul(attn_output, layer.attn_output_weight, true, backend); -} - -size_t Qwen3p5Model::build_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, - ComputeBackend backend) const { - const auto& layer = weight_nodes_.layers[layer_idx].weights; - size_t gate_output = gb->matmul(normalized_h, layer.ffn_gate_weight, true, backend); - size_t up_output = gb->matmul(normalized_h, layer.ffn_up_weight, true, backend); - size_t gate_silu = gb->silu(gate_output); - size_t gated = gb->multiply(gate_silu, up_output); - return gb->matmul(gated, layer.ffn_down_weight, true, backend); -} - -size_t Qwen3p5Model::build_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache, size_t position_offset) { - const auto& layer = weight_nodes_.layers[layer_idx].weights; - const size_t input_norm_weight = gb->scalar_add(layer.input_layernorm_weight, 1.0f); - auto normalized_input = gb->rms_norm(hidden, input_norm_weight, config_.layer_norm_eps); - auto attn_output = build_attention(gb, normalized_input, layer_idx, backend, use_cache, position_offset); - auto after_attention = gb->add(hidden, attn_output); - const size_t post_attn_norm_weight = gb->scalar_add(layer.post_attention_layernorm_weight, 1.0f); - auto normalized_after_attention = gb->rms_norm(after_attention, post_attn_norm_weight, config_.layer_norm_eps); - auto mlp_output = build_mlp(gb, normalized_after_attention, layer_idx, backend); - return gb->add(after_attention, mlp_output); -} - -size_t Qwen3p5Model::forward(const std::vector& tokens, bool use_cache) { - if (!initialized_ || !graph_handle_) { - throw std::runtime_error("Model not initialized - call init() first"); - } - - if (tokens.empty()) { - throw std::runtime_error("Token sequence cannot be empty"); - } - - auto* gb = static_cast(graph_handle_); - gb->soft_reset(); - - if (conv_cache_state_nodes_.size() != config_.num_layers) { - conv_cache_state_nodes_.assign(config_.num_layers, 0); - } - std::fill(conv_cache_state_nodes_.begin(), conv_cache_state_nodes_.end(), 0); - last_forward_used_cache_ = use_cache; - if (!use_cache && conv_cache_.window_size > 0) { - conv_cache_.reset(); - deltanet_total_seq_len_ = 0; - } - - auto seq_len = static_cast(tokens.size()); - const size_t kv_pos = use_cache ? kv_cache_.get_total_seq_len() : 0; - const size_t position_offset = use_cache ? std::max(kv_pos, deltanet_total_seq_len_) : 0; - - auto backend = config_.default_backend == Config::Backend::CPU - ? ComputeBackend::CPU - : ComputeBackend::NPU; - - auto input_node_id = gb->input({seq_len}, Precision::FP32); - auto hidden = gb->embedding(embedding_node_id_, input_node_id); - - std::vector input_data(seq_len); - for (size_t i = 0; i < seq_len; i++) { - input_data[i] = static_cast(tokens[i]); - } - gb->set_input(input_node_id, input_data.data(), Precision::FP32); - - static std::set skip_layers = {}; - for (uint32_t layer_idx = 0; layer_idx < config_.num_layers; layer_idx++) { - if (skip_layers.count(layer_idx)) { - continue; - } - hidden = build_transformer_block(gb, hidden, layer_idx, backend, use_cache, position_offset); - } - - const size_t final_norm_weight = gb->scalar_add(weight_nodes_.output_norm_weight, 1.0f); - auto final_hidden = gb->rms_norm(hidden, final_norm_weight, config_.layer_norm_eps); - return final_hidden; -} - -} -} diff --git a/cactus/models/model_siglip2.cpp b/cactus/models/model_siglip2.cpp deleted file mode 100644 index 8977c4c65..000000000 --- a/cactus/models/model_siglip2.cpp +++ /dev/null @@ -1,442 +0,0 @@ -#include "model.h" -#include "../graph/graph.h" -#include "../kernel/kernel.h" -#include -#include -#include -#include - -namespace cactus { -namespace engine { - -namespace { - -size_t infer_npu_patch_capacity(const std::vector& input_shape, size_t hidden_dim) { - if (input_shape.size() < 2) { - return 0; - } - - const int first = input_shape[0]; - const int second = input_shape[1]; - if (first <= 0 || second <= 0) { - return 0; - } - - if (static_cast(second) == hidden_dim) { - return static_cast(first); - } - if (static_cast(first) == hidden_dim) { - return static_cast(second); - } - - return static_cast(first); -} - -} // namespace - -Siglip2VisionModel::Siglip2VisionModel() : Model() { - config_.model_type = Config::ModelType::SIGLIP2; -} - -Siglip2VisionModel::Siglip2VisionModel(const Config& cfg) : Model(cfg) { - Siglip2Preprocessor::Config preprocessor_config; - preprocessor_config.patch_size = static_cast(config_.vision_patch_size); - preprocessor_config.downsample_factor = static_cast(config_.downsample_factor); - preprocessor_config.min_tiles = static_cast(config_.min_tiles); - preprocessor_config.max_tiles = static_cast(config_.max_tiles); - preprocessor_config.use_thumbnail = config_.use_thumbnail; - preprocessor_config.min_image_tokens = static_cast(config_.min_image_tokens); - preprocessor_config.max_image_tokens = static_cast(config_.max_image_tokens); - preprocessor_config.max_num_patches = static_cast(config_.max_num_patches); - preprocessor_config.tile_size = static_cast(config_.tile_size); - preprocessor_config.max_pixels_tolerance = config_.max_pixels_tolerance; - preprocessor_config.do_resize = true; - preprocessor_config.do_rescale = true; - preprocessor_config.do_normalize = true; - preprocessor_config.do_convert_rgb = true; - preprocessor_config.do_image_splitting = config_.do_image_splitting; - preprocessor_config.rescale_factor = config_.rescale_factor; - preprocessor_config.image_mean[0] = config_.image_mean; - preprocessor_config.image_mean[1] = config_.image_mean; - preprocessor_config.image_mean[2] = config_.image_mean; - preprocessor_config.image_std[0] = config_.image_std; - preprocessor_config.image_std[1] = config_.image_std; - preprocessor_config.image_std[2] = config_.image_std; - - preprocessor_ = Siglip2Preprocessor(preprocessor_config); -} - -void Siglip2VisionModel::ensure_cpu_vision_weights_loaded(CactusGraph* gb) { - if (cpu_vision_weights_loaded_) { - return; - } - - std::string base = model_folder_path_ + "/"; - vision_weight_nodes_.vision_layers.resize(config_.vision_num_layers); - - vision_weight_nodes_.post_layernorm_weight = gb->mmap_weights(base + "vision_post_layernorm.weights"); - vision_weight_nodes_.post_layernorm_bias = gb->mmap_weights(base + "vision_post_layernorm.bias.weights"); - - for (uint32_t i = 0; i < vision_weight_nodes_.vision_layers.size(); ++i) { - auto& layer = vision_weight_nodes_.vision_layers[i]; - std::string prefix = base + "vision_layer_" + std::to_string(i) + "_"; - - layer.attn_q_weight = gb->mmap_weights(prefix + "self_attn_q.weights"); - layer.attn_q_bias = gb->mmap_weights(prefix + "self_attn_q.bias.weights"); - layer.attn_k_weight = gb->mmap_weights(prefix + "self_attn_k.weights"); - layer.attn_k_bias = gb->mmap_weights(prefix + "self_attn_k.bias.weights"); - layer.attn_v_weight = gb->mmap_weights(prefix + "self_attn_v.weights"); - layer.attn_v_bias = gb->mmap_weights(prefix + "self_attn_v.bias.weights"); - layer.attn_output_weight = gb->mmap_weights(prefix + "self_attn_out.weights"); - layer.attn_output_bias = gb->mmap_weights(prefix + "self_attn_out.bias.weights"); - - layer.layer_norm1_weight = gb->mmap_weights(prefix + "layer_norm1.weights"); - layer.layer_norm1_bias = gb->mmap_weights(prefix + "layer_norm1.bias.weights"); - layer.layer_norm2_weight = gb->mmap_weights(prefix + "layer_norm2.weights"); - layer.layer_norm2_bias = gb->mmap_weights(prefix + "layer_norm2.bias.weights"); - - layer.mlp_fc1_weight = gb->mmap_weights(prefix + "ffn_fc1.weights"); - layer.mlp_fc1_bias = gb->mmap_weights(prefix + "ffn_fc1.bias.weights"); - layer.mlp_fc2_weight = gb->mmap_weights(prefix + "ffn_fc2.weights"); - layer.mlp_fc2_bias = gb->mmap_weights(prefix + "ffn_fc2.bias.weights"); - } - - cpu_vision_weights_loaded_ = true; -} - -void Siglip2VisionModel::load_weights_to_graph(CactusGraph* gb) { - std::string base = model_folder_path_ + "/"; - - if (npu::is_npu_available()) { - std::string npu_encoder_path = model_folder_path_ + "/model.mlpackage"; - npu_encoder_ = npu::create_encoder(); - if (npu_encoder_ && npu_encoder_->load(npu_encoder_path)) { - use_npu_encoder_ = true; - - std::vector input_shape = npu_encoder_->get_input_shape(); - if (input_shape.empty()) { - input_shape = { - static_cast(config_.max_num_patches), - static_cast(config_.vision_embed_dim) - }; - } - npu_encoder_->preallocate(input_shape, "x", ""); - } else { - use_npu_encoder_ = false; - npu_encoder_.reset(); - } - } - - // Always load patch embedding and position embedding weights (needed for both CPU and NPU paths) - vision_weight_nodes_.patch_embedding_weight = gb->mmap_weights(base + "vision_patch_embedding.weights"); - vision_weight_nodes_.patch_embedding_bias = gb->mmap_weights(base + "vision_patch_embedding.bias.weights"); - vision_weight_nodes_.position_embedding = gb->mmap_weights(base + "vision_position_embedding.weights"); - - if (!use_npu_encoder_) { - ensure_cpu_vision_weights_loaded(gb); - } -} - -Siglip2VisionModel::VisionEmbeddingResult Siglip2VisionModel::build_vision_embeddings( - CactusGraph* gb, - const Siglip2Preprocessor::PreprocessedImage& preprocessed_image, - ComputeBackend backend) { - const int num_tiles = preprocessed_image.num_tiles; - const int max_patches = preprocessed_image.max_patches_per_tile; - const int patch_dim = preprocessed_image.patch_dim; - - const size_t expected_size = static_cast(num_tiles) * static_cast(max_patches) * - static_cast(patch_dim); - if (preprocessed_image.pixel_values.size() != expected_size) { - throw std::runtime_error( - "Pixel values size mismatch: expected " + std::to_string(expected_size) + - " (tiles=" + std::to_string(num_tiles) + " * max_patches=" + std::to_string(max_patches) + - " * patch_dim=" + std::to_string(patch_dim) + ") but got " + - std::to_string(preprocessed_image.pixel_values.size())); - } - for (size_t i = 0; i < std::min(100, preprocessed_image.pixel_values.size()); ++i) { - float val = preprocessed_image.pixel_values[i]; - if (std::isnan(val) || std::isinf(val)) { - throw std::runtime_error( - "Invalid value in pixel_values at index " + std::to_string(i) + ": " + std::to_string(val)); - } - } - - size_t reshaped_weight = gb->reshape( - vision_weight_nodes_.patch_embedding_weight, - {static_cast(config_.vision_embed_dim), static_cast(patch_dim)}); - - size_t patch_bias = vision_weight_nodes_.patch_embedding_bias; - std::vector tile_embeddings; - tile_embeddings.reserve(static_cast(num_tiles)); - - for (int tile_idx = 0; tile_idx < num_tiles; ++tile_idx) { - const auto& shape = preprocessed_image.spatial_shapes[tile_idx]; - const int tile_h = shape.first; - const int tile_w = shape.second; - const int actual_patches = tile_h * tile_w; - if (actual_patches <= 0) { - continue; - } - - const float* tile_data = preprocessed_image.pixel_values.data() + - static_cast(tile_idx) * static_cast(max_patches) * - static_cast(patch_dim); - - size_t tile_input_fp32 = gb->input( - {static_cast(actual_patches), static_cast(patch_dim)}, Precision::FP32); - gb->set_input(tile_input_fp32, tile_data, Precision::FP32); - size_t tile_input = gb->precision_cast(tile_input_fp32, Precision::FP16); - size_t tile_patch = gb->matmul(tile_input, reshaped_weight, true, backend); - size_t tile_bias = gb->add(tile_patch, patch_bias); - size_t tile_pos = gb->bilinear_interpolation( - vision_weight_nodes_.position_embedding, - static_cast(tile_h), - static_cast(tile_w), - /*align_corners=*/false); - size_t tile_pos_cast = gb->precision_cast(tile_pos, Precision::FP16); - size_t tile_embed = gb->add(tile_bias, tile_pos_cast); - tile_embeddings.push_back(tile_embed); - } - - if (tile_embeddings.empty()) { - throw std::runtime_error("No valid tiles produced embeddings in build_vision_embeddings"); - } - auto concat_nodes = [&](const std::vector& nodes) { - if (nodes.empty()) { - throw std::runtime_error("Attempted to concatenate an empty node list"); - } - size_t combined = nodes.front(); - for (size_t i = 1; i < nodes.size(); ++i) { - combined = gb->concat(combined, nodes[i], /*axis=*/0); - } - return combined; - }; - - size_t embeddings = concat_nodes(tile_embeddings); - return VisionEmbeddingResult{embeddings, std::move(tile_embeddings)}; -} - -size_t Siglip2VisionModel::build_vision_attention(CactusGraph* gb, size_t hidden_states, - uint32_t layer_idx, ComputeBackend backend) { - const auto& layer = vision_weight_nodes_.vision_layers[layer_idx]; - - size_t q = gb->matmul(hidden_states, layer.attn_q_weight, true, backend); - q = gb->add(q, layer.attn_q_bias); - size_t k = gb->matmul(hidden_states, layer.attn_k_weight, true, backend); - k = gb->add(k, layer.attn_k_bias); - size_t v = gb->matmul(hidden_states, layer.attn_v_weight, true, backend); - v = gb->add(v, layer.attn_v_bias); - const size_t num_heads = static_cast(config_.vision_attention_heads); - const size_t head_dim = static_cast(config_.vision_embed_dim / config_.vision_attention_heads); - const auto& q_buf = gb->get_output_buffer(q); - size_t seq_len = q_buf.shape[0]; - - size_t q_4d = gb->reshape(q, {1, seq_len, num_heads, head_dim}); - size_t k_4d = gb->reshape(k, {1, seq_len, num_heads, head_dim}); - size_t v_4d = gb->reshape(v, {1, seq_len, num_heads, head_dim}); - - float scale = 1.0f / std::sqrt(static_cast(head_dim)); - size_t attn_output = gb->attention(q_4d, k_4d, v_4d, scale, false, backend); - size_t attn_2d = gb->reshape(attn_output, {seq_len, num_heads * head_dim}); - size_t output = gb->matmul(attn_2d, layer.attn_output_weight, true, backend); - output = gb->add(output, layer.attn_output_bias); - return output; -} - -size_t Siglip2VisionModel::build_vision_mlp(CactusGraph* gb, size_t hidden_states, - uint32_t layer_idx, ComputeBackend backend) { - const auto& layer = vision_weight_nodes_.vision_layers[layer_idx]; - - size_t fc1_output = gb->matmul(hidden_states, layer.mlp_fc1_weight, true, backend); - fc1_output = gb->add(fc1_output, layer.mlp_fc1_bias); - size_t activated = gb->gelu(fc1_output); - size_t fc2_output = gb->matmul(activated, layer.mlp_fc2_weight, true, backend); - fc2_output = gb->add(fc2_output, layer.mlp_fc2_bias); - return fc2_output; -} - -size_t Siglip2VisionModel::build_vision_transformer_layer(CactusGraph* gb, size_t hidden_states, - uint32_t layer_idx, ComputeBackend backend) { - const auto& layer = vision_weight_nodes_.vision_layers[layer_idx]; - - size_t residual = hidden_states; - size_t normalized = gb->layernorm(hidden_states, layer.layer_norm1_weight, - layer.layer_norm1_bias, config_.layer_norm_eps); - size_t attn_output = build_vision_attention(gb, normalized, layer_idx, backend); - hidden_states = gb->add(residual, attn_output); - residual = hidden_states; - normalized = gb->layernorm(hidden_states, layer.layer_norm2_weight, - layer.layer_norm2_bias, config_.layer_norm_eps); - size_t mlp_output = build_vision_mlp(gb, normalized, layer_idx, backend); - hidden_states = gb->add(residual, mlp_output); - return hidden_states; -} - -size_t Siglip2VisionModel::forward_vision( - CactusGraph* gb, - const Siglip2Preprocessor::PreprocessedImage& preprocessed_image, - ComputeBackend backend) { - - bool can_use_npu_encoder = use_npu_encoder_ && npu_encoder_ && npu_encoder_->is_available(); - size_t total_patches = 0; - for (const auto& shape : preprocessed_image.spatial_shapes) { - total_patches += shape.first * shape.second; - } - - if (can_use_npu_encoder) { - size_t max_npu_patches = - infer_npu_patch_capacity(npu_encoder_->get_input_shape(), config_.vision_embed_dim); - if (max_npu_patches > 0 && total_patches > max_npu_patches) { - CACTUS_LOG_WARN("npu", - "[siglip2-vision] image has " - << total_patches << " patches but the NPU encoder supports " - << max_npu_patches << "; falling back to CPU vision encoder"); - can_use_npu_encoder = false; - } - } - - if (can_use_npu_encoder) { - // NPU path: build patch embeddings + position embeddings on CPU, then run transformer on NPU - auto embedding_result = build_vision_embeddings(gb, preprocessed_image, backend); - - gb->execute(); - - const auto& embed_buffer = gb->get_output_buffer(embedding_result.combined_embeddings); - void* embed_ptr = gb->get_output(embedding_result.combined_embeddings); - - std::vector<__fp16> embed_f16; - const __fp16* input_ptr; - if (embed_buffer.precision == Precision::FP16) { - input_ptr = static_cast(embed_ptr); - } else { - embed_f16.resize(total_patches * config_.vision_embed_dim); - cactus_fp32_to_fp16(static_cast(embed_ptr), - embed_f16.data(), embed_f16.size()); - input_ptr = embed_f16.data(); - } - - std::vector input_shape = { - static_cast(total_patches), - static_cast(config_.vision_embed_dim) - }; - - __fp16* output_buffer = npu_encoder_->get_output_buffer(); - if (output_buffer) { - size_t elements = npu_encoder_->encode( - input_ptr, output_buffer, input_shape, "x", ""); - - if (elements > 0) { - gb->soft_reset(); - size_t vision_output = gb->input({total_patches, config_.vision_embed_dim}, - Precision::FP16); - gb->set_external_input(vision_output, output_buffer, Precision::FP16); - return vision_output; - } - } else { - std::vector<__fp16> npu_output(total_patches * config_.vision_embed_dim); - size_t elements = npu_encoder_->encode( - input_ptr, npu_output.data(), input_shape, "x", ""); - - if (elements > 0) { - gb->soft_reset(); - size_t vision_output = gb->input({total_patches, config_.vision_embed_dim}, - Precision::FP16); - gb->set_input(vision_output, npu_output.data(), Precision::FP16); - return vision_output; - } - } - - CACTUS_LOG_WARN("npu", "[siglip2-vision] NPU encoder failed; falling back to CPU vision encoder"); - gb->soft_reset(); - } - - ensure_cpu_vision_weights_loaded(gb); - - // CPU path: full forward pass through transformer layers - auto embedding_result = build_vision_embeddings(gb, preprocessed_image, backend); - - auto concat_nodes = [&](const std::vector& nodes) { - if (nodes.empty()) { - throw std::runtime_error("Attempted to concatenate an empty node list in forward_vision"); - } - size_t combined = nodes.front(); - for (size_t i = 1; i < nodes.size(); ++i) { - combined = gb->concat(combined, nodes[i], /*axis=*/0); - } - return combined; - }; - - std::vector tile_outputs; - tile_outputs.reserve(embedding_result.tile_embeddings.size()); - - for (size_t tile_idx = 0; tile_idx < embedding_result.tile_embeddings.size(); ++tile_idx) { - size_t hidden_states = embedding_result.tile_embeddings[tile_idx]; - - for (uint32_t layer_idx = 0; layer_idx < config_.vision_num_layers; ++layer_idx) { - hidden_states = build_vision_transformer_layer(gb, hidden_states, layer_idx, backend); - } - - hidden_states = gb->layernorm(hidden_states, - vision_weight_nodes_.post_layernorm_weight, - vision_weight_nodes_.post_layernorm_bias, - config_.layer_norm_eps); - tile_outputs.push_back(hidden_states); - } - - if (tile_outputs.empty()) { - throw std::runtime_error("No tile outputs generated in forward_vision"); - } - - size_t combined_output = concat_nodes(tile_outputs); - - return combined_output; -} - -size_t Siglip2VisionModel::forward_vision(const Siglip2Preprocessor::PreprocessedImage& preprocessed_image) { - if (!initialized_ || !graph_handle_) { - throw std::runtime_error("Model not initialized - call init() first"); - } - auto* gb = static_cast(graph_handle_); - gb->soft_reset(); - auto backend = config_.default_backend == Config::Backend::CPU ? ComputeBackend::CPU : ComputeBackend::NPU; - return forward_vision(gb, preprocessed_image, backend); -} - -std::vector Siglip2VisionModel::get_image_embedding(const std::string& image_path) { - auto preprocessed = preprocessor_.preprocess_from_file(image_path); - size_t last_hidden_state = forward_vision(preprocessed); - - auto* gb = static_cast(graph_handle_); - size_t pooled = gb->mean(last_hidden_state, 0); - gb->execute(); - - const auto& output_buf = gb->get_output_buffer(pooled); - size_t hidden_dim = output_buf.total_size; - - std::vector embedding(hidden_dim); - void* output_data = gb->get_output(pooled); - const float* output_ptr = static_cast(output_data); - std::copy(output_ptr, output_ptr + hidden_dim, embedding.begin()); - return embedding; -} - -size_t Siglip2VisionModel::forward(const std::vector&, bool) {return 0;} - -size_t Siglip2VisionModel::build_attention(CactusGraph*, size_t, uint32_t, - ComputeBackend, bool, size_t) { - return 0; -} - -size_t Siglip2VisionModel::build_mlp(CactusGraph*, size_t, uint32_t, - ComputeBackend) const { - return 0; -} - -size_t Siglip2VisionModel::build_transformer_block(CactusGraph*, size_t, uint32_t, - ComputeBackend, bool, size_t) { - return 0; -} - -} -} diff --git a/cactus/models/model_silero_vad.cpp b/cactus/models/model_silero_vad.cpp deleted file mode 100644 index 7573ac2de..000000000 --- a/cactus/models/model_silero_vad.cpp +++ /dev/null @@ -1,338 +0,0 @@ -#include "model.h" -#include "../graph/graph.h" -#include "../kernel/kernel.h" -#include -#include - -namespace cactus { -namespace engine { - -SileroVADModel::SileroVADModel() : Model() { - weight_nodes_.encoder_blocks.resize(4); -} - -SileroVADModel::SileroVADModel(const Config& config) : Model(config) { - weight_nodes_.encoder_blocks.resize(4); -} - -SileroVADModel::~SileroVADModel() = default; - -void SileroVADModel::load_weights_to_graph(CactusGraph* gb) { - weight_nodes_.stft_basis = gb->mmap_weights(model_folder_path_ + "/stft_basis.weights"); - - for (uint32_t i = 0; i < 4; i++) { - std::string block_prefix = model_folder_path_ + "/encoder_block_" + std::to_string(i) + "_"; - weight_nodes_.encoder_blocks[i].conv_weight = gb->mmap_weights(block_prefix + "conv_weight.weights"); - weight_nodes_.encoder_blocks[i].conv_bias = gb->mmap_weights(block_prefix + "conv_bias.weights"); - } - - weight_nodes_.lstm_weight_ih = gb->mmap_weights(model_folder_path_ + "/lstm_weight_ih.weights"); - weight_nodes_.lstm_weight_hh = gb->mmap_weights(model_folder_path_ + "/lstm_weight_hh.weights"); - weight_nodes_.lstm_bias_ih = gb->mmap_weights(model_folder_path_ + "/lstm_bias_ih.weights"); - weight_nodes_.lstm_bias_hh = gb->mmap_weights(model_folder_path_ + "/lstm_bias_hh.weights"); - - weight_nodes_.output_conv_weight = gb->mmap_weights(model_folder_path_ + "/output_conv_weight.weights"); - weight_nodes_.output_conv_bias = gb->mmap_weights(model_folder_path_ + "/output_conv_bias.weights"); -} - -void SileroVADModel::build_graph() { - const size_t input_size = CONTEXT_SIZE + CHUNK_SIZE + REFLECT_PAD_SIZE; - - graph_nodes_.input = graph_.input({1, 1, input_size}, Precision::FP16); - graph_nodes_.h_prev = graph_.input({1, HIDDEN_SIZE}, Precision::FP16); - graph_nodes_.c_prev = graph_.input({1, HIDDEN_SIZE}, Precision::FP16); - - auto cplx = graph_.stft(graph_nodes_.input, weight_nodes_.stft_basis, 128, 129); - auto re = graph_.slice(cplx, 1, 0, 129); - auto im = graph_.slice(cplx, 1, 129, 129); - auto re_sq = graph_.multiply(re, re); - auto im_sq = graph_.multiply(im, im); - auto mag = graph_.scalar_sqrt(graph_.add(re_sq, im_sq)); - - const size_t strides[4] = {1, 2, 2, 1}; - auto x = mag; - for (uint32_t i = 0; i < 4; i++) { - auto conv = graph_.conv1d_k3(x, weight_nodes_.encoder_blocks[i].conv_weight, strides[i]); - auto bias_reshaped = graph_.reshape(weight_nodes_.encoder_blocks[i].conv_bias, - {1, graph_.get_output_buffer(weight_nodes_.encoder_blocks[i].conv_bias).shape[0], 1}); - auto with_bias = graph_.add(conv, bias_reshaped); - x = graph_.relu(with_bias); - } - - auto x_squeezed = graph_.reshape(x, {1, HIDDEN_SIZE}); - - graph_nodes_.lstm_output = graph_.lstm_cell( - x_squeezed, - graph_nodes_.h_prev, - graph_nodes_.c_prev, - weight_nodes_.lstm_weight_ih, - weight_nodes_.lstm_weight_hh, - weight_nodes_.lstm_bias_ih, - weight_nodes_.lstm_bias_hh - ); - - auto h_new = graph_.slice(graph_nodes_.lstm_output, 2, 0, 1); - auto c_new = graph_.slice(graph_nodes_.lstm_output, 2, 1, 1); - graph_nodes_.h_new = graph_.reshape(h_new, {1, HIDDEN_SIZE}); - graph_nodes_.c_new = graph_.reshape(c_new, {1, HIDDEN_SIZE}); - - auto h_relu = graph_.relu(graph_nodes_.h_new); - auto h_unsqueezed = graph_.reshape(h_relu, {1, HIDDEN_SIZE, 1}); - auto logits = graph_.conv1d(h_unsqueezed, weight_nodes_.output_conv_weight, weight_nodes_.output_conv_bias, 1); - graph_nodes_.output = graph_.sigmoid(logits); -} - -bool SileroVADModel::init(const std::string& model_folder, size_t context_size, - const std::string& system_prompt, bool do_warmup) { - (void)context_size; - (void)system_prompt; - (void)do_warmup; - - if (initialized_) { - return true; - } - - state_.h.resize(HIDDEN_SIZE, (__fp16)0.0f); - state_.c.resize(HIDDEN_SIZE, (__fp16)0.0f); - state_.context.resize(CONTEXT_SIZE, 0.0f); - state_.input_buf.resize(CONTEXT_SIZE + CHUNK_SIZE + REFLECT_PAD_SIZE, 0.0f); - state_.input_fp16.resize(CONTEXT_SIZE + CHUNK_SIZE + REFLECT_PAD_SIZE); - - model_folder_path_ = model_folder; - - try { - load_weights_to_graph(&graph_); - build_graph(); - initialized_ = true; - return true; - } catch (const std::exception& e) { - initialized_ = false; - return false; - } -} - -float SileroVADModel::process_chunk(const std::vector& audio_chunk) { - const float* audio = audio_chunk.data(); - size_t samples = audio_chunk.size(); - - if (samples != CHUNK_SIZE) { - throw std::runtime_error("SileroVAD: Expected 512 samples"); - } - if (!initialized_) { - throw std::runtime_error("SileroVAD: Not initialized"); - } - - const size_t input_size = CONTEXT_SIZE + CHUNK_SIZE + REFLECT_PAD_SIZE; - - float* input_buf = state_.input_buf.data(); - __fp16* input_fp16 = state_.input_fp16.data(); - - std::memcpy(input_buf, state_.context.data(), CONTEXT_SIZE * sizeof(float)); - std::memcpy(input_buf + CONTEXT_SIZE, audio, CHUNK_SIZE * sizeof(float)); - - for (size_t i = 0; i < REFLECT_PAD_SIZE; i++) { - input_buf[CONTEXT_SIZE + CHUNK_SIZE + i] = - input_buf[CONTEXT_SIZE + CHUNK_SIZE - 2 - i]; - } - - cactus_fp32_to_fp16(input_buf, input_fp16, input_size); - - graph_.set_input(graph_nodes_.input, input_fp16, Precision::FP16); - graph_.set_input(graph_nodes_.h_prev, state_.h.data(), Precision::FP16); - graph_.set_input(graph_nodes_.c_prev, state_.c.data(), Precision::FP16); - graph_.execute(); - - void* h_out = graph_.get_output(graph_nodes_.h_new); - void* c_out = graph_.get_output(graph_nodes_.c_new); - void* vad_out = graph_.get_output(graph_nodes_.output); - - const __fp16* h_new = static_cast(h_out); - const __fp16* c_new = static_cast(c_out); - const __fp16* vad_score = static_cast(vad_out); - - std::memcpy(state_.h.data(), h_new, HIDDEN_SIZE * sizeof(__fp16)); - std::memcpy(state_.c.data(), c_new, HIDDEN_SIZE * sizeof(__fp16)); - - float vad_score_f32 = static_cast(vad_score[0]); - - std::memcpy(state_.context.data(), input_buf + CHUNK_SIZE, CONTEXT_SIZE * sizeof(float)); - - return vad_score_f32; -} - -void SileroVADModel::reset_states() { - state_.h.assign(HIDDEN_SIZE, (__fp16)0.0f); - state_.c.assign(HIDDEN_SIZE, (__fp16)0.0f); - state_.context.assign(CONTEXT_SIZE, 0.0f); -} - -std::vector SileroVADModel::get_speech_timestamps(const std::vector& audio, const SpeechTimestampsOptions& options) { - const size_t audio_length_samples = audio.size(); - const size_t window_size_samples = options.window_size_samples; - const float min_speech_samples = options.sampling_rate * options.min_speech_duration_ms / 1000.0f; - const float speech_pad_samples = options.sampling_rate * options.speech_pad_ms / 1000.0f; - const float max_speech_samples = options.sampling_rate * options.max_speech_duration_s - window_size_samples - 2 * speech_pad_samples; - const float min_silence_samples = options.sampling_rate * options.min_silence_duration_ms / 1000.0f; - const float neg_threshold = (options.neg_threshold == 0.0f) - ? std::max(options.threshold - 0.15f, 0.01f) - : options.neg_threshold; - reset_states(); - - std::vector speech_probs; - speech_probs.reserve(audio_length_samples / window_size_samples + 1); - - std::vector chunk(window_size_samples, 0.0f); - - for (size_t current_start = 0; current_start < audio_length_samples; current_start += window_size_samples) { - size_t remaining = audio_length_samples - current_start; - size_t copy_len = std::min(remaining, window_size_samples); - - std::memcpy(chunk.data(), audio.data() + current_start, copy_len * sizeof(float)); - - if (copy_len < window_size_samples) { - std::memset(chunk.data() + copy_len, 0, (window_size_samples - copy_len) * sizeof(float)); - } - - float speech_prob = process_chunk(chunk); - speech_probs.push_back(speech_prob); - } - - bool triggered = false; - std::vector speeches; - SpeechTimestamp current_speech = {0, 0}; - size_t temp_end = 0; - size_t prev_end = 0; - size_t next_start = 0; - std::vector> possible_ends; - - const float min_silence_samples_at_max_speech = options.sampling_rate * options.min_silence_at_max_speech / 1000.0f; - - for (size_t i = 0; i < speech_probs.size(); ++i) { - float speech_prob = speech_probs[i]; - size_t cur_sample = window_size_samples * i; - - if (speech_prob >= options.threshold && temp_end) { - size_t sil_dur = cur_sample - temp_end; - if (sil_dur > min_silence_samples_at_max_speech) { - possible_ends.push_back({temp_end, sil_dur}); - } - temp_end = 0; - if (next_start < prev_end) { - next_start = cur_sample; - } - } - - if (speech_prob >= options.threshold && !triggered) { - triggered = true; - current_speech.start = cur_sample; - continue; - } - - if (triggered && (cur_sample - current_speech.start > max_speech_samples)) { - if (options.use_max_poss_sil_at_max_speech && !possible_ends.empty()) { - auto max_silence = std::max_element(possible_ends.begin(), possible_ends.end(), - [](const std::pair& a, const std::pair& b) { - return a.second < b.second; - }); - prev_end = max_silence->first; - size_t dur = max_silence->second; - current_speech.end = prev_end; - speeches.push_back(current_speech); - current_speech = {0, 0}; - next_start = prev_end + dur; - - if (next_start < prev_end + cur_sample) { - current_speech.start = next_start; - } else { - triggered = false; - } - prev_end = next_start = temp_end = 0; - possible_ends.clear(); - } else { - if (prev_end) { - current_speech.end = prev_end; - speeches.push_back(current_speech); - current_speech = {0, 0}; - if (next_start < prev_end) { - triggered = false; - } else { - current_speech.start = next_start; - } - prev_end = next_start = temp_end = 0; - possible_ends.clear(); - } else { - current_speech.end = cur_sample; - speeches.push_back(current_speech); - current_speech = {0, 0}; - prev_end = next_start = temp_end = 0; - triggered = false; - possible_ends.clear(); - continue; - } - } - } - - if (speech_prob < neg_threshold && triggered) { - if (!temp_end) { - temp_end = cur_sample; - } - size_t sil_dur_now = cur_sample - temp_end; - - if (!options.use_max_poss_sil_at_max_speech && sil_dur_now > min_silence_samples_at_max_speech) { - prev_end = temp_end; - } - - if (sil_dur_now < min_silence_samples) { - continue; - } else { - current_speech.end = temp_end; - if ((current_speech.end - current_speech.start) > min_speech_samples) { - speeches.push_back(current_speech); - } - current_speech = {0, 0}; - prev_end = next_start = temp_end = 0; - triggered = false; - possible_ends.clear(); - continue; - } - } - } - - if (triggered && (audio_length_samples - current_speech.start) > min_speech_samples) { - current_speech.end = audio_length_samples; - speeches.push_back(current_speech); - } - - for (size_t i = 0; i < speeches.size(); ++i) { - if (i == 0) { - speeches[i].start = speeches[i].start > static_cast(speech_pad_samples) - ? speeches[i].start - static_cast(speech_pad_samples) - : 0; - } - if (i != speeches.size() - 1) { - size_t silence_duration = speeches[i + 1].start - speeches[i].end; - if (silence_duration < 2 * static_cast(speech_pad_samples)) { - speeches[i].end += silence_duration / 2; - speeches[i + 1].start = speeches[i + 1].start > silence_duration / 2 - ? speeches[i + 1].start - silence_duration / 2 - : 0; - } else { - speeches[i].end = std::min(audio_length_samples, speeches[i].end + static_cast(speech_pad_samples)); - speeches[i + 1].start = speeches[i + 1].start > static_cast(speech_pad_samples) - ? speeches[i + 1].start - static_cast(speech_pad_samples) - : 0; - } - } else { - speeches[i].end = std::min(audio_length_samples, speeches[i].end + static_cast(speech_pad_samples)); - } - } - - return speeches; -} - -size_t SileroVADModel::forward(const std::vector&, const std::vector&, bool) { - return 0; -} - -} -} diff --git a/cactus/models/model_wespeaker.cpp b/cactus/models/model_wespeaker.cpp deleted file mode 100644 index 62d4642c8..000000000 --- a/cactus/models/model_wespeaker.cpp +++ /dev/null @@ -1,204 +0,0 @@ -#include "model.h" -#include "../graph/graph.h" -#include "../kernel/kernel.h" -#include - -namespace cactus { -namespace engine { - -WeSpeakerModel::WeSpeakerModel() : Model() {} -WeSpeakerModel::WeSpeakerModel(const Config& config) : Model(config) {} - -WeSpeakerModel::ResBlockWeights WeSpeakerModel::load_resblock(CactusGraph* gb, const std::string& prefix, bool has_shortcut) { - ResBlockWeights rb; - rb.has_shortcut = has_shortcut; - rb.conv1_w = gb->mmap_weights(prefix + "_conv1_weight.weights"); - rb.conv1_b = gb->mmap_weights(prefix + "_conv1_bias.weights"); - rb.conv2_w = gb->mmap_weights(prefix + "_conv2_weight.weights"); - rb.conv2_b = gb->mmap_weights(prefix + "_conv2_bias.weights"); - if (has_shortcut) { - rb.shortcut_conv_w = gb->mmap_weights(prefix + "_shortcut_0_weight.weights"); - rb.shortcut_conv_b = gb->mmap_weights(prefix + "_shortcut_0_bias.weights"); - } - return rb; -} - -size_t WeSpeakerModel::build_resblock(CactusGraph* gb, size_t x, const ResBlockWeights& rb, bool stride2) { - size_t identity = x; - - size_t out = stride2 ? gb->conv2d_k3s2p1(x, rb.conv1_w, rb.conv1_b) - : gb->conv2d_k3s1p1(x, rb.conv1_w, rb.conv1_b); - out = gb->relu(out); - out = gb->conv2d_k3s1p1(out, rb.conv2_w, rb.conv2_b); - - if (rb.has_shortcut) { - identity = gb->conv2d_k3s2p1(x, rb.shortcut_conv_w, rb.shortcut_conv_b); - } - - out = gb->add(out, identity); - out = gb->relu(out); - return out; -} - -void WeSpeakerModel::load_weights_to_graph(CactusGraph* gb) { - const std::string& p = model_folder_path_; - - weight_nodes_.conv1_w = gb->mmap_weights(p + "/resnet_conv1_weight.weights"); - weight_nodes_.conv1_b = gb->mmap_weights(p + "/resnet_conv1_bias.weights"); - - auto load_layer = [&](const std::string& layer_name, int num_blocks, bool first_has_shortcut) { - std::vector blocks; - for (int i = 0; i < num_blocks; ++i) { - std::string prefix = p + "/resnet_" + layer_name + "_" + std::to_string(i); - blocks.push_back(load_resblock(gb, prefix, i == 0 && first_has_shortcut)); - } - return blocks; - }; - - weight_nodes_.layer1 = load_layer("layer1", 3, false); - weight_nodes_.layer2 = load_layer("layer2", 4, true); - weight_nodes_.layer3 = load_layer("layer3", 6, true); - weight_nodes_.layer4 = load_layer("layer4", 3, true); - - weight_nodes_.seg1_w = gb->mmap_weights(p + "/resnet_seg_1_weight.weights"); - weight_nodes_.seg1_b = gb->mmap_weights(p + "/resnet_seg_1_bias.weights"); -} - -size_t WeSpeakerModel::build_resnet_body(CactusGraph* gb, size_t x) { - x = gb->conv2d_k3s1p1(x, weight_nodes_.conv1_w, weight_nodes_.conv1_b); - x = gb->relu(x); - - for (auto& rb : weight_nodes_.layer1) x = build_resblock(gb, x, rb, false); - for (size_t i = 0; i < weight_nodes_.layer2.size(); ++i) x = build_resblock(gb, x, weight_nodes_.layer2[i], i == 0); - for (size_t i = 0; i < weight_nodes_.layer3.size(); ++i) x = build_resblock(gb, x, weight_nodes_.layer3[i], i == 0); - for (size_t i = 0; i < weight_nodes_.layer4.size(); ++i) x = build_resblock(gb, x, weight_nodes_.layer4[i], i == 0); - return x; -} - -void WeSpeakerModel::build_graph(size_t num_frames) { - audio_input_ = graph_.input({1, 1, 80, num_frames}, Precision::FP16); - - size_t x = build_resnet_body(&graph_, audio_input_); - - x = graph_.stats_pool(x); - x = graph_.add(graph_.matmul(x, weight_nodes_.seg1_w, true), weight_nodes_.seg1_b); - - output_node_ = x; -} - -void WeSpeakerModel::build_graph_masked(size_t num_frames) { - audio_input_ = graph_.input({1, 1, 80, num_frames}, Precision::FP16); - - size_t x = build_resnet_body(&graph_, audio_input_); - - const auto& post_resnet = graph_.get_output_buffer(x); - size_t T_post = post_resnet.shape.back(); - - mask_input_ = graph_.input({1, T_post}, Precision::FP32); - - x = graph_.weighted_stats_pool(x, mask_input_); - x = graph_.add(graph_.matmul(x, weight_nodes_.seg1_w, true), weight_nodes_.seg1_b); - - output_node_ = x; -} - -bool WeSpeakerModel::init(const std::string& model_folder, size_t context_size, - const std::string& system_prompt, bool do_warmup) { - (void)context_size; - (void)system_prompt; - (void)do_warmup; - - if (initialized_) { - return true; - } - - model_folder_path_ = model_folder; - - try { - load_weights_to_graph(&graph_); - initialized_ = true; - return true; - } catch (const std::exception& e) { - initialized_ = false; - return false; - } -} - -std::vector WeSpeakerModel::embed(const float* fbank_features, size_t num_features) { - if (!initialized_) throw std::runtime_error("WeSpeaker model not initialized"); - - static constexpr size_t NUM_MEL = 80; - const size_t num_frames = num_features / NUM_MEL; - if (num_frames == 0) throw std::runtime_error("WeSpeaker: empty fbank input"); - - if (num_frames != current_num_frames_ || current_masked_) { - graph_.hard_reset(); - load_weights_to_graph(&graph_); - build_graph(num_frames); - current_num_frames_ = num_frames; - current_masked_ = false; - } - - input_buf_.resize(num_features); - for (size_t i = 0; i < num_features; ++i) - input_buf_[i] = static_cast<__fp16>(fbank_features[i]); - - graph_.set_input(audio_input_, input_buf_.data(), Precision::FP16); - graph_.execute(); - - const auto& out_buf = graph_.get_output_buffer(output_node_); - const __fp16* out_data = out_buf.data_as<__fp16>(); - size_t total = out_buf.total_size; - - std::vector result(total); - for (size_t i = 0; i < total; ++i) - result[i] = static_cast(out_data[i]); - return result; -} - -std::vector WeSpeakerModel::embed(const float* fbank_features, size_t num_features, - const float* mask_weights, size_t mask_num_frames) { - if (!initialized_) throw std::runtime_error("WeSpeaker model not initialized"); - - static constexpr size_t NUM_MEL = 80; - const size_t num_frames = num_features / NUM_MEL; - if (num_frames == 0) throw std::runtime_error("WeSpeaker: empty fbank input"); - - if (num_frames != current_num_frames_ || !current_masked_) { - graph_.hard_reset(); - load_weights_to_graph(&graph_); - build_graph_masked(num_frames); - current_num_frames_ = num_frames; - current_masked_ = true; - } - - input_buf_.resize(num_features); - for (size_t i = 0; i < num_features; ++i) - input_buf_[i] = static_cast<__fp16>(fbank_features[i]); - graph_.set_input(audio_input_, input_buf_.data(), Precision::FP16); - - const auto& mask_shape = graph_.get_output_buffer(mask_input_); - size_t T_post = mask_shape.shape.back(); - - mask_buf_.resize(T_post); - for (size_t t = 0; t < T_post; ++t) { - size_t src_t = (t * mask_num_frames) / T_post; - if (src_t >= mask_num_frames) src_t = mask_num_frames - 1; - mask_buf_[t] = mask_weights[src_t]; - } - graph_.set_input(mask_input_, mask_buf_.data(), Precision::FP32); - - graph_.execute(); - - const auto& out_buf = graph_.get_output_buffer(output_node_); - const __fp16* out_data = out_buf.data_as<__fp16>(); - size_t total = out_buf.total_size; - - std::vector result(total); - for (size_t i = 0; i < total; ++i) - result[i] = static_cast(out_data[i]); - return result; -} - -} -} diff --git a/cactus/models/model_whisper.cpp b/cactus/models/model_whisper.cpp deleted file mode 100644 index 70ea31c92..000000000 --- a/cactus/models/model_whisper.cpp +++ /dev/null @@ -1,778 +0,0 @@ -#include "model.h" -#include "../graph/graph.h" -#include "../npu/npu.h" -#include "../kernel/kernel.h" -#include -#include -#include -#include -#include -#include - -namespace { - -bool has_whisper_cpu_encoder_weights(const std::string& model_folder_path) { - const std::filesystem::path model_path(model_folder_path); - return std::filesystem::exists(model_path / "encoder_conv1_weight.weights") && - std::filesystem::exists(model_path / "encoder_position_embeddings.weights"); -} - -} // namespace - -namespace cactus { -namespace engine { - -WhisperModel::WhisperModel() : Model() {} - -WhisperModel::WhisperModel(const Config& config) : Model(config) { - enc_layers_ = config.num_encoder_layers > 0 ? config.num_encoder_layers : config.num_layers; - dec_layers_ = config.num_decoder_layers > 0 ? config.num_decoder_layers : config.num_layers; - - weight_nodes_.layers.resize(std::max(enc_layers_, dec_layers_)); - - float hd = static_cast(config.attention_head_dim); - if (hd <= 0.0f) { - hd = 64.0f; - } - - attention_scale_ = 1.0f / std::sqrt(hd); - - encoder_block_out_nodes_.resize(enc_layers_, 0); - encoder_k_persistent_.assign(enc_layers_, 0); - encoder_v_persistent_.assign(enc_layers_, 0); - - const float neg_inf = -std::numeric_limits::infinity(); - for (size_t tok : suppress_tokens_) { - suppress_bias_[static_cast(tok)] = neg_inf; - } - suppress_bias_first_step_ = suppress_bias_; - for (size_t tok : begin_suppress_tokens_) { - suppress_bias_first_step_[static_cast(tok)] = neg_inf; - } -} - -void WhisperModel::load_weights_to_graph(CactusGraph* gb) { - - embedding_node_id_ = gb->mmap_embeddings(embedding_file_path_); - - weight_nodes_.decoder_norm_weight = gb->mmap_weights(model_folder_path_ + "/decoder_norm.weights"); - weight_nodes_.decoder_norm_bias = gb->mmap_weights(model_folder_path_ + "/decoder_norm.bias"); - weight_nodes_.decoder_position_embeddings_weight = gb->mmap_weights(model_folder_path_ + "/decoder_position_embeddings.weights"); - - if (config_.tie_word_embeddings) { - weight_nodes_.output_weight = embedding_node_id_; - output_weight_node_id_ = embedding_node_id_; - } else { - weight_nodes_.output_weight = gb->mmap_weights(model_folder_path_ + "/output_weight.weights"); - output_weight_node_id_ = weight_nodes_.output_weight; - } - - if (npu::is_npu_available()) { - std::string npu_encoder_path = model_folder_path_ + "/model.mlpackage"; - npu_encoder_ = npu::create_encoder(); - if (npu_encoder_ && npu_encoder_->load(npu_encoder_path)) { - use_npu_encoder_ = true; - - std::vector typical_input_shape = {1, static_cast(config_.num_mel_bins), 3000}; - npu_encoder_->preallocate(typical_input_shape, "x", ""); - } else { - use_npu_encoder_ = false; - npu_encoder_.reset(); - } - } - - const bool has_cpu_encoder_weights = has_whisper_cpu_encoder_weights(model_folder_path_); - if (has_cpu_encoder_weights) { - weight_nodes_.encoder_position_embeddings = gb->mmap_weights(model_folder_path_ + "/encoder_position_embeddings.weights"); - weight_nodes_.encoder_conv1_weight = gb->mmap_weights(model_folder_path_ + "/encoder_conv1_weight.weights"); - weight_nodes_.encoder_conv1_bias = gb->mmap_weights(model_folder_path_ + "/encoder_conv1_bias.bias"); - weight_nodes_.encoder_conv2_weight = gb->mmap_weights(model_folder_path_ + "/encoder_conv2_weight.weights"); - weight_nodes_.encoder_conv2_bias = gb->mmap_weights(model_folder_path_ + "/encoder_conv2_bias.bias"); - weight_nodes_.encoder_norm_weight = gb->mmap_weights(model_folder_path_ + "/encoder_norm_weight.weights"); - weight_nodes_.encoder_norm_bias = gb->mmap_weights(model_folder_path_ + "/encoder_norm_bias.bias"); - } - - const uint32_t max_layers = std::max(enc_layers_, dec_layers_); - for (uint32_t i = 0; i < max_layers; i++) { - auto& layer = weight_nodes_.layers[i]; - - if (i < dec_layers_) { - // Decoder Layers - std::string layer_prefix = model_folder_path_ + "/decoder.layer_" + std::to_string(i) + "_"; - - layer.decoder_encoder_attn_k_weight = gb->mmap_weights(layer_prefix + "encoder_attn_k.weights"); - layer.decoder_encoder_attn_q_weight = gb->mmap_weights(layer_prefix + "encoder_attn_q.weights"); - layer.decoder_encoder_attn_v_weight = gb->mmap_weights(layer_prefix + "encoder_attn_v.weights"); - layer.decoder_encoder_attn_output_weight = gb->mmap_weights(layer_prefix + "encoder_attn_output.weights"); - layer.decoder_encoder_attn_q_bias = gb->mmap_weights(layer_prefix + "encoder_attn_q.bias"); - layer.decoder_encoder_attn_v_bias = gb->mmap_weights(layer_prefix + "encoder_attn_v.bias"); - layer.decoder_encoder_attn_output_bias = gb->mmap_weights(layer_prefix + "encoder_attn_output.bias"); - - layer.decoder_post_encoder_layernorm_weight = gb->mmap_weights(layer_prefix + "encoder_attn_norm.weights"); - layer.decoder_post_encoder_layernorm_bias = gb->mmap_weights(layer_prefix + "encoder_attn_norm.bias"); - - layer.decoder_ffn1_weight = gb->mmap_weights(layer_prefix + "mlp_fc1.weights"); - layer.decoder_ffn1_bias = gb->mmap_weights(layer_prefix + "mlp_fc1.bias"); - layer.decoder_ffn2_weight = gb->mmap_weights(layer_prefix + "mlp_fc2.weights"); - layer.decoder_ffn2_bias = gb->mmap_weights(layer_prefix + "mlp_fc2.bias"); - - layer.decoder_post_ffn_layernorm_weight = gb->mmap_weights(layer_prefix + "final_norm.weights"); - layer.decoder_post_ffn_layernorm_bias = gb->mmap_weights(layer_prefix + "final_norm.bias"); - - layer.decoder_self_attn_k_weight = gb->mmap_weights(layer_prefix + "self_attn_k.weights"); - layer.decoder_self_attn_q_weight = gb->mmap_weights(layer_prefix + "self_attn_q.weights"); - layer.decoder_self_attn_v_weight = gb->mmap_weights(layer_prefix + "self_attn_v.weights"); - layer.decoder_self_attn_output_weight = gb->mmap_weights(layer_prefix + "self_attn_output.weights"); - layer.decoder_self_attn_q_bias = gb->mmap_weights(layer_prefix + "self_attn_q.bias"); - layer.decoder_self_attn_v_bias = gb->mmap_weights(layer_prefix + "self_attn_v.bias"); - layer.decoder_self_attn_output_bias = gb->mmap_weights(layer_prefix + "self_attn_output.bias"); - - layer.decoder_post_attn_layernorm_weight = gb->mmap_weights(layer_prefix + "self_attn_norm.weights"); - layer.decoder_post_attn_layernorm_bias = gb->mmap_weights(layer_prefix + "self_attn_norm.bias"); - } - - if (has_cpu_encoder_weights && i < enc_layers_) { - std::string layer_prefix = model_folder_path_ + "/encoder.layer_" + std::to_string(i) + "_"; - - layer.encoder_ffn1_weight = gb->mmap_weights(layer_prefix + "mlp_fc1.weights"); - layer.encoder_ffn1_bias = gb->mmap_weights(layer_prefix + "mlp_fc1.bias"); - layer.encoder_ffn2_weight = gb->mmap_weights(layer_prefix + "mlp_fc2.weights"); - layer.encoder_ffn2_bias = gb->mmap_weights(layer_prefix + "mlp_fc2.bias"); - - layer.encoder_post_ffn_layernorm_weight = gb->mmap_weights(layer_prefix + "final_norm.weights"); - layer.encoder_post_ffn_layernorm_bias = gb->mmap_weights(layer_prefix + "final_norm.bias"); - - layer.encoder_self_attn_k_weight = gb->mmap_weights(layer_prefix + "self_attn_k.weights"); - layer.encoder_self_attn_q_weight = gb->mmap_weights(layer_prefix + "self_attn_q.weights"); - layer.encoder_self_attn_v_weight = gb->mmap_weights(layer_prefix + "self_attn_v.weights"); - layer.encoder_self_attn_output_weight = gb->mmap_weights(layer_prefix + "self_attn_output.weights"); - layer.encoder_self_attn_q_bias = gb->mmap_weights(layer_prefix + "self_attn_q.bias"); - layer.encoder_self_attn_v_bias = gb->mmap_weights(layer_prefix + "self_attn_v.bias"); - layer.encoder_self_attn_output_bias = gb->mmap_weights(layer_prefix + "self_attn_output.bias"); - - layer.encoder_post_attn_layernorm_weight = gb->mmap_weights(layer_prefix + "self_attn_norm.weights"); - layer.encoder_post_attn_layernorm_bias = gb->mmap_weights(layer_prefix + "self_attn_norm.bias"); - } - } -} - -size_t WhisperModel::build_encoder_mlp(CactusGraph* gb, size_t input, uint32_t layer_idx, ComputeBackend backend) { - const auto& layer = weight_nodes_.layers[layer_idx]; - - auto ffn1_weight = gb->matmul(input, layer.encoder_ffn1_weight, true, backend); - auto ffn1_bias = gb->add(ffn1_weight, layer.encoder_ffn1_bias); - - encoder_pre_gelu = ffn1_bias; - - auto ffn1_act = gb->gelu_erf(ffn1_bias); - - encoder_post_gelu = ffn1_act; - - auto ffn2_weight = gb->matmul(ffn1_act, layer.encoder_ffn2_weight, true, backend); - auto ffn2_bias = gb->add(ffn2_weight, layer.encoder_ffn2_bias); - return ffn2_bias; -} - -size_t WhisperModel::build_decoder_mlp(CactusGraph* gb, size_t input, uint32_t layer_idx, ComputeBackend backend) const { - const auto& layer = weight_nodes_.layers[layer_idx]; - auto ffn1_weight = gb->matmul(input, layer.decoder_ffn1_weight, true, backend); - auto ffn1_bias = gb->add(ffn1_weight, layer.decoder_ffn1_bias); - auto ffn1_act = gb->gelu_erf(ffn1_bias); - auto ffn2_weight = gb->matmul(ffn1_act, layer.decoder_ffn2_weight, true, backend); - auto ffn2_bias = gb->add(ffn2_weight, layer.decoder_ffn2_bias); - return ffn2_bias; -} - - - -size_t WhisperModel::build_encoder_attention(CactusGraph* gb, size_t input, uint32_t layer_idx, ComputeBackend backend, bool /*use_cache*/, size_t /*position_offset*/){ - - const auto& layer = weight_nodes_.layers[layer_idx]; - - size_t q = gb->matmul(input, layer.decoder_encoder_attn_q_weight, true, backend); - q = gb->add(q, layer.decoder_encoder_attn_q_bias); - - const auto& q_buf = gb->get_output_buffer(q); - if (q_buf.shape.size() != 2) { - throw std::runtime_error("encoder cross-attn: q must be [T_dec, D]"); - } - - size_t T_dec = q_buf.shape[0]; - size_t q_heads = config_.attention_heads; - size_t kv_heads = config_.attention_kv_heads; - size_t head_dim = config_.attention_head_dim; - - q = gb->reshape(q, {1, T_dec, q_heads, head_dim}); - - size_t k_4d = 0; - size_t v_4d = 0; - - bool is_populated = (encoder_k_persistent_[layer_idx] != 0 && - gb->is_populated(encoder_k_persistent_[layer_idx])); - - if (is_populated) { - k_4d = encoder_k_persistent_[layer_idx]; - v_4d = encoder_v_persistent_[layer_idx]; - } else { - size_t enc_norm = last_encoder_post_norm_node_; - - size_t k = gb->matmul(enc_norm, layer.decoder_encoder_attn_k_weight, true, backend); - size_t v = gb->matmul(enc_norm, layer.decoder_encoder_attn_v_weight, true, backend); - v = gb->add(v, layer.decoder_encoder_attn_v_bias); - - const auto& k_buf = gb->get_output_buffer(k); - if (k_buf.shape.size() != 2) { - throw std::runtime_error("encoder cross-attn: k must be [T_enc, D]"); - } - size_t T_enc = k_buf.shape[0]; - - k_4d = gb->reshape(k, {1, T_enc, kv_heads, head_dim}); - v_4d = gb->reshape(v, {1, T_enc, kv_heads, head_dim}); - - if (encoder_k_persistent_[layer_idx] == 0) { - encoder_k_persistent_[layer_idx] = gb->persistent(k_4d); - encoder_v_persistent_[layer_idx] = gb->persistent(v_4d); - } - k_4d = encoder_k_persistent_[layer_idx]; - v_4d = encoder_v_persistent_[layer_idx]; - } - - size_t attn = gb->attention(q, k_4d, v_4d, attention_scale_, false); - - attn = gb->reshape(attn, {T_dec, q_heads * head_dim}); - size_t out = gb->matmul(attn, layer.decoder_encoder_attn_output_weight, true, backend); - out = gb->add(out, layer.decoder_encoder_attn_output_bias); - - return out; -} - -void WhisperModel::reset_graph_side_cache_nodes() { - cache_k_output_nodes_.assign(dec_layers_, 0); - cache_v_output_nodes_.assign(dec_layers_, 0); -} - -void WhisperModel::reset_cache() { - Model::reset_cache(); - encoder_ready_ = false; - first_decode_step_ = true; - - auto* gb = static_cast(graph_handle_); - if (gb) { - if (encoder_output_persistent_ != 0) { - gb->invalidate_persistent(encoder_output_persistent_); - encoder_output_persistent_ = 0; - } - for (size_t i = 0; i < encoder_k_persistent_.size(); ++i) { - if (encoder_k_persistent_[i] != 0) { - gb->invalidate_persistent(encoder_k_persistent_[i]); - encoder_k_persistent_[i] = 0; - } - if (encoder_v_persistent_[i] != 0) { - gb->invalidate_persistent(encoder_v_persistent_[i]); - encoder_v_persistent_[i] = 0; - } - } - } -} - -size_t WhisperModel::build_decoder_self_attention(CactusGraph* gb, size_t input, uint32_t layer_idx, ComputeBackend backend, bool use_cache, size_t position_offset){ - const auto& layer = weight_nodes_.layers[layer_idx]; - - auto q = gb->matmul(input, layer.decoder_self_attn_q_weight, true, backend); - q = gb->add(q, layer.decoder_self_attn_q_bias); - - auto k = gb->matmul(input, layer.decoder_self_attn_k_weight, true, backend); - auto v = gb->matmul(input, layer.decoder_self_attn_v_weight, true, backend); - v = gb->add(v, layer.decoder_self_attn_v_bias); - - const auto& q_shape = gb->get_output_buffer(q).shape; - if (q_shape.size() != 2) { - throw std::runtime_error("decoder self-attn: q must be [T_new, D]"); - } - - size_t seq_new = q_shape[0]; - size_t num_heads = config_.attention_heads; - size_t head_dim = config_.attention_head_dim; - size_t num_kv_heads = config_.attention_kv_heads; - - auto q_4d = gb->reshape(q, {1, seq_new, num_heads, head_dim}); - auto k_4d = gb->reshape(k, {1, seq_new, num_kv_heads, head_dim}); - auto v_4d = gb->reshape(v, {1, seq_new, num_kv_heads, head_dim}); - - size_t final_k = k_4d; - size_t final_v = v_4d; - - if (use_cache && !kv_cache_.is_empty()) { - auto k_view = kv_cache_.get_key_view(layer_idx); - auto v_view = kv_cache_.get_value_view(layer_idx); - - if (!k_view.ptr1 || !v_view.ptr1) { - throw std::runtime_error("KV cache view is empty but kv_cache_.is_empty()==false"); - } - - size_t cache_len = kv_cache_.current_seq_len; - - size_t cache_k_node = gb->input( - {1, cache_len, num_kv_heads, head_dim}, - kv_cache_.precision - ); - size_t cache_v_node = gb->input( - {1, cache_len, num_kv_heads, head_dim}, - kv_cache_.precision - ); - - if (k_view.ptr2 == nullptr && v_view.ptr2 == nullptr) { - gb->set_external_input(cache_k_node, const_cast(k_view.ptr1), kv_cache_.precision); - gb->set_external_input(cache_v_node, const_cast(v_view.ptr1), kv_cache_.precision); - } else { - gb->set_external_input(cache_k_node, kv_cache_.get_key_ptr(layer_idx), kv_cache_.precision); - gb->set_external_input(cache_v_node, kv_cache_.get_value_ptr(layer_idx), kv_cache_.precision); - } - - final_k = gb->concat(cache_k_node, k_4d, 1); - final_v = gb->concat(cache_v_node, v_4d, 1); - } - - if (use_cache) { - cache_k_output_nodes_[layer_idx] = final_k; - cache_v_output_nodes_[layer_idx] = final_v; - } else { - cache_k_output_nodes_[layer_idx] = k_4d; - cache_v_output_nodes_[layer_idx] = v_4d; - } - - auto attn_out_4d = gb->attention(q_4d, final_k, final_v, attention_scale_, position_offset); - auto attn_out = gb->reshape(attn_out_4d, {seq_new, num_heads * head_dim}); - - auto output = gb->matmul(attn_out, layer.decoder_self_attn_output_weight, true, backend); - output = gb->add(output, layer.decoder_self_attn_output_bias); - return output; -} - -size_t WhisperModel::build_encoder_self_attention(CactusGraph* gb, size_t input, uint32_t layer_idx, ComputeBackend backend, bool use_cache, size_t /*position_offset*/){ - const auto& layer = weight_nodes_.layers[layer_idx]; - - if(use_cache) - throw std::runtime_error("The encoder attention layers are not auto-regressive, and thus don't use KV caching!"); - - auto q = gb->matmul(input, layer.encoder_self_attn_q_weight, true, backend); - q = gb->add(q, layer.encoder_self_attn_q_bias); - auto v = gb->matmul(input, layer.encoder_self_attn_v_weight, true, backend); - v = gb->add(v, layer.encoder_self_attn_v_bias); - auto k = gb->matmul(input, layer.encoder_self_attn_k_weight, true, backend); - - size_t seq_len = gb->get_output_buffer(q).shape[0]; - size_t num_heads = config_.attention_heads; - size_t head_dim = config_.attention_head_dim; - - q = gb->reshape(q, {1, seq_len, num_heads, head_dim}); - k = gb->reshape(k, {1, seq_len, num_heads, head_dim}); - v = gb->reshape(v, {1, seq_len, num_heads, head_dim}); - - auto attn = gb->attention(q, k, v, attention_scale_, false); - - attn = gb->reshape(attn, {seq_len, num_heads * head_dim}); - - auto output = gb->matmul(attn, layer.encoder_self_attn_output_weight, true, backend); - output = gb->add(output, layer.encoder_self_attn_output_bias); - - return output; -} - -size_t WhisperModel::build_conv1d(CactusGraph* gb, size_t input) -{ - size_t conv_input = input; - const auto& xbuf = gb->get_output_buffer(input); - - if (xbuf.precision == Precision::INT8) { - conv_input = gb->precision_cast(input, Precision::FP16); - } - - size_t conv1 = gb->conv1d_k3(conv_input, weight_nodes_.encoder_conv1_weight, 1); - - auto bias1_shape = gb->get_output_buffer(weight_nodes_.encoder_conv1_bias).shape; - size_t C1 = bias1_shape[0]; - size_t bias1 = gb->reshape(weight_nodes_.encoder_conv1_bias, {1, C1, 1}); - conv1 = gb->add(conv1, bias1); - - last_conv1_node_ = conv1; - - conv1 = gb->gelu_erf(conv1); - - size_t conv2 = gb->conv1d_k3(conv1, weight_nodes_.encoder_conv2_weight, 2); - auto bias2_shape = gb->get_output_buffer(weight_nodes_.encoder_conv2_bias).shape; - size_t C2 = bias2_shape[0]; - size_t bias2 = gb->reshape(weight_nodes_.encoder_conv2_bias, {1, C2, 1}); - conv2 = gb->add(conv2, bias2); - - last_conv2_node_ = conv2; - - conv2 = gb->gelu_erf(conv2); - - const auto& buf = gb->get_output_buffer(conv2); - - size_t conv2_transposed; - if (buf.precision == Precision::FP16) { - conv2_transposed = gb->transpose(conv2, ComputeBackend::CPU); - } else { - size_t conv2_f16 = gb->precision_cast(conv2, Precision::FP16); - conv2_transposed = gb->transpose(conv2_f16, ComputeBackend::CPU); - } - - return conv2_transposed; -} - -size_t WhisperModel::build_encoder_transformer_block( - CactusGraph* gb, - size_t hidden, - uint32_t layer_idx, - ComputeBackend backend, - bool use_cache, - size_t position_offset) -{ - const auto& layer = weight_nodes_.layers[layer_idx]; - - size_t ln1 = gb->layernorm( - hidden, - layer.encoder_post_attn_layernorm_weight, - layer.encoder_post_attn_layernorm_bias - ); - - size_t sa = build_encoder_self_attention( - gb, ln1, layer_idx, backend, use_cache, position_offset - ); - - size_t x_post_sa = gb->add(hidden, sa); - - size_t ln2 = gb->layernorm( - x_post_sa, - layer.encoder_post_ffn_layernorm_weight, - layer.encoder_post_ffn_layernorm_bias - ); - - size_t ffn_out = build_encoder_mlp( - gb, ln2, layer_idx, backend - ); - - size_t out = gb->add(x_post_sa, ffn_out); - - if (layer_idx < encoder_block_out_nodes_.size()) { - encoder_block_out_nodes_[layer_idx] = out; - } - - return out; -} - -size_t WhisperModel::build_decoder_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, ComputeBackend backend, bool use_cache, size_t position_offset){ - const auto& layer = weight_nodes_.layers[layer_idx]; - - size_t ln1 = gb->layernorm(hidden, layer.decoder_post_attn_layernorm_weight, layer.decoder_post_attn_layernorm_bias); - size_t sa = build_decoder_self_attention(gb, ln1, layer_idx, backend, use_cache, position_offset); - size_t x_post_sa = gb->add(hidden, sa); - - size_t ln2 = gb->layernorm(x_post_sa, layer.decoder_post_encoder_layernorm_weight, layer.decoder_post_encoder_layernorm_bias); - size_t ca = build_encoder_attention(gb, ln2, layer_idx, backend, use_cache, position_offset); - size_t x_post_ca = gb->add(x_post_sa, ca); - - size_t ln3 = gb->layernorm(x_post_ca,layer.decoder_post_ffn_layernorm_weight,layer.decoder_post_ffn_layernorm_bias); - size_t ffn_out = build_decoder_mlp(gb, ln3, layer_idx, backend); - size_t x_post_ffn = gb->add(x_post_ca, ffn_out); - - return x_post_ffn; - -} - -void WhisperModel::run_encoder(const std::vector& audio_features) -{ - const size_t mel_bins = config_.num_mel_bins; - if (audio_features.size() % mel_bins != 0) - throw std::runtime_error("Mel bins length must be divisible by " + std::to_string(mel_bins) + "."); - - size_t T_mel = audio_features.size() / mel_bins; - if (T_mel == 0) - throw std::runtime_error("Mel bins has zero frames."); - - auto* gb = static_cast(graph_handle_); - if (!gb) - throw std::runtime_error("Graph handle is null in run_encoder."); - - if (use_npu_encoder_ && npu_encoder_ && npu_encoder_->is_available()) { - std::vector out_shape = npu_encoder_->get_output_shape(); - size_t T_enc, D_enc; - if (out_shape.size() == 3) { - T_enc = static_cast(out_shape[1]); - D_enc = static_cast(out_shape[2]); - } else if (out_shape.size() == 2) { - T_enc = static_cast(out_shape[0]); - D_enc = static_cast(out_shape[1]); - } else { - throw std::runtime_error("NPU encoder output has unexpected shape"); - } - - std::vector<__fp16> audio_features_f16(audio_features.size()); - cactus_fp32_to_fp16(audio_features.data(), audio_features_f16.data(), audio_features.size()); - - std::vector input_shape = {1, static_cast(mel_bins), static_cast(T_mel)}; - size_t total_elements = T_enc * D_enc; - std::vector<__fp16> npu_output(total_elements); - size_t elements_written = npu_encoder_->encode( - audio_features_f16.data(), - npu_output.data(), - input_shape, - "x", - "" - ); - - if (elements_written > 0) { - size_t enc_output_node = gb->input({T_enc, D_enc}, Precision::FP16); - gb->set_input(enc_output_node, npu_output.data(), Precision::FP16); - - size_t enc_output_persistent = gb->persistent(enc_output_node); - encoder_output_persistent_ = enc_output_persistent; - last_encoder_post_norm_node_ = enc_output_persistent; - return; - } - } - - if (!has_whisper_cpu_encoder_weights(model_folder_path_)) { - throw std::runtime_error( - "Whisper requires either CPU encoder weights or model.mlpackage encoder output."); - } - - auto backend = - (config_.default_backend == Config::Backend::CPU) - ? ComputeBackend::CPU - : ComputeBackend::NPU; - - size_t mel_input = 0; - std::vector<__fp16> audio_features_f16(audio_features.size()); - cactus_fp32_to_fp16(audio_features.data(), audio_features_f16.data(), audio_features.size()); - - mel_input = gb->input({1, mel_bins, T_mel}, Precision::FP16); - gb->set_input(mel_input, audio_features_f16.data(), Precision::FP16); - - size_t conv2_transposed = build_conv1d(gb, mel_input); - - const auto& conv_shape = gb->get_output_buffer(conv2_transposed).shape; - if (conv_shape.size() != 3 || conv_shape[0] != 1) - throw std::runtime_error("Conv2 transpose should be [1, T_enc, D]."); - - size_t T_enc = conv_shape[1]; - size_t D_enc = conv_shape[2]; - - size_t pos_slice = gb->slice(weight_nodes_.encoder_position_embeddings, 0, 0, T_enc); - - size_t h2d = gb->reshape(conv2_transposed, {T_enc, D_enc}); - - auto& h2d_buf = gb->get_output_buffer(h2d); - auto& pos_buf = gb->get_output_buffer(pos_slice); - - if (pos_buf.precision != h2d_buf.precision) { - pos_slice = gb->precision_cast(pos_slice, h2d_buf.precision); - } - - size_t h_pos = gb->add(h2d, pos_slice); - last_enc_plus_pos_node_ = h_pos; - - size_t h = h_pos; - for (uint32_t i = 0; i < enc_layers_; ++i){ - h = build_encoder_transformer_block(gb, h, i, backend, false, 0); - if (i == 0) { - encoder_transformer_block_0 = h; - } - } - - size_t h_norm = gb->layernorm( - h, - weight_nodes_.encoder_norm_weight, - weight_nodes_.encoder_norm_bias - ); - - size_t h_norm_persistent = gb->persistent(h_norm); - encoder_output_persistent_ = h_norm_persistent; - last_encoder_post_norm_node_ = h_norm_persistent; -} - - - -size_t WhisperModel::run_decoder_step(const std::vector& tokens, bool use_cache, bool last_token_only) { - auto* gb = static_cast(graph_handle_); - - const size_t full_len = tokens.size(); - if (full_len == 0) { - throw std::runtime_error("Decoder token list cannot be empty."); - } - - auto backend = config_.default_backend == Config::Backend::CPU - ? ComputeBackend::CPU - : ComputeBackend::NPU; - - size_t start_idx = (use_cache && kv_cache_.current_seq_len > 0) ? full_len - 1 : 0; - size_t new_tokens = full_len - start_idx; - - size_t tok_input = gb->input({new_tokens}, Precision::FP32); - std::vector tok_f(new_tokens); - for (size_t i = 0; i < new_tokens; i++) { - tok_f[i] = static_cast(tokens[start_idx + i]); - } - gb->set_input(tok_input, tok_f.data(), Precision::FP32); - - size_t dec_hidden = gb->embedding(embedding_node_id_, tok_input); - - size_t position_offset = kv_cache_.current_seq_len; - size_t dec_pos = gb->slice(weight_nodes_.decoder_position_embeddings_weight,0,position_offset,new_tokens); - - { - const auto& h_buf = gb->get_output_buffer(dec_hidden); - const auto& pos_buf = gb->get_output_buffer(dec_pos); - - size_t pos_node_for_add = dec_pos; - if (pos_buf.precision != h_buf.precision) { - pos_node_for_add = gb->precision_cast(dec_pos, h_buf.precision); - } - - dec_hidden = gb->add(dec_hidden, pos_node_for_add); - } - - for (uint32_t layer_idx = 0; layer_idx < dec_layers_; ++layer_idx) { - dec_hidden = build_decoder_transformer_block( - gb, - dec_hidden, - layer_idx, - backend, - use_cache, - position_offset - ); - } - - size_t dec_norm = gb->layernorm( - dec_hidden, - weight_nodes_.decoder_norm_weight, - weight_nodes_.decoder_norm_bias - ); - - size_t logits_input = dec_norm; - if (last_token_only) { - size_t row_index = new_tokens - 1; - logits_input = gb->slice(logits_input, 0, row_index, 1); - } - - auto w_shape = gb->get_output_buffer(output_weight_node_id_).shape; - - size_t logits = gb->matmul(logits_input, output_weight_node_id_, true, backend); - - last_new_tokens_ = new_tokens; - return logits; -} - - -size_t WhisperModel::forward(const std::vector& audio_features, const std::vector& tokens, bool use_cache) -{ - - if (!initialized_ || !graph_handle_) { - throw std::runtime_error("Model not initialized"); - } - - if (!use_cache) { - kv_cache_.reset(); - kv_cache_.current_seq_len = 0; - reset_graph_side_cache_nodes(); - run_encoder(audio_features); - } - - return run_decoder_step(tokens, use_cache, false); -} - -std::vector WhisperModel::get_audio_embeddings(const std::vector& audio_features) { - run_encoder(audio_features); - - auto* gb = static_cast(graph_handle_); - - size_t pooled = gb->mean(encoder_output_persistent_, 0); - gb->execute(); - - const auto& output_buf = gb->get_output_buffer(pooled); - size_t hidden_dim = output_buf.total_size; - - std::vector embedding(hidden_dim); - void* output_data = gb->get_output(pooled); - const float* output_ptr = static_cast(output_data); - std::copy(output_ptr, output_ptr + hidden_dim, embedding.begin()); - - reset_cache(); - return embedding; -} - -uint32_t WhisperModel::decode_with_audio( - const std::vector& tokens, - const std::vector& audio_features, - float temperature, - float top_p, - size_t top_k, - const std::string& profile_file, - float* out_entropy, - float min_p, - float repetition_penalty, - float* /*out_token_time_start*/, - float* /*out_token_time_end*/) -{ - if (!initialized_ || !graph_handle_) - throw std::runtime_error("Model not initialized - call init() first"); - if (audio_features.empty()) - throw std::runtime_error("Mel bins cannot be empty in Whisper decode_with_audio"); - - auto* gb = static_cast(graph_handle_); - - bool cold_start = !encoder_ready_; - size_t logits_node = 0; - - std::vector full_tokens; - if (tokens.empty()) { - full_tokens.push_back(static_cast(get_tokenizer()->get_bos_token())); - } else { - full_tokens = tokens; - } - - if (cold_start) - { - gb->soft_reset(); - kv_cache_.reset(); - kv_cache_.current_seq_len = 0; - reset_graph_side_cache_nodes(); - - first_decode_step_ = true; - run_encoder(audio_features); - logits_node = run_decoder_step(full_tokens, false, false); - encoder_ready_ = true; - } - else - { - gb->soft_reset(); - reset_graph_side_cache_nodes(); - - std::vector last_token_vec = { tokens.back() }; - logits_node = run_decoder_step(last_token_vec, true, true); - } - - const auto& suppress_bias = first_decode_step_ ? suppress_bias_first_step_ : suppress_bias_; - if (first_decode_step_) first_decode_step_ = false; - size_t sampled_token_id = sample_token(gb, logits_node, temperature, top_p, top_k, min_p, repetition_penalty, &suppress_bias); - if (!profile_file.empty()) gb->execute(profile_file); - else gb->execute(); - - - compute_entropy(gb, logits_node, out_entropy); - - post_execute_updates(gb, full_tokens.size()); - update_kv_cache(gb, last_new_tokens_); - - auto* out_ptr = gb->get_output(sampled_token_id); - uint32_t sampled = *reinterpret_cast(out_ptr); - record_sampled_token(sampled); - - return sampled; -} - - -} -} diff --git a/cactus/models/model_youtu.cpp b/cactus/models/model_youtu.cpp deleted file mode 100644 index cfd1028ab..000000000 --- a/cactus/models/model_youtu.cpp +++ /dev/null @@ -1,246 +0,0 @@ -#include "model.h" -#include "../graph/graph.h" -#include -#include - -namespace cactus { -namespace engine { - -YoutuModel::YoutuModel() : Model() {} - -YoutuModel::YoutuModel(const Config& config) : Model(config) { - weight_nodes_.layers.resize(config.num_layers); -} - -std::vector YoutuModel::get_kv_layer_dims() const { - return std::vector(config_.num_layers, static_cast(config_.qk_head_dim)); -} - -void YoutuModel::post_init() { - std::vector v_dims(config_.num_layers, static_cast(config_.v_head_dim)); - std::vector v_kv_heads(config_.num_layers, static_cast(config_.attention_kv_heads)); - v_cache_.init(config_.num_layers, kv_cache_.max_seq_len, - v_dims, v_kv_heads, Precision::INT8); - v_cache_.set_window_size(kv_cache_.window_size, kv_cache_.sink_size); - cache_v_nodes_.resize(config_.num_layers, 0); -} - -void YoutuModel::post_execute_updates(CactusGraph* gb, size_t seq_len) { - v_cache_.update_from_graph(gb, cache_v_nodes_, cache_v_nodes_, seq_len, - config_.num_layers); -} - -void YoutuModel::reset_cache() { - Model::reset_cache(); - v_cache_.reset(); -} - -void YoutuModel::load_weights_to_graph(CactusGraph* gb) { - embedding_node_id_ = gb->mmap_embeddings(embedding_file_path_); - weight_nodes_.output_norm_weight = gb->mmap_weights(model_folder_path_ + "/output_norm.weights"); - - if (config_.tie_word_embeddings) { - weight_nodes_.output_weight = embedding_node_id_; - output_weight_node_id_ = embedding_node_id_; - } else { - weight_nodes_.output_weight = gb->mmap_weights(model_folder_path_ + "/output_weight.weights"); - output_weight_node_id_ = weight_nodes_.output_weight; - } - - for (uint32_t i = 0; i < config_.num_layers; i++) { - auto& layer = weight_nodes_.layers[i]; - std::string layer_prefix = model_folder_path_ + "/layer_" + std::to_string(i) + "_"; - if (config_.q_lora_rank == 0) { - layer.attn_q_weight = gb->mmap_weights(layer_prefix + "attn_q.weights"); - } else { - layer.attn_q_a_weight = gb->mmap_weights(layer_prefix + "attn_q_a.weights"); - layer.attn_q_a_norm_weight = gb->mmap_weights(layer_prefix + "attn_q_a_norm.weights"); - layer.attn_q_b_weight = gb->mmap_weights(layer_prefix + "attn_q_b.weights"); - } - layer.attn_kv_a_weight = gb->mmap_weights(layer_prefix + "attn_kv_a.weights"); - layer.attn_kv_a_norm_weight = gb->mmap_weights(layer_prefix + "attn_kv_a_norm.weights"); - layer.attn_kv_b_weight = gb->mmap_weights(layer_prefix + "attn_kv_b.weights"); - layer.attn_output_weight = gb->mmap_weights(layer_prefix + "attn_output.weights"); - if (config_.attention_bias) { - layer.attn_q_a_bias = gb->mmap_weights(layer_prefix + "attn_q_a_bias.weights"); - layer.attn_kv_a_bias = gb->mmap_weights(layer_prefix + "attn_kv_a_bias.weights"); - layer.attn_output_bias = gb->mmap_weights(layer_prefix + "attn_output_bias.weights"); - } - layer.input_layernorm_weight = gb->mmap_weights(layer_prefix + "input_norm.weights"); - layer.post_attention_layernorm_weight = gb->mmap_weights(layer_prefix + "post_attn_norm.weights"); - layer.ffn_gate_weight = gb->mmap_weights(layer_prefix + "ffn_gate.weights"); - layer.ffn_up_weight = gb->mmap_weights(layer_prefix + "ffn_up.weights"); - layer.ffn_down_weight = gb->mmap_weights(layer_prefix + "ffn_down.weights"); - } -} - -static float yarn_get_mscale(float scale, float mscale) { - if (scale <= 1.0f) return 1.0f; - return 0.1f * mscale * logf(scale) + 1.0f; -} - -size_t YoutuModel::build_attention(CactusGraph* gb, size_t normalized_input, uint32_t layer_idx, - ComputeBackend backend, bool use_cache, size_t position_offset) { - const auto& layer = weight_nodes_.layers[layer_idx]; - - const size_t seq_len = gb->get_output_buffer(normalized_input).shape[0]; - const size_t num_heads = config_.attention_heads; - const size_t num_kv_heads = config_.attention_kv_heads; - const size_t kv_lora = config_.kv_lora_rank; - const size_t qk_head = config_.qk_head_dim; - const size_t qk_nope = config_.qk_nope_head_dim; - const size_t qk_rope = config_.qk_rope_head_dim; - const size_t v_dim = config_.v_head_dim; - const float eps = config_.layer_norm_eps; - - size_t q_full; - if (config_.q_lora_rank == 0) { - q_full = gb->matmul(normalized_input, layer.attn_q_weight, true, backend); - } else { - auto q_latent = gb->matmul(normalized_input, layer.attn_q_a_weight, true, backend); - if (config_.attention_bias) { - q_latent = gb->add(q_latent, layer.attn_q_a_bias); - } - q_latent = gb->rms_norm(q_latent, layer.attn_q_a_norm_weight, eps); - q_full = gb->matmul(q_latent, layer.attn_q_b_weight, true, backend); - } - - q_full = gb->reshape(q_full, {seq_len * num_heads, qk_head}); - auto q_nope = gb->slice(q_full, 1, 0, qk_nope); - auto q_rope_raw = gb->slice(q_full, 1, qk_nope, qk_rope); - - q_rope_raw = gb->reshape(q_rope_raw, {1, seq_len, num_heads, qk_rope}); - size_t q_rope_rotated; - if (config_.rope_interleave) { - q_rope_rotated = gb->rope_gptj(q_rope_raw, config_.rope_theta, position_offset, qk_rope); - } else { - q_rope_rotated = gb->rope(q_rope_raw, config_.rope_theta, position_offset); - } - q_rope_rotated = gb->reshape(q_rope_rotated, {seq_len * num_heads, qk_rope}); - - auto q_combined = gb->concat(q_nope, q_rope_rotated, -1); - auto q_4d = gb->reshape(q_combined, {1, seq_len, num_heads, qk_head}); - - auto kv_combined = gb->matmul(normalized_input, layer.attn_kv_a_weight, true, backend); - if (config_.attention_bias) { - kv_combined = gb->add(kv_combined, layer.attn_kv_a_bias); - } - auto kv_latent = gb->slice(kv_combined, 1, 0, kv_lora); - auto k_rope_raw = gb->slice(kv_combined, 1, kv_lora, qk_rope); - - kv_latent = gb->rms_norm(kv_latent, layer.attn_kv_a_norm_weight, eps); - - auto kv_decoded = gb->matmul(kv_latent, layer.attn_kv_b_weight, true, backend); - kv_decoded = gb->reshape(kv_decoded, {seq_len * num_kv_heads, qk_nope + v_dim}); - auto k_nope = gb->slice(kv_decoded, 1, 0, qk_nope); - auto v_flat = gb->slice(kv_decoded, 1, qk_nope, v_dim); - - k_rope_raw = gb->reshape(k_rope_raw, {1, seq_len, 1, qk_rope}); - size_t k_rope_rotated; - if (config_.rope_interleave) { - k_rope_rotated = gb->rope_gptj(k_rope_raw, config_.rope_theta, position_offset, qk_rope); - } else { - k_rope_rotated = gb->rope(k_rope_raw, config_.rope_theta, position_offset); - } - std::vector k_rope_copies(num_kv_heads, k_rope_rotated); - auto k_rope_4d = gb->cat(k_rope_copies, 2); - auto k_rope_flat = gb->reshape(k_rope_4d, {seq_len * num_kv_heads, qk_rope}); - - auto k_combined = gb->concat(k_nope, k_rope_flat, -1); - auto k_4d = gb->reshape(k_combined, {1, seq_len, num_kv_heads, qk_head}); - auto v_4d = gb->reshape(v_flat, {1, seq_len, num_kv_heads, v_dim}); - - if (use_cache && layer_idx < cache_k_output_nodes_.size()) { - cache_k_output_nodes_[layer_idx] = gb->reshape(k_4d, {seq_len * num_kv_heads, qk_head}); - cache_v_output_nodes_[layer_idx] = cache_k_output_nodes_[layer_idx]; - cache_v_nodes_[layer_idx] = gb->reshape(v_4d, {seq_len * num_kv_heads, v_dim}); - } - - float scale = 1.0f / sqrtf(static_cast(qk_head)); - if (config_.rope_mscale_all_dim != 0.0f && config_.rope_scaling_factor > 1.0f) { - float mscale = yarn_get_mscale(config_.rope_scaling_factor, config_.rope_mscale_all_dim); - scale *= mscale * mscale; - } - - size_t attn_4d; - if (use_cache && !kv_cache_.is_empty()) { - attn_4d = gb->attention_int8_hybrid( - q_4d, k_4d, v_4d, scale, position_offset, - kv_cache_.get_keys_int8(layer_idx), - v_cache_.get_values_int8(layer_idx), - kv_cache_.get_key_scales(layer_idx), - v_cache_.get_value_scales(layer_idx), - kv_cache_.current_seq_len, num_kv_heads, qk_head, - kv_cache_.window_size, v_dim); - } else { - attn_4d = gb->attention(q_4d, k_4d, v_4d, scale, position_offset); - } - - auto attn_out = gb->reshape(attn_4d, {seq_len, num_heads * v_dim}); - size_t out = gb->matmul(attn_out, layer.attn_output_weight, true, backend); - if (config_.attention_bias) { - out = gb->add(out, layer.attn_output_bias); - } - return out; -} - - -size_t YoutuModel::build_mlp(CactusGraph* gb, size_t normalized_h, uint32_t layer_idx, - ComputeBackend backend) const { - const auto& layer = weight_nodes_.layers[layer_idx]; - size_t gate_output = gb->matmul(normalized_h, layer.ffn_gate_weight, true, backend); - size_t up_output = gb->matmul(normalized_h, layer.ffn_up_weight, true, backend); - size_t gate_silu = gb->silu(gate_output); - size_t gated = gb->multiply(gate_silu, up_output); - return gb->matmul(gated, layer.ffn_down_weight, true, backend); -} - - -size_t YoutuModel::build_transformer_block(CactusGraph* gb, size_t hidden, uint32_t layer_idx, - ComputeBackend backend, bool use_cache, size_t position_offset) { - const auto& layer = weight_nodes_.layers[layer_idx]; - auto normalized_input = gb->rms_norm(hidden, layer.input_layernorm_weight, config_.layer_norm_eps); - auto attn_output = build_attention(gb, normalized_input, layer_idx, backend, use_cache, position_offset); - auto after_attention = gb->add_clipped(hidden, attn_output); - auto normalized_after_attention = gb->rms_norm(after_attention, layer.post_attention_layernorm_weight, config_.layer_norm_eps); - auto mlp_output = build_mlp(gb, normalized_after_attention, layer_idx, backend); - return gb->add_clipped(after_attention, mlp_output); -} - - -size_t YoutuModel::forward(const std::vector& tokens, bool use_cache) { - if (!initialized_ || !graph_handle_) { - throw std::runtime_error("Model not initialized - call init() first"); - } - if (tokens.empty()) { - throw std::runtime_error("Token sequence cannot be empty"); - } - - auto* gb = static_cast(graph_handle_); - gb->soft_reset(); - - const size_t seq_len = tokens.size(); - const size_t position_offset = use_cache ? kv_cache_.get_total_seq_len() : 0; - - auto backend = config_.default_backend == Config::Backend::CPU - ? ComputeBackend::CPU - : ComputeBackend::NPU; - - auto input_node_id = gb->input({seq_len}, Precision::FP32); - auto hidden = gb->embedding(embedding_node_id_, input_node_id); - - std::vector input_data(seq_len); - for (size_t i = 0; i < seq_len; i++) { - input_data[i] = static_cast(tokens[i]); - } - gb->set_input(input_node_id, input_data.data(), Precision::FP32); - - for (uint32_t layer_idx = 0; layer_idx < config_.num_layers; layer_idx++) { - hidden = build_transformer_block(gb, hidden, layer_idx, backend, use_cache, position_offset); - } - - return gb->rms_norm(hidden, weight_nodes_.output_norm_weight, config_.layer_norm_eps); -} - -} -} diff --git a/cactus/npu/npu.cpp b/cactus/npu/npu.cpp deleted file mode 100644 index 69a9f80c7..000000000 --- a/cactus/npu/npu.cpp +++ /dev/null @@ -1,22 +0,0 @@ -// NPU stub implementation for non-Apple platforms -// On Apple platforms, npu_ane.mm provides the real implementation - -#include "npu.h" - -namespace cactus { -namespace npu { - -std::unique_ptr create_encoder() { - return nullptr; -} - -std::unique_ptr create_prefill() { - return nullptr; -} - -bool is_npu_available() { - return false; -} - -} // namespace npu -} // namespace cactus diff --git a/cactus/npu/npu.h b/cactus/npu/npu.h deleted file mode 100644 index 5474c872f..000000000 --- a/cactus/npu/npu.h +++ /dev/null @@ -1,89 +0,0 @@ -#ifndef CACTUS_NPU_H -#define CACTUS_NPU_H - -#include -#include -#include -#include - -namespace cactus { -namespace npu { - - -struct NPUNamedInput { - std::string name; - const __fp16* data; - std::vector shape; -}; - -class NPUEncoder { -public: - virtual ~NPUEncoder() = default; - - virtual bool load(const std::string& model_path) = 0; - - virtual bool preallocate(const std::vector& input_shape, - const std::string& input_name = "x", - const std::string& output_name = "") = 0; - - virtual size_t encode(const __fp16* input, - __fp16* output, - const std::vector& shape, - const std::string& input_name = "x", - const std::string& output_name = "") = 0; - - virtual bool is_available() const = 0; - - virtual std::vector get_input_shape() const = 0; - - virtual std::vector get_output_shape() const = 0; - - virtual __fp16* get_output_buffer() = 0; - - virtual size_t get_output_buffer_size() const = 0; - - virtual size_t encode_multimodal_input( - const std::vector& inputs, - __fp16* output, - const std::string& output_name = "") = 0; -}; - -std::unique_ptr create_encoder(); - -bool is_npu_available(); - -struct NPUBufferRef { - const __fp16* data; - size_t count; -}; - -struct NPUPrefillDirectResult { - NPUBufferRef hidden; - std::vector k_caches; - std::vector v_caches; - bool valid; -}; - -class NPUPrefill { -public: - virtual ~NPUPrefill() = default; - virtual bool load(const std::string& model_path) = 0; - virtual bool is_available() const = 0; - virtual int get_chunk_size() const = 0; - virtual int get_hidden_dim() const = 0; - virtual int get_num_layers() const = 0; - virtual int get_num_kv_heads() const = 0; - virtual int get_head_dim() const = 0; - - virtual NPUPrefillDirectResult prefill_chunk_direct( - const std::vector<__fp16>& embeddings, - int position_offset = 0, - const std::string& input_name = "x") = 0; -}; - -std::unique_ptr create_prefill(); - -} // namespace npu -} // namespace cactus - -#endif // CACTUS_NPU_H \ No newline at end of file diff --git a/cactus/npu/npu_ane.h b/cactus/npu/npu_ane.h deleted file mode 100644 index c590fb0c4..000000000 --- a/cactus/npu/npu_ane.h +++ /dev/null @@ -1,154 +0,0 @@ -#ifndef CACTUS_NPU_ANE_H -#define CACTUS_NPU_ANE_H - -#include "npu.h" - -#if defined(__APPLE__) -#define CACTUS_HAS_ANE 1 -#else -#define CACTUS_HAS_ANE 0 -#endif - -namespace cactus { -namespace npu { - -#if CACTUS_HAS_ANE - - -class ANEEncoder : public NPUEncoder { -public: - ANEEncoder(); - ~ANEEncoder() override; - - ANEEncoder(const ANEEncoder&) = delete; - ANEEncoder& operator=(const ANEEncoder&) = delete; - - ANEEncoder(ANEEncoder&& other) noexcept; - ANEEncoder& operator=(ANEEncoder&& other) noexcept; - - bool load(const std::string& model_path) override; - - bool preallocate(const std::vector& input_shape, - const std::string& input_name = "x", - const std::string& output_name = "") override; - - size_t encode(const __fp16* input, - __fp16* output, - const std::vector& shape, - const std::string& input_name = "x", - const std::string& output_name = "") override; - - bool is_available() const override; - - std::vector get_input_shape() const override; - - std::vector get_output_shape() const override; - - __fp16* get_output_buffer() override; - - size_t get_output_buffer_size() const override; - - size_t encode_multimodal_input( - const std::vector& inputs, - __fp16* output, - const std::string& output_name = "") override; - -private: - void* impl_; -}; - -class ANEPrefill : public NPUPrefill { -public: - ANEPrefill(); - ~ANEPrefill() override; - - ANEPrefill(const ANEPrefill&) = delete; - ANEPrefill& operator=(const ANEPrefill&) = delete; - - ANEPrefill(ANEPrefill&& other) noexcept; - ANEPrefill& operator=(ANEPrefill&& other) noexcept; - - bool load(const std::string& model_path) override; - bool is_available() const override; - - int get_chunk_size() const override; - int get_hidden_dim() const override; - int get_num_layers() const override; - int get_num_kv_heads() const override; - int get_head_dim() const override; - - NPUPrefillDirectResult prefill_chunk_direct( - const std::vector<__fp16>& embeddings, - int position_offset = 0, - const std::string& input_name = "x") override; - -private: - void* impl_; - int chunk_size_ = 256; - int hidden_dim_ = 0; - int num_layers_ = 0; - int num_kv_heads_ = 0; - int head_dim_ = 0; -}; - -#else - -class ANEEncoder : public NPUEncoder { -public: - ANEEncoder() = default; - ~ANEEncoder() override = default; - - bool load(const std::string&) override { return false; } - - bool preallocate(const std::vector&, - const std::string& = "x", - const std::string& = "") override { return false; } - - size_t encode(const __fp16*, - __fp16*, - const std::vector&, - const std::string& = "x", - const std::string& = "") override { return 0; } - - bool is_available() const override { return false; } - - std::vector get_input_shape() const override { return {}; } - - std::vector get_output_shape() const override { return {}; } - - __fp16* get_output_buffer() override { return nullptr; } - - size_t get_output_buffer_size() const override { return 0; } - - size_t encode_multimodal_input( - const std::vector&, - __fp16*, - const std::string& = "") override { return 0; } -}; - -class ANEPrefill : public NPUPrefill { -public: - ANEPrefill() = default; - ~ANEPrefill() override = default; - - bool load(const std::string&) override { return false; } - bool is_available() const override { return false; } - - int get_chunk_size() const override { return 0; } - int get_hidden_dim() const override { return 0; } - int get_num_layers() const override { return 0; } - int get_num_kv_heads() const override { return 0; } - int get_head_dim() const override { return 0; } - - NPUPrefillDirectResult prefill_chunk_direct( - const std::vector<__fp16>&, - int = 0, - const std::string& = "x") override { return {}; } -}; - -#endif // CACTUS_HAS_ANE - -} // namespace npu -} // namespace cactus - -#endif // CACTUS_NPU_ANE_H \ No newline at end of file diff --git a/cactus/npu/npu_ane.mm b/cactus/npu/npu_ane.mm deleted file mode 100644 index 488d43d6b..000000000 --- a/cactus/npu/npu_ane.mm +++ /dev/null @@ -1,1293 +0,0 @@ -#include "npu_ane.h" - -#if CACTUS_HAS_ANE - -#import -#import -#include -#include -#include -#include -#include -#include -#include -#include "../graph/graph.h" - -namespace { - -static std::string to_lower_ascii(std::string value) { - std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { - return static_cast(std::tolower(c)); - }); - return value; -} - -static bool parse_compute_units(const char* raw_value, MLComputeUnits& out_units) { - if (!raw_value || raw_value[0] == '\0') { - return false; - } - - std::string value = to_lower_ascii(raw_value); - if (value == "all" || value == "default") { - out_units = MLComputeUnitsAll; - return true; - } - if (value == "cpu_and_ne" || value == "cpu-ne" || value == "cpu_ne" || - value == "ne" || value == "ane" || value == "cpuandneuralengine") { - out_units = MLComputeUnitsCPUAndNeuralEngine; - return true; - } - if (value == "cpu_and_gpu" || value == "cpu_gpu" || value == "cpu-gpu" || - value == "cpuandgpu") { - out_units = MLComputeUnitsCPUAndGPU; - return true; - } - if (value == "cpu_only" || value == "cpu-only" || value == "cpu") { - out_units = MLComputeUnitsCPUOnly; - return true; - } - - return false; -} - -static const char* compute_units_to_string(MLComputeUnits units) { - switch (units) { - case MLComputeUnitsAll: - return "ALL"; - case MLComputeUnitsCPUAndGPU: - return "CPU_AND_GPU"; - case MLComputeUnitsCPUOnly: - return "CPU_ONLY"; - case MLComputeUnitsCPUAndNeuralEngine: - return "CPU_AND_NE"; - } - return "UNKNOWN"; -} - -static bool model_path_looks_like_audio_encoder(NSString* model_path) { - if (!model_path) return false; - NSString* lower = [[model_path lastPathComponent] lowercaseString]; - return [lower containsString:@"audio_encoder"]; -} - -static bool model_path_looks_like_vision_encoder(NSString* model_path) { - if (!model_path) return false; - NSString* lower = [[model_path lastPathComponent] lowercaseString]; - return [lower containsString:@"vision_encoder"]; -} - -static void maybe_apply_compute_units_env(const char* env_name, MLComputeUnits& target) { - const char* raw = std::getenv(env_name); - if (!raw || raw[0] == '\0') return; - - MLComputeUnits parsed; - if (parse_compute_units(raw, parsed)) { - target = parsed; - return; - } - - CACTUS_LOG_WARN("npu", "Ignoring invalid " << env_name << "=" << raw); -} - -static MLComputeUnits resolve_compute_units_for_model(NSString* model_path, bool is_prefill) { - MLComputeUnits units = MLComputeUnitsCPUAndNeuralEngine; - - // Gemma4 multimodal encoders show better fidelity on CPU+GPU. - if (!is_prefill && model_path_looks_like_audio_encoder(model_path)) { - units = MLComputeUnitsCPUAndGPU; - } - if (!is_prefill && model_path_looks_like_vision_encoder(model_path)) { - units = MLComputeUnitsCPUAndGPU; - } - - maybe_apply_compute_units_env("CACTUS_ANE_COMPUTE_UNITS", units); - if (is_prefill) { - maybe_apply_compute_units_env("CACTUS_ANE_PREFILL_COMPUTE_UNITS", units); - } else { - maybe_apply_compute_units_env("CACTUS_ANE_ENCODER_COMPUTE_UNITS", units); - if (model_path_looks_like_audio_encoder(model_path)) { - maybe_apply_compute_units_env("CACTUS_ANE_AUDIO_COMPUTE_UNITS", units); - } - if (model_path_looks_like_vision_encoder(model_path)) { - maybe_apply_compute_units_env("CACTUS_ANE_VISION_COMPUTE_UNITS", units); - } - } - - return units; -} - -static bool should_recompile_mlpackage(NSString* mlpackage_path, NSString* mlmodelc_path) { - NSFileManager* fm = [NSFileManager defaultManager]; - if (![fm fileExistsAtPath:mlmodelc_path]) return true; - - const char* force = std::getenv("CACTUS_ANE_FORCE_RECOMPILE"); - if (force && force[0] != '\0' && std::strcmp(force, "0") != 0) { - return true; - } - - NSError* pkg_err = nil; - NSError* cache_err = nil; - NSDictionary* pkg_attr = [fm attributesOfItemAtPath:mlpackage_path error:&pkg_err]; - NSDictionary* cache_attr = [fm attributesOfItemAtPath:mlmodelc_path error:&cache_err]; - if (!pkg_attr || !cache_attr || pkg_err || cache_err) { - return false; - } - - NSDate* pkg_mtime = pkg_attr[NSFileModificationDate]; - NSDate* cache_mtime = cache_attr[NSFileModificationDate]; - if (!pkg_mtime || !cache_mtime) { - return false; - } - return ([pkg_mtime compare:cache_mtime] == NSOrderedDescending); -} - -static NSURL* resolve_or_compile_model_url(NSString* path, NSError** error) { - NSURL* modelURL = [NSURL fileURLWithPath:path]; - if (![path hasSuffix:@".mlpackage"]) { - return modelURL; - } - - BOOL isDir = NO; - if (![[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir] || !isDir) { - CACTUS_LOG_ERROR("npu", "ANE mlpackage path is not a valid directory: " << [path UTF8String]); - return modelURL; - } - - NSString* cachedPath = [[path stringByDeletingPathExtension] stringByAppendingPathExtension:@"mlmodelc"]; - NSURL* cachedURL = [NSURL fileURLWithPath:cachedPath]; - NSFileManager* fm = [NSFileManager defaultManager]; - - if (!should_recompile_mlpackage(path, cachedPath)) { - return cachedURL; - } - - if ([fm fileExistsAtPath:cachedPath]) { - NSError* rmErr = nil; - [fm removeItemAtPath:cachedPath error:&rmErr]; - if (rmErr) { - CACTUS_LOG_WARN("npu", "Failed to remove stale mlmodelc: " << [[rmErr localizedDescription] UTF8String]); - } else { - CACTUS_LOG_INFO("npu", "Removed stale mlmodelc cache, recompiling: " << [cachedPath UTF8String]); - } - } - - NSURL* compiledURL = [MLModel compileModelAtURL:modelURL error:error]; - if (*error || !compiledURL) { - CACTUS_LOG_ERROR("npu", "ANE model compilation failed: " << [[*error localizedDescription] UTF8String]); - return modelURL; - } - - NSError* moveError = nil; - [fm moveItemAtURL:compiledURL toURL:cachedURL error:&moveError]; - if (moveError) { - CACTUS_LOG_WARN("npu", "Could not persist compiled mlmodelc cache: " << [[moveError localizedDescription] UTF8String]); - return compiledURL; - } - return cachedURL; -} - -} // namespace - -@interface CactusANEImpl : NSObject - -@property (nonatomic, strong) MLModel* model; -@property (nonatomic, strong) MLModelDescription* modelDescription; -@property (nonatomic, strong) MLMultiArray* cachedInputArray; -@property (nonatomic, strong) MLMultiArray* cachedOutputArray; -@property (nonatomic, strong) NSArray* cachedShape; -@property (nonatomic, strong) NSArray* cachedOutputShape; -@property (nonatomic, strong) NSString* cachedInputName; -@property (nonatomic, strong) NSString* cachedOutputName; -@property (nonatomic, assign) NSUInteger cachedOutputSize; -@property (nonatomic, strong) MLPredictionOptions* predictionOptions; -@property (nonatomic, strong) NSMutableDictionary* cachedMultiInputArrays; -@property (nonatomic, strong) MLMultiArray* cachedMultiOutputArray; -@property (nonatomic, strong) MLPredictionOptions* multiPredictionOptions; -@property (nonatomic, assign) BOOL multiInputPreallocated; -@property (nonatomic, assign) BOOL hasOutputBackings; -@property (nonatomic, assign) BOOL hasMultiOutputBackings; - -- (instancetype)initWithModelPath:(NSString*)path; -- (NSArray*)getInputShape; -- (NSArray*)getOutputShape; -- (BOOL)preallocateBuffersWithInput:(NSString*)inputName - shape:(NSArray*)shape - outputName:(NSString*)outputName; -- (BOOL)canUseCachedBufferWithInput:(NSString*)inputName - shape:(NSArray*)shape - outputName:(NSString*)outputName; -- (MLMultiArray*)predictWithInput:(NSString*)inputName - data:(const __fp16*)data - shape:(NSArray*)shape - outputName:(NSString*)outputName; -- (BOOL)preallocateMultiInputBuffersWithOutputName:(NSString*)outputName; -- (size_t)predictMultiInputWithDict:(NSMutableDictionary*)inputDict - outputData:(__fp16*)output - outputName:(NSString*)outputName; - -@end - -@implementation CactusANEImpl - -- (instancetype)initWithModelPath:(NSString*)path { - self = [super init]; - if (self) { - NSError* error = nil; - NSURL* modelURL = resolve_or_compile_model_url(path, &error); - - MLModelConfiguration* config = [[MLModelConfiguration alloc] init]; - config.computeUnits = resolve_compute_units_for_model(path, false); - CACTUS_LOG_INFO("npu", - "ANEEncoder compute units: " - << compute_units_to_string(config.computeUnits) - << " for " << [path UTF8String]); - - _model = [MLModel modelWithContentsOfURL:modelURL configuration:config error:&error]; - if (_model) { - _modelDescription = _model.modelDescription; - } - if (error) { - CACTUS_LOG_ERROR("npu", "ANE model load failed: " << [[error localizedDescription] UTF8String]); - } - } - return self; -} - -- (NSArray*)getInputShape { - if (!_modelDescription) return @[]; - - NSString* inputName = _cachedInputName; - if (!inputName) { - inputName = _modelDescription.inputDescriptionsByName.allKeys.firstObject; - } - - MLFeatureDescription* inputDesc = _modelDescription.inputDescriptionsByName[inputName]; - if (inputDesc && inputDesc.type == MLFeatureTypeMultiArray) { - return inputDesc.multiArrayConstraint.shape; - } - - return @[]; -} - -- (NSArray*)getOutputShape { - if (!_modelDescription) return @[]; - - NSString* outputName = _cachedOutputName; - if (!outputName) { - outputName = _modelDescription.outputDescriptionsByName.allKeys.firstObject; - } - - MLFeatureDescription* outputDesc = _modelDescription.outputDescriptionsByName[outputName]; - if (outputDesc && outputDesc.type == MLFeatureTypeMultiArray) { - return outputDesc.multiArrayConstraint.shape; - } - - return @[]; -} - -- (BOOL)preallocateBuffersWithInput:(NSString*)inputName - shape:(NSArray*)shape - outputName:(NSString*)outputName { - if (!_model) return NO; - - NSError* error = nil; - - MLMultiArrayDataType inputDataType = MLMultiArrayDataTypeFloat16; - MLFeatureDescription* inputDesc = _modelDescription.inputDescriptionsByName[inputName]; - if (inputDesc && inputDesc.multiArrayConstraint) { - inputDataType = inputDesc.multiArrayConstraint.dataType; - } - - _cachedInputArray = [[MLMultiArray alloc] - initWithShape:shape - dataType:inputDataType - error:&error]; - - if (error) { - CACTUS_LOG_ERROR("npu", "ANE preallocate input array failed: " << [[error localizedDescription] UTF8String]); - return NO; - } - - _cachedShape = [shape copy]; - _cachedInputName = [inputName copy]; - _cachedOutputName = outputName ? [outputName copy] - : _modelDescription.outputDescriptionsByName.allKeys.firstObject; - - CACTUS_LOG_DEBUG("npu", - "[CactusANE] prealloc input name=" - << [_cachedInputName UTF8String] - << " shape=" << [[_cachedInputArray.shape description] UTF8String] - << " strides=" << [[_cachedInputArray.strides description] UTF8String] - << " dtype=" << (long)_cachedInputArray.dataType); - - MLFeatureDescription* outputDesc = _modelDescription.outputDescriptionsByName[_cachedOutputName]; - if (outputDesc && outputDesc.type == MLFeatureTypeMultiArray) { - NSArray* outputShape = outputDesc.multiArrayConstraint.shape; - MLMultiArrayDataType outputDataType = outputDesc.multiArrayConstraint.dataType; - - _cachedOutputArray = [[MLMultiArray alloc] - initWithShape:outputShape - dataType:outputDataType - error:&error]; - - if (error) { - CACTUS_LOG_ERROR("npu", "ANE preallocate output array failed: " << [[error localizedDescription] UTF8String]); - return NO; - } - - _cachedOutputShape = [outputShape copy]; - _cachedOutputSize = 1; - for (NSNumber* dim in outputShape) { - _cachedOutputSize *= [dim unsignedIntegerValue]; - } - - CACTUS_LOG_DEBUG("npu", - "[CactusANE] prealloc output name=" - << [_cachedOutputName UTF8String] - << " shape=" << [[_cachedOutputArray.shape description] UTF8String] - << " strides=" << [[_cachedOutputArray.strides description] UTF8String] - << " dtype=" << (long)_cachedOutputArray.dataType); - - _predictionOptions = [[MLPredictionOptions alloc] init]; - _hasOutputBackings = NO; - if (_predictionOptions && _cachedOutputArray && _cachedOutputName) { - _predictionOptions.outputBackings = - @{ _cachedOutputName: _cachedOutputArray }; - _hasOutputBackings = YES; - } - } - - return YES; -} - -- (BOOL)canUseCachedBufferWithInput:(NSString*)inputName - shape:(NSArray*)shape - outputName:(NSString*)outputName { - if (!_cachedInputArray || !_cachedShape) return NO; - if (![_cachedInputName isEqualToString:inputName]) return NO; - if (_cachedShape.count != shape.count) return NO; - - for (NSUInteger i = 0; i < shape.count; i++) { - if (![_cachedShape[i] isEqualToNumber:shape[i]]) return NO; - } - - if (outputName && outputName.length > 0 && ![_cachedOutputName isEqualToString:outputName]) { - return NO; - } - - return YES; -} - -- (MLMultiArray*)predictWithInput:(NSString*)inputName - data:(const __fp16*)data - shape:(NSArray*)shape - outputName:(NSString*)outputName { - if (!_model) return nil; - - NSError* error = nil; - MLMultiArray* inputArray = nil; - - MLMultiArrayDataType expectedDataType = MLMultiArrayDataTypeFloat16; - MLFeatureDescription* inputDesc = _modelDescription.inputDescriptionsByName[inputName]; - if (inputDesc && inputDesc.multiArrayConstraint) { - expectedDataType = inputDesc.multiArrayConstraint.dataType; - } - - BOOL useCached = (expectedDataType == MLMultiArrayDataTypeFloat16) && - [self canUseCachedBufferWithInput:inputName shape:shape outputName:outputName]; - - if (useCached) { - inputArray = _cachedInputArray; - } else { - inputArray = [[MLMultiArray alloc] - initWithShape:shape - dataType:expectedDataType - error:&error]; - - if (error) { - CACTUS_LOG_ERROR("npu", "ANE create input array failed: " << [[error localizedDescription] UTF8String]); - return nil; - } - } - - NSUInteger totalElements = 1; - for (NSNumber* dim in shape) { - totalElements *= [dim unsignedIntegerValue]; - } - - if (expectedDataType == MLMultiArrayDataTypeFloat16) { - __fp16* inputPtr = (__fp16*)inputArray.dataPointer; - memcpy(inputPtr, data, totalElements * sizeof(__fp16)); - } else { - float* inputPtr = (float*)inputArray.dataPointer; - for (NSUInteger i = 0; i < totalElements; i++) { - inputPtr[i] = (float)data[i]; - } - } - - MLFeatureValue* inputFeature = [MLFeatureValue featureValueWithMultiArray:inputArray]; - NSDictionary* inputDict = @{inputName: inputFeature}; - id inputProvider = [[MLDictionaryFeatureProvider alloc] - initWithDictionary:inputDict - error:&error]; - - if (error) { - CACTUS_LOG_ERROR("npu", "ANE create feature provider failed: " << [[error localizedDescription] UTF8String]); - return nil; - } - - id outputProvider = nil; - - if (useCached && _hasOutputBackings && _predictionOptions) { - outputProvider = [_model predictionFromFeatures:inputProvider - options:_predictionOptions - error:&error]; - } else { - outputProvider = [_model predictionFromFeatures:inputProvider error:&error]; - } - - if (error) { - CACTUS_LOG_ERROR("npu", "ANE prediction failed: " << [[error localizedDescription] UTF8String]); - return nil; - } - - NSString* outName = outputName; - if (!outName || outName.length == 0) { - outName = useCached ? _cachedOutputName - : _modelDescription.outputDescriptionsByName.allKeys.firstObject; - } - - MLFeatureValue* outputFeature = [outputProvider featureValueForName:outName]; - static bool logged_output_feature_layout = false; - if (!logged_output_feature_layout && outputFeature && outputFeature.multiArrayValue) { - MLMultiArray* outArr = outputFeature.multiArrayValue; - CACTUS_LOG_DEBUG("npu", - "[CactusANE] output feature name=" - << [outName UTF8String] - << " shape=" << [[outArr.shape description] UTF8String] - << " strides=" << [[outArr.strides description] UTF8String] - << " dtype=" << (long)outArr.dataType); - logged_output_feature_layout = true; - } - return outputFeature.multiArrayValue; -} - -static size_t copyMLArrayToFP16(MLMultiArray* array, __fp16* output) { - size_t count = array.count; - if (!array || !output || count == 0) return 0; - - std::vector dims; - std::vector strides; - const size_t rank = array.shape.count; - bool have_layout = rank == array.strides.count && rank > 0; - if (have_layout) { - dims.resize(rank); - strides.resize(rank); - for (size_t i = 0; i < rank; ++i) { - NSInteger d = [array.shape[i] integerValue]; - NSInteger s = [array.strides[i] integerValue]; - if (d <= 0 || s < 0) { - have_layout = false; - break; - } - dims[i] = static_cast(d); - strides[i] = static_cast(s); - } - } - - if (!have_layout) { - if (array.dataType == MLMultiArrayDataTypeFloat16) { - if (output != (__fp16*)array.dataPointer) { - memcpy(output, array.dataPointer, count * sizeof(__fp16)); - } - } else { - const float* src = (const float*)array.dataPointer; - for (size_t i = 0; i < count; i++) output[i] = (__fp16)src[i]; - } - return count; - } - - auto is_contiguous = [&]() -> bool { - if (!have_layout) return false; - size_t expected = 1; - for (size_t i = rank; i-- > 0;) { - if (dims[i] > 1 && strides[i] != expected) return false; - expected *= dims[i]; - } - return true; - }; - - if (false && is_contiguous()) { - if (array.dataType == MLMultiArrayDataTypeFloat16) { - if (output != (__fp16*)array.dataPointer) { - memcpy(output, array.dataPointer, count * sizeof(__fp16)); - } - } else { - const float* src = (const float*)array.dataPointer; - for (size_t i = 0; i < count; i++) output[i] = (__fp16)src[i]; - } - return count; - } - - std::vector idx(rank, 0); - if (array.dataType == MLMultiArrayDataTypeFloat16) { - const __fp16* src = (const __fp16*)array.dataPointer; - for (size_t i = 0; i < count; ++i) { - size_t offset = 0; - for (size_t d = 0; d < rank; ++d) { - offset += idx[d] * strides[d]; - } - output[i] = src[offset]; - - for (size_t d = rank; d-- > 0;) { - idx[d]++; - if (idx[d] < dims[d]) break; - idx[d] = 0; - } - } - } else { - const float* src = (const float*)array.dataPointer; - for (size_t i = 0; i < count; ++i) { - size_t offset = 0; - for (size_t d = 0; d < rank; ++d) { - offset += idx[d] * strides[d]; - } - output[i] = (__fp16)src[offset]; - - for (size_t d = rank; d-- > 0;) { - idx[d]++; - if (idx[d] < dims[d]) break; - idx[d] = 0; - } - } - } - return count; -} - -static void copyFP16ToMLArray(const __fp16* data, size_t count, MLMultiArray* array) { - if (!array || !data || count == 0) return; - - std::vector dims; - std::vector strides; - const size_t rank = array.shape.count; - bool have_layout = rank == array.strides.count && rank > 0; - if (have_layout) { - dims.resize(rank); - strides.resize(rank); - for (size_t i = 0; i < rank; ++i) { - NSInteger d = [array.shape[i] integerValue]; - NSInteger s = [array.strides[i] integerValue]; - if (d <= 0 || s < 0) { - have_layout = false; - break; - } - dims[i] = static_cast(d); - strides[i] = static_cast(s); - } - } - - if (!have_layout) { - if (array.dataType == MLMultiArrayDataTypeFloat16) { - memcpy(array.dataPointer, data, count * sizeof(__fp16)); - } else { - float* dst = (float*)array.dataPointer; - for (size_t i = 0; i < count; i++) dst[i] = (float)data[i]; - } - return; - } - - auto is_contiguous = [&]() -> bool { - if (!have_layout) return false; - size_t expected = 1; - for (size_t i = rank; i-- > 0;) { - if (dims[i] > 1 && strides[i] != expected) return false; - expected *= dims[i]; - } - return true; - }; - - if (false && is_contiguous()) { - if (array.dataType == MLMultiArrayDataTypeFloat16) { - memcpy(array.dataPointer, data, count * sizeof(__fp16)); - } else { - float* dst = (float*)array.dataPointer; - for (size_t i = 0; i < count; i++) dst[i] = (float)data[i]; - } - return; - } - - std::vector idx(rank, 0); - if (array.dataType == MLMultiArrayDataTypeFloat16) { - __fp16* dst = (__fp16*)array.dataPointer; - for (size_t i = 0; i < count; ++i) { - size_t offset = 0; - for (size_t d = 0; d < rank; ++d) { - offset += idx[d] * strides[d]; - } - dst[offset] = data[i]; - - for (size_t d = rank; d-- > 0;) { - idx[d]++; - if (idx[d] < dims[d]) break; - idx[d] = 0; - } - } - } else { - float* dst = (float*)array.dataPointer; - for (size_t i = 0; i < count; ++i) { - size_t offset = 0; - for (size_t d = 0; d < rank; ++d) { - offset += idx[d] * strides[d]; - } - dst[offset] = (float)data[i]; - - for (size_t d = rank; d-- > 0;) { - idx[d]++; - if (idx[d] < dims[d]) break; - idx[d] = 0; - } - } - } -} - -- (BOOL)preallocateMultiInputBuffersWithOutputName:(NSString*)outputName { - if (!_model || !_modelDescription) return NO; - - NSError* error = nil; - _cachedMultiInputArrays = [NSMutableDictionary new]; - - for (NSString* inputName in _modelDescription.inputDescriptionsByName) { - MLFeatureDescription* desc = _modelDescription.inputDescriptionsByName[inputName]; - if (desc.type != MLFeatureTypeMultiArray) continue; - - MLMultiArray* array = [[MLMultiArray alloc] - initWithShape:desc.multiArrayConstraint.shape - dataType:desc.multiArrayConstraint.dataType - error:&error]; - if (!array || error) return NO; - _cachedMultiInputArrays[inputName] = array; - } - - NSString* outName = (outputName && outputName.length > 0) - ? outputName : _modelDescription.outputDescriptionsByName.allKeys.firstObject; - - MLFeatureDescription* outputDesc = _modelDescription.outputDescriptionsByName[outName]; - if (outputDesc && outputDesc.type == MLFeatureTypeMultiArray) { - _cachedMultiOutputArray = [[MLMultiArray alloc] - initWithShape:outputDesc.multiArrayConstraint.shape - dataType:outputDesc.multiArrayConstraint.dataType - error:&error]; - if (!_cachedMultiOutputArray || error) return NO; - - _multiPredictionOptions = [[MLPredictionOptions alloc] init]; - _hasMultiOutputBackings = NO; - } - - _multiInputPreallocated = YES; - return YES; -} - -- (size_t)predictMultiInputWithDict:(NSMutableDictionary*)inputDict - outputData:(__fp16*)output - outputName:(NSString*)outputName { - if (!_model) return 0; - - NSError* error = nil; - MLDictionaryFeatureProvider* provider = - [[MLDictionaryFeatureProvider alloc] initWithDictionary:inputDict error:&error]; - if (!provider || error) return 0; - - id result = nil; - if (_hasMultiOutputBackings && _multiPredictionOptions) { - result = [_model predictionFromFeatures:provider options:_multiPredictionOptions error:&error]; - } else { - result = [_model predictionFromFeatures:provider error:&error]; - } - if (!result || error) return 0; - - static bool logged_multi_layout = false; - if (!logged_multi_layout) { - for (NSString* key in inputDict) { - MLFeatureValue* inFeature = inputDict[key]; - if (inFeature && inFeature.multiArrayValue) { - MLMultiArray* arr = inFeature.multiArrayValue; - } - } - } - - MLFeatureValue* outFeature = [result featureValueForName:outputName]; - if (!outFeature || !outFeature.multiArrayValue) return 0; - if (!logged_multi_layout) { - MLMultiArray* outArr = outFeature.multiArrayValue; - logged_multi_layout = true; - } - return copyMLArrayToFP16(outFeature.multiArrayValue, output); -} - -@end - -namespace cactus { -namespace npu { - -static bool g_npu_enabled = true; - -ANEEncoder::ANEEncoder() : impl_(nullptr) {} - -ANEEncoder::~ANEEncoder() { - if (impl_) { - (void)(__bridge_transfer CactusANEImpl*)impl_; - impl_ = nullptr; - } -} - -ANEEncoder::ANEEncoder(ANEEncoder&& other) noexcept : impl_(other.impl_) { - other.impl_ = nullptr; -} - -ANEEncoder& ANEEncoder::operator=(ANEEncoder&& other) noexcept { - if (this != &other) { - if (impl_) { - (void)(__bridge_transfer CactusANEImpl*)impl_; - } - impl_ = other.impl_; - other.impl_ = nullptr; - } - return *this; -} - -bool ANEEncoder::load(const std::string& model_path) { - @autoreleasepool { - CACTUS_LOG_INFO("npu", "ANEEncoder loading model: " << model_path); - NSString* path = [NSString stringWithUTF8String:model_path.c_str()]; - - if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { - CACTUS_LOG_WARN("npu", "not using NPU"); - return false; - } - - CactusANEImpl* impl = [[CactusANEImpl alloc] initWithModelPath:path]; - - if (impl && impl.model) { - impl_ = (__bridge_retained void*)impl; - CACTUS_LOG_INFO("npu", "ANEEncoder model loaded successfully: " << model_path); - return true; - } - CACTUS_LOG_ERROR("npu", "ANEEncoder model load failed: " << model_path); - return false; - } -} - -bool ANEEncoder::preallocate(const std::vector& input_shape, - const std::string& input_name, - const std::string& output_name) { - if (!impl_) return false; - - @autoreleasepool { - CactusANEImpl* impl = (__bridge CactusANEImpl*)impl_; - - NSMutableArray* shapeArray = [NSMutableArray array]; - for (int dim : input_shape) { - [shapeArray addObject:@(dim)]; - } - - NSString* inName = [NSString stringWithUTF8String:input_name.c_str()]; - NSString* outName = output_name.empty() - ? nil - : [NSString stringWithUTF8String:output_name.c_str()]; - - return [impl preallocateBuffersWithInput:inName shape:shapeArray outputName:outName]; - } -} - -size_t ANEEncoder::encode(const __fp16* input, - __fp16* output, - const std::vector& shape, - const std::string& input_name, - const std::string& output_name) { - if (!impl_ || !input || !output) return 0; - - @autoreleasepool { - CactusANEImpl* impl = (__bridge CactusANEImpl*)impl_; - - NSArray* shapeArray = impl.cachedShape; - bool shapeMatches = (shapeArray && shapeArray.count == shape.size()); - if (shapeMatches) { - for (size_t i = 0; i < shape.size(); ++i) { - if ([shapeArray[i] intValue] != shape[i]) { - shapeMatches = false; - break; - } - } - } - - if (!shapeMatches) { - NSMutableArray* newShapeArray = [NSMutableArray arrayWithCapacity:shape.size()]; - for (int dim : shape) { - [newShapeArray addObject:@(dim)]; - } - shapeArray = newShapeArray; - } - - NSString* inName = impl.cachedInputName; - if (!inName) { - inName = [NSString stringWithUTF8String:input_name.c_str()]; - } - NSString* outName = impl.cachedOutputName; - if (!outName && !output_name.empty()) { - outName = [NSString stringWithUTF8String:output_name.c_str()]; - } - - MLMultiArray* mlOutput = [impl predictWithInput:inName - data:input - shape:shapeArray - outputName:outName]; - - if (mlOutput) { - return copyMLArrayToFP16(mlOutput, output); - } - } - - return 0; -} - -bool ANEEncoder::is_available() const { - if (!impl_) return false; - CactusANEImpl* impl = (__bridge CactusANEImpl*)impl_; - return impl.model != nil; -} - -std::vector ANEEncoder::get_input_shape() const { - std::vector result; - if (!impl_) return result; - CactusANEImpl* impl = (__bridge CactusANEImpl*)impl_; - NSArray* shape = [impl getInputShape]; - for (NSNumber* dim in shape) { - result.push_back([dim intValue]); - } - return result; -} - -std::vector ANEEncoder::get_output_shape() const { - std::vector result; - if (!impl_) return result; - CactusANEImpl* impl = (__bridge CactusANEImpl*)impl_; - NSArray* shape = [impl getOutputShape]; - for (NSNumber* dim in shape) { - result.push_back([dim intValue]); - } - return result; -} - -__fp16* ANEEncoder::get_output_buffer() { - if (!impl_) return nullptr; - CactusANEImpl* impl = (__bridge CactusANEImpl*)impl_; - if (!impl.hasOutputBackings || !impl.cachedOutputArray) return nullptr; - return (__fp16*)impl.cachedOutputArray.dataPointer; -} - -size_t ANEEncoder::get_output_buffer_size() const { - if (!impl_) return 0; - CactusANEImpl* impl = (__bridge CactusANEImpl*)impl_; - if (!impl.hasOutputBackings || !impl.cachedOutputArray) return 0; - return impl.cachedOutputSize; -} - -size_t ANEEncoder::encode_multimodal_input( - const std::vector& inputs, - __fp16* output, - const std::string& output_name) { - if (!impl_ || !output || inputs.empty()) return 0; - - @autoreleasepool { - CactusANEImpl* impl = (__bridge CactusANEImpl*)impl_; - - NSString* outName = output_name.empty() - ? impl.modelDescription.outputDescriptionsByName.allKeys.firstObject - : [NSString stringWithUTF8String:output_name.c_str()]; - - if (!impl.multiInputPreallocated) { - [impl preallocateMultiInputBuffersWithOutputName:outName]; - } - - NSMutableDictionary* inputDict = [NSMutableDictionary new]; - - for (const auto& input : inputs) { - NSString* nsName = [NSString stringWithUTF8String:input.name.c_str()]; - - size_t total = 1; - for (int dim : input.shape) total *= dim; - - MLMultiArray* array = impl.cachedMultiInputArrays[nsName]; - if (!array) { - MLMultiArrayDataType dataType = MLMultiArrayDataTypeFloat16; - MLFeatureDescription* desc = impl.modelDescription.inputDescriptionsByName[nsName]; - if (desc && desc.multiArrayConstraint) dataType = desc.multiArrayConstraint.dataType; - - NSMutableArray* shapeArray = [NSMutableArray arrayWithCapacity:input.shape.size()]; - for (int dim : input.shape) [shapeArray addObject:@(dim)]; - - NSError* arrayError = nil; - array = [[MLMultiArray alloc] initWithShape:shapeArray dataType:dataType error:&arrayError]; - if (!array || arrayError) return 0; - } - - copyFP16ToMLArray(input.data, total, array); - inputDict[nsName] = [MLFeatureValue featureValueWithMultiArray:array]; - } - - return [impl predictMultiInputWithDict:inputDict outputData:output outputName:outName]; - } -} - -std::unique_ptr create_encoder() { - return std::make_unique(); -} - -bool is_npu_available() { - CACTUS_LOG_INFO("npu", "is_npu_available: " << (g_npu_enabled ? "true" : "false")); - return g_npu_enabled; -} - -} // namespace npu -} // namespace cactus - -@interface CactusANEPrefillImpl : NSObject - -@property (nonatomic, strong) MLModel* model; -@property (nonatomic, strong) MLModelDescription* modelDescription; -@property (nonatomic, assign) int chunkSize; -@property (nonatomic, assign) int hiddenDim; -@property (nonatomic, assign) int numLayers; -@property (nonatomic, assign) int numKvHeads; -@property (nonatomic, assign) int headDim; - -@property (nonatomic, strong) MLMultiArray* cachedInputArray; -@property (nonatomic, strong) MLMultiArray* cachedOffsetArray; -@property (nonatomic, strong) NSMutableDictionary* cachedOutputArrays; -@property (nonatomic, strong) MLPredictionOptions* predictionOptions; -@property (nonatomic, assign) BOOL hasOutputBackings; - -- (instancetype)initWithModelPath:(NSString*)path; -- (void)preallocateBuffers; -- (BOOL)predictDirectWithInput:(NSString*)inputName - data:(const __fp16*)data - offset:(int)offset; -- (MLMultiArray*)getOutputArray:(NSString*)name; -@end - -@implementation CactusANEPrefillImpl - -- (instancetype)initWithModelPath:(NSString*)path { - self = [super init]; - if (self) { - NSError* error = nil; - NSURL* modelURL = resolve_or_compile_model_url(path, &error); - - MLModelConfiguration* config = [[MLModelConfiguration alloc] init]; - config.computeUnits = resolve_compute_units_for_model(path, true); - CACTUS_LOG_INFO("npu", - "ANEPrefill compute units: " - << compute_units_to_string(config.computeUnits) - << " for " << [path UTF8String]); - - _model = [MLModel modelWithContentsOfURL:modelURL configuration:config error:&error]; - if (_model) { - _modelDescription = _model.modelDescription; - [self inferModelDimensions]; - [self preallocateBuffers]; - } - if (error) { - CACTUS_LOG_ERROR("npu", "ANE prefill model load failed: " << [[error localizedDescription] UTF8String]); - } - } - return self; -} - -- (void)inferModelDimensions { - if (!_modelDescription) return; - - NSString* inputName = _modelDescription.inputDescriptionsByName.allKeys.firstObject; - MLFeatureDescription* inputDesc = _modelDescription.inputDescriptionsByName[inputName]; - if (inputDesc && inputDesc.type == MLFeatureTypeMultiArray) { - NSArray* shape = inputDesc.multiArrayConstraint.shape; - if (shape.count >= 2) { - _chunkSize = [shape[0] intValue]; - _hiddenDim = [shape[1] intValue]; - } - } - - int maxLayerIdx = -1; - for (NSString* outputName in _modelDescription.outputDescriptionsByName.allKeys) { - if ([outputName hasPrefix:@"k_"]) { - int layerIdx = [[outputName substringFromIndex:2] intValue]; - maxLayerIdx = MAX(maxLayerIdx, layerIdx); - - MLFeatureDescription* outputDesc = _modelDescription.outputDescriptionsByName[outputName]; - if (outputDesc && outputDesc.type == MLFeatureTypeMultiArray) { - NSArray* shape = outputDesc.multiArrayConstraint.shape; - if (shape.count >= 3) { - _numKvHeads = [shape[1] intValue]; - _headDim = [shape[2] intValue]; - } - } - } - } - _numLayers = maxLayerIdx + 1; -} - -- (void)preallocateBuffers { - if (!_model || !_modelDescription || _chunkSize == 0 || _hiddenDim == 0) return; - - NSError* error = nil; - - NSArray* inputShape = @[@(_chunkSize), @(_hiddenDim)]; - _cachedInputArray = [[MLMultiArray alloc] initWithShape:inputShape - dataType:MLMultiArrayDataTypeFloat16 - error:&error]; - if (error) { - _cachedInputArray = nil; - return; - } - - if (_modelDescription.inputDescriptionsByName[@"offset"] != nil) { - _cachedOffsetArray = [[MLMultiArray alloc] initWithShape:@[@1] - dataType:MLMultiArrayDataTypeFloat16 - error:&error]; - if (error) { - _cachedOffsetArray = nil; - } - } - - _cachedOutputArrays = [NSMutableDictionary dictionary]; - NSMutableDictionary* outputBackings = [NSMutableDictionary dictionary]; - - for (NSString* outputName in _modelDescription.outputDescriptionsByName.allKeys) { - MLFeatureDescription* outputDesc = _modelDescription.outputDescriptionsByName[outputName]; - if (outputDesc && outputDesc.type == MLFeatureTypeMultiArray) { - NSArray* outputShape = outputDesc.multiArrayConstraint.shape; - - MLMultiArray* outputArray = [[MLMultiArray alloc] initWithShape:outputShape - dataType:MLMultiArrayDataTypeFloat16 - error:&error]; - if (!error && outputArray) { - _cachedOutputArrays[outputName] = outputArray; - outputBackings[outputName] = outputArray; - } - } - } - - _predictionOptions = [[MLPredictionOptions alloc] init]; - _hasOutputBackings = NO; - if (@available(macOS 14.0, iOS 17.0, *)) { - _predictionOptions.outputBackings = outputBackings; - _hasOutputBackings = YES; - } -} - -- (BOOL)predictDirectWithInput:(NSString*)inputName - data:(const __fp16*)data - offset:(int)offset { - if (!_model || !_cachedInputArray) return NO; - - NSError* error = nil; - - NSUInteger totalElements = (NSUInteger)(_chunkSize * _hiddenDim); - __fp16* inputPtr = (__fp16*)_cachedInputArray.dataPointer; - memcpy(inputPtr, data, totalElements * sizeof(__fp16)); - - MLFeatureValue* inputFeature = [MLFeatureValue featureValueWithMultiArray:_cachedInputArray]; - NSMutableDictionary* inputDict = [NSMutableDictionary dictionaryWithObject:inputFeature forKey:inputName]; - - if (_cachedOffsetArray) { - ((__fp16*)_cachedOffsetArray.dataPointer)[0] = (__fp16)offset; - MLFeatureValue* offsetFeature = [MLFeatureValue featureValueWithMultiArray:_cachedOffsetArray]; - inputDict[@"offset"] = offsetFeature; - } - - id inputProvider = [[MLDictionaryFeatureProvider alloc] - initWithDictionary:inputDict - error:&error]; - if (error) return NO; - - id outputProvider = [_model predictionFromFeatures:inputProvider - options:_predictionOptions - error:&error]; - if (error || !outputProvider) return NO; - - if (!_hasOutputBackings) { - for (NSString* outputName in _cachedOutputArrays) { - MLFeatureValue* outFeature = [outputProvider featureValueForName:outputName]; - if (!outFeature || !outFeature.multiArrayValue) return NO; - - MLMultiArray* dst = _cachedOutputArrays[outputName]; - if (!dst) return NO; - - size_t copied = copyMLArrayToFP16(outFeature.multiArrayValue, (__fp16*)dst.dataPointer); - if (copied == 0) return NO; - } - } - - return YES; -} - -- (MLMultiArray*)getOutputArray:(NSString*)name { - return _cachedOutputArrays[name]; -} - -@end - -namespace cactus { -namespace npu { - -ANEPrefill::ANEPrefill() : impl_(nullptr) {} - -ANEPrefill::~ANEPrefill() { - if (impl_) { - (void)(__bridge_transfer CactusANEPrefillImpl*)impl_; - impl_ = nullptr; - } -} - -ANEPrefill::ANEPrefill(ANEPrefill&& other) noexcept : impl_(other.impl_), - chunk_size_(other.chunk_size_), hidden_dim_(other.hidden_dim_), - num_layers_(other.num_layers_), num_kv_heads_(other.num_kv_heads_), - head_dim_(other.head_dim_) { - other.impl_ = nullptr; -} - -ANEPrefill& ANEPrefill::operator=(ANEPrefill&& other) noexcept { - if (this != &other) { - if (impl_) { - (void)(__bridge_transfer CactusANEPrefillImpl*)impl_; - } - impl_ = other.impl_; - chunk_size_ = other.chunk_size_; - hidden_dim_ = other.hidden_dim_; - num_layers_ = other.num_layers_; - num_kv_heads_ = other.num_kv_heads_; - head_dim_ = other.head_dim_; - other.impl_ = nullptr; - } - return *this; -} - -bool ANEPrefill::load(const std::string& model_path) { - @autoreleasepool { - CACTUS_LOG_INFO("npu", "ANEPrefill loading model: " << model_path); - NSString* path = [NSString stringWithUTF8String:model_path.c_str()]; - - if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { - CACTUS_LOG_DEBUG("npu", "ANEPrefill model file not found: " << model_path); - return false; - } - - CactusANEPrefillImpl* impl = [[CactusANEPrefillImpl alloc] initWithModelPath:path]; - - if (impl && impl.model) { - impl_ = (__bridge_retained void*)impl; - chunk_size_ = impl.chunkSize; - hidden_dim_ = impl.hiddenDim; - num_layers_ = impl.numLayers; - num_kv_heads_ = impl.numKvHeads; - head_dim_ = impl.headDim; - CACTUS_LOG_INFO("npu", "ANEPrefill model loaded successfully: " << model_path); - return true; - } - CACTUS_LOG_ERROR("npu", "ANEPrefill model load failed: " << model_path); - return false; - } -} - -bool ANEPrefill::is_available() const { - if (!impl_) return false; - CactusANEPrefillImpl* impl = (__bridge CactusANEPrefillImpl*)impl_; - return impl.model != nil; -} - -int ANEPrefill::get_chunk_size() const { return chunk_size_; } -int ANEPrefill::get_hidden_dim() const { return hidden_dim_; } -int ANEPrefill::get_num_layers() const { return num_layers_; } -int ANEPrefill::get_num_kv_heads() const { return num_kv_heads_; } -int ANEPrefill::get_head_dim() const { return head_dim_; } - -NPUPrefillDirectResult ANEPrefill::prefill_chunk_direct( - const std::vector<__fp16>& embeddings, - int position_offset, - const std::string& input_name) { - - NPUPrefillDirectResult result; - result.valid = false; - result.hidden = {nullptr, 0}; - result.k_caches.resize(num_layers_, {nullptr, 0}); - result.v_caches.resize(num_layers_, {nullptr, 0}); - - if (!impl_) return result; - - @autoreleasepool { - CactusANEPrefillImpl* impl = (__bridge CactusANEPrefillImpl*)impl_; - - NSString* inName = [NSString stringWithUTF8String:input_name.c_str()]; - - BOOL success = [impl predictDirectWithInput:inName - data:embeddings.data() - offset:position_offset]; - - if (!success) return result; - - MLMultiArray* hiddenArray = [impl getOutputArray:@"hidden"]; - if (hiddenArray) { - result.hidden.data = (__fp16*)hiddenArray.dataPointer; - result.hidden.count = hiddenArray.count; - } - - for (int layer = 0; layer < num_layers_; layer++) { - NSString* kName = [NSString stringWithFormat:@"k_%d", layer]; - NSString* vName = [NSString stringWithFormat:@"v_%d", layer]; - - MLMultiArray* kArray = [impl getOutputArray:kName]; - MLMultiArray* vArray = [impl getOutputArray:vName]; - - if (kArray) { - result.k_caches[layer].data = (__fp16*)kArray.dataPointer; - result.k_caches[layer].count = kArray.count; - } - if (vArray) { - result.v_caches[layer].data = (__fp16*)vArray.dataPointer; - result.v_caches[layer].count = vArray.count; - } - } - - result.valid = true; - } - - return result; -} - -std::unique_ptr create_prefill() { - return std::make_unique(); -} - -} // namespace npu -} // namespace cactus - -#else // !CACTUS_HAS_ANE - -namespace cactus { -namespace npu { - -std::unique_ptr create_encoder() { - return nullptr; -} - -bool is_npu_available() { - return false; -} - -std::unique_ptr create_prefill() { - return nullptr; -} - -} // namespace npu -} // namespace cactus - -#endif // CACTUS_HAS_ANE diff --git a/cactus/telemetry/telemetry.cpp b/cactus/telemetry/telemetry.cpp deleted file mode 100644 index 7fdbcfc6c..000000000 --- a/cactus/telemetry/telemetry.cpp +++ /dev/null @@ -1,1547 +0,0 @@ -#include "telemetry/telemetry.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#if defined(__APPLE__) -#include -#include -#endif -#include -#include -#include -#include -#include - -namespace cactus { -namespace telemetry { - -enum EventType { INIT = 0, COMPLETION = 1, EMBEDDING = 2, TRANSCRIPTION = 3, STREAM_TRANSCRIBE = 4 }; - -struct Event { - EventType type; - char model[128]; - bool success; - bool cloud_handoff; - double ttft_ms; - double prefill_tps; - double decode_tps; - double tps; - double response_time_ms; - double confidence; - double ram_usage_mb; - int tokens; - int prefill_tokens; - int decode_tokens; - double session_ttft_ms; - double session_tps; - double session_time_ms; - int session_tokens; - char message[256]; - char error[256]; - char function_calls[1024]; - std::chrono::system_clock::time_point timestamp; -}; -static bool enabled = false; -static int inference_active = 0; -static bool shutdown_called = false; -static bool atexit_registered = false; -static bool curl_initialized = false; -static bool cloud_disabled = false; -static std::string supabase_url = "https://vlqqczxwyaodtcdmdmlw.supabase.co"; -static std::string supabase_key = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZscXFjenh3eWFvZHRjZG1kbWx3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTE1MTg2MzIsImV4cCI6MjA2NzA5NDYzMn0.nBzqGuK9j6RZ6mOPWU2boAC_5H9XDs-fPpo5P3WZYbI"; -static std::string device_id; -static std::string project_id; -static std::string cloud_key; -static std::string project_scope; -static std::string device_model; -static std::string device_os; -static std::string device_os_version; -static std::string device_brand; -static std::string cactus_version; -#ifndef CACTUS_DEFAULT_FRAMEWORK -#define CACTUS_DEFAULT_FRAMEWORK "cpp" -#endif -static std::string framework = CACTUS_DEFAULT_FRAMEWORK; -static std::string custom_cache_location; -static std::string app_id; -static std::string device_registered_file; -static std::string project_registered_file; -static bool device_registered = false; -static bool project_registered = false; -static bool ids_ready = false; -static bool in_stream_mode = false; - -static std::mutex telemetry_mutex; -static std::condition_variable telemetry_lifecycle_cv; - -enum class TelemetryLifecycleState { - Stopped, - Running, - ShuttingDown, -}; - -static TelemetryLifecycleState lifecycle_state = TelemetryLifecycleState::Stopped; - -static bool can_record_event_locked() { - return enabled && ids_ready && lifecycle_state == TelemetryLifecycleState::Running; -} - -struct CloudSendResult { - bool payload_ok = false; - bool project_registered_ok = false; - bool device_registered_ok = false; -}; - -struct CloudConfigurationStateSnapshot { - std::string supabase_url; - std::string supabase_key; - std::string device_id; - std::string project_id; - std::string cloud_key; - std::string project_scope; - std::string framework; - std::string cactus_version; - std::string device_model; - std::string device_os; - std::string device_os_version; - std::string device_brand; - std::string app_id; - std::string device_registered_file; - std::string project_registered_file; - bool enabled = false; - bool cloud_disabled = false; - bool device_registered = false; - bool project_registered = false; -}; - -static std::string new_uuid(); -static std::string format_timestamp(const std::chrono::system_clock::time_point& tp); -static std::string model_basename(const char* model_path) { - if (!model_path) return {}; - std::string m(model_path); - size_t pos = m.find_last_of("/\\"); - if (pos != std::string::npos && pos + 1 < m.size()) { - m = m.substr(pos + 1); - } - return m; -} - -// Forward declarations for helpers used before definition -static std::string event_type_to_string(EventType t); -static bool event_type_from_string(const std::string& s, EventType& t); -static bool extract_string_field(const std::string& line, const std::string& key, std::string& out); -static bool extract_json_field(const std::string& line, const std::string& key, std::string& out); -static bool extract_bool_field(const std::string& line, const std::string& key, bool& out); -static bool extract_double_field(const std::string& line, const std::string& key, double& out); -static bool extract_int_field(const std::string& line, const std::string& key, int& out); -static bool extract_double_field_raw(const std::string& line, const std::string& key, double& out); -static void process_events(const std::vector& fresh_events); -static std::string get_telemetry_dir_locked(); -static CloudConfigurationStateSnapshot capture_cloud_configuration_state_snapshot_locked(); - -class TelemetryDispatcher { -public: - TelemetryDispatcher() = default; - - static TelemetryDispatcher& instance() { - static std::once_flag once; - static TelemetryDispatcher* singleton = nullptr; - std::call_once(once, [] { - singleton = new TelemetryDispatcher(); - }); - return *singleton; - } - - void start() { - std::lock_guard lock(queue_mutex_); - if (running_) return; - stop_ = false; - processing_ = false; - running_ = true; - worker_thread_ = std::thread([this] { worker_loop(); }); - } - - bool enqueue(const Event& event) { - std::lock_guard lock(queue_mutex_); - if (!running_) return false; - pending_events_.push_back(event); - enqueue_seq_ += 1; - queue_cv_.notify_one(); - return true; - } - - void flush() { - uint64_t target = 0; - { - std::unique_lock lock(queue_mutex_); - if (running_) { - target = enqueue_seq_; - queue_cv_.notify_one(); - flush_cv_.wait(lock, [this, target] { - return processed_seq_ >= target && !processing_; - }); - } - } - process_with_io_lock({}); - } - - void stop() { - { - std::lock_guard lock(queue_mutex_); - stop_ = true; - queue_cv_.notify_all(); - } - - if (worker_thread_.joinable()) { - worker_thread_.join(); - } - - std::lock_guard lock(queue_mutex_); - running_ = false; - stop_ = false; - processing_ = false; - pending_events_.clear(); - enqueue_seq_ = 0; - processed_seq_ = 0; - } - -private: - void process_with_io_lock(const std::vector& batch) { - std::lock_guard io_guard(io_mutex_); - process_events(batch); - } - - void worker_loop() { - while (true) { - std::vector batch; - uint64_t processed_count = 0; - { - std::unique_lock lock(queue_mutex_); - queue_cv_.wait(lock, [this] { - return stop_ || !pending_events_.empty(); - }); - if (stop_ && pending_events_.empty()) { - break; - } - batch.assign(pending_events_.begin(), pending_events_.end()); - processed_count = static_cast(batch.size()); - pending_events_.clear(); - processing_ = true; - } - - process_with_io_lock(batch); - - { - std::lock_guard lock(queue_mutex_); - processed_seq_ += processed_count; - processing_ = false; - flush_cv_.notify_all(); - } - } - - std::lock_guard lock(queue_mutex_); - running_ = false; - processing_ = false; - flush_cv_.notify_all(); - } - std::mutex queue_mutex_; - std::mutex io_mutex_; - std::condition_variable queue_cv_; - std::condition_variable flush_cv_; - std::deque pending_events_; - std::thread worker_thread_; - bool stop_ = false; - bool running_ = false; - bool processing_ = false; - uint64_t enqueue_seq_ = 0; - uint64_t processed_seq_ = 0; -}; - -static void mkdir_p(const std::string& path) { - if (path.empty()) return; - - std::string current; - for (size_t i = 0; i < path.size(); ++i) { - current += path[i]; - if (path[i] == '/' && i > 0) { - mkdir(current.c_str(), 0755); - } - } - mkdir(path.c_str(), 0755); -} - -static std::string get_telemetry_dir_locked() { - const std::string& cache_location = custom_cache_location; - if (!cache_location.empty()) { - mkdir_p(cache_location); - return cache_location; - } - -#if defined(__ANDROID__) - { - std::ifstream f("/proc/self/cmdline"); - if (f.is_open()) { - std::string pkg; - std::getline(f, pkg, '\0'); - size_t colon = pkg.find(':'); - if (colon != std::string::npos) pkg = pkg.substr(0, colon); - if (!pkg.empty()) { - uid_t uid = getuid(); - int user_id = static_cast((uid - 10000) / 100000); - std::string dir = "/data/user/" + std::to_string(user_id) + "/" + pkg + "/cache/cactus/telemetry"; - mkdir_p(dir); - struct stat st; - if (stat(dir.c_str(), &st) == 0) return dir; - dir = "/data/data/" + pkg + "/cache/cactus/telemetry"; - mkdir_p(dir); - if (stat(dir.c_str(), &st) == 0) return dir; - } - } - } -#endif - - const char* home = getenv("HOME"); - if (!home) home = "/tmp"; - std::string dir = std::string(home) + "/Library/Caches/cactus/telemetry"; - mkdir((std::string(home) + "/Library").c_str(), 0755); - mkdir((std::string(home) + "/Library/Caches").c_str(), 0755); - mkdir((std::string(home) + "/Library/Caches/cactus").c_str(), 0755); - mkdir(dir.c_str(), 0755); - return dir; -} - -static std::string cloud_api_key_cache_file_locked() { - return get_telemetry_dir_locked() + "/cloud_api_key"; -} - -static std::string load_or_create_id(const std::string& file) { - std::ifstream in(file); - if (in.is_open()) { - std::string line; - if (std::getline(in, line) && !line.empty()) { - return line; - } - } - std::string id = new_uuid(); - std::ofstream out(file, std::ios::trunc); - if (out.is_open()) { - out << id; - } - return id; -} - -static bool load_registered_flag(const std::string& file) { - std::ifstream in(file); - if (!in.is_open()) return false; - std::string line; - if (std::getline(in, line) && !line.empty()) { - return line[0] == '1'; - } - return false; -} - -static void persist_registered_flag(const std::string& file) { - std::ofstream out(file, std::ios::trunc); - if (out.is_open()) { - out << "1"; - } -} - -static std::string sysctl_string(const char* key) { -#if defined(__APPLE__) - size_t size = 0; - if (sysctlbyname(key, nullptr, &size, nullptr, 0) != 0 || size == 0) return {}; - std::string out(size, '\0'); - if (sysctlbyname(key, out.data(), &size, nullptr, 0) != 0) return {}; - if (!out.empty() && out.back() == '\0') out.pop_back(); - return out; -#else - (void)key; - return {}; -#endif -} - -static Event make_event(EventType type, const char* model, bool success, double ttft_ms, double tps, double response_time_ms, int tokens, const char* message) { - Event e; - e.type = type; - e.success = success; - e.cloud_handoff = false; - e.ttft_ms = ttft_ms; - e.prefill_tps = 0.0; - e.decode_tps = tps; - e.tps = tps; - e.response_time_ms = response_time_ms; - e.confidence = 0.0; - e.ram_usage_mb = 0.0; - e.tokens = tokens; - e.prefill_tokens = 0; - e.decode_tokens = tokens; - e.session_ttft_ms = 0.0; - e.session_tps = 0.0; - e.session_time_ms = 0.0; - e.session_tokens = 0; - e.timestamp = std::chrono::system_clock::now(); - std::memset(e.model, 0, sizeof(e.model)); - std::memset(e.message, 0, sizeof(e.message)); - std::memset(e.error, 0, sizeof(e.error)); - std::memset(e.function_calls, 0, sizeof(e.function_calls)); - std::string safe_model = model_basename(model); - if (!safe_model.empty()) std::strncpy(e.model, safe_model.c_str(), sizeof(e.model)-1); - if (message) std::strncpy(e.message, message, sizeof(e.message)-1); - return e; -} - -static Event make_event_extended(EventType type, const char* model, const CompletionMetrics& metrics) { - Event e; - e.type = type; - e.success = metrics.success; - e.cloud_handoff = metrics.cloud_handoff; - e.ttft_ms = metrics.ttft_ms; - e.prefill_tps = metrics.prefill_tps; - e.decode_tps = metrics.decode_tps; - e.tps = metrics.decode_tps; - e.response_time_ms = metrics.response_time_ms; - e.confidence = metrics.confidence; - e.ram_usage_mb = metrics.ram_usage_mb; - e.tokens = static_cast(metrics.prefill_tokens + metrics.decode_tokens); - e.prefill_tokens = static_cast(metrics.prefill_tokens); - e.decode_tokens = static_cast(metrics.decode_tokens); - e.session_ttft_ms = 0.0; - e.session_tps = 0.0; - e.session_time_ms = 0.0; - e.session_tokens = 0; - e.timestamp = std::chrono::system_clock::now(); - std::memset(e.model, 0, sizeof(e.model)); - std::memset(e.message, 0, sizeof(e.message)); - std::memset(e.error, 0, sizeof(e.error)); - std::memset(e.function_calls, 0, sizeof(e.function_calls)); - std::string safe_model = model_basename(model); - if (!safe_model.empty()) std::strncpy(e.model, safe_model.c_str(), sizeof(e.model)-1); - if (!metrics.success && metrics.error_message) std::strncpy(e.error, metrics.error_message, sizeof(e.error)-1); - if (metrics.function_calls_json) std::strncpy(e.function_calls, metrics.function_calls_json, sizeof(e.function_calls)-1); - return e; -} - -static bool parse_event_line(const std::string& line, Event& out) { - std::string type_str; - if (!extract_string_field(line, "event_type", type_str)) return false; - EventType et; - if (!event_type_from_string(type_str, et)) return false; - - std::string model; - extract_string_field(line, "model", model); - bool success = false; - extract_bool_field(line, "success", success); - bool cloud_handoff = false; - extract_bool_field(line, "cloud_handoff", cloud_handoff); - double ttft = 0.0; - extract_double_field(line, "ttft", ttft); - double prefill_tps = 0.0; - extract_double_field(line, "prefill_tps", prefill_tps); - double decode_tps = 0.0; - extract_double_field(line, "decode_tps", decode_tps); - double tps = 0.0; - extract_double_field(line, "tps", tps); - if (decode_tps == 0.0 && tps != 0.0) decode_tps = tps; - double response_time = 0.0; - extract_double_field(line, "response_time", response_time); - double confidence = 0.0; - extract_double_field(line, "confidence", confidence); - double ram_usage_mb = 0.0; - extract_double_field(line, "ram_usage_mb", ram_usage_mb); - int tokens = 0; - extract_int_field(line, "tokens", tokens); - int prefill_tokens = 0; - extract_int_field(line, "prefill_tokens", prefill_tokens); - int decode_tokens = 0; - extract_int_field(line, "decode_tokens", decode_tokens); - double session_ttft_ms = 0.0; - extract_double_field(line, "session_ttft", session_ttft_ms); - double session_tps = 0.0; - extract_double_field(line, "session_tps", session_tps); - double session_time_ms = 0.0; - extract_double_field(line, "session_time_ms", session_time_ms); - int session_tokens = 0; - extract_int_field(line, "session_tokens", session_tokens); - std::string message; - extract_string_field(line, "message", message); - std::string error; - extract_string_field(line, "error", error); - std::string function_calls; - extract_json_field(line, "function_calls", function_calls); - double ts_ms = 0.0; - if (!extract_double_field_raw(line, "ts_ms", ts_ms)) { - extract_double_field(line, "ts_ms", ts_ms); - } - auto ts_point = std::chrono::system_clock::now(); - if (ts_ms > 0.0) { - ts_point = std::chrono::system_clock::time_point(std::chrono::milliseconds(static_cast(ts_ms))); - } - - out = make_event(et, - model.empty() ? nullptr : model.c_str(), - success, - ttft, - tps, - response_time, - tokens, - message.empty() ? nullptr : message.c_str()); - out.timestamp = ts_point; - out.cloud_handoff = cloud_handoff; - out.prefill_tps = prefill_tps; - out.decode_tps = decode_tps; - out.confidence = confidence; - out.ram_usage_mb = ram_usage_mb; - out.prefill_tokens = prefill_tokens; - out.decode_tokens = decode_tokens; - out.session_ttft_ms = session_ttft_ms; - out.session_tps = session_tps; - out.session_time_ms = session_time_ms; - out.session_tokens = session_tokens; - if (!error.empty()) std::strncpy(out.error, error.c_str(), sizeof(out.error)-1); - if (!function_calls.empty()) std::strncpy(out.function_calls, function_calls.c_str(), sizeof(out.function_calls)-1); - return true; -} - -static std::string event_type_to_string(EventType t) { - switch (t) { - case INIT: return "init"; - case COMPLETION: return "completion"; - case EMBEDDING: return "embedding"; - case TRANSCRIPTION: return "transcription"; - case STREAM_TRANSCRIBE: return "stream_transcribe"; - default: return "unknown"; - } -} - -static bool event_type_from_string(const std::string& s, EventType& t) { - if (s == "init") { t = INIT; return true; } - if (s == "completion") { t = COMPLETION; return true; } - if (s == "embedding") { t = EMBEDDING; return true; } - if (s == "transcription") { t = TRANSCRIPTION; return true; } - if (s == "stream_transcribe") { t = STREAM_TRANSCRIBE; return true; } - return false; -} - -static bool extract_string_field(const std::string& line, const std::string& key, std::string& out) { - std::string needle = "\"" + key + "\":"; - size_t pos = line.find(needle); - if (pos == std::string::npos) return false; - pos += needle.size(); - while (pos < line.size() && line[pos] == ' ') pos++; - if (pos >= line.size()) return false; - if (line[pos] == '"') { - pos++; - size_t end = line.find('"', pos); - if (end == std::string::npos) return false; - out = line.substr(pos, end - pos); - return true; - } - size_t end = line.find_first_of(",}", pos); - if (end == std::string::npos) end = line.size(); - out = line.substr(pos, end - pos); - if (out == "null") out.clear(); - return true; -} - -static bool extract_json_field(const std::string& line, const std::string& key, std::string& out) { - std::string needle = "\"" + key + "\":"; - size_t pos = line.find(needle); - if (pos == std::string::npos) return false; - pos += needle.size(); - while (pos < line.size() && line[pos] == ' ') pos++; - if (pos >= line.size()) return false; - - char start_char = line[pos]; - if (start_char == '[' || start_char == '{') { - char end_char = (start_char == '[') ? ']' : '}'; - int depth = 0; - size_t end = pos; - bool in_string = false; - bool escape_next = false; - - for (; end < line.size(); end++) { - if (escape_next) { - escape_next = false; - continue; - } - if (line[end] == '\\') { - escape_next = true; - continue; - } - if (line[end] == '"') { - in_string = !in_string; - continue; - } - if (in_string) continue; - - if (line[end] == start_char) { - depth++; - } else if (line[end] == end_char) { - depth--; - if (depth == 0) { - out = line.substr(pos, end - pos + 1); - return true; - } - } - } - return false; - } - - size_t end = line.find_first_of(",}", pos); - if (end == std::string::npos) end = line.size(); - out = line.substr(pos, end - pos); - if (out == "null") out.clear(); - return true; -} - -static bool extract_bool_field(const std::string& line, const std::string& key, bool& out) { - std::string raw; - if (!extract_string_field(line, key, raw)) return false; - if (raw == "true") { out = true; return true; } - if (raw == "false") { out = false; return true; } - return false; -} - -static bool extract_double_field(const std::string& line, const std::string& key, double& out) { - std::string raw; - if (!extract_string_field(line, key, raw)) return false; - try { - out = std::stod(raw); - return true; - } catch (...) { - return false; - } -} - -// Helper used when the field is already numeric (no quotes) in the log cache. -static bool extract_double_field_raw(const std::string& line, const std::string& key, double& out) { - std::string needle = "\"" + key + "\":"; - size_t pos = line.find(needle); - if (pos == std::string::npos) return false; - pos += needle.size(); - while (pos < line.size() && line[pos] == ' ') pos++; - if (pos >= line.size()) return false; - size_t end = line.find_first_of(",}", pos); - if (end == std::string::npos) end = line.size(); - std::string raw = line.substr(pos, end - pos); - try { - out = std::stod(raw); - return true; - } catch (...) { - return false; - } -} - -static bool extract_int_field(const std::string& line, const std::string& key, int& out) { - std::string raw; - if (!extract_string_field(line, key, raw)) return false; - try { - out = std::stoi(raw); - return true; - } catch (...) { - return false; - } -} - -static std::string new_uuid() { - static thread_local std::mt19937_64 rng(std::random_device{}()); - uint64_t a = rng(); - uint64_t b = rng(); - a = (a & 0xffffffffffff0fffULL) | 0x0000000000004000ULL; - b = (b & 0x3fffffffffffffffULL) | 0x8000000000000000ULL; - std::ostringstream oss; - oss << std::hex << std::setfill('0'); - oss << std::setw(8) << ((a >> 32) & 0xffffffffULL); - oss << "-" << std::setw(4) << ((a >> 16) & 0xffffULL); - oss << "-" << std::setw(4) << (a & 0xffffULL); - oss << "-" << std::setw(4) << ((b >> 48) & 0xffffULL); - oss << "-" << std::setw(12) << (b & 0xffffffffffffULL); - return oss.str(); -} - -static uint64_t fnv1a_64(const std::string& s) { - uint64_t h = 14695981039346656037ULL; - for (unsigned char c : s) { h ^= c; h *= 1099511628211ULL; } - return h; -} - -static std::string uuid_from_string(const std::string& input) { - uint64_t a = fnv1a_64(input); - uint64_t b = fnv1a_64(input + "\xff"); - a = (a & 0xffffffffffff0fffULL) | 0x0000000000004000ULL; - b = (b & 0x3fffffffffffffffULL) | 0x8000000000000000ULL; - std::ostringstream oss; - oss << std::hex << std::setfill('0'); - oss << std::setw(8) << ((a >> 32) & 0xffffffffULL); - oss << "-" << std::setw(4) << ((a >> 16) & 0xffffULL); - oss << "-" << std::setw(4) << (a & 0xffffULL); - oss << "-" << std::setw(4) << ((b >> 48) & 0xffffULL); - oss << "-" << std::setw(12) << (b & 0xffffffffffffULL); - return oss.str(); -} - -static std::string read_git_remote_url() { - const char* cwd_env = std::getenv("PWD"); - if (!cwd_env) return ""; - std::string dir = cwd_env; - while (!dir.empty()) { - std::ifstream f(dir + "/.git/config"); - if (f.is_open()) { - std::string line; - bool in_origin = false; - while (std::getline(f, line)) { - if (line.find("[remote \"origin\"]") != std::string::npos) { - in_origin = true; - } else if (in_origin) { - size_t pos = line.find("url = "); - if (pos != std::string::npos) return line.substr(pos + 6); - if (!line.empty() && line[0] == '[') break; - } - } - return ""; - } - size_t pos = dir.rfind('/'); - if (pos == 0 || pos == std::string::npos) break; - dir = dir.substr(0, pos); - } - return ""; -} - -#if defined(__APPLE__) -static std::string detect_app_id() { - CFBundleRef bundle = CFBundleGetMainBundle(); - if (!bundle) return ""; - CFStringRef id = (CFStringRef)CFBundleGetValueForInfoDictionaryKey(bundle, CFSTR("CFBundleIdentifier")); - if (!id) return ""; - char buf[256]; - return CFStringGetCString(id, buf, sizeof(buf), kCFStringEncodingUTF8) ? buf : ""; -} -#elif defined(__linux__) || defined(__ANDROID__) -static std::string detect_app_id() { - std::ifstream f("/proc/self/cmdline"); - if (!f.is_open()) return ""; - std::string s; - std::getline(f, s, '\0'); - size_t colon = s.find(':'); - if (colon != std::string::npos) s = s.substr(0, colon); - return s; -} -#else -static std::string detect_app_id() { return ""; } -#endif - -static std::string extract_android_package_from_path(const std::string& path) { - for (const char* prefix : {"/data/user/", "/data/user_de/", "/data/data/"}) { - size_t pos = path.find(prefix); - if (pos == std::string::npos) continue; - size_t start = pos + strlen(prefix); - if (strcmp(prefix, "/data/data/") != 0) { - start = path.find('/', start); - if (start == std::string::npos) continue; - start++; - } - size_t end = path.find('/', start); - if (end == std::string::npos) end = path.size(); - std::string pkg = path.substr(start, end - start); - if (!pkg.empty()) return pkg; - } - return ""; -} - -static std::string format_timestamp(const std::chrono::system_clock::time_point& tp) { - using namespace std::chrono; - auto secs = time_point_cast(tp); - auto ms = duration_cast(tp - secs).count(); - std::time_t t = system_clock::to_time_t(secs); - std::tm tm{}; -#if defined(_WIN32) - gmtime_s(&tm, &t); -#else - gmtime_r(&t, &tm); -#endif - char buf[32]; - std::snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03lldZ", - tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, - tm.tm_hour, tm.tm_min, tm.tm_sec, - static_cast(ms)); - return std::string(buf); -} - -[[maybe_unused]] static bool is_valid_uuid(const std::string& s) { - // Simple check used only for validation when accepting user-provided IDs. - if (s.size() != 36) return false; - for (size_t i = 0; i < s.size(); ++i) { - char c = s[i]; - if (i == 8 || i == 13 || i == 18 || i == 23) { - if (c != '-') return false; - } else { - bool hex = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); - if (!hex) return false; - } - } - return true; -} - -static std::string escape_json_string(const char* str) { - if (!str) return ""; - std::string result; - result.reserve(std::strlen(str)); - for (const char* p = str; *p; ++p) { - switch (*p) { - case '"': result += "\\\""; break; - case '\\': result += "\\\\"; break; - case '\b': result += "\\b"; break; - case '\f': result += "\\f"; break; - case '\n': result += "\\n"; break; - case '\r': result += "\\r"; break; - case '\t': result += "\\t"; break; - default: - if (static_cast(*p) < 0x20) { - char buf[7]; - snprintf(buf, sizeof(buf), "\\u%04x", static_cast(*p)); - result += buf; - } else { - result += *p; - } - break; - } - } - return result; -} - -static void collect_device_info() { - struct utsname u; - if (uname(&u) == 0) { - const std::string sysname = u.sysname; - device_os = (sysname == "Darwin") ? "macos" : sysname; - device_model = u.machine; - device_os_version = u.release; - device_brand = "unknown"; -#if defined(__APPLE__) - // Prefer user-facing macOS info over Darwin kernel version. - std::string hw_model = sysctl_string("hw.model"); - std::string os_product = sysctl_string("kern.osproductversion"); - if (!hw_model.empty()) device_model = hw_model; - if (!os_product.empty()) device_os_version = os_product; - device_brand = "apple"; -#elif defined(__ANDROID__) - device_brand = "android"; -#endif - } -} - -static bool parse_version_line(const std::string& line) { - size_t start = line.find_first_not_of(" \t\r\n"); - size_t end = line.find_last_not_of(" \t\r\n"); - if (start != std::string::npos && end != std::string::npos) { - cactus_version = line.substr(start, end - start + 1); - return true; - } - return false; -} - -static void read_cactus_version() { - const char* version_paths[] = { - "CACTUS_VERSION", - "../CACTUS_VERSION", - "../../CACTUS_VERSION", - "../../../CACTUS_VERSION" - }; - - for (const char* path : version_paths) { - std::ifstream file(path); - if (file.is_open()) { - std::string line; - if (std::getline(file, line) && parse_version_line(line)) return; - } - } - -#if defined(__APPLE__) - CFBundleRef bundle = CFBundleGetMainBundle(); - if (bundle) { - CFURLRef url = CFBundleCopyResourceURL(bundle, CFSTR("CACTUS_VERSION"), NULL, NULL); - if (url) { - char fspath[4096]; - if (CFURLGetFileSystemRepresentation(url, true, (UInt8*)fspath, sizeof(fspath))) { - std::ifstream file(fspath); - if (file.is_open()) { - std::string line; - if (std::getline(file, line) && parse_version_line(line)) { - CFRelease(url); - return; - } - } - } - CFRelease(url); - } - } -#endif - -#ifdef CACTUS_COMPILE_TIME_VERSION - cactus_version = CACTUS_COMPILE_TIME_VERSION; - return; -#endif - - cactus_version = ""; -} - -static void apply_curl_tls_trust(CURL* curl) { - if (!curl) return; - const char* ca_bundle = std::getenv("CACTUS_CA_BUNDLE"); - if (ca_bundle && ca_bundle[0] != '\0') { - curl_easy_setopt(curl, CURLOPT_CAINFO, ca_bundle); - } -#if defined(__ANDROID__) - const char* ca_path = std::getenv("CACTUS_CA_PATH"); - if (ca_path && ca_path[0] != '\0') { - curl_easy_setopt(curl, CURLOPT_CAPATH, ca_path); - } else { - curl_easy_setopt(curl, CURLOPT_CAPATH, "/system/etc/security/cacerts"); - } -#endif -} - -static bool ensure_project_row_remote(CURL* curl, const CloudConfigurationStateSnapshot& snapshot) { - if (snapshot.project_id.empty() || snapshot.project_registered) return true; - std::string url = snapshot.supabase_url + "/rest/v1/projects"; - std::ostringstream payload; - payload << "[{"; - payload << "\"project_key\":\"" << snapshot.project_id << "\""; - if (!snapshot.project_scope.empty()) { - payload << ",\"name\":\"" << snapshot.project_scope << "\""; - } - payload << "}]"; - struct curl_slist* headers = nullptr; - headers = curl_slist_append(headers, "Content-Type: application/json"); - std::string apikey_hdr = std::string("apikey: ") + snapshot.supabase_key; - std::string auth_hdr = std::string("Authorization: Bearer ") + snapshot.supabase_key; - headers = curl_slist_append(headers, apikey_hdr.c_str()); - headers = curl_slist_append(headers, auth_hdr.c_str()); - headers = curl_slist_append(headers, "Prefer: resolution=ignore-duplicates"); - headers = curl_slist_append(headers, "Content-Profile: cactus"); - headers = curl_slist_append(headers, "Accept-Profile: cactus"); - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); - curl_easy_setopt(curl, CURLOPT_POST, 1L); - std::string body = payload.str(); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size()); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); - curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L); - apply_curl_tls_trust(curl); - CURLcode res = curl_easy_perform(curl); - long code = 0; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code); - if (headers) curl_slist_free_all(headers); - bool ok = (res == CURLE_OK) && ((code >= 200 && code < 300) || code == 409); - return ok; -} - -static bool ensure_device_row_remote(CURL* curl, const CloudConfigurationStateSnapshot& snapshot) { - if (snapshot.device_registered) return true; - if (snapshot.device_id.empty()) return false; - std::string url = snapshot.supabase_url + "/rest/v1/devices"; - std::ostringstream payload; - payload << "[{"; - payload << "\"id\":\"" << snapshot.device_id << "\""; - payload << ",\"device_id\":\"" << snapshot.device_id << "\""; - if (!snapshot.device_model.empty()) payload << ",\"model\":\"" << snapshot.device_model << "\""; - if (!snapshot.device_os.empty()) payload << ",\"os\":\"" << snapshot.device_os << "\""; - if (!snapshot.device_os_version.empty()) payload << ",\"os_version\":\"" << snapshot.device_os_version << "\""; - if (!snapshot.device_brand.empty()) payload << ",\"brand\":\"" << snapshot.device_brand << "\""; - payload << "}]"; - struct curl_slist* headers = nullptr; - headers = curl_slist_append(headers, "Content-Type: application/json"); - std::string apikey_hdr = std::string("apikey: ") + snapshot.supabase_key; - std::string auth_hdr = std::string("Authorization: Bearer ") + snapshot.supabase_key; - headers = curl_slist_append(headers, apikey_hdr.c_str()); - headers = curl_slist_append(headers, auth_hdr.c_str()); - headers = curl_slist_append(headers, "Prefer: resolution=ignore-duplicates"); - headers = curl_slist_append(headers, "Content-Profile: cactus"); - headers = curl_slist_append(headers, "Accept-Profile: cactus"); - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); - curl_easy_setopt(curl, CURLOPT_POST, 1L); - std::string body = payload.str(); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size()); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); - curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L); - apply_curl_tls_trust(curl); - CURLcode res = curl_easy_perform(curl); - long code = 0; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code); - if (headers) curl_slist_free_all(headers); - bool ok = (res == CURLE_OK) && ((code >= 200 && code < 300) || code == 409); - return ok; -} - -static bool send_payload(CURL* curl, const std::string& url, const std::string& body, const CloudConfigurationStateSnapshot& snapshot) { - struct curl_slist* headers = nullptr; - headers = curl_slist_append(headers, "Content-Type: application/json"); - std::string apikey_hdr = std::string("apikey: ") + snapshot.supabase_key; - std::string auth_hdr = std::string("Authorization: Bearer ") + snapshot.supabase_key; - headers = curl_slist_append(headers, apikey_hdr.c_str()); - headers = curl_slist_append(headers, auth_hdr.c_str()); - headers = curl_slist_append(headers, "Prefer: return=minimal"); - headers = curl_slist_append(headers, "Content-Profile: cactus"); - headers = curl_slist_append(headers, "Accept-Profile: cactus"); - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); - curl_easy_setopt(curl, CURLOPT_POST, 1L); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size()); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); - curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L); - apply_curl_tls_trust(curl); - CURLcode res = curl_easy_perform(curl); - long code = 0; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code); - if (headers) curl_slist_free_all(headers); - return (res == CURLE_OK) && (code >= 200 && code < 300); -} - -static CloudSendResult send_batch_to_cloud(const std::vector& local, const CloudConfigurationStateSnapshot& snapshot) { - CloudSendResult result; - if (!snapshot.enabled) return result; - if (local.empty()) { - result.payload_ok = true; - return result; - } - if (snapshot.cloud_disabled) return result; - if (snapshot.device_id.empty()) return result; - CURL* curl = curl_easy_init(); - if (!curl) return result; - result.project_registered_ok = ensure_project_row_remote(curl, snapshot); - if (!snapshot.device_registered) { - result.device_registered_ok = ensure_device_row_remote(curl, snapshot); - } else { - result.device_registered_ok = true; - } - std::string url = snapshot.supabase_url + "/rest/v1/logs"; - std::ostringstream payload; - payload << "["; - for (size_t i = 0; i < local.size(); ++i) { - const Event& e = local[i]; - payload << "{"; - payload << "\"event_type\":\"" << event_type_to_string(e.type) << "\","; - payload << "\"model\":\"" << escape_json_string(e.model) << "\","; - payload << "\"success\":" << (e.success ? "true" : "false") << ","; - payload << "\"cloud_handoff\":" << (e.cloud_handoff ? "true" : "false") << ","; - if (e.type == INIT || !e.success) { - payload << "\"ttft\":null,"; - payload << "\"prefill_tps\":null,"; - payload << "\"decode_tps\":null,"; - payload << "\"tps\":null,"; - } else { - payload << "\"ttft\":" << e.ttft_ms << ","; - payload << "\"prefill_tps\":" << e.prefill_tps << ","; - payload << "\"decode_tps\":" << e.decode_tps << ","; - payload << "\"tps\":" << e.tps << ","; - } - if (!e.success) { - payload << "\"response_time\":null,"; - payload << "\"ram_usage_mb\":null,"; - } else { - payload << "\"response_time\":" << e.response_time_ms << ","; - payload << "\"ram_usage_mb\":" << e.ram_usage_mb << ","; - } - if (e.type == INIT || !e.success) { - payload << "\"confidence\":null,"; - payload << "\"tokens\":null,"; - payload << "\"prefill_tokens\":null,"; - payload << "\"decode_tokens\":null,"; - } else { - payload << "\"confidence\":" << e.confidence << ","; - payload << "\"tokens\":" << e.tokens << ","; - payload << "\"prefill_tokens\":" << e.prefill_tokens << ","; - payload << "\"decode_tokens\":" << e.decode_tokens << ","; - } - if (e.type == STREAM_TRANSCRIBE) { - payload << "\"session_ttft\":" << e.session_ttft_ms << ","; - payload << "\"session_tps\":" << e.session_tps << ","; - payload << "\"session_time_ms\":" << e.session_time_ms << ","; - payload << "\"session_tokens\":" << e.session_tokens << ","; - } else { - payload << "\"session_ttft\":null,"; - payload << "\"session_tps\":null,"; - payload << "\"session_time_ms\":null,"; - payload << "\"session_tokens\":null,"; - } - payload << "\"created_at\":\"" << format_timestamp(e.timestamp) << "\","; - if (!snapshot.project_id.empty()) { - payload << "\"project_id\":\"" << snapshot.project_id << "\","; - } - if (!snapshot.cloud_key.empty()) { - payload << "\"key_hash\":\"" << snapshot.cloud_key << "\","; - } - payload << "\"framework\":\"" << snapshot.framework << "\","; - if (!snapshot.cactus_version.empty()) { - payload << "\"framework_version\":\"" << snapshot.cactus_version << "\","; - } - payload << "\"device_id\":\"" << snapshot.device_id << "\""; - if (!snapshot.app_id.empty()) { - payload << ",\"app_id\":\"" << escape_json_string(snapshot.app_id.c_str()) << "\""; - } else { - payload << ",\"app_id\":null"; - } - if (e.message[0] != '\0') { - payload << ",\"message\":\"" << escape_json_string(e.message) << "\""; - } else { - payload << ",\"message\":null"; - } - if (e.error[0] != '\0') { - payload << ",\"error\":\"" << escape_json_string(e.error) << "\""; - } else { - payload << ",\"error\":null"; - } - payload << ",\"function_calls\":null"; - payload << "}"; - if (i + 1 < local.size()) payload << ","; - } - payload << "]"; - result.payload_ok = send_payload(curl, url, payload.str(), snapshot); - curl_easy_cleanup(curl); - return result; -} - -static void write_events_to_cache_in_dir(const std::vector& local, const std::string& dir) { - for (const auto &e : local) { - std::ostringstream oss; - oss << "{\"event_type\":\"" << event_type_to_string(e.type) << "\","; - oss << "\"model\":\"" << escape_json_string(e.model) << "\","; - oss << "\"success\":" << (e.success ? "true" : "false") << ","; - oss << "\"cloud_handoff\":" << (e.cloud_handoff ? "true" : "false") << ","; - if (e.type == INIT || !e.success) { - oss << "\"ttft\":null,"; - oss << "\"prefill_tps\":null,"; - oss << "\"decode_tps\":null,"; - oss << "\"tps\":null,"; - } else { - oss << "\"ttft\":" << e.ttft_ms << ","; - oss << "\"prefill_tps\":" << e.prefill_tps << ","; - oss << "\"decode_tps\":" << e.decode_tps << ","; - oss << "\"tps\":" << e.tps << ","; - } - if (!e.success) { - oss << "\"response_time\":null,"; - oss << "\"ram_usage_mb\":null,"; - } else { - oss << "\"response_time\":" << e.response_time_ms << ","; - oss << "\"ram_usage_mb\":" << e.ram_usage_mb << ","; - } - if (e.type == INIT || !e.success) { - oss << "\"confidence\":null,"; - oss << "\"tokens\":null,"; - oss << "\"prefill_tokens\":null,"; - oss << "\"decode_tokens\":null,"; - } else { - oss << "\"confidence\":" << e.confidence << ","; - oss << "\"tokens\":" << e.tokens << ","; - oss << "\"prefill_tokens\":" << e.prefill_tokens << ","; - oss << "\"decode_tokens\":" << e.decode_tokens << ","; - } - if (e.type == STREAM_TRANSCRIBE) { - oss << "\"session_ttft\":" << e.session_ttft_ms << ","; - oss << "\"session_tps\":" << e.session_tps << ","; - oss << "\"session_time_ms\":" << e.session_time_ms << ","; - oss << "\"session_tokens\":" << e.session_tokens; - } else { - oss << "\"session_ttft\":null,"; - oss << "\"session_tps\":null,"; - oss << "\"session_time_ms\":null,"; - oss << "\"session_tokens\":null"; - } - oss << ",\"ts_ms\":" << std::chrono::duration_cast(e.timestamp.time_since_epoch()).count(); - if (e.message[0] != '\0') { - oss << ",\"message\":\"" << escape_json_string(e.message) << "\""; - } else { - oss << ",\"message\":null"; - } - if (e.error[0] != '\0') { - oss << ",\"error\":\"" << escape_json_string(e.error) << "\""; - } else { - oss << ",\"error\":null"; - } - oss << ",\"function_calls\":null"; - if (!app_id.empty()) { - oss << ",\"app_id\":\"" << escape_json_string(app_id.c_str()) << "\""; - } else { - oss << ",\"app_id\":null"; - } - oss << "}"; - std::string file = dir + "/" + event_type_to_string(e.type) + ".log"; - std::ofstream out(file, std::ios::app); - if (out.is_open()) { - out << oss.str() << "\n"; - out.close(); - } - } -} - -static std::vector load_cached_events_in_dir(const std::string& dir) { - std::vector events; - DIR* d = opendir(dir.c_str()); - if (!d) return events; - struct dirent* ent; - while ((ent = readdir(d)) != nullptr) { - std::string name = ent->d_name; - if (name.size() < 5 || name.substr(name.size() - 4) != ".log") continue; - std::ifstream in(dir + "/" + name); - std::string line; - while (std::getline(in, line)) { - Event e; - if (parse_event_line(line, e)) { - events.push_back(e); - } - } - } - closedir(d); - return events; -} - -static void clear_cache_files_in_dir(const std::string& dir) { - DIR* d = opendir(dir.c_str()); - if (!d) return; - struct dirent* ent; - while ((ent = readdir(d)) != nullptr) { - std::string name = ent->d_name; - if (name.size() < 5 || name.substr(name.size() - 4) != ".log") continue; - std::remove((dir + "/" + name).c_str()); - } - closedir(d); -} - -static CloudConfigurationStateSnapshot capture_cloud_configuration_state_snapshot_locked() { - CloudConfigurationStateSnapshot snapshot; - snapshot.supabase_url = supabase_url; - snapshot.supabase_key = supabase_key; - snapshot.device_id = device_id; - snapshot.project_id = project_id; - snapshot.cloud_key = cloud_key; - snapshot.project_scope = project_scope; - snapshot.framework = framework; - snapshot.cactus_version = cactus_version; - snapshot.device_model = device_model; - snapshot.device_os = device_os; - snapshot.device_os_version = device_os_version; - snapshot.device_brand = device_brand; - snapshot.app_id = app_id; - snapshot.device_registered_file = device_registered_file; - snapshot.project_registered_file = project_registered_file; - snapshot.enabled = enabled; - snapshot.cloud_disabled = cloud_disabled; - snapshot.device_registered = device_registered; - snapshot.project_registered = project_registered; - return snapshot; -} - -static void process_events(const std::vector& fresh_events) { - std::vector events; - CloudConfigurationStateSnapshot snapshot; - std::string telemetry_dir; - - { - std::lock_guard guard(telemetry_mutex); - telemetry_dir = get_telemetry_dir_locked(); - events = load_cached_events_in_dir(telemetry_dir); - if (!fresh_events.empty()) { - events.insert(events.end(), fresh_events.begin(), fresh_events.end()); - } - if (events.empty()) return; - snapshot = capture_cloud_configuration_state_snapshot_locked(); - } - - CloudSendResult send_result = send_batch_to_cloud(events, snapshot); - - { - std::lock_guard guard(telemetry_mutex); - if (send_result.project_registered_ok && !project_registered) { - if (!project_registered_file.empty()) { - persist_registered_flag(project_registered_file); - } - project_registered = true; - } - if (send_result.device_registered_ok && !device_registered) { - if (!device_registered_file.empty()) { - persist_registered_flag(device_registered_file); - } - device_registered = true; - } - - if (send_result.payload_ok) { - clear_cache_files_in_dir(telemetry_dir); - } else if (!fresh_events.empty()) { - write_events_to_cache_in_dir(fresh_events, telemetry_dir); - } - } -} - -void init(const char* project_id_param, const char* project_scope_param, const char* cloud_key_param) { - std::unique_lock lifecycle_guard(telemetry_mutex); - telemetry_lifecycle_cv.wait(lifecycle_guard, [] { - return lifecycle_state != TelemetryLifecycleState::ShuttingDown; - }); - - std::string scope = project_scope_param ? project_scope_param : "default"; - - bool cloud_disabled_from_env = false; - if (const char* env = std::getenv("CACTUS_NO_CLOUD_TELE")) { - if (env[0] != '\0' && !(env[0] == '0' && env[1] == '\0')) { - cloud_disabled_from_env = true; - } - } - const char* env_url = std::getenv("CACTUS_SUPABASE_URL"); - const char* env_key = std::getenv("CACTUS_SUPABASE_KEY"); - const char* env_project = std::getenv("CACTUS_PROJECT_ID"); - const char* env_cloud = std::getenv("CACTUS_CLOUD_KEY"); - - std::string dir = get_telemetry_dir_locked(); - - std::string device_file = dir + "/device_id"; - std::string device_flag_file = dir + "/device_registered"; - bool device_was_registered = load_registered_flag(device_flag_file); - std::string resolved_device_id = load_or_create_id(device_file); - - std::string resolved_project_id; - if (project_id_param && *project_id_param) { - resolved_project_id = project_id_param; - } else if (env_project && *env_project) { - resolved_project_id = env_project; - } else { - std::string remote_url = read_git_remote_url(); - resolved_project_id = remote_url.empty() ? resolved_device_id : uuid_from_string(remote_url); - } - - std::string project_flag_file = dir + "/project_reg_" + resolved_project_id.substr(0, 8); - bool project_was_registered = load_registered_flag(project_flag_file); - - std::string resolved_cloud_key; - if (cloud_key_param && *cloud_key_param) { - resolved_cloud_key = cloud_key_param; - } else if (env_cloud && *env_cloud) { - resolved_cloud_key = env_cloud; - } - - collect_device_info(); - if (cactus_version.empty()) read_cactus_version(); - - if (env_url && *env_url) supabase_url = env_url; - if (env_key && *env_key) supabase_key = env_key; - project_id = resolved_project_id; - project_scope = scope; - project_registered_file = project_flag_file; - cloud_key = resolved_cloud_key; - device_registered_file = device_flag_file; - device_id = resolved_device_id; - if (app_id.empty()) app_id = detect_app_id(); - - project_registered = project_was_registered; - device_registered = device_was_registered; - if (cloud_disabled_from_env) { - cloud_disabled = true; - } - - if (!curl_initialized) { - if (curl_global_init(CURL_GLOBAL_DEFAULT) == CURLE_OK) { - curl_initialized = true; - } - } - - if (!atexit_registered) { - atexit_registered = true; - std::atexit([](){ shutdown(); }); - } - - shutdown_called = false; - ids_ready = true; - enabled = true; - lifecycle_state = TelemetryLifecycleState::Running; - TelemetryDispatcher::instance().start(); -} - -void setEnabled(bool en) { - std::lock_guard guard(telemetry_mutex); - enabled = en; -} - -void setCloudDisabled(bool disabled) { - std::lock_guard guard(telemetry_mutex); - cloud_disabled = disabled; -} - -void setAppId(const char* id) { - std::lock_guard guard(telemetry_mutex); - if (id && id[0] != '\0') app_id = id; -} - -void setTelemetryEnvironment(const char* framework_str, const char* cache_location_str, const char* version_str) { - std::lock_guard guard(telemetry_mutex); - if (framework_str && framework_str[0] != '\0') { - framework = framework_str; - } - if (cache_location_str && cache_location_str[0] != '\0') { - custom_cache_location = cache_location_str; - if (app_id.empty()) { - std::string pkg = extract_android_package_from_path(custom_cache_location); - if (!pkg.empty()) app_id = pkg; - } - } - if (version_str && version_str[0] != '\0') { - cactus_version = version_str; - } -} - -void setCloudKey(const char* key) { - if (!key || key[0] == '\0') return; - std::lock_guard guard(telemetry_mutex); - cloud_key = key; -} - -void cacheCloudApiKey(const char* key) { - if (!key || key[0] == '\0') return; - std::lock_guard guard(telemetry_mutex); - const std::string file = cloud_api_key_cache_file_locked(); - std::ofstream out(file, std::ios::trunc); - if (out.is_open()) { - out << key; - } -} - -std::string loadCachedCloudApiKey() { - std::lock_guard guard(telemetry_mutex); - const std::string file = cloud_api_key_cache_file_locked(); - std::ifstream in(file); - if (!in.is_open()) return {}; - std::string line; - if (std::getline(in, line) && !line.empty()) { - return line; - } - return {}; -} - -void recordInit(const char* model, bool success, double response_time_ms, const char* message) { - double nan = std::numeric_limits::quiet_NaN(); - Event e = make_event(INIT, model, success, nan, nan, response_time_ms, 0, message); - std::lock_guard guard(telemetry_mutex); - if (!can_record_event_locked()) return; - TelemetryDispatcher::instance().enqueue(e); -} - -void recordCompletion(const char* model, const CompletionMetrics& metrics) { - Event e = make_event_extended(COMPLETION, model, metrics); - std::lock_guard guard(telemetry_mutex); - if (!can_record_event_locked()) return; - TelemetryDispatcher::instance().enqueue(e); -} - -void recordCompletion(const char* model, bool success, double ttft_ms, double tps, double response_time_ms, int tokens, const char* message) { - Event e = make_event(COMPLETION, model, success, ttft_ms, tps, response_time_ms, tokens, message); - std::lock_guard guard(telemetry_mutex); - if (!can_record_event_locked()) return; - TelemetryDispatcher::instance().enqueue(e); -} - -void recordEmbedding(const char* model, bool success, const char* message) { - Event e = make_event(EMBEDDING, model, success, 0.0, 0.0, 0.0, 0, message); - std::lock_guard guard(telemetry_mutex); - if (!can_record_event_locked()) return; - TelemetryDispatcher::instance().enqueue(e); -} - -void recordTranscription(const char* model, bool success, double ttft_ms, double tps, double response_time_ms, int tokens, double ram_usage_mb, const char* message) { - std::lock_guard guard(telemetry_mutex); - if (in_stream_mode) return; - Event e = make_event(TRANSCRIPTION, model, success, ttft_ms, tps, response_time_ms, tokens, message); - e.ram_usage_mb = ram_usage_mb; - if (!can_record_event_locked()) return; - TelemetryDispatcher::instance().enqueue(e); -} - -void recordStreamTranscription(const char* model, bool success, double ttft_ms, double tps, double response_time_ms, int tokens, double session_ttft_ms, double session_tps, double session_time_ms, int session_tokens, const char* message) { - Event e = make_event(STREAM_TRANSCRIBE, model, success, ttft_ms, tps, response_time_ms, tokens, message); - e.session_ttft_ms = session_ttft_ms; - e.session_tps = session_tps; - e.session_time_ms = session_time_ms; - e.session_tokens = session_tokens; - std::lock_guard guard(telemetry_mutex); - if (!can_record_event_locked()) return; - TelemetryDispatcher::instance().enqueue(e); -} - -void setStreamMode(bool in_stream) { - std::lock_guard guard(telemetry_mutex); - in_stream_mode = in_stream; -} - -bool isStreamMode() { - std::lock_guard guard(telemetry_mutex); - return in_stream_mode; -} - -void markInference(bool active) { - std::lock_guard guard(telemetry_mutex); - if (active) { - inference_active += 1; - } else if (inference_active > 0) { - inference_active -= 1; - } -} - -void flush() { - TelemetryDispatcher::instance().flush(); -} - -void shutdown() { - { - std::lock_guard lifecycle_guard(telemetry_mutex); - if (shutdown_called) { - return; - } - - shutdown_called = true; - lifecycle_state = TelemetryLifecycleState::ShuttingDown; - } - - flush(); - - { - std::lock_guard lifecycle_guard(telemetry_mutex); - enabled = false; - ids_ready = false; - } - - TelemetryDispatcher::instance().stop(); - - { - std::lock_guard lifecycle_guard(telemetry_mutex); - if (curl_initialized) { - curl_initialized = false; - curl_global_cleanup(); - } - lifecycle_state = TelemetryLifecycleState::Stopped; - telemetry_lifecycle_cv.notify_all(); - } -} - -} // namespace telemetry -} // namespace cactus diff --git a/cactus/telemetry/telemetry.h b/cactus/telemetry/telemetry.h deleted file mode 100644 index cd7b49f45..000000000 --- a/cactus/telemetry/telemetry.h +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once -#include -#include - -namespace cactus { -namespace telemetry { - -struct CompletionMetrics { - bool success; - bool cloud_handoff; - double ttft_ms; - double prefill_tps; - double decode_tps; - double response_time_ms; - double confidence; - double ram_usage_mb; - size_t prefill_tokens; - size_t decode_tokens; - const char* error_message; - const char* function_calls_json; -}; - -void init(const char* project_id = nullptr, const char* project_scope = nullptr, const char* cloud_key = nullptr); -void setEnabled(bool enabled); -void setCloudDisabled(bool disabled); -void setAppId(const char* app_id); -void setTelemetryEnvironment(const char* framework, const char* cache_location, const char* version = nullptr); -void setCloudKey(const char* key); -void cacheCloudApiKey(const char* key); -std::string loadCachedCloudApiKey(); -void recordInit(const char* model, bool success, double response_time_ms, const char* message); -void recordCompletion(const char* model, const CompletionMetrics& metrics); -void recordCompletion(const char* model, bool success, double ttft_ms, double tps, double response_time_ms, int tokens, const char* message); -void recordEmbedding(const char* model, bool success, const char* message); -void recordTranscription(const char* model, bool success, double ttft_ms, double tps, double response_time_ms, int tokens, double ram_usage_mb, const char* message); -void recordStreamTranscription(const char* model, bool success, double ttft_ms, double tps, double response_time_ms, int tokens, double session_ttft_ms, double session_tps, double session_time_ms, int session_tokens, const char* message); -void setStreamMode(bool in_stream); -bool isStreamMode(); -void markInference(bool active); -void flush(); -void shutdown(); - -} // namespace telemetry -} // namespace cactus diff --git a/docs/cactus_engine.md b/docs/cactus_engine.md index b0053e06d..dfcf47236 100644 --- a/docs/cactus_engine.md +++ b/docs/cactus_engine.md @@ -22,11 +22,15 @@ cactus download openai/whisper-small cactus auth ``` -`cactus download` fetches pre-converted CQ weights from +`cactus download` fetches a **pre-built runtime bundle** (CQ weights + serialized +graph + manifest) from [huggingface.co/Cactus-Compute](https://huggingface.co/Cactus-Compute) into -`weights/` and they can be loaded directly via `cactus_init()`. For -models without a pre-converted archive, use `cactus convert ` to build -them from source. +`transpiled/-cq[-]/`. Defaults to the generic CPU +bundle; pass `--platform apple` for the Apple Silicon variant. The result +can be loaded directly via `cactus_init()`. + +For models not on Cactus-Compute, build a bundle from source with +`cactus convert ` followed by `cactus transpile `. ## Types @@ -171,7 +175,7 @@ int cactus_complete( | `include_stop_sequences` | bool | false | Include stop sequence tokens in the response | | `force_tools` | bool | false | Constrain output to tool call format | | `tool_rag_top_k` | int | 2 | Select top-k relevant tools via Tool RAG (0 = disabled, use all tools) | -| `confidence_threshold` | float | 0.7 | Minimum confidence for local generation; triggers cloud_handoff when below | +| `confidence_threshold` | float | model-dependent | Minimum confidence for local generation; triggers cloud_handoff when below. Resolved in this order: `0.5` if the bundle ships a `handoff_probe.bin`; else the model's `default_cloud_handoff_threshold` (Gemma 4 = `0.92`); else `0.7`. | | `auto_handoff` | bool | true | Automatically attempt cloud handoff when confidence is low | | `cloud_timeout_ms` | int | 15000 | Timeout in milliseconds for cloud handoff requests | | `handoff_with_images` | bool | true | Allow cloud handoff for requests that include images | @@ -466,6 +470,24 @@ if (result >= 0) { } ``` +### `cactus_benchmark_tokens` +Runs a prefill + decode benchmark on the given prompt tokens and returns timing JSON +(prefill ms, decode ms, tokens/sec). Useful for measuring inference performance on +a specific model + device without running the full chat loop. + +```c +int cactus_benchmark_tokens( + cactus_model_t model, + const uint32_t* prompt_tokens, + size_t prompt_token_len, + size_t decode_token_len, + char* response_buffer, + size_t buffer_size +); +``` + +Returns the number of bytes written to `response_buffer` on success, `-1` on error. + ### `cactus_transcribe` Transcribes audio to text. Supports Whisper, Moonshine, and Parakeet models. Supports both file-based and buffer-based audio input. @@ -574,7 +596,7 @@ int result = cactus_transcribe(whisper, NULL, NULL, | Option | Type | Default | Description | |--------|------|---------|-------------| -| `max_tokens` | int | 448 | Maximum tokens to generate | +| `max_tokens` | int | auto | Maximum tokens to generate. When unset, scales with audio length (`audio_sec × 20` for Whisper, `× 30` for Parakeet), then clamped to the Whisper decoder ceiling of 448. | | `custom_vocabulary` | array | [] | List of words or phrases to bias the decoder toward. Useful for proper nouns, acronyms, medical terms, and domain-specific jargon. | | `vocabulary_boost` | float | 5.0 | Logit bias strength applied to tokens from `custom_vocabulary`. Clamped to 0.0–20.0. Higher values make the listed words more likely to appear. | @@ -595,70 +617,6 @@ if (result > 0) { } ``` -### `cactus_detect_language` -Detects the spoken language in an audio file or PCM buffer. - -```c -int cactus_detect_language( - cactus_model_t model, // Model handle (must be Whisper model) - const char* audio_file_path, // Path to WAV file (16-bit PCM) - can be NULL if using pcm_buffer - char* response_buffer, // Buffer for response JSON - size_t buffer_size, // Size of response buffer - const char* options_json, // Optional options (can be NULL) - const uint8_t* pcm_buffer, // Optional raw PCM audio buffer (can be NULL if using file) - size_t pcm_buffer_size // Size of PCM buffer in bytes (must be even and >= 2) -); -``` - -**Returns:** Number of bytes written to response_buffer on success, negative value on error - -**Note:** Exactly one of `audio_file_path` or `pcm_buffer` must be provided; passing both or neither returns -1. The file path must point to a 16-bit PCM WAV file. The `pcm_buffer` must contain 16-bit signed PCM samples at 16 kHz and `pcm_buffer_size` must be even and at least 2. Only Whisper models are supported; passing any other model type returns -1. - -**Options Format:** -```json -{ - "use_vad": true -} -``` - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `use_vad` | bool | true | Filter audio through VAD before language detection (requires model initialized with a VAD component) | - -**Response Format:** -```json -{ - "success": true, - "error": null, - "language": "en", - "language_token": "<|en|>", - "token_id": 50259, - "confidence": 0.9812, - "entropy": 0.0188, - "total_time_ms": 234.56, - "ram_usage_mb": 512.34 -} -``` - -- `language`: ISO 639-1 language code, or `"unknown"` if detection failed -- `language_token`: Raw token text emitted by the model for the language (e.g. `"<|en|>"`) -- `token_id`: Vocabulary token ID of the language token -- `confidence`: Detection confidence (0.0–1.0), derived as `1.0 - entropy` -- `entropy`: Normalized entropy of the sampled token -- `total_time_ms`: Total detection time in milliseconds -- `ram_usage_mb`: Current process RAM usage - -**Example:** -```c -cactus_model_t whisper = cactus_init("../../weights/whisper-small", NULL, false); - -char response[1024]; -int result = cactus_detect_language(whisper, "audio.wav", response, sizeof(response), NULL, NULL, 0); -if (result >= 0) { - printf("Detected language: %s\n", response); -} -``` - ### `cactus_embed` Generates text embeddings for semantic search, similarity, and RAG applications. @@ -1229,20 +1187,6 @@ int find_similar_image(cactus_model_t model, const char* query, } ``` -## Environment Variables - -| Variable | Default | Description | -|----------|---------|-------------| -| `CACTUS_KV_WINDOW_SIZE` | 512 | Sliding window size for KV cache | -| `CACTUS_KV_SINK_SIZE` | 4 | Number of attention sink tokens to preserve | - -**Example:** -```bash -export CACTUS_KV_WINDOW_SIZE=1024 -export CACTUS_KV_SINK_SIZE=8 -./my_app -``` - ## Best Practices 1. **Always Check Return Values**: Functions return negative values on error @@ -1272,7 +1216,6 @@ Common error scenarios: 2. **Streaming for UX**: Use callbacks for responsive user interfaces 3. **Early Stopping**: Use `cactus_stop()` to avoid unnecessary generation 4. **Batch Embeddings**: When possible, process multiple texts in sequence without resetting -5. **KV Cache Tuning**: Adjust `CACTUS_KV_WINDOW_SIZE` based on your context needs ## Logging diff --git a/docs/cactus_graph.md b/docs/cactus_graph.md index a3c6fc50e..cbf0f339e 100644 --- a/docs/cactus_graph.md +++ b/docs/cactus_graph.md @@ -197,18 +197,22 @@ size_t glu_out = graph.glu(input, axis); // Gated Linear Unit ``` #### Convolution Operations + +Each conv has two overloads: with and without bias. + ```cpp -// 1D convolutions -size_t conv1d_out = graph.conv1d(input, weight, has_bias, bias, stride); -size_t conv1d_k3_out = graph.conv1d_k3(input, weight, stride); -size_t conv1d_causal = graph.conv1d_causal(input, weight, kernel_size, dilation); -size_t conv1d_pointwise = graph.conv1d_pointwise(input, weight, has_bias, bias); -size_t conv1d_depthwise = graph.conv1d_same_depthwise_k9(input, weight, has_bias, bias); +// 1D convolutions (no bias / with bias overloads) +size_t conv1d_out = graph.conv1d(input, weight, stride); +size_t conv1d_with_bias = graph.conv1d(input, weight, bias, stride); +size_t conv1d_k3_out = graph.conv1d_k3(input, weight, stride); +size_t conv1d_causal = graph.conv1d_causal(input, weight, kernel_size, dilation); +size_t conv1d_pointwise = graph.conv1d_pointwise(input, weight); // or (input, weight, bias) +size_t conv1d_depthwise = graph.conv1d_same_depthwise_k9(input, weight); // or (input, weight, bias) // 2D convolutions -size_t conv2d_out = graph.conv2d_k3s2p1(input, weight, has_bias, bias); -size_t conv2d_dw = graph.conv2d_depthwise_k3s2p1(input, weight, has_bias, bias); -size_t conv2d_pw = graph.conv2d_pointwise_1x1(input, weight, has_bias, bias); +size_t conv2d_out = graph.conv2d_k3s2p1(input, weight); // or (input, weight, bias) +size_t conv2d_dw = graph.conv2d_depthwise_k3s2p1(input, weight); // or (input, weight, bias) +size_t conv2d_pw = graph.conv2d_pointwise_1x1(input, weight); // or (input, weight, bias) ``` #### Normalization @@ -226,11 +230,13 @@ size_t deltanet_prefill = graph.gated_deltanet_prefill(query, key, value, gate_l #### Mixture of Experts (MoE) ```cpp -size_t moe_gated = graph.moe_layer_gated(hidden, routing_probs, topk_indices, +// Gated SwiGLU MoE: pass three weight sets (w1=gate, w3=up, w2=down) +size_t moe_gated = graph.moe_layer(hidden, routing_probs, topk_indices, w1_weights, w3_weights, w2_weights, num_experts, num_experts_per_tok, normalize_routing, epsilon, routed_scaling_factor); -size_t moe_ungated = graph.moe_layer_ungated(hidden, routing_probs, topk_indices, +// Ungated MoE: pass two weight sets (w1=up, w2=down) and the activation explicitly +size_t moe_ungated = graph.moe_layer(hidden, routing_probs, topk_indices, w1_weights, w2_weights, num_experts, num_experts_per_tok, normalize_routing, epsilon, routed_scaling_factor, activation); ``` @@ -302,7 +308,12 @@ size_t interpolated = graph.bilinear_interpolation(pos_embeds, dst_height, dst_w #### Sampling ```cpp +// Defaults: temperature=0.6, top_p=0.95, top_k=20 size_t sampled = graph.sample(logits, temperature, top_p, top_k); + +// With per-token bias map and repetition penalty / min_p: +size_t sampled_ext = graph.sample_with_options( + logits, temperature, top_p, min_p, repetition_penalty, top_k, /*logit_bias=*/{}); ``` ## Advanced Features @@ -328,7 +339,6 @@ size_t result = graph.add(a, b); // {2,3} -> {2,2,3} ```cpp size_t int8_tensor = graph.input({4}, Precision::INT8); size_t fp16_tensor = graph.precision_cast(int8_tensor, Precision::FP16); -graph.set_quantization_scale(node_id, scale); ``` ### Graph Persistence @@ -359,15 +369,16 @@ loaded.execute(); GraphFile::save_node(graph, node_id, "output.bin"); ``` -#### Loading Nodes +#### Loading Graphs ```cpp -CactusGraph new_graph; -auto loaded = GraphFile::load_into_graph(new_graph, "output.bin"); -size_t node_id = loaded.node_id; -std::vector shape = loaded.shape; -Precision precision = loaded.precision; +CactusGraph loaded = GraphFile::load_graph("graph.cactus"); ``` +The `GraphFile` namespace currently exposes `save_graph`, `load_graph`, and +`save_node`. There is no per-node loader; reload a saved subgraph by +serializing the parent graph with `save_graph` and reading it back with +`load_graph`. + ### Graph Management #### Execution @@ -504,8 +515,7 @@ Add a builder method in `cactus-graph/src/builder.cpp` and its declaration in Follow the pattern of existing builder methods, e.g. for a new "relu" op: ```cpp size_t CactusGraph::relu(size_t input) { - OpParams params; - return add_node(OpType::RELU, {input}, params); + return add_node(OpType::RELU, {input}, {}); } ``` 3. ** Implement the op in the execution engine ** @@ -513,14 +523,14 @@ Implement the kernel or graph op code in the relevant file, usually in `cactus-k Register the new op in the dispatch table in `cactus-graph/src/execute.cpp` for the supported backends (CPU, NPU) 4. ** Export op in FFI bindings ** -- header: `cactus-engine/cactus_engine.h` -- implementation: `cactus-engine/src/graph_ffi.cpp` +- header: `cactus-graph/cactus_graph.h` (in the `extern "C"` block) +- implementation: `cactus-graph/src/graph_ffi.cpp` 5. ** Add python ctypes declaration ** Add `_lib.cactus_graph_my_new_op.argtypes/restype` in `python/cactus/bindings/cactus.py` 6. ** Add python graph wrapper ** -Add `Graph.my_new_op(...)` in `python/cactus/bindings/graph.py`, and optionally a Tensor +Add `Graph.my_new_op(...)` in `python/cactus/bindings/cactus.py`, and optionally a Tensor convenience method. 7. ** Add serialization schema entry if needed ** @@ -544,7 +554,7 @@ The syntax pattern there is: }}, ``` 8. ** Add test coverage ** -Add unit tests to `tests/test_graph.cpp` covering the native graph function, and +Add unit tests to `cactus-graph/tests/test_graph.cpp` covering the native graph function, and add python tests in `python/tests/test_graph.py` covering the Python API and end-to-end execution. and then support those fields in `write_field(...)` / `read_field(...)`. @@ -569,10 +579,10 @@ try { ```cpp CactusGraph graph; size_t x = graph.input({batch, dim}, Precision::FP16); -x = graph.linear(x, weight1, bias1); +x = graph.add(graph.matmul(x, weight1), bias1); x = graph.gelu(x); x = graph.layernorm(x, ln_weight1, ln_bias1); -x = graph.linear(x, weight2, bias2); +x = graph.add(graph.matmul(x, weight2), bias2); ``` ### Residual Connections diff --git a/docs/cactus_hybrid.md b/docs/cactus_hybrid.md index e304ca65a..1bc7f12b9 100644 --- a/docs/cactus_hybrid.md +++ b/docs/cactus_hybrid.md @@ -179,7 +179,7 @@ options = json.dumps({ "auto_handoff": True }) messages = json.dumps([{"role": "user", "content": "Explain quantum entanglement"}]) -result = json.loads(cactus_complete(model, messages, options, None, None)) +result = cactus_complete(model, messages, options, None, None) if result["cloud_handoff"]: print("Answered by cloud") diff --git a/docs/cactus_index.md b/docs/cactus_index.md index 373a67e2e..a0e669117 100644 --- a/docs/cactus_index.md +++ b/docs/cactus_index.md @@ -11,7 +11,7 @@ The Cactus Index provides a clean C FFI (Foreign Function Interface) for integra ## Getting Started The index uses memory-mapped files: -- `index.bin`: Embeddings (FP16) and metadata pointers +- `index.bin`: Embeddings (FP16) plus per-doc offsets into `data.bin` - `data.bin`: Document content and metadata (UTF-8) All embeddings are automatically normalized to unit length diff --git a/docs/cactus_kernels.md b/docs/cactus_kernels.md index eb48b0e9f..de6602d32 100644 --- a/docs/cactus_kernels.md +++ b/docs/cactus_kernels.md @@ -31,7 +31,7 @@ void cactus_add_f16(const __fp16* a, const __fp16* b, __fp16* output, size_t n); void cactus_subtract_f16(const __fp16* a, const __fp16* b, __fp16* output, size_t n); void cactus_multiply_f16(const __fp16* a, const __fp16* b, __fp16* output, size_t n); void cactus_divide_f16(const __fp16* a, const __fp16* b, __fp16* output, size_t n); -void cactus_add_clipped_f16(const __fp16* a, const __fp16* b, __fp16* output, size_t n); +void cactus_add_f16_clipped(const __fp16* a, const __fp16* b, __fp16* output, size_t n); void cactus_add_scaled_f16(const __fp16* base, const __fp16* src, __fp16* output, size_t n, float scale); ``` @@ -156,7 +156,7 @@ void cactus_attention_f16( size_t batch_size, size_t seq_len, size_t kv_seq_len, size_t num_q_heads, size_t num_kv_heads, size_t head_dim, float scale, - const __fp16* mask = nullptr, + const __fp16* mask, size_t position_offset = 0, size_t window_size = 0, // 0 = full context, >0 = sliding window bool is_causal = true, @@ -431,30 +431,39 @@ void cactus_gaussian_topk_f16(const __fp16* input, __fp16* output, ``` cactus-kernels/ - cactus_kernels.h # public API (this file) - threading.h # thread pool utilities + cactus_kernels.h # public API (this file) libs/ - stb_image.h # vendored image loading - stb_image_resize2.h # vendored image resizing + stb_image.h # vendored image loading + stb_image_resize2.h # vendored image resizing src/ - arithmetic.cpp # element-wise ops, broadcast, reductions - matmul.cpp # FP16 GEMM - quant.cpp # CQ 1-4 bit GEMV/GEMM, dequantization - attention.cpp # attention kernels (FP16, hybrid INT8/FP16) - activations.cpp # relu, silu, gelu, sigmoid, tanh, glu, clamp - norm.cpp # rms_norm, layer_norm, batchnorm, softmax - conv.cpp # conv1d, conv2d variants - sample.cpp # top-k/top-p/min-p sampling - dsp.cpp # rfft, irfft, mel filter bank, spectrogram - image.cpp # image load/resize/normalize/patches - rope.cpp # rotary position embedding - transpose.cpp # 2D and N-D transpose - lstm.cpp # LSTM cell, BiLSTM sequence - deltanet.cpp # gated DeltaNet decode/prefill - wav.h # WAV file loading + 16kHz resampling + threading.h # thread pool utilities + wav.h # WAV file loading + 16 kHz resampling + attention.cpp # attention kernels (FP16) + attention_hybrid.cpp # hybrid INT8/FP16 attention + blas.cpp # BLAS-backed paths + conv.cpp # conv1d variants + conv2d.cpp # conv2d variants + dsp.cpp # rfft, irfft, mel filter bank, spectrogram, STFT + fused.cpp # fused op kernels + image.cpp # image load/resize/normalize/patches + lstm.cpp # LSTM cell, BiLSTM sequence + matmul.cpp # FP16/INT8 GEMM + nn.cpp # activations (relu/silu/gelu/...), glu, clamp + norms_rope.cpp # rms_norm, layer_norm, RoPE + quants.cpp # CQ 1-4 bit GEMV/GEMM, dequantization + reduce.cpp # reductions (sum/mean/var/min/max + axis variants) + scalar.cpp # scalar elementwise ops tests/ - test_kernels.cpp # 36 tests across 7 suites - test_utils.h # test runner, fp16 comparison helpers + test_utils.h # test runner, fp16 comparison helpers + test_attention.cpp + test_conv.cpp + test_dsp.cpp + test_elementwise.cpp + test_matmul.cpp + test_performance.cpp # perf benchmarks (run via `cactus test --suite performance`) + test_quant.cpp + test_reduce.cpp + android/ # on-device test runner for Android ``` ## See Also diff --git a/docs/cactus_quants.md b/docs/cactus_quants.md index cd6c868db..ac1079b5a 100644 --- a/docs/cactus_quants.md +++ b/docs/cactus_quants.md @@ -183,23 +183,30 @@ Three patterns are visible: ## Using CQ Weights -CQ weights are produced by `cactus convert` and consumed by the Cactus runtime: +CQ weights are produced by `cactus convert`. The runtime bundle (graph + manifest) +is then built from those weights with `cactus transpile`, and run via +`cactus run`: ```bash -# Convert at 4-bit (default) +# 1. Convert at 4-bit (default) cactus convert google/gemma-4-E2B-it ./gemma4-weights -# Convert at 2-bit +# 1'. Convert at 2-bit cactus convert google/gemma-4-E2B-it ./gemma4-weights --bits 2 -# Run -cactus run ./gemma4-weights +# 2. Build the runtime bundle from the CQ weights +cactus transpile google/gemma-4-E2B-it \ + --weights-dir ./gemma4-weights --artifact-dir ./gemma4-bundle + +# 3. Run (pass either a HF model id or a bundle path) +cactus run ./gemma4-bundle ``` -Pre-converted CQ weights for all supported models are available at -[huggingface.co/Cactus-Compute](https://huggingface.co/Cactus-Compute). +For models on huggingface.co/Cactus-Compute, you can skip the convert+transpile +steps entirely: `cactus run ` will fetch the pre-built bundle (or fall +back to local convert+transpile if no bundle is published), then run. -The CQ kernel implementation lives in `cactus-kernels/src/quant.cpp`. At inference, +The CQ matmul kernels live in `cactus-kernels/src/matmul.cpp`. At inference, the kernel applies the inverse Hadamard rotation to the FP16 activation (not the weight), then performs a fused codebook-lookup + norm-scale + matmul in a single NEON pass. diff --git a/docs/cactus_transpiler.md b/docs/cactus_transpiler.md index 90d18f0a3..5b0005c8b 100644 --- a/docs/cactus_transpiler.md +++ b/docs/cactus_transpiler.md @@ -55,7 +55,7 @@ cactus convert ./classifier.pt ./classifier-cactus --bits 4 cactus transpile ./classifier.pt --weights-dir ./classifier-cactus # Run the transpiled bundle -cactus run ./transpiled/classifier +cactus run ./classifier-cactus ``` That's it. The output is a self-contained bundle with memory-mapped weights that @@ -198,7 +198,7 @@ cactus convert google/gemma-4-E2B-it ./gemma4-weights --bits 4 cactus transpile google/gemma-4-E2B-it --weights-dir ./gemma4-weights # 3. Run -cactus run ./transpiled/google/gemma-4-E2B-it --prompt "Hello!" +cactus run ./gemma4-weights --prompt "Hello!" ``` ### Text Models @@ -240,26 +240,28 @@ cactus transpile nvidia/parakeet-tdt-0.6b-v3 \ ## Running Saved Bundles -Transpiled bundles are saved to `./transpiled//` by default. Run them later -without re-transpiling: +Transpiled bundles are saved to `./weights//` by default (alongside their CQ +weights). Run them later without re-transpiling: ```bash # Text -cactus run ./transpiled/Qwen_Qwen3-0.6B --prompt "Write a haiku" +cactus run ./weights/qwen3-0.6b --prompt "Write a haiku" # Multimodal -cactus run-transpiled ./transpiled/google/gemma-4-E2B-it \ - --image-file photo.jpg \ - --file speech.wav \ +cactus run ./weights/gemma-4-e2b-it \ + --image photo.jpg \ + --audio speech.wav \ --prompt "What do you see?" # Audio -cactus run-transpiled ./transpiled/nvidia/parakeet-tdt-0.6b-v3 \ - --file meeting.wav +cactus run ./weights/parakeet-tdt-0.6b-v3 \ + --audio meeting.wav ``` -`cactus run` auto-detects transpiled bundles by looking for a `manifest.json` in the -directory. +`cactus run` accepts either a HuggingFace model id (downloads the bundle) or a +local bundle directory path (detected by the presence of +`components/manifest.json`). The standalone `run-transpiled` subcommand was +removed — `cactus run` handles both cases. --- @@ -270,7 +272,7 @@ You can also drive the transpiler from Python: ```python from cactus.transpile.capture_pytorch import capture_model from cactus.transpile.lower import transpile_ir -from cactus.bindings.graph import Graph +from cactus.bindings.cactus import Graph import torch import numpy as np @@ -385,7 +387,7 @@ component. Use `loaded.reset()` between independent generations. A transpiled bundle looks like this: ``` -transpiled// +weights// raw_ir.json # IR before optimization (debugging) optimized_ir.json # IR after fusion passes (debugging) graph.cactus # serialized runtime graph @@ -420,34 +422,56 @@ cactus transpile [options] | Option | Description | |--------|-------------| -| `--weights-dir ` | Path to converted Cactus weights (required) | -| `--task ` | Force task type (`auto` by default) | -| `--prompt ` | Example text prompt for capture | -| `--image-file ` | Example image for multimodal capture | -| `--audio-file ` | Example audio for multimodal/speech capture | -| `--artifact-dir ` | Output directory (default: `./transpiled/`) | +| `--weights-dir ` | Path to converted CQ weights (default: `weights/`) | +| `--task ` | Force task type (default: `auto` — inferred from weights). Choices: `causal_lm_logits`, `multimodal_causal_lm_logits`, `ctc_logits`, `encoder_hidden_states`, `seq2seq_transcription`, `tdt_transcription` | +| `--prompt ` | Representative prompt for shape capture | +| `--image-file ` | Representative image (repeatable) | +| `--audio-file ` | Representative audio file | +| `--max-new-tokens ` | Generation room to preallocate for causal decode graphs | +| `--artifact-dir ` | Output directory (default: `weights/`) | | `--execute-after-transpile` | Run the graph after saving | | `--skip-reference-compare` | Skip PyTorch vs transpiled comparison | -| `--component-pipeline auto\|on\|off` | Control component splitting | +| `--component-pipeline auto\|on\|off` | Use split component graph transpilation when supported | +| `--components ` | Comma-separated component subset for component-pipeline models | +| `--trust-remote-code` | Allow HF remote code during the transpile phase | +| `--local-files-only` | Require HF files to already be local | +| `--allow-unconverted-weights` | Transpile against an unconverted source checkpoint | | `--no-fuse-rms-norm` | Disable RMSNorm fusion | | `--no-fuse-rope` | Disable RoPE fusion | | `--no-fuse-attention` | Disable attention fusion | -### `cactus run-transpiled` +NPU emission (CoreML `.mlpackage`s for Apple Silicon audio/vision encoders) is +implemented on the underlying `python -m cactus.transpile.hf_model` script +(`--npu`, `--npu-quantize`, `--npu-audio-quantize`, `--npu-vision-quantize`) +but is not yet exposed on the user-facing `cactus transpile` argparser. + +### `cactus run` ```bash -cactus run-transpiled [options] +cactus run [options] ``` +`model_id` may be a HuggingFace model id (downloads the matching bundle from +huggingface.co/Cactus-Compute) or a local path to a bundle directory. + | Option | Description | |--------|-------------| +| `--bits 1\|2\|3\|4` | CQ quantization bits when downloading (default: 4) | +| `--platform cpu\|apple` | Target accelerator (default: cpu — generic ARM) | +| `--token ` | HuggingFace token (gated models) | | `--prompt ` | Input prompt | | `--input-ids ` | Comma-separated token IDs | -| `--file ` | Audio file for speech bundles | -| `--image-file ` | Image file for multimodal bundles | -| `--weights-dir ` | Override weight directory | +| `--image ` | Image file for multimodal bundles | +| `--audio ` | Audio file for speech / audio bundles | | `--system ` | System prompt | -| `--thinking` | Enable chain-of-thought mode | +| `--thinking` | Enable thinking / reasoning mode | +| `--max-new-tokens ` | Max tokens to generate | +| `--result-json ` | Save bundle result payload to JSON | +| `--no-cloud-handoff` | Disable automatic cloud handoff | +| `--confidence-threshold ` | Confidence threshold below which to hand off to cloud | +| `--cloud-timeout-ms ` | Max wait time for cloud handoff | +| `--no-cloud-tele` | Disable cloud telemetry (write to cache only) | +| `--reconvert` | Force local convert+transpile fallback | --- diff --git a/docs/choose-bindings.md b/docs/choose-bindings.md index 8a31bb023..edb5f2526 100644 --- a/docs/choose-bindings.md +++ b/docs/choose-bindings.md @@ -6,8 +6,8 @@ Pick the right one for your platform and use case: | | React Native | Flutter | Kotlin | Swift | Python | Rust | CLI | C++ | |---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| -| **Platforms** | iOS, Android | iOS, Android, macOS | Android, iOS (KMP) | iOS, macOS | Arm Linux, macOS | All | macOS, Linux | All | -| **Install** | npm | build | build | build | build | cargo | brew / source | header | +| **Platforms** | iOS, Android | iOS, Android, macOS | Android, iOS (KMP) | iOS, macOS | Arm Linux, macOS | Arm Linux, macOS | macOS, Arm Linux | Arm Linux, macOS, iOS, Android | +| **Install** | build / source | build | build | build | pip | build / source | brew / source | header | | **LLM** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | **Streaming** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | **Vision** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | diff --git a/docs/finetuning.md b/docs/finetuning.md index 0ac0f3710..25ad62ee4 100644 --- a/docs/finetuning.md +++ b/docs/finetuning.md @@ -9,7 +9,7 @@ keywords: ["fine-tuning", "LoRA", "Unsloth", "mobile deployment", "on-device AI" - Cactus is an inference engine for mobile devices, macs and ARM chips like Raspberry Pi. - At INT8, Cactus runs `Qwen3-0.6B` and `LFM2-1.2B` at `60-70 toks/sec` on iPhone 17 Pro, `13-18 toks/sec` on budget Pixel 6a. - INT4 quantization provides ~50% memory reduction with minimal quality loss. -- Task-Specific INT8 tunes of `Gemma3-270m` hit `150 toks/sec` on iPhone 17 Pro and `23 toks/sec` on Raspberry Pi. +- Task-Specific INT8 tunes of `Gemma3-270m` hit `150 toks/sec` on iPhone 17 Pro and `23 toks/sec` on Raspberry Pi 5. ## Quick Start @@ -67,12 +67,17 @@ cactus convert Qwen/Qwen3-0.6B ./my-qwen3-0.6b --lora username/my-lora-adapter ### 4. Run -Build the native library, then test your model on Mac: +Build the native library, transpile the weights into a runtime bundle, then run: ```bash cactus build +cactus transpile Qwen/Qwen3-0.6B \ + --weights-dir ./my-qwen3-0.6b --artifact-dir ./my-qwen3-0.6b cactus run ./my-qwen3-0.6b ``` + +`cactus run` accepts the bundle path directly, or auto-builds from a HF model id +when no bundle exists locally. Logo ### 5. Use in iOS/macOS App @@ -86,35 +91,37 @@ cactus build --apple Build complete! Total time: 58 seconds Static libraries: - Device: /apple/libcactus-device.a - Simulator: /apple/libcactus-simulator.a + Device: /apple/libcactus_engine-device.a + Simulator: /apple/libcactus_engine-simulator.a XCFrameworks: iOS: /apple/cactus-ios.xcframework macOS: /apple/cactus-macos.xcframework -Apple build complete! +Building Cactus for Apple platforms complete! ``` Link `cactus-ios.xcframework` to your Xcode project, then: ```swift import Foundation -import cactus +import cactus // module map → cactus_engine.h -// Load model from app bundle let modelPath = Bundle.main.path(forResource: "my-model", ofType: nil)! -let model = try cactusInit(modelPath, nil, false) +let model = cactus_init(modelPath, nil, false) -// Run completion let messages = "[{\"role\":\"user\",\"content\":\"Hello!\"}]" -let resultJson = try cactusComplete(model, messages, nil, nil, nil) +var buf = [Int8](repeating: 0, count: 65536) +buf.withUnsafeMutableBufferPointer { ptr in + _ = cactus_complete(model, messages, ptr.baseAddress, ptr.count, + nil, nil, nil, nil, nil, 0) +} +let resultJson = String(cString: buf) if let data = resultJson.data(using: .utf8), let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let response = obj["response"] as? String { print(response) } -// Cleanup -cactusDestroy(model) +cactus_destroy(model) ``` Logo @@ -123,7 +130,7 @@ but to see performance on any device while testing, run cactus tests by plugging any iphone to your Mac then running: ```bash -cactus test --model --ios +cactus test --model --transcription-model --ios ``` Cactus demo apps will eventually expand to using your custom fine-tunes. @@ -140,12 +147,12 @@ cactus build --android ``` ```bash Build complete! -Shared library location: /android/libcactus.so -Static library location: /android/libcactus.a -Android build complete! +Shared library location: /android/libcactus_engine.so +Static library location: /android/libcactus_engine.a +Building Cactus for Android complete! ``` -Copy `libcactus.so` to `app/src/main/jniLibs/arm64-v8a/`, then: +Copy `libcactus_engine.so` to `app/src/main/jniLibs/arm64-v8a/`, then: ```kotlin import com.cactus.* @@ -153,8 +160,14 @@ import org.json.JSONObject val model = cactusInit("/data/local/tmp/my-model", null, false) -// Run completion -val resultJson = cactusComplete(model, """[{"role":"user","content":"Hello!"}]""", null, null, null) +val resultJson = cactusComplete( + model, + """[{"role":"user","content":"Hello!"}]""", + null, // optionsJson + null, // toolsJson + null, // callback + null, // pcmData +) val response = JSONObject(resultJson).getString("response") println(response) cactusDestroy(model) @@ -165,7 +178,7 @@ but to see performance on any device while testing, run cactus tests by plugging any android phone to your Mac then running: ```bash -cactus test --model --android +cactus test --model --transcription-model --android ``` Cactus demo apps will eventually expand to using your custom fine-tunes. diff --git a/docs/quickstart.md b/docs/quickstart.md index 7b6f52520..8d7bfa9ed 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -55,14 +55,15 @@ Install Cactus and run your first on-device AI completion. **From Source (macOS):** ```bash - git clone https://github.com/cactus-compute/cactus && cd cactus && source ./setup + brew install cmake + git clone https://github.com/cactus-compute/cactus && cd cactus && source ./setup && cactus build --python ``` **From Source (Linux):** ```bash sudo apt-get install python3 python3-venv python3-pip cmake build-essential libcurl4-openssl-dev - git clone https://github.com/cactus-compute/cactus && cd cactus && source ./setup + git clone https://github.com/cactus-compute/cactus && cd cactus && source ./setup && cactus build --python ``` === "C++" @@ -70,7 +71,7 @@ Install Cactus and run your first on-device AI completion. Include the Cactus header in your project: ```cpp - #include + #include ``` See the [Cactus repository](https://github.com/cactus-compute/cactus) for CMake build instructions. @@ -107,38 +108,40 @@ Install Cactus and run your first on-device AI completion. mod cactus; - unsafe { - let model_path = CString::new("path/to/weight/folder").unwrap(); - let model = cactus::cactus_init(model_path.as_ptr(), std::ptr::null(), false); - - let messages = CString::new( - r#"[{"role": "user", "content": "What is the capital of France?"}]"# - ).unwrap(); - - let mut response = vec![0u8; 4096]; - cactus::cactus_complete( - model, messages.as_ptr(), - response.as_mut_ptr() as *mut c_char, response.len(), - std::ptr::null(), std::ptr::null(), - None, std::ptr::null_mut(), - std::ptr::null(), 0, - ); - - println!("{}", String::from_utf8_lossy(&response)); - cactus::cactus_destroy(model); + fn main() { + unsafe { + let model_path = CString::new("path/to/weight/folder").unwrap(); + let model = cactus::cactus_init(model_path.as_ptr(), std::ptr::null(), false); + + let messages = CString::new( + r#"[{"role": "user", "content": "What is the capital of France?"}]"# + ).unwrap(); + + let mut response = vec![0u8; 4096]; + cactus::cactus_complete( + model, messages.as_ptr(), + response.as_mut_ptr() as *mut c_char, response.len(), + std::ptr::null(), std::ptr::null(), + None, std::ptr::null_mut(), + std::ptr::null(), 0, + ); + + println!("{}", String::from_utf8_lossy(&response)); + cactus::cactus_destroy(model); + } } ``` === "CLI" ```bash - cactus run + cactus run ``` === "C++" ```cpp - #include + #include cactus_model_t model = cactus_init( "path/to/weight/folder", diff --git a/libs/audio/wav.h b/libs/audio/wav.h deleted file mode 100644 index 576ef43c1..000000000 --- a/libs/audio/wav.h +++ /dev/null @@ -1,163 +0,0 @@ -#ifndef WAV_LOADER_H -#define WAV_LOADER_H - -#include -#include -#include -#include -#include -#include -#include - - -struct AudioFP32 { - int sample_rate; - std::vector samples; -}; - -inline AudioFP32 load_wav_fp32(const std::string& path) { - std::ifstream file(path, std::ios::binary); - if (!file) - throw std::runtime_error("Could not open WAV file: " + path); - - char riff[4]; - file.read(riff, 4); - if (std::string(riff, 4) != "RIFF") throw std::runtime_error("Not RIFF"); - - uint32_t chunk_size; - file.read(reinterpret_cast(&chunk_size), 4); - - char wave[4]; - file.read(wave, 4); - if (std::string(wave, 4) != "WAVE") throw std::runtime_error("Not WAVE"); - - char fmt_id[4]; - uint32_t fmt_size; - file.read(fmt_id, 4); - file.read(reinterpret_cast(&fmt_size), 4); - if (std::string(fmt_id, 4) != "fmt ") - throw std::runtime_error("Missing fmt chunk"); - - uint16_t audio_format, num_channels, bits_per_sample; - uint32_t sample_rate, byte_rate; - uint16_t block_align; - - file.read(reinterpret_cast(&audio_format), 2); - file.read(reinterpret_cast(&num_channels), 2); - file.read(reinterpret_cast(&sample_rate), 4); - file.read(reinterpret_cast(&byte_rate), 4); - file.read(reinterpret_cast(&block_align), 2); - file.read(reinterpret_cast(&bits_per_sample), 2); - - if (audio_format != 1 || bits_per_sample != 16) - throw std::runtime_error("Only 16-bit PCM WAV supported"); - - if (fmt_size > 16) - file.seekg(fmt_size - 16, std::ios::cur); - - char data_id[4]; - uint32_t data_size; - - while (true) { - file.read(data_id, 4); - file.read(reinterpret_cast(&data_size), 4); - if (!file) throw std::runtime_error("Malformed WAV: missing data chunk"); - - if (std::string(data_id, 4) == "data") - break; - - file.seekg(data_size, std::ios::cur); - } - - size_t num_samples = data_size / 2; - std::vector raw(num_samples); - file.read(reinterpret_cast(raw.data()), data_size); - - std::vector tmp(num_samples); - constexpr float scale = 1.0f / 32768.0f; - for (size_t i = 0; i < num_samples; i++) { - tmp[i] = float(raw[i]) * scale; - } - - std::vector mono; - - if (num_channels == 1) { - mono = std::move(tmp); - } else if (num_channels == 2) { - mono.reserve(num_samples / 2); - for (size_t i = 0; i < num_samples; i += 2) - mono.push_back(0.5f * (tmp[i] + tmp[i + 1])); - } else { - throw std::runtime_error("Unsupported channel count"); - } - - return AudioFP32{ (int)sample_rate, std::move(mono) }; -} - -inline AudioFP32 load_wav(const std::string& path) { - return load_wav_fp32(path); -} - -inline std::vector resample_to_16k_fp32( - const std::vector& in, int sr_in) -{ - const int sr_out = 16000; - if (sr_in == sr_out) return in; - - const double ratio = double(sr_out) / double(sr_in); - const size_t out_len = size_t(in.size() * ratio); - const size_t in_len = in.size(); - - if (sr_in <= sr_out) { - std::vector out(out_len); - for (size_t i = 0; i < out_len; i++) { - double pos = i / ratio; - size_t i0 = (size_t)pos; - double frac = pos - i0; - out[i] = (i0 + 1 < in_len) - ? float((1.0 - frac) * in[i0] + frac * in[i0 + 1]) - : in.back(); - } - return out; - } - - const double cutoff = ratio; - constexpr int SINC_HALF_WIDTH = 16; - const double M_PI_VAL = 3.14159265358979323846; - - std::vector out(out_len); - - for (size_t i = 0; i < out_len; i++) { - double center = i / ratio; - int left = (int)std::ceil(center) - SINC_HALF_WIDTH; - int right = (int)std::floor(center) + SINC_HALF_WIDTH; - - double sum = 0.0; - double weight_sum = 0.0; - - for (int j = left; j <= right; j++) { - if (j < 0 || j >= (int)in_len) continue; - - double x = center - j; - double sinc_val; - if (std::fabs(x) < 1e-9) { - sinc_val = cutoff; - } else { - double pix = M_PI_VAL * x * cutoff; - sinc_val = cutoff * std::sin(pix) / pix; - } - - double window_pos = (x + SINC_HALF_WIDTH) / (2.0 * SINC_HALF_WIDTH); - double window = 0.5 * (1.0 - std::cos(2.0 * M_PI_VAL * window_pos)); - - double w = sinc_val * window; - sum += in[j] * w; - weight_sum += w; - } - - out[i] = (weight_sum > 1e-9) ? float(sum / weight_sum) : 0.0f; - } - return out; -} - -#endif diff --git a/libs/curl/android/arm64-v8a/libcurl.a b/libs/curl/android/arm64-v8a/libcurl.a deleted file mode 100644 index 959af061a..000000000 Binary files a/libs/curl/android/arm64-v8a/libcurl.a and /dev/null differ diff --git a/libs/curl/include/Makefile.am b/libs/curl/include/Makefile.am deleted file mode 100644 index d65bfeaa0..000000000 --- a/libs/curl/include/Makefile.am +++ /dev/null @@ -1,28 +0,0 @@ -#*************************************************************************** -# _ _ ____ _ -# Project ___| | | | _ \| | -# / __| | | | |_) | | -# | (__| |_| | _ <| |___ -# \___|\___/|_| \_\_____| -# -# Copyright (C) Daniel Stenberg, , et al. -# -# This software is licensed as described in the file COPYING, which -# you should have received as part of this distribution. The terms -# are also available at https://curl.se/docs/copyright.html. -# -# You may opt to use, copy, modify, merge, publish, distribute and/or sell -# copies of the Software, and permit persons to whom the Software is -# furnished to do so, under the terms of the COPYING file. -# -# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY -# KIND, either express or implied. -# -# SPDX-License-Identifier: curl -# -########################################################################### -SUBDIRS = curl - -EXTRA_DIST = README.md - -AUTOMAKE_OPTIONS = foreign no-dependencies diff --git a/libs/curl/include/Makefile.in b/libs/curl/include/Makefile.in deleted file mode 100644 index e7e6e7582..000000000 --- a/libs/curl/include/Makefile.in +++ /dev/null @@ -1,713 +0,0 @@ -# Makefile.in generated by automake 1.16.5 from Makefile.am. -# @configure_input@ - -# Copyright (C) 1994-2021 Free Software Foundation, Inc. - -# This Makefile.in is free software; the Free Software Foundation -# gives unlimited permission to copy and/or distribute it, -# with or without modifications, as long as this notice is preserved. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY, to the extent permitted by law; without -# even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. - -@SET_MAKE@ -VPATH = @srcdir@ -am__is_gnu_make = { \ - if test -z '$(MAKELEVEL)'; then \ - false; \ - elif test -n '$(MAKE_HOST)'; then \ - true; \ - elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \ - true; \ - else \ - false; \ - fi; \ -} -am__make_running_with_option = \ - case $${target_option-} in \ - ?) ;; \ - *) echo "am__make_running_with_option: internal error: invalid" \ - "target option '$${target_option-}' specified" >&2; \ - exit 1;; \ - esac; \ - has_opt=no; \ - sane_makeflags=$$MAKEFLAGS; \ - if $(am__is_gnu_make); then \ - sane_makeflags=$$MFLAGS; \ - else \ - case $$MAKEFLAGS in \ - *\\[\ \ ]*) \ - bs=\\; \ - sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \ - | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \ - esac; \ - fi; \ - skip_next=no; \ - strip_trailopt () \ - { \ - flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \ - }; \ - for flg in $$sane_makeflags; do \ - test $$skip_next = yes && { skip_next=no; continue; }; \ - case $$flg in \ - *=*|--*) continue;; \ - -*I) strip_trailopt 'I'; skip_next=yes;; \ - -*I?*) strip_trailopt 'I';; \ - -*O) strip_trailopt 'O'; skip_next=yes;; \ - -*O?*) strip_trailopt 'O';; \ - -*l) strip_trailopt 'l'; skip_next=yes;; \ - -*l?*) strip_trailopt 'l';; \ - -[dEDm]) skip_next=yes;; \ - -[JT]) skip_next=yes;; \ - esac; \ - case $$flg in \ - *$$target_option*) has_opt=yes; break;; \ - esac; \ - done; \ - test $$has_opt = yes -am__make_dryrun = (target_option=n; $(am__make_running_with_option)) -am__make_keepgoing = (target_option=k; $(am__make_running_with_option)) -pkgdatadir = $(datadir)/@PACKAGE@ -pkgincludedir = $(includedir)/@PACKAGE@ -pkglibdir = $(libdir)/@PACKAGE@ -pkglibexecdir = $(libexecdir)/@PACKAGE@ -am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd -install_sh_DATA = $(install_sh) -c -m 644 -install_sh_PROGRAM = $(install_sh) -c -install_sh_SCRIPT = $(install_sh) -c -INSTALL_HEADER = $(INSTALL_DATA) -transform = $(program_transform_name) -NORMAL_INSTALL = : -PRE_INSTALL = : -POST_INSTALL = : -NORMAL_UNINSTALL = : -PRE_UNINSTALL = : -POST_UNINSTALL = : -build_triplet = @build@ -host_triplet = @host@ -subdir = include -ACLOCAL_M4 = $(top_srcdir)/aclocal.m4 -am__aclocal_m4_deps = $(top_srcdir)/m4/curl-amissl.m4 \ - $(top_srcdir)/m4/curl-apple-sectrust.m4 \ - $(top_srcdir)/m4/curl-compilers.m4 \ - $(top_srcdir)/m4/curl-confopts.m4 \ - $(top_srcdir)/m4/curl-functions.m4 \ - $(top_srcdir)/m4/curl-gnutls.m4 \ - $(top_srcdir)/m4/curl-mbedtls.m4 \ - $(top_srcdir)/m4/curl-openssl.m4 \ - $(top_srcdir)/m4/curl-override.m4 \ - $(top_srcdir)/m4/curl-reentrant.m4 \ - $(top_srcdir)/m4/curl-rustls.m4 \ - $(top_srcdir)/m4/curl-schannel.m4 \ - $(top_srcdir)/m4/curl-sysconfig.m4 \ - $(top_srcdir)/m4/curl-wolfssl.m4 $(top_srcdir)/m4/libtool.m4 \ - $(top_srcdir)/m4/ltoptions.m4 $(top_srcdir)/m4/ltsugar.m4 \ - $(top_srcdir)/m4/ltversion.m4 $(top_srcdir)/m4/lt~obsolete.m4 \ - $(top_srcdir)/m4/xc-am-iface.m4 \ - $(top_srcdir)/m4/xc-cc-check.m4 \ - $(top_srcdir)/m4/xc-lt-iface.m4 \ - $(top_srcdir)/m4/xc-val-flgs.m4 \ - $(top_srcdir)/m4/zz40-xc-ovr.m4 \ - $(top_srcdir)/m4/zz50-xc-ovr.m4 $(top_srcdir)/acinclude.m4 \ - $(top_srcdir)/configure.ac -am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ - $(ACLOCAL_M4) -DIST_COMMON = $(srcdir)/Makefile.am $(am__DIST_COMMON) -mkinstalldirs = $(install_sh) -d -CONFIG_HEADER = $(top_builddir)/lib/curl_config.h -CONFIG_CLEAN_FILES = -CONFIG_CLEAN_VPATH_FILES = -AM_V_P = $(am__v_P_@AM_V@) -am__v_P_ = $(am__v_P_@AM_DEFAULT_V@) -am__v_P_0 = false -am__v_P_1 = : -AM_V_GEN = $(am__v_GEN_@AM_V@) -am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@) -am__v_GEN_0 = @echo " GEN " $@; -am__v_GEN_1 = -AM_V_at = $(am__v_at_@AM_V@) -am__v_at_ = $(am__v_at_@AM_DEFAULT_V@) -am__v_at_0 = @ -am__v_at_1 = -depcomp = -am__maybe_remake_depfiles = -SOURCES = -DIST_SOURCES = -RECURSIVE_TARGETS = all-recursive check-recursive cscopelist-recursive \ - ctags-recursive dvi-recursive html-recursive info-recursive \ - install-data-recursive install-dvi-recursive \ - install-exec-recursive install-html-recursive \ - install-info-recursive install-pdf-recursive \ - install-ps-recursive install-recursive installcheck-recursive \ - installdirs-recursive pdf-recursive ps-recursive \ - tags-recursive uninstall-recursive -am__can_run_installinfo = \ - case $$AM_UPDATE_INFO_DIR in \ - n|no|NO) false;; \ - *) (install-info --version) >/dev/null 2>&1;; \ - esac -RECURSIVE_CLEAN_TARGETS = mostlyclean-recursive clean-recursive \ - distclean-recursive maintainer-clean-recursive -am__recursive_targets = \ - $(RECURSIVE_TARGETS) \ - $(RECURSIVE_CLEAN_TARGETS) \ - $(am__extra_recursive_targets) -AM_RECURSIVE_TARGETS = $(am__recursive_targets:-recursive=) TAGS CTAGS \ - distdir distdir-am -am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP) -# Read a list of newline-separated strings from the standard input, -# and print each of them once, without duplicates. Input order is -# *not* preserved. -am__uniquify_input = $(AWK) '\ - BEGIN { nonempty = 0; } \ - { items[$$0] = 1; nonempty = 1; } \ - END { if (nonempty) { for (i in items) print i; }; } \ -' -# Make sure the list of sources is unique. This is necessary because, -# e.g., the same source file might be shared among _SOURCES variables -# for different programs/libraries. -am__define_uniq_tagged_files = \ - list='$(am__tagged_files)'; \ - unique=`for i in $$list; do \ - if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \ - done | $(am__uniquify_input)` -DIST_SUBDIRS = $(SUBDIRS) -am__DIST_COMMON = $(srcdir)/Makefile.in README.md -DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) -am__relativize = \ - dir0=`pwd`; \ - sed_first='s,^\([^/]*\)/.*$$,\1,'; \ - sed_rest='s,^[^/]*/*,,'; \ - sed_last='s,^.*/\([^/]*\)$$,\1,'; \ - sed_butlast='s,/*[^/]*$$,,'; \ - while test -n "$$dir1"; do \ - first=`echo "$$dir1" | sed -e "$$sed_first"`; \ - if test "$$first" != "."; then \ - if test "$$first" = ".."; then \ - dir2=`echo "$$dir0" | sed -e "$$sed_last"`/"$$dir2"; \ - dir0=`echo "$$dir0" | sed -e "$$sed_butlast"`; \ - else \ - first2=`echo "$$dir2" | sed -e "$$sed_first"`; \ - if test "$$first2" = "$$first"; then \ - dir2=`echo "$$dir2" | sed -e "$$sed_rest"`; \ - else \ - dir2="../$$dir2"; \ - fi; \ - dir0="$$dir0"/"$$first"; \ - fi; \ - fi; \ - dir1=`echo "$$dir1" | sed -e "$$sed_rest"`; \ - done; \ - reldir="$$dir2" -ACLOCAL = @ACLOCAL@ -AMTAR = @AMTAR@ -AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@ -APXS = @APXS@ -AR = @AR@ -AR_FLAGS = @AR_FLAGS@ -AS = @AS@ -AUTOCONF = @AUTOCONF@ -AUTOHEADER = @AUTOHEADER@ -AUTOMAKE = @AUTOMAKE@ -AWK = @AWK@ -BLANK_AT_MAKETIME = @BLANK_AT_MAKETIME@ -CADDY = @CADDY@ -CC = @CC@ -CCDEPMODE = @CCDEPMODE@ -CFLAGS = @CFLAGS@ -CFLAG_CURL_SYMBOL_HIDING = @CFLAG_CURL_SYMBOL_HIDING@ -CONFIGURE_OPTIONS = @CONFIGURE_OPTIONS@ -CPP = @CPP@ -CPPFLAGS = @CPPFLAGS@ -CSCOPE = @CSCOPE@ -CTAGS = @CTAGS@ -CURLVERSION = @CURLVERSION@ -CURL_CA_BUNDLE = @CURL_CA_BUNDLE@ -CURL_CA_EMBED = @CURL_CA_EMBED@ -CURL_CFLAG_EXTRAS = @CURL_CFLAG_EXTRAS@ -CURL_CPP = @CURL_CPP@ -CURL_LIBCURL_VERSIONED_SYMBOLS_PREFIX = @CURL_LIBCURL_VERSIONED_SYMBOLS_PREFIX@ -CURL_LIBCURL_VERSIONED_SYMBOLS_SONAME = @CURL_LIBCURL_VERSIONED_SYMBOLS_SONAME@ -CURL_NETWORK_AND_TIME_LIBS = @CURL_NETWORK_AND_TIME_LIBS@ -CYGPATH_W = @CYGPATH_W@ -DANTED = @DANTED@ -DEFS = @DEFS@ -DEPDIR = @DEPDIR@ -DLLTOOL = @DLLTOOL@ -DSYMUTIL = @DSYMUTIL@ -DUMPBIN = @DUMPBIN@ -ECHO_C = @ECHO_C@ -ECHO_N = @ECHO_N@ -ECHO_T = @ECHO_T@ -EGREP = @EGREP@ -ENABLE_SHARED = @ENABLE_SHARED@ -ENABLE_STATIC = @ENABLE_STATIC@ -ETAGS = @ETAGS@ -EXEEXT = @EXEEXT@ -FGREP = @FGREP@ -FILECMD = @FILECMD@ -FISH_FUNCTIONS_DIR = @FISH_FUNCTIONS_DIR@ -GCOV = @GCOV@ -GREP = @GREP@ -HAVE_LIBZ = @HAVE_LIBZ@ -HTTPD = @HTTPD@ -HTTPD_NGHTTPX = @HTTPD_NGHTTPX@ -INSTALL = @INSTALL@ -INSTALL_DATA = @INSTALL_DATA@ -INSTALL_PROGRAM = @INSTALL_PROGRAM@ -INSTALL_SCRIPT = @INSTALL_SCRIPT@ -INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@ -LCOV = @LCOV@ -LD = @LD@ -LDFLAGS = @LDFLAGS@ -LIBCURL_PC_CFLAGS = @LIBCURL_PC_CFLAGS@ -LIBCURL_PC_CFLAGS_PRIVATE = @LIBCURL_PC_CFLAGS_PRIVATE@ -LIBCURL_PC_LDFLAGS_PRIVATE = @LIBCURL_PC_LDFLAGS_PRIVATE@ -LIBCURL_PC_LIBS = @LIBCURL_PC_LIBS@ -LIBCURL_PC_LIBS_PRIVATE = @LIBCURL_PC_LIBS_PRIVATE@ -LIBCURL_PC_REQUIRES = @LIBCURL_PC_REQUIRES@ -LIBCURL_PC_REQUIRES_PRIVATE = @LIBCURL_PC_REQUIRES_PRIVATE@ -LIBOBJS = @LIBOBJS@ -LIBS = @LIBS@ -LIBTOOL = @LIBTOOL@ -LIPO = @LIPO@ -LN_S = @LN_S@ -LTLIBOBJS = @LTLIBOBJS@ -LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@ -MAINT = @MAINT@ -MAKEINFO = @MAKEINFO@ -MANIFEST_TOOL = @MANIFEST_TOOL@ -MKDIR_P = @MKDIR_P@ -NM = @NM@ -NMEDIT = @NMEDIT@ -OBJDUMP = @OBJDUMP@ -OBJEXT = @OBJEXT@ -OTOOL = @OTOOL@ -OTOOL64 = @OTOOL64@ -PACKAGE = @PACKAGE@ -PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@ -PACKAGE_NAME = @PACKAGE_NAME@ -PACKAGE_STRING = @PACKAGE_STRING@ -PACKAGE_TARNAME = @PACKAGE_TARNAME@ -PACKAGE_URL = @PACKAGE_URL@ -PACKAGE_VERSION = @PACKAGE_VERSION@ -PATH_SEPARATOR = @PATH_SEPARATOR@ -PERL = @PERL@ -PKGCONFIG = @PKGCONFIG@ -RANLIB = @RANLIB@ -RC = @RC@ -SED = @SED@ -SET_MAKE = @SET_MAKE@ -SFTPD = @SFTPD@ -SHELL = @SHELL@ -SSHD = @SSHD@ -SSL_BACKENDS = @SSL_BACKENDS@ -STRIP = @STRIP@ -SUPPORT_FEATURES = @SUPPORT_FEATURES@ -SUPPORT_PROTOCOLS = @SUPPORT_PROTOCOLS@ -TEST_NGHTTPX = @TEST_NGHTTPX@ -VERSION = @VERSION@ -VERSIONNUM = @VERSIONNUM@ -VSFTPD = @VSFTPD@ -ZLIB_LIBS = @ZLIB_LIBS@ -ZSH_FUNCTIONS_DIR = @ZSH_FUNCTIONS_DIR@ -abs_builddir = @abs_builddir@ -abs_srcdir = @abs_srcdir@ -abs_top_builddir = @abs_top_builddir@ -abs_top_srcdir = @abs_top_srcdir@ -ac_ct_AR = @ac_ct_AR@ -ac_ct_CC = @ac_ct_CC@ -ac_ct_DUMPBIN = @ac_ct_DUMPBIN@ -am__include = @am__include@ -am__leading_dot = @am__leading_dot@ -am__quote = @am__quote@ -am__tar = @am__tar@ -am__untar = @am__untar@ -bindir = @bindir@ -build = @build@ -build_alias = @build_alias@ -build_cpu = @build_cpu@ -build_os = @build_os@ -build_vendor = @build_vendor@ -builddir = @builddir@ -datadir = @datadir@ -datarootdir = @datarootdir@ -docdir = @docdir@ -dvidir = @dvidir@ -exec_prefix = @exec_prefix@ -host = @host@ -host_alias = @host_alias@ -host_cpu = @host_cpu@ -host_os = @host_os@ -host_vendor = @host_vendor@ -htmldir = @htmldir@ -includedir = @includedir@ -infodir = @infodir@ -install_sh = @install_sh@ -libdir = @libdir@ -libexecdir = @libexecdir@ -libext = @libext@ -localedir = @localedir@ -localstatedir = @localstatedir@ -mandir = @mandir@ -mkdir_p = @mkdir_p@ -oldincludedir = @oldincludedir@ -pdfdir = @pdfdir@ -prefix = @prefix@ -program_transform_name = @program_transform_name@ -psdir = @psdir@ -runstatedir = @runstatedir@ -sbindir = @sbindir@ -sharedstatedir = @sharedstatedir@ -srcdir = @srcdir@ -sysconfdir = @sysconfdir@ -target_alias = @target_alias@ -top_build_prefix = @top_build_prefix@ -top_builddir = @top_builddir@ -top_srcdir = @top_srcdir@ - -#*************************************************************************** -# _ _ ____ _ -# Project ___| | | | _ \| | -# / __| | | | |_) | | -# | (__| |_| | _ <| |___ -# \___|\___/|_| \_\_____| -# -# Copyright (C) Daniel Stenberg, , et al. -# -# This software is licensed as described in the file COPYING, which -# you should have received as part of this distribution. The terms -# are also available at https://curl.se/docs/copyright.html. -# -# You may opt to use, copy, modify, merge, publish, distribute and/or sell -# copies of the Software, and permit persons to whom the Software is -# furnished to do so, under the terms of the COPYING file. -# -# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY -# KIND, either express or implied. -# -# SPDX-License-Identifier: curl -# -########################################################################### -SUBDIRS = curl -EXTRA_DIST = README.md -AUTOMAKE_OPTIONS = foreign no-dependencies -all: all-recursive - -.SUFFIXES: -$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps) - @for dep in $?; do \ - case '$(am__configure_deps)' in \ - *$$dep*) \ - ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \ - && { if test -f $@; then exit 0; else break; fi; }; \ - exit 1;; \ - esac; \ - done; \ - echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign include/Makefile'; \ - $(am__cd) $(top_srcdir) && \ - $(AUTOMAKE) --foreign include/Makefile -Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status - @case '$?' in \ - *config.status*) \ - cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \ - *) \ - echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \ - cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \ - esac; - -$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES) - cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh - -$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps) - cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh -$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps) - cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh -$(am__aclocal_m4_deps): - -mostlyclean-libtool: - -rm -f *.lo - -clean-libtool: - -rm -rf .libs _libs - -# This directory's subdirectories are mostly independent; you can cd -# into them and run 'make' without going through this Makefile. -# To change the values of 'make' variables: instead of editing Makefiles, -# (1) if the variable is set in 'config.status', edit 'config.status' -# (which will cause the Makefiles to be regenerated when you run 'make'); -# (2) otherwise, pass the desired values on the 'make' command line. -$(am__recursive_targets): - @fail=; \ - if $(am__make_keepgoing); then \ - failcom='fail=yes'; \ - else \ - failcom='exit 1'; \ - fi; \ - dot_seen=no; \ - target=`echo $@ | sed s/-recursive//`; \ - case "$@" in \ - distclean-* | maintainer-clean-*) list='$(DIST_SUBDIRS)' ;; \ - *) list='$(SUBDIRS)' ;; \ - esac; \ - for subdir in $$list; do \ - echo "Making $$target in $$subdir"; \ - if test "$$subdir" = "."; then \ - dot_seen=yes; \ - local_target="$$target-am"; \ - else \ - local_target="$$target"; \ - fi; \ - ($(am__cd) $$subdir && $(MAKE) $(AM_MAKEFLAGS) $$local_target) \ - || eval $$failcom; \ - done; \ - if test "$$dot_seen" = "no"; then \ - $(MAKE) $(AM_MAKEFLAGS) "$$target-am" || exit 1; \ - fi; test -z "$$fail" - -ID: $(am__tagged_files) - $(am__define_uniq_tagged_files); mkid -fID $$unique -tags: tags-recursive -TAGS: tags - -tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files) - set x; \ - here=`pwd`; \ - if ($(ETAGS) --etags-include --version) >/dev/null 2>&1; then \ - include_option=--etags-include; \ - empty_fix=.; \ - else \ - include_option=--include; \ - empty_fix=; \ - fi; \ - list='$(SUBDIRS)'; for subdir in $$list; do \ - if test "$$subdir" = .; then :; else \ - test ! -f $$subdir/TAGS || \ - set "$$@" "$$include_option=$$here/$$subdir/TAGS"; \ - fi; \ - done; \ - $(am__define_uniq_tagged_files); \ - shift; \ - if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \ - test -n "$$unique" || unique=$$empty_fix; \ - if test $$# -gt 0; then \ - $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ - "$$@" $$unique; \ - else \ - $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ - $$unique; \ - fi; \ - fi -ctags: ctags-recursive - -CTAGS: ctags -ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files) - $(am__define_uniq_tagged_files); \ - test -z "$(CTAGS_ARGS)$$unique" \ - || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \ - $$unique - -GTAGS: - here=`$(am__cd) $(top_builddir) && pwd` \ - && $(am__cd) $(top_srcdir) \ - && gtags -i $(GTAGS_ARGS) "$$here" -cscopelist: cscopelist-recursive - -cscopelist-am: $(am__tagged_files) - list='$(am__tagged_files)'; \ - case "$(srcdir)" in \ - [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \ - *) sdir=$(subdir)/$(srcdir) ;; \ - esac; \ - for i in $$list; do \ - if test -f "$$i"; then \ - echo "$(subdir)/$$i"; \ - else \ - echo "$$sdir/$$i"; \ - fi; \ - done >> $(top_builddir)/cscope.files - -distclean-tags: - -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags -distdir: $(BUILT_SOURCES) - $(MAKE) $(AM_MAKEFLAGS) distdir-am - -distdir-am: $(DISTFILES) - @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ - topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ - list='$(DISTFILES)'; \ - dist_files=`for file in $$list; do echo $$file; done | \ - sed -e "s|^$$srcdirstrip/||;t" \ - -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \ - case $$dist_files in \ - */*) $(MKDIR_P) `echo "$$dist_files" | \ - sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \ - sort -u` ;; \ - esac; \ - for file in $$dist_files; do \ - if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \ - if test -d $$d/$$file; then \ - dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \ - if test -d "$(distdir)/$$file"; then \ - find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ - fi; \ - if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \ - cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \ - find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ - fi; \ - cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \ - else \ - test -f "$(distdir)/$$file" \ - || cp -p $$d/$$file "$(distdir)/$$file" \ - || exit 1; \ - fi; \ - done - @list='$(DIST_SUBDIRS)'; for subdir in $$list; do \ - if test "$$subdir" = .; then :; else \ - $(am__make_dryrun) \ - || test -d "$(distdir)/$$subdir" \ - || $(MKDIR_P) "$(distdir)/$$subdir" \ - || exit 1; \ - dir1=$$subdir; dir2="$(distdir)/$$subdir"; \ - $(am__relativize); \ - new_distdir=$$reldir; \ - dir1=$$subdir; dir2="$(top_distdir)"; \ - $(am__relativize); \ - new_top_distdir=$$reldir; \ - echo " (cd $$subdir && $(MAKE) $(AM_MAKEFLAGS) top_distdir="$$new_top_distdir" distdir="$$new_distdir" \\"; \ - echo " am__remove_distdir=: am__skip_length_check=: am__skip_mode_fix=: distdir)"; \ - ($(am__cd) $$subdir && \ - $(MAKE) $(AM_MAKEFLAGS) \ - top_distdir="$$new_top_distdir" \ - distdir="$$new_distdir" \ - am__remove_distdir=: \ - am__skip_length_check=: \ - am__skip_mode_fix=: \ - distdir) \ - || exit 1; \ - fi; \ - done -check-am: all-am -check: check-recursive -all-am: Makefile -installdirs: installdirs-recursive -installdirs-am: -install: install-recursive -install-exec: install-exec-recursive -install-data: install-data-recursive -uninstall: uninstall-recursive - -install-am: all-am - @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am - -installcheck: installcheck-recursive -install-strip: - if test -z '$(STRIP)'; then \ - $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ - install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ - install; \ - else \ - $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ - install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ - "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \ - fi -mostlyclean-generic: - -clean-generic: - -distclean-generic: - -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES) - -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES) - -maintainer-clean-generic: - @echo "This command is intended for maintainers to use" - @echo "it deletes files that may require special tools to rebuild." -clean: clean-recursive - -clean-am: clean-generic clean-libtool mostlyclean-am - -distclean: distclean-recursive - -rm -f Makefile -distclean-am: clean-am distclean-generic distclean-tags - -dvi: dvi-recursive - -dvi-am: - -html: html-recursive - -html-am: - -info: info-recursive - -info-am: - -install-data-am: - -install-dvi: install-dvi-recursive - -install-dvi-am: - -install-exec-am: - -install-html: install-html-recursive - -install-html-am: - -install-info: install-info-recursive - -install-info-am: - -install-man: - -install-pdf: install-pdf-recursive - -install-pdf-am: - -install-ps: install-ps-recursive - -install-ps-am: - -installcheck-am: - -maintainer-clean: maintainer-clean-recursive - -rm -f Makefile -maintainer-clean-am: distclean-am maintainer-clean-generic - -mostlyclean: mostlyclean-recursive - -mostlyclean-am: mostlyclean-generic mostlyclean-libtool - -pdf: pdf-recursive - -pdf-am: - -ps: ps-recursive - -ps-am: - -uninstall-am: - -.MAKE: $(am__recursive_targets) install-am install-strip - -.PHONY: $(am__recursive_targets) CTAGS GTAGS TAGS all all-am check \ - check-am clean clean-generic clean-libtool cscopelist-am ctags \ - ctags-am distclean distclean-generic distclean-libtool \ - distclean-tags distdir dvi dvi-am html html-am info info-am \ - install install-am install-data install-data-am install-dvi \ - install-dvi-am install-exec install-exec-am install-html \ - install-html-am install-info install-info-am install-man \ - install-pdf install-pdf-am install-ps install-ps-am \ - install-strip installcheck installcheck-am installdirs \ - installdirs-am maintainer-clean maintainer-clean-generic \ - mostlyclean mostlyclean-generic mostlyclean-libtool pdf pdf-am \ - ps ps-am tags tags-am uninstall uninstall-am - -.PRECIOUS: Makefile - - -# Tell versions [3.59,3.63) of GNU make to not export all variables. -# Otherwise a system limit (for SysV at least) may be exceeded. -.NOEXPORT: diff --git a/libs/curl/include/curl/Makefile.am b/libs/curl/include/curl/Makefile.am deleted file mode 100644 index f664f6ab7..000000000 --- a/libs/curl/include/curl/Makefile.am +++ /dev/null @@ -1,43 +0,0 @@ -#*************************************************************************** -# _ _ ____ _ -# Project ___| | | | _ \| | -# / __| | | | |_) | | -# | (__| |_| | _ <| |___ -# \___|\___/|_| \_\_____| -# -# Copyright (C) Daniel Stenberg, , et al. -# -# This software is licensed as described in the file COPYING, which -# you should have received as part of this distribution. The terms -# are also available at https://curl.se/docs/copyright.html. -# -# You may opt to use, copy, modify, merge, publish, distribute and/or sell -# copies of the Software, and permit persons to whom the Software is -# furnished to do so, under the terms of the COPYING file. -# -# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY -# KIND, either express or implied. -# -# SPDX-License-Identifier: curl -# -########################################################################### -pkginclude_HEADERS = \ - curl.h curlver.h easy.h mprintf.h stdcheaders.h multi.h \ - typecheck-gcc.h system.h urlapi.h options.h header.h websockets.h - -pkgincludedir= $(includedir)/curl - -CHECKSRC = $(CS_$(V)) -CS_0 = @echo " RUN " $@; -CS_1 = -CS_ = $(CS_0) - -checksrc: - $(CHECKSRC)@PERL@ $(top_srcdir)/scripts/checksrc.pl -D$(top_srcdir)/include/curl $(pkginclude_HEADERS) - -if NOT_CURL_CI -if DEBUGBUILD -# for debug builds, we scan the sources on all regular make invokes -all-local: checksrc -endif -endif diff --git a/libs/curl/include/curl/Makefile.in b/libs/curl/include/curl/Makefile.in deleted file mode 100644 index ed4f28818..000000000 --- a/libs/curl/include/curl/Makefile.in +++ /dev/null @@ -1,665 +0,0 @@ -# Makefile.in generated by automake 1.16.5 from Makefile.am. -# @configure_input@ - -# Copyright (C) 1994-2021 Free Software Foundation, Inc. - -# This Makefile.in is free software; the Free Software Foundation -# gives unlimited permission to copy and/or distribute it, -# with or without modifications, as long as this notice is preserved. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY, to the extent permitted by law; without -# even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. - -@SET_MAKE@ - -VPATH = @srcdir@ -am__is_gnu_make = { \ - if test -z '$(MAKELEVEL)'; then \ - false; \ - elif test -n '$(MAKE_HOST)'; then \ - true; \ - elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \ - true; \ - else \ - false; \ - fi; \ -} -am__make_running_with_option = \ - case $${target_option-} in \ - ?) ;; \ - *) echo "am__make_running_with_option: internal error: invalid" \ - "target option '$${target_option-}' specified" >&2; \ - exit 1;; \ - esac; \ - has_opt=no; \ - sane_makeflags=$$MAKEFLAGS; \ - if $(am__is_gnu_make); then \ - sane_makeflags=$$MFLAGS; \ - else \ - case $$MAKEFLAGS in \ - *\\[\ \ ]*) \ - bs=\\; \ - sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \ - | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \ - esac; \ - fi; \ - skip_next=no; \ - strip_trailopt () \ - { \ - flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \ - }; \ - for flg in $$sane_makeflags; do \ - test $$skip_next = yes && { skip_next=no; continue; }; \ - case $$flg in \ - *=*|--*) continue;; \ - -*I) strip_trailopt 'I'; skip_next=yes;; \ - -*I?*) strip_trailopt 'I';; \ - -*O) strip_trailopt 'O'; skip_next=yes;; \ - -*O?*) strip_trailopt 'O';; \ - -*l) strip_trailopt 'l'; skip_next=yes;; \ - -*l?*) strip_trailopt 'l';; \ - -[dEDm]) skip_next=yes;; \ - -[JT]) skip_next=yes;; \ - esac; \ - case $$flg in \ - *$$target_option*) has_opt=yes; break;; \ - esac; \ - done; \ - test $$has_opt = yes -am__make_dryrun = (target_option=n; $(am__make_running_with_option)) -am__make_keepgoing = (target_option=k; $(am__make_running_with_option)) -pkgdatadir = $(datadir)/@PACKAGE@ -pkglibdir = $(libdir)/@PACKAGE@ -pkglibexecdir = $(libexecdir)/@PACKAGE@ -am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd -install_sh_DATA = $(install_sh) -c -m 644 -install_sh_PROGRAM = $(install_sh) -c -install_sh_SCRIPT = $(install_sh) -c -INSTALL_HEADER = $(INSTALL_DATA) -transform = $(program_transform_name) -NORMAL_INSTALL = : -PRE_INSTALL = : -POST_INSTALL = : -NORMAL_UNINSTALL = : -PRE_UNINSTALL = : -POST_UNINSTALL = : -build_triplet = @build@ -host_triplet = @host@ -subdir = include/curl -ACLOCAL_M4 = $(top_srcdir)/aclocal.m4 -am__aclocal_m4_deps = $(top_srcdir)/m4/curl-amissl.m4 \ - $(top_srcdir)/m4/curl-apple-sectrust.m4 \ - $(top_srcdir)/m4/curl-compilers.m4 \ - $(top_srcdir)/m4/curl-confopts.m4 \ - $(top_srcdir)/m4/curl-functions.m4 \ - $(top_srcdir)/m4/curl-gnutls.m4 \ - $(top_srcdir)/m4/curl-mbedtls.m4 \ - $(top_srcdir)/m4/curl-openssl.m4 \ - $(top_srcdir)/m4/curl-override.m4 \ - $(top_srcdir)/m4/curl-reentrant.m4 \ - $(top_srcdir)/m4/curl-rustls.m4 \ - $(top_srcdir)/m4/curl-schannel.m4 \ - $(top_srcdir)/m4/curl-sysconfig.m4 \ - $(top_srcdir)/m4/curl-wolfssl.m4 $(top_srcdir)/m4/libtool.m4 \ - $(top_srcdir)/m4/ltoptions.m4 $(top_srcdir)/m4/ltsugar.m4 \ - $(top_srcdir)/m4/ltversion.m4 $(top_srcdir)/m4/lt~obsolete.m4 \ - $(top_srcdir)/m4/xc-am-iface.m4 \ - $(top_srcdir)/m4/xc-cc-check.m4 \ - $(top_srcdir)/m4/xc-lt-iface.m4 \ - $(top_srcdir)/m4/xc-val-flgs.m4 \ - $(top_srcdir)/m4/zz40-xc-ovr.m4 \ - $(top_srcdir)/m4/zz50-xc-ovr.m4 $(top_srcdir)/acinclude.m4 \ - $(top_srcdir)/configure.ac -am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ - $(ACLOCAL_M4) -DIST_COMMON = $(srcdir)/Makefile.am $(pkginclude_HEADERS) \ - $(am__DIST_COMMON) -mkinstalldirs = $(install_sh) -d -CONFIG_HEADER = $(top_builddir)/lib/curl_config.h -CONFIG_CLEAN_FILES = -CONFIG_CLEAN_VPATH_FILES = -AM_V_P = $(am__v_P_@AM_V@) -am__v_P_ = $(am__v_P_@AM_DEFAULT_V@) -am__v_P_0 = false -am__v_P_1 = : -AM_V_GEN = $(am__v_GEN_@AM_V@) -am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@) -am__v_GEN_0 = @echo " GEN " $@; -am__v_GEN_1 = -AM_V_at = $(am__v_at_@AM_V@) -am__v_at_ = $(am__v_at_@AM_DEFAULT_V@) -am__v_at_0 = @ -am__v_at_1 = -SOURCES = -DIST_SOURCES = -am__can_run_installinfo = \ - case $$AM_UPDATE_INFO_DIR in \ - n|no|NO) false;; \ - *) (install-info --version) >/dev/null 2>&1;; \ - esac -am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`; -am__vpath_adj = case $$p in \ - $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \ - *) f=$$p;; \ - esac; -am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`; -am__install_max = 40 -am__nobase_strip_setup = \ - srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'` -am__nobase_strip = \ - for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||" -am__nobase_list = $(am__nobase_strip_setup); \ - for p in $$list; do echo "$$p $$p"; done | \ - sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \ - $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \ - if (++n[$$2] == $(am__install_max)) \ - { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \ - END { for (dir in files) print dir, files[dir] }' -am__base_list = \ - sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \ - sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g' -am__uninstall_files_from_dir = { \ - test -z "$$files" \ - || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \ - || { echo " ( cd '$$dir' && rm -f" $$files ")"; \ - $(am__cd) "$$dir" && rm -f $$files; }; \ - } -am__installdirs = "$(DESTDIR)$(pkgincludedir)" -HEADERS = $(pkginclude_HEADERS) -am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP) -# Read a list of newline-separated strings from the standard input, -# and print each of them once, without duplicates. Input order is -# *not* preserved. -am__uniquify_input = $(AWK) '\ - BEGIN { nonempty = 0; } \ - { items[$$0] = 1; nonempty = 1; } \ - END { if (nonempty) { for (i in items) print i; }; } \ -' -# Make sure the list of sources is unique. This is necessary because, -# e.g., the same source file might be shared among _SOURCES variables -# for different programs/libraries. -am__define_uniq_tagged_files = \ - list='$(am__tagged_files)'; \ - unique=`for i in $$list; do \ - if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \ - done | $(am__uniquify_input)` -am__DIST_COMMON = $(srcdir)/Makefile.in -DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) -pkgincludedir = $(includedir)/curl -ACLOCAL = @ACLOCAL@ -AMTAR = @AMTAR@ -AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@ -APXS = @APXS@ -AR = @AR@ -AR_FLAGS = @AR_FLAGS@ -AS = @AS@ -AUTOCONF = @AUTOCONF@ -AUTOHEADER = @AUTOHEADER@ -AUTOMAKE = @AUTOMAKE@ -AWK = @AWK@ -BLANK_AT_MAKETIME = @BLANK_AT_MAKETIME@ -CADDY = @CADDY@ -CC = @CC@ -CCDEPMODE = @CCDEPMODE@ -CFLAGS = @CFLAGS@ -CFLAG_CURL_SYMBOL_HIDING = @CFLAG_CURL_SYMBOL_HIDING@ -CONFIGURE_OPTIONS = @CONFIGURE_OPTIONS@ -CPP = @CPP@ -CPPFLAGS = @CPPFLAGS@ -CSCOPE = @CSCOPE@ -CTAGS = @CTAGS@ -CURLVERSION = @CURLVERSION@ -CURL_CA_BUNDLE = @CURL_CA_BUNDLE@ -CURL_CA_EMBED = @CURL_CA_EMBED@ -CURL_CFLAG_EXTRAS = @CURL_CFLAG_EXTRAS@ -CURL_CPP = @CURL_CPP@ -CURL_LIBCURL_VERSIONED_SYMBOLS_PREFIX = @CURL_LIBCURL_VERSIONED_SYMBOLS_PREFIX@ -CURL_LIBCURL_VERSIONED_SYMBOLS_SONAME = @CURL_LIBCURL_VERSIONED_SYMBOLS_SONAME@ -CURL_NETWORK_AND_TIME_LIBS = @CURL_NETWORK_AND_TIME_LIBS@ -CYGPATH_W = @CYGPATH_W@ -DANTED = @DANTED@ -DEFS = @DEFS@ -DEPDIR = @DEPDIR@ -DLLTOOL = @DLLTOOL@ -DSYMUTIL = @DSYMUTIL@ -DUMPBIN = @DUMPBIN@ -ECHO_C = @ECHO_C@ -ECHO_N = @ECHO_N@ -ECHO_T = @ECHO_T@ -EGREP = @EGREP@ -ENABLE_SHARED = @ENABLE_SHARED@ -ENABLE_STATIC = @ENABLE_STATIC@ -ETAGS = @ETAGS@ -EXEEXT = @EXEEXT@ -FGREP = @FGREP@ -FILECMD = @FILECMD@ -FISH_FUNCTIONS_DIR = @FISH_FUNCTIONS_DIR@ -GCOV = @GCOV@ -GREP = @GREP@ -HAVE_LIBZ = @HAVE_LIBZ@ -HTTPD = @HTTPD@ -HTTPD_NGHTTPX = @HTTPD_NGHTTPX@ -INSTALL = @INSTALL@ -INSTALL_DATA = @INSTALL_DATA@ -INSTALL_PROGRAM = @INSTALL_PROGRAM@ -INSTALL_SCRIPT = @INSTALL_SCRIPT@ -INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@ -LCOV = @LCOV@ -LD = @LD@ -LDFLAGS = @LDFLAGS@ -LIBCURL_PC_CFLAGS = @LIBCURL_PC_CFLAGS@ -LIBCURL_PC_CFLAGS_PRIVATE = @LIBCURL_PC_CFLAGS_PRIVATE@ -LIBCURL_PC_LDFLAGS_PRIVATE = @LIBCURL_PC_LDFLAGS_PRIVATE@ -LIBCURL_PC_LIBS = @LIBCURL_PC_LIBS@ -LIBCURL_PC_LIBS_PRIVATE = @LIBCURL_PC_LIBS_PRIVATE@ -LIBCURL_PC_REQUIRES = @LIBCURL_PC_REQUIRES@ -LIBCURL_PC_REQUIRES_PRIVATE = @LIBCURL_PC_REQUIRES_PRIVATE@ -LIBOBJS = @LIBOBJS@ -LIBS = @LIBS@ -LIBTOOL = @LIBTOOL@ -LIPO = @LIPO@ -LN_S = @LN_S@ -LTLIBOBJS = @LTLIBOBJS@ -LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@ -MAINT = @MAINT@ -MAKEINFO = @MAKEINFO@ -MANIFEST_TOOL = @MANIFEST_TOOL@ -MKDIR_P = @MKDIR_P@ -NM = @NM@ -NMEDIT = @NMEDIT@ -OBJDUMP = @OBJDUMP@ -OBJEXT = @OBJEXT@ -OTOOL = @OTOOL@ -OTOOL64 = @OTOOL64@ -PACKAGE = @PACKAGE@ -PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@ -PACKAGE_NAME = @PACKAGE_NAME@ -PACKAGE_STRING = @PACKAGE_STRING@ -PACKAGE_TARNAME = @PACKAGE_TARNAME@ -PACKAGE_URL = @PACKAGE_URL@ -PACKAGE_VERSION = @PACKAGE_VERSION@ -PATH_SEPARATOR = @PATH_SEPARATOR@ -PERL = @PERL@ -PKGCONFIG = @PKGCONFIG@ -RANLIB = @RANLIB@ -RC = @RC@ -SED = @SED@ -SET_MAKE = @SET_MAKE@ -SFTPD = @SFTPD@ -SHELL = @SHELL@ -SSHD = @SSHD@ -SSL_BACKENDS = @SSL_BACKENDS@ -STRIP = @STRIP@ -SUPPORT_FEATURES = @SUPPORT_FEATURES@ -SUPPORT_PROTOCOLS = @SUPPORT_PROTOCOLS@ -TEST_NGHTTPX = @TEST_NGHTTPX@ -VERSION = @VERSION@ -VERSIONNUM = @VERSIONNUM@ -VSFTPD = @VSFTPD@ -ZLIB_LIBS = @ZLIB_LIBS@ -ZSH_FUNCTIONS_DIR = @ZSH_FUNCTIONS_DIR@ -abs_builddir = @abs_builddir@ -abs_srcdir = @abs_srcdir@ -abs_top_builddir = @abs_top_builddir@ -abs_top_srcdir = @abs_top_srcdir@ -ac_ct_AR = @ac_ct_AR@ -ac_ct_CC = @ac_ct_CC@ -ac_ct_DUMPBIN = @ac_ct_DUMPBIN@ -am__include = @am__include@ -am__leading_dot = @am__leading_dot@ -am__quote = @am__quote@ -am__tar = @am__tar@ -am__untar = @am__untar@ -bindir = @bindir@ -build = @build@ -build_alias = @build_alias@ -build_cpu = @build_cpu@ -build_os = @build_os@ -build_vendor = @build_vendor@ -builddir = @builddir@ -datadir = @datadir@ -datarootdir = @datarootdir@ -docdir = @docdir@ -dvidir = @dvidir@ -exec_prefix = @exec_prefix@ -host = @host@ -host_alias = @host_alias@ -host_cpu = @host_cpu@ -host_os = @host_os@ -host_vendor = @host_vendor@ -htmldir = @htmldir@ -includedir = @includedir@ -infodir = @infodir@ -install_sh = @install_sh@ -libdir = @libdir@ -libexecdir = @libexecdir@ -libext = @libext@ -localedir = @localedir@ -localstatedir = @localstatedir@ -mandir = @mandir@ -mkdir_p = @mkdir_p@ -oldincludedir = @oldincludedir@ -pdfdir = @pdfdir@ -prefix = @prefix@ -program_transform_name = @program_transform_name@ -psdir = @psdir@ -runstatedir = @runstatedir@ -sbindir = @sbindir@ -sharedstatedir = @sharedstatedir@ -srcdir = @srcdir@ -sysconfdir = @sysconfdir@ -target_alias = @target_alias@ -top_build_prefix = @top_build_prefix@ -top_builddir = @top_builddir@ -top_srcdir = @top_srcdir@ - -#*************************************************************************** -# _ _ ____ _ -# Project ___| | | | _ \| | -# / __| | | | |_) | | -# | (__| |_| | _ <| |___ -# \___|\___/|_| \_\_____| -# -# Copyright (C) Daniel Stenberg, , et al. -# -# This software is licensed as described in the file COPYING, which -# you should have received as part of this distribution. The terms -# are also available at https://curl.se/docs/copyright.html. -# -# You may opt to use, copy, modify, merge, publish, distribute and/or sell -# copies of the Software, and permit persons to whom the Software is -# furnished to do so, under the terms of the COPYING file. -# -# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY -# KIND, either express or implied. -# -# SPDX-License-Identifier: curl -# -########################################################################### -pkginclude_HEADERS = \ - curl.h curlver.h easy.h mprintf.h stdcheaders.h multi.h \ - typecheck-gcc.h system.h urlapi.h options.h header.h websockets.h - -CHECKSRC = $(CS_$(V)) -CS_0 = @echo " RUN " $@; -CS_1 = -CS_ = $(CS_0) -all: all-am - -.SUFFIXES: -$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps) - @for dep in $?; do \ - case '$(am__configure_deps)' in \ - *$$dep*) \ - ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \ - && { if test -f $@; then exit 0; else break; fi; }; \ - exit 1;; \ - esac; \ - done; \ - echo ' cd $(top_srcdir) && $(AUTOMAKE) --gnu include/curl/Makefile'; \ - $(am__cd) $(top_srcdir) && \ - $(AUTOMAKE) --gnu include/curl/Makefile -Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status - @case '$?' in \ - *config.status*) \ - cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \ - *) \ - echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \ - cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \ - esac; - -$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES) - cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh - -$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps) - cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh -$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps) - cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh -$(am__aclocal_m4_deps): - -mostlyclean-libtool: - -rm -f *.lo - -clean-libtool: - -rm -rf .libs _libs -install-pkgincludeHEADERS: $(pkginclude_HEADERS) - @$(NORMAL_INSTALL) - @list='$(pkginclude_HEADERS)'; test -n "$(pkgincludedir)" || list=; \ - if test -n "$$list"; then \ - echo " $(MKDIR_P) '$(DESTDIR)$(pkgincludedir)'"; \ - $(MKDIR_P) "$(DESTDIR)$(pkgincludedir)" || exit 1; \ - fi; \ - for p in $$list; do \ - if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \ - echo "$$d$$p"; \ - done | $(am__base_list) | \ - while read files; do \ - echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkgincludedir)'"; \ - $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkgincludedir)" || exit $$?; \ - done - -uninstall-pkgincludeHEADERS: - @$(NORMAL_UNINSTALL) - @list='$(pkginclude_HEADERS)'; test -n "$(pkgincludedir)" || list=; \ - files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \ - dir='$(DESTDIR)$(pkgincludedir)'; $(am__uninstall_files_from_dir) - -ID: $(am__tagged_files) - $(am__define_uniq_tagged_files); mkid -fID $$unique -tags: tags-am -TAGS: tags - -tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files) - set x; \ - here=`pwd`; \ - $(am__define_uniq_tagged_files); \ - shift; \ - if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \ - test -n "$$unique" || unique=$$empty_fix; \ - if test $$# -gt 0; then \ - $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ - "$$@" $$unique; \ - else \ - $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ - $$unique; \ - fi; \ - fi -ctags: ctags-am - -CTAGS: ctags -ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files) - $(am__define_uniq_tagged_files); \ - test -z "$(CTAGS_ARGS)$$unique" \ - || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \ - $$unique - -GTAGS: - here=`$(am__cd) $(top_builddir) && pwd` \ - && $(am__cd) $(top_srcdir) \ - && gtags -i $(GTAGS_ARGS) "$$here" -cscopelist: cscopelist-am - -cscopelist-am: $(am__tagged_files) - list='$(am__tagged_files)'; \ - case "$(srcdir)" in \ - [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \ - *) sdir=$(subdir)/$(srcdir) ;; \ - esac; \ - for i in $$list; do \ - if test -f "$$i"; then \ - echo "$(subdir)/$$i"; \ - else \ - echo "$$sdir/$$i"; \ - fi; \ - done >> $(top_builddir)/cscope.files - -distclean-tags: - -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags -distdir: $(BUILT_SOURCES) - $(MAKE) $(AM_MAKEFLAGS) distdir-am - -distdir-am: $(DISTFILES) - @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ - topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ - list='$(DISTFILES)'; \ - dist_files=`for file in $$list; do echo $$file; done | \ - sed -e "s|^$$srcdirstrip/||;t" \ - -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \ - case $$dist_files in \ - */*) $(MKDIR_P) `echo "$$dist_files" | \ - sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \ - sort -u` ;; \ - esac; \ - for file in $$dist_files; do \ - if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \ - if test -d $$d/$$file; then \ - dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \ - if test -d "$(distdir)/$$file"; then \ - find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ - fi; \ - if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \ - cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \ - find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ - fi; \ - cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \ - else \ - test -f "$(distdir)/$$file" \ - || cp -p $$d/$$file "$(distdir)/$$file" \ - || exit 1; \ - fi; \ - done -check-am: all-am -check: check-am -@DEBUGBUILD_FALSE@all-local: -@NOT_CURL_CI_FALSE@all-local: -all-am: Makefile $(HEADERS) all-local -installdirs: - for dir in "$(DESTDIR)$(pkgincludedir)"; do \ - test -z "$$dir" || $(MKDIR_P) "$$dir"; \ - done -install: install-am -install-exec: install-exec-am -install-data: install-data-am -uninstall: uninstall-am - -install-am: all-am - @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am - -installcheck: installcheck-am -install-strip: - if test -z '$(STRIP)'; then \ - $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ - install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ - install; \ - else \ - $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ - install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ - "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \ - fi -mostlyclean-generic: - -clean-generic: - -distclean-generic: - -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES) - -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES) - -maintainer-clean-generic: - @echo "This command is intended for maintainers to use" - @echo "it deletes files that may require special tools to rebuild." -clean: clean-am - -clean-am: clean-generic clean-libtool mostlyclean-am - -distclean: distclean-am - -rm -f Makefile -distclean-am: clean-am distclean-generic distclean-tags - -dvi: dvi-am - -dvi-am: - -html: html-am - -html-am: - -info: info-am - -info-am: - -install-data-am: install-pkgincludeHEADERS - -install-dvi: install-dvi-am - -install-dvi-am: - -install-exec-am: - -install-html: install-html-am - -install-html-am: - -install-info: install-info-am - -install-info-am: - -install-man: - -install-pdf: install-pdf-am - -install-pdf-am: - -install-ps: install-ps-am - -install-ps-am: - -installcheck-am: - -maintainer-clean: maintainer-clean-am - -rm -f Makefile -maintainer-clean-am: distclean-am maintainer-clean-generic - -mostlyclean: mostlyclean-am - -mostlyclean-am: mostlyclean-generic mostlyclean-libtool - -pdf: pdf-am - -pdf-am: - -ps: ps-am - -ps-am: - -uninstall-am: uninstall-pkgincludeHEADERS - -.MAKE: install-am install-strip - -.PHONY: CTAGS GTAGS TAGS all all-am all-local check check-am clean \ - clean-generic clean-libtool cscopelist-am ctags ctags-am \ - distclean distclean-generic distclean-libtool distclean-tags \ - distdir dvi dvi-am html html-am info info-am install \ - install-am install-data install-data-am install-dvi \ - install-dvi-am install-exec install-exec-am install-html \ - install-html-am install-info install-info-am install-man \ - install-pdf install-pdf-am install-pkgincludeHEADERS \ - install-ps install-ps-am install-strip installcheck \ - installcheck-am installdirs maintainer-clean \ - maintainer-clean-generic mostlyclean mostlyclean-generic \ - mostlyclean-libtool pdf pdf-am ps ps-am tags tags-am uninstall \ - uninstall-am uninstall-pkgincludeHEADERS - -.PRECIOUS: Makefile - - -checksrc: - $(CHECKSRC)@PERL@ $(top_srcdir)/scripts/checksrc.pl -D$(top_srcdir)/include/curl $(pkginclude_HEADERS) - -# for debug builds, we scan the sources on all regular make invokes -@DEBUGBUILD_TRUE@@NOT_CURL_CI_TRUE@all-local: checksrc - -# Tell versions [3.59,3.63) of GNU make to not export all variables. -# Otherwise a system limit (for SysV at least) may be exceeded. -.NOEXPORT: diff --git a/libs/curl/include/curl/curl.h b/libs/curl/include/curl/curl.h deleted file mode 100644 index e755f098f..000000000 --- a/libs/curl/include/curl/curl.h +++ /dev/null @@ -1,3334 +0,0 @@ -#ifndef CURLINC_CURL_H -#define CURLINC_CURL_H -/*************************************************************************** - * _ _ ____ _ - * Project ___| | | | _ \| | - * / __| | | | |_) | | - * | (__| |_| | _ <| |___ - * \___|\___/|_| \_\_____| - * - * Copyright (C) Daniel Stenberg, , et al. - * - * This software is licensed as described in the file COPYING, which - * you should have received as part of this distribution. The terms - * are also available at https://curl.se/docs/copyright.html. - * - * You may opt to use, copy, modify, merge, publish, distribute and/or sell - * copies of the Software, and permit persons to whom the Software is - * furnished to do so, under the terms of the COPYING file. - * - * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY - * KIND, either express or implied. - * - * SPDX-License-Identifier: curl - * - ***************************************************************************/ - -/* - * If you have libcurl problems, all docs and details are found here: - * https://curl.se/libcurl/ - */ - -#ifdef CURL_NO_OLDIES -#define CURL_STRICTER /* not used since 8.11.0 */ -#endif - -/* Compile-time deprecation macros. */ -#if (defined(__GNUC__) && \ - ((__GNUC__ > 12) || ((__GNUC__ == 12) && (__GNUC_MINOR__ >= 1))) || \ - (defined(__clang__) && __clang_major__ >= 3) || \ - defined(__IAR_SYSTEMS_ICC__)) && \ - !defined(__INTEL_COMPILER) && \ - !defined(CURL_DISABLE_DEPRECATION) && !defined(BUILDING_LIBCURL) -#define CURL_DEPRECATED(version, message) \ - __attribute__((deprecated("since " # version ". " message))) -#ifdef __IAR_SYSTEMS_ICC__ -#define CURL_IGNORE_DEPRECATION(statements) \ - _Pragma("diag_suppress=Pe1444") \ - statements \ - _Pragma("diag_default=Pe1444") -#else -#define CURL_IGNORE_DEPRECATION(statements) \ - _Pragma("GCC diagnostic push") \ - _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") \ - statements \ - _Pragma("GCC diagnostic pop") -#endif -#else -#define CURL_DEPRECATED(version, message) -#define CURL_IGNORE_DEPRECATION(statements) statements -#endif - -#include "curlver.h" /* libcurl version defines */ -#include "system.h" /* determine things runtime */ - -#include -#include - -#if defined(__FreeBSD__) || defined(__MidnightBSD__) -/* Needed for __FreeBSD_version or __MidnightBSD_version symbol definition */ -#include -#endif - -/* The include stuff here below is mainly for time_t! */ -#include -#include - -#if defined(_WIN32) && !defined(_WIN32_WCE) && !defined(__CYGWIN__) -#if !(defined(_WINSOCKAPI_) || defined(_WINSOCK_H) || \ - defined(__LWIP_OPT_H__) || defined(LWIP_HDR_OPT_H)) -/* The check above prevents the winsock2.h inclusion if winsock.h already was - included, since they cannot co-exist without problems */ -#include -#include -#endif -#endif - -/* HP-UX systems version 9, 10 and 11 lack sys/select.h and so does oldish - libc5-based Linux systems. Only include it on systems that are known to - require it! */ -#if defined(_AIX) || defined(__NOVELL_LIBC__) || defined(__NetBSD__) || \ - defined(__minix) || defined(__INTEGRITY) || \ - defined(ANDROID) || defined(__ANDROID__) || defined(__OpenBSD__) || \ - defined(__CYGWIN__) || defined(AMIGA) || defined(__NuttX__) || \ - (defined(__FreeBSD_version) && (__FreeBSD_version < 800000)) || \ - (defined(__MidnightBSD_version) && (__MidnightBSD_version < 100000)) || \ - defined(__sun__) || defined(__serenity__) || defined(__vxworks__) -#include -#endif - -#ifndef _WIN32 -#include -#include -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -typedef void CURL; -typedef void CURLSH; - -/* - * libcurl external API function linkage decorations. - */ - -#ifdef __has_declspec_attribute -#define CURL_HAS_DECLSPEC_ATTRIBUTE(x) __has_declspec_attribute(x) -#else -#define CURL_HAS_DECLSPEC_ATTRIBUTE(x) 0 -#endif - -#ifdef CURL_STATICLIB -# define CURL_EXTERN -#elif defined(_WIN32) || \ - (CURL_HAS_DECLSPEC_ATTRIBUTE(dllexport) && \ - CURL_HAS_DECLSPEC_ATTRIBUTE(dllimport)) -# ifdef BUILDING_LIBCURL -# define CURL_EXTERN __declspec(dllexport) -# else -# define CURL_EXTERN __declspec(dllimport) -# endif -#elif defined(BUILDING_LIBCURL) && defined(CURL_HIDDEN_SYMBOLS) -# define CURL_EXTERN CURL_EXTERN_SYMBOL -#else -# define CURL_EXTERN -#endif - -#ifndef curl_socket_typedef -/* socket typedef */ -#if defined(_WIN32) && !defined(__LWIP_OPT_H__) && !defined(LWIP_HDR_OPT_H) -typedef SOCKET curl_socket_t; -#define CURL_SOCKET_BAD INVALID_SOCKET -#else -typedef int curl_socket_t; -#define CURL_SOCKET_BAD -1 -#endif -#define curl_socket_typedef -#endif /* curl_socket_typedef */ - -/* enum for the different supported SSL backends */ -typedef enum { - CURLSSLBACKEND_NONE = 0, - CURLSSLBACKEND_OPENSSL = 1, - CURLSSLBACKEND_GNUTLS = 2, - CURLSSLBACKEND_NSS CURL_DEPRECATED(8.3.0, "") = 3, - CURLSSLBACKEND_OBSOLETE4 = 4, /* Was QSOSSL. */ - CURLSSLBACKEND_GSKIT CURL_DEPRECATED(8.3.0, "") = 5, - CURLSSLBACKEND_POLARSSL CURL_DEPRECATED(7.69.0, "") = 6, - CURLSSLBACKEND_WOLFSSL = 7, - CURLSSLBACKEND_SCHANNEL = 8, - CURLSSLBACKEND_SECURETRANSPORT CURL_DEPRECATED(8.15.0, "") = 9, - CURLSSLBACKEND_AXTLS CURL_DEPRECATED(7.61.0, "") = 10, - CURLSSLBACKEND_MBEDTLS = 11, - CURLSSLBACKEND_MESALINK CURL_DEPRECATED(7.82.0, "") = 12, - CURLSSLBACKEND_BEARSSL CURL_DEPRECATED(8.15.0, "") = 13, - CURLSSLBACKEND_RUSTLS = 14 -} curl_sslbackend; - -/* aliases for library clones and renames */ -#define CURLSSLBACKEND_AWSLC CURLSSLBACKEND_OPENSSL -#define CURLSSLBACKEND_BORINGSSL CURLSSLBACKEND_OPENSSL -#define CURLSSLBACKEND_LIBRESSL CURLSSLBACKEND_OPENSSL - -/* deprecated names: */ -#define CURLSSLBACKEND_CYASSL CURLSSLBACKEND_WOLFSSL -#define CURLSSLBACKEND_DARWINSSL CURLSSLBACKEND_SECURETRANSPORT - -/* bits for the CURLOPT_FOLLOWLOCATION option */ -#define CURLFOLLOW_ALL 1L /* generic follow redirects */ - -/* Do not use the custom method in the follow-up request if the HTTP code - instructs so (301, 302, 303). */ -#define CURLFOLLOW_OBEYCODE 2L - -/* Only use the custom method in the first request, always reset in the next */ -#define CURLFOLLOW_FIRSTONLY 3L - -struct curl_httppost { - struct curl_httppost *next; /* next entry in the list */ - char *name; /* pointer to allocated name */ - long namelength; /* length of name length */ - char *contents; /* pointer to allocated data contents */ - long contentslength; /* length of contents field, see also - CURL_HTTPPOST_LARGE */ - char *buffer; /* pointer to allocated buffer contents */ - long bufferlength; /* length of buffer field */ - char *contenttype; /* Content-Type */ - struct curl_slist *contentheader; /* list of extra headers for this form */ - struct curl_httppost *more; /* if one field name has more than one - file, this link should link to following - files */ - long flags; /* as defined below */ - -/* specified content is a filename */ -#define CURL_HTTPPOST_FILENAME (1 << 0) -/* specified content is a filename */ -#define CURL_HTTPPOST_READFILE (1 << 1) -/* name is only stored pointer do not free in formfree */ -#define CURL_HTTPPOST_PTRNAME (1 << 2) -/* contents is only stored pointer do not free in formfree */ -#define CURL_HTTPPOST_PTRCONTENTS (1 << 3) -/* upload file from buffer */ -#define CURL_HTTPPOST_BUFFER (1 << 4) -/* upload file from pointer contents */ -#define CURL_HTTPPOST_PTRBUFFER (1 << 5) -/* upload file contents by using the regular read callback to get the data and - pass the given pointer as custom pointer */ -#define CURL_HTTPPOST_CALLBACK (1 << 6) -/* use size in 'contentlen', added in 7.46.0 */ -#define CURL_HTTPPOST_LARGE (1 << 7) - - char *showfilename; /* The filename to show. If not set, the - actual filename will be used (if this - is a file part) */ - void *userp; /* custom pointer used for - HTTPPOST_CALLBACK posts */ - curl_off_t contentlen; /* alternative length of contents - field. Used if CURL_HTTPPOST_LARGE is - set. Added in 7.46.0 */ -}; - -/* This is a return code for the progress callback that, when returned, will - signal libcurl to continue executing the default progress function */ -#define CURL_PROGRESSFUNC_CONTINUE 0x10000001 - -/* This is the CURLOPT_PROGRESSFUNCTION callback prototype. It is now - considered deprecated but was the only choice up until 7.31.0 */ -typedef int (*curl_progress_callback)(void *clientp, - double dltotal, - double dlnow, - double ultotal, - double ulnow); - -/* This is the CURLOPT_XFERINFOFUNCTION callback prototype. It was introduced - in 7.32.0, avoids the use of floating point numbers and provides more - detailed information. */ -typedef int (*curl_xferinfo_callback)(void *clientp, - curl_off_t dltotal, - curl_off_t dlnow, - curl_off_t ultotal, - curl_off_t ulnow); - -#ifndef CURL_MAX_READ_SIZE - /* The maximum receive buffer size configurable via CURLOPT_BUFFERSIZE. */ -#define CURL_MAX_READ_SIZE (10*1024*1024) -#endif - -#ifndef CURL_MAX_WRITE_SIZE - /* Tests have proven that 20K is a bad buffer size for uploads on Windows, - while 16K for some odd reason performed a lot better. We do the ifndef - check to allow this value to easier be changed at build time for those - who feel adventurous. The practical minimum is about 400 bytes since - libcurl uses a buffer of this size as a scratch area (unrelated to - network send operations). */ -#define CURL_MAX_WRITE_SIZE 16384 -#endif - -#ifndef CURL_MAX_HTTP_HEADER -/* The only reason to have a max limit for this is to avoid the risk of a bad - server feeding libcurl with a never-ending header that will cause reallocs - infinitely */ -#define CURL_MAX_HTTP_HEADER (100*1024) -#endif - -/* This is a magic return code for the write callback that, when returned, - will signal libcurl to pause receiving on the current transfer. */ -#define CURL_WRITEFUNC_PAUSE 0x10000001 - -/* This is a magic return code for the write callback that, when returned, - will signal an error from the callback. */ -#define CURL_WRITEFUNC_ERROR 0xFFFFFFFF - -typedef size_t (*curl_write_callback)(char *buffer, - size_t size, - size_t nitems, - void *outstream); - -/* This callback will be called when a new resolver request is made */ -typedef int (*curl_resolver_start_callback)(void *resolver_state, - void *reserved, void *userdata); - -/* enumeration of file types */ -typedef enum { - CURLFILETYPE_FILE = 0, - CURLFILETYPE_DIRECTORY, - CURLFILETYPE_SYMLINK, - CURLFILETYPE_DEVICE_BLOCK, - CURLFILETYPE_DEVICE_CHAR, - CURLFILETYPE_NAMEDPIPE, - CURLFILETYPE_SOCKET, - CURLFILETYPE_DOOR, /* is possible only on Sun Solaris now */ - - CURLFILETYPE_UNKNOWN /* should never occur */ -} curlfiletype; - -#define CURLFINFOFLAG_KNOWN_FILENAME (1 << 0) -#define CURLFINFOFLAG_KNOWN_FILETYPE (1 << 1) -#define CURLFINFOFLAG_KNOWN_TIME (1 << 2) -#define CURLFINFOFLAG_KNOWN_PERM (1 << 3) -#define CURLFINFOFLAG_KNOWN_UID (1 << 4) -#define CURLFINFOFLAG_KNOWN_GID (1 << 5) -#define CURLFINFOFLAG_KNOWN_SIZE (1 << 6) -#define CURLFINFOFLAG_KNOWN_HLINKCOUNT (1 << 7) - -/* Information about a single file, used when doing FTP wildcard matching */ -struct curl_fileinfo { - char *filename; - curlfiletype filetype; - time_t time; /* always zero! */ - unsigned int perm; - int uid; - int gid; - curl_off_t size; - long int hardlinks; - - struct { - /* If some of these fields is not NULL, it is a pointer to b_data. */ - char *time; - char *perm; - char *user; - char *group; - char *target; /* pointer to the target filename of a symlink */ - } strings; - - unsigned int flags; - - /* These are libcurl private struct fields. Previously used by libcurl, so - they must never be interfered with. */ - char *b_data; - size_t b_size; - size_t b_used; -}; - -/* return codes for CURLOPT_CHUNK_BGN_FUNCTION */ -#define CURL_CHUNK_BGN_FUNC_OK 0 -#define CURL_CHUNK_BGN_FUNC_FAIL 1 /* tell the lib to end the task */ -#define CURL_CHUNK_BGN_FUNC_SKIP 2 /* skip this chunk over */ - -/* if splitting of data transfer is enabled, this callback is called before - download of an individual chunk started. Note that parameter "remains" works - only for FTP wildcard downloading (for now), otherwise is not used */ -typedef long (*curl_chunk_bgn_callback)(const void *transfer_info, - void *ptr, - int remains); - -/* return codes for CURLOPT_CHUNK_END_FUNCTION */ -#define CURL_CHUNK_END_FUNC_OK 0 -#define CURL_CHUNK_END_FUNC_FAIL 1 /* tell the lib to end the task */ - -/* If splitting of data transfer is enabled this callback is called after - download of an individual chunk finished. - Note! After this callback was set then it have to be called FOR ALL chunks. - Even if downloading of this chunk was skipped in CHUNK_BGN_FUNC. - This is the reason why we do not need "transfer_info" parameter in this - callback and we are not interested in "remains" parameter too. */ -typedef long (*curl_chunk_end_callback)(void *ptr); - -/* return codes for FNMATCHFUNCTION */ -#define CURL_FNMATCHFUNC_MATCH 0 /* string corresponds to the pattern */ -#define CURL_FNMATCHFUNC_NOMATCH 1 /* pattern does not match the string */ -#define CURL_FNMATCHFUNC_FAIL 2 /* an error occurred */ - -/* callback type for wildcard downloading pattern matching. If the - string matches the pattern, return CURL_FNMATCHFUNC_MATCH value, etc. */ -typedef int (*curl_fnmatch_callback)(void *ptr, - const char *pattern, - const char *string); - -/* These are the return codes for the seek callbacks */ -#define CURL_SEEKFUNC_OK 0 -#define CURL_SEEKFUNC_FAIL 1 /* fail the entire transfer */ -#define CURL_SEEKFUNC_CANTSEEK 2 /* tell libcurl seeking cannot be done, so - libcurl might try other means instead */ -typedef int (*curl_seek_callback)(void *instream, - curl_off_t offset, - int origin); /* 'whence' */ - -/* This is a return code for the read callback that, when returned, will - signal libcurl to immediately abort the current transfer. */ -#define CURL_READFUNC_ABORT 0x10000000 -/* This is a return code for the read callback that, when returned, will - signal libcurl to pause sending data on the current transfer. */ -#define CURL_READFUNC_PAUSE 0x10000001 - -/* Return code for when the trailing headers' callback has terminated - without any errors */ -#define CURL_TRAILERFUNC_OK 0 -/* Return code for when was an error in the trailing header's list and we - want to abort the request */ -#define CURL_TRAILERFUNC_ABORT 1 - -typedef size_t (*curl_read_callback)(char *buffer, - size_t size, - size_t nitems, - void *instream); - -typedef int (*curl_trailer_callback)(struct curl_slist **list, - void *userdata); - -typedef enum { - CURLSOCKTYPE_IPCXN, /* socket created for a specific IP connection */ - CURLSOCKTYPE_ACCEPT, /* socket created by accept() call */ - CURLSOCKTYPE_LAST /* never use */ -} curlsocktype; - -/* The return code from the sockopt_callback can signal information back - to libcurl: */ -#define CURL_SOCKOPT_OK 0 -#define CURL_SOCKOPT_ERROR 1 /* causes libcurl to abort and return - CURLE_ABORTED_BY_CALLBACK */ -#define CURL_SOCKOPT_ALREADY_CONNECTED 2 - -typedef int (*curl_sockopt_callback)(void *clientp, - curl_socket_t curlfd, - curlsocktype purpose); - -struct curl_sockaddr { - int family; - int socktype; - int protocol; - unsigned int addrlen; /* addrlen was a socklen_t type before 7.18.0 but it - turned really ugly and painful on the systems that - lack this type */ - struct sockaddr addr; -}; - -typedef curl_socket_t -(*curl_opensocket_callback)(void *clientp, - curlsocktype purpose, - struct curl_sockaddr *address); - -typedef int -(*curl_closesocket_callback)(void *clientp, curl_socket_t item); - -typedef enum { - CURLIOE_OK, /* I/O operation successful */ - CURLIOE_UNKNOWNCMD, /* command was unknown to callback */ - CURLIOE_FAILRESTART, /* failed to restart the read */ - CURLIOE_LAST /* never use */ -} curlioerr; - -typedef enum { - CURLIOCMD_NOP, /* no operation */ - CURLIOCMD_RESTARTREAD, /* restart the read stream from start */ - CURLIOCMD_LAST /* never use */ -} curliocmd; - -typedef curlioerr (*curl_ioctl_callback)(CURL *handle, - int cmd, - void *clientp); - -#ifndef CURL_DID_MEMORY_FUNC_TYPEDEFS -/* - * The following typedef's are signatures of malloc, free, realloc, strdup and - * calloc respectively. Function pointers of these types can be passed to the - * curl_global_init_mem() function to set user defined memory management - * callback routines. - */ -typedef void *(*curl_malloc_callback)(size_t size); -typedef void (*curl_free_callback)(void *ptr); -typedef void *(*curl_realloc_callback)(void *ptr, size_t size); -typedef char *(*curl_strdup_callback)(const char *str); -typedef void *(*curl_calloc_callback)(size_t nmemb, size_t size); - -#define CURL_DID_MEMORY_FUNC_TYPEDEFS -#endif - -/* the kind of data that is passed to information_callback */ -typedef enum { - CURLINFO_TEXT = 0, - CURLINFO_HEADER_IN, /* 1 */ - CURLINFO_HEADER_OUT, /* 2 */ - CURLINFO_DATA_IN, /* 3 */ - CURLINFO_DATA_OUT, /* 4 */ - CURLINFO_SSL_DATA_IN, /* 5 */ - CURLINFO_SSL_DATA_OUT, /* 6 */ - CURLINFO_END -} curl_infotype; - -typedef int (*curl_debug_callback) - (CURL *handle, /* the handle/transfer this concerns */ - curl_infotype type, /* what kind of data */ - char *data, /* points to the data */ - size_t size, /* size of the data pointed to */ - void *userptr); /* whatever the user please */ - -/* This is the CURLOPT_PREREQFUNCTION callback prototype. */ -typedef int (*curl_prereq_callback)(void *clientp, - char *conn_primary_ip, - char *conn_local_ip, - int conn_primary_port, - int conn_local_port); - -/* Return code for when the pre-request callback has terminated without - any errors */ -#define CURL_PREREQFUNC_OK 0 -/* Return code for when the pre-request callback wants to abort the - request */ -#define CURL_PREREQFUNC_ABORT 1 - -/* All possible error codes from all sorts of curl functions. Future versions - may return other values, stay prepared. - - Always add new return codes last. Never *EVER* remove any. The return - codes must remain the same! - */ - -typedef enum { - CURLE_OK = 0, - CURLE_UNSUPPORTED_PROTOCOL, /* 1 */ - CURLE_FAILED_INIT, /* 2 */ - CURLE_URL_MALFORMAT, /* 3 */ - CURLE_NOT_BUILT_IN, /* 4 - [was obsoleted in August 2007 for - 7.17.0, reused in April 2011 for 7.21.5] */ - CURLE_COULDNT_RESOLVE_PROXY, /* 5 */ - CURLE_COULDNT_RESOLVE_HOST, /* 6 */ - CURLE_COULDNT_CONNECT, /* 7 */ - CURLE_WEIRD_SERVER_REPLY, /* 8 */ - CURLE_REMOTE_ACCESS_DENIED, /* 9 a service was denied by the server - due to lack of access - when login fails - this is not returned. */ - CURLE_FTP_ACCEPT_FAILED, /* 10 - [was obsoleted in April 2006 for - 7.15.4, reused in Dec 2011 for 7.24.0]*/ - CURLE_FTP_WEIRD_PASS_REPLY, /* 11 */ - CURLE_FTP_ACCEPT_TIMEOUT, /* 12 - timeout occurred accepting server - [was obsoleted in August 2007 for 7.17.0, - reused in Dec 2011 for 7.24.0]*/ - CURLE_FTP_WEIRD_PASV_REPLY, /* 13 */ - CURLE_FTP_WEIRD_227_FORMAT, /* 14 */ - CURLE_FTP_CANT_GET_HOST, /* 15 */ - CURLE_HTTP2, /* 16 - A problem in the http2 framing layer. - [was obsoleted in August 2007 for 7.17.0, - reused in July 2014 for 7.38.0] */ - CURLE_FTP_COULDNT_SET_TYPE, /* 17 */ - CURLE_PARTIAL_FILE, /* 18 */ - CURLE_FTP_COULDNT_RETR_FILE, /* 19 */ - CURLE_OBSOLETE20, /* 20 - NOT USED */ - CURLE_QUOTE_ERROR, /* 21 - quote command failure */ - CURLE_HTTP_RETURNED_ERROR, /* 22 */ - CURLE_WRITE_ERROR, /* 23 */ - CURLE_OBSOLETE24, /* 24 - NOT USED */ - CURLE_UPLOAD_FAILED, /* 25 - failed upload "command" */ - CURLE_READ_ERROR, /* 26 - could not open/read from file */ - CURLE_OUT_OF_MEMORY, /* 27 */ - CURLE_OPERATION_TIMEDOUT, /* 28 - the timeout time was reached */ - CURLE_OBSOLETE29, /* 29 - NOT USED */ - CURLE_FTP_PORT_FAILED, /* 30 - FTP PORT operation failed */ - CURLE_FTP_COULDNT_USE_REST, /* 31 - the REST command failed */ - CURLE_OBSOLETE32, /* 32 - NOT USED */ - CURLE_RANGE_ERROR, /* 33 - RANGE "command" did not work */ - CURLE_OBSOLETE34, /* 34 */ - CURLE_SSL_CONNECT_ERROR, /* 35 - wrong when connecting with SSL */ - CURLE_BAD_DOWNLOAD_RESUME, /* 36 - could not resume download */ - CURLE_FILE_COULDNT_READ_FILE, /* 37 */ - CURLE_LDAP_CANNOT_BIND, /* 38 */ - CURLE_LDAP_SEARCH_FAILED, /* 39 */ - CURLE_OBSOLETE40, /* 40 - NOT USED */ - CURLE_OBSOLETE41, /* 41 - NOT USED starting with 7.53.0 */ - CURLE_ABORTED_BY_CALLBACK, /* 42 */ - CURLE_BAD_FUNCTION_ARGUMENT, /* 43 */ - CURLE_OBSOLETE44, /* 44 - NOT USED */ - CURLE_INTERFACE_FAILED, /* 45 - CURLOPT_INTERFACE failed */ - CURLE_OBSOLETE46, /* 46 - NOT USED */ - CURLE_TOO_MANY_REDIRECTS, /* 47 - catch endless re-direct loops */ - CURLE_UNKNOWN_OPTION, /* 48 - User specified an unknown option */ - CURLE_SETOPT_OPTION_SYNTAX, /* 49 - Malformed setopt option */ - CURLE_OBSOLETE50, /* 50 - NOT USED */ - CURLE_OBSOLETE51, /* 51 - NOT USED */ - CURLE_GOT_NOTHING, /* 52 - when this is a specific error */ - CURLE_SSL_ENGINE_NOTFOUND, /* 53 - SSL crypto engine not found */ - CURLE_SSL_ENGINE_SETFAILED, /* 54 - can not set SSL crypto engine as - default */ - CURLE_SEND_ERROR, /* 55 - failed sending network data */ - CURLE_RECV_ERROR, /* 56 - failure in receiving network data */ - CURLE_OBSOLETE57, /* 57 - NOT IN USE */ - CURLE_SSL_CERTPROBLEM, /* 58 - problem with the local certificate */ - CURLE_SSL_CIPHER, /* 59 - could not use specified cipher */ - CURLE_PEER_FAILED_VERIFICATION, /* 60 - peer's certificate or fingerprint - was not verified fine */ - CURLE_BAD_CONTENT_ENCODING, /* 61 - Unrecognized/bad encoding */ - CURLE_OBSOLETE62, /* 62 - NOT IN USE since 7.82.0 */ - CURLE_FILESIZE_EXCEEDED, /* 63 - Maximum file size exceeded */ - CURLE_USE_SSL_FAILED, /* 64 - Requested FTP SSL level failed */ - CURLE_SEND_FAIL_REWIND, /* 65 - Sending the data requires a rewind - that failed */ - CURLE_SSL_ENGINE_INITFAILED, /* 66 - failed to initialise ENGINE */ - CURLE_LOGIN_DENIED, /* 67 - user, password or similar was not - accepted and we failed to login */ - CURLE_TFTP_NOTFOUND, /* 68 - file not found on server */ - CURLE_TFTP_PERM, /* 69 - permission problem on server */ - CURLE_REMOTE_DISK_FULL, /* 70 - out of disk space on server */ - CURLE_TFTP_ILLEGAL, /* 71 - Illegal TFTP operation */ - CURLE_TFTP_UNKNOWNID, /* 72 - Unknown transfer ID */ - CURLE_REMOTE_FILE_EXISTS, /* 73 - File already exists */ - CURLE_TFTP_NOSUCHUSER, /* 74 - No such user */ - CURLE_OBSOLETE75, /* 75 - NOT IN USE since 7.82.0 */ - CURLE_OBSOLETE76, /* 76 - NOT IN USE since 7.82.0 */ - CURLE_SSL_CACERT_BADFILE, /* 77 - could not load CACERT file, missing - or wrong format */ - CURLE_REMOTE_FILE_NOT_FOUND, /* 78 - remote file not found */ - CURLE_SSH, /* 79 - error from the SSH layer, somewhat - generic so the error message will be of - interest when this has happened */ - - CURLE_SSL_SHUTDOWN_FAILED, /* 80 - Failed to shut down the SSL - connection */ - CURLE_AGAIN, /* 81 - socket is not ready for send/recv, - wait till it is ready and try again (Added - in 7.18.2) */ - CURLE_SSL_CRL_BADFILE, /* 82 - could not load CRL file, missing or - wrong format (Added in 7.19.0) */ - CURLE_SSL_ISSUER_ERROR, /* 83 - Issuer check failed. (Added in - 7.19.0) */ - CURLE_FTP_PRET_FAILED, /* 84 - a PRET command failed */ - CURLE_RTSP_CSEQ_ERROR, /* 85 - mismatch of RTSP CSeq numbers */ - CURLE_RTSP_SESSION_ERROR, /* 86 - mismatch of RTSP Session Ids */ - CURLE_FTP_BAD_FILE_LIST, /* 87 - unable to parse FTP file list */ - CURLE_CHUNK_FAILED, /* 88 - chunk callback reported error */ - CURLE_NO_CONNECTION_AVAILABLE, /* 89 - No connection available, the - session will be queued */ - CURLE_SSL_PINNEDPUBKEYNOTMATCH, /* 90 - specified pinned public key did not - match */ - CURLE_SSL_INVALIDCERTSTATUS, /* 91 - invalid certificate status */ - CURLE_HTTP2_STREAM, /* 92 - stream error in HTTP/2 framing layer - */ - CURLE_RECURSIVE_API_CALL, /* 93 - an api function was called from - inside a callback */ - CURLE_AUTH_ERROR, /* 94 - an authentication function returned an - error */ - CURLE_HTTP3, /* 95 - An HTTP/3 layer problem */ - CURLE_QUIC_CONNECT_ERROR, /* 96 - QUIC connection error */ - CURLE_PROXY, /* 97 - proxy handshake error */ - CURLE_SSL_CLIENTCERT, /* 98 - client-side certificate required */ - CURLE_UNRECOVERABLE_POLL, /* 99 - poll/select returned fatal error */ - CURLE_TOO_LARGE, /* 100 - a value/data met its maximum */ - CURLE_ECH_REQUIRED, /* 101 - ECH tried but failed */ - CURL_LAST /* never use! */ -} CURLcode; - -#ifndef CURL_NO_OLDIES /* define this to test if your app builds with all - the obsolete stuff removed! */ - -/* removed in 7.53.0 */ -#define CURLE_FUNCTION_NOT_FOUND CURLE_OBSOLETE41 - -/* removed in 7.56.0 */ -#define CURLE_HTTP_POST_ERROR CURLE_OBSOLETE34 - -/* Previously obsolete error code reused in 7.38.0 */ -#define CURLE_OBSOLETE16 CURLE_HTTP2 - -/* Previously obsolete error codes reused in 7.24.0 */ -#define CURLE_OBSOLETE10 CURLE_FTP_ACCEPT_FAILED -#define CURLE_OBSOLETE12 CURLE_FTP_ACCEPT_TIMEOUT - -/* compatibility with older names */ -#define CURLOPT_ENCODING CURLOPT_ACCEPT_ENCODING -#define CURLE_FTP_WEIRD_SERVER_REPLY CURLE_WEIRD_SERVER_REPLY - -/* The following were added in 7.62.0 */ -#define CURLE_SSL_CACERT CURLE_PEER_FAILED_VERIFICATION - -/* The following were added in 7.21.5, April 2011 */ -#define CURLE_UNKNOWN_TELNET_OPTION CURLE_UNKNOWN_OPTION - -/* Added for 7.78.0 */ -#define CURLE_TELNET_OPTION_SYNTAX CURLE_SETOPT_OPTION_SYNTAX - -/* The following were added in 7.17.1 */ -/* These are scheduled to disappear by 2009 */ -#define CURLE_SSL_PEER_CERTIFICATE CURLE_PEER_FAILED_VERIFICATION - -/* The following were added in 7.17.0 */ -/* These are scheduled to disappear by 2009 */ -#define CURLE_OBSOLETE CURLE_OBSOLETE50 /* no one should be using this! */ -#define CURLE_BAD_PASSWORD_ENTERED CURLE_OBSOLETE46 -#define CURLE_BAD_CALLING_ORDER CURLE_OBSOLETE44 -#define CURLE_FTP_USER_PASSWORD_INCORRECT CURLE_OBSOLETE10 -#define CURLE_FTP_CANT_RECONNECT CURLE_OBSOLETE16 -#define CURLE_FTP_COULDNT_GET_SIZE CURLE_OBSOLETE32 -#define CURLE_FTP_COULDNT_SET_ASCII CURLE_OBSOLETE29 -#define CURLE_FTP_WEIRD_USER_REPLY CURLE_OBSOLETE12 -#define CURLE_FTP_WRITE_ERROR CURLE_OBSOLETE20 -#define CURLE_LIBRARY_NOT_FOUND CURLE_OBSOLETE40 -#define CURLE_MALFORMAT_USER CURLE_OBSOLETE24 -#define CURLE_SHARE_IN_USE CURLE_OBSOLETE57 -#define CURLE_URL_MALFORMAT_USER CURLE_NOT_BUILT_IN - -#define CURLE_FTP_ACCESS_DENIED CURLE_REMOTE_ACCESS_DENIED -#define CURLE_FTP_COULDNT_SET_BINARY CURLE_FTP_COULDNT_SET_TYPE -#define CURLE_FTP_QUOTE_ERROR CURLE_QUOTE_ERROR -#define CURLE_TFTP_DISKFULL CURLE_REMOTE_DISK_FULL -#define CURLE_TFTP_EXISTS CURLE_REMOTE_FILE_EXISTS -#define CURLE_HTTP_RANGE_ERROR CURLE_RANGE_ERROR -#define CURLE_FTP_SSL_FAILED CURLE_USE_SSL_FAILED - -/* The following were added earlier */ - -#define CURLE_OPERATION_TIMEOUTED CURLE_OPERATION_TIMEDOUT -#define CURLE_HTTP_NOT_FOUND CURLE_HTTP_RETURNED_ERROR -#define CURLE_HTTP_PORT_FAILED CURLE_INTERFACE_FAILED -#define CURLE_FTP_COULDNT_STOR_FILE CURLE_UPLOAD_FAILED -#define CURLE_FTP_PARTIAL_FILE CURLE_PARTIAL_FILE -#define CURLE_FTP_BAD_DOWNLOAD_RESUME CURLE_BAD_DOWNLOAD_RESUME -#define CURLE_LDAP_INVALID_URL CURLE_OBSOLETE62 -#define CURLE_CONV_REQD CURLE_OBSOLETE76 -#define CURLE_CONV_FAILED CURLE_OBSOLETE75 - -/* This was the error code 50 in 7.7.3 and a few earlier versions, this - is no longer used by libcurl but is instead #defined here only to not - make programs break */ -#define CURLE_ALREADY_COMPLETE 99999 - -/* Provide defines for really old option names */ -#define CURLOPT_FILE CURLOPT_WRITEDATA /* name changed in 7.9.7 */ -#define CURLOPT_INFILE CURLOPT_READDATA /* name changed in 7.9.7 */ -#define CURLOPT_WRITEHEADER CURLOPT_HEADERDATA - -/* Since long deprecated options with no code in the lib that does anything - with them. */ -#define CURLOPT_WRITEINFO CURLOPT_OBSOLETE40 -#define CURLOPT_CLOSEPOLICY CURLOPT_OBSOLETE72 -#define CURLOPT_OBSOLETE72 9999 -#define CURLOPT_OBSOLETE40 9999 - -#endif /* !CURL_NO_OLDIES */ - -/* - * Proxy error codes. Returned in CURLINFO_PROXY_ERROR if CURLE_PROXY was - * return for the transfers. - */ -typedef enum { - CURLPX_OK, - CURLPX_BAD_ADDRESS_TYPE, - CURLPX_BAD_VERSION, - CURLPX_CLOSED, - CURLPX_GSSAPI, - CURLPX_GSSAPI_PERMSG, - CURLPX_GSSAPI_PROTECTION, - CURLPX_IDENTD, - CURLPX_IDENTD_DIFFER, - CURLPX_LONG_HOSTNAME, - CURLPX_LONG_PASSWD, - CURLPX_LONG_USER, - CURLPX_NO_AUTH, - CURLPX_RECV_ADDRESS, - CURLPX_RECV_AUTH, - CURLPX_RECV_CONNECT, - CURLPX_RECV_REQACK, - CURLPX_REPLY_ADDRESS_TYPE_NOT_SUPPORTED, - CURLPX_REPLY_COMMAND_NOT_SUPPORTED, - CURLPX_REPLY_CONNECTION_REFUSED, - CURLPX_REPLY_GENERAL_SERVER_FAILURE, - CURLPX_REPLY_HOST_UNREACHABLE, - CURLPX_REPLY_NETWORK_UNREACHABLE, - CURLPX_REPLY_NOT_ALLOWED, - CURLPX_REPLY_TTL_EXPIRED, - CURLPX_REPLY_UNASSIGNED, - CURLPX_REQUEST_FAILED, - CURLPX_RESOLVE_HOST, - CURLPX_SEND_AUTH, - CURLPX_SEND_CONNECT, - CURLPX_SEND_REQUEST, - CURLPX_UNKNOWN_FAIL, - CURLPX_UNKNOWN_MODE, - CURLPX_USER_REJECTED, - CURLPX_LAST /* never use */ -} CURLproxycode; - -/* This prototype applies to all conversion callbacks */ -typedef CURLcode (*curl_conv_callback)(char *buffer, size_t length); - -typedef CURLcode (*curl_ssl_ctx_callback)(CURL *curl, /* easy handle */ - void *ssl_ctx, /* actually an OpenSSL - or wolfSSL SSL_CTX, - or an mbedTLS - mbedtls_ssl_config */ - void *userptr); - -#define CURLPROXY_HTTP 0L /* added in 7.10, new in 7.19.4 default is - to use CONNECT HTTP/1.1 */ -#define CURLPROXY_HTTP_1_0 1L /* force to use CONNECT HTTP/1.0 - added in 7.19.4 */ -#define CURLPROXY_HTTPS 2L /* HTTPS but stick to HTTP/1 - added in 7.52.0 */ -#define CURLPROXY_HTTPS2 3L /* HTTPS and attempt HTTP/2 - added in 8.2.0 */ -#define CURLPROXY_SOCKS4 4L /* support added in 7.15.2, enum existed - already in 7.10 */ -#define CURLPROXY_SOCKS5 5L /* added in 7.10 */ -#define CURLPROXY_SOCKS4A 6L /* added in 7.18.0 */ -#define CURLPROXY_SOCKS5_HOSTNAME 7L /* Use the SOCKS5 protocol but pass along - the hostname rather than the IP - address. added in 7.18.0 */ - -typedef enum { - CURLPROXY_LAST = 8 /* never use */ -} curl_proxytype; /* this enum was added in 7.10 */ - -/* - * Bitmasks for CURLOPT_HTTPAUTH and CURLOPT_PROXYAUTH options: - * - * CURLAUTH_NONE - No HTTP authentication - * CURLAUTH_BASIC - HTTP Basic authentication (default) - * CURLAUTH_DIGEST - HTTP Digest authentication - * CURLAUTH_NEGOTIATE - HTTP Negotiate (SPNEGO) authentication - * CURLAUTH_GSSNEGOTIATE - Alias for CURLAUTH_NEGOTIATE (deprecated) - * CURLAUTH_NTLM - HTTP NTLM authentication - * CURLAUTH_DIGEST_IE - HTTP Digest authentication with IE flavour - * CURLAUTH_NTLM_WB - HTTP NTLM authentication delegated to winbind helper - * CURLAUTH_BEARER - HTTP Bearer token authentication - * CURLAUTH_ONLY - Use together with a single other type to force no - * authentication or just that single type - * CURLAUTH_ANY - All fine types set - * CURLAUTH_ANYSAFE - All fine types except Basic - */ - -#define CURLAUTH_NONE ((unsigned long)0) -#define CURLAUTH_BASIC (((unsigned long)1) << 0) -#define CURLAUTH_DIGEST (((unsigned long)1) << 1) -#define CURLAUTH_NEGOTIATE (((unsigned long)1) << 2) -/* Deprecated since the advent of CURLAUTH_NEGOTIATE */ -#define CURLAUTH_GSSNEGOTIATE CURLAUTH_NEGOTIATE -/* Used for CURLOPT_SOCKS5_AUTH to stay terminologically correct */ -#define CURLAUTH_GSSAPI CURLAUTH_NEGOTIATE -#define CURLAUTH_NTLM (((unsigned long)1) << 3) -#define CURLAUTH_DIGEST_IE (((unsigned long)1) << 4) -#ifndef CURL_NO_OLDIES - /* functionality removed since 8.8.0 */ -#define CURLAUTH_NTLM_WB (((unsigned long)1) << 5) -#endif -#define CURLAUTH_BEARER (((unsigned long)1) << 6) -#define CURLAUTH_AWS_SIGV4 (((unsigned long)1) << 7) -#define CURLAUTH_ONLY (((unsigned long)1) << 31) -#define CURLAUTH_ANY (~CURLAUTH_DIGEST_IE) -#define CURLAUTH_ANYSAFE (~(CURLAUTH_BASIC | CURLAUTH_DIGEST_IE)) - -#define CURLSSH_AUTH_ANY ~0L /* all types supported by server */ -#define CURLSSH_AUTH_NONE 0L /* none allowed, silly but complete */ -#define CURLSSH_AUTH_PUBLICKEY (1L << 0) /* public/private key files */ -#define CURLSSH_AUTH_PASSWORD (1L << 1) /* password */ -#define CURLSSH_AUTH_HOST (1L << 2) /* host key files */ -#define CURLSSH_AUTH_KEYBOARD (1L << 3) /* keyboard interactive */ -#define CURLSSH_AUTH_AGENT (1L << 4) /* agent (ssh-agent, pageant...) */ -#define CURLSSH_AUTH_GSSAPI (1L << 5) /* gssapi (kerberos, ...) */ -#define CURLSSH_AUTH_DEFAULT CURLSSH_AUTH_ANY - -#define CURLGSSAPI_DELEGATION_NONE 0L /* no delegation (default) */ -#define CURLGSSAPI_DELEGATION_POLICY_FLAG (1L<<0) /* if permitted by policy */ -#define CURLGSSAPI_DELEGATION_FLAG (1L<<1) /* delegate always */ - -#define CURL_ERROR_SIZE 256 - -enum curl_khtype { - CURLKHTYPE_UNKNOWN, - CURLKHTYPE_RSA1, - CURLKHTYPE_RSA, - CURLKHTYPE_DSS, - CURLKHTYPE_ECDSA, - CURLKHTYPE_ED25519 -}; - -struct curl_khkey { - const char *key; /* points to a null-terminated string encoded with base64 - if len is zero, otherwise to the "raw" data */ - size_t len; - enum curl_khtype keytype; -}; - -/* this is the set of return values expected from the curl_sshkeycallback - callback */ -enum curl_khstat { - CURLKHSTAT_FINE_ADD_TO_FILE, - CURLKHSTAT_FINE, - CURLKHSTAT_REJECT, /* reject the connection, return an error */ - CURLKHSTAT_DEFER, /* do not accept it, but we cannot answer right now. - Causes a CURLE_PEER_FAILED_VERIFICATION error but the - connection will be left intact etc */ - CURLKHSTAT_FINE_REPLACE, /* accept and replace the wrong key */ - CURLKHSTAT_LAST /* not for use, only a marker for last-in-list */ -}; - -/* this is the set of status codes pass in to the callback */ -enum curl_khmatch { - CURLKHMATCH_OK, /* match */ - CURLKHMATCH_MISMATCH, /* host found, key mismatch! */ - CURLKHMATCH_MISSING, /* no matching host/key found */ - CURLKHMATCH_LAST /* not for use, only a marker for last-in-list */ -}; - -typedef int - (*curl_sshkeycallback) (CURL *easy, /* easy handle */ - const struct curl_khkey *knownkey, /* known */ - const struct curl_khkey *foundkey, /* found */ - enum curl_khmatch, /* libcurl's view on the keys */ - void *clientp); /* custom pointer passed with */ - /* CURLOPT_SSH_KEYDATA */ - -typedef int - (*curl_sshhostkeycallback) (void *clientp,/* custom pointer passed */ - /* with CURLOPT_SSH_HOSTKEYDATA */ - int keytype, /* CURLKHTYPE */ - const char *key, /* hostkey to check */ - size_t keylen); /* length of the key */ - /* return CURLE_OK to accept */ - /* or something else to refuse */ - -/* parameter for the CURLOPT_USE_SSL option */ -#define CURLUSESSL_NONE 0L /* do not attempt to use SSL */ -#define CURLUSESSL_TRY 1L /* try using SSL, proceed anyway otherwise */ -#define CURLUSESSL_CONTROL 2L /* SSL for the control connection or fail */ -#define CURLUSESSL_ALL 3L /* SSL for all communication or fail */ - -typedef enum { - CURLUSESSL_LAST = 4 /* not an option, never use */ -} curl_usessl; - -/* Definition of bits for the CURLOPT_SSL_OPTIONS argument: */ - -/* - ALLOW_BEAST tells libcurl to allow the BEAST SSL vulnerability in the - name of improving interoperability with older servers. Some SSL libraries - have introduced work-arounds for this flaw but those work-arounds sometimes - make the SSL communication fail. To regain functionality with those broken - servers, a user can this way allow the vulnerability back. */ -#define CURLSSLOPT_ALLOW_BEAST (1L << 0) - -/* - NO_REVOKE tells libcurl to disable certificate revocation checks for those - SSL backends where such behavior is present. */ -#define CURLSSLOPT_NO_REVOKE (1L << 1) - -/* - NO_PARTIALCHAIN tells libcurl to *NOT* accept a partial certificate chain - if possible. The OpenSSL backend has this ability. */ -#define CURLSSLOPT_NO_PARTIALCHAIN (1L << 2) - -/* - REVOKE_BEST_EFFORT tells libcurl to ignore certificate revocation offline - checks and ignore missing revocation list for those SSL backends where such - behavior is present. */ -#define CURLSSLOPT_REVOKE_BEST_EFFORT (1L << 3) - -/* - CURLSSLOPT_NATIVE_CA tells libcurl to use standard certificate store of - operating system. Currently implemented under MS-Windows. */ -#define CURLSSLOPT_NATIVE_CA (1L << 4) - -/* - CURLSSLOPT_AUTO_CLIENT_CERT tells libcurl to automatically locate and use - a client certificate for authentication. (Schannel) */ -#define CURLSSLOPT_AUTO_CLIENT_CERT (1L << 5) - -/* If possible, send data using TLS 1.3 early data */ -#define CURLSSLOPT_EARLYDATA (1L << 6) - -/* The default connection attempt delay in milliseconds for happy eyeballs. - CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS.3 and happy-eyeballs-timeout-ms.d document - this value, keep them in sync. */ -#define CURL_HET_DEFAULT 200L - -/* The default connection upkeep interval in milliseconds. */ -#define CURL_UPKEEP_INTERVAL_DEFAULT 60000L - -#ifndef CURL_NO_OLDIES /* define this to test if your app builds with all - the obsolete stuff removed! */ - -/* Backwards compatibility with older names */ -/* These are scheduled to disappear by 2009 */ - -#define CURLFTPSSL_NONE CURLUSESSL_NONE -#define CURLFTPSSL_TRY CURLUSESSL_TRY -#define CURLFTPSSL_CONTROL CURLUSESSL_CONTROL -#define CURLFTPSSL_ALL CURLUSESSL_ALL -#define CURLFTPSSL_LAST CURLUSESSL_LAST -#define curl_ftpssl curl_usessl -#endif /* !CURL_NO_OLDIES */ - -/* parameter for the CURLOPT_FTP_SSL_CCC option */ -#define CURLFTPSSL_CCC_NONE 0L /* do not send CCC */ -#define CURLFTPSSL_CCC_PASSIVE 1L /* Let the server initiate the shutdown */ -#define CURLFTPSSL_CCC_ACTIVE 2L /* Initiate the shutdown */ - -typedef enum { - CURLFTPSSL_CCC_LAST = 3 /* not an option, never use */ -} curl_ftpccc; - -/* parameter for the CURLOPT_FTPSSLAUTH option */ -#define CURLFTPAUTH_DEFAULT 0L /* let libcurl decide */ -#define CURLFTPAUTH_SSL 1L /* use "AUTH SSL" */ -#define CURLFTPAUTH_TLS 2L /* use "AUTH TLS" */ - -typedef enum { - CURLFTPAUTH_LAST = 3 /* not an option, never use */ -} curl_ftpauth; - -/* parameter for the CURLOPT_FTP_CREATE_MISSING_DIRS option */ -#define CURLFTP_CREATE_DIR_NONE 0L /* do NOT create missing dirs! */ -#define CURLFTP_CREATE_DIR 1L /* (FTP/SFTP) if CWD fails, try MKD and - then CWD again if MKD succeeded, for - SFTP this does similar magic */ -#define CURLFTP_CREATE_DIR_RETRY 2L /* (FTP only) if CWD fails, try MKD and - then CWD again even if MKD failed! */ - -typedef enum { - CURLFTP_CREATE_DIR_LAST = 3 /* not an option, never use */ -} curl_ftpcreatedir; - -/* parameter for the CURLOPT_FTP_FILEMETHOD option */ -#define CURLFTPMETHOD_DEFAULT 0L /* let libcurl pick */ -#define CURLFTPMETHOD_MULTICWD 1L /* single CWD operation for each path - part */ -#define CURLFTPMETHOD_NOCWD 2L /* no CWD at all */ -#define CURLFTPMETHOD_SINGLECWD 3L /* one CWD to full dir, then work on file */ - -typedef enum { - CURLFTPMETHOD_LAST = 4 /* not an option, never use */ -} curl_ftpmethod; - -/* bitmask defines for CURLOPT_HEADEROPT */ -#define CURLHEADER_UNIFIED 0L -#define CURLHEADER_SEPARATE (1L << 0) - -/* CURLALTSVC_* are bits for the CURLOPT_ALTSVC_CTRL option */ -#define CURLALTSVC_READONLYFILE (1L << 2) -#define CURLALTSVC_H1 (1L << 3) -#define CURLALTSVC_H2 (1L << 4) -#define CURLALTSVC_H3 (1L << 5) - -/* bitmask values for CURLOPT_UPLOAD_FLAGS */ -#define CURLULFLAG_ANSWERED (1L << 0) -#define CURLULFLAG_DELETED (1L << 1) -#define CURLULFLAG_DRAFT (1L << 2) -#define CURLULFLAG_FLAGGED (1L << 3) -#define CURLULFLAG_SEEN (1L << 4) - -struct curl_hstsentry { - char *name; - size_t namelen; - unsigned int includeSubDomains:1; - char expire[18]; /* YYYYMMDD HH:MM:SS [null-terminated] */ -}; - -struct curl_index { - size_t index; /* the provided entry's "index" or count */ - size_t total; /* total number of entries to save */ -}; - -typedef enum { - CURLSTS_OK, - CURLSTS_DONE, - CURLSTS_FAIL -} CURLSTScode; - -typedef CURLSTScode (*curl_hstsread_callback)(CURL *easy, - struct curl_hstsentry *e, - void *userp); -typedef CURLSTScode (*curl_hstswrite_callback)(CURL *easy, - struct curl_hstsentry *e, - struct curl_index *i, - void *userp); - -/* CURLHSTS_* are bits for the CURLOPT_HSTS option */ -#define CURLHSTS_ENABLE (1L << 0) -#define CURLHSTS_READONLYFILE (1L << 1) - -/* The CURLPROTO_ defines below are for the **deprecated** CURLOPT_*PROTOCOLS - options. Do not use. */ -#define CURLPROTO_HTTP (1L << 0) -#define CURLPROTO_HTTPS (1L << 1) -#define CURLPROTO_FTP (1L << 2) -#define CURLPROTO_FTPS (1L << 3) -#define CURLPROTO_SCP (1L << 4) -#define CURLPROTO_SFTP (1L << 5) -#define CURLPROTO_TELNET (1L << 6) -#define CURLPROTO_LDAP (1L << 7) -#define CURLPROTO_LDAPS (1L << 8) -#define CURLPROTO_DICT (1L << 9) -#define CURLPROTO_FILE (1L << 10) -#define CURLPROTO_TFTP (1L << 11) -#define CURLPROTO_IMAP (1L << 12) -#define CURLPROTO_IMAPS (1L << 13) -#define CURLPROTO_POP3 (1L << 14) -#define CURLPROTO_POP3S (1L << 15) -#define CURLPROTO_SMTP (1L << 16) -#define CURLPROTO_SMTPS (1L << 17) -#define CURLPROTO_RTSP (1L << 18) -#define CURLPROTO_RTMP (1L << 19) -#define CURLPROTO_RTMPT (1L << 20) -#define CURLPROTO_RTMPE (1L << 21) -#define CURLPROTO_RTMPTE (1L << 22) -#define CURLPROTO_RTMPS (1L << 23) -#define CURLPROTO_RTMPTS (1L << 24) -#define CURLPROTO_GOPHER (1L << 25) -#define CURLPROTO_SMB (1L << 26) -#define CURLPROTO_SMBS (1L << 27) -#define CURLPROTO_MQTT (1L << 28) -#define CURLPROTO_GOPHERS (1L << 29) -#define CURLPROTO_ALL (~0L) /* enable everything */ - -/* long may be 32 or 64 bits, but we should never depend on anything else - but 32 */ -#define CURLOPTTYPE_LONG 0 -#define CURLOPTTYPE_OBJECTPOINT 10000 -#define CURLOPTTYPE_FUNCTIONPOINT 20000 -#define CURLOPTTYPE_OFF_T 30000 -#define CURLOPTTYPE_BLOB 40000 - -/* *STRINGPOINT is an alias for OBJECTPOINT to allow tools to extract the - string options from the header file */ - -#define CURLOPT(na,t,nu) na = t + nu -#define CURLOPTDEPRECATED(na,t,nu,v,m) na CURL_DEPRECATED(v,m) = t + nu - -/* CURLOPT aliases that make no runtime difference */ - -/* 'char *' argument to a string with a trailing zero */ -#define CURLOPTTYPE_STRINGPOINT CURLOPTTYPE_OBJECTPOINT - -/* 'struct curl_slist *' argument */ -#define CURLOPTTYPE_SLISTPOINT CURLOPTTYPE_OBJECTPOINT - -/* 'void *' argument passed untouched to callback */ -#define CURLOPTTYPE_CBPOINT CURLOPTTYPE_OBJECTPOINT - -/* 'long' argument with a set of values/bitmask */ -#define CURLOPTTYPE_VALUES CURLOPTTYPE_LONG - -/* - * All CURLOPT_* values. - */ - -typedef enum { - /* This is the FILE * or void * the regular output should be written to. */ - CURLOPT(CURLOPT_WRITEDATA, CURLOPTTYPE_CBPOINT, 1), - - /* The full URL to get/put */ - CURLOPT(CURLOPT_URL, CURLOPTTYPE_STRINGPOINT, 2), - - /* Port number to connect to, if other than default. */ - CURLOPT(CURLOPT_PORT, CURLOPTTYPE_LONG, 3), - - /* Name of proxy to use. */ - CURLOPT(CURLOPT_PROXY, CURLOPTTYPE_STRINGPOINT, 4), - - /* "user:password;options" to use when fetching. */ - CURLOPT(CURLOPT_USERPWD, CURLOPTTYPE_STRINGPOINT, 5), - - /* "user:password" to use with proxy. */ - CURLOPT(CURLOPT_PROXYUSERPWD, CURLOPTTYPE_STRINGPOINT, 6), - - /* Range to get, specified as an ASCII string. */ - CURLOPT(CURLOPT_RANGE, CURLOPTTYPE_STRINGPOINT, 7), - - /* not used */ - - /* Specified file stream to upload from (use as input): */ - CURLOPT(CURLOPT_READDATA, CURLOPTTYPE_CBPOINT, 9), - - /* Buffer to receive error messages in, must be at least CURL_ERROR_SIZE - * bytes big. */ - CURLOPT(CURLOPT_ERRORBUFFER, CURLOPTTYPE_OBJECTPOINT, 10), - - /* Function that will be called to store the output (instead of fwrite). The - * parameters will use fwrite() syntax, make sure to follow them. */ - CURLOPT(CURLOPT_WRITEFUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 11), - - /* Function that will be called to read the input (instead of fread). The - * parameters will use fread() syntax, make sure to follow them. */ - CURLOPT(CURLOPT_READFUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 12), - - /* Time-out the read operation after this amount of seconds */ - CURLOPT(CURLOPT_TIMEOUT, CURLOPTTYPE_LONG, 13), - - /* If CURLOPT_READDATA is used, this can be used to inform libcurl about - * how large the file being sent really is. That allows better error - * checking and better verifies that the upload was successful. -1 means - * unknown size. - * - * For large file support, there is also a _LARGE version of the key - * which takes an off_t type, allowing platforms with larger off_t - * sizes to handle larger files. See below for INFILESIZE_LARGE. - */ - CURLOPT(CURLOPT_INFILESIZE, CURLOPTTYPE_LONG, 14), - - /* POST static input fields. */ - CURLOPT(CURLOPT_POSTFIELDS, CURLOPTTYPE_OBJECTPOINT, 15), - - /* Set the referrer page (needed by some CGIs) */ - CURLOPT(CURLOPT_REFERER, CURLOPTTYPE_STRINGPOINT, 16), - - /* Set the FTP PORT string (interface name, named or numerical IP address) - Use i.e '-' to use default address. */ - CURLOPT(CURLOPT_FTPPORT, CURLOPTTYPE_STRINGPOINT, 17), - - /* Set the User-Agent string (examined by some CGIs) */ - CURLOPT(CURLOPT_USERAGENT, CURLOPTTYPE_STRINGPOINT, 18), - - /* If the download receives less than "low speed limit" bytes/second - * during "low speed time" seconds, the operations is aborted. - * You could i.e if you have a pretty high speed connection, abort if - * it is less than 2000 bytes/sec during 20 seconds. - */ - - /* Set the "low speed limit" */ - CURLOPT(CURLOPT_LOW_SPEED_LIMIT, CURLOPTTYPE_LONG, 19), - - /* Set the "low speed time" */ - CURLOPT(CURLOPT_LOW_SPEED_TIME, CURLOPTTYPE_LONG, 20), - - /* Set the continuation offset. - * - * Note there is also a _LARGE version of this key which uses - * off_t types, allowing for large file offsets on platforms which - * use larger-than-32-bit off_t's. Look below for RESUME_FROM_LARGE. - */ - CURLOPT(CURLOPT_RESUME_FROM, CURLOPTTYPE_LONG, 21), - - /* Set cookie in request: */ - CURLOPT(CURLOPT_COOKIE, CURLOPTTYPE_STRINGPOINT, 22), - - /* This points to a linked list of headers, struct curl_slist kind. This - list is also used for RTSP (in spite of its name) */ - CURLOPT(CURLOPT_HTTPHEADER, CURLOPTTYPE_SLISTPOINT, 23), - - /* This points to a linked list of post entries, struct curl_httppost */ - CURLOPTDEPRECATED(CURLOPT_HTTPPOST, CURLOPTTYPE_OBJECTPOINT, 24, - 7.56.0, "Use CURLOPT_MIMEPOST"), - - /* name of the file keeping your private SSL-certificate */ - CURLOPT(CURLOPT_SSLCERT, CURLOPTTYPE_STRINGPOINT, 25), - - /* password for the SSL or SSH private key */ - CURLOPT(CURLOPT_KEYPASSWD, CURLOPTTYPE_STRINGPOINT, 26), - - /* send TYPE parameter? */ - CURLOPT(CURLOPT_CRLF, CURLOPTTYPE_LONG, 27), - - /* send linked-list of QUOTE commands */ - CURLOPT(CURLOPT_QUOTE, CURLOPTTYPE_SLISTPOINT, 28), - - /* send FILE * or void * to store headers to, if you use a callback it - is simply passed to the callback unmodified */ - CURLOPT(CURLOPT_HEADERDATA, CURLOPTTYPE_CBPOINT, 29), - - /* point to a file to read the initial cookies from, also enables - "cookie awareness" */ - CURLOPT(CURLOPT_COOKIEFILE, CURLOPTTYPE_STRINGPOINT, 31), - - /* What version to specifically try to use. - See CURL_SSLVERSION defines below. */ - CURLOPT(CURLOPT_SSLVERSION, CURLOPTTYPE_VALUES, 32), - - /* What kind of HTTP time condition to use, see defines */ - CURLOPT(CURLOPT_TIMECONDITION, CURLOPTTYPE_VALUES, 33), - - /* Time to use with the above condition. Specified in number of seconds - since 1 Jan 1970 */ - CURLOPT(CURLOPT_TIMEVALUE, CURLOPTTYPE_LONG, 34), - - /* 35 = OBSOLETE */ - - /* Custom request, for customizing the get command like - HTTP: DELETE, TRACE and others - FTP: to use a different list command - */ - CURLOPT(CURLOPT_CUSTOMREQUEST, CURLOPTTYPE_STRINGPOINT, 36), - - /* FILE handle to use instead of stderr */ - CURLOPT(CURLOPT_STDERR, CURLOPTTYPE_OBJECTPOINT, 37), - - /* 38 is not used */ - - /* send linked-list of post-transfer QUOTE commands */ - CURLOPT(CURLOPT_POSTQUOTE, CURLOPTTYPE_SLISTPOINT, 39), - - /* 40 is not used */ - - /* talk a lot */ - CURLOPT(CURLOPT_VERBOSE, CURLOPTTYPE_LONG, 41), - - /* throw the header out too */ - CURLOPT(CURLOPT_HEADER, CURLOPTTYPE_LONG, 42), - - /* shut off the progress meter */ - CURLOPT(CURLOPT_NOPROGRESS, CURLOPTTYPE_LONG, 43), - - /* use HEAD to get http document */ - CURLOPT(CURLOPT_NOBODY, CURLOPTTYPE_LONG, 44), - - /* no output on http error codes >= 400 */ - CURLOPT(CURLOPT_FAILONERROR, CURLOPTTYPE_LONG, 45), - - /* this is an upload */ - CURLOPT(CURLOPT_UPLOAD, CURLOPTTYPE_LONG, 46), - - /* HTTP POST method */ - CURLOPT(CURLOPT_POST, CURLOPTTYPE_LONG, 47), - - /* bare names when listing directories */ - CURLOPT(CURLOPT_DIRLISTONLY, CURLOPTTYPE_LONG, 48), - - /* Append instead of overwrite on upload! */ - CURLOPT(CURLOPT_APPEND, CURLOPTTYPE_LONG, 50), - - /* Specify whether to read the user+password from the .netrc or the URL. - * This must be one of the CURL_NETRC_* enums below. */ - CURLOPT(CURLOPT_NETRC, CURLOPTTYPE_VALUES, 51), - - /* use Location: Luke! */ - CURLOPT(CURLOPT_FOLLOWLOCATION, CURLOPTTYPE_LONG, 52), - - /* transfer data in text/ASCII format */ - CURLOPT(CURLOPT_TRANSFERTEXT, CURLOPTTYPE_LONG, 53), - - /* HTTP PUT */ - CURLOPTDEPRECATED(CURLOPT_PUT, CURLOPTTYPE_LONG, 54, - 7.12.1, "Use CURLOPT_UPLOAD"), - - /* 55 = OBSOLETE */ - - /* DEPRECATED - * Function that will be called instead of the internal progress display - * function. This function should be defined as the curl_progress_callback - * prototype defines. */ - CURLOPTDEPRECATED(CURLOPT_PROGRESSFUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 56, - 7.32.0, "Use CURLOPT_XFERINFOFUNCTION"), - - /* Data passed to the CURLOPT_PROGRESSFUNCTION and CURLOPT_XFERINFOFUNCTION - callbacks */ - CURLOPT(CURLOPT_XFERINFODATA, CURLOPTTYPE_CBPOINT, 57), -#define CURLOPT_PROGRESSDATA CURLOPT_XFERINFODATA - - /* We want the referrer field set automatically when following locations */ - CURLOPT(CURLOPT_AUTOREFERER, CURLOPTTYPE_LONG, 58), - - /* Port of the proxy, can be set in the proxy string as well with: - "[host]:[port]" */ - CURLOPT(CURLOPT_PROXYPORT, CURLOPTTYPE_LONG, 59), - - /* size of the POST input data, if strlen() is not good to use */ - CURLOPT(CURLOPT_POSTFIELDSIZE, CURLOPTTYPE_LONG, 60), - - /* tunnel non-http operations through an HTTP proxy */ - CURLOPT(CURLOPT_HTTPPROXYTUNNEL, CURLOPTTYPE_LONG, 61), - - /* Set the interface string to use as outgoing network interface */ - CURLOPT(CURLOPT_INTERFACE, CURLOPTTYPE_STRINGPOINT, 62), - - /* Set the krb4/5 security level, this also enables krb4/5 awareness. This - * is a string, 'clear', 'safe', 'confidential' or 'private'. If the string - * is set but does not match one of these, 'private' will be used. */ - CURLOPTDEPRECATED(CURLOPT_KRBLEVEL, CURLOPTTYPE_STRINGPOINT, 63, - 8.17.0, "removed"), - - /* Set if we should verify the peer in ssl handshake, set 1 to verify. */ - CURLOPT(CURLOPT_SSL_VERIFYPEER, CURLOPTTYPE_LONG, 64), - - /* The CApath or CAfile used to validate the peer certificate - this option is used only if SSL_VERIFYPEER is true */ - CURLOPT(CURLOPT_CAINFO, CURLOPTTYPE_STRINGPOINT, 65), - - /* 66 = OBSOLETE */ - /* 67 = OBSOLETE */ - - /* Maximum number of http redirects to follow */ - CURLOPT(CURLOPT_MAXREDIRS, CURLOPTTYPE_LONG, 68), - - /* Pass a long set to 1 to get the date of the requested document (if - possible)! Pass a zero to shut it off. */ - CURLOPT(CURLOPT_FILETIME, CURLOPTTYPE_LONG, 69), - - /* This points to a linked list of telnet options */ - CURLOPT(CURLOPT_TELNETOPTIONS, CURLOPTTYPE_SLISTPOINT, 70), - - /* Max amount of cached alive connections */ - CURLOPT(CURLOPT_MAXCONNECTS, CURLOPTTYPE_LONG, 71), - - /* 72 = OBSOLETE */ - /* 73 = OBSOLETE */ - - /* Set to explicitly use a new connection for the upcoming transfer. - Do not use this unless you are absolutely sure of this, as it makes the - operation slower and is less friendly for the network. */ - CURLOPT(CURLOPT_FRESH_CONNECT, CURLOPTTYPE_LONG, 74), - - /* Set to explicitly forbid the upcoming transfer's connection to be reused - when done. Do not use this unless you are absolutely sure of this, as it - makes the operation slower and is less friendly for the network. */ - CURLOPT(CURLOPT_FORBID_REUSE, CURLOPTTYPE_LONG, 75), - - /* Set to a filename that contains random data for libcurl to use to - seed the random engine when doing SSL connects. */ - CURLOPTDEPRECATED(CURLOPT_RANDOM_FILE, CURLOPTTYPE_STRINGPOINT, 76, - 7.84.0, "Serves no purpose anymore"), - - /* Set to the Entropy Gathering Daemon socket pathname */ - CURLOPTDEPRECATED(CURLOPT_EGDSOCKET, CURLOPTTYPE_STRINGPOINT, 77, - 7.84.0, "Serves no purpose anymore"), - - /* Time-out connect operations after this amount of seconds, if connects are - OK within this time, then fine... This only aborts the connect phase. */ - CURLOPT(CURLOPT_CONNECTTIMEOUT, CURLOPTTYPE_LONG, 78), - - /* Function that will be called to store headers (instead of fwrite). The - * parameters will use fwrite() syntax, make sure to follow them. */ - CURLOPT(CURLOPT_HEADERFUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 79), - - /* Set this to force the HTTP request to get back to GET. Only really usable - if POST, PUT or a custom request have been used first. - */ - CURLOPT(CURLOPT_HTTPGET, CURLOPTTYPE_LONG, 80), - - /* Set if we should verify the Common name from the peer certificate in ssl - * handshake, set 1 to check existence, 2 to ensure that it matches the - * provided hostname. */ - CURLOPT(CURLOPT_SSL_VERIFYHOST, CURLOPTTYPE_LONG, 81), - - /* Specify which filename to write all known cookies in after completed - operation. Set filename to "-" (dash) to make it go to stdout. */ - CURLOPT(CURLOPT_COOKIEJAR, CURLOPTTYPE_STRINGPOINT, 82), - - /* Specify which TLS 1.2 (1.1, 1.0) ciphers to use */ - CURLOPT(CURLOPT_SSL_CIPHER_LIST, CURLOPTTYPE_STRINGPOINT, 83), - - /* Specify which HTTP version to use! This must be set to one of the - CURL_HTTP_VERSION* enums set below. */ - CURLOPT(CURLOPT_HTTP_VERSION, CURLOPTTYPE_VALUES, 84), - - /* Specifically switch on or off the FTP engine's use of the EPSV command. By - default, that one will always be attempted before the more traditional - PASV command. */ - CURLOPT(CURLOPT_FTP_USE_EPSV, CURLOPTTYPE_LONG, 85), - - /* type of the file keeping your SSL-certificate ("DER", "PEM", "ENG") */ - CURLOPT(CURLOPT_SSLCERTTYPE, CURLOPTTYPE_STRINGPOINT, 86), - - /* name of the file keeping your private SSL-key */ - CURLOPT(CURLOPT_SSLKEY, CURLOPTTYPE_STRINGPOINT, 87), - - /* type of the file keeping your private SSL-key ("DER", "PEM", "ENG") */ - CURLOPT(CURLOPT_SSLKEYTYPE, CURLOPTTYPE_STRINGPOINT, 88), - - /* crypto engine for the SSL-sub system */ - CURLOPT(CURLOPT_SSLENGINE, CURLOPTTYPE_STRINGPOINT, 89), - - /* set the crypto engine for the SSL-sub system as default - the param has no meaning... - */ - CURLOPT(CURLOPT_SSLENGINE_DEFAULT, CURLOPTTYPE_LONG, 90), - - /* Non-zero value means to use the global dns cache */ - /* DEPRECATED, do not use! */ - CURLOPTDEPRECATED(CURLOPT_DNS_USE_GLOBAL_CACHE, CURLOPTTYPE_LONG, 91, - 7.11.1, "Use CURLOPT_SHARE"), - - /* DNS cache timeout */ - CURLOPT(CURLOPT_DNS_CACHE_TIMEOUT, CURLOPTTYPE_LONG, 92), - - /* send linked-list of pre-transfer QUOTE commands */ - CURLOPT(CURLOPT_PREQUOTE, CURLOPTTYPE_SLISTPOINT, 93), - - /* set the debug function */ - CURLOPT(CURLOPT_DEBUGFUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 94), - - /* set the data for the debug function */ - CURLOPT(CURLOPT_DEBUGDATA, CURLOPTTYPE_CBPOINT, 95), - - /* mark this as start of a cookie session */ - CURLOPT(CURLOPT_COOKIESESSION, CURLOPTTYPE_LONG, 96), - - /* The CApath directory used to validate the peer certificate - this option is used only if SSL_VERIFYPEER is true */ - CURLOPT(CURLOPT_CAPATH, CURLOPTTYPE_STRINGPOINT, 97), - - /* Instruct libcurl to use a smaller receive buffer */ - CURLOPT(CURLOPT_BUFFERSIZE, CURLOPTTYPE_LONG, 98), - - /* Instruct libcurl to not use any signal/alarm handlers, even when using - timeouts. This option is useful for multi-threaded applications. - See libcurl-the-guide for more background information. */ - CURLOPT(CURLOPT_NOSIGNAL, CURLOPTTYPE_LONG, 99), - - /* Provide a CURLShare for mutexing non-ts data */ - CURLOPT(CURLOPT_SHARE, CURLOPTTYPE_OBJECTPOINT, 100), - - /* indicates type of proxy. accepted values are CURLPROXY_HTTP (default), - CURLPROXY_HTTPS, CURLPROXY_SOCKS4, CURLPROXY_SOCKS4A and - CURLPROXY_SOCKS5. */ - CURLOPT(CURLOPT_PROXYTYPE, CURLOPTTYPE_VALUES, 101), - - /* Set the Accept-Encoding string. Use this to tell a server you would like - the response to be compressed. Before 7.21.6, this was known as - CURLOPT_ENCODING */ - CURLOPT(CURLOPT_ACCEPT_ENCODING, CURLOPTTYPE_STRINGPOINT, 102), - - /* Set pointer to private data */ - CURLOPT(CURLOPT_PRIVATE, CURLOPTTYPE_OBJECTPOINT, 103), - - /* Set aliases for HTTP 200 in the HTTP Response header */ - CURLOPT(CURLOPT_HTTP200ALIASES, CURLOPTTYPE_SLISTPOINT, 104), - - /* Continue to send authentication (user+password) when following locations, - even when hostname changed. This can potentially send off the name - and password to whatever host the server decides. */ - CURLOPT(CURLOPT_UNRESTRICTED_AUTH, CURLOPTTYPE_LONG, 105), - - /* Specifically switch on or off the FTP engine's use of the EPRT command ( - it also disables the LPRT attempt). By default, those ones will always be - attempted before the good old traditional PORT command. */ - CURLOPT(CURLOPT_FTP_USE_EPRT, CURLOPTTYPE_LONG, 106), - - /* Set this to a bitmask value to enable the particular authentications - methods you like. Use this in combination with CURLOPT_USERPWD. - Note that setting multiple bits may cause extra network round-trips. */ - CURLOPT(CURLOPT_HTTPAUTH, CURLOPTTYPE_VALUES, 107), - - /* Set the ssl context callback function, currently only for OpenSSL or - wolfSSL ssl_ctx, or mbedTLS mbedtls_ssl_config in the second argument. - The function must match the curl_ssl_ctx_callback prototype. */ - CURLOPT(CURLOPT_SSL_CTX_FUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 108), - - /* Set the userdata for the ssl context callback function's third - argument */ - CURLOPT(CURLOPT_SSL_CTX_DATA, CURLOPTTYPE_CBPOINT, 109), - - /* FTP Option that causes missing dirs to be created on the remote server. - In 7.19.4 we introduced the convenience enums for this option using the - CURLFTP_CREATE_DIR prefix. - */ - CURLOPT(CURLOPT_FTP_CREATE_MISSING_DIRS, CURLOPTTYPE_LONG, 110), - - /* Set this to a bitmask value to enable the particular authentications - methods you like. Use this in combination with CURLOPT_PROXYUSERPWD. - Note that setting multiple bits may cause extra network round-trips. */ - CURLOPT(CURLOPT_PROXYAUTH, CURLOPTTYPE_VALUES, 111), - - /* Option that changes the timeout, in seconds, associated with getting a - response. This is different from transfer timeout time and essentially - places a demand on the server to acknowledge commands in a timely - manner. For FTP, SMTP, IMAP and POP3. */ - CURLOPT(CURLOPT_SERVER_RESPONSE_TIMEOUT, CURLOPTTYPE_LONG, 112), - - /* Set this option to one of the CURL_IPRESOLVE_* defines (see below) to - tell libcurl to use those IP versions only. This only has effect on - systems with support for more than one, i.e IPv4 _and_ IPv6. */ - CURLOPT(CURLOPT_IPRESOLVE, CURLOPTTYPE_VALUES, 113), - - /* Set this option to limit the size of a file that will be downloaded from - an HTTP or FTP server. - - Note there is also _LARGE version which adds large file support for - platforms which have larger off_t sizes. See MAXFILESIZE_LARGE below. */ - CURLOPT(CURLOPT_MAXFILESIZE, CURLOPTTYPE_LONG, 114), - - /* See the comment for INFILESIZE above, but in short, specifies - * the size of the file being uploaded. -1 means unknown. - */ - CURLOPT(CURLOPT_INFILESIZE_LARGE, CURLOPTTYPE_OFF_T, 115), - - /* Sets the continuation offset. There is also a CURLOPTTYPE_LONG version - * of this; look above for RESUME_FROM. - */ - CURLOPT(CURLOPT_RESUME_FROM_LARGE, CURLOPTTYPE_OFF_T, 116), - - /* Sets the maximum size of data that will be downloaded from - * an HTTP or FTP server. See MAXFILESIZE above for the LONG version. - */ - CURLOPT(CURLOPT_MAXFILESIZE_LARGE, CURLOPTTYPE_OFF_T, 117), - - /* Set this option to the filename of your .netrc file you want libcurl - to parse (using the CURLOPT_NETRC option). If not set, libcurl will do - a poor attempt to find the user's home directory and check for a .netrc - file in there. */ - CURLOPT(CURLOPT_NETRC_FILE, CURLOPTTYPE_STRINGPOINT, 118), - - /* Enable SSL/TLS for FTP, pick one of: - CURLUSESSL_TRY - try using SSL, proceed anyway otherwise - CURLUSESSL_CONTROL - SSL for the control connection or fail - CURLUSESSL_ALL - SSL for all communication or fail - */ - CURLOPT(CURLOPT_USE_SSL, CURLOPTTYPE_VALUES, 119), - - /* The _LARGE version of the standard POSTFIELDSIZE option */ - CURLOPT(CURLOPT_POSTFIELDSIZE_LARGE, CURLOPTTYPE_OFF_T, 120), - - /* Enable/disable the TCP Nagle algorithm */ - CURLOPT(CURLOPT_TCP_NODELAY, CURLOPTTYPE_LONG, 121), - - /* 122 OBSOLETE, used in 7.12.3. Gone in 7.13.0 */ - /* 123 OBSOLETE. Gone in 7.16.0 */ - /* 124 OBSOLETE, used in 7.12.3. Gone in 7.13.0 */ - /* 125 OBSOLETE, used in 7.12.3. Gone in 7.13.0 */ - /* 126 OBSOLETE, used in 7.12.3. Gone in 7.13.0 */ - /* 127 OBSOLETE. Gone in 7.16.0 */ - /* 128 OBSOLETE. Gone in 7.16.0 */ - - /* When FTP over SSL/TLS is selected (with CURLOPT_USE_SSL), this option - can be used to change libcurl's default action which is to first try - "AUTH SSL" and then "AUTH TLS" in this order, and proceed when a OK - response has been received. - - Available parameters are: - CURLFTPAUTH_DEFAULT - let libcurl decide - CURLFTPAUTH_SSL - try "AUTH SSL" first, then TLS - CURLFTPAUTH_TLS - try "AUTH TLS" first, then SSL - */ - CURLOPT(CURLOPT_FTPSSLAUTH, CURLOPTTYPE_VALUES, 129), - - CURLOPTDEPRECATED(CURLOPT_IOCTLFUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 130, - 7.18.0, "Use CURLOPT_SEEKFUNCTION"), - CURLOPTDEPRECATED(CURLOPT_IOCTLDATA, CURLOPTTYPE_CBPOINT, 131, - 7.18.0, "Use CURLOPT_SEEKDATA"), - - /* 132 OBSOLETE. Gone in 7.16.0 */ - /* 133 OBSOLETE. Gone in 7.16.0 */ - - /* null-terminated string for pass on to the FTP server when asked for - "account" info */ - CURLOPT(CURLOPT_FTP_ACCOUNT, CURLOPTTYPE_STRINGPOINT, 134), - - /* feed cookie into cookie engine */ - CURLOPT(CURLOPT_COOKIELIST, CURLOPTTYPE_STRINGPOINT, 135), - - /* ignore Content-Length */ - CURLOPT(CURLOPT_IGNORE_CONTENT_LENGTH, CURLOPTTYPE_LONG, 136), - - /* Set to non-zero to skip the IP address received in a 227 PASV FTP server - response. Typically used for FTP-SSL purposes but is not restricted to - that. libcurl will then instead use the same IP address it used for the - control connection. */ - CURLOPT(CURLOPT_FTP_SKIP_PASV_IP, CURLOPTTYPE_LONG, 137), - - /* Select "file method" to use when doing FTP, see the curl_ftpmethod - above. */ - CURLOPT(CURLOPT_FTP_FILEMETHOD, CURLOPTTYPE_VALUES, 138), - - /* Local port number to bind the socket to */ - CURLOPT(CURLOPT_LOCALPORT, CURLOPTTYPE_LONG, 139), - - /* Number of ports to try, including the first one set with LOCALPORT. - Thus, setting it to 1 will make no additional attempts but the first. - */ - CURLOPT(CURLOPT_LOCALPORTRANGE, CURLOPTTYPE_LONG, 140), - - /* no transfer, set up connection and let application use the socket by - extracting it with CURLINFO_LASTSOCKET */ - CURLOPT(CURLOPT_CONNECT_ONLY, CURLOPTTYPE_LONG, 141), - - /* Function that will be called to convert from the - network encoding (instead of using the iconv calls in libcurl) */ - CURLOPTDEPRECATED(CURLOPT_CONV_FROM_NETWORK_FUNCTION, - CURLOPTTYPE_FUNCTIONPOINT, 142, - 7.82.0, "Serves no purpose anymore"), - - /* Function that will be called to convert to the - network encoding (instead of using the iconv calls in libcurl) */ - CURLOPTDEPRECATED(CURLOPT_CONV_TO_NETWORK_FUNCTION, - CURLOPTTYPE_FUNCTIONPOINT, 143, - 7.82.0, "Serves no purpose anymore"), - - /* Function that will be called to convert from UTF8 - (instead of using the iconv calls in libcurl) - Note that this is used only for SSL certificate processing */ - CURLOPTDEPRECATED(CURLOPT_CONV_FROM_UTF8_FUNCTION, - CURLOPTTYPE_FUNCTIONPOINT, 144, - 7.82.0, "Serves no purpose anymore"), - - /* if the connection proceeds too quickly then need to slow it down */ - /* limit-rate: maximum number of bytes per second to send or receive */ - CURLOPT(CURLOPT_MAX_SEND_SPEED_LARGE, CURLOPTTYPE_OFF_T, 145), - CURLOPT(CURLOPT_MAX_RECV_SPEED_LARGE, CURLOPTTYPE_OFF_T, 146), - - /* Pointer to command string to send if USER/PASS fails. */ - CURLOPT(CURLOPT_FTP_ALTERNATIVE_TO_USER, CURLOPTTYPE_STRINGPOINT, 147), - - /* callback function for setting socket options */ - CURLOPT(CURLOPT_SOCKOPTFUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 148), - CURLOPT(CURLOPT_SOCKOPTDATA, CURLOPTTYPE_CBPOINT, 149), - - /* set to 0 to disable session ID reuse for this transfer, default is - enabled (== 1) */ - CURLOPT(CURLOPT_SSL_SESSIONID_CACHE, CURLOPTTYPE_LONG, 150), - - /* allowed SSH authentication methods */ - CURLOPT(CURLOPT_SSH_AUTH_TYPES, CURLOPTTYPE_VALUES, 151), - - /* Used by scp/sftp to do public/private key authentication */ - CURLOPT(CURLOPT_SSH_PUBLIC_KEYFILE, CURLOPTTYPE_STRINGPOINT, 152), - CURLOPT(CURLOPT_SSH_PRIVATE_KEYFILE, CURLOPTTYPE_STRINGPOINT, 153), - - /* Send CCC (Clear Command Channel) after authentication */ - CURLOPT(CURLOPT_FTP_SSL_CCC, CURLOPTTYPE_LONG, 154), - - /* Same as TIMEOUT and CONNECTTIMEOUT, but with ms resolution */ - CURLOPT(CURLOPT_TIMEOUT_MS, CURLOPTTYPE_LONG, 155), - CURLOPT(CURLOPT_CONNECTTIMEOUT_MS, CURLOPTTYPE_LONG, 156), - - /* set to zero to disable the libcurl's decoding and thus pass the raw body - data to the application even when it is encoded/compressed */ - CURLOPT(CURLOPT_HTTP_TRANSFER_DECODING, CURLOPTTYPE_LONG, 157), - CURLOPT(CURLOPT_HTTP_CONTENT_DECODING, CURLOPTTYPE_LONG, 158), - - /* Permission used when creating new files and directories on the remote - server for protocols that support it, SFTP/SCP/FILE */ - CURLOPT(CURLOPT_NEW_FILE_PERMS, CURLOPTTYPE_LONG, 159), - CURLOPT(CURLOPT_NEW_DIRECTORY_PERMS, CURLOPTTYPE_LONG, 160), - - /* Set the behavior of POST when redirecting. Values must be set to one - of CURL_REDIR* defines below. This used to be called CURLOPT_POST301 */ - CURLOPT(CURLOPT_POSTREDIR, CURLOPTTYPE_VALUES, 161), - - /* used by scp/sftp to verify the host's public key */ - CURLOPT(CURLOPT_SSH_HOST_PUBLIC_KEY_MD5, CURLOPTTYPE_STRINGPOINT, 162), - - /* Callback function for opening socket (instead of socket(2)). Optionally, - callback is able change the address or refuse to connect returning - CURL_SOCKET_BAD. The callback should have type - curl_opensocket_callback */ - CURLOPT(CURLOPT_OPENSOCKETFUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 163), - CURLOPT(CURLOPT_OPENSOCKETDATA, CURLOPTTYPE_CBPOINT, 164), - - /* POST volatile input fields. */ - CURLOPT(CURLOPT_COPYPOSTFIELDS, CURLOPTTYPE_OBJECTPOINT, 165), - - /* set transfer mode (;type=) when doing FTP via an HTTP proxy */ - CURLOPT(CURLOPT_PROXY_TRANSFER_MODE, CURLOPTTYPE_LONG, 166), - - /* Callback function for seeking in the input stream */ - CURLOPT(CURLOPT_SEEKFUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 167), - CURLOPT(CURLOPT_SEEKDATA, CURLOPTTYPE_CBPOINT, 168), - - /* CRL file */ - CURLOPT(CURLOPT_CRLFILE, CURLOPTTYPE_STRINGPOINT, 169), - - /* Issuer certificate */ - CURLOPT(CURLOPT_ISSUERCERT, CURLOPTTYPE_STRINGPOINT, 170), - - /* (IPv6) Address scope */ - CURLOPT(CURLOPT_ADDRESS_SCOPE, CURLOPTTYPE_LONG, 171), - - /* Collect certificate chain info and allow it to get retrievable with - CURLINFO_CERTINFO after the transfer is complete. */ - CURLOPT(CURLOPT_CERTINFO, CURLOPTTYPE_LONG, 172), - - /* "name" and "pwd" to use when fetching. */ - CURLOPT(CURLOPT_USERNAME, CURLOPTTYPE_STRINGPOINT, 173), - CURLOPT(CURLOPT_PASSWORD, CURLOPTTYPE_STRINGPOINT, 174), - - /* "name" and "pwd" to use with Proxy when fetching. */ - CURLOPT(CURLOPT_PROXYUSERNAME, CURLOPTTYPE_STRINGPOINT, 175), - CURLOPT(CURLOPT_PROXYPASSWORD, CURLOPTTYPE_STRINGPOINT, 176), - - /* Comma separated list of hostnames defining no-proxy zones. These should - match both hostnames directly, and hostnames within a domain. For - example, local.com will match local.com and www.local.com, but NOT - notlocal.com or www.notlocal.com. For compatibility with other - implementations of this, .local.com will be considered to be the same as - local.com. A single * is the only valid wildcard, and effectively - disables the use of proxy. */ - CURLOPT(CURLOPT_NOPROXY, CURLOPTTYPE_STRINGPOINT, 177), - - /* block size for TFTP transfers */ - CURLOPT(CURLOPT_TFTP_BLKSIZE, CURLOPTTYPE_LONG, 178), - - /* Socks Service */ - /* DEPRECATED, do not use! */ - CURLOPTDEPRECATED(CURLOPT_SOCKS5_GSSAPI_SERVICE, - CURLOPTTYPE_STRINGPOINT, 179, - 7.49.0, "Use CURLOPT_PROXY_SERVICE_NAME"), - - /* Socks Service */ - CURLOPT(CURLOPT_SOCKS5_GSSAPI_NEC, CURLOPTTYPE_LONG, 180), - - /* set the bitmask for the protocols that are allowed to be used for the - transfer, which thus helps the app which takes URLs from users or other - external inputs and want to restrict what protocol(s) to deal - with. Defaults to CURLPROTO_ALL. */ - CURLOPTDEPRECATED(CURLOPT_PROTOCOLS, CURLOPTTYPE_LONG, 181, - 7.85.0, "Use CURLOPT_PROTOCOLS_STR"), - - /* set the bitmask for the protocols that libcurl is allowed to follow to, - as a subset of the CURLOPT_PROTOCOLS ones. That means the protocol needs - to be set in both bitmasks to be allowed to get redirected to. */ - CURLOPTDEPRECATED(CURLOPT_REDIR_PROTOCOLS, CURLOPTTYPE_LONG, 182, - 7.85.0, "Use CURLOPT_REDIR_PROTOCOLS_STR"), - - /* set the SSH knownhost filename to use */ - CURLOPT(CURLOPT_SSH_KNOWNHOSTS, CURLOPTTYPE_STRINGPOINT, 183), - - /* set the SSH host key callback, must point to a curl_sshkeycallback - function */ - CURLOPT(CURLOPT_SSH_KEYFUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 184), - - /* set the SSH host key callback custom pointer */ - CURLOPT(CURLOPT_SSH_KEYDATA, CURLOPTTYPE_CBPOINT, 185), - - /* set the SMTP mail originator */ - CURLOPT(CURLOPT_MAIL_FROM, CURLOPTTYPE_STRINGPOINT, 186), - - /* set the list of SMTP mail receiver(s) */ - CURLOPT(CURLOPT_MAIL_RCPT, CURLOPTTYPE_SLISTPOINT, 187), - - /* FTP: send PRET before PASV */ - CURLOPT(CURLOPT_FTP_USE_PRET, CURLOPTTYPE_LONG, 188), - - /* RTSP request method (OPTIONS, SETUP, PLAY, etc...) */ - CURLOPT(CURLOPT_RTSP_REQUEST, CURLOPTTYPE_VALUES, 189), - - /* The RTSP session identifier */ - CURLOPT(CURLOPT_RTSP_SESSION_ID, CURLOPTTYPE_STRINGPOINT, 190), - - /* The RTSP stream URI */ - CURLOPT(CURLOPT_RTSP_STREAM_URI, CURLOPTTYPE_STRINGPOINT, 191), - - /* The Transport: header to use in RTSP requests */ - CURLOPT(CURLOPT_RTSP_TRANSPORT, CURLOPTTYPE_STRINGPOINT, 192), - - /* Manually initialize the client RTSP CSeq for this handle */ - CURLOPT(CURLOPT_RTSP_CLIENT_CSEQ, CURLOPTTYPE_LONG, 193), - - /* Manually initialize the server RTSP CSeq for this handle */ - CURLOPT(CURLOPT_RTSP_SERVER_CSEQ, CURLOPTTYPE_LONG, 194), - - /* The stream to pass to INTERLEAVEFUNCTION. */ - CURLOPT(CURLOPT_INTERLEAVEDATA, CURLOPTTYPE_CBPOINT, 195), - - /* Let the application define a custom write method for RTP data */ - CURLOPT(CURLOPT_INTERLEAVEFUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 196), - - /* Turn on wildcard matching */ - CURLOPT(CURLOPT_WILDCARDMATCH, CURLOPTTYPE_LONG, 197), - - /* Directory matching callback called before downloading of an - individual file (chunk) started */ - CURLOPT(CURLOPT_CHUNK_BGN_FUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 198), - - /* Directory matching callback called after the file (chunk) - was downloaded, or skipped */ - CURLOPT(CURLOPT_CHUNK_END_FUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 199), - - /* Change match (fnmatch-like) callback for wildcard matching */ - CURLOPT(CURLOPT_FNMATCH_FUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 200), - - /* Let the application define custom chunk data pointer */ - CURLOPT(CURLOPT_CHUNK_DATA, CURLOPTTYPE_CBPOINT, 201), - - /* FNMATCH_FUNCTION user pointer */ - CURLOPT(CURLOPT_FNMATCH_DATA, CURLOPTTYPE_CBPOINT, 202), - - /* send linked-list of name:port:address sets */ - CURLOPT(CURLOPT_RESOLVE, CURLOPTTYPE_SLISTPOINT, 203), - - /* Set a username for authenticated TLS */ - CURLOPT(CURLOPT_TLSAUTH_USERNAME, CURLOPTTYPE_STRINGPOINT, 204), - - /* Set a password for authenticated TLS */ - CURLOPT(CURLOPT_TLSAUTH_PASSWORD, CURLOPTTYPE_STRINGPOINT, 205), - - /* Set authentication type for authenticated TLS */ - CURLOPT(CURLOPT_TLSAUTH_TYPE, CURLOPTTYPE_STRINGPOINT, 206), - - /* Set to 1 to enable the "TE:" header in HTTP requests to ask for - compressed transfer-encoded responses. Set to 0 to disable the use of TE: - in outgoing requests. The current default is 0, but it might change in a - future libcurl release. - - libcurl will ask for the compressed methods it knows of, and if that - is not any, it will not ask for transfer-encoding at all even if this - option is set to 1. */ - CURLOPT(CURLOPT_TRANSFER_ENCODING, CURLOPTTYPE_LONG, 207), - - /* Callback function for closing socket (instead of close(2)). The callback - should have type curl_closesocket_callback */ - CURLOPT(CURLOPT_CLOSESOCKETFUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 208), - CURLOPT(CURLOPT_CLOSESOCKETDATA, CURLOPTTYPE_CBPOINT, 209), - - /* allow GSSAPI credential delegation */ - CURLOPT(CURLOPT_GSSAPI_DELEGATION, CURLOPTTYPE_VALUES, 210), - - /* Set the name servers to use for DNS resolution. - * Only supported by the c-ares DNS backend */ - CURLOPT(CURLOPT_DNS_SERVERS, CURLOPTTYPE_STRINGPOINT, 211), - - /* Time-out accept operations (currently for FTP only) after this amount - of milliseconds. */ - CURLOPT(CURLOPT_ACCEPTTIMEOUT_MS, CURLOPTTYPE_LONG, 212), - - /* Set TCP keepalive */ - CURLOPT(CURLOPT_TCP_KEEPALIVE, CURLOPTTYPE_LONG, 213), - - /* non-universal keepalive knobs (Linux, AIX, HP-UX, more) */ - CURLOPT(CURLOPT_TCP_KEEPIDLE, CURLOPTTYPE_LONG, 214), - CURLOPT(CURLOPT_TCP_KEEPINTVL, CURLOPTTYPE_LONG, 215), - - /* Enable/disable specific SSL features with a bitmask, see CURLSSLOPT_* */ - CURLOPT(CURLOPT_SSL_OPTIONS, CURLOPTTYPE_VALUES, 216), - - /* Set the SMTP auth originator */ - CURLOPT(CURLOPT_MAIL_AUTH, CURLOPTTYPE_STRINGPOINT, 217), - - /* Enable/disable SASL initial response */ - CURLOPT(CURLOPT_SASL_IR, CURLOPTTYPE_LONG, 218), - - /* Function that will be called instead of the internal progress display - * function. This function should be defined as the curl_xferinfo_callback - * prototype defines. (Deprecates CURLOPT_PROGRESSFUNCTION) */ - CURLOPT(CURLOPT_XFERINFOFUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 219), - - /* The XOAUTH2 bearer token */ - CURLOPT(CURLOPT_XOAUTH2_BEARER, CURLOPTTYPE_STRINGPOINT, 220), - - /* Set the interface string to use as outgoing network - * interface for DNS requests. - * Only supported by the c-ares DNS backend */ - CURLOPT(CURLOPT_DNS_INTERFACE, CURLOPTTYPE_STRINGPOINT, 221), - - /* Set the local IPv4 address to use for outgoing DNS requests. - * Only supported by the c-ares DNS backend */ - CURLOPT(CURLOPT_DNS_LOCAL_IP4, CURLOPTTYPE_STRINGPOINT, 222), - - /* Set the local IPv6 address to use for outgoing DNS requests. - * Only supported by the c-ares DNS backend */ - CURLOPT(CURLOPT_DNS_LOCAL_IP6, CURLOPTTYPE_STRINGPOINT, 223), - - /* Set authentication options directly */ - CURLOPT(CURLOPT_LOGIN_OPTIONS, CURLOPTTYPE_STRINGPOINT, 224), - - /* Enable/disable TLS NPN extension (http2 over ssl might fail without) */ - CURLOPTDEPRECATED(CURLOPT_SSL_ENABLE_NPN, CURLOPTTYPE_LONG, 225, - 7.86.0, "Has no function"), - - /* Enable/disable TLS ALPN extension (http2 over ssl might fail without) */ - CURLOPT(CURLOPT_SSL_ENABLE_ALPN, CURLOPTTYPE_LONG, 226), - - /* Time to wait for a response to an HTTP request containing an - * Expect: 100-continue header before sending the data anyway. */ - CURLOPT(CURLOPT_EXPECT_100_TIMEOUT_MS, CURLOPTTYPE_LONG, 227), - - /* This points to a linked list of headers used for proxy requests only, - struct curl_slist kind */ - CURLOPT(CURLOPT_PROXYHEADER, CURLOPTTYPE_SLISTPOINT, 228), - - /* Pass in a bitmask of "header options" */ - CURLOPT(CURLOPT_HEADEROPT, CURLOPTTYPE_VALUES, 229), - - /* The public key used to validate the peer public key */ - CURLOPT(CURLOPT_PINNEDPUBLICKEY, CURLOPTTYPE_STRINGPOINT, 230), - - /* Path to Unix domain socket */ - CURLOPT(CURLOPT_UNIX_SOCKET_PATH, CURLOPTTYPE_STRINGPOINT, 231), - - /* Set if we should verify the certificate status. */ - CURLOPT(CURLOPT_SSL_VERIFYSTATUS, CURLOPTTYPE_LONG, 232), - - /* Set if we should enable TLS false start. */ - CURLOPTDEPRECATED(CURLOPT_SSL_FALSESTART, CURLOPTTYPE_LONG, 233, - 8.15.0, "Has no function"), - - /* Do not squash dot-dot sequences */ - CURLOPT(CURLOPT_PATH_AS_IS, CURLOPTTYPE_LONG, 234), - - /* Proxy Service Name */ - CURLOPT(CURLOPT_PROXY_SERVICE_NAME, CURLOPTTYPE_STRINGPOINT, 235), - - /* Service Name */ - CURLOPT(CURLOPT_SERVICE_NAME, CURLOPTTYPE_STRINGPOINT, 236), - - /* Wait/do not wait for pipe/mutex to clarify */ - CURLOPT(CURLOPT_PIPEWAIT, CURLOPTTYPE_LONG, 237), - - /* Set the protocol used when curl is given a URL without a protocol */ - CURLOPT(CURLOPT_DEFAULT_PROTOCOL, CURLOPTTYPE_STRINGPOINT, 238), - - /* Set stream weight, 1 - 256 (default is 16) */ - CURLOPT(CURLOPT_STREAM_WEIGHT, CURLOPTTYPE_LONG, 239), - - /* Set stream dependency on another curl handle */ - CURLOPT(CURLOPT_STREAM_DEPENDS, CURLOPTTYPE_OBJECTPOINT, 240), - - /* Set E-xclusive stream dependency on another curl handle */ - CURLOPT(CURLOPT_STREAM_DEPENDS_E, CURLOPTTYPE_OBJECTPOINT, 241), - - /* Do not send any tftp option requests to the server */ - CURLOPT(CURLOPT_TFTP_NO_OPTIONS, CURLOPTTYPE_LONG, 242), - - /* Linked-list of host:port:connect-to-host:connect-to-port, - overrides the URL's host:port (only for the network layer) */ - CURLOPT(CURLOPT_CONNECT_TO, CURLOPTTYPE_SLISTPOINT, 243), - - /* Set TCP Fast Open */ - CURLOPT(CURLOPT_TCP_FASTOPEN, CURLOPTTYPE_LONG, 244), - - /* Continue to send data if the server responds early with an - * HTTP status code >= 300 */ - CURLOPT(CURLOPT_KEEP_SENDING_ON_ERROR, CURLOPTTYPE_LONG, 245), - - /* The CApath or CAfile used to validate the proxy certificate - this option is used only if PROXY_SSL_VERIFYPEER is true */ - CURLOPT(CURLOPT_PROXY_CAINFO, CURLOPTTYPE_STRINGPOINT, 246), - - /* The CApath directory used to validate the proxy certificate - this option is used only if PROXY_SSL_VERIFYPEER is true */ - CURLOPT(CURLOPT_PROXY_CAPATH, CURLOPTTYPE_STRINGPOINT, 247), - - /* Set if we should verify the proxy in ssl handshake, - set 1 to verify. */ - CURLOPT(CURLOPT_PROXY_SSL_VERIFYPEER, CURLOPTTYPE_LONG, 248), - - /* Set if we should verify the Common name from the proxy certificate in ssl - * handshake, set 1 to check existence, 2 to ensure that it matches - * the provided hostname. */ - CURLOPT(CURLOPT_PROXY_SSL_VERIFYHOST, CURLOPTTYPE_LONG, 249), - - /* What version to specifically try to use for proxy. - See CURL_SSLVERSION defines below. */ - CURLOPT(CURLOPT_PROXY_SSLVERSION, CURLOPTTYPE_VALUES, 250), - - /* Set a username for authenticated TLS for proxy */ - CURLOPT(CURLOPT_PROXY_TLSAUTH_USERNAME, CURLOPTTYPE_STRINGPOINT, 251), - - /* Set a password for authenticated TLS for proxy */ - CURLOPT(CURLOPT_PROXY_TLSAUTH_PASSWORD, CURLOPTTYPE_STRINGPOINT, 252), - - /* Set authentication type for authenticated TLS for proxy */ - CURLOPT(CURLOPT_PROXY_TLSAUTH_TYPE, CURLOPTTYPE_STRINGPOINT, 253), - - /* name of the file keeping your private SSL-certificate for proxy */ - CURLOPT(CURLOPT_PROXY_SSLCERT, CURLOPTTYPE_STRINGPOINT, 254), - - /* type of the file keeping your SSL-certificate ("DER", "PEM", "ENG") for - proxy */ - CURLOPT(CURLOPT_PROXY_SSLCERTTYPE, CURLOPTTYPE_STRINGPOINT, 255), - - /* name of the file keeping your private SSL-key for proxy */ - CURLOPT(CURLOPT_PROXY_SSLKEY, CURLOPTTYPE_STRINGPOINT, 256), - - /* type of the file keeping your private SSL-key ("DER", "PEM", "ENG") for - proxy */ - CURLOPT(CURLOPT_PROXY_SSLKEYTYPE, CURLOPTTYPE_STRINGPOINT, 257), - - /* password for the SSL private key for proxy */ - CURLOPT(CURLOPT_PROXY_KEYPASSWD, CURLOPTTYPE_STRINGPOINT, 258), - - /* Specify which TLS 1.2 (1.1, 1.0) ciphers to use for proxy */ - CURLOPT(CURLOPT_PROXY_SSL_CIPHER_LIST, CURLOPTTYPE_STRINGPOINT, 259), - - /* CRL file for proxy */ - CURLOPT(CURLOPT_PROXY_CRLFILE, CURLOPTTYPE_STRINGPOINT, 260), - - /* Enable/disable specific SSL features with a bitmask for proxy, see - CURLSSLOPT_* */ - CURLOPT(CURLOPT_PROXY_SSL_OPTIONS, CURLOPTTYPE_LONG, 261), - - /* Name of pre proxy to use. */ - CURLOPT(CURLOPT_PRE_PROXY, CURLOPTTYPE_STRINGPOINT, 262), - - /* The public key in DER form used to validate the proxy public key - this option is used only if PROXY_SSL_VERIFYPEER is true */ - CURLOPT(CURLOPT_PROXY_PINNEDPUBLICKEY, CURLOPTTYPE_STRINGPOINT, 263), - - /* Path to an abstract Unix domain socket */ - CURLOPT(CURLOPT_ABSTRACT_UNIX_SOCKET, CURLOPTTYPE_STRINGPOINT, 264), - - /* Suppress proxy CONNECT response headers from user callbacks */ - CURLOPT(CURLOPT_SUPPRESS_CONNECT_HEADERS, CURLOPTTYPE_LONG, 265), - - /* The request target, instead of extracted from the URL */ - CURLOPT(CURLOPT_REQUEST_TARGET, CURLOPTTYPE_STRINGPOINT, 266), - - /* bitmask of allowed auth methods for connections to SOCKS5 proxies */ - CURLOPT(CURLOPT_SOCKS5_AUTH, CURLOPTTYPE_LONG, 267), - - /* Enable/disable SSH compression */ - CURLOPT(CURLOPT_SSH_COMPRESSION, CURLOPTTYPE_LONG, 268), - - /* Post MIME data. */ - CURLOPT(CURLOPT_MIMEPOST, CURLOPTTYPE_OBJECTPOINT, 269), - - /* Time to use with the CURLOPT_TIMECONDITION. Specified in number of - seconds since 1 Jan 1970. */ - CURLOPT(CURLOPT_TIMEVALUE_LARGE, CURLOPTTYPE_OFF_T, 270), - - /* Head start in milliseconds to give happy eyeballs. */ - CURLOPT(CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS, CURLOPTTYPE_LONG, 271), - - /* Function that will be called before a resolver request is made */ - CURLOPT(CURLOPT_RESOLVER_START_FUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 272), - - /* User data to pass to the resolver start callback. */ - CURLOPT(CURLOPT_RESOLVER_START_DATA, CURLOPTTYPE_CBPOINT, 273), - - /* send HAProxy PROXY protocol header? */ - CURLOPT(CURLOPT_HAPROXYPROTOCOL, CURLOPTTYPE_LONG, 274), - - /* shuffle addresses before use when DNS returns multiple */ - CURLOPT(CURLOPT_DNS_SHUFFLE_ADDRESSES, CURLOPTTYPE_LONG, 275), - - /* Specify which TLS 1.3 ciphers suites to use */ - CURLOPT(CURLOPT_TLS13_CIPHERS, CURLOPTTYPE_STRINGPOINT, 276), - CURLOPT(CURLOPT_PROXY_TLS13_CIPHERS, CURLOPTTYPE_STRINGPOINT, 277), - - /* Disallow specifying username/login in URL. */ - CURLOPT(CURLOPT_DISALLOW_USERNAME_IN_URL, CURLOPTTYPE_LONG, 278), - - /* DNS-over-HTTPS URL */ - CURLOPT(CURLOPT_DOH_URL, CURLOPTTYPE_STRINGPOINT, 279), - - /* Preferred buffer size to use for uploads */ - CURLOPT(CURLOPT_UPLOAD_BUFFERSIZE, CURLOPTTYPE_LONG, 280), - - /* Time in ms between connection upkeep calls for long-lived connections. */ - CURLOPT(CURLOPT_UPKEEP_INTERVAL_MS, CURLOPTTYPE_LONG, 281), - - /* Specify URL using CURL URL API. */ - CURLOPT(CURLOPT_CURLU, CURLOPTTYPE_OBJECTPOINT, 282), - - /* add trailing data just after no more data is available */ - CURLOPT(CURLOPT_TRAILERFUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 283), - - /* pointer to be passed to HTTP_TRAILER_FUNCTION */ - CURLOPT(CURLOPT_TRAILERDATA, CURLOPTTYPE_CBPOINT, 284), - - /* set this to 1L to allow HTTP/0.9 responses or 0L to disallow */ - CURLOPT(CURLOPT_HTTP09_ALLOWED, CURLOPTTYPE_LONG, 285), - - /* alt-svc control bitmask */ - CURLOPT(CURLOPT_ALTSVC_CTRL, CURLOPTTYPE_LONG, 286), - - /* alt-svc cache filename to possibly read from/write to */ - CURLOPT(CURLOPT_ALTSVC, CURLOPTTYPE_STRINGPOINT, 287), - - /* maximum age (idle time) of a connection to consider it for reuse - * (in seconds) */ - CURLOPT(CURLOPT_MAXAGE_CONN, CURLOPTTYPE_LONG, 288), - - /* SASL authorization identity */ - CURLOPT(CURLOPT_SASL_AUTHZID, CURLOPTTYPE_STRINGPOINT, 289), - - /* allow RCPT TO command to fail for some recipients */ - CURLOPT(CURLOPT_MAIL_RCPT_ALLOWFAILS, CURLOPTTYPE_LONG, 290), - - /* the private SSL-certificate as a "blob" */ - CURLOPT(CURLOPT_SSLCERT_BLOB, CURLOPTTYPE_BLOB, 291), - CURLOPT(CURLOPT_SSLKEY_BLOB, CURLOPTTYPE_BLOB, 292), - CURLOPT(CURLOPT_PROXY_SSLCERT_BLOB, CURLOPTTYPE_BLOB, 293), - CURLOPT(CURLOPT_PROXY_SSLKEY_BLOB, CURLOPTTYPE_BLOB, 294), - CURLOPT(CURLOPT_ISSUERCERT_BLOB, CURLOPTTYPE_BLOB, 295), - - /* Issuer certificate for proxy */ - CURLOPT(CURLOPT_PROXY_ISSUERCERT, CURLOPTTYPE_STRINGPOINT, 296), - CURLOPT(CURLOPT_PROXY_ISSUERCERT_BLOB, CURLOPTTYPE_BLOB, 297), - - /* the EC curves requested by the TLS client (RFC 8422, 5.1); - * OpenSSL support via 'set_groups'/'set_curves': - * https://docs.openssl.org/master/man3/SSL_CTX_set1_curves/ - */ - CURLOPT(CURLOPT_SSL_EC_CURVES, CURLOPTTYPE_STRINGPOINT, 298), - - /* HSTS bitmask */ - CURLOPT(CURLOPT_HSTS_CTRL, CURLOPTTYPE_LONG, 299), - /* HSTS filename */ - CURLOPT(CURLOPT_HSTS, CURLOPTTYPE_STRINGPOINT, 300), - - /* HSTS read callback */ - CURLOPT(CURLOPT_HSTSREADFUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 301), - CURLOPT(CURLOPT_HSTSREADDATA, CURLOPTTYPE_CBPOINT, 302), - - /* HSTS write callback */ - CURLOPT(CURLOPT_HSTSWRITEFUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 303), - CURLOPT(CURLOPT_HSTSWRITEDATA, CURLOPTTYPE_CBPOINT, 304), - - /* Parameters for V4 signature */ - CURLOPT(CURLOPT_AWS_SIGV4, CURLOPTTYPE_STRINGPOINT, 305), - - /* Same as CURLOPT_SSL_VERIFYPEER but for DoH (DNS-over-HTTPS) servers. */ - CURLOPT(CURLOPT_DOH_SSL_VERIFYPEER, CURLOPTTYPE_LONG, 306), - - /* Same as CURLOPT_SSL_VERIFYHOST but for DoH (DNS-over-HTTPS) servers. */ - CURLOPT(CURLOPT_DOH_SSL_VERIFYHOST, CURLOPTTYPE_LONG, 307), - - /* Same as CURLOPT_SSL_VERIFYSTATUS but for DoH (DNS-over-HTTPS) servers. */ - CURLOPT(CURLOPT_DOH_SSL_VERIFYSTATUS, CURLOPTTYPE_LONG, 308), - - /* The CA certificates as "blob" used to validate the peer certificate - this option is used only if SSL_VERIFYPEER is true */ - CURLOPT(CURLOPT_CAINFO_BLOB, CURLOPTTYPE_BLOB, 309), - - /* The CA certificates as "blob" used to validate the proxy certificate - this option is used only if PROXY_SSL_VERIFYPEER is true */ - CURLOPT(CURLOPT_PROXY_CAINFO_BLOB, CURLOPTTYPE_BLOB, 310), - - /* used by scp/sftp to verify the host's public key */ - CURLOPT(CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256, CURLOPTTYPE_STRINGPOINT, 311), - - /* Function that will be called immediately before the initial request - is made on a connection (after any protocol negotiation step). */ - CURLOPT(CURLOPT_PREREQFUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 312), - - /* Data passed to the CURLOPT_PREREQFUNCTION callback */ - CURLOPT(CURLOPT_PREREQDATA, CURLOPTTYPE_CBPOINT, 313), - - /* maximum age (since creation) of a connection to consider it for reuse - * (in seconds) */ - CURLOPT(CURLOPT_MAXLIFETIME_CONN, CURLOPTTYPE_LONG, 314), - - /* Set MIME option flags. */ - CURLOPT(CURLOPT_MIME_OPTIONS, CURLOPTTYPE_LONG, 315), - - /* set the SSH host key callback, must point to a curl_sshkeycallback - function */ - CURLOPT(CURLOPT_SSH_HOSTKEYFUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 316), - - /* set the SSH host key callback custom pointer */ - CURLOPT(CURLOPT_SSH_HOSTKEYDATA, CURLOPTTYPE_CBPOINT, 317), - - /* specify which protocols that are allowed to be used for the transfer, - which thus helps the app which takes URLs from users or other external - inputs and want to restrict what protocol(s) to deal with. Defaults to - all built-in protocols. */ - CURLOPT(CURLOPT_PROTOCOLS_STR, CURLOPTTYPE_STRINGPOINT, 318), - - /* specify which protocols that libcurl is allowed to follow directs to */ - CURLOPT(CURLOPT_REDIR_PROTOCOLS_STR, CURLOPTTYPE_STRINGPOINT, 319), - - /* WebSockets options */ - CURLOPT(CURLOPT_WS_OPTIONS, CURLOPTTYPE_LONG, 320), - - /* CA cache timeout */ - CURLOPT(CURLOPT_CA_CACHE_TIMEOUT, CURLOPTTYPE_LONG, 321), - - /* Can leak things, gonna exit() soon */ - CURLOPT(CURLOPT_QUICK_EXIT, CURLOPTTYPE_LONG, 322), - - /* set a specific client IP for HAProxy PROXY protocol header? */ - CURLOPT(CURLOPT_HAPROXY_CLIENT_IP, CURLOPTTYPE_STRINGPOINT, 323), - - /* millisecond version */ - CURLOPT(CURLOPT_SERVER_RESPONSE_TIMEOUT_MS, CURLOPTTYPE_LONG, 324), - - /* set ECH configuration */ - CURLOPT(CURLOPT_ECH, CURLOPTTYPE_STRINGPOINT, 325), - - /* maximum number of keepalive probes (Linux, *BSD, macOS, etc.) */ - CURLOPT(CURLOPT_TCP_KEEPCNT, CURLOPTTYPE_LONG, 326), - - CURLOPT(CURLOPT_UPLOAD_FLAGS, CURLOPTTYPE_LONG, 327), - - /* set TLS supported signature algorithms */ - CURLOPT(CURLOPT_SSL_SIGNATURE_ALGORITHMS, CURLOPTTYPE_STRINGPOINT, 328), - - CURLOPT_LASTENTRY /* the last unused */ -} CURLoption; - -#ifndef CURL_NO_OLDIES /* define this to test if your app builds with all - the obsolete stuff removed! */ - -/* Backwards compatibility with older names */ -/* These are scheduled to disappear by 2011 */ - -/* This was added in version 7.19.1 */ -#define CURLOPT_POST301 CURLOPT_POSTREDIR - -/* These are scheduled to disappear by 2009 */ - -/* The following were added in 7.17.0 */ -#define CURLOPT_SSLKEYPASSWD CURLOPT_KEYPASSWD -#define CURLOPT_FTPAPPEND CURLOPT_APPEND -#define CURLOPT_FTPLISTONLY CURLOPT_DIRLISTONLY -#define CURLOPT_FTP_SSL CURLOPT_USE_SSL - -/* The following were added earlier */ - -#define CURLOPT_SSLCERTPASSWD CURLOPT_KEYPASSWD -#define CURLOPT_KRB4LEVEL CURLOPT_KRBLEVEL - -/* */ -#define CURLOPT_FTP_RESPONSE_TIMEOUT CURLOPT_SERVER_RESPONSE_TIMEOUT - -/* Added in 8.2.0 */ -#define CURLOPT_MAIL_RCPT_ALLLOWFAILS CURLOPT_MAIL_RCPT_ALLOWFAILS - -#else -/* This is set if CURL_NO_OLDIES is defined at compile-time */ -#undef CURLOPT_DNS_USE_GLOBAL_CACHE /* soon obsolete */ -#endif - -/* Below here follows defines for the CURLOPT_IPRESOLVE option. If a host - name resolves addresses using more than one IP protocol version, this - option might be handy to force libcurl to use a specific IP version. */ -#define CURL_IPRESOLVE_WHATEVER 0L /* default, uses addresses to all IP - versions that your system allows */ -#define CURL_IPRESOLVE_V4 1L /* uses only IPv4 addresses/connections */ -#define CURL_IPRESOLVE_V6 2L /* uses only IPv6 addresses/connections */ - - /* Convenient "aliases" */ -#define CURLOPT_RTSPHEADER CURLOPT_HTTPHEADER - -/* These constants are for use with the CURLOPT_HTTP_VERSION option. */ -#define CURL_HTTP_VERSION_NONE 0L /* setting this means we do not care, and - that we would like the library to choose - the best possible for us! */ -#define CURL_HTTP_VERSION_1_0 1L /* please use HTTP 1.0 in the request */ -#define CURL_HTTP_VERSION_1_1 2L /* please use HTTP 1.1 in the request */ -#define CURL_HTTP_VERSION_2_0 3L /* please use HTTP 2 in the request */ -#define CURL_HTTP_VERSION_2TLS 4L /* use version 2 for HTTPS, version 1.1 for - HTTP */ -#define CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE 5L /* please use HTTP 2 without - HTTP/1.1 Upgrade */ -#define CURL_HTTP_VERSION_3 30L /* Use HTTP/3, fallback to HTTP/2 or - HTTP/1 if needed. For HTTPS only. For - HTTP, this option makes libcurl - return error. */ -#define CURL_HTTP_VERSION_3ONLY 31L /* Use HTTP/3 without fallback. For - HTTPS only. For HTTP, this makes - libcurl return error. */ -#define CURL_HTTP_VERSION_LAST 32L /* *ILLEGAL* http version */ - -/* Convenience definition simple because the name of the version is HTTP/2 and - not 2.0. The 2_0 version of the enum name was set while the version was - still planned to be 2.0 and we stick to it for compatibility. */ -#define CURL_HTTP_VERSION_2 CURL_HTTP_VERSION_2_0 - -/* - * Public API enums for RTSP requests - */ - -#define CURL_RTSPREQ_NONE 0L -#define CURL_RTSPREQ_OPTIONS 1L -#define CURL_RTSPREQ_DESCRIBE 2L -#define CURL_RTSPREQ_ANNOUNCE 3L -#define CURL_RTSPREQ_SETUP 4L -#define CURL_RTSPREQ_PLAY 5L -#define CURL_RTSPREQ_PAUSE 6L -#define CURL_RTSPREQ_TEARDOWN 7L -#define CURL_RTSPREQ_GET_PARAMETER 8L -#define CURL_RTSPREQ_SET_PARAMETER 9L -#define CURL_RTSPREQ_RECORD 10L -#define CURL_RTSPREQ_RECEIVE 11L -#define CURL_RTSPREQ_LAST 12L /* not used */ - - /* These enums are for use with the CURLOPT_NETRC option. */ -#define CURL_NETRC_IGNORED 0L /* The .netrc will never be read. - This is the default. */ -#define CURL_NETRC_OPTIONAL 1L /* A user:password in the URL will be preferred - to one in the .netrc. */ -#define CURL_NETRC_REQUIRED 2L /* A user:password in the URL will be ignored. - Unless one is set programmatically, the - .netrc will be queried. */ -enum CURL_NETRC_OPTION { - /* we set a single member here, just to make sure we still provide the enum, - but the values to use are defined above with L suffixes */ - CURL_NETRC_LAST = 3 -}; - -#define CURL_SSLVERSION_DEFAULT 0L -#define CURL_SSLVERSION_TLSv1 1L /* TLS 1.x */ -#define CURL_SSLVERSION_SSLv2 2L -#define CURL_SSLVERSION_SSLv3 3L -#define CURL_SSLVERSION_TLSv1_0 4L -#define CURL_SSLVERSION_TLSv1_1 5L -#define CURL_SSLVERSION_TLSv1_2 6L -#define CURL_SSLVERSION_TLSv1_3 7L - -#define CURL_SSLVERSION_LAST 8L /* never use, keep last */ - -#define CURL_SSLVERSION_MAX_NONE 0L -#define CURL_SSLVERSION_MAX_DEFAULT (CURL_SSLVERSION_TLSv1 << 16) -#define CURL_SSLVERSION_MAX_TLSv1_0 (CURL_SSLVERSION_TLSv1_0 << 16) -#define CURL_SSLVERSION_MAX_TLSv1_1 (CURL_SSLVERSION_TLSv1_1 << 16) -#define CURL_SSLVERSION_MAX_TLSv1_2 (CURL_SSLVERSION_TLSv1_2 << 16) -#define CURL_SSLVERSION_MAX_TLSv1_3 (CURL_SSLVERSION_TLSv1_3 << 16) - -/* never use, keep last */ -#define CURL_SSLVERSION_MAX_LAST (CURL_SSLVERSION_LAST << 16) - -#define CURL_TLSAUTH_NONE 0L -#define CURL_TLSAUTH_SRP 1L - -enum CURL_TLSAUTH { - /* we set a single member here, just to make sure we still provide the enum, - but the values to use are defined above with L suffixes */ - CURL_TLSAUTH_LAST = 2 -}; - -/* symbols to use with CURLOPT_POSTREDIR. - CURL_REDIR_POST_301, CURL_REDIR_POST_302 and CURL_REDIR_POST_303 - can be bitwise ORed so that CURL_REDIR_POST_301 | CURL_REDIR_POST_302 - | CURL_REDIR_POST_303 == CURL_REDIR_POST_ALL */ - -#define CURL_REDIR_GET_ALL 0L -#define CURL_REDIR_POST_301 1L -#define CURL_REDIR_POST_302 2L -#define CURL_REDIR_POST_303 4L -#define CURL_REDIR_POST_ALL \ - (CURL_REDIR_POST_301 | CURL_REDIR_POST_302 | CURL_REDIR_POST_303) - -#define CURL_TIMECOND_NONE 0L -#define CURL_TIMECOND_IFMODSINCE 1L -#define CURL_TIMECOND_IFUNMODSINCE 2L -#define CURL_TIMECOND_LASTMOD 3L - -typedef enum { - /* we set a single member here, just to make sure we still provide - the enum typedef, but the values to use are defined above with L - suffixes */ - CURL_TIMECOND_LAST = 4 -} curl_TimeCond; - -/* Special size_t value signaling a null-terminated string. */ -#define CURL_ZERO_TERMINATED ((size_t)-1) - -/* curl_strequal() and curl_strnequal() are subject for removal in a future - release */ -CURL_EXTERN int curl_strequal(const char *s1, const char *s2); -CURL_EXTERN int curl_strnequal(const char *s1, const char *s2, size_t n); - -/* Mime/form handling support. */ -typedef struct curl_mime curl_mime; /* Mime context. */ -typedef struct curl_mimepart curl_mimepart; /* Mime part context. */ - -/* CURLMIMEOPT_ defines are for the CURLOPT_MIME_OPTIONS option. */ -#define CURLMIMEOPT_FORMESCAPE (1L << 0) /* Use backslash-escaping for forms */ - -/* - * NAME curl_mime_init() - * - * DESCRIPTION - * - * Create a mime context and return its handle. The easy parameter is the - * target handle. - */ -CURL_EXTERN curl_mime *curl_mime_init(CURL *easy); - -/* - * NAME curl_mime_free() - * - * DESCRIPTION - * - * release a mime handle and its substructures. - */ -CURL_EXTERN void curl_mime_free(curl_mime *mime); - -/* - * NAME curl_mime_addpart() - * - * DESCRIPTION - * - * Append a new empty part to the given mime context and return a handle to - * the created part. - */ -CURL_EXTERN curl_mimepart *curl_mime_addpart(curl_mime *mime); - -/* - * NAME curl_mime_name() - * - * DESCRIPTION - * - * Set mime/form part name. - */ -CURL_EXTERN CURLcode curl_mime_name(curl_mimepart *part, const char *name); - -/* - * NAME curl_mime_filename() - * - * DESCRIPTION - * - * Set mime part remote filename. - */ -CURL_EXTERN CURLcode curl_mime_filename(curl_mimepart *part, - const char *filename); - -/* - * NAME curl_mime_type() - * - * DESCRIPTION - * - * Set mime part type. - */ -CURL_EXTERN CURLcode curl_mime_type(curl_mimepart *part, const char *mimetype); - -/* - * NAME curl_mime_encoder() - * - * DESCRIPTION - * - * Set mime data transfer encoder. - */ -CURL_EXTERN CURLcode curl_mime_encoder(curl_mimepart *part, - const char *encoding); - -/* - * NAME curl_mime_data() - * - * DESCRIPTION - * - * Set mime part data source from memory data, - */ -CURL_EXTERN CURLcode curl_mime_data(curl_mimepart *part, - const char *data, size_t datasize); - -/* - * NAME curl_mime_filedata() - * - * DESCRIPTION - * - * Set mime part data source from named file. - */ -CURL_EXTERN CURLcode curl_mime_filedata(curl_mimepart *part, - const char *filename); - -/* - * NAME curl_mime_data_cb() - * - * DESCRIPTION - * - * Set mime part data source from callback function. - */ -CURL_EXTERN CURLcode curl_mime_data_cb(curl_mimepart *part, - curl_off_t datasize, - curl_read_callback readfunc, - curl_seek_callback seekfunc, - curl_free_callback freefunc, - void *arg); - -/* - * NAME curl_mime_subparts() - * - * DESCRIPTION - * - * Set mime part data source from subparts. - */ -CURL_EXTERN CURLcode curl_mime_subparts(curl_mimepart *part, - curl_mime *subparts); -/* - * NAME curl_mime_headers() - * - * DESCRIPTION - * - * Set mime part headers. - */ -CURL_EXTERN CURLcode curl_mime_headers(curl_mimepart *part, - struct curl_slist *headers, - int take_ownership); - -typedef enum { - /********* the first one is unused ************/ - CURLFORM_NOTHING CURL_DEPRECATED(7.56.0, ""), - CURLFORM_COPYNAME CURL_DEPRECATED(7.56.0, "Use curl_mime_name()"), - CURLFORM_PTRNAME CURL_DEPRECATED(7.56.0, "Use curl_mime_name()"), - CURLFORM_NAMELENGTH CURL_DEPRECATED(7.56.0, ""), - CURLFORM_COPYCONTENTS CURL_DEPRECATED(7.56.0, "Use curl_mime_data()"), - CURLFORM_PTRCONTENTS CURL_DEPRECATED(7.56.0, "Use curl_mime_data()"), - CURLFORM_CONTENTSLENGTH CURL_DEPRECATED(7.56.0, "Use curl_mime_data()"), - CURLFORM_FILECONTENT CURL_DEPRECATED(7.56.0, "Use curl_mime_data_cb()"), - CURLFORM_ARRAY CURL_DEPRECATED(7.56.0, ""), - CURLFORM_OBSOLETE, - CURLFORM_FILE CURL_DEPRECATED(7.56.0, "Use curl_mime_filedata()"), - - CURLFORM_BUFFER CURL_DEPRECATED(7.56.0, "Use curl_mime_filename()"), - CURLFORM_BUFFERPTR CURL_DEPRECATED(7.56.0, "Use curl_mime_data()"), - CURLFORM_BUFFERLENGTH CURL_DEPRECATED(7.56.0, "Use curl_mime_data()"), - - CURLFORM_CONTENTTYPE CURL_DEPRECATED(7.56.0, "Use curl_mime_type()"), - CURLFORM_CONTENTHEADER CURL_DEPRECATED(7.56.0, "Use curl_mime_headers()"), - CURLFORM_FILENAME CURL_DEPRECATED(7.56.0, "Use curl_mime_filename()"), - CURLFORM_END, - CURLFORM_OBSOLETE2, - - CURLFORM_STREAM CURL_DEPRECATED(7.56.0, "Use curl_mime_data_cb()"), - CURLFORM_CONTENTLEN /* added in 7.46.0, provide a curl_off_t length */ - CURL_DEPRECATED(7.56.0, "Use curl_mime_data()"), - - CURLFORM_LASTENTRY /* the last unused */ -} CURLformoption; - -/* structure to be used as parameter for CURLFORM_ARRAY */ -struct curl_forms { - CURLformoption option; - const char *value; -}; - -/* use this for multipart formpost building */ -/* Returns code for curl_formadd() - * - * Returns: - * CURL_FORMADD_OK on success - * CURL_FORMADD_MEMORY if the FormInfo allocation fails - * CURL_FORMADD_OPTION_TWICE if one option is given twice for one Form - * CURL_FORMADD_NULL if a null pointer was given for a char - * CURL_FORMADD_MEMORY if the allocation of a FormInfo struct failed - * CURL_FORMADD_UNKNOWN_OPTION if an unknown option was used - * CURL_FORMADD_INCOMPLETE if the some FormInfo is not complete (or error) - * CURL_FORMADD_MEMORY if a curl_httppost struct cannot be allocated - * CURL_FORMADD_MEMORY if some allocation for string copying failed. - * CURL_FORMADD_ILLEGAL_ARRAY if an illegal option is used in an array - * - ***************************************************************************/ -typedef enum { - CURL_FORMADD_OK CURL_DEPRECATED(7.56.0, ""), /* 1st, no error */ - - CURL_FORMADD_MEMORY CURL_DEPRECATED(7.56.0, ""), - CURL_FORMADD_OPTION_TWICE CURL_DEPRECATED(7.56.0, ""), - CURL_FORMADD_NULL CURL_DEPRECATED(7.56.0, ""), - CURL_FORMADD_UNKNOWN_OPTION CURL_DEPRECATED(7.56.0, ""), - CURL_FORMADD_INCOMPLETE CURL_DEPRECATED(7.56.0, ""), - CURL_FORMADD_ILLEGAL_ARRAY CURL_DEPRECATED(7.56.0, ""), - /* libcurl was built with form api disabled */ - CURL_FORMADD_DISABLED CURL_DEPRECATED(7.56.0, ""), - - CURL_FORMADD_LAST /* last */ -} CURLFORMcode; - -/* - * NAME curl_formadd() - * - * DESCRIPTION - * - * Pretty advanced function for building multi-part formposts. Each invoke - * adds one part that together construct a full post. Then use - * CURLOPT_HTTPPOST to send it off to libcurl. - */ -CURL_EXTERN CURLFORMcode CURL_DEPRECATED(7.56.0, "Use curl_mime_init()") -curl_formadd(struct curl_httppost **httppost, - struct curl_httppost **last_post, - ...); - -/* - * callback function for curl_formget() - * The void *arg pointer will be the one passed as second argument to - * curl_formget(). - * The character buffer passed to it must not be freed. - * Should return the buffer length passed to it as the argument "len" on - * success. - */ -typedef size_t (*curl_formget_callback)(void *arg, const char *buf, - size_t len); - -/* - * NAME curl_formget() - * - * DESCRIPTION - * - * Serialize a curl_httppost struct built with curl_formadd(). - * Accepts a void pointer as second argument which will be passed to - * the curl_formget_callback function. - * Returns 0 on success. - */ -CURL_EXTERN int CURL_DEPRECATED(7.56.0, "") -curl_formget(struct curl_httppost *form, void *arg, - curl_formget_callback append); -/* - * NAME curl_formfree() - * - * DESCRIPTION - * - * Free a multipart formpost previously built with curl_formadd(). - */ -CURL_EXTERN void CURL_DEPRECATED(7.56.0, "Use curl_mime_free()") -curl_formfree(struct curl_httppost *form); - -/* - * NAME curl_getenv() - * - * DESCRIPTION - * - * Returns a malloc()'ed string that MUST be curl_free()ed after usage is - * complete. DEPRECATED - see lib/README.curlx - */ -CURL_EXTERN char *curl_getenv(const char *variable); - -/* - * NAME curl_version() - * - * DESCRIPTION - * - * Returns a static ASCII string of the libcurl version. - */ -CURL_EXTERN char *curl_version(void); - -/* - * NAME curl_easy_escape() - * - * DESCRIPTION - * - * Escapes URL strings (converts all letters consider illegal in URLs to their - * %XX versions). This function returns a new allocated string or NULL if an - * error occurred. - */ -CURL_EXTERN char *curl_easy_escape(CURL *handle, - const char *string, - int length); - -/* the previous version: */ -CURL_EXTERN char *curl_escape(const char *string, - int length); - -/* - * NAME curl_easy_unescape() - * - * DESCRIPTION - * - * Unescapes URL encoding in strings (converts all %XX codes to their 8bit - * versions). This function returns a new allocated string or NULL if an error - * occurred. - * Conversion Note: On non-ASCII platforms the ASCII %XX codes are - * converted into the host encoding. - */ -CURL_EXTERN char *curl_easy_unescape(CURL *handle, - const char *string, - int length, - int *outlength); - -/* the previous version */ -CURL_EXTERN char *curl_unescape(const char *string, - int length); - -/* - * NAME curl_free() - * - * DESCRIPTION - * - * Provided for de-allocation in the same translation unit that did the - * allocation. Added in libcurl 7.10 - */ -CURL_EXTERN void curl_free(void *p); - -/* - * NAME curl_global_init() - * - * DESCRIPTION - * - * curl_global_init() should be invoked exactly once for each application that - * uses libcurl and before any call of other libcurl functions. - * - * This function is thread-safe if CURL_VERSION_THREADSAFE is set in the - * curl_version_info_data.features flag (fetch by curl_version_info()). - */ -CURL_EXTERN CURLcode curl_global_init(long flags); - -/* - * NAME curl_global_init_mem() - * - * DESCRIPTION - * - * curl_global_init() or curl_global_init_mem() should be invoked exactly once - * for each application that uses libcurl. This function can be used to - * initialize libcurl and set user defined memory management callback - * functions. Users can implement memory management routines to check for - * memory leaks, check for misuse of the curl library etc. User registered - * callback routines will be invoked by this library instead of the system - * memory management routines like malloc, free etc. - */ -CURL_EXTERN CURLcode curl_global_init_mem(long flags, - curl_malloc_callback m, - curl_free_callback f, - curl_realloc_callback r, - curl_strdup_callback s, - curl_calloc_callback c); - -/* - * NAME curl_global_cleanup() - * - * DESCRIPTION - * - * curl_global_cleanup() should be invoked exactly once for each application - * that uses libcurl - */ -CURL_EXTERN void curl_global_cleanup(void); - -/* - * NAME curl_global_trace() - * - * DESCRIPTION - * - * curl_global_trace() can be invoked at application start to - * configure which components in curl should participate in tracing. - * - * This function is thread-safe if CURL_VERSION_THREADSAFE is set in the - * curl_version_info_data.features flag (fetch by curl_version_info()). - */ -CURL_EXTERN CURLcode curl_global_trace(const char *config); - -/* linked-list structure for the CURLOPT_QUOTE option (and other) */ -struct curl_slist { - char *data; - struct curl_slist *next; -}; - -/* - * NAME curl_global_sslset() - * - * DESCRIPTION - * - * When built with multiple SSL backends, curl_global_sslset() allows to - * choose one. This function can only be called once, and it must be called - * *before* curl_global_init(). - * - * The backend can be identified by the id (e.g. CURLSSLBACKEND_OPENSSL). The - * backend can also be specified via the name parameter (passing -1 as id). If - * both id and name are specified, the name will be ignored. If neither id nor - * name are specified, the function will fail with CURLSSLSET_UNKNOWN_BACKEND - * and set the "avail" pointer to the NULL-terminated list of available - * backends. - * - * Upon success, the function returns CURLSSLSET_OK. - * - * If the specified SSL backend is not available, the function returns - * CURLSSLSET_UNKNOWN_BACKEND and sets the "avail" pointer to a - * NULL-terminated list of available SSL backends. - * - * The SSL backend can be set only once. If it has already been set, a - * subsequent attempt to change it will result in a CURLSSLSET_TOO_LATE. - */ - -struct curl_ssl_backend { - curl_sslbackend id; - const char *name; -}; -typedef struct curl_ssl_backend curl_ssl_backend; - -typedef enum { - CURLSSLSET_OK = 0, - CURLSSLSET_UNKNOWN_BACKEND, - CURLSSLSET_TOO_LATE, - CURLSSLSET_NO_BACKENDS /* libcurl was built without any SSL support */ -} CURLsslset; - -CURL_EXTERN CURLsslset curl_global_sslset(curl_sslbackend id, const char *name, - const curl_ssl_backend ***avail); - -/* - * NAME curl_slist_append() - * - * DESCRIPTION - * - * Appends a string to a linked list. If no list exists, it will be created - * first. Returns the new list, after appending. - */ -CURL_EXTERN struct curl_slist *curl_slist_append(struct curl_slist *list, - const char *data); - -/* - * NAME curl_slist_free_all() - * - * DESCRIPTION - * - * free a previously built curl_slist. - */ -CURL_EXTERN void curl_slist_free_all(struct curl_slist *list); - -/* - * NAME curl_getdate() - * - * DESCRIPTION - * - * Returns the time, in seconds since 1 Jan 1970 of the time string given in - * the first argument. The time argument in the second parameter is unused - * and should be set to NULL. - */ -CURL_EXTERN time_t curl_getdate(const char *p, const time_t *unused); - -/* info about the certificate chain, for SSL backends that support it. Asked - for with CURLOPT_CERTINFO / CURLINFO_CERTINFO */ -struct curl_certinfo { - int num_of_certs; /* number of certificates with information */ - struct curl_slist **certinfo; /* for each index in this array, there is a - linked list with textual information for a - certificate in the format "name:content". - eg "Subject:foo", "Issuer:bar", etc. */ -}; - -/* Information about the SSL library used and the respective internal SSL - handle, which can be used to obtain further information regarding the - connection. Asked for with CURLINFO_TLS_SSL_PTR or CURLINFO_TLS_SESSION. */ -struct curl_tlssessioninfo { - curl_sslbackend backend; - void *internals; -}; - -#define CURLINFO_STRING 0x100000 -#define CURLINFO_LONG 0x200000 -#define CURLINFO_DOUBLE 0x300000 -#define CURLINFO_SLIST 0x400000 -#define CURLINFO_PTR 0x400000 /* same as SLIST */ -#define CURLINFO_SOCKET 0x500000 -#define CURLINFO_OFF_T 0x600000 -#define CURLINFO_MASK 0x0fffff -#define CURLINFO_TYPEMASK 0xf00000 - -typedef enum { - CURLINFO_NONE, /* first, never use this */ - CURLINFO_EFFECTIVE_URL = CURLINFO_STRING + 1, - CURLINFO_RESPONSE_CODE = CURLINFO_LONG + 2, - CURLINFO_TOTAL_TIME = CURLINFO_DOUBLE + 3, - CURLINFO_NAMELOOKUP_TIME = CURLINFO_DOUBLE + 4, - CURLINFO_CONNECT_TIME = CURLINFO_DOUBLE + 5, - CURLINFO_PRETRANSFER_TIME = CURLINFO_DOUBLE + 6, - CURLINFO_SIZE_UPLOAD CURL_DEPRECATED(7.55.0, "Use CURLINFO_SIZE_UPLOAD_T") - = CURLINFO_DOUBLE + 7, - CURLINFO_SIZE_UPLOAD_T = CURLINFO_OFF_T + 7, - CURLINFO_SIZE_DOWNLOAD - CURL_DEPRECATED(7.55.0, "Use CURLINFO_SIZE_DOWNLOAD_T") - = CURLINFO_DOUBLE + 8, - CURLINFO_SIZE_DOWNLOAD_T = CURLINFO_OFF_T + 8, - CURLINFO_SPEED_DOWNLOAD - CURL_DEPRECATED(7.55.0, "Use CURLINFO_SPEED_DOWNLOAD_T") - = CURLINFO_DOUBLE + 9, - CURLINFO_SPEED_DOWNLOAD_T = CURLINFO_OFF_T + 9, - CURLINFO_SPEED_UPLOAD - CURL_DEPRECATED(7.55.0, "Use CURLINFO_SPEED_UPLOAD_T") - = CURLINFO_DOUBLE + 10, - CURLINFO_SPEED_UPLOAD_T = CURLINFO_OFF_T + 10, - CURLINFO_HEADER_SIZE = CURLINFO_LONG + 11, - CURLINFO_REQUEST_SIZE = CURLINFO_LONG + 12, - CURLINFO_SSL_VERIFYRESULT = CURLINFO_LONG + 13, - CURLINFO_FILETIME = CURLINFO_LONG + 14, - CURLINFO_FILETIME_T = CURLINFO_OFF_T + 14, - CURLINFO_CONTENT_LENGTH_DOWNLOAD - CURL_DEPRECATED(7.55.0, - "Use CURLINFO_CONTENT_LENGTH_DOWNLOAD_T") - = CURLINFO_DOUBLE + 15, - CURLINFO_CONTENT_LENGTH_DOWNLOAD_T = CURLINFO_OFF_T + 15, - CURLINFO_CONTENT_LENGTH_UPLOAD - CURL_DEPRECATED(7.55.0, - "Use CURLINFO_CONTENT_LENGTH_UPLOAD_T") - = CURLINFO_DOUBLE + 16, - CURLINFO_CONTENT_LENGTH_UPLOAD_T = CURLINFO_OFF_T + 16, - CURLINFO_STARTTRANSFER_TIME = CURLINFO_DOUBLE + 17, - CURLINFO_CONTENT_TYPE = CURLINFO_STRING + 18, - CURLINFO_REDIRECT_TIME = CURLINFO_DOUBLE + 19, - CURLINFO_REDIRECT_COUNT = CURLINFO_LONG + 20, - CURLINFO_PRIVATE = CURLINFO_STRING + 21, - CURLINFO_HTTP_CONNECTCODE = CURLINFO_LONG + 22, - CURLINFO_HTTPAUTH_AVAIL = CURLINFO_LONG + 23, - CURLINFO_PROXYAUTH_AVAIL = CURLINFO_LONG + 24, - CURLINFO_OS_ERRNO = CURLINFO_LONG + 25, - CURLINFO_NUM_CONNECTS = CURLINFO_LONG + 26, - CURLINFO_SSL_ENGINES = CURLINFO_SLIST + 27, - CURLINFO_COOKIELIST = CURLINFO_SLIST + 28, - CURLINFO_LASTSOCKET CURL_DEPRECATED(7.45.0, "Use CURLINFO_ACTIVESOCKET") - = CURLINFO_LONG + 29, - CURLINFO_FTP_ENTRY_PATH = CURLINFO_STRING + 30, - CURLINFO_REDIRECT_URL = CURLINFO_STRING + 31, - CURLINFO_PRIMARY_IP = CURLINFO_STRING + 32, - CURLINFO_APPCONNECT_TIME = CURLINFO_DOUBLE + 33, - CURLINFO_CERTINFO = CURLINFO_PTR + 34, - CURLINFO_CONDITION_UNMET = CURLINFO_LONG + 35, - CURLINFO_RTSP_SESSION_ID = CURLINFO_STRING + 36, - CURLINFO_RTSP_CLIENT_CSEQ = CURLINFO_LONG + 37, - CURLINFO_RTSP_SERVER_CSEQ = CURLINFO_LONG + 38, - CURLINFO_RTSP_CSEQ_RECV = CURLINFO_LONG + 39, - CURLINFO_PRIMARY_PORT = CURLINFO_LONG + 40, - CURLINFO_LOCAL_IP = CURLINFO_STRING + 41, - CURLINFO_LOCAL_PORT = CURLINFO_LONG + 42, - CURLINFO_TLS_SESSION CURL_DEPRECATED(7.48.0, "Use CURLINFO_TLS_SSL_PTR") - = CURLINFO_PTR + 43, - CURLINFO_ACTIVESOCKET = CURLINFO_SOCKET + 44, - CURLINFO_TLS_SSL_PTR = CURLINFO_PTR + 45, - CURLINFO_HTTP_VERSION = CURLINFO_LONG + 46, - CURLINFO_PROXY_SSL_VERIFYRESULT = CURLINFO_LONG + 47, - CURLINFO_PROTOCOL CURL_DEPRECATED(7.85.0, "Use CURLINFO_SCHEME") - = CURLINFO_LONG + 48, - CURLINFO_SCHEME = CURLINFO_STRING + 49, - CURLINFO_TOTAL_TIME_T = CURLINFO_OFF_T + 50, - CURLINFO_NAMELOOKUP_TIME_T = CURLINFO_OFF_T + 51, - CURLINFO_CONNECT_TIME_T = CURLINFO_OFF_T + 52, - CURLINFO_PRETRANSFER_TIME_T = CURLINFO_OFF_T + 53, - CURLINFO_STARTTRANSFER_TIME_T = CURLINFO_OFF_T + 54, - CURLINFO_REDIRECT_TIME_T = CURLINFO_OFF_T + 55, - CURLINFO_APPCONNECT_TIME_T = CURLINFO_OFF_T + 56, - CURLINFO_RETRY_AFTER = CURLINFO_OFF_T + 57, - CURLINFO_EFFECTIVE_METHOD = CURLINFO_STRING + 58, - CURLINFO_PROXY_ERROR = CURLINFO_LONG + 59, - CURLINFO_REFERER = CURLINFO_STRING + 60, - CURLINFO_CAINFO = CURLINFO_STRING + 61, - CURLINFO_CAPATH = CURLINFO_STRING + 62, - CURLINFO_XFER_ID = CURLINFO_OFF_T + 63, - CURLINFO_CONN_ID = CURLINFO_OFF_T + 64, - CURLINFO_QUEUE_TIME_T = CURLINFO_OFF_T + 65, - CURLINFO_USED_PROXY = CURLINFO_LONG + 66, - CURLINFO_POSTTRANSFER_TIME_T = CURLINFO_OFF_T + 67, - CURLINFO_EARLYDATA_SENT_T = CURLINFO_OFF_T + 68, - CURLINFO_HTTPAUTH_USED = CURLINFO_LONG + 69, - CURLINFO_PROXYAUTH_USED = CURLINFO_LONG + 70, - CURLINFO_LASTONE = 70 -} CURLINFO; - -/* CURLINFO_RESPONSE_CODE is the new name for the option previously known as - CURLINFO_HTTP_CODE */ -#define CURLINFO_HTTP_CODE CURLINFO_RESPONSE_CODE - -typedef enum { - CURLCLOSEPOLICY_NONE, /* first, never use this */ - - CURLCLOSEPOLICY_OLDEST, - CURLCLOSEPOLICY_LEAST_RECENTLY_USED, - CURLCLOSEPOLICY_LEAST_TRAFFIC, - CURLCLOSEPOLICY_SLOWEST, - CURLCLOSEPOLICY_CALLBACK, - - CURLCLOSEPOLICY_LAST /* last, never use this */ -} curl_closepolicy; - -#define CURL_GLOBAL_SSL (1 << 0) /* no purpose since 7.57.0 */ -#define CURL_GLOBAL_WIN32 (1 << 1) -#define CURL_GLOBAL_ALL (CURL_GLOBAL_SSL | CURL_GLOBAL_WIN32) -#define CURL_GLOBAL_NOTHING 0 -#define CURL_GLOBAL_DEFAULT CURL_GLOBAL_ALL -#define CURL_GLOBAL_ACK_EINTR (1 << 2) - -/***************************************************************************** - * Setup defines, protos etc for the sharing stuff. - */ - -/* Different data locks for a single share */ -typedef enum { - CURL_LOCK_DATA_NONE = 0, - /* CURL_LOCK_DATA_SHARE is used internally to say that - * the locking is just made to change the internal state of the share - * itself. - */ - CURL_LOCK_DATA_SHARE, - CURL_LOCK_DATA_COOKIE, - CURL_LOCK_DATA_DNS, - CURL_LOCK_DATA_SSL_SESSION, - CURL_LOCK_DATA_CONNECT, - CURL_LOCK_DATA_PSL, - CURL_LOCK_DATA_HSTS, - CURL_LOCK_DATA_LAST -} curl_lock_data; - -/* Different lock access types */ -typedef enum { - CURL_LOCK_ACCESS_NONE = 0, /* unspecified action */ - CURL_LOCK_ACCESS_SHARED = 1, /* for read perhaps */ - CURL_LOCK_ACCESS_SINGLE = 2, /* for write perhaps */ - CURL_LOCK_ACCESS_LAST /* never use */ -} curl_lock_access; - -typedef void (*curl_lock_function)(CURL *handle, - curl_lock_data data, - curl_lock_access locktype, - void *userptr); -typedef void (*curl_unlock_function)(CURL *handle, - curl_lock_data data, - void *userptr); - -typedef enum { - CURLSHE_OK, /* all is fine */ - CURLSHE_BAD_OPTION, /* 1 */ - CURLSHE_IN_USE, /* 2 */ - CURLSHE_INVALID, /* 3 */ - CURLSHE_NOMEM, /* 4 out of memory */ - CURLSHE_NOT_BUILT_IN, /* 5 feature not present in lib */ - CURLSHE_LAST /* never use */ -} CURLSHcode; - -typedef enum { - CURLSHOPT_NONE, /* do not use */ - CURLSHOPT_SHARE, /* specify a data type to share */ - CURLSHOPT_UNSHARE, /* specify which data type to stop sharing */ - CURLSHOPT_LOCKFUNC, /* pass in a 'curl_lock_function' pointer */ - CURLSHOPT_UNLOCKFUNC, /* pass in a 'curl_unlock_function' pointer */ - CURLSHOPT_USERDATA, /* pass in a user data pointer used in the lock/unlock - callback functions */ - CURLSHOPT_LAST /* never use */ -} CURLSHoption; - -CURL_EXTERN CURLSH *curl_share_init(void); -CURL_EXTERN CURLSHcode curl_share_setopt(CURLSH *share, CURLSHoption option, - ...); -CURL_EXTERN CURLSHcode curl_share_cleanup(CURLSH *share); - -/**************************************************************************** - * Structures for querying information about the curl library at runtime. - */ - -typedef enum { - CURLVERSION_FIRST, /* 7.10 */ - CURLVERSION_SECOND, /* 7.11.1 */ - CURLVERSION_THIRD, /* 7.12.0 */ - CURLVERSION_FOURTH, /* 7.16.1 */ - CURLVERSION_FIFTH, /* 7.57.0 */ - CURLVERSION_SIXTH, /* 7.66.0 */ - CURLVERSION_SEVENTH, /* 7.70.0 */ - CURLVERSION_EIGHTH, /* 7.72.0 */ - CURLVERSION_NINTH, /* 7.75.0 */ - CURLVERSION_TENTH, /* 7.77.0 */ - CURLVERSION_ELEVENTH, /* 7.87.0 */ - CURLVERSION_TWELFTH, /* 8.8.0 */ - CURLVERSION_LAST /* never actually use this */ -} CURLversion; - -/* The 'CURLVERSION_NOW' is the symbolic name meant to be used by - basically all programs ever that want to get version information. It is - meant to be a built-in version number for what kind of struct the caller - expects. If the struct ever changes, we redefine the NOW to another enum - from above. */ -#define CURLVERSION_NOW CURLVERSION_TWELFTH - -struct curl_version_info_data { - CURLversion age; /* age of the returned struct */ - const char *version; /* LIBCURL_VERSION */ - unsigned int version_num; /* LIBCURL_VERSION_NUM */ - const char *host; /* OS/host/cpu/machine when configured */ - int features; /* bitmask, see defines below */ - const char *ssl_version; /* human readable string */ - long ssl_version_num; /* not used anymore, always 0 */ - const char *libz_version; /* human readable string */ - /* protocols is terminated by an entry with a NULL protoname */ - const char * const *protocols; - - /* The fields below this were added in CURLVERSION_SECOND */ - const char *ares; - int ares_num; - - /* This field was added in CURLVERSION_THIRD */ - const char *libidn; - - /* These field were added in CURLVERSION_FOURTH */ - - /* Same as '_libiconv_version' if built with HAVE_ICONV */ - int iconv_ver_num; - - const char *libssh_version; /* human readable string */ - - /* These fields were added in CURLVERSION_FIFTH */ - unsigned int brotli_ver_num; /* Numeric Brotli version - (MAJOR << 24) | (MINOR << 12) | PATCH */ - const char *brotli_version; /* human readable string. */ - - /* These fields were added in CURLVERSION_SIXTH */ - unsigned int nghttp2_ver_num; /* Numeric nghttp2 version - (MAJOR << 16) | (MINOR << 8) | PATCH */ - const char *nghttp2_version; /* human readable string. */ - const char *quic_version; /* human readable quic (+ HTTP/3) library + - version or NULL */ - - /* These fields were added in CURLVERSION_SEVENTH */ - const char *cainfo; /* the built-in default CURLOPT_CAINFO, might - be NULL */ - const char *capath; /* the built-in default CURLOPT_CAPATH, might - be NULL */ - - /* These fields were added in CURLVERSION_EIGHTH */ - unsigned int zstd_ver_num; /* Numeric Zstd version - (MAJOR << 24) | (MINOR << 12) | PATCH */ - const char *zstd_version; /* human readable string. */ - - /* These fields were added in CURLVERSION_NINTH */ - const char *hyper_version; /* human readable string. */ - - /* These fields were added in CURLVERSION_TENTH */ - const char *gsasl_version; /* human readable string. */ - - /* These fields were added in CURLVERSION_ELEVENTH */ - /* feature_names is terminated by an entry with a NULL feature name */ - const char * const *feature_names; - - /* These fields were added in CURLVERSION_TWELFTH */ - const char *rtmp_version; /* human readable string. */ -}; -typedef struct curl_version_info_data curl_version_info_data; - -#define CURL_VERSION_IPV6 (1<<0) /* IPv6-enabled */ -#define CURL_VERSION_KERBEROS4 (1<<1) /* Kerberos V4 auth is supported - (deprecated) */ -#define CURL_VERSION_SSL (1<<2) /* SSL options are present */ -#define CURL_VERSION_LIBZ (1<<3) /* libz features are present */ -#define CURL_VERSION_NTLM (1<<4) /* NTLM auth is supported */ -#define CURL_VERSION_GSSNEGOTIATE (1<<5) /* Negotiate auth is supported - (deprecated) */ -#define CURL_VERSION_DEBUG (1<<6) /* Built with debug capabilities */ -#define CURL_VERSION_ASYNCHDNS (1<<7) /* Asynchronous DNS resolves */ -#define CURL_VERSION_SPNEGO (1<<8) /* SPNEGO auth is supported */ -#define CURL_VERSION_LARGEFILE (1<<9) /* Supports files larger than 2GB */ -#define CURL_VERSION_IDN (1<<10) /* Internationized Domain Names are - supported */ -#define CURL_VERSION_SSPI (1<<11) /* Built against Windows SSPI */ -#define CURL_VERSION_CONV (1<<12) /* Character conversions supported */ -#define CURL_VERSION_CURLDEBUG (1<<13) /* Debug memory tracking supported */ -#define CURL_VERSION_TLSAUTH_SRP (1<<14) /* TLS-SRP auth is supported */ -#define CURL_VERSION_NTLM_WB (1<<15) /* NTLM delegation to winbind helper - is supported */ -#define CURL_VERSION_HTTP2 (1<<16) /* HTTP2 support built-in */ -#define CURL_VERSION_GSSAPI (1<<17) /* Built against a GSS-API library */ -#define CURL_VERSION_KERBEROS5 (1<<18) /* Kerberos V5 auth is supported */ -#define CURL_VERSION_UNIX_SOCKETS (1<<19) /* Unix domain sockets support */ -#define CURL_VERSION_PSL (1<<20) /* Mozilla's Public Suffix List, used - for cookie domain verification */ -#define CURL_VERSION_HTTPS_PROXY (1<<21) /* HTTPS-proxy support built-in */ -#define CURL_VERSION_MULTI_SSL (1<<22) /* Multiple SSL backends available */ -#define CURL_VERSION_BROTLI (1<<23) /* Brotli features are present. */ -#define CURL_VERSION_ALTSVC (1<<24) /* Alt-Svc handling built-in */ -#define CURL_VERSION_HTTP3 (1<<25) /* HTTP3 support built-in */ -#define CURL_VERSION_ZSTD (1<<26) /* zstd features are present */ -#define CURL_VERSION_UNICODE (1<<27) /* Unicode support on Windows */ -#define CURL_VERSION_HSTS (1<<28) /* HSTS is supported */ -#define CURL_VERSION_GSASL (1<<29) /* libgsasl is supported */ -#define CURL_VERSION_THREADSAFE (1<<30) /* libcurl API is thread-safe */ - -/* - * NAME curl_version_info() - * - * DESCRIPTION - * - * This function returns a pointer to a static copy of the version info - * struct. See above. - */ -CURL_EXTERN curl_version_info_data *curl_version_info(CURLversion); - -/* - * NAME curl_easy_strerror() - * - * DESCRIPTION - * - * The curl_easy_strerror function may be used to turn a CURLcode value - * into the equivalent human readable error string. This is useful - * for printing meaningful error messages. - */ -CURL_EXTERN const char *curl_easy_strerror(CURLcode); - -/* - * NAME curl_share_strerror() - * - * DESCRIPTION - * - * The curl_share_strerror function may be used to turn a CURLSHcode value - * into the equivalent human readable error string. This is useful - * for printing meaningful error messages. - */ -CURL_EXTERN const char *curl_share_strerror(CURLSHcode); - -/* - * NAME curl_easy_pause() - * - * DESCRIPTION - * - * The curl_easy_pause function pauses or unpauses transfers. Select the new - * state by setting the bitmask, use the convenience defines below. - * - */ -CURL_EXTERN CURLcode curl_easy_pause(CURL *handle, int bitmask); - -#define CURLPAUSE_RECV (1 << 0) -#define CURLPAUSE_RECV_CONT (0) - -#define CURLPAUSE_SEND (1 << 2) -#define CURLPAUSE_SEND_CONT (0) - -#define CURLPAUSE_ALL (CURLPAUSE_RECV | CURLPAUSE_SEND) -#define CURLPAUSE_CONT (CURLPAUSE_RECV_CONT | CURLPAUSE_SEND_CONT) - -/* - * NAME curl_easy_ssls_import() - * - * DESCRIPTION - * - * The curl_easy_ssls_import function adds a previously exported SSL session - * to the SSL session cache of the easy handle (or the underlying share). - */ -CURL_EXTERN CURLcode curl_easy_ssls_import(CURL *handle, - const char *session_key, - const unsigned char *shmac, - size_t shmac_len, - const unsigned char *sdata, - size_t sdata_len); - -/* This is the curl_ssls_export_cb callback prototype. It - * is passed to curl_easy_ssls_export() to extract SSL sessions/tickets. */ -typedef CURLcode curl_ssls_export_cb(CURL *handle, - void *userptr, - const char *session_key, - const unsigned char *shmac, - size_t shmac_len, - const unsigned char *sdata, - size_t sdata_len, - curl_off_t valid_until, - int ietf_tls_id, - const char *alpn, - size_t earlydata_max); - -/* - * NAME curl_easy_ssls_export() - * - * DESCRIPTION - * - * The curl_easy_ssls_export function iterates over all SSL sessions stored - * in the easy handle (or underlying share) and invokes the passed - * callback. - * - */ -CURL_EXTERN CURLcode curl_easy_ssls_export(CURL *handle, - curl_ssls_export_cb *export_fn, - void *userptr); - -#ifdef __cplusplus -} /* end of extern "C" */ -#endif - -/* unfortunately, the easy.h and multi.h include files need options and info - stuff before they can be included! */ -#include "easy.h" /* nothing in curl is fun without the easy stuff */ -#include "multi.h" -#include "urlapi.h" -#include "options.h" -#include "header.h" -#include "websockets.h" -#include "mprintf.h" - -/* the typechecker does not work in C++ (yet) */ -#if ((defined(__GNUC__) && defined(__GNUC_MINOR__) && \ - ((__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 3))) || \ - (defined(__clang__) && __clang_major__ >= 14)) && \ - !defined(__cplusplus) && !defined(CURL_DISABLE_TYPECHECK) -#include "typecheck-gcc.h" -#else -#if defined(__STDC__) && (__STDC__ >= 1) -/* This preprocessor magic that replaces a call with the exact same call is - only done to make sure application authors pass exactly three arguments - to these functions. */ -#define curl_easy_setopt(handle,opt,param) curl_easy_setopt(handle,opt,param) -#define curl_easy_getinfo(handle,info,arg) curl_easy_getinfo(handle,info,arg) -#define curl_share_setopt(share,opt,param) curl_share_setopt(share,opt,param) -#define curl_multi_setopt(handle,opt,param) curl_multi_setopt(handle,opt,param) -#endif /* __STDC__ >= 1 */ -#endif /* gcc >= 4.3 && !__cplusplus && !CURL_DISABLE_TYPECHECK */ - -#endif /* CURLINC_CURL_H */ diff --git a/libs/curl/include/curl/curlver.h b/libs/curl/include/curl/curlver.h deleted file mode 100644 index d1608c9ce..000000000 --- a/libs/curl/include/curl/curlver.h +++ /dev/null @@ -1,78 +0,0 @@ -#ifndef CURLINC_CURLVER_H -#define CURLINC_CURLVER_H -/*************************************************************************** - * _ _ ____ _ - * Project ___| | | | _ \| | - * / __| | | | |_) | | - * | (__| |_| | _ <| |___ - * \___|\___/|_| \_\_____| - * - * Copyright (C) Daniel Stenberg, , et al. - * - * This software is licensed as described in the file COPYING, which - * you should have received as part of this distribution. The terms - * are also available at https://curl.se/docs/copyright.html. - * - * You may opt to use, copy, modify, merge, publish, distribute and/or sell - * copies of the Software, and permit persons to whom the Software is - * furnished to do so, under the terms of the COPYING file. - * - * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY - * KIND, either express or implied. - * - * SPDX-License-Identifier: curl - * - ***************************************************************************/ - -/* This header file contains nothing but libcurl version info, generated by - a script at release-time. This was made its own header file in 7.11.2 */ - -/* This is the global package copyright */ -#define LIBCURL_COPYRIGHT "Daniel Stenberg, ." - -/* This is the version number of the libcurl package from which this header - file origins: */ -#define LIBCURL_VERSION "8.18.0" - -/* The numeric version number is also available "in parts" by using these - defines: */ -#define LIBCURL_VERSION_MAJOR 8 -#define LIBCURL_VERSION_MINOR 18 -#define LIBCURL_VERSION_PATCH 0 -/* This is the numeric version of the libcurl version number, meant for easier - parsing and comparisons by programs. The LIBCURL_VERSION_NUM define will - always follow this syntax: - - 0xXXYYZZ - - Where XX, YY and ZZ are the main version, release and patch numbers in - hexadecimal (using 8 bits each). All three numbers are always represented - using two digits. 1.2 would appear as "0x010200" while version 9.11.7 - appears as "0x090b07". - - This 6-digit (24 bits) hexadecimal number does not show pre-release number, - and it is always a greater number in a more recent release. It makes - comparisons with greater than and less than work. - - Note: This define is the full hex number and _does not_ use the - CURL_VERSION_BITS() macro since curl's own configure script greps for it - and needs it to contain the full number. -*/ -#define LIBCURL_VERSION_NUM 0x081200 - -/* - * This is the date and time when the full source package was created. The - * timestamp is not stored in git, as the timestamp is properly set in the - * tarballs by the maketgz script. - * - * The format of the date follows this template: - * - * "2007-11-23" - */ -#define LIBCURL_TIMESTAMP "2026-01-07" - -#define CURL_VERSION_BITS(x, y, z) ((x) << 16 | (y) << 8 | (z)) -#define CURL_AT_LEAST_VERSION(x, y, z) \ - (LIBCURL_VERSION_NUM >= CURL_VERSION_BITS(x, y, z)) - -#endif /* CURLINC_CURLVER_H */ diff --git a/libs/curl/include/curl/easy.h b/libs/curl/include/curl/easy.h deleted file mode 100644 index 5b3cdbd64..000000000 --- a/libs/curl/include/curl/easy.h +++ /dev/null @@ -1,123 +0,0 @@ -#ifndef CURLINC_EASY_H -#define CURLINC_EASY_H -/*************************************************************************** - * _ _ ____ _ - * Project ___| | | | _ \| | - * / __| | | | |_) | | - * | (__| |_| | _ <| |___ - * \___|\___/|_| \_\_____| - * - * Copyright (C) Daniel Stenberg, , et al. - * - * This software is licensed as described in the file COPYING, which - * you should have received as part of this distribution. The terms - * are also available at https://curl.se/docs/copyright.html. - * - * You may opt to use, copy, modify, merge, publish, distribute and/or sell - * copies of the Software, and permit persons to whom the Software is - * furnished to do so, under the terms of the COPYING file. - * - * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY - * KIND, either express or implied. - * - * SPDX-License-Identifier: curl - * - ***************************************************************************/ -#ifdef __cplusplus -extern "C" { -#endif - -/* Flag bits in the curl_blob struct: */ -#define CURL_BLOB_COPY 1 /* tell libcurl to copy the data */ -#define CURL_BLOB_NOCOPY 0 /* tell libcurl to NOT copy the data */ - -struct curl_blob { - void *data; - size_t len; - unsigned int flags; /* bit 0 is defined, the rest are reserved and should be - left zeroes */ -}; - -CURL_EXTERN CURL *curl_easy_init(void); -CURL_EXTERN CURLcode curl_easy_setopt(CURL *curl, CURLoption option, ...); -CURL_EXTERN CURLcode curl_easy_perform(CURL *curl); -CURL_EXTERN void curl_easy_cleanup(CURL *curl); - -/* - * NAME curl_easy_getinfo() - * - * DESCRIPTION - * - * Request internal information from the curl session with this function. - * The third argument MUST be pointing to the specific type of the used option - * which is documented in each man page of the option. The data pointed to - * will be filled in accordingly and can be relied upon only if the function - * returns CURLE_OK. This function is intended to get used *AFTER* a performed - * transfer, all results from this function are undefined until the transfer - * is completed. - */ -CURL_EXTERN CURLcode curl_easy_getinfo(CURL *curl, CURLINFO info, ...); - -/* - * NAME curl_easy_duphandle() - * - * DESCRIPTION - * - * Creates a new curl session handle with the same options set for the handle - * passed in. Duplicating a handle could only be a matter of cloning data and - * options, internal state info and things like persistent connections cannot - * be transferred. It is useful in multi-threaded applications when you can run - * curl_easy_duphandle() for each new thread to avoid a series of identical - * curl_easy_setopt() invokes in every thread. - */ -CURL_EXTERN CURL *curl_easy_duphandle(CURL *curl); - -/* - * NAME curl_easy_reset() - * - * DESCRIPTION - * - * Re-initializes a curl handle to the default values. This puts back the - * handle to the same state as it was in when it was just created. - * - * It does keep: live connections, the Session ID cache, the DNS cache and the - * cookies. - */ -CURL_EXTERN void curl_easy_reset(CURL *curl); - -/* - * NAME curl_easy_recv() - * - * DESCRIPTION - * - * Receives data from the connected socket. Use after successful - * curl_easy_perform() with CURLOPT_CONNECT_ONLY option. - */ -CURL_EXTERN CURLcode curl_easy_recv(CURL *curl, void *buffer, size_t buflen, - size_t *n); - -/* - * NAME curl_easy_send() - * - * DESCRIPTION - * - * Sends data over the connected socket. Use after successful - * curl_easy_perform() with CURLOPT_CONNECT_ONLY option. - */ -CURL_EXTERN CURLcode curl_easy_send(CURL *curl, const void *buffer, - size_t buflen, size_t *n); - -/* - * NAME curl_easy_upkeep() - * - * DESCRIPTION - * - * Performs connection upkeep for the given session handle. - */ -CURL_EXTERN CURLcode curl_easy_upkeep(CURL *curl); - -#ifdef __cplusplus -} /* end of extern "C" */ -#endif - -#endif diff --git a/libs/curl/include/curl/header.h b/libs/curl/include/curl/header.h deleted file mode 100644 index e7334b5a3..000000000 --- a/libs/curl/include/curl/header.h +++ /dev/null @@ -1,74 +0,0 @@ -#ifndef CURLINC_HEADER_H -#define CURLINC_HEADER_H -/*************************************************************************** - * _ _ ____ _ - * Project ___| | | | _ \| | - * / __| | | | |_) | | - * | (__| |_| | _ <| |___ - * \___|\___/|_| \_\_____| - * - * Copyright (C) Daniel Stenberg, , et al. - * - * This software is licensed as described in the file COPYING, which - * you should have received as part of this distribution. The terms - * are also available at https://curl.se/docs/copyright.html. - * - * You may opt to use, copy, modify, merge, publish, distribute and/or sell - * copies of the Software, and permit persons to whom the Software is - * furnished to do so, under the terms of the COPYING file. - * - * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY - * KIND, either express or implied. - * - * SPDX-License-Identifier: curl - * - ***************************************************************************/ - -#ifdef __cplusplus -extern "C" { -#endif - -struct curl_header { - char *name; /* this might not use the same case */ - char *value; - size_t amount; /* number of headers using this name */ - size_t index; /* ... of this instance, 0 or higher */ - unsigned int origin; /* see bits below */ - void *anchor; /* handle privately used by libcurl */ -}; - -/* 'origin' bits */ -#define CURLH_HEADER (1 << 0) /* plain server header */ -#define CURLH_TRAILER (1 << 1) /* trailers */ -#define CURLH_CONNECT (1 << 2) /* CONNECT headers */ -#define CURLH_1XX (1 << 3) /* 1xx headers */ -#define CURLH_PSEUDO (1 << 4) /* pseudo headers */ - -typedef enum { - CURLHE_OK, - CURLHE_BADINDEX, /* header exists but not with this index */ - CURLHE_MISSING, /* no such header exists */ - CURLHE_NOHEADERS, /* no headers at all exist (yet) */ - CURLHE_NOREQUEST, /* no request with this number was used */ - CURLHE_OUT_OF_MEMORY, /* out of memory while processing */ - CURLHE_BAD_ARGUMENT, /* a function argument was not okay */ - CURLHE_NOT_BUILT_IN /* if API was disabled in the build */ -} CURLHcode; - -CURL_EXTERN CURLHcode curl_easy_header(CURL *easy, - const char *name, - size_t index, - unsigned int origin, - int request, - struct curl_header **hout); - -CURL_EXTERN struct curl_header *curl_easy_nextheader(CURL *easy, - unsigned int origin, - int request, - struct curl_header *prev); - -#ifdef __cplusplus -} /* end of extern "C" */ -#endif - -#endif /* CURLINC_HEADER_H */ diff --git a/libs/curl/include/curl/mprintf.h b/libs/curl/include/curl/mprintf.h deleted file mode 100644 index 9272e7489..000000000 --- a/libs/curl/include/curl/mprintf.h +++ /dev/null @@ -1,85 +0,0 @@ -#ifndef CURLINC_MPRINTF_H -#define CURLINC_MPRINTF_H -/*************************************************************************** - * _ _ ____ _ - * Project ___| | | | _ \| | - * / __| | | | |_) | | - * | (__| |_| | _ <| |___ - * \___|\___/|_| \_\_____| - * - * Copyright (C) Daniel Stenberg, , et al. - * - * This software is licensed as described in the file COPYING, which - * you should have received as part of this distribution. The terms - * are also available at https://curl.se/docs/copyright.html. - * - * You may opt to use, copy, modify, merge, publish, distribute and/or sell - * copies of the Software, and permit persons to whom the Software is - * furnished to do so, under the terms of the COPYING file. - * - * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY - * KIND, either express or implied. - * - * SPDX-License-Identifier: curl - * - ***************************************************************************/ - -#include -#include /* needed for FILE */ -#include "curl.h" /* for CURL_EXTERN */ - -#ifdef __cplusplus -extern "C" { -#endif - -#ifndef CURL_TEMP_PRINTF -#if (defined(__GNUC__) || defined(__clang__) || \ - defined(__IAR_SYSTEMS_ICC__)) && \ - defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) && \ - !defined(CURL_NO_FMT_CHECKS) -#if defined(__MINGW32__) && !defined(__clang__) -#ifdef __MINGW_PRINTF_FORMAT /* mingw-w64 3.0.0+. Needs stdio.h. */ -#define CURL_TEMP_PRINTF(fmt, arg) \ - __attribute__((format(__MINGW_PRINTF_FORMAT, fmt, arg))) -#else -#define CURL_TEMP_PRINTF(fmt, arg) -#endif -#else -#define CURL_TEMP_PRINTF(fmt, arg) \ - __attribute__((format(printf, fmt, arg))) -#endif -#else -#define CURL_TEMP_PRINTF(fmt, arg) -#endif -#endif - -CURL_EXTERN int curl_mprintf(const char *format, ...) - CURL_TEMP_PRINTF(1, 2); -CURL_EXTERN int curl_mfprintf(FILE *fd, const char *format, ...) - CURL_TEMP_PRINTF(2, 3); -CURL_EXTERN int curl_msprintf(char *buffer, const char *format, ...) - CURL_TEMP_PRINTF(2, 3); -CURL_EXTERN int curl_msnprintf(char *buffer, size_t maxlength, - const char *format, ...) - CURL_TEMP_PRINTF(3, 4); -CURL_EXTERN int curl_mvprintf(const char *format, va_list args) - CURL_TEMP_PRINTF(1, 0); -CURL_EXTERN int curl_mvfprintf(FILE *fd, const char *format, va_list args) - CURL_TEMP_PRINTF(2, 0); -CURL_EXTERN int curl_mvsprintf(char *buffer, const char *format, va_list args) - CURL_TEMP_PRINTF(2, 0); -CURL_EXTERN int curl_mvsnprintf(char *buffer, size_t maxlength, - const char *format, va_list args) - CURL_TEMP_PRINTF(3, 0); -CURL_EXTERN char *curl_maprintf(const char *format, ...) - CURL_TEMP_PRINTF(1, 2); -CURL_EXTERN char *curl_mvaprintf(const char *format, va_list args) - CURL_TEMP_PRINTF(1, 0); - -#undef CURL_TEMP_PRINTF - -#ifdef __cplusplus -} /* end of extern "C" */ -#endif - -#endif /* CURLINC_MPRINTF_H */ diff --git a/libs/curl/include/curl/multi.h b/libs/curl/include/curl/multi.h deleted file mode 100644 index 531c1a954..000000000 --- a/libs/curl/include/curl/multi.h +++ /dev/null @@ -1,551 +0,0 @@ -#ifndef CURLINC_MULTI_H -#define CURLINC_MULTI_H -/*************************************************************************** - * _ _ ____ _ - * Project ___| | | | _ \| | - * / __| | | | |_) | | - * | (__| |_| | _ <| |___ - * \___|\___/|_| \_\_____| - * - * Copyright (C) Daniel Stenberg, , et al. - * - * This software is licensed as described in the file COPYING, which - * you should have received as part of this distribution. The terms - * are also available at https://curl.se/docs/copyright.html. - * - * You may opt to use, copy, modify, merge, publish, distribute and/or sell - * copies of the Software, and permit persons to whom the Software is - * furnished to do so, under the terms of the COPYING file. - * - * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY - * KIND, either express or implied. - * - * SPDX-License-Identifier: curl - * - ***************************************************************************/ -/* - This is an "external" header file. Do not give away any internals here! - - GOALS - - o Enable a "pull" interface. The application that uses libcurl decides where - and when to ask libcurl to get/send data. - - o Enable multiple simultaneous transfers in the same thread without making it - complicated for the application. - - o Enable the application to select() on its own file descriptors and curl's - file descriptors simultaneous easily. - -*/ - -/* - * This header file should not really need to include "curl.h" since curl.h - * itself includes this file and we expect user applications to do #include - * without the need for especially including multi.h. - * - * For some reason we added this include here at one point, and rather than to - * break existing (wrongly written) libcurl applications, we leave it as-is - * but with this warning attached. - */ -#include "curl.h" - -#ifdef __cplusplus -extern "C" { -#endif - -typedef void CURLM; - -typedef enum { - CURLM_CALL_MULTI_PERFORM = -1, /* please call curl_multi_perform() or - curl_multi_socket*() soon */ - CURLM_OK, - CURLM_BAD_HANDLE, /* the passed-in handle is not a valid CURLM handle */ - CURLM_BAD_EASY_HANDLE, /* an easy handle was not good/valid */ - CURLM_OUT_OF_MEMORY, /* if you ever get this, you are in deep sh*t */ - CURLM_INTERNAL_ERROR, /* this is a libcurl bug */ - CURLM_BAD_SOCKET, /* the passed in socket argument did not match */ - CURLM_UNKNOWN_OPTION, /* curl_multi_setopt() with unsupported option */ - CURLM_ADDED_ALREADY, /* an easy handle already added to a multi handle was - attempted to get added - again */ - CURLM_RECURSIVE_API_CALL, /* an api function was called from inside a - callback */ - CURLM_WAKEUP_FAILURE, /* wakeup is unavailable or failed */ - CURLM_BAD_FUNCTION_ARGUMENT, /* function called with a bad parameter */ - CURLM_ABORTED_BY_CALLBACK, - CURLM_UNRECOVERABLE_POLL, - CURLM_LAST -} CURLMcode; - -/* just to make code nicer when using curl_multi_socket() you can now check - for CURLM_CALL_MULTI_SOCKET too in the same style it works for - curl_multi_perform() and CURLM_CALL_MULTI_PERFORM */ -#define CURLM_CALL_MULTI_SOCKET CURLM_CALL_MULTI_PERFORM - -/* bitmask bits for CURLMOPT_PIPELINING */ -#define CURLPIPE_NOTHING 0L -#define CURLPIPE_HTTP1 1L -#define CURLPIPE_MULTIPLEX 2L - -typedef enum { - CURLMSG_NONE, /* first, not used */ - CURLMSG_DONE, /* This easy handle has completed. 'result' contains - the CURLcode of the transfer */ - CURLMSG_LAST /* last, not used */ -} CURLMSG; - -struct CURLMsg { - CURLMSG msg; /* what this message means */ - CURL *easy_handle; /* the handle it concerns */ - union { - void *whatever; /* message-specific data */ - CURLcode result; /* return code for transfer */ - } data; -}; -typedef struct CURLMsg CURLMsg; - -/* Based on poll(2) structure and values. - * We do not use pollfd and POLL* constants explicitly - * to cover platforms without poll(). */ -#define CURL_WAIT_POLLIN 0x0001 -#define CURL_WAIT_POLLPRI 0x0002 -#define CURL_WAIT_POLLOUT 0x0004 - -struct curl_waitfd { - curl_socket_t fd; - short events; - short revents; -}; - -/* - * Name: curl_multi_init() - * - * Desc: initialize multi-style curl usage - * - * Returns: a new CURLM handle to use in all 'curl_multi' functions. - */ -CURL_EXTERN CURLM *curl_multi_init(void); - -/* - * Name: curl_multi_add_handle() - * - * Desc: add a standard curl handle to the multi stack - * - * Returns: CURLMcode type, general multi error code. - */ -CURL_EXTERN CURLMcode curl_multi_add_handle(CURLM *multi_handle, - CURL *curl_handle); - -/* - * Name: curl_multi_remove_handle() - * - * Desc: removes a curl handle from the multi stack again - * - * Returns: CURLMcode type, general multi error code. - */ -CURL_EXTERN CURLMcode curl_multi_remove_handle(CURLM *multi_handle, - CURL *curl_handle); - -/* - * Name: curl_multi_fdset() - * - * Desc: Ask curl for its fd_set sets. The app can use these to select() or - * poll() on. We want curl_multi_perform() called as soon as one of - * them are ready. - * - * Returns: CURLMcode type, general multi error code. - */ -CURL_EXTERN CURLMcode curl_multi_fdset(CURLM *multi_handle, - fd_set *read_fd_set, - fd_set *write_fd_set, - fd_set *exc_fd_set, - int *max_fd); - -/* - * Name: curl_multi_wait() - * - * Desc: Poll on all fds within a CURLM set as well as any - * additional fds passed to the function. - * - * Returns: CURLMcode type, general multi error code. - */ -CURL_EXTERN CURLMcode curl_multi_wait(CURLM *multi_handle, - struct curl_waitfd extra_fds[], - unsigned int extra_nfds, - int timeout_ms, - int *ret); - -/* - * Name: curl_multi_poll() - * - * Desc: Poll on all fds within a CURLM set as well as any - * additional fds passed to the function. - * - * Returns: CURLMcode type, general multi error code. - */ -CURL_EXTERN CURLMcode curl_multi_poll(CURLM *multi_handle, - struct curl_waitfd extra_fds[], - unsigned int extra_nfds, - int timeout_ms, - int *ret); - -/* - * Name: curl_multi_wakeup() - * - * Desc: wakes up a sleeping curl_multi_poll call. - * - * Returns: CURLMcode type, general multi error code. - */ -CURL_EXTERN CURLMcode curl_multi_wakeup(CURLM *multi_handle); - -/* - * Name: curl_multi_perform() - * - * Desc: When the app thinks there is data available for curl it calls this - * function to read/write whatever there is right now. This returns - * as soon as the reads and writes are done. This function does not - * require that there actually is data available for reading or that - * data can be written, it can be called just in case. It returns - * the number of handles that still transfer data in the second - * argument's integer-pointer. - * - * Returns: CURLMcode type, general multi error code. *NOTE* that this only - * returns errors etc regarding the whole multi stack. There might - * still have occurred problems on individual transfers even when - * this returns OK. - */ -CURL_EXTERN CURLMcode curl_multi_perform(CURLM *multi_handle, - int *running_handles); - -/* - * Name: curl_multi_cleanup() - * - * Desc: Cleans up and removes a whole multi stack. It does not free or - * touch any individual easy handles in any way. We need to define - * in what state those handles will be if this function is called - * in the middle of a transfer. - * - * Returns: CURLMcode type, general multi error code. - */ -CURL_EXTERN CURLMcode curl_multi_cleanup(CURLM *multi_handle); - -/* - * Name: curl_multi_info_read() - * - * Desc: Ask the multi handle if there is any messages/informationals from - * the individual transfers. Messages include informationals such as - * error code from the transfer or just the fact that a transfer is - * completed. More details on these should be written down as well. - * - * Repeated calls to this function will return a new struct each - * time, until a special "end of msgs" struct is returned as a signal - * that there is no more to get at this point. - * - * The data the returned pointer points to will not survive calling - * curl_multi_cleanup(). - * - * The 'CURLMsg' struct is meant to be simple and only contain basic - * information. If more involved information is wanted, we will - * provide the particular "transfer handle" in that struct and that - * should/could/would be used in subsequent curl_easy_getinfo() calls - * (or similar). The point being that we must never expose complex - * structs to applications, as then we will undoubtably get backwards - * compatibility problems in the future. - * - * Returns: A pointer to a filled-in struct, or NULL if it failed or ran out - * of structs. It also writes the number of messages left in the - * queue (after this read) in the integer the second argument points - * to. - */ -CURL_EXTERN CURLMsg *curl_multi_info_read(CURLM *multi_handle, - int *msgs_in_queue); - -/* - * Name: curl_multi_strerror() - * - * Desc: The curl_multi_strerror function may be used to turn a CURLMcode - * value into the equivalent human readable error string. This is - * useful for printing meaningful error messages. - * - * Returns: A pointer to a null-terminated error message. - */ -CURL_EXTERN const char *curl_multi_strerror(CURLMcode); - -/* - * Name: curl_multi_socket() and - * curl_multi_socket_all() - * - * Desc: An alternative version of curl_multi_perform() that allows the - * application to pass in one of the file descriptors that have been - * detected to have "action" on them and let libcurl perform. - * See man page for details. - */ -#define CURL_POLL_NONE 0 -#define CURL_POLL_IN 1 -#define CURL_POLL_OUT 2 -#define CURL_POLL_INOUT 3 -#define CURL_POLL_REMOVE 4 - -#define CURL_SOCKET_TIMEOUT CURL_SOCKET_BAD - -#define CURL_CSELECT_IN 0x01 -#define CURL_CSELECT_OUT 0x02 -#define CURL_CSELECT_ERR 0x04 - -typedef int (*curl_socket_callback)(CURL *easy, /* easy handle */ - curl_socket_t s, /* socket */ - int what, /* see above */ - void *userp, /* private callback - pointer */ - void *socketp); /* private socket - pointer */ -/* - * Name: curl_multi_timer_callback - * - * Desc: Called by libcurl whenever the library detects a change in the - * maximum number of milliseconds the app is allowed to wait before - * curl_multi_socket() or curl_multi_perform() must be called - * (to allow libcurl's timed events to take place). - * - * Returns: The callback should return zero. - */ -typedef int (*curl_multi_timer_callback)(CURLM *multi, /* multi handle */ - long timeout_ms, /* see above */ - void *userp); /* private callback - pointer */ - -CURL_EXTERN CURLMcode CURL_DEPRECATED(7.19.5, "Use curl_multi_socket_action()") -curl_multi_socket(CURLM *multi_handle, curl_socket_t s, int *running_handles); - -CURL_EXTERN CURLMcode curl_multi_socket_action(CURLM *multi_handle, - curl_socket_t s, - int ev_bitmask, - int *running_handles); - -CURL_EXTERN CURLMcode CURL_DEPRECATED(7.19.5, "Use curl_multi_socket_action()") -curl_multi_socket_all(CURLM *multi_handle, int *running_handles); - -#ifndef CURL_ALLOW_OLD_MULTI_SOCKET -/* This macro below was added in 7.16.3 to push users who recompile to use - * the new curl_multi_socket_action() instead of the old curl_multi_socket() - */ -#define curl_multi_socket(x,y,z) curl_multi_socket_action(x,y,0,z) -#endif - -/* - * Name: curl_multi_timeout() - * - * Desc: Returns the maximum number of milliseconds the app is allowed to - * wait before curl_multi_socket() or curl_multi_perform() must be - * called (to allow libcurl's timed events to take place). - * - * Returns: CURLM error code. - */ -CURL_EXTERN CURLMcode curl_multi_timeout(CURLM *multi_handle, - long *milliseconds); - -typedef enum { - /* This is the socket callback function pointer */ - CURLOPT(CURLMOPT_SOCKETFUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 1), - - /* This is the argument passed to the socket callback */ - CURLOPT(CURLMOPT_SOCKETDATA, CURLOPTTYPE_OBJECTPOINT, 2), - - /* set to 1 to enable pipelining for this multi handle */ - CURLOPT(CURLMOPT_PIPELINING, CURLOPTTYPE_LONG, 3), - - /* This is the timer callback function pointer */ - CURLOPT(CURLMOPT_TIMERFUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 4), - - /* This is the argument passed to the timer callback */ - CURLOPT(CURLMOPT_TIMERDATA, CURLOPTTYPE_OBJECTPOINT, 5), - - /* maximum number of entries in the connection cache */ - CURLOPT(CURLMOPT_MAXCONNECTS, CURLOPTTYPE_LONG, 6), - - /* maximum number of (pipelining) connections to one host */ - CURLOPT(CURLMOPT_MAX_HOST_CONNECTIONS, CURLOPTTYPE_LONG, 7), - - /* maximum number of requests in a pipeline */ - CURLOPT(CURLMOPT_MAX_PIPELINE_LENGTH, CURLOPTTYPE_LONG, 8), - - /* a connection with a content-length longer than this - will not be considered for pipelining */ - CURLOPT(CURLMOPT_CONTENT_LENGTH_PENALTY_SIZE, CURLOPTTYPE_OFF_T, 9), - - /* a connection with a chunk length longer than this - will not be considered for pipelining */ - CURLOPT(CURLMOPT_CHUNK_LENGTH_PENALTY_SIZE, CURLOPTTYPE_OFF_T, 10), - - /* a list of site names(+port) that are blocked from pipelining */ - CURLOPT(CURLMOPT_PIPELINING_SITE_BL, CURLOPTTYPE_OBJECTPOINT, 11), - - /* a list of server types that are blocked from pipelining */ - CURLOPT(CURLMOPT_PIPELINING_SERVER_BL, CURLOPTTYPE_OBJECTPOINT, 12), - - /* maximum number of open connections in total */ - CURLOPT(CURLMOPT_MAX_TOTAL_CONNECTIONS, CURLOPTTYPE_LONG, 13), - - /* This is the server push callback function pointer */ - CURLOPT(CURLMOPT_PUSHFUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 14), - - /* This is the argument passed to the server push callback */ - CURLOPT(CURLMOPT_PUSHDATA, CURLOPTTYPE_OBJECTPOINT, 15), - - /* maximum number of concurrent streams to support on a connection */ - CURLOPT(CURLMOPT_MAX_CONCURRENT_STREAMS, CURLOPTTYPE_LONG, 16), - - /* network has changed, adjust caches/connection reuse */ - CURLOPT(CURLMOPT_NETWORK_CHANGED, CURLOPTTYPE_LONG, 17), - - /* This is the notify callback function pointer */ - CURLOPT(CURLMOPT_NOTIFYFUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 18), - - /* This is the argument passed to the notify callback */ - CURLOPT(CURLMOPT_NOTIFYDATA, CURLOPTTYPE_OBJECTPOINT, 19), - - CURLMOPT_LASTENTRY /* the last unused */ -} CURLMoption; - -/* Definition of bits for the CURLMOPT_NETWORK_CHANGED argument: */ - -/* - CURLMNWC_CLEAR_CONNS tells libcurl to prevent further reuse of existing - connections. Connections that are idle will be closed. Ongoing transfers - will continue with the connection they have. */ -#define CURLMNWC_CLEAR_CONNS (1L << 0) - -/* - CURLMNWC_CLEAR_DNS tells libcurl to prevent further reuse of existing - connections. Connections that are idle will be closed. Ongoing transfers - will continue with the connection they have. */ -#define CURLMNWC_CLEAR_DNS (1L << 0) - -/* - * Name: curl_multi_setopt() - * - * Desc: Sets options for the multi handle. - * - * Returns: CURLM error code. - */ -CURL_EXTERN CURLMcode curl_multi_setopt(CURLM *multi_handle, - CURLMoption option, ...); - -/* - * Name: curl_multi_assign() - * - * Desc: This function sets an association in the multi handle between the - * given socket and a private pointer of the application. This is - * (only) useful for curl_multi_socket uses. - * - * Returns: CURLM error code. - */ -CURL_EXTERN CURLMcode curl_multi_assign(CURLM *multi_handle, - curl_socket_t sockfd, void *sockp); - -/* - * Name: curl_multi_get_handles() - * - * Desc: Returns an allocated array holding all handles currently added to - * the multi handle. Marks the final entry with a NULL pointer. If - * there is no easy handle added to the multi handle, this function - * returns an array with the first entry as a NULL pointer. - * - * Returns: NULL on failure, otherwise a CURL **array pointer - */ -CURL_EXTERN CURL **curl_multi_get_handles(CURLM *multi_handle); - -typedef enum { - CURLMINFO_NONE, /* first, never use this */ - /* The number of easy handles currently managed by the multi handle, - * e.g. have been added but not yet removed. */ - CURLMINFO_XFERS_CURRENT = 1, - /* The number of easy handles running, e.g. not done and not queueing. */ - CURLMINFO_XFERS_RUNNING = 2, - /* The number of easy handles waiting to start, e.g. for a connection - * to become available due to limits on parallelism, max connections - * or other factors. */ - CURLMINFO_XFERS_PENDING = 3, - /* The number of easy handles finished, waiting for their results to - * be read via `curl_multi_info_read()`. */ - CURLMINFO_XFERS_DONE = 4, - /* The total number of easy handles added to the multi handle, ever. */ - CURLMINFO_XFERS_ADDED = 5, - - CURLMINFO_LASTENTRY /* the last unused */ -} CURLMinfo_offt; - -/* - * Name: curl_multi_get_offt() - * - * Desc: Retrieves a numeric value for the `CURLMINFO_*` enums. - * - * Returns: CULRM_OK or error when value could not be obtained. - */ -CURL_EXTERN CURLMcode curl_multi_get_offt(CURLM *multi_handle, - CURLMinfo_offt info, - curl_off_t *pvalue); - -/* - * Name: curl_push_callback - * - * Desc: This callback gets called when a new stream is being pushed by the - * server. It approves or denies the new stream. It can also decide - * to completely fail the connection. - * - * Returns: CURL_PUSH_OK, CURL_PUSH_DENY or CURL_PUSH_ERROROUT - */ -#define CURL_PUSH_OK 0 -#define CURL_PUSH_DENY 1 -#define CURL_PUSH_ERROROUT 2 /* added in 7.72.0 */ - -struct curl_pushheaders; /* forward declaration only */ - -CURL_EXTERN char *curl_pushheader_bynum(struct curl_pushheaders *h, - size_t num); -CURL_EXTERN char *curl_pushheader_byname(struct curl_pushheaders *h, - const char *name); - -typedef int (*curl_push_callback)(CURL *parent, - CURL *easy, - size_t num_headers, - struct curl_pushheaders *headers, - void *userp); - -/* - * Name: curl_multi_waitfds() - * - * Desc: Ask curl for fds for polling. The app can use these to poll on. - * We want curl_multi_perform() called as soon as one of them are - * ready. Passing zero size allows to get just a number of fds. - * - * Returns: CURLMcode type, general multi error code. - */ -CURL_EXTERN CURLMcode curl_multi_waitfds(CURLM *multi, - struct curl_waitfd *ufds, - unsigned int size, - unsigned int *fd_count); - -/* - * Notifications dispatched by a multi handle, when enabled. - */ -#define CURLMNOTIFY_INFO_READ 0 -#define CURLMNOTIFY_EASY_DONE 1 - -/* - * Callback to install via CURLMOPT_NOTIFYFUNCTION. - */ -typedef void (*curl_notify_callback)(CURLM *multi, - unsigned int notification, - CURL *easy, - void *user_data); - -CURL_EXTERN CURLMcode curl_multi_notify_disable(CURLM *multi, - unsigned int notification); - -CURL_EXTERN CURLMcode curl_multi_notify_enable(CURLM *multi, - unsigned int notification); - -#ifdef __cplusplus -} /* end of extern "C" */ -#endif - -#endif diff --git a/libs/curl/include/curl/options.h b/libs/curl/include/curl/options.h deleted file mode 100644 index 835a722e9..000000000 --- a/libs/curl/include/curl/options.h +++ /dev/null @@ -1,70 +0,0 @@ -#ifndef CURLINC_OPTIONS_H -#define CURLINC_OPTIONS_H -/*************************************************************************** - * _ _ ____ _ - * Project ___| | | | _ \| | - * / __| | | | |_) | | - * | (__| |_| | _ <| |___ - * \___|\___/|_| \_\_____| - * - * Copyright (C) Daniel Stenberg, , et al. - * - * This software is licensed as described in the file COPYING, which - * you should have received as part of this distribution. The terms - * are also available at https://curl.se/docs/copyright.html. - * - * You may opt to use, copy, modify, merge, publish, distribute and/or sell - * copies of the Software, and permit persons to whom the Software is - * furnished to do so, under the terms of the COPYING file. - * - * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY - * KIND, either express or implied. - * - * SPDX-License-Identifier: curl - * - ***************************************************************************/ - -#ifdef __cplusplus -extern "C" { -#endif - -typedef enum { - CURLOT_LONG, /* long (a range of values) */ - CURLOT_VALUES, /* (a defined set or bitmask) */ - CURLOT_OFF_T, /* curl_off_t (a range of values) */ - CURLOT_OBJECT, /* pointer (void *) */ - CURLOT_STRING, /* (char * to null-terminated buffer) */ - CURLOT_SLIST, /* (struct curl_slist *) */ - CURLOT_CBPTR, /* (void * passed as-is to a callback) */ - CURLOT_BLOB, /* blob (struct curl_blob *) */ - CURLOT_FUNCTION /* function pointer */ -} curl_easytype; - -/* Flag bits */ - -/* "alias" means it is provided for old programs to remain functional, - we prefer another name */ -#define CURLOT_FLAG_ALIAS (1 << 0) - -/* The CURLOPTTYPE_* id ranges can still be used to figure out what type/size - to use for curl_easy_setopt() for the given id */ -struct curl_easyoption { - const char *name; - CURLoption id; - curl_easytype type; - unsigned int flags; -}; - -CURL_EXTERN const struct curl_easyoption * -curl_easy_option_by_name(const char *name); - -CURL_EXTERN const struct curl_easyoption * -curl_easy_option_by_id(CURLoption id); - -CURL_EXTERN const struct curl_easyoption * -curl_easy_option_next(const struct curl_easyoption *prev); - -#ifdef __cplusplus -} /* end of extern "C" */ -#endif -#endif /* CURLINC_OPTIONS_H */ diff --git a/libs/curl/include/curl/stdcheaders.h b/libs/curl/include/curl/stdcheaders.h deleted file mode 100644 index 7451aa305..000000000 --- a/libs/curl/include/curl/stdcheaders.h +++ /dev/null @@ -1,35 +0,0 @@ -#ifndef CURLINC_STDCHEADERS_H -#define CURLINC_STDCHEADERS_H -/*************************************************************************** - * _ _ ____ _ - * Project ___| | | | _ \| | - * / __| | | | |_) | | - * | (__| |_| | _ <| |___ - * \___|\___/|_| \_\_____| - * - * Copyright (C) Daniel Stenberg, , et al. - * - * This software is licensed as described in the file COPYING, which - * you should have received as part of this distribution. The terms - * are also available at https://curl.se/docs/copyright.html. - * - * You may opt to use, copy, modify, merge, publish, distribute and/or sell - * copies of the Software, and permit persons to whom the Software is - * furnished to do so, under the terms of the COPYING file. - * - * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY - * KIND, either express or implied. - * - * SPDX-License-Identifier: curl - * - ***************************************************************************/ - -#include - -size_t fread(void *, size_t, size_t, FILE *); -size_t fwrite(const void *, size_t, size_t, FILE *); - -int strcasecmp(const char *, const char *); -int strncasecmp(const char *, const char *, size_t); - -#endif /* CURLINC_STDCHEADERS_H */ diff --git a/libs/curl/include/curl/system.h b/libs/curl/include/curl/system.h deleted file mode 100644 index a5b3e9eba..000000000 --- a/libs/curl/include/curl/system.h +++ /dev/null @@ -1,399 +0,0 @@ -#ifndef CURLINC_SYSTEM_H -#define CURLINC_SYSTEM_H -/*************************************************************************** - * _ _ ____ _ - * Project ___| | | | _ \| | - * / __| | | | |_) | | - * | (__| |_| | _ <| |___ - * \___|\___/|_| \_\_____| - * - * Copyright (C) Daniel Stenberg, , et al. - * - * This software is licensed as described in the file COPYING, which - * you should have received as part of this distribution. The terms - * are also available at https://curl.se/docs/copyright.html. - * - * You may opt to use, copy, modify, merge, publish, distribute and/or sell - * copies of the Software, and permit persons to whom the Software is - * furnished to do so, under the terms of the COPYING file. - * - * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY - * KIND, either express or implied. - * - * SPDX-License-Identifier: curl - * - ***************************************************************************/ - -/* - * Try to keep one section per platform, compiler and architecture, otherwise, - * if an existing section is reused for a different one and later on the - * original is adjusted, probably the piggybacking one can be adversely - * changed. - * - * In order to differentiate between platforms/compilers/architectures use - * only compiler built-in predefined preprocessor symbols. - * - * curl_off_t - * ---------- - * - * For any given platform/compiler curl_off_t MUST be typedef'ed to a 64-bit - * wide signed integral data type. The width of this data type must remain - * constant and independent of any possible large file support settings. - * - * As a general rule, curl_off_t shall not be mapped to off_t. This rule shall - * only be violated if off_t is the only 64-bit data type available and the - * size of off_t is independent of large file support settings. Keep your - * build on the safe side avoiding an off_t gating. If you have a 64-bit - * off_t then take for sure that another 64-bit data type exists, dig deeper - * and you will find it. - * - */ - -#ifdef __DJGPP__ -# define CURL_TYPEOF_CURL_OFF_T long long -# define CURL_FORMAT_CURL_OFF_T "lld" -# define CURL_FORMAT_CURL_OFF_TU "llu" -# define CURL_SUFFIX_CURL_OFF_T LL -# define CURL_SUFFIX_CURL_OFF_TU ULL -# define CURL_TYPEOF_CURL_SOCKLEN_T int - -#elif defined(__BORLANDC__) -# define CURL_TYPEOF_CURL_OFF_T __int64 -# define CURL_FORMAT_CURL_OFF_T "I64d" -# define CURL_FORMAT_CURL_OFF_TU "I64u" -# define CURL_SUFFIX_CURL_OFF_T i64 -# define CURL_SUFFIX_CURL_OFF_TU ui64 -# define CURL_TYPEOF_CURL_SOCKLEN_T int - -#elif defined(__POCC__) -# ifdef _MSC_VER -# define CURL_TYPEOF_CURL_OFF_T __int64 -# define CURL_FORMAT_CURL_OFF_T "I64d" -# define CURL_FORMAT_CURL_OFF_TU "I64u" -# define CURL_SUFFIX_CURL_OFF_T i64 -# define CURL_SUFFIX_CURL_OFF_TU ui64 -# else -# define CURL_TYPEOF_CURL_OFF_T long long -# define CURL_FORMAT_CURL_OFF_T "lld" -# define CURL_FORMAT_CURL_OFF_TU "llu" -# define CURL_SUFFIX_CURL_OFF_T LL -# define CURL_SUFFIX_CURL_OFF_TU ULL -# endif -# define CURL_TYPEOF_CURL_SOCKLEN_T int - -#elif defined(__LCC__) -# ifdef __MCST__ /* MCST eLbrus Compiler Collection */ -# define CURL_TYPEOF_CURL_OFF_T long -# define CURL_FORMAT_CURL_OFF_T "ld" -# define CURL_FORMAT_CURL_OFF_TU "lu" -# define CURL_SUFFIX_CURL_OFF_T L -# define CURL_SUFFIX_CURL_OFF_TU UL -# define CURL_TYPEOF_CURL_SOCKLEN_T socklen_t -# define CURL_PULL_SYS_TYPES_H 1 -# define CURL_PULL_SYS_SOCKET_H 1 -# else /* Local (or Little) C Compiler */ -# define CURL_TYPEOF_CURL_OFF_T long -# define CURL_FORMAT_CURL_OFF_T "ld" -# define CURL_FORMAT_CURL_OFF_TU "lu" -# define CURL_SUFFIX_CURL_OFF_T L -# define CURL_SUFFIX_CURL_OFF_TU UL -# define CURL_TYPEOF_CURL_SOCKLEN_T int -# endif - -#elif defined(macintosh) -# include -# if TYPE_LONGLONG -# define CURL_TYPEOF_CURL_OFF_T long long -# define CURL_FORMAT_CURL_OFF_T "lld" -# define CURL_FORMAT_CURL_OFF_TU "llu" -# define CURL_SUFFIX_CURL_OFF_T LL -# define CURL_SUFFIX_CURL_OFF_TU ULL -# else -# define CURL_TYPEOF_CURL_OFF_T long -# define CURL_FORMAT_CURL_OFF_T "ld" -# define CURL_FORMAT_CURL_OFF_TU "lu" -# define CURL_SUFFIX_CURL_OFF_T L -# define CURL_SUFFIX_CURL_OFF_TU UL -# endif -# define CURL_TYPEOF_CURL_SOCKLEN_T unsigned int - -#elif defined(__TANDEM) -# ifndef __LP64 -# define CURL_TYPEOF_CURL_OFF_T long long -# define CURL_FORMAT_CURL_OFF_T "lld" -# define CURL_FORMAT_CURL_OFF_TU "llu" -# define CURL_SUFFIX_CURL_OFF_T LL -# define CURL_SUFFIX_CURL_OFF_TU ULL -# define CURL_TYPEOF_CURL_SOCKLEN_T int -# else -# define CURL_TYPEOF_CURL_OFF_T long -# define CURL_FORMAT_CURL_OFF_T "ld" -# define CURL_FORMAT_CURL_OFF_TU "lu" -# define CURL_SUFFIX_CURL_OFF_T L -# define CURL_SUFFIX_CURL_OFF_TU UL -# define CURL_TYPEOF_CURL_SOCKLEN_T unsigned int -# endif - -#elif defined(UNDER_CE) -# define CURL_TYPEOF_CURL_OFF_T __int64 -# define CURL_FORMAT_CURL_OFF_T "I64d" -# define CURL_FORMAT_CURL_OFF_TU "I64u" -# define CURL_SUFFIX_CURL_OFF_T i64 -# define CURL_SUFFIX_CURL_OFF_TU ui64 -# define CURL_TYPEOF_CURL_SOCKLEN_T int - -#elif defined(__MINGW32__) -# include -# define CURL_TYPEOF_CURL_OFF_T long long -# define CURL_FORMAT_CURL_OFF_T PRId64 -# define CURL_FORMAT_CURL_OFF_TU PRIu64 -# define CURL_SUFFIX_CURL_OFF_T LL -# define CURL_SUFFIX_CURL_OFF_TU ULL -# define CURL_TYPEOF_CURL_SOCKLEN_T int -# define CURL_PULL_SYS_TYPES_H 1 - -#elif defined(__VMS) -# ifdef __VAX -# define CURL_TYPEOF_CURL_OFF_T long -# define CURL_FORMAT_CURL_OFF_T "ld" -# define CURL_FORMAT_CURL_OFF_TU "lu" -# define CURL_SUFFIX_CURL_OFF_T L -# define CURL_SUFFIX_CURL_OFF_TU UL -# else -# define CURL_TYPEOF_CURL_OFF_T long long -# define CURL_FORMAT_CURL_OFF_T "lld" -# define CURL_FORMAT_CURL_OFF_TU "llu" -# define CURL_SUFFIX_CURL_OFF_T LL -# define CURL_SUFFIX_CURL_OFF_TU ULL -# endif -# define CURL_TYPEOF_CURL_SOCKLEN_T unsigned int - -#elif defined(__OS400__) -# define CURL_TYPEOF_CURL_OFF_T long long -# define CURL_FORMAT_CURL_OFF_T "lld" -# define CURL_FORMAT_CURL_OFF_TU "llu" -# define CURL_SUFFIX_CURL_OFF_T LL -# define CURL_SUFFIX_CURL_OFF_TU ULL -# define CURL_TYPEOF_CURL_SOCKLEN_T socklen_t -# define CURL_PULL_SYS_TYPES_H 1 -# define CURL_PULL_SYS_SOCKET_H 1 - -#elif defined(__MVS__) -# ifdef _LONG_LONG -# define CURL_TYPEOF_CURL_OFF_T long long -# define CURL_FORMAT_CURL_OFF_T "lld" -# define CURL_FORMAT_CURL_OFF_TU "llu" -# define CURL_SUFFIX_CURL_OFF_T LL -# define CURL_SUFFIX_CURL_OFF_TU ULL -# else /* _LP64 and default */ -# define CURL_TYPEOF_CURL_OFF_T long -# define CURL_FORMAT_CURL_OFF_T "ld" -# define CURL_FORMAT_CURL_OFF_TU "lu" -# define CURL_SUFFIX_CURL_OFF_T L -# define CURL_SUFFIX_CURL_OFF_TU UL -# endif -# define CURL_TYPEOF_CURL_SOCKLEN_T socklen_t -# define CURL_PULL_SYS_TYPES_H 1 -# define CURL_PULL_SYS_SOCKET_H 1 - -#elif defined(__370__) -# if defined(__IBMC__) || defined(__IBMCPP__) -# ifdef _LONG_LONG -# define CURL_TYPEOF_CURL_OFF_T long long -# define CURL_FORMAT_CURL_OFF_T "lld" -# define CURL_FORMAT_CURL_OFF_TU "llu" -# define CURL_SUFFIX_CURL_OFF_T LL -# define CURL_SUFFIX_CURL_OFF_TU ULL -# else /* _LP64 and default */ -# define CURL_TYPEOF_CURL_OFF_T long -# define CURL_FORMAT_CURL_OFF_T "ld" -# define CURL_FORMAT_CURL_OFF_TU "lu" -# define CURL_SUFFIX_CURL_OFF_T L -# define CURL_SUFFIX_CURL_OFF_TU UL -# endif -# define CURL_TYPEOF_CURL_SOCKLEN_T socklen_t -# define CURL_PULL_SYS_TYPES_H 1 -# define CURL_PULL_SYS_SOCKET_H 1 -# endif - -#elif defined(TPF) -# define CURL_TYPEOF_CURL_OFF_T long -# define CURL_FORMAT_CURL_OFF_T "ld" -# define CURL_FORMAT_CURL_OFF_TU "lu" -# define CURL_SUFFIX_CURL_OFF_T L -# define CURL_SUFFIX_CURL_OFF_TU UL -# define CURL_TYPEOF_CURL_SOCKLEN_T int - -#elif defined(__TINYC__) /* also known as tcc */ -# define CURL_TYPEOF_CURL_OFF_T long long -# define CURL_FORMAT_CURL_OFF_T "lld" -# define CURL_FORMAT_CURL_OFF_TU "llu" -# define CURL_SUFFIX_CURL_OFF_T LL -# define CURL_SUFFIX_CURL_OFF_TU ULL -# define CURL_TYPEOF_CURL_SOCKLEN_T socklen_t -# define CURL_PULL_SYS_TYPES_H 1 -# define CURL_PULL_SYS_SOCKET_H 1 - -#elif defined(__SUNPRO_C) || defined(__SUNPRO_CC) /* Oracle Solaris Studio */ -# if !defined(__LP64) && (defined(__ILP32) || \ - defined(__i386) || \ - defined(__sparcv8) || \ - defined(__sparcv8plus)) -# define CURL_TYPEOF_CURL_OFF_T long long -# define CURL_FORMAT_CURL_OFF_T "lld" -# define CURL_FORMAT_CURL_OFF_TU "llu" -# define CURL_SUFFIX_CURL_OFF_T LL -# define CURL_SUFFIX_CURL_OFF_TU ULL -# elif defined(__LP64) || \ - defined(__amd64) || defined(__sparcv9) -# define CURL_TYPEOF_CURL_OFF_T long -# define CURL_FORMAT_CURL_OFF_T "ld" -# define CURL_FORMAT_CURL_OFF_TU "lu" -# define CURL_SUFFIX_CURL_OFF_T L -# define CURL_SUFFIX_CURL_OFF_TU UL -# endif -# define CURL_TYPEOF_CURL_SOCKLEN_T socklen_t -# define CURL_PULL_SYS_TYPES_H 1 -# define CURL_PULL_SYS_SOCKET_H 1 - -#elif defined(__xlc__) /* IBM xlc compiler */ -# ifndef _LP64 -# define CURL_TYPEOF_CURL_OFF_T long long -# define CURL_FORMAT_CURL_OFF_T "lld" -# define CURL_FORMAT_CURL_OFF_TU "llu" -# define CURL_SUFFIX_CURL_OFF_T LL -# define CURL_SUFFIX_CURL_OFF_TU ULL -# else -# define CURL_TYPEOF_CURL_OFF_T long -# define CURL_FORMAT_CURL_OFF_T "ld" -# define CURL_FORMAT_CURL_OFF_TU "lu" -# define CURL_SUFFIX_CURL_OFF_T L -# define CURL_SUFFIX_CURL_OFF_TU UL -# endif -# define CURL_TYPEOF_CURL_SOCKLEN_T socklen_t -# define CURL_PULL_SYS_TYPES_H 1 -# define CURL_PULL_SYS_SOCKET_H 1 - -#elif defined(__hpux) /* HP aCC compiler */ -# ifndef _LP64 -# define CURL_TYPEOF_CURL_OFF_T long long -# define CURL_FORMAT_CURL_OFF_T "lld" -# define CURL_FORMAT_CURL_OFF_TU "llu" -# define CURL_SUFFIX_CURL_OFF_T LL -# define CURL_SUFFIX_CURL_OFF_TU ULL -# else -# define CURL_TYPEOF_CURL_OFF_T long -# define CURL_FORMAT_CURL_OFF_T "ld" -# define CURL_FORMAT_CURL_OFF_TU "lu" -# define CURL_SUFFIX_CURL_OFF_T L -# define CURL_SUFFIX_CURL_OFF_TU UL -# endif -# define CURL_TYPEOF_CURL_SOCKLEN_T socklen_t -# define CURL_PULL_SYS_TYPES_H 1 -# define CURL_PULL_SYS_SOCKET_H 1 - -/* ===================================== */ -/* KEEP MSVC THE PENULTIMATE ENTRY */ -/* ===================================== */ - -#elif defined(_MSC_VER) -# if (_MSC_VER >= 1800) -# include -# define CURL_FORMAT_CURL_OFF_T PRId64 -# define CURL_FORMAT_CURL_OFF_TU PRIu64 -# else -# define CURL_FORMAT_CURL_OFF_T "I64d" -# define CURL_FORMAT_CURL_OFF_TU "I64u" -# endif -# define CURL_TYPEOF_CURL_OFF_T __int64 -# define CURL_SUFFIX_CURL_OFF_T i64 -# define CURL_SUFFIX_CURL_OFF_TU ui64 -# define CURL_TYPEOF_CURL_SOCKLEN_T int - -/* ===================================== */ -/* KEEP GENERIC GCC THE LAST ENTRY */ -/* ===================================== */ - -#elif defined(__GNUC__) && !defined(_SCO_DS) -# if !defined(__LP64__) && \ - (defined(__ILP32__) || defined(__i386__) || defined(__hppa__) || \ - defined(__ppc__) || defined(__powerpc__) || defined(__arm__) || \ - defined(__sparc__) || defined(__mips__) || defined(__sh__) || \ - defined(__XTENSA__) || \ - (defined(__SIZEOF_LONG__) && __SIZEOF_LONG__ == 4) || \ - (defined(__LONG_MAX__) && __LONG_MAX__ == 2147483647L)) -# define CURL_TYPEOF_CURL_OFF_T long long -# define CURL_FORMAT_CURL_OFF_T "lld" -# define CURL_FORMAT_CURL_OFF_TU "llu" -# define CURL_SUFFIX_CURL_OFF_T LL -# define CURL_SUFFIX_CURL_OFF_TU ULL -# if (__GNUC__ >= 4) || \ - ((__GNUC__ == 3) && defined(__GNUC_MINOR__) && (__GNUC_MINOR__ >= 4)) -# define CURL_POPCOUNT64(x) __builtin_popcountll(x) -# define CURL_CTZ64(x) __builtin_ctzll(x) -# endif -# elif defined(__LP64__) || \ - defined(__x86_64__) || defined(__ppc64__) || defined(__sparc64__) || \ - defined(__e2k__) || \ - (defined(__SIZEOF_LONG__) && __SIZEOF_LONG__ == 8) || \ - (defined(__LONG_MAX__) && __LONG_MAX__ == 9223372036854775807L) -# define CURL_TYPEOF_CURL_OFF_T long -# define CURL_FORMAT_CURL_OFF_T "ld" -# define CURL_FORMAT_CURL_OFF_TU "lu" -# define CURL_SUFFIX_CURL_OFF_T L -# define CURL_SUFFIX_CURL_OFF_TU UL -# if (__GNUC__ >= 4) || \ - ((__GNUC__ == 3) && defined(__GNUC_MINOR__) && (__GNUC_MINOR__ >= 4)) -# define CURL_POPCOUNT64(x) __builtin_popcountl(x) -# define CURL_CTZ64(x) __builtin_ctzl(x) -# endif -# endif -# define CURL_TYPEOF_CURL_SOCKLEN_T socklen_t -# define CURL_PULL_SYS_TYPES_H 1 -# define CURL_PULL_SYS_SOCKET_H 1 - -#else -/* generic "safe guess" on old 32-bit style */ -# define CURL_TYPEOF_CURL_OFF_T long long -# define CURL_FORMAT_CURL_OFF_T "lld" -# define CURL_FORMAT_CURL_OFF_TU "llu" -# define CURL_SUFFIX_CURL_OFF_T LL -# define CURL_SUFFIX_CURL_OFF_TU ULL -# define CURL_TYPEOF_CURL_SOCKLEN_T int -#endif - -#ifdef _AIX -/* AIX needs */ -#define CURL_PULL_SYS_POLL_H -#endif - -/* CURL_PULL_SYS_TYPES_H is defined above when inclusion of header file */ -/* sys/types.h is required here to properly make type definitions below. */ -#ifdef CURL_PULL_SYS_TYPES_H -# include -#endif - -/* CURL_PULL_SYS_SOCKET_H is defined above when inclusion of header file */ -/* sys/socket.h is required here to properly make type definitions below. */ -#ifdef CURL_PULL_SYS_SOCKET_H -# include -#endif - -/* CURL_PULL_SYS_POLL_H is defined above when inclusion of header file */ -/* sys/poll.h is required here to properly make type definitions below. */ -#ifdef CURL_PULL_SYS_POLL_H -# include -#endif - -/* Data type definition of curl_socklen_t. */ -#ifdef CURL_TYPEOF_CURL_SOCKLEN_T - typedef CURL_TYPEOF_CURL_SOCKLEN_T curl_socklen_t; -#endif - -/* Data type definition of curl_off_t. */ - -#ifdef CURL_TYPEOF_CURL_OFF_T - typedef CURL_TYPEOF_CURL_OFF_T curl_off_t; -#endif - -#endif /* CURLINC_SYSTEM_H */ diff --git a/libs/curl/include/curl/typecheck-gcc.h b/libs/curl/include/curl/typecheck-gcc.h deleted file mode 100644 index 0642afd91..000000000 --- a/libs/curl/include/curl/typecheck-gcc.h +++ /dev/null @@ -1,957 +0,0 @@ -#ifndef CURLINC_TYPECHECK_GCC_H -#define CURLINC_TYPECHECK_GCC_H -/*************************************************************************** - * _ _ ____ _ - * Project ___| | | | _ \| | - * / __| | | | |_) | | - * | (__| |_| | _ <| |___ - * \___|\___/|_| \_\_____| - * - * Copyright (C) Daniel Stenberg, , et al. - * - * This software is licensed as described in the file COPYING, which - * you should have received as part of this distribution. The terms - * are also available at https://curl.se/docs/copyright.html. - * - * You may opt to use, copy, modify, merge, publish, distribute and/or sell - * copies of the Software, and permit persons to whom the Software is - * furnished to do so, under the terms of the COPYING file. - * - * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY - * KIND, either express or implied. - * - * SPDX-License-Identifier: curl - * - ***************************************************************************/ - -/* wraps curl_easy_setopt() with type checking */ - -/* To add a new kind of warning, add an - * if(curlcheck_sometype_option(_curl_opt)) - * if(!curlcheck_sometype(value)) - * Wcurl_easy_setopt_err_sometype(); - * block and define curlcheck_sometype_option, curlcheck_sometype and - * Wcurl_easy_setopt_err_sometype below - * - * NOTE: We use two nested 'if' statements here instead of the && operator, in - * order to work around gcc bug #32061. It affects only gcc 4.3.x/4.4.x - * when compiling with -Wlogical-op. - * - * To add an option that uses the same type as an existing option, you will - * just need to extend the appropriate _curl_*_option macro - */ - -#define curl_easy_setopt(handle, option, value) \ - __extension__({ \ - if(__builtin_constant_p(option)) { \ - CURL_IGNORE_DEPRECATION( \ - if(curlcheck_long_option(option)) \ - if(!curlcheck_long(value)) \ - Wcurl_easy_setopt_err_long(); \ - if(curlcheck_off_t_option(option)) \ - if(!curlcheck_off_t(value)) \ - Wcurl_easy_setopt_err_curl_off_t(); \ - if(curlcheck_string_option(option)) \ - if(!curlcheck_string(value)) \ - Wcurl_easy_setopt_err_string(); \ - if((option) == CURLOPT_PRIVATE) { } \ - if(curlcheck_write_cb_option(option)) \ - if(!curlcheck_write_cb(value)) \ - Wcurl_easy_setopt_err_write_callback(); \ - if(curlcheck_curl_option(option)) \ - if(!curlcheck_curl(value)) \ - Wcurl_easy_setopt_err_curl(); \ - if((option) == CURLOPT_RESOLVER_START_FUNCTION) \ - if(!curlcheck_resolver_start_callback(value)) \ - Wcurl_easy_setopt_err_resolver_start_callback(); \ - if((option) == CURLOPT_READFUNCTION) \ - if(!curlcheck_read_cb(value)) \ - Wcurl_easy_setopt_err_read_cb(); \ - if((option) == CURLOPT_IOCTLFUNCTION) \ - if(!curlcheck_ioctl_cb(value)) \ - Wcurl_easy_setopt_err_ioctl_cb(); \ - if((option) == CURLOPT_SOCKOPTFUNCTION) \ - if(!curlcheck_sockopt_cb(value)) \ - Wcurl_easy_setopt_err_sockopt_cb(); \ - if((option) == CURLOPT_OPENSOCKETFUNCTION) \ - if(!curlcheck_opensocket_cb(value)) \ - Wcurl_easy_setopt_err_opensocket_cb(); \ - if((option) == CURLOPT_PROGRESSFUNCTION) \ - if(!curlcheck_progress_cb(value)) \ - Wcurl_easy_setopt_err_progress_cb(); \ - if((option) == CURLOPT_XFERINFOFUNCTION) \ - if(!curlcheck_xferinfo_cb(value)) \ - Wcurl_easy_setopt_err_xferinfo_cb(); \ - if((option) == CURLOPT_DEBUGFUNCTION) \ - if(!curlcheck_debug_cb(value)) \ - Wcurl_easy_setopt_err_debug_cb(); \ - if((option) == CURLOPT_SSL_CTX_FUNCTION) \ - if(!curlcheck_ssl_ctx_cb(value)) \ - Wcurl_easy_setopt_err_ssl_ctx_cb(); \ - if(curlcheck_conv_cb_option(option)) \ - if(!curlcheck_conv_cb(value)) \ - Wcurl_easy_setopt_err_conv_cb(); \ - if((option) == CURLOPT_SEEKFUNCTION) \ - if(!curlcheck_seek_cb(value)) \ - Wcurl_easy_setopt_err_seek_cb(); \ - if((option) == CURLOPT_CHUNK_BGN_FUNCTION) \ - if(!curlcheck_chunk_bgn_cb(value)) \ - Wcurl_easy_setopt_err_chunk_bgn_cb(); \ - if((option) == CURLOPT_CHUNK_END_FUNCTION) \ - if(!curlcheck_chunk_end_cb(value)) \ - Wcurl_easy_setopt_err_chunk_end_cb(); \ - if((option) == CURLOPT_CLOSESOCKETFUNCTION) \ - if(!curlcheck_close_socket_cb(value)) \ - Wcurl_easy_setopt_err_close_socket_cb(); \ - if((option) == CURLOPT_FNMATCH_FUNCTION) \ - if(!curlcheck_fnmatch_cb(value)) \ - Wcurl_easy_setopt_err_fnmatch_cb(); \ - if((option) == CURLOPT_HSTSREADFUNCTION) \ - if(!curlcheck_hstsread_cb(value)) \ - Wcurl_easy_setopt_err_hstsread_cb(); \ - if((option) == CURLOPT_HSTSWRITEFUNCTION) \ - if(!curlcheck_hstswrite_cb(value)) \ - Wcurl_easy_setopt_err_hstswrite_cb(); \ - if((option) == CURLOPT_SSH_HOSTKEYFUNCTION) \ - if(!curlcheck_ssh_hostkey_cb(value)) \ - Wcurl_easy_setopt_err_ssh_hostkey_cb(); \ - if((option) == CURLOPT_SSH_KEYFUNCTION) \ - if(!curlcheck_ssh_key_cb(value)) \ - Wcurl_easy_setopt_err_ssh_key_cb(); \ - if((option) == CURLOPT_INTERLEAVEFUNCTION) \ - if(!curlcheck_interleave_cb(value)) \ - Wcurl_easy_setopt_err_interleave_cb(); \ - if((option) == CURLOPT_PREREQFUNCTION) \ - if(!curlcheck_prereq_cb(value)) \ - Wcurl_easy_setopt_err_prereq_cb(); \ - if((option) == CURLOPT_TRAILERFUNCTION) \ - if(!curlcheck_trailer_cb(value)) \ - Wcurl_easy_setopt_err_trailer_cb(); \ - if(curlcheck_cb_data_option(option)) \ - if(!curlcheck_cb_data(value)) \ - Wcurl_easy_setopt_err_cb_data(); \ - if((option) == CURLOPT_ERRORBUFFER) \ - if(!curlcheck_error_buffer(value)) \ - Wcurl_easy_setopt_err_error_buffer(); \ - if((option) == CURLOPT_CURLU) \ - if(!curlcheck_ptr((value), CURLU)) \ - Wcurl_easy_setopt_err_curlu(); \ - if((option) == CURLOPT_STDERR) \ - if(!curlcheck_FILE(value)) \ - Wcurl_easy_setopt_err_FILE(); \ - if(curlcheck_postfields_option(option)) \ - if(!curlcheck_postfields(value)) \ - Wcurl_easy_setopt_err_postfields(); \ - if((option) == CURLOPT_HTTPPOST) \ - if(!curlcheck_arr((value), struct curl_httppost)) \ - Wcurl_easy_setopt_err_curl_httpost(); \ - if((option) == CURLOPT_MIMEPOST) \ - if(!curlcheck_ptr((value), curl_mime)) \ - Wcurl_easy_setopt_err_curl_mimepost(); \ - if(curlcheck_slist_option(option)) \ - if(!curlcheck_arr((value), struct curl_slist)) \ - Wcurl_easy_setopt_err_curl_slist(); \ - if((option) == CURLOPT_SHARE) \ - if(!curlcheck_ptr((value), CURLSH)) \ - Wcurl_easy_setopt_err_CURLSH(); \ - ) \ - } \ - curl_easy_setopt(handle, option, value); \ - }) - -/* wraps curl_easy_getinfo() with type checking */ -#define curl_easy_getinfo(handle, info, arg) \ - __extension__({ \ - if(__builtin_constant_p(info)) { \ - CURL_IGNORE_DEPRECATION( \ - if(curlcheck_string_info(info)) \ - if(!curlcheck_arr((arg), char *)) \ - Wcurl_easy_getinfo_err_string(); \ - if(curlcheck_long_info(info)) \ - if(!curlcheck_arr((arg), long)) \ - Wcurl_easy_getinfo_err_long(); \ - if(curlcheck_double_info(info)) \ - if(!curlcheck_arr((arg), double)) \ - Wcurl_easy_getinfo_err_double(); \ - if(curlcheck_slist_info(info)) \ - if(!curlcheck_arr((arg), struct curl_slist *)) \ - Wcurl_easy_getinfo_err_curl_slist(); \ - if(curlcheck_tlssessioninfo_info(info)) \ - if(!curlcheck_arr((arg), struct curl_tlssessioninfo *)) \ - Wcurl_easy_getinfo_err_curl_tlssessioninfo(); \ - if(curlcheck_certinfo_info(info)) \ - if(!curlcheck_arr((arg), struct curl_certinfo *)) \ - Wcurl_easy_getinfo_err_curl_certinfo(); \ - if(curlcheck_socket_info(info)) \ - if(!curlcheck_arr((arg), curl_socket_t)) \ - Wcurl_easy_getinfo_err_curl_socket(); \ - if(curlcheck_off_t_info(info)) \ - if(!curlcheck_arr((arg), curl_off_t)) \ - Wcurl_easy_getinfo_err_curl_off_t(); \ - ) \ - } \ - curl_easy_getinfo(handle, info, arg); \ - }) - -#define curl_multi_setopt(handle, option, value) \ - __extension__({ \ - if(__builtin_constant_p(option)) { \ - if(curlcheck_long_option(option)) \ - if(!curlcheck_long(value)) \ - Wcurl_multi_setopt_err_long(); \ - if(curlcheck_off_t_option(option)) \ - if(!curlcheck_off_t(value)) \ - Wcurl_multi_setopt_err_curl_off_t(); \ - if(curlcheck_multicb_data_option(option)) \ - if(!curlcheck_cb_data(value)) \ - Wcurl_multi_setopt_err_cb_data(); \ - if(curlcheck_charpp_option(option)) \ - if(!curlcheck_ptrptr(value, char)) \ - Wcurl_multi_setopt_err_charpp(); \ - if((option) == CURLMOPT_NOTIFYFUNCTION) \ - if(!curlcheck_multinotify_cb(value)) \ - Wcurl_multi_setopt_err_notifycb(); \ - if((option) == CURLMOPT_PUSHFUNCTION) \ - if(!curlcheck_multipush_cb(value)) \ - Wcurl_multi_setopt_err_pushcb(); \ - if((option) == CURLMOPT_SOCKETFUNCTION) \ - if(!curlcheck_multisocket_cb(value)) \ - Wcurl_multi_setopt_err_socketcb(); \ - if((option) == CURLMOPT_TIMERFUNCTION) \ - if(!curlcheck_multitimer_cb(value)) \ - Wcurl_multi_setopt_err_timercb(); \ - } \ - curl_multi_setopt(handle, option, value); \ - }) - -/* evaluates to true if the option takes a data argument to pass to a - callback */ -#define curlcheck_multicb_data_option(option) \ - ((option) == CURLMOPT_NOTIFYDATA || \ - (option) == CURLMOPT_PUSHDATA || \ - (option) == CURLMOPT_SOCKETDATA || \ - (option) == CURLMOPT_TIMERDATA || \ - 0) - -/* evaluates to true if the option takes a char ** argument */ -#define curlcheck_charpp_option(option) \ - ((option) == CURLMOPT_PIPELINING_SERVER_BL || \ - (option) == CURLMOPT_PIPELINING_SITE_BL || \ - 0) - -/* evaluates to true if expr is of type curl_multi_timer_callback */ -#define curlcheck_multitimer_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), curl_multi_timer_callback)) - -/* evaluates to true if expr is of type curl_socket_callback */ -#define curlcheck_multisocket_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), curl_socket_callback)) - -/* evaluates to true if expr is of type curl_push_callback */ -#define curlcheck_multipush_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), curl_push_callback)) - -/* evaluates to true if expr is of type curl_push_callback */ -#define curlcheck_multinotify_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), curl_notify_callback)) - -/* - * For now, just make sure that the functions are called with three arguments - */ -#define curl_share_setopt(share,opt,param) curl_share_setopt(share,opt,param) - -/* the actual warnings, triggered by calling the Wcurl_easy_setopt_err* - * functions */ - -/* To define a new warning, use _CURL_WARNING(identifier, "message") */ -#define CURLWARNING(id, message) \ - static void __attribute__((__warning__(message))) \ - __attribute__((__unused__)) __attribute__((__noinline__)) \ - id(void) { __asm__(""); } - -CURLWARNING(Wcurl_multi_setopt_err_long, - "curl_multi_setopt expects a long argument") -CURLWARNING(Wcurl_multi_setopt_err_curl_off_t, - "curl_multi_setopt expects a curl_off_t argument") -CURLWARNING(Wcurl_multi_setopt_err_cb_data, - "curl_multi_setopt expects a 'void *' argument") -CURLWARNING(Wcurl_multi_setopt_err_charpp, - "curl_multi_setopt expects a 'char **' argument") -CURLWARNING(Wcurl_multi_setopt_err_pushcb, - "curl_multi_setopt expects a curl_push_callback argument") -CURLWARNING(Wcurl_multi_setopt_err_notifycb, - "curl_multi_setopt expects a curl_notify_callback argument") -CURLWARNING(Wcurl_multi_setopt_err_socketcb, - "curl_multi_setopt expects a curl_socket_callback argument") -CURLWARNING(Wcurl_multi_setopt_err_timercb, - "curl_multi_setopt expects a curl_multi_timer_callback argument") - -CURLWARNING(Wcurl_easy_setopt_err_long, - "curl_easy_setopt expects a long argument") -CURLWARNING(Wcurl_easy_setopt_err_curl_off_t, - "curl_easy_setopt expects a curl_off_t argument") -CURLWARNING(Wcurl_easy_setopt_err_string, - "curl_easy_setopt expects a " - "string ('char *' or char[]) argument") -CURLWARNING(Wcurl_easy_setopt_err_write_callback, - "curl_easy_setopt expects a curl_write_callback argument") -CURLWARNING(Wcurl_easy_setopt_err_resolver_start_callback, - "curl_easy_setopt expects a " - "curl_resolver_start_callback argument") -CURLWARNING(Wcurl_easy_setopt_err_read_cb, - "curl_easy_setopt expects a curl_read_callback argument") -CURLWARNING(Wcurl_easy_setopt_err_ioctl_cb, - "curl_easy_setopt expects a curl_ioctl_callback argument") -CURLWARNING(Wcurl_easy_setopt_err_sockopt_cb, - "curl_easy_setopt expects a curl_sockopt_callback argument") -CURLWARNING(Wcurl_easy_setopt_err_opensocket_cb, - "curl_easy_setopt expects a " - "curl_opensocket_callback argument") -CURLWARNING(Wcurl_easy_setopt_err_progress_cb, - "curl_easy_setopt expects a curl_progress_callback argument") -CURLWARNING(Wcurl_easy_setopt_err_xferinfo_cb, - "curl_easy_setopt expects a curl_xferinfo_callback argument") -CURLWARNING(Wcurl_easy_setopt_err_debug_cb, - "curl_easy_setopt expects a curl_debug_callback argument") -CURLWARNING(Wcurl_easy_setopt_err_ssl_ctx_cb, - "curl_easy_setopt expects a curl_ssl_ctx_callback argument") -CURLWARNING(Wcurl_easy_setopt_err_conv_cb, - "curl_easy_setopt expects a curl_conv_callback argument") -CURLWARNING(Wcurl_easy_setopt_err_seek_cb, - "curl_easy_setopt expects a curl_seek_callback argument") -CURLWARNING(Wcurl_easy_setopt_err_cb_data, - "curl_easy_setopt expects a " - "private data pointer as argument") -CURLWARNING(Wcurl_easy_setopt_err_chunk_bgn_cb, - "curl_easy_setopt expects a curl_chunk_bgn_callback argument") -CURLWARNING(Wcurl_easy_setopt_err_chunk_end_cb, - "curl_easy_setopt expects a curl_chunk_end_callback argument") -CURLWARNING(Wcurl_easy_setopt_err_close_socket_cb, - "curl_easy_setopt expects a curl_closesocket_callback argument") -CURLWARNING(Wcurl_easy_setopt_err_fnmatch_cb, - "curl_easy_setopt expects a curl_fnmatch_callback argument") -CURLWARNING(Wcurl_easy_setopt_err_hstsread_cb, - "curl_easy_setopt expects a curl_hstsread_callback argument") -CURLWARNING(Wcurl_easy_setopt_err_hstswrite_cb, - "curl_easy_setopt expects a curl_hstswrite_callback argument") -CURLWARNING(Wcurl_easy_setopt_err_ssh_key_cb, - "curl_easy_setopt expects a curl_sshkeycallback argument") -CURLWARNING(Wcurl_easy_setopt_err_ssh_hostkey_cb, - "curl_easy_setopt expects a curl_sshhostkeycallback argument") -CURLWARNING(Wcurl_easy_setopt_err_interleave_cb, - "curl_easy_setopt expects a curl_interleave_callback argument") -CURLWARNING(Wcurl_easy_setopt_err_prereq_cb, - "curl_easy_setopt expects a curl_prereq_callback argument") -CURLWARNING(Wcurl_easy_setopt_err_trailer_cb, - "curl_easy_setopt expects a curl_trailerfunc_ok argument") -CURLWARNING(Wcurl_easy_setopt_err_error_buffer, - "curl_easy_setopt expects a " - "char buffer of CURL_ERROR_SIZE as argument") -CURLWARNING(Wcurl_easy_setopt_err_curlu, - "curl_easy_setopt expects a 'CURLU *' argument") -CURLWARNING(Wcurl_easy_setopt_err_curl, - "curl_easy_setopt expects a 'CURL *' argument") -CURLWARNING(Wcurl_easy_setopt_err_FILE, - "curl_easy_setopt expects a 'FILE *' argument") -CURLWARNING(Wcurl_easy_setopt_err_postfields, - "curl_easy_setopt expects a 'void *' or 'char *' argument") -CURLWARNING(Wcurl_easy_setopt_err_curl_httpost, - "curl_easy_setopt expects a 'struct curl_httppost *' " - "argument") -CURLWARNING(Wcurl_easy_setopt_err_curl_mimepost, - "curl_easy_setopt expects a 'curl_mime *' " - "argument") -CURLWARNING(Wcurl_easy_setopt_err_curl_slist, - "curl_easy_setopt expects a 'struct curl_slist *' argument") -CURLWARNING(Wcurl_easy_setopt_err_CURLSH, - "curl_easy_setopt expects a CURLSH* argument") -CURLWARNING(Wcurl_easy_getinfo_err_string, - "curl_easy_getinfo expects a pointer to 'char *'") -CURLWARNING(Wcurl_easy_getinfo_err_long, - "curl_easy_getinfo expects a pointer to long") -CURLWARNING(Wcurl_easy_getinfo_err_double, - "curl_easy_getinfo expects a pointer to double") -CURLWARNING(Wcurl_easy_getinfo_err_curl_slist, - "curl_easy_getinfo expects a pointer to 'struct curl_slist *'") -CURLWARNING(Wcurl_easy_getinfo_err_curl_tlssessioninfo, - "curl_easy_getinfo expects a pointer to " - "'struct curl_tlssessioninfo *'") -CURLWARNING(Wcurl_easy_getinfo_err_curl_certinfo, - "curl_easy_getinfo expects a pointer to " - "'struct curl_certinfo *'") -CURLWARNING(Wcurl_easy_getinfo_err_curl_socket, - "curl_easy_getinfo expects a pointer to curl_socket_t") -CURLWARNING(Wcurl_easy_getinfo_err_curl_off_t, - "curl_easy_getinfo expects a pointer to curl_off_t") - -/* groups of curl_easy_setops options that take the same type of argument */ - -/* evaluates to true if option takes a long argument */ -#define curlcheck_long_option(option) \ - (0 < (option) && (option) < CURLOPTTYPE_OBJECTPOINT) - -#define curlcheck_off_t_option(option) \ - (((option) > CURLOPTTYPE_OFF_T) && ((option) < CURLOPTTYPE_BLOB)) - -/* option takes a CURL * argument */ -#define curlcheck_curl_option(option) \ - ((option) == CURLOPT_STREAM_DEPENDS || \ - (option) == CURLOPT_STREAM_DEPENDS_E || \ - 0) - -/* evaluates to true if option takes a char* argument */ -#define curlcheck_string_option(option) \ - ((option) == CURLOPT_ABSTRACT_UNIX_SOCKET || \ - (option) == CURLOPT_ACCEPT_ENCODING || \ - (option) == CURLOPT_ALTSVC || \ - (option) == CURLOPT_CAINFO || \ - (option) == CURLOPT_CAPATH || \ - (option) == CURLOPT_COOKIE || \ - (option) == CURLOPT_COOKIEFILE || \ - (option) == CURLOPT_COOKIEJAR || \ - (option) == CURLOPT_COOKIELIST || \ - (option) == CURLOPT_CRLFILE || \ - (option) == CURLOPT_CUSTOMREQUEST || \ - (option) == CURLOPT_DEFAULT_PROTOCOL || \ - (option) == CURLOPT_DNS_INTERFACE || \ - (option) == CURLOPT_DNS_LOCAL_IP4 || \ - (option) == CURLOPT_DNS_LOCAL_IP6 || \ - (option) == CURLOPT_DNS_SERVERS || \ - (option) == CURLOPT_DOH_URL || \ - (option) == CURLOPT_ECH || \ - (option) == CURLOPT_EGDSOCKET || \ - (option) == CURLOPT_FTP_ACCOUNT || \ - (option) == CURLOPT_FTP_ALTERNATIVE_TO_USER || \ - (option) == CURLOPT_FTPPORT || \ - (option) == CURLOPT_HAPROXY_CLIENT_IP || \ - (option) == CURLOPT_HSTS || \ - (option) == CURLOPT_INTERFACE || \ - (option) == CURLOPT_ISSUERCERT || \ - (option) == CURLOPT_KEYPASSWD || \ - (option) == CURLOPT_KRBLEVEL || \ - (option) == CURLOPT_LOGIN_OPTIONS || \ - (option) == CURLOPT_MAIL_AUTH || \ - (option) == CURLOPT_MAIL_FROM || \ - (option) == CURLOPT_NETRC_FILE || \ - (option) == CURLOPT_NOPROXY || \ - (option) == CURLOPT_PASSWORD || \ - (option) == CURLOPT_PINNEDPUBLICKEY || \ - (option) == CURLOPT_PRE_PROXY || \ - (option) == CURLOPT_PROTOCOLS_STR || \ - (option) == CURLOPT_PROXY || \ - (option) == CURLOPT_PROXY_CAINFO || \ - (option) == CURLOPT_PROXY_CAPATH || \ - (option) == CURLOPT_PROXY_CRLFILE || \ - (option) == CURLOPT_PROXY_ISSUERCERT || \ - (option) == CURLOPT_PROXY_KEYPASSWD || \ - (option) == CURLOPT_PROXY_PINNEDPUBLICKEY || \ - (option) == CURLOPT_PROXY_SERVICE_NAME || \ - (option) == CURLOPT_PROXY_SSL_CIPHER_LIST || \ - (option) == CURLOPT_PROXY_SSLCERT || \ - (option) == CURLOPT_PROXY_SSLCERTTYPE || \ - (option) == CURLOPT_PROXY_SSLKEY || \ - (option) == CURLOPT_PROXY_SSLKEYTYPE || \ - (option) == CURLOPT_PROXY_TLS13_CIPHERS || \ - (option) == CURLOPT_PROXY_TLSAUTH_PASSWORD || \ - (option) == CURLOPT_PROXY_TLSAUTH_TYPE || \ - (option) == CURLOPT_PROXY_TLSAUTH_USERNAME || \ - (option) == CURLOPT_PROXYPASSWORD || \ - (option) == CURLOPT_PROXYUSERNAME || \ - (option) == CURLOPT_PROXYUSERPWD || \ - (option) == CURLOPT_RANDOM_FILE || \ - (option) == CURLOPT_RANGE || \ - (option) == CURLOPT_REDIR_PROTOCOLS_STR || \ - (option) == CURLOPT_REFERER || \ - (option) == CURLOPT_REQUEST_TARGET || \ - (option) == CURLOPT_RTSP_SESSION_ID || \ - (option) == CURLOPT_RTSP_STREAM_URI || \ - (option) == CURLOPT_RTSP_TRANSPORT || \ - (option) == CURLOPT_SASL_AUTHZID || \ - (option) == CURLOPT_SERVICE_NAME || \ - (option) == CURLOPT_SOCKS5_GSSAPI_SERVICE || \ - (option) == CURLOPT_SSH_HOST_PUBLIC_KEY_MD5 || \ - (option) == CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256 || \ - (option) == CURLOPT_SSH_KNOWNHOSTS || \ - (option) == CURLOPT_SSH_PRIVATE_KEYFILE || \ - (option) == CURLOPT_SSH_PUBLIC_KEYFILE || \ - (option) == CURLOPT_SSLCERT || \ - (option) == CURLOPT_SSLCERTTYPE || \ - (option) == CURLOPT_SSLENGINE || \ - (option) == CURLOPT_SSLKEY || \ - (option) == CURLOPT_SSLKEYTYPE || \ - (option) == CURLOPT_SSL_CIPHER_LIST || \ - (option) == CURLOPT_SSL_EC_CURVES || \ - (option) == CURLOPT_SSL_SIGNATURE_ALGORITHMS || \ - (option) == CURLOPT_TLS13_CIPHERS || \ - (option) == CURLOPT_TLSAUTH_PASSWORD || \ - (option) == CURLOPT_TLSAUTH_TYPE || \ - (option) == CURLOPT_TLSAUTH_USERNAME || \ - (option) == CURLOPT_UNIX_SOCKET_PATH || \ - (option) == CURLOPT_URL || \ - (option) == CURLOPT_USERAGENT || \ - (option) == CURLOPT_USERNAME || \ - (option) == CURLOPT_AWS_SIGV4 || \ - (option) == CURLOPT_USERPWD || \ - (option) == CURLOPT_XOAUTH2_BEARER || \ - 0) - -/* evaluates to true if option takes a curl_write_callback argument */ -#define curlcheck_write_cb_option(option) \ - ((option) == CURLOPT_HEADERFUNCTION || \ - (option) == CURLOPT_WRITEFUNCTION) - -/* evaluates to true if option takes a curl_conv_callback argument */ -#define curlcheck_conv_cb_option(option) \ - ((option) == CURLOPT_CONV_TO_NETWORK_FUNCTION || \ - (option) == CURLOPT_CONV_FROM_NETWORK_FUNCTION || \ - (option) == CURLOPT_CONV_FROM_UTF8_FUNCTION) - -/* evaluates to true if option takes a data argument to pass to a callback */ -#define curlcheck_cb_data_option(option) \ - ((option) == CURLOPT_CHUNK_DATA || \ - (option) == CURLOPT_CLOSESOCKETDATA || \ - (option) == CURLOPT_DEBUGDATA || \ - (option) == CURLOPT_FNMATCH_DATA || \ - (option) == CURLOPT_HEADERDATA || \ - (option) == CURLOPT_HSTSREADDATA || \ - (option) == CURLOPT_HSTSWRITEDATA || \ - (option) == CURLOPT_INTERLEAVEDATA || \ - (option) == CURLOPT_IOCTLDATA || \ - (option) == CURLOPT_OPENSOCKETDATA || \ - (option) == CURLOPT_PREREQDATA || \ - (option) == CURLOPT_XFERINFODATA || \ - (option) == CURLOPT_READDATA || \ - (option) == CURLOPT_SEEKDATA || \ - (option) == CURLOPT_SOCKOPTDATA || \ - (option) == CURLOPT_SSH_KEYDATA || \ - (option) == CURLOPT_SSL_CTX_DATA || \ - (option) == CURLOPT_WRITEDATA || \ - (option) == CURLOPT_RESOLVER_START_DATA || \ - (option) == CURLOPT_TRAILERDATA || \ - (option) == CURLOPT_SSH_HOSTKEYDATA || \ - 0) - -/* evaluates to true if option takes a POST data argument (void* or char*) */ -#define curlcheck_postfields_option(option) \ - ((option) == CURLOPT_POSTFIELDS || \ - (option) == CURLOPT_COPYPOSTFIELDS || \ - 0) - -/* evaluates to true if option takes a struct curl_slist * argument */ -#define curlcheck_slist_option(option) \ - ((option) == CURLOPT_HTTP200ALIASES || \ - (option) == CURLOPT_HTTPHEADER || \ - (option) == CURLOPT_MAIL_RCPT || \ - (option) == CURLOPT_POSTQUOTE || \ - (option) == CURLOPT_PREQUOTE || \ - (option) == CURLOPT_PROXYHEADER || \ - (option) == CURLOPT_QUOTE || \ - (option) == CURLOPT_RESOLVE || \ - (option) == CURLOPT_TELNETOPTIONS || \ - (option) == CURLOPT_CONNECT_TO || \ - 0) - -/* groups of curl_easy_getinfo infos that take the same type of argument */ - -/* evaluates to true if info expects a pointer to char * argument */ -#define curlcheck_string_info(info) \ - (CURLINFO_STRING < (info) && (info) < CURLINFO_LONG && \ - (info) != CURLINFO_PRIVATE) - -/* evaluates to true if info expects a pointer to long argument */ -#define curlcheck_long_info(info) \ - (CURLINFO_LONG < (info) && (info) < CURLINFO_DOUBLE) - -/* evaluates to true if info expects a pointer to double argument */ -#define curlcheck_double_info(info) \ - (CURLINFO_DOUBLE < (info) && (info) < CURLINFO_SLIST) - -/* true if info expects a pointer to struct curl_slist * argument */ -#define curlcheck_slist_info(info) \ - (((info) == CURLINFO_SSL_ENGINES) || ((info) == CURLINFO_COOKIELIST)) - -/* true if info expects a pointer to struct curl_tlssessioninfo * argument */ -#define curlcheck_tlssessioninfo_info(info) \ - (((info) == CURLINFO_TLS_SSL_PTR) || ((info) == CURLINFO_TLS_SESSION)) - -/* true if info expects a pointer to struct curl_certinfo * argument */ -#define curlcheck_certinfo_info(info) ((info) == CURLINFO_CERTINFO) - -/* true if info expects a pointer to struct curl_socket_t argument */ -#define curlcheck_socket_info(info) \ - (CURLINFO_SOCKET < (info) && (info) < CURLINFO_OFF_T) - -/* true if info expects a pointer to curl_off_t argument */ -#define curlcheck_off_t_info(info) \ - (CURLINFO_OFF_T < (info)) - -/* - * typecheck helpers -- check whether given expression has requested type - */ - -/* For pointers, you can use the curlcheck_ptr/curlcheck_arr macros, - * otherwise define a new macro. Search for __builtin_types_compatible_p - * in the GCC manual. - * NOTE: these macros MUST NOT EVALUATE their arguments! The argument is - * the actual expression passed to the curl_easy_setopt macro. This - * means that you can only apply the sizeof and __typeof__ operators, no - * == or whatsoever. - */ - -/* XXX: should evaluate to true if expr is a pointer */ -#define curlcheck_any_ptr(expr) \ - (sizeof(expr) == sizeof(void *)) - -/* evaluates to true if expr is NULL */ -/* XXX: must not evaluate expr, so this check is not accurate */ -#define curlcheck_NULL(expr) \ - (__builtin_types_compatible_p(__typeof__(expr), __typeof__(NULL))) - -/* evaluates to true if expr is type*, const type* or NULL */ -#define curlcheck_ptr(expr, type) \ - (curlcheck_NULL(expr) || \ - __builtin_types_compatible_p(__typeof__(expr), type *) || \ - __builtin_types_compatible_p(__typeof__(expr), const type *)) - -/* evaluates to true if expr is type**, const type** or NULL */ -#define curlcheck_ptrptr(expr, type) \ - (curlcheck_NULL(expr) || \ - __builtin_types_compatible_p(__typeof__(expr), type **) || \ - __builtin_types_compatible_p(__typeof__(expr), type *[]) || \ - __builtin_types_compatible_p(__typeof__(expr), const type *[]) || \ - __builtin_types_compatible_p(__typeof__(expr), const type **)) - -/* evaluates to true if expr is one of type[], type*, NULL or const type* */ -#define curlcheck_arr(expr, type) \ - (curlcheck_ptr((expr), type) || \ - __builtin_types_compatible_p(__typeof__(expr), type [])) - -/* evaluates to true if expr is a string */ -#define curlcheck_string(expr) \ - (curlcheck_arr((expr), char) || \ - curlcheck_arr((expr), signed char) || \ - curlcheck_arr((expr), unsigned char)) - -/* evaluates to true if expr is a CURL * */ -#define curlcheck_curl(expr) \ - (curlcheck_NULL(expr) || \ - __builtin_types_compatible_p(__typeof__(expr), CURL *)) - -/* evaluates to true if expr is a long (no matter the signedness) - * XXX: for now, int is also accepted (and therefore short and char, which - * are promoted to int when passed to a variadic function) */ -#define curlcheck_long(expr) \ - ( \ - ((sizeof(long) != sizeof(int)) && \ - (__builtin_types_compatible_p(__typeof__(expr), long) || \ - __builtin_types_compatible_p(__typeof__(expr), signed long) || \ - __builtin_types_compatible_p(__typeof__(expr), unsigned long))) \ - || \ - ((sizeof(long) == sizeof(int)) && \ - (__builtin_types_compatible_p(__typeof__(expr), long) || \ - __builtin_types_compatible_p(__typeof__(expr), signed long) || \ - __builtin_types_compatible_p(__typeof__(expr), unsigned long) || \ - __builtin_types_compatible_p(__typeof__(expr), int) || \ - __builtin_types_compatible_p(__typeof__(expr), signed int) || \ - __builtin_types_compatible_p(__typeof__(expr), unsigned int) || \ - __builtin_types_compatible_p(__typeof__(expr), short) || \ - __builtin_types_compatible_p(__typeof__(expr), signed short) || \ - __builtin_types_compatible_p(__typeof__(expr), unsigned short) || \ - __builtin_types_compatible_p(__typeof__(expr), char) || \ - __builtin_types_compatible_p(__typeof__(expr), signed char) || \ - __builtin_types_compatible_p(__typeof__(expr), unsigned char))) \ - ) - -/* evaluates to true if expr is of type curl_off_t */ -#define curlcheck_off_t(expr) \ - (__builtin_types_compatible_p(__typeof__(expr), curl_off_t)) - -/* evaluates to true if expr is abuffer suitable for CURLOPT_ERRORBUFFER */ -/* XXX: also check size of an char[] array? */ -#define curlcheck_error_buffer(expr) \ - (curlcheck_NULL(expr) || \ - __builtin_types_compatible_p(__typeof__(expr), char *) || \ - __builtin_types_compatible_p(__typeof__(expr), char[])) - -/* evaluates to true if expr is of type (const) void* or (const) FILE* */ -#if 0 -#define curlcheck_cb_data(expr) \ - (curlcheck_ptr((expr), void) || \ - curlcheck_ptr((expr), FILE)) -#else /* be less strict */ -#define curlcheck_cb_data(expr) \ - curlcheck_any_ptr(expr) -#endif - -/* evaluates to true if expr is of type FILE* */ -#define curlcheck_FILE(expr) \ - (curlcheck_NULL(expr) || \ - (__builtin_types_compatible_p(__typeof__(expr), FILE *))) - -/* evaluates to true if expr can be passed as POST data (void* or char*) */ -#define curlcheck_postfields(expr) \ - (curlcheck_ptr((expr), void) || \ - curlcheck_arr((expr), char) || \ - curlcheck_arr((expr), unsigned char)) - -/* helper: __builtin_types_compatible_p distinguishes between functions and - * function pointers, hide it */ -#define curlcheck_cb_compatible(func, type) \ - (__builtin_types_compatible_p(__typeof__(func), type) || \ - __builtin_types_compatible_p(__typeof__(func) *, type)) - -/* evaluates to true if expr is of type curl_resolver_start_callback */ -#define curlcheck_resolver_start_callback(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), curl_resolver_start_callback)) - -/* evaluates to true if expr is of type curl_read_callback or "similar" */ -#define curlcheck_read_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), __typeof__(fread) *) || \ - curlcheck_cb_compatible((expr), curl_read_callback) || \ - curlcheck_cb_compatible((expr), Wcurl_read_callback1) || \ - curlcheck_cb_compatible((expr), Wcurl_read_callback2) || \ - curlcheck_cb_compatible((expr), Wcurl_read_callback3) || \ - curlcheck_cb_compatible((expr), Wcurl_read_callback4) || \ - curlcheck_cb_compatible((expr), Wcurl_read_callback5) || \ - curlcheck_cb_compatible((expr), Wcurl_read_callback6)) -typedef size_t (*Wcurl_read_callback1)(char *, size_t, size_t, void *); -typedef size_t (*Wcurl_read_callback2)(char *, size_t, size_t, const void *); -typedef size_t (*Wcurl_read_callback3)(char *, size_t, size_t, FILE *); -typedef size_t (*Wcurl_read_callback4)(void *, size_t, size_t, void *); -typedef size_t (*Wcurl_read_callback5)(void *, size_t, size_t, const void *); -typedef size_t (*Wcurl_read_callback6)(void *, size_t, size_t, FILE *); - -/* evaluates to true if expr is of type curl_write_callback or "similar" */ -#define curlcheck_write_cb(expr) \ - (curlcheck_read_cb(expr) || \ - curlcheck_cb_compatible((expr), __typeof__(fwrite) *) || \ - curlcheck_cb_compatible((expr), curl_write_callback) || \ - curlcheck_cb_compatible((expr), Wcurl_write_callback1) || \ - curlcheck_cb_compatible((expr), Wcurl_write_callback2) || \ - curlcheck_cb_compatible((expr), Wcurl_write_callback3) || \ - curlcheck_cb_compatible((expr), Wcurl_write_callback4) || \ - curlcheck_cb_compatible((expr), Wcurl_write_callback5) || \ - curlcheck_cb_compatible((expr), Wcurl_write_callback6)) -typedef size_t (*Wcurl_write_callback1)(const char *, size_t, size_t, void *); -typedef size_t (*Wcurl_write_callback2)(const char *, size_t, size_t, - const void *); -typedef size_t (*Wcurl_write_callback3)(const char *, size_t, size_t, FILE *); -typedef size_t (*Wcurl_write_callback4)(const void *, size_t, size_t, void *); -typedef size_t (*Wcurl_write_callback5)(const void *, size_t, size_t, - const void *); -typedef size_t (*Wcurl_write_callback6)(const void *, size_t, size_t, FILE *); - -/* evaluates to true if expr is of type curl_ioctl_callback or "similar" */ -#define curlcheck_ioctl_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), curl_ioctl_callback) || \ - curlcheck_cb_compatible((expr), Wcurl_ioctl_callback1) || \ - curlcheck_cb_compatible((expr), Wcurl_ioctl_callback2) || \ - curlcheck_cb_compatible((expr), Wcurl_ioctl_callback3) || \ - curlcheck_cb_compatible((expr), Wcurl_ioctl_callback4)) -typedef curlioerr (*Wcurl_ioctl_callback1)(CURL *, int, void *); -typedef curlioerr (*Wcurl_ioctl_callback2)(CURL *, int, const void *); -typedef curlioerr (*Wcurl_ioctl_callback3)(CURL *, curliocmd, void *); -typedef curlioerr (*Wcurl_ioctl_callback4)(CURL *, curliocmd, const void *); - -/* evaluates to true if expr is of type curl_sockopt_callback or "similar" */ -#define curlcheck_sockopt_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), curl_sockopt_callback) || \ - curlcheck_cb_compatible((expr), Wcurl_sockopt_callback1) || \ - curlcheck_cb_compatible((expr), Wcurl_sockopt_callback2)) -typedef int (*Wcurl_sockopt_callback1)(void *, curl_socket_t, curlsocktype); -typedef int (*Wcurl_sockopt_callback2)(const void *, curl_socket_t, - curlsocktype); - -/* evaluates to true if expr is of type curl_opensocket_callback or - "similar" */ -#define curlcheck_opensocket_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), curl_opensocket_callback) || \ - curlcheck_cb_compatible((expr), Wcurl_opensocket_callback1) || \ - curlcheck_cb_compatible((expr), Wcurl_opensocket_callback2) || \ - curlcheck_cb_compatible((expr), Wcurl_opensocket_callback3) || \ - curlcheck_cb_compatible((expr), Wcurl_opensocket_callback4)) -typedef curl_socket_t (*Wcurl_opensocket_callback1) - (void *, curlsocktype, struct curl_sockaddr *); -typedef curl_socket_t (*Wcurl_opensocket_callback2) - (void *, curlsocktype, const struct curl_sockaddr *); -typedef curl_socket_t (*Wcurl_opensocket_callback3) - (const void *, curlsocktype, struct curl_sockaddr *); -typedef curl_socket_t (*Wcurl_opensocket_callback4) - (const void *, curlsocktype, const struct curl_sockaddr *); - -/* evaluates to true if expr is of type curl_progress_callback or "similar" */ -#define curlcheck_progress_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), curl_progress_callback) || \ - curlcheck_cb_compatible((expr), Wcurl_progress_callback1) || \ - curlcheck_cb_compatible((expr), Wcurl_progress_callback2)) -typedef int (*Wcurl_progress_callback1)(void *, - double, double, double, double); -typedef int (*Wcurl_progress_callback2)(const void *, - double, double, double, double); - -/* evaluates to true if expr is of type curl_xferinfo_callback */ -#define curlcheck_xferinfo_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), curl_xferinfo_callback)) - -/* evaluates to true if expr is of type curl_debug_callback or "similar" */ -#define curlcheck_debug_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), curl_debug_callback) || \ - curlcheck_cb_compatible((expr), Wcurl_debug_callback1) || \ - curlcheck_cb_compatible((expr), Wcurl_debug_callback2) || \ - curlcheck_cb_compatible((expr), Wcurl_debug_callback3) || \ - curlcheck_cb_compatible((expr), Wcurl_debug_callback4) || \ - curlcheck_cb_compatible((expr), Wcurl_debug_callback5) || \ - curlcheck_cb_compatible((expr), Wcurl_debug_callback6) || \ - curlcheck_cb_compatible((expr), Wcurl_debug_callback7) || \ - curlcheck_cb_compatible((expr), Wcurl_debug_callback8)) -typedef int (*Wcurl_debug_callback1) (CURL *, - curl_infotype, char *, size_t, void *); -typedef int (*Wcurl_debug_callback2) (CURL *, - curl_infotype, char *, size_t, const void *); -typedef int (*Wcurl_debug_callback3) (CURL *, - curl_infotype, const char *, size_t, void *); -typedef int (*Wcurl_debug_callback4) (CURL *, - curl_infotype, const char *, size_t, const void *); -typedef int (*Wcurl_debug_callback5) (CURL *, - curl_infotype, unsigned char *, size_t, void *); -typedef int (*Wcurl_debug_callback6) (CURL *, - curl_infotype, unsigned char *, size_t, const void *); -typedef int (*Wcurl_debug_callback7) (CURL *, - curl_infotype, const unsigned char *, size_t, void *); -typedef int (*Wcurl_debug_callback8) (CURL *, - curl_infotype, const unsigned char *, size_t, const void *); - -/* evaluates to true if expr is of type curl_ssl_ctx_callback or "similar" */ -/* this is getting even messier... */ -#define curlcheck_ssl_ctx_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), curl_ssl_ctx_callback) || \ - curlcheck_cb_compatible((expr), Wcurl_ssl_ctx_callback1) || \ - curlcheck_cb_compatible((expr), Wcurl_ssl_ctx_callback2) || \ - curlcheck_cb_compatible((expr), Wcurl_ssl_ctx_callback3) || \ - curlcheck_cb_compatible((expr), Wcurl_ssl_ctx_callback4) || \ - curlcheck_cb_compatible((expr), Wcurl_ssl_ctx_callback5) || \ - curlcheck_cb_compatible((expr), Wcurl_ssl_ctx_callback6) || \ - curlcheck_cb_compatible((expr), Wcurl_ssl_ctx_callback7) || \ - curlcheck_cb_compatible((expr), Wcurl_ssl_ctx_callback8)) -typedef CURLcode (*Wcurl_ssl_ctx_callback1)(CURL *, void *, void *); -typedef CURLcode (*Wcurl_ssl_ctx_callback2)(CURL *, void *, const void *); -typedef CURLcode (*Wcurl_ssl_ctx_callback3)(CURL *, const void *, void *); -typedef CURLcode (*Wcurl_ssl_ctx_callback4)(CURL *, const void *, - const void *); -#ifdef HEADER_SSL_H -/* hack: if we included OpenSSL's ssl.h, we know about SSL_CTX - * this will of course break if we are included before OpenSSL headers... - */ -typedef CURLcode (*Wcurl_ssl_ctx_callback5)(CURL *, SSL_CTX *, void *); -typedef CURLcode (*Wcurl_ssl_ctx_callback6)(CURL *, SSL_CTX *, const void *); -typedef CURLcode (*Wcurl_ssl_ctx_callback7)(CURL *, const SSL_CTX *, void *); -typedef CURLcode (*Wcurl_ssl_ctx_callback8)(CURL *, const SSL_CTX *, - const void *); -#else -typedef Wcurl_ssl_ctx_callback1 Wcurl_ssl_ctx_callback5; -typedef Wcurl_ssl_ctx_callback1 Wcurl_ssl_ctx_callback6; -typedef Wcurl_ssl_ctx_callback1 Wcurl_ssl_ctx_callback7; -typedef Wcurl_ssl_ctx_callback1 Wcurl_ssl_ctx_callback8; -#endif - -/* evaluates to true if expr is of type curl_conv_callback or "similar" */ -#define curlcheck_conv_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), curl_conv_callback) || \ - curlcheck_cb_compatible((expr), Wcurl_conv_callback1) || \ - curlcheck_cb_compatible((expr), Wcurl_conv_callback2) || \ - curlcheck_cb_compatible((expr), Wcurl_conv_callback3) || \ - curlcheck_cb_compatible((expr), Wcurl_conv_callback4)) -typedef CURLcode (*Wcurl_conv_callback1)(char *, size_t length); -typedef CURLcode (*Wcurl_conv_callback2)(const char *, size_t length); -typedef CURLcode (*Wcurl_conv_callback3)(void *, size_t length); -typedef CURLcode (*Wcurl_conv_callback4)(const void *, size_t length); - -/* evaluates to true if expr is of type curl_seek_callback or "similar" */ -#define curlcheck_seek_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), curl_seek_callback) || \ - curlcheck_cb_compatible((expr), Wcurl_seek_callback1) || \ - curlcheck_cb_compatible((expr), Wcurl_seek_callback2)) -typedef CURLcode (*Wcurl_seek_callback1)(void *, curl_off_t, int); -typedef CURLcode (*Wcurl_seek_callback2)(const void *, curl_off_t, int); - -/* evaluates to true if expr is of type curl_chunk_bgn_callback */ -#define curlcheck_chunk_bgn_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), curl_chunk_bgn_callback) || \ - curlcheck_cb_compatible((expr), Wcurl_chunk_bgn_callback1) || \ - curlcheck_cb_compatible((expr), Wcurl_chunk_bgn_callback2)) -typedef long (*Wcurl_chunk_bgn_callback1)(struct curl_fileinfo *, - void *, int); -typedef long (*Wcurl_chunk_bgn_callback2)(void *, void *, int); - -/* evaluates to true if expr is of type curl_chunk_end_callback */ -#define curlcheck_chunk_end_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), curl_chunk_end_callback)) - -/* evaluates to true if expr is of type curl_closesocket_callback */ -#define curlcheck_close_socket_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), curl_closesocket_callback)) - -/* evaluates to true if expr is of type curl_fnmatch_callback */ -#define curlcheck_fnmatch_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), curl_fnmatch_callback)) - -/* evaluates to true if expr is of type curl_hstsread_callback */ -#define curlcheck_hstsread_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), curl_hstsread_callback)) - -/* evaluates to true if expr is of type curl_hstswrite_callback */ -#define curlcheck_hstswrite_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), curl_hstswrite_callback)) - -/* evaluates to true if expr is of type curl_sshhostkeycallback */ -#define curlcheck_ssh_hostkey_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), curl_sshhostkeycallback)) - -/* evaluates to true if expr is of type curl_sshkeycallback */ -#define curlcheck_ssh_key_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), curl_sshkeycallback)) - -/* evaluates to true if expr is of type curl_interleave_callback */ -#define curlcheck_interleave_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), Wcurl_interleave_callback1) || \ - curlcheck_cb_compatible((expr), Wcurl_interleave_callback2)) -typedef size_t (*Wcurl_interleave_callback1)(void *p, size_t s, - size_t n, void *u); -typedef size_t (*Wcurl_interleave_callback2)(char *p, size_t s, - size_t n, void *u); - -/* evaluates to true if expr is of type curl_prereq_callback */ -#define curlcheck_prereq_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), curl_prereq_callback)) - -/* evaluates to true if expr is of type curl_trailer_callback */ -#define curlcheck_trailer_cb(expr) \ - (curlcheck_NULL(expr) || \ - curlcheck_cb_compatible((expr), curl_trailer_callback)) - -#endif /* CURLINC_TYPECHECK_GCC_H */ diff --git a/libs/curl/include/curl/urlapi.h b/libs/curl/include/curl/urlapi.h deleted file mode 100644 index dabe44a47..000000000 --- a/libs/curl/include/curl/urlapi.h +++ /dev/null @@ -1,155 +0,0 @@ -#ifndef CURLINC_URLAPI_H -#define CURLINC_URLAPI_H -/*************************************************************************** - * _ _ ____ _ - * Project ___| | | | _ \| | - * / __| | | | |_) | | - * | (__| |_| | _ <| |___ - * \___|\___/|_| \_\_____| - * - * Copyright (C) Daniel Stenberg, , et al. - * - * This software is licensed as described in the file COPYING, which - * you should have received as part of this distribution. The terms - * are also available at https://curl.se/docs/copyright.html. - * - * You may opt to use, copy, modify, merge, publish, distribute and/or sell - * copies of the Software, and permit persons to whom the Software is - * furnished to do so, under the terms of the COPYING file. - * - * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY - * KIND, either express or implied. - * - * SPDX-License-Identifier: curl - * - ***************************************************************************/ - -#include "curl.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/* the error codes for the URL API */ -typedef enum { - CURLUE_OK, - CURLUE_BAD_HANDLE, /* 1 */ - CURLUE_BAD_PARTPOINTER, /* 2 */ - CURLUE_MALFORMED_INPUT, /* 3 */ - CURLUE_BAD_PORT_NUMBER, /* 4 */ - CURLUE_UNSUPPORTED_SCHEME, /* 5 */ - CURLUE_URLDECODE, /* 6 */ - CURLUE_OUT_OF_MEMORY, /* 7 */ - CURLUE_USER_NOT_ALLOWED, /* 8 */ - CURLUE_UNKNOWN_PART, /* 9 */ - CURLUE_NO_SCHEME, /* 10 */ - CURLUE_NO_USER, /* 11 */ - CURLUE_NO_PASSWORD, /* 12 */ - CURLUE_NO_OPTIONS, /* 13 */ - CURLUE_NO_HOST, /* 14 */ - CURLUE_NO_PORT, /* 15 */ - CURLUE_NO_QUERY, /* 16 */ - CURLUE_NO_FRAGMENT, /* 17 */ - CURLUE_NO_ZONEID, /* 18 */ - CURLUE_BAD_FILE_URL, /* 19 */ - CURLUE_BAD_FRAGMENT, /* 20 */ - CURLUE_BAD_HOSTNAME, /* 21 */ - CURLUE_BAD_IPV6, /* 22 */ - CURLUE_BAD_LOGIN, /* 23 */ - CURLUE_BAD_PASSWORD, /* 24 */ - CURLUE_BAD_PATH, /* 25 */ - CURLUE_BAD_QUERY, /* 26 */ - CURLUE_BAD_SCHEME, /* 27 */ - CURLUE_BAD_SLASHES, /* 28 */ - CURLUE_BAD_USER, /* 29 */ - CURLUE_LACKS_IDN, /* 30 */ - CURLUE_TOO_LARGE, /* 31 */ - CURLUE_LAST -} CURLUcode; - -typedef enum { - CURLUPART_URL, - CURLUPART_SCHEME, - CURLUPART_USER, - CURLUPART_PASSWORD, - CURLUPART_OPTIONS, - CURLUPART_HOST, - CURLUPART_PORT, - CURLUPART_PATH, - CURLUPART_QUERY, - CURLUPART_FRAGMENT, - CURLUPART_ZONEID /* added in 7.65.0 */ -} CURLUPart; - -#define CURLU_DEFAULT_PORT (1 << 0) /* return default port number */ -#define CURLU_NO_DEFAULT_PORT (1 << 1) /* act as if no port number was set, - if the port number matches the - default for the scheme */ -#define CURLU_DEFAULT_SCHEME (1 << 2) /* return default scheme if - missing */ -#define CURLU_NON_SUPPORT_SCHEME (1 << 3) /* allow non-supported scheme */ -#define CURLU_PATH_AS_IS (1 << 4) /* leave dot sequences */ -#define CURLU_DISALLOW_USER (1 << 5) /* no user+password allowed */ -#define CURLU_URLDECODE (1 << 6) /* URL decode on get */ -#define CURLU_URLENCODE (1 << 7) /* URL encode on set */ -#define CURLU_APPENDQUERY (1 << 8) /* append a form style part */ -#define CURLU_GUESS_SCHEME (1 << 9) /* legacy curl-style guessing */ -#define CURLU_NO_AUTHORITY (1 << 10) /* Allow empty authority when the - scheme is unknown. */ -#define CURLU_ALLOW_SPACE (1 << 11) /* Allow spaces in the URL */ -#define CURLU_PUNYCODE (1 << 12) /* get the hostname in punycode */ -#define CURLU_PUNY2IDN (1 << 13) /* punycode => IDN conversion */ -#define CURLU_GET_EMPTY (1 << 14) /* allow empty queries and fragments - when extracting the URL or the - components */ -#define CURLU_NO_GUESS_SCHEME (1 << 15) /* for get, do not accept a guess */ - -typedef struct Curl_URL CURLU; - -/* - * curl_url() creates a new CURLU handle and returns a pointer to it. - * Must be freed with curl_url_cleanup(). - */ -CURL_EXTERN CURLU *curl_url(void); - -/* - * curl_url_cleanup() frees the CURLU handle and related resources used for - * the URL parsing. It will not free strings previously returned with the URL - * API. - */ -CURL_EXTERN void curl_url_cleanup(CURLU *handle); - -/* - * curl_url_dup() duplicates a CURLU handle and returns a new copy. The new - * handle must also be freed with curl_url_cleanup(). - */ -CURL_EXTERN CURLU *curl_url_dup(const CURLU *in); - -/* - * curl_url_get() extracts a specific part of the URL from a CURLU - * handle. Returns error code. The returned pointer MUST be freed with - * curl_free() afterwards. - */ -CURL_EXTERN CURLUcode curl_url_get(const CURLU *handle, CURLUPart what, - char **part, unsigned int flags); - -/* - * curl_url_set() sets a specific part of the URL in a CURLU handle. Returns - * error code. The passed in string will be copied. Passing a NULL instead of - * a part string, clears that part. - */ -CURL_EXTERN CURLUcode curl_url_set(CURLU *handle, CURLUPart what, - const char *part, unsigned int flags); - -/* - * curl_url_strerror() turns a CURLUcode value into the equivalent human - * readable error string. This is useful for printing meaningful error - * messages. - */ -CURL_EXTERN const char *curl_url_strerror(CURLUcode); - -#ifdef __cplusplus -} /* end of extern "C" */ -#endif - -#endif /* CURLINC_URLAPI_H */ diff --git a/libs/curl/include/curl/websockets.h b/libs/curl/include/curl/websockets.h deleted file mode 100644 index 402a2ca0f..000000000 --- a/libs/curl/include/curl/websockets.h +++ /dev/null @@ -1,98 +0,0 @@ -#ifndef CURLINC_WEBSOCKETS_H -#define CURLINC_WEBSOCKETS_H -/*************************************************************************** - * _ _ ____ _ - * Project ___| | | | _ \| | - * / __| | | | |_) | | - * | (__| |_| | _ <| |___ - * \___|\___/|_| \_\_____| - * - * Copyright (C) Daniel Stenberg, , et al. - * - * This software is licensed as described in the file COPYING, which - * you should have received as part of this distribution. The terms - * are also available at https://curl.se/docs/copyright.html. - * - * You may opt to use, copy, modify, merge, publish, distribute and/or sell - * copies of the Software, and permit persons to whom the Software is - * furnished to do so, under the terms of the COPYING file. - * - * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY - * KIND, either express or implied. - * - * SPDX-License-Identifier: curl - * - ***************************************************************************/ - -#ifdef __cplusplus -extern "C" { -#endif - -struct curl_ws_frame { - int age; /* zero */ - int flags; /* See the CURLWS_* defines */ - curl_off_t offset; /* the offset of this data into the frame */ - curl_off_t bytesleft; /* number of pending bytes left of the payload */ - size_t len; /* size of the current data chunk */ -}; - -/* flag bits */ -#define CURLWS_TEXT (1 << 0) -#define CURLWS_BINARY (1 << 1) -#define CURLWS_CONT (1 << 2) -#define CURLWS_CLOSE (1 << 3) -#define CURLWS_PING (1 << 4) -#define CURLWS_OFFSET (1 << 5) - -/* - * NAME curl_ws_recv() - * - * DESCRIPTION - * - * Receives data from the websocket connection. Use after successful - * curl_easy_perform() with CURLOPT_CONNECT_ONLY option. - */ -CURL_EXTERN CURLcode curl_ws_recv(CURL *curl, void *buffer, size_t buflen, - size_t *recv, - const struct curl_ws_frame **metap); - -/* flags for curl_ws_send() */ -#define CURLWS_PONG (1 << 6) - -/* - * NAME curl_ws_send() - * - * DESCRIPTION - * - * Sends data over the websocket connection. Use after successful - * curl_easy_perform() with CURLOPT_CONNECT_ONLY option. - */ -CURL_EXTERN CURLcode curl_ws_send(CURL *curl, const void *buffer, - size_t buflen, size_t *sent, - curl_off_t fragsize, - unsigned int flags); - -/* - * NAME curl_ws_start_frame() - * - * DESCRIPTION - * - * Buffers a websocket frame header with the given flags and length. - * Errors when a previous frame is not complete, e.g. not all its - * payload has been added. - */ -CURL_EXTERN CURLcode curl_ws_start_frame(CURL *curl, - unsigned int flags, - curl_off_t frame_len); - -/* bits for the CURLOPT_WS_OPTIONS bitmask: */ -#define CURLWS_RAW_MODE (1L << 0) -#define CURLWS_NOAUTOPONG (1L << 1) - -CURL_EXTERN const struct curl_ws_frame *curl_ws_meta(CURL *curl); - -#ifdef __cplusplus -} -#endif - -#endif /* CURLINC_WEBSOCKETS_H */ diff --git a/libs/curl/ios/device/libcurl.a b/libs/curl/ios/device/libcurl.a deleted file mode 100644 index ee93a86b0..000000000 Binary files a/libs/curl/ios/device/libcurl.a and /dev/null differ diff --git a/libs/curl/ios/simulator/libcurl.a b/libs/curl/ios/simulator/libcurl.a deleted file mode 100644 index 13bde09d7..000000000 Binary files a/libs/curl/ios/simulator/libcurl.a and /dev/null differ diff --git a/libs/curl/macos/libcurl.a b/libs/curl/macos/libcurl.a deleted file mode 100644 index 0d2583b2e..000000000 Binary files a/libs/curl/macos/libcurl.a and /dev/null differ diff --git a/libs/stb/stb_image.h b/libs/stb/stb_image.h deleted file mode 100644 index 5150daf49..000000000 --- a/libs/stb/stb_image.h +++ /dev/null @@ -1,4908 +0,0 @@ -#ifndef STBI_INCLUDE_STB_IMAGE_H -#define STBI_INCLUDE_STB_IMAGE_H - -#ifndef STBI_NO_STDIO -#include -#endif // STBI_NO_STDIO - -#define STBI_VERSION 1 - -enum -{ - STBI_default = 0, // only used for desired_channels - - STBI_grey = 1, - STBI_grey_alpha = 2, - STBI_rgb = 3, - STBI_rgb_alpha = 4 -}; - -#include -typedef unsigned char stbi_uc; -typedef unsigned short stbi_us; - -#ifndef STBI_NO_HDR -#define STBI_NO_HDR -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -#ifndef STBIDEF -#ifdef STB_IMAGE_STATIC -#define STBIDEF static -#else -#define STBIDEF extern -#endif -#endif - -typedef struct -{ - int (*read) (void *user,char *data,int size); // fill 'data' with 'size' bytes. return number of bytes actually read - void (*skip) (void *user,int n); // skip the next 'n' bytes, or 'unget' the last -n bytes if negative - int (*eof) (void *user); // returns nonzero if we are at end of file/data -} stbi_io_callbacks; - -STBIDEF stbi_uc *stbi_load_from_memory (stbi_uc const *buffer, int len , int *x, int *y, int *channels_in_file, int desired_channels); -STBIDEF stbi_uc *stbi_load_from_callbacks(stbi_io_callbacks const *clbk , void *user, int *x, int *y, int *channels_in_file, int desired_channels); - -#ifndef STBI_NO_STDIO -STBIDEF stbi_uc *stbi_load (char const *filename, int *x, int *y, int *channels_in_file, int desired_channels); -STBIDEF stbi_uc *stbi_load_from_file (FILE *f, int *x, int *y, int *channels_in_file, int desired_channels); -#endif - -#ifndef STBI_NO_GIF -STBIDEF stbi_uc *stbi_load_gif_from_memory(stbi_uc const *buffer, int len, int **delays, int *x, int *y, int *z, int *comp, int req_comp); -#endif - -#ifdef STBI_WINDOWS_UTF8 -STBIDEF int stbi_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input); -#endif - -STBIDEF stbi_us *stbi_load_16_from_memory (stbi_uc const *buffer, int len, int *x, int *y, int *channels_in_file, int desired_channels); -STBIDEF stbi_us *stbi_load_16_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *channels_in_file, int desired_channels); - -#ifndef STBI_NO_STDIO -STBIDEF stbi_us *stbi_load_16 (char const *filename, int *x, int *y, int *channels_in_file, int desired_channels); -STBIDEF stbi_us *stbi_load_from_file_16(FILE *f, int *x, int *y, int *channels_in_file, int desired_channels); -#endif - -#ifndef STBI_NO_LINEAR - STBIDEF float *stbi_loadf_from_memory (stbi_uc const *buffer, int len, int *x, int *y, int *channels_in_file, int desired_channels); - STBIDEF float *stbi_loadf_from_callbacks (stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *channels_in_file, int desired_channels); - - #ifndef STBI_NO_STDIO - STBIDEF float *stbi_loadf (char const *filename, int *x, int *y, int *channels_in_file, int desired_channels); - STBIDEF float *stbi_loadf_from_file (FILE *f, int *x, int *y, int *channels_in_file, int desired_channels); - #endif -#endif -#ifndef STBI_NO_LINEAR - STBIDEF void stbi_ldr_to_hdr_gamma(float gamma); - STBIDEF void stbi_ldr_to_hdr_scale(float scale); -#endif // STBI_NO_LINEAR - -STBIDEF int stbi_is_hdr_from_callbacks(stbi_io_callbacks const *clbk, void *user); -STBIDEF int stbi_is_hdr_from_memory(stbi_uc const *buffer, int len); -#ifndef STBI_NO_STDIO -STBIDEF int stbi_is_hdr (char const *filename); -STBIDEF int stbi_is_hdr_from_file(FILE *f); -#endif // STBI_NO_STDIO - -STBIDEF const char *stbi_failure_reason (void); - -STBIDEF void stbi_image_free (void *retval_from_stbi_load); - -STBIDEF int stbi_info_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp); -STBIDEF int stbi_info_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp); -STBIDEF int stbi_is_16_bit_from_memory(stbi_uc const *buffer, int len); -STBIDEF int stbi_is_16_bit_from_callbacks(stbi_io_callbacks const *clbk, void *user); - -#ifndef STBI_NO_STDIO -STBIDEF int stbi_info (char const *filename, int *x, int *y, int *comp); -STBIDEF int stbi_info_from_file (FILE *f, int *x, int *y, int *comp); -STBIDEF int stbi_is_16_bit (char const *filename); -STBIDEF int stbi_is_16_bit_from_file(FILE *f); -#endif - -STBIDEF void stbi_set_unpremultiply_on_load(int flag_true_if_should_unpremultiply); - -STBIDEF void stbi_convert_iphone_png_to_rgb(int flag_true_if_should_convert); - -STBIDEF void stbi_set_flip_vertically_on_load(int flag_true_if_should_flip); - -STBIDEF void stbi_set_unpremultiply_on_load_thread(int flag_true_if_should_unpremultiply); -STBIDEF void stbi_convert_iphone_png_to_rgb_thread(int flag_true_if_should_convert); -STBIDEF void stbi_set_flip_vertically_on_load_thread(int flag_true_if_should_flip); - -STBIDEF char *stbi_zlib_decode_malloc_guesssize(const char *buffer, int len, int initial_size, int *outlen); -STBIDEF char *stbi_zlib_decode_malloc_guesssize_headerflag(const char *buffer, int len, int initial_size, int *outlen, int parse_header); -STBIDEF char *stbi_zlib_decode_malloc(const char *buffer, int len, int *outlen); -STBIDEF int stbi_zlib_decode_buffer(char *obuffer, int olen, const char *ibuffer, int ilen); - -STBIDEF char *stbi_zlib_decode_noheader_malloc(const char *buffer, int len, int *outlen); -STBIDEF int stbi_zlib_decode_noheader_buffer(char *obuffer, int olen, const char *ibuffer, int ilen); - -#ifdef __cplusplus -} -#endif - -#endif // STBI_INCLUDE_STB_IMAGE_H - -#ifdef STB_IMAGE_IMPLEMENTATION - -#if defined(STBI_ONLY_JPEG) || defined(STBI_ONLY_PNG) || defined(STBI_ONLY_BMP) \ - || defined(STBI_ONLY_TGA) || defined(STBI_ONLY_GIF) || defined(STBI_ONLY_PSD) \ - || defined(STBI_ONLY_HDR) || defined(STBI_ONLY_PIC) || defined(STBI_ONLY_PNM) \ - || defined(STBI_ONLY_ZLIB) - #ifndef STBI_ONLY_JPEG - #define STBI_NO_JPEG - #endif - #ifndef STBI_ONLY_PNG - #define STBI_NO_PNG - #endif - #ifndef STBI_ONLY_BMP - #define STBI_NO_BMP - #endif - #ifndef STBI_ONLY_PSD - #define STBI_NO_PSD - #endif - #ifndef STBI_ONLY_TGA - #define STBI_NO_TGA - #endif - #ifndef STBI_ONLY_GIF - #define STBI_NO_GIF - #endif - #ifndef STBI_ONLY_HDR - #define STBI_NO_HDR - #endif - #ifndef STBI_ONLY_PIC - #define STBI_NO_PIC - #endif - #ifndef STBI_ONLY_PNM - #define STBI_NO_PNM - #endif -#endif - -#if defined(STBI_NO_PNG) && !defined(STBI_SUPPORT_ZLIB) && !defined(STBI_NO_ZLIB) -#define STBI_NO_ZLIB -#endif - -#include -#include // ptrdiff_t on osx -#include -#include -#include - -#if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR) -#include // ldexp, pow -#endif - -#ifndef STBI_NO_STDIO -#include -#endif - -#ifndef STBI_ASSERT -#include -#define STBI_ASSERT(x) assert(x) -#endif - -#ifdef __cplusplus -#define STBI_EXTERN extern "C" -#else -#define STBI_EXTERN extern -#endif - -#ifndef _MSC_VER - #ifdef __cplusplus - #define stbi_inline inline - #else - #define stbi_inline - #endif -#else - #define stbi_inline __forceinline -#endif - -#ifndef STBI_NO_THREAD_LOCALS - #if defined(__cplusplus) && __cplusplus >= 201103L - #define STBI_THREAD_LOCAL thread_local - #elif defined(__GNUC__) && __GNUC__ < 5 - #define STBI_THREAD_LOCAL __thread - #elif defined(_MSC_VER) - #define STBI_THREAD_LOCAL __declspec(thread) - #elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 201112L && !defined(__STDC_NO_THREADS__) - #define STBI_THREAD_LOCAL _Thread_local - #endif - - #ifndef STBI_THREAD_LOCAL - #if defined(__GNUC__) - #define STBI_THREAD_LOCAL __thread - #endif - #endif -#endif - -#if defined(_MSC_VER) || defined(__SYMBIAN32__) -typedef unsigned short stbi__uint16; -typedef signed short stbi__int16; -typedef unsigned int stbi__uint32; -typedef signed int stbi__int32; -#else -#include -typedef uint16_t stbi__uint16; -typedef int16_t stbi__int16; -typedef uint32_t stbi__uint32; -typedef int32_t stbi__int32; -#endif - -typedef unsigned char validate_uint32[sizeof(stbi__uint32)==4 ? 1 : -1]; - -#ifdef _MSC_VER -#define STBI_NOTUSED(v) (void)(v) -#else -#define STBI_NOTUSED(v) (void)sizeof(v) -#endif - -#ifdef _MSC_VER -#define STBI_HAS_LROTL -#endif - -#ifdef STBI_HAS_LROTL - #define stbi_lrot(x,y) _lrotl(x,y) -#else - #define stbi_lrot(x,y) (((x) << (y)) | ((x) >> (-(y) & 31))) -#endif - -#if defined(STBI_MALLOC) && defined(STBI_FREE) && (defined(STBI_REALLOC) || defined(STBI_REALLOC_SIZED)) -#elif !defined(STBI_MALLOC) && !defined(STBI_FREE) && !defined(STBI_REALLOC) && !defined(STBI_REALLOC_SIZED) -#else -#error "Must define all or none of STBI_MALLOC, STBI_FREE, and STBI_REALLOC (or STBI_REALLOC_SIZED)." -#endif - -#ifndef STBI_MALLOC -#define STBI_MALLOC(sz) malloc(sz) -#define STBI_REALLOC(p,newsz) realloc(p,newsz) -#define STBI_FREE(p) free(p) -#endif - -#ifndef STBI_REALLOC_SIZED -#define STBI_REALLOC_SIZED(p,oldsz,newsz) STBI_REALLOC(p,newsz) -#endif - -#if defined(__x86_64__) || defined(_M_X64) -#define STBI__X64_TARGET -#elif defined(__i386) || defined(_M_IX86) -#define STBI__X86_TARGET -#endif - -#if defined(__GNUC__) && defined(STBI__X86_TARGET) && !defined(__SSE2__) && !defined(STBI_NO_SIMD) -#define STBI_NO_SIMD -#endif - -#if defined(__MINGW32__) && defined(STBI__X86_TARGET) && !defined(STBI_MINGW_ENABLE_SSE2) && !defined(STBI_NO_SIMD) -#define STBI_NO_SIMD -#endif - -#if !defined(STBI_NO_SIMD) && (defined(STBI__X86_TARGET) || defined(STBI__X64_TARGET)) -#define STBI_SSE2 -#include - -#ifdef _MSC_VER - -#if _MSC_VER >= 1400 // not VC6 -#include // __cpuid -static int stbi__cpuid3(void) -{ - int info[4]; - __cpuid(info,1); - return info[3]; -} -#else -static int stbi__cpuid3(void) -{ - int res; - __asm { - mov eax,1 - cpuid - mov res,edx - } - return res; -} -#endif - -#define STBI_SIMD_ALIGN(type, name) __declspec(align(16)) type name - -#if !defined(STBI_NO_JPEG) && defined(STBI_SSE2) -static int stbi__sse2_available(void) -{ - int info3 = stbi__cpuid3(); - return ((info3 >> 26) & 1) != 0; -} -#endif - -#else // assume GCC-style if not VC++ -#define STBI_SIMD_ALIGN(type, name) type name __attribute__((aligned(16))) - -#if !defined(STBI_NO_JPEG) && defined(STBI_SSE2) -static int stbi__sse2_available(void) -{ - return 1; -} -#endif - -#endif -#endif - -#if defined(STBI_NO_SIMD) && defined(STBI_NEON) -#undef STBI_NEON -#endif - -#ifdef STBI_NEON -#include -#ifdef _MSC_VER -#define STBI_SIMD_ALIGN(type, name) __declspec(align(16)) type name -#else -#define STBI_SIMD_ALIGN(type, name) type name __attribute__((aligned(16))) -#endif -#endif - -#ifndef STBI_SIMD_ALIGN -#define STBI_SIMD_ALIGN(type, name) type name -#endif - -#ifndef STBI_MAX_DIMENSIONS -#define STBI_MAX_DIMENSIONS (1 << 24) -#endif - -typedef struct -{ - stbi__uint32 img_x, img_y; - int img_n, img_out_n; - - stbi_io_callbacks io; - void *io_user_data; - - int read_from_callbacks; - int buflen; - stbi_uc buffer_start[128]; - int callback_already_read; - - stbi_uc *img_buffer, *img_buffer_end; - stbi_uc *img_buffer_original, *img_buffer_original_end; -} stbi__context; - -static void stbi__refill_buffer(stbi__context *s); - -static void stbi__start_mem(stbi__context *s, stbi_uc const *buffer, int len) -{ - s->io.read = NULL; - s->read_from_callbacks = 0; - s->callback_already_read = 0; - s->img_buffer = s->img_buffer_original = (stbi_uc *) buffer; - s->img_buffer_end = s->img_buffer_original_end = (stbi_uc *) buffer+len; -} - -static void stbi__start_callbacks(stbi__context *s, stbi_io_callbacks *c, void *user) -{ - s->io = *c; - s->io_user_data = user; - s->buflen = sizeof(s->buffer_start); - s->read_from_callbacks = 1; - s->callback_already_read = 0; - s->img_buffer = s->img_buffer_original = s->buffer_start; - stbi__refill_buffer(s); - s->img_buffer_original_end = s->img_buffer_end; -} - -#ifndef STBI_NO_STDIO - -static int stbi__stdio_read(void *user, char *data, int size) -{ - return (int) fread(data,1,size,(FILE*) user); -} - -static void stbi__stdio_skip(void *user, int n) -{ - int ch; - fseek((FILE*) user, n, SEEK_CUR); - ch = fgetc((FILE*) user); /* have to read a byte to reset feof()'s flag */ - if (ch != EOF) { - ungetc(ch, (FILE *) user); /* push byte back onto stream if valid. */ - } -} - -static int stbi__stdio_eof(void *user) -{ - return feof((FILE*) user) || ferror((FILE *) user); -} - -static stbi_io_callbacks stbi__stdio_callbacks = -{ - stbi__stdio_read, - stbi__stdio_skip, - stbi__stdio_eof, -}; - -static void stbi__start_file(stbi__context *s, FILE *f) -{ - stbi__start_callbacks(s, &stbi__stdio_callbacks, (void *) f); -} - -#endif // !STBI_NO_STDIO - -static void stbi__rewind(stbi__context *s) -{ - s->img_buffer = s->img_buffer_original; - s->img_buffer_end = s->img_buffer_original_end; -} - -enum -{ - STBI_ORDER_RGB, - STBI_ORDER_BGR -}; - -typedef struct -{ - int bits_per_channel; - int num_channels; - int channel_order; -} stbi__result_info; - -#ifndef STBI_NO_JPEG -static int stbi__jpeg_test(stbi__context *s); -static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); -static int stbi__jpeg_info(stbi__context *s, int *x, int *y, int *comp); -#endif - -#ifndef STBI_NO_PNG -static int stbi__png_test(stbi__context *s); -static void *stbi__png_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); -static int stbi__png_info(stbi__context *s, int *x, int *y, int *comp); -static int stbi__png_is16(stbi__context *s); -#endif - -/* PIC format declarations removed */ - -#ifndef STBI_NO_GIF -static int stbi__gif_test(stbi__context *s); -static void *stbi__gif_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); -static void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y, int *z, int *comp, int req_comp); -static int stbi__gif_info(stbi__context *s, int *x, int *y, int *comp); -#endif - -static -#ifdef STBI_THREAD_LOCAL -STBI_THREAD_LOCAL -#endif -const char *stbi__g_failure_reason; - -STBIDEF const char *stbi_failure_reason(void) -{ - return stbi__g_failure_reason; -} - -#ifndef STBI_NO_FAILURE_STRINGS -static int stbi__err(const char *str) -{ - stbi__g_failure_reason = str; - return 0; -} -#endif - -static void *stbi__malloc(size_t size) -{ - return STBI_MALLOC(size); -} - -static int stbi__addsizes_valid(int a, int b) -{ - if (b < 0) return 0; - return a <= INT_MAX - b; -} - -static int stbi__mul2sizes_valid(int a, int b) -{ - if (a < 0 || b < 0) return 0; - if (b == 0) return 1; // mul-by-0 is always safe - return a <= INT_MAX/b; -} - -static int stbi__mad2sizes_valid(int a, int b, int add) -{ - return stbi__mul2sizes_valid(a, b) && stbi__addsizes_valid(a*b, add); -} - -static int stbi__mad3sizes_valid(int a, int b, int c, int add) -{ - return stbi__mul2sizes_valid(a, b) && stbi__mul2sizes_valid(a*b, c) && - stbi__addsizes_valid(a*b*c, add); -} - -static int stbi__mad4sizes_valid(int a, int b, int c, int d, int add) -{ - return stbi__mul2sizes_valid(a, b) && stbi__mul2sizes_valid(a*b, c) && - stbi__mul2sizes_valid(a*b*c, d) && stbi__addsizes_valid(a*b*c*d, add); -} - -static void *stbi__malloc_mad2(int a, int b, int add) -{ - if (!stbi__mad2sizes_valid(a, b, add)) return NULL; - return stbi__malloc(a*b + add); -} - -static void *stbi__malloc_mad3(int a, int b, int c, int add) -{ - if (!stbi__mad3sizes_valid(a, b, c, add)) return NULL; - return stbi__malloc(a*b*c + add); -} - -static void *stbi__malloc_mad4(int a, int b, int c, int d, int add) -{ - if (!stbi__mad4sizes_valid(a, b, c, d, add)) return NULL; - return stbi__malloc(a*b*c*d + add); -} - -static int stbi__addints_valid(int a, int b) -{ - if ((a >= 0) != (b >= 0)) return 1; // a and b have different signs, so no overflow - if (a < 0 && b < 0) return a >= INT_MIN - b; // same as a + b >= INT_MIN; INT_MIN - b cannot overflow since b < 0. - return a <= INT_MAX - b; -} - -static int stbi__mul2shorts_valid(int a, int b) -{ - if (b == 0 || b == -1) return 1; // multiplication by 0 is always 0; check for -1 so SHRT_MIN/b doesn't overflow - if ((a >= 0) == (b >= 0)) return a <= SHRT_MAX/b; // product is positive, so similar to mul2sizes_valid - if (b < 0) return a <= SHRT_MIN / b; // same as a * b >= SHRT_MIN - return a >= SHRT_MIN / b; -} - -#ifdef STBI_NO_FAILURE_STRINGS - #define stbi__err(x,y) 0 -#elif defined(STBI_FAILURE_USERMSG) - #define stbi__err(x,y) stbi__err(y) -#else - #define stbi__err(x,y) stbi__err(x) -#endif - -#define stbi__errpf(x,y) ((float *)(size_t) (stbi__err(x,y)?NULL:NULL)) -#define stbi__errpuc(x,y) ((unsigned char *)(size_t) (stbi__err(x,y)?NULL:NULL)) - -STBIDEF void stbi_image_free(void *retval_from_stbi_load) -{ - STBI_FREE(retval_from_stbi_load); -} - -#ifndef STBI_NO_LINEAR -static float *stbi__ldr_to_hdr(stbi_uc *data, int x, int y, int comp); -#endif - -static int stbi__vertically_flip_on_load_global = 0; - -STBIDEF void stbi_set_flip_vertically_on_load(int flag_true_if_should_flip) -{ - stbi__vertically_flip_on_load_global = flag_true_if_should_flip; -} - -#ifndef STBI_THREAD_LOCAL -#define stbi__vertically_flip_on_load stbi__vertically_flip_on_load_global -#else -static STBI_THREAD_LOCAL int stbi__vertically_flip_on_load_local, stbi__vertically_flip_on_load_set; - -STBIDEF void stbi_set_flip_vertically_on_load_thread(int flag_true_if_should_flip) -{ - stbi__vertically_flip_on_load_local = flag_true_if_should_flip; - stbi__vertically_flip_on_load_set = 1; -} - -#define stbi__vertically_flip_on_load (stbi__vertically_flip_on_load_set \ - ? stbi__vertically_flip_on_load_local \ - : stbi__vertically_flip_on_load_global) -#endif // STBI_THREAD_LOCAL - -static void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc) -{ - memset(ri, 0, sizeof(*ri)); // make sure it's initialized if we add new fields - ri->bits_per_channel = 8; // default is 8 so most paths don't have to be changed - ri->channel_order = STBI_ORDER_RGB; // all current input & output are this, but this is here so we can add BGR order - ri->num_channels = 0; - - #ifndef STBI_NO_PNG - if (stbi__png_test(s)) return stbi__png_load(s,x,y,comp,req_comp, ri); - #endif - - #ifndef STBI_NO_GIF - if (stbi__gif_test(s)) return stbi__gif_load(s,x,y,comp,req_comp, ri); - #endif - - #ifndef STBI_NO_JPEG - if (stbi__jpeg_test(s)) return stbi__jpeg_load(s,x,y,comp,req_comp, ri); - #endif - - return stbi__errpuc("unknown image type", "Image not of any known type, or corrupt"); -} - -static stbi_uc *stbi__convert_16_to_8(stbi__uint16 *orig, int w, int h, int channels) -{ - int i; - int img_len = w * h * channels; - stbi_uc *reduced; - - reduced = (stbi_uc *) stbi__malloc(img_len); - if (reduced == NULL) return stbi__errpuc("outofmem", "Out of memory"); - - for (i = 0; i < img_len; ++i) - reduced[i] = (stbi_uc)((orig[i] >> 8) & 0xFF); // top half of each byte is sufficient approx of 16->8 bit scaling - - STBI_FREE(orig); - return reduced; -} - -static stbi__uint16 *stbi__convert_8_to_16(stbi_uc *orig, int w, int h, int channels) -{ - int i; - int img_len = w * h * channels; - stbi__uint16 *enlarged; - - enlarged = (stbi__uint16 *) stbi__malloc(img_len*2); - if (enlarged == NULL) return (stbi__uint16 *) stbi__errpuc("outofmem", "Out of memory"); - - for (i = 0; i < img_len; ++i) - enlarged[i] = (stbi__uint16)((orig[i] << 8) + orig[i]); // replicate to high and low byte, maps 0->0, 255->0xffff - - STBI_FREE(orig); - return enlarged; -} - -static void stbi__vertical_flip(void *image, int w, int h, int bytes_per_pixel) -{ - int row; - size_t bytes_per_row = (size_t)w * bytes_per_pixel; - stbi_uc temp[2048]; - stbi_uc *bytes = (stbi_uc *)image; - - for (row = 0; row < (h>>1); row++) { - stbi_uc *row0 = bytes + row*bytes_per_row; - stbi_uc *row1 = bytes + (h - row - 1)*bytes_per_row; - size_t bytes_left = bytes_per_row; - while (bytes_left) { - size_t bytes_copy = (bytes_left < sizeof(temp)) ? bytes_left : sizeof(temp); - memcpy(temp, row0, bytes_copy); - memcpy(row0, row1, bytes_copy); - memcpy(row1, temp, bytes_copy); - row0 += bytes_copy; - row1 += bytes_copy; - bytes_left -= bytes_copy; - } - } -} - -#ifndef STBI_NO_GIF -static void stbi__vertical_flip_slices(void *image, int w, int h, int z, int bytes_per_pixel) -{ - int slice; - int slice_size = w * h * bytes_per_pixel; - - stbi_uc *bytes = (stbi_uc *)image; - for (slice = 0; slice < z; ++slice) { - stbi__vertical_flip(bytes, w, h, bytes_per_pixel); - bytes += slice_size; - } -} -#endif - -static unsigned char *stbi__load_and_postprocess_8bit(stbi__context *s, int *x, int *y, int *comp, int req_comp) -{ - stbi__result_info ri; - void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 8); - - if (result == NULL) - return NULL; - - STBI_ASSERT(ri.bits_per_channel == 8 || ri.bits_per_channel == 16); - - if (ri.bits_per_channel != 8) { - result = stbi__convert_16_to_8((stbi__uint16 *) result, *x, *y, req_comp == 0 ? *comp : req_comp); - ri.bits_per_channel = 8; - } - - if (stbi__vertically_flip_on_load) { - int channels = req_comp ? req_comp : *comp; - stbi__vertical_flip(result, *x, *y, channels * sizeof(stbi_uc)); - } - - return (unsigned char *) result; -} - -static stbi__uint16 *stbi__load_and_postprocess_16bit(stbi__context *s, int *x, int *y, int *comp, int req_comp) -{ - stbi__result_info ri; - void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 16); - - if (result == NULL) - return NULL; - - STBI_ASSERT(ri.bits_per_channel == 8 || ri.bits_per_channel == 16); - - if (ri.bits_per_channel != 16) { - result = stbi__convert_8_to_16((stbi_uc *) result, *x, *y, req_comp == 0 ? *comp : req_comp); - ri.bits_per_channel = 16; - } - - if (stbi__vertically_flip_on_load) { - int channels = req_comp ? req_comp : *comp; - stbi__vertical_flip(result, *x, *y, channels * sizeof(stbi__uint16)); - } - - return (stbi__uint16 *) result; -} - -#if !defined(STBI_NO_HDR) && !defined(STBI_NO_LINEAR) -static void stbi__float_postprocess(float *result, int *x, int *y, int *comp, int req_comp) -{ - if (stbi__vertically_flip_on_load && result != NULL) { - int channels = req_comp ? req_comp : *comp; - stbi__vertical_flip(result, *x, *y, channels * sizeof(float)); - } -} -#endif - -#ifndef STBI_NO_STDIO - -#if defined(_WIN32) && defined(STBI_WINDOWS_UTF8) -STBI_EXTERN __declspec(dllimport) int __stdcall MultiByteToWideChar(unsigned int cp, unsigned long flags, const char *str, int cbmb, wchar_t *widestr, int cchwide); -STBI_EXTERN __declspec(dllimport) int __stdcall WideCharToMultiByte(unsigned int cp, unsigned long flags, const wchar_t *widestr, int cchwide, char *str, int cbmb, const char *defchar, int *used_default); -#endif - -#if defined(_WIN32) && defined(STBI_WINDOWS_UTF8) -STBIDEF int stbi_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input) -{ - return WideCharToMultiByte(65001 /* UTF8 */, 0, input, -1, buffer, (int) bufferlen, NULL, NULL); -} -#endif - -static FILE *stbi__fopen(char const *filename, char const *mode) -{ - FILE *f; -#if defined(_WIN32) && defined(STBI_WINDOWS_UTF8) - wchar_t wMode[64]; - wchar_t wFilename[1024]; - if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, filename, -1, wFilename, sizeof(wFilename)/sizeof(*wFilename))) - return 0; - - if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, mode, -1, wMode, sizeof(wMode)/sizeof(*wMode))) - return 0; - -#if defined(_MSC_VER) && _MSC_VER >= 1400 - if (0 != _wfopen_s(&f, wFilename, wMode)) - f = 0; -#else - f = _wfopen(wFilename, wMode); -#endif - -#elif defined(_MSC_VER) && _MSC_VER >= 1400 - if (0 != fopen_s(&f, filename, mode)) - f=0; -#else - f = fopen(filename, mode); -#endif - return f; -} - -STBIDEF stbi_uc *stbi_load(char const *filename, int *x, int *y, int *comp, int req_comp) -{ - FILE *f = stbi__fopen(filename, "rb"); - unsigned char *result; - if (!f) return stbi__errpuc("can't fopen", "Unable to open file"); - result = stbi_load_from_file(f,x,y,comp,req_comp); - fclose(f); - return result; -} - -STBIDEF stbi_uc *stbi_load_from_file(FILE *f, int *x, int *y, int *comp, int req_comp) -{ - unsigned char *result; - stbi__context s; - stbi__start_file(&s,f); - result = stbi__load_and_postprocess_8bit(&s,x,y,comp,req_comp); - if (result) { - fseek(f, - (int) (s.img_buffer_end - s.img_buffer), SEEK_CUR); - } - return result; -} - -STBIDEF stbi__uint16 *stbi_load_from_file_16(FILE *f, int *x, int *y, int *comp, int req_comp) -{ - stbi__uint16 *result; - stbi__context s; - stbi__start_file(&s,f); - result = stbi__load_and_postprocess_16bit(&s,x,y,comp,req_comp); - if (result) { - fseek(f, - (int) (s.img_buffer_end - s.img_buffer), SEEK_CUR); - } - return result; -} - -STBIDEF stbi_us *stbi_load_16(char const *filename, int *x, int *y, int *comp, int req_comp) -{ - FILE *f = stbi__fopen(filename, "rb"); - stbi__uint16 *result; - if (!f) return (stbi_us *) stbi__errpuc("can't fopen", "Unable to open file"); - result = stbi_load_from_file_16(f,x,y,comp,req_comp); - fclose(f); - return result; -} - -#endif //!STBI_NO_STDIO - -STBIDEF stbi_us *stbi_load_16_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *channels_in_file, int desired_channels) -{ - stbi__context s; - stbi__start_mem(&s,buffer,len); - return stbi__load_and_postprocess_16bit(&s,x,y,channels_in_file,desired_channels); -} - -STBIDEF stbi_us *stbi_load_16_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *channels_in_file, int desired_channels) -{ - stbi__context s; - stbi__start_callbacks(&s, (stbi_io_callbacks *)clbk, user); - return stbi__load_and_postprocess_16bit(&s,x,y,channels_in_file,desired_channels); -} - -STBIDEF stbi_uc *stbi_load_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp) -{ - stbi__context s; - stbi__start_mem(&s,buffer,len); - return stbi__load_and_postprocess_8bit(&s,x,y,comp,req_comp); -} - -STBIDEF stbi_uc *stbi_load_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp, int req_comp) -{ - stbi__context s; - stbi__start_callbacks(&s, (stbi_io_callbacks *) clbk, user); - return stbi__load_and_postprocess_8bit(&s,x,y,comp,req_comp); -} - -#ifndef STBI_NO_GIF -STBIDEF stbi_uc *stbi_load_gif_from_memory(stbi_uc const *buffer, int len, int **delays, int *x, int *y, int *z, int *comp, int req_comp) -{ - unsigned char *result; - stbi__context s; - stbi__start_mem(&s,buffer,len); - - result = (unsigned char*) stbi__load_gif_main(&s, delays, x, y, z, comp, req_comp); - if (stbi__vertically_flip_on_load) { - stbi__vertical_flip_slices( result, *x, *y, *z, *comp ); - } - - return result; -} -#endif - -#ifndef STBI_NO_LINEAR -static float *stbi__loadf_main(stbi__context *s, int *x, int *y, int *comp, int req_comp) -{ - unsigned char *data; - data = stbi__load_and_postprocess_8bit(s, x, y, comp, req_comp); - if (data) - return stbi__ldr_to_hdr(data, *x, *y, req_comp ? req_comp : *comp); - return stbi__errpf("unknown image type", "Image not of any known type, or corrupt"); -} - -STBIDEF float *stbi_loadf_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp) -{ - stbi__context s; - stbi__start_mem(&s,buffer,len); - return stbi__loadf_main(&s,x,y,comp,req_comp); -} - -STBIDEF float *stbi_loadf_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp, int req_comp) -{ - stbi__context s; - stbi__start_callbacks(&s, (stbi_io_callbacks *) clbk, user); - return stbi__loadf_main(&s,x,y,comp,req_comp); -} - -#ifndef STBI_NO_STDIO -STBIDEF float *stbi_loadf(char const *filename, int *x, int *y, int *comp, int req_comp) -{ - float *result; - FILE *f = stbi__fopen(filename, "rb"); - if (!f) return stbi__errpf("can't fopen", "Unable to open file"); - result = stbi_loadf_from_file(f,x,y,comp,req_comp); - fclose(f); - return result; -} - -STBIDEF float *stbi_loadf_from_file(FILE *f, int *x, int *y, int *comp, int req_comp) -{ - stbi__context s; - stbi__start_file(&s,f); - return stbi__loadf_main(&s,x,y,comp,req_comp); -} -#endif // !STBI_NO_STDIO - -#endif // !STBI_NO_LINEAR - -STBIDEF int stbi_is_hdr_from_memory(stbi_uc const *buffer, int len) -{ - STBI_NOTUSED(buffer); - STBI_NOTUSED(len); - return 0; -} - -#ifndef STBI_NO_STDIO -STBIDEF int stbi_is_hdr (char const *filename) -{ - FILE *f = stbi__fopen(filename, "rb"); - int result=0; - if (f) { - result = stbi_is_hdr_from_file(f); - fclose(f); - } - return result; -} - -STBIDEF int stbi_is_hdr_from_file(FILE *f) -{ - STBI_NOTUSED(f); - return 0; -} -#endif // !STBI_NO_STDIO - -STBIDEF int stbi_is_hdr_from_callbacks(stbi_io_callbacks const *clbk, void *user) -{ - STBI_NOTUSED(clbk); - STBI_NOTUSED(user); - return 0; -} - -#ifndef STBI_NO_LINEAR -static float stbi__l2h_gamma=2.2f, stbi__l2h_scale=1.0f; - -STBIDEF void stbi_ldr_to_hdr_gamma(float gamma) { stbi__l2h_gamma = gamma; } -STBIDEF void stbi_ldr_to_hdr_scale(float scale) { stbi__l2h_scale = scale; } -#endif - -static float stbi__h2l_gamma_i=1.0f/2.2f, stbi__h2l_scale_i=1.0f; - -STBIDEF void stbi_hdr_to_ldr_gamma(float gamma) { stbi__h2l_gamma_i = 1/gamma; } -STBIDEF void stbi_hdr_to_ldr_scale(float scale) { stbi__h2l_scale_i = 1/scale; } - -enum -{ - STBI__SCAN_load=0, - STBI__SCAN_type, - STBI__SCAN_header -}; - -static void stbi__refill_buffer(stbi__context *s) -{ - int n = (s->io.read)(s->io_user_data,(char*)s->buffer_start,s->buflen); - s->callback_already_read += (int) (s->img_buffer - s->img_buffer_original); - if (n == 0) { - s->read_from_callbacks = 0; - s->img_buffer = s->buffer_start; - s->img_buffer_end = s->buffer_start+1; - *s->img_buffer = 0; - } else { - s->img_buffer = s->buffer_start; - s->img_buffer_end = s->buffer_start + n; - } -} - -stbi_inline static stbi_uc stbi__get8(stbi__context *s) -{ - if (s->img_buffer < s->img_buffer_end) - return *s->img_buffer++; - if (s->read_from_callbacks) { - stbi__refill_buffer(s); - return *s->img_buffer++; - } - return 0; -} - -stbi_inline static int stbi__at_eof(stbi__context *s) -{ - if (s->io.read) { - if (!(s->io.eof)(s->io_user_data)) return 0; - if (s->read_from_callbacks == 0) return 1; - } - - return s->img_buffer >= s->img_buffer_end; -} - -static void stbi__skip(stbi__context *s, int n) -{ - if (n == 0) return; // already there! - if (n < 0) { - s->img_buffer = s->img_buffer_end; - return; - } - if (s->io.read) { - int blen = (int) (s->img_buffer_end - s->img_buffer); - if (blen < n) { - s->img_buffer = s->img_buffer_end; - (s->io.skip)(s->io_user_data, n - blen); - return; - } - } - s->img_buffer += n; -} - -static int stbi__getn(stbi__context *s, stbi_uc *buffer, int n) -{ - if (s->io.read) { - int blen = (int) (s->img_buffer_end - s->img_buffer); - if (blen < n) { - int res, count; - - memcpy(buffer, s->img_buffer, blen); - - count = (s->io.read)(s->io_user_data, (char*) buffer + blen, n - blen); - res = (count == (n-blen)); - s->img_buffer = s->img_buffer_end; - return res; - } - } - - if (s->img_buffer+n <= s->img_buffer_end) { - memcpy(buffer, s->img_buffer, n); - s->img_buffer += n; - return 1; - } else - return 0; -} - -static int stbi__get16be(stbi__context *s) -{ - int z = stbi__get8(s); - return (z << 8) + stbi__get8(s); -} - -static stbi__uint32 stbi__get32be(stbi__context *s) -{ - stbi__uint32 z = stbi__get16be(s); - return (z << 16) + stbi__get16be(s); -} - -static int stbi__get16le(stbi__context *s) -{ - int z = stbi__get8(s); - return z + (stbi__get8(s) << 8); -} - -#ifndef STBI_NO_BMP -static stbi__uint32 stbi__get32le(stbi__context *s) -{ - stbi__uint32 z = stbi__get16le(s); - z += (stbi__uint32)stbi__get16le(s) << 16; - return z; -} -#endif - -#define STBI__BYTECAST(x) ((stbi_uc) ((x) & 255)) // truncate int to byte without warnings - -static stbi_uc stbi__compute_y(int r, int g, int b) -{ - return (stbi_uc) (((r*77) + (g*150) + (29*b)) >> 8); -} - -static unsigned char *stbi__convert_format(unsigned char *data, int img_n, int req_comp, unsigned int x, unsigned int y) -{ - int i,j; - unsigned char *good; - - if (req_comp == img_n) return data; - STBI_ASSERT(req_comp >= 1 && req_comp <= 4); - - good = (unsigned char *) stbi__malloc_mad3(req_comp, x, y, 0); - if (good == NULL) { - STBI_FREE(data); - return stbi__errpuc("outofmem", "Out of memory"); - } - - for (j=0; j < (int) y; ++j) { - unsigned char *src = data + j * x * img_n ; - unsigned char *dest = good + j * x * req_comp; - - #define STBI__COMBO(a,b) ((a)*8+(b)) - #define STBI__CASE(a,b) case STBI__COMBO(a,b): for(i=x-1; i >= 0; --i, src += a, dest += b) - switch (STBI__COMBO(img_n, req_comp)) { - STBI__CASE(1,2) { dest[0]=src[0]; dest[1]=255; } break; - STBI__CASE(1,3) { dest[0]=dest[1]=dest[2]=src[0]; } break; - STBI__CASE(1,4) { dest[0]=dest[1]=dest[2]=src[0]; dest[3]=255; } break; - STBI__CASE(2,1) { dest[0]=src[0]; } break; - STBI__CASE(2,3) { dest[0]=dest[1]=dest[2]=src[0]; } break; - STBI__CASE(2,4) { dest[0]=dest[1]=dest[2]=src[0]; dest[3]=src[1]; } break; - STBI__CASE(3,4) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2];dest[3]=255; } break; - STBI__CASE(3,1) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); } break; - STBI__CASE(3,2) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); dest[1] = 255; } break; - STBI__CASE(4,1) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); } break; - STBI__CASE(4,2) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); dest[1] = src[3]; } break; - STBI__CASE(4,3) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2]; } break; - default: STBI_ASSERT(0); STBI_FREE(data); STBI_FREE(good); return stbi__errpuc("unsupported", "Unsupported format conversion"); - } - #undef STBI__CASE - } - - STBI_FREE(data); - return good; -} - -static stbi__uint16 stbi__compute_y_16(int r, int g, int b) -{ - return (stbi__uint16) (((r*77) + (g*150) + (29*b)) >> 8); -} - -static stbi__uint16 *stbi__convert_format16(stbi__uint16 *data, int img_n, int req_comp, unsigned int x, unsigned int y) -{ - int i,j; - stbi__uint16 *good; - - if (req_comp == img_n) return data; - STBI_ASSERT(req_comp >= 1 && req_comp <= 4); - - good = (stbi__uint16 *) stbi__malloc(req_comp * x * y * 2); - if (good == NULL) { - STBI_FREE(data); - return (stbi__uint16 *) stbi__errpuc("outofmem", "Out of memory"); - } - - for (j=0; j < (int) y; ++j) { - stbi__uint16 *src = data + j * x * img_n ; - stbi__uint16 *dest = good + j * x * req_comp; - - #define STBI__COMBO(a,b) ((a)*8+(b)) - #define STBI__CASE(a,b) case STBI__COMBO(a,b): for(i=x-1; i >= 0; --i, src += a, dest += b) - switch (STBI__COMBO(img_n, req_comp)) { - STBI__CASE(1,2) { dest[0]=src[0]; dest[1]=0xffff; } break; - STBI__CASE(1,3) { dest[0]=dest[1]=dest[2]=src[0]; } break; - STBI__CASE(1,4) { dest[0]=dest[1]=dest[2]=src[0]; dest[3]=0xffff; } break; - STBI__CASE(2,1) { dest[0]=src[0]; } break; - STBI__CASE(2,3) { dest[0]=dest[1]=dest[2]=src[0]; } break; - STBI__CASE(2,4) { dest[0]=dest[1]=dest[2]=src[0]; dest[3]=src[1]; } break; - STBI__CASE(3,4) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2];dest[3]=0xffff; } break; - STBI__CASE(3,1) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); } break; - STBI__CASE(3,2) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); dest[1] = 0xffff; } break; - STBI__CASE(4,1) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); } break; - STBI__CASE(4,2) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); dest[1] = src[3]; } break; - STBI__CASE(4,3) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2]; } break; - default: STBI_ASSERT(0); STBI_FREE(data); STBI_FREE(good); return (stbi__uint16*) stbi__errpuc("unsupported", "Unsupported format conversion"); - } - #undef STBI__CASE - } - - STBI_FREE(data); - return good; -} - -#ifndef STBI_NO_LINEAR -static float *stbi__ldr_to_hdr(stbi_uc *data, int x, int y, int comp) -{ - int i,k,n; - float *output; - if (!data) return NULL; - output = (float *) stbi__malloc_mad4(x, y, comp, sizeof(float), 0); - if (output == NULL) { STBI_FREE(data); return stbi__errpf("outofmem", "Out of memory"); } - if (comp & 1) n = comp; else n = comp-1; - for (i=0; i < x*y; ++i) { - for (k=0; k < n; ++k) { - output[i*comp + k] = (float) (pow(data[i*comp+k]/255.0f, stbi__l2h_gamma) * stbi__l2h_scale); - } - } - if (n < comp) { - for (i=0; i < x*y; ++i) { - output[i*comp + n] = data[i*comp + n]/255.0f; - } - } - STBI_FREE(data); - return output; -} -#endif -/* HDR conversion helper removed */ - -#ifndef STBI_NO_JPEG - -#define FAST_BITS 9 // larger handles more cases; smaller stomps less cache - -typedef struct -{ - stbi_uc fast[1 << FAST_BITS]; - stbi__uint16 code[256]; - stbi_uc values[256]; - stbi_uc size[257]; - unsigned int maxcode[18]; - int delta[17]; // old 'firstsymbol' - old 'firstcode' -} stbi__huffman; - -typedef struct -{ - stbi__context *s; - stbi__huffman huff_dc[4]; - stbi__huffman huff_ac[4]; - stbi__uint16 dequant[4][64]; - stbi__int16 fast_ac[4][1 << FAST_BITS]; - - int img_h_max, img_v_max; - int img_mcu_x, img_mcu_y; - int img_mcu_w, img_mcu_h; - - struct - { - int id; - int h,v; - int tq; - int hd,ha; - int dc_pred; - - int x,y,w2,h2; - stbi_uc *data; - void *raw_data, *raw_coeff; - stbi_uc *linebuf; - short *coeff; // progressive only - int coeff_w, coeff_h; // number of 8x8 coefficient blocks - } img_comp[4]; - - stbi__uint32 code_buffer; // jpeg entropy-coded buffer - int code_bits; // number of valid bits - unsigned char marker; // marker seen while filling entropy buffer - int nomore; // flag if we saw a marker so must stop - - int progressive; - int spec_start; - int spec_end; - int succ_high; - int succ_low; - int eob_run; - int jfif; - int app14_color_transform; // Adobe APP14 tag - int rgb; - - int scan_n, order[4]; - int restart_interval, todo; - - void (*idct_block_kernel)(stbi_uc *out, int out_stride, short data[64]); - void (*YCbCr_to_RGB_kernel)(stbi_uc *out, const stbi_uc *y, const stbi_uc *pcb, const stbi_uc *pcr, int count, int step); - stbi_uc *(*resample_row_hv_2_kernel)(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs); -} stbi__jpeg; - -static int stbi__build_huffman(stbi__huffman *h, int *count) -{ - int i,j,k=0; - unsigned int code; - for (i=0; i < 16; ++i) { - for (j=0; j < count[i]; ++j) { - h->size[k++] = (stbi_uc) (i+1); - if(k >= 257) return stbi__err("bad size list","Corrupt JPEG"); - } - } - h->size[k] = 0; - - code = 0; - k = 0; - for(j=1; j <= 16; ++j) { - h->delta[j] = k - code; - if (h->size[k] == j) { - while (h->size[k] == j) - h->code[k++] = (stbi__uint16) (code++); - if (code-1 >= (1u << j)) return stbi__err("bad code lengths","Corrupt JPEG"); - } - h->maxcode[j] = code << (16-j); - code <<= 1; - } - h->maxcode[j] = 0xffffffff; - - memset(h->fast, 255, 1 << FAST_BITS); - for (i=0; i < k; ++i) { - int s = h->size[i]; - if (s <= FAST_BITS) { - int c = h->code[i] << (FAST_BITS-s); - int m = 1 << (FAST_BITS-s); - for (j=0; j < m; ++j) { - h->fast[c+j] = (stbi_uc) i; - } - } - } - return 1; -} - -static void stbi__build_fast_ac(stbi__int16 *fast_ac, stbi__huffman *h) -{ - int i; - for (i=0; i < (1 << FAST_BITS); ++i) { - stbi_uc fast = h->fast[i]; - fast_ac[i] = 0; - if (fast < 255) { - int rs = h->values[fast]; - int run = (rs >> 4) & 15; - int magbits = rs & 15; - int len = h->size[fast]; - - if (magbits && len + magbits <= FAST_BITS) { - int k = ((i << len) & ((1 << FAST_BITS) - 1)) >> (FAST_BITS - magbits); - int m = 1 << (magbits - 1); - if (k < m) k += (~0U << magbits) + 1; - if (k >= -128 && k <= 127) - fast_ac[i] = (stbi__int16) ((k * 256) + (run * 16) + (len + magbits)); - } - } - } -} - -static void stbi__grow_buffer_unsafe(stbi__jpeg *j) -{ - do { - unsigned int b = j->nomore ? 0 : stbi__get8(j->s); - if (b == 0xff) { - int c = stbi__get8(j->s); - while (c == 0xff) c = stbi__get8(j->s); // consume fill bytes - if (c != 0) { - j->marker = (unsigned char) c; - j->nomore = 1; - return; - } - } - j->code_buffer |= b << (24 - j->code_bits); - j->code_bits += 8; - } while (j->code_bits <= 24); -} - -static const stbi__uint32 stbi__bmask[17]={0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535}; - -stbi_inline static int stbi__jpeg_huff_decode(stbi__jpeg *j, stbi__huffman *h) -{ - unsigned int temp; - int c,k; - - if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); - - c = (j->code_buffer >> (32 - FAST_BITS)) & ((1 << FAST_BITS)-1); - k = h->fast[c]; - if (k < 255) { - int s = h->size[k]; - if (s > j->code_bits) - return -1; - j->code_buffer <<= s; - j->code_bits -= s; - return h->values[k]; - } - - temp = j->code_buffer >> 16; - for (k=FAST_BITS+1 ; ; ++k) - if (temp < h->maxcode[k]) - break; - if (k == 17) { - j->code_bits -= 16; - return -1; - } - - if (k > j->code_bits) - return -1; - - c = ((j->code_buffer >> (32 - k)) & stbi__bmask[k]) + h->delta[k]; - if(c < 0 || c >= 256) // symbol id out of bounds! - return -1; - STBI_ASSERT((((j->code_buffer) >> (32 - h->size[c])) & stbi__bmask[h->size[c]]) == h->code[c]); - - j->code_bits -= k; - j->code_buffer <<= k; - return h->values[c]; -} - -static const int stbi__jbias[16] = {0,-1,-3,-7,-15,-31,-63,-127,-255,-511,-1023,-2047,-4095,-8191,-16383,-32767}; - -stbi_inline static int stbi__extend_receive(stbi__jpeg *j, int n) -{ - unsigned int k; - int sgn; - if (j->code_bits < n) stbi__grow_buffer_unsafe(j); - if (j->code_bits < n) return 0; // ran out of bits from stream, return 0s intead of continuing - - sgn = j->code_buffer >> 31; // sign bit always in MSB; 0 if MSB clear (positive), 1 if MSB set (negative) - k = stbi_lrot(j->code_buffer, n); - j->code_buffer = k & ~stbi__bmask[n]; - k &= stbi__bmask[n]; - j->code_bits -= n; - return k + (stbi__jbias[n] & (sgn - 1)); -} - -stbi_inline static int stbi__jpeg_get_bits(stbi__jpeg *j, int n) -{ - unsigned int k; - if (j->code_bits < n) stbi__grow_buffer_unsafe(j); - if (j->code_bits < n) return 0; // ran out of bits from stream, return 0s intead of continuing - k = stbi_lrot(j->code_buffer, n); - j->code_buffer = k & ~stbi__bmask[n]; - k &= stbi__bmask[n]; - j->code_bits -= n; - return k; -} - -stbi_inline static int stbi__jpeg_get_bit(stbi__jpeg *j) -{ - unsigned int k; - if (j->code_bits < 1) stbi__grow_buffer_unsafe(j); - if (j->code_bits < 1) return 0; // ran out of bits from stream, return 0s intead of continuing - k = j->code_buffer; - j->code_buffer <<= 1; - --j->code_bits; - return k & 0x80000000; -} - -static const stbi_uc stbi__jpeg_dezigzag[64+15] = -{ - 0, 1, 8, 16, 9, 2, 3, 10, - 17, 24, 32, 25, 18, 11, 4, 5, - 12, 19, 26, 33, 40, 48, 41, 34, - 27, 20, 13, 6, 7, 14, 21, 28, - 35, 42, 49, 56, 57, 50, 43, 36, - 29, 22, 15, 23, 30, 37, 44, 51, - 58, 59, 52, 45, 38, 31, 39, 46, - 53, 60, 61, 54, 47, 55, 62, 63, - 63, 63, 63, 63, 63, 63, 63, 63, - 63, 63, 63, 63, 63, 63, 63 -}; - -static int stbi__jpeg_decode_block(stbi__jpeg *j, short data[64], stbi__huffman *hdc, stbi__huffman *hac, stbi__int16 *fac, int b, stbi__uint16 *dequant) -{ - int diff,dc,k; - int t; - - if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); - t = stbi__jpeg_huff_decode(j, hdc); - if (t < 0 || t > 15) return stbi__err("bad huffman code","Corrupt JPEG"); - - memset(data,0,64*sizeof(data[0])); - - diff = t ? stbi__extend_receive(j, t) : 0; - if (!stbi__addints_valid(j->img_comp[b].dc_pred, diff)) return stbi__err("bad delta","Corrupt JPEG"); - dc = j->img_comp[b].dc_pred + diff; - j->img_comp[b].dc_pred = dc; - if (!stbi__mul2shorts_valid(dc, dequant[0])) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); - data[0] = (short) (dc * dequant[0]); - - k = 1; - do { - unsigned int zig; - int c,r,s; - if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); - c = (j->code_buffer >> (32 - FAST_BITS)) & ((1 << FAST_BITS)-1); - r = fac[c]; - if (r) { // fast-AC path - k += (r >> 4) & 15; // run - s = r & 15; // combined length - if (s > j->code_bits) return stbi__err("bad huffman code", "Combined length longer than code bits available"); - j->code_buffer <<= s; - j->code_bits -= s; - zig = stbi__jpeg_dezigzag[k++]; - data[zig] = (short) ((r >> 8) * dequant[zig]); - } else { - int rs = stbi__jpeg_huff_decode(j, hac); - if (rs < 0) return stbi__err("bad huffman code","Corrupt JPEG"); - s = rs & 15; - r = rs >> 4; - if (s == 0) { - if (rs != 0xf0) break; // end block - k += 16; - } else { - k += r; - zig = stbi__jpeg_dezigzag[k++]; - data[zig] = (short) (stbi__extend_receive(j,s) * dequant[zig]); - } - } - } while (k < 64); - return 1; -} - -static int stbi__jpeg_decode_block_prog_dc(stbi__jpeg *j, short data[64], stbi__huffman *hdc, int b) -{ - int diff,dc; - int t; - if (j->spec_end != 0) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); - - if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); - - if (j->succ_high == 0) { - memset(data,0,64*sizeof(data[0])); // 0 all the ac values now - t = stbi__jpeg_huff_decode(j, hdc); - if (t < 0 || t > 15) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); - diff = t ? stbi__extend_receive(j, t) : 0; - - if (!stbi__addints_valid(j->img_comp[b].dc_pred, diff)) return stbi__err("bad delta", "Corrupt JPEG"); - dc = j->img_comp[b].dc_pred + diff; - j->img_comp[b].dc_pred = dc; - if (!stbi__mul2shorts_valid(dc, 1 << j->succ_low)) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); - data[0] = (short) (dc * (1 << j->succ_low)); - } else { - if (stbi__jpeg_get_bit(j)) - data[0] += (short) (1 << j->succ_low); - } - return 1; -} - -static int stbi__jpeg_decode_block_prog_ac(stbi__jpeg *j, short data[64], stbi__huffman *hac, stbi__int16 *fac) -{ - int k; - if (j->spec_start == 0) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); - - if (j->succ_high == 0) { - int shift = j->succ_low; - - if (j->eob_run) { - --j->eob_run; - return 1; - } - - k = j->spec_start; - do { - unsigned int zig; - int c,r,s; - if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); - c = (j->code_buffer >> (32 - FAST_BITS)) & ((1 << FAST_BITS)-1); - r = fac[c]; - if (r) { // fast-AC path - k += (r >> 4) & 15; // run - s = r & 15; // combined length - if (s > j->code_bits) return stbi__err("bad huffman code", "Combined length longer than code bits available"); - j->code_buffer <<= s; - j->code_bits -= s; - zig = stbi__jpeg_dezigzag[k++]; - data[zig] = (short) ((r >> 8) * (1 << shift)); - } else { - int rs = stbi__jpeg_huff_decode(j, hac); - if (rs < 0) return stbi__err("bad huffman code","Corrupt JPEG"); - s = rs & 15; - r = rs >> 4; - if (s == 0) { - if (r < 15) { - j->eob_run = (1 << r); - if (r) - j->eob_run += stbi__jpeg_get_bits(j, r); - --j->eob_run; - break; - } - k += 16; - } else { - k += r; - zig = stbi__jpeg_dezigzag[k++]; - data[zig] = (short) (stbi__extend_receive(j,s) * (1 << shift)); - } - } - } while (k <= j->spec_end); - } else { - - short bit = (short) (1 << j->succ_low); - - if (j->eob_run) { - --j->eob_run; - for (k = j->spec_start; k <= j->spec_end; ++k) { - short *p = &data[stbi__jpeg_dezigzag[k]]; - if (*p != 0) - if (stbi__jpeg_get_bit(j)) - if ((*p & bit)==0) { - if (*p > 0) - *p += bit; - else - *p -= bit; - } - } - } else { - k = j->spec_start; - do { - int r,s; - int rs = stbi__jpeg_huff_decode(j, hac); // @OPTIMIZE see if we can use the fast path here, advance-by-r is so slow, eh - if (rs < 0) return stbi__err("bad huffman code","Corrupt JPEG"); - s = rs & 15; - r = rs >> 4; - if (s == 0) { - if (r < 15) { - j->eob_run = (1 << r) - 1; - if (r) - j->eob_run += stbi__jpeg_get_bits(j, r); - r = 64; // force end of block - } else { - } - } else { - if (s != 1) return stbi__err("bad huffman code", "Corrupt JPEG"); - if (stbi__jpeg_get_bit(j)) - s = bit; - else - s = -bit; - } - - while (k <= j->spec_end) { - short *p = &data[stbi__jpeg_dezigzag[k++]]; - if (*p != 0) { - if (stbi__jpeg_get_bit(j)) - if ((*p & bit)==0) { - if (*p > 0) - *p += bit; - else - *p -= bit; - } - } else { - if (r == 0) { - *p = (short) s; - break; - } - --r; - } - } - } while (k <= j->spec_end); - } - } - return 1; -} - -stbi_inline static stbi_uc stbi__clamp(int x) -{ - if ((unsigned int) x > 255) { - if (x < 0) return 0; - if (x > 255) return 255; - } - return (stbi_uc) x; -} - -#define stbi__f2f(x) ((int) (((x) * 4096 + 0.5))) -#define stbi__fsh(x) ((x) * 4096) - -#define STBI__IDCT_1D(s0,s1,s2,s3,s4,s5,s6,s7) \ - int t0,t1,t2,t3,p1,p2,p3,p4,p5,x0,x1,x2,x3; \ - p2 = s2; \ - p3 = s6; \ - p1 = (p2+p3) * stbi__f2f(0.5411961f); \ - t2 = p1 + p3*stbi__f2f(-1.847759065f); \ - t3 = p1 + p2*stbi__f2f( 0.765366865f); \ - p2 = s0; \ - p3 = s4; \ - t0 = stbi__fsh(p2+p3); \ - t1 = stbi__fsh(p2-p3); \ - x0 = t0+t3; \ - x3 = t0-t3; \ - x1 = t1+t2; \ - x2 = t1-t2; \ - t0 = s7; \ - t1 = s5; \ - t2 = s3; \ - t3 = s1; \ - p3 = t0+t2; \ - p4 = t1+t3; \ - p1 = t0+t3; \ - p2 = t1+t2; \ - p5 = (p3+p4)*stbi__f2f( 1.175875602f); \ - t0 = t0*stbi__f2f( 0.298631336f); \ - t1 = t1*stbi__f2f( 2.053119869f); \ - t2 = t2*stbi__f2f( 3.072711026f); \ - t3 = t3*stbi__f2f( 1.501321110f); \ - p1 = p5 + p1*stbi__f2f(-0.899976223f); \ - p2 = p5 + p2*stbi__f2f(-2.562915447f); \ - p3 = p3*stbi__f2f(-1.961570560f); \ - p4 = p4*stbi__f2f(-0.390180644f); \ - t3 += p1+p4; \ - t2 += p2+p3; \ - t1 += p2+p4; \ - t0 += p1+p3; - -static void stbi__idct_block(stbi_uc *out, int out_stride, short data[64]) -{ - int i,val[64],*v=val; - stbi_uc *o; - short *d = data; - - for (i=0; i < 8; ++i,++d, ++v) { - if (d[ 8]==0 && d[16]==0 && d[24]==0 && d[32]==0 - && d[40]==0 && d[48]==0 && d[56]==0) { - int dcterm = d[0]*4; - v[0] = v[8] = v[16] = v[24] = v[32] = v[40] = v[48] = v[56] = dcterm; - } else { - STBI__IDCT_1D(d[ 0],d[ 8],d[16],d[24],d[32],d[40],d[48],d[56]) - x0 += 512; x1 += 512; x2 += 512; x3 += 512; - v[ 0] = (x0+t3) >> 10; - v[56] = (x0-t3) >> 10; - v[ 8] = (x1+t2) >> 10; - v[48] = (x1-t2) >> 10; - v[16] = (x2+t1) >> 10; - v[40] = (x2-t1) >> 10; - v[24] = (x3+t0) >> 10; - v[32] = (x3-t0) >> 10; - } - } - - for (i=0, v=val, o=out; i < 8; ++i,v+=8,o+=out_stride) { - STBI__IDCT_1D(v[0],v[1],v[2],v[3],v[4],v[5],v[6],v[7]) - x0 += 65536 + (128<<17); - x1 += 65536 + (128<<17); - x2 += 65536 + (128<<17); - x3 += 65536 + (128<<17); - o[0] = stbi__clamp((x0+t3) >> 17); - o[7] = stbi__clamp((x0-t3) >> 17); - o[1] = stbi__clamp((x1+t2) >> 17); - o[6] = stbi__clamp((x1-t2) >> 17); - o[2] = stbi__clamp((x2+t1) >> 17); - o[5] = stbi__clamp((x2-t1) >> 17); - o[3] = stbi__clamp((x3+t0) >> 17); - o[4] = stbi__clamp((x3-t0) >> 17); - } -} - -#ifdef STBI_SSE2 -static void stbi__idct_simd(stbi_uc *out, int out_stride, short data[64]) -{ - __m128i row0, row1, row2, row3, row4, row5, row6, row7; - __m128i tmp; - - #define dct_const(x,y) _mm_setr_epi16((x),(y),(x),(y),(x),(y),(x),(y)) - - #define dct_rot(out0,out1, x,y,c0,c1) \ - __m128i c0##lo = _mm_unpacklo_epi16((x),(y)); \ - __m128i c0##hi = _mm_unpackhi_epi16((x),(y)); \ - __m128i out0##_l = _mm_madd_epi16(c0##lo, c0); \ - __m128i out0##_h = _mm_madd_epi16(c0##hi, c0); \ - __m128i out1##_l = _mm_madd_epi16(c0##lo, c1); \ - __m128i out1##_h = _mm_madd_epi16(c0##hi, c1) - - #define dct_widen(out, in) \ - __m128i out##_l = _mm_srai_epi32(_mm_unpacklo_epi16(_mm_setzero_si128(), (in)), 4); \ - __m128i out##_h = _mm_srai_epi32(_mm_unpackhi_epi16(_mm_setzero_si128(), (in)), 4) - - #define dct_wadd(out, a, b) \ - __m128i out##_l = _mm_add_epi32(a##_l, b##_l); \ - __m128i out##_h = _mm_add_epi32(a##_h, b##_h) - - #define dct_wsub(out, a, b) \ - __m128i out##_l = _mm_sub_epi32(a##_l, b##_l); \ - __m128i out##_h = _mm_sub_epi32(a##_h, b##_h) - - #define dct_bfly32o(out0, out1, a,b,bias,s) \ - { \ - __m128i abiased_l = _mm_add_epi32(a##_l, bias); \ - __m128i abiased_h = _mm_add_epi32(a##_h, bias); \ - dct_wadd(sum, abiased, b); \ - dct_wsub(dif, abiased, b); \ - out0 = _mm_packs_epi32(_mm_srai_epi32(sum_l, s), _mm_srai_epi32(sum_h, s)); \ - out1 = _mm_packs_epi32(_mm_srai_epi32(dif_l, s), _mm_srai_epi32(dif_h, s)); \ - } - - #define dct_interleave8(a, b) \ - tmp = a; \ - a = _mm_unpacklo_epi8(a, b); \ - b = _mm_unpackhi_epi8(tmp, b) - - #define dct_interleave16(a, b) \ - tmp = a; \ - a = _mm_unpacklo_epi16(a, b); \ - b = _mm_unpackhi_epi16(tmp, b) - - #define dct_pass(bias,shift) \ - { \ - /* even part */ \ - dct_rot(t2e,t3e, row2,row6, rot0_0,rot0_1); \ - __m128i sum04 = _mm_add_epi16(row0, row4); \ - __m128i dif04 = _mm_sub_epi16(row0, row4); \ - dct_widen(t0e, sum04); \ - dct_widen(t1e, dif04); \ - dct_wadd(x0, t0e, t3e); \ - dct_wsub(x3, t0e, t3e); \ - dct_wadd(x1, t1e, t2e); \ - dct_wsub(x2, t1e, t2e); \ - /* odd part */ \ - dct_rot(y0o,y2o, row7,row3, rot2_0,rot2_1); \ - dct_rot(y1o,y3o, row5,row1, rot3_0,rot3_1); \ - __m128i sum17 = _mm_add_epi16(row1, row7); \ - __m128i sum35 = _mm_add_epi16(row3, row5); \ - dct_rot(y4o,y5o, sum17,sum35, rot1_0,rot1_1); \ - dct_wadd(x4, y0o, y4o); \ - dct_wadd(x5, y1o, y5o); \ - dct_wadd(x6, y2o, y5o); \ - dct_wadd(x7, y3o, y4o); \ - dct_bfly32o(row0,row7, x0,x7,bias,shift); \ - dct_bfly32o(row1,row6, x1,x6,bias,shift); \ - dct_bfly32o(row2,row5, x2,x5,bias,shift); \ - dct_bfly32o(row3,row4, x3,x4,bias,shift); \ - } - - __m128i rot0_0 = dct_const(stbi__f2f(0.5411961f), stbi__f2f(0.5411961f) + stbi__f2f(-1.847759065f)); - __m128i rot0_1 = dct_const(stbi__f2f(0.5411961f) + stbi__f2f( 0.765366865f), stbi__f2f(0.5411961f)); - __m128i rot1_0 = dct_const(stbi__f2f(1.175875602f) + stbi__f2f(-0.899976223f), stbi__f2f(1.175875602f)); - __m128i rot1_1 = dct_const(stbi__f2f(1.175875602f), stbi__f2f(1.175875602f) + stbi__f2f(-2.562915447f)); - __m128i rot2_0 = dct_const(stbi__f2f(-1.961570560f) + stbi__f2f( 0.298631336f), stbi__f2f(-1.961570560f)); - __m128i rot2_1 = dct_const(stbi__f2f(-1.961570560f), stbi__f2f(-1.961570560f) + stbi__f2f( 3.072711026f)); - __m128i rot3_0 = dct_const(stbi__f2f(-0.390180644f) + stbi__f2f( 2.053119869f), stbi__f2f(-0.390180644f)); - __m128i rot3_1 = dct_const(stbi__f2f(-0.390180644f), stbi__f2f(-0.390180644f) + stbi__f2f( 1.501321110f)); - - __m128i bias_0 = _mm_set1_epi32(512); - __m128i bias_1 = _mm_set1_epi32(65536 + (128<<17)); - - row0 = _mm_load_si128((const __m128i *) (data + 0*8)); - row1 = _mm_load_si128((const __m128i *) (data + 1*8)); - row2 = _mm_load_si128((const __m128i *) (data + 2*8)); - row3 = _mm_load_si128((const __m128i *) (data + 3*8)); - row4 = _mm_load_si128((const __m128i *) (data + 4*8)); - row5 = _mm_load_si128((const __m128i *) (data + 5*8)); - row6 = _mm_load_si128((const __m128i *) (data + 6*8)); - row7 = _mm_load_si128((const __m128i *) (data + 7*8)); - - dct_pass(bias_0, 10); - - { - dct_interleave16(row0, row4); - dct_interleave16(row1, row5); - dct_interleave16(row2, row6); - dct_interleave16(row3, row7); - - dct_interleave16(row0, row2); - dct_interleave16(row1, row3); - dct_interleave16(row4, row6); - dct_interleave16(row5, row7); - - dct_interleave16(row0, row1); - dct_interleave16(row2, row3); - dct_interleave16(row4, row5); - dct_interleave16(row6, row7); - } - - dct_pass(bias_1, 17); - - { - __m128i p0 = _mm_packus_epi16(row0, row1); // a0a1a2a3...a7b0b1b2b3...b7 - __m128i p1 = _mm_packus_epi16(row2, row3); - __m128i p2 = _mm_packus_epi16(row4, row5); - __m128i p3 = _mm_packus_epi16(row6, row7); - - dct_interleave8(p0, p2); // a0e0a1e1... - dct_interleave8(p1, p3); // c0g0c1g1... - - dct_interleave8(p0, p1); // a0c0e0g0... - dct_interleave8(p2, p3); // b0d0f0h0... - - dct_interleave8(p0, p2); // a0b0c0d0... - dct_interleave8(p1, p3); // a4b4c4d4... - - _mm_storel_epi64((__m128i *) out, p0); out += out_stride; - _mm_storel_epi64((__m128i *) out, _mm_shuffle_epi32(p0, 0x4e)); out += out_stride; - _mm_storel_epi64((__m128i *) out, p2); out += out_stride; - _mm_storel_epi64((__m128i *) out, _mm_shuffle_epi32(p2, 0x4e)); out += out_stride; - _mm_storel_epi64((__m128i *) out, p1); out += out_stride; - _mm_storel_epi64((__m128i *) out, _mm_shuffle_epi32(p1, 0x4e)); out += out_stride; - _mm_storel_epi64((__m128i *) out, p3); out += out_stride; - _mm_storel_epi64((__m128i *) out, _mm_shuffle_epi32(p3, 0x4e)); - } - -#undef dct_const -#undef dct_rot -#undef dct_widen -#undef dct_wadd -#undef dct_wsub -#undef dct_bfly32o -#undef dct_interleave8 -#undef dct_interleave16 -#undef dct_pass -} - -#endif // STBI_SSE2 - -#ifdef STBI_NEON - -static void stbi__idct_simd(stbi_uc *out, int out_stride, short data[64]) -{ - int16x8_t row0, row1, row2, row3, row4, row5, row6, row7; - - int16x4_t rot0_0 = vdup_n_s16(stbi__f2f(0.5411961f)); - int16x4_t rot0_1 = vdup_n_s16(stbi__f2f(-1.847759065f)); - int16x4_t rot0_2 = vdup_n_s16(stbi__f2f( 0.765366865f)); - int16x4_t rot1_0 = vdup_n_s16(stbi__f2f( 1.175875602f)); - int16x4_t rot1_1 = vdup_n_s16(stbi__f2f(-0.899976223f)); - int16x4_t rot1_2 = vdup_n_s16(stbi__f2f(-2.562915447f)); - int16x4_t rot2_0 = vdup_n_s16(stbi__f2f(-1.961570560f)); - int16x4_t rot2_1 = vdup_n_s16(stbi__f2f(-0.390180644f)); - int16x4_t rot3_0 = vdup_n_s16(stbi__f2f( 0.298631336f)); - int16x4_t rot3_1 = vdup_n_s16(stbi__f2f( 2.053119869f)); - int16x4_t rot3_2 = vdup_n_s16(stbi__f2f( 3.072711026f)); - int16x4_t rot3_3 = vdup_n_s16(stbi__f2f( 1.501321110f)); - -#define dct_long_mul(out, inq, coeff) \ - int32x4_t out##_l = vmull_s16(vget_low_s16(inq), coeff); \ - int32x4_t out##_h = vmull_s16(vget_high_s16(inq), coeff) - -#define dct_long_mac(out, acc, inq, coeff) \ - int32x4_t out##_l = vmlal_s16(acc##_l, vget_low_s16(inq), coeff); \ - int32x4_t out##_h = vmlal_s16(acc##_h, vget_high_s16(inq), coeff) - -#define dct_widen(out, inq) \ - int32x4_t out##_l = vshll_n_s16(vget_low_s16(inq), 12); \ - int32x4_t out##_h = vshll_n_s16(vget_high_s16(inq), 12) - -#define dct_wadd(out, a, b) \ - int32x4_t out##_l = vaddq_s32(a##_l, b##_l); \ - int32x4_t out##_h = vaddq_s32(a##_h, b##_h) - -#define dct_wsub(out, a, b) \ - int32x4_t out##_l = vsubq_s32(a##_l, b##_l); \ - int32x4_t out##_h = vsubq_s32(a##_h, b##_h) - -#define dct_bfly32o(out0,out1, a,b,shiftop,s) \ - { \ - dct_wadd(sum, a, b); \ - dct_wsub(dif, a, b); \ - out0 = vcombine_s16(shiftop(sum_l, s), shiftop(sum_h, s)); \ - out1 = vcombine_s16(shiftop(dif_l, s), shiftop(dif_h, s)); \ - } - -#define dct_pass(shiftop, shift) \ - { \ - /* even part */ \ - int16x8_t sum26 = vaddq_s16(row2, row6); \ - dct_long_mul(p1e, sum26, rot0_0); \ - dct_long_mac(t2e, p1e, row6, rot0_1); \ - dct_long_mac(t3e, p1e, row2, rot0_2); \ - int16x8_t sum04 = vaddq_s16(row0, row4); \ - int16x8_t dif04 = vsubq_s16(row0, row4); \ - dct_widen(t0e, sum04); \ - dct_widen(t1e, dif04); \ - dct_wadd(x0, t0e, t3e); \ - dct_wsub(x3, t0e, t3e); \ - dct_wadd(x1, t1e, t2e); \ - dct_wsub(x2, t1e, t2e); \ - /* odd part */ \ - int16x8_t sum15 = vaddq_s16(row1, row5); \ - int16x8_t sum17 = vaddq_s16(row1, row7); \ - int16x8_t sum35 = vaddq_s16(row3, row5); \ - int16x8_t sum37 = vaddq_s16(row3, row7); \ - int16x8_t sumodd = vaddq_s16(sum17, sum35); \ - dct_long_mul(p5o, sumodd, rot1_0); \ - dct_long_mac(p1o, p5o, sum17, rot1_1); \ - dct_long_mac(p2o, p5o, sum35, rot1_2); \ - dct_long_mul(p3o, sum37, rot2_0); \ - dct_long_mul(p4o, sum15, rot2_1); \ - dct_wadd(sump13o, p1o, p3o); \ - dct_wadd(sump24o, p2o, p4o); \ - dct_wadd(sump23o, p2o, p3o); \ - dct_wadd(sump14o, p1o, p4o); \ - dct_long_mac(x4, sump13o, row7, rot3_0); \ - dct_long_mac(x5, sump24o, row5, rot3_1); \ - dct_long_mac(x6, sump23o, row3, rot3_2); \ - dct_long_mac(x7, sump14o, row1, rot3_3); \ - dct_bfly32o(row0,row7, x0,x7,shiftop,shift); \ - dct_bfly32o(row1,row6, x1,x6,shiftop,shift); \ - dct_bfly32o(row2,row5, x2,x5,shiftop,shift); \ - dct_bfly32o(row3,row4, x3,x4,shiftop,shift); \ - } - - row0 = vld1q_s16(data + 0*8); - row1 = vld1q_s16(data + 1*8); - row2 = vld1q_s16(data + 2*8); - row3 = vld1q_s16(data + 3*8); - row4 = vld1q_s16(data + 4*8); - row5 = vld1q_s16(data + 5*8); - row6 = vld1q_s16(data + 6*8); - row7 = vld1q_s16(data + 7*8); - - row0 = vaddq_s16(row0, vsetq_lane_s16(1024, vdupq_n_s16(0), 0)); - - dct_pass(vrshrn_n_s32, 10); - - { -#define dct_trn16(x, y) { int16x8x2_t t = vtrnq_s16(x, y); x = t.val[0]; y = t.val[1]; } -#define dct_trn32(x, y) { int32x4x2_t t = vtrnq_s32(vreinterpretq_s32_s16(x), vreinterpretq_s32_s16(y)); x = vreinterpretq_s16_s32(t.val[0]); y = vreinterpretq_s16_s32(t.val[1]); } -#define dct_trn64(x, y) { int16x8_t x0 = x; int16x8_t y0 = y; x = vcombine_s16(vget_low_s16(x0), vget_low_s16(y0)); y = vcombine_s16(vget_high_s16(x0), vget_high_s16(y0)); } - - dct_trn16(row0, row1); // a0b0a2b2a4b4a6b6 - dct_trn16(row2, row3); - dct_trn16(row4, row5); - dct_trn16(row6, row7); - - dct_trn32(row0, row2); // a0b0c0d0a4b4c4d4 - dct_trn32(row1, row3); - dct_trn32(row4, row6); - dct_trn32(row5, row7); - - dct_trn64(row0, row4); // a0b0c0d0e0f0g0h0 - dct_trn64(row1, row5); - dct_trn64(row2, row6); - dct_trn64(row3, row7); - -#undef dct_trn16 -#undef dct_trn32 -#undef dct_trn64 - } - - dct_pass(vshrn_n_s32, 16); - - { - uint8x8_t p0 = vqrshrun_n_s16(row0, 1); - uint8x8_t p1 = vqrshrun_n_s16(row1, 1); - uint8x8_t p2 = vqrshrun_n_s16(row2, 1); - uint8x8_t p3 = vqrshrun_n_s16(row3, 1); - uint8x8_t p4 = vqrshrun_n_s16(row4, 1); - uint8x8_t p5 = vqrshrun_n_s16(row5, 1); - uint8x8_t p6 = vqrshrun_n_s16(row6, 1); - uint8x8_t p7 = vqrshrun_n_s16(row7, 1); - -#define dct_trn8_8(x, y) { uint8x8x2_t t = vtrn_u8(x, y); x = t.val[0]; y = t.val[1]; } -#define dct_trn8_16(x, y) { uint16x4x2_t t = vtrn_u16(vreinterpret_u16_u8(x), vreinterpret_u16_u8(y)); x = vreinterpret_u8_u16(t.val[0]); y = vreinterpret_u8_u16(t.val[1]); } -#define dct_trn8_32(x, y) { uint32x2x2_t t = vtrn_u32(vreinterpret_u32_u8(x), vreinterpret_u32_u8(y)); x = vreinterpret_u8_u32(t.val[0]); y = vreinterpret_u8_u32(t.val[1]); } - - dct_trn8_8(p0, p1); - dct_trn8_8(p2, p3); - dct_trn8_8(p4, p5); - dct_trn8_8(p6, p7); - - dct_trn8_16(p0, p2); - dct_trn8_16(p1, p3); - dct_trn8_16(p4, p6); - dct_trn8_16(p5, p7); - - dct_trn8_32(p0, p4); - dct_trn8_32(p1, p5); - dct_trn8_32(p2, p6); - dct_trn8_32(p3, p7); - - vst1_u8(out, p0); out += out_stride; - vst1_u8(out, p1); out += out_stride; - vst1_u8(out, p2); out += out_stride; - vst1_u8(out, p3); out += out_stride; - vst1_u8(out, p4); out += out_stride; - vst1_u8(out, p5); out += out_stride; - vst1_u8(out, p6); out += out_stride; - vst1_u8(out, p7); - -#undef dct_trn8_8 -#undef dct_trn8_16 -#undef dct_trn8_32 - } - -#undef dct_long_mul -#undef dct_long_mac -#undef dct_widen -#undef dct_wadd -#undef dct_wsub -#undef dct_bfly32o -#undef dct_pass -} - -#endif // STBI_NEON - -#define STBI__MARKER_none 0xff -static stbi_uc stbi__get_marker(stbi__jpeg *j) -{ - stbi_uc x; - if (j->marker != STBI__MARKER_none) { x = j->marker; j->marker = STBI__MARKER_none; return x; } - x = stbi__get8(j->s); - if (x != 0xff) return STBI__MARKER_none; - while (x == 0xff) - x = stbi__get8(j->s); // consume repeated 0xff fill bytes - return x; -} - -#define STBI__RESTART(x) ((x) >= 0xd0 && (x) <= 0xd7) - -static void stbi__jpeg_reset(stbi__jpeg *j) -{ - j->code_bits = 0; - j->code_buffer = 0; - j->nomore = 0; - j->img_comp[0].dc_pred = j->img_comp[1].dc_pred = j->img_comp[2].dc_pred = j->img_comp[3].dc_pred = 0; - j->marker = STBI__MARKER_none; - j->todo = j->restart_interval ? j->restart_interval : 0x7fffffff; - j->eob_run = 0; -} - -static int stbi__parse_entropy_coded_data(stbi__jpeg *z) -{ - stbi__jpeg_reset(z); - if (!z->progressive) { - if (z->scan_n == 1) { - int i,j; - STBI_SIMD_ALIGN(short, data[64]); - int n = z->order[0]; - int w = (z->img_comp[n].x+7) >> 3; - int h = (z->img_comp[n].y+7) >> 3; - for (j=0; j < h; ++j) { - for (i=0; i < w; ++i) { - int ha = z->img_comp[n].ha; - if (!stbi__jpeg_decode_block(z, data, z->huff_dc+z->img_comp[n].hd, z->huff_ac+ha, z->fast_ac[ha], n, z->dequant[z->img_comp[n].tq])) return 0; - z->idct_block_kernel(z->img_comp[n].data+z->img_comp[n].w2*j*8+i*8, z->img_comp[n].w2, data); - if (--z->todo <= 0) { - if (z->code_bits < 24) stbi__grow_buffer_unsafe(z); - if (!STBI__RESTART(z->marker)) return 1; - stbi__jpeg_reset(z); - } - } - } - return 1; - } else { // interleaved - int i,j,k,x,y; - STBI_SIMD_ALIGN(short, data[64]); - for (j=0; j < z->img_mcu_y; ++j) { - for (i=0; i < z->img_mcu_x; ++i) { - for (k=0; k < z->scan_n; ++k) { - int n = z->order[k]; - for (y=0; y < z->img_comp[n].v; ++y) { - for (x=0; x < z->img_comp[n].h; ++x) { - int x2 = (i*z->img_comp[n].h + x)*8; - int y2 = (j*z->img_comp[n].v + y)*8; - int ha = z->img_comp[n].ha; - if (!stbi__jpeg_decode_block(z, data, z->huff_dc+z->img_comp[n].hd, z->huff_ac+ha, z->fast_ac[ha], n, z->dequant[z->img_comp[n].tq])) return 0; - z->idct_block_kernel(z->img_comp[n].data+z->img_comp[n].w2*y2+x2, z->img_comp[n].w2, data); - } - } - } - if (--z->todo <= 0) { - if (z->code_bits < 24) stbi__grow_buffer_unsafe(z); - if (!STBI__RESTART(z->marker)) return 1; - stbi__jpeg_reset(z); - } - } - } - return 1; - } - } else { - if (z->scan_n == 1) { - int i,j; - int n = z->order[0]; - int w = (z->img_comp[n].x+7) >> 3; - int h = (z->img_comp[n].y+7) >> 3; - for (j=0; j < h; ++j) { - for (i=0; i < w; ++i) { - short *data = z->img_comp[n].coeff + 64 * (i + j * z->img_comp[n].coeff_w); - if (z->spec_start == 0) { - if (!stbi__jpeg_decode_block_prog_dc(z, data, &z->huff_dc[z->img_comp[n].hd], n)) - return 0; - } else { - int ha = z->img_comp[n].ha; - if (!stbi__jpeg_decode_block_prog_ac(z, data, &z->huff_ac[ha], z->fast_ac[ha])) - return 0; - } - if (--z->todo <= 0) { - if (z->code_bits < 24) stbi__grow_buffer_unsafe(z); - if (!STBI__RESTART(z->marker)) return 1; - stbi__jpeg_reset(z); - } - } - } - return 1; - } else { // interleaved - int i,j,k,x,y; - for (j=0; j < z->img_mcu_y; ++j) { - for (i=0; i < z->img_mcu_x; ++i) { - for (k=0; k < z->scan_n; ++k) { - int n = z->order[k]; - for (y=0; y < z->img_comp[n].v; ++y) { - for (x=0; x < z->img_comp[n].h; ++x) { - int x2 = (i*z->img_comp[n].h + x); - int y2 = (j*z->img_comp[n].v + y); - short *data = z->img_comp[n].coeff + 64 * (x2 + y2 * z->img_comp[n].coeff_w); - if (!stbi__jpeg_decode_block_prog_dc(z, data, &z->huff_dc[z->img_comp[n].hd], n)) - return 0; - } - } - } - if (--z->todo <= 0) { - if (z->code_bits < 24) stbi__grow_buffer_unsafe(z); - if (!STBI__RESTART(z->marker)) return 1; - stbi__jpeg_reset(z); - } - } - } - return 1; - } - } -} - -static void stbi__jpeg_dequantize(short *data, stbi__uint16 *dequant) -{ - int i; - for (i=0; i < 64; ++i) - data[i] *= dequant[i]; -} - -static void stbi__jpeg_finish(stbi__jpeg *z) -{ - if (z->progressive) { - int i,j,n; - for (n=0; n < z->s->img_n; ++n) { - int w = (z->img_comp[n].x+7) >> 3; - int h = (z->img_comp[n].y+7) >> 3; - for (j=0; j < h; ++j) { - for (i=0; i < w; ++i) { - short *data = z->img_comp[n].coeff + 64 * (i + j * z->img_comp[n].coeff_w); - stbi__jpeg_dequantize(data, z->dequant[z->img_comp[n].tq]); - z->idct_block_kernel(z->img_comp[n].data+z->img_comp[n].w2*j*8+i*8, z->img_comp[n].w2, data); - } - } - } - } -} - -static int stbi__process_marker(stbi__jpeg *z, int m) -{ - int L; - switch (m) { - case STBI__MARKER_none: // no marker found - return stbi__err("expected marker","Corrupt JPEG"); - - case 0xDD: // DRI - specify restart interval - if (stbi__get16be(z->s) != 4) return stbi__err("bad DRI len","Corrupt JPEG"); - z->restart_interval = stbi__get16be(z->s); - return 1; - - case 0xDB: // DQT - define quantization table - L = stbi__get16be(z->s)-2; - while (L > 0) { - int q = stbi__get8(z->s); - int p = q >> 4, sixteen = (p != 0); - int t = q & 15,i; - if (p != 0 && p != 1) return stbi__err("bad DQT type","Corrupt JPEG"); - if (t > 3) return stbi__err("bad DQT table","Corrupt JPEG"); - - for (i=0; i < 64; ++i) - z->dequant[t][stbi__jpeg_dezigzag[i]] = (stbi__uint16)(sixteen ? stbi__get16be(z->s) : stbi__get8(z->s)); - L -= (sixteen ? 129 : 65); - } - return L==0; - - case 0xC4: // DHT - define huffman table - L = stbi__get16be(z->s)-2; - while (L > 0) { - stbi_uc *v; - int sizes[16],i,n=0; - int q = stbi__get8(z->s); - int tc = q >> 4; - int th = q & 15; - if (tc > 1 || th > 3) return stbi__err("bad DHT header","Corrupt JPEG"); - for (i=0; i < 16; ++i) { - sizes[i] = stbi__get8(z->s); - n += sizes[i]; - } - if(n > 256) return stbi__err("bad DHT header","Corrupt JPEG"); // Loop over i < n would write past end of values! - L -= 17; - if (tc == 0) { - if (!stbi__build_huffman(z->huff_dc+th, sizes)) return 0; - v = z->huff_dc[th].values; - } else { - if (!stbi__build_huffman(z->huff_ac+th, sizes)) return 0; - v = z->huff_ac[th].values; - } - for (i=0; i < n; ++i) - v[i] = stbi__get8(z->s); - if (tc != 0) - stbi__build_fast_ac(z->fast_ac[th], z->huff_ac + th); - L -= n; - } - return L==0; - } - - if ((m >= 0xE0 && m <= 0xEF) || m == 0xFE) { - L = stbi__get16be(z->s); - if (L < 2) { - if (m == 0xFE) - return stbi__err("bad COM len","Corrupt JPEG"); - else - return stbi__err("bad APP len","Corrupt JPEG"); - } - L -= 2; - - if (m == 0xE0 && L >= 5) { // JFIF APP0 segment - static const unsigned char tag[5] = {'J','F','I','F','\0'}; - int ok = 1; - int i; - for (i=0; i < 5; ++i) - if (stbi__get8(z->s) != tag[i]) - ok = 0; - L -= 5; - if (ok) - z->jfif = 1; - } else if (m == 0xEE && L >= 12) { // Adobe APP14 segment - static const unsigned char tag[6] = {'A','d','o','b','e','\0'}; - int ok = 1; - int i; - for (i=0; i < 6; ++i) - if (stbi__get8(z->s) != tag[i]) - ok = 0; - L -= 6; - if (ok) { - stbi__get8(z->s); // version - stbi__get16be(z->s); // flags0 - stbi__get16be(z->s); // flags1 - z->app14_color_transform = stbi__get8(z->s); // color transform - L -= 6; - } - } - - stbi__skip(z->s, L); - return 1; - } - - return stbi__err("unknown marker","Corrupt JPEG"); -} - -static int stbi__process_scan_header(stbi__jpeg *z) -{ - int i; - int Ls = stbi__get16be(z->s); - z->scan_n = stbi__get8(z->s); - if (z->scan_n < 1 || z->scan_n > 4 || z->scan_n > (int) z->s->img_n) return stbi__err("bad SOS component count","Corrupt JPEG"); - if (Ls != 6+2*z->scan_n) return stbi__err("bad SOS len","Corrupt JPEG"); - for (i=0; i < z->scan_n; ++i) { - int id = stbi__get8(z->s), which; - int q = stbi__get8(z->s); - for (which = 0; which < z->s->img_n; ++which) - if (z->img_comp[which].id == id) - break; - if (which == z->s->img_n) return 0; // no match - z->img_comp[which].hd = q >> 4; if (z->img_comp[which].hd > 3) return stbi__err("bad DC huff","Corrupt JPEG"); - z->img_comp[which].ha = q & 15; if (z->img_comp[which].ha > 3) return stbi__err("bad AC huff","Corrupt JPEG"); - z->order[i] = which; - } - - { - int aa; - z->spec_start = stbi__get8(z->s); - z->spec_end = stbi__get8(z->s); // should be 63, but might be 0 - aa = stbi__get8(z->s); - z->succ_high = (aa >> 4); - z->succ_low = (aa & 15); - if (z->progressive) { - if (z->spec_start > 63 || z->spec_end > 63 || z->spec_start > z->spec_end || z->succ_high > 13 || z->succ_low > 13) - return stbi__err("bad SOS", "Corrupt JPEG"); - } else { - if (z->spec_start != 0) return stbi__err("bad SOS","Corrupt JPEG"); - if (z->succ_high != 0 || z->succ_low != 0) return stbi__err("bad SOS","Corrupt JPEG"); - z->spec_end = 63; - } - } - - return 1; -} - -static int stbi__free_jpeg_components(stbi__jpeg *z, int ncomp, int why) -{ - int i; - for (i=0; i < ncomp; ++i) { - if (z->img_comp[i].raw_data) { - STBI_FREE(z->img_comp[i].raw_data); - z->img_comp[i].raw_data = NULL; - z->img_comp[i].data = NULL; - } - if (z->img_comp[i].raw_coeff) { - STBI_FREE(z->img_comp[i].raw_coeff); - z->img_comp[i].raw_coeff = 0; - z->img_comp[i].coeff = 0; - } - if (z->img_comp[i].linebuf) { - STBI_FREE(z->img_comp[i].linebuf); - z->img_comp[i].linebuf = NULL; - } - } - return why; -} - -static int stbi__process_frame_header(stbi__jpeg *z, int scan) -{ - stbi__context *s = z->s; - int Lf,p,i,q, h_max=1,v_max=1,c; - Lf = stbi__get16be(s); if (Lf < 11) return stbi__err("bad SOF len","Corrupt JPEG"); // JPEG - p = stbi__get8(s); if (p != 8) return stbi__err("only 8-bit","JPEG format not supported: 8-bit only"); // JPEG baseline - s->img_y = stbi__get16be(s); if (s->img_y == 0) return stbi__err("no header height", "JPEG format not supported: delayed height"); // Legal, but we don't handle it--but neither does IJG - s->img_x = stbi__get16be(s); if (s->img_x == 0) return stbi__err("0 width","Corrupt JPEG"); // JPEG requires - if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); - if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); - c = stbi__get8(s); - if (c != 3 && c != 1 && c != 4) return stbi__err("bad component count","Corrupt JPEG"); - s->img_n = c; - for (i=0; i < c; ++i) { - z->img_comp[i].data = NULL; - z->img_comp[i].linebuf = NULL; - } - - if (Lf != 8+3*s->img_n) return stbi__err("bad SOF len","Corrupt JPEG"); - - z->rgb = 0; - for (i=0; i < s->img_n; ++i) { - static const unsigned char rgb[3] = { 'R', 'G', 'B' }; - z->img_comp[i].id = stbi__get8(s); - if (s->img_n == 3 && z->img_comp[i].id == rgb[i]) - ++z->rgb; - q = stbi__get8(s); - z->img_comp[i].h = (q >> 4); if (!z->img_comp[i].h || z->img_comp[i].h > 4) return stbi__err("bad H","Corrupt JPEG"); - z->img_comp[i].v = q & 15; if (!z->img_comp[i].v || z->img_comp[i].v > 4) return stbi__err("bad V","Corrupt JPEG"); - z->img_comp[i].tq = stbi__get8(s); if (z->img_comp[i].tq > 3) return stbi__err("bad TQ","Corrupt JPEG"); - } - - if (scan != STBI__SCAN_load) return 1; - - if (!stbi__mad3sizes_valid(s->img_x, s->img_y, s->img_n, 0)) return stbi__err("too large", "Image too large to decode"); - - for (i=0; i < s->img_n; ++i) { - if (z->img_comp[i].h > h_max) h_max = z->img_comp[i].h; - if (z->img_comp[i].v > v_max) v_max = z->img_comp[i].v; - } - - for (i=0; i < s->img_n; ++i) { - if (h_max % z->img_comp[i].h != 0) return stbi__err("bad H","Corrupt JPEG"); - if (v_max % z->img_comp[i].v != 0) return stbi__err("bad V","Corrupt JPEG"); - } - - z->img_h_max = h_max; - z->img_v_max = v_max; - z->img_mcu_w = h_max * 8; - z->img_mcu_h = v_max * 8; - z->img_mcu_x = (s->img_x + z->img_mcu_w-1) / z->img_mcu_w; - z->img_mcu_y = (s->img_y + z->img_mcu_h-1) / z->img_mcu_h; - - for (i=0; i < s->img_n; ++i) { - z->img_comp[i].x = (s->img_x * z->img_comp[i].h + h_max-1) / h_max; - z->img_comp[i].y = (s->img_y * z->img_comp[i].v + v_max-1) / v_max; - z->img_comp[i].w2 = z->img_mcu_x * z->img_comp[i].h * 8; - z->img_comp[i].h2 = z->img_mcu_y * z->img_comp[i].v * 8; - z->img_comp[i].coeff = 0; - z->img_comp[i].raw_coeff = 0; - z->img_comp[i].linebuf = NULL; - z->img_comp[i].raw_data = stbi__malloc_mad2(z->img_comp[i].w2, z->img_comp[i].h2, 15); - if (z->img_comp[i].raw_data == NULL) - return stbi__free_jpeg_components(z, i+1, stbi__err("outofmem", "Out of memory")); - z->img_comp[i].data = (stbi_uc*) (((size_t) z->img_comp[i].raw_data + 15) & ~15); - if (z->progressive) { - z->img_comp[i].coeff_w = z->img_comp[i].w2 / 8; - z->img_comp[i].coeff_h = z->img_comp[i].h2 / 8; - z->img_comp[i].raw_coeff = stbi__malloc_mad3(z->img_comp[i].w2, z->img_comp[i].h2, sizeof(short), 15); - if (z->img_comp[i].raw_coeff == NULL) - return stbi__free_jpeg_components(z, i+1, stbi__err("outofmem", "Out of memory")); - z->img_comp[i].coeff = (short*) (((size_t) z->img_comp[i].raw_coeff + 15) & ~15); - } - } - - return 1; -} - -#define stbi__DNL(x) ((x) == 0xdc) -#define stbi__SOI(x) ((x) == 0xd8) -#define stbi__EOI(x) ((x) == 0xd9) -#define stbi__SOF(x) ((x) == 0xc0 || (x) == 0xc1 || (x) == 0xc2) -#define stbi__SOS(x) ((x) == 0xda) - -#define stbi__SOF_progressive(x) ((x) == 0xc2) - -static int stbi__decode_jpeg_header(stbi__jpeg *z, int scan) -{ - int m; - z->jfif = 0; - z->app14_color_transform = -1; // valid values are 0,1,2 - z->marker = STBI__MARKER_none; // initialize cached marker to empty - m = stbi__get_marker(z); - if (!stbi__SOI(m)) return stbi__err("no SOI","Corrupt JPEG"); - if (scan == STBI__SCAN_type) return 1; - m = stbi__get_marker(z); - while (!stbi__SOF(m)) { - if (!stbi__process_marker(z,m)) return 0; - m = stbi__get_marker(z); - while (m == STBI__MARKER_none) { - if (stbi__at_eof(z->s)) return stbi__err("no SOF", "Corrupt JPEG"); - m = stbi__get_marker(z); - } - } - z->progressive = stbi__SOF_progressive(m); - if (!stbi__process_frame_header(z, scan)) return 0; - return 1; -} - -static stbi_uc stbi__skip_jpeg_junk_at_end(stbi__jpeg *j) -{ - while (!stbi__at_eof(j->s)) { - stbi_uc x = stbi__get8(j->s); - while (x == 0xff) { // might be a marker - if (stbi__at_eof(j->s)) return STBI__MARKER_none; - x = stbi__get8(j->s); - if (x != 0x00 && x != 0xff) { - return x; - } - } - } - return STBI__MARKER_none; -} - -static int stbi__decode_jpeg_image(stbi__jpeg *j) -{ - int m; - for (m = 0; m < 4; m++) { - j->img_comp[m].raw_data = NULL; - j->img_comp[m].raw_coeff = NULL; - } - j->restart_interval = 0; - if (!stbi__decode_jpeg_header(j, STBI__SCAN_load)) return 0; - m = stbi__get_marker(j); - while (!stbi__EOI(m)) { - if (stbi__SOS(m)) { - if (!stbi__process_scan_header(j)) return 0; - if (!stbi__parse_entropy_coded_data(j)) return 0; - if (j->marker == STBI__MARKER_none ) { - j->marker = stbi__skip_jpeg_junk_at_end(j); - } - m = stbi__get_marker(j); - if (STBI__RESTART(m)) - m = stbi__get_marker(j); - } else if (stbi__DNL(m)) { - int Ld = stbi__get16be(j->s); - stbi__uint32 NL = stbi__get16be(j->s); - if (Ld != 4) return stbi__err("bad DNL len", "Corrupt JPEG"); - if (NL != j->s->img_y) return stbi__err("bad DNL height", "Corrupt JPEG"); - m = stbi__get_marker(j); - } else { - if (!stbi__process_marker(j, m)) return 1; - m = stbi__get_marker(j); - } - } - if (j->progressive) - stbi__jpeg_finish(j); - return 1; -} - -typedef stbi_uc *(*resample_row_func)(stbi_uc *out, stbi_uc *in0, stbi_uc *in1, - int w, int hs); - -#define stbi__div4(x) ((stbi_uc) ((x) >> 2)) - -static stbi_uc *resample_row_1(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) -{ - STBI_NOTUSED(out); - STBI_NOTUSED(in_far); - STBI_NOTUSED(w); - STBI_NOTUSED(hs); - return in_near; -} - -static stbi_uc* stbi__resample_row_v_2(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) -{ - int i; - STBI_NOTUSED(hs); - for (i=0; i < w; ++i) - out[i] = stbi__div4(3*in_near[i] + in_far[i] + 2); - return out; -} - -static stbi_uc* stbi__resample_row_h_2(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) -{ - int i; - stbi_uc *input = in_near; - - if (w == 1) { - out[0] = out[1] = input[0]; - return out; - } - - out[0] = input[0]; - out[1] = stbi__div4(input[0]*3 + input[1] + 2); - for (i=1; i < w-1; ++i) { - int n = 3*input[i]+2; - out[i*2+0] = stbi__div4(n+input[i-1]); - out[i*2+1] = stbi__div4(n+input[i+1]); - } - out[i*2+0] = stbi__div4(input[w-2]*3 + input[w-1] + 2); - out[i*2+1] = input[w-1]; - - STBI_NOTUSED(in_far); - STBI_NOTUSED(hs); - - return out; -} - -#define stbi__div16(x) ((stbi_uc) ((x) >> 4)) - -static stbi_uc *stbi__resample_row_hv_2(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) -{ - int i,t0,t1; - if (w == 1) { - out[0] = out[1] = stbi__div4(3*in_near[0] + in_far[0] + 2); - return out; - } - - t1 = 3*in_near[0] + in_far[0]; - out[0] = stbi__div4(t1+2); - for (i=1; i < w; ++i) { - t0 = t1; - t1 = 3*in_near[i]+in_far[i]; - out[i*2-1] = stbi__div16(3*t0 + t1 + 8); - out[i*2 ] = stbi__div16(3*t1 + t0 + 8); - } - out[w*2-1] = stbi__div4(t1+2); - - STBI_NOTUSED(hs); - - return out; -} - -#if defined(STBI_SSE2) || defined(STBI_NEON) -static stbi_uc *stbi__resample_row_hv_2_simd(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) -{ - int i=0,t0,t1; - - if (w == 1) { - out[0] = out[1] = stbi__div4(3*in_near[0] + in_far[0] + 2); - return out; - } - - t1 = 3*in_near[0] + in_far[0]; - for (; i < ((w-1) & ~7); i += 8) { -#if defined(STBI_SSE2) - __m128i zero = _mm_setzero_si128(); - __m128i farb = _mm_loadl_epi64((__m128i *) (in_far + i)); - __m128i nearb = _mm_loadl_epi64((__m128i *) (in_near + i)); - __m128i farw = _mm_unpacklo_epi8(farb, zero); - __m128i nearw = _mm_unpacklo_epi8(nearb, zero); - __m128i diff = _mm_sub_epi16(farw, nearw); - __m128i nears = _mm_slli_epi16(nearw, 2); - __m128i curr = _mm_add_epi16(nears, diff); // current row - - __m128i prv0 = _mm_slli_si128(curr, 2); - __m128i nxt0 = _mm_srli_si128(curr, 2); - __m128i prev = _mm_insert_epi16(prv0, t1, 0); - __m128i next = _mm_insert_epi16(nxt0, 3*in_near[i+8] + in_far[i+8], 7); - - __m128i bias = _mm_set1_epi16(8); - __m128i curs = _mm_slli_epi16(curr, 2); - __m128i prvd = _mm_sub_epi16(prev, curr); - __m128i nxtd = _mm_sub_epi16(next, curr); - __m128i curb = _mm_add_epi16(curs, bias); - __m128i even = _mm_add_epi16(prvd, curb); - __m128i odd = _mm_add_epi16(nxtd, curb); - - __m128i int0 = _mm_unpacklo_epi16(even, odd); - __m128i int1 = _mm_unpackhi_epi16(even, odd); - __m128i de0 = _mm_srli_epi16(int0, 4); - __m128i de1 = _mm_srli_epi16(int1, 4); - - __m128i outv = _mm_packus_epi16(de0, de1); - _mm_storeu_si128((__m128i *) (out + i*2), outv); -#elif defined(STBI_NEON) - uint8x8_t farb = vld1_u8(in_far + i); - uint8x8_t nearb = vld1_u8(in_near + i); - int16x8_t diff = vreinterpretq_s16_u16(vsubl_u8(farb, nearb)); - int16x8_t nears = vreinterpretq_s16_u16(vshll_n_u8(nearb, 2)); - int16x8_t curr = vaddq_s16(nears, diff); // current row - - int16x8_t prv0 = vextq_s16(curr, curr, 7); - int16x8_t nxt0 = vextq_s16(curr, curr, 1); - int16x8_t prev = vsetq_lane_s16(t1, prv0, 0); - int16x8_t next = vsetq_lane_s16(3*in_near[i+8] + in_far[i+8], nxt0, 7); - - int16x8_t curs = vshlq_n_s16(curr, 2); - int16x8_t prvd = vsubq_s16(prev, curr); - int16x8_t nxtd = vsubq_s16(next, curr); - int16x8_t even = vaddq_s16(curs, prvd); - int16x8_t odd = vaddq_s16(curs, nxtd); - - uint8x8x2_t o; - o.val[0] = vqrshrun_n_s16(even, 4); - o.val[1] = vqrshrun_n_s16(odd, 4); - vst2_u8(out + i*2, o); -#endif - - t1 = 3*in_near[i+7] + in_far[i+7]; - } - - t0 = t1; - t1 = 3*in_near[i] + in_far[i]; - out[i*2] = stbi__div16(3*t1 + t0 + 8); - - for (++i; i < w; ++i) { - t0 = t1; - t1 = 3*in_near[i]+in_far[i]; - out[i*2-1] = stbi__div16(3*t0 + t1 + 8); - out[i*2 ] = stbi__div16(3*t1 + t0 + 8); - } - out[w*2-1] = stbi__div4(t1+2); - - STBI_NOTUSED(hs); - - return out; -} -#endif - -static stbi_uc *stbi__resample_row_generic(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) -{ - int i,j; - STBI_NOTUSED(in_far); - for (i=0; i < w; ++i) - for (j=0; j < hs; ++j) - out[i*hs+j] = in_near[i]; - return out; -} - -#define stbi__float2fixed(x) (((int) ((x) * 4096.0f + 0.5f)) << 8) -static void stbi__YCbCr_to_RGB_row(stbi_uc *out, const stbi_uc *y, const stbi_uc *pcb, const stbi_uc *pcr, int count, int step) -{ - int i; - for (i=0; i < count; ++i) { - int y_fixed = (y[i] << 20) + (1<<19); // rounding - int r,g,b; - int cr = pcr[i] - 128; - int cb = pcb[i] - 128; - r = y_fixed + cr* stbi__float2fixed(1.40200f); - g = y_fixed + (cr*-stbi__float2fixed(0.71414f)) + ((cb*-stbi__float2fixed(0.34414f)) & 0xffff0000); - b = y_fixed + cb* stbi__float2fixed(1.77200f); - r >>= 20; - g >>= 20; - b >>= 20; - if ((unsigned) r > 255) { if (r < 0) r = 0; else r = 255; } - if ((unsigned) g > 255) { if (g < 0) g = 0; else g = 255; } - if ((unsigned) b > 255) { if (b < 0) b = 0; else b = 255; } - out[0] = (stbi_uc)r; - out[1] = (stbi_uc)g; - out[2] = (stbi_uc)b; - out[3] = 255; - out += step; - } -} - -#if defined(STBI_SSE2) || defined(STBI_NEON) -static void stbi__YCbCr_to_RGB_simd(stbi_uc *out, stbi_uc const *y, stbi_uc const *pcb, stbi_uc const *pcr, int count, int step) -{ - int i = 0; - -#ifdef STBI_SSE2 - if (step == 4) { - __m128i signflip = _mm_set1_epi8(-0x80); - __m128i cr_const0 = _mm_set1_epi16( (short) ( 1.40200f*4096.0f+0.5f)); - __m128i cr_const1 = _mm_set1_epi16( - (short) ( 0.71414f*4096.0f+0.5f)); - __m128i cb_const0 = _mm_set1_epi16( - (short) ( 0.34414f*4096.0f+0.5f)); - __m128i cb_const1 = _mm_set1_epi16( (short) ( 1.77200f*4096.0f+0.5f)); - __m128i y_bias = _mm_set1_epi8((char) (unsigned char) 128); - __m128i xw = _mm_set1_epi16(255); // alpha channel - - for (; i+7 < count; i += 8) { - __m128i y_bytes = _mm_loadl_epi64((__m128i *) (y+i)); - __m128i cr_bytes = _mm_loadl_epi64((__m128i *) (pcr+i)); - __m128i cb_bytes = _mm_loadl_epi64((__m128i *) (pcb+i)); - __m128i cr_biased = _mm_xor_si128(cr_bytes, signflip); // -128 - __m128i cb_biased = _mm_xor_si128(cb_bytes, signflip); // -128 - - __m128i yw = _mm_unpacklo_epi8(y_bias, y_bytes); - __m128i crw = _mm_unpacklo_epi8(_mm_setzero_si128(), cr_biased); - __m128i cbw = _mm_unpacklo_epi8(_mm_setzero_si128(), cb_biased); - - __m128i yws = _mm_srli_epi16(yw, 4); - __m128i cr0 = _mm_mulhi_epi16(cr_const0, crw); - __m128i cb0 = _mm_mulhi_epi16(cb_const0, cbw); - __m128i cb1 = _mm_mulhi_epi16(cbw, cb_const1); - __m128i cr1 = _mm_mulhi_epi16(crw, cr_const1); - __m128i rws = _mm_add_epi16(cr0, yws); - __m128i gwt = _mm_add_epi16(cb0, yws); - __m128i bws = _mm_add_epi16(yws, cb1); - __m128i gws = _mm_add_epi16(gwt, cr1); - - __m128i rw = _mm_srai_epi16(rws, 4); - __m128i bw = _mm_srai_epi16(bws, 4); - __m128i gw = _mm_srai_epi16(gws, 4); - - __m128i brb = _mm_packus_epi16(rw, bw); - __m128i gxb = _mm_packus_epi16(gw, xw); - - __m128i t0 = _mm_unpacklo_epi8(brb, gxb); - __m128i t1 = _mm_unpackhi_epi8(brb, gxb); - __m128i o0 = _mm_unpacklo_epi16(t0, t1); - __m128i o1 = _mm_unpackhi_epi16(t0, t1); - - _mm_storeu_si128((__m128i *) (out + 0), o0); - _mm_storeu_si128((__m128i *) (out + 16), o1); - out += 32; - } - } -#endif - -#ifdef STBI_NEON - if (step == 4) { - uint8x8_t signflip = vdup_n_u8(0x80); - int16x8_t cr_const0 = vdupq_n_s16( (short) ( 1.40200f*4096.0f+0.5f)); - int16x8_t cr_const1 = vdupq_n_s16( - (short) ( 0.71414f*4096.0f+0.5f)); - int16x8_t cb_const0 = vdupq_n_s16( - (short) ( 0.34414f*4096.0f+0.5f)); - int16x8_t cb_const1 = vdupq_n_s16( (short) ( 1.77200f*4096.0f+0.5f)); - - for (; i+7 < count; i += 8) { - uint8x8_t y_bytes = vld1_u8(y + i); - uint8x8_t cr_bytes = vld1_u8(pcr + i); - uint8x8_t cb_bytes = vld1_u8(pcb + i); - int8x8_t cr_biased = vreinterpret_s8_u8(vsub_u8(cr_bytes, signflip)); - int8x8_t cb_biased = vreinterpret_s8_u8(vsub_u8(cb_bytes, signflip)); - - int16x8_t yws = vreinterpretq_s16_u16(vshll_n_u8(y_bytes, 4)); - int16x8_t crw = vshll_n_s8(cr_biased, 7); - int16x8_t cbw = vshll_n_s8(cb_biased, 7); - - int16x8_t cr0 = vqdmulhq_s16(crw, cr_const0); - int16x8_t cb0 = vqdmulhq_s16(cbw, cb_const0); - int16x8_t cr1 = vqdmulhq_s16(crw, cr_const1); - int16x8_t cb1 = vqdmulhq_s16(cbw, cb_const1); - int16x8_t rws = vaddq_s16(yws, cr0); - int16x8_t gws = vaddq_s16(vaddq_s16(yws, cb0), cr1); - int16x8_t bws = vaddq_s16(yws, cb1); - - uint8x8x4_t o; - o.val[0] = vqrshrun_n_s16(rws, 4); - o.val[1] = vqrshrun_n_s16(gws, 4); - o.val[2] = vqrshrun_n_s16(bws, 4); - o.val[3] = vdup_n_u8(255); - - vst4_u8(out, o); - out += 8*4; - } - } -#endif - - for (; i < count; ++i) { - int y_fixed = (y[i] << 20) + (1<<19); // rounding - int r,g,b; - int cr = pcr[i] - 128; - int cb = pcb[i] - 128; - r = y_fixed + cr* stbi__float2fixed(1.40200f); - g = y_fixed + cr*-stbi__float2fixed(0.71414f) + ((cb*-stbi__float2fixed(0.34414f)) & 0xffff0000); - b = y_fixed + cb* stbi__float2fixed(1.77200f); - r >>= 20; - g >>= 20; - b >>= 20; - if ((unsigned) r > 255) { if (r < 0) r = 0; else r = 255; } - if ((unsigned) g > 255) { if (g < 0) g = 0; else g = 255; } - if ((unsigned) b > 255) { if (b < 0) b = 0; else b = 255; } - out[0] = (stbi_uc)r; - out[1] = (stbi_uc)g; - out[2] = (stbi_uc)b; - out[3] = 255; - out += step; - } -} -#endif - -static void stbi__setup_jpeg(stbi__jpeg *j) -{ - j->idct_block_kernel = stbi__idct_block; - j->YCbCr_to_RGB_kernel = stbi__YCbCr_to_RGB_row; - j->resample_row_hv_2_kernel = stbi__resample_row_hv_2; - -#ifdef STBI_SSE2 - if (stbi__sse2_available()) { - j->idct_block_kernel = stbi__idct_simd; - j->YCbCr_to_RGB_kernel = stbi__YCbCr_to_RGB_simd; - j->resample_row_hv_2_kernel = stbi__resample_row_hv_2_simd; - } -#endif - -#ifdef STBI_NEON - j->idct_block_kernel = stbi__idct_simd; - j->YCbCr_to_RGB_kernel = stbi__YCbCr_to_RGB_simd; - j->resample_row_hv_2_kernel = stbi__resample_row_hv_2_simd; -#endif -} - -static void stbi__cleanup_jpeg(stbi__jpeg *j) -{ - stbi__free_jpeg_components(j, j->s->img_n, 0); -} - -typedef struct -{ - resample_row_func resample; - stbi_uc *line0,*line1; - int hs,vs; // expansion factor in each axis - int w_lores; // horizontal pixels pre-expansion - int ystep; // how far through vertical expansion we are - int ypos; // which pre-expansion row we're on -} stbi__resample; - -static stbi_uc stbi__blinn_8x8(stbi_uc x, stbi_uc y) -{ - unsigned int t = x*y + 128; - return (stbi_uc) ((t + (t >>8)) >> 8); -} - -static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp, int req_comp) -{ - int n, decode_n, is_rgb; - z->s->img_n = 0; // make stbi__cleanup_jpeg safe - - if (req_comp < 0 || req_comp > 4) return stbi__errpuc("bad req_comp", "Internal error"); - - if (!stbi__decode_jpeg_image(z)) { stbi__cleanup_jpeg(z); return NULL; } - - n = req_comp ? req_comp : z->s->img_n >= 3 ? 3 : 1; - - is_rgb = z->s->img_n == 3 && (z->rgb == 3 || (z->app14_color_transform == 0 && !z->jfif)); - - if (z->s->img_n == 3 && n < 3 && !is_rgb) - decode_n = 1; - else - decode_n = z->s->img_n; - - if (decode_n <= 0) { stbi__cleanup_jpeg(z); return NULL; } - - { - int k; - unsigned int i,j; - stbi_uc *output; - stbi_uc *coutput[4] = { NULL, NULL, NULL, NULL }; - - stbi__resample res_comp[4]; - - for (k=0; k < decode_n; ++k) { - stbi__resample *r = &res_comp[k]; - - z->img_comp[k].linebuf = (stbi_uc *) stbi__malloc(z->s->img_x + 3); - if (!z->img_comp[k].linebuf) { stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); } - - r->hs = z->img_h_max / z->img_comp[k].h; - r->vs = z->img_v_max / z->img_comp[k].v; - r->ystep = r->vs >> 1; - r->w_lores = (z->s->img_x + r->hs-1) / r->hs; - r->ypos = 0; - r->line0 = r->line1 = z->img_comp[k].data; - - if (r->hs == 1 && r->vs == 1) r->resample = resample_row_1; - else if (r->hs == 1 && r->vs == 2) r->resample = stbi__resample_row_v_2; - else if (r->hs == 2 && r->vs == 1) r->resample = stbi__resample_row_h_2; - else if (r->hs == 2 && r->vs == 2) r->resample = z->resample_row_hv_2_kernel; - else r->resample = stbi__resample_row_generic; - } - - output = (stbi_uc *) stbi__malloc_mad3(n, z->s->img_x, z->s->img_y, 1); - if (!output) { stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); } - - for (j=0; j < z->s->img_y; ++j) { - stbi_uc *out = output + n * z->s->img_x * j; - for (k=0; k < decode_n; ++k) { - stbi__resample *r = &res_comp[k]; - int y_bot = r->ystep >= (r->vs >> 1); - coutput[k] = r->resample(z->img_comp[k].linebuf, - y_bot ? r->line1 : r->line0, - y_bot ? r->line0 : r->line1, - r->w_lores, r->hs); - if (++r->ystep >= r->vs) { - r->ystep = 0; - r->line0 = r->line1; - if (++r->ypos < z->img_comp[k].y) - r->line1 += z->img_comp[k].w2; - } - } - if (n >= 3) { - stbi_uc *y = coutput[0]; - if (z->s->img_n == 3) { - if (is_rgb) { - for (i=0; i < z->s->img_x; ++i) { - out[0] = y[i]; - out[1] = coutput[1][i]; - out[2] = coutput[2][i]; - out[3] = 255; - out += n; - } - } else { - z->YCbCr_to_RGB_kernel(out, y, coutput[1], coutput[2], z->s->img_x, n); - } - } else if (z->s->img_n == 4) { - if (z->app14_color_transform == 0) { // CMYK - for (i=0; i < z->s->img_x; ++i) { - stbi_uc m = coutput[3][i]; - out[0] = stbi__blinn_8x8(coutput[0][i], m); - out[1] = stbi__blinn_8x8(coutput[1][i], m); - out[2] = stbi__blinn_8x8(coutput[2][i], m); - out[3] = 255; - out += n; - } - } else if (z->app14_color_transform == 2) { // YCCK - z->YCbCr_to_RGB_kernel(out, y, coutput[1], coutput[2], z->s->img_x, n); - for (i=0; i < z->s->img_x; ++i) { - stbi_uc m = coutput[3][i]; - out[0] = stbi__blinn_8x8(255 - out[0], m); - out[1] = stbi__blinn_8x8(255 - out[1], m); - out[2] = stbi__blinn_8x8(255 - out[2], m); - out += n; - } - } else { // YCbCr + alpha? Ignore the fourth channel for now - z->YCbCr_to_RGB_kernel(out, y, coutput[1], coutput[2], z->s->img_x, n); - } - } else - for (i=0; i < z->s->img_x; ++i) { - out[0] = out[1] = out[2] = y[i]; - out[3] = 255; // not used if n==3 - out += n; - } - } else { - if (is_rgb) { - if (n == 1) - for (i=0; i < z->s->img_x; ++i) - *out++ = stbi__compute_y(coutput[0][i], coutput[1][i], coutput[2][i]); - else { - for (i=0; i < z->s->img_x; ++i, out += 2) { - out[0] = stbi__compute_y(coutput[0][i], coutput[1][i], coutput[2][i]); - out[1] = 255; - } - } - } else if (z->s->img_n == 4 && z->app14_color_transform == 0) { - for (i=0; i < z->s->img_x; ++i) { - stbi_uc m = coutput[3][i]; - stbi_uc r = stbi__blinn_8x8(coutput[0][i], m); - stbi_uc g = stbi__blinn_8x8(coutput[1][i], m); - stbi_uc b = stbi__blinn_8x8(coutput[2][i], m); - out[0] = stbi__compute_y(r, g, b); - out[1] = 255; - out += n; - } - } else if (z->s->img_n == 4 && z->app14_color_transform == 2) { - for (i=0; i < z->s->img_x; ++i) { - out[0] = stbi__blinn_8x8(255 - coutput[0][i], coutput[3][i]); - out[1] = 255; - out += n; - } - } else { - stbi_uc *y = coutput[0]; - if (n == 1) - for (i=0; i < z->s->img_x; ++i) out[i] = y[i]; - else - for (i=0; i < z->s->img_x; ++i) { *out++ = y[i]; *out++ = 255; } - } - } - } - stbi__cleanup_jpeg(z); - *out_x = z->s->img_x; - *out_y = z->s->img_y; - if (comp) *comp = z->s->img_n >= 3 ? 3 : 1; // report original components, not output - return output; - } -} - -static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) -{ - unsigned char* result; - stbi__jpeg* j = (stbi__jpeg*) stbi__malloc(sizeof(stbi__jpeg)); - if (!j) return stbi__errpuc("outofmem", "Out of memory"); - memset(j, 0, sizeof(stbi__jpeg)); - STBI_NOTUSED(ri); - j->s = s; - stbi__setup_jpeg(j); - result = load_jpeg_image(j, x,y,comp,req_comp); - STBI_FREE(j); - return result; -} - -static int stbi__jpeg_test(stbi__context *s) -{ - int r; - stbi__jpeg* j = (stbi__jpeg*)stbi__malloc(sizeof(stbi__jpeg)); - if (!j) return stbi__err("outofmem", "Out of memory"); - memset(j, 0, sizeof(stbi__jpeg)); - j->s = s; - stbi__setup_jpeg(j); - r = stbi__decode_jpeg_header(j, STBI__SCAN_type); - stbi__rewind(s); - STBI_FREE(j); - return r; -} - -static int stbi__jpeg_info_raw(stbi__jpeg *j, int *x, int *y, int *comp) -{ - if (!stbi__decode_jpeg_header(j, STBI__SCAN_header)) { - stbi__rewind( j->s ); - return 0; - } - if (x) *x = j->s->img_x; - if (y) *y = j->s->img_y; - if (comp) *comp = j->s->img_n >= 3 ? 3 : 1; - return 1; -} - -static int stbi__jpeg_info(stbi__context *s, int *x, int *y, int *comp) -{ - int result; - stbi__jpeg* j = (stbi__jpeg*) (stbi__malloc(sizeof(stbi__jpeg))); - if (!j) return stbi__err("outofmem", "Out of memory"); - memset(j, 0, sizeof(stbi__jpeg)); - j->s = s; - result = stbi__jpeg_info_raw(j, x, y, comp); - STBI_FREE(j); - return result; -} -#endif - -#ifndef STBI_NO_ZLIB - -#define STBI__ZFAST_BITS 9 // accelerate all cases in default tables -#define STBI__ZFAST_MASK ((1 << STBI__ZFAST_BITS) - 1) -#define STBI__ZNSYMS 288 // number of symbols in literal/length alphabet - -typedef struct -{ - stbi__uint16 fast[1 << STBI__ZFAST_BITS]; - stbi__uint16 firstcode[16]; - int maxcode[17]; - stbi__uint16 firstsymbol[16]; - stbi_uc size[STBI__ZNSYMS]; - stbi__uint16 value[STBI__ZNSYMS]; -} stbi__zhuffman; - -stbi_inline static int stbi__bitreverse16(int n) -{ - n = ((n & 0xAAAA) >> 1) | ((n & 0x5555) << 1); - n = ((n & 0xCCCC) >> 2) | ((n & 0x3333) << 2); - n = ((n & 0xF0F0) >> 4) | ((n & 0x0F0F) << 4); - n = ((n & 0xFF00) >> 8) | ((n & 0x00FF) << 8); - return n; -} - -stbi_inline static int stbi__bit_reverse(int v, int bits) -{ - STBI_ASSERT(bits <= 16); - return stbi__bitreverse16(v) >> (16-bits); -} - -static int stbi__zbuild_huffman(stbi__zhuffman *z, const stbi_uc *sizelist, int num) -{ - int i,k=0; - int code, next_code[16], sizes[17]; - - memset(sizes, 0, sizeof(sizes)); - memset(z->fast, 0, sizeof(z->fast)); - for (i=0; i < num; ++i) - ++sizes[sizelist[i]]; - sizes[0] = 0; - for (i=1; i < 16; ++i) - if (sizes[i] > (1 << i)) - return stbi__err("bad sizes", "Corrupt PNG"); - code = 0; - for (i=1; i < 16; ++i) { - next_code[i] = code; - z->firstcode[i] = (stbi__uint16) code; - z->firstsymbol[i] = (stbi__uint16) k; - code = (code + sizes[i]); - if (sizes[i]) - if (code-1 >= (1 << i)) return stbi__err("bad codelengths","Corrupt PNG"); - z->maxcode[i] = code << (16-i); // preshift for inner loop - code <<= 1; - k += sizes[i]; - } - z->maxcode[16] = 0x10000; // sentinel - for (i=0; i < num; ++i) { - int s = sizelist[i]; - if (s) { - int c = next_code[s] - z->firstcode[s] + z->firstsymbol[s]; - stbi__uint16 fastv = (stbi__uint16) ((s << 9) | i); - z->size [c] = (stbi_uc ) s; - z->value[c] = (stbi__uint16) i; - if (s <= STBI__ZFAST_BITS) { - int j = stbi__bit_reverse(next_code[s],s); - while (j < (1 << STBI__ZFAST_BITS)) { - z->fast[j] = fastv; - j += (1 << s); - } - } - ++next_code[s]; - } - } - return 1; -} - -typedef struct -{ - stbi_uc *zbuffer, *zbuffer_end; - int num_bits; - int hit_zeof_once; - stbi__uint32 code_buffer; - - char *zout; - char *zout_start; - char *zout_end; - int z_expandable; - - stbi__zhuffman z_length, z_distance; -} stbi__zbuf; - -stbi_inline static int stbi__zeof(stbi__zbuf *z) -{ - return (z->zbuffer >= z->zbuffer_end); -} - -stbi_inline static stbi_uc stbi__zget8(stbi__zbuf *z) -{ - return stbi__zeof(z) ? 0 : *z->zbuffer++; -} - -static void stbi__fill_bits(stbi__zbuf *z) -{ - do { - if (z->code_buffer >= (1U << z->num_bits)) { - z->zbuffer = z->zbuffer_end; /* treat this as EOF so we fail. */ - return; - } - z->code_buffer |= (unsigned int) stbi__zget8(z) << z->num_bits; - z->num_bits += 8; - } while (z->num_bits <= 24); -} - -stbi_inline static unsigned int stbi__zreceive(stbi__zbuf *z, int n) -{ - unsigned int k; - if (z->num_bits < n) stbi__fill_bits(z); - k = z->code_buffer & ((1 << n) - 1); - z->code_buffer >>= n; - z->num_bits -= n; - return k; -} - -static int stbi__zhuffman_decode_slowpath(stbi__zbuf *a, stbi__zhuffman *z) -{ - int b,s,k; - k = stbi__bit_reverse(a->code_buffer, 16); - for (s=STBI__ZFAST_BITS+1; ; ++s) - if (k < z->maxcode[s]) - break; - if (s >= 16) return -1; // invalid code! - b = (k >> (16-s)) - z->firstcode[s] + z->firstsymbol[s]; - if (b >= STBI__ZNSYMS) return -1; // some data was corrupt somewhere! - if (z->size[b] != s) return -1; // was originally an assert, but report failure instead. - a->code_buffer >>= s; - a->num_bits -= s; - return z->value[b]; -} - -stbi_inline static int stbi__zhuffman_decode(stbi__zbuf *a, stbi__zhuffman *z) -{ - int b,s; - if (a->num_bits < 16) { - if (stbi__zeof(a)) { - if (!a->hit_zeof_once) { - a->hit_zeof_once = 1; - a->num_bits += 16; // add 16 implicit zero bits - } else { - return -1; - } - } else { - stbi__fill_bits(a); - } - } - b = z->fast[a->code_buffer & STBI__ZFAST_MASK]; - if (b) { - s = b >> 9; - a->code_buffer >>= s; - a->num_bits -= s; - return b & 511; - } - return stbi__zhuffman_decode_slowpath(a, z); -} - -static int stbi__zexpand(stbi__zbuf *z, char *zout, int n) // need to make room for n bytes -{ - char *q; - unsigned int cur, limit, old_limit; - z->zout = zout; - if (!z->z_expandable) return stbi__err("output buffer limit","Corrupt PNG"); - cur = (unsigned int) (z->zout - z->zout_start); - limit = old_limit = (unsigned) (z->zout_end - z->zout_start); - if (UINT_MAX - cur < (unsigned) n) return stbi__err("outofmem", "Out of memory"); - while (cur + n > limit) { - if(limit > UINT_MAX / 2) return stbi__err("outofmem", "Out of memory"); - limit *= 2; - } - q = (char *) STBI_REALLOC_SIZED(z->zout_start, old_limit, limit); - STBI_NOTUSED(old_limit); - if (q == NULL) return stbi__err("outofmem", "Out of memory"); - z->zout_start = q; - z->zout = q + cur; - z->zout_end = q + limit; - return 1; -} - -static const int stbi__zlength_base[31] = { - 3,4,5,6,7,8,9,10,11,13, - 15,17,19,23,27,31,35,43,51,59, - 67,83,99,115,131,163,195,227,258,0,0 }; - -static const int stbi__zlength_extra[31]= -{ 0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0 }; - -static const int stbi__zdist_base[32] = { 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193, -257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0}; - -static const int stbi__zdist_extra[32] = -{ 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13}; - -static int stbi__parse_huffman_block(stbi__zbuf *a) -{ - char *zout = a->zout; - for(;;) { - int z = stbi__zhuffman_decode(a, &a->z_length); - if (z < 256) { - if (z < 0) return stbi__err("bad huffman code","Corrupt PNG"); // error in huffman codes - if (zout >= a->zout_end) { - if (!stbi__zexpand(a, zout, 1)) return 0; - zout = a->zout; - } - *zout++ = (char) z; - } else { - stbi_uc *p; - int len,dist; - if (z == 256) { - a->zout = zout; - if (a->hit_zeof_once && a->num_bits < 16) { - return stbi__err("unexpected end","Corrupt PNG"); - } - return 1; - } - if (z >= 286) return stbi__err("bad huffman code","Corrupt PNG"); // per DEFLATE, length codes 286 and 287 must not appear in compressed data - z -= 257; - len = stbi__zlength_base[z]; - if (stbi__zlength_extra[z]) len += stbi__zreceive(a, stbi__zlength_extra[z]); - z = stbi__zhuffman_decode(a, &a->z_distance); - if (z < 0 || z >= 30) return stbi__err("bad huffman code","Corrupt PNG"); // per DEFLATE, distance codes 30 and 31 must not appear in compressed data - dist = stbi__zdist_base[z]; - if (stbi__zdist_extra[z]) dist += stbi__zreceive(a, stbi__zdist_extra[z]); - if (zout - a->zout_start < dist) return stbi__err("bad dist","Corrupt PNG"); - if (len > a->zout_end - zout) { - if (!stbi__zexpand(a, zout, len)) return 0; - zout = a->zout; - } - p = (stbi_uc *) (zout - dist); - if (dist == 1) { // run of one byte; common in images. - stbi_uc v = *p; - if (len) { do *zout++ = v; while (--len); } - } else { - if (len) { do *zout++ = *p++; while (--len); } - } - } - } -} - -static int stbi__compute_huffman_codes(stbi__zbuf *a) -{ - static const stbi_uc length_dezigzag[19] = { 16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15 }; - stbi__zhuffman z_codelength; - stbi_uc lencodes[286+32+137];//padding for maximum single op - stbi_uc codelength_sizes[19]; - int i,n; - - int hlit = stbi__zreceive(a,5) + 257; - int hdist = stbi__zreceive(a,5) + 1; - int hclen = stbi__zreceive(a,4) + 4; - int ntot = hlit + hdist; - - memset(codelength_sizes, 0, sizeof(codelength_sizes)); - for (i=0; i < hclen; ++i) { - int s = stbi__zreceive(a,3); - codelength_sizes[length_dezigzag[i]] = (stbi_uc) s; - } - if (!stbi__zbuild_huffman(&z_codelength, codelength_sizes, 19)) return 0; - - n = 0; - while (n < ntot) { - int c = stbi__zhuffman_decode(a, &z_codelength); - if (c < 0 || c >= 19) return stbi__err("bad codelengths", "Corrupt PNG"); - if (c < 16) - lencodes[n++] = (stbi_uc) c; - else { - stbi_uc fill = 0; - if (c == 16) { - c = stbi__zreceive(a,2)+3; - if (n == 0) return stbi__err("bad codelengths", "Corrupt PNG"); - fill = lencodes[n-1]; - } else if (c == 17) { - c = stbi__zreceive(a,3)+3; - } else if (c == 18) { - c = stbi__zreceive(a,7)+11; - } else { - return stbi__err("bad codelengths", "Corrupt PNG"); - } - if (ntot - n < c) return stbi__err("bad codelengths", "Corrupt PNG"); - memset(lencodes+n, fill, c); - n += c; - } - } - if (n != ntot) return stbi__err("bad codelengths","Corrupt PNG"); - if (!stbi__zbuild_huffman(&a->z_length, lencodes, hlit)) return 0; - if (!stbi__zbuild_huffman(&a->z_distance, lencodes+hlit, hdist)) return 0; - return 1; -} - -static int stbi__parse_uncompressed_block(stbi__zbuf *a) -{ - stbi_uc header[4]; - int len,nlen,k; - if (a->num_bits & 7) - stbi__zreceive(a, a->num_bits & 7); // discard - k = 0; - while (a->num_bits > 0) { - header[k++] = (stbi_uc) (a->code_buffer & 255); // suppress MSVC run-time check - a->code_buffer >>= 8; - a->num_bits -= 8; - } - if (a->num_bits < 0) return stbi__err("zlib corrupt","Corrupt PNG"); - while (k < 4) - header[k++] = stbi__zget8(a); - len = header[1] * 256 + header[0]; - nlen = header[3] * 256 + header[2]; - if (nlen != (len ^ 0xffff)) return stbi__err("zlib corrupt","Corrupt PNG"); - if (a->zbuffer + len > a->zbuffer_end) return stbi__err("read past buffer","Corrupt PNG"); - if (a->zout + len > a->zout_end) - if (!stbi__zexpand(a, a->zout, len)) return 0; - memcpy(a->zout, a->zbuffer, len); - a->zbuffer += len; - a->zout += len; - return 1; -} - -static int stbi__parse_zlib_header(stbi__zbuf *a) -{ - int cmf = stbi__zget8(a); - int cm = cmf & 15; - int flg = stbi__zget8(a); - if (stbi__zeof(a)) return stbi__err("bad zlib header","Corrupt PNG"); // zlib spec - if ((cmf*256+flg) % 31 != 0) return stbi__err("bad zlib header","Corrupt PNG"); // zlib spec - if (flg & 32) return stbi__err("no preset dict","Corrupt PNG"); // preset dictionary not allowed in png - if (cm != 8) return stbi__err("bad compression","Corrupt PNG"); // DEFLATE required for png - return 1; -} - -static const stbi_uc stbi__zdefault_length[STBI__ZNSYMS] = -{ - 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, - 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, - 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, - 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, - 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, - 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, - 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, - 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, - 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8 -}; -static const stbi_uc stbi__zdefault_distance[32] = -{ - 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5 -}; -/* -Init algorithm: -{ - int i; // use <= to match clearly with spec - for (i=0; i <= 143; ++i) stbi__zdefault_length[i] = 8; - for ( ; i <= 255; ++i) stbi__zdefault_length[i] = 9; - for ( ; i <= 279; ++i) stbi__zdefault_length[i] = 7; - for ( ; i <= 287; ++i) stbi__zdefault_length[i] = 8; - - for (i=0; i <= 31; ++i) stbi__zdefault_distance[i] = 5; -} -*/ - -static int stbi__parse_zlib(stbi__zbuf *a, int parse_header) -{ - int final, type; - if (parse_header) - if (!stbi__parse_zlib_header(a)) return 0; - a->num_bits = 0; - a->code_buffer = 0; - a->hit_zeof_once = 0; - do { - final = stbi__zreceive(a,1); - type = stbi__zreceive(a,2); - if (type == 0) { - if (!stbi__parse_uncompressed_block(a)) return 0; - } else if (type == 3) { - return 0; - } else { - if (type == 1) { - if (!stbi__zbuild_huffman(&a->z_length , stbi__zdefault_length , STBI__ZNSYMS)) return 0; - if (!stbi__zbuild_huffman(&a->z_distance, stbi__zdefault_distance, 32)) return 0; - } else { - if (!stbi__compute_huffman_codes(a)) return 0; - } - if (!stbi__parse_huffman_block(a)) return 0; - } - } while (!final); - return 1; -} - -static int stbi__do_zlib(stbi__zbuf *a, char *obuf, int olen, int exp, int parse_header) -{ - a->zout_start = obuf; - a->zout = obuf; - a->zout_end = obuf + olen; - a->z_expandable = exp; - - return stbi__parse_zlib(a, parse_header); -} - -STBIDEF char *stbi_zlib_decode_malloc_guesssize(const char *buffer, int len, int initial_size, int *outlen) -{ - stbi__zbuf a; - char *p = (char *) stbi__malloc(initial_size); - if (p == NULL) return NULL; - a.zbuffer = (stbi_uc *) buffer; - a.zbuffer_end = (stbi_uc *) buffer + len; - if (stbi__do_zlib(&a, p, initial_size, 1, 1)) { - if (outlen) *outlen = (int) (a.zout - a.zout_start); - return a.zout_start; - } else { - STBI_FREE(a.zout_start); - return NULL; - } -} - -STBIDEF char *stbi_zlib_decode_malloc(char const *buffer, int len, int *outlen) -{ - return stbi_zlib_decode_malloc_guesssize(buffer, len, 16384, outlen); -} - -STBIDEF char *stbi_zlib_decode_malloc_guesssize_headerflag(const char *buffer, int len, int initial_size, int *outlen, int parse_header) -{ - stbi__zbuf a; - char *p = (char *) stbi__malloc(initial_size); - if (p == NULL) return NULL; - a.zbuffer = (stbi_uc *) buffer; - a.zbuffer_end = (stbi_uc *) buffer + len; - if (stbi__do_zlib(&a, p, initial_size, 1, parse_header)) { - if (outlen) *outlen = (int) (a.zout - a.zout_start); - return a.zout_start; - } else { - STBI_FREE(a.zout_start); - return NULL; - } -} - -STBIDEF int stbi_zlib_decode_buffer(char *obuffer, int olen, char const *ibuffer, int ilen) -{ - stbi__zbuf a; - a.zbuffer = (stbi_uc *) ibuffer; - a.zbuffer_end = (stbi_uc *) ibuffer + ilen; - if (stbi__do_zlib(&a, obuffer, olen, 0, 1)) - return (int) (a.zout - a.zout_start); - else - return -1; -} - -STBIDEF char *stbi_zlib_decode_noheader_malloc(char const *buffer, int len, int *outlen) -{ - stbi__zbuf a; - char *p = (char *) stbi__malloc(16384); - if (p == NULL) return NULL; - a.zbuffer = (stbi_uc *) buffer; - a.zbuffer_end = (stbi_uc *) buffer+len; - if (stbi__do_zlib(&a, p, 16384, 1, 0)) { - if (outlen) *outlen = (int) (a.zout - a.zout_start); - return a.zout_start; - } else { - STBI_FREE(a.zout_start); - return NULL; - } -} - -STBIDEF int stbi_zlib_decode_noheader_buffer(char *obuffer, int olen, const char *ibuffer, int ilen) -{ - stbi__zbuf a; - a.zbuffer = (stbi_uc *) ibuffer; - a.zbuffer_end = (stbi_uc *) ibuffer + ilen; - if (stbi__do_zlib(&a, obuffer, olen, 0, 0)) - return (int) (a.zout - a.zout_start); - else - return -1; -} -#endif - -#ifndef STBI_NO_PNG -typedef struct -{ - stbi__uint32 length; - stbi__uint32 type; -} stbi__pngchunk; - -static stbi__pngchunk stbi__get_chunk_header(stbi__context *s) -{ - stbi__pngchunk c; - c.length = stbi__get32be(s); - c.type = stbi__get32be(s); - return c; -} - -static int stbi__check_png_header(stbi__context *s) -{ - static const stbi_uc png_sig[8] = { 137,80,78,71,13,10,26,10 }; - int i; - for (i=0; i < 8; ++i) - if (stbi__get8(s) != png_sig[i]) return stbi__err("bad png sig","Not a PNG"); - return 1; -} - -typedef struct -{ - stbi__context *s; - stbi_uc *idata, *expanded, *out; - int depth; -} stbi__png; - -enum { - STBI__F_none=0, - STBI__F_sub=1, - STBI__F_up=2, - STBI__F_avg=3, - STBI__F_paeth=4, - STBI__F_avg_first -}; - -static stbi_uc first_row_filter[5] = -{ - STBI__F_none, - STBI__F_sub, - STBI__F_none, - STBI__F_avg_first, - STBI__F_sub // Paeth with b=c=0 turns out to be equivalent to sub -}; - -static int stbi__paeth(int a, int b, int c) -{ - int thresh = c*3 - (a + b); - int lo = a < b ? a : b; - int hi = a < b ? b : a; - int t0 = (hi <= thresh) ? lo : c; - int t1 = (thresh <= lo) ? hi : t0; - return t1; -} - -static const stbi_uc stbi__depth_scale_table[9] = { 0, 0xff, 0x55, 0, 0x11, 0,0,0, 0x01 }; - -static void stbi__create_png_alpha_expand8(stbi_uc *dest, stbi_uc *src, stbi__uint32 x, int img_n) -{ - int i; - if (img_n == 1) { - for (i=x-1; i >= 0; --i) { - dest[i*2+1] = 255; - dest[i*2+0] = src[i]; - } - } else { - STBI_ASSERT(img_n == 3); - for (i=x-1; i >= 0; --i) { - dest[i*4+3] = 255; - dest[i*4+2] = src[i*3+2]; - dest[i*4+1] = src[i*3+1]; - dest[i*4+0] = src[i*3+0]; - } - } -} - -static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 raw_len, int out_n, stbi__uint32 x, stbi__uint32 y, int depth, int color) -{ - int bytes = (depth == 16 ? 2 : 1); - stbi__context *s = a->s; - stbi__uint32 i,j,stride = x*out_n*bytes; - stbi__uint32 img_len, img_width_bytes; - stbi_uc *filter_buf; - int all_ok = 1; - int k; - int img_n = s->img_n; // copy it into a local for later - - int output_bytes = out_n*bytes; - int filter_bytes = img_n*bytes; - int width = x; - - STBI_ASSERT(out_n == s->img_n || out_n == s->img_n+1); - a->out = (stbi_uc *) stbi__malloc_mad3(x, y, output_bytes, 0); // extra bytes to write off the end into - if (!a->out) return stbi__err("outofmem", "Out of memory"); - - if (!stbi__mad3sizes_valid(img_n, x, depth, 7)) return stbi__err("too large", "Corrupt PNG"); - img_width_bytes = (((img_n * x * depth) + 7) >> 3); - if (!stbi__mad2sizes_valid(img_width_bytes, y, img_width_bytes)) return stbi__err("too large", "Corrupt PNG"); - img_len = (img_width_bytes + 1) * y; - - if (raw_len < img_len) return stbi__err("not enough pixels","Corrupt PNG"); - - filter_buf = (stbi_uc *) stbi__malloc_mad2(img_width_bytes, 2, 0); - if (!filter_buf) return stbi__err("outofmem", "Out of memory"); - - if (depth < 8) { - filter_bytes = 1; - width = img_width_bytes; - } - - for (j=0; j < y; ++j) { - stbi_uc *cur = filter_buf + (j & 1)*img_width_bytes; - stbi_uc *prior = filter_buf + (~j & 1)*img_width_bytes; - stbi_uc *dest = a->out + stride*j; - int nk = width * filter_bytes; - int filter = *raw++; - - if (filter > 4) { - all_ok = stbi__err("invalid filter","Corrupt PNG"); - break; - } - - if (j == 0) filter = first_row_filter[filter]; - - switch (filter) { - case STBI__F_none: - memcpy(cur, raw, nk); - break; - case STBI__F_sub: - memcpy(cur, raw, filter_bytes); - for (k = filter_bytes; k < nk; ++k) - cur[k] = STBI__BYTECAST(raw[k] + cur[k-filter_bytes]); - break; - case STBI__F_up: - for (k = 0; k < nk; ++k) - cur[k] = STBI__BYTECAST(raw[k] + prior[k]); - break; - case STBI__F_avg: - for (k = 0; k < filter_bytes; ++k) - cur[k] = STBI__BYTECAST(raw[k] + (prior[k]>>1)); - for (k = filter_bytes; k < nk; ++k) - cur[k] = STBI__BYTECAST(raw[k] + ((prior[k] + cur[k-filter_bytes])>>1)); - break; - case STBI__F_paeth: - for (k = 0; k < filter_bytes; ++k) - cur[k] = STBI__BYTECAST(raw[k] + prior[k]); // prior[k] == stbi__paeth(0,prior[k],0) - for (k = filter_bytes; k < nk; ++k) - cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k-filter_bytes], prior[k], prior[k-filter_bytes])); - break; - case STBI__F_avg_first: - memcpy(cur, raw, filter_bytes); - for (k = filter_bytes; k < nk; ++k) - cur[k] = STBI__BYTECAST(raw[k] + (cur[k-filter_bytes] >> 1)); - break; - } - - raw += nk; - - if (depth < 8) { - stbi_uc scale = (color == 0) ? stbi__depth_scale_table[depth] : 1; // scale grayscale values to 0..255 range - stbi_uc *in = cur; - stbi_uc *out = dest; - stbi_uc inb = 0; - stbi__uint32 nsmp = x*img_n; - - if (depth == 4) { - for (i=0; i < nsmp; ++i) { - if ((i & 1) == 0) inb = *in++; - *out++ = scale * (inb >> 4); - inb <<= 4; - } - } else if (depth == 2) { - for (i=0; i < nsmp; ++i) { - if ((i & 3) == 0) inb = *in++; - *out++ = scale * (inb >> 6); - inb <<= 2; - } - } else { - STBI_ASSERT(depth == 1); - for (i=0; i < nsmp; ++i) { - if ((i & 7) == 0) inb = *in++; - *out++ = scale * (inb >> 7); - inb <<= 1; - } - } - - if (img_n != out_n) - stbi__create_png_alpha_expand8(dest, dest, x, img_n); - } else if (depth == 8) { - if (img_n == out_n) - memcpy(dest, cur, x*img_n); - else - stbi__create_png_alpha_expand8(dest, cur, x, img_n); - } else if (depth == 16) { - stbi__uint16 *dest16 = (stbi__uint16*)dest; - stbi__uint32 nsmp = x*img_n; - - if (img_n == out_n) { - for (i = 0; i < nsmp; ++i, ++dest16, cur += 2) - *dest16 = (cur[0] << 8) | cur[1]; - } else { - STBI_ASSERT(img_n+1 == out_n); - if (img_n == 1) { - for (i = 0; i < x; ++i, dest16 += 2, cur += 2) { - dest16[0] = (cur[0] << 8) | cur[1]; - dest16[1] = 0xffff; - } - } else { - STBI_ASSERT(img_n == 3); - for (i = 0; i < x; ++i, dest16 += 4, cur += 6) { - dest16[0] = (cur[0] << 8) | cur[1]; - dest16[1] = (cur[2] << 8) | cur[3]; - dest16[2] = (cur[4] << 8) | cur[5]; - dest16[3] = 0xffff; - } - } - } - } - } - - STBI_FREE(filter_buf); - if (!all_ok) return 0; - - return 1; -} - -static int stbi__create_png_image(stbi__png *a, stbi_uc *image_data, stbi__uint32 image_data_len, int out_n, int depth, int color, int interlaced) -{ - int bytes = (depth == 16 ? 2 : 1); - int out_bytes = out_n * bytes; - stbi_uc *final; - int p; - if (!interlaced) - return stbi__create_png_image_raw(a, image_data, image_data_len, out_n, a->s->img_x, a->s->img_y, depth, color); - - final = (stbi_uc *) stbi__malloc_mad3(a->s->img_x, a->s->img_y, out_bytes, 0); - if (!final) return stbi__err("outofmem", "Out of memory"); - for (p=0; p < 7; ++p) { - int xorig[] = { 0,4,0,2,0,1,0 }; - int yorig[] = { 0,0,4,0,2,0,1 }; - int xspc[] = { 8,8,4,4,2,2,1 }; - int yspc[] = { 8,8,8,4,4,2,2 }; - int i,j,x,y; - x = (a->s->img_x - xorig[p] + xspc[p]-1) / xspc[p]; - y = (a->s->img_y - yorig[p] + yspc[p]-1) / yspc[p]; - if (x && y) { - stbi__uint32 img_len = ((((a->s->img_n * x * depth) + 7) >> 3) + 1) * y; - if (!stbi__create_png_image_raw(a, image_data, image_data_len, out_n, x, y, depth, color)) { - STBI_FREE(final); - return 0; - } - for (j=0; j < y; ++j) { - for (i=0; i < x; ++i) { - int out_y = j*yspc[p]+yorig[p]; - int out_x = i*xspc[p]+xorig[p]; - memcpy(final + out_y*a->s->img_x*out_bytes + out_x*out_bytes, - a->out + (j*x+i)*out_bytes, out_bytes); - } - } - STBI_FREE(a->out); - image_data += img_len; - image_data_len -= img_len; - } - } - a->out = final; - - return 1; -} - -static int stbi__compute_transparency(stbi__png *z, stbi_uc tc[3], int out_n) -{ - stbi__context *s = z->s; - stbi__uint32 i, pixel_count = s->img_x * s->img_y; - stbi_uc *p = z->out; - - STBI_ASSERT(out_n == 2 || out_n == 4); - - if (out_n == 2) { - for (i=0; i < pixel_count; ++i) { - p[1] = (p[0] == tc[0] ? 0 : 255); - p += 2; - } - } else { - for (i=0; i < pixel_count; ++i) { - if (p[0] == tc[0] && p[1] == tc[1] && p[2] == tc[2]) - p[3] = 0; - p += 4; - } - } - return 1; -} - -static int stbi__compute_transparency16(stbi__png *z, stbi__uint16 tc[3], int out_n) -{ - stbi__context *s = z->s; - stbi__uint32 i, pixel_count = s->img_x * s->img_y; - stbi__uint16 *p = (stbi__uint16*) z->out; - - STBI_ASSERT(out_n == 2 || out_n == 4); - - if (out_n == 2) { - for (i = 0; i < pixel_count; ++i) { - p[1] = (p[0] == tc[0] ? 0 : 65535); - p += 2; - } - } else { - for (i = 0; i < pixel_count; ++i) { - if (p[0] == tc[0] && p[1] == tc[1] && p[2] == tc[2]) - p[3] = 0; - p += 4; - } - } - return 1; -} - -static int stbi__expand_png_palette(stbi__png *a, stbi_uc *palette, int len, int pal_img_n) -{ - stbi__uint32 i, pixel_count = a->s->img_x * a->s->img_y; - stbi_uc *p, *temp_out, *orig = a->out; - - p = (stbi_uc *) stbi__malloc_mad2(pixel_count, pal_img_n, 0); - if (p == NULL) return stbi__err("outofmem", "Out of memory"); - - temp_out = p; - - if (pal_img_n == 3) { - for (i=0; i < pixel_count; ++i) { - int n = orig[i]*4; - p[0] = palette[n ]; - p[1] = palette[n+1]; - p[2] = palette[n+2]; - p += 3; - } - } else { - for (i=0; i < pixel_count; ++i) { - int n = orig[i]*4; - p[0] = palette[n ]; - p[1] = palette[n+1]; - p[2] = palette[n+2]; - p[3] = palette[n+3]; - p += 4; - } - } - STBI_FREE(a->out); - a->out = temp_out; - - STBI_NOTUSED(len); - - return 1; -} - -static int stbi__unpremultiply_on_load_global = 0; -static int stbi__de_iphone_flag_global = 0; - -STBIDEF void stbi_set_unpremultiply_on_load(int flag_true_if_should_unpremultiply) -{ - stbi__unpremultiply_on_load_global = flag_true_if_should_unpremultiply; -} - -STBIDEF void stbi_convert_iphone_png_to_rgb(int flag_true_if_should_convert) -{ - stbi__de_iphone_flag_global = flag_true_if_should_convert; -} - -#ifndef STBI_THREAD_LOCAL -#define stbi__unpremultiply_on_load stbi__unpremultiply_on_load_global -#define stbi__de_iphone_flag stbi__de_iphone_flag_global -#else -static STBI_THREAD_LOCAL int stbi__unpremultiply_on_load_local, stbi__unpremultiply_on_load_set; -static STBI_THREAD_LOCAL int stbi__de_iphone_flag_local, stbi__de_iphone_flag_set; - -STBIDEF void stbi_set_unpremultiply_on_load_thread(int flag_true_if_should_unpremultiply) -{ - stbi__unpremultiply_on_load_local = flag_true_if_should_unpremultiply; - stbi__unpremultiply_on_load_set = 1; -} - -STBIDEF void stbi_convert_iphone_png_to_rgb_thread(int flag_true_if_should_convert) -{ - stbi__de_iphone_flag_local = flag_true_if_should_convert; - stbi__de_iphone_flag_set = 1; -} - -#define stbi__unpremultiply_on_load (stbi__unpremultiply_on_load_set \ - ? stbi__unpremultiply_on_load_local \ - : stbi__unpremultiply_on_load_global) -#define stbi__de_iphone_flag (stbi__de_iphone_flag_set \ - ? stbi__de_iphone_flag_local \ - : stbi__de_iphone_flag_global) -#endif // STBI_THREAD_LOCAL - -static void stbi__de_iphone(stbi__png *z) -{ - stbi__context *s = z->s; - stbi__uint32 i, pixel_count = s->img_x * s->img_y; - stbi_uc *p = z->out; - - if (s->img_out_n == 3) { // convert bgr to rgb - for (i=0; i < pixel_count; ++i) { - stbi_uc t = p[0]; - p[0] = p[2]; - p[2] = t; - p += 3; - } - } else { - STBI_ASSERT(s->img_out_n == 4); - if (stbi__unpremultiply_on_load) { - for (i=0; i < pixel_count; ++i) { - stbi_uc a = p[3]; - stbi_uc t = p[0]; - if (a) { - stbi_uc half = a / 2; - p[0] = (p[2] * 255 + half) / a; - p[1] = (p[1] * 255 + half) / a; - p[2] = ( t * 255 + half) / a; - } else { - p[0] = p[2]; - p[2] = t; - } - p += 4; - } - } else { - for (i=0; i < pixel_count; ++i) { - stbi_uc t = p[0]; - p[0] = p[2]; - p[2] = t; - p += 4; - } - } - } -} - -#define STBI__PNG_TYPE(a,b,c,d) (((unsigned) (a) << 24) + ((unsigned) (b) << 16) + ((unsigned) (c) << 8) + (unsigned) (d)) - -static int stbi__parse_png_file(stbi__png *z, int scan, int req_comp) -{ - stbi_uc palette[1024], pal_img_n=0; - stbi_uc has_trans=0, tc[3]={0}; - stbi__uint16 tc16[3]; - stbi__uint32 ioff=0, idata_limit=0, i, pal_len=0; - int first=1,k,interlace=0, color=0, is_iphone=0; - stbi__context *s = z->s; - - z->expanded = NULL; - z->idata = NULL; - z->out = NULL; - - if (!stbi__check_png_header(s)) return 0; - - if (scan == STBI__SCAN_type) return 1; - - for (;;) { - stbi__pngchunk c = stbi__get_chunk_header(s); - switch (c.type) { - case STBI__PNG_TYPE('C','g','B','I'): - is_iphone = 1; - stbi__skip(s, c.length); - break; - case STBI__PNG_TYPE('I','H','D','R'): { - int comp,filter; - if (!first) return stbi__err("multiple IHDR","Corrupt PNG"); - first = 0; - if (c.length != 13) return stbi__err("bad IHDR len","Corrupt PNG"); - s->img_x = stbi__get32be(s); - s->img_y = stbi__get32be(s); - if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); - if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); - z->depth = stbi__get8(s); if (z->depth != 1 && z->depth != 2 && z->depth != 4 && z->depth != 8 && z->depth != 16) return stbi__err("1/2/4/8/16-bit only","PNG not supported: 1/2/4/8/16-bit only"); - color = stbi__get8(s); if (color > 6) return stbi__err("bad ctype","Corrupt PNG"); - if (color == 3 && z->depth == 16) return stbi__err("bad ctype","Corrupt PNG"); - if (color == 3) pal_img_n = 3; else if (color & 1) return stbi__err("bad ctype","Corrupt PNG"); - comp = stbi__get8(s); if (comp) return stbi__err("bad comp method","Corrupt PNG"); - filter= stbi__get8(s); if (filter) return stbi__err("bad filter method","Corrupt PNG"); - interlace = stbi__get8(s); if (interlace>1) return stbi__err("bad interlace method","Corrupt PNG"); - if (!s->img_x || !s->img_y) return stbi__err("0-pixel image","Corrupt PNG"); - if (!pal_img_n) { - s->img_n = (color & 2 ? 3 : 1) + (color & 4 ? 1 : 0); - if ((1 << 30) / s->img_x / s->img_n < s->img_y) return stbi__err("too large", "Image too large to decode"); - } else { - s->img_n = 1; - if ((1 << 30) / s->img_x / 4 < s->img_y) return stbi__err("too large","Corrupt PNG"); - } - break; - } - - case STBI__PNG_TYPE('P','L','T','E'): { - if (first) return stbi__err("first not IHDR", "Corrupt PNG"); - if (c.length > 256*3) return stbi__err("invalid PLTE","Corrupt PNG"); - pal_len = c.length / 3; - if (pal_len * 3 != c.length) return stbi__err("invalid PLTE","Corrupt PNG"); - for (i=0; i < pal_len; ++i) { - palette[i*4+0] = stbi__get8(s); - palette[i*4+1] = stbi__get8(s); - palette[i*4+2] = stbi__get8(s); - palette[i*4+3] = 255; - } - break; - } - - case STBI__PNG_TYPE('t','R','N','S'): { - if (first) return stbi__err("first not IHDR", "Corrupt PNG"); - if (z->idata) return stbi__err("tRNS after IDAT","Corrupt PNG"); - if (pal_img_n) { - if (scan == STBI__SCAN_header) { s->img_n = 4; return 1; } - if (pal_len == 0) return stbi__err("tRNS before PLTE","Corrupt PNG"); - if (c.length > pal_len) return stbi__err("bad tRNS len","Corrupt PNG"); - pal_img_n = 4; - for (i=0; i < c.length; ++i) - palette[i*4+3] = stbi__get8(s); - } else { - if (!(s->img_n & 1)) return stbi__err("tRNS with alpha","Corrupt PNG"); - if (c.length != (stbi__uint32) s->img_n*2) return stbi__err("bad tRNS len","Corrupt PNG"); - has_trans = 1; - if (scan == STBI__SCAN_header) { ++s->img_n; return 1; } - if (z->depth == 16) { - for (k = 0; k < s->img_n && k < 3; ++k) // extra loop test to suppress false GCC warning - tc16[k] = (stbi__uint16)stbi__get16be(s); // copy the values as-is - } else { - for (k = 0; k < s->img_n && k < 3; ++k) - tc[k] = (stbi_uc)(stbi__get16be(s) & 255) * stbi__depth_scale_table[z->depth]; // non 8-bit images will be larger - } - } - break; - } - - case STBI__PNG_TYPE('I','D','A','T'): { - if (first) return stbi__err("first not IHDR", "Corrupt PNG"); - if (pal_img_n && !pal_len) return stbi__err("no PLTE","Corrupt PNG"); - if (scan == STBI__SCAN_header) { - if (pal_img_n) - s->img_n = pal_img_n; - return 1; - } - if (c.length > (1u << 30)) return stbi__err("IDAT size limit", "IDAT section larger than 2^30 bytes"); - if ((int)(ioff + c.length) < (int)ioff) return 0; - if (ioff + c.length > idata_limit) { - stbi__uint32 idata_limit_old = idata_limit; - stbi_uc *p; - if (idata_limit == 0) idata_limit = c.length > 4096 ? c.length : 4096; - while (ioff + c.length > idata_limit) - idata_limit *= 2; - STBI_NOTUSED(idata_limit_old); - p = (stbi_uc *) STBI_REALLOC_SIZED(z->idata, idata_limit_old, idata_limit); if (p == NULL) return stbi__err("outofmem", "Out of memory"); - z->idata = p; - } - if (!stbi__getn(s, z->idata+ioff,c.length)) return stbi__err("outofdata","Corrupt PNG"); - ioff += c.length; - break; - } - - case STBI__PNG_TYPE('I','E','N','D'): { - stbi__uint32 raw_len, bpl; - if (first) return stbi__err("first not IHDR", "Corrupt PNG"); - if (scan != STBI__SCAN_load) return 1; - if (z->idata == NULL) return stbi__err("no IDAT","Corrupt PNG"); - bpl = (s->img_x * z->depth + 7) / 8; // bytes per line, per component - raw_len = bpl * s->img_y * s->img_n /* pixels */ + s->img_y /* filter mode per row */; - z->expanded = (stbi_uc *) stbi_zlib_decode_malloc_guesssize_headerflag((char *) z->idata, ioff, raw_len, (int *) &raw_len, !is_iphone); - if (z->expanded == NULL) return 0; // zlib should set error - STBI_FREE(z->idata); z->idata = NULL; - if ((req_comp == s->img_n+1 && req_comp != 3 && !pal_img_n) || has_trans) - s->img_out_n = s->img_n+1; - else - s->img_out_n = s->img_n; - if (!stbi__create_png_image(z, z->expanded, raw_len, s->img_out_n, z->depth, color, interlace)) return 0; - if (has_trans) { - if (z->depth == 16) { - if (!stbi__compute_transparency16(z, tc16, s->img_out_n)) return 0; - } else { - if (!stbi__compute_transparency(z, tc, s->img_out_n)) return 0; - } - } - if (is_iphone && stbi__de_iphone_flag && s->img_out_n > 2) - stbi__de_iphone(z); - if (pal_img_n) { - s->img_n = pal_img_n; // record the actual colors we had - s->img_out_n = pal_img_n; - if (req_comp >= 3) s->img_out_n = req_comp; - if (!stbi__expand_png_palette(z, palette, pal_len, s->img_out_n)) - return 0; - } else if (has_trans) { - ++s->img_n; - } - STBI_FREE(z->expanded); z->expanded = NULL; - stbi__get32be(s); - return 1; - } - - default: - if (first) return stbi__err("first not IHDR", "Corrupt PNG"); - if ((c.type & (1 << 29)) == 0) { - #ifndef STBI_NO_FAILURE_STRINGS - static char invalid_chunk[] = "XXXX PNG chunk not known"; - invalid_chunk[0] = STBI__BYTECAST(c.type >> 24); - invalid_chunk[1] = STBI__BYTECAST(c.type >> 16); - invalid_chunk[2] = STBI__BYTECAST(c.type >> 8); - invalid_chunk[3] = STBI__BYTECAST(c.type >> 0); - #endif - return stbi__err(invalid_chunk, "PNG not supported: unknown PNG chunk type"); - } - stbi__skip(s, c.length); - break; - } - stbi__get32be(s); - } -} - -static void *stbi__do_png(stbi__png *p, int *x, int *y, int *n, int req_comp, stbi__result_info *ri) -{ - void *result=NULL; - if (req_comp < 0 || req_comp > 4) return stbi__errpuc("bad req_comp", "Internal error"); - if (stbi__parse_png_file(p, STBI__SCAN_load, req_comp)) { - if (p->depth <= 8) - ri->bits_per_channel = 8; - else if (p->depth == 16) - ri->bits_per_channel = 16; - else - return stbi__errpuc("bad bits_per_channel", "PNG not supported: unsupported color depth"); - result = p->out; - p->out = NULL; - if (req_comp && req_comp != p->s->img_out_n) { - if (ri->bits_per_channel == 8) - result = stbi__convert_format((unsigned char *) result, p->s->img_out_n, req_comp, p->s->img_x, p->s->img_y); - else - result = stbi__convert_format16((stbi__uint16 *) result, p->s->img_out_n, req_comp, p->s->img_x, p->s->img_y); - p->s->img_out_n = req_comp; - if (result == NULL) return result; - } - *x = p->s->img_x; - *y = p->s->img_y; - if (n) *n = p->s->img_n; - } - STBI_FREE(p->out); p->out = NULL; - STBI_FREE(p->expanded); p->expanded = NULL; - STBI_FREE(p->idata); p->idata = NULL; - - return result; -} - -static void *stbi__png_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) -{ - stbi__png p; - p.s = s; - return stbi__do_png(&p, x,y,comp,req_comp, ri); -} - -static int stbi__png_test(stbi__context *s) -{ - int r; - r = stbi__check_png_header(s); - stbi__rewind(s); - return r; -} - -static int stbi__png_info_raw(stbi__png *p, int *x, int *y, int *comp) -{ - if (!stbi__parse_png_file(p, STBI__SCAN_header, 0)) { - stbi__rewind( p->s ); - return 0; - } - if (x) *x = p->s->img_x; - if (y) *y = p->s->img_y; - if (comp) *comp = p->s->img_n; - return 1; -} - -static int stbi__png_info(stbi__context *s, int *x, int *y, int *comp) -{ - stbi__png p; - p.s = s; - return stbi__png_info_raw(&p, x, y, comp); -} - -static int stbi__png_is16(stbi__context *s) -{ - stbi__png p; - p.s = s; - if (!stbi__png_info_raw(&p, NULL, NULL, NULL)) - return 0; - if (p.depth != 16) { - stbi__rewind(p.s); - return 0; - } - return 1; -} -#endif - -#if 0 -#endif - -/* PIC format implementation removed */ - -#ifndef STBI_NO_GIF -typedef struct -{ - stbi__int16 prefix; - stbi_uc first; - stbi_uc suffix; -} stbi__gif_lzw; - -typedef struct -{ - int w,h; - stbi_uc *out; // output buffer (always 4 components) - stbi_uc *background; // The current "background" as far as a gif is concerned - stbi_uc *history; - int flags, bgindex, ratio, transparent, eflags; - stbi_uc pal[256][4]; - stbi_uc lpal[256][4]; - stbi__gif_lzw codes[8192]; - stbi_uc *color_table; - int parse, step; - int lflags; - int start_x, start_y; - int max_x, max_y; - int cur_x, cur_y; - int line_size; - int delay; -} stbi__gif; - -static int stbi__gif_test_raw(stbi__context *s) -{ - int sz; - if (stbi__get8(s) != 'G' || stbi__get8(s) != 'I' || stbi__get8(s) != 'F' || stbi__get8(s) != '8') return 0; - sz = stbi__get8(s); - if (sz != '9' && sz != '7') return 0; - if (stbi__get8(s) != 'a') return 0; - return 1; -} - -static int stbi__gif_test(stbi__context *s) -{ - int r = stbi__gif_test_raw(s); - stbi__rewind(s); - return r; -} - -static void stbi__gif_parse_colortable(stbi__context *s, stbi_uc pal[256][4], int num_entries, int transp) -{ - int i; - for (i=0; i < num_entries; ++i) { - pal[i][2] = stbi__get8(s); - pal[i][1] = stbi__get8(s); - pal[i][0] = stbi__get8(s); - pal[i][3] = transp == i ? 0 : 255; - } -} - -static int stbi__gif_header(stbi__context *s, stbi__gif *g, int *comp, int is_info) -{ - stbi_uc version; - if (stbi__get8(s) != 'G' || stbi__get8(s) != 'I' || stbi__get8(s) != 'F' || stbi__get8(s) != '8') - return stbi__err("not GIF", "Corrupt GIF"); - - version = stbi__get8(s); - if (version != '7' && version != '9') return stbi__err("not GIF", "Corrupt GIF"); - if (stbi__get8(s) != 'a') return stbi__err("not GIF", "Corrupt GIF"); - - stbi__g_failure_reason = ""; - g->w = stbi__get16le(s); - g->h = stbi__get16le(s); - g->flags = stbi__get8(s); - g->bgindex = stbi__get8(s); - g->ratio = stbi__get8(s); - g->transparent = -1; - - if (g->w > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); - if (g->h > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); - - if (comp != 0) *comp = 4; // can't actually tell whether it's 3 or 4 until we parse the comments - - if (is_info) return 1; - - if (g->flags & 0x80) - stbi__gif_parse_colortable(s,g->pal, 2 << (g->flags & 7), -1); - - return 1; -} - -static int stbi__gif_info_raw(stbi__context *s, int *x, int *y, int *comp) -{ - stbi__gif* g = (stbi__gif*) stbi__malloc(sizeof(stbi__gif)); - if (!g) return stbi__err("outofmem", "Out of memory"); - if (!stbi__gif_header(s, g, comp, 1)) { - STBI_FREE(g); - stbi__rewind( s ); - return 0; - } - if (x) *x = g->w; - if (y) *y = g->h; - STBI_FREE(g); - return 1; -} - -static void stbi__out_gif_code(stbi__gif *g, stbi__uint16 code) -{ - stbi_uc *p, *c; - int idx; - - if (g->codes[code].prefix >= 0) - stbi__out_gif_code(g, g->codes[code].prefix); - - if (g->cur_y >= g->max_y) return; - - idx = g->cur_x + g->cur_y; - p = &g->out[idx]; - g->history[idx / 4] = 1; - - c = &g->color_table[g->codes[code].suffix * 4]; - if (c[3] > 128) { // don't render transparent pixels; - p[0] = c[2]; - p[1] = c[1]; - p[2] = c[0]; - p[3] = c[3]; - } - g->cur_x += 4; - - if (g->cur_x >= g->max_x) { - g->cur_x = g->start_x; - g->cur_y += g->step; - - while (g->cur_y >= g->max_y && g->parse > 0) { - g->step = (1 << g->parse) * g->line_size; - g->cur_y = g->start_y + (g->step >> 1); - --g->parse; - } - } -} - -static stbi_uc *stbi__process_gif_raster(stbi__context *s, stbi__gif *g) -{ - stbi_uc lzw_cs; - stbi__int32 len, init_code; - stbi__uint32 first; - stbi__int32 codesize, codemask, avail, oldcode, bits, valid_bits, clear; - stbi__gif_lzw *p; - - lzw_cs = stbi__get8(s); - if (lzw_cs > 12) return NULL; - clear = 1 << lzw_cs; - first = 1; - codesize = lzw_cs + 1; - codemask = (1 << codesize) - 1; - bits = 0; - valid_bits = 0; - for (init_code = 0; init_code < clear; init_code++) { - g->codes[init_code].prefix = -1; - g->codes[init_code].first = (stbi_uc) init_code; - g->codes[init_code].suffix = (stbi_uc) init_code; - } - - avail = clear+2; - oldcode = -1; - - len = 0; - for(;;) { - if (valid_bits < codesize) { - if (len == 0) { - len = stbi__get8(s); // start new block - if (len == 0) - return g->out; - } - --len; - bits |= (stbi__int32) stbi__get8(s) << valid_bits; - valid_bits += 8; - } else { - stbi__int32 code = bits & codemask; - bits >>= codesize; - valid_bits -= codesize; - if (code == clear) { // clear code - codesize = lzw_cs + 1; - codemask = (1 << codesize) - 1; - avail = clear + 2; - oldcode = -1; - first = 0; - } else if (code == clear + 1) { // end of stream code - stbi__skip(s, len); - while ((len = stbi__get8(s)) > 0) - stbi__skip(s,len); - return g->out; - } else if (code <= avail) { - if (first) { - return stbi__errpuc("no clear code", "Corrupt GIF"); - } - - if (oldcode >= 0) { - p = &g->codes[avail++]; - if (avail > 8192) { - return stbi__errpuc("too many codes", "Corrupt GIF"); - } - - p->prefix = (stbi__int16) oldcode; - p->first = g->codes[oldcode].first; - p->suffix = (code == avail) ? p->first : g->codes[code].first; - } else if (code == avail) - return stbi__errpuc("illegal code in raster", "Corrupt GIF"); - - stbi__out_gif_code(g, (stbi__uint16) code); - - if ((avail & codemask) == 0 && avail <= 0x0FFF) { - codesize++; - codemask = (1 << codesize) - 1; - } - - oldcode = code; - } else { - return stbi__errpuc("illegal code in raster", "Corrupt GIF"); - } - } - } -} - -static stbi_uc *stbi__gif_load_next(stbi__context *s, stbi__gif *g, int *comp, int req_comp, stbi_uc *two_back) -{ - int dispose; - int first_frame; - int pi; - int pcount; - STBI_NOTUSED(req_comp); - - first_frame = 0; - if (g->out == 0) { - if (!stbi__gif_header(s, g, comp,0)) return 0; // stbi__g_failure_reason set by stbi__gif_header - if (!stbi__mad3sizes_valid(4, g->w, g->h, 0)) - return stbi__errpuc("too large", "GIF image is too large"); - pcount = g->w * g->h; - g->out = (stbi_uc *) stbi__malloc(4 * pcount); - g->background = (stbi_uc *) stbi__malloc(4 * pcount); - g->history = (stbi_uc *) stbi__malloc(pcount); - if (!g->out || !g->background || !g->history) - return stbi__errpuc("outofmem", "Out of memory"); - - memset(g->out, 0x00, 4 * pcount); - memset(g->background, 0x00, 4 * pcount); // state of the background (starts transparent) - memset(g->history, 0x00, pcount); // pixels that were affected previous frame - first_frame = 1; - } else { - dispose = (g->eflags & 0x1C) >> 2; - pcount = g->w * g->h; - - if ((dispose == 3) && (two_back == 0)) { - dispose = 2; // if I don't have an image to revert back to, default to the old background - } - - if (dispose == 3) { // use previous graphic - for (pi = 0; pi < pcount; ++pi) { - if (g->history[pi]) { - memcpy( &g->out[pi * 4], &two_back[pi * 4], 4 ); - } - } - } else if (dispose == 2) { - for (pi = 0; pi < pcount; ++pi) { - if (g->history[pi]) { - memcpy( &g->out[pi * 4], &g->background[pi * 4], 4 ); - } - } - } else { - } - - memcpy( g->background, g->out, 4 * g->w * g->h ); - } - - memset( g->history, 0x00, g->w * g->h ); // pixels that were affected previous frame - - for (;;) { - int tag = stbi__get8(s); - switch (tag) { - case 0x2C: /* Image Descriptor */ - { - stbi__int32 x, y, w, h; - stbi_uc *o; - - x = stbi__get16le(s); - y = stbi__get16le(s); - w = stbi__get16le(s); - h = stbi__get16le(s); - if (((x + w) > (g->w)) || ((y + h) > (g->h))) - return stbi__errpuc("bad Image Descriptor", "Corrupt GIF"); - - g->line_size = g->w * 4; - g->start_x = x * 4; - g->start_y = y * g->line_size; - g->max_x = g->start_x + w * 4; - g->max_y = g->start_y + h * g->line_size; - g->cur_x = g->start_x; - g->cur_y = g->start_y; - - if (w == 0) - g->cur_y = g->max_y; - - g->lflags = stbi__get8(s); - - if (g->lflags & 0x40) { - g->step = 8 * g->line_size; // first interlaced spacing - g->parse = 3; - } else { - g->step = g->line_size; - g->parse = 0; - } - - if (g->lflags & 0x80) { - stbi__gif_parse_colortable(s,g->lpal, 2 << (g->lflags & 7), g->eflags & 0x01 ? g->transparent : -1); - g->color_table = (stbi_uc *) g->lpal; - } else if (g->flags & 0x80) { - g->color_table = (stbi_uc *) g->pal; - } else - return stbi__errpuc("missing color table", "Corrupt GIF"); - - o = stbi__process_gif_raster(s, g); - if (!o) return NULL; - - pcount = g->w * g->h; - if (first_frame && (g->bgindex > 0)) { - for (pi = 0; pi < pcount; ++pi) { - if (g->history[pi] == 0) { - g->pal[g->bgindex][3] = 255; // just in case it was made transparent, undo that; It will be reset next frame if need be; - memcpy( &g->out[pi * 4], &g->pal[g->bgindex], 4 ); - } - } - } - - return o; - } - - case 0x21: // Comment Extension. - { - int len; - int ext = stbi__get8(s); - if (ext == 0xF9) { // Graphic Control Extension. - len = stbi__get8(s); - if (len == 4) { - g->eflags = stbi__get8(s); - g->delay = 10 * stbi__get16le(s); // delay - 1/100th of a second, saving as 1/1000ths. - - if (g->transparent >= 0) { - g->pal[g->transparent][3] = 255; - } - if (g->eflags & 0x01) { - g->transparent = stbi__get8(s); - if (g->transparent >= 0) { - g->pal[g->transparent][3] = 0; - } - } else { - stbi__skip(s, 1); - g->transparent = -1; - } - } else { - stbi__skip(s, len); - break; - } - } - while ((len = stbi__get8(s)) != 0) { - stbi__skip(s, len); - } - break; - } - - case 0x3B: // gif stream termination code - return (stbi_uc *) s; // using '1' causes warning on some compilers - - default: - return stbi__errpuc("unknown code", "Corrupt GIF"); - } - } -} - -static void *stbi__load_gif_main_outofmem(stbi__gif *g, stbi_uc *out, int **delays) -{ - STBI_FREE(g->out); - STBI_FREE(g->history); - STBI_FREE(g->background); - - if (out) STBI_FREE(out); - if (delays && *delays) STBI_FREE(*delays); - return stbi__errpuc("outofmem", "Out of memory"); -} - -static void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y, int *z, int *comp, int req_comp) -{ - if (stbi__gif_test(s)) { - int layers = 0; - stbi_uc *u = 0; - stbi_uc *out = 0; - stbi_uc *two_back = 0; - stbi__gif g; - int stride; - int out_size = 0; - int delays_size = 0; - - STBI_NOTUSED(out_size); - STBI_NOTUSED(delays_size); - - memset(&g, 0, sizeof(g)); - if (delays) { - *delays = 0; - } - - do { - u = stbi__gif_load_next(s, &g, comp, req_comp, two_back); - if (u == (stbi_uc *) s) u = 0; // end of animated gif marker - - if (u) { - *x = g.w; - *y = g.h; - ++layers; - stride = g.w * g.h * 4; - - if (out) { - void *tmp = (stbi_uc*) STBI_REALLOC_SIZED( out, out_size, layers * stride ); - if (!tmp) - return stbi__load_gif_main_outofmem(&g, out, delays); - else { - out = (stbi_uc*) tmp; - out_size = layers * stride; - } - - if (delays) { - int *new_delays = (int*) STBI_REALLOC_SIZED( *delays, delays_size, sizeof(int) * layers ); - if (!new_delays) - return stbi__load_gif_main_outofmem(&g, out, delays); - *delays = new_delays; - delays_size = layers * sizeof(int); - } - } else { - out = (stbi_uc*)stbi__malloc( layers * stride ); - if (!out) - return stbi__load_gif_main_outofmem(&g, out, delays); - out_size = layers * stride; - if (delays) { - *delays = (int*) stbi__malloc( layers * sizeof(int) ); - if (!*delays) - return stbi__load_gif_main_outofmem(&g, out, delays); - delays_size = layers * sizeof(int); - } - } - memcpy( out + ((layers - 1) * stride), u, stride ); - if (layers >= 2) { - two_back = out - 2 * stride; - } - - if (delays) { - (*delays)[layers - 1U] = g.delay; - } - } - } while (u != 0); - - STBI_FREE(g.out); - STBI_FREE(g.history); - STBI_FREE(g.background); - - if (req_comp && req_comp != 4) - out = stbi__convert_format(out, 4, req_comp, layers * g.w, g.h); - - *z = layers; - return out; - } else { - return stbi__errpuc("not GIF", "Image was not as a gif type."); - } -} - -static void *stbi__gif_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) -{ - stbi_uc *u = 0; - stbi__gif g; - memset(&g, 0, sizeof(g)); - STBI_NOTUSED(ri); - - u = stbi__gif_load_next(s, &g, comp, req_comp, 0); - if (u == (stbi_uc *) s) u = 0; // end of animated gif marker - if (u) { - *x = g.w; - *y = g.h; - - if (req_comp && req_comp != 4) - u = stbi__convert_format(u, 4, req_comp, g.w, g.h); - } else if (g.out) { - STBI_FREE(g.out); - } - - STBI_FREE(g.history); - STBI_FREE(g.background); - - return u; -} - -static int stbi__gif_info(stbi__context *s, int *x, int *y, int *comp) -{ - return stbi__gif_info_raw(s,x,y,comp); -} -#endif - -/* HDR implementation removed */ - -#ifndef STBI_NO_BMP -static int stbi__bmp_info(stbi__context *s, int *x, int *y, int *comp) -{ - void *p; - stbi__bmp_data info; - - info.all_a = 255; - p = stbi__bmp_parse_header(s, &info); - if (p == NULL) { - stbi__rewind( s ); - return 0; - } - if (x) *x = s->img_x; - if (y) *y = s->img_y; - if (comp) { - if (info.bpp == 24 && info.ma == 0xff000000) - *comp = 3; - else - *comp = info.ma ? 4 : 3; - } - return 1; -} -#endif - -#if 0 -#endif - -/* PIC info removed */ - -static int stbi__info_main(stbi__context *s, int *x, int *y, int *comp) -{ - #ifndef STBI_NO_JPEG - if (stbi__jpeg_info(s, x, y, comp)) return 1; - #endif - - #ifndef STBI_NO_PNG - if (stbi__png_info(s, x, y, comp)) return 1; - #endif - - #ifndef STBI_NO_GIF - if (stbi__gif_info(s, x, y, comp)) return 1; - #endif - - (void)0; - - return stbi__err("unknown image type", "Image not of any known type, or corrupt"); -} - -static int stbi__is_16_main(stbi__context *s) -{ - #ifndef STBI_NO_PNG - if (stbi__png_is16(s)) return 1; - #endif - - return 0; -} - -#ifndef STBI_NO_STDIO -STBIDEF int stbi_info(char const *filename, int *x, int *y, int *comp) -{ - FILE *f = stbi__fopen(filename, "rb"); - int result; - if (!f) return stbi__err("can't fopen", "Unable to open file"); - result = stbi_info_from_file(f, x, y, comp); - fclose(f); - return result; -} - -STBIDEF int stbi_info_from_file(FILE *f, int *x, int *y, int *comp) -{ - int r; - stbi__context s; - long pos = ftell(f); - stbi__start_file(&s, f); - r = stbi__info_main(&s,x,y,comp); - fseek(f,pos,SEEK_SET); - return r; -} - -STBIDEF int stbi_is_16_bit(char const *filename) -{ - FILE *f = stbi__fopen(filename, "rb"); - int result; - if (!f) return stbi__err("can't fopen", "Unable to open file"); - result = stbi_is_16_bit_from_file(f); - fclose(f); - return result; -} - -STBIDEF int stbi_is_16_bit_from_file(FILE *f) -{ - int r; - stbi__context s; - long pos = ftell(f); - stbi__start_file(&s, f); - r = stbi__is_16_main(&s); - fseek(f,pos,SEEK_SET); - return r; -} -#endif // !STBI_NO_STDIO - -STBIDEF int stbi_info_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp) -{ - stbi__context s; - stbi__start_mem(&s,buffer,len); - return stbi__info_main(&s,x,y,comp); -} - -STBIDEF int stbi_info_from_callbacks(stbi_io_callbacks const *c, void *user, int *x, int *y, int *comp) -{ - stbi__context s; - stbi__start_callbacks(&s, (stbi_io_callbacks *) c, user); - return stbi__info_main(&s,x,y,comp); -} - -STBIDEF int stbi_is_16_bit_from_memory(stbi_uc const *buffer, int len) -{ - stbi__context s; - stbi__start_mem(&s,buffer,len); - return stbi__is_16_main(&s); -} - -STBIDEF int stbi_is_16_bit_from_callbacks(stbi_io_callbacks const *c, void *user) -{ - stbi__context s; - stbi__start_callbacks(&s, (stbi_io_callbacks *) c, user); - return stbi__is_16_main(&s); -} - -#endif // STB_IMAGE_IMPLEMENTATION diff --git a/libs/stb/stb_image_resize2.h b/libs/stb/stb_image_resize2.h deleted file mode 100644 index 8ac4c6e2f..000000000 --- a/libs/stb/stb_image_resize2.h +++ /dev/null @@ -1,9446 +0,0 @@ -#if !defined(STB_IMAGE_RESIZE_DO_HORIZONTALS) && !defined(STB_IMAGE_RESIZE_DO_VERTICALS) && !defined(STB_IMAGE_RESIZE_DO_CODERS) // for internal re-includes - -#ifndef STBIR_INCLUDE_STB_IMAGE_RESIZE2_H -#define STBIR_INCLUDE_STB_IMAGE_RESIZE2_H - -#include -#ifdef _MSC_VER -typedef unsigned char stbir_uint8; -typedef unsigned short stbir_uint16; -typedef unsigned int stbir_uint32; -typedef unsigned __int64 stbir_uint64; -#else -#include -typedef uint8_t stbir_uint8; -typedef uint16_t stbir_uint16; -typedef uint32_t stbir_uint32; -typedef uint64_t stbir_uint64; -#endif - -#ifndef STBIRDEF -#ifdef STB_IMAGE_RESIZE_STATIC -#define STBIRDEF static -#else -#ifdef __cplusplus -#define STBIRDEF extern "C" -#else -#define STBIRDEF extern -#endif -#endif -#endif - -typedef enum -{ - STBIR_1CHANNEL = 1, - STBIR_2CHANNEL = 2, - STBIR_RGB = 3, // 3-chan, with order specified (for channel flipping) - STBIR_BGR = 0, // 3-chan, with order specified (for channel flipping) - STBIR_4CHANNEL = 5, - - STBIR_RGBA = 4, // alpha formats, where alpha is NOT premultiplied into color channels - STBIR_BGRA = 6, - STBIR_ARGB = 7, - STBIR_ABGR = 8, - STBIR_RA = 9, - STBIR_AR = 10, - - STBIR_RGBA_PM = 11, // alpha formats, where alpha is premultiplied into color channels - STBIR_BGRA_PM = 12, - STBIR_ARGB_PM = 13, - STBIR_ABGR_PM = 14, - STBIR_RA_PM = 15, - STBIR_AR_PM = 16, - - STBIR_RGBA_NO_AW = 11, // alpha formats, where NO alpha weighting is applied at all! - STBIR_BGRA_NO_AW = 12, // these are just synonyms for the _PM flags (which also do - STBIR_ARGB_NO_AW = 13, // no alpha weighting). These names just make it more clear - STBIR_ABGR_NO_AW = 14, // for some folks). - STBIR_RA_NO_AW = 15, - STBIR_AR_NO_AW = 16, - -} stbir_pixel_layout; - -STBIRDEF unsigned char * stbir_resize_uint8_srgb( const unsigned char *input_pixels , int input_w , int input_h, int input_stride_in_bytes, - unsigned char *output_pixels, int output_w, int output_h, int output_stride_in_bytes, - stbir_pixel_layout pixel_type ); - -STBIRDEF unsigned char * stbir_resize_uint8_linear( const unsigned char *input_pixels , int input_w , int input_h, int input_stride_in_bytes, - unsigned char *output_pixels, int output_w, int output_h, int output_stride_in_bytes, - stbir_pixel_layout pixel_type ); - -STBIRDEF float * stbir_resize_float_linear( const float *input_pixels , int input_w , int input_h, int input_stride_in_bytes, - float *output_pixels, int output_w, int output_h, int output_stride_in_bytes, - stbir_pixel_layout pixel_type ); - -typedef enum -{ - STBIR_EDGE_CLAMP = 0, - STBIR_EDGE_REFLECT = 1, - STBIR_EDGE_WRAP = 2, // this edge mode is slower and uses more memory - STBIR_EDGE_ZERO = 3, -} stbir_edge; - -typedef enum -{ - STBIR_FILTER_DEFAULT = 0, // use same filter type that easy-to-use API chooses - STBIR_FILTER_BOX = 1, // A trapezoid w/1-pixel wide ramps, same result as box for integer scale ratios - STBIR_FILTER_TRIANGLE = 2, // On upsampling, produces same results as bilinear texture filtering - STBIR_FILTER_CUBICBSPLINE = 3, // The cubic b-spline (aka Mitchell-Netrevalli with B=1,C=0), gaussian-esque - STBIR_FILTER_CATMULLROM = 4, // An interpolating cubic spline - STBIR_FILTER_MITCHELL = 5, // Mitchell-Netrevalli filter with B=1/3, C=1/3 - STBIR_FILTER_POINT_SAMPLE = 6, // Simple point sampling - STBIR_FILTER_OTHER = 7, // User callback specified -} stbir_filter; - -typedef enum -{ - STBIR_TYPE_UINT8 = 0, - STBIR_TYPE_UINT8_SRGB = 1, - STBIR_TYPE_UINT8_SRGB_ALPHA = 2, // alpha channel, when present, should also be SRGB (this is very unusual) - STBIR_TYPE_UINT16 = 3, - STBIR_TYPE_FLOAT = 4, - STBIR_TYPE_HALF_FLOAT = 5 -} stbir_datatype; - -STBIRDEF void * stbir_resize( const void *input_pixels , int input_w , int input_h, int input_stride_in_bytes, - void *output_pixels, int output_w, int output_h, int output_stride_in_bytes, - stbir_pixel_layout pixel_layout, stbir_datatype data_type, - stbir_edge edge, stbir_filter filter ); - -typedef void const * stbir_input_callback( void * optional_output, void const * input_ptr, int num_pixels, int x, int y, void * context ); - -typedef void stbir_output_callback( void const * output_ptr, int num_pixels, int y, void * context ); - -typedef float stbir__kernel_callback( float x, float scale, void * user_data ); // centered at zero -typedef float stbir__support_callback( float scale, void * user_data ); - -typedef struct stbir__info stbir__info; - -typedef struct STBIR_RESIZE // use the stbir_resize_init and stbir_override functions to set these values for future compatibility -{ - void * user_data; - void const * input_pixels; - int input_w, input_h; - double input_s0, input_t0, input_s1, input_t1; - stbir_input_callback * input_cb; - void * output_pixels; - int output_w, output_h; - int output_subx, output_suby, output_subw, output_subh; - stbir_output_callback * output_cb; - int input_stride_in_bytes; - int output_stride_in_bytes; - int splits; - int fast_alpha; - int needs_rebuild; - int called_alloc; - stbir_pixel_layout input_pixel_layout_public; - stbir_pixel_layout output_pixel_layout_public; - stbir_datatype input_data_type; - stbir_datatype output_data_type; - stbir_filter horizontal_filter, vertical_filter; - stbir_edge horizontal_edge, vertical_edge; - stbir__kernel_callback * horizontal_filter_kernel; stbir__support_callback * horizontal_filter_support; - stbir__kernel_callback * vertical_filter_kernel; stbir__support_callback * vertical_filter_support; - stbir__info * samplers; -} STBIR_RESIZE; - -STBIRDEF void stbir_resize_init( STBIR_RESIZE * resize, - const void *input_pixels, int input_w, int input_h, int input_stride_in_bytes, // stride can be zero - void *output_pixels, int output_w, int output_h, int output_stride_in_bytes, // stride can be zero - stbir_pixel_layout pixel_layout, stbir_datatype data_type ); - -STBIRDEF void stbir_set_datatypes( STBIR_RESIZE * resize, stbir_datatype input_type, stbir_datatype output_type ); -STBIRDEF void stbir_set_pixel_callbacks( STBIR_RESIZE * resize, stbir_input_callback * input_cb, stbir_output_callback * output_cb ); // no callbacks by default -STBIRDEF void stbir_set_user_data( STBIR_RESIZE * resize, void * user_data ); // pass back STBIR_RESIZE* by default -STBIRDEF void stbir_set_buffer_ptrs( STBIR_RESIZE * resize, const void * input_pixels, int input_stride_in_bytes, void * output_pixels, int output_stride_in_bytes ); - -STBIRDEF int stbir_set_pixel_layouts( STBIR_RESIZE * resize, stbir_pixel_layout input_pixel_layout, stbir_pixel_layout output_pixel_layout ); // sets new buffer layouts -STBIRDEF int stbir_set_edgemodes( STBIR_RESIZE * resize, stbir_edge horizontal_edge, stbir_edge vertical_edge ); // CLAMP by default - -STBIRDEF int stbir_set_filters( STBIR_RESIZE * resize, stbir_filter horizontal_filter, stbir_filter vertical_filter ); // STBIR_DEFAULT_FILTER_UPSAMPLE/DOWNSAMPLE by default -STBIRDEF int stbir_set_filter_callbacks( STBIR_RESIZE * resize, stbir__kernel_callback * horizontal_filter, stbir__support_callback * horizontal_support, stbir__kernel_callback * vertical_filter, stbir__support_callback * vertical_support ); - -STBIRDEF int stbir_set_pixel_subrect( STBIR_RESIZE * resize, int subx, int suby, int subw, int subh ); // sets both sub-regions (full regions by default) -STBIRDEF int stbir_set_input_subrect( STBIR_RESIZE * resize, double s0, double t0, double s1, double t1 ); // sets input sub-region (full region by default) -STBIRDEF int stbir_set_output_pixel_subrect( STBIR_RESIZE * resize, int subx, int suby, int subw, int subh ); // sets output sub-region (full region by default) - -STBIRDEF int stbir_set_non_pm_alpha_speed_over_quality( STBIR_RESIZE * resize, int non_pma_alpha_speed_over_quality ); - -STBIRDEF int stbir_build_samplers( STBIR_RESIZE * resize ); - -STBIRDEF void stbir_free_samplers( STBIR_RESIZE * resize ); - -STBIRDEF int stbir_resize_extended( STBIR_RESIZE * resize ); - -STBIRDEF int stbir_build_samplers_with_splits( STBIR_RESIZE * resize, int try_splits ); - -STBIRDEF int stbir_resize_extended_split( STBIR_RESIZE * resize, int split_start, int split_count ); - -#endif // STBIR_INCLUDE_STB_IMAGE_RESIZE2_H - -#if defined(STB_IMAGE_RESIZE_IMPLEMENTATION) || defined(STB_IMAGE_RESIZE2_IMPLEMENTATION) - -#ifndef STBIR_ASSERT -#include -#define STBIR_ASSERT(x) assert(x) -#endif - -#ifndef STBIR_MALLOC -#include -#define STBIR_MALLOC(size,user_data) ((void)(user_data), malloc(size)) -#define STBIR_FREE(ptr,user_data) ((void)(user_data), free(ptr)) -#endif - -#ifdef _MSC_VER - -#define stbir__inline __forceinline - -#else - -#define stbir__inline __inline__ - -#if defined(__has_feature) - #if __has_feature(address_sanitizer) || __has_feature(memory_sanitizer) - #ifndef STBIR__SEPARATE_ALLOCATIONS - #define STBIR__SEPARATE_ALLOCATIONS - #endif - #endif -#endif - -#endif - -#if defined(__SANITIZE_ADDRESS__) - #ifndef STBIR__SEPARATE_ALLOCATIONS - #define STBIR__SEPARATE_ALLOCATIONS - #endif -#endif - -#ifndef STBIR_DONT_CHANGE_FP_CONTRACT // override in case you don't want this behavior -#if defined(_MSC_VER) && !defined(__clang__) -#if _MSC_VER > 1200 -#pragma fp_contract(off) -#endif -#elif defined(__GNUC__) && !defined(__clang__) -#pragma GCC optimize("fp-contract=off") -#else -#pragma STDC FP_CONTRACT OFF -#endif -#endif - -#ifdef _MSC_VER -#define STBIR__UNUSED(v) (void)(v) -#else -#define STBIR__UNUSED(v) (void)sizeof(v) -#endif - -#define STBIR__ARRAY_SIZE(a) (sizeof((a))/sizeof((a)[0])) - -#ifndef STBIR_DEFAULT_FILTER_UPSAMPLE -#define STBIR_DEFAULT_FILTER_UPSAMPLE STBIR_FILTER_CATMULLROM -#endif - -#ifndef STBIR_DEFAULT_FILTER_DOWNSAMPLE -#define STBIR_DEFAULT_FILTER_DOWNSAMPLE STBIR_FILTER_MITCHELL -#endif - -#ifndef STBIR__HEADER_FILENAME -#define STBIR__HEADER_FILENAME "stb_image_resize2.h" -#endif - -typedef enum -{ - STBIRI_1CHANNEL = 0, - STBIRI_2CHANNEL = 1, - STBIRI_RGB = 2, - STBIRI_BGR = 3, - STBIRI_4CHANNEL = 4, - - STBIRI_RGBA = 5, - STBIRI_BGRA = 6, - STBIRI_ARGB = 7, - STBIRI_ABGR = 8, - STBIRI_RA = 9, - STBIRI_AR = 10, - - STBIRI_RGBA_PM = 11, - STBIRI_BGRA_PM = 12, - STBIRI_ARGB_PM = 13, - STBIRI_ABGR_PM = 14, - STBIRI_RA_PM = 15, - STBIRI_AR_PM = 16, -} stbir_internal_pixel_layout; - -#define STBIR_BGR bad_dont_use_in_implementation -#define STBIR_1CHANNEL STBIR_BGR -#define STBIR_2CHANNEL STBIR_BGR -#define STBIR_RGB STBIR_BGR -#define STBIR_RGBA STBIR_BGR -#define STBIR_4CHANNEL STBIR_BGR -#define STBIR_BGRA STBIR_BGR -#define STBIR_ARGB STBIR_BGR -#define STBIR_ABGR STBIR_BGR -#define STBIR_RA STBIR_BGR -#define STBIR_AR STBIR_BGR -#define STBIR_RGBA_PM STBIR_BGR -#define STBIR_BGRA_PM STBIR_BGR -#define STBIR_ARGB_PM STBIR_BGR -#define STBIR_ABGR_PM STBIR_BGR -#define STBIR_RA_PM STBIR_BGR -#define STBIR_AR_PM STBIR_BGR - -static unsigned char stbir__type_size[] = { - 1,1,1,2,4,2 // STBIR_TYPE_UINT8,STBIR_TYPE_UINT8_SRGB,STBIR_TYPE_UINT8_SRGB_ALPHA,STBIR_TYPE_UINT16,STBIR_TYPE_FLOAT,STBIR_TYPE_HALF_FLOAT -}; - -typedef struct -{ - int n0; // First contributing pixel - int n1; // Last contributing pixel -} stbir__contributors; - -typedef struct -{ - int lowest; // First sample index for whole filter - int highest; // Last sample index for whole filter - int widest; // widest single set of samples for an output -} stbir__filter_extent_info; - -typedef struct -{ - int n0; // First pixel of decode buffer to write to - int n1; // Last pixel of decode that will be written to - int pixel_offset_for_input; // Pixel offset into input_scanline -} stbir__span; - -typedef struct stbir__scale_info -{ - int input_full_size; - int output_sub_size; - float scale; - float inv_scale; - float pixel_shift; // starting shift in output pixel space (in pixels) - int scale_is_rational; - stbir_uint32 scale_numerator, scale_denominator; -} stbir__scale_info; - -typedef struct -{ - stbir__contributors * contributors; - float* coefficients; - stbir__contributors * gather_prescatter_contributors; - float * gather_prescatter_coefficients; - stbir__scale_info scale_info; - float support; - stbir_filter filter_enum; - stbir__kernel_callback * filter_kernel; - stbir__support_callback * filter_support; - stbir_edge edge; - int coefficient_width; - int filter_pixel_width; - int filter_pixel_margin; - int num_contributors; - int contributors_size; - int coefficients_size; - stbir__filter_extent_info extent_info; - int is_gather; // 0 = scatter, 1 = gather with scale >= 1, 2 = gather with scale < 1 - int gather_prescatter_num_contributors; - int gather_prescatter_coefficient_width; - int gather_prescatter_contributors_size; - int gather_prescatter_coefficients_size; -} stbir__sampler; - -typedef struct -{ - stbir__contributors conservative; - int edge_sizes[2]; // this can be less than filter_pixel_margin, if the filter and scaling falls off - stbir__span spans[2]; // can be two spans, if doing input subrect with clamp mode WRAP -} stbir__extents; - -typedef struct -{ - float* decode_buffer; - - int ring_buffer_first_scanline; - int ring_buffer_last_scanline; - int ring_buffer_begin_index; // first_scanline is at this index in the ring buffer - int start_output_y, end_output_y; - int start_input_y, end_input_y; // used in scatter only - - #ifdef STBIR__SEPARATE_ALLOCATIONS - float** ring_buffers; // one pointer for each ring buffer - #else - float* ring_buffer; // one big buffer that we index into - #endif - - float* vertical_buffer; - - char no_cache_straddle[64]; -} stbir__per_split_info; - -typedef float * stbir__decode_pixels_func( float * decode, int width_times_channels, void const * input ); -typedef void stbir__alpha_weight_func( float * decode_buffer, int width_times_channels ); -typedef void stbir__horizontal_gather_channels_func( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, - stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ); -typedef void stbir__alpha_unweight_func(float * encode_buffer, int width_times_channels ); -typedef void stbir__encode_pixels_func( void * output, int width_times_channels, float const * encode ); - -struct stbir__info -{ - stbir__sampler horizontal; - stbir__sampler vertical; - - void const * input_data; - void * output_data; - - int input_stride_bytes; - int output_stride_bytes; - int ring_buffer_length_bytes; // The length of an individual entry in the ring buffer. The total number of ring buffers is stbir__get_filter_pixel_width(filter) - int ring_buffer_num_entries; // Total number of entries in the ring buffer. - - stbir_datatype input_type; - stbir_datatype output_type; - - stbir_input_callback * in_pixels_cb; - void * user_data; - stbir_output_callback * out_pixels_cb; - - stbir__extents scanline_extents; - - void * alloced_mem; - stbir__per_split_info * split_info; // by default 1, but there will be N of these allocated based on the thread init you did - - stbir__decode_pixels_func * decode_pixels; - stbir__alpha_weight_func * alpha_weight; - stbir__horizontal_gather_channels_func * horizontal_gather_channels; - stbir__alpha_unweight_func * alpha_unweight; - stbir__encode_pixels_func * encode_pixels; - - int alloc_ring_buffer_num_entries; // Number of entries in the ring buffer that will be allocated - int splits; // count of splits - - stbir_internal_pixel_layout input_pixel_layout_internal; - stbir_internal_pixel_layout output_pixel_layout_internal; - - int input_color_and_type; - int offset_x, offset_y; // offset within output_data - int vertical_first; - int channels; - int effective_channels; // same as channels, except on RGBA/ARGB (7), or XA/AX (3) - size_t alloced_total; -}; - -#define stbir__max_uint8_as_float 255.0f -#define stbir__max_uint16_as_float 65535.0f -#define stbir__max_uint8_as_float_inverted 3.9215689e-03f // (1.0f/255.0f) -#define stbir__max_uint16_as_float_inverted 1.5259022e-05f // (1.0f/65535.0f) -#define stbir__small_float ((float)1 / (1 << 20) / (1 << 20) / (1 << 20) / (1 << 20) / (1 << 20) / (1 << 20)) - -#define STBIR_CLAMP(x, xmin, xmax) for(;;) { \ - if ( (x) < (xmin) ) (x) = (xmin); \ - if ( (x) > (xmax) ) (x) = (xmax); \ - break; \ -} - -static stbir__inline int stbir__min(int a, int b) -{ - return a < b ? a : b; -} - -static stbir__inline int stbir__max(int a, int b) -{ - return a > b ? a : b; -} - -static float stbir__srgb_uchar_to_linear_float[256] = { - 0.000000f, 0.000304f, 0.000607f, 0.000911f, 0.001214f, 0.001518f, 0.001821f, 0.002125f, 0.002428f, 0.002732f, 0.003035f, - 0.003347f, 0.003677f, 0.004025f, 0.004391f, 0.004777f, 0.005182f, 0.005605f, 0.006049f, 0.006512f, 0.006995f, 0.007499f, - 0.008023f, 0.008568f, 0.009134f, 0.009721f, 0.010330f, 0.010960f, 0.011612f, 0.012286f, 0.012983f, 0.013702f, 0.014444f, - 0.015209f, 0.015996f, 0.016807f, 0.017642f, 0.018500f, 0.019382f, 0.020289f, 0.021219f, 0.022174f, 0.023153f, 0.024158f, - 0.025187f, 0.026241f, 0.027321f, 0.028426f, 0.029557f, 0.030713f, 0.031896f, 0.033105f, 0.034340f, 0.035601f, 0.036889f, - 0.038204f, 0.039546f, 0.040915f, 0.042311f, 0.043735f, 0.045186f, 0.046665f, 0.048172f, 0.049707f, 0.051269f, 0.052861f, - 0.054480f, 0.056128f, 0.057805f, 0.059511f, 0.061246f, 0.063010f, 0.064803f, 0.066626f, 0.068478f, 0.070360f, 0.072272f, - 0.074214f, 0.076185f, 0.078187f, 0.080220f, 0.082283f, 0.084376f, 0.086500f, 0.088656f, 0.090842f, 0.093059f, 0.095307f, - 0.097587f, 0.099899f, 0.102242f, 0.104616f, 0.107023f, 0.109462f, 0.111932f, 0.114435f, 0.116971f, 0.119538f, 0.122139f, - 0.124772f, 0.127438f, 0.130136f, 0.132868f, 0.135633f, 0.138432f, 0.141263f, 0.144128f, 0.147027f, 0.149960f, 0.152926f, - 0.155926f, 0.158961f, 0.162029f, 0.165132f, 0.168269f, 0.171441f, 0.174647f, 0.177888f, 0.181164f, 0.184475f, 0.187821f, - 0.191202f, 0.194618f, 0.198069f, 0.201556f, 0.205079f, 0.208637f, 0.212231f, 0.215861f, 0.219526f, 0.223228f, 0.226966f, - 0.230740f, 0.234551f, 0.238398f, 0.242281f, 0.246201f, 0.250158f, 0.254152f, 0.258183f, 0.262251f, 0.266356f, 0.270498f, - 0.274677f, 0.278894f, 0.283149f, 0.287441f, 0.291771f, 0.296138f, 0.300544f, 0.304987f, 0.309469f, 0.313989f, 0.318547f, - 0.323143f, 0.327778f, 0.332452f, 0.337164f, 0.341914f, 0.346704f, 0.351533f, 0.356400f, 0.361307f, 0.366253f, 0.371238f, - 0.376262f, 0.381326f, 0.386430f, 0.391573f, 0.396755f, 0.401978f, 0.407240f, 0.412543f, 0.417885f, 0.423268f, 0.428691f, - 0.434154f, 0.439657f, 0.445201f, 0.450786f, 0.456411f, 0.462077f, 0.467784f, 0.473532f, 0.479320f, 0.485150f, 0.491021f, - 0.496933f, 0.502887f, 0.508881f, 0.514918f, 0.520996f, 0.527115f, 0.533276f, 0.539480f, 0.545725f, 0.552011f, 0.558340f, - 0.564712f, 0.571125f, 0.577581f, 0.584078f, 0.590619f, 0.597202f, 0.603827f, 0.610496f, 0.617207f, 0.623960f, 0.630757f, - 0.637597f, 0.644480f, 0.651406f, 0.658375f, 0.665387f, 0.672443f, 0.679543f, 0.686685f, 0.693872f, 0.701102f, 0.708376f, - 0.715694f, 0.723055f, 0.730461f, 0.737911f, 0.745404f, 0.752942f, 0.760525f, 0.768151f, 0.775822f, 0.783538f, 0.791298f, - 0.799103f, 0.806952f, 0.814847f, 0.822786f, 0.830770f, 0.838799f, 0.846873f, 0.854993f, 0.863157f, 0.871367f, 0.879622f, - 0.887923f, 0.896269f, 0.904661f, 0.913099f, 0.921582f, 0.930111f, 0.938686f, 0.947307f, 0.955974f, 0.964686f, 0.973445f, - 0.982251f, 0.991102f, 1.0f -}; - -typedef union -{ - unsigned int u; - float f; -} stbir__FP32; - -static const stbir_uint32 fp32_to_srgb8_tab4[104] = { - 0x0073000d, 0x007a000d, 0x0080000d, 0x0087000d, 0x008d000d, 0x0094000d, 0x009a000d, 0x00a1000d, - 0x00a7001a, 0x00b4001a, 0x00c1001a, 0x00ce001a, 0x00da001a, 0x00e7001a, 0x00f4001a, 0x0101001a, - 0x010e0033, 0x01280033, 0x01410033, 0x015b0033, 0x01750033, 0x018f0033, 0x01a80033, 0x01c20033, - 0x01dc0067, 0x020f0067, 0x02430067, 0x02760067, 0x02aa0067, 0x02dd0067, 0x03110067, 0x03440067, - 0x037800ce, 0x03df00ce, 0x044600ce, 0x04ad00ce, 0x051400ce, 0x057b00c5, 0x05dd00bc, 0x063b00b5, - 0x06970158, 0x07420142, 0x07e30130, 0x087b0120, 0x090b0112, 0x09940106, 0x0a1700fc, 0x0a9500f2, - 0x0b0f01cb, 0x0bf401ae, 0x0ccb0195, 0x0d950180, 0x0e56016e, 0x0f0d015e, 0x0fbc0150, 0x10630143, - 0x11070264, 0x1238023e, 0x1357021d, 0x14660201, 0x156601e9, 0x165a01d3, 0x174401c0, 0x182401af, - 0x18fe0331, 0x1a9602fe, 0x1c1502d2, 0x1d7e02ad, 0x1ed4028d, 0x201a0270, 0x21520256, 0x227d0240, - 0x239f0443, 0x25c003fe, 0x27bf03c4, 0x29a10392, 0x2b6a0367, 0x2d1d0341, 0x2ebe031f, 0x304d0300, - 0x31d105b0, 0x34a80555, 0x37520507, 0x39d504c5, 0x3c37048b, 0x3e7c0458, 0x40a8042a, 0x42bd0401, - 0x44c20798, 0x488e071e, 0x4c1c06b6, 0x4f76065d, 0x52a50610, 0x55ac05cc, 0x5892058f, 0x5b590559, - 0x5e0c0a23, 0x631c0980, 0x67db08f6, 0x6c55087f, 0x70940818, 0x74a007bd, 0x787d076c, 0x7c330723, -}; - -static stbir__inline stbir_uint8 stbir__linear_to_srgb_uchar(float in) -{ - static const stbir__FP32 almostone = { 0x3f7fffff }; // 1-eps - static const stbir__FP32 minval = { (127-13) << 23 }; - stbir_uint32 tab,bias,scale,t; - stbir__FP32 f; - - if (!(in > minval.f)) // written this way to catch NaNs - return 0; - if (in > almostone.f) - return 255; - - f.f = in; - tab = fp32_to_srgb8_tab4[(f.u - minval.u) >> 20]; - bias = (tab >> 16) << 9; - scale = tab & 0xffff; - - t = (f.u >> 12) & 0xff; - return (unsigned char) ((bias + scale*t) >> 16); -} - -#ifndef STBIR_FORCE_GATHER_FILTER_SCANLINES_AMOUNT -#define STBIR_FORCE_GATHER_FILTER_SCANLINES_AMOUNT 32 // when downsampling and <= 32 scanlines of buffering, use gather. gather used down to 1/8th scaling for 25% win. -#endif - -#ifndef STBIR_FORCE_MINIMUM_SCANLINES_FOR_SPLITS -#define STBIR_FORCE_MINIMUM_SCANLINES_FOR_SPLITS 4 // when threading, what is the minimum number of scanlines for a split? -#endif - -#define STBIR_INPUT_CALLBACK_PADDING 3 - -#ifdef _M_IX86_FP -#if ( _M_IX86_FP >= 1 ) -#ifndef STBIR_SSE -#define STBIR_SSE -#endif -#endif -#endif - -#ifdef __TINYC__ - #define STBIR_NO_SIMD -#endif - -#if defined(_x86_64) || defined( __x86_64__ ) || defined( _M_X64 ) || defined(__x86_64) || defined(_M_AMD64) || defined(__SSE2__) || defined(STBIR_SSE) || defined(STBIR_SSE2) - #ifndef STBIR_SSE2 - #define STBIR_SSE2 - #endif - #if defined(__AVX__) || defined(STBIR_AVX2) - #ifndef STBIR_AVX - #ifndef STBIR_NO_AVX - #define STBIR_AVX - #endif - #endif - #endif - #if defined(__AVX2__) || defined(STBIR_AVX2) - #ifndef STBIR_NO_AVX2 - #ifndef STBIR_AVX2 - #define STBIR_AVX2 - #endif - #if defined( _MSC_VER ) && !defined(__clang__) - #ifndef STBIR_FP16C // FP16C instructions are on all AVX2 cpus, so we can autoselect it here on microsoft - clang needs -m16c - #define STBIR_FP16C - #endif - #endif - #endif - #endif - #ifdef __F16C__ - #ifndef STBIR_FP16C // turn on FP16C instructions if the define is set (for clang and gcc) - #define STBIR_FP16C - #endif - #endif -#endif - -#if defined( _M_ARM64 ) || defined( __aarch64__ ) || defined( __arm64__ ) || ((__ARM_NEON_FP & 4) != 0) || defined(__ARM_NEON__) -#ifndef STBIR_NEON -#define STBIR_NEON -#endif -#endif - -#if defined(_M_ARM) || defined(__arm__) -#ifdef STBIR_USE_FMA -#undef STBIR_USE_FMA // no FMA for 32-bit arm on MSVC -#endif -#endif - -#if defined(__wasm__) && defined(__wasm_simd128__) -#ifndef STBIR_WASM -#define STBIR_WASM -#endif -#endif - -#if defined( _MSC_VER ) && !defined(__clang__) - #define STBIR_STREAMOUT_PTR( star ) star __restrict - #define STBIR_NO_UNROLL( ptr ) __assume(ptr) // this oddly keeps msvc from unrolling a loop - #if _MSC_VER >= 1900 - #define STBIR_NO_UNROLL_LOOP_START __pragma(loop( no_vector )) - #else - #define STBIR_NO_UNROLL_LOOP_START - #endif -#elif defined( __clang__ ) - #define STBIR_STREAMOUT_PTR( star ) star __restrict__ - #define STBIR_NO_UNROLL( ptr ) __asm__ (""::"r"(ptr)) - #if ( __clang_major__ >= 4 ) || ( ( __clang_major__ >= 3 ) && ( __clang_minor__ >= 5 ) ) - #define STBIR_NO_UNROLL_LOOP_START _Pragma("clang loop unroll(disable)") _Pragma("clang loop vectorize(disable)") - #else - #define STBIR_NO_UNROLL_LOOP_START - #endif -#elif defined( __GNUC__ ) - #define STBIR_STREAMOUT_PTR( star ) star __restrict__ - #define STBIR_NO_UNROLL( ptr ) __asm__ (""::"r"(ptr)) - #if __GNUC__ >= 14 - #define STBIR_NO_UNROLL_LOOP_START _Pragma("GCC unroll 0") _Pragma("GCC novector") - #else - #define STBIR_NO_UNROLL_LOOP_START - #endif - #define STBIR_NO_UNROLL_LOOP_START_INF_FOR -#else - #define STBIR_STREAMOUT_PTR( star ) star - #define STBIR_NO_UNROLL( ptr ) - #define STBIR_NO_UNROLL_LOOP_START -#endif - -#ifndef STBIR_NO_UNROLL_LOOP_START_INF_FOR -#define STBIR_NO_UNROLL_LOOP_START_INF_FOR STBIR_NO_UNROLL_LOOP_START -#endif - -#ifdef STBIR_NO_SIMD // force simd off for whatever reason - -#ifdef STBIR_SSE2 -#undef STBIR_SSE2 -#endif - -#ifdef STBIR_AVX -#undef STBIR_AVX -#endif - -#ifdef STBIR_NEON -#undef STBIR_NEON -#endif - -#ifdef STBIR_AVX2 -#undef STBIR_AVX2 -#endif - -#ifdef STBIR_FP16C -#undef STBIR_FP16C -#endif - -#ifdef STBIR_WASM -#undef STBIR_WASM -#endif - -#ifdef STBIR_SIMD -#undef STBIR_SIMD -#endif - -#else // STBIR_SIMD - -#ifdef STBIR_SSE2 - #include - - #define stbir__simdf __m128 - #define stbir__simdi __m128i - - #define stbir_simdi_castf( reg ) _mm_castps_si128(reg) - #define stbir_simdf_casti( reg ) _mm_castsi128_ps(reg) - - #define stbir__simdf_load( reg, ptr ) (reg) = _mm_loadu_ps( (float const*)(ptr) ) - #define stbir__simdi_load( reg, ptr ) (reg) = _mm_loadu_si128 ( (stbir__simdi const*)(ptr) ) - #define stbir__simdf_load1( out, ptr ) (out) = _mm_load_ss( (float const*)(ptr) ) // top values can be random (not denormal or nan for perf) - #define stbir__simdi_load1( out, ptr ) (out) = _mm_castps_si128( _mm_load_ss( (float const*)(ptr) )) - #define stbir__simdf_load1z( out, ptr ) (out) = _mm_load_ss( (float const*)(ptr) ) // top values must be zero - #define stbir__simdf_frep4( fvar ) _mm_set_ps1( fvar ) - #define stbir__simdf_load1frep4( out, fvar ) (out) = _mm_set_ps1( fvar ) - #define stbir__simdf_load2( out, ptr ) (out) = _mm_castsi128_ps( _mm_loadl_epi64( (__m128i*)(ptr)) ) // top values can be random (not denormal or nan for perf) - #define stbir__simdf_load2z( out, ptr ) (out) = _mm_castsi128_ps( _mm_loadl_epi64( (__m128i*)(ptr)) ) // top values must be zero - #define stbir__simdf_load2hmerge( out, reg, ptr ) (out) = _mm_castpd_ps(_mm_loadh_pd( _mm_castps_pd(reg), (double*)(ptr) )) - - #define stbir__simdf_zeroP() _mm_setzero_ps() - #define stbir__simdf_zero( reg ) (reg) = _mm_setzero_ps() - - #define stbir__simdf_store( ptr, reg ) _mm_storeu_ps( (float*)(ptr), reg ) - #define stbir__simdf_store1( ptr, reg ) _mm_store_ss( (float*)(ptr), reg ) - #define stbir__simdf_store2( ptr, reg ) _mm_storel_epi64( (__m128i*)(ptr), _mm_castps_si128(reg) ) - #define stbir__simdf_store2h( ptr, reg ) _mm_storeh_pd( (double*)(ptr), _mm_castps_pd(reg) ) - - #define stbir__simdi_store( ptr, reg ) _mm_storeu_si128( (__m128i*)(ptr), reg ) - #define stbir__simdi_store1( ptr, reg ) _mm_store_ss( (float*)(ptr), _mm_castsi128_ps(reg) ) - #define stbir__simdi_store2( ptr, reg ) _mm_storel_epi64( (__m128i*)(ptr), (reg) ) - - #define stbir__prefetch( ptr ) _mm_prefetch((char*)(ptr), _MM_HINT_T0 ) - - #define stbir__simdi_expand_u8_to_u32(out0,out1,out2,out3,ireg) \ - { \ - stbir__simdi zero = _mm_setzero_si128(); \ - out2 = _mm_unpacklo_epi8( ireg, zero ); \ - out3 = _mm_unpackhi_epi8( ireg, zero ); \ - out0 = _mm_unpacklo_epi16( out2, zero ); \ - out1 = _mm_unpackhi_epi16( out2, zero ); \ - out2 = _mm_unpacklo_epi16( out3, zero ); \ - out3 = _mm_unpackhi_epi16( out3, zero ); \ - } - -#define stbir__simdi_expand_u8_to_1u32(out,ireg) \ - { \ - stbir__simdi zero = _mm_setzero_si128(); \ - out = _mm_unpacklo_epi8( ireg, zero ); \ - out = _mm_unpacklo_epi16( out, zero ); \ - } - - #define stbir__simdi_expand_u16_to_u32(out0,out1,ireg) \ - { \ - stbir__simdi zero = _mm_setzero_si128(); \ - out0 = _mm_unpacklo_epi16( ireg, zero ); \ - out1 = _mm_unpackhi_epi16( ireg, zero ); \ - } - - #define stbir__simdf_convert_float_to_i32( i, f ) (i) = _mm_cvttps_epi32(f) - #define stbir__simdf_convert_float_to_int( f ) _mm_cvtt_ss2si(f) - #define stbir__simdf_convert_float_to_uint8( f ) ((unsigned char)_mm_cvtsi128_si32(_mm_cvttps_epi32(_mm_max_ps(_mm_min_ps(f,STBIR__CONSTF(STBIR_max_uint8_as_float)),_mm_setzero_ps())))) - #define stbir__simdf_convert_float_to_short( f ) ((unsigned short)_mm_cvtsi128_si32(_mm_cvttps_epi32(_mm_max_ps(_mm_min_ps(f,STBIR__CONSTF(STBIR_max_uint16_as_float)),_mm_setzero_ps())))) - - #define stbir__simdi_to_int( i ) _mm_cvtsi128_si32(i) - #define stbir__simdi_convert_i32_to_float(out, ireg) (out) = _mm_cvtepi32_ps( ireg ) - #define stbir__simdf_add( out, reg0, reg1 ) (out) = _mm_add_ps( reg0, reg1 ) - #define stbir__simdf_mult( out, reg0, reg1 ) (out) = _mm_mul_ps( reg0, reg1 ) - #define stbir__simdf_mult_mem( out, reg, ptr ) (out) = _mm_mul_ps( reg, _mm_loadu_ps( (float const*)(ptr) ) ) - #define stbir__simdf_mult1_mem( out, reg, ptr ) (out) = _mm_mul_ss( reg, _mm_load_ss( (float const*)(ptr) ) ) - #define stbir__simdf_add_mem( out, reg, ptr ) (out) = _mm_add_ps( reg, _mm_loadu_ps( (float const*)(ptr) ) ) - #define stbir__simdf_add1_mem( out, reg, ptr ) (out) = _mm_add_ss( reg, _mm_load_ss( (float const*)(ptr) ) ) - - #ifdef STBIR_USE_FMA // not on by default to maintain bit identical simd to non-simd - #include - #define stbir__simdf_madd( out, add, mul1, mul2 ) (out) = _mm_fmadd_ps( mul1, mul2, add ) - #define stbir__simdf_madd1( out, add, mul1, mul2 ) (out) = _mm_fmadd_ss( mul1, mul2, add ) - #define stbir__simdf_madd_mem( out, add, mul, ptr ) (out) = _mm_fmadd_ps( mul, _mm_loadu_ps( (float const*)(ptr) ), add ) - #define stbir__simdf_madd1_mem( out, add, mul, ptr ) (out) = _mm_fmadd_ss( mul, _mm_load_ss( (float const*)(ptr) ), add ) - #else - #define stbir__simdf_madd( out, add, mul1, mul2 ) (out) = _mm_add_ps( add, _mm_mul_ps( mul1, mul2 ) ) - #define stbir__simdf_madd1( out, add, mul1, mul2 ) (out) = _mm_add_ss( add, _mm_mul_ss( mul1, mul2 ) ) - #define stbir__simdf_madd_mem( out, add, mul, ptr ) (out) = _mm_add_ps( add, _mm_mul_ps( mul, _mm_loadu_ps( (float const*)(ptr) ) ) ) - #define stbir__simdf_madd1_mem( out, add, mul, ptr ) (out) = _mm_add_ss( add, _mm_mul_ss( mul, _mm_load_ss( (float const*)(ptr) ) ) ) - #endif - - #define stbir__simdf_add1( out, reg0, reg1 ) (out) = _mm_add_ss( reg0, reg1 ) - #define stbir__simdf_mult1( out, reg0, reg1 ) (out) = _mm_mul_ss( reg0, reg1 ) - - #define stbir__simdf_and( out, reg0, reg1 ) (out) = _mm_and_ps( reg0, reg1 ) - #define stbir__simdf_or( out, reg0, reg1 ) (out) = _mm_or_ps( reg0, reg1 ) - - #define stbir__simdf_min( out, reg0, reg1 ) (out) = _mm_min_ps( reg0, reg1 ) - #define stbir__simdf_max( out, reg0, reg1 ) (out) = _mm_max_ps( reg0, reg1 ) - #define stbir__simdf_min1( out, reg0, reg1 ) (out) = _mm_min_ss( reg0, reg1 ) - #define stbir__simdf_max1( out, reg0, reg1 ) (out) = _mm_max_ss( reg0, reg1 ) - - #define stbir__simdf_0123ABCDto3ABx( out, reg0, reg1 ) (out)=_mm_castsi128_ps( _mm_shuffle_epi32( _mm_castps_si128( _mm_shuffle_ps( reg1,reg0, (0<<0) + (1<<2) + (2<<4) + (3<<6) )), (3<<0) + (0<<2) + (1<<4) + (2<<6) ) ) - #define stbir__simdf_0123ABCDto23Ax( out, reg0, reg1 ) (out)=_mm_castsi128_ps( _mm_shuffle_epi32( _mm_castps_si128( _mm_shuffle_ps( reg1,reg0, (0<<0) + (1<<2) + (2<<4) + (3<<6) )), (2<<0) + (3<<2) + (0<<4) + (1<<6) ) ) - - static const stbir__simdf STBIR_zeroones = { 0.0f,1.0f,0.0f,1.0f }; - static const stbir__simdf STBIR_onezeros = { 1.0f,0.0f,1.0f,0.0f }; - #define stbir__simdf_aaa1( out, alp, ones ) (out)=_mm_castsi128_ps( _mm_shuffle_epi32( _mm_castps_si128( _mm_movehl_ps( ones, alp ) ), (1<<0) + (1<<2) + (1<<4) + (2<<6) ) ) - #define stbir__simdf_1aaa( out, alp, ones ) (out)=_mm_castsi128_ps( _mm_shuffle_epi32( _mm_castps_si128( _mm_movelh_ps( ones, alp ) ), (0<<0) + (2<<2) + (2<<4) + (2<<6) ) ) - #define stbir__simdf_a1a1( out, alp, ones) (out) = _mm_or_ps( _mm_castsi128_ps( _mm_srli_epi64( _mm_castps_si128(alp), 32 ) ), STBIR_zeroones ) - #define stbir__simdf_1a1a( out, alp, ones) (out) = _mm_or_ps( _mm_castsi128_ps( _mm_slli_epi64( _mm_castps_si128(alp), 32 ) ), STBIR_onezeros ) - - #define stbir__simdf_swiz( reg, one, two, three, four ) _mm_castsi128_ps( _mm_shuffle_epi32( _mm_castps_si128( reg ), (one<<0) + (two<<2) + (three<<4) + (four<<6) ) ) - - #define stbir__simdi_and( out, reg0, reg1 ) (out) = _mm_and_si128( reg0, reg1 ) - #define stbir__simdi_or( out, reg0, reg1 ) (out) = _mm_or_si128( reg0, reg1 ) - #define stbir__simdi_16madd( out, reg0, reg1 ) (out) = _mm_madd_epi16( reg0, reg1 ) - - #define stbir__simdf_pack_to_8bytes(out,aa,bb) \ - { \ - stbir__simdf af,bf; \ - stbir__simdi a,b; \ - af = _mm_min_ps( aa, STBIR_max_uint8_as_float ); \ - bf = _mm_min_ps( bb, STBIR_max_uint8_as_float ); \ - af = _mm_max_ps( af, _mm_setzero_ps() ); \ - bf = _mm_max_ps( bf, _mm_setzero_ps() ); \ - a = _mm_cvttps_epi32( af ); \ - b = _mm_cvttps_epi32( bf ); \ - a = _mm_packs_epi32( a, b ); \ - out = _mm_packus_epi16( a, a ); \ - } - - #define stbir__simdf_load4_transposed( o0, o1, o2, o3, ptr ) \ - stbir__simdf_load( o0, (ptr) ); \ - stbir__simdf_load( o1, (ptr)+4 ); \ - stbir__simdf_load( o2, (ptr)+8 ); \ - stbir__simdf_load( o3, (ptr)+12 ); \ - { \ - __m128 tmp0, tmp1, tmp2, tmp3; \ - tmp0 = _mm_unpacklo_ps(o0, o1); \ - tmp2 = _mm_unpacklo_ps(o2, o3); \ - tmp1 = _mm_unpackhi_ps(o0, o1); \ - tmp3 = _mm_unpackhi_ps(o2, o3); \ - o0 = _mm_movelh_ps(tmp0, tmp2); \ - o1 = _mm_movehl_ps(tmp2, tmp0); \ - o2 = _mm_movelh_ps(tmp1, tmp3); \ - o3 = _mm_movehl_ps(tmp3, tmp1); \ - } - - #define stbir__interleave_pack_and_store_16_u8( ptr, r0, r1, r2, r3 ) \ - r0 = _mm_packs_epi32( r0, r1 ); \ - r2 = _mm_packs_epi32( r2, r3 ); \ - r1 = _mm_unpacklo_epi16( r0, r2 ); \ - r3 = _mm_unpackhi_epi16( r0, r2 ); \ - r0 = _mm_unpacklo_epi16( r1, r3 ); \ - r2 = _mm_unpackhi_epi16( r1, r3 ); \ - r0 = _mm_packus_epi16( r0, r2 ); \ - stbir__simdi_store( ptr, r0 ); \ - - #define stbir__simdi_32shr( out, reg, imm ) out = _mm_srli_epi32( reg, imm ) - - #if defined(_MSC_VER) && !defined(__clang__) - #define STBIR__CONST_32_TO_8( v ) (char)(unsigned char)((v)&255),(char)(unsigned char)(((v)>>8)&255),(char)(unsigned char)(((v)>>16)&255),(char)(unsigned char)(((v)>>24)&255) - #define STBIR__CONST_4_32i( v ) STBIR__CONST_32_TO_8( v ), STBIR__CONST_32_TO_8( v ), STBIR__CONST_32_TO_8( v ), STBIR__CONST_32_TO_8( v ) - #define STBIR__CONST_4d_32i( v0, v1, v2, v3 ) STBIR__CONST_32_TO_8( v0 ), STBIR__CONST_32_TO_8( v1 ), STBIR__CONST_32_TO_8( v2 ), STBIR__CONST_32_TO_8( v3 ) - #else - #define STBIR__CONST_4_32i( v ) (long long)((((stbir_uint64)(stbir_uint32)(v))<<32)|((stbir_uint64)(stbir_uint32)(v))),(long long)((((stbir_uint64)(stbir_uint32)(v))<<32)|((stbir_uint64)(stbir_uint32)(v))) - #define STBIR__CONST_4d_32i( v0, v1, v2, v3 ) (long long)((((stbir_uint64)(stbir_uint32)(v1))<<32)|((stbir_uint64)(stbir_uint32)(v0))),(long long)((((stbir_uint64)(stbir_uint32)(v3))<<32)|((stbir_uint64)(stbir_uint32)(v2))) - #endif - - #define STBIR__SIMDF_CONST(var, x) stbir__simdf var = { x, x, x, x } - #define STBIR__SIMDI_CONST(var, x) stbir__simdi var = { STBIR__CONST_4_32i(x) } - #define STBIR__CONSTF(var) (var) - #define STBIR__CONSTI(var) (var) - - #if defined(STBIR_AVX) || defined(__SSE4_1__) - #include - #define stbir__simdf_pack_to_8words(out,reg0,reg1) out = _mm_packus_epi32(_mm_cvttps_epi32(_mm_max_ps(_mm_min_ps(reg0,STBIR__CONSTF(STBIR_max_uint16_as_float)),_mm_setzero_ps())), _mm_cvttps_epi32(_mm_max_ps(_mm_min_ps(reg1,STBIR__CONSTF(STBIR_max_uint16_as_float)),_mm_setzero_ps()))) - #else - static STBIR__SIMDI_CONST(stbir__s32_32768, 32768); - static STBIR__SIMDI_CONST(stbir__s16_32768, ((32768<<16)|32768)); - - #define stbir__simdf_pack_to_8words(out,reg0,reg1) \ - { \ - stbir__simdi tmp0,tmp1; \ - tmp0 = _mm_cvttps_epi32(_mm_max_ps(_mm_min_ps(reg0,STBIR__CONSTF(STBIR_max_uint16_as_float)),_mm_setzero_ps())); \ - tmp1 = _mm_cvttps_epi32(_mm_max_ps(_mm_min_ps(reg1,STBIR__CONSTF(STBIR_max_uint16_as_float)),_mm_setzero_ps())); \ - tmp0 = _mm_sub_epi32( tmp0, stbir__s32_32768 ); \ - tmp1 = _mm_sub_epi32( tmp1, stbir__s32_32768 ); \ - out = _mm_packs_epi32( tmp0, tmp1 ); \ - out = _mm_sub_epi16( out, stbir__s16_32768 ); \ - } - - #endif - - #define STBIR_SIMD - - #ifdef STBIR_AVX - #include - #define STBIR_SIMD8 - #define stbir__simdf8 __m256 - #define stbir__simdi8 __m256i - #define stbir__simdf8_load( out, ptr ) (out) = _mm256_loadu_ps( (float const *)(ptr) ) - #define stbir__simdi8_load( out, ptr ) (out) = _mm256_loadu_si256( (__m256i const *)(ptr) ) - #define stbir__simdf8_mult( out, a, b ) (out) = _mm256_mul_ps( (a), (b) ) - #define stbir__simdf8_store( ptr, out ) _mm256_storeu_ps( (float*)(ptr), out ) - #define stbir__simdi8_store( ptr, reg ) _mm256_storeu_si256( (__m256i*)(ptr), reg ) - #define stbir__simdf8_frep8( fval ) _mm256_set1_ps( fval ) - - #define stbir__simdf8_min( out, reg0, reg1 ) (out) = _mm256_min_ps( reg0, reg1 ) - #define stbir__simdf8_max( out, reg0, reg1 ) (out) = _mm256_max_ps( reg0, reg1 ) - - #define stbir__simdf8_add4halves( out, bot4, top8 ) (out) = _mm_add_ps( bot4, _mm256_extractf128_ps( top8, 1 ) ) - #define stbir__simdf8_mult_mem( out, reg, ptr ) (out) = _mm256_mul_ps( reg, _mm256_loadu_ps( (float const*)(ptr) ) ) - #define stbir__simdf8_add_mem( out, reg, ptr ) (out) = _mm256_add_ps( reg, _mm256_loadu_ps( (float const*)(ptr) ) ) - #define stbir__simdf8_add( out, a, b ) (out) = _mm256_add_ps( a, b ) - #define stbir__simdf8_load1b( out, ptr ) (out) = _mm256_broadcast_ss( ptr ) - #define stbir__simdf_load1rep4( out, ptr ) (out) = _mm_broadcast_ss( ptr ) // avx load instruction - - #define stbir__simdi8_convert_i32_to_float(out, ireg) (out) = _mm256_cvtepi32_ps( ireg ) - #define stbir__simdf8_convert_float_to_i32( i, f ) (i) = _mm256_cvttps_epi32(f) - - #define stbir__simdf8_bot4s( out, a, b ) (out) = _mm256_permute2f128_ps(a,b, (0<<0)+(2<<4) ) - #define stbir__simdf8_top4s( out, a, b ) (out) = _mm256_permute2f128_ps(a,b, (1<<0)+(3<<4) ) - - #define stbir__simdf8_gettop4( reg ) _mm256_extractf128_ps(reg,1) - - #ifdef STBIR_AVX2 - - #define stbir__simdi8_expand_u8_to_u32(out0,out1,ireg) \ - { \ - stbir__simdi8 a, zero =_mm256_setzero_si256();\ - a = _mm256_permute4x64_epi64( _mm256_unpacklo_epi8( _mm256_permute4x64_epi64(_mm256_castsi128_si256(ireg),(0<<0)+(2<<2)+(1<<4)+(3<<6)), zero ),(0<<0)+(2<<2)+(1<<4)+(3<<6)); \ - out0 = _mm256_unpacklo_epi16( a, zero ); \ - out1 = _mm256_unpackhi_epi16( a, zero ); \ - } - - #define stbir__simdf8_pack_to_16bytes(out,aa,bb) \ - { \ - stbir__simdi8 t; \ - stbir__simdf8 af,bf; \ - stbir__simdi8 a,b; \ - af = _mm256_min_ps( aa, STBIR_max_uint8_as_floatX ); \ - bf = _mm256_min_ps( bb, STBIR_max_uint8_as_floatX ); \ - af = _mm256_max_ps( af, _mm256_setzero_ps() ); \ - bf = _mm256_max_ps( bf, _mm256_setzero_ps() ); \ - a = _mm256_cvttps_epi32( af ); \ - b = _mm256_cvttps_epi32( bf ); \ - t = _mm256_permute4x64_epi64( _mm256_packs_epi32( a, b ), (0<<0)+(2<<2)+(1<<4)+(3<<6) ); \ - out = _mm256_castsi256_si128( _mm256_permute4x64_epi64( _mm256_packus_epi16( t, t ), (0<<0)+(2<<2)+(1<<4)+(3<<6) ) ); \ - } - - #define stbir__simdi8_expand_u16_to_u32(out,ireg) out = _mm256_unpacklo_epi16( _mm256_permute4x64_epi64(_mm256_castsi128_si256(ireg),(0<<0)+(2<<2)+(1<<4)+(3<<6)), _mm256_setzero_si256() ); - - #define stbir__simdf8_pack_to_16words(out,aa,bb) \ - { \ - stbir__simdf8 af,bf; \ - stbir__simdi8 a,b; \ - af = _mm256_min_ps( aa, STBIR_max_uint16_as_floatX ); \ - bf = _mm256_min_ps( bb, STBIR_max_uint16_as_floatX ); \ - af = _mm256_max_ps( af, _mm256_setzero_ps() ); \ - bf = _mm256_max_ps( bf, _mm256_setzero_ps() ); \ - a = _mm256_cvttps_epi32( af ); \ - b = _mm256_cvttps_epi32( bf ); \ - (out) = _mm256_permute4x64_epi64( _mm256_packus_epi32(a, b), (0<<0)+(2<<2)+(1<<4)+(3<<6) ); \ - } - - #else - - #define stbir__simdi8_expand_u8_to_u32(out0,out1,ireg) \ - { \ - stbir__simdi a,zero = _mm_setzero_si128(); \ - a = _mm_unpacklo_epi8( ireg, zero ); \ - out0 = _mm256_setr_m128i( _mm_unpacklo_epi16( a, zero ), _mm_unpackhi_epi16( a, zero ) ); \ - a = _mm_unpackhi_epi8( ireg, zero ); \ - out1 = _mm256_setr_m128i( _mm_unpacklo_epi16( a, zero ), _mm_unpackhi_epi16( a, zero ) ); \ - } - - #define stbir__simdf8_pack_to_16bytes(out,aa,bb) \ - { \ - stbir__simdi t; \ - stbir__simdf8 af,bf; \ - stbir__simdi8 a,b; \ - af = _mm256_min_ps( aa, STBIR_max_uint8_as_floatX ); \ - bf = _mm256_min_ps( bb, STBIR_max_uint8_as_floatX ); \ - af = _mm256_max_ps( af, _mm256_setzero_ps() ); \ - bf = _mm256_max_ps( bf, _mm256_setzero_ps() ); \ - a = _mm256_cvttps_epi32( af ); \ - b = _mm256_cvttps_epi32( bf ); \ - out = _mm_packs_epi32( _mm256_castsi256_si128(a), _mm256_extractf128_si256( a, 1 ) ); \ - out = _mm_packus_epi16( out, out ); \ - t = _mm_packs_epi32( _mm256_castsi256_si128(b), _mm256_extractf128_si256( b, 1 ) ); \ - t = _mm_packus_epi16( t, t ); \ - out = _mm_castps_si128( _mm_shuffle_ps( _mm_castsi128_ps(out), _mm_castsi128_ps(t), (0<<0)+(1<<2)+(0<<4)+(1<<6) ) ); \ - } - - #define stbir__simdi8_expand_u16_to_u32(out,ireg) \ - { \ - stbir__simdi a,b,zero = _mm_setzero_si128(); \ - a = _mm_unpacklo_epi16( ireg, zero ); \ - b = _mm_unpackhi_epi16( ireg, zero ); \ - out = _mm256_insertf128_si256( _mm256_castsi128_si256( a ), b, 1 ); \ - } - - #define stbir__simdf8_pack_to_16words(out,aa,bb) \ - { \ - stbir__simdi t0,t1; \ - stbir__simdf8 af,bf; \ - stbir__simdi8 a,b; \ - af = _mm256_min_ps( aa, STBIR_max_uint16_as_floatX ); \ - bf = _mm256_min_ps( bb, STBIR_max_uint16_as_floatX ); \ - af = _mm256_max_ps( af, _mm256_setzero_ps() ); \ - bf = _mm256_max_ps( bf, _mm256_setzero_ps() ); \ - a = _mm256_cvttps_epi32( af ); \ - b = _mm256_cvttps_epi32( bf ); \ - t0 = _mm_packus_epi32( _mm256_castsi256_si128(a), _mm256_extractf128_si256( a, 1 ) ); \ - t1 = _mm_packus_epi32( _mm256_castsi256_si128(b), _mm256_extractf128_si256( b, 1 ) ); \ - out = _mm256_setr_m128i( t0, t1 ); \ - } - - #endif - - static __m256i stbir_00001111 = { STBIR__CONST_4d_32i( 0, 0, 0, 0 ), STBIR__CONST_4d_32i( 1, 1, 1, 1 ) }; - #define stbir__simdf8_0123to00001111( out, in ) (out) = _mm256_permutevar_ps ( in, stbir_00001111 ) - - static __m256i stbir_22223333 = { STBIR__CONST_4d_32i( 2, 2, 2, 2 ), STBIR__CONST_4d_32i( 3, 3, 3, 3 ) }; - #define stbir__simdf8_0123to22223333( out, in ) (out) = _mm256_permutevar_ps ( in, stbir_22223333 ) - - #define stbir__simdf8_0123to2222( out, in ) (out) = stbir__simdf_swiz(_mm256_castps256_ps128(in), 2,2,2,2 ) - - #define stbir__simdf8_load4b( out, ptr ) (out) = _mm256_broadcast_ps( (__m128 const *)(ptr) ) - - static __m256i stbir_00112233 = { STBIR__CONST_4d_32i( 0, 0, 1, 1 ), STBIR__CONST_4d_32i( 2, 2, 3, 3 ) }; - #define stbir__simdf8_0123to00112233( out, in ) (out) = _mm256_permutevar_ps ( in, stbir_00112233 ) - #define stbir__simdf8_add4( out, a8, b ) (out) = _mm256_add_ps( a8, _mm256_castps128_ps256( b ) ) - - static __m256i stbir_load6 = { STBIR__CONST_4_32i( 0x80000000 ), STBIR__CONST_4d_32i( 0x80000000, 0x80000000, 0, 0 ) }; - #define stbir__simdf8_load6z( out, ptr ) (out) = _mm256_maskload_ps( ptr, stbir_load6 ) - - #define stbir__simdf8_0123to00000000( out, in ) (out) = _mm256_shuffle_ps ( in, in, (0<<0)+(0<<2)+(0<<4)+(0<<6) ) - #define stbir__simdf8_0123to11111111( out, in ) (out) = _mm256_shuffle_ps ( in, in, (1<<0)+(1<<2)+(1<<4)+(1<<6) ) - #define stbir__simdf8_0123to22222222( out, in ) (out) = _mm256_shuffle_ps ( in, in, (2<<0)+(2<<2)+(2<<4)+(2<<6) ) - #define stbir__simdf8_0123to33333333( out, in ) (out) = _mm256_shuffle_ps ( in, in, (3<<0)+(3<<2)+(3<<4)+(3<<6) ) - #define stbir__simdf8_0123to21032103( out, in ) (out) = _mm256_shuffle_ps ( in, in, (2<<0)+(1<<2)+(0<<4)+(3<<6) ) - #define stbir__simdf8_0123to32103210( out, in ) (out) = _mm256_shuffle_ps ( in, in, (3<<0)+(2<<2)+(1<<4)+(0<<6) ) - #define stbir__simdf8_0123to12301230( out, in ) (out) = _mm256_shuffle_ps ( in, in, (1<<0)+(2<<2)+(3<<4)+(0<<6) ) - #define stbir__simdf8_0123to10321032( out, in ) (out) = _mm256_shuffle_ps ( in, in, (1<<0)+(0<<2)+(3<<4)+(2<<6) ) - #define stbir__simdf8_0123to30123012( out, in ) (out) = _mm256_shuffle_ps ( in, in, (3<<0)+(0<<2)+(1<<4)+(2<<6) ) - - #define stbir__simdf8_0123to11331133( out, in ) (out) = _mm256_shuffle_ps ( in, in, (1<<0)+(1<<2)+(3<<4)+(3<<6) ) - #define stbir__simdf8_0123to00220022( out, in ) (out) = _mm256_shuffle_ps ( in, in, (0<<0)+(0<<2)+(2<<4)+(2<<6) ) - - #define stbir__simdf8_aaa1( out, alp, ones ) (out) = _mm256_blend_ps( alp, ones, (1<<0)+(1<<1)+(1<<2)+(0<<3)+(1<<4)+(1<<5)+(1<<6)+(0<<7)); (out)=_mm256_shuffle_ps( out,out, (3<<0) + (3<<2) + (3<<4) + (0<<6) ) - #define stbir__simdf8_1aaa( out, alp, ones ) (out) = _mm256_blend_ps( alp, ones, (0<<0)+(1<<1)+(1<<2)+(1<<3)+(0<<4)+(1<<5)+(1<<6)+(1<<7)); (out)=_mm256_shuffle_ps( out,out, (1<<0) + (0<<2) + (0<<4) + (0<<6) ) - #define stbir__simdf8_a1a1( out, alp, ones) (out) = _mm256_blend_ps( alp, ones, (1<<0)+(0<<1)+(1<<2)+(0<<3)+(1<<4)+(0<<5)+(1<<6)+(0<<7)); (out)=_mm256_shuffle_ps( out,out, (1<<0) + (0<<2) + (3<<4) + (2<<6) ) - #define stbir__simdf8_1a1a( out, alp, ones) (out) = _mm256_blend_ps( alp, ones, (0<<0)+(1<<1)+(0<<2)+(1<<3)+(0<<4)+(1<<5)+(0<<6)+(1<<7)); (out)=_mm256_shuffle_ps( out,out, (1<<0) + (0<<2) + (3<<4) + (2<<6) ) - - #define stbir__simdf8_zero( reg ) (reg) = _mm256_setzero_ps() - - #ifdef STBIR_USE_FMA // not on by default to maintain bit identical simd to non-simd - #define stbir__simdf8_madd( out, add, mul1, mul2 ) (out) = _mm256_fmadd_ps( mul1, mul2, add ) - #define stbir__simdf8_madd_mem( out, add, mul, ptr ) (out) = _mm256_fmadd_ps( mul, _mm256_loadu_ps( (float const*)(ptr) ), add ) - #define stbir__simdf8_madd_mem4( out, add, mul, ptr )(out) = _mm256_fmadd_ps( _mm256_setr_m128( mul, _mm_setzero_ps() ), _mm256_setr_m128( _mm_loadu_ps( (float const*)(ptr) ), _mm_setzero_ps() ), add ) - #else - #define stbir__simdf8_madd( out, add, mul1, mul2 ) (out) = _mm256_add_ps( add, _mm256_mul_ps( mul1, mul2 ) ) - #define stbir__simdf8_madd_mem( out, add, mul, ptr ) (out) = _mm256_add_ps( add, _mm256_mul_ps( mul, _mm256_loadu_ps( (float const*)(ptr) ) ) ) - #define stbir__simdf8_madd_mem4( out, add, mul, ptr ) (out) = _mm256_add_ps( add, _mm256_setr_m128( _mm_mul_ps( mul, _mm_loadu_ps( (float const*)(ptr) ) ), _mm_setzero_ps() ) ) - #endif - #define stbir__if_simdf8_cast_to_simdf4( val ) _mm256_castps256_ps128( val ) - - #endif - - #ifdef STBIR_FLOORF - #undef STBIR_FLOORF - #endif - #define STBIR_FLOORF stbir_simd_floorf - static stbir__inline float stbir_simd_floorf(float x) // martins floorf - { - #if defined(STBIR_AVX) || defined(__SSE4_1__) || defined(STBIR_SSE41) - __m128 t = _mm_set_ss(x); - return _mm_cvtss_f32( _mm_floor_ss(t, t) ); - #else - __m128 f = _mm_set_ss(x); - __m128 t = _mm_cvtepi32_ps(_mm_cvttps_epi32(f)); - __m128 r = _mm_add_ss(t, _mm_and_ps(_mm_cmplt_ss(f, t), _mm_set_ss(-1.0f))); - return _mm_cvtss_f32(r); - #endif - } - - #ifdef STBIR_CEILF - #undef STBIR_CEILF - #endif - #define STBIR_CEILF stbir_simd_ceilf - static stbir__inline float stbir_simd_ceilf(float x) // martins ceilf - { - #if defined(STBIR_AVX) || defined(__SSE4_1__) || defined(STBIR_SSE41) - __m128 t = _mm_set_ss(x); - return _mm_cvtss_f32( _mm_ceil_ss(t, t) ); - #else - __m128 f = _mm_set_ss(x); - __m128 t = _mm_cvtepi32_ps(_mm_cvttps_epi32(f)); - __m128 r = _mm_add_ss(t, _mm_and_ps(_mm_cmplt_ss(t, f), _mm_set_ss(1.0f))); - return _mm_cvtss_f32(r); - #endif - } - -#elif defined(STBIR_NEON) - - #include - - #define stbir__simdf float32x4_t - #define stbir__simdi uint32x4_t - - #define stbir_simdi_castf( reg ) vreinterpretq_u32_f32(reg) - #define stbir_simdf_casti( reg ) vreinterpretq_f32_u32(reg) - - #define stbir__simdf_load( reg, ptr ) (reg) = vld1q_f32( (float const*)(ptr) ) - #define stbir__simdi_load( reg, ptr ) (reg) = vld1q_u32( (uint32_t const*)(ptr) ) - #define stbir__simdf_load1( out, ptr ) (out) = vld1q_dup_f32( (float const*)(ptr) ) // top values can be random (not denormal or nan for perf) - #define stbir__simdi_load1( out, ptr ) (out) = vld1q_dup_u32( (uint32_t const*)(ptr) ) - #define stbir__simdf_load1z( out, ptr ) (out) = vld1q_lane_f32( (float const*)(ptr), vdupq_n_f32(0), 0 ) // top values must be zero - #define stbir__simdf_frep4( fvar ) vdupq_n_f32( fvar ) - #define stbir__simdf_load1frep4( out, fvar ) (out) = vdupq_n_f32( fvar ) - #define stbir__simdf_load2( out, ptr ) (out) = vcombine_f32( vld1_f32( (float const*)(ptr) ), vcreate_f32(0) ) // top values can be random (not denormal or nan for perf) - #define stbir__simdf_load2z( out, ptr ) (out) = vcombine_f32( vld1_f32( (float const*)(ptr) ), vcreate_f32(0) ) // top values must be zero - #define stbir__simdf_load2hmerge( out, reg, ptr ) (out) = vcombine_f32( vget_low_f32(reg), vld1_f32( (float const*)(ptr) ) ) - - #define stbir__simdf_zeroP() vdupq_n_f32(0) - #define stbir__simdf_zero( reg ) (reg) = vdupq_n_f32(0) - - #define stbir__simdf_store( ptr, reg ) vst1q_f32( (float*)(ptr), reg ) - #define stbir__simdf_store1( ptr, reg ) vst1q_lane_f32( (float*)(ptr), reg, 0) - #define stbir__simdf_store2( ptr, reg ) vst1_f32( (float*)(ptr), vget_low_f32(reg) ) - #define stbir__simdf_store2h( ptr, reg ) vst1_f32( (float*)(ptr), vget_high_f32(reg) ) - - #define stbir__simdi_store( ptr, reg ) vst1q_u32( (uint32_t*)(ptr), reg ) - #define stbir__simdi_store1( ptr, reg ) vst1q_lane_u32( (uint32_t*)(ptr), reg, 0 ) - #define stbir__simdi_store2( ptr, reg ) vst1_u32( (uint32_t*)(ptr), vget_low_u32(reg) ) - - #define stbir__prefetch( ptr ) - - #define stbir__simdi_expand_u8_to_u32(out0,out1,out2,out3,ireg) \ - { \ - uint16x8_t l = vmovl_u8( vget_low_u8 ( vreinterpretq_u8_u32(ireg) ) ); \ - uint16x8_t h = vmovl_u8( vget_high_u8( vreinterpretq_u8_u32(ireg) ) ); \ - out0 = vmovl_u16( vget_low_u16 ( l ) ); \ - out1 = vmovl_u16( vget_high_u16( l ) ); \ - out2 = vmovl_u16( vget_low_u16 ( h ) ); \ - out3 = vmovl_u16( vget_high_u16( h ) ); \ - } - - #define stbir__simdi_expand_u8_to_1u32(out,ireg) \ - { \ - uint16x8_t tmp = vmovl_u8( vget_low_u8( vreinterpretq_u8_u32(ireg) ) ); \ - out = vmovl_u16( vget_low_u16( tmp ) ); \ - } - - #define stbir__simdi_expand_u16_to_u32(out0,out1,ireg) \ - { \ - uint16x8_t tmp = vreinterpretq_u16_u32(ireg); \ - out0 = vmovl_u16( vget_low_u16 ( tmp ) ); \ - out1 = vmovl_u16( vget_high_u16( tmp ) ); \ - } - - #define stbir__simdf_convert_float_to_i32( i, f ) (i) = vreinterpretq_u32_s32( vcvtq_s32_f32(f) ) - #define stbir__simdf_convert_float_to_int( f ) vgetq_lane_s32(vcvtq_s32_f32(f), 0) - #define stbir__simdi_to_int( i ) (int)vgetq_lane_u32(i, 0) - #define stbir__simdf_convert_float_to_uint8( f ) ((unsigned char)vgetq_lane_s32(vcvtq_s32_f32(vmaxq_f32(vminq_f32(f,STBIR__CONSTF(STBIR_max_uint8_as_float)),vdupq_n_f32(0))), 0)) - #define stbir__simdf_convert_float_to_short( f ) ((unsigned short)vgetq_lane_s32(vcvtq_s32_f32(vmaxq_f32(vminq_f32(f,STBIR__CONSTF(STBIR_max_uint16_as_float)),vdupq_n_f32(0))), 0)) - #define stbir__simdi_convert_i32_to_float(out, ireg) (out) = vcvtq_f32_s32( vreinterpretq_s32_u32(ireg) ) - #define stbir__simdf_add( out, reg0, reg1 ) (out) = vaddq_f32( reg0, reg1 ) - #define stbir__simdf_mult( out, reg0, reg1 ) (out) = vmulq_f32( reg0, reg1 ) - #define stbir__simdf_mult_mem( out, reg, ptr ) (out) = vmulq_f32( reg, vld1q_f32( (float const*)(ptr) ) ) - #define stbir__simdf_mult1_mem( out, reg, ptr ) (out) = vmulq_f32( reg, vld1q_dup_f32( (float const*)(ptr) ) ) - #define stbir__simdf_add_mem( out, reg, ptr ) (out) = vaddq_f32( reg, vld1q_f32( (float const*)(ptr) ) ) - #define stbir__simdf_add1_mem( out, reg, ptr ) (out) = vaddq_f32( reg, vld1q_dup_f32( (float const*)(ptr) ) ) - - #ifdef STBIR_USE_FMA // not on by default to maintain bit identical simd to non-simd (and also x64 no madd to arm madd) - #define stbir__simdf_madd( out, add, mul1, mul2 ) (out) = vfmaq_f32( add, mul1, mul2 ) - #define stbir__simdf_madd1( out, add, mul1, mul2 ) (out) = vfmaq_f32( add, mul1, mul2 ) - #define stbir__simdf_madd_mem( out, add, mul, ptr ) (out) = vfmaq_f32( add, mul, vld1q_f32( (float const*)(ptr) ) ) - #define stbir__simdf_madd1_mem( out, add, mul, ptr ) (out) = vfmaq_f32( add, mul, vld1q_dup_f32( (float const*)(ptr) ) ) - #else - #define stbir__simdf_madd( out, add, mul1, mul2 ) (out) = vaddq_f32( add, vmulq_f32( mul1, mul2 ) ) - #define stbir__simdf_madd1( out, add, mul1, mul2 ) (out) = vaddq_f32( add, vmulq_f32( mul1, mul2 ) ) - #define stbir__simdf_madd_mem( out, add, mul, ptr ) (out) = vaddq_f32( add, vmulq_f32( mul, vld1q_f32( (float const*)(ptr) ) ) ) - #define stbir__simdf_madd1_mem( out, add, mul, ptr ) (out) = vaddq_f32( add, vmulq_f32( mul, vld1q_dup_f32( (float const*)(ptr) ) ) ) - #endif - - #define stbir__simdf_add1( out, reg0, reg1 ) (out) = vaddq_f32( reg0, reg1 ) - #define stbir__simdf_mult1( out, reg0, reg1 ) (out) = vmulq_f32( reg0, reg1 ) - - #define stbir__simdf_and( out, reg0, reg1 ) (out) = vreinterpretq_f32_u32( vandq_u32( vreinterpretq_u32_f32(reg0), vreinterpretq_u32_f32(reg1) ) ) - #define stbir__simdf_or( out, reg0, reg1 ) (out) = vreinterpretq_f32_u32( vorrq_u32( vreinterpretq_u32_f32(reg0), vreinterpretq_u32_f32(reg1) ) ) - - #define stbir__simdf_min( out, reg0, reg1 ) (out) = vminq_f32( reg0, reg1 ) - #define stbir__simdf_max( out, reg0, reg1 ) (out) = vmaxq_f32( reg0, reg1 ) - #define stbir__simdf_min1( out, reg0, reg1 ) (out) = vminq_f32( reg0, reg1 ) - #define stbir__simdf_max1( out, reg0, reg1 ) (out) = vmaxq_f32( reg0, reg1 ) - - #define stbir__simdf_0123ABCDto3ABx( out, reg0, reg1 ) (out) = vextq_f32( reg0, reg1, 3 ) - #define stbir__simdf_0123ABCDto23Ax( out, reg0, reg1 ) (out) = vextq_f32( reg0, reg1, 2 ) - - #define stbir__simdf_a1a1( out, alp, ones ) (out) = vzipq_f32(vuzpq_f32(alp, alp).val[1], ones).val[0] - #define stbir__simdf_1a1a( out, alp, ones ) (out) = vzipq_f32(ones, vuzpq_f32(alp, alp).val[0]).val[0] - - #if defined( _M_ARM64 ) || defined( __aarch64__ ) || defined( __arm64__ ) - - #define stbir__simdf_aaa1( out, alp, ones ) (out) = vcopyq_laneq_f32(vdupq_n_f32(vgetq_lane_f32(alp, 3)), 3, ones, 3) - #define stbir__simdf_1aaa( out, alp, ones ) (out) = vcopyq_laneq_f32(vdupq_n_f32(vgetq_lane_f32(alp, 0)), 0, ones, 0) - - #if defined( _MSC_VER ) && !defined(__clang__) - #define stbir_make16(a,b,c,d) vcombine_u8( \ - vcreate_u8( (4*a+0) | ((4*a+1)<<8) | ((4*a+2)<<16) | ((4*a+3)<<24) | \ - ((stbir_uint64)(4*b+0)<<32) | ((stbir_uint64)(4*b+1)<<40) | ((stbir_uint64)(4*b+2)<<48) | ((stbir_uint64)(4*b+3)<<56)), \ - vcreate_u8( (4*c+0) | ((4*c+1)<<8) | ((4*c+2)<<16) | ((4*c+3)<<24) | \ - ((stbir_uint64)(4*d+0)<<32) | ((stbir_uint64)(4*d+1)<<40) | ((stbir_uint64)(4*d+2)<<48) | ((stbir_uint64)(4*d+3)<<56) ) ) - - static stbir__inline uint8x16x2_t stbir_make16x2(float32x4_t rega,float32x4_t regb) - { - uint8x16x2_t r = { vreinterpretq_u8_f32(rega), vreinterpretq_u8_f32(regb) }; - return r; - } - #else - #define stbir_make16(a,b,c,d) (uint8x16_t){4*a+0,4*a+1,4*a+2,4*a+3,4*b+0,4*b+1,4*b+2,4*b+3,4*c+0,4*c+1,4*c+2,4*c+3,4*d+0,4*d+1,4*d+2,4*d+3} - #define stbir_make16x2(a,b) (uint8x16x2_t){{vreinterpretq_u8_f32(a),vreinterpretq_u8_f32(b)}} - #endif - - #define stbir__simdf_swiz( reg, one, two, three, four ) vreinterpretq_f32_u8( vqtbl1q_u8( vreinterpretq_u8_f32(reg), stbir_make16(one, two, three, four) ) ) - #define stbir__simdf_swiz2( rega, regb, one, two, three, four ) vreinterpretq_f32_u8( vqtbl2q_u8( stbir_make16x2(rega,regb), stbir_make16(one, two, three, four) ) ) - - #define stbir__simdi_16madd( out, reg0, reg1 ) \ - { \ - int16x8_t r0 = vreinterpretq_s16_u32(reg0); \ - int16x8_t r1 = vreinterpretq_s16_u32(reg1); \ - int32x4_t tmp0 = vmull_s16( vget_low_s16(r0), vget_low_s16(r1) ); \ - int32x4_t tmp1 = vmull_s16( vget_high_s16(r0), vget_high_s16(r1) ); \ - (out) = vreinterpretq_u32_s32( vpaddq_s32(tmp0, tmp1) ); \ - } - - #else - - #define stbir__simdf_aaa1( out, alp, ones ) (out) = vsetq_lane_f32(1.0f, vdupq_n_f32(vgetq_lane_f32(alp, 3)), 3) - #define stbir__simdf_1aaa( out, alp, ones ) (out) = vsetq_lane_f32(1.0f, vdupq_n_f32(vgetq_lane_f32(alp, 0)), 0) - - #if defined( _MSC_VER ) && !defined(__clang__) - static stbir__inline uint8x8x2_t stbir_make8x2(float32x4_t reg) - { - uint8x8x2_t r = { { vget_low_u8(vreinterpretq_u8_f32(reg)), vget_high_u8(vreinterpretq_u8_f32(reg)) } }; - return r; - } - #define stbir_make8(a,b) vcreate_u8( \ - (4*a+0) | ((4*a+1)<<8) | ((4*a+2)<<16) | ((4*a+3)<<24) | \ - ((stbir_uint64)(4*b+0)<<32) | ((stbir_uint64)(4*b+1)<<40) | ((stbir_uint64)(4*b+2)<<48) | ((stbir_uint64)(4*b+3)<<56) ) - #else - #define stbir_make8x2(reg) (uint8x8x2_t){ { vget_low_u8(vreinterpretq_u8_f32(reg)), vget_high_u8(vreinterpretq_u8_f32(reg)) } } - #define stbir_make8(a,b) (uint8x8_t){4*a+0,4*a+1,4*a+2,4*a+3,4*b+0,4*b+1,4*b+2,4*b+3} - #endif - - #define stbir__simdf_swiz( reg, one, two, three, four ) vreinterpretq_f32_u8( vcombine_u8( \ - vtbl2_u8( stbir_make8x2( reg ), stbir_make8( one, two ) ), \ - vtbl2_u8( stbir_make8x2( reg ), stbir_make8( three, four ) ) ) ) - - #define stbir__simdi_16madd( out, reg0, reg1 ) \ - { \ - int16x8_t r0 = vreinterpretq_s16_u32(reg0); \ - int16x8_t r1 = vreinterpretq_s16_u32(reg1); \ - int32x4_t tmp0 = vmull_s16( vget_low_s16(r0), vget_low_s16(r1) ); \ - int32x4_t tmp1 = vmull_s16( vget_high_s16(r0), vget_high_s16(r1) ); \ - int32x2_t out0 = vpadd_s32( vget_low_s32(tmp0), vget_high_s32(tmp0) ); \ - int32x2_t out1 = vpadd_s32( vget_low_s32(tmp1), vget_high_s32(tmp1) ); \ - (out) = vreinterpretq_u32_s32( vcombine_s32(out0, out1) ); \ - } - - #endif - - #define stbir__simdi_and( out, reg0, reg1 ) (out) = vandq_u32( reg0, reg1 ) - #define stbir__simdi_or( out, reg0, reg1 ) (out) = vorrq_u32( reg0, reg1 ) - - #define stbir__simdf_pack_to_8bytes(out,aa,bb) \ - { \ - float32x4_t af = vmaxq_f32( vminq_f32(aa,STBIR__CONSTF(STBIR_max_uint8_as_float) ), vdupq_n_f32(0) ); \ - float32x4_t bf = vmaxq_f32( vminq_f32(bb,STBIR__CONSTF(STBIR_max_uint8_as_float) ), vdupq_n_f32(0) ); \ - int16x4_t ai = vqmovn_s32( vcvtq_s32_f32( af ) ); \ - int16x4_t bi = vqmovn_s32( vcvtq_s32_f32( bf ) ); \ - uint8x8_t out8 = vqmovun_s16( vcombine_s16(ai, bi) ); \ - out = vreinterpretq_u32_u8( vcombine_u8(out8, out8) ); \ - } - - #define stbir__simdf_pack_to_8words(out,aa,bb) \ - { \ - float32x4_t af = vmaxq_f32( vminq_f32(aa,STBIR__CONSTF(STBIR_max_uint16_as_float) ), vdupq_n_f32(0) ); \ - float32x4_t bf = vmaxq_f32( vminq_f32(bb,STBIR__CONSTF(STBIR_max_uint16_as_float) ), vdupq_n_f32(0) ); \ - int32x4_t ai = vcvtq_s32_f32( af ); \ - int32x4_t bi = vcvtq_s32_f32( bf ); \ - out = vreinterpretq_u32_u16( vcombine_u16(vqmovun_s32(ai), vqmovun_s32(bi)) ); \ - } - - #define stbir__interleave_pack_and_store_16_u8( ptr, r0, r1, r2, r3 ) \ - { \ - int16x4x2_t tmp0 = vzip_s16( vqmovn_s32(vreinterpretq_s32_u32(r0)), vqmovn_s32(vreinterpretq_s32_u32(r2)) ); \ - int16x4x2_t tmp1 = vzip_s16( vqmovn_s32(vreinterpretq_s32_u32(r1)), vqmovn_s32(vreinterpretq_s32_u32(r3)) ); \ - uint8x8x2_t out = \ - { { \ - vqmovun_s16( vcombine_s16(tmp0.val[0], tmp0.val[1]) ), \ - vqmovun_s16( vcombine_s16(tmp1.val[0], tmp1.val[1]) ), \ - } }; \ - vst2_u8(ptr, out); \ - } - - #define stbir__simdf_load4_transposed( o0, o1, o2, o3, ptr ) \ - { \ - float32x4x4_t tmp = vld4q_f32(ptr); \ - o0 = tmp.val[0]; \ - o1 = tmp.val[1]; \ - o2 = tmp.val[2]; \ - o3 = tmp.val[3]; \ - } - - #define stbir__simdi_32shr( out, reg, imm ) out = vshrq_n_u32( reg, imm ) - - #if defined( _MSC_VER ) && !defined(__clang__) - #define STBIR__SIMDF_CONST(var, x) __declspec(align(8)) float var[] = { x, x, x, x } - #define STBIR__SIMDI_CONST(var, x) __declspec(align(8)) uint32_t var[] = { x, x, x, x } - #define STBIR__CONSTF(var) (*(const float32x4_t*)var) - #define STBIR__CONSTI(var) (*(const uint32x4_t*)var) - #else - #define STBIR__SIMDF_CONST(var, x) stbir__simdf var = { x, x, x, x } - #define STBIR__SIMDI_CONST(var, x) stbir__simdi var = { x, x, x, x } - #define STBIR__CONSTF(var) (var) - #define STBIR__CONSTI(var) (var) - #endif - - #ifdef STBIR_FLOORF - #undef STBIR_FLOORF - #endif - #define STBIR_FLOORF stbir_simd_floorf - static stbir__inline float stbir_simd_floorf(float x) - { - #if defined( _M_ARM64 ) || defined( __aarch64__ ) || defined( __arm64__ ) - return vget_lane_f32( vrndm_f32( vdup_n_f32(x) ), 0); - #else - float32x2_t f = vdup_n_f32(x); - float32x2_t t = vcvt_f32_s32(vcvt_s32_f32(f)); - uint32x2_t a = vclt_f32(f, t); - uint32x2_t b = vreinterpret_u32_f32(vdup_n_f32(-1.0f)); - float32x2_t r = vadd_f32(t, vreinterpret_f32_u32(vand_u32(a, b))); - return vget_lane_f32(r, 0); - #endif - } - - #ifdef STBIR_CEILF - #undef STBIR_CEILF - #endif - #define STBIR_CEILF stbir_simd_ceilf - static stbir__inline float stbir_simd_ceilf(float x) - { - #if defined( _M_ARM64 ) || defined( __aarch64__ ) || defined( __arm64__ ) - return vget_lane_f32( vrndp_f32( vdup_n_f32(x) ), 0); - #else - float32x2_t f = vdup_n_f32(x); - float32x2_t t = vcvt_f32_s32(vcvt_s32_f32(f)); - uint32x2_t a = vclt_f32(t, f); - uint32x2_t b = vreinterpret_u32_f32(vdup_n_f32(1.0f)); - float32x2_t r = vadd_f32(t, vreinterpret_f32_u32(vand_u32(a, b))); - return vget_lane_f32(r, 0); - #endif - } - - #define STBIR_SIMD - -#elif defined(STBIR_WASM) - - #include - - #define stbir__simdf v128_t - #define stbir__simdi v128_t - - #define stbir_simdi_castf( reg ) (reg) - #define stbir_simdf_casti( reg ) (reg) - - #define stbir__simdf_load( reg, ptr ) (reg) = wasm_v128_load( (void const*)(ptr) ) - #define stbir__simdi_load( reg, ptr ) (reg) = wasm_v128_load( (void const*)(ptr) ) - #define stbir__simdf_load1( out, ptr ) (out) = wasm_v128_load32_splat( (void const*)(ptr) ) // top values can be random (not denormal or nan for perf) - #define stbir__simdi_load1( out, ptr ) (out) = wasm_v128_load32_splat( (void const*)(ptr) ) - #define stbir__simdf_load1z( out, ptr ) (out) = wasm_v128_load32_zero( (void const*)(ptr) ) // top values must be zero - #define stbir__simdf_frep4( fvar ) wasm_f32x4_splat( fvar ) - #define stbir__simdf_load1frep4( out, fvar ) (out) = wasm_f32x4_splat( fvar ) - #define stbir__simdf_load2( out, ptr ) (out) = wasm_v128_load64_splat( (void const*)(ptr) ) // top values can be random (not denormal or nan for perf) - #define stbir__simdf_load2z( out, ptr ) (out) = wasm_v128_load64_zero( (void const*)(ptr) ) // top values must be zero - #define stbir__simdf_load2hmerge( out, reg, ptr ) (out) = wasm_v128_load64_lane( (void const*)(ptr), reg, 1 ) - - #define stbir__simdf_zeroP() wasm_f32x4_const_splat(0) - #define stbir__simdf_zero( reg ) (reg) = wasm_f32x4_const_splat(0) - - #define stbir__simdf_store( ptr, reg ) wasm_v128_store( (void*)(ptr), reg ) - #define stbir__simdf_store1( ptr, reg ) wasm_v128_store32_lane( (void*)(ptr), reg, 0 ) - #define stbir__simdf_store2( ptr, reg ) wasm_v128_store64_lane( (void*)(ptr), reg, 0 ) - #define stbir__simdf_store2h( ptr, reg ) wasm_v128_store64_lane( (void*)(ptr), reg, 1 ) - - #define stbir__simdi_store( ptr, reg ) wasm_v128_store( (void*)(ptr), reg ) - #define stbir__simdi_store1( ptr, reg ) wasm_v128_store32_lane( (void*)(ptr), reg, 0 ) - #define stbir__simdi_store2( ptr, reg ) wasm_v128_store64_lane( (void*)(ptr), reg, 0 ) - - #define stbir__prefetch( ptr ) - - #define stbir__simdi_expand_u8_to_u32(out0,out1,out2,out3,ireg) \ - { \ - v128_t l = wasm_u16x8_extend_low_u8x16 ( ireg ); \ - v128_t h = wasm_u16x8_extend_high_u8x16( ireg ); \ - out0 = wasm_u32x4_extend_low_u16x8 ( l ); \ - out1 = wasm_u32x4_extend_high_u16x8( l ); \ - out2 = wasm_u32x4_extend_low_u16x8 ( h ); \ - out3 = wasm_u32x4_extend_high_u16x8( h ); \ - } - - #define stbir__simdi_expand_u8_to_1u32(out,ireg) \ - { \ - v128_t tmp = wasm_u16x8_extend_low_u8x16(ireg); \ - out = wasm_u32x4_extend_low_u16x8(tmp); \ - } - - #define stbir__simdi_expand_u16_to_u32(out0,out1,ireg) \ - { \ - out0 = wasm_u32x4_extend_low_u16x8 ( ireg ); \ - out1 = wasm_u32x4_extend_high_u16x8( ireg ); \ - } - - #define stbir__simdf_convert_float_to_i32( i, f ) (i) = wasm_i32x4_trunc_sat_f32x4(f) - #define stbir__simdf_convert_float_to_int( f ) wasm_i32x4_extract_lane(wasm_i32x4_trunc_sat_f32x4(f), 0) - #define stbir__simdi_to_int( i ) wasm_i32x4_extract_lane(i, 0) - #define stbir__simdf_convert_float_to_uint8( f ) ((unsigned char)wasm_i32x4_extract_lane(wasm_i32x4_trunc_sat_f32x4(wasm_f32x4_max(wasm_f32x4_min(f,STBIR_max_uint8_as_float),wasm_f32x4_const_splat(0))), 0)) - #define stbir__simdf_convert_float_to_short( f ) ((unsigned short)wasm_i32x4_extract_lane(wasm_i32x4_trunc_sat_f32x4(wasm_f32x4_max(wasm_f32x4_min(f,STBIR_max_uint16_as_float),wasm_f32x4_const_splat(0))), 0)) - #define stbir__simdi_convert_i32_to_float(out, ireg) (out) = wasm_f32x4_convert_i32x4(ireg) - #define stbir__simdf_add( out, reg0, reg1 ) (out) = wasm_f32x4_add( reg0, reg1 ) - #define stbir__simdf_mult( out, reg0, reg1 ) (out) = wasm_f32x4_mul( reg0, reg1 ) - #define stbir__simdf_mult_mem( out, reg, ptr ) (out) = wasm_f32x4_mul( reg, wasm_v128_load( (void const*)(ptr) ) ) - #define stbir__simdf_mult1_mem( out, reg, ptr ) (out) = wasm_f32x4_mul( reg, wasm_v128_load32_splat( (void const*)(ptr) ) ) - #define stbir__simdf_add_mem( out, reg, ptr ) (out) = wasm_f32x4_add( reg, wasm_v128_load( (void const*)(ptr) ) ) - #define stbir__simdf_add1_mem( out, reg, ptr ) (out) = wasm_f32x4_add( reg, wasm_v128_load32_splat( (void const*)(ptr) ) ) - - #define stbir__simdf_madd( out, add, mul1, mul2 ) (out) = wasm_f32x4_add( add, wasm_f32x4_mul( mul1, mul2 ) ) - #define stbir__simdf_madd1( out, add, mul1, mul2 ) (out) = wasm_f32x4_add( add, wasm_f32x4_mul( mul1, mul2 ) ) - #define stbir__simdf_madd_mem( out, add, mul, ptr ) (out) = wasm_f32x4_add( add, wasm_f32x4_mul( mul, wasm_v128_load( (void const*)(ptr) ) ) ) - #define stbir__simdf_madd1_mem( out, add, mul, ptr ) (out) = wasm_f32x4_add( add, wasm_f32x4_mul( mul, wasm_v128_load32_splat( (void const*)(ptr) ) ) ) - - #define stbir__simdf_add1( out, reg0, reg1 ) (out) = wasm_f32x4_add( reg0, reg1 ) - #define stbir__simdf_mult1( out, reg0, reg1 ) (out) = wasm_f32x4_mul( reg0, reg1 ) - - #define stbir__simdf_and( out, reg0, reg1 ) (out) = wasm_v128_and( reg0, reg1 ) - #define stbir__simdf_or( out, reg0, reg1 ) (out) = wasm_v128_or( reg0, reg1 ) - - #define stbir__simdf_min( out, reg0, reg1 ) (out) = wasm_f32x4_min( reg0, reg1 ) - #define stbir__simdf_max( out, reg0, reg1 ) (out) = wasm_f32x4_max( reg0, reg1 ) - #define stbir__simdf_min1( out, reg0, reg1 ) (out) = wasm_f32x4_min( reg0, reg1 ) - #define stbir__simdf_max1( out, reg0, reg1 ) (out) = wasm_f32x4_max( reg0, reg1 ) - - #define stbir__simdf_0123ABCDto3ABx( out, reg0, reg1 ) (out) = wasm_i32x4_shuffle( reg0, reg1, 3, 4, 5, -1 ) - #define stbir__simdf_0123ABCDto23Ax( out, reg0, reg1 ) (out) = wasm_i32x4_shuffle( reg0, reg1, 2, 3, 4, -1 ) - - #define stbir__simdf_aaa1(out,alp,ones) (out) = wasm_i32x4_shuffle(alp, ones, 3, 3, 3, 4) - #define stbir__simdf_1aaa(out,alp,ones) (out) = wasm_i32x4_shuffle(alp, ones, 4, 0, 0, 0) - #define stbir__simdf_a1a1(out,alp,ones) (out) = wasm_i32x4_shuffle(alp, ones, 1, 4, 3, 4) - #define stbir__simdf_1a1a(out,alp,ones) (out) = wasm_i32x4_shuffle(alp, ones, 4, 0, 4, 2) - - #define stbir__simdf_swiz( reg, one, two, three, four ) wasm_i32x4_shuffle(reg, reg, one, two, three, four) - - #define stbir__simdi_and( out, reg0, reg1 ) (out) = wasm_v128_and( reg0, reg1 ) - #define stbir__simdi_or( out, reg0, reg1 ) (out) = wasm_v128_or( reg0, reg1 ) - #define stbir__simdi_16madd( out, reg0, reg1 ) (out) = wasm_i32x4_dot_i16x8( reg0, reg1 ) - - #define stbir__simdf_pack_to_8bytes(out,aa,bb) \ - { \ - v128_t af = wasm_f32x4_max( wasm_f32x4_min(aa, STBIR_max_uint8_as_float), wasm_f32x4_const_splat(0) ); \ - v128_t bf = wasm_f32x4_max( wasm_f32x4_min(bb, STBIR_max_uint8_as_float), wasm_f32x4_const_splat(0) ); \ - v128_t ai = wasm_i32x4_trunc_sat_f32x4( af ); \ - v128_t bi = wasm_i32x4_trunc_sat_f32x4( bf ); \ - v128_t out16 = wasm_i16x8_narrow_i32x4( ai, bi ); \ - out = wasm_u8x16_narrow_i16x8( out16, out16 ); \ - } - - #define stbir__simdf_pack_to_8words(out,aa,bb) \ - { \ - v128_t af = wasm_f32x4_max( wasm_f32x4_min(aa, STBIR_max_uint16_as_float), wasm_f32x4_const_splat(0)); \ - v128_t bf = wasm_f32x4_max( wasm_f32x4_min(bb, STBIR_max_uint16_as_float), wasm_f32x4_const_splat(0)); \ - v128_t ai = wasm_i32x4_trunc_sat_f32x4( af ); \ - v128_t bi = wasm_i32x4_trunc_sat_f32x4( bf ); \ - out = wasm_u16x8_narrow_i32x4( ai, bi ); \ - } - - #define stbir__interleave_pack_and_store_16_u8( ptr, r0, r1, r2, r3 ) \ - { \ - v128_t tmp0 = wasm_i16x8_narrow_i32x4(r0, r1); \ - v128_t tmp1 = wasm_i16x8_narrow_i32x4(r2, r3); \ - v128_t tmp = wasm_u8x16_narrow_i16x8(tmp0, tmp1); \ - tmp = wasm_i8x16_shuffle(tmp, tmp, 0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15); \ - wasm_v128_store( (void*)(ptr), tmp); \ - } - - #define stbir__simdf_load4_transposed( o0, o1, o2, o3, ptr ) \ - { \ - v128_t t0 = wasm_v128_load( ptr ); \ - v128_t t1 = wasm_v128_load( ptr+4 ); \ - v128_t t2 = wasm_v128_load( ptr+8 ); \ - v128_t t3 = wasm_v128_load( ptr+12 ); \ - v128_t s0 = wasm_i32x4_shuffle(t0, t1, 0, 4, 2, 6); \ - v128_t s1 = wasm_i32x4_shuffle(t0, t1, 1, 5, 3, 7); \ - v128_t s2 = wasm_i32x4_shuffle(t2, t3, 0, 4, 2, 6); \ - v128_t s3 = wasm_i32x4_shuffle(t2, t3, 1, 5, 3, 7); \ - o0 = wasm_i32x4_shuffle(s0, s2, 0, 1, 4, 5); \ - o1 = wasm_i32x4_shuffle(s1, s3, 0, 1, 4, 5); \ - o2 = wasm_i32x4_shuffle(s0, s2, 2, 3, 6, 7); \ - o3 = wasm_i32x4_shuffle(s1, s3, 2, 3, 6, 7); \ - } - - #define stbir__simdi_32shr( out, reg, imm ) out = wasm_u32x4_shr( reg, imm ) - - typedef float stbir__f32x4 __attribute__((__vector_size__(16), __aligned__(16))); - #define STBIR__SIMDF_CONST(var, x) stbir__simdf var = (v128_t)(stbir__f32x4){ x, x, x, x } - #define STBIR__SIMDI_CONST(var, x) stbir__simdi var = { x, x, x, x } - #define STBIR__CONSTF(var) (var) - #define STBIR__CONSTI(var) (var) - - #ifdef STBIR_FLOORF - #undef STBIR_FLOORF - #endif - #define STBIR_FLOORF stbir_simd_floorf - static stbir__inline float stbir_simd_floorf(float x) - { - return wasm_f32x4_extract_lane( wasm_f32x4_floor( wasm_f32x4_splat(x) ), 0); - } - - #ifdef STBIR_CEILF - #undef STBIR_CEILF - #endif - #define STBIR_CEILF stbir_simd_ceilf - static stbir__inline float stbir_simd_ceilf(float x) - { - return wasm_f32x4_extract_lane( wasm_f32x4_ceil( wasm_f32x4_splat(x) ), 0); - } - - #define STBIR_SIMD - -#endif // SSE2/NEON/WASM - -#endif // NO SIMD - -#ifdef STBIR_SIMD8 - #define stbir__simdfX stbir__simdf8 - #define stbir__simdiX stbir__simdi8 - #define stbir__simdfX_load stbir__simdf8_load - #define stbir__simdiX_load stbir__simdi8_load - #define stbir__simdfX_mult stbir__simdf8_mult - #define stbir__simdfX_add_mem stbir__simdf8_add_mem - #define stbir__simdfX_madd_mem stbir__simdf8_madd_mem - #define stbir__simdfX_store stbir__simdf8_store - #define stbir__simdiX_store stbir__simdi8_store - #define stbir__simdf_frepX stbir__simdf8_frep8 - #define stbir__simdfX_madd stbir__simdf8_madd - #define stbir__simdfX_min stbir__simdf8_min - #define stbir__simdfX_max stbir__simdf8_max - #define stbir__simdfX_aaa1 stbir__simdf8_aaa1 - #define stbir__simdfX_1aaa stbir__simdf8_1aaa - #define stbir__simdfX_a1a1 stbir__simdf8_a1a1 - #define stbir__simdfX_1a1a stbir__simdf8_1a1a - #define stbir__simdfX_convert_float_to_i32 stbir__simdf8_convert_float_to_i32 - #define stbir__simdfX_pack_to_words stbir__simdf8_pack_to_16words - #define stbir__simdfX_zero stbir__simdf8_zero - #define STBIR_onesX STBIR_ones8 - #define STBIR_max_uint8_as_floatX STBIR_max_uint8_as_float8 - #define STBIR_max_uint16_as_floatX STBIR_max_uint16_as_float8 - #define STBIR_simd_point5X STBIR_simd_point58 - #define stbir__simdfX_float_count 8 - #define stbir__simdfX_0123to1230 stbir__simdf8_0123to12301230 - #define stbir__simdfX_0123to2103 stbir__simdf8_0123to21032103 - static const stbir__simdf8 STBIR_max_uint16_as_float_inverted8 = { stbir__max_uint16_as_float_inverted,stbir__max_uint16_as_float_inverted,stbir__max_uint16_as_float_inverted,stbir__max_uint16_as_float_inverted,stbir__max_uint16_as_float_inverted,stbir__max_uint16_as_float_inverted,stbir__max_uint16_as_float_inverted,stbir__max_uint16_as_float_inverted }; - static const stbir__simdf8 STBIR_max_uint8_as_float_inverted8 = { stbir__max_uint8_as_float_inverted,stbir__max_uint8_as_float_inverted,stbir__max_uint8_as_float_inverted,stbir__max_uint8_as_float_inverted,stbir__max_uint8_as_float_inverted,stbir__max_uint8_as_float_inverted,stbir__max_uint8_as_float_inverted,stbir__max_uint8_as_float_inverted }; - static const stbir__simdf8 STBIR_ones8 = { 1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0 }; - static const stbir__simdf8 STBIR_simd_point58 = { 0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5 }; - static const stbir__simdf8 STBIR_max_uint8_as_float8 = { stbir__max_uint8_as_float,stbir__max_uint8_as_float,stbir__max_uint8_as_float,stbir__max_uint8_as_float, stbir__max_uint8_as_float,stbir__max_uint8_as_float,stbir__max_uint8_as_float,stbir__max_uint8_as_float }; - static const stbir__simdf8 STBIR_max_uint16_as_float8 = { stbir__max_uint16_as_float,stbir__max_uint16_as_float,stbir__max_uint16_as_float,stbir__max_uint16_as_float, stbir__max_uint16_as_float,stbir__max_uint16_as_float,stbir__max_uint16_as_float,stbir__max_uint16_as_float }; -#else - #define stbir__simdfX stbir__simdf - #define stbir__simdiX stbir__simdi - #define stbir__simdfX_load stbir__simdf_load - #define stbir__simdiX_load stbir__simdi_load - #define stbir__simdfX_mult stbir__simdf_mult - #define stbir__simdfX_add_mem stbir__simdf_add_mem - #define stbir__simdfX_madd_mem stbir__simdf_madd_mem - #define stbir__simdfX_store stbir__simdf_store - #define stbir__simdiX_store stbir__simdi_store - #define stbir__simdf_frepX stbir__simdf_frep4 - #define stbir__simdfX_madd stbir__simdf_madd - #define stbir__simdfX_min stbir__simdf_min - #define stbir__simdfX_max stbir__simdf_max - #define stbir__simdfX_aaa1 stbir__simdf_aaa1 - #define stbir__simdfX_1aaa stbir__simdf_1aaa - #define stbir__simdfX_a1a1 stbir__simdf_a1a1 - #define stbir__simdfX_1a1a stbir__simdf_1a1a - #define stbir__simdfX_convert_float_to_i32 stbir__simdf_convert_float_to_i32 - #define stbir__simdfX_pack_to_words stbir__simdf_pack_to_8words - #define stbir__simdfX_zero stbir__simdf_zero - #define STBIR_onesX STBIR__CONSTF(STBIR_ones) - #define STBIR_simd_point5X STBIR__CONSTF(STBIR_simd_point5) - #define STBIR_max_uint8_as_floatX STBIR__CONSTF(STBIR_max_uint8_as_float) - #define STBIR_max_uint16_as_floatX STBIR__CONSTF(STBIR_max_uint16_as_float) - #define stbir__simdfX_float_count 4 - #define stbir__if_simdf8_cast_to_simdf4( val ) ( val ) - #define stbir__simdfX_0123to1230 stbir__simdf_0123to1230 - #define stbir__simdfX_0123to2103 stbir__simdf_0123to2103 -#endif - -#if defined(STBIR_NEON) && !defined(_M_ARM) && !defined(__arm__) - - #if defined( _MSC_VER ) && !defined(__clang__) - typedef __int16 stbir__FP16; - #else - typedef float16_t stbir__FP16; - #endif - -#else // no NEON, or 32-bit ARM for MSVC - - typedef union stbir__FP16 - { - unsigned short u; - } stbir__FP16; - -#endif - -#if (!defined(STBIR_NEON) && !defined(STBIR_FP16C)) || (defined(STBIR_NEON) && defined(_M_ARM)) || (defined(STBIR_NEON) && defined(__arm__)) - - static stbir__inline float stbir__half_to_float( stbir__FP16 h ) - { - static const stbir__FP32 magic = { (254 - 15) << 23 }; - static const stbir__FP32 was_infnan = { (127 + 16) << 23 }; - stbir__FP32 o; - - o.u = (h.u & 0x7fff) << 13; // exponent/mantissa bits - o.f *= magic.f; // exponent adjust - if (o.f >= was_infnan.f) // make sure Inf/NaN survive - o.u |= 255 << 23; - o.u |= (h.u & 0x8000) << 16; // sign bit - return o.f; - } - - static stbir__inline stbir__FP16 stbir__float_to_half(float val) - { - stbir__FP32 f32infty = { 255 << 23 }; - stbir__FP32 f16max = { (127 + 16) << 23 }; - stbir__FP32 denorm_magic = { ((127 - 15) + (23 - 10) + 1) << 23 }; - unsigned int sign_mask = 0x80000000u; - stbir__FP16 o = { 0 }; - stbir__FP32 f; - unsigned int sign; - - f.f = val; - sign = f.u & sign_mask; - f.u ^= sign; - - if (f.u >= f16max.u) // result is Inf or NaN (all exponent bits set) - o.u = (f.u > f32infty.u) ? 0x7e00 : 0x7c00; // NaN->qNaN and Inf->Inf - else // (De)normalized number or zero - { - if (f.u < (113 << 23)) // resulting FP16 is subnormal or zero - { - f.f += denorm_magic.f; - o.u = (unsigned short) ( f.u - denorm_magic.u ); - } - else - { - unsigned int mant_odd = (f.u >> 13) & 1; // resulting mantissa is odd - f.u = f.u + ((15u - 127) << 23) + 0xfff; - f.u += mant_odd; - o.u = (unsigned short) ( f.u >> 13 ); - } - } - - o.u |= sign >> 16; - return o; - } - -#endif - -#if defined(STBIR_FP16C) - - #include - - static stbir__inline void stbir__half_to_float_SIMD(float * output, stbir__FP16 const * input) - { - _mm256_storeu_ps( (float*)output, _mm256_cvtph_ps( _mm_loadu_si128( (__m128i const* )input ) ) ); - } - - static stbir__inline void stbir__float_to_half_SIMD(stbir__FP16 * output, float const * input) - { - _mm_storeu_si128( (__m128i*)output, _mm256_cvtps_ph( _mm256_loadu_ps( input ), 0 ) ); - } - - static stbir__inline float stbir__half_to_float( stbir__FP16 h ) - { - return _mm_cvtss_f32( _mm_cvtph_ps( _mm_cvtsi32_si128( (int)h.u ) ) ); - } - - static stbir__inline stbir__FP16 stbir__float_to_half( float f ) - { - stbir__FP16 h; - h.u = (unsigned short) _mm_cvtsi128_si32( _mm_cvtps_ph( _mm_set_ss( f ), 0 ) ); - return h; - } - -#elif defined(STBIR_SSE2) - - stbir__inline static void stbir__half_to_float_SIMD(float * output, void const * input) - { - static const STBIR__SIMDI_CONST(mask_nosign, 0x7fff); - static const STBIR__SIMDI_CONST(smallest_normal, 0x0400); - static const STBIR__SIMDI_CONST(infinity, 0x7c00); - static const STBIR__SIMDI_CONST(expadjust_normal, (127 - 15) << 23); - static const STBIR__SIMDI_CONST(magic_denorm, 113 << 23); - - __m128i i = _mm_loadu_si128 ( (__m128i const*)(input) ); - __m128i h = _mm_unpacklo_epi16 ( i, _mm_setzero_si128() ); - __m128i mnosign = STBIR__CONSTI(mask_nosign); - __m128i eadjust = STBIR__CONSTI(expadjust_normal); - __m128i smallest = STBIR__CONSTI(smallest_normal); - __m128i infty = STBIR__CONSTI(infinity); - __m128i expmant = _mm_and_si128(mnosign, h); - __m128i justsign = _mm_xor_si128(h, expmant); - __m128i b_notinfnan = _mm_cmpgt_epi32(infty, expmant); - __m128i b_isdenorm = _mm_cmpgt_epi32(smallest, expmant); - __m128i shifted = _mm_slli_epi32(expmant, 13); - __m128i adj_infnan = _mm_andnot_si128(b_notinfnan, eadjust); - __m128i adjusted = _mm_add_epi32(eadjust, shifted); - __m128i den1 = _mm_add_epi32(shifted, STBIR__CONSTI(magic_denorm)); - __m128i adjusted2 = _mm_add_epi32(adjusted, adj_infnan); - __m128 den2 = _mm_sub_ps(_mm_castsi128_ps(den1), *(const __m128 *)&magic_denorm); - __m128 adjusted3 = _mm_and_ps(den2, _mm_castsi128_ps(b_isdenorm)); - __m128 adjusted4 = _mm_andnot_ps(_mm_castsi128_ps(b_isdenorm), _mm_castsi128_ps(adjusted2)); - __m128 adjusted5 = _mm_or_ps(adjusted3, adjusted4); - __m128i sign = _mm_slli_epi32(justsign, 16); - __m128 final = _mm_or_ps(adjusted5, _mm_castsi128_ps(sign)); - stbir__simdf_store( output + 0, final ); - - h = _mm_unpackhi_epi16 ( i, _mm_setzero_si128() ); - expmant = _mm_and_si128(mnosign, h); - justsign = _mm_xor_si128(h, expmant); - b_notinfnan = _mm_cmpgt_epi32(infty, expmant); - b_isdenorm = _mm_cmpgt_epi32(smallest, expmant); - shifted = _mm_slli_epi32(expmant, 13); - adj_infnan = _mm_andnot_si128(b_notinfnan, eadjust); - adjusted = _mm_add_epi32(eadjust, shifted); - den1 = _mm_add_epi32(shifted, STBIR__CONSTI(magic_denorm)); - adjusted2 = _mm_add_epi32(adjusted, adj_infnan); - den2 = _mm_sub_ps(_mm_castsi128_ps(den1), *(const __m128 *)&magic_denorm); - adjusted3 = _mm_and_ps(den2, _mm_castsi128_ps(b_isdenorm)); - adjusted4 = _mm_andnot_ps(_mm_castsi128_ps(b_isdenorm), _mm_castsi128_ps(adjusted2)); - adjusted5 = _mm_or_ps(adjusted3, adjusted4); - sign = _mm_slli_epi32(justsign, 16); - final = _mm_or_ps(adjusted5, _mm_castsi128_ps(sign)); - stbir__simdf_store( output + 4, final ); - - } - - stbir__inline static void stbir__float_to_half_SIMD(void * output, float const * input) - { - static const STBIR__SIMDI_CONST(mask_sign, 0x80000000u); - static const STBIR__SIMDI_CONST(c_f16max, (127 + 16) << 23); // all FP32 values >=this round to +inf - static const STBIR__SIMDI_CONST(c_nanbit, 0x200); - static const STBIR__SIMDI_CONST(c_infty_as_fp16, 0x7c00); - static const STBIR__SIMDI_CONST(c_min_normal, (127 - 14) << 23); // smallest FP32 that yields a normalized FP16 - static const STBIR__SIMDI_CONST(c_subnorm_magic, ((127 - 15) + (23 - 10) + 1) << 23); - static const STBIR__SIMDI_CONST(c_normal_bias, 0xfff - ((127 - 15) << 23)); // adjust exponent and add mantissa rounding - - __m128 f = _mm_loadu_ps(input); - __m128 msign = _mm_castsi128_ps(STBIR__CONSTI(mask_sign)); - __m128 justsign = _mm_and_ps(msign, f); - __m128 absf = _mm_xor_ps(f, justsign); - __m128i absf_int = _mm_castps_si128(absf); // the cast is "free" (extra bypass latency, but no thruput hit) - __m128i f16max = STBIR__CONSTI(c_f16max); - __m128 b_isnan = _mm_cmpunord_ps(absf, absf); // is this a NaN? - __m128i b_isregular = _mm_cmpgt_epi32(f16max, absf_int); // (sub)normalized or special? - __m128i nanbit = _mm_and_si128(_mm_castps_si128(b_isnan), STBIR__CONSTI(c_nanbit)); - __m128i inf_or_nan = _mm_or_si128(nanbit, STBIR__CONSTI(c_infty_as_fp16)); // output for specials - - __m128i min_normal = STBIR__CONSTI(c_min_normal); - __m128i b_issub = _mm_cmpgt_epi32(min_normal, absf_int); - - __m128 subnorm1 = _mm_add_ps(absf, _mm_castsi128_ps(STBIR__CONSTI(c_subnorm_magic))); // magic value to round output mantissa - __m128i subnorm2 = _mm_sub_epi32(_mm_castps_si128(subnorm1), STBIR__CONSTI(c_subnorm_magic)); // subtract out bias - - __m128i mantoddbit = _mm_slli_epi32(absf_int, 31 - 13); // shift bit 13 (mantissa LSB) to sign - __m128i mantodd = _mm_srai_epi32(mantoddbit, 31); // -1 if FP16 mantissa odd, else 0 - - __m128i round1 = _mm_add_epi32(absf_int, STBIR__CONSTI(c_normal_bias)); - __m128i round2 = _mm_sub_epi32(round1, mantodd); // if mantissa LSB odd, bias towards rounding up (RTNE) - __m128i normal = _mm_srli_epi32(round2, 13); // rounded result - - __m128i nonspecial = _mm_or_si128(_mm_and_si128(subnorm2, b_issub), _mm_andnot_si128(b_issub, normal)); - - __m128i joined = _mm_or_si128(_mm_and_si128(nonspecial, b_isregular), _mm_andnot_si128(b_isregular, inf_or_nan)); - - __m128i sign_shift = _mm_srai_epi32(_mm_castps_si128(justsign), 16); - __m128i final2, final= _mm_or_si128(joined, sign_shift); - - f = _mm_loadu_ps(input+4); - justsign = _mm_and_ps(msign, f); - absf = _mm_xor_ps(f, justsign); - absf_int = _mm_castps_si128(absf); // the cast is "free" (extra bypass latency, but no thruput hit) - b_isnan = _mm_cmpunord_ps(absf, absf); // is this a NaN? - b_isregular = _mm_cmpgt_epi32(f16max, absf_int); // (sub)normalized or special? - nanbit = _mm_and_si128(_mm_castps_si128(b_isnan), c_nanbit); - inf_or_nan = _mm_or_si128(nanbit, STBIR__CONSTI(c_infty_as_fp16)); // output for specials - - b_issub = _mm_cmpgt_epi32(min_normal, absf_int); - - subnorm1 = _mm_add_ps(absf, _mm_castsi128_ps(STBIR__CONSTI(c_subnorm_magic))); // magic value to round output mantissa - subnorm2 = _mm_sub_epi32(_mm_castps_si128(subnorm1), STBIR__CONSTI(c_subnorm_magic)); // subtract out bias - - mantoddbit = _mm_slli_epi32(absf_int, 31 - 13); // shift bit 13 (mantissa LSB) to sign - mantodd = _mm_srai_epi32(mantoddbit, 31); // -1 if FP16 mantissa odd, else 0 - - round1 = _mm_add_epi32(absf_int, STBIR__CONSTI(c_normal_bias)); - round2 = _mm_sub_epi32(round1, mantodd); // if mantissa LSB odd, bias towards rounding up (RTNE) - normal = _mm_srli_epi32(round2, 13); // rounded result - - nonspecial = _mm_or_si128(_mm_and_si128(subnorm2, b_issub), _mm_andnot_si128(b_issub, normal)); - - joined = _mm_or_si128(_mm_and_si128(nonspecial, b_isregular), _mm_andnot_si128(b_isregular, inf_or_nan)); - - sign_shift = _mm_srai_epi32(_mm_castps_si128(justsign), 16); - final2 = _mm_or_si128(joined, sign_shift); - final = _mm_packs_epi32(final, final2); - stbir__simdi_store( output,final ); - } - -#elif defined(STBIR_NEON) && defined(_MSC_VER) && defined(_M_ARM64) && !defined(__clang__) // 64-bit ARM on MSVC (not clang) - - static stbir__inline void stbir__half_to_float_SIMD(float * output, stbir__FP16 const * input) - { - float16x4_t in0 = vld1_f16(input + 0); - float16x4_t in1 = vld1_f16(input + 4); - vst1q_f32(output + 0, vcvt_f32_f16(in0)); - vst1q_f32(output + 4, vcvt_f32_f16(in1)); - } - - static stbir__inline void stbir__float_to_half_SIMD(stbir__FP16 * output, float const * input) - { - float16x4_t out0 = vcvt_f16_f32(vld1q_f32(input + 0)); - float16x4_t out1 = vcvt_f16_f32(vld1q_f32(input + 4)); - vst1_f16(output+0, out0); - vst1_f16(output+4, out1); - } - - static stbir__inline float stbir__half_to_float( stbir__FP16 h ) - { - return vgetq_lane_f32(vcvt_f32_f16(vld1_dup_f16(&h)), 0); - } - - static stbir__inline stbir__FP16 stbir__float_to_half( float f ) - { - return vget_lane_f16(vcvt_f16_f32(vdupq_n_f32(f)), 0).n16_u16[0]; - } - -#elif defined(STBIR_NEON) && ( defined( _M_ARM64 ) || defined( __aarch64__ ) || defined( __arm64__ ) ) // 64-bit ARM - - static stbir__inline void stbir__half_to_float_SIMD(float * output, stbir__FP16 const * input) - { - float16x8_t in = vld1q_f16(input); - vst1q_f32(output + 0, vcvt_f32_f16(vget_low_f16(in))); - vst1q_f32(output + 4, vcvt_f32_f16(vget_high_f16(in))); - } - - static stbir__inline void stbir__float_to_half_SIMD(stbir__FP16 * output, float const * input) - { - float16x4_t out0 = vcvt_f16_f32(vld1q_f32(input + 0)); - float16x4_t out1 = vcvt_f16_f32(vld1q_f32(input + 4)); - vst1q_f16(output, vcombine_f16(out0, out1)); - } - - static stbir__inline float stbir__half_to_float( stbir__FP16 h ) - { - return vgetq_lane_f32(vcvt_f32_f16(vdup_n_f16(h)), 0); - } - - static stbir__inline stbir__FP16 stbir__float_to_half( float f ) - { - return vget_lane_f16(vcvt_f16_f32(vdupq_n_f32(f)), 0); - } - -#elif defined(STBIR_WASM) || (defined(STBIR_NEON) && (defined(_MSC_VER) || defined(_M_ARM) || defined(__arm__))) // WASM or 32-bit ARM on MSVC/clang - - static stbir__inline void stbir__half_to_float_SIMD(float * output, stbir__FP16 const * input) - { - for (int i=0; i<8; i++) - { - output[i] = stbir__half_to_float(input[i]); - } - } - static stbir__inline void stbir__float_to_half_SIMD(stbir__FP16 * output, float const * input) - { - for (int i=0; i<8; i++) - { - output[i] = stbir__float_to_half(input[i]); - } - } - -#endif - -#ifdef STBIR_SIMD - -#define stbir__simdf_0123to3333( out, reg ) (out) = stbir__simdf_swiz( reg, 3,3,3,3 ) -#define stbir__simdf_0123to2222( out, reg ) (out) = stbir__simdf_swiz( reg, 2,2,2,2 ) -#define stbir__simdf_0123to1111( out, reg ) (out) = stbir__simdf_swiz( reg, 1,1,1,1 ) -#define stbir__simdf_0123to0000( out, reg ) (out) = stbir__simdf_swiz( reg, 0,0,0,0 ) -#define stbir__simdf_0123to0003( out, reg ) (out) = stbir__simdf_swiz( reg, 0,0,0,3 ) -#define stbir__simdf_0123to0001( out, reg ) (out) = stbir__simdf_swiz( reg, 0,0,0,1 ) -#define stbir__simdf_0123to1122( out, reg ) (out) = stbir__simdf_swiz( reg, 1,1,2,2 ) -#define stbir__simdf_0123to2333( out, reg ) (out) = stbir__simdf_swiz( reg, 2,3,3,3 ) -#define stbir__simdf_0123to0023( out, reg ) (out) = stbir__simdf_swiz( reg, 0,0,2,3 ) -#define stbir__simdf_0123to1230( out, reg ) (out) = stbir__simdf_swiz( reg, 1,2,3,0 ) -#define stbir__simdf_0123to2103( out, reg ) (out) = stbir__simdf_swiz( reg, 2,1,0,3 ) -#define stbir__simdf_0123to3210( out, reg ) (out) = stbir__simdf_swiz( reg, 3,2,1,0 ) -#define stbir__simdf_0123to2301( out, reg ) (out) = stbir__simdf_swiz( reg, 2,3,0,1 ) -#define stbir__simdf_0123to3012( out, reg ) (out) = stbir__simdf_swiz( reg, 3,0,1,2 ) -#define stbir__simdf_0123to0011( out, reg ) (out) = stbir__simdf_swiz( reg, 0,0,1,1 ) -#define stbir__simdf_0123to1100( out, reg ) (out) = stbir__simdf_swiz( reg, 1,1,0,0 ) -#define stbir__simdf_0123to2233( out, reg ) (out) = stbir__simdf_swiz( reg, 2,2,3,3 ) -#define stbir__simdf_0123to1133( out, reg ) (out) = stbir__simdf_swiz( reg, 1,1,3,3 ) -#define stbir__simdf_0123to0022( out, reg ) (out) = stbir__simdf_swiz( reg, 0,0,2,2 ) -#define stbir__simdf_0123to1032( out, reg ) (out) = stbir__simdf_swiz( reg, 1,0,3,2 ) - -typedef union stbir__simdi_u32 -{ - stbir_uint32 m128i_u32[4]; - int m128i_i32[4]; - stbir__simdi m128i_i128; -} stbir__simdi_u32; - -static const int STBIR_mask[9] = { 0,0,0,-1,-1,-1,0,0,0 }; - -static const STBIR__SIMDF_CONST(STBIR_max_uint8_as_float, stbir__max_uint8_as_float); -static const STBIR__SIMDF_CONST(STBIR_max_uint16_as_float, stbir__max_uint16_as_float); -static const STBIR__SIMDF_CONST(STBIR_max_uint8_as_float_inverted, stbir__max_uint8_as_float_inverted); -static const STBIR__SIMDF_CONST(STBIR_max_uint16_as_float_inverted, stbir__max_uint16_as_float_inverted); - -static const STBIR__SIMDF_CONST(STBIR_simd_point5, 0.5f); -static const STBIR__SIMDF_CONST(STBIR_ones, 1.0f); -static const STBIR__SIMDI_CONST(STBIR_almost_zero, (127 - 13) << 23); -static const STBIR__SIMDI_CONST(STBIR_almost_one, 0x3f7fffff); -static const STBIR__SIMDI_CONST(STBIR_mastissa_mask, 0xff); -static const STBIR__SIMDI_CONST(STBIR_topscale, 0x02000000); - -#define STBIR_SIMD_STREAMOUT_PTR( star ) STBIR_STREAMOUT_PTR( star ) -#define STBIR_SIMD_NO_UNROLL(ptr) STBIR_NO_UNROLL(ptr) -#define STBIR_SIMD_NO_UNROLL_LOOP_START STBIR_NO_UNROLL_LOOP_START -#define STBIR_SIMD_NO_UNROLL_LOOP_START_INF_FOR STBIR_NO_UNROLL_LOOP_START_INF_FOR - -#ifdef STBIR_MEMCPY -#undef STBIR_MEMCPY -#endif -#define STBIR_MEMCPY stbir_simd_memcpy - -static void stbir_simd_memcpy( void * dest, void const * src, size_t bytes ) -{ - char STBIR_SIMD_STREAMOUT_PTR (*) d = (char*) dest; - char STBIR_SIMD_STREAMOUT_PTR( * ) d_end = ((char*) dest) + bytes; - ptrdiff_t ofs_to_src = (char*)src - (char*)dest; - - STBIR_ASSERT( ( ( d >= ( (char*)src) + bytes ) ) || ( ( d + bytes ) <= (char*)src ) ); - - if ( bytes < (16*stbir__simdfX_float_count) ) - { - if ( bytes < 16 ) - { - if ( bytes ) - { - STBIR_SIMD_NO_UNROLL_LOOP_START - do - { - STBIR_SIMD_NO_UNROLL(d); - d[ 0 ] = d[ ofs_to_src ]; - ++d; - } while ( d < d_end ); - } - } - else - { - stbir__simdf x; - stbir__simdf_load( x, ( d + ofs_to_src ) ); - stbir__simdf_store( d, x ); - d = (char*)( ( ( (size_t)d ) + 16 ) & ~15 ); - - STBIR_SIMD_NO_UNROLL_LOOP_START_INF_FOR - for(;;) - { - STBIR_SIMD_NO_UNROLL(d); - - if ( d > ( d_end - 16 ) ) - { - if ( d == d_end ) - return; - d = d_end - 16; - } - - stbir__simdf_load( x, ( d + ofs_to_src ) ); - stbir__simdf_store( d, x ); - d += 16; - } - } - } - else - { - stbir__simdfX x0,x1,x2,x3; - - stbir__simdfX_load( x0, ( d + ofs_to_src ) + 0*stbir__simdfX_float_count ); - stbir__simdfX_load( x1, ( d + ofs_to_src ) + 4*stbir__simdfX_float_count ); - stbir__simdfX_load( x2, ( d + ofs_to_src ) + 8*stbir__simdfX_float_count ); - stbir__simdfX_load( x3, ( d + ofs_to_src ) + 12*stbir__simdfX_float_count ); - stbir__simdfX_store( d + 0*stbir__simdfX_float_count, x0 ); - stbir__simdfX_store( d + 4*stbir__simdfX_float_count, x1 ); - stbir__simdfX_store( d + 8*stbir__simdfX_float_count, x2 ); - stbir__simdfX_store( d + 12*stbir__simdfX_float_count, x3 ); - d = (char*)( ( ( (size_t)d ) + (16*stbir__simdfX_float_count) ) & ~((16*stbir__simdfX_float_count)-1) ); - - STBIR_SIMD_NO_UNROLL_LOOP_START_INF_FOR - for(;;) - { - STBIR_SIMD_NO_UNROLL(d); - - if ( d > ( d_end - (16*stbir__simdfX_float_count) ) ) - { - if ( d == d_end ) - return; - d = d_end - (16*stbir__simdfX_float_count); - } - - stbir__simdfX_load( x0, ( d + ofs_to_src ) + 0*stbir__simdfX_float_count ); - stbir__simdfX_load( x1, ( d + ofs_to_src ) + 4*stbir__simdfX_float_count ); - stbir__simdfX_load( x2, ( d + ofs_to_src ) + 8*stbir__simdfX_float_count ); - stbir__simdfX_load( x3, ( d + ofs_to_src ) + 12*stbir__simdfX_float_count ); - stbir__simdfX_store( d + 0*stbir__simdfX_float_count, x0 ); - stbir__simdfX_store( d + 4*stbir__simdfX_float_count, x1 ); - stbir__simdfX_store( d + 8*stbir__simdfX_float_count, x2 ); - stbir__simdfX_store( d + 12*stbir__simdfX_float_count, x3 ); - d += (16*stbir__simdfX_float_count); - } - } -} - -static void stbir_overlapping_memcpy( void * dest, void const * src, size_t bytes ) -{ - char STBIR_SIMD_STREAMOUT_PTR (*) sd = (char*) src; - char STBIR_SIMD_STREAMOUT_PTR( * ) s_end = ((char*) src) + bytes; - ptrdiff_t ofs_to_dest = (char*)dest - (char*)src; - - if ( ofs_to_dest >= 16 ) // is the overlap more than 16 away? - { - char STBIR_SIMD_STREAMOUT_PTR( * ) s_end16 = ((char*) src) + (bytes&~15); - STBIR_SIMD_NO_UNROLL_LOOP_START - do - { - stbir__simdf x; - STBIR_SIMD_NO_UNROLL(sd); - stbir__simdf_load( x, sd ); - stbir__simdf_store( ( sd + ofs_to_dest ), x ); - sd += 16; - } while ( sd < s_end16 ); - - if ( sd == s_end ) - return; - } - - do - { - STBIR_SIMD_NO_UNROLL(sd); - *(int*)( sd + ofs_to_dest ) = *(int*) sd; - sd += 4; - } while ( sd < s_end ); -} - -#else // no SSE2 - -#define STBIR_SIMD_STREAMOUT_PTR( star ) STBIR_STREAMOUT_PTR( star ) -#define STBIR_SIMD_NO_UNROLL(ptr) -#define STBIR_SIMD_NO_UNROLL_LOOP_START -#define STBIR_SIMD_NO_UNROLL_LOOP_START_INF_FOR - -#endif // SSE2 - -#define STBIR_ONLY_PROFILE_GET_SPLIT_INFO -#define STBIR_ONLY_PROFILE_SET_SPLIT_INFO - -#define STBIR_ONLY_PROFILE_BUILD_GET_INFO -#define STBIR_ONLY_PROFILE_BUILD_SET_INFO - -#define STBIR_PROFILE_START( wh ) -#define STBIR_PROFILE_END( wh ) -#define STBIR_PROFILE_FIRST_START( wh ) -#define STBIR_PROFILE_CLEAR_EXTRAS( ) - -#define STBIR_PROFILE_BUILD_START( wh ) -#define STBIR_PROFILE_BUILD_END( wh ) -#define STBIR_PROFILE_BUILD_FIRST_START( wh ) -#define STBIR_PROFILE_BUILD_CLEAR( info ) - -#ifndef STBIR_CEILF -#include -#if _MSC_VER <= 1200 // support VC6 for Sean -#define STBIR_CEILF(x) ((float)ceil((float)(x))) -#define STBIR_FLOORF(x) ((float)floor((float)(x))) -#else -#define STBIR_CEILF(x) ceilf(x) -#define STBIR_FLOORF(x) floorf(x) -#endif -#endif - -#ifndef STBIR_MEMCPY -#include -#define STBIR_MEMCPY( dest, src, len ) memcpy( dest, src, len ) -#endif - -#ifndef STBIR_SIMD - -static void stbir_overlapping_memcpy( void * dest, void const * src, size_t bytes ) -{ - char STBIR_SIMD_STREAMOUT_PTR (*) sd = (char*) src; - char STBIR_SIMD_STREAMOUT_PTR( * ) s_end = ((char*) src) + bytes; - ptrdiff_t ofs_to_dest = (char*)dest - (char*)src; - - if ( ofs_to_dest >= 8 ) // is the overlap more than 8 away? - { - char STBIR_SIMD_STREAMOUT_PTR( * ) s_end8 = ((char*) src) + (bytes&~7); - STBIR_NO_UNROLL_LOOP_START - do - { - STBIR_NO_UNROLL(sd); - *(stbir_uint64*)( sd + ofs_to_dest ) = *(stbir_uint64*) sd; - sd += 8; - } while ( sd < s_end8 ); - - if ( sd == s_end ) - return; - } - - STBIR_NO_UNROLL_LOOP_START - do - { - STBIR_NO_UNROLL(sd); - *(int*)( sd + ofs_to_dest ) = *(int*) sd; - sd += 4; - } while ( sd < s_end ); -} - -#endif - -static float stbir__filter_trapezoid(float x, float scale, void * user_data) -{ - float halfscale = scale / 2; - float t = 0.5f + halfscale; - STBIR_ASSERT(scale <= 1); - STBIR__UNUSED(user_data); - - if ( x < 0.0f ) x = -x; - - if (x >= t) - return 0.0f; - else - { - float r = 0.5f - halfscale; - if (x <= r) - return 1.0f; - else - return (t - x) / scale; - } -} - -static float stbir__support_trapezoid(float scale, void * user_data) -{ - STBIR__UNUSED(user_data); - return 0.5f + scale / 2.0f; -} - -static float stbir__filter_triangle(float x, float s, void * user_data) -{ - STBIR__UNUSED(s); - STBIR__UNUSED(user_data); - - if ( x < 0.0f ) x = -x; - - if (x <= 1.0f) - return 1.0f - x; - else - return 0.0f; -} - -static float stbir__filter_point(float x, float s, void * user_data) -{ - STBIR__UNUSED(x); - STBIR__UNUSED(s); - STBIR__UNUSED(user_data); - - return 1.0f; -} - -static float stbir__filter_cubic(float x, float s, void * user_data) -{ - STBIR__UNUSED(s); - STBIR__UNUSED(user_data); - - if ( x < 0.0f ) x = -x; - - if (x < 1.0f) - return (4.0f + x*x*(3.0f*x - 6.0f))/6.0f; - else if (x < 2.0f) - return (8.0f + x*(-12.0f + x*(6.0f - x)))/6.0f; - - return (0.0f); -} - -static float stbir__filter_catmullrom(float x, float s, void * user_data) -{ - STBIR__UNUSED(s); - STBIR__UNUSED(user_data); - - if ( x < 0.0f ) x = -x; - - if (x < 1.0f) - return 1.0f - x*x*(2.5f - 1.5f*x); - else if (x < 2.0f) - return 2.0f - x*(4.0f + x*(0.5f*x - 2.5f)); - - return (0.0f); -} - -static float stbir__filter_mitchell(float x, float s, void * user_data) -{ - STBIR__UNUSED(s); - STBIR__UNUSED(user_data); - - if ( x < 0.0f ) x = -x; - - if (x < 1.0f) - return (16.0f + x*x*(21.0f * x - 36.0f))/18.0f; - else if (x < 2.0f) - return (32.0f + x*(-60.0f + x*(36.0f - 7.0f*x)))/18.0f; - - return (0.0f); -} - -static float stbir__support_zeropoint5(float s, void * user_data) -{ - STBIR__UNUSED(s); - STBIR__UNUSED(user_data); - return 0.5f; -} - -static float stbir__support_one(float s, void * user_data) -{ - STBIR__UNUSED(s); - STBIR__UNUSED(user_data); - return 1; -} - -static float stbir__support_two(float s, void * user_data) -{ - STBIR__UNUSED(s); - STBIR__UNUSED(user_data); - return 2; -} - -static int stbir__get_filter_pixel_width(stbir__support_callback * support, float scale, void * user_data) -{ - STBIR_ASSERT(support != 0); - - if ( scale >= ( 1.0f-stbir__small_float ) ) // upscale - return (int)STBIR_CEILF(support(1.0f/scale,user_data) * 2.0f); - else - return (int)STBIR_CEILF(support(scale,user_data) * 2.0f / scale); -} - -static int stbir__get_coefficient_width(stbir__sampler * samp, int is_gather, void * user_data) -{ - float scale = samp->scale_info.scale; - stbir__support_callback * support = samp->filter_support; - - switch( is_gather ) - { - case 1: - return (int)STBIR_CEILF(support(1.0f / scale, user_data) * 2.0f); - case 2: - return (int)STBIR_CEILF(support(scale, user_data) * 2.0f / scale); - case 0: - return (int)STBIR_CEILF(support(scale, user_data) * 2.0f); - default: - STBIR_ASSERT( (is_gather >= 0 ) && (is_gather <= 2 ) ); - return 0; - } -} - -static int stbir__get_contributors(stbir__sampler * samp, int is_gather) -{ - if (is_gather) - return samp->scale_info.output_sub_size; - else - return (samp->scale_info.input_full_size + samp->filter_pixel_margin * 2); -} - -static int stbir__edge_zero_full( int n, int max ) -{ - STBIR__UNUSED(n); - STBIR__UNUSED(max); - return 0; // NOTREACHED -} - -static int stbir__edge_clamp_full( int n, int max ) -{ - if (n < 0) - return 0; - - if (n >= max) - return max - 1; - - return n; // NOTREACHED -} - -static int stbir__edge_reflect_full( int n, int max ) -{ - if (n < 0) - { - if (n > -max) - return -n; - else - return max - 1; - } - - if (n >= max) - { - int max2 = max * 2; - if (n >= max2) - return 0; - else - return max2 - n - 1; - } - - return n; // NOTREACHED -} - -static int stbir__edge_wrap_full( int n, int max ) -{ - if (n >= 0) - return (n % max); - else - { - int m = (-n) % max; - - if (m != 0) - m = max - m; - - return (m); - } -} - -typedef int stbir__edge_wrap_func( int n, int max ); -static stbir__edge_wrap_func * stbir__edge_wrap_slow[] = -{ - stbir__edge_clamp_full, // STBIR_EDGE_CLAMP - stbir__edge_reflect_full, // STBIR_EDGE_REFLECT - stbir__edge_wrap_full, // STBIR_EDGE_WRAP - stbir__edge_zero_full, // STBIR_EDGE_ZERO -}; - -stbir__inline static int stbir__edge_wrap(stbir_edge edge, int n, int max) -{ - if (n >= 0 && n < max) - return n; - return stbir__edge_wrap_slow[edge]( n, max ); -} - -#define STBIR__MERGE_RUNS_PIXEL_THRESHOLD 16 - -static void stbir__get_extents( stbir__sampler * samp, stbir__extents * scanline_extents ) -{ - int j, stop; - int left_margin, right_margin; - int min_n = 0x7fffffff, max_n = -0x7fffffff; - int min_left = 0x7fffffff, max_left = -0x7fffffff; - int min_right = 0x7fffffff, max_right = -0x7fffffff; - stbir_edge edge = samp->edge; - stbir__contributors* contributors = samp->contributors; - int output_sub_size = samp->scale_info.output_sub_size; - int input_full_size = samp->scale_info.input_full_size; - int filter_pixel_margin = samp->filter_pixel_margin; - - STBIR_ASSERT( samp->is_gather ); - - stop = output_sub_size; - for (j = 0; j < stop; j++ ) - { - STBIR_ASSERT( contributors[j].n1 >= contributors[j].n0 ); - if ( contributors[j].n0 < min_n ) - { - min_n = contributors[j].n0; - stop = j + filter_pixel_margin; // if we find a new min, only scan another filter width - if ( stop > output_sub_size ) stop = output_sub_size; - } - } - - stop = 0; - for (j = output_sub_size - 1; j >= stop; j-- ) - { - STBIR_ASSERT( contributors[j].n1 >= contributors[j].n0 ); - if ( contributors[j].n1 > max_n ) - { - max_n = contributors[j].n1; - stop = j - filter_pixel_margin; // if we find a new max, only scan another filter width - if (stop<0) stop = 0; - } - } - - STBIR_ASSERT( scanline_extents->conservative.n0 <= min_n ); - STBIR_ASSERT( scanline_extents->conservative.n1 >= max_n ); - - left_margin = 0; - if ( min_n < 0 ) - { - left_margin = -min_n; - min_n = 0; - } - - right_margin = 0; - if ( max_n >= input_full_size ) - { - right_margin = max_n - input_full_size + 1; - max_n = input_full_size - 1; - } - - scanline_extents->edge_sizes[0] = left_margin; - scanline_extents->edge_sizes[1] = right_margin; - - scanline_extents->spans[0].n0 = min_n; - scanline_extents->spans[0].n1 = max_n; - scanline_extents->spans[0].pixel_offset_for_input = min_n; - - scanline_extents->spans[1].n0 = 0; - scanline_extents->spans[1].n1 = -1; - scanline_extents->spans[1].pixel_offset_for_input = 0; - - if ( edge == STBIR_EDGE_ZERO ) - return; - - for( j = -left_margin ; j < 0 ; j++ ) - { - int p = stbir__edge_wrap( edge, j, input_full_size ); - if ( p < min_left ) - min_left = p; - if ( p > max_left ) - max_left = p; - } - - for( j = input_full_size ; j < (input_full_size + right_margin) ; j++ ) - { - int p = stbir__edge_wrap( edge, j, input_full_size ); - if ( p < min_right ) - min_right = p; - if ( p > max_right ) - max_right = p; - } - - if ( min_left != 0x7fffffff ) - { - if ( ( ( min_left <= min_n ) && ( ( max_left + STBIR__MERGE_RUNS_PIXEL_THRESHOLD ) >= min_n ) ) || - ( ( min_n <= min_left ) && ( ( max_n + STBIR__MERGE_RUNS_PIXEL_THRESHOLD ) >= max_left ) ) ) - { - scanline_extents->spans[0].n0 = min_n = stbir__min( min_n, min_left ); - scanline_extents->spans[0].n1 = max_n = stbir__max( max_n, max_left ); - scanline_extents->spans[0].pixel_offset_for_input = min_n; - left_margin = 0; - } - } - - if ( min_right != 0x7fffffff ) - { - if ( ( ( min_right <= min_n ) && ( ( max_right + STBIR__MERGE_RUNS_PIXEL_THRESHOLD ) >= min_n ) ) || - ( ( min_n <= min_right ) && ( ( max_n + STBIR__MERGE_RUNS_PIXEL_THRESHOLD ) >= max_right ) ) ) - { - scanline_extents->spans[0].n0 = min_n = stbir__min( min_n, min_right ); - scanline_extents->spans[0].n1 = max_n = stbir__max( max_n, max_right ); - scanline_extents->spans[0].pixel_offset_for_input = min_n; - right_margin = 0; - } - } - - STBIR_ASSERT( scanline_extents->conservative.n0 <= min_n ); - STBIR_ASSERT( scanline_extents->conservative.n1 >= max_n ); - - if ( ( left_margin ) && ( min_left != 0x7fffffff ) ) - { - stbir__span * newspan = scanline_extents->spans + 1; - STBIR_ASSERT( right_margin == 0 ); - if ( min_left < scanline_extents->spans[0].n0 ) - { - scanline_extents->spans[1].pixel_offset_for_input = scanline_extents->spans[0].n0; - scanline_extents->spans[1].n0 = scanline_extents->spans[0].n0; - scanline_extents->spans[1].n1 = scanline_extents->spans[0].n1; - --newspan; - } - newspan->pixel_offset_for_input = min_left; - newspan->n0 = -left_margin; - newspan->n1 = ( max_left - min_left ) - left_margin; - scanline_extents->edge_sizes[0] = 0; // don't need to copy the left margin, since we are directly decoding into the margin - } - else - if ( ( right_margin ) && ( min_right != 0x7fffffff ) ) - { - stbir__span * newspan = scanline_extents->spans + 1; - if ( min_right < scanline_extents->spans[0].n0 ) - { - scanline_extents->spans[1].pixel_offset_for_input = scanline_extents->spans[0].n0; - scanline_extents->spans[1].n0 = scanline_extents->spans[0].n0; - scanline_extents->spans[1].n1 = scanline_extents->spans[0].n1; - --newspan; - } - newspan->pixel_offset_for_input = min_right; - newspan->n0 = scanline_extents->spans[1].n1 + 1; - newspan->n1 = scanline_extents->spans[1].n1 + 1 + ( max_right - min_right ); - scanline_extents->edge_sizes[1] = 0; // don't need to copy the right margin, since we are directly decoding into the margin - } - - if ( ( scanline_extents->spans[1].n1 > scanline_extents->spans[1].n0 ) && ( scanline_extents->spans[0].n0 > scanline_extents->spans[1].n0 ) ) - { - stbir__span tspan = scanline_extents->spans[0]; - scanline_extents->spans[0] = scanline_extents->spans[1]; - scanline_extents->spans[1] = tspan; - } -} - -static void stbir__calculate_in_pixel_range( int * first_pixel, int * last_pixel, float out_pixel_center, float out_filter_radius, float inv_scale, float out_shift, int input_size, stbir_edge edge ) -{ - int first, last; - float out_pixel_influence_lowerbound = out_pixel_center - out_filter_radius; - float out_pixel_influence_upperbound = out_pixel_center + out_filter_radius; - - float in_pixel_influence_lowerbound = (out_pixel_influence_lowerbound + out_shift) * inv_scale; - float in_pixel_influence_upperbound = (out_pixel_influence_upperbound + out_shift) * inv_scale; - - first = (int)(STBIR_FLOORF(in_pixel_influence_lowerbound + 0.5f)); - last = (int)(STBIR_FLOORF(in_pixel_influence_upperbound - 0.5f)); - if ( last < first ) last = first; // point sample mode can span a value *right* at 0.5, and cause these to cross - - if ( edge == STBIR_EDGE_WRAP ) - { - if ( first < -input_size ) - first = -input_size; - if ( last >= (input_size*2)) - last = (input_size*2) - 1; - } - - *first_pixel = first; - *last_pixel = last; -} - -static void stbir__calculate_coefficients_for_gather_upsample( float out_filter_radius, stbir__kernel_callback * kernel, stbir__scale_info * scale_info, int num_contributors, stbir__contributors* contributors, float* coefficient_group, int coefficient_width, stbir_edge edge, void * user_data ) -{ - int n, end; - float inv_scale = scale_info->inv_scale; - float out_shift = scale_info->pixel_shift; - int input_size = scale_info->input_full_size; - int numerator = scale_info->scale_numerator; - int polyphase = ( ( scale_info->scale_is_rational ) && ( numerator < num_contributors ) ); - - end = num_contributors; if ( polyphase ) end = numerator; - for (n = 0; n < end; n++) - { - int i; - int last_non_zero; - float out_pixel_center = (float)n + 0.5f; - float in_center_of_out = (out_pixel_center + out_shift) * inv_scale; - - int in_first_pixel, in_last_pixel; - - stbir__calculate_in_pixel_range( &in_first_pixel, &in_last_pixel, out_pixel_center, out_filter_radius, inv_scale, out_shift, input_size, edge ); - - if ( ( in_last_pixel - in_first_pixel + 1 ) > coefficient_width ) - in_last_pixel = in_first_pixel + coefficient_width - 1; - - last_non_zero = -1; - for (i = 0; i <= in_last_pixel - in_first_pixel; i++) - { - float in_pixel_center = (float)(i + in_first_pixel) + 0.5f; - float coeff = kernel(in_center_of_out - in_pixel_center, inv_scale, user_data); - - if ( ( ( coeff < stbir__small_float ) && ( coeff > -stbir__small_float ) ) ) - { - if ( i == 0 ) // if we're at the front, just eat zero contributors - { - STBIR_ASSERT ( ( in_last_pixel - in_first_pixel ) != 0 ); // there should be at least one contrib - ++in_first_pixel; - i--; - continue; - } - coeff = 0; // make sure is fully zero (should keep denormals away) - } - else - last_non_zero = i; - - coefficient_group[i] = coeff; - } - - in_last_pixel = last_non_zero+in_first_pixel; // kills trailing zeros - contributors->n0 = in_first_pixel; - contributors->n1 = in_last_pixel; - - STBIR_ASSERT(contributors->n1 >= contributors->n0); - - ++contributors; - coefficient_group += coefficient_width; - } -} - -static void stbir__insert_coeff( stbir__contributors * contribs, float * coeffs, int new_pixel, float new_coeff, int max_width ) -{ - if ( new_pixel <= contribs->n1 ) // before the end - { - if ( new_pixel < contribs->n0 ) // before the front? - { - if ( ( contribs->n1 - new_pixel + 1 ) <= max_width ) - { - int j, o = contribs->n0 - new_pixel; - for ( j = contribs->n1 - contribs->n0 ; j <= 0 ; j-- ) - coeffs[ j + o ] = coeffs[ j ]; - for ( j = 1 ; j < o ; j-- ) - coeffs[ j ] = coeffs[ 0 ]; - coeffs[ 0 ] = new_coeff; - contribs->n0 = new_pixel; - } - } - else - { - coeffs[ new_pixel - contribs->n0 ] += new_coeff; - } - } - else - { - if ( ( new_pixel - contribs->n0 + 1 ) <= max_width ) - { - int j, e = new_pixel - contribs->n0; - for( j = ( contribs->n1 - contribs->n0 ) + 1 ; j < e ; j++ ) // clear in-betweens coeffs if there are any - coeffs[j] = 0; - - coeffs[ e ] = new_coeff; - contribs->n1 = new_pixel; - } - } -} - -static void stbir__calculate_out_pixel_range( int * first_pixel, int * last_pixel, float in_pixel_center, float in_pixels_radius, float scale, float out_shift, int out_size ) -{ - float in_pixel_influence_lowerbound = in_pixel_center - in_pixels_radius; - float in_pixel_influence_upperbound = in_pixel_center + in_pixels_radius; - float out_pixel_influence_lowerbound = in_pixel_influence_lowerbound * scale - out_shift; - float out_pixel_influence_upperbound = in_pixel_influence_upperbound * scale - out_shift; - int out_first_pixel = (int)(STBIR_FLOORF(out_pixel_influence_lowerbound + 0.5f)); - int out_last_pixel = (int)(STBIR_FLOORF(out_pixel_influence_upperbound - 0.5f)); - - if ( out_first_pixel < 0 ) - out_first_pixel = 0; - if ( out_last_pixel >= out_size ) - out_last_pixel = out_size - 1; - *first_pixel = out_first_pixel; - *last_pixel = out_last_pixel; -} - -static void stbir__calculate_coefficients_for_gather_downsample( int start, int end, float in_pixels_radius, stbir__kernel_callback * kernel, stbir__scale_info * scale_info, int coefficient_width, int num_contributors, stbir__contributors * contributors, float * coefficient_group, void * user_data ) -{ - int in_pixel; - int i; - int first_out_inited = -1; - float scale = scale_info->scale; - float out_shift = scale_info->pixel_shift; - int out_size = scale_info->output_sub_size; - int numerator = scale_info->scale_numerator; - int polyphase = ( ( scale_info->scale_is_rational ) && ( numerator < out_size ) ); - - STBIR__UNUSED(num_contributors); - - for (in_pixel = start; in_pixel < end; in_pixel++) - { - float in_pixel_center = (float)in_pixel + 0.5f; - float out_center_of_in = in_pixel_center * scale - out_shift; - int out_first_pixel, out_last_pixel; - - stbir__calculate_out_pixel_range( &out_first_pixel, &out_last_pixel, in_pixel_center, in_pixels_radius, scale, out_shift, out_size ); - - if ( out_first_pixel > out_last_pixel ) - continue; - - if ( polyphase ) - { - if ( out_first_pixel == numerator ) - break; - - if ( out_last_pixel >= numerator ) - out_last_pixel = numerator - 1; - } - - for (i = 0; i <= out_last_pixel - out_first_pixel; i++) - { - float out_pixel_center = (float)(i + out_first_pixel) + 0.5f; - float x = out_pixel_center - out_center_of_in; - float coeff = kernel(x, scale, user_data) * scale; - - if ( ( ( coeff < stbir__small_float ) && ( coeff > -stbir__small_float ) ) ) - coeff = 0.0f; - - { - int out = i + out_first_pixel; - float * coeffs = coefficient_group + out * coefficient_width; - stbir__contributors * contribs = contributors + out; - - if ( out > first_out_inited ) - { - STBIR_ASSERT( out == ( first_out_inited + 1 ) ); // ensure we have only advanced one at time - first_out_inited = out; - contribs->n0 = in_pixel; - contribs->n1 = in_pixel; - coeffs[0] = coeff; - } - else - { - if ( coeffs[0] == 0.0f ) // if the first coefficent is zero, then zap it for this coeffs - { - STBIR_ASSERT( ( in_pixel - contribs->n0 ) == 1 ); // ensure that when we zap, we're at the 2nd pos - contribs->n0 = in_pixel; - } - contribs->n1 = in_pixel; - STBIR_ASSERT( ( in_pixel - contribs->n0 ) < coefficient_width ); - coeffs[in_pixel - contribs->n0] = coeff; - } - } - } - } -} - -#ifdef STBIR_RENORMALIZE_IN_FLOAT -#define STBIR_RENORM_TYPE float -#else -#define STBIR_RENORM_TYPE double -#endif - -static void stbir__cleanup_gathered_coefficients( stbir_edge edge, stbir__filter_extent_info* filter_info, stbir__scale_info * scale_info, int num_contributors, stbir__contributors* contributors, float * coefficient_group, int coefficient_width ) -{ - int input_size = scale_info->input_full_size; - int input_last_n1 = input_size - 1; - int n, end; - int lowest = 0x7fffffff; - int highest = -0x7fffffff; - int widest = -1; - int numerator = scale_info->scale_numerator; - int denominator = scale_info->scale_denominator; - int polyphase = ( ( scale_info->scale_is_rational ) && ( numerator < num_contributors ) ); - float * coeffs; - stbir__contributors * contribs; - - coeffs = coefficient_group; - contribs = contributors; - end = num_contributors; if ( polyphase ) end = numerator; - for (n = 0; n < end; n++) - { - int i; - STBIR_RENORM_TYPE filter_scale, total_filter = 0; - int e; - - e = contribs->n1 - contribs->n0; - for( i = 0 ; i <= e ; i++ ) - { - total_filter += (STBIR_RENORM_TYPE) coeffs[i]; - STBIR_ASSERT( ( coeffs[i] >= -2.0f ) && ( coeffs[i] <= 2.0f ) ); // check for wonky weights - } - - if ( ( total_filter < stbir__small_float ) && ( total_filter > -stbir__small_float ) ) - { - contribs->n1 = contribs->n0; - coeffs[0] = 0.0f; - } - else - { - if ( ( total_filter < (1.0f-stbir__small_float) ) || ( total_filter > (1.0f+stbir__small_float) ) ) - { - filter_scale = ((STBIR_RENORM_TYPE)1.0) / total_filter; - - for (i = 0; i <= e; i++) - coeffs[i] = (float) ( coeffs[i] * filter_scale ); - } - } - ++contribs; - coeffs += coefficient_width; - } - - if ( polyphase ) - { - stbir__contributors * prev_contribs = contributors; - stbir__contributors * cur_contribs = contributors + numerator; - - for( n = numerator ; n < num_contributors ; n++ ) - { - cur_contribs->n0 = prev_contribs->n0 + denominator; - cur_contribs->n1 = prev_contribs->n1 + denominator; - ++cur_contribs; - ++prev_contribs; - } - stbir_overlapping_memcpy( coefficient_group + numerator * coefficient_width, coefficient_group, ( num_contributors - numerator ) * coefficient_width * sizeof( coeffs[ 0 ] ) ); - } - - coeffs = coefficient_group; - contribs = contributors; - - for (n = 0; n < num_contributors; n++) - { - int i; - - if ( edge == STBIR_EDGE_ZERO ) - { - if ( contribs->n1 > input_last_n1 ) - contribs->n1 = input_last_n1; - - if ( contribs->n0 < 0 ) - { - int j, left, skips = 0; - - skips = -contribs->n0; - contribs->n0 = 0; - - left = contribs->n1 - contribs->n0 + 1; - if ( left > 0 ) - { - for( j = 0 ; j < left ; j++ ) - coeffs[ j ] = coeffs[ j + skips ]; - } - } - } - else if ( ( edge == STBIR_EDGE_CLAMP ) || ( edge == STBIR_EDGE_REFLECT ) ) - { - - if ( contribs->n1 > input_last_n1 ) - { - int start = contribs->n0; - int endi = contribs->n1; - contribs->n1 = input_last_n1; - for( i = input_size; i <= endi; i++ ) - stbir__insert_coeff( contribs, coeffs, stbir__edge_wrap_slow[edge]( i, input_size ), coeffs[i-start], coefficient_width ); - } - - if ( contribs->n0 < 0 ) - { - int save_n0; - float save_n0_coeff; - float * c = coeffs - ( contribs->n0 + 1 ); - - for( i = -1 ; i > contribs->n0 ; i-- ) - stbir__insert_coeff( contribs, coeffs, stbir__edge_wrap_slow[edge]( i, input_size ), *c--, coefficient_width ); - save_n0 = contribs->n0; - save_n0_coeff = c[0]; // save it, since we didn't do the final one (i==n0), because there might be too many coeffs to hold (before we resize)! - - contribs->n0 = 0; - for(i = 0 ; i <= contribs->n1 ; i++ ) - coeffs[i] = coeffs[i-save_n0]; - - stbir__insert_coeff( contribs, coeffs, stbir__edge_wrap_slow[edge]( save_n0, input_size ), save_n0_coeff, coefficient_width ); - } - } - - if ( contribs->n0 <= contribs->n1 ) - { - int diff = contribs->n1 - contribs->n0 + 1; - while ( diff && ( coeffs[ diff-1 ] == 0.0f ) ) - --diff; - - contribs->n1 = contribs->n0 + diff - 1; - - if ( contribs->n0 <= contribs->n1 ) - { - if ( contribs->n0 < lowest ) - lowest = contribs->n0; - if ( contribs->n1 > highest ) - highest = contribs->n1; - if ( diff > widest ) - widest = diff; - } - - for( i = diff ; i < coefficient_width ; i++ ) - coeffs[i] = 0.0f; - } - - ++contribs; - coeffs += coefficient_width; - } - filter_info->lowest = lowest; - filter_info->highest = highest; - filter_info->widest = widest; -} - -#undef STBIR_RENORM_TYPE - -static int stbir__pack_coefficients( int num_contributors, stbir__contributors* contributors, float * coefficents, int coefficient_width, int widest, int row0, int row1 ) -{ - #define STBIR_MOVE_1( dest, src ) { STBIR_NO_UNROLL(dest); ((stbir_uint32*)(dest))[0] = ((stbir_uint32*)(src))[0]; } - #define STBIR_MOVE_2( dest, src ) { STBIR_NO_UNROLL(dest); ((stbir_uint64*)(dest))[0] = ((stbir_uint64*)(src))[0]; } - #ifdef STBIR_SIMD - #define STBIR_MOVE_4( dest, src ) { stbir__simdf t; STBIR_NO_UNROLL(dest); stbir__simdf_load( t, src ); stbir__simdf_store( dest, t ); } - #else - #define STBIR_MOVE_4( dest, src ) { STBIR_NO_UNROLL(dest); ((stbir_uint64*)(dest))[0] = ((stbir_uint64*)(src))[0]; ((stbir_uint64*)(dest))[1] = ((stbir_uint64*)(src))[1]; } - #endif - - int row_end = row1 + 1; - STBIR__UNUSED( row0 ); // only used in an assert - - if ( coefficient_width != widest ) - { - float * pc = coefficents; - float * coeffs = coefficents; - float * pc_end = coefficents + num_contributors * widest; - switch( widest ) - { - case 1: - STBIR_NO_UNROLL_LOOP_START - do { - STBIR_MOVE_1( pc, coeffs ); - ++pc; - coeffs += coefficient_width; - } while ( pc < pc_end ); - break; - case 2: - STBIR_NO_UNROLL_LOOP_START - do { - STBIR_MOVE_2( pc, coeffs ); - pc += 2; - coeffs += coefficient_width; - } while ( pc < pc_end ); - break; - case 3: - STBIR_NO_UNROLL_LOOP_START - do { - STBIR_MOVE_2( pc, coeffs ); - STBIR_MOVE_1( pc+2, coeffs+2 ); - pc += 3; - coeffs += coefficient_width; - } while ( pc < pc_end ); - break; - case 4: - STBIR_NO_UNROLL_LOOP_START - do { - STBIR_MOVE_4( pc, coeffs ); - pc += 4; - coeffs += coefficient_width; - } while ( pc < pc_end ); - break; - case 5: - STBIR_NO_UNROLL_LOOP_START - do { - STBIR_MOVE_4( pc, coeffs ); - STBIR_MOVE_1( pc+4, coeffs+4 ); - pc += 5; - coeffs += coefficient_width; - } while ( pc < pc_end ); - break; - case 6: - STBIR_NO_UNROLL_LOOP_START - do { - STBIR_MOVE_4( pc, coeffs ); - STBIR_MOVE_2( pc+4, coeffs+4 ); - pc += 6; - coeffs += coefficient_width; - } while ( pc < pc_end ); - break; - case 7: - STBIR_NO_UNROLL_LOOP_START - do { - STBIR_MOVE_4( pc, coeffs ); - STBIR_MOVE_2( pc+4, coeffs+4 ); - STBIR_MOVE_1( pc+6, coeffs+6 ); - pc += 7; - coeffs += coefficient_width; - } while ( pc < pc_end ); - break; - case 8: - STBIR_NO_UNROLL_LOOP_START - do { - STBIR_MOVE_4( pc, coeffs ); - STBIR_MOVE_4( pc+4, coeffs+4 ); - pc += 8; - coeffs += coefficient_width; - } while ( pc < pc_end ); - break; - case 9: - STBIR_NO_UNROLL_LOOP_START - do { - STBIR_MOVE_4( pc, coeffs ); - STBIR_MOVE_4( pc+4, coeffs+4 ); - STBIR_MOVE_1( pc+8, coeffs+8 ); - pc += 9; - coeffs += coefficient_width; - } while ( pc < pc_end ); - break; - case 10: - STBIR_NO_UNROLL_LOOP_START - do { - STBIR_MOVE_4( pc, coeffs ); - STBIR_MOVE_4( pc+4, coeffs+4 ); - STBIR_MOVE_2( pc+8, coeffs+8 ); - pc += 10; - coeffs += coefficient_width; - } while ( pc < pc_end ); - break; - case 11: - STBIR_NO_UNROLL_LOOP_START - do { - STBIR_MOVE_4( pc, coeffs ); - STBIR_MOVE_4( pc+4, coeffs+4 ); - STBIR_MOVE_2( pc+8, coeffs+8 ); - STBIR_MOVE_1( pc+10, coeffs+10 ); - pc += 11; - coeffs += coefficient_width; - } while ( pc < pc_end ); - break; - case 12: - STBIR_NO_UNROLL_LOOP_START - do { - STBIR_MOVE_4( pc, coeffs ); - STBIR_MOVE_4( pc+4, coeffs+4 ); - STBIR_MOVE_4( pc+8, coeffs+8 ); - pc += 12; - coeffs += coefficient_width; - } while ( pc < pc_end ); - break; - default: - STBIR_NO_UNROLL_LOOP_START - do { - float * copy_end = pc + widest - 4; - float * c = coeffs; - do { - STBIR_NO_UNROLL( pc ); - STBIR_MOVE_4( pc, c ); - pc += 4; - c += 4; - } while ( pc <= copy_end ); - copy_end += 4; - STBIR_NO_UNROLL_LOOP_START - while ( pc < copy_end ) - { - STBIR_MOVE_1( pc, c ); - ++pc; ++c; - } - coeffs += coefficient_width; - } while ( pc < pc_end ); - break; - } - } - - coefficents[ widest * num_contributors ] = 8888.0f; - - { - stbir__contributors * contribs = contributors + num_contributors - 1; - float * coeffs = coefficents + widest * ( num_contributors - 1 ); - - while ( ( contribs >= contributors ) && ( ( contribs->n0 + widest*2 ) >= row_end ) ) - { - if ( ( contribs->n0 + widest ) > row_end ) - { - int stop_range = widest; - - if ( widest > 12 ) - { - int mod; - - mod = widest & 3; - stop_range = ( ( ( contribs->n1 - contribs->n0 + 1 ) - mod + 3 ) & ~3 ) + mod; - - if ( stop_range < ( 8 + mod ) ) stop_range = 8 + mod; - } - - if ( ( contribs->n0 + stop_range ) > row_end ) - { - int new_n0 = row_end - stop_range; - int num = contribs->n1 - contribs->n0 + 1; - int backup = contribs->n0 - new_n0; - float * from_co = coeffs + num - 1; - float * to_co = from_co + backup; - - STBIR_ASSERT( ( new_n0 >= row0 ) && ( new_n0 < contribs->n0 ) ); - - while( num ) - { - *to_co-- = *from_co--; - --num; - } - while ( to_co >= coeffs ) - *to_co-- = 0; - contribs->n0 = new_n0; - if ( widest > 12 ) - { - int mod; - - mod = widest & 3; - stop_range = ( ( ( contribs->n1 - contribs->n0 + 1 ) - mod + 3 ) & ~3 ) + mod; - - if ( stop_range < ( 8 + mod ) ) stop_range = 8 + mod; - } - } - } - --contribs; - coeffs -= widest; - } - } - - return widest; - #undef STBIR_MOVE_1 - #undef STBIR_MOVE_2 - #undef STBIR_MOVE_4 -} - -static void stbir__calculate_filters( stbir__sampler * samp, stbir__sampler * other_axis_for_pivot, void * user_data STBIR_ONLY_PROFILE_BUILD_GET_INFO ) -{ - int n; - float scale = samp->scale_info.scale; - stbir__kernel_callback * kernel = samp->filter_kernel; - stbir__support_callback * support = samp->filter_support; - float inv_scale = samp->scale_info.inv_scale; - int input_full_size = samp->scale_info.input_full_size; - int gather_num_contributors = samp->num_contributors; - stbir__contributors* gather_contributors = samp->contributors; - float * gather_coeffs = samp->coefficients; - int gather_coefficient_width = samp->coefficient_width; - - switch ( samp->is_gather ) - { - case 1: // gather upsample - { - float out_pixels_radius = support(inv_scale,user_data) * scale; - - stbir__calculate_coefficients_for_gather_upsample( out_pixels_radius, kernel, &samp->scale_info, gather_num_contributors, gather_contributors, gather_coeffs, gather_coefficient_width, samp->edge, user_data ); - - STBIR_PROFILE_BUILD_START( cleanup ); - stbir__cleanup_gathered_coefficients( samp->edge, &samp->extent_info, &samp->scale_info, gather_num_contributors, gather_contributors, gather_coeffs, gather_coefficient_width ); - STBIR_PROFILE_BUILD_END( cleanup ); - } - break; - - case 0: // scatter downsample (only on vertical) - case 2: // gather downsample - { - float in_pixels_radius = support(scale,user_data) * inv_scale; - int filter_pixel_margin = samp->filter_pixel_margin; - int input_end = input_full_size + filter_pixel_margin; - - if ( !samp->is_gather ) - { - if ( other_axis_for_pivot ) - { - gather_contributors = other_axis_for_pivot->contributors; - gather_coeffs = other_axis_for_pivot->coefficients; - gather_coefficient_width = other_axis_for_pivot->coefficient_width; - gather_num_contributors = other_axis_for_pivot->num_contributors; - samp->extent_info.lowest = other_axis_for_pivot->extent_info.lowest; - samp->extent_info.highest = other_axis_for_pivot->extent_info.highest; - samp->extent_info.widest = other_axis_for_pivot->extent_info.widest; - goto jump_right_to_pivot; - } - - gather_contributors = samp->gather_prescatter_contributors; - gather_coeffs = samp->gather_prescatter_coefficients; - gather_coefficient_width = samp->gather_prescatter_coefficient_width; - gather_num_contributors = samp->gather_prescatter_num_contributors; - } - - stbir__calculate_coefficients_for_gather_downsample( -filter_pixel_margin, input_end, in_pixels_radius, kernel, &samp->scale_info, gather_coefficient_width, gather_num_contributors, gather_contributors, gather_coeffs, user_data ); - - STBIR_PROFILE_BUILD_START( cleanup ); - stbir__cleanup_gathered_coefficients( samp->edge, &samp->extent_info, &samp->scale_info, gather_num_contributors, gather_contributors, gather_coeffs, gather_coefficient_width ); - STBIR_PROFILE_BUILD_END( cleanup ); - - if ( !samp->is_gather ) - { - stbir__contributors * scatter_contributors; - int highest_set; - - jump_right_to_pivot: - - STBIR_PROFILE_BUILD_START( pivot ); - - highest_set = (-filter_pixel_margin) - 1; - for (n = 0; n < gather_num_contributors; n++) - { - int k; - int gn0 = gather_contributors->n0, gn1 = gather_contributors->n1; - int scatter_coefficient_width = samp->coefficient_width; - float * scatter_coeffs = samp->coefficients + ( gn0 + filter_pixel_margin ) * scatter_coefficient_width; - float * g_coeffs = gather_coeffs; - scatter_contributors = samp->contributors + ( gn0 + filter_pixel_margin ); - - for (k = gn0 ; k <= gn1 ; k++ ) - { - float gc = *g_coeffs++; - - if ( ( ( gc >= stbir__small_float ) || ( gc <= -stbir__small_float ) ) ) - { - if ( ( k > highest_set ) || ( scatter_contributors->n0 > scatter_contributors->n1 ) ) - { - { - stbir__contributors * clear_contributors = samp->contributors + ( highest_set + filter_pixel_margin + 1); - while ( clear_contributors < scatter_contributors ) - { - clear_contributors->n0 = 0; - clear_contributors->n1 = -1; - ++clear_contributors; - } - } - scatter_contributors->n0 = n; - scatter_contributors->n1 = n; - scatter_coeffs[0] = gc; - highest_set = k; - } - else - { - stbir__insert_coeff( scatter_contributors, scatter_coeffs, n, gc, scatter_coefficient_width ); - } - STBIR_ASSERT( ( scatter_contributors->n1 - scatter_contributors->n0 + 1 ) <= scatter_coefficient_width ); - } - ++scatter_contributors; - scatter_coeffs += scatter_coefficient_width; - } - - ++gather_contributors; - gather_coeffs += gather_coefficient_width; - } - - { - stbir__contributors * clear_contributors = samp->contributors + ( highest_set + filter_pixel_margin + 1); - stbir__contributors * end_contributors = samp->contributors + samp->num_contributors; - while ( clear_contributors < end_contributors ) - { - clear_contributors->n0 = 0; - clear_contributors->n1 = -1; - ++clear_contributors; - } - } - - STBIR_PROFILE_BUILD_END( pivot ); - } - } - break; - } -} - -#define stbir__coder_min_num 1 -#define STB_IMAGE_RESIZE_DO_CODERS -#include STBIR__HEADER_FILENAME - -#define stbir__decode_suffix BGRA -#define stbir__decode_swizzle -#define stbir__decode_order0 2 -#define stbir__decode_order1 1 -#define stbir__decode_order2 0 -#define stbir__decode_order3 3 -#define stbir__encode_order0 2 -#define stbir__encode_order1 1 -#define stbir__encode_order2 0 -#define stbir__encode_order3 3 -#define stbir__coder_min_num 4 -#define STB_IMAGE_RESIZE_DO_CODERS -#include STBIR__HEADER_FILENAME - -#define stbir__decode_suffix ARGB -#define stbir__decode_swizzle -#define stbir__decode_order0 1 -#define stbir__decode_order1 2 -#define stbir__decode_order2 3 -#define stbir__decode_order3 0 -#define stbir__encode_order0 3 -#define stbir__encode_order1 0 -#define stbir__encode_order2 1 -#define stbir__encode_order3 2 -#define stbir__coder_min_num 4 -#define STB_IMAGE_RESIZE_DO_CODERS -#include STBIR__HEADER_FILENAME - -#define stbir__decode_suffix ABGR -#define stbir__decode_swizzle -#define stbir__decode_order0 3 -#define stbir__decode_order1 2 -#define stbir__decode_order2 1 -#define stbir__decode_order3 0 -#define stbir__encode_order0 3 -#define stbir__encode_order1 2 -#define stbir__encode_order2 1 -#define stbir__encode_order3 0 -#define stbir__coder_min_num 4 -#define STB_IMAGE_RESIZE_DO_CODERS -#include STBIR__HEADER_FILENAME - -#define stbir__decode_suffix AR -#define stbir__decode_swizzle -#define stbir__decode_order0 1 -#define stbir__decode_order1 0 -#define stbir__decode_order2 3 -#define stbir__decode_order3 2 -#define stbir__encode_order0 1 -#define stbir__encode_order1 0 -#define stbir__encode_order2 3 -#define stbir__encode_order3 2 -#define stbir__coder_min_num 2 -#define STB_IMAGE_RESIZE_DO_CODERS -#include STBIR__HEADER_FILENAME - -static void stbir__fancy_alpha_weight_4ch( float * out_buffer, int width_times_channels ) -{ - float STBIR_STREAMOUT_PTR(*) out = out_buffer; - float const * end_decode = out_buffer + ( width_times_channels / 4 ) * 7; // decode buffer aligned to end of out_buffer - float STBIR_STREAMOUT_PTR(*) decode = (float*)end_decode - width_times_channels; - - #ifdef STBIR_SIMD - - #ifdef STBIR_SIMD8 - decode += 16; - STBIR_NO_UNROLL_LOOP_START - while ( decode <= end_decode ) - { - stbir__simdf8 d0,d1,a0,a1,p0,p1; - STBIR_NO_UNROLL(decode); - stbir__simdf8_load( d0, decode-16 ); - stbir__simdf8_load( d1, decode-16+8 ); - stbir__simdf8_0123to33333333( a0, d0 ); - stbir__simdf8_0123to33333333( a1, d1 ); - stbir__simdf8_mult( p0, a0, d0 ); - stbir__simdf8_mult( p1, a1, d1 ); - stbir__simdf8_bot4s( a0, d0, p0 ); - stbir__simdf8_bot4s( a1, d1, p1 ); - stbir__simdf8_top4s( d0, d0, p0 ); - stbir__simdf8_top4s( d1, d1, p1 ); - stbir__simdf8_store ( out, a0 ); - stbir__simdf8_store ( out+7, d0 ); - stbir__simdf8_store ( out+14, a1 ); - stbir__simdf8_store ( out+21, d1 ); - decode += 16; - out += 28; - } - decode -= 16; - #else - decode += 8; - STBIR_NO_UNROLL_LOOP_START - while ( decode <= end_decode ) - { - stbir__simdf d0,a0,d1,a1,p0,p1; - STBIR_NO_UNROLL(decode); - stbir__simdf_load( d0, decode-8 ); - stbir__simdf_load( d1, decode-8+4 ); - stbir__simdf_0123to3333( a0, d0 ); - stbir__simdf_0123to3333( a1, d1 ); - stbir__simdf_mult( p0, a0, d0 ); - stbir__simdf_mult( p1, a1, d1 ); - stbir__simdf_store ( out, d0 ); - stbir__simdf_store ( out+4, p0 ); - stbir__simdf_store ( out+7, d1 ); - stbir__simdf_store ( out+7+4, p1 ); - decode += 8; - out += 14; - } - decode -= 8; - #endif - - #ifdef STBIR_SIMD8 - STBIR_NO_UNROLL_LOOP_START - while ( decode < end_decode ) - #else - if ( decode < end_decode ) - #endif - { - stbir__simdf d,a,p; - STBIR_NO_UNROLL(decode); - stbir__simdf_load( d, decode ); - stbir__simdf_0123to3333( a, d ); - stbir__simdf_mult( p, a, d ); - stbir__simdf_store ( out, d ); - stbir__simdf_store ( out+4, p ); - decode += 4; - out += 7; - } - - #else - - while( decode < end_decode ) - { - float r = decode[0], g = decode[1], b = decode[2], alpha = decode[3]; - out[0] = r; - out[1] = g; - out[2] = b; - out[3] = alpha; - out[4] = r * alpha; - out[5] = g * alpha; - out[6] = b * alpha; - out += 7; - decode += 4; - } - - #endif -} - -static void stbir__fancy_alpha_weight_2ch( float * out_buffer, int width_times_channels ) -{ - float STBIR_STREAMOUT_PTR(*) out = out_buffer; - float const * end_decode = out_buffer + ( width_times_channels / 2 ) * 3; - float STBIR_STREAMOUT_PTR(*) decode = (float*)end_decode - width_times_channels; - - #ifdef STBIR_SIMD - - decode += 8; - if ( decode <= end_decode ) - { - STBIR_NO_UNROLL_LOOP_START - do { - #ifdef STBIR_SIMD8 - stbir__simdf8 d0,a0,p0; - STBIR_NO_UNROLL(decode); - stbir__simdf8_load( d0, decode-8 ); - stbir__simdf8_0123to11331133( p0, d0 ); - stbir__simdf8_0123to00220022( a0, d0 ); - stbir__simdf8_mult( p0, p0, a0 ); - - stbir__simdf_store2( out, stbir__if_simdf8_cast_to_simdf4( d0 ) ); - stbir__simdf_store( out+2, stbir__if_simdf8_cast_to_simdf4( p0 ) ); - stbir__simdf_store2h( out+3, stbir__if_simdf8_cast_to_simdf4( d0 ) ); - - stbir__simdf_store2( out+6, stbir__simdf8_gettop4( d0 ) ); - stbir__simdf_store( out+8, stbir__simdf8_gettop4( p0 ) ); - stbir__simdf_store2h( out+9, stbir__simdf8_gettop4( d0 ) ); - #else - stbir__simdf d0,a0,d1,a1,p0,p1; - STBIR_NO_UNROLL(decode); - stbir__simdf_load( d0, decode-8 ); - stbir__simdf_load( d1, decode-8+4 ); - stbir__simdf_0123to1133( p0, d0 ); - stbir__simdf_0123to1133( p1, d1 ); - stbir__simdf_0123to0022( a0, d0 ); - stbir__simdf_0123to0022( a1, d1 ); - stbir__simdf_mult( p0, p0, a0 ); - stbir__simdf_mult( p1, p1, a1 ); - - stbir__simdf_store2( out, d0 ); - stbir__simdf_store( out+2, p0 ); - stbir__simdf_store2h( out+3, d0 ); - - stbir__simdf_store2( out+6, d1 ); - stbir__simdf_store( out+8, p1 ); - stbir__simdf_store2h( out+9, d1 ); - #endif - decode += 8; - out += 12; - } while ( decode <= end_decode ); - } - decode -= 8; - #endif - - STBIR_SIMD_NO_UNROLL_LOOP_START - while( decode < end_decode ) - { - float x = decode[0], y = decode[1]; - STBIR_SIMD_NO_UNROLL(decode); - out[0] = x; - out[1] = y; - out[2] = x * y; - out += 3; - decode += 2; - } -} - -static void stbir__fancy_alpha_unweight_4ch( float * encode_buffer, int width_times_channels ) -{ - float STBIR_SIMD_STREAMOUT_PTR(*) encode = encode_buffer; - float STBIR_SIMD_STREAMOUT_PTR(*) input = encode_buffer; - float const * end_output = encode_buffer + width_times_channels; - - STBIR_SIMD_NO_UNROLL_LOOP_START - do { - float alpha = input[3]; -#ifdef STBIR_SIMD - stbir__simdf i,ia; - STBIR_SIMD_NO_UNROLL(encode); - if ( alpha < stbir__small_float ) - { - stbir__simdf_load( i, input ); - stbir__simdf_store( encode, i ); - } - else - { - stbir__simdf_load1frep4( ia, 1.0f / alpha ); - stbir__simdf_load( i, input+4 ); - stbir__simdf_mult( i, i, ia ); - stbir__simdf_store( encode, i ); - encode[3] = alpha; - } -#else - if ( alpha < stbir__small_float ) - { - encode[0] = input[0]; - encode[1] = input[1]; - encode[2] = input[2]; - } - else - { - float ialpha = 1.0f / alpha; - encode[0] = input[4] * ialpha; - encode[1] = input[5] * ialpha; - encode[2] = input[6] * ialpha; - } - encode[3] = alpha; -#endif - - input += 7; - encode += 4; - } while ( encode < end_output ); -} - -static void stbir__fancy_alpha_unweight_2ch( float * encode_buffer, int width_times_channels ) -{ - float STBIR_SIMD_STREAMOUT_PTR(*) encode = encode_buffer; - float STBIR_SIMD_STREAMOUT_PTR(*) input = encode_buffer; - float const * end_output = encode_buffer + width_times_channels; - - do { - float alpha = input[1]; - encode[0] = input[0]; - if ( alpha >= stbir__small_float ) - encode[0] = input[2] / alpha; - encode[1] = alpha; - - input += 3; - encode += 2; - } while ( encode < end_output ); -} - -static void stbir__simple_alpha_weight_4ch( float * decode_buffer, int width_times_channels ) -{ - float STBIR_STREAMOUT_PTR(*) decode = decode_buffer; - float const * end_decode = decode_buffer + width_times_channels; - - #ifdef STBIR_SIMD - { - decode += 2 * stbir__simdfX_float_count; - STBIR_NO_UNROLL_LOOP_START - while ( decode <= end_decode ) - { - stbir__simdfX d0,a0,d1,a1; - STBIR_NO_UNROLL(decode); - stbir__simdfX_load( d0, decode-2*stbir__simdfX_float_count ); - stbir__simdfX_load( d1, decode-2*stbir__simdfX_float_count+stbir__simdfX_float_count ); - stbir__simdfX_aaa1( a0, d0, STBIR_onesX ); - stbir__simdfX_aaa1( a1, d1, STBIR_onesX ); - stbir__simdfX_mult( d0, d0, a0 ); - stbir__simdfX_mult( d1, d1, a1 ); - stbir__simdfX_store ( decode-2*stbir__simdfX_float_count, d0 ); - stbir__simdfX_store ( decode-2*stbir__simdfX_float_count+stbir__simdfX_float_count, d1 ); - decode += 2 * stbir__simdfX_float_count; - } - decode -= 2 * stbir__simdfX_float_count; - - #ifdef STBIR_SIMD8 - STBIR_NO_UNROLL_LOOP_START - while ( decode < end_decode ) - #else - if ( decode < end_decode ) - #endif - { - stbir__simdf d,a; - stbir__simdf_load( d, decode ); - stbir__simdf_aaa1( a, d, STBIR__CONSTF(STBIR_ones) ); - stbir__simdf_mult( d, d, a ); - stbir__simdf_store ( decode, d ); - decode += 4; - } - } - - #else - - while( decode < end_decode ) - { - float alpha = decode[3]; - decode[0] *= alpha; - decode[1] *= alpha; - decode[2] *= alpha; - decode += 4; - } - - #endif -} - -static void stbir__simple_alpha_weight_2ch( float * decode_buffer, int width_times_channels ) -{ - float STBIR_STREAMOUT_PTR(*) decode = decode_buffer; - float const * end_decode = decode_buffer + width_times_channels; - - #ifdef STBIR_SIMD - decode += 2 * stbir__simdfX_float_count; - STBIR_NO_UNROLL_LOOP_START - while ( decode <= end_decode ) - { - stbir__simdfX d0,a0,d1,a1; - STBIR_NO_UNROLL(decode); - stbir__simdfX_load( d0, decode-2*stbir__simdfX_float_count ); - stbir__simdfX_load( d1, decode-2*stbir__simdfX_float_count+stbir__simdfX_float_count ); - stbir__simdfX_a1a1( a0, d0, STBIR_onesX ); - stbir__simdfX_a1a1( a1, d1, STBIR_onesX ); - stbir__simdfX_mult( d0, d0, a0 ); - stbir__simdfX_mult( d1, d1, a1 ); - stbir__simdfX_store ( decode-2*stbir__simdfX_float_count, d0 ); - stbir__simdfX_store ( decode-2*stbir__simdfX_float_count+stbir__simdfX_float_count, d1 ); - decode += 2 * stbir__simdfX_float_count; - } - decode -= 2 * stbir__simdfX_float_count; - #endif - - STBIR_SIMD_NO_UNROLL_LOOP_START - while( decode < end_decode ) - { - float alpha = decode[1]; - STBIR_SIMD_NO_UNROLL(decode); - decode[0] *= alpha; - decode += 2; - } -} - -static void stbir__simple_alpha_unweight_4ch( float * encode_buffer, int width_times_channels ) -{ - float STBIR_SIMD_STREAMOUT_PTR(*) encode = encode_buffer; - float const * end_output = encode_buffer + width_times_channels; - - STBIR_SIMD_NO_UNROLL_LOOP_START - do { - float alpha = encode[3]; - -#ifdef STBIR_SIMD - stbir__simdf i,ia; - STBIR_SIMD_NO_UNROLL(encode); - if ( alpha >= stbir__small_float ) - { - stbir__simdf_load1frep4( ia, 1.0f / alpha ); - stbir__simdf_load( i, encode ); - stbir__simdf_mult( i, i, ia ); - stbir__simdf_store( encode, i ); - encode[3] = alpha; - } -#else - if ( alpha >= stbir__small_float ) - { - float ialpha = 1.0f / alpha; - encode[0] *= ialpha; - encode[1] *= ialpha; - encode[2] *= ialpha; - } -#endif - encode += 4; - } while ( encode < end_output ); -} - -static void stbir__simple_alpha_unweight_2ch( float * encode_buffer, int width_times_channels ) -{ - float STBIR_SIMD_STREAMOUT_PTR(*) encode = encode_buffer; - float const * end_output = encode_buffer + width_times_channels; - - do { - float alpha = encode[1]; - if ( alpha >= stbir__small_float ) - encode[0] /= alpha; - encode += 2; - } while ( encode < end_output ); -} - -static void stbir__simple_flip_3ch( float * decode_buffer, int width_times_channels ) -{ - float STBIR_STREAMOUT_PTR(*) decode = decode_buffer; - float const * end_decode = decode_buffer + width_times_channels; - -#ifdef STBIR_SIMD - #ifdef stbir__simdf_swiz2 // do we have two argument swizzles? - end_decode -= 12; - STBIR_NO_UNROLL_LOOP_START - while( decode <= end_decode ) - { - stbir__simdf a,b,c,na,nb; - STBIR_SIMD_NO_UNROLL(decode); - stbir__simdf_load( a, decode ); - stbir__simdf_load( b, decode+4 ); - stbir__simdf_load( c, decode+8 ); - - na = stbir__simdf_swiz2( a, b, 2, 1, 0, 5 ); - b = stbir__simdf_swiz2( a, b, 4, 3, 6, 7 ); - nb = stbir__simdf_swiz2( b, c, 0, 1, 4, 3 ); - c = stbir__simdf_swiz2( b, c, 2, 7, 6, 5 ); - - stbir__simdf_store( decode, na ); - stbir__simdf_store( decode+4, nb ); - stbir__simdf_store( decode+8, c ); - decode += 12; - } - end_decode += 12; - #else - end_decode -= 24; - STBIR_NO_UNROLL_LOOP_START - while( decode <= end_decode ) - { - stbir__simdf a,b,c,d,e,f,g; - float i21, i23; - STBIR_SIMD_NO_UNROLL(decode); - stbir__simdf_load( a, decode ); - stbir__simdf_load( b, decode+3 ); - stbir__simdf_load( c, decode+6 ); - stbir__simdf_load( d, decode+9 ); - stbir__simdf_load( e, decode+12 ); - stbir__simdf_load( f, decode+15 ); - stbir__simdf_load( g, decode+18 ); - - a = stbir__simdf_swiz( a, 2, 1, 0, 3 ); - b = stbir__simdf_swiz( b, 2, 1, 0, 3 ); - c = stbir__simdf_swiz( c, 2, 1, 0, 3 ); - d = stbir__simdf_swiz( d, 2, 1, 0, 3 ); - e = stbir__simdf_swiz( e, 2, 1, 0, 3 ); - f = stbir__simdf_swiz( f, 2, 1, 0, 3 ); - g = stbir__simdf_swiz( g, 2, 1, 0, 3 ); - - stbir__simdf_store( decode, a ); - i21 = decode[21]; - stbir__simdf_store( decode+3, b ); - i23 = decode[23]; - stbir__simdf_store( decode+6, c ); - stbir__simdf_store( decode+9, d ); - stbir__simdf_store( decode+12, e ); - stbir__simdf_store( decode+15, f ); - stbir__simdf_store( decode+18, g ); - decode[21] = i23; - decode[23] = i21; - decode += 24; - } - end_decode += 24; - #endif -#else - end_decode -= 12; - STBIR_NO_UNROLL_LOOP_START - while( decode <= end_decode ) - { - float t0,t1,t2,t3; - STBIR_NO_UNROLL(decode); - t0 = decode[0]; t1 = decode[3]; t2 = decode[6]; t3 = decode[9]; - decode[0] = decode[2]; decode[3] = decode[5]; decode[6] = decode[8]; decode[9] = decode[11]; - decode[2] = t0; decode[5] = t1; decode[8] = t2; decode[11] = t3; - decode += 12; - } - end_decode += 12; -#endif - - STBIR_NO_UNROLL_LOOP_START - while( decode < end_decode ) - { - float t = decode[0]; - STBIR_NO_UNROLL(decode); - decode[0] = decode[2]; - decode[2] = t; - decode += 3; - } -} - -static void stbir__decode_scanline(stbir__info const * stbir_info, int n, float * output_buffer STBIR_ONLY_PROFILE_GET_SPLIT_INFO ) -{ - int channels = stbir_info->channels; - int effective_channels = stbir_info->effective_channels; - int input_sample_in_bytes = stbir__type_size[stbir_info->input_type] * channels; - stbir_edge edge_horizontal = stbir_info->horizontal.edge; - stbir_edge edge_vertical = stbir_info->vertical.edge; - int row = stbir__edge_wrap(edge_vertical, n, stbir_info->vertical.scale_info.input_full_size); - const void* input_plane_data = ( (char *) stbir_info->input_data ) + (size_t)row * (size_t) stbir_info->input_stride_bytes; - stbir__span const * spans = stbir_info->scanline_extents.spans; - float * full_decode_buffer = output_buffer - stbir_info->scanline_extents.conservative.n0 * effective_channels; - float * last_decoded = 0; - - STBIR_ASSERT( !(edge_vertical == STBIR_EDGE_ZERO && (n < 0 || n >= stbir_info->vertical.scale_info.input_full_size)) ); - - do - { - float * decode_buffer; - void const * input_data; - float * end_decode; - int width_times_channels; - int width; - - if ( spans->n1 < spans->n0 ) - break; - - width = spans->n1 + 1 - spans->n0; - decode_buffer = full_decode_buffer + spans->n0 * effective_channels; - end_decode = full_decode_buffer + ( spans->n1 + 1 ) * effective_channels; - width_times_channels = width * channels; - - input_data = ( (char*)input_plane_data ) + spans->pixel_offset_for_input * input_sample_in_bytes; - - if ( stbir_info->in_pixels_cb ) - { - input_data = stbir_info->in_pixels_cb( ( (char*) end_decode ) - ( width * input_sample_in_bytes ) + ( ( stbir_info->input_type != STBIR_TYPE_FLOAT ) ? ( sizeof(float)*STBIR_INPUT_CALLBACK_PADDING ) : 0 ), input_plane_data, width, spans->pixel_offset_for_input, row, stbir_info->user_data ); - } - - STBIR_PROFILE_START( decode ); - last_decoded = stbir_info->decode_pixels( (float*)end_decode - width_times_channels, width_times_channels, input_data ); - STBIR_PROFILE_END( decode ); - - if (stbir_info->alpha_weight) - { - STBIR_PROFILE_START( alpha ); - stbir_info->alpha_weight( decode_buffer, width_times_channels ); - STBIR_PROFILE_END( alpha ); - } - - ++spans; - } while ( spans <= ( &stbir_info->scanline_extents.spans[1] ) ); - - if ( ( edge_horizontal == STBIR_EDGE_WRAP ) && ( stbir_info->scanline_extents.edge_sizes[0] | stbir_info->scanline_extents.edge_sizes[1] ) ) - { - int e, start_x[2]; - int input_full_size = stbir_info->horizontal.scale_info.input_full_size; - - start_x[0] = -stbir_info->scanline_extents.edge_sizes[0]; // left edge start x - start_x[1] = input_full_size; // right edge - - for( e = 0; e < 2 ; e++ ) - { - int margin = stbir_info->scanline_extents.edge_sizes[e]; - if ( margin ) - { - int x = start_x[e]; - float * marg = full_decode_buffer + x * effective_channels; - float const * src = full_decode_buffer + stbir__edge_wrap(edge_horizontal, x, input_full_size) * effective_channels; - STBIR_MEMCPY( marg, src, margin * effective_channels * sizeof(float) ); - if ( e == 1 ) last_decoded = marg + margin * effective_channels; - } - } - } - - last_decoded[0] = 0.0f; - - last_decoded[1] = 0.0f; -} - -#ifdef STBIR_SIMD - -#define stbir__1_coeff_only() \ - stbir__simdf tot,c; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load1( c, hc ); \ - stbir__simdf_mult1_mem( tot, c, decode ); - -#define stbir__2_coeff_only() \ - stbir__simdf tot,c,d; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load2z( c, hc ); \ - stbir__simdf_load2( d, decode ); \ - stbir__simdf_mult( tot, c, d ); \ - stbir__simdf_0123to1230( c, tot ); \ - stbir__simdf_add1( tot, tot, c ); - -#define stbir__3_coeff_only() \ - stbir__simdf tot,c,t; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load( c, hc ); \ - stbir__simdf_mult_mem( tot, c, decode ); \ - stbir__simdf_0123to1230( c, tot ); \ - stbir__simdf_0123to2301( t, tot ); \ - stbir__simdf_add1( tot, tot, c ); \ - stbir__simdf_add1( tot, tot, t ); - -#define stbir__store_output_tiny() \ - stbir__simdf_store1( output, tot ); \ - horizontal_coefficients += coefficient_width; \ - ++horizontal_contributors; \ - output += 1; - -#define stbir__4_coeff_start() \ - stbir__simdf tot,c; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load( c, hc ); \ - stbir__simdf_mult_mem( tot, c, decode ); \ - -#define stbir__4_coeff_continue_from_4( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load( c, hc + (ofs) ); \ - stbir__simdf_madd_mem( tot, tot, c, decode+(ofs) ); - -#define stbir__1_coeff_remnant( ofs ) \ - { stbir__simdf d; \ - stbir__simdf_load1z( c, hc + (ofs) ); \ - stbir__simdf_load1( d, decode + (ofs) ); \ - stbir__simdf_madd( tot, tot, d, c ); } - -#define stbir__2_coeff_remnant( ofs ) \ - { stbir__simdf d; \ - stbir__simdf_load2z( c, hc+(ofs) ); \ - stbir__simdf_load2( d, decode+(ofs) ); \ - stbir__simdf_madd( tot, tot, d, c ); } - -#define stbir__3_coeff_setup() \ - stbir__simdf mask; \ - stbir__simdf_load( mask, STBIR_mask + 3 ); - -#define stbir__3_coeff_remnant( ofs ) \ - stbir__simdf_load( c, hc+(ofs) ); \ - stbir__simdf_and( c, c, mask ); \ - stbir__simdf_madd_mem( tot, tot, c, decode+(ofs) ); - -#define stbir__store_output() \ - stbir__simdf_0123to2301( c, tot ); \ - stbir__simdf_add( tot, tot, c ); \ - stbir__simdf_0123to1230( c, tot ); \ - stbir__simdf_add1( tot, tot, c ); \ - stbir__simdf_store1( output, tot ); \ - horizontal_coefficients += coefficient_width; \ - ++horizontal_contributors; \ - output += 1; - -#else - -#define stbir__1_coeff_only() \ - float tot; \ - tot = decode[0]*hc[0]; - -#define stbir__2_coeff_only() \ - float tot; \ - tot = decode[0] * hc[0]; \ - tot += decode[1] * hc[1]; - -#define stbir__3_coeff_only() \ - float tot; \ - tot = decode[0] * hc[0]; \ - tot += decode[1] * hc[1]; \ - tot += decode[2] * hc[2]; - -#define stbir__store_output_tiny() \ - output[0] = tot; \ - horizontal_coefficients += coefficient_width; \ - ++horizontal_contributors; \ - output += 1; - -#define stbir__4_coeff_start() \ - float tot0,tot1,tot2,tot3; \ - tot0 = decode[0] * hc[0]; \ - tot1 = decode[1] * hc[1]; \ - tot2 = decode[2] * hc[2]; \ - tot3 = decode[3] * hc[3]; - -#define stbir__4_coeff_continue_from_4( ofs ) \ - tot0 += decode[0+(ofs)] * hc[0+(ofs)]; \ - tot1 += decode[1+(ofs)] * hc[1+(ofs)]; \ - tot2 += decode[2+(ofs)] * hc[2+(ofs)]; \ - tot3 += decode[3+(ofs)] * hc[3+(ofs)]; - -#define stbir__1_coeff_remnant( ofs ) \ - tot0 += decode[0+(ofs)] * hc[0+(ofs)]; - -#define stbir__2_coeff_remnant( ofs ) \ - tot0 += decode[0+(ofs)] * hc[0+(ofs)]; \ - tot1 += decode[1+(ofs)] * hc[1+(ofs)]; \ - -#define stbir__3_coeff_remnant( ofs ) \ - tot0 += decode[0+(ofs)] * hc[0+(ofs)]; \ - tot1 += decode[1+(ofs)] * hc[1+(ofs)]; \ - tot2 += decode[2+(ofs)] * hc[2+(ofs)]; - -#define stbir__store_output() \ - output[0] = (tot0+tot2)+(tot1+tot3); \ - horizontal_coefficients += coefficient_width; \ - ++horizontal_contributors; \ - output += 1; - -#endif - -#define STBIR__horizontal_channels 1 -#define STB_IMAGE_RESIZE_DO_HORIZONTALS -#include STBIR__HEADER_FILENAME - -#ifdef STBIR_SIMD - -#define stbir__1_coeff_only() \ - stbir__simdf tot,c,d; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load1z( c, hc ); \ - stbir__simdf_0123to0011( c, c ); \ - stbir__simdf_load2( d, decode ); \ - stbir__simdf_mult( tot, d, c ); - -#define stbir__2_coeff_only() \ - stbir__simdf tot,c; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load2( c, hc ); \ - stbir__simdf_0123to0011( c, c ); \ - stbir__simdf_mult_mem( tot, c, decode ); - -#define stbir__3_coeff_only() \ - stbir__simdf tot,c,cs,d; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load( cs, hc ); \ - stbir__simdf_0123to0011( c, cs ); \ - stbir__simdf_mult_mem( tot, c, decode ); \ - stbir__simdf_0123to2222( c, cs ); \ - stbir__simdf_load2z( d, decode+4 ); \ - stbir__simdf_madd( tot, tot, d, c ); - -#define stbir__store_output_tiny() \ - stbir__simdf_0123to2301( c, tot ); \ - stbir__simdf_add( tot, tot, c ); \ - stbir__simdf_store2( output, tot ); \ - horizontal_coefficients += coefficient_width; \ - ++horizontal_contributors; \ - output += 2; - -#ifdef STBIR_SIMD8 - -#define stbir__4_coeff_start() \ - stbir__simdf8 tot0,c,cs; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf8_load4b( cs, hc ); \ - stbir__simdf8_0123to00112233( c, cs ); \ - stbir__simdf8_mult_mem( tot0, c, decode ); - -#define stbir__4_coeff_continue_from_4( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf8_load4b( cs, hc + (ofs) ); \ - stbir__simdf8_0123to00112233( c, cs ); \ - stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*2 ); - -#define stbir__1_coeff_remnant( ofs ) \ - { stbir__simdf t,d; \ - stbir__simdf_load1z( t, hc + (ofs) ); \ - stbir__simdf_load2( d, decode + (ofs) * 2 ); \ - stbir__simdf_0123to0011( t, t ); \ - stbir__simdf_mult( t, t, d ); \ - stbir__simdf8_add4( tot0, tot0, t ); } - -#define stbir__2_coeff_remnant( ofs ) \ - { stbir__simdf t; \ - stbir__simdf_load2( t, hc + (ofs) ); \ - stbir__simdf_0123to0011( t, t ); \ - stbir__simdf_mult_mem( t, t, decode+(ofs)*2 ); \ - stbir__simdf8_add4( tot0, tot0, t ); } - -#define stbir__3_coeff_remnant( ofs ) \ - { stbir__simdf8 d; \ - stbir__simdf8_load4b( cs, hc + (ofs) ); \ - stbir__simdf8_0123to00112233( c, cs ); \ - stbir__simdf8_load6z( d, decode+(ofs)*2 ); \ - stbir__simdf8_madd( tot0, tot0, c, d ); } - -#define stbir__store_output() \ - { stbir__simdf t,d; \ - stbir__simdf8_add4halves( t, stbir__if_simdf8_cast_to_simdf4(tot0), tot0 ); \ - stbir__simdf_0123to2301( d, t ); \ - stbir__simdf_add( t, t, d ); \ - stbir__simdf_store2( output, t ); \ - horizontal_coefficients += coefficient_width; \ - ++horizontal_contributors; \ - output += 2; } - -#else - -#define stbir__4_coeff_start() \ - stbir__simdf tot0,tot1,c,cs; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load( cs, hc ); \ - stbir__simdf_0123to0011( c, cs ); \ - stbir__simdf_mult_mem( tot0, c, decode ); \ - stbir__simdf_0123to2233( c, cs ); \ - stbir__simdf_mult_mem( tot1, c, decode+4 ); - -#define stbir__4_coeff_continue_from_4( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load( cs, hc + (ofs) ); \ - stbir__simdf_0123to0011( c, cs ); \ - stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*2 ); \ - stbir__simdf_0123to2233( c, cs ); \ - stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*2+4 ); - -#define stbir__1_coeff_remnant( ofs ) \ - { stbir__simdf d; \ - stbir__simdf_load1z( cs, hc + (ofs) ); \ - stbir__simdf_0123to0011( c, cs ); \ - stbir__simdf_load2( d, decode + (ofs) * 2 ); \ - stbir__simdf_madd( tot0, tot0, d, c ); } - -#define stbir__2_coeff_remnant( ofs ) \ - stbir__simdf_load2( cs, hc + (ofs) ); \ - stbir__simdf_0123to0011( c, cs ); \ - stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*2 ); - -#define stbir__3_coeff_remnant( ofs ) \ - { stbir__simdf d; \ - stbir__simdf_load( cs, hc + (ofs) ); \ - stbir__simdf_0123to0011( c, cs ); \ - stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*2 ); \ - stbir__simdf_0123to2222( c, cs ); \ - stbir__simdf_load2z( d, decode + (ofs) * 2 + 4 ); \ - stbir__simdf_madd( tot1, tot1, d, c ); } - -#define stbir__store_output() \ - stbir__simdf_add( tot0, tot0, tot1 ); \ - stbir__simdf_0123to2301( c, tot0 ); \ - stbir__simdf_add( tot0, tot0, c ); \ - stbir__simdf_store2( output, tot0 ); \ - horizontal_coefficients += coefficient_width; \ - ++horizontal_contributors; \ - output += 2; - -#endif - -#else - -#define stbir__1_coeff_only() \ - float tota,totb,c; \ - c = hc[0]; \ - tota = decode[0]*c; \ - totb = decode[1]*c; - -#define stbir__2_coeff_only() \ - float tota,totb,c; \ - c = hc[0]; \ - tota = decode[0]*c; \ - totb = decode[1]*c; \ - c = hc[1]; \ - tota += decode[2]*c; \ - totb += decode[3]*c; - -#define stbir__3_coeff_only() \ - float tota,totb,c; \ - c = hc[0]; \ - tota = decode[0]*c; \ - totb = decode[1]*c; \ - c = hc[2]; \ - tota += decode[4]*c; \ - totb += decode[5]*c; \ - c = hc[1]; \ - tota += decode[2]*c; \ - totb += decode[3]*c; - -#define stbir__store_output_tiny() \ - output[0] = tota; \ - output[1] = totb; \ - horizontal_coefficients += coefficient_width; \ - ++horizontal_contributors; \ - output += 2; - -#define stbir__4_coeff_start() \ - float tota0,tota1,tota2,tota3,totb0,totb1,totb2,totb3,c; \ - c = hc[0]; \ - tota0 = decode[0]*c; \ - totb0 = decode[1]*c; \ - c = hc[1]; \ - tota1 = decode[2]*c; \ - totb1 = decode[3]*c; \ - c = hc[2]; \ - tota2 = decode[4]*c; \ - totb2 = decode[5]*c; \ - c = hc[3]; \ - tota3 = decode[6]*c; \ - totb3 = decode[7]*c; - -#define stbir__4_coeff_continue_from_4( ofs ) \ - c = hc[0+(ofs)]; \ - tota0 += decode[0+(ofs)*2]*c; \ - totb0 += decode[1+(ofs)*2]*c; \ - c = hc[1+(ofs)]; \ - tota1 += decode[2+(ofs)*2]*c; \ - totb1 += decode[3+(ofs)*2]*c; \ - c = hc[2+(ofs)]; \ - tota2 += decode[4+(ofs)*2]*c; \ - totb2 += decode[5+(ofs)*2]*c; \ - c = hc[3+(ofs)]; \ - tota3 += decode[6+(ofs)*2]*c; \ - totb3 += decode[7+(ofs)*2]*c; - -#define stbir__1_coeff_remnant( ofs ) \ - c = hc[0+(ofs)]; \ - tota0 += decode[0+(ofs)*2] * c; \ - totb0 += decode[1+(ofs)*2] * c; - -#define stbir__2_coeff_remnant( ofs ) \ - c = hc[0+(ofs)]; \ - tota0 += decode[0+(ofs)*2] * c; \ - totb0 += decode[1+(ofs)*2] * c; \ - c = hc[1+(ofs)]; \ - tota1 += decode[2+(ofs)*2] * c; \ - totb1 += decode[3+(ofs)*2] * c; - -#define stbir__3_coeff_remnant( ofs ) \ - c = hc[0+(ofs)]; \ - tota0 += decode[0+(ofs)*2] * c; \ - totb0 += decode[1+(ofs)*2] * c; \ - c = hc[1+(ofs)]; \ - tota1 += decode[2+(ofs)*2] * c; \ - totb1 += decode[3+(ofs)*2] * c; \ - c = hc[2+(ofs)]; \ - tota2 += decode[4+(ofs)*2] * c; \ - totb2 += decode[5+(ofs)*2] * c; - -#define stbir__store_output() \ - output[0] = (tota0+tota2)+(tota1+tota3); \ - output[1] = (totb0+totb2)+(totb1+totb3); \ - horizontal_coefficients += coefficient_width; \ - ++horizontal_contributors; \ - output += 2; - -#endif - -#define STBIR__horizontal_channels 2 -#define STB_IMAGE_RESIZE_DO_HORIZONTALS -#include STBIR__HEADER_FILENAME - -#ifdef STBIR_SIMD - -#define stbir__1_coeff_only() \ - stbir__simdf tot,c,d; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load1z( c, hc ); \ - stbir__simdf_0123to0001( c, c ); \ - stbir__simdf_load( d, decode ); \ - stbir__simdf_mult( tot, d, c ); - -#define stbir__2_coeff_only() \ - stbir__simdf tot,c,cs,d; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load2( cs, hc ); \ - stbir__simdf_0123to0000( c, cs ); \ - stbir__simdf_load( d, decode ); \ - stbir__simdf_mult( tot, d, c ); \ - stbir__simdf_0123to1111( c, cs ); \ - stbir__simdf_load( d, decode+3 ); \ - stbir__simdf_madd( tot, tot, d, c ); - -#define stbir__3_coeff_only() \ - stbir__simdf tot,c,d,cs; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load( cs, hc ); \ - stbir__simdf_0123to0000( c, cs ); \ - stbir__simdf_load( d, decode ); \ - stbir__simdf_mult( tot, d, c ); \ - stbir__simdf_0123to1111( c, cs ); \ - stbir__simdf_load( d, decode+3 ); \ - stbir__simdf_madd( tot, tot, d, c ); \ - stbir__simdf_0123to2222( c, cs ); \ - stbir__simdf_load( d, decode+6 ); \ - stbir__simdf_madd( tot, tot, d, c ); - -#define stbir__store_output_tiny() \ - stbir__simdf_store2( output, tot ); \ - stbir__simdf_0123to2301( tot, tot ); \ - stbir__simdf_store1( output+2, tot ); \ - horizontal_coefficients += coefficient_width; \ - ++horizontal_contributors; \ - output += 3; - -#ifdef STBIR_SIMD8 - -#define stbir__4_coeff_start() \ - stbir__simdf8 tot0,tot1,c,cs; stbir__simdf t; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf8_load4b( cs, hc ); \ - stbir__simdf8_0123to00001111( c, cs ); \ - stbir__simdf8_mult_mem( tot0, c, decode - 1 ); \ - stbir__simdf8_0123to22223333( c, cs ); \ - stbir__simdf8_mult_mem( tot1, c, decode+6 - 1 ); - -#define stbir__4_coeff_continue_from_4( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf8_load4b( cs, hc + (ofs) ); \ - stbir__simdf8_0123to00001111( c, cs ); \ - stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*3 - 1 ); \ - stbir__simdf8_0123to22223333( c, cs ); \ - stbir__simdf8_madd_mem( tot1, tot1, c, decode+(ofs)*3 + 6 - 1 ); - -#define stbir__1_coeff_remnant( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load1rep4( t, hc + (ofs) ); \ - stbir__simdf8_madd_mem4( tot0, tot0, t, decode+(ofs)*3 - 1 ); - -#define stbir__2_coeff_remnant( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf8_load4b( cs, hc + (ofs) - 2 ); \ - stbir__simdf8_0123to22223333( c, cs ); \ - stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*3 - 1 ); - - #define stbir__3_coeff_remnant( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf8_load4b( cs, hc + (ofs) ); \ - stbir__simdf8_0123to00001111( c, cs ); \ - stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*3 - 1 ); \ - stbir__simdf8_0123to2222( t, cs ); \ - stbir__simdf8_madd_mem4( tot1, tot1, t, decode+(ofs)*3 + 6 - 1 ); - -#define stbir__store_output() \ - stbir__simdf8_add( tot0, tot0, tot1 ); \ - stbir__simdf_0123to1230( t, stbir__if_simdf8_cast_to_simdf4( tot0 ) ); \ - stbir__simdf8_add4halves( t, t, tot0 ); \ - horizontal_coefficients += coefficient_width; \ - ++horizontal_contributors; \ - output += 3; \ - if ( output < output_end ) \ - { \ - stbir__simdf_store( output-3, t ); \ - continue; \ - } \ - { stbir__simdf tt; stbir__simdf_0123to2301( tt, t ); \ - stbir__simdf_store2( output-3, t ); \ - stbir__simdf_store1( output+2-3, tt ); } \ - break; - -#else - -#define stbir__4_coeff_start() \ - stbir__simdf tot0,tot1,tot2,c,cs; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load( cs, hc ); \ - stbir__simdf_0123to0001( c, cs ); \ - stbir__simdf_mult_mem( tot0, c, decode ); \ - stbir__simdf_0123to1122( c, cs ); \ - stbir__simdf_mult_mem( tot1, c, decode+4 ); \ - stbir__simdf_0123to2333( c, cs ); \ - stbir__simdf_mult_mem( tot2, c, decode+8 ); - -#define stbir__4_coeff_continue_from_4( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load( cs, hc + (ofs) ); \ - stbir__simdf_0123to0001( c, cs ); \ - stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*3 ); \ - stbir__simdf_0123to1122( c, cs ); \ - stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*3+4 ); \ - stbir__simdf_0123to2333( c, cs ); \ - stbir__simdf_madd_mem( tot2, tot2, c, decode+(ofs)*3+8 ); - -#define stbir__1_coeff_remnant( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load1z( c, hc + (ofs) ); \ - stbir__simdf_0123to0001( c, c ); \ - stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*3 ); - -#define stbir__2_coeff_remnant( ofs ) \ - { stbir__simdf d; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load2z( cs, hc + (ofs) ); \ - stbir__simdf_0123to0001( c, cs ); \ - stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*3 ); \ - stbir__simdf_0123to1122( c, cs ); \ - stbir__simdf_load2z( d, decode+(ofs)*3+4 ); \ - stbir__simdf_madd( tot1, tot1, c, d ); } - -#define stbir__3_coeff_remnant( ofs ) \ - { stbir__simdf d; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load( cs, hc + (ofs) ); \ - stbir__simdf_0123to0001( c, cs ); \ - stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*3 ); \ - stbir__simdf_0123to1122( c, cs ); \ - stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*3+4 ); \ - stbir__simdf_0123to2222( c, cs ); \ - stbir__simdf_load1z( d, decode+(ofs)*3+8 ); \ - stbir__simdf_madd( tot2, tot2, c, d ); } - -#define stbir__store_output() \ - stbir__simdf_0123ABCDto3ABx( c, tot0, tot1 ); \ - stbir__simdf_0123ABCDto23Ax( cs, tot1, tot2 ); \ - stbir__simdf_0123to1230( tot2, tot2 ); \ - stbir__simdf_add( tot0, tot0, cs ); \ - stbir__simdf_add( c, c, tot2 ); \ - stbir__simdf_add( tot0, tot0, c ); \ - horizontal_coefficients += coefficient_width; \ - ++horizontal_contributors; \ - output += 3; \ - if ( output < output_end ) \ - { \ - stbir__simdf_store( output-3, tot0 ); \ - continue; \ - } \ - stbir__simdf_0123to2301( tot1, tot0 ); \ - stbir__simdf_store2( output-3, tot0 ); \ - stbir__simdf_store1( output+2-3, tot1 ); \ - break; - -#endif - -#else - -#define stbir__1_coeff_only() \ - float tot0, tot1, tot2, c; \ - c = hc[0]; \ - tot0 = decode[0]*c; \ - tot1 = decode[1]*c; \ - tot2 = decode[2]*c; - -#define stbir__2_coeff_only() \ - float tot0, tot1, tot2, c; \ - c = hc[0]; \ - tot0 = decode[0]*c; \ - tot1 = decode[1]*c; \ - tot2 = decode[2]*c; \ - c = hc[1]; \ - tot0 += decode[3]*c; \ - tot1 += decode[4]*c; \ - tot2 += decode[5]*c; - -#define stbir__3_coeff_only() \ - float tot0, tot1, tot2, c; \ - c = hc[0]; \ - tot0 = decode[0]*c; \ - tot1 = decode[1]*c; \ - tot2 = decode[2]*c; \ - c = hc[1]; \ - tot0 += decode[3]*c; \ - tot1 += decode[4]*c; \ - tot2 += decode[5]*c; \ - c = hc[2]; \ - tot0 += decode[6]*c; \ - tot1 += decode[7]*c; \ - tot2 += decode[8]*c; - -#define stbir__store_output_tiny() \ - output[0] = tot0; \ - output[1] = tot1; \ - output[2] = tot2; \ - horizontal_coefficients += coefficient_width; \ - ++horizontal_contributors; \ - output += 3; - -#define stbir__4_coeff_start() \ - float tota0,tota1,tota2,totb0,totb1,totb2,totc0,totc1,totc2,totd0,totd1,totd2,c; \ - c = hc[0]; \ - tota0 = decode[0]*c; \ - tota1 = decode[1]*c; \ - tota2 = decode[2]*c; \ - c = hc[1]; \ - totb0 = decode[3]*c; \ - totb1 = decode[4]*c; \ - totb2 = decode[5]*c; \ - c = hc[2]; \ - totc0 = decode[6]*c; \ - totc1 = decode[7]*c; \ - totc2 = decode[8]*c; \ - c = hc[3]; \ - totd0 = decode[9]*c; \ - totd1 = decode[10]*c; \ - totd2 = decode[11]*c; - -#define stbir__4_coeff_continue_from_4( ofs ) \ - c = hc[0+(ofs)]; \ - tota0 += decode[0+(ofs)*3]*c; \ - tota1 += decode[1+(ofs)*3]*c; \ - tota2 += decode[2+(ofs)*3]*c; \ - c = hc[1+(ofs)]; \ - totb0 += decode[3+(ofs)*3]*c; \ - totb1 += decode[4+(ofs)*3]*c; \ - totb2 += decode[5+(ofs)*3]*c; \ - c = hc[2+(ofs)]; \ - totc0 += decode[6+(ofs)*3]*c; \ - totc1 += decode[7+(ofs)*3]*c; \ - totc2 += decode[8+(ofs)*3]*c; \ - c = hc[3+(ofs)]; \ - totd0 += decode[9+(ofs)*3]*c; \ - totd1 += decode[10+(ofs)*3]*c; \ - totd2 += decode[11+(ofs)*3]*c; - -#define stbir__1_coeff_remnant( ofs ) \ - c = hc[0+(ofs)]; \ - tota0 += decode[0+(ofs)*3]*c; \ - tota1 += decode[1+(ofs)*3]*c; \ - tota2 += decode[2+(ofs)*3]*c; - -#define stbir__2_coeff_remnant( ofs ) \ - c = hc[0+(ofs)]; \ - tota0 += decode[0+(ofs)*3]*c; \ - tota1 += decode[1+(ofs)*3]*c; \ - tota2 += decode[2+(ofs)*3]*c; \ - c = hc[1+(ofs)]; \ - totb0 += decode[3+(ofs)*3]*c; \ - totb1 += decode[4+(ofs)*3]*c; \ - totb2 += decode[5+(ofs)*3]*c; \ - -#define stbir__3_coeff_remnant( ofs ) \ - c = hc[0+(ofs)]; \ - tota0 += decode[0+(ofs)*3]*c; \ - tota1 += decode[1+(ofs)*3]*c; \ - tota2 += decode[2+(ofs)*3]*c; \ - c = hc[1+(ofs)]; \ - totb0 += decode[3+(ofs)*3]*c; \ - totb1 += decode[4+(ofs)*3]*c; \ - totb2 += decode[5+(ofs)*3]*c; \ - c = hc[2+(ofs)]; \ - totc0 += decode[6+(ofs)*3]*c; \ - totc1 += decode[7+(ofs)*3]*c; \ - totc2 += decode[8+(ofs)*3]*c; - -#define stbir__store_output() \ - output[0] = (tota0+totc0)+(totb0+totd0); \ - output[1] = (tota1+totc1)+(totb1+totd1); \ - output[2] = (tota2+totc2)+(totb2+totd2); \ - horizontal_coefficients += coefficient_width; \ - ++horizontal_contributors; \ - output += 3; - -#endif - -#define STBIR__horizontal_channels 3 -#define STB_IMAGE_RESIZE_DO_HORIZONTALS -#include STBIR__HEADER_FILENAME - -#ifdef STBIR_SIMD - -#define stbir__1_coeff_only() \ - stbir__simdf tot,c; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load1( c, hc ); \ - stbir__simdf_0123to0000( c, c ); \ - stbir__simdf_mult_mem( tot, c, decode ); - -#define stbir__2_coeff_only() \ - stbir__simdf tot,c,cs; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load2( cs, hc ); \ - stbir__simdf_0123to0000( c, cs ); \ - stbir__simdf_mult_mem( tot, c, decode ); \ - stbir__simdf_0123to1111( c, cs ); \ - stbir__simdf_madd_mem( tot, tot, c, decode+4 ); - -#define stbir__3_coeff_only() \ - stbir__simdf tot,c,cs; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load( cs, hc ); \ - stbir__simdf_0123to0000( c, cs ); \ - stbir__simdf_mult_mem( tot, c, decode ); \ - stbir__simdf_0123to1111( c, cs ); \ - stbir__simdf_madd_mem( tot, tot, c, decode+4 ); \ - stbir__simdf_0123to2222( c, cs ); \ - stbir__simdf_madd_mem( tot, tot, c, decode+8 ); - -#define stbir__store_output_tiny() \ - stbir__simdf_store( output, tot ); \ - horizontal_coefficients += coefficient_width; \ - ++horizontal_contributors; \ - output += 4; - -#ifdef STBIR_SIMD8 - -#define stbir__4_coeff_start() \ - stbir__simdf8 tot0,c,cs; stbir__simdf t; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf8_load4b( cs, hc ); \ - stbir__simdf8_0123to00001111( c, cs ); \ - stbir__simdf8_mult_mem( tot0, c, decode ); \ - stbir__simdf8_0123to22223333( c, cs ); \ - stbir__simdf8_madd_mem( tot0, tot0, c, decode+8 ); - -#define stbir__4_coeff_continue_from_4( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf8_load4b( cs, hc + (ofs) ); \ - stbir__simdf8_0123to00001111( c, cs ); \ - stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*4 ); \ - stbir__simdf8_0123to22223333( c, cs ); \ - stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*4+8 ); - -#define stbir__1_coeff_remnant( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load1rep4( t, hc + (ofs) ); \ - stbir__simdf8_madd_mem4( tot0, tot0, t, decode+(ofs)*4 ); - -#define stbir__2_coeff_remnant( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf8_load4b( cs, hc + (ofs) - 2 ); \ - stbir__simdf8_0123to22223333( c, cs ); \ - stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*4 ); - - #define stbir__3_coeff_remnant( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf8_load4b( cs, hc + (ofs) ); \ - stbir__simdf8_0123to00001111( c, cs ); \ - stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*4 ); \ - stbir__simdf8_0123to2222( t, cs ); \ - stbir__simdf8_madd_mem4( tot0, tot0, t, decode+(ofs)*4+8 ); - -#define stbir__store_output() \ - stbir__simdf8_add4halves( t, stbir__if_simdf8_cast_to_simdf4(tot0), tot0 ); \ - stbir__simdf_store( output, t ); \ - horizontal_coefficients += coefficient_width; \ - ++horizontal_contributors; \ - output += 4; - -#else - -#define stbir__4_coeff_start() \ - stbir__simdf tot0,tot1,c,cs; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load( cs, hc ); \ - stbir__simdf_0123to0000( c, cs ); \ - stbir__simdf_mult_mem( tot0, c, decode ); \ - stbir__simdf_0123to1111( c, cs ); \ - stbir__simdf_mult_mem( tot1, c, decode+4 ); \ - stbir__simdf_0123to2222( c, cs ); \ - stbir__simdf_madd_mem( tot0, tot0, c, decode+8 ); \ - stbir__simdf_0123to3333( c, cs ); \ - stbir__simdf_madd_mem( tot1, tot1, c, decode+12 ); - -#define stbir__4_coeff_continue_from_4( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load( cs, hc + (ofs) ); \ - stbir__simdf_0123to0000( c, cs ); \ - stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*4 ); \ - stbir__simdf_0123to1111( c, cs ); \ - stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*4+4 ); \ - stbir__simdf_0123to2222( c, cs ); \ - stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*4+8 ); \ - stbir__simdf_0123to3333( c, cs ); \ - stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*4+12 ); - -#define stbir__1_coeff_remnant( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load1( c, hc + (ofs) ); \ - stbir__simdf_0123to0000( c, c ); \ - stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*4 ); - -#define stbir__2_coeff_remnant( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load2( cs, hc + (ofs) ); \ - stbir__simdf_0123to0000( c, cs ); \ - stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*4 ); \ - stbir__simdf_0123to1111( c, cs ); \ - stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*4+4 ); - -#define stbir__3_coeff_remnant( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load( cs, hc + (ofs) ); \ - stbir__simdf_0123to0000( c, cs ); \ - stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*4 ); \ - stbir__simdf_0123to1111( c, cs ); \ - stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*4+4 ); \ - stbir__simdf_0123to2222( c, cs ); \ - stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*4+8 ); - -#define stbir__store_output() \ - stbir__simdf_add( tot0, tot0, tot1 ); \ - stbir__simdf_store( output, tot0 ); \ - horizontal_coefficients += coefficient_width; \ - ++horizontal_contributors; \ - output += 4; - -#endif - -#else - -#define stbir__1_coeff_only() \ - float p0,p1,p2,p3,c; \ - STBIR_SIMD_NO_UNROLL(decode); \ - c = hc[0]; \ - p0 = decode[0] * c; \ - p1 = decode[1] * c; \ - p2 = decode[2] * c; \ - p3 = decode[3] * c; - -#define stbir__2_coeff_only() \ - float p0,p1,p2,p3,c; \ - STBIR_SIMD_NO_UNROLL(decode); \ - c = hc[0]; \ - p0 = decode[0] * c; \ - p1 = decode[1] * c; \ - p2 = decode[2] * c; \ - p3 = decode[3] * c; \ - c = hc[1]; \ - p0 += decode[4] * c; \ - p1 += decode[5] * c; \ - p2 += decode[6] * c; \ - p3 += decode[7] * c; - -#define stbir__3_coeff_only() \ - float p0,p1,p2,p3,c; \ - STBIR_SIMD_NO_UNROLL(decode); \ - c = hc[0]; \ - p0 = decode[0] * c; \ - p1 = decode[1] * c; \ - p2 = decode[2] * c; \ - p3 = decode[3] * c; \ - c = hc[1]; \ - p0 += decode[4] * c; \ - p1 += decode[5] * c; \ - p2 += decode[6] * c; \ - p3 += decode[7] * c; \ - c = hc[2]; \ - p0 += decode[8] * c; \ - p1 += decode[9] * c; \ - p2 += decode[10] * c; \ - p3 += decode[11] * c; - -#define stbir__store_output_tiny() \ - output[0] = p0; \ - output[1] = p1; \ - output[2] = p2; \ - output[3] = p3; \ - horizontal_coefficients += coefficient_width; \ - ++horizontal_contributors; \ - output += 4; - -#define stbir__4_coeff_start() \ - float x0,x1,x2,x3,y0,y1,y2,y3,c; \ - STBIR_SIMD_NO_UNROLL(decode); \ - c = hc[0]; \ - x0 = decode[0] * c; \ - x1 = decode[1] * c; \ - x2 = decode[2] * c; \ - x3 = decode[3] * c; \ - c = hc[1]; \ - y0 = decode[4] * c; \ - y1 = decode[5] * c; \ - y2 = decode[6] * c; \ - y3 = decode[7] * c; \ - c = hc[2]; \ - x0 += decode[8] * c; \ - x1 += decode[9] * c; \ - x2 += decode[10] * c; \ - x3 += decode[11] * c; \ - c = hc[3]; \ - y0 += decode[12] * c; \ - y1 += decode[13] * c; \ - y2 += decode[14] * c; \ - y3 += decode[15] * c; - -#define stbir__4_coeff_continue_from_4( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - c = hc[0+(ofs)]; \ - x0 += decode[0+(ofs)*4] * c; \ - x1 += decode[1+(ofs)*4] * c; \ - x2 += decode[2+(ofs)*4] * c; \ - x3 += decode[3+(ofs)*4] * c; \ - c = hc[1+(ofs)]; \ - y0 += decode[4+(ofs)*4] * c; \ - y1 += decode[5+(ofs)*4] * c; \ - y2 += decode[6+(ofs)*4] * c; \ - y3 += decode[7+(ofs)*4] * c; \ - c = hc[2+(ofs)]; \ - x0 += decode[8+(ofs)*4] * c; \ - x1 += decode[9+(ofs)*4] * c; \ - x2 += decode[10+(ofs)*4] * c; \ - x3 += decode[11+(ofs)*4] * c; \ - c = hc[3+(ofs)]; \ - y0 += decode[12+(ofs)*4] * c; \ - y1 += decode[13+(ofs)*4] * c; \ - y2 += decode[14+(ofs)*4] * c; \ - y3 += decode[15+(ofs)*4] * c; - -#define stbir__1_coeff_remnant( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - c = hc[0+(ofs)]; \ - x0 += decode[0+(ofs)*4] * c; \ - x1 += decode[1+(ofs)*4] * c; \ - x2 += decode[2+(ofs)*4] * c; \ - x3 += decode[3+(ofs)*4] * c; - -#define stbir__2_coeff_remnant( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - c = hc[0+(ofs)]; \ - x0 += decode[0+(ofs)*4] * c; \ - x1 += decode[1+(ofs)*4] * c; \ - x2 += decode[2+(ofs)*4] * c; \ - x3 += decode[3+(ofs)*4] * c; \ - c = hc[1+(ofs)]; \ - y0 += decode[4+(ofs)*4] * c; \ - y1 += decode[5+(ofs)*4] * c; \ - y2 += decode[6+(ofs)*4] * c; \ - y3 += decode[7+(ofs)*4] * c; - -#define stbir__3_coeff_remnant( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - c = hc[0+(ofs)]; \ - x0 += decode[0+(ofs)*4] * c; \ - x1 += decode[1+(ofs)*4] * c; \ - x2 += decode[2+(ofs)*4] * c; \ - x3 += decode[3+(ofs)*4] * c; \ - c = hc[1+(ofs)]; \ - y0 += decode[4+(ofs)*4] * c; \ - y1 += decode[5+(ofs)*4] * c; \ - y2 += decode[6+(ofs)*4] * c; \ - y3 += decode[7+(ofs)*4] * c; \ - c = hc[2+(ofs)]; \ - x0 += decode[8+(ofs)*4] * c; \ - x1 += decode[9+(ofs)*4] * c; \ - x2 += decode[10+(ofs)*4] * c; \ - x3 += decode[11+(ofs)*4] * c; - -#define stbir__store_output() \ - output[0] = x0 + y0; \ - output[1] = x1 + y1; \ - output[2] = x2 + y2; \ - output[3] = x3 + y3; \ - horizontal_coefficients += coefficient_width; \ - ++horizontal_contributors; \ - output += 4; - -#endif - -#define STBIR__horizontal_channels 4 -#define STB_IMAGE_RESIZE_DO_HORIZONTALS -#include STBIR__HEADER_FILENAME - -#ifdef STBIR_SIMD - -#define stbir__1_coeff_only() \ - stbir__simdf tot0,tot1,c; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load1( c, hc ); \ - stbir__simdf_0123to0000( c, c ); \ - stbir__simdf_mult_mem( tot0, c, decode ); \ - stbir__simdf_mult_mem( tot1, c, decode+3 ); - -#define stbir__2_coeff_only() \ - stbir__simdf tot0,tot1,c,cs; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load2( cs, hc ); \ - stbir__simdf_0123to0000( c, cs ); \ - stbir__simdf_mult_mem( tot0, c, decode ); \ - stbir__simdf_mult_mem( tot1, c, decode+3 ); \ - stbir__simdf_0123to1111( c, cs ); \ - stbir__simdf_madd_mem( tot0, tot0, c, decode+7 ); \ - stbir__simdf_madd_mem( tot1, tot1, c,decode+10 ); - -#define stbir__3_coeff_only() \ - stbir__simdf tot0,tot1,c,cs; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load( cs, hc ); \ - stbir__simdf_0123to0000( c, cs ); \ - stbir__simdf_mult_mem( tot0, c, decode ); \ - stbir__simdf_mult_mem( tot1, c, decode+3 ); \ - stbir__simdf_0123to1111( c, cs ); \ - stbir__simdf_madd_mem( tot0, tot0, c, decode+7 ); \ - stbir__simdf_madd_mem( tot1, tot1, c, decode+10 ); \ - stbir__simdf_0123to2222( c, cs ); \ - stbir__simdf_madd_mem( tot0, tot0, c, decode+14 ); \ - stbir__simdf_madd_mem( tot1, tot1, c, decode+17 ); - -#define stbir__store_output_tiny() \ - stbir__simdf_store( output+3, tot1 ); \ - stbir__simdf_store( output, tot0 ); \ - horizontal_coefficients += coefficient_width; \ - ++horizontal_contributors; \ - output += 7; - -#ifdef STBIR_SIMD8 - -#define stbir__4_coeff_start() \ - stbir__simdf8 tot0,tot1,c,cs; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf8_load4b( cs, hc ); \ - stbir__simdf8_0123to00000000( c, cs ); \ - stbir__simdf8_mult_mem( tot0, c, decode ); \ - stbir__simdf8_0123to11111111( c, cs ); \ - stbir__simdf8_mult_mem( tot1, c, decode+7 ); \ - stbir__simdf8_0123to22222222( c, cs ); \ - stbir__simdf8_madd_mem( tot0, tot0, c, decode+14 ); \ - stbir__simdf8_0123to33333333( c, cs ); \ - stbir__simdf8_madd_mem( tot1, tot1, c, decode+21 ); - -#define stbir__4_coeff_continue_from_4( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf8_load4b( cs, hc + (ofs) ); \ - stbir__simdf8_0123to00000000( c, cs ); \ - stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*7 ); \ - stbir__simdf8_0123to11111111( c, cs ); \ - stbir__simdf8_madd_mem( tot1, tot1, c, decode+(ofs)*7+7 ); \ - stbir__simdf8_0123to22222222( c, cs ); \ - stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*7+14 ); \ - stbir__simdf8_0123to33333333( c, cs ); \ - stbir__simdf8_madd_mem( tot1, tot1, c, decode+(ofs)*7+21 ); - -#define stbir__1_coeff_remnant( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf8_load1b( c, hc + (ofs) ); \ - stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*7 ); - -#define stbir__2_coeff_remnant( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf8_load1b( c, hc + (ofs) ); \ - stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*7 ); \ - stbir__simdf8_load1b( c, hc + (ofs)+1 ); \ - stbir__simdf8_madd_mem( tot1, tot1, c, decode+(ofs)*7+7 ); - -#define stbir__3_coeff_remnant( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf8_load4b( cs, hc + (ofs) ); \ - stbir__simdf8_0123to00000000( c, cs ); \ - stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*7 ); \ - stbir__simdf8_0123to11111111( c, cs ); \ - stbir__simdf8_madd_mem( tot1, tot1, c, decode+(ofs)*7+7 ); \ - stbir__simdf8_0123to22222222( c, cs ); \ - stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*7+14 ); - -#define stbir__store_output() \ - stbir__simdf8_add( tot0, tot0, tot1 ); \ - horizontal_coefficients += coefficient_width; \ - ++horizontal_contributors; \ - output += 7; \ - if ( output < output_end ) \ - { \ - stbir__simdf8_store( output-7, tot0 ); \ - continue; \ - } \ - stbir__simdf_store( output-7+3, stbir__simdf_swiz(stbir__simdf8_gettop4(tot0),0,0,1,2) ); \ - stbir__simdf_store( output-7, stbir__if_simdf8_cast_to_simdf4(tot0) ); \ - break; - -#else - -#define stbir__4_coeff_start() \ - stbir__simdf tot0,tot1,tot2,tot3,c,cs; \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load( cs, hc ); \ - stbir__simdf_0123to0000( c, cs ); \ - stbir__simdf_mult_mem( tot0, c, decode ); \ - stbir__simdf_mult_mem( tot1, c, decode+3 ); \ - stbir__simdf_0123to1111( c, cs ); \ - stbir__simdf_mult_mem( tot2, c, decode+7 ); \ - stbir__simdf_mult_mem( tot3, c, decode+10 ); \ - stbir__simdf_0123to2222( c, cs ); \ - stbir__simdf_madd_mem( tot0, tot0, c, decode+14 ); \ - stbir__simdf_madd_mem( tot1, tot1, c, decode+17 ); \ - stbir__simdf_0123to3333( c, cs ); \ - stbir__simdf_madd_mem( tot2, tot2, c, decode+21 ); \ - stbir__simdf_madd_mem( tot3, tot3, c, decode+24 ); - -#define stbir__4_coeff_continue_from_4( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load( cs, hc + (ofs) ); \ - stbir__simdf_0123to0000( c, cs ); \ - stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*7 ); \ - stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*7+3 ); \ - stbir__simdf_0123to1111( c, cs ); \ - stbir__simdf_madd_mem( tot2, tot2, c, decode+(ofs)*7+7 ); \ - stbir__simdf_madd_mem( tot3, tot3, c, decode+(ofs)*7+10 ); \ - stbir__simdf_0123to2222( c, cs ); \ - stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*7+14 ); \ - stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*7+17 ); \ - stbir__simdf_0123to3333( c, cs ); \ - stbir__simdf_madd_mem( tot2, tot2, c, decode+(ofs)*7+21 ); \ - stbir__simdf_madd_mem( tot3, tot3, c, decode+(ofs)*7+24 ); - -#define stbir__1_coeff_remnant( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load1( c, hc + (ofs) ); \ - stbir__simdf_0123to0000( c, c ); \ - stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*7 ); \ - stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*7+3 ); \ - -#define stbir__2_coeff_remnant( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load2( cs, hc + (ofs) ); \ - stbir__simdf_0123to0000( c, cs ); \ - stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*7 ); \ - stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*7+3 ); \ - stbir__simdf_0123to1111( c, cs ); \ - stbir__simdf_madd_mem( tot2, tot2, c, decode+(ofs)*7+7 ); \ - stbir__simdf_madd_mem( tot3, tot3, c, decode+(ofs)*7+10 ); - -#define stbir__3_coeff_remnant( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - stbir__simdf_load( cs, hc + (ofs) ); \ - stbir__simdf_0123to0000( c, cs ); \ - stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*7 ); \ - stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*7+3 ); \ - stbir__simdf_0123to1111( c, cs ); \ - stbir__simdf_madd_mem( tot2, tot2, c, decode+(ofs)*7+7 ); \ - stbir__simdf_madd_mem( tot3, tot3, c, decode+(ofs)*7+10 ); \ - stbir__simdf_0123to2222( c, cs ); \ - stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*7+14 ); \ - stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*7+17 ); - -#define stbir__store_output() \ - stbir__simdf_add( tot0, tot0, tot2 ); \ - stbir__simdf_add( tot1, tot1, tot3 ); \ - stbir__simdf_store( output+3, tot1 ); \ - stbir__simdf_store( output, tot0 ); \ - horizontal_coefficients += coefficient_width; \ - ++horizontal_contributors; \ - output += 7; - -#endif - -#else - -#define stbir__1_coeff_only() \ - float tot0, tot1, tot2, tot3, tot4, tot5, tot6, c; \ - c = hc[0]; \ - tot0 = decode[0]*c; \ - tot1 = decode[1]*c; \ - tot2 = decode[2]*c; \ - tot3 = decode[3]*c; \ - tot4 = decode[4]*c; \ - tot5 = decode[5]*c; \ - tot6 = decode[6]*c; - -#define stbir__2_coeff_only() \ - float tot0, tot1, tot2, tot3, tot4, tot5, tot6, c; \ - c = hc[0]; \ - tot0 = decode[0]*c; \ - tot1 = decode[1]*c; \ - tot2 = decode[2]*c; \ - tot3 = decode[3]*c; \ - tot4 = decode[4]*c; \ - tot5 = decode[5]*c; \ - tot6 = decode[6]*c; \ - c = hc[1]; \ - tot0 += decode[7]*c; \ - tot1 += decode[8]*c; \ - tot2 += decode[9]*c; \ - tot3 += decode[10]*c; \ - tot4 += decode[11]*c; \ - tot5 += decode[12]*c; \ - tot6 += decode[13]*c; \ - -#define stbir__3_coeff_only() \ - float tot0, tot1, tot2, tot3, tot4, tot5, tot6, c; \ - c = hc[0]; \ - tot0 = decode[0]*c; \ - tot1 = decode[1]*c; \ - tot2 = decode[2]*c; \ - tot3 = decode[3]*c; \ - tot4 = decode[4]*c; \ - tot5 = decode[5]*c; \ - tot6 = decode[6]*c; \ - c = hc[1]; \ - tot0 += decode[7]*c; \ - tot1 += decode[8]*c; \ - tot2 += decode[9]*c; \ - tot3 += decode[10]*c; \ - tot4 += decode[11]*c; \ - tot5 += decode[12]*c; \ - tot6 += decode[13]*c; \ - c = hc[2]; \ - tot0 += decode[14]*c; \ - tot1 += decode[15]*c; \ - tot2 += decode[16]*c; \ - tot3 += decode[17]*c; \ - tot4 += decode[18]*c; \ - tot5 += decode[19]*c; \ - tot6 += decode[20]*c; \ - -#define stbir__store_output_tiny() \ - output[0] = tot0; \ - output[1] = tot1; \ - output[2] = tot2; \ - output[3] = tot3; \ - output[4] = tot4; \ - output[5] = tot5; \ - output[6] = tot6; \ - horizontal_coefficients += coefficient_width; \ - ++horizontal_contributors; \ - output += 7; - -#define stbir__4_coeff_start() \ - float x0,x1,x2,x3,x4,x5,x6,y0,y1,y2,y3,y4,y5,y6,c; \ - STBIR_SIMD_NO_UNROLL(decode); \ - c = hc[0]; \ - x0 = decode[0] * c; \ - x1 = decode[1] * c; \ - x2 = decode[2] * c; \ - x3 = decode[3] * c; \ - x4 = decode[4] * c; \ - x5 = decode[5] * c; \ - x6 = decode[6] * c; \ - c = hc[1]; \ - y0 = decode[7] * c; \ - y1 = decode[8] * c; \ - y2 = decode[9] * c; \ - y3 = decode[10] * c; \ - y4 = decode[11] * c; \ - y5 = decode[12] * c; \ - y6 = decode[13] * c; \ - c = hc[2]; \ - x0 += decode[14] * c; \ - x1 += decode[15] * c; \ - x2 += decode[16] * c; \ - x3 += decode[17] * c; \ - x4 += decode[18] * c; \ - x5 += decode[19] * c; \ - x6 += decode[20] * c; \ - c = hc[3]; \ - y0 += decode[21] * c; \ - y1 += decode[22] * c; \ - y2 += decode[23] * c; \ - y3 += decode[24] * c; \ - y4 += decode[25] * c; \ - y5 += decode[26] * c; \ - y6 += decode[27] * c; - -#define stbir__4_coeff_continue_from_4( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - c = hc[0+(ofs)]; \ - x0 += decode[0+(ofs)*7] * c; \ - x1 += decode[1+(ofs)*7] * c; \ - x2 += decode[2+(ofs)*7] * c; \ - x3 += decode[3+(ofs)*7] * c; \ - x4 += decode[4+(ofs)*7] * c; \ - x5 += decode[5+(ofs)*7] * c; \ - x6 += decode[6+(ofs)*7] * c; \ - c = hc[1+(ofs)]; \ - y0 += decode[7+(ofs)*7] * c; \ - y1 += decode[8+(ofs)*7] * c; \ - y2 += decode[9+(ofs)*7] * c; \ - y3 += decode[10+(ofs)*7] * c; \ - y4 += decode[11+(ofs)*7] * c; \ - y5 += decode[12+(ofs)*7] * c; \ - y6 += decode[13+(ofs)*7] * c; \ - c = hc[2+(ofs)]; \ - x0 += decode[14+(ofs)*7] * c; \ - x1 += decode[15+(ofs)*7] * c; \ - x2 += decode[16+(ofs)*7] * c; \ - x3 += decode[17+(ofs)*7] * c; \ - x4 += decode[18+(ofs)*7] * c; \ - x5 += decode[19+(ofs)*7] * c; \ - x6 += decode[20+(ofs)*7] * c; \ - c = hc[3+(ofs)]; \ - y0 += decode[21+(ofs)*7] * c; \ - y1 += decode[22+(ofs)*7] * c; \ - y2 += decode[23+(ofs)*7] * c; \ - y3 += decode[24+(ofs)*7] * c; \ - y4 += decode[25+(ofs)*7] * c; \ - y5 += decode[26+(ofs)*7] * c; \ - y6 += decode[27+(ofs)*7] * c; - -#define stbir__1_coeff_remnant( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - c = hc[0+(ofs)]; \ - x0 += decode[0+(ofs)*7] * c; \ - x1 += decode[1+(ofs)*7] * c; \ - x2 += decode[2+(ofs)*7] * c; \ - x3 += decode[3+(ofs)*7] * c; \ - x4 += decode[4+(ofs)*7] * c; \ - x5 += decode[5+(ofs)*7] * c; \ - x6 += decode[6+(ofs)*7] * c; \ - -#define stbir__2_coeff_remnant( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - c = hc[0+(ofs)]; \ - x0 += decode[0+(ofs)*7] * c; \ - x1 += decode[1+(ofs)*7] * c; \ - x2 += decode[2+(ofs)*7] * c; \ - x3 += decode[3+(ofs)*7] * c; \ - x4 += decode[4+(ofs)*7] * c; \ - x5 += decode[5+(ofs)*7] * c; \ - x6 += decode[6+(ofs)*7] * c; \ - c = hc[1+(ofs)]; \ - y0 += decode[7+(ofs)*7] * c; \ - y1 += decode[8+(ofs)*7] * c; \ - y2 += decode[9+(ofs)*7] * c; \ - y3 += decode[10+(ofs)*7] * c; \ - y4 += decode[11+(ofs)*7] * c; \ - y5 += decode[12+(ofs)*7] * c; \ - y6 += decode[13+(ofs)*7] * c; \ - -#define stbir__3_coeff_remnant( ofs ) \ - STBIR_SIMD_NO_UNROLL(decode); \ - c = hc[0+(ofs)]; \ - x0 += decode[0+(ofs)*7] * c; \ - x1 += decode[1+(ofs)*7] * c; \ - x2 += decode[2+(ofs)*7] * c; \ - x3 += decode[3+(ofs)*7] * c; \ - x4 += decode[4+(ofs)*7] * c; \ - x5 += decode[5+(ofs)*7] * c; \ - x6 += decode[6+(ofs)*7] * c; \ - c = hc[1+(ofs)]; \ - y0 += decode[7+(ofs)*7] * c; \ - y1 += decode[8+(ofs)*7] * c; \ - y2 += decode[9+(ofs)*7] * c; \ - y3 += decode[10+(ofs)*7] * c; \ - y4 += decode[11+(ofs)*7] * c; \ - y5 += decode[12+(ofs)*7] * c; \ - y6 += decode[13+(ofs)*7] * c; \ - c = hc[2+(ofs)]; \ - x0 += decode[14+(ofs)*7] * c; \ - x1 += decode[15+(ofs)*7] * c; \ - x2 += decode[16+(ofs)*7] * c; \ - x3 += decode[17+(ofs)*7] * c; \ - x4 += decode[18+(ofs)*7] * c; \ - x5 += decode[19+(ofs)*7] * c; \ - x6 += decode[20+(ofs)*7] * c; \ - -#define stbir__store_output() \ - output[0] = x0 + y0; \ - output[1] = x1 + y1; \ - output[2] = x2 + y2; \ - output[3] = x3 + y3; \ - output[4] = x4 + y4; \ - output[5] = x5 + y5; \ - output[6] = x6 + y6; \ - horizontal_coefficients += coefficient_width; \ - ++horizontal_contributors; \ - output += 7; - -#endif - -#define STBIR__horizontal_channels 7 -#define STB_IMAGE_RESIZE_DO_HORIZONTALS -#include STBIR__HEADER_FILENAME - -#define STBIR__vertical_channels 1 -#define STB_IMAGE_RESIZE_DO_VERTICALS -#include STBIR__HEADER_FILENAME - -#define STBIR__vertical_channels 1 -#define STB_IMAGE_RESIZE_DO_VERTICALS -#define STB_IMAGE_RESIZE_VERTICAL_CONTINUE -#include STBIR__HEADER_FILENAME - -#define STBIR__vertical_channels 2 -#define STB_IMAGE_RESIZE_DO_VERTICALS -#include STBIR__HEADER_FILENAME - -#define STBIR__vertical_channels 2 -#define STB_IMAGE_RESIZE_DO_VERTICALS -#define STB_IMAGE_RESIZE_VERTICAL_CONTINUE -#include STBIR__HEADER_FILENAME - -#define STBIR__vertical_channels 3 -#define STB_IMAGE_RESIZE_DO_VERTICALS -#include STBIR__HEADER_FILENAME - -#define STBIR__vertical_channels 3 -#define STB_IMAGE_RESIZE_DO_VERTICALS -#define STB_IMAGE_RESIZE_VERTICAL_CONTINUE -#include STBIR__HEADER_FILENAME - -#define STBIR__vertical_channels 4 -#define STB_IMAGE_RESIZE_DO_VERTICALS -#include STBIR__HEADER_FILENAME - -#define STBIR__vertical_channels 4 -#define STB_IMAGE_RESIZE_DO_VERTICALS -#define STB_IMAGE_RESIZE_VERTICAL_CONTINUE -#include STBIR__HEADER_FILENAME - -#define STBIR__vertical_channels 5 -#define STB_IMAGE_RESIZE_DO_VERTICALS -#include STBIR__HEADER_FILENAME - -#define STBIR__vertical_channels 5 -#define STB_IMAGE_RESIZE_DO_VERTICALS -#define STB_IMAGE_RESIZE_VERTICAL_CONTINUE -#include STBIR__HEADER_FILENAME - -#define STBIR__vertical_channels 6 -#define STB_IMAGE_RESIZE_DO_VERTICALS -#include STBIR__HEADER_FILENAME - -#define STBIR__vertical_channels 6 -#define STB_IMAGE_RESIZE_DO_VERTICALS -#define STB_IMAGE_RESIZE_VERTICAL_CONTINUE -#include STBIR__HEADER_FILENAME - -#define STBIR__vertical_channels 7 -#define STB_IMAGE_RESIZE_DO_VERTICALS -#include STBIR__HEADER_FILENAME - -#define STBIR__vertical_channels 7 -#define STB_IMAGE_RESIZE_DO_VERTICALS -#define STB_IMAGE_RESIZE_VERTICAL_CONTINUE -#include STBIR__HEADER_FILENAME - -#define STBIR__vertical_channels 8 -#define STB_IMAGE_RESIZE_DO_VERTICALS -#include STBIR__HEADER_FILENAME - -#define STBIR__vertical_channels 8 -#define STB_IMAGE_RESIZE_DO_VERTICALS -#define STB_IMAGE_RESIZE_VERTICAL_CONTINUE -#include STBIR__HEADER_FILENAME - -typedef void STBIR_VERTICAL_GATHERFUNC( float * output, float const * coeffs, float const ** inputs, float const * input0_end ); - -static STBIR_VERTICAL_GATHERFUNC * stbir__vertical_gathers[ 8 ] = -{ - stbir__vertical_gather_with_1_coeffs,stbir__vertical_gather_with_2_coeffs,stbir__vertical_gather_with_3_coeffs,stbir__vertical_gather_with_4_coeffs,stbir__vertical_gather_with_5_coeffs,stbir__vertical_gather_with_6_coeffs,stbir__vertical_gather_with_7_coeffs,stbir__vertical_gather_with_8_coeffs -}; - -static STBIR_VERTICAL_GATHERFUNC * stbir__vertical_gathers_continues[ 8 ] = -{ - stbir__vertical_gather_with_1_coeffs_cont,stbir__vertical_gather_with_2_coeffs_cont,stbir__vertical_gather_with_3_coeffs_cont,stbir__vertical_gather_with_4_coeffs_cont,stbir__vertical_gather_with_5_coeffs_cont,stbir__vertical_gather_with_6_coeffs_cont,stbir__vertical_gather_with_7_coeffs_cont,stbir__vertical_gather_with_8_coeffs_cont -}; - -typedef void STBIR_VERTICAL_SCATTERFUNC( float ** outputs, float const * coeffs, float const * input, float const * input_end ); - -static STBIR_VERTICAL_SCATTERFUNC * stbir__vertical_scatter_sets[ 8 ] = -{ - stbir__vertical_scatter_with_1_coeffs,stbir__vertical_scatter_with_2_coeffs,stbir__vertical_scatter_with_3_coeffs,stbir__vertical_scatter_with_4_coeffs,stbir__vertical_scatter_with_5_coeffs,stbir__vertical_scatter_with_6_coeffs,stbir__vertical_scatter_with_7_coeffs,stbir__vertical_scatter_with_8_coeffs -}; - -static STBIR_VERTICAL_SCATTERFUNC * stbir__vertical_scatter_blends[ 8 ] = -{ - stbir__vertical_scatter_with_1_coeffs_cont,stbir__vertical_scatter_with_2_coeffs_cont,stbir__vertical_scatter_with_3_coeffs_cont,stbir__vertical_scatter_with_4_coeffs_cont,stbir__vertical_scatter_with_5_coeffs_cont,stbir__vertical_scatter_with_6_coeffs_cont,stbir__vertical_scatter_with_7_coeffs_cont,stbir__vertical_scatter_with_8_coeffs_cont -}; - -static void stbir__encode_scanline( stbir__info const * stbir_info, void *output_buffer_data, float * encode_buffer, int row STBIR_ONLY_PROFILE_GET_SPLIT_INFO ) -{ - int num_pixels = stbir_info->horizontal.scale_info.output_sub_size; - int channels = stbir_info->channels; - int width_times_channels = num_pixels * channels; - void * output_buffer; - - if ( stbir_info->alpha_unweight ) - { - STBIR_PROFILE_START( unalpha ); - stbir_info->alpha_unweight( encode_buffer, width_times_channels ); - STBIR_PROFILE_END( unalpha ); - } - - output_buffer = output_buffer_data; - - if ( stbir_info->out_pixels_cb ) - output_buffer = encode_buffer; - - STBIR_PROFILE_START( encode ); - stbir_info->encode_pixels( output_buffer, width_times_channels, encode_buffer ); - STBIR_PROFILE_END( encode ); - - if ( stbir_info->out_pixels_cb ) - stbir_info->out_pixels_cb( output_buffer, num_pixels, row, stbir_info->user_data ); -} - -static float* stbir__get_ring_buffer_entry(stbir__info const * stbir_info, stbir__per_split_info const * split_info, int index ) -{ - STBIR_ASSERT( index < stbir_info->ring_buffer_num_entries ); - - #ifdef STBIR__SEPARATE_ALLOCATIONS - return split_info->ring_buffers[ index ]; - #else - return (float*) ( ( (char*) split_info->ring_buffer ) + ( index * stbir_info->ring_buffer_length_bytes ) ); - #endif -} - -static float* stbir__get_ring_buffer_scanline(stbir__info const * stbir_info, stbir__per_split_info const * split_info, int get_scanline) -{ - int ring_buffer_index = (split_info->ring_buffer_begin_index + (get_scanline - split_info->ring_buffer_first_scanline)) % stbir_info->ring_buffer_num_entries; - return stbir__get_ring_buffer_entry( stbir_info, split_info, ring_buffer_index ); -} - -static void stbir__resample_horizontal_gather(stbir__info const * stbir_info, float* output_buffer, float const * input_buffer STBIR_ONLY_PROFILE_GET_SPLIT_INFO ) -{ - float const * decode_buffer = input_buffer - ( stbir_info->scanline_extents.conservative.n0 * stbir_info->effective_channels ); - - STBIR_PROFILE_START( horizontal ); - if ( ( stbir_info->horizontal.filter_enum == STBIR_FILTER_POINT_SAMPLE ) && ( stbir_info->horizontal.scale_info.scale == 1.0f ) ) - STBIR_MEMCPY( output_buffer, input_buffer, stbir_info->horizontal.scale_info.output_sub_size * sizeof( float ) * stbir_info->effective_channels ); - else - stbir_info->horizontal_gather_channels( output_buffer, stbir_info->horizontal.scale_info.output_sub_size, decode_buffer, stbir_info->horizontal.contributors, stbir_info->horizontal.coefficients, stbir_info->horizontal.coefficient_width ); - STBIR_PROFILE_END( horizontal ); -} - -static void stbir__resample_vertical_gather(stbir__info const * stbir_info, stbir__per_split_info* split_info, int n, int contrib_n0, int contrib_n1, float const * vertical_coefficients ) -{ - float* encode_buffer = split_info->vertical_buffer; - float* decode_buffer = split_info->decode_buffer; - int vertical_first = stbir_info->vertical_first; - int width = (vertical_first) ? ( stbir_info->scanline_extents.conservative.n1-stbir_info->scanline_extents.conservative.n0+1 ) : stbir_info->horizontal.scale_info.output_sub_size; - int width_times_channels = stbir_info->effective_channels * width; - - STBIR_ASSERT( stbir_info->vertical.is_gather ); - - STBIR_PROFILE_START( vertical ); - { - int k = 0, total = contrib_n1 - contrib_n0 + 1; - STBIR_ASSERT( total > 0 ); - do { - float const * inputs[8]; - int i, cnt = total; if ( cnt > 8 ) cnt = 8; - for( i = 0 ; i < cnt ; i++ ) - inputs[ i ] = stbir__get_ring_buffer_scanline(stbir_info, split_info, k+i+contrib_n0 ); - - ((k==0)?stbir__vertical_gathers:stbir__vertical_gathers_continues)[cnt-1]( (vertical_first) ? decode_buffer : encode_buffer, vertical_coefficients + k, inputs, inputs[0] + width_times_channels ); - k += cnt; - total -= cnt; - } while ( total ); - } - STBIR_PROFILE_END( vertical ); - - if ( vertical_first ) - { - decode_buffer[ width_times_channels ] = 0.0f; // clear two over for horizontals with a remnant of 3 - decode_buffer[ width_times_channels+1 ] = 0.0f; - stbir__resample_horizontal_gather(stbir_info, encode_buffer, decode_buffer STBIR_ONLY_PROFILE_SET_SPLIT_INFO ); - } - - stbir__encode_scanline( stbir_info, ( (char *) stbir_info->output_data ) + ((size_t)n * (size_t)stbir_info->output_stride_bytes), - encode_buffer, n STBIR_ONLY_PROFILE_SET_SPLIT_INFO ); -} - -static void stbir__decode_and_resample_for_vertical_gather_loop(stbir__info const * stbir_info, stbir__per_split_info* split_info, int n) -{ - int ring_buffer_index; - float* ring_buffer; - - stbir__decode_scanline( stbir_info, n, split_info->decode_buffer STBIR_ONLY_PROFILE_SET_SPLIT_INFO ); - - split_info->ring_buffer_last_scanline = n; - - ring_buffer_index = (split_info->ring_buffer_begin_index + (split_info->ring_buffer_last_scanline - split_info->ring_buffer_first_scanline)) % stbir_info->ring_buffer_num_entries; - ring_buffer = stbir__get_ring_buffer_entry(stbir_info, split_info, ring_buffer_index); - - stbir__resample_horizontal_gather( stbir_info, ring_buffer, split_info->decode_buffer STBIR_ONLY_PROFILE_SET_SPLIT_INFO ); - -} - -static void stbir__vertical_gather_loop( stbir__info const * stbir_info, stbir__per_split_info* split_info, int split_count ) -{ - int y, start_output_y, end_output_y; - stbir__contributors* vertical_contributors = stbir_info->vertical.contributors; - float const * vertical_coefficients = stbir_info->vertical.coefficients; - - STBIR_ASSERT( stbir_info->vertical.is_gather ); - - start_output_y = split_info->start_output_y; - end_output_y = split_info[split_count-1].end_output_y; - - vertical_contributors += start_output_y; - vertical_coefficients += start_output_y * stbir_info->vertical.coefficient_width; - - split_info->ring_buffer_begin_index = 0; - split_info->ring_buffer_first_scanline = vertical_contributors->n0; - split_info->ring_buffer_last_scanline = split_info->ring_buffer_first_scanline - 1; // means "empty" - - for (y = start_output_y; y < end_output_y; y++) - { - int in_first_scanline, in_last_scanline; - - in_first_scanline = vertical_contributors->n0; - in_last_scanline = vertical_contributors->n1; - - STBIR_ASSERT( in_first_scanline >= split_info->ring_buffer_first_scanline ); - - while (in_last_scanline > split_info->ring_buffer_last_scanline) - { - STBIR_ASSERT( ( split_info->ring_buffer_last_scanline - split_info->ring_buffer_first_scanline + 1 ) <= stbir_info->ring_buffer_num_entries ); - - if ( ( split_info->ring_buffer_last_scanline - split_info->ring_buffer_first_scanline + 1 ) == stbir_info->ring_buffer_num_entries ) - { - split_info->ring_buffer_first_scanline++; - split_info->ring_buffer_begin_index++; - } - - if ( stbir_info->vertical_first ) - { - float * ring_buffer = stbir__get_ring_buffer_scanline( stbir_info, split_info, ++split_info->ring_buffer_last_scanline ); - stbir__decode_scanline( stbir_info, split_info->ring_buffer_last_scanline, ring_buffer STBIR_ONLY_PROFILE_SET_SPLIT_INFO ); - } - else - { - stbir__decode_and_resample_for_vertical_gather_loop(stbir_info, split_info, split_info->ring_buffer_last_scanline + 1); - } - } - - stbir__resample_vertical_gather(stbir_info, split_info, y, in_first_scanline, in_last_scanline, vertical_coefficients ); - - ++vertical_contributors; - vertical_coefficients += stbir_info->vertical.coefficient_width; - } -} - -#define STBIR__FLOAT_EMPTY_MARKER 3.0e+38F -#define STBIR__FLOAT_BUFFER_IS_EMPTY(ptr) ((ptr)[0]==STBIR__FLOAT_EMPTY_MARKER) - -static void stbir__encode_first_scanline_from_scatter(stbir__info const * stbir_info, stbir__per_split_info* split_info) -{ - float* ring_buffer_entry = stbir__get_ring_buffer_entry(stbir_info, split_info, split_info->ring_buffer_begin_index ); - - stbir__encode_scanline( stbir_info, ( (char *)stbir_info->output_data ) + ( (size_t)split_info->ring_buffer_first_scanline * (size_t)stbir_info->output_stride_bytes ), ring_buffer_entry, split_info->ring_buffer_first_scanline STBIR_ONLY_PROFILE_SET_SPLIT_INFO ); - - ring_buffer_entry[ 0 ] = STBIR__FLOAT_EMPTY_MARKER; - - split_info->ring_buffer_first_scanline++; - if ( ++split_info->ring_buffer_begin_index == stbir_info->ring_buffer_num_entries ) - split_info->ring_buffer_begin_index = 0; -} - -static void stbir__horizontal_resample_and_encode_first_scanline_from_scatter(stbir__info const * stbir_info, stbir__per_split_info* split_info) -{ - - float* ring_buffer_entry = stbir__get_ring_buffer_entry(stbir_info, split_info, split_info->ring_buffer_begin_index ); - - stbir__resample_horizontal_gather( stbir_info, split_info->vertical_buffer, ring_buffer_entry STBIR_ONLY_PROFILE_SET_SPLIT_INFO ); - - stbir__encode_scanline( stbir_info, ( (char *)stbir_info->output_data ) + ( (size_t)split_info->ring_buffer_first_scanline * (size_t)stbir_info->output_stride_bytes ), split_info->vertical_buffer, split_info->ring_buffer_first_scanline STBIR_ONLY_PROFILE_SET_SPLIT_INFO ); - - ring_buffer_entry[ 0 ] = STBIR__FLOAT_EMPTY_MARKER; - - split_info->ring_buffer_first_scanline++; - if ( ++split_info->ring_buffer_begin_index == stbir_info->ring_buffer_num_entries ) - split_info->ring_buffer_begin_index = 0; -} - -static void stbir__resample_vertical_scatter(stbir__info const * stbir_info, stbir__per_split_info* split_info, int n0, int n1, float const * vertical_coefficients, float const * vertical_buffer, float const * vertical_buffer_end ) -{ - STBIR_ASSERT( !stbir_info->vertical.is_gather ); - - STBIR_PROFILE_START( vertical ); - { - int k = 0, total = n1 - n0 + 1; - STBIR_ASSERT( total > 0 ); - do { - float * outputs[8]; - int i, n = total; if ( n > 8 ) n = 8; - for( i = 0 ; i < n ; i++ ) - { - outputs[ i ] = stbir__get_ring_buffer_scanline(stbir_info, split_info, k+i+n0 ); - if ( ( i ) && ( STBIR__FLOAT_BUFFER_IS_EMPTY( outputs[i] ) != STBIR__FLOAT_BUFFER_IS_EMPTY( outputs[0] ) ) ) // make sure runs are of the same type - { - n = i; - break; - } - } - ((STBIR__FLOAT_BUFFER_IS_EMPTY( outputs[0] ))?stbir__vertical_scatter_sets:stbir__vertical_scatter_blends)[n-1]( outputs, vertical_coefficients + k, vertical_buffer, vertical_buffer_end ); - k += n; - total -= n; - } while ( total ); - } - - STBIR_PROFILE_END( vertical ); -} - -typedef void stbir__handle_scanline_for_scatter_func(stbir__info const * stbir_info, stbir__per_split_info* split_info); - -static void stbir__vertical_scatter_loop( stbir__info const * stbir_info, stbir__per_split_info* split_info, int split_count ) -{ - int y, start_output_y, end_output_y, start_input_y, end_input_y; - stbir__contributors* vertical_contributors = stbir_info->vertical.contributors; - float const * vertical_coefficients = stbir_info->vertical.coefficients; - stbir__handle_scanline_for_scatter_func * handle_scanline_for_scatter; - void * scanline_scatter_buffer; - void * scanline_scatter_buffer_end; - int on_first_input_y, last_input_y; - int width = (stbir_info->vertical_first) ? ( stbir_info->scanline_extents.conservative.n1-stbir_info->scanline_extents.conservative.n0+1 ) : stbir_info->horizontal.scale_info.output_sub_size; - int width_times_channels = stbir_info->effective_channels * width; - - STBIR_ASSERT( !stbir_info->vertical.is_gather ); - - start_output_y = split_info->start_output_y; - end_output_y = split_info[split_count-1].end_output_y; // may do multiple split counts - - start_input_y = split_info->start_input_y; - end_input_y = split_info[split_count-1].end_input_y; - - y = start_input_y + stbir_info->vertical.filter_pixel_margin; - vertical_contributors += y ; - vertical_coefficients += stbir_info->vertical.coefficient_width * y; - - if ( stbir_info->vertical_first ) - { - handle_scanline_for_scatter = stbir__horizontal_resample_and_encode_first_scanline_from_scatter; - scanline_scatter_buffer = split_info->decode_buffer; - scanline_scatter_buffer_end = ( (char*) scanline_scatter_buffer ) + sizeof( float ) * stbir_info->effective_channels * (stbir_info->scanline_extents.conservative.n1-stbir_info->scanline_extents.conservative.n0+1); - } - else - { - handle_scanline_for_scatter = stbir__encode_first_scanline_from_scatter; - scanline_scatter_buffer = split_info->vertical_buffer; - scanline_scatter_buffer_end = ( (char*) scanline_scatter_buffer ) + sizeof( float ) * stbir_info->effective_channels * stbir_info->horizontal.scale_info.output_sub_size; - } - - split_info->ring_buffer_first_scanline = start_output_y; - split_info->ring_buffer_last_scanline = -1; - split_info->ring_buffer_begin_index = -1; - - for( y = 0 ; y < stbir_info->ring_buffer_num_entries ; y++ ) - { - float * decode_buffer = stbir__get_ring_buffer_entry( stbir_info, split_info, y ); - decode_buffer[ width_times_channels ] = 0.0f; // clear two over for horizontals with a remnant of 3 - decode_buffer[ width_times_channels+1 ] = 0.0f; - decode_buffer[0] = STBIR__FLOAT_EMPTY_MARKER; // only used on scatter - } - - on_first_input_y = 1; last_input_y = start_input_y; - for (y = start_input_y ; y < end_input_y; y++) - { - int out_first_scanline, out_last_scanline; - - out_first_scanline = vertical_contributors->n0; - out_last_scanline = vertical_contributors->n1; - - STBIR_ASSERT(out_last_scanline - out_first_scanline + 1 <= stbir_info->ring_buffer_num_entries); - - if ( ( out_last_scanline >= out_first_scanline ) && ( ( ( out_first_scanline >= start_output_y ) && ( out_first_scanline < end_output_y ) ) || ( ( out_last_scanline >= start_output_y ) && ( out_last_scanline < end_output_y ) ) ) ) - { - float const * vc = vertical_coefficients; - - last_input_y = y; - if ( ( on_first_input_y ) && ( y > start_input_y ) ) - split_info->start_input_y = y; - on_first_input_y = 0; - - if ( out_first_scanline < start_output_y ) - { - vc += start_output_y - out_first_scanline; - out_first_scanline = start_output_y; - } - - if ( out_last_scanline >= end_output_y ) - out_last_scanline = end_output_y - 1; - - if (split_info->ring_buffer_begin_index < 0) - split_info->ring_buffer_begin_index = out_first_scanline - start_output_y; - - STBIR_ASSERT( split_info->ring_buffer_begin_index <= out_first_scanline ); - - stbir__decode_scanline( stbir_info, y, split_info->decode_buffer STBIR_ONLY_PROFILE_SET_SPLIT_INFO ); - - if ( !stbir_info->vertical_first ) - stbir__resample_horizontal_gather( stbir_info, split_info->vertical_buffer, split_info->decode_buffer STBIR_ONLY_PROFILE_SET_SPLIT_INFO ); - - if ( ( ( split_info->ring_buffer_last_scanline - split_info->ring_buffer_first_scanline + 1 ) == stbir_info->ring_buffer_num_entries ) && - ( out_last_scanline > split_info->ring_buffer_last_scanline ) ) - handle_scanline_for_scatter( stbir_info, split_info ); - - stbir__resample_vertical_scatter(stbir_info, split_info, out_first_scanline, out_last_scanline, vc, (float*)scanline_scatter_buffer, (float*)scanline_scatter_buffer_end ); - - if ( out_last_scanline > split_info->ring_buffer_last_scanline ) - split_info->ring_buffer_last_scanline = out_last_scanline; - } - ++vertical_contributors; - vertical_coefficients += stbir_info->vertical.coefficient_width; - } - - while ( split_info->ring_buffer_first_scanline < end_output_y ) - handle_scanline_for_scatter(stbir_info, split_info); - - ++last_input_y; - for( y = 0 ; y < split_count; y++ ) - if ( split_info[y].end_input_y > last_input_y ) - split_info[y].end_input_y = last_input_y; -} - -static stbir__kernel_callback * stbir__builtin_kernels[] = { 0, stbir__filter_trapezoid, stbir__filter_triangle, stbir__filter_cubic, stbir__filter_catmullrom, stbir__filter_mitchell, stbir__filter_point }; -static stbir__support_callback * stbir__builtin_supports[] = { 0, stbir__support_trapezoid, stbir__support_one, stbir__support_two, stbir__support_two, stbir__support_two, stbir__support_zeropoint5 }; - -static void stbir__set_sampler(stbir__sampler * samp, stbir_filter filter, stbir__kernel_callback * kernel, stbir__support_callback * support, stbir_edge edge, stbir__scale_info * scale_info, int always_gather, void * user_data ) -{ - if (filter == 0) - { - filter = STBIR_DEFAULT_FILTER_DOWNSAMPLE; // default to downsample - if (scale_info->scale >= ( 1.0f - stbir__small_float ) ) - { - if ( (scale_info->scale <= ( 1.0f + stbir__small_float ) ) && ( STBIR_CEILF(scale_info->pixel_shift) == scale_info->pixel_shift ) ) - filter = STBIR_FILTER_POINT_SAMPLE; - else - filter = STBIR_DEFAULT_FILTER_UPSAMPLE; - } - } - samp->filter_enum = filter; - - STBIR_ASSERT(samp->filter_enum != 0); - STBIR_ASSERT((unsigned)samp->filter_enum < STBIR_FILTER_OTHER); - samp->filter_kernel = stbir__builtin_kernels[ filter ]; - samp->filter_support = stbir__builtin_supports[ filter ]; - - if ( kernel && support ) - { - samp->filter_kernel = kernel; - samp->filter_support = support; - samp->filter_enum = STBIR_FILTER_OTHER; - } - - samp->edge = edge; - samp->filter_pixel_width = stbir__get_filter_pixel_width (samp->filter_support, scale_info->scale, user_data ); - samp->is_gather = 0; - if ( scale_info->scale >= ( 1.0f - stbir__small_float ) ) - samp->is_gather = 1; - else if ( ( always_gather ) || ( samp->filter_pixel_width <= STBIR_FORCE_GATHER_FILTER_SCANLINES_AMOUNT ) ) - samp->is_gather = 2; - - samp->coefficient_width = stbir__get_coefficient_width(samp, samp->is_gather, user_data); - - if ( edge == STBIR_EDGE_WRAP ) - if ( samp->filter_pixel_width > ( scale_info->input_full_size * 3 ) ) - samp->filter_pixel_width = scale_info->input_full_size * 3; - - samp->filter_pixel_margin = samp->filter_pixel_width / 2; - - if ( edge == STBIR_EDGE_WRAP ) - if ( samp->filter_pixel_margin > scale_info->input_full_size ) - samp->filter_pixel_margin = scale_info->input_full_size; - - samp->num_contributors = stbir__get_contributors(samp, samp->is_gather); - - samp->contributors_size = samp->num_contributors * sizeof(stbir__contributors); - samp->coefficients_size = samp->num_contributors * samp->coefficient_width * sizeof(float) + sizeof(float)*STBIR_INPUT_CALLBACK_PADDING; // extra sizeof(float) is padding - - samp->gather_prescatter_contributors = 0; - samp->gather_prescatter_coefficients = 0; - if ( samp->is_gather == 0 ) - { - samp->gather_prescatter_coefficient_width = samp->filter_pixel_width; - samp->gather_prescatter_num_contributors = stbir__get_contributors(samp, 2); - samp->gather_prescatter_contributors_size = samp->gather_prescatter_num_contributors * sizeof(stbir__contributors); - samp->gather_prescatter_coefficients_size = samp->gather_prescatter_num_contributors * samp->gather_prescatter_coefficient_width * sizeof(float); - } -} - -static void stbir__get_conservative_extents( stbir__sampler * samp, stbir__contributors * range, void * user_data ) -{ - float scale = samp->scale_info.scale; - float out_shift = samp->scale_info.pixel_shift; - stbir__support_callback * support = samp->filter_support; - int input_full_size = samp->scale_info.input_full_size; - stbir_edge edge = samp->edge; - float inv_scale = samp->scale_info.inv_scale; - - STBIR_ASSERT( samp->is_gather != 0 ); - - if ( samp->is_gather == 1 ) - { - int in_first_pixel, in_last_pixel; - float out_filter_radius = support(inv_scale, user_data) * scale; - - stbir__calculate_in_pixel_range( &in_first_pixel, &in_last_pixel, 0.5, out_filter_radius, inv_scale, out_shift, input_full_size, edge ); - range->n0 = in_first_pixel; - stbir__calculate_in_pixel_range( &in_first_pixel, &in_last_pixel, ( (float)(samp->scale_info.output_sub_size-1) ) + 0.5f, out_filter_radius, inv_scale, out_shift, input_full_size, edge ); - range->n1 = in_last_pixel; - } - else if ( samp->is_gather == 2 ) // downsample gather, refine - { - float in_pixels_radius = support(scale, user_data) * inv_scale; - int filter_pixel_margin = samp->filter_pixel_margin; - int output_sub_size = samp->scale_info.output_sub_size; - int input_end; - int n; - int in_first_pixel, in_last_pixel; - - stbir__calculate_in_pixel_range( &in_first_pixel, &in_last_pixel, 0, 0, inv_scale, out_shift, input_full_size, edge ); - range->n0 = in_first_pixel; - stbir__calculate_in_pixel_range( &in_first_pixel, &in_last_pixel, (float)output_sub_size, 0, inv_scale, out_shift, input_full_size, edge ); - range->n1 = in_last_pixel; - - n = range->n0 + 1; - input_end = -filter_pixel_margin; - while( n >= input_end ) - { - int out_first_pixel, out_last_pixel; - stbir__calculate_out_pixel_range( &out_first_pixel, &out_last_pixel, ((float)n)+0.5f, in_pixels_radius, scale, out_shift, output_sub_size ); - if ( out_first_pixel > out_last_pixel ) - break; - - if ( ( out_first_pixel < output_sub_size ) || ( out_last_pixel >= 0 ) ) - range->n0 = n; - --n; - } - - n = range->n1 - 1; - input_end = n + 1 + filter_pixel_margin; - while( n <= input_end ) - { - int out_first_pixel, out_last_pixel; - stbir__calculate_out_pixel_range( &out_first_pixel, &out_last_pixel, ((float)n)+0.5f, in_pixels_radius, scale, out_shift, output_sub_size ); - if ( out_first_pixel > out_last_pixel ) - break; - if ( ( out_first_pixel < output_sub_size ) || ( out_last_pixel >= 0 ) ) - range->n1 = n; - ++n; - } - } - - if ( samp->edge == STBIR_EDGE_WRAP ) - { - if ( ( range->n0 > 0 ) && ( range->n1 >= input_full_size ) ) - { - int marg = range->n1 - input_full_size + 1; - if ( ( marg + STBIR__MERGE_RUNS_PIXEL_THRESHOLD ) >= range->n0 ) - range->n0 = 0; - } - if ( ( range->n0 < 0 ) && ( range->n1 < (input_full_size-1) ) ) - { - int marg = -range->n0; - if ( ( input_full_size - marg - STBIR__MERGE_RUNS_PIXEL_THRESHOLD - 1 ) <= range->n1 ) - range->n1 = input_full_size - 1; - } - } - else - { - if ( range->n0 < 0 ) - range->n0 = 0; - if ( range->n1 >= input_full_size ) - range->n1 = input_full_size - 1; - } -} - -static void stbir__get_split_info( stbir__per_split_info* split_info, int splits, int output_height, int vertical_pixel_margin, int input_full_height ) -{ - int i, cur; - int left = output_height; - - cur = 0; - for( i = 0 ; i < splits ; i++ ) - { - int each; - split_info[i].start_output_y = cur; - each = left / ( splits - i ); - split_info[i].end_output_y = cur + each; - cur += each; - left -= each; - - split_info[i].start_input_y = -vertical_pixel_margin; - split_info[i].end_input_y = input_full_height + vertical_pixel_margin; - } -} - -static void stbir__free_internal_mem( stbir__info *info ) -{ - #define STBIR__FREE_AND_CLEAR( ptr ) { if ( ptr ) { void * p = (ptr); (ptr) = 0; STBIR_FREE( p, info->user_data); } } - - if ( info ) - { - #ifndef STBIR__SEPARATE_ALLOCATIONS - STBIR__FREE_AND_CLEAR( info->alloced_mem ); - #else - int i,j; - - if ( ( info->vertical.gather_prescatter_contributors ) && ( (void*)info->vertical.gather_prescatter_contributors != (void*)info->split_info[0].decode_buffer ) ) - { - STBIR__FREE_AND_CLEAR( info->vertical.gather_prescatter_coefficients ); - STBIR__FREE_AND_CLEAR( info->vertical.gather_prescatter_contributors ); - } - for( i = 0 ; i < info->splits ; i++ ) - { - for( j = 0 ; j < info->alloc_ring_buffer_num_entries ; j++ ) - { - #ifdef STBIR_SIMD8 - if ( info->effective_channels == 3 ) - --info->split_info[i].ring_buffers[j]; // avx in 3 channel mode needs one float at the start of the buffer - #endif - STBIR__FREE_AND_CLEAR( info->split_info[i].ring_buffers[j] ); - } - - #ifdef STBIR_SIMD8 - if ( info->effective_channels == 3 ) - --info->split_info[i].decode_buffer; // avx in 3 channel mode needs one float at the start of the buffer - #endif - STBIR__FREE_AND_CLEAR( info->split_info[i].decode_buffer ); - STBIR__FREE_AND_CLEAR( info->split_info[i].ring_buffers ); - STBIR__FREE_AND_CLEAR( info->split_info[i].vertical_buffer ); - } - STBIR__FREE_AND_CLEAR( info->split_info ); - if ( info->vertical.coefficients != info->horizontal.coefficients ) - { - STBIR__FREE_AND_CLEAR( info->vertical.coefficients ); - STBIR__FREE_AND_CLEAR( info->vertical.contributors ); - } - STBIR__FREE_AND_CLEAR( info->horizontal.coefficients ); - STBIR__FREE_AND_CLEAR( info->horizontal.contributors ); - STBIR__FREE_AND_CLEAR( info->alloced_mem ); - STBIR_FREE( info, info->user_data ); - #endif - } - - #undef STBIR__FREE_AND_CLEAR -} - -static int stbir__get_max_split( int splits, int height ) -{ - int i; - int max = 0; - - for( i = 0 ; i < splits ; i++ ) - { - int each = height / ( splits - i ); - if ( each > max ) - max = each; - height -= each; - } - return max; -} - -static stbir__horizontal_gather_channels_func ** stbir__horizontal_gather_n_coeffs_funcs[8] = -{ - 0, stbir__horizontal_gather_1_channels_with_n_coeffs_funcs, stbir__horizontal_gather_2_channels_with_n_coeffs_funcs, stbir__horizontal_gather_3_channels_with_n_coeffs_funcs, stbir__horizontal_gather_4_channels_with_n_coeffs_funcs, 0,0, stbir__horizontal_gather_7_channels_with_n_coeffs_funcs -}; - -static stbir__horizontal_gather_channels_func ** stbir__horizontal_gather_channels_funcs[8] = -{ - 0, stbir__horizontal_gather_1_channels_funcs, stbir__horizontal_gather_2_channels_funcs, stbir__horizontal_gather_3_channels_funcs, stbir__horizontal_gather_4_channels_funcs, 0,0, stbir__horizontal_gather_7_channels_funcs -}; - -#define STBIR_RESIZE_CLASSIFICATIONS 8 - -static float stbir__compute_weights[5][STBIR_RESIZE_CLASSIFICATIONS][4]= // 5 = 0=1chan, 1=2chan, 2=3chan, 3=4chan, 4=7chan -{ - { - { 1.00000f, 1.00000f, 0.31250f, 1.00000f }, - { 0.56250f, 0.59375f, 0.00000f, 0.96875f }, - { 1.00000f, 0.06250f, 0.00000f, 1.00000f }, - { 0.00000f, 0.09375f, 1.00000f, 1.00000f }, - { 1.00000f, 1.00000f, 1.00000f, 1.00000f }, - { 0.03125f, 0.12500f, 1.00000f, 1.00000f }, - { 0.06250f, 0.12500f, 0.00000f, 1.00000f }, - { 0.00000f, 1.00000f, 0.00000f, 0.03125f }, - }, { - { 0.00000f, 0.84375f, 0.00000f, 0.03125f }, - { 0.09375f, 0.93750f, 0.00000f, 0.78125f }, - { 0.87500f, 0.21875f, 0.00000f, 0.96875f }, - { 0.09375f, 0.09375f, 1.00000f, 1.00000f }, - { 1.00000f, 1.00000f, 1.00000f, 1.00000f }, - { 0.03125f, 0.12500f, 1.00000f, 1.00000f }, - { 0.06250f, 0.12500f, 0.00000f, 1.00000f }, - { 0.00000f, 1.00000f, 0.00000f, 0.53125f }, - }, { - { 0.00000f, 0.53125f, 0.00000f, 0.03125f }, - { 0.06250f, 0.96875f, 0.00000f, 0.53125f }, - { 0.87500f, 0.18750f, 0.00000f, 0.93750f }, - { 0.00000f, 0.09375f, 1.00000f, 1.00000f }, - { 1.00000f, 1.00000f, 1.00000f, 1.00000f }, - { 0.03125f, 0.12500f, 1.00000f, 1.00000f }, - { 0.06250f, 0.12500f, 0.00000f, 1.00000f }, - { 0.00000f, 1.00000f, 0.00000f, 0.56250f }, - }, { - { 0.00000f, 0.50000f, 0.00000f, 0.71875f }, - { 0.06250f, 0.84375f, 0.00000f, 0.87500f }, - { 1.00000f, 0.50000f, 0.50000f, 0.96875f }, - { 1.00000f, 0.09375f, 0.31250f, 0.50000f }, - { 1.00000f, 1.00000f, 1.00000f, 1.00000f }, - { 1.00000f, 0.03125f, 0.03125f, 0.53125f }, - { 0.18750f, 0.12500f, 0.00000f, 1.00000f }, - { 0.00000f, 1.00000f, 0.03125f, 0.18750f }, - }, { - { 0.00000f, 0.59375f, 0.00000f, 0.96875f }, - { 0.06250f, 0.81250f, 0.06250f, 0.59375f }, - { 0.75000f, 0.43750f, 0.12500f, 0.96875f }, - { 0.87500f, 0.06250f, 0.18750f, 0.43750f }, - { 1.00000f, 1.00000f, 1.00000f, 1.00000f }, - { 0.15625f, 0.12500f, 1.00000f, 1.00000f }, - { 0.06250f, 0.12500f, 0.00000f, 1.00000f }, - { 0.00000f, 1.00000f, 0.03125f, 0.34375f }, - } -}; - -typedef struct STBIR__V_FIRST_INFO -{ - double v_cost, h_cost; - int control_v_first; // 0 = no control, 1 = force hori, 2 = force vert - int v_first; - int v_resize_classification; - int is_gather; -} STBIR__V_FIRST_INFO; - -#ifdef STBIR__V_FIRST_INFO_BUFFER -static STBIR__V_FIRST_INFO STBIR__V_FIRST_INFO_BUFFER = {0}; -#define STBIR__V_FIRST_INFO_POINTER &STBIR__V_FIRST_INFO_BUFFER -#else -#define STBIR__V_FIRST_INFO_POINTER 0 -#endif - -static int stbir__should_do_vertical_first( float weights_table[STBIR_RESIZE_CLASSIFICATIONS][4], int horizontal_filter_pixel_width, float horizontal_scale, int horizontal_output_size, int vertical_filter_pixel_width, float vertical_scale, int vertical_output_size, int is_gather, STBIR__V_FIRST_INFO * info ) -{ - double v_cost, h_cost; - float * weights; - int vertical_first; - int v_classification; - - if ( ( vertical_output_size <= 4 ) || ( horizontal_output_size <= 4 ) ) - v_classification = ( vertical_output_size < horizontal_output_size ) ? 6 : 7; - else if ( vertical_scale <= 1.0f ) - v_classification = ( is_gather ) ? 1 : 0; - else if ( vertical_scale <= 2.0f) - v_classification = 2; - else if ( vertical_scale <= 3.0f) - v_classification = 3; - else if ( vertical_scale <= 4.0f) - v_classification = 5; - else - v_classification = 6; - - weights = weights_table[ v_classification ]; - - h_cost = (float)horizontal_filter_pixel_width * weights[0] + horizontal_scale * (float)vertical_filter_pixel_width * weights[1]; - v_cost = (float)vertical_filter_pixel_width * weights[2] + vertical_scale * (float)horizontal_filter_pixel_width * weights[3]; - - vertical_first = ( v_cost <= h_cost ) ? 1 : 0; - - if ( info ) - { - info->h_cost = h_cost; - info->v_cost = v_cost; - info->v_resize_classification = v_classification; - info->v_first = vertical_first; - info->is_gather = is_gather; - } - - if ( ( info ) && ( info->control_v_first ) ) - vertical_first = ( info->control_v_first == 2 ) ? 1 : 0; - - return vertical_first; -} - -static unsigned char stbir__pixel_channels[] = { - 1,2,3,3,4, // 1ch, 2ch, rgb, bgr, 4ch - 4,4,4,4,2,2, // RGBA,BGRA,ARGB,ABGR,RA,AR - 4,4,4,4,2,2, // RGBA_PM,BGRA_PM,ARGB_PM,ABGR_PM,RA_PM,AR_PM -}; - -static stbir_internal_pixel_layout stbir__pixel_layout_convert_public_to_internal[] = { - STBIRI_BGR, STBIRI_1CHANNEL, STBIRI_2CHANNEL, STBIRI_RGB, STBIRI_RGBA, - STBIRI_4CHANNEL, STBIRI_BGRA, STBIRI_ARGB, STBIRI_ABGR, STBIRI_RA, STBIRI_AR, - STBIRI_RGBA_PM, STBIRI_BGRA_PM, STBIRI_ARGB_PM, STBIRI_ABGR_PM, STBIRI_RA_PM, STBIRI_AR_PM, -}; - -static stbir__info * stbir__alloc_internal_mem_and_build_samplers( stbir__sampler * horizontal, stbir__sampler * vertical, stbir__contributors * conservative, stbir_pixel_layout input_pixel_layout_public, stbir_pixel_layout output_pixel_layout_public, int splits, int new_x, int new_y, int fast_alpha, void * user_data STBIR_ONLY_PROFILE_BUILD_GET_INFO ) -{ - static char stbir_channel_count_index[8]={ 9,0,1,2, 3,9,9,4 }; - - stbir__info * info = 0; - void * alloced = 0; - size_t alloced_total = 0; - int vertical_first; - size_t decode_buffer_size, ring_buffer_length_bytes, ring_buffer_size, vertical_buffer_size; - int alloc_ring_buffer_num_entries; - - int alpha_weighting_type = 0; // 0=none, 1=simple, 2=fancy - int conservative_split_output_size = stbir__get_max_split( splits, vertical->scale_info.output_sub_size ); - stbir_internal_pixel_layout input_pixel_layout = stbir__pixel_layout_convert_public_to_internal[ input_pixel_layout_public ]; - stbir_internal_pixel_layout output_pixel_layout = stbir__pixel_layout_convert_public_to_internal[ output_pixel_layout_public ]; - int channels = stbir__pixel_channels[ input_pixel_layout ]; - int effective_channels = channels; - - if ( ( horizontal->filter_enum != STBIR_FILTER_POINT_SAMPLE ) || ( vertical->filter_enum != STBIR_FILTER_POINT_SAMPLE ) ) // no alpha weighting on point sampling - { - if ( ( input_pixel_layout >= STBIRI_RGBA ) && ( input_pixel_layout <= STBIRI_AR ) && ( output_pixel_layout >= STBIRI_RGBA ) && ( output_pixel_layout <= STBIRI_AR ) ) - { - if ( fast_alpha ) - { - alpha_weighting_type = 4; - } - else - { - static int fancy_alpha_effective_cnts[6] = { 7, 7, 7, 7, 3, 3 }; - alpha_weighting_type = 2; - effective_channels = fancy_alpha_effective_cnts[ input_pixel_layout - STBIRI_RGBA ]; - } - } - else if ( ( input_pixel_layout >= STBIRI_RGBA_PM ) && ( input_pixel_layout <= STBIRI_AR_PM ) && ( output_pixel_layout >= STBIRI_RGBA ) && ( output_pixel_layout <= STBIRI_AR ) ) - { - alpha_weighting_type = 3; - } - else if ( ( input_pixel_layout >= STBIRI_RGBA ) && ( input_pixel_layout <= STBIRI_AR ) && ( output_pixel_layout >= STBIRI_RGBA_PM ) && ( output_pixel_layout <= STBIRI_AR_PM ) ) - { - alpha_weighting_type = 1; - } - } - - if ( channels != stbir__pixel_channels[ output_pixel_layout ] ) - return 0; - - vertical_first = stbir__should_do_vertical_first( stbir__compute_weights[ (int)stbir_channel_count_index[ effective_channels ] ], horizontal->filter_pixel_width, horizontal->scale_info.scale, horizontal->scale_info.output_sub_size, vertical->filter_pixel_width, vertical->scale_info.scale, vertical->scale_info.output_sub_size, vertical->is_gather, STBIR__V_FIRST_INFO_POINTER ); - - decode_buffer_size = ( conservative->n1 - conservative->n0 + 1 ) * effective_channels * sizeof(float) + sizeof(float)*STBIR_INPUT_CALLBACK_PADDING; // extra floats for input callback stagger - -#if defined( STBIR__SEPARATE_ALLOCATIONS ) && defined(STBIR_SIMD8) - if ( effective_channels == 3 ) - decode_buffer_size += sizeof(float); // avx in 3 channel mode needs one float at the start of the buffer (only with separate allocations) -#endif - - ring_buffer_length_bytes = (size_t)horizontal->scale_info.output_sub_size * (size_t)effective_channels * sizeof(float) + sizeof(float)*STBIR_INPUT_CALLBACK_PADDING; // extra floats for padding - - if ( vertical_first ) - ring_buffer_length_bytes = ( decode_buffer_size + 15 ) & ~15; - - if ( ( ring_buffer_length_bytes & 4095 ) == 0 ) ring_buffer_length_bytes += 64*3; // avoid 4k alias - - alloc_ring_buffer_num_entries = vertical->filter_pixel_width + 1; - - if ( ( !vertical->is_gather ) && ( alloc_ring_buffer_num_entries > conservative_split_output_size ) ) - alloc_ring_buffer_num_entries = conservative_split_output_size; - - ring_buffer_size = (size_t)alloc_ring_buffer_num_entries * (size_t)ring_buffer_length_bytes; - - vertical_buffer_size = (size_t)horizontal->scale_info.output_sub_size * (size_t)effective_channels * sizeof(float) + sizeof(float); // extra float for padding - - for(;;) - { - int i; - void * advance_mem = alloced; - int copy_horizontal = 0; - stbir__sampler * possibly_use_horizontal_for_pivot = 0; - -#ifdef STBIR__SEPARATE_ALLOCATIONS - #define STBIR__NEXT_PTR( ptr, size, ntype ) if ( alloced ) { void * p = STBIR_MALLOC( size, user_data); if ( p == 0 ) { stbir__free_internal_mem( info ); return 0; } (ptr) = (ntype*)p; } -#else - #define STBIR__NEXT_PTR( ptr, size, ntype ) advance_mem = (void*) ( ( ((size_t)advance_mem) + 15 ) & ~15 ); if ( alloced ) ptr = (ntype*)advance_mem; advance_mem = (char*)(((size_t)advance_mem) + (size)); -#endif - - STBIR__NEXT_PTR( info, sizeof( stbir__info ), stbir__info ); - - STBIR__NEXT_PTR( info->split_info, sizeof( stbir__per_split_info ) * splits, stbir__per_split_info ); - - if ( info ) - { - static stbir__alpha_weight_func * fancy_alpha_weights[6] = { stbir__fancy_alpha_weight_4ch, stbir__fancy_alpha_weight_4ch, stbir__fancy_alpha_weight_4ch, stbir__fancy_alpha_weight_4ch, stbir__fancy_alpha_weight_2ch, stbir__fancy_alpha_weight_2ch }; - static stbir__alpha_unweight_func * fancy_alpha_unweights[6] = { stbir__fancy_alpha_unweight_4ch, stbir__fancy_alpha_unweight_4ch, stbir__fancy_alpha_unweight_4ch, stbir__fancy_alpha_unweight_4ch, stbir__fancy_alpha_unweight_2ch, stbir__fancy_alpha_unweight_2ch }; - static stbir__alpha_weight_func * simple_alpha_weights[6] = { stbir__simple_alpha_weight_4ch, stbir__simple_alpha_weight_4ch, stbir__simple_alpha_weight_4ch, stbir__simple_alpha_weight_4ch, stbir__simple_alpha_weight_2ch, stbir__simple_alpha_weight_2ch }; - static stbir__alpha_unweight_func * simple_alpha_unweights[6] = { stbir__simple_alpha_unweight_4ch, stbir__simple_alpha_unweight_4ch, stbir__simple_alpha_unweight_4ch, stbir__simple_alpha_unweight_4ch, stbir__simple_alpha_unweight_2ch, stbir__simple_alpha_unweight_2ch }; - - info->alloced_mem = alloced; - info->alloced_total = alloced_total; - - info->channels = channels; - info->effective_channels = effective_channels; - - info->offset_x = new_x; - info->offset_y = new_y; - info->alloc_ring_buffer_num_entries = (int)alloc_ring_buffer_num_entries; - info->ring_buffer_num_entries = 0; - info->ring_buffer_length_bytes = (int)ring_buffer_length_bytes; - info->splits = splits; - info->vertical_first = vertical_first; - - info->input_pixel_layout_internal = input_pixel_layout; - info->output_pixel_layout_internal = output_pixel_layout; - - info->alpha_weight = 0; - info->alpha_unweight = 0; - - if ( alpha_weighting_type == 2 ) - { - info->alpha_weight = fancy_alpha_weights[ input_pixel_layout - STBIRI_RGBA ]; - info->alpha_unweight = fancy_alpha_unweights[ output_pixel_layout - STBIRI_RGBA ]; - } - else if ( alpha_weighting_type == 4 ) - { - info->alpha_weight = simple_alpha_weights[ input_pixel_layout - STBIRI_RGBA ]; - info->alpha_unweight = simple_alpha_unweights[ output_pixel_layout - STBIRI_RGBA ]; - } - else if ( alpha_weighting_type == 1 ) - { - info->alpha_weight = simple_alpha_weights[ input_pixel_layout - STBIRI_RGBA ]; - } - else if ( alpha_weighting_type == 3 ) - { - info->alpha_unweight = simple_alpha_unweights[ output_pixel_layout - STBIRI_RGBA ]; - } - - if ( ( ( input_pixel_layout == STBIRI_RGB ) && ( output_pixel_layout == STBIRI_BGR ) ) || - ( ( input_pixel_layout == STBIRI_BGR ) && ( output_pixel_layout == STBIRI_RGB ) ) ) - { - if ( horizontal->scale_info.scale < 1.0f ) - info->alpha_unweight = stbir__simple_flip_3ch; - else - info->alpha_weight = stbir__simple_flip_3ch; - } - - } - - for( i = 0 ; i < splits ; i++ ) - { - STBIR__NEXT_PTR( info->split_info[i].decode_buffer, decode_buffer_size, float ); - -#ifdef STBIR__SEPARATE_ALLOCATIONS - - #ifdef STBIR_SIMD8 - if ( ( info ) && ( effective_channels == 3 ) ) - ++info->split_info[i].decode_buffer; // avx in 3 channel mode needs one float at the start of the buffer - #endif - - STBIR__NEXT_PTR( info->split_info[i].ring_buffers, alloc_ring_buffer_num_entries * sizeof(float*), float* ); - { - int j; - for( j = 0 ; j < alloc_ring_buffer_num_entries ; j++ ) - { - STBIR__NEXT_PTR( info->split_info[i].ring_buffers[j], ring_buffer_length_bytes, float ); - #ifdef STBIR_SIMD8 - if ( ( info ) && ( effective_channels == 3 ) ) - ++info->split_info[i].ring_buffers[j]; // avx in 3 channel mode needs one float at the start of the buffer - #endif - } - } -#else - STBIR__NEXT_PTR( info->split_info[i].ring_buffer, ring_buffer_size, float ); -#endif - STBIR__NEXT_PTR( info->split_info[i].vertical_buffer, vertical_buffer_size, float ); - } - - if ( vertical->is_gather == 0 ) - { - size_t both; - size_t temp_mem_amt; - - both = (size_t)vertical->gather_prescatter_contributors_size + (size_t)vertical->gather_prescatter_coefficients_size; - -#ifdef STBIR__SEPARATE_ALLOCATIONS - temp_mem_amt = decode_buffer_size; - - #ifdef STBIR_SIMD8 - if ( effective_channels == 3 ) - --temp_mem_amt; // avx in 3 channel mode needs one float at the start of the buffer - #endif -#else - temp_mem_amt = (size_t)( decode_buffer_size + ring_buffer_size + vertical_buffer_size ) * (size_t)splits; -#endif - if ( temp_mem_amt >= both ) - { - if ( info ) - { - vertical->gather_prescatter_contributors = (stbir__contributors*)info->split_info[0].decode_buffer; - vertical->gather_prescatter_coefficients = (float*) ( ( (char*)info->split_info[0].decode_buffer ) + vertical->gather_prescatter_contributors_size ); - } - } - else - { - STBIR__NEXT_PTR( vertical->gather_prescatter_contributors, vertical->gather_prescatter_contributors_size, stbir__contributors ); - STBIR__NEXT_PTR( vertical->gather_prescatter_coefficients, vertical->gather_prescatter_coefficients_size, float ); - } - } - - STBIR__NEXT_PTR( horizontal->contributors, horizontal->contributors_size, stbir__contributors ); - STBIR__NEXT_PTR( horizontal->coefficients, horizontal->coefficients_size, float ); - - if ( ( horizontal->filter_kernel == vertical->filter_kernel ) && ( horizontal->filter_support == vertical->filter_support ) && ( horizontal->edge == vertical->edge ) && ( horizontal->scale_info.output_sub_size == vertical->scale_info.output_sub_size ) ) - { - float diff_scale = horizontal->scale_info.scale - vertical->scale_info.scale; - float diff_shift = horizontal->scale_info.pixel_shift - vertical->scale_info.pixel_shift; - if ( diff_scale < 0.0f ) diff_scale = -diff_scale; - if ( diff_shift < 0.0f ) diff_shift = -diff_shift; - if ( ( diff_scale <= stbir__small_float ) && ( diff_shift <= stbir__small_float ) ) - { - if ( horizontal->is_gather == vertical->is_gather ) - { - copy_horizontal = 1; - goto no_vert_alloc; - } - possibly_use_horizontal_for_pivot = horizontal; - } - } - - STBIR__NEXT_PTR( vertical->contributors, vertical->contributors_size, stbir__contributors ); - STBIR__NEXT_PTR( vertical->coefficients, vertical->coefficients_size, float ); - - no_vert_alloc: - - if ( info ) - { - STBIR_PROFILE_BUILD_START( horizontal ); - - stbir__calculate_filters( horizontal, 0, user_data STBIR_ONLY_PROFILE_BUILD_SET_INFO ); - - info->horizontal_gather_channels = stbir__horizontal_gather_n_coeffs_funcs[ effective_channels ][ horizontal->extent_info.widest & 3 ]; - if ( horizontal->extent_info.widest <= 12 ) - info->horizontal_gather_channels = stbir__horizontal_gather_channels_funcs[ effective_channels ][ horizontal->extent_info.widest - 1 ]; - - info->scanline_extents.conservative.n0 = conservative->n0; - info->scanline_extents.conservative.n1 = conservative->n1; - - stbir__get_extents( horizontal, &info->scanline_extents ); - - horizontal->coefficient_width = stbir__pack_coefficients(horizontal->num_contributors, horizontal->contributors, horizontal->coefficients, horizontal->coefficient_width, horizontal->extent_info.widest, info->scanline_extents.conservative.n0, info->scanline_extents.conservative.n1 ); - - STBIR_MEMCPY( &info->horizontal, horizontal, sizeof( stbir__sampler ) ); - - STBIR_PROFILE_BUILD_END( horizontal ); - - if ( copy_horizontal ) - { - STBIR_MEMCPY( &info->vertical, horizontal, sizeof( stbir__sampler ) ); - } - else - { - STBIR_PROFILE_BUILD_START( vertical ); - - stbir__calculate_filters( vertical, possibly_use_horizontal_for_pivot, user_data STBIR_ONLY_PROFILE_BUILD_SET_INFO ); - STBIR_MEMCPY( &info->vertical, vertical, sizeof( stbir__sampler ) ); - - STBIR_PROFILE_BUILD_END( vertical ); - } - - stbir__get_split_info( info->split_info, info->splits, info->vertical.scale_info.output_sub_size, info->vertical.filter_pixel_margin, info->vertical.scale_info.input_full_size ); - - info->ring_buffer_num_entries = info->vertical.extent_info.widest; - - if ( ( !info->vertical.is_gather ) && ( info->ring_buffer_num_entries > conservative_split_output_size ) ) - info->ring_buffer_num_entries = conservative_split_output_size; - STBIR_ASSERT( info->ring_buffer_num_entries <= info->alloc_ring_buffer_num_entries ); - } - #undef STBIR__NEXT_PTR - - if ( info == 0 ) - { - alloced_total = ( 15 + (size_t)advance_mem ); - alloced = STBIR_MALLOC( alloced_total, user_data ); - if ( alloced == 0 ) - return 0; - } - else - return info; // success - } -} - -static int stbir__perform_resize( stbir__info const * info, int split_start, int split_count ) -{ - stbir__per_split_info * split_info = info->split_info + split_start; - - STBIR_PROFILE_CLEAR_EXTRAS(); - - STBIR_PROFILE_FIRST_START( looping ); - if (info->vertical.is_gather) - stbir__vertical_gather_loop( info, split_info, split_count ); - else - stbir__vertical_scatter_loop( info, split_info, split_count ); - STBIR_PROFILE_END( looping ); - - return 1; -} - -static void stbir__update_info_from_resize( stbir__info * info, STBIR_RESIZE * resize ) -{ - static stbir__decode_pixels_func * decode_simple[STBIR_TYPE_HALF_FLOAT-STBIR_TYPE_UINT8_SRGB+1]= - { - /* 1ch-4ch */ stbir__decode_uint8_srgb, stbir__decode_uint8_srgb, 0, stbir__decode_float_linear, stbir__decode_half_float_linear, - }; - - static stbir__decode_pixels_func * decode_alphas[STBIRI_AR-STBIRI_RGBA+1][STBIR_TYPE_HALF_FLOAT-STBIR_TYPE_UINT8_SRGB+1]= - { - { /* RGBA */ stbir__decode_uint8_srgb4_linearalpha, stbir__decode_uint8_srgb, 0, stbir__decode_float_linear, stbir__decode_half_float_linear }, - { /* BGRA */ stbir__decode_uint8_srgb4_linearalpha_BGRA, stbir__decode_uint8_srgb_BGRA, 0, stbir__decode_float_linear_BGRA, stbir__decode_half_float_linear_BGRA }, - { /* ARGB */ stbir__decode_uint8_srgb4_linearalpha_ARGB, stbir__decode_uint8_srgb_ARGB, 0, stbir__decode_float_linear_ARGB, stbir__decode_half_float_linear_ARGB }, - { /* ABGR */ stbir__decode_uint8_srgb4_linearalpha_ABGR, stbir__decode_uint8_srgb_ABGR, 0, stbir__decode_float_linear_ABGR, stbir__decode_half_float_linear_ABGR }, - { /* RA */ stbir__decode_uint8_srgb2_linearalpha, stbir__decode_uint8_srgb, 0, stbir__decode_float_linear, stbir__decode_half_float_linear }, - { /* AR */ stbir__decode_uint8_srgb2_linearalpha_AR, stbir__decode_uint8_srgb_AR, 0, stbir__decode_float_linear_AR, stbir__decode_half_float_linear_AR }, - }; - - static stbir__decode_pixels_func * decode_simple_scaled_or_not[2][2]= - { - { stbir__decode_uint8_linear_scaled, stbir__decode_uint8_linear }, { stbir__decode_uint16_linear_scaled, stbir__decode_uint16_linear }, - }; - - static stbir__decode_pixels_func * decode_alphas_scaled_or_not[STBIRI_AR-STBIRI_RGBA+1][2][2]= - { - { /* RGBA */ { stbir__decode_uint8_linear_scaled, stbir__decode_uint8_linear }, { stbir__decode_uint16_linear_scaled, stbir__decode_uint16_linear } }, - { /* BGRA */ { stbir__decode_uint8_linear_scaled_BGRA, stbir__decode_uint8_linear_BGRA }, { stbir__decode_uint16_linear_scaled_BGRA, stbir__decode_uint16_linear_BGRA } }, - { /* ARGB */ { stbir__decode_uint8_linear_scaled_ARGB, stbir__decode_uint8_linear_ARGB }, { stbir__decode_uint16_linear_scaled_ARGB, stbir__decode_uint16_linear_ARGB } }, - { /* ABGR */ { stbir__decode_uint8_linear_scaled_ABGR, stbir__decode_uint8_linear_ABGR }, { stbir__decode_uint16_linear_scaled_ABGR, stbir__decode_uint16_linear_ABGR } }, - { /* RA */ { stbir__decode_uint8_linear_scaled, stbir__decode_uint8_linear }, { stbir__decode_uint16_linear_scaled, stbir__decode_uint16_linear } }, - { /* AR */ { stbir__decode_uint8_linear_scaled_AR, stbir__decode_uint8_linear_AR }, { stbir__decode_uint16_linear_scaled_AR, stbir__decode_uint16_linear_AR } } - }; - - static stbir__encode_pixels_func * encode_simple[STBIR_TYPE_HALF_FLOAT-STBIR_TYPE_UINT8_SRGB+1]= - { - /* 1ch-4ch */ stbir__encode_uint8_srgb, stbir__encode_uint8_srgb, 0, stbir__encode_float_linear, stbir__encode_half_float_linear, - }; - - static stbir__encode_pixels_func * encode_alphas[STBIRI_AR-STBIRI_RGBA+1][STBIR_TYPE_HALF_FLOAT-STBIR_TYPE_UINT8_SRGB+1]= - { - { /* RGBA */ stbir__encode_uint8_srgb4_linearalpha, stbir__encode_uint8_srgb, 0, stbir__encode_float_linear, stbir__encode_half_float_linear }, - { /* BGRA */ stbir__encode_uint8_srgb4_linearalpha_BGRA, stbir__encode_uint8_srgb_BGRA, 0, stbir__encode_float_linear_BGRA, stbir__encode_half_float_linear_BGRA }, - { /* ARGB */ stbir__encode_uint8_srgb4_linearalpha_ARGB, stbir__encode_uint8_srgb_ARGB, 0, stbir__encode_float_linear_ARGB, stbir__encode_half_float_linear_ARGB }, - { /* ABGR */ stbir__encode_uint8_srgb4_linearalpha_ABGR, stbir__encode_uint8_srgb_ABGR, 0, stbir__encode_float_linear_ABGR, stbir__encode_half_float_linear_ABGR }, - { /* RA */ stbir__encode_uint8_srgb2_linearalpha, stbir__encode_uint8_srgb, 0, stbir__encode_float_linear, stbir__encode_half_float_linear }, - { /* AR */ stbir__encode_uint8_srgb2_linearalpha_AR, stbir__encode_uint8_srgb_AR, 0, stbir__encode_float_linear_AR, stbir__encode_half_float_linear_AR } - }; - - static stbir__encode_pixels_func * encode_simple_scaled_or_not[2][2]= - { - { stbir__encode_uint8_linear_scaled, stbir__encode_uint8_linear }, { stbir__encode_uint16_linear_scaled, stbir__encode_uint16_linear }, - }; - - static stbir__encode_pixels_func * encode_alphas_scaled_or_not[STBIRI_AR-STBIRI_RGBA+1][2][2]= - { - { /* RGBA */ { stbir__encode_uint8_linear_scaled, stbir__encode_uint8_linear }, { stbir__encode_uint16_linear_scaled, stbir__encode_uint16_linear } }, - { /* BGRA */ { stbir__encode_uint8_linear_scaled_BGRA, stbir__encode_uint8_linear_BGRA }, { stbir__encode_uint16_linear_scaled_BGRA, stbir__encode_uint16_linear_BGRA } }, - { /* ARGB */ { stbir__encode_uint8_linear_scaled_ARGB, stbir__encode_uint8_linear_ARGB }, { stbir__encode_uint16_linear_scaled_ARGB, stbir__encode_uint16_linear_ARGB } }, - { /* ABGR */ { stbir__encode_uint8_linear_scaled_ABGR, stbir__encode_uint8_linear_ABGR }, { stbir__encode_uint16_linear_scaled_ABGR, stbir__encode_uint16_linear_ABGR } }, - { /* RA */ { stbir__encode_uint8_linear_scaled, stbir__encode_uint8_linear }, { stbir__encode_uint16_linear_scaled, stbir__encode_uint16_linear } }, - { /* AR */ { stbir__encode_uint8_linear_scaled_AR, stbir__encode_uint8_linear_AR }, { stbir__encode_uint16_linear_scaled_AR, stbir__encode_uint16_linear_AR } } - }; - - stbir__decode_pixels_func * decode_pixels = 0; - stbir__encode_pixels_func * encode_pixels = 0; - stbir_datatype input_type, output_type; - - input_type = resize->input_data_type; - output_type = resize->output_data_type; - info->input_data = resize->input_pixels; - info->input_stride_bytes = resize->input_stride_in_bytes; - info->output_stride_bytes = resize->output_stride_in_bytes; - - if ( ( info->horizontal.filter_enum == STBIR_FILTER_POINT_SAMPLE ) && ( info->vertical.filter_enum == STBIR_FILTER_POINT_SAMPLE ) ) - { - if ( ( ( input_type == STBIR_TYPE_UINT8_SRGB ) || ( input_type == STBIR_TYPE_UINT8_SRGB_ALPHA ) ) && - ( ( output_type == STBIR_TYPE_UINT8_SRGB ) || ( output_type == STBIR_TYPE_UINT8_SRGB_ALPHA ) ) ) - { - input_type = STBIR_TYPE_UINT8; - output_type = STBIR_TYPE_UINT8; - } - } - - if ( info->input_stride_bytes == 0 ) - info->input_stride_bytes = info->channels * info->horizontal.scale_info.input_full_size * stbir__type_size[input_type]; - - if ( info->output_stride_bytes == 0 ) - info->output_stride_bytes = info->channels * info->horizontal.scale_info.output_sub_size * stbir__type_size[output_type]; - - info->output_data = ( (char*) resize->output_pixels ) + ( (size_t) info->offset_y * (size_t) resize->output_stride_in_bytes ) + ( info->offset_x * info->channels * stbir__type_size[output_type] ); - - info->in_pixels_cb = resize->input_cb; - info->user_data = resize->user_data; - info->out_pixels_cb = resize->output_cb; - - if ( ( input_type == STBIR_TYPE_UINT8 ) || ( input_type == STBIR_TYPE_UINT16 ) ) - { - int non_scaled = 0; - - if ( ( !info->alpha_weight ) && ( !info->alpha_unweight ) ) // don't short circuit when alpha weighting (get everything to 0-1.0 as usual) - if ( ( ( input_type == STBIR_TYPE_UINT8 ) && ( output_type == STBIR_TYPE_UINT8 ) ) || ( ( input_type == STBIR_TYPE_UINT16 ) && ( output_type == STBIR_TYPE_UINT16 ) ) ) - non_scaled = 1; - - if ( info->input_pixel_layout_internal <= STBIRI_4CHANNEL ) - decode_pixels = decode_simple_scaled_or_not[ input_type == STBIR_TYPE_UINT16 ][ non_scaled ]; - else - decode_pixels = decode_alphas_scaled_or_not[ ( info->input_pixel_layout_internal - STBIRI_RGBA ) % ( STBIRI_AR-STBIRI_RGBA+1 ) ][ input_type == STBIR_TYPE_UINT16 ][ non_scaled ]; - } - else - { - if ( info->input_pixel_layout_internal <= STBIRI_4CHANNEL ) - decode_pixels = decode_simple[ input_type - STBIR_TYPE_UINT8_SRGB ]; - else - decode_pixels = decode_alphas[ ( info->input_pixel_layout_internal - STBIRI_RGBA ) % ( STBIRI_AR-STBIRI_RGBA+1 ) ][ input_type - STBIR_TYPE_UINT8_SRGB ]; - } - - if ( ( output_type == STBIR_TYPE_UINT8 ) || ( output_type == STBIR_TYPE_UINT16 ) ) - { - int non_scaled = 0; - - if ( ( !info->alpha_weight ) && ( !info->alpha_unweight ) ) // don't short circuit when alpha weighting (get everything to 0-1.0 as usual) - if ( ( ( input_type == STBIR_TYPE_UINT8 ) && ( output_type == STBIR_TYPE_UINT8 ) ) || ( ( input_type == STBIR_TYPE_UINT16 ) && ( output_type == STBIR_TYPE_UINT16 ) ) ) - non_scaled = 1; - - if ( info->output_pixel_layout_internal <= STBIRI_4CHANNEL ) - encode_pixels = encode_simple_scaled_or_not[ output_type == STBIR_TYPE_UINT16 ][ non_scaled ]; - else - encode_pixels = encode_alphas_scaled_or_not[ ( info->output_pixel_layout_internal - STBIRI_RGBA ) % ( STBIRI_AR-STBIRI_RGBA+1 ) ][ output_type == STBIR_TYPE_UINT16 ][ non_scaled ]; - } - else - { - if ( info->output_pixel_layout_internal <= STBIRI_4CHANNEL ) - encode_pixels = encode_simple[ output_type - STBIR_TYPE_UINT8_SRGB ]; - else - encode_pixels = encode_alphas[ ( info->output_pixel_layout_internal - STBIRI_RGBA ) % ( STBIRI_AR-STBIRI_RGBA+1 ) ][ output_type - STBIR_TYPE_UINT8_SRGB ]; - } - - info->input_type = input_type; - info->output_type = output_type; - info->decode_pixels = decode_pixels; - info->encode_pixels = encode_pixels; -} - -static void stbir__clip( int * outx, int * outsubw, int outw, double * u0, double * u1 ) -{ - double per, adj; - int over; - - if ( *outx < 0 ) - { - per = ( (double)*outx ) / ( (double)*outsubw ); // is negative - adj = per * ( *u1 - *u0 ); - *u0 -= adj; // increases u0 - *outx = 0; - } - - over = outw - ( *outx + *outsubw ); - if ( over < 0 ) - { - per = ( (double)over ) / ( (double)*outsubw ); // is negative - adj = per * ( *u1 - *u0 ); - *u1 += adj; // decrease u1 - *outsubw = outw - *outx; - } -} - -static int stbir__double_to_rational(double f, stbir_uint32 limit, stbir_uint32 *numer, stbir_uint32 *denom, int limit_denom ) // limit_denom (1) or limit numer (0) -{ - double err; - stbir_uint64 top, bot; - stbir_uint64 numer_last = 0; - stbir_uint64 denom_last = 1; - stbir_uint64 numer_estimate = 1; - stbir_uint64 denom_estimate = 0; - - top = (stbir_uint64)( f * (double)(1 << 25) ); - bot = 1 << 25; - - for(;;) - { - stbir_uint64 est, temp; - - if ( ( ( limit_denom ) ? denom_estimate : numer_estimate ) >= limit ) - break; - - if ( denom_estimate ) - { - err = ( (double)numer_estimate / (double)denom_estimate ) - f; - if ( err < 0.0 ) err = -err; - if ( err < ( 1.0 / (double)(1<<24) ) ) - { - *numer = (stbir_uint32) numer_estimate; - *denom = (stbir_uint32) denom_estimate; - return 1; - } - } - - if ( bot == 0 ) - break; - - est = top / bot; - temp = top % bot; - top = bot; - bot = temp; - - temp = est * denom_estimate + denom_last; - denom_last = denom_estimate; - denom_estimate = temp; - - temp = est * numer_estimate + numer_last; - numer_last = numer_estimate; - numer_estimate = temp; - } - - if ( limit_denom ) - { - numer_estimate= (stbir_uint64)( f * (double)limit + 0.5 ); - denom_estimate = limit; - } - else - { - numer_estimate = limit; - denom_estimate = (stbir_uint64)( ( (double)limit / f ) + 0.5 ); - } - - *numer = (stbir_uint32) numer_estimate; - *denom = (stbir_uint32) denom_estimate; - - err = ( denom_estimate ) ? ( ( (double)(stbir_uint32)numer_estimate / (double)(stbir_uint32)denom_estimate ) - f ) : 1.0; - if ( err < 0.0 ) err = -err; - return ( err < ( 1.0 / (double)(1<<24) ) ) ? 1 : 0; -} - -static int stbir__calculate_region_transform( stbir__scale_info * scale_info, int output_full_range, int * output_offset, int output_sub_range, int input_full_range, double input_s0, double input_s1 ) -{ - double output_range, input_range, output_s, input_s, ratio, scale; - - input_s = input_s1 - input_s0; - - if ( ( output_full_range == 0 ) || ( input_full_range == 0 ) || - ( output_sub_range == 0 ) || ( input_s <= stbir__small_float ) ) - return 0; - - if ( ( *output_offset >= output_full_range ) || ( ( *output_offset + output_sub_range ) <= 0 ) || ( input_s0 >= (1.0f-stbir__small_float) ) || ( input_s1 <= stbir__small_float ) ) - return 0; - - output_range = (double)output_full_range; - input_range = (double)input_full_range; - - output_s = ( (double)output_sub_range) / output_range; - - ratio = output_s / input_s; - - scale = ( output_range / input_range ) * ratio; - scale_info->scale = (float)scale; - scale_info->inv_scale = (float)( 1.0 / scale ); - - stbir__clip( output_offset, &output_sub_range, output_full_range, &input_s0, &input_s1 ); - - input_s = input_s1 - input_s0; - - if ( input_s <= stbir__small_float ) - return 0; - - scale_info->pixel_shift = (float) ( input_s0 * ratio * output_range ); - - scale_info->scale_is_rational = stbir__double_to_rational( scale, ( scale <= 1.0 ) ? output_full_range : input_full_range, &scale_info->scale_numerator, &scale_info->scale_denominator, ( scale >= 1.0 ) ); - - scale_info->input_full_size = input_full_range; - scale_info->output_sub_size = output_sub_range; - - return 1; -} - -static void stbir__init_and_set_layout( STBIR_RESIZE * resize, stbir_pixel_layout pixel_layout, stbir_datatype data_type ) -{ - resize->input_cb = 0; - resize->output_cb = 0; - resize->user_data = resize; - resize->samplers = 0; - resize->called_alloc = 0; - resize->horizontal_filter = STBIR_FILTER_DEFAULT; - resize->horizontal_filter_kernel = 0; resize->horizontal_filter_support = 0; - resize->vertical_filter = STBIR_FILTER_DEFAULT; - resize->vertical_filter_kernel = 0; resize->vertical_filter_support = 0; - resize->horizontal_edge = STBIR_EDGE_CLAMP; - resize->vertical_edge = STBIR_EDGE_CLAMP; - resize->input_s0 = 0; resize->input_t0 = 0; resize->input_s1 = 1; resize->input_t1 = 1; - resize->output_subx = 0; resize->output_suby = 0; resize->output_subw = resize->output_w; resize->output_subh = resize->output_h; - resize->input_data_type = data_type; - resize->output_data_type = data_type; - resize->input_pixel_layout_public = pixel_layout; - resize->output_pixel_layout_public = pixel_layout; - resize->needs_rebuild = 1; -} - -STBIRDEF void stbir_resize_init( STBIR_RESIZE * resize, - const void *input_pixels, int input_w, int input_h, int input_stride_in_bytes, // stride can be zero - void *output_pixels, int output_w, int output_h, int output_stride_in_bytes, // stride can be zero - stbir_pixel_layout pixel_layout, stbir_datatype data_type ) -{ - resize->input_pixels = input_pixels; - resize->input_w = input_w; - resize->input_h = input_h; - resize->input_stride_in_bytes = input_stride_in_bytes; - resize->output_pixels = output_pixels; - resize->output_w = output_w; - resize->output_h = output_h; - resize->output_stride_in_bytes = output_stride_in_bytes; - resize->fast_alpha = 0; - - stbir__init_and_set_layout( resize, pixel_layout, data_type ); -} - -STBIRDEF void stbir_set_datatypes( STBIR_RESIZE * resize, stbir_datatype input_type, stbir_datatype output_type ) // by default, datatype from resize_init -{ - resize->input_data_type = input_type; - resize->output_data_type = output_type; - if ( ( resize->samplers ) && ( !resize->needs_rebuild ) ) - stbir__update_info_from_resize( resize->samplers, resize ); -} - -STBIRDEF void stbir_set_pixel_callbacks( STBIR_RESIZE * resize, stbir_input_callback * input_cb, stbir_output_callback * output_cb ) // no callbacks by default -{ - resize->input_cb = input_cb; - resize->output_cb = output_cb; - - if ( ( resize->samplers ) && ( !resize->needs_rebuild ) ) - { - resize->samplers->in_pixels_cb = input_cb; - resize->samplers->out_pixels_cb = output_cb; - } -} - -STBIRDEF void stbir_set_user_data( STBIR_RESIZE * resize, void * user_data ) // pass back STBIR_RESIZE* by default -{ - resize->user_data = user_data; - if ( ( resize->samplers ) && ( !resize->needs_rebuild ) ) - resize->samplers->user_data = user_data; -} - -STBIRDEF void stbir_set_buffer_ptrs( STBIR_RESIZE * resize, const void * input_pixels, int input_stride_in_bytes, void * output_pixels, int output_stride_in_bytes ) -{ - resize->input_pixels = input_pixels; - resize->input_stride_in_bytes = input_stride_in_bytes; - resize->output_pixels = output_pixels; - resize->output_stride_in_bytes = output_stride_in_bytes; - if ( ( resize->samplers ) && ( !resize->needs_rebuild ) ) - stbir__update_info_from_resize( resize->samplers, resize ); -} - -STBIRDEF int stbir_set_edgemodes( STBIR_RESIZE * resize, stbir_edge horizontal_edge, stbir_edge vertical_edge ) // CLAMP by default -{ - resize->horizontal_edge = horizontal_edge; - resize->vertical_edge = vertical_edge; - resize->needs_rebuild = 1; - return 1; -} - -STBIRDEF int stbir_set_filters( STBIR_RESIZE * resize, stbir_filter horizontal_filter, stbir_filter vertical_filter ) // STBIR_DEFAULT_FILTER_UPSAMPLE/DOWNSAMPLE by default -{ - resize->horizontal_filter = horizontal_filter; - resize->vertical_filter = vertical_filter; - resize->needs_rebuild = 1; - return 1; -} - -STBIRDEF int stbir_set_filter_callbacks( STBIR_RESIZE * resize, stbir__kernel_callback * horizontal_filter, stbir__support_callback * horizontal_support, stbir__kernel_callback * vertical_filter, stbir__support_callback * vertical_support ) -{ - resize->horizontal_filter_kernel = horizontal_filter; resize->horizontal_filter_support = horizontal_support; - resize->vertical_filter_kernel = vertical_filter; resize->vertical_filter_support = vertical_support; - resize->needs_rebuild = 1; - return 1; -} - -STBIRDEF int stbir_set_pixel_layouts( STBIR_RESIZE * resize, stbir_pixel_layout input_pixel_layout, stbir_pixel_layout output_pixel_layout ) // sets new pixel layouts -{ - resize->input_pixel_layout_public = input_pixel_layout; - resize->output_pixel_layout_public = output_pixel_layout; - resize->needs_rebuild = 1; - return 1; -} - -STBIRDEF int stbir_set_non_pm_alpha_speed_over_quality( STBIR_RESIZE * resize, int non_pma_alpha_speed_over_quality ) // sets alpha speed -{ - resize->fast_alpha = non_pma_alpha_speed_over_quality; - resize->needs_rebuild = 1; - return 1; -} - -STBIRDEF int stbir_set_input_subrect( STBIR_RESIZE * resize, double s0, double t0, double s1, double t1 ) // sets input region (full region by default) -{ - resize->input_s0 = s0; - resize->input_t0 = t0; - resize->input_s1 = s1; - resize->input_t1 = t1; - resize->needs_rebuild = 1; - - if ( ( s1 < stbir__small_float ) || ( (s1-s0) < stbir__small_float ) || - ( t1 < stbir__small_float ) || ( (t1-t0) < stbir__small_float ) || - ( s0 > (1.0f-stbir__small_float) ) || - ( t0 > (1.0f-stbir__small_float) ) ) - return 0; - - return 1; -} - -STBIRDEF int stbir_set_output_pixel_subrect( STBIR_RESIZE * resize, int subx, int suby, int subw, int subh ) // sets input region (full region by default) -{ - resize->output_subx = subx; - resize->output_suby = suby; - resize->output_subw = subw; - resize->output_subh = subh; - resize->needs_rebuild = 1; - - if ( ( subx >= resize->output_w ) || ( ( subx + subw ) <= 0 ) || ( suby >= resize->output_h ) || ( ( suby + subh ) <= 0 ) || ( subw == 0 ) || ( subh == 0 ) ) - return 0; - - return 1; -} - -STBIRDEF int stbir_set_pixel_subrect( STBIR_RESIZE * resize, int subx, int suby, int subw, int subh ) // sets both regions (full regions by default) -{ - double s0, t0, s1, t1; - - s0 = ( (double)subx ) / ( (double)resize->output_w ); - t0 = ( (double)suby ) / ( (double)resize->output_h ); - s1 = ( (double)(subx+subw) ) / ( (double)resize->output_w ); - t1 = ( (double)(suby+subh) ) / ( (double)resize->output_h ); - - resize->input_s0 = s0; - resize->input_t0 = t0; - resize->input_s1 = s1; - resize->input_t1 = t1; - resize->output_subx = subx; - resize->output_suby = suby; - resize->output_subw = subw; - resize->output_subh = subh; - resize->needs_rebuild = 1; - - if ( ( subx >= resize->output_w ) || ( ( subx + subw ) <= 0 ) || ( suby >= resize->output_h ) || ( ( suby + subh ) <= 0 ) || ( subw == 0 ) || ( subh == 0 ) ) - return 0; - - return 1; -} - -static int stbir__perform_build( STBIR_RESIZE * resize, int splits ) -{ - stbir__contributors conservative = { 0, 0 }; - stbir__sampler horizontal, vertical; - int new_output_subx, new_output_suby; - stbir__info * out_info; - - if ( resize->samplers ) - return 0; - - #define STBIR_RETURN_ERROR_AND_ASSERT( exp ) STBIR_ASSERT( !(exp) ); if (exp) return 0; - STBIR_RETURN_ERROR_AND_ASSERT( (unsigned)resize->horizontal_filter >= STBIR_FILTER_OTHER) - STBIR_RETURN_ERROR_AND_ASSERT( (unsigned)resize->vertical_filter >= STBIR_FILTER_OTHER) - #undef STBIR_RETURN_ERROR_AND_ASSERT - - if ( splits <= 0 ) - return 0; - - STBIR_PROFILE_BUILD_FIRST_START( build ); - - new_output_subx = resize->output_subx; - new_output_suby = resize->output_suby; - - if ( !stbir__calculate_region_transform( &horizontal.scale_info, resize->output_w, &new_output_subx, resize->output_subw, resize->input_w, resize->input_s0, resize->input_s1 ) ) - return 0; - - if ( !stbir__calculate_region_transform( &vertical.scale_info, resize->output_h, &new_output_suby, resize->output_subh, resize->input_h, resize->input_t0, resize->input_t1 ) ) - return 0; - - if ( ( horizontal.scale_info.output_sub_size == 0 ) || ( vertical.scale_info.output_sub_size == 0 ) ) - return 0; - - stbir__set_sampler(&horizontal, resize->horizontal_filter, resize->horizontal_filter_kernel, resize->horizontal_filter_support, resize->horizontal_edge, &horizontal.scale_info, 1, resize->user_data ); - stbir__get_conservative_extents( &horizontal, &conservative, resize->user_data ); - stbir__set_sampler(&vertical, resize->vertical_filter, resize->horizontal_filter_kernel, resize->vertical_filter_support, resize->vertical_edge, &vertical.scale_info, 0, resize->user_data ); - - if ( ( vertical.scale_info.output_sub_size / splits ) < STBIR_FORCE_MINIMUM_SCANLINES_FOR_SPLITS ) // each split should be a minimum of 4 scanlines (handwavey choice) - { - splits = vertical.scale_info.output_sub_size / STBIR_FORCE_MINIMUM_SCANLINES_FOR_SPLITS; - if ( splits == 0 ) splits = 1; - } - - STBIR_PROFILE_BUILD_START( alloc ); - out_info = stbir__alloc_internal_mem_and_build_samplers( &horizontal, &vertical, &conservative, resize->input_pixel_layout_public, resize->output_pixel_layout_public, splits, new_output_subx, new_output_suby, resize->fast_alpha, resize->user_data STBIR_ONLY_PROFILE_BUILD_SET_INFO ); - STBIR_PROFILE_BUILD_END( alloc ); - STBIR_PROFILE_BUILD_END( build ); - - if ( out_info ) - { - resize->splits = splits; - resize->samplers = out_info; - resize->needs_rebuild = 0; - - stbir__update_info_from_resize( out_info, resize ); - - return splits; - } - - return 0; -} - -void stbir_free_samplers( STBIR_RESIZE * resize ) -{ - if ( resize->samplers ) - { - stbir__free_internal_mem( resize->samplers ); - resize->samplers = 0; - resize->called_alloc = 0; - } -} - -STBIRDEF int stbir_build_samplers_with_splits( STBIR_RESIZE * resize, int splits ) -{ - if ( ( resize->samplers == 0 ) || ( resize->needs_rebuild ) ) - { - if ( resize->samplers ) - stbir_free_samplers( resize ); - - resize->called_alloc = 1; - return stbir__perform_build( resize, splits ); - } - - STBIR_PROFILE_BUILD_CLEAR( resize->samplers ); - - return 1; -} - -STBIRDEF int stbir_build_samplers( STBIR_RESIZE * resize ) -{ - return stbir_build_samplers_with_splits( resize, 1 ); -} - -STBIRDEF int stbir_resize_extended( STBIR_RESIZE * resize ) -{ - int result; - - if ( ( resize->samplers == 0 ) || ( resize->needs_rebuild ) ) - { - int alloc_state = resize->called_alloc; // remember allocated state - - if ( resize->samplers ) - { - stbir__free_internal_mem( resize->samplers ); - resize->samplers = 0; - } - - if ( !stbir_build_samplers( resize ) ) - return 0; - - resize->called_alloc = alloc_state; - - // if build_samplers succeeded (above), but there are no samplers set, then - // the area to stretch into was zero pixels, so don't do anything and return - // success - if ( resize->samplers == 0 ) - return 1; - } - else - { - // didn't build anything - clear it - STBIR_PROFILE_BUILD_CLEAR( resize->samplers ); - } - - // do resize - result = stbir__perform_resize( resize->samplers, 0, resize->splits ); - - // if we alloced, then free - if ( !resize->called_alloc ) - { - stbir_free_samplers( resize ); - resize->samplers = 0; - } - - return result; -} - -STBIRDEF int stbir_resize_extended_split( STBIR_RESIZE * resize, int split_start, int split_count ) -{ - STBIR_ASSERT( resize->samplers ); - - // if we're just doing the whole thing, call full - if ( ( split_start == -1 ) || ( ( split_start == 0 ) && ( split_count == resize->splits ) ) ) - return stbir_resize_extended( resize ); - - // you must build samplers first when using split resize - if ( ( resize->samplers == 0 ) || ( resize->needs_rebuild ) ) - return 0; - - if ( ( split_start >= resize->splits ) || ( split_start < 0 ) || ( ( split_start + split_count ) > resize->splits ) || ( split_count <= 0 ) ) - return 0; - - // do resize - return stbir__perform_resize( resize->samplers, split_start, split_count ); -} - -static int stbir__check_output_stuff( void ** ret_ptr, int * ret_pitch, void * output_pixels, int type_size, int output_w, int output_h, int output_stride_in_bytes, stbir_internal_pixel_layout pixel_layout ) -{ - size_t size; - int pitch; - void * ptr; - - pitch = output_w * type_size * stbir__pixel_channels[ pixel_layout ]; - if ( pitch == 0 ) - return 0; - - if ( output_stride_in_bytes == 0 ) - output_stride_in_bytes = pitch; - - if ( output_stride_in_bytes < pitch ) - return 0; - - size = (size_t)output_stride_in_bytes * (size_t)output_h; - if ( size == 0 ) - return 0; - - *ret_ptr = 0; - *ret_pitch = output_stride_in_bytes; - - if ( output_pixels == 0 ) - { - ptr = STBIR_MALLOC( size, 0 ); - if ( ptr == 0 ) - return 0; - - *ret_ptr = ptr; - *ret_pitch = pitch; - } - - return 1; -} - -STBIRDEF unsigned char * stbir_resize_uint8_linear( const unsigned char *input_pixels , int input_w , int input_h, int input_stride_in_bytes, - unsigned char *output_pixels, int output_w, int output_h, int output_stride_in_bytes, - stbir_pixel_layout pixel_layout ) -{ - STBIR_RESIZE resize; - unsigned char * optr; - int opitch; - - if ( !stbir__check_output_stuff( (void**)&optr, &opitch, output_pixels, sizeof( unsigned char ), output_w, output_h, output_stride_in_bytes, stbir__pixel_layout_convert_public_to_internal[ pixel_layout ] ) ) - return 0; - - stbir_resize_init( &resize, - input_pixels, input_w, input_h, input_stride_in_bytes, - (optr) ? optr : output_pixels, output_w, output_h, opitch, - pixel_layout, STBIR_TYPE_UINT8 ); - - if ( !stbir_resize_extended( &resize ) ) - { - if ( optr ) - STBIR_FREE( optr, 0 ); - return 0; - } - - return (optr) ? optr : output_pixels; -} - -STBIRDEF unsigned char * stbir_resize_uint8_srgb( const unsigned char *input_pixels , int input_w , int input_h, int input_stride_in_bytes, - unsigned char *output_pixels, int output_w, int output_h, int output_stride_in_bytes, - stbir_pixel_layout pixel_layout ) -{ - STBIR_RESIZE resize; - unsigned char * optr; - int opitch; - - if ( !stbir__check_output_stuff( (void**)&optr, &opitch, output_pixels, sizeof( unsigned char ), output_w, output_h, output_stride_in_bytes, stbir__pixel_layout_convert_public_to_internal[ pixel_layout ] ) ) - return 0; - - stbir_resize_init( &resize, - input_pixels, input_w, input_h, input_stride_in_bytes, - (optr) ? optr : output_pixels, output_w, output_h, opitch, - pixel_layout, STBIR_TYPE_UINT8_SRGB ); - - if ( !stbir_resize_extended( &resize ) ) - { - if ( optr ) - STBIR_FREE( optr, 0 ); - return 0; - } - - return (optr) ? optr : output_pixels; -} - -STBIRDEF float * stbir_resize_float_linear( const float *input_pixels , int input_w , int input_h, int input_stride_in_bytes, - float *output_pixels, int output_w, int output_h, int output_stride_in_bytes, - stbir_pixel_layout pixel_layout ) -{ - STBIR_RESIZE resize; - float * optr; - int opitch; - - if ( !stbir__check_output_stuff( (void**)&optr, &opitch, output_pixels, sizeof( float ), output_w, output_h, output_stride_in_bytes, stbir__pixel_layout_convert_public_to_internal[ pixel_layout ] ) ) - return 0; - - stbir_resize_init( &resize, - input_pixels, input_w, input_h, input_stride_in_bytes, - (optr) ? optr : output_pixels, output_w, output_h, opitch, - pixel_layout, STBIR_TYPE_FLOAT ); - - if ( !stbir_resize_extended( &resize ) ) - { - if ( optr ) - STBIR_FREE( optr, 0 ); - return 0; - } - - return (optr) ? optr : output_pixels; -} - -STBIRDEF void * stbir_resize( const void *input_pixels , int input_w , int input_h, int input_stride_in_bytes, - void *output_pixels, int output_w, int output_h, int output_stride_in_bytes, - stbir_pixel_layout pixel_layout, stbir_datatype data_type, - stbir_edge edge, stbir_filter filter ) -{ - STBIR_RESIZE resize; - float * optr; - int opitch; - - if ( !stbir__check_output_stuff( (void**)&optr, &opitch, output_pixels, stbir__type_size[data_type], output_w, output_h, output_stride_in_bytes, stbir__pixel_layout_convert_public_to_internal[ pixel_layout ] ) ) - return 0; - - stbir_resize_init( &resize, - input_pixels, input_w, input_h, input_stride_in_bytes, - (optr) ? optr : output_pixels, output_w, output_h, output_stride_in_bytes, - pixel_layout, data_type ); - - resize.horizontal_edge = edge; - resize.vertical_edge = edge; - resize.horizontal_filter = filter; - resize.vertical_filter = filter; - - if ( !stbir_resize_extended( &resize ) ) - { - if ( optr ) - STBIR_FREE( optr, 0 ); - return 0; - } - - return (optr) ? optr : output_pixels; -} - -#ifdef STBIR_PROFILE - -STBIRDEF void stbir_resize_build_profile_info( STBIR_PROFILE_INFO * info, STBIR_RESIZE const * resize ) -{ - static char const * bdescriptions[6] = { "Building", "Allocating", "Horizontal sampler", "Vertical sampler", "Coefficient cleanup", "Coefficient piovot" } ; - stbir__info* samp = resize->samplers; - int i; - - typedef int testa[ (STBIR__ARRAY_SIZE( bdescriptions ) == (STBIR__ARRAY_SIZE( samp->profile.array )-1) )?1:-1]; - typedef int testb[ (sizeof( samp->profile.array ) == (sizeof(samp->profile.named)) )?1:-1]; - typedef int testc[ (sizeof( info->clocks ) >= (sizeof(samp->profile.named)) )?1:-1]; - - for( i = 0 ; i < STBIR__ARRAY_SIZE( bdescriptions ) ; i++) - info->clocks[i] = samp->profile.array[i+1]; - - info->total_clocks = samp->profile.named.total; - info->descriptions = bdescriptions; - info->count = STBIR__ARRAY_SIZE( bdescriptions ); -} - -STBIRDEF void stbir_resize_split_profile_info( STBIR_PROFILE_INFO * info, STBIR_RESIZE const * resize, int split_start, int split_count ) -{ - static char const * descriptions[7] = { "Looping", "Vertical sampling", "Horizontal sampling", "Scanline input", "Scanline output", "Alpha weighting", "Alpha unweighting" }; - stbir__per_split_info * split_info; - int s, i; - - typedef int testa[ (STBIR__ARRAY_SIZE( descriptions ) == (STBIR__ARRAY_SIZE( split_info->profile.array )-1) )?1:-1]; - typedef int testb[ (sizeof( split_info->profile.array ) == (sizeof(split_info->profile.named)) )?1:-1]; - typedef int testc[ (sizeof( info->clocks ) >= (sizeof(split_info->profile.named)) )?1:-1]; - - if ( split_start == -1 ) - { - split_start = 0; - split_count = resize->samplers->splits; - } - - if ( ( split_start >= resize->splits ) || ( split_start < 0 ) || ( ( split_start + split_count ) > resize->splits ) || ( split_count <= 0 ) ) - { - info->total_clocks = 0; - info->descriptions = 0; - info->count = 0; - return; - } - - split_info = resize->samplers->split_info + split_start; - - for( i = 0 ; i < STBIR__ARRAY_SIZE( descriptions ) ; i++ ) - { - stbir_uint64 sum = 0; - for( s = 0 ; s < split_count ; s++ ) - sum += split_info[s].profile.array[i+1]; - info->clocks[i] = sum; - } - - info->total_clocks = split_info->profile.named.total; - info->descriptions = descriptions; - info->count = STBIR__ARRAY_SIZE( descriptions ); -} - -STBIRDEF void stbir_resize_extended_profile_info( STBIR_PROFILE_INFO * info, STBIR_RESIZE const * resize ) -{ - stbir_resize_split_profile_info( info, resize, -1, 0 ); -} - -#endif // STBIR_PROFILE - -#undef STBIR_BGR -#undef STBIR_1CHANNEL -#undef STBIR_2CHANNEL -#undef STBIR_RGB -#undef STBIR_RGBA -#undef STBIR_4CHANNEL -#undef STBIR_BGRA -#undef STBIR_ARGB -#undef STBIR_ABGR -#undef STBIR_RA -#undef STBIR_AR -#undef STBIR_RGBA_PM -#undef STBIR_BGRA_PM -#undef STBIR_ARGB_PM -#undef STBIR_ABGR_PM -#undef STBIR_RA_PM -#undef STBIR_AR_PM - -#endif // STB_IMAGE_RESIZE_IMPLEMENTATION - -#else // STB_IMAGE_RESIZE_HORIZONTALS&STB_IMAGE_RESIZE_DO_VERTICALS - -#define STBIR_strs_join2( start, mid, end ) start##mid##end -#define STBIR_strs_join1( start, mid, end ) STBIR_strs_join2( start, mid, end ) - -#define STBIR_strs_join24( start, mid1, mid2, end ) start##mid1##mid2##end -#define STBIR_strs_join14( start, mid1, mid2, end ) STBIR_strs_join24( start, mid1, mid2, end ) - -#ifdef STB_IMAGE_RESIZE_DO_CODERS - -#ifdef stbir__decode_suffix -#define STBIR__CODER_NAME( name ) STBIR_strs_join1( name, _, stbir__decode_suffix ) -#else -#define STBIR__CODER_NAME( name ) name -#endif - -#ifdef stbir__decode_swizzle -#define stbir__decode_simdf8_flip(reg) STBIR_strs_join1( STBIR_strs_join1( STBIR_strs_join1( STBIR_strs_join1( stbir__simdf8_0123to,stbir__decode_order0,stbir__decode_order1),stbir__decode_order2,stbir__decode_order3),stbir__decode_order0,stbir__decode_order1),stbir__decode_order2,stbir__decode_order3)(reg, reg) -#define stbir__decode_simdf4_flip(reg) STBIR_strs_join1( STBIR_strs_join1( stbir__simdf_0123to,stbir__decode_order0,stbir__decode_order1),stbir__decode_order2,stbir__decode_order3)(reg, reg) -#define stbir__encode_simdf8_unflip(reg) STBIR_strs_join1( STBIR_strs_join1( STBIR_strs_join1( STBIR_strs_join1( stbir__simdf8_0123to,stbir__encode_order0,stbir__encode_order1),stbir__encode_order2,stbir__encode_order3),stbir__encode_order0,stbir__encode_order1),stbir__encode_order2,stbir__encode_order3)(reg, reg) -#define stbir__encode_simdf4_unflip(reg) STBIR_strs_join1( STBIR_strs_join1( stbir__simdf_0123to,stbir__encode_order0,stbir__encode_order1),stbir__encode_order2,stbir__encode_order3)(reg, reg) -#else -#define stbir__decode_order0 0 -#define stbir__decode_order1 1 -#define stbir__decode_order2 2 -#define stbir__decode_order3 3 -#define stbir__encode_order0 0 -#define stbir__encode_order1 1 -#define stbir__encode_order2 2 -#define stbir__encode_order3 3 -#define stbir__decode_simdf8_flip(reg) -#define stbir__decode_simdf4_flip(reg) -#define stbir__encode_simdf8_unflip(reg) -#define stbir__encode_simdf4_unflip(reg) -#endif - -#ifdef STBIR_SIMD8 -#define stbir__encode_simdfX_unflip stbir__encode_simdf8_unflip -#else -#define stbir__encode_simdfX_unflip stbir__encode_simdf4_unflip -#endif - -static float * STBIR__CODER_NAME( stbir__decode_uint8_linear_scaled )( float * decodep, int width_times_channels, void const * inputp ) -{ - float STBIR_STREAMOUT_PTR( * ) decode = decodep; - float * decode_end = (float*) decode + width_times_channels; - unsigned char const * input = (unsigned char const*)inputp; - - #ifdef STBIR_SIMD - unsigned char const * end_input_m16 = input + width_times_channels - 16; - if ( width_times_channels >= 16 ) - { - decode_end -= 16; - STBIR_NO_UNROLL_LOOP_START_INF_FOR - for(;;) - { - #ifdef STBIR_SIMD8 - stbir__simdi i; stbir__simdi8 o0,o1; - stbir__simdf8 of0, of1; - STBIR_NO_UNROLL(decode); - stbir__simdi_load( i, input ); - stbir__simdi8_expand_u8_to_u32( o0, o1, i ); - stbir__simdi8_convert_i32_to_float( of0, o0 ); - stbir__simdi8_convert_i32_to_float( of1, o1 ); - stbir__simdf8_mult( of0, of0, STBIR_max_uint8_as_float_inverted8); - stbir__simdf8_mult( of1, of1, STBIR_max_uint8_as_float_inverted8); - stbir__decode_simdf8_flip( of0 ); - stbir__decode_simdf8_flip( of1 ); - stbir__simdf8_store( decode + 0, of0 ); - stbir__simdf8_store( decode + 8, of1 ); - #else - stbir__simdi i, o0, o1, o2, o3; - stbir__simdf of0, of1, of2, of3; - STBIR_NO_UNROLL(decode); - stbir__simdi_load( i, input ); - stbir__simdi_expand_u8_to_u32( o0,o1,o2,o3,i); - stbir__simdi_convert_i32_to_float( of0, o0 ); - stbir__simdi_convert_i32_to_float( of1, o1 ); - stbir__simdi_convert_i32_to_float( of2, o2 ); - stbir__simdi_convert_i32_to_float( of3, o3 ); - stbir__simdf_mult( of0, of0, STBIR__CONSTF(STBIR_max_uint8_as_float_inverted) ); - stbir__simdf_mult( of1, of1, STBIR__CONSTF(STBIR_max_uint8_as_float_inverted) ); - stbir__simdf_mult( of2, of2, STBIR__CONSTF(STBIR_max_uint8_as_float_inverted) ); - stbir__simdf_mult( of3, of3, STBIR__CONSTF(STBIR_max_uint8_as_float_inverted) ); - stbir__decode_simdf4_flip( of0 ); - stbir__decode_simdf4_flip( of1 ); - stbir__decode_simdf4_flip( of2 ); - stbir__decode_simdf4_flip( of3 ); - stbir__simdf_store( decode + 0, of0 ); - stbir__simdf_store( decode + 4, of1 ); - stbir__simdf_store( decode + 8, of2 ); - stbir__simdf_store( decode + 12, of3 ); - #endif - decode += 16; - input += 16; - if ( decode <= decode_end ) - continue; - if ( decode == ( decode_end + 16 ) ) - break; - decode = decode_end; // backup and do last couple - input = end_input_m16; - } - return decode_end + 16; - } - #endif - - #if stbir__coder_min_num != 3 // doesn't divide cleanly by four - decode += 4; - STBIR_SIMD_NO_UNROLL_LOOP_START - while( decode <= decode_end ) - { - STBIR_SIMD_NO_UNROLL(decode); - decode[0-4] = ((float)(input[stbir__decode_order0])) * stbir__max_uint8_as_float_inverted; - decode[1-4] = ((float)(input[stbir__decode_order1])) * stbir__max_uint8_as_float_inverted; - decode[2-4] = ((float)(input[stbir__decode_order2])) * stbir__max_uint8_as_float_inverted; - decode[3-4] = ((float)(input[stbir__decode_order3])) * stbir__max_uint8_as_float_inverted; - decode += 4; - input += 4; - } - decode -= 4; - #endif - - #if stbir__coder_min_num < 4 - STBIR_NO_UNROLL_LOOP_START - while( decode < decode_end ) - { - STBIR_NO_UNROLL(decode); - decode[0] = ((float)(input[stbir__decode_order0])) * stbir__max_uint8_as_float_inverted; - #if stbir__coder_min_num >= 2 - decode[1] = ((float)(input[stbir__decode_order1])) * stbir__max_uint8_as_float_inverted; - #endif - #if stbir__coder_min_num >= 3 - decode[2] = ((float)(input[stbir__decode_order2])) * stbir__max_uint8_as_float_inverted; - #endif - decode += stbir__coder_min_num; - input += stbir__coder_min_num; - } - #endif - - return decode_end; -} - -static void STBIR__CODER_NAME( stbir__encode_uint8_linear_scaled )( void * outputp, int width_times_channels, float const * encode ) -{ - unsigned char STBIR_SIMD_STREAMOUT_PTR( * ) output = (unsigned char *) outputp; - unsigned char * end_output = ( (unsigned char *) output ) + width_times_channels; - - #ifdef STBIR_SIMD - if ( width_times_channels >= stbir__simdfX_float_count*2 ) - { - float const * end_encode_m8 = encode + width_times_channels - stbir__simdfX_float_count*2; - end_output -= stbir__simdfX_float_count*2; - STBIR_NO_UNROLL_LOOP_START_INF_FOR - for(;;) - { - stbir__simdfX e0, e1; - stbir__simdi i; - STBIR_SIMD_NO_UNROLL(encode); - stbir__simdfX_madd_mem( e0, STBIR_simd_point5X, STBIR_max_uint8_as_floatX, encode ); - stbir__simdfX_madd_mem( e1, STBIR_simd_point5X, STBIR_max_uint8_as_floatX, encode+stbir__simdfX_float_count ); - stbir__encode_simdfX_unflip( e0 ); - stbir__encode_simdfX_unflip( e1 ); - #ifdef STBIR_SIMD8 - stbir__simdf8_pack_to_16bytes( i, e0, e1 ); - stbir__simdi_store( output, i ); - #else - stbir__simdf_pack_to_8bytes( i, e0, e1 ); - stbir__simdi_store2( output, i ); - #endif - encode += stbir__simdfX_float_count*2; - output += stbir__simdfX_float_count*2; - if ( output <= end_output ) - continue; - if ( output == ( end_output + stbir__simdfX_float_count*2 ) ) - break; - output = end_output; // backup and do last couple - encode = end_encode_m8; - } - return; - } - - #if stbir__coder_min_num != 3 // doesn't divide cleanly by four - output += 4; - STBIR_NO_UNROLL_LOOP_START - while( output <= end_output ) - { - stbir__simdf e0; - stbir__simdi i0; - STBIR_NO_UNROLL(encode); - stbir__simdf_load( e0, encode ); - stbir__simdf_madd( e0, STBIR__CONSTF(STBIR_simd_point5), STBIR__CONSTF(STBIR_max_uint8_as_float), e0 ); - stbir__encode_simdf4_unflip( e0 ); - stbir__simdf_pack_to_8bytes( i0, e0, e0 ); // only use first 4 - *(int*)(output-4) = stbir__simdi_to_int( i0 ); - output += 4; - encode += 4; - } - output -= 4; - #endif - - #if stbir__coder_min_num < 4 - STBIR_NO_UNROLL_LOOP_START - while( output < end_output ) - { - stbir__simdf e0; - STBIR_NO_UNROLL(encode); - stbir__simdf_madd1_mem( e0, STBIR__CONSTF(STBIR_simd_point5), STBIR__CONSTF(STBIR_max_uint8_as_float), encode+stbir__encode_order0 ); output[0] = stbir__simdf_convert_float_to_uint8( e0 ); - #if stbir__coder_min_num >= 2 - stbir__simdf_madd1_mem( e0, STBIR__CONSTF(STBIR_simd_point5), STBIR__CONSTF(STBIR_max_uint8_as_float), encode+stbir__encode_order1 ); output[1] = stbir__simdf_convert_float_to_uint8( e0 ); - #endif - #if stbir__coder_min_num >= 3 - stbir__simdf_madd1_mem( e0, STBIR__CONSTF(STBIR_simd_point5), STBIR__CONSTF(STBIR_max_uint8_as_float), encode+stbir__encode_order2 ); output[2] = stbir__simdf_convert_float_to_uint8( e0 ); - #endif - output += stbir__coder_min_num; - encode += stbir__coder_min_num; - } - #endif - - #else - - #if stbir__coder_min_num != 3 // doesn't divide cleanly by four - output += 4; - while( output <= end_output ) - { - float f; - f = encode[stbir__encode_order0] * stbir__max_uint8_as_float + 0.5f; STBIR_CLAMP(f, 0, 255); output[0-4] = (unsigned char)f; - f = encode[stbir__encode_order1] * stbir__max_uint8_as_float + 0.5f; STBIR_CLAMP(f, 0, 255); output[1-4] = (unsigned char)f; - f = encode[stbir__encode_order2] * stbir__max_uint8_as_float + 0.5f; STBIR_CLAMP(f, 0, 255); output[2-4] = (unsigned char)f; - f = encode[stbir__encode_order3] * stbir__max_uint8_as_float + 0.5f; STBIR_CLAMP(f, 0, 255); output[3-4] = (unsigned char)f; - output += 4; - encode += 4; - } - output -= 4; - #endif - - #if stbir__coder_min_num < 4 - STBIR_NO_UNROLL_LOOP_START - while( output < end_output ) - { - float f; - STBIR_NO_UNROLL(encode); - f = encode[stbir__encode_order0] * stbir__max_uint8_as_float + 0.5f; STBIR_CLAMP(f, 0, 255); output[0] = (unsigned char)f; - #if stbir__coder_min_num >= 2 - f = encode[stbir__encode_order1] * stbir__max_uint8_as_float + 0.5f; STBIR_CLAMP(f, 0, 255); output[1] = (unsigned char)f; - #endif - #if stbir__coder_min_num >= 3 - f = encode[stbir__encode_order2] * stbir__max_uint8_as_float + 0.5f; STBIR_CLAMP(f, 0, 255); output[2] = (unsigned char)f; - #endif - output += stbir__coder_min_num; - encode += stbir__coder_min_num; - } - #endif - #endif -} - -static float * STBIR__CODER_NAME(stbir__decode_uint8_linear)( float * decodep, int width_times_channels, void const * inputp ) -{ - float STBIR_STREAMOUT_PTR( * ) decode = decodep; - float * decode_end = (float*) decode + width_times_channels; - unsigned char const * input = (unsigned char const*)inputp; - - #ifdef STBIR_SIMD - unsigned char const * end_input_m16 = input + width_times_channels - 16; - if ( width_times_channels >= 16 ) - { - decode_end -= 16; - STBIR_NO_UNROLL_LOOP_START_INF_FOR - for(;;) - { - #ifdef STBIR_SIMD8 - stbir__simdi i; stbir__simdi8 o0,o1; - stbir__simdf8 of0, of1; - STBIR_NO_UNROLL(decode); - stbir__simdi_load( i, input ); - stbir__simdi8_expand_u8_to_u32( o0, o1, i ); - stbir__simdi8_convert_i32_to_float( of0, o0 ); - stbir__simdi8_convert_i32_to_float( of1, o1 ); - stbir__decode_simdf8_flip( of0 ); - stbir__decode_simdf8_flip( of1 ); - stbir__simdf8_store( decode + 0, of0 ); - stbir__simdf8_store( decode + 8, of1 ); - #else - stbir__simdi i, o0, o1, o2, o3; - stbir__simdf of0, of1, of2, of3; - STBIR_NO_UNROLL(decode); - stbir__simdi_load( i, input ); - stbir__simdi_expand_u8_to_u32( o0,o1,o2,o3,i); - stbir__simdi_convert_i32_to_float( of0, o0 ); - stbir__simdi_convert_i32_to_float( of1, o1 ); - stbir__simdi_convert_i32_to_float( of2, o2 ); - stbir__simdi_convert_i32_to_float( of3, o3 ); - stbir__decode_simdf4_flip( of0 ); - stbir__decode_simdf4_flip( of1 ); - stbir__decode_simdf4_flip( of2 ); - stbir__decode_simdf4_flip( of3 ); - stbir__simdf_store( decode + 0, of0 ); - stbir__simdf_store( decode + 4, of1 ); - stbir__simdf_store( decode + 8, of2 ); - stbir__simdf_store( decode + 12, of3 ); -#endif - decode += 16; - input += 16; - if ( decode <= decode_end ) - continue; - if ( decode == ( decode_end + 16 ) ) - break; - decode = decode_end; // backup and do last couple - input = end_input_m16; - } - return decode_end + 16; - } - #endif - - #if stbir__coder_min_num != 3 // doesn't divide cleanly by four - decode += 4; - STBIR_SIMD_NO_UNROLL_LOOP_START - while( decode <= decode_end ) - { - STBIR_SIMD_NO_UNROLL(decode); - decode[0-4] = ((float)(input[stbir__decode_order0])); - decode[1-4] = ((float)(input[stbir__decode_order1])); - decode[2-4] = ((float)(input[stbir__decode_order2])); - decode[3-4] = ((float)(input[stbir__decode_order3])); - decode += 4; - input += 4; - } - decode -= 4; - #endif - - #if stbir__coder_min_num < 4 - STBIR_NO_UNROLL_LOOP_START - while( decode < decode_end ) - { - STBIR_NO_UNROLL(decode); - decode[0] = ((float)(input[stbir__decode_order0])); - #if stbir__coder_min_num >= 2 - decode[1] = ((float)(input[stbir__decode_order1])); - #endif - #if stbir__coder_min_num >= 3 - decode[2] = ((float)(input[stbir__decode_order2])); - #endif - decode += stbir__coder_min_num; - input += stbir__coder_min_num; - } - #endif - return decode_end; -} - -static void STBIR__CODER_NAME( stbir__encode_uint8_linear )( void * outputp, int width_times_channels, float const * encode ) -{ - unsigned char STBIR_SIMD_STREAMOUT_PTR( * ) output = (unsigned char *) outputp; - unsigned char * end_output = ( (unsigned char *) output ) + width_times_channels; - - #ifdef STBIR_SIMD - if ( width_times_channels >= stbir__simdfX_float_count*2 ) - { - float const * end_encode_m8 = encode + width_times_channels - stbir__simdfX_float_count*2; - end_output -= stbir__simdfX_float_count*2; - STBIR_SIMD_NO_UNROLL_LOOP_START_INF_FOR - for(;;) - { - stbir__simdfX e0, e1; - stbir__simdi i; - STBIR_SIMD_NO_UNROLL(encode); - stbir__simdfX_add_mem( e0, STBIR_simd_point5X, encode ); - stbir__simdfX_add_mem( e1, STBIR_simd_point5X, encode+stbir__simdfX_float_count ); - stbir__encode_simdfX_unflip( e0 ); - stbir__encode_simdfX_unflip( e1 ); - #ifdef STBIR_SIMD8 - stbir__simdf8_pack_to_16bytes( i, e0, e1 ); - stbir__simdi_store( output, i ); - #else - stbir__simdf_pack_to_8bytes( i, e0, e1 ); - stbir__simdi_store2( output, i ); - #endif - encode += stbir__simdfX_float_count*2; - output += stbir__simdfX_float_count*2; - if ( output <= end_output ) - continue; - if ( output == ( end_output + stbir__simdfX_float_count*2 ) ) - break; - output = end_output; // backup and do last couple - encode = end_encode_m8; - } - return; - } - - #if stbir__coder_min_num != 3 // doesn't divide cleanly by four - output += 4; - STBIR_NO_UNROLL_LOOP_START - while( output <= end_output ) - { - stbir__simdf e0; - stbir__simdi i0; - STBIR_NO_UNROLL(encode); - stbir__simdf_load( e0, encode ); - stbir__simdf_add( e0, STBIR__CONSTF(STBIR_simd_point5), e0 ); - stbir__encode_simdf4_unflip( e0 ); - stbir__simdf_pack_to_8bytes( i0, e0, e0 ); // only use first 4 - *(int*)(output-4) = stbir__simdi_to_int( i0 ); - output += 4; - encode += 4; - } - output -= 4; - #endif - - #else - - #if stbir__coder_min_num != 3 // doesn't divide cleanly by four - output += 4; - while( output <= end_output ) - { - float f; - f = encode[stbir__encode_order0] + 0.5f; STBIR_CLAMP(f, 0, 255); output[0-4] = (unsigned char)f; - f = encode[stbir__encode_order1] + 0.5f; STBIR_CLAMP(f, 0, 255); output[1-4] = (unsigned char)f; - f = encode[stbir__encode_order2] + 0.5f; STBIR_CLAMP(f, 0, 255); output[2-4] = (unsigned char)f; - f = encode[stbir__encode_order3] + 0.5f; STBIR_CLAMP(f, 0, 255); output[3-4] = (unsigned char)f; - output += 4; - encode += 4; - } - output -= 4; - #endif - - #endif - - #if stbir__coder_min_num < 4 - STBIR_NO_UNROLL_LOOP_START - while( output < end_output ) - { - float f; - STBIR_NO_UNROLL(encode); - f = encode[stbir__encode_order0] + 0.5f; STBIR_CLAMP(f, 0, 255); output[0] = (unsigned char)f; - #if stbir__coder_min_num >= 2 - f = encode[stbir__encode_order1] + 0.5f; STBIR_CLAMP(f, 0, 255); output[1] = (unsigned char)f; - #endif - #if stbir__coder_min_num >= 3 - f = encode[stbir__encode_order2] + 0.5f; STBIR_CLAMP(f, 0, 255); output[2] = (unsigned char)f; - #endif - output += stbir__coder_min_num; - encode += stbir__coder_min_num; - } - #endif -} - -static float * STBIR__CODER_NAME(stbir__decode_uint8_srgb)( float * decodep, int width_times_channels, void const * inputp ) -{ - float STBIR_STREAMOUT_PTR( * ) decode = decodep; - float * decode_end = (float*) decode + width_times_channels; - unsigned char const * input = (unsigned char const *)inputp; - - #if stbir__coder_min_num != 3 // doesn't divide cleanly by four - decode += 4; - while( decode <= decode_end ) - { - decode[0-4] = stbir__srgb_uchar_to_linear_float[ input[ stbir__decode_order0 ] ]; - decode[1-4] = stbir__srgb_uchar_to_linear_float[ input[ stbir__decode_order1 ] ]; - decode[2-4] = stbir__srgb_uchar_to_linear_float[ input[ stbir__decode_order2 ] ]; - decode[3-4] = stbir__srgb_uchar_to_linear_float[ input[ stbir__decode_order3 ] ]; - decode += 4; - input += 4; - } - decode -= 4; - #endif - - #if stbir__coder_min_num < 4 - STBIR_NO_UNROLL_LOOP_START - while( decode < decode_end ) - { - STBIR_NO_UNROLL(decode); - decode[0] = stbir__srgb_uchar_to_linear_float[ input[ stbir__decode_order0 ] ]; - #if stbir__coder_min_num >= 2 - decode[1] = stbir__srgb_uchar_to_linear_float[ input[ stbir__decode_order1 ] ]; - #endif - #if stbir__coder_min_num >= 3 - decode[2] = stbir__srgb_uchar_to_linear_float[ input[ stbir__decode_order2 ] ]; - #endif - decode += stbir__coder_min_num; - input += stbir__coder_min_num; - } - #endif - return decode_end; -} - -#define stbir__min_max_shift20( i, f ) \ - stbir__simdf_max( f, f, stbir_simdf_casti(STBIR__CONSTI( STBIR_almost_zero )) ); \ - stbir__simdf_min( f, f, stbir_simdf_casti(STBIR__CONSTI( STBIR_almost_one )) ); \ - stbir__simdi_32shr( i, stbir_simdi_castf( f ), 20 ); - -#define stbir__scale_and_convert( i, f ) \ - stbir__simdf_madd( f, STBIR__CONSTF( STBIR_simd_point5 ), STBIR__CONSTF( STBIR_max_uint8_as_float ), f ); \ - stbir__simdf_max( f, f, stbir__simdf_zeroP() ); \ - stbir__simdf_min( f, f, STBIR__CONSTF( STBIR_max_uint8_as_float ) ); \ - stbir__simdf_convert_float_to_i32( i, f ); - -#define stbir__linear_to_srgb_finish( i, f ) \ -{ \ - stbir__simdi temp; \ - stbir__simdi_32shr( temp, stbir_simdi_castf( f ), 12 ) ; \ - stbir__simdi_and( temp, temp, STBIR__CONSTI(STBIR_mastissa_mask) ); \ - stbir__simdi_or( temp, temp, STBIR__CONSTI(STBIR_topscale) ); \ - stbir__simdi_16madd( i, i, temp ); \ - stbir__simdi_32shr( i, i, 16 ); \ -} - -#define stbir__simdi_table_lookup2( v0,v1, table ) \ -{ \ - stbir__simdi_u32 temp0,temp1; \ - temp0.m128i_i128 = v0; \ - temp1.m128i_i128 = v1; \ - temp0.m128i_u32[0] = table[temp0.m128i_i32[0]]; temp0.m128i_u32[1] = table[temp0.m128i_i32[1]]; temp0.m128i_u32[2] = table[temp0.m128i_i32[2]]; temp0.m128i_u32[3] = table[temp0.m128i_i32[3]]; \ - temp1.m128i_u32[0] = table[temp1.m128i_i32[0]]; temp1.m128i_u32[1] = table[temp1.m128i_i32[1]]; temp1.m128i_u32[2] = table[temp1.m128i_i32[2]]; temp1.m128i_u32[3] = table[temp1.m128i_i32[3]]; \ - v0 = temp0.m128i_i128; \ - v1 = temp1.m128i_i128; \ -} - -#define stbir__simdi_table_lookup3( v0,v1,v2, table ) \ -{ \ - stbir__simdi_u32 temp0,temp1,temp2; \ - temp0.m128i_i128 = v0; \ - temp1.m128i_i128 = v1; \ - temp2.m128i_i128 = v2; \ - temp0.m128i_u32[0] = table[temp0.m128i_i32[0]]; temp0.m128i_u32[1] = table[temp0.m128i_i32[1]]; temp0.m128i_u32[2] = table[temp0.m128i_i32[2]]; temp0.m128i_u32[3] = table[temp0.m128i_i32[3]]; \ - temp1.m128i_u32[0] = table[temp1.m128i_i32[0]]; temp1.m128i_u32[1] = table[temp1.m128i_i32[1]]; temp1.m128i_u32[2] = table[temp1.m128i_i32[2]]; temp1.m128i_u32[3] = table[temp1.m128i_i32[3]]; \ - temp2.m128i_u32[0] = table[temp2.m128i_i32[0]]; temp2.m128i_u32[1] = table[temp2.m128i_i32[1]]; temp2.m128i_u32[2] = table[temp2.m128i_i32[2]]; temp2.m128i_u32[3] = table[temp2.m128i_i32[3]]; \ - v0 = temp0.m128i_i128; \ - v1 = temp1.m128i_i128; \ - v2 = temp2.m128i_i128; \ -} - -#define stbir__simdi_table_lookup4( v0,v1,v2,v3, table ) \ -{ \ - stbir__simdi_u32 temp0,temp1,temp2,temp3; \ - temp0.m128i_i128 = v0; \ - temp1.m128i_i128 = v1; \ - temp2.m128i_i128 = v2; \ - temp3.m128i_i128 = v3; \ - temp0.m128i_u32[0] = table[temp0.m128i_i32[0]]; temp0.m128i_u32[1] = table[temp0.m128i_i32[1]]; temp0.m128i_u32[2] = table[temp0.m128i_i32[2]]; temp0.m128i_u32[3] = table[temp0.m128i_i32[3]]; \ - temp1.m128i_u32[0] = table[temp1.m128i_i32[0]]; temp1.m128i_u32[1] = table[temp1.m128i_i32[1]]; temp1.m128i_u32[2] = table[temp1.m128i_i32[2]]; temp1.m128i_u32[3] = table[temp1.m128i_i32[3]]; \ - temp2.m128i_u32[0] = table[temp2.m128i_i32[0]]; temp2.m128i_u32[1] = table[temp2.m128i_i32[1]]; temp2.m128i_u32[2] = table[temp2.m128i_i32[2]]; temp2.m128i_u32[3] = table[temp2.m128i_i32[3]]; \ - temp3.m128i_u32[0] = table[temp3.m128i_i32[0]]; temp3.m128i_u32[1] = table[temp3.m128i_i32[1]]; temp3.m128i_u32[2] = table[temp3.m128i_i32[2]]; temp3.m128i_u32[3] = table[temp3.m128i_i32[3]]; \ - v0 = temp0.m128i_i128; \ - v1 = temp1.m128i_i128; \ - v2 = temp2.m128i_i128; \ - v3 = temp3.m128i_i128; \ -} - -static void STBIR__CODER_NAME( stbir__encode_uint8_srgb )( void * outputp, int width_times_channels, float const * encode ) -{ - unsigned char STBIR_SIMD_STREAMOUT_PTR( * ) output = (unsigned char*) outputp; - unsigned char * end_output = ( (unsigned char*) output ) + width_times_channels; - - #ifdef STBIR_SIMD - - if ( width_times_channels >= 16 ) - { - float const * end_encode_m16 = encode + width_times_channels - 16; - end_output -= 16; - STBIR_SIMD_NO_UNROLL_LOOP_START_INF_FOR - for(;;) - { - stbir__simdf f0, f1, f2, f3; - stbir__simdi i0, i1, i2, i3; - STBIR_SIMD_NO_UNROLL(encode); - - stbir__simdf_load4_transposed( f0, f1, f2, f3, encode ); - - stbir__min_max_shift20( i0, f0 ); - stbir__min_max_shift20( i1, f1 ); - stbir__min_max_shift20( i2, f2 ); - stbir__min_max_shift20( i3, f3 ); - - stbir__simdi_table_lookup4( i0, i1, i2, i3, ( fp32_to_srgb8_tab4 - (127-13)*8 ) ); - - stbir__linear_to_srgb_finish( i0, f0 ); - stbir__linear_to_srgb_finish( i1, f1 ); - stbir__linear_to_srgb_finish( i2, f2 ); - stbir__linear_to_srgb_finish( i3, f3 ); - - stbir__interleave_pack_and_store_16_u8( output, STBIR_strs_join1(i, ,stbir__encode_order0), STBIR_strs_join1(i, ,stbir__encode_order1), STBIR_strs_join1(i, ,stbir__encode_order2), STBIR_strs_join1(i, ,stbir__encode_order3) ); - - encode += 16; - output += 16; - if ( output <= end_output ) - continue; - if ( output == ( end_output + 16 ) ) - break; - output = end_output; // backup and do last couple - encode = end_encode_m16; - } - return; - } - #endif - - #if stbir__coder_min_num != 3 // doesn't divide cleanly by four - output += 4; - STBIR_SIMD_NO_UNROLL_LOOP_START - while ( output <= end_output ) - { - STBIR_SIMD_NO_UNROLL(encode); - - output[0-4] = stbir__linear_to_srgb_uchar( encode[stbir__encode_order0] ); - output[1-4] = stbir__linear_to_srgb_uchar( encode[stbir__encode_order1] ); - output[2-4] = stbir__linear_to_srgb_uchar( encode[stbir__encode_order2] ); - output[3-4] = stbir__linear_to_srgb_uchar( encode[stbir__encode_order3] ); - - output += 4; - encode += 4; - } - output -= 4; - #endif - - #if stbir__coder_min_num < 4 - STBIR_NO_UNROLL_LOOP_START - while( output < end_output ) - { - STBIR_NO_UNROLL(encode); - output[0] = stbir__linear_to_srgb_uchar( encode[stbir__encode_order0] ); - #if stbir__coder_min_num >= 2 - output[1] = stbir__linear_to_srgb_uchar( encode[stbir__encode_order1] ); - #endif - #if stbir__coder_min_num >= 3 - output[2] = stbir__linear_to_srgb_uchar( encode[stbir__encode_order2] ); - #endif - output += stbir__coder_min_num; - encode += stbir__coder_min_num; - } - #endif -} - -#if ( stbir__coder_min_num == 4 ) || ( ( stbir__coder_min_num == 1 ) && ( !defined(stbir__decode_swizzle) ) ) - -static float * STBIR__CODER_NAME(stbir__decode_uint8_srgb4_linearalpha)( float * decodep, int width_times_channels, void const * inputp ) -{ - float STBIR_STREAMOUT_PTR( * ) decode = decodep; - float * decode_end = (float*) decode + width_times_channels; - unsigned char const * input = (unsigned char const *)inputp; - - do { - decode[0] = stbir__srgb_uchar_to_linear_float[ input[stbir__decode_order0] ]; - decode[1] = stbir__srgb_uchar_to_linear_float[ input[stbir__decode_order1] ]; - decode[2] = stbir__srgb_uchar_to_linear_float[ input[stbir__decode_order2] ]; - decode[3] = ( (float) input[stbir__decode_order3] ) * stbir__max_uint8_as_float_inverted; - input += 4; - decode += 4; - } while( decode < decode_end ); - return decode_end; -} - -static void STBIR__CODER_NAME( stbir__encode_uint8_srgb4_linearalpha )( void * outputp, int width_times_channels, float const * encode ) -{ - unsigned char STBIR_SIMD_STREAMOUT_PTR( * ) output = (unsigned char*) outputp; - unsigned char * end_output = ( (unsigned char*) output ) + width_times_channels; - - #ifdef STBIR_SIMD - - if ( width_times_channels >= 16 ) - { - float const * end_encode_m16 = encode + width_times_channels - 16; - end_output -= 16; - STBIR_SIMD_NO_UNROLL_LOOP_START_INF_FOR - for(;;) - { - stbir__simdf f0, f1, f2, f3; - stbir__simdi i0, i1, i2, i3; - - STBIR_SIMD_NO_UNROLL(encode); - stbir__simdf_load4_transposed( f0, f1, f2, f3, encode ); - - stbir__min_max_shift20( i0, f0 ); - stbir__min_max_shift20( i1, f1 ); - stbir__min_max_shift20( i2, f2 ); - stbir__scale_and_convert( i3, f3 ); - - stbir__simdi_table_lookup3( i0, i1, i2, ( fp32_to_srgb8_tab4 - (127-13)*8 ) ); - - stbir__linear_to_srgb_finish( i0, f0 ); - stbir__linear_to_srgb_finish( i1, f1 ); - stbir__linear_to_srgb_finish( i2, f2 ); - - stbir__interleave_pack_and_store_16_u8( output, STBIR_strs_join1(i, ,stbir__encode_order0), STBIR_strs_join1(i, ,stbir__encode_order1), STBIR_strs_join1(i, ,stbir__encode_order2), STBIR_strs_join1(i, ,stbir__encode_order3) ); - - output += 16; - encode += 16; - - if ( output <= end_output ) - continue; - if ( output == ( end_output + 16 ) ) - break; - output = end_output; // backup and do last couple - encode = end_encode_m16; - } - return; - } - #endif - - STBIR_SIMD_NO_UNROLL_LOOP_START - do { - float f; - STBIR_SIMD_NO_UNROLL(encode); - - output[stbir__decode_order0] = stbir__linear_to_srgb_uchar( encode[0] ); - output[stbir__decode_order1] = stbir__linear_to_srgb_uchar( encode[1] ); - output[stbir__decode_order2] = stbir__linear_to_srgb_uchar( encode[2] ); - - f = encode[3] * stbir__max_uint8_as_float + 0.5f; - STBIR_CLAMP(f, 0, 255); - output[stbir__decode_order3] = (unsigned char) f; - - output += 4; - encode += 4; - } while( output < end_output ); -} - -#endif - -#if ( stbir__coder_min_num == 2 ) || ( ( stbir__coder_min_num == 1 ) && ( !defined(stbir__decode_swizzle) ) ) - -static float * STBIR__CODER_NAME(stbir__decode_uint8_srgb2_linearalpha)( float * decodep, int width_times_channels, void const * inputp ) -{ - float STBIR_STREAMOUT_PTR( * ) decode = decodep; - float * decode_end = (float*) decode + width_times_channels; - unsigned char const * input = (unsigned char const *)inputp; - - decode += 4; - while( decode <= decode_end ) - { - decode[0-4] = stbir__srgb_uchar_to_linear_float[ input[stbir__decode_order0] ]; - decode[1-4] = ( (float) input[stbir__decode_order1] ) * stbir__max_uint8_as_float_inverted; - decode[2-4] = stbir__srgb_uchar_to_linear_float[ input[stbir__decode_order0+2] ]; - decode[3-4] = ( (float) input[stbir__decode_order1+2] ) * stbir__max_uint8_as_float_inverted; - input += 4; - decode += 4; - } - decode -= 4; - if( decode < decode_end ) - { - decode[0] = stbir__srgb_uchar_to_linear_float[ stbir__decode_order0 ]; - decode[1] = ( (float) input[stbir__decode_order1] ) * stbir__max_uint8_as_float_inverted; - } - return decode_end; -} - -static void STBIR__CODER_NAME( stbir__encode_uint8_srgb2_linearalpha )( void * outputp, int width_times_channels, float const * encode ) -{ - unsigned char STBIR_SIMD_STREAMOUT_PTR( * ) output = (unsigned char*) outputp; - unsigned char * end_output = ( (unsigned char*) output ) + width_times_channels; - - #ifdef STBIR_SIMD - - if ( width_times_channels >= 16 ) - { - float const * end_encode_m16 = encode + width_times_channels - 16; - end_output -= 16; - STBIR_SIMD_NO_UNROLL_LOOP_START_INF_FOR - for(;;) - { - stbir__simdf f0, f1, f2, f3; - stbir__simdi i0, i1, i2, i3; - - STBIR_SIMD_NO_UNROLL(encode); - stbir__simdf_load4_transposed( f0, f1, f2, f3, encode ); - - stbir__min_max_shift20( i0, f0 ); - stbir__scale_and_convert( i1, f1 ); - stbir__min_max_shift20( i2, f2 ); - stbir__scale_and_convert( i3, f3 ); - - stbir__simdi_table_lookup2( i0, i2, ( fp32_to_srgb8_tab4 - (127-13)*8 ) ); - - stbir__linear_to_srgb_finish( i0, f0 ); - stbir__linear_to_srgb_finish( i2, f2 ); - - stbir__interleave_pack_and_store_16_u8( output, STBIR_strs_join1(i, ,stbir__encode_order0), STBIR_strs_join1(i, ,stbir__encode_order1), STBIR_strs_join1(i, ,stbir__encode_order2), STBIR_strs_join1(i, ,stbir__encode_order3) ); - - output += 16; - encode += 16; - if ( output <= end_output ) - continue; - if ( output == ( end_output + 16 ) ) - break; - output = end_output; // backup and do last couple - encode = end_encode_m16; - } - return; - } - #endif - - STBIR_SIMD_NO_UNROLL_LOOP_START - do { - float f; - STBIR_SIMD_NO_UNROLL(encode); - - output[stbir__decode_order0] = stbir__linear_to_srgb_uchar( encode[0] ); - - f = encode[1] * stbir__max_uint8_as_float + 0.5f; - STBIR_CLAMP(f, 0, 255); - output[stbir__decode_order1] = (unsigned char) f; - - output += 2; - encode += 2; - } while( output < end_output ); -} - -#endif - -static float * STBIR__CODER_NAME(stbir__decode_uint16_linear_scaled)( float * decodep, int width_times_channels, void const * inputp ) -{ - float STBIR_STREAMOUT_PTR( * ) decode = decodep; - float * decode_end = (float*) decode + width_times_channels; - unsigned short const * input = (unsigned short const *)inputp; - - #ifdef STBIR_SIMD - unsigned short const * end_input_m8 = input + width_times_channels - 8; - if ( width_times_channels >= 8 ) - { - decode_end -= 8; - STBIR_NO_UNROLL_LOOP_START_INF_FOR - for(;;) - { - #ifdef STBIR_SIMD8 - stbir__simdi i; stbir__simdi8 o; - stbir__simdf8 of; - STBIR_NO_UNROLL(decode); - stbir__simdi_load( i, input ); - stbir__simdi8_expand_u16_to_u32( o, i ); - stbir__simdi8_convert_i32_to_float( of, o ); - stbir__simdf8_mult( of, of, STBIR_max_uint16_as_float_inverted8); - stbir__decode_simdf8_flip( of ); - stbir__simdf8_store( decode + 0, of ); - #else - stbir__simdi i, o0, o1; - stbir__simdf of0, of1; - STBIR_NO_UNROLL(decode); - stbir__simdi_load( i, input ); - stbir__simdi_expand_u16_to_u32( o0,o1,i ); - stbir__simdi_convert_i32_to_float( of0, o0 ); - stbir__simdi_convert_i32_to_float( of1, o1 ); - stbir__simdf_mult( of0, of0, STBIR__CONSTF(STBIR_max_uint16_as_float_inverted) ); - stbir__simdf_mult( of1, of1, STBIR__CONSTF(STBIR_max_uint16_as_float_inverted)); - stbir__decode_simdf4_flip( of0 ); - stbir__decode_simdf4_flip( of1 ); - stbir__simdf_store( decode + 0, of0 ); - stbir__simdf_store( decode + 4, of1 ); - #endif - decode += 8; - input += 8; - if ( decode <= decode_end ) - continue; - if ( decode == ( decode_end + 8 ) ) - break; - decode = decode_end; // backup and do last couple - input = end_input_m8; - } - return decode_end + 8; - } - #endif - - #if stbir__coder_min_num != 3 // doesn't divide cleanly by four - decode += 4; - STBIR_SIMD_NO_UNROLL_LOOP_START - while( decode <= decode_end ) - { - STBIR_SIMD_NO_UNROLL(decode); - decode[0-4] = ((float)(input[stbir__decode_order0])) * stbir__max_uint16_as_float_inverted; - decode[1-4] = ((float)(input[stbir__decode_order1])) * stbir__max_uint16_as_float_inverted; - decode[2-4] = ((float)(input[stbir__decode_order2])) * stbir__max_uint16_as_float_inverted; - decode[3-4] = ((float)(input[stbir__decode_order3])) * stbir__max_uint16_as_float_inverted; - decode += 4; - input += 4; - } - decode -= 4; - #endif - - #if stbir__coder_min_num < 4 - STBIR_NO_UNROLL_LOOP_START - while( decode < decode_end ) - { - STBIR_NO_UNROLL(decode); - decode[0] = ((float)(input[stbir__decode_order0])) * stbir__max_uint16_as_float_inverted; - #if stbir__coder_min_num >= 2 - decode[1] = ((float)(input[stbir__decode_order1])) * stbir__max_uint16_as_float_inverted; - #endif - #if stbir__coder_min_num >= 3 - decode[2] = ((float)(input[stbir__decode_order2])) * stbir__max_uint16_as_float_inverted; - #endif - decode += stbir__coder_min_num; - input += stbir__coder_min_num; - } - #endif - return decode_end; -} - -static void STBIR__CODER_NAME(stbir__encode_uint16_linear_scaled)( void * outputp, int width_times_channels, float const * encode ) -{ - unsigned short STBIR_SIMD_STREAMOUT_PTR( * ) output = (unsigned short*) outputp; - unsigned short * end_output = ( (unsigned short*) output ) + width_times_channels; - - #ifdef STBIR_SIMD - { - if ( width_times_channels >= stbir__simdfX_float_count*2 ) - { - float const * end_encode_m8 = encode + width_times_channels - stbir__simdfX_float_count*2; - end_output -= stbir__simdfX_float_count*2; - STBIR_SIMD_NO_UNROLL_LOOP_START_INF_FOR - for(;;) - { - stbir__simdfX e0, e1; - stbir__simdiX i; - STBIR_SIMD_NO_UNROLL(encode); - stbir__simdfX_madd_mem( e0, STBIR_simd_point5X, STBIR_max_uint16_as_floatX, encode ); - stbir__simdfX_madd_mem( e1, STBIR_simd_point5X, STBIR_max_uint16_as_floatX, encode+stbir__simdfX_float_count ); - stbir__encode_simdfX_unflip( e0 ); - stbir__encode_simdfX_unflip( e1 ); - stbir__simdfX_pack_to_words( i, e0, e1 ); - stbir__simdiX_store( output, i ); - encode += stbir__simdfX_float_count*2; - output += stbir__simdfX_float_count*2; - if ( output <= end_output ) - continue; - if ( output == ( end_output + stbir__simdfX_float_count*2 ) ) - break; - output = end_output; // backup and do last couple - encode = end_encode_m8; - } - return; - } - } - - #if stbir__coder_min_num != 3 // doesn't divide cleanly by four - output += 4; - STBIR_NO_UNROLL_LOOP_START - while( output <= end_output ) - { - stbir__simdf e; - stbir__simdi i; - STBIR_NO_UNROLL(encode); - stbir__simdf_load( e, encode ); - stbir__simdf_madd( e, STBIR__CONSTF(STBIR_simd_point5), STBIR__CONSTF(STBIR_max_uint16_as_float), e ); - stbir__encode_simdf4_unflip( e ); - stbir__simdf_pack_to_8words( i, e, e ); // only use first 4 - stbir__simdi_store2( output-4, i ); - output += 4; - encode += 4; - } - output -= 4; - #endif - - #if stbir__coder_min_num < 4 - STBIR_NO_UNROLL_LOOP_START - while( output < end_output ) - { - stbir__simdf e; - STBIR_NO_UNROLL(encode); - stbir__simdf_madd1_mem( e, STBIR__CONSTF(STBIR_simd_point5), STBIR__CONSTF(STBIR_max_uint16_as_float), encode+stbir__encode_order0 ); output[0] = stbir__simdf_convert_float_to_short( e ); - #if stbir__coder_min_num >= 2 - stbir__simdf_madd1_mem( e, STBIR__CONSTF(STBIR_simd_point5), STBIR__CONSTF(STBIR_max_uint16_as_float), encode+stbir__encode_order1 ); output[1] = stbir__simdf_convert_float_to_short( e ); - #endif - #if stbir__coder_min_num >= 3 - stbir__simdf_madd1_mem( e, STBIR__CONSTF(STBIR_simd_point5), STBIR__CONSTF(STBIR_max_uint16_as_float), encode+stbir__encode_order2 ); output[2] = stbir__simdf_convert_float_to_short( e ); - #endif - output += stbir__coder_min_num; - encode += stbir__coder_min_num; - } - #endif - - #else - - #if stbir__coder_min_num != 3 // doesn't divide cleanly by four - output += 4; - STBIR_SIMD_NO_UNROLL_LOOP_START - while( output <= end_output ) - { - float f; - STBIR_SIMD_NO_UNROLL(encode); - f = encode[stbir__encode_order0] * stbir__max_uint16_as_float + 0.5f; STBIR_CLAMP(f, 0, 65535); output[0-4] = (unsigned short)f; - f = encode[stbir__encode_order1] * stbir__max_uint16_as_float + 0.5f; STBIR_CLAMP(f, 0, 65535); output[1-4] = (unsigned short)f; - f = encode[stbir__encode_order2] * stbir__max_uint16_as_float + 0.5f; STBIR_CLAMP(f, 0, 65535); output[2-4] = (unsigned short)f; - f = encode[stbir__encode_order3] * stbir__max_uint16_as_float + 0.5f; STBIR_CLAMP(f, 0, 65535); output[3-4] = (unsigned short)f; - output += 4; - encode += 4; - } - output -= 4; - #endif - - #if stbir__coder_min_num < 4 - STBIR_NO_UNROLL_LOOP_START - while( output < end_output ) - { - float f; - STBIR_NO_UNROLL(encode); - f = encode[stbir__encode_order0] * stbir__max_uint16_as_float + 0.5f; STBIR_CLAMP(f, 0, 65535); output[0] = (unsigned short)f; - #if stbir__coder_min_num >= 2 - f = encode[stbir__encode_order1] * stbir__max_uint16_as_float + 0.5f; STBIR_CLAMP(f, 0, 65535); output[1] = (unsigned short)f; - #endif - #if stbir__coder_min_num >= 3 - f = encode[stbir__encode_order2] * stbir__max_uint16_as_float + 0.5f; STBIR_CLAMP(f, 0, 65535); output[2] = (unsigned short)f; - #endif - output += stbir__coder_min_num; - encode += stbir__coder_min_num; - } - #endif - #endif -} - -static float * STBIR__CODER_NAME(stbir__decode_uint16_linear)( float * decodep, int width_times_channels, void const * inputp ) -{ - float STBIR_STREAMOUT_PTR( * ) decode = decodep; - float * decode_end = (float*) decode + width_times_channels; - unsigned short const * input = (unsigned short const *)inputp; - - #ifdef STBIR_SIMD - unsigned short const * end_input_m8 = input + width_times_channels - 8; - if ( width_times_channels >= 8 ) - { - decode_end -= 8; - STBIR_NO_UNROLL_LOOP_START_INF_FOR - for(;;) - { - #ifdef STBIR_SIMD8 - stbir__simdi i; stbir__simdi8 o; - stbir__simdf8 of; - STBIR_NO_UNROLL(decode); - stbir__simdi_load( i, input ); - stbir__simdi8_expand_u16_to_u32( o, i ); - stbir__simdi8_convert_i32_to_float( of, o ); - stbir__decode_simdf8_flip( of ); - stbir__simdf8_store( decode + 0, of ); - #else - stbir__simdi i, o0, o1; - stbir__simdf of0, of1; - STBIR_NO_UNROLL(decode); - stbir__simdi_load( i, input ); - stbir__simdi_expand_u16_to_u32( o0, o1, i ); - stbir__simdi_convert_i32_to_float( of0, o0 ); - stbir__simdi_convert_i32_to_float( of1, o1 ); - stbir__decode_simdf4_flip( of0 ); - stbir__decode_simdf4_flip( of1 ); - stbir__simdf_store( decode + 0, of0 ); - stbir__simdf_store( decode + 4, of1 ); - #endif - decode += 8; - input += 8; - if ( decode <= decode_end ) - continue; - if ( decode == ( decode_end + 8 ) ) - break; - decode = decode_end; // backup and do last couple - input = end_input_m8; - } - return decode_end + 8; - } - #endif - - #if stbir__coder_min_num != 3 // doesn't divide cleanly by four - decode += 4; - STBIR_SIMD_NO_UNROLL_LOOP_START - while( decode <= decode_end ) - { - STBIR_SIMD_NO_UNROLL(decode); - decode[0-4] = ((float)(input[stbir__decode_order0])); - decode[1-4] = ((float)(input[stbir__decode_order1])); - decode[2-4] = ((float)(input[stbir__decode_order2])); - decode[3-4] = ((float)(input[stbir__decode_order3])); - decode += 4; - input += 4; - } - decode -= 4; - #endif - - #if stbir__coder_min_num < 4 - STBIR_NO_UNROLL_LOOP_START - while( decode < decode_end ) - { - STBIR_NO_UNROLL(decode); - decode[0] = ((float)(input[stbir__decode_order0])); - #if stbir__coder_min_num >= 2 - decode[1] = ((float)(input[stbir__decode_order1])); - #endif - #if stbir__coder_min_num >= 3 - decode[2] = ((float)(input[stbir__decode_order2])); - #endif - decode += stbir__coder_min_num; - input += stbir__coder_min_num; - } - #endif - return decode_end; -} - -static void STBIR__CODER_NAME(stbir__encode_uint16_linear)( void * outputp, int width_times_channels, float const * encode ) -{ - unsigned short STBIR_SIMD_STREAMOUT_PTR( * ) output = (unsigned short*) outputp; - unsigned short * end_output = ( (unsigned short*) output ) + width_times_channels; - - #ifdef STBIR_SIMD - { - if ( width_times_channels >= stbir__simdfX_float_count*2 ) - { - float const * end_encode_m8 = encode + width_times_channels - stbir__simdfX_float_count*2; - end_output -= stbir__simdfX_float_count*2; - STBIR_SIMD_NO_UNROLL_LOOP_START_INF_FOR - for(;;) - { - stbir__simdfX e0, e1; - stbir__simdiX i; - STBIR_SIMD_NO_UNROLL(encode); - stbir__simdfX_add_mem( e0, STBIR_simd_point5X, encode ); - stbir__simdfX_add_mem( e1, STBIR_simd_point5X, encode+stbir__simdfX_float_count ); - stbir__encode_simdfX_unflip( e0 ); - stbir__encode_simdfX_unflip( e1 ); - stbir__simdfX_pack_to_words( i, e0, e1 ); - stbir__simdiX_store( output, i ); - encode += stbir__simdfX_float_count*2; - output += stbir__simdfX_float_count*2; - if ( output <= end_output ) - continue; - if ( output == ( end_output + stbir__simdfX_float_count*2 ) ) - break; - output = end_output; // backup and do last couple - encode = end_encode_m8; - } - return; - } - } - - #if stbir__coder_min_num != 3 // doesn't divide cleanly by four - output += 4; - STBIR_NO_UNROLL_LOOP_START - while( output <= end_output ) - { - stbir__simdf e; - stbir__simdi i; - STBIR_NO_UNROLL(encode); - stbir__simdf_load( e, encode ); - stbir__simdf_add( e, STBIR__CONSTF(STBIR_simd_point5), e ); - stbir__encode_simdf4_unflip( e ); - stbir__simdf_pack_to_8words( i, e, e ); // only use first 4 - stbir__simdi_store2( output-4, i ); - output += 4; - encode += 4; - } - output -= 4; - #endif - - #else - - #if stbir__coder_min_num != 3 // doesn't divide cleanly by four - output += 4; - STBIR_SIMD_NO_UNROLL_LOOP_START - while( output <= end_output ) - { - float f; - STBIR_SIMD_NO_UNROLL(encode); - f = encode[stbir__encode_order0] + 0.5f; STBIR_CLAMP(f, 0, 65535); output[0-4] = (unsigned short)f; - f = encode[stbir__encode_order1] + 0.5f; STBIR_CLAMP(f, 0, 65535); output[1-4] = (unsigned short)f; - f = encode[stbir__encode_order2] + 0.5f; STBIR_CLAMP(f, 0, 65535); output[2-4] = (unsigned short)f; - f = encode[stbir__encode_order3] + 0.5f; STBIR_CLAMP(f, 0, 65535); output[3-4] = (unsigned short)f; - output += 4; - encode += 4; - } - output -= 4; - #endif - - #endif - - #if stbir__coder_min_num < 4 - STBIR_NO_UNROLL_LOOP_START - while( output < end_output ) - { - float f; - STBIR_NO_UNROLL(encode); - f = encode[stbir__encode_order0] + 0.5f; STBIR_CLAMP(f, 0, 65535); output[0] = (unsigned short)f; - #if stbir__coder_min_num >= 2 - f = encode[stbir__encode_order1] + 0.5f; STBIR_CLAMP(f, 0, 65535); output[1] = (unsigned short)f; - #endif - #if stbir__coder_min_num >= 3 - f = encode[stbir__encode_order2] + 0.5f; STBIR_CLAMP(f, 0, 65535); output[2] = (unsigned short)f; - #endif - output += stbir__coder_min_num; - encode += stbir__coder_min_num; - } - #endif -} - -static float * STBIR__CODER_NAME(stbir__decode_half_float_linear)( float * decodep, int width_times_channels, void const * inputp ) -{ - float STBIR_STREAMOUT_PTR( * ) decode = decodep; - float * decode_end = (float*) decode + width_times_channels; - stbir__FP16 const * input = (stbir__FP16 const *)inputp; - - #ifdef STBIR_SIMD - if ( width_times_channels >= 8 ) - { - stbir__FP16 const * end_input_m8 = input + width_times_channels - 8; - decode_end -= 8; - STBIR_NO_UNROLL_LOOP_START_INF_FOR - for(;;) - { - STBIR_NO_UNROLL(decode); - - stbir__half_to_float_SIMD( decode, input ); - #ifdef stbir__decode_swizzle - #ifdef STBIR_SIMD8 - { - stbir__simdf8 of; - stbir__simdf8_load( of, decode ); - stbir__decode_simdf8_flip( of ); - stbir__simdf8_store( decode, of ); - } - #else - { - stbir__simdf of0,of1; - stbir__simdf_load( of0, decode ); - stbir__simdf_load( of1, decode+4 ); - stbir__decode_simdf4_flip( of0 ); - stbir__decode_simdf4_flip( of1 ); - stbir__simdf_store( decode, of0 ); - stbir__simdf_store( decode+4, of1 ); - } - #endif - #endif - decode += 8; - input += 8; - if ( decode <= decode_end ) - continue; - if ( decode == ( decode_end + 8 ) ) - break; - decode = decode_end; // backup and do last couple - input = end_input_m8; - } - return decode_end + 8; - } - #endif - - #if stbir__coder_min_num != 3 // doesn't divide cleanly by four - decode += 4; - STBIR_SIMD_NO_UNROLL_LOOP_START - while( decode <= decode_end ) - { - STBIR_SIMD_NO_UNROLL(decode); - decode[0-4] = stbir__half_to_float(input[stbir__decode_order0]); - decode[1-4] = stbir__half_to_float(input[stbir__decode_order1]); - decode[2-4] = stbir__half_to_float(input[stbir__decode_order2]); - decode[3-4] = stbir__half_to_float(input[stbir__decode_order3]); - decode += 4; - input += 4; - } - decode -= 4; - #endif - - #if stbir__coder_min_num < 4 - STBIR_NO_UNROLL_LOOP_START - while( decode < decode_end ) - { - STBIR_NO_UNROLL(decode); - decode[0] = stbir__half_to_float(input[stbir__decode_order0]); - #if stbir__coder_min_num >= 2 - decode[1] = stbir__half_to_float(input[stbir__decode_order1]); - #endif - #if stbir__coder_min_num >= 3 - decode[2] = stbir__half_to_float(input[stbir__decode_order2]); - #endif - decode += stbir__coder_min_num; - input += stbir__coder_min_num; - } - #endif - return decode_end; -} - -static void STBIR__CODER_NAME( stbir__encode_half_float_linear )( void * outputp, int width_times_channels, float const * encode ) -{ - stbir__FP16 STBIR_SIMD_STREAMOUT_PTR( * ) output = (stbir__FP16*) outputp; - stbir__FP16 * end_output = ( (stbir__FP16*) output ) + width_times_channels; - - #ifdef STBIR_SIMD - if ( width_times_channels >= 8 ) - { - float const * end_encode_m8 = encode + width_times_channels - 8; - end_output -= 8; - STBIR_SIMD_NO_UNROLL_LOOP_START_INF_FOR - for(;;) - { - STBIR_SIMD_NO_UNROLL(encode); - #ifdef stbir__decode_swizzle - #ifdef STBIR_SIMD8 - { - stbir__simdf8 of; - stbir__simdf8_load( of, encode ); - stbir__encode_simdf8_unflip( of ); - stbir__float_to_half_SIMD( output, (float*)&of ); - } - #else - { - stbir__simdf of[2]; - stbir__simdf_load( of[0], encode ); - stbir__simdf_load( of[1], encode+4 ); - stbir__encode_simdf4_unflip( of[0] ); - stbir__encode_simdf4_unflip( of[1] ); - stbir__float_to_half_SIMD( output, (float*)of ); - } - #endif - #else - stbir__float_to_half_SIMD( output, encode ); - #endif - encode += 8; - output += 8; - if ( output <= end_output ) - continue; - if ( output == ( end_output + 8 ) ) - break; - output = end_output; // backup and do last couple - encode = end_encode_m8; - } - return; - } - #endif - - #if stbir__coder_min_num != 3 // doesn't divide cleanly by four - output += 4; - STBIR_SIMD_NO_UNROLL_LOOP_START - while( output <= end_output ) - { - STBIR_SIMD_NO_UNROLL(output); - output[0-4] = stbir__float_to_half(encode[stbir__encode_order0]); - output[1-4] = stbir__float_to_half(encode[stbir__encode_order1]); - output[2-4] = stbir__float_to_half(encode[stbir__encode_order2]); - output[3-4] = stbir__float_to_half(encode[stbir__encode_order3]); - output += 4; - encode += 4; - } - output -= 4; - #endif - - #if stbir__coder_min_num < 4 - STBIR_NO_UNROLL_LOOP_START - while( output < end_output ) - { - STBIR_NO_UNROLL(output); - output[0] = stbir__float_to_half(encode[stbir__encode_order0]); - #if stbir__coder_min_num >= 2 - output[1] = stbir__float_to_half(encode[stbir__encode_order1]); - #endif - #if stbir__coder_min_num >= 3 - output[2] = stbir__float_to_half(encode[stbir__encode_order2]); - #endif - output += stbir__coder_min_num; - encode += stbir__coder_min_num; - } - #endif -} - -static float * STBIR__CODER_NAME(stbir__decode_float_linear)( float * decodep, int width_times_channels, void const * inputp ) -{ - #ifdef stbir__decode_swizzle - float STBIR_STREAMOUT_PTR( * ) decode = decodep; - float * decode_end = (float*) decode + width_times_channels; - float const * input = (float const *)inputp; - - #ifdef STBIR_SIMD - if ( width_times_channels >= 16 ) - { - float const * end_input_m16 = input + width_times_channels - 16; - decode_end -= 16; - STBIR_NO_UNROLL_LOOP_START_INF_FOR - for(;;) - { - STBIR_NO_UNROLL(decode); - #ifdef stbir__decode_swizzle - #ifdef STBIR_SIMD8 - { - stbir__simdf8 of0,of1; - stbir__simdf8_load( of0, input ); - stbir__simdf8_load( of1, input+8 ); - stbir__decode_simdf8_flip( of0 ); - stbir__decode_simdf8_flip( of1 ); - stbir__simdf8_store( decode, of0 ); - stbir__simdf8_store( decode+8, of1 ); - } - #else - { - stbir__simdf of0,of1,of2,of3; - stbir__simdf_load( of0, input ); - stbir__simdf_load( of1, input+4 ); - stbir__simdf_load( of2, input+8 ); - stbir__simdf_load( of3, input+12 ); - stbir__decode_simdf4_flip( of0 ); - stbir__decode_simdf4_flip( of1 ); - stbir__decode_simdf4_flip( of2 ); - stbir__decode_simdf4_flip( of3 ); - stbir__simdf_store( decode, of0 ); - stbir__simdf_store( decode+4, of1 ); - stbir__simdf_store( decode+8, of2 ); - stbir__simdf_store( decode+12, of3 ); - } - #endif - #endif - decode += 16; - input += 16; - if ( decode <= decode_end ) - continue; - if ( decode == ( decode_end + 16 ) ) - break; - decode = decode_end; // backup and do last couple - input = end_input_m16; - } - return decode_end + 16; - } - #endif - - #if stbir__coder_min_num != 3 // doesn't divide cleanly by four - decode += 4; - STBIR_SIMD_NO_UNROLL_LOOP_START - while( decode <= decode_end ) - { - STBIR_SIMD_NO_UNROLL(decode); - decode[0-4] = input[stbir__decode_order0]; - decode[1-4] = input[stbir__decode_order1]; - decode[2-4] = input[stbir__decode_order2]; - decode[3-4] = input[stbir__decode_order3]; - decode += 4; - input += 4; - } - decode -= 4; - #endif - - #if stbir__coder_min_num < 4 - STBIR_NO_UNROLL_LOOP_START - while( decode < decode_end ) - { - STBIR_NO_UNROLL(decode); - decode[0] = input[stbir__decode_order0]; - #if stbir__coder_min_num >= 2 - decode[1] = input[stbir__decode_order1]; - #endif - #if stbir__coder_min_num >= 3 - decode[2] = input[stbir__decode_order2]; - #endif - decode += stbir__coder_min_num; - input += stbir__coder_min_num; - } - #endif - return decode_end; - - #else - - if ( (void*)decodep != inputp ) - STBIR_MEMCPY( decodep, inputp, width_times_channels * sizeof( float ) ); - - return decodep + width_times_channels; - - #endif -} - -static void STBIR__CODER_NAME( stbir__encode_float_linear )( void * outputp, int width_times_channels, float const * encode ) -{ - #if !defined( STBIR_FLOAT_HIGH_CLAMP ) && !defined(STBIR_FLOAT_LO_CLAMP) && !defined(stbir__decode_swizzle) - - if ( (void*)outputp != (void*) encode ) - STBIR_MEMCPY( outputp, encode, width_times_channels * sizeof( float ) ); - - #else - - float STBIR_SIMD_STREAMOUT_PTR( * ) output = (float*) outputp; - float * end_output = ( (float*) output ) + width_times_channels; - - #ifdef STBIR_FLOAT_HIGH_CLAMP - #define stbir_scalar_hi_clamp( v ) if ( v > STBIR_FLOAT_HIGH_CLAMP ) v = STBIR_FLOAT_HIGH_CLAMP; - #else - #define stbir_scalar_hi_clamp( v ) - #endif - #ifdef STBIR_FLOAT_LOW_CLAMP - #define stbir_scalar_lo_clamp( v ) if ( v < STBIR_FLOAT_LOW_CLAMP ) v = STBIR_FLOAT_LOW_CLAMP; - #else - #define stbir_scalar_lo_clamp( v ) - #endif - - #ifdef STBIR_SIMD - - #ifdef STBIR_FLOAT_HIGH_CLAMP - const stbir__simdfX high_clamp = stbir__simdf_frepX(STBIR_FLOAT_HIGH_CLAMP); - #endif - #ifdef STBIR_FLOAT_LOW_CLAMP - const stbir__simdfX low_clamp = stbir__simdf_frepX(STBIR_FLOAT_LOW_CLAMP); - #endif - - if ( width_times_channels >= ( stbir__simdfX_float_count * 2 ) ) - { - float const * end_encode_m8 = encode + width_times_channels - ( stbir__simdfX_float_count * 2 ); - end_output -= ( stbir__simdfX_float_count * 2 ); - STBIR_SIMD_NO_UNROLL_LOOP_START_INF_FOR - for(;;) - { - stbir__simdfX e0, e1; - STBIR_SIMD_NO_UNROLL(encode); - stbir__simdfX_load( e0, encode ); - stbir__simdfX_load( e1, encode+stbir__simdfX_float_count ); -#ifdef STBIR_FLOAT_HIGH_CLAMP - stbir__simdfX_min( e0, e0, high_clamp ); - stbir__simdfX_min( e1, e1, high_clamp ); -#endif -#ifdef STBIR_FLOAT_LOW_CLAMP - stbir__simdfX_max( e0, e0, low_clamp ); - stbir__simdfX_max( e1, e1, low_clamp ); -#endif - stbir__encode_simdfX_unflip( e0 ); - stbir__encode_simdfX_unflip( e1 ); - stbir__simdfX_store( output, e0 ); - stbir__simdfX_store( output+stbir__simdfX_float_count, e1 ); - encode += stbir__simdfX_float_count * 2; - output += stbir__simdfX_float_count * 2; - if ( output < end_output ) - continue; - if ( output == ( end_output + ( stbir__simdfX_float_count * 2 ) ) ) - break; - output = end_output; // backup and do last couple - encode = end_encode_m8; - } - return; - } - - #if stbir__coder_min_num != 3 // doesn't divide cleanly by four - output += 4; - STBIR_NO_UNROLL_LOOP_START - while( output <= end_output ) - { - stbir__simdf e0; - STBIR_NO_UNROLL(encode); - stbir__simdf_load( e0, encode ); -#ifdef STBIR_FLOAT_HIGH_CLAMP - stbir__simdf_min( e0, e0, high_clamp ); -#endif -#ifdef STBIR_FLOAT_LOW_CLAMP - stbir__simdf_max( e0, e0, low_clamp ); -#endif - stbir__encode_simdf4_unflip( e0 ); - stbir__simdf_store( output-4, e0 ); - output += 4; - encode += 4; - } - output -= 4; - #endif - - #else - - #if stbir__coder_min_num != 3 // doesn't divide cleanly by four - output += 4; - STBIR_SIMD_NO_UNROLL_LOOP_START - while( output <= end_output ) - { - float e; - STBIR_SIMD_NO_UNROLL(encode); - e = encode[ stbir__encode_order0 ]; stbir_scalar_hi_clamp( e ); stbir_scalar_lo_clamp( e ); output[0-4] = e; - e = encode[ stbir__encode_order1 ]; stbir_scalar_hi_clamp( e ); stbir_scalar_lo_clamp( e ); output[1-4] = e; - e = encode[ stbir__encode_order2 ]; stbir_scalar_hi_clamp( e ); stbir_scalar_lo_clamp( e ); output[2-4] = e; - e = encode[ stbir__encode_order3 ]; stbir_scalar_hi_clamp( e ); stbir_scalar_lo_clamp( e ); output[3-4] = e; - output += 4; - encode += 4; - } - output -= 4; - - #endif - - #endif - - #if stbir__coder_min_num < 4 - STBIR_NO_UNROLL_LOOP_START - while( output < end_output ) - { - float e; - STBIR_NO_UNROLL(encode); - e = encode[ stbir__encode_order0 ]; stbir_scalar_hi_clamp( e ); stbir_scalar_lo_clamp( e ); output[0] = e; - #if stbir__coder_min_num >= 2 - e = encode[ stbir__encode_order1 ]; stbir_scalar_hi_clamp( e ); stbir_scalar_lo_clamp( e ); output[1] = e; - #endif - #if stbir__coder_min_num >= 3 - e = encode[ stbir__encode_order2 ]; stbir_scalar_hi_clamp( e ); stbir_scalar_lo_clamp( e ); output[2] = e; - #endif - output += stbir__coder_min_num; - encode += stbir__coder_min_num; - } - #endif - - #endif -} - -#undef stbir__decode_suffix -#undef stbir__decode_simdf8_flip -#undef stbir__decode_simdf4_flip -#undef stbir__decode_order0 -#undef stbir__decode_order1 -#undef stbir__decode_order2 -#undef stbir__decode_order3 -#undef stbir__encode_order0 -#undef stbir__encode_order1 -#undef stbir__encode_order2 -#undef stbir__encode_order3 -#undef stbir__encode_simdf8_unflip -#undef stbir__encode_simdf4_unflip -#undef stbir__encode_simdfX_unflip -#undef STBIR__CODER_NAME -#undef stbir__coder_min_num -#undef stbir__decode_swizzle -#undef stbir_scalar_hi_clamp -#undef stbir_scalar_lo_clamp -#undef STB_IMAGE_RESIZE_DO_CODERS - -#elif defined( STB_IMAGE_RESIZE_DO_VERTICALS) - -#ifdef STB_IMAGE_RESIZE_VERTICAL_CONTINUE -#define STBIR_chans( start, end ) STBIR_strs_join14(start,STBIR__vertical_channels,end,_cont) -#else -#define STBIR_chans( start, end ) STBIR_strs_join1(start,STBIR__vertical_channels,end) -#endif - -#if STBIR__vertical_channels >= 1 -#define stbIF0( code ) code -#else -#define stbIF0( code ) -#endif -#if STBIR__vertical_channels >= 2 -#define stbIF1( code ) code -#else -#define stbIF1( code ) -#endif -#if STBIR__vertical_channels >= 3 -#define stbIF2( code ) code -#else -#define stbIF2( code ) -#endif -#if STBIR__vertical_channels >= 4 -#define stbIF3( code ) code -#else -#define stbIF3( code ) -#endif -#if STBIR__vertical_channels >= 5 -#define stbIF4( code ) code -#else -#define stbIF4( code ) -#endif -#if STBIR__vertical_channels >= 6 -#define stbIF5( code ) code -#else -#define stbIF5( code ) -#endif -#if STBIR__vertical_channels >= 7 -#define stbIF6( code ) code -#else -#define stbIF6( code ) -#endif -#if STBIR__vertical_channels >= 8 -#define stbIF7( code ) code -#else -#define stbIF7( code ) -#endif - -static void STBIR_chans( stbir__vertical_scatter_with_,_coeffs)( float ** outputs, float const * vertical_coefficients, float const * input, float const * input_end ) -{ - stbIF0( float STBIR_SIMD_STREAMOUT_PTR( * ) output0 = outputs[0]; float c0s = vertical_coefficients[0]; ) - stbIF1( float STBIR_SIMD_STREAMOUT_PTR( * ) output1 = outputs[1]; float c1s = vertical_coefficients[1]; ) - stbIF2( float STBIR_SIMD_STREAMOUT_PTR( * ) output2 = outputs[2]; float c2s = vertical_coefficients[2]; ) - stbIF3( float STBIR_SIMD_STREAMOUT_PTR( * ) output3 = outputs[3]; float c3s = vertical_coefficients[3]; ) - stbIF4( float STBIR_SIMD_STREAMOUT_PTR( * ) output4 = outputs[4]; float c4s = vertical_coefficients[4]; ) - stbIF5( float STBIR_SIMD_STREAMOUT_PTR( * ) output5 = outputs[5]; float c5s = vertical_coefficients[5]; ) - stbIF6( float STBIR_SIMD_STREAMOUT_PTR( * ) output6 = outputs[6]; float c6s = vertical_coefficients[6]; ) - stbIF7( float STBIR_SIMD_STREAMOUT_PTR( * ) output7 = outputs[7]; float c7s = vertical_coefficients[7]; ) - - #ifdef STBIR_SIMD - { - stbIF0(stbir__simdfX c0 = stbir__simdf_frepX( c0s ); ) - stbIF1(stbir__simdfX c1 = stbir__simdf_frepX( c1s ); ) - stbIF2(stbir__simdfX c2 = stbir__simdf_frepX( c2s ); ) - stbIF3(stbir__simdfX c3 = stbir__simdf_frepX( c3s ); ) - stbIF4(stbir__simdfX c4 = stbir__simdf_frepX( c4s ); ) - stbIF5(stbir__simdfX c5 = stbir__simdf_frepX( c5s ); ) - stbIF6(stbir__simdfX c6 = stbir__simdf_frepX( c6s ); ) - stbIF7(stbir__simdfX c7 = stbir__simdf_frepX( c7s ); ) - STBIR_SIMD_NO_UNROLL_LOOP_START - while ( ( (char*)input_end - (char*) input ) >= (16*stbir__simdfX_float_count) ) - { - stbir__simdfX o0, o1, o2, o3, r0, r1, r2, r3; - STBIR_SIMD_NO_UNROLL(output0); - - stbir__simdfX_load( r0, input ); stbir__simdfX_load( r1, input+stbir__simdfX_float_count ); stbir__simdfX_load( r2, input+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( r3, input+(3*stbir__simdfX_float_count) ); - - #ifdef STB_IMAGE_RESIZE_VERTICAL_CONTINUE - stbIF0( stbir__simdfX_load( o0, output0 ); stbir__simdfX_load( o1, output0+stbir__simdfX_float_count ); stbir__simdfX_load( o2, output0+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( o3, output0+(3*stbir__simdfX_float_count) ); - stbir__simdfX_madd( o0, o0, r0, c0 ); stbir__simdfX_madd( o1, o1, r1, c0 ); stbir__simdfX_madd( o2, o2, r2, c0 ); stbir__simdfX_madd( o3, o3, r3, c0 ); - stbir__simdfX_store( output0, o0 ); stbir__simdfX_store( output0+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output0+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output0+(3*stbir__simdfX_float_count), o3 ); ) - stbIF1( stbir__simdfX_load( o0, output1 ); stbir__simdfX_load( o1, output1+stbir__simdfX_float_count ); stbir__simdfX_load( o2, output1+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( o3, output1+(3*stbir__simdfX_float_count) ); - stbir__simdfX_madd( o0, o0, r0, c1 ); stbir__simdfX_madd( o1, o1, r1, c1 ); stbir__simdfX_madd( o2, o2, r2, c1 ); stbir__simdfX_madd( o3, o3, r3, c1 ); - stbir__simdfX_store( output1, o0 ); stbir__simdfX_store( output1+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output1+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output1+(3*stbir__simdfX_float_count), o3 ); ) - stbIF2( stbir__simdfX_load( o0, output2 ); stbir__simdfX_load( o1, output2+stbir__simdfX_float_count ); stbir__simdfX_load( o2, output2+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( o3, output2+(3*stbir__simdfX_float_count) ); - stbir__simdfX_madd( o0, o0, r0, c2 ); stbir__simdfX_madd( o1, o1, r1, c2 ); stbir__simdfX_madd( o2, o2, r2, c2 ); stbir__simdfX_madd( o3, o3, r3, c2 ); - stbir__simdfX_store( output2, o0 ); stbir__simdfX_store( output2+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output2+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output2+(3*stbir__simdfX_float_count), o3 ); ) - stbIF3( stbir__simdfX_load( o0, output3 ); stbir__simdfX_load( o1, output3+stbir__simdfX_float_count ); stbir__simdfX_load( o2, output3+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( o3, output3+(3*stbir__simdfX_float_count) ); - stbir__simdfX_madd( o0, o0, r0, c3 ); stbir__simdfX_madd( o1, o1, r1, c3 ); stbir__simdfX_madd( o2, o2, r2, c3 ); stbir__simdfX_madd( o3, o3, r3, c3 ); - stbir__simdfX_store( output3, o0 ); stbir__simdfX_store( output3+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output3+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output3+(3*stbir__simdfX_float_count), o3 ); ) - stbIF4( stbir__simdfX_load( o0, output4 ); stbir__simdfX_load( o1, output4+stbir__simdfX_float_count ); stbir__simdfX_load( o2, output4+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( o3, output4+(3*stbir__simdfX_float_count) ); - stbir__simdfX_madd( o0, o0, r0, c4 ); stbir__simdfX_madd( o1, o1, r1, c4 ); stbir__simdfX_madd( o2, o2, r2, c4 ); stbir__simdfX_madd( o3, o3, r3, c4 ); - stbir__simdfX_store( output4, o0 ); stbir__simdfX_store( output4+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output4+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output4+(3*stbir__simdfX_float_count), o3 ); ) - stbIF5( stbir__simdfX_load( o0, output5 ); stbir__simdfX_load( o1, output5+stbir__simdfX_float_count ); stbir__simdfX_load( o2, output5+(2*stbir__simdfX_float_count)); stbir__simdfX_load( o3, output5+(3*stbir__simdfX_float_count) ); - stbir__simdfX_madd( o0, o0, r0, c5 ); stbir__simdfX_madd( o1, o1, r1, c5 ); stbir__simdfX_madd( o2, o2, r2, c5 ); stbir__simdfX_madd( o3, o3, r3, c5 ); - stbir__simdfX_store( output5, o0 ); stbir__simdfX_store( output5+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output5+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output5+(3*stbir__simdfX_float_count), o3 ); ) - stbIF6( stbir__simdfX_load( o0, output6 ); stbir__simdfX_load( o1, output6+stbir__simdfX_float_count ); stbir__simdfX_load( o2, output6+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( o3, output6+(3*stbir__simdfX_float_count) ); - stbir__simdfX_madd( o0, o0, r0, c6 ); stbir__simdfX_madd( o1, o1, r1, c6 ); stbir__simdfX_madd( o2, o2, r2, c6 ); stbir__simdfX_madd( o3, o3, r3, c6 ); - stbir__simdfX_store( output6, o0 ); stbir__simdfX_store( output6+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output6+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output6+(3*stbir__simdfX_float_count), o3 ); ) - stbIF7( stbir__simdfX_load( o0, output7 ); stbir__simdfX_load( o1, output7+stbir__simdfX_float_count ); stbir__simdfX_load( o2, output7+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( o3, output7+(3*stbir__simdfX_float_count) ); - stbir__simdfX_madd( o0, o0, r0, c7 ); stbir__simdfX_madd( o1, o1, r1, c7 ); stbir__simdfX_madd( o2, o2, r2, c7 ); stbir__simdfX_madd( o3, o3, r3, c7 ); - stbir__simdfX_store( output7, o0 ); stbir__simdfX_store( output7+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output7+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output7+(3*stbir__simdfX_float_count), o3 ); ) - #else - stbIF0( stbir__simdfX_mult( o0, r0, c0 ); stbir__simdfX_mult( o1, r1, c0 ); stbir__simdfX_mult( o2, r2, c0 ); stbir__simdfX_mult( o3, r3, c0 ); - stbir__simdfX_store( output0, o0 ); stbir__simdfX_store( output0+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output0+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output0+(3*stbir__simdfX_float_count), o3 ); ) - stbIF1( stbir__simdfX_mult( o0, r0, c1 ); stbir__simdfX_mult( o1, r1, c1 ); stbir__simdfX_mult( o2, r2, c1 ); stbir__simdfX_mult( o3, r3, c1 ); - stbir__simdfX_store( output1, o0 ); stbir__simdfX_store( output1+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output1+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output1+(3*stbir__simdfX_float_count), o3 ); ) - stbIF2( stbir__simdfX_mult( o0, r0, c2 ); stbir__simdfX_mult( o1, r1, c2 ); stbir__simdfX_mult( o2, r2, c2 ); stbir__simdfX_mult( o3, r3, c2 ); - stbir__simdfX_store( output2, o0 ); stbir__simdfX_store( output2+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output2+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output2+(3*stbir__simdfX_float_count), o3 ); ) - stbIF3( stbir__simdfX_mult( o0, r0, c3 ); stbir__simdfX_mult( o1, r1, c3 ); stbir__simdfX_mult( o2, r2, c3 ); stbir__simdfX_mult( o3, r3, c3 ); - stbir__simdfX_store( output3, o0 ); stbir__simdfX_store( output3+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output3+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output3+(3*stbir__simdfX_float_count), o3 ); ) - stbIF4( stbir__simdfX_mult( o0, r0, c4 ); stbir__simdfX_mult( o1, r1, c4 ); stbir__simdfX_mult( o2, r2, c4 ); stbir__simdfX_mult( o3, r3, c4 ); - stbir__simdfX_store( output4, o0 ); stbir__simdfX_store( output4+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output4+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output4+(3*stbir__simdfX_float_count), o3 ); ) - stbIF5( stbir__simdfX_mult( o0, r0, c5 ); stbir__simdfX_mult( o1, r1, c5 ); stbir__simdfX_mult( o2, r2, c5 ); stbir__simdfX_mult( o3, r3, c5 ); - stbir__simdfX_store( output5, o0 ); stbir__simdfX_store( output5+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output5+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output5+(3*stbir__simdfX_float_count), o3 ); ) - stbIF6( stbir__simdfX_mult( o0, r0, c6 ); stbir__simdfX_mult( o1, r1, c6 ); stbir__simdfX_mult( o2, r2, c6 ); stbir__simdfX_mult( o3, r3, c6 ); - stbir__simdfX_store( output6, o0 ); stbir__simdfX_store( output6+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output6+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output6+(3*stbir__simdfX_float_count), o3 ); ) - stbIF7( stbir__simdfX_mult( o0, r0, c7 ); stbir__simdfX_mult( o1, r1, c7 ); stbir__simdfX_mult( o2, r2, c7 ); stbir__simdfX_mult( o3, r3, c7 ); - stbir__simdfX_store( output7, o0 ); stbir__simdfX_store( output7+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output7+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output7+(3*stbir__simdfX_float_count), o3 ); ) - #endif - - input += (4*stbir__simdfX_float_count); - stbIF0( output0 += (4*stbir__simdfX_float_count); ) stbIF1( output1 += (4*stbir__simdfX_float_count); ) stbIF2( output2 += (4*stbir__simdfX_float_count); ) stbIF3( output3 += (4*stbir__simdfX_float_count); ) stbIF4( output4 += (4*stbir__simdfX_float_count); ) stbIF5( output5 += (4*stbir__simdfX_float_count); ) stbIF6( output6 += (4*stbir__simdfX_float_count); ) stbIF7( output7 += (4*stbir__simdfX_float_count); ) - } - STBIR_SIMD_NO_UNROLL_LOOP_START - while ( ( (char*)input_end - (char*) input ) >= 16 ) - { - stbir__simdf o0, r0; - STBIR_SIMD_NO_UNROLL(output0); - - stbir__simdf_load( r0, input ); - - #ifdef STB_IMAGE_RESIZE_VERTICAL_CONTINUE - stbIF0( stbir__simdf_load( o0, output0 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c0 ) ); stbir__simdf_store( output0, o0 ); ) - stbIF1( stbir__simdf_load( o0, output1 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c1 ) ); stbir__simdf_store( output1, o0 ); ) - stbIF2( stbir__simdf_load( o0, output2 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c2 ) ); stbir__simdf_store( output2, o0 ); ) - stbIF3( stbir__simdf_load( o0, output3 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c3 ) ); stbir__simdf_store( output3, o0 ); ) - stbIF4( stbir__simdf_load( o0, output4 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c4 ) ); stbir__simdf_store( output4, o0 ); ) - stbIF5( stbir__simdf_load( o0, output5 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c5 ) ); stbir__simdf_store( output5, o0 ); ) - stbIF6( stbir__simdf_load( o0, output6 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c6 ) ); stbir__simdf_store( output6, o0 ); ) - stbIF7( stbir__simdf_load( o0, output7 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c7 ) ); stbir__simdf_store( output7, o0 ); ) - #else - stbIF0( stbir__simdf_mult( o0, r0, stbir__if_simdf8_cast_to_simdf4( c0 ) ); stbir__simdf_store( output0, o0 ); ) - stbIF1( stbir__simdf_mult( o0, r0, stbir__if_simdf8_cast_to_simdf4( c1 ) ); stbir__simdf_store( output1, o0 ); ) - stbIF2( stbir__simdf_mult( o0, r0, stbir__if_simdf8_cast_to_simdf4( c2 ) ); stbir__simdf_store( output2, o0 ); ) - stbIF3( stbir__simdf_mult( o0, r0, stbir__if_simdf8_cast_to_simdf4( c3 ) ); stbir__simdf_store( output3, o0 ); ) - stbIF4( stbir__simdf_mult( o0, r0, stbir__if_simdf8_cast_to_simdf4( c4 ) ); stbir__simdf_store( output4, o0 ); ) - stbIF5( stbir__simdf_mult( o0, r0, stbir__if_simdf8_cast_to_simdf4( c5 ) ); stbir__simdf_store( output5, o0 ); ) - stbIF6( stbir__simdf_mult( o0, r0, stbir__if_simdf8_cast_to_simdf4( c6 ) ); stbir__simdf_store( output6, o0 ); ) - stbIF7( stbir__simdf_mult( o0, r0, stbir__if_simdf8_cast_to_simdf4( c7 ) ); stbir__simdf_store( output7, o0 ); ) - #endif - - input += 4; - stbIF0( output0 += 4; ) stbIF1( output1 += 4; ) stbIF2( output2 += 4; ) stbIF3( output3 += 4; ) stbIF4( output4 += 4; ) stbIF5( output5 += 4; ) stbIF6( output6 += 4; ) stbIF7( output7 += 4; ) - } - } - #else - STBIR_NO_UNROLL_LOOP_START - while ( ( (char*)input_end - (char*) input ) >= 16 ) - { - float r0, r1, r2, r3; - STBIR_NO_UNROLL(input); - - r0 = input[0], r1 = input[1], r2 = input[2], r3 = input[3]; - - #ifdef STB_IMAGE_RESIZE_VERTICAL_CONTINUE - stbIF0( output0[0] += ( r0 * c0s ); output0[1] += ( r1 * c0s ); output0[2] += ( r2 * c0s ); output0[3] += ( r3 * c0s ); ) - stbIF1( output1[0] += ( r0 * c1s ); output1[1] += ( r1 * c1s ); output1[2] += ( r2 * c1s ); output1[3] += ( r3 * c1s ); ) - stbIF2( output2[0] += ( r0 * c2s ); output2[1] += ( r1 * c2s ); output2[2] += ( r2 * c2s ); output2[3] += ( r3 * c2s ); ) - stbIF3( output3[0] += ( r0 * c3s ); output3[1] += ( r1 * c3s ); output3[2] += ( r2 * c3s ); output3[3] += ( r3 * c3s ); ) - stbIF4( output4[0] += ( r0 * c4s ); output4[1] += ( r1 * c4s ); output4[2] += ( r2 * c4s ); output4[3] += ( r3 * c4s ); ) - stbIF5( output5[0] += ( r0 * c5s ); output5[1] += ( r1 * c5s ); output5[2] += ( r2 * c5s ); output5[3] += ( r3 * c5s ); ) - stbIF6( output6[0] += ( r0 * c6s ); output6[1] += ( r1 * c6s ); output6[2] += ( r2 * c6s ); output6[3] += ( r3 * c6s ); ) - stbIF7( output7[0] += ( r0 * c7s ); output7[1] += ( r1 * c7s ); output7[2] += ( r2 * c7s ); output7[3] += ( r3 * c7s ); ) - #else - stbIF0( output0[0] = ( r0 * c0s ); output0[1] = ( r1 * c0s ); output0[2] = ( r2 * c0s ); output0[3] = ( r3 * c0s ); ) - stbIF1( output1[0] = ( r0 * c1s ); output1[1] = ( r1 * c1s ); output1[2] = ( r2 * c1s ); output1[3] = ( r3 * c1s ); ) - stbIF2( output2[0] = ( r0 * c2s ); output2[1] = ( r1 * c2s ); output2[2] = ( r2 * c2s ); output2[3] = ( r3 * c2s ); ) - stbIF3( output3[0] = ( r0 * c3s ); output3[1] = ( r1 * c3s ); output3[2] = ( r2 * c3s ); output3[3] = ( r3 * c3s ); ) - stbIF4( output4[0] = ( r0 * c4s ); output4[1] = ( r1 * c4s ); output4[2] = ( r2 * c4s ); output4[3] = ( r3 * c4s ); ) - stbIF5( output5[0] = ( r0 * c5s ); output5[1] = ( r1 * c5s ); output5[2] = ( r2 * c5s ); output5[3] = ( r3 * c5s ); ) - stbIF6( output6[0] = ( r0 * c6s ); output6[1] = ( r1 * c6s ); output6[2] = ( r2 * c6s ); output6[3] = ( r3 * c6s ); ) - stbIF7( output7[0] = ( r0 * c7s ); output7[1] = ( r1 * c7s ); output7[2] = ( r2 * c7s ); output7[3] = ( r3 * c7s ); ) - #endif - - input += 4; - stbIF0( output0 += 4; ) stbIF1( output1 += 4; ) stbIF2( output2 += 4; ) stbIF3( output3 += 4; ) stbIF4( output4 += 4; ) stbIF5( output5 += 4; ) stbIF6( output6 += 4; ) stbIF7( output7 += 4; ) - } - #endif - STBIR_NO_UNROLL_LOOP_START - while ( input < input_end ) - { - float r = input[0]; - STBIR_NO_UNROLL(output0); - - #ifdef STB_IMAGE_RESIZE_VERTICAL_CONTINUE - stbIF0( output0[0] += ( r * c0s ); ) - stbIF1( output1[0] += ( r * c1s ); ) - stbIF2( output2[0] += ( r * c2s ); ) - stbIF3( output3[0] += ( r * c3s ); ) - stbIF4( output4[0] += ( r * c4s ); ) - stbIF5( output5[0] += ( r * c5s ); ) - stbIF6( output6[0] += ( r * c6s ); ) - stbIF7( output7[0] += ( r * c7s ); ) - #else - stbIF0( output0[0] = ( r * c0s ); ) - stbIF1( output1[0] = ( r * c1s ); ) - stbIF2( output2[0] = ( r * c2s ); ) - stbIF3( output3[0] = ( r * c3s ); ) - stbIF4( output4[0] = ( r * c4s ); ) - stbIF5( output5[0] = ( r * c5s ); ) - stbIF6( output6[0] = ( r * c6s ); ) - stbIF7( output7[0] = ( r * c7s ); ) - #endif - - ++input; - stbIF0( ++output0; ) stbIF1( ++output1; ) stbIF2( ++output2; ) stbIF3( ++output3; ) stbIF4( ++output4; ) stbIF5( ++output5; ) stbIF6( ++output6; ) stbIF7( ++output7; ) - } -} - -static void STBIR_chans( stbir__vertical_gather_with_,_coeffs)( float * outputp, float const * vertical_coefficients, float const ** inputs, float const * input0_end ) -{ - float STBIR_SIMD_STREAMOUT_PTR( * ) output = outputp; - - stbIF0( float const * input0 = inputs[0]; float c0s = vertical_coefficients[0]; ) - stbIF1( float const * input1 = inputs[1]; float c1s = vertical_coefficients[1]; ) - stbIF2( float const * input2 = inputs[2]; float c2s = vertical_coefficients[2]; ) - stbIF3( float const * input3 = inputs[3]; float c3s = vertical_coefficients[3]; ) - stbIF4( float const * input4 = inputs[4]; float c4s = vertical_coefficients[4]; ) - stbIF5( float const * input5 = inputs[5]; float c5s = vertical_coefficients[5]; ) - stbIF6( float const * input6 = inputs[6]; float c6s = vertical_coefficients[6]; ) - stbIF7( float const * input7 = inputs[7]; float c7s = vertical_coefficients[7]; ) - -#if ( STBIR__vertical_channels == 1 ) && !defined(STB_IMAGE_RESIZE_VERTICAL_CONTINUE) - if ( ( c0s >= (1.0f-0.000001f) ) && ( c0s <= (1.0f+0.000001f) ) ) - { - STBIR_MEMCPY( output, input0, (char*)input0_end - (char*)input0 ); - return; - } -#endif - - #ifdef STBIR_SIMD - { - stbIF0(stbir__simdfX c0 = stbir__simdf_frepX( c0s ); ) - stbIF1(stbir__simdfX c1 = stbir__simdf_frepX( c1s ); ) - stbIF2(stbir__simdfX c2 = stbir__simdf_frepX( c2s ); ) - stbIF3(stbir__simdfX c3 = stbir__simdf_frepX( c3s ); ) - stbIF4(stbir__simdfX c4 = stbir__simdf_frepX( c4s ); ) - stbIF5(stbir__simdfX c5 = stbir__simdf_frepX( c5s ); ) - stbIF6(stbir__simdfX c6 = stbir__simdf_frepX( c6s ); ) - stbIF7(stbir__simdfX c7 = stbir__simdf_frepX( c7s ); ) - - STBIR_SIMD_NO_UNROLL_LOOP_START - while ( ( (char*)input0_end - (char*) input0 ) >= (16*stbir__simdfX_float_count) ) - { - stbir__simdfX o0, o1, o2, o3, r0, r1, r2, r3; - STBIR_SIMD_NO_UNROLL(output); - - stbIF0( stbir__prefetch( input0 + (16*stbir__simdfX_float_count) ); ) - stbIF1( stbir__prefetch( input1 + (16*stbir__simdfX_float_count) ); ) - stbIF2( stbir__prefetch( input2 + (16*stbir__simdfX_float_count) ); ) - stbIF3( stbir__prefetch( input3 + (16*stbir__simdfX_float_count) ); ) - stbIF4( stbir__prefetch( input4 + (16*stbir__simdfX_float_count) ); ) - stbIF5( stbir__prefetch( input5 + (16*stbir__simdfX_float_count) ); ) - stbIF6( stbir__prefetch( input6 + (16*stbir__simdfX_float_count) ); ) - stbIF7( stbir__prefetch( input7 + (16*stbir__simdfX_float_count) ); ) - - #ifdef STB_IMAGE_RESIZE_VERTICAL_CONTINUE - stbIF0( stbir__simdfX_load( o0, output ); stbir__simdfX_load( o1, output+stbir__simdfX_float_count ); stbir__simdfX_load( o2, output+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( o3, output+(3*stbir__simdfX_float_count) ); - stbir__simdfX_load( r0, input0 ); stbir__simdfX_load( r1, input0+stbir__simdfX_float_count ); stbir__simdfX_load( r2, input0+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( r3, input0+(3*stbir__simdfX_float_count) ); - stbir__simdfX_madd( o0, o0, r0, c0 ); stbir__simdfX_madd( o1, o1, r1, c0 ); stbir__simdfX_madd( o2, o2, r2, c0 ); stbir__simdfX_madd( o3, o3, r3, c0 ); ) - #else - stbIF0( stbir__simdfX_load( r0, input0 ); stbir__simdfX_load( r1, input0+stbir__simdfX_float_count ); stbir__simdfX_load( r2, input0+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( r3, input0+(3*stbir__simdfX_float_count) ); - stbir__simdfX_mult( o0, r0, c0 ); stbir__simdfX_mult( o1, r1, c0 ); stbir__simdfX_mult( o2, r2, c0 ); stbir__simdfX_mult( o3, r3, c0 ); ) - #endif - - stbIF1( stbir__simdfX_load( r0, input1 ); stbir__simdfX_load( r1, input1+stbir__simdfX_float_count ); stbir__simdfX_load( r2, input1+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( r3, input1+(3*stbir__simdfX_float_count) ); - stbir__simdfX_madd( o0, o0, r0, c1 ); stbir__simdfX_madd( o1, o1, r1, c1 ); stbir__simdfX_madd( o2, o2, r2, c1 ); stbir__simdfX_madd( o3, o3, r3, c1 ); ) - stbIF2( stbir__simdfX_load( r0, input2 ); stbir__simdfX_load( r1, input2+stbir__simdfX_float_count ); stbir__simdfX_load( r2, input2+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( r3, input2+(3*stbir__simdfX_float_count) ); - stbir__simdfX_madd( o0, o0, r0, c2 ); stbir__simdfX_madd( o1, o1, r1, c2 ); stbir__simdfX_madd( o2, o2, r2, c2 ); stbir__simdfX_madd( o3, o3, r3, c2 ); ) - stbIF3( stbir__simdfX_load( r0, input3 ); stbir__simdfX_load( r1, input3+stbir__simdfX_float_count ); stbir__simdfX_load( r2, input3+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( r3, input3+(3*stbir__simdfX_float_count) ); - stbir__simdfX_madd( o0, o0, r0, c3 ); stbir__simdfX_madd( o1, o1, r1, c3 ); stbir__simdfX_madd( o2, o2, r2, c3 ); stbir__simdfX_madd( o3, o3, r3, c3 ); ) - stbIF4( stbir__simdfX_load( r0, input4 ); stbir__simdfX_load( r1, input4+stbir__simdfX_float_count ); stbir__simdfX_load( r2, input4+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( r3, input4+(3*stbir__simdfX_float_count) ); - stbir__simdfX_madd( o0, o0, r0, c4 ); stbir__simdfX_madd( o1, o1, r1, c4 ); stbir__simdfX_madd( o2, o2, r2, c4 ); stbir__simdfX_madd( o3, o3, r3, c4 ); ) - stbIF5( stbir__simdfX_load( r0, input5 ); stbir__simdfX_load( r1, input5+stbir__simdfX_float_count ); stbir__simdfX_load( r2, input5+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( r3, input5+(3*stbir__simdfX_float_count) ); - stbir__simdfX_madd( o0, o0, r0, c5 ); stbir__simdfX_madd( o1, o1, r1, c5 ); stbir__simdfX_madd( o2, o2, r2, c5 ); stbir__simdfX_madd( o3, o3, r3, c5 ); ) - stbIF6( stbir__simdfX_load( r0, input6 ); stbir__simdfX_load( r1, input6+stbir__simdfX_float_count ); stbir__simdfX_load( r2, input6+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( r3, input6+(3*stbir__simdfX_float_count) ); - stbir__simdfX_madd( o0, o0, r0, c6 ); stbir__simdfX_madd( o1, o1, r1, c6 ); stbir__simdfX_madd( o2, o2, r2, c6 ); stbir__simdfX_madd( o3, o3, r3, c6 ); ) - stbIF7( stbir__simdfX_load( r0, input7 ); stbir__simdfX_load( r1, input7+stbir__simdfX_float_count ); stbir__simdfX_load( r2, input7+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( r3, input7+(3*stbir__simdfX_float_count) ); - stbir__simdfX_madd( o0, o0, r0, c7 ); stbir__simdfX_madd( o1, o1, r1, c7 ); stbir__simdfX_madd( o2, o2, r2, c7 ); stbir__simdfX_madd( o3, o3, r3, c7 ); ) - - stbir__simdfX_store( output, o0 ); stbir__simdfX_store( output+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output+(3*stbir__simdfX_float_count), o3 ); - output += (4*stbir__simdfX_float_count); - stbIF0( input0 += (4*stbir__simdfX_float_count); ) stbIF1( input1 += (4*stbir__simdfX_float_count); ) stbIF2( input2 += (4*stbir__simdfX_float_count); ) stbIF3( input3 += (4*stbir__simdfX_float_count); ) stbIF4( input4 += (4*stbir__simdfX_float_count); ) stbIF5( input5 += (4*stbir__simdfX_float_count); ) stbIF6( input6 += (4*stbir__simdfX_float_count); ) stbIF7( input7 += (4*stbir__simdfX_float_count); ) - } - - STBIR_SIMD_NO_UNROLL_LOOP_START - while ( ( (char*)input0_end - (char*) input0 ) >= 16 ) - { - stbir__simdf o0, r0; - STBIR_SIMD_NO_UNROLL(output); - - #ifdef STB_IMAGE_RESIZE_VERTICAL_CONTINUE - stbIF0( stbir__simdf_load( o0, output ); stbir__simdf_load( r0, input0 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c0 ) ); ) - #else - stbIF0( stbir__simdf_load( r0, input0 ); stbir__simdf_mult( o0, r0, stbir__if_simdf8_cast_to_simdf4( c0 ) ); ) - #endif - stbIF1( stbir__simdf_load( r0, input1 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c1 ) ); ) - stbIF2( stbir__simdf_load( r0, input2 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c2 ) ); ) - stbIF3( stbir__simdf_load( r0, input3 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c3 ) ); ) - stbIF4( stbir__simdf_load( r0, input4 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c4 ) ); ) - stbIF5( stbir__simdf_load( r0, input5 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c5 ) ); ) - stbIF6( stbir__simdf_load( r0, input6 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c6 ) ); ) - stbIF7( stbir__simdf_load( r0, input7 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c7 ) ); ) - - stbir__simdf_store( output, o0 ); - output += 4; - stbIF0( input0 += 4; ) stbIF1( input1 += 4; ) stbIF2( input2 += 4; ) stbIF3( input3 += 4; ) stbIF4( input4 += 4; ) stbIF5( input5 += 4; ) stbIF6( input6 += 4; ) stbIF7( input7 += 4; ) - } - } - #else - STBIR_NO_UNROLL_LOOP_START - while ( ( (char*)input0_end - (char*) input0 ) >= 16 ) - { - float o0, o1, o2, o3; - STBIR_NO_UNROLL(output); - #ifdef STB_IMAGE_RESIZE_VERTICAL_CONTINUE - stbIF0( o0 = output[0] + input0[0] * c0s; o1 = output[1] + input0[1] * c0s; o2 = output[2] + input0[2] * c0s; o3 = output[3] + input0[3] * c0s; ) - #else - stbIF0( o0 = input0[0] * c0s; o1 = input0[1] * c0s; o2 = input0[2] * c0s; o3 = input0[3] * c0s; ) - #endif - stbIF1( o0 += input1[0] * c1s; o1 += input1[1] * c1s; o2 += input1[2] * c1s; o3 += input1[3] * c1s; ) - stbIF2( o0 += input2[0] * c2s; o1 += input2[1] * c2s; o2 += input2[2] * c2s; o3 += input2[3] * c2s; ) - stbIF3( o0 += input3[0] * c3s; o1 += input3[1] * c3s; o2 += input3[2] * c3s; o3 += input3[3] * c3s; ) - stbIF4( o0 += input4[0] * c4s; o1 += input4[1] * c4s; o2 += input4[2] * c4s; o3 += input4[3] * c4s; ) - stbIF5( o0 += input5[0] * c5s; o1 += input5[1] * c5s; o2 += input5[2] * c5s; o3 += input5[3] * c5s; ) - stbIF6( o0 += input6[0] * c6s; o1 += input6[1] * c6s; o2 += input6[2] * c6s; o3 += input6[3] * c6s; ) - stbIF7( o0 += input7[0] * c7s; o1 += input7[1] * c7s; o2 += input7[2] * c7s; o3 += input7[3] * c7s; ) - output[0] = o0; output[1] = o1; output[2] = o2; output[3] = o3; - output += 4; - stbIF0( input0 += 4; ) stbIF1( input1 += 4; ) stbIF2( input2 += 4; ) stbIF3( input3 += 4; ) stbIF4( input4 += 4; ) stbIF5( input5 += 4; ) stbIF6( input6 += 4; ) stbIF7( input7 += 4; ) - } - #endif - STBIR_NO_UNROLL_LOOP_START - while ( input0 < input0_end ) - { - float o0; - STBIR_NO_UNROLL(output); - #ifdef STB_IMAGE_RESIZE_VERTICAL_CONTINUE - stbIF0( o0 = output[0] + input0[0] * c0s; ) - #else - stbIF0( o0 = input0[0] * c0s; ) - #endif - stbIF1( o0 += input1[0] * c1s; ) - stbIF2( o0 += input2[0] * c2s; ) - stbIF3( o0 += input3[0] * c3s; ) - stbIF4( o0 += input4[0] * c4s; ) - stbIF5( o0 += input5[0] * c5s; ) - stbIF6( o0 += input6[0] * c6s; ) - stbIF7( o0 += input7[0] * c7s; ) - output[0] = o0; - ++output; - stbIF0( ++input0; ) stbIF1( ++input1; ) stbIF2( ++input2; ) stbIF3( ++input3; ) stbIF4( ++input4; ) stbIF5( ++input5; ) stbIF6( ++input6; ) stbIF7( ++input7; ) - } -} - -#undef stbIF0 -#undef stbIF1 -#undef stbIF2 -#undef stbIF3 -#undef stbIF4 -#undef stbIF5 -#undef stbIF6 -#undef stbIF7 -#undef STB_IMAGE_RESIZE_DO_VERTICALS -#undef STBIR__vertical_channels -#undef STB_IMAGE_RESIZE_DO_HORIZONTALS -#undef STBIR_strs_join24 -#undef STBIR_strs_join14 -#undef STBIR_chans -#ifdef STB_IMAGE_RESIZE_VERTICAL_CONTINUE -#undef STB_IMAGE_RESIZE_VERTICAL_CONTINUE -#endif - -#else // !STB_IMAGE_RESIZE_DO_VERTICALS - -#define STBIR_chans( start, end ) STBIR_strs_join1(start,STBIR__horizontal_channels,end) - -#ifndef stbir__2_coeff_only -#define stbir__2_coeff_only() \ - stbir__1_coeff_only(); \ - stbir__1_coeff_remnant(1); -#endif - -#ifndef stbir__2_coeff_remnant -#define stbir__2_coeff_remnant( ofs ) \ - stbir__1_coeff_remnant(ofs); \ - stbir__1_coeff_remnant((ofs)+1); -#endif - -#ifndef stbir__3_coeff_only -#define stbir__3_coeff_only() \ - stbir__2_coeff_only(); \ - stbir__1_coeff_remnant(2); -#endif - -#ifndef stbir__3_coeff_remnant -#define stbir__3_coeff_remnant( ofs ) \ - stbir__2_coeff_remnant(ofs); \ - stbir__1_coeff_remnant((ofs)+2); -#endif - -#ifndef stbir__3_coeff_setup -#define stbir__3_coeff_setup() -#endif - -#ifndef stbir__4_coeff_start -#define stbir__4_coeff_start() \ - stbir__2_coeff_only(); \ - stbir__2_coeff_remnant(2); -#endif - -#ifndef stbir__4_coeff_continue_from_4 -#define stbir__4_coeff_continue_from_4( ofs ) \ - stbir__2_coeff_remnant(ofs); \ - stbir__2_coeff_remnant((ofs)+2); -#endif - -#ifndef stbir__store_output_tiny -#define stbir__store_output_tiny stbir__store_output -#endif - -static void STBIR_chans( stbir__horizontal_gather_,_channels_with_1_coeff)( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) -{ - float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; - float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; - STBIR_SIMD_NO_UNROLL_LOOP_START - do { - float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; - float const * hc = horizontal_coefficients; - stbir__1_coeff_only(); - stbir__store_output_tiny(); - } while ( output < output_end ); -} - -static void STBIR_chans( stbir__horizontal_gather_,_channels_with_2_coeffs)( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) -{ - float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; - float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; - STBIR_SIMD_NO_UNROLL_LOOP_START - do { - float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; - float const * hc = horizontal_coefficients; - stbir__2_coeff_only(); - stbir__store_output_tiny(); - } while ( output < output_end ); -} - -static void STBIR_chans( stbir__horizontal_gather_,_channels_with_3_coeffs)( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) -{ - float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; - float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; - STBIR_SIMD_NO_UNROLL_LOOP_START - do { - float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; - float const * hc = horizontal_coefficients; - stbir__3_coeff_only(); - stbir__store_output_tiny(); - } while ( output < output_end ); -} - -static void STBIR_chans( stbir__horizontal_gather_,_channels_with_4_coeffs)( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) -{ - float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; - float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; - STBIR_SIMD_NO_UNROLL_LOOP_START - do { - float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; - float const * hc = horizontal_coefficients; - stbir__4_coeff_start(); - stbir__store_output(); - } while ( output < output_end ); -} - -static void STBIR_chans( stbir__horizontal_gather_,_channels_with_5_coeffs)( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) -{ - float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; - float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; - STBIR_SIMD_NO_UNROLL_LOOP_START - do { - float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; - float const * hc = horizontal_coefficients; - stbir__4_coeff_start(); - stbir__1_coeff_remnant(4); - stbir__store_output(); - } while ( output < output_end ); -} - -static void STBIR_chans( stbir__horizontal_gather_,_channels_with_6_coeffs)( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) -{ - float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; - float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; - STBIR_SIMD_NO_UNROLL_LOOP_START - do { - float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; - float const * hc = horizontal_coefficients; - stbir__4_coeff_start(); - stbir__2_coeff_remnant(4); - stbir__store_output(); - } while ( output < output_end ); -} - -static void STBIR_chans( stbir__horizontal_gather_,_channels_with_7_coeffs)( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) -{ - float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; - float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; - stbir__3_coeff_setup(); - STBIR_SIMD_NO_UNROLL_LOOP_START - do { - float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; - float const * hc = horizontal_coefficients; - - stbir__4_coeff_start(); - stbir__3_coeff_remnant(4); - stbir__store_output(); - } while ( output < output_end ); -} - -static void STBIR_chans( stbir__horizontal_gather_,_channels_with_8_coeffs)( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) -{ - float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; - float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; - STBIR_SIMD_NO_UNROLL_LOOP_START - do { - float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; - float const * hc = horizontal_coefficients; - stbir__4_coeff_start(); - stbir__4_coeff_continue_from_4(4); - stbir__store_output(); - } while ( output < output_end ); -} - -static void STBIR_chans( stbir__horizontal_gather_,_channels_with_9_coeffs)( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) -{ - float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; - float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; - STBIR_SIMD_NO_UNROLL_LOOP_START - do { - float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; - float const * hc = horizontal_coefficients; - stbir__4_coeff_start(); - stbir__4_coeff_continue_from_4(4); - stbir__1_coeff_remnant(8); - stbir__store_output(); - } while ( output < output_end ); -} - -static void STBIR_chans( stbir__horizontal_gather_,_channels_with_10_coeffs)( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) -{ - float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; - float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; - STBIR_SIMD_NO_UNROLL_LOOP_START - do { - float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; - float const * hc = horizontal_coefficients; - stbir__4_coeff_start(); - stbir__4_coeff_continue_from_4(4); - stbir__2_coeff_remnant(8); - stbir__store_output(); - } while ( output < output_end ); -} - -static void STBIR_chans( stbir__horizontal_gather_,_channels_with_11_coeffs)( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) -{ - float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; - float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; - stbir__3_coeff_setup(); - STBIR_SIMD_NO_UNROLL_LOOP_START - do { - float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; - float const * hc = horizontal_coefficients; - stbir__4_coeff_start(); - stbir__4_coeff_continue_from_4(4); - stbir__3_coeff_remnant(8); - stbir__store_output(); - } while ( output < output_end ); -} - -static void STBIR_chans( stbir__horizontal_gather_,_channels_with_12_coeffs)( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) -{ - float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; - float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; - STBIR_SIMD_NO_UNROLL_LOOP_START - do { - float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; - float const * hc = horizontal_coefficients; - stbir__4_coeff_start(); - stbir__4_coeff_continue_from_4(4); - stbir__4_coeff_continue_from_4(8); - stbir__store_output(); - } while ( output < output_end ); -} - -static void STBIR_chans( stbir__horizontal_gather_,_channels_with_n_coeffs_mod0 )( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) -{ - float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; - float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; - STBIR_SIMD_NO_UNROLL_LOOP_START - do { - float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; - int n = ( ( horizontal_contributors->n1 - horizontal_contributors->n0 + 1 ) - 4 + 3 ) >> 2; - float const * hc = horizontal_coefficients; - - stbir__4_coeff_start(); - STBIR_SIMD_NO_UNROLL_LOOP_START - do { - hc += 4; - decode += STBIR__horizontal_channels * 4; - stbir__4_coeff_continue_from_4( 0 ); - --n; - } while ( n > 0 ); - stbir__store_output(); - } while ( output < output_end ); -} - -static void STBIR_chans( stbir__horizontal_gather_,_channels_with_n_coeffs_mod1 )( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) -{ - float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; - float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; - STBIR_SIMD_NO_UNROLL_LOOP_START - do { - float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; - int n = ( ( horizontal_contributors->n1 - horizontal_contributors->n0 + 1 ) - 5 + 3 ) >> 2; - float const * hc = horizontal_coefficients; - - stbir__4_coeff_start(); - STBIR_SIMD_NO_UNROLL_LOOP_START - do { - hc += 4; - decode += STBIR__horizontal_channels * 4; - stbir__4_coeff_continue_from_4( 0 ); - --n; - } while ( n > 0 ); - stbir__1_coeff_remnant( 4 ); - stbir__store_output(); - } while ( output < output_end ); -} - -static void STBIR_chans( stbir__horizontal_gather_,_channels_with_n_coeffs_mod2 )( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) -{ - float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; - float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; - STBIR_SIMD_NO_UNROLL_LOOP_START - do { - float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; - int n = ( ( horizontal_contributors->n1 - horizontal_contributors->n0 + 1 ) - 6 + 3 ) >> 2; - float const * hc = horizontal_coefficients; - - stbir__4_coeff_start(); - STBIR_SIMD_NO_UNROLL_LOOP_START - do { - hc += 4; - decode += STBIR__horizontal_channels * 4; - stbir__4_coeff_continue_from_4( 0 ); - --n; - } while ( n > 0 ); - stbir__2_coeff_remnant( 4 ); - - stbir__store_output(); - } while ( output < output_end ); -} - -static void STBIR_chans( stbir__horizontal_gather_,_channels_with_n_coeffs_mod3 )( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) -{ - float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; - float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; - stbir__3_coeff_setup(); - STBIR_SIMD_NO_UNROLL_LOOP_START - do { - float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; - int n = ( ( horizontal_contributors->n1 - horizontal_contributors->n0 + 1 ) - 7 + 3 ) >> 2; - float const * hc = horizontal_coefficients; - - stbir__4_coeff_start(); - STBIR_SIMD_NO_UNROLL_LOOP_START - do { - hc += 4; - decode += STBIR__horizontal_channels * 4; - stbir__4_coeff_continue_from_4( 0 ); - --n; - } while ( n > 0 ); - stbir__3_coeff_remnant( 4 ); - - stbir__store_output(); - } while ( output < output_end ); -} - -static stbir__horizontal_gather_channels_func * STBIR_chans(stbir__horizontal_gather_,_channels_with_n_coeffs_funcs)[4]= -{ - STBIR_chans(stbir__horizontal_gather_,_channels_with_n_coeffs_mod0), - STBIR_chans(stbir__horizontal_gather_,_channels_with_n_coeffs_mod1), - STBIR_chans(stbir__horizontal_gather_,_channels_with_n_coeffs_mod2), - STBIR_chans(stbir__horizontal_gather_,_channels_with_n_coeffs_mod3), -}; - -static stbir__horizontal_gather_channels_func * STBIR_chans(stbir__horizontal_gather_,_channels_funcs)[12]= -{ - STBIR_chans(stbir__horizontal_gather_,_channels_with_1_coeff), - STBIR_chans(stbir__horizontal_gather_,_channels_with_2_coeffs), - STBIR_chans(stbir__horizontal_gather_,_channels_with_3_coeffs), - STBIR_chans(stbir__horizontal_gather_,_channels_with_4_coeffs), - STBIR_chans(stbir__horizontal_gather_,_channels_with_5_coeffs), - STBIR_chans(stbir__horizontal_gather_,_channels_with_6_coeffs), - STBIR_chans(stbir__horizontal_gather_,_channels_with_7_coeffs), - STBIR_chans(stbir__horizontal_gather_,_channels_with_8_coeffs), - STBIR_chans(stbir__horizontal_gather_,_channels_with_9_coeffs), - STBIR_chans(stbir__horizontal_gather_,_channels_with_10_coeffs), - STBIR_chans(stbir__horizontal_gather_,_channels_with_11_coeffs), - STBIR_chans(stbir__horizontal_gather_,_channels_with_12_coeffs), -}; - -#undef STBIR__horizontal_channels -#undef STB_IMAGE_RESIZE_DO_HORIZONTALS -#undef stbir__1_coeff_only -#undef stbir__1_coeff_remnant -#undef stbir__2_coeff_only -#undef stbir__2_coeff_remnant -#undef stbir__3_coeff_only -#undef stbir__3_coeff_remnant -#undef stbir__3_coeff_setup -#undef stbir__4_coeff_start -#undef stbir__4_coeff_continue_from_4 -#undef stbir__store_output -#undef stbir__store_output_tiny -#undef STBIR_chans - -#endif // HORIZONALS - -#undef STBIR_strs_join2 -#undef STBIR_strs_join1 - -#endif // STB_IMAGE_RESIZE_DO_HORIZONTALS/VERTICALS/CODERS diff --git a/models.json b/models.json deleted file mode 100644 index 62ff234a5..000000000 --- a/models.json +++ /dev/null @@ -1,37 +0,0 @@ -[ - {"model": "google/gemma-3-270m-it", "int4": true, "int8": true, "fp16": false, "apple": false, "pipeline_tag": "text-generation", "tags": ["completion"], "description": "Gemma 3 270M instruction-tuned model for on-device text completion."}, - {"model": "google/functiongemma-270m-it", "int4": true, "int8": true, "fp16": false, "apple": false, "pipeline_tag": "text-generation", "tags": ["completion","tools"], "description": "Gemma 3 270M fine-tuned for structured tool and function calling."}, - {"model": "LiquidAI/LFM2.5-350M", "int4": true, "int8": true, "fp16": false, "apple": false, "pipeline_tag": "text-generation", "tags": ["completion","tools","embed"], "description": "LFM2.5 350M compact hybrid language model from Liquid AI designed for edge deployment."}, - {"model": "LiquidAI/LFM2-700M", "int4": true, "int8": true, "fp16": false, "apple": false, "pipeline_tag": "text-generation", "tags": ["completion","tools","embed"], "description": "LFM2 700M hybrid language model from Liquid AI for on-device chat and embeddings."}, - {"model": "Qwen/Qwen3-0.6B", "int4": true, "int8": true, "fp16": false, "apple": false, "pipeline_tag": "text-generation", "tags": ["completion","tools","embed"], "description": "Qwen3 0.6B compact model supporting thinking and non-thinking modes for on-device chat."}, - {"model": "Qwen/Qwen3.5-0.8B", "int4": true, "int8": true, "fp16": false, "apple": false, "pipeline_tag": "image-text-to-text", "tags": ["vision","completion","tools","embed"], "description": "Qwen3.5 0.8B hybrid vision-language model with DeltaNet for on-device multimodal inference."}, - {"model": "google/gemma-3-1b-it", "int4": true, "int8": false, "fp16": false, "apple": false, "pipeline_tag": "text-generation", "tags": ["completion"], "description": "Gemma 3 1B instruction-tuned model for on-device text completion."}, - {"model": "google/gemma-3n-E2B-it", "int4": true, "int8": false, "fp16": false, "apple": false, "pipeline_tag": "text-generation", "tags": ["completion","tools"], "description": "Gemma 3n E2B instruction-tuned model with 2B effective parameters for on-device completion and tool use."}, - {"model": "google/gemma-3n-E4B-it", "int4": true, "int8": false, "fp16": false, "apple": false, "pipeline_tag": "text-generation", "tags": ["completion","tools"], "description": "Gemma 3n E4B instruction-tuned model with 4B effective parameters for on-device completion and tool use."}, - {"model": "google/gemma-4-E2B-it", "int4": true, "int8": false, "fp16": false, "apple": true, "pipeline_tag": "image-text-to-text", "tags": ["vision","audio","completion","tools","apple-npu"], "description": "Gemma 4 E2B instruction-tuned multimodal model with vision and audio for on-device inference."}, - {"model": "google/gemma-4-E4B-it", "int4": true, "int8": false, "fp16": false, "apple": true, "pipeline_tag": "image-text-to-text", "tags": ["vision","audio","completion","tools","apple-npu"], "description": "Gemma 4 E4B instruction-tuned multimodal model with vision and audio for on-device inference."}, - {"model": "LiquidAI/LFM2.5-1.2B-Thinking", "int4": true, "int8": false, "fp16": false, "apple": false, "pipeline_tag": "text-generation", "tags": ["completion","tools","embed"], "description": "LFM2.5 1.2B reasoning model from Liquid AI with extended thinking for on-device deployment."}, - {"model": "LiquidAI/LFM2.5-1.2B-Instruct", "int4": true, "int8": false, "fp16": false, "apple": false, "pipeline_tag": "text-generation", "tags": ["completion","tools","embed"], "description": "LFM2.5 1.2B instruction-tuned language model from Liquid AI designed for edge deployment."}, - {"model": "Qwen/Qwen3-1.7B", "int4": true, "int8": false, "fp16": false, "apple": false, "pipeline_tag": "text-generation", "tags": ["completion","tools","embed"], "description": "Qwen3 1.7B model supporting thinking and non-thinking modes for on-device reasoning."}, - {"model": "Qwen/Qwen3.5-2B", "int4": true, "int8": false, "fp16": false, "apple": false, "pipeline_tag": "image-text-to-text", "tags": ["vision","completion","tools","embed"], "description": "Qwen3.5 2B hybrid vision-language model with DeltaNet for on-device multimodal inference."}, - {"model": "tencent/Youtu-LLM-2B", "int4": true, "int8": false, "fp16": false, "apple": false, "pipeline_tag": "text-generation", "tags": ["completion","tools", "embed"], "description": "Youtu-LLM 1.96B model from Tencent with Dense MLA attention, 128k context, and native agentic capabilities excelling at coding, STEM, and reasoning."}, - {"model": "LiquidAI/LFM2-2.6B", "int4": true, "int8": false, "fp16": false, "apple": false, "pipeline_tag": "text-generation", "tags": ["completion","tools","embed"], "description": "LFM2 2.6B hybrid language model from Liquid AI for on-device chat and embeddings."}, - {"model": "LiquidAI/LFM2-VL-450M", "int4": true, "int8": true, "fp16": false, "apple": true, "pipeline_tag": "image-text-to-text", "tags": ["vision","text-embed","image-embed","apple-npu"], "description": "LFM2-VL 450M compact vision-language model from Liquid AI for on-device image understanding."}, - {"model": "LiquidAI/LFM2.5-VL-450M", "int4": true, "int8": true, "fp16": false, "apple": true, "pipeline_tag": "image-text-to-text", "tags": ["vision","text-embed","image-embed","apple-npu"], "description": "LFM2.5-VL 450M refreshed compact vision-language model from Liquid AI for image and text understanding."}, - {"model": "LiquidAI/LFM2.5-VL-1.6B", "int4": true, "int8": false, "fp16": false, "apple": true, "pipeline_tag": "image-text-to-text", "tags": ["vision","text-embed","image-embed","apple-npu"], "description": "LFM2.5-VL 1.6B vision-language model from Liquid AI for image and text understanding."}, - {"model": "UsefulSensors/moonshine-base", "int4": true, "int8": true, "fp16": false, "apple": true, "pipeline_tag": "automatic-speech-recognition", "tags": ["transcription","speech-embed"], "description": "Moonshine Base 61M parameter English speech recognition model optimized for live transcription."}, - {"model": "openai/whisper-tiny", "int4": true, "int8": true, "fp16": false, "apple": true, "pipeline_tag": "automatic-speech-recognition", "tags": ["transcription","speech-embed","apple-npu"], "description": "Whisper Tiny 39M parameter multilingual speech recognition model by OpenAI."}, - {"model": "openai/whisper-base", "int4": true, "int8": true, "fp16": false, "apple": true, "pipeline_tag": "automatic-speech-recognition", "tags": ["transcription","speech-embed","apple-npu"], "description": "Whisper Base 74M parameter multilingual speech recognition model by OpenAI."}, - {"model": "openai/whisper-small", "int4": true, "int8": true, "fp16": false, "apple": true, "pipeline_tag": "automatic-speech-recognition", "tags": ["transcription","speech-embed","apple-npu"], "description": "Whisper Small 244M parameter multilingual speech recognition model by OpenAI."}, - {"model": "openai/whisper-medium", "int4": true, "int8": true, "fp16": false, "apple": true, "pipeline_tag": "automatic-speech-recognition", "tags": ["transcription","speech-embed","apple-npu"], "description": "Whisper Medium 769M parameter multilingual speech recognition model by OpenAI."}, - {"model": "openai/whisper-large-v3", "int4": true, "int8": true, "fp16": false, "apple": true, "pipeline_tag": "automatic-speech-recognition", "tags": ["transcription","speech-embed","apple-npu"], "description": "Whisper Large v3 1.55B parameter multilingual speech recognition model by OpenAI with improved accuracy and 128 mel bins."}, - {"model": "snakers4/silero-vad", "int4": true, "int8": true, "fp16": false, "apple": false, "pipeline_tag": "voice-activity-detection", "tags": ["vad"], "description": "Silero VAD tiny voice activity detection model supporting over 100 languages."}, - {"model": "nomic-ai/nomic-embed-text-v2-moe", "int4": true, "int8": true, "fp16": false, "apple": false, "pipeline_tag": "feature-extraction", "tags": ["embed"], "description": "Nomic Embed Text v2 MoE 305M multilingual text embedding model using mixture-of-experts."}, - {"model": "Qwen/Qwen3-Embedding-0.6B", "int4": true, "int8": true, "fp16": false, "apple": false, "pipeline_tag": "feature-extraction", "tags": ["embed"], "description": "Qwen3 0.6B text embedding model supporting 100+ languages with 1024-dimensional vectors."}, - {"model": "nvidia/parakeet-ctc-0.6b", "int4": true, "int8": true, "fp16": false, "apple": true, "pipeline_tag": "automatic-speech-recognition", "tags": ["transcription","speech-embed","apple-npu"], "description": "Parakeet CTC 0.6b is a speech recognition model optimized for on-device performance and live transcription."}, - {"model": "nvidia/parakeet-ctc-1.1b", "int4": true, "int8": false, "fp16": false, "apple": true, "pipeline_tag": "automatic-speech-recognition", "tags": ["transcription","speech-embed","apple-npu"], "description": "Parakeet CTC 1.1b is a speech recognition model optimized for on-device performance and live transcription."}, - {"model": "nvidia/parakeet-tdt-0.6b-v3", "int4": true, "int8": true, "fp16": false, "apple": true, "pipeline_tag": "automatic-speech-recognition", "tags": ["transcription","speech-embed","apple-npu"], "description": "Parakeet TDT 0.6b is a speech recognition model optimized for on-device performance and live transcription."}, - {"model": "LiquidAI/LFM2-8B-A1B", "int4": true, "int8": false, "fp16": false, "apple": false, "pipeline_tag": "text-generation", "tags": ["completion","tools","embed"], "description": "LFM2 8B MoE model with 1.5B active parameters for high-quality on-device inference."}, - {"model": "pyannote/segmentation-3.0", "int4": true, "int8": true, "fp16": false, "apple": false, "pipeline_tag": "voice-activity-detection", "tags": ["diarization"], "description": "PyAnnote segmentation-3.0 speaker diarization model using SincNet frontend and BiLSTM layers."}, - {"model": "pyannote/wespeaker-voxceleb-resnet34-LM", "int4": true, "int8": true, "fp16": false, "apple": false, "pipeline_tag": "speaker-recognition", "tags": ["speaker-embed"], "description": "WeSpeaker ResNet34-LM speaker embedding model producing 256-dimensional speaker representations."} -] diff --git a/python/README.md b/python/README.md index c71c5b526..76e6da126 100644 --- a/python/README.md +++ b/python/README.md @@ -8,7 +8,7 @@ keywords: ["Python package", "Python bindings", "on-device AI", "Python FFI", "e Python bindings for Cactus Engine via FFI. Auto-installed when you run `source ./setup`. -> **Model weights:** Pre-converted weights for all supported models at [huggingface.co/Cactus-Compute](https://huggingface.co/Cactus-Compute). +> **Model bundles:** Pre-built runtime bundles for all supported models at [huggingface.co/Cactus-Compute](https://huggingface.co/Cactus-Compute). ## Getting Started @@ -20,9 +20,9 @@ cactus build --python ```bash -# Download models (CLI) +# Download pre-built bundles (defaults to the generic CPU variant) cactus download LiquidAI/LFM2-VL-450M -cactus download openai/whisper-small +cactus download openai/whisper-small --platform apple # CoreML/NPU variant # Optional: set your Cactus Cloud API key for automatic cloud fallback cactus auth @@ -36,12 +36,12 @@ from cactus import ensure_model from cactus import cactus_init, cactus_complete, cactus_destroy import json -# Downloads weights from HuggingFace if not already present -weights = ensure_model("LiquidAI/LFM2-VL-450M") +# Downloads the pre-built bundle from HuggingFace if not already present +bundle = ensure_model("LiquidAI/LFM2-VL-450M") -model = cactus_init(str(weights), None, False) +model = cactus_init(str(bundle), None, False) messages = json.dumps([{"role": "user", "content": "What is 2+2?"}]) -result = json.loads(cactus_complete(model, messages, None, None, None)) +result = cactus_complete(model, messages, None, None, None) print(result["response"]) cactus_destroy(model) ``` @@ -53,16 +53,17 @@ All functions are module-level and mirror the C FFI directly. Handles are plain ### Model Downloads -Download pre-converted weights programmatically (no CLI needed): +Download pre-built bundles programmatically (no CLI needed): ```python -from cactus import ensure_model, get_weights_dir +from cactus import ensure_model, get_bundle_dir -# ensure_model downloads if missing, returns Path to weights dir -weights = ensure_model("openai/whisper-tiny") +# ensure_model downloads the pre-built bundle if missing, returns its Path +bundle = ensure_model("openai/whisper-tiny") -# Or check / download manually -weights_dir = get_weights_dir("openai/whisper-tiny") # -> Path("weights/whisper-tiny") +# Or resolve the expected on-disk location explicitly +bundle_dir = get_bundle_dir("openai/whisper-tiny", bits=4, platform=None) +# -> Path("transpiled/whisper-tiny-cq4") (or `-cq4-apple` with platform="apple") ``` ### Init / Lifecycle @@ -77,17 +78,17 @@ cactus_get_last_error() -> str | None ### Completion -Returns a JSON string with `success`, `error`, `cloud_handoff`, `response`, optional `thinking` (only present when the model emits chain-of-thought content, placed before `function_calls`), `function_calls`, `segments` (always `[]` for completion — populated only in transcription responses), `confidence`, timing stats (`time_to_first_token_ms`, `total_time_ms`, `prefill_tps`, `decode_tps`, `ram_usage_mb`), and token counts (`prefill_tokens`, `decode_tokens`, `total_tokens`). +Returns a `dict` with `success`, `error`, `cloud_handoff`, `response`, optional `thinking` (only present when the model emits chain-of-thought content, placed before `function_calls`), `function_calls`, `segments` (always `[]` for completion — populated only in transcription responses), `confidence`, timing stats (`time_to_first_token_ms`, `total_time_ms`, `prefill_tps`, `decode_tps`, `ram_usage_mb`), and token counts (`prefill_tokens`, `decode_tokens`, `total_tokens`). ```python -result_json = cactus_complete( +result = cactus_complete( model: int, messages_json: str, # JSON array of {role, content} options_json: str | None, # optional inference options tools_json: str | None, # optional tool definitions callback: Callable[[str, int], None] | None, # streaming token callback pcm_data: list[int] | None = None # optional raw audio bytes -) -> str +) -> dict ``` ```python @@ -95,7 +96,7 @@ result_json = cactus_complete( options = json.dumps({"max_tokens": 256, "temperature": 0.7}) def on_token(token, token_id): print(token, end="", flush=True) -result = json.loads(cactus_complete(model, messages_json, options, None, on_token)) +result = cactus_complete(model, messages_json, options, None, on_token) if result["cloud_handoff"]: # response already contains cloud result pass @@ -169,7 +170,7 @@ completion_messages = json.dumps([ {"role": "assistant", "content": "It's sunny and 72°F in Paris!"}, {"role": "user", "content": "What about SF?"} ]) -result = json.loads(cactus_complete(model, completion_messages, None, tools, None)) +result = cactus_complete(model, completion_messages, None, tools, None) ``` **Response format:** @@ -186,17 +187,17 @@ result = json.loads(cactus_complete(model, completion_messages, None, tools, Non ### Transcription -Returns a JSON string. Use `json.loads()` to access the `response` field (transcribed text), the `segments` array (timestamped segments as `{"start": , "end": , "text": ""}` — Whisper: phrase-level from timestamp tokens; Parakeet TDT: word-level from frame timing; Parakeet CTC and Moonshine: one segment per transcription window (consecutive VAD speech regions up to 30s)), and other metadata. +Returns a `dict` with the `response` field (transcribed text), the `segments` array (timestamped segments as `{"start": , "end": , "text": ""}` — Whisper: phrase-level from timestamp tokens; Parakeet TDT: word-level from frame timing; Parakeet CTC and Moonshine: one segment per transcription window (consecutive VAD speech regions up to 30s)), and other metadata. ```python -result_json = cactus_transcribe( +result = cactus_transcribe( model: int, audio_path: str | None, prompt: str | None, options_json: str | None, callback: Callable[[str, int], None] | None, - pcm_data: bytes | None -) -> str + pcm_data: list[int] | bytes | None +) -> dict ``` **Custom vocabulary** biases the decoder toward domain-specific words (supported for Whisper and Moonshine models). Pass `custom_vocabulary` and `vocabulary_boost` in `options_json`: @@ -206,11 +207,11 @@ options = json.dumps({ "custom_vocabulary": ["Omeprazole", "HIPAA", "Cactus"], "vocabulary_boost": 3.0 }) -result = json.loads(cactus_transcribe(model, "medical_notes.wav", None, options, None, None)) +result = cactus_transcribe(model, "medical_notes.wav", None, options, None, None) ``` ```python -result = json.loads(cactus_transcribe(model, "/path/to/audio.wav", None, None, None, None)) +result = cactus_transcribe(model, "/path/to/audio.wav", None, None, None, None) print(result["response"]) for seg in result["segments"]: print(f"[{seg['start']:.3f}s - {seg['end']:.3f}s] {seg['text']}") @@ -227,30 +228,17 @@ embedding = cactus_audio_embed(model: int, audio_path: str) -> list[float] ### Tokenization ```python -tokens = cactus_tokenize(model: int, text: str) -> list[int] -result_json = cactus_score_window(model: int, tokens: list[int], start: int, end: int, context: int) -> str +tokens = cactus_tokenize(model: int, text: str) -> list[int] +result = cactus_score_window(model: int, tokens: list[int], start: int, end: int, context: int) -> dict ``` -### Detect Language - -```python -result_json = cactus_detect_language( - model: int, - audio_path: str | None, - options_json: str | None, - pcm_data: bytes | None -) -> str -``` - -Returns a JSON string with fields: `success`, `error`, `language` (BCP-47 code), `language_token`, `token_id`, `confidence`, `entropy`, `total_time_ms`, `ram_usage_mb`. - ### RAG ```python -result_json = cactus_rag_query(model: int, query: str, top_k: int) -> str +result = cactus_rag_query(model: int, query: str, top_k: int) -> dict ``` -Returns a JSON string with a `chunks` array. Each chunk has `score` (float), `source` (str, from document metadata), and `content` (str): +Returns a `dict` with a `chunks` array. Each chunk has `score` (float), `source` (str, from document metadata), and `content` (str): ```json { @@ -267,8 +255,8 @@ index = cactus_index_init(index_dir: str, embedding_dim: int) -> int cactus_index_add(index: int, ids: list[int], documents: list[str], metadatas: list[str] | None, embeddings: list[list[float]]) cactus_index_delete(index: int, ids: list[int]) -result_json = cactus_index_get(index: int, ids: list[int]) -> str -result_json = cactus_index_query(index: int, embedding: list[float], options_json: str | None) -> str +result = cactus_index_get(index: int, ids: list[int]) -> dict +result = cactus_index_query(index: int, embedding: list[float], options_json: str | None) -> dict cactus_index_compact(index: int) cactus_index_destroy(index: int) ``` @@ -285,7 +273,7 @@ cactus_log_set_callback(callback: Callable[[int, str, str], None] | None) ### Telemetry ```python -cactus_set_telemetry_environment(cache_location: str) +cactus_set_telemetry_environment(framework: str, cache_location: str | None, version: str | None) cactus_set_app_id(app_id: str) cactus_telemetry_flush() cactus_telemetry_shutdown() @@ -303,7 +291,7 @@ messages = json.dumps([{ "content": "Describe this image", "images": ["path/to/image.png"] }]) -result = json.loads(cactus_complete(model, messages, None, None, None)) +result = cactus_complete(model, messages, None, None, None) print(result["response"]) ``` @@ -317,7 +305,7 @@ messages = json.dumps([{ "content": "Transcribe the audio.", "audio": ["path/to/audio.wav"] }]) -result = json.loads(cactus_complete(model, messages, None, None, None)) +result = cactus_complete(model, messages, None, None, None) print(result["response"]) # Combined vision + audio @@ -327,7 +315,7 @@ messages = json.dumps([{ "images": ["path/to/image.png"], "audio": ["path/to/audio.wav"] }]) -result = json.loads(cactus_complete(model, messages, None, None, None)) +result = cactus_complete(model, messages, None, None, None) ``` ## Compute Graph @@ -335,7 +323,7 @@ result = json.loads(cactus_complete(model, messages, None, None, None)) The `Graph` API provides a tensor computation graph for building and executing dataflow pipelines on the Cactus kernel layer: ```python -from cactus.bindings.graph import Graph +from cactus.bindings.cactus import Graph import numpy as np g = Graph() @@ -361,10 +349,8 @@ python python/test.py # compact output python python/test.py -v # verbose ``` -Tests are in `python/tests/`: - -- `test_graph.py` — Graph elementwise, composed, tensor, activation, and softmax ops -- `test_model.py` — VLM completion/embeddings, Whisper transcription/embeddings (auto-downloads weights if missing) +Tests are in `python/tests/` — bindings, CLI, server, graph, model, transpile, +and component-partition coverage. Add a new `test_*.py` to extend. ## See Also diff --git a/python/cactus/__init__.py b/python/cactus/__init__.py index f5d0c383d..841fd0832 100644 --- a/python/cactus/__init__.py +++ b/python/cactus/__init__.py @@ -2,13 +2,14 @@ from ._version import __version__ -from .cli.download import ensure_model, get_weights_dir, get_model_dir_name +from .cli.download import ensure_model, get_weights_dir, get_bundle_dir, get_model_dir_name from .cli import main __all__ = [ "__version__", "ensure_model", "get_weights_dir", + "get_bundle_dir", "get_model_dir_name", "main", "Graph", @@ -49,10 +50,7 @@ def __getattr__(name): - if name in ("Graph", "Tensor"): - from .bindings import graph - return getattr(graph, name) - if name in _FFI_NAMES: + if name in ("Graph", "Tensor") or name in _FFI_NAMES: from .bindings import cactus return getattr(cactus, name) raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/python/cactus/bindings/cactus.py b/python/cactus/bindings/cactus.py index 0b9eff8bb..9ecbd9ef3 100644 --- a/python/cactus/bindings/cactus.py +++ b/python/cactus/bindings/cactus.py @@ -4,9 +4,11 @@ import platform from pathlib import Path +import numpy as np + TokenCallback = ctypes.CFUNCTYPE(None, ctypes.c_char_p, ctypes.c_uint32, ctypes.c_void_p) -_LIB_NAME = "libcactus.dylib" if platform.system() == "Darwin" else "libcactus.so" +_LIB_NAME = "libcactus_engine.dylib" if platform.system() == "Darwin" else "libcactus_engine.so" def _find_library(): @@ -14,7 +16,7 @@ def _find_library(): if bundled.exists(): return bundled - dev_build = Path(__file__).parent.parent.parent.parent / "cactus" / "build" / _LIB_NAME + dev_build = Path(__file__).parent.parent.parent.parent / "cactus-engine" / "build" / _LIB_NAME if dev_build.exists(): return dev_build @@ -26,7 +28,15 @@ def _find_library(): _LIB_PATH = _find_library() -_lib = ctypes.CDLL(str(_LIB_PATH)) +try: + _lib = ctypes.CDLL(str(_LIB_PATH)) +except OSError as exc: + raise RuntimeError( + f"Cactus library found at {_LIB_PATH} but failed to load: {exc}.\n" + "Common causes: wrong arch (the library is built for arm64 only), " + "missing libcurl on Linux, or a stale build. Rebuild with " + "`cactus build --python`." + ) from exc def _bind_optional(name, argtypes, restype): @@ -60,6 +70,7 @@ class cactus_tensor_info_t(ctypes.Structure): # cactus graph API _lib.cactus_graph_create.restype = cactus_graph_t _lib.cactus_graph_destroy.argtypes = [cactus_graph_t] +_lib.cactus_graph_destroy.restype = None _lib.cactus_graph_hard_reset.argtypes = [cactus_graph_t] _lib.cactus_graph_hard_reset.restype = ctypes.c_int @@ -841,7 +852,15 @@ def _from_json(buf): text = buf.value.decode("utf-8", errors="ignore") if not text: return {} - return json.loads(text) + try: + return json.loads(text) + except json.JSONDecodeError as exc: + if len(buf.value) >= len(buf) - 1: + raise RuntimeError( + f"C engine filled the {len(buf)} byte response buffer; " + "response was truncated before valid JSON could be parsed." + ) from exc + raise def _prepare_pcm(pcm_data): @@ -916,17 +935,26 @@ def cactus_init(model_path, corpus_dir=None, cache_index=False): def cactus_destroy(model): - """Free all resources associated with a model handle.""" + """Free all resources associated with a model handle. Safe to call on + a null/already-destroyed handle (no-op).""" + if not model: + return _lib.cactus_destroy(model) def cactus_reset(model): - """Clear the KV cache, resetting the model to a fresh conversation state.""" + """Clear the KV cache, resetting the model to a fresh conversation state. + No-op when ``model`` is null.""" + if not model: + return _lib.cactus_reset(model) def cactus_stop(model): - """Signal the model to stop the current generation early.""" + """Signal the model to stop the current generation early. No-op when + ``model`` is null.""" + if not model: + return _lib.cactus_stop(model) @@ -948,7 +976,7 @@ def cactus_complete(model, messages, options=None, tools=None, callback=None, pc Returns: A dict with the completion response and metrics. """ - buf = ctypes.create_string_buffer(65536) + buf = ctypes.create_string_buffer(1 << 20) cb = _make_token_callback(callback) pcm_ptr, pcm_size = _prepare_pcm(pcm_data) rc = _lib.cactus_complete( @@ -966,7 +994,7 @@ def cactus_prefill(model, messages, options=None, tools=None, pcm_data=None): Returns: A dict with prefill stats (tokens processed, latency, etc.). """ - buf = ctypes.create_string_buffer(65536) + buf = ctypes.create_string_buffer(1 << 20) pcm_ptr, pcm_size = _prepare_pcm(pcm_data) rc = _lib.cactus_prefill( model, _to_json(messages), buf, len(buf), @@ -994,7 +1022,7 @@ def cactus_transcribe(model, audio_path, prompt=None, options=None, callback=Non Returns: A dict with transcription text and segments. """ - buf = ctypes.create_string_buffer(65536) + buf = ctypes.create_string_buffer(1 << 20) cb = _make_token_callback(callback) pcm_ptr, pcm_size = _prepare_pcm(pcm_data) rc = _lib.cactus_transcribe( @@ -1179,7 +1207,15 @@ def cactus_index_add(index, ids, documents, metadatas=None, embeddings=None): embeddings: List of embedding vectors (list of floats each). """ count = len(ids) + if len(documents) != count: + raise ValueError(f"documents length ({len(documents)}) must match ids length ({count})") + if embeddings and len(embeddings) != count: + raise ValueError(f"embeddings length ({len(embeddings)}) must match ids length ({count})") + if metadatas and len(metadatas) != count: + raise ValueError(f"metadatas length ({len(metadatas)}) must match ids length ({count})") embedding_dim = len(embeddings[0]) if embeddings else 0 + if embeddings and any(len(e) != embedding_dim for e in embeddings): + raise ValueError("all embedding vectors must share the same dimension") ids_arr = (ctypes.c_int * count)(*ids) docs_arr = (ctypes.c_char_p * count)() @@ -1290,7 +1326,10 @@ def cactus_index_compact(index): def cactus_index_destroy(index): - """Free all resources associated with an index handle.""" + """Free all resources associated with an index handle. Safe to call on a + null/already-destroyed handle (no-op).""" + if not index: + return _lib.cactus_index_destroy(index) @@ -1322,3 +1361,1540 @@ def _bridge(level, component_bytes, message_bytes, _): _log_callback_ref = LogCallback(_bridge) _lib.cactus_log_set_callback(_log_callback_ref, None) + + +class Graph: + INT8 = 0 + FP16 = 1 + FP32 = 2 + CQ1 = 3 + CQ2 = 4 + CQ3 = 5 + CQ4 = 6 + CPU = 0 + NPU = 1 + ACT_SILU = 0 + ACT_GELU = 1 + ACT_GELU_ERF = 2 + ACT_RELU = 3 + ACT_SIGMOID = 4 + ACT_TANH = 5 + + def __init__(self): + self.h = _lib.cactus_graph_create() + if not self.h: + raise RuntimeError(_err("cactus_graph_create failed")) + + def save(self, filename): + rc = _lib.cactus_graph_save(self.h, str(filename).encode()) + if rc != 0: + raise RuntimeError(_err("graph_save failed")) + + @classmethod + def load(cls, filename): + h = _lib.cactus_graph_load(str(filename).encode()) + if not h: + raise RuntimeError(_err("cactus_graph_load failed")) + obj = cls.__new__(cls) + obj.h = h + return obj + + def __del__(self): + h = getattr(self, "h", None) + if h: + _lib.cactus_graph_destroy(h) + self.h = None + + def input(self, shape, dtype=FP16): + shape = tuple(int(x) for x in shape) + arr = (ctypes.c_size_t * len(shape))(*shape) + out = cactus_node_t() + rc = _lib.cactus_graph_input(self.h, arr, len(shape), int(dtype), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(_err("graph_input failed")) + return self._tensor_from_node(out.value) + + def set_input(self, tensor, data, dtype=None): + if not isinstance(tensor, Tensor): + raise TypeError("tensor must be a Tensor") + if tensor.g is not self: + raise ValueError("tensor belongs to a different graph") + target_dtype = int(tensor.dtype if dtype is None else dtype) + arr = self._coerce_input_array(data, target_dtype) + info = self._get_output_info(tensor.id) + expected_shape = tuple(int(x) for x in info["shape"]) + expected_num_elements = int(info["num_elements"]) + expected_byte_size = int(info["byte_size"]) + if tuple(int(x) for x in arr.shape) != expected_shape: + raise ValueError( + "graph input shape mismatch for node " + f"{tensor.id}: expected {expected_shape}, got {tuple(int(x) for x in arr.shape)}" + ) + if int(arr.size) != expected_num_elements: + raise ValueError( + "graph input element-count mismatch for node " + f"{tensor.id}: expected {expected_num_elements}, got {int(arr.size)}" + ) + if int(arr.nbytes) != expected_byte_size: + raise ValueError( + "graph input byte-size mismatch for node " + f"{tensor.id}: expected {expected_byte_size}, got {int(arr.nbytes)}" + ) + rc = _lib.cactus_graph_set_input( + self.h, + cactus_node_t(tensor.id), + arr.ctypes.data_as(ctypes.c_void_p), + target_dtype, + ) + if rc != 0: + raise RuntimeError(_err("graph_set_input failed")) + + def set_external_input(self, tensor, data_ptr, dtype=None): + if not isinstance(tensor, Tensor): + raise TypeError("tensor must be a Tensor") + if tensor.g is not self: + raise ValueError("tensor belongs to a different graph") + target_dtype = int(tensor.dtype if dtype is None else dtype) + ptr = ctypes.c_void_p(data_ptr if isinstance(data_ptr, int) else int(data_ptr)) + rc = _lib.cactus_graph_set_external_input( + self.h, + cactus_node_t(tensor.id), + ptr, + target_dtype, + ) + if rc != 0: + raise RuntimeError(_err("graph_set_external_input failed")) + + def mark_embedded_input(self, tensor): + if not isinstance(tensor, Tensor): + raise TypeError("tensor must be a Tensor") + if tensor.g is not self: + raise ValueError("tensor belongs to a different graph") + rc = _lib.cactus_graph_mark_embedded_input(self.h, cactus_node_t(tensor.id)) + if rc != 0: + raise RuntimeError(_err("graph_mark_embedded_input failed")) + + def hard_reset(self): + rc = _lib.cactus_graph_hard_reset(self.h) + if rc != 0: + raise RuntimeError(_err("graph_hard_reset failed")) + + def execute(self): + rc = _lib.cactus_graph_execute(self.h) + if rc != 0: + raise RuntimeError(_err("graph_execute failed")) + + def add(self, a, b): + return self._binary("cactus_graph_add", a, b) + + def add_clipped(self, a, b): + return self._binary("cactus_graph_add_clipped", a, b) + + def subtract(self, a, b): + return self._binary("cactus_graph_subtract", a, b) + + def multiply(self, a, b): + return self._binary("cactus_graph_multiply", a, b) + + def divide(self, a, b): + return self._binary("cactus_graph_divide", a, b) + + def not_equal(self, a, b): + return self._binary("cactus_graph_not_equal", a, b) + + def abs(self, x): + x = self._ensure_tensor(x) + out = cactus_node_t() + rc = _lib.cactus_graph_abs(self.h, cactus_node_t(x.id), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(_err("graph_abs failed")) + return self._tensor_from_node(out.value) + + def pow(self, x, exponent): + x = self._ensure_tensor(x) + out = cactus_node_t() + rc = _lib.cactus_graph_pow(self.h, cactus_node_t(x.id), ctypes.c_float(float(exponent)), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(_err("graph_pow failed")) + return self._tensor_from_node(out.value) + + def precision_cast(self, x, dtype): + x = self._ensure_tensor(x) + out = cactus_node_t() + rc = _lib.cactus_graph_precision_cast(self.h, cactus_node_t(x.id), int(dtype), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(_err("graph_precision_cast failed")) + return self._tensor_from_node(out.value) + + def quantize_activations(self, x): + x = self._ensure_tensor(x) + out = cactus_node_t() + rc = _lib.cactus_graph_quantize_activations(self.h, cactus_node_t(x.id), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(_err("graph_quantize_activations failed")) + return self._tensor_from_node(out.value) + + def _scalar(self, fn_name, x, value=None): + x = self._ensure_tensor(x) + out = cactus_node_t() + fn = getattr(_lib, fn_name) + if value is None: + rc = fn(self.h, cactus_node_t(x.id), ctypes.byref(out)) + else: + rc = fn(self.h, cactus_node_t(x.id), ctypes.c_float(float(value)), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(f"{fn_name} failed") + return self._tensor_from_node(out.value) + + def scalar_add(self, x, value): + return self._scalar("cactus_graph_scalar_add", x, value) + + def scalar_subtract(self, x, value): + return self._scalar("cactus_graph_scalar_subtract", x, value) + + def scalar_multiply(self, x, value): + return self._scalar("cactus_graph_scalar_multiply", x, value) + + def scalar_divide(self, x, value): + return self._scalar("cactus_graph_scalar_divide", x, value) + + def scalar_floor_divide(self, x, value): + return self._scalar("cactus_graph_scalar_floor_divide", x, value) + + def scalar_not_equal(self, x, value): + return self._scalar("cactus_graph_scalar_not_equal", x, value) + + def scalar_exp(self, x): + return self._scalar("cactus_graph_scalar_exp", x) + + def scalar_sqrt(self, x): + return self._scalar("cactus_graph_scalar_sqrt", x) + + def scalar_cos(self, x): + return self._scalar("cactus_graph_scalar_cos", x) + + def scalar_sin(self, x): + return self._scalar("cactus_graph_scalar_sin", x) + + def scalar_log(self, x): + return self._scalar("cactus_graph_scalar_log", x) + + def clamp(self, x, lo, hi): + x = self._ensure_tensor(x) + out = cactus_node_t() + rc = _lib.cactus_graph_clamp( + self.h, + cactus_node_t(x.id), + ctypes.c_float(float(lo)), + ctypes.c_float(float(hi)), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_clamp failed")) + return self._tensor_from_node(out.value) + + def masked_select_prefix(self, x, mask): + return self._binary("cactus_graph_masked_select_prefix", x, mask) + + def masked_scatter(self, x, mask, source): + x = self._ensure_tensor(x) + mask = self._ensure_tensor(mask) + source = self._ensure_tensor(source) + out = cactus_node_t() + rc = _lib.cactus_graph_masked_scatter( + self.h, + cactus_node_t(x.id), + cactus_node_t(mask.id), + cactus_node_t(source.id), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_masked_scatter failed")) + return self._tensor_from_node(out.value) + + def view(self, x, shape): + x = self._ensure_tensor(x) + shape = tuple(int(v) for v in shape) + arr = (ctypes.c_size_t * len(shape))(*shape) + out = cactus_node_t() + rc = _lib.cactus_graph_view(self.h, cactus_node_t(x.id), arr, len(shape), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(_err("graph_view failed")) + return self._tensor_from_node(out.value) + + def reshape(self, x, shape): + x = self._ensure_tensor(x) + shape = tuple(int(v) for v in shape) + arr = (ctypes.c_size_t * len(shape))(*shape) + out = cactus_node_t() + rc = _lib.cactus_graph_reshape(self.h, cactus_node_t(x.id), arr, len(shape), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(_err("graph_reshape failed")) + return self._tensor_from_node(out.value) + + def expand(self, x, shape): + x = self._ensure_tensor(x) + shape = tuple(int(v) for v in shape) + arr = (ctypes.c_size_t * len(shape))(*shape) + out = cactus_node_t() + rc = _lib.cactus_graph_expand(self.h, cactus_node_t(x.id), arr, len(shape), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(_err("graph_expand failed")) + return self._tensor_from_node(out.value) + + def flatten(self, x, start_dim=0, end_dim=-1): + x = self._ensure_tensor(x) + out = cactus_node_t() + rc = _lib.cactus_graph_flatten( + self.h, + cactus_node_t(x.id), + ctypes.c_int32(int(start_dim)), + ctypes.c_int32(int(end_dim)), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_flatten failed")) + return self._tensor_from_node(out.value) + + def slice(self, x, axis, start, length=0): + x = self._ensure_tensor(x) + out = cactus_node_t() + rc = _lib.cactus_graph_slice( + self.h, + cactus_node_t(x.id), + ctypes.c_int32(int(axis)), + ctypes.c_size_t(int(start)), + ctypes.c_size_t(int(length)), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_slice failed")) + return self._tensor_from_node(out.value) + + def index(self, x, index_value, axis=0): + x = self._ensure_tensor(x) + out = cactus_node_t() + rc = _lib.cactus_graph_index( + self.h, + cactus_node_t(x.id), + ctypes.c_size_t(int(index_value)), + ctypes.c_int32(int(axis)), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_index failed")) + return self._tensor_from_node(out.value) + + def transpose(self, x, backend=CPU): + x = self._ensure_tensor(x) + out = cactus_node_t() + rc = _lib.cactus_graph_transpose( + self.h, + cactus_node_t(x.id), + ctypes.c_int32(int(backend)), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_transpose failed")) + return self._tensor_from_node(out.value) + + def permute(self, x, permutation, backend=CPU): + x = self._ensure_tensor(x) + permutation = tuple(int(v) for v in permutation) + arr = (ctypes.c_size_t * len(permutation))(*permutation) + out = cactus_node_t() + rc = _lib.cactus_graph_transpose_n( + self.h, + cactus_node_t(x.id), + arr, + len(permutation), + ctypes.c_int32(int(backend)), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_transpose_n failed")) + return self._tensor_from_node(out.value) + + def matmul(self, a, b, pretransposed_rhs=False, backend=CPU, output_dtype=None): + a = self._ensure_tensor(a) + b = self._ensure_tensor(b) + out = cactus_node_t() + rc = _lib.cactus_graph_matmul( + self.h, + cactus_node_t(a.id), + cactus_node_t(b.id), + ctypes.c_bool(bool(pretransposed_rhs)), + ctypes.c_int32(int(backend)), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_matmul failed")) + return self._tensor_from_node(out.value) + + def gather(self, tensor, indices): + tensor = self._ensure_tensor(tensor) + indices = self._ensure_tensor(indices) + out = cactus_node_t() + rc = _lib.cactus_graph_gather( + self.h, + cactus_node_t(tensor.id), + cactus_node_t(indices.id), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_gather failed")) + return self._tensor_from_node(out.value) + + def embedding_from_tensor(self, embedding_tensor, indices): + embedding_tensor = self._ensure_tensor(embedding_tensor) + indices = self._ensure_tensor(indices) + out = cactus_node_t() + rc = _lib.cactus_graph_embedding_from_tensor( + self.h, cactus_node_t(embedding_tensor.id), cactus_node_t(indices.id), ctypes.byref(out) + ) + if rc != 0: + raise RuntimeError(_err("graph_embedding_from_tensor failed")) + return self._tensor_from_node(out.value) + + def embedding_from_file(self, filename, indices): + indices = self._ensure_tensor(indices) + out = cactus_node_t() + rc = _lib.cactus_graph_embedding_from_file(self.h, str(filename).encode(), cactus_node_t(indices.id), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(_err("graph_embedding_from_file failed")) + return self._tensor_from_node(out.value) + + def mmap_embeddings(self, filename): + out = cactus_node_t() + rc = _lib.cactus_graph_mmap_embeddings(self.h, str(filename).encode(), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(_err("graph_mmap_embeddings failed")) + return self._tensor_from_node(out.value) + + def mmap_weights(self, filename): + out = cactus_node_t() + rc = _lib.cactus_graph_mmap_weights(self.h, str(filename).encode(), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(_err("graph_mmap_weights failed")) + return self._tensor_from_node(out.value) + + def bind_mmap_weights(self, tensor, filename): + tensor = self._ensure_tensor(tensor) + rc = _lib.cactus_graph_bind_mmap_weights(self.h, cactus_node_t(tensor.id), str(filename).encode()) + if rc != 0: + raise RuntimeError(_err("graph_bind_mmap_weights failed")) + + def bilinear_interpolation(self, pos_embeds, dst_height, dst_width): + pos_embeds = self._ensure_tensor(pos_embeds) + out = cactus_node_t() + rc = _lib.cactus_graph_bilinear_interpolation( + self.h, cactus_node_t(pos_embeds.id), ctypes.c_size_t(int(dst_height)), ctypes.c_size_t(int(dst_width)), ctypes.byref(out) + ) + if rc != 0: + raise RuntimeError(_err("graph_bilinear_interpolation failed")) + return self._tensor_from_node(out.value) + + def set_grouped_scales(self, tensor, group_size, num_groups, scales): + tensor = self._ensure_tensor(tensor) + arr = np.ascontiguousarray(scales, dtype=np.float16) + rc = _lib.cactus_graph_set_grouped_scales( + self.h, + cactus_node_t(tensor.id), + ctypes.c_size_t(int(group_size)), + ctypes.c_size_t(int(num_groups)), + arr.ctypes.data_as(ctypes.c_void_p), + ) + if rc != 0: + raise RuntimeError(_err("graph_set_grouped_scales failed")) + + def set_interleaved(self, tensor, interleaved=True, original_n=0): + tensor = self._ensure_tensor(tensor) + rc = _lib.cactus_graph_set_interleaved( + self.h, cactus_node_t(tensor.id), ctypes.c_bool(bool(interleaved)), ctypes.c_size_t(int(original_n)) + ) + if rc != 0: + raise RuntimeError(_err("graph_set_interleaved failed")) + + def release_weight_pages(self, tensor): + tensor = self._ensure_tensor(tensor) + rc = _lib.cactus_graph_release_weight_pages(self.h, cactus_node_t(tensor.id)) + if rc != 0: + raise RuntimeError(_err("graph_release_weight_pages failed")) + + def prefetch_weight_pages(self, tensor): + tensor = self._ensure_tensor(tensor) + rc = _lib.cactus_graph_prefetch_weight_pages(self.h, cactus_node_t(tensor.id)) + if rc != 0: + raise RuntimeError(_err("graph_prefetch_weight_pages failed")) + + def release_all_weight_pages(self): + rc = _lib.cactus_graph_release_all_weight_pages(self.h) + if rc != 0: + raise RuntimeError(_err("graph_release_all_weight_pages failed")) + + def concat(self, a, b, axis=0): + a = self._ensure_tensor(a) + b = self._ensure_tensor(b) + out = cactus_node_t() + rc = _lib.cactus_graph_concat( + self.h, + cactus_node_t(a.id), + cactus_node_t(b.id), + ctypes.c_int32(int(axis)), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_concat failed")) + return self._tensor_from_node(out.value) + + def cat(self, tensors, axis=0): + tensors = [self._ensure_tensor(t) for t in tensors] + if not tensors: + raise ValueError("cat requires at least one tensor") + if len(tensors) == 1: + return tensors[0] + ids = (cactus_node_t * len(tensors))(*(cactus_node_t(t.id) for t in tensors)) + out = cactus_node_t() + rc = _lib.cactus_graph_cat( + self.h, ids, ctypes.c_size_t(len(tensors)), + ctypes.c_int32(int(axis)), ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_cat failed")) + return self._tensor_from_node(out.value) + + def groupnorm(self, x, weight, bias, num_groups, eps=1e-5): + x = self._ensure_tensor(x) + weight = self._ensure_tensor(weight) + bias = self._ensure_tensor(bias) + out = cactus_node_t() + rc = _lib.cactus_graph_groupnorm( + self.h, + cactus_node_t(x.id), + cactus_node_t(weight.id), + cactus_node_t(bias.id), + ctypes.c_size_t(int(num_groups)), + ctypes.c_float(float(eps)), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_group_norm failed")) + return self._tensor_from_node(out.value) + + def group_norm(self, x, weight, bias, num_groups, eps=1e-5): + return self.groupnorm(x, weight, bias, num_groups, eps=eps) + + def layernorm(self, x, weight, bias=None, eps=1e-5): + x = self._ensure_tensor(x) + weight = self._ensure_tensor(weight) + has_bias = bias is not None + bias_node = cactus_node_t(0) + if has_bias: + bias = self._ensure_tensor(bias) + bias_node = cactus_node_t(bias.id) + out = cactus_node_t() + rc = _lib.cactus_graph_layernorm( + self.h, + cactus_node_t(x.id), + cactus_node_t(weight.id), + bias_node, + ctypes.c_float(float(eps)), + ctypes.c_bool(has_bias), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_layer_norm failed")) + return self._tensor_from_node(out.value) + + def layer_norm(self, x, weight, bias=None, eps=1e-5): + return self.layernorm(x, weight, bias=bias, eps=eps) + + def batchnorm(self, x, weight, bias, running_mean, running_var, axis=1, eps=1e-5): + x = self._ensure_tensor(x) + weight = self._ensure_tensor(weight) + bias = self._ensure_tensor(bias) + running_mean = self._ensure_tensor(running_mean) + running_var = self._ensure_tensor(running_var) + out = cactus_node_t() + rc = _lib.cactus_graph_batchnorm( + self.h, + cactus_node_t(x.id), + cactus_node_t(weight.id), + cactus_node_t(bias.id), + cactus_node_t(running_mean.id), + cactus_node_t(running_var.id), + ctypes.c_int32(int(axis)), + ctypes.c_float(float(eps)), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_batchnorm failed")) + return self._tensor_from_node(out.value) + + def batch_norm(self, x, weight, bias, running_mean, running_var, axis=1, eps=1e-5): + return self.batchnorm(x, weight, bias, running_mean, running_var, axis=axis, eps=eps) + + def rms_norm(self, x, weight, eps=1e-5): + x = self._ensure_tensor(x) + weight = self._ensure_tensor(weight) + out = cactus_node_t() + rc = _lib.cactus_graph_rms_norm( + self.h, + cactus_node_t(x.id), + cactus_node_t(weight.id), + ctypes.c_float(float(eps)), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_rms_norm failed")) + return self._tensor_from_node(out.value) + + def topk(self, x, k): + x = self._ensure_tensor(x) + out = cactus_node_t() + rc = _lib.cactus_graph_topk(self.h, cactus_node_t(x.id), ctypes.c_size_t(int(k)), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(_err("graph_topk failed")) + return self._tensor_from_node(out.value) + + def rope(self, x, theta, position_offset=0, backend=CPU): + x = self._ensure_tensor(x) + out = cactus_node_t() + rc = _lib.cactus_graph_rope( + self.h, cactus_node_t(x.id), ctypes.c_float(float(theta)), ctypes.c_size_t(int(position_offset)), + ctypes.c_int32(int(backend)), ctypes.byref(out) + ) + if rc != 0: + raise RuntimeError(_err("graph_rope failed")) + return self._tensor_from_node(out.value) + + def rope_gptj(self, x, theta, position_offset=0, rot_dim=0, backend=CPU): + x = self._ensure_tensor(x) + out = cactus_node_t() + rc = _lib.cactus_graph_rope_gptj( + self.h, cactus_node_t(x.id), ctypes.c_float(float(theta)), ctypes.c_size_t(int(position_offset)), + ctypes.c_size_t(int(rot_dim)), ctypes.c_int32(int(backend)), ctypes.byref(out) + ) + if rc != 0: + raise RuntimeError(_err("graph_rope_gptj failed")) + return self._tensor_from_node(out.value) + + def _reduce(self, fn_name, x, axis): + x = self._ensure_tensor(x) + out = cactus_node_t() + rc = getattr(_lib, fn_name)(self.h, cactus_node_t(x.id), ctypes.c_int32(int(axis)), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(f"{fn_name} failed") + return self._tensor_from_node(out.value) + + def sum(self, x, axis): + return self._reduce("cactus_graph_sum", x, axis) + + def mean(self, x, axis): + return self._reduce("cactus_graph_mean", x, axis) + + def variance(self, x, axis): + return self._reduce("cactus_graph_variance", x, axis) + + def min(self, x, axis): + return self._reduce("cactus_graph_min", x, axis) + + def max(self, x, axis): + return self._reduce("cactus_graph_max", x, axis) + + def cumsum(self, x, axis): + return self._reduce("cactus_graph_cumsum", x, axis) + + def softmax(self, x, axis=-1): + x = self._ensure_tensor(x) + out = cactus_node_t() + rc = _lib.cactus_graph_softmax( + self.h, + cactus_node_t(x.id), + ctypes.c_int32(int(axis)), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_softmax failed")) + return self._tensor_from_node(out.value) + + def attention(self, query, key, value, scale, is_causal=True, position_offset=0, window_size=0, + backend=CPU, mask=None, additive_mask=False): + query = self._ensure_tensor(query) + key = self._ensure_tensor(key) + value = self._ensure_tensor(value) + mask_node = cactus_node_t(0) + use_mask = mask is not None + if use_mask: + mask_node = cactus_node_t(self._ensure_tensor(mask).id) + out = cactus_node_t() + rc = _lib.cactus_graph_attention( + self.h, + cactus_node_t(query.id), + cactus_node_t(key.id), + cactus_node_t(value.id), + ctypes.c_float(float(scale)), + ctypes.c_bool(bool(is_causal)), + ctypes.c_size_t(int(position_offset)), + ctypes.c_size_t(int(window_size)), + ctypes.c_int32(int(backend)), + ctypes.c_bool(use_mask), + mask_node, + ctypes.c_bool(bool(additive_mask)), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_attention failed")) + return self._tensor_from_node(out.value) + + def kv_cache_state(self, max_seq_len, num_kv_heads, head_dim, window_size=0, sink_size=0): + out = cactus_node_t() + rc = _lib.cactus_graph_kv_cache_state( + self.h, + ctypes.c_size_t(int(max_seq_len)), + ctypes.c_size_t(int(num_kv_heads)), + ctypes.c_size_t(int(head_dim)), + ctypes.c_size_t(int(window_size)), + ctypes.c_size_t(int(sink_size)), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_kv_cache_state failed")) + return self._tensor_from_node(out.value) + + def kv_cache_append(self, new_kv, cache_state, window_size=0, sink_size=0): + new_kv = self._ensure_tensor(new_kv) + cache_state = self._ensure_tensor(cache_state) + out = cactus_node_t() + rc = _lib.cactus_graph_kv_cache_append( + self.h, + cactus_node_t(new_kv.id), + cactus_node_t(cache_state.id), + ctypes.c_size_t(int(window_size)), + ctypes.c_size_t(int(sink_size)), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_kv_cache_append failed")) + return self._tensor_from_node(out.value) + + def attention_cached( + self, + query, + key_new, + value_new, + k_cache_state, + v_cache_state, + scale, + position_offset=0, + window_size=0, + v_head_dim=0, + ): + query = self._ensure_tensor(query) + key_new = self._ensure_tensor(key_new) + value_new = self._ensure_tensor(value_new) + k_cache_state = self._ensure_tensor(k_cache_state) + v_cache_state = self._ensure_tensor(v_cache_state) + out = cactus_node_t() + rc = _lib.cactus_graph_attention_cached( + self.h, + cactus_node_t(query.id), + cactus_node_t(key_new.id), + cactus_node_t(value_new.id), + cactus_node_t(k_cache_state.id), + cactus_node_t(v_cache_state.id), + ctypes.c_float(float(scale)), + ctypes.c_size_t(int(position_offset)), + ctypes.c_size_t(int(window_size)), + ctypes.c_size_t(int(v_head_dim)), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_attention_cached failed")) + return self._tensor_from_node(out.value) + + def conv_cache_state(self, window_size, hidden_dim): + out = cactus_node_t() + rc = _lib.cactus_graph_conv_cache_state( + self.h, + ctypes.c_size_t(int(window_size)), + ctypes.c_size_t(int(hidden_dim)), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_conv_cache_state failed")) + return self._tensor_from_node(out.value) + + def conv_cache_append(self, new_data, cache_state): + new_data = self._ensure_tensor(new_data) + cache_state = self._ensure_tensor(cache_state) + out = cactus_node_t() + rc = _lib.cactus_graph_conv_cache_append( + self.h, + cactus_node_t(new_data.id), + cactus_node_t(cache_state.id), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_conv_cache_append failed")) + return self._tensor_from_node(out.value) + + def conv_cache_initialize(self, rows, cache_state): + rows = self._ensure_tensor(rows) + cache_state = self._ensure_tensor(cache_state) + out = cactus_node_t() + rc = _lib.cactus_graph_conv_cache_initialize( + self.h, + cactus_node_t(rows.id), + cactus_node_t(cache_state.id), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_conv_cache_initialize failed")) + return self._tensor_from_node(out.value) + + def recurrent_cache_state(self, shape, dtype=FP16): + shape = tuple(int(x) for x in shape) + arr = (ctypes.c_size_t * len(shape))(*shape) + out = cactus_node_t() + rc = _lib.cactus_graph_recurrent_cache_state( + self.h, + arr, + ctypes.c_size_t(len(shape)), + ctypes.c_int(int(dtype)), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_recurrent_cache_state failed")) + return self._tensor_from_node(out.value) + + def recurrent_cache_write(self, new_value, cache_input): + new_value = self._ensure_tensor(new_value) + cache_input = self._ensure_tensor(cache_input) + out = cactus_node_t() + rc = _lib.cactus_graph_recurrent_cache_write( + self.h, + cactus_node_t(new_value.id), + cactus_node_t(cache_input.id), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_recurrent_cache_write failed")) + return self._tensor_from_node(out.value) + + def rel_pos_bias(self, query, relative_key, scale): + query = self._ensure_tensor(query) + relative_key = self._ensure_tensor(relative_key) + out = cactus_node_t() + rc = _lib.cactus_graph_rel_pos_bias( + self.h, cactus_node_t(query.id), cactus_node_t(relative_key.id), ctypes.c_float(float(scale)), ctypes.byref(out) + ) + if rc != 0: + raise RuntimeError(_err("graph_rel_pos_bias failed")) + return self._tensor_from_node(out.value) + + def attention_int8_hybrid(self, query, key_new, value_new, scale, position_offset, + cached_keys, cached_values, k_scales, v_scales, + cache_len, num_kv_heads, head_dim, window_size=0): + query = self._ensure_tensor(query) + key_new = self._ensure_tensor(key_new) + value_new = self._ensure_tensor(value_new) + ck = np.ascontiguousarray(cached_keys, dtype=np.int8) + cv = np.ascontiguousarray(cached_values, dtype=np.int8) + ks = np.ascontiguousarray(k_scales, dtype=np.float32) + vs = np.ascontiguousarray(v_scales, dtype=np.float32) + out = cactus_node_t() + rc = _lib.cactus_graph_attention_int8_hybrid( + self.h, + cactus_node_t(query.id), + cactus_node_t(key_new.id), + cactus_node_t(value_new.id), + ctypes.c_float(float(scale)), + ctypes.c_size_t(int(position_offset)), + ck.ctypes.data_as(ctypes.POINTER(ctypes.c_int8)), + cv.ctypes.data_as(ctypes.POINTER(ctypes.c_int8)), + ks.ctypes.data_as(ctypes.POINTER(ctypes.c_float)), + vs.ctypes.data_as(ctypes.POINTER(ctypes.c_float)), + ctypes.c_size_t(int(cache_len)), + ctypes.c_size_t(int(num_kv_heads)), + ctypes.c_size_t(int(head_dim)), + ctypes.c_size_t(int(window_size)), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError("graph_attention_int8_hybrid failed") + return self._tensor_from_node(out.value) + + def relu(self, x): + x = self._ensure_tensor(x) + out = cactus_node_t() + rc = _lib.cactus_graph_relu(self.h, cactus_node_t(x.id), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(_err("graph_relu failed")) + return self._tensor_from_node(out.value) + + def silu(self, x): + x = self._ensure_tensor(x) + out = cactus_node_t() + rc = _lib.cactus_graph_silu(self.h, cactus_node_t(x.id), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(_err("graph_silu failed")) + return self._tensor_from_node(out.value) + + def gelu(self, x): + x = self._ensure_tensor(x) + out = cactus_node_t() + rc = _lib.cactus_graph_gelu(self.h, cactus_node_t(x.id), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(_err("graph_gelu failed")) + return self._tensor_from_node(out.value) + + def gelu_erf(self, x): + x = self._ensure_tensor(x) + out = cactus_node_t() + rc = _lib.cactus_graph_gelu_erf(self.h, cactus_node_t(x.id), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(_err("graph_gelu_erf failed")) + return self._tensor_from_node(out.value) + + def sigmoid(self, x): + x = self._ensure_tensor(x) + out = cactus_node_t() + rc = _lib.cactus_graph_sigmoid(self.h, cactus_node_t(x.id), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(_err("graph_sigmoid failed")) + return self._tensor_from_node(out.value) + + def tanh(self, x): + x = self._ensure_tensor(x) + out = cactus_node_t() + rc = _lib.cactus_graph_tanh(self.h, cactus_node_t(x.id), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(_err("graph_tanh failed")) + return self._tensor_from_node(out.value) + + def glu(self, x, axis=-1): + x = self._ensure_tensor(x) + out = cactus_node_t() + rc = _lib.cactus_graph_glu(self.h, cactus_node_t(x.id), ctypes.c_int32(int(axis)), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(_err("graph_glu failed")) + return self._tensor_from_node(out.value) + + def conv1d_causal(self, x, weight, kernel_size, dilation): + x = self._ensure_tensor(x) + weight = self._ensure_tensor(weight) + out = cactus_node_t() + rc = _lib.cactus_graph_conv1d_causal( + self.h, cactus_node_t(x.id), cactus_node_t(weight.id), + ctypes.c_size_t(int(kernel_size)), ctypes.c_size_t(int(dilation)), ctypes.byref(out) + ) + if rc != 0: + raise RuntimeError("graph_conv1d_causal failed") + return self._tensor_from_node(out.value) + + def conv1d_k3(self, x, weight, stride=1): + x = self._ensure_tensor(x) + weight = self._ensure_tensor(weight) + out = cactus_node_t() + rc = _lib.cactus_graph_conv1d_k3( + self.h, cactus_node_t(x.id), cactus_node_t(weight.id), ctypes.c_size_t(int(stride)), ctypes.byref(out) + ) + if rc != 0: + raise RuntimeError("graph_conv1d_k3 failed") + return self._tensor_from_node(out.value) + + def conv1d_k7s3(self, x, weight, bias): + x = self._ensure_tensor(x) + weight = self._ensure_tensor(weight) + bias = self._ensure_tensor(bias) + out = cactus_node_t() + rc = _lib.cactus_graph_conv1d_k7s3( + self.h, cactus_node_t(x.id), cactus_node_t(weight.id), cactus_node_t(bias.id), ctypes.byref(out) + ) + if rc != 0: + raise RuntimeError("graph_conv1d_k7s3 failed") + return self._tensor_from_node(out.value) + + def conv1d(self, x, weight, bias=None, stride=1): + return self._conv_with_optional_bias("cactus_graph_conv1d", x, weight, bias, ctypes.c_size_t(int(stride))) + + def conv1d_same_depthwise_k9(self, x, weight, bias=None): + return self._conv_with_optional_bias("cactus_graph_conv1d_same_depthwise_k9", x, weight, bias) + + def conv1d_pointwise(self, x, weight, bias=None): + return self._conv_with_optional_bias("cactus_graph_conv1d_pointwise", x, weight, bias) + + def conv2d_k3s2p1(self, x, weight, bias=None): + return self._conv_with_optional_bias("cactus_graph_conv2d_k3s2p1", x, weight, bias) + + def conv2d_depthwise_k3s2p1(self, x, weight, bias=None): + return self._conv_with_optional_bias("cactus_graph_conv2d_depthwise_k3s2p1", x, weight, bias) + + def conv2d_pointwise_1x1(self, x, weight, bias=None): + return self._conv_with_optional_bias("cactus_graph_conv2d_pointwise_1x1", x, weight, bias) + + def conv2d_k3s1p1(self, x, weight, bias=None): + return self._conv_with_optional_bias("cactus_graph_conv2d_k3s1p1", x, weight, bias) + + def conv2d(self, x, weight, bias=None, stride=1, padding=0, dilation=1, groups=1): + return self._conv_with_optional_bias( + "cactus_graph_conv2d", + x, + weight, + bias, + ctypes.c_size_t(int(stride)), + ctypes.c_size_t(int(padding)), + ctypes.c_size_t(int(dilation)), + ctypes.c_size_t(int(groups)), + ) + + def _conv_with_optional_bias(self, fn_name, x, weight, bias=None, *extra): + x = self._ensure_tensor(x) + weight = self._ensure_tensor(weight) + has_bias = bias is not None + bias_node = cactus_node_t(0 if bias is None else self._ensure_tensor(bias).id) + out = cactus_node_t() + rc = getattr(_lib, fn_name)( + self.h, cactus_node_t(x.id), cactus_node_t(weight.id), ctypes.c_bool(has_bias), bias_node, *extra, ctypes.byref(out) + ) + if rc != 0: + raise RuntimeError(f"{fn_name} failed") + return self._tensor_from_node(out.value) + + def lstm_cell(self, input, h_prev, c_prev, weight_ih, weight_hh, bias_ih, bias_hh): + tensors = [self._ensure_tensor(t) for t in (input, h_prev, c_prev, weight_ih, weight_hh, bias_ih, bias_hh)] + out = cactus_node_t() + rc = _lib.cactus_graph_lstm_cell(self.h, *(cactus_node_t(t.id) for t in tensors), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(_err("graph_lstm_cell failed")) + return self._tensor_from_node(out.value) + + def gated_deltanet_decode(self, query, key, value, gate_log, beta, initial_state, scale): + tensors = [self._ensure_tensor(t) for t in (query, key, value, gate_log, beta, initial_state)] + out = cactus_node_t() + rc = _lib.cactus_graph_gated_deltanet_decode( + self.h, *(cactus_node_t(t.id) for t in tensors), ctypes.c_float(float(scale)), ctypes.byref(out) + ) + if rc != 0: + raise RuntimeError(_err("graph_gated_deltanet_decode failed")) + return self._tensor_from_node(out.value) + + def gated_deltanet_prefill(self, query, key, value, gate_log, beta, initial_state, chunk_size, scale): + tensors = [self._ensure_tensor(t) for t in (query, key, value, gate_log, beta, initial_state)] + out = cactus_node_t() + rc = _lib.cactus_graph_gated_deltanet_prefill( + self.h, *(cactus_node_t(t.id) for t in tensors), ctypes.c_size_t(int(chunk_size)), ctypes.c_float(float(scale)), ctypes.byref(out) + ) + if rc != 0: + raise RuntimeError(_err("graph_gated_deltanet_prefill failed")) + return self._tensor_from_node(out.value) + + def stft(self, x, weight, stride, num_fft_bins): + x = self._ensure_tensor(x) + weight = self._ensure_tensor(weight) + out = cactus_node_t() + rc = _lib.cactus_graph_stft( + self.h, cactus_node_t(x.id), cactus_node_t(weight.id), + ctypes.c_size_t(int(stride)), ctypes.c_size_t(int(num_fft_bins)), ctypes.byref(out) + ) + if rc != 0: + raise RuntimeError(_err("graph_stft failed")) + return self._tensor_from_node(out.value) + + def altup_predict(self, coefs, streams): + coefs = self._ensure_tensor(coefs) + streams = [self._ensure_tensor(t) for t in streams] + ids = (cactus_node_t * len(streams))(*(cactus_node_t(t.id) for t in streams)) + out = cactus_node_t() + rc = _lib.cactus_graph_altup_predict(self.h, cactus_node_t(coefs.id), ids, ctypes.c_size_t(len(streams)), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(_err("graph_altup_predict failed")) + return self._tensor_from_node(out.value) + + def altup_correct(self, coefs, innovation, predictions): + coefs = self._ensure_tensor(coefs) + innovation = self._ensure_tensor(innovation) + predictions = [self._ensure_tensor(t) for t in predictions] + ids = (cactus_node_t * len(predictions))(*(cactus_node_t(t.id) for t in predictions)) + out = cactus_node_t() + rc = _lib.cactus_graph_altup_correct( + self.h, cactus_node_t(coefs.id), cactus_node_t(innovation.id), ids, ctypes.c_size_t(len(predictions)), ctypes.byref(out) + ) + if rc != 0: + raise RuntimeError(_err("graph_altup_correct failed")) + return self._tensor_from_node(out.value) + + def gaussian_topk(self, x, ppf): + x = self._ensure_tensor(x) + out = cactus_node_t() + rc = _lib.cactus_graph_gaussian_topk(self.h, cactus_node_t(x.id), ctypes.c_float(float(ppf)), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(_err("graph_gaussian_topk failed")) + return self._tensor_from_node(out.value) + + def moe_layer_gated(self, hidden, routing_probs, topk_indices, w1_weights, w3_weights, w2_weights, + num_experts, num_experts_per_tok, normalize_routing=True, epsilon=1e-6, routed_scaling_factor=1.0): + hidden = self._ensure_tensor(hidden) + routing_probs = self._ensure_tensor(routing_probs) + topk_indices = self._ensure_tensor(topk_indices) + w1 = (cactus_node_t * len(w1_weights))(*(cactus_node_t(self._ensure_tensor(t).id) for t in w1_weights)) + w3 = (cactus_node_t * len(w3_weights))(*(cactus_node_t(self._ensure_tensor(t).id) for t in w3_weights)) + w2 = (cactus_node_t * len(w2_weights))(*(cactus_node_t(self._ensure_tensor(t).id) for t in w2_weights)) + out = cactus_node_t() + rc = _lib.cactus_graph_moe_layer_gated( + self.h, cactus_node_t(hidden.id), cactus_node_t(routing_probs.id), cactus_node_t(topk_indices.id), + w1, w3, w2, ctypes.c_size_t(int(num_experts)), ctypes.c_size_t(int(num_experts_per_tok)), + ctypes.c_bool(bool(normalize_routing)), ctypes.c_float(float(epsilon)), + ctypes.c_float(float(routed_scaling_factor)), ctypes.byref(out) + ) + if rc != 0: + raise RuntimeError(_err("graph_moe_layer_gated failed")) + return self._tensor_from_node(out.value) + + def dense_mlp_tq_fused(self, hidden, gate_weight, up_weight, down_weight, product_scale=1.0): + hidden = self._ensure_tensor(hidden) + gate_weight = self._ensure_tensor(gate_weight) + up_weight = self._ensure_tensor(up_weight) + down_weight = self._ensure_tensor(down_weight) + out = cactus_node_t() + rc = _lib.cactus_graph_dense_mlp_tq_fused( + self.h, + cactus_node_t(hidden.id), + cactus_node_t(gate_weight.id), + cactus_node_t(up_weight.id), + cactus_node_t(down_weight.id), + ctypes.c_float(float(product_scale)), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_dense_mlp_tq_fused failed")) + return self._tensor_from_node(out.value) + + def moe_layer_ungated(self, hidden, routing_probs, topk_indices, w1_weights, w2_weights, + num_experts, num_experts_per_tok, normalize_routing=True, epsilon=1e-6, + routed_scaling_factor=1.0, activation=ACT_GELU): + hidden = self._ensure_tensor(hidden) + routing_probs = self._ensure_tensor(routing_probs) + topk_indices = self._ensure_tensor(topk_indices) + w1 = (cactus_node_t * len(w1_weights))(*(cactus_node_t(self._ensure_tensor(t).id) for t in w1_weights)) + w2 = (cactus_node_t * len(w2_weights))(*(cactus_node_t(self._ensure_tensor(t).id) for t in w2_weights)) + out = cactus_node_t() + rc = _lib.cactus_graph_moe_layer_ungated( + self.h, cactus_node_t(hidden.id), cactus_node_t(routing_probs.id), cactus_node_t(topk_indices.id), + w1, w2, ctypes.c_size_t(int(num_experts)), ctypes.c_size_t(int(num_experts_per_tok)), + ctypes.c_bool(bool(normalize_routing)), ctypes.c_float(float(epsilon)), + ctypes.c_float(float(routed_scaling_factor)), ctypes.c_int32(int(activation)), ctypes.byref(out) + ) + if rc != 0: + raise RuntimeError(_err("graph_moe_layer_ungated failed")) + return self._tensor_from_node(out.value) + + def sample(self, logits, temperature=0.6, top_p=0.95, top_k=20): + logits = self._ensure_tensor(logits) + out = cactus_node_t() + rc = _lib.cactus_graph_sample( + self.h, cactus_node_t(logits.id), ctypes.c_float(float(temperature)), + ctypes.c_float(float(top_p)), ctypes.c_size_t(int(top_k)), ctypes.byref(out) + ) + if rc != 0: + raise RuntimeError(_err("graph_sample failed")) + return self._tensor_from_node(out.value) + + def scatter_topk(self, indices, values, num_classes): + indices = self._ensure_tensor(indices) + values = self._ensure_tensor(values) + out = cactus_node_t() + rc = _lib.cactus_graph_scatter_topk( + self.h, cactus_node_t(indices.id), cactus_node_t(values.id), ctypes.c_size_t(int(num_classes)), ctypes.byref(out) + ) + if rc != 0: + raise RuntimeError(_err("graph_scatter_topk failed")) + return self._tensor_from_node(out.value) + + def persistent(self, source_node): + source_node = self._ensure_tensor(source_node) + out = cactus_node_t() + rc = _lib.cactus_graph_persistent(self.h, cactus_node_t(source_node.id), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(_err("graph_persistent failed")) + return self._tensor_from_node(out.value) + + def is_populated(self, persistent_node): + persistent_node = self._ensure_tensor(persistent_node) + out_is_populated = ctypes.c_int32() + rc = _lib.cactus_graph_is_populated(self.h, cactus_node_t(persistent_node.id), ctypes.byref(out_is_populated)) + if rc != 0: + raise RuntimeError(_err("graph_is_populated failed")) + return bool(out_is_populated.value) + + def invalidate_persistent(self, persistent_node): + persistent_node = self._ensure_tensor(persistent_node) + rc = _lib.cactus_graph_invalidate_persistent(self.h, cactus_node_t(persistent_node.id)) + if rc != 0: + raise RuntimeError(_err("graph_invalidate_persistent failed")) + + # ── Audio / signal processing ─────────────────────────────────── + + def rfft(self, x): + """Real-to-complex FFT.""" + x = self._ensure_tensor(x) + out = cactus_node_t() + rc = _lib.cactus_graph_rfft(self.h, cactus_node_t(x.id), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(_err("graph_rfft failed")) + return self._tensor_from_node(out.value) + + def irfft(self, x, output_length): + """Complex-to-real inverse FFT.""" + x = self._ensure_tensor(x) + out = cactus_node_t() + rc = _lib.cactus_graph_irfft( + self.h, cactus_node_t(x.id), + ctypes.c_size_t(int(output_length)), ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_irfft failed")) + return self._tensor_from_node(out.value) + + def mel_filter_bank(self, num_frequency_bins, num_mel_filters, + min_frequency, max_frequency, sampling_rate, + norm_type=0, scale_type=0): + """Generate a mel-scale filter bank tensor.""" + out = cactus_node_t() + rc = _lib.cactus_graph_mel_filter_bank( + self.h, + ctypes.c_size_t(int(num_frequency_bins)), + ctypes.c_size_t(int(num_mel_filters)), + ctypes.c_float(float(min_frequency)), + ctypes.c_float(float(max_frequency)), + ctypes.c_size_t(int(sampling_rate)), + ctypes.c_int(int(norm_type)), + ctypes.c_int(int(scale_type)), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_mel_filter_bank failed")) + return self._tensor_from_node(out.value) + + def spectrogram(self, waveform, mel_filters, frame_length, hop_length, + fft_length, power=2.0, center=True, pad_mode=0, + mel_floor=1e-10, log_mel_mode=0, dither=0.0, + preemphasis=0.0, remove_dc_offset=False): + """Compute a spectrogram from a waveform tensor.""" + waveform = self._ensure_tensor(waveform) + mel_filters = self._ensure_tensor(mel_filters) + out = cactus_node_t() + rc = _lib.cactus_graph_spectrogram( + self.h, + cactus_node_t(waveform.id), + cactus_node_t(mel_filters.id), + ctypes.c_size_t(int(frame_length)), + ctypes.c_size_t(int(hop_length)), + ctypes.c_size_t(int(fft_length)), + ctypes.c_float(float(power)), + ctypes.c_bool(bool(center)), + ctypes.c_int(int(pad_mode)), + ctypes.c_float(float(mel_floor)), + ctypes.c_int(int(log_mel_mode)), + ctypes.c_float(float(dither)), + ctypes.c_float(float(preemphasis)), + ctypes.c_bool(bool(remove_dc_offset)), + ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_spectrogram failed")) + return self._tensor_from_node(out.value) + + # ── Image processing ───────────────────────────────────────────── + + def image_preprocess(self, pixel_input, src_width, src_height, + target_width, target_height, patch_size, channels, + rescale_factor, mean, std_dev): + """Resize, normalize, and patch an image tensor. + + *mean* and *std_dev* are per-channel float sequences (length = channels). + """ + pixel_input = self._ensure_tensor(pixel_input) + mean_arr = (ctypes.c_float * len(mean))(*[float(v) for v in mean]) + std_arr = (ctypes.c_float * len(std_dev))(*[float(v) for v in std_dev]) + out = cactus_node_t() + rc = _lib.cactus_graph_image_preprocess( + self.h, cactus_node_t(pixel_input.id), + ctypes.c_int(int(src_width)), + ctypes.c_int(int(src_height)), + ctypes.c_int(int(target_width)), + ctypes.c_int(int(target_height)), + ctypes.c_int(int(patch_size)), + ctypes.c_int(int(channels)), + ctypes.c_float(float(rescale_factor)), + mean_arr, std_arr, ctypes.byref(out), + ) + if rc != 0: + raise RuntimeError(_err("graph_image_preprocess failed")) + return self._tensor_from_node(out.value) + + # ── Introspection ──────────────────────────────────────────────── + + def output_info(self, x): + x = self._ensure_tensor(x) + return self._get_output_info(x.id) + + def _binary(self, fn_name, a, b): + a = self._ensure_tensor(a) + b = self._ensure_tensor(b) + out = cactus_node_t() + rc = getattr(_lib, fn_name)(self.h, cactus_node_t(a.id), cactus_node_t(b.id), ctypes.byref(out)) + if rc != 0: + raise RuntimeError(f"{fn_name} failed") + return self._tensor_from_node(out.value) + + def _ensure_tensor(self, x): + if not isinstance(x, Tensor): + raise TypeError("expected Tensor") + if x.g is not self: + raise ValueError("tensor belongs to a different graph") + return x + + def _get_output_info(self, node_id): + info = cactus_tensor_info_t() + rc = _lib.cactus_graph_get_output_info(self.h, cactus_node_t(node_id), ctypes.byref(info)) + if rc != 0: + raise RuntimeError(_err("graph_get_output_info failed")) + shape = tuple(int(info.shape[i]) for i in range(int(info.rank))) + return { + "precision": int(info.precision), + "rank": int(info.rank), + "shape": shape, + "num_elements": int(info.num_elements), + "byte_size": int(info.byte_size), + } + + def _tensor_from_node(self, node_id): + meta = self._get_output_info(node_id) + return Tensor(self, int(node_id), meta["shape"], meta["precision"]) + + def _coerce_input_array(self, data, precision): + if isinstance(data, Tensor): + arr = data.numpy() + else: + arr = np.asarray(data) + if precision == self.INT8: + arr = np.ascontiguousarray(arr, dtype=np.int8) + elif precision == self.FP16: + arr = np.ascontiguousarray(arr, dtype=np.float16) + elif precision == self.FP32: + arr = np.ascontiguousarray(arr, dtype=np.float32) + elif precision in (self.CQ1, self.CQ2, self.CQ3, self.CQ4): + arr = np.ascontiguousarray(arr, dtype=np.uint8) + else: + raise RuntimeError("unsupported precision") + return arr + + +class Tensor: + def __init__(self, g, node_id, shape, dtype): + self.g = g + self.id = int(node_id) + self.shape = tuple(shape) + self.dtype = int(dtype) + + def __add__(self, other): + return self.g.add(self, other) + + def __sub__(self, other): + return self.g.subtract(self, other) + + def __mul__(self, other): + return self.g.multiply(self, other) + + def __truediv__(self, other): + return self.g.divide(self, other) + + def __ne__(self, other): + if isinstance(other, Tensor): + return self.g.not_equal(self, other) + return self.g.scalar_not_equal(self, other) + + def abs(self): + return self.g.abs(self) + + def pow(self, exponent): + return self.g.pow(self, exponent) + + def precision_cast(self, dtype): + return self.g.precision_cast(self, dtype) + + def quantize_activations(self): + return self.g.quantize_activations(self) + + def scalar_add(self, value): + return self.g.scalar_add(self, value) + + def scalar_subtract(self, value): + return self.g.scalar_subtract(self, value) + + def scalar_multiply(self, value): + return self.g.scalar_multiply(self, value) + + def scalar_divide(self, value): + return self.g.scalar_divide(self, value) + + def scalar_floor_divide(self, value): + return self.g.scalar_floor_divide(self, value) + + def scalar_not_equal(self, value): + return self.g.scalar_not_equal(self, value) + + def scalar_exp(self): + return self.g.scalar_exp(self) + + def scalar_sqrt(self): + return self.g.scalar_sqrt(self) + + def scalar_cos(self): + return self.g.scalar_cos(self) + + def scalar_sin(self): + return self.g.scalar_sin(self) + + def scalar_log(self): + return self.g.scalar_log(self) + + def clamp(self, lo, hi): + return self.g.clamp(self, lo, hi) + + def relu(self): + return self.g.relu(self) + + def sigmoid(self): + return self.g.sigmoid(self) + + def tanh(self): + return self.g.tanh(self) + + def gelu(self): + return self.g.gelu(self) + + def gelu_erf(self): + return self.g.gelu_erf(self) + + def silu(self): + return self.g.silu(self) + + def view(self, shape): + return self.g.view(self, shape) + + def reshape(self, shape): + return self.g.reshape(self, shape) + + def expand(self, shape): + return self.g.expand(self, shape) + + def flatten(self, start_dim=0, end_dim=-1): + return self.g.flatten(self, start_dim=start_dim, end_dim=end_dim) + + def slice(self, axis, start, length=0): + return self.g.slice(self, axis, start, length=length) + + def index(self, index_value, axis=0): + return self.g.index(self, index_value, axis=axis) + + def transpose(self, backend=Graph.CPU): + return self.g.transpose(self, backend=backend) + + def permute(self, permutation, backend=Graph.CPU): + return self.g.permute(self, permutation, backend=backend) + + def concat(self, other, axis=0): + return self.g.concat(self, other, axis=axis) + + def cat(self, tensors, axis=0): + return self.g.cat([self] + tensors, axis=axis) + + def groupnorm(self, weight, bias, num_groups, eps=1e-5): + return self.g.groupnorm(self, weight, bias, num_groups, eps=eps) + + def layernorm(self, weight, bias=None, eps=1e-5): + return self.g.layernorm(self, weight, bias=bias, eps=eps) + + def batchnorm(self, weight, bias, running_mean, running_var, axis=1, eps=1e-5): + return self.g.batchnorm(self, weight, bias, running_mean, running_var, axis=axis, eps=eps) + + def group_norm(self, weight, bias, num_groups, eps=1e-5): + return self.groupnorm(weight, bias, num_groups, eps=eps) + + def layer_norm(self, weight, bias=None, eps=1e-5): + return self.layernorm(weight, bias=bias, eps=eps) + + def batch_norm(self, weight, bias, running_mean, running_var, axis=1, eps=1e-5): + return self.batchnorm(weight, bias, running_mean, running_var, axis=axis, eps=eps) + + def rms_norm(self, weight, eps=1e-5): + return self.g.rms_norm(self, weight, eps=eps) + + def softmax(self, axis=-1): + return self.g.softmax(self, axis) + + def glu(self, axis=-1): + return self.g.glu(self, axis=axis) + + def matmul(self, other, pretransposed_rhs=False, backend=Graph.CPU, output_dtype=None): + return self.g.matmul( + self, + other, + pretransposed_rhs=pretransposed_rhs, + backend=backend, + output_dtype=output_dtype, + ) + + def sum(self, axis): + return self.g.sum(self, axis) + + def mean(self, axis): + return self.g.mean(self, axis) + + def variance(self, axis): + return self.g.variance(self, axis) + + def min(self, axis): + return self.g.min(self, axis) + + def max(self, axis): + return self.g.max(self, axis) + + def cumsum(self, axis): + return self.g.cumsum(self, axis) + + def numpy(self): + info = cactus_tensor_info_t() + rc = _lib.cactus_graph_get_output_info(self.g.h, cactus_node_t(self.id), ctypes.byref(info)) + if rc != 0: + raise RuntimeError(_err("graph_get_output_info failed")) + + out_ptr = ctypes.c_void_p() + rc = _lib.cactus_graph_get_output_ptr(self.g.h, cactus_node_t(self.id), ctypes.byref(out_ptr)) + if rc != 0 or not out_ptr.value: + raise RuntimeError(_err("graph_get_output_ptr failed")) + + rank = int(info.rank) + shape = tuple(int(info.shape[i]) for i in range(rank)) + num_elements = int(info.num_elements) + precision = int(info.precision) + + if precision == Graph.FP16: + arr = np.ctypeslib.as_array((ctypes.c_uint16 * num_elements).from_address(out_ptr.value)).view(np.float16) + elif precision == Graph.FP32: + arr = np.ctypeslib.as_array((ctypes.c_float * num_elements).from_address(out_ptr.value)) + elif precision == Graph.INT8: + arr = np.ctypeslib.as_array((ctypes.c_int8 * num_elements).from_address(out_ptr.value)) + elif precision in (Graph.CQ1, Graph.CQ2, Graph.CQ3, Graph.CQ4): + arr = np.ctypeslib.as_array((ctypes.c_uint8 * int(info.byte_size)).from_address(out_ptr.value)) + return arr.copy() + else: + raise RuntimeError("unsupported precision") + + return arr.reshape(shape).copy() + + def __repr__(self): + return f"Tensor(id={self.id}, shape={self.shape}, dtype={self.dtype})" diff --git a/python/cactus/bindings/graph.py b/python/cactus/bindings/graph.py deleted file mode 100644 index a5b2f70b2..000000000 --- a/python/cactus/bindings/graph.py +++ /dev/null @@ -1,1541 +0,0 @@ -import ctypes -import numpy as np - -from .cactus import _err, _lib, cactus_node_t, cactus_tensor_info_t - - -class Graph: - INT8 = 0 - FP16 = 1 - FP32 = 2 - CQ1 = 3 - CQ2 = 4 - CQ3 = 5 - CQ4 = 6 - CPU = 0 - NPU = 1 - ACT_SILU = 0 - ACT_GELU = 1 - ACT_GELU_ERF = 2 - ACT_RELU = 3 - ACT_SIGMOID = 4 - ACT_TANH = 5 - - def __init__(self): - self.h = _lib.cactus_graph_create() - if not self.h: - raise RuntimeError("cactus_graph_create failed") - - def save(self, filename): - rc = _lib.cactus_graph_save(self.h, str(filename).encode()) - if rc != 0: - raise RuntimeError("graph_save failed") - - @classmethod - def load(cls, filename): - h = _lib.cactus_graph_load(str(filename).encode()) - if not h: - raise RuntimeError("cactus_graph_load failed") - obj = cls.__new__(cls) - obj.h = h - return obj - - def __del__(self): - h = getattr(self, "h", None) - if h: - _lib.cactus_graph_destroy(h) - self.h = None - - def input(self, shape, dtype=FP16): - shape = tuple(int(x) for x in shape) - arr = (ctypes.c_size_t * len(shape))(*shape) - out = cactus_node_t() - rc = _lib.cactus_graph_input(self.h, arr, len(shape), int(dtype), ctypes.byref(out)) - if rc != 0: - raise RuntimeError(_err("graph_input failed")) - return self._tensor_from_node(out.value) - - def set_input(self, tensor, data, dtype=None): - if not isinstance(tensor, Tensor): - raise TypeError("tensor must be a Tensor") - if tensor.g is not self: - raise ValueError("tensor belongs to a different graph") - target_dtype = int(tensor.dtype if dtype is None else dtype) - arr = self._coerce_input_array(data, target_dtype) - info = self._get_output_info(tensor.id) - expected_shape = tuple(int(x) for x in info["shape"]) - expected_num_elements = int(info["num_elements"]) - expected_byte_size = int(info["byte_size"]) - if tuple(int(x) for x in arr.shape) != expected_shape: - raise ValueError( - "graph input shape mismatch for node " - f"{tensor.id}: expected {expected_shape}, got {tuple(int(x) for x in arr.shape)}" - ) - if int(arr.size) != expected_num_elements: - raise ValueError( - "graph input element-count mismatch for node " - f"{tensor.id}: expected {expected_num_elements}, got {int(arr.size)}" - ) - if int(arr.nbytes) != expected_byte_size: - raise ValueError( - "graph input byte-size mismatch for node " - f"{tensor.id}: expected {expected_byte_size}, got {int(arr.nbytes)}" - ) - rc = _lib.cactus_graph_set_input( - self.h, - cactus_node_t(tensor.id), - arr.ctypes.data_as(ctypes.c_void_p), - target_dtype, - ) - if rc != 0: - raise RuntimeError(_err("graph_set_input failed")) - - def set_external_input(self, tensor, data_ptr, dtype=None): - if not isinstance(tensor, Tensor): - raise TypeError("tensor must be a Tensor") - if tensor.g is not self: - raise ValueError("tensor belongs to a different graph") - target_dtype = int(tensor.dtype if dtype is None else dtype) - ptr = ctypes.c_void_p(data_ptr if isinstance(data_ptr, int) else int(data_ptr)) - rc = _lib.cactus_graph_set_external_input( - self.h, - cactus_node_t(tensor.id), - ptr, - target_dtype, - ) - if rc != 0: - raise RuntimeError(_err("graph_set_external_input failed")) - - def mark_embedded_input(self, tensor): - if not isinstance(tensor, Tensor): - raise TypeError("tensor must be a Tensor") - if tensor.g is not self: - raise ValueError("tensor belongs to a different graph") - rc = _lib.cactus_graph_mark_embedded_input(self.h, cactus_node_t(tensor.id)) - if rc != 0: - raise RuntimeError(_err("graph_mark_embedded_input failed")) - - def hard_reset(self): - rc = _lib.cactus_graph_hard_reset(self.h) - if rc != 0: - raise RuntimeError("graph_hard_reset failed") - - def execute(self): - rc = _lib.cactus_graph_execute(self.h) - if rc != 0: - raise RuntimeError(_err("graph_execute failed")) - - def add(self, a, b): - return self._binary("cactus_graph_add", a, b) - - def add_clipped(self, a, b): - return self._binary("cactus_graph_add_clipped", a, b) - - def subtract(self, a, b): - return self._binary("cactus_graph_subtract", a, b) - - def multiply(self, a, b): - return self._binary("cactus_graph_multiply", a, b) - - def divide(self, a, b): - return self._binary("cactus_graph_divide", a, b) - - def not_equal(self, a, b): - return self._binary("cactus_graph_not_equal", a, b) - - def abs(self, x): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_abs(self.h, cactus_node_t(x.id), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_abs failed") - return self._tensor_from_node(out.value) - - def pow(self, x, exponent): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_pow(self.h, cactus_node_t(x.id), ctypes.c_float(float(exponent)), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_pow failed") - return self._tensor_from_node(out.value) - - def precision_cast(self, x, dtype): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_precision_cast(self.h, cactus_node_t(x.id), int(dtype), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_precision_cast failed") - return self._tensor_from_node(out.value) - - def quantize_activations(self, x): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_quantize_activations(self.h, cactus_node_t(x.id), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_quantize_activations failed") - return self._tensor_from_node(out.value) - - def _scalar(self, fn_name, x, value=None): - x = self._ensure_tensor(x) - out = cactus_node_t() - fn = getattr(_lib, fn_name) - if value is None: - rc = fn(self.h, cactus_node_t(x.id), ctypes.byref(out)) - else: - rc = fn(self.h, cactus_node_t(x.id), ctypes.c_float(float(value)), ctypes.byref(out)) - if rc != 0: - raise RuntimeError(f"{fn_name} failed") - return self._tensor_from_node(out.value) - - def scalar_add(self, x, value): - return self._scalar("cactus_graph_scalar_add", x, value) - - def scalar_subtract(self, x, value): - return self._scalar("cactus_graph_scalar_subtract", x, value) - - def scalar_multiply(self, x, value): - return self._scalar("cactus_graph_scalar_multiply", x, value) - - def scalar_divide(self, x, value): - return self._scalar("cactus_graph_scalar_divide", x, value) - - def scalar_floor_divide(self, x, value): - return self._scalar("cactus_graph_scalar_floor_divide", x, value) - - def scalar_not_equal(self, x, value): - return self._scalar("cactus_graph_scalar_not_equal", x, value) - - def scalar_exp(self, x): - return self._scalar("cactus_graph_scalar_exp", x) - - def scalar_sqrt(self, x): - return self._scalar("cactus_graph_scalar_sqrt", x) - - def scalar_cos(self, x): - return self._scalar("cactus_graph_scalar_cos", x) - - def scalar_sin(self, x): - return self._scalar("cactus_graph_scalar_sin", x) - - def scalar_log(self, x): - return self._scalar("cactus_graph_scalar_log", x) - - def clamp(self, x, lo, hi): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_clamp( - self.h, - cactus_node_t(x.id), - ctypes.c_float(float(lo)), - ctypes.c_float(float(hi)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError(_err("graph_clamp failed")) - return self._tensor_from_node(out.value) - - def masked_select_prefix(self, x, mask): - return self._binary("cactus_graph_masked_select_prefix", x, mask) - - def masked_scatter(self, x, mask, source): - x = self._ensure_tensor(x) - mask = self._ensure_tensor(mask) - source = self._ensure_tensor(source) - out = cactus_node_t() - rc = _lib.cactus_graph_masked_scatter( - self.h, - cactus_node_t(x.id), - cactus_node_t(mask.id), - cactus_node_t(source.id), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError(_err("graph_masked_scatter failed")) - return self._tensor_from_node(out.value) - - def view(self, x, shape): - x = self._ensure_tensor(x) - shape = tuple(int(v) for v in shape) - arr = (ctypes.c_size_t * len(shape))(*shape) - out = cactus_node_t() - rc = _lib.cactus_graph_view(self.h, cactus_node_t(x.id), arr, len(shape), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_view failed") - return self._tensor_from_node(out.value) - - def reshape(self, x, shape): - x = self._ensure_tensor(x) - shape = tuple(int(v) for v in shape) - arr = (ctypes.c_size_t * len(shape))(*shape) - out = cactus_node_t() - rc = _lib.cactus_graph_reshape(self.h, cactus_node_t(x.id), arr, len(shape), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_reshape failed") - return self._tensor_from_node(out.value) - - def expand(self, x, shape): - x = self._ensure_tensor(x) - shape = tuple(int(v) for v in shape) - arr = (ctypes.c_size_t * len(shape))(*shape) - out = cactus_node_t() - rc = _lib.cactus_graph_expand(self.h, cactus_node_t(x.id), arr, len(shape), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_expand failed") - return self._tensor_from_node(out.value) - - def flatten(self, x, start_dim=0, end_dim=-1): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_flatten( - self.h, - cactus_node_t(x.id), - ctypes.c_int32(int(start_dim)), - ctypes.c_int32(int(end_dim)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_flatten failed") - return self._tensor_from_node(out.value) - - def slice(self, x, axis, start, length=0): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_slice( - self.h, - cactus_node_t(x.id), - ctypes.c_int32(int(axis)), - ctypes.c_size_t(int(start)), - ctypes.c_size_t(int(length)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_slice failed") - return self._tensor_from_node(out.value) - - def index(self, x, index_value, axis=0): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_index( - self.h, - cactus_node_t(x.id), - ctypes.c_size_t(int(index_value)), - ctypes.c_int32(int(axis)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_index failed") - return self._tensor_from_node(out.value) - - def transpose(self, x, backend=CPU): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_transpose( - self.h, - cactus_node_t(x.id), - ctypes.c_int32(int(backend)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_transpose failed") - return self._tensor_from_node(out.value) - - def permute(self, x, permutation, backend=CPU): - x = self._ensure_tensor(x) - permutation = tuple(int(v) for v in permutation) - arr = (ctypes.c_size_t * len(permutation))(*permutation) - out = cactus_node_t() - rc = _lib.cactus_graph_transpose_n( - self.h, - cactus_node_t(x.id), - arr, - len(permutation), - ctypes.c_int32(int(backend)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_transpose_n failed") - return self._tensor_from_node(out.value) - - def matmul(self, a, b, pretransposed_rhs=False, backend=CPU, output_dtype=None): - a = self._ensure_tensor(a) - b = self._ensure_tensor(b) - out = cactus_node_t() - rc = _lib.cactus_graph_matmul( - self.h, - cactus_node_t(a.id), - cactus_node_t(b.id), - ctypes.c_bool(bool(pretransposed_rhs)), - ctypes.c_int32(int(backend)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError(_err("graph_matmul failed")) - return self._tensor_from_node(out.value) - - def gather(self, tensor, indices): - tensor = self._ensure_tensor(tensor) - indices = self._ensure_tensor(indices) - out = cactus_node_t() - rc = _lib.cactus_graph_gather( - self.h, - cactus_node_t(tensor.id), - cactus_node_t(indices.id), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_gather failed") - return self._tensor_from_node(out.value) - - def embedding_from_tensor(self, embedding_tensor, indices): - embedding_tensor = self._ensure_tensor(embedding_tensor) - indices = self._ensure_tensor(indices) - out = cactus_node_t() - rc = _lib.cactus_graph_embedding_from_tensor( - self.h, cactus_node_t(embedding_tensor.id), cactus_node_t(indices.id), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_embedding_from_tensor failed") - return self._tensor_from_node(out.value) - - def embedding_from_file(self, filename, indices): - indices = self._ensure_tensor(indices) - out = cactus_node_t() - rc = _lib.cactus_graph_embedding_from_file(self.h, str(filename).encode(), cactus_node_t(indices.id), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_embedding_from_file failed") - return self._tensor_from_node(out.value) - - def mmap_embeddings(self, filename): - out = cactus_node_t() - rc = _lib.cactus_graph_mmap_embeddings(self.h, str(filename).encode(), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_mmap_embeddings failed") - return self._tensor_from_node(out.value) - - def mmap_weights(self, filename): - out = cactus_node_t() - rc = _lib.cactus_graph_mmap_weights(self.h, str(filename).encode(), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_mmap_weights failed") - return self._tensor_from_node(out.value) - - def bind_mmap_weights(self, tensor, filename): - tensor = self._ensure_tensor(tensor) - rc = _lib.cactus_graph_bind_mmap_weights(self.h, cactus_node_t(tensor.id), str(filename).encode()) - if rc != 0: - raise RuntimeError("graph_bind_mmap_weights failed") - - def bilinear_interpolation(self, pos_embeds, dst_height, dst_width): - pos_embeds = self._ensure_tensor(pos_embeds) - out = cactus_node_t() - rc = _lib.cactus_graph_bilinear_interpolation( - self.h, cactus_node_t(pos_embeds.id), ctypes.c_size_t(int(dst_height)), ctypes.c_size_t(int(dst_width)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_bilinear_interpolation failed") - return self._tensor_from_node(out.value) - - def set_grouped_scales(self, tensor, group_size, num_groups, scales): - tensor = self._ensure_tensor(tensor) - arr = np.ascontiguousarray(scales, dtype=np.float16) - rc = _lib.cactus_graph_set_grouped_scales( - self.h, - cactus_node_t(tensor.id), - ctypes.c_size_t(int(group_size)), - ctypes.c_size_t(int(num_groups)), - arr.ctypes.data_as(ctypes.c_void_p), - ) - if rc != 0: - raise RuntimeError("graph_set_grouped_scales failed") - - def set_interleaved(self, tensor, interleaved=True, original_n=0): - tensor = self._ensure_tensor(tensor) - rc = _lib.cactus_graph_set_interleaved( - self.h, cactus_node_t(tensor.id), ctypes.c_bool(bool(interleaved)), ctypes.c_size_t(int(original_n)) - ) - if rc != 0: - raise RuntimeError("graph_set_interleaved failed") - - def release_weight_pages(self, tensor): - tensor = self._ensure_tensor(tensor) - rc = _lib.cactus_graph_release_weight_pages(self.h, cactus_node_t(tensor.id)) - if rc != 0: - raise RuntimeError("graph_release_weight_pages failed") - - def prefetch_weight_pages(self, tensor): - tensor = self._ensure_tensor(tensor) - rc = _lib.cactus_graph_prefetch_weight_pages(self.h, cactus_node_t(tensor.id)) - if rc != 0: - raise RuntimeError("graph_prefetch_weight_pages failed") - - def release_all_weight_pages(self): - rc = _lib.cactus_graph_release_all_weight_pages(self.h) - if rc != 0: - raise RuntimeError("graph_release_all_weight_pages failed") - - def concat(self, a, b, axis=0): - a = self._ensure_tensor(a) - b = self._ensure_tensor(b) - out = cactus_node_t() - rc = _lib.cactus_graph_concat( - self.h, - cactus_node_t(a.id), - cactus_node_t(b.id), - ctypes.c_int32(int(axis)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_concat failed") - return self._tensor_from_node(out.value) - - def cat(self, tensors, axis=0): - tensors = [self._ensure_tensor(t) for t in tensors] - if not tensors: - raise ValueError("cat requires at least one tensor") - if len(tensors) == 1: - return tensors[0] - ids = (cactus_node_t * len(tensors))(*(cactus_node_t(t.id) for t in tensors)) - out = cactus_node_t() - rc = _lib.cactus_graph_cat( - self.h, ids, ctypes.c_size_t(len(tensors)), - ctypes.c_int32(int(axis)), ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_cat failed") - return self._tensor_from_node(out.value) - - def groupnorm(self, x, weight, bias, num_groups, eps=1e-5): - x = self._ensure_tensor(x) - weight = self._ensure_tensor(weight) - bias = self._ensure_tensor(bias) - out = cactus_node_t() - rc = _lib.cactus_graph_groupnorm( - self.h, - cactus_node_t(x.id), - cactus_node_t(weight.id), - cactus_node_t(bias.id), - ctypes.c_size_t(int(num_groups)), - ctypes.c_float(float(eps)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_group_norm failed") - return self._tensor_from_node(out.value) - - def group_norm(self, x, weight, bias, num_groups, eps=1e-5): - return self.groupnorm(x, weight, bias, num_groups, eps=eps) - - def layernorm(self, x, weight, bias=None, eps=1e-5): - x = self._ensure_tensor(x) - weight = self._ensure_tensor(weight) - has_bias = bias is not None - bias_node = cactus_node_t(0) - if has_bias: - bias = self._ensure_tensor(bias) - bias_node = cactus_node_t(bias.id) - out = cactus_node_t() - rc = _lib.cactus_graph_layernorm( - self.h, - cactus_node_t(x.id), - cactus_node_t(weight.id), - bias_node, - ctypes.c_float(float(eps)), - ctypes.c_bool(has_bias), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_layer_norm failed") - return self._tensor_from_node(out.value) - - def layer_norm(self, x, weight, bias=None, eps=1e-5): - return self.layernorm(x, weight, bias=bias, eps=eps) - - def batchnorm(self, x, weight, bias, running_mean, running_var, axis=1, eps=1e-5): - x = self._ensure_tensor(x) - weight = self._ensure_tensor(weight) - bias = self._ensure_tensor(bias) - running_mean = self._ensure_tensor(running_mean) - running_var = self._ensure_tensor(running_var) - out = cactus_node_t() - rc = _lib.cactus_graph_batchnorm( - self.h, - cactus_node_t(x.id), - cactus_node_t(weight.id), - cactus_node_t(bias.id), - cactus_node_t(running_mean.id), - cactus_node_t(running_var.id), - ctypes.c_int32(int(axis)), - ctypes.c_float(float(eps)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_batchnorm failed") - return self._tensor_from_node(out.value) - - def batch_norm(self, x, weight, bias, running_mean, running_var, axis=1, eps=1e-5): - return self.batchnorm(x, weight, bias, running_mean, running_var, axis=axis, eps=eps) - - def rms_norm(self, x, weight, eps=1e-5): - x = self._ensure_tensor(x) - weight = self._ensure_tensor(weight) - out = cactus_node_t() - rc = _lib.cactus_graph_rms_norm( - self.h, - cactus_node_t(x.id), - cactus_node_t(weight.id), - ctypes.c_float(float(eps)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_rms_norm failed") - return self._tensor_from_node(out.value) - - def topk(self, x, k): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_topk(self.h, cactus_node_t(x.id), ctypes.c_size_t(int(k)), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_topk failed") - return self._tensor_from_node(out.value) - - def rope(self, x, theta, position_offset=0, backend=CPU): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_rope( - self.h, cactus_node_t(x.id), ctypes.c_float(float(theta)), ctypes.c_size_t(int(position_offset)), - ctypes.c_int32(int(backend)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_rope failed") - return self._tensor_from_node(out.value) - - def rope_gptj(self, x, theta, position_offset=0, rot_dim=0, backend=CPU): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_rope_gptj( - self.h, cactus_node_t(x.id), ctypes.c_float(float(theta)), ctypes.c_size_t(int(position_offset)), - ctypes.c_size_t(int(rot_dim)), ctypes.c_int32(int(backend)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_rope_gptj failed") - return self._tensor_from_node(out.value) - - def _reduce(self, fn_name, x, axis): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = getattr(_lib, fn_name)(self.h, cactus_node_t(x.id), ctypes.c_int32(int(axis)), ctypes.byref(out)) - if rc != 0: - raise RuntimeError(f"{fn_name} failed") - return self._tensor_from_node(out.value) - - def sum(self, x, axis): - return self._reduce("cactus_graph_sum", x, axis) - - def mean(self, x, axis): - return self._reduce("cactus_graph_mean", x, axis) - - def variance(self, x, axis): - return self._reduce("cactus_graph_variance", x, axis) - - def min(self, x, axis): - return self._reduce("cactus_graph_min", x, axis) - - def max(self, x, axis): - return self._reduce("cactus_graph_max", x, axis) - - def cumsum(self, x, axis): - return self._reduce("cactus_graph_cumsum", x, axis) - - def softmax(self, x, axis=-1): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_softmax( - self.h, - cactus_node_t(x.id), - ctypes.c_int32(int(axis)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_softmax failed") - return self._tensor_from_node(out.value) - - def attention(self, query, key, value, scale, is_causal=True, position_offset=0, window_size=0, - backend=CPU, mask=None, additive_mask=False): - query = self._ensure_tensor(query) - key = self._ensure_tensor(key) - value = self._ensure_tensor(value) - mask_node = cactus_node_t(0) - use_mask = mask is not None - if use_mask: - mask_node = cactus_node_t(self._ensure_tensor(mask).id) - out = cactus_node_t() - rc = _lib.cactus_graph_attention( - self.h, - cactus_node_t(query.id), - cactus_node_t(key.id), - cactus_node_t(value.id), - ctypes.c_float(float(scale)), - ctypes.c_bool(bool(is_causal)), - ctypes.c_size_t(int(position_offset)), - ctypes.c_size_t(int(window_size)), - ctypes.c_int32(int(backend)), - ctypes.c_bool(use_mask), - mask_node, - ctypes.c_bool(bool(additive_mask)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_attention failed") - return self._tensor_from_node(out.value) - - def kv_cache_state(self, max_seq_len, num_kv_heads, head_dim, window_size=0, sink_size=0): - out = cactus_node_t() - rc = _lib.cactus_graph_kv_cache_state( - self.h, - ctypes.c_size_t(int(max_seq_len)), - ctypes.c_size_t(int(num_kv_heads)), - ctypes.c_size_t(int(head_dim)), - ctypes.c_size_t(int(window_size)), - ctypes.c_size_t(int(sink_size)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError(_err("graph_kv_cache_state failed")) - return self._tensor_from_node(out.value) - - def kv_cache_append(self, new_kv, cache_state, window_size=0, sink_size=0): - new_kv = self._ensure_tensor(new_kv) - cache_state = self._ensure_tensor(cache_state) - out = cactus_node_t() - rc = _lib.cactus_graph_kv_cache_append( - self.h, - cactus_node_t(new_kv.id), - cactus_node_t(cache_state.id), - ctypes.c_size_t(int(window_size)), - ctypes.c_size_t(int(sink_size)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError(_err("graph_kv_cache_append failed")) - return self._tensor_from_node(out.value) - - def attention_cached( - self, - query, - key_new, - value_new, - k_cache_state, - v_cache_state, - scale, - position_offset=0, - window_size=0, - v_head_dim=0, - ): - query = self._ensure_tensor(query) - key_new = self._ensure_tensor(key_new) - value_new = self._ensure_tensor(value_new) - k_cache_state = self._ensure_tensor(k_cache_state) - v_cache_state = self._ensure_tensor(v_cache_state) - out = cactus_node_t() - rc = _lib.cactus_graph_attention_cached( - self.h, - cactus_node_t(query.id), - cactus_node_t(key_new.id), - cactus_node_t(value_new.id), - cactus_node_t(k_cache_state.id), - cactus_node_t(v_cache_state.id), - ctypes.c_float(float(scale)), - ctypes.c_size_t(int(position_offset)), - ctypes.c_size_t(int(window_size)), - ctypes.c_size_t(int(v_head_dim)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError(_err("graph_attention_cached failed")) - return self._tensor_from_node(out.value) - - def conv_cache_state(self, window_size, hidden_dim): - out = cactus_node_t() - rc = _lib.cactus_graph_conv_cache_state( - self.h, - ctypes.c_size_t(int(window_size)), - ctypes.c_size_t(int(hidden_dim)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError(_err("graph_conv_cache_state failed")) - return self._tensor_from_node(out.value) - - def conv_cache_append(self, new_data, cache_state): - new_data = self._ensure_tensor(new_data) - cache_state = self._ensure_tensor(cache_state) - out = cactus_node_t() - rc = _lib.cactus_graph_conv_cache_append( - self.h, - cactus_node_t(new_data.id), - cactus_node_t(cache_state.id), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError(_err("graph_conv_cache_append failed")) - return self._tensor_from_node(out.value) - - def conv_cache_initialize(self, rows, cache_state): - rows = self._ensure_tensor(rows) - cache_state = self._ensure_tensor(cache_state) - out = cactus_node_t() - rc = _lib.cactus_graph_conv_cache_initialize( - self.h, - cactus_node_t(rows.id), - cactus_node_t(cache_state.id), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError(_err("graph_conv_cache_initialize failed")) - return self._tensor_from_node(out.value) - - def recurrent_cache_state(self, shape, dtype=FP16): - shape = tuple(int(x) for x in shape) - arr = (ctypes.c_size_t * len(shape))(*shape) - out = cactus_node_t() - rc = _lib.cactus_graph_recurrent_cache_state( - self.h, - arr, - ctypes.c_size_t(len(shape)), - ctypes.c_int(int(dtype)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError(_err("graph_recurrent_cache_state failed")) - return self._tensor_from_node(out.value) - - def recurrent_cache_write(self, new_value, cache_input): - new_value = self._ensure_tensor(new_value) - cache_input = self._ensure_tensor(cache_input) - out = cactus_node_t() - rc = _lib.cactus_graph_recurrent_cache_write( - self.h, - cactus_node_t(new_value.id), - cactus_node_t(cache_input.id), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError(_err("graph_recurrent_cache_write failed")) - return self._tensor_from_node(out.value) - - def rel_pos_bias(self, query, relative_key, scale): - query = self._ensure_tensor(query) - relative_key = self._ensure_tensor(relative_key) - out = cactus_node_t() - rc = _lib.cactus_graph_rel_pos_bias( - self.h, cactus_node_t(query.id), cactus_node_t(relative_key.id), ctypes.c_float(float(scale)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_rel_pos_bias failed") - return self._tensor_from_node(out.value) - - def attention_int8_hybrid(self, query, key_new, value_new, scale, position_offset, - cached_keys, cached_values, k_scales, v_scales, - cache_len, num_kv_heads, head_dim, window_size=0): - query = self._ensure_tensor(query) - key_new = self._ensure_tensor(key_new) - value_new = self._ensure_tensor(value_new) - ck = np.ascontiguousarray(cached_keys, dtype=np.int8) - cv = np.ascontiguousarray(cached_values, dtype=np.int8) - ks = np.ascontiguousarray(k_scales, dtype=np.float32) - vs = np.ascontiguousarray(v_scales, dtype=np.float32) - out = cactus_node_t() - rc = _lib.cactus_graph_attention_int8_hybrid( - self.h, - cactus_node_t(query.id), - cactus_node_t(key_new.id), - cactus_node_t(value_new.id), - ctypes.c_float(float(scale)), - ctypes.c_size_t(int(position_offset)), - ck.ctypes.data_as(ctypes.POINTER(ctypes.c_int8)), - cv.ctypes.data_as(ctypes.POINTER(ctypes.c_int8)), - ks.ctypes.data_as(ctypes.POINTER(ctypes.c_float)), - vs.ctypes.data_as(ctypes.POINTER(ctypes.c_float)), - ctypes.c_size_t(int(cache_len)), - ctypes.c_size_t(int(num_kv_heads)), - ctypes.c_size_t(int(head_dim)), - ctypes.c_size_t(int(window_size)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_attention_int8_hybrid failed") - return self._tensor_from_node(out.value) - - def relu(self, x): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_relu(self.h, cactus_node_t(x.id), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_relu failed") - return self._tensor_from_node(out.value) - - def silu(self, x): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_silu(self.h, cactus_node_t(x.id), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_silu failed") - return self._tensor_from_node(out.value) - - def gelu(self, x): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_gelu(self.h, cactus_node_t(x.id), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_gelu failed") - return self._tensor_from_node(out.value) - - def gelu_erf(self, x): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_gelu_erf(self.h, cactus_node_t(x.id), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_gelu_erf failed") - return self._tensor_from_node(out.value) - - def sigmoid(self, x): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_sigmoid(self.h, cactus_node_t(x.id), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_sigmoid failed") - return self._tensor_from_node(out.value) - - def tanh(self, x): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_tanh(self.h, cactus_node_t(x.id), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_tanh failed") - return self._tensor_from_node(out.value) - - def glu(self, x, axis=-1): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_glu(self.h, cactus_node_t(x.id), ctypes.c_int32(int(axis)), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_glu failed") - return self._tensor_from_node(out.value) - - def conv1d_causal(self, x, weight, kernel_size, dilation): - x = self._ensure_tensor(x) - weight = self._ensure_tensor(weight) - out = cactus_node_t() - rc = _lib.cactus_graph_conv1d_causal( - self.h, cactus_node_t(x.id), cactus_node_t(weight.id), - ctypes.c_size_t(int(kernel_size)), ctypes.c_size_t(int(dilation)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_conv1d_causal failed") - return self._tensor_from_node(out.value) - - def conv1d_k3(self, x, weight, stride=1): - x = self._ensure_tensor(x) - weight = self._ensure_tensor(weight) - out = cactus_node_t() - rc = _lib.cactus_graph_conv1d_k3( - self.h, cactus_node_t(x.id), cactus_node_t(weight.id), ctypes.c_size_t(int(stride)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_conv1d_k3 failed") - return self._tensor_from_node(out.value) - - def conv1d_k7s3(self, x, weight, bias): - x = self._ensure_tensor(x) - weight = self._ensure_tensor(weight) - bias = self._ensure_tensor(bias) - out = cactus_node_t() - rc = _lib.cactus_graph_conv1d_k7s3( - self.h, cactus_node_t(x.id), cactus_node_t(weight.id), cactus_node_t(bias.id), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_conv1d_k7s3 failed") - return self._tensor_from_node(out.value) - - def conv1d(self, x, weight, bias=None, stride=1): - return self._conv_with_optional_bias("cactus_graph_conv1d", x, weight, bias, ctypes.c_size_t(int(stride))) - - def conv1d_same_depthwise_k9(self, x, weight, bias=None): - return self._conv_with_optional_bias("cactus_graph_conv1d_same_depthwise_k9", x, weight, bias) - - def conv1d_pointwise(self, x, weight, bias=None): - return self._conv_with_optional_bias("cactus_graph_conv1d_pointwise", x, weight, bias) - - def conv2d_k3s2p1(self, x, weight, bias=None): - return self._conv_with_optional_bias("cactus_graph_conv2d_k3s2p1", x, weight, bias) - - def conv2d_depthwise_k3s2p1(self, x, weight, bias=None): - return self._conv_with_optional_bias("cactus_graph_conv2d_depthwise_k3s2p1", x, weight, bias) - - def conv2d_pointwise_1x1(self, x, weight, bias=None): - return self._conv_with_optional_bias("cactus_graph_conv2d_pointwise_1x1", x, weight, bias) - - def conv2d_k3s1p1(self, x, weight, bias=None): - return self._conv_with_optional_bias("cactus_graph_conv2d_k3s1p1", x, weight, bias) - - def conv2d(self, x, weight, bias=None, stride=1, padding=0, dilation=1, groups=1): - return self._conv_with_optional_bias( - "cactus_graph_conv2d", - x, - weight, - bias, - ctypes.c_size_t(int(stride)), - ctypes.c_size_t(int(padding)), - ctypes.c_size_t(int(dilation)), - ctypes.c_size_t(int(groups)), - ) - - def _conv_with_optional_bias(self, fn_name, x, weight, bias=None, *extra): - x = self._ensure_tensor(x) - weight = self._ensure_tensor(weight) - has_bias = bias is not None - bias_node = cactus_node_t(0 if bias is None else self._ensure_tensor(bias).id) - out = cactus_node_t() - rc = getattr(_lib, fn_name)( - self.h, cactus_node_t(x.id), cactus_node_t(weight.id), ctypes.c_bool(has_bias), bias_node, *extra, ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError(f"{fn_name} failed") - return self._tensor_from_node(out.value) - - def lstm_cell(self, input, h_prev, c_prev, weight_ih, weight_hh, bias_ih, bias_hh): - tensors = [self._ensure_tensor(t) for t in (input, h_prev, c_prev, weight_ih, weight_hh, bias_ih, bias_hh)] - out = cactus_node_t() - rc = _lib.cactus_graph_lstm_cell(self.h, *(cactus_node_t(t.id) for t in tensors), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_lstm_cell failed") - return self._tensor_from_node(out.value) - - def gated_deltanet_decode(self, query, key, value, gate_log, beta, initial_state, scale): - tensors = [self._ensure_tensor(t) for t in (query, key, value, gate_log, beta, initial_state)] - out = cactus_node_t() - rc = _lib.cactus_graph_gated_deltanet_decode( - self.h, *(cactus_node_t(t.id) for t in tensors), ctypes.c_float(float(scale)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_gated_deltanet_decode failed") - return self._tensor_from_node(out.value) - - def gated_deltanet_prefill(self, query, key, value, gate_log, beta, initial_state, chunk_size, scale): - tensors = [self._ensure_tensor(t) for t in (query, key, value, gate_log, beta, initial_state)] - out = cactus_node_t() - rc = _lib.cactus_graph_gated_deltanet_prefill( - self.h, *(cactus_node_t(t.id) for t in tensors), ctypes.c_size_t(int(chunk_size)), ctypes.c_float(float(scale)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_gated_deltanet_prefill failed") - return self._tensor_from_node(out.value) - - def stft(self, x, weight, stride, num_fft_bins): - x = self._ensure_tensor(x) - weight = self._ensure_tensor(weight) - out = cactus_node_t() - rc = _lib.cactus_graph_stft( - self.h, cactus_node_t(x.id), cactus_node_t(weight.id), - ctypes.c_size_t(int(stride)), ctypes.c_size_t(int(num_fft_bins)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_stft failed") - return self._tensor_from_node(out.value) - - def altup_predict(self, coefs, streams): - coefs = self._ensure_tensor(coefs) - streams = [self._ensure_tensor(t) for t in streams] - ids = (cactus_node_t * len(streams))(*(cactus_node_t(t.id) for t in streams)) - out = cactus_node_t() - rc = _lib.cactus_graph_altup_predict(self.h, cactus_node_t(coefs.id), ids, ctypes.c_size_t(len(streams)), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_altup_predict failed") - return self._tensor_from_node(out.value) - - def altup_correct(self, coefs, innovation, predictions): - coefs = self._ensure_tensor(coefs) - innovation = self._ensure_tensor(innovation) - predictions = [self._ensure_tensor(t) for t in predictions] - ids = (cactus_node_t * len(predictions))(*(cactus_node_t(t.id) for t in predictions)) - out = cactus_node_t() - rc = _lib.cactus_graph_altup_correct( - self.h, cactus_node_t(coefs.id), cactus_node_t(innovation.id), ids, ctypes.c_size_t(len(predictions)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_altup_correct failed") - return self._tensor_from_node(out.value) - - def gaussian_topk(self, x, ppf): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_gaussian_topk(self.h, cactus_node_t(x.id), ctypes.c_float(float(ppf)), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_gaussian_topk failed") - return self._tensor_from_node(out.value) - - def moe_layer_gated(self, hidden, routing_probs, topk_indices, w1_weights, w3_weights, w2_weights, - num_experts, num_experts_per_tok, normalize_routing=True, epsilon=1e-6, routed_scaling_factor=1.0): - hidden = self._ensure_tensor(hidden) - routing_probs = self._ensure_tensor(routing_probs) - topk_indices = self._ensure_tensor(topk_indices) - w1 = (cactus_node_t * len(w1_weights))(*(cactus_node_t(self._ensure_tensor(t).id) for t in w1_weights)) - w3 = (cactus_node_t * len(w3_weights))(*(cactus_node_t(self._ensure_tensor(t).id) for t in w3_weights)) - w2 = (cactus_node_t * len(w2_weights))(*(cactus_node_t(self._ensure_tensor(t).id) for t in w2_weights)) - out = cactus_node_t() - rc = _lib.cactus_graph_moe_layer_gated( - self.h, cactus_node_t(hidden.id), cactus_node_t(routing_probs.id), cactus_node_t(topk_indices.id), - w1, w3, w2, ctypes.c_size_t(int(num_experts)), ctypes.c_size_t(int(num_experts_per_tok)), - ctypes.c_bool(bool(normalize_routing)), ctypes.c_float(float(epsilon)), - ctypes.c_float(float(routed_scaling_factor)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_moe_layer_gated failed") - return self._tensor_from_node(out.value) - - def dense_mlp_tq_fused(self, hidden, gate_weight, up_weight, down_weight, product_scale=1.0): - hidden = self._ensure_tensor(hidden) - gate_weight = self._ensure_tensor(gate_weight) - up_weight = self._ensure_tensor(up_weight) - down_weight = self._ensure_tensor(down_weight) - out = cactus_node_t() - rc = _lib.cactus_graph_dense_mlp_tq_fused( - self.h, - cactus_node_t(hidden.id), - cactus_node_t(gate_weight.id), - cactus_node_t(up_weight.id), - cactus_node_t(down_weight.id), - ctypes.c_float(float(product_scale)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError(_err("graph_dense_mlp_tq_fused failed")) - return self._tensor_from_node(out.value) - - def moe_layer_ungated(self, hidden, routing_probs, topk_indices, w1_weights, w2_weights, - num_experts, num_experts_per_tok, normalize_routing=True, epsilon=1e-6, - routed_scaling_factor=1.0, activation=ACT_GELU): - hidden = self._ensure_tensor(hidden) - routing_probs = self._ensure_tensor(routing_probs) - topk_indices = self._ensure_tensor(topk_indices) - w1 = (cactus_node_t * len(w1_weights))(*(cactus_node_t(self._ensure_tensor(t).id) for t in w1_weights)) - w2 = (cactus_node_t * len(w2_weights))(*(cactus_node_t(self._ensure_tensor(t).id) for t in w2_weights)) - out = cactus_node_t() - rc = _lib.cactus_graph_moe_layer_ungated( - self.h, cactus_node_t(hidden.id), cactus_node_t(routing_probs.id), cactus_node_t(topk_indices.id), - w1, w2, ctypes.c_size_t(int(num_experts)), ctypes.c_size_t(int(num_experts_per_tok)), - ctypes.c_bool(bool(normalize_routing)), ctypes.c_float(float(epsilon)), - ctypes.c_float(float(routed_scaling_factor)), ctypes.c_int32(int(activation)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_moe_layer_ungated failed") - return self._tensor_from_node(out.value) - - def sample(self, logits, temperature=1.0, top_p=1.0, top_k=0): - logits = self._ensure_tensor(logits) - out = cactus_node_t() - rc = _lib.cactus_graph_sample( - self.h, cactus_node_t(logits.id), ctypes.c_float(float(temperature)), - ctypes.c_float(float(top_p)), ctypes.c_size_t(int(top_k)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_sample failed") - return self._tensor_from_node(out.value) - - def scatter_topk(self, indices, values, num_classes): - indices = self._ensure_tensor(indices) - values = self._ensure_tensor(values) - out = cactus_node_t() - rc = _lib.cactus_graph_scatter_topk( - self.h, cactus_node_t(indices.id), cactus_node_t(values.id), ctypes.c_size_t(int(num_classes)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_scatter_topk failed") - return self._tensor_from_node(out.value) - - def persistent(self, source_node): - source_node = self._ensure_tensor(source_node) - out = cactus_node_t() - rc = _lib.cactus_graph_persistent(self.h, cactus_node_t(source_node.id), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_persistent failed") - return self._tensor_from_node(out.value) - - def is_populated(self, persistent_node): - persistent_node = self._ensure_tensor(persistent_node) - out_is_populated = ctypes.c_int32() - rc = _lib.cactus_graph_is_populated(self.h, cactus_node_t(persistent_node.id), ctypes.byref(out_is_populated)) - if rc != 0: - raise RuntimeError("graph_is_populated failed") - return bool(out_is_populated.value) - - def invalidate_persistent(self, persistent_node): - persistent_node = self._ensure_tensor(persistent_node) - rc = _lib.cactus_graph_invalidate_persistent(self.h, cactus_node_t(persistent_node.id)) - if rc != 0: - raise RuntimeError("graph_invalidate_persistent failed") - - # ── Audio / signal processing ─────────────────────────────────── - - def rfft(self, x): - """Real-to-complex FFT.""" - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_rfft(self.h, cactus_node_t(x.id), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_rfft failed") - return self._tensor_from_node(out.value) - - def irfft(self, x, output_length): - """Complex-to-real inverse FFT.""" - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_irfft( - self.h, cactus_node_t(x.id), - ctypes.c_size_t(int(output_length)), ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_irfft failed") - return self._tensor_from_node(out.value) - - def mel_filter_bank(self, num_frequency_bins, num_mel_filters, - min_frequency, max_frequency, sampling_rate, - norm_type=0, scale_type=0): - """Generate a mel-scale filter bank tensor.""" - out = cactus_node_t() - rc = _lib.cactus_graph_mel_filter_bank( - self.h, - ctypes.c_size_t(int(num_frequency_bins)), - ctypes.c_size_t(int(num_mel_filters)), - ctypes.c_float(float(min_frequency)), - ctypes.c_float(float(max_frequency)), - ctypes.c_size_t(int(sampling_rate)), - ctypes.c_int(int(norm_type)), - ctypes.c_int(int(scale_type)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_mel_filter_bank failed") - return self._tensor_from_node(out.value) - - def spectrogram(self, waveform, mel_filters, frame_length, hop_length, - fft_length, power=2.0, center=True, pad_mode=0, - mel_floor=1e-10, log_mel_mode=0, dither=0.0, - preemphasis=0.0, remove_dc_offset=False): - """Compute a spectrogram from a waveform tensor.""" - waveform = self._ensure_tensor(waveform) - mel_filters = self._ensure_tensor(mel_filters) - out = cactus_node_t() - rc = _lib.cactus_graph_spectrogram( - self.h, - cactus_node_t(waveform.id), - cactus_node_t(mel_filters.id), - ctypes.c_size_t(int(frame_length)), - ctypes.c_size_t(int(hop_length)), - ctypes.c_size_t(int(fft_length)), - ctypes.c_float(float(power)), - ctypes.c_bool(bool(center)), - ctypes.c_int(int(pad_mode)), - ctypes.c_float(float(mel_floor)), - ctypes.c_int(int(log_mel_mode)), - ctypes.c_float(float(dither)), - ctypes.c_float(float(preemphasis)), - ctypes.c_bool(bool(remove_dc_offset)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_spectrogram failed") - return self._tensor_from_node(out.value) - - # ── Image processing ───────────────────────────────────────────── - - def image_preprocess(self, pixel_input, src_width, src_height, - target_width, target_height, patch_size, channels, - rescale_factor, mean, std_dev): - """Resize, normalize, and patch an image tensor. - - *mean* and *std_dev* are per-channel float sequences (length = channels). - """ - pixel_input = self._ensure_tensor(pixel_input) - mean_arr = (ctypes.c_float * len(mean))(*[float(v) for v in mean]) - std_arr = (ctypes.c_float * len(std_dev))(*[float(v) for v in std_dev]) - out = cactus_node_t() - rc = _lib.cactus_graph_image_preprocess( - self.h, cactus_node_t(pixel_input.id), - ctypes.c_int(int(src_width)), - ctypes.c_int(int(src_height)), - ctypes.c_int(int(target_width)), - ctypes.c_int(int(target_height)), - ctypes.c_int(int(patch_size)), - ctypes.c_int(int(channels)), - ctypes.c_float(float(rescale_factor)), - mean_arr, std_arr, ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_image_preprocess failed") - return self._tensor_from_node(out.value) - - # ── Introspection ──────────────────────────────────────────────── - - def output_info(self, x): - x = self._ensure_tensor(x) - return self._get_output_info(x.id) - - def _binary(self, fn_name, a, b): - a = self._ensure_tensor(a) - b = self._ensure_tensor(b) - out = cactus_node_t() - rc = getattr(_lib, fn_name)(self.h, cactus_node_t(a.id), cactus_node_t(b.id), ctypes.byref(out)) - if rc != 0: - raise RuntimeError(f"{fn_name} failed") - return self._tensor_from_node(out.value) - - def _ensure_tensor(self, x): - if not isinstance(x, Tensor): - raise TypeError("expected Tensor") - if x.g is not self: - raise ValueError("tensor belongs to a different graph") - return x - - def _get_output_info(self, node_id): - info = cactus_tensor_info_t() - rc = _lib.cactus_graph_get_output_info(self.h, cactus_node_t(node_id), ctypes.byref(info)) - if rc != 0: - raise RuntimeError("graph_get_output_info failed") - shape = tuple(int(info.shape[i]) for i in range(int(info.rank))) - return { - "precision": int(info.precision), - "rank": int(info.rank), - "shape": shape, - "num_elements": int(info.num_elements), - "byte_size": int(info.byte_size), - } - - def _tensor_from_node(self, node_id): - meta = self._get_output_info(node_id) - return Tensor(self, int(node_id), meta["shape"], meta["precision"]) - - def _coerce_input_array(self, data, precision): - if isinstance(data, Tensor): - arr = data.numpy() - else: - arr = np.asarray(data) - if precision == self.INT8: - arr = np.ascontiguousarray(arr, dtype=np.int8) - elif precision == self.FP16: - arr = np.ascontiguousarray(arr, dtype=np.float16) - elif precision == self.FP32: - arr = np.ascontiguousarray(arr, dtype=np.float32) - elif precision in (self.CQ1, self.CQ2, self.CQ3, self.CQ4): - arr = np.ascontiguousarray(arr, dtype=np.uint8) - else: - raise RuntimeError("unsupported precision") - return arr - - -class Tensor: - def __init__(self, g, node_id, shape, dtype): - self.g = g - self.id = int(node_id) - self.shape = tuple(shape) - self.dtype = int(dtype) - - def __add__(self, other): - return self.g.add(self, other) - - def __sub__(self, other): - return self.g.subtract(self, other) - - def __mul__(self, other): - return self.g.multiply(self, other) - - def __truediv__(self, other): - return self.g.divide(self, other) - - def __ne__(self, other): - if isinstance(other, Tensor): - return self.g.not_equal(self, other) - return self.g.scalar_not_equal(self, other) - - def abs(self): - return self.g.abs(self) - - def pow(self, exponent): - return self.g.pow(self, exponent) - - def precision_cast(self, dtype): - return self.g.precision_cast(self, dtype) - - def quantize_activations(self): - return self.g.quantize_activations(self) - - def scalar_add(self, value): - return self.g.scalar_add(self, value) - - def scalar_subtract(self, value): - return self.g.scalar_subtract(self, value) - - def scalar_multiply(self, value): - return self.g.scalar_multiply(self, value) - - def scalar_divide(self, value): - return self.g.scalar_divide(self, value) - - def scalar_floor_divide(self, value): - return self.g.scalar_floor_divide(self, value) - - def scalar_not_equal(self, value): - return self.g.scalar_not_equal(self, value) - - def scalar_exp(self): - return self.g.scalar_exp(self) - - def scalar_sqrt(self): - return self.g.scalar_sqrt(self) - - def scalar_cos(self): - return self.g.scalar_cos(self) - - def scalar_sin(self): - return self.g.scalar_sin(self) - - def scalar_log(self): - return self.g.scalar_log(self) - - def clamp(self, lo, hi): - return self.g.clamp(self, lo, hi) - - def relu(self): - return self.g.relu(self) - - def sigmoid(self): - return self.g.sigmoid(self) - - def tanh(self): - return self.g.tanh(self) - - def gelu(self): - return self.g.gelu(self) - - def gelu_erf(self): - return self.g.gelu_erf(self) - - def silu(self): - return self.g.silu(self) - - def view(self, shape): - return self.g.view(self, shape) - - def reshape(self, shape): - return self.g.reshape(self, shape) - - def expand(self, shape): - return self.g.expand(self, shape) - - def flatten(self, start_dim=0, end_dim=-1): - return self.g.flatten(self, start_dim=start_dim, end_dim=end_dim) - - def slice(self, axis, start, length=0): - return self.g.slice(self, axis, start, length=length) - - def index(self, index_value, axis=0): - return self.g.index(self, index_value, axis=axis) - - def transpose(self, backend=Graph.CPU): - return self.g.transpose(self, backend=backend) - - def permute(self, permutation, backend=Graph.CPU): - return self.g.permute(self, permutation, backend=backend) - - def concat(self, other, axis=0): - return self.g.concat(self, other, axis=axis) - - def cat(self, tensors, axis=0): - return self.g.cat([self] + tensors, axis=axis) - - def groupnorm(self, weight, bias, num_groups, eps=1e-5): - return self.g.groupnorm(self, weight, bias, num_groups, eps=eps) - - def layernorm(self, weight, bias=None, eps=1e-5): - return self.g.layernorm(self, weight, bias=bias, eps=eps) - - def batchnorm(self, weight, bias, running_mean, running_var, axis=1, eps=1e-5): - return self.g.batchnorm(self, weight, bias, running_mean, running_var, axis=axis, eps=eps) - - def group_norm(self, weight, bias, num_groups, eps=1e-5): - return self.groupnorm(weight, bias, num_groups, eps=eps) - - def layer_norm(self, weight, bias=None, eps=1e-5): - return self.layernorm(weight, bias=bias, eps=eps) - - def batch_norm(self, weight, bias, running_mean, running_var, axis=1, eps=1e-5): - return self.batchnorm(weight, bias, running_mean, running_var, axis=axis, eps=eps) - - def rms_norm(self, weight, eps=1e-5): - return self.g.rms_norm(self, weight, eps=eps) - - def softmax(self, axis=-1): - return self.g.softmax(self, axis) - - def glu(self, axis=-1): - return self.g.glu(self, axis=axis) - - def matmul(self, other, pretransposed_rhs=False, backend=Graph.CPU, output_dtype=None): - return self.g.matmul( - self, - other, - pretransposed_rhs=pretransposed_rhs, - backend=backend, - output_dtype=output_dtype, - ) - - def sum(self, axis): - return self.g.sum(self, axis) - - def mean(self, axis): - return self.g.mean(self, axis) - - def variance(self, axis): - return self.g.variance(self, axis) - - def min(self, axis): - return self.g.min(self, axis) - - def max(self, axis): - return self.g.max(self, axis) - - def cumsum(self, axis): - return self.g.cumsum(self, axis) - - def numpy(self): - info = cactus_tensor_info_t() - rc = _lib.cactus_graph_get_output_info(self.g.h, cactus_node_t(self.id), ctypes.byref(info)) - if rc != 0: - raise RuntimeError("graph_get_output_info failed") - - out_ptr = ctypes.c_void_p() - rc = _lib.cactus_graph_get_output_ptr(self.g.h, cactus_node_t(self.id), ctypes.byref(out_ptr)) - if rc != 0 or not out_ptr.value: - raise RuntimeError("graph_get_output_ptr failed") - - rank = int(info.rank) - shape = tuple(int(info.shape[i]) for i in range(rank)) - num_elements = int(info.num_elements) - precision = int(info.precision) - - if precision == Graph.FP16: - arr = np.ctypeslib.as_array((ctypes.c_uint16 * num_elements).from_address(out_ptr.value)).view(np.float16) - elif precision == Graph.FP32: - arr = np.ctypeslib.as_array((ctypes.c_float * num_elements).from_address(out_ptr.value)) - elif precision == Graph.INT8: - arr = np.ctypeslib.as_array((ctypes.c_int8 * num_elements).from_address(out_ptr.value)) - elif precision in (Graph.CQ1, Graph.CQ2, Graph.CQ3, Graph.CQ4): - arr = np.ctypeslib.as_array((ctypes.c_uint8 * int(info.byte_size)).from_address(out_ptr.value)) - return arr.copy() - else: - raise RuntimeError("unsupported precision") - - return arr.reshape(shape).copy() - - def __repr__(self): - return f"Tensor(id={self.id}, shape={self.shape}, dtype={self.dtype})" diff --git a/python/cactus/cli/__init__.py b/python/cactus/cli/__init__.py index 48108abe5..40d991905 100644 --- a/python/cactus/cli/__init__.py +++ b/python/cactus/cli/__init__.py @@ -4,16 +4,24 @@ from .. import __version__ from .common import ( DEFAULT_MODEL_ID, - DEFAULT_TEST_MODEL_ID, - DEFAULT_ASR_MODEL_ID, + DEFAULT_TRANSCRIPTION_MODEL_ID, + SUPPORTED_PLATFORMS, ) from .download import cmd_download + +_PLATFORM_CHOICES = ("cpu", *SUPPORTED_PLATFORMS) +_PLATFORM_HELP = ( + f"target accelerator: cpu = generic ARM (default); " + f"or one of: {', '.join(SUPPORTED_PLATFORMS) if SUPPORTED_PLATFORMS else '(none yet)'}" +) +_PLATFORM_PIPE = "|".join(_PLATFORM_CHOICES) from .compile import cmd_build -from .run import cmd_run from .serve import cmd_serve from .transcribe import cmd_transcribe -from .test import cmd_test -from .convert import cmd_convert +from .test import cmd_test, COMPONENTS +from .convert import cmd_convert, cmd_transpile +from .run import cmd_run +from .list import cmd_list from .auth import cmd_auth from .clean import cmd_clean @@ -27,15 +35,51 @@ def _telemetry_parent(): return p -# ── Parser setup ────────────────────────────────────────────────────── +def _positive_int(value): + n = int(value) + if n <= 0: + raise argparse.ArgumentTypeError(f"must be > 0, got {n}") + return n + + +def _non_negative_int(value): + n = int(value) + if n < 0: + raise argparse.ArgumentTypeError(f"must be >= 0, got {n}") + return n + + +def _port_int(value): + n = int(value) + if n < 1 or n > 65535: + raise argparse.ArgumentTypeError(f"port must be in 1..65535, got {n}") + return n + + +def _unit_float(value): + f = float(value) + if not (0.0 <= f <= 1.0): + raise argparse.ArgumentTypeError(f"must be in [0.0, 1.0], got {f}") + return f + + +def _hf_id_or_path(value): + v = (value or "").strip() + if "/" not in v: + raise argparse.ArgumentTypeError( + f"invalid model {value!r}. Use a HuggingFace id like 'openai/whisper-base' " + f"or a path like '/abs/path' or './bundle'." + ) + return v + + def create_parser(): - """Create the argument parser with all subcommands.""" parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, usage=argparse.SUPPRESS, - description=""" + description=f""" ----------------------------------------------------------------- @@ -43,97 +87,68 @@ def create_parser(): ----------------------------------------------------------------- - cactus run chat playground for any model - auto-converts and transpiles if needed - default model: LiquidAI/LFM2-VL-450M + cactus auth manage cloud API key + --status show key status + --clear remove saved key - Optional flags: + cactus run [model|path] run a model (default: {DEFAULT_MODEL_ID}) + --bits 1|2|3|4 CQ quantization (default: 4) + --platform {_PLATFORM_PIPE:<22} target accelerator (default: cpu) --image image file for VLM inference - --audio audio file (WAV) for audio chat + --audio audio file for audio chat --system system prompt --prompt send prompt immediately - --thinking enable reasoning - --token HF token (for gated models) - --reconvert force reconversion from source + --thinking enable thinking/reasoning mode + --token HuggingFace token (gated models) + --reconvert force local convert+transpile fallback - ----------------------------------------------------------------- - - cactus transcribe [model] speech-to-text transcription - default model: openai/whisper-base - - Optional flags: - --file audio file to transcribe + cactus transcribe [model] transcribe audio with a model + --file audio file to transcribe (required) --language language code (default: en) - --token HF token (for gated models) + --token HuggingFace token (gated models) --reconvert force reconversion from source - ----------------------------------------------------------------- - - cactus serve [model] OpenAI-compatible local HTTP server - serves prepared v2 bundles only - - Optional flags: - --host bind address (default: 127.0.0.1) - --port port (default: 8080) - - ----------------------------------------------------------------- - - cactus download fetch pre-converted CQ from Cactus-Compute - - Optional flags: + cactus download [model] download a pre-built bundle (default: {DEFAULT_MODEL_ID}) --bits 1|2|3|4 CQ quantization (default: 4) - --token HuggingFace API token + --platform {_PLATFORM_PIPE:<22} target accelerator (default: cpu) + --token HuggingFace token - ----------------------------------------------------------------- - - cactus convert [dir] convert model to CQ format - (pre-converted if available, else - built from source) - - Optional flags: + cactus convert [dir] convert HuggingFace weights to CQ --bits 1|2|3|4 CQ quantization (default: 4) - --token HuggingFace API token + --token HuggingFace token --reconvert force build from source + --lora merge a LoRA adapter before converting - ----------------------------------------------------------------- + cactus transpile build a runnable bundle from CQ weights + --weights-dir path to CQ weights (default: weights/) + --task force task type (default: auto) + --artifact-dir write bundle here (default: weights/) - cactus build builds cactus for ARM chips - output: build/libcactus.a - - Optional flags: - --apple build for Apple (iOS/macOS) - --android build for Android - --python build shared lib for Python FFI - - ----------------------------------------------------------------- - - cactus test runs unit tests and benchmarks - all must pass for contributions + cactus serve [model] OpenAI-compatible local HTTP server + --host bind address (default: 127.0.0.1) + --port port (default: 8080) - Optional flags: - --model default: LiquidAI/LFM2-VL-450M - --token HuggingFace API token - --suite run a specific test suite - --reconvert force reconversion from source + cactus list list local converted weights and bundles + + 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: {DEFAULT_MODEL_ID} + --transcription-model default: {DEFAULT_TRANSCRIPTION_MODEL_ID} + --suite run a single test suite from any + component (kernels, graph, or 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 auth manage Cactus Cloud API key - shows status and prompts to set key - - Optional flags: - --status show key status without prompting - --clear remove the saved API key - - ----------------------------------------------------------------- - - cactus clean removes all build artifacts - - ----------------------------------------------------------------- - - cactus --help shows these instructions + cactus clean delete build artifacts + cactus --help show this help ----------------------------------------------------------------- """ @@ -150,33 +165,38 @@ def create_parser(): parser._action_groups = [] - # ── download ────────────────────────────────────────────────────── download_parser = subparsers.add_parser("download", - help="Fetch pre-converted CQ weights from huggingface.co/Cactus-Compute") + help="Download a pre-built bundle from huggingface.co/Cactus-Compute") download_parser.add_argument("model_id", nargs="?", default=DEFAULT_MODEL_ID, - help=f"HuggingFace model ID (default: {DEFAULT_MODEL_ID})") + type=_hf_id_or_path, + help=f"HuggingFace model id (default: {DEFAULT_MODEL_ID})") download_parser.add_argument("--bits", type=int, choices=[1, 2, 3, 4], default=4, help="CQ quantization bits (default: 4)") - download_parser.add_argument("--token", help="HuggingFace API token") + download_parser.add_argument("--platform", choices=_PLATFORM_CHOICES, default="cpu", + help=_PLATFORM_HELP) + download_parser.add_argument("--token", help="HuggingFace token") - # ── build ───────────────────────────────────────────────────────── - build_parser = subparsers.add_parser("build", help="Build the chat application") + build_parser = subparsers.add_parser("build", help="Build cactus libraries") build_group = build_parser.add_mutually_exclusive_group() build_group.add_argument("--apple", action="store_true", - help="Build for Apple platforms (iOS/macOS)") + help="Build for Apple (iOS/macOS)") build_group.add_argument("--android", action="store_true", help="Build for Android") build_group.add_argument("--python", action="store_true", help="Build shared library for Python FFI") - # ── run ─────────────────────────────────────────────────────────── - run_parser = subparsers.add_parser("run", help="Build, download (if needed), and run chat", + run_parser = subparsers.add_parser("run", help="Run a model (downloads bundle if needed)", parents=[_telemetry_parent()]) run_parser.add_argument("model_id", nargs="?", default=DEFAULT_MODEL_ID, - help=f"HuggingFace model ID (default: {DEFAULT_MODEL_ID})") - run_parser.add_argument("--token", help="HuggingFace API token") + type=_hf_id_or_path, + help=f"HuggingFace model id or local bundle path (default: {DEFAULT_MODEL_ID})") + run_parser.add_argument("--bits", type=int, choices=[1, 2, 3, 4], default=4, + help="CQ quantization bits (default: 4)") + run_parser.add_argument("--platform", choices=_PLATFORM_CHOICES, default="cpu", + help=_PLATFORM_HELP) + run_parser.add_argument("--token", help="HuggingFace token") run_parser.add_argument("--reconvert", action="store_true", - help="Force conversion from source") + help="Force local convert+transpile fallback") run_parser.add_argument("--image", help="Path to image file for VLM inference (attached to first message)") run_parser.add_argument("--audio", @@ -187,7 +207,7 @@ def create_parser(): help="Initial prompt to send immediately") run_parser.add_argument("--input-ids", default=None, help="Comma-separated token ids for transpiled causal-LM bundles") - run_parser.add_argument("--max-new-tokens", type=int, default=None, + run_parser.add_argument("--max-new-tokens", type=_positive_int, default=None, help="Maximum tokens to generate for transpiled causal-LM bundles") run_parser.add_argument("--result-json", default=None, help="Optional path to save transpiled bundle results as JSON") @@ -195,109 +215,122 @@ def create_parser(): help="Enable thinking/reasoning for models that support it") run_parser.add_argument("--no-cloud-handoff", action="store_true", help="Disable automatic cloud handoff for this run") - run_parser.add_argument("--confidence-threshold", type=float, default=None, + run_parser.add_argument("--confidence-threshold", type=_unit_float, default=None, help="Confidence threshold below which local completions may hand off to cloud") - run_parser.add_argument("--cloud-timeout-ms", type=int, default=None, + run_parser.add_argument("--cloud-timeout-ms", type=_non_negative_int, default=None, help="Maximum time to wait for cloud handoff before falling back locally") - # ── transcribe ──────────────────────────────────────────────────── - transcribe_parser = subparsers.add_parser("transcribe", help="Download ASR model and run transcription", + transcribe_parser = subparsers.add_parser("transcribe", help="Transcribe audio with a model", parents=[_telemetry_parent()]) - transcribe_parser.add_argument("model_id", nargs="?", default=DEFAULT_ASR_MODEL_ID, - help=f"HuggingFace model ID (default: {DEFAULT_ASR_MODEL_ID})") + transcribe_parser.add_argument("model_id", nargs="?", default=DEFAULT_TRANSCRIPTION_MODEL_ID, + type=_hf_id_or_path, + help=f"HuggingFace model id (default: {DEFAULT_TRANSCRIPTION_MODEL_ID})") transcribe_parser.add_argument("--file", dest="audio_file", required=True, - help="Audio file to transcribe (WAV format)") + help="Audio file to transcribe (WAV)") transcribe_parser.add_argument("--language", default="en", - help="Language code for transcription (default: en). Examples: es, fr, de, zh, ja") - transcribe_parser.add_argument("--token", help="HuggingFace API token") + help="Language code (default: en)") + transcribe_parser.add_argument("--token", help="HuggingFace token") transcribe_parser.add_argument("--force-handoff", action="store_true", help=argparse.SUPPRESS) transcribe_parser.add_argument("--reconvert", action="store_true", - help="Download original model and convert (instead of using pre-converted from Cactus-Compute)") + help="Force reconversion from source") - # ── serve ───────────────────────────────────────────────────────── - serve_parser = subparsers.add_parser("serve", help="Start OpenAI-compatible HTTP server") + serve_parser = subparsers.add_parser("serve", help="OpenAI-compatible local HTTP server") serve_parser.add_argument("model", nargs="?", default=None, - help="Prepared v2 bundle path, local model dir name, or HF model ID") + type=_hf_id_or_path, + help="HuggingFace model id (e.g. openai/whisper-base) or bundle path") serve_parser.add_argument("--host", default="127.0.0.1", help="Bind address (default: 127.0.0.1)") - serve_parser.add_argument("--port", type=int, default=8080, + serve_parser.add_argument("--port", type=_port_int, default=8080, help="Port (default: 8080)") - # ── test ────────────────────────────────────────────────────────── test_parser = subparsers.add_parser("test", help="Run the test suite") - test_parser.add_argument("--model", dest="model_id", default=DEFAULT_TEST_MODEL_ID, - help="Model to use for tests (default: Gemma4)") - test_parser.add_argument("--token", help="HuggingFace API token") - test_parser.add_argument("--android", action="store_true", - help="Run tests on Android") - test_parser.add_argument("--ios", action="store_true", - help="Run tests on iOS") - from .test import discover_suites - suites = discover_suites() - test_parser.add_argument("--suite", choices=suites or None, - help="Run a specific test suite") + test_parser.add_argument("--component", choices=COMPONENTS, default="all", + help="Component to test (default: all)") + test_parser.add_argument("--model", dest="model_id", default=None, + type=_hf_id_or_path, + help=f"HF model ID under test (default: {DEFAULT_MODEL_ID})") + test_parser.add_argument("--transcription-model", dest="transcription_model_id", default=None, + type=_hf_id_or_path, + help=f"HF transcription model ID under test (default: {DEFAULT_TRANSCRIPTION_MODEL_ID})") + test_parser.add_argument("--suite", default=None, + help="Run a single test suite by name; resolved across all components (e.g. llm → engine, performance → kernels + graph)") + test_parser.add_argument("--list", action="store_true", + help="List available components and engine tests, then exit") + test_parser.add_argument("--android", action="store_true", help="Run tests on Android") + test_parser.add_argument("--ios", action="store_true", help="Run tests on iOS") test_parser.add_argument("--enable-telemetry", action="store_true", help="Enable cloud telemetry (disabled by default in tests)") - test_parser.add_argument("--reconvert", action="store_true", - help="Download original model and convert (instead of using pre-converted from Cactus-Compute)") - # ── auth ────────────────────────────────────────────────────────── - auth_parser = subparsers.add_parser("auth", help="Manage Cactus Cloud API key") + auth_parser = subparsers.add_parser("auth", help="Manage cloud API key") auth_parser.add_argument("--clear", action="store_true", help="Remove the saved API key") auth_parser.add_argument("--status", action="store_true", - help="Show current key status without prompting") + help="Show current key status") + + subparsers.add_parser("clean", help="Delete build artifacts") - # ── clean ───────────────────────────────────────────────────────── - subparsers.add_parser("clean", help="Remove all build artifacts") + subparsers.add_parser("list", help="List local converted weights and bundles") - # ── convert ─────────────────────────────────────────────────────── - convert_parser = subparsers.add_parser("convert", help="Convert HuggingFace model to CQ format") - convert_parser.add_argument("model_id", help="HuggingFace model name") + convert_parser = subparsers.add_parser("convert", + help="Convert HuggingFace weights to CQ format") + convert_parser.add_argument("model_id", type=_hf_id_or_path, + help="HuggingFace model id (e.g. openai/whisper-base)") convert_parser.add_argument("output_dir", nargs="?", default=None, - help="Output directory (default: weights/)") + help="Output directory (default: weights/)") convert_parser.add_argument("--bits", type=int, choices=[1, 2, 3, 4], default=4, help="CQ quantization bits (default: 4)") - convert_parser.add_argument("--token", help="HuggingFace API token") - convert_parser.add_argument("--task", default="auto", - choices=["auto", "causal_lm_logits", "multimodal_causal_lm_logits", - "ctc_logits", "encoder_hidden_states", - "seq2seq_transcription", "tdt_transcription"], - help="Transpile task after conversion (default: auto)") - convert_parser.add_argument("--prompt", - help="Prompt used for causal or multimodal graph shape capture") - convert_parser.add_argument("--system-prompt", default="", - help="Optional system prompt for multimodal prompt construction") - convert_parser.add_argument("--image-file", action="append", default=[], - help="Representative image file for multimodal transpile") - convert_parser.add_argument("--audio-file", - help="Representative audio file for audio/multimodal transpile") - convert_parser.add_argument("--max-new-tokens", type=int, default=None, - help="Generation room to preallocate for causal decode graphs") - convert_parser.add_argument("--component-pipeline", default="auto", choices=["auto", "on", "off"], - help="Use split component graph transpilation when supported") - convert_parser.add_argument("--components", - help="Comma-separated component subset for component-pipeline models") - convert_parser.add_argument("--trust-remote-code", action="store_true", - help="Allow HF remote code during the transpile phase") - convert_parser.add_argument("--local-files-only", action="store_true", - help="Require HF model/processor files to already be local during transpile") + convert_parser.add_argument("--token", help="HuggingFace token") + convert_parser.add_argument("--lora", + help="Path or HF id of a LoRA adapter to merge before converting (requires `peft`)") convert_parser.add_argument("--reconvert", action="store_true", help="Force conversion from source") - convert_parser.add_argument("--npu", action="store_true", - help="Also emit CoreML .mlpackage(s) for NPU (Apple Neural Engine) audio + vision encoders") - convert_parser.add_argument("--npu-quantize", type=int, choices=[0, 4, 8], default=None, - help="Legacy override that forces BOTH audio and vision .mlpackages to the same quant (0=fp16, 4=int4, 8=int8). When unset, per-component defaults apply: audio=int8, vision=fp16.") - convert_parser.add_argument("--npu-audio-quantize", type=int, choices=[0, 4, 8], default=None, - help="Audio encoder weight quant (0=fp16, 4=int4, 8=int8). Default int8.") - convert_parser.add_argument("--npu-vision-quantize", type=int, choices=[0, 4, 8], default=None, - help="Vision encoder weight quant (0=fp16, 4=int4, 8=int8). Default fp16 — int4 is known to degrade Gemma 4 vision output.") + + transpile_parser = subparsers.add_parser("transpile", + help="Build a runnable bundle from CQ weights") + transpile_parser.add_argument("model_id", type=_hf_id_or_path, + help="HuggingFace model id (e.g. openai/whisper-base) or local PyTorch checkpoint path") + transpile_parser.add_argument("--weights-dir", + help="Path to converted CQ weights (default: weights/)") + transpile_parser.add_argument("--task", default="auto", + choices=["auto", "causal_lm_logits", "multimodal_causal_lm_logits", + "ctc_logits", "encoder_hidden_states", + "seq2seq_transcription", "tdt_transcription"], + help="Transpile task (default: auto, inferred from weights)") + transpile_parser.add_argument("--prompt", + help="Representative prompt for causal/multimodal graph shape capture") + transpile_parser.add_argument("--image-file", action="append", default=[], + help="Representative image file for multimodal transpile (repeatable)") + transpile_parser.add_argument("--audio-file", + help="Representative audio file for audio/multimodal transpile") + transpile_parser.add_argument("--max-new-tokens", type=_positive_int, default=None, + help="Generation room to preallocate for causal decode graphs") + transpile_parser.add_argument("--component-pipeline", default="auto", choices=["auto", "on", "off"], + help="Use split component graph transpilation when supported") + transpile_parser.add_argument("--components", + help="Comma-separated component subset for component-pipeline models") + transpile_parser.add_argument("--trust-remote-code", action="store_true", + help="Allow HF remote code during the transpile phase") + transpile_parser.add_argument("--local-files-only", action="store_true", + help="Require HF model/processor files to already be local during transpile") + transpile_parser.add_argument("--allow-unconverted-weights", action="store_true", + help="Transpile against an unconverted source checkpoint (skip the CQ weights check)") + transpile_parser.add_argument("--execute-after-transpile", action="store_true", + help="Run a reference execution against the produced bundle after transpiling") + transpile_parser.add_argument("--artifact-dir", + help="Output directory (default: weights/)") + transpile_parser.add_argument("--skip-reference-compare", action="store_true", + help="Skip PyTorch vs transpiled output comparison") + transpile_parser.add_argument("--no-fuse-rms-norm", action="store_true", + help="Disable RMSNorm fusion") + transpile_parser.add_argument("--no-fuse-rope", action="store_true", + help="Disable RoPE fusion") + transpile_parser.add_argument("--no-fuse-attention", action="store_true", + help="Disable attention fusion") return parser -# ── Command dispatch ────────────────────────────────────────────────── _COMMANDS = { "download": cmd_download, @@ -306,10 +339,12 @@ def create_parser(): "serve": cmd_serve, "transcribe": cmd_transcribe, "test": cmd_test, + "list": cmd_list, - "auth": cmd_auth, - "clean": cmd_clean, - "convert": cmd_convert, + "auth": cmd_auth, + "clean": cmd_clean, + "convert": cmd_convert, + "transpile": cmd_transpile, } @@ -317,7 +352,6 @@ def create_parser(): def main(): - """Main entry point for the Cactus CLI.""" from .common import is_repo_checkout parser = create_parser() diff --git a/python/cactus/cli/auth.py b/python/cactus/cli/auth.py index 6c0194b00..39c01214f 100644 --- a/python/cactus/cli/auth.py +++ b/python/cactus/cli/auth.py @@ -1,4 +1,7 @@ -from .common import print_color, mask_key, GREEN +import getpass +import sys + +from .common import print_color, mask_key, CYAN, GREEN, NC, YELLOW def cmd_auth(args): @@ -21,9 +24,13 @@ def cmd_auth(args): if args.status: return 0 + if not sys.stdin.isatty(): + print_color(YELLOW, "stdin is not a TTY; refusing interactive key entry. Set CACTUS_CLOUD_KEY env var instead.") + return 0 + print() - print("Get your cloud key at \033[1;36mhttps://www.cactuscompute.com/dashboard/api-keys\033[0m") - new_key = input("Enter new API key (press Enter to skip): ").strip() + print(f"Get your cloud key at {CYAN}https://www.cactuscompute.com/dashboard/api-keys{NC}") + new_key = getpass.getpass("Enter new API key (press Enter to skip): ").strip() if new_key: config.set_api_key(new_key) print_color(GREEN, f"API key saved: {mask_key(new_key)}") diff --git a/python/cactus/cli/clean.py b/python/cactus/cli/clean.py index 780f2e3bb..06502766f 100644 --- a/python/cactus/cli/clean.py +++ b/python/cactus/cli/clean.py @@ -22,28 +22,22 @@ def remove_if_exists(path): else: print(f"Not found: {path}") - remove_if_exists(PROJECT_ROOT / "cactus" / "build") - remove_if_exists(PROJECT_ROOT / "android" / "build") - remove_if_exists(PROJECT_ROOT / "android" / "libs") - remove_if_exists(PROJECT_ROOT / "android" / "arm64-v8a") - remove_if_exists(PROJECT_ROOT / "apple" / "build") + for apple_build in sorted((PROJECT_ROOT / "apple").glob("build*")): + remove_if_exists(apple_build) - remove_if_exists(PROJECT_ROOT / "tests" / "build") + remove_if_exists(PROJECT_ROOT / "cactus-engine" / "build") + remove_if_exists(PROJECT_ROOT / "cactus-engine" / "tests" / "build") + remove_if_exists(PROJECT_ROOT / "cactus-graph" / "build") + remove_if_exists(PROJECT_ROOT / "cactus-kernels" / "build") remove_if_exists(PROJECT_ROOT / "python" / "cactus" / "bin") remove_if_exists(PROJECT_ROOT / "venv") remove_if_exists(PROJECT_ROOT / "weights") - - telemetry_cache = Path.home() / "Library" / "Caches" / "cactus" / "telemetry" - if telemetry_cache.exists(): - print(f"Removing telemetry cache: {telemetry_cache}") - shutil.rmtree(telemetry_cache) - else: - print(f"Telemetry cache not found: {telemetry_cache}") + remove_if_exists(PROJECT_ROOT / "transpiled") print() print("Removing compiled libraries and frameworks...") @@ -107,6 +101,13 @@ def should_preserve_artifact(path): egg_count += 1 print(f"Removed {egg_count} .egg-info directories" if egg_count else "No .egg-info directories found") + pytest_cache_count = 0 + for cache_dir in PROJECT_ROOT.rglob(".pytest_cache"): + if cache_dir.is_dir(): + shutil.rmtree(cache_dir) + pytest_cache_count += 1 + print(f"Removed {pytest_cache_count} .pytest_cache directories" if pytest_cache_count else "No .pytest_cache directories found") + print() print_color(GREEN, "Clean complete!") print("All build artifacts have been removed.") diff --git a/python/cactus/cli/common.py b/python/cactus/cli/common.py index 277ef5397..fedd5c357 100644 --- a/python/cactus/cli/common.py +++ b/python/cactus/cli/common.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import os -import subprocess -import shutil +import sys from pathlib import Path SCRIPT_DIR = Path(__file__).resolve().parent @@ -21,53 +20,66 @@ def is_repo_checkout(): return _looks_like_project_root(PROJECT_ROOT) +def weights_root() -> Path: + if is_repo_checkout(): + return PROJECT_ROOT / "weights" + return Path.home() / ".cache" / "cactus" / "weights" + + +def transpiled_root() -> Path: + if is_repo_checkout(): + return PROJECT_ROOT / "transpiled" + return Path.home() / ".cache" / "cactus" / "transpiled" + + DEFAULT_MODEL_ID = "LiquidAI/LFM2-VL-450M" -DEFAULT_TEST_MODEL_ID = "LiquidAI/LFM2-VL-450M" -DEFAULT_ASR_MODEL_ID = "openai/whisper-base" +DEFAULT_TRANSCRIPTION_MODEL_ID = "openai/whisper-base" + + +# Add a new vendor accelerator by appending its name here. +SUPPORTED_PLATFORMS: tuple[str, ...] = ("apple",) RED = '\033[0;31m' GREEN = '\033[0;32m' YELLOW = '\033[1;33m' BLUE = '\033[0;34m' +CYAN = '\033[1;36m' NC = '\033[0m' +def _color_enabled(): + if os.environ.get("NO_COLOR"): + return False + return sys.stdout.isatty() + + def print_color(color, message): - """Print a message with ANSI color codes.""" - print(f"{color}{message}{NC}") + if _color_enabled(): + print(f"{color}{message}{NC}") + else: + print(message) def mask_key(key): return key[:4] + "..." + key[-4:] if len(key) >= 8 else "***" -def check_command(cmd): - """Check if a command is available in PATH.""" - return shutil.which(cmd) is not None - +BIN_DIR = SCRIPT_DIR.parent / "bin" -def run_command(cmd, cwd=None): - if isinstance(cmd, str): - cmd = [cmd] - return subprocess.run(cmd, cwd=cwd) - -def prompt_for_api_key(config): - """Prompt user to set Cactus Cloud API key if not already configured.""" - api_key = config.get_api_key() +def apply_cloud_api_key_env() -> None: + from .config_utils import CactusConfig + api_key = CactusConfig().get_api_key() if api_key: - return api_key + os.environ["CACTUS_CLOUD_KEY"] = api_key - print("\n" + "="*50) - print(" Cactus Cloud Setup (Optional)") - print("="*50 + "\n") - print("Get your cloud key at \033[1;36mhttps://www.cactuscompute.com/dashboard/api-keys\033[0m") - print("to enable automatic cloud fallback.\n") - api_key = input("Your Cactus Cloud key (press Enter to skip): ").strip() - if api_key: - config.set_api_key(api_key) - print_color(GREEN, f"API key saved: {mask_key(api_key)}") - print() - return api_key +def resolve_binary(name): + path = BIN_DIR / name + if path.exists(): + return path + print_color(RED, f"{name} binary not found at {path}.") + if is_repo_checkout(): + print_color(RED, "Run `cactus build` first.") + return None diff --git a/python/cactus/cli/compile.py b/python/cactus/cli/compile.py index d3da4845e..a91b8774b 100644 --- a/python/cactus/cli/compile.py +++ b/python/cactus/cli/compile.py @@ -5,15 +5,22 @@ from .common import ( PROJECT_ROOT, - check_command, - run_command, print_color, RED, GREEN, YELLOW, BLUE, ) +def check_command(cmd): + return shutil.which(cmd) is not None + + +def run_command(cmd, cwd=None): + if isinstance(cmd, str): + cmd = [cmd] + return subprocess.run(cmd, cwd=cwd) + + def check_libcurl(): - """Check if libcurl development libraries are installed.""" if platform.system() == 'Darwin': return True @@ -36,7 +43,6 @@ def check_libcurl(): def _detect_sdl2() -> tuple[list[str], list[str]]: - """Detect SDL2 and return (compiler_flags, linker_flags).""" is_darwin = platform.system() == "Darwin" if is_darwin: @@ -51,7 +57,7 @@ def _detect_sdl2() -> tuple[list[str], list[str]]: ["-DHAVE_SDL2", f"-I{sdl2_prefix}/include", f"-I{sdl2_prefix}/include/SDL2"], [f"-L{sdl2_prefix}/lib", "-lSDL2"], ) - else: + elif check_command("pkg-config"): sdl2_check = subprocess.run(["pkg-config", "--exists", "sdl2"], capture_output=True) if sdl2_check.returncode == 0: cflags = subprocess.run(["pkg-config", "--cflags", "sdl2"], capture_output=True, text=True) @@ -71,31 +77,21 @@ def build_binary( *, sdl2: tuple[list[str], list[str]] | None = None, ) -> int: - """Compile a single C++ binary (chat or asr) against libcactus.a. - - Handles darwin vs linux, vendored curl. - Pass *sdl2=(flags, link)* to reuse a prior detection; None re-detects. - Returns 0 on success. - """ tests_dir = PROJECT_ROOT / "cactus-engine" / "tests" source = tests_dir / f"{name}.cpp" build_dir = tests_dir / "build" build_dir.mkdir(parents=True, exist_ok=True) - if not source.exists(): - print_color(RED, f"Error: {name}.cpp not found at {source}") - return 1 - is_darwin = platform.system() == "Darwin" sdl2_flags, sdl2_link = sdl2 if sdl2 is not None else _detect_sdl2() - include_dirs = [PROJECT_ROOT] - if name == "chat": - include_dirs += [ - PROJECT_ROOT / "cactus-engine", - PROJECT_ROOT / "cactus-graph", - PROJECT_ROOT / "cactus-kernels", - ] + include_dirs = [ + PROJECT_ROOT, + PROJECT_ROOT / "cactus-engine", + PROJECT_ROOT / "cactus-graph", + PROJECT_ROOT / "cactus-kernels", + PROJECT_ROOT / "cactus-kernels" / "src", + ] print(f"Compiling {name}.cpp...") @@ -148,7 +144,6 @@ def build_binary( def cmd_build(args): - """Build the Cactus library.""" if args.apple: return _build_with_script("apple", "Building Cactus for Apple platforms") if args.android: @@ -171,38 +166,27 @@ def cmd_build(args): print(" Ubuntu: sudo apt-get install libcurl4-openssl-dev") return 1 - cactus_dir = PROJECT_ROOT / "cactus" - lib_path = cactus_dir / "build" / "libcactus.a" + cactus_dir = PROJECT_ROOT / "cactus-engine" + lib_path = cactus_dir / "build" / "libcactus_engine.a" print_color(YELLOW, "Building Cactus library...") - build_script = cactus_dir / "build.sh" - if not build_script.exists(): - print_color(RED, f"Error: build.sh not found at {build_script}") - return 1 - result = run_command(str(build_script), cwd=cactus_dir) - if result.returncode != 0: + if run_command(str(cactus_dir / "build.sh"), cwd=cactus_dir).returncode != 0: print_color(RED, "Failed to build cactus library") return 1 - if not lib_path.exists(): - print_color(RED, f"Build did not produce {lib_path}") - return 1 sdl2 = _detect_sdl2() if sdl2[0]: - print_color(GREEN, "SDL2 found - chat voice input enabled") + print_color(GREEN, "SDL2 found - voice input enabled") else: - print_color(YELLOW, "SDL2 not found - chat voice input disabled") - print_color(YELLOW, "Install SDL2 to enable chat voice input: brew install sdl2 (macOS)") + print_color(YELLOW, "SDL2 not found - voice input disabled") + print_color(YELLOW, "Install SDL2 to enable voice input: brew install sdl2 (macOS)") - rc = build_binary("chat", lib_path, sdl2=sdl2) + rc = build_binary("run", lib_path, sdl2=sdl2) + if rc != 0: + return rc + rc = build_binary("transcribe", lib_path, sdl2=sdl2) if rc != 0: return rc - - asr_cpp = PROJECT_ROOT / "cactus-engine" / "tests" / "asr.cpp" - if asr_cpp.exists(): - rc = build_binary("asr", lib_path, sdl2=sdl2) - if rc != 0: - return rc print_color(GREEN, "Cactus library built successfully!") print(f"Library location: {lib_path}") @@ -211,20 +195,13 @@ def cmd_build(args): def _build_with_script(subdir, title): - """Run a platform build.sh script from the given subdirectory.""" print_color(BLUE, f"{title}...") if subdir == "apple" and platform.system() != "Darwin": print_color(RED, "Error: Apple builds require macOS") return 1 - build_script = PROJECT_ROOT / subdir / "build.sh" - if not build_script.exists(): - print_color(RED, f"Error: build.sh not found at {build_script}") - return 1 - - result = run_command(str(build_script), cwd=PROJECT_ROOT / subdir) - if result.returncode != 0: + if run_command(str(PROJECT_ROOT / subdir / "build.sh"), cwd=PROJECT_ROOT / subdir).returncode != 0: print_color(RED, f"{title} failed") return 1 @@ -233,7 +210,6 @@ def _build_with_script(subdir, title): def cmd_build_python(): - """Build Cactus shared library for Python FFI.""" print_color(BLUE, "Building Cactus for Python...") if not check_command('cmake'): @@ -242,27 +218,13 @@ def cmd_build_python(): print(" Ubuntu: sudo apt-get install cmake") return 1 - cactus_dir = PROJECT_ROOT / "cactus" - build_script = cactus_dir / "build.sh" - if not build_script.exists(): - print_color(RED, f"Error: build.sh not found at {build_script}") - return 1 - - result = run_command(str(build_script), cwd=cactus_dir) - if result.returncode != 0: + cactus_dir = PROJECT_ROOT / "cactus-engine" + if run_command(str(cactus_dir / "build.sh"), cwd=cactus_dir).returncode != 0: print_color(RED, "Build failed") return 1 - if platform.system() == "Darwin": - lib_name = "libcactus.dylib" - else: - lib_name = "libcactus.so" - + lib_name = "libcactus_engine.dylib" if platform.system() == "Darwin" else "libcactus_engine.so" lib_path = cactus_dir / "build" / lib_name - if not lib_path.exists(): - print_color(RED, f"Shared library not found at {lib_path}") - return 1 - print_color(GREEN, "Python build complete!") print(f"Library: {lib_path}") return 0 diff --git a/python/cactus/cli/config_utils.py b/python/cactus/cli/config_utils.py index d9d7411b2..24c09c1a1 100644 --- a/python/cactus/cli/config_utils.py +++ b/python/cactus/cli/config_utils.py @@ -1,5 +1,6 @@ import json import os +import tempfile from pathlib import Path @@ -8,6 +9,10 @@ def __init__(self): self.config_dir = Path.home() / ".cactus" self.config_file = self.config_dir / "config.json" self.config_dir.mkdir(exist_ok=True) + try: + os.chmod(self.config_dir, 0o700) + except OSError: + pass def load_config(self): if self.config_file.exists(): @@ -15,13 +20,25 @@ def load_config(self): return {} def save_config(self, config): - self.config_file.write_text(json.dumps(config, indent=2)) + payload = json.dumps(config, indent=2) + fd, tmp_path = tempfile.mkstemp(prefix="config.", suffix=".tmp", dir=str(self.config_dir)) + try: + with os.fdopen(fd, "w") as f: + f.write(payload) + os.chmod(tmp_path, 0o600) + os.replace(tmp_path, self.config_file) + except Exception: + try: + os.unlink(tmp_path) + except OSError: + pass + raise def get_api_key(self): env_key = os.getenv("CACTUS_CLOUD_KEY") or os.getenv("CACTUS_CLOUD_API_KEY") if env_key: return env_key - return self.load_config().get("api_key", "") + return self.load_config().get("api_key") or None def set_api_key(self, key): config = self.load_config() diff --git a/python/cactus/cli/convert.py b/python/cactus/cli/convert.py index 2eaef3c40..0ca79832f 100644 --- a/python/cactus/cli/convert.py +++ b/python/cactus/cli/convert.py @@ -1,38 +1,130 @@ -from .common import RED, print_color +import shutil +import tempfile +from pathlib import Path + +from .common import GREEN, RED, YELLOW, print_color from .download import get_weights_dir +def _merge_lora_adapter(base_model_id, lora_path, token=None): + """Merge a LoRA/PEFT adapter into the base model, save to a temp dir, return path.""" + try: + from peft import PeftModel + except ImportError: + print_color(RED, "Error: `peft` is required for LoRA merging.") + print("Install with: pip install peft") + return None + + from transformers import AutoModelForCausalLM, AutoTokenizer + + print_color(YELLOW, f"Loading base model: {base_model_id}") + base = AutoModelForCausalLM.from_pretrained( + base_model_id, token=token, trust_remote_code=True, + ) + tokenizer = AutoTokenizer.from_pretrained( + base_model_id, token=token, trust_remote_code=True, + ) + + print_color(YELLOW, f"Loading LoRA adapter: {lora_path}") + merged = PeftModel.from_pretrained(base, lora_path, token=token).merge_and_unload() + + out_dir = Path(tempfile.mkdtemp(prefix="cactus_lora_merged_")) + print_color(YELLOW, f"Saving merged model to: {out_dir}") + merged.save_pretrained(out_dir) + tokenizer.save_pretrained(out_dir) + + lora_tok = Path(lora_path) / "tokenizer_config.json" + if lora_tok.is_file(): + shutil.copy2(lora_tok, out_dir / "tokenizer_config.json") + + print_color(GREEN, "LoRA merge complete") + return str(out_dir) + + def cmd_convert(args): - """Convert a HuggingFace model to CQ format and transpile it in place.""" - from .model import ensure_bundle, TranspileOptions + """Convert a HuggingFace model to CQ format. Does not transpile — + use `cactus transpile` afterwards to build the runtime graph bundle. + """ + from .model import ensure_weights + + source_model_id = args.model_id + merged_dir = None + + if args.lora: + merged_dir = _merge_lora_adapter(args.model_id, args.lora, token=args.token) + if merged_dir is None: + return 1 + source_model_id = merged_dir output_dir = args.output_dir or str(get_weights_dir(args.model_id)) try: - ensure_bundle( - args.model_id, - bits=args.bits or 4, + ensure_weights( + source_model_id, + bits=args.bits, token=args.token, reconvert=args.reconvert, output_dir=output_dir, - transpile=TranspileOptions( - task=args.task or "auto", - prompt=args.prompt, - image_files=args.image_file or None, - audio_file=args.audio_file, - max_new_tokens=args.max_new_tokens, - component_pipeline=args.component_pipeline or "auto", - components=args.components, - system_prompt=args.system_prompt, - trust_remote_code=args.trust_remote_code, - local_files_only=args.local_files_only, - npu=getattr(args, "npu", False), - npu_quantize=getattr(args, "npu_quantize", None), - npu_audio_quantize=getattr(args, "npu_audio_quantize", None), - npu_vision_quantize=getattr(args, "npu_vision_quantize", None), - ), ) return 0 except RuntimeError as e: print_color(RED, f"Conversion error: {e}") return 1 + finally: + if merged_dir: + shutil.rmtree(merged_dir, ignore_errors=True) + + +def cmd_transpile(args): + """Build the runtime graph bundle from already-converted CQ weights.""" + from .transpile import run_transpile + from .model import _default_multimodal_assets + + extra_args = [] + if args.weights_dir: + extra_args.extend(["--weights-dir", args.weights_dir]) + if args.task and args.task != "auto": + extra_args.extend(["--task", args.task]) + if args.prompt is not None: + extra_args.extend(["--prompt", args.prompt]) + + image_files = list(args.image_file or []) + audio_file = args.audio_file + if not image_files or not audio_file: + default_images, default_audio = _default_multimodal_assets() + if not image_files: + image_files = default_images + if not audio_file and default_audio: + audio_file = default_audio + + for img in image_files: + extra_args.extend(["--image-file", img]) + if audio_file: + extra_args.extend(["--audio-file", audio_file]) + if args.max_new_tokens is not None: + extra_args.extend(["--max-new-tokens", str(args.max_new_tokens)]) + if args.component_pipeline and args.component_pipeline != "auto": + extra_args.extend(["--component-pipeline", args.component_pipeline]) + if args.components: + extra_args.extend(["--components", args.components]) + if args.trust_remote_code: + extra_args.append("--trust-remote-code") + if args.local_files_only: + extra_args.append("--local-files-only") + if args.artifact_dir: + extra_args.extend(["--artifact-dir", args.artifact_dir]) + if args.skip_reference_compare: + extra_args.append("--skip-reference-compare") + if args.no_fuse_rms_norm: + extra_args.append("--no-fuse-rms-norm") + if args.no_fuse_rope: + extra_args.append("--no-fuse-rope") + if args.no_fuse_attention: + extra_args.append("--no-fuse-attention") + + return run_transpile( + args.model_id, + extra_args=extra_args, + execute_after_transpile=args.execute_after_transpile, + allow_unconverted_weights=args.allow_unconverted_weights, + ) diff --git a/python/cactus/cli/download.py b/python/cactus/cli/download.py index 8d263cbc7..57d4f482c 100644 --- a/python/cactus/cli/download.py +++ b/python/cactus/cli/download.py @@ -1,73 +1,73 @@ -"""Download command and weights path helpers.""" +"""Download pre-transpiled bundles from huggingface.co/Cactus-Compute.""" from pathlib import Path from .common import ( BLUE, GREEN, RED, YELLOW, - PROJECT_ROOT, is_repo_checkout, print_color, + SUPPORTED_PLATFORMS, + print_color, transpiled_root, weights_root, ) -def get_model_dir_name(model_id): - """Convert HuggingFace model ID to local directory name.""" +def get_model_dir_name(model_id: str) -> str: return model_id.split("/")[-1].lower() -def _weights_root(): - if is_repo_checkout(): - return PROJECT_ROOT / "weights" - return Path.home() / ".cache" / "cactus" / "weights" +def get_weights_dir(model_id: str) -> Path: + return weights_root() / get_model_dir_name(model_id) -def get_weights_dir(model_id): - """Return the weights directory for a model.""" - return _weights_root() / get_model_dir_name(model_id) +def get_bundle_dir(model_id: str, *, bits: int = 4, platform: str | None = None) -> Path: + from .utils import variant_suffix + return transpiled_root() / f"{get_model_dir_name(model_id)}-{variant_suffix(bits, platform)}" -def ensure_model(model_id): - """Return the weights directory, downloading or converting as needed. +def resolve_platform(choice: str) -> str | None: + if choice == "cpu": + return None + if choice in SUPPORTED_PLATFORMS: + return choice + raise ValueError(f"unknown platform {choice!r}; supported: cpu, {', '.join(SUPPORTED_PLATFORMS)}") - Public API — re-exported by ``cactus/__init__.py``. - """ - from .model import ensure_weights - return ensure_weights(model_id) +def ensure_model(model_id: str) -> Path: + return download_bundle(model_id) -def download_cq_weights(model_id, *, bits=4, token=None, output_dir=None): - """Download pre-converted CQ weights from huggingface.co/Cactus-Compute. - Raises ``RuntimeError`` if no archive is published for the requested model - or bit-width. - """ +def download_bundle(model_id: str, *, bits: int = 4, platform: str | None = None, + token: str | None = None, output_dir: Path | None = None, + reconvert: bool = False) -> Path: from .utils import ( download_cq_archive, list_hf_cq_archives, resolve_archive, suggested_cq_repo, + variant_suffix, ) - weights_dir = Path(output_dir) if output_dir else get_weights_dir(model_id) - cq_repo_id = suggested_cq_repo(model_id) + repo_id = suggested_cq_repo(model_id) local_name = get_model_dir_name(model_id) + bundle_dir = Path(output_dir) if output_dir else get_bundle_dir(model_id, bits=bits, platform=platform) + label = variant_suffix(bits, platform) print() - print_color(BLUE, f"Fetching {cq_repo_id} [cq{bits}]") + print_color(BLUE, f"Fetching {repo_id} [{label}]") - archives = list_hf_cq_archives(cq_repo_id, token=token) + archives = list_hf_cq_archives(repo_id, token=token) if not archives: - raise RuntimeError(f"no CQ archives published at {cq_repo_id}") + raise RuntimeError(f"no bundles published at {repo_id}") - resolution = resolve_archive(cq_repo_id, local_name, archives, bits) - download_cq_archive(resolution, weights_dir, token=token) - print_color(GREEN, f"Ready at {weights_dir}") - return weights_dir + resolution = resolve_archive(repo_id, local_name, archives, bits, platform=platform) + download_cq_archive(resolution, bundle_dir, token=token, reconvert=reconvert) + print_color(GREEN, f"Ready at {bundle_dir}") + return bundle_dir -def cmd_download(args): - """Download pre-converted CQ weights from huggingface.co/Cactus-Compute.""" +def cmd_download(args) -> int: try: - download_cq_weights(args.model_id, bits=args.bits, token=args.token) + platform = resolve_platform(args.platform) + download_bundle(args.model_id, bits=args.bits, platform=platform, token=args.token) return 0 - except (RuntimeError, OSError) as e: + except (RuntimeError, OSError, ValueError) as e: print_color(RED, f"Download failed: {e}") - print_color(YELLOW, f"Try: cactus convert {args.model_id} --bits {args.bits}") + print_color(YELLOW, f"Try: cactus convert {args.model_id} --bits {args.bits} && cactus transpile {args.model_id}") return 1 diff --git a/python/cactus/cli/list.py b/python/cactus/cli/list.py new file mode 100644 index 000000000..aa9534753 --- /dev/null +++ b/python/cactus/cli/list.py @@ -0,0 +1,68 @@ +import stat as _stat +from pathlib import Path + +from .common import BLUE, CYAN, print_color, transpiled_root, weights_root + + +def _read_config(path): + pairs = ( + line.split("=", 1) + for line in path.read_text(errors="ignore").splitlines() + if "=" in line and not line.lstrip().startswith("#") + ) + return {k.strip(): v.strip() for k, v in pairs} + + +def _human_size(n): + for unit in ("B", "KB", "MB", "GB"): + if n < 1024 or unit == "GB": + return f"{n:.1f} {unit}" + n /= 1024 + + +def _dir_size(path): + total = 0 + for f in path.rglob("*"): + try: + st = f.lstat() + except OSError: + continue + if _stat.S_ISREG(st.st_mode): + total += st.st_size + return total + + +def _categorize(roots): + converted, transpiled = [], [] + for root in roots: + if not root.is_dir(): + continue + for p in sorted(root.iterdir()): + if not p.is_dir() or not (p / "config.txt").is_file(): + continue + cfg = _read_config(p / "config.txt") + entry = (p, cfg.get("model_type", "?"), _dir_size(p)) + if (p / "components" / "manifest.json").is_file(): + transpiled.append(entry) + else: + converted.append(entry) + return converted, transpiled + + +def _print_section(title: str, color: str, entries): + print_color(color, title) + if not entries: + print(" (none)") + return + width = max(len(p.name) for p, _, _ in entries) + for p, model_type, size in entries: + print(f" {p.name:<{width}} {model_type:<12} {_human_size(size):>10} {p.parent}") + + +def cmd_list(_args): + roots = (weights_root(), transpiled_root()) + converted, transpiled = _categorize(roots) + _print_section("Converted weights (cactus convert)", BLUE, converted) + print() + _print_section("Transpiled bundles (cactus transpile / download)", CYAN, transpiled) + return 0 diff --git a/python/cactus/cli/model.py b/python/cactus/cli/model.py index decdb5c6c..6071d46bb 100644 --- a/python/cactus/cli/model.py +++ b/python/cactus/cli/model.py @@ -8,7 +8,6 @@ from .common import GREEN, PROJECT_ROOT, YELLOW, print_color -# ── Weight download / conversion ────────────────────────────────────── def _convert_from_source(model_id, *, bits, token, weights_dir): @@ -20,7 +19,6 @@ def _convert_from_source(model_id, *, bits, token, weights_dir): "convert", "--model", model_id, "--out", str(weights_dir), "--bits", str(bits), - "--force", ] if token: cq_args.extend(["--token", token]) @@ -31,12 +29,7 @@ def _convert_from_source(model_id, *, bits, token, weights_dir): def ensure_weights(model_id, *, bits=4, token=None, reconvert=False, output_dir=None): - """Return path to CQ weights dir, downloading or converting as needed. - - Fast path: pull a pre-converted CQ archive from huggingface.co/Cactus-Compute. - Fallback: ``--reconvert`` or no archive available → build from source. - """ - from .download import get_weights_dir, download_cq_weights + from .download import get_weights_dir weights_dir = Path(output_dir) if output_dir else get_weights_dir(model_id) @@ -48,18 +41,9 @@ def ensure_weights(model_id, *, bits=4, token=None, reconvert=False, output_dir= print_color(GREEN, f"Model weights found at {weights_dir}") return weights_dir - if not reconvert: - try: - return download_cq_weights( - model_id, bits=bits, token=token, output_dir=weights_dir, - ) - except (RuntimeError, OSError) as exc: - print(f" Pre-converted CQ not available ({exc})") - return _convert_from_source(model_id, bits=bits, token=token, weights_dir=weights_dir) -# ── Transpile spec helpers ──────────────────────────────────────────── _DEFAULT_MULTIMODAL_PROMPT = ( "Respond with 2 lines. The first should be a description of the image, " @@ -78,7 +62,6 @@ class _TranspileSpec: def _spec_from_plan(plan): - """Convert a ComponentPlan into a _TranspileSpec.""" return _TranspileSpec( task=plan.task, components=tuple(plan.components or ()), @@ -89,7 +72,6 @@ def _spec_from_plan(plan): def _infer_transpile_spec(*, task, plan): - """Determine transpile parameters from task + component plan.""" if task != "auto": if plan is not None and task == plan.task: return _spec_from_plan(plan) @@ -114,7 +96,6 @@ def _infer_transpile_spec(*, task, plan): def _default_max_new_tokens(task): - """Sensible token budget per task type.""" return { "seq2seq_transcription": 128, "multimodal_causal_lm_logits": 512, @@ -141,7 +122,6 @@ def _default_audio_asset(): def _remove_stale_transpile_artifacts(output_dir): - """Clean old transpile outputs before re-transpiling.""" for relative in ( "components", "transpile_entrypoints.json", @@ -163,7 +143,6 @@ def _remove_stale_transpile_artifacts(output_dir): def _has_transpiled_bundle(path): - """Check if path contains a transpiled bundle.""" return (path / "components" / "manifest.json").exists() @@ -173,23 +152,17 @@ def _has_transpiled_bundle(path): }) -# ── Bundle preparation (weights + transpile) ────────────────────────── def resolve_bundle_dir(model_id): path = Path(model_id).expanduser() - if not path.is_dir(): - return None - if (path / "components" / "manifest.json").exists(): + if path.is_dir() and (path / "components" / "manifest.json").exists(): return path - if path.name == "components" and (path / "manifest.json").exists(): - return path.parent return None @dataclass(frozen=True) class TranspileOptions: - """Transpile-phase parameters for ensure_bundle.""" task: str = "auto" prompt: str | None = None image_files: list[str] | None = None @@ -208,8 +181,6 @@ class TranspileOptions: def ensure_bundle(model_id, *, bits=4, token=None, reconvert=False, output_dir=None, transpile=None): - """Return path to transpiled bundle, creating it if needed. - """ from .download import get_weights_dir from .transpile import run_transpile from cactus.transpile.component_plan import infer_component_plan_from_output @@ -221,22 +192,18 @@ def ensure_bundle(model_id, *, bits=4, token=None, else: output_dir = get_weights_dir(model_id) - # Step 1: ensure CQ weights exist ensure_weights( model_id, bits=bits, token=token, reconvert=reconvert, output_dir=output_dir, ) - # Step 2: skip if already transpiled if _has_transpiled_bundle(output_dir): return output_dir - # Step 3: infer transpile spec from converted output plan = infer_component_plan_from_output(str(output_dir), model_id=model_id) spec = _infer_transpile_spec(task=opts.task, plan=plan) _remove_stale_transpile_artifacts(output_dir) - # Step 4: resolve defaults for prompt, images, audio spec_prompt = opts.prompt spec_image_files = list(opts.image_files or []) spec_audio_file = opts.audio_file @@ -276,7 +243,6 @@ def ensure_bundle(model_id, *, bits=4, token=None, if effective_components is None and spec.components: effective_components = ",".join(spec.components) - # Handle audio-only tasks used_default_audio = False if spec.task in _AUDIO_TASKS and not spec_audio_file: spec_audio_file = _default_audio_asset() @@ -290,7 +256,6 @@ def ensure_bundle(model_id, *, bits=4, token=None, elif spec.task in _AUDIO_TASKS and not spec_audio_file: raise RuntimeError(f"{spec.task} transpile requires --audio-file.") - # Step 5: build transpile args and call run_transpile effective_max_new_tokens = opts.max_new_tokens or _default_max_new_tokens(spec.task) extra_args = [ diff --git a/python/cactus/cli/run.py b/python/cactus/cli/run.py index bcc049aaf..52c32ac4d 100644 --- a/python/cactus/cli/run.py +++ b/python/cactus/cli/run.py @@ -2,42 +2,37 @@ import subprocess from pathlib import Path -from .common import print_color, is_repo_checkout, RED, GREEN +from .common import GREEN, RED, YELLOW, apply_cloud_api_key_env, print_color, resolve_binary -def cmd_run(args): - from .model import ensure_bundle, resolve_bundle_dir, TranspileOptions - from .config_utils import CactusConfig +def cmd_run(args) -> int: + from .download import resolve_platform if args.no_cloud_tele: os.environ["CACTUS_NO_CLOUD_TELE"] = "1" + apply_cloud_api_key_env() - api_key = CactusConfig().get_api_key() - if api_key: - os.environ["CACTUS_CLOUD_KEY"] = api_key + if args.image: + args.image = str(Path(args.image).expanduser()) + if args.audio: + args.audio = str(Path(args.audio).expanduser()) + if args.result_json: + args.result_json = str(Path(args.result_json).expanduser()) - bundle_dir = resolve_bundle_dir(args.model_id) + platform = resolve_platform(args.platform) + bundle_dir = _resolve_or_fetch_bundle( + args.model_id, bits=args.bits, platform=platform, + token=args.token, reconvert=args.reconvert, + image=args.image, audio=args.audio, + ) if bundle_dir is None: - try: - bundle_dir = ensure_bundle( - args.model_id, - token=args.token, - reconvert=args.reconvert, - transpile=TranspileOptions( - image_files=[args.image] if args.image else None, - audio_file=args.audio, - ), - ) - except RuntimeError as e: - print_color(RED, f"Model setup failed: {e}") - return 1 - - chat = Path(__file__).resolve().parent.parent / "bin" / "chat" - if is_repo_checkout() and not chat.exists(): - print_color(RED, "Chat binary not found. Run `cactus build` first.") return 1 - cmd = [str(chat), str(bundle_dir)] + binary = resolve_binary("run") + if binary is None: + return 1 + + cmd = [str(binary), str(bundle_dir)] for flag, value in ( ("--system", args.system), ("--prompt", args.prompt), @@ -56,7 +51,37 @@ def cmd_run(args): if args.no_cloud_handoff: cmd.append("--no-cloud-handoff") - print_color(GREEN, f"Starting Cactus Chat with model: {bundle_dir}") + print_color(GREEN, f"Running: {bundle_dir}") print() - return subprocess.run(cmd).returncode + + +def _resolve_or_fetch_bundle(model_id, *, bits, platform, token, reconvert, image, audio): + from .download import download_bundle, get_bundle_dir + from .model import TranspileOptions, ensure_bundle, resolve_bundle_dir + + local = resolve_bundle_dir(model_id) + if local is not None: + return local + + cached = get_bundle_dir(model_id, bits=bits, platform=platform) + if (cached / "components" / "manifest.json").exists() and not reconvert: + return cached + + if not reconvert: + try: + return download_bundle(model_id, bits=bits, platform=platform, token=token, reconvert=reconvert) + except (RuntimeError, OSError) as exc: + print_color(YELLOW, f"HF download unavailable ({exc}); building locally") + + try: + return ensure_bundle( + model_id, bits=bits, token=token, reconvert=reconvert, + transpile=TranspileOptions( + image_files=[image] if image else None, + audio_file=audio, + ), + ) + except RuntimeError as exc: + print_color(RED, f"Model setup failed: {exc}") + return None diff --git a/python/cactus/cli/runtime.py b/python/cactus/cli/runtime.py index d96e75ec3..bf8ecaaa0 100644 --- a/python/cactus/cli/runtime.py +++ b/python/cactus/cli/runtime.py @@ -1,4 +1,3 @@ -"""Build-system helpers for the C++ runtime.""" from __future__ import annotations import platform @@ -9,98 +8,71 @@ from .common import PROJECT_ROOT, YELLOW, print_color -# ── Static library ──────────────────────────────────────────────────── - - def _static_library_path(): - return PROJECT_ROOT / "cactus" / "build" / "libcactus.a" + return PROJECT_ROOT / "cactus-engine" / "build" / "libcactus_engine.a" def ensure_library(): - """Build libcactus.a if it doesn't exist. Return its path.""" lib = _static_library_path() if lib.exists(): return lib - build_script = PROJECT_ROOT / "cactus" / "build.sh" - if not build_script.exists(): - raise RuntimeError(f"The Cactus build script is missing: {build_script}") - - result = subprocess.run([str(build_script)], cwd=PROJECT_ROOT / "cactus") - if result.returncode != 0: + build_script = PROJECT_ROOT / "cactus-engine" / "build.sh" + if subprocess.run([str(build_script)], cwd=build_script.parent).returncode != 0: raise RuntimeError("Failed to build the Cactus static runtime") - - if not lib.exists(): - raise RuntimeError( - "The Cactus build completed, but the static library was not produced.\n" - f"Expected: {lib}" - ) return lib -# ── Shared dynamic library (for transpiler Python FFI) ──────────────── - - def _python_runtime_library_path(): suffix = ".dylib" if platform.system() == "Darwin" else ".so" - bundled = Path(__file__).resolve().parent.parent / "bindings" / "lib" / f"libcactus{suffix}" + bundled = Path(__file__).resolve().parent.parent / "bindings" / "lib" / f"libcactus_engine{suffix}" if bundled.exists(): return bundled - return PROJECT_ROOT / "cactus" / "build" / f"libcactus{suffix}" + return PROJECT_ROOT / "cactus-engine" / "build" / f"libcactus_engine{suffix}" def _public_cactus_api_symbols(static_lib): - """Extract cactus_* symbols from the static library.""" - if platform.system() == "Darwin": - command = ["nm", "-gU", str(static_lib)] - else: - command = ["nm", "-g", "--defined-only", str(static_lib)] - - result = subprocess.run(command, capture_output=True, text=True) + cmd = ( + ["nm", "-gU", str(static_lib)] if platform.system() == "Darwin" + else ["nm", "-g", "--defined-only", str(static_lib)] + ) + result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: - raise RuntimeError( - "Failed to inspect the Cactus static runtime symbols.\n" - f"Command: {' '.join(command)}\n" - f"{result.stderr.strip()}" - ) - - seen = set() - symbols = [] - for line in result.stdout.splitlines(): - parts = line.split() - if not parts: - continue - symbol = parts[-1].strip() - normalized = symbol[1:] if symbol.startswith("_") else symbol - if normalized.startswith("cactus_") and symbol not in seen: - seen.add(symbol) - symbols.append(symbol) + raise RuntimeError(f"`{' '.join(cmd)}` failed: {result.stderr.strip()}") + + def is_cactus_api(symbol): + name = symbol[1:] if symbol.startswith("_") else symbol + return name.startswith("cactus_") + + symbols = sorted({ + parts[-1] for line in result.stdout.splitlines() + if (parts := line.split()) and is_cactus_api(parts[-1]) + }) if not symbols: - raise RuntimeError( - f"Could not find any public cactus_* symbols in {static_lib}" - ) + raise RuntimeError(f"No public cactus_* symbols in {static_lib}") return symbols def _link_python_runtime_library(*, static_lib, library_path): - """Link static lib into a shared .dylib/.so with exported symbols.""" build_dir = library_path.parent build_dir.mkdir(parents=True, exist_ok=True) if library_path.exists(): library_path.unlink() + compiler_name = "clang++" if platform.system() == "Darwin" else "g++" + compiler = shutil.which(compiler_name) + if compiler is None: + raise RuntimeError(f"{compiler_name} is not installed; cannot link libcactus_engine") + exported_symbols = _public_cactus_api_symbols(static_lib) if platform.system() == "Darwin": - compiler = shutil.which("clang++") or shutil.which("c++") - if not compiler: - raise RuntimeError("Failed to find a C++ compiler for linking libcactus.dylib") command = [ compiler, "-dynamiclib", "-o", str(library_path), *[f"-Wl,-u,{s}" for s in exported_symbols], str(static_lib), - "-Wl,-install_name,@rpath/libcactus.dylib", + "-Wl,-install_name,@rpath/libcactus_engine.dylib", "-lcurl", "-framework", "Accelerate", "-framework", "CoreML", @@ -110,9 +82,6 @@ def _link_python_runtime_library(*, static_lib, library_path): "-framework", "CFNetwork", ] else: - compiler = shutil.which("g++") or shutil.which("c++") - if not compiler: - raise RuntimeError("Failed to find a C++ compiler for linking libcactus.so") command = [ compiler, "-shared", @@ -122,13 +91,11 @@ def _link_python_runtime_library(*, static_lib, library_path): "-lcurl", "-pthread", "-ldl", "-lm", ] - result = subprocess.run(command, cwd=build_dir) - if result.returncode != 0 or not library_path.exists(): + if subprocess.run(command, cwd=build_dir).returncode != 0: raise RuntimeError(f"Failed to link the Cactus shared runtime: {library_path}") def ensure_python_runtime_library(): - """Build shared .dylib/.so for the transpiler. Return its path.""" library_path = _python_runtime_library_path() static_lib = _static_library_path() diff --git a/python/cactus/cli/serve.py b/python/cactus/cli/serve.py index 0a9d4fc3a..c7812fd18 100644 --- a/python/cactus/cli/serve.py +++ b/python/cactus/cli/serve.py @@ -1,22 +1,16 @@ from pathlib import Path -from .common import BLUE, GREEN, RED, PROJECT_ROOT, is_repo_checkout, print_color +from .common import BLUE, GREEN, RED, YELLOW, print_color, weights_root from .download import get_weights_dir -def _weights_root() -> Path: - if is_repo_checkout(): - return PROJECT_ROOT / "weights" - return Path.home() / ".cache" / "cactus" / "weights" - - def _resolve_model_arg(model: str | None) -> tuple[Path | None, str | None]: if not model: return None, None path = Path(model).expanduser() if path.is_dir(): return path, path.name - candidate = _weights_root() / model + candidate = weights_root() / model if candidate.is_dir(): return candidate, candidate.name hf_candidate = get_weights_dir(model) @@ -34,7 +28,7 @@ def cmd_serve(args): model_path, model_name = _resolve_model_arg(args.model) if args.model and model_path is None: print_color(RED, f"Error: model not found: {args.model}") - print("Prepare a v2 bundle first with `cactus run ` or `cactus convert `.") + print("Prepare a v2 bundle first with `cactus run ` (or `cactus convert ` then `cactus transpile `).") return 1 if model_path is not None and not _is_valid_bundle(model_path): print_color(RED, f"Error: not a valid v2 Cactus bundle: {model_path}") @@ -55,17 +49,19 @@ def cmd_serve(args): try: application = create_app( - weights_root=_weights_root(), + weights_root=weights_root(), model_path=model_path, default_model=model_name, ) except RuntimeError as exc: print_color(RED, f"Error: {exc}") - print("Prepare a v2 bundle first with `cactus run ` or `cactus convert `.") + print("Prepare a v2 bundle first with `cactus run ` (or `cactus convert ` then `cactus transpile `).") return 1 models = sorted(application.state.registry.models) print_color(GREEN, f"Available models: {', '.join(models)}") + if args.host not in {"127.0.0.1", "localhost", "::1"}: + print_color(YELLOW, f"Warning: binding to {args.host} exposes the server beyond loopback; no auth is enforced.") print_color(BLUE, f"Starting server on {args.host}:{args.port}") uvicorn.run(application, host=args.host, port=args.port, log_level="info") return 0 diff --git a/python/cactus/cli/test.py b/python/cactus/cli/test.py index adba63b07..4a347d9e3 100644 --- a/python/cactus/cli/test.py +++ b/python/cactus/cli/test.py @@ -2,67 +2,80 @@ import subprocess from .common import ( - PROJECT_ROOT, - print_color, - RED, YELLOW, BLUE, + BLUE, DEFAULT_MODEL_ID, DEFAULT_TRANSCRIPTION_MODEL_ID, PROJECT_ROOT, RED, + apply_cloud_api_key_env, print_color, ) -LAYERS = { - "kernel": PROJECT_ROOT / "cactus-kernels", - "graph": PROJECT_ROOT / "cactus-graph", -} +COMPONENTS = ("kernels", "graph", "engine", "all") -ENGINE_DIR = PROJECT_ROOT / "cactus-engine" +def _list_component_suites(component): + tests_dir = PROJECT_ROOT / f"cactus-{component}" / "tests" + if not tests_dir.exists(): + return [] + return sorted( + f.stem.removeprefix("test_") + for f in tests_dir.glob("test_*.cpp") + if f.stem != "test_utils" + ) -def discover_suites(): - suites = [] - for name, path in LAYERS.items(): - if (path / "test.sh").exists(): - suites.append(name) - for f in sorted((ENGINE_DIR / "tests").glob("test_*.cpp")): - name = f.stem.removeprefix("test_") - if name != "utils": - suites.append(name) - return suites +def _components_with_suite(suite): + return tuple(c for c in ("kernels", "graph", "engine") if suite in _list_component_suites(c)) -def cmd_test(args): - from .model import ensure_bundle, resolve_bundle_dir - - print_color(BLUE, "Running test suite...") - - if args.ios and not args.reconvert: - print_color(YELLOW, "Warning: iOS tests without --reconvert may use stale weights.") - bundle_dir = resolve_bundle_dir(args.model_id) - if bundle_dir is None: - try: - bundle_dir = ensure_bundle(args.model_id, token=args.token, reconvert=args.reconvert) - except RuntimeError as e: - print_color(RED, f"Model setup failed: {e}") - return 1 +def _component_args(component, args): + cmd = [str(PROJECT_ROOT / f"cactus-{component}" / "test.sh")] + if args.suite: + cmd.extend(["--suite", args.suite]) + if component == "engine": + cmd.extend(["--model", args.model_id or DEFAULT_MODEL_ID]) + cmd.extend(["--transcription-model", args.transcription_model_id or DEFAULT_TRANSCRIPTION_MODEL_ID]) + if args.android: + cmd.append("--android") + if args.ios: + cmd.append("--ios") + return cmd - suite = args.suite - test_cwd = LAYERS.get(suite, ENGINE_DIR) - test_script = test_cwd / "test.sh" - if not test_script.exists(): - print_color(RED, f"Test script not found: {test_script}") - return 1 +def cmd_test(args): + if getattr(args, "list", False): + print_color(BLUE, "Components:") + for c in COMPONENTS: + print(f" {c}") + print_color(BLUE, "\nSuites by component:") + for c in ("kernels", "graph", "engine"): + suites = _list_component_suites(c) + if suites: + print(f" {c}: {', '.join(suites)}") + return 0 - cmd = [str(test_script), "--model", str(bundle_dir)] - if args.android: - cmd.append("--android") - if args.ios: - cmd.append("--ios") - if suite: - cmd.extend(["--only", suite]) + if args.suite: + matches = _components_with_suite(args.suite) + if not matches: + print_color(RED, f"unknown suite '{args.suite}'.") + print_color(RED, "Run `cactus test --list` to see available suites.") + return 2 + if args.component != "all" and args.component not in matches: + print_color(RED, + f"suite '{args.suite}' does not exist in component '{args.component}'. " + f"Found in: {', '.join(matches)}.") + return 2 + targets = matches if args.component == "all" else (args.component,) + else: + targets = ("kernels", "graph", "engine") if args.component == "all" else (args.component,) + apply_cloud_api_key_env() env = os.environ.copy() if args.enable_telemetry: env.pop("CACTUS_NO_CLOUD_TELE", None) else: env["CACTUS_NO_CLOUD_TELE"] = "1" - return subprocess.run(cmd, cwd=test_cwd, env=env).returncode + for c in targets: + cmd = _component_args(c, args) + print_color(BLUE, f"$ {' '.join(cmd)}") + rc = subprocess.run(cmd, cwd=PROJECT_ROOT, env=env).returncode + if rc != 0: + return rc + return 0 diff --git a/python/cactus/cli/transcribe.py b/python/cactus/cli/transcribe.py index 4a8825dd9..870430838 100644 --- a/python/cactus/cli/transcribe.py +++ b/python/cactus/cli/transcribe.py @@ -2,12 +2,17 @@ import subprocess from pathlib import Path -from .common import is_repo_checkout, print_color, RED, GREEN +from .common import apply_cloud_api_key_env, print_color, resolve_binary, RED, GREEN def cmd_transcribe(args): from .model import ensure_bundle, resolve_bundle_dir, TranspileOptions - from .config_utils import CactusConfig + + audio_path = Path(args.audio_file).expanduser() + if not audio_path.is_file(): + print_color(RED, f"Audio file not found: {audio_path}") + return 1 + args.audio_file = str(audio_path) if args.no_cloud_tele: os.environ["CACTUS_NO_CLOUD_TELE"] = "1" @@ -17,14 +22,10 @@ def cmd_transcribe(args): else: os.environ.pop("CACTUS_FORCE_HANDOFF", None) - api_key = CactusConfig().get_api_key() - if api_key: - os.environ["CACTUS_CLOUD_KEY"] = api_key + apply_cloud_api_key_env() bundle_dir = resolve_bundle_dir(args.model_id) - if bundle_dir is not None: - print_color(GREEN, f"Using local model: {bundle_dir}") - else: + if bundle_dir is None: try: bundle_dir = ensure_bundle( args.model_id, @@ -36,16 +37,15 @@ def cmd_transcribe(args): print_color(RED, f"Model setup failed: {e}") return 1 - asr = Path(__file__).resolve().parent.parent / "bin" / "asr" - if is_repo_checkout() and not asr.exists(): - print_color(RED, "ASR binary not found. Run `cactus build` first.") + binary = resolve_binary("transcribe") + if binary is None: return 1 - cmd = [str(asr), str(bundle_dir), args.audio_file] + cmd = [str(binary), str(bundle_dir), args.audio_file] if args.language: cmd.extend(["--language", args.language]) - print_color(GREEN, f"Starting Cactus ASR with model: {args.model_id}") + print_color(GREEN, f"Starting Cactus transcription with model: {args.model_id}") print() return subprocess.run(cmd).returncode diff --git a/python/cactus/cli/transpile.py b/python/cactus/cli/transpile.py index 4a3b27a64..8e8f77cad 100644 --- a/python/cactus/cli/transpile.py +++ b/python/cactus/cli/transpile.py @@ -37,14 +37,20 @@ def run_transpile(model_id, *, extra_args, execute_after_transpile=False, extra_args = list(extra_args or []) command = [sys.executable, "-m", "cactus.transpile.hf_model", "--model-id", model_id] + default_weights_dir = get_weights_dir(model_id) + if not default_weights_dir.name: + print_color(RED, f"Error: cannot derive a unique output dir from model_id {model_id!r}.") + print_color(YELLOW, "Pass --weights-dir and --artifact-dir explicitly.") + return 1 if not _extra_args_has_option(extra_args, "--weights-dir"): - default_weights_dir = get_weights_dir(model_id) if _weights_dir_looks_transpile_ready(default_weights_dir): command.extend(["--weights-dir", str(default_weights_dir)]) elif not allow_unconverted_weights: print_color(RED, "Error: transpilation requires converted Cactus CQ weights.") print_color(YELLOW, f"Run conversion first: cactus convert {model_id}") return 1 + if not _extra_args_has_option(extra_args, "--artifact-dir"): + command.extend(["--artifact-dir", str(default_weights_dir)]) if allow_unconverted_weights: command.append("--allow-unconverted-weights") diff --git a/python/cactus/cli/utils.py b/python/cactus/cli/utils.py index 549283077..909acc7c9 100644 --- a/python/cactus/cli/utils.py +++ b/python/cactus/cli/utils.py @@ -1,4 +1,3 @@ -"""CQ archive discovery, download, and validation utilities.""" from __future__ import annotations import hashlib @@ -16,17 +15,26 @@ from typing import Iterable -CQ_BITS_RE = re.compile(r"-cq([1-4])$", re.IGNORECASE) +CQ_VARIANT_RE = re.compile(r"-cq([1-4])(?:-([a-z]+))?$", re.IGNORECASE) ARCHIVE_SUFFIXES = (".zip", ".tar", ".tar.gz", ".tgz") +def variant_suffix(bits: int, platform: str | None) -> str: + return f"cq{bits}-{platform.lower()}" if platform else f"cq{bits}" + + @dataclass(frozen=True) class CqArchive: filename: str bits: int + platform: str | None = None # None = generic CPU bundle size: int | None = None sha256: str | None = None + @property + def suffix(self) -> str: + return variant_suffix(self.bits, self.platform) + @dataclass(frozen=True) class CqResolution: @@ -38,7 +46,7 @@ class CqResolution: def suggested_cq_repo(model_id: str) -> str: name = str(model_id).strip().replace("_", "-").split("/")[-1] - name = re.sub(r"-cq\d*$", "", name, flags=re.IGNORECASE) + name = re.sub(r"-cq\d*(?:-[a-z]+)?$", "", name, flags=re.IGNORECASE) return f"Cactus-Compute/{name}" @@ -49,10 +57,13 @@ def archive_stem(filename: str) -> str: return filename -def parse_cq_bits(filename: str) -> int | None: +def parse_cq_variant(filename: str) -> tuple[int, str | None] | None: stem = archive_stem(Path(filename).name) - m = CQ_BITS_RE.search(stem) - return int(m.group(1)) if m else None + m = CQ_VARIANT_RE.search(stem) + if not m: + return None + platform = m.group(2).lower() if m.group(2) else None + return int(m.group(1)), platform def is_supported_archive(filename: str) -> bool: @@ -60,6 +71,10 @@ def is_supported_archive(filename: str) -> bool: return any(lower.endswith(suffix) for suffix in ARCHIVE_SUFFIXES) +def _platform_sort_key(platform: str | None) -> tuple[int, str]: + return (0, "") if platform is None else (1, platform) + + def archives_from_repo_files(repo_files: Iterable[str], sizes: dict[str, int] | None = None, sha256s: dict[str, str] | None = None) -> tuple[CqArchive, ...]: sizes = sizes or {} @@ -68,24 +83,26 @@ def archives_from_repo_files(repo_files: Iterable[str], sizes: dict[str, int] | for filename in repo_files: if not is_supported_archive(filename): continue - bits = parse_cq_bits(filename) - if bits is None: + variant = parse_cq_variant(filename) + if variant is None: continue - archives.append(CqArchive(filename=filename, bits=bits, + bits, platform = variant + archives.append(CqArchive(filename=filename, bits=bits, platform=platform, size=sizes.get(filename), sha256=sha256s.get(filename))) - return tuple(sorted(archives, key=lambda a: a.bits)) + return tuple(sorted(archives, key=lambda a: (a.bits, _platform_sort_key(a.platform)))) def resolve_archive(repo_id: str, local_name: str, archives: Iterable[CqArchive], - bits: int) -> CqResolution: + bits: int, platform: str | None = None) -> CqResolution: available = tuple(archives) if not available: raise RuntimeError(f"No CQ archives found in {repo_id}") - match = next((a for a in available if a.bits == bits), None) + wanted = variant_suffix(bits, platform) + match = next((a for a in available if a.bits == bits and a.platform == platform), None) if not match: - choices = ", ".join(f"cq{a.bits}" for a in available) - raise RuntimeError(f"cq{bits} not found in {repo_id}. Available: {choices}") + choices = ", ".join(a.suffix for a in available) + raise RuntimeError(f"{wanted} not found in {repo_id}. Available: {choices}") return CqResolution( repo_id=repo_id, @@ -150,11 +167,17 @@ def promote_single_root(output_dir: Path) -> None: nested.rmdir() -def validate_extracted_cq(output_dir: Path) -> None: - required = ("config.txt", "token_embeddings.weights", "vocab.txt", "tokenizer_config.txt") +def validate_extracted_bundle(output_dir: Path) -> None: + required = ( + "config.txt", + "token_embeddings.weights", + "vocab.txt", + "tokenizer_config.txt", + "components/manifest.json", + ) missing = [name for name in required if not (output_dir / name).exists()] if missing: - raise RuntimeError(f"Downloaded CQ package is missing required file(s): {', '.join(missing)}") + raise RuntimeError(f"Downloaded bundle is missing required file(s): {', '.join(missing)}") tokenizer_config = {} for line in (output_dir / "tokenizer_config.txt").read_text(encoding="utf-8").splitlines(): @@ -171,7 +194,7 @@ def validate_extracted_cq(output_dir: Path) -> None: missing_sidecars = [name for name in optional_required if not (output_dir / name).exists()] if missing_sidecars: - raise RuntimeError(f"Downloaded CQ package is missing tokenizer sidecar file(s): {', '.join(missing_sidecars)}") + raise RuntimeError(f"Downloaded bundle is missing tokenizer sidecar file(s): {', '.join(missing_sidecars)}") def sha256_file(path: Path) -> str: @@ -234,16 +257,16 @@ def write_download_metadata(output_dir: Path, resolution: CqResolution, archive_ def download_cq_archive(resolution: CqResolution, output_dir: Path, *, token=None, - revision=None, force=False, dry_run=False) -> Path: + revision=None, reconvert=False, dry_run=False) -> Path: if dry_run: return output_dir - if output_dir.exists() and force: + if output_dir.exists() and reconvert: shutil.rmtree(output_dir) - if output_dir.exists() and (output_dir / "config.txt").exists(): + if output_dir.exists() and (output_dir / "components" / "manifest.json").exists(): return output_dir if output_dir.exists() and any(output_dir.iterdir()): - raise RuntimeError(f"Output directory already exists and is not a complete CQ package: {output_dir}") + raise RuntimeError(f"Output directory already exists and is not a complete bundle: {output_dir}") try: from huggingface_hub import hf_hub_download @@ -267,7 +290,7 @@ def download_cq_archive(resolution: CqResolution, output_dir: Path, *, token=Non tmp_dir = Path(tmp) safe_extract_archive(archive_path, tmp_dir) promote_single_root(tmp_dir) - validate_extracted_cq(tmp_dir) + validate_extracted_bundle(tmp_dir) write_download_metadata(tmp_dir, resolution, archive_path) if output_dir.exists(): shutil.rmtree(output_dir) @@ -307,7 +330,10 @@ def list_hf_cq_archives(repo_id: str, *, token=None, revision=None) -> tuple[CqA sha256s[filename] = lfs["sha256"] return archives_from_repo_files(files, sizes=sizes, sha256s=sha256s) - info = HfApi().model_info(repo_id, revision=revision, token=token, files_metadata=True) + try: + info = HfApi().model_info(repo_id, revision=revision, token=token, files_metadata=True) + except Exception as exc: + raise RuntimeError(f"could not query {repo_id} on huggingface.co: {exc}") from exc files = [] sizes = {} sha256s = {} diff --git a/python/cactus/convert/cactus_adapters/tokenizer.py b/python/cactus/convert/cactus_adapters/tokenizer.py index 9e302fd47..f041ce616 100644 --- a/python/cactus/convert/cactus_adapters/tokenizer.py +++ b/python/cactus/convert/cactus_adapters/tokenizer.py @@ -7,11 +7,7 @@ hf_hub_download = None SENTENCEPIECE_MODEL_TYPES = { - 'gemma', 'gemma3n', 'llama', 'smol', 'bert', 't5', -} - -SENTENCEPIECE_MODEL_TYPES = { - 'gemma', 'gemma3n', 'llama', 'gemma4', 'smol', 'bert', 't5', + 'gemma', 'gemma3n', 'gemma4', 'llama', 'smol', 'bert', 't5', } BPE_MODEL_TYPES = { diff --git a/python/cactus/convert/calibration/hessian.py b/python/cactus/convert/calibration/hessian.py index 72b4550d0..ad6ffa165 100644 --- a/python/cactus/convert/calibration/hessian.py +++ b/python/cactus/convert/calibration/hessian.py @@ -60,18 +60,20 @@ def hook(_module, inputs, _name=name): handles.append(module.register_forward_pre_hook(hook)) if not handles or not texts: return stats - model.eval() - model.to(device) - with torch.no_grad(): - for text in texts: - batch = tokenizer(text, return_tensors="pt", truncation=True, max_length=max_length) - batch = {k: v.to(device) for k, v in batch.items()} - try: - model(**batch) - except Exception: - continue - for h in handles: - h.remove() + try: + model.eval() + model.to(device) + with torch.no_grad(): + for text in texts: + batch = tokenizer(text, return_tensors="pt", truncation=True, max_length=max_length) + batch = {k: v.to(device) for k, v in batch.items()} + try: + model(**batch) + except Exception: + continue + finally: + for h in handles: + h.remove() for name, count in list(stats.samples.items()): if count > 0: stats.hessians[name] = stats.hessians[name] / float(count) diff --git a/python/cactus/convert/cli.py b/python/cactus/convert/cli.py index c94d6022c..68c4f4916 100644 --- a/python/cactus/convert/cli.py +++ b/python/cactus/convert/cli.py @@ -59,12 +59,15 @@ def _load_hf(model_id_or_path: str, device: str): try: cfg = AutoConfig.from_pretrained(model_id_or_path, trust_remote_code=True, local_files_only=Path(model_id_or_path).exists()) except Exception: - if Path(model_id_or_path).exists(): - cfg_path = Path(model_id_or_path) / "config.json" - else: - from huggingface_hub import hf_hub_download - cfg_path = Path(hf_hub_download(model_id_or_path, "config.json", cache_dir=_hf_cache_dir())) - cfg = json.loads(cfg_path.read_text(encoding="utf-8")) + try: + if Path(model_id_or_path).exists(): + cfg_path = Path(model_id_or_path) / "config.json" + else: + from huggingface_hub import hf_hub_download + cfg_path = Path(hf_hub_download(model_id_or_path, "config.json", cache_dir=_hf_cache_dir())) + cfg = json.loads(cfg_path.read_text(encoding="utf-8")) + except Exception as exc: + raise RuntimeError(f"Could not load model {model_id_or_path!r}: {exc}") from exc family = detect_family(cfg, "auto") adapter = adapter_for_family(family) processor = adapter.load_processor(model_id_or_path) diff --git a/python/cactus/convert/export/qdq.py b/python/cactus/convert/export/qdq.py index 87eec6f62..5c73081d6 100644 --- a/python/cactus/convert/export/qdq.py +++ b/python/cactus/convert/export/qdq.py @@ -18,7 +18,6 @@ from ..model_adapters.detection import detect_family from ..model_adapters.adapters import adapter_for_family -from ..model_adapters.naming import restore_hf_key_for_family CACTUS_MAGIC = b"CACT" HEADER_SIZE = 84 @@ -449,13 +448,6 @@ def load_conversion_manifest(cactus_root: Path) -> list[dict[str, Any]]: return rows -def output_key_for_row(row: dict[str, Any], family: str) -> str: - key = row.get("hf_name") or row.get("source_name") - if not key: - raise ValueError(f"manifest row has no hf/source key: {row}") - return restore_hf_key_for_family(str(key), family) - - def output_keys_for_row(row: dict[str, Any], family: str) -> list[str]: return adapter_for_family(family).qdq_output_keys(row) diff --git a/python/cactus/server.py b/python/cactus/server.py index cfab62ca2..b7d55c117 100644 --- a/python/cactus/server.py +++ b/python/cactus/server.py @@ -296,20 +296,23 @@ def _translate_tools(tools: list[Tool] | None, tool_choice) -> tuple[list[dict[s return translated, tool_choice == "required" -def _make_tool_calls(function_calls: list[Any]) -> list[dict[str, Any]]: +def _make_tool_calls(function_calls: list[Any], *, with_index: bool = False) -> list[dict[str, Any]]: out = [] - for call in function_calls: + for i, call in enumerate(function_calls): if not isinstance(call, dict): continue args = call.get("arguments", {}) - out.append({ + entry: dict[str, Any] = { "id": f"call_{uuid.uuid4().hex[:24]}", "type": "function", "function": { "name": call.get("name", ""), "arguments": json.dumps(args) if isinstance(args, dict) else str(args), }, - }) + } + if with_index: + entry["index"] = len(out) + out.append(entry) return out @@ -341,7 +344,6 @@ def _build_chat_response(result: dict[str, Any], model_id: str, request_id: str) "object": "chat.completion", "created": int(time.time()), "model": model_id, - "system_fingerprint": None, "choices": [{ "index": 0, "message": message, @@ -391,7 +393,6 @@ async def run_inference(): "object": "chat.completion.chunk", "created": created, "model": req.model, - "system_fingerprint": None, "choices": [{"index": 0, "delta": {"role": "assistant", "content": ""}, "logprobs": None, "finish_reason": None}], }) @@ -405,7 +406,6 @@ async def run_inference(): "object": "chat.completion.chunk", "created": created, "model": req.model, - "system_fingerprint": None, "choices": [{"index": 0, "delta": {"content": value}, "logprobs": None, "finish_reason": None}], }) elif kind == "done": @@ -414,12 +414,23 @@ async def run_inference(): await task if error is not None: - yield f"event: error\ndata: {json.dumps({'error': str(error)})}\n\n" + # OpenAI streaming spec embeds errors in a `data:` line with an `error` + # object; clients (openai-python, vercel ai) parse `event:` lines as + # comments. We also emit a final chunk so the client gets a + # finish_reason before [DONE] and never hangs. + yield f"data: {json.dumps({'error': {'message': str(error), 'type': 'server_error'}})}\n\n" + yield _event({ + "id": request_id, + "object": "chat.completion.chunk", + "created": created, + "model": req.model, + "choices": [{"index": 0, "delta": {}, "logprobs": None, "finish_reason": "stop"}], + }) yield "data: [DONE]\n\n" return - function_calls = result.get("function_calls") or [] - tool_calls = _make_tool_calls(function_calls) + function_calls = (result or {}).get("function_calls") or [] + tool_calls = _make_tool_calls(function_calls, with_index=True) finish_reason = "tool_calls" if tool_calls else "stop" if tool_calls: yield _event({ @@ -427,7 +438,6 @@ async def run_inference(): "object": "chat.completion.chunk", "created": created, "model": req.model, - "system_fingerprint": None, "choices": [{"index": 0, "delta": {"tool_calls": tool_calls}, "logprobs": None, "finish_reason": None}], }) final = { @@ -435,12 +445,11 @@ async def run_inference(): "object": "chat.completion.chunk", "created": created, "model": req.model, - "system_fingerprint": None, "choices": [{"index": 0, "delta": {}, "logprobs": None, "finish_reason": finish_reason}], "usage": { - "prompt_tokens": int(result.get("prefill_tokens") or 0), - "completion_tokens": int(result.get("decode_tokens") or 0), - "total_tokens": int(result.get("total_tokens") or 0), + "prompt_tokens": int((result or {}).get("prefill_tokens") or 0), + "completion_tokens": int((result or {}).get("decode_tokens") or 0), + "total_tokens": int((result or {}).get("total_tokens") or 0), }, } yield _event(final) @@ -591,10 +600,13 @@ async def create_transcription( return { "task": "transcribe", "language": language or "", - "duration": segments[-1]["end"] if segments else 0.0, + "duration": float(segments[-1].get("end", 0.0)) if segments else 0.0, "text": text, "segments": [ - {"id": i, "start": seg["start"], "end": seg["end"], "text": seg["text"]} + {"id": i, + "start": float(seg.get("start", 0.0)), + "end": float(seg.get("end", 0.0)), + "text": seg.get("text", "")} for i, seg in enumerate(segments) ], } diff --git a/python/cactus/transpile/README.md b/python/cactus/transpile/README.md index a5a3ef4e2..e2af2deb2 100644 --- a/python/cactus/transpile/README.md +++ b/python/cactus/transpile/README.md @@ -57,30 +57,18 @@ Two important details: - The CLI only has an explicit `--execute-after-transpile` flag; most other transpile options are forwarded as unknown args to `hf_model.py`. -### `cactus run-transpiled` +### `cactus run` -Saved bundles are executed through `cmd_run_transpiled()` in `python/cactus/cli/transpile.py`: +`cactus run ` is the single user-facing entry. Resolution +order (`python/cactus/cli/run.py:_resolve_or_fetch_bundle`): -```python -def cmd_run_transpiled(args): - transpile_lib = _ensure_python_runtime_library() - os.environ["CACTUS_LIB_PATH"] = str(transpile_lib) - from .transpile.component_bundle_runtime import run_transpiled_bundle - - result = run_transpiled_bundle( - bundle_dir, - audio_file=getattr(args, "audio_file", None) or getattr(args, "audio", None), - image_files=tuple(image_values), - prompt=getattr(args, "prompt", None), - input_ids=getattr(args, "input_ids", None), - weights_dir=getattr(args, "weights_dir", None), - system_prompt=getattr(args, "system", None), - enable_thinking=bool(getattr(args, "thinking", False)), - ) -``` +1. `` is an existing path with `components/manifest.json` → run as-is. +2. Cached download at `transpiled/-cq[-]/` → run. +3. Fresh download from `huggingface.co/Cactus-Compute` via `download_bundle`. +4. Fallback: local `ensure_bundle` (convert + transpile for custom models not on HF). -`cactus run ` also auto-detects transpiled bundles by looking for a -`manifest.json` and dispatches to the same runtime path. +Bundle handoff is to the native `run` binary (`python/cactus/bin/run`), which +loads the bundle via `cactus_init()`. ## End-To-End Timeline @@ -512,7 +500,7 @@ Gemma4 gets extra handling in this pass: ## 12. Lower IR Into A Cactus `Graph` `lower.py` is the last compiler stage. It maps `IRValue`s and `IRNode`s into the -runtime `Graph` object from `cactus.bindings.graph`. +runtime `Graph` object from `cactus.bindings.cactus`. The high-level structure is: @@ -665,55 +653,20 @@ unless `--skip-reference-compare` is set. ## 15. Run A Saved Bundle Later -`component_bundle_runtime.py` is the runtime-side loader for saved bundles. +Saved bundles are executed by the native C++ `run` binary, not by Python. The +Python CLI just resolves the bundle path and exec's the binary: -The public entry is: - -```python -def run_transpiled_bundle(bundle_dir_or_manifest, *, audio_file=None, image_files=(), prompt=None, input_ids=None, weights_dir=None, ...): - bundle_root, manifest = load_component_bundle_manifest(bundle_dir_or_manifest) - component_graphs, manifest = load_saved_component_graphs( - bundle_dir_or_manifest, - weights_dir=resolved_weights_dir, - ) - ... ``` - -There are two ways a saved component can be reloaded: - -1. Load a serialized `.cactus` graph and rebind its constants. -2. Rebuild from saved raw/optimized IR JSON and re-run lowering. - -That second path is handled by `_load_component_graph_from_ir()`: - -```python -ir_graph = _deserialize_saved_ir_graph(...) -if use_raw_ir: - canonicalize_exported_graph(ir_graph) - optimize_graph(ir_graph) -else: - if family == "gemma4" and component in {"decoder", "decoder_step"}: - optimize_graph(ir_graph) -transpiled = transpile_preoptimized_ir(ir_graph) +cactus run + → python/cactus/cli/run.py:cmd_run + → subprocess.run([run_bin, bundle_dir, ...]) + → run invokes cactus_init(bundle_dir) from libcactus_engine ``` -So saved IR artifacts are not just for inspection; they are also a fallback loading -format that can be re-lowered on demand. - -### Bound constant rebinding at runtime - -The runtime loader reattaches Cactus `.weights` tensor files with -`graph.set_external_input(...)`: - -```python -tensor = graph._tensor_from_node(node_id) -graph.set_external_input(tensor, int(tensor_file.data.ctypes.data), dtype=tensor_file.precision) -... -if tensor_file.scales is not None and tensor_file.group_size > 0: - _lib.cactus_graph_set_grouped_scales(...) -``` - -That is why the binding manifests are essential. +The runtime reads `components/manifest.json` from the bundle directory, mmaps +the `.weights` files, and loads the serialized `.cactus` graphs. Per-platform +`.mlpackage` files (Apple CoreML / future vendor accelerators) are dispatched +to the matching hardware when present. ## Major Situations And How They Are Handled @@ -844,7 +797,6 @@ Notes: | --- | --- | | `audio_preprocess.py` | WAV loading, resampling, generic log-mel extraction, Cactus audio frontend wrappers, Parakeet-native features, Gemma4-native audio features. | | `capture_pytorch.py` | `torch.export` capture wrapper, export-safe kwargs injection, metadata collection, shape propagation, and `CapturedModel` creation. | -| `component_bundle_runtime.py` | Loader and executor for saved transpiled bundles; reloads `.cactus` graphs or re-lowers saved IR, then runs task-specific bundle executors. | | `component_partition.py` | Heuristic component labeling of IR nodes/values and subgraph extraction into `audio_encoder` / `vision_encoder` / `lm_encoder` / `decoder`. | | `component_pipeline.py` | Capture/optimize/lower loop for one component spec, plus runtime execution of a staged pipeline of components. | | `graph_ir.py` | Definitions for `IRValue`, `IRNode`, `IRGraph`, deep-copy behavior, and IR consistency verification. | @@ -993,36 +945,32 @@ python -m cactus.transpile.hf_model --help ## 3. Run A Saved Transpiled Bundle -Use the bundle root directory or a `manifest.json` path: +Use the bundle root directory: ```bash -cactus run-transpiled /path/to/transpiled/model +cactus run /path/to/transpiled/model ``` -For direct bundle execution, audio input is passed with `--file`. The `--audio` -flag belongs to `cactus run`, which may forward into the same runtime after -bundle auto-detection. - ### Text causal LM bundle ```bash -cactus run-transpiled /path/to/transpiled/model \ +cactus run /path/to/transpiled/model \ --prompt "The capital of France is" ``` Or pass token ids directly: ```bash -cactus run-transpiled /path/to/transpiled/model \ +cactus run /path/to/transpiled/model \ --input-ids 2,13,42 ``` ### Multimodal Gemma4 bundle ```bash -cactus run-transpiled /path/to/transpiled/model \ - --image-file /path/to/image.jpg \ - --file /path/to/audio.wav \ +cactus run /path/to/transpiled/model \ + --image /path/to/image.jpg \ + --audio /path/to/audio.wav \ --prompt "What do you observe?" \ --system "Answer concisely." ``` @@ -1030,42 +978,21 @@ cactus run-transpiled /path/to/transpiled/model \ ### Audio encoder / transcription bundle ```bash -cactus run-transpiled /path/to/transpiled/model \ - --file /path/to/sample.wav -``` - -### Bundles with bound weights - -If the bundle expects converted mmap weights, pass the weights directory when the -saved paths are not directly reachable: - -```bash -cactus run-transpiled /path/to/transpiled/model \ - --weights-dir /path/to/converted_weights \ - --prompt "Hello" +cactus run /path/to/transpiled/model \ + --audio /path/to/sample.wav ``` ### Save result payload ```bash -cactus run-transpiled /path/to/transpiled/model \ +cactus run /path/to/transpiled/model \ --prompt "Hello" \ --result-json ./result.json ``` -## 4. `cactus run` Also Works - -`cactus run` detects transpiled bundles automatically: - -```bash -cactus run /path/to/transpiled/model --prompt "Hello" -``` - -If you are passing media through `cactus run`, use `--image` and `--audio` -rather than `run-transpiled`'s direct `--file` audio flag. - -Internally, `cli.py` checks whether the path contains a transpiled manifest and -redirects to `cmd_run_transpiled()`. +Internally, `cli/run.py:_resolve_or_fetch_bundle` checks whether the positional +argument is a local path with `components/manifest.json` and dispatches the +native `run` binary against it. ## Current Runtime Limitations @@ -1074,8 +1001,8 @@ As of the current code: - Saved bundle execution is implemented for `causal_lm_logits`, `multimodal_causal_lm_logits`, `encoder_hidden_states`, and `tdt_transcription`. -- `ctc_logits` can be transpiled, but `run_transpiled_bundle()` does not currently - implement a saved-bundle executor for that task. +- `ctc_logits` can be transpiled, but the runtime does not currently implement + a saved-bundle executor for that task. - The component pipeline is model-family-specific; it is not a universal split-IR path. @@ -1106,6 +1033,6 @@ If you only want the high-level lifecycle, it is: materializes everything else. 10. The script writes `raw_ir.json`, `optimized_ir.json`, `graph.cactus`, and `components/manifest.json`. -11. `cactus run-transpiled` later reloads those artifacts with - `component_bundle_runtime.py`, rebinds external tensors, and executes the - task-specific bundle runner. +11. `cactus run` later resolves the bundle path and invokes the native `run` + binary, which mmaps the weights, loads the serialized graph, and runs it + via `cactus_init()` + `cactus_complete()` (see `cactus-engine`). diff --git a/python/cactus/transpile/capture_pytorch.py b/python/cactus/transpile/capture_pytorch.py index 00190dd89..22c63833e 100644 --- a/python/cactus/transpile/capture_pytorch.py +++ b/python/cactus/transpile/capture_pytorch.py @@ -212,43 +212,55 @@ def capture_model(model, args, kwargs=None, *, strict=True) -> CapturedModel: if not normalized_args and not normalized_kwargs: raise ValueError("capture_model requires example args or kwargs") - model = model.eval() - example_args, example_kwargs = _clone_examples(normalized_args, normalized_kwargs) - transpile_metadata = _collect_transpile_metadata(model, example_args, example_kwargs) - + # nn.Module.eval() is an in-place mutation on the caller's model. Remember + # the prior training flag so capture failures don't leave the caller's + # module silently in eval mode. + was_training = bool(getattr(model, "training", False)) + model.eval() try: - with _suppress_transformers_model_output_registration(): - ep = export(model, args=example_args, kwargs=example_kwargs, strict=strict) - except Exception as exc: - raise CapturePhaseError( - "export", - f"torch.export failed for model={type(model).__name__} strict={strict}: {exc}", - cause=exc, - ) from exc - from cactus.transpile.import_ir import import_captured_to_ir - - import_graph_module = _prepare_import_graph_module(ep, example_args, example_kwargs) - - raw_captured = CapturedModel( - exported_program=ep, - graph_module=import_graph_module, - graph=import_graph_module.graph, - state_dict=dict(ep.state_dict), - ir_graph=IRGraph(values={}, nodes={}, order=[], inputs=[], outputs=[]), - source_module=model, - example_args=example_args, - example_kwargs=example_kwargs, - strict=strict, - transpile_metadata=transpile_metadata, - ) - try: - ir_graph = import_captured_to_ir(raw_captured, strict=strict) - except Exception as exc: - raise CapturePhaseError( - "import", - f"failed to import exported graph to IR for model={type(model).__name__} strict={strict}: {exc}", - cause=exc, - ) from exc + example_args, example_kwargs = _clone_examples(normalized_args, normalized_kwargs) + transpile_metadata = _collect_transpile_metadata(model, example_args, example_kwargs) + + try: + with _suppress_transformers_model_output_registration(): + ep = export(model, args=example_args, kwargs=example_kwargs, strict=strict) + except Exception as exc: + raise CapturePhaseError( + "export", + f"torch.export failed for model={type(model).__name__} strict={strict}: {exc}", + cause=exc, + ) from exc + from cactus.transpile.import_ir import import_captured_to_ir + + import_graph_module = _prepare_import_graph_module(ep, example_args, example_kwargs) + + raw_captured = CapturedModel( + exported_program=ep, + graph_module=import_graph_module, + graph=import_graph_module.graph, + state_dict=dict(ep.state_dict), + ir_graph=IRGraph(values={}, nodes={}, order=[], inputs=[], outputs=[]), + source_module=model, + example_args=example_args, + example_kwargs=example_kwargs, + strict=strict, + transpile_metadata=transpile_metadata, + ) + try: + ir_graph = import_captured_to_ir(raw_captured, strict=strict) + except Exception as exc: + raise CapturePhaseError( + "import", + f"failed to import exported graph to IR for model={type(model).__name__} strict={strict}: {exc}", + cause=exc, + ) from exc + except Exception: + if was_training: + try: + model.train() + except Exception: + pass + raise return CapturedModel( exported_program=ep, @@ -361,5 +373,3 @@ def dump_graph(captured: CapturedModel, *, include_meta: bool = True) -> str: return "\n".join(lines) -def print_graph(captured: CapturedModel, *, include_meta: bool = True) -> None: - print(dump_graph(captured, include_meta=include_meta)) diff --git a/python/cactus/transpile/hf_model.py b/python/cactus/transpile/hf_model.py index a21e73d2b..7fe99854a 100644 --- a/python/cactus/transpile/hf_model.py +++ b/python/cactus/transpile/hf_model.py @@ -2959,7 +2959,7 @@ def main() -> int: default=32, help=( "For causal_lm_logits bundles, preallocate prompt-plus-generation context " - "so run-transpiled can do greedy autoregressive decoding." + "so the saved bundle can do greedy autoregressive decoding." ), ) parser.add_argument( diff --git a/python/cactus/transpile/import_ir.py b/python/cactus/transpile/import_ir.py index c3dc44bf6..19cd05112 100644 --- a/python/cactus/transpile/import_ir.py +++ b/python/cactus/transpile/import_ir.py @@ -240,7 +240,7 @@ def _inline_wrap_with_set_grad_enabled(node: Any, shape: Any, dtype: Any) -> Non if node.op == "call_function": torch_op = format_target(node) - if "wrap_with_set_grad_enabled" in torch_op: + if torch_op.endswith(".wrap_with_set_grad_enabled") or torch_op == "wrap_with_set_grad_enabled": _inline_wrap_with_set_grad_enabled(node, shape, dtype) continue if torch_op == "": @@ -249,7 +249,13 @@ def _inline_wrap_with_set_grad_enabled(node: Any, shape: Any, dtype: Any) -> Non index_value = extract_literals(node.args[1]) if not isinstance(index_value, int): raise NotImplementedError(f"unsupported tuple getitem index: {node.args!r}") - ctx.alias_values[node.name] = ctx.tuple_aliases[source.name][index_value] + alias_seq = ctx.tuple_aliases[source.name] + if index_value < 0 or index_value >= len(alias_seq): + raise NotImplementedError( + f"tuple getitem index {index_value} out of range for " + f"inlined subgraph {source.name!r} (size {len(alias_seq)})" + ) + ctx.alias_values[node.name] = alias_seq[index_value] continue import_call_function(ir, node, ctx, shape=shape, dtype=dtype, torch_op=torch_op) continue diff --git a/python/cactus/transpile/jax_user_graph_bundle.py b/python/cactus/transpile/jax_user_graph_bundle.py index 60ab0d65d..b06dc3657 100644 --- a/python/cactus/transpile/jax_user_graph_bundle.py +++ b/python/cactus/transpile/jax_user_graph_bundle.py @@ -4,6 +4,8 @@ from dataclasses import dataclass from dataclasses import fields import json +import os +import tempfile from pathlib import Path from typing import Any @@ -131,7 +133,7 @@ def write_fp16_weights_manifest( save_tensor_with_header(np.asarray(value), weights_root / filename, precision="FP16") manifest[name] = {"filename": filename, "kind": "weight"} manifest_path = weights_root / "weights_manifest.json" - manifest_path.write_text(json.dumps(manifest, indent=2) + "\n") + _atomic_write_text(manifest_path, json.dumps(manifest, indent=2) + "\n") return manifest_path @@ -195,14 +197,31 @@ def _resolve_components_manifest(bundle_dir_or_manifest: str | Path) -> Path: raise FileNotFoundError(f"JAX user graph bundle manifest not found: {path}") +def _reject_relative_escape(value: str, *, field: str) -> None: + """Reject relative manifest entries that try to escape via `..`. Absolute + paths are a legitimate workflow (shared weights cache) and stay allowed.""" + if not value: + return + p = Path(value) + if p.is_absolute(): + return + if any(part == ".." for part in p.parts): + raise RuntimeError( + f"refusing to follow bundle manifest relative path with '..' segments: " + f"{field}={value}" + ) + + def _resolve_manifest_path(root: Path, value: object) -> str: if not isinstance(value, str) or not value: return str(root) + _reject_relative_escape(value, field="manifest path") path = Path(value).expanduser() return str(path if path.is_absolute() else root / path) def _resolve_weight_path(root: Path, weights_dir: str, value: str) -> str: + _reject_relative_escape(value, field="weight path") path = Path(value).expanduser() if path.is_absolute(): return str(path) @@ -283,9 +302,25 @@ def _ir_graph_to_dict(graph: IRGraph) -> dict[str, object]: } -def _write_json(path: Path, payload: dict[str, object]) -> None: +def _atomic_write_text(path: Path, text: str) -> None: + """Write text to path via tempfile + os.replace so a crash mid-write + cannot leave a half-written file the next loader would choke on.""" path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") + fd, tmp = tempfile.mkstemp(prefix=path.name + ".", suffix=".tmp", dir=str(path.parent)) + try: + with os.fdopen(fd, "w") as f: + f.write(text) + os.replace(tmp, path) + except Exception: + try: + os.unlink(tmp) + except OSError: + pass + raise + + +def _write_json(path: Path, payload: dict[str, object]) -> None: + _atomic_write_text(path, json.dumps(payload, indent=2, sort_keys=True) + "\n") def build_jax_user_graph_bundle( @@ -530,7 +565,7 @@ def _write_jax_user_graph_bundle( "weights_manifest": str(weights_root / "weights_manifest.json"), } manifest_path = component_root / "manifest.json" - manifest_path.write_text(json.dumps(manifest, indent=2) + "\n") + _atomic_write_text(manifest_path, json.dumps(manifest, indent=2) + "\n") return JaxUserGraphBundleResult( bundle=bundle, diff --git a/python/cactus/transpile/npu/README.md b/python/cactus/transpile/npu/README.md index 40057fb42..5c2a9c6ab 100644 --- a/python/cactus/transpile/npu/README.md +++ b/python/cactus/transpile/npu/README.md @@ -1,7 +1,10 @@ # NPU Transpiler Pipeline Emits CoreML `.mlpackage`s so the runtime engine can dispatch through the -Apple Neural Engine. Triggered by `cactus convert --npu`. Runtime side: +Apple Neural Engine. Triggered by `--npu` on the underlying transpile script +(`python -m cactus.transpile.hf_model --npu …`), or programmatically via +`TranspileOptions(npu=True)` in `cli/model.py`. The flag is not yet +surfaced on the user-facing `cactus transpile` CLI. Runtime side: `cactus-engine/src/model_npu.cpp`. **Scope: audio + vision encoders only.** Text-decoder prefill is @@ -9,8 +12,8 @@ intentionally *not* on NPU — CPU prefill is the supported path. | Entry point | Emits | manifest key | Runtime dispatch | |---|---|---|---| -| `run_encoder_pipeline` → `audio.py` | audio encoder | `npu_audio_encoder` | `audio_encode_via_npu` | -| `run_encoder_pipeline` → `vision.py` | vision encoder | `npu_vision_encoder` | `vision_encode_via_npu` | +| `run_encoder_pipeline` → `emit_audio_encoder_mlpackage` (audio.py) | audio encoder | `npu_audio_encoder` | `audio_encode_via_npu` | +| `run_encoder_pipeline` → `emit_vision_encoder_mlpackage` (vision.py) | vision encoder | `npu_vision_encoder` | `vision_encode_via_npu` | `run_encoder_pipeline` reuses the exact `ComponentModuleSpec` adapter modules + example inputs the graph transpiler captures, so the NPU encoder diff --git a/python/cactus/transpile/runtime_compat.py b/python/cactus/transpile/runtime_compat.py index 9c17c722e..028682a96 100644 --- a/python/cactus/transpile/runtime_compat.py +++ b/python/cactus/transpile/runtime_compat.py @@ -34,21 +34,19 @@ def _patched_cdll_getattr(self: ctypes.CDLL, name: str): return missing -def _load_runtime_modules(): - if "cactus.bindings.cactus" in sys.modules and "cactus.bindings.graph" in sys.modules: - return sys.modules["cactus.bindings.cactus"], sys.modules["cactus.bindings.graph"] +def _load_runtime_module(): + if "cactus.bindings.cactus" in sys.modules: + return sys.modules["cactus.bindings.cactus"] ctypes.CDLL.__getattr__ = _patched_cdll_getattr try: - cactus_module = importlib.import_module("cactus.bindings.cactus") - graph_module = importlib.import_module("cactus.bindings.graph") + return importlib.import_module("cactus.bindings.cactus") finally: ctypes.CDLL.__getattr__ = _ORIG_CDLL_GETATTR - return cactus_module, graph_module -def _patch_graph_runtime(graph_module, cactus_module) -> None: - Graph = graph_module.Graph +def _patch_graph_runtime(cactus_module) -> None: + Graph = cactus_module.Graph if getattr(Graph, "_transpile_runtime_compat_patched", False): return @@ -345,7 +343,7 @@ def expand(self, x, shape): Graph._transpile_runtime_compat_patched = True -_cactus_module, _graph_module = _load_runtime_modules() +_cactus_module = _load_runtime_module() if hasattr(_cactus_module, "_lib"): _lib_obj = _cactus_module._lib @@ -368,11 +366,11 @@ def expand(self, x, shape): ] _lib_obj.cactus_graph_gather.restype = ctypes.c_int -_patch_graph_runtime(_graph_module, _cactus_module) +_patch_graph_runtime(_cactus_module) _lib = _cactus_module._lib _err = _cactus_module._err cactus_node_t = _cactus_module.cactus_node_t cactus_tensor_info_t = _cactus_module.cactus_tensor_info_t -Graph = _graph_module.Graph -Tensor = _graph_module.Tensor +Graph = _cactus_module.Graph +Tensor = _cactus_module.Tensor diff --git a/python/cactus/transpile/weight_binding.py b/python/cactus/transpile/weight_binding.py index 6e1e7d416..68cd5992f 100644 --- a/python/cactus/transpile/weight_binding.py +++ b/python/cactus/transpile/weight_binding.py @@ -62,8 +62,8 @@ def _add(name: str) -> None: def _default_weights_dir_for_model_name(model_name_or_path: str) -> str | None: if not model_name_or_path: return None - from ..cli.download import _weights_root - root = _weights_root() + from ..cli.common import weights_root + root = weights_root() for model_dir_name in _candidate_model_dir_names(model_name_or_path): candidate = root / model_dir_name if candidate.exists(): @@ -276,10 +276,10 @@ def _load_weights_manifest(root: Path) -> dict[str, object]: return {} try: loaded_manifest = json.loads(manifest_path.read_text()) - except Exception: - return {} + except json.JSONDecodeError as exc: + raise RuntimeError(f"corrupt weights_manifest.json at {manifest_path}: {exc}") from exc if not isinstance(loaded_manifest, dict): - return {} + raise RuntimeError(f"weights_manifest.json at {manifest_path} is not a JSON object") return _flatten_convert_manifest(loaded_manifest) diff --git a/python/pyproject.toml b/python/pyproject.toml index 5e6e8cebc..4017f308e 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -39,7 +39,8 @@ dependencies = [ ] [project.optional-dependencies] -dev = ["pytest>=8.0", "httpx>=0.25.0"] +dev = ["pytest>=8.0", "httpx>=0.25.0", "einops>=0.7"] +lora = ["peft>=0.15"] serve = ["fastapi>=0.100.0", "uvicorn>=0.20.0", "python-multipart>=0.0.5"] [project.urls] @@ -64,7 +65,7 @@ include = ["cactus*"] [tool.setuptools.package-data] "cactus.bindings" = ["lib/*.dylib", "lib/*.so"] -"cactus" = ["bin/chat", "bin/asr", "assets/*"] +"cactus" = ["bin/run", "bin/transcribe", "assets/*"] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/python/src/__init__.py b/python/src/__init__.py deleted file mode 100644 index 554a1dca7..000000000 --- a/python/src/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -__version__ = "0.1.0" - - -def __getattr__(name): - if name == "main": - from .cli import main - return main - if name == "convert_hf_to_cactus": - from .cli import convert_hf_to_cactus - return convert_hf_to_cactus - if name == "get_model_dir_name": - from .downloads import get_model_dir_name - return get_model_dir_name - if name == "ensure_model": - from .downloads import ensure_model - return ensure_model - if name == "get_weights_dir": - from .downloads import get_weights_dir - return get_weights_dir - if name == "save_tensor_with_header": - from .tensor_io import save_tensor_with_header - return save_tensor_with_header - if name == "convert_hf_tokenizer": - from .tokenizer import convert_hf_tokenizer - return convert_hf_tokenizer - if name == "CactusModel": - from .cactus import CactusModel - return CactusModel - if name == "CactusIndex": - from .cactus import CactusIndex - return CactusIndex - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/python/src/__main__.py b/python/src/__main__.py deleted file mode 100644 index 9ae637f13..000000000 --- a/python/src/__main__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .cli import main - -if __name__ == "__main__": - main() diff --git a/python/src/cactus.py b/python/src/cactus.py deleted file mode 100644 index d338ba853..000000000 --- a/python/src/cactus.py +++ /dev/null @@ -1,1084 +0,0 @@ -"""Cactus Python FFI bindings.""" -import ctypes -import platform -from pathlib import Path - -TokenCallback = ctypes.CFUNCTYPE(None, ctypes.c_char_p, ctypes.c_uint32, ctypes.c_void_p) - -_DIR = Path(__file__).parent.parent.parent -if platform.system() == "Darwin": - _LIB_PATH = _DIR / "cactus" / "build" / "libcactus.dylib" -else: - _LIB_PATH = _DIR / "cactus" / "build" / "libcactus.so" - -if not _LIB_PATH.exists(): - raise RuntimeError( - f"Cactus library not found at {_LIB_PATH}\n" - f"Please build first: cactus build --python" - ) - -_lib = ctypes.CDLL(str(_LIB_PATH)) - -cactus_graph_t = ctypes.c_void_p -cactus_node_t = ctypes.c_uint64 - -class cactus_tensor_info_t(ctypes.Structure): - _fields_ = [ - ("precision", ctypes.c_int32), - ("rank", ctypes.c_size_t), - ("shape", ctypes.c_size_t * 8), - ("num_elements", ctypes.c_size_t), - ("byte_size", ctypes.c_size_t), - ] - -_lib.cactus_set_telemetry_environment.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p] -_lib.cactus_set_telemetry_environment.restype = None -_lib.cactus_set_telemetry_environment(b"python", None, None) - -_lib.cactus_init.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_bool] -_lib.cactus_init.restype = ctypes.c_void_p - -# cactus graph API -_lib.cactus_graph_create.restype = cactus_graph_t -_lib.cactus_graph_destroy.argtypes = [cactus_graph_t] -_lib.cactus_graph_hard_reset.argtypes = [cactus_graph_t] -_lib.cactus_graph_hard_reset.restype = ctypes.c_int - -_lib.cactus_graph_save.argtypes = [cactus_graph_t, ctypes.c_char_p] -_lib.cactus_graph_save.restype = ctypes.c_int - -_lib.cactus_graph_load.argtypes = [ctypes.c_char_p] -_lib.cactus_graph_load.restype = cactus_graph_t - -_lib.cactus_graph_input.argtypes = [ - cactus_graph_t, - ctypes.POINTER(ctypes.c_size_t), ctypes.c_size_t, - ctypes.c_int32, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_input.restype = ctypes.c_int - -_lib.cactus_graph_set_input.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_void_p, ctypes.c_int32 -] -_lib.cactus_graph_set_input.restype = ctypes.c_int -_lib.cactus_graph_set_external_input.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_void_p, ctypes.c_int32 -] -_lib.cactus_graph_set_external_input.restype = ctypes.c_int - -_lib.cactus_graph_add.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_add.restype = ctypes.c_int -_lib.cactus_graph_add_clipped.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_add_clipped.restype = ctypes.c_int - -_lib.cactus_graph_subtract.argtypes = [cactus_graph_t, cactus_node_t, - cactus_node_t, ctypes.POINTER(cactus_node_t)] -_lib.cactus_graph_subtract.restype = ctypes.c_int - -_lib.cactus_graph_multiply.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_multiply.restype = ctypes.c_int - -_lib.cactus_graph_divide.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_divide.restype = ctypes.c_int - -_lib.cactus_graph_precision_cast.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_int32, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_precision_cast.restype = ctypes.c_int -_lib.cactus_graph_quantize_activations.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_quantize_activations.restype = ctypes.c_int - -_lib.cactus_graph_scalar_add.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_float, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_scalar_add.restype = ctypes.c_int -_lib.cactus_graph_scalar_subtract.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_float, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_scalar_subtract.restype = ctypes.c_int -_lib.cactus_graph_scalar_multiply.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_float, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_scalar_multiply.restype = ctypes.c_int -_lib.cactus_graph_scalar_divide.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_float, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_scalar_divide.restype = ctypes.c_int -_lib.cactus_graph_scalar_exp.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_scalar_exp.restype = ctypes.c_int -_lib.cactus_graph_scalar_sqrt.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_scalar_sqrt.restype = ctypes.c_int -_lib.cactus_graph_scalar_cos.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_scalar_cos.restype = ctypes.c_int -_lib.cactus_graph_scalar_sin.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_scalar_sin.restype = ctypes.c_int -_lib.cactus_graph_scalar_log.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_scalar_log.restype = ctypes.c_int - -_lib.cactus_graph_abs.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_abs.restype = ctypes.c_int - -_lib.cactus_graph_pow.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_float, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_pow.restype = ctypes.c_int - -_lib.cactus_graph_view.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.POINTER(ctypes.c_size_t), ctypes.c_size_t, - ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_view.restype = ctypes.c_int - -_lib.cactus_graph_flatten.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_int32, ctypes.c_int32, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_flatten.restype = ctypes.c_int -_lib.cactus_graph_reshape.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.POINTER(ctypes.c_size_t), ctypes.c_size_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_reshape.restype = ctypes.c_int -_lib.cactus_graph_transpose.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_int32, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_transpose.restype = ctypes.c_int -_lib.cactus_graph_transpose_n.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.POINTER(ctypes.c_size_t), ctypes.c_size_t, ctypes.c_int32, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_transpose_n.restype = ctypes.c_int -_lib.cactus_graph_slice.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_int32, ctypes.c_size_t, ctypes.c_size_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_slice.restype = ctypes.c_int -_lib.cactus_graph_index.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_size_t, ctypes.c_int32, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_index.restype = ctypes.c_int - -_lib.cactus_graph_concat.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, ctypes.c_int32, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_concat.restype = ctypes.c_int - -_lib.cactus_graph_cat.argtypes = [ - cactus_graph_t, ctypes.POINTER(cactus_node_t), ctypes.c_size_t, ctypes.c_int32, - ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_cat.restype = ctypes.c_int -_lib.cactus_graph_matmul.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, ctypes.c_bool, ctypes.c_int32, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_matmul.restype = ctypes.c_int -_lib.cactus_graph_gather.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_gather.restype = ctypes.c_int -_lib.cactus_graph_embedding_from_tensor.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_embedding_from_tensor.restype = ctypes.c_int -_lib.cactus_graph_embedding_from_file.argtypes = [ - cactus_graph_t, ctypes.c_char_p, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_embedding_from_file.restype = ctypes.c_int -_lib.cactus_graph_mmap_embeddings.argtypes = [ - cactus_graph_t, ctypes.c_char_p, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_mmap_embeddings.restype = ctypes.c_int -_lib.cactus_graph_mmap_weights.argtypes = [ - cactus_graph_t, ctypes.c_char_p, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_mmap_weights.restype = ctypes.c_int -_lib.cactus_graph_bilinear_interpolation.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_size_t, ctypes.c_size_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_bilinear_interpolation.restype = ctypes.c_int -_lib.cactus_graph_set_grouped_scales.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_size_t, ctypes.c_size_t, ctypes.c_void_p -] -_lib.cactus_graph_set_grouped_scales.restype = ctypes.c_int -_lib.cactus_graph_set_interleaved.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_bool, ctypes.c_size_t -] -_lib.cactus_graph_set_interleaved.restype = ctypes.c_int -_lib.cactus_graph_release_weight_pages.argtypes = [cactus_graph_t, cactus_node_t] -_lib.cactus_graph_release_weight_pages.restype = ctypes.c_int -_lib.cactus_graph_prefetch_weight_pages.argtypes = [cactus_graph_t, cactus_node_t] -_lib.cactus_graph_prefetch_weight_pages.restype = ctypes.c_int -_lib.cactus_graph_release_all_weight_pages.argtypes = [cactus_graph_t] -_lib.cactus_graph_release_all_weight_pages.restype = ctypes.c_int - -_lib.cactus_graph_sum.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_int32, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_sum.restype = ctypes.c_int -_lib.cactus_graph_mean.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_int32, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_mean.restype = ctypes.c_int -_lib.cactus_graph_variance.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_int32, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_variance.restype = ctypes.c_int -_lib.cactus_graph_min.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_int32, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_min.restype = ctypes.c_int -_lib.cactus_graph_max.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_int32, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_max.restype = ctypes.c_int - -_lib.cactus_graph_relu.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_relu.restype = ctypes.c_int -_lib.cactus_graph_silu.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_silu.restype = ctypes.c_int -_lib.cactus_graph_gelu.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_gelu.restype = ctypes.c_int -_lib.cactus_graph_gelu_erf.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_gelu_erf.restype = ctypes.c_int -_lib.cactus_graph_sigmoid.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_sigmoid.restype = ctypes.c_int -_lib.cactus_graph_tanh.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_tanh.restype = ctypes.c_int -_lib.cactus_graph_glu.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_int32, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_glu.restype = ctypes.c_int - -_lib.cactus_graph_layernorm.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, cactus_node_t, ctypes.c_float, ctypes.c_bool, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_layernorm.restype = ctypes.c_int -_lib.cactus_graph_groupnorm.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, cactus_node_t, ctypes.c_size_t, ctypes.c_float, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_groupnorm.restype = ctypes.c_int -_lib.cactus_graph_batchnorm.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, cactus_node_t, cactus_node_t, cactus_node_t, ctypes.c_int32, ctypes.c_float, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_batchnorm.restype = ctypes.c_int -_lib.cactus_graph_rms_norm.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, ctypes.c_float, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_rms_norm.restype = ctypes.c_int -_lib.cactus_graph_topk.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_size_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_topk.restype = ctypes.c_int -_lib.cactus_graph_rope.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_float, ctypes.c_size_t, ctypes.c_int32, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_rope.restype = ctypes.c_int -_lib.cactus_graph_rope_gptj.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_float, ctypes.c_size_t, ctypes.c_size_t, ctypes.c_int32, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_rope_gptj.restype = ctypes.c_int -_lib.cactus_graph_softmax.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_int32, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_softmax.restype = ctypes.c_int -_lib.cactus_graph_attention.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, cactus_node_t, ctypes.c_float, ctypes.c_bool, - ctypes.c_size_t, ctypes.c_size_t, ctypes.c_int32, ctypes.c_bool, cactus_node_t, ctypes.c_bool, - ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_attention.restype = ctypes.c_int -_lib.cactus_graph_rel_pos_bias.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, ctypes.c_float, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_rel_pos_bias.restype = ctypes.c_int -_lib.cactus_graph_attention_int8_hybrid.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, cactus_node_t, ctypes.c_float, ctypes.c_size_t, - ctypes.POINTER(ctypes.c_int8), ctypes.POINTER(ctypes.c_int8), - ctypes.POINTER(ctypes.c_float), ctypes.POINTER(ctypes.c_float), - ctypes.c_size_t, ctypes.c_size_t, ctypes.c_size_t, ctypes.c_size_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_attention_int8_hybrid.restype = ctypes.c_int -_lib.cactus_graph_conv1d_causal.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, ctypes.c_size_t, ctypes.c_size_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_conv1d_causal.restype = ctypes.c_int -_lib.cactus_graph_conv1d_k3.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, ctypes.c_size_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_conv1d_k3.restype = ctypes.c_int -_lib.cactus_graph_conv1d_k7s3.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_conv1d_k7s3.restype = ctypes.c_int -_lib.cactus_graph_conv1d.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, ctypes.c_bool, cactus_node_t, ctypes.c_size_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_conv1d.restype = ctypes.c_int -_lib.cactus_graph_conv1d_same_depthwise_k9.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, ctypes.c_bool, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_conv1d_same_depthwise_k9.restype = ctypes.c_int -_lib.cactus_graph_conv1d_pointwise.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, ctypes.c_bool, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_conv1d_pointwise.restype = ctypes.c_int -_lib.cactus_graph_conv2d_k3s2p1.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, ctypes.c_bool, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_conv2d_k3s2p1.restype = ctypes.c_int -_lib.cactus_graph_conv2d_depthwise_k3s2p1.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, ctypes.c_bool, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_conv2d_depthwise_k3s2p1.restype = ctypes.c_int -_lib.cactus_graph_conv2d_pointwise_1x1.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, ctypes.c_bool, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_conv2d_pointwise_1x1.restype = ctypes.c_int -_lib.cactus_graph_lstm_cell.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, cactus_node_t, cactus_node_t, cactus_node_t, cactus_node_t, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_lstm_cell.restype = ctypes.c_int -_lib.cactus_graph_gated_deltanet_decode.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, cactus_node_t, cactus_node_t, cactus_node_t, cactus_node_t, ctypes.c_float, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_gated_deltanet_decode.restype = ctypes.c_int -_lib.cactus_graph_gated_deltanet_prefill.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, cactus_node_t, cactus_node_t, cactus_node_t, cactus_node_t, ctypes.c_size_t, ctypes.c_float, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_gated_deltanet_prefill.restype = ctypes.c_int -_lib.cactus_graph_stft.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, ctypes.c_size_t, ctypes.c_size_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_stft.restype = ctypes.c_int -_lib.cactus_graph_altup_predict.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.POINTER(cactus_node_t), ctypes.c_size_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_altup_predict.restype = ctypes.c_int -_lib.cactus_graph_altup_correct.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, ctypes.POINTER(cactus_node_t), ctypes.c_size_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_altup_correct.restype = ctypes.c_int -_lib.cactus_graph_gaussian_topk.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_float, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_gaussian_topk.restype = ctypes.c_int -_lib.cactus_graph_moe_layer_gated.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, cactus_node_t, - ctypes.POINTER(cactus_node_t), ctypes.POINTER(cactus_node_t), ctypes.POINTER(cactus_node_t), - ctypes.c_size_t, ctypes.c_size_t, ctypes.c_bool, ctypes.c_float, ctypes.c_float, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_moe_layer_gated.restype = ctypes.c_int -_lib.cactus_graph_moe_layer_ungated.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, cactus_node_t, - ctypes.POINTER(cactus_node_t), ctypes.POINTER(cactus_node_t), - ctypes.c_size_t, ctypes.c_size_t, ctypes.c_bool, ctypes.c_float, ctypes.c_float, ctypes.c_int32, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_moe_layer_ungated.restype = ctypes.c_int -_lib.cactus_graph_sample.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.c_float, ctypes.c_float, ctypes.c_size_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_sample.restype = ctypes.c_int -_lib.cactus_graph_scatter_topk.argtypes = [ - cactus_graph_t, cactus_node_t, cactus_node_t, ctypes.c_size_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_scatter_topk.restype = ctypes.c_int -_lib.cactus_graph_persistent.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.POINTER(cactus_node_t) -] -_lib.cactus_graph_persistent.restype = ctypes.c_int -_lib.cactus_graph_is_populated.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.POINTER(ctypes.c_int32) -] -_lib.cactus_graph_is_populated.restype = ctypes.c_int -_lib.cactus_graph_invalidate_persistent.argtypes = [cactus_graph_t, cactus_node_t] -_lib.cactus_graph_invalidate_persistent.restype = ctypes.c_int - -_lib.cactus_graph_execute.argtypes = [cactus_graph_t] -_lib.cactus_graph_execute.restype = ctypes.c_int - -_lib.cactus_graph_get_output_ptr.argtypes = [cactus_graph_t, cactus_node_t, - ctypes.POINTER(ctypes.c_void_p)] -_lib.cactus_graph_get_output_ptr.restype = ctypes.c_int - -_lib.cactus_graph_get_output_info.argtypes = [ - cactus_graph_t, cactus_node_t, ctypes.POINTER(cactus_tensor_info_t) -] -_lib.cactus_graph_get_output_info.restype = ctypes.c_int - -_lib.cactus_complete.argtypes = [ - ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t, - ctypes.c_char_p, ctypes.c_char_p, TokenCallback, ctypes.c_void_p, - ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t -] -_lib.cactus_complete.restype = ctypes.c_int - -_lib.cactus_prefill.argtypes = [ - ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t, - ctypes.c_char_p, ctypes.c_char_p, - ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t -] -_lib.cactus_prefill.restype = ctypes.c_int - -_lib.cactus_transcribe.argtypes = [ - ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p, - ctypes.c_size_t, ctypes.c_char_p, TokenCallback, ctypes.c_void_p, - ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t -] -_lib.cactus_transcribe.restype = ctypes.c_int - -_lib.cactus_detect_language.argtypes = [ - ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t, - ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t -] -_lib.cactus_detect_language.restype = ctypes.c_int - -_lib.cactus_embed.argtypes = [ - ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_float), - ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.c_bool -] -_lib.cactus_embed.restype = ctypes.c_int - -_lib.cactus_image_embed.argtypes = [ - ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_float), - ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t) -] -_lib.cactus_image_embed.restype = ctypes.c_int - -_lib.cactus_audio_embed.argtypes = [ - ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_float), - ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t) -] -_lib.cactus_audio_embed.restype = ctypes.c_int - -_lib.cactus_vad.argtypes = [ - ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t, - ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t -] -_lib.cactus_vad.restype = ctypes.c_int - -_lib.cactus_diarize.argtypes = [ - ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t, - ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t -] -_lib.cactus_diarize.restype = ctypes.c_int - -_lib.cactus_embed_speaker.argtypes = [ - ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t, - ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t, - ctypes.POINTER(ctypes.c_float), ctypes.c_size_t -] -_lib.cactus_embed_speaker.restype = ctypes.c_int - -_lib.cactus_reset.argtypes = [ctypes.c_void_p] -_lib.cactus_reset.restype = None - -_lib.cactus_stop.argtypes = [ctypes.c_void_p] -_lib.cactus_stop.restype = None - -_lib.cactus_destroy.argtypes = [ctypes.c_void_p] -_lib.cactus_destroy.restype = None - -_lib.cactus_get_last_error.argtypes = [] -_lib.cactus_get_last_error.restype = ctypes.c_char_p - -_lib.cactus_tokenize.argtypes = [ - ctypes.c_void_p, - ctypes.c_char_p, - ctypes.POINTER(ctypes.c_uint32), - ctypes.c_size_t, - ctypes.POINTER(ctypes.c_size_t), -] -_lib.cactus_tokenize.restype = ctypes.c_int - -_lib.cactus_score_window.argtypes = [ - ctypes.c_void_p, - ctypes.POINTER(ctypes.c_uint32), - ctypes.c_size_t, - ctypes.c_size_t, - ctypes.c_size_t, - ctypes.c_size_t, - ctypes.c_char_p, - ctypes.c_size_t, -] -_lib.cactus_score_window.restype = ctypes.c_int - -_lib.cactus_rag_query.argtypes = [ - ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, - ctypes.c_size_t, ctypes.c_size_t -] -_lib.cactus_rag_query.restype = ctypes.c_int - -_lib.cactus_stream_transcribe_start.argtypes = [ctypes.c_void_p, ctypes.c_char_p] -_lib.cactus_stream_transcribe_start.restype = ctypes.c_void_p - -_lib.cactus_stream_transcribe_process.argtypes = [ - ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t, - ctypes.c_char_p, ctypes.c_size_t -] -_lib.cactus_stream_transcribe_process.restype = ctypes.c_int - -_lib.cactus_stream_transcribe_stop.argtypes = [ - ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t -] -_lib.cactus_stream_transcribe_stop.restype = ctypes.c_int - -_lib.cactus_index_init.argtypes = [ctypes.c_char_p, ctypes.c_size_t] -_lib.cactus_index_init.restype = ctypes.c_void_p - -_lib.cactus_index_add.argtypes = [ - ctypes.c_void_p, - ctypes.POINTER(ctypes.c_int), - ctypes.POINTER(ctypes.c_char_p), - ctypes.POINTER(ctypes.c_char_p), - ctypes.POINTER(ctypes.POINTER(ctypes.c_float)), - ctypes.c_size_t, - ctypes.c_size_t -] -_lib.cactus_index_add.restype = ctypes.c_int - -_lib.cactus_index_delete.argtypes = [ - ctypes.c_void_p, - ctypes.POINTER(ctypes.c_int), - ctypes.c_size_t -] -_lib.cactus_index_delete.restype = ctypes.c_int - -_lib.cactus_index_query.argtypes = [ - ctypes.c_void_p, - ctypes.POINTER(ctypes.POINTER(ctypes.c_float)), - ctypes.c_size_t, - ctypes.c_size_t, - ctypes.c_char_p, - ctypes.POINTER(ctypes.POINTER(ctypes.c_int)), - ctypes.POINTER(ctypes.c_size_t), - ctypes.POINTER(ctypes.POINTER(ctypes.c_float)), - ctypes.POINTER(ctypes.c_size_t) -] -_lib.cactus_index_query.restype = ctypes.c_int - -_lib.cactus_index_compact.argtypes = [ctypes.c_void_p] -_lib.cactus_index_compact.restype = ctypes.c_int - -_lib.cactus_index_destroy.argtypes = [ctypes.c_void_p] -_lib.cactus_index_destroy.restype = None - -_lib.cactus_index_get.argtypes = [ - ctypes.c_void_p, - ctypes.POINTER(ctypes.c_int), - ctypes.c_size_t, - ctypes.POINTER(ctypes.c_char_p), - ctypes.POINTER(ctypes.c_size_t), - ctypes.POINTER(ctypes.c_char_p), - ctypes.POINTER(ctypes.c_size_t), - ctypes.POINTER(ctypes.POINTER(ctypes.c_float)), - ctypes.POINTER(ctypes.c_size_t) -] -_lib.cactus_index_get.restype = ctypes.c_int - -_lib.cactus_set_app_id.argtypes = [ctypes.c_char_p] -_lib.cactus_set_app_id.restype = None - -_lib.cactus_telemetry_flush.argtypes = [] -_lib.cactus_telemetry_flush.restype = None - -_lib.cactus_telemetry_shutdown.argtypes = [] -_lib.cactus_telemetry_shutdown.restype = None - -LogCallback = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_void_p) - -_lib.cactus_log_set_level.argtypes = [ctypes.c_int] -_lib.cactus_log_set_level.restype = None - -_lib.cactus_log_set_callback.argtypes = [LogCallback, ctypes.c_void_p] -_lib.cactus_log_set_callback.restype = None - - -def _enc(s): - if s is None: - return None - return s.encode() if isinstance(s, str) else s - - -def cactus_get_last_error(): - """Returns the last error message or None.""" - result = _lib.cactus_get_last_error() - return result.decode() if result else None - - -def _err(default): - return cactus_get_last_error() or default - - -def cactus_set_telemetry_environment(cache_location): - """Sets the telemetry cache directory.""" - _lib.cactus_set_telemetry_environment(None, _enc(cache_location), None) - - -def cactus_set_app_id(app_id): - """Sets the application identifier for telemetry.""" - _lib.cactus_set_app_id(_enc(app_id)) - - -def cactus_telemetry_flush(): - """Flushes pending telemetry events.""" - _lib.cactus_telemetry_flush() - - -def cactus_telemetry_shutdown(): - """Flushes and shuts down the telemetry subsystem.""" - _lib.cactus_telemetry_shutdown() - - -def cactus_init(model_path, corpus_dir, cache_index): - """Initializes a model from the given path. Returns handle.""" - handle = _lib.cactus_init(_enc(model_path), _enc(corpus_dir), cache_index) - if not handle: - raise RuntimeError(_err("Failed to initialize model")) - return handle - - -def cactus_destroy(model): - """Frees all model resources.""" - _lib.cactus_destroy(model) - - -def cactus_reset(model): - """Clears the KV cache.""" - _lib.cactus_reset(model) - - -def cactus_stop(model): - """Signals the current generation to stop.""" - _lib.cactus_stop(model) - - -def cactus_complete(model, messages_json, options_json, tools_json, callback, pcm_data=None): - """Runs chat completion. Returns response string.""" - buf = ctypes.create_string_buffer(65536) - if callback: - def _bridge(token_bytes, token_id, _): - callback(token_bytes.decode("utf-8", errors="ignore") if token_bytes else "", token_id) - cb = TokenCallback(_bridge) - else: - cb = TokenCallback() - if pcm_data is not None: - pcm_arr = (ctypes.c_uint8 * len(pcm_data))(*pcm_data) - pcm_ptr = ctypes.cast(pcm_arr, ctypes.POINTER(ctypes.c_uint8)) - pcm_size = len(pcm_data) - else: - pcm_ptr = None - pcm_size = 0 - rc = _lib.cactus_complete( - model, _enc(messages_json), buf, len(buf), - _enc(options_json), _enc(tools_json), cb, None, pcm_ptr, pcm_size - ) - if rc < 0: - raise RuntimeError(_err("Completion failed")) - return buf.value.decode("utf-8", errors="ignore") - - -def cactus_prefill(model, messages_json, options_json, tools_json, pcm_data=None): - """Prefills the KV cache with messages.""" - buf = ctypes.create_string_buffer(65536) - if pcm_data is not None: - pcm_arr = (ctypes.c_uint8 * len(pcm_data))(*pcm_data) - pcm_ptr = ctypes.cast(pcm_arr, ctypes.POINTER(ctypes.c_uint8)) - pcm_size = len(pcm_data) - else: - pcm_ptr = None - pcm_size = 0 - rc = _lib.cactus_prefill( - model, _enc(messages_json), buf, len(buf), - _enc(options_json), _enc(tools_json), pcm_ptr, pcm_size - ) - if rc < 0: - raise RuntimeError(_err("Prefill failed")) - return buf.value.decode("utf-8", errors="ignore") - -def cactus_detect_language(model, audio_path, options_json, pcm_data): - """Detects the spoken language in audio. Returns JSON string.""" - buf = ctypes.create_string_buffer(65536) - if pcm_data is not None: - pcm_arr = (ctypes.c_uint8 * len(pcm_data))(*pcm_data) - pcm_ptr = ctypes.cast(pcm_arr, ctypes.POINTER(ctypes.c_uint8)) - pcm_size = len(pcm_data) - else: - pcm_ptr = None - pcm_size = 0 - rc = _lib.cactus_detect_language( - model, _enc(audio_path), buf, len(buf), _enc(options_json), pcm_ptr, pcm_size - ) - if rc < 0: - raise RuntimeError(_err("Detect language failed")) - return buf.value.decode("utf-8", errors="ignore") - - -def cactus_transcribe(model, audio_path, prompt, options_json, callback, pcm_data): - """Transcribes audio to text. Returns transcription string.""" - buf = ctypes.create_string_buffer(65536) - if callback: - def _bridge(token_bytes, token_id, _): - callback(token_bytes.decode("utf-8", errors="ignore") if token_bytes else "", token_id) - cb = TokenCallback(_bridge) - else: - cb = TokenCallback() - if pcm_data is not None: - pcm_arr = (ctypes.c_uint8 * len(pcm_data))(*pcm_data) - pcm_ptr = ctypes.cast(pcm_arr, ctypes.POINTER(ctypes.c_uint8)) - pcm_size = len(pcm_data) - else: - pcm_ptr = None - pcm_size = 0 - rc = _lib.cactus_transcribe( - model, _enc(audio_path), _enc(prompt), buf, len(buf), - _enc(options_json), cb, None, pcm_ptr, pcm_size - ) - if rc < 0: - raise RuntimeError(_err("Transcription failed")) - return buf.value.decode("utf-8", errors="ignore") - - -def cactus_embed(model, text, normalize): - """Generates a text embedding. Returns list of floats.""" - buf = (ctypes.c_float * 4096)() - dim = ctypes.c_size_t() - rc = _lib.cactus_embed(model, _enc(text), buf, 4096, ctypes.byref(dim), normalize) - if rc < 0: - raise RuntimeError(_err("Embedding failed")) - return list(buf[:dim.value]) - - -def cactus_image_embed(model, image_path): - """Generates an image embedding. Returns list of floats.""" - buf = (ctypes.c_float * 4096)() - dim = ctypes.c_size_t() - rc = _lib.cactus_image_embed(model, _enc(image_path), buf, 4096, ctypes.byref(dim)) - if rc < 0: - raise RuntimeError(_err("Image embedding failed")) - return list(buf[:dim.value]) - - -def cactus_audio_embed(model, audio_path): - """Generates an audio embedding. Returns list of floats.""" - buf = (ctypes.c_float * 4096)() - dim = ctypes.c_size_t() - rc = _lib.cactus_audio_embed(model, _enc(audio_path), buf, 4096, ctypes.byref(dim)) - if rc < 0: - raise RuntimeError(_err("Audio embedding failed")) - return list(buf[:dim.value]) - - -def cactus_vad(model, audio_path, options_json, pcm_data): - """Runs voice activity detection. Returns JSON string.""" - buf = ctypes.create_string_buffer(65536) - if pcm_data is not None: - pcm_arr = (ctypes.c_uint8 * len(pcm_data))(*pcm_data) - pcm_ptr = ctypes.cast(pcm_arr, ctypes.POINTER(ctypes.c_uint8)) - pcm_size = len(pcm_data) - else: - pcm_ptr = None - pcm_size = 0 - rc = _lib.cactus_vad( - model, _enc(audio_path), buf, len(buf), _enc(options_json), pcm_ptr, pcm_size - ) - if rc < 0: - raise RuntimeError(_err("VAD failed")) - return buf.value.decode("utf-8", errors="ignore") - - -def cactus_diarize(model, audio_path, options_json, pcm_data): - """Runs speaker diarization. Returns JSON string.""" - buf = ctypes.create_string_buffer(1 << 20) - if pcm_data is not None: - pcm_arr = (ctypes.c_uint8 * len(pcm_data))(*pcm_data) - pcm_ptr = ctypes.cast(pcm_arr, ctypes.POINTER(ctypes.c_uint8)) - pcm_size = len(pcm_data) - else: - pcm_ptr = None - pcm_size = 0 - rc = _lib.cactus_diarize( - model, _enc(audio_path), buf, len(buf), _enc(options_json), pcm_ptr, pcm_size - ) - if rc < 0: - raise RuntimeError(_err("Diarize failed")) - return buf.value.decode("utf-8", errors="ignore") - - -def cactus_embed_speaker(model, audio_path, options_json, pcm_data, mask_weights=None): - """Extracts a speaker embedding vector. Returns JSON string.""" - buf = ctypes.create_string_buffer(65536) - if pcm_data is not None: - pcm_arr = (ctypes.c_uint8 * len(pcm_data))(*pcm_data) - pcm_ptr = ctypes.cast(pcm_arr, ctypes.POINTER(ctypes.c_uint8)) - pcm_size = len(pcm_data) - else: - pcm_ptr = None - pcm_size = 0 - if mask_weights is not None: - mask_arr = (ctypes.c_float * len(mask_weights))(*mask_weights) - mask_ptr = ctypes.cast(mask_arr, ctypes.POINTER(ctypes.c_float)) - mask_size = len(mask_weights) - else: - mask_ptr = None - mask_size = 0 - rc = _lib.cactus_embed_speaker( - model, _enc(audio_path), buf, len(buf), _enc(options_json), - pcm_ptr, pcm_size, mask_ptr, mask_size - ) - if rc < 0: - raise RuntimeError(_err("EmbedSpeaker failed")) - return buf.value.decode("utf-8", errors="ignore") - - -def cactus_tokenize(model, text): - """Tokenizes text into token IDs. Returns list of ints.""" - max_tokens = 8192 - arr = (ctypes.c_uint32 * max_tokens)() - n = ctypes.c_size_t(0) - rc = _lib.cactus_tokenize(model, _enc(text), arr, max_tokens, ctypes.byref(n)) - if rc < 0: - raise RuntimeError(_err("Tokenization failed")) - return list(arr[:n.value]) - - -def cactus_score_window(model, tokens, start, end, context): - """Scores a window of tokens. Returns JSON string.""" - buf = ctypes.create_string_buffer(65536) - token_len = len(tokens) - arr = (ctypes.c_uint32 * token_len)(*tokens) - rc = _lib.cactus_score_window(model, arr, token_len, start, end, context, buf, len(buf)) - if rc < 0: - raise RuntimeError(_err("Score window failed")) - return buf.value.decode("utf-8", errors="ignore") - - -def cactus_rag_query(model, query, top_k): - """Queries the RAG corpus. Returns JSON string.""" - buf = ctypes.create_string_buffer(65536) - rc = _lib.cactus_rag_query(model, _enc(query), buf, len(buf), top_k) - if rc < 0: - raise RuntimeError(_err("RAG query failed")) - return buf.value.decode("utf-8", errors="ignore") - - -def cactus_stream_transcribe_start(model, options_json): - """Creates a streaming transcription session. Returns stream handle.""" - handle = _lib.cactus_stream_transcribe_start(model, _enc(options_json)) - if not handle: - raise RuntimeError(_err("Failed to start stream transcription")) - return handle - - -def cactus_stream_transcribe_process(stream, pcm_data): - """Processes a chunk of PCM audio. Returns partial text.""" - buf = ctypes.create_string_buffer(65536) - pcm_arr = (ctypes.c_uint8 * len(pcm_data))(*pcm_data) - pcm_ptr = ctypes.cast(pcm_arr, ctypes.POINTER(ctypes.c_uint8)) - rc = _lib.cactus_stream_transcribe_process(stream, pcm_ptr, len(pcm_data), buf, len(buf)) - if rc < 0: - raise RuntimeError(_err("Stream process failed")) - return buf.value.decode("utf-8", errors="ignore") - - -def cactus_stream_transcribe_stop(stream): - """Finalizes transcription. Returns final text.""" - buf = ctypes.create_string_buffer(65536) - rc = _lib.cactus_stream_transcribe_stop(stream, buf, len(buf)) - if rc < 0: - raise RuntimeError(_err("Stream stop failed")) - return buf.value.decode("utf-8", errors="ignore") - - -def cactus_index_init(index_dir, embedding_dim): - """Initializes a vector index in the given directory. Returns handle.""" - handle = _lib.cactus_index_init(_enc(index_dir), embedding_dim) - if not handle: - raise RuntimeError(_err("Failed to initialize index")) - return handle - - -def cactus_index_add(index, ids, documents, embeddings, metadatas): - """Adds documents with embeddings to the index.""" - count = len(ids) - embedding_dim = len(embeddings[0]) if embeddings else 0 - - ids_arr = (ctypes.c_int * count)(*ids) - - docs_arr = (ctypes.c_char_p * count)() - for i, doc in enumerate(documents): - docs_arr[i] = _enc(doc) - - meta_arr = None - if metadatas: - meta_arr = (ctypes.c_char_p * count)() - for i, meta in enumerate(metadatas): - meta_arr[i] = _enc(meta) - - emb_ptrs = (ctypes.POINTER(ctypes.c_float) * count)() - emb_arrays = [] - for i, emb in enumerate(embeddings): - arr = (ctypes.c_float * len(emb))(*emb) - emb_arrays.append(arr) - emb_ptrs[i] = ctypes.cast(arr, ctypes.POINTER(ctypes.c_float)) - - rc = _lib.cactus_index_add(index, ids_arr, docs_arr, meta_arr, emb_ptrs, count, embedding_dim) - if rc < 0: - raise RuntimeError(_err("Failed to add to index")) - - -def cactus_index_delete(index, ids): - """Removes documents by ID.""" - count = len(ids) - ids_arr = (ctypes.c_int * count)(*ids) - rc = _lib.cactus_index_delete(index, ids_arr, count) - if rc < 0: - raise RuntimeError(_err("Failed to delete from index")) - - -def cactus_index_query(index, embedding, options_json): - """Searches the index by embedding. Returns JSON string.""" - result_capacity = 1000 - embedding_dim = len(embedding) - emb_arr = (ctypes.c_float * embedding_dim)(*embedding) - emb_ptr = ctypes.cast(emb_arr, ctypes.POINTER(ctypes.c_float)) - emb_ptr_ptr = ctypes.pointer(emb_ptr) - - id_buffer = (ctypes.c_int * result_capacity)() - score_buffer = (ctypes.c_float * result_capacity)() - id_ptr = ctypes.cast(id_buffer, ctypes.POINTER(ctypes.c_int)) - score_ptr = ctypes.cast(score_buffer, ctypes.POINTER(ctypes.c_float)) - id_size = ctypes.c_size_t(result_capacity) - score_size = ctypes.c_size_t(result_capacity) - id_ptr_ptr = ctypes.pointer(id_ptr) - score_ptr_ptr = ctypes.pointer(score_ptr) - - rc = _lib.cactus_index_query( - index, emb_ptr_ptr, 1, embedding_dim, _enc(options_json), - id_ptr_ptr, ctypes.byref(id_size), score_ptr_ptr, ctypes.byref(score_size) - ) - if rc < 0: - raise RuntimeError(_err("Index query failed")) - n = id_size.value - parts = [f'{{"id":{id_buffer[i]},"score":{score_buffer[i]}}}' for i in range(n)] - return '{"results":[' + ",".join(parts) + ']}' - - -_INDEX_DOC_BUF_SIZE = 4096 -_INDEX_EMB_BUF_SIZE = 4096 - - -def cactus_index_get(index, ids): - """Retrieves documents by ID. Returns JSON string.""" - count = len(ids) - if count == 0: - return '{"results":[]}' - - ids_arr = (ctypes.c_int * count)(*ids) - - doc_raw = [ctypes.create_string_buffer(_INDEX_DOC_BUF_SIZE) for _ in range(count)] - doc_ptrs = (ctypes.c_char_p * count)() - doc_sizes = (ctypes.c_size_t * count)() - - meta_raw = [ctypes.create_string_buffer(_INDEX_DOC_BUF_SIZE) for _ in range(count)] - meta_ptrs = (ctypes.c_char_p * count)() - meta_sizes = (ctypes.c_size_t * count)() - - emb_raw = [(ctypes.c_float * _INDEX_EMB_BUF_SIZE)() for _ in range(count)] - emb_ptrs = (ctypes.POINTER(ctypes.c_float) * count)() - emb_sizes = (ctypes.c_size_t * count)() - - for i in range(count): - doc_ptrs[i] = ctypes.cast(doc_raw[i], ctypes.c_char_p) - doc_sizes[i] = _INDEX_DOC_BUF_SIZE - meta_ptrs[i] = ctypes.cast(meta_raw[i], ctypes.c_char_p) - meta_sizes[i] = _INDEX_DOC_BUF_SIZE - emb_ptrs[i] = ctypes.cast(emb_raw[i], ctypes.POINTER(ctypes.c_float)) - emb_sizes[i] = _INDEX_EMB_BUF_SIZE - - rc = _lib.cactus_index_get( - index, ids_arr, count, - doc_ptrs, doc_sizes, meta_ptrs, meta_sizes, emb_ptrs, emb_sizes - ) - if rc < 0: - raise RuntimeError(_err("Failed to get from index")) - - parts = [] - for i in range(count): - doc = doc_raw[i].value.decode("utf-8", errors="ignore") - meta_bytes = meta_raw[i].value - meta = meta_bytes.decode("utf-8", errors="ignore") if meta_bytes else None - emb_dim = emb_sizes[i] - emb = list(emb_raw[i][:emb_dim]) if emb_dim > 0 else [] - meta_str = f'"{meta}"' if meta is not None else "null" - emb_str = "[" + ",".join(str(v) for v in emb) + "]" - parts.append(f'{{"document":"{doc}","metadata":{meta_str},"embedding":{emb_str}}}') - return '{"results":[' + ",".join(parts) + ']}' - - -def cactus_index_compact(index): - """Compacts the index storage.""" - rc = _lib.cactus_index_compact(index) - if rc < 0: - raise RuntimeError(_err("Failed to compact index")) - - -def cactus_index_destroy(index): - """Frees all index resources.""" - _lib.cactus_index_destroy(index) - - -def cactus_log_set_level(level): - """Sets the log level. 0=DEBUG, 1=INFO, 2=WARN, 3=ERROR, 4=NONE.""" - _lib.cactus_log_set_level(level) - - -_log_callback_ref = None - - -def cactus_log_set_callback(callback): - """Sets a log callback. Pass None to clear. Callback signature: (level: int, component: str, message: str).""" - global _log_callback_ref - if callback is None: - _log_callback_ref = None - _lib.cactus_log_set_callback(LogCallback(), None) - return - - def _bridge(level, component_bytes, message_bytes, _): - callback( - level, - component_bytes.decode("utf-8", errors="ignore") if component_bytes else "", - message_bytes.decode("utf-8", errors="ignore") if message_bytes else "", - ) - - _log_callback_ref = LogCallback(_bridge) - _lib.cactus_log_set_callback(_log_callback_ref, None) diff --git a/python/src/cli.py b/python/src/cli.py deleted file mode 100644 index 3bb8dd112..000000000 --- a/python/src/cli.py +++ /dev/null @@ -1,2202 +0,0 @@ -#!/usr/bin/env python3 -import sys -import os -import argparse -import re -import json -import subprocess -import shutil -import platform -from pathlib import Path - -SCRIPT_DIR = Path(__file__).resolve().parent - - -def _looks_like_project_root(path: Path) -> bool: - return ( - (path / "python" / "src" / "cli.py").exists() - and (path / "cactus").exists() - and (path / "tests").exists() - ) - - -def _resolve_project_root() -> Path: - # Optional explicit override for environments running from installed packages. - env_root = os.getenv("CACTUS_PROJECT_ROOT", "").strip() - if env_root: - candidate = Path(env_root).expanduser().resolve() - if _looks_like_project_root(candidate): - return candidate - - # Prefer the repo containing this CLI module. - module_root = SCRIPT_DIR.parent.parent - if _looks_like_project_root(module_root): - return module_root - - # Fallback: repo containing current working directory. - cwd = Path.cwd().resolve() - for candidate in [cwd, *cwd.parents]: - if _looks_like_project_root(candidate): - return candidate - - # Final fallback for unusual layouts. - return module_root - - -PROJECT_ROOT = _resolve_project_root() -DEFAULT_MODEL_ID = "LiquidAI/LFM2-VL-450M" -DEFAULT_TEST_TRANSCRIBE_MODEL_ID = "openai/whisper-base" -WEIGHTS_VARIANT_CHOICES = ["auto", "apple", "standard"] - -with open(PROJECT_ROOT / "models.json") as _f: - MODELS_REGISTRY = json.load(_f) - -RED = '\033[0;31m' -GREEN = '\033[0;32m' -YELLOW = '\033[1;33m' -BLUE = '\033[0;34m' -NC = '\033[0m' - - -def print_color(color, message): - """Print a message with ANSI color codes.""" - print(f"{color}{message}{NC}") - - -from .downloads import get_model_dir_name, get_weights_dir, download_from_hf as _download_from_hf_impl - - -NEEDLE_CHECKPOINT_REPO = "Cactus-Compute/needle" -NEEDLE_TOKENIZER_REPO = "Cactus-Compute/needle-tokenizer" -NEEDLE_CHECKPOINT_FILE = "needle.pkl" - - -def is_needle_model_id(model_id): - normalized = (model_id or "").strip().lower() - if "/" in normalized: - normalized = normalized.split("/")[-1] - return normalized == "needle" or normalized.startswith("needle-") or normalized.startswith("needle_") - - -def get_effective_weights_dir(model_id, args=None): - if not is_needle_model_id(model_id): - return get_weights_dir(model_id) - return (PROJECT_ROOT / "weights" / "needle").resolve() - - - -def check_command(cmd): - """Check if a command is available in PATH.""" - return shutil.which(cmd) is not None - - -def run_command(cmd, cwd=None, check=True): - """Run a script or command and optionally exit on failure. - - Args: - cmd: Script path (str) or command list. String paths are executed - directly without shell interpretation to handle spaces safely. - cwd: Working directory for the command. - check: If True, exit on non-zero return code. - """ - # Convert string paths to list to avoid shell=True and handle spaces safely - if isinstance(cmd, str): - cmd = [cmd] - result = subprocess.run(cmd, cwd=cwd) - if check and result.returncode != 0: - sys.exit(result.returncode) - return result - - -def _is_stale_binary(binary_path, dependency_paths): - binary_path = Path(binary_path) - if not binary_path.exists(): - return True - - try: - binary_mtime = binary_path.stat().st_mtime - except OSError: - return True - - for dep in dependency_paths: - dep_path = Path(dep) - if not dep_path.exists(): - continue - try: - if dep_path.stat().st_mtime > binary_mtime: - return True - except OSError: - continue - - return False - - -def _ensure_chat_binary(project_root, lib_path): - tests_dir = project_root / "tests" - build_dir = tests_dir / "build" - chat_binary = build_dir / "chat" - chat_cpp = tests_dir / "chat.cpp" - - if not _is_stale_binary(chat_binary, [lib_path, chat_cpp]): - return chat_binary - - print_color(YELLOW, "Refreshing chat binary for current Cactus library...") - build_args = argparse.Namespace( - apple=False, - android=False, - python=False, - ) - result = cmd_build(build_args) - if result != 0 or not chat_binary.exists(): - raise RuntimeError("Failed to rebuild chat binary") - - return chat_binary - - -def ensure_vad_weights(model_id, weights_dir, precision='INT8'): - """Bundle Silero VAD weights into /vad/ for ASR models.""" - is_asr = ( - 'whisper' in model_id.lower() - or 'moonshine' in model_id.lower() - or 'parakeet' in model_id.lower() - ) - if not is_asr: - return - vad_dir = weights_dir / "vad" - if (vad_dir / "config.txt").exists(): - return - try: - import torch - import urllib.request - import tempfile - from .converter import convert_silero_vad_weights - - print_color(YELLOW, "Bundling VAD weights for speech model...") - vad_jit_url = "https://github.com/snakers4/silero-vad/raw/master/src/silero_vad/data/silero_vad.jit" - with tempfile.NamedTemporaryFile(suffix='.jit', delete=False) as f: - jit_path = f.name - urllib.request.urlretrieve(vad_jit_url, jit_path) - vad_model = torch.jit.load(jit_path, map_location='cpu') - os.unlink(jit_path) - - convert_silero_vad_weights(vad_model, str(vad_dir), precision) - del vad_model - if torch.cuda.is_available(): - torch.cuda.empty_cache() - print_color(GREEN, "VAD weights bundled successfully") - except Exception as e: - print_color(RED, f"Warning: Failed to bundle VAD weights: {e}") - print("Transcription may fail without VAD. Try: cactus download snakers4/silero-vad") - - -def download_from_hf(model_id, weights_dir, precision): - """Download pre-converted model from Cactus-Compute HuggingFace.""" - return _download_from_hf_impl(model_id, weights_dir, precision) - - -def cmd_download(args): - """Download model weights. By default downloads pre-converted weights from Cactus-Compute.""" - model_id = args.model_id - is_local = Path(model_id).is_dir() - weights_dir = get_effective_weights_dir(model_id, args) - reconvert = getattr(args, 'reconvert', False) - precision = getattr(args, 'precision', 'INT4') - - if reconvert and weights_dir.exists(): - print_color(YELLOW, f"Removing cached weights for reconversion...") - shutil.rmtree(weights_dir) - - if weights_dir.exists() and (weights_dir / "config.txt").exists(): - ensure_vad_weights(model_id, weights_dir, precision) - print_color(GREEN, f"Model weights found at {weights_dir}") - return 0 - - print() - print_color(YELLOW, f"Model weights not found. Downloading {model_id}...") - print("=" * 45) - - if not is_local and is_needle_model_id(model_id): - try: - from huggingface_hub import hf_hub_download, snapshot_download - from .converter import convert_needle_checkpoint - - print_color(YELLOW, "Using Needle exporter...") - token = getattr(args, 'token', None) - cache_dir = getattr(args, 'cache_dir', None) - - ck_path = hf_hub_download(repo_id=NEEDLE_CHECKPOINT_REPO, filename=NEEDLE_CHECKPOINT_FILE, - repo_type="model", token=token, cache_dir=cache_dir) - tk_snap = Path(snapshot_download(repo_id=NEEDLE_TOKENIZER_REPO, repo_type="dataset", - allow_patterns=["*.model"], token=token, cache_dir=cache_dir)) - tk_path = next(tk_snap.rglob("*.model"), None) - if not tk_path: - raise FileNotFoundError(f"No .model file in tokenizer snapshot: {tk_snap}") - - convert_needle_checkpoint(ck_path, tk_path, weights_dir, precision) - print_color(GREEN, f"Successfully exported Needle weights to {weights_dir}") - return 0 - except Exception as e: - print_color(RED, f"Error: {e}") - return 1 - - if not reconvert and not is_local: - if download_from_hf(model_id, weights_dir, precision): - ensure_vad_weights(model_id, weights_dir, precision) - return 0 - - tokenizer_labels = None - - try: - import torch - from transformers import AutoTokenizer - except ImportError: - print_color(RED, "Error: Required Python packages not found.") - print("Please run: ./setup") - return 1 - - from .converter import convert_hf_model_weights - from .tokenizer import convert_hf_tokenizer - from .tensor_io import format_config_value - from .config_utils import is_lfm2_vl, pick_dtype, vision_weight_sanity_check - - weights_dir.mkdir(parents=True, exist_ok=True) - - precision = getattr(args, 'precision', 'INT4') - cache_dir = getattr(args, 'cache_dir', None) - token = getattr(args, 'token', None) - - print(f"Converting {model_id} to {precision}...") - - from transformers import AutoTokenizer, AutoModelForCausalLM, AutoModel - - import logging - logging.getLogger("transformers").setLevel(logging.ERROR) - import transformers - transformers.logging.set_verbosity_error() - - def _download_config_json(repo_id, revision=None): - if Path(repo_id).is_dir(): - config_path = Path(repo_id) / "config.json" - else: - from huggingface_hub import hf_hub_download - config_path = hf_hub_download( - repo_id=repo_id, - filename="config.json", - cache_dir=cache_dir, - token=token, - revision=revision, - ) - with open(config_path, 'r', encoding='utf-8') as fh: - return json.load(fh) - - def _resolve_hf_revision(repo_id): - env_revision = os.getenv("CACTUS_HF_REVISION", "").strip() - if env_revision: - return env_revision - if repo_id.lower() == "nvidia/parakeet-tdt-0.6b-v3": - return "refs/pr/7" - return None - - class _MinimalTokenizer: - """Fallback tokenizer for Parakeet-TDT when HF tokenizer load fails.""" - def __init__(self, name_or_path, config_obj=None): - self.name_or_path = name_or_path - self.model_max_length = 131072 - self.pad_token_id = 0 - self.eos_token_id = 0 - - try: - pad_id = config_obj.get('pad_token_id', 0) if isinstance(config_obj, dict) else 0 - self.pad_token_id = int(pad_id) if pad_id is not None else 0 - except Exception: - self.pad_token_id = 0 - - try: - decoding = config_obj.get('decoding', {}) if isinstance(config_obj, dict) else {} - blank_id = decoding.get('blank_id', None) if isinstance(decoding, dict) else None - if blank_id is not None: - self.eos_token_id = int(blank_id) - else: - self.eos_token_id = self.pad_token_id - except Exception: - self.eos_token_id = self.pad_token_id - - def _load_raw_hf_state_dict(repo_id, cast_to_bf16=True): - from safetensors.torch import load_file as load_safetensors_file - - if Path(repo_id).is_dir(): - snapshot_path = Path(repo_id) - else: - from huggingface_hub import snapshot_download - snapshot_path = Path(snapshot_download( - repo_id=repo_id, - cache_dir=cache_dir, - token=token, - allow_patterns=["*.safetensors", "*.safetensors.index.json", "*.bin", "*.bin.index.json"], - )) - - index_candidates = [ - "model.safetensors.index.json", - "pytorch_model.bin.index.json", - ] - - shard_files = [] - for index_name in index_candidates: - index_path = snapshot_path / index_name - if index_path.exists(): - with open(index_path, 'r', encoding='utf-8') as fh: - index_data = json.load(fh) - shard_files = sorted(set(index_data.get("weight_map", {}).values())) - if shard_files: - break - - if not shard_files: - shard_files = sorted([p.name for p in snapshot_path.glob("*.safetensors")]) - if not shard_files: - shard_files = sorted([p.name for p in snapshot_path.glob("*.bin")]) - - if not shard_files: - raise RuntimeError("No checkpoint shard files found in HuggingFace snapshot.") - - print(f" Found {len(shard_files)} checkpoint shard file(s)") - merged_state_dict = {} - for idx, shard_name in enumerate(shard_files, 1): - shard_path = snapshot_path / shard_name - print(f" Loading shard {idx}/{len(shard_files)}: {shard_name}") - if shard_name.endswith(".safetensors"): - shard_state = load_safetensors_file(str(shard_path), device="cpu") - elif shard_name.endswith(".bin"): - shard_state = torch.load(str(shard_path), map_location="cpu") - else: - continue - merged_state_dict.update(shard_state) - - if cast_to_bf16: - fp_keys = [ - k for k, v in merged_state_dict.items() - if hasattr(v, "is_floating_point") and v.is_floating_point() and v.dtype != torch.bfloat16 - ] - total_fp = len(fp_keys) - if total_fp > 0: - print(f" Normalizing {total_fp} floating tensors to bfloat16...") - for i, k in enumerate(fp_keys, 1): - merged_state_dict[k] = merged_state_dict[k].to(torch.bfloat16) - if i % 200 == 0 or i == total_fp: - print(f" dtype normalize progress: {i}/{total_fp}") - else: - print(" Keeping checkpoint dtypes as-is (Gemma4 fast path)") - - - return merged_state_dict - - try: - from transformers import Lfm2VlForConditionalGeneration - except ImportError: - Lfm2VlForConditionalGeneration = None - - model_name = str(model_id) - is_vlm = 'vl' in model_name.lower() or 'vlm' in model_name.lower() - is_whisper = 'whisper' in model_name.lower() - is_parakeet = 'parakeet' in model_name.lower() - is_vad = 'silero-vad' in model_name.lower() - is_pyannote = 'segmentation-3.0' in model_name.lower() - is_wespeaker = 'wespeaker' in model_name.lower() - - try: - if is_vlm: - from transformers import AutoProcessor, AutoModelForImageTextToText, AutoConfig - missing_deps = [] - try: - from PIL import Image - except Exception: - missing_deps.append('Pillow') - try: - import num2words - except Exception: - missing_deps.append('num2words') - try: - import torchvision - except Exception: - missing_deps.append('torchvision') - - if missing_deps: - print_color(RED, f"Error: Missing packages for VLM: {', '.join(missing_deps)}") - print(f"Install with: pip install {' '.join(missing_deps)}") - return 1 - - processor = None - try: - processor = AutoProcessor.from_pretrained(model_id, cache_dir=cache_dir, trust_remote_code=True, token=token) - except Exception as proc_err: - if "TokenizersBackend" in str(proc_err) or "does not exist or is not currently imported" in str(proc_err): - print(f" Note: AutoProcessor failed, using fallback tokenizer loading...") - else: - raise - - cfg = AutoConfig.from_pretrained(model_id, cache_dir=cache_dir, trust_remote_code=True, token=token) - dtype = pick_dtype() - - if is_lfm2_vl(model_id, cfg) and Lfm2VlForConditionalGeneration is not None: - model = Lfm2VlForConditionalGeneration.from_pretrained(model_id, cache_dir=cache_dir, trust_remote_code=True, dtype=dtype, token=token) - else: - model = AutoModelForImageTextToText.from_pretrained(model_id, cache_dir=cache_dir, trust_remote_code=True, dtype=dtype, token=token) - - tokenizer = getattr(processor, "tokenizer", None) if processor else None - if tokenizer is None: - try: - tokenizer = AutoTokenizer.from_pretrained(model_id, cache_dir=cache_dir, trust_remote_code=True, token=token) - except Exception as tok_err: - if "TokenizersBackend" in str(tok_err) or "does not exist or is not currently imported" in str(tok_err): - from transformers import PreTrainedTokenizerFast - print(f" Note: Using PreTrainedTokenizerFast fallback for invalid tokenizer_class...") - tokenizer = PreTrainedTokenizerFast.from_pretrained(model_id, cache_dir=cache_dir, token=token) - else: - raise - - if is_lfm2_vl(model_id, cfg) and not vision_weight_sanity_check(model): - print_color(RED, "Vision embeddings look randomly initialized.") - return 1 - - elif 'moonshine' in model_id.lower(): - from transformers import MoonshineForConditionalGeneration - print(f" Note: Loading Moonshine model using MoonshineForConditionalGeneration...") - model = MoonshineForConditionalGeneration.from_pretrained(model_id, cache_dir=cache_dir, trust_remote_code=True, dtype=torch.bfloat16, token=token) - tokenizer = AutoTokenizer.from_pretrained(model_id, cache_dir=cache_dir, trust_remote_code=True, token=token) - - elif is_whisper: - tokenizer = AutoTokenizer.from_pretrained(model_id, cache_dir=cache_dir, trust_remote_code=True, token=token) - model = AutoModel.from_pretrained(model_id, cache_dir=cache_dir, trust_remote_code=True, dtype=torch.bfloat16, token=token) - - elif is_parakeet: - from huggingface_hub import hf_hub_download, snapshot_download - from safetensors.torch import load_file as load_safetensors - - revision = _resolve_hf_revision(model_id) - config_obj = _download_config_json(model_id, revision=revision) - is_parakeet_tdt = 'parakeet-tdt' in model_id.lower() - if 'parakeet-tdt' in model_id.lower(): - cfg_labels = config_obj.get('labels', []) - if isinstance(cfg_labels, list) and cfg_labels: - tokenizer_labels = cfg_labels - try: - tokenizer = AutoTokenizer.from_pretrained( - model_id, - cache_dir=cache_dir, - trust_remote_code=True, - token=token, - revision=revision, - ) - except Exception as tok_err: - tokenizer = None - if "TokenizersBackend" in str(tok_err) or "does not exist or is not currently imported" in str(tok_err): - from transformers import PreTrainedTokenizerFast - print(" Note: Using PreTrainedTokenizerFast fallback for Parakeet tokenizer...") - try: - tokenizer = PreTrainedTokenizerFast.from_pretrained( - model_id, - cache_dir=cache_dir, - token=token, - revision=revision, - ) - except Exception as fast_tok_err: - tok_err = fast_tok_err - - if tokenizer is None: - if is_parakeet_tdt and isinstance(tokenizer_labels, list) and tokenizer_labels: - print(f" Note: Parakeet-TDT tokenizer load failed, using labels fallback ({tok_err})") - tokenizer = _MinimalTokenizer(model_id, config_obj) - else: - raise - - state_dict = None - try: - weights_path = hf_hub_download( - repo_id=model_id, - filename="model.safetensors", - cache_dir=cache_dir, - token=token, - revision=revision, - ) - state_dict = load_safetensors(weights_path, device="cpu") - except Exception: - snapshot_path = snapshot_download( - repo_id=model_id, - cache_dir=cache_dir, - token=token, - revision=revision, - ) - index_path = Path(snapshot_path) / "model.safetensors.index.json" - if not index_path.exists(): - raise - with open(index_path, "r", encoding="utf-8") as f: - index_data = json.load(f) - shard_files = sorted(set(index_data.get("weight_map", {}).values())) - if not shard_files: - raise RuntimeError("Parakeet safetensors index has no shard entries") - state_dict = {} - for shard_name in shard_files: - shard_path = Path(snapshot_path) / shard_name - shard_state = load_safetensors(str(shard_path), device="cpu") - state_dict.update(shard_state) - - class _StateDictModel: - def __init__(self, config, state_dict): - self.config = config - self._state_dict = state_dict - - def state_dict(self): - return self._state_dict - - model = _StateDictModel(config_obj, state_dict) - - elif is_vad: - import urllib.request - import tempfile - from .converter import convert_silero_vad_weights - - vad_jit_url = "https://github.com/snakers4/silero-vad/raw/master/src/silero_vad/data/silero_vad.jit" - with tempfile.NamedTemporaryFile(suffix='.jit', delete=False) as f: - jit_path = f.name - urllib.request.urlretrieve(vad_jit_url, jit_path) - model = torch.jit.load(jit_path, map_location='cpu') - os.unlink(jit_path) - convert_silero_vad_weights(model, weights_dir, precision, args) - - del model - import torch - if torch.cuda.is_available(): - torch.cuda.empty_cache() - - print_color(GREEN, f"Successfully downloaded and converted weights to {weights_dir}") - return 0 - - elif is_pyannote or is_wespeaker: - try: - import warnings - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - from pyannote.audio import Model as PyannoteModel - except ImportError: - print_color(RED, "Error: pyannote.audio is required. Install with: pip install pyannote.audio") - return 1 - from .converter import convert_pyannote_weights, convert_wespeaker_weights - - import warnings - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - pyannote_model = PyannoteModel.from_pretrained(model_id, token=token) - pyannote_model.eval() - - if is_pyannote: - convert_pyannote_weights(pyannote_model, weights_dir, precision, args) - else: - convert_wespeaker_weights(pyannote_model, weights_dir, precision, args) - - del pyannote_model - if torch.cuda.is_available(): - torch.cuda.empty_cache() - - print_color(GREEN, f"Successfully downloaded and converted weights to {weights_dir}") - return 0 - - else: - config_json = _download_config_json(model_id) - model_type = str(config_json.get('model_type', '')).lower() - - try: - tokenizer = AutoTokenizer.from_pretrained(model_id, cache_dir=cache_dir, trust_remote_code=True, token=token) - except Exception as tok_err: - if "TokenizersBackend" in str(tok_err) or "does not exist or is not currently imported" in str(tok_err): - from transformers import PreTrainedTokenizerFast - print(" Note: Using PreTrainedTokenizerFast fallback for invalid tokenizer_class...") - tokenizer = PreTrainedTokenizerFast.from_pretrained(model_id, cache_dir=cache_dir, token=token) - else: - raise - - if ( - model_type == 'lfm2_moe' - or model_type.startswith('qwen3_5') - or model_type == 'youtu' - or 'gemma4' in model_type - or 'gemma3n' in model_type - ): - if model_type == 'lfm2_moe': - print(" Note: Loading raw checkpoint tensors for lfm2_moe conversion...") - elif 'gemma4' in model_type: - print(f" Note: Loading raw checkpoint tensors for {model_type} conversion...") - else: - print(f" Note: Loading raw checkpoint tensors for {model_type} conversion...") - cast_to_bf16 = ('gemma4' not in model_type) - raw_state_dict = _load_raw_hf_state_dict(model_id, cast_to_bf16=cast_to_bf16) - - class _RawModelWrapper: - def __init__(self, state_dict, config): - self._state_dict = state_dict - self.config = config - - def state_dict(self): - return self._state_dict - - model = _RawModelWrapper(raw_state_dict, config_json) - else: - try: - model = AutoModelForCausalLM.from_pretrained(model_id, cache_dir=cache_dir, trust_remote_code=True, dtype=torch.bfloat16, token=token) - except ValueError: - model = AutoModel.from_pretrained(model_id, cache_dir=cache_dir, trust_remote_code=True, dtype=torch.bfloat16, token=token) - - config = convert_hf_model_weights(model, weights_dir, precision, args) - del model - - model_id_lower = model_id.lower() - if 'extract' in model_id_lower: - config['model_variant'] = 'extract' - elif 'vlm' in model_id_lower: - config['model_variant'] = 'vlm' - elif 'rag' in model_id_lower: - config['model_variant'] = 'rag' - else: - config.setdefault('model_variant', 'default') - - # Config precision stores the compute precision (weights are quantized, activations stay FP16) - if precision in ('INT8', 'INT4'): - config['precision'] = "FP16" - else: - config['precision'] = precision - config['quantization'] = precision # this is for CLI display only - - config_path = weights_dir / "config.txt" - with open(config_path, 'w') as f: - for key, value in config.items(): - f.write(f"{key}={format_config_value(value)}\n") - - convert_hf_tokenizer( - tokenizer, - weights_dir, - token=token, - model_id=model_id, - labels=tokenizer_labels, - model_type=config.get('model_type'), - ) - - del tokenizer - import torch - if torch.cuda.is_available(): - torch.cuda.empty_cache() - - print_color(GREEN, f"Successfully downloaded and converted weights to {weights_dir}") - return 0 - - except Exception as e: - print_color(RED, f"Error: {e}") - return 1 - - -def check_libcurl(): - """Check if libcurl development libraries are installed.""" - import platform - - if platform.system() == 'Darwin': - return True - - if check_command('pkg-config'): - result = subprocess.run(['pkg-config', '--exists', 'libcurl'], capture_output=True) - if result.returncode == 0: - return True - - curl_paths = [ - '/usr/include/curl/curl.h', - '/usr/include/x86_64-linux-gnu/curl/curl.h', - '/usr/include/aarch64-linux-gnu/curl/curl.h', - '/usr/local/include/curl/curl.h', - ] - for path in curl_paths: - if Path(path).exists(): - return True - - return False - - -def cmd_build(args): - """Build the Cactus library and chat binary.""" - if getattr(args, 'apple', False): - return cmd_build_apple(args) - if getattr(args, 'android', False): - return cmd_build_android(args) - if getattr(args, 'python', False): - return cmd_build_python(args) - - print_color(BLUE, "Building Cactus chat...") - print("=" * 23) - - if not check_command('cmake'): - print_color(RED, "Error: CMake is not installed") - print(" macOS: brew install cmake") - print(" Ubuntu: sudo apt-get install cmake build-essential") - return 1 - - if not check_libcurl(): - print_color(RED, "Error: libcurl development libraries not found") - print(" macOS: brew install curl") - print(" Ubuntu: sudo apt-get install libcurl4-openssl-dev") - return 1 - - cactus_dir = PROJECT_ROOT / "cactus" - lib_path = cactus_dir / "build" / "libcactus.a" - vendored_curl = PROJECT_ROOT / "libs" / "curl" / "macos" / "libcurl.a" - - print_color(YELLOW, "Building Cactus library...") - build_script = cactus_dir / "build.sh" - if not build_script.exists(): - print_color(RED, f"Error: build.sh not found at {build_script}") - return 1 - result = run_command(str(build_script), cwd=cactus_dir, check=False) - if result.returncode != 0: - print_color(RED, "Failed to build cactus library") - return 1 - - tests_dir = PROJECT_ROOT / "tests" - build_dir = tests_dir / "build" - build_dir.mkdir(parents=True, exist_ok=True) - - print("Compiling chat.cpp...") - - chat_cpp = tests_dir / "chat.cpp" - if not chat_cpp.exists(): - print_color(RED, f"Error: chat.cpp not found at {chat_cpp}") - return 1 - - is_darwin = platform.system() == "Darwin" - - sdl2_available = False - sdl2_flags = [] - sdl2_link = [] - if is_darwin: - sdl2_check = subprocess.run(["brew", "list", "sdl2"], capture_output=True) - if sdl2_check.returncode == 0: - sdl2_prefix_result = subprocess.run(["brew", "--prefix", "sdl2"], capture_output=True, text=True) - if sdl2_prefix_result.returncode == 0: - sdl2_prefix = sdl2_prefix_result.stdout.strip() - sdl2_flags = ["-DHAVE_SDL2", f"-I{sdl2_prefix}/include", f"-I{sdl2_prefix}/include/SDL2"] - sdl2_link = [f"-L{sdl2_prefix}/lib", "-lSDL2"] - sdl2_available = True - else: - sdl2_check = subprocess.run(["pkg-config", "--exists", "sdl2"], capture_output=True) - if sdl2_check.returncode == 0: - cflags = subprocess.run(["pkg-config", "--cflags", "sdl2"], capture_output=True, text=True) - libs = subprocess.run(["pkg-config", "--libs", "sdl2"], capture_output=True, text=True) - if cflags.returncode == 0 and libs.returncode == 0: - sdl2_flags = ["-DHAVE_SDL2"] + cflags.stdout.strip().split() - sdl2_link = libs.stdout.strip().split() - sdl2_available = True - - if sdl2_available: - print_color(GREEN, "SDL2 found - building with live audio support") - else: - print_color(YELLOW, "SDL2 not found - live mic recording will be disabled") - print_color(YELLOW, "Install SDL2 for live mic support: brew install sdl2 (macOS)") - print_color(YELLOW, "Then run `cactus build`") - - if is_darwin: - if not vendored_curl.exists(): - print_color(RED, f"Error: vendored libcurl not found at {vendored_curl}") - print("Build it first and place it in libs/curl/macos/libcurl.a") - return 1 - compiler = "clang++" - cmd = [ - compiler, "-std=c++20", "-O3", - "-DACCELERATE_NEW_LAPACK", - f"-I{PROJECT_ROOT}", - *sdl2_flags, - str(chat_cpp), - str(lib_path), - "-o", "chat", - str(vendored_curl), - "-framework", "Accelerate", - "-framework", "CoreML", - "-framework", "Foundation", - "-framework", "Security", - "-framework", "SystemConfiguration", - "-framework", "CFNetwork", - *sdl2_link, - ] - else: - compiler = "g++" - cmd = [ - compiler, "-std=c++20", "-O3", - f"-I{PROJECT_ROOT}", - *sdl2_flags, - str(chat_cpp), - str(lib_path), - "-o", "chat", - "-lcurl", - "-pthread", - *sdl2_link, - ] - - if not check_command(compiler): - print_color(RED, f"Error: {compiler} is not installed") - return 1 - - result = subprocess.run(cmd, cwd=build_dir) - if result.returncode != 0: - print_color(RED, "Build failed") - return 1 - - print_color(GREEN, f"Build complete: {build_dir / 'chat'}") - - asr_cpp = tests_dir / "asr.cpp" - if asr_cpp.exists(): - print("Compiling asr.cpp...") - - if is_darwin: - cmd = [ - compiler, "-std=c++20", "-O3", - "-DACCELERATE_NEW_LAPACK", - f"-I{PROJECT_ROOT}", - *sdl2_flags, - str(asr_cpp), - str(lib_path), - "-o", "asr", - str(vendored_curl), - "-framework", "Accelerate", - "-framework", "CoreML", - "-framework", "Foundation", - "-framework", "Security", - "-framework", "SystemConfiguration", - "-framework", "CFNetwork", - *sdl2_link, - ] - else: - cmd = [ - compiler, "-std=c++20", "-O3", - f"-I{PROJECT_ROOT}", - *sdl2_flags, - str(asr_cpp), - str(lib_path), - "-o", "asr", - "-lcurl", - "-pthread", - *sdl2_link, - ] - - result = subprocess.run(cmd, cwd=build_dir) - if result.returncode != 0: - print_color(RED, "ASR build failed") - return 1 - - print_color(GREEN, f"Build complete: {build_dir / 'asr'}") - - return 0 - - -def cmd_build_apple(args): - """Build Cactus for Apple platforms (iOS/macOS).""" - print_color(BLUE, "Building Cactus for Apple platforms...") - print("=" * 40) - - if platform.system() != "Darwin": - print_color(RED, "Error: Apple builds require macOS") - return 1 - - build_script = PROJECT_ROOT / "apple" / "build.sh" - if not build_script.exists(): - print_color(RED, f"Error: build.sh not found at {build_script}") - return 1 - - result = run_command(str(build_script), cwd=PROJECT_ROOT / "apple", check=False) - if result.returncode != 0: - print_color(RED, "Apple build failed") - return 1 - - print_color(GREEN, "Apple build complete!") - return 0 - - -def cmd_build_android(args): - """Build Cactus for Android.""" - print_color(BLUE, "Building Cactus for Android...") - print("=" * 32) - - build_script = PROJECT_ROOT / "android" / "build.sh" - if not build_script.exists(): - print_color(RED, f"Error: build.sh not found at {build_script}") - return 1 - - result = run_command(str(build_script), cwd=PROJECT_ROOT / "android", check=False) - if result.returncode != 0: - print_color(RED, "Android build failed") - return 1 - - print_color(GREEN, "Android build complete!") - return 0 - - -def cmd_build_python(args): - """Build Cactus shared library for Python FFI.""" - print_color(BLUE, "Building Cactus for Python...") - print("=" * 30) - - if not check_command('cmake'): - print_color(RED, "Error: CMake is not installed") - print(" macOS: brew install cmake") - print(" Ubuntu: sudo apt-get install cmake") - return 1 - - cactus_dir = PROJECT_ROOT / "cactus" - build_script = cactus_dir / "build.sh" - if not build_script.exists(): - print_color(RED, f"Error: build.sh not found at {build_script}") - return 1 - - result = run_command(str(build_script), cwd=cactus_dir, check=False) - if result.returncode != 0: - print_color(RED, "Build failed") - return 1 - - if platform.system() == "Darwin": - lib_name = "libcactus.dylib" - else: - lib_name = "libcactus.so" - - lib_path = cactus_dir / "build" / lib_name - if not lib_path.exists(): - print_color(RED, f"Shared library not found at {lib_path}") - return 1 - - print_color(GREEN, "Python build complete!") - print(f"Library: {lib_path}") - return 0 - - -def prompt_for_api_key(config): - """Prompt user to set Cactus Cloud API key if not already configured. Returns the key or empty string.""" - api_key = config.get_api_key() - if api_key: - return api_key - - print("\n" + "="*50) - print(" Cactus Cloud Setup (Optional)") - print("="*50 + "\n") - print("Get your cloud key at \033[1;36mhttps://www.cactuscompute.com/dashboard/api-keys\033[0m") - print("to enable automatic cloud fallback.\n") - - api_key = input("Your Cactus Cloud key (press Enter to skip): ").strip() - if api_key: - config.set_api_key(api_key) - masked = api_key[:4] + "..." + api_key[-4:] - print_color(GREEN, f"API key saved: {masked}") - print() - return api_key - - -def cmd_run(args): - """Download model if needed and start interactive chat.""" - from .config_utils import CactusConfig - - config = CactusConfig() - api_key = prompt_for_api_key(config) - - if api_key: - os.environ["CACTUS_CLOUD_KEY"] = api_key - - model_id = args.model_id - - if getattr(args, 'no_cloud_tele', False): - os.environ["CACTUS_NO_CLOUD_TELE"] = "1" - - lib_path = PROJECT_ROOT / "cactus" / "build" / "libcactus.a" - if not lib_path.exists(): - print_color(RED, "Error: Cactus library not built. Run 'cactus build' first.") - return 1 - - local_path = Path(model_id) - if local_path.exists() and (local_path / "config.txt").exists(): - weights_dir = local_path - print_color(GREEN, f"Using local model: {weights_dir}") - else: - download_result = cmd_download(args) - if download_result != 0: - return download_result - weights_dir = get_effective_weights_dir(model_id, args) - - image_path = getattr(args, 'image', None) - if image_path: - image_path = str(Path(image_path).resolve()) - if not Path(image_path).exists(): - print_color(RED, f"Error: Image file not found: {image_path}") - return 1 - valid_exts = {'.png', '.jpg', '.jpeg', '.bmp'} - if Path(image_path).suffix.lower() not in valid_exts: - print_color(RED, f"Error: Unsupported image format. Supported: {', '.join(valid_exts)}") - return 1 - - try: - chat_binary = _ensure_chat_binary(PROJECT_ROOT, lib_path) - except RuntimeError as exc: - print_color(RED, f"Error: {exc}") - return 1 - - os.system('clear' if platform.system() != 'Windows' else 'cls') - print_color(GREEN, f"Starting Cactus Chat with model: {model_id}") - print() - - audio_path = getattr(args, 'audio', None) - if audio_path: - audio_path = str(Path(audio_path).resolve()) - if not Path(audio_path).exists(): - print_color(RED, f"Error: Audio file not found: {audio_path}") - return 1 - - cmd_args = [str(chat_binary), str(weights_dir)] - if image_path: - cmd_args.extend(['--image', image_path]) - if audio_path: - cmd_args.extend(['--audio', audio_path]) - system_prompt = getattr(args, 'system', None) - if system_prompt: - cmd_args.extend(['--system', system_prompt]) - prompt = getattr(args, 'prompt', None) - if prompt: - cmd_args.extend(['--prompt', prompt]) - if getattr(args, 'thinking', False): - cmd_args.append('--thinking') - - os.execv(str(chat_binary), cmd_args) - - -DEFAULT_ASR_MODEL_ID = "openai/whisper-base" - -def _pick_android_device_id(preferred_device=None): - if preferred_device: - return preferred_device - - result = subprocess.run(["adb", "devices"], capture_output=True, text=True) - if result.returncode != 0: - return None - - devices = [] - for line in result.stdout.splitlines(): - line = line.strip() - if not line or line.startswith("List of devices attached"): - continue - parts = line.split() - if len(parts) >= 2 and parts[1] == "device": - devices.append(parts[0]) - - if len(devices) == 1: - return devices[0] - return None - - -def _cmd_transcribe_android(weights_dir, audio_file, args): - if not audio_file: - print_color(RED, "Error: --android requires --file ") - return 1 - if not check_command("adb"): - print_color(RED, "Error: adb not found in PATH") - return 1 - - audio_path = Path(audio_file).expanduser().resolve() - if not audio_path.exists(): - print_color(RED, f"Error: audio file not found: {audio_path}") - return 1 - - device_id = _pick_android_device_id(getattr(args, "device", None)) - if not device_id: - print_color(RED, "Error: could not select Android device. Use --device .") - return 1 - - print_color(BLUE, f"Using Android device: {device_id}") - - android_build_script = PROJECT_ROOT / "android" / "build.sh" - if not android_build_script.exists(): - print_color(RED, f"Error: build.sh not found at {android_build_script}") - return 1 - if run_command(str(android_build_script), cwd=PROJECT_ROOT / "android", check=False).returncode != 0: - print_color(RED, "Android library build failed") - return 1 - - if not check_command("cmake"): - print_color(RED, "Error: CMake is not installed") - return 1 - - android_test_dir = PROJECT_ROOT / "tests" / "android" - android_build_dir = android_test_dir / "build" - ndk_home = os.environ.get("ANDROID_NDK_HOME") - if not ndk_home: - android_home = os.environ.get("ANDROID_HOME") or str(Path.home() / "Library" / "Android" / "sdk") - ndk_root = Path(android_home) / "ndk" - if ndk_root.exists(): - ndk_versions = sorted([p for p in ndk_root.iterdir() if p.is_dir()]) - if ndk_versions: - ndk_home = str(ndk_versions[-1]) - if not ndk_home or not Path(ndk_home).exists(): - print_color(RED, "Error: Android NDK not found. Set ANDROID_NDK_HOME.") - return 1 - - toolchain = Path(ndk_home) / "build" / "cmake" / "android.toolchain.cmake" - if not toolchain.exists(): - print_color(RED, f"Error: Android toolchain not found at {toolchain}") - return 1 - - android_build_dir.mkdir(parents=True, exist_ok=True) - cfg_cmd = [ - "cmake", "-S", str(android_test_dir), "-B", str(android_build_dir), - f"-DCMAKE_TOOLCHAIN_FILE={toolchain}", - "-DANDROID_ABI=arm64-v8a", - f"-DANDROID_PLATFORM={os.environ.get('ANDROID_PLATFORM', 'android-21')}", - "-DCMAKE_BUILD_TYPE=Release", - ] - if subprocess.run(cfg_cmd).returncode != 0: - print_color(RED, "Failed to configure Android transcribe build") - return 1 - build_cmd = ["cmake", "--build", str(android_build_dir), "--target", "asr", "-j", str(os.cpu_count() or 4)] - if subprocess.run(build_cmd).returncode != 0: - print_color(RED, "Failed to build Android asr binary") - return 1 - - asr_bin = android_build_dir / "asr" - if not asr_bin.exists(): - print_color(RED, f"Error: Android asr binary not found at {asr_bin}") - return 1 - - model_name = Path(weights_dir).name - device_root = "/data/local/tmp/cactus_transcribe" - device_model_root = f"{device_root}/models" - device_audio_root = f"{device_root}/audio" - device_bin_root = f"{device_root}/bin" - device_audio = f"{device_audio_root}/{audio_path.name}" - device_model = f"{device_model_root}/{model_name}" - - subprocess.run(["adb", "-s", device_id, "shell", f"mkdir -p {device_bin_root} {device_model_root} {device_audio_root}"], check=False) - if subprocess.run(["adb", "-s", device_id, "push", str(asr_bin), f"{device_bin_root}/asr"]).returncode != 0: - print_color(RED, "Failed to push Android asr binary") - return 1 - subprocess.run(["adb", "-s", device_id, "shell", f"chmod +x {device_bin_root}/asr"], check=False) - if subprocess.run(["adb", "-s", device_id, "push", str(weights_dir), device_model_root]).returncode != 0: - print_color(RED, "Failed to push ASR model weights to device") - return 1 - if subprocess.run(["adb", "-s", device_id, "push", str(audio_path), device_audio]).returncode != 0: - print_color(RED, "Failed to push audio file to device") - return 1 - - cloud_api_key = os.environ.get("CACTUS_CLOUD_KEY", os.environ.get("CACTUS_CLOUD_API_KEY", "")) - cloud_strict_ssl = os.environ.get("CACTUS_CLOUD_STRICT_SSL", "") - cloud_handoff_threshold = os.environ.get("CACTUS_CLOUD_HANDOFF_THRESHOLD", "") - ca_bundle = os.environ.get("CACTUS_CA_BUNDLE", "") - ca_path = os.environ.get("CACTUS_CA_PATH", "") - force_handoff = os.environ.get("CACTUS_FORCE_HANDOFF", "") - env_exports = [] - if cloud_api_key: - env_exports.append(f"export CACTUS_CLOUD_KEY='{cloud_api_key}'") - if cloud_strict_ssl: - env_exports.append(f"export CACTUS_CLOUD_STRICT_SSL='{cloud_strict_ssl}'") - if cloud_handoff_threshold: - env_exports.append(f"export CACTUS_CLOUD_HANDOFF_THRESHOLD='{cloud_handoff_threshold}'") - if ca_bundle: - env_exports.append(f"export CACTUS_CA_BUNDLE='{ca_bundle}'") - if ca_path: - env_exports.append(f"export CACTUS_CA_PATH='{ca_path}'") - if getattr(args, "no_cloud_tele", False): - env_exports.append("export CACTUS_NO_CLOUD_TELE=1") - if force_handoff: - env_exports.append(f"export CACTUS_FORCE_HANDOFF='{force_handoff}'") - - shell_cmd = " && ".join(env_exports + [f"{device_bin_root}/asr {device_model} {device_audio}"]) - print_color(BLUE, "Running Android transcription...") - return subprocess.run(["adb", "-s", device_id, "shell", shell_cmd]).returncode - - -def _cmd_transcribe_ios(weights_dir, audio_file, args): - if not audio_file: - print_color(RED, "Error: --ios requires --file ") - return 1 - - audio_path = Path(audio_file).expanduser().resolve() - if not audio_path.exists(): - print_color(RED, f"Error: audio file not found: {audio_path}") - return 1 - - ios_script = PROJECT_ROOT / "tests" / "ios" / "run.sh" - if not ios_script.exists(): - print_color(RED, f"Error: iOS runner not found at {ios_script}") - return 1 - - transcribe_model_id = Path(weights_dir).name - env = os.environ.copy() - env["CACTUS_RUN_ASR"] = "1" - env["CACTUS_ASR_AUDIO_SOURCE"] = str(audio_path) - env["CACTUS_ASR_AUDIO_FILE"] = audio_path.name - - cmd = [str(ios_script), transcribe_model_id, transcribe_model_id, "snakers4/silero-vad"] - print_color(BLUE, "Running iOS transcription...") - return subprocess.run(cmd, cwd=PROJECT_ROOT / "tests" / "ios", env=env).returncode - - -def cmd_transcribe(args): - """Download ASR model if needed and start transcription.""" - from .config_utils import CactusConfig - - config = CactusConfig() - api_key = prompt_for_api_key(config) - - if api_key: - os.environ["CACTUS_CLOUD_KEY"] = api_key - - model_id = getattr(args, 'model_id', DEFAULT_ASR_MODEL_ID) - audio_file = getattr(args, 'audio_file', None) - - if getattr(args, 'no_cloud_tele', False): - os.environ["CACTUS_NO_CLOUD_TELE"] = "1" - - if getattr(args, 'force_handoff', False): - os.environ["CACTUS_FORCE_HANDOFF"] = "1" - else: - os.environ.pop("CACTUS_FORCE_HANDOFF", None) - - audio_extensions = ('.wav', '.mp3', '.flac', '.ogg', '.m4a', '.aac') - if model_id and model_id.lower().endswith(audio_extensions): - audio_file = model_id - model_id = DEFAULT_ASR_MODEL_ID - args.model_id = model_id - - local_path = Path(model_id) - if local_path.exists() and (local_path / "config.txt").exists(): - weights_dir = local_path - print_color(GREEN, f"Using local model: {weights_dir}") - else: - download_result = cmd_download(args) - if download_result != 0: - return download_result - weights_dir = get_weights_dir(model_id) - - if getattr(args, 'android', False) and getattr(args, 'ios', False): - print_color(RED, "Error: choose only one of --android or --ios") - return 1 - if getattr(args, 'android', False): - return _cmd_transcribe_android(weights_dir, audio_file, args) - if getattr(args, 'ios', False): - return _cmd_transcribe_ios(weights_dir, audio_file, args) - - asr_binary = PROJECT_ROOT / "tests" / "build" / "asr" - if not asr_binary.exists(): - print_color(RED, "Error: ASR binary not built. Run 'cactus build' first.") - return 1 - - os.system('clear' if platform.system() != 'Windows' else 'cls') - print_color(GREEN, f"Starting Cactus ASR with model: {model_id}") - print() - - cmd_args = [str(asr_binary), str(weights_dir)] - if audio_file: - cmd_args.append(audio_file) - if hasattr(args, 'language') and args.language: - cmd_args.extend(['--language', args.language]) - - os.execv(str(asr_binary), cmd_args) - - -def cmd_auth(args): - """Manage Cactus Cloud API key.""" - from .config_utils import CactusConfig - - config = CactusConfig() - - if args.clear: - config.clear_api_key() - print_color(GREEN, "API key cleared.") - return 0 - - api_key = config.get_api_key() - - if api_key: - masked = api_key[:4] + "..." + api_key[-4:] - print(f"Current API key: {masked}") - else: - print("No API key set.") - - if args.status: - return 0 - - print() - print("Get your cloud key at \033[1;36mhttps://www.cactuscompute.com/dashboard/api-keys\033[0m") - new_key = input("Enter new API key (press Enter to skip): ").strip() - if new_key: - config.set_api_key(new_key) - masked = new_key[:4] + "..." + new_key[-4:] - print_color(GREEN, f"API key saved: {masked}") - return 0 - - -def cmd_eval(args): - model_id = getattr(args, 'model_id', DEFAULT_MODEL_ID) - - if PROJECT_ROOT.parent.name != 'evals': - print_color(RED, "Skipping internal eval checks: companion repo not found.") - return 1 - - # Check if cactus library exists - lib_path = PROJECT_ROOT / "cactus" / "build" / "libcactus.a" - if not lib_path.exists(): - print_color(RED, "Error: Cactus library not built. Run 'cactus build' first.") - return 1 - - class DownloadArgs: - pass - - dlargs = DownloadArgs() - dlargs.model_id = model_id - dlargs.precision = getattr(args, 'precision', 'INT4') - dlargs.cache_dir = getattr(args, 'cache_dir', None) - dlargs.token = getattr(args, 'token', None) - dlargs.reconvert = getattr(args, 'reconvert', False) - - download_result = cmd_download(dlargs) - if download_result != 0: - return download_result - - weights_dir = get_effective_weights_dir(model_id, args) - extra = getattr(args, 'extra_args', None) or [] - - def extra_has_flag(flag: str) -> bool: - for a in extra: - if a == flag or a.startswith(flag + "="): - return True - return False - - mode_flags = [] - if getattr(args, 'tools', False): mode_flags.append('tools') - if getattr(args, 'llm', False): mode_flags.append('llm') - if getattr(args, 'stt', False): mode_flags.append('stt') - if getattr(args, 'vlm', False): mode_flags.append('vlm') - if getattr(args, 'embed', False): mode_flags.append('embed') - - if len(mode_flags) > 1: - print_color(RED, f"Error: choose only one eval mode flag, got: {' '.join(mode_flags)}") - return 1 - - mode = mode_flags[0] if mode_flags else "tools" - repo_root = PROJECT_ROOT.parent # evals/ - cwd = repo_root - - if mode == "tools": - eval_runner = repo_root / "tool-evals" / "run_eval_berk.py" - elif mode == "stt": - eval_runner = repo_root / "speech-evals" / "speech_eval.py" - elif mode == "llm": - eval_runner = repo_root / "text-evals" / "perplexity_eval.py" - elif mode == "vlm": - eval_runner = repo_root / "video-evals" / "run_benchmarks.py" - elif mode == "embed": - print_color(RED, f"Error: eval mode '{mode}' is not supported in this repo layout") - return 1 - else: - print_color(RED, f"Error: unknown eval mode '{mode}'") - return 1 - - if not eval_runner.exists(): - print_color(RED, f"Eval runner not found at {eval_runner}") - return 1 - - cmd = [sys.executable, str(eval_runner)] - - if mode == "vlm": - if not extra_has_flag("--model"): - cmd += ["--model", str(weights_dir)] - if not extra_has_flag("--all") and not extra_has_flag("--benchmarks"): - cmd += ["--all"] - else: - if not extra_has_flag("--model-path"): - cmd += ["--model-path", str(weights_dir)] - - if mode == "llm" and not extra_has_flag("--model-id"): - cmd += ["--model-id", str(model_id)] - - if mode == "stt" and not extra_has_flag("--dataset-path"): - default_dataset_path = repo_root / "speech-evals" / "dataset-retrieval" - cmd += ["--dataset-path", str(default_dataset_path)] - - if not extra_has_flag("--output-dir"): - if mode == "tools": - default_out = repo_root / "tool-evals" / "results" - elif mode == "stt": - default_out = repo_root / "speech-evals" / "results" - elif mode == "llm": - default_out = repo_root / "text-evals" / "results" - else: - default_out = None - if default_out is not None: - cmd += ["--output-dir", str(default_out)] - - cmd += extra - - print_color(BLUE, f"[cactus] launching {mode} eval runner") - print(" ".join(cmd)) - - env = os.environ.copy() - if getattr(args, 'no_cloud_tele', False): - env["CACTUS_NO_CLOUD_TELE"] = "1" - if mode == "vlm": - ffi_dir = str(repo_root / "cactus" / "tools" / "src") - existing = env.get("PYTHONPATH", "") - env["PYTHONPATH"] = ffi_dir if not existing else (ffi_dir + os.pathsep + existing) - - r = subprocess.run(cmd, cwd=str(cwd), env=env) - return r.returncode - - -def cmd_test(args): - """Run the Cactus test suite.""" - print_color(BLUE, "Running test suite...") - print("=" * 20) - - if getattr(args, 'ios', False) and not getattr(args, 'reconvert', False): - print_color( - YELLOW, - "Warning: iOS tests without --reconvert may use stale or inconsistent local weights. " - "If tests fail unexpectedly, rerun with --reconvert." - ) - - if getattr(args, 'benchmark', False): - args.model = 'LiquidAI/LFM2.5-VL-1.6B' - args.transcribe_model = 'nvidia/parakeet-ctc-1.1b' - print_color(BLUE, f"Using large models: {args.model}, {args.transcribe_model}") - - if getattr(args, 'reconvert', False): - reconvert_models = [ - getattr(args, 'model', 'LiquidAI/LFM2-VL-450M'), - getattr(args, 'transcribe_model', DEFAULT_TEST_TRANSCRIBE_MODEL_ID), - ] - for model_id in reconvert_models: - class DownloadArgs: - pass - dl_args = DownloadArgs() - dl_args.model_id = model_id - dl_args.reconvert = True - dl_args.cache_dir = None - if args.precision: - dl_args.precision = args.precision - else: - is_asr = 'whisper' in model_id.lower() or 'moonshine' in model_id.lower() or 'silero-vad' in model_id.lower() - is_fp16_only = 'segmentation-3.0' in model_id.lower() or 'wespeaker' in model_id.lower() - dl_args.precision = 'FP16' if is_fp16_only else ('INT8' if is_asr else 'INT4') - if args.token: - dl_args.token = args.token - if cmd_download(dl_args) != 0: - return 1 - - test_script = PROJECT_ROOT / "tests" / "run.sh" - - if not test_script.exists(): - print_color(RED, f"Error: Test script not found at {test_script}") - return 1 - - cmd = [str(test_script)] - - if args.model: - cmd.extend(["--model", args.model]) - if args.transcribe_model: - cmd.extend(["--transcribe_model", args.transcribe_model]) - if args.precision: - cmd.extend(["--precision", args.precision]) - if getattr(args, 'no_rebuild', False): - cmd.append("--no-rebuild") - if args.android: - cmd.append("--android") - if args.ios: - cmd.append("--ios") - if getattr(args, 'exhaustive', False): - cmd.append("--exhaustive") - test_filter = args.only - for _test_name in ['llm', 'vlm', 'stt', 'embed', 'rag', 'graph', 'index', 'kernel', 'kv_cache', 'performance']: - if getattr(args, _test_name, False): - test_filter = _test_name - break - if test_filter: - cmd.extend(["--only", test_filter]) - env = os.environ.copy() - if getattr(args, 'enable_telemetry', False): - env.pop("CACTUS_NO_CLOUD_TELE", None) - else: - env["CACTUS_NO_CLOUD_TELE"] = "1" - - result = subprocess.run(cmd, cwd=PROJECT_ROOT / "tests", env=env) - return result.returncode - - -def cmd_clean(args): - """Remove all build artifacts, caches, and downloaded weights.""" - print_color(BLUE, "Cleaning all build artifacts from Cactus project...") - print(f"Project root: {PROJECT_ROOT}") - print() - - def remove_if_exists(path): - if path.is_dir(): - print(f"Removing: {path}") - shutil.rmtree(path) - else: - print(f"Not found: {path}") - - remove_if_exists(PROJECT_ROOT / "cactus" / "build") - - remove_if_exists(PROJECT_ROOT / "android" / "build") - remove_if_exists(PROJECT_ROOT / "android" / "libs") - remove_if_exists(PROJECT_ROOT / "android" / "arm64-v8a") - - remove_if_exists(PROJECT_ROOT / "apple" / "build") - - remove_if_exists(PROJECT_ROOT / "tests" / "build") - - remove_if_exists(PROJECT_ROOT / "venv") - - remove_if_exists(PROJECT_ROOT / "weights") - - # Clean telemetry cache - telemetry_cache = Path.home() / "Library" / "Caches" / "cactus" / "telemetry" - if telemetry_cache.exists(): - print(f"Removing telemetry cache: {telemetry_cache}") - shutil.rmtree(telemetry_cache) - else: - print(f"Telemetry cache not found: {telemetry_cache}") - - # Re-cache API key from config so users don't need to run `cactus auth` again - from .config_utils import CactusConfig - config = CactusConfig() - saved_key = config.load_config().get("api_key", "") - if saved_key: - config.cache_api_key(saved_key) - masked = saved_key[:4] + "..." + saved_key[-4:] - print(f"Restored cached API key: {masked}") - - print() - print("Removing compiled libraries and frameworks...") - - preserve_roots = [ - PROJECT_ROOT / "libs" / "curl", - PROJECT_ROOT / "android" / "mbedtls", - PROJECT_ROOT / "libs" / "mbedtls", - ] - - def should_preserve_artifact(path: Path) -> bool: - try: - resolved = path.resolve() - except FileNotFoundError: - return False - for root in preserve_roots: - try: - if resolved.is_relative_to(root.resolve()): - return True - except FileNotFoundError: - continue - return False - - so_count = 0 - for so_file in PROJECT_ROOT.rglob("*.so"): - so_file.unlink() - so_count += 1 - print(f"Removed {so_count} .so files" if so_count else "No .so files found") - - a_count = 0 - a_preserved_count = 0 - for a_file in PROJECT_ROOT.rglob("*.a"): - if should_preserve_artifact(a_file): - a_preserved_count += 1 - continue - a_file.unlink() - a_count += 1 - if a_count or a_preserved_count: - print(f"Removed {a_count} .a files (preserved {a_preserved_count} vendored static libs)") - else: - print("No .a files found") - - bin_count = 0 - for bin_file in PROJECT_ROOT.rglob("*.bin"): - bin_file.unlink() - bin_count += 1 - print(f"Removed {bin_count} .bin files" if bin_count else "No .bin files found") - - xcf_count = 0 - for xcf_dir in PROJECT_ROOT.rglob("*.xcframework"): - if xcf_dir.is_dir(): - shutil.rmtree(xcf_dir) - xcf_count += 1 - print(f"Removed {xcf_count} .xcframework directories" if xcf_count else "No .xcframework directories found") - - pycache_count = 0 - for pycache_dir in PROJECT_ROOT.rglob("__pycache__"): - if pycache_dir.is_dir(): - shutil.rmtree(pycache_dir) - pycache_count += 1 - print(f"Removed {pycache_count} __pycache__ directories" if pycache_count else "No __pycache__ directories found") - - egg_count = 0 - for egg_dir in PROJECT_ROOT.rglob("*.egg-info"): - if egg_dir.is_dir(): - shutil.rmtree(egg_dir) - egg_count += 1 - print(f"Removed {egg_count} .egg-info directories" if egg_count else "No .egg-info directories found") - - print() - print_color(GREEN, "Clean complete!") - print("All build artifacts have been removed.") - print() - - # Re-run setup automatically - print_color(BLUE, "Re-running setup...") - setup_script = PROJECT_ROOT / "setup" - result = subprocess.run( - ["bash", "-c", f"source {setup_script}"], - cwd=PROJECT_ROOT - ) - if result.returncode == 0: - print_color(GREEN, "Setup complete!") - else: - print_color(YELLOW, "Setup had issues. Please run manually:") - print(" source ./setup") - return 0 - - -def merge_lora_adapter(base_model_id, lora_path, cache_dir=None, token=None): - """Merge a LoRA adapter into a base model and return the merged model.""" - try: - from peft import PeftModel - except ImportError: - print_color(RED, "Error: peft package required for LoRA merging") - print("Install with: pip install peft") - return None, None - - from transformers import AutoModelForCausalLM, AutoTokenizer - - print_color(YELLOW, f"Loading base model: {base_model_id}") - base_model = AutoModelForCausalLM.from_pretrained( - base_model_id, - cache_dir=cache_dir, - trust_remote_code=True, - dtype=torch.bfloat16, - token=token - ) - tokenizer = AutoTokenizer.from_pretrained( - base_model_id, - cache_dir=cache_dir, - trust_remote_code=True, - token=token - ) - - print_color(YELLOW, f"Loading LoRA adapter: {lora_path}") - model = PeftModel.from_pretrained(base_model, lora_path, token=token) - - print_color(YELLOW, "Merging LoRA weights into base model...") - merged_model = model.merge_and_unload() - - print_color(GREEN, "LoRA merge complete") - return merged_model, tokenizer - - -def cmd_convert(args): - """Convert a HuggingFace model to a custom output directory.""" - import tempfile - - model_id = args.model_name - output_dir = args.output_dir - lora_path = getattr(args, 'lora', None) - - if output_dir is None: - output_dir = get_weights_dir(model_id) - else: - output_dir = Path(output_dir) - - cache_dir = getattr(args, 'cache_dir', None) - token = getattr(args, 'token', None) - - temp_merged_dir = None - - if lora_path: - merged_model, tokenizer = merge_lora_adapter(model_id, lora_path, cache_dir, token) - if merged_model is None: - return 1 - - temp_merged_dir = tempfile.mkdtemp(prefix="cactus_lora_merged_") - print_color(YELLOW, f"Saving merged model to temp directory: {temp_merged_dir}") - merged_model.save_pretrained(temp_merged_dir) - tokenizer.save_pretrained(temp_merged_dir) - - lora_tok_config = Path(lora_path) / "tokenizer_config.json" - if lora_tok_config.exists(): - shutil.copy2(lora_tok_config, Path(temp_merged_dir) / "tokenizer_config.json") - - del merged_model - import torch - if torch.cuda.is_available(): - torch.cuda.empty_cache() - - model_id = temp_merged_dir - - class DownloadArgs: - pass - - download_args = DownloadArgs() - download_args.model_id = model_id - download_args.original_model_id = args.model_name - download_args.precision = args.precision - download_args.cache_dir = cache_dir - download_args.token = token - download_args.reconvert = True - - original_get_weights = get_weights_dir - - def custom_weights_dir(mid): - return output_dir - - import src.cli as cli_module - cli_module.get_weights_dir = custom_weights_dir - - try: - result = cmd_download(download_args) - return result - finally: - cli_module.get_weights_dir = original_get_weights - if temp_merged_dir and Path(temp_merged_dir).exists(): - print_color(YELLOW, "Cleaning up temp directory...") - shutil.rmtree(temp_merged_dir) - - -def cmd_list(args): - """List all supported models and their download status.""" - PIPELINE_DISPLAY = { - "text-generation": "Text Generation", - "image-text-to-text": "Vision", - "automatic-speech-recognition": "Speech Recognition", - "feature-extraction": "Embeddings", - "voice-activity-detection": "Voice Activity Detection", - } - PIPELINE_ORDER = list(PIPELINE_DISPLAY.keys()) - SHOW_TAGS = {"tools", "vision", "embed", "transcription"} - EMBED_ALIASES = {"text-embed", "image-embed", "speech-embed"} - - DIM = '\033[2m' - BOLD = '\033[1m' - - def filter_tags(tags): - result = set() - for t in tags: - if t in SHOW_TAGS: - result.add(t) - elif t in EMBED_ALIASES: - result.add("embed") - return sorted(result) - - def get_dir_size(path): - total = 0 - for entry in path.rglob('*'): - if entry.is_file(): - total += entry.stat().st_size - return total - - def format_size(size_bytes): - if size_bytes >= 1_000_000_000: - return f"{size_bytes / 1_073_741_824:.1f} GB" - return f"{size_bytes / 1_048_576:.0f} MB" - - # Group models by pipeline_tag preserving order - groups = {} - for entry in MODELS_REGISTRY: - tag = entry["pipeline_tag"] - groups.setdefault(tag, []).append(entry) - - # Find max model name length for alignment - max_name = max(len(e["model"]) for e in MODELS_REGISTRY) - max_tags_len = 20 - - only_downloaded = getattr(args, 'downloaded', False) - - if only_downloaded: - print(f"\n {BOLD}Downloaded Models{NC}") - else: - print(f"\n {BOLD}Supported Models{NC}") - print(f" {'─' * 66}") - - for ptag in PIPELINE_ORDER: - models = groups.get(ptag) - if not models: - continue - - section = PIPELINE_DISPLAY[ptag] - section_printed = False - - for entry in models: - model_id = entry["model"] - tags = filter_tags(entry["tags"]) - tags_str = ", ".join(tags) - - weights_dir = get_weights_dir(model_id) - config_path = weights_dir / "config.txt" - downloaded = config_path.exists() - - if only_downloaded and not downloaded: - continue - - if not section_printed: - print(f"\n {BOLD}{section}{NC}") - section_printed = True - - if downloaded: - prefix = f" {GREEN}\u2b07{NC} " - # Read quantization (weight quantization level, not compute precision) - quantization = "" - try: - for line in config_path.read_text().splitlines(): - if line.startswith("quantization="): - quantization = line.split("=", 1)[1].strip() - break - except OSError: - pass - dir_size = get_dir_size(weights_dir) - size_str = format_size(dir_size) - if quantization: - info = f"{size_str} ({quantization})" - else: - info = size_str - else: - prefix = " " - info = "" - - name_pad = model_id.ljust(max_name) - tags_pad = tags_str.ljust(max_tags_len) - - if info: - print(f"{prefix}{name_pad} {DIM}{tags_pad}{NC} {info}") - else: - print(f"{prefix}{name_pad} {DIM}{tags_pad}{NC}") - - print() - return 0 - - -def create_parser(): - """Create the argument parser with all subcommands.""" - parser = argparse.ArgumentParser( - formatter_class=argparse.RawDescriptionHelpFormatter, - usage=argparse.SUPPRESS, - description=""" - - ----------------------------------------------------------------- - - How to use the Cactus Repo/CLI: - - ----------------------------------------------------------------- - - cactus auth manage Cactus Cloud API key - shows status and prompts to set key - - Optional flags: - --status show key status without prompting - --clear remove the saved API key - - ----------------------------------------------------------------- - - cactus run opens playground for the model - auto downloads and spins up - - Optional flags: - --precision INT4|INT8|FP16 default: INT4 - --token HF token (for gated models) - --reconvert force model weights reconversion from source - - ----------------------------------------------------------------- - - cactus transcribe [model] live microphone transcription - default model: parakeet-tdt-0.6b-v3 - - Optional flags: - --file transcribe audio file instead of mic - --precision INT4|INT8|FP16 default: INT4 - --token HF token (for gated models) - --reconvert force model weights reconversion from source - - Examples: - cactus transcribe live microphone transcription - cactus transcribe --file audio.wav transcribe single file - cactus transcribe nvidia/parakeet-ctc-1.1b use different model - cactus transcribe nvidia/parakeet-tdt-0.6b-v3 --file audio.wav - - ----------------------------------------------------------------- - - cactus download downloads model to ./weights - see supported weights on ReadMe - - Optional flags: - --precision INT4|INT8|FP16 quantization (default: INT4) - --token HuggingFace API token - --reconvert force model weights reconversion from source - - ----------------------------------------------------------------- - - cactus convert [output_dir] converts model to custom directory - supports LoRA adapter merging - - Optional flags: - --precision INT4|INT8|FP16 quantization (default: INT4) - --lora LoRA adapter path to merge - --token HuggingFace API token - - ----------------------------------------------------------------- - - cactus build builds cactus for ARM chips - output: build/libcactus.a - - Optional flags: - --apple build for Apple (iOS/macOS) - --android build for Android - --python build shared lib for Python FFI - - ----------------------------------------------------------------- - - cactus test runs unit tests and benchmarks - all must pass for contributions - - Optional flags: - --model default: LiquidAI/LFM2-VL-450M - --transcribe_model default: openai/whisper-base - --benchmark use larger models (LFM2.5-VL-1.6B + nvidia/parakeet-ctc-1.1b) - --precision INT4|INT8|FP16 regenerates weights with precision - --reconvert force model weights reconversion from source - --no-rebuild skip building library and tests - --llm run only LLM tests - --vlm run only VLM tests - --stt run only speech-to-text tests - --embed run only embedding tests - --rag run only RAG tests - --graph run only graph tests - --index run only index tests - --kernel run only kernel tests - --kv_cache run only KV cache tests - --performance run only performance benchmarks - --ios run on connected iPhone - --android run on connected Android - - ----------------------------------------------------------------- - - cactus list list all supported models - shows download status - - ----------------------------------------------------------------- - - cactus clean removes all build artifacts - - ----------------------------------------------------------------- - - cactus --help shows these instructions - - ----------------------------------------------------------------- - - Python bindings: - - Cactus python package is auto installed for researchers and testing - Please see python/example.py and run the following instructions. - - 1. cactus build - 2. cactus download LiquidAI/LFM2-VL-450M - 3. python python/example.py - - Note: Use any supported model - - ----------------------------------------------------------------- -""" - ) - - subparsers = parser.add_subparsers(dest='command') - subparsers.required = False - - for action in parser._actions: - if isinstance(action, argparse._SubParsersAction): - action.help = argparse.SUPPRESS - - parser._action_groups = [] - - download_parser = subparsers.add_parser('download', help='Download and convert model weights') - download_parser.add_argument('model_id', nargs='?', default=DEFAULT_MODEL_ID, - help=f'HuggingFace model ID (default: {DEFAULT_MODEL_ID})') - download_parser.add_argument('--precision', choices=['INT4', 'INT8', 'FP16'], default='INT4', - help='Quantization precision (default: INT4)') - download_parser.add_argument('--cache-dir', help='Cache directory for HuggingFace models') - download_parser.add_argument('--token', help='HuggingFace API token') - - download_parser.add_argument('--weights-variant', choices=WEIGHTS_VARIANT_CHOICES, default='auto', - help='Weights package preference: auto (default), apple, or standard') - download_parser.add_argument('--reconvert', action='store_true', - help='Download original model and convert (instead of using pre-converted from Cactus-Compute)') - - build_parser = subparsers.add_parser('build', help='Build the chat application') - build_parser.add_argument('--apple', action='store_true', - help='Build for Apple platforms (iOS/macOS)') - build_parser.add_argument('--android', action='store_true', - help='Build for Android') - build_parser.add_argument('--python', action='store_true', - help='Build shared library for Python FFI') - - run_parser = subparsers.add_parser('run', help='Build, download (if needed), and run chat') - run_parser.add_argument('model_id', nargs='?', default=DEFAULT_MODEL_ID, - help=f'HuggingFace model ID (default: {DEFAULT_MODEL_ID})') - run_parser.add_argument('--precision', choices=['INT4', 'INT8', 'FP16'], default='INT4', - help='Quantization precision (default: INT4)') - run_parser.add_argument('--cache-dir', help='Cache directory for HuggingFace models') - run_parser.add_argument('--token', help='HuggingFace API token') - run_parser.add_argument('--weights-variant', choices=WEIGHTS_VARIANT_CHOICES, default='auto', - help='Weights package preference for auto-download: auto, apple, or standard') - run_parser.add_argument('--no-cloud-tele', action='store_true', - help='Disable cloud telemetry (write to cache only)') - run_parser.add_argument('--reconvert', action='store_true', - help='Download original model and convert (instead of using pre-converted from Cactus-Compute)') - run_parser.add_argument('--image', - help='Path to image file for VLM inference (attached to first message)') - run_parser.add_argument('--audio', - help='Path to audio file (WAV) for audio chat (attached to first message)') - run_parser.add_argument('--system', - help='System prompt to prepend to all messages') - run_parser.add_argument('--prompt', - help='Initial prompt to send immediately') - run_parser.add_argument('--thinking', action='store_true', - help='Enable thinking/reasoning for models that support it') - - transcribe_parser = subparsers.add_parser('transcribe', help='Download ASR model and run transcription') - transcribe_parser.add_argument('model_id', nargs='?', default=DEFAULT_ASR_MODEL_ID, - help=f'HuggingFace model ID (default: {DEFAULT_ASR_MODEL_ID})') - transcribe_parser.add_argument('--file', dest='audio_file', default=None, - help='Audio file to transcribe (WAV format). Omit for live microphone.') - transcribe_parser.add_argument('--language', default='en', - help='Language code for transcription (default: en). Examples: es, fr, de, zh, ja') - transcribe_parser.add_argument('--precision', choices=['INT4', 'INT8', 'FP16'], default='INT4', - help='Quantization precision (default: INT4)') - transcribe_parser.add_argument('--cache-dir', help='Cache directory for HuggingFace models') - transcribe_parser.add_argument('--token', help='HuggingFace API token') - transcribe_parser.add_argument('--no-cloud-tele', action='store_true', - help='Disable cloud telemetry (write to cache only)') - transcribe_parser.add_argument('--force-handoff', action='store_true', - help='Force cloud handoff by assuming low confidence') - transcribe_parser.add_argument('--reconvert', action='store_true', - help='Download original model and convert (instead of using pre-converted from Cactus-Compute)') - transcribe_parser.add_argument('--android', action='store_true', - help='Run transcription on a connected Android device (requires --file)') - transcribe_parser.add_argument('--ios', action='store_true', - help='Run transcription on a connected iOS device (requires --file)') - transcribe_parser.add_argument('--device', default=None, - help='ADB device ID to use with --android') - - eval_parser = subparsers.add_parser('eval', help='Run evaluation scripts outside the cactus submodule') - eval_parser.add_argument('model_id', nargs='?', default=DEFAULT_MODEL_ID, - help=f'HuggingFace model ID (default: {DEFAULT_MODEL_ID})') - eval_parser.add_argument('--precision', choices=['INT4', 'INT8', 'FP16'], default='INT4', - help='Quantization precision (default: INT4)') - eval_parser.add_argument('--cache-dir', help='Cache directory for HuggingFace models') - eval_parser.add_argument('--token', help='HuggingFace API token') - eval_parser.add_argument('--weights-variant', choices=WEIGHTS_VARIANT_CHOICES, default='auto', - help='Weights package preference for auto-download: auto, apple, or standard') - eval_parser.add_argument('--tools', action='store_true', help='Run tools evals (default)') - eval_parser.add_argument('--vlm', action='store_true', help='Run VLM-specific evals') - eval_parser.add_argument('--stt', action='store_true', help='Run speech-to-text evals') - eval_parser.add_argument('--llm', action='store_true', help='Run LLM evals') - eval_parser.add_argument('--embed', action='store_true', help='Run embedding evals') - eval_parser.add_argument('--no-cloud-tele', action='store_true', - help='Disable cloud telemetry (write to cache only)') - eval_parser.add_argument('--reconvert', action='store_true', - help='Download original model and convert (instead of using pre-converted from Cactus-Compute)') - - test_parser = subparsers.add_parser('test', help='Run the test suite') - test_parser.add_argument('--model', default='LiquidAI/LFM2-VL-450M', - help='Model to use for tests') - test_parser.add_argument('--transcribe_model', default=DEFAULT_TEST_TRANSCRIBE_MODEL_ID, - help='Transcribe model to use') - test_parser.add_argument('--benchmark', action='store_true', - help='Use larger models (LFM2.5-VL-1.6B + nvidia/parakeet-ctc-1.1b)') - test_parser.add_argument('--precision', choices=['INT4', 'INT8', 'FP16'], - help='Regenerate weights with this precision (deletes existing weights)') - test_parser.add_argument('--no-rebuild', action='store_true', - help='Skip building cactus library and tests') - test_parser.add_argument('--token', help='HuggingFace API token') - test_parser.add_argument('--android', action='store_true', - help='Run tests on Android') - test_parser.add_argument('--ios', action='store_true', - help='Run tests on iOS') - test_parser.add_argument('--exhaustive', action='store_true', - help='Run exhaustive golden tests for all model families and precisions') - test_parser.add_argument('--only', help='(deprecated, use -- instead) Only run the specified test') - for _test_name in ['llm', 'vlm', 'stt', 'embed', 'rag', 'graph', 'index', 'kernel', 'kv_cache', 'performance']: - test_parser.add_argument(f'--{_test_name}', action='store_true', - help=f'Only run the {_test_name} tests') - test_parser.add_argument('--enable-telemetry', action='store_true', - help='Enable cloud telemetry (disabled by default in tests)') - test_parser.add_argument('--reconvert', action='store_true', - help='Download original model and convert (instead of using pre-converted from Cactus-Compute)') - - auth_parser = subparsers.add_parser('auth', help='Manage Cactus Cloud API key') - auth_parser.add_argument('--clear', action='store_true', - help='Remove the saved API key') - auth_parser.add_argument('--status', action='store_true', - help='Show current key status without prompting') - - clean_parser = subparsers.add_parser('clean', help='Remove all build artifacts') - - list_parser = subparsers.add_parser('list', help='List supported models') - list_parser.add_argument('--downloaded', action='store_true', - help='Only show downloaded models') - - convert_parser = subparsers.add_parser('convert', help='Convert model to custom output directory') - convert_parser.add_argument('model_name', help='HuggingFace model name') - convert_parser.add_argument('output_dir', nargs='?', default=None, - help='Output directory (default: weights/)') - convert_parser.add_argument('--precision', choices=['INT4', 'INT8', 'FP16'], default='INT4', - help='Quantization precision (default: INT4)') - convert_parser.add_argument('--cache-dir', help='Cache directory for HuggingFace models') - convert_parser.add_argument('--token', help='HuggingFace API token') - convert_parser.add_argument('--lora', help='Path to LoRA adapter (local path or HuggingFace ID) to merge before conversion') - - return parser - - -def preprocess_eval_args(parser, argv): - args, unknown = parser.parse_known_args(argv) - - if getattr(args, 'command', None) == 'eval': - setattr(args, 'extra_args', unknown) - return args - - if unknown: - parser.error(f"unrecognized arguments: {' '.join(unknown)}") - - return args - - -def main(): - """Main entry point for the Cactus CLI.""" - parser = create_parser() - - argv = sys.argv[1:] - args = preprocess_eval_args(parser, argv) - - if args.command == 'download': - sys.exit(cmd_download(args)) - elif args.command == 'build': - sys.exit(cmd_build(args)) - elif args.command == 'run': - sys.exit(cmd_run(args)) - elif args.command == 'transcribe': - sys.exit(cmd_transcribe(args)) - elif args.command == 'test': - sys.exit(cmd_test(args)) - elif args.command == 'eval': - sys.exit(cmd_eval(args)) - elif args.command == 'auth': - sys.exit(cmd_auth(args)) - elif args.command == 'clean': - sys.exit(cmd_clean(args)) - elif args.command == 'list': - sys.exit(cmd_list(args)) - elif args.command == 'convert': - sys.exit(cmd_convert(args)) - else: - parser.print_help() - sys.exit(1) - - -if __name__ == '__main__': - main() diff --git a/python/src/config_utils.py b/python/src/config_utils.py deleted file mode 100644 index 78ca17809..000000000 --- a/python/src/config_utils.py +++ /dev/null @@ -1,525 +0,0 @@ -import json -import math -import os -from pathlib import Path -from typing import Optional, Any, Dict - - -class CactusConfig: - def __init__(self): - self.config_dir = Path.home() / ".cactus" - self.config_file = self.config_dir / "config.json" - self.config_dir.mkdir(exist_ok=True) - self.telemetry_cache_dir = Path.home() / "Library" / "Caches" / "cactus" / "telemetry" - self.cloud_api_key_cache_file = self.telemetry_cache_dir / "cloud_api_key" - - def load_config(self): - if self.config_file.exists(): - return json.loads(self.config_file.read_text()) - return {} - - def save_config(self, config): - self.config_file.write_text(json.dumps(config, indent=2)) - - def get_api_key(self): - env_key = os.getenv("CACTUS_CLOUD_KEY") - if not env_key: - env_key = os.getenv("CACTUS_CLOUD_API_KEY") - if env_key: - return env_key - config = self.load_config() - config_key = config.get("api_key", "") - if config_key: - return config_key - return self.load_cached_api_key() - - def cache_api_key(self, key): - if not key: - return - self.telemetry_cache_dir.mkdir(parents=True, exist_ok=True) - self.cloud_api_key_cache_file.write_text(key) - - def load_cached_api_key(self): - if not self.cloud_api_key_cache_file.exists(): - return "" - return self.cloud_api_key_cache_file.read_text().strip() - - def clear_cached_api_key(self): - if self.cloud_api_key_cache_file.exists(): - self.cloud_api_key_cache_file.unlink() - - def set_api_key(self, key): - config = self.load_config() - config["api_key"] = key - self.save_config(config) - self.cache_api_key(key) - - def clear_api_key(self): - config = self.load_config() - config.pop("api_key", None) - self.save_config(config) - self.clear_cached_api_key() - - -def cfg_get(c, key, default=None): - """Get a config value from a dict or object, with fallback default.""" - if c is None: - return default - try: - if isinstance(c, dict): - return c.get(key, default) - except Exception: - pass - try: - return getattr(c, key, default) - except Exception: - return default - - -def detect_model_type(cfg, config, output_dir=None): - """Detect the model architecture type from config.""" - model_type_str = str(cfg_get(cfg, 'model_type', cfg_get(config, 'model_type', '')) or '').lower().strip() - decoding_cfg = cfg_get(cfg, 'decoding', cfg_get(config, 'decoding', None)) - decoding_model_type = str(cfg_get(decoding_cfg, 'model_type', '')).lower() - loss_cfg = cfg_get(cfg, 'loss', cfg_get(config, 'loss', None)) - loss_name = str(cfg_get(loss_cfg, 'loss_name', '')).lower() - - # NeMo Parakeet-TDT configs often do not expose HF-style model_type names. - if decoding_model_type == 'tdt' or loss_name == 'tdt': - return 'parakeet_tdt' - - if 'gemma4' in model_type_str or 'tinyllama' in model_type_str: - return 'gemma4' - elif 'gemma3n' in model_type_str: - return 'gemma3n' - elif 'gemma' in model_type_str: - return 'gemma' - elif 'lfm2' in model_type_str: - return 'lfm2' - elif model_type_str.startswith('qwen3_5'): - return 'qwen3_5' - elif 'qwen' in model_type_str: - return 'qwen' - elif 'moonshine' in model_type_str: - return 'moonshine' - elif 'llama' in model_type_str: - if output_dir and 'smol' in str(output_dir): - return 'smol' - else: - return 'llama' - elif 'youtu' in model_type_str: - return 'youtu' - elif 'bert' in model_type_str: - return 'bert' - elif 'whisper' in model_type_str: - return 'whisper' - elif 'parakeet' in model_type_str: - return 'parakeet' - else: - if model_type_str: - print(f" Warning: Unknown model type '{model_type_str}', defaulting to 'qwen'") - return 'qwen' - - -def resolve_audio_fft_length(audio_cfg): - sampling_rate = int(cfg_get(audio_cfg, 'sampling_rate', 16000)) - frame_length_ms = float(cfg_get(audio_cfg, 'frame_length_ms', 20.0)) - fft_overdrive = bool(cfg_get(audio_cfg, 'fft_overdrive', False)) - frame_length = int(round(sampling_rate * frame_length_ms / 1000.0)) - fft_length = 2 ** math.ceil(math.log2(max(1, frame_length))) - if fft_overdrive: - fft_length *= 2 - return fft_length - - -def extract_base_config(cfg, config): - """Extract base model configuration parameters.""" - rope_parameters = cfg_get(cfg, 'rope_parameters', {}) - rope_theta = cfg_get(cfg, 'rope_theta', None) - partial_rotary_factor = cfg_get(cfg, 'partial_rotary_factor', None) - if rope_theta is None and isinstance(rope_parameters, dict): - rope_theta = rope_parameters.get('rope_theta', None) - if partial_rotary_factor is None and isinstance(rope_parameters, dict): - partial_rotary_factor = rope_parameters.get('partial_rotary_factor', None) - if rope_theta is None: - rope_theta = cfg_get(config, 'rope_theta', 10000.0) - - num_experts_per_tok = cfg_get(cfg, 'num_experts_per_tok', cfg_get(cfg, 'moe_top_k', cfg_get(cfg, 'num_top_experts', cfg_get(cfg, 'top_k_experts', 0)))) or 0 - - base = { - 'vocab_size': cfg_get(cfg, 'vocab_size', cfg_get(config, 'vocab_size', 0)), - 'hidden_dim': cfg_get(cfg, 'hidden_size', cfg_get(cfg, 'hidden_dim', 0)), - 'num_layers': int(cfg_get(cfg, 'num_hidden_layers', cfg_get(cfg, 'num_layers', 0) or 0)), - 'attention_heads': cfg_get(cfg, 'num_attention_heads', 0), - 'attention_kv_heads': cfg_get(cfg, 'num_key_value_heads', cfg_get(cfg, 'num_attention_heads', 0)), - 'ffn_intermediate_dim': cfg_get(cfg, 'intermediate_size', cfg_get(cfg, 'n_inner', 0)), - 'context_length': cfg_get(cfg, 'max_position_embeddings', cfg_get(cfg, 'max_sequence_length', 0)), - 'rope_theta': rope_theta, - 'attention_head_dim': int(cfg_get(cfg, 'head_dim', int(cfg_get(cfg, 'hidden_size', cfg_get(cfg, 'hidden_dim', 0)) // max(1, cfg_get(cfg, 'num_attention_heads', 1))))), - 'layer_norm_eps': cfg_get(cfg, 'layer_norm_eps', cfg_get(cfg, 'layer_norm_epsilon', cfg_get(cfg, 'rms_norm_eps', cfg_get(cfg, 'norm_eps', 1e-6)))), - 'num_experts': cfg_get(cfg, 'num_experts', 0) or 0, - 'num_shared_experts': cfg_get(cfg, 'num_shared_experts', 0), - 'num_top_experts': num_experts_per_tok, - 'num_experts_per_tok': num_experts_per_tok, - 'moe_every_n_layers': cfg_get(cfg, 'moe_every_n_layers', 0), - } - if partial_rotary_factor is not None: - base['partial_rotary_factor'] = float(partial_rotary_factor) - - layer_types = cfg_get(cfg, 'layer_types', None) - if isinstance(layer_types, (list, tuple)) and layer_types: - base['layer_types'] = list(layer_types) - - conv_l_cache = cfg_get(cfg, 'conv_L_cache', None) - if conv_l_cache is not None: - base['conv_L_cache'] = int(conv_l_cache) - - linear_num_key_heads = cfg_get(cfg, 'linear_num_key_heads', None) - linear_key_head_dim = cfg_get(cfg, 'linear_key_head_dim', None) - linear_num_value_heads = cfg_get(cfg, 'linear_num_value_heads', None) - linear_value_head_dim = cfg_get(cfg, 'linear_value_head_dim', None) - if linear_num_key_heads is not None: - base['linear_num_key_heads'] = int(linear_num_key_heads) - if linear_key_head_dim is not None: - base['linear_key_head_dim'] = int(linear_key_head_dim) - if linear_num_value_heads is not None: - base['linear_num_value_heads'] = int(linear_num_value_heads) - if linear_value_head_dim is not None: - base['linear_value_head_dim'] = int(linear_value_head_dim) - if linear_num_key_heads is not None and linear_key_head_dim is not None: - base['linear_q_proj_dim'] = int(linear_num_key_heads) * int(linear_key_head_dim) - base['linear_k_proj_dim'] = int(linear_num_key_heads) * int(linear_key_head_dim) - if linear_num_value_heads is not None and linear_value_head_dim is not None: - base['linear_v_proj_dim'] = int(linear_num_value_heads) * int(linear_value_head_dim) - - return base - - -def extract_vision_config(config, vision_cfg): - """Extract vision encoder configuration for VLM models.""" - vision_hidden = int(cfg_get(vision_cfg, 'hidden_size', 0)) - vision_image_size = cfg_get(vision_cfg, 'image_size', cfg_get(vision_cfg, 'size', {}).get('longest_edge', 0) if isinstance(cfg_get(vision_cfg, 'size', {}), dict) else cfg_get(vision_cfg, 'image_size', 0)) - vision_patch = int(cfg_get(vision_cfg, 'patch_size', 0)) - vision_heads = int(cfg_get(vision_cfg, 'num_attention_heads', 0)) - vision_num_layers = int(cfg_get(vision_cfg, 'num_hidden_layers', cfg_get(vision_cfg, 'num_layers', 0) or 0)) - num_channels = int(cfg_get(vision_cfg, 'num_channels', 3)) - visual_tokens_per_img = 0 - try: - if vision_patch > 0 and vision_image_size > 0: - per_side = vision_image_size // vision_patch - visual_tokens_per_img = per_side * per_side - except Exception: - visual_tokens_per_img = 0 - - pixel_shuffle_factor = int(cfg_get(config, 'scale_factor', cfg_get(vision_cfg, 'scale_factor', 1) or 1)) - downsample_factor = int(cfg_get(config, 'downsample_factor', 2)) - - vision_head_dim = int(cfg_get(vision_cfg, 'head_dim', 64)) - vision_kv_heads = int(cfg_get(vision_cfg, 'num_key_value_heads', vision_heads)) - vision_intermediate_size = int(cfg_get(vision_cfg, 'intermediate_size', 4 * vision_hidden)) - vision_position_embedding_size = int(cfg_get(vision_cfg, 'position_embedding_size', 10240)) - vision_pooling_kernel_size = int(cfg_get(vision_cfg, 'pooling_kernel_size', 3)) - vision_default_output_length = cfg_get(vision_cfg, 'default_output_length', 280) - if isinstance(vision_default_output_length, (list, tuple)): - vision_default_output_length = vision_default_output_length[0] - vision_default_output_length = int(vision_default_output_length) - - vision_rope_params = cfg_get(vision_cfg, 'rope_parameters', {}) - vision_rope_theta = 100.0 - if isinstance(vision_rope_params, dict): - for v in vision_rope_params.values(): - if isinstance(v, dict) and 'rope_theta' in v: - vision_rope_theta = float(v['rope_theta']) - break - - return { - 'vision_hidden_size': int(vision_hidden), - 'vision_num_layers': int(vision_num_layers), - 'vision_image_size': int(vision_image_size), - 'vision_patch_size': int(vision_patch), - 'vision_attention_heads': int(vision_heads), - 'vision_embed_dim': int(vision_hidden), - 'num_channels': int(num_channels), - 'visual_tokens_per_img': int(visual_tokens_per_img), - 'use_pixel_shuffle': bool(pixel_shuffle_factor > 1), - 'pixel_shuffle_factor': int(pixel_shuffle_factor), - 'use_image_tokens': bool(cfg_get(config, 'image_token_id', None) is not None), - 'image_token_id': int(cfg_get(config, 'image_token_id', 0)), - 'use_layout_tags': False, - 'downsample_factor': int(downsample_factor), - 'vision_head_dim': vision_head_dim, - 'vision_kv_heads': vision_kv_heads, - 'vision_intermediate_size': vision_intermediate_size, - 'vision_position_embedding_size': vision_position_embedding_size, - 'vision_pooling_kernel_size': vision_pooling_kernel_size, - 'vision_default_output_length': vision_default_output_length, - 'vision_rope_theta': vision_rope_theta, - } - - -def extract_lfm2_config(cfg): - """Extract LFM2-specific configuration parameters.""" - layer_types = cfg_get(cfg, 'layer_types', []) - conv_L_cache = cfg_get(cfg, 'conv_L_cache', 0) - moe_intermediate_size = cfg_get(cfg, 'moe_intermediate_size', 0) - num_dense_layers = cfg_get(cfg, 'num_dense_layers', 0) - num_experts_per_tok = cfg_get(cfg, 'num_experts_per_tok', 0) - norm_topk_prob = bool(cfg_get(cfg, 'norm_topk_prob', False)) - use_expert_bias = bool(cfg_get(cfg, 'use_expert_bias', False)) - routed_scaling_factor = float(cfg_get(cfg, 'routed_scaling_factor', 1.0)) - return { - 'layer_types': layer_types, - 'conv_L_cache': conv_L_cache, - 'moe_intermediate_size': int(moe_intermediate_size), - 'num_dense_layers': int(num_dense_layers), - 'num_experts_per_tok': int(num_experts_per_tok), - 'norm_topk_prob': norm_topk_prob, - 'use_expert_bias': use_expert_bias, - 'routed_scaling_factor': routed_scaling_factor, - } - - -def extract_youtu_config(cfg): - rope_scaling = cfg_get(cfg, 'rope_scaling', {}) or {} - return { - 'kv_lora_rank': int(cfg_get(cfg, 'kv_lora_rank', 0)), - 'q_lora_rank': int(cfg_get(cfg, 'q_lora_rank', 0)), - 'qk_head_dim': int(cfg_get(cfg, 'qk_head_dim', 0)), - 'qk_nope_head_dim': int(cfg_get(cfg, 'qk_nope_head_dim', 0)), - 'qk_rope_head_dim': int(cfg_get(cfg, 'qk_rope_head_dim', 0)), - 'v_head_dim': int(cfg_get(cfg, 'v_head_dim', 0)), - 'rope_interleave': bool(cfg_get(cfg, 'rope_interleave', False)), - 'attention_bias': bool(cfg_get(cfg, 'attention_bias', False)), - 'rope_scaling_factor': float(cfg_get(rope_scaling, 'factor', 1.0)), - 'rope_mscale_all_dim': float(cfg_get(rope_scaling, 'mscale_all_dim', 0.0)), - } - - -def extract_moonshine_config(cfg): - """Extract Moonshine-specific configuration parameters.""" - rot_factor = getattr(cfg, "partial_rotary_factor", 0.9) - return { - 'partial_rotary_factor': rot_factor, - } - - -def extract_complex_gemma_config(cfg, root_config): - """Extract configuration parameters for Gemma3n and Gemma4 models.""" - altup_num_inputs = int(cfg_get(cfg, 'altup_num_inputs', cfg_get(root_config, 'altup_num_inputs', 4))) - laurel_rank = int(cfg_get(cfg, 'laurel_rank', cfg_get(root_config, 'laurel_rank', 64))) - hidden_size_per_layer_input_raw = cfg_get(cfg, 'hidden_size_per_layer_input', - cfg_get(root_config, 'hidden_size_per_layer_input', None)) - hidden_size_per_layer_input = int(hidden_size_per_layer_input_raw) if hidden_size_per_layer_input_raw is not None else 0 - rope_local_base_freq = float(cfg_get(cfg, 'rope_local_base_freq', - cfg_get(root_config, 'rope_local_base_freq', 10000.0))) - - num_kv_shared_layers = int(cfg_get(cfg, 'num_kv_shared_layers', - cfg_get(root_config, 'num_kv_shared_layers', 0))) - sliding_window = int(cfg_get(cfg, 'sliding_window', - cfg_get(root_config, 'sliding_window', 512))) - - rope_theta = cfg_get(root_config, 'rope_theta', None) - if rope_theta is None: - rope_theta = cfg_get(cfg, 'rope_theta', 1000000.0) - - attention_types = cfg_get(cfg, 'attention_type_pattern', cfg_get(root_config, 'attention_type_pattern', None)) - if attention_types is None: - attention_types = cfg_get(cfg, 'attention_types', cfg_get(root_config, 'attention_types', None)) - if attention_types is None: - attention_types = cfg_get(cfg, 'layer_types', cfg_get(root_config, 'layer_types', None)) - layer_types = [] - if attention_types: - num_layers = int(cfg_get(cfg, 'num_hidden_layers', cfg_get(cfg, 'num_layers', 30))) - if isinstance(attention_types, (list, tuple)): - pattern = list(attention_types) - else: - pattern = [str(attention_types)] - pattern_len = len(pattern) - for i in range(num_layers): - at = str(pattern[i % pattern_len]).lower() - if 'global' in at or 'full' in at: - layer_types.append('global') - else: - layer_types.append('sliding') - - final_logit_softcapping = float(cfg_get(cfg, 'final_logit_softcapping', - cfg_get(root_config, 'final_logit_softcapping', 30.0))) - - query_pre_attn_scalar = int(cfg_get(cfg, 'query_pre_attn_scalar', - cfg_get(root_config, 'query_pre_attn_scalar', 0))) - - rope_params = cfg_get(cfg, 'rope_parameters', cfg_get(root_config, 'rope_parameters', {})) - global_rope = rope_params.get('full_attention', {}) if isinstance(rope_params, dict) else {} - global_partial_rotary_factor = float(cfg_get(cfg, 'global_partial_rotary_factor', - global_rope.get('partial_rotary_factor', 1.0) if isinstance(global_rope, dict) else 1.0)) - - if rope_theta is None or rope_theta == 1000000.0: - global_rope_theta = global_rope.get('rope_theta', None) if isinstance(global_rope, dict) else None - if global_rope_theta is not None: - rope_theta = float(global_rope_theta) - - sliding_rope = rope_params.get('sliding_attention', {}) if isinstance(rope_params, dict) else {} - if isinstance(sliding_rope, dict) and 'rope_theta' in sliding_rope: - rope_local_base_freq = float(sliding_rope['rope_theta']) - - activation_sparsity = cfg_get(cfg, 'activation_sparsity_pattern', - cfg_get(root_config, 'activation_sparsity_pattern', None)) - - activation_sparsity_ppf = None - if activation_sparsity: - import torch, math - activation_sparsity_ppf = [] - for s in activation_sparsity: - if s > 0: - activation_sparsity_ppf.append( - round(math.sqrt(2) * torch.erfinv(torch.tensor(2.0 * s - 1.0)).item(), 7)) - else: - activation_sparsity_ppf.append(0.0) - - query_pre_attn_scalar = int(cfg_get(cfg, 'query_pre_attn_scalar', - cfg_get(root_config, 'query_pre_attn_scalar', 0))) - - rope_params = cfg_get(cfg, 'rope_parameters', cfg_get(root_config, 'rope_parameters', {})) - global_rope = rope_params.get('full_attention', {}) if isinstance(rope_params, dict) else {} - global_partial_rotary_factor = float(cfg_get(cfg, 'global_partial_rotary_factor', - global_rope.get('partial_rotary_factor', 1.0) if isinstance(global_rope, dict) else 1.0)) - - if rope_theta is None or rope_theta == 1000000.0: - global_rope_theta = global_rope.get('rope_theta', None) if isinstance(global_rope, dict) else None - if global_rope_theta is not None: - rope_theta = float(global_rope_theta) - - sliding_rope = rope_params.get('sliding_attention', {}) if isinstance(rope_params, dict) else {} - if isinstance(sliding_rope, dict) and 'rope_theta' in sliding_rope: - rope_local_base_freq = float(sliding_rope['rope_theta']) - - global_head_dim = cfg_get(cfg, 'global_head_dim', cfg_get(root_config, 'global_head_dim', None)) - num_global_kv_heads = cfg_get(cfg, 'num_global_key_value_heads', - cfg_get(root_config, 'num_global_key_value_heads', None)) - expert_intermediate_size = cfg_get(cfg, 'expert_intermediate_size', - cfg_get(root_config, 'expert_intermediate_size', None)) - attention_k_eq_v = bool(cfg_get(cfg, 'attention_k_eq_v', - cfg_get(root_config, 'attention_k_eq_v', False))) - vocab_size_per_layer_input = cfg_get(cfg, 'vocab_size_per_layer_input', - cfg_get(root_config, 'vocab_size_per_layer_input', None)) - sliding_window_pattern = cfg_get(cfg, '_sliding_window_pattern', - cfg_get(root_config, '_sliding_window_pattern', None)) - enable_moe_block = bool(cfg_get(cfg, 'enable_moe_block', - cfg_get(root_config, 'enable_moe_block', False))) - - result = { - 'altup_num_inputs': altup_num_inputs, - 'laurel_rank': laurel_rank, - 'hidden_size_per_layer_input': hidden_size_per_layer_input, - 'num_kv_shared_layers': num_kv_shared_layers, - 'sliding_window': sliding_window, - 'rope_local_base_freq': rope_local_base_freq, - 'rope_theta': float(rope_theta), - 'final_logit_softcapping': final_logit_softcapping, - 'query_pre_attn_scalar': query_pre_attn_scalar, - 'global_partial_rotary_factor': global_partial_rotary_factor, - 'attention_k_eq_v': attention_k_eq_v, - 'enable_moe_block': enable_moe_block, - } - if global_head_dim is not None: - result['global_head_dim'] = int(global_head_dim) - if num_global_kv_heads is not None: - result['num_global_key_value_heads'] = int(num_global_kv_heads) - if expert_intermediate_size is not None: - result['expert_intermediate_size'] = int(expert_intermediate_size) - if vocab_size_per_layer_input is not None: - result['vocab_size_per_layer_input'] = int(vocab_size_per_layer_input) - if sliding_window_pattern is not None: - result['sliding_window_pattern'] = int(sliding_window_pattern) - if layer_types: - result['layer_types'] = layer_types - if activation_sparsity_ppf: - result['activation_sparsity_ppf'] = activation_sparsity_ppf - return result - -def extract_audio_config(config, audio_cfg): - """Extract audio encoder configuration for multimodal models.""" - hidden = int(cfg_get(audio_cfg, 'hidden_size', 1024)) - num_heads = int(cfg_get(audio_cfg, 'conf_num_attention_heads', 8)) - sscp_channels = cfg_get(audio_cfg, 'sscp_conv_channel_size', [128, 32]) - if not isinstance(sscp_channels, (list, tuple)): - sscp_channels = [128, 32] - output_proj = cfg_get(audio_cfg, 'output_proj_dims', None) - if output_proj is None: - output_proj = 0 - return { - 'audio_hidden_dim': hidden, - 'audio_num_layers': int(cfg_get(audio_cfg, 'conf_num_hidden_layers', 12)), - 'audio_num_heads': num_heads, - 'audio_head_dim': hidden // max(1, num_heads), - 'audio_input_feat_size': int(cfg_get(audio_cfg, 'input_feat_size', 128)), - 'audio_conf_conv_kernel_size': int(cfg_get(audio_cfg, 'conf_conv_kernel_size', 5)), - 'audio_chunk_size': int(cfg_get(audio_cfg, 'conf_attention_chunk_size', 12)), - 'audio_context_left': int(cfg_get(audio_cfg, 'conf_attention_context_left', 13)), - 'audio_context_right': int(cfg_get(audio_cfg, 'conf_attention_context_right', 0)), - 'audio_logit_cap': float(cfg_get(audio_cfg, 'conf_attention_logit_cap', 50.0)), - 'audio_residual_weight': float(cfg_get(audio_cfg, 'conf_residual_weight', 0.5)), - 'audio_output_proj_dims': int(output_proj), - 'audio_vocab_size': int(cfg_get(audio_cfg, 'vocab_size', 128)), - 'audio_vocab_offset': int(cfg_get(audio_cfg, 'vocab_offset', 0)), - 'audio_soft_tokens': int(cfg_get(audio_cfg, 'audio_soft_tokens_per_image', 188)), - 'audio_sscp_conv0_channels': int(sscp_channels[0]), - 'audio_sscp_conv1_channels': int(sscp_channels[1]), - 'audio_sscp_conv_eps': float(cfg_get(audio_cfg, 'sscp_conv_eps', 1e-3)), - 'audio_rms_norm_eps': float(cfg_get(audio_cfg, 'rms_norm_eps', 1e-6)), - 'audio_fft_length': resolve_audio_fft_length(audio_cfg), - 'audio_token_id': int(cfg_get(config, 'audio_token_id', 0)), - } - - -def is_vlm_model(config): - """Check if a model config indicates a vision-language model.""" - text_cfg = cfg_get(config, 'text_config', None) - vision_cfg = cfg_get(config, 'vision_config', None) - return text_cfg is not None or vision_cfg is not None - - -def is_lfm2_vl(model_name, cfg): - """Check if the model is an LFM2 vision-language model.""" - model_type = str(cfg_get(cfg, 'model_type', '') or '').lower().strip() - if model_type.replace('_', '-') == "lfm2-vl": - return True - architectures = cfg_get(cfg, 'architectures', []) - if isinstance(architectures, (list, tuple)): - for architecture in architectures: - normalized_arch = str(architecture or '').lower().replace('_', '').replace('-', '') - if normalized_arch == 'lfm2vlforconditionalgeneration': - return True - name = (model_name or "").lower() - return ( - "lfm2-vl" in name - or "lfm2_vl" in name - or "lfm2.5-vl" in name - or "lfm2.5_vl" in name - ) - - -def pick_dtype(): - """Select torch dtype for model loading — bf16 preferred, float16 fallback.""" - import torch - try: - torch.zeros(1, dtype=torch.bfloat16) - return torch.bfloat16 - except Exception: - return torch.float16 - - -def vision_weight_sanity_check(model): - """Verify vision tower weights are properly initialized.""" - ok = True - vt = getattr(model, "vision_tower", None) - try: - emb = vt.vision_model.embeddings - w_mean = emb.patch_embedding.weight.detach().abs().mean().item() - p_mean = emb.position_embedding.weight.detach().abs().mean().item() - print(f"[sanity] |patch W| mean={w_mean:.5f} |pos W| mean={p_mean:.5f}") - if w_mean < 1e-3 or p_mean < 1e-3: - ok = False - except Exception: - pass - return ok diff --git a/python/src/converter.py b/python/src/converter.py deleted file mode 100644 index b94a5bb1d..000000000 --- a/python/src/converter.py +++ /dev/null @@ -1,1429 +0,0 @@ -import os -import re -from pathlib import Path - -import numpy as np -try: - import torch -except ImportError: - torch = None - -from .tensor_io import save_tensor_with_header, create_quantization_stats, print_quantization_summary, fold_bn_into_conv -from .config_utils import cfg_get, detect_model_type, extract_base_config, extract_vision_config, extract_lfm2_config, is_vlm_model, extract_moonshine_config, extract_complex_gemma_config, extract_audio_config, extract_youtu_config -from .weight_patterns import ( - EMBED_NAMES, OUTPUT_NAMES, OUTPUT_NORM_NAMES, LAYER_PREFIXES, - VISION_ITEMS, PROJECTOR_WEIGHTS, WHISPER_GLOBAL_WEIGHTS, MOONSHINE_GLOBAL_WEIGHTS, - GEMMA3N_GLOBAL_WEIGHTS, GEMMA3N_VISION_TOWER_PREFIX, GEMMA3N_AUDIO_TOWER_PREFIX, - GEMMA4_GLOBAL_WEIGHTS, GEMMA4_VISION_TOWER_PREFIX, GEMMA4_AUDIO_TOWER_PREFIX, - NEEDLE_GLOBAL_WEIGHTS, NEEDLE_ENCODER_LAYER_WEIGHTS, NEEDLE_DECODER_LAYER_WEIGHTS, - get_layer_weight_patterns, get_vision_layer_weights -) - - -def _remap_gemma4_audio_keys(state_dict): - """Remap Gemma-4 audio tower keys from checkpoint naming to HF model naming. - - The gg-hf-gg/gemma-4-e2b-it checkpoint uses an older naming convention - for audio encoder weights that doesn't match the HF Gemma4 model class. - When HF loads the model, these weights end up as randomly initialized. - This function remaps them so the converter gets the real trained weights. - """ - remapped = {} - for key, value in state_dict.items(): - if 'audio_tower' not in key: - remapped[key] = value - continue - new_key = key - new_key = re.sub(r'subsample_conv_projection\.layer(\d+)\.', r'subsample_conv_projection.conv_\1.', new_key) - new_key = re.sub(r'audio_tower\.layers\.', 'audio_tower.conformer.', new_key) - new_key = new_key.replace('.feed_forward1.', '.ffw_layer_start.') - new_key = new_key.replace('.feed_forward2.', '.ffw_layer_end.') - new_key = re.sub(r'\.self_attn\.(q_proj|k_proj|v_proj)\.', r'.attention.attn.\1.', new_key) - new_key = new_key.replace('.self_attn.per_dim_scale', '.attention.attn.per_dim_scale') - new_key = new_key.replace('.self_attn.relative_k_proj.', '.attention.attn.relative_position_embedding.pos_proj.') - new_key = new_key.replace('.self_attn.post.', '.attention.post.') - new_key = new_key.replace('.norm_pre_attn.', '.attention.pre_attn_norm.') - new_key = new_key.replace('.norm_post_attn.', '.attention.post_norm.') - new_key = re.sub(r'\.norm_out\.', '.norm.', new_key) - remapped[new_key] = value - return remapped - - -def _find_first_key(state_dict, candidates): - for key in candidates: - if key in state_dict: - return key - return None - - -def _gemma_tower_output_name(hf_key, strip_prefix, add_prefix): - name = hf_key[len(strip_prefix):] - if name.endswith('.weight'): - name = name[:-len('.weight')] - ext = '.weights' - elif name.endswith('.bias'): - name = name[:-len('.bias')] - ext = '.bias' - else: - ext = '.weights' - if name.endswith('.linear'): - name = name[:-len('.linear')] - elif name.endswith('_linear'): - name = name[:-len('_linear')] - name = name.replace('.', '_') - return add_prefix + name + ext - - -def convert_hf_model_weights(model, output_dir, precision='INT8', args=None): - """Convert HuggingFace model weights to Cactus binary format.""" - import gc - quantization_stats = create_quantization_stats() - - state_dict = model.state_dict() - root_config = model.config - model_name = getattr(model, 'name_or_path', '') or '' - del model - gc.collect() - - # Fix Gemma-4 audio tower weights: the checkpoint may use old key names - # that HF can't map, leaving audio weights randomly initialized. - # Detect this by checking if clip bounds are inf (default init value). - audio_needs_fix = False - for k, v in state_dict.items(): - if 'audio_tower' in k and 'input_max' in k: - if torch.isinf(v).any(): - audio_needs_fix = True - break - if audio_needs_fix and model_name: - try: - from huggingface_hub import hf_hub_download - from safetensors.torch import load_file - sf_path = hf_hub_download(repo_id=model_name, filename='model.safetensors') - raw_sd = load_file(sf_path) - remapped = _remap_gemma4_audio_keys(raw_sd) - # Replace audio tower keys in state_dict with correctly loaded ones - audio_prefix = 'model.audio_tower.' - for k in list(state_dict.keys()): - if k.startswith(audio_prefix): - del state_dict[k] - for k, v in remapped.items(): - if k.startswith(audio_prefix): - state_dict[k] = v - print(" Fixed audio tower weights from checkpoint (key remapping applied)") - except Exception as e: - print(f" Warning: Could not fix audio tower weights: {e}") - - saved_tensor_full_names = set() - - text_config = cfg_get(root_config, 'text_config', None) - vision_config = cfg_get(root_config, 'vision_config', None) - is_vlm = text_config is not None or vision_config is not None - - config = text_config if text_config is not None else root_config - - model_type_str = str(cfg_get(config, 'model_type', cfg_get(root_config, 'model_type', '')) or '').lower().strip() - tie_word_embeddings = cfg_get(config, 'tie_word_embeddings', cfg_get(root_config, 'tie_word_embeddings', None)) - if tie_word_embeddings is None: - # HF snapshots for lfm2_moe/gemma3n may omit this field; runtime expects tied embeddings by default. - tie_word_embeddings = (model_type_str == 'lfm2_moe' or 'gemma3n' in model_type_str) - else: - tie_word_embeddings = bool(tie_word_embeddings) - - detected_model_type = detect_model_type(config, root_config, output_dir) - if detected_model_type == 'gemma4': - # Normalize Gemma-4 audio tower naming variants (legacy -> runtime expected) - # before exporting filenames so generated weights match the C++ loader. - remapped_state_dict = _remap_gemma4_audio_keys(state_dict) - if set(remapped_state_dict.keys()) != set(state_dict.keys()): - print(" Normalized gemma4 audio tower key naming for conversion") - state_dict = remapped_state_dict - - model_config = extract_base_config(config, root_config) - model_config['tie_word_embeddings'] = tie_word_embeddings - model_config['model_type'] = detected_model_type - - if is_vlm and vision_config is not None: - model_config.update(extract_vision_config(root_config, vision_config)) - - if detected_model_type == 'gemma3n': - model_config.update(extract_complex_gemma_config(config, root_config)) - elif detected_model_type == 'gemma4': - model_config.update(extract_complex_gemma_config(config, root_config)) - audio_cfg = cfg_get(root_config, 'audio_config', cfg_get(config, 'audio_config', None)) - if audio_cfg is not None: - model_config.update(extract_audio_config(root_config, audio_cfg)) - # New models don't use weight pre-scaling; HF inference uses raw weights. - if audio_cfg is not None and not bool(cfg_get(audio_cfg, 'fft_overdrive', False)): - if args is None: - class _Args: - pass - args = _Args() - args.weight_scale = 1.0 - elif detected_model_type == 'lfm2': - model_config.update(extract_lfm2_config(config)) - elif detected_model_type == 'youtu': - model_config.update(extract_youtu_config(config)) - elif detected_model_type == 'moonshine': - model_config.update(extract_moonshine_config(config)) - elif detected_model_type == 'parakeet': - encoder_cfg = cfg_get(config, 'encoder_config', None) - if encoder_cfg is None: - raise ValueError("Parakeet conversion requires encoder_config in model config") - - hidden_dim = int(cfg_get(encoder_cfg, 'hidden_size', 0)) - num_layers = int(cfg_get(encoder_cfg, 'num_hidden_layers', 0)) - attention_heads = int(cfg_get(encoder_cfg, 'num_attention_heads', 0)) - attention_kv_heads = int(cfg_get(encoder_cfg, 'num_key_value_heads', attention_heads)) - head_dim = int(hidden_dim // max(1, attention_heads)) - layer_norm_eps = cfg_get(encoder_cfg, 'layer_norm_eps', cfg_get(encoder_cfg, 'norm_eps', 1e-5)) - if layer_norm_eps is None: - layer_norm_eps = 1e-5 - rope_theta = cfg_get(encoder_cfg, 'rope_theta', 0.0) - if rope_theta is None: - rope_theta = 0.0 - - model_config.update({ - 'vocab_size': int(cfg_get(config, 'vocab_size', cfg_get(root_config, 'vocab_size', 0))), - 'hidden_dim': hidden_dim, - 'num_layers': num_layers, - 'attention_heads': attention_heads, - 'attention_kv_heads': attention_kv_heads, - 'attention_head_dim': head_dim, - 'ffn_intermediate_dim': int(cfg_get(encoder_cfg, 'intermediate_size', 0)), - 'context_length': int(cfg_get(encoder_cfg, 'max_position_embeddings', 0)), - 'layer_norm_eps': float(layer_norm_eps), - 'rope_theta': float(rope_theta), - 'conv_kernel_size': int(cfg_get(encoder_cfg, 'conv_kernel_size', 9)), - 'subsampling_conv_kernel_size': int(cfg_get(encoder_cfg, 'subsampling_conv_kernel_size', 3)), - 'subsampling_conv_stride': int(cfg_get(encoder_cfg, 'subsampling_conv_stride', 2)), - 'subsampling_conv_channels': int(cfg_get(encoder_cfg, 'subsampling_conv_channels', 256)), - 'subsampling_factor': int(cfg_get(encoder_cfg, 'subsampling_factor', 8)), - 'num_mel_bins': int(cfg_get(encoder_cfg, 'num_mel_bins', 80)), - 'pad_token_id': int(cfg_get(config, 'pad_token_id', cfg_get(root_config, 'pad_token_id', 0))), - 'encoder_hidden_act': cfg_get(encoder_cfg, 'hidden_act', 'silu'), - }) - elif detected_model_type == 'parakeet_tdt': - encoder_cfg = cfg_get(config, 'encoder', cfg_get(root_config, 'encoder', None)) - if encoder_cfg is None: - raise ValueError("Parakeet TDT conversion requires encoder config") - - preprocessor_cfg = cfg_get(config, 'preprocessor', cfg_get(root_config, 'preprocessor', {})) - decoder_cfg = cfg_get(config, 'decoder', cfg_get(root_config, 'decoder', {})) - prediction_cfg = cfg_get(decoder_cfg, 'prediction', {}) - joint_cfg = cfg_get(config, 'joint', cfg_get(root_config, 'joint', {})) - jointnet_cfg = cfg_get(joint_cfg, 'jointnet', {}) - model_defaults_cfg = cfg_get(config, 'model_defaults', cfg_get(root_config, 'model_defaults', {})) - - hidden_dim = int(cfg_get(encoder_cfg, 'd_model', cfg_get(encoder_cfg, 'hidden_size', 0))) - num_layers = int(cfg_get(encoder_cfg, 'n_layers', cfg_get(encoder_cfg, 'num_hidden_layers', 0))) - attention_heads = int(cfg_get(encoder_cfg, 'n_heads', cfg_get(encoder_cfg, 'num_attention_heads', 0))) - attention_kv_heads = int(cfg_get(encoder_cfg, 'n_kv_heads', attention_heads)) - head_dim = int(hidden_dim // max(1, attention_heads)) - - ff_intermediate = int(cfg_get(encoder_cfg, 'ffn_hidden_size', 0)) - if ff_intermediate == 0: - ff_expansion = float(cfg_get(encoder_cfg, 'ff_expansion_factor', 4.0)) - ff_intermediate = int(round(hidden_dim * ff_expansion)) - - layer_norm_eps = float(cfg_get(encoder_cfg, 'layer_norm_eps', cfg_get(encoder_cfg, 'norm_eps', 1e-5)) or 1e-5) - rope_theta = float(cfg_get(encoder_cfg, 'rope_theta', 0.0) or 0.0) - labels = cfg_get(config, 'labels', cfg_get(root_config, 'labels', [])) - if not isinstance(labels, (list, tuple)): - labels = [] - tdt_durations = cfg_get(model_defaults_cfg, 'tdt_durations', []) - if not isinstance(tdt_durations, (list, tuple)): - tdt_durations = [] - - vocab_size = int(cfg_get(decoder_cfg, 'vocab_size', len(labels))) - blank_id_cfg = cfg_get(cfg_get(config, 'decoding', {}), 'blank_id', cfg_get(decoder_cfg, 'blank_id', None)) - if blank_id_cfg is None: - tdt_blank_id = vocab_size - else: - try: - tdt_blank_id = int(blank_id_cfg) - except (TypeError, ValueError): - tdt_blank_id = vocab_size - if tdt_blank_id < 0: - tdt_blank_id = vocab_size - - model_config.update({ - 'vocab_size': vocab_size, - 'hidden_dim': hidden_dim, - 'num_layers': num_layers, - 'attention_heads': attention_heads, - 'attention_kv_heads': attention_kv_heads, - 'attention_head_dim': head_dim, - 'ffn_intermediate_dim': ff_intermediate, - 'context_length': int(cfg_get(encoder_cfg, 'max_position_embeddings', 0)), - 'layer_norm_eps': layer_norm_eps, - 'rope_theta': rope_theta, - 'conv_kernel_size': int(cfg_get(encoder_cfg, 'conv_kernel_size', 9)), - 'subsampling_conv_kernel_size': int(cfg_get(encoder_cfg, 'subsampling_conv_kernel_size', 3)), - 'subsampling_conv_stride': int(cfg_get(encoder_cfg, 'subsampling_conv_stride', 2)), - 'subsampling_conv_channels': int(cfg_get(encoder_cfg, 'subsampling_conv_channels', 256)), - 'subsampling_factor': int(cfg_get(encoder_cfg, 'subsampling_factor', 8)), - 'num_mel_bins': int(cfg_get(preprocessor_cfg, 'features', cfg_get(encoder_cfg, 'feat_in', 128))), - 'pad_token_id': int(cfg_get(config, 'pad_token_id', cfg_get(root_config, 'pad_token_id', 0))), - 'encoder_hidden_act': cfg_get(encoder_cfg, 'activation', cfg_get(encoder_cfg, 'hidden_act', 'silu')), - 'predictor_hidden_dim': int(cfg_get(prediction_cfg, 'pred_hidden', 0)), - 'predictor_num_layers': int(cfg_get(prediction_cfg, 'pred_rnn_layers', 0)), - 'tdt_joint_dim': int(cfg_get(jointnet_cfg, 'joint_hidden', 0)), - 'tdt_num_durations': len(tdt_durations), - 'tdt_durations': [int(v) for v in tdt_durations], - 'tdt_blank_id': tdt_blank_id, - }) - - num_layers = model_config['num_layers'] - - embedding_found = False - for name in EMBED_NAMES: - if name in state_dict: - embedding_tensor = state_dict[name] - save_tensor_with_header(embedding_tensor, output_dir / "token_embeddings.weights", precision, transpose=False, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - saved_tensor_full_names.add(name) - embedding_found = True - break - - if model_type_str == 'nomic_bert': - if 'embeddings.word_embeddings.weight' in state_dict: - fused_embedding_tensor = state_dict['embeddings.word_embeddings.weight'] + state_dict.get('embeddings.token_type_embeddings.weight', torch.zeros([1])) - save_tensor_with_header(fused_embedding_tensor, output_dir / "token_embeddings.weights", precision, transpose=False, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - saved_tensor_full_names.add('embeddings.word_embeddings.weight') - if 'embeddings.token_type_embeddings.weight' in state_dict: - saved_tensor_full_names.add('embeddings.token_type_embeddings.weight') - embedding_found = True - - elif model_type_str == 'whisper': - for name, save_name in WHISPER_GLOBAL_WEIGHTS: - if name in state_dict: - save_tensor_with_header(state_dict[name], output_dir / save_name, precision, transpose=False, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - saved_tensor_full_names.add(name) - embedding_found = True - model_config['num_mel_bins'] = int(cfg_get(config, 'num_mel_bins', 80)) - model_config['num_encoder_layers'] = int(cfg_get(config, 'encoder_layers', model_config['num_layers'])) - model_config['num_decoder_layers'] = int(cfg_get(config, 'decoder_layers', model_config['num_layers'])) - - elif model_type_str == 'moonshine': - for name, save_name in MOONSHINE_GLOBAL_WEIGHTS: - if name in state_dict: - tensor = state_dict[name] - if name == 'model.encoder.conv2.weight': - tensor = tensor.permute(1, 2, 0).contiguous() - - save_tensor_with_header(tensor, output_dir / save_name, precision, transpose=False, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - saved_tensor_full_names.add(name) - embedding_found = True - model_config['dec_hidden_act'] = config.decoder_hidden_act - model_config['enc_hidden_act'] = config.encoder_hidden_act - model_config['num_encoder_layers'] = config.encoder_num_hidden_layers - model_config['num_decoder_layers'] = config.decoder_num_hidden_layers - - # Whisper: ensure num_layers = max(enc, dec) so the weight loader covers both. - if model_type_str == 'whisper': - enc_l = int(model_config.get('num_encoder_layers', 0)) - dec_l = int(model_config.get('num_decoder_layers', 0)) - if enc_l > 0 or dec_l > 0: - num_layers = max(enc_l, dec_l, num_layers) - model_config['num_layers'] = num_layers - - if embedding_found: - embedding_norm_names = {'emb_ln.weight': 'embedding_layernorm.weight', 'emb_ln.bias': 'embedding_layernorm.bias'} - for name, file_name in embedding_norm_names.items(): - if name in state_dict: - save_tensor_with_header(state_dict[name], output_dir / file_name, precision, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - saved_tensor_full_names.add(name) - - if tie_word_embeddings: - if embedding_found: - for name in OUTPUT_NAMES: - if name in state_dict: - saved_tensor_full_names.add(name) - else: - for name in OUTPUT_NAMES: - if name in state_dict: - tensor = state_dict[name] - save_tensor_with_header(tensor, output_dir / "output_weight.weights", precision, transpose=False, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - saved_tensor_full_names.add(name) - break - - for name in OUTPUT_NORM_NAMES: - if name in state_dict: - tensor = state_dict[name] - save_tensor_with_header(tensor, output_dir / "output_norm.weights", precision, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - saved_tensor_full_names.add(name) - break - - mtp_global_mappings = [ - ('mtp.norm.weight', 'mtp_norm.weights'), - ('mtp.fc.weight', 'mtp_fc.weights'), - ('mtp.pre_fc_norm_embedding.weight', 'mtp_pre_fc_norm_embedding.weights'), - ('mtp.pre_fc_norm_hidden.weight', 'mtp_pre_fc_norm_hidden.weights'), - ] - for key, out_name in mtp_global_mappings: - if key in state_dict: - save_tensor_with_header( - state_dict[key], output_dir / out_name, precision, transpose=False, - stats_tracker=quantization_stats, args=args, model_type=detected_model_type - ) - saved_tensor_full_names.add(key) - - mtp_layer_mappings = [ - ('mtp.layers.0.input_layernorm.weight', 'mtp_layer_0_input_norm.weights'), - ('mtp.layers.0.self_attn.q_proj.weight', 'mtp_layer_0_attn_q.weights'), - ('mtp.layers.0.self_attn.k_proj.weight', 'mtp_layer_0_attn_k.weights'), - ('mtp.layers.0.self_attn.v_proj.weight', 'mtp_layer_0_attn_v.weights'), - ('mtp.layers.0.self_attn.o_proj.weight', 'mtp_layer_0_attn_output.weights'), - ('mtp.layers.0.self_attn.q_norm.weight', 'mtp_layer_0_attn_q_norm.weights'), - ('mtp.layers.0.self_attn.k_norm.weight', 'mtp_layer_0_attn_k_norm.weights'), - ('mtp.layers.0.mlp.gate_proj.weight', 'mtp_layer_0_ffn_gate.weights'), - ('mtp.layers.0.mlp.up_proj.weight', 'mtp_layer_0_ffn_up.weights'), - ('mtp.layers.0.mlp.down_proj.weight', 'mtp_layer_0_ffn_down.weights'), - ('mtp.layers.0.post_attention_layernorm.weight', 'mtp_layer_0_post_attn_norm.weights'), - ] - for key, out_name in mtp_layer_mappings: - if key in state_dict: - save_tensor_with_header( - state_dict[key], output_dir / out_name, precision, transpose=False, - stats_tracker=quantization_stats, args=args, model_type=detected_model_type - ) - saved_tensor_full_names.add(key) - - if is_vlm: - for key, outname in VISION_ITEMS: - if key in state_dict: - save_tensor_with_header(state_dict[key], output_dir / outname, precision, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - saved_tensor_full_names.add(key) - - for key, outname in PROJECTOR_WEIGHTS: - if key in state_dict: - save_tensor_with_header(state_dict[key], output_dir / outname, precision, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - saved_tensor_full_names.add(key) - - max_v_idx = -1 - vision_prefix = None - for k in state_dict.keys(): - m = re.search(r'model\.vision_tower\.vision_model\.encoder\.layers\.(\d+)\.', k) - if m: - vision_prefix = 'model.vision_tower.vision_model.encoder.layers.' - try: - idx = int(m.group(1)) - if idx > max_v_idx: - max_v_idx = idx - except Exception: - pass - if not vision_prefix: - m = re.search(r'model\.vision_model\.encoder\.layers\.(\d+)\.', k) - if m: - vision_prefix = 'model.vision_model.encoder.layers.' - try: - idx = int(m.group(1)) - if idx > max_v_idx: - max_v_idx = idx - except Exception: - pass - - if not vision_prefix: - vision_prefix = 'model.vision_model.encoder.layers.' - - vision_layers = max_v_idx + 1 if max_v_idx >= 0 else 0 - - for i_v in range(vision_layers): - vpref = f'{vision_prefix}{i_v}.' - vision_layer_weights = get_vision_layer_weights(i_v, vpref) - for fname, out in vision_layer_weights: - if fname in state_dict: - save_tensor_with_header(state_dict[fname], output_dir / out, precision, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - saved_tensor_full_names.add(fname) - - if detected_model_type == 'gemma3n': - pli_key = 'model.language_model.embed_tokens_per_layer.weight' - if pli_key in state_dict: - pli_tensor = state_dict[pli_key] - main_vocab = int(model_config.get('vocab_size', pli_tensor.shape[0])) - if pli_tensor.shape[0] < main_vocab: - pad_rows = main_vocab - pli_tensor.shape[0] - state_dict[pli_key] = torch.cat([pli_tensor, pli_tensor[0:1].expand(pad_rows, -1)], dim=0) - - for name, save_name in GEMMA3N_GLOBAL_WEIGHTS: - if name in state_dict: - save_tensor_with_header(state_dict[name], output_dir / save_name, precision, transpose=False, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - saved_tensor_full_names.add(name) - - for hf_key in sorted(state_dict.keys()): - if hf_key.startswith(GEMMA3N_VISION_TOWER_PREFIX) and hf_key not in saved_tensor_full_names: - out_name = _gemma_tower_output_name(hf_key, GEMMA3N_VISION_TOWER_PREFIX, 'vision_') - save_tensor_with_header(state_dict[hf_key], output_dir / out_name, precision, transpose=False, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - saved_tensor_full_names.add(hf_key) - - for hf_key in sorted(state_dict.keys()): - if hf_key.startswith(GEMMA3N_AUDIO_TOWER_PREFIX) and hf_key not in saved_tensor_full_names: - out_name = _gemma_tower_output_name(hf_key, GEMMA3N_AUDIO_TOWER_PREFIX, 'audio_') - save_tensor_with_header(state_dict[hf_key], output_dir / out_name, precision, transpose=False, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - saved_tensor_full_names.add(hf_key) - - if detected_model_type == 'gemma4': - pli_key = 'model.language_model.embed_tokens_per_layer.weight' - if pli_key in state_dict: - pli_tensor = state_dict[pli_key] - main_vocab = int(model_config.get('vocab_size', pli_tensor.shape[0])) - if pli_tensor.shape[0] < main_vocab: - pad_rows = main_vocab - pli_tensor.shape[0] - state_dict[pli_key] = torch.cat([pli_tensor, pli_tensor[0:1].expand(pad_rows, -1)], dim=0) - - for name, save_name in GEMMA4_GLOBAL_WEIGHTS: - if name in state_dict: - save_tensor_with_header(state_dict[name], output_dir / save_name, precision, transpose=False, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - saved_tensor_full_names.add(name) - - text_hidden = int(model_config.get('hidden_dim', 0)) - assert text_hidden > 0, "Hidden dim must be specified in config for gemma4 model" - proj_norm = np.ones(text_hidden, dtype=np.float32) - save_tensor_with_header(proj_norm, output_dir / 'embed_vision_post_proj_norm.weights', 'FP16', - stats_tracker=quantization_stats, model_type=detected_model_type) - - for hf_key in sorted(state_dict.keys()): - if hf_key.startswith(GEMMA4_VISION_TOWER_PREFIX) and hf_key not in saved_tensor_full_names: - out_name = _gemma_tower_output_name(hf_key, GEMMA4_VISION_TOWER_PREFIX, 'vision_') - save_tensor_with_header(state_dict[hf_key], output_dir / out_name, precision, transpose=False, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - saved_tensor_full_names.add(hf_key) - del state_dict[hf_key] - - for hf_key in sorted(state_dict.keys()): - if hf_key.startswith(GEMMA4_AUDIO_TOWER_PREFIX) and hf_key not in saved_tensor_full_names: - out_name = _gemma_tower_output_name(hf_key, GEMMA4_AUDIO_TOWER_PREFIX, 'audio_') - save_tensor_with_header(state_dict[hf_key], output_dir / out_name, precision, transpose=False, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - saved_tensor_full_names.add(hf_key) - del state_dict[hf_key] - - gc.collect() - - missing_tensors = [] - if detected_model_type == 'parakeet': - global_mappings = [ - (['encoder.subsampling.layers.0.weight'], 'subsampling_conv0_weight.weights'), - (['encoder.subsampling.layers.0.bias'], 'subsampling_conv0_bias.bias'), - (['encoder.subsampling.layers.2.weight'], 'subsampling_depthwise1_weight.weights'), - (['encoder.subsampling.layers.2.bias'], 'subsampling_depthwise1_bias.bias'), - (['encoder.subsampling.layers.3.weight'], 'subsampling_pointwise1_weight.weights'), - (['encoder.subsampling.layers.3.bias'], 'subsampling_pointwise1_bias.bias'), - (['encoder.subsampling.layers.5.weight'], 'subsampling_depthwise2_weight.weights'), - (['encoder.subsampling.layers.5.bias'], 'subsampling_depthwise2_bias.bias'), - (['encoder.subsampling.layers.6.weight'], 'subsampling_pointwise2_weight.weights'), - (['encoder.subsampling.layers.6.bias'], 'subsampling_pointwise2_bias.bias'), - (['encoder.subsampling.linear.weight'], 'subsampling_linear_weight.weights'), - (['encoder.subsampling.linear.bias'], 'subsampling_linear_bias.bias'), - (['ctc_head.weight'], 'ctc_head_weight.weights'), - (['ctc_head.bias'], 'ctc_head_bias.bias'), - ] - - for candidate_keys, out_name in global_mappings: - key = _find_first_key(state_dict, candidate_keys) - if key is None: - missing_tensors.append((-1, out_name, candidate_keys)) - continue - tensor = state_dict[key] - - # Some NeMo exports store conv2d kernels as [O, H, W, I]. - # Runtime expects [O, I, H, W]. - if out_name in { - 'subsampling_conv0_weight.weights', - 'subsampling_depthwise1_weight.weights', - 'subsampling_pointwise1_weight.weights', - 'subsampling_depthwise2_weight.weights', - 'subsampling_pointwise2_weight.weights', - } and hasattr(tensor, 'shape') and len(tensor.shape) == 4: - is_hwio_k3 = tensor.shape[1] == 3 and tensor.shape[2] == 3 and tensor.shape[3] >= 1 - is_hwio_pw = tensor.shape[1] == 1 and tensor.shape[2] == 1 and tensor.shape[3] > 1 - if is_hwio_k3 or is_hwio_pw: - tensor = tensor.permute(0, 3, 1, 2).contiguous() - save_tensor_with_header( - tensor, output_dir / out_name, precision, transpose=False, - stats_tracker=quantization_stats, args=args, model_type=detected_model_type - ) - saved_tensor_full_names.add(key) - - layer_mappings = [ - ('feed_forward1.linear1.weight', 'ff1_linear1.weights'), - ('feed_forward1.linear1.bias', 'ff1_linear1.bias'), - ('feed_forward1.linear2.weight', 'ff1_linear2.weights'), - ('feed_forward1.linear2.bias', 'ff1_linear2.bias'), - ('feed_forward2.linear1.weight', 'ff2_linear1.weights'), - ('feed_forward2.linear1.bias', 'ff2_linear1.bias'), - ('feed_forward2.linear2.weight', 'ff2_linear2.weights'), - ('feed_forward2.linear2.bias', 'ff2_linear2.bias'), - ('self_attn.q_proj.weight', 'self_attn_q.weights'), - ('self_attn.q_proj.bias', 'self_attn_q.bias'), - ('self_attn.k_proj.weight', 'self_attn_k.weights'), - ('self_attn.k_proj.bias', 'self_attn_k.bias'), - ('self_attn.v_proj.weight', 'self_attn_v.weights'), - ('self_attn.v_proj.bias', 'self_attn_v.bias'), - ('self_attn.o_proj.weight', 'self_attn_output.weights'), - ('self_attn.o_proj.bias', 'self_attn_output.bias'), - ('self_attn.relative_k_proj.weight', 'self_attn_relative_k.weights'), - ('self_attn.bias_u', 'self_attn_bias_u.weights'), - ('self_attn.bias_v', 'self_attn_bias_v.weights'), - ('conv.pointwise_conv1.weight', 'conv_pointwise1.weights'), - ('conv.pointwise_conv1.bias', 'conv_pointwise1.bias'), - ('conv.depthwise_conv.weight', 'conv_depthwise.weights'), - ('conv.depthwise_conv.bias', 'conv_depthwise.bias'), - ('conv.pointwise_conv2.weight', 'conv_pointwise2.weights'), - ('conv.pointwise_conv2.bias', 'conv_pointwise2.bias'), - ('conv.norm.weight', 'conv_batchnorm_weight.weights'), - ('conv.norm.bias', 'conv_batchnorm_bias.bias'), - ('conv.norm.running_mean', 'conv_batchnorm_running_mean.weights'), - ('conv.norm.running_var', 'conv_batchnorm_running_var.weights'), - ('norm_feed_forward1.weight', 'norm_ff1.weights'), - ('norm_feed_forward1.bias', 'norm_ff1.bias'), - ('norm_self_att.weight', 'norm_self_attn.weights'), - ('norm_self_att.bias', 'norm_self_attn.bias'), - ('norm_conv.weight', 'norm_conv.weights'), - ('norm_conv.bias', 'norm_conv.bias'), - ('norm_feed_forward2.weight', 'norm_ff2.weights'), - ('norm_feed_forward2.bias', 'norm_ff2.bias'), - ('norm_out.weight', 'norm_out.weights'), - ('norm_out.bias', 'norm_out.bias'), - ] - - for i in range(num_layers): - layer_prefix = f'encoder.layers.{i}.' - for suffix, out_suffix in layer_mappings: - key = layer_prefix + suffix - out_name = f'layer_{i}_{out_suffix}' - if key not in state_dict: - if suffix.endswith('num_batches_tracked'): - continue - missing_tensors.append((i, out_name, [key])) - continue - save_tensor_with_header( - state_dict[key], output_dir / out_name, precision, transpose=False, - stats_tracker=quantization_stats, args=args, model_type=detected_model_type - ) - saved_tensor_full_names.add(key) - tracked_key = layer_prefix + 'conv.norm.num_batches_tracked' - if tracked_key in state_dict: - saved_tensor_full_names.add(tracked_key) - elif detected_model_type == 'parakeet_tdt': - global_mappings = [ - (['encoder.pre_encode.conv.0.weight', 'encoder.subsampling.layers.0.weight'], 'subsampling_conv0_weight.weights'), - (['encoder.pre_encode.conv.0.bias', 'encoder.subsampling.layers.0.bias'], 'subsampling_conv0_bias.bias'), - (['encoder.pre_encode.conv.2.weight', 'encoder.subsampling.layers.2.weight'], 'subsampling_depthwise1_weight.weights'), - (['encoder.pre_encode.conv.2.bias', 'encoder.subsampling.layers.2.bias'], 'subsampling_depthwise1_bias.bias'), - (['encoder.pre_encode.conv.3.weight', 'encoder.subsampling.layers.3.weight'], 'subsampling_pointwise1_weight.weights'), - (['encoder.pre_encode.conv.3.bias', 'encoder.subsampling.layers.3.bias'], 'subsampling_pointwise1_bias.bias'), - (['encoder.pre_encode.conv.5.weight', 'encoder.subsampling.layers.5.weight'], 'subsampling_depthwise2_weight.weights'), - (['encoder.pre_encode.conv.5.bias', 'encoder.subsampling.layers.5.bias'], 'subsampling_depthwise2_bias.bias'), - (['encoder.pre_encode.conv.6.weight', 'encoder.subsampling.layers.6.weight'], 'subsampling_pointwise2_weight.weights'), - (['encoder.pre_encode.conv.6.bias', 'encoder.subsampling.layers.6.bias'], 'subsampling_pointwise2_bias.bias'), - (['encoder.pre_encode.out.weight', 'encoder.subsampling.linear.weight'], 'subsampling_linear_weight.weights'), - (['encoder.pre_encode.out.bias', 'encoder.subsampling.linear.bias'], 'subsampling_linear_bias.bias'), - ] - - for candidate_keys, out_name in global_mappings: - key = _find_first_key(state_dict, candidate_keys) - if key is None: - missing_tensors.append((-1, out_name, candidate_keys)) - continue - tensor = state_dict[key] - - # Some NeMo exports store conv2d kernels as [O, H, W, I]. - # Runtime expects [O, I, H, W]. - if out_name in { - 'subsampling_conv0_weight.weights', - 'subsampling_depthwise1_weight.weights', - 'subsampling_pointwise1_weight.weights', - 'subsampling_depthwise2_weight.weights', - 'subsampling_pointwise2_weight.weights', - } and hasattr(tensor, 'shape') and len(tensor.shape) == 4: - is_hwio_k3 = tensor.shape[1] == 3 and tensor.shape[2] == 3 and tensor.shape[3] >= 1 - is_hwio_pw = tensor.shape[1] == 1 and tensor.shape[2] == 1 and tensor.shape[3] > 1 - if is_hwio_k3 or is_hwio_pw: - tensor = tensor.permute(0, 3, 1, 2).contiguous() - save_tensor_with_header( - tensor, output_dir / out_name, precision, transpose=False, - stats_tracker=quantization_stats, args=args, model_type=detected_model_type - ) - saved_tensor_full_names.add(key) - - predictor_layers = int(model_config.get('predictor_num_layers', 0)) - if predictor_layers <= 0: - predictor_layers = 2 - - predictor_global = [ - (['decoder.prediction.embed.weight'], 'tdt_predictor_embed.weights'), - ] - for candidate_keys, out_name in predictor_global: - key = _find_first_key(state_dict, candidate_keys) - if key is None: - missing_tensors.append((-1, out_name, candidate_keys)) - continue - save_tensor_with_header( - state_dict[key], output_dir / out_name, "FP16", transpose=False, - stats_tracker=quantization_stats, args=args, model_type=detected_model_type - ) - saved_tensor_full_names.add(key) - - for i in range(predictor_layers): - lstm_mappings = [ - ([f'decoder.prediction.dec_rnn.lstm.{i}.Wx'], f'tdt_predictor_lstm_{i}_weight_ih.weights'), - ([f'decoder.prediction.dec_rnn.lstm.{i}.Wh'], f'tdt_predictor_lstm_{i}_weight_hh.weights'), - ([f'decoder.prediction.dec_rnn.lstm.{i}.bias'], f'tdt_predictor_lstm_{i}_bias.weights'), - ] - for candidate_keys, out_name in lstm_mappings: - key = _find_first_key(state_dict, candidate_keys) - if key is None: - missing_tensors.append((-1, out_name, candidate_keys)) - continue - save_tensor_with_header( - state_dict[key], output_dir / out_name, "FP16", transpose=False, - stats_tracker=quantization_stats, args=args, model_type=detected_model_type - ) - saved_tensor_full_names.add(key) - - joint_mappings = [ - (['joint.enc.weight'], 'tdt_joint_enc.weights'), - (['joint.enc.bias'], 'tdt_joint_enc.bias'), - (['joint.pred.weight'], 'tdt_joint_pred.weights'), - (['joint.pred.bias'], 'tdt_joint_pred.bias'), - (['joint.joint_net.2.weight', 'joint.joint_net.0.weight'], 'tdt_joint_out.weights'), - (['joint.joint_net.2.bias', 'joint.joint_net.0.bias'], 'tdt_joint_out.bias'), - ] - for candidate_keys, out_name in joint_mappings: - key = _find_first_key(state_dict, candidate_keys) - if key is None: - missing_tensors.append((-1, out_name, candidate_keys)) - continue - save_tensor_with_header( - state_dict[key], output_dir / out_name, precision, transpose=False, - stats_tracker=quantization_stats, args=args, model_type=detected_model_type - ) - saved_tensor_full_names.add(key) - - layer_mappings = [ - (['feed_forward1.linear1.weight'], 'ff1_linear1.weights'), - (['feed_forward1.linear1.bias'], 'ff1_linear1.bias'), - (['feed_forward1.linear2.weight'], 'ff1_linear2.weights'), - (['feed_forward1.linear2.bias'], 'ff1_linear2.bias'), - (['feed_forward2.linear1.weight'], 'ff2_linear1.weights'), - (['feed_forward2.linear1.bias'], 'ff2_linear1.bias'), - (['feed_forward2.linear2.weight'], 'ff2_linear2.weights'), - (['feed_forward2.linear2.bias'], 'ff2_linear2.bias'), - ([ - 'self_attn.q_proj.weight', - 'self_attn.linear_q.weight', - 'self_attention.q_proj.weight', - 'self_attention.linear_q.weight' - ], 'self_attn_q.weights'), - ([ - 'self_attn.q_proj.bias', - 'self_attn.linear_q.bias', - 'self_attention.q_proj.bias', - 'self_attention.linear_q.bias' - ], 'self_attn_q.bias'), - ([ - 'self_attn.k_proj.weight', - 'self_attn.linear_k.weight', - 'self_attention.k_proj.weight', - 'self_attention.linear_k.weight' - ], 'self_attn_k.weights'), - ([ - 'self_attn.k_proj.bias', - 'self_attn.linear_k.bias', - 'self_attention.k_proj.bias', - 'self_attention.linear_k.bias' - ], 'self_attn_k.bias'), - ([ - 'self_attn.v_proj.weight', - 'self_attn.linear_v.weight', - 'self_attention.v_proj.weight', - 'self_attention.linear_v.weight' - ], 'self_attn_v.weights'), - ([ - 'self_attn.v_proj.bias', - 'self_attn.linear_v.bias', - 'self_attention.v_proj.bias', - 'self_attention.linear_v.bias' - ], 'self_attn_v.bias'), - ([ - 'self_attn.o_proj.weight', - 'self_attn.linear_out.weight', - 'self_attention.o_proj.weight', - 'self_attention.linear_out.weight' - ], 'self_attn_output.weights'), - ([ - 'self_attn.o_proj.bias', - 'self_attn.linear_out.bias', - 'self_attention.o_proj.bias', - 'self_attention.linear_out.bias' - ], 'self_attn_output.bias'), - ([ - 'self_attn.relative_k_proj.weight', - 'self_attn.linear_pos.weight', - 'self_attention.relative_k_proj.weight', - 'self_attention.linear_pos.weight' - ], 'self_attn_relative_k.weights'), - ([ - 'self_attn.bias_u', - 'self_attn.pos_bias_u', - 'self_attention.bias_u', - 'self_attention.pos_bias_u' - ], 'self_attn_bias_u.weights'), - ([ - 'self_attn.bias_v', - 'self_attn.pos_bias_v', - 'self_attention.bias_v', - 'self_attention.pos_bias_v' - ], 'self_attn_bias_v.weights'), - (['conv.pointwise_conv1.weight'], 'conv_pointwise1.weights'), - (['conv.pointwise_conv1.bias'], 'conv_pointwise1.bias'), - (['conv.depthwise_conv.weight'], 'conv_depthwise.weights'), - (['conv.depthwise_conv.bias'], 'conv_depthwise.bias'), - (['conv.pointwise_conv2.weight'], 'conv_pointwise2.weights'), - (['conv.pointwise_conv2.bias'], 'conv_pointwise2.bias'), - (['conv.norm.weight', 'conv.batch_norm.weight'], 'conv_batchnorm_weight.weights'), - (['conv.norm.bias', 'conv.batch_norm.bias'], 'conv_batchnorm_bias.bias'), - (['conv.norm.running_mean', 'conv.batch_norm.running_mean'], 'conv_batchnorm_running_mean.weights'), - (['conv.norm.running_var', 'conv.batch_norm.running_var'], 'conv_batchnorm_running_var.weights'), - (['norm_feed_forward1.weight'], 'norm_ff1.weights'), - (['norm_feed_forward1.bias'], 'norm_ff1.bias'), - (['norm_self_att.weight'], 'norm_self_attn.weights'), - (['norm_self_att.bias'], 'norm_self_attn.bias'), - (['norm_conv.weight'], 'norm_conv.weights'), - (['norm_conv.bias'], 'norm_conv.bias'), - (['norm_feed_forward2.weight'], 'norm_ff2.weights'), - (['norm_feed_forward2.bias'], 'norm_ff2.bias'), - (['norm_out.weight'], 'norm_out.weights'), - (['norm_out.bias'], 'norm_out.bias'), - ] - - # Some Parakeet-TDT checkpoints are bias-free in several encoder submodules. - # Runtime currently expects bias files to exist, so synthesize zero biases - # from the matching weight output dimension when missing. - zero_bias_fallbacks = { - 'ff1_linear1.bias': ['feed_forward1.linear1.weight'], - 'ff1_linear2.bias': ['feed_forward1.linear2.weight'], - 'ff2_linear1.bias': ['feed_forward2.linear1.weight'], - 'ff2_linear2.bias': ['feed_forward2.linear2.weight'], - 'self_attn_q.bias': ['self_attn.q_proj.weight', 'self_attn.linear_q.weight', 'self_attention.q_proj.weight', 'self_attention.linear_q.weight'], - 'self_attn_k.bias': ['self_attn.k_proj.weight', 'self_attn.linear_k.weight', 'self_attention.k_proj.weight', 'self_attention.linear_k.weight'], - 'self_attn_v.bias': ['self_attn.v_proj.weight', 'self_attn.linear_v.weight', 'self_attention.v_proj.weight', 'self_attention.linear_v.weight'], - 'self_attn_output.bias': ['self_attn.o_proj.weight', 'self_attn.linear_out.weight', 'self_attention.o_proj.weight', 'self_attention.linear_out.weight'], - 'conv_pointwise1.bias': ['conv.pointwise_conv1.weight'], - 'conv_depthwise.bias': ['conv.depthwise_conv.weight'], - 'conv_pointwise2.bias': ['conv.pointwise_conv2.weight'], - } - - for i in range(num_layers): - layer_prefix = f'encoder.layers.{i}.' - for suffixes, out_suffix in layer_mappings: - candidate_keys = [layer_prefix + suffix for suffix in suffixes] - key = _find_first_key(state_dict, candidate_keys) - out_name = f'layer_{i}_{out_suffix}' - if key is None: - fallback_suffixes = zero_bias_fallbacks.get(out_suffix) - if fallback_suffixes is not None and torch is not None: - weight_keys = [layer_prefix + suffix for suffix in fallback_suffixes] - weight_key = _find_first_key(state_dict, weight_keys) - if weight_key is not None: - weight_tensor = state_dict[weight_key] - out_dim = int(weight_tensor.shape[0]) if len(weight_tensor.shape) >= 1 else 0 - if out_dim > 0: - zero_bias = torch.zeros((out_dim,), dtype=torch.float32) - save_tensor_with_header( - zero_bias, output_dir / out_name, "FP16", transpose=False, - stats_tracker=quantization_stats, args=args, model_type=detected_model_type - ) - continue - missing_tensors.append((i, out_name, candidate_keys)) - continue - tensor = state_dict[key] - - # Some NeMo checkpoints store conv1d kernels in [C_out, K, C_in]. - # Runtime expects [C_out, C_in, K]. - if out_suffix in {'conv_pointwise1.weights', 'conv_pointwise2.weights'}: - if hasattr(tensor, 'shape') and len(tensor.shape) == 3 and tensor.shape[1] == 1 and tensor.shape[2] > 1: - tensor = tensor.permute(0, 2, 1).contiguous() - elif out_suffix == 'conv_depthwise.weights': - if hasattr(tensor, 'shape') and len(tensor.shape) == 3 and tensor.shape[1] == 9 and tensor.shape[2] == 1: - tensor = tensor.permute(0, 2, 1).contiguous() - save_tensor_with_header( - tensor, output_dir / out_name, precision, transpose=False, - stats_tracker=quantization_stats, args=args, model_type=detected_model_type - ) - saved_tensor_full_names.add(key) - tracked_keys = [ - layer_prefix + 'conv.norm.num_batches_tracked', - layer_prefix + 'conv.batch_norm.num_batches_tracked', - ] - for tracked_key in tracked_keys: - if tracked_key in state_dict: - saved_tensor_full_names.add(tracked_key) - else: - for i in range(num_layers): - layer_prefixes = [p.format(i=i) for p in LAYER_PREFIXES] - - existing_prefixes = set() - for prefix in layer_prefixes: - for key in state_dict.keys(): - if key.startswith(prefix): - existing_prefixes.add(prefix) - - if not existing_prefixes: - missing_tensors.append((i, "", [""])) - continue - - num_kv_shared = int(model_config.get('num_kv_shared_layers', 0)) - first_shared = num_layers - num_kv_shared if num_layers > num_kv_shared else num_layers - weight_patterns = get_layer_weight_patterns(i, precision, model_type_str, skip_kv=(i >= first_shared)) - - for layer_prefix in existing_prefixes: - for name_patterns, tensor_precision, output_name, should_transpose in weight_patterns: - found = False - for pattern in name_patterns: - if model_type_str == 'lfm2_moe' and pattern.startswith('feed_forward.experts.{channel}.'): - num_channels = int(model_config.get('num_experts', 0)) - if num_channels <= 0: - continue - - matched_any_channel = False - for channel_idx in range(num_channels): - full_name = layer_prefix + pattern.replace('{channel}', str(channel_idx)) - if full_name not in state_dict: - continue - - channel_output_name = output_name.replace('{channel}', str(channel_idx)) - tensor = state_dict[full_name] - save_tensor_with_header(tensor, output_dir / channel_output_name, tensor_precision, transpose=should_transpose, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - saved_tensor_full_names.add(full_name) - matched_any_channel = True - - if matched_any_channel: - found = True - break - - full_name = layer_prefix + pattern - if full_name in state_dict: - tensor = state_dict[full_name] - - if 'mlp.fc1.weight' in pattern and model_type_str == 'moonshine': - activation = model_config.get('enc_hidden_act', 'gelu') if 'encoder' in layer_prefix else model_config.get('dec_hidden_act', 'gelu') - if activation == 'silu': - w = tensor - b_name = full_name.replace('weight', 'bias') - b = state_dict.get(b_name) - - inter_size = model_config.get('intermediate_size', 0) - if inter_size == 0: - inter_size = model_config.get('ffn_intermediate_dim', 0) - - half_dim = w.shape[0] // 2 - w_up = w[:half_dim, :] - w_gate = w[half_dim:, :] - - save_name_prefix = output_name.replace('mlp_fc1.weights', '') - if 'encoder' in layer_prefix: - save_name_prefix = "encoder_" + save_name_prefix - - save_tensor_with_header(w_gate, output_dir / (save_name_prefix + "ffn_gate.weights"), precision, transpose=should_transpose, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - save_tensor_with_header(w_up, output_dir / (save_name_prefix + "ffn_up.weights"), precision, transpose=should_transpose, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - - if b is not None: - b_up = b[:half_dim] - b_gate = b[half_dim:] - save_tensor_with_header(b_gate, output_dir / (save_name_prefix + "ffn_gate.bias"), precision, transpose=should_transpose, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - save_tensor_with_header(b_up, output_dir / (save_name_prefix + "ffn_up.bias"), precision, transpose=should_transpose, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - - saved_tensor_full_names.add(full_name) - if b is not None: - saved_tensor_full_names.add(b_name) - found = True - break - - if model_type_str.startswith('qwen3_5') and pattern == 'linear_attn.in_proj_qkv.weight': - if tensor.ndim != 2: - raise ValueError(f"Invalid qwen3_5 linear_attn.in_proj_qkv shape: {tensor.shape}") - - q_dim = int(model_config.get('linear_q_proj_dim', 0)) - k_dim = int(model_config.get('linear_k_proj_dim', 0)) - v_dim = int(model_config.get('linear_v_proj_dim', 0)) - row_dim = int(tensor.shape[0]) - - save_tensor_with_header( - tensor, output_dir / output_name, tensor_precision, transpose=should_transpose, - stats_tracker=quantization_stats, args=args, model_type=detected_model_type - ) - - if q_dim > 0 and k_dim > 0 and v_dim > 0 and row_dim == (q_dim + k_dim + v_dim): - q_weight = tensor[:q_dim, :] - k_weight = tensor[q_dim:q_dim + k_dim, :] - v_weight = tensor[q_dim + k_dim:, :] - - save_tensor_with_header( - q_weight, output_dir / f'layer_{i}_linear_attn_q.weights', tensor_precision, transpose=False, - stats_tracker=quantization_stats, args=args, model_type=detected_model_type - ) - save_tensor_with_header( - k_weight, output_dir / f'layer_{i}_linear_attn_k.weights', tensor_precision, transpose=False, - stats_tracker=quantization_stats, args=args, model_type=detected_model_type - ) - save_tensor_with_header( - v_weight, output_dir / f'layer_{i}_linear_attn_v.weights', tensor_precision, transpose=False, - stats_tracker=quantization_stats, args=args, model_type=detected_model_type - ) - - saved_tensor_full_names.add(full_name) - found = True - break - - if pattern.startswith('attn.Wqkv.') and model_type_str == 'nomic_bert': - if tensor.ndim == 1: - tensor = tensor.reshape(3, -1) - elif tensor.ndim == 2: - tensor = tensor.reshape(3, -1, tensor.size(-1)) - else: - raise ValueError(f"Invalid tensor shape: {tensor.shape}") - for j, ch in enumerate(['q', 'k', 'v']): - channel_output_name = output_name.replace('{channel}', ch) - save_tensor_with_header(tensor[j], output_dir / channel_output_name, tensor_precision, transpose=should_transpose, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - saved_tensor_full_names.add(full_name) - found = True - break - elif model_type_str == 'nomic_bert' and pattern.startswith('mlp.experts.') and 'bias' not in pattern: - num_experts = model_config['num_experts'] - if tensor.ndim != 2: - raise ValueError(f"Invalid tensor shape: {tensor.shape}") - tensor = tensor.reshape(num_experts, -1, tensor.size(-1)) - for expert_idx in range(num_experts): - expert_tensor = tensor[expert_idx] - expert_output_name = output_name.replace('{channel}', str(expert_idx)) - save_tensor_with_header(expert_tensor, output_dir / expert_output_name, tensor_precision, transpose=should_transpose, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - saved_tensor_full_names.add(full_name) - found = True - break - if model_type_str == 'whisper': - temp = layer_prefix[:layer_prefix.find('.')] + "." + output_name - save_tensor_with_header(tensor, output_dir / temp, tensor_precision, transpose=should_transpose, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - elif model_type_str == 'moonshine' and 'encoder' in layer_prefix: - enc_output_name = "encoder_" + output_name - save_tensor_with_header(tensor, output_dir / enc_output_name, tensor_precision, transpose=should_transpose, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - else: - save_tensor_with_header(tensor, output_dir / output_name, tensor_precision, transpose=should_transpose, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - saved_tensor_full_names.add(full_name) - found = True - break - - if not found and 'mlp.fc1.weight' in output_name and model_type_str == 'moonshine': - activation = model_config.get('enc_hidden_act', 'gelu') if 'encoder' in layer_prefix else model_config.get('dec_hidden_act', 'gelu') - - if activation == 'silu': - full_name = layer_prefix + name_patterns[0][0] - w_name = layer_prefix + 'mlp.fc1.weight' - b_name = layer_prefix + 'mlp.fc1.bias' - - if w_name in state_dict: - w = state_dict[w_name] - if b_name in state_dict: - b = state_dict[b_name] - else: - b = None - - inter_size = model_config.get('intermediate_size', 0) - if inter_size == 0: - inter_size = model_config.get('ffn_intermediate_dim', 0) - - half_dim = w.shape[0] // 2 - - w_up = w[:half_dim, :] - w_gate = w[half_dim:, :] - - save_name_prefix = output_name.replace('mlp_fc1.weights', '') - if 'encoder' in layer_prefix: - save_name_prefix = "encoder_" + save_name_prefix - - save_tensor_with_header(w_gate, output_dir / (save_name_prefix + "ffn_gate.weights"), precision, transpose=should_transpose, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - save_tensor_with_header(w_up, output_dir / (save_name_prefix + "ffn_up.weights"), precision, transpose=should_transpose, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - - if b is not None: - b_up = b[:half_dim] - b_gate = b[half_dim:] - save_tensor_with_header(b_gate, output_dir / (save_name_prefix + "ffn_gate.bias"), precision, transpose=should_transpose, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - save_tensor_with_header(b_up, output_dir / (save_name_prefix + "ffn_up.bias"), precision, transpose=should_transpose, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - - saved_tensor_full_names.add(w_name) - if b_name in state_dict: - saved_tensor_full_names.add(b_name) - found = True - break - - if not found and 'c_attn.weight' in name_patterns[0]: - attn_name = layer_prefix + 'attn.c_attn.weight' - if attn_name in state_dict: - combined_weight = state_dict[attn_name] - hidden_size = combined_weight.shape[0] - q_weight = combined_weight[:, :hidden_size] - k_weight = combined_weight[:, hidden_size:2*hidden_size] - v_weight = combined_weight[:, 2*hidden_size:] - - save_tensor_with_header(q_weight, output_dir / f'layer_{i}_attn_q.weights', precision, transpose=False, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - save_tensor_with_header(k_weight, output_dir / f'layer_{i}_attn_k.weights', precision, transpose=False, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - save_tensor_with_header(v_weight, output_dir / f'layer_{i}_attn_v.weights', precision, transpose=False, stats_tracker=quantization_stats, args=args, model_type=detected_model_type) - saved_tensor_full_names.add(attn_name) - found = True - - num_kv_shared = int(model_config.get('num_kv_shared_layers', 0)) - if num_kv_shared > 0: - first_shared = num_layers - num_kv_shared if num_layers > num_kv_shared else num_layers - for i in range(first_shared, num_layers): - for prefix in LAYER_PREFIXES: - lp = prefix.format(i=i) - for suffix in ['k_proj.weight', 'v_proj.weight', 'k_norm.weight', 'k_layernorm.weight']: - skipped_key = lp + 'self_attn.' + suffix - if skipped_key in state_dict: - saved_tensor_full_names.add(skipped_key) - - if saved_tensor_full_names != set(state_dict.keys()): - print(f"Warning: Unsaved tensors: {set(state_dict.keys()) - saved_tensor_full_names}") - - if missing_tensors: - missing_report = output_dir / "missing_weights.txt" - with open(missing_report, 'w') as fh: - fh.write("# Missing tensors during conversion\n") - for layer_idx, output_name, patterns in missing_tensors: - pattern_list = ', '.join(patterns) - fh.write(f"layer={layer_idx}, output={output_name}, patterns=[{pattern_list}]\n") - print(f"Warning: {len(missing_tensors)} tensors were not exported. See {missing_report.name} for details.") - - print_quantization_summary(quantization_stats, args) - - if detected_model_type in ['whisper', 'moonshine', 'parakeet', 'parakeet_tdt']: - if torch is None: - print("Warning: torch not available, skipping VAD bundling") - else: - print(f"Bundling Silero VAD weights for {detected_model_type} model...") - try: - import urllib.request - import tempfile - - # Download silero VAD JIT model directly to avoid torchaudio import issues - vad_jit_url = "https://github.com/snakers4/silero-vad/raw/master/src/silero_vad/data/silero_vad.jit" - with tempfile.NamedTemporaryFile(suffix='.jit', delete=False) as f: - jit_path = f.name - urllib.request.urlretrieve(vad_jit_url, jit_path) - vad_model = torch.jit.load(jit_path, map_location='cpu') - os.unlink(jit_path) - - vad_output_dir = str(Path(output_dir) / "vad") - convert_silero_vad_weights(vad_model, vad_output_dir, precision, args) - del vad_model - if torch.cuda.is_available(): - torch.cuda.empty_cache() - print("VAD weights bundled successfully") - except Exception as e: - print(f"Warning: Failed to bundle VAD weights: {e}") - - return model_config - - -def convert_pyannote_weights(model, output_dir, precision="FP16", args=None): - precision = 'FP16' - output_dir = Path(output_dir) - output_dir.mkdir(parents=True, exist_ok=True) - - sd = model.state_dict() - - def save(filename, key): - save_tensor_with_header(sd[key], output_dir / filename, precision=precision) - - save("sincnet_wav_norm_weight.weights", "sincnet.wav_norm1d.weight") - save("sincnet_wav_norm_bias.weights", "sincnet.wav_norm1d.bias") - - with torch.no_grad(): - sinc_filters = model.sincnet.conv1d[0].filterbank.filters() - save_tensor_with_header(sinc_filters, output_dir / "sincnet_sinc_filters.weights", precision=precision) - - save("sincnet_norm0_weight.weights", "sincnet.norm1d.0.weight") - save("sincnet_norm0_bias.weights", "sincnet.norm1d.0.bias") - save("sincnet_conv1_weight.weights", "sincnet.conv1d.1.weight") - save("sincnet_conv1_bias.weights", "sincnet.conv1d.1.bias") - save("sincnet_norm1_weight.weights", "sincnet.norm1d.1.weight") - save("sincnet_norm1_bias.weights", "sincnet.norm1d.1.bias") - save("sincnet_conv2_weight.weights", "sincnet.conv1d.2.weight") - save("sincnet_conv2_bias.weights", "sincnet.conv1d.2.bias") - save("sincnet_norm2_weight.weights", "sincnet.norm1d.2.weight") - save("sincnet_norm2_bias.weights", "sincnet.norm1d.2.bias") - - for i in range(4): - for direction, suffix in [("fwd", ""), ("bwd", "_reverse")]: - for w in ["weight_ih", "weight_hh", "bias_ih", "bias_hh"]: - save(f"lstm_{direction}_{i}_{w}.weights", f"lstm.{w}_l{i}{suffix}") - - save("linear_0_weight.weights", "linear.0.weight") - save("linear_0_bias.weights", "linear.0.bias") - save("linear_1_weight.weights", "linear.1.weight") - save("linear_1_bias.weights", "linear.1.bias") - save("classifier_weight.weights", "classifier.weight") - save("classifier_bias.weights", "classifier.bias") - - config = {"model_type": "pyannote", "precision": precision} - config_path = output_dir / "config.txt" - with open(config_path, "w") as f: - for key, value in config.items(): - f.write(f"{key}={value}\n") - return config - - -def convert_wespeaker_weights(model, output_dir, precision="FP16", args=None): - precision = 'FP16' - output_dir = Path(output_dir) - output_dir.mkdir(parents=True, exist_ok=True) - - sd = model.state_dict() - - def save(filename, tensor): - save_tensor_with_header(tensor, output_dir / filename, precision=precision) - - def fold_and_save(conv_name, bn_name, out_prefix): - w, b = fold_bn_into_conv( - sd[conv_name + '.weight'], - sd[bn_name + '.weight'], sd[bn_name + '.bias'], - sd[bn_name + '.running_mean'], sd[bn_name + '.running_var'], - ) - save(f'{out_prefix}_weight.weights', w) - save(f'{out_prefix}_bias.weights', b) - - fold_and_save('resnet.conv1', 'resnet.bn1', 'resnet_conv1') - - layer_blocks = {'layer1': 3, 'layer2': 4, 'layer3': 6, 'layer4': 3} - shortcut_layers = {'layer2', 'layer3', 'layer4'} - - for layer_name, num_blocks in layer_blocks.items(): - for block_idx in range(num_blocks): - prefix = f'resnet.{layer_name}.{block_idx}' - out = f'resnet_{layer_name}_{block_idx}' - - fold_and_save(f'{prefix}.conv1', f'{prefix}.bn1', f'{out}_conv1') - fold_and_save(f'{prefix}.conv2', f'{prefix}.bn2', f'{out}_conv2') - - if block_idx == 0 and layer_name in shortcut_layers: - w, b = fold_bn_into_conv( - sd[f'{prefix}.shortcut.0.weight'], - sd[f'{prefix}.shortcut.1.weight'], sd[f'{prefix}.shortcut.1.bias'], - sd[f'{prefix}.shortcut.1.running_mean'], sd[f'{prefix}.shortcut.1.running_var'], - ) - padded = np.zeros((w.shape[0], w.shape[1], 3, 3), dtype=w.dtype) - padded[:, :, 1, 1] = w[:, :, 0, 0] - save(f'{out}_shortcut_0_weight.weights', padded) - save(f'{out}_shortcut_0_bias.weights', b) - - save('resnet_seg_1_weight.weights', sd['resnet.seg_1.weight'].float().numpy()) - save('resnet_seg_1_bias.weights', sd['resnet.seg_1.bias'].float().numpy()) - - config = {"model_type": "wespeaker", "precision": precision} - config_path = output_dir / "config.txt" - with open(config_path, "w") as f: - for key, value in config.items(): - f.write(f"{key}={value}\n") - return config - - -def convert_silero_vad_weights(model, output_dir, precision="FP16", args=None): - output_dir = Path(output_dir) - output_dir.mkdir(parents=True, exist_ok=True) - - state_dict = model.state_dict() - - stft_basis = state_dict["_model.stft.forward_basis_buffer"] - n_fft_bins, _, window_size = stft_basis.shape - - lstm_weight_ih = state_dict["_model.decoder.rnn.weight_ih"] - lstm_hidden_size = lstm_weight_ih.shape[1] - - encoder_channels = [] - for i in range(4): - key = f"_model.encoder.{i}.reparam_conv.weight" - if key in state_dict: - weight = state_dict[key] - out_ch, in_ch, kernel = weight.shape - encoder_channels.append((in_ch, out_ch, kernel)) - - config = { - "model_type": "silero_vad", - "sampling_rate": 16000, - "window_size": int(window_size), - "n_fft_bins": int(n_fft_bins), - "num_encoder_blocks": len(encoder_channels), - "lstm_hidden_size": int(lstm_hidden_size), - "model_variant": "default", - "precision": precision, - } - - save_tensor_with_header( - stft_basis, output_dir / "stft_basis.weights", precision=precision - ) - - for i in range(config["num_encoder_blocks"]): - save_tensor_with_header( - state_dict[f"_model.encoder.{i}.reparam_conv.weight"], - output_dir / f"encoder_block_{i}_conv_weight.weights", - precision=precision, - ) - save_tensor_with_header( - state_dict[f"_model.encoder.{i}.reparam_conv.bias"], - output_dir / f"encoder_block_{i}_conv_bias.weights", - precision=precision, - ) - - lstm_weights = [ - ("_model.decoder.rnn.weight_ih", "lstm_weight_ih.weights"), - ("_model.decoder.rnn.weight_hh", "lstm_weight_hh.weights"), - ("_model.decoder.rnn.bias_ih", "lstm_bias_ih.weights"), - ("_model.decoder.rnn.bias_hh", "lstm_bias_hh.weights"), - ] - for key, filename in lstm_weights: - save_tensor_with_header( - state_dict[key], output_dir / filename, precision="FP16" - ) - - save_tensor_with_header( - state_dict["_model.decoder.decoder.2.weight"], - output_dir / "output_conv_weight.weights", - precision=precision, - ) - save_tensor_with_header( - state_dict["_model.decoder.decoder.2.bias"], - output_dir / "output_conv_bias.weights", - precision=precision, - ) - - config_path = output_dir / "config.txt" - with open(config_path, "w") as f: - for key, value in config.items(): - f.write(f"{key}={value}\n") - - return config - - -def _resolve_nested(tree, dotted_path): - node = tree - for key in dotted_path.split("."): - if not isinstance(node, dict) or key not in node: - return None - node = node[key] - return node - - -def _take_layer(tree, index): - if isinstance(tree, dict): - return {k: _take_layer(v, index) for k, v in tree.items()} - return tree[index] - - -def _count_params(tree): - if isinstance(tree, dict): - return sum(_count_params(v) for v in tree.values()) - shape = getattr(tree, "shape", None) - if shape is None: - return 0 - n = 1 - for d in shape: - n *= int(d) - return n - - -def _load_needle_checkpoint(checkpoint_path): - import pickle - with open(checkpoint_path, "rb") as f: - payload = pickle.load(f) - return payload["params"], dict(payload["config"]) - - -def _build_needle_config(model_cfg, params, precision): - hidden_dim = int(model_cfg["d_model"]) - heads = int(model_cfg["num_heads"]) - decoder_layers = int(model_cfg["num_decoder_layers"]) - compute_precision = "FP16" if precision in ("INT4", "INT8") else precision - return { - "model_type": "needle", - "precision": compute_precision, - "quantization": precision, - "vocab_size": int(model_cfg["vocab_size"]), - "hidden_dim": hidden_dim, - "num_layers": decoder_layers, - "num_encoder_layers": int(model_cfg["num_encoder_layers"]), - "num_decoder_layers": decoder_layers, - "attention_heads": heads, - "attention_kv_heads": int(model_cfg["num_kv_heads"]), - "attention_head_dim": hidden_dim // max(1, heads), - "ffn_intermediate_dim": int(model_cfg["d_ff"]), - "context_length": int(model_cfg["max_seq_len"]), - "rope_theta": float(model_cfg.get("rope_theta", 10000.0)), - "layer_norm_eps": 1e-6, - "pad_token_id": int(model_cfg.get("pad_token_id", 0)), - "tie_word_embeddings": True, - "no_feedforward": bool(model_cfg.get("no_feedforward", False)), - } - - -def _write_needle_config(output_dir, config): - config_path = output_dir / "config.txt" - with open(config_path, "w") as f: - for k, v in config.items(): - if isinstance(v, bool): - f.write(f"{k}={'true' if v else 'false'}\n") - else: - f.write(f"{k}={v}\n") - return config_path - - -def _export_needle_weights(output_dir, params, model_cfg, precision): - import warnings - - stats = create_quantization_stats() - saved = [] - - def save(filename, tensor, transpose=False, tensor_precision=None): - save_tensor_with_header(tensor, output_dir / filename, precision=tensor_precision or precision, - transpose=transpose, stats_tracker=stats, model_type="needle") - saved.append(filename) - - with warnings.catch_warnings(): - warnings.simplefilter("ignore", RuntimeWarning) - - for src, dst, tr in NEEDLE_GLOBAL_WEIGHTS: - tensor = _resolve_nested(params, src) - if tensor is not None: - save(dst, tensor, transpose=tr, tensor_precision="FP16" if src == "log_temp" else None) - - enc_stack = params["encoder"]["layers"]["EncoderBlock_0"] - for i in range(int(model_cfg["num_encoder_layers"])): - block = _take_layer(enc_stack, i) - for src, dst, tr in NEEDLE_ENCODER_LAYER_WEIGHTS: - tensor = _resolve_nested(block, src) - if tensor is not None: - save(f"encoder_layer_{i}_{dst}", tensor, transpose=tr) - - dec_stack = params["decoder"]["layers"]["DecoderBlock_0"] - for i in range(int(model_cfg["num_decoder_layers"])): - block = _take_layer(dec_stack, i) - for src, dst, tr in NEEDLE_DECODER_LAYER_WEIGHTS: - tensor = _resolve_nested(block, src) - if tensor is not None: - save(f"layer_{i}_{dst}", tensor, transpose=tr) - - print_quantization_summary(stats) - return saved - - -def convert_needle_checkpoint(checkpoint_path, tokenizer_path, output_dir, requested_precision='FP16'): - from .tokenizer import convert_sentencepiece_tokenizer - - output_dir = Path(output_dir) - output_dir.mkdir(parents=True, exist_ok=True) - precision = (requested_precision or "FP16").upper() - - params, model_cfg = _load_needle_checkpoint(checkpoint_path) - config = _build_needle_config(model_cfg, params, precision) - config_path = _write_needle_config(output_dir, config) - convert_sentencepiece_tokenizer(tokenizer_path, output_dir, int(model_cfg.get("max_seq_len", 131072))) - saved = _export_needle_weights(output_dir, params, model_cfg, precision) - - return {"output_dir": output_dir, "config_path": config_path, "weight_count": len(saved)} diff --git a/python/src/downloads.py b/python/src/downloads.py deleted file mode 100644 index 0d5d56479..000000000 --- a/python/src/downloads.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Download pre-converted model weights from Cactus-Compute HuggingFace. - -Usage:: - - from src.downloads import ensure_model - weights_dir = ensure_model("openai/whisper-tiny") -""" -import shutil -from pathlib import Path - -_PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent - - -def get_model_dir_name(model_id: str) -> str: - """Convert HuggingFace model ID to local directory name.""" - return model_id.split("/")[-1].lower() - - -def get_weights_dir(model_id: str) -> Path: - """Return ``/weights/``.""" - if "silero-vad" in model_id.lower(): - return _PROJECT_ROOT / "weights" / "silero-vad" - return _PROJECT_ROOT / "weights" / get_model_dir_name(model_id) - - -def download_from_hf(model_id: str, weights_dir: Path, precision: str = "INT4") -> bool: - """Download pre-converted weights from Cactus-Compute HuggingFace. - - Returns True on success, False if the model is unavailable or download fails. - """ - try: - from huggingface_hub import hf_hub_download, list_repo_files - import zipfile - except ImportError: - print("huggingface_hub not installed — run: pip install huggingface_hub") - return False - - model_name = get_model_dir_name(model_id) - repo_id = f"Cactus-Compute/{model_id.split('/')[-1]}" - - try: - precision_lower = precision.lower() - apple_zip = f"{model_name}-{precision_lower}-apple.zip" - standard_zip = f"{model_name}-{precision_lower}.zip" - - repo_files = list_repo_files(repo_id, repo_type="model") - - zip_file = None - if f"weights/{apple_zip}" in repo_files: - zip_file = apple_zip - elif f"weights/{standard_zip}" in repo_files: - zip_file = standard_zip - else: - print(f"Pre-converted model not found in {repo_id}") - return False - - print(f"Downloading {repo_id}/{zip_file} ...") - - zip_path = hf_hub_download( - repo_id=repo_id, - filename=f"weights/{zip_file}", - repo_type="model", - ) - - weights_dir.mkdir(parents=True, exist_ok=True) - - print("Extracting model weights...") - with zipfile.ZipFile(zip_path, "r") as zip_ref: - zip_ref.extractall(weights_dir) - - if not (weights_dir / "config.txt").exists(): - print(f"Error: downloaded model is missing config.txt") - if weights_dir.exists(): - shutil.rmtree(weights_dir) - return False - - config_path = weights_dir / "config.txt" - config_text = config_path.read_text() - if "quantization=" not in config_text: - with open(config_path, "a") as f: - f.write(f"quantization={precision}\n") - - print(f"Model ready at {weights_dir}") - return True - - except Exception as exc: - print(f"Download failed: {exc}") - if weights_dir.exists(): - shutil.rmtree(weights_dir) - return False - - -def ensure_model(model_id: str, precision: str = "INT4") -> Path: - """Return the weights directory, downloading if necessary. - - Raises ``RuntimeError`` if the model cannot be obtained. - """ - weights_dir = get_weights_dir(model_id) - if (weights_dir / "config.txt").exists(): - return weights_dir - if not download_from_hf(model_id, weights_dir, precision): - raise RuntimeError(f"Could not download model {model_id}") - return weights_dir diff --git a/python/src/graph.py b/python/src/graph.py deleted file mode 100644 index b7eb3b03b..000000000 --- a/python/src/graph.py +++ /dev/null @@ -1,1148 +0,0 @@ -import ctypes -import numpy as np - -from .cactus import _lib, cactus_node_t, cactus_tensor_info_t - - -class Graph: - INT8 = 0 - FP16 = 1 - FP32 = 2 - INT4 = 3 - CPU = 0 - NPU = 1 - ACT_SILU = 0 - ACT_GELU = 1 - ACT_GELU_ERF = 2 - ACT_RELU = 3 - ACT_SIGMOID = 4 - ACT_TANH = 5 - - def __init__(self): - self.h = _lib.cactus_graph_create() - if not self.h: - raise RuntimeError("cactus_graph_create failed") - - def save(self, filename): - rc = _lib.cactus_graph_save(self.h, str(filename).encode()) - if rc != 0: - raise RuntimeError("graph_save failed") - - @classmethod - def load(cls, filename): - h = _lib.cactus_graph_load(str(filename).encode()) - if not h: - raise RuntimeError("cactus_graph_load failed") - obj = cls.__new__(cls) - obj.h = h - return obj - - def __del__(self): - h = getattr(self, "h", None) - if h: - _lib.cactus_graph_destroy(h) - self.h = None - - def input(self, shape, dtype=FP16): - shape = tuple(int(x) for x in shape) - arr = (ctypes.c_size_t * len(shape))(*shape) - out = cactus_node_t() - rc = _lib.cactus_graph_input(self.h, arr, len(shape), int(dtype), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_input failed") - return self._tensor_from_node(out.value) - - def set_input(self, tensor, data, dtype=None): - if not isinstance(tensor, Tensor): - raise TypeError("tensor must be a Tensor") - if tensor.g is not self: - raise ValueError("tensor belongs to a different graph") - target_dtype = int(tensor.dtype if dtype is None else dtype) - arr = self._coerce_input_array(data, target_dtype) - rc = _lib.cactus_graph_set_input( - self.h, - cactus_node_t(tensor.id), - arr.ctypes.data_as(ctypes.c_void_p), - target_dtype, - ) - if rc != 0: - raise RuntimeError("graph_set_input failed") - - def set_external_input(self, tensor, data_ptr, dtype=None): - if not isinstance(tensor, Tensor): - raise TypeError("tensor must be a Tensor") - if tensor.g is not self: - raise ValueError("tensor belongs to a different graph") - target_dtype = int(tensor.dtype if dtype is None else dtype) - ptr = ctypes.c_void_p(data_ptr if isinstance(data_ptr, int) else int(data_ptr)) - rc = _lib.cactus_graph_set_external_input( - self.h, - cactus_node_t(tensor.id), - ptr, - target_dtype, - ) - if rc != 0: - raise RuntimeError("graph_set_external_input failed") - - def hard_reset(self): - rc = _lib.cactus_graph_hard_reset(self.h) - if rc != 0: - raise RuntimeError("graph_hard_reset failed") - - def execute(self): - rc = _lib.cactus_graph_execute(self.h) - if rc != 0: - raise RuntimeError("graph_execute failed") - - def add(self, a, b): - return self._binary("cactus_graph_add", a, b) - - def add_clipped(self, a, b): - return self._binary("cactus_graph_add_clipped", a, b) - - def subtract(self, a, b): - return self._binary("cactus_graph_subtract", a, b) - - def multiply(self, a, b): - return self._binary("cactus_graph_multiply", a, b) - - def divide(self, a, b): - return self._binary("cactus_graph_divide", a, b) - - def abs(self, x): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_abs(self.h, cactus_node_t(x.id), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_abs failed") - return self._tensor_from_node(out.value) - - def pow(self, x, exponent): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_pow(self.h, cactus_node_t(x.id), ctypes.c_float(float(exponent)), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_pow failed") - return self._tensor_from_node(out.value) - - def precision_cast(self, x, dtype): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_precision_cast(self.h, cactus_node_t(x.id), int(dtype), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_precision_cast failed") - return self._tensor_from_node(out.value) - - def quantize_activations(self, x): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_quantize_activations(self.h, cactus_node_t(x.id), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_quantize_activations failed") - return self._tensor_from_node(out.value) - - def _scalar(self, fn_name, x, value=None): - x = self._ensure_tensor(x) - out = cactus_node_t() - fn = getattr(_lib, fn_name) - if value is None: - rc = fn(self.h, cactus_node_t(x.id), ctypes.byref(out)) - else: - rc = fn(self.h, cactus_node_t(x.id), ctypes.c_float(float(value)), ctypes.byref(out)) - if rc != 0: - raise RuntimeError(f"{fn_name} failed") - return self._tensor_from_node(out.value) - - def scalar_add(self, x, value): - return self._scalar("cactus_graph_scalar_add", x, value) - - def scalar_subtract(self, x, value): - return self._scalar("cactus_graph_scalar_subtract", x, value) - - def scalar_multiply(self, x, value): - return self._scalar("cactus_graph_scalar_multiply", x, value) - - def scalar_divide(self, x, value): - return self._scalar("cactus_graph_scalar_divide", x, value) - - def scalar_exp(self, x): - return self._scalar("cactus_graph_scalar_exp", x) - - def scalar_sqrt(self, x): - return self._scalar("cactus_graph_scalar_sqrt", x) - - def scalar_cos(self, x): - return self._scalar("cactus_graph_scalar_cos", x) - - def scalar_sin(self, x): - return self._scalar("cactus_graph_scalar_sin", x) - - def scalar_log(self, x): - return self._scalar("cactus_graph_scalar_log", x) - - def view(self, x, shape): - x = self._ensure_tensor(x) - shape = tuple(int(v) for v in shape) - arr = (ctypes.c_size_t * len(shape))(*shape) - out = cactus_node_t() - rc = _lib.cactus_graph_view(self.h, cactus_node_t(x.id), arr, len(shape), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_view failed") - return self._tensor_from_node(out.value) - - def reshape(self, x, shape): - x = self._ensure_tensor(x) - shape = tuple(int(v) for v in shape) - arr = (ctypes.c_size_t * len(shape))(*shape) - out = cactus_node_t() - rc = _lib.cactus_graph_reshape(self.h, cactus_node_t(x.id), arr, len(shape), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_reshape failed") - return self._tensor_from_node(out.value) - - def flatten(self, x, start_dim=0, end_dim=-1): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_flatten( - self.h, - cactus_node_t(x.id), - ctypes.c_int32(int(start_dim)), - ctypes.c_int32(int(end_dim)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_flatten failed") - return self._tensor_from_node(out.value) - - def slice(self, x, axis, start, length=0): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_slice( - self.h, - cactus_node_t(x.id), - ctypes.c_int32(int(axis)), - ctypes.c_size_t(int(start)), - ctypes.c_size_t(int(length)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_slice failed") - return self._tensor_from_node(out.value) - - def index(self, x, index_value, axis=0): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_index( - self.h, - cactus_node_t(x.id), - ctypes.c_size_t(int(index_value)), - ctypes.c_int32(int(axis)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_index failed") - return self._tensor_from_node(out.value) - - def transpose(self, x, backend=CPU): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_transpose( - self.h, - cactus_node_t(x.id), - ctypes.c_int32(int(backend)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_transpose failed") - return self._tensor_from_node(out.value) - - def permute(self, x, permutation, backend=CPU): - x = self._ensure_tensor(x) - permutation = tuple(int(v) for v in permutation) - arr = (ctypes.c_size_t * len(permutation))(*permutation) - out = cactus_node_t() - rc = _lib.cactus_graph_transpose_n( - self.h, - cactus_node_t(x.id), - arr, - len(permutation), - ctypes.c_int32(int(backend)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_transpose_n failed") - return self._tensor_from_node(out.value) - - def matmul(self, a, b, pretransposed_rhs=False, backend=CPU): - a = self._ensure_tensor(a) - b = self._ensure_tensor(b) - out = cactus_node_t() - rc = _lib.cactus_graph_matmul( - self.h, - cactus_node_t(a.id), - cactus_node_t(b.id), - ctypes.c_bool(bool(pretransposed_rhs)), - ctypes.c_int32(int(backend)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_matmul failed") - return self._tensor_from_node(out.value) - - def gather(self, tensor, indices): - tensor = self._ensure_tensor(tensor) - indices = self._ensure_tensor(indices) - out = cactus_node_t() - rc = _lib.cactus_graph_gather(self.h, cactus_node_t(tensor.id), cactus_node_t(indices.id), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_gather failed") - return self._tensor_from_node(out.value) - - def embedding_from_tensor(self, embedding_tensor, indices): - embedding_tensor = self._ensure_tensor(embedding_tensor) - indices = self._ensure_tensor(indices) - out = cactus_node_t() - rc = _lib.cactus_graph_embedding_from_tensor( - self.h, cactus_node_t(embedding_tensor.id), cactus_node_t(indices.id), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_embedding_from_tensor failed") - return self._tensor_from_node(out.value) - - def embedding_from_file(self, filename, indices): - indices = self._ensure_tensor(indices) - out = cactus_node_t() - rc = _lib.cactus_graph_embedding_from_file(self.h, str(filename).encode(), cactus_node_t(indices.id), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_embedding_from_file failed") - return self._tensor_from_node(out.value) - - def mmap_embeddings(self, filename): - out = cactus_node_t() - rc = _lib.cactus_graph_mmap_embeddings(self.h, str(filename).encode(), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_mmap_embeddings failed") - return self._tensor_from_node(out.value) - - def mmap_weights(self, filename): - out = cactus_node_t() - rc = _lib.cactus_graph_mmap_weights(self.h, str(filename).encode(), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_mmap_weights failed") - return self._tensor_from_node(out.value) - - def bilinear_interpolation(self, pos_embeds, dst_height, dst_width): - pos_embeds = self._ensure_tensor(pos_embeds) - out = cactus_node_t() - rc = _lib.cactus_graph_bilinear_interpolation( - self.h, cactus_node_t(pos_embeds.id), ctypes.c_size_t(int(dst_height)), ctypes.c_size_t(int(dst_width)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_bilinear_interpolation failed") - return self._tensor_from_node(out.value) - - def set_grouped_scales(self, tensor, group_size, num_groups, scales): - tensor = self._ensure_tensor(tensor) - arr = np.ascontiguousarray(scales, dtype=np.float16) - rc = _lib.cactus_graph_set_grouped_scales( - self.h, - cactus_node_t(tensor.id), - ctypes.c_size_t(int(group_size)), - ctypes.c_size_t(int(num_groups)), - arr.ctypes.data_as(ctypes.c_void_p), - ) - if rc != 0: - raise RuntimeError("graph_set_grouped_scales failed") - - def set_interleaved(self, tensor, interleaved=True, original_n=0): - tensor = self._ensure_tensor(tensor) - rc = _lib.cactus_graph_set_interleaved( - self.h, cactus_node_t(tensor.id), ctypes.c_bool(bool(interleaved)), ctypes.c_size_t(int(original_n)) - ) - if rc != 0: - raise RuntimeError("graph_set_interleaved failed") - - def release_weight_pages(self, tensor): - tensor = self._ensure_tensor(tensor) - rc = _lib.cactus_graph_release_weight_pages(self.h, cactus_node_t(tensor.id)) - if rc != 0: - raise RuntimeError("graph_release_weight_pages failed") - - def prefetch_weight_pages(self, tensor): - tensor = self._ensure_tensor(tensor) - rc = _lib.cactus_graph_prefetch_weight_pages(self.h, cactus_node_t(tensor.id)) - if rc != 0: - raise RuntimeError("graph_prefetch_weight_pages failed") - - def release_all_weight_pages(self): - rc = _lib.cactus_graph_release_all_weight_pages(self.h) - if rc != 0: - raise RuntimeError("graph_release_all_weight_pages failed") - - def concat(self, a, b, axis=0): - a = self._ensure_tensor(a) - b = self._ensure_tensor(b) - out = cactus_node_t() - rc = _lib.cactus_graph_concat( - self.h, - cactus_node_t(a.id), - cactus_node_t(b.id), - ctypes.c_int32(int(axis)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_concat failed") - return self._tensor_from_node(out.value) - - def cat(self, tensors, axis=0): - tensors = [self._ensure_tensor(t) for t in tensors] - if not tensors: - raise ValueError("cat requires at least one tensor") - ids = (cactus_node_t * len(tensors))(*(cactus_node_t(t.id) for t in tensors)) - out = cactus_node_t() - rc = _lib.cactus_graph_cat( - self.h, - ids, - ctypes.c_size_t(len(tensors)), - ctypes.c_int32(int(axis)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_cat failed") - return self._tensor_from_node(out.value) - - def groupnorm(self, x, weight, bias, num_groups, eps=1e-5): - x = self._ensure_tensor(x) - weight = self._ensure_tensor(weight) - bias = self._ensure_tensor(bias) - out = cactus_node_t() - rc = _lib.cactus_graph_groupnorm( - self.h, - cactus_node_t(x.id), - cactus_node_t(weight.id), - cactus_node_t(bias.id), - ctypes.c_size_t(int(num_groups)), - ctypes.c_float(float(eps)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_group_norm failed") - return self._tensor_from_node(out.value) - - def group_norm(self, x, weight, bias, num_groups, eps=1e-5): - return self.groupnorm(x, weight, bias, num_groups, eps=eps) - - def layernorm(self, x, weight, bias=None, eps=1e-5): - x = self._ensure_tensor(x) - weight = self._ensure_tensor(weight) - has_bias = bias is not None - bias_node = cactus_node_t(0) - if has_bias: - bias = self._ensure_tensor(bias) - bias_node = cactus_node_t(bias.id) - out = cactus_node_t() - rc = _lib.cactus_graph_layernorm( - self.h, - cactus_node_t(x.id), - cactus_node_t(weight.id), - bias_node, - ctypes.c_float(float(eps)), - ctypes.c_bool(has_bias), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_layer_norm failed") - return self._tensor_from_node(out.value) - - def layer_norm(self, x, weight, bias=None, eps=1e-5): - return self.layernorm(x, weight, bias=bias, eps=eps) - - def batchnorm(self, x, weight, bias, running_mean, running_var, axis=1, eps=1e-5): - x = self._ensure_tensor(x) - weight = self._ensure_tensor(weight) - bias = self._ensure_tensor(bias) - running_mean = self._ensure_tensor(running_mean) - running_var = self._ensure_tensor(running_var) - out = cactus_node_t() - rc = _lib.cactus_graph_batchnorm( - self.h, - cactus_node_t(x.id), - cactus_node_t(weight.id), - cactus_node_t(bias.id), - cactus_node_t(running_mean.id), - cactus_node_t(running_var.id), - ctypes.c_int32(int(axis)), - ctypes.c_float(float(eps)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_batchnorm failed") - return self._tensor_from_node(out.value) - - def batch_norm(self, x, weight, bias, running_mean, running_var, axis=1, eps=1e-5): - return self.batchnorm(x, weight, bias, running_mean, running_var, axis=axis, eps=eps) - - def rms_norm(self, x, weight, eps=1e-5): - x = self._ensure_tensor(x) - weight = self._ensure_tensor(weight) - out = cactus_node_t() - rc = _lib.cactus_graph_rms_norm( - self.h, - cactus_node_t(x.id), - cactus_node_t(weight.id), - ctypes.c_float(float(eps)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_rms_norm failed") - return self._tensor_from_node(out.value) - - def topk(self, x, k): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_topk(self.h, cactus_node_t(x.id), ctypes.c_size_t(int(k)), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_topk failed") - return self._tensor_from_node(out.value) - - def rope(self, x, theta, position_offset=0, backend=CPU): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_rope( - self.h, cactus_node_t(x.id), ctypes.c_float(float(theta)), ctypes.c_size_t(int(position_offset)), - ctypes.c_int32(int(backend)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_rope failed") - return self._tensor_from_node(out.value) - - def rope_gptj(self, x, theta, position_offset=0, rot_dim=0, backend=CPU): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_rope_gptj( - self.h, cactus_node_t(x.id), ctypes.c_float(float(theta)), ctypes.c_size_t(int(position_offset)), - ctypes.c_size_t(int(rot_dim)), ctypes.c_int32(int(backend)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_rope_gptj failed") - return self._tensor_from_node(out.value) - - def _reduce(self, fn_name, x, axis): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = getattr(_lib, fn_name)(self.h, cactus_node_t(x.id), ctypes.c_int32(int(axis)), ctypes.byref(out)) - if rc != 0: - raise RuntimeError(f"{fn_name} failed") - return self._tensor_from_node(out.value) - - def sum(self, x, axis): - return self._reduce("cactus_graph_sum", x, axis) - - def mean(self, x, axis): - return self._reduce("cactus_graph_mean", x, axis) - - def variance(self, x, axis): - return self._reduce("cactus_graph_variance", x, axis) - - def min(self, x, axis): - return self._reduce("cactus_graph_min", x, axis) - - def max(self, x, axis): - return self._reduce("cactus_graph_max", x, axis) - - def softmax(self, x, axis=-1): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_softmax( - self.h, - cactus_node_t(x.id), - ctypes.c_int32(int(axis)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_softmax failed") - return self._tensor_from_node(out.value) - - def attention(self, query, key, value, scale, is_causal=True, position_offset=0, window_size=0, - backend=CPU, mask=None, additive_mask=False): - query = self._ensure_tensor(query) - key = self._ensure_tensor(key) - value = self._ensure_tensor(value) - mask_node = cactus_node_t(0) - use_mask = mask is not None - if use_mask: - mask_node = cactus_node_t(self._ensure_tensor(mask).id) - out = cactus_node_t() - rc = _lib.cactus_graph_attention( - self.h, - cactus_node_t(query.id), - cactus_node_t(key.id), - cactus_node_t(value.id), - ctypes.c_float(float(scale)), - ctypes.c_bool(bool(is_causal)), - ctypes.c_size_t(int(position_offset)), - ctypes.c_size_t(int(window_size)), - ctypes.c_int32(int(backend)), - ctypes.c_bool(use_mask), - mask_node, - ctypes.c_bool(bool(additive_mask)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_attention failed") - return self._tensor_from_node(out.value) - - def rel_pos_bias(self, query, relative_key, scale): - query = self._ensure_tensor(query) - relative_key = self._ensure_tensor(relative_key) - out = cactus_node_t() - rc = _lib.cactus_graph_rel_pos_bias( - self.h, cactus_node_t(query.id), cactus_node_t(relative_key.id), ctypes.c_float(float(scale)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_rel_pos_bias failed") - return self._tensor_from_node(out.value) - - def attention_int8_hybrid(self, query, key_new, value_new, scale, position_offset, - cached_keys, cached_values, k_scales, v_scales, - cache_len, num_kv_heads, head_dim, window_size=0): - query = self._ensure_tensor(query) - key_new = self._ensure_tensor(key_new) - value_new = self._ensure_tensor(value_new) - ck = np.ascontiguousarray(cached_keys, dtype=np.int8) - cv = np.ascontiguousarray(cached_values, dtype=np.int8) - ks = np.ascontiguousarray(k_scales, dtype=np.float32) - vs = np.ascontiguousarray(v_scales, dtype=np.float32) - out = cactus_node_t() - rc = _lib.cactus_graph_attention_int8_hybrid( - self.h, - cactus_node_t(query.id), - cactus_node_t(key_new.id), - cactus_node_t(value_new.id), - ctypes.c_float(float(scale)), - ctypes.c_size_t(int(position_offset)), - ck.ctypes.data_as(ctypes.POINTER(ctypes.c_int8)), - cv.ctypes.data_as(ctypes.POINTER(ctypes.c_int8)), - ks.ctypes.data_as(ctypes.POINTER(ctypes.c_float)), - vs.ctypes.data_as(ctypes.POINTER(ctypes.c_float)), - ctypes.c_size_t(int(cache_len)), - ctypes.c_size_t(int(num_kv_heads)), - ctypes.c_size_t(int(head_dim)), - ctypes.c_size_t(int(window_size)), - ctypes.byref(out), - ) - if rc != 0: - raise RuntimeError("graph_attention_int8_hybrid failed") - return self._tensor_from_node(out.value) - - def relu(self, x): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_relu(self.h, cactus_node_t(x.id), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_relu failed") - return self._tensor_from_node(out.value) - - def silu(self, x): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_silu(self.h, cactus_node_t(x.id), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_silu failed") - return self._tensor_from_node(out.value) - - def gelu(self, x): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_gelu(self.h, cactus_node_t(x.id), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_gelu failed") - return self._tensor_from_node(out.value) - - def gelu_erf(self, x): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_gelu_erf(self.h, cactus_node_t(x.id), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_gelu_erf failed") - return self._tensor_from_node(out.value) - - def sigmoid(self, x): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_sigmoid(self.h, cactus_node_t(x.id), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_sigmoid failed") - return self._tensor_from_node(out.value) - - def tanh(self, x): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_tanh(self.h, cactus_node_t(x.id), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_tanh failed") - return self._tensor_from_node(out.value) - - def glu(self, x, axis=-1): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_glu(self.h, cactus_node_t(x.id), ctypes.c_int32(int(axis)), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_glu failed") - return self._tensor_from_node(out.value) - - def conv1d_causal(self, x, weight, kernel_size, dilation): - x = self._ensure_tensor(x) - weight = self._ensure_tensor(weight) - out = cactus_node_t() - rc = _lib.cactus_graph_conv1d_causal( - self.h, cactus_node_t(x.id), cactus_node_t(weight.id), - ctypes.c_size_t(int(kernel_size)), ctypes.c_size_t(int(dilation)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_conv1d_causal failed") - return self._tensor_from_node(out.value) - - def conv1d_k3(self, x, weight, stride=1): - x = self._ensure_tensor(x) - weight = self._ensure_tensor(weight) - out = cactus_node_t() - rc = _lib.cactus_graph_conv1d_k3( - self.h, cactus_node_t(x.id), cactus_node_t(weight.id), ctypes.c_size_t(int(stride)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_conv1d_k3 failed") - return self._tensor_from_node(out.value) - - def conv1d_k7s3(self, x, weight, bias): - x = self._ensure_tensor(x) - weight = self._ensure_tensor(weight) - bias = self._ensure_tensor(bias) - out = cactus_node_t() - rc = _lib.cactus_graph_conv1d_k7s3( - self.h, cactus_node_t(x.id), cactus_node_t(weight.id), cactus_node_t(bias.id), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_conv1d_k7s3 failed") - return self._tensor_from_node(out.value) - - def conv1d(self, x, weight, bias=None, stride=1): - return self._conv_with_optional_bias("cactus_graph_conv1d", x, weight, bias, ctypes.c_size_t(int(stride))) - - def conv1d_same_depthwise_k9(self, x, weight, bias=None): - return self._conv_with_optional_bias("cactus_graph_conv1d_same_depthwise_k9", x, weight, bias) - - def conv1d_pointwise(self, x, weight, bias=None): - return self._conv_with_optional_bias("cactus_graph_conv1d_pointwise", x, weight, bias) - - def conv2d_k3s2p1(self, x, weight, bias=None): - return self._conv_with_optional_bias("cactus_graph_conv2d_k3s2p1", x, weight, bias) - - def conv2d_depthwise_k3s2p1(self, x, weight, bias=None): - return self._conv_with_optional_bias("cactus_graph_conv2d_depthwise_k3s2p1", x, weight, bias) - - def conv2d_pointwise_1x1(self, x, weight, bias=None): - return self._conv_with_optional_bias("cactus_graph_conv2d_pointwise_1x1", x, weight, bias) - - def _conv_with_optional_bias(self, fn_name, x, weight, bias=None, *extra): - x = self._ensure_tensor(x) - weight = self._ensure_tensor(weight) - has_bias = bias is not None - bias_node = cactus_node_t(0 if bias is None else self._ensure_tensor(bias).id) - out = cactus_node_t() - rc = getattr(_lib, fn_name)( - self.h, cactus_node_t(x.id), cactus_node_t(weight.id), ctypes.c_bool(has_bias), bias_node, *extra, ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError(f"{fn_name} failed") - return self._tensor_from_node(out.value) - - def lstm_cell(self, input, h_prev, c_prev, weight_ih, weight_hh, bias_ih, bias_hh): - tensors = [self._ensure_tensor(t) for t in (input, h_prev, c_prev, weight_ih, weight_hh, bias_ih, bias_hh)] - out = cactus_node_t() - rc = _lib.cactus_graph_lstm_cell(self.h, *(cactus_node_t(t.id) for t in tensors), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_lstm_cell failed") - return self._tensor_from_node(out.value) - - def gated_deltanet_decode(self, query, key, value, gate_log, beta, initial_state, scale): - tensors = [self._ensure_tensor(t) for t in (query, key, value, gate_log, beta, initial_state)] - out = cactus_node_t() - rc = _lib.cactus_graph_gated_deltanet_decode( - self.h, *(cactus_node_t(t.id) for t in tensors), ctypes.c_float(float(scale)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_gated_deltanet_decode failed") - return self._tensor_from_node(out.value) - - def gated_deltanet_prefill(self, query, key, value, gate_log, beta, initial_state, chunk_size, scale): - tensors = [self._ensure_tensor(t) for t in (query, key, value, gate_log, beta, initial_state)] - out = cactus_node_t() - rc = _lib.cactus_graph_gated_deltanet_prefill( - self.h, *(cactus_node_t(t.id) for t in tensors), ctypes.c_size_t(int(chunk_size)), ctypes.c_float(float(scale)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_gated_deltanet_prefill failed") - return self._tensor_from_node(out.value) - - def stft(self, x, weight, stride, num_fft_bins): - x = self._ensure_tensor(x) - weight = self._ensure_tensor(weight) - out = cactus_node_t() - rc = _lib.cactus_graph_stft( - self.h, cactus_node_t(x.id), cactus_node_t(weight.id), - ctypes.c_size_t(int(stride)), ctypes.c_size_t(int(num_fft_bins)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_stft failed") - return self._tensor_from_node(out.value) - - def altup_predict(self, coefs, streams): - coefs = self._ensure_tensor(coefs) - streams = [self._ensure_tensor(t) for t in streams] - ids = (cactus_node_t * len(streams))(*(cactus_node_t(t.id) for t in streams)) - out = cactus_node_t() - rc = _lib.cactus_graph_altup_predict(self.h, cactus_node_t(coefs.id), ids, ctypes.c_size_t(len(streams)), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_altup_predict failed") - return self._tensor_from_node(out.value) - - def altup_correct(self, coefs, innovation, predictions): - coefs = self._ensure_tensor(coefs) - innovation = self._ensure_tensor(innovation) - predictions = [self._ensure_tensor(t) for t in predictions] - ids = (cactus_node_t * len(predictions))(*(cactus_node_t(t.id) for t in predictions)) - out = cactus_node_t() - rc = _lib.cactus_graph_altup_correct( - self.h, cactus_node_t(coefs.id), cactus_node_t(innovation.id), ids, ctypes.c_size_t(len(predictions)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_altup_correct failed") - return self._tensor_from_node(out.value) - - def gaussian_topk(self, x, ppf): - x = self._ensure_tensor(x) - out = cactus_node_t() - rc = _lib.cactus_graph_gaussian_topk(self.h, cactus_node_t(x.id), ctypes.c_float(float(ppf)), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_gaussian_topk failed") - return self._tensor_from_node(out.value) - - def moe_layer_gated(self, hidden, routing_probs, topk_indices, w1_weights, w3_weights, w2_weights, - num_experts, num_experts_per_tok, normalize_routing=True, epsilon=1e-6, routed_scaling_factor=1.0): - hidden = self._ensure_tensor(hidden) - routing_probs = self._ensure_tensor(routing_probs) - topk_indices = self._ensure_tensor(topk_indices) - w1 = (cactus_node_t * len(w1_weights))(*(cactus_node_t(self._ensure_tensor(t).id) for t in w1_weights)) - w3 = (cactus_node_t * len(w3_weights))(*(cactus_node_t(self._ensure_tensor(t).id) for t in w3_weights)) - w2 = (cactus_node_t * len(w2_weights))(*(cactus_node_t(self._ensure_tensor(t).id) for t in w2_weights)) - out = cactus_node_t() - rc = _lib.cactus_graph_moe_layer_gated( - self.h, cactus_node_t(hidden.id), cactus_node_t(routing_probs.id), cactus_node_t(topk_indices.id), - w1, w3, w2, ctypes.c_size_t(int(num_experts)), ctypes.c_size_t(int(num_experts_per_tok)), - ctypes.c_bool(bool(normalize_routing)), ctypes.c_float(float(epsilon)), - ctypes.c_float(float(routed_scaling_factor)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_moe_layer_gated failed") - return self._tensor_from_node(out.value) - - def moe_layer_ungated(self, hidden, routing_probs, topk_indices, w1_weights, w2_weights, - num_experts, num_experts_per_tok, normalize_routing=True, epsilon=1e-6, - routed_scaling_factor=1.0, activation=ACT_GELU): - hidden = self._ensure_tensor(hidden) - routing_probs = self._ensure_tensor(routing_probs) - topk_indices = self._ensure_tensor(topk_indices) - w1 = (cactus_node_t * len(w1_weights))(*(cactus_node_t(self._ensure_tensor(t).id) for t in w1_weights)) - w2 = (cactus_node_t * len(w2_weights))(*(cactus_node_t(self._ensure_tensor(t).id) for t in w2_weights)) - out = cactus_node_t() - rc = _lib.cactus_graph_moe_layer_ungated( - self.h, cactus_node_t(hidden.id), cactus_node_t(routing_probs.id), cactus_node_t(topk_indices.id), - w1, w2, ctypes.c_size_t(int(num_experts)), ctypes.c_size_t(int(num_experts_per_tok)), - ctypes.c_bool(bool(normalize_routing)), ctypes.c_float(float(epsilon)), - ctypes.c_float(float(routed_scaling_factor)), ctypes.c_int32(int(activation)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_moe_layer_ungated failed") - return self._tensor_from_node(out.value) - - def sample(self, logits, temperature=1.0, top_p=1.0, top_k=0): - logits = self._ensure_tensor(logits) - out = cactus_node_t() - rc = _lib.cactus_graph_sample( - self.h, cactus_node_t(logits.id), ctypes.c_float(float(temperature)), - ctypes.c_float(float(top_p)), ctypes.c_size_t(int(top_k)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_sample failed") - return self._tensor_from_node(out.value) - - def scatter_topk(self, indices, values, num_classes): - indices = self._ensure_tensor(indices) - values = self._ensure_tensor(values) - out = cactus_node_t() - rc = _lib.cactus_graph_scatter_topk( - self.h, cactus_node_t(indices.id), cactus_node_t(values.id), ctypes.c_size_t(int(num_classes)), ctypes.byref(out) - ) - if rc != 0: - raise RuntimeError("graph_scatter_topk failed") - return self._tensor_from_node(out.value) - - def persistent(self, source_node): - source_node = self._ensure_tensor(source_node) - out = cactus_node_t() - rc = _lib.cactus_graph_persistent(self.h, cactus_node_t(source_node.id), ctypes.byref(out)) - if rc != 0: - raise RuntimeError("graph_persistent failed") - return self._tensor_from_node(out.value) - - def is_populated(self, persistent_node): - persistent_node = self._ensure_tensor(persistent_node) - out_is_populated = ctypes.c_int32() - rc = _lib.cactus_graph_is_populated(self.h, cactus_node_t(persistent_node.id), ctypes.byref(out_is_populated)) - if rc != 0: - raise RuntimeError("graph_is_populated failed") - return bool(out_is_populated.value) - - def invalidate_persistent(self, persistent_node): - persistent_node = self._ensure_tensor(persistent_node) - rc = _lib.cactus_graph_invalidate_persistent(self.h, cactus_node_t(persistent_node.id)) - if rc != 0: - raise RuntimeError("graph_invalidate_persistent failed") - - def output_info(self, x): - x = self._ensure_tensor(x) - return self._get_output_info(x.id) - - def _binary(self, fn_name, a, b): - a = self._ensure_tensor(a) - b = self._ensure_tensor(b) - out = cactus_node_t() - rc = getattr(_lib, fn_name)(self.h, cactus_node_t(a.id), cactus_node_t(b.id), ctypes.byref(out)) - if rc != 0: - raise RuntimeError(f"{fn_name} failed") - return self._tensor_from_node(out.value) - - def _ensure_tensor(self, x): - if not isinstance(x, Tensor): - raise TypeError("expected Tensor") - if x.g is not self: - raise ValueError("tensor belongs to a different graph") - return x - - def _get_output_info(self, node_id): - info = cactus_tensor_info_t() - rc = _lib.cactus_graph_get_output_info(self.h, cactus_node_t(node_id), ctypes.byref(info)) - if rc != 0: - raise RuntimeError("graph_get_output_info failed") - shape = tuple(int(info.shape[i]) for i in range(int(info.rank))) - return { - "precision": int(info.precision), - "rank": int(info.rank), - "shape": shape, - "num_elements": int(info.num_elements), - "byte_size": int(info.byte_size), - } - - def _tensor_from_node(self, node_id): - meta = self._get_output_info(node_id) - return Tensor(self, int(node_id), meta["shape"], meta["precision"]) - - def _coerce_input_array(self, data, precision): - if isinstance(data, Tensor): - arr = data.numpy() - else: - arr = np.asarray(data) - if precision == self.INT8: - arr = np.ascontiguousarray(arr, dtype=np.int8) - elif precision == self.FP16: - arr = np.ascontiguousarray(arr, dtype=np.float16) - elif precision == self.FP32: - arr = np.ascontiguousarray(arr, dtype=np.float32) - elif precision == self.INT4: - arr = np.ascontiguousarray(arr, dtype=np.uint8) - else: - raise RuntimeError("unsupported precision") - return arr - - -class Tensor: - def __init__(self, g, node_id, shape, dtype): - self.g = g - self.id = int(node_id) - self.shape = tuple(shape) - self.dtype = int(dtype) - - def __add__(self, other): - return self.g.add(self, other) - - def __sub__(self, other): - return self.g.subtract(self, other) - - def __mul__(self, other): - return self.g.multiply(self, other) - - def __truediv__(self, other): - return self.g.divide(self, other) - - def abs(self): - return self.g.abs(self) - - def pow(self, exponent): - return self.g.pow(self, exponent) - - def precision_cast(self, dtype): - return self.g.precision_cast(self, dtype) - - def quantize_activations(self): - return self.g.quantize_activations(self) - - def scalar_add(self, value): - return self.g.scalar_add(self, value) - - def scalar_subtract(self, value): - return self.g.scalar_subtract(self, value) - - def scalar_multiply(self, value): - return self.g.scalar_multiply(self, value) - - def scalar_divide(self, value): - return self.g.scalar_divide(self, value) - - def scalar_exp(self): - return self.g.scalar_exp(self) - - def scalar_sqrt(self): - return self.g.scalar_sqrt(self) - - def scalar_cos(self): - return self.g.scalar_cos(self) - - def scalar_sin(self): - return self.g.scalar_sin(self) - - def scalar_log(self): - return self.g.scalar_log(self) - - def relu(self): - return self.g.relu(self) - - def sigmoid(self): - return self.g.sigmoid(self) - - def tanh(self): - return self.g.tanh(self) - - def gelu(self): - return self.g.gelu(self) - - def gelu_erf(self): - return self.g.gelu_erf(self) - - def silu(self): - return self.g.silu(self) - - def view(self, shape): - return self.g.view(self, shape) - - def reshape(self, shape): - return self.g.reshape(self, shape) - - def flatten(self, start_dim=0, end_dim=-1): - return self.g.flatten(self, start_dim=start_dim, end_dim=end_dim) - - def slice(self, axis, start, length=0): - return self.g.slice(self, axis, start, length=length) - - def index(self, index_value, axis=0): - return self.g.index(self, index_value, axis=axis) - - def transpose(self, backend=Graph.CPU): - return self.g.transpose(self, backend=backend) - - def permute(self, permutation, backend=Graph.CPU): - return self.g.permute(self, permutation, backend=backend) - - def concat(self, other, axis=0): - return self.g.concat(self, other, axis=axis) - - def cat(self, tensors, axis=0): - return self.g.cat([self] + tensors, axis=axis) - - def groupnorm(self, weight, bias, num_groups, eps=1e-5): - return self.g.groupnorm(self, weight, bias, num_groups, eps=eps) - - def layernorm(self, weight, bias=None, eps=1e-5): - return self.g.layernorm(self, weight, bias=bias, eps=eps) - - def batchnorm(self, weight, bias, running_mean, running_var, axis=1, eps=1e-5): - return self.g.batchnorm(self, weight, bias, running_mean, running_var, axis=axis, eps=eps) - - def group_norm(self, weight, bias, num_groups, eps=1e-5): - return self.groupnorm(weight, bias, num_groups, eps=eps) - - def layer_norm(self, weight, bias=None, eps=1e-5): - return self.layernorm(weight, bias=bias, eps=eps) - - def batch_norm(self, weight, bias, running_mean, running_var, axis=1, eps=1e-5): - return self.batchnorm(weight, bias, running_mean, running_var, axis=axis, eps=eps) - - def rms_norm(self, weight, eps=1e-5): - return self.g.rms_norm(self, weight, eps=eps) - - def softmax(self, axis=-1): - return self.g.softmax(self, axis) - - def glu(self, axis=-1): - return self.g.glu(self, axis=axis) - - def matmul(self, other, pretransposed_rhs=False, backend=Graph.CPU): - return self.g.matmul(self, other, pretransposed_rhs=pretransposed_rhs, backend=backend) - - def sum(self, axis): - return self.g.sum(self, axis) - - def mean(self, axis): - return self.g.mean(self, axis) - - def variance(self, axis): - return self.g.variance(self, axis) - - def min(self, axis): - return self.g.min(self, axis) - - def max(self, axis): - return self.g.max(self, axis) - - def numpy(self): - info = cactus_tensor_info_t() - rc = _lib.cactus_graph_get_output_info(self.g.h, cactus_node_t(self.id), ctypes.byref(info)) - if rc != 0: - raise RuntimeError("graph_get_output_info failed") - - out_ptr = ctypes.c_void_p() - rc = _lib.cactus_graph_get_output_ptr(self.g.h, cactus_node_t(self.id), ctypes.byref(out_ptr)) - if rc != 0 or not out_ptr.value: - raise RuntimeError("graph_get_output_ptr failed") - - rank = int(info.rank) - shape = tuple(int(info.shape[i]) for i in range(rank)) - num_elements = int(info.num_elements) - precision = int(info.precision) - - if precision == Graph.FP16: - arr = np.ctypeslib.as_array((ctypes.c_uint16 * num_elements).from_address(out_ptr.value)).view(np.float16) - elif precision == Graph.FP32: - arr = np.ctypeslib.as_array((ctypes.c_float * num_elements).from_address(out_ptr.value)) - elif precision == Graph.INT8: - arr = np.ctypeslib.as_array((ctypes.c_int8 * num_elements).from_address(out_ptr.value)) - elif precision == Graph.INT4: - arr = np.ctypeslib.as_array((ctypes.c_uint8 * int(info.byte_size)).from_address(out_ptr.value)) - return arr.copy() - else: - raise RuntimeError("unsupported precision") - - return arr.reshape(shape).copy() - - def __repr__(self): - return f"Tensor(id={self.id}, shape={self.shape}, dtype={self.dtype})" diff --git a/python/src/publish_to_hf.py b/python/src/publish_to_hf.py deleted file mode 100644 index 8c84144dd..000000000 --- a/python/src/publish_to_hf.py +++ /dev/null @@ -1,305 +0,0 @@ -import argparse -import hashlib -import json -import os -import shutil -import subprocess -import sys -from pathlib import Path -import yaml -from huggingface_hub import HfApi, hf_hub_download -from huggingface_hub.constants import HF_HUB_CACHE -from .cli import cmd_convert, get_weights_dir, PROJECT_ROOT - -STAGE_DIR = PROJECT_ROOT / "stage" - -FALLBACK_LICENSES = { - "snakers4/silero-vad": "mit", -} - - -def sha256(file): - h = hashlib.sha256() - with open(file, "rb") as f: - for block in iter(lambda: f.read(1024 * 1024), b""): - h.update(block) - return h.hexdigest() - - -def zip_dir(source_dir, output_path): - subprocess.run( - ["find", ".", "-exec", "touch", "-t", "200310131122", "{}", "+"], - cwd=source_dir, - check=True, - ) - subprocess.run( - ["zip", "-X", "-o", "-r", "-9", str(output_path), "."], - cwd=source_dir, - check=True, - capture_output=True, - ) - - -def get_model_name(model_id): - return model_id.split("/")[-1] - - -def export_model(model_id, token, precision): - args = argparse.Namespace( - model_name=model_id, output_dir=None, precision=precision, token=token - ) - if cmd_convert(args) != 0: - return None - return get_weights_dir(model_id) - - -def export_pro_weights(model_id, bits): - pro_repo = PROJECT_ROOT / "cactus-pro" - if not pro_repo.exists(): - return None - build_script = pro_repo / "apple" / "build.sh" - if not build_script.exists(): - return None - - if "gemma-4" in model_id.lower(): - build_dir = pro_repo / "apple" / "build" - output_names = { - "gemma4-vision": "vision_encoder", - "gemma4-audio": "audio_encoder" - } - mlpackages = [] - for enc_type, out_name in output_names.items(): - result = subprocess.run( - ["bash", str(build_script), "--model", model_id, "--bits", bits, "--type", enc_type], - cwd=pro_repo, - capture_output=True, - ) - mlpackage = build_dir / "model.mlpackage" - if result.returncode == 0 and mlpackage.exists(): - dest = build_dir.parent / f"{out_name}.mlpackage" - if dest.exists(): - shutil.rmtree(dest) - shutil.move(str(mlpackage), str(dest)) - mlpackages.append(dest) - return mlpackages or None - - result = subprocess.run( - ["bash", str(build_script), "--model", model_id, "--bits", bits], - cwd=pro_repo, - capture_output=True, - ) - if result.returncode != 0: - return None - mlpackage = pro_repo / "apple" / "build" / "model.mlpackage" - return [mlpackage] if mlpackage.exists() else None - - -def get_prev_config(api, repo, current): - try: - tags = api.list_repo_refs(repo_id=repo, repo_type="model").tags - versions = sorted( - [t.name for t in tags], - key=lambda v: tuple(int(x) for x in v.lstrip("v").split(".")), - reverse=True, - ) - prev_ver = next((v for v in versions if v != current), None) - if not prev_ver: - return None - local = hf_hub_download( - repo_id=repo, - filename="config.json", - revision=prev_ver, - repo_type="model", - ) - with open(local) as f: - return json.load(f) - except Exception: - return None - - -def changed(curr, prev): - if not prev: - return True - return curr.get("fingerprint") != prev.get("fingerprint") - - -def update_org_readme(api, org): - readme = PROJECT_ROOT / "README.md" - if not readme.exists(): - print("README.md not found") - return 1 - - try: - api.create_repo(repo_id=f"{org}/README", repo_type="space", space_sdk="static", exist_ok=True) - frontmatter_data = {"title": org, "sdk": "static", "pinned": True} - frontmatter = "---\n" + yaml.safe_dump(frontmatter_data, sort_keys=False) + "---\n\n" - content = (frontmatter + readme.read_text()).encode() - api.upload_file( - path_or_fileobj=content, - path_in_repo="README.md", - repo_id=f"{org}/README", - repo_type="space", - commit_message="Update organization README", - ) - print("Updated organization README") - return 0 - except Exception: - print("Failed to update organization README") - return 1 - - -def export_and_publish_model(args, api): - model_name = get_model_name(args.model) - model_name_lower = model_name.lower() - repo_id = f"{args.org}/{model_name}" - - precisions = [] - if args.int4: - precisions.append(("int4", "4")) - if args.int8: - precisions.append(("int8", "8")) - if args.fp16: - precisions.append(("fp16", "16")) - - stage = STAGE_DIR / model_name - if stage.exists(): - shutil.rmtree(stage) - stage.mkdir(parents=True) - weights_dir = stage / "weights" - weights_dir.mkdir() - - try: - fingerprint = hashlib.sha256() - precisions_list = [] - - for precision, bits in precisions: - print(f"Exporting {args.model} with {precision}...") - - exported = export_model(args.model, os.environ.get("HF_TOKEN"), precision.upper()) - if not exported: - print(f"Failed to export {precision}") - continue - - base_zip = weights_dir / f"{model_name_lower}-{precision}.zip" - zip_dir(exported, base_zip) - fingerprint.update(sha256(base_zip).encode()) - - if args.apple: - try: - mlpackages = export_pro_weights(args.model, bits) - if mlpackages: - for mlp in mlpackages: - shutil.copytree(str(mlp), str(exported / mlp.name)) - apple_zip = weights_dir / f"{model_name_lower}-{precision}-apple.zip" - zip_dir(exported, apple_zip) - fingerprint.update(sha256(apple_zip).encode()) - except Exception: - print(f"Failed to export Apple weights for {precision}") - - shutil.rmtree(exported) - precisions_list.append(precision) - - config = {"model_type": model_name, "precisions": precisions_list, "fingerprint": fingerprint.hexdigest()} - with open(stage / "config.json", "w") as f: - json.dump(config, f, indent=2) - - api.create_repo(repo_id=repo_id, repo_type="model", exist_ok=True) - - try: - info = api.model_info(args.model) - source_license = (getattr(info.card_data, "license", None) if info.card_data is not None else None) or FALLBACK_LICENSES.get(args.model) - except Exception: - source_license = FALLBACK_LICENSES.get(args.model) - - meta = {"base_model": args.model} - if args.pipeline_tag: - meta["pipeline_tag"] = args.pipeline_tag - if args.tags: - meta["tags"] = [tag.strip() for tag in args.tags.split(",") if tag.strip()] - if source_license: - meta["license"] = source_license - if args.description: - meta["description"] = args.description - readme = f"---\n{yaml.safe_dump(meta, default_flow_style=False, allow_unicode=True).strip()}\n---\n" - try: - api.upload_file( - path_or_fileobj=readme.encode("utf-8"), - path_in_repo="README.md", - repo_id=repo_id, - repo_type="model", - commit_message="Update model card", - ) - except Exception: - print("Model card update failed") - - if changed(config, get_prev_config(api, repo_id, args.version)): - api.upload_folder( - folder_path=str(stage), - path_in_repo=".", - repo_id=repo_id, - repo_type="model", - commit_message=f"Upload {args.version}", - ) - api.create_tag( - repo_id=repo_id, - tag=args.version, - revision=api.repo_info(repo_id=repo_id, repo_type="model").sha, - repo_type="model", - tag_message=f"Release {args.version}", - exist_ok=True, - ) - print("Uploaded and tagged") - else: - print("Unchanged") - return 0 - - except Exception: - print("Model processing failed") - return 1 - finally: - if stage.exists(): - shutil.rmtree(stage) - hf_model_cache = Path(HF_HUB_CACHE) / ("models--" + args.model.replace("/", "--")) - if hf_model_cache.exists(): - print(f"Cleaning HF cache: {hf_model_cache}") - shutil.rmtree(hf_model_cache) - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("--task", required=True, choices=["export_model", "update_org_readme"]) - parser.add_argument("--version") - parser.add_argument("--org") - parser.add_argument("--model") - parser.add_argument("--int4", action="store_true") - parser.add_argument("--int8", action="store_true") - parser.add_argument("--fp16", action="store_true") - parser.add_argument("--apple", action="store_true") - parser.add_argument("--pipeline-tag", dest="pipeline_tag") - parser.add_argument("--tags", help="Comma-separated list of HuggingFace tags") - parser.add_argument("--description") - args = parser.parse_args() - - token = os.environ.get("HF_TOKEN") - if not token: - print("Error: HF_TOKEN not set") - return 1 - api = HfApi(token=token) - - if args.task == "export_model": - if not all([args.version, args.org, args.model]): - print("Error: export_model requires --version, --org, and --model") - return 1 - if not any([args.int4, args.int8, args.fp16]): - print("Error: At least one precision flag must be set") - return 1 - return export_and_publish_model(args, api) - elif args.task == "update_org_readme": - if not args.org: - print("Error: update_org_readme requires --org") - return 1 - return update_org_readme(api, args.org) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/python/src/tensor_io.py b/python/src/tensor_io.py deleted file mode 100644 index 726903482..000000000 --- a/python/src/tensor_io.py +++ /dev/null @@ -1,531 +0,0 @@ -import numpy as np -import struct -from pathlib import Path -from typing import Optional, Dict, Any, List -from .weight_patterns import EMBED_NAMES - -try: - import torch -except ImportError: - torch = None - - -GROUP_SIZE = 32 -INTERLEAVE_BLOCK = 4 - -CACTUS_MAGIC = b'CACT' -CACTUS_ALIGNMENT = 32 - -FLAG_HAS_SCALES = 1 << 0 -FLAG_PAGE_ALIGNED = 1 << 1 -FLAG_TRANSPOSED = 1 << 2 -FLAG_INTERLEAVED = 1 << 3 - - -def align_offset(offset: int, alignment: int) -> int: - """Round up offset to next alignment boundary.""" - remainder = offset % alignment - if remainder == 0: - return offset - return offset + (alignment - remainder) - - -def compute_padding(current_offset: int, alignment: int) -> bytes: - """Compute padding bytes needed to reach alignment boundary.""" - aligned = align_offset(current_offset, alignment) - padding_size = aligned - current_offset - return b'\x00' * padding_size - - -def interleave_weights(data: np.ndarray, block_size: int = INTERLEAVE_BLOCK) -> tuple[np.ndarray, int]: - """Interleave rows for SIMD-friendly GEMM access using vdotq_laneq_s32. - - Input: data[N, K] - row-major weights - Output: data_interleaved[N_padded/block_size, K/4, block_size, 4] flattened - original_N - the original N before padding - - Memory layout after interleaving (GGML-style): - For each block of 4 rows and each group of 4 K positions: - [row0_k0..k3, row1_k0..k3, row2_k0..k3, row3_k0..k3, row0_k4..k7, ...] - - This enables vdotq_laneq_s32 to broadcast activation lanes efficiently: - - Load 16 bytes of activation: a[k:k+16] (4 groups of 4) - - Load 16 bytes of weights: 4 columns x 4 K values - - Use lane broadcast to compute 4 output columns simultaneously - """ - N, K = data.shape - original_N = N - - if N % block_size != 0: - pad_n = block_size - (N % block_size) - data = np.pad(data, ((0, pad_n), (0, 0)), mode='constant', constant_values=0) - N = data.shape[0] - - if K % 4 != 0: - pad_k = 4 - (K % 4) - data = np.pad(data, ((0, 0), (0, pad_k)), mode='constant', constant_values=0) - K = data.shape[1] - - data = data.reshape(N // block_size, block_size, K // 4, 4) - data = data.transpose(0, 2, 1, 3) - return data.reshape(-1), original_N - - -def interleave_scales(scales: np.ndarray, block_size: int = INTERLEAVE_BLOCK) -> tuple[np.ndarray, int]: - """Interleave scales to match interleaved weight layout. - - Input: scales[N, num_groups] - Output: scales_interleaved[N_padded/block_size, num_groups, block_size] flattened - original_N - """ - N, num_groups = scales.shape - original_N = N - - if N % block_size != 0: - pad_n = block_size - (N % block_size) - scales = np.pad(scales, ((0, pad_n), (0, 0)), mode='constant', constant_values=1e-10) - N = scales.shape[0] - - scales = scales.reshape(N // block_size, block_size, num_groups) - scales = scales.transpose(0, 2, 1) - return scales.reshape(-1), original_N - - -def pack_int4_pairs(data: np.ndarray) -> np.ndarray: - """Pack INT4 values (stored as int8) into bytes using planar layout. - - Input: array of int8 values in range [-8, 7], length must be a multiple of 32 - Output: array of uint8 with half the length - - Packing format (planar, groups of 32): - For each group of 32 values, the first 16 are stored in the low nibbles - and the next 16 in the high nibbles of 16 consecutive bytes. - Nibbles are stored in two's complement form (value & 0x0F). - """ - assert len(data) % 32 == 0, "Data length must be a multiple of 32 for INT4 planar packing" - - groups = data.reshape(-1, 32) - low = (groups[:, :16].astype(np.int8).view(np.uint8) & 0x0F).astype(np.uint8) - high = ((groups[:, 16:].astype(np.int8).view(np.uint8) & 0x0F).astype(np.uint8)) << 4 - - return (low | high).astype(np.uint8).reshape(-1) - - -def fold_bn_into_conv(conv_w, bn_weight, bn_bias, bn_mean, bn_var, eps=1e-5): - gamma = bn_weight.float().numpy() - beta = bn_bias.float().numpy() - mean = bn_mean.float().numpy() - var = bn_var.float().numpy() - w = conv_w.float().numpy() - - inv_std = gamma / np.sqrt(var + eps) - shape = [w.shape[0]] + [1] * (w.ndim - 1) - new_w = w * inv_std.reshape(shape) - new_b = beta - mean * inv_std - - return new_w, new_b - - -def save_tensor_with_header(tensor, output_path, precision='INT8', transpose=False, stats_tracker=None, args=None, model_type=None): - """Save a tensor to binary format with header metadata and group-wise quantization. - - For 2D tensors with INT8/INT4 precision: - - INT4 is packed (2 values per byte) for 50% storage savings, unpacked to INT8 at load time - - Weights are interleaved in blocks of 4 rows for SIMD efficiency - - Layout: [N/4, K/4, 4, 4] enables vdotq_laneq_s32 for efficient GEMV/GEMM - - Args: - tensor: The tensor to save (PyTorch or NumPy) - output_path: Path to save the tensor - precision: Quantization precision ('INT4', 'INT8', 'FP16') - transpose: Whether to transpose 2D tensors - stats_tracker: Optional dict to track quantization statistics - args: Optional args object with additional settings - model_type: Model type string (e.g., 'gemma', 'llama') - """ - if torch is not None and isinstance(tensor, torch.Tensor): - t = tensor.detach().cpu() - if t.dtype == torch.bfloat16: - t = t.float() - data = t.numpy() - else: - data = np.array(tensor) - - if model_type == 'gemma' and 'norm' in str(output_path): - data = data + 1.0 - - if model_type == 'gemma4': - GEMMA4_WEIGHT_SCALE = 16.0 - filename = output_path.name - is_audio_weight = filename.startswith('audio_') - if any(x in filename for x in ['input_norm', 'post_attn_norm', 'pre_ffn_norm', 'post_ffn_norm', - 'post_per_layer_norm', 'post_proj_norm']): - data = data / GEMMA4_WEIGHT_SCALE - elif any(x in filename for x in ['ffn_gate', 'ffn_up', 'per_layer_gate', 'moe_gate_proj', 'moe_up_proj']): - data = data * GEMMA4_WEIGHT_SCALE - elif 'router_scale' in filename: - data = data / GEMMA4_WEIGHT_SCALE - elif filename in ('token_embeddings.weights', 'output_weight.weights', - 'embed_vision_proj.weights', 'embed_vision_embedding.weights'): - data = data / GEMMA4_WEIGHT_SCALE - elif filename == 'output_norm.weights': - data = data * GEMMA4_WEIGHT_SCALE - - if precision in ('INT8', 'INT4'): - filename = output_path.name - if any(x in filename for x in ['norm', 'bias', 'vision', 'position_embeddings', 'embed_positions']): - precision = 'FP16' - elif model_type == 'gemma4' and ( - filename.startswith('audio_') or - filename in ('embed_audio_proj.weights', 'embed_audio_embedding.weights') - ): - precision = 'FP16' - elif precision == 'INT4' and ( - any(x in filename for x in EMBED_NAMES) - or filename in ('token_embeddings.weights', 'embed_tokens_per_layer.weights') - ): - precision = 'INT8' - - shape = list(data.shape) - if transpose and len(shape) == 2: - data = data.T - shape = [shape[1], shape[0]] - - if ( - model_type == 'gemma4' - and len(shape) == 3 - and 'vision' not in output_path.name - and not output_path.name.startswith('audio_') - ): - data = data.transpose(0, 2, 1) - shape = [shape[0], shape[2], shape[1]] - - if precision == 'INT8' and len(shape) == 2: - N, K = shape - original_N = N - - if K % GROUP_SIZE != 0: - pad_k = GROUP_SIZE - (K % GROUP_SIZE) - data = np.pad(data, ((0, 0), (0, pad_k)), mode='constant', constant_values=0) - K = data.shape[1] - - num_groups = K // GROUP_SIZE - - data_grouped = data.reshape(N, num_groups, GROUP_SIZE) - group_abs_max = np.max(np.abs(data_grouped), axis=2) - scales = (group_abs_max / 127.0).astype(np.float32) - scales = np.maximum(scales, 1e-10) - - quantized = np.clip( - np.round(data_grouped / scales[:, :, np.newaxis]), - -128, 127 - ).astype(np.int8) - quantized_2d = quantized.reshape(N, K) - - dequantized = (quantized.astype(np.float32) * scales[:, :, np.newaxis]).reshape(N, K) - mse_error = np.mean((data[:original_N, :] - dequantized[:original_N, :]) ** 2) - snr_db = 10 * np.log10(np.var(data[:original_N, :]) / mse_error) if mse_error > 0 else float('inf') - data_flat = data[:original_N, :].flatten() - dequant_flat = dequantized[:original_N, :].flatten() - cos_sim = np.dot(data_flat, dequant_flat) / (np.linalg.norm(data_flat) * np.linalg.norm(dequant_flat) + 1e-10) - del data_flat, dequantized - - quantized_interleaved, _ = interleave_weights(quantized_2d, INTERLEAVE_BLOCK) - scales_interleaved, _ = interleave_scales(scales, INTERLEAVE_BLOCK) - scales_fp16 = scales_interleaved.astype(np.float16) - - N_padded = ((N + INTERLEAVE_BLOCK - 1) // INTERLEAVE_BLOCK) * INTERLEAVE_BLOCK - - if stats_tracker: - stats_tracker['int8_tensors'] += 1 - stats_tracker['quantized_parameters'] += original_N * K - stats_tracker['mse_values'].append(mse_error) - stats_tracker['snr_values'].append(snr_db) - stats_tracker['cos_sim_values'].append(cos_sim) - stats_tracker['total_tensors'] += 1 - stats_tracker['total_parameters'] += original_N * K - - del data - - with open(output_path, 'wb') as f: - ndim = 2 - data_bytes = quantized_interleaved.size - scales_bytes = scales_fp16.size * 2 - flags = FLAG_HAS_SCALES | FLAG_INTERLEAVED - if transpose: - flags |= FLAG_TRANSPOSED - - f.write(CACTUS_MAGIC) # 4 bytes - f.write(struct.pack(' 0 else float('inf') - data_flat = data[:original_N, :].flatten() - dequant_flat = dequantized[:original_N, :].flatten() - cos_sim = np.dot(data_flat, dequant_flat) / (np.linalg.norm(data_flat) * np.linalg.norm(dequant_flat) + 1e-10) - del data_flat, dequantized - - quantized_interleaved, _ = interleave_weights(quantized_2d, INTERLEAVE_BLOCK) - scales_interleaved, _ = interleave_scales(scales, INTERLEAVE_BLOCK) - scales_fp16 = scales_interleaved.astype(np.float16) - - packed_data = pack_int4_pairs(quantized_interleaved) - - N_padded = ((N + INTERLEAVE_BLOCK - 1) // INTERLEAVE_BLOCK) * INTERLEAVE_BLOCK - - if stats_tracker: - stats_tracker['int4_tensors'] += 1 - stats_tracker['quantized_parameters'] += original_N * K - stats_tracker['mse_values'].append(mse_error) - stats_tracker['snr_values'].append(snr_db) - stats_tracker['cos_sim_values'].append(cos_sim) - stats_tracker['total_tensors'] += 1 - stats_tracker['total_parameters'] += original_N * K - - del data - - with open(output_path, 'wb') as f: - ndim = 2 - data_bytes = packed_data.size - scales_bytes = scales_fp16.size * 2 - flags = FLAG_HAS_SCALES | FLAG_INTERLEAVED - if transpose: - flags |= FLAG_TRANSPOSED - - f.write(CACTUS_MAGIC) # 4 bytes - f.write(struct.pack(' 0 else float('inf') - cos_sim = np.dot(data, dequantized) / (np.linalg.norm(data) * np.linalg.norm(dequantized) + 1e-10) - del dequantized - - scales_fp16 = scales.flatten().astype(np.float16) - - if stats_tracker: - stats_tracker['int8_tensors'] += 1 - stats_tracker['quantized_parameters'] += data.size - stats_tracker['mse_values'].append(mse_error) - stats_tracker['snr_values'].append(snr_db) - stats_tracker['cos_sim_values'].append(cos_sim) - stats_tracker['total_tensors'] += 1 - stats_tracker['total_parameters'] += data.size - - with open(output_path, 'wb') as f: - ndim = len(shape) - data_bytes = quantized_flat.size - scales_bytes = scales_fp16.size * 2 - flags = FLAG_HAS_SCALES - if transpose: - flags |= FLAG_TRANSPOSED - - f.write(CACTUS_MAGIC) - f.write(struct.pack('= 1 else 0)) - - header_size = 84 - f.write(compute_padding(header_size, CACTUS_ALIGNMENT)) - - f.write(data_flat.tobytes()) - - -def format_config_value(value): - """Format a config value for writing to config.txt.""" - if isinstance(value, bool): - return 'true' if value else 'false' - if isinstance(value, (list, tuple)): - return ','.join(str(v) for v in value) - return str(value) - - -def create_quantization_stats(): - """Create an empty stats tracker dictionary for quantization metrics.""" - return { - 'total_tensors': 0, - 'int8_tensors': 0, - 'int4_tensors': 0, - 'fp16_tensors': 0, - 'total_parameters': 0, - 'quantized_parameters': 0, - 'mse_values': [], - 'snr_values': [], - 'cos_sim_values': [], - 'saturation_warnings': 0 - } - - -def print_quantization_summary(quantization_stats, args=None): - """Print a summary of quantization statistics.""" - int8_count = quantization_stats.get('int8_tensors', 0) - int4_count = quantization_stats.get('int4_tensors', 0) - fp16_count = quantization_stats.get('fp16_tensors', 0) - quantized_count = int8_count + int4_count - - if quantized_count > 0: - mse_values = np.array(quantization_stats['mse_values']) - snr_values = np.array(quantization_stats['snr_values']) - cos_sim_values = np.array(quantization_stats['cos_sim_values']) - - print("\nQuantization Summary:") - print(f"MSE - Mean: {np.mean(mse_values):.2e}, Max: {np.max(mse_values):.2e}, Median: {np.median(mse_values):.2e}, Min: {np.min(mse_values):.2e}") - print(f"SNR - Mean: {np.mean(snr_values):.1f}dB, Max: {np.max(snr_values):.1f}dB, Median: {np.median(snr_values):.1f}dB, Min: {np.min(snr_values):.1f}dB") - print(f"CosSim - Mean: {np.mean(cos_sim_values):.6f}, Max: {np.max(cos_sim_values):.6f}, Median: {np.median(cos_sim_values):.6f}, Min: {np.min(cos_sim_values):.6f}") - print(f"Processed {int8_count} INT8 tensors, {int4_count} INT4 tensors, {fp16_count} FP16 tensors") diff --git a/python/src/tokenizer.py b/python/src/tokenizer.py deleted file mode 100644 index 2ec17713b..000000000 --- a/python/src/tokenizer.py +++ /dev/null @@ -1,534 +0,0 @@ -import json -from pathlib import Path - -try: - from huggingface_hub import hf_hub_download -except ImportError: - hf_hub_download = None - -SENTENCEPIECE_MODEL_TYPES = { - 'gemma', 'gemma3n', 'llama', 'smol', 'bert', 't5', -} - -SENTENCEPIECE_MODEL_TYPES = { - 'gemma', 'gemma3n', 'llama', 'gemma4', 'smol', 'bert', 't5', -} - -BPE_MODEL_TYPES = { - 'qwen', 'qwen3_5', 'lfm2', - 'whisper', 'moonshine', - 'parakeet', 'parakeet_tdt', -} - - -def _is_metaspace_normalizer(normalizer): - return ( - isinstance(normalizer, dict) - and normalizer.get("type") == "Replace" - and normalizer.get("pattern", {}).get("String") == " " - and normalizer.get("content") == "▁" - ) - - -def _decoder_has_type(decoder, decoder_type): - if not isinstance(decoder, dict): - return False - if decoder.get("type") == decoder_type: - return True - if decoder.get("type") == "Sequence": - return any(_decoder_has_type(item, decoder_type) for item in decoder.get("decoders", [])) - return False - - -def _is_replace_metaspace_decoder(decoder): - if not isinstance(decoder, dict): - return False - if decoder.get("type") == "Replace": - return ( - decoder.get("pattern", {}).get("String") == "▁" - and decoder.get("content") == " " - ) - if decoder.get("type") == "Sequence": - return any(_is_replace_metaspace_decoder(item) for item in decoder.get("decoders", [])) - return False - - -def convert_hf_tokenizer(tokenizer, output_dir, token=None, model_id=None, labels=None, model_type=None): - """Convert a HuggingFace tokenizer to Cactus format.""" - model_name_l = (model_id or getattr(tokenizer, 'name_or_path', '') or '').lower() - - # Parakeet-TDT exports labels directly in config.json (8192 token classes). - # HF tokenizer object for this repo exposes only a byte-level 256 vocab, - # which breaks runtime decode. Prefer labels when provided. - if labels and isinstance(labels, (list, tuple)) and 'parakeet-tdt' in model_name_l: - id_to_token = [str(tok) for tok in labels] - vocab_size = len(id_to_token) - - vocab_output = output_dir / "vocab.txt" - with open(vocab_output, 'w', encoding='utf-8') as f: - for token_id, token_str in enumerate(id_to_token): - f.write(f"{token_id}\t{token_str}\n") - print(f" Saved Parakeet-TDT vocabulary from labels (ID\\ttoken format, {vocab_size} tokens)") - - merges_output = output_dir / "merges.txt" - with open(merges_output, 'w', encoding='utf-8', newline='') as f: - f.write("#version: 0.2\n") - - token_to_id = {tok: i for i, tok in enumerate(id_to_token)} - unk_id = token_to_id.get('', 0) - pad_id = token_to_id.get('', getattr(tokenizer, 'pad_token_id', 0) or 0) - eos_id = token_to_id.get('<|endoftext|>', getattr(tokenizer, 'eos_token_id', None)) - if eos_id is None: - eos_id = pad_id - bos_id = token_to_id.get('<|startoftranscript|>', None) - - special_token_ids = { - 'pad_token_id': int(pad_id), - 'unk_token_id': int(unk_id), - 'eos_token_id': int(eos_id), - } - if bos_id is not None: - special_token_ids['bos_token_id'] = int(bos_id) - - # Keep special token map dense for known control tags in labels. - special_tokens = {} - for token_id, tok in enumerate(id_to_token): - if tok.startswith('<') and tok.endswith('>'): - special_tokens[token_id] = tok - # Ensure core IDs are present even if token strings differ. - special_tokens.setdefault(int(pad_id), id_to_token[int(pad_id)] if int(pad_id) < vocab_size else "") - special_tokens.setdefault(int(unk_id), id_to_token[int(unk_id)] if int(unk_id) < vocab_size else "") - special_tokens.setdefault(int(eos_id), id_to_token[int(eos_id)] if int(eos_id) < vocab_size else "<|endoftext|>") - - special_tokens_output = output_dir / "special_tokens.json" - with open(special_tokens_output, 'w', encoding='utf-8') as f: - json.dump({ - **special_token_ids, - "vocab_size": vocab_size, - "model_max_length": getattr(tokenizer, 'model_max_length', 131072), - "special_tokens": special_tokens, - "additional_special_tokens": [], - }, f, indent=2, ensure_ascii=False) - - tokenizer_config_output = output_dir / "tokenizer_config.txt" - with open(tokenizer_config_output, 'w') as f: - f.write(f"vocab_size={vocab_size}\n") - for key, value in special_token_ids.items(): - f.write(f"{key}={value}\n") - f.write(f"model_max_length={getattr(tokenizer, 'model_max_length', 131072)}\n") - f.write("tokenizer_type=sentencepiece\n") - f.write("has_chat_template=false\n") - return - - tokenizer_json_data = {} - tokenizer_json_path = output_dir / "tokenizer.json" - try: - tokenizer.save_pretrained(output_dir) - if tokenizer_json_path.exists(): - with open(tokenizer_json_path, 'r', encoding='utf-8') as f: - tokenizer_json_data = json.load(f) - - unused_files = [ - "tokenizer_config.json", - "special_tokens_map.json", - "added_tokens.json", - "chat_template.jinja", - ] - for filename in unused_files: - filepath = output_dir / filename - if filepath.exists(): - filepath.unlink() - except Exception as e: - print(f" Warning: Could not save tokenizer JSON: {e}") - - tokenizer_model = tokenizer_json_data.get("model", {}) if tokenizer_json_data else {} - tokenizer_model_type = str(tokenizer_model.get("type", "")).upper() - is_sentencepiece = tokenizer_model_type != "BPE" and model_type in SENTENCEPIECE_MODEL_TYPES - - if tokenizer_model_type == "BPE" and tokenizer_model.get("vocab"): - vocab = tokenizer_model["vocab"] - vocab_size = max(vocab.values()) + 1 - id_to_token = [""] * vocab_size - for token_str, token_id in vocab.items(): - if token_id < vocab_size: - id_to_token[token_id] = token_str - else: - vocab = tokenizer.get_vocab() - id_to_token = [""] * len(vocab) - for token_str, token_id in vocab.items(): - if token_id < len(id_to_token): - id_to_token[token_id] = token_str - - # vocab.txt is written later, after special tokens are collected - - - merges_output = output_dir / "merges.txt" - - def write_merges_file(merges_list): - with open(merges_output, 'w', encoding='utf-8', newline='') as f: - f.write("#version: 0.2\n") - for merge in merges_list: - f.write(f"{' '.join(merge)}\n") - - merges_written = False - - if not is_sentencepiece and tokenizer_json_data: - merges_from_json = tokenizer_json_data.get("model", {}).get("merges", []) or [] - write_merges_file(merges_from_json) - merges_written = True - - if not merges_written and hf_hub_download: - try: - import shutil - merges_file = hf_hub_download(repo_id=tokenizer.name_or_path, filename="merges.txt", token=token) - shutil.copy2(merges_file, merges_output) - merges_written = True - except Exception: - pass - - if not merges_written and hasattr(tokenizer, 'backend_tokenizer') and tokenizer.backend_tokenizer: - backend = tokenizer.backend_tokenizer - merges = [] - - if hasattr(backend, 'model'): - model = backend.model - if hasattr(model, 'merges'): - merges = model.merges - - write_merges_file(merges) - merges_written = True - - if not merges_written: - write_merges_file([]) - - - special_tokens = {} - special_token_ids = {} - - if hasattr(tokenizer, 'eos_token_id') and tokenizer.eos_token_id is not None: - special_token_ids['eos_token_id'] = tokenizer.eos_token_id - special_tokens[tokenizer.eos_token_id] = tokenizer.eos_token or "<|endoftext|>" - - if hasattr(tokenizer, 'pad_token_id') and tokenizer.pad_token_id is not None: - special_token_ids['pad_token_id'] = tokenizer.pad_token_id - special_tokens[tokenizer.pad_token_id] = tokenizer.pad_token or "<|endoftext|>" - - if hasattr(tokenizer, 'bos_token_id') and tokenizer.bos_token_id is not None: - special_token_ids['bos_token_id'] = tokenizer.bos_token_id - special_tokens[tokenizer.bos_token_id] = tokenizer.bos_token or "<|startoftext|>" - - if hasattr(tokenizer, 'unk_token_id') and tokenizer.unk_token_id is not None: - special_token_ids['unk_token_id'] = tokenizer.unk_token_id - special_tokens[tokenizer.unk_token_id] = tokenizer.unk_token or "<|unknown|>" - - core_token_fallbacks = { - 'pad_token_id': '', - 'eos_token_id': '', - 'bos_token_id': '', - 'unk_token_id': '', - } - vocab_lookup = {token: token_id for token_id, token in enumerate(id_to_token) if token} - for key, token_str in core_token_fallbacks.items(): - if key not in special_token_ids and token_str in vocab_lookup: - token_id = vocab_lookup[token_str] - special_token_ids[key] = token_id - special_tokens[token_id] = token_str - - if 'eos_token_id' not in special_token_ids and 'parakeet' in model_name_l: - pad_id = special_token_ids.get('pad_token_id') - if pad_id is not None: - special_token_ids['eos_token_id'] = pad_id - - additional_special_tokens = [] - if hasattr(tokenizer, 'additional_special_tokens'): - for token_str in tokenizer.additional_special_tokens or []: - token_id = tokenizer.convert_tokens_to_ids(token_str) - if token_id != tokenizer.unk_token_id: - special_tokens[token_id] = token_str - additional_special_tokens.append({"token": token_str, "id": token_id}) - - for token_info in tokenizer_json_data.get("added_tokens", []) or []: - token_str = token_info.get("content") - token_id = token_info.get("id") - if token_str is None or token_id is None: - continue - special_tokens[int(token_id)] = token_str - if not any(item["token"] == token_str and item["id"] == int(token_id) for item in additional_special_tokens): - additional_special_tokens.append({"token": token_str, "id": int(token_id)}) - - model_type = model_name_l or getattr(tokenizer, 'name_or_path', '').lower() - if 'gemma' in model_type: - gemma_special_tokens = { - '': None, - '': None, - '': None, - '': None, - # Gemma 3 function calling tokens - '': None, - '': None, - '': None, - '': None, - '': None, - '': None, - '': None - } - - vocab = tokenizer.get_vocab() - for token_str in gemma_special_tokens.keys(): - if token_str in vocab: - token_id = vocab[token_str] - gemma_special_tokens[token_str] = token_id - special_tokens[token_id] = token_str - print(f" Found Gemma special token: {token_str} (ID: {token_id})") - - missing_tokens = [k for k, v in gemma_special_tokens.items() if v is None] - if missing_tokens: - unk_id = getattr(tokenizer, 'unk_token_id', None) - for token_str in missing_tokens: - token_id = tokenizer.convert_tokens_to_ids(token_str) - if token_id != unk_id and token_id is not None: - gemma_special_tokens[token_str] = token_id - special_tokens[token_id] = token_str - print(f" Found Gemma special token: {token_str} (ID: {token_id})") - - if gemma_special_tokens[''] is None: - hardcoded_ids = { - '': 105, - '': 106 - } - for token_str, token_id in hardcoded_ids.items(): - if token_str in gemma_special_tokens and gemma_special_tokens[token_str] is None: - if token_id not in special_tokens: - gemma_special_tokens[token_str] = token_id - special_tokens[token_id] = token_str - print(f" Using hardcoded Gemma special token: {token_str} (ID: {token_id})") - - chat_template_data = {} - if hasattr(tokenizer, 'chat_template') and tokenizer.chat_template: - chat_template_output = output_dir / "chat_template.jinja2" - with open(chat_template_output, 'w', encoding='utf-8') as f: - f.write(tokenizer.chat_template) - chat_template_data["chat_template"] = tokenizer.chat_template - elif (output_dir / "chat_template.jinja2").exists(): - chat_template_data["chat_template"] = (output_dir / "chat_template.jinja2").read_text(encoding='utf-8') - - tokenizer_full_config = {} - added_tokens_decoder = {} - tool_tokens = {} - - try: - config_path = None - if hasattr(tokenizer, 'name_or_path') and hf_hub_download: - try: - local_candidate = Path(tokenizer.name_or_path) / "tokenizer_config.json" - if Path(tokenizer.name_or_path).is_dir() and local_candidate.exists(): - config_path = str(local_candidate) - else: - config_path = hf_hub_download(repo_id=tokenizer.name_or_path, filename="tokenizer_config.json", token=token) - with open(config_path, 'r') as f: - tokenizer_full_config = json.load(f) - - if 'chat_template' in tokenizer_full_config and not chat_template_data: - chat_template_output = output_dir / "chat_template.jinja2" - with open(chat_template_output, 'w', encoding='utf-8') as f: - f.write(tokenizer_full_config['chat_template']) - chat_template_data["chat_template"] = tokenizer_full_config['chat_template'] - - if 'added_tokens_decoder' in tokenizer_full_config: - added_tokens_decoder = tokenizer_full_config['added_tokens_decoder'] - - print(" Extracting special tokens from tokenizer_config.json...") - for token_id_str, token_info in added_tokens_decoder.items(): - content = token_info.get('content', '') - token_id = int(token_id_str) - - tool_related = ['', '', - '', '', - '', '', - '', '', - # Gemma 3 function calling tokens - '', '', - '', '', - '', '', - ''] - - if any(x == content for x in tool_related): - tool_tokens[token_id] = token_info - print(f" Found tool token: {content} (ID: {token_id})") - special_tokens[token_id] = content - - except Exception as e: - print(f" Note: Could not load full tokenizer config: {e}") - pass - except Exception: - pass - - - # Extend id_to_token to include special/added tokens so vocab.txt is self-contained - if special_tokens: - max_special_id = max(special_tokens.keys()) - if max_special_id >= len(id_to_token): - id_to_token.extend([""] * (max_special_id - len(id_to_token) + 1)) - for token_id, token_str in special_tokens.items(): - id_to_token[token_id] = token_str - - vocab_output = output_dir / "vocab.txt" - with open(vocab_output, 'w', encoding='utf-8') as f: - for token_id, token_str in enumerate(id_to_token): - if token_str: - f.write(f"{token_id}\t{token_str}\n") - print(f" Saved tokenizer vocabulary (ID\\ttoken format)") - - special_tokens_output = output_dir / "special_tokens.json" - with open(special_tokens_output, 'w', encoding='utf-8') as f: - json.dump({ - **special_token_ids, - "vocab_size": len(id_to_token), - "model_max_length": getattr(tokenizer, 'model_max_length', 131072), - "special_tokens": special_tokens, - "additional_special_tokens": additional_special_tokens, - **chat_template_data - }, f, indent=2, ensure_ascii=False) - - normalizer = "none" - decoder = "none" - byte_fallback = False - vocab_format = "id_tab_token" - tokenizer_type = "sentencepiece" if is_sentencepiece else "bpe" - - if tokenizer_model_type == "BPE": - tokenizer_type = "bpe" - byte_fallback = bool(tokenizer_model.get("byte_fallback", False)) - if _is_metaspace_normalizer(tokenizer_json_data.get("normalizer")): - normalizer = "metaspace" - elif _decoder_has_type(tokenizer_json_data.get("decoder"), "ByteFallback"): - normalizer = "byte_level" - - if _is_replace_metaspace_decoder(tokenizer_json_data.get("decoder")): - decoder = "replace_metaspace" - elif _decoder_has_type(tokenizer_json_data.get("decoder"), "ByteFallback"): - decoder = "byte_level" - elif is_sentencepiece: - tokenizer_type = "sentencepiece" - - tokenizer_config_output = output_dir / "tokenizer_config.txt" - with open(tokenizer_config_output, 'w') as f: - f.write(f"vocab_size={len(id_to_token)}\n") - for key, value in special_token_ids.items(): - f.write(f"{key}={value}\n") - f.write(f"model_max_length={getattr(tokenizer, 'model_max_length', 131072)}\n") - f.write(f"tokenizer_type={tokenizer_type}\n") - f.write(f"vocab_format={vocab_format}\n") - f.write(f"normalizer={normalizer}\n") - f.write(f"decoder={decoder}\n") - f.write(f"byte_fallback={'true' if byte_fallback else 'false'}\n") - - if chat_template_data: - f.write("has_chat_template=true\n") - else: - f.write("has_chat_template=false\n") - if len(tool_tokens) > 0: - f.write(f"has_tool_support=true\n") - f.write(f"tool_token_count={len(tool_tokens)}\n") - - -def _read_varint(data, pos): - shift, value = 0, 0 - while True: - byte = data[pos]; pos += 1 - value |= (byte & 0x7F) << shift - if not (byte & 0x80): return value, pos - shift += 7 - - -def _skip_proto(data, pos, wire_type): - if wire_type == 0: _, pos = _read_varint(data, pos); return pos - if wire_type == 1: return pos + 8 - if wire_type == 2: length, pos = _read_varint(data, pos); return pos + length - if wire_type == 5: return pos + 4 - raise ValueError(f"Unsupported wire type: {wire_type}") - - -def parse_sentencepiece_pieces(path): - import struct - data = Path(path).read_bytes() - pos, pieces = 0, [] - while pos < len(data): - tag, pos = _read_varint(data, pos) - if (tag >> 3) != 1 or (tag & 7) != 2: - pos = _skip_proto(data, pos, tag & 7); continue - msg_len, pos = _read_varint(data, pos) - end = pos + msg_len; msg = data[pos:end]; pos = end - inner_pos, piece, score = 0, None, 0.0 - while inner_pos < len(msg): - itag, inner_pos = _read_varint(msg, inner_pos) - ifield, iwire = itag >> 3, itag & 7 - if ifield == 1 and iwire == 2: - vlen, inner_pos = _read_varint(msg, inner_pos) - piece = msg[inner_pos:inner_pos + vlen].decode("utf-8", errors="replace") - inner_pos += vlen - elif ifield == 2 and iwire == 5: - score = struct.unpack("", 0) - eos_id = piece_to_id.get("", 1) - bos_id = piece_to_id.get("", 2) - unk_id = piece_to_id.get("", 3) - - special = {pad_id: "", eos_id: "", bos_id: "", unk_id: ""} - tool_tokens = [] - for tok in ("", ""): - tid = piece_to_id.get(tok) - if tid is not None: - special[tid] = tok - tool_tokens.append({"token": tok, "id": tid}) - - return { - "vocab_size": len(pieces), "pad_token_id": pad_id, "eos_token_id": eos_id, - "bos_token_id": bos_id, "unk_token_id": unk_id, "model_max_length": model_max_length, - "sp_model_type": "bpe", "sp_add_dummy_prefix": True, - "sp_remove_extra_whitespaces": True, "sp_escape_whitespaces": True, - "sp_byte_fallback": True, "special_tokens": {str(k): v for k, v in special.items()}, - "additional_special_tokens": tool_tokens, - } - - -def _write_sentencepiece_files(output_dir, pieces, meta): - with open(output_dir / "vocab.txt", "w", encoding="utf-8") as f: - for i, p in enumerate(pieces): - f.write(f"{i}\t{p['piece']}\t{p['score']}\n") - - with open(output_dir / "merges.txt", "w", encoding="utf-8") as f: - f.write("#version: 0.2\n") - - with open(output_dir / "special_tokens.json", "w", encoding="utf-8") as f: - json.dump(meta, f, indent=2, ensure_ascii=False) - - tool_tokens = meta.get("additional_special_tokens", []) - with open(output_dir / "tokenizer_config.txt", "w", encoding="utf-8") as f: - f.write(f"vocab_size={meta['vocab_size']}\n") - f.write(f"eos_token_id={meta['eos_token_id']}\npad_token_id={meta['pad_token_id']}\n") - f.write(f"bos_token_id={meta['bos_token_id']}\nunk_token_id={meta['unk_token_id']}\n") - f.write(f"model_max_length={meta['model_max_length']}\n") - f.write("tokenizer_type=sentencepiece\nsp_model_type=bpe\n") - f.write("sp_add_dummy_prefix=true\nsp_remove_extra_whitespaces=true\n") - f.write("sp_escape_whitespaces=true\nsp_byte_fallback=true\n") - f.write("has_chat_template=false\n") - if tool_tokens: - f.write(f"has_tool_support=true\ntool_token_count={len(tool_tokens)}\n") - - -def convert_sentencepiece_tokenizer(tokenizer_path, output_dir, model_max_length=131072): - output_dir = Path(output_dir) - pieces = parse_sentencepiece_pieces(tokenizer_path) - meta = _build_sentencepiece_metadata(pieces, model_max_length) - _write_sentencepiece_files(output_dir, pieces, meta) diff --git a/python/src/weight_patterns.py b/python/src/weight_patterns.py deleted file mode 100644 index 046588c56..000000000 --- a/python/src/weight_patterns.py +++ /dev/null @@ -1,340 +0,0 @@ -EMBED_NAMES = [ - 'model.language_model.embed_tokens.weight', - 'model.text_model.embed_tokens.weight', - 'model.embed_tokens.weight', - 'embed_tokens.weight', - 'embeddings.weight', - 'transformer.wte.weight', - 'model.decoder.embed_tokens.weight', - 'decoder.embed_tokens.weight', - 'token_embeddings' -] - -MOONSHINE_GLOBAL_WEIGHTS = [ - ('model.encoder.conv1.weight', 'encoder_conv1_weight.weights'), - ('model.encoder.conv2.weight', 'encoder_conv2_weight.weights'), - ('model.encoder.conv2.bias', 'encoder_conv2_bias.bias'), - ('model.encoder.conv3.weight', 'encoder_conv3_weight.weights'), - ('model.encoder.conv3.bias', 'encoder_conv3_bias.bias'), - ('model.encoder.groupnorm.weight', 'encoder_norm_weight.weights'), - ('model.encoder.groupnorm.bias', 'encoder_norm_bias.bias'), - ('model.encoder.layer_norm.weight', 'encoder_layer_norm_weight.weights'), - ('model.encoder.layer_norm.bias', 'encoder_layer_norm_bias.bias'), - ('model.decoder.norm.weight', 'output_norm.weights'), - ('model.decoder.norm.bias', 'output_norm.bias'), - ('decoder.norm.weight', 'output_norm.weights'), - ('proj_out.weight', 'output_weight.weights'), - ('model.proj_out.weight', 'output_weight.weights'), - ('model.decoder.embed_tokens.weight', 'token_embeddings.weights'), - ('model.decoder.embed_positions.weight', 'decoder_position_embeddings.weights'), -] - -OUTPUT_NAMES = [ - 'lm_head.weight', - 'output.weight', - 'transformer.lm_head.weight', - 'model.text_model.lm_head.weight' -] - -OUTPUT_NORM_NAMES = [ - 'model.norm.weight', - 'norm.weight', - 'final_layernorm.weight', - 'transformer.ln_f.weight', - 'model.embedding_norm.weight', - 'model.language_model.embedding_norm.weight', - 'model.language_model.norm.weight', - 'model.text_model.norm.weight', - 'decoder.norm.weight' -] - -LAYER_PREFIXES = [ - 'model.language_model.layers.{i}.', - 'model.text_model.layers.{i}.', - 'model.layers.{i}.', - 'layers.{i}.', - 'transformer.h.{i}.', - 'encoder.layers.{i}.', - 'decoder.layers.{i}.', - 'model.encoder.layers.{i}.', - 'model.decoder.layers.{i}.' -] - -VISION_ITEMS = [ - ('model.vision_tower.vision_model.embeddings.patch_embedding.weight', 'vision_patch_embedding.weights'), - ('model.vision_model.embeddings.patch_embedding.weight', 'vision_patch_embedding.weights'), - ('model.vision_tower.vision_model.embeddings.patch_embedding.bias', 'vision_patch_embedding.bias.weights'), - ('model.vision_model.embeddings.patch_embedding.bias', 'vision_patch_embedding.bias.weights'), - ('model.vision_tower.vision_model.embeddings.position_embedding.weight', 'vision_position_embedding.weights'), - ('model.vision_model.embeddings.position_embedding.weight', 'vision_position_embedding.weights'), - ('model.vision_tower.vision_model.post_layernorm.weight', 'vision_post_layernorm.weights'), - ('model.vision_model.post_layernorm.weight', 'vision_post_layernorm.weights'), - ('model.vision_tower.vision_model.post_layernorm.bias', 'vision_post_layernorm.bias.weights'), - ('model.vision_model.post_layernorm.bias', 'vision_post_layernorm.bias.weights') -] - -PROJECTOR_WEIGHTS = [ - ('model.multi_modal_projector.linear_1.weight', 'projector_linear1.weights'), - ('model.multi_modal_projector.linear_1.bias', 'projector_linear1.bias.weights'), - ('model.multi_modal_projector.linear_2.weight', 'projector_linear2.weights'), - ('model.multi_modal_projector.linear_2.bias', 'projector_linear2.bias.weights'), - ('model.multi_modal_projector.layer_norm.weight', 'projector_layer_norm.weights'), - ('model.multi_modal_projector.layer_norm.bias', 'projector_layer_norm.bias.weights'), -] - -CONNECTOR_KEYS = [ - 'model.connector.modality_projection.proj.weight', - 'connector.modality_projection.proj.weight', - 'model.connector.proj.weight', - 'connector.proj.weight' -] - -GEMMA3N_GLOBAL_WEIGHTS = [ - ('model.language_model.altup_projections.0.weight', 'altup_proj_0.weights'), - ('model.language_model.altup_projections.1.weight', 'altup_proj_1.weights'), - ('model.language_model.altup_projections.2.weight', 'altup_proj_2.weights'), - ('model.language_model.altup_unembed_projections.0.weight', 'altup_unembed_proj_0.weights'), - ('model.language_model.altup_unembed_projections.1.weight', 'altup_unembed_proj_1.weights'), - ('model.language_model.altup_unembed_projections.2.weight', 'altup_unembed_proj_2.weights'), - ('model.language_model.embed_tokens_per_layer.weight', 'embed_tokens_per_layer.weights'), - ('model.language_model.per_layer_model_projection.weight', 'per_layer_model_proj.weights'), - ('model.language_model.per_layer_projection_norm.weight', 'per_layer_proj_norm.weights'), - ('model.embed_vision.embedding.weight', 'embed_vision_embedding.weights'), - ('model.embed_vision.embedding_projection.weight', 'embed_vision_proj.weights'), - ('model.embed_vision.soft_embedding_norm.weight', 'embed_vision_soft_norm.weights'), - ('model.embed_vision.hard_embedding_norm.weight', 'embed_vision_hard_norm.weights'), - ('model.embed_audio.embedding.weight', 'embed_audio_embedding.weights'), - ('model.embed_audio.embedding_projection.weight', 'embed_audio_proj.weights'), - ('model.embed_audio.soft_embedding_norm.weight', 'embed_audio_soft_norm.weights'), - ('model.embed_audio.hard_embedding_norm.weight', 'embed_audio_hard_norm.weights'), -] - -GEMMA3N_VISION_TOWER_PREFIX = 'model.vision_tower.timm_model.' -GEMMA3N_AUDIO_TOWER_PREFIX = 'model.audio_tower.' - -GEMMA4_GLOBAL_WEIGHTS = [ - ('model.language_model.embed_tokens_per_layer.weight', 'embed_tokens_per_layer.weights'), - ('model.language_model.per_layer_model_projection.weight', 'per_layer_model_proj.weights'), - ('model.language_model.per_layer_projection_norm.weight', 'per_layer_proj_norm.weights'), - ('model.embed_vision.embedding.weight', 'embed_vision_embedding.weights'), - ('model.embed_vision.embedding_projection.weight', 'embed_vision_proj.weights'), - ('model.embed_audio.embedding.weight', 'embed_audio_embedding.weights'), - ('model.embed_audio.embedding_projection.weight', 'embed_audio_proj.weights'), -] - -GEMMA4_VISION_TOWER_PREFIX = 'model.vision_tower.' -GEMMA4_AUDIO_TOWER_PREFIX = 'model.audio_tower.' - -WHISPER_GLOBAL_WEIGHTS = [ - ('decoder.embed_tokens.weight', 'decoder_token_embeddings.weights'), - ('decoder.embed_positions.weight', 'decoder_position_embeddings.weights'), - ('decoder.layer_norm.weight', 'decoder_norm.weights'), - ('decoder.layer_norm.bias', 'decoder_norm.bias'), - ('proj_out.weight', 'output_layer.weights'), - ('encoder.embed_positions.weight', 'encoder_position_embeddings.weights'), - ('encoder.conv1.bias', 'encoder_conv1_bias.bias'), - ('encoder.conv1.weight', 'encoder_conv1_weight.weights'), - ('encoder.conv2.bias', 'encoder_conv2_bias.bias'), - ('encoder.conv2.weight', 'encoder_conv2_weight.weights'), - ('encoder.layer_norm.bias', 'encoder_norm_bias.bias'), - ('encoder.layer_norm.weight', 'encoder_norm_weight.weights') -] - - -NEEDLE_GLOBAL_WEIGHTS = [ - ('embedding.embedding', 'token_embeddings.weights', False), - ('embedding.embedding', 'output_weight.weights', False), - ('encoder.final_norm.scale', 'encoder_layer_norm_weight.weights', False), - ('decoder.ZCRMSNorm_0.scale', 'output_norm.weights', False), - ('contrastive_proj.kernel', 'contrastive_proj.weights', True), - ('log_temp', 'log_temp.weights', False), -] - -NEEDLE_ENCODER_LAYER_WEIGHTS = [ - ('ZCRMSNorm_0.scale', 'input_norm.weights', False), - ('attn_gate', 'attn_gate.weights', False), - ('ZCRMSNorm_1.scale', 'post_attn_norm.weights', False), - ('self_attn.q_proj.kernel', 'attn_q.weights', True), - ('self_attn.k_proj.kernel', 'attn_k.weights', True), - ('self_attn.v_proj.kernel', 'attn_v.weights', True), - ('self_attn.out_proj.kernel', 'attn_output.weights', True), - ('self_attn.q_norm.scale', 'attn_q_norm.weights', False), - ('self_attn.k_norm.scale', 'attn_k_norm.weights', False), - ('FeedForward_0.gate_proj.kernel', 'ffn_gate.weights', True), - ('FeedForward_0.up_proj.kernel', 'ffn_up.weights', True), - ('FeedForward_0.down_proj.kernel', 'mlp_fc2.weights', True), -] - -NEEDLE_DECODER_LAYER_WEIGHTS = [ - ('ZCRMSNorm_0.scale', 'input_norm.weights', False), - ('ZCRMSNorm_1.scale', 'post_attn_norm.weights', False), - ('self_attn_gate', 'self_attn_gate.weights', False), - ('cross_attn_gate', 'cross_attn_gate.weights', False), - ('ZCRMSNorm_2.scale', 'final_norm.weights', False), - ('self_attn.q_proj.kernel', 'attn_q.weights', True), - ('self_attn.k_proj.kernel', 'attn_k.weights', True), - ('self_attn.v_proj.kernel', 'attn_v.weights', True), - ('self_attn.out_proj.kernel', 'attn_output.weights', True), - ('self_attn.q_norm.scale', 'attn_q_norm.weights', False), - ('self_attn.k_norm.scale', 'attn_k_norm.weights', False), - ('cross_attn.q_proj.kernel', 'encoder_attn_q.weights', True), - ('cross_attn.k_proj.kernel', 'encoder_attn_k.weights', True), - ('cross_attn.v_proj.kernel', 'encoder_attn_v.weights', True), - ('cross_attn.out_proj.kernel', 'encoder_attn_output.weights', True), - ('cross_attn.q_norm.scale', 'encoder_attn_q_norm.weights', False), - ('cross_attn.k_norm.scale', 'encoder_attn_k_norm.weights', False), - ('FeedForward_0.gate_proj.kernel', 'ffn_gate.weights', True), - ('FeedForward_0.up_proj.kernel', 'ffn_up.weights', True), - ('FeedForward_0.down_proj.kernel', 'mlp_fc2.weights', True), -] - - -def get_layer_weight_patterns(i, precision, model_type=None, skip_kv=False): - is_whisper = model_type == 'whisper' - is_qwen_family = isinstance(model_type, str) and ('qwen' in model_type) - is_youtu = model_type == 'youtu' - - patterns = [ - # Youtu MLA attention weights - (['self_attn.q_a_proj.weight'], precision, f'layer_{i}_attn_q_a.weights', False) if is_youtu else None, - (['self_attn.q_a_layernorm.weight'], precision, f'layer_{i}_attn_q_a_norm.weights', False) if is_youtu else None, - (['self_attn.q_b_proj.weight'], precision, f'layer_{i}_attn_q_b.weights', False) if is_youtu else None, - (['self_attn.kv_a_proj_with_mqa.weight'], precision, f'layer_{i}_attn_kv_a.weights', False) if is_youtu else None, - (['self_attn.kv_a_layernorm.weight'], precision, f'layer_{i}_attn_kv_a_norm.weights', False) if is_youtu else None, - (['self_attn.kv_b_proj.weight'], precision, f'layer_{i}_attn_kv_b.weights', False) if is_youtu else None, - (['self_attn.q_proj.weight', 'attn.q_proj.weight', 'attn.c_attn.weight'], precision, f'layer_{i}_attn_q.weights', False) if not is_whisper and not is_youtu else None, - (['self_attn.k_proj.weight', 'attn.k_proj.weight'], precision, f'layer_{i}_attn_k.weights', False) if not is_whisper and not skip_kv and not is_youtu else None, - (['self_attn.v_proj.weight', 'attn.v_proj.weight'], precision, f'layer_{i}_attn_v.weights', False) if not is_whisper and not skip_kv and not is_youtu else None, - (['self_attn.o_proj.weight', 'attn.o_proj.weight', 'attn.c_proj.weight', 'self_attn.out_proj.weight'], precision, f'layer_{i}_attn_output.weights', False) if not is_whisper else None, - # Qwen3.5 linear-attention path - (['linear_attn.in_proj_qkv.weight'], precision, f'layer_{i}_linear_attn_qkv.weights', False) if is_qwen_family else None, - (['linear_attn.in_proj_a.weight'], precision, f'layer_{i}_linear_attn_a.weights', False) if is_qwen_family else None, - (['linear_attn.in_proj_b.weight'], precision, f'layer_{i}_linear_attn_b.weights', False) if is_qwen_family else None, - (['linear_attn.in_proj_z.weight'], precision, f'layer_{i}_linear_attn_z.weights', False) if is_qwen_family else None, - (['linear_attn.out_proj.weight'], precision, f'layer_{i}_linear_attn_output.weights', False) if is_qwen_family else None, - (['linear_attn.norm.weight'], precision, f'layer_{i}_linear_attn_norm.weights', False) if is_qwen_family else None, - (['linear_attn.conv1d.weight'], precision, f'layer_{i}_linear_attn_conv1d.weights', False) if is_qwen_family else None, - (['linear_attn.A_log'], precision, f'layer_{i}_linear_attn_A_log.weights', False) if is_qwen_family else None, - (['linear_attn.dt_bias'], precision, f'layer_{i}_linear_attn_dt_bias.weights', False) if is_qwen_family else None, - ([ - 'self_attn.deltanet_gate_proj.weight', - 'self_attn.gated_deltanet_gate_proj.weight', - 'self_attn.attn_gate_proj.weight', - 'self_attn.f_gate_proj.weight', - 'self_attn.attn_f_gate_proj.weight', - 'self_attn.attn_gate.weight', - 'self_attn.attn_f_gate.weight', - ], precision, f'layer_{i}_deltanet_gate.weights', False) if is_qwen_family else None, - ([ - 'self_attn.deltanet_beta_proj.weight', - 'self_attn.gated_deltanet_beta_proj.weight', - 'self_attn.attn_beta_proj.weight', - 'self_attn.f_beta_proj.weight', - 'self_attn.attn_f_beta_proj.weight', - 'self_attn.attn_beta.weight', - 'self_attn.attn_f_beta.weight', - ], precision, f'layer_{i}_deltanet_beta.weights', False) if is_qwen_family else None, - (['input_layernorm.weight', 'ln_1.weight', 'operator_norm.weight'], precision, f'layer_{i}_input_norm.weights', False), - (['self_attn.q_norm.weight', 'self_attn.q_layernorm.weight'], precision, f'layer_{i}_attn_q_norm.weights', False), - (['self_attn.k_norm.weight', 'self_attn.k_layernorm.weight'], precision, f'layer_{i}_attn_k_norm.weights', False) if not skip_kv else None, - (['mlp.gate_proj.weight', 'mlp.c_fc.weight', 'feed_forward.w1.weight', 'ff.ff_proj.weight'], precision, f'layer_{i}_ffn_gate.weights', False), - (['mlp.up_proj.weight', 'feed_forward.w3.weight', 'ff.ff_noact.weight'], precision, f'layer_{i}_ffn_up.weights', False), - (['mlp.down_proj.weight', 'mlp.c_proj.weight', 'feed_forward.w2.weight', 'ff.ff_out.weight'], precision, f'layer_{i}_ffn_down.weights', False), - (['feed_forward.gate.weight'], precision, f'layer_{i}_moe_router.weights', False), - (['feed_forward.expert_bias'], precision, f'layer_{i}_moe_expert_bias.weights', False), - (['feed_forward.experts.{channel}.w1.weight'], precision, f'layer_{i}_moe_expert_{{channel}}_w1.weights', False), - (['feed_forward.experts.{channel}.w3.weight'], precision, f'layer_{i}_moe_expert_{{channel}}_w3.weights', False), - (['feed_forward.experts.{channel}.w2.weight'], precision, f'layer_{i}_moe_expert_{{channel}}_w2.weights', False), - (['moe.gate_proj'], precision, f'layer_{i}_moe_gate_proj.weights', False), - (['moe.up_proj'], precision, f'layer_{i}_moe_up_proj.weights', False), - (['moe.down_proj'], precision, f'layer_{i}_moe_down_proj.weights', False), - (['moe.per_expert_scale'], 'FP16', f'layer_{i}_moe_per_expert_scale.weights', False), - (['router.proj.weight'], precision, f'layer_{i}_router_proj.weights', False), - (['router.scale'], 'FP16', f'layer_{i}_router_scale.weights', False), - (['post_attention_layernorm.weight', 'ln_2.weight', 'ffn_norm.weight', 'norm2.weight'], precision, f'layer_{i}_post_attn_norm.weights', False), - (['pre_feedforward_layernorm.weight'], precision, f'layer_{i}_pre_ffn_norm.weights', False), - (['post_feedforward_layernorm.weight'], precision, f'layer_{i}_post_ffn_norm.weights', False), - (['post_feedforward_layernorm_1.weight'], precision, f'layer_{i}_post_ffn_norm_1.weights', False), - (['post_feedforward_layernorm_2.weight'], precision, f'layer_{i}_post_ffn_norm_2.weights', False), - (['pre_feedforward_layernorm_2.weight'], precision, f'layer_{i}_pre_ffn_norm_2.weights', False), - (['conv.in_proj.weight'], precision, f'layer_{i}_conv_in_proj.weights', False), - (['conv.out_proj.weight'], precision, f'layer_{i}_conv_out_proj.weights', False), - (['conv.conv.weight'], precision, f'layer_{i}_conv_depthwise.weights', False), - (['attn.Wqkv.bias'], precision, f'layer_{i}_attn_{{channel}}.bias', False), - (['attn.Wqkv.weight'], precision, f'layer_{i}_attn_{{channel}}.weights', False), - (['attn.out_proj.bias', 'attention.to_out.bias'], precision, f'layer_{i}_attn_output.bias', False), - (['attn.out_proj.weight', 'attention.to_out.weight'], precision, f'layer_{i}_attn_output.weights', False), - (['mlp.fc1.bias', 'ff.ff.0.bias'], precision, f'layer_{i}_mlp_fc1.bias', False), - (['mlp.fc1.weight', 'ff.ff.0.weight'], precision, f'layer_{i}_mlp_fc1.weights', False), - (['mlp.fc2.bias', 'ff.ff.2.bias'], precision, f'layer_{i}_mlp_fc2.bias', False), - (['mlp.fc2.weight', 'ff.ff.2.weight'], precision, f'layer_{i}_mlp_fc2.weights', False), - (['norm1.bias'], precision, f'layer_{i}_norm1.bias', False), - (['norm1.weight'], precision, f'layer_{i}_norm1.weights', False), - (['norm2.bias'], precision, f'layer_{i}_norm2.bias', False), - (['norm2.weight'], precision, f'layer_{i}_norm2.weights', False), - (['mlp.experts.bias'], precision, f'layer_{i}_mlp_experts.bias', False), - (['mlp.experts.mlp.w1'], precision, f'layer_{i}_mlp_expert_{{channel}}.mlp1.weights', False), - (['mlp.experts.mlp.w2'], precision, f'layer_{i}_mlp_expert_{{channel}}.mlp2.weights', True), - (['mlp.router.layer.weight'], precision, f'layer_{i}_mlp_router.layer.weights', False), - (['encoder_attn.q_proj.weight', 'attention.to_q.weight'], precision, f'layer_{i}_encoder_attn_q.weights', False), - (['encoder_attn.k_proj.weight', 'attention.to_k.weight'], precision, f'layer_{i}_encoder_attn_k.weights', False), - (['encoder_attn.v_proj.weight', 'attention.to_v.weight'], precision, f'layer_{i}_encoder_attn_v.weights', False), - (['encoder_attn.out_proj.weight', 'encoder_attn.o_proj.weight'], precision, f'layer_{i}_encoder_attn_output.weights', False), - (['encoder_attn.q_proj.bias'], precision, f'layer_{i}_encoder_attn_q.bias', False), - (['encoder_attn.v_proj.bias'], precision, f'layer_{i}_encoder_attn_v.bias', False), - (['encoder_attn.out_proj.bias', 'encoder_attn.o_proj.bias'], precision, f'layer_{i}_encoder_attn_output.bias', False), - (['encoder_attn_layer_norm.weight'], precision, f'layer_{i}_encoder_attn_norm.weights', False), - (['encoder_attn_layer_norm.bias'], precision, f'layer_{i}_encoder_attn_norm.bias', False), - (['fc1.weight'], precision, f'layer_{i}_mlp_fc1.weights', False), - (['fc1.bias'], precision, f'layer_{i}_mlp_fc1.bias', False), - (['fc2.weight'], precision, f'layer_{i}_mlp_fc2.weights', False), - (['fc2.bias'], precision, f'layer_{i}_mlp_fc2.bias', False), - (['final_layer_norm.weight', 'final_layernorm.weight'], precision, f'layer_{i}_final_norm.weights', False), - (['final_layer_norm.bias', 'final_layernorm.bias'], precision, f'layer_{i}_final_norm.bias', False), - - # Whisper-only: separate self_attn_* outputs (non-Whisper uses attn_* above) - (['self_attn.q_proj.weight'], precision, f'layer_{i}_self_attn_q.weights', False) if is_whisper else None, - (['self_attn.k_proj.weight'], precision, f'layer_{i}_self_attn_k.weights', False) if is_whisper else None, - (['self_attn.v_proj.weight'], precision, f'layer_{i}_self_attn_v.weights', False) if is_whisper else None, - (['self_attn.q_proj.bias'], precision, f'layer_{i}_self_attn_q.bias', False) if is_whisper else None, - (['self_attn.v_proj.bias'], precision, f'layer_{i}_self_attn_v.bias', False) if is_whisper else None, - (['self_attn.out_proj.weight'], precision, f'layer_{i}_self_attn_output.weights', False) if is_whisper else None, - (['self_attn.out_proj.bias'], precision, f'layer_{i}_self_attn_output.bias', False) if is_whisper else None, - (['self_attn_layer_norm.weight'], precision, f'layer_{i}_self_attn_norm.weights', False), - (['self_attn_layer_norm.bias'], precision, f'layer_{i}_self_attn_norm.bias', False), - (['altup.router_norm.weight'], precision, f'layer_{i}_altup_router_norm.weights', False), - (['altup.prediction_coefs.weight'], 'FP16', f'layer_{i}_altup_prediction_coefs.weights', False), - (['altup.correction_coefs.weight'], 'FP16', f'layer_{i}_altup_correction_coefs.weights', False), - (['altup.correct_output_scale'], 'FP16', f'layer_{i}_altup_correct_output_scale.weights', False), - (['altup.modality_router.weight'], precision, f'layer_{i}_altup_modality_router.weights', False), - (['laurel.linear_left.weight'], precision, f'layer_{i}_laurel_left.weights', False), - (['laurel.linear_right.weight'], precision, f'layer_{i}_laurel_right.weights', False), - (['laurel.post_laurel_norm.weight'], precision, f'layer_{i}_laurel_norm.weights', False), - (['per_layer_projection.weight'], precision, f'layer_{i}_per_layer_proj.weights', False), - (['per_layer_input_gate.weight'], precision, f'layer_{i}_per_layer_gate.weights', False), - (['post_per_layer_input_norm.weight'], precision, f'layer_{i}_post_per_layer_norm.weights', False), - (['layer_scalar'], 'FP16', f'layer_{i}_layer_scalar.weights', False), - ] - - return [p for p in patterns if p is not None] - - -def get_vision_layer_weights(i_v, vpref): - return [ - (vpref + 'layer_norm1.weight', f'vision_layer_{i_v}_layer_norm1.weights'), - (vpref + 'layer_norm1.bias', f'vision_layer_{i_v}_layer_norm1.bias.weights'), - (vpref + 'layer_norm2.weight', f'vision_layer_{i_v}_layer_norm2.weights'), - (vpref + 'layer_norm2.bias', f'vision_layer_{i_v}_layer_norm2.bias.weights'), - (vpref + 'mlp.fc1.weight', f'vision_layer_{i_v}_ffn_fc1.weights'), - (vpref + 'mlp.fc1.bias', f'vision_layer_{i_v}_ffn_fc1.bias.weights'), - (vpref + 'mlp.fc2.weight', f'vision_layer_{i_v}_ffn_fc2.weights'), - (vpref + 'mlp.fc2.bias', f'vision_layer_{i_v}_ffn_fc2.bias.weights'), - (vpref + 'self_attn.q_proj.weight', f'vision_layer_{i_v}_self_attn_q.weights'), - (vpref + 'self_attn.k_proj.weight', f'vision_layer_{i_v}_self_attn_k.weights'), - (vpref + 'self_attn.v_proj.weight', f'vision_layer_{i_v}_self_attn_v.weights'), - (vpref + 'self_attn.out_proj.weight', f'vision_layer_{i_v}_self_attn_out.weights'), - (vpref + 'self_attn.q_proj.bias', f'vision_layer_{i_v}_self_attn_q.bias.weights'), - (vpref + 'self_attn.k_proj.bias', f'vision_layer_{i_v}_self_attn_k.bias.weights'), - (vpref + 'self_attn.v_proj.bias', f'vision_layer_{i_v}_self_attn_v.bias.weights'), - (vpref + 'self_attn.out_proj.bias', f'vision_layer_{i_v}_self_attn_out.bias.weights'), - ] diff --git a/python/tests/test_bindings.py b/python/tests/test_bindings.py index 7ab6412b7..2db25d2d1 100644 --- a/python/tests/test_bindings.py +++ b/python/tests/test_bindings.py @@ -286,14 +286,14 @@ def test_download_defaults(self): assert args.bits == 4 def test_run_command(self): - args = self.parser.parse_args(["run", "gemma4", "--prompt", "hi"]) + args = self.parser.parse_args(["run", "google/gemma-4-E2B-it", "--prompt", "hi"]) assert args.command == "run" - assert args.model_id == "gemma4" + assert args.model_id == "google/gemma-4-E2B-it" assert args.prompt == "hi" def test_run_command_chunked_bundle_flags(self): args = self.parser.parse_args([ - "run", "bundle", + "run", "Foo/Bar", "--audio", "audio.wav", "--image", "image.png", "--input-ids", "1,2,3", @@ -308,9 +308,9 @@ def test_run_command_chunked_bundle_flags(self): assert args.result_json == "result.json" def test_convert_command(self): - args = self.parser.parse_args(["convert", "qwen", "--bits", "2"]) + args = self.parser.parse_args(["convert", "Qwen/Qwen3-0.6B", "--bits", "2"]) assert args.command == "convert" - assert args.model_id == "qwen" + assert args.model_id == "Qwen/Qwen3-0.6B" assert args.bits == 2 def test_build_command(self): @@ -322,10 +322,18 @@ def test_test_command_suite(self): args = self.parser.parse_args(["test", "--suite", "llm"]) assert args.command == "test" assert args.suite == "llm" + assert args.component == "all" + + def test_test_command_component(self): + args = self.parser.parse_args(["test", "--component", "kernels"]) + assert args.command == "test" + assert args.component == "kernels" + assert args.suite is None - def test_test_command_default_suite(self): + def test_test_command_defaults(self): args = self.parser.parse_args(["test"]) assert args.command == "test" + assert args.component == "all" assert args.suite is None def test_auth_command(self): @@ -341,9 +349,31 @@ def test_no_command_prints_help(self): args = self.parser.parse_args([]) assert args.command is None - def test_transpile_not_exposed(self): + def test_transpile_registered(self): + args = self.parser.parse_args(["transpile", "google/gemma-4-E2B-it"]) + assert args.command == "transpile" + assert args.model_id == "google/gemma-4-E2B-it" + + def test_run_rejects_bare_name(self): + import pytest with pytest.raises(SystemExit): - self.parser.parse_args(["transpile", "gemma4"]) + self.parser.parse_args(["run", "whisper-base", "--prompt", "hi"]) + + def test_run_accepts_local_bundle_path(self): + args = self.parser.parse_args(["run", "/tmp/bundle", "--prompt", "hi"]) + assert args.command == "run" + assert args.model_id == "/tmp/bundle" + assert args.prompt == "hi" + + def test_run_platform_flag(self): + args = self.parser.parse_args(["run", "Foo/Bar", "--platform", "apple"]) + assert args.platform == "apple" + assert args.bits == 4 + + def test_download_platform_flag(self): + args = self.parser.parse_args(["download", "Foo/Bar", "--platform", "cpu", "--bits", "2"]) + assert args.platform == "cpu" + assert args.bits == 2 diff --git a/python/tests/test_capture.py b/python/tests/test_capture.py deleted file mode 100644 index 528402de0..000000000 --- a/python/tests/test_capture.py +++ /dev/null @@ -1,187 +0,0 @@ -from __future__ import annotations - -import json -import re -from dataclasses import asdict -from pathlib import Path -from typing import Any - -import torch -from transformers import AutoModelForCausalLM -from transformers import AutoTokenizer - -from cactus.transpile.capture_pytorch import capture_model_with_fallback -from cactus.transpile.capture_pytorch import dump_graph -from cactus.transpile.graph_ir import verify_ir - - -MODELS = ( - "google/gemma-4-E2B", - "google/gemma-3-270m-it", - "google/gemma-2b-it", - "google/gemma-2-2b", - "Qwen/Qwen3.5-2B", - "TinyLlama/TinyLlama-1.1B-Chat-v1.0", - "HuggingFaceTB/SmolLM2-360M-Instruct", -) - -PROMPT = "The capital of France is" -ARTIFACTS_DIR = Path(__file__).resolve().parents[3] / "artifacts" / "python_capture_tests" - - -def slugify(value: str) -> str: - return re.sub(r"[^A-Za-z0-9._-]+", "_", value).strip("._-") or "model" - - -def resolve_local_snapshot(model_id: str) -> str | None: - explicit = Path(model_id) - if explicit.exists(): - return str(explicit) - - snapshots_dir = ( - Path.home() - / ".cache" - / "huggingface" - / "hub" - / ("models--" + model_id.replace("/", "--")) - / "snapshots" - ) - if not snapshots_dir.exists(): - return None - - snapshots = sorted(path for path in snapshots_dir.iterdir() if path.is_dir()) - if not snapshots: - return None - return str(snapshots[-1]) - - -def jsonify(value: Any) -> Any: - if isinstance(value, dict): - return {str(key): jsonify(inner) for key, inner in value.items()} - if isinstance(value, (list, tuple)): - return [jsonify(inner) for inner in value] - if isinstance(value, Path): - return str(value) - if isinstance(value, torch.dtype): - return str(value) - if isinstance(value, torch.Tensor): - return { - "type": "torch.Tensor", - "dtype": str(value.dtype), - "shape": list(value.shape), - } - if hasattr(value, "__dataclass_fields__"): - return jsonify(asdict(value)) - if isinstance(value, (str, int, float, bool)) or value is None: - return value - return repr(value) - - -def graph_to_dict(graph) -> dict[str, Any]: - return { - "meta": jsonify(graph.meta), - "inputs": list(graph.inputs), - "outputs": list(graph.outputs), - "constants": {value_id: jsonify(constant) for value_id, constant in graph.constants.items()}, - "values": {value_id: jsonify(asdict(value)) for value_id, value in graph.values.items()}, - "nodes": [jsonify(asdict(graph.nodes[node_id])) for node_id in graph.order], - } - - -def write_json(path: Path, payload: dict[str, Any]) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") - - -class CausalLMCaptureWrapper(torch.nn.Module): - def __init__(self, model: torch.nn.Module): - super().__init__() - self.model = model.eval() - - def forward(self, input_ids: torch.Tensor) -> torch.Tensor: - outputs = self.model( - input_ids=input_ids, - use_cache=False, - return_dict=False, - ) - if isinstance(outputs, torch.Tensor): - return outputs - if isinstance(outputs, (tuple, list)) and outputs and isinstance(outputs[0], torch.Tensor): - return outputs[0] - logits = getattr(outputs, "logits", None) - if isinstance(logits, torch.Tensor): - return logits - raise TypeError(f"unsupported model output type: {type(outputs).__name__}") - - -def capture_one_model(model_id: str) -> dict[str, Any]: - snapshot = resolve_local_snapshot(model_id) - if snapshot is None: - raise FileNotFoundError(f"no local snapshot found for {model_id}") - - artifact_dir = ARTIFACTS_DIR / slugify(model_id) - tokenizer = AutoTokenizer.from_pretrained(snapshot, local_files_only=True) - encoded = tokenizer(PROMPT, return_tensors="pt") - input_ids = encoded["input_ids"] - - model = AutoModelForCausalLM.from_pretrained( - snapshot, - torch_dtype="auto", - device_map=None, - low_cpu_mem_usage=True, - local_files_only=True, - ).eval() - - captured = capture_model_with_fallback( - CausalLMCaptureWrapper(model), - (input_ids,), - ) - verify_ir(captured.ir_graph) - - write_json( - artifact_dir / "graph_ir.json", - { - "model_id": model_id, - "snapshot": snapshot, - "prompt": PROMPT, - "strict": captured.strict, - "graph": graph_to_dict(captured.ir_graph), - }, - ) - (artifact_dir / "fx_graph.txt").write_text(dump_graph(captured) + "\n") - - return { - "model_id": model_id, - "snapshot": snapshot, - "artifact_dir": str(artifact_dir), - "strict": captured.strict, - "ir_nodes": len(captured.ir_graph.order), - "ir_values": len(captured.ir_graph.values), - } - - -def main() -> None: - results: list[dict[str, Any]] = [] - for model_id in MODELS: - print(f"capturing {model_id}") - try: - result = capture_one_model(model_id) - except Exception as exc: - result = { - "model_id": model_id, - "error": str(exc), - } - results.append(result) - print(json.dumps(result, indent=2)) - - write_json( - ARTIFACTS_DIR / "capture_manifest.json", - { - "prompt": PROMPT, - "results": results, - }, - ) - - -if __name__ == "__main__": - main() diff --git a/python/tests/test_cli_run.py b/python/tests/test_cli_run.py index 4c2b82c7c..f63772dfc 100644 --- a/python/tests/test_cli_run.py +++ b/python/tests/test_cli_run.py @@ -2,6 +2,7 @@ from pathlib import Path from types import SimpleNamespace +import cactus.cli.common as common_mod import cactus.cli.run as run_mod @@ -10,11 +11,11 @@ def test_cmd_run_forwards_chunked_bundle_flags(monkeypatch, tmp_path: Path) -> N (bundle_dir / "components").mkdir(parents=True) (bundle_dir / "components" / "manifest.json").write_text("{}", encoding="utf-8") - fake_pkg = tmp_path / "pkg" - fake_chat = fake_pkg / "bin" / "chat" - fake_chat.parent.mkdir(parents=True) - fake_chat.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") - monkeypatch.setattr(run_mod, "__file__", str(fake_pkg / "cli" / "run.py")) + fake_bin = tmp_path / "bin" + fake_run = fake_bin / "run" + fake_bin.mkdir(parents=True) + fake_run.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + monkeypatch.setattr(common_mod, "BIN_DIR", fake_bin) image_file = tmp_path / "image.png" audio_file = tmp_path / "audio.wav" @@ -24,15 +25,18 @@ def test_cmd_run_forwards_chunked_bundle_flags(monkeypatch, tmp_path: Path) -> N calls = [] - def fake_run(cmd): + def fake_subprocess_run(cmd): calls.append(cmd) return SimpleNamespace(returncode=0) - monkeypatch.setattr(run_mod.subprocess, "run", fake_run) + monkeypatch.setattr(run_mod.subprocess, "run", fake_subprocess_run) + monkeypatch.setattr(run_mod, "_resolve_or_fetch_bundle", lambda *a, **k: bundle_dir) args = Namespace( no_cloud_tele=False, - model_id=str(bundle_dir), + model_id="org/model", + bits=4, + platform="cpu", token=None, reconvert=False, system=None, @@ -42,13 +46,16 @@ def fake_run(cmd): input_ids="1,2,3", max_new_tokens=4, result_json=str(result_json), + confidence_threshold=None, + cloud_timeout_ms=None, thinking=False, + no_cloud_handoff=False, ) assert run_mod.cmd_run(args) == 0 assert len(calls) == 1 cmd = calls[0] - assert cmd[:2] == [str(fake_chat), str(bundle_dir)] + assert cmd[:2] == [str(fake_run), str(bundle_dir)] assert cmd[cmd.index("--prompt") + 1] == "hi" assert cmd[cmd.index("--image") + 1] == str(image_file) assert cmd[cmd.index("--audio") + 1] == str(audio_file) diff --git a/python/tests/test_cli_transpile_defaults.py b/python/tests/test_cli_transpile_defaults.py index 62ea0c99a..e3c7b78c9 100644 --- a/python/tests/test_cli_transpile_defaults.py +++ b/python/tests/test_cli_transpile_defaults.py @@ -4,318 +4,61 @@ from cactus import cli from cactus.cli import convert as convert_cli -import cactus.cli.model as model_mod -def _write_gemma4_multimodal_config(output_dir: Path) -> None: - output_dir.mkdir(parents=True, exist_ok=True) - (output_dir / "hf_config.json").write_text( - ( - '{"model_type":"gemma4",' - '"architectures":["Gemma4ForConditionalGeneration"],' - '"vision_config":{"model_type":"gemma4_vision"},' - '"audio_config":{"model_type":"gemma4_audio"}}' - ), - encoding="utf-8", - ) - - -def _gemma4_multimodal_extra_args(model_dir: Path, artifact_dir: Path) -> list[str]: - assets_dir = Path(model_mod.__file__).resolve().parent.parent / "assets" - return [ - "--weights-dir", - str(model_dir), - "--artifact-dir", - str(artifact_dir), - "--task", - "multimodal_causal_lm_logits", - "--max-new-tokens", - "512", - "--component-pipeline", - "on", - "--prompt", - model_mod._DEFAULT_MULTIMODAL_PROMPT, - "--components", - "vision_encoder,audio_encoder,lm_encoder,decoder", - "--image-file", - str(assets_dir / "test_monkey.png"), - "--audio-file", - str(assets_dir / "test.wav"), - "--trust-remote-code", - ] - - -def _patch_transpile(monkeypatch, capture): - import cactus.cli.transpile as transpile_mod - - def _fake_run_transpile(model_id, *, extra_args, execute_after_transpile=False, - allow_unconverted_weights=False): - capture.append({ - "model_id": model_id, - "extra_args": list(extra_args), - "execute_after_transpile": execute_after_transpile, - "allow_unconverted_weights": allow_unconverted_weights, - }) - return 0 - - monkeypatch.setattr(transpile_mod, "run_transpile", _fake_run_transpile) - monkeypatch.setattr(model_mod, "run_transpile", _fake_run_transpile, raising=False) - - -def test_cmd_convert_transpiles_into_same_weights_folder(monkeypatch, tmp_path: Path) -> None: - parser = cli.create_parser() - args = parser.parse_args(["convert", "google/gemma-4-E2B-it", "--reconvert"]) - - model_dir = tmp_path / "weights" / "gemma-4-e2b-it" - cq_calls: list[list[str]] = [] - transpile_calls: list[dict] = [] - - def _fake_cq_main(command): - cq_calls.append(list(command)) - _write_gemma4_multimodal_config(model_dir) - return 0 - - monkeypatch.setattr(convert_cli, "get_weights_dir", lambda model_id: model_dir) - _patch_transpile(monkeypatch, transpile_calls) - - import cactus.convert.cli as cq_cli - monkeypatch.setattr(cq_cli, "main", _fake_cq_main) - - rc = convert_cli.cmd_convert(args) - - assert rc == 0 - assert cq_calls[0] == [ - "convert", - "--model", - "google/gemma-4-E2B-it", - "--out", - str(model_dir), - "--bits", - "4", - "--force", - ] - assert len(transpile_calls) == 1 - call = transpile_calls[0] - assert call["model_id"] == "google/gemma-4-E2B-it" - assert call["execute_after_transpile"] is False - assert call["allow_unconverted_weights"] is False - assert call["extra_args"] == _gemma4_multimodal_extra_args(model_dir, model_dir) - assert not (model_dir / "transpile_entrypoints.json").exists() - - -def test_cmd_convert_honors_explicit_output_dir(monkeypatch, tmp_path: Path) -> None: +def test_cmd_convert_does_not_transpile(monkeypatch, tmp_path: Path) -> None: + """`cactus convert` does CQ quantization only; the runtime graph is built + by `cactus transpile` per docs/cactus_transpiler.md.""" parser = cli.create_parser() - output_dir = tmp_path / "custom" - args = parser.parse_args(["convert", "google/gemma-4-E2B-it", str(output_dir), "--reconvert"]) + args = parser.parse_args(["convert", "google/gemma-4-E2B-it", str(tmp_path / "out"), "--reconvert"]) cq_calls: list[list[str]] = [] - transpile_calls: list[dict] = [] def _fake_cq_main(command): cq_calls.append(list(command)) - _write_gemma4_multimodal_config(output_dir) + out_dir = tmp_path / "out" + out_dir.mkdir(parents=True, exist_ok=True) + (out_dir / "config.txt").write_text("model_type=gemma4\n", encoding="utf-8") return 0 - _patch_transpile(monkeypatch, transpile_calls) - import cactus.convert.cli as cq_cli monkeypatch.setattr(cq_cli, "main", _fake_cq_main) - rc = convert_cli.cmd_convert(args) + # cmd_convert should call ensure_weights, never ensure_bundle (the latter + # transpiles as well). Patch ensure_bundle to fail loudly if invoked. + import cactus.cli.model as model_mod - assert rc == 0 - assert cq_calls[0] == [ - "convert", - "--model", - "google/gemma-4-E2B-it", - "--out", - str(output_dir), - "--bits", - "4", - "--force", - ] - assert len(transpile_calls) == 1 - assert transpile_calls[0]["extra_args"] == _gemma4_multimodal_extra_args(output_dir, output_dir) + def _fail_if_called(*a, **kw): + raise AssertionError("cmd_convert must not invoke ensure_bundle (would transpile)") - -def test_cmd_convert_supplies_default_audio_for_parakeet(monkeypatch, tmp_path: Path) -> None: - parser = cli.create_parser() - output_dir = tmp_path / "parakeet" - args = parser.parse_args(["convert", "nvidia/parakeet-tdt-0.6b-v3", str(output_dir)]) - - def _fake_cq_main(command): - output_dir.mkdir(parents=True, exist_ok=True) - (output_dir / "hf_config.json").write_text( - '{"model_type":"parakeet_tdt","architectures":["ParakeetForTDT"]}', - encoding="utf-8", - ) - return 0 - - transpile_calls: list[dict] = [] - _patch_transpile(monkeypatch, transpile_calls) - - import cactus.convert.cli as cq_cli - monkeypatch.setattr(cq_cli, "main", _fake_cq_main) + monkeypatch.setattr(model_mod, "ensure_bundle", _fail_if_called) rc = convert_cli.cmd_convert(args) assert rc == 0 - extra_args = transpile_calls[0]["extra_args"] - assert extra_args[extra_args.index("--task") + 1] == "tdt_transcription" - assert "--audio-file" in extra_args - assert extra_args[extra_args.index("--audio-file") + 1].endswith("python/cactus/assets/test.wav") + assert len(cq_calls) == 1 + assert "--out" in cq_calls[0] -def test_cmd_convert_supplies_default_audio_for_whisper(monkeypatch, tmp_path: Path) -> None: +def test_cli_registers_transpile_command() -> None: + """`cactus transpile` is a first-class subcommand per docs/cactus_transpiler.md.""" parser = cli.create_parser() - output_dir = tmp_path / "whisper" - args = parser.parse_args(["convert", "openai/whisper-small", str(output_dir)]) - - def _fake_cq_main(command): - output_dir.mkdir(parents=True, exist_ok=True) - (output_dir / "hf_config.json").write_text( - '{"model_type":"whisper","architectures":["WhisperForConditionalGeneration"]}', - encoding="utf-8", - ) - return 0 - - transpile_calls: list[dict] = [] - _patch_transpile(monkeypatch, transpile_calls) - - import cactus.convert.cli as cq_cli - monkeypatch.setattr(cq_cli, "main", _fake_cq_main) - - rc = convert_cli.cmd_convert(args) - - assert rc == 0 - extra_args = transpile_calls[0]["extra_args"] - assert extra_args[extra_args.index("--task") + 1] == "seq2seq_transcription" - assert "--audio-file" in extra_args - - -def test_cmd_convert_infers_multimodal_for_qwen_and_lfm(monkeypatch, tmp_path: Path) -> None: - """Qwen/LFM models WITH vision_config get multimodal task.""" - parser = cli.create_parser() - - import cactus.convert.cli as cq_cli - - for model_id, model_type, arch, vision, expected_components in ( - ( - "Qwen/Qwen3-1.7B", - "qwen3", - "Qwen3ForCausalLM", - "qwen3_vision", - "vision_encoder,lm_encoder,decoder,lm_encoder_text_chunk,decoder_prefill_chunk,lm_encoder_step,decoder_media_step,decoder_step", - ), - ( - "LiquidAI/LFM2-VL-450M", - "lfm2_vl", - "Lfm2VlForConditionalGeneration", - "siglip2_vision_model", - "vision_encoder,lm_encoder,decoder", - ), - ): - dir_name = model_id.split("/")[-1].lower() + "_mm" - output_dir = tmp_path / dir_name - args = parser.parse_args(["convert", model_id, str(output_dir), "--reconvert"]) - transpile_calls: list[dict] = [] - - def _fake_cq_main(command, *, output_dir=output_dir, model_type=model_type, arch=arch, vision=vision): - output_dir.mkdir(parents=True, exist_ok=True) - (output_dir / "hf_config.json").write_text( - f'{{"model_type":"{model_type}","architectures":["{arch}"],' - f'"vision_config":{{"model_type":"{vision}"}}}}', - encoding="utf-8", - ) - return 0 - - _patch_transpile(monkeypatch, transpile_calls) - monkeypatch.setattr(cq_cli, "main", _fake_cq_main) - - rc = convert_cli.cmd_convert(args) - - assert rc == 0 - extra_args = transpile_calls[0]["extra_args"] - assert extra_args[extra_args.index("--task") + 1] == "multimodal_causal_lm_logits" - assert extra_args[extra_args.index("--component-pipeline") + 1] == "on" - assert extra_args[extra_args.index("--components") + 1] == expected_components - assert "--image-file" in extra_args - assert "--audio-file" not in extra_args - - -def test_cmd_convert_infers_causal_lm_for_qwen_and_lfm_without_vision(monkeypatch, tmp_path: Path) -> None: - """Qwen/LFM models WITHOUT vision_config get causal_lm_logits.""" - parser = cli.create_parser() - - import cactus.convert.cli as cq_cli - - for model_id, model_type, arch in ( - ("Qwen/Qwen3-1.7B", "qwen3", "Qwen3ForCausalLM"), - ("LiquidAI/LFM2-VL-450M", "lfm2_vl", "Lfm2VlForConditionalGeneration"), - ): - dir_name = model_id.split("/")[-1].lower() - output_dir = tmp_path / dir_name - args = parser.parse_args(["convert", model_id, str(output_dir), "--reconvert"]) - transpile_calls: list[dict] = [] - - def _fake_cq_main(command, *, output_dir=output_dir, model_type=model_type, arch=arch): - output_dir.mkdir(parents=True, exist_ok=True) - (output_dir / "hf_config.json").write_text( - f'{{"model_type":"{model_type}","architectures":["{arch}"]}}', - encoding="utf-8", - ) - return 0 - - _patch_transpile(monkeypatch, transpile_calls) - monkeypatch.setattr(cq_cli, "main", _fake_cq_main) - - rc = convert_cli.cmd_convert(args) - - assert rc == 0 - extra_args = transpile_calls[0]["extra_args"] - assert extra_args[extra_args.index("--task") + 1] == "causal_lm_logits" - - -def test_cmd_convert_infers_multimodal_components_from_vision_config(monkeypatch, tmp_path: Path) -> None: - parser = cli.create_parser() - output_dir = tmp_path / "lfm2-vl" - args = parser.parse_args(["convert", "LiquidAI/LFM2-VL-450M", str(output_dir)]) - - def _fake_cq_main(command): - output_dir.mkdir(parents=True, exist_ok=True) - (output_dir / "hf_config.json").write_text( - ( - '{"model_type":"lfm2_vl",' - '"architectures":["Lfm2VlForConditionalGeneration"],' - '"vision_config":{"model_type":"siglip2_vision_model"}}' - ), - encoding="utf-8", - ) - return 0 - - transpile_calls: list[dict] = [] - _patch_transpile(monkeypatch, transpile_calls) - - import cactus.convert.cli as cq_cli - monkeypatch.setattr(cq_cli, "main", _fake_cq_main) - - rc = convert_cli.cmd_convert(args) - - assert rc == 0 - assert len(transpile_calls) == 1 - extra_args = transpile_calls[0]["extra_args"] - assert extra_args[extra_args.index("--task") + 1] == "multimodal_causal_lm_logits" - assert extra_args[extra_args.index("--component-pipeline") + 1] == "on" - assert extra_args[extra_args.index("--components") + 1] == "vision_encoder,lm_encoder,decoder" - assert "--image-file" in extra_args - assert "--audio-file" not in extra_args + args = parser.parse_args(["transpile", "google/gemma-4-E2B-it", + "--weights-dir", "/tmp/x", "--task", "causal_lm_logits"]) + assert args.command == "transpile" + assert args.model_id == "google/gemma-4-E2B-it" + assert args.weights_dir == "/tmp/x" + assert args.task == "causal_lm_logits" -def test_cli_no_longer_registers_transpile_command() -> None: +def test_run_accepts_local_bundle_path() -> None: + """`cactus run` accepts a HF id (org/model) OR a local path. Bare names + like 'whisper-base' (no slash) are rejected.""" parser = cli.create_parser() - try: - parser.parse_args(["transpile", "gemma4"]) - except SystemExit as exc: - assert exc.code != 0 - else: - raise AssertionError("transpile should no longer be a public CLI command") + args = parser.parse_args(["run", "/tmp/bundle", + "--prompt", "hi", "--system", "be brief", "--thinking"]) + assert args.command == "run" + assert args.model_id == "/tmp/bundle" + assert args.prompt == "hi" + assert args.system == "be brief" + assert args.thinking is True diff --git a/python/tests/test_download.py b/python/tests/test_download.py index 3e72044cf..997406ecb 100644 --- a/python/tests/test_download.py +++ b/python/tests/test_download.py @@ -10,12 +10,12 @@ from cactus.cli.utils import ( archives_from_repo_files, - parse_cq_bits, + parse_cq_variant, promote_single_root, resolve_archive, safe_extract_archive, suggested_cq_repo, - validate_extracted_cq, + validate_extracted_bundle, verify_archive_sha256, ) @@ -41,12 +41,16 @@ def test_suggested_cq_repo_strips_existing_cq_suffix(self): "Cactus-Compute/model", ) - def test_parse_cq_bits(self): - self.assertEqual(parse_cq_bits("gemma-4-E2B-it-cq1.zip"), 1) - self.assertEqual(parse_cq_bits("gemma-4-E2B-it-cq4.zip"), 4) - self.assertEqual(parse_cq_bits("gemma-4-E2B-it-CQ3.tar.gz"), 3) - self.assertIsNone(parse_cq_bits("README.md")) - self.assertIsNone(parse_cq_bits("L4V4A4.zip")) + def test_parse_cq_variant(self): + self.assertEqual(parse_cq_variant("gemma-4-E2B-it-cq1.zip"), (1, None)) + self.assertEqual(parse_cq_variant("gemma-4-E2B-it-cq4.zip"), (4, None)) + self.assertEqual(parse_cq_variant("gemma-4-E2B-it-CQ3.tar.gz"), (3, None)) + self.assertEqual(parse_cq_variant("gemma-4-E2B-it-cq4-apple.tar.gz"), (4, "apple")) + self.assertEqual(parse_cq_variant("model-cq2-APPLE.zip"), (2, "apple")) + # Forward-compatible: any vendor name matching [a-z]+ parses. + self.assertEqual(parse_cq_variant("model-cq4-qualcomm.tar.gz"), (4, "qualcomm")) + self.assertIsNone(parse_cq_variant("README.md")) + self.assertIsNone(parse_cq_variant("L4V4A4.zip")) def test_archives_from_repo_files(self): files = [ @@ -54,29 +58,39 @@ def test_archives_from_repo_files(self): "gemma-4-E2B-it-cq2.zip", "gemma-4-E2B-it-cq3.zip", "gemma-4-E2B-it-cq4.zip", + "gemma-4-E2B-it-cq4-apple.zip", "README.md", "config.json", ] sizes = {"gemma-4-E2B-it-cq4.zip": 500_000_000} archives = archives_from_repo_files(files, sizes=sizes) - self.assertEqual(len(archives), 4) + self.assertEqual(len(archives), 5) self.assertEqual(archives[0].bits, 1) - self.assertEqual(archives[3].bits, 4) + self.assertIsNone(archives[0].platform) + # Generic CPU bundle sorts BEFORE vendor-specific for the same bits. + self.assertEqual((archives[3].bits, archives[3].platform), (4, None)) + self.assertEqual((archives[4].bits, archives[4].platform), (4, "apple")) self.assertEqual(archives[3].size, 500_000_000) - self.assertIsNone(archives[0].size) def test_resolve_archive_finds_match(self): - files = ["model-cq1.zip", "model-cq2.zip", "model-cq3.zip", "model-cq4.zip"] + files = ["model-cq1.zip", "model-cq2.zip", "model-cq3.zip", "model-cq4.zip", "model-cq4-apple.zip"] archives = archives_from_repo_files(files) resolution = resolve_archive("Cactus-Compute/model", "model", archives, 4) self.assertEqual(resolution.archive.filename, "model-cq4.zip") self.assertEqual(resolution.archive.bits, 4) + self.assertIsNone(resolution.archive.platform) - def test_resolve_archive_missing_bits_errors(self): + apple = resolve_archive("Cactus-Compute/model", "model", archives, 4, platform="apple") + self.assertEqual(apple.archive.filename, "model-cq4-apple.zip") + self.assertEqual(apple.archive.platform, "apple") + + def test_resolve_archive_missing_variant_errors(self): files = ["model-cq1.zip", "model-cq2.zip"] archives = archives_from_repo_files(files) with self.assertRaisesRegex(RuntimeError, "cq4 not found"): resolve_archive("Cactus-Compute/model", "model", archives, 4) + with self.assertRaisesRegex(RuntimeError, "cq2-apple not found"): + resolve_archive("Cactus-Compute/model", "model", archives, 2, platform="apple") def test_resolve_archive_empty_errors(self): with self.assertRaisesRegex(RuntimeError, "No CQ archives"): @@ -99,20 +113,25 @@ def _write_minimal_package(self, base: Path): (base / "special_tokens.json").write_text("{}", encoding="utf-8") (base / "tokenizer.json").write_text("{}", encoding="utf-8") (base / "merges.txt").write_text("", encoding="utf-8") + components = base / "components" + components.mkdir(parents=True, exist_ok=True) + (components / "manifest.json").write_text("{}", encoding="utf-8") def test_zip_extract_and_promote_single_root(self): package = self.root / "pkg" self._write_minimal_package(package / "model-cq4") archive = self.root / "model-cq4.zip" + src = package / "model-cq4" with zipfile.ZipFile(archive, "w") as zf: - for path in (package / "model-cq4").iterdir(): - zf.write(path, f"model-cq4/{path.name}") + for path in src.rglob("*"): + if path.is_file(): + zf.write(path, f"model-cq4/{path.relative_to(src)}") out = self.root / "out" out.mkdir() safe_extract_archive(archive, out) promote_single_root(out) - validate_extracted_cq(out) + validate_extracted_bundle(out) self.assertTrue((out / "config.txt").exists()) def test_promote_single_root_rejects_ambiguous_layout(self): @@ -122,12 +141,12 @@ def test_promote_single_root_rejects_ambiguous_layout(self): with self.assertRaisesRegex(RuntimeError, "archive root or under one top-level directory"): promote_single_root(out) - def test_validate_extracted_cq_requires_tokenizer_sidecars(self): + def test_validate_extracted_bundle_requires_tokenizer_sidecars(self): package = self.root / "pkg" self._write_minimal_package(package) (package / "tokenizer.json").unlink() with self.assertRaisesRegex(RuntimeError, "missing tokenizer sidecar"): - validate_extracted_cq(package) + validate_extracted_bundle(package) def test_verify_archive_sha256_rejects_mismatch(self): archive = self.root / "model-cq4.zip" diff --git a/python/tests/test_model.py b/python/tests/test_model.py index 94af3f5b6..543243ae7 100644 --- a/python/tests/test_model.py +++ b/python/tests/test_model.py @@ -19,19 +19,9 @@ from cactus.cli.model import ensure_bundle -def _find_asset(name): - for candidate in ( - PROJECT_ROOT / "python" / "cactus" / "assets" / name, - PROJECT_ROOT / "cactus-engine" / "tests" / "assets" / name, - PROJECT_ROOT / "tests" / "assets" / name, - ): - if candidate.exists(): - return candidate - return None - - -_TEST_IMAGE = _find_asset("test_monkey.png") -_TEST_AUDIO = _find_asset("test.wav") +_ASSETS_DIR = PROJECT_ROOT / "cactus-engine" / "tests" / "assets" +_TEST_IMAGE = _ASSETS_DIR / "test_monkey.png" +_TEST_AUDIO = _ASSETS_DIR / "test.wav" class TestVLMModel(unittest.TestCase): @@ -52,13 +42,11 @@ def test_text_completion(self): self.assertTrue(result.get("success", False)) self.assertGreater(len(result.get("response", "")), 0) - @unittest.skipUnless(_TEST_IMAGE is not None, "test_monkey.png not found") def test_image_embedding(self): embedding = cactus_image_embed(self.model, str(_TEST_IMAGE)) self.assertIsInstance(embedding, list) self.assertGreater(len(embedding), 0) - @unittest.skipUnless(_TEST_IMAGE is not None, "test_monkey.png not found") def test_vlm_image_completion(self): messages = [{ "role": "user", @@ -83,7 +71,6 @@ def setUpClass(cls): def tearDownClass(cls): cactus_destroy(cls.model) - @unittest.skipUnless(_TEST_AUDIO is not None, "test.wav not found") def test_transcription(self): prompt = "<|startoftranscript|><|en|><|transcribe|><|notimestamps|>" result = cactus_transcribe( @@ -99,7 +86,6 @@ def test_transcription(self): self.assertTrue(result.get("success", False)) self.assertGreater(len(result.get("response", "")), 0) - @unittest.skipUnless(_TEST_AUDIO is not None, "test.wav not found") def test_audio_embedding(self): embedding = cactus_audio_embed(self.model, str(_TEST_AUDIO)) self.assertIsInstance(embedding, list) @@ -141,12 +127,9 @@ def test_retrieval_discrimination(self): self.assertGreater(_cosine(query, relevant), _cosine(query, unrelated)) def test_hf_parity(self): - try: - import torch - import torch.nn.functional as F - from transformers import AutoModel, AutoTokenizer - except ImportError: - self.skipTest("transformers/torch not available for HF parity check") + import torch + import torch.nn.functional as F + from transformers import AutoModel, AutoTokenizer texts = [ "search_query: What is the capital of France?", diff --git a/python/tests/test_server.py b/python/tests/test_server.py index 5f570e070..d290a3a3c 100644 --- a/python/tests/test_server.py +++ b/python/tests/test_server.py @@ -3,6 +3,9 @@ import asyncio import pytest + +pytest.importorskip("fastapi") + from fastapi import HTTPException from fastapi.testclient import TestClient @@ -118,8 +121,12 @@ def fail_complete(*args, **kwargs): body = "".join(res.iter_text()) assert res.status_code == 200 - assert "event: error" in body + # OpenAI streaming spec carries errors in a `data:` JSON line, not in + # `event:` lines. We also emit a final chunk with finish_reason so + # clients don't hang before [DONE]. + assert '"error"' in body assert "boom" in body + assert '"finish_reason": "stop"' in body assert "data: [DONE]" in body diff --git a/python/tests/test_server_live.py b/python/tests/test_server_live.py index a2655f057..65fad4a87 100644 --- a/python/tests/test_server_live.py +++ b/python/tests/test_server_live.py @@ -437,18 +437,19 @@ def test_live_transcription_rejects_non_wav(live_server) -> None: EMBED_TYPES = {"bert", "nomic"} -def _find_embed_bundle() -> Path | None: +def _find_embed_bundle() -> Path: for candidate in sorted(WEIGHTS.iterdir()) if WEIGHTS.exists() else []: if candidate.is_dir() and _valid_bundle(candidate) and _read_model_type(candidate) in EMBED_TYPES: return candidate - return None + raise RuntimeError( + "No embedding (nomic/bert) bundle under weights/; " + "run `cactus convert nomic-ai/nomic-embed-text-v2-moe`" + ) @pytest.fixture(scope="module") def embed_server(): bundle = _find_embed_bundle() - if bundle is None: - pytest.skip("No embedding (nomic/bert) bundle under weights/; run `cactus convert nomic-ai/nomic-embed-text-v2-moe`") port = _free_port() proc = _start_server(bundle, port) base_url = f"http://127.0.0.1:{port}" diff --git a/setup b/setup index 0cd57c34b..4539a168c 100755 --- a/setup +++ b/setup @@ -82,15 +82,6 @@ else echo -e "${YELLOW}Warning: requirements.txt not found${NC}" fi -echo "" -echo -e "${BLUE}Step 3b: Upgrading Transformers...${NC}" -if python3 -m pip install --upgrade "transformers==5.5.4" -q; then - echo -e "${GREEN}✓ Transformers upgraded to 5.5.4${NC}" -else - echo -e "${RED}Failed to upgrade Transformers${NC}" - return 1 -fi - if [ -f "$PARENT_REQUIREMENTS" ] && [ "$PARENT_REQUIREMENTS" != "$REQUIREMENTS_FILE" ]; then echo "" echo -e "${BLUE}Detected parent repo requirements at: $PARENT_REQUIREMENTS${NC}" diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt deleted file mode 100644 index 23b9ae586..000000000 --- a/tests/CMakeLists.txt +++ /dev/null @@ -1,149 +0,0 @@ -cmake_minimum_required(VERSION 3.10) -project(CactusTests LANGUAGES CXX) - -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED True) - -function(cactus_require_vendored_curl include_dir library_path platform_label) - if(NOT EXISTS "${include_dir}/curl/curl.h" OR NOT EXISTS "${library_path}") - message(WARNING "==============================================================================") - message(WARNING " Vendored libcurl is required for ${platform_label} tests but is missing.") - message(WARNING " Expected header: ${include_dir}/curl/curl.h") - message(WARNING " Expected library: ${library_path}") - message(WARNING " Populate libs/curl from your local curl build before running cactus test.") - message(WARNING "==============================================================================") - message(FATAL_ERROR "Missing vendored libcurl artifacts for ${platform_label} tests") - endif() -endfunction() - -if(APPLE) - set(CMAKE_OSX_ARCHITECTURES "arm64") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -arch arm64 -pthread -Wall -Wextra -pedantic -O3") - set(CACTUS_CURL_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../libs/curl" CACHE PATH "Path to vendored libcurl artifacts") - set(CACTUS_CURL_INCLUDE_DIR "${CACTUS_CURL_ROOT}/include") - set(CACTUS_CURL_LIBRARY_MACOS "${CACTUS_CURL_ROOT}/macos/libcurl.a") - cactus_require_vendored_curl("${CACTUS_CURL_INCLUDE_DIR}" "${CACTUS_CURL_LIBRARY_MACOS}" "macOS") -else() - find_package(CURL) -endif() - -set(CACTUS_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../cactus) -set(CACTUS_BUILD_DIR ${CACTUS_ROOT}/build) - -find_library(CACTUS_LIB - NAMES cactus - HINTS ${CACTUS_BUILD_DIR} ${CACTUS_BUILD_DIR}/lib - REQUIRED -) - -set(MMAN_LIB "") -if(NOT APPLE) - find_library(MMAN_LIB mman) - if(MMAN_LIB) - message(STATUS "Found libmman: ${MMAN_LIB}") - else() - message(STATUS "libmman not found; assuming mmap symbols are provided by the C runtime") - endif() -else() - find_library(COREML_FRAMEWORK CoreML REQUIRED) - find_library(FOUNDATION_FRAMEWORK Foundation REQUIRED) - find_library(ACCELERATE_FRAMEWORK Accelerate REQUIRED) - find_library(SECURITY_FRAMEWORK Security REQUIRED) - find_library(SYSTEMCONFIGURATION_FRAMEWORK SystemConfiguration REQUIRED) - find_library(CFNETWORK_FRAMEWORK CFNetwork REQUIRED) -endif() - -find_package(SDL2) -if(SDL2_FOUND) - message(STATUS "Found SDL2: ${SDL2_LIBRARIES}") -else() - message(STATUS "SDL2 not found; microphone tests will be skipped") -endif() - -file(GLOB TEST_SOURCES CONFIGURE_DEPENDS "test_*.cpp") -list(REMOVE_ITEM TEST_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/test_utils.cpp") - -foreach(TEST_FILE ${TEST_SOURCES}) - - get_filename_component(TEST_NAME ${TEST_FILE} NAME_WE) - add_executable(${TEST_NAME} ${TEST_FILE} test_utils.cpp) - target_link_libraries(${TEST_NAME} PRIVATE ${CACTUS_LIB}) - - - if(MMAN_LIB) - target_link_libraries(${TEST_NAME} PRIVATE ${MMAN_LIB}) - message(STATUS "Test ${TEST_NAME} will link with -lmman") - endif() - - if(SDL2_FOUND) - target_link_libraries(${TEST_NAME} PRIVATE ${SDL2_LIBRARIES}) - target_include_directories(${TEST_NAME} PRIVATE ${SDL2_INCLUDE_DIRS}) - target_compile_definitions(${TEST_NAME} PRIVATE HAVE_SDL2) - message(STATUS "Test ${TEST_NAME} will link with SDL2") - endif() - - if(APPLE) - target_link_libraries(${TEST_NAME} PRIVATE ${CACTUS_CURL_LIBRARY_MACOS}) - target_include_directories(${TEST_NAME} PRIVATE ${CACTUS_CURL_INCLUDE_DIR}) - message(STATUS "Test ${TEST_NAME} will link with libcurl") - elseif(CURL_FOUND) - target_link_libraries(${TEST_NAME} PRIVATE ${CURL_LIBRARIES}) - target_include_directories(${TEST_NAME} PRIVATE ${CURL_INCLUDE_DIRS}) - message(STATUS "Test ${TEST_NAME} will link with system libcurl") - endif() - if(APPLE) - target_link_libraries(${TEST_NAME} PRIVATE - ${ACCELERATE_FRAMEWORK} - ${COREML_FRAMEWORK} - ${FOUNDATION_FRAMEWORK} - ${SECURITY_FRAMEWORK} - ${SYSTEMCONFIGURATION_FRAMEWORK} - ${CFNETWORK_FRAMEWORK} - ) - endif() - - target_include_directories(${TEST_NAME} PRIVATE ${CACTUS_ROOT}) - - message(STATUS "Added test: ${TEST_NAME}") -endforeach() - -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) - -foreach(APP_NAME chat asr) - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${APP_NAME}.cpp") - add_executable(${APP_NAME} ${APP_NAME}.cpp) - target_link_libraries(${APP_NAME} PRIVATE ${CACTUS_LIB}) - - if(MMAN_LIB) - target_link_libraries(${APP_NAME} PRIVATE ${MMAN_LIB}) - endif() - - if(APPLE) - target_link_libraries(${APP_NAME} PRIVATE ${CACTUS_CURL_LIBRARY_MACOS}) - target_include_directories(${APP_NAME} PRIVATE ${CACTUS_CURL_INCLUDE_DIR}) - elseif(CURL_FOUND) - target_link_libraries(${APP_NAME} PRIVATE ${CURL_LIBRARIES}) - target_include_directories(${APP_NAME} PRIVATE ${CURL_INCLUDE_DIRS}) - endif() - if(APPLE) - target_link_libraries(${APP_NAME} PRIVATE - ${ACCELERATE_FRAMEWORK} - ${COREML_FRAMEWORK} - ${FOUNDATION_FRAMEWORK} - ${SECURITY_FRAMEWORK} - ${SYSTEMCONFIGURATION_FRAMEWORK} - ${CFNETWORK_FRAMEWORK} - ) - endif() - - if(SDL2_FOUND AND (APP_NAME STREQUAL "asr" OR APP_NAME STREQUAL "chat")) - target_link_libraries(${APP_NAME} PRIVATE ${SDL2_LIBRARIES}) - target_include_directories(${APP_NAME} PRIVATE ${SDL2_INCLUDE_DIRS}) - target_compile_definitions(${APP_NAME} PRIVATE HAVE_SDL2) - message(STATUS "${APP_NAME} will link with SDL2 for live audio") - endif() - - target_include_directories(${APP_NAME} PRIVATE ${CACTUS_ROOT}) - message(STATUS "Added executable: ${APP_NAME}") - endif() -endforeach() diff --git a/tests/android/CMakeLists.txt b/tests/android/CMakeLists.txt deleted file mode 100644 index ca45910a4..000000000 --- a/tests/android/CMakeLists.txt +++ /dev/null @@ -1,77 +0,0 @@ -cmake_minimum_required(VERSION 3.10) -project(CactusAndroidTests LANGUAGES CXX) - -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED True) -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread -Wall -Wextra -pedantic -O3") - -set(CACTUS_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../../cactus) -set(ANDROID_BUILD_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../android/build) -set(CACTUS_LIB ${ANDROID_BUILD_DIR}/libcactus_static.a) -set(CACTUS_CURL_ROOT "$ENV{CACTUS_CURL_ROOT}") -if(NOT CACTUS_CURL_ROOT) - set(CACTUS_CURL_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../../libs/curl) -endif() -set(CACTUS_CURL_INCLUDE_DIR "${CACTUS_CURL_ROOT}/include") -set(CACTUS_CURL_LIBRARY "${CACTUS_CURL_ROOT}/android/arm64-v8a/libcurl.a") -set(CACTUS_MBEDTLS_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../../android/mbedtls/arm64-v8a) -if(NOT EXISTS "${CACTUS_MBEDTLS_ROOT}") - set(CACTUS_MBEDTLS_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../../libs/mbedtls/android/arm64-v8a) -endif() -set(CACTUS_MBEDTLS_CRYPTO_LIBRARY "${CACTUS_MBEDTLS_ROOT}/lib/libmbedcrypto.a") -set(CACTUS_MBEDTLS_TLS_LIBRARY "${CACTUS_MBEDTLS_ROOT}/lib/libmbedtls.a") -set(CACTUS_MBEDTLS_X509_LIBRARY "${CACTUS_MBEDTLS_ROOT}/lib/libmbedx509.a") - -if(NOT EXISTS ${CACTUS_LIB}) - message(FATAL_ERROR "Cactus library not found at: ${CACTUS_LIB}") -endif() - -message(STATUS "Using Cactus library: ${CACTUS_LIB}") -message(STATUS "Using vendored curl root: ${CACTUS_CURL_ROOT}") - -set(TESTS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/..) -file(GLOB TEST_SOURCES "${TESTS_DIR}/test*.cpp") -list(REMOVE_ITEM TEST_SOURCES "${TESTS_DIR}/test_utils.cpp") - -function(link_vendored_curl_if_present target_name) - if(CACTUS_CURL_ROOT AND EXISTS "${CACTUS_CURL_INCLUDE_DIR}/curl/curl.h" AND EXISTS "${CACTUS_CURL_LIBRARY}") - if(NOT EXISTS "${CACTUS_MBEDTLS_CRYPTO_LIBRARY}" OR - NOT EXISTS "${CACTUS_MBEDTLS_TLS_LIBRARY}" OR - NOT EXISTS "${CACTUS_MBEDTLS_X509_LIBRARY}") - message(FATAL_ERROR - "Vendored Android libcurl is present but mbedTLS static libs are missing.\n" - "Expected:\n" - " ${CACTUS_MBEDTLS_CRYPTO_LIBRARY}\n" - " ${CACTUS_MBEDTLS_TLS_LIBRARY}\n" - " ${CACTUS_MBEDTLS_X509_LIBRARY}") - endif() - target_include_directories(${target_name} PRIVATE "${CACTUS_CURL_INCLUDE_DIR}") - target_link_libraries(${target_name} PRIVATE - "${CACTUS_CURL_LIBRARY}" - "${CACTUS_MBEDTLS_TLS_LIBRARY}" - "${CACTUS_MBEDTLS_X509_LIBRARY}" - "${CACTUS_MBEDTLS_CRYPTO_LIBRARY}" - ) - message(STATUS "Target ${target_name} will link vendored libcurl: ${CACTUS_CURL_LIBRARY}") - endif() -endfunction() - -foreach(TEST_FILE ${TEST_SOURCES}) - get_filename_component(TEST_NAME ${TEST_FILE} NAME_WE) - add_executable(${TEST_NAME} - ${TEST_FILE} - ${TESTS_DIR}/test_utils.cpp - ) - target_link_libraries(${TEST_NAME} PRIVATE ${CACTUS_LIB}) - link_vendored_curl_if_present(${TEST_NAME}) - target_include_directories(${TEST_NAME} PRIVATE ${CACTUS_ROOT}) - message(STATUS "Added Android test: ${TEST_NAME}") -endforeach() - -add_executable(asr "${TESTS_DIR}/asr.cpp") -target_link_libraries(asr PRIVATE ${CACTUS_LIB}) -link_vendored_curl_if_present(asr) -target_include_directories(asr PRIVATE ${CACTUS_ROOT}) -message(STATUS "Added Android executable: asr") - -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) diff --git a/tests/android/run.sh b/tests/android/run.sh deleted file mode 100755 index d8230e0d0..000000000 --- a/tests/android/run.sh +++ /dev/null @@ -1,314 +0,0 @@ -#!/bin/bash - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -CACTUS_CURL_ROOT="${CACTUS_CURL_ROOT:-$PROJECT_ROOT/libs/curl}" -export CACTUS_CURL_ROOT - -MODEL_NAME="$1" -TRANSCRIBE_MODEL_NAME="$2" - -echo "Running Cactus tests on Android..." -echo "============================" - -if ! command -v adb; then - echo "adb not found" - echo "Install Android SDK Platform Tools and ensure it's in your PATH" - echo "Installation:" - echo " - Via Android Studio: Settings > Android SDK > SDK Tools > Android SDK Platform-Tools" - exit 1 -fi - -echo "" -echo "Step 1: Selecting Android device..." - -adb start-server - -connected_devices=$(adb devices | grep -E "device$|emulator" | grep -v "^List" | awk '{print $1}') - -if command -v emulator; then - available_emulators=$(emulator -list-avds | grep -v "^INFO" || true) -else - available_emulators="" -fi - -all_devices="" - -if [ -n "$connected_devices" ]; then - while read -r device_id; do - if [[ "$device_id" == emulator-* ]]; then - device_model=$(adb -s "$device_id" shell getprop ro.product.model | tr -d '\r') - device_android=$(adb -s "$device_id" shell getprop ro.build.version.release | tr -d '\r') - avd_name=$(adb -s "$device_id" shell getprop ro.kernel.qemu.avd_name | tr -d '\r') - if [ -n "$avd_name" ]; then - all_devices=$(printf "%s\n%s|emulator|%s|running|%s|%s" "$all_devices" "$avd_name" "$device_id" "$device_model" "$device_android") - else - all_devices=$(printf "%s\n%s|emulator|%s|running|%s|%s" "$all_devices" "$device_id" "$device_id" "$device_model" "$device_android") - fi - else - device_model=$(adb -s "$device_id" shell getprop ro.product.model | tr -d '\r') - device_android=$(adb -s "$device_id" shell getprop ro.build.version.release | tr -d '\r') - all_devices=$(printf "%s\n%s|device|%s|running|%s|%s" "$all_devices" "$device_model" "$device_id" "$device_model" "$device_android") - fi - done <<< "$connected_devices" -fi - -if [ -n "$available_emulators" ]; then - while read -r avd_name; do - if ! echo "$all_devices" | grep -q "^$avd_name|emulator|"; then - all_devices=$(printf "%s\n%s|emulator||offline||" "$all_devices" "$avd_name") - fi - done <<< "$available_emulators" -fi - -all_devices=$(echo "$all_devices" | grep -v '^$') - -if [ -z "$all_devices" ]; then - echo "No devices or emulators found" - echo "To use an emulator:" - echo " - Create one through Android Studio AVD Manager" - echo " - Or start one with: emulator -avd " - echo "To use a physical device:" - echo " - Connect via USB" - echo " - Enable USB debugging in Developer Options" - echo " - Authorize the computer when prompted" - exit 1 -fi - -physical_count=$(echo "$all_devices" | grep -c '|device|' || true) -device_num=0 - -if [ "$physical_count" -gt 0 ]; then - echo "Devices:" - while IFS='|' read -r name type device_id status model android_version; do - if [ "$type" = "device" ]; then - device_num=$((device_num + 1)) - if [ -n "$android_version" ]; then - printf " %2d. %s (Android %s)\n" "$device_num" "$name" "$android_version" - else - printf " %2d. %s\n" "$device_num" "$name" - fi - fi - done <<< "$all_devices" - echo "" -fi - -echo "Emulators:" -while IFS='|' read -r name type device_id status model android_version; do - if [ "$type" = "emulator" ]; then - device_num=$((device_num + 1)) - if [ "$status" = "offline" ]; then - printf " %2d. %s [not running]\n" "$device_num" "$name" - else - if [ -n "$android_version" ]; then - printf " %2d. %s (Android %s)\n" "$device_num" "$name" "$android_version" - else - printf " %2d. %s\n" "$device_num" "$name" - fi - fi - fi -done <<< "$all_devices" - -echo "" -read -p "Select device number (1-$device_num): " device_number - -if ! [[ "$device_number" =~ ^[0-9]+$ ]] || [ "$device_number" -lt 1 ] || [ "$device_number" -gt "$device_num" ]; then - echo "Invalid selection" - exit 1 -fi - -selected_line=$(echo "$all_devices" | sed -n "${device_number}p") -device_name=$(echo "$selected_line" | cut -d'|' -f1) -device_type=$(echo "$selected_line" | cut -d'|' -f2) -DEVICE_ID=$(echo "$selected_line" | cut -d'|' -f3) -device_status=$(echo "$selected_line" | cut -d'|' -f4) - -if [ -z "$device_name" ]; then - echo "Could not parse device information" - exit 1 -fi - -echo "" -if [ "$device_type" = "emulator" ]; then - if [ "$device_status" = "offline" ]; then - echo "Selected: $device_name (Emulator - Not Running)" - echo "Starting emulator..." - - if ! command -v emulator; then - echo "Emulator command not found" - echo "Add Android SDK emulator to your PATH" - echo "Location: \$ANDROID_HOME/emulator or \$ANDROID_SDK_ROOT/emulator" - exit 1 - fi - - emulator -avd "$device_name" -no-snapshot-load & - emulator_pid=$! - - echo "Waiting for emulator to boot..." - timeout=300 - elapsed=0 - while [ $elapsed -lt $timeout ]; do - sleep 2 - elapsed=$((elapsed + 2)) - - DEVICE_ID=$(adb devices | grep emulator | head -1 | awk '{print $1}') - - if [ -n "$DEVICE_ID" ]; then - boot_complete=$(adb -s "$DEVICE_ID" shell getprop sys.boot_completed | tr -d '\r') - if [ "$boot_complete" = "1" ]; then - echo "Emulator started successfully" - break - fi - fi - done - - if [ -z "$DEVICE_ID" ] || [ "$boot_complete" != "1" ]; then - echo "Failed to start emulator (timeout)" - exit 1 - fi - else - echo "Selected: $device_name (Emulator)" - fi -else - echo "Selected: $device_name (Device)" -fi - -if ! adb -s "$DEVICE_ID" shell echo "test"; then - echo "Device not responding" - echo "Ensure the device is connected and USB debugging is authorized" - exit 1 -fi - -echo "" -echo "Step 2: Building Cactus library for Android..." - -if ! "$PROJECT_ROOT/android/build.sh"; then - echo "Failed to build Cactus library" - exit 1 -fi - -echo "" -echo "Step 3: Building Android tests..." - -android_test_dir="$SCRIPT_DIR" -android_build_dir="$android_test_dir/build" - -if [ -z "$ANDROID_NDK_HOME" ]; then - if [ -n "$ANDROID_HOME" ]; then - ANDROID_NDK_HOME=$(ls -d "$ANDROID_HOME/ndk/"* | sort -V | tail -1) - elif [ -d "$HOME/Library/Android/sdk" ]; then - ANDROID_NDK_HOME=$(ls -d "$HOME/Library/Android/sdk/ndk/"* | sort -V | tail -1) - fi -fi - -if [ -z "$ANDROID_NDK_HOME" ] || [ ! -d "$ANDROID_NDK_HOME" ]; then - echo "Android NDK not found" - echo "Install the NDK through Android Studio: Settings > Android SDK > SDK Tools > NDK" - exit 1 -fi - -cmake_toolchain_file="$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake" -android_platform=${ANDROID_PLATFORM:-android-21} -android_abi="arm64-v8a" - -rm -rf "$android_build_dir" -mkdir -p "$android_build_dir" - -if ! cmake -S "$android_test_dir" -B "$android_build_dir" \ - -DCMAKE_TOOLCHAIN_FILE="$cmake_toolchain_file" \ - -DANDROID_ABI="$android_abi" \ - -DANDROID_PLATFORM="$android_platform" \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_RULE_MESSAGES=OFF \ - -DCMAKE_VERBOSE_MAKEFILE=OFF; then - echo "Failed to configure tests" - exit 1 -fi - -n_jobs=$(nproc || sysctl -n hw.logicalcpu || echo 4) -if ! cmake --build "$android_build_dir" -j "$n_jobs"; then - echo "Failed to build tests" - exit 1 -fi - -echo "Discovering test executables..." -all_test_executables=($(find "$android_build_dir" -maxdepth 1 -name "test_*" -type f | sort)) - -if [ ${#all_test_executables[@]} -eq 0 ]; then - echo "No test executables found" - exit 1 -fi - -test_executables=() -for test_exe in "${all_test_executables[@]}"; do - test_name=$(basename "$test_exe" | sed 's/^test_//') - if [ -z "${CACTUS_TEST_ONLY:-}" ] || [ "$test_name" = "$CACTUS_TEST_ONLY" ]; then - test_executables+=("$test_exe") - fi -done - -if [ ${#test_executables[@]} -eq 0 ]; then - echo "No test executables match filter: $CACTUS_TEST_ONLY" - exit 1 -fi - -echo "Found ${#test_executables[@]} test executable(s)" - -echo "" -echo "Step 4: Deploying to device..." - -model_dir=$(echo "$MODEL_NAME" | sed 's|.*/||' | tr '[:upper:]' '[:lower:]') -transcribe_model_dir=$(echo "$TRANSCRIBE_MODEL_NAME" | sed 's|.*/||' | tr '[:upper:]' '[:lower:]') -model_src="$PROJECT_ROOT/weights/$model_dir" -transcribe_model_src="$PROJECT_ROOT/weights/$transcribe_model_dir" -assets_src="$PROJECT_ROOT/tests/assets" - -device_test_dir="/data/local/tmp/cactus_tests" -device_model_dir="/data/local/tmp/cactus_models" -device_assets_dir="/data/local/tmp/cactus_assets" - -adb -s "$DEVICE_ID" shell "mkdir -p $device_test_dir $device_model_dir $device_assets_dir" - -echo "Pushing model weights..." -adb -s "$DEVICE_ID" push "$model_src" "$device_model_dir/" -adb -s "$DEVICE_ID" push "$transcribe_model_src" "$device_model_dir/" - -echo "Pushing test assets..." -adb -s "$DEVICE_ID" push "$assets_src" "$device_assets_dir/" - -echo "Pushing test executables..." -for test_exe in "${test_executables[@]}"; do - test_name=$(basename "$test_exe") - adb -s "$DEVICE_ID" push "$test_exe" "$device_test_dir/" - adb -s "$DEVICE_ID" shell "chmod +x $device_test_dir/$test_name" -done - -echo "" -echo "Step 5: Running tests..." -echo "------------------------" -echo "Using model path: $device_model_dir/$model_dir" -echo "Using transcribe model path: $device_model_dir/$transcribe_model_dir" -echo "Using assets path: $device_assets_dir/assets" - -failures=0 -for test_exe in "${test_executables[@]}"; do - test_name=$(basename "$test_exe") - - if ! adb -s "$DEVICE_ID" shell "cd $device_test_dir && \ - export CACTUS_TEST_MODEL=$device_model_dir/$model_dir && \ - export CACTUS_TEST_TRANSCRIBE_MODEL=$device_model_dir/$transcribe_model_dir && \ - export CACTUS_TEST_ASSETS=$device_assets_dir/assets && \ - export CACTUS_INDEX_PATH=$device_assets_dir/assets && \ - export CACTUS_NO_CLOUD_TELE=${CACTUS_NO_CLOUD_TELE} && \ - ./$test_name"; then - echo "Test executable failed: $test_name" - failures=$((failures + 1)) - fi -done - -if [ "$failures" -ne 0 ]; then - echo "$failures test executable(s) failed" - exit 1 -fi - -echo "" diff --git a/tests/asr.cpp b/tests/asr.cpp deleted file mode 100644 index 33dd6befd..000000000 --- a/tests/asr.cpp +++ /dev/null @@ -1,870 +0,0 @@ -#include "../cactus/ffi/cactus_ffi.h" -#include "../cactus/ffi/cactus_cloud.h" -#include "../cactus/telemetry/telemetry.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef HAVE_SDL2 -#include -#endif - -static std::string get_cloud_api_key() { - return cactus::ffi::resolve_cloud_api_key(nullptr); -} - -static std::string get_transcribe_options_json() { - const char* threshold = std::getenv("CACTUS_CLOUD_HANDOFF_THRESHOLD"); - std::ostringstream oss; - oss << "{\"max_tokens\":500,\"telemetry_enabled\":true"; - if (threshold && threshold[0] != '\0') { - oss << ",\"cloud_handoff_threshold\":" << threshold; - } - oss << "}"; - return oss.str(); -} - -constexpr size_t RESPONSE_BUFFER_SIZE = 65536; - -namespace Color { - const std::string RESET = "\033[0m"; - const std::string BOLD = "\033[1m"; - const std::string DIM = "\033[2m"; - const std::string CYAN = "\033[36m"; - const std::string GREEN = "\033[32m"; - const std::string YELLOW = "\033[33m"; - const std::string BLUE = "\033[34m"; - const std::string MAGENTA = "\033[35m"; - const std::string RED = "\033[31m"; - const std::string GRAY = "\033[90m"; -} - -bool supports_color() { -#ifdef _WIN32 - return false; -#else - const char* term = std::getenv("TERM"); - return term && std::string(term) != "dumb"; -#endif -} - -bool use_colors = supports_color(); - -std::string colored(const std::string& text, const std::string& color) { - if (!use_colors) return text; - return color + text + Color::RESET; -} - -void print_separator(char ch = '-', int width = 60) { - std::cout << colored(std::string(width, ch), Color::DIM) << "\n"; -} - -void print_header_live_mode() { - std::cout << "\n"; - print_separator('='); - std::cout << colored(" 🌵 CACTUS LIVE TRANSCRIPTION 🌵", Color::GREEN + Color::BOLD) << "\n"; - print_separator('='); - std::cout << colored("Listening...", Color::YELLOW) << " Press " << colored("Enter", Color::CYAN) << " to stop\n"; - print_separator(); - std::cout << "\n"; -} - -bool file_exists(const std::string& path) { - std::ifstream f(path); - return f.good(); -} - -bool read_file_bytes(const std::string& path, std::vector& out) { - std::ifstream in(path, std::ios::binary); - if (!in.is_open()) return false; - in.seekg(0, std::ios::end); - std::streamsize size = in.tellg(); - if (size <= 0) return false; - in.seekg(0, std::ios::beg); - out.resize(static_cast(size)); - return static_cast(in.read(reinterpret_cast(out.data()), size)); -} - -bool ends_with_wav(const std::string& path) { - std::string lower = path; - std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { - return static_cast(std::tolower(c)); - }); - return lower.size() >= 4 && lower.substr(lower.size() - 4) == ".wav"; -} - -std::string extract_json_value(const std::string& json, const std::string& key) { - std::string pattern = "\"" + key + "\":\""; - size_t start = json.find(pattern); - if (start == std::string::npos) return ""; - start += pattern.length(); - size_t end = start; - while (end < json.length() && json[end] != '"') { - if (json[end] == '\\' && end + 1 < json.length()) end++; - end++; - } - return json.substr(start, end - start); -} - -std::string extract_json_number(const std::string& json, const std::string& key) { - std::string pattern = "\"" + key + "\":"; - size_t start = json.find(pattern); - if (start == std::string::npos) return ""; - start += pattern.length(); - while (start < json.length() && std::isspace(json[start])) start++; - const char* begin = json.c_str() + start; - char* end_ptr = nullptr; - std::strtod(begin, &end_ptr); - if (end_ptr == begin) return ""; - return std::string(begin, static_cast(end_ptr - begin)); -} - -size_t visible_length(const std::string& s) { - size_t len = 0; - bool in_esc = false; - for (char c : s) { - if (c == '\033') { in_esc = true; continue; } - if (in_esc) { if (c == 'm') in_esc = false; continue; } - if ((c & 0xC0) != 0x80) len++; - } - return len; -} - -int get_terminal_width() { - struct winsize w; - return (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == -1) ? 80 : w.ws_col; -} - -std::string truncate_visible(const std::string& s, size_t limit) { - std::string out; - size_t len = 0; - bool in_esc = false; - for (char c : s) { - if (c == '\033') { in_esc = true; out += c; continue; } - if (in_esc) { out += c; if (c == 'm') in_esc = false; continue; } - if (len >= limit && (c & 0xC0) != 0x80) break; - out += c; - if ((c & 0xC0) != 0x80) len++; - } - return out + "\033[0m"; -} - -size_t find_safe_split_index(const std::string& s, size_t limit) { - size_t len = 0; - bool in_esc = false; - for (size_t i = 0; i < s.length(); ++i) { - char c = s[i]; - if (c == '\033') { in_esc = true; continue; } - if (in_esc) { if (c == 'm') in_esc = false; continue; } - if ((c & 0xC0) != 0x80) len++; - if (len >= limit && c == ' ' && !in_esc) return i; - } - return std::string::npos; -} - -void print_token(const char* token, uint32_t /*token_id*/, void* /*user_data*/) { - std::cout << token << std::flush; -} - -bool is_gemma4_model(const std::string& model_path) { - std::string p = model_path; - std::transform(p.begin(), p.end(), p.begin(), [](unsigned char c) { return std::tolower(c); }); - return p.find("gemma4") != std::string::npos || - p.find("gemma-3n") != std::string::npos || - p.find("gemma3n") != std::string::npos; -} - -bool is_parakeet_model(const std::string& model_path) { - std::string p = model_path; - std::transform(p.begin(), p.end(), p.begin(), [](unsigned char c) { return std::tolower(c); }); - return p.find("parakeet") != std::string::npos; -} - -std::string get_transcribe_prompt(const std::string& model_path, const std::string& language = "en") { - std::string path_lower = model_path; - std::transform(path_lower.begin(), path_lower.end(), path_lower.begin(), - [](unsigned char c) { return std::tolower(c); }); - - if (path_lower.find("whisper") != std::string::npos) { - return "<|startoftranscript|><|" + language + "|><|transcribe|>"; - } - if (is_gemma4_model(model_path)) { - return "Transcribe the audio."; - } - return ""; -} - -int transcribe_file(cactus_model_t model, const std::string& audio_path, const std::string& model_path, const std::string& language = "en") { - if (!file_exists(audio_path)) { - std::cerr << colored("Error: ", Color::RED + Color::BOLD) - << "File not found: " << audio_path << "\n"; - return -1; - } - - std::string prompt = get_transcribe_prompt(model_path, language); - std::vector response_buffer(RESPONSE_BUFFER_SIZE, 0); - - auto start_time = std::chrono::steady_clock::now(); - - const std::string options_json = get_transcribe_options_json(); - - int result = cactus_transcribe( - model, - audio_path.c_str(), - prompt.c_str(), - response_buffer.data(), - response_buffer.size(), - options_json.c_str(), - print_token, - nullptr, - nullptr, - 0 - ); - - auto end_time = std::chrono::steady_clock::now(); - auto duration = std::chrono::duration_cast(end_time - start_time); - double total_seconds = duration.count() / 1000.0; - - if (result < 0) { - std::cerr << "\n" << colored("Error: ", Color::RED + Color::BOLD) - << "Transcription failed\n"; - const char* err = cactus_get_last_error(); - if (err) { - std::cerr << colored("Details: ", Color::RED) << err << "\n"; - } - return -1; - } - - std::string json_str(response_buffer.data()); - std::string local_response = extract_json_value(json_str, "response"); - bool cloud_handoff = json_str.find("\"cloud_handoff\":true") != std::string::npos; - bool cloud_key_detected = !get_cloud_api_key().empty(); - - bool cloud_result_used_cloud = false; - bool cloud_result_attempted = false; - std::string cloud_result_error; - std::string cloud_result_text; - bool cloud_transcript_changed = false; - - if (cloud_handoff) { - cloud_result_attempted = true; - if (!ends_with_wav(audio_path)) { - cloud_result_error = "cloud_file_mode_requires_wav"; - cloud_result_text = local_response; - } else { - std::vector audio_bytes; - if (!read_file_bytes(audio_path, audio_bytes)) { - cloud_result_error = "audio_read_failed"; - cloud_result_text = local_response; - } else { - std::string audio_b64 = cactus::ffi::cloud_base64_encode(audio_bytes.data(), audio_bytes.size()); - cactus::ffi::CloudResponse cloud_result = - cactus::ffi::cloud_transcribe_request(audio_b64, local_response, 15L, nullptr); - cloud_result_used_cloud = cloud_result.used_cloud; - cloud_result_error = cloud_result.error; - cloud_result_text = cloud_result.transcript; - if (!cloud_result.api_key_hash.empty()) { - cactus::telemetry::setCloudKey(cloud_result.api_key_hash.c_str()); - } - cloud_transcript_changed = cloud_result_used_cloud && - !cloud_result_text.empty() && - cloud_result_text != local_response; - } - } - } - - std::string time_str; - size_t time_pos = json_str.find("\"total_time_ms\":"); - if (time_pos != std::string::npos) { - size_t start = time_pos + 16; - size_t end = json_str.find_first_of(",}", start); - time_str = json_str.substr(start, end - start); - } - - std::ostringstream stats; - stats << std::fixed << std::setprecision(2); - stats << "\n\n" << colored("[", Color::GRAY); - stats << colored("processed in: ", Color::GRAY) << total_seconds << "s"; - if (!time_str.empty()) { - double model_time = std::stod(time_str) / 1000.0; - stats << colored(" | model time: ", Color::GRAY) << model_time << "s"; - } - stats << colored("]", Color::GRAY); - stats << "\n" << colored("[cloud_handoff: ", Color::GRAY) - << (cloud_handoff ? colored("true", Color::YELLOW) : colored("false", Color::GREEN)) - << colored("]", Color::GRAY); - stats << "\n" << colored("[cloud_key_detected: ", Color::GRAY) - << (cloud_key_detected ? colored("true", Color::CYAN) : colored("false", Color::RED)) - << colored("]", Color::GRAY); - if (cloud_result_attempted) { - stats << "\n" << colored("[cloud_result_used_cloud: ", Color::GRAY) - << (cloud_result_used_cloud ? colored("true", Color::CYAN) : colored("false", Color::YELLOW)) - << colored("]", Color::GRAY); - if (!cloud_result_error.empty()) { - stats << "\n" << colored("[cloud_result_error: ", Color::GRAY) - << colored(cloud_result_error, Color::YELLOW) - << colored("]", Color::GRAY); - } - } - - std::cout << stats.str() << "\n"; - if (cloud_result_attempted && cloud_transcript_changed) { - std::cout << "\n" << colored("Cloud corrected transcript:", Color::GREEN + Color::BOLD) << "\n"; - std::cout << cloud_result_text << "\n"; - } - - return 0; -} - -#ifdef HAVE_SDL2 - -constexpr int TARGET_SAMPLE_RATE = 16000; -constexpr int AUDIO_BUFFER_MS = 100; - -struct AudioState { - std::mutex mutex; - std::vector buffer; - std::atomic recording{false}; - int actual_sample_rate{TARGET_SAMPLE_RATE}; -}; - -std::vector resample_audio(const std::vector& input, int source_rate, int target_rate) { - if (source_rate == target_rate || input.empty()) { - return input; - } - - size_t num_input_samples = input.size() / 2; - if (num_input_samples == 0) return input; - - const int16_t* input_samples = reinterpret_cast(input.data()); - - double ratio = static_cast(target_rate) / source_rate; - size_t num_output_samples = static_cast(num_input_samples * ratio); - if (num_output_samples == 0) return {}; - - std::vector output_samples(num_output_samples); - - for (size_t i = 0; i < num_output_samples; i++) { - double src_idx = i / ratio; - size_t idx0 = static_cast(src_idx); - size_t idx1 = std::min(idx0 + 1, num_input_samples - 1); - double frac = src_idx - idx0; - - double sample = input_samples[idx0] * (1.0 - frac) + input_samples[idx1] * frac; - output_samples[i] = static_cast(std::clamp(sample, -32768.0, 32767.0)); - } - - std::vector result(num_output_samples * 2); - std::memcpy(result.data(), output_samples.data(), result.size()); - return result; -} - -struct Segment { - std::string text; - bool pending_cloud = false; - std::chrono::steady_clock::time_point cloud_start_time; - int64_t cloud_job_id = -1; -}; - -AudioState g_audio_state; - -void audio_callback(void* /*userdata*/, Uint8* stream, int len) { - if (!g_audio_state.recording) return; - - std::lock_guard lock(g_audio_state.mutex); - g_audio_state.buffer.insert(g_audio_state.buffer.end(), stream, stream + len); -} - -int run_live_transcription(cactus_model_t model, const std::string& model_path, const std::string& language = "en") { - bool gemma4_mode = is_gemma4_model(model_path); - bool parakeet_mode = is_parakeet_model(model_path); - if (SDL_Init(SDL_INIT_AUDIO) < 0) { - std::cerr << colored("Error: ", Color::RED + Color::BOLD) - << "Failed to initialize SDL: " << SDL_GetError() << "\n"; - return 1; - } - - int num_devices = SDL_GetNumAudioDevices(1); - if (num_devices == 0) { - std::cerr << colored("Error: ", Color::RED + Color::BOLD) - << "No audio capture devices found\n"; - SDL_Quit(); - return 1; - } - - std::cout << colored("Available microphones:", Color::YELLOW) << "\n"; - for (int i = 0; i < num_devices; i++) { - std::cout << " [" << i << "] " << SDL_GetAudioDeviceName(i, 1) << "\n"; - } - std::cout << "\n"; - - SDL_AudioSpec want, have; - SDL_zero(want); - want.freq = TARGET_SAMPLE_RATE; - want.format = AUDIO_S16LSB; - want.channels = 1; - want.samples = (TARGET_SAMPLE_RATE * AUDIO_BUFFER_MS) / 1000; - want.callback = audio_callback; - want.userdata = nullptr; - - SDL_AudioDeviceID device = SDL_OpenAudioDevice(nullptr, 1, &want, &have, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE); - if (device == 0) { - std::cerr << colored("Error: ", Color::RED + Color::BOLD) - << "Failed to open audio device: " << SDL_GetError() << "\n"; - SDL_Quit(); - return 1; - } - - g_audio_state.actual_sample_rate = have.freq; - if (have.freq != TARGET_SAMPLE_RATE) { - std::cout << colored("Note: ", Color::YELLOW) << "Audio device uses " << have.freq - << "Hz, will resample to " << TARGET_SAMPLE_RATE << "Hz\n"; - } - - const size_t min_chunk_size = parakeet_mode ? 4000 : 16000; // 0.25s vs 1.0s at 16kHz - const double confirmation_threshold = parakeet_mode ? 0.95 : 0.99; - std::string options = - std::string("{\"confirmation_threshold\":") + std::to_string(confirmation_threshold) + - ",\"min_chunk_size\":" + std::to_string(min_chunk_size) + - ",\"telemetry_enabled\":true,\"language\":\"" + language + "\"}"; - cactus_stream_transcribe_t stream = nullptr; - - if (!gemma4_mode) { - stream = cactus_stream_transcribe_start(model, options.c_str()); - if (!stream) { - std::cerr << colored("Error: ", Color::RED + Color::BOLD) - << "Failed to initialize streaming transcription\n"; - SDL_CloseAudioDevice(device); - SDL_Quit(); - return 1; - } - } - - std::string api_key = get_cloud_api_key(); - if (api_key.empty()) { - std::cout << colored("Warning: ", Color::YELLOW + Color::BOLD) - << "CACTUS_CLOUD_KEY environment variable not set.\n"; - std::cout << colored(" Cloud handoff will be disabled (fallback to local transcription).\n", Color::YELLOW); - std::cout << "\n"; - } - - print_header_live_mode(); - - g_audio_state.recording = true; - g_audio_state.buffer.clear(); - SDL_PauseAudioDevice(device, 0); - - std::atomic should_stop{false}; - std::thread input_thread([&should_stop]() { - std::string line; - std::getline(std::cin, line); - should_stop = true; - }); - - std::deque segments; - std::string confirmed_text; - std::string current_line_confirmed; - - int last_pending_line_count = 0; - std::string last_stats; - - std::vector response_buffer(RESPONSE_BUFFER_SIZE, 0); - - auto last_process_time = std::chrono::steady_clock::now(); - const auto process_interval = std::chrono::milliseconds(parakeet_mode ? 250 : 1000); - - while (!should_stop) { - if (gemma4_mode) { - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - continue; - } - - auto now = std::chrono::steady_clock::now(); - - if (now - last_process_time >= process_interval) { - last_process_time = now; - - std::vector audio_chunk; - { - std::lock_guard lock(g_audio_state.mutex); - if (!g_audio_state.buffer.empty()) { - audio_chunk = std::move(g_audio_state.buffer); - g_audio_state.buffer.clear(); - } - } - - if (!audio_chunk.empty()) { - std::vector resampled = resample_audio( - audio_chunk, g_audio_state.actual_sample_rate, TARGET_SAMPLE_RATE - ); - - auto t_start = std::chrono::high_resolution_clock::now(); - int process_result = cactus_stream_transcribe_process( - stream, - resampled.data(), - resampled.size(), - response_buffer.data(), - response_buffer.size() - ); - auto t_end = std::chrono::high_resolution_clock::now(); - double latency_ms = std::chrono::duration_cast(t_end - t_start).count() / 1000.0; - - if (process_result >= 0) { - std::string json_str(response_buffer.data()); - std::string confirmed = extract_json_value(json_str, "confirmed"); - std::string pending = extract_json_value(json_str, "pending"); - std::string cloud_result = extract_json_value(json_str, "cloud_result"); - std::string cloud_job_id = extract_json_number(json_str, "cloud_job_id"); - std::string cloud_result_job_id = extract_json_number(json_str, "cloud_result_job_id"); - std::string ttft = extract_json_number(json_str, "time_to_first_token_ms"); - std::string decode_tps = extract_json_number(json_str, "decode_tps"); - std::string raw_decoder_tps = extract_json_number(json_str, "raw_decoder_tps"); - - bool matched_cloud_result_segment = false; - if (!cloud_result.empty()) { - int64_t result_job_id = cloud_result_job_id.empty() ? 0 : std::stoll(cloud_result_job_id); - if (result_job_id > 0) { - for (auto& seg : segments) { - if (seg.pending_cloud && seg.cloud_job_id == result_job_id) { - seg.text = cloud_result; - seg.pending_cloud = false; - matched_cloud_result_segment = true; - break; - } - } - } - } - - bool should_enqueue_confirmed = !confirmed.empty(); - if (should_enqueue_confirmed && - matched_cloud_result_segment && - !cloud_result.empty() && - confirmed == cloud_result) { - should_enqueue_confirmed = false; - } - - if (should_enqueue_confirmed) { - Segment seg; - seg.text = confirmed; - - bool is_cloud = json_str.find("\"cloud_handoff\":true") != std::string::npos; - int64_t parsed_cloud_job_id = cloud_job_id.empty() ? 0 : std::stoll(cloud_job_id); - if (is_cloud && parsed_cloud_job_id > 0) { - seg.pending_cloud = true; - seg.cloud_start_time = std::chrono::steady_clock::now(); - seg.cloud_job_id = parsed_cloud_job_id; - } - segments.push_back(seg); - } - - for (auto& seg : segments) { - if (seg.pending_cloud && - std::chrono::steady_clock::now() - seg.cloud_start_time > std::chrono::seconds(10)) { - seg.pending_cloud = false; - } - } - - if (!confirmed.empty() || !pending.empty()) { - last_stats = colored("[Latency:" + std::to_string(int(latency_ms)) + "ms Decode speed:" + decode_tps + " tokens/sec", Color::GRAY); - if (!raw_decoder_tps.empty()) { - try { - if (std::stod(raw_decoder_tps) > 0.0) { - last_stats += colored(" Raw decoder:" + raw_decoder_tps + " tokens/sec", Color::GRAY); - } - } catch (...) { - } - } - last_stats += colored("] ", Color::GRAY); - } - - int width = get_terminal_width(); - int limit = (width < 20 ? 80 : width) * 0.7; - - if (last_pending_line_count > 0) { - std::cout << "\r\033[2K"; - for (int i = 0; i < last_pending_line_count; ++i) - std::cout << "\033[1A\033[2K"; - } else { - std::cout << "\r"; - } - - while (!segments.empty() && !segments.front().pending_cloud) { - current_line_confirmed += colored(segments.front().text, Color::GREEN) + " "; - confirmed_text += segments.front().text + " "; - segments.pop_front(); - } - - while (true) { - size_t idx = find_safe_split_index(current_line_confirmed, limit); - if (idx == std::string::npos) break; - std::string part = current_line_confirmed.substr(0, idx); - std::string rem = current_line_confirmed.substr(idx + 1); - std::cout << "\r\033[K" << part + Color::RESET << "\n"; - current_line_confirmed = Color::GREEN + rem; - } - - std::cout << "\r\033[K" << current_line_confirmed; - - std::string ghost = last_stats; - if (!segments.empty()) { - if (!ghost.empty()) ghost += "\n"; - ghost += colored("[Awaiting Cloud] ", Color::RED); - for (const auto& seg : segments) - ghost += colored(seg.text, seg.pending_cloud ? Color::RED : Color::GREEN) + " "; - } - if (!pending.empty()) { - if (!ghost.empty()) ghost += "\n"; - ghost += colored("[Uncommitted] ", Color::YELLOW) + colored(pending, Color::YELLOW); - } - - last_pending_line_count = 0; - if (!ghost.empty()) { - std::cout << "\n"; - std::stringstream ss(ghost); - std::string line; - bool first = true; - while (std::getline(ss, line)) { - while (true) { - size_t idx = find_safe_split_index(line, limit); - if (idx == std::string::npos) break; - if (!first) std::cout << "\n"; - std::cout << line.substr(0, idx); - line = line.substr(idx + 1); - last_pending_line_count++; - first = false; - } - if (!first) std::cout << "\n"; - std::cout << line; - last_pending_line_count++; - first = false; - } - } - - std::cout << std::flush; - } - } - } - - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - - g_audio_state.recording = false; - SDL_PauseAudioDevice(device, 1); - - if (!gemma4_mode) { - std::vector tail_audio_chunk; - { - std::lock_guard lock(g_audio_state.mutex); - if (!g_audio_state.buffer.empty()) { - tail_audio_chunk = std::move(g_audio_state.buffer); - g_audio_state.buffer.clear(); - } - } - - if (!tail_audio_chunk.empty()) { - std::vector resampled_tail = resample_audio( - tail_audio_chunk, g_audio_state.actual_sample_rate, TARGET_SAMPLE_RATE - ); - - int tail_process_result = cactus_stream_transcribe_process( - stream, - resampled_tail.data(), - resampled_tail.size(), - response_buffer.data(), - response_buffer.size() - ); - - if (tail_process_result >= 0) { - std::string json_str(response_buffer.data()); - std::string confirmed = extract_json_value(json_str, "confirmed"); - std::string cloud_result = extract_json_value(json_str, "cloud_result"); - bool cloud_handoff = json_str.find("\"cloud_handoff\":true") != std::string::npos; - std::string cloud_job_id = extract_json_number(json_str, "cloud_job_id"); - int64_t parsed_cloud_job_id = cloud_job_id.empty() ? 0 : std::stoll(cloud_job_id); - - if (!confirmed.empty()) { - Segment seg; - seg.text = confirmed; - if (cloud_handoff && parsed_cloud_job_id > 0) { - seg.pending_cloud = true; - seg.cloud_start_time = std::chrono::steady_clock::now(); - seg.cloud_job_id = parsed_cloud_job_id; - } - segments.push_back(seg); - } - - if (!cloud_result.empty()) { - std::string cloud_result_job_id = extract_json_number(json_str, "cloud_result_job_id"); - int64_t result_job_id = cloud_result_job_id.empty() ? 0 : std::stoll(cloud_result_job_id); - if (result_job_id > 0) { - for (auto& seg : segments) { - if (seg.pending_cloud && seg.cloud_job_id == result_job_id) { - seg.text = cloud_result; - seg.pending_cloud = false; - break; - } - } - } - } - } - } - } - - if (gemma4_mode) { - std::vector gemma4_all_audio; - { - std::lock_guard lock(g_audio_state.mutex); - gemma4_all_audio = resample_audio(g_audio_state.buffer, g_audio_state.actual_sample_rate, TARGET_SAMPLE_RATE); - } - - double duration_sec = (gemma4_all_audio.size() / 2) / static_cast(TARGET_SAMPLE_RATE); - std::cout << "\n" << colored("Recorded ", Color::GREEN) - << std::fixed << std::setprecision(1) << duration_sec << "s" - << colored(" — transcribing...", Color::YELLOW) << "\n\n"; - - std::fill(response_buffer.begin(), response_buffer.end(), 0); - std::string prompt = get_transcribe_prompt(model_path, language); - std::string tl_options = R"({"max_tokens":500,"telemetry_enabled":true,"use_vad":false})"; - - int rc = cactus_transcribe( - model, nullptr, prompt.c_str(), - response_buffer.data(), response_buffer.size(), - tl_options.c_str(), print_token, nullptr, - gemma4_all_audio.data(), gemma4_all_audio.size() - ); - - std::cout << "\n\n"; - print_separator(); - - if (rc >= 0) { - std::string json_str(response_buffer.data()); - std::string total = extract_json_number(json_str, "total_time_ms"); - std::string tps = extract_json_number(json_str, "decode_tps"); - if (!total.empty() || !tps.empty()) { - std::cout << colored("[", Color::GRAY); - if (!total.empty()) std::cout << colored("time: ", Color::GRAY) << std::stod(total) / 1000.0 << "s"; - if (!tps.empty()) std::cout << colored(" | speed: ", Color::GRAY) << tps << " tok/s"; - std::cout << colored("]", Color::GRAY) << "\n"; - } - } else { - std::cerr << colored("Transcription failed.", Color::RED) << "\n"; - } - - cactus_reset(model); - } else { - int stopping_result = cactus_stream_transcribe_stop( - stream, - response_buffer.data(), - response_buffer.size() - ); - - std::cout << "\n\n"; - print_separator(); - - if (stopping_result >= 0) { - for (const auto& seg : segments) { - confirmed_text += seg.text + " "; - } - - std::string json_str(response_buffer.data()); - std::string final_text = extract_json_value(json_str, "confirmed"); - std::string full_transcript; - if (parakeet_mode) { - full_transcript = final_text.empty() ? confirmed_text : final_text; - } else { - full_transcript = confirmed_text + final_text; - } - - std::cout << colored("Final transcript:", Color::GREEN + Color::BOLD) << "\n"; - std::cout << full_transcript << "\n"; - } else { - if (!confirmed_text.empty()) { - std::cout << colored("Partial transcript:", Color::YELLOW + Color::BOLD) << "\n"; - std::cout << confirmed_text << "\n"; - } - } - } - - print_separator(); - - if (input_thread.joinable()) { - input_thread.detach(); - } - SDL_CloseAudioDevice(device); - SDL_Quit(); - - return 0; -} - -#endif // HAVE_SDL2 - -int main(int argc, char* argv[]) { - if (argc < 2) { - std::cerr << colored("Error: ", Color::RED + Color::BOLD) << "Missing model path\n"; - std::cerr << "Usage: " << argv[0] << " [audio_file] [--language ]\n"; - return 1; - } - - const char* model_path = argv[1]; - const char* audio_file = nullptr; - std::string language = "en"; - - for (int i = 2; i < argc; ++i) { - if (std::string(argv[i]) == "--language" && i + 1 < argc) { - language = argv[++i]; - } else if (argv[i][0] != '-') { - audio_file = argv[i]; - } - } - - std::cout << "\n" << colored("Loading model from ", Color::YELLOW) - << colored(model_path, Color::CYAN) << colored("...", Color::YELLOW) << "\n"; - - cactus_model_t model = cactus_init(model_path, nullptr, false); - - if (!model) { - std::cerr << colored("Failed to initialize model\n", Color::RED + Color::BOLD); - const char* err = cactus_get_last_error(); - if (err) { - std::cerr << colored("Error: ", Color::RED) << err << "\n"; - } - return 1; - } - - std::cout << colored("Model loaded successfully!\n", Color::GREEN + Color::BOLD); - - int result = 0; - - if (audio_file) { - std::cout << "\n" << colored("Transcribing: ", Color::BLUE + Color::BOLD) - << audio_file << "\n\n"; - result = transcribe_file(model, audio_file, model_path, language); - } else { -#ifdef HAVE_SDL2 - result = run_live_transcription(model, model_path, language); -#else - std::cerr << colored("Error: ", Color::RED + Color::BOLD) - << "Live transcription requires SDL2.\n"; - std::cerr << "Please install SDL2 and rebuild:\n"; - std::cerr << " macOS: brew install sdl2\n"; - std::cerr << " Linux: sudo apt-get install libsdl2-dev\n"; - result = 1; -#endif - } - - std::cout << colored("\n👋 Goodbye!\n", Color::MAGENTA + Color::BOLD); - cactus_destroy(model); - return result >= 0 ? 0 : 1; -} diff --git a/tests/assets/hotword.wav b/tests/assets/hotword.wav deleted file mode 100644 index ad2dd47cd..000000000 Binary files a/tests/assets/hotword.wav and /dev/null differ diff --git a/tests/assets/rag_corpus/rag_sample1.txt b/tests/assets/rag_corpus/rag_sample1.txt deleted file mode 100644 index 7791bab34..000000000 --- a/tests/assets/rag_corpus/rag_sample1.txt +++ /dev/null @@ -1,35 +0,0 @@ -Cactus Compute Inc: Pioneering On-Device AI Infrastructure - -Cactus Compute Inc (often referred to simply as Cactus) is an emerging technology startup focused on advancing edge artificial intelligence—that is, enabling powerful AI capabilities directly on mobile and wearable devices rather than in the cloud. It represents a new wave of infrastructure companies aiming to solve some of the key limitations of traditional cloud-based AI, such as latency, privacy, and cost. - -Core Vision and Technology - -The central offering from Cactus is an open-source software development kit (SDK) and inference engine that allows developers to run large language models (LLMs), vision, and speech models directly on devices like smartphones and wearables. This approach dramatically reduces dependence on cloud servers, which can introduce higher latency and raise privacy concerns because data must be transmitted over the internet. Instead, Cactus optimizes AI to run locally—leading to faster responses, improved privacy, and lower operating costs. - -Cactus achieves this through hardware-aware kernels and optimized inference pipelines that work across platforms including iOS and Android, as well as support for popular developer frameworks like Flutter, React Native, and Kotlin. The company’s SDK is designed to be cross-platform and compatible with a wide range of AI models from repositories such as Hugging Face, allowing developers to integrate multimodal AI (text, vision, speech) into apps that function offline. - -Founders and Funding - -Cactus was launched in 2025 and has quickly gained attention in the AI infrastructure space, even being featured in Y Combinator’s Summer 2025 cohort, a sign of early credibility and startup acceleration support. - -The co-founders Henry Ndubuaku and Roman Shemet bring complementary expertise in AI/ML and finance. Ndubuaku notably chose to focus on Cactus instead of pursuing a research role at Nvidia, highlighting the startup’s emphasis on practical on-device AI engineering. - -Cactus has also attracted investment from early-stage backers like Antigravity Capital, which sees the company’s SDK as a potential “standard layer” for edge AI akin to how CUDA became fundamental for GPU computing. - -Use Cases and Impact - -The on-device AI enabled by Cactus supports numerous practical use cases: - -Mobile Applications: Real-time AI assistants that work without an internet connection. - -Privacy-Sensitive Apps: Healthcare or financial tools where user data must remain local. - -Low-Connectivity Environments: Regions or scenarios with limited network access. - -Cost-Efficient Operations: Companies can significantly reduce cloud computing costs by shifting inference to user devices. - -Early adoption indicators show Cactus’s framework being used in hundreds of developer projects and products, suggesting traction within the developer community who are building the next generation of edge AI experiences. - -Broader Significance - -If successful, Cactus could redefine how AI is integrated into everyday devices—creating experiences that are faster, more private, and less reliant on centralized cloud infrastructure. By providing a cross-platform, open basis for running AI workloads locally, Cactus stands at the intersection of AI, mobile computing, and developer empowerment, positioning itself as a foundational platform for the emerging edge AI era. \ No newline at end of file diff --git a/tests/assets/record.wav b/tests/assets/record.wav deleted file mode 100644 index a5f238194..000000000 Binary files a/tests/assets/record.wav and /dev/null differ diff --git a/tests/assets/test.wav b/tests/assets/test.wav deleted file mode 100644 index 99638e372..000000000 Binary files a/tests/assets/test.wav and /dev/null differ diff --git a/tests/assets/test_long.wav b/tests/assets/test_long.wav deleted file mode 100644 index 788a9ace9..000000000 Binary files a/tests/assets/test_long.wav and /dev/null differ diff --git a/tests/assets/test_monkey.png b/tests/assets/test_monkey.png deleted file mode 100644 index cab0ac1f4..000000000 Binary files a/tests/assets/test_monkey.png and /dev/null differ diff --git a/tests/assets/test_thing.png b/tests/assets/test_thing.png deleted file mode 100644 index 395edada6..000000000 Binary files a/tests/assets/test_thing.png and /dev/null differ diff --git a/tests/chat.cpp b/tests/chat.cpp deleted file mode 100644 index 42b735645..000000000 --- a/tests/chat.cpp +++ /dev/null @@ -1,651 +0,0 @@ -#include "../cactus/ffi/cactus_ffi.h" -#include "../cactus/telemetry/telemetry.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef HAVE_SDL2 -#include -#include - -namespace { - -constexpr int RECORD_SAMPLE_RATE = 16000; - -struct RecordState { - std::mutex mutex; - std::vector buffer; - std::atomic recording{false}; - int actual_sample_rate{RECORD_SAMPLE_RATE}; -}; - -RecordState g_record; - -void record_callback(void* /*userdata*/, Uint8* stream, int len) { - if (!g_record.recording) return; - std::lock_guard lock(g_record.mutex); - g_record.buffer.insert(g_record.buffer.end(), stream, stream + len); -} - -std::vector resample_s16(const std::vector& input, int source_rate, int target_rate) { - if (source_rate == target_rate || input.empty()) return input; - size_t num_in = input.size() / 2; - if (num_in == 0) return input; - const int16_t* in = reinterpret_cast(input.data()); - double ratio = static_cast(target_rate) / source_rate; - size_t num_out = static_cast(num_in * ratio); - if (num_out == 0) return {}; - std::vector out(num_out); - for (size_t i = 0; i < num_out; i++) { - double src_idx = i / ratio; - size_t i0 = static_cast(src_idx); - size_t i1 = std::min(i0 + 1, num_in - 1); - double frac = src_idx - i0; - double sample = in[i0] * (1.0 - frac) + in[i1] * frac; - if (sample > 32767.0) sample = 32767.0; - if (sample < -32768.0) sample = -32768.0; - out[i] = static_cast(sample); - } - std::vector result(num_out * 2); - std::memcpy(result.data(), out.data(), result.size()); - return result; -} - -bool record_audio(std::vector& pcm_out) { - if (SDL_Init(SDL_INIT_AUDIO) < 0) { - std::cerr << "Failed to init SDL: " << SDL_GetError() << "\n"; - return false; - } - - int num_devices = SDL_GetNumAudioDevices(1); - if (num_devices == 0) { - std::cerr << "No audio capture devices found\n"; - SDL_QuitSubSystem(SDL_INIT_AUDIO); - return false; - } - - SDL_AudioSpec want, have; - SDL_zero(want); - want.freq = RECORD_SAMPLE_RATE; - want.format = AUDIO_S16LSB; - want.channels = 1; - want.samples = (RECORD_SAMPLE_RATE * 100) / 1000; - want.callback = record_callback; - - SDL_AudioDeviceID device = SDL_OpenAudioDevice(nullptr, 1, &want, &have, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE); - if (device == 0) { - std::cerr << "Failed to open mic: " << SDL_GetError() << "\n"; - SDL_QuitSubSystem(SDL_INIT_AUDIO); - return false; - } - - g_record.actual_sample_rate = have.freq; - g_record.buffer.clear(); - g_record.recording = true; - SDL_PauseAudioDevice(device, 0); - - std::cout << "Recording... press Enter to stop.\n" << std::flush; - - std::atomic stop{false}; - std::thread input_thread([&stop]() { - std::string line; - std::getline(std::cin, line); - stop = true; - }); - - while (!stop) { - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - } - - g_record.recording = false; - SDL_PauseAudioDevice(device, 1); - - { - std::lock_guard lock(g_record.mutex); - pcm_out = resample_s16(g_record.buffer, g_record.actual_sample_rate, RECORD_SAMPLE_RATE); - } - - double duration = (pcm_out.size() / 2) / static_cast(RECORD_SAMPLE_RATE); - std::cout << "Recorded " << std::fixed << std::setprecision(1) << duration << "s of audio.\n"; - - input_thread.join(); - SDL_CloseAudioDevice(device); - SDL_QuitSubSystem(SDL_INIT_AUDIO); - return !pcm_out.empty(); -} - -} // anonymous namespace -#endif // HAVE_SDL2 - -constexpr int MAX_TOKENS = 1024; -constexpr size_t MAX_BYTES_PER_TOKEN = 64; -constexpr size_t RESPONSE_BUFFER_SIZE = MAX_TOKENS * MAX_BYTES_PER_TOKEN; - -namespace Color { - const std::string RESET = "\033[0m"; - const std::string BOLD = "\033[1m"; - const std::string DIM = "\033[2m"; - const std::string CYAN = "\033[36m"; - const std::string GREEN = "\033[32m"; - const std::string YELLOW = "\033[33m"; - const std::string BLUE = "\033[34m"; - const std::string MAGENTA = "\033[35m"; - const std::string RED = "\033[31m"; - const std::string GRAY = "\033[90m"; -} - -bool supports_color() { -#ifdef _WIN32 - return false; -#else - const char* term = std::getenv("TERM"); - return term && std::string(term) != "dumb"; -#endif -} - -bool use_colors = supports_color(); - -std::string colored(const std::string& text, const std::string& color) { - if (!use_colors) return text; - return color + text + Color::RESET; -} - -void print_separator(char ch = '-', int width = 60) { - std::cout << colored(std::string(width, ch), Color::DIM) << "\n"; -} - -void print_header(const std::string& sys_prompt, const std::string& image, bool has_vision = true) { - std::cout << "\n"; - print_separator('='); - std::cout << colored(" 🌵 CACTUS CHAT INTERFACE 🌵", Color::GREEN + Color::BOLD) << "\n"; - print_separator('='); - std::cout << colored(" Commands: ", Color::YELLOW); - if (has_vision) { - std::cout << colored("/image ", Color::CYAN) << colored(" | ", Color::DIM); - } - std::cout << colored("/audio ", Color::CYAN) << colored(" | ", Color::DIM) -#ifdef HAVE_SDL2 - << colored("/record [prompt]", Color::CYAN) << colored(" | ", Color::DIM) -#endif - << colored("/clear", Color::CYAN) << colored(" | ", Color::DIM) - << colored("reset", Color::CYAN) << colored(" | ", Color::DIM) - << colored("exit", Color::CYAN) << "\n"; - if (!sys_prompt.empty()) { - std::cout << colored(" System prompt active", Color::MAGENTA) << "\n"; - } - if (!image.empty()) { - std::cout << colored(" Image: ", Color::MAGENTA) << colored(image, Color::CYAN) << "\n"; - } - print_separator(); - std::cout << "\n"; -} - -struct TokenPrinter { - bool first_token = true; - int token_count = 0; - std::chrono::steady_clock::time_point start_time; - std::chrono::steady_clock::time_point first_token_time; - double time_to_first_token = 0.0; - - void reset() { - first_token = true; - token_count = 0; - time_to_first_token = 0.0; - start_time = std::chrono::steady_clock::now(); - } - - void print(const char* token) { - if (first_token) { - first_token = false; - first_token_time = std::chrono::steady_clock::now(); - auto latency = std::chrono::duration_cast(first_token_time - start_time); - time_to_first_token = latency.count() / 1000.0; - } - std::cout << token << std::flush; - token_count++; - } - - void print_stats(double ram_mb = 0.0) { - auto end_time = std::chrono::steady_clock::now(); - auto duration = std::chrono::duration_cast(end_time - start_time); - double total_seconds = duration.count() / 1000.0; - double tokens_per_second = token_count / total_seconds; - - std::ostringstream stats; - stats << std::fixed << std::setprecision(3); - stats << "[" << token_count << " tokens | "; - stats << "latency: " << time_to_first_token << "s | "; - stats << "total: " << total_seconds << "s | "; - stats << std::setprecision(0) << static_cast(tokens_per_second) << " tok/s"; - if (ram_mb > 0.0) { - stats << std::fixed << std::setprecision(1) << " | RAM: " << ram_mb << " MB"; - } - stats << "]"; - - std::cout << "\n" << colored(stats.str(), Color::GRAY) << "\n"; - } -}; - -TokenPrinter* g_printer = nullptr; - -void print_token(const char* token, uint32_t /*token_id*/, void* /*user_data*/) { - if (g_printer) { - g_printer->print(token); - } -} - -std::string escape_json(const std::string& s) { - std::ostringstream o; - for (unsigned char c : s) { - switch (c) { - case '"': o << "\\\""; break; - case '\\': o << "\\\\"; break; - case '\b': o << "\\b"; break; - case '\f': o << "\\f"; break; - case '\n': o << "\\n"; break; - case '\r': o << "\\r"; break; - case '\t': o << "\\t"; break; - default: - if (c < 0x20) { - o << "\\u" << std::hex << std::setw(4) << std::setfill('0') << (int)c; - } else { - o << c; - } - break; - } - } - return o.str(); -} - -std::string unescape_json(const std::string& s) { - std::string result; - result.reserve(s.length()); - - for (size_t i = 0; i < s.length(); i++) { - if (s[i] == '\\' && i + 1 < s.length()) { - switch (s[i + 1]) { - case '"': result += '"'; i++; break; - case '\\': result += '\\'; i++; break; - case 'b': result += '\b'; i++; break; - case 'f': result += '\f'; i++; break; - case 'n': result += '\n'; i++; break; - case 'r': result += '\r'; i++; break; - case 't': result += '\t'; i++; break; - case 'u': - if (i + 5 < s.length()) { - std::string hex = s.substr(i + 2, 4); - char* end; - int codepoint = std::strtol(hex.c_str(), &end, 16); - if (end == hex.c_str() + 4) { - result += static_cast(codepoint); - i += 5; - } else { - result += s[i]; - } - } else { - result += s[i]; - } - break; - default: result += s[i]; break; - } - } else { - result += s[i]; - } - } - return result; -} - -std::string expand_tilde(const std::string& path) { - if (path.size() < 2 || path[0] != '~' || path[1] != '/') return path; - const char* home = std::getenv("HOME"); - if (!home) return path; - return std::string(home) + path.substr(1); -} - -int main(int argc, char* argv[]) { - if (argc < 2) { - std::cerr << colored("Error: ", Color::RED + Color::BOLD) << "Missing model path\n"; - std::cerr << "Usage: " << argv[0] << " [--system ] [--image ] [--audio ] [--prompt ] [--thinking]\n"; - return 1; - } - - const char* model_path = argv[1]; - std::string system_prompt; - std::string current_image; - std::string current_audio; - std::string initial_prompt; - bool enable_thinking = false; - - for (int i = 2; i < argc; ++i) { - if (std::string(argv[i]) == "--system" && i + 1 < argc) { - system_prompt = argv[++i]; - } else if (std::string(argv[i]) == "--image" && i + 1 < argc) { - current_image = expand_tilde(argv[++i]); - } else if (std::string(argv[i]) == "--audio" && i + 1 < argc) { - current_audio = expand_tilde(argv[++i]); - } else if (std::string(argv[i]) == "--prompt" && i + 1 < argc) { - initial_prompt = argv[++i]; - } else if (std::string(argv[i]) == "--thinking") { - enable_thinking = true; - } - } - - if (!current_image.empty()) { - std::ifstream f(current_image); - if (!f.good()) { - std::cerr << colored("Error: ", Color::RED + Color::BOLD) - << "Image not found: " << current_image << "\n"; - return 1; - } - } - - if (!current_audio.empty()) { - std::ifstream f(current_audio); - if (!f.good()) { - std::cerr << colored("Error: ", Color::RED + Color::BOLD) - << "Audio file not found: " << current_audio << "\n"; - return 1; - } - } - - std::cout << "\n" << colored("Loading model from ", Color::YELLOW) - << colored(model_path, Color::CYAN) << colored("...", Color::YELLOW) << "\n"; - - cactus_model_t model = cactus_init(model_path, nullptr, false); - - if (!model) { - std::cerr << colored("Failed to initialize model\n", Color::RED + Color::BOLD); - return 1; - } - - std::cout << colored("Model loaded successfully!\n", Color::GREEN + Color::BOLD); - - // Check if model supports vision/audio by reading config.txt - bool has_vision = false; - bool has_audio_cap = false; - { - std::ifstream cfg(std::string(model_path) + "/config.txt"); - std::string line; - while (std::getline(cfg, line)) { - if (line.substr(0, 19) == "vision_hidden_size=" && std::stoi(line.substr(19)) > 0) { - has_vision = true; - } - if (line.substr(0, 17) == "audio_hidden_dim=" && std::stoi(line.substr(17)) > 0) { - has_audio_cap = true; - } - } - } - - if (!current_image.empty() && !has_vision) { - std::cerr << colored("Warning: ", Color::YELLOW + Color::BOLD) - << "This model does not support vision — image will be ignored.\n"; - current_image.clear(); - } - - if (!current_audio.empty() && !has_audio_cap) { - std::cerr << colored("Warning: ", Color::YELLOW + Color::BOLD) - << "This model does not support audio — audio will be ignored.\n"; - current_audio.clear(); - } - - print_header(system_prompt, current_image, has_vision); - - std::vector history; - std::vector history_images; - std::vector history_audio; - std::vector current_pcm; - bool image_committed = false; - TokenPrinter printer; - g_printer = &printer; - - bool auto_send = !current_audio.empty() || !initial_prompt.empty(); - - while (true) { - bool has_media = !current_image.empty() || !current_audio.empty(); - std::string input; - - if (auto_send) { - auto_send = false; - input = initial_prompt; - initial_prompt.clear(); - if (has_media && input.empty()) { - std::cout << colored("You \xf0\x9f\x8e\xa4: ", Color::BLUE + Color::BOLD) - << colored("[audio input]", Color::DIM) << "\n"; - } else { - std::string prompt_label = has_media ? "You \xf0\x9f\x93\x8e: " : "You: "; - std::cout << colored(prompt_label, Color::BLUE + Color::BOLD) << input << "\n"; - } - } else { - std::string prompt = has_media ? "You \xf0\x9f\x93\x8e: " : "You: "; - std::cout << colored(prompt, Color::BLUE + Color::BOLD); - if (!std::getline(std::cin, input)) break; - - while (!input.empty() && (input.back() == ' ' || input.back() == '\t')) input.pop_back(); - if (input.empty()) continue; - if (input == "exit" || input == "quit") break; - } - - if (input == "reset") { - history.clear(); - history_images.clear(); - history_audio.clear(); - current_image.clear(); - current_audio.clear(); - image_committed = false; - cactus_reset(model); - std::cout << colored("Conversation reset.\n", Color::YELLOW); - print_header(system_prompt, current_image, has_vision); - continue; - } - - if (input == "/clear") { - current_image.clear(); - current_audio.clear(); - image_committed = false; - std::cout << colored("Image/audio cleared.\n", Color::YELLOW); - continue; - } - - // Parse /image or /audio commands: extract file path and optional trailing message - auto parse_file_cmd = [](const std::string& in, size_t prefix_len, std::string& out_path, std::string& out_msg) -> bool { - std::string rest = in.substr(prefix_len); - size_t space = rest.find(' '); - out_path = (space != std::string::npos) ? rest.substr(0, space) : rest; - while (!out_path.empty() && (out_path.back() == ' ' || out_path.back() == '\t')) out_path.pop_back(); - out_path = expand_tilde(out_path); - std::ifstream f(out_path); - if (!f.good()) return false; - out_msg.clear(); - if (space != std::string::npos) { - out_msg = rest.substr(space + 1); - while (!out_msg.empty() && (out_msg.front() == ' ' || out_msg.front() == '\t')) out_msg.erase(out_msg.begin()); - } - return true; - }; - - if (input.substr(0, 7) == "/image ") { - if (!has_vision) { - std::cerr << colored(" This model does not support vision.\n", Color::RED); - continue; - } - std::string path, msg; - if (!parse_file_cmd(input, 7, path, msg)) { - std::cerr << colored(" File not found: ", Color::RED) << path << "\n"; - continue; - } - current_image = path; - image_committed = false; - if (msg.empty()) continue; - input = msg; - } - - if (input.substr(0, 7) == "/audio ") { - std::string path, msg; - if (!parse_file_cmd(input, 7, path, msg)) { - std::cerr << colored(" File not found: ", Color::RED) << path << "\n"; - continue; - } - current_audio = path; - input = msg; - } - - if (input == "/record" || input.rfind("/record ", 0) == 0) { -#ifdef HAVE_SDL2 - if (!has_audio_cap) { - std::cerr << colored(" This model does not support audio.\n", Color::RED); - continue; - } - std::string record_prompt; - if (input.size() > 8) { - record_prompt = input.substr(8); - while (!record_prompt.empty() && (record_prompt.front() == ' ' || record_prompt.front() == '\t')) { - record_prompt.erase(record_prompt.begin()); - } - } - current_pcm.clear(); - if (!record_audio(current_pcm)) { - std::cerr << colored(" Recording failed.\n", Color::RED); - continue; - } - input = record_prompt; -#else - std::cerr << colored(" Recording requires SDL2 (not available in this build).\n", Color::RED); - continue; -#endif - } - - history.push_back(input); - history_images.push_back(image_committed ? std::string() : current_image); - if (!current_image.empty()) image_committed = true; - history_audio.push_back(current_audio); - - // Build messages JSON - std::ostringstream messages_json; - messages_json << "["; - if (!system_prompt.empty()) { - messages_json << "{\"role\":\"system\",\"content\":\"" - << escape_json(system_prompt) << "\"},"; - } - for (size_t i = 0; i < history.size(); i++) { - if (i > 0) messages_json << ","; - if (i % 2 == 0) { - messages_json << "{\"role\":\"user\",\"content\":\"" - << escape_json(history[i]) << "\""; - if (!history_images[i].empty()) { - messages_json << ",\"images\":[\"" << escape_json(history_images[i]) << "\"]"; - } - if (!history_audio[i].empty()) { - messages_json << ",\"audio\":[\"" << escape_json(history_audio[i]) << "\"]"; - } - messages_json << "}"; - } else { - messages_json << "{\"role\":\"assistant\",\"content\":\"" - << escape_json(history[i]) << "\"}"; - } - } - messages_json << "]"; - - std::string options = "{\"temperature\":0.7,\"top_p\":0.95,\"top_k\":40,\"max_tokens\":" - + std::to_string(MAX_TOKENS) - + ",\"enable_thinking_if_supported\":" + (enable_thinking ? "true" : "false") - + ",\"stop_sequences\":[\"<|im_end|>\",\"\"]}"; - - std::vector response_buffer(RESPONSE_BUFFER_SIZE, 0); - - if (!current_audio.empty()) { - std::cout << colored(" [audio: " + current_audio + "]\n", Color::MAGENTA); - current_audio.clear(); - } - if (!current_pcm.empty()) { - double dur = static_cast(current_pcm.size() / 2) / 16000.0; - std::cout << colored(" [mic recording: ", Color::MAGENTA) - << std::fixed << std::setprecision(1) << dur << "s" - << colored("]\n", Color::MAGENTA); - } - if (!current_image.empty()) { - std::cout << colored(" [" + current_image + "]\n", Color::MAGENTA); - } - std::cout << colored("Assistant: ", Color::GREEN + Color::BOLD); - - const uint8_t* pcm_ptr = current_pcm.empty() ? nullptr : current_pcm.data(); - size_t pcm_size = current_pcm.size(); - - printer.reset(); - int result = cactus_complete( - model, - messages_json.str().c_str(), - response_buffer.data(), - response_buffer.size(), - options.c_str(), - nullptr, - print_token, - nullptr, - pcm_ptr, - pcm_size - ); - - current_pcm.clear(); - - std::string json_str(response_buffer.data(), response_buffer.size()); - - double ram_mb = 0.0; - { - const std::string ram_key = "\"ram_usage_mb\":"; - size_t ram_pos = json_str.find(ram_key); - if (ram_pos != std::string::npos) { - ram_mb = std::stod(json_str.substr(ram_pos + ram_key.length())); - } - } - - if (result >= 0) { - printer.print_stats(ram_mb); - } - - std::cout << "\n"; - print_separator(); - std::cout << "\n"; - - if (result < 0) { - std::cerr << colored("Error: ", Color::RED + Color::BOLD) - << response_buffer.data() << "\n\n"; - history.pop_back(); - history_images.pop_back(); - history_audio.pop_back(); - continue; - } - - const std::string search_str = "\"response\":\""; - size_t response_start = json_str.find(search_str); - if (response_start != std::string::npos) { - response_start += search_str.length(); - size_t response_end = json_str.find("\"", response_start); - while (response_end != std::string::npos) { - size_t prior_backslashes = 0; - for (size_t i = response_end; i > response_start && json_str[i - 1] == '\\'; i--) { - prior_backslashes++; - } - if (prior_backslashes % 2 == 0) break; - response_end = json_str.find("\"", response_end + 1); - } - if (response_end != std::string::npos) { - std::string response = json_str.substr(response_start, - response_end - response_start); - history.push_back(unescape_json(response)); - history_images.push_back(""); - history_audio.push_back(""); - } - } - } - - std::cout << colored("\n👋 Goodbye!\n", Color::MAGENTA + Color::BOLD); - cactus_destroy(model); - return 0; -} diff --git a/tests/generate_audio_reference.py b/tests/generate_audio_reference.py deleted file mode 100644 index 385d4a937..000000000 --- a/tests/generate_audio_reference.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 -"""Generate reference audio encoder outputs from HF model for C++ validation. - -Uses the SAME HF model weights that were converted to cactus format, -so the only differences should be from quantization and FP16 precision.""" - -import os -import sys -import numpy as np -import torch -import torch.nn as nn -import torch.nn.functional as F - -HF_MODEL_PATH = "gg-hf-gg/gemma-4-e2b-it" - -def main(): - output_dir = os.path.join(os.path.dirname(__file__), 'assets') - os.makedirs(output_dir, exist_ok=True) - - torch.manual_seed(42) - np.random.seed(42) - - num_frames = 100 - mel_bins = 128 - mel_input = torch.randn(1, num_frames, mel_bins, dtype=torch.float32) - mel_mask = torch.zeros(1, num_frames, dtype=torch.bool) - - mel_flat = mel_input.squeeze(0).numpy() - np.save(os.path.join(output_dir, 'audio_test_mel_input.npy'), mel_input.numpy()) - mel_flat.astype(np.float32).tofile(os.path.join(output_dir, 'audio_test_mel_input.bin')) - print(f"Saved mel input: shape={mel_input.shape}") - - print(f"\nLoading HF model from {HF_MODEL_PATH}...") - from transformers import AutoModelForCausalLM - model = AutoModelForCausalLM.from_pretrained( - HF_MODEL_PATH, dtype=torch.float32, trust_remote_code=True, - ) - model.eval() - - audio_tower = model.model.audio_tower - embed_audio = model.model.embed_audio - - hooks = {} - def make_hook(name): - def hook_fn(module, input, output): - if isinstance(output, tuple): - hooks[name] = output[0].detach().float() - else: - hooks[name] = output.detach().float() - return hook_fn - - audio_tower.subsample_conv_projection.register_forward_hook(make_hook('sscp')) - for i in range(min(12, len(audio_tower.conformer))): - audio_tower.conformer[i].register_forward_hook(make_hook(f'conformer_{i}')) - - with torch.no_grad(): - encoder_out, encoder_mask = audio_tower(mel_input, mel_mask) - projected = embed_audio(inputs_embeds=encoder_out) - - for name, tensor in sorted(hooks.items()): - t = tensor.squeeze(0).numpy() - fname = f'audio_ref_{name}.npy' - np.save(os.path.join(output_dir, fname), t) - t.astype(np.float32).tofile(os.path.join(output_dir, f'audio_ref_{name}.bin')) - print(f" {name}: shape={t.shape}, mean={t.mean():.4f}, std={t.std():.4f}") - - enc_np = encoder_out.squeeze(0).float().numpy() - np.save(os.path.join(output_dir, 'audio_ref_encoder.npy'), enc_np) - enc_np.astype(np.float32).tofile(os.path.join(output_dir, 'audio_ref_encoder.bin')) - print(f" encoder: shape={enc_np.shape}, mean={enc_np.mean():.4f}, std={enc_np.std():.4f}") - - proj_np = projected.squeeze(0).float().numpy() - np.save(os.path.join(output_dir, 'audio_ref_projected.npy'), proj_np) - proj_np.astype(np.float32).tofile(os.path.join(output_dir, 'audio_ref_projected.bin')) - print(f" projected: shape={proj_np.shape}, mean={proj_np.mean():.4f}, std={proj_np.std():.4f}") - - print("\nDone. Reference files in", output_dir) - -if __name__ == '__main__': - main() diff --git a/tests/golden/generate_exhaustive_golden.sh b/tests/golden/generate_exhaustive_golden.sh deleted file mode 100755 index ffb10c474..000000000 --- a/tests/golden/generate_exhaustive_golden.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/bash -# for running the golden tests locally -set -e - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")" -GOLDEN_DIR="$SCRIPT_DIR" -TEST_BUILD_DIR="$PROJECT_ROOT/tests/build" - -if [ ! -f "$TEST_BUILD_DIR/test_exhaustive" ]; then - echo "Error: test_exhaustive not found at $TEST_BUILD_DIR/test_exhaustive" - echo "Build tests first: cd tests && mkdir -p build && cd build && cmake .. && make -j\$(nproc)" - exit 1 -fi - -MODELS=( - "qwen:Qwen/Qwen3-0.6B:INT8:llm:CACTUS_TEST_MODEL" - "nomic:nomic-ai/nomic-embed-text-v2-moe:INT8:embedding:CACTUS_TEST_MODEL" - "lfm2:LiquidAI/LFM2.5-350M:INT8:llm:CACTUS_TEST_MODEL" - "lfm2moe:LiquidAI/LFM2-8B-A1B:INT8:llm:CACTUS_TEST_MODEL" - "gemma:google/gemma-3-270m-it:INT8:llm:CACTUS_TEST_MODEL" - "whisper:openai/whisper-small:INT8:stt:CACTUS_TEST_TRANSCRIBE_MODEL" - "moonshine:UsefulSensors/moonshine-base:INT8:stt:CACTUS_TEST_TRANSCRIBE_MODEL" - "parakeet:nvidia/parakeet-ctc-0.6b:INT8:stt:CACTUS_TEST_TRANSCRIBE_MODEL" - "lfm2vl:LiquidAI/LFM2-VL-450M:INT8:vlm:CACTUS_TEST_MODEL" - "lfm2vl:LiquidAI/LFM2-VL-450M:INT4:vlm:CACTUS_TEST_MODEL" - "lfm2vl:LiquidAI/LFM2-VL-450M:FP16:vlm:CACTUS_TEST_MODEL" -) - -echo "=== Downloading VAD model ===" -cactus download "snakers4/silero-vad" --precision INT8 - -PASSED=0 -FAILED=0 - -for entry in "${MODELS[@]}"; do - IFS=: read -r family model precision test_type env_var <<< "$entry" - - echo "" - echo "=== Generating golden output: $family / $precision ===" - echo " Model: $model" - - if ! cactus download "$model" --precision "$precision"; then - echo " FAILED to download $model" - FAILED=$((FAILED + 1)) - continue - fi - - MODEL_DIR=$(echo "$model" | sed 's|.*/||' | tr '[:upper:]' '[:lower:]') - MODEL_PATH="$PROJECT_ROOT/weights/$MODEL_DIR" - - export CACTUS_NO_CLOUD_TELE=1 - export CACTUS_GOLDEN_GENERATE=1 - export CACTUS_TEST_GOLDEN_FILE="$GOLDEN_DIR/golden.json" - export CACTUS_TEST_GOLDEN_FAMILY="$family" - export CACTUS_TEST_GOLDEN_PRECISION="$precision" - export CACTUS_TEST_ASSETS="$PROJECT_ROOT/tests/assets" - export "$env_var"="$MODEL_PATH" - - if "$TEST_BUILD_DIR/test_exhaustive"; then - echo " OK" - PASSED=$((PASSED + 1)) - else - echo " FAILED" - FAILED=$((FAILED + 1)) - fi -done - -echo "" -echo "=== Golden generation complete ===" -echo "Passed: $PASSED" -echo "Failed: $FAILED" -echo "" -echo "Golden file: $GOLDEN_DIR/golden.json" -echo "After verifying outputs, update golden.json with the actual expected values," -echo "then commit it to the repository." - -exit $FAILED diff --git a/tests/golden/golden.json b/tests/golden/golden.json deleted file mode 100644 index 5620dbb45..000000000 --- a/tests/golden/golden.json +++ /dev/null @@ -1,105 +0,0 @@ -[ - { - "model_family": "gemma", - "precision": "INT8", - "test_type": "llm", - "input_messages": "[{\"role\":\"user\",\"content\":\"What is 2+2? Answer with just the number.\"}]", - "options": "{\"max_tokens\":32,\"temperature\":0,\"top_k\":1,\"stop_sequences\":[\"\"],\"telemetry_enabled\":false}", - "expected_output": "2", - "token_count_min": 1, - "token_count_max": 10 - }, - { - "model_family": "lfm2", - "precision": "INT8", - "test_type": "llm", - "input_messages": "[{\"role\":\"user\",\"content\":\"What is 2+2? Answer with just the number.\"}]", - "options": "{\"max_tokens\":32,\"temperature\":0,\"top_k\":1,\"stop_sequences\":[\"<|im_end|>\"],\"telemetry_enabled\":false}", - "expected_output": "2", - "token_count_min": 1, - "token_count_max": 10 - }, - { - "model_family": "lfm2moe", - "precision": "INT8", - "test_type": "llm", - "input_messages": "[{\"role\":\"user\",\"content\":\"What is 2+2? Answer with just the number.\"}]", - "options": "{\"max_tokens\":32,\"temperature\":0,\"top_k\":1,\"stop_sequences\":[\"<|im_end|>\"],\"telemetry_enabled\":false}", - "expected_output": "4", - "token_count_min": 1, - "token_count_max": 10 - }, - { - "model_family": "lfm2vl", - "precision": "FP16", - "test_type": "vlm", - "input_messages": "[{\"role\":\"user\",\"content\":\"What animal is in this image? One word.\",\"images\":[\"test_monkey.png\"]}]", - "options": "{\"max_tokens\":16,\"temperature\":0,\"top_k\":1,\"stop_sequences\":[\"<|im_end|>\"],\"telemetry_enabled\":false}", - "expected_output": "The animal in this image is a monkey.", - "token_count_min": 1, - "token_count_max": 16 - }, - { - "model_family": "lfm2vl", - "precision": "INT4", - "test_type": "vlm", - "input_messages": "[{\"role\":\"user\",\"content\":\"What animal is in this image? One word.\",\"images\":[\"test_monkey.png\"]}]", - "options": "{\"max_tokens\":16,\"temperature\":0,\"top_k\":1,\"stop_sequences\":[\"<|im_end|>\"],\"telemetry_enabled\":false}", - "expected_output": "Monkey", - "token_count_min": 1, - "token_count_max": 16 - }, - { - "model_family": "lfm2vl", - "precision": "INT8", - "test_type": "vlm", - "input_messages": "[{\"role\":\"user\",\"content\":\"What animal is in this image? One word.\",\"images\":[\"test_monkey.png\"]}]", - "options": "{\"max_tokens\":16,\"temperature\":0,\"top_k\":1,\"stop_sequences\":[\"<|im_end|>\"],\"telemetry_enabled\":false}", - "expected_output": "monkey", - "token_count_min": 1, - "token_count_max": 16 - }, - { - "model_family": "moonshine", - "precision": "INT8", - "test_type": "stt", - "audio_file": "test.wav", - "reference_transcript": "Hello hello hello um quickly testing out creating a way of file through voice memos um the goal is to use this way file to test out whisper hopefully this will transcribe properly that's all i can hope for all right here we go", - "wer_threshold": 0.15 - }, - { - "model_family": "parakeet", - "precision": "INT8", - "test_type": "stt", - "audio_file": "test.wav", - "reference_transcript": "hello hello hello quickly testing out creating a way file through voice memos the goal is to use this wh file to test out whisper hopefully this will transcribe properly that's all i can hope for all right here we go", - "wer_threshold": 0.15 - }, - { - "model_family": "nomic", - "precision": "INT8", - "test_type": "embedding", - "input_text": "The quick brown fox jumps over the lazy dog", - "expected_embedding_dim": 768, - "reference_embedding": [], - "cosine_similarity_threshold": 0.99 - }, - { - "model_family": "qwen", - "precision": "INT8", - "test_type": "llm", - "input_messages": "[{\"role\":\"user\",\"content\":\"what is 2 + 2\"}]", - "options": "{\"max_tokens\":32,\"temperature\":0,\"top_k\":1,\"stop_sequences\":[\"<|im_end|>\"],\"telemetry_enabled\":false,\"enable_thinking_if_supported\":false}", - "expected_output": "4", - "token_count_min": 1, - "token_count_max": 64 - }, - { - "model_family": "whisper", - "precision": "INT8", - "test_type": "stt", - "audio_file": "test.wav", - "reference_transcript": "Hello hello hello, just quickly testing out creating a WAV file through voice memos. The goal is to use this WAV file to test out Whisper. Hopefully this will transcribe properly. That's all I can hope for. Alright, here we go.", - "wer_threshold": 0.15 - } -] diff --git a/tests/ios/.gitignore b/tests/ios/.gitignore deleted file mode 100644 index 28eb45e98..000000000 --- a/tests/ios/.gitignore +++ /dev/null @@ -1 +0,0 @@ -CactusTest/CactusTest.xcodeproj/* diff --git a/tests/ios/CactusTest/CactusTest.xcodeproj/project.pbxproj.template b/tests/ios/CactusTest/CactusTest.xcodeproj/project.pbxproj.template deleted file mode 100644 index aea0cf79a..000000000 --- a/tests/ios/CactusTest/CactusTest.xcodeproj/project.pbxproj.template +++ /dev/null @@ -1,334 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 77; - objects = { - -/* Begin PBXFileReference section */ - 2AA889CA2EE510D50037A29D /* CactusTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CactusTest.app; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - 2AA889E12EE510D60037A29D /* Exceptions for "CactusTest" folder in "CactusTest" target */ = { - isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - membershipExceptions = ( - Info.plist, - ); - target = 2AA889C92EE510D50037A29D /* CactusTest */; - }; -/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ - -/* Begin PBXFileSystemSynchronizedRootGroup section */ - 2AA889CC2EE510D50037A29D /* CactusTest */ = { - isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - 2AA889E12EE510D60037A29D /* Exceptions for "CactusTest" folder in "CactusTest" target */, - ); - path = CactusTest; - sourceTree = ""; - }; -/* End PBXFileSystemSynchronizedRootGroup section */ - -/* Begin PBXFrameworksBuildPhase section */ - 2AA889C72EE510D50037A29D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 2AA889C12EE510D50037A29D = { - isa = PBXGroup; - children = ( - 2AA889CC2EE510D50037A29D /* CactusTest */, - 2AA889CB2EE510D50037A29D /* Products */, - ); - sourceTree = ""; - }; - 2AA889CB2EE510D50037A29D /* Products */ = { - isa = PBXGroup; - children = ( - 2AA889CA2EE510D50037A29D /* CactusTest.app */, - ); - name = Products; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 2AA889C92EE510D50037A29D /* CactusTest */ = { - isa = PBXNativeTarget; - buildConfigurationList = 2AA889E22EE510D60037A29D /* Build configuration list for PBXNativeTarget "CactusTest" */; - buildPhases = ( - 2AA889C62EE510D50037A29D /* Sources */, - 2AA889C72EE510D50037A29D /* Frameworks */, - 2AA889C82EE510D50037A29D /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - fileSystemSynchronizedGroups = ( - 2AA889CC2EE510D50037A29D /* CactusTest */, - ); - name = CactusTest; - packageProductDependencies = ( - ); - productName = CactusTest; - productReference = 2AA889CA2EE510D50037A29D /* CactusTest.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 2AA889C22EE510D50037A29D /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = 1; - LastUpgradeCheck = 2600; - TargetAttributes = { - 2AA889C92EE510D50037A29D = { - CreatedOnToolsVersion = 26.0.1; - }; - }; - }; - buildConfigurationList = 2AA889C52EE510D50037A29D /* Build configuration list for PBXProject "CactusTest" */; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 2AA889C12EE510D50037A29D; - minimizedProjectReferenceProxies = 1; - preferredProjectObjectVersion = 77; - productRefGroup = 2AA889CB2EE510D50037A29D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 2AA889C92EE510D50037A29D /* CactusTest */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 2AA889C82EE510D50037A29D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 2AA889C62EE510D50037A29D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin XCBuildConfiguration section */ - 2AA889E32EE510D60037A29D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = CactusTest/Info.plist; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = cactus.CactusTest; - PRODUCT_NAME = "$(TARGET_NAME)"; - STRING_CATALOG_GENERATE_SYMBOLS = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 2AA889E42EE510D60037A29D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = CactusTest/Info.plist; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = cactus.CactusTest; - PRODUCT_NAME = "$(TARGET_NAME)"; - STRING_CATALOG_GENERATE_SYMBOLS = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - 2AA889E52EE510D60037A29D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - }; - name = Debug; - }; - 2AA889E62EE510D60037A29D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 2AA889C52EE510D50037A29D /* Build configuration list for PBXProject "CactusTest" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 2AA889E52EE510D60037A29D /* Debug */, - 2AA889E62EE510D60037A29D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 2AA889E22EE510D60037A29D /* Build configuration list for PBXNativeTarget "CactusTest" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 2AA889E32EE510D60037A29D /* Debug */, - 2AA889E42EE510D60037A29D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 2AA889C22EE510D50037A29D /* Project object */; -} diff --git a/tests/ios/CactusTest/CactusTest/AppDelegate.h b/tests/ios/CactusTest/CactusTest/AppDelegate.h deleted file mode 100644 index 25a0c524a..000000000 --- a/tests/ios/CactusTest/CactusTest/AppDelegate.h +++ /dev/null @@ -1,5 +0,0 @@ -#import - -@interface AppDelegate : UIResponder - -@end diff --git a/tests/ios/CactusTest/CactusTest/AppDelegate.mm b/tests/ios/CactusTest/CactusTest/AppDelegate.mm deleted file mode 100644 index 643d983b6..000000000 --- a/tests/ios/CactusTest/CactusTest/AppDelegate.mm +++ /dev/null @@ -1,204 +0,0 @@ -// AUTO-GENERATED by configure_xcode.rb - DO NOT EDIT MANUALLY -// This file is regenerated on each test run with discovered test files - -#import "AppDelegate.h" -#import -#include "graph/graph.h" -#include "ffi/cactus_ffi.h" -#include -#include -#include -#include -#include - -extern int test_audio_chat_pcm_main(); -extern int test_audio_cosine_main(); -extern int test_curl_main(); -extern int test_embed_main(); -extern int test_exhaustive_main(); -extern int test_gemma4_audio_main(); -extern int test_gemma4_audio_layers_main(); -extern int test_gemma4_suite_main(); -extern int test_gemma4_thinking_main(); -extern int test_gemma4_vision_main(); -extern int test_graph_main(); -extern int test_index_main(); -extern int test_kernel_main(); -extern int test_kv_cache_main(); -extern int test_llm_main(); -extern int test_model_loading_main(); -extern int test_performance_main(); -extern int test_rag_main(); -extern int test_stt_main(); -extern int test_telemetry_main(); -extern int test_vlm_main(); - -static void asr_token_callback(const char* token, uint32_t, void*) { - if (!token) return; - fputs(token, stdout); - fflush(stdout); -} - -@implementation AppDelegate - -- (void)copyFromBundle:(NSString *)bundlePath toDocuments:(const char *)name { - if (!name) return; - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSString *itemName = [NSString stringWithUTF8String:name]; - NSString *sourceItemPath = [NSString stringWithFormat:@"%@/%@", bundlePath, itemName]; - if (![fileManager fileExistsAtPath:sourceItemPath]) { - fprintf(stderr, "[CactusTest] copyFromBundle: source not found: %s\n", [sourceItemPath UTF8String]); - return; - } - if ([fileManager fileExistsAtPath:itemName]) { - NSError *removeError = nil; - [fileManager removeItemAtPath:itemName error:&removeError]; - if (removeError) { - fprintf(stderr, "[CactusTest] copyFromBundle: failed to remove existing %s: %s\n", - [itemName UTF8String], [[removeError localizedDescription] UTF8String]); - } - } - NSError *copyError = nil; - [fileManager copyItemAtPath:sourceItemPath toPath:itemName error:©Error]; - if (copyError) { - fprintf(stderr, "[CactusTest] copyFromBundle: failed to copy %s -> %s: %s\n", - [sourceItemPath UTF8String], [itemName UTF8String], [[copyError localizedDescription] UTF8String]); - } else { - fprintf(stderr, "[CactusTest] copyFromBundle: copied %s -> %s\n", - [sourceItemPath UTF8String], [itemName UTF8String]); - } -} - -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); - NSString *documentsDirectory = paths[0]; - chdir([documentsDirectory UTF8String]); - -#if !TARGET_OS_SIMULATOR - freopen("cactus_test.log", "w", stdout); - freopen("cactus_test.log", "a", stderr); - setbuf(stdout, NULL); - setbuf(stderr, NULL); -#endif - - cactus::Logger::instance().set_level(cactus::LogLevel::WARN); - - NSString *bundlePath = [[NSBundle mainBundle] bundlePath]; - [self copyFromBundle:bundlePath toDocuments:getenv("CACTUS_TEST_MODEL")]; - [self copyFromBundle:bundlePath toDocuments:getenv("CACTUS_TEST_TRANSCRIBE_MODEL")]; - [self copyFromBundle:bundlePath toDocuments:getenv("CACTUS_TEST_ASSETS")]; - [self copyFromBundle:bundlePath toDocuments:getenv("CACTUS_ASR_AUDIO_FILE")]; - - const char* run_asr = getenv("CACTUS_RUN_ASR"); - if (run_asr && run_asr[0] == '1') { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - const char* model_path = getenv("CACTUS_TEST_TRANSCRIBE_MODEL"); - const char* audio_file = getenv("CACTUS_ASR_AUDIO_FILE"); - if (!model_path || !model_path[0] || !audio_file || !audio_file[0]) { - fprintf(stderr, "[ASR] Missing CACTUS_TEST_TRANSCRIBE_MODEL or CACTUS_ASR_AUDIO_FILE\n"); - exit(1); - } - - cactus_model_t model = cactus_init(model_path, nullptr, false); - if (!model) { - const char* err = cactus_get_last_error(); - fprintf(stderr, "[ASR] Failed to initialize model: %s\n", err ? err : "unknown"); - exit(1); - } - - std::string model_l = model_path; - std::transform(model_l.begin(), model_l.end(), model_l.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - const char* prompt = (model_l.find("whisper") != std::string::npos) - ? "<|startoftranscript|><|en|><|transcribe|><|notimestamps|>" - : ""; - - std::string options = "{\"max_tokens\":500,\"telemetry_enabled\":true"; - const char* threshold = getenv("CACTUS_CLOUD_HANDOFF_THRESHOLD"); - if (threshold && threshold[0]) { - options += ",\"cloud_handoff_threshold\":"; - options += threshold; - } - options += "}"; - - std::vector response(65536, 0); - auto t0 = std::chrono::high_resolution_clock::now(); - int rc = cactus_transcribe( - model, - audio_file, - prompt, - response.data(), - response.size(), - options.c_str(), - asr_token_callback, - nullptr, - nullptr, - 0 - ); - auto t1 = std::chrono::high_resolution_clock::now(); - double wall_ms = std::chrono::duration_cast(t1 - t0).count() / 1000.0; - - if (rc < 0) { - const char* err = cactus_get_last_error(); - fprintf(stderr, "\n[ASR] Transcription failed: %s\n", err ? err : "unknown"); - cactus_destroy(model); - exit(1); - } - - std::string json(response.data()); - bool cloud_handoff = json.find("\"cloud_handoff\":true") != std::string::npos; - double model_ms = 0.0; - size_t p = json.find("\"total_time_ms\":"); - if (p != std::string::npos) { - p += 16; - size_t e = json.find_first_of(",}", p); - try { model_ms = std::stod(json.substr(p, e - p)); } catch (...) {} - } - - printf("\n\n[processed in: %.2fs | model time: %.2fs]\n", - wall_ms / 1000.0, model_ms / 1000.0); - printf("[cloud_handoff: %s]\n", cloud_handoff ? "true" : "false"); - printf("\n👋 Goodbye!\n"); - fflush(stdout); - - cactus_destroy(model); - exit(0); - }); - return YES; - } - - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - const char* only = getenv("CACTUS_TEST_ONLY"); - std::string filter = (only && only[0]) ? std::string(only) : ""; - auto should_run = [&](const char* name) { - return filter.empty() || filter == name; - }; - - if (should_run("audio_chat_pcm")) test_audio_chat_pcm_main(); - if (should_run("audio_cosine")) test_audio_cosine_main(); - if (should_run("curl")) test_curl_main(); - if (should_run("embed")) test_embed_main(); - if (should_run("exhaustive")) test_exhaustive_main(); - if (should_run("gemma4_audio")) test_gemma4_audio_main(); - if (should_run("gemma4_audio_layers")) test_gemma4_audio_layers_main(); - if (should_run("gemma4_suite")) test_gemma4_suite_main(); - if (should_run("gemma4_thinking")) test_gemma4_thinking_main(); - if (should_run("gemma4_vision")) test_gemma4_vision_main(); - if (should_run("graph")) test_graph_main(); - if (should_run("index")) test_index_main(); - if (should_run("kernel")) test_kernel_main(); - if (should_run("kv_cache")) test_kv_cache_main(); - if (should_run("llm")) test_llm_main(); - if (should_run("model_loading")) test_model_loading_main(); - if (should_run("performance")) test_performance_main(); - if (should_run("rag")) test_rag_main(); - if (should_run("stt")) test_stt_main(); - if (should_run("telemetry")) test_telemetry_main(); - if (should_run("vlm")) test_vlm_main(); - exit(0); - }); - - return YES; -} - -@end diff --git a/tests/ios/CactusTest/CactusTest/Info.plist b/tests/ios/CactusTest/CactusTest/Info.plist deleted file mode 100644 index 6631ffa6f..000000000 --- a/tests/ios/CactusTest/CactusTest/Info.plist +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/tests/ios/CactusTest/CactusTest/main.m b/tests/ios/CactusTest/CactusTest/main.m deleted file mode 100644 index 3631c9476..000000000 --- a/tests/ios/CactusTest/CactusTest/main.m +++ /dev/null @@ -1,10 +0,0 @@ -#import -#import "AppDelegate.h" - -int main(int argc, char * argv[]) { - NSString * appDelegateClassName; - @autoreleasepool { - appDelegateClassName = NSStringFromClass([AppDelegate class]); - } - return UIApplicationMain(argc, argv, nil, appDelegateClassName); -} diff --git a/tests/ios/configure_xcode.rb b/tests/ios/configure_xcode.rb deleted file mode 100755 index 59d10cfbb..000000000 --- a/tests/ios/configure_xcode.rb +++ /dev/null @@ -1,331 +0,0 @@ -#!/usr/bin/env ruby -require 'xcodeproj' - -def fail_with(message) - STDERR.puts "Error: #{message}" - exit 1 -end - -def generate_app_delegate(output_path, test_files) - raw_level = ENV.fetch('CACTUS_TEST_LOG_LEVEL', 'WARN').to_s.upcase - level = case raw_level - when 'DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE' - raw_level - else - 'WARN' - end - - test_names = test_files.map { |f| File.basename(f, '.cpp') } - extern_declarations = test_names.map { |name| "extern int #{name}_main();" }.join("\n") - test_calls = test_names.map { |name| - filter_name = name.sub(/^test_/, '') - " if (should_run(\"#{filter_name}\")) #{name}_main();" - }.join("\n") - - app_delegate_content = <<~OBJC - // AUTO-GENERATED by configure_xcode.rb - DO NOT EDIT MANUALLY - // This file is regenerated on each test run with discovered test files - - #import "AppDelegate.h" - #import - #include "graph/graph.h" - #include "ffi/cactus_ffi.h" - #include - #include - #include - #include - #include - - #{extern_declarations} - - static void asr_token_callback(const char* token, uint32_t, void*) { - if (!token) return; - fputs(token, stdout); - fflush(stdout); - } - - @implementation AppDelegate - - - (void)copyFromBundle:(NSString *)bundlePath toDocuments:(const char *)name { - if (!name) return; - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSString *itemName = [NSString stringWithUTF8String:name]; - NSString *sourceItemPath = [NSString stringWithFormat:@"%@/%@", bundlePath, itemName]; - if (![fileManager fileExistsAtPath:sourceItemPath]) { - fprintf(stderr, "[CactusTest] copyFromBundle: source not found: %s\\n", [sourceItemPath UTF8String]); - return; - } - if ([fileManager fileExistsAtPath:itemName]) { - NSError *removeError = nil; - [fileManager removeItemAtPath:itemName error:&removeError]; - if (removeError) { - fprintf(stderr, "[CactusTest] copyFromBundle: failed to remove existing %s: %s\\n", - [itemName UTF8String], [[removeError localizedDescription] UTF8String]); - } - } - NSError *copyError = nil; - [fileManager copyItemAtPath:sourceItemPath toPath:itemName error:©Error]; - if (copyError) { - fprintf(stderr, "[CactusTest] copyFromBundle: failed to copy %s -> %s: %s\\n", - [sourceItemPath UTF8String], [itemName UTF8String], [[copyError localizedDescription] UTF8String]); - } else { - fprintf(stderr, "[CactusTest] copyFromBundle: copied %s -> %s\\n", - [sourceItemPath UTF8String], [itemName UTF8String]); - } - } - - - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); - NSString *documentsDirectory = paths[0]; - chdir([documentsDirectory UTF8String]); - - #if !TARGET_OS_SIMULATOR - freopen("cactus_test.log", "w", stdout); - freopen("cactus_test.log", "a", stderr); - setbuf(stdout, NULL); - setbuf(stderr, NULL); - #endif - - cactus::Logger::instance().set_level(cactus::LogLevel::#{level}); - - NSString *bundlePath = [[NSBundle mainBundle] bundlePath]; - [self copyFromBundle:bundlePath toDocuments:getenv("CACTUS_TEST_MODEL")]; - [self copyFromBundle:bundlePath toDocuments:getenv("CACTUS_TEST_TRANSCRIBE_MODEL")]; - [self copyFromBundle:bundlePath toDocuments:getenv("CACTUS_TEST_ASSETS")]; - [self copyFromBundle:bundlePath toDocuments:getenv("CACTUS_ASR_AUDIO_FILE")]; - - const char* run_asr = getenv("CACTUS_RUN_ASR"); - if (run_asr && run_asr[0] == '1') { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - const char* model_path = getenv("CACTUS_TEST_TRANSCRIBE_MODEL"); - const char* audio_file = getenv("CACTUS_ASR_AUDIO_FILE"); - if (!model_path || !model_path[0] || !audio_file || !audio_file[0]) { - fprintf(stderr, "[ASR] Missing CACTUS_TEST_TRANSCRIBE_MODEL or CACTUS_ASR_AUDIO_FILE\\n"); - exit(1); - } - - cactus_model_t model = cactus_init(model_path, nullptr, false); - if (!model) { - const char* err = cactus_get_last_error(); - fprintf(stderr, "[ASR] Failed to initialize model: %s\\n", err ? err : "unknown"); - exit(1); - } - - std::string model_l = model_path; - std::transform(model_l.begin(), model_l.end(), model_l.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - const char* prompt = (model_l.find("whisper") != std::string::npos) - ? "<|startoftranscript|><|en|><|transcribe|><|notimestamps|>" - : ""; - - std::string options = "{\\\"max_tokens\\\":500,\\\"telemetry_enabled\\\":true"; - const char* threshold = getenv("CACTUS_CLOUD_HANDOFF_THRESHOLD"); - if (threshold && threshold[0]) { - options += ",\\\"cloud_handoff_threshold\\\":"; - options += threshold; - } - options += "}"; - - std::vector response(65536, 0); - auto t0 = std::chrono::high_resolution_clock::now(); - int rc = cactus_transcribe( - model, - audio_file, - prompt, - response.data(), - response.size(), - options.c_str(), - asr_token_callback, - nullptr, - nullptr, - 0 - ); - auto t1 = std::chrono::high_resolution_clock::now(); - double wall_ms = std::chrono::duration_cast(t1 - t0).count() / 1000.0; - - if (rc < 0) { - const char* err = cactus_get_last_error(); - fprintf(stderr, "\\n[ASR] Transcription failed: %s\\n", err ? err : "unknown"); - cactus_destroy(model); - exit(1); - } - - std::string json(response.data()); - bool cloud_handoff = json.find("\\\"cloud_handoff\\\":true") != std::string::npos; - double model_ms = 0.0; - size_t p = json.find("\\\"total_time_ms\\\":"); - if (p != std::string::npos) { - p += 16; - size_t e = json.find_first_of(",}", p); - try { model_ms = std::stod(json.substr(p, e - p)); } catch (...) {} - } - - printf("\\n\\n[processed in: %.2fs | model time: %.2fs]\\n", - wall_ms / 1000.0, model_ms / 1000.0); - printf("[cloud_handoff: %s]\\n", cloud_handoff ? "true" : "false"); - printf("\\n👋 Goodbye!\\n"); - fflush(stdout); - - cactus_destroy(model); - exit(0); - }); - return YES; - } - - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - const char* only = getenv("CACTUS_TEST_ONLY"); - std::string filter = (only && only[0]) ? std::string(only) : ""; - auto should_run = [&](const char* name) { - return filter.empty() || filter == name; - }; - - #{test_calls} - exit(0); - }); - - return YES; - } - - @end - OBJC - - File.write(output_path, app_delegate_content) - puts "Generated AppDelegate.mm with #{test_names.length} test(s)" -end - -project_root = ENV['PROJECT_ROOT'] -tests_root = ENV['TESTS_ROOT'] -cactus_root = ENV['CACTUS_ROOT'] -project_path = ENV['XCODEPROJ_PATH'] -bundle_id = ENV['BUNDLE_ID'] -team_id = ENV['DEVELOPMENT_TEAM'] -device_type = ENV['DEVICE_TYPE'] - -fail_with("PROJECT_ROOT not set") unless project_root -fail_with("TESTS_ROOT not set") unless tests_root -fail_with("CACTUS_ROOT not set") unless cactus_root -fail_with("XCODEPROJ_PATH not set") unless project_path -fail_with("DEVICE_TYPE not set (should be 'device' or 'simulator')") unless device_type -fail_with("Xcode project not found") unless File.exist?(project_path) - -project = Xcodeproj::Project.open(project_path) rescue fail_with("Failed to open Xcode project") -target = project.targets.first or fail_with("No targets found") - -tests_group = project.main_group.find_subpath('Tests', true) -tests_group.set_path(tests_root) -tests_group.set_source_tree('') - -discovered_test_files = Dir.glob(File.join(tests_root, 'test_*.cpp')) - .reject { |f| File.basename(f) == 'test_utils.cpp' } - .sort - .map { |f| File.basename(f) } - -puts "Discovered #{discovered_test_files.length} test file(s):" -discovered_test_files.each { |f| puts " - #{f}" } - -test_files = {} -discovered_test_files.each { |f| test_files[f] = "#{File.basename(f, '.cpp')}_main" } -test_files['test_utils.cpp'] = nil - -test_files.each do |filename, renamed_main| - file_path = File.join(tests_root, filename) - next unless File.exist?(file_path) - existing = tests_group.files.find { |f| f.path == filename || f.real_path&.to_s == file_path } - file_ref = existing || begin - ref = tests_group.new_reference(file_path) - ref.set_source_tree('') - ref - end - build_file = target.source_build_phase.files.find { |bf| bf.file_ref == file_ref } - build_file ||= target.source_build_phase.add_file_reference(file_ref) - build_file.settings = { 'COMPILER_FLAGS' => "-Dmain=#{renamed_main}" } if renamed_main -end - -unless tests_group.files.any? { |f| f.path == 'test_utils.h' } - ref = tests_group.new_reference(File.join(tests_root, 'test_utils.h')) - ref.set_source_tree('') -end - -generate_app_delegate(File.join(File.dirname(project_path), 'CactusTest', 'AppDelegate.mm'), discovered_test_files) - -cactus_sources_group = project.main_group.groups.find { |g| g.name == 'CactusSources' } -if cactus_sources_group - cactus_sources_group.files.each do |f| - bf = target.source_build_phase.files.find { |b| b.file_ref == f } - target.source_build_phase.files.delete(bf) if bf - end - cactus_sources_group.clear - cactus_sources_group.remove_from_project -end - -apple_dir = File.join(project_root, 'apple') -static_lib_path = device_type == 'simulator' ? - File.join(apple_dir, 'libcactus-simulator.a') : - File.join(apple_dir, 'libcactus-device.a') - -fail_with("Static library not found at: #{static_lib_path}") unless File.exist?(static_lib_path) -puts "Using static library: #{static_lib_path}" - -curl_root = ENV['CACTUS_CURL_ROOT'] -vendored_curl_lib = nil -if curl_root && !curl_root.empty? - vendored_curl_lib = device_type == 'simulator' ? - File.join(curl_root, 'ios', 'simulator', 'libcurl.a') : - File.join(curl_root, 'ios', 'device', 'libcurl.a') - if File.exist?(vendored_curl_lib) - puts "Using vendored iOS libcurl: #{vendored_curl_lib}" - else - vendored_curl_lib = nil - puts "Vendored iOS libcurl not found under CACTUS_CURL_ROOT=#{curl_root}; continuing without explicit curl link" - end -end - -target.frameworks_build_phase.files.to_a.each do |build_file| - if build_file.file_ref&.path&.to_s&.include?('libcactus') - target.frameworks_build_phase.files.delete(build_file) - end -end - -libs_group = project.main_group.groups.find { |g| g.name == 'Frameworks' } -if libs_group - libs_group.files.to_a.each do |f| - f.remove_from_project if f.path&.to_s&.include?('libcactus') - end -end - -target.build_configurations.each do |config| - config.build_settings['HEADER_SEARCH_PATHS'] ||= ['$(inherited)'] - [tests_root, cactus_root].each do |path| - config.build_settings['HEADER_SEARCH_PATHS'] << path unless config.build_settings['HEADER_SEARCH_PATHS'].include?(path) - end - if curl_root && !curl_root.empty? - curl_include = File.join(curl_root, 'include') - if File.exist?(File.join(curl_include, 'curl', 'curl.h')) - config.build_settings['HEADER_SEARCH_PATHS'] << curl_include unless config.build_settings['HEADER_SEARCH_PATHS'].include?(curl_include) - end - end - - config.build_settings['CLANG_CXX_LANGUAGE_STANDARD'] = 'c++20' - config.build_settings['CLANG_CXX_LIBRARY'] = 'libc++' - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' - config.build_settings['CODE_SIGN_STYLE'] = 'Automatic' - config.build_settings['PRODUCT_BUNDLE_IDENTIFIER'] = bundle_id if bundle_id - config.build_settings['DEVELOPMENT_TEAM'] = team_id if team_id - config.build_settings.delete('INFOPLIST_KEY_UILaunchStoryboardName') - config.build_settings.delete('INFOPLIST_KEY_UIMainStoryboardFile') - - config.build_settings['OTHER_CPLUSPLUSFLAGS'] = ['$(inherited)', '-pthread', '-Wall', '-Wextra', '-pedantic', '-O3'] - - config.build_settings['OTHER_LDFLAGS'] ||= ['$(inherited)'] - config.build_settings['OTHER_LDFLAGS'].reject! { |flag| flag.to_s.include?('libcactus') } - config.build_settings['OTHER_LDFLAGS'] << static_lib_path - - ['-framework CoreML', '-framework Foundation', '-framework Accelerate', '-framework Security', '-framework SystemConfiguration', '-framework CFNetwork'].each do |framework| - config.build_settings['OTHER_LDFLAGS'] << framework unless config.build_settings['OTHER_LDFLAGS'].include?(framework) - end - if vendored_curl_lib - config.build_settings['OTHER_LDFLAGS'] << vendored_curl_lib unless config.build_settings['OTHER_LDFLAGS'].include?(vendored_curl_lib) - end -end - -project.save rescue fail_with("Failed to save Xcode project") diff --git a/tests/ios/run.sh b/tests/ios/run.sh deleted file mode 100755 index 7096dfc58..000000000 --- a/tests/ios/run.sh +++ /dev/null @@ -1,475 +0,0 @@ -#!/bin/bash - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -CACTUS_CURL_ROOT="${CACTUS_CURL_ROOT:-$PROJECT_ROOT/libs/curl}" -export CACTUS_CURL_ROOT - -MODEL_NAME="$1" -TRANSCRIBE_MODEL_NAME="$2" -RUN_ASR="${CACTUS_RUN_ASR:-0}" -ASR_AUDIO_SOURCE="${CACTUS_ASR_AUDIO_SOURCE:-}" -ASR_AUDIO_FILE="${CACTUS_ASR_AUDIO_FILE:-}" - -echo "Running Cactus tests on iOS..." -echo "============================" - -if [ ! -d "/Applications/Xcode.app" ]; then - echo "Xcode not installed" - echo "Install Xcode from the Mac App Store" - exit 1 -fi - -if ! xcode-select -p; then - echo "Xcode Command Line Tools not installed" - echo "Install with: xcode-select --install" - exit 1 -fi - -if ! /usr/bin/xcrun --version; then - echo "Xcode license not accepted" - echo "Accept the license with: sudo xcodebuild -license accept" - exit 1 -fi - -if ! command -v xcodebuild; then - echo "xcodebuild not found" - exit 1 -fi - -echo "" -echo "Step 1: Selecting iOS device..." - -simulators=$(xcrun simctl list devices available | grep -E "^\s+(iPhone|iPad)" | grep -v "unavailable" | sed 's/^[[:space:]]*//' | while read line; do - uuid=$(echo "$line" | grep -oE '\([A-F0-9-]{36}\)' | head -1 | tr -d '()') - if [ -n "$uuid" ]; then - name=$(echo "$line" | sed -E 's/ \([^)]*\)//g' | xargs) - echo "${name}|simulator|${uuid}" - fi -done) - -xctrace_output=$(xcrun xctrace list devices 2>&1) - -physical_devices=$(echo "$xctrace_output" | awk ' - /== Devices ==/ { in_online=1; in_offline=0; next } - /== Devices Offline ==/ { in_online=0; in_offline=1; next } - /== Simulators ==/ { exit } - /00008[A-F0-9]{3}-[A-F0-9]{16}/ { - if (in_online || in_offline) { - status = in_offline ? "offline" : "" - print $0 "|" status - } - } -' | while read line; do - uuid=$(echo "$line" | grep -oE '00008[A-F0-9]{3}-[A-F0-9]{16}') - status=$(echo "$line" | awk -F'|' '{print $2}') - if [ -n "$uuid" ]; then - name=$(echo "$line" | awk -F'|' '{print $1}' | sed -E 's/ \([0-9]+\.[0-9]+.*$//' | xargs) - ios_version=$(echo "$line" | awk -F'|' '{print $1}' | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -1) - echo "${name} (iOS ${ios_version})|device|${uuid}|${status}" - fi -done) - -all_devices=$(printf "%s\n%s\n" "$physical_devices" "$simulators" | grep -v '^$') - -if [ -z "$all_devices" ]; then - echo "No devices or simulators found" - echo "To use a simulator:" - echo " - Install iOS simulators through Xcode" - echo "To use a physical device:" - echo " - Connect via USB or network" - echo " - Enable Developer Mode in Settings" - exit 1 -fi - -physical_count=$(echo "$all_devices" | grep -c '|device|' || true) -device_num=0 - -if [ "$physical_count" -gt 0 ]; then - echo "Devices:" - while IFS='|' read -r name type uuid status; do - if [ "$type" = "device" ]; then - device_num=$((device_num + 1)) - if [ "$status" = "offline" ]; then - printf " %2d. %s [offline]\n" "$device_num" "$name" - else - printf " %2d. %s\n" "$device_num" "$name" - fi - fi - done <<< "$all_devices" - echo "" -fi - -echo "Simulators:" -while IFS='|' read -r name type uuid status; do - if [ "$type" = "simulator" ]; then - device_num=$((device_num + 1)) - printf " %2d. %s\n" "$device_num" "$name" - fi -done <<< "$all_devices" - -echo "" -read -p "Select device number (1-$device_num): " device_number - -if ! [[ "$device_number" =~ ^[0-9]+$ ]] || [ "$device_number" -lt 1 ] || [ "$device_number" -gt "$device_num" ]; then - echo "Invalid selection" - exit 1 -fi - -selected_line=$(echo "$all_devices" | sed -n "${device_number}p") -device_name=$(echo "$selected_line" | cut -d'|' -f1) -device_type=$(echo "$selected_line" | cut -d'|' -f2) -device_uuid=$(echo "$selected_line" | cut -d'|' -f3) -device_status=$(echo "$selected_line" | cut -d'|' -f4) - -if [ -z "$device_uuid" ]; then - echo "Could not parse device information" - exit 1 -fi - -echo "" -if [ "$device_type" = "simulator" ]; then - echo "Selected: $device_name (Simulator)" -else - if [ "$device_status" = "offline" ]; then - echo "Selected: $device_name (Device - Offline)" - echo "Warning: This device is currently offline" - echo "Ensure the device is:" - echo " - Connected via USB or network" - echo " - Unlocked and trusted" - echo " - Has Developer Mode enabled" - echo "" - read -p "Continue anyway? (y/N): " continue_choice - if [[ ! "$continue_choice" =~ ^[Yy]$ ]]; then - echo "Aborted" - exit 0 - fi - else - echo "Selected: $device_name (Device)" - fi - - matching_identity=$(security find-identity -p codesigning 2>/dev/null | grep "Apple Development" | head -1 || true) - valid_identity=$(security find-identity -v -p codesigning 2>/dev/null | grep "Apple Development" | head -1 || true) - - if [ -z "$matching_identity" ]; then - echo "No development certificates found" - echo "To fix this:" - echo " 1. Open Xcode > Settings > Accounts" - echo " 2. Add your Apple ID" - echo " 3. Download development certificates" - exit 1 - fi - - if [ -z "$valid_identity" ]; then - echo "Warning: Apple Development identity found, but macOS did not report it as a valid identity." - echo "Proceeding and letting xcodebuild attempt automatic signing anyway." - echo "If signing fails later, reissue the Apple Development certificate/private key pair in Xcode." - echo "Detected identity: $matching_identity" - fi -fi - -echo "" -echo "Step 2: Building Cactus library for iOS..." - -if ! BUILD_STATIC=true BUILD_XCFRAMEWORK=false "$PROJECT_ROOT/apple/build.sh"; then - echo "Failed to build Cactus library" - exit 1 -fi - -echo "" -echo "Step 3: Configuring Xcode project..." - -xcodeproj_path="$SCRIPT_DIR/CactusTest/CactusTest.xcodeproj" -tests_root="$(cd "$SCRIPT_DIR/.." && pwd)" -cactus_root="$PROJECT_ROOT/cactus" - -project_file="$xcodeproj_path/project.pbxproj" -template_file="$xcodeproj_path/project.pbxproj.template" -echo "Copying project template..." -cp "$template_file" "$project_file" - -bundle_id="com.cactus.test.${USER}" -echo "Using Bundle ID: $bundle_id" - -if [ "$device_type" = "device" ]; then - development_team=$(security find-certificate -a -c "Apple Development" -p "$HOME/Library/Keychains/login.keychain-db" | openssl x509 -noout -subject | grep -oE 'OU=[A-Z0-9]{10}' | head -1 | cut -d= -f2) - if [ -z "$development_team" ]; then - echo "Could not extract Team ID from certificate" - exit 1 - fi - echo "Using Team ID: $development_team" -fi - -if ! command -v ruby; then - echo "Ruby not found" - exit 1 -fi - -if ! gem list xcodeproj -i; then - echo "Installing xcodeproj gem..." - if ! gem install xcodeproj; then - echo "Failed to install xcodeproj gem" - echo "To fix this:" - echo " brew install rbenv ruby-build" - echo " rbenv init" - echo " rbenv install 2.6.10" - echo " rbenv global 2.6.10" - exit 1 - fi -fi - -export PROJECT_ROOT TESTS_ROOT="$tests_root" CACTUS_ROOT="$cactus_root" XCODEPROJ_PATH="$xcodeproj_path" BUNDLE_ID="$bundle_id" DEVELOPMENT_TEAM="$development_team" DEVICE_TYPE="$device_type" CACTUS_CURL_ROOT="$CACTUS_CURL_ROOT" -if ! ruby "$SCRIPT_DIR/configure_xcode.rb"; then - echo "Failed to configure Xcode project" - exit 1 -fi - -echo "" -echo "Step 4: Building iOS test application..." - -if [ "$device_type" = "simulator" ]; then - ios_sim_sdk_path=$(xcrun --sdk iphonesimulator --show-sdk-path) - if [ -z "$ios_sim_sdk_path" ] || [ ! -d "$ios_sim_sdk_path" ]; then - echo "iOS Simulator SDK not found" - exit 1 - fi - - if ! xcodebuild -project "$xcodeproj_path" \ - -scheme CactusTest \ - -configuration Release \ - -destination "platform=iOS Simulator,id=$device_uuid" \ - -derivedDataPath "$SCRIPT_DIR/build" \ - ARCHS=arm64 \ - ONLY_ACTIVE_ARCH=NO \ - IPHONEOS_DEPLOYMENT_TARGET=13.0 \ - SDKROOT="$ios_sim_sdk_path" \ - PRODUCT_BUNDLE_IDENTIFIER="$bundle_id" \ - build; then - echo "Build failed" - exit 1 - fi - - app_path="$SCRIPT_DIR/build/Build/Products/Release-iphonesimulator/CactusTest.app" -else - ios_sdk_path=$(xcrun --sdk iphoneos --show-sdk-path) - if [ -z "$ios_sdk_path" ] || [ ! -d "$ios_sdk_path" ]; then - echo "iOS SDK not found" - exit 1 - fi - - if ! xcodebuild -project "$xcodeproj_path" \ - -scheme CactusTest \ - -configuration Release \ - -destination "platform=iOS,id=$device_uuid" \ - -derivedDataPath "$SCRIPT_DIR/build" \ - -allowProvisioningUpdates \ - ARCHS=arm64 \ - ONLY_ACTIVE_ARCH=NO \ - IPHONEOS_DEPLOYMENT_TARGET=13.0 \ - SDKROOT="$ios_sdk_path" \ - PRODUCT_BUNDLE_IDENTIFIER="$bundle_id" \ - CODE_SIGN_STYLE="Automatic" \ - build; then - echo "Build failed" - exit 1 - fi - - app_path="$SCRIPT_DIR/build/Build/Products/Release-iphoneos/CactusTest.app" -fi - -model_dir=$(echo "$MODEL_NAME" | sed 's|.*/||' | tr '[:upper:]' '[:lower:]') -transcribe_model_dir=$(echo "$TRANSCRIBE_MODEL_NAME" | sed 's|.*/||' | tr '[:upper:]' '[:lower:]') -model_src="$PROJECT_ROOT/weights/$model_dir" -transcribe_model_src="$PROJECT_ROOT/weights/$transcribe_model_dir" -assets_src="$PROJECT_ROOT/tests/assets" - -if [ ! -d "$model_src" ] || [ ! -f "$model_src/config.txt" ]; then - echo "Error: model weights missing or invalid at $model_src" - exit 1 -fi -if [ ! -d "$transcribe_model_src" ] || [ ! -f "$transcribe_model_src/config.txt" ]; then - echo "Error: transcribe model weights missing or invalid at $transcribe_model_src" - exit 1 -fi - -echo "Copying model weights to app bundle..." -rm -rf "$app_path/$model_dir" "$app_path/$transcribe_model_dir" -if ! cp -R "$model_src" "$app_path/"; then - echo "Error: Could not copy model weights from $model_src" - exit 1 -fi -if ! cp -R "$transcribe_model_src" "$app_path/"; then - echo "Error: Could not copy transcribe model weights from $transcribe_model_src" - exit 1 -fi - -echo "Copying test assets to app bundle..." -if ! cp -R "$assets_src" "$app_path/"; then - echo "Error: Could not copy test assets from $assets_src" - exit 1 -fi - -if [ "$RUN_ASR" = "1" ]; then - if [ -z "$ASR_AUDIO_SOURCE" ] || [ ! -f "$ASR_AUDIO_SOURCE" ]; then - echo "Error: CACTUS_ASR_AUDIO_SOURCE must point to an existing audio file when CACTUS_RUN_ASR=1" - exit 1 - fi - if [ -z "$ASR_AUDIO_FILE" ]; then - ASR_AUDIO_FILE=$(basename "$ASR_AUDIO_SOURCE") - fi - echo "Copying ASR audio file to app bundle..." - if ! cp "$ASR_AUDIO_SOURCE" "$app_path/$ASR_AUDIO_FILE"; then - echo "Error: Could not copy ASR audio file into app bundle" - exit 1 - fi -fi - -echo "" -echo "Step 5: Running tests..." -echo "------------------------" - -if [ "$device_type" = "simulator" ]; then - echo "Installing on: $device_name" - - xcrun simctl boot "$device_uuid" || true - - if ! xcrun simctl install "$device_uuid" "$app_path"; then - echo "Failed to install app on simulator" - exit 1 - fi - - if [ "$RUN_ASR" = "1" ]; then - echo "Launching ASR transcription..." - else - echo "Launching tests..." - fi - echo "Using model path: $model_dir" - echo "Using transcribe model path: $transcribe_model_dir" - echo "Using assets path: assets" - echo "Using index path: assets" - - sim_env=( - "SIMCTL_CHILD_CACTUS_TEST_MODEL=$model_dir" - "SIMCTL_CHILD_CACTUS_TEST_TRANSCRIBE_MODEL=$transcribe_model_dir" - "SIMCTL_CHILD_CACTUS_TEST_ASSETS=assets" - "SIMCTL_CHILD_CACTUS_INDEX_PATH=assets" - "SIMCTL_CHILD_CACTUS_NO_CLOUD_TELE=$CACTUS_NO_CLOUD_TELE" - "SIMCTL_CHILD_CACTUS_TEST_ONLY=${CACTUS_TEST_ONLY:-}" - ) - if [ "$RUN_ASR" = "1" ]; then - sim_env+=("SIMCTL_CHILD_CACTUS_RUN_ASR=1") - sim_env+=("SIMCTL_CHILD_CACTUS_ASR_AUDIO_FILE=$ASR_AUDIO_FILE") - fi - if [ -n "$CACTUS_CLOUD_KEY" ]; then - sim_env+=("SIMCTL_CHILD_CACTUS_CLOUD_KEY=$CACTUS_CLOUD_KEY") - fi - if [ -n "$CACTUS_CLOUD_STRICT_SSL" ]; then - sim_env+=("SIMCTL_CHILD_CACTUS_CLOUD_STRICT_SSL=$CACTUS_CLOUD_STRICT_SSL") - fi - if [ -n "$CACTUS_CLOUD_HANDOFF_THRESHOLD" ]; then - sim_env+=("SIMCTL_CHILD_CACTUS_CLOUD_HANDOFF_THRESHOLD=$CACTUS_CLOUD_HANDOFF_THRESHOLD") - fi - - env "${sim_env[@]}" xcrun simctl launch --console-pty "$device_uuid" "$bundle_id" -else - echo "Installing on: $device_name" - - if ! xcrun devicectl device install app --device "$device_uuid" "$app_path"; then - echo "Failed to install app on device" - echo "Common issues:" - echo " - Device not trusted" - echo " - Code signing failed" - echo " - Device locked or screen off" - exit 1 - fi - - if [ "$RUN_ASR" = "1" ]; then - echo "Launching ASR transcription..." - else - echo "Launching tests..." - fi - echo "(Logs will be fetched from device after completion)" - echo "Using model path: $model_dir" - echo "Using transcribe model path: $transcribe_model_dir" - echo "Using assets path: assets" - echo "Using index path: assets" - - device_env=( - "DEVICECTL_CHILD_CACTUS_TEST_MODEL=$model_dir" - "DEVICECTL_CHILD_CACTUS_TEST_TRANSCRIBE_MODEL=$transcribe_model_dir" - "DEVICECTL_CHILD_CACTUS_TEST_ASSETS=assets" - "DEVICECTL_CHILD_CACTUS_INDEX_PATH=assets" - "DEVICECTL_CHILD_CACTUS_NO_CLOUD_TELE=$CACTUS_NO_CLOUD_TELE" - "DEVICECTL_CHILD_CACTUS_TEST_ONLY=${CACTUS_TEST_ONLY:-}" - ) - if [ "$RUN_ASR" = "1" ]; then - device_env+=("DEVICECTL_CHILD_CACTUS_RUN_ASR=1") - device_env+=("DEVICECTL_CHILD_CACTUS_ASR_AUDIO_FILE=$ASR_AUDIO_FILE") - fi - if [ -n "$CACTUS_CLOUD_KEY" ]; then - device_env+=("DEVICECTL_CHILD_CACTUS_CLOUD_KEY=$CACTUS_CLOUD_KEY") - fi - if [ -n "$CACTUS_CLOUD_STRICT_SSL" ]; then - device_env+=("DEVICECTL_CHILD_CACTUS_CLOUD_STRICT_SSL=$CACTUS_CLOUD_STRICT_SSL") - fi - if [ -n "$CACTUS_CLOUD_HANDOFF_THRESHOLD" ]; then - device_env+=("DEVICECTL_CHILD_CACTUS_CLOUD_HANDOFF_THRESHOLD=$CACTUS_CLOUD_HANDOFF_THRESHOLD") - fi - - launch_output=$(env "${device_env[@]}" \ - xcrun devicectl device process launch --device "$device_uuid" "$bundle_id" 2>&1) || true - - echo "$launch_output" - - max_wait=300 - elapsed=0 - while [ $elapsed -lt $max_wait ]; do - if xcrun devicectl device info processes --device "$device_uuid" | grep -q "CactusTest.app/CactusTest"; then - sleep 2 - elapsed=$((elapsed + 2)) - else - break - fi - done - - if [ $elapsed -ge $max_wait ]; then - echo "Warning: Test execution timeout reached (${max_wait}s)" - fi - - sleep 1 - - echo "Fetching logs from device..." - - temp_log_dir=$(mktemp -d) - temp_log_file="$temp_log_dir/cactus_test.log" - - if xcrun devicectl device copy from \ - --device "$device_uuid" \ - --source "Documents/cactus_test.log" \ - --destination "$temp_log_file" \ - --domain-type appDataContainer \ - --domain-identifier "$bundle_id"; then - - if [ -f "$temp_log_file" ]; then - echo "" - cat "$temp_log_file" - else - echo "Warning: Could not find downloaded log file" - fi - - rm -rf "$temp_log_dir" - else - echo "Warning: Could not fetch log file from device" - fi - - if echo "$launch_output" | grep -q "FBSOpenApplicationErrorDomain error 3"; then - echo "" - echo "App launch failed: Untrusted Developer" - echo "To trust the developer profile on your device:" - echo " 1. Open Settings > General > VPN & Device Management" - echo " 2. Under Developer App, tap your Apple ID" - echo " 3. Tap Trust and confirm" - echo "Then run this script again." - echo "" - fi -fi diff --git a/tests/run.sh b/tests/run.sh deleted file mode 100755 index a08c135ab..000000000 --- a/tests/run.sh +++ /dev/null @@ -1,233 +0,0 @@ -#!/bin/bash - -echo "Running Cactus test suite..." -echo "============================" - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" - -DEFAULT_MODEL="LiquidAI/LFM2-VL-450M" -DEFAULT_TRANSCRIBE_MODEL="openai/whisper-base" - -MODEL_NAME="$DEFAULT_MODEL" -TRANSCRIBE_MODEL_NAME="$DEFAULT_TRANSCRIBE_MODEL" -ANDROID_MODE=false -IOS_MODE=false -NO_REBUILD=false -EXHAUSTIVE_MODE=false -ONLY_EXEC="" - -while [[ $# -gt 0 ]]; do - case $1 in - --model) - MODEL_NAME="$2" - shift 2 - ;; - --transcribe_model) - TRANSCRIBE_MODEL_NAME="$2" - shift 2 - ;; - --android) - ANDROID_MODE=true - shift - ;; - --ios) - IOS_MODE=true - shift - ;; - --no-rebuild) - NO_REBUILD=true - shift - ;; - --only) - ONLY_EXEC="$2" - shift 2 - ;; - --exhaustive) - EXHAUSTIVE_MODE=true - shift - ;; - --precision) - PRECISION="$2" - shift 2 - ;; - --help|-h) - echo "Usage: $0 [OPTIONS]" - echo "" - echo "Options:" - echo " --model Model to use for tests (default: $DEFAULT_MODEL)" - echo " --transcribe_model Transcribe model to use (default: $DEFAULT_TRANSCRIBE_MODEL)" - echo " --precision Precision for model conversion (MIXED, FP16, INT8, INT4)" - echo " --android Run tests on Android device or emulator" - echo " --ios Run tests on iOS device or simulator" - echo " --no-rebuild Skip building cactus library and tests" - echo " --exhaustive Run exhaustive golden tests for all model families and precisions" - echo " --only Only run the specified test (llm, vlm, stt, embed, rag, graph, index, kernel, kv_cache, performance)" - echo " --help, -h Show this help message" - exit 0 - ;; - *) - echo "Unknown option: $1" - echo "Use --help for usage information" - exit 1 - ;; - esac -done - -echo "" -echo "Using model: $MODEL_NAME" -echo "Using transcribe model: $TRANSCRIBE_MODEL_NAME" -if [ -n "$PRECISION" ]; then - echo "Using precision: $PRECISION" - PRECISION_FLAG="--precision $PRECISION" -else - PRECISION_FLAG="" -fi - -echo "" -SKIP_STANDARD_DOWNLOADS=false -if [ "$ONLY_EXEC" = "gemma4_suite" ]; then - SKIP_STANDARD_DOWNLOADS=true -fi - -if [ "$SKIP_STANDARD_DOWNLOADS" = false ]; then - echo "Step 1: Downloading model weights..." - if ! cactus download "$MODEL_NAME" $PRECISION_FLAG; then - echo "Failed to download model weights" - exit 1 - fi - - if ! cactus download "$TRANSCRIBE_MODEL_NAME" $PRECISION_FLAG; then - echo "Failed to download transcribe model weights" - exit 1 - fi -else - echo "Step 1: Skipping standard downloads for --only gemma4_suite" -fi - -echo "" -if [ "$ANDROID_MODE" = true ]; then - export CACTUS_TEST_ONLY="$ONLY_EXEC" - exec "$SCRIPT_DIR/android/run.sh" "$MODEL_NAME" "$TRANSCRIBE_MODEL_NAME" -fi - -if [ "$IOS_MODE" = true ]; then - export CACTUS_TEST_ONLY="$ONLY_EXEC" - exec "$SCRIPT_DIR/ios/run.sh" "$MODEL_NAME" "$TRANSCRIBE_MODEL_NAME" -fi - -if [ "$NO_REBUILD" = false ]; then - echo "Step 2: Building Cactus library..." - if ! cactus build; then - echo "Failed to build cactus library" - exit 1 - fi - - echo "" - echo "Step 3: Building tests..." - cd "$PROJECT_ROOT/tests" - - rm -rf build - mkdir -p build - cd build - - if ! cmake .. -DCMAKE_RULE_MESSAGES=OFF -DCMAKE_VERBOSE_MAKEFILE=OFF > /dev/null 2>&1; then - echo "Failed to configure tests" - exit 1 - fi - - if ! make -j$(nproc 2>/dev/null || echo 4); then - echo "Failed to build tests" - exit 1 - fi -else - echo "Skipping build (--no-rebuild)" - cd "$PROJECT_ROOT/tests/build" -fi - -echo "" -echo "Step 4: Running tests..." -echo "------------------------" - -# Set model path environment variables for tests -MODEL_DIR=$(echo "$MODEL_NAME" | sed 's|.*/||' | tr '[:upper:]' '[:lower:]') -TRANSCRIBE_MODEL_DIR=$(echo "$TRANSCRIBE_MODEL_NAME" | sed 's|.*/||' | tr '[:upper:]' '[:lower:]') -export CACTUS_TEST_MODEL="$PROJECT_ROOT/weights/$MODEL_DIR" -export CACTUS_TEST_GEMMA4_MODEL="${CACTUS_TEST_GEMMA4_MODEL:-$PROJECT_ROOT/weights/gemma4_int4}" -export CACTUS_TEST_TRANSCRIBE_MODEL="$PROJECT_ROOT/weights/$TRANSCRIBE_MODEL_DIR" -export CACTUS_TEST_ASSETS="$PROJECT_ROOT/tests/assets" -export CACTUS_INDEX_PATH="$PROJECT_ROOT/tests/assets" - -echo "Using model path: $CACTUS_TEST_MODEL" -echo "Using transcribe model path: $CACTUS_TEST_TRANSCRIBE_MODEL" -echo "Using assets path: $CACTUS_TEST_ASSETS" - -echo "Discovering test executables..." -test_executables=($(find . -maxdepth 1 -name "test_*" ! -name "test_exhaustive" -type f | sort)) - -executable_tests=() -for test_file in "${test_executables[@]}"; do - if [ -x "$test_file" ]; then - executable_tests+=("$test_file") - fi -done - -if [ ${#executable_tests[@]} -eq 0 ]; then - echo "No test executables found!" - exit 1 -fi - -test_executables=("${executable_tests[@]}") - -# If --only is set, execute only the named test -if [ -n "$ONLY_EXEC" ]; then - allowed=() - for test_file in "${executable_tests[@]}"; do - test_name=$(basename "$test_file" | sed 's/^test_//') - allowed+=("$test_name") - done - - ok=false - for a in "${allowed[@]}"; do - if [ "$a" = "$ONLY_EXEC" ]; then - ok=true - break - fi - done - if [ "$ok" = false ]; then - echo "Unknown test name: $ONLY_EXEC" - echo "Allowed: ${allowed[*]}" - exit 1 - fi - - target="./test_$ONLY_EXEC" - if [ ! -f "$target" ] || [ ! -x "$target" ]; then - echo "Could not find or execute test: $target" - exit 1 - fi - - test_executables=("$target") -fi - -echo "Found ${#test_executables[@]} test executable(s)" - -failures=0 -for executable in "${test_executables[@]}"; do - exec_name=$(basename "$executable") - if ! ./"$exec_name"; then - echo "Test executable failed: $exec_name" - failures=$((failures + 1)) - fi -done - -if [ "$failures" -ne 0 ]; then - echo "$failures test executable(s) failed" - exit 1 -fi - -if [ "$EXHAUSTIVE_MODE" = true ]; then - echo "" - echo "Step 5: Running exhaustive tests..." - echo "------------------------------------" - exec "$SCRIPT_DIR/golden/generate_exhaustive_golden.sh" -fi diff --git a/tests/test_audio_chat_pcm.cpp b/tests/test_audio_chat_pcm.cpp deleted file mode 100644 index 90190a4b4..000000000 --- a/tests/test_audio_chat_pcm.cpp +++ /dev/null @@ -1,77 +0,0 @@ -#include "../cactus/ffi/cactus_ffi.h" -#include "../libs/audio/wav.h" -#include -#include -#include -#include -#include - -static const char* MODEL_PATH = "/Users/noahcylich/Documents/Desert/cactus2/weights/gemma-4-e2b-it"; -static const char* WAV_PATH = "/Users/noahcylich/Documents/Desert/cactus2/tests/assets/record.wav"; - -void token_cb(const char* token, uint32_t, void*) { - std::cout << token << std::flush; -} - -int main() { - AudioFP32 wav = load_wav(WAV_PATH); - if (wav.samples.empty()) { - std::cerr << "FAIL: Could not load WAV file\n"; - return 1; - } - - std::vector pcm16(wav.samples.size()); - for (size_t i = 0; i < wav.samples.size(); i++) { - float s = wav.samples[i]; - if (s > 1.0f) s = 1.0f; - if (s < -1.0f) s = -1.0f; - pcm16[i] = static_cast(s * 32767.0f); - } - - const uint8_t* pcm_buffer = reinterpret_cast(pcm16.data()); - size_t pcm_buffer_size = pcm16.size() * sizeof(int16_t); - - cactus_model_t model = cactus_init(MODEL_PATH, nullptr, false); - if (!model) { - std::cerr << "FAIL: Could not load model\n"; - return 1; - } - - const char* messages = R"([{"role":"user","content":""}])"; - const char* options = R"({"max_tokens":128,"temperature":0.7,"enable_thinking_if_supported":false,"telemetry_enabled":false})"; - - char response[4096] = {0}; - std::cout << "Response: "; - int result = cactus_complete( - model, messages, response, sizeof(response), - options, nullptr, token_cb, nullptr, - pcm_buffer, pcm_buffer_size - ); - std::cout << "\n"; - - cactus_destroy(model); - - if (result < 0) { - std::cerr << "FAIL: cactus_complete returned error\n"; - return 1; - } - - std::string resp(response); - if (resp.find("\"success\":true") == std::string::npos) { - std::cerr << "FAIL: response not successful\n"; - return 1; - } - - if (resp.find("\"response\":\"\"") != std::string::npos) { - std::cerr << "FAIL: empty response\n"; - return 1; - } - - if (resp.find("sorry") != std::string::npos || resp.find("Sorry") != std::string::npos) { - std::cerr << "FAIL: model said sorry (likely didn't receive audio)\n"; - return 1; - } - - std::cout << "PASS\n"; - return 0; -} diff --git a/tests/test_audio_cosine.cpp b/tests/test_audio_cosine.cpp deleted file mode 100644 index 0546a43e0..000000000 --- a/tests/test_audio_cosine.cpp +++ /dev/null @@ -1,32 +0,0 @@ -#include "test_utils.h" -#include -#include -#include - -using namespace cactus::engine; -using TestUtils::load_bin; -using TestUtils::cosine_sim; - -int main() { - const char* model_path = std::getenv("CACTUS_TEST_GEMMA4_MODEL"); - std::string assets = std::getenv("CACTUS_TEST_ASSETS") ? std::getenv("CACTUS_TEST_ASSETS") : "../assets"; - - auto mel = load_bin(assets + "/test_wav_mel.bin"); - auto ref = load_bin(assets + "/test_wav_hf_projected.bin"); - if (mel.empty() || ref.empty()) { std::cerr << "Missing files\n"; return 1; } - - auto model = create_model(model_path); - if (!model || !model->init(model_path, 2048, "", false)) { std::cerr << "Init failed\n"; return 1; } - - auto cpp = model->get_audio_embeddings(mel); - float cos = cosine_sim(ref, cpp); - std::cout << "Real audio projected cosine (C++ vs HF FP32): " << cos << "\n"; - std::cout << "C++ size=" << cpp.size() << " ref size=" << ref.size() << "\n"; - - double cpp_sum = 0, ref_sum = 0; - for (auto v : cpp) cpp_sum += std::abs(v); - for (auto v : ref) ref_sum += std::abs(v); - std::cout << "C++ mean_abs=" << cpp_sum/cpp.size() << " ref mean_abs=" << ref_sum/ref.size() << "\n"; - - return 0; -} diff --git a/tests/test_embed.cpp b/tests/test_embed.cpp deleted file mode 100644 index 79e49043f..000000000 --- a/tests/test_embed.cpp +++ /dev/null @@ -1,129 +0,0 @@ -#include "test_utils.h" -#include -#include - -using namespace EngineTestUtils; - -static const char* g_model_path = std::getenv("CACTUS_TEST_MODEL"); -static const char* g_transcribe_model_path = std::getenv("CACTUS_TEST_TRANSCRIBE_MODEL"); -static const char* g_assets_path = std::getenv("CACTUS_TEST_ASSETS"); - -bool test_embeddings() { - std::cout << "\n╔══════════════════════════════════════════╗\n" - << "║ EMBEDDINGS TEST ║\n" - << "╚══════════════════════════════════════════╝\n"; - - cactus_model_t model = cactus_init(g_model_path, nullptr, false); - if (!model) return false; - - const char* texts[] = {"My name is Henry Ndubuaku", "Your name is Henry Ndubuaku"}; - std::vector emb1(2048), emb2(2048); - size_t dim1, dim2; - - Timer t1; - cactus_embed(model, texts[0], emb1.data(), emb1.size() * sizeof(float), &dim1, true); - double time1 = t1.elapsed_ms(); - - Timer t2; - cactus_embed(model, texts[1], emb2.data(), emb2.size() * sizeof(float), &dim2, true); - double time2 = t2.elapsed_ms(); - - float similarity = 0; - for (size_t i = 0; i < dim1; ++i) { - similarity += emb1[i] * emb2[i]; - } - - std::cout << "\n[Results]\n" - << "├─ Embedding dim: " << dim1 << "\n" - << "├─ Time (text1): " << std::fixed << std::setprecision(2) << time1 << "ms\n" - << "├─ Time (text2): " << time2 << "ms\n" - << "└─ Similarity: " << std::setprecision(4) << similarity << std::endl; - - cactus_destroy(model); - return true; -} - -static bool test_image_embeddings() { - std::cout << "\n╔══════════════════════════════════════════╗\n" - << "║ IMAGE EMBEDDING TEST ║\n" - << "╚══════════════════════════════════════════╝\n"; - - if (!g_model_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_MODEL not set\n"; - return true; - } - - std::string image_path = std::string(g_assets_path) + "/test_monkey.png"; - const size_t buffer_size = 1024 * 1024 * 4; - std::vector embeddings(buffer_size / sizeof(float)); - size_t embedding_dim = 0; - - cactus_model_t model = cactus_init(g_model_path, nullptr, false); - if (!model) { - std::cout << "⊘ SKIP │ Model doesn't support image embeddings\n"; - return true; - } - - Timer t; - int result = cactus_image_embed(model, image_path.c_str(), embeddings.data(), buffer_size, &embedding_dim); - double elapsed = t.elapsed_ms(); - - cactus_destroy(model); - - if (result == -1) { - std::cout << "⊘ SKIP │ Model doesn't support image embeddings\n"; - return true; - } - - std::cout << "├─ Embedding dim: " << embedding_dim << "\n" - << "└─ Time: " << std::fixed << std::setprecision(2) << elapsed << "ms" << std::endl; - - return result > 0 && embedding_dim > 0; -} - -static bool test_audio_embeddings() { - std::cout << "\n╔══════════════════════════════════════════╗\n" - << "║ AUDIO EMBEDDING TEST ║\n" - << "╚══════════════════════════════════════════╝\n"; - - if (!g_transcribe_model_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_TRANSCRIBE_MODEL not set\n"; - return true; - } - - const size_t buffer_size = 1024 * 1024; - std::vector embeddings(buffer_size / sizeof(float)); - size_t embedding_dim = 0; - - cactus_model_t model = cactus_init(g_transcribe_model_path, nullptr, false); - if (!model) { - std::cout << "⊘ SKIP │ Failed to init Whisper model\n"; - return true; - } - - std::string audio_path = std::string(g_assets_path) + "/test.wav"; - Timer t; - int result = cactus_audio_embed(model, audio_path.c_str(), embeddings.data(), buffer_size, &embedding_dim); - double elapsed = t.elapsed_ms(); - - cactus_destroy(model); - - if (result == -1) { - std::cout << "⊘ SKIP │ Model doesn't support audio embeddings\n"; - return true; - } - - std::cout << "├─ Embedding dim: " << embedding_dim << "\n" - << "└─ Time: " << std::fixed << std::setprecision(2) << elapsed << "ms" << std::endl; - - return result > 0 && embedding_dim > 0; -} - -int main() { - TestUtils::TestRunner runner("Embedding Tests"); - runner.run_test("embeddings", test_embeddings()); - runner.run_test("image_embeddings", test_image_embeddings()); - runner.run_test("audio_embeddings", test_audio_embeddings()); - runner.print_summary(); - return runner.all_passed() ? 0 : 1; -} diff --git a/tests/test_exhaustive.cpp b/tests/test_exhaustive.cpp deleted file mode 100644 index a6ccd95c3..000000000 --- a/tests/test_exhaustive.cpp +++ /dev/null @@ -1,253 +0,0 @@ -#include "test_utils.h" -#include -#include -#include -#include -#include -#include -#include - -using namespace EngineTestUtils; - -static const char* g_model_path = std::getenv("CACTUS_TEST_MODEL"); -static const char* g_transcribe_path = std::getenv("CACTUS_TEST_TRANSCRIBE_MODEL"); -static const char* g_assets_path = std::getenv("CACTUS_TEST_ASSETS"); -static const char* g_golden_file = std::getenv("CACTUS_TEST_GOLDEN_FILE"); -static const char* g_golden_family = std::getenv("CACTUS_TEST_GOLDEN_FAMILY"); -static const char* g_golden_prec = std::getenv("CACTUS_TEST_GOLDEN_PRECISION"); -static bool g_generate = std::getenv("CACTUS_GOLDEN_GENERATE") && - std::string(std::getenv("CACTUS_GOLDEN_GENERATE")) == "1"; - -static std::string read_file(const std::string& path) { - std::ifstream f(path); - if (!f.is_open()) return ""; - return std::string((std::istreambuf_iterator(f)), std::istreambuf_iterator()); -} - -static std::string find_golden_entry(const std::string& s, const std::string& family, const std::string& precision) { - size_t i = 0; - for (; i < s.size() && s[i] != '{'; i++); - while (i < s.size()) { - size_t start = i; - int depth = 1; - i++; - for (;i < s.size() && depth > 0; i++) { - if (s[i] == '"') { - i++; - for (;i < s.size() && s[i] != '"'; i++) { - if (s[i] == '\\') i++; - } - } - else if (s[i] == '{') depth++; - else if (s[i] == '}') depth--; - } - std::string entry = s.substr(start, i - start); - if (json_string(entry, "model_family") == family && json_string(entry, "precision") == precision) - return entry; - for (; i < s.size() && s[i] != '{'; i++); - } - return ""; -} - -static std::string tolower(const std::string& s) { - std::string o = s; - std::transform(o.begin(), o.end(), o.begin(), [](unsigned char c) { return std::tolower(c); }); - return o; -} - -static std::string stream_text(const StreamingData& d) { - std::string t; - for (const auto& s : d.tokens) t += s; - return t; -} - -static float compute_wer(const std::string& hyp, const std::string& ref) { - auto split = [](const std::string& s) { - std::vector w; - std::istringstream iss(s); - std::string tok; - while (iss >> tok) { - std::string c; - for (unsigned char ch : tok) if (std::isalnum(ch)) c += std::tolower(ch); - if (!c.empty()) w.push_back(c); - } - return w; - }; - auto h = split(hyp), r = split(ref); - if (r.empty()) return h.empty() ? 0.0f : 1.0f; - size_t n = r.size(), m = h.size(); - std::vector> dp(n + 1, std::vector(m + 1)); - for (size_t i = 0; i <= n; i++) dp[i][0] = i; - for (size_t j = 0; j <= m; j++) dp[0][j] = j; - for (size_t i = 1; i <= n; i++) - for (size_t j = 1; j <= m; j++) - dp[i][j] = r[i-1] == h[j-1] ? dp[i-1][j-1] - : 1 + std::min({dp[i-1][j], dp[i][j-1], dp[i-1][j-1]}); - return float(dp[n][m]) / float(n); -} - -static float cosine_sim(const float* a, const float* b, size_t n) { - float dot = 0, na = 0, nb = 0; - for (size_t i = 0; i < n; i++) { dot += a[i]*b[i]; na += a[i]*a[i]; nb += b[i]*b[i]; } - return (na == 0 || nb == 0) ? 0.0f : dot / (std::sqrt(na) * std::sqrt(nb)); -} - -static std::vector parse_float_array(const std::string& json, const std::string& key) { - std::string pat = "\"" + key + "\":"; - size_t pos = json.find(pat); - if (pos == std::string::npos) return {}; - size_t s = pos + pat.size(); - while (s < json.size() && json[s] == ' ') s++; - if (s >= json.size() || json[s] != '[') return {}; - int depth = 1; size_t e = s + 1; - while (e < json.size() && depth > 0) { if (json[e] == '[') depth++; else if (json[e] == ']') depth--; e++; } - std::vector r; - std::istringstream iss(json.substr(s + 1, e - s - 2)); - std::string tok; - while (std::getline(iss, tok, ',')) { try { r.push_back(std::stof(tok)); } catch (...) {} } - return r; -} - -static std::string resolve_asset_paths(const std::string& messages) { - if (!g_assets_path) return messages; - std::string prefix = std::string(g_assets_path) + "/"; - std::string result = messages; - for (const char* ext : {".png", ".jpg", ".jpeg", ".wav", ".mp3"}) { - size_t pos = 0; - while ((pos = result.find(ext, pos)) != std::string::npos) { - size_t name_start = pos; - while (name_start > 0 && result[name_start - 1] != '"') name_start--; - std::string name = result.substr(name_start, pos + strlen(ext) - name_start); - if (name.find('/') == std::string::npos) { - result.replace(name_start, name.size(), prefix + name); - pos = name_start + prefix.size() + name.size(); - } else { - pos += strlen(ext); - } - } - } - return result; -} - -static bool test_completion_golden(const std::string& golden) { - std::string messages = resolve_asset_paths(json_string(golden, "input_messages")); - std::string options = json_string(golden, "options"); - std::string expected = tolower(json_string(golden, "expected_output")); - int min_tok = (int)json_number(golden, "token_count_min", 1); - int max_tok = (int)json_number(golden, "token_count_max", 256); - - if (!g_model_path) { std::cerr << "CACTUS_TEST_MODEL not set\n"; return false; } - cactus_model_t model = cactus_init(g_model_path, nullptr, false); - if (!model) { std::cerr << "Failed to init model\n"; return false; } - - StreamingData data; data.model = model; - char resp[8192] = {}; - std::cout << "Response: "; - int rc = cactus_complete(model, messages.c_str(), resp, sizeof(resp), - options.c_str(), nullptr, stream_callback, &data, nullptr, 0); - std::cout << "\n"; - cactus_destroy(model); - if (rc <= 0) return false; - - std::string gen = tolower(stream_text(data)); - - if (g_generate) { - std::cout << " [GOLDEN] output: " << stream_text(data) << "\n" - << " [GOLDEN] tokens: " << data.token_count << "\n"; - return true; - } - - bool match = gen.find(expected) != std::string::npos; - bool range = data.token_count >= min_tok && data.token_count <= max_tok; - std::cout << " expected: \"" << expected << "\" match=" << (match?"Y":"N") - << " tokens=" << data.token_count << " [" << min_tok << "," << max_tok << "]\n"; - return match && range; -} - -static bool test_embedding_golden(const std::string& golden) { - std::string text = json_string(golden, "input_text"); - int exp_dim = (int)json_number(golden, "expected_embedding_dim", 0); - float threshold = (float)json_number(golden, "cosine_similarity_threshold", 0.99); - auto ref = parse_float_array(golden, "reference_embedding"); - - if (!g_model_path) { std::cerr << "CACTUS_TEST_MODEL not set\n"; return false; } - cactus_model_t model = cactus_init(g_model_path, nullptr, false); - if (!model) { std::cerr << "Failed to init model\n"; return false; } - - std::vector emb(4096); size_t dim = 0; - int rc = cactus_embed(model, text.c_str(), emb.data(), emb.size() * sizeof(float), &dim, true); - cactus_destroy(model); - if (rc <= 0 || dim == 0) return false; - - if (g_generate) { - std::cout << " [GOLDEN] dim: " << dim << "\n"; - return true; - } - - bool dim_ok = exp_dim == 0 || (int)dim == exp_dim; - float sim = (!ref.empty() && ref.size() == dim) ? cosine_sim(emb.data(), ref.data(), dim) : 0.0f; - bool sim_ok = ref.empty() || (ref.size() == dim && sim >= threshold); - std::cout << " dim=" << dim << " sim=" << std::fixed << std::setprecision(4) << sim - << " threshold=" << threshold << "\n"; - return dim_ok && sim_ok; -} - -static bool test_stt_golden(const std::string& golden, const std::string& family) { - std::string audio = json_string(golden, "audio_file"); - std::string reference = json_string(golden, "reference_transcript"); - float threshold = (float)json_number(golden, "wer_threshold", 0.15); - - if (!g_transcribe_path) { std::cerr << "CACTUS_TEST_TRANSCRIBE_MODEL not set\n"; return false; } - if (!g_assets_path) { std::cerr << "CACTUS_TEST_ASSETS not set\n"; return false; } - cactus_model_t model = cactus_init(g_transcribe_path, nullptr, false); - if (!model) { std::cerr << "Failed to init model\n"; return false; } - - std::string path = std::string(g_assets_path) + "/" + audio; - const char* prompt = family == "whisper" ? "<|startoftranscript|><|en|><|transcribe|><|notimestamps|>" : ""; - StreamingData data; data.model = model; - char resp[1 << 15] = {}; - std::cout << "Transcript: "; - int rc = cactus_transcribe(model, path.c_str(), prompt, resp, sizeof(resp), - R"({"max_tokens":100,"telemetry_enabled":false})", - stream_callback, &data, nullptr, 0); - std::cout << "\n"; - cactus_destroy(model); - if (rc <= 0) return false; - - std::string transcript = stream_text(data); - if (g_generate) { - std::cout << " [GOLDEN] transcript: " << transcript << "\n"; - return true; - } - - float wer = compute_wer(transcript, reference); - std::cout << " ref=\"" << reference << "\" wer=" << std::fixed << std::setprecision(4) << wer - << " threshold=" << threshold << "\n"; - return wer <= threshold; -} - -int main() { - if (!g_golden_file || !g_golden_family || !g_golden_prec) { - std::cout << "Skipping exhaustive tests (CACTUS_TEST_GOLDEN_FILE, CACTUS_TEST_GOLDEN_FAMILY, CACTUS_TEST_GOLDEN_PRECISION not set)\n"; - return 0; - } - - std::string all = read_file(g_golden_file); - if (all.empty()) { std::cerr << "Cannot read: " << g_golden_file << "\n"; return 1; } - std::string json = find_golden_entry(all, g_golden_family, g_golden_prec); - if (json.empty()) { std::cerr << "No golden entry for " << g_golden_family << "/" << g_golden_prec << "\n"; return 1; } - - std::string type = json_string(json, "test_type"); - std::string family(g_golden_family); - std::string label = type + "_" + family + "_" + g_golden_prec; - - TestUtils::TestRunner runner("Exhaustive Tests"); - - if (type == "llm" || type == "vlm") runner.run_test(label, test_completion_golden(json)); - else if (type == "embedding") runner.run_test(label, test_embedding_golden(json)); - else if (type == "stt") runner.run_test(label, test_stt_golden(json, family)); - else { std::cerr << "Unknown test_type: " << type << "\n"; return 1; } - - runner.print_summary(); - return runner.all_passed() ? 0 : 1; -} diff --git a/tests/test_gemma4_audio.cpp b/tests/test_gemma4_audio.cpp deleted file mode 100644 index 110ac9e6d..000000000 --- a/tests/test_gemma4_audio.cpp +++ /dev/null @@ -1,101 +0,0 @@ -#include "test_utils.h" -#include -#include -#include -#include - -using namespace cactus::engine; -using TestUtils::load_bin; - -static std::string get_assets_dir() { - const char* dir = std::getenv("CACTUS_TEST_ASSETS"); - if (dir) return dir; - return "../assets"; -} - -bool test_real_audio_transcription() { - const char* model_path = std::getenv("CACTUS_TEST_GEMMA4_MODEL"); - std::string assets = get_assets_dir(); - if (!model_path) { - std::cerr << " SKIP: CACTUS_TEST_GEMMA4_MODEL not set\n"; - return true; - } - - auto mel = load_bin(assets + "/test_wav_mel.bin"); - if (mel.empty()) { - std::cerr << " SKIP: run python tests/generate_audio_reference.py and ensure test_wav_mel.bin exists\n"; - return true; - } - - size_t num_frames = mel.size() / 128; - std::cout << " Mel: " << num_frames << " frames x 128 bins\n"; - - auto model = create_model(model_path); - if (!model || !model->init(model_path, 2048, "", true)) { - std::cerr << " FAIL: model init\n"; - return false; - } - - auto* tokenizer = model->get_tokenizer(); - if (!tokenizer) { - std::cerr << " FAIL: no tokenizer\n"; - return false; - } - - uint32_t audio_token_id = model->get_config().audio_token_id; - if (audio_token_id == 0) audio_token_id = 258881; - - size_t after_stage1 = (num_frames + 1) / 2; - size_t num_soft_tokens = (after_stage1 + 1) / 2; - std::cout << " Encoder will produce " << num_soft_tokens << " soft tokens (computed from " << num_frames << " frames)\n"; - - std::string prompt_text = "Transcribe the audio."; - auto text_before = tokenizer->encode("<|turn>user\n" + prompt_text + "<|audio>"); - auto text_after = tokenizer->encode("\n<|turn>model\n"); - - std::vector tokens; - tokens.insert(tokens.end(), text_before.begin(), text_before.end()); - for (size_t i = 0; i < num_soft_tokens; i++) - tokens.push_back(audio_token_id); - tokens.insert(tokens.end(), text_after.begin(), text_after.end()); - - std::cout << " Tokens: " << tokens.size() << " (" << text_before.size() << " + " << num_soft_tokens << " audio + " << text_after.size() << ")\n"; - - std::cout << " Generating...\n"; - - std::string output; - for (int i = 0; i < 100; i++) { - uint32_t token = model->decode_with_audio(tokens, mel, 0.0f, 1.0f, 1, ""); - std::string piece = tokenizer->decode({token}); - output += piece; - tokens.push_back(token); - - if (output.find("") != std::string::npos || output.find("") != std::string::npos) - break; - } - - std::cout << " Output: \"" << output << "\"\n"; - - if (output.empty()) { - std::cerr << " FAIL: empty output\n"; - return false; - } - - bool has_content = false; - for (char c : output) { - if (std::isalpha(c)) { has_content = true; break; } - } - if (!has_content) { - std::cerr << " FAIL: output has no alphabetic content\n"; - return false; - } - - return true; -} - -int main() { - std::cout << "[1] real_audio_transcription:\n"; - bool ok = test_real_audio_transcription(); - std::cout << " -> " << (ok ? "PASS" : "FAIL") << "\n"; - return ok ? 0 : 1; -} diff --git a/tests/test_gemma4_audio_image_audio.cpp b/tests/test_gemma4_audio_image_audio.cpp deleted file mode 100644 index 8958bcb9b..000000000 --- a/tests/test_gemma4_audio_image_audio.cpp +++ /dev/null @@ -1,196 +0,0 @@ -#include "../cactus/ffi/cactus_ffi.h" -#include "../libs/audio/wav.h" -#include -#include -#include -#include -#include -#include -#include - -namespace fs = std::filesystem; - -static std::string escape_json(const std::string& s) { - std::string out; - out.reserve(s.size() + 8); - for (char c : s) { - switch (c) { - case '"': out += "\\\""; break; - case '\\': out += "\\\\"; break; - case '\b': out += "\\b"; break; - case '\f': out += "\\f"; break; - case '\n': out += "\\n"; break; - case '\r': out += "\\r"; break; - case '\t': out += "\\t"; break; - default: - if (static_cast(c) < 0x20) { - char buf[8]; - std::snprintf(buf, sizeof(buf), "\\u%04x", c); - out += buf; - } else { - out += c; - } - } - } - return out; -} - -static std::string unescape_json(const std::string& s) { - std::string out; - out.reserve(s.size()); - for (size_t i = 0; i < s.size(); i++) { - if (s[i] == '\\' && i + 1 < s.size()) { - switch (s[i + 1]) { - case '"': out += '"'; i++; break; - case '\\': out += '\\'; i++; break; - case 'n': out += '\n'; i++; break; - case 'r': out += '\r'; i++; break; - case 't': out += '\t'; i++; break; - case 'b': out += '\b'; i++; break; - case 'f': out += '\f'; i++; break; - default: out += s[i]; break; - } - } else { - out += s[i]; - } - } - return out; -} - -static std::string extract_response_field(const std::string& json) { - const std::string key = "\"response\":\""; - size_t start = json.find(key); - if (start == std::string::npos) return ""; - start += key.size(); - size_t end = start; - while (end < json.size()) { - end = json.find('"', end); - if (end == std::string::npos) return ""; - size_t back = 0; - for (size_t i = end; i > start && json[i - 1] == '\\'; i--) back++; - if (back % 2 == 0) break; - end++; - } - return unescape_json(json.substr(start, end - start)); -} - -static std::string ensure_wav_from_mp3(const std::string& mp3_path) { - fs::path src(mp3_path); - fs::path out = fs::temp_directory_path() / (src.stem().string() + "_cactus_test_16k.wav"); - if (fs::exists(out)) return out.string(); - std::string cmd = "afconvert -f WAVE -d LEI16@16000 -c 1 " - "'" + mp3_path + "' '" + out.string() + "' >/dev/null 2>&1"; - if (std::system(cmd.c_str()) != 0) return ""; - return fs::exists(out) ? out.string() : ""; -} - -static std::vector wav_to_pcm16(const std::string& wav_path) { - AudioFP32 wav = load_wav(wav_path); - std::vector pcm(wav.samples.size()); - for (size_t i = 0; i < wav.samples.size(); i++) { - float s = wav.samples[i]; - if (s > 1.0f) s = 1.0f; - if (s < -1.0f) s = -1.0f; - pcm[i] = static_cast(s * 32767.0f); - } - std::vector bytes(pcm.size() * sizeof(int16_t)); - std::memcpy(bytes.data(), pcm.data(), bytes.size()); - return bytes; -} - -struct TurnResult { - bool ok = false; - std::string response; -}; - -static TurnResult run_turn(cactus_model_t model, - const std::string& messages_json, - const std::vector& pcm) { - const char* options = R"({"max_tokens":128,"temperature":0.0,"top_p":1.0,"top_k":1,"auto_handoff":false,"telemetry_enabled":false})"; - std::vector buf(64 * 1024, 0); - int rc = cactus_complete( - model, messages_json.c_str(), buf.data(), buf.size(), - options, nullptr, nullptr, nullptr, - pcm.empty() ? nullptr : pcm.data(), pcm.size() - ); - TurnResult r; - if (rc < 0) { - std::cerr << " cactus_complete failed: " << buf.data() << "\n"; - return r; - } - r.response = extract_response_field(std::string(buf.data())); - r.ok = !r.response.empty(); - return r; -} - -int main() { - const char* model_path = std::getenv("CACTUS_TEST_GEMMA4_MODEL"); - if (!model_path) { - std::cerr << "SKIP: CACTUS_TEST_GEMMA4_MODEL not set\n"; - return 0; - } - const char* repo_root_env = std::getenv("CACTUS_TEST_REPO_ROOT"); - fs::path repo_root = repo_root_env ? fs::path(repo_root_env) : fs::current_path() / ".."; - - fs::path who_mp3 = repo_root / "who_are_you.mp3"; - fs::path math_mp3 = repo_root / "2+2.mp3"; - fs::path banner = repo_root / "assets" / "banner.jpg"; - - for (const auto& p : {who_mp3, math_mp3, banner}) { - if (!fs::exists(p)) { - std::cerr << "SKIP: missing asset " << p << "\n"; - return 0; - } - } - - std::string who_wav = ensure_wav_from_mp3(who_mp3.string()); - std::string math_wav = ensure_wav_from_mp3(math_mp3.string()); - if (who_wav.empty() || math_wav.empty()) { - std::cerr << "SKIP: afconvert failed (needed to transcode mp3 -> wav)\n"; - return 0; - } - - auto who_pcm = wav_to_pcm16(who_wav); - auto math_pcm = wav_to_pcm16(math_wav); - - cactus_model_t model = cactus_init(model_path, nullptr, false); - if (!model) { - std::cerr << "FAIL: cactus_init\n"; - return 1; - } - - std::string m1 = R"([{"role":"user","content":""}])"; - std::cout << "Turn 1 (audio who_are_you.mp3):\n"; - auto t1 = run_turn(model, m1, who_pcm); - if (!t1.ok) { cactus_destroy(model); return 1; } - std::cout << " -> " << t1.response << "\n"; - - std::string m2 = - std::string(R"([{"role":"user","content":""},)") + - R"({"role":"assistant","content":")" + escape_json(t1.response) + R"("},)" + - R"({"role":"user","content":"describe this","images":[")" + escape_json(banner.string()) + R"("]}])"; - std::cout << "Turn 2 (image + 'describe this'):\n"; - auto t2 = run_turn(model, m2, {}); - if (!t2.ok) { cactus_destroy(model); return 1; } - std::cout << " -> " << t2.response << "\n"; - - std::string m3 = - std::string(R"([{"role":"user","content":""},)") + - R"({"role":"assistant","content":")" + escape_json(t1.response) + R"("},)" + - R"({"role":"user","content":"describe this","images":[")" + escape_json(banner.string()) + R"("]},)" + - R"({"role":"assistant","content":")" + escape_json(t2.response) + R"("},)" + - R"({"role":"user","content":"","images":[")" + escape_json(banner.string()) + R"("]}])"; - std::cout << "Turn 3 (audio 2+2.mp3, image still attached):\n"; - auto t3 = run_turn(model, m3, math_pcm); - if (!t3.ok) { cactus_destroy(model); return 1; } - std::cout << " -> " << t3.response << "\n"; - - cactus_destroy(model); - - if (t3.response == t2.response) { - std::cerr << "FAIL: turn 3 response identical to turn 2 — new audio is being ignored\n"; - return 1; - } - std::cout << "PASS\n"; - return 0; -} diff --git a/tests/test_gemma4_audio_layers.cpp b/tests/test_gemma4_audio_layers.cpp deleted file mode 100644 index c937e6ada..000000000 --- a/tests/test_gemma4_audio_layers.cpp +++ /dev/null @@ -1,58 +0,0 @@ -#include "test_utils.h" -#include -#include -#include -#include - -using namespace cactus::engine; -using TestUtils::load_bin; -using TestUtils::cosine_sim; - -static std::vector extract(CactusGraph* gb, size_t node) { - gb->execute(); - const auto& buf = gb->get_output_buffer(node); - size_t n = buf.total_size; - std::vector out(n); - const __fp16* src = buf.data_as<__fp16>(); - for (size_t i = 0; i < n; i++) out[i] = static_cast(src[i]); - return out; -} - -int main() { - const char* model_path = std::getenv("CACTUS_TEST_GEMMA4_MODEL"); - std::string assets = std::getenv("CACTUS_TEST_ASSETS") ? std::getenv("CACTUS_TEST_ASSETS") : "../assets"; - if (!model_path) { std::cerr << "Set CACTUS_TEST_GEMMA4_MODEL\n"; return 1; } - - auto mel = load_bin(assets + "/audio_test_mel_input.bin"); - if (mel.empty()) { std::cerr << "No mel input\n"; return 1; } - - auto model = create_model(model_path); - if (!model || !model->init(model_path, 2048, "", false)) { std::cerr << "Init failed\n"; return 1; } - - auto* mm = dynamic_cast(model.get()); - auto* gb = static_cast(model->graph_handle_); - size_t num_frames = mel.size() / 128; - - gb->soft_reset(); - auto backend = ComputeBackend::CPU; - auto& enc = mm->audio_encoder(); - size_t hidden = enc.build_sscp(gb, mel, num_frames, backend); - auto ctx = enc.build_conformer_context(gb, hidden); - - for (uint32_t i = 0; i < 12; i++) { - size_t ffw_s = enc.build_conformer_ffw(gb, hidden, i, false, backend); - size_t attn = enc.build_conformer_attention(gb, ffw_s, i, ctx, backend); - size_t lconv = enc.build_conformer_lconv1d(gb, attn, i, backend); - size_t ffw_e = enc.build_conformer_ffw(gb, lconv, i, true, backend); - hidden = gb->rms_norm(ffw_e, enc.audio_weights_.layers[i].block_norm, - enc.get_config().audio_rms_norm_eps); - - auto cpp = extract(gb, hidden); - char buf[64]; - snprintf(buf, sizeof(buf), "%s/audio_ref_L%02d_block.bin", assets.c_str(), i); - auto ref = load_bin(buf); - float cos = ref.empty() ? -1 : cosine_sim(ref, cpp); - std::cout << "L" << i << "_block: cos=" << cos << "\n"; - } - return 0; -} diff --git a/tests/test_gemma4_suite.cpp b/tests/test_gemma4_suite.cpp deleted file mode 100644 index b249859bc..000000000 --- a/tests/test_gemma4_suite.cpp +++ /dev/null @@ -1,599 +0,0 @@ -#include "test_utils.h" -#include "../cactus/models/gemma4/model_gemma4.h" -#include "../cactus/ffi/cactus_utils.h" -#include "../libs/audio/wav.h" -#include -#include -#include -#include -#include -#include -#include - -using namespace cactus::engine; -using namespace cactus::audio; -using namespace EngineTestUtils; - -static const char* get_model_path() { - const char* path = std::getenv("CACTUS_TEST_GEMMA4_MODEL"); - if (path) return path; - return std::getenv("CACTUS_TEST_MODEL"); -} - -static const char* get_image_path() { - return std::getenv("CACTUS_TEST_IMAGE"); -} - -static std::string get_audio_path() { - const char* dir = std::getenv("CACTUS_TEST_ASSETS"); - if (dir) return std::string(dir) + "/test.wav"; - return "../assets/test.wav"; -} - -static std::string get_assets_dir() { - const char* dir = std::getenv("CACTUS_TEST_ASSETS"); - if (dir) return dir; - return "../assets"; -} - -static bool has_npu_package(const char* model_path, const std::string& name) { - struct stat st; - return stat((std::string(model_path) + "/" + name).c_str(), &st) == 0; -} - -static const char* g_options = R"({ - "max_tokens": 256, - "temperature": 0, - "top_k": 1, - "stop_sequences": ["", "", "", "<|im_end|>"], - "enable_thinking_if_supported": false, - "auto_handoff": false, - "telemetry_enabled": false -})"; - -struct LocalPerfMetrics { - double time_to_first_token_ms = 0.0; - double total_time_ms = 0.0; - double prefill_tps = 0.0; - double decode_tps = 0.0; - size_t prompt_tokens = 0; - size_t completion_tokens = 0; -}; - -struct EncoderOnlyMetrics { - bool collected = false; - bool npu_enabled = false; - std::string input_label = "in_units"; - std::string output_label = "out_units"; - size_t input_units = 0; - size_t output_units = 0; - double encoder_ms = 0.0; - double output_units_per_s = 0.0; -}; - -static LocalPerfMetrics compute_local_perf_metrics(size_t prompt_tokens, - size_t completion_tokens, - double ttft_ms, - double total_time_ms) { - LocalPerfMetrics m; - m.time_to_first_token_ms = ttft_ms; - m.total_time_ms = total_time_ms; - m.prompt_tokens = prompt_tokens; - m.completion_tokens = completion_tokens; - m.prefill_tps = ttft_ms > 0.0 ? (static_cast(prompt_tokens) * 1000.0) / ttft_ms : 0.0; - const double decode_time_ms = std::max(0.0, total_time_ms - ttft_ms); - m.decode_tps = (completion_tokens > 1 && decode_time_ms > 0.0) - ? ((static_cast(completion_tokens) - 1.0) * 1000.0) / decode_time_ms - : 0.0; - return m; -} - -static void print_local_perf_metrics(const LocalPerfMetrics& m, - const EncoderOnlyMetrics* encoder_metrics = nullptr) { - std::streamsize old_precision = std::cout.precision(); - std::ios::fmtflags old_flags = std::cout.flags(); - std::cout << std::fixed << std::setprecision(2); - std::cout << " Performance:\n"; - if (encoder_metrics && encoder_metrics->collected) { - std::cout << " encoder_only : " << encoder_metrics->encoder_ms << " ms" - << " | mode=" << (encoder_metrics->npu_enabled ? "npu" : "cpu") - << " | " << encoder_metrics->input_label << "=" << encoder_metrics->input_units - << " | " << encoder_metrics->output_label << "=" << encoder_metrics->output_units - << " | " << encoder_metrics->output_label << "_per_s=" << encoder_metrics->output_units_per_s - << "\n"; - } - std::cout << " end_to_end : " - << "ttft_ms=" << m.time_to_first_token_ms - << " | total_time_ms=" << m.total_time_ms - << "\n"; - std::cout << " throughput : " - << "prefill_tps=" << m.prefill_tps - << " | decode_tps=" << m.decode_tps - << "\n"; - std::cout << " token_counts : " - << "prompt=" << m.prompt_tokens - << " | completion=" << m.completion_tokens - << "\n"; - std::cout.flags(old_flags); - std::cout.precision(old_precision); -} - -static std::string preview_text(const std::string& text, size_t max_chars = 240) { - if (text.size() <= max_chars) return text; - return text.substr(0, max_chars) + "..."; -} - -static void print_modality_box(const std::string& title, const std::string& prompt) { - std::cout << "\n╔══════════════════════════════════════════╗\n" - << "║" << std::setw(42) << std::left << std::string(" ") + title << "║\n" - << "╚══════════════════════════════════════════╝\n"; - if (!prompt.empty()) { - std::cout << "├─ User prompt: " << prompt << "\n"; - } -} - - -bool test_text_generation() { - const char* model_path = get_model_path(); - if (!model_path) { - std::cerr << " SKIP: model path not set\n"; - return true; - } - - const char* messages = R"([ - {"role": "system", "content": "/no_think You are a helpful assistant. Be concise."}, - {"role": "user", "content": "What is the capital of France?"} - ])"; - - return EngineTestUtils::run_test("TEXT GENERATION", model_path, messages, g_options, - [](int result, const StreamingData& data, const std::string& /*response*/, const Metrics& m) { - std::string text; - for (const auto& t : data.tokens) text += t; - std::string lower_text; - for (char c : text) lower_text += std::tolower(c); - - bool has_paris = lower_text.find("paris") != std::string::npos; - std::cout << "├─ Output: " << text.substr(0, 200) << "\n" - << "├─ Contains 'paris': " << (has_paris ? "YES" : "NO") << "\n"; - m.print_json(); - return result > 0 && data.token_count > 0 && has_paris; - }, nullptr, -1, "What is the capital of France?"); -} - - -bool test_tool_call() { - const char* model_path = get_model_path(); - if (!model_path) { - std::cerr << " SKIP: model path not set\n"; - return true; - } - - const char* messages = R"([ - {"role": "system", "content": "/no_think You are a helpful assistant that can use tools."}, - {"role": "user", "content": "What's the weather in Tokyo?"} - ])"; - - const char* tools = R"([{ - "type": "function", - "function": { - "name": "get_weather", - "description": "Get weather for a location", - "parameters": { - "type": "object", - "properties": { - "location": {"type": "string", "description": "City, Country"} - }, - "required": ["location"] - } - } - }])"; - - const char* options = R"({ - "max_tokens": 256, - "temperature": 0, - "top_k": 1, - "stop_sequences": ["", "", "", "<|im_end|>"], - "enable_thinking_if_supported": false, - "auto_handoff": false, - "force_tools": true, - "telemetry_enabled": false - })"; - - return EngineTestUtils::run_test("TOOL CALL", model_path, messages, options, - [](int result, const StreamingData&, const std::string& response, const Metrics& m) { - bool has_function = response.find("\"function_calls\":[") != std::string::npos; - bool has_tool = has_function && response.find("get_weather") != std::string::npos; - std::cout << "├─ Function call: " << (has_function ? "YES" : "NO") << "\n" - << "├─ Correct tool: " << (has_tool ? "YES" : "NO") << "\n"; - m.print_json(); - return result > 0 && has_function && has_tool; - }, tools, -1, "What's the weather in Tokyo?"); -} - - -bool test_1k_context() { - const char* model_path = get_model_path(); - if (!model_path) { - std::cerr << " SKIP: model path not set\n"; - return true; - } - - std::string msg = "[{\"role\": \"system\", \"content\": \"/no_think You are helpful. "; - for (int i = 0; i < 50; i++) - msg += "Context " + std::to_string(i) + ": The quick brown fox jumps over the lazy dog. "; - msg += "\"}, {\"role\": \"user\", \"content\": \""; - for (int i = 0; i < 50; i++) - msg += "Data point " + std::to_string(i) + " = " + std::to_string(i * 2.71828) + ". "; - msg += "Summarize the data briefly.\"}]"; - - return EngineTestUtils::run_test("1K CONTEXT", model_path, msg.c_str(), g_options, - [](int result, const StreamingData& data, const std::string&, const Metrics& m) { - std::cout << "├─ Tokens generated: " << data.token_count << "\n"; - m.print_json(); - return result > 0 && data.token_count > 0; - }, nullptr, 100, "Summarize the data briefly."); -} - - -bool test_gemma4_vision(bool expect_npu) { - const char* model_path = get_model_path(); - const char* image_path = get_image_path(); - if (!model_path || !image_path) { - std::cerr << " SKIP: CACTUS_TEST_GEMMA4_MODEL or CACTUS_TEST_IMAGE not set\n"; - return true; - } - - if (expect_npu && !has_npu_package(model_path, "vision_encoder.mlpackage")) { - std::cerr << " SKIP: vision_encoder.mlpackage not found in model folder\n"; - return true; - } - - print_modality_box(expect_npu ? "VISION NPU" : "VISION", "Describe this image briefly."); - - auto model = create_model(model_path); - if (!model) { - std::cerr << " FAIL: create_model returned null\n"; - return false; - } - - auto* mm = dynamic_cast(model.get()); - if (mm && !expect_npu) { - mm->vision_encoder().disable_npu_ = true; - } - - if (!model->init(model_path, 2048, "", false)) { - std::cerr << " FAIL: model init\n"; - return false; - } - - auto* tokenizer = model->get_tokenizer(); - if (!tokenizer) { - std::cerr << " FAIL: no tokenizer\n"; - return false; - } - - std::vector messages; - messages.push_back({"user", "Describe this image briefly.", "", {image_path}, {}, 0, {}}); - std::string prompt = tokenizer->format_chat_prompt(messages, true, "", false); - auto tokens = tokenizer->encode(prompt); - - uint32_t image_token_id = model->get_config().image_token_id; - if (image_token_id == 0) - image_token_id = 258880; - - size_t vision_count = 0; - for (auto t : tokens) { - if (t == image_token_id) - vision_count++; - } - std::cout << " tokens: " << tokens.size() - << ", image_token_id: " << image_token_id - << ", vision soft tokens: " << vision_count << "\n"; - const size_t prompt_token_count = tokens.size(); - - EncoderOnlyMetrics encoder_only_metrics; - encoder_only_metrics.input_label = "in_patches"; - encoder_only_metrics.output_label = "out_steps"; - if (mm) { - auto preprocessed = mm->vision_encoder().preprocess_image(image_path); - encoder_only_metrics.input_units = preprocessed.num_patches; - encoder_only_metrics.npu_enabled = mm->vision_encoder().use_npu_encoder_; - if (model->graph_handle_) { - auto* gb = static_cast(model->graph_handle_); - auto backend = model->get_config().default_backend == Config::Backend::CPU ? ComputeBackend::CPU : ComputeBackend::NPU; - gb->soft_reset(); - auto encoder_start = std::chrono::high_resolution_clock::now(); - size_t vision_output = mm->vision_encoder().forward_vision(gb, preprocessed, backend); - gb->execute(); - auto encoder_end = std::chrono::high_resolution_clock::now(); - - encoder_only_metrics.encoder_ms = - std::chrono::duration_cast(encoder_end - encoder_start).count() / 1000.0; - const auto& out_buf = gb->get_output_buffer(vision_output); - encoder_only_metrics.output_units = out_buf.shape.empty() ? 0 : out_buf.shape[0]; - encoder_only_metrics.output_units_per_s = encoder_only_metrics.encoder_ms > 0.0 - ? (static_cast(encoder_only_metrics.output_units) * 1000.0) / encoder_only_metrics.encoder_ms - : 0.0; - encoder_only_metrics.collected = true; - } - } - - std::vector images = {image_path}; - std::string output; - size_t completion_tokens = 0; - double ttft_ms = 0.0; - bool saw_first_token = false; - auto start_time = std::chrono::high_resolution_clock::now(); - - for (int i = 0; i < 150; i++) { - uint32_t token = model->decode_with_images(tokens, images, 0.0f, 1.0f, 1, ""); - if (!saw_first_token) { - auto t_first = std::chrono::high_resolution_clock::now(); - ttft_ms = std::chrono::duration_cast(t_first - start_time).count() / 1000.0; - saw_first_token = true; - } - std::string piece = tokenizer->decode({token}); - output += piece; - tokens.push_back(token); - completion_tokens++; - if (piece.find("") != std::string::npos || piece.find("") != std::string::npos) - break; - } - auto end_time = std::chrono::high_resolution_clock::now(); - double total_time_ms = std::chrono::duration_cast(end_time - start_time).count() / 1000.0; - auto metrics = compute_local_perf_metrics(prompt_token_count, completion_tokens, ttft_ms, total_time_ms); - - print_local_perf_metrics(metrics, &encoder_only_metrics); - std::cout << " Output: " << output.substr(0, 300) << "\n"; - std::cout << " NPU" << (expect_npu ? " (expected)" : " (not required)") << ": " - << (has_npu_package(model_path, "vision_encoder.mlpackage") ? "available" : "not available") << "\n"; - - if (output.empty()) { - std::cerr << " FAIL: empty output\n"; - return false; - } - - bool has_content = false; - for (char c : output) - if (std::isalpha(c)) { has_content = true; break; } - - if (!has_content) { - std::cerr << " FAIL: output has no alphabetic content\n"; - return false; - } - - return true; -} - - -bool test_gemma4_audio(bool expect_npu) { - const char* model_path = get_model_path(); - std::string assets = get_assets_dir(); - if (!model_path) { - std::cerr << " SKIP: model path not set\n"; - return true; - } - - if (expect_npu && !has_npu_package(model_path, "audio_encoder.mlpackage")) { - std::cerr << " SKIP: audio_encoder.mlpackage not found in model folder\n"; - return true; - } - - print_modality_box(expect_npu ? "AUDIO NPU" : "AUDIO", "Transcribe the audio."); - - std::string audio_path = assets + "/test.wav"; - struct stat st; - if (stat(audio_path.c_str(), &st) != 0) { - std::cerr << " SKIP: test.wav not found in assets\n"; - return true; - } - - auto model = create_model(model_path); - if (!model) { - std::cerr << " FAIL: create_model returned null\n"; - return false; - } - - auto* mm = dynamic_cast(model.get()); - if (mm) { - if (!expect_npu) - mm->audio_encoder().disable_npu_ = true; - } - - if (!model->init(model_path, 2048, "", true)) { - std::cerr << " FAIL: model init\n"; - return false; - } - - AudioFP32 wav = load_wav(audio_path.c_str()); - auto audio_samples = resample_to_16k_fp32(wav.samples, wav.sample_rate); - - size_t pad_amt = 320 - (audio_samples.size() % 320); - if (pad_amt < 320) - audio_samples.resize(audio_samples.size() + pad_amt, 0.0f); - - const auto& cfg = model->get_config(); - size_t mel_bins = cfg.audio_input_feat_size; - uint32_t audio_token_id = cfg.audio_token_id; - if (audio_token_id == 0) audio_token_id = 258881; - - auto spec_cfg = get_htk_spectrogram_config(); - AudioProcessor ap; - size_t fft_for_mel = spec_cfg.fft_override > 0 ? spec_cfg.fft_override : spec_cfg.n_fft; - ap.init_mel_filters(fft_for_mel / 2 + 1, mel_bins, 0.0f, 8000.0f, 16000, nullptr, "htk"); - auto mel = ap.compute_spectrogram(audio_samples, spec_cfg); - - size_t num_frames = mel.size() / mel_bins; - -#ifdef __APPLE__ - { - static constexpr float LN2 = 0.693147180559945f; - for (auto& v : mel) v -= LN2; - } -#endif - - auto audio_features = transpose_mel_to_frame_major(mel, mel_bins, num_frames); - - EncoderOnlyMetrics encoder_only_metrics; - encoder_only_metrics.input_label = "in_frames"; - encoder_only_metrics.output_label = "out_steps"; - encoder_only_metrics.input_units = num_frames; - if (mm) { - encoder_only_metrics.npu_enabled = mm->audio_encoder().use_npu_encoder_; - if (model->graph_handle_) { - auto* gb = static_cast(model->graph_handle_); - auto backend = cfg.default_backend == Config::Backend::CPU ? ComputeBackend::CPU : ComputeBackend::NPU; - gb->soft_reset(); - - auto encoder_start = std::chrono::high_resolution_clock::now(); - size_t encoded_audio = mm->audio_encoder().forward_audio(gb, audio_features, num_frames, backend); - gb->execute(); - auto encoder_end = std::chrono::high_resolution_clock::now(); - - encoder_only_metrics.encoder_ms = std::chrono::duration_cast(encoder_end - encoder_start).count() / 1000.0; - const auto& encoded_buf = gb->get_output_buffer(encoded_audio); - encoder_only_metrics.output_units = encoded_buf.shape.empty() ? 0 : encoded_buf.shape[0]; - encoder_only_metrics.output_units_per_s = encoder_only_metrics.encoder_ms > 0.0 - ? (static_cast(encoder_only_metrics.output_units) * 1000.0) / encoder_only_metrics.encoder_ms - : 0.0; - encoder_only_metrics.collected = true; - } - } - - size_t after_stage1 = (num_frames + 1) / 2; - size_t num_soft_tokens = (after_stage1 + 1) / 2; - - auto* tokenizer = model->get_tokenizer(); - auto prefix = tokenizer->encode("<|turn>user\nTranscribe the audio.<|audio>"); - auto suffix = tokenizer->encode("\n<|turn>model\n"); - - std::vector tokens; - tokens.insert(tokens.end(), prefix.begin(), prefix.end()); - for (size_t i = 0; i < num_soft_tokens; i++) - tokens.push_back(audio_token_id); - tokens.insert(tokens.end(), suffix.begin(), suffix.end()); - const size_t prompt_token_count = tokens.size(); - - std::string output; - size_t completion_tokens = 0; - double ttft_ms = 0.0; - bool saw_first_token = false; - auto start_time = std::chrono::high_resolution_clock::now(); - for (int i = 0; i < 200; i++) { - uint32_t token = model->decode_with_audio(tokens, audio_features, 0.0f, 1.0f, 1, ""); - if (!saw_first_token) { - auto t_first = std::chrono::high_resolution_clock::now(); - ttft_ms = std::chrono::duration_cast(t_first - start_time).count() / 1000.0; - saw_first_token = true; - } - std::string piece = tokenizer->decode({token}); - output += piece; - tokens.push_back(token); - completion_tokens++; - if (output.find("") != std::string::npos || output.find("") != std::string::npos) - break; - } - auto end_time = std::chrono::high_resolution_clock::now(); - double total_time_ms = std::chrono::duration_cast(end_time - start_time).count() / 1000.0; - auto metrics = compute_local_perf_metrics(prompt_token_count, completion_tokens, ttft_ms, total_time_ms); - - print_local_perf_metrics(metrics, &encoder_only_metrics); - std::cout << " Transcript: " << preview_text(output) << "\n"; - - if (output.empty()) { - std::cerr << " FAIL: empty output\n"; - return false; - } - - bool has_content = false; - for (char c : output) - if (std::isalpha(c)) { has_content = true; break; } - - if (!has_content) { - std::cerr << " FAIL: output has no alphabetic content\n"; - return false; - } - - return true; -} - -bool test_gemma4_vision_audio() { - const char* model_path = get_model_path(); - const char* image_path = get_image_path(); - if (!model_path || !image_path) { - std::cerr << " SKIP: CACTUS_TEST_GEMMA4_MODEL or CACTUS_TEST_IMAGE not set\n"; - return true; - } - - std::string audio_path = get_audio_path(); - struct stat st; - if (stat(audio_path.c_str(), &st) != 0) { - std::cerr << " SKIP: test.wav not found in assets\n"; - return true; - } - - std::string image_audio_prompt = "Please give a two line output. First line describes the image in strong detail, second line contains audio transcription."; - - print_modality_box("VISION + AUDIO", image_audio_prompt); - - cactus_model_t model = cactus_init(model_path, nullptr, false); - if (!model) { - std::cerr << " FAIL: cactus_init returned null\n"; - return false; - } - - std::string messages = "[{\"role\":\"user\",\"content\":\"" + image_audio_prompt + "\"," - "\"images\":[\"" + escape_json(image_path) + "\"]," - "\"audio\":[\"" + escape_json(audio_path) + "\"]}]"; - std::vector response_buffer(16384, 0); - - int result = cactus_complete(model, messages.c_str(), response_buffer.data(), response_buffer.size(), - g_options, nullptr, nullptr, nullptr, nullptr, 0); - cactus_destroy(model); - - std::string response_json(response_buffer.data()); - Metrics metrics; - metrics.parse(response_json); - metrics.print_json(); - - if (result <= 0 || !metrics.success) { - std::cerr << " FAIL: completion failed: " << response_json << "\n"; - return false; - } - if (metrics.response.empty()) { - std::cerr << " FAIL: empty response\n"; - return false; - } - - bool has_content = false; - for (char c : metrics.response) { - if (std::isalpha(static_cast(c))) { - has_content = true; - break; - } - } - if (!has_content) { - std::cerr << " FAIL: response has no alphabetic content\n"; - return false; - } - - std::cout << " Output: " << preview_text(metrics.response) << "\n"; - return true; -} - - -int main() { - TestUtils::TestRunner runner("Gemma4 Suite"); - - runner.run_test("text_generation", test_text_generation()); - runner.run_test("tool_call", test_tool_call()); - runner.run_test("1k_context", test_1k_context()); - runner.run_test("vision", test_gemma4_vision(false)); - runner.run_test("vision_npu", test_gemma4_vision(true)); - runner.run_test("audio", test_gemma4_audio(false)); - runner.run_test("audio_npu", test_gemma4_audio(true)); - runner.run_test("vision_audio", test_gemma4_vision_audio()); - - runner.print_summary(); - return runner.all_passed() ? 0 : 1; -} diff --git a/tests/test_gemma4_thinking.cpp b/tests/test_gemma4_thinking.cpp deleted file mode 100644 index 482715480..000000000 --- a/tests/test_gemma4_thinking.cpp +++ /dev/null @@ -1,311 +0,0 @@ -#include "test_utils.h" -#include "../cactus/ffi/cactus_utils.h" -#include -#include -#include -#include - -using namespace cactus::ffi; -using namespace cactus::engine; - -static const char* get_gemma4_model_path() { - const char* path = std::getenv("CACTUS_TEST_GEMMA4_MODEL"); - if (path) return path; - return std::getenv("CACTUS_TEST_MODEL"); -} - -static bool check_strip(const std::string& input, - const std::string& expected_thinking, - const std::string& expected_content) { - std::string thinking, content; - strip_thinking_block(input, thinking, content); - if (thinking != expected_thinking) { - std::cerr << " thinking: '" << thinking << "' != '" << expected_thinking << "'\n"; - return false; - } - if (content != expected_content) { - std::cerr << " content: '" << content << "' != '" << expected_content << "'\n"; - return false; - } - return true; -} - -static std::string escape_for_json(const std::string& s) { - std::string out; - for (char c : s) { - if (c == '"') out += "\\\""; - else if (c == '\\') out += "\\\\"; - else if (c == '\n') out += "\\n"; - else if (c == '\r') out += "\\r"; - else if (c == '\t') out += "\\t"; - else out += c; - } - return out; -} - -bool test_strip_thinking_channel() { - return check_strip("<|channel>reasonanswer", "reason", "answer") - && check_strip("<|channel>\n reason\n\n\nanswer", "reason", "answer") - && check_strip("no tags here", "", "no tags here") - && check_strip("<|channel>unclosed", "unclosed", "") - && check_strip("reason\n\n\nanswer", "reason", "answer") - && check_strip("<|channel>thought1text1<|channel>thought2text2", - "thought1\nthought2", "text1text2"); -} - -bool test_find_channel_token_ranges() { - constexpr uint32_t OPEN = 100; - constexpr uint32_t CLOSE = 101; - - { - std::vector tokens = {OPEN, 50, 51, 52, CLOSE, 60, 61}; - auto ranges = find_channel_token_ranges(tokens, 0, OPEN, CLOSE); - if (ranges.size() != 1 || ranges[0].first != 0 || ranges[0].second != 5) { - std::cerr << " Single block failed\n"; - return false; - } - } - { - std::vector tokens = {10, OPEN, 50, CLOSE, 20, OPEN, 51, 52, CLOSE, 30}; - auto ranges = find_channel_token_ranges(tokens, 0, OPEN, CLOSE); - if (ranges.size() != 2 || ranges[0].first != 1 || ranges[0].second != 3 - || ranges[1].first != 5 || ranges[1].second != 4) { - std::cerr << " Two blocks failed\n"; - return false; - } - } - { - std::vector tokens = {10, 20, 30}; - if (!find_channel_token_ranges(tokens, 0, OPEN, CLOSE).empty()) { - std::cerr << " No blocks failed\n"; - return false; - } - } - { - std::vector tokens = {OPEN, 50, 51}; - auto ranges = find_channel_token_ranges(tokens, 0, OPEN, CLOSE); - if (ranges.size() != 1 || ranges[0].first != 0 || ranges[0].second != 3) { - std::cerr << " Unclosed block failed\n"; - return false; - } - } - { - std::vector tokens = {OPEN, 50, CLOSE, 60}; - auto ranges = find_channel_token_ranges(tokens, 100, OPEN, CLOSE); - if (ranges.size() != 1 || ranges[0].first != 100 || ranges[0].second != 3) { - std::cerr << " Offset failed\n"; - return false; - } - } - - return true; -} - -bool test_prompt_gemma4_thinking_injection() { - const char* model_path = get_gemma4_model_path(); - if (!model_path) { std::cout << " [SKIP] CACTUS_TEST_GEMMA4_MODEL not set\n"; return true; } - - cactus_model_t model = cactus_init(model_path, nullptr, false); - if (!model) { std::cout << " [SKIP] Could not load model\n"; return true; } - - auto* handle = static_cast(model); - auto* tok = handle->model->get_tokenizer(); - std::vector msgs = {{"user", "hello", "", {}, {}, 0, {}}}; - - std::string enabled = tok->format_chat_prompt(msgs, true, "", true); - std::string disabled = tok->format_chat_prompt(msgs, true, "", false); - cactus_destroy(model); - - bool ok = enabled.find("<|think|>") != std::string::npos - && disabled.find("<|think|>") == std::string::npos - && disabled.find("<|channel>") == std::string::npos - && disabled.find("") == std::string::npos; - if (!ok) { - std::cerr << " thinking injection mismatch in prompt\n"; - std::cerr << " enabled prompt: " << enabled << "\n"; - std::cerr << " disabled prompt: " << disabled << "\n"; - } - return ok; -} - -bool test_prompt_gemma4_assistant_stripping() { - const char* model_path = get_gemma4_model_path(); - if (!model_path) { std::cout << " [SKIP] CACTUS_TEST_GEMMA4_MODEL not set\n"; return true; } - - cactus_model_t model = cactus_init(model_path, nullptr, false); - if (!model) { std::cout << " [SKIP] Could not load model\n"; return true; } - - auto* handle = static_cast(model); - auto* tok = handle->model->get_tokenizer(); - - std::vector msgs = { - {"user", "hello", "", {}, {}, 0, {}}, - {"assistant", "<|channel>internal reasoningvisible response", "", {}, {}, 0, {}}, - {"user", "followup", "", {}, {}, 0, {}} - }; - - std::string prompt = tok->format_chat_prompt(msgs, true, "", true); - cactus_destroy(model); - - bool has_visible = prompt.find("visible response") != std::string::npos; - bool no_reasoning = prompt.find("internal reasoning") == std::string::npos; - bool no_channel_tags = prompt.find("<|channel>") == std::string::npos - && prompt.find("") == std::string::npos; - - if (!has_visible) std::cerr << " missing visible response in prompt\n"; - if (!no_reasoning) std::cerr << " thinking content not stripped from assistant turn\n"; - if (!no_channel_tags) std::cerr << " channel tags not stripped from prompt\n"; - - return has_visible && no_reasoning && no_channel_tags; -} - -bool test_complete_gemma4_thinking_toggle() { - const char* model_path = get_gemma4_model_path(); - if (!model_path) { std::cout << " [SKIP] CACTUS_TEST_GEMMA4_MODEL not set\n"; return true; } - - cactus_model_t model = cactus_init(model_path, nullptr, false); - if (!model) return false; - - auto* handle = static_cast(model); - auto mtype = handle->model->get_config().model_type; - if (mtype != Config::ModelType::GEMMA4) { - std::cout << " [SKIP] Not a Gemma4 model\n"; - cactus_destroy(model); - return true; - } - - const char* msgs = R"([{"role": "user", "content": "What is 2+2?"}])"; - char buf[8192]; - - int r1 = cactus_complete(model, msgs, buf, sizeof(buf), - R"({"max_tokens":128,"enable_thinking_if_supported":true,"telemetry_enabled":false})", - nullptr, nullptr, nullptr, nullptr, 0); - std::string resp1(buf); - bool ok1 = r1 > 0 && resp1.find("\"success\":true") != std::string::npos; - - handle->model->reset_cache(); - handle->processed_tokens.clear(); - - int r2 = cactus_complete(model, msgs, buf, sizeof(buf), - R"({"max_tokens":128,"enable_thinking_if_supported":false,"telemetry_enabled":false})", - nullptr, nullptr, nullptr, nullptr, 0); - std::string resp2(buf); - bool ok2 = r2 > 0 - && resp2.find("\"thinking\"") == std::string::npos - && resp2.find("<|channel>") == std::string::npos - && resp2.find("") == std::string::npos; - - cactus_destroy(model); - if (!ok1) std::cerr << " thinking-enabled completion failed\n"; - if (!ok2) std::cerr << " thinking-disabled completion should not have thinking field\n"; - return ok1 && ok2; -} - -bool test_complete_gemma4_tool_call() { - const char* model_path = get_gemma4_model_path(); - if (!model_path) { std::cout << " [SKIP] CACTUS_TEST_GEMMA4_MODEL not set\n"; return true; } - - cactus_model_t model = cactus_init(model_path, nullptr, false); - if (!model) return false; - - auto* handle = static_cast(model); - auto mtype = handle->model->get_config().model_type; - if (mtype != Config::ModelType::GEMMA4) { - std::cout << " [SKIP] Not a Gemma4 model\n"; - cactus_destroy(model); - return true; - } - - const char* msgs = R"([{"role": "user", "content": "What is the weather in San Francisco?"}])"; - const char* tools = R"([{"type":"function","function":{"name":"get_weather","description":"Get current weather","parameters":{"type":"object","properties":{"location":{"type":"string","description":"City name"}},"required":["location"]}}}])"; - char buf[8192]; - - int r = cactus_complete(model, msgs, buf, sizeof(buf), - R"({"max_tokens":256,"force_tool_call":true,"enable_thinking_if_supported":true,"telemetry_enabled":false})", - tools, nullptr, nullptr, nullptr, 0); - std::string resp(buf); - - cactus_destroy(model); - - bool success = r > 0 && resp.find("\"success\":true") != std::string::npos; - bool has_calls = resp.find("\"function_calls\"") != std::string::npos - && resp.find("get_weather") != std::string::npos; - - if (!success) std::cerr << " tool call completion failed: " << resp << "\n"; - if (!has_calls) std::cerr << " expected get_weather tool call in response: " << resp << "\n"; - return success && has_calls; -} - -bool test_multiturn_cache_reuse() { - const char* model_path = get_gemma4_model_path(); - if (!model_path) { std::cout << " [SKIP] CACTUS_TEST_GEMMA4_MODEL not set\n"; return true; } - - cactus_model_t model = cactus_init(model_path, nullptr, false); - if (!model) { std::cerr << " Failed to load model\n"; return false; } - - auto* handle = static_cast(model); - if (handle->model->get_config().model_type != Config::ModelType::GEMMA4) { - std::cout << " [SKIP] Not a Gemma4 model\n"; - cactus_destroy(model); - return true; - } - - auto* tokenizer = handle->model->get_tokenizer(); - const char* options = R"({"max_tokens":128,"temperature":0,"top_k":1,"enable_thinking_if_supported":true,"telemetry_enabled":false,"auto_handoff":false})"; - const char* turn1_msgs = R"([{"role": "user", "content": "My name is Alice. Please remember this."}])"; - char buf[16384]; - - int r1 = cactus_complete(model, turn1_msgs, buf, sizeof(buf), options, nullptr, nullptr, nullptr, nullptr, 0); - if (r1 <= 0) { std::cerr << " Turn 1 failed\n"; cactus_destroy(model); return false; } - - std::vector processed_after_t1 = handle->processed_tokens; - - std::vector t1_chat = {{"user", "My name is Alice. Please remember this.", "", {}, {}, 0, {}}}; - std::vector t1_prompt_tokens = tokenizer->encode(tokenizer->format_chat_prompt(t1_chat, true, "", true)); - std::vector gen_tokens(processed_after_t1.begin() + t1_prompt_tokens.size(), processed_after_t1.end()); - std::string assistant_text = tokenizer->decode(gen_tokens); - - std::vector t2_chat = { - {"user", "My name is Alice. Please remember this.", "", {}, {}, 0, {}}, - {"assistant", assistant_text, "", {}, {}, 0, {}}, - {"user", "What is my name?", "", {}, {}, 0, {}} - }; - std::vector t2_prompt_tokens = tokenizer->encode(tokenizer->format_chat_prompt(t2_chat, true, "", true)); - - bool prefix_ok = (t2_prompt_tokens.size() >= processed_after_t1.size()) && - std::equal(processed_after_t1.begin(), processed_after_t1.end(), t2_prompt_tokens.begin()); - std::cout << " Prefix match (cache reuse): " << (prefix_ok ? "YES" : "NO") << "\n"; - - size_t new_tokens_for_t2 = t2_prompt_tokens.size() - processed_after_t1.size(); - std::cout << " Turn 2 new tokens: " << new_tokens_for_t2 << " (full prompt: " << t2_prompt_tokens.size() << ")\n"; - - std::string escaped = escape_for_json(assistant_text); - std::string turn2_json = R"([{"role": "user", "content": "My name is Alice. Please remember this."},{"role": "assistant", "content": ")" - + escaped + R"("},{"role": "user", "content": "What is my name?"}])"; - - int r2 = cactus_complete(model, turn2_json.c_str(), buf, sizeof(buf), options, nullptr, nullptr, nullptr, nullptr, 0); - if (r2 <= 0) { std::cerr << " Turn 2 failed\n"; cactus_destroy(model); return false; } - - std::string turn2_response(buf); - bool mentions_alice = turn2_response.find("Alice") != std::string::npos - || turn2_response.find("alice") != std::string::npos; - std::cout << " Turn 2 mentions Alice: " << (mentions_alice ? "YES" : "NO") << "\n"; - - cactus_destroy(model); - - if (!prefix_ok) std::cerr << " FAIL: cache surgery did not produce valid prefix\n"; - return prefix_ok; -} - -int main() { - TestUtils::TestRunner runner("Gemma4 Thinking Tests"); - runner.run_test("strip_thinking_channel", test_strip_thinking_channel()); - runner.run_test("find_channel_token_ranges", test_find_channel_token_ranges()); - runner.run_test("prompt_thinking_injection", test_prompt_gemma4_thinking_injection()); - runner.run_test("prompt_assistant_stripping", test_prompt_gemma4_assistant_stripping()); - runner.run_test("complete_thinking_toggle", test_complete_gemma4_thinking_toggle()); - runner.run_test("complete_tool_call", test_complete_gemma4_tool_call()); - runner.run_test("multiturn_cache_reuse", test_multiturn_cache_reuse()); - runner.print_summary(); - return runner.all_passed() ? 0 : 1; -} diff --git a/tests/test_gemma4_vision.cpp b/tests/test_gemma4_vision.cpp deleted file mode 100644 index f89af3872..000000000 --- a/tests/test_gemma4_vision.cpp +++ /dev/null @@ -1,149 +0,0 @@ -#include "test_utils.h" -#include -#include -#include -#include - -using namespace cactus::engine; - -static const char* get_gemma4_model_path() { - const char* path = std::getenv("CACTUS_TEST_GEMMA4_MODEL"); - if (path) return path; - return std::getenv("CACTUS_TEST_MODEL"); -} - -static const char* get_test_image_path() { - return std::getenv("CACTUS_TEST_IMAGE"); -} - -bool test_vlm_model_creation() { - const char* model_path = get_gemma4_model_path(); - if (!model_path) { - std::cerr << " SKIP: CACTUS_TEST_GEMMA4_MODEL not set\n"; - return true; - } - - auto model = create_model(model_path); - if (!model) { - std::cerr << " FAIL: create_model returned null\n"; - return false; - } - - return true; -} - -bool test_vlm_init() { - const char* model_path = get_gemma4_model_path(); - if (!model_path) { - std::cerr << " SKIP: CACTUS_TEST_GEMMA4_MODEL not set\n"; - return true; - } - - auto model = create_model(model_path); - if (!model) { - std::cerr << " FAIL: create_model returned null\n"; - return false; - } - - bool ok = model->init(model_path, 2048, "", false); - if (!ok) { - std::cerr << " FAIL: model init failed\n"; - return false; - } - - return true; -} - -bool test_vlm_decode_with_image() { - const char* model_path = get_gemma4_model_path(); - const char* image_path = get_test_image_path(); - if (!model_path || !image_path) { - std::cerr << " SKIP: CACTUS_TEST_GEMMA4_MODEL or CACTUS_TEST_IMAGE not set\n"; - return true; - } - - auto model = create_model(model_path); - if (!model) { - std::cerr << " FAIL: create_model returned null\n"; - return false; - } - - if (!model->init(model_path, 2048, "", false)) { - std::cerr << " FAIL: model init failed\n"; - return false; - } - - auto* tokenizer = model->get_tokenizer(); - if (!tokenizer) { - std::cerr << " FAIL: no tokenizer\n"; - return false; - } - - std::vector messages; - messages.push_back({"user", "Describe this image briefly.", "", {image_path}, {}, 0, {}}); - std::string prompt = tokenizer->format_chat_prompt(messages, true, "", false); - auto tokens = tokenizer->encode(prompt); - - size_t vision_count = 0; - for (auto t : tokens) - if (t == 262145) vision_count++; - std::cout << " tokens: " << tokens.size() << ", vision soft tokens (262145): " << vision_count << "\n"; - - std::vector images = {image_path}; - std::string output; - - for (int i = 0; i < 150; i++) { - uint32_t token = model->decode_with_images(tokens, images, 0.0f, 1.0f, 1, ""); - std::string piece = tokenizer->decode({token}); - output += piece; - tokens.push_back(token); - - if (piece.find("") != std::string::npos || piece.find("") != std::string::npos) - break; - } - - std::cout << " VLM output: " << output.substr(0, 500) << "\n"; - - if (output.empty()) { - std::cerr << " FAIL: empty output\n"; - return false; - } - - return true; -} - -int main() { - int pass = 0, fail = 0, total = 0; - - auto run = [&](const char* name, bool (*fn)()) { - total++; - std::cout << "[" << total << "] " << name << ": "; - if (fn()) { - std::cout << "PASS\n"; - pass++; - } else { - std::cout << "FAIL\n"; - fail++; - } - }; - - auto test_image_embeddings = []() -> bool { - const char* mp = std::getenv("CACTUS_TEST_GEMMA4_MODEL"); - const char* ip = std::getenv("CACTUS_TEST_IMAGE"); - if (!mp || !ip) return true; - auto m = create_model(mp); - if (!m || !m->init(mp, 2048, "", false)) return false; - auto embeddings = m->get_image_embeddings(ip); - return !embeddings.empty(); - }; - - run("image_embeddings", +test_image_embeddings); - run("vlm_model_creation", test_vlm_model_creation); - run("vlm_init", test_vlm_init); - run("vlm_decode_with_image", test_vlm_decode_with_image); - - std::cout << "\n" << pass << "/" << total << " passed"; - if (fail > 0) std::cout << " (" << fail << " FAILED)"; - std::cout << "\n"; - return fail > 0 ? 1 : 0; -} diff --git a/tests/test_graph.cpp b/tests/test_graph.cpp deleted file mode 100644 index e88f088d8..000000000 --- a/tests/test_graph.cpp +++ /dev/null @@ -1,1531 +0,0 @@ -#include "test_utils.h" -#include -#include -#include -#include -#include -#include -#include - -// helper function to print vectors for debugging -namespace { -template -void print_vector_inline(const std::vector& values) { - std::cout << "["; - for (size_t i = 0; i < values.size(); ++i) { - if (i > 0) std::cout << ", "; - std::cout << values[i]; - } - std::cout << "]"; -} - -size_t runtime_id_from_serialized_index(uint32_t serialized_index) { - // Graph deserialization rebuilds nodes in serialized order, and runtime node - // ids are allocated sequentially starting from 1. - return static_cast(serialized_index) + 1; -} -} // namespace - -bool test_abs() { - TestUtils::FP16TestFixture fixture("absval"); - size_t input = fixture.create_input({2, 3}); - - size_t abs_result = fixture.graph().abs(input); - std::vector<__fp16> data = {1, -2, 3, 4, -5, -6}; - - fixture.set_input_data(input, data); - fixture.execute(); - - std::vector<__fp16> expected = {1, 2, 3, 4, 5, 6}; - return fixture.verify_output(abs_result, expected); -} - -bool test_concat() { - TestUtils::FP16TestFixture fixture("Concat"); - - size_t input_a = fixture.create_input({2, 3}); - size_t input_b = fixture.create_input({2, 5}); - size_t concat_result = fixture.graph().concat(input_a, input_b, 1); - - std::vector<__fp16> data_a = {1, 2, 3, 4, 5, 6}; - std::vector<__fp16> data_b = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; - std::vector<__fp16> expected = {1, 2, 3, 1, 2, 3, 4, 5, 4, 5, 6, 6, 7, 8, 9, 10}; - fixture.set_input_data(input_a, data_a); - fixture.set_input_data(input_b, data_b); - fixture.execute(); - - return fixture.verify_output(concat_result, expected); -} - -bool test_cat() { - TestUtils::FP16TestFixture fixture("Cat (multiple input tensors)"); - - size_t input_a = fixture.create_input({2, 3}); - size_t input_b = fixture.create_input({2, 5}); - size_t input_c = fixture.create_input({2, 2}); - - size_t cat_result = fixture.graph().cat({input_a, input_b, input_c}, 1); - - std::vector<__fp16> data_a = {1, 2, 3, - 4, 5, 6}; - - std::vector<__fp16> data_b = {1, 2, 3, 4, 5, - 6, 7, 8, 9, 10}; - - std::vector<__fp16> data_c = {-1, -2, - -1, -2}; - - std::vector<__fp16> expected = { - 1, 2, 3, 1, 2, 3, 4, 5, -1, -2, - 4, 5, 6, 6, 7, 8, 9, 10, -1, -2 - }; - - fixture.set_input_data(input_a, data_a); - fixture.set_input_data(input_b, data_b); - fixture.set_input_data(input_c, data_c); - - fixture.execute(); - - return fixture.verify_output(cat_result, expected); -} - -bool test_view() { - TestUtils::FP16TestFixture fixture("View"); - - size_t input = fixture.create_input({2, 3}); - size_t view_result = fixture.graph().view(input, {3, 2}); - - std::vector<__fp16> data = {1, 2, 3, 4, 5, 6}; - std::vector<__fp16> expected = {1, 2, 3, 4, 5, 6}; - - fixture.set_input_data(input, data); - fixture.execute(); - - return fixture.verify_output(view_result, expected); -} - -bool test_flatten() { - TestUtils::FP16TestFixture fixture("Flatten"); - - size_t input = fixture.create_input({2, 3}); - size_t flatten_result = fixture.graph().flatten(input); - - std::vector<__fp16> data = {1, 2, 3, 4, 5, 6}; - std::vector<__fp16> expected = {1, 2, 3, 4, 5, 6}; - - fixture.set_input_data(input, data); - fixture.execute(); - - return fixture.verify_output(flatten_result, expected); -} - -bool test_basic_operations() { - TestUtils::FP16TestFixture fixture("Basic Operations"); - - size_t input_a = fixture.create_input({2, 3}); - size_t input_b = fixture.create_input({2, 3}); - size_t add_result = fixture.graph().add(input_a, input_b); - size_t mul_result = fixture.graph().multiply(add_result, input_a); - size_t scalar_result = fixture.graph().scalar_multiply(mul_result, 2.0f); - - std::vector<__fp16> data_a = {1, 2, 3, 4, 5, 6}; - std::vector<__fp16> data_b = {2, 3, 4, 5, 6, 7}; - - fixture.set_input_data(input_a, data_a); - fixture.set_input_data(input_b, data_b); - fixture.execute(); - - std::vector<__fp16> expected(6); - for (int i = 0; i < 6; i++) { - float result = ((static_cast(data_a[i]) + static_cast(data_b[i])) * static_cast(data_a[i])) * 2.0f; - expected[i] = static_cast<__fp16>(result); - } - - return fixture.verify_output(scalar_result, expected); -} - -bool test_basic_addition() { - return TestUtils::test_basic_operation( - "Addition", - [](CactusGraph& graph, size_t a, size_t b) { return graph.add(a, b); }, - {1, 2, 3, 4}, - {5, 6, 7, 8}, - {6, 8, 10, 12} - ); -} - -bool test_basic_subtraction() { - return TestUtils::test_basic_operation( - "Subtraction", - [](CactusGraph& graph, size_t a, size_t b) { return graph.subtract(a, b); }, - {10, 8, 6, 4}, - {2, 3, 1, 2}, - {8, 5, 5, 2} - ); -} - -bool test_basic_multiplication() { - return TestUtils::test_basic_operation( - "Multiplication", - [](CactusGraph& graph, size_t a, size_t b) { return graph.multiply(a, b); }, - {2, 3, 4, 5}, - {3, 4, 2, 2}, - {6, 12, 8, 10} - ); -} - -bool test_basic_division() { - return TestUtils::test_basic_operation( - "Division", - [](CactusGraph& graph, size_t a, size_t b) { return graph.divide(a, b); }, - {12, 15, 8, 9}, - {3, 5, 2, 3}, - {4, 3, 4, 3} - ); -} - -bool test_matrix_multiplication() { - TestUtils::FP16TestFixture fixture("Matrix Multiplication"); - - size_t input_a = fixture.create_input({2, 3}); - size_t input_b = fixture.create_input({3, 2}); - size_t matmul_result = fixture.graph().matmul(input_a, input_b, false); - - std::vector<__fp16> data_a = {1, 2, 3, 4, 5, 6}; - std::vector<__fp16> data_b = {1, 2, 3, 4, 5, 6}; - fixture.set_input_data(input_a, data_a); - fixture.set_input_data(input_b, data_b); - fixture.execute(); - - std::vector<__fp16> expected = {22, 28, 49, 64}; - return fixture.verify_output(matmul_result, expected); -} - -bool test_transpose() { - TestUtils::FP16TestFixture fixture("Transpose"); - - size_t input_a = fixture.create_input({2, 3}); - size_t transpose_result = fixture.graph().transpose(input_a); - - std::vector<__fp16> data_a = {1, 2, 3, 4, 5, 6}; - fixture.set_input_data(input_a, data_a); - fixture.execute(); - - std::vector<__fp16> expected = {1, 4, 2, 5, 3, 6}; - return fixture.verify_output(transpose_result, expected); -} - -bool test_reshape() { - TestUtils::FP16TestFixture fixture("Reshape"); - - size_t input_a = fixture.create_input({2, 3}); - size_t reshape_result = fixture.graph().reshape(input_a, {3, 2}); - - std::vector<__fp16> data_a = {1, 2, 3, 4, 5, 6}; - fixture.set_input_data(input_a, data_a); - fixture.execute(); - - return fixture.verify_output(reshape_result, data_a); -} - - -bool test_scalar_operations() { - TestUtils::FP16TestFixture fixture("Scalar Operations"); - - size_t input_a = fixture.create_input({4}); - size_t add_result = fixture.graph().scalar_add(input_a, 5.0f); - size_t mul_result = fixture.graph().scalar_multiply(add_result, 2.0f); - - std::vector<__fp16> data_a = {1, 2, 3, 4}; - fixture.set_input_data(input_a, data_a); - fixture.execute(); - - std::vector<__fp16> expected = {12, 14, 16, 18}; - return fixture.verify_output(mul_result, expected); -} - -bool test_scalar_operations_with_pow() { - TestUtils::FP16TestFixture fixture("Scalar Operations with Pow"); - - size_t input_a = fixture.create_input({4}); - size_t add_result = fixture.graph().scalar_add(input_a, 5.0f); - size_t mul_result = fixture.graph().scalar_multiply(add_result, 2.0f); - size_t pow_result = fixture.graph().pow(mul_result, 2.0f); - - std::vector<__fp16> data_a = {1, 2, 3, 4}; - fixture.set_input_data(input_a, data_a); - fixture.execute(); - - std::vector<__fp16> expected = {144, 196, 256, 324}; - return fixture.verify_output(pow_result, expected); -} - -bool test_scalar_subtract_divide() { - TestUtils::FP16TestFixture fixture("Scalar Subtract/Divide"); - - size_t input_a = fixture.create_input({4}); - size_t sub_result = fixture.graph().scalar_subtract(input_a, 2.0f); - size_t div_result = fixture.graph().scalar_divide(input_a, 2.0f); - - std::vector<__fp16> data_a = {10, 8, 6, 4}; - fixture.set_input_data(input_a, data_a); - fixture.execute(); - - std::vector<__fp16> expected_sub = {8, 6, 4, 2}; - std::vector<__fp16> expected_div = {5, 4, 3, 2}; - return fixture.verify_output(sub_result, expected_sub) && - fixture.verify_output(div_result, expected_div); -} - -bool test_scalar_math_functions() { - TestUtils::FP16TestFixture fixture("Scalar Math Functions"); - - size_t input_a = fixture.create_input({3}); - size_t exp_result = fixture.graph().scalar_exp(input_a); - size_t sqrt_result = fixture.graph().scalar_sqrt(input_a); - size_t cos_result = fixture.graph().scalar_cos(input_a); - size_t sin_result = fixture.graph().scalar_sin(input_a); - size_t log_result = fixture.graph().scalar_log(input_a); - - std::vector<__fp16> input_data = {0.5f, 1.0f, 4.0f}; - fixture.set_input_data(input_a, input_data); - fixture.execute(); - - std::vector<__fp16> exp_expected = {1.64872f, 2.71828f, 54.5982f}; - std::vector<__fp16> sqrt_expected = {0.70711f, 1.0f, 2.0f}; - std::vector<__fp16> cos_expected = {0.87758f, 0.54030f, -0.65364f}; - std::vector<__fp16> sin_expected = {0.47943f, 0.84147f, -0.75680f}; - std::vector<__fp16> log_expected = {-0.69315f, 0.0f, 1.38629f}; - - return fixture.verify_output(exp_result, exp_expected, 0.01f) && - fixture.verify_output(sqrt_result, sqrt_expected, 0.01f) && - fixture.verify_output(cos_result, cos_expected, 0.01f) && - fixture.verify_output(sin_result, sin_expected, 0.01f) && - fixture.verify_output(log_result, log_expected, 0.01f); -} - -bool test_rms_norm() { - TestUtils::FP16TestFixture fixture("RMS Norm"); - - size_t input_a = fixture.create_input({1, 8}); - size_t weight = fixture.create_input({8}); - size_t norm_result = fixture.graph().rms_norm(input_a, weight); - - std::vector<__fp16> input_data = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f}; - std::vector<__fp16> weight_data = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; - - fixture.set_input_data(input_a, input_data); - fixture.set_input_data(weight, weight_data); - fixture.execute(); - - float sum_squares = 0.0f; - for (auto val : input_data) { - float v = static_cast(val); - sum_squares += v * v; - } - float rms = sqrtf(sum_squares / 8.0f + 1e-5f); - float inv_rms = 1.0f / rms; - - std::vector<__fp16> expected; - for (size_t i = 0; i < input_data.size(); i++) { - expected.push_back(static_cast<__fp16>(static_cast(input_data[i]) * inv_rms * static_cast(weight_data[i]))); - } - - return fixture.verify_output(norm_result, expected, 0.01f); -} - -bool test_softmax() { - TestUtils::FP16TestFixture fixture("Softmax"); - - size_t input_a = fixture.create_input({2, 3}); - size_t softmax_result = fixture.graph().softmax(input_a, -1); - - std::vector<__fp16> input_data = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f}; - fixture.set_input_data(input_a, input_data); - fixture.execute(); - - std::vector<__fp16> expected = {0.09003f, 0.24473f, 0.66524f, 0.09003f, 0.24473f, 0.66524f}; - return fixture.verify_output(softmax_result, expected, 0.01f); -} - -bool test_attention() { - TestUtils::FP16TestFixture fixture("Attention"); - - size_t query = fixture.create_input({1, 2, 1, 4}); - size_t key = fixture.create_input({1, 2, 1, 4}); - size_t value = fixture.create_input({1, 2, 1, 4}); - - size_t attention_result = fixture.graph().attention(query, key, value, 0.5f); - (void)attention_result; - - std::vector<__fp16> q_data = {1, 0, 0, 0, 0, 1, 0, 0}; - std::vector<__fp16> k_data = {1, 0, 0, 0, 0, 1, 0, 0}; - std::vector<__fp16> v_data = {1, 2, 3, 4, 5, 6, 7, 8}; - - fixture.set_input_data(query, q_data); - fixture.set_input_data(key, k_data); - fixture.set_input_data(value, v_data); - fixture.execute(); - - return true; -} - -bool test_reduction_operations() { - TestUtils::FP16TestFixture fixture("Reduction Operations"); - - size_t input_a = fixture.create_input({2, 3}); - size_t sum_all = fixture.graph().sum(input_a, -1); - size_t sum_axis0 = fixture.graph().sum(input_a, 0); - size_t sum_axis1 = fixture.graph().sum(input_a, 1); - - std::vector<__fp16> data_a = {1, 2, 3, 4, 5, 6}; - fixture.set_input_data(input_a, data_a); - fixture.execute(); - - std::vector<__fp16> expected_all = {21}; - std::vector<__fp16> expected_axis0 = {5, 7, 9}; - std::vector<__fp16> expected_axis1 = {6, 15}; - - return fixture.verify_output(sum_all, expected_all) && - fixture.verify_output(sum_axis0, expected_axis0) && - fixture.verify_output(sum_axis1, expected_axis1); -} - -bool test_fp16_reduction_operations() { - CactusGraph graph; - - size_t input_a = graph.input({2, 3}, Precision::FP16); - size_t sum_all = graph.sum(input_a, -1); - - std::vector<__fp16> input_data = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f}; - graph.set_input(input_a, input_data.data(), Precision::FP16); - graph.execute(); - - __fp16* output = static_cast<__fp16*>(graph.get_output(sum_all)); - double result = static_cast(output[0]); - double expected = 21.0; - - bool success = std::abs(result - expected) < 0.1f; // FP16 has lower precision - - graph.hard_reset(); - return success; -} - -bool test_mean_operations() { - TestUtils::FP16TestFixture fixture("Mean Operations"); - - size_t input_a = fixture.create_input({2, 4}); - size_t mean_all = fixture.graph().mean(input_a, -1); - size_t mean_axis0 = fixture.graph().mean(input_a, 0); - size_t mean_axis1 = fixture.graph().mean(input_a, 1); - - std::vector<__fp16> data_a = {2, 4, 6, 8, 10, 12, 14, 16}; - fixture.set_input_data(input_a, data_a); - fixture.execute(); - - std::vector<__fp16> expected_all = {9}; - std::vector<__fp16> expected_axis0 = {6, 8, 10, 12}; - std::vector<__fp16> expected_axis1 = {5, 13}; - - return fixture.verify_output(mean_all, expected_all) && - fixture.verify_output(mean_axis0, expected_axis0) && - fixture.verify_output(mean_axis1, expected_axis1); -} - -bool test_variance_operations() { - TestUtils::FP16TestFixture fixture("Variance Operations"); - - size_t input_a = fixture.create_input({1, 4}); - size_t var_axis1 = fixture.graph().variance(input_a, 1); - - std::vector<__fp16> input_data = {1.0f, 2.0f, 3.0f, 4.0f}; - fixture.set_input_data(input_a, input_data); - fixture.execute(); - - std::vector<__fp16> expected = {1.25f}; - return fixture.verify_output(var_axis1, expected, 0.01f); -} - -bool test_min_max_operations() { - TestUtils::FP16TestFixture fixture("Min/Max Operations"); - - size_t input_a = fixture.create_input({2, 3}); - size_t min_axis0 = fixture.graph().min(input_a, 0); - size_t max_axis0 = fixture.graph().max(input_a, 0); - size_t min_axis1 = fixture.graph().min(input_a, 1); - size_t max_axis1 = fixture.graph().max(input_a, 1); - - std::vector<__fp16> data_a = {6, 2, 8, 1, 5, 3}; - fixture.set_input_data(input_a, data_a); - fixture.execute(); - - std::vector<__fp16> expected_min_axis0 = {1, 2, 3}; - std::vector<__fp16> expected_max_axis0 = {6, 5, 8}; - std::vector<__fp16> expected_min_axis1 = {2, 1}; - std::vector<__fp16> expected_max_axis1 = {8, 5}; - - return fixture.verify_output(min_axis0, expected_min_axis0) && - fixture.verify_output(max_axis0, expected_max_axis0) && - fixture.verify_output(min_axis1, expected_min_axis1) && - fixture.verify_output(max_axis1, expected_max_axis1); -} - -bool test_fp16_precision() { - TestUtils::FP16TestFixture fixture("FP16 Precision"); - - size_t input_a = fixture.create_input({3}); - size_t input_b = fixture.create_input({3}); - size_t result_id = fixture.graph().add(input_a, input_b); - - std::vector<__fp16> data_a = {1.5f, 2.5f, 3.5f}; - std::vector<__fp16> data_b = {0.5f, 1.5f, 2.5f}; - fixture.set_input_data(input_a, data_a); - fixture.set_input_data(input_b, data_b); - fixture.execute(); - - std::vector<__fp16> expected = {2.0f, 4.0f, 6.0f}; - return fixture.verify_output(result_id, expected); -} - -bool test_broadcast_shape_compatibility() { - TestUtils::FP16TestFixture fixture("Broadcast Shape Compatibility"); - - size_t a_id = fixture.create_input({2, 3}); - size_t b_id = fixture.create_input({2, 1}); - - std::vector<__fp16> data_a = {1, 2, 3, 4, 5, 6}; - std::vector<__fp16> data_b = {10, 20}; - fixture.set_input_data(a_id, data_a); - fixture.set_input_data(b_id, data_b); - - size_t result_id = fixture.graph().add(a_id, b_id); - fixture.execute(); - - std::vector<__fp16> expected = {11, 12, 13, 24, 25, 26}; - return fixture.verify_output(result_id, expected); -} - -bool test_broadcast_scalar_tensor() { - TestUtils::FP16TestFixture fixture("Broadcast Scalar Tensor"); - - size_t a_id = fixture.create_input({2, 2}); - size_t b_id = fixture.create_input({1}); - - std::vector<__fp16> data_a = {1, 2, 3, 4}; - std::vector<__fp16> data_b = {5}; - fixture.set_input_data(a_id, data_a); - fixture.set_input_data(b_id, data_b); - - size_t result_id = fixture.graph().add(a_id, b_id); - fixture.execute(); - - std::vector<__fp16> expected = {6, 7, 8, 9}; - return fixture.verify_output(result_id, expected); -} - -bool test_broadcast_different_ranks() { - TestUtils::FP16TestFixture fixture("Broadcast Different Ranks"); - - size_t a_id = fixture.create_input({2, 2, 3}); - size_t b_id = fixture.create_input({2, 3}); - - std::vector<__fp16> data_a = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; - std::vector<__fp16> data_b = {1, 1, 1, 2, 2, 2}; - fixture.set_input_data(a_id, data_a); - fixture.set_input_data(b_id, data_b); - - size_t result_id = fixture.graph().add(a_id, b_id); - fixture.execute(); - - std::vector<__fp16> expected = {2, 3, 4, 6, 7, 8, 8, 9, 10, 12, 13, 14}; - return fixture.verify_output(result_id, expected); -} - -bool test_broadcast_fp16_precision() { - TestUtils::FP16TestFixture fixture("Broadcast FP16 Precision"); - - size_t a_id = fixture.create_input({2, 2}); - size_t b_id = fixture.create_input({1}); - - std::vector<__fp16> data_a = {1.5f, 2.5f, 3.5f, 4.5f}; - std::vector<__fp16> data_b = {0.5f}; - fixture.set_input_data(a_id, data_a); - fixture.set_input_data(b_id, data_b); - - size_t result_id = fixture.graph().add(a_id, b_id); - fixture.execute(); - - std::vector<__fp16> expected = {2.0f, 3.0f, 4.0f, 5.0f}; - return fixture.verify_output(result_id, expected); -} - -bool test_precision_traits() { - assert(PrecisionTraits::size_of(Precision::INT8) == 1); - assert(PrecisionTraits::size_of(Precision::FP32) == 4); - return true; -} - -bool test_graph_precision_construction() { - TestUtils::FP16TestFixture fixture("Graph Precision Construction"); - - size_t fp16_id = fixture.create_input({2, 3}, Precision::FP16); - size_t fp32_id = fixture.create_input({3, 4}, Precision::FP32); - - const auto& fp16_buffer = fixture.graph().get_output_buffer(fp16_id); - const auto& fp32_buffer = fixture.graph().get_output_buffer(fp32_id); - - assert(fp16_buffer.precision == Precision::FP16); - assert(fp16_buffer.shape[0] == 2); - assert(fp16_buffer.shape[1] == 3); - assert(fp16_buffer.byte_size == 12); // 6 elements * 2 bytes - - assert(fp32_buffer.precision == Precision::FP32); - assert(fp32_buffer.shape[0] == 3); - assert(fp32_buffer.shape[1] == 4); - assert(fp32_buffer.byte_size == 48); - - return true; -} - -bool test_precision_conversion() { - TestUtils::FP16TestFixture fixture("Precision Conversion"); - - size_t fp16_id = fixture.create_input({2, 2}, Precision::FP16); - std::vector<__fp16> data = {1, 2, 3, 4}; - fixture.set_input_data(fp16_id, data); - - size_t fp32_converted_id = fixture.graph().precision_cast(fp16_id, Precision::FP32); - fixture.execute(); - - auto* fp32_data = static_cast(fixture.graph().get_output(fp32_converted_id)); - - for (size_t i = 0; i < 4; ++i) { - assert(std::abs(fp32_data[i] - static_cast(data[i])) < 1e-3f); - } - - return true; -} - -bool test_node_save_load() { - try { - CactusGraph graph; - - size_t input_a = graph.input({2, 3}, Precision::FP16); - size_t input_b = graph.input({2, 3}, Precision::FP16); - size_t result_id = graph.add(input_a, input_b); - - std::vector<__fp16> data_a = {1, 2, 3, 4, 5, 6}; - std::vector<__fp16> data_b = {10, 20, 30, 40, 50, 60}; - - graph.set_input(input_a, const_cast(static_cast(data_a.data())), Precision::FP16); - graph.set_input(input_b, const_cast(static_cast(data_b.data())), Precision::FP16); - graph.execute(); - - std::string filename = "test_graph_save_load.bin"; - GraphFile::save_node(graph, result_id, filename); - - CactusGraph new_graph; - size_t loaded_id = new_graph.mmap_weights(filename); - new_graph.execute(); - - __fp16* original_data = static_cast<__fp16*>(graph.get_output(result_id)); - __fp16* loaded_data = static_cast<__fp16*>(new_graph.get_output(loaded_id)); - - for (size_t i = 0; i < 6; ++i) { - if (std::abs(static_cast(original_data[i]) - static_cast(loaded_data[i])) > 1e-3f) { - graph.hard_reset(); - new_graph.hard_reset(); - std::remove(filename.c_str()); - return false; - } - } - - const auto& buf = new_graph.get_output_buffer(loaded_id); - bool result = (buf.shape == std::vector{2, 3}) && - (buf.precision == Precision::FP16) && - (buf.byte_size == 12); - - graph.hard_reset(); - new_graph.hard_reset(); - std::remove(filename.c_str()); - return result; - } catch (const std::exception& e) { - return false; - } -} - - -bool test_graph_save_load() { - try { - const std::string filename = "test_graph_save_load.cg"; - - CactusGraph graph; - size_t input_a = graph.input({2, 3}, Precision::FP16); - size_t input_b = graph.input({2, 3}, Precision::FP16); - size_t sum_id = graph.add(input_a, input_b); - graph.pow(sum_id, 2.0f); - - graph.save(filename); - - GraphFile::SerializedGraph sg = GraphFile::load_graph(filename); - - if (sg.header.node_count != 4) { - std::cout << "[graph_save_load] unexpected node_count: " - << sg.header.node_count << " expected 4" << std::endl; - std::remove(filename.c_str()); - return false; - } - - if (sg.graph_inputs.size() != 2 || sg.graph_inputs[0] != 0 || - sg.graph_inputs[1] != 1) { - std::cout << "[graph_save_load] unexpected graph_inputs:"; - for (uint32_t idx : sg.graph_inputs) { - std::cout << " " << idx; - } - std::cout << std::endl; - std::remove(filename.c_str()); - return false; - } - - if (sg.graph_outputs.size() != 1 || sg.graph_outputs[0] != 3) { - std::cout << "[graph_save_load] unexpected graph_outputs:"; - for (uint32_t idx : sg.graph_outputs) { - std::cout << " " << idx; - } - std::cout << std::endl; - std::remove(filename.c_str()); - return false; - } - - CactusGraph loaded = CactusGraph::load(filename); - - std::vector<__fp16> data_a = {1, 2, 3, 4, 5, 6}; - std::vector<__fp16> data_b = {10, 20, 30, 40, 50, 60}; - - const size_t loaded_input_a = runtime_id_from_serialized_index(sg.graph_inputs[0]); - const size_t loaded_input_b = runtime_id_from_serialized_index(sg.graph_inputs[1]); - const size_t loaded_output_id = runtime_id_from_serialized_index(sg.graph_outputs[0]); - - loaded.set_input(loaded_input_a, data_a.data(), Precision::FP16); - loaded.set_input(loaded_input_b, data_b.data(), Precision::FP16); - loaded.execute(); - - __fp16* output = static_cast<__fp16*>(loaded.get_output(loaded_output_id)); - std::vector expected = { - 121.0f, 484.0f, 1089.0f, - 1936.0f, 3025.0f, 4356.0f - }; - - for (size_t i = 0; i < expected.size(); ++i) { - float got = static_cast(output[i]); - if (std::abs(got - expected[i]) > 1.5f) { - std::cout << "[graph_save_load] mismatch at index " << i - << ": got=" << got - << " expected=" << expected[i] << std::endl; - std::remove(filename.c_str()); - return false; - } - } - - std::remove(filename.c_str()); - return true; - } catch (const std::exception& e) { - std::cout << "[graph_save_load] exception: " << e.what() << std::endl; - return false; - } -} - -bool test_graph_save_load_roundtrip_execution() { - try { - const std::string filename = "test_graph_save_load_roundtrip.cg"; - - CactusGraph original; - size_t input_a = original.input({2, 3}, Precision::FP16); - size_t input_b = original.input({2, 3}, Precision::FP16); - size_t sum_id = original.add(input_a, input_b); - size_t pow_id = original.pow(sum_id, 2.0f); - - std::vector<__fp16> data_a = {1, 2, 3, 4, 5, 6}; - std::vector<__fp16> data_b = {10, 20, 30, 40, 50, 60}; - - original.set_input(input_a, data_a.data(), Precision::FP16); - original.set_input(input_b, data_b.data(), Precision::FP16); - original.execute(); - - __fp16* original_output = static_cast<__fp16*>(original.get_output(pow_id)); - std::vector expected(6); - for (size_t i = 0; i < expected.size(); ++i) { - expected[i] = static_cast(original_output[i]); - } - - original.save(filename); - - CactusGraph loaded = CactusGraph::load(filename); - GraphFile::SerializedGraph loaded_graph = GraphFile::load_graph(filename); - const size_t loaded_input_a = runtime_id_from_serialized_index(loaded_graph.graph_inputs[0]); - const size_t loaded_input_b = runtime_id_from_serialized_index(loaded_graph.graph_inputs[1]); - const size_t loaded_output_id = runtime_id_from_serialized_index(loaded_graph.graph_outputs[0]); - - loaded.set_input(loaded_input_a, data_a.data(), Precision::FP16); - loaded.set_input(loaded_input_b, data_b.data(), Precision::FP16); - loaded.execute(); - - __fp16* loaded_output = static_cast<__fp16*>(loaded.get_output(loaded_output_id)); - for (size_t i = 0; i < expected.size(); ++i) { - float got = static_cast(loaded_output[i]); - if (std::abs(got - expected[i]) > 1e-3f) { - std::cout << "[graph_save_load_roundtrip] mismatch at index " << i - << ": got=" << got - << " expected(original)=" << expected[i] << std::endl; - std::remove(filename.c_str()); - return false; - } - } - - std::remove(filename.c_str()); - return true; - } catch (const std::exception& e) { - std::cout << "[graph_save_load_roundtrip] exception: " << e.what() << std::endl; - return false; - } -} - -bool test_graph_save_for_inspection() { - try { - const std::string filename = "test_graph_inspect.cg"; - - CactusGraph graph; - size_t input_a = graph.input({2, 3}, Precision::FP16); - size_t input_b = graph.input({2, 3}, Precision::FP16); - size_t sum_id = graph.add(input_a, input_b); - size_t pow_id = graph.pow(sum_id, 2.0f); - - (void)input_a; - (void)input_b; - (void)sum_id; - (void)pow_id; - - graph.save(filename); - return true; - } catch (const std::exception& e) { - std::cout << "[graph_save_for_inspection] exception: " << e.what() << std::endl; - return false; - } -} - -bool test_graph_save_load_supported_ops_roundtrip() { - try { - const std::string filename = "test_graph_supported_roundtrip.cg"; - - CactusGraph original; - - size_t a = original.input({2, 3}, Precision::FP16); - size_t b = original.input({2, 3}, Precision::FP16); - size_t m1 = original.input({2, 3}, Precision::FP16); - size_t m2 = original.input({3, 2}, Precision::FP16); - - size_t add_id = original.add(a, b); - size_t add_clipped_id = original.add_clipped(a, b); - size_t sub_id = original.subtract(a, b); - size_t mul_id = original.multiply(a, b); - size_t div_id = original.divide(a, b); - - size_t abs_id = original.abs(sub_id); - size_t relu_id = original.relu(sub_id); - size_t silu_id = original.silu(add_id); - size_t gelu_id = original.gelu(add_id); - size_t gelu_erf_id = original.gelu_erf(add_id); - size_t sigmoid_id = original.sigmoid(sub_id); - size_t tanh_id = original.tanh(sub_id); - - size_t pow_id = original.pow(abs_id, 2.0f); - size_t scalar_add_id = original.scalar_add(add_id, 1.5f); - size_t scalar_sub_id = original.scalar_subtract(add_id, 0.5f); - size_t scalar_mul_id = original.scalar_multiply(add_id, 2.0f); - size_t scalar_div_id = original.scalar_divide(add_id, 2.0f); - - size_t view_id = original.view(add_id, {3, 2}); - size_t reshape_id = original.reshape(add_id, {3, 2}); - size_t flatten_id = original.flatten(add_id); - size_t concat_id = original.concat(a, b, 1); - size_t cat_id = original.cat({a, b}, 1); - - size_t slice_id = original.slice(add_id, 1, 1, 2); - size_t index_id = original.index(add_id, 1, 0); - - size_t sum_id = original.sum(add_id, -1); - size_t mean_id = original.mean(add_id, 1); - size_t variance_id = original.variance(add_id, 1); - size_t min_id = original.min(add_id, 1); - size_t max_id = original.max(add_id, 1); - size_t softmax_id = original.softmax(add_id, 1); - - size_t matmul_id = original.matmul(m1, m2, false); - size_t persistent_id = original.persistent(add_id); - - std::vector<__fp16> data_a = {1, 2, 3, 4, 5, 6}; - std::vector<__fp16> data_b = {6, 5, 4, 3, 2, 1}; - std::vector<__fp16> data_m1 = {1, 2, 3, 4, 5, 6}; - std::vector<__fp16> data_m2 = {1, 2, 3, 4, 5, 6}; - - original.set_input(a, data_a.data(), Precision::FP16); - original.set_input(b, data_b.data(), Precision::FP16); - original.set_input(m1, data_m1.data(), Precision::FP16); - original.set_input(m2, data_m2.data(), Precision::FP16); - original.execute(); - - std::vector check_nodes = { - add_id, add_clipped_id, sub_id, mul_id, div_id, - abs_id, relu_id, silu_id, gelu_id, gelu_erf_id, sigmoid_id, tanh_id, - pow_id, scalar_add_id, scalar_sub_id, scalar_mul_id, scalar_div_id, - view_id, reshape_id, flatten_id, concat_id, cat_id, - slice_id, index_id, - sum_id, mean_id, variance_id, min_id, max_id, softmax_id, - matmul_id, persistent_id - }; - - std::vector> expected_outputs; - std::vector> expected_shapes; - expected_outputs.reserve(check_nodes.size()); - expected_shapes.reserve(check_nodes.size()); - for (size_t node_id : check_nodes) { - const auto& buf = original.get_output_buffer(node_id); - __fp16* out = static_cast<__fp16*>(original.get_output(node_id)); - std::vector values(buf.total_size); - for (size_t i = 0; i < buf.total_size; ++i) { - values[i] = static_cast(out[i]); - } - expected_outputs.push_back(std::move(values)); - expected_shapes.push_back(buf.shape); - } - - original.save(filename); - - CactusGraph loaded = CactusGraph::load(filename); - GraphFile::SerializedGraph loaded_graph = GraphFile::load_graph(filename); - if (loaded_graph.graph_inputs.size() != 4 || loaded_graph.graph_outputs.empty()) { - std::remove(filename.c_str()); - return false; - } - - loaded.set_input(runtime_id_from_serialized_index(loaded_graph.graph_inputs[0]), data_a.data(), Precision::FP16); - loaded.set_input(runtime_id_from_serialized_index(loaded_graph.graph_inputs[1]), data_b.data(), Precision::FP16); - loaded.set_input(runtime_id_from_serialized_index(loaded_graph.graph_inputs[2]), data_m1.data(), Precision::FP16); - loaded.set_input(runtime_id_from_serialized_index(loaded_graph.graph_inputs[3]), data_m2.data(), Precision::FP16); - loaded.execute(); - - for (size_t node_idx = 0; node_idx < check_nodes.size(); ++node_idx) { - size_t node_id = check_nodes[node_idx]; - const auto& loaded_buf = loaded.get_output_buffer(node_id); - __fp16* loaded_out = static_cast<__fp16*>(loaded.get_output(node_id)); - - if (loaded_buf.shape != expected_shapes[node_idx]) { - std::cout << "[supported_roundtrip] shape mismatch for node " << node_id - << ": got="; - print_vector_inline(loaded_buf.shape); - std::cout << " expected="; - print_vector_inline(expected_shapes[node_idx]); - std::cout << std::endl; - std::remove(filename.c_str()); - return false; - } - - if (loaded_buf.total_size != expected_outputs[node_idx].size()) { - std::cout << "[supported_roundtrip] size mismatch for node " << node_id - << ": got=" << loaded_buf.total_size - << " expected=" << expected_outputs[node_idx].size() << std::endl; - std::remove(filename.c_str()); - return false; - } - - for (size_t i = 0; i < loaded_buf.total_size; ++i) { - float got = static_cast(loaded_out[i]); - float expected = expected_outputs[node_idx][i]; - if (std::abs(got - expected) > 1e-3f) { - std::cout << "[supported_roundtrip] mismatch for node " << node_id - << " at index " << i - << ": got=" << got - << " expected=" << expected << std::endl; - std::remove(filename.c_str()); - return false; - } - } - } - - std::remove(filename.c_str()); - return true; - } catch (const std::exception& e) { - std::cout << "[supported_roundtrip] exception: " << e.what() << std::endl; - return false; - } -} - -bool test_complex_graph_structure() { - TestUtils::FP16TestFixture fixture("Complex Graph Structure"); - - size_t input_a = fixture.create_input({2, 2}); - size_t input_b = fixture.create_input({2, 2}); - size_t input_c = fixture.create_input({2, 2}); - - size_t add_ab = fixture.graph().add(input_a, input_b); - size_t mul_result = fixture.graph().multiply(add_ab, input_c); - size_t scalar_result = fixture.graph().scalar_add(mul_result, 1.0f); - - std::vector<__fp16> data_a = {1, 2, 3, 4}; - std::vector<__fp16> data_b = {2, 3, 4, 5}; - std::vector<__fp16> data_c = {2, 2, 2, 2}; - fixture.set_input_data(input_a, data_a); - fixture.set_input_data(input_b, data_b); - fixture.set_input_data(input_c, data_c); - - fixture.execute(); - - std::vector<__fp16> expected = {7, 11, 15, 19}; - return fixture.verify_output(scalar_result, expected); -} - -bool test_multiple_outputs() { - TestUtils::FP16TestFixture fixture("Multiple Outputs"); - - size_t input_a = fixture.create_input({3}); - size_t add_result = fixture.graph().scalar_add(input_a, 10.0f); - size_t mul_result = fixture.graph().scalar_multiply(input_a, 2.0f); - size_t combine_result = fixture.graph().add(add_result, mul_result); - - std::vector<__fp16> data_a = {1, 2, 3}; - fixture.set_input_data(input_a, data_a); - fixture.execute(); - - std::vector<__fp16> expected_add = {11, 12, 13}; - std::vector<__fp16> expected_mul = {2, 4, 6}; - std::vector<__fp16> expected_combine = {13, 16, 19}; - - return fixture.verify_output(add_result, expected_add) && - fixture.verify_output(mul_result, expected_mul) && - fixture.verify_output(combine_result, expected_combine); -} - -bool test_graph_reset() { - CactusGraph graph; - - size_t input_a = graph.input({2}, Precision::FP16); - size_t result_id = graph.scalar_add(input_a, 5.0f); - - std::vector<__fp16> data_a = {1, 2}; - graph.set_input(input_a, data_a.data(), Precision::FP16); - graph.execute(); - - __fp16* output1 = static_cast<__fp16*>(graph.get_output(result_id)); - if (std::abs(static_cast(output1[0]) - 6.0f) > 1e-2f || - std::abs(static_cast(output1[1]) - 7.0f) > 1e-2f) return false; - - graph.hard_reset(); - if (graph.get_node_count() != 0) return false; - - size_t new_input = graph.input({2}, Precision::FP16); - size_t new_result = graph.scalar_add(new_input, 5.0f); - - std::vector<__fp16> data_b = {10, 20}; - graph.set_input(new_input, data_b.data(), Precision::FP16); - graph.execute(); - - __fp16* output2 = static_cast<__fp16*>(graph.get_output(new_result)); - return (std::abs(static_cast(output2[0]) - 15.0f) < 1e-2f && - std::abs(static_cast(output2[1]) - 25.0f) < 1e-2f); -} - -bool test_gather_operation() { - // Gather uses INT8 indices but can work with FP16 data - CactusGraph graph; - - size_t embeddings = graph.input({5, 3}, Precision::FP16); - size_t indices = graph.input({2, 2}, Precision::INT8); - size_t gathered = graph.gather(embeddings, indices); - - std::vector<__fp16> emb_data = { - 1, 2, 3, - 4, 5, 6, - 7, 8, 9, - 10, 11, 12, - 13, 14, 15 - }; - std::vector idx_data = {0, 2, 4, 1}; - - graph.set_input(embeddings, emb_data.data(), Precision::FP16); - graph.set_input(indices, idx_data.data(), Precision::INT8); - graph.execute(); - - __fp16* output = static_cast<__fp16*>(graph.get_output(gathered)); - std::vector<__fp16> expected = { - 1, 2, 3, - 7, 8, 9, - 13, 14, 15, - 4, 5, 6 - }; - - for (size_t i = 0; i < expected.size(); ++i) { - if (std::abs(static_cast(output[i]) - static_cast(expected[i])) > 1e-3f) { - graph.hard_reset(); - return false; - } - } - - graph.hard_reset(); - return true; -} - -bool test_gather_1d_tensor() { - CactusGraph graph; - - size_t tensor = graph.input({8}, Precision::FP16); - size_t indices = graph.input({3}, Precision::INT8); - size_t gathered = graph.gather(tensor, indices); - - std::vector<__fp16> tensor_data = {10, 20, 30, 40, 50, 60, 70, 80}; - std::vector idx_data = {7, 2, 0}; - - graph.set_input(tensor, tensor_data.data(), Precision::FP16); - graph.set_input(indices, idx_data.data(), Precision::INT8); - graph.execute(); - - __fp16* output = static_cast<__fp16*>(graph.get_output(gathered)); - std::vector<__fp16> expected = {80, 30, 10}; - - for (size_t i = 0; i < expected.size(); ++i) { - if (std::abs(static_cast(output[i]) - static_cast(expected[i])) > 1e-3f) { - graph.hard_reset(); - return false; - } - } - - graph.hard_reset(); - return true; -} - -bool test_gather_3d_tensor() { - TestUtils::FP16TestFixture fixture("Gather 3D Tensor"); - - size_t tensor = fixture.create_input({3, 2, 4}); - size_t indices = fixture.graph().input({2}, Precision::INT8); - size_t gathered = fixture.graph().gather(tensor, indices); - - std::vector<__fp16> tensor_data = { - // First 2x4 slice - 1.0f, 2.0f, 3.0f, 4.0f, - 5.0f, 6.0f, 7.0f, 8.0f, - // Second 2x4 slice - 9.0f, 10.0f, 11.0f, 12.0f, - 13.0f, 14.0f, 15.0f, 16.0f, - // Third 2x4 slice - 17.0f, 18.0f, 19.0f, 20.0f, - 21.0f, 22.0f, 23.0f, 24.0f - }; - std::vector idx_data = {2, 0}; - - fixture.set_input_data(tensor, tensor_data); - fixture.graph().set_input(indices, idx_data.data(), Precision::INT8); - fixture.execute(); - - std::vector<__fp16> expected = { - // Third slice (index 2) - 17.0f, 18.0f, 19.0f, 20.0f, - 21.0f, 22.0f, 23.0f, 24.0f, - // First slice (index 0) - 1.0f, 2.0f, 3.0f, 4.0f, - 5.0f, 6.0f, 7.0f, 8.0f - }; - - return fixture.verify_output(gathered, expected); -} - -bool test_gather_fp16() { - TestUtils::FP16TestFixture fixture("Gather FP16"); - - size_t embeddings = fixture.create_input({4, 2}); - CactusGraph& graph = fixture.graph(); - size_t indices = graph.input({3}, Precision::INT8); - size_t gathered = graph.gather(embeddings, indices); - - std::vector<__fp16> emb_data = { - 1.0f, 2.0f, - 3.0f, 4.0f, - 5.0f, 6.0f, - 7.0f, 8.0f - }; - std::vector idx_data = {2, 0, 3}; - - fixture.set_input_data(embeddings, emb_data); - graph.set_input(indices, idx_data.data(), Precision::INT8); - fixture.execute(); - - std::vector<__fp16> expected = { - 5.0f, 6.0f, - 1.0f, 2.0f, - 7.0f, 8.0f - }; - - return fixture.verify_output(gathered, expected); -} - -bool test_mmap_gather() { - CactusGraph graph; - - std::vector<__fp16> embeddings_data = { - 1.0f, 2.0f, 3.0f, - 4.0f, 5.0f, 6.0f, - 7.0f, 8.0f, 9.0f, - 10.0f, 11.0f, 12.0f - }; - - size_t temp_embeddings = graph.input({4, 3}, Precision::FP16); - graph.set_input(temp_embeddings, embeddings_data.data(), Precision::FP16); - - const std::string temp_file = "test_embeddings.bin"; - GraphFile::save_node(graph, temp_embeddings, temp_file); - - graph.hard_reset(); - - size_t mmap_embeddings = graph.mmap_embeddings(temp_file); - size_t indices = graph.input({3}, Precision::INT8); - size_t gathered = graph.gather(mmap_embeddings, indices); - - std::vector idx_data = {2, 0, 3}; - graph.set_input(indices, idx_data.data(), Precision::INT8); - graph.execute(); - - std::vector<__fp16> expected = { - 7.0f, 8.0f, 9.0f, - 1.0f, 2.0f, 3.0f, - 10.0f, 11.0f, 12.0f - }; - - __fp16* output = static_cast<__fp16*>(graph.get_output(gathered)); - bool passed = true; - for (size_t i = 0; i < expected.size(); i++) { - if (std::abs(static_cast(output[i]) - static_cast(expected[i])) > 0.01f) { - passed = false; - break; - } - } - - std::remove(temp_file.c_str()); - - return passed; -} - -bool test_embedding_operation() { - CactusGraph graph; - - const size_t vocab_size = 4; - const size_t hidden_dim = 8; - const size_t group_size = 8; - const size_t num_groups = hidden_dim / group_size; - const size_t BLOCK_SIZE = 4; - - std::vector emb_rowmajor(vocab_size * hidden_dim); - for (size_t row = 0; row < vocab_size; ++row) { - for (size_t k = 0; k < hidden_dim; ++k) { - emb_rowmajor[row * hidden_dim + k] = static_cast((row + 1) * 10 + k); - } - } - - std::vector emb_interleaved(vocab_size * hidden_dim); - size_t N_blocks = vocab_size / BLOCK_SIZE; - size_t K_groups = hidden_dim / BLOCK_SIZE; - - for (size_t n_blk = 0; n_blk < N_blocks; ++n_blk) { - for (size_t k_grp = 0; k_grp < K_groups; ++k_grp) { - for (size_t lane = 0; lane < BLOCK_SIZE; ++lane) { - for (size_t k_within = 0; k_within < BLOCK_SIZE; ++k_within) { - size_t src_row = n_blk * BLOCK_SIZE + lane; - size_t src_k = k_grp * BLOCK_SIZE + k_within; - size_t dst_idx = (n_blk * K_groups + k_grp) * 16 + lane * 4 + k_within; - emb_interleaved[dst_idx] = emb_rowmajor[src_row * hidden_dim + src_k]; - } - } - } - } - - std::vector<__fp16> scales_rowmajor(vocab_size * num_groups); - for (size_t i = 0; i < scales_rowmajor.size(); ++i) { - scales_rowmajor[i] = static_cast<__fp16>(1.0f); - } - - std::vector<__fp16> scales_interleaved(vocab_size * num_groups); - for (size_t n_blk = 0; n_blk < N_blocks; ++n_blk) { - for (size_t g = 0; g < num_groups; ++g) { - for (size_t lane = 0; lane < BLOCK_SIZE; ++lane) { - size_t src_row = n_blk * BLOCK_SIZE + lane; - size_t dst_idx = (n_blk * num_groups + g) * BLOCK_SIZE + lane; - scales_interleaved[dst_idx] = scales_rowmajor[src_row * num_groups + g]; - } - } - } - - size_t embeddings = graph.input({vocab_size, hidden_dim}, Precision::INT8); - graph.set_input(embeddings, emb_interleaved.data(), Precision::INT8); - - graph.set_grouped_scales(embeddings, group_size, num_groups, scales_interleaved.data()); - graph.set_interleaved(embeddings, true, vocab_size); - - size_t indices = graph.input({4}, Precision::INT8); - size_t embedded = graph.embedding(embeddings, indices); - - std::vector idx_data = {0, 2, 3, 1}; - graph.set_input(indices, idx_data.data(), Precision::INT8); - graph.execute(); - - __fp16* output = static_cast<__fp16*>(graph.get_output(embedded)); - - std::vector expected = { - 10, 11, 12, 13, 14, 15, 16, 17, // idx 0 - 30, 31, 32, 33, 34, 35, 36, 37, // idx 2 - 40, 41, 42, 43, 44, 45, 46, 47, // idx 3 - 20, 21, 22, 23, 24, 25, 26, 27 // idx 1 - }; - - for (size_t i = 0; i < expected.size(); ++i) { - float out_val = static_cast(output[i]); - if (std::abs(out_val - expected[i]) > 0.5f) { - std::cerr << "Embedding mismatch at " << i << ": got " << out_val - << ", expected " << expected[i] << std::endl; - return false; - } - } - - return true; -} - -bool test_embedding_from_file() { - CactusGraph graph; - - std::vector<__fp16> embeddings_data = { - 1.0f, 5.0f, 9.0f, - 2.0f, 6.0f, 10.0f, - 3.0f, 7.0f, 11.0f, - 4.0f, 8.0f, 12.0f - }; - - size_t temp_embeddings = graph.input({4, 3}, Precision::FP16); - graph.set_input(temp_embeddings, embeddings_data.data(), Precision::FP16); - - const std::string temp_file = "test_embedding.bin"; - GraphFile::save_node(graph, temp_embeddings, temp_file); - - graph.hard_reset(); - - size_t indices = graph.input({3}, Precision::INT8); - size_t embedded = graph.embedding(temp_file, indices); - - std::vector idx_data = {2, 0, 3}; - graph.set_input(indices, idx_data.data(), Precision::INT8); - graph.execute(); - - std::vector<__fp16> expected = { - 3.0f, 7.0f, 11.0f, - 1.0f, 5.0f, 9.0f, - 4.0f, 8.0f, 12.0f - }; - - __fp16* output = static_cast<__fp16*>(graph.get_output(embedded)); - bool passed = true; - for (size_t i = 0; i < expected.size(); i++) { - if (std::abs(static_cast(output[i]) - static_cast(expected[i])) > 0.01f) { - passed = false; - break; - } - } - - std::remove(temp_file.c_str()); - - return passed; -} - -bool test_stft() { - const size_t N = 2, C_in = 1, L = 8, K = 4, stride = 2, num_fft_bins = 2; - const size_t C_out = 2 * num_fft_bins; - const size_t out_len = (L - K) / stride + 1; - - std::vector<__fp16> weight_data = { - (__fp16) 1, (__fp16) 1, (__fp16) 1, (__fp16) 1, - (__fp16) 1, (__fp16) 0, (__fp16)-1, (__fp16) 0, - (__fp16) 0, (__fp16) 0, (__fp16) 0, (__fp16) 0, - (__fp16) 0, (__fp16)-1, (__fp16) 0, (__fp16) 1, - }; - std::vector<__fp16> input_data = { - (__fp16)1, (__fp16)2, (__fp16)3, (__fp16)4, (__fp16)5, (__fp16)6, (__fp16)7, (__fp16)8, - (__fp16)0, (__fp16)1, (__fp16)0, (__fp16)-1, (__fp16)0, (__fp16)1, (__fp16)0, (__fp16)-1, - }; - - TestUtils::FP16TestFixture fx; - size_t inp = fx.create_input({N, C_in, L}); - size_t wt = fx.create_input({C_out, C_in, K}); - size_t out = fx.graph().stft(inp, wt, stride, num_fft_bins); - - if (fx.graph().get_output_buffer(out).shape != std::vector{N, C_out, out_len}) return false; - - fx.set_input_data(inp, input_data); - fx.set_input_data(wt, weight_data); - fx.execute(); - - const __fp16* cplx = fx.get_output(out); - const size_t out_bs = C_out * out_len; - const float tol = 0.1f; - - for (size_t t = 0; t < out_len; ++t) { - if (std::abs((float)cplx[1 * out_len + t] - (-2.0f)) > tol) return false; - if (std::abs((float)cplx[(1 + num_fft_bins) * out_len + t] - 2.0f) > tol) return false; - } - - const float batch1_bin1_imag[3] = {-2.0f, 2.0f, -2.0f}; - for (size_t t = 0; t < out_len; ++t) { - if (std::abs((float)cplx[out_bs + 1 * out_len + t] - 0.0f) > tol) return false; - if (std::abs((float)cplx[out_bs + (1 + num_fft_bins) * out_len + t] - batch1_bin1_imag[t]) > tol) return false; - } - - return true; -} - -template -static bool run_layernorm_case( - size_t batch, size_t feat, bool with_bias, float epsilon, - float weight_scale, float bias_val) -{ - const size_t total = batch * feat; - - std::vector input_f(total), weight_f(feat), bias_f(feat); - for (size_t b = 0; b < batch; ++b) - for (size_t j = 0; j < feat; ++j) - input_f[b * feat + j] = static_cast(j + 1); - for (size_t j = 0; j < feat; ++j) { - weight_f[j] = weight_scale; - bias_f[j] = bias_val; - } - - std::vector inp_data(total), w_data(feat), b_data(feat); - for (size_t i = 0; i < total; ++i) inp_data[i] = static_cast(input_f[i]); - for (size_t j = 0; j < feat; ++j) { - w_data[j] = static_cast(weight_f[j]); - b_data[j] = static_cast(bias_f[j]); - } - - TestUtils::TestFixture fx; - size_t inp_id = fx.create_input({batch, feat}); - size_t w_id = fx.create_input({feat}); - fx.set_input_data(inp_id, inp_data); - fx.set_input_data(w_id, w_data); - - size_t out_id; - if (with_bias) { - size_t b_id = fx.create_input({feat}); - fx.set_input_data(b_id, b_data); - out_id = fx.graph().layernorm(inp_id, w_id, b_id, epsilon); - } else { - out_id = fx.graph().layernorm(inp_id, w_id, epsilon); - } - - fx.execute(); - - std::vector expected(total); - for (size_t b = 0; b < batch; ++b) { - float mean = 0.0f; - for (size_t j = 0; j < feat; ++j) mean += input_f[b * feat + j]; - mean /= static_cast(feat); - float var = 0.0f; - for (size_t j = 0; j < feat; ++j) { - float d = input_f[b * feat + j] - mean; - var += d * d; - } - var /= static_cast(feat); - float inv_std = 1.0f / std::sqrt(var + epsilon); - for (size_t j = 0; j < feat; ++j) { - float val = (input_f[b * feat + j] - mean) * inv_std * weight_f[j]; - if (with_bias) val += bias_f[j]; - expected[b * feat + j] = static_cast(val); - } - } - - return fx.verify_output(out_id, expected, TestUtils::default_tolerance()); -} - -bool test_layernorm() { - struct Case { size_t batch, feat; bool fp32, with_bias; float epsilon, weight_scale, bias_val; }; - const std::vector cases = { - {1, 1, false, false, 1e-5f, 1.0f, 0.0f}, - {1, 7, false, false, 1e-5f, 1.0f, 0.0f}, - {1, 8, false, false, 1e-5f, 1.0f, 0.0f}, - {4, 8, false, false, 1e-5f, 1.0f, 0.0f}, - {4, 8, false, true, 1e-5f, 1.0f, 0.0f}, - {4, 8, true, false, 1e-5f, 1.0f, 0.0f}, - {4, 8, true, true, 1e-5f, 1.0f, 0.0f}, - {1, 8, false, false, 1.0f, 1.0f, 0.0f}, - {2, 16, false, true, 1e-5f, 0.5f, 0.3f}, - }; - - for (const auto& c : cases) { - bool ok = c.fp32 - ? run_layernorm_case(c.batch, c.feat, c.with_bias, c.epsilon, c.weight_scale, c.bias_val) - : run_layernorm_case<__fp16>(c.batch, c.feat, c.with_bias, c.epsilon, c.weight_scale, c.bias_val); - if (!ok) return false; - } - return true; -} - -int main() { - TestUtils::TestRunner runner("Graph Operations Tests"); - - runner.run_test("Basic Operations", test_basic_operations()); - runner.run_test("Basic Addition", test_basic_addition()); - runner.run_test("Basic Subtraction", test_basic_subtraction()); - runner.run_test("Basic Multiplication", test_basic_multiplication()); - runner.run_test("Basic Division", test_basic_division()); - runner.run_test("Matrix Multiplication", test_matrix_multiplication()); - runner.run_test("Transpose", test_transpose()); - runner.run_test("Reshape", test_reshape()); - runner.run_test("Scalar Operations", test_scalar_operations()); - runner.run_test("Scalar Subtract/Divide", test_scalar_subtract_divide()); - runner.run_test("Scalar Math Functions", test_scalar_math_functions()); - // new tests - runner.run_test("Abs Operation", test_abs()); - runner.run_test("Pow Operation", test_scalar_operations_with_pow()); - runner.run_test("Concat Operation", test_concat()); - runner.run_test("Cat Operation", test_cat()); - runner.run_test("View Operation", test_view()); - runner.run_test("Flatten Operation", test_flatten()); - - runner.run_test("Reduction Operations", test_reduction_operations()); - runner.run_test("FP16 Reduction Operations", test_fp16_reduction_operations()); - runner.run_test("Mean Operations", test_mean_operations()); - runner.run_test("Variance Operations", test_variance_operations()); - runner.run_test("Min/Max Operations", test_min_max_operations()); - runner.run_test("RMS Norm", test_rms_norm()); - runner.run_test("Softmax", test_softmax()); - runner.run_test("Attention", test_attention()); - runner.run_test("FP16 Precision", test_fp16_precision()); - runner.run_test("Broadcast Shape Compatibility", test_broadcast_shape_compatibility()); - runner.run_test("Broadcast Scalar Tensor", test_broadcast_scalar_tensor()); - runner.run_test("Broadcast Different Ranks", test_broadcast_different_ranks()); - runner.run_test("Broadcast FP16 Precision", test_broadcast_fp16_precision()); - runner.run_test("Precision Traits", test_precision_traits()); - runner.run_test("Graph Precision Construction", test_graph_precision_construction()); - runner.run_test("Precision Conversion", test_precision_conversion()); - runner.run_test("Node Save/Load", test_node_save_load()); - runner.run_test("Graph Save/Load", test_graph_save_load()); - runner.run_test("Graph Save/Load Roundtrip Execution", test_graph_save_load_roundtrip_execution()); - runner.run_test("Graph Save/Load Supported Ops Roundtrip", test_graph_save_load_supported_ops_roundtrip()); - runner.run_test("Graph Save For Inspection", test_graph_save_for_inspection()); - runner.run_test("Complex Graph Structure", test_complex_graph_structure()); - runner.run_test("Multiple Outputs", test_multiple_outputs()); - runner.run_test("Graph Reset", test_graph_reset()); - runner.run_test("Gather Operation", test_gather_operation()); - runner.run_test("Gather 1D Tensor", test_gather_1d_tensor()); - runner.run_test("Gather 3D Tensor", test_gather_3d_tensor()); - runner.run_test("Gather FP16", test_gather_fp16()); - runner.run_test("Memory-Mapped Gather", test_mmap_gather()); - runner.run_test("Embedding Operation", test_embedding_operation()); - runner.run_test("Embedding from File", test_embedding_from_file()); - runner.run_test("STFT Complex", test_stft()); - runner.run_test("LayerNorm", test_layernorm()); - runner.print_summary(); - return runner.all_passed() ? 0 : 1; -} diff --git a/tests/test_index.cpp b/tests/test_index.cpp deleted file mode 100644 index d6459c9bb..000000000 --- a/tests/test_index.cpp +++ /dev/null @@ -1,364 +0,0 @@ -#include "../cactus/ffi/cactus_ffi.h" -#include "test_utils.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -const char* g_index_path = std::getenv("CACTUS_INDEX_PATH"); -constexpr size_t DIM = 1024; - -std::vector random_embedding(size_t dim = DIM) { - static std::mt19937 gen(42); - static std::uniform_real_distribution dist(-1.0f, 1.0f); - std::vector emb(dim); - for (auto& v : emb) v = dist(gen); - return emb; -} - -class IndexFixture { -public: - explicit IndexFixture(const std::string& name, size_t dim = DIM, bool fresh = true) - : dir_(std::string(g_index_path) + "/" + name), dim_(dim), idx_(nullptr), cleanup_(fresh) { - if (fresh) { - cleanup(); - mkdir(dir_.c_str(), 0755); - } - } - - ~IndexFixture() { - if (idx_) cactus_index_destroy(idx_); - if (cleanup_) cleanup(); - } - - void keep_files() { cleanup_ = false; } - - cactus_index_t init() { return idx_ = cactus_index_init(dir_.c_str(), dim_); } - - cactus_index_t reopen() { - if (idx_) { cactus_index_destroy(idx_); idx_ = nullptr; } - return init(); - } - - int add(int id, const char* content, const std::vector& emb) { - const float* p = emb.data(); - const char* meta = "meta"; - return cactus_index_add(idx_, &id, &content, &meta, &p, 1, dim_); - } - - int add(int id, const char* content) { return add(id, content, random_embedding(dim_)); } - - int add_batch(int start, int count) { - std::vector ids(count); - std::vector docs(count), metas(count); - std::vector doc_ptrs(count), meta_ptrs(count); - std::vector> embs(count); - std::vector emb_ptrs(count); - - for (int i = 0; i < count; ++i) { - ids[i] = start + i; - docs[i] = "doc" + std::to_string(ids[i]); - metas[i] = "meta"; - doc_ptrs[i] = docs[i].c_str(); - meta_ptrs[i] = metas[i].c_str(); - embs[i] = random_embedding(dim_); - emb_ptrs[i] = embs[i].data(); - } - return cactus_index_add(idx_, ids.data(), doc_ptrs.data(), meta_ptrs.data(), emb_ptrs.data(), count, dim_); - } - - std::pair get(int id) { - char* buf = (char*)malloc(65536); - size_t size = 65536; - int r = cactus_index_get(idx_, &id, 1, &buf, &size, nullptr, nullptr, nullptr, nullptr); - std::string content = (r == 0) ? std::string(buf) : ""; - free(buf); - return {r, content}; - } - - int del(int id) { return cactus_index_delete(idx_, &id, 1); } - int del(const std::vector& ids) { return cactus_index_delete(idx_, ids.data(), ids.size()); } - int compact() { return cactus_index_compact(idx_); } - - std::vector query(const std::vector& emb, int k = 10) { - const float* p = emb.data(); - int* ids = (int*)malloc(k * sizeof(int)); - float* scores = (float*)malloc(k * sizeof(float)); - size_t id_sz = k, sc_sz = k; - std::string opts = "{\"top_k\":" + std::to_string(k) + "}"; - cactus_index_query(idx_, &p, 1, dim_, opts.c_str(), &ids, &id_sz, &scores, &sc_sz); - std::vector result(ids, ids + id_sz); - free(ids); free(scores); - return result; - } - - const std::string& path() const { return dir_; } - cactus_index_t get_idx() const { return idx_; } - -private: - void cleanup() { - unlink((dir_ + "/index.bin").c_str()); - unlink((dir_ + "/data.bin").c_str()); - unlink((dir_ + "/index.bin.backup").c_str()); - unlink((dir_ + "/data.bin.backup").c_str()); - rmdir(dir_.c_str()); - } - std::string dir_; - size_t dim_; - cactus_index_t idx_; - bool cleanup_; -}; - -bool test_crud() { - IndexFixture f("test_crud"); - if (!f.init()) return false; - - if (f.add(1, "hello") != 0) return false; - if (f.add(2, "world") != 0) return false; - - auto [r1, c1] = f.get(1); - auto [r2, c2] = f.get(2); - if (r1 != 0 || c1 != "hello") return false; - if (r2 != 0 || c2 != "world") return false; - - if (f.del(1) != 0) return false; - if (f.get(1).first == 0) return false; - if (f.get(2).first != 0) return false; - - return true; -} - -bool test_batch() { - IndexFixture f("test_batch"); - if (!f.init()) return false; - - if (f.add_batch(0, 100) != 0) return false; - - if (f.get(0).second != "doc0") return false; - if (f.get(50).second != "doc50") return false; - if (f.get(99).second != "doc99") return false; - - if (f.del({10, 20, 30, 40, 50}) != 0) return false; - if (f.get(50).first == 0) return false; - if (f.get(51).first != 0) return false; - - return true; -} - -bool test_query() { - IndexFixture f("test_query"); - if (!f.init()) return false; - - auto target_emb = random_embedding(); - f.add(1, "target", target_emb); - for (int i = 2; i <= 10; ++i) f.add(i, "other"); - - auto results = f.query(target_emb, 1); - if (results.empty() || results[0] != 1) return false; - - f.del(1); - results = f.query(target_emb, 10); - for (int id : results) { - if (id == 1) return false; - } - - return true; -} - -bool test_compact() { - IndexFixture f("test_compact"); - if (!f.init()) return false; - - f.add_batch(0, 20); - - struct stat st_before; - stat((f.path() + "/index.bin").c_str(), &st_before); - - f.del({0, 2, 4, 6, 8, 10, 12, 14, 16, 18}); // Delete even IDs - f.compact(); - - struct stat st_after; - stat((f.path() + "/index.bin").c_str(), &st_after); - - if (st_after.st_size >= st_before.st_size) return false; - - if (f.get(1).second != "doc1") return false; - if (f.get(19).second != "doc19") return false; - - if (f.get(0).first == 0) return false; - - return true; -} - -bool test_persistence() { - IndexFixture f("test_persist"); - if (!f.init()) return false; - - f.add_batch(0, 10); - f.del(5); - f.compact(); - - if (!f.reopen()) return false; - - if (f.get(0).second != "doc0") return false; - if (f.get(5).first == 0) return false; - if (f.get(9).second != "doc9") return false; - - return true; -} - -bool test_errors() { - IndexFixture f("test_errors"); - if (!f.init()) return false; - - f.add(1, "first"); - if (f.add(1, "second") == 0) return false; - - if (f.get(999).first == 0) return false; - - if (f.del(999) == 0) return false; - - f.del(1); - if (f.del(1) == 0) return false; - - std::vector zero_emb(DIM, 0.0f); - if (f.add(2, "zero", zero_emb) == 0) return false; - - return true; -} - -bool test_unicode() { - IndexFixture f("test_unicode"); - if (!f.init()) return false; - - f.add(1, "Hello 世界 🌍"); - return f.get(1).second == "Hello 世界 🌍"; -} - -bool test_constructor() { - { - IndexFixture f("test_ctor_valid"); - if (!f.init()) return false; - } - - { - IndexFixture f("test_ctor_dim"); - if (!f.init()) return false; - f.add(1, "test"); - f.keep_files(); - } - { - IndexFixture f("test_ctor_dim", 256, false); - bool failed = (f.init() == nullptr); - std::string dir = f.path(); - unlink((dir + "/index.bin").c_str()); - unlink((dir + "/data.bin").c_str()); - rmdir(dir.c_str()); - if (!failed) return false; - } - - return true; -} - -void run_benchmarks(TestUtils::TestRunner& runner, uint32_t num_docs) { - IndexFixture f("bench", DIM); - f.init(); - - std::vector ids(num_docs); - std::vector docs(num_docs); - std::vector doc_ptrs(num_docs), meta_ptrs(num_docs); - std::vector> embs(num_docs); - std::vector emb_ptrs(num_docs); - - for (uint32_t i = 0; i < num_docs; ++i) { - ids[i] = i; - docs[i] = "doc" + std::to_string(i); - doc_ptrs[i] = docs[i].c_str(); - meta_ptrs[i] = "meta"; - embs[i] = random_embedding(); - emb_ptrs[i] = embs[i].data(); - } - - auto t0 = std::chrono::high_resolution_clock::now(); - for (size_t i = 0; i < num_docs; i += 1000) { - size_t n = std::min(size_t(1000), num_docs - i); - cactus_index_add(f.get_idx(), ids.data() + i, doc_ptrs.data() + i, meta_ptrs.data() + i, - emb_ptrs.data() + i, n, DIM); - } - auto t1 = std::chrono::high_resolution_clock::now(); - auto add_ms = std::chrono::duration_cast(t1 - t0).count() / 1000.0; - - t0 = std::chrono::high_resolution_clock::now(); - f.reopen(); - t1 = std::chrono::high_resolution_clock::now(); - auto load_ms = std::chrono::duration_cast(t1 - t0).count() / 1000.0; - - t0 = std::chrono::high_resolution_clock::now(); - f.query(random_embedding(), 10); - t1 = std::chrono::high_resolution_clock::now(); - auto query_ms = std::chrono::duration_cast(t1 - t0).count() / 1000.0; - - t0 = std::chrono::high_resolution_clock::now(); - for (int i = 0; i < 1000; ++i) f.get(i * (num_docs / 1000)); - t1 = std::chrono::high_resolution_clock::now(); - auto get_ms = std::chrono::duration_cast(t1 - t0).count() / 1000.0; - - std::vector del_ids(1000); - for (int i = 0; i < 1000; ++i) del_ids[i] = i; - t0 = std::chrono::high_resolution_clock::now(); - f.del(del_ids); - t1 = std::chrono::high_resolution_clock::now(); - auto del_ms = std::chrono::duration_cast(t1 - t0).count() / 1000.0; - - t0 = std::chrono::high_resolution_clock::now(); - f.compact(); - t1 = std::chrono::high_resolution_clock::now(); - auto compact_ms = std::chrono::duration_cast(t1 - t0).count() / 1000.0; - - std::ostringstream ss; - ss << std::fixed << std::setprecision(2); - - ss.str(""); ss << add_ms << "ms"; - runner.log_performance("Add 100k docs", ss.str()); - - ss.str(""); ss << load_ms << "ms"; - runner.log_performance("Load 100k docs", ss.str()); - - ss.str(""); ss << query_ms << "ms"; - runner.log_performance("Query top-10", ss.str()); - - ss.str(""); ss << get_ms << "ms"; - runner.log_performance("Get 1k docs", ss.str()); - - ss.str(""); ss << del_ms << "ms"; - runner.log_performance("Delete 1k docs", ss.str()); - - ss.str(""); ss << compact_ms << "ms"; - runner.log_performance("Compact", ss.str()); -} - -int main() { - TestUtils::TestRunner runner("Index Tests"); - - runner.run_test("crud", test_crud()); - runner.run_test("batch", test_batch()); - runner.run_test("query", test_query()); - runner.run_test("compact", test_compact()); - runner.run_test("persistence", test_persistence()); - runner.run_test("errors", test_errors()); - runner.run_test("unicode", test_unicode()); - runner.run_test("constructor", test_constructor()); - - run_benchmarks(runner, 100000); - - runner.print_summary(); - - return runner.all_passed() ? 0 : 1; -} diff --git a/tests/test_kernel.cpp b/tests/test_kernel.cpp deleted file mode 100644 index 3eaf176bb..000000000 --- a/tests/test_kernel.cpp +++ /dev/null @@ -1,630 +0,0 @@ -#include "test_utils.h" -#include -#include -#include -#include -#include - -bool test_neon_add_fp16_correctness() { - const size_t size = 16; - std::vector<__fp16> a(size), b(size), result(size), expected(size); - - std::random_device rd; - std::mt19937 gen(rd()); - std::uniform_real_distribution dis(-1.0f, 1.0f); - for (size_t i = 0; i < size; ++i) { - a[i] = static_cast<__fp16>(dis(gen)); - b[i] = static_cast<__fp16>(dis(gen)); - expected[i] = a[i] + b[i]; - } - - cactus_add_f16(a.data(), b.data(), result.data(), size); - - for (size_t i = 0; i < size; ++i) { - if (std::abs(static_cast(result[i] - expected[i])) > 1e-3f) { - return false; - } - } - return true; -} - -bool test_neon_subtract_fp16_correctness() { - const size_t size = 16; - std::vector<__fp16> a(size), b(size), result(size), expected(size); - - std::random_device rd; - std::mt19937 gen(rd()); - std::uniform_real_distribution dis(-1.0f, 1.0f); - for (size_t i = 0; i < size; ++i) { - a[i] = static_cast<__fp16>(dis(gen)); - b[i] = static_cast<__fp16>(dis(gen)); - expected[i] = a[i] - b[i]; - } - - cactus_subtract_f16(a.data(), b.data(), result.data(), size); - - for (size_t i = 0; i < size; ++i) { - if (std::abs(static_cast(result[i] - expected[i])) > 1e-3f) { - return false; - } - } - return true; -} - -bool test_neon_multiply_fp16_correctness() { - const size_t size = 16; - std::vector<__fp16> a(size), b(size), result(size), expected(size); - - std::random_device rd; - std::mt19937 gen(rd()); - std::uniform_real_distribution dis(-1.0f, 1.0f); - for (size_t i = 0; i < size; ++i) { - a[i] = static_cast<__fp16>(dis(gen)); - b[i] = static_cast<__fp16>(dis(gen)); - expected[i] = a[i] * b[i]; - } - - cactus_multiply_f16(a.data(), b.data(), result.data(), size); - - for (size_t i = 0; i < size; ++i) { - if (std::abs(static_cast(result[i] - expected[i])) > 1e-3f) { - return false; - } - } - return true; -} - -bool test_neon_scalar_operations_fp16_correctness() { - const size_t size = 8; - std::vector<__fp16> input = {1.0f, 2.0f, 3.0f, 4.0f, -1.0f, -2.0f, -3.0f, -4.0f}; - std::vector<__fp16> result(size); - const float scalar = 2.0f; - - std::vector<__fp16> expected_add(size); - for (size_t i = 0; i < size; ++i) { - expected_add[i] = static_cast<__fp16>(static_cast(input[i]) + scalar); - } - - cactus_scalar_op_f16(input.data(), result.data(), size, scalar, ScalarOpType::ADD); - - if (!TestUtils::compare_arrays(result.data(), expected_add.data(), size)) { - return false; - } - - std::vector<__fp16> expected_mul(size); - for (size_t i = 0; i < size; ++i) { - expected_mul[i] = static_cast<__fp16>(static_cast(input[i]) * scalar); - } - - cactus_scalar_op_f16(input.data(), result.data(), size, scalar, ScalarOpType::MULTIPLY); - - return TestUtils::compare_arrays(result.data(), expected_mul.data(), size); -} - -bool test_neon_reduction_correctness() { - std::vector<__fp16> input = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f}; - - double sum_result = cactus_sum_all_f16(input.data(), input.size()); - double expected_sum = 36.0; - - if (std::abs(sum_result - expected_sum) > 1e-2) { - return false; - } - - double mean_result = cactus_mean_all_f16(input.data(), input.size()); - double expected_mean = 4.5; - - if (std::abs(mean_result - expected_mean) > 1e-2) { - return false; - } - - return true; -} - -bool test_neon_sum_axis_inner1_correctness() { - const size_t outer_size = 2; - const size_t axis_size = 9; - const size_t inner_size = 1; - - std::vector<__fp16> input = { - 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, - 2.0f, 4.0f, 6.0f, 8.0f, 10.0f, 12.0f, 14.0f, 16.0f, 18.0f - }; - - std::vector<__fp16> output(outer_size * inner_size); - std::vector<__fp16> expected = {45.0f, 90.0f}; - - cactus_sum_axis_f16(input.data(), output.data(), outer_size, axis_size, inner_size); - return TestUtils::compare_arrays(output.data(), expected.data(), expected.size(), 1e-2f); -} - -bool test_neon_mean_axis_inner1_correctness() { - const size_t outer_size = 2; - const size_t axis_size = 9; - const size_t inner_size = 1; - - std::vector<__fp16> input = { - 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, - 2.0f, 4.0f, 6.0f, 8.0f, 10.0f, 12.0f, 14.0f, 16.0f, 18.0f - }; - std::vector<__fp16> output(outer_size * inner_size); - std::vector<__fp16> expected = {5.0f, 10.0f}; - - cactus_mean_axis_f16(input.data(), output.data(), outer_size, axis_size, inner_size); - return TestUtils::compare_arrays(output.data(), expected.data(), expected.size(), 1e-2f); -} - -bool test_neon_variance_axis_inner1_correctness() { - const size_t outer_size = 2; - const size_t axis_size = 9; - const size_t inner_size = 1; - - std::vector<__fp16> input = { - 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, - 2.0f, 4.0f, 6.0f, 8.0f, 10.0f, 12.0f, 14.0f, 16.0f, 18.0f - }; - std::vector<__fp16> output(outer_size * inner_size); - std::vector<__fp16> expected = {6.6667f, 26.6667f}; - - cactus_variance_axis_f16(input.data(), output.data(), outer_size, axis_size, inner_size); - return TestUtils::compare_arrays(output.data(), expected.data(), expected.size(), 0.05f); -} - -bool test_neon_variance_axis_non_inner1_correctness() { - const size_t outer_size = 2; - const size_t axis_size = 9; - const size_t inner_size = 3; - - std::vector<__fp16> input(outer_size * axis_size * inner_size); - auto idx = [&](size_t outer, size_t axis, size_t inner) { - return outer * axis_size * inner_size + axis * inner_size + inner; - }; - - for (size_t a = 0; a < axis_size; ++a) { - input[idx(0, a, 0)] = static_cast<__fp16>(1.0f + static_cast(a)); - input[idx(0, a, 1)] = static_cast<__fp16>(2.0f + static_cast(a)); - input[idx(0, a, 2)] = static_cast<__fp16>(-4.0f + static_cast(a)); - - input[idx(1, a, 0)] = static_cast<__fp16>(2.0f + 2.0f * static_cast(a)); - input[idx(1, a, 1)] = static_cast<__fp16>(3.0f + 2.0f * static_cast(a)); - input[idx(1, a, 2)] = static_cast<__fp16>(-8.0f + 2.0f * static_cast(a)); - } - - std::vector<__fp16> output(outer_size * inner_size); - std::vector<__fp16> expected = { - 6.6667f, 6.6667f, 6.6667f, - 26.6667f, 26.6667f, 26.6667f - }; - - cactus_variance_axis_f16(input.data(), output.data(), outer_size, axis_size, inner_size); - return TestUtils::compare_arrays(output.data(), expected.data(), expected.size(), 0.05f); -} - -bool test_neon_min_axis_inner1_correctness() { - const size_t outer_size = 2; - const size_t axis_size = 9; - const size_t inner_size = 1; - - std::vector<__fp16> input = { - 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, - 2.0f, 4.0f, 6.0f, 8.0f, 10.0f, 12.0f, 14.0f, 16.0f, 18.0f - }; - std::vector<__fp16> output(outer_size * inner_size); - std::vector<__fp16> expected = {1.0f, 2.0f}; - - cactus_min_axis_f16(input.data(), output.data(), outer_size, axis_size, inner_size); - return TestUtils::compare_arrays(output.data(), expected.data(), expected.size(), 1e-2f); -} - -bool test_neon_max_axis_inner1_correctness() { - const size_t outer_size = 2; - const size_t axis_size = 9; - const size_t inner_size = 1; - - std::vector<__fp16> input = { - 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, - 2.0f, 4.0f, 6.0f, 8.0f, 10.0f, 12.0f, 14.0f, 16.0f, 18.0f - }; - std::vector<__fp16> output(outer_size * inner_size); - std::vector<__fp16> expected = {9.0f, 18.0f}; - - cactus_max_axis_f16(input.data(), output.data(), outer_size, axis_size, inner_size); - return TestUtils::compare_arrays(output.data(), expected.data(), expected.size(), 1e-2f); -} - -bool test_neon_transpose_fp16_correctness() { - const size_t M = 3, N = 4; - std::vector<__fp16> input = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; - std::vector<__fp16> result(M * N); - std::vector<__fp16> expected = {1, 5, 9, 2, 6, 10, 3, 7, 11, 4, 8, 12}; - - size_t shape[] = {M, N}; - size_t perm[] = {1, 0}; - - cactus_transpose_f16(input.data(), result.data(), shape, perm, 2, 0, M); - - return TestUtils::compare_arrays(result.data(), expected.data(), M * N); -} - -bool test_neon_softmax_correctness() { - const size_t batch_size = 1, seq_len = 4, vocab_size = 3; - std::vector<__fp16> input = {1.0f, 2.0f, 3.0f, - 2.0f, 3.0f, 4.0f, - 3.0f, 4.0f, 5.0f, - 4.0f, 5.0f, 6.0f}; - std::vector<__fp16> result(input.size()); - - cactus_softmax_f16(input.data(), result.data(), batch_size, seq_len, vocab_size); - - for (size_t i = 0; i < seq_len; ++i) { - float row_sum = 0.0f; - for (size_t j = 0; j < vocab_size; ++j) { - row_sum += static_cast(result[i * vocab_size + j]); - } - if (std::abs(row_sum - 1.0f) > 1e-2f) { - return false; - } - } - - return true; -} - - -bool test_neon_rope_correctness() { - const size_t batch_size = 1, seq_len = 2, num_heads = 1, head_dim = 4; - const size_t start_pos = 0; - const float theta = 10000.0f; - const size_t total_elements = batch_size * seq_len * num_heads * head_dim; - - std::vector<__fp16> input(total_elements); - std::vector<__fp16> result(total_elements); - - TestUtils::fill_random_fp16(input); - - cactus_rope_f16(input.data(), result.data(), - batch_size, seq_len, num_heads, head_dim, start_pos, theta); - - bool different_from_input = false; - for (size_t i = 0; i < total_elements; ++i) { - if (std::abs(static_cast(result[i]) - static_cast(input[i])) > 1e-3f) { - different_from_input = true; - break; - } - } - - return different_from_input; -} - -bool test_neon_attention_fp16_correctness() { - const size_t batch_size = 1, seq_len = 2, num_heads = 1, head_dim = 8; - const float scale = 1.0f / sqrtf(static_cast(head_dim)); - const size_t total_elements = batch_size * seq_len * num_heads * head_dim; - - std::vector<__fp16> queries(total_elements); - std::vector<__fp16> keys(total_elements); - std::vector<__fp16> values(total_elements); - std::vector<__fp16> result(total_elements); - - TestUtils::fill_random_fp16(queries); - TestUtils::fill_random_fp16(keys); - TestUtils::fill_random_fp16(values); - - cactus_attention_f16(queries.data(), keys.data(), values.data(), result.data(), - batch_size, seq_len, seq_len, num_heads, num_heads, head_dim, scale, nullptr); - - bool has_non_zero = false; - for (size_t i = 0; i < total_elements; ++i) { - if (static_cast(result[i]) != 0) { - has_non_zero = true; - break; - } - } - - return has_non_zero; -} - -bool test_matmul_int8_grouped_correctness() { - const size_t M = 2, K = 128, N = 4; - const size_t group_size = 32; - const size_t num_groups = K / group_size; - const size_t BLOCK_SIZE = 4; - - std::vector<__fp16> A(M * K); - for (size_t i = 0; i < M * K; ++i) { - A[i] = static_cast<__fp16>((static_cast(rand()) / static_cast(RAND_MAX) - 0.5f) * 0.5f); - } - - std::vector B_rowmajor(N * K); - for (size_t i = 0; i < N * K; ++i) { - B_rowmajor[i] = static_cast((rand() % 128) - 64); - } - - std::vector<__fp16> B_scales_rowmajor(N * num_groups); - for (size_t n = 0; n < N; ++n) { - for (size_t g = 0; g < num_groups; ++g) { - float max_abs = 0.0f; - for (size_t k = 0; k < group_size; ++k) { - float val = std::abs(static_cast(B_rowmajor[n * K + g * group_size + k])); - if (val > max_abs) max_abs = val; - } - float scale = max_abs / 127.0f; - if (scale < 1e-6f) scale = 1e-6f; - B_scales_rowmajor[n * num_groups + g] = static_cast<__fp16>(scale); - } - } - - std::vector B_interleaved(N * K); - size_t N_blocks = N / BLOCK_SIZE; - size_t K_blocks = K / BLOCK_SIZE; - for (size_t n_blk = 0; n_blk < N_blocks; ++n_blk) { - for (size_t k_blk = 0; k_blk < K_blocks; ++k_blk) { - for (size_t ni = 0; ni < BLOCK_SIZE; ++ni) { - for (size_t ki = 0; ki < BLOCK_SIZE; ++ki) { - size_t src_n = n_blk * BLOCK_SIZE + ni; - size_t src_k = k_blk * BLOCK_SIZE + ki; - size_t dst_idx = (n_blk * K_blocks + k_blk) * (BLOCK_SIZE * BLOCK_SIZE) + - ni * BLOCK_SIZE + ki; - B_interleaved[dst_idx] = B_rowmajor[src_n * K + src_k]; - } - } - } - } - - std::vector<__fp16> B_scales_interleaved(N * num_groups); - for (size_t n_blk = 0; n_blk < N_blocks; ++n_blk) { - for (size_t g = 0; g < num_groups; ++g) { - for (size_t ni = 0; ni < BLOCK_SIZE; ++ni) { - size_t src_n = n_blk * BLOCK_SIZE + ni; - size_t dst_idx = (n_blk * num_groups + g) * BLOCK_SIZE + ni; - B_scales_interleaved[dst_idx] = B_scales_rowmajor[src_n * num_groups + g]; - } - } - } - - std::vector A_quant(M * K); - std::vector A_scales(M); - for (size_t m = 0; m < M; ++m) { - float max_abs = cactus_fp16_max_abs(A.data() + m * K, K); - float scale = max_abs / 127.0f; - if (scale < 1e-10f) scale = 1e-10f; - A_scales[m] = scale; - cactus_fp16_to_int8(A.data() + m * K, A_quant.data() + m * K, K, scale); - } - - std::vector<__fp16> C(M * N); - - cactus_matmul_int8(A_quant.data(), A_scales.data(), B_interleaved.data(), - B_scales_interleaved.data(), C.data(), M, K, N, group_size); - - std::vector C_ref(M * N, 0.0f); - for (size_t m = 0; m < M; ++m) { - float a_max_abs = 0.0f; - for (size_t k = 0; k < K; ++k) { - float val = std::abs(static_cast(A[m * K + k])); - if (val > a_max_abs) a_max_abs = val; - } - float a_scale = a_max_abs / 127.0f; - if (a_scale < 1e-10f) a_scale = 1e-10f; - - std::vector A_quant_ref(K); - for (size_t k = 0; k < K; ++k) { - float val = static_cast(A[m * K + k]) / a_scale; - int32_t q = static_cast(std::round(val)); - q = std::max(-128, std::min(127, q)); - A_quant_ref[k] = static_cast(q); - } - - for (size_t n = 0; n < N; ++n) { - float acc = 0.0f; - for (size_t g = 0; g < num_groups; ++g) { - float b_scale = static_cast(B_scales_rowmajor[n * num_groups + g]); - float combined_scale = a_scale * b_scale; - - int32_t group_sum = 0; - for (size_t k = 0; k < group_size; ++k) { - size_t k_idx = g * group_size + k; - group_sum += static_cast(A_quant_ref[k_idx]) * - static_cast(B_rowmajor[n * K + k_idx]); - } - acc += static_cast(group_sum) * combined_scale; - } - C_ref[m * N + n] = acc; - } - } - - float max_abs_error = 0.0f; - for (size_t i = 0; i < M * N; ++i) { - float error = std::abs(static_cast(C[i]) - C_ref[i]); - if (error > max_abs_error) max_abs_error = error; - } - - return max_abs_error < 0.1f; -} - -bool test_int4_matmul_correctness() { - const size_t K = 128, N = 8, group_size = 32; - const size_t num_groups = K / group_size; - const size_t BS = 4; - - std::mt19937 gen(42); - std::uniform_real_distribution dis(-1.0f, 1.0f); - - std::vector B_fp32(N * K); - for (size_t i = 0; i < N * K; ++i) B_fp32[i] = dis(gen); - - std::vector B_raw(N * K); - std::vector B_scales(N * num_groups); - for (size_t n = 0; n < N; ++n) { - for (size_t g = 0; g < num_groups; ++g) { - float max_abs = 0.0f; - for (size_t k = 0; k < group_size; ++k) { - float val = std::abs(B_fp32[n * K + g * group_size + k]); - if (val > max_abs) max_abs = val; - } - float scale = std::max(max_abs / 7.0f, 1e-10f); - B_scales[n * num_groups + g] = scale; - for (size_t k = 0; k < group_size; ++k) { - int32_t q = static_cast(std::round(B_fp32[n * K + g * group_size + k] / scale)); - B_raw[n * K + g * group_size + k] = static_cast(std::clamp(q, -8, 7)); - } - } - } - - std::vector B_interleaved(N * K); - for (size_t n_blk = 0; n_blk < N / BS; ++n_blk) - for (size_t k_blk = 0; k_blk < K / BS; ++k_blk) - for (size_t ni = 0; ni < BS; ++ni) - for (size_t ki = 0; ki < BS; ++ki) - B_interleaved[(n_blk * (K / BS) + k_blk) * BS * BS + ni * BS + ki] = - B_raw[(n_blk * BS + ni) * K + k_blk * BS + ki]; - - std::vector B_packed(N * K / 2); - for (size_t i = 0; i < N * K; i += 32) { - for (size_t j = 0; j < 16; ++j) { - uint8_t lo = static_cast(B_interleaved[i + j] & 0x0F); - uint8_t hi = static_cast((B_interleaved[i + 16 + j] & 0x0F) << 4); - B_packed[i / 2 + j] = lo | hi; - } - } - - std::vector<__fp16> B_scales_interleaved(N * num_groups); - for (size_t n_blk = 0; n_blk < N / BS; ++n_blk) - for (size_t g = 0; g < num_groups; ++g) - for (size_t ni = 0; ni < BS; ++ni) - B_scales_interleaved[(n_blk * num_groups + g) * BS + ni] = - static_cast<__fp16>(B_scales[(n_blk * BS + ni) * num_groups + g]); - - for (size_t M : {1, 5}) { - std::vector<__fp16> A_fp16(M * K); - for (size_t i = 0; i < M * K; ++i) A_fp16[i] = static_cast<__fp16>(dis(gen)); - - std::vector A_quant(M * K); - std::vector A_scales(M); - for (size_t m = 0; m < M; ++m) { - A_scales[m] = std::max(cactus_fp16_max_abs(A_fp16.data() + m * K, K) / 127.0f, 1e-10f); - cactus_fp16_to_int8(A_fp16.data() + m * K, A_quant.data() + m * K, K, A_scales[m]); - } - - std::vector<__fp16> C(M * N); - cactus_matmul_int4(A_quant.data(), A_scales.data(), - reinterpret_cast(B_packed.data()), - B_scales_interleaved.data(), C.data(), M, K, N, group_size); - - std::vector C_ref(M * N, 0.0f); - for (size_t m = 0; m < M; ++m) { - for (size_t n = 0; n < N; ++n) { - float acc = 0.0f; - for (size_t g = 0; g < num_groups; ++g) { - int32_t group_sum = 0; - for (size_t k = 0; k < group_size; ++k) { - size_t k_idx = g * group_size + k; - group_sum += static_cast(A_quant[m * K + k_idx]) * - static_cast(B_raw[n * K + k_idx]); - } - acc += static_cast(group_sum) * A_scales[m] * B_scales[n * num_groups + g]; - } - C_ref[m * N + n] = acc; - } - } - - float max_err = 0.0f; - for (size_t i = 0; i < M * N; ++i) { - float err = std::abs(static_cast(C[i]) - C_ref[i]); - if (err > max_err) max_err = err; - } - - std::cout << " INT4 " << (M == 1 ? "GEMV" : "GEMM") << " max abs error: " << max_err << std::endl; - if (max_err >= 0.1f) return false; - } - - return true; -} - -bool test_stft_kernel_correctness() { - const size_t N = 2, C_in = 1, L = 8, K = 4, stride = 2, num_fft_bins = 2; - const size_t C_out = 2 * num_fft_bins; - const size_t out_len = (L - K) / stride + 1; - - const __fp16 bin0_re[] = {(__fp16) 1, (__fp16) 1, (__fp16) 1, (__fp16) 1}; - const __fp16 bin1_re[] = {(__fp16) 1, (__fp16) 0, (__fp16)-1, (__fp16) 0}; - const __fp16 bin0_im[] = {(__fp16) 0, (__fp16) 0, (__fp16) 0, (__fp16) 0}; - const __fp16 bin1_im[] = {(__fp16) 0, (__fp16)-1, (__fp16) 0, (__fp16) 1}; - std::vector<__fp16> weight; - for (const __fp16* row : {bin0_re, bin1_re, bin0_im, bin1_im}) - weight.insert(weight.end(), row, row + K); - - const __fp16 ramp[] = {(__fp16)1, (__fp16)2, (__fp16)3, (__fp16)4, - (__fp16)5, (__fp16)6, (__fp16)7, (__fp16)8}; - const __fp16 cosine[] = {(__fp16)0, (__fp16)1, (__fp16) 0, (__fp16)-1, - (__fp16)0, (__fp16)1, (__fp16) 0, (__fp16)-1}; - std::vector<__fp16> input; - input.insert(input.end(), ramp, ramp + L); - input.insert(input.end(), cosine, cosine + L); - - struct Cplx { float r, i; }; - const Cplx expected[2][2][3] = { - { { {10,0},{18,0},{26,0} }, { {-2,2},{-2,2},{-2,2} } }, - { { { 0,0},{ 0,0},{ 0,0} }, { { 0,-2},{ 0,2},{ 0,-2} } }, - }; - - std::vector<__fp16> cplx(N * C_out * out_len, (__fp16)0); - cactus_stft_f16(input.data(), weight.data(), cplx.data(), - N, L, C_in, C_out, K, stride, num_fft_bins); - - const size_t out_bs = C_out * out_len; - const float tol = 0.1f; - for (size_t n = 0; n < N; ++n) { - for (size_t b = 0; b < num_fft_bins; ++b) { - for (size_t t = 0; t < out_len; ++t) { - float r = (float)cplx[n * out_bs + b * out_len + t]; - float im = (float)cplx[n * out_bs + (b + num_fft_bins) * out_len + t]; - if (std::abs(r - expected[n][b][t].r) > tol) return false; - if (std::abs(im - expected[n][b][t].i) > tol) return false; - } - } - } - - return true; -} - -bool test_fast_tanh_f32x4_correctness() { - constexpr float TOL = 1e-5f; - for (int i = -10000; i <= 10000; ++i) { - float x = 0.001f * static_cast(i); - float32x4_t r = fast_tanh_f32x4(vdupq_n_f32(x)); - float lanes[4]; - vst1q_f32(lanes, r); - float want = std::tanh(x); - for (int k = 0; k < 4; ++k) { - if (std::fabs(lanes[k] - want) > TOL) return false; - } - } - return true; -} - -int main() { - TestUtils::TestRunner runner("Kernel Backend Tests"); - - runner.run_test("Kernel Add FP16 Correctness", test_neon_add_fp16_correctness()); - runner.run_test("Kernel Subtract FP16 Correctness", test_neon_subtract_fp16_correctness()); - runner.run_test("Kernel Multiply FP16 Correctness", test_neon_multiply_fp16_correctness()); - runner.run_test("Kernel Scalar Ops FP16 Correctness", test_neon_scalar_operations_fp16_correctness()); - runner.run_test("Kernel Reduction Correctness", test_neon_reduction_correctness()); - runner.run_test("Kernel Sum Axis Inner1 FP16 Correctness", test_neon_sum_axis_inner1_correctness()); - runner.run_test("Kernel Mean Axis Inner1 FP16 Correctness", test_neon_mean_axis_inner1_correctness()); - runner.run_test("Kernel Variance Axis Inner1 FP16 Correctness", test_neon_variance_axis_inner1_correctness()); - runner.run_test("Kernel Variance Axis Non-Inner1 FP16 Correctness", test_neon_variance_axis_non_inner1_correctness()); - runner.run_test("Kernel Min Axis Inner1 FP16 Correctness", test_neon_min_axis_inner1_correctness()); - runner.run_test("Kernel Max Axis Inner1 FP16 Correctness", test_neon_max_axis_inner1_correctness()); - runner.run_test("Kernel Transpose FP16 Correctness", test_neon_transpose_fp16_correctness()); - runner.run_test("Kernel Softmax Correctness", test_neon_softmax_correctness()); - runner.run_test("Kernel RoPE Correctness", test_neon_rope_correctness()); - runner.run_test("Kernel Attention FP16 Correctness", test_neon_attention_fp16_correctness()); - runner.run_test("Kernel Grouped INT8 MatMul Correctness", test_matmul_int8_grouped_correctness()); - runner.run_test("Kernel INT4 MatMul Correctness", test_int4_matmul_correctness()); - runner.run_test("Kernel STFT Complex Correctness", test_stft_kernel_correctness()); - runner.run_test("Kernel Fast Tanh Correctness", test_fast_tanh_f32x4_correctness()); - - runner.print_summary(); - return runner.all_passed() ? 0 : 1; -} diff --git a/tests/test_kv_cache.cpp b/tests/test_kv_cache.cpp deleted file mode 100644 index be6491cf0..000000000 --- a/tests/test_kv_cache.cpp +++ /dev/null @@ -1,271 +0,0 @@ -#include "../cactus/cactus.h" -#include "test_utils.h" -#include -#include -#include -#include -#include -#include - -using namespace cactus::engine; -using namespace std; - -void fill_fp16(vector& buffer, size_t count, float value) { - buffer.resize(count * sizeof(__fp16)); - __fp16* ptr = reinterpret_cast<__fp16*>(buffer.data()); - for (size_t i = 0; i < count; i++) { - ptr[i] = static_cast<__fp16>(value); - } -} - -bool test_sliding_window_basic() { - const size_t num_layers = 2; - const size_t max_seq = 2048; - const size_t num_kv_heads = 8; - const size_t head_dim = 64; - const size_t window_size = 16; - const size_t sink_size = 4; - - KVCache cache; - cache.init(num_layers, max_seq, std::vector(num_layers, head_dim), std::vector(num_layers, num_kv_heads), Precision::INT8); - cache.set_window_size(window_size, sink_size); - - CactusGraph graph; - graph.input({1}, Precision::FP32); - - { - size_t seq_len = 10; - vector k_nodes, v_nodes; - - for (size_t layer = 0; layer < num_layers; layer++) { - size_t k_node = graph.input({seq_len, num_kv_heads, head_dim}, Precision::FP16); - size_t v_node = graph.input({seq_len, num_kv_heads, head_dim}, Precision::FP16); - - vector k_data, v_data; - fill_fp16(k_data, seq_len * num_kv_heads * head_dim, layer + 1.0f); - fill_fp16(v_data, seq_len * num_kv_heads * head_dim, layer + 2.0f); - graph.set_input(k_node, k_data.data(), Precision::FP16); - graph.set_input(v_node, v_data.data(), Precision::FP16); - - k_nodes.push_back(k_node); - v_nodes.push_back(v_node); - } - - graph.execute(); - cache.update_from_graph(&graph, k_nodes, v_nodes, seq_len, num_layers); - - assert(cache.get_effective_seq_len() == seq_len); - } - - { - size_t additional_seq = 6; - vector k_nodes, v_nodes; - - for (size_t layer = 0; layer < num_layers; layer++) { - size_t k_node = graph.input({additional_seq, num_kv_heads, head_dim}, Precision::FP16); - size_t v_node = graph.input({additional_seq, num_kv_heads, head_dim}, Precision::FP16); - - vector k_data, v_data; - fill_fp16(k_data, additional_seq * num_kv_heads * head_dim, layer + 10.0f); - fill_fp16(v_data, additional_seq * num_kv_heads * head_dim, layer + 20.0f); - graph.set_input(k_node, k_data.data(), Precision::FP16); - graph.set_input(v_node, v_data.data(), Precision::FP16); - - k_nodes.push_back(k_node); - v_nodes.push_back(v_node); - } - - graph.execute(); - cache.update_from_graph(&graph, k_nodes, v_nodes, additional_seq, num_layers); - - assert(cache.get_effective_seq_len() == window_size); - } - - { - size_t additional_seq = 10; - vector k_nodes, v_nodes; - - float new_token_value = 100.0f; - - for (size_t layer = 0; layer < num_layers; layer++) { - size_t k_node = graph.input({additional_seq, num_kv_heads, head_dim}, Precision::FP16); - size_t v_node = graph.input({additional_seq, num_kv_heads, head_dim}, Precision::FP16); - - vector k_data, v_data; - fill_fp16(k_data, additional_seq * num_kv_heads * head_dim, new_token_value); - fill_fp16(v_data, additional_seq * num_kv_heads * head_dim, new_token_value); - graph.set_input(k_node, k_data.data(), Precision::FP16); - graph.set_input(v_node, v_data.data(), Precision::FP16); - - k_nodes.push_back(k_node); - v_nodes.push_back(v_node); - } - - graph.execute(); - cache.update_from_graph(&graph, k_nodes, v_nodes, additional_seq, num_layers); - - assert(cache.get_effective_seq_len() == window_size); - - const int8_t* key_data = cache.get_keys_int8(0); - const float* key_scales = cache.get_key_scales(0); - if (key_data && key_scales) { - bool sink_preserved = true; - - float expected_sink_value = 1.0f; - float sink_scale = key_scales[0]; - int8_t expected_sink_q = static_cast(std::round(expected_sink_value / sink_scale)); - - for (size_t i = 0; i < min((size_t)8, sink_size * num_kv_heads * head_dim); i++) { - if (abs(key_data[i] - expected_sink_q) > 2) { - sink_preserved = false; - break; - } - } - assert(sink_preserved); - } - - assert(cache.get_total_seq_len() == 26); - } - - return true; -} - -bool test_incremental_updates() { - const size_t num_layers = 1; - const size_t num_kv_heads = 4; - const size_t head_dim = 32; - const size_t window_size = 8; - const size_t sink_size = 2; - - KVCache cache; - cache.init(num_layers, 2048, std::vector(num_layers, head_dim), std::vector(num_layers, num_kv_heads), Precision::FP16); - cache.set_window_size(window_size, sink_size); - - CactusGraph graph; - graph.input({1}, Precision::FP32); - - for (size_t token = 0; token < 12; token++) { - vector k_nodes, v_nodes; - - size_t seq_len = 1; - size_t k_node = graph.input({seq_len, num_kv_heads, head_dim}, Precision::FP16); - size_t v_node = graph.input({seq_len, num_kv_heads, head_dim}, Precision::FP16); - - vector k_data, v_data; - fill_fp16(k_data, seq_len * num_kv_heads * head_dim, float(token + 1)); - fill_fp16(v_data, seq_len * num_kv_heads * head_dim, float(token + 101)); - - graph.set_input(k_node, k_data.data(), Precision::FP16); - graph.set_input(v_node, v_data.data(), Precision::FP16); - - k_nodes.push_back(k_node); - v_nodes.push_back(v_node); - - graph.execute(); - cache.update_from_graph(&graph, k_nodes, v_nodes, seq_len, num_layers); - - size_t expected_len = min(token + 1, window_size); - assert(cache.get_effective_seq_len() == expected_len); - } - - assert(cache.get_effective_seq_len() == window_size); - assert(cache.get_total_seq_len() == 12); - - const int8_t* key_data = cache.get_keys_int8(0); - const float* key_scales = cache.get_key_scales(0); - assert(key_data != nullptr); - assert(key_scales != nullptr); - - return true; -} - -bool test_reset_functionality() { - KVCache cache; - cache.init(2, 1024, std::vector(2, 64), std::vector(2, 8), Precision::FP16); - cache.set_window_size(16, 4); - - CactusGraph graph; - graph.input({1}, Precision::FP32); - vector k_nodes, v_nodes; - - for (size_t layer = 0; layer < 2; layer++) { - size_t k_node = graph.input({10, 8, 64}, Precision::FP16); - size_t v_node = graph.input({10, 8, 64}, Precision::FP16); - - vector data; - fill_fp16(data, 10 * 8 * 64, 1.0f); - graph.set_input(k_node, data.data(), Precision::FP16); - graph.set_input(v_node, data.data(), Precision::FP16); - - k_nodes.push_back(k_node); - v_nodes.push_back(v_node); - } - - graph.execute(); - cache.update_from_graph(&graph, k_nodes, v_nodes, 10, 2); - - assert(cache.get_effective_seq_len() == 10); - assert(cache.get_total_seq_len() == 10); - - cache.reset(); - - assert(cache.get_effective_seq_len() == 0); - assert(cache.get_total_seq_len() == 0); - assert(cache.get_keys_int8(0) == nullptr); - assert(cache.get_values_int8(0) == nullptr); - - return true; -} - -bool test_large_window() { - const size_t num_layers = 4; - const size_t num_kv_heads = 8; - const size_t head_dim = 64; - const size_t window_size = 512; - - KVCache cache; - cache.init(num_layers, 2048, std::vector(num_layers, head_dim), std::vector(num_layers, num_kv_heads), Precision::FP16); - cache.set_window_size(window_size, 4); - - CactusGraph graph; - graph.input({1}, Precision::FP32); - - size_t seq_len = 600; - vector k_nodes, v_nodes; - - for (size_t layer = 0; layer < num_layers; layer++) { - size_t k_node = graph.input({seq_len, num_kv_heads, head_dim}, Precision::FP16); - size_t v_node = graph.input({seq_len, num_kv_heads, head_dim}, Precision::FP16); - - vector k_data, v_data; - fill_fp16(k_data, seq_len * num_kv_heads * head_dim, float(layer + 1)); - fill_fp16(v_data, seq_len * num_kv_heads * head_dim, float(layer + 101)); - - graph.set_input(k_node, k_data.data(), Precision::FP16); - graph.set_input(v_node, v_data.data(), Precision::FP16); - - k_nodes.push_back(k_node); - v_nodes.push_back(v_node); - } - - graph.execute(); - cache.update_from_graph(&graph, k_nodes, v_nodes, seq_len, num_layers); - - assert(cache.get_effective_seq_len() == window_size); - assert(cache.get_total_seq_len() == seq_len); - - return true; -} - -int main() { - TestUtils::TestRunner runner("KV Cache Sliding Window Tests"); - runner.run_test("Basic Sliding Window", test_sliding_window_basic()); - runner.run_test("Incremental Updates", test_incremental_updates()); - runner.run_test("Reset Functionality", test_reset_functionality()); - runner.run_test("Large Window (512 tokens)", test_large_window()); - - cout << "────────────────────────────────────────────────────────────────────────────────────────\n"; - runner.print_summary(); - - return runner.all_passed() ? 0 : 1; -} \ No newline at end of file diff --git a/tests/test_llm.cpp b/tests/test_llm.cpp deleted file mode 100644 index 56382474d..000000000 --- a/tests/test_llm.cpp +++ /dev/null @@ -1,575 +0,0 @@ -#include "test_utils.h" -#include -#include -#include -#include -#include -#include - -#if __has_include() -#include -#define CACTUS_ENGINE_TEST_HAS_CURL 1 -#else -#define CACTUS_ENGINE_TEST_HAS_CURL 0 -#endif - -using namespace EngineTestUtils; - -static const char* g_model_path = std::getenv("CACTUS_TEST_MODEL"); - -static const char* g_options = R"({ - "max_tokens": 256, - "stop_sequences": ["<|im_end|>", ""], - "telemetry_enabled": false - })"; - -template -bool run_test(const char* title, const char* messages, TestFunc test_logic, - const char* tools = nullptr, int stop_at = -1) { - return EngineTestUtils::run_test(title, g_model_path, messages, g_options, test_logic, tools, stop_at); -} - -bool test_streaming() { - std::cout << "\n╔══════════════════════════════════════════╗\n" - << "║" << std::setw(42) << std::left << " STREAMING & FOLLOW-UP TEST" << "║\n" - << "╚══════════════════════════════════════════╝\n"; - - cactus_model_t model = cactus_init(g_model_path, nullptr, false); - if (!model) { - std::cerr << "[✗] Failed to initialize model\n"; - return false; - } - - const char* messages1 = R"([ - {"role": "system", "content": "You are a helpful assistant. Be concise."}, - {"role": "user", "content": "My name is Henry Ndubuaku, how are you?"} - ])"; - - StreamingData data1; - data1.model = model; - char response1[4096]; - - std::cout << "\n[Turn 1]\n"; - std::cout << "User: My name is Henry Ndubuaku, how are you?\n"; - std::cout << "Assistant: "; - - int result1 = cactus_complete(model, messages1, response1, sizeof(response1), - g_options, nullptr, stream_callback, &data1, nullptr, 0); - - std::cout << "\n\n[Results - Turn 1]\n"; - Metrics metrics1; - metrics1.parse(response1); - metrics1.print_json(); - - bool success1 = result1 > 0 && data1.token_count > 0; - - if (!success1) { - std::cout << "└─ Status: FAILED ✗\n"; - cactus_destroy(model); - return false; - } - - std::string assistant_response; - for(const auto& token : data1.tokens) { - assistant_response += token; - } - - std::string messages2_str = R"([ - {"role": "system", "content": "You are a helpful assistant. Be concise."}, - {"role": "user", "content": "My name is Henry Ndubuaku, how are you?"}, - {"role": "assistant", "content": ")" + escape_json(assistant_response) + R"("}, - {"role": "user", "content": "What is my name?"} - ])"; - - StreamingData data2; - data2.model = model; - char response2[4096]; - - std::cout << "\n[Turn 2]\n"; - std::cout << "User: What is my name?\n"; - std::cout << "Assistant: "; - - int result2 = cactus_complete(model, messages2_str.c_str(), response2, sizeof(response2), - g_options, nullptr, stream_callback, &data2, nullptr, 0); - - std::cout << "\n\n[Results - Turn 2]\n"; - Metrics metrics2; - metrics2.parse(response2); - metrics2.print_json(); - - bool success2 = result2 > 0 && data2.token_count > 0; - - cactus_destroy(model); - return success1 && success2; -} - -bool test_prefill_idempotent_reuse() { - std::cout << "\n╔══════════════════════════════════════════╗\n" - << "║" << std::setw(42) << std::left << " PREFILL IDEMPOTENT REUSE TEST" << "║\n" - << "╚══════════════════════════════════════════╝\n"; - - cactus_model_t model = cactus_init(g_model_path, nullptr, false); - if (!model) { - std::cerr << "[✗] Failed to initialize model\n"; - return false; - } - - const char* messages = R"([ - {"role": "system", "content": "You are a helpful assistant. Be concise."}, - {"role": "user", "content": "Write one short sentence about brainrot."} - ])"; - - const char* tools = R"([{ - "type": "function", - "function": { - "name": "summarize_topic", - "description": "Summarize a topic in one short sentence", - "parameters": { - "type": "object", - "properties": { - "topic": {"type": "string", "description": "Topic to summarize"} - }, - "required": ["topic"] - } - } - }])"; - - char prefill_response1[2048] = {0}; - int prefill_result1 = cactus_prefill(model, messages, prefill_response1, sizeof(prefill_response1), nullptr, tools, nullptr, 0); - - PrefillMetrics prefill_metrics1; - prefill_metrics1.parse(prefill_response1); - - char prefill_response2[2048] = {0}; - int prefill_result2 = cactus_prefill(model, messages, prefill_response2, sizeof(prefill_response2), nullptr, tools, nullptr, 0); - - PrefillMetrics prefill_metrics2; - prefill_metrics2.parse(prefill_response2); - - std::cout << "\n\n[Results]\n"; - std::cout << "├─ Prefill#1 benchmark: "; - prefill_metrics1.print_line(); - std::cout << "\n" - << "├─ Prefill#2 benchmark: "; - prefill_metrics2.print_line(); - std::cout << "\n"; - - bool prefill_success = prefill_result1 > 0 && prefill_result2 > 0 - && prefill_metrics1.success && prefill_metrics2.success; - bool skipped_recompute = prefill_metrics2.prefill_tokens == 0; - - std::cout << "├─ Prefill calls success: " << (prefill_success ? "YES" : "NO") << "\n" - << "└─ Second prefill skipped recompute: " << (skipped_recompute ? "YES" : "NO") << std::endl; - - cactus_destroy(model); - return prefill_success && skipped_recompute; -} - -bool test_prefill_prefix_extension_reuse() { - std::cout << "\n╔══════════════════════════════════════════╗\n" - << "║" << std::setw(42) << std::left << " PREFILL PREFIX EXTENSION TEST" << "║\n" - << "╚══════════════════════════════════════════╝\n"; - - cactus_model_t model = cactus_init(g_model_path, nullptr, false); - if (!model) { - std::cerr << "[✗] Failed to initialize model\n"; - return false; - } - - const char* messages_base = R"([ - {"role": "system", "content": "You are a helpful assistant. Be concise."}, - {"role": "user", "content": "Write one short sentence about brainrot."} - ])"; - - const char* messages_extended = R"([ - {"role": "system", "content": "You are a helpful assistant. Be concise."}, - {"role": "user", "content": "Write one short sentence about brainrot."}, - {"role": "assistant", "content": "Brainrot is internet slang for obsessive, meme-heavy online fixation."}, - {"role": "user", "content": "Now rewrite that in six words."} - ])"; - - const char* tools = R"([{ - "type": "function", - "function": { - "name": "summarize_topic", - "description": "Summarize a topic in one short sentence", - "parameters": { - "type": "object", - "properties": { - "topic": {"type": "string", "description": "Topic to summarize"} - }, - "required": ["topic"] - } - } - }])"; - - char prefill_response1[2048] = {0}; - int prefill_result1 = cactus_prefill(model, messages_base, prefill_response1, sizeof(prefill_response1), nullptr, tools, nullptr, 0); - PrefillMetrics prefill_metrics1; - prefill_metrics1.parse(prefill_response1); - - char prefill_response2[2048] = {0}; - int prefill_result2 = cactus_prefill(model, messages_extended, prefill_response2, sizeof(prefill_response2), nullptr, tools, nullptr, 0); - PrefillMetrics prefill_metrics2; - prefill_metrics2.parse(prefill_response2); - - cactus_reset(model); - - char prefill_response3[2048] = {0}; - int prefill_result3 = cactus_prefill(model, messages_extended, prefill_response3, sizeof(prefill_response3), nullptr, tools, nullptr, 0); - PrefillMetrics prefill_metrics3; - prefill_metrics3.parse(prefill_response3); - - std::cout << "\n\n[Results]\n"; - std::cout << "├─ Prefill#1 (base): "; - prefill_metrics1.print_line(); - std::cout << "\n" - << "├─ Prefill#2 (extended, warm): "; - prefill_metrics2.print_line(); - std::cout << "\n" - << "├─ Prefill#3 (extended, cold): "; - prefill_metrics3.print_line(); - std::cout << "\n"; - - bool prefill_success = prefill_result1 > 0 && prefill_result2 > 0 && prefill_result3 > 0 - && prefill_metrics1.success && prefill_metrics2.success && prefill_metrics3.success; - bool second_call_prefilled = prefill_metrics2.prefill_tokens > 0; - bool warm_reused_prefix = prefill_metrics2.prefill_tokens < prefill_metrics3.prefill_tokens; - - std::cout << "├─ Prefill calls success: " << (prefill_success ? "YES" : "NO") << "\n" - << "├─ Warm extension prefilled tokens: " << (second_call_prefilled ? "YES" : "NO") << "\n" - << "└─ Warm extension < cold extension: " << (warm_reused_prefix ? "YES" : "NO") << std::endl; - - cactus_destroy(model); - return prefill_success && second_call_prefilled && warm_reused_prefix; -} - -bool test_prefill_invalidated_on_message_change() { - std::cout << "\n╔══════════════════════════════════════════╗\n" - << "║" << std::setw(42) << std::left << " PREFILL INVALIDATION (LLM) TEST" << "║\n" - << "╚══════════════════════════════════════════╝\n"; - - cactus_model_t model = cactus_init(g_model_path, nullptr, false); - if (!model) { - std::cerr << "[✗] Failed to initialize model\n"; - return false; - } - - const char* prefill_messages = R"([ - {"role": "system", "content": "You are a helpful assistant. Be concise."}, - {"role": "user", "content": "Summarize the phrase 'brainrot' in one sentence."} - ])"; - - const char* complete_messages = R"([ - {"role": "system", "content": "You are a helpful assistant. Be concise."}, - {"role": "user", "content": "Give one sentence about the power of the 'brainrot'."} - ])"; - - const char* options = R"({ - "max_tokens": 128, - "stop_sequences": ["<|im_end|>", ""], - "confidence_threshold": 0.0, - "telemetry_enabled": false - })"; - - char prefill_response[2048] = {0}; - int prefill_result = cactus_prefill(model, prefill_messages, prefill_response, sizeof(prefill_response), nullptr, nullptr, nullptr, 0); - PrefillMetrics prefill_metrics; - prefill_metrics.parse(prefill_response); - - char complete_response_warm[4096] = {0}; - int complete_result_warm = cactus_complete(model, complete_messages, complete_response_warm, sizeof(complete_response_warm), - options, nullptr, nullptr, nullptr, nullptr, 0); - Metrics warm_metrics; - warm_metrics.parse(complete_response_warm); - - cactus_reset(model); - - char complete_response_cold[4096] = {0}; - int complete_result_cold = cactus_complete(model, complete_messages, complete_response_cold, sizeof(complete_response_cold), - options, nullptr, nullptr, nullptr, nullptr, 0); - Metrics cold_metrics; - cold_metrics.parse(complete_response_cold); - - std::cout << "\n\n[Results]\n"; - std::cout << "├─ Prefill success: " << ((prefill_result > 0 && prefill_metrics.success) ? "YES" : "NO") << "\n" - << "├─ Complete(warm mismatched) prefill_tokens: " << warm_metrics.prefill_tokens << "\n" - << "├─ Complete(cold) prefill_tokens: " << cold_metrics.prefill_tokens << "\n"; - - bool all_success = prefill_result > 0 && prefill_metrics.success - && complete_result_warm > 0 && warm_metrics.success - && complete_result_cold > 0 && cold_metrics.success; - bool invalidated = warm_metrics.prefill_tokens == cold_metrics.prefill_tokens; - - std::cout << "├─ Calls successful: " << (all_success ? "YES" : "NO") << "\n" - << "└─ Mismatch invalidated cache: " << (invalidated ? "YES" : "NO") << std::endl; - - cactus_destroy(model); - return all_success && invalidated; -} - -bool test_prefill() { - std::cout << "\n╔══════════════════════════════════════════╗\n" - << "║" << std::setw(42) << std::left << " PREFILL API TEST" << "║\n" - << "╚══════════════════════════════════════════╝\n"; - - cactus_model_t model = cactus_init(g_model_path, nullptr, false); - if (!model) { - std::cerr << "[✗] Failed to initialize model\n"; - return false; - } - - const char* prefill_messages = R"([ - {"role": "system", "content": "You are a helpful assistant. Be concise."}, - {"role": "user", "content": "Explain what brainrot means in one short sentence."}, - {"role": "assistant", "content": "Brainrot is internet slang for obsessive, meme-heavy online fixation."} - ])"; - - const char* complete_messages = R"([ - {"role": "system", "content": "You are a helpful assistant. Be concise."}, - {"role": "user", "content": "Explain what brainrot means in one short sentence."}, - {"role": "assistant", "content": "Brainrot is internet slang for obsessive, meme-heavy online fixation."}, - {"role": "user", "content": "Now rewrite that in six words."} - ])"; - - const char* options = R"({ - "max_tokens": 128, - "stop_sequences": ["<|im_end|>", ""], - "confidence_threshold": 0.0, - "telemetry_enabled": false - })"; - - char prefill_response[2048] = {0}; - int prefill_result = cactus_prefill(model, prefill_messages, prefill_response, sizeof(prefill_response), nullptr, nullptr, nullptr, 0); - PrefillMetrics prefill_metrics; - prefill_metrics.parse(prefill_response); - - char complete_response_warm[4096] = {0}; - int complete_result_warm = cactus_complete(model, complete_messages, complete_response_warm, sizeof(complete_response_warm), - options, nullptr, nullptr, nullptr, nullptr, 0); - Metrics warm_metrics; - warm_metrics.parse(complete_response_warm); - - cactus_reset(model); - - char complete_response_cold[4096] = {0}; - int complete_result_cold = cactus_complete(model, complete_messages, complete_response_cold, sizeof(complete_response_cold), - options, nullptr, nullptr, nullptr, nullptr, 0); - Metrics cold_metrics; - cold_metrics.parse(complete_response_cold); - - std::cout << "\n\n[Results]\n"; - std::cout << "├─ Prefill success: " << ((prefill_result > 0 && prefill_metrics.success) ? "YES" : "NO") << "\n" - << "├─ Prefill metrics: "; - prefill_metrics.print_line(); - std::cout << "\n"; - std::cout << "├─ Complete warm metrics:\n"; - warm_metrics.print_json(); - std::cout << "├─ Complete cold metrics:\n"; - cold_metrics.print_json(); - - bool all_success = prefill_result > 0 && prefill_metrics.success - && complete_result_warm > 0 && warm_metrics.success - && complete_result_cold > 0 && cold_metrics.success; - bool warm_prefilled_less = warm_metrics.prefill_tokens < cold_metrics.prefill_tokens; - - std::cout << "├─ Calls successful: " << (all_success ? "YES" : "NO") << "\n" - << "└─ Warm prefilled less than cold: " << (warm_prefilled_less ? "YES" : "NO") << std::endl; - - cactus_destroy(model); - return all_success && warm_prefilled_less; -} - -bool test_tool_call() { - const char* messages = R"([ - {"role": "system", "content": "You are a helpful assistant that can use tools."}, - {"role": "user", "content": "What's the weather in San Francisco?"} - ])"; - - const char* tools = R"([{ - "type": "function", - "function": { - "name": "get_weather", - "description": "Get weather for a location", - "parameters": { - "type": "object", - "properties": { - "location": {"type": "string", "description": "City, State, Country"} - }, - "required": ["location"] - } - } - }])"; - - const char* options_with_force_tools = R"({ - "max_tokens": 256, - "stop_sequences": ["<|im_end|>", ""], - "force_tools": true - })"; - - return EngineTestUtils::run_test("TOOL CALL TEST", g_model_path, messages, options_with_force_tools, - [](int result, const StreamingData&, const std::string& response, const Metrics& m) { - bool has_function = response.find("\"function_calls\":[") != std::string::npos; - bool has_tool = has_function && response.find("get_weather") != std::string::npos; - std::cout << "├─ Function call: " << (has_function ? "YES" : "NO") << "\n" - << "├─ Correct tool: " << (has_tool ? "YES" : "NO") << "\n"; - m.print_json(); - return result > 0 && has_function && has_tool; - }, tools, -1, "What's the weather in San Francisco?"); -} - -bool test_multiple_tool_call_invocations() { - const char* messages = R"([ - {"role": "system", "content": "You are a helpful assistant that can use tools."}, - {"role": "user", "content": "Send a message to Bob and get the weather for San Francisco."} - ])"; - - const char* tools = R"([{ - "type": "function", - "function": { - "name": "get_weather", - "description": "Get weather for a location", - "parameters": { - "type": "object", - "properties": { - "location": {"type": "string", "description": "City, State, Country"} - }, - "required": ["location"] - } - } - }, { - "type": "function", - "function": { - "name": "send_message", - "description": "Send a message to a contact", - "parameters": { - "type": "object", - "properties": { - "recipient": {"type": "string", "description": "Name of the person to send the message to"}, - "message": {"type": "string", "description": "The message content to send"} - }, - "required": ["recipient", "message"] - } - } - }])"; - - const char* options_with_force_tools = R"({ - "max_tokens": 256, - "stop_sequences": ["<|im_end|>", ""], - "force_tools": true - })"; - - return EngineTestUtils::run_test("MULTIPLE TOOLS TEST", g_model_path, messages, options_with_force_tools, - [](int result, const StreamingData&, const std::string& response, const Metrics& m) { - bool has_function = response.find("\"function_calls\":[") != std::string::npos; - bool has_weather_tool = has_function - && (response.find("\"name\":\"get_weather\"") != std::string::npos - || response.find("\"name\": \"get_weather\"") != std::string::npos); - bool has_message_tool = has_function - && (response.find("\"name\":\"send_message\"") != std::string::npos - || response.find("\"name\": \"send_message\"") != std::string::npos); - std::cout << "├─ Function call: " << (has_function ? "YES" : "NO") << "\n" - << "├─ Correct tool: " << (has_weather_tool && has_message_tool ? "YES" : "NO") << "\n"; - m.print_json(); - return result > 0 && has_function && has_weather_tool && has_message_tool; - }, tools, -1, "Send a message to Bob and get the weather for San Francisco."); -} - -bool test_tool_call_with_three_tools() { - const char* messages = R"([ - {"role": "system", "content": "You are a helpful assistant that can use tools."}, - {"role": "user", "content": "Send a message to John saying hello."} - ])"; - - const char* tools = R"([{ - "type": "function", - "function": { - "name": "get_weather", - "description": "Get weather for a location", - "parameters": { - "type": "object", - "properties": { - "location": {"type": "string", "description": "City, State, Country"} - }, - "required": ["location"] - } - } - }, { - "type": "function", - "function": { - "name": "set_alarm", - "description": "Set an alarm for a given time", - "parameters": { - "type": "object", - "properties": { - "hour": {"type": "integer", "description": "Hour to set the alarm for"}, - "minute": {"type": "integer", "description": "Minute to set the alarm for"} - }, - "required": ["hour", "minute"] - } - } - }, { - "type": "function", - "function": { - "name": "send_message", - "description": "Send a message to a contact", - "parameters": { - "type": "object", - "properties": { - "recipient": {"type": "string", "description": "Name of the person to send the message to"}, - "message": {"type": "string", "description": "The message content to send"} - }, - "required": ["recipient", "message"] - } - } - }])"; - - const char* options_with_force_tools = R"({ - "max_tokens": 256, - "stop_sequences": ["<|im_end|>", ""], - "force_tools": true - })"; - - return EngineTestUtils::run_test("TRIPLE TOOLS TEST", g_model_path, messages, options_with_force_tools, - [](int result, const StreamingData&, const std::string& response, const Metrics& m) { - bool has_function = response.find("\"function_calls\":[") != std::string::npos; - bool has_tool = has_function && response.find("send_message") != std::string::npos; - std::cout << "├─ Function call: " << (has_function ? "YES" : "NO") << "\n" - << "├─ Correct tool: " << (has_tool ? "YES" : "NO") << "\n"; - m.print_json(); - return result > 0 && has_function && has_tool; - }, tools, -1, "Send a message to John saying hello."); -} - -bool test_1k_context() { - std::string msg = "[{\"role\": \"system\", \"content\": \"/no_think You are helpful. "; - for (int i = 0; i < 50; i++) { - msg += "Context " + std::to_string(i) + ": Background knowledge. "; - } - msg += "\"}, {\"role\": \"user\", \"content\": \""; - for (int i = 0; i < 50; i++) { - msg += "Data " + std::to_string(i) + " = " + std::to_string(i * 3.14159) + ". "; - } - msg += "Explain the data.\"}]"; - - return run_test("1K CONTEXT TEST", msg.c_str(), - [](int result, const StreamingData&, const std::string&, const Metrics& m) { - m.print_json(); - return result > 0; - }, nullptr, 100); -} - -int main() { - TestUtils::TestRunner runner("LLM Tests"); - runner.run_test("1k_context", test_1k_context()); - runner.run_test("streaming", test_streaming()); - runner.run_test("prefill", test_prefill()); - runner.run_test("prefill_idempotent_reuse", test_prefill_idempotent_reuse()); - runner.run_test("prefill_prefix_extension_reuse", test_prefill_prefix_extension_reuse()); - runner.run_test("prefill_invalidated_on_message_change", test_prefill_invalidated_on_message_change()); - runner.run_test("tool_calls", test_tool_call()); - runner.run_test("tool_multiple_tool_call_invocations", test_multiple_tool_call_invocations()); - runner.run_test("tool_calls_with_three_tools", test_tool_call_with_three_tools()); - runner.print_summary(); - return runner.all_passed() ? 0 : 1; -} diff --git a/tests/test_model_loading.cpp b/tests/test_model_loading.cpp deleted file mode 100644 index 648ea7de0..000000000 --- a/tests/test_model_loading.cpp +++ /dev/null @@ -1,74 +0,0 @@ -#include "test_utils.h" -#include -#include - -namespace fs = std::filesystem; - -static std::string make_temp_dir(const std::string& suffix) { - std::string dir = fs::temp_directory_path().string() + "/cactus_test_" + suffix; - fs::create_directories(dir); - return dir; -} - -static void write_file(const std::string& path, const std::string& content) { - std::ofstream(path, std::ios::binary) << content; -} - -static bool expect_init_fails(const std::string& path) { - cactus_model_t model = cactus_init(path.c_str(), nullptr, false); - if (model) { cactus_destroy(model); return false; } - return true; -} - -static const char* MINIMAL_CONFIG = R"({"model_type":"qwen","model_variant":"default","precision":"INT8","num_layers":2,"hidden_dim":64,"ffn_intermediate_dim":128,"attention_heads":2,"attention_kv_heads":2,"attention_head_dim":32,"vocab_size":100,"context_length":512})"; - -static bool test_missing_directory() { - return expect_init_fails("/nonexistent/path/to/model"); -} - -static bool test_missing_config() { - std::string dir = make_temp_dir("missing_config"); - write_file(dir + "/dummy.bin", "placeholder"); - bool ok = expect_init_fails(dir); - fs::remove_all(dir); - return ok; -} - -static bool test_corrupt_weights() { - std::string dir = make_temp_dir("corrupt_weights"); - write_file(dir + "/config.txt", MINIMAL_CONFIG); - write_file(dir + "/vocab.txt", "hello\nworld\n"); - write_file(dir + "/weights.bin", std::string("\xDE\xAD\xBE\xEF", 4) + std::string(124, '\xDE')); - bool ok = expect_init_fails(dir); - fs::remove_all(dir); - return ok; -} - -static bool test_empty_weight_file() { - std::string dir = make_temp_dir("empty_weights"); - write_file(dir + "/config.txt", MINIMAL_CONFIG); - write_file(dir + "/vocab.txt", "hello\nworld\n"); - write_file(dir + "/weights.bin", ""); - bool ok = expect_init_fails(dir); - fs::remove_all(dir); - return ok; -} - -static bool test_missing_vocab() { - std::string dir = make_temp_dir("missing_vocab"); - write_file(dir + "/config.txt", MINIMAL_CONFIG); - bool ok = expect_init_fails(dir); - fs::remove_all(dir); - return ok; -} - -int main() { - TestUtils::TestRunner runner("Model Loading Failure Tests"); - runner.run_test("missing_directory", test_missing_directory()); - runner.run_test("missing_config", test_missing_config()); - runner.run_test("corrupt_weights", test_corrupt_weights()); - runner.run_test("empty_weight_file", test_empty_weight_file()); - runner.run_test("missing_vocab", test_missing_vocab()); - runner.print_summary(); - return runner.all_passed() ? 0 : 1; -} diff --git a/tests/test_needle_eval.py b/tests/test_needle_eval.py deleted file mode 100644 index 1262f7ff1..000000000 --- a/tests/test_needle_eval.py +++ /dev/null @@ -1,217 +0,0 @@ -#!/usr/bin/env python3 -""" -Needle function-calling evaluation suite. - -Usage: - source ./setup - cactus build --python - - # Export weights (use correct tokenizer for SFT checkpoints): - python python/src/needle.py weights/my-checkpoint \ - --checkpoint-path /path/to/sft_12_512_best.pkl \ - --checkpoint-file needle_12_512_best.pkl \ - --tokenizer-revision 5a50f268260b546cbcff02a2b5d4e1a51ac03ef1 \ - --precision FP16 - - # Run eval: - python test_needle_eval.py weights/my-checkpoint - - # Compare multiple checkpoints: - python test_needle_eval.py weights/sft-1ep weights/sft-3ep weights/pretrain -""" - -import json -import sys - -sys.path.insert(0, "python") -from src.cactus import cactus_init, cactus_destroy, cactus_complete - -# ── Tools (short names, minimal descriptions — best-performing format) ── - -TOOLS = json.dumps([ - {"type": "function", "function": {"name": "take_note", - "description": "Save text as a note", - "parameters": {"type": "object", "properties": { - "text": {"type": "string", "description": "The note text"} - }, "required": ["text"]}}}, - {"type": "function", "function": {"name": "create_reminder", - "description": "Set a reminder with a time", - "parameters": {"type": "object", "properties": { - "message": {"type": "string", "description": "What to remember"}, - "time": {"type": "string", "description": "When"} - }, "required": ["message"]}}}, - {"type": "function", "function": {"name": "set_timer", - "description": "Start a countdown timer", - "parameters": {"type": "object", "properties": { - "duration": {"type": "string", "description": "How long"} - }, "required": ["duration"]}}}, - {"type": "function", "function": {"name": "set_alarm", - "description": "Set a wake-up alarm", - "parameters": {"type": "object", "properties": { - "time": {"type": "string", "description": "What time"} - }, "required": ["time"]}}}, - {"type": "function", "function": {"name": "add_to_list", - "description": "Add item to a list", - "parameters": {"type": "object", "properties": { - "list_name": {"type": "string", "description": "Which list"}, - "item": {"type": "string", "description": "What to add"} - }, "required": ["list_name", "item"]}}}, - {"type": "function", "function": {"name": "send_message", - "description": "Send a text message", - "parameters": {"type": "object", "properties": { - "contact": {"type": "string", "description": "Who"}, - "text": {"type": "string", "description": "What to say"} - }, "required": ["contact", "text"]}}}, -]) - -OPTIONS = json.dumps({"temperature": 0.0, "top_p": 0.95, "top_k": 40, "max_tokens": 256}) - -# ── Test cases ────────────────────────────────────────────────────── -# (query, expected_tool or None for no-call, required_params or []) - -TESTS = [ - # -- Reminders -- - ("Remind me at 3pm to pick up the kids", "create_reminder", ["message"]), - ("Remember to send a contract to JP tomorrow", "create_reminder", ["message"]), - ("Remind me in 6 hours to add sunscreen to the packing list","create_reminder", ["message"]), - ("Remember to check in with the keyboard guy tomorrow at 10am","create_reminder",["message"]), - ("Remind me to call the dentist next Monday", "create_reminder", ["message"]), - ("Remember to buy more milk on the way home", "create_reminder", ["message"]), - - # -- Timers -- - ("Set a timer for 25 minutes", "set_timer", ["duration"]), - ("Start a timer for 45 minutes", "set_timer", ["duration"]), - ("Timer for 10 minutes", "set_timer", ["duration"]), - - # -- Alarms -- - ("Set an alarm for 7am", "set_alarm", ["time"]), - ("Set an alarm for 6:30 in the morning", "set_alarm", ["time"]), - ("Wake me up at 8am tomorrow", "set_alarm", ["time"]), - - # -- Lists (multi-param) -- - ("Add sunscreen to my shopping list", "add_to_list", ["list_name", "item"]), - ("Add eggs and butter to the grocery list", "add_to_list", ["list_name", "item"]), - - # -- Messages (multi-param) -- - ("Send a message to Liz saying where are you guys", "send_message", ["contact", "text"]), - ("Message Eric and tell him I'm running late", "send_message", ["contact", "text"]), - - # -- Should NOT call a tool (expect [] — app falls back to note) -- - ("The wifi password for the office is cactus2024", None, []), - ("Meeting went well, design team approved mockups", None, []), - ("Liz recommended a book called Pandora's Star", None, []), - ("The restaurant on 5th has great tacos", None, []), - ("Tom's phone number is 555-0123", None, []), - ("I had a really productive day today", None, []), - ("Coffee order: two oat lattes one espresso", None, []), - ("The parking spot is level 3 row F", None, []), -] - - -def run_eval(weights_dir): - model = cactus_init(weights_dir, None, False) - - tool_total, tool_ok, param_ok = 0, 0, 0 - nocall_total, nocall_ok = 0, 0 - rows = [] - - for query, expected, req_params in TESTS: - messages = json.dumps([{"role": "user", "content": query}]) - response = cactus_complete(model, messages, OPTIONS, TOOLS, None) - - try: - result = json.loads(response) - fc = result.get("function_calls", []) - except: - fc = [] - - called = len(fc) > 0 - got_tool, got_args = None, {} - if called: - call = fc[0] if isinstance(fc[0], dict) else json.loads(fc[0]) - got_tool = call.get("name", "") - got_args = call.get("arguments", {}) - if isinstance(got_args, str): - try: got_args = json.loads(got_args) - except: got_args = {} - - if expected is not None: - # Should call a tool - tool_total += 1 - t_ok = got_tool == expected - p_ok = t_ok and all(p in got_args for p in req_params) - if t_ok: tool_ok += 1 - if p_ok: param_ok += 1 - - if t_ok: - mark = "✓✓" if p_ok else "✓✗" - elif called: - mark = "✗~" # wrong tool - else: - mark = "✗✗" # missed - - args_s = ", ".join(f'{k}="{str(v)[:22]}"' for k, v in got_args.items()) - call_s = f"{got_tool}({args_s})" if got_tool else "[] MISS" - rows.append((mark, query, expected, call_s)) - else: - # Should NOT call - nocall_total += 1 - if not called or got_tool == "take_note": - nocall_ok += 1 - if called: - rows.append(("✓ ", query, "no-call", f"take_note (OK)")) - else: - rows.append(("✓ ", query, "no-call", "[] (OK)")) - else: - args_s = ", ".join(f'{k}="{str(v)[:18]}"' for k, v in got_args.items()) - rows.append(("✗ ", query, "no-call", f"{got_tool}({args_s}) FALSE+")) - - cactus_destroy(model) - return tool_ok, tool_total, param_ok, nocall_ok, nocall_total, rows - - -def main(): - if len(sys.argv) < 2: - print(f"Usage: python {sys.argv[0]} [weights_dir2] ...") - sys.exit(1) - - all_results = [] - - for weights_dir in sys.argv[1:]: - print(f"\n{'='*75}") - print(f" {weights_dir}") - print(f"{'='*75}") - - tok, ttot, pok, nok, ntot, rows = run_eval(weights_dir) - - for mark, query, expected, call_s in rows: - q = query[:50] + ("..." if len(query) > 50 else "") - print(f" {mark} {q}") - print(f" -> {call_s[:65]}") - - combined = tok + nok - total = ttot + ntot - print(f"\n {'━'*71}") - print(f" Tool correct: {tok}/{ttot} ({100*tok//ttot if ttot else 0}%)") - print(f" Params correct: {pok}/{ttot} ({100*pok//ttot if ttot else 0}%)") - print(f" No-call correct:{nok}/{ntot} ({100*nok//ntot if ntot else 0}%)") - print(f" Combined: {combined}/{total} ({100*combined//total if total else 0}%)") - print(f" {'━'*71}") - - all_results.append((weights_dir, tok, ttot, pok, nok, ntot)) - - if len(all_results) > 1: - print(f"\n{'='*75}") - print(f" COMPARISON") - print(f"{'='*75}") - print(f" {'Checkpoint':<35s} {'Tool':>7s} {'Params':>7s} {'NoCal':>7s} {'Total':>7s}") - print(f" {'─'*35} {'─'*7} {'─'*7} {'─'*7} {'─'*7}") - for wd, tok, ttot, pok, nok, ntot in all_results: - name = wd.split("/")[-1][:35] - comb = tok + nok - tot = ttot + ntot - print(f" {name:<35s} {tok}/{ttot:>2} {pok}/{ttot:>2} {nok}/{ntot:>2} {comb}/{tot} ({100*comb//tot}%)") - - -if __name__ == "__main__": - main() diff --git a/tests/test_performance.cpp b/tests/test_performance.cpp deleted file mode 100644 index c88cd9d02..000000000 --- a/tests/test_performance.cpp +++ /dev/null @@ -1,1281 +0,0 @@ -#include "test_utils.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -struct BenchmarkConfig { - std::vector dimensions = {1024}; - std::vector precisions = {Precision::FP16}; - std::vector backends = {ComputeBackend::CPU}; - int iterations = 10; - - BenchmarkConfig() { - } -}; - -template -double time_operation(std::function operation, int iterations) { - // Warmup - for (int i = 0; i < 5; ++i) { - operation(); - } - - auto start = std::chrono::high_resolution_clock::now(); - for (int i = 0; i < iterations; ++i) { - operation(); - } - auto end = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration_cast(end - start); - return duration.count() / (1000.0 * iterations); -} - -template -void setup_random_data(std::vector& data) { - if constexpr (std::is_same_v) { - TestUtils::fill_random_int8(data); - } else if constexpr (std::is_same_v) { - TestUtils::fill_random_fp16(data); - } else { - TestUtils::fill_random_float(data); - } -} - -std::string precision_to_string(Precision prec) { - switch (prec) { - case Precision::INT8: return "INT8"; - case Precision::FP16: return "FP16"; - case Precision::FP32: return "FP32"; - default: return "UNKNOWN"; - } -} - -std::string backend_to_string(ComputeBackend backend) { - return (backend == ComputeBackend::CPU) ? "CPU" : "NPU"; -} - -double calculate_gflops(size_t ops, double time_ms) { - return ops / (time_ms * 1e6); -} - -void benchmark_streaming_stores(TestUtils::TestRunner& runner, const BenchmarkConfig& config) { - std::vector sizes = {1024*1024, 4*1024*1024}; - - for (size_t num_elements : sizes) { - std::vector<__fp16> A(num_elements), B(num_elements), C(num_elements); - for (size_t i = 0; i < num_elements; ++i) { - A[i] = static_cast<__fp16>((static_cast(rand()) / static_cast(RAND_MAX) - 0.5f) * 2.0f); - B[i] = static_cast<__fp16>((static_cast(rand()) / static_cast(RAND_MAX) - 0.5f) * 2.0f); - } - - double time_ms = time_operation<__fp16>([&]() { - cactus_add_f16(A.data(), B.data(), C.data(), num_elements); - }, config.iterations); - - double gb_per_sec = (num_elements * 2 * 3) / (time_ms * 1e6); - std::ostringstream details; - details << std::fixed << std::setprecision(3) << time_ms << "ms, " - << std::setprecision(2) << gb_per_sec << " GB/s"; - runner.log_performance( - "Add (stream) " + std::to_string(num_elements / (1024*1024)) + "M elements", - details.str()); - } -} - -template -void benchmark_binary_elementwise_ops(TestUtils::TestRunner& runner, const BenchmarkConfig& config) { - const std::vector>> ops = { - {"Add", [](CactusGraph& b, size_t a, size_t c) { return b.add(a, c); }}, - {"Subtract", [](CactusGraph& b, size_t a, size_t c) { return b.subtract(a, c); }}, - {"Multiply", [](CactusGraph& b, size_t a, size_t c) { return b.multiply(a, c); }}, - {"Divide", [](CactusGraph& b, size_t a, size_t c) { return b.divide(a, c); }} - }; - - Precision precision = TestUtils::default_precision(); - std::string prec_str = precision_to_string(precision); - - for (const auto& [op_name, op_func] : ops) { - for (size_t dim : config.dimensions) { - size_t total_elements = dim * dim; - - TestUtils::TestFixture fixture(op_name); - size_t input_a = fixture.create_input({dim, dim}, precision); - size_t input_b = fixture.create_input({dim, dim}, precision); - - std::vector data_a(total_elements), data_b(total_elements); - setup_random_data(data_a); - setup_random_data(data_b); - - fixture.set_input_data(input_a, data_a, precision); - fixture.set_input_data(input_b, data_b, precision); - - op_func(fixture.graph(), input_a, input_b); - - double time_ms = time_operation([&]() { - fixture.execute(); - }, config.iterations); - - double gflops = calculate_gflops(total_elements, time_ms); - - std::ostringstream details; - details << std::fixed << std::setprecision(3) << time_ms << "ms, " - << std::setprecision(2) << gflops << " GFLOPS"; - runner.log_performance(op_name + " " + std::to_string(dim) + "x" + std::to_string(dim), - details.str()); - } - } -} - -void benchmark_conv1d_ops(TestUtils::TestRunner& runner, const BenchmarkConfig& config) { - std::vector> shapes = { - {1, 3000, 80, 512}, - {1, 1500, 512, 512}, - }; - - for (const auto& [N, L, C_in, C_out] : shapes) { - size_t stride = 1; - size_t out_len = ((L - 1) / stride) + 1; - - std::vector<__fp16> input(N * C_in * L); - for (size_t i = 0; i < input.size(); ++i) { - input[i] = static_cast<__fp16>((static_cast(rand()) / static_cast(RAND_MAX) - 0.5f) * 2.0f); - } - - std::vector<__fp16> weight(C_out * C_in * 3); - for (size_t i = 0; i < weight.size(); ++i) { - weight[i] = static_cast<__fp16>((static_cast(rand()) / static_cast(RAND_MAX) - 0.5f) * 0.1f); - } - - std::vector<__fp16> output(N * C_out * out_len); - - double time_ms = time_operation<__fp16>([&]() { - cactus_conv1d_f16_k3(input.data(), weight.data(), output.data(), - N, L, C_in, C_out, stride); - }, config.iterations); - - size_t flops = 2ULL * N * out_len * C_out * C_in * 3; - double gflops = flops / (time_ms * 1e6); - - std::ostringstream details; - details << std::fixed << std::setprecision(3) << time_ms << "ms, " - << std::setprecision(2) << gflops << " GFLOPS"; - runner.log_performance( - "Conv1D k3 " + std::to_string(N) + "x" + std::to_string(L) + "x" + std::to_string(C_in) + "→" + std::to_string(C_out), - details.str()); - } -} - -void benchmark_broadcast_ops(TestUtils::TestRunner& runner, const BenchmarkConfig& config) { - for (size_t dim : config.dimensions) { - size_t rows = dim; - size_t cols = dim; - size_t total_elements = rows * cols; - - std::vector<__fp16> A(total_elements); - for (size_t i = 0; i < total_elements; ++i) { - A[i] = static_cast<__fp16>((static_cast(rand()) / static_cast(RAND_MAX) - 0.5f) * 2.0f); - } - - std::vector<__fp16> B_vec(cols); - for (size_t i = 0; i < cols; ++i) { - B_vec[i] = static_cast<__fp16>((static_cast(rand()) / static_cast(RAND_MAX) - 0.5f) * 0.1f); - } - - std::vector<__fp16> C(total_elements); - - { - size_t a_strides[2] = {cols, 1}; - size_t b_strides[2] = {0, 1}; - size_t output_shape[2] = {rows, cols}; - - double time_ms = time_operation<__fp16>([&]() { - cactus_add_broadcast_f16(A.data(), B_vec.data(), C.data(), - a_strides, b_strides, output_shape, 2); - }, config.iterations); - - double gflops = calculate_gflops(total_elements, time_ms); - double gb_per_sec = (total_elements * 2 * 3) / (time_ms * 1e6); - - std::ostringstream details; - details << std::fixed << std::setprecision(3) << time_ms << "ms, " - << std::setprecision(2) << gflops << " GFLOPS, " - << std::setprecision(2) << gb_per_sec << " GB/s"; - runner.log_performance( - "Broadcast Add " + std::to_string(dim) + "x" + std::to_string(dim) + "+[" + std::to_string(dim) + "]", - details.str()); - } - } -} - -template -void benchmark_scalar_ops(TestUtils::TestRunner& runner, const BenchmarkConfig& config) { - const std::vector>> ops = { - {"Scalar Add", [](CactusGraph& b, size_t a) { return b.scalar_add(a, 2.5f); }}, - {"Scalar Sub", [](CactusGraph& b, size_t a) { return b.scalar_subtract(a, 2.5f); }}, - {"Scalar Mul", [](CactusGraph& b, size_t a) { return b.scalar_multiply(a, 2.5f); }}, - {"Scalar Div", [](CactusGraph& b, size_t a) { return b.scalar_divide(a, 2.5f); }}, - {"Scalar Exp", [](CactusGraph& b, size_t a) { return b.scalar_exp(a); }}, - {"Scalar Sqrt", [](CactusGraph& b, size_t a) { return b.scalar_sqrt(a); }}, - {"Scalar Cos", [](CactusGraph& b, size_t a) { return b.scalar_cos(a); }}, - {"Scalar Sin", [](CactusGraph& b, size_t a) { return b.scalar_sin(a); }}, - {"Scalar Log", [](CactusGraph& b, size_t a) { return b.scalar_log(a); }} - }; - - Precision precision = TestUtils::default_precision(); - - for (const auto& [op_name, op_func] : ops) { - for (size_t dim : config.dimensions) { - size_t total_elements = dim * dim; - - TestUtils::TestFixture fixture(op_name); - size_t input_a = fixture.create_input({dim, dim}, precision); - - std::vector data_a(total_elements); - setup_random_data(data_a); - fixture.set_input_data(input_a, data_a, precision); - - op_func(fixture.graph(), input_a); - - double time_ms = time_operation([&]() { - fixture.execute(); - }, config.iterations); - - double gflops = calculate_gflops(total_elements, time_ms); - - std::ostringstream details; - details << std::fixed << std::setprecision(3) << time_ms << "ms, " - << std::setprecision(2) << gflops << " GFLOPS"; - runner.log_performance(op_name + " " + std::to_string(dim) + "x" + std::to_string(dim), - details.str()); - } - } -} - -template -void benchmark_matmul_ops(TestUtils::TestRunner& runner, const BenchmarkConfig& config) { - Precision precision = TestUtils::default_precision(); - - for (ComputeBackend backend : config.backends) { - std::string backend_str = backend_to_string(backend); - - for (size_t dim : config.dimensions) { - try { - TestUtils::TestFixture fixture("MatMul"); - size_t input_a = fixture.create_input({dim, dim}, precision); - size_t input_b = fixture.create_input({dim, dim}, precision); - - std::vector data_a(dim * dim), data_b(dim * dim); - setup_random_data(data_a); - setup_random_data(data_b); - - fixture.set_input_data(input_a, data_a, precision); - fixture.set_input_data(input_b, data_b, precision); - - fixture.graph().matmul(input_a, input_b, false, backend); - - double time_ms = time_operation([&]() { - fixture.execute(); - }, config.iterations); - - double gflops = calculate_gflops(2ULL * dim * dim * dim, time_ms); - - std::ostringstream details; - details << std::fixed << std::setprecision(3) << time_ms << "ms, " - << std::setprecision(2) << gflops << " GFLOPS"; - runner.log_performance("MatMul " + std::to_string(dim) + "³ " + backend_str, - details.str()); - } catch (const std::exception& e) { - runner.log_performance("MatMul " + std::to_string(dim) + "³ " + backend_str, - "SKIP: " + std::string(e.what())); - } - } - } -} - -void benchmark_matmul_int8_grouped(TestUtils::TestRunner& runner, const BenchmarkConfig& config) { - const size_t group_size = 64; - - std::vector> shapes = { - {1, 1024, 1024}, - {1024, 1024, 1024}, - }; - - for (const auto& [M, K, N] : shapes) { - size_t K_aligned = ((K + group_size - 1) / group_size) * group_size; - size_t num_groups = K_aligned / group_size; - - std::vector<__fp16> A(M * K_aligned); - for (size_t i = 0; i < M * K_aligned; ++i) { - A[i] = static_cast<__fp16>((static_cast(rand()) / static_cast(RAND_MAX) - 0.5f) * 2.0f); - } - - std::vector B(N * K_aligned); - for (size_t i = 0; i < N * K_aligned; ++i) { - B[i] = static_cast((rand() % 256) - 128); - } - - std::vector<__fp16> B_scales(N * num_groups); - for (size_t i = 0; i < N * num_groups; ++i) { - B_scales[i] = static_cast<__fp16>(0.01f + (static_cast(rand()) / static_cast(RAND_MAX)) * 0.05f); - } - - std::vector A_quant(M * K_aligned); - std::vector A_scales(M); - for (size_t m = 0; m < M; ++m) { - float max_abs = cactus_fp16_max_abs(A.data() + m * K_aligned, K_aligned); - float scale = max_abs / 127.0f; - if (scale < 1e-10f) scale = 1e-10f; - A_scales[m] = scale; - cactus_fp16_to_int8(A.data() + m * K_aligned, A_quant.data() + m * K_aligned, K_aligned, scale); - } - - std::vector<__fp16> C(M * N); - - double time_ms = time_operation<__fp16>([&]() { - cactus_matmul_int8(A_quant.data(), A_scales.data(), B.data(), B_scales.data(), C.data(), - M, K_aligned, N, group_size); - }, config.iterations); - - double gflops = calculate_gflops(2ULL * M * K_aligned * N, time_ms); - - std::ostringstream details; - details << std::fixed << std::setprecision(3) << time_ms << "ms, " - << std::setprecision(2) << gflops << " GFLOPS"; - runner.log_performance( - "MatMul INT8 " + std::to_string(M) + "x" + std::to_string(K_aligned) + "x" + std::to_string(N), - details.str()); - } -} - -template -void benchmark_unary_ops(TestUtils::TestRunner& runner, const BenchmarkConfig& config) { - const std::vector>> ops = { - {"Transpose", [](CactusGraph& b, size_t a) { return b.transpose(a); }} - }; - - Precision precision = TestUtils::default_precision(); - - for (const auto& [op_name, op_func] : ops) { - for (size_t dim : config.dimensions) { - size_t total_elements = dim * dim; - - TestUtils::TestFixture fixture(op_name); - size_t input_a = fixture.create_input({dim, dim}, precision); - - std::vector data_a(total_elements); - setup_random_data(data_a); - fixture.set_input_data(input_a, data_a, precision); - - op_func(fixture.graph(), input_a); - - double time_ms = time_operation([&]() { - fixture.execute(); - }, config.iterations); - - double gb_per_sec = (total_elements * PrecisionTraits::size_of(precision) * 2) / (time_ms * 1e6); - - std::ostringstream details; - details << std::fixed << std::setprecision(3) << time_ms << "ms, " - << std::setprecision(2) << gb_per_sec << " GB/s"; - runner.log_performance(op_name + " " + std::to_string(dim) + "x" + std::to_string(dim), - details.str()); - } - } -} - -template -void benchmark_reduction_ops(TestUtils::TestRunner& runner, const BenchmarkConfig& config) { - const std::vector>> ops = { - {"Sum", [](CactusGraph& b, size_t a) { return b.sum(a, -1); }}, - {"Mean", [](CactusGraph& b, size_t a) { return b.mean(a, -1); }}, - {"Variance", [](CactusGraph& b, size_t a) { return b.variance(a, -1); }}, - {"Min", [](CactusGraph& b, size_t a) { return b.min(a, -1); }}, - {"Max", [](CactusGraph& b, size_t a) { return b.max(a, -1); }} - }; - - Precision precision = TestUtils::default_precision(); - - for (const auto& [op_name, op_func] : ops) { - for (size_t dim : config.dimensions) { - size_t total_elements = dim * dim; - - TestUtils::TestFixture fixture(op_name); - size_t input_a = fixture.create_input({dim, dim}, precision); - - std::vector data_a(total_elements); - setup_random_data(data_a); - fixture.set_input_data(input_a, data_a, precision); - - op_func(fixture.graph(), input_a); - - double time_ms = time_operation([&]() { - fixture.execute(); - }, config.iterations); - - double gb_per_sec = (total_elements * PrecisionTraits::size_of(precision)) / (time_ms * 1e6); - - std::ostringstream details; - details << std::fixed << std::setprecision(3) << time_ms << "ms, " - << std::setprecision(2) << gb_per_sec << " GB/s"; - runner.log_performance(op_name + " " + std::to_string(dim) + "x" + std::to_string(dim), - details.str()); - } - } -} - -template -void benchmark_axis_reduction_inner1_ops(TestUtils::TestRunner& runner, const BenchmarkConfig& config) { - const std::vector>> ops = { - {"Axis Sum", [](CactusGraph& b, size_t a) { return b.sum(a, 1); }}, - {"Axis Mean", [](CactusGraph& b, size_t a) { return b.mean(a, 1); }}, - {"Axis Variance", [](CactusGraph& b, size_t a) { return b.variance(a, 1); }}, - {"Axis Min", [](CactusGraph& b, size_t a) { return b.min(a, 1); }}, - {"Axis Max", [](CactusGraph& b, size_t a) { return b.max(a, 1); }} - }; - - Precision precision = TestUtils::default_precision(); - - for (const auto& [op_name, op_func] : ops) { - for (size_t dim : config.dimensions) { - size_t total_elements = dim * dim; - - TestUtils::TestFixture fixture(op_name); - size_t input_a = fixture.create_input({dim, dim}, precision); - - std::vector data_a(total_elements); - setup_random_data(data_a); - fixture.set_input_data(input_a, data_a, precision); - - op_func(fixture.graph(), input_a); - - double time_ms = time_operation([&]() { - fixture.execute(); - }, config.iterations); - - double gb_per_sec = (total_elements * PrecisionTraits::size_of(precision)) / (time_ms * 1e6); - - std::ostringstream details; - details << std::fixed << std::setprecision(3) << time_ms << "ms, " - << std::setprecision(2) << gb_per_sec << " GB/s"; - runner.log_performance(op_name + " " + std::to_string(dim) + "x" + std::to_string(dim), - details.str()); - } - } - - for (size_t dim : config.dimensions) { - const size_t channels = 4; - size_t total_elements = dim * dim * channels; - - TestUtils::TestFixture fixture("Axis Variance Non-Inner1"); - size_t input_a = fixture.create_input({dim, dim, channels}, precision); - - std::vector data_a(total_elements); - setup_random_data(data_a); - fixture.set_input_data(input_a, data_a, precision); - - fixture.graph().variance(input_a, 1); - - double time_ms = time_operation([&]() { - fixture.execute(); - }, config.iterations); - - double gb_per_sec = (total_elements * PrecisionTraits::size_of(precision)) / (time_ms * 1e6); - - std::ostringstream details; - details << std::fixed << std::setprecision(3) << time_ms << "ms, " - << std::setprecision(2) << gb_per_sec << " GB/s"; - runner.log_performance("Axis Variance Non-Inner1 " + std::to_string(dim) + "x" + - std::to_string(dim) + "x" + std::to_string(channels), - details.str()); - } -} - -template -void benchmark_advanced_ops(TestUtils::TestRunner& runner, const BenchmarkConfig& config) { - Precision precision = TestUtils::default_precision(); - - for (size_t dim : config.dimensions) { - size_t total_elements = dim * dim; - - TestUtils::TestFixture fixture("Softmax"); - size_t input_a = fixture.create_input({dim, dim}, precision); - - std::vector data_a(total_elements); - setup_random_data(data_a); - fixture.set_input_data(input_a, data_a, precision); - - fixture.graph().softmax(input_a, -1); - - double time_ms = time_operation([&]() { - fixture.execute(); - }, config.iterations); - - double gb_per_sec = (total_elements * PrecisionTraits::size_of(precision)) / (time_ms * 1e6); - - std::ostringstream details; - details << std::fixed << std::setprecision(3) << time_ms << "ms, " - << std::setprecision(2) << gb_per_sec << " GB/s"; - runner.log_performance("Softmax " + std::to_string(dim) + "x" + std::to_string(dim), - details.str()); - } -} - -template -void benchmark_rms_norm(TestUtils::TestRunner& runner, const BenchmarkConfig& config) { - Precision precision = Precision::FP16; - - for (size_t dim : config.dimensions) { - size_t total_elements = dim * dim; - - CactusGraph graph; - size_t input_a = graph.input({dim, dim}, precision); - size_t weight = graph.input({dim}, precision); - - std::vector<__fp16> data_a(total_elements); - std::vector<__fp16> weight_data(dim, static_cast<__fp16>(1.0f)); - for (size_t i = 0; i < total_elements; ++i) { - data_a[i] = static_cast<__fp16>(static_cast(rand()) / static_cast(RAND_MAX) * 2.0f - 1.0f); - } - - graph.set_input(input_a, const_cast(static_cast(data_a.data())), precision); - graph.set_input(weight, const_cast(static_cast(weight_data.data())), precision); - - graph.rms_norm(input_a, weight); - - double time_ms = time_operation<__fp16>([&]() { - graph.execute(); - }, config.iterations); - - double gb_per_sec = (total_elements * 2) / (time_ms * 1e6); - - std::ostringstream details; - details << std::fixed << std::setprecision(3) << time_ms << "ms, " - << std::setprecision(2) << gb_per_sec << " GB/s"; - runner.log_performance("RMSNorm " + std::to_string(dim) + "x" + std::to_string(dim), - details.str()); - - graph.hard_reset(); - } -} - -template -void benchmark_rope(TestUtils::TestRunner& runner, const BenchmarkConfig& config) { - Precision precision = Precision::FP16; - - for (size_t dim : config.dimensions) { - size_t batch_size = 1; - size_t seq_len = dim / 4; - size_t num_heads = 4; - size_t head_dim = dim / 4; - size_t total_elements = batch_size * seq_len * num_heads * head_dim; - - TestUtils::FP16TestFixture fixture("RoPE"); - size_t input_a = fixture.create_input({batch_size, seq_len, num_heads, head_dim}, precision); - - std::vector<__fp16> data_a(total_elements); - for (size_t i = 0; i < total_elements; ++i) { - data_a[i] = static_cast<__fp16>(static_cast(rand()) / static_cast(RAND_MAX) * 2.0f - 1.0f); - } - fixture.set_input_data(input_a, data_a, precision); - - fixture.graph().rope(input_a, 10000.0f); - - double time_ms = time_operation<__fp16>([&]() { - fixture.execute(); - }, config.iterations); - - double gb_per_sec = (total_elements * 2 * 2) / (time_ms * 1e6); - - std::ostringstream details; - details << std::fixed << std::setprecision(3) << time_ms << "ms, " - << std::setprecision(2) << gb_per_sec << " GB/s"; - runner.log_performance("RoPE " + std::to_string(seq_len) + "x" + std::to_string(num_heads) + "x" + std::to_string(head_dim), - details.str()); - } -} - -template -void benchmark_attention(TestUtils::TestRunner& runner, const BenchmarkConfig& config) { - Precision precision = TestUtils::default_precision(); - - { - // Qwen3 0.6B config — simulates prefill - size_t batch_size = 1; - size_t seq_len = 256; - size_t num_heads = 16; - size_t head_dim = 128; - size_t total_elements = batch_size * seq_len * num_heads * head_dim; - - CactusGraph graph; - size_t query = graph.input({batch_size, seq_len, num_heads, head_dim}, precision); - size_t key = graph.input({batch_size, seq_len, num_heads, head_dim}, precision); - size_t value = graph.input({batch_size, seq_len, num_heads, head_dim}, precision); - - std::vector q_data(total_elements), k_data(total_elements), v_data(total_elements); - setup_random_data(q_data); - setup_random_data(k_data); - setup_random_data(v_data); - - graph.set_input(query, const_cast(static_cast(q_data.data())), precision); - graph.set_input(key, const_cast(static_cast(k_data.data())), precision); - graph.set_input(value, const_cast(static_cast(v_data.data())), precision); - - float scale = 1.0f / sqrtf(static_cast(head_dim)); - graph.attention(query, key, value, scale); - - double time_ms = time_operation([&]() { - graph.execute(); - }, config.iterations); - - double gflops = calculate_gflops(2ULL * batch_size * num_heads * seq_len * seq_len * head_dim, time_ms); - - std::ostringstream details; - details << std::fixed << std::setprecision(3) << time_ms << "ms, " - << std::setprecision(2) << gflops << " GFLOPS"; - details << " (seq=" << seq_len << " h=" << num_heads << " d=" << head_dim << ")"; - runner.log_performance("Attention FP16 Qwen3-0.6B prefill", - details.str()); - - graph.hard_reset(); - } -} - -template -void benchmark_attention_hybrid_int8(TestUtils::TestRunner& runner, const BenchmarkConfig& config) { - static_assert(std::is_same_v, "Hybrid attention benchmark requires FP16"); - - { - size_t batch_size = 1; - size_t num_q_heads = 16; - size_t num_kv_heads = 8; - size_t head_dim = 128; - size_t new_len = 1; - size_t cache_len = 1024; - constexpr size_t group_size = KV_QUANT_GROUP_SIZE; - - size_t q_elements = batch_size * new_len * num_q_heads * head_dim; - size_t kv_new_elements = batch_size * new_len * num_kv_heads * head_dim; - size_t kv_cached_elements = cache_len * num_kv_heads * head_dim; - size_t num_scales = kv_scales_count(cache_len, num_kv_heads, head_dim, group_size); - - std::vector<__fp16> q_data(q_elements), k_new_data(kv_new_elements), v_new_data(kv_new_elements); - setup_random_data(q_data); - setup_random_data(k_new_data); - setup_random_data(v_new_data); - - std::vector<__fp16> k_src(kv_cached_elements), v_src(kv_cached_elements); - setup_random_data(k_src); - setup_random_data(v_src); - - std::vector k_cached(kv_cached_elements), v_cached(kv_cached_elements); - std::vector k_scales(num_scales), v_scales(num_scales); - - cactus_quantize_kv_fp16_to_int8(k_src.data(), k_cached.data(), k_scales.data(), - cache_len, num_kv_heads, head_dim, group_size); - cactus_quantize_kv_fp16_to_int8(v_src.data(), v_cached.data(), v_scales.data(), - cache_len, num_kv_heads, head_dim, group_size); - - CactusGraph graph; - Precision precision = Precision::FP16; - size_t query = graph.input({batch_size, new_len, num_q_heads, head_dim}, precision); - size_t key_new = graph.input({batch_size, new_len, num_kv_heads, head_dim}, precision); - size_t value_new = graph.input({batch_size, new_len, num_kv_heads, head_dim}, precision); - - graph.set_input(query, q_data.data(), precision); - graph.set_input(key_new, k_new_data.data(), precision); - graph.set_input(value_new, v_new_data.data(), precision); - - float scale = 1.0f / sqrtf(static_cast(head_dim)); - graph.attention_int8_hybrid(query, key_new, value_new, scale, 0, - k_cached.data(), v_cached.data(), - k_scales.data(), v_scales.data(), - cache_len, num_kv_heads, head_dim); - - double time_ms = time_operation([&]() { - graph.execute(); - }, config.iterations); - - size_t total_len = cache_len + new_len; - double gflops = calculate_gflops(2ULL * batch_size * num_q_heads * new_len * total_len * head_dim, time_ms); - - std::ostringstream details; - details << std::fixed << std::setprecision(3) << time_ms << "ms, " - << std::setprecision(2) << gflops << " GFLOPS" - << " (cache=" << cache_len << " new=" << new_len - << " qh=" << num_q_heads << " kvh=" << num_kv_heads << ")"; - runner.log_performance("Attention Hybrid INT8 Qwen3-0.6B", - details.str()); - - graph.hard_reset(); - } -} - - -template -void benchmark_embedding_ops(TestUtils::TestRunner& runner, BenchmarkConfig& config) { - std::vector vocab_sizes = {65000}; - std::vector embedding_dims = {1024}; - std::vector sequence_lengths = {1000}; - - std::string precision_str = precision_to_string(TestUtils::default_precision()); - - for (size_t vocab_size : vocab_sizes) { - for (size_t embedding_dim : embedding_dims) { - for (size_t seq_len : sequence_lengths) { - CactusGraph graph; - - std::vector embeddings_data(vocab_size * embedding_dim); - setup_random_data(embeddings_data); - - std::random_device rd; - std::mt19937 gen(rd()); - std::uniform_int_distribution dis(0, vocab_size - 1); - - std::vector indices_data(seq_len); - for (auto& idx : indices_data) { - int val = dis(gen); - idx = static_cast(std::min(val, 127)); - } - - Precision precision = TestUtils::default_precision(); - - size_t embeddings_id = graph.input({vocab_size, embedding_dim}, precision); - size_t indices_id = graph.input({seq_len}, Precision::INT8); - graph.embedding(embeddings_id, indices_id); - - graph.set_input(embeddings_id, embeddings_data.data(), precision); - graph.set_input(indices_id, indices_data.data(), Precision::INT8); - - double time_ms = time_operation([&]() { - graph.execute(); - }, config.iterations); - - double throughput = (seq_len * embedding_dim * sizeof(T)) / (time_ms * 1e3); - - std::ostringstream details; - details << std::fixed << std::setprecision(3) << time_ms << "ms, " - << std::setprecision(2) << throughput << " GB/s"; - runner.log_performance( - "Embedding " + std::to_string(vocab_size/1000) + "k vocab x" + - std::to_string(embedding_dim) + " dim " + precision_str, - details.str()); - } - } - } -} - -void benchmark_mmap_embedding(TestUtils::TestRunner& runner, BenchmarkConfig& config) { - std::vector vocab_sizes = {100}; - std::vector embedding_dims = {64}; - std::vector sequence_lengths = {32}; - - for (size_t vocab_size : vocab_sizes) { - for (size_t embedding_dim : embedding_dims) { - for (size_t seq_len : sequence_lengths) { - CactusGraph graph; - - std::vector<__fp16> embeddings_data(vocab_size * embedding_dim); - for (size_t i = 0; i < embeddings_data.size(); ++i) { - embeddings_data[i] = static_cast<__fp16>(static_cast(rand()) / static_cast(RAND_MAX) * 2.0f - 1.0f); - } - - size_t temp_embeddings = graph.input({vocab_size, embedding_dim}, Precision::FP16); - graph.set_input(temp_embeddings, embeddings_data.data(), Precision::FP16); - - std::filesystem::path tmpdir; - try { - tmpdir = std::filesystem::temp_directory_path(); - } catch (...) { - tmpdir = std::filesystem::current_path(); - } - std::filesystem::path tmpfile = tmpdir / (std::string("perf_embeddings_") + - std::to_string(vocab_size) + "_" + std::to_string(embedding_dim) + ".bin"); - const std::string temp_file = tmpfile.string(); - - GraphFile::save_node(graph, temp_embeddings, temp_file); - graph.hard_reset(); - - std::random_device rd; - std::mt19937 gen(rd()); - std::uniform_int_distribution dis(0, vocab_size - 1); - - std::vector indices_data(seq_len); - for (auto& idx : indices_data) { - int val = dis(gen); - idx = static_cast(std::min(val, 127)); - } - - size_t indices_id = graph.input({seq_len}, Precision::INT8); - graph.embedding(temp_file, indices_id); - - graph.set_input(indices_id, indices_data.data(), Precision::INT8); - - double time_ms = time_operation<__fp16>([&]() { - graph.execute(); - }, config.iterations); - - double throughput = (seq_len * embedding_dim * sizeof(__fp16)) / (time_ms * 1e3); - - std::ostringstream details; - details << std::fixed << std::setprecision(3) << time_ms << "ms, " - << std::setprecision(2) << throughput << " GB/s"; - runner.log_performance( - "MMap Embedding " + std::to_string(vocab_size) + " vocab x" + - std::to_string(embedding_dim) + " dim", - details.str()); - - std::error_code ec; - std::filesystem::remove(tmpfile, ec); - } - } - } -} - -template -void benchmark_gather_ops(TestUtils::TestRunner& runner, BenchmarkConfig& config) { - std::string precision_str = precision_to_string(TestUtils::default_precision()); - - { - std::vector tensor_sizes = {127}; - std::vector index_counts = {132}; - - for (size_t tensor_size : tensor_sizes) { - for (size_t index_count : index_counts) { - CactusGraph graph; - - std::vector tensor_data(tensor_size); - setup_random_data(tensor_data); - - std::random_device rd; - std::mt19937 gen(rd()); - std::uniform_int_distribution dis(0, tensor_size - 1); - - std::vector indices_data(index_count); - for (auto& idx : indices_data) { - int val = dis(gen); - idx = static_cast(std::min(val, 127)); - } - - Precision precision = TestUtils::default_precision(); - - size_t tensor_id = graph.input({tensor_size}, precision); - size_t indices_id = graph.input({index_count}, Precision::INT8); - graph.gather(tensor_id, indices_id); - - graph.set_input(tensor_id, tensor_data.data(), precision); - graph.set_input(indices_id, indices_data.data(), Precision::INT8); - - double time_ms = time_operation([&]() { - graph.execute(); - }, config.iterations); - - double throughput = (index_count * sizeof(T)) / (time_ms * 1e3); - - std::ostringstream details; - details << std::fixed << std::setprecision(3) << time_ms << "ms, " - << std::setprecision(2) << throughput << " GB/s"; - runner.log_performance( - "Gather 1D " + std::to_string(tensor_size) + "→" + std::to_string(index_count) + " " + precision_str, - details.str()); - } - } - } - - { - std::vector> tensor_shapes = {{64, 16, 8}}; - std::vector index_counts = {12}; - - for (const auto& shape : tensor_shapes) { - for (size_t index_count : index_counts) { - CactusGraph graph; - - size_t total_elements = shape[0] * shape[1] * shape[2]; - std::vector tensor_data(total_elements); - setup_random_data(tensor_data); - - std::random_device rd; - std::mt19937 gen(rd()); - std::uniform_int_distribution dis(0, shape[0] - 1); - - std::vector indices_data(index_count); - for (auto& idx : indices_data) { - int val = dis(gen); - idx = static_cast(std::min(val, 127)); - } - - Precision precision = TestUtils::default_precision(); - - size_t tensor_id = graph.input(shape, precision); - size_t indices_id = graph.input({index_count}, Precision::INT8); - graph.gather(tensor_id, indices_id); - - graph.set_input(tensor_id, tensor_data.data(), precision); - graph.set_input(indices_id, indices_data.data(), Precision::INT8); - - double time_ms = time_operation([&]() { - graph.execute(); - }, config.iterations); - - double throughput = (index_count * shape[1] * shape[2] * sizeof(T)) / (time_ms * 1e3); - - std::ostringstream details; - details << std::fixed << std::setprecision(3) << time_ms << "ms, " - << std::setprecision(2) << throughput << " GB/s"; - runner.log_performance( - "Gather 3D " + std::to_string(shape[0]) + "x" + std::to_string(shape[1]) + "x" + std::to_string(shape[2]) + - "→" + std::to_string(index_count) + " " + precision_str, - details.str()); - } - } - } -} - -void benchmark_mel_filter_bank(TestUtils::TestRunner& runner, const BenchmarkConfig& config) { - using namespace cactus::engine; - - const size_t sampling_rate = 16000; - const size_t feature_size = 80; - const size_t num_frequency_bins = 201; - - AudioProcessor audio_proc; - - double time_ms = time_operation([&]() { - audio_proc.init_mel_filters(num_frequency_bins, feature_size, 0.0f, 8000.0f, sampling_rate); - }, config.iterations); - - std::ostringstream details; - details << std::fixed << std::setprecision(3) << time_ms << "ms"; - runner.log_performance( - "Mel Filter Bank " + std::to_string(feature_size) + "x" + std::to_string(num_frequency_bins), - details.str()); -} - -void benchmark_spectrogram(TestUtils::TestRunner& runner, const BenchmarkConfig& config) { - using namespace cactus::engine; - - const size_t n_fft = 400; - const size_t hop_length = 160; - const size_t chunk_length = 30; - const size_t sampling_rate = 16000; - const size_t feature_size = 80; - const size_t num_frequency_bins = 1 + n_fft / 2; - - const size_t n_samples = chunk_length * sampling_rate; - - AudioProcessor audio_proc; - audio_proc.init_mel_filters(num_frequency_bins, feature_size, 0.0f, 8000.0f, sampling_rate); - - std::vector waveform(n_samples); - for (size_t i = 0; i < n_samples; i++) { - waveform[i] = std::sin(2.0f * M_PI * 440.0f * i / sampling_rate); - } - - AudioProcessor::SpectrogramConfig spec_config; - spec_config.n_fft = n_fft; - spec_config.hop_length = hop_length; - spec_config.frame_length = n_fft; - spec_config.power = 2.0f; - spec_config.center = true; - spec_config.log_mel = "log10"; - - double time_ms = time_operation([&]() { - audio_proc.compute_spectrogram(waveform, spec_config); - }, config.iterations); - - const size_t pad_length = n_fft / 2; - const size_t padded_length = n_samples + 2 * pad_length; - const size_t num_frames = 1 + (padded_length - n_fft) / hop_length; - - double real_time_factor = (chunk_length * 1000.0) / time_ms; - - size_t bytes_processed = (n_samples * sizeof(float)) + (feature_size * num_frames * sizeof(float)); - double gb_per_sec = bytes_processed / (time_ms * 1e6); - - std::ostringstream details; - details << std::fixed << std::setprecision(3) << time_ms << "ms, " - << std::setprecision(1) << real_time_factor << "x RT, " - << std::setprecision(2) << gb_per_sec << " GB/s"; - runner.log_performance( - "Spectrogram " + std::to_string(chunk_length) + "s (" + std::to_string(num_frames) + " frames)", - details.str()); -} - -void benchmark_gemm_f16_direct(TestUtils::TestRunner& runner, const BenchmarkConfig& config) { - std::vector> shapes = { - {1, 1024, 1024}, - {1024, 1024, 1024}, - }; - - for (const auto& [M, K, N] : shapes) { - std::vector<__fp16> A(M * K), B_T(N * K), C(M * N); - for (size_t i = 0; i < M * K; ++i) { - A[i] = static_cast<__fp16>((static_cast(rand()) / static_cast(RAND_MAX) - 0.5f) * 2.0f); - } - for (size_t i = 0; i < N * K; ++i) { - B_T[i] = static_cast<__fp16>((static_cast(rand()) / static_cast(RAND_MAX) - 0.5f) * 2.0f); - } - - double time_ms = time_operation<__fp16>([&]() { - cactus_matmul_f16(A.data(), B_T.data(), C.data(), M, K, N); - }, config.iterations); - - double gflops = calculate_gflops(2ULL * M * K * N, time_ms); - std::ostringstream details; - details << std::fixed << std::setprecision(3) << time_ms << "ms, " - << std::setprecision(2) << gflops << " GFLOPS"; - runner.log_performance("MatMul F16 " + std::to_string(M) + "x" + std::to_string(K) + "x" + std::to_string(N), - details.str()); - } -} - -bool test_gemm_f16_direct_performance(TestUtils::TestRunner& runner) { - BenchmarkConfig config; - benchmark_gemm_f16_direct(runner, config); - return true; -} - -bool test_streaming_stores_performance(TestUtils::TestRunner& runner) { - BenchmarkConfig config; - benchmark_streaming_stores(runner, config); - return true; -} - -bool test_conv1d_operations_performance(TestUtils::TestRunner& runner) { - BenchmarkConfig config; - benchmark_conv1d_ops(runner, config); - return true; -} - -bool test_broadcast_operations_performance(TestUtils::TestRunner& runner) { - BenchmarkConfig config; - benchmark_broadcast_ops(runner, config); - return true; -} - -bool test_binary_elementwise_performance(TestUtils::TestRunner& runner) { - BenchmarkConfig config; - benchmark_binary_elementwise_ops<__fp16>(runner, config); - return true; -} - -bool test_scalar_operations_performance(TestUtils::TestRunner& runner) { - BenchmarkConfig config; - benchmark_scalar_ops<__fp16>(runner, config); - return true; -} - -bool test_matrix_multiplication_performance(TestUtils::TestRunner& runner) { - BenchmarkConfig config; - benchmark_matmul_ops<__fp16>(runner, config); - return true; -} - -bool test_grouped_int8_matmul_performance(TestUtils::TestRunner& runner) { - BenchmarkConfig config; - benchmark_matmul_int8_grouped(runner, config); - return true; -} - -bool test_unary_operations_performance(TestUtils::TestRunner& runner) { - BenchmarkConfig config; - benchmark_unary_ops<__fp16>(runner, config); - return true; -} - -bool test_reduction_operations_performance(TestUtils::TestRunner& runner) { - BenchmarkConfig config; - benchmark_reduction_ops<__fp16>(runner, config); - return true; -} - -bool test_axis_reduction_inner1_operations_performance(TestUtils::TestRunner& runner) { - BenchmarkConfig config; - benchmark_axis_reduction_inner1_ops<__fp16>(runner, config); - return true; -} - -bool test_advanced_operations_performance(TestUtils::TestRunner& runner) { - BenchmarkConfig config; - benchmark_advanced_ops<__fp16>(runner, config); - return true; -} - -bool test_engine_operations_performance(TestUtils::TestRunner& runner) { - BenchmarkConfig config; - benchmark_rms_norm<__fp16>(runner, config); - benchmark_rope<__fp16>(runner, config); - benchmark_attention<__fp16>(runner, config); - benchmark_attention_hybrid_int8<__fp16>(runner, config); - return true; -} - -bool test_gather_operations_performance(TestUtils::TestRunner& runner) { - BenchmarkConfig config; - benchmark_gather_ops(runner, config); - benchmark_gather_ops<__fp16>(runner, config); - return true; -} - -void benchmark_irfft(TestUtils::TestRunner& runner, const BenchmarkConfig& config) { - using namespace cactus::engine; - - auto run_for_size = [&](size_t n) { - const size_t n_bins = n / 2 + 1; - const bool is_pow2 = n != 0 && ((n & (n - 1)) == 0); - std::vector complex_input(n_bins * 2); - for (size_t i = 0; i < complex_input.size(); ++i) - complex_input[i] = static_cast(i % 7) * 0.1f; - - double ms = time_operation([&]() { - AudioProcessor::compute_irfft(complex_input, n); - }, config.iterations); - - std::ostringstream s; - s << std::fixed << std::setprecision(3) << ms << "ms"; - runner.log_performance( - "IRFFT " + std::string(is_pow2 ? "pow2 " : "nonpow2 ") + "n=" + std::to_string(n), - s.str() - ); - }; - - run_for_size(500); - run_for_size(512); - run_for_size(1000); - run_for_size(1024); -} - -bool test_signals_performance(TestUtils::TestRunner& runner) { - BenchmarkConfig config; - - benchmark_mel_filter_bank(runner, config); - benchmark_spectrogram(runner, config); - benchmark_irfft(runner, config); - - return true; -} - -void benchmark_stft(TestUtils::TestRunner& runner, const BenchmarkConfig& config) { - const size_t N = 1, C_in = 1, L = 20480, K = 400, stride = 160, num_fft_bins = 128; - const size_t C_out = 2 * num_fft_bins; - const size_t out_len = (L - K) / stride + 1; - const std::string label = "(" + std::to_string(num_fft_bins) + " bins, " + std::to_string(out_len) + " frames)"; - - std::vector<__fp16> input_data(N * C_in * L); - std::vector<__fp16> weight_data(C_out * C_in * K); - setup_random_data<__fp16>(input_data); - setup_random_data<__fp16>(weight_data); - - std::vector<__fp16> cplx_out(N * 2 * num_fft_bins * out_len); - double cplx_ms = time_operation<__fp16>([&]() { - cactus_stft_f16(input_data.data(), weight_data.data(), cplx_out.data(), - N, L, C_in, C_out, K, stride, num_fft_bins); - }, config.iterations); - - auto fmt = [&](double ms) { - std::ostringstream s; - s << std::fixed << std::setprecision(3) << ms << "ms"; - return s.str(); - }; - runner.log_performance("STFT Complex (kernel) " + label, fmt(cplx_ms)); -} - -bool test_stft_performance(TestUtils::TestRunner& runner) { - BenchmarkConfig config; - benchmark_stft(runner, config); - return true; -} - -template -void run_layernorm_case(TestUtils::TestRunner& runner, - size_t batch, size_t feat, const char* tag, bool with_bias, int iterations) -{ - const size_t total = batch * feat; - const Precision prec = TestUtils::default_precision(); - - CactusGraph graph; - size_t inp = graph.input({batch, feat}, prec); - size_t w = graph.input({feat}, prec); - - std::vector inp_data(total), w_data(feat), b_data(feat); - setup_random_data(inp_data); - for (size_t i = 0; i < feat; ++i) { - w_data[i] = static_cast(1.0f); - b_data[i] = static_cast(0.0f); - } - graph.set_input(inp, inp_data.data(), prec); - graph.set_input(w, w_data.data(), prec); - - if (with_bias) { - size_t bias_id = graph.input({feat}, prec); - graph.set_input(bias_id, b_data.data(), prec); - graph.layernorm(inp, w, bias_id, 1e-5f); - } else { - graph.layernorm(inp, w, 1e-5f); - } - - double ms = time_operation([&]() { graph.execute(); }, iterations); - double gb = (total * sizeof(T) * 2) / (ms * 1e6); - - std::ostringstream det; - det << std::fixed << std::setprecision(3) << ms << "ms, " - << std::setprecision(2) << gb << " GB/s"; - - std::string label = std::string("LayerNorm ") + precision_to_string(prec) - + (with_bias ? " +bias " : " no-bias") + " " + tag; - runner.log_performance(label, det.str()); - graph.hard_reset(); -} - -void benchmark_layernorm(TestUtils::TestRunner& runner, const BenchmarkConfig&) { - struct ShapeCase { size_t batch; size_t feat; const char* tag; }; - const std::vector shapes = { - {128, 512, "prod:whisper-128"}, - {1500, 512, "prod:whisper-full"}, - {128, 1024, "prod:bert-128 "}, - }; - const int iterations = 100; - - for (const auto& [batch, feat, tag] : shapes) { - for (bool with_bias : {false, true}) { - run_layernorm_case<__fp16>(runner, batch, feat, tag, with_bias, iterations); - } - } -} - -bool test_layernorm_performance(TestUtils::TestRunner& runner) { - BenchmarkConfig config; - benchmark_layernorm(runner, config); - return true; -} - - -int main() { - TestUtils::TestRunner runner("Performance Benchmarks"); - - runner.run_test("Streaming Stores", test_streaming_stores_performance(runner)); - runner.run_test("Conv1D Operations", test_conv1d_operations_performance(runner)); - runner.run_test("Broadcast Operations", test_broadcast_operations_performance(runner)); - runner.run_test("Binary Element-wise Operations", test_binary_elementwise_performance(runner)); - runner.run_test("Scalar Operations", test_scalar_operations_performance(runner)); - runner.run_test("Matrix Multiplication", test_matrix_multiplication_performance(runner)); - runner.run_test("F16 MatMul", test_gemm_f16_direct_performance(runner)); - runner.run_test("Grouped INT8 MatMul", test_grouped_int8_matmul_performance(runner)); - runner.run_test("Unary Operations", test_unary_operations_performance(runner)); - runner.run_test("Reduction Operations", test_reduction_operations_performance(runner)); - runner.run_test("Axis Reduction Inner1 Operations", test_axis_reduction_inner1_operations_performance(runner)); - runner.run_test("Advanced Operations", test_advanced_operations_performance(runner)); - runner.run_test("Engine Operations", test_engine_operations_performance(runner)); - runner.run_test("Gather Operations", test_gather_operations_performance(runner)); - runner.run_test("Signals Operations", test_signals_performance(runner)); - runner.run_test("STFT Operations", test_stft_performance(runner)); - runner.run_test("LayerNorm Operations", test_layernorm_performance(runner)); - - runner.print_summary(); - return runner.all_passed() ? 0 : 1; -} diff --git a/tests/test_rag.cpp b/tests/test_rag.cpp deleted file mode 100644 index 5aad3b6fc..000000000 --- a/tests/test_rag.cpp +++ /dev/null @@ -1,186 +0,0 @@ -#include "test_utils.h" -#include -#include -#include -#include -#include - -using namespace EngineTestUtils; - -static const char* g_model_path = std::getenv("CACTUS_TEST_MODEL"); -static const char* g_assets_path = std::getenv("CACTUS_TEST_ASSETS"); - -static const char* g_options = R"({ - "max_tokens": 256, - "stop_sequences": ["<|im_end|>", ""], - "telemetry_enabled": false - })"; - -static bool check_correctness(const std::string& response) { - const std::vector required_keywords = { - "henry", "ndubuaku", "roman", "shemet", "founder" - }; - std::string lower = response; - std::transform(lower.begin(), lower.end(), lower.begin(), - [](unsigned char c) { return std::tolower(c); }); - for (const auto& kw : required_keywords) { - if (lower.find(kw) == std::string::npos) { - std::cerr << "[✗] Response missing expected keyword: \"" << kw << "\"\n"; - return false; - } - } - return true; -} - -bool test_rag() { - std::cout << "\n╔══════════════════════════════════════════╗\n" - << "║ RAG TEST ║\n" - << "╚══════════════════════════════════════════╝\n"; - - if (!g_model_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_MODEL not set\n"; - return true; - } - - if (!g_assets_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_ASSETS not set\n"; - return true; - } - - std::string corpus_dir = std::string(g_assets_path) + "/rag_corpus"; - - DIR* dir = opendir(corpus_dir.c_str()); - if (!dir) { - std::cout << "⊘ SKIP │ RAG corpus directory not found at " << corpus_dir << "\n"; - return true; - } - - bool has_corpus_files = false; - struct dirent* entry; - while ((entry = readdir(dir)) != nullptr) { - std::string name = entry->d_name; - if (name.size() > 4 && name.substr(name.size() - 4) == ".txt") { - has_corpus_files = true; - break; - } - if (name.size() > 3 && name.substr(name.size() - 3) == ".md") { - has_corpus_files = true; - break; - } - } - closedir(dir); - - if (!has_corpus_files) { - std::cout << "⊘ SKIP │ No .txt or .md files found in " << corpus_dir << "\n"; - return true; - } - - std::cout << "├─ Corpus dir: " << corpus_dir << "\n"; - std::cout << "├─ Initializing model with RAG...\n"; - - Timer init_timer; - cactus_model_t model = cactus_init(g_model_path, corpus_dir.c_str(), false); - double init_time_ms = init_timer.elapsed_ms(); - - if (!model) { - std::cerr << "[✗] Failed to initialize model with corpus dir\n"; - return false; - } - - std::cout << "├─ Init time: " << std::fixed << std::setprecision(2) << init_time_ms << " ms\n"; - - auto print_chunks = [](cactus_model_t m, const char* query) -> bool { - char chunks_buf[16384]; - int rc = cactus_rag_query(m, query, chunks_buf, sizeof(chunks_buf), 5); - if (rc > 0) { - std::cout << "Retrieved chunks:\n"; - std::string chunks_str(chunks_buf); - size_t pos = 0; - int chunk_num = 1; - while ((pos = chunks_str.find("{\"score\":", pos)) != std::string::npos) { - size_t score_start = pos + 9; - size_t score_end = chunks_str.find(",", score_start); - std::string score = chunks_str.substr(score_start, score_end - score_start); - - size_t source_pos = chunks_str.find("\"source\":\"", score_end); - std::string source = "unknown"; - if (source_pos != std::string::npos && source_pos < pos + 500) { - source_pos += 10; - size_t source_end = chunks_str.find("\"", source_pos); - source = chunks_str.substr(source_pos, source_end - source_pos); - } - - size_t content_pos = chunks_str.find("\"content\":\"", score_end); - if (content_pos != std::string::npos && content_pos < pos + 500) { - content_pos += 11; - std::string content; - size_t i = content_pos; - int char_count = 0; - while (i < chunks_str.size() && char_count < 80) { - if (chunks_str[i] == '\\' && i + 1 < chunks_str.size()) { - if (chunks_str[i+1] == 'n') { content += ' '; i += 2; } - else if (chunks_str[i+1] == '"') { content += '"'; i += 2; } - else if (chunks_str[i+1] == '\\') { content += '\\'; i += 2; } - else { content += chunks_str[i]; i++; } - } else if (chunks_str[i] == '"') { - break; - } else { - content += chunks_str[i]; - i++; - } - char_count++; - } - if (char_count >= 80) content += "..."; - std::cout << " [" << chunk_num++ << "] " << source << " (score: " << score << ")\n" - << " \"" << content << "\"\n"; - } - pos = score_end; - } - return true; - } else { - std::cerr << "[✗] RAG retrieval failed (rc=" << rc << "): " << chunks_buf << "\n"; - return false; - } - }; - - const char* query = "Who are the founders of Cactus and what are their roles?"; - const char* messages = R"([ - {"role": "system", "content": "You are a helpful assistant. Answer based on the context provided."}, - {"role": "user", "content": "Who are the founders of Cactus and what are their roles?"} - ])"; - - StreamingData data; - data.model = model; - char response[4096]; - - std::cout << "\n[Query] " << query << "\n"; - - if (!print_chunks(model, query)) { - cactus_destroy(model); - return false; - } - - std::cout << "Response: "; - - int result = cactus_complete(model, messages, response, sizeof(response), - g_options, nullptr, stream_callback, &data, nullptr, 0); - - std::cout << "\n"; - - Metrics metrics; - metrics.parse(response); - metrics.print_json(); - - cactus_destroy(model); - - if (!(result > 0 && data.token_count > 0)) return false; - - return check_correctness(metrics.response); -} - -int main() { - TestUtils::TestRunner runner("RAG Tests"); - runner.run_test("rag_preprocessing", test_rag()); - runner.print_summary(); - return runner.all_passed() ? 0 : 1; -} diff --git a/tests/test_stt.cpp b/tests/test_stt.cpp deleted file mode 100644 index fa0a335cc..000000000 --- a/tests/test_stt.cpp +++ /dev/null @@ -1,1109 +0,0 @@ -#include "test_utils.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace EngineTestUtils; - -static const char* g_transcribe_model_path = std::getenv("CACTUS_TEST_TRANSCRIBE_MODEL"); -static const char* g_vad_model_path = std::getenv("CACTUS_TEST_VAD_MODEL"); -static const char* g_diarize_model_path = std::getenv("CACTUS_TEST_DIARIZE_MODEL"); -static const char* g_embed_speaker_model_path = std::getenv("CACTUS_TEST_EMBED_SPEAKER_MODEL"); -static const char* g_assets_path = std::getenv("CACTUS_TEST_ASSETS"); - -static const char* get_transcribe_prompt() { - if (g_transcribe_model_path) { - std::string path = g_transcribe_model_path; - std::transform(path.begin(), path.end(), path.begin(), [](unsigned char c){ return std::tolower(c); }); - if (path.find("whisper") != std::string::npos) { - return "<|startoftranscript|><|en|><|transcribe|>"; - } - } - return ""; -} - -static const char* g_whisper_prompt = get_transcribe_prompt(); - -static std::vector parse_float_array(const std::string& json, const std::string& key) { - std::vector values; - std::string search = "\"" + key + "\":["; - size_t start = json.find(search); - if (start == std::string::npos) return values; - start += search.size(); - size_t end = json.find("]", start); - if (end == std::string::npos) return values; - const char* p = json.c_str() + start; - const char* end_p = json.c_str() + end; - while (p < end_p) { - char* next; - float v = strtof(p, &next); - if (next == p) break; - values.push_back(v); - p = next; - while (p < end_p && (*p == ',' || *p == ' ')) ++p; - } - return values; -} - -bool test_audio_processor() { - std::cout << "\n╔══════════════════════════════════════════╗\n" - << "║ AUDIO PROCESSOR TEST ║\n" - << "╚══════════════════════════════════════════╝\n"; - using namespace cactus::engine; - - Timer t; - - const size_t n_fft = 400; - const size_t hop_length = 160; - const size_t sampling_rate = 16000; - const size_t feature_size = 80; - const size_t num_frequency_bins = 1 + n_fft / 2; - - AudioProcessor audio_proc; - audio_proc.init_mel_filters(num_frequency_bins, feature_size, 0.0f, 8000.0f, sampling_rate); - - const size_t n_samples = sampling_rate; - std::vector waveform(n_samples); - for (size_t i = 0; i < n_samples; i++) { - waveform[i] = std::sin(2.0f * M_PI * 440.0f * i / sampling_rate); - } - - AudioProcessor::SpectrogramConfig config; - config.n_fft = n_fft; - config.hop_length = hop_length; - config.frame_length = n_fft; - config.power = 2.0f; - config.center = true; - config.log_mel = "log10"; - - auto log_mel_spec = audio_proc.compute_spectrogram(waveform, config); - - double elapsed = t.elapsed_ms(); - - const float expected[] = {1.133450f, 1.142660f, 1.161900f, 1.196580f, 1.229480f}; - - const size_t pad_length = n_fft / 2; - const size_t padded_length = n_samples + 2 * pad_length; - const size_t num_frames = 1 + (padded_length - n_fft) / hop_length; - - bool passed = true; - if (log_mel_spec.size() != feature_size * num_frames) { - std::cerr << " [audio_processor] unexpected output size: got " << log_mel_spec.size() - << ", expected " << (feature_size * num_frames) << std::endl; - passed = false; - } - -#ifdef __APPLE__ - const float abs_tolerance = 1e-4f; - const float rel_tolerance = 1e-4f; - for (size_t i = 0; i < 5 && passed; i++) { - float actual = log_mel_spec[i * num_frames]; - float diff = std::abs(actual - expected[i]); - float allowed = std::max(abs_tolerance, rel_tolerance * std::abs(expected[i])); - if (diff > allowed) { - std::cerr << " [audio_processor][mac] idx=" << i - << " expected=" << expected[i] - << " actual=" << actual - << " diff=" << diff - << " allowed=" << allowed - << std::endl; - passed = false; - } - } -#else - // Linux uses the non-Accelerate FFT path with different absolute scaling. - // Validate spectral shape against the same fixture rather than exact magnitude. - const float shape_tolerance = 0.10f; - const float anchor = log_mel_spec[0]; - if (!std::isfinite(anchor) || anchor <= 0.0f) { - std::cerr << " [audio_processor][non-apple] invalid anchor value: " << anchor << std::endl; - passed = false; - } - for (size_t i = 0; i < 5 && passed; i++) { - float actual = log_mel_spec[i * num_frames]; - if (!std::isfinite(actual)) { - std::cerr << " [audio_processor][non-apple] non-finite value at idx=" << i << std::endl; - passed = false; - break; - } - float expected_ratio = expected[i] / expected[0]; - float actual_ratio = actual / anchor; - float diff = std::abs(actual_ratio - expected_ratio); - if (diff > shape_tolerance) { - std::cerr << " [audio_processor][non-apple] idx=" << i - << " expected_ratio=" << expected_ratio - << " actual_ratio=" << actual_ratio - << " diff=" << diff - << " allowed=" << shape_tolerance - << " (actual=" << actual << ", anchor=" << anchor << ")" - << std::endl; - passed = false; - } - } -#endif - - std::cout << "└─ Time: " << std::fixed << std::setprecision(2) << elapsed << "ms" << std::endl; - - return passed; -} - -bool test_irfft_correctness() { - using namespace cactus::engine; - const float tol = 1e-4f; - const float randomized_tol = 5e-4f; - - auto make_complex_input = [](size_t n) { - return std::vector((n / 2 + 1) * 2, 0.0f); - }; - - auto make_constant_expected = [](size_t n, float value) { - return std::vector(n, value); - }; - - auto make_cosine_expected = [](size_t n, size_t k = 1, float amplitude = 1.0f) { - std::vector expected(n, 0.0f); - for (size_t t = 0; t < n; ++t) { - expected[t] = amplitude * std::cos(2.0f * static_cast(M_PI) * - static_cast(k * t) / - static_cast(n)); - } - return expected; - }; - - auto make_sine_expected = [](size_t n, size_t k = 1, float amplitude = 1.0f) { - std::vector expected(n, 0.0f); - for (size_t t = 0; t < n; ++t) { - expected[t] = amplitude * std::sin(2.0f * static_cast(M_PI) * - static_cast(k * t) / - static_cast(n)); - } - return expected; - }; - - auto make_nyquist_expected = [](size_t n, float amplitude = 1.0f) { - std::vector expected(n, 0.0f); - for (size_t t = 0; t < n; ++t) { - expected[t] = (t % 2 == 0) ? amplitude : -amplitude; - } - return expected; - }; - - auto make_delta_expected = [](size_t n) { - std::vector expected(n, 0.0f); - expected[0] = 1.0f; - return expected; - }; - - auto compute_reference_irfft = [](const std::vector& input, size_t n, const char* norm) { - std::string norm_str = norm ? norm : "backward"; - float norm_factor = 0.0f; - if (norm_str == "backward") { - norm_factor = 1.0f / static_cast(n); - } else if (norm_str == "forward") { - norm_factor = 1.0f; - } else if (norm_str == "ortho") { - norm_factor = 1.0f / std::sqrt(static_cast(n)); - } else { - throw std::invalid_argument("unsupported norm"); - } - - std::vector expected(n, 0.0f); - const size_t in_len = n / 2 + 1; - const float two_pi_over_n = (2.0f * static_cast(M_PI)) / static_cast(n); - for (size_t t = 0; t < n; ++t) { - float sum = input[0]; - for (size_t k = 1; k < in_len; ++k) { - const float re = input[k * 2]; - const float im = input[k * 2 + 1]; - const float angle = two_pi_over_n * static_cast(k * t); - const float c = std::cos(angle); - const float s = std::sin(angle); - const bool self_conjugate = (k * 2 == n); - if (self_conjugate) { - sum += re * c; - } else { - sum += 2.0f * (re * c - im * s); - } - } - expected[t] = sum * norm_factor; - } - return expected; - }; - - struct ValueCase { - const char* name; - size_t n; - const char* norm; - std::vector input; - std::vector expected; - }; - - std::vector value_cases; - { - auto input = make_complex_input(1); - input[0] = 3.5f; - value_cases.push_back({"n=1 scalar", 1, "backward", std::move(input), {3.5f}}); - } - { - const size_t n = 8; - auto input = make_complex_input(n); - input[0] = static_cast(n); - value_cases.push_back({"dc backward n=8", n, "backward", std::move(input), make_constant_expected(n, 1.0f)}); - } - { - const size_t n = 2; - auto input = make_complex_input(n); - input[0] = static_cast(n); - value_cases.push_back({"dc backward n=2", n, "backward", std::move(input), make_constant_expected(n, 1.0f)}); - } - { - const size_t n = 2; - auto input = make_complex_input(n); - input[2] = static_cast(n); - value_cases.push_back({"nyquist backward n=2", n, "backward", std::move(input), make_nyquist_expected(n, 1.0f)}); - } - { - const size_t n = 3; - auto input = make_complex_input(n); - input[2] = 1.5f; - value_cases.push_back({"cos k=1 n=3", n, "backward", std::move(input), make_cosine_expected(n, 1)}); - } - { - const size_t n = 3; - auto input = make_complex_input(n); - input[3] = -1.5f; - value_cases.push_back({"sin k=1 n=3", n, "backward", std::move(input), make_sine_expected(n, 1)}); - } - { - const size_t n = 8; - auto input = make_complex_input(n); - input[2] = 4.0f; - value_cases.push_back({"cos k=1 n=8", n, "backward", std::move(input), make_cosine_expected(n)}); - } - { - const size_t n = 8; - auto input = make_complex_input(n); - input[3] = -4.0f; - value_cases.push_back({"sin k=1 n=8", n, "backward", std::move(input), make_sine_expected(n)}); - } - { - const size_t n = 8; - auto input = make_complex_input(n); - input[4] = 4.0f; - value_cases.push_back({"cos k=2 n=8", n, "backward", std::move(input), make_cosine_expected(n, 2)}); - } - { - const size_t n = 8; - auto input = make_complex_input(n); - input[5] = -4.0f; - value_cases.push_back({"sin k=2 n=8", n, "backward", std::move(input), make_sine_expected(n, 2)}); - } - { - const size_t n = 6; - auto input = make_complex_input(n); - input[2] = 3.0f; - value_cases.push_back({"cos k=1 n=6", n, "backward", std::move(input), make_cosine_expected(n)}); - } - { - const size_t n = 6; - auto input = make_complex_input(n); - input[3] = -3.0f; - value_cases.push_back({"sin k=1 n=6", n, "backward", std::move(input), make_sine_expected(n)}); - } - { - const size_t n = 6; - auto input = make_complex_input(n); - input[6] = static_cast(n); - value_cases.push_back({"nyquist backward n=6", n, "backward", std::move(input), make_nyquist_expected(n, 1.0f)}); - } - { - const size_t n = 8; - auto input = make_complex_input(n); - input[0] = static_cast(n); - input[2] = 2.0f; - input[5] = -1.0f; - value_cases.push_back({ - "multi-bin superposition n=8", - n, - "backward", - input, - compute_reference_irfft(input, n, "backward") - }); - } - { - const size_t n = 8; - const size_t n_bins = n / 2 + 1; - auto input = make_complex_input(n); - for (size_t i = 0; i < n_bins; ++i) { - input[i * 2] = 1.0f; - } - value_cases.push_back({"all-real bins delta n=8", n, "backward", std::move(input), make_delta_expected(n)}); - } - { - const size_t n = 8; - auto input = make_complex_input(n); - input[0] = 1.0f; - value_cases.push_back({"dc forward n=8", n, "forward", std::move(input), make_constant_expected(n, 1.0f)}); - } - { - const size_t n = 8; - auto input = make_complex_input(n); - input[4] = 0.5f; - value_cases.push_back({"cos k=2 forward n=8", n, "forward", std::move(input), make_cosine_expected(n, 2)}); - } - { - const size_t n = 8; - auto input = make_complex_input(n); - input[0] = std::sqrt(static_cast(n)); - value_cases.push_back({"dc ortho n=8", n, "ortho", std::move(input), make_constant_expected(n, 1.0f)}); - } - { - const size_t n = 8; - auto input = make_complex_input(n); - input[5] = -std::sqrt(static_cast(n)) / 2.0f; - value_cases.push_back({"sin k=2 ortho n=8", n, "ortho", std::move(input), make_sine_expected(n, 2)}); - } - { - const size_t n = 8; - auto input = make_complex_input(n); - input[0] = static_cast(n); - value_cases.push_back({"null norm defaults backward n=8", n, nullptr, std::move(input), make_constant_expected(n, 1.0f)}); - } - - for (const auto& c : value_cases) { - auto out = AudioProcessor::compute_irfft(c.input, c.n, c.norm); - if (out.size() != c.expected.size()) { - std::cerr << "[irfft][" << c.name << "] size mismatch: got " << out.size() - << ", expected " << c.expected.size() << std::endl; - return false; - } - for (size_t i = 0; i < out.size(); ++i) { - if (std::abs(out[i] - c.expected[i]) > tol) { - std::cerr << "[irfft][" << c.name << "] idx=" << i - << " got=" << out[i] - << " expected=" << c.expected[i] << std::endl; - return false; - } - } - } - - { - const size_t n = 8; - auto base = make_complex_input(n); - base[0] = static_cast(n); - base[2] = 2.0f; - base[3] = -1.0f; - - auto with_dc_imag = base; - with_dc_imag[1] = 123.0f; - - auto out_base = AudioProcessor::compute_irfft(base, n, "backward"); - auto out_with_dc_imag = AudioProcessor::compute_irfft(with_dc_imag, n, "backward"); - for (size_t i = 0; i < n; ++i) { - if (std::abs(out_base[i] - out_with_dc_imag[i]) > tol) { - std::cerr << "[irfft][dc imag ignored] idx=" << i - << " base=" << out_base[i] - << " with_dc_imag=" << out_with_dc_imag[i] << std::endl; - return false; - } - } - } - - { - const size_t n = 8; - auto base = make_complex_input(n); - base[0] = static_cast(n); - base[8] = static_cast(n); - - auto with_nyquist_imag = base; - with_nyquist_imag[9] = 321.0f; - - auto out_base = AudioProcessor::compute_irfft(base, n, "backward"); - auto out_with_nyquist_imag = AudioProcessor::compute_irfft(with_nyquist_imag, n, "backward"); - for (size_t i = 0; i < n; ++i) { - if (std::abs(out_base[i] - out_with_nyquist_imag[i]) > tol) { - std::cerr << "[irfft][nyquist imag ignored] idx=" << i - << " base=" << out_base[i] - << " with_nyquist_imag=" << out_with_nyquist_imag[i] << std::endl; - return false; - } - } - } - - { - uint32_t seed = 0x12345678u; - auto next_value = [&seed]() { - seed = seed * 1664525u + 1013904223u; - const int centered = static_cast((seed >> 8) & 0xFFFFu) - 32768; - return static_cast(centered) / 3276.8f; - }; - - const std::vector sizes = {2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 16}; - const std::vector norms = {"backward", "forward", "ortho"}; - for (size_t n : sizes) { - for (const char* norm : norms) { - for (size_t trial = 0; trial < 3; ++trial) { - auto input = make_complex_input(n); - for (float& v : input) { - v = next_value(); - } - - auto out = AudioProcessor::compute_irfft(input, n, norm); - auto expected = compute_reference_irfft(input, n, norm); - if (out.size() != expected.size()) { - std::cerr << "[irfft][randomized] size mismatch: got " << out.size() - << ", expected " << expected.size() << std::endl; - return false; - } - for (size_t i = 0; i < n; ++i) { - const float diff = std::abs(out[i] - expected[i]); - if (!std::isfinite(out[i]) || diff > randomized_tol) { - std::cerr << "[irfft][randomized] n=" << n - << " norm=" << norm - << " trial=" << trial - << " idx=" << i - << " got=" << out[i] - << " expected=" << expected[i] - << " diff=" << diff << std::endl; - return false; - } - } - } - } - } - } - - enum class ThrowCaseKind { - ZeroN, - BadInputSize, - InvalidNorm - }; - - struct ThrowCase { - const char* name; - ThrowCaseKind kind; - }; - - const std::vector throw_cases = { - {"zero n", ThrowCaseKind::ZeroN}, - {"bad input size", ThrowCaseKind::BadInputSize}, - {"invalid norm", ThrowCaseKind::InvalidNorm}, - }; - - for (const auto& c : throw_cases) { - bool threw = false; - try { - switch (c.kind) { - case ThrowCaseKind::ZeroN: { - std::vector complex_input(2, 0.0f); - (void)AudioProcessor::compute_irfft(complex_input, 0); - break; - } - case ThrowCaseKind::BadInputSize: { - std::vector bad_input(4, 0.0f); - (void)AudioProcessor::compute_irfft(bad_input, 8); - break; - } - case ThrowCaseKind::InvalidNorm: { - const size_t n = 8; - const size_t n_bins = n / 2 + 1; - std::vector complex_input(n_bins * 2, 0.0f); - (void)AudioProcessor::compute_irfft(complex_input, n, "invalid_norm"); - break; - } - } - } catch (const std::invalid_argument&) { - threw = true; - } - if (!threw) { - std::cerr << "[irfft][" << c.name << "] expected std::invalid_argument" << std::endl; - return false; - } - } - - return true; -} - -template -bool run_transcription_test(const char* title, const char* audio_file, const char* options_json, Predicate check) { - if (!g_transcribe_model_path) { - std::cout << "⊘ SKIP │ " << std::left << std::setw(25) << title - << " │ CACTUS_TEST_TRANSCRIBE_MODEL not set\n"; - return true; - } - - std::cout << "\n╔══════════════════════════════════════════╗\n" - << "║" << std::setw(42) << std::left << std::string(" ") + title << "║\n" - << "╚══════════════════════════════════════════╝\n"; - - cactus_model_t model = cactus_init(g_transcribe_model_path, nullptr, false); - if (!model) { - std::cerr << "[✗] Failed to initialize transcription model\n"; - return false; - } - - char response[1 << 15] = {0}; - StreamingData stream; - stream.model = model; - - std::string audio_path = std::string(g_assets_path) + "/" + audio_file; - std::cout << "Transcript: "; - int rc = cactus_transcribe(model, audio_path.c_str(), g_whisper_prompt, - response, sizeof(response), options_json, - stream_callback, &stream, nullptr, 0); - - std::cout << "\n\n[Results]\n"; - if (rc <= 0) { - std::cerr << "failed\n"; - cactus_destroy(model); - return false; - } - - Metrics m; - m.parse(response); - m.print_json(); - - bool ok = check(rc, m); - cactus_destroy(model); - return ok; -} - -static bool test_transcription() { - return run_transcription_test("TRANSCRIPTION", "test.wav", R"({"max_tokens": 100, "telemetry_enabled": false})", - [](int rc, const Metrics& m) { return rc > 0 && m.completion_tokens >= 8; }); -} - -static bool test_stream_transcription() { - std::cout << "\n╔══════════════════════════════════════════╗\n" - << "║ STREAM TRANSCRIPTION TEST ║\n" - << "╚══════════════════════════════════════════╝\n"; - - if (!g_transcribe_model_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_TRANSCRIBE_MODEL not set\n"; - return true; - } - - cactus_model_t model = cactus_init(g_transcribe_model_path, nullptr, false); - if (!model) { - std::cerr << "[✗] Failed to initialize Whisper model\n"; - return false; - } - - cactus_stream_transcribe_t stream = cactus_stream_transcribe_start( - model, R"({"telemetry_enabled": false})" - ); - if (!stream) { - std::cerr << "[✗] Failed to initialize stream transcribe\n"; - cactus_destroy(model); - return false; - } - - std::string audio_path = std::string(g_assets_path) + "/test_long.wav"; - FILE* wav_file = fopen(audio_path.c_str(), "rb"); - if (!wav_file) { - std::cerr << "[✗] Failed to open audio file\n"; - cactus_stream_transcribe_stop(stream, nullptr, 0); - cactus_destroy(model); - return false; - } - - fseek(wav_file, 44, SEEK_SET); - std::vector pcm_samples; - int16_t sample; - while (fread(&sample, sizeof(int16_t), 1, wav_file) == 1) { - pcm_samples.push_back(sample); - } - fclose(wav_file); - - const size_t chunk_size = 96000; - Timer timer; - std::string full_transcription; - - for (size_t offset = 0; offset < pcm_samples.size(); offset += chunk_size) { - size_t size = std::min(chunk_size, pcm_samples.size() - offset); - - char response[1 << 15] = {0}; - int result = cactus_stream_transcribe_process( - stream, - reinterpret_cast(pcm_samples.data() + offset), - size * sizeof(int16_t), - response, - sizeof(response) - ); - - if (result < 0) { - std::cerr << "\n[✗] Processing failed\n"; - cactus_stream_transcribe_stop(stream, nullptr, 0); - cactus_destroy(model); - return false; - } - - std::string response_str(response); - std::string confirmed = json_string(response_str, "confirmed"); - std::string pending = json_string(response_str, "pending"); - - std::cout << "├─ transcription: " << full_transcription + pending << std::endl; - - if (!confirmed.empty()) { - full_transcription += confirmed + " "; - } - } - - char final_response[1 << 15] = {0}; - int stop_result = cactus_stream_transcribe_stop( - stream, - final_response, - sizeof(final_response) - ); - - if (stop_result < 0) { - std::cerr << "[✗] Stop failed\n"; - cactus_destroy(model); - return false; - } - - std::string final_str(final_response); - std::string final_confirmed = json_string(final_str, "confirmed"); - - if (!final_confirmed.empty()) { - full_transcription += final_confirmed; - std::cout << "└─ confirmed: " << final_confirmed << "\n"; - } - - double elapsed = timer.elapsed_ms(); - - size_t word_count = 0; - bool in_word = false; - for (char c : full_transcription) { - if (std::isspace(c)) { - in_word = false; - } else if (!in_word) { - in_word = true; - word_count++; - } - } - - std::cout << "\n[Results]\n" - << " \"success\": true,\n" - << " \"total_time_ms\": " << std::fixed << std::setprecision(2) << elapsed << ",\n" - << " \"audio_chunks\": " << ((pcm_samples.size() + chunk_size - 1) / chunk_size) << ",\n" - << " \"pcm_samples\": " << pcm_samples.size() << ",\n" - << " \"duration_sec\": " << std::setprecision(2) << (pcm_samples.size() / 16000.0) << ",\n" - << " \"words_transcribed\": " << word_count << "\n" - << "├─ Full transcription: \"" << full_transcription << "\"" << std::endl; - - cactus_destroy(model); - return true; -} - -static bool test_transcription_long() { - return run_transcription_test("TRANSCRIPTION LONG", "test_long.wav", R"({"max_tokens": 1000, "telemetry_enabled": false})", - [](int rc, const Metrics& m) { return rc > 0 && m.completion_tokens >= 8; }); -} - -static bool test_language_detection() { - std::cout << "\n╔══════════════════════════════════════════╗\n" - << "║ LANGUAGE DETECTION ║\n" - << "╚══════════════════════════════════════════╝\n"; - - if (!g_assets_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_ASSETS not set\n"; - return true; - } - - if (!g_transcribe_model_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_TRANSCRIBE_MODEL not set\n"; - return true; - } - - cactus_model_t model = cactus_init(g_transcribe_model_path, nullptr, false); - if (!model) { - std::cerr << "[✗] Failed to initialize Whisper model for language detection\n"; - return false; - } - - std::string audio_path = std::string(g_assets_path) + "/test.wav"; - char response[1 << 14] = {0}; - - int rc = cactus_detect_language( - model, - audio_path.c_str(), - response, - sizeof(response), - R"({"telemetry_enabled": false})", - nullptr, - 0 - ); - - std::string response_str(response); - if (rc <= 0) { - std::cerr << "[✗] Language detection failed: " << response_str << "\n"; - cactus_destroy(model); - return false; - } - - const bool success = response_str.find("\"success\":true") != std::string::npos; - const std::string language = json_string(response_str, "language"); - const std::string language_token = json_string(response_str, "language_token"); - const double confidence = json_number(response_str, "confidence", -1.0); - - std::cout << "\n[Results]\n" - << " \"success\": " << (success ? "true" : "false") << ",\n" - << " \"language\": \"" << language << "\",\n" - << " \"language_token\": \"" << language_token << "\",\n" - << " \"confidence\": " << std::fixed << std::setprecision(4) << confidence << "\n"; - - cactus_destroy(model); - return success && language == "en" && confidence >= 0.0 && confidence <= 1.0; -} - -static bool test_vad_process() { - std::cout << "\n╔══════════════════════════════════════════╗\n" - << "║ VAD PROCESS TEST ║\n" - << "╚══════════════════════════════════════════╝\n"; - - if (!g_vad_model_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_VAD_MODEL not set\n"; - return true; - } - - cactus_model_t model = cactus_init(g_vad_model_path, nullptr, false); - if (!model) { - std::cerr << "[✗] Failed to initialize VAD model\n"; - return false; - } - - std::string audio_path = std::string(g_assets_path) + "/test.wav"; - char response[8192] = {0}; - - Timer timer; - int result = cactus_vad(model, audio_path.c_str(), response, sizeof(response), R"({"threshold": 0.5})", nullptr, 0); - double elapsed = timer.elapsed_ms(); - - cactus_destroy(model); - - if (result < 0) { - std::cerr << "[✗] VAD processing failed\n"; - return false; - } - - std::string response_str(response); - if (response_str.find("\"success\":true") == std::string::npos) { - std::cerr << "[✗] VAD response indicates failure\n"; - return false; - } - - std::vector> segments; - size_t pos = 0; - while ((pos = response_str.find("{\"start\":", pos)) != std::string::npos) { - size_t start_pos = response_str.find(":", pos) + 1; - size_t end_pos = response_str.find(",", start_pos); - size_t start = std::stoull(response_str.substr(start_pos, end_pos - start_pos)); - - pos = response_str.find("\"end\":", pos) + 6; - end_pos = response_str.find("}", pos); - size_t end = std::stoull(response_str.substr(pos, end_pos - pos)); - - segments.push_back({start, end}); - pos = end_pos; - } - - size_t total_speech_samples = 0; - for (const auto& segment : segments) { - total_speech_samples += (segment.second - segment.first); - } - - std::cout << "\n[Results]\n" - << " \"success\": true,\n" - << " \"total_time_ms\": " << std::fixed << std::setprecision(2) << elapsed << ",\n" - << " \"speech_duration_sec\": " << std::setprecision(2) << (total_speech_samples / 16000.0) << ",\n" - << " \"segments_detected\": " << segments.size() << "\n"; - - for (size_t i = 0; i < segments.size(); ++i) { - float start_sec = segments[i].first / 16000.0f; - float end_sec = segments[i].second / 16000.0f; - const char* prefix = (i == segments.size() - 1) ? "└─" : "├─"; - std::cout << prefix << " Segment " << (i + 1) << ": " - << std::fixed << std::setprecision(2) << start_sec << "s - " - << std::setprecision(2) << end_sec << "s (" - << std::setprecision(2) << (end_sec - start_sec) << "s)" << std::endl; - } - - return result > 0 && !segments.empty(); -} - -static bool test_diarize() { - std::cout << "\n╔══════════════════════════════════════════╗\n" - << "║ DIARIZE TEST ║\n" - << "╚══════════════════════════════════════════╝\n"; - - if (!g_diarize_model_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_DIARIZE_MODEL not set\n"; - return true; - } - if (!g_assets_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_ASSETS not set\n"; - return true; - } - - cactus_model_t model = cactus_init(g_diarize_model_path, nullptr, false); - if (!model) { - std::cerr << "[✗] Failed to initialize diarization model\n"; - return false; - } - - std::string audio_path = std::string(g_assets_path) + "/test.wav"; - char response[1 << 20] = {0}; - - int result = cactus_diarize(model, audio_path.c_str(), response, sizeof(response), nullptr, nullptr, 0); - cactus_destroy(model); - - if (result < 0) { - std::cerr << "[✗] Diarization failed: " << response << "\n"; - return false; - } - - std::string response_str(response); - if (response_str.find("\"success\":true") == std::string::npos) { - std::cerr << "[✗] Diarization response indicates failure: " << response_str << "\n"; - return false; - } - - std::vector scores = parse_float_array(response_str, "scores"); - if (scores.empty() || scores.size() % 3 != 0) { - std::cerr << "[✗] scores array size " << scores.size() << " is not a multiple of 3\n"; - return false; - } - const size_t num_frames = scores.size() / 3; - const double frame_dur_s = 10.0 / 589.0; - - bool probs_valid = true; - for (size_t f = 0; f < num_frames; ++f) { - for (int s = 0; s < 3; ++s) { - float v = scores[f * 3 + s]; - if (v < -0.01f || v > 1.01f) { probs_valid = false; break; } - } - if (!probs_valid) break; - } - - float activity[3] = {0.0f, 0.0f, 0.0f}; - for (size_t f = 0; f < num_frames; ++f) - for (int s = 0; s < 3; ++s) - activity[s] += scores[f * 3 + s]; - - struct Seg { int speaker; double start, end; }; - std::vector segments; - int last_spk = -1; - static constexpr float ACTIVE_THRESHOLD = 0.5f; - - for (size_t f = 0; f < num_frames; ++f) { - int dom = -1; - float best = ACTIVE_THRESHOLD; - for (int s = 0; s < 3; ++s) - if (scores[f * 3 + s] > best) { best = scores[f * 3 + s]; dom = s; } - double t = f * frame_dur_s; - if (dom != last_spk) { - if (!segments.empty()) segments.back().end = t; - segments.push_back({dom, t, t + frame_dur_s}); - last_spk = dom; - } - } - if (!segments.empty()) segments.back().end = num_frames * frame_dur_s; - - float max_activity = std::max({activity[0], activity[1], activity[2]}); - float dom_fraction = max_activity / static_cast(num_frames); - - std::cout << "\n[Results]\n" - << " frames: " << num_frames - << " (" << std::fixed << std::setprecision(1) << frame_dur_s * 1000 << "ms/frame)\n" - << " total_time_ms: " << std::fixed << std::setprecision(2) - << json_number(response_str, "total_time_ms") << "\n" - << "\n[Speaker activity]\n"; - for (int s = 0; s < 3; ++s) - std::cout << " speaker_" << s << ": avg_prob=" << std::fixed << std::setprecision(3) - << activity[s] / num_frames << "\n"; - - std::cout << "\n[Speaker timeline]\n"; - for (const auto& seg : segments) { - std::cout << " " << std::fixed << std::setprecision(2) << seg.start << "s - " - << seg.end << "s -> " - << (seg.speaker >= 0 ? ("speaker_" + std::to_string(seg.speaker)) : "silence") << "\n"; - } - - if (!probs_valid) { - std::cerr << "[✗] Scores contain values outside [0, 1]\n"; - return false; - } - if (dom_fraction < 0.1f) { - std::cerr << "[✗] No speaker shows meaningful activity (max avg prob: " << dom_fraction << ")\n"; - return false; - } - - return true; -} - -static bool test_embed_speaker() { - std::cout << "\n╔══════════════════════════════════════════╗\n" - << "║ EMBED SPEAKER TEST ║\n" - << "╚══════════════════════════════════════════╝\n"; - - if (!g_embed_speaker_model_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_EMBED_SPEAKER_MODEL not set\n"; - return true; - } - if (!g_assets_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_ASSETS not set\n"; - return true; - } - - cactus_model_t model = cactus_init(g_embed_speaker_model_path, nullptr, false); - if (!model) { - std::cerr << "[✗] Failed to initialize speaker embedding model\n"; - return false; - } - - std::string audio_path = std::string(g_assets_path) + "/test.wav"; - char response1[1 << 16] = {0}; - char response2[1 << 16] = {0}; - - int r1 = cactus_embed_speaker(model, audio_path.c_str(), response1, sizeof(response1), nullptr, nullptr, 0, nullptr, 0); - int r2 = cactus_embed_speaker(model, audio_path.c_str(), response2, sizeof(response2), nullptr, nullptr, 0, nullptr, 0); - cactus_destroy(model); - - if (r1 < 0) { std::cerr << "[✗] Speaker embedding (run 1) failed: " << response1 << "\n"; return false; } - if (r2 < 0) { std::cerr << "[✗] Speaker embedding (run 2) failed: " << response2 << "\n"; return false; } - - std::string rs1(response1), rs2(response2); - if (rs1.find("\"success\":true") == std::string::npos) { - std::cerr << "[✗] Embedding response indicates failure: " << rs1 << "\n"; - return false; - } - - std::vector emb1 = parse_float_array(rs1, "embedding"); - std::vector emb2 = parse_float_array(rs2, "embedding"); - - if (emb1.size() != 256) { - std::cerr << "[✗] Expected 256-dim embedding, got " << emb1.size() << "\n"; - return false; - } - - bool has_nan = false, has_inf = false; - for (float v : emb1) { - if (std::isnan(v)) has_nan = true; - if (std::isinf(v)) has_inf = true; - } - - float norm = 0.0f; - for (float v : emb1) norm += v * v; - norm = std::sqrt(norm); - - float dot = 0.0f, n1 = 0.0f, n2 = 0.0f; - for (size_t i = 0; i < 256; ++i) { - dot += emb1[i] * emb2[i]; - n1 += emb1[i] * emb1[i]; - n2 += emb2[i] * emb2[i]; - } - float cos_sim = (n1 > 0 && n2 > 0) ? dot / (std::sqrt(n1) * std::sqrt(n2)) : 0.0f; - - std::cout << "\n[Results]\n" - << " dims: " << emb1.size() << "\n" - << " norm: " << std::fixed << std::setprecision(4) << norm << "\n" - << " has_nan: " << (has_nan ? "YES" : "no") << "\n" - << " has_inf: " << (has_inf ? "YES" : "no") << "\n" - << " cos_sim(same file x2): " << std::fixed << std::setprecision(6) << cos_sim - << " (expected 1.0)\n" - << " total_time_ms: " << std::fixed << std::setprecision(2) - << json_number(rs1, "total_time_ms") << "\n" - << "\n[First 8 values]\n "; - for (int i = 0; i < 8; ++i) - std::cout << std::fixed << std::setprecision(4) << emb1[i] - << (i < 7 ? " " : "\n"); - - if (has_nan) { std::cerr << "[✗] Embedding contains NaN\n"; return false; } - if (has_inf) { std::cerr << "[✗] Embedding contains Inf\n"; return false; } - if (norm < 0.01f) { std::cerr << "[✗] Embedding norm is near zero\n"; return false; } - if (cos_sim < 0.9999f) { - std::cerr << "[✗] Same-file cosine similarity " << cos_sim << " < 0.9999 (not deterministic)\n"; - return false; - } - - return true; -} - -static bool test_vocab_bias_base_class() { - if (!g_transcribe_model_path || !g_assets_path) { - std::cout << "⊘ SKIP │ VOCAB BIAS BASE CLASS │ test model/assets not set\n"; - return true; - } - - std::cout << "\n╔══════════════════════════════════════════╗\n" - << "║ VOCAB BIAS BASE CLASS TEST ║\n" - << "╚══════════════════════════════════════════╝\n"; - - char response_no_bias[1 << 15] = {0}; - char response_with_bias[1 << 15] = {0}; - - { - cactus_model_t model = cactus_init(g_transcribe_model_path, nullptr, false); - if (!model) return false; - StreamingData stream; stream.model = model; - std::string audio_path = std::string(g_assets_path) + "/hotword.wav"; - cactus_transcribe(model, audio_path.c_str(), g_whisper_prompt, - response_no_bias, sizeof(response_no_bias), - R"({"max_tokens": 100, "telemetry_enabled": false})", - stream_callback, &stream, nullptr, 0); - cactus_destroy(model); - } - - { - cactus_model_t model = cactus_init(g_transcribe_model_path, nullptr, false); - if (!model) return false; - StreamingData stream; stream.model = model; - std::string audio_path = std::string(g_assets_path) + "/hotword.wav"; - cactus_transcribe(model, audio_path.c_str(), g_whisper_prompt, - response_with_bias, sizeof(response_with_bias), - R"({ - "max_tokens": 100, - "telemetry_enabled": false, - "custom_vocabulary": ["Omeprazole", "HIPAA", "Cactus"], - "vocabulary_boost": 3.0 - })", - stream_callback, &stream, nullptr, 0); - cactus_destroy(model); - } - - std::string no_bias_text = json_string(std::string(response_no_bias), "response"); - std::string with_bias_text = json_string(std::string(response_with_bias), "response"); - - std::cout << "├─ Without bias: \"" << no_bias_text << "\"\n"; - std::cout << "└─ With bias: \"" << with_bias_text << "\"\n"; - - bool outputs_differ = (no_bias_text != with_bias_text); - std::cout << (outputs_differ ? "✓ Bias affected output\n" : "⚠ Bias had no effect on output\n"); - - return !with_bias_text.empty(); -} - -int main() { - TestUtils::TestRunner runner("STT Tests"); - runner.run_test("audio_processor", test_audio_processor()); - runner.run_test("irfft_correctness", test_irfft_correctness()); - runner.run_test("vad_process", test_vad_process()); - runner.run_test("diarize", test_diarize()); - runner.run_test("embed_speaker", test_embed_speaker()); - runner.run_test("transcription", test_transcription()); - runner.run_test("transcription_long", test_transcription_long()); - runner.run_test("language_detection", test_language_detection()); - runner.run_test("stream_transcription", test_stream_transcription()); - runner.run_test("vocab_bias_base_class", test_vocab_bias_base_class()); - runner.print_summary(); - return runner.all_passed() ? 0 : 1; -} diff --git a/tests/test_telemetry.cpp b/tests/test_telemetry.cpp deleted file mode 100644 index aa981a043..000000000 --- a/tests/test_telemetry.cpp +++ /dev/null @@ -1,203 +0,0 @@ -#include "test_utils.h" -#include "../cactus/ffi/cactus_cloud.h" -#include "../cactus/telemetry/telemetry.h" -#include "../cactus/kernel/kernel_utils.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -std::string make_temp_dir(const char* prefix) { - char pattern[256] = {0}; - std::snprintf(pattern, sizeof(pattern), "/tmp/%s_XXXXXX", prefix); - return std::string(mkdtemp(pattern)); -} - -int count_events(const std::string& file_path) { - std::ifstream in(file_path); - if (!in.is_open()) return 0; - int count = 0; - std::string line; - while (std::getline(in, line)) { - ++count; - } - return count; -} - -std::string random_project_id() { - static thread_local std::mt19937_64 rng(std::random_device{}()); - uint64_t a = rng(); - uint64_t b = rng(); - a = (a & 0xffffffffffff0fffULL) | 0x0000000000004000ULL; - b = (b & 0x3fffffffffffffffULL) | 0x8000000000000000ULL; - char buf[37]; - std::snprintf(buf, sizeof(buf), - "%08llx-%04llx-%04llx-%04llx-%012llx", - (unsigned long long)((a >> 32) & 0xffffffffULL), - (unsigned long long)((a >> 16) & 0xffffULL), - (unsigned long long)(a & 0xffffULL), - (unsigned long long)((b >> 48) & 0xffffULL), - (unsigned long long)(b & 0xffffffffffffULL)); - return std::string(buf); -} - -enum class CloudTelemetryTestResult { - Passed, - Failed, - Skipped, -}; - -bool test_record_many_then_flush() { - const std::string cache_dir = make_temp_dir("cactus_record_many_flush"); - - cactus::telemetry::setTelemetryEnvironment("cpp-test", cache_dir.c_str()); - cactus::telemetry::setCloudDisabled(true); - cactus::telemetry::init("telemetry-test-project", "record-many", nullptr); - - constexpr int expected_event_count = 200; - for (int i = 0; i < expected_event_count; ++i) { - cactus::telemetry::recordCompletion("test-model", true, 10.0, 25.0, 30.0, 32, "ok"); - } - - cactus::telemetry::flush(); - - const std::string completion_log = cache_dir + "/completion.log"; - const int event_count = count_events(completion_log); - - cactus::telemetry::shutdown(); - rmdir(cache_dir.c_str()); - return event_count == expected_event_count; -} - -bool test_shutdown_then_reinit_then_record() { - const std::string cache_dir = make_temp_dir("cactus_shutdown_reinit"); - - cactus::telemetry::setTelemetryEnvironment("cpp-test", cache_dir.c_str()); - cactus::telemetry::setCloudDisabled(true); - cactus::telemetry::init("telemetry-test-project", "shutdown-reinit", nullptr); - - cactus::telemetry::recordCompletion("test-model", true, 5.0, 20.0, 18.0, 16, "before-shutdown"); - cactus::telemetry::flush(); - - const std::string completion_log = cache_dir + "/completion.log"; - const int lines_before_shutdown = count_events(completion_log); - - cactus::telemetry::shutdown(); - - cactus::telemetry::init("telemetry-test-project", "shutdown-reinit", nullptr); - cactus::telemetry::recordCompletion("test-model", true, 6.0, 21.0, 19.0, 17, "after-reinit"); - cactus::telemetry::flush(); - - const int lines_after_reinit = count_events(completion_log); - - cactus::telemetry::shutdown(); - rmdir(cache_dir.c_str()); - return lines_after_reinit > lines_before_shutdown; -} - -bool test_record_and_flush_race_no_deadlock() { - const std::string cache_dir = make_temp_dir("cactus_telemetry_race"); - - cactus::telemetry::setTelemetryEnvironment("cpp-test", cache_dir.c_str()); - cactus::telemetry::setCloudDisabled(true); - cactus::telemetry::init("telemetry-test-project", "race-test", nullptr); - - constexpr int producer_tasks = 8; - constexpr int records_per_task = 200; - constexpr int flusher_tasks = 4; - constexpr int flushes_per_task = 40; - constexpr int expected_event_count = producer_tasks * records_per_task; - - auto& pool = CactusThreading::get_thread_pool(); - std::vector> futures; - futures.reserve(producer_tasks + flusher_tasks); - - for (int i = 0; i < producer_tasks; ++i) { - futures.push_back(pool.enqueue([]() { - for (int j = 0; j < records_per_task; ++j) { - cactus::telemetry::recordCompletion("race-model", true, 1.0, 1.0, 2.0, 1, "race"); - } - })); - } - - for (int i = 0; i < flusher_tasks; ++i) { - futures.push_back(pool.enqueue([]() { - for (int j = 0; j < flushes_per_task; ++j) { - cactus::telemetry::flush(); - } - })); - } - - bool all_completed = true; - for (auto& future : futures) { - if (future.wait_for(std::chrono::seconds(10)) != std::future_status::ready) { - all_completed = false; - break; - } - future.get(); - } - - cactus::telemetry::flush(); - - const std::string completion_log = cache_dir + "/completion.log"; - const int event_count = count_events(completion_log); - - cactus::telemetry::shutdown(); - std::remove(completion_log.c_str()); - rmdir(cache_dir.c_str()); - - return all_completed && event_count == expected_event_count; -} - -CloudTelemetryTestResult test_cloud_upload_record_then_flush() { - const char* no_cloud_tele = std::getenv("CACTUS_NO_CLOUD_TELE"); - if (no_cloud_tele && no_cloud_tele[0] != '\0') { - return CloudTelemetryTestResult::Skipped; - } - - const std::string telemetry_key = cactus::ffi::resolve_cloud_api_key(nullptr); - if (telemetry_key.empty()) { - return CloudTelemetryTestResult::Skipped; - } - - const std::string cache_dir = make_temp_dir("cactus_cloud_telemetry"); - const std::string completion_log = cache_dir + "/completion.log"; - const std::string project_id = random_project_id(); - - cactus::telemetry::setTelemetryEnvironment("cpp", cache_dir.c_str()); - cactus::telemetry::setCloudDisabled(false); - cactus::telemetry::init(project_id.c_str(), "cloud-upload", telemetry_key.c_str()); - - cactus::telemetry::recordCompletion("cloud-model", true, 7.0, 19.0, 15.0, 9, "cloud-ok"); - cactus::telemetry::flush(); - - const int cached_count = count_events(completion_log); - - cactus::telemetry::shutdown(); - std::remove(completion_log.c_str()); - rmdir(cache_dir.c_str()); - - return cached_count == 0 ? CloudTelemetryTestResult::Passed : CloudTelemetryTestResult::Failed; -} - -int main() { - TestUtils::TestRunner runner("Telemetry Tests"); - runner.run_test("Record many then Flush", test_record_many_then_flush()); - runner.run_test("Shutdown then Reinit", test_shutdown_then_reinit_then_record()); - runner.run_test("Record and Flush Race", test_record_and_flush_race_no_deadlock()); - CloudTelemetryTestResult cloud_result = test_cloud_upload_record_then_flush(); - if (cloud_result == CloudTelemetryTestResult::Skipped) { - runner.log_skip("Cloud record + Flush", "--enable-telemetry and resolved cloud key (env/cache) required"); - } else { - runner.run_test("Cloud record + Flush", cloud_result == CloudTelemetryTestResult::Passed); - } - runner.print_summary(); - return runner.all_passed() ? 0 : 1; -} diff --git a/tests/test_utils.cpp b/tests/test_utils.cpp deleted file mode 100644 index 6333b39a5..000000000 --- a/tests/test_utils.cpp +++ /dev/null @@ -1,516 +0,0 @@ -#include "test_utils.h" -#include -#include -#include - -namespace TestUtils { - -static std::mt19937 gen(42); - -size_t random_graph_input(CactusGraph& graph, const std::vector& shape, Precision precision) { - size_t node_id = graph.input(shape, precision); - size_t total_elements = 1; - for (size_t dim : shape) total_elements *= dim; - - if (precision == Precision::INT8) { - std::uniform_int_distribution dist(-50, 50); - std::vector data(total_elements); - for (size_t i = 0; i < total_elements; ++i) data[i] = static_cast(dist(gen)); - graph.set_input(node_id, data.data(), precision); - } else { - std::uniform_real_distribution dist(-2.0f, 2.0f); - std::vector data(total_elements); - for (size_t i = 0; i < total_elements; ++i) data[i] = dist(gen); - graph.set_input(node_id, data.data(), precision); - } - return node_id; -} - -bool verify_graph_outputs(CactusGraph& graph, size_t node_a, size_t node_b, float tolerance) { - graph.execute(); - const auto& buffer_a = graph.get_output_buffer(node_a); - const auto& buffer_b = graph.get_output_buffer(node_b); - - if (buffer_a.shape != buffer_b.shape || buffer_a.precision != buffer_b.precision) return false; - - void* data_a = graph.get_output(node_a); - void* data_b = graph.get_output(node_b); - size_t total_elements = 1; - for (size_t dim : buffer_a.shape) total_elements *= dim; - - if (buffer_a.precision == Precision::INT8) { - const int8_t* ptr_a = static_cast(data_a); - const int8_t* ptr_b = static_cast(data_b); - for (size_t i = 0; i < total_elements; ++i) - if (std::abs(ptr_a[i] - ptr_b[i]) > tolerance) return false; - } else { - const float* ptr_a = static_cast(data_a); - const float* ptr_b = static_cast(data_b); - for (size_t i = 0; i < total_elements; ++i) - if (std::abs(ptr_a[i] - ptr_b[i]) > tolerance) return false; - } - - graph.hard_reset(); - return true; -} - -bool verify_graph_against_data(CactusGraph& graph, size_t node_id, const void* expected_data, size_t byte_size, float tolerance) { - graph.execute(); - void* actual_data = graph.get_output(node_id); - const auto& buffer = graph.get_output_buffer(node_id); - - if (buffer.precision == Precision::INT8) { - const int8_t* actual = static_cast(actual_data); - const int8_t* expected = static_cast(expected_data); - for (size_t i = 0; i < byte_size; ++i) - if (std::abs(actual[i] - expected[i]) > tolerance) return false; - } else { - const float* actual = static_cast(actual_data); - const float* expected = static_cast(expected_data); - size_t count = byte_size / sizeof(float); - for (size_t i = 0; i < count; ++i) - if (std::abs(actual[i] - expected[i]) > tolerance) return false; - } - - graph.hard_reset(); - return true; -} - -void fill_random_int8(std::vector& data) { - std::uniform_int_distribution dist(-50, 50); - for (auto& val : data) val = static_cast(dist(gen)); -} - -void fill_random_float(std::vector& data) { - std::uniform_real_distribution dist(-2.0f, 2.0f); - for (auto& val : data) val = dist(gen); -} - -void fill_random_fp16(std::vector<__fp16>& data) { - std::uniform_real_distribution dist(-2.0f, 2.0f); - for (auto& val : data) val = static_cast<__fp16>(dist(gen)); -} - -TestRunner::TestRunner(const std::string& suite_name) - : suite_name_(suite_name), passed_count_(0), total_count_(0) { - std::cout << "\n╔══════════════════════════════════════════════════════════════════════════════════════╗\n" - << "║ Running " << std::left << std::setw(76) << suite_name_ << " ║\n" - << "╚══════════════════════════════════════════════════════════════════════════════════════╝\n"; -} - -void TestRunner::run_test(const std::string& test_name, bool result) { - total_count_++; - if (result) { - passed_count_++; - std::cout << "✓ PASS │ " << std::left << std::setw(25) << test_name << "\n"; - } else { - std::cout << "✗ FAIL │ " << std::left << std::setw(25) << test_name << "\n"; - } -} - -void TestRunner::log_performance(const std::string& test_name, const std::string& details) { - std::cout << "⚡PERF │ " << std::left << std::setw(38) << test_name << " │ " << details << "\n"; -} - -void TestRunner::log_skip(const std::string& test_name, const std::string& reason) { - std::cout << "⊘ SKIP │ " << std::left << std::setw(25) << test_name << " │ " << reason << "\n"; -} - -void TestRunner::print_summary() { - std::cout << "────────────────────────────────────────────────────────────────────────────────────────\n"; - if (all_passed()) - std::cout << "✓ All " << total_count_ << " tests passed!\n"; - else - std::cout << "✗ " << (total_count_ - passed_count_) << " of " << total_count_ << " tests failed!\n"; - std::cout << "\n"; -} - -bool TestRunner::all_passed() const { - return passed_count_ == total_count_; -} - -bool test_basic_operation(const std::string& op_name, - std::function op_func, - const std::vector<__fp16>& data_a, - const std::vector<__fp16>& data_b, - const std::vector<__fp16>& expected, - const std::vector& shape) { - (void)op_name; - CactusGraph graph; - size_t input_a = graph.input(shape, Precision::FP16); - size_t input_b = graph.input(shape, Precision::FP16); - size_t result_id = op_func(graph, input_a, input_b); - - graph.set_input(input_a, const_cast(static_cast(data_a.data())), Precision::FP16); - graph.set_input(input_b, const_cast(static_cast(data_b.data())), Precision::FP16); - graph.execute(); - - __fp16* output = static_cast<__fp16*>(graph.get_output(result_id)); - for (size_t i = 0; i < expected.size(); ++i) { - if (std::abs(static_cast(output[i]) - static_cast(expected[i])) > 1e-2f) { - graph.hard_reset(); - return false; - } - } - graph.hard_reset(); - return true; -} - -bool test_scalar_operation(const std::string& op_name, - std::function op_func, - const std::vector<__fp16>& data, - float scalar, - const std::vector<__fp16>& expected, - const std::vector& shape) { - (void)op_name; - CactusGraph graph; - size_t input_a = graph.input(shape, Precision::FP16); - size_t result_id = op_func(graph, input_a, scalar); - - graph.set_input(input_a, const_cast(static_cast(data.data())), Precision::FP16); - graph.execute(); - - __fp16* output = static_cast<__fp16*>(graph.get_output(result_id)); - for (size_t i = 0; i < expected.size(); ++i) { - if (std::abs(static_cast(output[i]) - static_cast(expected[i])) > 1e-2f) { - graph.hard_reset(); - return false; - } - } - graph.hard_reset(); - return true; -} - -} - -namespace EngineTestUtils { - -Timer::Timer() : start(std::chrono::high_resolution_clock::now()) {} - -double Timer::elapsed_ms() const { - auto end = std::chrono::high_resolution_clock::now(); - return std::chrono::duration_cast(end - start).count() / 1000.0; -} - -double json_number(const std::string& json, const std::string& key, double def) { - std::string pattern = "\"" + key + "\":"; - size_t pos = json.find(pattern); - if (pos == std::string::npos) return def; - size_t start = pos + pattern.size(); - while (start < json.size() && (json[start] == ' ' || json[start] == '\t')) ++start; - size_t end = start; - while (end < json.size() && std::string(",}] \t\n\r").find(json[end]) == std::string::npos) ++end; - try { return std::stod(json.substr(start, end - start)); } - catch (...) { return def; } -} - -std::string json_string(const std::string& json, const std::string& key) { - std::string pattern = "\"" + key + "\":"; - size_t pos = json.find(pattern); - if (pos == std::string::npos) return {}; - size_t start = pos + pattern.size(); - - while (start < json.size() && (json[start] == ' ' || json[start] == '\t')) ++start; - if (start >= json.size() || json[start] != '"') return {}; - ++start; - - std::string out; - out.reserve(128); - bool escaped = false; - for (size_t i = start; i < json.size(); ++i) { - char c = json[i]; - if (escaped) { - switch (c) { - case '"': out.push_back('"'); break; - case '\\': out.push_back('\\'); break; - case '/': out.push_back('/'); break; - case 'b': out.push_back('\b'); break; - case 'f': out.push_back('\f'); break; - case 'n': out.push_back('\n'); break; - case 'r': out.push_back('\r'); break; - case 't': out.push_back('\t'); break; - default: out.push_back(c); break; - } - escaped = false; - continue; - } - - if (c == '\\') { - escaped = true; - continue; - } - - if (c == '"') { - return out; - } - - out.push_back(c); - } - return {}; -} - -std::string escape_json(const std::string& s) { - std::ostringstream o; - for (auto c : s) { - switch (c) { - case '"': o << "\\\""; break; - case '\\': o << "\\\\"; break; - case '\n': o << "\\n"; break; - case '\r': o << "\\r"; break; - default: o << c; break; - } - } - return o.str(); -} - -void stream_callback(const char* token, uint32_t token_id, void* user_data) { - auto* data = static_cast(user_data); - data->tokens.push_back(token ? token : ""); - data->token_ids.push_back(token_id); - data->token_count++; - - std::string out = token ? token : ""; - for (char& c : out) if (c == '\n') c = ' '; - std::cout << out << std::flush; - - if (data->stop_at > 0 && data->token_count >= data->stop_at) { - std::cout << " [-> stopped]" << std::flush; - cactus_stop(data->model); - } -} - -bool json_bool(const std::string& json, const std::string& key, bool def = false) { - std::string pattern = "\"" + key + "\":"; - size_t pos = json.find(pattern); - if (pos == std::string::npos) return def; - size_t start = pos + pattern.size(); - while (start < json.size() && (json[start] == ' ' || json[start] == '\t')) ++start; - if (start + 4 <= json.size() && json.substr(start, 4) == "true") return true; - if (start + 5 <= json.size() && json.substr(start, 5) == "false") return false; - return def; -} - -std::string json_array(const std::string& json, const std::string& key) { - std::string pattern = "\"" + key + "\":"; - size_t pos = json.find(pattern); - if (pos == std::string::npos) return "[]"; - size_t start = pos + pattern.size(); - while (start < json.size() && (json[start] == ' ' || json[start] == '\t')) ++start; - if (start >= json.size() || json[start] != '[') return "[]"; - int depth = 1; - size_t end = start + 1; - while (end < json.size() && depth > 0) { - if (json[end] == '[') depth++; - else if (json[end] == ']') depth--; - end++; - } - return json.substr(start, end - start); -} - -void Metrics::parse(const std::string& json) { - success = json_bool(json, "success", false); - error = json_string(json, "error"); - cloud_handoff = json_bool(json, "cloud_handoff", false); - response = json_string(json, "response"); - thinking = json_string(json, "thinking"); - function_calls = json_array(json, "function_calls"); - confidence = json_number(json, "confidence", -1.0); - ttft = json_number(json, "time_to_first_token_ms"); - total_ms = json_number(json, "total_time_ms"); - prefill_tps = json_number(json, "prefill_tps"); - decode_tps = json_number(json, "decode_tps"); - ram_mb = json_number(json, "ram_usage_mb"); - prefill_tokens = json_number(json, "prefill_tokens"); - completion_tokens = json_number(json, "decode_tokens"); - total_tokens = json_number(json, "total_tokens"); - segments = json_array(json, "segments"); -} - -void Metrics::print_json() const { - std::cout << " \"success\": " << (success ? "true" : "false") << ",\n" - << " \"error\": " << (error.empty() ? "null" : "\"" + error + "\"") << ",\n" - << " \"cloud_handoff\": " << (cloud_handoff ? "true" : "false") << ",\n" - << " \"response\": \"" << response << "\",\n" - << " \"thinking\": " << (thinking.empty() ? "null" : "\"" + thinking + "\"") << ",\n" - << " \"function_calls\": " << function_calls << ",\n" - << " \"segments\": " << segments << ",\n" - << " \"confidence\": " << std::fixed << std::setprecision(4) << confidence << ",\n" - << " \"time_to_first_token_ms\": " << std::setprecision(2) << ttft << ",\n" - << " \"total_time_ms\": " << total_ms << ",\n" - << " \"prefill_tps\": " << prefill_tps << ",\n" - << " \"decode_tps\": " << decode_tps << ",\n" - << " \"ram_usage_mb\": " << ram_mb << ",\n" - << " \"prefill_tokens\": " << std::setprecision(0) << prefill_tokens << ",\n" - << " \"decode_tokens\": " << completion_tokens << ",\n" - << " \"total_tokens\": " << total_tokens << std::endl; -} - -void PrefillMetrics::parse(const std::string& json) { - success = json_bool(json, "success", false); - error = json_string(json, "error"); - prefill_tokens = json_number(json, "prefill_tokens"); - prefill_tps = json_number(json, "prefill_tps"); - total_ms = json_number(json, "total_time_ms"); - ram_mb = json_number(json, "ram_usage_mb"); -} - -std::string PrefillMetrics::line() const { - std::ostringstream oss; - oss << std::fixed << std::setprecision(2) - << "prefill_tokens=" << std::setprecision(0) << prefill_tokens - << ", prefill_tps=" << std::setprecision(2) << prefill_tps - << ", total_time_ms=" << std::setprecision(2) << total_ms - << ", ram_usage_mb=" << std::setprecision(2) << ram_mb; - return oss.str(); -} - -void PrefillMetrics::print_line() const { - std::cout << line(); -} - -} - -#ifdef HAVE_SDL2 - -AudioCapture::AudioCapture(int len_ms) - : m_len_ms(len_ms) - , m_running(false) - , m_dev_id_in(0) - , m_sdl_initialized(false) - , m_audio_pos(0) - , m_audio_len(0) - , m_total_samples_received(0) {} - -AudioCapture::~AudioCapture() { - if (m_dev_id_in) SDL_CloseAudioDevice(m_dev_id_in); - if (m_sdl_initialized) SDL_Quit(); -} - -bool AudioCapture::init(int capture_id, int sample_rate) { - static bool sdl_globally_initialized = false; - - if (!sdl_globally_initialized) { - if (SDL_Init(SDL_INIT_AUDIO) < 0) { - std::cerr << "SDL_Init failed: " << SDL_GetError() << std::endl; - return false; - } - sdl_globally_initialized = true; - m_sdl_initialized = true; - } - - SDL_SetHintWithPriority(SDL_HINT_AUDIO_RESAMPLING_MODE, "medium", SDL_HINT_OVERRIDE); - m_audio.resize((m_len_ms * sample_rate) / 1000); - - int num_devices = SDL_GetNumAudioDevices(SDL_TRUE); - std::cout << "\nAvailable audio capture devices:\n"; - for (int i = 0; i < num_devices; i++) - std::cout << " [" << i << "] " << SDL_GetAudioDeviceName(i, SDL_TRUE) << "\n"; - - if (capture_id >= num_devices) { - std::cerr << "Invalid capture device ID: " << capture_id << std::endl; - return false; - } - - std::cout << "Selected device: [" << capture_id << "] " - << SDL_GetAudioDeviceName(capture_id, SDL_TRUE) << "\n\n"; - - SDL_AudioSpec capture_spec_requested; - SDL_zero(capture_spec_requested); - capture_spec_requested.freq = sample_rate; - capture_spec_requested.format = AUDIO_F32; - capture_spec_requested.channels = 1; - capture_spec_requested.samples = 1024; - capture_spec_requested.callback = [](void* userdata, uint8_t* stream, int len) { - static_cast(userdata)->callback(stream, len); - }; - capture_spec_requested.userdata = this; - - SDL_AudioSpec capture_spec_obtained; - m_dev_id_in = SDL_OpenAudioDevice( - SDL_GetAudioDeviceName(capture_id, SDL_TRUE), - SDL_TRUE, &capture_spec_requested, &capture_spec_obtained, 0); - - if (!m_dev_id_in) { - std::cerr << "SDL_OpenAudioDevice failed: " << SDL_GetError() << std::endl; - return false; - } - - std::cout << "Audio capture initialized:\n" - << " Sample rate: " << capture_spec_obtained.freq << " Hz\n" - << " Channels: " << (int)capture_spec_obtained.channels << "\n" - << " Samples: " << capture_spec_obtained.samples << "\n" - << " Buffer length: " << m_len_ms << " ms\n"; - - return true; -} - -void AudioCapture::resume() { - std::lock_guard lock(m_mutex); - if (!m_running && m_dev_id_in) { - SDL_PauseAudioDevice(m_dev_id_in, 0); - m_running = true; - } -} - -void AudioCapture::pause() { - std::lock_guard lock(m_mutex); - if (m_running && m_dev_id_in) { - SDL_PauseAudioDevice(m_dev_id_in, 1); - m_running = false; - } -} - -void AudioCapture::clear() { - std::lock_guard lock(m_mutex); - m_audio_pos = 0; - m_audio_len = 0; -} - -size_t AudioCapture::get(int duration_ms, std::vector& result) { - std::lock_guard lock(m_mutex); - const size_t n_samples = (duration_ms * m_audio.size()) / m_len_ms; - if (n_samples > m_audio_len) return 0; - - result.resize(n_samples); - size_t start_pos = (m_audio_pos + m_audio.size() - m_audio_len) % m_audio.size(); - for (size_t i = 0; i < n_samples; i++) - result[i] = m_audio[(start_pos + i) % m_audio.size()]; - - m_audio_len = (m_audio_len > n_samples) ? (m_audio_len - n_samples) : 0; - return n_samples; -} - -size_t AudioCapture::get_all(std::vector& result) { - std::lock_guard lock(m_mutex); - if (m_audio_len == 0) return 0; - - result.resize(m_audio_len); - size_t start_pos = (m_audio_pos + m_audio.size() - m_audio_len) % m_audio.size(); - for (size_t i = 0; i < m_audio_len; i++) - result[i] = m_audio[(start_pos + i) % m_audio.size()]; - - size_t n_samples = m_audio_len; - m_audio_len = 0; - return n_samples; -} - -size_t AudioCapture::get_buffer_length() const { - std::lock_guard lock(m_mutex); - return m_audio_len; -} - -void AudioCapture::callback(uint8_t* stream, int len) { - const size_t n_samples = len / sizeof(float); - const float* samples = reinterpret_cast(stream); - if (!m_running) return; - - std::lock_guard lock(m_mutex); - for (size_t i = 0; i < n_samples; i++) { - m_audio[m_audio_pos] = samples[i]; - m_audio_pos = (m_audio_pos + 1) % m_audio.size(); - if (m_audio_len < m_audio.size()) m_audio_len++; - } - m_total_samples_received += n_samples; -} - -#endif diff --git a/tests/test_utils.h b/tests/test_utils.h deleted file mode 100644 index b6c8767b4..000000000 --- a/tests/test_utils.h +++ /dev/null @@ -1,283 +0,0 @@ -#ifndef TEST_UTILS_H -#define TEST_UTILS_H - -#include "../cactus/cactus.h" -#include "../cactus/ffi/cactus_ffi.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -namespace TestUtils { - -size_t random_graph_input(CactusGraph& graph, const std::vector& shape, Precision precision = Precision::INT8); -bool verify_graph_outputs(CactusGraph& graph, size_t node_a, size_t node_b, float tolerance = 1e-6f); -bool verify_graph_against_data(CactusGraph& graph, size_t node_id, const void* expected_data, size_t byte_size, float tolerance = 1e-6f); -void fill_random_int8(std::vector& data); -void fill_random_float(std::vector& data); - -template -double time_function(Func&& func, int iterations = 1) { - auto start = std::chrono::high_resolution_clock::now(); - for (int i = 0; i < iterations; ++i) func(); - auto end = std::chrono::high_resolution_clock::now(); - return std::chrono::duration(end - start).count(); -} - -class TestRunner { -public: - TestRunner(const std::string& suite_name); - void run_test(const std::string& test_name, bool result); - void log_performance(const std::string& test_name, const std::string& details); - void log_skip(const std::string& test_name, const std::string& reason); - void print_summary(); - bool all_passed() const; - -private: - std::string suite_name_; - int passed_count_; - int total_count_; -}; - -template -constexpr Precision default_precision() { - if constexpr (std::is_same_v) return Precision::INT8; - else if constexpr (std::is_same_v) return Precision::FP16; - else return Precision::FP32; -} - -template -constexpr float default_tolerance() { - if constexpr (std::is_same_v) return 1e-2f; - else return 1e-6f; -} - -template -bool compare_arrays(const T* actual, const T* expected, size_t count, float tolerance = default_tolerance()) { - for (size_t i = 0; i < count; ++i) { - if constexpr (std::is_same_v) { - if (std::abs(static_cast(actual[i]) - static_cast(expected[i])) > tolerance) return false; - } else if constexpr (std::is_floating_point_v) { - if (std::abs(actual[i] - expected[i]) > tolerance) return false; - } else { - if (actual[i] != expected[i]) return false; - } - } - return true; -} - -template -class TestFixture { -public: - TestFixture(const std::string& = "") {} - ~TestFixture() { graph_.hard_reset(); } - - CactusGraph& graph() { return graph_; } - - size_t create_input(const std::vector& shape, Precision precision = default_precision()) { - return graph_.input(shape, precision); - } - - void set_input_data(size_t input_id, const std::vector& data, Precision precision = default_precision()) { - graph_.set_input(input_id, const_cast(static_cast(data.data())), precision); - } - - void execute() { graph_.execute(); } - - T* get_output(size_t node_id) { - return static_cast(graph_.get_output(node_id)); - } - - bool verify_output(size_t node_id, const std::vector& expected, float tolerance = default_tolerance()) { - return compare_arrays(get_output(node_id), expected.data(), expected.size(), tolerance); - } - -private: - CactusGraph graph_; -}; - -using Int8TestFixture = TestFixture; -using FP16TestFixture = TestFixture<__fp16>; - -void fill_random_fp16(std::vector<__fp16>& data); - -bool test_basic_operation(const std::string& op_name, - std::function op_func, - const std::vector<__fp16>& data_a, - const std::vector<__fp16>& data_b, - const std::vector<__fp16>& expected, - const std::vector& shape = {4}); - -bool test_scalar_operation(const std::string& op_name, - std::function op_func, - const std::vector<__fp16>& data, - float scalar, - const std::vector<__fp16>& expected, - const std::vector& shape = {4}); - -inline std::vector load_bin(const std::string& path) { - std::ifstream f(path, std::ios::binary | std::ios::ate); - if (!f.is_open()) return {}; - size_t bytes = f.tellg(); - f.seekg(0); - std::vector data(bytes / sizeof(float)); - f.read(reinterpret_cast(data.data()), bytes); - return data; -} - -inline float cosine_sim(const std::vector& a, const std::vector& b) { - size_t n = std::min(a.size(), b.size()); - if (n == 0 || a.size() != b.size()) return -1; - double dot = 0, na = 0, nb = 0; - for (size_t i = 0; i < n; i++) { - dot += (double)a[i]*b[i]; na += (double)a[i]*a[i]; nb += (double)b[i]*b[i]; - } - return (na > 0 && nb > 0) ? (float)(dot / (std::sqrt(na) * std::sqrt(nb))) : 0; -} - -} - -namespace EngineTestUtils { - -struct Timer { - std::chrono::high_resolution_clock::time_point start; - Timer(); - double elapsed_ms() const; -}; - -double json_number(const std::string& json, const std::string& key, double def = 0.0); -std::string json_string(const std::string& json, const std::string& key); -std::string escape_json(const std::string& s); - -struct StreamingData { - std::vector tokens; - std::vector token_ids; - int token_count = 0; - cactus_model_t model = nullptr; - int stop_at = -1; -}; - -void stream_callback(const char* token, uint32_t token_id, void* user_data); - -struct Metrics { - bool success = false; - std::string error; - bool cloud_handoff = false; - std::string response; - std::string thinking; - std::string function_calls; - double confidence = -1.0; - double ttft = 0.0; - double total_ms = 0.0; - double prefill_tps = 0.0; - double decode_tps = 0.0; - double ram_mb = 0.0; - double prefill_tokens = 0.0; - double completion_tokens = 0.0; - double total_tokens = 0.0; - std::string segments; - - void parse(const std::string& json); - void print_json() const; -}; - -struct PrefillMetrics { - bool success = false; - std::string error; - double prefill_tokens = 0.0; - double prefill_tps = 0.0; - double total_ms = 0.0; - double ram_mb = 0.0; - - void parse(const std::string& json); - std::string line() const; - void print_line() const; -}; - -template -bool run_test(const char* title, const char* model_path, const char* messages, - const char* options, TestFunc test_logic, - const char* tools = nullptr, int stop_at = -1, - const char* user_prompt = nullptr) { - std::cout << "\n╔══════════════════════════════════════════╗\n" - << "║" << std::setw(42) << std::left << std::string(" ") + title << "║\n" - << "╚══════════════════════════════════════════╝\n"; - - if (user_prompt) { - std::cout << "├─ User prompt: " << user_prompt << "\n"; - } - - cactus_model_t model = cactus_init(model_path, nullptr, false); - if (!model) { - std::cerr << "[✗] Failed to initialize model\n"; - return false; - } - - StreamingData data; - data.model = model; - data.stop_at = stop_at; - - char response[4096]; - std::cout << "Response: "; - - int result = cactus_complete(model, messages, response, sizeof(response), - options, tools, stream_callback, &data, nullptr, 0); - - std::cout << "\n\n[Results]\n"; - - Metrics metrics; - metrics.parse(response); - - bool success = test_logic(result, data, response, metrics); - std::cout << "└─ Status: " << (success ? "PASSED ✓" : "FAILED ✗") << std::endl; - - cactus_destroy(model); - return success; -} - -} - -#ifdef HAVE_SDL2 - -#include -#include - -class AudioCapture { -public: - AudioCapture(int len_ms = 10000); - ~AudioCapture(); - - bool init(int capture_id, int sample_rate); - void resume(); - void pause(); - void clear(); - size_t get(int duration_ms, std::vector& result); - size_t get_all(std::vector& result); - bool is_running() const { return m_running; } - size_t get_total_samples_received() const { return m_total_samples_received; } - size_t get_buffer_length() const; - -private: - void callback(uint8_t* stream, int len); - - int m_len_ms; - std::atomic m_running; - SDL_AudioDeviceID m_dev_id_in; - bool m_sdl_initialized; - std::vector m_audio; - size_t m_audio_pos; - size_t m_audio_len; - std::atomic m_total_samples_received; - mutable std::mutex m_mutex; -}; - -#endif - -#endif diff --git a/tests/test_vlm.cpp b/tests/test_vlm.cpp deleted file mode 100644 index 46333c152..000000000 --- a/tests/test_vlm.cpp +++ /dev/null @@ -1,406 +0,0 @@ -#include "test_utils.h" -#include -#include -#include - -using namespace EngineTestUtils; - -static const char* g_model_path = std::getenv("CACTUS_TEST_MODEL"); -static const char* g_assets_path = std::getenv("CACTUS_TEST_ASSETS"); - -static const char* g_options = R"({ - "max_tokens": 256, - "stop_sequences": ["<|im_end|>", ""], - "telemetry_enabled": false - })"; - -bool test_vlm_multiturn() { - std::string model_path_str(g_model_path ? g_model_path : ""); - - std::string vision_file = model_path_str + "/vision_patch_embedding.weights"; - std::ifstream vf(vision_file); - if (!vf.good()) { - std::cout << "Skipping VLM multi-turn test: vision weights not found." << std::endl; - return true; - } - vf.close(); - - std::cout << "\n╔══════════════════════════════════════════╗\n" - << "║ VLM MULTI-TURN TEST ║\n" - << "╚══════════════════════════════════════════╝\n"; - - cactus_model_t model = cactus_init(g_model_path, nullptr, false); - if (!model) { - std::cerr << "Failed to initialize model for VLM multi-turn test" << std::endl; - return false; - } - - std::string img_path = std::string(g_assets_path) + "/test_monkey.png"; - - std::string messages1 = "[{\"role\": \"user\", " - "\"content\": \"Describe what is happening in this image in two sentences.\", " - "\"images\": [\"" + img_path + "\"]}]"; - - StreamingData stream_data1; - stream_data1.model = model; - char response1[4096]; - - std::cout << "\n[Turn 1]\n"; - std::cout << "User: Describe what is happening in this image in two sentences.\n"; - std::cout << "Assistant: "; - - int result1 = cactus_complete(model, messages1.c_str(), response1, sizeof(response1), - g_options, nullptr, stream_callback, &stream_data1, nullptr, 0); - - std::cout << "\n\n[Results - Turn 1]\n"; - Metrics metrics1; - metrics1.parse(response1); - metrics1.print_json(); - - bool success1 = result1 > 0 && stream_data1.token_count > 0; - - if (!success1) { - std::cout << "└─ Status: FAILED ✗\n"; - cactus_destroy(model); - return false; - } - - std::string assistant_response; - for (const auto& token : stream_data1.tokens) { - assistant_response += token; - } - - std::string messages2 = "[{\"role\": \"user\", " - "\"content\": \"Describe what is happening in this image in two sentences.\", " - "\"images\": [\"" + img_path + "\"]}, " - "{\"role\": \"assistant\", \"content\": \"" + escape_json(assistant_response) + "\"}, " - "{\"role\": \"user\", \"content\": \"Describe the image once again.\"}]"; - - StreamingData stream_data2; - stream_data2.model = model; - char response2[4096]; - - std::cout << "\n[Turn 2]\n"; - std::cout << "User: Describe the image once again.\n"; - std::cout << "Assistant: "; - - int result2 = cactus_complete(model, messages2.c_str(), response2, sizeof(response2), - g_options, nullptr, stream_callback, &stream_data2, nullptr, 0); - - std::cout << "\n\n[Results - Turn 2]\n"; - Metrics metrics2; - metrics2.parse(response2); - metrics2.print_json(); - - bool success2 = result2 > 0 && stream_data2.token_count > 0; - - if (!success2) { - std::cout << "└─ Status: FAILED ✗ (Follow-up message failed)\n"; - } - - cactus_destroy(model); - return success1 && success2; -} - -bool test_prefill_invalidated_on_message_change_vlm() { - std::cout << "\n╔══════════════════════════════════════════╗\n" - << "║" << std::setw(42) << std::left << " PREFILL INVALIDATION (VLM) TEST" << "║\n" - << "╚══════════════════════════════════════════╝\n"; - - std::string model_path_str(g_model_path ? g_model_path : ""); - std::string vision_file = model_path_str + "/vision_patch_embedding.weights"; - std::ifstream vf(vision_file); - if (!vf.good()) { - std::cout << "⊘ SKIP │ Vision weights not found\n"; - return true; - } - vf.close(); - - if (!g_assets_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_ASSETS not set\n"; - return true; - } - - std::string prefill_img_path = std::string(g_assets_path) + "/test_monkey.png"; - std::ifstream imgf(prefill_img_path); - if (!imgf.good()) { - std::cout << "⊘ SKIP │ test_monkey.png not found\n"; - return true; - } - imgf.close(); - - std::string complete_img_path = std::string(g_assets_path) + "/test_thing.png"; - std::ifstream imgf2(complete_img_path); - if (!imgf2.good()) { - std::cout << "⊘ SKIP │ test_thing.png not found\n"; - return true; - } - imgf2.close(); - - cactus_model_t model = cactus_init(g_model_path, nullptr, false); - if (!model) { - std::cerr << "[✗] Failed to initialize model\n"; - return false; - } - - std::string prefill_messages = "[{\"role\": \"user\", \"content\": \"Describe this image in one short sentence.\", \"images\": [\"" - + prefill_img_path + "\"]}]"; - - std::string complete_messages = "[{\"role\": \"user\", \"content\": \"Describe this image in one short sentence.\", \"images\": [\"" - + complete_img_path + "\"]}]"; - - const char* options = R"({ - "max_tokens": 128, - "stop_sequences": ["<|im_end|>", ""], - "confidence_threshold": 0.0, - "telemetry_enabled": false - })"; - - char prefill_response[2048] = {0}; - int prefill_result = cactus_prefill(model, prefill_messages.c_str(), prefill_response, sizeof(prefill_response), nullptr, nullptr, nullptr, 0); - PrefillMetrics prefill_metrics; - prefill_metrics.parse(prefill_response); - - char complete_response_warm[4096] = {0}; - int complete_result_warm = cactus_complete(model, complete_messages.c_str(), complete_response_warm, sizeof(complete_response_warm), - options, nullptr, nullptr, nullptr, nullptr, 0); - Metrics warm_metrics; - warm_metrics.parse(complete_response_warm); - - cactus_reset(model); - - char complete_response_cold[4096] = {0}; - int complete_result_cold = cactus_complete(model, complete_messages.c_str(), complete_response_cold, sizeof(complete_response_cold), - options, nullptr, nullptr, nullptr, nullptr, 0); - Metrics cold_metrics; - cold_metrics.parse(complete_response_cold); - - std::cout << "\n\n[Results]\n"; - std::cout << "├─ Prefill success: " << ((prefill_result > 0 && prefill_metrics.success) ? "YES" : "NO") << "\n" - << "├─ Complete(warm mismatched) prefill_tokens: " << warm_metrics.prefill_tokens << "\n" - << "├─ Complete(cold) prefill_tokens: " << cold_metrics.prefill_tokens << "\n"; - - bool all_success = prefill_result > 0 && prefill_metrics.success - && complete_result_warm > 0 && warm_metrics.success - && complete_result_cold > 0 && cold_metrics.success; - bool invalidated = warm_metrics.prefill_tokens == cold_metrics.prefill_tokens; - - std::cout << "├─ Calls successful: " << (all_success ? "YES" : "NO") << "\n" - << "└─ Mismatch invalidated cache: " << (invalidated ? "YES" : "NO") << std::endl; - - cactus_destroy(model); - return all_success && invalidated; -} - -bool test_prefill_with_images() { - std::cout << "\n╔══════════════════════════════════════════╗\n" - << "║" << std::setw(42) << std::left << " PREFILL WITH IMAGES TEST" << "║\n" - << "╚══════════════════════════════════════════╝\n"; - - std::string model_path_str(g_model_path ? g_model_path : ""); - std::string vision_file = model_path_str + "/vision_patch_embedding.weights"; - std::ifstream vf(vision_file); - if (!vf.good()) { - std::cout << "⊘ SKIP │ Vision weights not found\n"; - return true; - } - vf.close(); - - if (!g_assets_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_ASSETS not set\n"; - return true; - } - - std::string prefill_img_path = std::string(g_assets_path) + "/test_monkey.png"; - std::ifstream imgf(prefill_img_path); - if (!imgf.good()) { - std::cout << "⊘ SKIP │ test_monkey.png not found\n"; - return true; - } - imgf.close(); - - std::string extension_img_path = std::string(g_assets_path) + "/test_thing.png"; - std::ifstream imgf2(extension_img_path); - if (!imgf2.good()) { - std::cout << "⊘ SKIP │ test_thing.png not found\n"; - return true; - } - imgf2.close(); - - cactus_model_t model = cactus_init(g_model_path, nullptr, false); - if (!model) { - std::cerr << "[✗] Failed to initialize model\n"; - return false; - } - - std::string prefill_messages = "[" - "{\"role\": \"system\", \"content\": \"You are a helpful assistant. Be concise.\"}," - "{\"role\": \"user\", \"content\": \"Describe this image in one short sentence.\", \"images\": [\"" - + prefill_img_path + "\"]}," - "{\"role\": \"assistant\", \"content\": \"This image shows a close-up of a monkey face.\"}" - "]"; - - std::string complete_messages = "[" - "{\"role\": \"system\", \"content\": \"You are a helpful assistant. Be concise.\"}," - "{\"role\": \"user\", \"content\": \"Describe this image in one short sentence.\", \"images\": [\"" - + prefill_img_path + "\"]}," - "{\"role\": \"assistant\", \"content\": \"This image shows a close-up of a monkey face.\"}," - "{\"role\": \"user\", \"content\": \"Now describe this second image in one short sentence.\", \"images\": [\"" - + extension_img_path + "\"]}" - "]"; - - const char* options = R"({ - "max_tokens": 128, - "stop_sequences": ["<|im_end|>", ""], - "confidence_threshold": 0.0, - "telemetry_enabled": false - })"; - - char prefill_response[2048] = {0}; - int prefill_result = cactus_prefill(model, prefill_messages.c_str(), prefill_response, sizeof(prefill_response), nullptr, nullptr, nullptr, 0); - PrefillMetrics prefill_metrics; - prefill_metrics.parse(prefill_response); - - char complete_response_warm[4096] = {0}; - int complete_result_warm = cactus_complete(model, complete_messages.c_str(), complete_response_warm, sizeof(complete_response_warm), - options, nullptr, nullptr, nullptr, nullptr, 0); - Metrics warm_metrics; - warm_metrics.parse(complete_response_warm); - - cactus_reset(model); - - char complete_response_cold[4096] = {0}; - int complete_result_cold = cactus_complete(model, complete_messages.c_str(), complete_response_cold, sizeof(complete_response_cold), - options, nullptr, nullptr, nullptr, nullptr, 0); - Metrics cold_metrics; - cold_metrics.parse(complete_response_cold); - - std::cout << "\n\n[Results]\n"; - std::cout << "├─ Prefill success: " << ((prefill_result > 0 && prefill_metrics.success) ? "YES" : "NO") << "\n" - << "├─ Prefill metrics: "; - prefill_metrics.print_line(); - std::cout << "\n"; - std::cout << "├─ Complete warm metrics:\n"; - warm_metrics.print_json(); - std::cout << "├─ Complete cold metrics:\n"; - cold_metrics.print_json(); - - bool all_success = prefill_result > 0 && prefill_metrics.success - && complete_result_warm > 0 && warm_metrics.success - && complete_result_cold > 0 && cold_metrics.success; - bool warm_prefilled_less = warm_metrics.prefill_tokens < cold_metrics.prefill_tokens; - - std::cout << "├─ Calls successful: " << (all_success ? "YES" : "NO") << "\n" - << "└─ Warm prefilled less than cold: " << (warm_prefilled_less ? "YES" : "NO") << std::endl; - - cactus_destroy(model); - return all_success && warm_prefilled_less; -} - -bool test_prefill_prefix_extension_reuse_vlm() { - std::cout << "\n╔══════════════════════════════════════════╗\n" - << "║" << std::setw(42) << std::left << " PREFILL PREFIX EXTENSION (VLM)" << "║\n" - << "╚══════════════════════════════════════════╝\n"; - - std::string model_path_str(g_model_path ? g_model_path : ""); - std::string vision_file = model_path_str + "/vision_patch_embedding.weights"; - std::ifstream vf(vision_file); - if (!vf.good()) { - std::cout << "⊘ SKIP │ Vision weights not found\n"; - return true; - } - vf.close(); - - if (!g_assets_path) { - std::cout << "⊘ SKIP │ CACTUS_TEST_ASSETS not set\n"; - return true; - } - - std::string base_img_path = std::string(g_assets_path) + "/test_monkey.png"; - std::ifstream imgf(base_img_path); - if (!imgf.good()) { - std::cout << "⊘ SKIP │ test_monkey.png not found\n"; - return true; - } - imgf.close(); - - std::string extension_img_path = std::string(g_assets_path) + "/test_thing.png"; - std::ifstream imgf2(extension_img_path); - if (!imgf2.good()) { - std::cout << "⊘ SKIP │ test_thing.png not found\n"; - return true; - } - imgf2.close(); - - cactus_model_t model = cactus_init(g_model_path, nullptr, false); - if (!model) { - std::cerr << "[✗] Failed to initialize model\n"; - return false; - } - - std::string base_messages = "[" - "{\"role\": \"system\", \"content\": \"You are a helpful assistant. Be concise.\"}," - "{\"role\": \"user\", \"content\": \"Describe this image in one short sentence.\", \"images\": [\"" - + base_img_path + "\"]}," - "{\"role\": \"assistant\", \"content\": \"This image shows a close-up of a monkey face.\"}" - "]"; - - std::string extended_messages = "[" - "{\"role\": \"system\", \"content\": \"You are a helpful assistant. Be concise.\"}," - "{\"role\": \"user\", \"content\": \"Describe this image in one short sentence.\", \"images\": [\"" - + base_img_path + "\"]}," - "{\"role\": \"assistant\", \"content\": \"This image shows a close-up of a monkey face.\"}," - "{\"role\": \"user\", \"content\": \"Now describe this second image in one short sentence.\", \"images\": [\"" - + extension_img_path + "\"]}" - "]"; - - char prefill_response1[2048] = {0}; - int prefill_result1 = cactus_prefill(model, base_messages.c_str(), prefill_response1, sizeof(prefill_response1), nullptr, nullptr, nullptr, 0); - PrefillMetrics prefill_metrics1; - prefill_metrics1.parse(prefill_response1); - - char prefill_response2[2048] = {0}; - int prefill_result2 = cactus_prefill(model, extended_messages.c_str(), prefill_response2, sizeof(prefill_response2), nullptr, nullptr, nullptr, 0); - PrefillMetrics prefill_metrics2; - prefill_metrics2.parse(prefill_response2); - - cactus_reset(model); - - char prefill_response3[2048] = {0}; - int prefill_result3 = cactus_prefill(model, extended_messages.c_str(), prefill_response3, sizeof(prefill_response3), nullptr, nullptr, nullptr, 0); - PrefillMetrics prefill_metrics3; - prefill_metrics3.parse(prefill_response3); - - std::cout << "\n\n[Results]\n"; - std::cout << "├─ Prefill#1 (base): "; - prefill_metrics1.print_line(); - std::cout << "\n" - << "├─ Prefill#2 (extended, warm): "; - prefill_metrics2.print_line(); - std::cout << "\n" - << "├─ Prefill#3 (extended, cold): "; - prefill_metrics3.print_line(); - std::cout << "\n"; - - bool prefill_success = prefill_result1 > 0 && prefill_result2 > 0 && prefill_result3 > 0 - && prefill_metrics1.success && prefill_metrics2.success && prefill_metrics3.success; - bool second_call_prefilled = prefill_metrics2.prefill_tokens > 0; - bool warm_reused_prefix = prefill_metrics2.prefill_tokens < prefill_metrics3.prefill_tokens; - - std::cout << "├─ Prefill calls success: " << (prefill_success ? "YES" : "NO") << "\n" - << "├─ Warm extension prefilled tokens: " << (second_call_prefilled ? "YES" : "NO") << "\n" - << "└─ Warm extension < cold extension: " << (warm_reused_prefix ? "YES" : "NO") << std::endl; - - cactus_destroy(model); - return prefill_success && second_call_prefilled && warm_reused_prefix; -} - -int main() { - TestUtils::TestRunner runner("VLM Tests"); - runner.run_test("prefill_with_images", test_prefill_with_images()); - runner.run_test("prefill_prefix_extension_reuse", test_prefill_prefix_extension_reuse_vlm()); - runner.run_test("prefill_invalidated_on_message_change", test_prefill_invalidated_on_message_change_vlm()); - runner.run_test("vlm_multiturn", test_vlm_multiturn()); - runner.print_summary(); - return runner.all_passed() ? 0 : 1; -}