diff --git a/.dockerignore b/.dockerignore index 5ccf492fd..2936d9abc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,8 @@ .git .github -dist/ +dist/* +!dist/docker-packages/ +!dist/docker-packages/** compat-artifacts/ .cache/ benchmarks/results diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75befee9c..1bf4b6f16 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,10 +62,12 @@ jobs: fail-fast: false matrix: include: - - runner: ubuntu-24.04 + - runner: king arch-label: linux-amd64 + local-lsquic-cache-dir: /opt/king/cache/lsquic/runtime - runner: ubuntu-24.04-arm arch-label: linux-arm64 + local-lsquic-cache-dir: "" steps: - name: Checkout repository @@ -74,8 +76,17 @@ jobs: - name: Install system dependencies run: bash ./infra/scripts/install-ci-system-dependencies.sh + - name: Restore local pinned LSQUIC runtime cache + id: restore-local-lsquic-runtime-cache + if: matrix.local-lsquic-cache-dir != '' + env: + KING_CI_LOCAL_LSQUIC_CACHE_DIR: ${{ matrix.local-lsquic-cache-dir }} + KING_LSQUIC_RUNTIME_PREFIX: ${{ github.workspace }}/.cache/king/lsquic/runtime/prefix + run: bash ./infra/scripts/local-lsquic-runtime-cache.sh restore --github-output "${GITHUB_OUTPUT}" + - name: Restore pinned LSQUIC runtime cache id: restore-lsquic-runtime-cache + if: steps.restore-local-lsquic-runtime-cache.outputs.cache-hit != 'true' uses: actions/cache/restore@v5 with: path: .cache/king/lsquic/runtime/prefix @@ -86,8 +97,7 @@ jobs: env: KING_LSQUIC_RUNTIME_PREFIX: ${{ github.workspace }}/.cache/king/lsquic/runtime/prefix run: | - if [[ "${{ steps.restore-lsquic-runtime-cache.outputs.cache-hit }}" == "true" ]] \ - && bash ./infra/scripts/build-lsquic-runtime.sh --verify-current; then + if bash ./infra/scripts/build-lsquic-runtime.sh --verify-current; then echo "cache-current=true" >> "${GITHUB_OUTPUT}" exit 0 fi @@ -100,6 +110,13 @@ jobs: KING_LSQUIC_RUNTIME_PREFIX: ${{ github.workspace }}/.cache/king/lsquic/runtime/prefix run: bash ./infra/scripts/build-lsquic-runtime.sh + - name: Save local pinned LSQUIC runtime cache + if: matrix.local-lsquic-cache-dir != '' && steps.restore-local-lsquic-runtime-cache.outputs.cache-hit != 'true' + env: + KING_CI_LOCAL_LSQUIC_CACHE_DIR: ${{ matrix.local-lsquic-cache-dir }} + KING_LSQUIC_RUNTIME_PREFIX: ${{ github.workspace }}/.cache/king/lsquic/runtime/prefix + run: bash ./infra/scripts/local-lsquic-runtime-cache.sh save + - name: Save pinned LSQUIC runtime cache if: steps.verify-lsquic-runtime-cache.outputs.cache-current != 'true' uses: actions/cache/save@v5 @@ -187,6 +204,11 @@ jobs: working-directory: extension run: ../infra/scripts/check-stub-parity.sh + - name: Run model-optional inference verification matrix + if: matrix.shard-index == 1 + working-directory: extension + run: ../bin/king-inference-verify-matrix --json + - name: Run canonical PHPT suite working-directory: extension env: diff --git a/.github/workflows/local-inference-runner.yml b/.github/workflows/local-inference-runner.yml new file mode 100644 index 000000000..f378f21be --- /dev/null +++ b/.github/workflows/local-inference-runner.yml @@ -0,0 +1,59 @@ +name: King Local Inference Runner + +on: + push: + branches: + - '**' + pull_request: + branches: + - main + types: + - opened + - synchronize + - reopened + workflow_dispatch: + +concurrency: + group: king-local-inference-runner-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.event.pull_request.head.ref || github.ref_name }} + cancel-in-progress: true + +permissions: + contents: read + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" + KING_CI_LOCAL_INFERENCE_MODEL_DIR: /opt/king/cache/inference-models + KING_CI_LOCAL_LSQUIC_CACHE_DIR: /opt/king/cache/lsquic/runtime + +jobs: + local-inference-runner: + name: Local inference cache readiness + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: king + timeout-minutes: 10 + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Resolve local GGUF inference model cache + id: inference-model + run: bash ./infra/scripts/local-inference-model-cache.sh resolve --require --github-output "${GITHUB_OUTPUT}" + + - name: Export local inference model path + env: + MODEL_PRESENT: ${{ steps.inference-model.outputs.model-present }} + MODEL_PATH: ${{ steps.inference-model.outputs.model-realpath }} + run: | + test "${MODEL_PRESENT}" = "true" + test -r "${MODEL_PATH}" + printf 'KING_INFERENCE_TEST_MODEL_PATH=%s\n' "${MODEL_PATH}" >> "${GITHUB_ENV}" + printf 'KING_INFERENCE_CPU_TEST_MODEL_PATH=%s\n' "${MODEL_PATH}" >> "${GITHUB_ENV}" + + - name: Verify heavy native dependency cache hook + run: | + test -x ./infra/scripts/local-lsquic-runtime-cache.sh + test -n "${KING_CI_LOCAL_LSQUIC_CACHE_DIR}" + + - name: Verify no public inference port is exposed + run: bash ./infra/scripts/check-inference-port-exposure.sh --ports 8080,18129 diff --git a/.github/workflows/release-merge-publish.yml b/.github/workflows/release-merge-publish.yml index 7aad3c287..c83e9fe6b 100644 --- a/.github/workflows/release-merge-publish.yml +++ b/.github/workflows/release-merge-publish.yml @@ -21,6 +21,7 @@ env: REGISTRY: ghcr.io FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" PHP_BASE_MAX_AGE_DAYS: "14" + KING_CI_LOCAL_DOCKER_BUILDX_CACHE_DIR: /opt/king/cache/docker/buildx jobs: resolve-release-merge: @@ -158,6 +159,13 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 + - name: Resolve Docker Buildx cache + id: base-buildx-cache + run: | + bash ./infra/scripts/resolve-docker-buildx-cache.sh \ + --scope "php-base-${{ matrix.php-version }}" \ + --github-output "${GITHUB_OUTPUT}" + - name: Log in to Container Registry uses: docker/login-action@v4 with: @@ -251,11 +259,18 @@ jobs: push: true tags: ${{ steps.meta-base.outputs.ref }} labels: ${{ steps.meta-base.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: ${{ steps.base-buildx-cache.outputs.cache-from }} + cache-to: ${{ steps.base-buildx-cache.outputs.cache-to }} build-args: | PHP_VERSION=${{ matrix.php-version }} + - name: Commit local Docker Buildx cache + if: steps.decide-base.outputs.rebuild == 'true' && steps.base-buildx-cache.outputs.use-local-cache == 'true' + run: | + bash ./infra/scripts/commit-docker-buildx-cache.sh \ + --cache-dir "${{ steps.base-buildx-cache.outputs.local-cache-dir }}" \ + --next-dir "${{ steps.base-buildx-cache.outputs.local-cache-next-dir }}" + build-release-packages: name: Build release artifacts for ${{ matrix.php-version }} / ${{ matrix.arch-label }} needs: @@ -722,6 +737,13 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 + - name: Resolve Docker Buildx cache + id: dockerhub-buildx-cache + run: | + bash ./infra/scripts/resolve-docker-buildx-cache.sh \ + --scope "dockerhub-runtime-php8.5" \ + --github-output "${GITHUB_OUTPUT}" + - name: Log in to Docker Hub uses: docker/login-action@v4 with: @@ -744,14 +766,21 @@ jobs: push: true tags: ${{ steps.meta-dockerhub.outputs.tags }} labels: ${{ steps.meta-dockerhub.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: ${{ steps.dockerhub-buildx-cache.outputs.cache-from }} + cache-to: ${{ steps.dockerhub-buildx-cache.outputs.cache-to }} build-args: | PHP_VERSION=8.5 PHP_BASE_IMAGE=${{ steps.meta-dockerhub.outputs.php_base_image }} BUILD_DATE=${{ steps.meta-dockerhub.outputs.created_at }} VCS_REF=${{ needs.resolve-release-merge.outputs.release-commit }} + - name: Commit local Docker Buildx cache + if: steps.dockerhub-buildx-cache.outputs.use-local-cache == 'true' + run: | + bash ./infra/scripts/commit-docker-buildx-cache.sh \ + --cache-dir "${{ steps.dockerhub-buildx-cache.outputs.local-cache-dir }}" \ + --next-dir "${{ steps.dockerhub-buildx-cache.outputs.local-cache-next-dir }}" + - name: Generate Docker Hub runtime CVE inventory id: dockerhub-cve-inventory run: | @@ -867,6 +896,13 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 + - name: Resolve Docker Buildx cache + id: runtime-buildx-cache + run: | + bash ./infra/scripts/resolve-docker-buildx-cache.sh \ + --scope "runtime-php${{ matrix.php-version }}" \ + --github-output "${GITHUB_OUTPUT}" + - name: Log in to Container Registry uses: docker/login-action@v4 with: @@ -883,10 +919,17 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: ${{ steps.runtime-buildx-cache.outputs.cache-from }} + cache-to: ${{ steps.runtime-buildx-cache.outputs.cache-to }} build-args: | PHP_VERSION=${{ matrix.php-version }} PHP_BASE_IMAGE=${{ steps.meta.outputs.php_base_image }} BUILD_DATE=${{ steps.meta.outputs.created_at }} VCS_REF=${{ needs.resolve-release-merge.outputs.release-commit }} + + - name: Commit local Docker Buildx cache + if: steps.runtime-buildx-cache.outputs.use-local-cache == 'true' + run: | + bash ./infra/scripts/commit-docker-buildx-cache.sh \ + --cache-dir "${{ steps.runtime-buildx-cache.outputs.local-cache-dir }}" \ + --next-dir "${{ steps.runtime-buildx-cache.outputs.local-cache-next-dir }}" diff --git a/.gitignore b/.gitignore index 050f552dc..dd0b7b291 100755 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ libcurl quiche llama-fork/ node_modules +.venv/ .cargo/ # Archived source snapshots @@ -39,6 +40,7 @@ compat-artifacts/ dist/ !packages/iibin/dist/ !packages/iibin/dist/** +var/ .cache/ .vite/ .codex @@ -47,6 +49,9 @@ dist/ .env.local *.env.local +# Local planning notes that are not part of the public docs set. +docs/future-plan.md + # Model weights MUST NEVER be checked in (they are large and provenance # belongs on a model registry / object store, not in git). Catch-all # patterns repo-wide — if you actually need a tiny test fixture that diff --git a/README.md b/README.md index 5edb11fe9..1e787e7a5 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ signals, and cluster-facing workflows in one coherent runtime model. The current line is beta. The repo-local baseline is green, the multi-backend object-store and control-plane surfaces are real, and the remaining closure work is now about narrower hardening, distributed-operating proof, and -multi-node fleet behavior rather than placeholder subsystem stories. +multi-node fleet behavior rather than paper subsystem claims. If you do not have a Hetzner account yet, it is time to fix that. Use the [Hetzner Cloud referral link](https://hetzner.cloud/?ref=VYfKUSIni63u) and you @@ -186,7 +186,8 @@ King brings the following into one extension: - Smart DNS / Semantic DNS for service discovery and routing - router and load-balancer control-plane configuration and policy - IIBIN for schema-defined native binary encoding and decoding -- MCP for agent and tool protocol integration +- MCP public server handling over JSON-RPC stdio and Streamable HTTP +- King-internal MCP peer calls, transfers, deadlines, and configured IIBIN payload contracts - XSLT 2.0/3.0 transformation hooks for XML standards and Schematron/SVRL pipelines through optional SaxonC runtime loading - real multi-backend object-store and CDN primitives - telemetry, metrics, tracing, and admin control surfaces @@ -229,9 +230,12 @@ the application logic. ### 3. Control Plane The control plane is for remote work that is not just "serve one response now". -This is where MCP and the pipeline orchestrator live. -MCP moves structured requests, uploads, downloads, deadlines, and cancellation -between peers. +This is where MCP and the pipeline orchestrator live. King's public MCP server +surface handles JSON-RPC stdio and Streamable HTTP requests for tools, +resources, prompts, initialization, and ping. The internal MCP peer surface +moves structured requests, uploads, downloads, deadlines, and cancellation +between trusted King peers. IIBIN schemas for internal MCP calls are fixed on +the connection config, not passed as mutable request-time switches. The orchestrator manages multi-step work, queue-backed execution, remote-worker execution, run snapshots, and restart-aware control flow. If work needs to continue beyond one request, move to another process, or be @@ -323,7 +327,7 @@ King includes a native control-plane model around: King is also a data and protocol runtime: - IIBIN for schema-defined binary serialization -- MCP for tool and agent protocol traffic +- MCP for public JSON-RPC tool/resource/prompt servers and King-internal peer traffic - XSLT-backed XML transformation and validation pipelines for standards-heavy payloads - real multi-backend object-store primitives - CDN-oriented cache distribution hooks @@ -352,8 +356,89 @@ The core programming model is: - `King\MCP`, `King\IIBIN`, `King\ObjectStore`, `King\PipelineOrchestrator`, `King\Autoscaling`, and `King\WebSocket\Connection` expose subsystem-specific runtime surfaces. -- Procedural `king_xslt_*` functions expose the SaxonC-backed XSLT runtime - status and file transformation primitives. +- `King\RTP\Socket` exposes the native RTP/ICE-lite/DTLS-SRTP runtime as a + resource-backed OO surface. +- `King\XSLT\Processor` and the procedural `king_xslt_*` functions expose the + SaxonC-backed XSLT runtime status and file transformation primitives. +- `King\Inference`, `King\Inference\Model`, `King\Inference\Stream`, and the + procedural `king_inference_*` functions expose local quantized GGUF model + registration through a backend contract. King parses GGUF metadata and tensor + directories natively, `local` provides the current token-streaming runner + contract, and `king_native_cpu` exposes model structure, native tokenizer + lookup, paged KV-cache planning, public tensor views, bounded + dequantization, K-quant block decoding, first CPU tensor/vector math, and + a native mini-graph for embedding, RMSNorm, linear projection, RoPE, dot, + stack, softmax, weighted-sum context assembly, serializable KV state, + range-based KV attention, token selection from logits, scale, add steps, and + graph-backed native CPU token streaming without an external inference runtime. + Plain-text OpenAI-compatible chat requests on `king_native_cpu` now compile + validated `messages` into transient native CPU prompt/decode graphs instead + of requiring callers to provide `graph` or `graphs` manually, and + `stream=true` returns OpenAI-compatible Chat Completions SSE chunks from the + same native event stream. + `king_native_gpu` uses the same OpenAI streaming surface when GPU runtime + readiness admits the native prompt loop, while graph payloads stay on the + native stream contract. + Native graph streams are stateless by default; `king.inference_with_memory` + sets the php.ini baseline and `with_memory` opts into carrying graph result + state between decode steps. The LLM cache for that memory mode is also + disabled by default and becomes active only when both memory mode and + `king.inference_llm_cache_enable=1` are configured. The stream layer can emit + explicit OpenAI-compatible Chat + Completions chunks, and the HTTP helper can serve + `GET /v1/models`, `GET /v1/models/{model}`, `POST /v1/chat/completions`, + `POST /v1/responses`, legacy `POST /v1/completions`, and + `POST /v1/embeddings` through King server response arrays while broader + native layer coverage and quantized kernels are added. Omitted inference + backend config selects `king_native_cpu`; the runtime model primitive can + resolve the configured `auto|gpu|cpu` model profile, preferring + `gemma4:12b` when GPU use and a GPU GGUF artifact are configured, and + falling back to `gemma3:1b` for CPU. `gemma3:1b` is the compact King baseline: + useful for fast local preflight and smoke checks, but still expected to handle + simple chat, exact output, language following, small PHP/King snippets, and + basic local Coder help. Future fine-tuning keeps that baseline useful instead + of treating it as a disposable test model. The GPU path owns a CUDA context, + allocates device memory, uploads required weights, caches uploaded tensors, + and exposes token embedding row loading, native Q8_0 quantized matrix/vector, + device vector operations, a lazy F32 device KV-cache with device-to-device + slot writes, RMSNorm, RoPE, attention score, attention softmax, attention + value aggregation, FFN/SwiGLU, final output projection paths, bounded top-K + logits readback, and a decoder graph executor contract for the complete + token-decode op set. The executor also exposes a graph result envelope for + native GPU graph streams, carrying graph, terminal, and token-selection + metadata plus a graph execution plan that separates CUDA-device work from + host sampling after bounded logits readback. The first graph device op, + token embedding row loading, now executes through the CUDA embedding kernel; + the following RMSNorm op can run on that device vector through the CUDA + RMSNorm kernel, and the following Q8_0 linear projection can run through the + CUDA quantized matvec kernel. When the next graph op slices that linear + output for attention heads, the CUDA slice vector kernel runs. When that + slice feeds RoPE, the CUDA RoPE kernel rotates the head vector. The executor + also prepares the sibling K/V head path by running K and V projections, + slicing the first key/value head, and rotating the key slice before temporary + buffers are released. The contract still refuses to claim decoded token + output before full device execution results exist. The native GPU + prompt-loop admission path can + tokenize prompt text, build token-decode graphs, and validate those graphs + into result envelopes against the GPU executor without falling back to CPU + execution. The GPU + backend now uses the same native stream object contract as the CPU backend + for start events, native events, cancellation, metrics, and thermal + preflight/abort metadata, while OpenAI + text generation remains blocked until the full GPU decoder loop is ready. + GPU metadata exposes the remaining bridge explicitly: the remaining residual, + FFN, and logits device ops after the initial embedding/RMSNorm/linear/slice/ + RoPE plus K/V head-preparation, KV-cache write, and KV-attention chain still + need execution and bounded logits results before the prompt loop may emit + decoded tokens. + Sampling stays CPU-side for the current native GPU + contract: the GPU narrows logits to bounded candidates, then the existing + deterministic token-selection policy applies temperature, top-k, top-p, and + seed handling without copying the full vocabulary logits back. Memory-enabled + native graph streams also + enforce an LLM-cache admission policy with a configurable disk-free floor and + webhook/MCP alert metadata. The process-runner path is explicit `local` + configuration. The procedural API exists for direct systems work and low-friction interop. The OO API exists for typed composition and long-lived application structure. @@ -401,6 +486,10 @@ It is supposed to look like one native system with a PHP-facing control surface. Core source areas: - [`extension/`](extension/): PHP extension source, tests, headers, and build glue. + The public native header root is [`extension/include/`](extension/include/) + and the active runtime source root is [`extension/src/`](extension/src/). + See [`docs/source-layout.md`](docs/source-layout.md) for the subsystem layout + and bootstrap boundaries. - [`infra/scripts/`](infra/scripts/): build, package, release, and local install scripts. - [`libcurl/`](libcurl/): vendored curl headers used by the extension build. - [`userland/flow-php/`](userland/flow-php/): repository-local PHP adapters for streaming ETL, checkpointing, and King-backed pipeline orchestration. diff --git a/bin/king-coder-fine-tune b/bin/king-coder-fine-tune new file mode 100755 index 000000000..998d4e801 --- /dev/null +++ b/bin/king-coder-fine-tune @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PHP_BIN="${PHP_BIN:-php}" +EXTENSION="${KING_EXTENSION:-$ROOT/extension/modules/king.so}" +SCRIPT="$ROOT/infra/inference/fine-tune/coder-dataset.php" +MEMORY_LIMIT="${KING_FINE_TUNE_PHP_MEMORY_LIMIT:-4G}" + +if [[ ! -f "$EXTENSION" ]]; then + echo "king-coder-fine-tune: King extension module not found at $EXTENSION" >&2 + exit 127 +fi + +exec "$PHP_BIN" -n \ + -d "extension=$EXTENSION" \ + -d king.security_allow_config_override=1 \ + -d "memory_limit=$MEMORY_LIMIT" \ + -d display_errors=stderr \ + -d log_errors=1 \ + "$SCRIPT" "$@" diff --git a/bin/king-inference-layer0-debug b/bin/king-inference-layer0-debug new file mode 100755 index 000000000..a63da95ad --- /dev/null +++ b/bin/king-inference-layer0-debug @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PHP_BIN="${PHP_BIN:-php}" +EXTENSION="${KING_EXTENSION:-$ROOT/extension/modules/king.so}" +INI="${KING_INFERENCE_PHP_INI:-$ROOT/infra/inference/local-gpu.php.ini}" + +if [[ ! -f "$EXTENSION" ]]; then + echo "king-inference-layer0-debug: King extension module not found at $EXTENSION" >&2 + exit 127 +fi + +if [[ ! -f "$INI" ]]; then + echo "king-inference-layer0-debug: PHP INI not found at $INI" >&2 + exit 127 +fi + +export KING_ROOT="$ROOT" +export KING_INFERENCE_PHP_INI="$INI" +export PHP_INI_SCAN_DIR="${PHP_INI_SCAN_DIR:-}" + +exec "$PHP_BIN" \ + -c "$INI" \ + -d "extension=$EXTENSION" \ + -d king.security_allow_config_override=1 \ + -d display_errors=1 \ + -d error_reporting=32767 \ + "$ROOT/infra/inference/layer0-debug.php" "$@" diff --git a/bin/king-inference-release-gate b/bin/king-inference-release-gate new file mode 100755 index 000000000..87126be5f --- /dev/null +++ b/bin/king-inference-release-gate @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PHP_BIN="${PHP_BIN:-php}" + +exec "$PHP_BIN" "$ROOT/infra/inference/release-gate.php" "$@" diff --git a/bin/king-inference-status b/bin/king-inference-status new file mode 100755 index 000000000..7238f1dc2 --- /dev/null +++ b/bin/king-inference-status @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PHP_BIN="${PHP_BIN:-php}" +EXTENSION="${KING_EXTENSION:-$ROOT/extension/modules/king.so}" +INI="${KING_INFERENCE_PHP_INI:-$ROOT/infra/inference/local-gpu.php.ini}" + +if [[ ! -f "$EXTENSION" ]]; then + echo "king-inference-status: King extension module not found at $EXTENSION" >&2 + exit 127 +fi + +if [[ ! -f "$INI" ]]; then + echo "king-inference-status: PHP INI not found at $INI" >&2 + exit 127 +fi + +export KING_ROOT="$ROOT" +export KING_INFERENCE_PHP_INI="$INI" +export PHP_INI_SCAN_DIR="${PHP_INI_SCAN_DIR:-}" + +exec "$PHP_BIN" \ + -c "$INI" \ + -d "extension=$EXTENSION" \ + -d king.security_allow_config_override=1 \ + -d display_errors=1 \ + -d error_reporting=32767 \ + "$ROOT/infra/inference/status.php" "$@" diff --git a/bin/king-inference-verify-matrix b/bin/king-inference-verify-matrix new file mode 100755 index 000000000..d95fa2ec3 --- /dev/null +++ b/bin/king-inference-verify-matrix @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PHP_BIN="${PHP_BIN:-php}" +EXTENSION="${KING_EXTENSION:-$ROOT/extension/modules/king.so}" +MEMORY_LIMIT="${KING_INFERENCE_PHP_MEMORY_LIMIT:-4G}" + +if [[ ! -f "$EXTENSION" ]]; then + echo "king-inference-verify-matrix: King extension module not found at $EXTENSION" >&2 + exit 127 +fi + +export KING_ROOT="$ROOT" +export PHP_INI_SCAN_DIR="${PHP_INI_SCAN_DIR:-}" + +exec "$PHP_BIN" -n \ + -d "extension=$EXTENSION" \ + -d king.security_allow_config_override=1 \ + -d king.gpu_bindings_enable=1 \ + -d king.gpu_default_backend=cuda \ + -d "memory_limit=$MEMORY_LIMIT" \ + -d display_errors=stderr \ + -d log_errors=1 \ + "$ROOT/infra/inference/verification-matrix.php" "$@" diff --git a/bin/king-local-infer b/bin/king-local-infer new file mode 100755 index 000000000..25f74666b --- /dev/null +++ b/bin/king-local-infer @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PHP_BIN="${PHP_BIN:-php}" +EXTENSION="${KING_EXTENSION:-$ROOT/extension/modules/king.so}" +SCRIPT="$ROOT/infra/inference/king-local-infer.php" +MEMORY_LIMIT="${KING_INFERENCE_PHP_MEMORY_LIMIT:-2G}" + +if [[ ! -f "$EXTENSION" ]]; then + echo "king-local-infer: King extension module not found at $EXTENSION" >&2 + exit 127 +fi + +exec "$PHP_BIN" -n \ + -d "extension=$EXTENSION" \ + -d king.security_allow_config_override=1 \ + -d "memory_limit=$MEMORY_LIMIT" \ + -d display_errors=stderr \ + -d log_errors=1 \ + "$SCRIPT" "$@" diff --git a/bin/king-native-hello-world b/bin/king-native-hello-world new file mode 100755 index 000000000..3b461d50d --- /dev/null +++ b/bin/king-native-hello-world @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PHP_BIN="${PHP_BIN:-php}" +EXTENSION="${KING_EXTENSION:-$ROOT/extension/modules/king.so}" +SCRIPT="$ROOT/infra/inference/king-native-hello-world.php" +MEMORY_LIMIT="${KING_INFERENCE_PHP_MEMORY_LIMIT:-4G}" + +if [[ ! -f "$EXTENSION" ]]; then + echo "king-native-hello-world: King extension module not found at $EXTENSION" >&2 + exit 127 +fi + +exec "$PHP_BIN" -n \ + -d "extension=$EXTENSION" \ + -d king.security_allow_config_override=1 \ + -d king.gpu_bindings_enable=1 \ + -d king.gpu_default_backend=cuda \ + -d "memory_limit=$MEMORY_LIMIT" \ + -d display_errors=stderr \ + -d log_errors=1 \ + "$SCRIPT" "$@" diff --git a/bin/king-openai-baseline b/bin/king-openai-baseline new file mode 100755 index 000000000..96dffa714 --- /dev/null +++ b/bin/king-openai-baseline @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PHP_BIN="${PHP_BIN:-php}" + +exec "$PHP_BIN" "$ROOT/infra/inference/performance-baseline.php" "$@" diff --git a/bin/king-openai-bench b/bin/king-openai-bench new file mode 100755 index 000000000..3d2a02b43 --- /dev/null +++ b/bin/king-openai-bench @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PHP_BIN="${PHP_BIN:-php}" + +exec "$PHP_BIN" "$ROOT/infra/inference/bench-openai-route.php" "$@" diff --git a/bin/king-openai-golden-prompts b/bin/king-openai-golden-prompts new file mode 100755 index 000000000..0d025bbab --- /dev/null +++ b/bin/king-openai-golden-prompts @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PHP_BIN="${PHP_BIN:-php}" +EXTENSION="${KING_EXTENSION:-$ROOT/extension/modules/king.so}" +HOST="${KING_INFERENCE_GOLDEN_HOST:-127.0.0.1}" +PORT="${KING_INFERENCE_GOLDEN_PORT:-18092}" +URL="${KING_INFERENCE_GOLDEN_BASE_URL:-http://${HOST}:${PORT}/v1}" +ARTIFACT="${KING_INFERENCE_GOLDEN_MODEL_PATH:-$ROOT/var/inference-models/gemma3-1b.gguf}" +MODEL="${KING_INFERENCE_GOLDEN_MODEL:-gemma3:1b}" +BACKEND="${KING_INFERENCE_GOLDEN_BACKEND:-cpu}" +START_TIMEOUT_SEC="${KING_INFERENCE_GOLDEN_START_TIMEOUT_SEC:-45}" +USE_EXISTING="${KING_INFERENCE_GOLDEN_USE_EXISTING_SERVER:-0}" +LOG_FILE="$(mktemp "${TMPDIR:-/tmp}/king-openai-golden.XXXXXX.log")" +PACK_SUMMARY=0 + +for arg in "$@"; do + if [[ "$arg" == "--pack-summary" ]]; then + PACK_SUMMARY=1 + fi +done + +cleanup() { + local status=$? + if [[ -n "${ROUTER_PID:-}" ]] && kill -0 "$ROUTER_PID" 2>/dev/null; then + kill "$ROUTER_PID" 2>/dev/null || true + wait "$ROUTER_PID" 2>/dev/null || true + fi + if [[ "$status" -ne 0 && -s "$LOG_FILE" ]]; then + echo "king-openai-golden-prompts: router log follows:" >&2 + tail -n 80 "$LOG_FILE" >&2 || true + fi + rm -f "$LOG_FILE" +} +trap cleanup EXIT INT TERM + +if [[ "$USE_EXISTING" != "1" && "$PACK_SUMMARY" != "1" ]]; then + if [[ ! -f "$EXTENSION" ]]; then + echo "king-openai-golden-prompts: King extension module not found at $EXTENSION" >&2 + exit 127 + fi + if [[ ! -r "$ARTIFACT" ]]; then + echo "king-openai-golden-prompts: model artifact is not readable: $ARTIFACT" >&2 + exit 1 + fi + ARTIFACT_CONFIG="$ARTIFACT" + if [[ "$ARTIFACT_CONFIG" != /* ]]; then + ARTIFACT_CONFIG="$ROOT/${ARTIFACT_CONFIG#./}" + fi + GPU_ENABLE=0 + GPU_DEFAULT_BACKEND=disabled + PROFILE=cpu + case "$BACKEND" in + gpu|cuda|king_native_gpu) + GPU_ENABLE=1 + GPU_DEFAULT_BACKEND=cuda + PROFILE=gpu + BACKEND=king_native_gpu + ;; + cpu|king_native_cpu) + BACKEND=king_native_cpu + ;; + esac + PHP_ARGS=( + -n + -d "extension=$EXTENSION" + -d king.security_allow_config_override=1 + -d "king.inference_preferred_model_profile=$PROFILE" + -d "king.inference_models=${MODEL},${BACKEND},${ARTIFACT_CONFIG},golden" + -d "memory_limit=${KING_INFERENCE_PHP_MEMORY_LIMIT:-4G}" + -d display_errors=stderr + -d log_errors=1 + ) + if [[ "$PROFILE" == "gpu" ]]; then + PHP_ARGS+=( + -d "king.gpu_bindings_enable=$GPU_ENABLE" + -d "king.gpu_default_backend=$GPU_DEFAULT_BACKEND" + -d king.inference_gpu_max_gpu_layers=99 + -d king.inference_gpu_vram_reserve_mb=2048 + -d king.inference_gpu_min_free_vram_mb=4096 + -d king.inference_gpu_allow_system_ram_offload=0 + ) + fi + + KING_OPENAI_HOST="$HOST" \ + KING_OPENAI_PORT="$PORT" \ + KING_OPENAI_DEFAULT_MAX_TOKENS="${KING_OPENAI_DEFAULT_MAX_TOKENS:-128}" \ + KING_OPENAI_MAX_COMPLETION_TOKENS="${KING_OPENAI_MAX_COMPLETION_TOKENS:-256}" \ + KING_OPENAI_CONTEXT_POLICY="${KING_OPENAI_CONTEXT_POLICY:-full}" \ + "$PHP_BIN" "${PHP_ARGS[@]}" "$ROOT/infra/inference/openai-router.php" >"$LOG_FILE" 2>&1 & + ROUTER_PID=$! + + deadline=$((SECONDS + START_TIMEOUT_SEC)) + until curl -fsS --max-time 1 "${URL}/models" >/dev/null 2>&1; do + if ! kill -0 "$ROUTER_PID" 2>/dev/null; then + echo "king-openai-golden-prompts: router exited before /v1/models became reachable" >&2 + exit 1 + fi + if (( SECONDS >= deadline )); then + echo "king-openai-golden-prompts: timed out waiting for ${URL}/models" >&2 + exit 1 + fi + sleep 0.5 + done +fi + +KING_INFERENCE_GOLDEN_BASE_URL="$URL" \ +KING_INFERENCE_GOLDEN_MODEL="$MODEL" \ +KING_INFERENCE_GOLDEN_MODEL_PATH="$ARTIFACT" \ + "$PHP_BIN" "$ROOT/infra/inference/golden-prompts.php" "$@" diff --git a/bin/king-openai-router b/bin/king-openai-router new file mode 100755 index 000000000..7a9e7e4f1 --- /dev/null +++ b/bin/king-openai-router @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PHP_BIN="${PHP_BIN:-php}" +EXTENSION="${KING_EXTENSION:-$ROOT/extension/modules/king.so}" +INI="${KING_INFERENCE_PHP_INI:-$ROOT/infra/inference/local-gpu.php.ini}" +MEMORY_LIMIT="${KING_INFERENCE_PHP_MEMORY_LIMIT:-4G}" + +if [[ ! -f "$EXTENSION" ]]; then + echo "king-openai-router: King extension module not found at $EXTENSION" >&2 + exit 127 +fi + +if [[ ! -f "$INI" ]]; then + echo "king-openai-router: PHP INI not found at $INI" >&2 + exit 127 +fi + +export KING_ROOT="$ROOT" +export KING_INFERENCE_PHP_INI="$INI" +export PHP_INI_SCAN_DIR="${PHP_INI_SCAN_DIR:-}" +export KING_OPENAI_CONTEXT_POLICY="${KING_OPENAI_CONTEXT_POLICY:-full}" +export KING_OPENAI_DEFAULT_MAX_TOKENS="${KING_OPENAI_DEFAULT_MAX_TOKENS:-128}" +export KING_OPENAI_MAX_COMPLETION_TOKENS="${KING_OPENAI_MAX_COMPLETION_TOKENS:-512}" + +exec "$PHP_BIN" \ + -c "$INI" \ + -d "extension=$EXTENSION" \ + -d king.security_allow_config_override=1 \ + -d "memory_limit=$MEMORY_LIMIT" \ + -d display_errors=1 \ + -d error_reporting=32767 \ + "$ROOT/infra/inference/openai-router.php" diff --git a/bin/king-openai-router-start b/bin/king-openai-router-start new file mode 100755 index 000000000..a7142fc27 --- /dev/null +++ b/bin/king-openai-router-start @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +INI="${KING_INFERENCE_PHP_INI:-$ROOT/infra/inference/local-gpu.php.ini}" +CONFIRM="${KING_OPENAI_CONFIRM_START:-0}" + +usage() { + cat <<'EOF' +Usage: bin/king-openai-router-start --confirm + +Prints the selected local inference profile and current GPU state, then starts +the King OpenAI-compatible router on the configured host/port. +EOF +} + +for arg in "$@"; do + case "$arg" in + --confirm) + CONFIRM=1 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "king-openai-router-start: unknown argument: $arg" >&2 + usage >&2 + exit 2 + ;; + esac +done + +ini_value() { + local key="$1" + local fallback="${2:-}" + local value="" + + if [[ -f "$INI" ]]; then + value="$( + awk -F= -v key="$key" ' + { + lhs = $1 + gsub(/^[ \t]+|[ \t]+$/, "", lhs) + if (lhs == key) { + rhs = substr($0, index($0, "=") + 1) + gsub(/^[ \t]+|[ \t\r]+$/, "", rhs) + gsub(/^"|"$/, "", rhs) + print rhs + exit + } + } + ' "$INI" + )" + fi + + if [[ -n "$value" ]]; then + printf '%s\n' "$value" + else + printf '%s\n' "$fallback" + fi +} + +resolve_king_root() { + local value="$1" + value="${value//\$\{KING_ROOT\}/$ROOT}" + value="${value//\$KING_ROOT/$ROOT}" + printf '%s\n' "$value" +} + +gpu_snapshot() { + if ! command -v nvidia-smi >/dev/null 2>&1; then + printf 'unavailable,nvidia-smi-missing\n' + return + fi + + nvidia-smi \ + --query-gpu=temperature.gpu,utilization.gpu,memory.used,memory.total,power.draw \ + --format=csv,noheader,nounits 2>/dev/null \ + | head -n 1 \ + | awk -F, '{for (i=1;i<=NF;i++) {gsub(/^[ \t]+|[ \t]+$/, "", $i)}; print $1 "," $2 "," $3 "," $4 "," $5}' +} + +HOST="${KING_OPENAI_HOST:-127.0.0.1}" +PORT="${KING_OPENAI_PORT:-8080}" +PROFILE="$(ini_value king.inference_preferred_model_profile auto)" +MODEL_REGISTRY="$(ini_value king.inference_models "")" +GPU_ENABLED="$(ini_value king.gpu_bindings_enable 0)" +GPU_BACKEND="$(ini_value king.gpu_default_backend disabled)" +GPU_LAYERS="$(ini_value king.inference_gpu_max_gpu_layers 0)" +CPU_MODEL="$(ini_value king.inference_cpu_model_name gemma3:1b)" +GPU_MODEL="$(ini_value king.inference_gpu_model_name gemma4:12b)" +CPU_ARTIFACT="$(resolve_king_root "$(ini_value king.inference_cpu_model_artifact "$ROOT/var/inference-models/gemma3-1b.gguf")")" +GPU_ARTIFACT="$(resolve_king_root "$(ini_value king.inference_gpu_model_artifact "$ROOT/var/inference-models/gemma4-12b.gguf")")" +THERMAL_CEILING="$(ini_value king.inference_gpu_thermal_max_temperature_c 78)" +THERMAL_INTERVAL="$(ini_value king.inference_gpu_thermal_check_interval_sec 15)" +VRAM_RESERVE="$(ini_value king.inference_gpu_vram_reserve_mb 2048)" +MIN_FREE_VRAM="$(ini_value king.inference_gpu_min_free_vram_mb 4096)" +SYSTEM_RAM_OFFLOAD="$(ini_value king.inference_gpu_allow_system_ram_offload 0)" +SYSTEM_RAM_OFFLOAD_MAX="$(ini_value king.inference_gpu_system_ram_offload_max_mb 0)" +SYSTEM_RAM_OFFLOAD_MIN_FREE="$(ini_value king.inference_gpu_system_ram_offload_min_free_mb 8192)" +CONTEXT_TOKENS="$(ini_value king.inference_context_tokens 2048)" +KV_PAGE_TOKENS="$(ini_value king.inference_kv_page_tokens 16)" +KV_ELEMENT_BYTES="$(ini_value king.inference_kv_element_bytes 2)" +WITH_MEMORY="$(ini_value king.inference_with_memory 0)" +LLM_CACHE_ENABLE="$(ini_value king.inference_llm_cache_enable 0)" +LLM_CACHE_MIN_FREE="$(ini_value king.inference_llm_cache_min_free_mb 5120)" +CONTEXT_POLICY="${KING_OPENAI_CONTEXT_POLICY:-full}" +DEFAULT_MAX_TOKENS="${KING_OPENAI_DEFAULT_MAX_TOKENS:-128}" +MAX_COMPLETION_TOKENS="${KING_OPENAI_MAX_COMPLETION_TOKENS:-512}" + +IFS=, read -r GPU_TEMP GPU_UTIL GPU_MEM_USED GPU_MEM_TOTAL GPU_POWER <<<"$(gpu_snapshot)" + +cat <&2 + exit 2 +fi + +exec "$ROOT/bin/king-openai-router" diff --git a/bin/king-openai-smoke b/bin/king-openai-smoke new file mode 100755 index 000000000..9a3bef6b5 --- /dev/null +++ b/bin/king-openai-smoke @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +HOST="${KING_OPENAI_SMOKE_HOST:-127.0.0.1}" +PORT="${KING_OPENAI_SMOKE_PORT:-18090}" +URL="http://${HOST}:${PORT}/v1" +MODEL="${KING_OPENAI_SMOKE_MODEL:-gemma4:12b}" +MESSAGE="${KING_OPENAI_SMOKE_MESSAGE:-Hello}" +MAX_TOKENS="${KING_OPENAI_SMOKE_MAX_TOKENS:-2}" +START_TIMEOUT_SEC="${KING_OPENAI_SMOKE_START_TIMEOUT_SEC:-45}" +REQUEST_TIMEOUT_SEC="${KING_OPENAI_SMOKE_REQUEST_TIMEOUT_SEC:-30}" +LOG_FILE="$(mktemp "${TMPDIR:-/tmp}/king-openai-smoke.XXXXXX.log")" + +cleanup() { + local status=$? + if [[ -n "${ROUTER_PID:-}" ]] && kill -0 "$ROUTER_PID" 2>/dev/null; then + kill "$ROUTER_PID" 2>/dev/null || true + wait "$ROUTER_PID" 2>/dev/null || true + fi + if [[ "$status" -ne 0 ]]; then + echo "king-openai-smoke: router log follows:" >&2 + tail -n 80 "$LOG_FILE" >&2 || true + fi + rm -f "$LOG_FILE" +} +trap cleanup EXIT INT TERM + +KING_OPENAI_HOST="$HOST" \ +KING_OPENAI_PORT="$PORT" \ +KING_OPENAI_DEFAULT_MAX_TOKENS="$MAX_TOKENS" \ +KING_OPENAI_MAX_COMPLETION_TOKENS="$MAX_TOKENS" \ +KING_OPENAI_CONTEXT_POLICY="${KING_OPENAI_CONTEXT_POLICY:-full}" \ +KING_OPENAI_CONFIRM_START=1 \ + "$ROOT/bin/king-openai-router-start" --confirm >"$LOG_FILE" 2>&1 & +ROUTER_PID=$! + +deadline=$((SECONDS + START_TIMEOUT_SEC)) +until curl -fsS --max-time 1 "${URL}/models" >/dev/null 2>&1; do + if ! kill -0 "$ROUTER_PID" 2>/dev/null; then + echo "king-openai-smoke: router exited before /v1/models became reachable" >&2 + exit 1 + fi + if (( SECONDS >= deadline )); then + echo "king-openai-smoke: timed out waiting for ${URL}/models" >&2 + exit 1 + fi + sleep 0.5 +done + +payload="$( + KING_OPENAI_SMOKE_MODEL="$MODEL" \ + KING_OPENAI_SMOKE_MESSAGE="$MESSAGE" \ + KING_OPENAI_SMOKE_MAX_TOKENS="$MAX_TOKENS" \ + php -r ' + echo json_encode([ + "model" => getenv("KING_OPENAI_SMOKE_MODEL") ?: "gemma4:12b", + "stream" => true, + "max_tokens" => (int) (getenv("KING_OPENAI_SMOKE_MAX_TOKENS") ?: 2), + "messages" => [[ + "role" => "user", + "content" => getenv("KING_OPENAI_SMOKE_MESSAGE") ?: "Hello", + ]], + ], JSON_UNESCAPED_SLASHES); + ' +)" + +body="$( + curl -sS --max-time "$REQUEST_TIMEOUT_SEC" -N \ + -H 'content-type: application/json' \ + -d "$payload" \ + "${URL}/chat/completions" +)" + +if [[ "$body" != *'"delta":{"content":'* ]]; then + echo "king-openai-smoke: response did not contain a streamed content delta" >&2 + exit 1 +fi +if [[ "$body" != *'[DONE]'* ]]; then + echo "king-openai-smoke: response did not finish with [DONE]" >&2 + exit 1 +fi +if [[ "$body" == *'event: error'* ]]; then + echo "king-openai-smoke: response contained an SSE error event" >&2 + exit 1 +fi + + KING_OPENAI_SMOKE_URL="$URL" \ + KING_OPENAI_SMOKE_MODEL="$MODEL" \ + php -r ' + $body = stream_get_contents(STDIN); + echo json_encode([ + "ok" => true, + "url" => getenv("KING_OPENAI_SMOKE_URL") ?: "", + "model" => getenv("KING_OPENAI_SMOKE_MODEL") ?: "gemma4:12b", + "bytes" => strlen($body), + "has_content_delta" => str_contains($body, "\"delta\":{\"content\":"), + "has_done" => str_contains($body, "[DONE]"), + "has_error_event" => str_contains($body, "event: error"), + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), "\n"; +' <<<"$body" diff --git a/demo/chat/.env.example b/demo/chat/.env.example new file mode 100644 index 000000000..123eb238c --- /dev/null +++ b/demo/chat/.env.example @@ -0,0 +1,3 @@ +KING_MODEL_BLOBS=/usr/share/ollama/.ollama/models/blobs +KING_CHAT_UID=1000 +KING_CHAT_GID=1000 diff --git a/demo/chat/README.md b/demo/chat/README.md new file mode 100644 index 000000000..a47f5bc0b --- /dev/null +++ b/demo/chat/README.md @@ -0,0 +1,101 @@ +# King Chat Test App + +This folder contains a small browser chat that talks to the local King +OpenAI-compatible inference router. It is a local runtime harness for the King +router, not an Ollama client and not a hosted API wrapper. + +## Requirements + +From the repository root, the chat expects: + +- `extension/modules/king.so` built from the current tree. +- Local Docker image `king-local:php8.4-inference`. +- Local GGUF artifact at `var/inference-models/gemma3-1b.gguf`. +- Docker Compose and, for GPU execution, NVIDIA container runtime support. + +Build the extension after native code changes: + +```bash +make build +``` + +Build the local runtime image when `docker image inspect +king-local:php8.4-inference` fails. If PHP 8.4 package artifacts are missing, +create them first: + +```bash +make release-package +docker build --load \ + -f infra/php-runtime.Dockerfile \ + -t king-local:php8.4-inference \ + --build-arg PHP_VERSION=8.4 \ + . +``` + +## Model Artifact + +King needs a direct local GGUF path. Model files are ignored by git and should +stay in `var/`, a model registry, or an object store. + +With an already approved GGUF file: + +```bash +mkdir -p var/inference-models +ln -sf /absolute/path/to/gemma3-1b.gguf var/inference-models/gemma3-1b.gguf +``` + +With a local Ollama install as the download source: + +```bash +ollama pull gemma3:1b +ollama show --modelfile gemma3:1b +mkdir -p var/inference-models +ln -sf /path/to/ollama/models/blobs/sha256-... var/inference-models/gemma3-1b.gguf +``` + +Use the `FROM` digest shown by `ollama show --modelfile gemma3:1b` to pick the +matching blob. Common blob roots are `/usr/share/ollama/.ollama/models/blobs` +and `$HOME/.ollama/models/blobs`. The router reads the linked GGUF directly; it +does not call the Ollama API during inference. + +If the symlink points into a blob directory outside the repository, expose that +directory to Compose: + +```bash +cd demo/chat +cp .env.example .env +# Edit KING_MODEL_BLOBS when your blob root differs from the default. +``` + +## Start + +```bash +cd demo/chat +docker compose up -d +``` + +Open: + +- Chat UI: http://127.0.0.1:19480 +- OpenAI-compatible base URL: http://127.0.0.1:8080/v1 +- King model list: http://127.0.0.1:19481/v1/models + +## Runtime Shape + +- `inference` runs `bin/king-openai-router-start --confirm` from the repository. +- `chat` runs a PHP backend that validates browser messages and proxies them to + `/v1/chat/completions`. +- The browser receives streamed Server-Sent Events from the PHP backend. +- Threads are stored in `demo/chat/var/chat.sqlite`, which is local runtime + state and ignored by git. + +The router start log prints the effective profile, CPU/GPU model artifact, +context policy, VRAM guardrails, thermal ceiling, and streaming flags. Check +that log first when generation is slow or the wrong profile is selected. + +## Stop + +```bash +cd demo/chat +docker compose down +``` diff --git a/demo/chat/backend/router.php b/demo/chat/backend/router.php new file mode 100644 index 000000000..81248cd0c --- /dev/null +++ b/demo/chat/backend/router.php @@ -0,0 +1,790 @@ + false, + 'error' => [ + 'code' => $code, + 'message' => $message, + ], + ]); +} + +function chat_http_status_from_headers(array $headers): ?int +{ + foreach ($headers as $header) { + if (preg_match('#^HTTP/\S+\s+(\d{3})#', (string) $header, $match) === 1) { + return (int) $match[1]; + } + } + return null; +} + +function chat_runtime_models(): array +{ + $modelsUrl = chat_env_string('KING_CHAT_MODELS_URL', 'http://inference:8080/v1/models'); + $context = stream_context_create(['http' => ['method' => 'GET', 'timeout' => 2, 'ignore_errors' => true]]); + $raw = @file_get_contents($modelsUrl, false, $context); + $status = chat_http_status_from_headers($http_response_header ?? []); + if ($status !== 200 || !is_string($raw) || trim($raw) === '') { + return []; + } + + $decoded = json_decode($raw, true); + if (!is_array($decoded) || !is_array($decoded['data'] ?? null)) { + return []; + } + + $models = []; + foreach ($decoded['data'] as $model) { + if (!is_array($model) || !is_string($model['id'] ?? null) || trim($model['id']) === '') { + continue; + } + $models[] = trim($model['id']); + } + + return array_values(array_unique($models)); +} + +function chat_runtime_model(): string +{ + $configured = chat_env_string('KING_CHAT_MODEL', 'gemma4:12b'); + $models = chat_runtime_models(); + if (in_array($configured, $models, true)) { + return $configured; + } + return $models[0] ?? $configured; +} + +function chat_read_json_body(): array +{ + $raw = file_get_contents('php://input'); + if (!is_string($raw) || trim($raw) === '') { + chat_error(400, 'empty_body', 'Request body must be a JSON object.'); + exit; + } + + $decoded = json_decode($raw, true); + if (!is_array($decoded)) { + chat_error(400, 'invalid_json', 'Request body must be valid JSON.'); + exit; + } + + return $decoded; +} + +function chat_normalize_messages(mixed $messages, int $maxMessages, int $maxChars): array +{ + if (!is_array($messages) || $messages === []) { + chat_error(400, 'messages_required', 'At least one user message is required.'); + exit; + } + if (count($messages) > $maxMessages) { + chat_error(413, 'too_many_messages', 'The chat history is too long for this test route.'); + exit; + } + + $normalized = []; + foreach ($messages as $message) { + if (!is_array($message)) { + chat_error(400, 'invalid_message', 'Each message must be an object.'); + exit; + } + + $role = $message['role'] ?? null; + $content = $message['content'] ?? null; + if (!in_array($role, ['system', 'user', 'assistant'], true) || !is_string($content)) { + chat_error(400, 'invalid_message_shape', 'Messages require role system/user/assistant and string content.'); + exit; + } + + $content = trim($content); + if ($content === '') { + continue; + } + if (mb_strlen($content, 'UTF-8') > $maxChars) { + chat_error(413, 'message_too_large', 'A message exceeds the configured character limit.'); + exit; + } + + $normalized[] = [ + 'role' => $role, + 'content' => $content, + ]; + } + + if ($normalized === [] || end($normalized)['role'] !== 'user') { + chat_error(400, 'last_message_must_be_user', 'The last non-empty message must be a user message.'); + exit; + } + + return array_values($normalized); +} + +function chat_system_instruction(): ?array +{ + $content = chat_env_string('KING_CHAT_SYSTEM_PROMPT', ''); + if ($content === '') { + return null; + } + + return [ + 'role' => 'system', + 'content' => $content, + ]; +} + +function chat_latest_user_text(array $messages): string +{ + for ($index = count($messages) - 1; $index >= 0; $index--) { + $message = $messages[$index]; + if (($message['role'] ?? null) === 'user' && is_string($message['content'] ?? null)) { + return $message['content']; + } + } + return ''; +} + +function chat_utf8_chars(string $value): array +{ + if ($value === '') { + return []; + } + return preg_split('//u', $value, -1, PREG_SPLIT_NO_EMPTY) ?: []; +} + +function chat_count_occurrences(string $text, string $needle, bool $caseSensitive): int +{ + if (!$caseSensitive) { + $text = mb_strtolower($text, 'UTF-8'); + $needle = mb_strtolower($needle, 'UTF-8'); + } + + $count = 0; + foreach (chat_utf8_chars($text) as $char) { + if ($char === $needle) { + $count++; + } + } + return $count; +} + +function chat_extract_count_occurrences_task(string $text): ?array +{ + $normalized = trim(preg_replace('/\s+/u', ' ', $text) ?? $text); + if ($normalized === '') { + return null; + } + + $patterns = [ + '/\bhow many (?:letters?|characters?) ["\']?(\p{L}|\p{N})["\']? (?:are )?in (?:the )?(?:word|string )?["\']?([\p{L}\p{N}_ -]{1,80})["\']?\??$/iu', + '/\bhow often (?:does|is) ["\']?(\p{L}|\p{N})["\']? (?:appear|occurs?|contained) in (?:the )?(?:word|string )?["\']?([\p{L}\p{N}_ -]{1,80})["\']?\??$/iu', + '/\bwie ?viele (?:buchstaben|zeichen) ["\']?(\p{L}|\p{N})["\']? (?:hat|sind in|kommen in) (?:dem |der |das |wort |string )?["\']?([\p{L}\p{N}_ -]{1,80})["\']?\??$/iu', + '/\bwie oft (?:kommt|ist) ["\']?(\p{L}|\p{N})["\']? in (?:dem |der |das |wort |string )?["\']?([\p{L}\p{N}_ -]{1,80})["\']? (?:vor|enthalten)\??$/iu', + ]; + + foreach ($patterns as $pattern) { + if (preg_match($pattern, $normalized, $match) !== 1) { + continue; + } + + $needle = trim($match[1]); + $subject = trim($match[2], " \t\n\r\0\x0B\"'"); + if ($needle === '' || $subject === '') { + continue; + } + + return [ + 'operation' => 'count_occurrences', + 'subject' => $subject, + 'needle' => $needle, + 'case_sensitive' => true, + 'result' => chat_count_occurrences($subject, $needle, true), + ]; + } + + return null; +} + +function chat_miniops_verifier_message(array $messages): ?array +{ + if (chat_env_string('KING_CHAT_MINIOPS_ENABLE', '0') !== '1') { + return null; + } + + $task = chat_extract_count_occurrences_task(chat_latest_user_text($messages)); + if ($task === null) { + return null; + } + + $program = [ + 'version' => 1, + 'operation' => $task['operation'], + 'input' => [ + 'text' => $task['subject'], + 'needle' => $task['needle'], + 'case_sensitive' => $task['case_sensitive'], + 'unit' => 'unicode_codepoint', + ], + 'result' => $task['result'], + 'safety' => [ + 'filesystem' => false, + 'network' => false, + 'cli' => false, + 'eval' => false, + ], + ]; + + return [ + 'role' => 'system', + 'content' => "King deterministic mini-op verifier:\n" + . "Use this verified result. Do not estimate this task from model memory.\n" + . "~~~king-miniops\n" + . json_encode($program, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) + . "\n~~~", + ]; +} + +function chat_prepare_model_messages(array $messages): array +{ + $prepared = []; + $instruction = chat_system_instruction(); + if ($instruction !== null) { + $prepared[] = $instruction; + } + + $verifier = chat_miniops_verifier_message($messages); + if ($verifier !== null) { + $prepared[] = $verifier; + } + foreach ($messages as $message) { + if (($message['role'] ?? null) === 'system') { + continue; + } + $prepared[] = $message; + } + return $prepared; +} + +function chat_now(): string +{ + return gmdate('Y-m-d\TH:i:s\Z'); +} + +function chat_db_path(): string +{ + return chat_env_string('KING_CHAT_DB_PATH', dirname(__DIR__) . '/var/chat.sqlite'); +} + +function chat_db(): PDO +{ + static $pdo = null; + if ($pdo instanceof PDO) { + return $pdo; + } + + $path = chat_db_path(); + $directory = dirname($path); + if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) { + throw new RuntimeException('Could not create chat database directory.'); + } + + $pdo = new PDO('sqlite:' . $path, null, null, [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_STRINGIFY_FETCHES => false, + ]); + $pdo->exec('PRAGMA foreign_keys = ON'); + $pdo->exec('PRAGMA journal_mode = WAL'); + $pdo->exec('PRAGMA busy_timeout = 3000'); + $pdo->exec( + 'CREATE TABLE IF NOT EXISTS chat_threads ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + )' + ); + $pdo->exec( + 'CREATE TABLE IF NOT EXISTS chat_messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + thread_id TEXT NOT NULL, + role TEXT NOT NULL CHECK (role IN (\'user\', \'assistant\')), + content TEXT NOT NULL, + created_at TEXT NOT NULL, + FOREIGN KEY (thread_id) REFERENCES chat_threads(id) ON DELETE CASCADE + )' + ); + $pdo->exec('CREATE INDEX IF NOT EXISTS chat_messages_thread_created_idx ON chat_messages(thread_id, id)'); + $pdo->exec('CREATE INDEX IF NOT EXISTS chat_threads_updated_idx ON chat_threads(updated_at DESC)'); + + return $pdo; +} + +function chat_thread_id(): string +{ + return 'thr_' . bin2hex(random_bytes(12)); +} + +function chat_normalize_thread_id(mixed $value): string +{ + if (!is_string($value) || preg_match('/^thr_[a-f0-9]{24}$/', $value) !== 1) { + chat_error(400, 'invalid_thread_id', 'Thread id is invalid.'); + exit; + } + return $value; +} + +function chat_title_from_text(string $text): string +{ + $line = trim((string) preg_replace('/\s+/u', ' ', $text)); + if ($line === '') { + return 'New chat'; + } + if (mb_strlen($line, 'UTF-8') > 72) { + $line = mb_substr($line, 0, 69, 'UTF-8') . '...'; + } + return $line; +} + +function chat_thread_row(string $threadId): ?array +{ + $stmt = chat_db()->prepare( + 'SELECT t.id, t.title, t.created_at, t.updated_at, + (SELECT COUNT(*) FROM chat_messages m WHERE m.thread_id = t.id) AS message_count + FROM chat_threads t + WHERE t.id = :id' + ); + $stmt->execute([':id' => $threadId]); + $row = $stmt->fetch(); + return is_array($row) ? $row : null; +} + +function chat_require_thread(string $threadId): array +{ + $thread = chat_thread_row($threadId); + if ($thread === null) { + chat_error(404, 'thread_not_found', 'Thread was not found.'); + exit; + } + return $thread; +} + +function chat_create_thread(string $title = 'New chat'): array +{ + $threadId = chat_thread_id(); + $now = chat_now(); + $stmt = chat_db()->prepare( + 'INSERT INTO chat_threads (id, title, created_at, updated_at) + VALUES (:id, :title, :created_at, :updated_at)' + ); + $stmt->execute([ + ':id' => $threadId, + ':title' => chat_title_from_text($title), + ':created_at' => $now, + ':updated_at' => $now, + ]); + return chat_require_thread($threadId); +} + +function chat_update_thread_timestamp(string $threadId): void +{ + $stmt = chat_db()->prepare('UPDATE chat_threads SET updated_at = :updated_at WHERE id = :id'); + $stmt->execute([':updated_at' => chat_now(), ':id' => $threadId]); +} + +function chat_maybe_title_thread_from_user(string $threadId, string $content): void +{ + $thread = chat_require_thread($threadId); + if (($thread['message_count'] ?? 0) > 0 || ($thread['title'] ?? '') !== 'New chat') { + return; + } + $stmt = chat_db()->prepare('UPDATE chat_threads SET title = :title, updated_at = :updated_at WHERE id = :id'); + $stmt->execute([ + ':title' => chat_title_from_text($content), + ':updated_at' => chat_now(), + ':id' => $threadId, + ]); +} + +function chat_insert_message(string $threadId, string $role, string $content): void +{ + if (!in_array($role, ['user', 'assistant'], true)) { + throw new InvalidArgumentException('Invalid chat message role.'); + } + $stmt = chat_db()->prepare( + 'INSERT INTO chat_messages (thread_id, role, content, created_at) + VALUES (:thread_id, :role, :content, :created_at)' + ); + $stmt->execute([ + ':thread_id' => $threadId, + ':role' => $role, + ':content' => $content, + ':created_at' => chat_now(), + ]); + chat_update_thread_timestamp($threadId); +} + +function chat_thread_messages(string $threadId): array +{ + chat_require_thread($threadId); + $stmt = chat_db()->prepare( + 'SELECT role, content, created_at + FROM chat_messages + WHERE thread_id = :thread_id + ORDER BY id ASC' + ); + $stmt->execute([':thread_id' => $threadId]); + + $messages = []; + foreach ($stmt->fetchAll() as $row) { + $messages[] = [ + 'role' => (string) $row['role'], + 'content' => (string) $row['content'], + 'created_at' => (string) $row['created_at'], + ]; + } + return $messages; +} + +function chat_api_list_threads(): void +{ + $stmt = chat_db()->query( + 'SELECT t.id, t.title, t.created_at, t.updated_at, + (SELECT COUNT(*) FROM chat_messages m WHERE m.thread_id = t.id) AS message_count + FROM chat_threads t + ORDER BY t.updated_at DESC + LIMIT 100' + ); + chat_json_response(200, [ + 'ok' => true, + 'threads' => $stmt->fetchAll(), + ]); +} + +function chat_api_create_thread(): void +{ + $body = []; + $raw = file_get_contents('php://input'); + if (is_string($raw) && trim($raw) !== '') { + $decoded = json_decode($raw, true); + if (!is_array($decoded)) { + chat_error(400, 'invalid_json', 'Request body must be valid JSON.'); + return; + } + $body = $decoded; + } + + $thread = chat_create_thread(is_string($body['title'] ?? null) ? $body['title'] : 'New chat'); + chat_json_response(201, ['ok' => true, 'thread' => $thread, 'messages' => []]); +} + +function chat_api_get_thread(string $threadId): void +{ + chat_json_response(200, [ + 'ok' => true, + 'thread' => chat_require_thread($threadId), + 'messages' => chat_thread_messages($threadId), + ]); +} + +function chat_api_update_thread(string $threadId): void +{ + chat_require_thread($threadId); + $body = chat_read_json_body(); + $title = is_string($body['title'] ?? null) ? chat_title_from_text($body['title']) : ''; + if ($title === '') { + chat_error(400, 'title_required', 'Thread title is required.'); + return; + } + + $stmt = chat_db()->prepare('UPDATE chat_threads SET title = :title, updated_at = :updated_at WHERE id = :id'); + $stmt->execute([':title' => $title, ':updated_at' => chat_now(), ':id' => $threadId]); + chat_json_response(200, ['ok' => true, 'thread' => chat_require_thread($threadId)]); +} + +function chat_api_delete_thread(string $threadId): void +{ + chat_require_thread($threadId); + $stmt = chat_db()->prepare('DELETE FROM chat_threads WHERE id = :id'); + $stmt->execute([':id' => $threadId]); + chat_json_response(200, ['ok' => true]); +} + +function chat_user_message_from_body(array $body): string +{ + if (is_string($body['message'] ?? null)) { + $message = trim($body['message']); + if ($message !== '') { + return $message; + } + } + + $messages = $body['messages'] ?? null; + if (is_array($messages)) { + $normalized = chat_normalize_messages( + $messages, + chat_env_int('KING_CHAT_MAX_MESSAGES', 40), + chat_env_int('KING_CHAT_MAX_MESSAGE_CHARS', 12000) + ); + return chat_latest_user_text($normalized); + } + + chat_error(400, 'message_required', 'A user message is required.'); + exit; +} + +function chat_send_sse(string $event, array $payload): void +{ + echo 'event: ', $event, "\n"; + echo 'data: ', json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), "\n\n"; + @ob_flush(); + flush(); +} + +function chat_health(): void +{ + $modelsUrl = chat_env_string('KING_CHAT_MODELS_URL', 'http://inference:8080/v1/models'); + $context = stream_context_create(['http' => ['method' => 'GET', 'timeout' => 2, 'ignore_errors' => true]]); + $raw = @file_get_contents($modelsUrl, false, $context); + $status = chat_http_status_from_headers($http_response_header ?? []); + + chat_json_response($status === 200 ? 200 : 503, [ + 'ok' => $status === 200, + 'models_status' => $status, + 'model' => [ + 'configured' => chat_env_string('KING_CHAT_MODEL', 'gemma4:12b'), + 'effective' => chat_runtime_model(), + 'available' => chat_runtime_models(), + ], + 'storage' => [ + 'driver' => 'sqlite', + 'ready' => is_file(chat_db_path()) || is_dir(dirname(chat_db_path())), + ], + ]); +} + +function chat_stream_completion(): void +{ + if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') { + chat_error(405, 'method_not_allowed', 'Use POST.'); + return; + } + + $body = chat_read_json_body(); + $threadId = isset($body['thread_id']) ? chat_normalize_thread_id($body['thread_id']) : chat_create_thread()['id']; + chat_require_thread($threadId); + $userMessage = chat_user_message_from_body($body); + if (mb_strlen($userMessage, 'UTF-8') > chat_env_int('KING_CHAT_MAX_MESSAGE_CHARS', 12000)) { + chat_error(413, 'message_too_large', 'The message exceeds the configured character limit.'); + return; + } + chat_maybe_title_thread_from_user($threadId, $userMessage); + chat_insert_message($threadId, 'user', $userMessage); + + $storedMessages = chat_thread_messages($threadId); + $modelMessages = []; + foreach ($storedMessages as $message) { + $modelMessages[] = [ + 'role' => $message['role'], + 'content' => $message['content'], + ]; + } + + $maxTokens = min( + chat_env_int('KING_CHAT_MAX_TOKENS', 1024), + chat_env_int('KING_CHAT_MAX_COMPLETION_TOKENS', 2048) + ); + + $payload = [ + 'model' => chat_runtime_model(), + 'stream' => true, + 'messages' => chat_prepare_model_messages($modelMessages), + 'max_tokens' => $maxTokens, + 'temperature' => 0, + 'stream_options' => ['include_usage' => true], + ]; + + header('content-type: text/event-stream; charset=utf-8'); + header('cache-control: no-cache'); + header('x-accel-buffering: no'); + + chat_send_sse('status', ['state' => 'connecting']); + chat_send_sse('thread', ['thread' => chat_require_thread($threadId)]); + + $context = stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => "content-type: application/json\r\naccept: text/event-stream\r\nconnection: close\r\n", + 'content' => json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), + 'timeout' => chat_env_int('KING_CHAT_REQUEST_TIMEOUT_SECONDS', 120), + 'ignore_errors' => true, + ], + ]); + + $stream = @fopen(chat_env_string('KING_CHAT_INFERENCE_URL', 'http://inference:8080/v1/chat/completions'), 'rb', false, $context); + if (!is_resource($stream)) { + chat_send_sse('error', ['message' => 'Could not connect to King inference route.']); + return; + } + + $status = null; + foreach (($http_response_header ?? []) as $header) { + if (preg_match('#^HTTP/\S+\s+(\d{3})#', $header, $match) === 1) { + $status = (int) $match[1]; + break; + } + } + if ($status === null || $status < 200 || $status >= 300) { + $errorBody = stream_get_contents($stream); + fclose($stream); + chat_send_sse('error', [ + 'message' => 'King inference route returned an error.', + 'status' => $status, + 'body' => is_string($errorBody) ? mb_substr($errorBody, 0, 2000, 'UTF-8') : '', + ]); + return; + } + + chat_send_sse('status', ['state' => 'streaming']); + $buffer = ''; + $streamEvent = 'message'; + $streamError = null; + $finishReason = null; + while (!feof($stream)) { + $line = fgets($stream); + if (!is_string($line)) { + break; + } + $line = trim($line); + if ($line === '') { + continue; + } + if (str_starts_with($line, 'event:')) { + $streamEvent = trim(substr($line, 6)); + continue; + } + if (!str_starts_with($line, 'data:')) { + continue; + } + + $data = trim(substr($line, 5)); + if ($data === '[DONE]') { + break; + } + + $event = json_decode($data, true); + if (!is_array($event)) { + continue; + } + + if ($streamEvent === 'error') { + $message = $event['message'] ?? 'King inference stream returned an error.'; + $streamError = is_string($message) ? $message : 'King inference stream returned an error.'; + chat_send_sse('error', ['message' => $streamError]); + break; + } + + $choice = $event['choices'][0] ?? null; + if (!is_array($choice)) { + continue; + } + + $delta = $choice['delta'] ?? null; + if (is_array($delta) && is_string($delta['content'] ?? null) && $delta['content'] !== '') { + $buffer .= $delta['content']; + chat_send_sse('delta', ['content' => $delta['content']]); + } + + if (is_string($choice['finish_reason'] ?? null)) { + $finishReason = $choice['finish_reason']; + chat_send_sse('done', [ + 'finish_reason' => $choice['finish_reason'], + 'chars' => mb_strlen($buffer, 'UTF-8'), + ]); + } + } + + fclose($stream); + if ($streamError !== null) { + return; + } + if ($buffer === '') { + chat_send_sse('error', [ + 'message' => 'King inference returned HTTP 200 but no assistant content.', + 'finish_reason' => $finishReason, + ]); + return; + } + + chat_insert_message($threadId, 'assistant', $buffer); + chat_send_sse('thread', ['thread' => chat_require_thread($threadId)]); +} + +$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH); +if ($path === '/api/health') { + chat_health(); + return; +} +if ($path === '/api/threads' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'GET') { + chat_api_list_threads(); + return; +} +if ($path === '/api/threads' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') { + chat_api_create_thread(); + return; +} +if (is_string($path) && preg_match('#^/api/threads/(thr_[a-f0-9]{24})$#', $path, $match) === 1) { + $method = $_SERVER['REQUEST_METHOD'] ?? ''; + if ($method === 'GET') { + chat_api_get_thread($match[1]); + return; + } + if ($method === 'PATCH') { + chat_api_update_thread($match[1]); + return; + } + if ($method === 'DELETE') { + chat_api_delete_thread($match[1]); + return; + } + chat_error(405, 'method_not_allowed', 'Unsupported thread method.'); + return; +} +if ($path === '/api/chat') { + chat_stream_completion(); + return; +} + +return false; diff --git a/demo/chat/docker-compose.yml b/demo/chat/docker-compose.yml new file mode 100644 index 000000000..98a44252d --- /dev/null +++ b/demo/chat/docker-compose.yml @@ -0,0 +1,62 @@ +services: + inference: + image: king-local:php8.4-inference + working_dir: /workspace + command: ["./bin/king-openai-router-start", "--confirm"] + environment: + KING_OPENAI_HOST: 0.0.0.0 + KING_OPENAI_PORT: 8080 + KING_OPENAI_CONFIRM_START: "1" + KING_OPENAI_CONTEXT_POLICY: full + KING_OPENAI_DEFAULT_MAX_TOKENS: "64" + KING_OPENAI_MAX_COMPLETION_TOKENS: "128" + KING_OPENAI_CODER_INSTRUCTION_WRAPPER: "0" + KING_INFERENCE_PHP_INI: /workspace/infra/inference/local-gpu.php.ini + KING_EXTENSION: /workspace/extension/modules/king.so + PHP_INI_SCAN_DIR: "" + LD_LIBRARY_PATH: /usr/local/cuda/targets/x86_64-linux/lib:/opt/king/package/runtime + volumes: + - ../..:/workspace + - /usr/local/cuda/targets/x86_64-linux/lib:/usr/local/cuda/targets/x86_64-linux/lib:ro + - ${KING_MODEL_BLOBS:-/usr/share/ollama/.ollama/models/blobs}:/usr/share/ollama/.ollama/models/blobs:ro + ports: + - "8080:8080" + - "19481:8080" + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + healthcheck: + test: ["CMD", "php", "-r", "exit(@file_get_contents('http://127.0.0.1:8080/v1/models') === false ? 1 : 0);"] + interval: 10s + timeout: 3s + retries: 12 + start_period: 30s + + chat: + image: king-local:php8.4-inference + working_dir: /app + user: "${KING_CHAT_UID:-1000}:${KING_CHAT_GID:-1000}" + command: ["php", "-d", "variables_order=EGPCS", "-S", "0.0.0.0:8090", "-t", "public", "backend/router.php"] + environment: + KING_CHAT_INFERENCE_URL: http://inference:8080/v1/chat/completions + KING_CHAT_MODELS_URL: http://inference:8080/v1/models + KING_CHAT_MODEL: gemma3:1b + KING_CHAT_MAX_MESSAGES: "40" + KING_CHAT_MAX_MESSAGE_CHARS: "12000" + KING_CHAT_MAX_TOKENS: "64" + KING_CHAT_MAX_COMPLETION_TOKENS: "128" + KING_CHAT_REQUEST_TIMEOUT_SECONDS: "240" + KING_CHAT_DB_PATH: /app/var/chat.sqlite + volumes: + - ./public:/app/public:ro + - ./backend:/app/backend:ro + - ./var:/app/var + ports: + - "19480:8090" + depends_on: + inference: + condition: service_healthy diff --git a/demo/chat/public/app.js b/demo/chat/public/app.js new file mode 100644 index 000000000..effaaf209 --- /dev/null +++ b/demo/chat/public/app.js @@ -0,0 +1,550 @@ +const messagesEl = document.getElementById('messages'); +const formEl = document.getElementById('chatForm'); +const inputEl = document.getElementById('promptInput'); +const sendButton = document.getElementById('sendButton'); +const deleteThreadButton = document.getElementById('deleteThreadButton'); +const newThreadButton = document.getElementById('newThreadButton'); +const threadListEl = document.getElementById('threadList'); +const statusDot = document.getElementById('statusDot'); +const statusText = document.getElementById('statusText'); + +let history = []; +let activeController = null; +let threads = []; +let currentThreadId = null; + +function escapeHtml(value) { + return value + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +function safeLinkTarget(href) { + const trimmed = href.trim(); + if (/^(https?:|mailto:|\/|#)/i.test(trimmed)) { + return trimmed; + } + return '#'; +} + +function highlightCode(source, language = '') { + const lang = language.toLowerCase(); + let html = escapeHtml(source); + + if (['json', 'iibin'].includes(lang)) { + html = html + .replace(/("[^&]*?")(\s*:)/g, '$1$2') + .replace(/:\s*(".*?")/g, ': $1') + .replace(/\b(true|false|null)\b/g, '$1') + .replace(/\b-?\d+(?:\.\d+)?\b/g, '$&'); + return html; + } + + if (['php', 'js', 'javascript', 'ts', 'typescript'].includes(lang)) { + html = html + .replace(/(\/\/.*)$/gm, '$1') + .replace(/(".*?"|'.*?'|`.*?`)/g, '$1') + .replace(/\b(function|return|class|public|private|protected|static|const|let|var|if|else|foreach|for|while|true|false|null|new|try|catch|throw|bool|int|string|array|void)\b/g, '$1') + .replace(/\b-?\d+(?:\.\d+)?\b/g, '$&'); + return html; + } + + if (['html', 'xml', 'svg'].includes(lang)) { + html = html + .replace(/(<\/?)([A-Za-z0-9:_-]+)/g, '$1$2') + .replace(/([A-Za-z0-9:_-]+)=(".*?")/g, '$1=$2'); + return html; + } + + if (['css', 'scss'].includes(lang)) { + html = html + .replace(/([.#]?[A-Za-z0-9_-]+)(\s*\{)/g, '$1$2') + .replace(/([A-Za-z-]+)(\s*:)/g, '$1$2') + .replace(/(#(?:[0-9a-f]{3}){1,2}\b|\b\d+(?:px|rem|em|%)?\b)/gi, '$1'); + return html; + } + + if (['bash', 'sh', 'shell'].includes(lang)) { + html = html + .replace(/(^|\s)(sudo|docker|php|curl|git|make|npm|composer|export|cd|cp|mv|rm)(?=\s|$)/g, '$1$2') + .replace(/(--?[A-Za-z0-9_-]+)/g, '$1') + .replace(/(".*?"|'.*?')/g, '$1'); + return html; + } + + return html; +} + +function createCodeBox(code, language = 'text') { + const box = document.createElement('figure'); + box.className = 'codebox'; + const header = document.createElement('figcaption'); + header.textContent = language || 'text'; + const pre = document.createElement('pre'); + const codeEl = document.createElement('code'); + codeEl.className = `language-${language || 'text'}`; + codeEl.innerHTML = highlightCode(code, language); + pre.append(codeEl); + box.append(header, pre); + return box; +} + +function renderInlineMarkdown(text) { + const template = document.createElement('template'); + let html = escapeHtml(text); + const inlineCodes = []; + html = html.replace(/`([^`]+)`/g, (_, code) => { + const index = inlineCodes.push(`${code}`) - 1; + return `\u0000INLINE${index}\u0000`; + }); + html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => { + const safeHref = escapeHtml(safeLinkTarget(href)); + return `${label}`; + }); + html = html + .replace(/\*\*([^*]+)\*\*/g, '$1') + .replace(/\b_([^_]+)_\b/g, '$1'); + html = html.replace(/\u0000INLINE(\d+)\u0000/g, (_, index) => inlineCodes[Number(index)] || ''); + template.innerHTML = html; + return template.content; +} + +function appendParagraph(container, lines) { + const text = lines.join(' ').trim(); + if (text === '') { + return; + } + const paragraph = document.createElement('p'); + paragraph.append(renderInlineMarkdown(text)); + container.append(paragraph); +} + +function renderMarkdownDocument(source) { + const root = document.createElement('div'); + root.className = 'rendered-markdown'; + const lines = source.replace(/\r\n/g, '\n').split('\n'); + let paragraph = []; + let list = null; + + const flushList = () => { + if (list !== null) { + root.append(list); + list = null; + } + }; + const flushParagraph = () => { + appendParagraph(root, paragraph); + paragraph = []; + }; + + for (let index = 0; index < lines.length; index++) { + const line = lines[index]; + const fence = line.match(/^(```|~~~)([A-Za-z0-9_.+-]*)\s*$/); + if (fence !== null) { + flushParagraph(); + flushList(); + const marker = fence[1]; + const language = fence[2] || 'text'; + const codeLines = []; + index++; + while (index < lines.length && lines[index].trim() !== marker) { + codeLines.push(lines[index]); + index++; + } + root.append(createCodeBox(codeLines.join('\n'), language)); + continue; + } + + const heading = line.match(/^(#{1,4})\s+(.+)$/); + if (heading !== null) { + flushParagraph(); + flushList(); + const level = Math.min(4, heading[1].length + 1); + const node = document.createElement(`h${level}`); + node.append(renderInlineMarkdown(heading[2].trim())); + root.append(node); + continue; + } + + const item = line.match(/^\s*[-*]\s+(.+)$/); + if (item !== null) { + flushParagraph(); + if (list === null) { + list = document.createElement('ul'); + } + const li = document.createElement('li'); + li.append(renderInlineMarkdown(item[1].trim())); + list.append(li); + continue; + } + + const quote = line.match(/^>\s?(.+)$/); + if (quote !== null) { + flushParagraph(); + flushList(); + const blockquote = document.createElement('blockquote'); + blockquote.append(renderInlineMarkdown(quote[1].trim())); + root.append(blockquote); + continue; + } + + if (line.trim() === '') { + flushParagraph(); + flushList(); + continue; + } + + paragraph.push(line); + } + + flushParagraph(); + flushList(); + return root; +} + +function unwrapCompleteFence(raw, marker) { + const escaped = marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const match = raw.match(new RegExp(`^${escaped}([A-Za-z0-9_.+-]*)\\s*\\n([\\s\\S]*?)\\n${escaped}\\s*$`)); + if (match !== null) { + return { language: match[1] || 'text', body: match[2], complete: true }; + } + const partial = raw.match(new RegExp(`^${escaped}([A-Za-z0-9_.+-]*)\\s*\\n([\\s\\S]*)$`)); + if (partial !== null) { + return { language: partial[1] || 'text', body: partial[2], complete: false }; + } + return null; +} + +function renderContent(raw, role) { + const content = raw.trim(); + if (role !== 'assistant' || content === '') { + const plain = document.createElement('span'); + plain.textContent = raw; + return plain; + } + + const tildeFence = unwrapCompleteFence(content, '~~~'); + if (tildeFence !== null) { + return createCodeBox(tildeFence.body, tildeFence.language || 'markdown'); + } + + const backtickFence = unwrapCompleteFence(content, '```'); + if (backtickFence !== null && backtickFence.language.toLowerCase() === 'markdown') { + return renderMarkdownDocument(backtickFence.body); + } + if (backtickFence !== null) { + return createCodeBox(backtickFence.body, backtickFence.language); + } + + return renderMarkdownDocument(raw); +} + +function setMessageContent(body, content) { + const role = body.closest('.message')?.classList.contains('assistant') ? 'assistant' : 'user'; + body.dataset.rawContent = content; + body.replaceChildren(renderContent(content, role)); +} + +function setStatus(state, text) { + statusDot.classList.toggle('ok', state === 'ok'); + statusDot.classList.toggle('error', state === 'error'); + statusText.textContent = text; +} + +function clearMessages() { + messagesEl.textContent = ''; +} + +function addMessage(role, content, extraClass = '') { + const node = document.createElement('article'); + node.className = `message ${role} ${extraClass}`.trim(); + const label = document.createElement('span'); + label.className = 'role'; + label.textContent = role; + const body = document.createElement('div'); + body.className = 'content'; + node.append(label, body); + messagesEl.appendChild(node); + setMessageContent(body, content); + messagesEl.scrollTop = messagesEl.scrollHeight; + return body; +} + +function appendAssistantDelta(body, delta) { + setMessageContent(body, (body.dataset.rawContent || '') + delta); + messagesEl.scrollTop = messagesEl.scrollHeight; +} + +async function loadHealth() { + try { + const response = await fetch('/api/health', { cache: 'no-store' }); + const data = await response.json(); + if (!response.ok || !data.ok) { + setStatus('error', 'Inference offline'); + return; + } + + setStatus('ok', 'Ready'); + } catch (error) { + setStatus('error', 'Health check failed'); + } +} + +async function apiJson(url, options = {}) { + const response = await fetch(url, { + cache: 'no-store', + ...options, + headers: { + ...(options.body ? { 'content-type': 'application/json' } : {}), + ...(options.headers || {}), + }, + }); + const data = await response.json().catch(() => null); + if (!response.ok || data?.ok === false) { + throw new Error(data?.error?.message || `HTTP ${response.status}`); + } + return data; +} + +function threadPreviewTitle(thread) { + const title = typeof thread.title === 'string' && thread.title.trim() !== '' ? thread.title.trim() : 'New chat'; + return title.length > 64 ? `${title.slice(0, 61)}...` : title; +} + +function renderThreads() { + threadListEl.textContent = ''; + if (threads.length === 0) { + const empty = document.createElement('div'); + empty.className = 'thread-empty'; + empty.textContent = 'No threads yet'; + threadListEl.append(empty); + return; + } + + for (const thread of threads) { + const button = document.createElement('button'); + button.type = 'button'; + button.className = `thread-item${thread.id === currentThreadId ? ' active' : ''}`; + button.dataset.threadId = thread.id; + + const title = document.createElement('span'); + title.className = 'thread-title'; + title.textContent = threadPreviewTitle(thread); + + const meta = document.createElement('span'); + meta.className = 'thread-meta'; + const count = Number(thread.message_count || 0); + meta.textContent = `${count} message${count === 1 ? '' : 's'}`; + + button.append(title, meta); + button.addEventListener('click', () => selectThread(thread.id)); + threadListEl.append(button); + } +} + +async function refreshThreads() { + const data = await apiJson('/api/threads'); + threads = Array.isArray(data.threads) ? data.threads : []; + renderThreads(); +} + +function renderLoadedMessages(messages) { + clearMessages(); + history = []; + for (const message of messages) { + if (!['user', 'assistant'].includes(message.role) || typeof message.content !== 'string') { + continue; + } + history.push({ role: message.role, content: message.content }); + addMessage(message.role, message.content); + } + if (history.length === 0) { + addMessage('assistant', 'Thread ready.'); + } +} + +async function selectThread(threadId) { + currentThreadId = threadId; + localStorage.setItem('king-chat-thread-id', threadId); + renderThreads(); + const data = await apiJson(`/api/threads/${encodeURIComponent(threadId)}`); + currentThreadId = data.thread.id; + renderLoadedMessages(Array.isArray(data.messages) ? data.messages : []); + renderThreads(); + inputEl.focus(); +} + +async function createThread() { + const data = await apiJson('/api/threads', { + method: 'POST', + body: JSON.stringify({ title: 'New chat' }), + }); + await refreshThreads(); + await selectThread(data.thread.id); +} + +async function deleteCurrentThread() { + if (!currentThreadId) { + return; + } + const deleting = currentThreadId; + await apiJson(`/api/threads/${encodeURIComponent(deleting)}`, { method: 'DELETE' }); + currentThreadId = null; + history = []; + clearMessages(); + await refreshThreads(); + if (threads.length === 0) { + await createThread(); + } else { + await selectThread(threads[0].id); + } +} + +async function loadThreads() { + await refreshThreads(); + if (threads.length === 0) { + await createThread(); + return; + } + const stored = localStorage.getItem('king-chat-thread-id'); + const selected = threads.find((thread) => thread.id === stored)?.id || threads[0].id; + await selectThread(selected); +} + +function parseSseBlock(block) { + let event = 'message'; + let data = ''; + for (const rawLine of block.split('\n')) { + const line = rawLine.trimEnd(); + if (line.startsWith('event:')) { + event = line.slice(6).trim(); + } else if (line.startsWith('data:')) { + data += line.slice(5).trim(); + } + } + if (data === '') { + return null; + } + try { + return { event, data: JSON.parse(data) }; + } catch { + return { event, data: { raw: data } }; + } +} + +async function sendMessage(prompt) { + if (activeController) { + activeController.abort(); + } + if (!currentThreadId) { + await createThread(); + } + + const userMessage = { role: 'user', content: prompt }; + if (history.length === 0) { + clearMessages(); + } + history.push(userMessage); + addMessage('user', prompt); + const assistantBody = addMessage('assistant', ''); + + sendButton.disabled = true; + inputEl.disabled = true; + activeController = new AbortController(); + + try { + const response = await fetch('/api/chat', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ thread_id: currentThreadId, message: prompt }), + signal: activeController.signal, + }); + + if (!response.ok || !response.body) { + const text = await response.text(); + throw new Error(text || `HTTP ${response.status}`); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + let assistantText = ''; + + while (true) { + const { value, done } = await reader.read(); + if (done) { + break; + } + buffer += decoder.decode(value, { stream: true }); + + let boundary = buffer.indexOf('\n\n'); + while (boundary !== -1) { + const block = buffer.slice(0, boundary); + buffer = buffer.slice(boundary + 2); + const parsed = parseSseBlock(block); + if (parsed?.event === 'delta' && typeof parsed.data.content === 'string') { + assistantText += parsed.data.content; + appendAssistantDelta(assistantBody, parsed.data.content); + } else if (parsed?.event === 'thread' && parsed.data.thread?.id) { + currentThreadId = parsed.data.thread.id; + localStorage.setItem('king-chat-thread-id', currentThreadId); + const existing = threads.findIndex((thread) => thread.id === currentThreadId); + if (existing >= 0) { + threads[existing] = parsed.data.thread; + } else { + threads.unshift(parsed.data.thread); + } + renderThreads(); + } else if (parsed?.event === 'error') { + throw new Error(parsed.data.message || 'Inference error'); + } + boundary = buffer.indexOf('\n\n'); + } + } + + if (assistantText.trim() === '') { + assistantBody.parentElement.classList.add('error'); + setMessageContent(assistantBody, 'No assistant content returned.'); + } else { + history.push({ role: 'assistant', content: assistantText }); + await refreshThreads(); + } + } catch (error) { + assistantBody.parentElement.classList.add('error'); + setMessageContent(assistantBody, error instanceof Error ? error.message : 'Request failed.'); + } finally { + sendButton.disabled = false; + inputEl.disabled = false; + inputEl.focus(); + activeController = null; + } +} + +formEl.addEventListener('submit', (event) => { + event.preventDefault(); + const prompt = inputEl.value.trim(); + if (prompt === '') { + return; + } + inputEl.value = ''; + sendMessage(prompt); +}); + +newThreadButton.addEventListener('click', () => { + createThread().catch((error) => { + setStatus('error', error instanceof Error ? error.message : 'Could not create thread'); + }); +}); + +deleteThreadButton.addEventListener('click', () => { + deleteCurrentThread().catch((error) => { + setStatus('error', error instanceof Error ? error.message : 'Could not delete thread'); + }); +}); + +Promise.all([loadHealth(), loadThreads()]).catch((error) => { + setStatus('error', error instanceof Error ? error.message : 'Startup failed'); +}); diff --git a/demo/chat/public/index.html b/demo/chat/public/index.html new file mode 100644 index 000000000..0590d3632 --- /dev/null +++ b/demo/chat/public/index.html @@ -0,0 +1,53 @@ + + + + + + King Chat + + + +
+ + +
+
+ +
+ +
+ + +
+
+
+
+ + + + diff --git a/demo/chat/public/styles.css b/demo/chat/public/styles.css new file mode 100644 index 000000000..e6a985db6 --- /dev/null +++ b/demo/chat/public/styles.css @@ -0,0 +1,468 @@ +:root { + color-scheme: dark; + --bg: #0b0d10; + --panel: #15191f; + --panel-2: #1d232b; + --text: #f4f7fb; + --muted: #aab4c0; + --line: #303946; + --blue: #00a3ff; + --green: #00d06d; + --red: #ff4242; + --yellow: #ffd400; +} + +* { + box-sizing: border-box; +} + +html, +body { + height: 100%; +} + +body { + margin: 0; + background: var(--bg); + color: var(--text); + font: 15px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + overflow: hidden; +} + +button, +textarea { + font: inherit; +} + +.shell { + height: 100vh; + min-height: 0; + display: grid; + grid-template-columns: 320px minmax(0, 1fr); + overflow: hidden; +} + +.sidebar { + min-height: 0; + border-right: 1px solid var(--line); + padding: 22px; + background: var(--panel); + display: flex; + flex-direction: column; + gap: 18px; + overflow: auto; +} + +.brand { + display: flex; + gap: 12px; + align-items: center; +} + +.mark { + width: 42px; + height: 42px; + display: grid; + place-items: center; + background: var(--blue); + color: #00131f; + font-weight: 800; + border-radius: 8px; +} + +h1 { + margin: 0; + font-size: 20px; + letter-spacing: 0; +} + +p { + margin: 2px 0 0; + color: var(--muted); +} + +.status-card { + border: 1px solid var(--line); + background: var(--panel-2); + border-radius: 8px; + padding: 14px; +} + +.status-row { + display: flex; + align-items: center; + gap: 8px; + font-weight: 650; +} + +.status-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--yellow); +} + +.status-dot.ok { + background: var(--green); +} + +.status-dot.error { + background: var(--red); +} + +dl { + display: grid; + gap: 8px; + margin: 0; +} + +dl div { + display: flex; + justify-content: space-between; + gap: 12px; +} + +dt { + color: var(--muted); +} + +.threads-panel { + min-height: 0; + display: grid; + grid-template-rows: auto minmax(0, 1fr); + gap: 10px; +} + +.threads-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.threads-header h2 { + margin: 0; + font-size: 13px; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0; +} + +.threads-header button { + min-height: 32px; + border: 1px solid var(--line); + background: #222a34; + color: var(--text); + border-radius: 8px; + padding: 0 12px; + cursor: pointer; + font-weight: 750; +} + +.threads-header button:hover { + border-color: var(--blue); +} + +.thread-list { + min-height: 150px; + max-height: 34vh; + overflow: auto; + display: grid; + align-content: start; + gap: 8px; + padding-right: 2px; +} + +.thread-item { + width: 100%; + min-height: 58px; + border: 1px solid var(--line); + background: #10161d; + color: var(--text); + border-radius: 8px; + padding: 9px 10px; + text-align: left; + display: grid; + gap: 3px; + cursor: pointer; +} + +.thread-item:hover { + border-color: var(--blue); +} + +.thread-item.active { + border-color: var(--blue); + background: #082234; +} + +.thread-title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: 750; +} + +.thread-meta, +.thread-empty { + color: var(--muted); + font-size: 12px; +} + +.thread-empty { + border: 1px dashed var(--line); + border-radius: 8px; + padding: 14px; +} + +.secondary, +#sendButton { + min-height: 40px; + border: 1px solid var(--line); + background: #222a34; + color: var(--text); + border-radius: 8px; + cursor: pointer; +} + +.secondary:hover, +#sendButton:hover { + border-color: var(--blue); +} + +.secondary { + margin-top: auto; +} + +.secondary.danger:hover { + border-color: var(--red); + color: #ffd8d8; +} + +.chat-panel { + min-width: 0; + min-height: 0; + display: grid; + grid-template-rows: minmax(0, 1fr) auto; + overflow: hidden; +} + +.messages { + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + padding: 28px; + display: flex; + flex-direction: column; + gap: 14px; + overscroll-behavior: contain; +} + +.message { + max-width: 920px; + border: 1px solid var(--line); + border-radius: 8px; + padding: 13px 14px; + background: var(--panel); + overflow-wrap: anywhere; +} + +.message.user { + margin-left: auto; + background: #0c2d3f; + border-color: #12618a; +} + +.message.assistant { + margin-right: auto; +} + +.message.error { + border-color: var(--red); + color: #ffd8d8; +} + +.message .role { + display: block; + color: var(--muted); + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + margin-bottom: 6px; +} + +.message .content { + display: grid; + gap: 10px; +} + +.message.user .content { + white-space: pre-wrap; +} + +.rendered-markdown { + display: grid; + gap: 10px; +} + +.rendered-markdown > * { + margin: 0; +} + +.rendered-markdown h2, +.rendered-markdown h3, +.rendered-markdown h4, +.rendered-markdown h5 { + color: var(--text); + font-size: 15px; + line-height: 1.25; +} + +.rendered-markdown ul { + padding-left: 22px; +} + +.rendered-markdown blockquote { + border-left: 3px solid var(--blue); + padding-left: 12px; + color: var(--muted); +} + +.rendered-markdown a { + color: var(--blue); +} + +.rendered-markdown :not(pre) > code { + border: 1px solid #3b4654; + background: #07090c; + border-radius: 5px; + padding: 1px 5px; + color: #ffd400; +} + +.codebox { + margin: 2px 0; + border: 1px solid #3b4654; + background: #05070a; + border-radius: 8px; + overflow: hidden; + max-width: 100%; +} + +.codebox figcaption { + min-height: 32px; + display: flex; + align-items: center; + padding: 0 12px; + border-bottom: 1px solid #3b4654; + background: #10161d; + color: var(--muted); + font-size: 12px; + font-weight: 800; + letter-spacing: 0; + text-transform: uppercase; +} + +.codebox pre { + margin: 0; + padding: 13px 14px; + overflow: auto; + white-space: pre; + tab-size: 2; +} + +.codebox code { + font: 13px/1.55 "SFMono-Regular", Consolas, "Liberation Mono", monospace; + color: #f4f7fb; +} + +.tok-keyword { + color: #00a3ff; +} + +.tok-key, +.tok-const { + color: #ffd400; +} + +.tok-string { + color: #00d06d; +} + +.tok-number { + color: #ff4242; +} + +.tok-comment { + color: #8a96a3; +} + +.composer { + position: relative; + z-index: 2; + border-top: 1px solid var(--line); + padding: 18px 24px 22px; + background: var(--panel); + box-shadow: 0 -10px 22px rgba(0, 0, 0, 0.22); +} + +.composer label { + display: block; + color: var(--muted); + margin-bottom: 8px; + font-weight: 650; +} + +.composer-row { + display: grid; + grid-template-columns: minmax(0, 1fr) 110px; + gap: 12px; + align-items: end; +} + +textarea { + width: 100%; + resize: vertical; + min-height: 76px; + max-height: 240px; + border-radius: 8px; + border: 1px solid var(--line); + background: #0f1318; + color: var(--text); + padding: 12px; + outline: none; +} + +textarea:focus { + border-color: var(--blue); +} + +#sendButton { + background: var(--blue); + color: #00131f; + border-color: var(--blue); + font-weight: 800; +} + +#sendButton:disabled { + opacity: 0.6; + cursor: wait; +} + +@media (max-width: 820px) { + .shell { + height: 100dvh; + grid-template-columns: 1fr; + grid-template-rows: auto minmax(0, 1fr); + } + + .sidebar { + max-height: 38dvh; + border-right: 0; + border-bottom: 1px solid var(--line); + } + + .composer-row { + grid-template-columns: 1fr; + } +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..a917d18e6 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,54 @@ +# King Primitives + +This documentation shows the public King primitives in practical terms without +release language. Each primitive follows the same structure: + +- Function, example 1: compact example +- Function, example 2: extended example +- OO, example 1: compact example +- OO, example 2: extended example + +If King does not export a native OO class for a primitive yet, the respective +file states that explicitly. In that case the OO examples are small userland +adapters around the real `king_*` functions. + +## Table of Contents + +- [Source Layout](source-layout.md) +- [Async and Awaitables](async-awaitables.md) +- [Config, Session, TLS, and QUIC](config-session-tls-quic.md) +- [HTTP/1](http1.md) +- [HTTP/2](http2.md) +- [HTTP/3](http3.md) +- [WebSocket](websocket.md) +- [Pipeline Orchestrator](pipeline-orchestrator.md) +- [XSLT 2.0/3.0 for E-Invoicing](xslt.md) +- [MCP](mcp.md) +- [IIBIN](iibin.md) +- [Object Store](object-store.md) +- [CDN](cdn.md) +- [Semantic DNS](semantic-dns.md) +- [Autoscaling](autoscaling.md) +- [Telemetry](telemetry.md) +- [System Runtime](system-runtime.md) +- [RTP](rtp.md) +- [DB Ingest](db-ingest.md) +- [Local Quantized Inference](inference.md) +- [Inference 3D Transformer Flow](inference/index.html) +- [Fine-Tuning Preparation](fine-tuning.md) +- [GPU Readiness Runbook](gpu-readiness.md) +- [OpenAI-Compatible Inference Router](openai-compatible-inference.md) +- [E-Invoicing, EDI, and B2B Commerce Platform](e-invoicing-commerce-platform.md) +- [E-Invoicing, EDI, and B2B Commerce Platform: OO Implementation](e-invoicing-commerce-platform-oo.md) + +## Ground Rules + +- Procedural APIs use the `king_*` naming scheme. +- Native OO APIs live under `King\...`, `King\Client\...`, or + `King\WebSocket\...`. +- Async methods return `King\Awaitable` and are resolved with `king_await()` + or `$awaitable->await()`. +- Errors should be handled through exceptions. `king_get_last_error()` exists + for older integration points. +- Configuration flows through either `King\Config` or, for older functions, + an array or `king_new_config()`. diff --git a/docs/async-awaitables.md b/docs/async-awaitables.md new file mode 100644 index 000000000..6d4e4b81e --- /dev/null +++ b/docs/async-awaitables.md @@ -0,0 +1,197 @@ +# Async and Awaitables + +King uses `King\Awaitable` for asynchronous HTTP, MCP, orchestrator, and +inference calls. +Procedural code can use `king_await()`, `king_awaitable_poll()`, +`king_awaitable_cancel()`, `king_awaitable_status()`, +`king_awaitable_any()`, and `king_awaitable_all()`. OO code calls the methods +directly on the awaitable or uses `King\Awaitable::any()` and +`King\Awaitable::all()` for aggregate waits. + +Aggregate awaitables resolve to status envelopes with `key`, `status`, +`operation`, `value`, and, for rejected children, `error`. This keeps failed +children visible without losing the caller's original array keys. + +`poll(0)` is a non-blocking state check. A positive poll timeout or an +unbounded `await()` call is allowed to drive deferred native work once. On the +current deferred backend, hard IO deadlines still belong in the async operation +options such as HTTP, MCP, or orchestrator `timeout_ms`; the await timeout +controls whether pending work may be started by the awaitable layer. + +## Internal Layout + +The native object contract and procedural declarations live in +`extension/include/awaitable/awaitable.h` and are exported through +`extension/include/awaitable/index.h`. The implementation lives under +`extension/src/awaitable/`. PHP-facing registration metadata, class-method +entries, and object contracts live under `extension/include/awaitable/`; object +implementation and aggregate helpers remain under `extension/src/awaitable/`. + +## Function, Example 1: Await an HTTP Request + +```php + 'application/json'], + null, + ['timeout_ms' => 1500] +); + +if (king_awaitable_status($awaitable) === 'pending') { + king_awaitable_poll($awaitable, 10); +} + +$response = king_await($awaitable, 2000); +echo $response['status'] . PHP_EOL; +echo $response['body'] . PHP_EOL; +``` + +## Function, Example 2: Cancel a Long Orchestrator Run + +```php + 'php-handler', +]); + +king_pipeline_orchestrator_register_handler('normalize-invoice', static function (array $ctx): array { + $input = $ctx['input']; + $input['normalized_at'] = date(DATE_ATOM); + + return ['output' => $input]; +}); + +$awaitable = king_pipeline_orchestrator_run_async( + ['invoice_id' => 'INV-1001'], + [['tool' => 'normalize-invoice']], + ['trace_id' => 'invoice-normalization-1001'] +); + +if (!king_awaitable_poll($awaitable, 50)) { + king_awaitable_cancel($awaitable); +} + +if (king_awaitable_status($awaitable) !== 'cancelled') { + $result = king_await($awaitable, 2000); + var_dump($result); +} +``` + +## Function, Example 3: Wait for the First Completed Operation + +```php + 'application/json'] +); +$invoiceStatus = king_client_send_request_async( + 'http://127.0.0.1:8080/api/invoices/INV-1001/status', + 'GET', + ['accept' => 'application/json'] +); + +$first = king_awaitable_any([ + 'tenant' => $tenantProfile, + 'invoice' => $invoiceStatus, +]); + +if ($first->poll(25)) { + $ready = king_await($first); + + if ($ready['status'] === 'resolved') { + printf("%s finished first\n", $ready['key']); + var_dump($ready['value']); + } +} +``` + +## OO, Example 1: Awaitable Directly on the Object + +```php +requestAsync('GET', 'http://127.0.0.1:8080/status'); + +$response = $awaitable->await(2000); +echo $response->getStatusCode() . PHP_EOL; +echo $response->getBody() . PHP_EOL; + +$client->close(); +``` + +## OO, Example 2: Wrap Multiple Awaitables in Project Code + +```php + */ + public function start(): array + { + $http = new Http2Client(); + + return [ + $http->requestAsync('GET', 'https://invoice-api.internal.local/v1/tenant/42'), + PipelineOrchestrator::runAsync( + ['tenant_id' => 42], + [['tool' => 'refresh-tenant-cache']], + ['trace_id' => 'tenant-42-cache-refresh'] + ), + ]; + } + + /** @param list $awaitables */ + public function collect(array $awaitables): array + { + $results = []; + + foreach ($awaitables as $awaitable) { + if ($awaitable->isPending()) { + $awaitable->poll(25); + } + + $results[] = $awaitable->await(3000); + } + + return $results; + } +} +``` + +## OO, Example 3: Collect All Results with Status Envelopes + +```php + $client->requestAsync('GET', 'http://127.0.0.1:8080/catalog/import/status'), + 'stock' => $client->requestAsync('GET', 'http://127.0.0.1:8080/stock/sync/status'), +]); + +if ($aggregate->poll(50)) { + foreach ($aggregate->await() as $name => $ready) { + if ($ready['status'] !== 'resolved') { + error_log($name . ' failed: ' . ($ready['error'] ?? $ready['status'])); + continue; + } + + echo $name . ': ' . $ready['value']->getStatusCode() . PHP_EOL; + } +} + +$client->close(); +``` diff --git a/docs/autoscaling.md b/docs/autoscaling.md new file mode 100644 index 000000000..c444959b4 --- /dev/null +++ b/docs/autoscaling.md @@ -0,0 +1,97 @@ +# Autoscaling + +Autoscaling can be used procedurally through `king_autoscaling_*` and through +the `King\Autoscaling` OO facade. + +## Internal Layout + +The autoscaling runtime lives under `extension/src/autoscaling/runtime/` with +public contracts in `extension/include/autoscaling/`. Public declarations are +exported through `extension/include/autoscaling/index.h`. The PHP arginfo, +`King\Autoscaling` method table, and function-table entries live under +`extension/include/autoscaling/` and are consumed by the extension bootstrap +through `extension/include/php_king/`. + +## Function, Example 1: Initialize Runtime and Read Status + +```php + 'hetzner', + 'min_instances' => 2, + 'max_instances' => 8, + 'scale_up_policy' => 'cpu_or_queue', + 'cooldown_seconds' => 120, +]); + +king_autoscaling_start_monitoring(); + +$status = king_autoscaling_get_status(); +$metrics = king_autoscaling_get_metrics(); + +var_dump($status['provider_mode'], $metrics); +``` + +## Function, Example 2: Node Lifecycle + +```php + 'hetzner', + 'min_instances' => 1, + 'max_instances' => 4, +]); + +Autoscaling::startMonitoring(); +var_dump(Autoscaling::getStatus()); +Autoscaling::stopMonitoring(); +``` + +## OO, Example 2: Controller Class + +```php + 100 && $status['current_instances'] < 8) { + Autoscaling::scaleUp(1); + } + + if ($queueDepth === 0 && $status['current_instances'] > 2) { + Autoscaling::scaleDown(1); + } + } + + public function drain(int $serverId): void + { + Autoscaling::drainNode($serverId); + } +} + +$controller = new WorkerPoolController(); +$controller->ensureCapacity(180); +``` diff --git a/docs/cdn.md b/docs/cdn.md new file mode 100644 index 000000000..a0464d796 --- /dev/null +++ b/docs/cdn.md @@ -0,0 +1,115 @@ +# CDN + +The CDN primitive is available procedurally through `king_cdn_*` and is tied +to the object-store/CDN runtime state. A native `King\CDN` class is not +exported yet. The OO examples below are userland adapters around the real +functions. + +## Internal Layout + +CDN registration shares the object-store module registration metadata under +`extension/include/object_store/`, because the CDN cache is backed by the same +runtime state and public function table section. Runtime implementation remains +under `extension/src/object_store/`. + +## Function, Example 1: Admit an Object into the CDN Cache + +```php + 'local_fs', + 'storage_root_path' => __DIR__ . '/var/object-store', + 'cdn_config' => [ + 'enabled' => true, + 'cache_size_mb' => 128, + 'default_ttl_seconds' => 300, + ], +]); + +king_object_store_put('public/invoice-report.json', '{"ok":true}', [ + 'content_type' => 'application/json', + 'cache_ttl_sec' => 300, +]); + +if (!king_cdn_cache_object('public/invoice-report.json', ['ttl_sec' => 300])) { + throw new RuntimeException('object could not be admitted to CDN cache'); +} + +$stats = king_object_store_get_stats(); +var_dump($stats['cdn']); +``` + +## Function, Example 2: Configure QUERY as an Allowed CDN Method + +```php + true, + 'cdn.allowed_http_methods' => 'GET,HEAD,QUERY', +]); + +$session = king_connect('127.0.0.1', 443, $config); +$stats = king_get_stats($session); + +echo $stats['config_cdn_allowed_http_methods'] . PHP_EOL; +king_close($session); + +$removed = king_cdn_invalidate_cache(); +echo "invalidated=$removed\n"; +``` + +## OO, Example 1: Userland CDN Adapter + +```php + $ttlSeconds])) { + throw new RuntimeException("CDN cache miss or admission failure: $objectId"); + } + } + + public function invalidate(?string $objectId = null): int + { + return king_cdn_invalidate_cache($objectId); + } +} + +$cdn = new CdnCache(); +$cdn->cache('public/invoice-report.json', 300); +``` + +## OO, Example 2: Cache Warmup Service + +```php + $objectIds */ + public function warm(array $objectIds): array + { + $result = ['cached' => [], 'failed' => []]; + + foreach ($objectIds as $objectId) { + try { + $this->cdn->cache($objectId, 600); + $result['cached'][] = $objectId; + } catch (Throwable $e) { + $result['failed'][$objectId] = $e->getMessage(); + } + } + + return $result; + } +} + +$warmup = new InvoiceReportWarmup(new CdnCache()); +var_dump($warmup->warm([ + 'public/reports/INV-1001.json', + 'public/reports/INV-1002.json', +])); +``` diff --git a/docs/config-session-tls-quic.md b/docs/config-session-tls-quic.md new file mode 100644 index 000000000..b926a4686 --- /dev/null +++ b/docs/config-session-tls-quic.md @@ -0,0 +1,144 @@ +# Config, Session, TLS, and QUIC + +`King\Config` is the OO configuration surface. `king_new_config()` is the +procedural variant. Low-level sessions are created with `king_connect()` or +`new King\Session(...)`. + +## Internal Layout + +The config include-side umbrella lives in `extension/include/config/index.h`. +The native config snapshot lives in `extension/include/config/config.h`; the +PHP-visible `King\Config` object contract lives in +`extension/include/config/object.h`. `King\CancelToken` is shared across +client, awaitable, inference, MCP, and orchestrator paths, so its object +contract lives in `extension/include/awaitable/cancel_token.h`. + +Client session, TLS, request, and polling registration metadata lives under +`extension/include/client/`, including `arginfo/` and `function_entries.h`. +Public declarations for the original unprefixed client surface live in +`extension/include/client/legacy.h`; protocol-specific sync and async +declarations live under `extension/include/client/` and are exported through +`extension/include/client/index.h`. Client runtime implementation remains under +`extension/src/client/`. + +Server session, TLS reload, cancellation, and admin listener registration +metadata lives under `extension/include/server/`, including `arginfo/` and +`function_entries.h`. Public declarations live under +`extension/include/server/` and are exported through +`extension/include/server/index.h`; server runtime implementation remains under +`extension/src/server/`. + +Core exception, class, and object-handler registration is factored into +`extension/src/php_king/class_registration.inc`; `extension/src/php_king/lifecycle.inc` +keeps the MINIT/MSHUTDOWN/RINIT/RSHUTDOWN ordering. + +## Function, Example 1: Config Resource and Session Stats + +```php + true, + 'tls.ca_file' => __DIR__ . '/certs/ca.pem', + 'quic.ping_interval_ms' => 1000, +]); + +$session = king_connect('127.0.0.1', 443, $config); +if ($session === false) { + throw new RuntimeException(king_get_last_error()); +} + +king_poll($session, 50); +$stats = king_get_stats($session); + +printf("state=%s tx=%d rx=%d\n", + $stats['state'] ?? 'unknown', + $stats['transport_tx_bytes'] ?? 0, + $stats['transport_rx_bytes'] ?? 0 +); + +king_close($session); +``` + +## Function, Example 2: TLS Defaults and Session Tickets + +```php + true, + 'tcp.connect_timeout_ms' => 1500, +]); + +$ticket = king_client_tls_export_session_ticket($first); +king_close($first); + +$second = king_connect('gateway.internal.local', 443, [ + 'tls.enable_early_data' => true, +]); + +king_client_tls_import_session_ticket($second, $ticket); +king_poll($second, 20); + +$stats = king_get_stats($second); +echo ($stats['tls_ticket_source'] ?? 'none') . PHP_EOL; + +king_close($second); +``` + +## OO, Example 1: Set and Read Config + +```php + true, + 'tls.ca_file' => __DIR__ . '/certs/ca.pem', +]); + +$config->set('cdn.allowed_http_methods', 'GET,HEAD,QUERY'); + +var_dump($config->get('http2.enable')); +var_dump($config->toArray()['cdn.allowed_http_methods']); +``` + +## OO, Example 2: Session, Stream, Response, and CancelToken + +```php + __DIR__ . '/certs/ca.pem', + 'quic.ping_interval_ms' => 500, +]); + +$session = new Session('invoice-api.internal.local', 443, $config, [ + 'sni' => 'invoice-api.internal.local', +]); + +$cancel = new CancelToken(); +$stream = $session->sendRequest( + 'GET', + '/v1/health', + ['accept' => 'application/json'], + '', + $cancel +); + +$response = $stream->receiveResponse(2000, $cancel); +if ($response !== null) { + echo $response->getStatusCode() . PHP_EOL; + echo $response->getBody() . PHP_EOL; +} + +var_dump($session->stats()); +$session->close(); +``` diff --git a/docs/db-ingest.md b/docs/db-ingest.md new file mode 100644 index 000000000..dc27e1bf3 --- /dev/null +++ b/docs/db-ingest.md @@ -0,0 +1,102 @@ +# DB Ingest + +`king_db_ingest()` executes a writer callback under a host-local exclusive +lock. This is useful for small local write paths where multiple processes may +touch the same local state. + +## Internal Layout + +The public `king_db_ingest()` declaration lives in +`extension/include/db_ingest/db_ingest.h` and is exported through +`extension/include/db_ingest/index.h`. The implementation lives in +`extension/src/db_ingest/api.inc`. Its arginfo and function-table entry live +under `extension/include/db_ingest/` and are consumed by the root extension +bootstrap through `extension/include/php_king/`. + +## Function, Example 1: Append to a JSONL Ledger Atomically + +```php + 'INV-1001', 'received_at' => date(DATE_ATOM)]; + + file_put_contents( + $path, + json_encode($entry, JSON_THROW_ON_ERROR) . PHP_EOL, + FILE_APPEND | LOCK_EX + ); + + return $entry; +}, [ + 'lock_path' => __DIR__ . '/var/invoice-ledger.lock', + 'timeout_ms' => 1500, +]); + +var_dump($result); +``` + +## Function, Example 2: Protect a SQLite Write Transaction + +```php +exec('CREATE TABLE IF NOT EXISTS invoices (id TEXT PRIMARY KEY, status TEXT)'); + + $stmt = $db->prepare('INSERT OR REPLACE INTO invoices (id, status) VALUES (?, ?)'); + $stmt->execute(['INV-1002', 'accepted']); + + return $stmt->rowCount(); +}, [ + 'lock_path' => __DIR__ . '/var/invoices.sqlite.lock', + 'timeout_ms' => 3000, + 'poll_us' => 10000, +]); +``` + +## OO, Example 1: Ingest Adapter + +```php + $lockPath, + 'timeout_ms' => 2000, + ]); + } +} + +$ingest = new LockedIngest(); +$ingest->write('audit', fn () => file_put_contents(__DIR__ . '/var/audit.log', "ok\n", FILE_APPEND), __DIR__ . '/var/audit.lock'); +``` + +## OO, Example 2: Repository with Protected Writer + +```php +ingest->write( + 'invoice-ledger', + function () use ($entry): array { + $line = json_encode($entry, JSON_THROW_ON_ERROR) . PHP_EOL; + file_put_contents($this->root . '/invoice-ledger.jsonl', $line, FILE_APPEND); + + return $entry; + }, + $this->root . '/invoice-ledger.lock' + ); + } +} + +$repo = new InvoiceLedgerRepository(new LockedIngest(), __DIR__ . '/var'); +$repo->append(['invoice_id' => 'INV-1003', 'status' => 'accepted']); +``` diff --git a/docs/e-invoicing-commerce-platform-oo.md b/docs/e-invoicing-commerce-platform-oo.md new file mode 100644 index 000000000..d2c3c2c90 --- /dev/null +++ b/docs/e-invoicing-commerce-platform-oo.md @@ -0,0 +1,523 @@ +# E-Invoicing, EDI, and B2B Commerce Platform: OO Implementation + +This page implements the architecture from +[E-Invoicing, EDI, and B2B Commerce Platform](e-invoicing-commerce-platform.md) +as OO services. Each operation publishes a readable JSON event first and then +the same payload as `Next Level: IIBIN` when the contract is ready for compact +internal handoff. + +## Component Model + +```mermaid +classDiagram + class PlatformKernel { + +boot(): void + } + class EvidenceStore { + +putStream(id, stream, metadata): void + +putJson(id, payload, metadata): void + +putIibin(id, payload, type): void + } + class PlatformEventCodec { + +registerSchemas(): void + +json(payload): string + +iibin(schema, payload): string + } + class PlatformEventPublisher { + +publish(name, id, payload): void + } + class InvoiceIntakeService { + +receiveAs4(context, stream): string + } + class InvoiceValidationService { + +validate(document): ValidationResult + } + class ProcurementService { + +compare(request): ProcurementDecision + } + class PrivacySafeSupportGateway { + +answer(caseContext, question): SupportAnswer + } + class AvailabilityPublisher { + +publish(delta): void + } + + PlatformKernel --> EvidenceStore + PlatformKernel --> PlatformEventCodec + PlatformEventPublisher --> EvidenceStore + PlatformEventPublisher --> PlatformEventCodec + InvoiceIntakeService --> PlatformEventPublisher + InvoiceValidationService --> PlatformEventPublisher + ProcurementService --> PlatformEventPublisher + PrivacySafeSupportGateway --> PlatformEventPublisher + AvailabilityPublisher --> EvidenceStore +``` + +The OO layer owns invariants: tenant authorization, idempotency, immutable +evidence, privacy minimization, state transitions, event publication, and +workflow dispatch. King primitives remain visible, but they are called through +services that preserve those invariants. + +## Bootstrap and Evidence Store + +```php + 'b2b-einvoice-platform', + 'node_id' => $this->nodeId, + 'state_root_path' => $this->stateRoot . '/system', + 'components' => ['client', 'server', 'object_store', 'pipeline_orchestrator', 'telemetry', 'mcp', 'iibin'], + ]); + + ObjectStore::init([ + 'primary_backend' => 'local_fs', + 'storage_root_path' => $this->stateRoot . '/objects', + 'max_storage_size_bytes' => 500 * 1024 * 1024 * 1024, + ]); + + $this->events->registerSchemas(); + } +} + +final class EvidenceStore +{ + public function putStream(string $objectId, mixed $stream, array $metadata): void + { + ObjectStore::putFromStream($objectId, $stream, $metadata + ['stored_at' => date(DATE_ATOM)]); + } + + public function putJson(string $objectId, array $payload, array $metadata = []): void + { + ObjectStore::put($objectId, json_encode($payload, JSON_THROW_ON_ERROR), $metadata + [ + 'content_type' => 'application/json', + 'stored_at' => date(DATE_ATOM), + ]); + } + + public function putIibin(string $objectId, string $payload, string $eventType): void + { + ObjectStore::put($objectId, $payload, [ + 'content_type' => 'application/x-king-iibin', + 'object_type' => $eventType, + 'stored_at' => date(DATE_ATOM), + ]); + } +} +``` + +`PlatformKernel` starts the coordinated runtime and registers IIBIN schemas +once. `EvidenceStore` centralizes object-store metadata so raw AS4 envelopes, +JSON event evidence, IIBIN events, validation reports, support audits, and +procurement offer sets are stored consistently. + +## Event Codec + +### Event Payload: JSON + +```php + ['tag' => 1, 'type' => 'int32', 'required' => true], + 'message_id' => ['tag' => 2, 'type' => 'string', 'required' => true], + 'partner_id' => ['tag' => 3, 'type' => 'string', 'required' => true], + 'raw_object_id' => ['tag' => 4, 'type' => 'string', 'required' => true], + 'transport' => ['tag' => 5, 'type' => 'string', 'default' => 'as4'], + 'state' => ['tag' => 6, 'type' => 'string', 'required' => true], + 'trace_id' => ['tag' => 7, 'type' => 'string', 'required' => true], + ]); + + king_proto_define_schema('InvoiceValidationEvent', [ + 'tenant_id' => ['tag' => 1, 'type' => 'int32', 'required' => true], + 'invoice_id' => ['tag' => 2, 'type' => 'string', 'required' => true], + 'profile' => ['tag' => 3, 'type' => 'string', 'required' => true], + 'status' => ['tag' => 4, 'type' => 'string', 'required' => true], + 'report_object_id' => ['tag' => 5, 'type' => 'string', 'required' => true], + 'error_count' => ['tag' => 6, 'type' => 'int32', 'default' => 0], + ]); + + king_proto_define_schema('ProcurementDecisionEvent', [ + 'tenant_id' => ['tag' => 1, 'type' => 'int32', 'required' => true], + 'sku' => ['tag' => 2, 'type' => 'string', 'required' => true], + 'quantity' => ['tag' => 3, 'type' => 'int32', 'required' => true], + 'chosen_supplier_id' => ['tag' => 4, 'type' => 'string', 'required' => true], + 'currency' => ['tag' => 5, 'type' => 'string', 'default' => 'EUR'], + 'total_cents' => ['tag' => 6, 'type' => 'int32', 'required' => true], + 'offer_cache_object_id' => ['tag' => 7, 'type' => 'string', 'required' => true], + ]); + + king_proto_define_schema('SupportToolRequest', [ + 'tenant_id' => ['tag' => 1, 'type' => 'string', 'required' => true], + 'support_case_id' => ['tag' => 2, 'type' => 'string', 'required' => true], + 'purpose' => ['tag' => 3, 'type' => 'string', 'required' => true], + 'question' => ['tag' => 4, 'type' => 'string', 'required' => true], + 'invoice_ref' => ['tag' => 5, 'type' => 'string', 'required' => true], + 'allowed_fields' => ['tag' => 6, 'type' => 'string', 'repeated' => true], + ]); + + king_proto_define_schema('SupportToolResponse', [ + 'answer' => ['tag' => 1, 'type' => 'string', 'required' => true], + 'confidence' => ['tag' => 2, 'type' => 'double'], + 'source_count' => ['tag' => 3, 'type' => 'uint32'], + ]); + + king_proto_define_schema('SupportToolAuditEvent', [ + 'tenant_id' => ['tag' => 1, 'type' => 'int32', 'required' => true], + 'support_case_id' => ['tag' => 2, 'type' => 'string', 'required' => true], + 'tool' => ['tag' => 3, 'type' => 'string', 'required' => true], + 'purpose' => ['tag' => 4, 'type' => 'string', 'required' => true], + 'input_classification' => ['tag' => 5, 'type' => 'string', 'required' => true], + 'retention_policy' => ['tag' => 6, 'type' => 'string', 'required' => true], + ]); + } +} +``` + +### Next Level: IIBIN + +```php +store->putJson($objectBase . '.json', $payload, [ + 'object_type' => $schema . '-json-event', + ]); + + $this->store->putIibin( + $objectBase . '.iibin', + $this->codec->iibin($schema, $payload), + $schema . '-iibin-event' + ); + + return $objectBase . '.iibin'; + } +} +``` + +The publisher deliberately writes JSON first. Humans, support staff, and +operations can inspect that artifact. IIBIN is the next level for worker +handoff, compact storage, and versioned internal contracts. + +## AS4 Intake Service + +```php +policy->assertAllowed($context->tenantId, $context->partnerId, 'invoice.receive'); + + $key = hash('sha256', $context->tenantId . ':' . $context->messageId); + if (!$this->idempotency->claim($key)) { + return 'duplicate'; + } + + $rawObjectId = sprintf('inbound/as4/%d/%s.xml', $context->tenantId, $context->messageId); + $this->store->putStream($rawObjectId, $envelopeStream, [ + 'content_type' => 'application/soap+xml', + 'object_type' => 'as4-inbound-envelope', + 'tenant_id' => $context->tenantId, + 'partner_id' => $context->partnerId, + 'idempotency_key' => $key, + ]); + + $eventObjectId = $this->events->publish('InvoiceIntakeEvent', 'events/invoice-intake/' . $context->messageId, [ + 'tenant_id' => $context->tenantId, + 'message_id' => $context->messageId, + 'partner_id' => $context->partnerId, + 'raw_object_id' => $rawObjectId, + 'transport' => 'as4', + 'state' => 'received', + 'trace_id' => 'as4-' . $context->messageId, + ]); + + PipelineOrchestrator::dispatch( + ['event_object_id' => $eventObjectId], + [['tool' => 'verify-as4-envelope'], ['tool' => 'extract-business-document'], ['tool' => 'validate-invoice-profile']], + ['trace_id' => 'as4-' . $context->messageId] + ); + + return 'accepted_for_processing'; + } +} +``` + +The service writes raw evidence, JSON event evidence, and IIBIN event handoff +before dispatching the workflow. That ordering prevents receipts without audit +evidence and worker jobs without reconstructable input. + +## Validation Service + +```php +invoiceId . '.svrl.xml'; + $this->xslt->transformToFile($document->xmlPath, $this->rulesetFor($document->profile), $svrlPath, [ + 'properties' => ['indent' => 'yes'], + ]); + + $errors = parse_svrl_failed_asserts($svrlPath); + $status = count($errors) === 0 ? 'valid' : 'invalid'; + $reportObjectId = 'validation/' . $document->tenantId . '/' . $document->invoiceId . '.svrl.xml'; + + $this->store->putStream($reportObjectId, fopen($svrlPath, 'rb'), [ + 'content_type' => 'application/xml', + 'object_type' => 'invoice-validation-report', + 'tenant_id' => $document->tenantId, + 'invoice_id' => $document->invoiceId, + ]); + + $this->events->publish('InvoiceValidationEvent', 'events/invoice-validation/' . $document->invoiceId, [ + 'tenant_id' => $document->tenantId, + 'invoice_id' => $document->invoiceId, + 'profile' => $document->profile, + 'status' => $status, + 'report_object_id' => $reportObjectId, + 'error_count' => count($errors), + ]); + + return new ValidationResult($status, $reportObjectId, $errors); + } + + private function rulesetFor(string $profile): string + { + return __DIR__ . '/rules/' . $profile . '.xsl'; + } +} +``` + +Business validation failures become validation results, not generic exceptions. +The JSON report event is readable; the IIBIN event is the next-level contract +for downstream workers. + +## Procurement Service + +```php +suppliers->forSku($request->sku) as $supplier) { + $awaitables[$supplier->id] = king_client_send_request_async( + $supplier->endpoint, + 'QUERY', + ['accept' => 'application/json', 'content-type' => 'application/query'], + $this->queryBody($request), + ['timeout_ms' => 1500] + ); + } + + $offers = []; + foreach ($awaitables as $supplierId => $awaitable) { + $response = king_await($awaitable, 2000); + $offers[$supplierId] = json_decode($response['body'], true, flags: JSON_THROW_ON_ERROR); + } + + $decision = ProcurementDecision::fromOffers($request, $offers); + $offerObjectId = 'procurement/offers/' . $decision->id . '.json'; + + $this->store->putJson($offerObjectId, $offers, [ + 'object_type' => 'procurement-offer-set', + 'cache_ttl_sec' => 120, + ]); + + $this->events->publish('ProcurementDecisionEvent', 'events/procurement/' . $decision->id, [ + 'tenant_id' => $request->tenantId, + 'sku' => $request->sku, + 'quantity' => $request->quantity, + 'chosen_supplier_id' => $decision->supplierId, + 'currency' => $decision->currency, + 'total_cents' => $decision->totalCents, + 'offer_cache_object_id' => $offerObjectId, + ]); + + return $decision; + } + + private function queryBody(ProcurementRequest $request): string + { + return sprintf('sku = "%s" AND quantity >= %d', addslashes($request->sku), $request->quantity); + } +} +``` + +Supplier calls are concurrent and bounded. The service stores the full offer +set separately and emits a compact decision event that points to it. + +## Privacy-Safe Support Gateway + +```php + 1500, + 'mcp.iibin_routes' => [ + 'support.faq/answer' => [ + 'request_schema' => 'SupportToolRequest', + 'response_schema' => 'SupportToolResponse', + 'decode_as_object' => false, + ], + ], + ])); + } +} + +final class PrivacySafeSupportGateway +{ + public function __construct( + private readonly MCP $mcp, + private readonly PlatformEventPublisher $events, + private readonly SupportPolicy $policy, + ) {} + + public function answer(SupportCaseContext $case, string $question): SupportAnswer + { + $tool = 'support.faq'; + $toolInput = [ + 'tenant_id' => $case->tenantId, + 'purpose' => 'support_case_answer', + 'support_case_id' => $case->caseId, + 'question' => redact_personal_data($question), + 'invoice_ref' => hash_hmac('sha256', $case->invoiceId, 'tenant-' . $case->tenantId), + 'allowed_fields' => ['status', 'received_at', 'rejection_code'], + ]; + + $this->policy->assertToolAllowed($case->userId, $case->tenantId, $tool, $toolInput); + $answer = SupportAnswer::fromDecodedPayload( + $this->mcp->requestIibin($tool, 'answer', $toolInput) + ); + + $this->events->publish('SupportToolAuditEvent', 'events/support-tools/' . $case->caseId, [ + 'tenant_id' => $case->tenantId, + 'support_case_id' => $case->caseId, + 'tool' => $tool, + 'purpose' => 'support_case_answer', + 'input_classification' => 'redacted-support-context', + 'retention_policy' => 'support-audit-180d', + ]); + + return $answer->confidence >= 0.75 + ? $answer + : SupportAnswer::manualReviewRequired($answer->sources); + } +} +``` + +The gateway strips direct identifiers before the internal King MCP peer. The +IIBIN request and response schemas are fixed in the `King\Config` used to +construct that peer connection; a support call cannot change schemas at +runtime. JSON audit evidence records what class of data was used; IIBIN gives +downstream compliance workers a stable event without storing the redacted +support question again. + +## Availability Publisher + +```php +> */ + private array $subscribers = []; + + public function __construct(private readonly EvidenceStore $store) {} + + public function subscribe(string $sku, mixed $websocket): void + { + $this->subscribers[$sku][] = $websocket; + } + + public function publish(AvailabilityDelta $delta): void + { + $event = [ + 'type' => 'availability.changed', + 'tenant_id' => $delta->tenantId, + 'sku' => $delta->sku, + 'available_quantity' => $delta->availableQuantity, + 'warehouse' => $delta->warehouse, + 'changed_at' => date(DATE_ATOM), + ]; + + $snapshotObjectId = 'availability/' . $delta->tenantId . '/' . $delta->sku . '.json'; + $this->store->putJson($snapshotObjectId, $event, [ + 'object_type' => 'availability-snapshot', + 'cache_ttl_sec' => 30, + ]); + + foreach ($this->subscribers[$delta->sku] ?? [] as $index => $websocket) { + if (!is_resource($websocket) || !king_websocket_send($websocket, json_encode($event, JSON_THROW_ON_ERROR))) { + unset($this->subscribers[$delta->sku][$index]); + } + } + } +} +``` + +Availability is JSON-first because the shop needs readable payloads. If the +same event becomes an internal worker contract, promote that payload through +the same `PlatformEventPublisher` and add a dedicated IIBIN schema. diff --git a/docs/e-invoicing-commerce-platform.md b/docs/e-invoicing-commerce-platform.md new file mode 100644 index 000000000..bc5a918d7 --- /dev/null +++ b/docs/e-invoicing-commerce-platform.md @@ -0,0 +1,714 @@ +# E-Invoicing, EDI, and B2B Commerce Platform + +This page documents a production-grade King architecture for a B2B commerce +and e-invoicing platform. It covers AS4 invoice exchange, EDIFACT processing, +XSLT validation, catalog import, live availability, procurement across +supplier APIs, privacy-safe MCP support, JSON event contracts, and the next +level IIBIN representation for internal platform events. + +The companion page shows the same design as OO service composition: +[E-Invoicing, EDI, and B2B Commerce Platform: OO Implementation](e-invoicing-commerce-platform-oo.md). + +## Event Strategy + +External protocols keep their mandated formats: AS4/SOAP, UBL, CII, EDIFACT, +supplier JSON/XML, and authority payloads are stored as immutable evidence. +Inside the platform, every meaningful state transition is first shaped as a +readable JSON event. The next level is the same event encoded as IIBIN for +compact internal handoff, stable schemas, and fast worker communication. + +JSON is the review and troubleshooting format. IIBIN is the runtime event +format once the contract is stable. Both reference durable object IDs instead +of copying full invoices, PDFs, buyer addresses, payment data, or chat text +into every event. + +## Overall Architecture + +```mermaid +flowchart LR + Buyers[Buyer ERP / AP Platforms] + Suppliers[Supplier Systems] + Authority[Tax Authority / Network AP] + Shop[B2B Shop] + Support[Support Desk] + Ops[Operations Console] + + subgraph Edge["King Edge Runtime"] + H1[HTTP/1 APIs + QUERY] + H2[HTTP/2 Supplier APIs] + H3[HTTP/3 Low Latency APIs] + WS[WebSocket Live Events] + AS4[AS4 Gateway] + end + + subgraph Core["Business Core"] + Policy[Tenant + Partner Policy] + EDI[EDIFACT Processor] + Intake[Invoice Intake] + Validation[XSLT Validation] + Catalog[Catalog Import] + Availability[Availability Publisher] + Procurement[Procurement Comparator] + SupportTools[Privacy-Safe MCP Tools] + Orchestrator[Pipeline Orchestrator] + end + + subgraph State["Durable State"] + Objects[Object Store] + Locks[DB Ingest Locks] + JsonEvents[JSON Event Evidence] + IibinEvents[IIBIN Event Store] + CDN[CDN Cache] + Telemetry[Telemetry] + end + + Buyers -->|AS4 UBL / CII| AS4 + Buyers -->|EDIFACT ORDERS / INVOIC| EDI + Suppliers -->|catalog feeds| Catalog + Suppliers -->|price + stock APIs| H2 + Authority <-->|AS4 receipts / reports| AS4 + Shop <-->|search, orders, account data| H1 + Shop <-->|stock and order events| WS + Support --> SupportTools + Ops --> H1 + + AS4 --> Policy --> Intake --> Validation --> Orchestrator + EDI --> Orchestrator + Catalog --> Locks --> Objects --> CDN + H2 --> Procurement --> Objects + Availability --> WS + SupportTools --> Objects + Orchestrator --> JsonEvents --> IibinEvents + Orchestrator --> Objects + Edge --> Telemetry + Core --> Telemetry +``` + +The edge runtime owns protocol work and does not decide business acceptance. +AS4 handles signed and encrypted exchange, HTTP handles platform APIs, +WebSocket delivers committed live changes, and supplier integrations use +bounded HTTP awaitables. The core normalizes every external message into a +workflow state with object-store evidence and a typed event. + +## End-to-End Process + +```mermaid +sequenceDiagram + participant Partner as Partner / Access Point + participant Edge as King Edge Runtime + participant Policy as Tenant Policy + participant Store as Object Store + participant Pipe as Pipeline Orchestrator + participant Json as JSON Event Evidence + participant Iibin as IIBIN Event Store + participant Ops as Operations + + Partner->>Edge: AS4 / EDIFACT / Supplier API payload + Edge->>Policy: authenticate tenant, partner, certificate, role + Policy-->>Edge: policy context + Edge->>Store: store immutable raw payload + Edge->>Json: write readable platform event + Edge->>Iibin: write Next Level IIBIN event + Edge->>Pipe: dispatch workflow with event object id + Pipe->>Store: persist normalized document, report, or decision + Pipe-->>Partner: receipt, rejection, or pending response + Pipe-->>Ops: status, trace id, failure category +``` + +Transport success is not business acceptance. Between receipt and final +acceptance the platform uses explicit states: `received`, `duplicate`, +`validated`, `rejected`, `authority_pending`, `accepted`, `manual_review`, or +`aborted`. + +## AS4 Invoice Intake + +### Architecture + +```mermaid +flowchart TB + AP[External Access Point] + Gateway[AS4 Gateway] + Certs[Certificate Store] + Policy[Partner Policy] + Idem[Idempotency Ledger] + Raw[Raw AS4 Envelope] + Json[JSON InvoiceIntakeEvent] + Iibin[Next Level IIBIN] + Workflow[Invoice Workflow] + Receipt[Signed AS4 Receipt] + + AP --> Gateway + Gateway --> Certs --> Policy --> Idem + Idem -->|new| Raw --> Json --> Iibin --> Workflow --> Receipt + Idem -->|duplicate| Receipt +``` + +### Process + +```mermaid +sequenceDiagram + participant AP as Access Point + participant AS4 as AS4 Gateway + participant Policy as Policy Engine + participant Store as Object Store + participant Events as Event Store + participant Pipe as Pipeline + + AP->>AS4: submit signed/encrypted envelope + AS4->>Policy: validate cert, tenant, partner, role + Policy-->>AS4: allowed context + AS4->>Store: store raw AS4 envelope + AS4->>Events: store JSON event + AS4->>Events: store IIBIN event + AS4->>Pipe: dispatch workflow + Pipe-->>AS4: accepted for processing + AS4-->>AP: signed receipt +``` + +```php + 'application/soap+xml', + 'object_type' => 'as4-inbound-envelope', + 'tenant_id' => $tenantId, + 'partner_id' => $partnerId, + 'idempotency_key' => $idempotencyKey, +]); +``` + +#### Event Payload: JSON + +```php + $tenantId, + 'message_id' => $messageId, + 'partner_id' => $partnerId, + 'raw_object_id' => $rawObjectId, + 'transport' => 'as4', + 'state' => 'received', + 'trace_id' => 'as4-' . $messageId, +]; + +ObjectStore::put('events/invoice-intake/' . $messageId . '.json', json_encode($eventPayload, JSON_THROW_ON_ERROR), [ + 'content_type' => 'application/json', + 'object_type' => 'invoice-intake-event-json', +]); +``` + +#### Next Level: IIBIN + +```php + ['tag' => 1, 'type' => 'int32', 'required' => true], + 'message_id' => ['tag' => 2, 'type' => 'string', 'required' => true], + 'partner_id' => ['tag' => 3, 'type' => 'string', 'required' => true], + 'raw_object_id' => ['tag' => 4, 'type' => 'string', 'required' => true], + 'transport' => ['tag' => 5, 'type' => 'string', 'default' => 'as4'], + 'state' => ['tag' => 6, 'type' => 'string', 'required' => true], + 'trace_id' => ['tag' => 7, 'type' => 'string', 'required' => true], +]); + +$eventObjectId = 'events/invoice-intake/' . $messageId . '.iibin'; +ObjectStore::put($eventObjectId, king_proto_encode('InvoiceIntakeEvent', $eventPayload), [ + 'content_type' => 'application/x-king-iibin', + 'object_type' => 'invoice-intake-event', +]); + +PipelineOrchestrator::dispatch( + ['event_object_id' => $eventObjectId], + [['tool' => 'verify-as4-envelope'], ['tool' => 'extract-business-document'], ['tool' => 'validate-invoice-profile']], + ['trace_id' => $eventPayload['trace_id']] +); +``` + +The AS4 gateway is a trust boundary. It validates certificates, partner +policy, tenant ownership, and idempotency before the business workflow sees +anything. The event references the raw envelope instead of duplicating invoice +content into the event stream. + +## Invoice Validation + +### Architecture + +```mermaid +flowchart LR + XML[Extracted XML Invoice] + Detect[Profile Detection] + XSLT[XSLT Processor] + Report[SVRL Report Object] + Ledger[Invoice Ledger] + Reject[Structured Rejection] + Json[JSON Validation Event] + Iibin[Next Level IIBIN] + + XML --> Detect --> XSLT --> Report + Report -->|valid| Ledger + Report -->|invalid| Reject + Report --> Json --> Iibin +``` + +### Process + +```mermaid +sequenceDiagram + participant Pipe as Pipeline + participant XSLT as King XSLT Processor + participant Store as Object Store + participant Ledger as Invoice Ledger + participant Events as Event Store + + Pipe->>XSLT: run profile ruleset + XSLT->>Store: store SVRL report + Pipe->>Ledger: persist accepted/rejected state + Pipe->>Events: JSON validation event + Pipe->>Events: IIBIN validation event +``` + +```php + __DIR__ . '/rules', + 'properties' => ['http://saxon.sf.net/feature/version-warning' => 'false'], +]); + +$processor->transformToFile($invoiceXmlPath, profile_ruleset_path($profile), $svrlPath, [ + 'properties' => ['indent' => 'yes'], +]); + +$errorCount = count_svrl_failed_asserts($svrlPath); +$status = $errorCount === 0 ? 'valid' : 'invalid'; +$reportObjectId = 'validation/' . $tenantId . '/' . $invoiceId . '.svrl.xml'; + +king_object_store_put($reportObjectId, file_get_contents($svrlPath), [ + 'content_type' => 'application/xml', + 'object_type' => 'invoice-validation-report', + 'tenant_id' => $tenantId, +]); +``` + +#### Event Payload: JSON + +```php + $tenantId, + 'invoice_id' => $invoiceId, + 'profile' => $profile, + 'status' => $status, + 'report_object_id' => $reportObjectId, + 'error_count' => $errorCount, +]; + +king_object_store_put('events/invoice-validation/' . $invoiceId . '.json', json_encode($eventPayload, JSON_THROW_ON_ERROR), [ + 'content_type' => 'application/json', +]); +``` + +#### Next Level: IIBIN + +```php + ['tag' => 1, 'type' => 'int32', 'required' => true], + 'invoice_id' => ['tag' => 2, 'type' => 'string', 'required' => true], + 'profile' => ['tag' => 3, 'type' => 'string', 'required' => true], + 'status' => ['tag' => 4, 'type' => 'string', 'required' => true], + 'report_object_id' => ['tag' => 5, 'type' => 'string', 'required' => true], + 'error_count' => ['tag' => 6, 'type' => 'int32', 'default' => 0], +]); + +king_object_store_put('events/invoice-validation/' . $invoiceId . '.iibin', king_proto_encode('InvoiceValidationEvent', $eventPayload), [ + 'content_type' => 'application/x-king-iibin', +]); +``` + +Validation failures are not generic runtime failures. XML parse errors, +schema violations, Schematron assertions, duplicate documents, profile +mismatches, and external authority rejections should become separate states +that can be shown to support, customers, and operations. + +## EDIFACT and Procurement + +```mermaid +flowchart LR + EDI[EDIFACT Inbox] + Parser[Interchange Parser] + Mapper[Canonical Mapper] + Procurement[Procurement Comparator] + Suppliers[Supplier APIs] + Offers[Offer Cache] + Decision[Purchase Decision] + Json[JSON Decision Event] + Iibin[Next Level IIBIN] + + EDI --> Parser --> Mapper --> Procurement + Procurement --> Suppliers --> Procurement + Procurement --> Offers + Procurement --> Decision --> Json --> Iibin +``` + +```mermaid +sequenceDiagram + participant Buyer as Buyer ERP + participant EDI as EDIFACT Processor + participant Proc as Procurement + participant Suppliers as Supplier APIs + participant Store as Object Store + participant Events as Event Store + + Buyer->>EDI: ORDERS + EDI->>Proc: canonical purchase request + Proc->>Suppliers: concurrent QUERY requests + Suppliers-->>Proc: price, stock, lead time + Proc->>Store: cache offer set + Proc->>Events: JSON + IIBIN decision event +``` + +```php + $url) { + $awaitables[$supplierId] = king_client_send_request_async( + $url, + 'QUERY', + ['accept' => 'application/json', 'content-type' => 'application/query'], + build_supplier_query($sku, $quantity, $deliveryCountry), + ['timeout_ms' => 1500] + ); +} + +$offers = collect_supplier_offers($awaitables, 2000); +$decision = choose_supplier_offer($offers, [ + 'quantity' => $quantity, + 'currency' => 'EUR', + 'required_delivery_date' => $requiredDeliveryDate, +]); + +$offerObjectId = 'procurement/offers/' . $decision['request_id'] . '.json'; +king_object_store_put($offerObjectId, json_encode($offers, JSON_THROW_ON_ERROR), [ + 'content_type' => 'application/json', + 'cache_ttl_sec' => 120, +]); +``` + +#### Event Payload: JSON + +```php + $tenantId, + 'sku' => $sku, + 'quantity' => $quantity, + 'chosen_supplier_id' => $decision['supplier_id'], + 'currency' => $decision['currency'], + 'total_cents' => $decision['total_cents'], + 'offer_cache_object_id' => $offerObjectId, +]; + +king_object_store_put('events/procurement/' . $decision['request_id'] . '.json', json_encode($eventPayload, JSON_THROW_ON_ERROR), [ + 'content_type' => 'application/json', +]); +``` + +#### Next Level: IIBIN + +```php + ['tag' => 1, 'type' => 'int32', 'required' => true], + 'sku' => ['tag' => 2, 'type' => 'string', 'required' => true], + 'quantity' => ['tag' => 3, 'type' => 'int32', 'required' => true], + 'chosen_supplier_id' => ['tag' => 4, 'type' => 'string', 'required' => true], + 'currency' => ['tag' => 5, 'type' => 'string', 'default' => 'EUR'], + 'total_cents' => ['tag' => 6, 'type' => 'int32', 'required' => true], + 'offer_cache_object_id' => ['tag' => 7, 'type' => 'string', 'required' => true], +]); + +king_object_store_put('events/procurement/' . $decision['request_id'] . '.iibin', king_proto_encode('ProcurementDecisionEvent', $eventPayload), [ + 'content_type' => 'application/x-king-iibin', +]); +``` + +Procurement decisions include price, lead time, stock, minimum order quantity, +currency, tax handling, delivery constraints, reliability, and cached fallback +policy. The event references the full offer set instead of embedding it. + +## Catalog and Live Availability + +```mermaid +flowchart TB + Feeds[Supplier Catalog Feeds] + Import[Catalog Import] + Lock[DB Ingest Lock] + Snapshot[Catalog Snapshot] + CDN[CDN Publication] + ERP[ERP / Warehouse] + Availability[Availability Publisher] + WS[WebSocket Topic] + Shop[B2B Shop] + Json[JSON Catalog Event] + Iibin[Next Level IIBIN] + + Feeds --> Import --> Lock --> Snapshot --> CDN --> Shop + ERP --> Availability --> Snapshot + Availability --> WS --> Shop + Import --> Json --> Iibin +``` + +```php + __DIR__ . '/var/catalog-' . $tenantId . '.lock', + 'timeout_ms' => 5000, +]); + +$snapshotObjectId = 'catalog/snapshots/' . $tenantId . '/' . $importId . '.json'; +king_object_store_put($snapshotObjectId, json_encode($result['snapshot'], JSON_THROW_ON_ERROR), [ + 'content_type' => 'application/json', + 'object_type' => 'catalog-snapshot', + 'cache_ttl_sec' => 300, +]); + +king_cdn_cache_object($snapshotObjectId, ['ttl_sec' => 300]); +``` + +#### Event Payload: JSON + +```php + $tenantId, + 'supplier_id' => $supplierId, + 'snapshot_object_id' => $snapshotObjectId, + 'changed_count' => count($result['changed_skus']), +]; + +king_object_store_put('events/catalog/' . $importId . '.json', json_encode($eventPayload, JSON_THROW_ON_ERROR), [ + 'content_type' => 'application/json', +]); +``` + +#### Next Level: IIBIN + +```php + ['tag' => 1, 'type' => 'int32', 'required' => true], + 'supplier_id' => ['tag' => 2, 'type' => 'string', 'required' => true], + 'snapshot_object_id' => ['tag' => 3, 'type' => 'string', 'required' => true], + 'changed_count' => ['tag' => 4, 'type' => 'int32', 'required' => true], +]); + +king_object_store_put('events/catalog/' . $importId . '.iibin', king_proto_encode('CatalogPublishedEvent', $eventPayload), [ + 'content_type' => 'application/x-king-iibin', +]); +``` + +Catalog publication is two-phase: commit the new state first, then publish +cacheable objects and live notifications. WebSocket clients can always recover +from the latest snapshot after reconnect. + +## Privacy-Safe MCP Support + +```mermaid +flowchart TB + Chat[Support Chat UI] + API[Support API] + Authz[Tenant Authorization] + Purpose[Purpose Check] + Redact[PII Redaction] + Policy[Tool Allowlist] + MCP[In-Region King MCP Peer] + Audit[Support Audit] + Human[Human Agent] + + Chat --> API --> Authz --> Purpose --> Redact --> Policy --> MCP + MCP --> Audit + API --> Human +``` + +```mermaid +sequenceDiagram + participant Agent as Support Agent + participant API as Support API + participant Privacy as Privacy Gate + participant MCP as King MCP Peer + participant Events as Event Store + + Agent->>API: ask invoice support question + API->>Privacy: authorize tenant, purpose, role, data class + Privacy-->>API: redacted tool input + API->>MCP: call allowlisted internal IIBIN route + MCP-->>API: answer with source metadata + API->>Events: JSON + IIBIN audit event +``` + +```php + ['tag' => 1, 'type' => 'string', 'required' => true], + 'support_case_id' => ['tag' => 2, 'type' => 'string', 'required' => true], + 'purpose' => ['tag' => 3, 'type' => 'string', 'required' => true], + 'question' => ['tag' => 4, 'type' => 'string', 'required' => true], + 'invoice_ref' => ['tag' => 5, 'type' => 'string', 'required' => true], + 'allowed_fields' => ['tag' => 6, 'type' => 'string', 'repeated' => true], +]); + +king_proto_define_schema('SupportToolResponse', [ + 'answer' => ['tag' => 1, 'type' => 'string', 'required' => true], + 'confidence' => ['tag' => 2, 'type' => 'double'], + 'source_count' => ['tag' => 3, 'type' => 'uint32'], +]); + +$toolInput = build_redacted_support_context( + tenantId: $tenantId, + caseId: $caseId, + question: $customerQuestion, + invoiceId: $invoiceId, + allowedFields: ['status', 'received_at', 'rejection_code'] +); + +assert_support_tool_allowed($currentUser, $tenantId, 'support.faq', $toolInput); + +$mcpConfig = new Config([ + 'mcp.default_request_timeout_ms' => 1500, + 'mcp.iibin_routes' => [ + 'support.faq/answer' => [ + 'request_schema' => 'SupportToolRequest', + 'response_schema' => 'SupportToolResponse', + 'decode_as_object' => false, + ], + ], +]); + +$mcp = new MCP('127.0.0.1', 9090, $mcpConfig); +$answer = $mcp->requestIibin('support.faq', 'answer', $toolInput); +``` + +#### Event Payload: JSON + +```php + $tenantId, + 'support_case_id' => $caseId, + 'tool' => 'support.faq', + 'purpose' => 'support_case_answer', + 'input_classification' => 'redacted-support-context', + 'retention_policy' => 'support-audit-180d', +]; + +king_object_store_put('events/support-tools/' . $caseId . '.json', json_encode($eventPayload, JSON_THROW_ON_ERROR), [ + 'content_type' => 'application/json', +]); +``` + +#### Next Level: IIBIN + +```php + ['tag' => 1, 'type' => 'int32', 'required' => true], + 'support_case_id' => ['tag' => 2, 'type' => 'string', 'required' => true], + 'tool' => ['tag' => 3, 'type' => 'string', 'required' => true], + 'purpose' => ['tag' => 4, 'type' => 'string', 'required' => true], + 'input_classification' => ['tag' => 5, 'type' => 'string', 'required' => true], + 'retention_policy' => ['tag' => 6, 'type' => 'string', 'required' => true], +]); + +king_object_store_put('events/support-tools/' . $caseId . '.iibin', king_proto_encode('SupportToolAuditEvent', $eventPayload), [ + 'content_type' => 'application/x-king-iibin', +]); +``` + +The King MCP peer receives a pseudonymous invoice reference and an allowlist +of fields, not raw invoice XML, PDFs, addresses, VAT numbers, payment data, +private-person identifiers, or full chat transcripts. The IIBIN schemas are +fixed on the connection config and cannot be changed per support call. Broader +access requires manual review and an explicit access request. + +## Runtime Operations + +```mermaid +flowchart LR + Runtime[King System Runtime] + Telemetry[Telemetry] + DNS[Semantic DNS] + Scale[Autoscaling] + Workers[Pipeline Workers] + Ops[Operations Console] + Json[JSON Runtime Event] + Iibin[Next Level IIBIN] + + Runtime --> Telemetry --> Ops + Runtime --> DNS --> Ops + Runtime --> Scale --> Workers + Runtime --> Json --> Iibin --> Ops +``` + +#### Event Payload: JSON + +```php + 'b2b-einvoice-platform', + 'node_id' => getenv('KING_NODE_ID') ?: 'node-1', + 'state_root_path' => __DIR__ . '/var/system', + 'components' => ['client', 'server', 'object_store', 'pipeline_orchestrator', 'telemetry', 'mcp', 'iibin'], +]); + +$eventPayload = [ + 'cluster_id' => 'b2b-einvoice-platform', + 'node_id' => getenv('KING_NODE_ID') ?: 'node-1', + 'component' => 'pipeline_orchestrator', + 'status' => 'ready', + 'blocker_count' => 0, +]; + +king_object_store_put('events/runtime/' . date('YmdHis') . '.json', json_encode($eventPayload, JSON_THROW_ON_ERROR), [ + 'content_type' => 'application/json', +]); +``` + +#### Next Level: IIBIN + +```php + ['tag' => 1, 'type' => 'string', 'required' => true], + 'node_id' => ['tag' => 2, 'type' => 'string', 'required' => true], + 'component' => ['tag' => 3, 'type' => 'string', 'required' => true], + 'status' => ['tag' => 4, 'type' => 'string', 'required' => true], + 'blocker_count' => ['tag' => 5, 'type' => 'int32', 'default' => 0], +]); + +king_object_store_put('events/runtime/' . date('YmdHis') . '.iibin', king_proto_encode('RuntimeComponentEvent', $eventPayload), [ + 'content_type' => 'application/x-king-iibin', +]); +``` + +Operations must expose lifecycle state, admission decisions, blocked pipeline +runs, degraded dependencies, and autoscaling decisions as first-class data. +Deep diagnostics belong in telemetry and object-store reports; JSON/IIBIN +events keep the control plane fast and readable. diff --git a/docs/fine-tuning.md b/docs/fine-tuning.md new file mode 100644 index 000000000..934244c2c --- /dev/null +++ b/docs/fine-tuning.md @@ -0,0 +1,56 @@ +# Fine-Tuning Preparation + +King does not fine-tune an already quantized GGUF artifact in place. GGUF is +the local inference artifact. For supervised tuning, King uses the GGUF model as +the tokenizer and runtime compatibility reference, while the training step needs +a trainable base checkpoint for adapter training. + +The first target is the compact King Coder baseline around `gemma3:1b`. That +model remains part of the normal runtime profile because it is useful for fast +local diagnostics and low-latency editor assistance. The fine-tune goal is not +general chat polish; it is command selection, exact output formatting, correct +King/PHP snippets, and predictable handling of local router contracts. A larger +GPU model may become the preferred interactive default, but the 1B baseline +must still carry the same capability floor and stay reproducible. + +The repository tool for the first phase is: + +```bash +bin/king-coder-fine-tune prepare \ + --model=var/inference-models/gemma3-1b.gguf \ + --out=var/fine-tuning/gemma3-1b-coder +``` + +That command runs through PHP with the King extension loaded. It extracts +source-grounded code examples from the King docs, builds OpenAI-style chat JSONL +for coder tuning, validates each example with `king_inference_tokenize()`, and +writes a reproducible run directory under `var/fine-tuning/`. + +The output contains: + +- `train.jsonl` +- `validation.jsonl` +- `manifest.json` +- `run.md` + +The manifest deliberately separates the tokenizer artifact from the trainable +base checkpoint. A run without `--trainable-base=/path/to/checkpoint-dir` is a +prepared dataset, not a completed fine-tune. That is intentional: claiming a +fine-tuned model without training adapter weights would be worse than doing +nothing. + +## Intended Flow + +1. Build or install the King extension. +2. Prepare the coder dataset with `bin/king-coder-fine-tune prepare`. +3. Provide a real trainable base checkpoint that matches the runtime family. +4. Train an adapter against `train.jsonl` and evaluate against + `validation.jsonl`. +5. Merge or load the adapter according to the selected runtime path. +6. Export the final runtime artifact and configure it through the normal King + inference config. + +The current implemented King-owned part is dataset extraction, tokenizer +validation, split generation, and run metadata. Native in-King optimizer and +backpropagation kernels are not implemented yet, so the tool fails closed +instead of pretending a training run happened. diff --git a/docs/gpu-readiness.md b/docs/gpu-readiness.md new file mode 100644 index 000000000..6267b59fc --- /dev/null +++ b/docs/gpu-readiness.md @@ -0,0 +1,195 @@ +# GPU Readiness Runbook + +This page is the operator-facing preflight for a local King GPU inference +deployment. It is intentionally about runtime admission, not model quality. +The goal is to answer four questions before traffic is sent to the router: + +- Is the intended GPU model profile selected? +- Can King see the CUDA driver, artifact, VRAM budget, and thermal monitor? +- Is plain-text generation currently admitted? +- If it is not admitted, what exact refusal reasons must be fixed? + +For production-like local operation, pin the runtime profile to `gpu`. `auto` +is useful on developer machines where the CPU profile may be acceptable when no +GPU artifact is configured. A pinned `gpu` profile fails closed instead of +falling back to CPU. + +## php.ini Profile + +Use a dedicated PHP ini fragment for the local inference process. The router +and any operator preflight script should start with the same fragment, so the +status probe and the serving process see the same process-level settings. +The bundled OpenAI-compatible router resolves its active model through the +King runtime config object; do not pass separate model or GPU-layer environment +overrides to the router as a second source of truth. + +```ini +extension=/opt/king/extension/modules/king.so + +king.gpu_bindings_enable=1 +king.gpu_default_backend=cuda + +king.inference_preferred_model_profile=gpu +king.inference_cpu_model_name=gemma3:1b +king.inference_cpu_model_artifact=/var/lib/king/models/gemma3-1b.gguf +king.inference_gpu_model_name=gemma4:12b +king.inference_gpu_model_artifact=/var/lib/king/models/gemma4-12b.gguf + +king.inference_gpu_max_gpu_layers=99 +king.inference_gpu_vram_reserve_mb=2048 +king.inference_gpu_min_free_vram_mb=4096 + +king.inference_gpu_thermal_sensor_path= +king.inference_gpu_thermal_sensor_command=nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader,nounits +king.inference_gpu_thermal_max_temperature_c=78 +king.inference_gpu_thermal_check_interval_sec=15 +king.inference_gpu_allow_unmonitored=0 + +king.inference_with_memory=0 +king.inference_llm_cache_enable=0 +``` + +Keep the CPU model configured even when the process is pinned to `gpu`. It +keeps the same config usable for explicit CPU preflights and for environments +that intentionally switch to `auto`. It must not be used as an implicit +fallback for a request that selected the GPU profile. + +## Sensor Check + +Before starting King, verify that the configured thermal source returns one +numeric Celsius value. This command is the common NVIDIA path: + +```bash +nvidia-smi --query-gpu=name,memory.free,temperature.gpu --format=csv,noheader,nounits +``` + +For a sysfs sensor path, the file normally returns millidegrees Celsius: + +```bash +cat /sys/class/hwmon/hwmon0/temp1_input +``` + +If no thermal source is configured and `king.inference_gpu_allow_unmonitored=0`, +King refuses GPU inference. Setting `king.inference_gpu_allow_unmonitored=1` is +an explicit operator decision and should not be the default on a workstation. + +## Preflight Command + +`bin/king-inference-status` is safe to run before starting the local router. It +does not send a generation request first. It checks the selected runtime +profile, the raw GPU readiness payload, the loaded model metadata, and the +router-facing model listing flags. + +The command loads the same PHP ini fragment as `bin/king-openai-router` by +default: + +```bash +bin/king-inference-status --require-gpu +``` + +Use `--json` when a supervisor, deployment script, or health gate needs the +machine-readable report: + +```bash +bin/king-inference-status --require-gpu --json +``` + +Override the PHP binary, extension path, or ini fragment through the same +environment variables as the router: + +```bash +PHP_BIN=/usr/bin/php8.4 \ +KING_EXTENSION=/opt/king/extension/modules/king.so \ +KING_INFERENCE_PHP_INI=/opt/king/infra/inference/local-gpu.php.ini \ +bin/king-inference-status --require-gpu +``` + +Exit code `0` means the selected model can serve local OpenAI-compatible chat. +Exit code `2` means `--require-gpu` was set but the active config did not select +`king_native_gpu`. Exit code `3` means the selected profile loaded but King +refused generation for concrete runtime reasons. Exit code `1` means the status +command itself hit a config or runtime error before readiness could be +evaluated. + +## Interpreting Readiness + +`king_inference_gpu_runtime_status()` and `King\Inference::gpuRuntimeStatus()` +return the pre-load process and config view. The loaded model's `gpu_runtime` +payload adds artifact and KV-cache based admission details. + +Important fields: + +- `config_ready`: King can see the GPU-facing configuration, artifact, driver + signal, VRAM policy, and thermal policy. +- `model_vram_admitted`: the configured model artifact passes the pre-load + VRAM admission check. +- `runtime_vram_fits_free`: the loaded model plus estimated KV-cache fits after + the configured reserve. +- `decoder_kernel_ready`: the native GPU decoder graph and prompt loop are + ready. +- `generation_ready`: runtime policy and decoder readiness both admit + plain-text generation. +- `reason`: the first operator-facing blocker. +- `refusal_reasons`: every currently active blocker in ordered form. +- `silent_cpu_fallback`: must remain `false` for GPU profiles. + +The OpenAI router repeats the executable contract under +`x_king.client_capabilities`. UI and editor integrations should use +`gpu_generation_ready`, `requires_gpu`, `gpu_runtime_required`, and +`openai_chat_completions` from that object instead of guessing from the model +name. + +## Common Refusal Reasons + +`gpu_thermal_monitor_missing` means no sensor path or command is configured and +unmonitored GPU use is not allowed. Configure a sensor or explicitly accept +unmonitored operation. + +`gpu_free_vram_below_configured_floor` means the driver reports less free VRAM +than `king.inference_gpu_min_free_vram_mb`. Close other GPU consumers or lower +the floor intentionally. + +`gpu_required_vram_exceeds_free_vram_after_reserve` means the model plus +estimated KV cache does not fit after `king.inference_gpu_vram_reserve_mb` is +subtracted. Use a smaller model, lower context requirements, reduce GPU layers +only if the selected backend supports that mode, or adjust the reserve. + +`gpu_cuda_context_unavailable`, `gpu_device_memory_allocator_unavailable`, and +`gpu_required_weight_upload_incomplete` are runtime blockers after King sees the +driver. They normally indicate driver/library access, CUDA symbol resolution, +or artifact tensor resolution problems. + +## Startup Gate + +The local router can start before `generation_ready=true` if operators want +`GET /v1/models` to expose the current refusal details. Do not mark the route +healthy for user traffic until the preflight script exits `0`. + +For a hard serving gate, run the preflight first and start the router only when +it succeeds: + +```bash +bin/king-inference-status --require-gpu +exec bin/king-openai-router +``` + +This preserves the King contract: a selected GPU profile either runs on the GPU +or fails with explicit runtime reasons. It does not silently burn CPU for a +large GPU model. + +## Router Log States + +`bin/king-openai-router` writes structured readiness lines to STDERR: + +- `state=configured` is emitted once after environment variables and PHP ini + values have been resolved. It reports host, port, selected profile, backend, + CPU/GPU artifact paths, GPU layers, VRAM floors, and the thermal source. +- `state=admitted` is emitted for every registered model after model load and + metadata inspection. CPU lines report generation readiness. GPU lines include + `config_ready`, `generation_ready`, `reason`, and ordered refusal reasons. + If a readable GPU artifact exists but GPU bindings or layers are disabled, + the router emits `admitted=no` with a concrete reason. +- `state=executing` is emitted for every incoming request immediately before + it is routed through `king_inference_openai_http_response()`. The line reports + method, path, requested model, and registered model count. Prompt, message, + and response content are intentionally not logged. diff --git a/docs/http1.md b/docs/http1.md new file mode 100644 index 000000000..28d4972cd --- /dev/null +++ b/docs/http1.md @@ -0,0 +1,124 @@ +# HTTP/1 + +HTTP/1 can be used directly through `king_http1_request_send()` or through +`King\Client\Http1Client`. The client can also send the HTTP `QUERY` method. + +## Internal Layout + +The PHP-visible client/session/stream/response object contracts live in +`extension/include/client/objects.h`. HTTP/1 request runtime code lives under +`extension/src/client/http1/`; shared client object glue lives in +`extension/src/client/object.inc`. + +The client module owns its PHP binding metadata under +`extension/include/client/`, including `arginfo/`, `function_entries.h`, and +the client OO method-entry headers. Those declarations are consumed by the root +extension bootstrap through `extension/include/php_king/`. + +Server listener binding metadata lives under `extension/include/server/`, +including `arginfo/` and `function_entries.h`, with runtime implementation +anchored under `extension/src/server/`. + +## Function, Example 1: Simple GET + +```php + 'application/json'], + null, + ['timeout_ms' => 1500] +); + +if ($response === false) { + throw new RuntimeException(king_get_last_error()); +} + +echo $response['status'] . PHP_EOL; +echo $response['body'] . PHP_EOL; +``` + +## Function, Example 2: QUERY with Body and Streaming Response + +```php + 'application/query', + 'accept' => 'application/json', + ], + 'tenant_id = 42 AND status = "accepted"', + [ + 'timeout_ms' => 3000, + 'response_stream' => true, + ] +); + +if ($context === false) { + throw new RuntimeException(king_get_last_error()); +} + +$response = king_receive_response($context); +if ($response->getStatusCode() !== 200) { + throw new RuntimeException($response->getBody()); +} + +while (!$response->isEndOfBody()) { + echo $response->read(8192); +} +``` + +## OO, Example 1: Http1Client + +```php +request('GET', 'http://127.0.0.1:8080/health', [ + 'accept' => 'application/json', +]); + +echo $response->getStatusCode() . PHP_EOL; +echo $response->getBody() . PHP_EOL; + +$client->close(); +``` + +## OO, Example 2: Request Service with CancelToken + +```php +client->request( + 'QUERY', + 'http://127.0.0.1:8080/invoices/search', + ['content-type' => 'application/query', 'accept' => 'application/json'], + sprintf('tenant_id = %d AND status = "%s"', $tenantId, $status), + $cancel + ); + + if ($response->getStatusCode() !== 200) { + throw new RuntimeException($response->getBody()); + } + + return json_decode($response->getBody(), true, flags: JSON_THROW_ON_ERROR); + } +} + +$service = new InvoiceSearchClient(new Http1Client()); +var_dump($service->search(42, 'accepted')); +``` diff --git a/docs/http2.md b/docs/http2.md new file mode 100644 index 000000000..c2ff2c8cb --- /dev/null +++ b/docs/http2.md @@ -0,0 +1,110 @@ +# HTTP/2 + +HTTP/2 uses the libcurl-backed runtime. Single requests use +`king_http2_request_send()`, while multiplexing uses +`king_http2_request_send_multi()`. OO code uses `King\Client\Http2Client`. + +## Function, Example 1: Single Request + +```php + 'application/json'], + null, + [ + 'timeout_ms' => 2000, + 'ca_file' => __DIR__ . '/certs/ca.pem', + ] +); + +if ($response === false) { + throw new RuntimeException(king_get_last_error()); +} + +echo $response['status'] . PHP_EOL; +echo $response['protocol'] . PHP_EOL; +``` + +## Function, Example 2: Multiplex Against One Origin + +```php + 'https://invoice-api.internal.local/v1/invoices/INV-1001', + 'method' => 'GET', + 'headers' => ['accept' => 'application/json'], + ], + [ + 'url' => 'https://invoice-api.internal.local/v1/invoices/INV-1002', + 'method' => 'GET', + 'headers' => ['accept' => 'application/json'], + ], +], [ + 'timeout_ms' => 3000, + 'capture_push' => true, + 'ca_file' => __DIR__ . '/certs/ca.pem', +]); + +if ($responses === false) { + throw new RuntimeException(king_get_last_error()); +} + +foreach ($responses as $response) { + printf("%d %s\n", $response['status'], $response['effective_url']); +} +``` + +## OO, Example 1: Http2Client + +```php + __DIR__ . '/certs/ca.pem', +])); + +$response = $client->request( + 'GET', + 'https://invoice-api.internal.local/v1/tenants/42', + ['accept' => 'application/json'] +); + +echo $response->getStatusCode() . PHP_EOL; +$client->close(); +``` + +## OO, Example 2: Async HTTP/2 with Error Handling + +```php + __DIR__ . '/certs/ca.pem', +])); + +$awaitable = $client->requestAsync( + 'POST', + 'https://invoice-api.internal.local/v1/invoices', + ['content-type' => 'application/json'], + json_encode(['id' => 'INV-1003'], JSON_THROW_ON_ERROR) +); + +try { + $response = $awaitable->await(3000); + echo $response->getStatusCode() . PHP_EOL; + echo $response->getBody() . PHP_EOL; +} catch (TimeoutException $e) { + $awaitable->cancel(); + throw $e; +} finally { + $client->close(); +} +``` diff --git a/docs/http3.md b/docs/http3.md new file mode 100644 index 000000000..deee5b0ab --- /dev/null +++ b/docs/http3.md @@ -0,0 +1,113 @@ +# HTTP/3 + +HTTP/3 uses QUIC and returns additional transport and TLS ticket metadata. +Procedural code uses `king_http3_request_send()` and +`king_http3_request_send_multi()`. OO code uses `King\Client\Http3Client`. + +## Function, Example 1: Single HTTP/3 Request + +```php + 'application/json'], + null, + [ + 'timeout_ms' => 2500, + 'ca_file' => __DIR__ . '/certs/ca.pem', + ] +); + +if ($response === false) { + throw new RuntimeException(king_get_last_error()); +} + +printf( + "status=%d resumed=%s lost=%d\n", + $response['status'], + ($response['tls_session_resumed'] ?? false) ? 'yes' : 'no', + $response['quic_packets_lost'] ?? 0 +); +``` + +## Function, Example 2: HTTP/3 Multiplex + +```php + 'https://invoice-api.internal.local/v1/invoices/INV-1001', + 'method' => 'GET', + 'headers' => ['accept' => 'application/json'], + ], + [ + 'url' => 'https://invoice-api.internal.local/v1/invoices/INV-1001/events', + 'method' => 'GET', + 'headers' => ['accept' => 'application/json'], + ], +], [ + 'timeout_ms' => 4000, + 'ca_file' => __DIR__ . '/certs/ca.pem', +]); + +if ($responses === false) { + throw new RuntimeException(king_get_last_error()); +} + +foreach ($responses as $response) { + echo $response['status'] . ' ' . ($response['stream_kind'] ?? 'h3') . PHP_EOL; +} +``` + +## OO, Example 1: Http3Client + +```php + __DIR__ . '/certs/ca.pem', + 'quic.ping_interval_ms' => 500, +])); + +$response = $client->request('GET', 'https://invoice-api.internal.local/v1/status'); +echo $response->getStatusCode() . PHP_EOL; + +$client->close(); +``` + +## OO, Example 2: Cancelable HTTP/3 Call + +```php + __DIR__ . '/certs/ca.pem', +])); + +$awaitable = $client->requestAsync( + 'POST', + 'https://invoice-api.internal.local/v1/reports', + ['content-type' => 'application/json'], + json_encode(['from' => '2026-01-01', 'to' => '2026-01-31'], JSON_THROW_ON_ERROR), + $cancel +); + +if (!$awaitable->poll(100)) { + $cancel->cancel(); + $awaitable->cancel(); +} + +if (!$awaitable->isCancelled()) { + $response = $awaitable->await(3000); + echo $response->getBody() . PHP_EOL; +} + +$client->close(); +``` diff --git a/docs/iibin.md b/docs/iibin.md new file mode 100644 index 000000000..e2ff39bf0 --- /dev/null +++ b/docs/iibin.md @@ -0,0 +1,98 @@ +# IIBIN + +IIBIN is King's native binary serialization format. The procedural API is +`king_proto_*`; the native OO facade is `King\IIBIN`. + +## Internal Layout + +The public C contract lives under `extension/include/iibin/`. The module +umbrella is `extension/include/iibin/index.h`, the procedural declarations are +in `extension/include/iibin/iibin.h`, and the binding arginfo is pulled through +`extension/include/iibin/arginfo/index.h`. + +The PHP binding metadata is owned by the module under `extension/include/iibin/`, +including `arginfo/`, `function_entries.h`, `class_method_entries.h`, and +`class_methods.h`. `extension/src/php_king.c` consumes those declarations +through `extension/include/php_king/` and only keeps the central extension +bootstrap and shared function table assembly. + +## Function, Example 1: Schema, Encode, Decode + +```php + ['tag' => 1, 'type' => 'string', 'required' => true], + 'tenant_id' => ['tag' => 2, 'type' => 'int32', 'required' => true], + 'currency' => ['tag' => 3, 'type' => 'string', 'default' => 'EUR'], +]); + +$binary = king_proto_encode('InvoiceHeader', [ + 'id' => 'INV-1001', + 'tenant_id' => 42, +]); + +$decoded = king_proto_decode('InvoiceHeader', $binary); +var_dump($decoded); +``` + +## Function, Example 2: Oneof and Batch + +```php + ['tag' => 1, 'type' => 'string', 'oneof' => 'result'], + 'rejected' => ['tag' => 2, 'type' => 'string', 'oneof' => 'result'], + 'source' => ['tag' => 3, 'type' => 'string'], +]); + +$records = king_proto_encode_batch('InvoiceEvent', [ + ['accepted' => 'NAV-OK-1001', 'source' => 'nav'], + ['rejected' => 'INVALID_VAT_SUMMARY', 'source' => 'validator'], +]); + +$events = king_proto_decode_batch('InvoiceEvent', $records); +var_dump($events); +``` + +## OO, Example 1: King\IIBIN + +```php + ['tag' => 1, 'type' => 'int32', 'required' => true], + 'name' => ['tag' => 2, 'type' => 'string'], +]); + +$payload = IIBIN::encode('TenantRef', ['tenant_id' => 42, 'name' => 'Acme GmbH']); +var_dump(IIBIN::decode('TenantRef', $payload)); +``` + +## OO, Example 2: Object Hydration + +```php + ['tag' => 1, 'type' => 'string', 'required' => true], + 'payable_cents' => ['tag' => 2, 'type' => 'int32', 'required' => true], + 'currency' => ['tag' => 3, 'type' => 'string', 'default' => 'EUR'], +]); + +$binary = IIBIN::encode('InvoiceTotal', [ + 'id' => 'INV-1004', + 'payable_cents' => 11900, +]); + +$invoice = IIBIN::decode('InvoiceTotal', $binary, InvoiceTotal::class); +echo $invoice->id . ' ' . $invoice->payable_cents . ' ' . $invoice->currency . PHP_EOL; +``` diff --git a/docs/inference.md b/docs/inference.md new file mode 100644 index 000000000..bf9754d68 --- /dev/null +++ b/docs/inference.md @@ -0,0 +1,1946 @@ +# Local Quantized Inference + +The interactive transformer-flow map lives at +[`docs/inference/index.html`](inference/index.html). + +King inference is available procedurally through `king_inference_*` and as the +native OO surface `King\Inference`, `King\Inference\Model`, and +`King\Inference\Stream`. + +This primitive is local and concrete: it registers a materialized GGUF model +artifact, parses the GGUF structure inside King, resolves an inference backend, +and streams backend output as King events. Model artifacts are passed as direct +filesystem paths through `artifact`, `artifact.path`, or `artifact_path`. +Those fields must be non-empty strings; object-store references are rejected +until they are materialized to a local GGUF path. +Optional public metadata fields are also strict: `name`, `quantization`, +`owned_by`, `embedding_tensor`, `token_embedding_tensor`, `output_tensor`, +`output_projection_tensor`, `lm_head_tensor`, +`attention_query_tensor_pattern`, `attention_key_tensor_pattern`, +`attention_value_tensor_pattern`, `attention_output_tensor_pattern`, +`rms_norm_attention_tensor_pattern`, `rms_norm_ffn_tensor_pattern`, and +`rms_norm_final_tensor`, `ffn_gate_tensor_pattern`, `ffn_up_tensor_pattern`, +and `ffn_down_tensor_pattern` must be non-empty strings when provided, and +`context_tokens` must be a positive integer. Invalid model metadata is rejected +during model load instead of being silently ignored by later model listings, +embedding routes, decoder graph construction, or runner argument mapping. +`king_inference_token_decode()` and `King\Inference\Model::tokenDecode()` +decode one native token id through the tokenizer loaded from the same GGUF +artifact. `king_inference_token_decode_graph()` and +`King\Inference\Model::tokenDecodeGraph()` build the complete native CPU +decode graph for one token position from the loaded model metadata and tensor +resolvers. The graph builder accepts either a direct token id or the tokenizer +output returned by `king_inference_tokenize()`; in the tokenizer-output form, +`tokens[position]` is validated and wired into the embedding operation. + +The implemented token-streaming backends are `local` and `king_native_cpu`. +`local` uses a King-owned process runner contract while the public backend name +stays independent from the runner implementation. The `king_native_cpu` backend +uses King's native GGUF loader, metadata parser, tokenizer lookup, paged +KV-cache planning, public tensor views, bounded tensor dequantization, first +CPU tensor/vector math, complete per-token decode graph construction, token +selection from logits, and a read-only memory map of the model artifact. Native +CPU streaming accepts explicit `graph` or `graphs` requests and decodes the +selected token ids through the artifact tokenizer; it does not call an external +inference runtime. For OpenAI-compatible plain-text chat requests, King renders +the validated `messages` into a deterministic prompt, tokenizes that prompt +with the loaded model tokenizer, runs prefix tokens through native CPU decode +graphs without emitting them, emits generated assistant tokens from the final +prompt state, and keeps that KV state transiently inside the one response +without enabling persistent graph memory. +For `stream=true`, the same native CPU prompt path emits OpenAI-compatible SSE +chunks from native token events, and `/v1/models` advertises that with +`x_king.openai_chat_completions_stream=true` plus +`x_king.capabilities.openai_chat_completions_stream=true`. UI integrations +should read the versioned `x_king.client_capabilities` object for selection and +button state instead of inferring support from the backend name. + +When `backend` is omitted, King selects `king_native_cpu`. The process-runner +backend is still available, but it must be selected intentionally with +`backend => 'local'`, `backend.name => 'local'`, `backend.type => 'local'`, or +a runner-bearing backend config. + +The repository ships `bin/king-local-infer` as the default local runner. It +loads the current King extension, materializes a GGUF model through the native +loader, builds token-step graphs through `king_inference_token_decode_graph()`, +keeps KV state between steps, and streams decoded token text on stdout for the +OpenAI-compatible router. Prompt processing passes the tokenizer output into the +graph builder, so each `tokens[position]` value enters the embedding operation +and then flows through every resolved transformer layer in the native graph. The +runner fails closed when `king_inference_graph_run()` does not return a +non-empty `state.kv_cache`, because later generated tokens must inherit the KV +entries written by earlier prompt and decode steps. It also owns the local +special-token policy: the prompt gets at most one leading BOS token when the +model metadata exposes one, generated BOS/EOS tokens stop generation before +they are decoded to text, and configured stop strings are withheld from stdout +instead of being emitted and recognized afterwards. + +`bin/king-native-hello-world` is the direct King-only native stream smoke for +the CPU and GPU backends. It resolves local GGUF artifacts from +`KING_INFERENCE_HELLO_CPU_MODEL_PATH`, +`KING_INFERENCE_HELLO_GPU_MODEL_PATH`, `KING_INFERENCE_HELLO_MODEL_PATH`, +`KING_INFERENCE_CPU_MODEL_PATH`, `KING_INFERENCE_GPU_MODEL_PATH`, +`KING_INFERENCE_MODEL_PATH`, `KING_INFERENCE_TEST_MODEL_PATH`, or +`var/inference-models/gemma3-1b.gguf`, then tokenizes `Hello world` and streams +the resulting token-vector graphs through `king_inference_stream()`. The CPU +path executes the native graph runner. The GPU path requires +`gpu.enabled=true`, opens the CUDA-backed model profile, refuses silent CPU +fallback, and emits token events for explicit token-vector graphs after GPU +graph admission succeeds. The command exits non-zero unless every requested +backend streams `Hello world`. + +The default backend is `both` and the default mode is `roundtrip`, so the plain +command is a deterministic CPU plus GPU `Hello world` proof. Use +`--backend=cpu`, `--backend=gpu`, or `--backend=both` to select the surface. +Use `--mode=prompt` only when you intentionally want a short native generation +smoke; prompt output depends on the current decoder quality and model behavior +and is not the deterministic Hello-World contract. The command does not contact +Ollama, vLLM, or another model server; the only model runtime in that path is +the King extension plus the local GGUF artifact. + +```bash +bin/king-native-hello-world --backend=both +KING_INFERENCE_HELLO_GPU_MODEL_PATH=/models/gemma3-1b.gguf bin/king-native-hello-world --backend=gpu +``` + +With `--json`, the command also emits the local performance measurement surface +used for native inference hardening: prompt token count, generated token count, +stream TTFB, stream total time, end-to-end time, model load time, tokens per +second, resident state, and GPU before/after/delta snapshots for temperature, +utilization, VRAM, and power. Set `KING_INFERENCE_GPU_POWER_MAX_WATTS` to enable +the optional power guardrail in that local smoke. + +For the local OpenAI-compatible router, `bin/king-openai-router` loads +`infra/inference/local-gpu.php.ini`. That local profile enables GPU bindings, +selects `gemma3:1b` for the CPU and GPU profiles, expects the materialized GGUF +artifact at `var/inference-models/gemma3-1b.gguf`, uses a 4096-token context, +keeps graph memory disabled, configures GPU layer admission, and uses +`nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader,nounits` as the +temperature source. The router loads exactly the model selected by +`king_inference_runtime_model_config()` and +`king_inference_runtime_model_load()`. CPU/GPU model names, artifacts, context, +memory mode, VRAM reserve, power limits, and thermal policy therefore come from +the effective King config snapshot instead of separate router-side inference +overrides. + +## Local Chat App + +The repository includes a browser chat under `demo/chat` for exercising the +OpenAI-compatible inference route with real streaming. The chat is intentionally +thin: it persists threads in SQLite, validates browser requests in PHP, proxies +to the King router, and streams the router's Server-Sent Events back to the +browser. It does not call Ollama, vLLM, or a hosted API at runtime. + +Required local inputs: + +- `extension/modules/king.so` built from the current tree. +- Local Docker image `king-local:php8.4-inference`. +- A materialized GGUF model at `var/inference-models/gemma3-1b.gguf`. +- Docker Compose and, for GPU execution, a working NVIDIA container runtime. + +Build the extension from the repository root whenever native code has changed: + +```bash +make build +``` + +Build the local runtime image when it is missing. The Dockerfile consumes the +release package artifacts from `dist/docker-packages`; create them first when +that directory is not present for PHP 8.4: + +```bash +make release-package +docker build --load \ + -f infra/php-runtime.Dockerfile \ + -t king-local:php8.4-inference \ + --build-arg PHP_VERSION=8.4 \ + . +``` + +Provide the model artifact. King needs a direct local GGUF file path; model +weights remain ignored by git. If you already have an approved GGUF artifact, +copy or symlink it to the path used by the local profile: + +```bash +mkdir -p var/inference-models +ln -sf /absolute/path/to/gemma3-1b.gguf var/inference-models/gemma3-1b.gguf +``` + +If the artifact source is a local Ollama installation, Ollama is only used to +download and store the model. The King router still reads the GGUF blob directly +and does not call Ollama's HTTP API: + +```bash +ollama pull gemma3:1b +ollama show --modelfile gemma3:1b +mkdir -p var/inference-models +ln -sf /path/to/ollama/models/blobs/sha256-... var/inference-models/gemma3-1b.gguf +``` + +Use the `FROM` digest shown by `ollama show --modelfile gemma3:1b` to choose the +matching blob. Common blob roots are `/usr/share/ollama/.ollama/models/blobs` +for the Linux service user and `$HOME/.ollama/models/blobs` for a user-local +Ollama installation. When the symlink points into the service path, make the same +directory visible to the container through `demo/chat/.env`: + +```bash +cd demo/chat +cp .env.example .env +# Adjust KING_MODEL_BLOBS when your Ollama blob directory is not the default. +``` + +Start the chat: + +```bash +cd demo/chat +docker compose up -d +``` + +Open: + +- Chat UI: `http://127.0.0.1:19480` +- OpenAI-compatible base URL: `http://127.0.0.1:8080/v1` +- Compose-scoped model list: `http://127.0.0.1:19481/v1/models` + +The chat backend stores threads in `demo/chat/var/chat.sqlite`, which is local +runtime state and ignored by git. Stop the stack with: + +```bash +cd demo/chat +docker compose down +``` + +If `/v1/models` works but chat generation is slow, inspect the router start log +first. It prints the effective model profile, artifact paths, context policy, +GPU backend, VRAM guardrails, thermal ceiling, and immediate-streaming flags +before binding the port. + +Native graph stream memory is opt-in. The compiled default is stateless and the +system baseline can be changed in `php.ini`: + +```ini +king.inference_with_memory=0 +``` + +The effective order is built-in default, `php.ini`, model config +`with_memory`, stream request, stream options, and finally `graph_options`. +Use `with_memory => true` only when a later graph should inherit the previous +graph result state. The alias `with-memory` is accepted for external payloads; +do not provide both spellings in the same array. + +```php + 'king-native-stateless', + 'artifact' => '/models/king-small-q4.gguf', + 'backend' => 'king_native_cpu', + 'with_memory' => false, +]); + +$stateless = king_inference_stream($model, [ + 'graphs' => $decodeSteps, +], [ + 'with_memory' => false, +]); + +$stateful = king_inference_stream($model, [ + 'graphs' => $decodeSteps, +], [ + 'with_memory' => true, +]); +``` + +## GraphAttention Memory Cache + +PagedAttention is not the first target here. The practical first step is +GraphAttention-style memory: native graph streams can carry state forward, +and a later evaluation pipeline can label prompts, inference results, and graph +paths as useful. A labelled "works well" path can then be promoted by +application code into a smaller candidate set for the next run instead of +forcing the model to see the full catalogue again. + +King now exposes the cache admission policy for that memory mode. The LLM cache +is never active for stateless inference. It is checked only when effective +`with_memory` is `true`. The compiled and php.ini defaults keep it disabled; +set `king.inference_llm_cache_enable=1` only for deployments that explicitly +want memory-enabled graph cache admission. + +```ini +king.inference_llm_cache_enable=1 +king.inference_llm_cache_path=/tmp/king-llm-cache +king.inference_llm_cache_min_free_mb=5120 +king.inference_llm_cache_fail_closed=1 +king.inference_llm_cache_disk_alert_webhook= +king.inference_llm_cache_disk_alert_mcp_service= +king.inference_llm_cache_disk_alert_mcp_method= +``` + +When active, King checks the cache path and the configured free-disk floor +before native graph streaming starts. With `fail_closed=1`, memory-enabled +inference is refused when the disk floor cannot be satisfied. With +`fail_closed=0`, a native King stream continues and emits a `llm_cache_status` +event before token events so the application can notify the configured webhook +or MCP target. OpenAI-compatible streams keep their response format pure; the +same cache policy is still checked, but the status should be queried through +`king_inference_llm_cache_status()` when an out-of-band preflight is needed. + +```php + true, + 'inference.llm_cache_path' => '/var/cache/king/llm', + 'inference.llm_cache_min_free_mb' => 8192, + 'inference.llm_cache_fail_closed' => true, + 'inference.llm_cache_disk_alert_webhook' => 'https://ops.example/cache-alert', + 'inference.llm_cache_disk_alert_mcp_service' => 'ops.cache', + 'inference.llm_cache_disk_alert_mcp_method' => 'diskFloorWarning', +]); + +$status = king_inference_llm_cache_status($config, ['with_memory' => true]); +``` + +Runtime-loaded models carry the same cache policy in their model config under +`llm_cache`, so `king_inference_runtime_model_load($config)` is enough for the +native stream to enforce the policy. Manual model configs may also provide a +`llm_cache` array with `enabled`, `path`, `min_free_mb`, `fail_closed`, +`disk_alert_webhook`, `disk_alert_mcp_service`, and +`disk_alert_mcp_method`. Request, stream options, and `graph_options` may +override that array for a single run. + +GPU execution is conservative. CPU-only execution is the default. GPU use must +be explicitly enabled in the model config, the global +`king.gpu_bindings_enable` setting must allow it, and either a thermal sensor +path or a thermal sensor command is required unless the operator explicitly +accepts unmonitored GPU execution. +The `gpu` config itself is strict: `gpu.enabled` must be a boolean, +`gpu.max_gpu_layers`, `gpu.vram_reserve_mb`, and `gpu.min_free_vram_mb` must be +non-negative integers, +`gpu.thermal` must be an array when provided, `gpu.thermal.sensor_path` and +`gpu.thermal.sensor_command` must be non-empty strings when provided, +`gpu.thermal.max_temperature_c` must be a positive finite number, and +`gpu.thermal.check_interval_seconds` must be a non-negative integer when +provided. `gpu.thermal.allow_unmonitored_gpu` must be a boolean. Optional +`gpu.power` guardrails use a command-based watt sensor. `gpu.power.max_watts=0` +keeps the power guardrail disabled; values greater than zero require +`gpu.power.sensor_command`, and `gpu.power.check_interval_seconds` must be a +non-negative integer. + +## Runtime Model Profile + +Applications that should use "the configured King model" do not have to build +the model config array themselves. King exposes a runtime model primitive with +one explicit profile switch: + +```ini +king.inference_preferred_model_profile=auto +king.inference_cpu_model_name=gemma3:1b +king.inference_cpu_model_artifact=/models/gemma3-1b.gguf +king.inference_gpu_model_name=gemma4:12b +king.inference_gpu_model_artifact=/models/gemma4-12b.gguf +king.inference_gpu_max_gpu_layers=99 +king.inference_gpu_vram_reserve_mb=2048 +king.inference_gpu_min_free_vram_mb=4096 +king.inference_gpu_thermal_sensor_path=/sys/class/hwmon/hwmon0/temp1_input +king.inference_gpu_thermal_sensor_command= +king.inference_gpu_thermal_max_temperature_c=78 +king.inference_gpu_thermal_check_interval_sec=15 +king.inference_gpu_allow_unmonitored=0 +king.inference_gpu_power_sensor_command="nvidia-smi --query-gpu=power.draw --format=csv,noheader,nounits" +king.inference_gpu_power_max_watts=450 +king.inference_gpu_power_check_interval_sec=15 +king.inference_llm_cache_enable=0 +king.inference_llm_cache_path=/tmp/king-llm-cache +king.inference_llm_cache_min_free_mb=5120 +king.inference_llm_cache_fail_closed=1 +``` + +`auto` selects the GPU profile only when process-level GPU bindings are enabled, +the config-level GPU bindings are enabled, and +`inference_gpu_model_artifact` points to a materialized local GGUF file. +Otherwise it selects the CPU profile. `gpu` requires the GPU profile and fails +fast when the GPU artifact, config-level GPU allowance, or process-level GPU +allowance is missing. `cpu` always selects the CPU profile. + +`gemma3:1b` is the compact baseline model for local King inference. It is small +enough for fast CPU/GPU preflight and short smoke checks, but it is not treated +as a disposable smoke-only artifact. The expected capability floor is simple +chat, exact-output following, language following, small PHP and King snippets, +and basic local Coder assistance. If one of those contracts regresses, the +problem is in runtime admission, prompt formatting, tokenizer handling, model +configuration, or the fine-tune dataset, not in a lowered expectation for the +model. + +The stronger GPU profile can prefer a larger local model such as `gemma4:12b` +after the `gemma3:1b` hot path is fast and honest. That keeps a cheap baseline +available for diagnostics while allowing editor and production-like workflows to +select a larger GPU model when configured. Future supervised tuning should keep +the 1B family useful for King/PHP/Coder tasks instead of replacing it with a +generic benchmark-only model. + +The same settings can be scoped to a `King\Config` snapshot: + +```php + 'auto', + 'inference.cpu_model_name' => 'gemma3:1b', + 'inference.cpu_model_artifact' => '/models/gemma3-1b.gguf', + 'inference.gpu_model_name' => 'gemma4:12b', + 'inference.gpu_model_artifact' => '/models/gemma4-12b.gguf', + 'inference.gpu_max_gpu_layers' => 48, + 'inference.gpu_vram_reserve_mb' => 2048, + 'inference.gpu_min_free_vram_mb' => 4096, + 'inference.gpu_thermal_sensor_path' => '/sys/class/hwmon/hwmon0/temp1_input', + 'inference.gpu_thermal_sensor_command' => '', + 'inference.gpu_thermal_max_temperature_c' => 78.0, + 'inference.gpu_allow_unmonitored' => false, + 'inference.llm_cache_path' => '/var/cache/king/llm', + 'inference.llm_cache_min_free_mb' => 5120, +]); + +$modelConfig = king_inference_runtime_model_config($config); +$model = king_inference_runtime_model_load($config); +``` + +The procedural and OO surfaces are equivalent: + +```php + false` and +`emit_logits => true` to make a single decode graph return the next-token +logits as its primary `final` output without running `sample_token` or +`argmax_token`. +`gguf_architecture_metadata.inc` captures model-shape metadata such as context +length, layer count, head count, KV head count, embedding length, and +key/value dimensions. It also classifies the loaded GGUF architecture against +King's native decoder target set. `gemma3` and `gemma4` are exposed as +supported decoder profiles; unsupported or missing architecture metadata remains +inspectable, but is not reported as decoder-ready. `paged_kv_cache.inc` turns +the shape metadata into a deterministic page plan for the native attention +cache. +`native_memory.inc` owns the read-only `mmap()` lifecycle used by native King +backends so tensor bytes can be addressed directly by later graph execution +without handing the model to an external runtime. + +`King\Inference\Model::info()` and `king_inference_model_info()` expose backend +metadata, including `backend`, `configured_backend`, `active_backend`, +`active_device`, `fallback_mode`, `silent_cpu_fallback`, +`gpu_admission_reason`, `with_memory`, `memory_mode`, `memory`, `readiness`, +`engine`, `artifact_bytes`, `gguf`, `runner_path`, `runner_protocol`, +`runner_executable`, `gpu_enabled`, and `backend_capabilities`. `/v1/models` +exposes the same runtime-surface fields under `x_king`. For `king_native_gpu`, +model info also exposes +`gpu_runtime.cuda_context`, `gpu_runtime.device_memory_allocator`, +`gpu_runtime.required_weight_upload`, `decoder_stream_contract_ready=true`, +`decoder_kernel_ready`, `plain_text_chat_ready`, and `generation_ready` +directly, plus `device_vector_ops.numeric_compare_hook` for local diagnostics. +That hook is explicitly marked `public_api=false`, so clients do not need to +infer decoder or generation state from model registration or backend name and +must not treat numeric comparison as a production inference feature. +The `gguf` entry contains `architecture`, `architecture_supported`, +`architecture_family`, `architecture_generation`, `decoder_profile`, +`decoder_shape_ready`, `decoder_ready`, `architecture_support_status`, +`architecture_missing_fields`, `supported_architectures`, `tokenizer_model`, +`tokenizer_token_count`, `tensor_data_offset`, `tensor_type_counts`, and parser +status fields when the source artifact provides them. Native backend info +additionally exposes `native_model_mapped`, `native_map_bytes`, +`native_tensor_index_count`, `native_tokenizer_token_count`, +`native_tokenizer_merge_count`, `tokenization_ready`, and +`paged_kv_cache_ready`. The model info payload also contains `paged_kv_cache` +and `resolved_tensors.token_embedding` plus +`resolved_tensors.output_projection` plus `resolved_tensors.attention` plus +`resolved_tensors.rms_norm` plus `resolved_tensors.ffn`, so callers can inspect +the selected embedding, logits projection, per-layer attention tensors, RMSNorm +weights, and FFN projection tensors before building decoder graphs. The output +projection entry includes +`tied_token_embedding=true` when the model uses the token embedding matrix for +logits projection. The attention entry exposes one layer record per GGUF block +with `query`, `key`, `value`, and `output` entries, including the resolved +tensor name, source, status, and expected matrix dimensions. The RMSNorm entry +exposes one layer record per GGUF block with `attention` and `feed_forward` +entries plus a `final` entry for the output norm. The FFN entry exposes one +layer record per GGUF block with `gate`, `up`, and `down` entries, including +the resolved tensor name, source, status, and expected matrix dimensions. +`backend_capabilities.gpu` and `backend_capabilities.gpu_backend` describe the +selected backend kind; configured GPU use remains visible through +`gpu_enabled`. `backend_capabilities.native_token_selection` refers to King +graph finishers such as `argmax_token` and `sample_token`, not to local runner +text generation. GPU-specific capability flags separate registration, +metadata, CUDA probing, CUDA context ownership, device-memory allocation, VRAM +admission, host-to-device weight upload, uploaded-weight caching, thermal +enforcement, bounded logits readback, and decoder generation so clients do not +infer generation readiness from model presence. `gpu_minimized_logits_readback` +means the GPU backend has a top-K candidate readback boundary; it does not mean +that final token sampling has moved to CUDA. + +Generation stream options are validated before the local runner process starts. +`max_tokens` must be a positive integer, numeric generation options must be +finite numbers, `temperature` must be non-negative, `top_p` must be greater than +zero and at most one, and `top_k` must be a non-negative integer. `stop` can be +one non-empty string or one to four non-empty strings; invalid stop sequences are +rejected before runner arguments are built. The runner also enforces the same +direct-CLI stop contract and buffers the trailing prefix of each possible stop +sequence so streamed output never includes the matched stop text. + +## Function, Tensor Index and Tensor View + +```php + 'invoice-assistant-small', + 'artifact' => '/models/invoice-assistant-q4.gguf', + 'backend' => 'king_native_cpu', +]); + +$index = king_inference_tensor_index($model, [ + 'prefix' => 'blk.0.', + 'limit' => 64, +]); + +foreach ($index['tensors'] as $name => $tensor) { + printf( + "%s %s elements=%d bytes=%d ready=%s\n", + $name, + $tensor['type_name'], + $tensor['elements'], + $tensor['byte_length'], + $tensor['native_view_ready'] ? 'yes' : 'no', + ); +} + +$query = king_inference_tensor_view($model, 'blk.0.attn_q.weight'); +if (!$query['bounds_safe']) { + throw new RuntimeException('Tensor byte range is outside the mapped model artifact.'); +} +``` + +Tensor views are the stable handoff between the GGUF parser and later native +execution. The view describes where a tensor lives in the memory-mapped model +file and how its bytes are packed. PHP receives the descriptor, not a raw +native address. Native kernels can use the same descriptor path internally when +the quantized block decoders and matrix operations are wired into King. + +## Function, Tensor Dequantization and CPU Matmul + +```php + 'invoice-assistant-small', + 'artifact' => '/models/invoice-assistant-q4.gguf', + 'backend' => 'king_native_cpu', +]); + +$sample = king_inference_tensor_dequantize($model, 'blk.0.attn_q.weight', [ + 'offset' => 0, + 'count' => 32, +]); + +print_r($sample['values']); + +$input = array_fill(0, 4096, 0.0); +$input[0] = 1.0; + +$projection = king_inference_tensor_matmul($model, 'blk.0.attn_q.weight', $input, [ + 'row_limit' => 64, + 'max_operations' => 262144, +]); + +printf( + "rows=%d cols=%d output=%d complete=%s\n", + $projection['rows'], + $projection['cols'], + $projection['output_count'], + $projection['complete'] ? 'yes' : 'no', +); +``` + +This is one compute step inside the native graph path. The CPU path currently +supports scalar F32, F16, BF16, I8, I16, I32, I64, F64 and the Q4_0, Q4_1, +Q5_0, Q8_0, Q4_K, Q5_K, and Q6_K block formats. Rank-2 matmul follows GGUF tensor +order: dimension 0 is the input width and dimension 1 is the output row count. +Quantized rows use blockwise dot decoding where supported. The operation +guards input size, output size, and total multiply-add count so a large model +tensor cannot accidentally consume the host without an explicit operator +decision. + +## Function, Mini Tensor Graph + +```php + 'invoice-assistant-small', + 'artifact' => '/models/invoice-assistant-q4.gguf', + 'backend' => 'king_native_cpu', +]); + +$result = king_inference_graph_run($model, [ + 'state' => [ + 'kv_cache' => [ + 'default/11/key' => [ + 'cache' => 'default', + 'slot' => 11, + 'kind' => 'key', + 'length' => 8, + 'values' => [0.12, -0.04, 0.25, 0.31, -0.19, 0.08, 0.44, -0.11], + ], + 'default/11/value' => [ + 'cache' => 'default', + 'slot' => 11, + 'kind' => 'value', + 'length' => 8, + 'values' => [0.03, 0.14, -0.09, 0.21, 0.18, -0.05, 0.07, 0.12], + ], + ], + ], + 'ops' => [ + [ + 'id' => 'x', + 'op' => 'embedding', + 'tensor' => 'token_embd.weight', + 'token_id' => 42, + ], + [ + 'id' => 'norm', + 'op' => 'rms_norm', + 'input' => 'x', + 'weight' => 'blk.0.attn_norm.weight', + 'epsilon' => 1e-6, + ], + [ + 'id' => 'query', + 'op' => 'linear', + 'input' => 'norm', + 'weight' => 'blk.0.attn_q.weight', + 'row_limit' => 8, + ], + [ + 'id' => 'key', + 'op' => 'linear', + 'input' => 'norm', + 'weight' => 'blk.0.attn_k.weight', + 'row_limit' => 8, + ], + [ + 'id' => 'value', + 'op' => 'linear', + 'input' => 'norm', + 'weight' => 'blk.0.attn_v.weight', + 'row_limit' => 8, + ], + [ + 'id' => 'query_rope', + 'op' => 'rope', + 'input' => 'query', + 'position' => 12, + 'head_dim' => 8, + 'inv_freqs' => [1.0, 0.1, 0.01, 0.001], + ], + [ + 'id' => 'key_rope', + 'op' => 'rope', + 'input' => 'key', + 'position' => 12, + 'head_dim' => 8, + 'inv_freqs' => [1.0, 0.1, 0.01, 0.001], + ], + [ + 'id' => 'cache_write', + 'op' => 'kv_write', + 'slot' => 12, + 'key' => 'key_rope', + 'value' => 'value', + ], + [ + 'id' => 'attention_context', + 'op' => 'kv_attention', + 'query' => 'query_rope', + 'slot_start' => 11, + 'slot_count' => 2, + 'scale' => 0.353553, + ], + [ + 'id' => 'logits', + 'op' => 'linear', + 'input' => 'attention_context', + 'weight' => 'output.weight', + 'row_limit' => 32000, + ], + [ + 'id' => 'next_token', + 'op' => 'sample_token', + 'logits' => 'logits', + 'temperature' => 0.7, + 'top_k' => 40, + 'top_p' => 0.95, + 'seed' => 123456, + 'sample_index' => 12, + ], + ], + 'output' => 'next_token', +], [ + 'max_vector_values' => 65536, + 'max_operations' => 524288, +]); + +print_r($result['final']); +$nextState = $result['state']; +``` + +The graph runner is a small native execution surface for layer-sized work. It +does not decide a model topology by itself; the local runner builds the +Gemma3 token-step graph on top of these operations. The graph runner executes +named operations in order, stores each vector by id, and feeds those vectors +into later steps. +`embedding` gathers one row from a rank-2 tensor. Its `tensor` field may be +omitted when the shared token embedding resolver can identify exactly one +supported embedding matrix from model config, known GGUF names, or guarded +shape scan. `rms_norm` applies native RMSNorm with an optional weight tensor, +and `linear` reuses the blockwise CPU matmul path. `rope` applies rotary +position embedding to an even head slice using caller-supplied inverse +frequencies or a previously produced frequency vector. `slice` isolates +head-sized spans for per-head normalization, and +`silu` plus `mul` cover the gated feed-forward path used by local decoder +layers. `dot`, `stack`, `softmax`, and `weighted_sum` cover the first useful +attention path: scores become probabilities and probabilities produce a context +vector from value vectors. `scale` and `add` cover score scaling and +residual-style vector composition. `kv_read` and `kv_write` make the KV cache a +serializable graph state: callers pass `state` into `graphRun()` and pass the +returned `state` into the next token step. `kv_attention` is the compact path for +token decoding: it reads a strict slot range from `state.kv_cache`, computes +scaled QK softmax, and returns the weighted context vector from the cached value +vectors. `argmax_token` and `sample_token` are the native token-selection +finishers for logits. `king_inference_token_decode_graph()` emits +`argmax_token` directly when options contain `sampler => 'argmax'` or +`temperature => 0`; the returned graph and `terminal` metadata expose +`token_selection=argmax`, `token_selection_op=argmax_token`, and +`token_selection_temperature=0` for that path; `token_selection_top_k=1` +describes the effective single-token selection. +Argmax selection is deterministic: the highest logit wins, and equal logits +keep the lowest token index. +For positive finite temperatures, the graph emits `sample_token`, forwards the +temperature value unchanged into the sampling op, and exposes the same effective +temperature through `token_selection_temperature`. The process-runner backend +passes temperature to `bin/king-local-infer` with double round-trip precision +instead of rounding it for display. +`sample_token` supports temperature, top-k, top-p, optional +seeded deterministic sampling, `sample_index` as a per-step seed salt, and +`token_offset` for sharded vocab projections. `seed` must be an integer when +provided; the decode graph builder forwards it unchanged into `sample_token`, +and the local runner rejects non-integer CLI seed values before graph +construction. For identical logits, temperature, top-k, top-p, seed, and +sample-index inputs, the selected rank is reproducible. Direct `sample_token` +ops without a seed keep the sorted top candidate; the decode graph builder and +local runner default to seed `0` so generated-token loops stay reproducible. +Graph numeric options such as +sampling temperature, top-p, vector scales, softmax scale, KV-attention scale, +RMS epsilon, and RoPE position scale must be finite numbers. `top_k` must be a +non-negative integer. `top_k=0` means no top-k cap; positive values cap the +candidate set after logits are converted to probabilities and sorted by +probability with token id as the deterministic tie-breaker. The graph and +`terminal` metadata expose the effective `token_selection_top_k` value. `top_p` +must be greater than zero and at most one. `top_p=1` keeps the top-k candidate +set intact; smaller values narrow the sorted candidate list until cumulative +probability reaches or exceeds the threshold. The decode graph builder forwards +the finite `top_p` value unchanged into `sample_token`, and the local runner +rejects invalid CLI values before building a graph. Both +token-selection ops return `[token_id, probability, logit, rank]`. This is still +CPU-side vector state, but it matches the page-table contract that later native +paged attention needs. + +Set graph option `return_outputs => false` for decoder loops that only need +`final` and `state`; the default stays `true` for interactive inspection. +Use `sampler => 'sample'` to force the sampling finalizer when temperature is +positive. Non-finite or negative temperature values are rejected while the graph +is built, and the local runner applies the same validation to CLI input before +it builds a decode graph. Any other sampler name is rejected while the graph is +built. + +## Function, Paged KV-Cache Plan + +```php + 'invoice-assistant-small', + 'artifact' => '/models/invoice-assistant-q4.gguf', + 'backend' => 'king_native_cpu', + 'paged_attention' => [ + 'page_tokens' => 16, + 'element_bytes' => 2, + ], +]); + +$plan = king_inference_kv_cache_plan($model, [ + 'max_context_tokens' => 8192, +]); + +if (!$plan['ready']) { + throw new RuntimeException('Incomplete model metadata: ' . implode(', ', $plan['missing_fields'])); +} + +printf( + "pages=%d pageBytes=%d maxSequenceBytes=%d\n", + $plan['pages_per_sequence'], + $plan['page_bytes_all_layers'], + $plan['max_sequence_bytes'], +); +``` + +The plan is exposed before native token generation because it is the memory +contract the later graph executor needs. Sequence state can reference fixed +KV pages by block table instead of reallocating one large contiguous cache per +request. Copy-on-write is intentionally reported as not ready until shared +prefix handling exists. + +## Function, Native Tokenization + +```php + 'invoice-assistant-small', + 'artifact' => '/models/invoice-assistant-q4.gguf', + 'backend' => 'king_native_cpu', +]); + +$encoded = king_inference_tokenize($model, 'Reject invoice if VAT total is missing.'); + +printf( + "tokens=%d unknown=%d normalization=%s\n", + $encoded['token_count'], + $encoded['unknown_count'], + $encoded['normalization'], +); + +print_r($encoded['tokens']); +``` + +The tokenizer API uses the token table embedded in the GGUF artifact. For +SentencePiece-style models King applies the expected space marker +normalization, then performs greedy longest-prefix matching against the loaded +token lookup. If the artifact exposes byte fallback tokens, those are used +before falling back to the model's unknown token id. + +## Function, Example 1: Compact Streaming + +```php + 'invoice-assistant-small', + 'artifact' => '/models/invoice-assistant-q4.gguf', + 'quantization' => 'q4', + 'backend' => [ + 'name' => 'local', + 'runner_path' => $runner, + ], +]); + +$stream = king_inference_stream($model, [ + 'prompt' => 'Explain why this invoice was rejected.', + 'max_tokens' => 256, + 'temperature' => 0.2, +]); + +while (($event = king_inference_next($stream, 1000)) !== null) { + if ($event['type'] === 'token') { + echo $event['text']; + } + if ($event['type'] === 'done') { + break; + } +} +``` + +## Function, Example 1a: OpenAI-Compatible Chat Streaming + +```php + 'king-local-invoice', + 'artifact' => '/models/invoice-assistant-q4.gguf', + 'quantization' => 'q4', + 'backend' => [ + 'name' => 'local', + 'runner_path' => $runner, + ], +]); + +$stream = king_inference_stream($model, [ + 'model' => 'king-local-invoice', + 'messages' => [ + ['role' => 'system', 'content' => 'You explain invoice validation decisions.'], + ['role' => 'user', 'content' => 'Why was invoice HU-2026-0007 rejected?'], + ], + 'stream' => true, + 'max_tokens' => 256, + 'temperature' => 0.2, +], [ + 'format' => 'openai_chat_completions', +]); + +while (($chunk = king_inference_next($stream, 1000)) !== null) { + // $chunk is shaped like a Chat Completions streaming chunk: + // id, object=chat.completion.chunk, created, model, choices[0].delta. + print json_encode($chunk, JSON_UNESCAPED_SLASHES) . "\n"; + + if (($chunk['choices'][0]['finish_reason'] ?? null) === 'stop') { + break; + } +} +``` + +The compatibility mode is explicit. Set `openai_compatible => true` or +`format => openai_chat_completions` in the request/options and King returns +Chat-Completions-style streaming chunks from `king_inference_next()`. The same +stream object still supports King-native events when the mode is not enabled. +`openai_compatible` must be a boolean when provided, and `format` must be one +of `openai`, `openai_chat`, or `openai_chat_completions`. +For native decoder streams, generated token text is emitted as +`chat.completion.chunk` content deltas with the same id, created timestamp, and +model name. The first read emits the assistant role chunk and the terminal read +emits `finish_reason=stop`; structured King-native status events are not leaked +raw into OpenAI-compatible streams. +For `king_native_cpu`, the request must provide a native `graph` or `graphs` +sequence whose final output is a token vector produced by `argmax_token` or +`sample_token`. Direct stream requests use the same native graph shape contract +as the HTTP router: `graph` is an object array, `graphs` is a list array, and +`graph_options` is an object array. King decodes those token ids through the +model tokenizer and emits the same stream surface without creating a second +inference runtime. + +## Function, Example 1b: Native Graph Streaming + +```php + 'king-native-invoice', + 'artifact' => '/models/invoice-assistant-q4.gguf', + 'backend' => 'king_native_cpu', + 'with_memory' => false, +]); + +$encoded = king_inference_tokenize($model, 'Explain invoice rejection HU-2026-0007.'); + +$graphs = []; +for ($position = 0; $position < $encoded['token_count']; $position++) { + $graphs[] = king_inference_token_decode_graph($model, $encoded, $position, [ + 'temperature' => 0.4, + 'top_k' => 40, + 'top_p' => 0.95, + 'seed' => 90210, + ]); +} + +$stream = king_inference_stream($model, [ + 'graphs' => $graphs, +], [ + 'max_native_stream_tokens' => 64, + 'with_memory' => true, + 'graph_options' => [ + 'max_vector_values' => 65536, + 'max_operations' => 524288, + ], +]); + +while (($event = king_inference_next($stream, 0)) !== null) { + if (($event['type'] ?? '') === 'token') { + echo $event['text']; + } + if (($event['type'] ?? '') === 'done') { + break; + } +} +``` + +The native stream path is intentionally graph-driven. A request can provide a +`graphs` sequence for explicit decode steps. When a single `graph` is a +`king_native_cpu_token_decode` graph, King runs it once, decodes the selected +token, builds the next token-decode graph from that token id at the next +position, carries the original token-selection settings, and continues until +`max_tokens` is reached. Other single graphs keep the bounded repeat behavior +for custom graph finishers. Streams are stateless by default: King does not +carry graph result state into the next graph unless +`with_memory => true` is set in the stream options, request, or `graph_options`. +When memory is enabled and a graph omits `state`, King carries the previous +graph result state into the next graph, so KV cache entries written by +`kv_write` can be read by later steps through `kv_read` or `kv_attention`. This +is the current native handoff for generated token events; higher-level +prompt-to-graph compilation is a later layer and does not need a second +inference runtime. + +Native graph stream startup is bounded by `max_native_stream_tokens`, which can +be set as a stream option or graph option. If it is not set, King allows up to +4096 native token steps before rejecting the stream request. +`with_memory` and the alias `with-memory` must be booleans when provided. + +## CI and Test Model Strategy + +King keeps the default CI path independent from downloaded model artifacts. +Contract tests can validate exported functions, INI defaults, strict config +validation, OpenAI-compatible routing, and error contracts without a GGUF file. +Native model integration tests remain opt-in through +`KING_INFERENCE_TEST_MODEL_PATH`, because CI should not silently fetch hundreds +of megabytes or depend on an external model host. +GPU runtime readiness and model-metadata checks are opt-in through +`KING_INFERENCE_GPU_TEST_MODEL_PATH` and fall back to +`KING_INFERENCE_TEST_MODEL_PATH` only when the GPU-specific variable is not +set. Those checks require a local GGUF artifact, visible CUDA driver state, and +free-VRAM status; they do not downgrade a GPU profile into a CPU-only check. +The metadata checks load the configured GPU model, inspect +`king_inference_model_info()`, and verify that `/v1/models` exposes matching +`x_king.gpu_runtime` and `x_king.client_capabilities` state. + +The split is deliberate: + +- Model-free CI tests prove the extension API, INI surface, and validation. +- Optional GGUF tests prove loader, tokenizer, tensor, graph, and stream + behavior against a real artifact. +- Optional GPU GGUF tests prove local GPU configuration, model registration, + metadata exposure, and runtime status against a real artifact and a visible + GPU runtime. +- Release or nightly CI can mount a cached GGUF artifact and set + `KING_INFERENCE_TEST_MODEL_PATH` and `KING_INFERENCE_GPU_TEST_MODEL_PATH`. + +For a small King command model, start with a compact instruct model and +fine-tune it on deterministic King-specific command traces instead of training +from scratch. The initial dataset should be JSONL chat records with: + +- system prompt describing the King runtime contract +- user request in natural language +- assistant response containing one validated King command or structured plan +- tool/result records for negative cases and error handling +- metadata tags for primitive, sync/async, stateless/stateful, and security + +Example record: + +```json +{"messages":[{"role":"system","content":"You emit precise King PHP runtime calls. Prefer stateless inference unless memory is explicitly requested."},{"role":"user","content":"Open a native inference stream without memory for three decode graphs."},{"role":"assistant","content":"$stream = king_inference_stream($model, ['graphs' => $graphs], ['with_memory' => false]);"}],"metadata":{"primitive":"inference","mode":"stateless","language":"php"}} +``` + +Build the first dataset from King docs, public stubs, PHPT tests, and manually +reviewed examples. Do not train on failing or obsolete snippets unless the +assistant answer explicitly fixes them. The first useful target is command +selection and argument correctness, not general chat quality. + +## Function, Example 1c: OpenAI-Compatible HTTP Route + +```php + 'local-small-model', + 'artifact_path' => $modelPath, + 'backend' => [ + 'name' => 'local', + 'runner_path' => $runner, + ], +]); + +while (true) { + king_http1_server_listen_once( + '127.0.0.1', + 8080, + null, + static function (array $request) use ($model): array { + return king_inference_openai_chat_http_response($model, $request, [ + 'read_timeout_ms' => 250, + 'max_events' => 4096, + 'max_idle_events' => 240, + ]); + } + ); +} +``` + +The helper accepts the normalized King HTTP request array and owns the +OpenAI-compatible endpoint contract for `POST /v1/chat/completions`. The request +body is decoded as a Chat Completions JSON payload, `messages` are validated, +and the loaded King model is used for both normal and streaming responses. For +`king_native_cpu` models, the same route accepts normal plain-text `messages` +as well as an explicit `graph` object or `graphs` array in the JSON payload and +emits OpenAI-shaped responses from the native CPU decoder. Plain text chat uses +transient KV state for the prompt and generated tokens but does not enable the +optional persistent graph-memory/cache policy. `graph_options` must be a JSON +object when provided. Explicit native graph streams are stateless unless the +payload or options set `with_memory` or `with-memory` to `true`. + +For `stream=false`, the helper drains native decoder content deltas into one +OpenAI-shaped `chat.completion` JSON response with `choices[0].message.content`. +Decoder text is treated as assistant content. Tool-call request fields are +accepted for editor-client compatibility and ignored by plain inference. Until +a real King MCP execution path is bound to the route, King does not dispatch +those tools, does not synthesize tool-call responses from decoder text, and does +not fail the request; the local router logs the tool metadata and continues +normal inference. +For `stream=true`, it returns a bounded `text/event-stream` body with +`data: {chunk}` events and a final `data: [DONE]` marker. +For `king_native_cpu`, that streaming response is backed by native decoder +events rather than the external process runner. +If the selected model uses `king_native_gpu`, `POST /v1/chat/completions` +accepts plain-text `messages` when the native GPU prompt loop is ready. The +route renders those messages into `native_prompt_text`, runs bounded GPU token +generation, drains the native OpenAI deltas into one response for +`stream=false`, or writes the same deltas as SSE chunks for `stream=true`. GPU +graph payloads remain on the native stream contract instead of being mixed into +the OpenAI chat route. +Clients should treat the `/v1/models` `x_king.gpu_runtime` object as the +authoritative runtime readiness source for GPU models. A registered +`king_native_gpu` model can be listed and selected for inspection, but UI and +autodetect flows must not infer current generation readiness from the model id +or backend name. For direct UI wiring, `x_king.client_capabilities` mirrors the +effective runtime contract with plain booleans for +`openai_chat_completions`, `openai_chat_completions_stream`, +`openai_responses`, `openai_completions`, `openai_embeddings`, +`native_graph_streaming`, `requires_gpu`, `gpu_runtime_required`, and +`gpu_generation_ready`. Tool-call related flags stay false until King has a +real tool execution path behind the OpenAI-compatible route. + +## Function, Example 1d: OpenAI-Compatible Model Router + +```php + king_inference_model_load([ + 'name' => 'support-small', + 'artifact_path' => $supportModelPath, + 'backend' => ['name' => 'local', 'runner_path' => $runner], + 'owned_by' => 'internal-platform', + ]), + 'invoice-checker' => king_inference_model_load([ + 'name' => 'invoice-checker', + 'artifact_path' => $invoiceModelPath, + 'backend' => ['name' => 'local', 'runner_path' => $runner], + 'owned_by' => 'internal-platform', + ]), +]; + +while (true) { + king_http1_server_listen_once('127.0.0.1', 8080, null, static fn (array $request): array => + king_inference_openai_http_response($models, $request, [ + 'read_timeout_ms' => 250, + 'max_events' => 4096, + 'max_idle_events' => 240, + ]) + ); +} +``` +`king_inference_openai_http_response()` is the higher-level router for +`GET /v1/models`, `GET /v1/models/{model}`, `POST /v1/chat/completions`, and +`POST /v1/responses`, legacy `POST /v1/completions`, and `POST /v1/embeddings`; generation requests +resolve the JSON `model` field against the `$models` key first and then against +the loaded model name. If exactly one model is registered, `model` may be omitted. + +The Responses route accepts string or message-list `input`, top-level +`instructions`, and maps into the same King model stream. Non-streaming calls +return a `response` object with `output` and `output_text`; `stream=true` +returns semantic SSE events such as `response.created`, +`response.output_text.delta`, and `response.completed`. +The legacy completions route accepts string prompts; embeddings use the native tokenizer plus the configured token embedding tensor. + +## Function, Example 1e: Configured Model Path + +```php + 'local-small-model', + 'artifact' => [ + 'path' => $modelPath, + ], + 'backend' => 'local', +]); + +print_r(king_inference_model_info($model)); +``` + +## Function, Example 2: GPU With Thermal Guard + +```php + 'local-support-model', + 'artifact' => [ + 'path' => '/models/support-q5.gguf', + ], + 'quantization' => 'q5', + 'context_tokens' => 8192, + 'backend' => [ + 'type' => 'local', + 'runner' => [ + 'path' => '/opt/king/bin/king-local-infer', + ], + ], + 'gpu' => [ + 'enabled' => true, + 'max_gpu_layers' => 24, + 'vram_reserve_mb' => 2048, + 'min_free_vram_mb' => 4096, + 'thermal' => [ + 'sensor_path' => '/sys/class/hwmon/hwmon2/temp1_input', + 'max_temperature_c' => 78.0, + 'check_interval_seconds' => 15, + ], + ], +]); + +$stream = king_inference_stream($model, [ + 'messages' => [ + ['role' => 'system', 'content' => 'Answer precisely and do not invent facts.'], + ['role' => 'user', 'content' => 'Summarize this NAV error.'], + ], + 'max_tokens' => 512, + 'temperature' => 0.1, + 'seed' => 42, + 'top_k' => 40, + 'top_p' => 0.92, +]); + +while (($event = king_inference_next($stream, 250)) !== null) { + if ($event['type'] === 'stderr') { + error_log($event['text']); + continue; + } + if ($event['type'] === 'token') { + echo $event['text']; + } + if ($event['type'] === 'done' || $event['type'] === 'cancelled') { + break; + } +} +``` + +## Function, Example 3: Parallel Stream Reads with king_awaitable_any + +```php + 'support-routing', + 'artifact' => '/models/support-routing-q4.gguf', + 'quantization' => 'q4', + 'backend' => [ + 'name' => 'local', + 'runner_path' => $runner, + ], +]); +$invoiceModel = king_inference_model_load([ + 'name' => 'invoice-format-check', + 'artifact' => '/models/invoice-format-check-q4.gguf', + 'quantization' => 'q4', + 'backend' => [ + 'name' => 'local', + 'runner_path' => $runner, + ], +]); + +$streams = [ + 'support' => king_inference_stream($supportModel, [ + 'messages' => [ + ['role' => 'system', 'content' => 'Classify support requests by department.'], + ['role' => 'user', 'content' => 'Customer cannot download an invoice PDF.'], + ], + 'max_tokens' => 128, + 'temperature' => 0.1, + ]), + 'invoice' => king_inference_stream($invoiceModel, [ + 'messages' => [ + ['role' => 'system', 'content' => 'Return concise validation observations.'], + ['role' => 'user', 'content' => 'Check whether this invoice has all required buyer tax fields.'], + ], + 'max_tokens' => 256, + 'temperature' => 0.0, + ]), +]; + +$reads = []; +foreach ($streams as $name => $stream) { + $reads[$name] = king_inference_next_async($stream, 0); +} + +while ($reads !== []) { + $readyAwaitable = king_awaitable_any($reads); + + if (!king_awaitable_poll($readyAwaitable, 25)) { + usleep(5_000); + continue; + } + + $ready = king_await($readyAwaitable); + $name = $ready['key']; + $event = $ready['value']; + + if ($ready['status'] !== 'resolved') { + error_log($name . ' failed: ' . ($ready['error'] ?? $ready['status'])); + unset($reads[$name]); + continue; + } + + if ($event === null || ($event['type'] ?? '') === 'done' || ($event['type'] ?? '') === 'cancelled') { + unset($reads[$name]); + continue; + } + + if (($event['type'] ?? '') === 'token') { + echo '[' . $name . '] ' . $event['text']; + } + + $reads[$name] = king_inference_next_async($streams[$name], 0); +} +``` + +## OO, Example 1: Static Facade + +```php + 'assistant', + 'artifact_path' => '/models/assistant-q4.gguf', + 'quantization' => 'q4', + 'backend' => [ + 'name' => 'local', + 'runner_path' => $runner, + ], +]); +$info = Inference::modelInfo($model); + +$stream = Inference::stream($model, [ + 'prompt' => 'Write a short customer support answer.', + 'max_tokens' => 128, +]); + +while (($event = Inference::next($stream, 500)) !== null) { + if ($event['type'] === 'token') { + echo $event['text']; + } +} +``` + +The static facade mirrors the procedural surface: `Inference::nextAsync($stream)` +returns a `King\Awaitable`, and `Inference::cancel($stream)` closes the stream. +For native decoder streams, cancellation also drops any pending start event and +advances the native event cursor to the end of the prepared token buffer, so no +buffered decoder tokens are emitted after cancellation. + +## OO, Example 2: Explicit Model And Cancellation + +```php + 'procurement-assistant', + 'artifact' => '/models/procurement-q4.gguf', + 'quantization' => 'q4', + 'backend' => [ + 'type' => 'local', + 'runner' => [ + 'path' => $runner, + ], + ], +]); + +$stream = new Stream($model, [ + 'messages' => [ + ['role' => 'user', 'content' => 'Compare these supplier offers.'], + ], + 'max_tokens' => 512, +]); + +while (!$stream->isDone()) { + $event = $stream->next(1000); + if ($event === null) { + continue; + } + if (($event['type'] ?? '') === 'token') { + echo $event['text']; + } +} + +$metrics = $stream->getMetrics(); +``` + +`Stream::getMetrics()` reports emitted token chunks, stderr chunks, bytes, +terminal state, cancellation state, exit code, OpenAI-compatible mode, and for +native graph streams also `native_stream`, `native_event_count`, +`native_event_index`, `native_decoder_tokens`, +`native_decoder_last_token_id`, `native_decoder_last_probability`, +`native_decoder_last_logit`, and `native_decoder_last_rank`. After native +stream cancellation, `native_event_index` points at the end of the prepared +native event buffer. `native_decoder_tokens` counts native token selections +prepared by the decoder; `chunks` remains the count of emitted stream reads. +GPU-enabled streams also report the last run preflight through +`gpu_thermal_preflight_checked`, `gpu_thermal_preflight_at`, and the optional +`gpu_thermal_preflight_temperature_c`. If a running GPU stream is +aborted at the configured thermal ceiling, metrics include +`gpu_thermal_aborted`, `gpu_thermal_abort_at`, +`gpu_thermal_abort_temperature_c`, and `gpu_thermal_abort_ceiling_c`. + +## OO, Example 3: Parallel Inference Streams + +```php + 'operations-assistant', + 'artifact_path' => '/models/operations-assistant-q4.gguf', + 'quantization' => 'q4', + 'backend' => [ + 'name' => 'local', + 'runner_path' => $runner, + ], +]); + +$streams = [ + 'purchase-order' => new Stream($model, [ + 'prompt' => 'Summarize open procurement risks for PO-1009.', + 'max_tokens' => 192, + ]), + 'invoice' => new Stream($model, [ + 'prompt' => 'Explain the invoice rounding difference for INV-1009.', + 'max_tokens' => 192, + ]), +]; + +$reads = []; +foreach ($streams as $name => $stream) { + $reads[$name] = $stream->nextAsync(0); +} + +while ($reads !== []) { + $first = Awaitable::any($reads); + + if (!$first->poll(25)) { + usleep(5_000); + continue; + } + + $ready = $first->await(); + $name = $ready['key']; + $event = $ready['value']; + + if ($ready['status'] !== 'resolved' || $event === null) { + unset($reads[$name]); + continue; + } + + if (($event['type'] ?? '') === 'token') { + echo '[' . $name . '] ' . $event['text']; + $reads[$name] = $streams[$name]->nextAsync(0); + continue; + } + + if (($event['type'] ?? '') === 'stderr') { + error_log('inference stderr: ' . $event['text']); + $reads[$name] = $streams[$name]->nextAsync(0); + continue; + } + + unset($reads[$name]); +} +``` diff --git a/docs/mcp.md b/docs/mcp.md new file mode 100644 index 000000000..65c373d8d --- /dev/null +++ b/docs/mcp.md @@ -0,0 +1,210 @@ +# MCP + +King has two MCP surfaces: + +- `King\MCPServer` and `king_mcp_server_*` implement public MCP server + handling over JSON-RPC, stdio, and Streamable HTTP. +- `King\MCP` and `king_mcp_request*` are King-internal peer calls for trusted + runtime-to-runtime work, including configured IIBIN payload contracts. + +The public server surface follows the MCP wire shape: JSON-RPC 2.0 messages, +newline-delimited stdio messages, and a single Streamable HTTP endpoint that +accepts POST requests and can return either `application/json` or +`text/event-stream`. + +## Internal Layout + +The native MCP runtime and object contracts live in +`extension/include/mcp/mcp.h`. Runtime code lives under +`extension/src/mcp/runtime/`; PHP userland binding code lives under +`extension/src/mcp/php_binding/`. PHP arginfo, function-table entries, and OO +method-table entries live under `extension/include/mcp/`. The binding source +directory owns object API, procedural API, public server API, and transfer +helpers. + +## Public Server Definition + +```php + [ + 'name' => 'invoice-support', + 'version' => '1.0.0', + ], + 'instructions' => 'Expose tenant-scoped invoice support tools only.', + 'streamable_http' => [ + 'prefer_sse' => true, + 'allowed_origins' => ['https://support.example.com'], + ], + 'tools' => [ + 'invoice.status' => [ + 'description' => 'Return the visible processing status for one invoice.', + 'inputSchema' => [ + 'type' => 'object', + 'required' => ['tenant_id', 'invoice_id'], + 'properties' => [ + 'tenant_id' => ['type' => 'string'], + 'invoice_id' => ['type' => 'string'], + ], + ], + 'handler' => static function (array $arguments, array $context): array { + authorize_support_lookup($context['user'] ?? null, $arguments['tenant_id']); + + return [ + 'content' => [[ + 'type' => 'text', + 'text' => lookup_invoice_status_text( + $arguments['tenant_id'], + $arguments['invoice_id'] + ), + ]], + ]; + }, + ], + ], +]); +``` + +`MCPServer` and `king_mcp_server_create()` also accept creation options for +transport defaults. These options are applied once to the normalized server +definition and are not read again at request time: + +```php + [ + 'prefer_sse' => true, + 'allowed_origins' => ['https://support.example.com'], + ], +]); +``` + +For convenience, `prefer_sse` and `allowed_origins` may also be passed as +top-level creation options. Unknown option keys are rejected so configuration +typos do not silently change the transport contract. + +The tool handler receives the MCP `arguments` and a caller-provided context. +Return a complete MCP tool result when you need exact control over `content`, +`structuredContent`, or `isError`. Returning a scalar or arbitrary array is +also accepted; King wraps it into a text result and, for structured values, +adds `structuredContent`. + +## stdio Server + +```php +runStdio([ + 'max_line_bytes' => 1024 * 1024, +]); +``` + +The stdio transport reads one UTF-8 JSON-RPC message per line from stdin and +writes only JSON-RPC messages to stdout. Notifications such as +`notifications/initialized` do not produce a response. + +## Streamable HTTP Server + +```php +handleHttp($request, [ + 'user' => authenticate_mcp_request($request['headers'] ?? []), + ]); + }); +} +``` + +The HTTP adapter expects King's normal request array with `method`, `headers`, +and `body`, and returns King's normal response array with `status`, `headers`, +and `body`. POST requests require an `Accept` header that includes both +`application/json` and `text/event-stream`. GET opens a short SSE stream +response; long-lived streaming belongs in the surrounding HTTP server loop and +session policy. Browser `Origin` headers are rejected unless the exact origin +is listed in `streamable_http.allowed_origins`. + +## Direct JSON-RPC Dispatch + +```php +handleJsonRpc(json_encode([ + 'jsonrpc' => '2.0', + 'id' => 1, + 'method' => 'tools/list', +], JSON_THROW_ON_ERROR)); + +echo $response . PHP_EOL; +``` + +Procedural code can use the same dispatcher: + +```php + '2.0', + 'id' => 'call-1', + 'method' => 'tools/call', + 'params' => [ + 'name' => 'invoice.status', + 'arguments' => [ + 'tenant_id' => 'tenant-42', + 'invoice_id' => 'INV-1001', + ], + ], +], ['user' => $supportUser]); +``` + +## Internal King Peer With IIBIN + +Use `King\MCP` for trusted King runtime peers that speak King's internal +line-framed transport. This is separate from the public MCP server surface. + +```php + ['tag' => 1, 'type' => 'string', 'required' => true], + 'case_id' => ['tag' => 2, 'type' => 'string', 'required' => true], + 'question' => ['tag' => 3, 'type' => 'string', 'required' => true], +]); + +king_proto_define_schema('SupportToolResponse', [ + 'answer' => ['tag' => 1, 'type' => 'string', 'required' => true], + 'source_count' => ['tag' => 2, 'type' => 'uint32'], +]); + +$mcp = new MCP('127.0.0.1', 9091, new Config([ + 'mcp.iibin_routes' => [ + 'support.faq/answer' => [ + 'request_schema' => 'SupportToolRequest', + 'response_schema' => 'SupportToolResponse', + 'decode_as_object' => false, + ], + ], +]), [ + 'default_timeout_ms' => 5000, +]); + +$answer = $mcp->requestIibin('support.faq', 'answer', [ + 'tenant_id' => 'tenant-42', + 'case_id' => 'CASE-1001', + 'question' => 'Which invoice status can the customer see?', +]); +``` + +The IIBIN route contract is copied into the MCP connection state when the +connection is created. Later changes to the `King\Config` object do not change +the schemas used by that connection. + +Connection options are also copied into the MCP state. `default_timeout_ms` +sets the timeout budget used by later request, IIBIN, upload, and download +operations when the individual call does not pass `timeout_ms`. The procedural +`king_mcp_connect()` API accepts the same connection option; `timeout_ms` is +also accepted as a connection-time alias for `default_timeout_ms`. diff --git a/docs/object-store.md b/docs/object-store.md new file mode 100644 index 000000000..c3aeeac29 --- /dev/null +++ b/docs/object-store.md @@ -0,0 +1,106 @@ +# Object Store + +The object store persists payloads, metadata, streams, backups, and resumable +uploads. The procedural API is `king_object_store_*`; the OO API is +`King\ObjectStore`. + +## Internal Layout + +The runtime contracts live under `extension/include/object_store/`; the runtime +implementation lives under `extension/src/object_store/`. The PHP arginfo, +`King\ObjectStore` method table, and function-table entries live under +`extension/include/object_store/` and are consumed by the extension bootstrap +through `extension/include/php_king/`. + +## Function, Example 1: Store and Read + +```php + 'local_fs', + 'storage_root_path' => __DIR__ . '/var/object-store', + 'max_storage_size_bytes' => 1024 * 1024 * 1024, +]); + +king_object_store_put( + 'tenant-42/invoices/INV-1001.xml', + file_get_contents(__DIR__ . '/invoice.xml'), + [ + 'content_type' => 'application/xml', + 'object_type' => 'einvoice', + 'cache_ttl_sec' => 3600, + ] +); + +$metadata = king_object_store_get_metadata('tenant-42/invoices/INV-1001.xml'); +$payload = king_object_store_get('tenant-42/invoices/INV-1001.xml'); + +var_dump($metadata, strlen($payload)); +``` + +## Function, Example 2: Stream, Backup, and Restore + +```php + 'application/xml'] +); +fclose($source); + +king_object_store_backup_object( + 'tenant-42/invoices/large.xml', + __DIR__ . '/var/backups' +); + +king_object_store_delete('tenant-42/invoices/large.xml'); + +king_object_store_restore_object( + 'tenant-42/invoices/large.xml', + __DIR__ . '/var/backups' +); + +$out = fopen(__DIR__ . '/restored-large.xml', 'wb'); +king_object_store_get_to_stream('tenant-42/invoices/large.xml', $out); +fclose($out); +``` + +## OO, Example 1: ObjectStore + +```php + 'local_fs', + 'storage_root_path' => __DIR__ . '/var/object-store', +]); + +ObjectStore::put('tenant-42/readme.txt', 'ready', ['content_type' => 'text/plain']); +echo ObjectStore::get('tenant-42/readme.txt') . PHP_EOL; +``` + +## OO, Example 2: Resumable Upload + +```php + 'application/zip'] +); + +foreach (glob(__DIR__ . '/chunks/*.part') as $path) { + $stream = fopen($path, 'rb'); + ObjectStore::appendResumableUploadChunk($session['upload_id'], $stream); + fclose($stream); +} + +$complete = ObjectStore::completeResumableUpload($session['upload_id']); +$stats = ObjectStore::getStats(); + +var_dump($complete, $stats); +``` diff --git a/docs/openai-compatible-inference.md b/docs/openai-compatible-inference.md new file mode 100644 index 000000000..260db19bd --- /dev/null +++ b/docs/openai-compatible-inference.md @@ -0,0 +1,316 @@ +# OpenAI-Compatible Inference Router + +King's OpenAI-compatible router is a King HTTP response helper over loaded +`King\Inference\Model` objects. It does not proxy to another inference service. +Chat, Responses, legacy Completions, Models, and Embeddings are all routed +through the same model registry. + +The bundled local router (`bin/king-openai-router`) loads its serving model +through `king_inference_runtime_model_config()` and +`king_inference_runtime_model_load()`. Inference backend, artifact path, GPU +admission, context length, memory mode, VRAM reserve, and thermal policy come +from the effective King config snapshot. Router bind settings and request drain +limits are router options; they do not override model backend selection. + +## Function, Example 1: One Router + +```php + king_inference_model_load([ + 'name' => 'support-small', + 'artifact_path' => $supportModelPath, + 'backend' => [ + 'name' => 'local', + 'runner_path' => $runner, + ], + 'owned_by' => 'internal-platform', + ]), + 'support-embeddings' => king_inference_model_load([ + 'name' => 'support-embeddings', + 'artifact_path' => $supportModelPath, + 'backend' => 'king_native_cpu', + 'embedding_tensor' => 'token_embd.weight', + 'owned_by' => 'internal-platform', + ]), +]; + +while (true) { + king_http1_server_listen_once( + '127.0.0.1', + 8080, + null, + static fn (array $request): array => + king_inference_openai_http_response($models, $request, [ + 'read_timeout_ms' => 250, + 'max_events' => 4096, + 'max_idle_events' => 240, + 'max_embedding_inputs' => 512, + 'max_embedding_tokens' => 2048, + 'max_embedding_dimensions' => 8192, + ]) + ); +} +``` + +The router supports: + +- `GET /v1/models` +- `GET /v1/models/{model}` +- `POST /v1/chat/completions` +- `POST /v1/responses` +- `POST /v1/completions` +- `POST /v1/embeddings` + +All POST routes require `body` to be a JSON string containing a JSON object. +Missing, empty, array, or scalar request bodies are rejected before model +selection so malformed requests receive endpoint-specific request-body errors +instead of misleading model-selection errors. + +Router options include bounded drain controls (`read_timeout_ms`, +`max_events`, `max_idle_events`), generation input limits +(`max_chat_messages`, `max_response_input_items`, `max_completion_prompts`), +and embedding limits (`max_embedding_inputs`, `max_embedding_tokens`, +`max_embedding_dimensions`). The default drain controls are +`read_timeout_ms=250`, `max_events=4096`, and `max_idle_events=240`. The +default generation input limits are `max_chat_messages=256`, +`max_response_input_items=256`, and `max_completion_prompts=128`; set them lower +for public or tenant-shared routes where one request must not fan out into +excessive prompt construction or backend generation work. The default embedding +limits are `max_embedding_inputs=2048`, `max_embedding_tokens=8192`, and +`max_embedding_dimensions=8192`. Drain control and limit option values must be +positive integers; invalid router options return a server error instead of being +silently replaced by defaults. + +Model selection is explicit when more than one model is registered. The JSON +`model` field is matched against the registry key first and then against the +loaded model name and the listed OpenAI model id. Direct helpers that already +receive one loaded model may omit `model`, but any provided `model` field must +be a non-empty string. The registered models must resolve to unique listed +OpenAI model ids; duplicate listed ids are rejected as router configuration +errors. +Loaded model metadata is strict before the router sees it: model `name`, +`owned_by`, `embedding_tensor`, `token_embedding_tensor`, `output_tensor`, +`output_projection_tensor`, `lm_head_tensor`, +`attention_query_tensor_pattern`, `attention_key_tensor_pattern`, +`attention_value_tensor_pattern`, `attention_output_tensor_pattern`, +`rms_norm_attention_tensor_pattern`, `rms_norm_ffn_tensor_pattern`, and +`rms_norm_final_tensor`, `ffn_gate_tensor_pattern`, `ffn_up_tensor_pattern`, +and `ffn_down_tensor_pattern` must be non-empty strings when provided. + +`GET /v1/models` and `GET /v1/models/{model}` return normal OpenAI model +objects plus an `x_king` extension object. Model objects always include integer +`created`; when the model config has no usable non-negative integer timestamp, +King reports `created: 0` and sets `x_king.created_config_valid` accordingly. +The `x_king` object also contains the resolved King backend, configured backend, +active backend, active device, fallback mode, silent CPU fallback state, CUDA +admission reason, memory mode, readiness object, whether the backend +configuration resolved cleanly, whether the model can serve the generic OpenAI +generation routes, whether it supports native graph streaming, whether it can +serve embeddings, whether configured GPU use is enabled for the model, and the +backend capability map from the loaded model. Frontends should prefer +`x_king.client_capabilities` for UI decisions because it is a versioned, +runtime-ready contract: `openai_chat_completions`, +`openai_chat_completions_stream`, `openai_responses`, +`openai_completions`, `openai_embeddings`, `native_graph_streaming`, +`requires_gpu`, `gpu_runtime_required`, and `gpu_generation_ready` are direct +booleans, and executable OpenAI tool-call dispatch is explicitly reported as +`false` with `tool_call_status=unsupported`. Tool schemas in chat requests are +accepted for editor-client compatibility and logged by the local router, but +plain inference ignores them. They do not make the route execute a tool, buffer +the stream for synthetic tool-call extraction, or fail inference when no King +MCP tool path is configured. +If a backend configuration cannot be resolved, `x_king.backend` is `invalid`, +`x_king.backend_config_valid` is `false`, and all executable client capability +flags are reported as +unavailable. The router-level `owned_by` option overrides the model config +owner for model-listing responses and must be a non-empty string when provided; +invalid listing options return a server error instead of being ignored. + +Chat message `content` and Responses input item `content` may be a string or an +array of text content parts. Responses `instructions` must be a non-empty string +when provided. King extracts text from `text`, `content`, or `refusal` fields +and feeds that into the local text-generation prompt. Non-text parts such as +image, audio, or file payloads are rejected for this local text inference route +instead of being silently ignored. + +The generic OpenAI HTTP generation routes accept either a text-generation stream +backend or a `king_native_cpu` request that explicitly carries `graph` or +`graphs`. `graph` must be a JSON object, `graphs` must be a JSON array, and +`graph_options` must be a JSON object where provided. Native graph requests +still return OpenAI-compatible Chat Completions chunks or JSON responses, but +the router does not synthesize a +native graph from arbitrary Chat Completions payloads. When a generation route +selects a native graph backend without an explicit graph request, King returns an +OpenAI-shaped `400` JSON error instead of falling through to a low-level stream +exception. Native graph streams are stateless by default. Set `with_memory` or +`with-memory` to `true` in the request, stream options, or `graph_options` only +when the next graph should inherit the previous graph result state. The system +baseline is `king.inference_with_memory=0` in `php.ini`; a loaded model config +can override that baseline before per-request values are applied. + +## Function, Example 2: Embeddings + +```php + 'POST', + 'uri' => '/v1/embeddings', + 'body' => json_encode([ + 'model' => 'support-embeddings', + 'input' => [ + 'invoice status: rejected because VAT summary does not match lines', + 'question: why did my electronic invoice fail validation?', + ], + 'encoding_format' => 'float', + 'dimensions' => 1024, + ], JSON_UNESCAPED_SLASHES), +], [ + 'embedding_tensor' => 'token_embd.weight', + 'max_embedding_inputs' => 512, + 'max_embedding_tokens' => 2048, +]); + +if ($response['status'] !== 200) { + throw new RuntimeException($response['body']); +} + +$payload = json_decode($response['body'], true, flags: JSON_THROW_ON_ERROR); +$firstVector = $payload['data'][0]['embedding']; +``` + +Embeddings are mean-pooled from the model's native token embedding tensor after +King tokenizes the input. The tensor name can be supplied in the loaded model +config as `embedding_tensor` or `token_embedding_tensor`, or as router option +`embedding_tensor`. If no name is supplied, King uses the shared token +embedding resolver: explicit names win, known GGUF names such as +`token_embd.weight` are checked next, and a guarded shape/name-hint scan is used +only when the model metadata makes the embedding matrix unambiguous. + +`input` may be a string, a list of strings, one token-id list, or a batch of +token-id lists. `dimensions` can request a bounded prefix of the native +embedding width; the tensor row stride remains the native width internally. +`encoding_format` supports `float` arrays and `base64` strings. The base64 form +contains the same mean-pooled vector encoded as Float32 little-endian bytes. +Embedding input count, token count, and output dimensions are bounded by the +positive router options `max_embedding_inputs`, `max_embedding_tokens`, and +`max_embedding_dimensions`. + +Generation responses include tokenizer-backed usage when the loaded model can +count both the prompt and the produced text. Chat Completions and legacy +Completions return `prompt_tokens`, `completion_tokens`, and `total_tokens`. +Responses payloads return `input_tokens`, `output_tokens`, and `total_tokens`. +If token counting is unavailable for the model, the router keeps `usage` as +`null` instead of failing an otherwise completed inference request. + +Generation controls are normalized before the stream starts. Chat Completions +accept `max_completion_tokens` and legacy `max_tokens`; Responses accepts +`max_output_tokens` and legacy `max_tokens`. King maps those fields to the +internal `max_tokens` stream contract and carries `temperature`, `top_p`, +`seed`, and the King-specific `top_k` option through the same path. Max-token +options must be positive integers, numeric generation options must be finite +numbers, `temperature` must be non-negative, `top_p` must be greater than zero +and at most one, and `top_k` must be a non-negative integer. Invalid generation +controls return an OpenAI-shaped `400` response before any backend process is +started. +Chat Completions and legacy Completions also validate OpenAI `stop` as a +string or an array of one to four non-empty strings and pass those stop +sequences to the local generation runner. The runner withholds possible stop +prefixes until they are known to be ordinary text, so the matched stop sequence +is not included in the streamed or non-streamed response content. They accept +`n` only when it is the integer `1`; requests for multiple independent choices +return an OpenAI-shaped `400` instead of silently returning fewer choices than +requested. + +The local generation routes reject active requests for features that this +runtime does not yet execute. Chat Completions accepts tool/function fields as +model context and leaves executable dispatch disabled, but rejects +multimodal/audio output, prediction hints, and logprob output; it accepts +`response_format` only when `type` is `text`. Legacy Completions rejects +suffix, echo, best-of, and logprob output. Responses rejects tool calling, +reasoning blocks, include filters, and continuation from a previous response. +Neutral defaults such as `null`, `false`, or empty arrays are treated as absent. +Chat Completions `messages` arrays are supported only up to +`max_chat_messages`. Responses input item lists are supported only up to +`max_response_input_items`. +Legacy Completions prompt arrays are supported only up to +`max_completion_prompts`; streaming legacy Completions still require a single +string prompt. + +All OpenAI generation routes validate `stream` as a boolean when it is provided. +They also validate `stream_options` as a JSON object, and +`stream_options.include_usage` must be a boolean. For streamed Chat Completions +and legacy Completions, `include_usage=true` appends a final OpenAI-shaped usage +chunk with empty `choices` immediately before the final `data: [DONE]` marker. +Other chunks carry `usage: null`, and the final usage chunk is computed through +the same tokenizer-backed path as non-streaming generation responses. +Live native streams created with `king_inference_openai_chat_stream()` return +token-content chunks immediately. They do not emit a leading role-only chunk +before the first native token. + +## OO, Example 1: Static Facade + +```php + 'POST', + 'path' => '/v1/chat/completions', + 'body' => json_encode([ + 'model' => 'support-small', + 'messages' => [ + ['role' => 'system', 'content' => 'Answer support questions from verified context.'], + ['role' => 'user', 'content' => 'Summarize the invoice rejection.'], + ], + 'stream' => false, + ], JSON_UNESCAPED_SLASHES), +]); +``` + +The OO facade uses the same router implementation as the procedural function. +Application code can mount it behind a King HTTP server, apply its own auth and +tenant checks before the call, and keep the response array compatible with +King's server handlers. + +## OO, Example 2: Tenant-Aware Handler + +```php + 403, + 'headers' => ['content-type' => 'application/json'], + 'body' => '{"error":{"message":"tenant not allowed","type":"invalid_request_error"}}', + ]; + } + + return Inference::openaiHttpResponse($tenantModels[$tenantId], $request, [ + 'read_timeout_ms' => 250, + 'max_events' => 4096, + 'max_embedding_tokens' => 2048, + ]); +}; +``` + +The router intentionally does not own tenant authorization. It expects the +application boundary to decide which model registry a request can access. That +keeps OpenAI-compatible transport behavior inside King while preserving the +host application's security model. diff --git a/docs/pipeline-orchestrator.md b/docs/pipeline-orchestrator.md new file mode 100644 index 000000000..25ba5f542 --- /dev/null +++ b/docs/pipeline-orchestrator.md @@ -0,0 +1,122 @@ +# Pipeline Orchestrator + +The orchestrator executes tool steps, can persist runs, and supports local, +file-worker, and remote-peer backends. Handlers are process-local and must be +registered inside the executing process. + +## Internal Layout + +The native orchestrator runtime lives under +`extension/src/pipeline_orchestrator/` with public contracts in +`extension/include/pipeline_orchestrator/`. PHP-facing registration metadata, +class-method table entries, and function-table entries live under +`extension/include/pipeline_orchestrator/`; runtime execution API, durable tool +registry, and queue/state persistence helpers remain under +`extension/src/pipeline_orchestrator/`. + +## Function, Example 1: Local Two-Step Run + +```php + 'php']); +king_pipeline_orchestrator_register_tool('normalize-vat', ['kind' => 'php']); + +king_pipeline_orchestrator_register_handler('read-invoice', static function (array $ctx): array { + return ['output' => [ + 'invoice_id' => $ctx['input']['invoice_id'], + 'net' => 10000, + 'vat_rate' => 19, + ]]; +}); + +king_pipeline_orchestrator_register_handler('normalize-vat', static function (array $ctx): array { + $invoice = $ctx['input']; + $invoice['vat'] = (int) round($invoice['net'] * $invoice['vat_rate'] / 100); + + return ['output' => $invoice]; +}); + +$result = king_pipeline_orchestrator_run( + ['invoice_id' => 'INV-1001'], + [['tool' => 'read-invoice'], ['tool' => 'normalize-vat']], + ['trace_id' => 'invoice-INV-1001'] +); + +var_dump($result); +``` + +## Function, Example 2: Queue for File Worker + +```php + 'php']); +king_pipeline_orchestrator_register_handler('validate-ubl', static function (array $ctx): array { + $input = $ctx['input']; + $input['validation'] = ['ok' => true, 'profile' => 'peppol-bis-billing-3']; + + return ['output' => $input]; +}); + +$queued = king_pipeline_orchestrator_dispatch( + ['object_id' => 'tenant-42/invoices/INV-1001.xml'], + [['tool' => 'validate-ubl']], + ['trace_id' => 'queued-ubl-validation-INV-1001'] +); + +$runId = $queued['run_id']; +$workerResult = king_pipeline_orchestrator_worker_run_next(); +$snapshot = king_pipeline_orchestrator_get_run($runId); + +var_dump($workerResult, $snapshot); +``` + +## OO, Example 1: Static OO Facade + +```php + 'php']); +PipelineOrchestrator::registerHandler('prepare', static function (array $ctx): array { + return ['output' => ['prepared' => true] + $ctx['input']]; +}); + +$result = PipelineOrchestrator::run( + ['invoice_id' => 'INV-1002'], + [['tool' => 'prepare']], + ['trace_id' => 'oo-prepare-INV-1002'] +); + +var_dump($result); +``` + +## OO, Example 2: Async Run and Run Snapshot + +```php + 'php']); +PipelineOrchestrator::registerHandler('classify', static function (array $ctx): array { + $input = $ctx['input']; + $input['class'] = $input['amount'] < 0 ? 'credit-note' : 'invoice'; + + return ['output' => $input]; +}); + +$awaitable = PipelineOrchestrator::runAsync( + ['invoice_id' => 'INV-1003', 'amount' => -2500], + [['tool' => 'classify']], + ['trace_id' => 'classify-INV-1003'] +); + +$result = $awaitable->await(2000); +$component = king_system_get_component_info('pipeline_orchestrator'); +$runId = $component['configuration']['last_run_id'] ?? null; + +if (is_string($runId)) { + var_dump(PipelineOrchestrator::getRun($runId)); +} + +var_dump($result); +``` diff --git a/docs/rtp.md b/docs/rtp.md new file mode 100644 index 000000000..15acd4fa6 --- /dev/null +++ b/docs/rtp.md @@ -0,0 +1,94 @@ +# RTP + +RTP is available procedurally through `king_rtp_*`. The native OO surface is +`King\RTP\Socket` and keeps the same native RTP resource internally. + +## Internal Layout + +The native RTP socket and object contracts live in +`extension/include/media/rtp.h` and are exported through +`extension/include/media/index.h`. Runtime code lives under +`extension/src/media/`. PHP-facing registration metadata, function-table +entries, and RTP OO method entries live under `extension/include/media/`; RTP +object binding implementation remains under `extension/src/media/`. + +## Function, Example 1: Bind Socket and Read ICE/DTLS Data + +```php +iceCredentials()); +echo $rtp->dtlsFingerprint() . PHP_EOL; +$rtp->close(); +``` + +## OO, Example 2: Media Peer Service with Native Socket Class + +```php +rtp->acceptDtls($ip, $port, 3000)) { + throw new RuntimeException(king_get_last_error()); + } + } + + public function sendAudioFrame(string $host, int $port, string $frame): void + { + if (!$this->rtp->send($host, $port, $frame)) { + throw new RuntimeException('RTP send failed'); + } + } + + public function receiveAudioFrame(): ?array + { + $packet = $this->rtp->receive(1000); + return $packet === false ? null : $packet; + } +} + +$peer = new MediaPeer(new Socket('0.0.0.0', 5004)); +$peer->sendAudioFrame('192.0.2.10', 5004, random_bytes(160)); +``` diff --git a/docs/semantic-dns.md b/docs/semantic-dns.md new file mode 100644 index 000000000..fde6a35d8 --- /dev/null +++ b/docs/semantic-dns.md @@ -0,0 +1,118 @@ +# Semantic DNS + +Semantic DNS registers services with load and status data and returns +discovery and routing decisions. The native API is procedural +`king_semantic_dns_*`. A native OO class is not exported yet; the OO examples +are userland adapters. + +## Internal Layout + +The Semantic DNS runtime lives under `extension/src/semantic_dns/` with public +contracts in `extension/include/semantic_dns/`. Its PHP arginfo and +function-table entries live under `extension/include/semantic_dns/` and are +consumed by the extension bootstrap through `extension/include/php_king/`. + +## Function, Example 1: Register and Discover a Service + +```php + '127.0.0.1', + 'port' => 5353, + 'ttl_seconds' => 30, +]); + +king_semantic_dns_register_service([ + 'service_id' => 'invoice-api-fra-1', + 'service_name' => 'invoice-api', + 'service_type' => 'http', + 'hostname' => '10.0.0.10', + 'port' => 8080, + 'status' => 'healthy', + 'current_load_percent' => 20, +]); + +$discovery = king_semantic_dns_discover_service('http', [ + 'service_name' => 'invoice-api', +]); + +var_dump($discovery); +``` + +## Function, Example 2: Status Update and Optimal Route + +```php + 85, + 'active_connections' => 120, +]); + +king_semantic_dns_register_service([ + 'service_id' => 'invoice-api-fra-2', + 'service_name' => 'invoice-api', + 'service_type' => 'http', + 'hostname' => '10.0.0.11', + 'port' => 8080, + 'status' => 'healthy', + 'current_load_percent' => 15, +]); + +$route = king_semantic_dns_get_optimal_route('invoice-api', [ + 'region' => 'eu-central', +]); + +var_dump($route); +``` + +## OO, Example 1: Registry Adapter + +```php + $id, + 'service_name' => $name, + 'service_type' => 'http', + 'hostname' => $host, + 'port' => $port, + 'status' => 'healthy', + ]); + } + + public function discover(string $type): array + { + return king_semantic_dns_discover_service($type); + } +} + +$registry = new SemanticDnsRegistry(); +$registry->registerHttp('portal-api-1', 'portal-api', '10.0.1.10', 8080); +var_dump($registry->discover('http')); +``` + +## OO, Example 2: Router Service + +```php +route('invoice-api', ['tenant_id' => 42]); + +printf("http://%s:%d\n", $route['hostname'], $route['port']); +``` diff --git a/docs/source-layout.md b/docs/source-layout.md new file mode 100644 index 000000000..88ae19dda --- /dev/null +++ b/docs/source-layout.md @@ -0,0 +1,256 @@ +# Source Layout + +King is a PHP extension repository. The native runtime is intentionally scoped +under `extension/` instead of placing C sources and headers directly at the +repository root. + +```text +extension/ ++-- config.m4 ++-- include/ +| +-- php_king.h +| +-- php_king/ +| +-- awaitable/ +| +-- inference/ +| +-- mcp/ +| +-- xslt/ +| +-- ... ++-- src/ + +-- php_king.c + +-- php_king/ + +-- awaitable/ + +-- inference/ + +-- mcp/ + +-- xslt/ + +-- ... +``` + +`extension/config.m4` adds both `extension/` and `extension/include/` to the C +include path. Inside extension sources, module headers are therefore included +as `awaitable/index.h`, `inference/index.h`, `mcp/index.h`, `xslt/index.h`, and +so on. + +## Public Header Root + +The public extension header root is `extension/include/`. + +- `extension/include/php_king.h` is the central extension header. +- `extension/include/php_king/` contains bootstrap-owned declarations, + constants, global state, helper contracts, and root registration metadata. +- `extension/include/awaitable/` contains the `King\Awaitable` object contract, + cancel-token declarations, arginfo, class entries, and function entries. +- `extension/include/inference/` contains the native inference umbrella header + and splits class metadata, function entries, registration hooks, arginfo, and + model/stream contracts into focused subdirectories. +- `extension/include/mcp/` contains public MCP server and King-internal MCP peer + contracts, resource identifiers, arginfo, class entries, and function + entries. +- `extension/include/xslt/` contains the XSLT processor contract, arginfo, + class entries, and function entries. + +There is no separate repository-root `include/` directory in the current +layout. Adding one would either duplicate extension headers or create a second +include root for the same ABI surface. The active contract is one native +extension include root: `extension/include/`. + +## Runtime Source Root + +The active runtime implementation root is `extension/src/`. + +- `extension/src/php_king.c` is the single extension translation unit. +- `extension/src/php_king/` contains root extension bootstrap glue: module + bindings, function table, class registration, lifecycle hooks, resources, and + shared exception registration. +- `extension/src/awaitable/`, `extension/src/inference/`, + `extension/src/mcp/`, and `extension/src/xslt/` are subsystem roots. They are + not supposed to live under `extension/src/php_king/`. + +The `php_king/` source directory is therefore not a primitive namespace. It is +the extension bootstrap layer that includes and registers the primitive +subsystems. + +## Adding A Primitive + +New primitives should follow this shape: + +```text +extension/include// ++-- index.h ++-- .h ++-- arginfo/ +| +-- index.h +| +-- arginfo.h ++-- function_entries.h ++-- class_entries.h ++-- class_method_entries.h ++-- class_methods.h + +extension/src// ++-- .c ++-- binding/ +| +-- php_binding.inc ++-- core/ +| +-- registration.inc +| +-- state.inc ++-- focused implementation leaves +``` + +The root extension header `extension/include/php_king.h` should include the +primitive umbrella header, and `extension/src/php_king/function_table.inc` or +the relevant class registration table should include the primitive function and +method entries. Active `.c` files must be listed in `extension/config.m4`. + +This keeps the ABI surface, PHP registration metadata, and runtime +implementation discoverable without flattening every subsystem into one large +directory. + +## Inference Header Shape + +Inference is larger than the smaller primitives, so its public header surface is +split by PHP-extension responsibility: + +```text +extension/include/inference/ ++-- index.h ++-- arginfo/ +| +-- index.h +| +-- arginfo.h ++-- classes/ +| +-- class_entries.h +| +-- class_method_entries.h +| +-- class_methods.h ++-- functions/ +| +-- function_entries.h ++-- registration/ +| +-- registration.h ++-- surface/ + +-- inference.h +``` + +`index.h` is the umbrella include. Subsystem bootstrap code should prefer that +umbrella unless it explicitly owns one narrower table such as function entries +or class method entries. + +## Inference Source Shape + +The inference primitive is intentionally split by ownership instead of keeping a +large flat implementation directory: + +```text +extension/src/inference/ ++-- inference.c ++-- binding/ +| +-- bootstrap/ +| +-- stacks/ +| +-- api/ +| +-- cuda/ +| +-- model/ +| +-- openai/ +| +-- request/ +| +-- runtime/ +| +-- stream/ ++-- api/ +| +-- async/ +| +-- oop/ +| | +-- model/ +| | +-- stream/ +| +-- procedural/ +| +-- surface/ ++-- backends/ +| +-- contracts/ +| +-- local/ +| +-- native/ +| +-- registry/ ++-- core/ +| +-- bootstrap/ +| +-- classes/ +| +-- support/ ++-- runtime/ +| +-- cache/ +| +-- config/ +| +-- events/ +| +-- memory/ +| +-- policy/ ++-- gguf/ +| +-- loader/ +| +-- metadata/ ++-- tokenizer/ ++-- tensor/ +| +-- core/ +| +-- graph/ +| | +-- builder/ +| | +-- core/ +| | +-- ops/ +| +-- resolvers/ +| +-- attention/ +| | +-- components/ +| | +-- metadata/ +| | +-- shape/ ++-- openai/ +| +-- chat/ +| +-- http/ +| +-- resources/ +| +-- runtime/ +| +-- backend/ +| +-- compat/ +| +-- options/ +| +-- stream/ ++-- cuda/ + +-- runtime/ + | +-- context/ + | +-- policy/ + | +-- status/ + | +-- weights/ + +-- kernels/ + | +-- attention/ + | +-- device/ + | +-- embedding/ + | +-- ffn/ + | +-- matvec/ + | +-- norm/ + | +-- projection/ + | +-- rope/ + +-- decoder_graph/ + | +-- core/ + | +-- device/ + | | +-- compare/ + | | | +-- embedding/ + | | | +-- matvec/ + | | | +-- norm/ + | | | +-- projection/ + | | +-- core/ + | +-- prefill/ + | | +-- ffn/ + | | +-- kv/ + | | +-- residual/ + | +-- ops/ + | +-- attention/ + | +-- control/ + | +-- ffn/ + | +-- final/ + | +-- kv/ + | +-- projection/ + | +-- residual/ + | +-- rope/ + +-- prompt/ + +-- loop/ + +-- prefill/ + +-- attention/ + +-- batch/ + +-- ffn/ + +-- kv/ + +-- query/ + +-- remaining/ +``` + +Decoder graph files are grouped by the operation family they own. Device +execution code may include those families, but individual operation files should +not drift back into a mixed `ops/` catch-all directory. + +Inference folders should stay responsibility-oriented. Aggregator files belong +under `binding/bootstrap/` or one of the `binding/stacks/*/` folders; backend +implementations belong under their runtime family; runtime support is separated +into cache, config, memory, events, and policy leaves; CUDA runtime code is +split into context, policy, status, and weight-upload ownership. Do not add new +inference implementation files back to the primitive root or to a mixed +catch-all directory. diff --git a/docs/system-runtime.md b/docs/system-runtime.md new file mode 100644 index 000000000..e20019f32 --- /dev/null +++ b/docs/system-runtime.md @@ -0,0 +1,109 @@ +# System Runtime + +The system runtime coordinates components such as client, server, object +store, CDN, telemetry, autoscaling, MCP, IIBIN, and orchestrator. The native +API is procedural `king_system_*`. A native OO class is not exported yet; the +OO examples are userland adapters. + +## Internal Layout + +The coordinated system runtime lives in +`extension/src/integration/system_integration.c` with public contracts in +`extension/include/integration/system_integration.h`. The integration module +owns PHP-facing registration metadata under `extension/include/integration/`, +while core status helpers also contribute system introspection leaves. + +## Function, Example 1: Init, Status, Shutdown + +```php + 'local-dev', + 'node_id' => 'node-1', + 'state_root_path' => __DIR__ . '/var/system', + 'components' => ['client', 'server', 'object_store', 'pipeline_orchestrator'], +]); + +$status = king_system_get_status(); + +if (($status['lifecycle'] ?? '') !== 'ready') { + var_dump($status['readiness_blockers'] ?? []); +} + +king_system_shutdown(); +``` + +## Function, Example 2: Request Through Runtime and Recovery + +```php + 'invoice.accept', + 'tenant_id' => 42, + 'invoice_id' => 'INV-1001', +]); + +if (($response['ok'] ?? false) !== true) { + king_system_fail_component('pipeline_orchestrator'); + king_system_recover(); +} + +$report = king_system_get_performance_report(); +var_dump($response, $report); +``` + +## OO, Example 1: Runtime Adapter + +```php +init(['cluster_id' => 'local-dev', 'node_id' => 'node-1']); +var_dump($runtime->status()); +``` + +## OO, Example 2: Health Gate for Worker + +```php +assertWorkerCanClaim(); +var_dump($admission->component('pipeline_orchestrator')); +``` diff --git a/docs/telemetry.md b/docs/telemetry.md new file mode 100644 index 000000000..c72b8160a --- /dev/null +++ b/docs/telemetry.md @@ -0,0 +1,123 @@ +# Telemetry + +Telemetry records spans, metrics, and logs and can propagate trace context. +The native API is procedural `king_telemetry_*`. A native OO class is not +exported yet; the OO examples are userland adapters. + +## Internal Layout + +The telemetry runtime lives under `extension/src/telemetry/` with public +contracts exported through `extension/include/telemetry/index.h`. Its PHP +arginfo and function-table entries live under `extension/include/telemetry/` +and are consumed by the extension bootstrap through +`extension/include/php_king/`. +Server-side telemetry bootstrap entry points such as +`king_server_init_telemetry()` are registered through the server binding +metadata under `extension/include/server/`. + +## Function, Example 1: Span, Metric, Log + +```php + 'invoice-platform', + 'exporter' => 'otlp', + 'endpoint' => 'http://127.0.0.1:4318', +]); + +$span = king_telemetry_start_span('invoice.receive', [ + 'tenant_id' => '42', + 'invoice_id' => 'INV-1001', +]); + +king_telemetry_record_metric('invoice_received_total', 1, ['tenant' => '42']); +king_telemetry_log('info', 'invoice received', ['invoice_id' => 'INV-1001']); + +king_telemetry_end_span($span, ['status' => 'accepted']); +$flush = king_telemetry_flush(); + +var_dump($flush); +``` + +## Function, Example 2: Propagate Trace Context to HTTP + +```php + 'INV-1002']); + +$headers = king_telemetry_inject_context([ + 'content-type' => 'application/json', + 'accept' => 'application/json', +]); + +$response = king_client_send_request( + 'http://127.0.0.1:8080/nav/submit', + 'POST', + $headers, + json_encode(['invoice_id' => 'INV-1002'], JSON_THROW_ON_ERROR), + ['timeout_ms' => 3000] +); + +king_telemetry_record_metric('nav_submit_total', 1, [ + 'status' => (string) ($response['status'] ?? 0), +]); + +king_telemetry_end_span($span); +king_telemetry_flush(); +``` + +## OO, Example 1: Userland Tracer + +```php +start('invoice.map', ['invoice_id' => 'INV-1003']); +$tracer->end($span, ['result' => 'ok']); +``` + +## OO, Example 2: Instrumented Service + +```php +tracer->start('invoice.submit', [ + 'invoice_id' => (string) $invoice['id'], + ]); + + try { + king_telemetry_record_metric('invoice_submit_attempt_total', 1); + + $response = king_client_send_request( + 'http://127.0.0.1:8080/invoices', + 'POST', + ['content-type' => 'application/json'], + json_encode($invoice, JSON_THROW_ON_ERROR) + ); + + $this->tracer->end($span, ['http.status_code' => $response['status'] ?? 0]); + return $response; + } catch (Throwable $e) { + $this->tracer->end($span, ['error' => $e->getMessage()]); + throw $e; + } + } +} +``` diff --git a/docs/websocket.md b/docs/websocket.md new file mode 100644 index 000000000..3bdd6e8c7 --- /dev/null +++ b/docs/websocket.md @@ -0,0 +1,114 @@ +# WebSocket + +The WebSocket client can be used procedurally through +`king_client_websocket_*` or through `King\WebSocket\Connection`. On the +server side, procedural upgrade is available through +`king_server_upgrade_to_websocket()` and OO through `King\WebSocket\Server`. + +## Internal Layout + +The shared WebSocket state and connection object contract live in +`extension/include/client/websocket.h`. The server object contract lives in +`extension/include/server/websocket.h`. Client runtime code lives under +`extension/src/client/websocket/`; server upgrade and `King\WebSocket\Server` +runtime code live under `extension/src/server/`. + +Client-side WebSocket registration metadata is owned by the client include +surface in `extension/include/client/`, including `websocket_arginfo.h`, +`websocket_class_method_entries.h`, `arginfo/`, and `function_entries.h`. +Server-side WebSocket upgrade and listener registration metadata is owned by +`extension/include/server/`, including `websocket_server_arginfo.h`, +`websocket_server_class_method_entries.h`, `arginfo/`, and +`function_entries.h`. + +## Function, Example 1: Client Echo + +```php + 'invoice-console'], + ['max_payload_size' => 1024 * 1024, 'handshake_timeout_ms' => 1500] +); + +if ($ws === false) { + throw new RuntimeException(king_client_websocket_get_last_error()); +} + +king_client_websocket_send($ws, json_encode([ + 'type' => 'ping', + 'request_id' => 'req-1001', +], JSON_THROW_ON_ERROR)); + +$payload = king_client_websocket_receive($ws, 1000); +var_dump($payload); + +king_client_websocket_close($ws, 1000, 'done'); +``` + +## Function, Example 2: HTTP/1 Upgrade in a Handler + +```php + 400, + 'headers' => ['content-type' => 'application/json'], + 'body' => '{"error":"websocket_upgrade_failed"}', + ]; + } + + king_websocket_send($ws, json_encode([ + 'type' => 'connected', + 'connection_id' => $request['headers']['sec-websocket-key'][0] ?? null, + ], JSON_THROW_ON_ERROR)); + + return ['status' => 101, 'headers' => [], 'body' => '']; +}); +``` + +## OO, Example 1: Connection + +```php + 'ops-console', +]); + +$connection->send(json_encode(['type' => 'subscribe', 'topic' => 'invoices'], JSON_THROW_ON_ERROR)); +$message = $connection->receive(1000); + +echo $message . PHP_EOL; +$connection->close(1000, 'done'); +``` + +## OO, Example 2: Server with Broadcast + +```php +accept(); +$info = $connection->getInfo(); + +$connection->send(json_encode([ + 'type' => 'accepted', + 'connection_id' => $info['connection_id'], +], JSON_THROW_ON_ERROR)); + +$server->broadcast(json_encode([ + 'type' => 'system', + 'message' => 'maintenance-window-open', +], JSON_THROW_ON_ERROR)); + +$server->stop(); +``` diff --git a/docs/xslt.md b/docs/xslt.md new file mode 100644 index 000000000..dd41869dc --- /dev/null +++ b/docs/xslt.md @@ -0,0 +1,180 @@ +# XSLT 2.0/3.0 for E-Invoicing + +XSLT is available procedurally through `king_xslt_engine_status()`, +`king_xslt_transform_file()`, and `king_xslt_transform_to_file()`. The native +OO surface is `King\XSLT\Processor`. The processor stores default options such +as SaxonC properties and can override them per transformation run. + +## Internal Layout + +The native XSLT runtime and object contracts live in +`extension/include/xslt/xslt.h` and are exported through +`extension/include/xslt/index.h`. Runtime code lives under +`extension/src/xslt/`; `extension/src/xslt/php_binding.inc` is the PHP userland +binding. PHP registration metadata lives under `extension/include/xslt/`, +including `arginfo/`, `function_entries.h`, `class_method_entries.h`, and +`class_methods.h`. + +## Options + +Supported transformation options are deliberately explicit: + +- `cwd`: readable and searchable local directory used as the SaxonC working + directory. When it is omitted, King uses the stylesheet directory. +- `properties`: associative array of SaxonC string properties. Scalar values + and null are accepted and converted to strings before the native call. + +The source XML and stylesheet paths must resolve to readable local files. +Directories are rejected before SaxonC is invoked. +`transformToFile()` writes to a temporary sibling file first and only replaces +the requested output path after SaxonC produced a readable result, so an older +output file cannot be mistaken for a successful transformation. + +Unknown options are rejected instead of being silently ignored. Stylesheet +parameters require SaxonC XDM values and are not accepted by this PHP-visible +surface yet. + +## Function, Example 1: Check Engine + +```php + + + INV-1001 + 2026-06-24 + EUR + + DE123456789 + + + 119.00 + + +``` + +`ubl-summary.xsl`: + +```xml + + + + + + + + + + + + + +``` + +PHP: + +```php + [ + 'http://saxon.sf.net/feature/version-warning' => 'false', + ], + ] +); + +file_put_contents(__DIR__ . '/invoice-report.xml', $result['result']); +``` + +## OO, Example 1: Native King\XSLT\Processor Class + +```php + [ + 'http://saxon.sf.net/feature/version-warning' => 'false', + ], +]); + +$status = $processor->engineStatus(); +if (!($status['available'] ?? false)) { + throw new RuntimeException($status['error'] ?? 'SaxonC runtime not available'); +} + +$result = $processor->transformFile( + __DIR__ . '/invoice.xml', + __DIR__ . '/ubl-summary.xsl' +); + +echo $result['result']; +``` + +## OO, Example 2: UBL Validator Service with Output File + +```php +processor->transformToFile( + $invoicePath, + $stylesheet, + $reportPath, + ['properties' => ['indent' => 'yes']] + ); + + return [ + 'ok' => (bool) ($result['ok'] ?? false), + 'engine' => $result['engine'] ?? 'saxonc', + 'report_path' => $reportPath, + ]; + } +} + +$processor = new Processor([ + 'cwd' => __DIR__, +]); +$service = new UblXsltValidationService($processor); +$report = $service->validateToFile( + __DIR__ . '/invoice.xml', + __DIR__ . '/invoice-validation-report.xml' +); + +var_dump($report); +``` diff --git a/extension/config.m4 b/extension/config.m4 index ba6753683..54397fb9b 100755 --- a/extension/config.m4 +++ b/extension/config.m4 @@ -239,6 +239,7 @@ if test "$PHP_KING" != "no"; then src/king_globals.c \ src/king_init.c \ src/config/config.c \ + src/db_ingest/db_ingest.c \ src/config/app_http3_websockets_webtransport/base_layer.c \ src/config/app_http3_websockets_webtransport/config.c \ src/config/app_http3_websockets_webtransport/default.c \ @@ -368,6 +369,7 @@ if test "$PHP_KING" != "no"; then src/validation/config_param/validate_scheduler_policy.c \ src/validation/config_param/validate_string.c \ src/validation/config_param/validate_string_from_allowlist.c \ + src/awaitable/awaitable.c \ src/client/session.c \ src/client/cancel.c \ src/client/tls.c \ @@ -381,6 +383,7 @@ if test "$PHP_KING" != "no"; then src/iibin/iibin_api.c \ src/autoscaling/autoscaling.c \ src/autoscaling/provisioning.c \ + src/inference/inference.c \ src/integration/system_integration.c \ src/iibin/iibin_registry.c \ src/iibin/iibin_schema.c \ @@ -410,6 +413,7 @@ if test "$PHP_KING" != "no"; then src/server/tls.c \ src/server/websocket.c \ src/xslt/xslt.c \ + src/media/media.c \ src/media/rtp.c \ src/core/version.c \ src/core/health.c \ @@ -421,7 +425,7 @@ if test "$PHP_KING" != "no"; then PHP_SUBST([KING_SHARED_LIBADD]) dnl Signal to php_king.h that we are in runtime mode. - dnl This disables includes for component headers that don't exist yet. + dnl This keeps the public header on the lightweight runtime include surface. PHP_ADD_EXTENSION_DEP(king, standard) CFLAGS="$CFLAGS -DKING_RUNTIME_BUILD" diff --git a/extension/include/autoscaling/arginfo/arginfo.h b/extension/include/autoscaling/arginfo/arginfo.h new file mode 100644 index 000000000..37efb2fa8 --- /dev/null +++ b/extension/include/autoscaling/arginfo/arginfo.h @@ -0,0 +1,8 @@ +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_optional_instances, 0, 0, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, instances, IS_LONG, 0, "1") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_autoscaling_register_node, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, server_id, IS_LONG, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, name, IS_STRING, 1, "null") +ZEND_END_ARG_INFO() diff --git a/extension/include/autoscaling/arginfo/index.h b/extension/include/autoscaling/arginfo/index.h old mode 100755 new mode 100644 index d98453bf8..8791a7990 --- a/extension/include/autoscaling/arginfo/index.h +++ b/extension/include/autoscaling/arginfo/index.h @@ -4,16 +4,13 @@ * PROJECT: king * * PURPOSE: - * Reserved arginfo aggregation point for autoscaling PHP entry points. - * - * The current runtime reuses most shared arginfo blocks from the central - * php_king arginfo table and only defines a small autoscaling-specific shape - * for node registration, so this header remains a thin include anchor rather - * than a generated block of declarations. + * Arginfo aggregation point for autoscaling PHP entry points. * ========================================================================= */ #ifndef KING_AUTOSCALING_ARGINFO_INDEX_H #define KING_AUTOSCALING_ARGINFO_INDEX_H +#include "arginfo.h" + #endif /* KING_AUTOSCALING_ARGINFO_INDEX_H */ diff --git a/extension/include/autoscaling/autoscaling.h b/extension/include/autoscaling/autoscaling.h old mode 100755 new mode 100644 index 8017ee99f..6d462233d --- a/extension/include/autoscaling/autoscaling.h +++ b/extension/include/autoscaling/autoscaling.h @@ -12,7 +12,7 @@ #define KING_AUTOSCALING_H #include -#include "include/config/cloud_autoscale/base_layer.h" +#include "config/cloud_autoscale/base_layer.h" #include #include diff --git a/extension/include/autoscaling/class_entries.h b/extension/include/autoscaling/class_entries.h new file mode 100644 index 000000000..6b0acf016 --- /dev/null +++ b/extension/include/autoscaling/class_entries.h @@ -0,0 +1,12 @@ +/* + * include/autoscaling/class_entries.h - Autoscaling class-entry externs + */ + +#ifndef KING_AUTOSCALING_CLASS_ENTRIES_H +#define KING_AUTOSCALING_CLASS_ENTRIES_H + +#include + +extern zend_class_entry *king_ce_autoscaling; + +#endif /* KING_AUTOSCALING_CLASS_ENTRIES_H */ diff --git a/extension/include/autoscaling/class_method_entries.h b/extension/include/autoscaling/class_method_entries.h new file mode 100644 index 000000000..4fafb8f19 --- /dev/null +++ b/extension/include/autoscaling/class_method_entries.h @@ -0,0 +1,14 @@ +const zend_function_entry king_autoscaling_class_methods[] = { + ZEND_ME_MAPPING(init, king_autoscaling_init, arginfo_king_config_array, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(startMonitoring, king_autoscaling_start_monitoring, arginfo_king_no_args, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(stopMonitoring, king_autoscaling_stop_monitoring, arginfo_king_no_args, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(getMetrics, king_autoscaling_get_metrics, arginfo_king_no_args, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(getStatus, king_autoscaling_get_status, arginfo_king_no_args, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(getNodes, king_autoscaling_get_nodes, arginfo_king_no_args, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(scaleUp, king_autoscaling_scale_up, arginfo_king_optional_instances, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(scaleDown, king_autoscaling_scale_down, arginfo_king_optional_instances, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(registerNode, king_autoscaling_register_node, arginfo_king_autoscaling_register_node, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(markNodeReady, king_autoscaling_mark_node_ready, arginfo_king_one_long, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(drainNode, king_autoscaling_drain_node, arginfo_king_one_long, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + PHP_FE_END +}; diff --git a/extension/include/autoscaling/class_methods.h b/extension/include/autoscaling/class_methods.h new file mode 100644 index 000000000..af25ef7bb --- /dev/null +++ b/extension/include/autoscaling/class_methods.h @@ -0,0 +1,12 @@ +/* + * include/autoscaling/class_methods.h - Autoscaling OO method table externs + */ + +#ifndef KING_AUTOSCALING_CLASS_METHODS_H +#define KING_AUTOSCALING_CLASS_METHODS_H + +#include + +extern const zend_function_entry king_autoscaling_class_methods[]; + +#endif /* KING_AUTOSCALING_CLASS_METHODS_H */ diff --git a/extension/include/autoscaling/function_entries.h b/extension/include/autoscaling/function_entries.h new file mode 100644 index 000000000..1f4ce2d3f --- /dev/null +++ b/extension/include/autoscaling/function_entries.h @@ -0,0 +1,11 @@ + PHP_FE(king_autoscaling_init, arginfo_king_config_array) + PHP_FE(king_autoscaling_start_monitoring, arginfo_king_no_args) + PHP_FE(king_autoscaling_stop_monitoring, arginfo_king_no_args) + PHP_FE(king_autoscaling_get_metrics, arginfo_king_no_args) + PHP_FE(king_autoscaling_get_status, arginfo_king_no_args) + PHP_FE(king_autoscaling_get_nodes, arginfo_king_no_args) + PHP_FE(king_autoscaling_scale_up, arginfo_king_optional_instances) + PHP_FE(king_autoscaling_scale_down, arginfo_king_optional_instances) + PHP_FE(king_autoscaling_register_node, arginfo_king_autoscaling_register_node) + PHP_FE(king_autoscaling_mark_node_ready, arginfo_king_one_long) + PHP_FE(king_autoscaling_drain_node, arginfo_king_one_long) diff --git a/extension/include/autoscaling/index.h b/extension/include/autoscaling/index.h old mode 100755 new mode 100644 index 8dee25d15..2e757a82a --- a/extension/include/autoscaling/index.h +++ b/extension/include/autoscaling/index.h @@ -4,15 +4,15 @@ * PROJECT: king * * PURPOSE: - * Umbrella header placeholder for the autoscaling subsystem. - * - * The current runtime exposes no shared autoscaling declarations here - * yet, but the guarded header keeps include sites stable as the subsystem - * grows. + * Public umbrella header for the autoscaling subsystem. * ========================================================================= */ #ifndef KING_AUTOSCALING_INDEX_H #define KING_AUTOSCALING_INDEX_H +#include "autoscaling.h" +#include "class_entries.h" +#include "registration.h" + #endif /* KING_AUTOSCALING_INDEX_H */ diff --git a/extension/include/autoscaling/registration.h b/extension/include/autoscaling/registration.h new file mode 100644 index 000000000..9d13e4e95 --- /dev/null +++ b/extension/include/autoscaling/registration.h @@ -0,0 +1,13 @@ +/* + * include/autoscaling/registration.h - Autoscaling MINIT hooks + */ + +#ifndef KING_AUTOSCALING_REGISTRATION_H +#define KING_AUTOSCALING_REGISTRATION_H + +#include +#include "class_methods.h" + +void king_autoscaling_register_classes(void); + +#endif /* KING_AUTOSCALING_REGISTRATION_H */ diff --git a/extension/include/awaitable/arginfo/arginfo.h b/extension/include/awaitable/arginfo/arginfo.h new file mode 100644 index 000000000..e09de1371 --- /dev/null +++ b/extension/include/awaitable/arginfo/arginfo.h @@ -0,0 +1,66 @@ +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_await, 0, 1, IS_MIXED, 0) + ZEND_ARG_OBJ_INFO(0, awaitable, King\\Awaitable, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_awaitable_poll, 0, 1, _IS_BOOL, 0) + ZEND_ARG_OBJ_INFO(0, awaitable, King\\Awaitable, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 0, "0") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_awaitable_cancel, 0, 1, _IS_BOOL, 0) + ZEND_ARG_OBJ_INFO(0, awaitable, King\\Awaitable, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_awaitable_status, 0, 1, IS_STRING, 0) + ZEND_ARG_OBJ_INFO(0, awaitable, King\\Awaitable, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_king_awaitable_any, 0, 1, King\\Awaitable, 0) + ZEND_ARG_TYPE_INFO(0, awaitables, IS_ARRAY, 0) + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, cancel, King\\CancelToken, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_king_awaitable_all, 0, 1, King\\Awaitable, 0) + ZEND_ARG_TYPE_INFO(0, awaitables, IS_ARRAY, 0) + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, cancel, King\\CancelToken, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_Awaitable___construct, 0, 0, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Awaitable_await, 0, 0, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Awaitable_poll, 0, 0, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 0, "0") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Awaitable_cancel, 0, 0, _IS_BOOL, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Awaitable_isPending, 0, 0, _IS_BOOL, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Awaitable_isDone, 0, 0, _IS_BOOL, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Awaitable_isCancelled, 0, 0, _IS_BOOL, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Awaitable_getStatus, 0, 0, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Awaitable_getOperation, 0, 0, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_King_Awaitable_any, 0, 1, King\\Awaitable, 0) + ZEND_ARG_TYPE_INFO(0, awaitables, IS_ARRAY, 0) + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, cancel, King\\CancelToken, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_King_Awaitable_all, 0, 1, King\\Awaitable, 0) + ZEND_ARG_TYPE_INFO(0, awaitables, IS_ARRAY, 0) + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, cancel, King\\CancelToken, 1, "null") +ZEND_END_ARG_INFO() diff --git a/extension/include/awaitable/arginfo/index.h b/extension/include/awaitable/arginfo/index.h new file mode 100644 index 000000000..35e6ab676 --- /dev/null +++ b/extension/include/awaitable/arginfo/index.h @@ -0,0 +1,16 @@ +/* + * ========================================================================= + * FILENAME: include/awaitable/arginfo/index.h + * PROJECT: king + * + * PURPOSE: + * Arginfo aggregation point for awaitable and cancellation PHP entry points. + * ========================================================================= + */ + +#ifndef KING_AWAITABLE_ARGINFO_INDEX_H +#define KING_AWAITABLE_ARGINFO_INDEX_H + +#include "arginfo.h" + +#endif /* KING_AWAITABLE_ARGINFO_INDEX_H */ diff --git a/extension/include/awaitable/awaitable.h b/extension/include/awaitable/awaitable.h new file mode 100644 index 000000000..0a57132ef --- /dev/null +++ b/extension/include/awaitable/awaitable.h @@ -0,0 +1,84 @@ +/* + * include/awaitable/awaitable.h - Native Awaitable object contract + */ + +#ifndef KING_AWAITABLE_H +#define KING_AWAITABLE_H + +#include +#include +#include +#include + +typedef enum _king_awaitable_status { + KING_AWAITABLE_PENDING = 0, + KING_AWAITABLE_RESOLVED = 1, + KING_AWAITABLE_REJECTED = 2, + KING_AWAITABLE_CANCELLED = 3 +} king_awaitable_status_t; + +typedef struct _king_awaitable_object king_awaitable_object; +typedef zend_result (*king_awaitable_runner)( + king_awaitable_object *intern, + zend_long timeout_ms, + zval *result +); + +struct _king_awaitable_object { + king_awaitable_status_t status; + zend_string *operation; + zval payload; + zval result; + zval error; + zval cancel_token; + king_awaitable_runner runner; + bool started; + bool cancel_requested; + zend_object std; +}; + +static inline king_awaitable_object * +php_king_awaitable_obj_from_zend(zend_object *obj) +{ + return (king_awaitable_object *) + ((char*)obj - XtOffsetOf(king_awaitable_object, std)); +} + +zend_result king_awaitable_create( + zval *return_value, + const char *operation, + size_t operation_len, + king_awaitable_runner runner, + zval *payload, + zval *cancel_token +); +zend_result king_awaitable_create_function_call( + zval *return_value, + const char *operation, + size_t operation_len, + const char *function_name, + size_t function_name_len, + zval *params, + uint32_t param_count, + zval *cancel_token +); +zend_result king_awaitable_create_method_call( + zval *return_value, + const char *operation, + size_t operation_len, + zval *object, + const char *method_name, + size_t method_name_len, + zval *params, + uint32_t param_count, + zval *cancel_token +); + +PHP_FUNCTION(king_await); +PHP_FUNCTION(king_awaitable_poll); +PHP_FUNCTION(king_awaitable_cancel); +PHP_FUNCTION(king_awaitable_status); +PHP_FUNCTION(king_awaitable_any); +PHP_FUNCTION(king_awaitable_all); + +#endif /* KING_AWAITABLE_H */ diff --git a/extension/include/awaitable/cancel_token.h b/extension/include/awaitable/cancel_token.h new file mode 100644 index 000000000..c3def1c0c --- /dev/null +++ b/extension/include/awaitable/cancel_token.h @@ -0,0 +1,24 @@ +/* + * include/awaitable/cancel_token.h - PHP-visible CancelToken object contract + */ + +#ifndef KING_CANCEL_TOKEN_H +#define KING_CANCEL_TOKEN_H + +#include +#include +#include + +typedef struct _king_cancel_token_object { + bool cancelled; + zend_object std; +} king_cancel_token_object; + +static inline king_cancel_token_object * +php_king_cancel_token_obj_from_zend(zend_object *obj) +{ + return (king_cancel_token_object *) + ((char*)obj - XtOffsetOf(king_cancel_token_object, std)); +} + +#endif /* KING_CANCEL_TOKEN_H */ diff --git a/extension/include/awaitable/cancel_token_arginfo.h b/extension/include/awaitable/cancel_token_arginfo.h new file mode 100644 index 000000000..7a1520af5 --- /dev/null +++ b/extension/include/awaitable/cancel_token_arginfo.h @@ -0,0 +1,16 @@ +/* + * include/awaitable/cancel_token_arginfo.h - King\CancelToken OO arginfo + */ + +#ifndef KING_AWAITABLE_CANCEL_TOKEN_ARGINFO_H +#define KING_AWAITABLE_CANCEL_TOKEN_ARGINFO_H + +#include + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_CancelToken_cancel, 0, 0, IS_VOID, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_CancelToken_isCancelled, 0, 0, _IS_BOOL, 0) +ZEND_END_ARG_INFO() + +#endif /* KING_AWAITABLE_CANCEL_TOKEN_ARGINFO_H */ diff --git a/extension/include/awaitable/class_entries.h b/extension/include/awaitable/class_entries.h new file mode 100644 index 000000000..f7c0ac7c2 --- /dev/null +++ b/extension/include/awaitable/class_entries.h @@ -0,0 +1,13 @@ +/* + * include/awaitable/class_entries.h - Awaitable class-entry externs + */ + +#ifndef KING_AWAITABLE_CLASS_ENTRIES_H +#define KING_AWAITABLE_CLASS_ENTRIES_H + +#include + +extern zend_class_entry *king_ce_cancel_token; +extern zend_class_entry *king_ce_awaitable; + +#endif /* KING_AWAITABLE_CLASS_ENTRIES_H */ diff --git a/extension/include/awaitable/class_method_entries.h b/extension/include/awaitable/class_method_entries.h new file mode 100644 index 000000000..b6d7e3818 --- /dev/null +++ b/extension/include/awaitable/class_method_entries.h @@ -0,0 +1,20 @@ +const zend_function_entry king_cancel_token_class_methods[] = { + PHP_ME(King_CancelToken, cancel, arginfo_class_King_CancelToken_cancel, ZEND_ACC_PUBLIC) + PHP_ME(King_CancelToken, isCancelled, arginfo_class_King_CancelToken_isCancelled, ZEND_ACC_PUBLIC) + PHP_FE_END +}; + +const zend_function_entry king_awaitable_class_methods[] = { + PHP_ME(King_Awaitable, __construct, arginfo_class_King_Awaitable___construct, ZEND_ACC_PRIVATE | ZEND_ACC_CTOR) + PHP_ME(King_Awaitable, await, arginfo_class_King_Awaitable_await, ZEND_ACC_PUBLIC) + PHP_ME(King_Awaitable, poll, arginfo_class_King_Awaitable_poll, ZEND_ACC_PUBLIC) + PHP_ME(King_Awaitable, cancel, arginfo_class_King_Awaitable_cancel, ZEND_ACC_PUBLIC) + PHP_ME(King_Awaitable, isPending, arginfo_class_King_Awaitable_isPending, ZEND_ACC_PUBLIC) + PHP_ME(King_Awaitable, isDone, arginfo_class_King_Awaitable_isDone, ZEND_ACC_PUBLIC) + PHP_ME(King_Awaitable, isCancelled, arginfo_class_King_Awaitable_isCancelled, ZEND_ACC_PUBLIC) + PHP_ME(King_Awaitable, getStatus, arginfo_class_King_Awaitable_getStatus, ZEND_ACC_PUBLIC) + PHP_ME(King_Awaitable, getOperation, arginfo_class_King_Awaitable_getOperation, ZEND_ACC_PUBLIC) + PHP_ME(King_Awaitable, any, arginfo_class_King_Awaitable_any, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + PHP_ME(King_Awaitable, all, arginfo_class_King_Awaitable_all, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + PHP_FE_END +}; diff --git a/extension/include/awaitable/class_methods.h b/extension/include/awaitable/class_methods.h new file mode 100644 index 000000000..1786026c4 --- /dev/null +++ b/extension/include/awaitable/class_methods.h @@ -0,0 +1,13 @@ +/* + * include/awaitable/class_methods.h - Awaitable OO method table externs + */ + +#ifndef KING_AWAITABLE_CLASS_METHODS_H +#define KING_AWAITABLE_CLASS_METHODS_H + +#include + +extern const zend_function_entry king_cancel_token_class_methods[]; +extern const zend_function_entry king_awaitable_class_methods[]; + +#endif /* KING_AWAITABLE_CLASS_METHODS_H */ diff --git a/extension/include/awaitable/function_entries.h b/extension/include/awaitable/function_entries.h new file mode 100644 index 000000000..7a3e18a9b --- /dev/null +++ b/extension/include/awaitable/function_entries.h @@ -0,0 +1,6 @@ + PHP_FE(king_await, arginfo_king_await) + PHP_FE(king_awaitable_poll, arginfo_king_awaitable_poll) + PHP_FE(king_awaitable_cancel, arginfo_king_awaitable_cancel) + PHP_FE(king_awaitable_status, arginfo_king_awaitable_status) + PHP_FE(king_awaitable_any, arginfo_king_awaitable_any) + PHP_FE(king_awaitable_all, arginfo_king_awaitable_all) diff --git a/extension/include/awaitable/index.h b/extension/include/awaitable/index.h new file mode 100644 index 000000000..a59d8c8cd --- /dev/null +++ b/extension/include/awaitable/index.h @@ -0,0 +1,19 @@ +/* + * ========================================================================= + * FILENAME: include/awaitable/index.h + * PROJECT: king + * + * PURPOSE: + * Public umbrella header for the Awaitable subsystem. + * ========================================================================= + */ + +#ifndef KING_AWAITABLE_INDEX_H +#define KING_AWAITABLE_INDEX_H + +#include "awaitable.h" +#include "cancel_token.h" +#include "class_entries.h" +#include "registration.h" + +#endif /* KING_AWAITABLE_INDEX_H */ diff --git a/extension/include/awaitable/registration.h b/extension/include/awaitable/registration.h new file mode 100644 index 000000000..6f7978d70 --- /dev/null +++ b/extension/include/awaitable/registration.h @@ -0,0 +1,14 @@ +/* + * include/awaitable/registration.h - Awaitable MINIT registration hooks + */ + +#ifndef KING_AWAITABLE_REGISTRATION_H +#define KING_AWAITABLE_REGISTRATION_H + +#include +#include "class_methods.h" + +void king_awaitable_register_classes(void); +void king_awaitable_init_object_handlers(void); + +#endif /* KING_AWAITABLE_REGISTRATION_H */ diff --git a/extension/include/client/arginfo/arginfo.h b/extension/include/client/arginfo/arginfo.h new file mode 100644 index 000000000..a280ce8b0 --- /dev/null +++ b/extension/include/client/arginfo/arginfo.h @@ -0,0 +1,141 @@ +/* + * Arginfo for the procedural client/session/TLS/request/WebSocket surface. + * The declarations are consumed through include/client/arginfo/index.h. + */ + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_connect, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, host, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, port, IS_LONG, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, config, IS_MIXED, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_session_resource, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, session, IS_MIXED, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_poll, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, session, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 0, "0") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_cancel_stream, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, stream_id, IS_LONG, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, how, IS_STRING, 0, "\"both\"") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, session, IS_MIXED, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_set_ca_file, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, path, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_set_client_cert, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, cert, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_session_ticket_export, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, session, IS_MIXED, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_session_ticket_import, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, session, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, ticket, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_request_send, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, url, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, method, IS_STRING, 1, "\"GET\"") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, headers, IS_ARRAY, 1, "null") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, body, IS_MIXED, 1, "null") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_king_request_send_async, 0, 1, King\\Awaitable, 0) + ZEND_ARG_TYPE_INFO(0, url, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, method, IS_STRING, 1, "\"GET\"") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, headers, IS_ARRAY, 1, "null") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, body, IS_MIXED, 1, "null") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_http2_request_send_multi, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, requests, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_king_http2_request_send_multi_async, 0, 1, King\\Awaitable, 0) + ZEND_ARG_TYPE_INFO(0, requests, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_http3_request_send_multi, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, requests, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_king_http3_request_send_multi_async, 0, 1, King\\Awaitable, 0) + ZEND_ARG_TYPE_INFO(0, requests, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_receive_response, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, request_context, IS_MIXED, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_early_hints_process, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, request_context, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, headers, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_request_context_resource, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, request_context, IS_MIXED, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_websocket_connect, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, url, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, headers, IS_ARRAY, 1, "null") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_king_websocket_connect_async, 0, 1, King\\Awaitable, 0) + ZEND_ARG_TYPE_INFO(0, url, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, headers, IS_ARRAY, 1, "null") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_websocket_send, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, websocket, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, data, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, is_binary, _IS_BOOL, 0, "false") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_king_websocket_send_async, 0, 2, King\\Awaitable, 0) + ZEND_ARG_TYPE_INFO(0, websocket, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, data, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, is_binary, _IS_BOOL, 0, "false") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_websocket_receive, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, websocket, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 0, "-1") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_king_websocket_receive_async, 0, 1, King\\Awaitable, 0) + ZEND_ARG_TYPE_INFO(0, websocket, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 0, "-1") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_websocket_ping, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, websocket, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, payload, IS_STRING, 0, "\"\"") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_websocket_status, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, websocket, IS_MIXED, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_websocket_close, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, websocket, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, status_code, IS_LONG, 0, "1000") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, reason, IS_STRING, 0, "\"\"") +ZEND_END_ARG_INFO() diff --git a/extension/include/client/arginfo/index.h b/extension/include/client/arginfo/index.h old mode 100755 new mode 100644 index 9132d6ff6..ad0790ec5 --- a/extension/include/client/arginfo/index.h +++ b/extension/include/client/arginfo/index.h @@ -4,15 +4,13 @@ * PROJECT: king * * PURPOSE: - * Reserved arginfo aggregation point for client-side PHP entry points. - * - * The current runtime relies on shared arginfo emitted through the central - * php_king arginfo table, so this header intentionally carries no - * declarations yet. + * Arginfo aggregation point for client-side PHP entry points. * ========================================================================= */ #ifndef KING_CLIENT_ARGINFO_INDEX_H #define KING_CLIENT_ARGINFO_INDEX_H +#include "arginfo.h" + #endif /* KING_CLIENT_ARGINFO_INDEX_H */ diff --git a/extension/include/client/cancel.h b/extension/include/client/cancel.h old mode 100755 new mode 100644 diff --git a/extension/include/client/class_entries.h b/extension/include/client/class_entries.h new file mode 100644 index 000000000..688142299 --- /dev/null +++ b/extension/include/client/class_entries.h @@ -0,0 +1,24 @@ +/* + * include/client/class_entries.h - Client and WebSocket class-entry externs + */ + +#ifndef KING_CLIENT_CLASS_ENTRIES_H +#define KING_CLIENT_CLASS_ENTRIES_H + +#include + +extern zend_class_entry *king_ce_session; +extern zend_class_entry *king_ce_stream; +extern zend_class_entry *king_ce_response; +extern zend_class_entry *king_ce_client_http; +extern zend_class_entry *king_ce_client_http1; +extern zend_class_entry *king_ce_client_http2; +extern zend_class_entry *king_ce_client_http3; +extern zend_class_entry *king_ce_ws_connection; +extern zend_class_entry *king_ce_ws_exception; +extern zend_class_entry *king_ce_ws_connection_error; +extern zend_class_entry *king_ce_ws_protocol_error; +extern zend_class_entry *king_ce_ws_timeout; +extern zend_class_entry *king_ce_ws_closed; + +#endif /* KING_CLIENT_CLASS_ENTRIES_H */ diff --git a/extension/include/client/class_methods.h b/extension/include/client/class_methods.h new file mode 100644 index 000000000..81ff856aa --- /dev/null +++ b/extension/include/client/class_methods.h @@ -0,0 +1,16 @@ +/* + * include/client/class_methods.h - Client/session OO method table externs + */ + +#ifndef KING_CLIENT_CLASS_METHODS_H +#define KING_CLIENT_CLASS_METHODS_H + +#include + +extern const zend_function_entry king_session_class_methods[]; +extern const zend_function_entry king_stream_class_methods[]; +extern const zend_function_entry king_response_class_methods[]; +extern const zend_function_entry king_http_client_class_methods[]; +extern const zend_function_entry king_ws_connection_class_methods[]; + +#endif /* KING_CLIENT_CLASS_METHODS_H */ diff --git a/extension/include/client/early_hints.h b/extension/include/client/early_hints.h old mode 100755 new mode 100644 diff --git a/extension/include/client/function_entries.h b/extension/include/client/function_entries.h new file mode 100644 index 000000000..3eb7a2880 --- /dev/null +++ b/extension/include/client/function_entries.h @@ -0,0 +1,45 @@ + PHP_FE(king_connect, arginfo_king_connect) + PHP_FE(king_close, arginfo_king_session_resource) + PHP_FE(king_send_request, arginfo_king_request_send) + PHP_FE(king_send_request_async, arginfo_king_request_send_async) + PHP_FE(king_receive_response, arginfo_king_receive_response) + PHP_FE(king_poll, arginfo_king_poll) + PHP_FE(king_cancel_stream, arginfo_king_cancel_stream) + PHP_FE(king_export_session_ticket, arginfo_king_session_ticket_export) + PHP_FE(king_import_session_ticket, arginfo_king_session_ticket_import) + PHP_FE(king_set_ca_file, arginfo_king_set_ca_file) + PHP_FE(king_set_client_cert, arginfo_king_set_client_cert) + PHP_FE(king_get_stats, arginfo_king_session_resource) + + PHP_FE(king_client_send_request, arginfo_king_request_send) + PHP_FE(king_client_send_request_async, arginfo_king_request_send_async) + PHP_FE(king_client_stream_cancel, arginfo_king_cancel_stream) + PHP_FE(king_client_tls_set_ca_file, arginfo_king_set_ca_file) + PHP_FE(king_client_tls_set_client_cert, arginfo_king_set_client_cert) + PHP_FE(king_client_tls_export_session_ticket, arginfo_king_session_ticket_export) + PHP_FE(king_client_tls_import_session_ticket, arginfo_king_session_ticket_import) + PHP_FE(king_client_early_hints_get_pending, arginfo_king_request_context_resource) + PHP_FE(king_client_early_hints_process, arginfo_king_early_hints_process) + + PHP_FE(king_http1_request_send, arginfo_king_request_send) + PHP_FE(king_http1_request_send_async, arginfo_king_request_send_async) + PHP_FE(king_http2_request_send, arginfo_king_request_send) + PHP_FE(king_http2_request_send_async, arginfo_king_request_send_async) + PHP_FE(king_http2_request_send_multi, arginfo_king_http2_request_send_multi) + PHP_FE(king_http2_request_send_multi_async, arginfo_king_http2_request_send_multi_async) + PHP_FE(king_http3_request_send, arginfo_king_request_send) + PHP_FE(king_http3_request_send_async, arginfo_king_request_send_async) + PHP_FE(king_http3_request_send_multi, arginfo_king_http3_request_send_multi) + PHP_FE(king_http3_request_send_multi_async, arginfo_king_http3_request_send_multi_async) + + PHP_FE(king_client_websocket_connect, arginfo_king_websocket_connect) + PHP_FE(king_client_websocket_connect_async, arginfo_king_websocket_connect_async) + PHP_FE(king_client_websocket_send, arginfo_king_websocket_send) + PHP_FE(king_client_websocket_send_async, arginfo_king_websocket_send_async) + PHP_FE(king_client_websocket_receive, arginfo_king_websocket_receive) + PHP_FE(king_client_websocket_receive_async, arginfo_king_websocket_receive_async) + PHP_FE(king_client_websocket_ping, arginfo_king_websocket_ping) + PHP_FE(king_client_websocket_get_status, arginfo_king_websocket_status) + PHP_FE(king_client_websocket_get_last_error, arginfo_king_no_args) + PHP_FE(king_client_websocket_close, arginfo_king_websocket_close) + PHP_FE(king_websocket_send, arginfo_king_websocket_send) diff --git a/extension/include/client/http1.h b/extension/include/client/http1.h old mode 100755 new mode 100644 index aa85bc874..8b1928e50 --- a/extension/include/client/http1.h +++ b/extension/include/client/http1.h @@ -33,5 +33,6 @@ zend_result king_http1_request_dispatch( * @return A PHP array on success, FALSE on failure. */ PHP_FUNCTION(king_http1_request_send); +PHP_FUNCTION(king_http1_request_send_async); #endif // KING_CLIENT_HTTP1_H diff --git a/extension/include/client/http2.h b/extension/include/client/http2.h old mode 100755 new mode 100644 index 291d60cfa..bb9a86fb9 --- a/extension/include/client/http2.h +++ b/extension/include/client/http2.h @@ -33,6 +33,8 @@ zend_result king_http2_request_dispatch( * @return A PHP array on success, FALSE on failure. */ PHP_FUNCTION(king_http2_request_send); +PHP_FUNCTION(king_http2_request_send_async); PHP_FUNCTION(king_http2_request_send_multi); +PHP_FUNCTION(king_http2_request_send_multi_async); #endif // KING_CLIENT_HTTP2_H diff --git a/extension/include/client/http3.h b/extension/include/client/http3.h old mode 100755 new mode 100644 index dc38eec1a..ba759dab0 --- a/extension/include/client/http3.h +++ b/extension/include/client/http3.h @@ -34,6 +34,7 @@ zend_result king_http3_request_dispatch( * @return A PHP array on success, FALSE on failure. */ PHP_FUNCTION(king_http3_request_send); +PHP_FUNCTION(king_http3_request_send_async); /** * @brief Sends one multiplexed HTTP/3 batch over one active QUIC connection. @@ -44,5 +45,6 @@ PHP_FUNCTION(king_http3_request_send); * honest HTTP/3 session. */ PHP_FUNCTION(king_http3_request_send_multi); +PHP_FUNCTION(king_http3_request_send_multi_async); #endif // KING_CLIENT_HTTP3_H diff --git a/extension/include/client/index.h b/extension/include/client/index.h old mode 100755 new mode 100644 index 18168a517..8fa38268a --- a/extension/include/client/index.h +++ b/extension/include/client/index.h @@ -2,10 +2,23 @@ #define KING_CLIENT_INDEX_H #include +#include "cancel.h" +#include "class_entries.h" +#include "early_hints.h" +#include "http1.h" +#include "http2.h" +#include "http3.h" +#include "legacy.h" +#include "objects.h" +#include "registration.h" +#include "resource_ids.h" +#include "session.h" +#include "tls.h" +#include "websocket.h" /** * @file extension/include/client/index.h - * @brief Protocol-agnostic client request entry point. + * @brief Public umbrella header and protocol-agnostic client request entry point. * * `king_client_send_request()` is the general client-facing dispatcher. In the * current runtime it defaults to the live HTTP/1 path and can force the active @@ -32,5 +45,6 @@ * protocol-specific `King\Exception` subclass. */ PHP_FUNCTION(king_client_send_request); +PHP_FUNCTION(king_client_send_request_async); #endif // KING_CLIENT_INDEX_H diff --git a/extension/include/client/legacy.h b/extension/include/client/legacy.h new file mode 100644 index 000000000..0aaaeba93 --- /dev/null +++ b/extension/include/client/legacy.h @@ -0,0 +1,27 @@ +/* + * include/client/legacy.h - Unprefixed client compatibility entry points + * ====================================================================== + * + * These declarations keep the original procedural client surface anchored in + * the client subsystem while newer APIs use the explicit king_client_* names. + */ + +#ifndef KING_CLIENT_LEGACY_H +#define KING_CLIENT_LEGACY_H + +#include + +PHP_FUNCTION(king_connect); +PHP_FUNCTION(king_close); +PHP_FUNCTION(king_send_request); +PHP_FUNCTION(king_send_request_async); +PHP_FUNCTION(king_receive_response); +PHP_FUNCTION(king_poll); +PHP_FUNCTION(king_cancel_stream); +PHP_FUNCTION(king_export_session_ticket); +PHP_FUNCTION(king_import_session_ticket); +PHP_FUNCTION(king_set_ca_file); +PHP_FUNCTION(king_set_client_cert); +PHP_FUNCTION(king_get_stats); + +#endif /* KING_CLIENT_LEGACY_H */ diff --git a/extension/include/client/objects.h b/extension/include/client/objects.h new file mode 100644 index 000000000..23a287526 --- /dev/null +++ b/extension/include/client/objects.h @@ -0,0 +1,93 @@ +/* + * include/client/objects.h - PHP-visible client/session object contracts + */ + +#ifndef KING_CLIENT_OBJECTS_H +#define KING_CLIENT_OBJECTS_H + +#include +#include +#include + +typedef struct _king_http1_request_context king_http1_request_context; + +typedef enum _king_client_protocol_preference { + KING_CLIENT_PROTOCOL_AUTO = 0, + KING_CLIENT_PROTOCOL_HTTP1, + KING_CLIENT_PROTOCOL_HTTP2, + KING_CLIENT_PROTOCOL_HTTP3 +} king_client_protocol_preference_t; + +typedef struct _king_response_object { + zval payload; + zval request_context; + zend_long read_offset; + zend_object std; +} king_response_object; + +typedef struct _king_http_client_object { + zval config; + king_client_protocol_preference_t preferred_protocol; + bool closed; + zend_object std; +} king_http_client_object; + +typedef struct _king_session_object { + zval resource; + zval config; + zend_object std; +} king_session_object; + +typedef struct _king_stream_object { + zval session; + zval cancel_token; + zval connection_config; + zval request_headers; + zend_string *request_method; + zend_string *request_path; + zend_string *request_body; + zend_long stream_id; + zend_long buffered_bytes; + bool request_body_was_supplied; + bool finished; + bool closed; + bool response_started; + zend_object std; +} king_stream_object; + +static inline king_session_object * +php_king_obj_from_zend(zend_object *obj) +{ + return (king_session_object *) + ((char*)obj - XtOffsetOf(king_session_object, std)); +} + +static inline king_stream_object * +php_king_stream_obj_from_zend(zend_object *obj) +{ + return (king_stream_object *) + ((char*)obj - XtOffsetOf(king_stream_object, std)); +} + +static inline king_response_object * +php_king_response_obj_from_zend(zend_object *obj) +{ + return (king_response_object *) + ((char*)obj - XtOffsetOf(king_response_object, std)); +} + +static inline king_http_client_object * +php_king_http_client_obj_from_zend(zend_object *obj) +{ + return (king_http_client_object *) + ((char*)obj - XtOffsetOf(king_http_client_object, std)); +} + +zend_result king_response_object_init_from_array(zval *target, zval *payload); +zend_result king_response_object_init_from_context( + zval *target, + zval *payload, + zval *request_context +); + +#endif /* KING_CLIENT_OBJECTS_H */ diff --git a/extension/include/client/objects_arginfo.h b/extension/include/client/objects_arginfo.h new file mode 100644 index 000000000..669c6c276 --- /dev/null +++ b/extension/include/client/objects_arginfo.h @@ -0,0 +1,49 @@ +/* + * include/client/objects_arginfo.h - King\Response and King\Client\HttpClient OO arginfo + */ + +#ifndef KING_CLIENT_OBJECTS_ARGINFO_H +#define KING_CLIENT_OBJECTS_ARGINFO_H + +#include + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Response_getStatusCode_ret, 0, 0, IS_LONG, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Response_getHeaders, 0, 0, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Response_getBody, 0, 0, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Response_read, 0, 0, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, length, IS_LONG, 0, "8192") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Response_isEndOfBody, 0, 0, _IS_BOOL, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_Client_HttpClient___construct, 0, 0, 0) + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, config, King\\Config, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_King_Client_HttpClient_request, 0, 2, King\\Response, 0) + ZEND_ARG_TYPE_INFO(0, method, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, url, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, headers, IS_ARRAY, 0, "[]") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, body, IS_STRING, 0, "\"\"") + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, cancel, King\\CancelToken, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_King_Client_HttpClient_requestAsync, 0, 2, King\\Awaitable, 0) + ZEND_ARG_TYPE_INFO(0, method, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, url, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, headers, IS_ARRAY, 0, "[]") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, body, IS_STRING, 0, "\"\"") + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, cancel, King\\CancelToken, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Client_HttpClient_close, 0, 0, IS_VOID, 0) +ZEND_END_ARG_INFO() + +#endif /* KING_CLIENT_OBJECTS_ARGINFO_H */ diff --git a/extension/include/client/objects_class_method_entries.h b/extension/include/client/objects_class_method_entries.h new file mode 100644 index 000000000..032ffada8 --- /dev/null +++ b/extension/include/client/objects_class_method_entries.h @@ -0,0 +1,16 @@ +const zend_function_entry king_response_class_methods[] = { + PHP_ME(King_Response, getStatusCode, arginfo_class_King_Response_getStatusCode_ret, ZEND_ACC_PUBLIC) + PHP_ME(King_Response, getHeaders, arginfo_class_King_Response_getHeaders, ZEND_ACC_PUBLIC) + PHP_ME(King_Response, getBody, arginfo_class_King_Response_getBody, ZEND_ACC_PUBLIC) + PHP_ME(King_Response, read, arginfo_class_King_Response_read, ZEND_ACC_PUBLIC) + PHP_ME(King_Response, isEndOfBody, arginfo_class_King_Response_isEndOfBody, ZEND_ACC_PUBLIC) + PHP_FE_END +}; + +const zend_function_entry king_http_client_class_methods[] = { + PHP_ME(King_Client_HttpClient, __construct, arginfo_class_King_Client_HttpClient___construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) + PHP_ME(King_Client_HttpClient, request, arginfo_class_King_Client_HttpClient_request, ZEND_ACC_PUBLIC) + PHP_ME(King_Client_HttpClient, requestAsync, arginfo_class_King_Client_HttpClient_requestAsync, ZEND_ACC_PUBLIC) + PHP_ME(King_Client_HttpClient, close, arginfo_class_King_Client_HttpClient_close, ZEND_ACC_PUBLIC) + PHP_FE_END +}; diff --git a/extension/include/client/registration.h b/extension/include/client/registration.h new file mode 100644 index 000000000..940f3e014 --- /dev/null +++ b/extension/include/client/registration.h @@ -0,0 +1,19 @@ +/* + * include/client/registration.h - Client and session MINIT hooks + */ + +#ifndef KING_CLIENT_REGISTRATION_H +#define KING_CLIENT_REGISTRATION_H + +#include +#include "class_methods.h" + +void king_client_register_resource_types(int module_number); +void king_client_register_websocket_exception_classes(void); +void king_client_register_session_classes(void); +void king_client_register_http_classes(void); +void king_client_register_websocket_classes(void); +void king_client_init_object_handlers(void); +void king_client_init_session_object_handlers(void); + +#endif /* KING_CLIENT_REGISTRATION_H */ diff --git a/extension/include/client/resource_ids.h b/extension/include/client/resource_ids.h new file mode 100644 index 000000000..1a46e4619 --- /dev/null +++ b/extension/include/client/resource_ids.h @@ -0,0 +1,12 @@ +/* + * include/client/resource_ids.h - Client Zend resource-id externs + */ + +#ifndef KING_CLIENT_RESOURCE_IDS_H +#define KING_CLIENT_RESOURCE_IDS_H + +extern int le_king_session; +extern int le_king_ws; +extern int le_king_request_context; + +#endif /* KING_CLIENT_RESOURCE_IDS_H */ diff --git a/extension/include/client/session.h b/extension/include/client/session.h old mode 100755 new mode 100644 diff --git a/extension/include/client/session_arginfo.h b/extension/include/client/session_arginfo.h new file mode 100644 index 000000000..fa5ee94e8 --- /dev/null +++ b/extension/include/client/session_arginfo.h @@ -0,0 +1,67 @@ +/* + * include/client/session_arginfo.h - King\Session and King\Stream OO arginfo + */ + +#ifndef KING_CLIENT_SESSION_ARGINFO_H +#define KING_CLIENT_SESSION_ARGINFO_H + +#include + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_Session___construct, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, host, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, port, IS_LONG, 0) + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, config, King\\Config, 1, "null") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, connect_options, IS_ARRAY, 0, "[]") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Session_isConnected, 0, 0, _IS_BOOL, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_King_Session_sendRequest, 0, 2, King\\Stream, 0) + ZEND_ARG_TYPE_INFO(0, method, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, path, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, headers, IS_ARRAY, 0, "[]") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, body, IS_STRING, 0, "\"\"") + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, cancel, King\\CancelToken, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Session_poll, 0, 0, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 0, "0") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_Session_close, 0, 0, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, code, IS_LONG, 0, "0") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, reason, IS_STRING, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Session_stats, 0, 0, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Session_alpn, 0, 0, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Session_enableEarlyHints, 0, 0, IS_VOID, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, enable, _IS_BOOL, 0, "true") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_King_Stream_receiveResponse, 0, 0, King\\Response, 1) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 1, "null") + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, cancel, King\\CancelToken, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Stream_send, 0, 1, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, data, IS_STRING, 0) + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, cancel, King\\CancelToken, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Stream_finish, 0, 0, IS_VOID, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, finalData, IS_STRING, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Stream_isClosed, 0, 0, _IS_BOOL, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Stream_close, 0, 0, IS_VOID, 0) +ZEND_END_ARG_INFO() + +#endif /* KING_CLIENT_SESSION_ARGINFO_H */ diff --git a/extension/include/client/session_class_method_entries.h b/extension/include/client/session_class_method_entries.h new file mode 100644 index 000000000..42718d541 --- /dev/null +++ b/extension/include/client/session_class_method_entries.h @@ -0,0 +1,20 @@ +const zend_function_entry king_session_class_methods[] = { + PHP_ME(King_Session, __construct, arginfo_class_King_Session___construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) + PHP_ME(King_Session, isConnected, arginfo_class_King_Session_isConnected, ZEND_ACC_PUBLIC) + PHP_ME(King_Session, sendRequest, arginfo_class_King_Session_sendRequest, ZEND_ACC_PUBLIC) + PHP_ME(King_Session, poll, arginfo_class_King_Session_poll, ZEND_ACC_PUBLIC) + PHP_ME(King_Session, close, arginfo_class_King_Session_close, ZEND_ACC_PUBLIC) + PHP_ME(King_Session, stats, arginfo_class_King_Session_stats, ZEND_ACC_PUBLIC) + PHP_ME(King_Session, alpn, arginfo_class_King_Session_alpn, ZEND_ACC_PUBLIC) + PHP_ME(King_Session, enableEarlyHints, arginfo_class_King_Session_enableEarlyHints, ZEND_ACC_PUBLIC) + PHP_FE_END +}; + +const zend_function_entry king_stream_class_methods[] = { + PHP_ME(King_Stream, receiveResponse, arginfo_class_King_Stream_receiveResponse, ZEND_ACC_PUBLIC) + PHP_ME(King_Stream, send, arginfo_class_King_Stream_send, ZEND_ACC_PUBLIC) + PHP_ME(King_Stream, finish, arginfo_class_King_Stream_finish, ZEND_ACC_PUBLIC) + PHP_ME(King_Stream, isClosed, arginfo_class_King_Stream_isClosed, ZEND_ACC_PUBLIC) + PHP_ME(King_Stream, close, arginfo_class_King_Stream_close, ZEND_ACC_PUBLIC) + PHP_FE_END +}; diff --git a/extension/include/client/tls.h b/extension/include/client/tls.h old mode 100755 new mode 100644 diff --git a/extension/include/client/websocket.h b/extension/include/client/websocket.h old mode 100755 new mode 100644 index cd8d8a56d..830dd04f7 --- a/extension/include/client/websocket.h +++ b/extension/include/client/websocket.h @@ -2,8 +2,68 @@ #define KING_CLIENT_WEBSOCKET_H #include +#include
+#include +#include -typedef struct _king_ws_state king_ws_state; +typedef struct _king_ws_server_object king_ws_server_object; + +typedef enum _king_ws_connection_state { + KING_WS_STATE_CONNECTING = 0, + KING_WS_STATE_OPEN = 1, + KING_WS_STATE_CLOSING = 2, + KING_WS_STATE_CLOSED = 3 +} king_ws_connection_state_t; + +typedef struct _king_ws_message { + zend_string *payload; + bool is_binary; + struct _king_ws_message *next; +} king_ws_message; + +typedef struct _king_ws_state { + zend_string *url; + zend_string *connection_id; + zend_string *scheme; + zend_string *host; + zend_string *request_target; + php_stream *transport_stream; + zval config; + zval headers; + zend_long port; + zend_long max_payload_size; + zend_long max_queued_messages; + zend_long max_queued_bytes; + zend_long queued_message_count; + zend_long queued_bytes; + zend_long ping_interval_ms; + zend_long handshake_timeout_ms; + zend_long last_close_status_code; + king_ws_connection_state_t state; + king_ws_message *incoming_head; + king_ws_message *incoming_tail; + zend_string *last_close_reason; + zend_string *last_ping_payload; + bool secure; + bool server_endpoint; + bool server_local_only; + bool handshake_complete; + bool close_frame_sent; + bool closed; + king_ws_server_object *server_owner; +} king_ws_state; + +typedef struct _king_ws_object { + zval resource; + zend_object std; +} king_ws_object; + +static inline king_ws_object * +php_king_ws_obj_from_zend(zend_object *obj) +{ + return (king_ws_object *) + ((char*)obj - XtOffsetOf(king_ws_object, std)); +} /** * @file extension/include/client/websocket.h @@ -26,6 +86,7 @@ typedef struct _king_ws_state king_ws_state; * @return A `King\WebSocket` resource on success, FALSE on failure. */ PHP_FUNCTION(king_client_websocket_connect); +PHP_FUNCTION(king_client_websocket_connect_async); /** * @brief Sends a WebSocket message. @@ -37,6 +98,7 @@ PHP_FUNCTION(king_client_websocket_connect); * @return TRUE on success, FALSE on failure. */ PHP_FUNCTION(king_client_websocket_send); +PHP_FUNCTION(king_client_websocket_send_async); /** * @brief Receives a WebSocket message. @@ -48,6 +110,7 @@ PHP_FUNCTION(king_client_websocket_send); * empty and the connection remains open, or FALSE on close or error. */ PHP_FUNCTION(king_client_websocket_receive); +PHP_FUNCTION(king_client_websocket_receive_async); /** * @brief Sends a WebSocket PING frame. @@ -136,5 +199,10 @@ void king_websocket_state_build_info_array( king_ws_state *state ); +void king_ws_server_registry_detach( + king_ws_server_object *server, + king_ws_state *state +); +void king_ws_state_free(king_ws_state *state); #endif // KING_CLIENT_WEBSOCKET_H diff --git a/extension/include/client/websocket_arginfo.h b/extension/include/client/websocket_arginfo.h new file mode 100644 index 000000000..72afa65f5 --- /dev/null +++ b/extension/include/client/websocket_arginfo.h @@ -0,0 +1,52 @@ +/* + * include/client/websocket_arginfo.h - King\WebSocket\Connection OO arginfo + */ + +#ifndef KING_CLIENT_WEBSOCKET_ARGINFO_H +#define KING_CLIENT_WEBSOCKET_ARGINFO_H + +#include + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_WebSocket_Connection___construct, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, url, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, headers, IS_ARRAY, 1, "null") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_WebSocket_Connection_send, 0, 1, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, message, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_King_WebSocket_Connection_sendAsync, 0, 1, King\\Awaitable, 0) + ZEND_ARG_TYPE_INFO(0, message, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_WebSocket_Connection_sendBinary, 0, 1, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, payload, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_King_WebSocket_Connection_sendBinaryAsync, 0, 1, King\\Awaitable, 0) + ZEND_ARG_TYPE_INFO(0, payload, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_WebSocket_Connection_receive, 0, 0, IS_STRING, 1) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_King_WebSocket_Connection_receiveAsync, 0, 0, King\\Awaitable, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_WebSocket_Connection_ping, 0, 0, IS_VOID, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, data, IS_STRING, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_WebSocket_Connection_close, 0, 0, IS_VOID, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, code, IS_LONG, 0, "1000") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, reason, IS_STRING, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_WebSocket_Connection_getInfo, 0, 0, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +#endif /* KING_CLIENT_WEBSOCKET_ARGINFO_H */ diff --git a/extension/include/client/websocket_class_method_entries.h b/extension/include/client/websocket_class_method_entries.h new file mode 100644 index 000000000..32deb80aa --- /dev/null +++ b/extension/include/client/websocket_class_method_entries.h @@ -0,0 +1,13 @@ +const zend_function_entry king_ws_connection_class_methods[] = { + PHP_ME(King_WebSocket_Connection, __construct, arginfo_class_King_WebSocket_Connection___construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) + PHP_ME(King_WebSocket_Connection, send, arginfo_class_King_WebSocket_Connection_send, ZEND_ACC_PUBLIC) + PHP_ME(King_WebSocket_Connection, sendAsync, arginfo_class_King_WebSocket_Connection_sendAsync, ZEND_ACC_PUBLIC) + PHP_ME(King_WebSocket_Connection, sendBinary, arginfo_class_King_WebSocket_Connection_sendBinary, ZEND_ACC_PUBLIC) + PHP_ME(King_WebSocket_Connection, sendBinaryAsync, arginfo_class_King_WebSocket_Connection_sendBinaryAsync, ZEND_ACC_PUBLIC) + PHP_ME(King_WebSocket_Connection, receive, arginfo_class_King_WebSocket_Connection_receive, ZEND_ACC_PUBLIC) + PHP_ME(King_WebSocket_Connection, receiveAsync, arginfo_class_King_WebSocket_Connection_receiveAsync, ZEND_ACC_PUBLIC) + PHP_ME(King_WebSocket_Connection, ping, arginfo_class_King_WebSocket_Connection_ping, ZEND_ACC_PUBLIC) + PHP_ME(King_WebSocket_Connection, close, arginfo_class_King_WebSocket_Connection_close, ZEND_ACC_PUBLIC) + PHP_ME(King_WebSocket_Connection, getInfo, arginfo_class_King_WebSocket_Connection_getInfo, ZEND_ACC_PUBLIC) + PHP_FE_END +}; diff --git a/extension/include/config/app_http3_websockets_webtransport/base_layer.h b/extension/include/config/app_http3_websockets_webtransport/base_layer.h old mode 100755 new mode 100644 diff --git a/extension/include/config/app_http3_websockets_webtransport/config.h b/extension/include/config/app_http3_websockets_webtransport/config.h old mode 100755 new mode 100644 diff --git a/extension/include/config/app_http3_websockets_webtransport/default.h b/extension/include/config/app_http3_websockets_webtransport/default.h old mode 100755 new mode 100644 diff --git a/extension/include/config/app_http3_websockets_webtransport/index.h b/extension/include/config/app_http3_websockets_webtransport/index.h old mode 100755 new mode 100644 diff --git a/extension/include/config/app_http3_websockets_webtransport/ini.h b/extension/include/config/app_http3_websockets_webtransport/ini.h old mode 100755 new mode 100644 diff --git a/extension/include/config/bare_metal_tuning/base_layer.h b/extension/include/config/bare_metal_tuning/base_layer.h old mode 100755 new mode 100644 diff --git a/extension/include/config/bare_metal_tuning/config.h b/extension/include/config/bare_metal_tuning/config.h old mode 100755 new mode 100644 diff --git a/extension/include/config/bare_metal_tuning/default.h b/extension/include/config/bare_metal_tuning/default.h old mode 100755 new mode 100644 diff --git a/extension/include/config/bare_metal_tuning/index.h b/extension/include/config/bare_metal_tuning/index.h old mode 100755 new mode 100644 diff --git a/extension/include/config/bare_metal_tuning/ini.h b/extension/include/config/bare_metal_tuning/ini.h old mode 100755 new mode 100644 diff --git a/extension/include/config/class_entries.h b/extension/include/config/class_entries.h new file mode 100644 index 000000000..705464c36 --- /dev/null +++ b/extension/include/config/class_entries.h @@ -0,0 +1,12 @@ +/* + * include/config/class_entries.h - Config class-entry externs + */ + +#ifndef KING_CONFIG_CLASS_ENTRIES_H +#define KING_CONFIG_CLASS_ENTRIES_H + +#include + +extern zend_class_entry *king_ce_config; + +#endif /* KING_CONFIG_CLASS_ENTRIES_H */ diff --git a/extension/include/config/class_methods.h b/extension/include/config/class_methods.h new file mode 100644 index 000000000..a6995da9e --- /dev/null +++ b/extension/include/config/class_methods.h @@ -0,0 +1,12 @@ +/* + * include/config/class_methods.h - Config OO method table externs + */ + +#ifndef KING_CONFIG_CLASS_METHODS_H +#define KING_CONFIG_CLASS_METHODS_H + +#include + +extern const zend_function_entry king_config_class_methods[]; + +#endif /* KING_CONFIG_CLASS_METHODS_H */ diff --git a/extension/include/config/cloud_autoscale/base_layer.h b/extension/include/config/cloud_autoscale/base_layer.h old mode 100755 new mode 100644 diff --git a/extension/include/config/cloud_autoscale/config.h b/extension/include/config/cloud_autoscale/config.h old mode 100755 new mode 100644 index 93a7dbc21..dbca11d53 --- a/extension/include/config/cloud_autoscale/config.h +++ b/extension/include/config/cloud_autoscale/config.h @@ -14,7 +14,7 @@ #define KING_CONFIG_CLOUD_AUTOSCALE_CONFIG_H #include "php.h" -#include "include/config/cloud_autoscale/base_layer.h" +#include "config/cloud_autoscale/base_layer.h" /** * @brief Applies runtime configuration settings from a PHP array. diff --git a/extension/include/config/cloud_autoscale/default.h b/extension/include/config/cloud_autoscale/default.h old mode 100755 new mode 100644 diff --git a/extension/include/config/cloud_autoscale/index.h b/extension/include/config/cloud_autoscale/index.h old mode 100755 new mode 100644 diff --git a/extension/include/config/cloud_autoscale/ini.h b/extension/include/config/cloud_autoscale/ini.h old mode 100755 new mode 100644 diff --git a/extension/include/config/cluster_and_process/base_layer.h b/extension/include/config/cluster_and_process/base_layer.h old mode 100755 new mode 100644 diff --git a/extension/include/config/cluster_and_process/config.h b/extension/include/config/cluster_and_process/config.h old mode 100755 new mode 100644 diff --git a/extension/include/config/cluster_and_process/default.h b/extension/include/config/cluster_and_process/default.h old mode 100755 new mode 100644 diff --git a/extension/include/config/cluster_and_process/index.h b/extension/include/config/cluster_and_process/index.h old mode 100755 new mode 100644 diff --git a/extension/include/config/cluster_and_process/ini.h b/extension/include/config/cluster_and_process/ini.h old mode 100755 new mode 100644 diff --git a/extension/include/config/config.h b/extension/include/config/config.h old mode 100755 new mode 100644 index dc61d54b4..38cb0a7b2 --- a/extension/include/config/config.h +++ b/extension/include/config/config.h @@ -40,6 +40,8 @@ #include +#include "config/object.h" + typedef void king_quic_backend_config; /* @@ -50,50 +52,50 @@ typedef void king_quic_backend_config; * The `index.h` headers provide the module lifecycle hooks. */ -#include "include/config/app_http3_websockets_webtransport/base_layer.h" -#include "include/config/app_http3_websockets_webtransport/index.h" -#include "include/config/bare_metal_tuning/base_layer.h" -#include "include/config/bare_metal_tuning/index.h" -#include "include/config/cloud_autoscale/base_layer.h" -#include "include/config/cloud_autoscale/index.h" -#include "include/config/cluster_and_process/base_layer.h" -#include "include/config/cluster_and_process/index.h" -#include "include/config/dynamic_admin_api/base_layer.h" -#include "include/config/dynamic_admin_api/index.h" -#include "include/config/high_perf_compute_and_ai/base_layer.h" -#include "include/config/high_perf_compute_and_ai/index.h" -#include "include/config/http2/base_layer.h" -#include "include/config/http2/index.h" -#include "include/config/iibin/base_layer.h" -#include "include/config/iibin/index.h" -#include "include/config/mcp_and_orchestrator/base_layer.h" -#include "include/config/mcp_and_orchestrator/index.h" -#include "include/config/native_cdn/base_layer.h" -#include "include/config/native_cdn/index.h" -#include "include/config/native_object_store/base_layer.h" -#include "include/config/native_object_store/index.h" -#include "include/config/open_telemetry/base_layer.h" -#include "include/config/open_telemetry/index.h" -#include "include/config/quic_transport/base_layer.h" -#include "include/config/quic_transport/index.h" -#include "include/config/router_and_loadbalancer/base_layer.h" -#include "include/config/router_and_loadbalancer/index.h" -#include "include/config/security_and_traffic/base_layer.h" -#include "include/config/security_and_traffic/index.h" -#include "include/config/semantic_geometry/base_layer.h" -#include "include/config/semantic_geometry/index.h" -#include "include/config/smart_contracts/base_layer.h" -#include "include/config/smart_contracts/index.h" -#include "include/config/smart_dns/base_layer.h" -#include "include/config/smart_dns/index.h" -#include "include/config/ssh_over_quic/base_layer.h" -#include "include/config/ssh_over_quic/index.h" -#include "include/config/state_management/base_layer.h" -#include "include/config/state_management/index.h" -#include "include/config/tcp_transport/base_layer.h" -#include "include/config/tcp_transport/index.h" -#include "include/config/tls_and_crypto/base_layer.h" -#include "include/config/tls_and_crypto/index.h" +#include "config/app_http3_websockets_webtransport/base_layer.h" +#include "config/app_http3_websockets_webtransport/index.h" +#include "config/bare_metal_tuning/base_layer.h" +#include "config/bare_metal_tuning/index.h" +#include "config/cloud_autoscale/base_layer.h" +#include "config/cloud_autoscale/index.h" +#include "config/cluster_and_process/base_layer.h" +#include "config/cluster_and_process/index.h" +#include "config/dynamic_admin_api/base_layer.h" +#include "config/dynamic_admin_api/index.h" +#include "config/high_perf_compute_and_ai/base_layer.h" +#include "config/high_perf_compute_and_ai/index.h" +#include "config/http2/base_layer.h" +#include "config/http2/index.h" +#include "config/iibin/base_layer.h" +#include "config/iibin/index.h" +#include "config/mcp_and_orchestrator/base_layer.h" +#include "config/mcp_and_orchestrator/index.h" +#include "config/native_cdn/base_layer.h" +#include "config/native_cdn/index.h" +#include "config/native_object_store/base_layer.h" +#include "config/native_object_store/index.h" +#include "config/open_telemetry/base_layer.h" +#include "config/open_telemetry/index.h" +#include "config/quic_transport/base_layer.h" +#include "config/quic_transport/index.h" +#include "config/router_and_loadbalancer/base_layer.h" +#include "config/router_and_loadbalancer/index.h" +#include "config/security_and_traffic/base_layer.h" +#include "config/security_and_traffic/index.h" +#include "config/semantic_geometry/base_layer.h" +#include "config/semantic_geometry/index.h" +#include "config/smart_contracts/base_layer.h" +#include "config/smart_contracts/index.h" +#include "config/smart_dns/base_layer.h" +#include "config/smart_dns/index.h" +#include "config/ssh_over_quic/base_layer.h" +#include "config/ssh_over_quic/index.h" +#include "config/state_management/base_layer.h" +#include "config/state_management/index.h" +#include "config/tcp_transport/base_layer.h" +#include "config/tcp_transport/index.h" +#include "config/tls_and_crypto/base_layer.h" +#include "config/tls_and_crypto/index.h" /** * @brief The master C-level representation of a `King\Config` snapshot. @@ -121,6 +123,7 @@ typedef struct king_cfg_s { zend_bool owns_tcp_strings; zend_bool owns_quic_cc_algorithm; zend_bool owns_mcp_orchestrator_strings; + zend_bool owns_compute_ai_strings; /* Composed configuration modules. */ kg_app_protocols_config_t app_protocols; diff --git a/extension/include/config/dynamic_admin_api/base_layer.h b/extension/include/config/dynamic_admin_api/base_layer.h old mode 100755 new mode 100644 diff --git a/extension/include/config/dynamic_admin_api/config.h b/extension/include/config/dynamic_admin_api/config.h old mode 100755 new mode 100644 diff --git a/extension/include/config/dynamic_admin_api/default.h b/extension/include/config/dynamic_admin_api/default.h old mode 100755 new mode 100644 diff --git a/extension/include/config/dynamic_admin_api/index.h b/extension/include/config/dynamic_admin_api/index.h old mode 100755 new mode 100644 diff --git a/extension/include/config/dynamic_admin_api/ini.h b/extension/include/config/dynamic_admin_api/ini.h old mode 100755 new mode 100644 diff --git a/extension/include/config/high_perf_compute_and_ai/base_layer.h b/extension/include/config/high_perf_compute_and_ai/base_layer.h old mode 100755 new mode 100644 index f57fcb3c3..79bcd5f4f --- a/extension/include/config/high_perf_compute_and_ai/base_layer.h +++ b/extension/include/config/high_perf_compute_and_ai/base_layer.h @@ -24,6 +24,41 @@ typedef struct _kg_high_perf_compute_ai_config_t { bool dataframe_string_interning_enable; zend_long dataframe_cpu_parallelism_default; + bool inference_with_memory; + zend_long inference_context_tokens; + zend_long inference_kv_page_tokens; + zend_long inference_kv_element_bytes; + char *inference_preferred_model_profile; + char *inference_models; + char *inference_cpu_model_name; + char *inference_cpu_model_artifact; + char *inference_gpu_model_name; + char *inference_gpu_model_artifact; + zend_long inference_gpu_max_gpu_layers; + zend_long inference_gpu_vram_reserve_mb; + zend_long inference_gpu_min_free_vram_mb; + bool inference_gpu_allow_system_ram_offload; + zend_long inference_gpu_system_ram_offload_max_mb; + zend_long inference_gpu_system_ram_offload_min_free_mb; + char *inference_gpu_thermal_sensor_path; + char *inference_gpu_thermal_sensor_command; + double inference_gpu_thermal_max_temperature_c; + zend_long inference_gpu_thermal_check_interval_sec; + bool inference_gpu_allow_unmonitored; + char *inference_gpu_power_sensor_command; + double inference_gpu_power_max_watts; + zend_long inference_gpu_power_check_interval_sec; + bool inference_gpu_batch_prefill_experimental_enable; + bool inference_cuda_numeric_compare_enable; + zend_long inference_cuda_numeric_compare_max_values; + bool inference_llm_cache_enable; + char *inference_llm_cache_path; + zend_long inference_llm_cache_min_free_mb; + bool inference_llm_cache_fail_closed; + char *inference_llm_cache_disk_alert_webhook; + char *inference_llm_cache_disk_alert_mcp_service; + char *inference_llm_cache_disk_alert_mcp_method; + /* --- General GPU Configuration --- */ bool gpu_bindings_enable; char *gpu_default_backend; diff --git a/extension/include/config/high_perf_compute_and_ai/config.h b/extension/include/config/high_perf_compute_and_ai/config.h old mode 100755 new mode 100644 index df0be2826..815305aee --- a/extension/include/config/high_perf_compute_and_ai/config.h +++ b/extension/include/config/high_perf_compute_and_ai/config.h @@ -19,6 +19,7 @@ #define KING_CONFIG_HIGH_PERF_COMPUTE_AI_CONFIG_H #include "php.h" +#include "config/high_perf_compute_and_ai/base_layer.h" /** * @brief Applies high-performance compute and AI settings from a PHP array. @@ -29,4 +30,8 @@ */ int kg_config_high_perf_compute_and_ai_apply_userland_config(zval *config_arr); +int kg_config_high_perf_compute_and_ai_apply_userland_config_to( + kg_high_perf_compute_ai_config_t *target, + zval *config_arr); + #endif /* KING_CONFIG_HIGH_PERF_COMPUTE_AI_CONFIG_H */ diff --git a/extension/include/config/high_perf_compute_and_ai/default.h b/extension/include/config/high_perf_compute_and_ai/default.h old mode 100755 new mode 100644 diff --git a/extension/include/config/high_perf_compute_and_ai/index.h b/extension/include/config/high_perf_compute_and_ai/index.h old mode 100755 new mode 100644 diff --git a/extension/include/config/high_perf_compute_and_ai/ini.h b/extension/include/config/high_perf_compute_and_ai/ini.h old mode 100755 new mode 100644 diff --git a/extension/include/config/http2/base_layer.h b/extension/include/config/http2/base_layer.h old mode 100755 new mode 100644 diff --git a/extension/include/config/http2/config.h b/extension/include/config/http2/config.h old mode 100755 new mode 100644 index 8489439b0..43226cb10 --- a/extension/include/config/http2/config.h +++ b/extension/include/config/http2/config.h @@ -21,7 +21,7 @@ #define KING_CONFIG_HTTP2_CONFIG_H #include "php.h" -#include "include/config/http2/base_layer.h" +#include "config/http2/base_layer.h" /** * @brief Applies HTTP/2 settings from a PHP array to the live runtime state. diff --git a/extension/include/config/http2/default.h b/extension/include/config/http2/default.h old mode 100755 new mode 100644 diff --git a/extension/include/config/http2/index.h b/extension/include/config/http2/index.h old mode 100755 new mode 100644 diff --git a/extension/include/config/http2/ini.h b/extension/include/config/http2/ini.h old mode 100755 new mode 100644 diff --git a/extension/include/config/iibin/base_layer.h b/extension/include/config/iibin/base_layer.h old mode 100755 new mode 100644 diff --git a/extension/include/config/iibin/config.h b/extension/include/config/iibin/config.h old mode 100755 new mode 100644 diff --git a/extension/include/config/iibin/default.h b/extension/include/config/iibin/default.h old mode 100755 new mode 100644 diff --git a/extension/include/config/iibin/index.h b/extension/include/config/iibin/index.h old mode 100755 new mode 100644 diff --git a/extension/include/config/iibin/ini.h b/extension/include/config/iibin/ini.h old mode 100755 new mode 100644 diff --git a/extension/include/config/index.h b/extension/include/config/index.h new file mode 100644 index 000000000..3d6e53c32 --- /dev/null +++ b/extension/include/config/index.h @@ -0,0 +1,19 @@ +/* + * include/config/index.h - Public config subsystem umbrella + */ + +#ifndef KING_CONFIG_INDEX_H +#define KING_CONFIG_INDEX_H + +#include "class_entries.h" +#include "class_methods.h" +#include "object.h" +#include "registration.h" +#include "resource_helpers.h" +#include "resource_ids.h" + +#ifndef KING_RUNTIME_BUILD +# include "config.h" +#endif + +#endif /* KING_CONFIG_INDEX_H */ diff --git a/extension/include/config/internal/class_method_entries.h b/extension/include/config/internal/class_method_entries.h new file mode 100644 index 000000000..375d992b3 --- /dev/null +++ b/extension/include/config/internal/class_method_entries.h @@ -0,0 +1,8 @@ +const zend_function_entry king_config_class_methods[] = { + PHP_ME(King_Config, __construct, arginfo_class_King_Config___construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) + PHP_ME(King_Config, new, arginfo_class_King_Config_new, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + PHP_ME(King_Config, get, arginfo_class_King_Config_get, ZEND_ACC_PUBLIC) + PHP_ME(King_Config, set, arginfo_class_King_Config_set, ZEND_ACC_PUBLIC) + PHP_ME(King_Config, toArray, arginfo_class_King_Config_toArray, ZEND_ACC_PUBLIC) + PHP_FE_END +}; diff --git a/extension/include/config/internal/object_arginfo.h b/extension/include/config/internal/object_arginfo.h new file mode 100644 index 000000000..b0674e4f3 --- /dev/null +++ b/extension/include/config/internal/object_arginfo.h @@ -0,0 +1,30 @@ +/* + * include/config/internal/object_arginfo.h - King\Config OO arginfo + */ + +#ifndef KING_CONFIG_INTERNAL_OBJECT_ARGINFO_H +#define KING_CONFIG_INTERNAL_OBJECT_ARGINFO_H + +#include + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_Config___construct, 0, 0, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_King_Config_new, 0, 0, King\\Config, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 0, "[]") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_Config_get, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Config_set, 0, 2, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, value, IS_MIXED, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Config_toArray, 0, 0, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +#endif /* KING_CONFIG_INTERNAL_OBJECT_ARGINFO_H */ diff --git a/extension/include/config/mcp_and_orchestrator/base_layer.h b/extension/include/config/mcp_and_orchestrator/base_layer.h old mode 100755 new mode 100644 diff --git a/extension/include/config/mcp_and_orchestrator/config.h b/extension/include/config/mcp_and_orchestrator/config.h old mode 100755 new mode 100644 index 4ab89ecf1..08578a80c --- a/extension/include/config/mcp_and_orchestrator/config.h +++ b/extension/include/config/mcp_and_orchestrator/config.h @@ -21,7 +21,7 @@ #define KING_CONFIG_MCP_ORCHESTRATOR_CONFIG_H #include "php.h" -#include "include/config/mcp_and_orchestrator/base_layer.h" +#include "config/mcp_and_orchestrator/base_layer.h" /** * @brief Applies MCP/orchestrator settings from a PHP array to live runtime state. diff --git a/extension/include/config/mcp_and_orchestrator/default.h b/extension/include/config/mcp_and_orchestrator/default.h old mode 100755 new mode 100644 diff --git a/extension/include/config/mcp_and_orchestrator/index.h b/extension/include/config/mcp_and_orchestrator/index.h old mode 100755 new mode 100644 diff --git a/extension/include/config/mcp_and_orchestrator/ini.h b/extension/include/config/mcp_and_orchestrator/ini.h old mode 100755 new mode 100644 diff --git a/extension/include/config/native_cdn/base_layer.h b/extension/include/config/native_cdn/base_layer.h old mode 100755 new mode 100644 diff --git a/extension/include/config/native_cdn/config.h b/extension/include/config/native_cdn/config.h old mode 100755 new mode 100644 index 46d29d2bf..97b685cfe --- a/extension/include/config/native_cdn/config.h +++ b/extension/include/config/native_cdn/config.h @@ -20,7 +20,7 @@ #define KING_CONFIG_NATIVE_CDN_CONFIG_H #include "php.h" -#include "include/config/native_cdn/base_layer.h" +#include "config/native_cdn/base_layer.h" /** * @brief Applies native CDN settings from a PHP array to the live runtime state. diff --git a/extension/include/config/native_cdn/default.h b/extension/include/config/native_cdn/default.h old mode 100755 new mode 100644 diff --git a/extension/include/config/native_cdn/index.h b/extension/include/config/native_cdn/index.h old mode 100755 new mode 100644 diff --git a/extension/include/config/native_cdn/ini.h b/extension/include/config/native_cdn/ini.h old mode 100755 new mode 100644 diff --git a/extension/include/config/native_object_store/base_layer.h b/extension/include/config/native_object_store/base_layer.h old mode 100755 new mode 100644 diff --git a/extension/include/config/native_object_store/config.h b/extension/include/config/native_object_store/config.h old mode 100755 new mode 100644 index fb5001440..1ed21b96d --- a/extension/include/config/native_object_store/config.h +++ b/extension/include/config/native_object_store/config.h @@ -21,7 +21,7 @@ #define KING_CONFIG_NATIVE_OBJECT_STORE_CONFIG_H #include "php.h" -#include "include/config/native_object_store/base_layer.h" +#include "config/native_object_store/base_layer.h" /** * @brief Applies native object-store settings from a PHP array to live runtime state. diff --git a/extension/include/config/native_object_store/default.h b/extension/include/config/native_object_store/default.h old mode 100755 new mode 100644 diff --git a/extension/include/config/native_object_store/index.h b/extension/include/config/native_object_store/index.h old mode 100755 new mode 100644 diff --git a/extension/include/config/native_object_store/ini.h b/extension/include/config/native_object_store/ini.h old mode 100755 new mode 100644 diff --git a/extension/include/config/object.h b/extension/include/config/object.h new file mode 100644 index 000000000..0ca1e9a54 --- /dev/null +++ b/extension/include/config/object.h @@ -0,0 +1,24 @@ +/* + * include/config/object.h - PHP-visible Config object contract + */ + +#ifndef KING_CONFIG_OBJECT_H +#define KING_CONFIG_OBJECT_H + +#include +#include + +typedef struct _king_config_object { + zval resource; + zval overrides; + zend_object std; +} king_config_object; + +static inline king_config_object * +php_king_config_obj_from_zend(zend_object *obj) +{ + return (king_config_object *) + ((char*)obj - XtOffsetOf(king_config_object, std)); +} + +#endif /* KING_CONFIG_OBJECT_H */ diff --git a/extension/include/config/open_telemetry/base_layer.h b/extension/include/config/open_telemetry/base_layer.h old mode 100755 new mode 100644 diff --git a/extension/include/config/open_telemetry/config.h b/extension/include/config/open_telemetry/config.h old mode 100755 new mode 100644 index a41f52759..04eeecd89 --- a/extension/include/config/open_telemetry/config.h +++ b/extension/include/config/open_telemetry/config.h @@ -20,7 +20,7 @@ #define KING_CONFIG_OPEN_TELEMETRY_CONFIG_H #include "php.h" -#include "include/config/open_telemetry/base_layer.h" +#include "config/open_telemetry/base_layer.h" /** * @brief Applies OpenTelemetry settings from a PHP array to the live runtime state. diff --git a/extension/include/config/open_telemetry/default.h b/extension/include/config/open_telemetry/default.h old mode 100755 new mode 100644 diff --git a/extension/include/config/open_telemetry/index.h b/extension/include/config/open_telemetry/index.h old mode 100755 new mode 100644 diff --git a/extension/include/config/open_telemetry/ini.h b/extension/include/config/open_telemetry/ini.h old mode 100755 new mode 100644 diff --git a/extension/include/config/quic_transport/base_layer.h b/extension/include/config/quic_transport/base_layer.h old mode 100755 new mode 100644 diff --git a/extension/include/config/quic_transport/config.h b/extension/include/config/quic_transport/config.h old mode 100755 new mode 100644 index 5ddc98195..f5b3d3dc7 --- a/extension/include/config/quic_transport/config.h +++ b/extension/include/config/quic_transport/config.h @@ -21,7 +21,7 @@ #define KING_CONFIG_QUIC_TRANSPORT_CONFIG_H #include "php.h" -#include "include/config/quic_transport/base_layer.h" +#include "config/quic_transport/base_layer.h" /** * @brief Applies QUIC transport settings from a PHP array to the live runtime state. diff --git a/extension/include/config/quic_transport/default.h b/extension/include/config/quic_transport/default.h old mode 100755 new mode 100644 diff --git a/extension/include/config/quic_transport/index.h b/extension/include/config/quic_transport/index.h old mode 100755 new mode 100644 diff --git a/extension/include/config/quic_transport/ini.h b/extension/include/config/quic_transport/ini.h old mode 100755 new mode 100644 diff --git a/extension/include/config/registration.h b/extension/include/config/registration.h new file mode 100644 index 000000000..3b1d3d9b8 --- /dev/null +++ b/extension/include/config/registration.h @@ -0,0 +1,16 @@ +/* + * include/config/registration.h - Config object MINIT hooks + */ + +#ifndef KING_CONFIG_REGISTRATION_H +#define KING_CONFIG_REGISTRATION_H + +#include +#include "class_entries.h" +#include "class_methods.h" + +void king_config_register_resource_types(int module_number); +void king_config_register_classes(void); +void king_config_init_object_handlers(void); + +#endif /* KING_CONFIG_REGISTRATION_H */ diff --git a/extension/include/config/resource_helpers.h b/extension/include/config/resource_helpers.h new file mode 100644 index 000000000..a82777fb1 --- /dev/null +++ b/extension/include/config/resource_helpers.h @@ -0,0 +1,14 @@ +/* + * include/config/resource_helpers.h - Config resource access helpers + */ + +#ifndef KING_CONFIG_RESOURCE_HELPERS_H +#define KING_CONFIG_RESOURCE_HELPERS_H + +#include + +typedef struct king_cfg_s king_cfg_t; + +king_cfg_t *king_fetch_config(zval *zcfg); + +#endif /* KING_CONFIG_RESOURCE_HELPERS_H */ diff --git a/extension/include/config/resource_ids.h b/extension/include/config/resource_ids.h new file mode 100644 index 000000000..8234f7076 --- /dev/null +++ b/extension/include/config/resource_ids.h @@ -0,0 +1,10 @@ +/* + * include/config/resource_ids.h - Config Zend resource-id externs + */ + +#ifndef KING_CONFIG_RESOURCE_IDS_H +#define KING_CONFIG_RESOURCE_IDS_H + +extern int le_king_cfg; + +#endif /* KING_CONFIG_RESOURCE_IDS_H */ diff --git a/extension/include/config/router_and_loadbalancer/base_layer.h b/extension/include/config/router_and_loadbalancer/base_layer.h old mode 100755 new mode 100644 diff --git a/extension/include/config/router_and_loadbalancer/config.h b/extension/include/config/router_and_loadbalancer/config.h old mode 100755 new mode 100644 diff --git a/extension/include/config/router_and_loadbalancer/default.h b/extension/include/config/router_and_loadbalancer/default.h old mode 100755 new mode 100644 diff --git a/extension/include/config/router_and_loadbalancer/index.h b/extension/include/config/router_and_loadbalancer/index.h old mode 100755 new mode 100644 diff --git a/extension/include/config/router_and_loadbalancer/ini.h b/extension/include/config/router_and_loadbalancer/ini.h old mode 100755 new mode 100644 diff --git a/extension/include/config/security_and_traffic/base_layer.h b/extension/include/config/security_and_traffic/base_layer.h old mode 100755 new mode 100644 diff --git a/extension/include/config/security_and_traffic/config.h b/extension/include/config/security_and_traffic/config.h old mode 100755 new mode 100644 diff --git a/extension/include/config/security_and_traffic/default.h b/extension/include/config/security_and_traffic/default.h old mode 100755 new mode 100644 diff --git a/extension/include/config/security_and_traffic/index.h b/extension/include/config/security_and_traffic/index.h old mode 100755 new mode 100644 diff --git a/extension/include/config/security_and_traffic/ini.h b/extension/include/config/security_and_traffic/ini.h old mode 100755 new mode 100644 diff --git a/extension/include/config/semantic_geometry/base_layer.h b/extension/include/config/semantic_geometry/base_layer.h old mode 100755 new mode 100644 diff --git a/extension/include/config/semantic_geometry/config.h b/extension/include/config/semantic_geometry/config.h old mode 100755 new mode 100644 index 2d980b23b..65e6965d4 --- a/extension/include/config/semantic_geometry/config.h +++ b/extension/include/config/semantic_geometry/config.h @@ -21,7 +21,7 @@ #define KING_CONFIG_SEMANTIC_GEOMETRY_CONFIG_H #include "php.h" -#include "include/config/semantic_geometry/base_layer.h" +#include "config/semantic_geometry/base_layer.h" /** * @brief Applies semantic-geometry settings from a PHP array to the live runtime state. diff --git a/extension/include/config/semantic_geometry/default.h b/extension/include/config/semantic_geometry/default.h old mode 100755 new mode 100644 diff --git a/extension/include/config/semantic_geometry/index.h b/extension/include/config/semantic_geometry/index.h old mode 100755 new mode 100644 diff --git a/extension/include/config/semantic_geometry/ini.h b/extension/include/config/semantic_geometry/ini.h old mode 100755 new mode 100644 diff --git a/extension/include/config/smart_contracts/base_layer.h b/extension/include/config/smart_contracts/base_layer.h old mode 100755 new mode 100644 diff --git a/extension/include/config/smart_contracts/config.h b/extension/include/config/smart_contracts/config.h old mode 100755 new mode 100644 index 1bdfa7c10..3ec6347a9 --- a/extension/include/config/smart_contracts/config.h +++ b/extension/include/config/smart_contracts/config.h @@ -21,7 +21,7 @@ #define KING_CONFIG_SMART_CONTRACTS_CONFIG_H #include "php.h" -#include "include/config/smart_contracts/base_layer.h" +#include "config/smart_contracts/base_layer.h" /** * @brief Applies smart-contract settings from a PHP array to the live runtime state. diff --git a/extension/include/config/smart_contracts/default.h b/extension/include/config/smart_contracts/default.h old mode 100755 new mode 100644 diff --git a/extension/include/config/smart_contracts/index.h b/extension/include/config/smart_contracts/index.h old mode 100755 new mode 100644 diff --git a/extension/include/config/smart_contracts/ini.h b/extension/include/config/smart_contracts/ini.h old mode 100755 new mode 100644 diff --git a/extension/include/config/smart_dns/base_layer.h b/extension/include/config/smart_dns/base_layer.h old mode 100755 new mode 100644 index 24ce505e2..f98c7ee38 --- a/extension/include/config/smart_dns/base_layer.h +++ b/extension/include/config/smart_dns/base_layer.h @@ -34,6 +34,7 @@ typedef struct _kg_smart_dns_config_t { bool semantic_mode_enable; char *mothernode_uri; char *live_probe_allowed_hosts; + char *state_path; } kg_smart_dns_config_t; /* Module-global configuration instance. */ diff --git a/extension/include/config/smart_dns/config.h b/extension/include/config/smart_dns/config.h old mode 100755 new mode 100644 index ec3662b9d..a1ec4fb35 --- a/extension/include/config/smart_dns/config.h +++ b/extension/include/config/smart_dns/config.h @@ -24,7 +24,7 @@ #define KING_CONFIG_SMART_DNS_CONFIG_H #include "php.h" -#include "include/config/smart_dns/base_layer.h" +#include "config/smart_dns/base_layer.h" /** * @brief Applies Smart-DNS settings from a PHP array to the live runtime state. diff --git a/extension/include/config/smart_dns/default.h b/extension/include/config/smart_dns/default.h old mode 100755 new mode 100644 diff --git a/extension/include/config/smart_dns/index.h b/extension/include/config/smart_dns/index.h old mode 100755 new mode 100644 diff --git a/extension/include/config/smart_dns/ini.h b/extension/include/config/smart_dns/ini.h old mode 100755 new mode 100644 diff --git a/extension/include/config/ssh_over_quic/base_layer.h b/extension/include/config/ssh_over_quic/base_layer.h old mode 100755 new mode 100644 diff --git a/extension/include/config/ssh_over_quic/config.h b/extension/include/config/ssh_over_quic/config.h old mode 100755 new mode 100644 index f901d0867..85c4ff316 --- a/extension/include/config/ssh_over_quic/config.h +++ b/extension/include/config/ssh_over_quic/config.h @@ -21,7 +21,7 @@ #define KING_CONFIG_SSH_OVER_QUIC_CONFIG_H #include "php.h" -#include "include/config/ssh_over_quic/base_layer.h" +#include "config/ssh_over_quic/base_layer.h" /** * @brief Applies SSH-over-QUIC settings from a PHP array to the live runtime state. diff --git a/extension/include/config/ssh_over_quic/default.h b/extension/include/config/ssh_over_quic/default.h old mode 100755 new mode 100644 diff --git a/extension/include/config/ssh_over_quic/index.h b/extension/include/config/ssh_over_quic/index.h old mode 100755 new mode 100644 diff --git a/extension/include/config/ssh_over_quic/ini.h b/extension/include/config/ssh_over_quic/ini.h old mode 100755 new mode 100644 diff --git a/extension/include/config/state_management/base_layer.h b/extension/include/config/state_management/base_layer.h old mode 100755 new mode 100644 diff --git a/extension/include/config/state_management/config.h b/extension/include/config/state_management/config.h old mode 100755 new mode 100644 diff --git a/extension/include/config/state_management/default.h b/extension/include/config/state_management/default.h old mode 100755 new mode 100644 diff --git a/extension/include/config/state_management/ini.h b/extension/include/config/state_management/ini.h old mode 100755 new mode 100644 diff --git a/extension/include/config/tcp_transport/base_layer.h b/extension/include/config/tcp_transport/base_layer.h old mode 100755 new mode 100644 diff --git a/extension/include/config/tcp_transport/config.h b/extension/include/config/tcp_transport/config.h old mode 100755 new mode 100644 index 024c3e75a..81824c5da --- a/extension/include/config/tcp_transport/config.h +++ b/extension/include/config/tcp_transport/config.h @@ -21,7 +21,7 @@ #define KING_CONFIG_TCP_TRANSPORT_CONFIG_H #include "php.h" -#include "include/config/tcp_transport/base_layer.h" +#include "config/tcp_transport/base_layer.h" /** * @brief Applies TCP transport settings from a PHP array to the live runtime state. diff --git a/extension/include/config/tcp_transport/default.h b/extension/include/config/tcp_transport/default.h old mode 100755 new mode 100644 diff --git a/extension/include/config/tcp_transport/index.h b/extension/include/config/tcp_transport/index.h old mode 100755 new mode 100644 diff --git a/extension/include/config/tcp_transport/ini.h b/extension/include/config/tcp_transport/ini.h old mode 100755 new mode 100644 diff --git a/extension/include/config/tls_and_crypto/base_layer.h b/extension/include/config/tls_and_crypto/base_layer.h old mode 100755 new mode 100644 diff --git a/extension/include/config/tls_and_crypto/config.h b/extension/include/config/tls_and_crypto/config.h old mode 100755 new mode 100644 index 03fed4ef1..14fb7728d --- a/extension/include/config/tls_and_crypto/config.h +++ b/extension/include/config/tls_and_crypto/config.h @@ -20,7 +20,7 @@ #define KING_CONFIG_TLS_CRYPTO_CONFIG_H #include "php.h" -#include "include/config/tls_and_crypto/base_layer.h" +#include "config/tls_and_crypto/base_layer.h" /** * @brief Applies TLS/crypto settings from a PHP array to the live runtime state. diff --git a/extension/include/config/tls_and_crypto/default.h b/extension/include/config/tls_and_crypto/default.h old mode 100755 new mode 100644 diff --git a/extension/include/config/tls_and_crypto/index.h b/extension/include/config/tls_and_crypto/index.h old mode 100755 new mode 100644 diff --git a/extension/include/config/tls_and_crypto/ini.h b/extension/include/config/tls_and_crypto/ini.h old mode 100755 new mode 100644 diff --git a/extension/include/db_ingest/arginfo/arginfo.h b/extension/include/db_ingest/arginfo/arginfo.h new file mode 100644 index 000000000..187a18b14 --- /dev/null +++ b/extension/include/db_ingest/arginfo/arginfo.h @@ -0,0 +1,5 @@ +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_db_ingest, 0, 2, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) + ZEND_ARG_CALLABLE_INFO(0, writer, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() diff --git a/extension/include/db_ingest/arginfo/index.h b/extension/include/db_ingest/arginfo/index.h new file mode 100644 index 000000000..2388f102b --- /dev/null +++ b/extension/include/db_ingest/arginfo/index.h @@ -0,0 +1,16 @@ +/* + * ========================================================================= + * FILENAME: include/db_ingest/arginfo/index.h + * PROJECT: king + * + * PURPOSE: + * Arginfo aggregation point for database ingest PHP entry points. + * ========================================================================= + */ + +#ifndef KING_DB_INGEST_ARGINFO_INDEX_H +#define KING_DB_INGEST_ARGINFO_INDEX_H + +#include "arginfo.h" + +#endif /* KING_DB_INGEST_ARGINFO_INDEX_H */ diff --git a/extension/include/db_ingest/db_ingest.h b/extension/include/db_ingest/db_ingest.h new file mode 100644 index 000000000..5faed11f4 --- /dev/null +++ b/extension/include/db_ingest/db_ingest.h @@ -0,0 +1,16 @@ +/* + * include/db_ingest/db_ingest.h - Public database-ingest entry point + * ================================================================== + * + * This header declares the exported PHP function behind King's host-local + * serialized writer lane. + */ + +#ifndef KING_DB_INGEST_H +#define KING_DB_INGEST_H + +#include + +PHP_FUNCTION(king_db_ingest); + +#endif /* KING_DB_INGEST_H */ diff --git a/extension/include/db_ingest/function_entries.h b/extension/include/db_ingest/function_entries.h new file mode 100644 index 000000000..1d91fb06b --- /dev/null +++ b/extension/include/db_ingest/function_entries.h @@ -0,0 +1 @@ + PHP_FE(king_db_ingest, arginfo_king_db_ingest) diff --git a/extension/include/db_ingest/index.h b/extension/include/db_ingest/index.h new file mode 100644 index 000000000..abe55e7c0 --- /dev/null +++ b/extension/include/db_ingest/index.h @@ -0,0 +1,16 @@ +/* + * ========================================================================= + * FILENAME: include/db_ingest/index.h + * PROJECT: king + * + * PURPOSE: + * Public umbrella header for the database-ingest binding surface. + * ========================================================================= + */ + +#ifndef KING_DB_INGEST_INDEX_H +#define KING_DB_INGEST_INDEX_H + +#include "db_ingest.h" + +#endif /* KING_DB_INGEST_INDEX_H */ diff --git a/extension/include/iibin/arginfo/arginfo.h b/extension/include/iibin/arginfo/arginfo.h new file mode 100644 index 000000000..edeb4def9 --- /dev/null +++ b/extension/include/iibin/arginfo/arginfo.h @@ -0,0 +1,75 @@ +/* + * Arginfo for the procedural king_proto_* IIBIN API and the King\IIBIN OO + * facade. The declarations are consumed through include/iibin/arginfo/index.h. + */ + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_proto_encode, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, schema_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, data, IS_MIXED, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_proto_encode_batch, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, schema_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, records, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_proto_decode, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, schema_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, binary_data, IS_STRING, 0) + ZEND_ARG_TYPE_MASK(0, decode_as_object, MAY_BE_BOOL|MAY_BE_STRING|MAY_BE_ARRAY, "false") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_proto_decode_batch, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, schema_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, binary_records, IS_ARRAY, 0) + ZEND_ARG_TYPE_MASK(0, decode_as_object, MAY_BE_BOOL|MAY_BE_STRING|MAY_BE_ARRAY, "false") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_proto_define_enum, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, enum_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, enum_values, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_proto_define_schema, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, schema_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, schema_definition, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_IIBIN_defineEnum, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, values, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_IIBIN_defineSchema, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, fields, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_IIBIN_encode, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, schema, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, data, IS_MIXED, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_IIBIN_encodeBatch, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, schema, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, records, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_IIBIN_decode, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, schema, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, data, IS_STRING, 0) + ZEND_ARG_TYPE_MASK(0, decodeAsObject, MAY_BE_BOOL|MAY_BE_STRING|MAY_BE_ARRAY, "false") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_IIBIN_decodeBatch, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, schema, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, records, IS_ARRAY, 0) + ZEND_ARG_TYPE_MASK(0, decodeAsObject, MAY_BE_BOOL|MAY_BE_STRING|MAY_BE_ARRAY, "false") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_IIBIN_name, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_IIBIN_no_args, 0, 0, 0) +ZEND_END_ARG_INFO() diff --git a/extension/include/iibin/arginfo/index.h b/extension/include/iibin/arginfo/index.h new file mode 100644 index 000000000..70cba4533 --- /dev/null +++ b/extension/include/iibin/arginfo/index.h @@ -0,0 +1,16 @@ +/* + * ========================================================================= + * FILENAME: include/iibin/arginfo/index.h + * PROJECT: king + * + * PURPOSE: + * Arginfo aggregation point for IIBIN/proto PHP entry points. + * ========================================================================= + */ + +#ifndef KING_IIBIN_ARGINFO_INDEX_H +#define KING_IIBIN_ARGINFO_INDEX_H + +#include "arginfo.h" + +#endif /* KING_IIBIN_ARGINFO_INDEX_H */ diff --git a/extension/include/iibin/class_method_entries.h b/extension/include/iibin/class_method_entries.h new file mode 100644 index 000000000..ffbbaf1e4 --- /dev/null +++ b/extension/include/iibin/class_method_entries.h @@ -0,0 +1,16 @@ +#include "iibin/arginfo/index.h" + +const zend_function_entry king_iibin_class_methods[] = { + ZEND_ME_MAPPING(defineEnum, king_proto_define_enum, arginfo_class_King_IIBIN_defineEnum, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(defineSchema, king_proto_define_schema, arginfo_class_King_IIBIN_defineSchema, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(encode, king_proto_encode, arginfo_class_King_IIBIN_encode, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(encodeBatch, king_proto_encode_batch, arginfo_class_King_IIBIN_encodeBatch, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(decode, king_proto_decode, arginfo_class_King_IIBIN_decode, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(decodeBatch, king_proto_decode_batch, arginfo_class_King_IIBIN_decodeBatch, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(isDefined, king_proto_is_defined, arginfo_class_King_IIBIN_name, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(isSchemaDefined, king_proto_is_schema_defined, arginfo_class_King_IIBIN_name, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(isEnumDefined, king_proto_is_enum_defined, arginfo_class_King_IIBIN_name, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(getDefinedSchemas, king_proto_get_defined_schemas, arginfo_class_King_IIBIN_no_args, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(getDefinedEnums, king_proto_get_defined_enums, arginfo_class_King_IIBIN_no_args, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + PHP_FE_END +}; diff --git a/extension/include/iibin/class_methods.h b/extension/include/iibin/class_methods.h new file mode 100644 index 000000000..97c9001dd --- /dev/null +++ b/extension/include/iibin/class_methods.h @@ -0,0 +1,12 @@ +/* + * include/iibin/class_methods.h - IIBIN OO method table externs + */ + +#ifndef KING_IIBIN_CLASS_METHODS_H +#define KING_IIBIN_CLASS_METHODS_H + +#include + +extern const zend_function_entry king_iibin_class_methods[]; + +#endif /* KING_IIBIN_CLASS_METHODS_H */ diff --git a/extension/include/iibin/function_entries.h b/extension/include/iibin/function_entries.h new file mode 100644 index 000000000..cf371f627 --- /dev/null +++ b/extension/include/iibin/function_entries.h @@ -0,0 +1,11 @@ + PHP_FE(king_proto_define_schema, arginfo_king_proto_define_schema) + PHP_FE(king_proto_define_enum, arginfo_king_proto_define_enum) + PHP_FE(king_proto_encode, arginfo_king_proto_encode) + PHP_FE(king_proto_encode_batch, arginfo_king_proto_encode_batch) + PHP_FE(king_proto_decode, arginfo_king_proto_decode) + PHP_FE(king_proto_decode_batch, arginfo_king_proto_decode_batch) + PHP_FE(king_proto_is_schema_defined, arginfo_king_one_string) + PHP_FE(king_proto_is_enum_defined, arginfo_king_one_string) + PHP_FE(king_proto_is_defined, arginfo_king_one_string) + PHP_FE(king_proto_get_defined_schemas, arginfo_king_no_args) + PHP_FE(king_proto_get_defined_enums, arginfo_king_no_args) diff --git a/extension/include/iibin/iibin.h b/extension/include/iibin/iibin.h old mode 100755 new mode 100644 diff --git a/extension/include/iibin/iibin_internal.h b/extension/include/iibin/iibin_internal.h old mode 100755 new mode 100644 index db4ffc09e..8ca698dcb --- a/extension/include/iibin/iibin_internal.h +++ b/extension/include/iibin/iibin_internal.h @@ -14,6 +14,8 @@ #include #include #include +#include "iibin/class_methods.h" +#include "iibin/lifecycle.h" /* --- Wire Format Constants --- */ #define KING_WIRETYPE_VARINT 0 @@ -86,9 +88,6 @@ extern HashTable king_proto_schema_registry; extern HashTable king_proto_enum_registry; extern bool king_proto_registries_initialized; -/* --- Lifecycle / Registry Helpers --- */ -int king_proto_registry_minit(void); -void king_proto_registry_mshutdown(void); zend_result king_iibin_define_enum( zend_string *enum_name, zval *enum_values @@ -140,9 +139,6 @@ void king_proto_runtime_schema_zval_dtor(zval *zv); void king_proto_runtime_enum_free(king_proto_runtime_enum *runtime_enum); void king_proto_runtime_enum_zval_dtor(zval *zv); -int king_iibin_minit(void); -extern const zend_function_entry king_iibin_class_methods[]; - /* --- Wire Helpers --- */ #define KING_IIBIN_BATCH_MAX_RECORDS 65536 diff --git a/extension/include/iibin/index.h b/extension/include/iibin/index.h new file mode 100644 index 000000000..935999b3f --- /dev/null +++ b/extension/include/iibin/index.h @@ -0,0 +1,18 @@ +/* + * ========================================================================= + * FILENAME: include/iibin/index.h + * PROJECT: king + * + * PURPOSE: + * Public umbrella header for the IIBIN/proto subsystem. + * ========================================================================= + */ + +#ifndef KING_IIBIN_INDEX_H +#define KING_IIBIN_INDEX_H + +#include "class_methods.h" +#include "iibin.h" +#include "lifecycle.h" + +#endif /* KING_IIBIN_INDEX_H */ diff --git a/extension/include/iibin/lifecycle.h b/extension/include/iibin/lifecycle.h new file mode 100644 index 000000000..526bee104 --- /dev/null +++ b/extension/include/iibin/lifecycle.h @@ -0,0 +1,12 @@ +/* + * include/iibin/lifecycle.h - IIBIN/proto module bootstrap hooks + */ + +#ifndef KING_IIBIN_LIFECYCLE_H +#define KING_IIBIN_LIFECYCLE_H + +int king_proto_registry_minit(void); +void king_proto_registry_mshutdown(void); +int king_iibin_minit(void); + +#endif /* KING_IIBIN_LIFECYCLE_H */ diff --git a/extension/include/inference/arginfo/arginfo.h b/extension/include/inference/arginfo/arginfo.h new file mode 100644 index 000000000..a6a780ac3 --- /dev/null +++ b/extension/include/inference/arginfo/arginfo.h @@ -0,0 +1,193 @@ +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_inference_runtime_model_config, 0, 0, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, config, IS_MIXED, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_inference_runtime_model_registry_config, 0, 0, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, config, IS_MIXED, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_king_inference_runtime_model_load, 0, 0, King\\Inference\\Model, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, config, IS_MIXED, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_inference_runtime_mini_op_content, 0, 1, IS_STRING, 1) + ZEND_ARG_TYPE_INFO(0, payload, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_inference_gpu_runtime_status, 0, 0, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, config, IS_MIXED, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_inference_llm_cache_status, 0, 0, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, config, IS_MIXED, 1, "null") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_king_inference_model_load, 0, 1, King\\Inference\\Model, 0) + ZEND_ARG_TYPE_INFO(0, config, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_inference_model_info, 0, 1, IS_ARRAY, 0) + ZEND_ARG_OBJ_INFO(0, model, King\\Inference\\Model, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_inference_tokenize, 0, 2, IS_ARRAY, 0) + ZEND_ARG_OBJ_INFO(0, model, King\\Inference\\Model, 0) + ZEND_ARG_TYPE_INFO(0, text, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_inference_token_decode, 0, 2, IS_STRING, 0) + ZEND_ARG_OBJ_INFO(0, model, King\\Inference\\Model, 0) + ZEND_ARG_TYPE_INFO(0, token_id, IS_LONG, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_inference_token_decode_graph, 0, 3, IS_ARRAY, 0) + ZEND_ARG_OBJ_INFO(0, model, King\\Inference\\Model, 0) + ZEND_ARG_TYPE_INFO(0, token, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, position, IS_LONG, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_inference_tensor_view, 0, 2, IS_ARRAY, 0) + ZEND_ARG_OBJ_INFO(0, model, King\\Inference\\Model, 0) + ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_inference_tensor_index, 0, 1, IS_ARRAY, 0) + ZEND_ARG_OBJ_INFO(0, model, King\\Inference\\Model, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_inference_tensor_dequantize, 0, 2, IS_ARRAY, 0) + ZEND_ARG_OBJ_INFO(0, model, King\\Inference\\Model, 0) + ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_inference_tensor_matmul, 0, 3, IS_ARRAY, 0) + ZEND_ARG_OBJ_INFO(0, model, King\\Inference\\Model, 0) + ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, input, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_inference_graph_run, 0, 2, IS_ARRAY, 0) + ZEND_ARG_OBJ_INFO(0, model, King\\Inference\\Model, 0) + ZEND_ARG_TYPE_INFO(0, graph, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_inference_kv_cache_plan, 0, 1, IS_ARRAY, 0) + ZEND_ARG_OBJ_INFO(0, model, King\\Inference\\Model, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, request, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_king_inference_stream, 0, 2, King\\Inference\\Stream, 0) + ZEND_ARG_OBJ_INFO(0, model, King\\Inference\\Model, 0) + ZEND_ARG_TYPE_INFO(0, request, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_king_inference_openai_chat_stream, 0, 2, King\\Inference\\Stream, 0) + ZEND_ARG_OBJ_INFO(0, model, King\\Inference\\Model, 0) + ZEND_ARG_TYPE_INFO(0, payload, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_inference_openai_chat_http_response, 0, 2, IS_ARRAY, 0) + ZEND_ARG_OBJ_INFO(0, model, King\\Inference\\Model, 0) + ZEND_ARG_TYPE_INFO(0, request, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_inference_openai_http_response, 0, 2, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, models, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, request, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_inference_next, 0, 1, IS_ARRAY, 1) + ZEND_ARG_OBJ_INFO(0, stream, King\\Inference\\Stream, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_king_inference_next_async, 0, 1, King\\Awaitable, 0) + ZEND_ARG_OBJ_INFO(0, stream, King\\Inference\\Stream, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_inference_cancel, 0, 1, _IS_BOOL, 0) + ZEND_ARG_OBJ_INFO(0, stream, King\\Inference\\Stream, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_Inference_Model___construct, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, config, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Inference_Model_info, 0, 0, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Inference_Model_tokenize, 0, 1, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, text, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Inference_Model_tokenDecode, 0, 1, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, token_id, IS_LONG, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Inference_Model_tokenDecodeGraph, 0, 2, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, token, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, position, IS_LONG, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Inference_Model_tensorView, 0, 1, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Inference_Model_tensorIndex, 0, 0, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Inference_Model_tensorDequantize, 0, 1, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Inference_Model_tensorMatmul, 0, 2, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, input, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Inference_Model_graphRun, 0, 1, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, graph, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Inference_Model_kvCachePlan, 0, 0, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, request, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_Inference_Stream___construct, 0, 0, 2) + ZEND_ARG_OBJ_INFO(0, model, King\\Inference\\Model, 0) + ZEND_ARG_TYPE_INFO(0, request, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Inference_Stream_next, 0, 0, IS_ARRAY, 1) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_King_Inference_Stream_nextAsync, 0, 0, King\\Awaitable, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Inference_Stream_cancel, 0, 0, _IS_BOOL, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Inference_Stream_isDone, 0, 0, _IS_BOOL, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Inference_Stream_getMetrics, 0, 0, IS_ARRAY, 0) +ZEND_END_ARG_INFO() diff --git a/extension/include/inference/arginfo/index.h b/extension/include/inference/arginfo/index.h new file mode 100644 index 000000000..361301feb --- /dev/null +++ b/extension/include/inference/arginfo/index.h @@ -0,0 +1,16 @@ +/* + * ========================================================================= + * FILENAME: include/inference/arginfo/index.h + * PROJECT: king + * + * PURPOSE: + * Arginfo aggregation point for native inference PHP entry points. + * ========================================================================= + */ + +#ifndef KING_INFERENCE_ARGINFO_INDEX_H +#define KING_INFERENCE_ARGINFO_INDEX_H + +#include "arginfo.h" + +#endif /* KING_INFERENCE_ARGINFO_INDEX_H */ diff --git a/extension/include/inference/classes/class_entries.h b/extension/include/inference/classes/class_entries.h new file mode 100644 index 000000000..5b0a766c5 --- /dev/null +++ b/extension/include/inference/classes/class_entries.h @@ -0,0 +1,14 @@ +/* + * include/inference/classes/class_entries.h - Inference class-entry externs + */ + +#ifndef KING_INFERENCE_CLASS_ENTRIES_H +#define KING_INFERENCE_CLASS_ENTRIES_H + +#include + +extern zend_class_entry *king_ce_inference; +extern zend_class_entry *king_ce_inference_model; +extern zend_class_entry *king_ce_inference_stream; + +#endif /* KING_INFERENCE_CLASS_ENTRIES_H */ diff --git a/extension/include/inference/classes/class_method_entries.h b/extension/include/inference/classes/class_method_entries.h new file mode 100644 index 000000000..9825aac61 --- /dev/null +++ b/extension/include/inference/classes/class_method_entries.h @@ -0,0 +1,51 @@ +const zend_function_entry king_inference_class_methods[] = { + ZEND_ME_MAPPING(runtimeModelConfig, king_inference_runtime_model_config, arginfo_king_inference_runtime_model_config, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(runtimeModelRegistryConfig, king_inference_runtime_model_registry_config, arginfo_king_inference_runtime_model_registry_config, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(runtimeModelLoad, king_inference_runtime_model_load, arginfo_king_inference_runtime_model_load, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(runtimeMiniOpContent, king_inference_runtime_mini_op_content, arginfo_king_inference_runtime_mini_op_content, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(gpuRuntimeStatus, king_inference_gpu_runtime_status, arginfo_king_inference_gpu_runtime_status, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(llmCacheStatus, king_inference_llm_cache_status, arginfo_king_inference_llm_cache_status, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(loadModel, king_inference_model_load, arginfo_king_inference_model_load, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(modelInfo, king_inference_model_info, arginfo_king_inference_model_info, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(tokenize, king_inference_tokenize, arginfo_king_inference_tokenize, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(tokenDecode, king_inference_token_decode, arginfo_king_inference_token_decode, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(tokenDecodeGraph, king_inference_token_decode_graph, arginfo_king_inference_token_decode_graph, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(tensorView, king_inference_tensor_view, arginfo_king_inference_tensor_view, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(tensorIndex, king_inference_tensor_index, arginfo_king_inference_tensor_index, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(tensorDequantize, king_inference_tensor_dequantize, arginfo_king_inference_tensor_dequantize, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(tensorMatmul, king_inference_tensor_matmul, arginfo_king_inference_tensor_matmul, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(graphRun, king_inference_graph_run, arginfo_king_inference_graph_run, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(kvCachePlan, king_inference_kv_cache_plan, arginfo_king_inference_kv_cache_plan, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(stream, king_inference_stream, arginfo_king_inference_stream, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(openaiChatHttpResponse, king_inference_openai_chat_http_response, arginfo_king_inference_openai_chat_http_response, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(openaiHttpResponse, king_inference_openai_http_response, arginfo_king_inference_openai_http_response, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(next, king_inference_next, arginfo_king_inference_next, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(nextAsync, king_inference_next_async, arginfo_king_inference_next_async, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(cancel, king_inference_cancel, arginfo_king_inference_cancel, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + PHP_FE_END +}; + +const zend_function_entry king_inference_model_class_methods[] = { + PHP_ME(King_Inference_Model, __construct, arginfo_class_King_Inference_Model___construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) + PHP_ME(King_Inference_Model, info, arginfo_class_King_Inference_Model_info, ZEND_ACC_PUBLIC) + PHP_ME(King_Inference_Model, tokenize, arginfo_class_King_Inference_Model_tokenize, ZEND_ACC_PUBLIC) + PHP_ME(King_Inference_Model, tokenDecode, arginfo_class_King_Inference_Model_tokenDecode, ZEND_ACC_PUBLIC) + PHP_ME(King_Inference_Model, tokenDecodeGraph, arginfo_class_King_Inference_Model_tokenDecodeGraph, ZEND_ACC_PUBLIC) + PHP_ME(King_Inference_Model, tensorView, arginfo_class_King_Inference_Model_tensorView, ZEND_ACC_PUBLIC) + PHP_ME(King_Inference_Model, tensorIndex, arginfo_class_King_Inference_Model_tensorIndex, ZEND_ACC_PUBLIC) + PHP_ME(King_Inference_Model, tensorDequantize, arginfo_class_King_Inference_Model_tensorDequantize, ZEND_ACC_PUBLIC) + PHP_ME(King_Inference_Model, tensorMatmul, arginfo_class_King_Inference_Model_tensorMatmul, ZEND_ACC_PUBLIC) + PHP_ME(King_Inference_Model, graphRun, arginfo_class_King_Inference_Model_graphRun, ZEND_ACC_PUBLIC) + PHP_ME(King_Inference_Model, kvCachePlan, arginfo_class_King_Inference_Model_kvCachePlan, ZEND_ACC_PUBLIC) + PHP_FE_END +}; + +const zend_function_entry king_inference_stream_class_methods[] = { + PHP_ME(King_Inference_Stream, __construct, arginfo_class_King_Inference_Stream___construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) + PHP_ME(King_Inference_Stream, next, arginfo_class_King_Inference_Stream_next, ZEND_ACC_PUBLIC) + PHP_ME(King_Inference_Stream, nextAsync, arginfo_class_King_Inference_Stream_nextAsync, ZEND_ACC_PUBLIC) + PHP_ME(King_Inference_Stream, cancel, arginfo_class_King_Inference_Stream_cancel, ZEND_ACC_PUBLIC) + PHP_ME(King_Inference_Stream, isDone, arginfo_class_King_Inference_Stream_isDone, ZEND_ACC_PUBLIC) + PHP_ME(King_Inference_Stream, getMetrics, arginfo_class_King_Inference_Stream_getMetrics, ZEND_ACC_PUBLIC) + PHP_FE_END +}; diff --git a/extension/include/inference/classes/class_methods.h b/extension/include/inference/classes/class_methods.h new file mode 100644 index 000000000..85dcc449f --- /dev/null +++ b/extension/include/inference/classes/class_methods.h @@ -0,0 +1,14 @@ +/* + * include/inference/classes/class_methods.h - Native inference OO method table externs + */ + +#ifndef KING_INFERENCE_CLASS_METHODS_H +#define KING_INFERENCE_CLASS_METHODS_H + +#include + +extern const zend_function_entry king_inference_class_methods[]; +extern const zend_function_entry king_inference_model_class_methods[]; +extern const zend_function_entry king_inference_stream_class_methods[]; + +#endif /* KING_INFERENCE_CLASS_METHODS_H */ diff --git a/extension/include/inference/functions/function_entries.h b/extension/include/inference/functions/function_entries.h new file mode 100644 index 000000000..d97915510 --- /dev/null +++ b/extension/include/inference/functions/function_entries.h @@ -0,0 +1,24 @@ + PHP_FE(king_inference_runtime_model_config, arginfo_king_inference_runtime_model_config) + PHP_FE(king_inference_runtime_model_registry_config, arginfo_king_inference_runtime_model_registry_config) + PHP_FE(king_inference_runtime_model_load, arginfo_king_inference_runtime_model_load) + PHP_FE(king_inference_runtime_mini_op_content, arginfo_king_inference_runtime_mini_op_content) + PHP_FE(king_inference_gpu_runtime_status, arginfo_king_inference_gpu_runtime_status) + PHP_FE(king_inference_llm_cache_status, arginfo_king_inference_llm_cache_status) + PHP_FE(king_inference_model_load, arginfo_king_inference_model_load) + PHP_FE(king_inference_model_info, arginfo_king_inference_model_info) + PHP_FE(king_inference_tokenize, arginfo_king_inference_tokenize) + PHP_FE(king_inference_token_decode, arginfo_king_inference_token_decode) + PHP_FE(king_inference_token_decode_graph, arginfo_king_inference_token_decode_graph) + PHP_FE(king_inference_tensor_view, arginfo_king_inference_tensor_view) + PHP_FE(king_inference_tensor_index, arginfo_king_inference_tensor_index) + PHP_FE(king_inference_tensor_dequantize, arginfo_king_inference_tensor_dequantize) + PHP_FE(king_inference_tensor_matmul, arginfo_king_inference_tensor_matmul) + PHP_FE(king_inference_graph_run, arginfo_king_inference_graph_run) + PHP_FE(king_inference_kv_cache_plan, arginfo_king_inference_kv_cache_plan) + PHP_FE(king_inference_stream, arginfo_king_inference_stream) + PHP_FE(king_inference_openai_chat_stream, arginfo_king_inference_openai_chat_stream) + PHP_FE(king_inference_openai_chat_http_response, arginfo_king_inference_openai_chat_http_response) + PHP_FE(king_inference_openai_http_response, arginfo_king_inference_openai_http_response) + PHP_FE(king_inference_next, arginfo_king_inference_next) + PHP_FE(king_inference_next_async, arginfo_king_inference_next_async) + PHP_FE(king_inference_cancel, arginfo_king_inference_cancel) diff --git a/extension/include/inference/index.h b/extension/include/inference/index.h new file mode 100644 index 000000000..123243010 --- /dev/null +++ b/extension/include/inference/index.h @@ -0,0 +1,18 @@ +/* + * ========================================================================= + * FILENAME: include/inference/index.h + * PROJECT: king + * + * PURPOSE: + * Public umbrella header for the native inference subsystem. + * ========================================================================= + */ + +#ifndef KING_INFERENCE_INDEX_H +#define KING_INFERENCE_INDEX_H + +#include "classes/class_entries.h" +#include "surface/inference.h" +#include "registration/registration.h" + +#endif /* KING_INFERENCE_INDEX_H */ diff --git a/extension/include/inference/registration/registration.h b/extension/include/inference/registration/registration.h new file mode 100644 index 000000000..aa57c8ae1 --- /dev/null +++ b/extension/include/inference/registration/registration.h @@ -0,0 +1,14 @@ +/* + * include/inference/registration/registration.h - Native inference MINIT hooks + */ + +#ifndef KING_INFERENCE_REGISTRATION_H +#define KING_INFERENCE_REGISTRATION_H + +#include +#include "inference/classes/class_methods.h" + +void king_inference_register_classes(void); +void king_inference_init_object_handlers(void); + +#endif /* KING_INFERENCE_REGISTRATION_H */ diff --git a/extension/include/inference/surface/inference.h b/extension/include/inference/surface/inference.h new file mode 100644 index 000000000..8d8e871c4 --- /dev/null +++ b/extension/include/inference/surface/inference.h @@ -0,0 +1,658 @@ +/* + * include/inference/surface/inference.h - Native model inference object contract + */ + +#ifndef KING_INFERENCE_H +#define KING_INFERENCE_H + +#include +#include +#include +#include + +typedef struct _king_inference_gguf_metadata { + zend_ulong version; + zend_ulong tensor_count; + zend_ulong metadata_count; + zend_ulong file_size; + zend_ulong parsed_metadata_count; + zend_ulong tensor_directory_count; + zend_ulong tensor_data_offset; + zend_ulong tensor_data_alignment; + zend_ulong tokenizer_token_count; + zend_ulong tokenizer_score_count; + zend_ulong tokenizer_type_count; + zend_ulong tokenizer_merge_count; + zend_ulong tokenizer_max_token_bytes; + zend_ulong architecture_params[10]; + double rope_freq_base; + double rope_freq_base_swa; + double attention_layer_norm_rms_epsilon; + double final_logit_softcapping; + zend_long tokenizer_bos_id; + zend_long tokenizer_eos_id; + zend_long tokenizer_unknown_id; + zend_long tokenizer_pad_id; + bool tokenizer_tokens_loaded; + bool tokenizer_lookup_loaded; + bool tokenizer_scores_loaded; + bool tokenizer_types_loaded; + bool tokenizer_merges_loaded; + zend_ulong max_tensor_elements; + zend_ulong max_tensor_rank; + zend_ulong tensor_type_counts[32]; + zend_long file_type; + zend_string *architecture; + zend_string *general_name; + zend_string *tokenizer_model; + bool loaded; + bool metadata_parsed; + bool tensor_directory_parsed; +} king_inference_gguf_metadata; + +typedef unsigned long long king_inference_cuda_device_ptr; +typedef struct _king_inference_cuda_device_allocation king_inference_cuda_device_allocation; +typedef struct _king_inference_cuda_weight_upload king_inference_cuda_weight_upload; + +typedef struct _king_inference_model_object { + zval config; + zval tensor_index; + zval tokenizer_tokens; + zval tokenizer_lookup; + zval tokenizer_scores; + zval tokenizer_types; + zval tokenizer_merges; + zval paged_kv_cache_plan; + zend_string *name; + zend_string *artifact_path; + king_inference_gguf_metadata gguf; + void *native_map; + size_t native_map_size; + void *cuda_driver_handle; + void *cuda_context; + void *cuda_nvrtc_handle; + void *cuda_quantized_matvec_module; + void *cuda_q8_0_matvec_function; + void *cuda_quantized_matvec_batch_function; + void *cuda_rms_norm_nvrtc_handle; + void *cuda_rms_norm_module; + void *cuda_rms_norm_function; + void *cuda_rms_norm_batch_function; + void *cuda_rope_nvrtc_handle; + void *cuda_rope_module; + void *cuda_rope_function; + void *cuda_rope_batch_function; + void *cuda_embedding_row_nvrtc_handle; + void *cuda_embedding_row_module; + void *cuda_embedding_row_function; + void *cuda_embedding_rows_function; + void *cuda_device_vector_ops_nvrtc_handle; + void *cuda_device_vector_ops_module; + void *cuda_vector_add_function; + void *cuda_vector_mul_function; + void *cuda_vector_scale_function; + void *cuda_vector_silu_function; + void *cuda_vector_slice_function; + void *cuda_vector_copy_to_offset_function; + king_inference_cuda_device_ptr cuda_device_kv_cache_keys; + king_inference_cuda_device_ptr cuda_device_kv_cache_values; + king_inference_cuda_device_ptr cuda_decoder_prompt_prefill_embeddings; + king_inference_cuda_device_ptr cuda_decoder_prompt_prefill_initial_norm; + king_inference_cuda_device_ptr cuda_decoder_prompt_prefill_initial_linear; + king_inference_cuda_device_ptr cuda_decoder_prompt_prefill_initial_query_rope; + king_inference_cuda_device_ptr cuda_decoder_prompt_prefill_layer0_attention_stack; + king_inference_cuda_device_ptr cuda_decoder_prompt_prefill_layer0_attention_residual; + king_inference_cuda_device_ptr cuda_decoder_prompt_prefill_layer0_ffn_norm; + king_inference_cuda_device_ptr cuda_decoder_prompt_prefill_layer0_ffn_gate; + king_inference_cuda_device_ptr cuda_decoder_prompt_prefill_layer0_ffn_up; + king_inference_cuda_device_ptr cuda_decoder_prompt_prefill_layer0_ffn_swiglu; + king_inference_cuda_device_ptr cuda_decoder_prompt_prefill_layer0_ffn_down; + king_inference_cuda_device_ptr cuda_decoder_prompt_prefill_layer0_ffn_output_residual; + void *cuda_attention_scores_nvrtc_handle; + void *cuda_attention_scores_module; + void *cuda_attention_scores_function; + void *cuda_attention_scores_batch_function; + void *cuda_attention_softmax_nvrtc_handle; + void *cuda_attention_softmax_module; + void *cuda_attention_softmax_function; + void *cuda_attention_softmax_batch_function; + void *cuda_attention_values_nvrtc_handle; + void *cuda_attention_values_module; + void *cuda_attention_values_function; + void *cuda_attention_values_batch_function; + void *cuda_ffn_swiglu_nvrtc_handle; + void *cuda_ffn_swiglu_module; + void *cuda_ffn_swiglu_function; + void *cuda_logits_readback_nvrtc_handle; + void *cuda_logits_readback_module; + void *cuda_logits_top_k_function; + king_inference_cuda_device_ptr cuda_logits_readback_device_indices; + king_inference_cuda_device_ptr cuda_logits_readback_device_logits; + king_inference_cuda_device_allocation *cuda_device_allocations; + king_inference_cuda_weight_upload *cuda_weight_uploads; + int cuda_device; + int cuda_context_result; + int cuda_device_allocator_result; + int cuda_weight_upload_result; + int cuda_quantized_matvec_result; + int cuda_rms_norm_result; + int cuda_rope_result; + int cuda_embedding_row_result; + int cuda_device_vector_ops_result; + int cuda_device_kv_cache_result; + int cuda_attention_scores_result; + int cuda_attention_softmax_result; + int cuda_attention_values_result; + int cuda_ffn_swiglu_result; + int cuda_output_projection_result; + int cuda_logits_readback_result; + int cuda_decoder_graph_executor_result; + int cuda_decoder_prompt_loop_result; + char cuda_context_error[160]; + char cuda_device_allocator_error[160]; + char cuda_weight_upload_error[160]; + char cuda_quantized_matvec_error[160]; + char cuda_rms_norm_error[160]; + char cuda_rope_error[160]; + char cuda_embedding_row_error[160]; + char cuda_device_vector_ops_error[160]; + char cuda_device_kv_cache_error[160]; + char cuda_device_kv_cache_guard_last_operation[64]; + char cuda_device_kv_cache_guard_last_failure[160]; + char cuda_attention_scores_error[160]; + char cuda_attention_softmax_error[160]; + char cuda_attention_values_error[160]; + char cuda_ffn_swiglu_error[160]; + char cuda_output_projection_error[160]; + char cuda_logits_readback_error[160]; + char cuda_decoder_graph_executor_error[160]; + char cuda_decoder_prompt_loop_error[160]; + size_t cuda_device_bytes_allocated; + size_t cuda_device_peak_bytes_allocated; + size_t cuda_device_allocation_count; + size_t cuda_weight_required_count; + size_t cuda_weight_resolved_count; + size_t cuda_weight_uploaded_count; + size_t cuda_weight_duplicate_count; + size_t cuda_weight_failed_count; + size_t cuda_weight_uploaded_bytes; + size_t cuda_weight_cache_hits; + size_t cuda_weight_cache_misses; + size_t cuda_weight_cache_stores; + size_t cuda_quantized_matvec_launch_count; + size_t cuda_quantized_matvec_batch_launch_count; + size_t cuda_rms_norm_launch_count; + size_t cuda_rms_norm_batch_launch_count; + size_t cuda_rope_launch_count; + size_t cuda_rope_batch_launch_count; + size_t cuda_rope_sync_count; + size_t cuda_rope_batch_sync_count; + size_t cuda_embedding_row_launch_count; + size_t cuda_embedding_rows_launch_count; + zend_ulong cuda_embedding_row_last_token_id; + zend_ulong cuda_embedding_row_last_width; + zend_ulong cuda_embedding_rows_last_batch_tokens; + zend_ulong cuda_embedding_rows_last_width; + size_t cuda_decoder_prompt_prefill_embedding_bytes; + size_t cuda_decoder_prompt_prefill_initial_norm_bytes; + size_t cuda_decoder_prompt_prefill_initial_linear_bytes; + size_t cuda_decoder_prompt_prefill_initial_query_rope_bytes; + size_t cuda_decoder_prompt_prefill_layer0_attention_stack_bytes; + size_t cuda_decoder_prompt_prefill_layer0_attention_residual_bytes; + size_t cuda_decoder_prompt_prefill_layer0_ffn_norm_bytes; + size_t cuda_decoder_prompt_prefill_layer0_ffn_gate_bytes; + size_t cuda_decoder_prompt_prefill_layer0_ffn_up_bytes; + size_t cuda_decoder_prompt_prefill_layer0_ffn_swiglu_bytes; + size_t cuda_decoder_prompt_prefill_layer0_ffn_down_bytes; + size_t cuda_decoder_prompt_prefill_layer0_ffn_output_residual_bytes; + size_t cuda_decoder_prompt_prefill_kv_cache_writes; + zend_ulong cuda_decoder_prompt_prefill_tokens; + zend_ulong cuda_decoder_prompt_prefill_width; + zend_ulong cuda_decoder_prompt_prefill_initial_linear_rows; + zend_ulong cuda_decoder_prompt_prefill_initial_query_rope_heads; + zend_ulong cuda_decoder_prompt_prefill_initial_query_rope_head_dim; + zend_ulong cuda_decoder_prompt_prefill_kv_heads; + zend_ulong cuda_decoder_prompt_prefill_kv_key_length; + zend_ulong cuda_decoder_prompt_prefill_kv_value_length; + zend_ulong cuda_decoder_prompt_prefill_layer0_attention_heads; + zend_ulong cuda_decoder_prompt_prefill_layer0_attention_stack_width; + zend_ulong cuda_decoder_prompt_prefill_layer0_attention_residual_width; + zend_ulong cuda_decoder_prompt_prefill_layer0_ffn_norm_width; + zend_ulong cuda_decoder_prompt_prefill_layer0_ffn_gate_rows; + zend_ulong cuda_decoder_prompt_prefill_layer0_ffn_up_rows; + zend_ulong cuda_decoder_prompt_prefill_layer0_ffn_swiglu_width; + zend_ulong cuda_decoder_prompt_prefill_layer0_ffn_down_width; + zend_ulong cuda_decoder_prompt_prefill_layer0_ffn_output_residual_width; + size_t cuda_device_vector_ops_launch_count; + zend_ulong cuda_device_vector_ops_last_length; + size_t cuda_device_kv_cache_bytes; + size_t cuda_device_kv_cache_key_bytes; + size_t cuda_device_kv_cache_value_bytes; + size_t cuda_device_kv_cache_reserve_call_count; + size_t cuda_device_kv_cache_allocation_count; + size_t cuda_device_kv_cache_write_count; + size_t cuda_device_kv_cache_batch_write_count; + size_t cuda_device_kv_cache_guard_check_count; + size_t cuda_device_kv_cache_guard_failure_count; + zend_ulong cuda_device_kv_cache_layers; + zend_ulong cuda_device_kv_cache_kv_heads; + zend_ulong cuda_device_kv_cache_context_tokens; + zend_ulong cuda_device_kv_cache_key_length; + zend_ulong cuda_device_kv_cache_value_length; + zend_ulong cuda_device_kv_cache_last_layer; + zend_ulong cuda_device_kv_cache_last_head; + zend_ulong cuda_device_kv_cache_last_position; + zend_ulong cuda_device_kv_cache_guard_last_layer; + zend_ulong cuda_device_kv_cache_guard_last_head; + zend_ulong cuda_device_kv_cache_guard_last_position; + zend_ulong cuda_device_kv_cache_guard_last_count; + zend_ulong cuda_device_kv_cache_guard_last_page_start; + zend_ulong cuda_device_kv_cache_guard_last_page_end; + size_t cuda_attention_scores_launch_count; + size_t cuda_attention_scores_batch_launch_count; + size_t cuda_attention_softmax_launch_count; + size_t cuda_attention_softmax_batch_launch_count; + size_t cuda_attention_values_launch_count; + size_t cuda_attention_values_batch_launch_count; + size_t cuda_ffn_swiglu_launch_count; + size_t cuda_output_projection_launch_count; + size_t cuda_logits_readback_launch_count; + size_t cuda_logits_readback_sync_count; + size_t cuda_logits_readback_dtoh_count; + size_t cuda_logits_readback_candidate_limit; + size_t cuda_logits_readback_last_candidate_count; + size_t cuda_logits_readback_last_bytes; + size_t cuda_logits_readback_full_bytes; + size_t cuda_logits_readback_saved_bytes; + size_t cuda_logits_readback_device_indices_bytes; + size_t cuda_logits_readback_device_logits_bytes; + size_t cuda_logits_readback_buffer_reuse_count; + zend_long cuda_logits_readback_last_ns; + size_t cuda_logits_readback_synthetic_vocab_size; + size_t cuda_logits_readback_synthetic_candidate_count; + size_t cuda_logits_readback_synthetic_matched_count; + size_t cuda_logits_readback_synthetic_rank_matched_count; + size_t cuda_logits_readback_synthetic_token_id_matched_count; + size_t cuda_logits_readback_synthetic_logit_matched_count; + size_t cuda_decoder_graph_executor_graph_count; + size_t cuda_decoder_graph_executor_last_op_count; + size_t cuda_decoder_graph_executor_last_supported_op_count; + size_t cuda_decoder_graph_executor_result_count; + size_t cuda_decoder_graph_executor_last_result_op_count; + size_t cuda_decoder_graph_executor_plan_count; + size_t cuda_decoder_graph_executor_last_plan_op_count; + size_t cuda_decoder_graph_executor_last_plan_device_ops; + size_t cuda_decoder_graph_executor_last_plan_host_sampling_ops; + size_t cuda_decoder_graph_executor_tensor_lookup_count; + size_t cuda_decoder_graph_executor_op_index_lookup_count; + size_t cuda_decoder_graph_executor_op_index_hit_count; + size_t cuda_decoder_graph_executor_op_index_fallback_scan_count; + size_t cuda_decoder_graph_executor_embedding_execution_count; + size_t cuda_decoder_graph_executor_last_embedding_width; + zend_ulong cuda_decoder_graph_executor_last_embedding_token_id; + size_t cuda_decoder_graph_executor_rms_norm_execution_count; + size_t cuda_decoder_graph_executor_last_rms_norm_width; + size_t cuda_decoder_graph_executor_linear_execution_count; + size_t cuda_decoder_graph_executor_last_linear_input_width; + size_t cuda_decoder_graph_executor_last_linear_rows; + size_t cuda_decoder_graph_executor_slice_execution_count; + size_t cuda_decoder_graph_executor_last_slice_offset; + size_t cuda_decoder_graph_executor_last_slice_length; + size_t cuda_decoder_graph_executor_rope_execution_count; + size_t cuda_decoder_graph_executor_last_rope_length; + size_t cuda_decoder_graph_executor_last_rope_position; + size_t cuda_decoder_graph_executor_kv_head_prepare_execution_count; + size_t cuda_decoder_graph_executor_last_kv_head_key_length; + size_t cuda_decoder_graph_executor_last_kv_head_value_length; + size_t cuda_decoder_graph_executor_kv_write_execution_count; + size_t cuda_decoder_graph_executor_last_kv_write_key_length; + size_t cuda_decoder_graph_executor_last_kv_write_value_length; + zend_ulong cuda_decoder_graph_executor_last_kv_write_layer; + zend_ulong cuda_decoder_graph_executor_last_kv_write_head; + zend_ulong cuda_decoder_graph_executor_last_kv_write_position; + size_t cuda_decoder_graph_executor_kv_attention_execution_count; + size_t cuda_decoder_graph_executor_last_kv_attention_key_length; + size_t cuda_decoder_graph_executor_last_kv_attention_value_length; + zend_ulong cuda_decoder_graph_executor_last_kv_attention_layer; + zend_ulong cuda_decoder_graph_executor_last_kv_attention_head; + zend_ulong cuda_decoder_graph_executor_last_kv_attention_slot_start; + zend_ulong cuda_decoder_graph_executor_last_kv_attention_slot_count; + size_t cuda_decoder_graph_executor_attention_stack_slot_execution_count; + size_t cuda_decoder_graph_executor_last_attention_stack_width; + zend_ulong cuda_decoder_graph_executor_last_attention_stack_input_index; + zend_ulong cuda_decoder_graph_executor_last_attention_stack_input_count; + size_t cuda_decoder_graph_executor_attention_heads_execution_count; + size_t cuda_decoder_graph_executor_attention_stack_execution_count; + size_t cuda_decoder_graph_executor_attention_output_projection_execution_count; + size_t cuda_decoder_graph_executor_attention_residual_execution_count; + size_t cuda_decoder_graph_executor_ffn_norm_execution_count; + size_t cuda_decoder_graph_executor_ffn_gate_up_projection_execution_count; + size_t cuda_decoder_graph_executor_ffn_swiglu_execution_count; + size_t cuda_decoder_graph_executor_ffn_down_projection_execution_count; + size_t cuda_decoder_graph_executor_ffn_output_residual_execution_count; + size_t cuda_decoder_graph_executor_final_norm_execution_count; + size_t cuda_decoder_graph_executor_logits_projection_execution_count; + zend_ulong cuda_decoder_graph_executor_last_attention_stack_expected_heads; + zend_ulong cuda_decoder_graph_executor_last_attention_stack_prepared_heads; + size_t cuda_decoder_graph_executor_last_attention_output_projection_width; + size_t cuda_decoder_graph_executor_last_attention_residual_width; + size_t cuda_decoder_graph_executor_last_ffn_norm_width; + size_t cuda_decoder_graph_executor_last_ffn_gate_rows; + size_t cuda_decoder_graph_executor_last_ffn_up_rows; + size_t cuda_decoder_graph_executor_last_ffn_swiglu_width; + size_t cuda_decoder_graph_executor_last_ffn_down_rows; + size_t cuda_decoder_graph_executor_last_ffn_output_residual_width; + size_t cuda_decoder_graph_executor_last_final_norm_width; + size_t cuda_decoder_graph_executor_last_logits_rows; + size_t cuda_decoder_prompt_loop_count; + size_t cuda_decoder_prompt_loop_last_prompt_tokens; + size_t cuda_decoder_prompt_loop_last_validated_graphs; + size_t cuda_decoder_prompt_loop_last_result_envelopes; + size_t cuda_decoder_prompt_loop_last_prefill_batches; + size_t cuda_decoder_prompt_loop_last_prefill_tokens; + size_t cuda_decoder_prompt_loop_last_prefill_template_reuses; + size_t cuda_decoder_prompt_loop_last_decoded_tokens; + size_t cuda_decoder_prompt_loop_last_kv_cache_reserve_calls; + size_t cuda_decoder_prompt_loop_last_kv_cache_allocations; + size_t cuda_decoder_prompt_loop_last_kv_cache_writes; + size_t cuda_decoder_prompt_loop_last_kv_cache_batch_writes; + size_t cuda_decoder_prompt_loop_last_graph_constructions; + size_t cuda_decoder_prompt_loop_last_plan_constructions; + size_t cuda_decoder_prompt_loop_last_policy_checks; + size_t cuda_decoder_prompt_loop_last_policy_interval_skips; + size_t cuda_decoder_prompt_loop_last_thermal_runtime_checks; + size_t cuda_decoder_prompt_loop_last_power_runtime_checks; + zend_long cuda_decoder_prompt_loop_last_max_tokens; + zend_long cuda_decoder_prompt_loop_last_thermal_interval_seconds; + zend_long cuda_decoder_prompt_loop_last_power_interval_seconds; + zend_long cuda_decoder_prompt_loop_last_hot_token_total_ns; + zend_long cuda_decoder_prompt_loop_last_hot_token_policy_ns; + zend_long cuda_decoder_prompt_loop_last_hot_token_mutation_ns; + zend_long cuda_decoder_prompt_loop_last_hot_token_device_execute_ns; + zend_long cuda_decoder_prompt_loop_last_hot_token_bounded_emit_ns; + zend_long cuda_decoder_prompt_loop_last_hot_token_logits_readback_ns; + zend_long cuda_decoder_prompt_loop_last_hot_token_sampler_ns; + zend_long cuda_decoder_prompt_loop_last_hot_token_token_decode_ns; + zend_long cuda_decoder_prompt_loop_last_hot_token_text_emit_ns; + zend_long cuda_decoder_prompt_loop_last_hot_token_event_shape_ns; + zend_long cuda_decoder_prompt_loop_last_prefill_ns; + size_t cuda_decoder_prompt_loop_last_hot_token_launches; + size_t cuda_decoder_prompt_loop_last_hot_token_syncs; + size_t cuda_decoder_prompt_loop_last_hot_token_dtoh_copies; + size_t cuda_decoder_prompt_loop_last_hot_token_allocator_acquisitions; + size_t cuda_decoder_prompt_loop_last_hot_token_graph_validations; + size_t cuda_decoder_prompt_loop_last_hot_token_graph_constructions; + size_t cuda_decoder_prompt_loop_last_hot_token_execution_plans; + size_t cuda_decoder_prompt_loop_last_hot_token_tensor_lookups; + size_t cuda_decoder_prompt_loop_last_hot_token_op_index_lookups; + size_t cuda_decoder_prompt_loop_last_hot_token_op_index_hits; + size_t cuda_decoder_prompt_loop_last_hot_token_op_index_fallback_scans; + size_t cuda_decoder_prompt_loop_last_hot_token_embedding_launches; + size_t cuda_decoder_prompt_loop_last_hot_token_matvec_launches; + size_t cuda_decoder_prompt_loop_last_hot_token_rms_norm_launches; + size_t cuda_decoder_prompt_loop_last_hot_token_rope_launches; + size_t cuda_decoder_prompt_loop_last_hot_token_attention_launches; + size_t cuda_decoder_prompt_loop_last_hot_token_vector_launches; + size_t cuda_decoder_prompt_loop_last_hot_token_ffn_launches; + size_t cuda_decoder_prompt_loop_last_hot_token_projection_launches; + size_t cuda_decoder_prompt_loop_last_hot_token_readback_launches; + size_t cuda_decoder_prompt_loop_last_hot_token_readback_candidates; + size_t cuda_decoder_prompt_loop_last_hot_token_readback_bytes; + size_t cuda_decoder_prompt_loop_last_hot_token_full_logits_bytes; + size_t cuda_decoder_prompt_loop_last_hot_token_saved_readback_bytes; + size_t cuda_decoder_prompt_loop_last_readback_tokens; + size_t cuda_decoder_prompt_loop_last_readback_bytes; + size_t cuda_decoder_prompt_loop_last_full_logits_bytes; + size_t cuda_decoder_prompt_loop_last_saved_readback_bytes; + zend_long runtime_last_started_ms; + zend_long runtime_last_first_token_ms; + zend_long runtime_last_finished_ms; + zend_long runtime_last_duration_ms; + zend_long runtime_last_time_to_first_token_ms; + zend_long runtime_last_generated_tokens; + zend_long tokenizer_last_token_count; + zend_long tokenizer_last_unknown_count; + zend_long tokenizer_last_input_bytes; + zend_long tokenizer_last_normalized_bytes; + double runtime_last_tokens_per_second; + HashTable cuda_weight_cache; + bool native_map_loaded; + bool cuda_context_attempted; + bool cuda_context_available; + bool cuda_context_owned; + bool cuda_device_allocator_attempted; + bool cuda_device_allocator_symbols_available; + bool cuda_device_allocator_available; + bool cuda_weight_upload_attempted; + bool cuda_weight_upload_complete; + bool cuda_weight_cache_initialized; + bool cuda_weight_cache_ready; + bool cuda_quantized_matvec_attempted; + bool cuda_quantized_matvec_available; + bool cuda_quantized_matvec_nvrtc_available; + bool cuda_quantized_matvec_module_loaded; + bool cuda_quantized_matvec_q8_0_available; + bool cuda_quantized_matvec_batch_available; + bool cuda_rms_norm_attempted; + bool cuda_rms_norm_available; + bool cuda_rms_norm_nvrtc_available; + bool cuda_rms_norm_module_loaded; + bool cuda_rms_norm_f32_available; + bool cuda_rms_norm_batch_available; + bool cuda_rope_attempted; + bool cuda_rope_available; + bool cuda_rope_nvrtc_available; + bool cuda_rope_module_loaded; + bool cuda_rope_f32_available; + bool cuda_rope_batch_available; + bool cuda_embedding_row_attempted; + bool cuda_embedding_row_available; + bool cuda_embedding_row_nvrtc_available; + bool cuda_embedding_row_module_loaded; + bool cuda_embedding_row_f32_available; + bool cuda_embedding_row_f16_available; + bool cuda_embedding_row_bf16_available; + bool cuda_embedding_row_q8_0_available; + bool cuda_embedding_row_q6_k_available; + bool cuda_embedding_rows_available; + bool cuda_decoder_prompt_prefill_embeddings_active; + bool cuda_device_vector_ops_attempted; + bool cuda_device_vector_ops_available; + bool cuda_device_vector_ops_nvrtc_available; + bool cuda_device_vector_ops_module_loaded; + bool cuda_vector_add_available; + bool cuda_vector_mul_available; + bool cuda_vector_scale_available; + bool cuda_vector_silu_available; + bool cuda_vector_slice_available; + bool cuda_vector_copy_to_offset_available; + bool cuda_device_kv_cache_attempted; + bool cuda_device_kv_cache_available; + bool cuda_device_kv_cache_shape_ready; + bool cuda_device_kv_cache_allocated; + bool cuda_device_kv_cache_f32; + bool cuda_device_kv_cache_dtod_available; + bool cuda_device_kv_cache_guard_fail_closed; + bool cuda_device_kv_cache_guard_last_page_crossed; + bool cuda_attention_scores_attempted; + bool cuda_attention_scores_available; + bool cuda_attention_scores_nvrtc_available; + bool cuda_attention_scores_module_loaded; + bool cuda_attention_scores_f32_available; + bool cuda_attention_scores_batch_available; + bool cuda_attention_softmax_attempted; + bool cuda_attention_softmax_available; + bool cuda_attention_softmax_nvrtc_available; + bool cuda_attention_softmax_module_loaded; + bool cuda_attention_softmax_f32_available; + bool cuda_attention_softmax_batch_available; + bool cuda_attention_values_attempted; + bool cuda_attention_values_available; + bool cuda_attention_values_nvrtc_available; + bool cuda_attention_values_module_loaded; + bool cuda_attention_values_f32_available; + bool cuda_attention_values_batch_available; + bool cuda_ffn_swiglu_attempted; + bool cuda_ffn_swiglu_available; + bool cuda_ffn_swiglu_nvrtc_available; + bool cuda_ffn_swiglu_module_loaded; + bool cuda_ffn_swiglu_f32_available; + bool cuda_ffn_swiglu_path_available; + bool cuda_output_projection_attempted; + bool cuda_output_projection_available; + bool cuda_output_projection_resolved; + bool cuda_output_projection_uploaded; + bool cuda_output_projection_tied_token_embedding; + bool cuda_output_projection_q8_0_available; + bool cuda_logits_readback_attempted; + bool cuda_logits_readback_available; + bool cuda_logits_readback_nvrtc_available; + bool cuda_logits_readback_module_loaded; + bool cuda_logits_readback_top_k_available; + bool cuda_logits_readback_bounded_cpu_available; + bool cuda_logits_readback_synthetic_ordering_checked; + bool cuda_logits_readback_synthetic_ordering_matched; + bool cuda_decoder_graph_executor_attempted; + bool cuda_decoder_graph_executor_available; + bool cuda_decoder_graph_executor_token_decode_available; + bool cuda_decoder_graph_executor_sampling_readback_available; + bool cuda_decoder_graph_executor_result_contract_available; + bool cuda_decoder_graph_executor_execution_plan_available; + bool cuda_decoder_graph_executor_embedding_execution_available; + bool cuda_decoder_graph_executor_last_embedding_token_available; + bool cuda_decoder_graph_executor_rms_norm_execution_available; + bool cuda_decoder_graph_executor_linear_execution_available; + bool cuda_decoder_graph_executor_slice_execution_available; + bool cuda_decoder_graph_executor_rope_execution_available; + bool cuda_decoder_graph_executor_kv_head_prepare_execution_available; + bool cuda_decoder_graph_executor_kv_write_execution_available; + bool cuda_decoder_graph_executor_kv_attention_execution_available; + bool cuda_decoder_graph_executor_attention_stack_slot_execution_available; + bool cuda_decoder_graph_executor_attention_heads_execution_available; + bool cuda_decoder_graph_executor_attention_stack_execution_available; + bool cuda_decoder_graph_executor_attention_output_projection_execution_available; + bool cuda_decoder_graph_executor_attention_residual_execution_available; + bool cuda_decoder_graph_executor_ffn_norm_execution_available; + bool cuda_decoder_graph_executor_ffn_gate_up_projection_execution_available; + bool cuda_decoder_graph_executor_ffn_swiglu_execution_available; + bool cuda_decoder_graph_executor_ffn_down_projection_execution_available; + bool cuda_decoder_graph_executor_ffn_output_residual_execution_available; + bool cuda_decoder_graph_executor_final_norm_execution_available; + bool cuda_decoder_graph_executor_logits_projection_execution_available; + bool cuda_decoder_prompt_loop_attempted; + bool cuda_decoder_prompt_loop_available; + bool cuda_decoder_prompt_loop_tokenizer_ready; + bool cuda_decoder_prompt_loop_graph_validation_ready; + bool cuda_decoder_prompt_loop_result_contract_ready; + bool cuda_decoder_prompt_loop_batch_prefill_available; + bool cuda_decoder_prompt_loop_last_used_batch_prefill; + bool cuda_decoder_prompt_loop_last_reuse_profile_available; + bool cuda_decoder_prompt_loop_last_hot_token_profile_available; + bool cuda_decoder_prompt_loop_last_hot_token_full_logits_readback_avoided; + bool runtime_last_timing_available; + bool runtime_last_first_token_available; + bool tokenizer_last_encode_available; + zend_object std; +} king_inference_model_object; + +typedef struct _king_inference_stream_object { + zval model; + zval request; + zval options; + zval native_events; + void *native_gpu_stream_state; + zend_ulong native_event_index; + int stdout_fd; + int stderr_fd; + zend_long child_pid; + zend_long exit_code; + zend_long chunk_count; + zend_long stderr_count; + zend_long bytes_emitted; + zend_long native_decoder_token_count; + zend_long created_at; + zend_long runtime_started_ms; + zend_long runtime_first_token_ms; + zend_long runtime_finished_ms; + zend_long runtime_generated_tokens; + zend_long gpu_thermal_preflight_at; + zend_long gpu_thermal_last_check_at; + zend_long gpu_thermal_abort_at; + zend_long gpu_power_preflight_at; + zend_long gpu_power_last_check_at; + zend_long gpu_power_abort_at; + zend_long gpu_policy_during_run_checks; + zend_long gpu_policy_interval_skips; + zend_long gpu_thermal_runtime_checks; + zend_long gpu_power_runtime_checks; + zend_long gpu_thermal_check_interval_seconds; + zend_long gpu_power_check_interval_seconds; + zend_ulong native_decoder_last_token_id; + double native_decoder_last_probability; + double native_decoder_last_logit; + double native_decoder_last_rank; + double gpu_thermal_preflight_temperature_c; + double gpu_thermal_abort_temperature_c; + double gpu_thermal_abort_ceiling_c; + double gpu_power_preflight_watts; + double gpu_power_abort_watts; + double gpu_power_abort_ceiling_watts; + zend_string *response_id; + bool start_event_pending; + bool openai_compatible; + bool native_decoder_last_token_available; + bool native_decoder_last_score_available; + bool gpu_thermal_preflight_checked; + bool gpu_thermal_preflight_temperature_available; + bool gpu_thermal_aborted; + bool gpu_power_preflight_checked; + bool gpu_power_preflight_available; + bool gpu_power_aborted; + bool done; + bool cancelled; + bool timed_out; + zend_object std; +} king_inference_stream_object; + +/* --- PHP Function Prototypes --- */ + +PHP_FUNCTION(king_inference_model_load); +PHP_FUNCTION(king_inference_runtime_model_config); +PHP_FUNCTION(king_inference_runtime_model_registry_config); +PHP_FUNCTION(king_inference_runtime_model_load); +PHP_FUNCTION(king_inference_runtime_mini_op_content); +PHP_FUNCTION(king_inference_gpu_runtime_status); +PHP_FUNCTION(king_inference_llm_cache_status); +PHP_FUNCTION(king_inference_model_info); +PHP_FUNCTION(king_inference_tokenize); +PHP_FUNCTION(king_inference_token_decode); +PHP_FUNCTION(king_inference_token_decode_graph); +PHP_FUNCTION(king_inference_tensor_view); +PHP_FUNCTION(king_inference_tensor_index); +PHP_FUNCTION(king_inference_tensor_dequantize); +PHP_FUNCTION(king_inference_tensor_matmul); +PHP_FUNCTION(king_inference_graph_run); +PHP_FUNCTION(king_inference_kv_cache_plan); +PHP_FUNCTION(king_inference_stream); +PHP_FUNCTION(king_inference_openai_chat_stream); +PHP_FUNCTION(king_inference_openai_chat_http_response); +PHP_FUNCTION(king_inference_openai_http_response); +PHP_FUNCTION(king_inference_next); +PHP_FUNCTION(king_inference_next_async); +PHP_FUNCTION(king_inference_cancel); + +static inline king_inference_model_object * +php_king_inference_model_obj_from_zend(zend_object *obj) +{ + return (king_inference_model_object *) + ((char*)obj - XtOffsetOf(king_inference_model_object, std)); +} + +static inline king_inference_stream_object * +php_king_inference_stream_obj_from_zend(zend_object *obj) +{ + return (king_inference_stream_object *) + ((char*)obj - XtOffsetOf(king_inference_stream_object, std)); +} + +#endif /* KING_INFERENCE_H */ diff --git a/extension/include/integration/arginfo/arginfo.h b/extension/include/integration/arginfo/arginfo.h new file mode 100644 index 000000000..145a3119e --- /dev/null +++ b/extension/include/integration/arginfo/arginfo.h @@ -0,0 +1,3 @@ +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_system_process_request, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, request_data, IS_ARRAY, 0) +ZEND_END_ARG_INFO() diff --git a/extension/include/integration/arginfo/index.h b/extension/include/integration/arginfo/index.h new file mode 100644 index 000000000..402705394 --- /dev/null +++ b/extension/include/integration/arginfo/index.h @@ -0,0 +1,16 @@ +/* + * ========================================================================= + * FILENAME: include/integration/arginfo/index.h + * PROJECT: king + * + * PURPOSE: + * Arginfo aggregation point for integration PHP entry points. + * ========================================================================= + */ + +#ifndef KING_INTEGRATION_ARGINFO_INDEX_H +#define KING_INTEGRATION_ARGINFO_INDEX_H + +#include "arginfo.h" + +#endif /* KING_INTEGRATION_ARGINFO_INDEX_H */ diff --git a/extension/include/integration/function_entries.h b/extension/include/integration/function_entries.h new file mode 100644 index 000000000..2d462a25d --- /dev/null +++ b/extension/include/integration/function_entries.h @@ -0,0 +1,11 @@ + PHP_FE(king_system_init, arginfo_king_config_array) + PHP_FE(king_system_health_check, arginfo_king_no_args) + PHP_FE(king_system_get_status, arginfo_king_no_args) + PHP_FE(king_system_get_metrics, arginfo_king_no_args) + PHP_FE(king_system_get_performance_report, arginfo_king_no_args) + PHP_FE(king_system_get_component_info, arginfo_king_one_string) + PHP_FE(king_system_process_request, arginfo_king_system_process_request) + PHP_FE(king_system_restart_component, arginfo_king_one_string) + PHP_FE(king_system_fail_component, arginfo_king_one_string) + PHP_FE(king_system_recover, arginfo_king_no_args) + PHP_FE(king_system_shutdown, arginfo_king_no_args) diff --git a/extension/include/integration/index.h b/extension/include/integration/index.h new file mode 100644 index 000000000..e66cafd72 --- /dev/null +++ b/extension/include/integration/index.h @@ -0,0 +1,16 @@ +/* + * ========================================================================= + * FILENAME: include/integration/index.h + * PROJECT: king + * + * PURPOSE: + * Public umbrella header for the system integration runtime. + * ========================================================================= + */ + +#ifndef KING_INTEGRATION_INDEX_H +#define KING_INTEGRATION_INDEX_H + +#include "system_integration.h" + +#endif /* KING_INTEGRATION_INDEX_H */ diff --git a/extension/include/integration/system_integration.h b/extension/include/integration/system_integration.h old mode 100755 new mode 100644 index a63b2b1c6..6186053e6 --- a/extension/include/integration/system_integration.h +++ b/extension/include/integration/system_integration.h @@ -24,13 +24,13 @@ #endif /* Component headers referenced by the system runtime / introspection layer. */ -#include "include/config/config.h" -#include "include/semantic_dns/semantic_dns.h" -#include "include/object_store/object_store.h" -#include "include/telemetry/telemetry.h" -#include "include/autoscaling/autoscaling.h" -#include "include/mcp/mcp.h" -#include "include/iibin/iibin.h" +#include "config/config.h" +#include "semantic_dns/semantic_dns.h" +#include "object_store/object_store.h" +#include "telemetry/telemetry.h" +#include "autoscaling/autoscaling.h" +#include "mcp/mcp.h" +#include "iibin/iibin.h" /* --- System Integration Types --- */ diff --git a/extension/include/king_init/ticket_ring.h b/extension/include/king_init/ticket_ring.h new file mode 100644 index 000000000..816bacf9a --- /dev/null +++ b/extension/include/king_init/ticket_ring.h @@ -0,0 +1,14 @@ +/* + * include/king_init/ticket_ring.h - Shared TLS ticket-ring API + */ + +#ifndef KING_INIT_TICKET_RING_H +#define KING_INIT_TICKET_RING_H + +#include +#include + +void king_ticket_ring_put(const uint8_t *ticket, size_t len); +int king_ticket_ring_get(uint8_t *out, size_t *out_len); + +#endif /* KING_INIT_TICKET_RING_H */ diff --git a/extension/include/mcp/arginfo/arginfo.h b/extension/include/mcp/arginfo/arginfo.h new file mode 100644 index 000000000..6a570336a --- /dev/null +++ b/extension/include/mcp/arginfo/arginfo.h @@ -0,0 +1,159 @@ +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_mcp_connect, 0, 0, 3) + ZEND_ARG_TYPE_INFO(0, host, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, port, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, config, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_mcp_request, 0, 0, 4) + ZEND_ARG_TYPE_INFO(0, connection, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, service_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, method_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, request_payload, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_king_mcp_request_async, 0, 4, King\\Awaitable, 0) + ZEND_ARG_TYPE_INFO(0, connection, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, service_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, method_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, request_payload, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_mcp_request_iibin, 0, 0, 4) + ZEND_ARG_TYPE_INFO(0, connection, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, service_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, method_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, data, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_king_mcp_request_iibin_async, 0, 4, King\\Awaitable, 0) + ZEND_ARG_TYPE_INFO(0, connection, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, service_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, method_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, data, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_mcp_server_create, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, definition, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_mcp_server_handle_jsonrpc, 0, 2, IS_STRING, 1) + ZEND_ARG_TYPE_INFO(0, server, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, message, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, context, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_mcp_server_handle_http, 0, 2, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, server, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, request, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, context, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_mcp_server_stdio, 0, 1, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, server, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_mcp_close, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, connection, IS_MIXED, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_mcp_upload_from_stream, 0, 0, 5) + ZEND_ARG_TYPE_INFO(0, connection, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, service_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, method_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, stream_identifier, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, stream, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_mcp_download_to_stream, 0, 0, 5) + ZEND_ARG_TYPE_INFO(0, connection, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, service_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, method_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, request_payload, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, stream, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_MCP___construct, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, host, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, port, IS_LONG, 0) + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, config, King\\Config, 1, "null") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_MCP_request, 0, 3, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, service, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, method, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, payload, IS_STRING, 0) + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, cancel, King\\CancelToken, 1, "null") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_King_MCP_requestAsync, 0, 3, King\\Awaitable, 0) + ZEND_ARG_TYPE_INFO(0, service, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, method, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, payload, IS_STRING, 0) + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, cancel, King\\CancelToken, 1, "null") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_MCP_requestIibin, 0, 0, 3) + ZEND_ARG_TYPE_INFO(0, service, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, method, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, data, IS_MIXED, 0) + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, cancel, King\\CancelToken, 1, "null") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_King_MCP_requestIibinAsync, 0, 3, King\\Awaitable, 0) + ZEND_ARG_TYPE_INFO(0, service, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, method, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, data, IS_MIXED, 0) + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, cancel, King\\CancelToken, 1, "null") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_MCP_close, 0, 0, IS_VOID, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_MCP_uploadFromStream, 0, 4, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, service, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, method, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, streamIdentifier, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, stream, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_MCP_downloadToStream, 0, 4, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, service, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, method, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, payload, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, stream, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_MCPServer___construct, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, definition, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_MCPServer_handleJsonRpc, 0, 1, IS_STRING, 1) + ZEND_ARG_TYPE_INFO(0, message, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, context, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_MCPServer_handleHttp, 0, 1, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, request, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, context, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_MCPServer_runStdio, 0, 0, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() diff --git a/extension/include/mcp/arginfo/index.h b/extension/include/mcp/arginfo/index.h new file mode 100644 index 000000000..01f7c3d7d --- /dev/null +++ b/extension/include/mcp/arginfo/index.h @@ -0,0 +1,16 @@ +/* + * ========================================================================= + * FILENAME: include/mcp/arginfo/index.h + * PROJECT: king + * + * PURPOSE: + * Arginfo aggregation point for MCP PHP entry points. + * ========================================================================= + */ + +#ifndef KING_MCP_ARGINFO_INDEX_H +#define KING_MCP_ARGINFO_INDEX_H + +#include "arginfo.h" + +#endif /* KING_MCP_ARGINFO_INDEX_H */ diff --git a/extension/include/mcp/class_entries.h b/extension/include/mcp/class_entries.h new file mode 100644 index 000000000..7d242af43 --- /dev/null +++ b/extension/include/mcp/class_entries.h @@ -0,0 +1,18 @@ +/* + * include/mcp/class_entries.h - MCP class-entry externs + */ + +#ifndef KING_MCP_CLASS_ENTRIES_H +#define KING_MCP_CLASS_ENTRIES_H + +#include + +extern zend_class_entry *king_ce_mcp; +extern zend_class_entry *king_ce_mcp_server; +extern zend_class_entry *king_ce_mcp_exception; +extern zend_class_entry *king_ce_mcp_connection_error; +extern zend_class_entry *king_ce_mcp_protocol_error; +extern zend_class_entry *king_ce_mcp_timeout; +extern zend_class_entry *king_ce_mcp_data_error; + +#endif /* KING_MCP_CLASS_ENTRIES_H */ diff --git a/extension/include/mcp/class_method_entries.h b/extension/include/mcp/class_method_entries.h new file mode 100644 index 000000000..bd651c967 --- /dev/null +++ b/extension/include/mcp/class_method_entries.h @@ -0,0 +1,19 @@ +const zend_function_entry king_mcp_class_methods[] = { + PHP_ME(King_MCP, __construct, arginfo_class_King_MCP___construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) + PHP_ME(King_MCP, request, arginfo_class_King_MCP_request, ZEND_ACC_PUBLIC) + PHP_ME(King_MCP, requestAsync, arginfo_class_King_MCP_requestAsync, ZEND_ACC_PUBLIC) + PHP_ME(King_MCP, requestIibin, arginfo_class_King_MCP_requestIibin, ZEND_ACC_PUBLIC) + PHP_ME(King_MCP, requestIibinAsync, arginfo_class_King_MCP_requestIibinAsync, ZEND_ACC_PUBLIC) + PHP_ME(King_MCP, uploadFromStream, arginfo_class_King_MCP_uploadFromStream, ZEND_ACC_PUBLIC) + PHP_ME(King_MCP, downloadToStream, arginfo_class_King_MCP_downloadToStream, ZEND_ACC_PUBLIC) + PHP_ME(King_MCP, close, arginfo_class_King_MCP_close, ZEND_ACC_PUBLIC) + PHP_FE_END +}; + +const zend_function_entry king_mcp_server_class_methods[] = { + PHP_ME(King_MCPServer, __construct, arginfo_class_King_MCPServer___construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) + PHP_ME(King_MCPServer, handleJsonRpc, arginfo_class_King_MCPServer_handleJsonRpc, ZEND_ACC_PUBLIC) + PHP_ME(King_MCPServer, handleHttp, arginfo_class_King_MCPServer_handleHttp, ZEND_ACC_PUBLIC) + PHP_ME(King_MCPServer, runStdio, arginfo_class_King_MCPServer_runStdio, ZEND_ACC_PUBLIC) + PHP_FE_END +}; diff --git a/extension/include/mcp/class_methods.h b/extension/include/mcp/class_methods.h new file mode 100644 index 000000000..ded1d740b --- /dev/null +++ b/extension/include/mcp/class_methods.h @@ -0,0 +1,13 @@ +/* + * include/mcp/class_methods.h - MCP OO method table externs + */ + +#ifndef KING_MCP_CLASS_METHODS_H +#define KING_MCP_CLASS_METHODS_H + +#include + +extern const zend_function_entry king_mcp_class_methods[]; +extern const zend_function_entry king_mcp_server_class_methods[]; + +#endif /* KING_MCP_CLASS_METHODS_H */ diff --git a/extension/include/mcp/function_entries.h b/extension/include/mcp/function_entries.h new file mode 100644 index 000000000..2012f0a38 --- /dev/null +++ b/extension/include/mcp/function_entries.h @@ -0,0 +1,13 @@ + PHP_FE(king_mcp_connect, arginfo_king_mcp_connect) + PHP_FE(king_mcp_request, arginfo_king_mcp_request) + PHP_FE(king_mcp_request_async, arginfo_king_mcp_request_async) + PHP_FE(king_mcp_request_iibin, arginfo_king_mcp_request_iibin) + PHP_FE(king_mcp_request_iibin_async, arginfo_king_mcp_request_iibin_async) + PHP_FE(king_mcp_server_create, arginfo_king_mcp_server_create) + PHP_FE(king_mcp_server_handle_jsonrpc, arginfo_king_mcp_server_handle_jsonrpc) + PHP_FE(king_mcp_server_handle_http, arginfo_king_mcp_server_handle_http) + PHP_FE(king_mcp_server_stdio, arginfo_king_mcp_server_stdio) + PHP_FE(king_mcp_upload_from_stream, arginfo_king_mcp_upload_from_stream) + PHP_FE(king_mcp_download_to_stream, arginfo_king_mcp_download_to_stream) + PHP_FE(king_mcp_close, arginfo_king_mcp_close) + PHP_FE(king_mcp_get_error, arginfo_king_no_args) diff --git a/extension/include/mcp/index.h b/extension/include/mcp/index.h new file mode 100644 index 000000000..2021e72de --- /dev/null +++ b/extension/include/mcp/index.h @@ -0,0 +1,19 @@ +/* + * ========================================================================= + * FILENAME: include/mcp/index.h + * PROJECT: king + * + * PURPOSE: + * Public umbrella header for the MCP subsystem. + * ========================================================================= + */ + +#ifndef KING_MCP_INDEX_H +#define KING_MCP_INDEX_H + +#include "class_entries.h" +#include "mcp.h" +#include "registration.h" +#include "resource_ids.h" + +#endif /* KING_MCP_INDEX_H */ diff --git a/extension/include/mcp/mcp.h b/extension/include/mcp/mcp.h old mode 100755 new mode 100644 index f5701d5a1..e9e73a848 --- a/extension/include/mcp/mcp.h +++ b/extension/include/mcp/mcp.h @@ -4,16 +4,19 @@ * PROJECT: king * * PURPOSE: - * Native MCP runtime primitives shared by the PHP resource/object wrapper - * and the subsystem-local transport helpers. The runtime keeps one normalized - * remote peer target, one reusable socket stream, and the local persisted - * transfer-state fallback used by upload/download acknowledgements. + * Native MCP runtime contracts shared by the public JSON-RPC/SSE/stdio server + * wrapper and the King-internal peer client. The public server surface is + * represented by King\MCPServer and king_mcp_server_*; the internal peer + * surface keeps one normalized remote target, one reusable socket stream, and + * the local persisted transfer-state fallback used by request/upload/download + * operations. * ========================================================================= */ #ifndef KING_MCP_H #define KING_MCP_H #include "php.h" +#include #include typedef enum _king_mcp_error_kind { @@ -26,7 +29,9 @@ typedef enum _king_mcp_error_kind { typedef struct _king_mcp_state { zend_string *host; zend_long port; - zval config; /* Optional King\Config snapshot copied at connect time. */ + zend_long default_timeout_ms; + zval config; /* Optional config holder retained for the connection state. */ + zval iibin_routes; /* Immutable per-connection MCP IIBIN route snapshot. */ php_stream *transport_stream; bool closed; bool operation_active; @@ -40,6 +45,46 @@ typedef struct _king_mcp_runtime_control { zval *cancel_token; /* Optional King\CancelToken zval. */ } king_mcp_runtime_control_t; +typedef struct _king_mcp_object { + zval resource; + zend_object std; +} king_mcp_object; + +typedef struct _king_mcp_server_object { + zval definition; + zend_object std; +} king_mcp_server_object; + +static inline king_mcp_object * +php_king_mcp_obj_from_zend(zend_object *obj) +{ + return (king_mcp_object *) + ((char*)obj - XtOffsetOf(king_mcp_object, std)); +} + +static inline king_mcp_server_object * +php_king_mcp_server_obj_from_zend(zend_object *obj) +{ + return (king_mcp_server_object *) + ((char*)obj - XtOffsetOf(king_mcp_server_object, std)); +} + +/* --- PHP Function Prototypes --- */ + +PHP_FUNCTION(king_mcp_get_error); +PHP_FUNCTION(king_mcp_connect); +PHP_FUNCTION(king_mcp_request); +PHP_FUNCTION(king_mcp_request_async); +PHP_FUNCTION(king_mcp_request_iibin); +PHP_FUNCTION(king_mcp_request_iibin_async); +PHP_FUNCTION(king_mcp_server_create); +PHP_FUNCTION(king_mcp_server_handle_jsonrpc); +PHP_FUNCTION(king_mcp_server_handle_http); +PHP_FUNCTION(king_mcp_server_stdio); +PHP_FUNCTION(king_mcp_upload_from_stream); +PHP_FUNCTION(king_mcp_download_to_stream); +PHP_FUNCTION(king_mcp_close); + /* Connection State Lifecycle */ king_mcp_state *king_mcp_state_create(const char *host, size_t host_len, zend_long port, zval *config); void king_mcp_state_close(king_mcp_state *state); diff --git a/extension/include/mcp/registration.h b/extension/include/mcp/registration.h new file mode 100644 index 000000000..f34d581b9 --- /dev/null +++ b/extension/include/mcp/registration.h @@ -0,0 +1,16 @@ +/* + * include/mcp/registration.h - MCP MINIT registration hooks + */ + +#ifndef KING_MCP_REGISTRATION_H +#define KING_MCP_REGISTRATION_H + +#include +#include "class_methods.h" + +void king_mcp_register_resource_types(int module_number); +void king_mcp_register_exception_classes(void); +void king_mcp_register_classes(void); +void king_mcp_init_object_handlers(void); + +#endif /* KING_MCP_REGISTRATION_H */ diff --git a/extension/include/mcp/resource_ids.h b/extension/include/mcp/resource_ids.h new file mode 100644 index 000000000..9339e67fc --- /dev/null +++ b/extension/include/mcp/resource_ids.h @@ -0,0 +1,10 @@ +/* + * include/mcp/resource_ids.h - MCP Zend resource-id externs + */ + +#ifndef KING_MCP_RESOURCE_IDS_H +#define KING_MCP_RESOURCE_IDS_H + +extern int le_king_mcp; + +#endif /* KING_MCP_RESOURCE_IDS_H */ diff --git a/extension/include/media/arginfo/arginfo.h b/extension/include/media/arginfo/arginfo.h new file mode 100644 index 000000000..3a3dbee96 --- /dev/null +++ b/extension/include/media/arginfo/arginfo.h @@ -0,0 +1,68 @@ +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_rtp_bind, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, host, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, port, IS_LONG, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_rtp_ice_credentials, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, socket, IS_MIXED, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_rtp_dtls_fingerprint, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, socket, IS_MIXED, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_rtp_dtls_accept, 0, 0, 3) + ZEND_ARG_TYPE_INFO(0, socket, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, ip, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, port, IS_LONG, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 0, "5000") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_rtp_recv, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, socket, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 0, "0") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_rtp_send, 0, 0, 4) + ZEND_ARG_TYPE_INFO(0, socket, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, host, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, port, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, data, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_rtp_close, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, socket, IS_MIXED, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_RTP_Socket___construct, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, host, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, port, IS_LONG, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_RTP_Socket_iceCredentials, 0, 0, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_RTP_Socket_dtlsFingerprint, 0, 0, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_RTP_Socket_acceptDtls, 0, 2, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, ip, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, port, IS_LONG, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 0, "5000") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_RTP_Socket_receive, 0, 0, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 0, "0") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_RTP_Socket_send, 0, 3, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, host, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, port, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, data, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_RTP_Socket_close, 0, 0, IS_VOID, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_RTP_Socket_isClosed, 0, 0, _IS_BOOL, 0) +ZEND_END_ARG_INFO() diff --git a/extension/include/media/arginfo/index.h b/extension/include/media/arginfo/index.h new file mode 100644 index 000000000..71b4b23e7 --- /dev/null +++ b/extension/include/media/arginfo/index.h @@ -0,0 +1,16 @@ +/* + * ========================================================================= + * FILENAME: include/media/arginfo/index.h + * PROJECT: king + * + * PURPOSE: + * Arginfo aggregation point for media and RTP PHP entry points. + * ========================================================================= + */ + +#ifndef KING_MEDIA_ARGINFO_INDEX_H +#define KING_MEDIA_ARGINFO_INDEX_H + +#include "arginfo.h" + +#endif /* KING_MEDIA_ARGINFO_INDEX_H */ diff --git a/extension/include/media/class_entries.h b/extension/include/media/class_entries.h new file mode 100644 index 000000000..21bcd906d --- /dev/null +++ b/extension/include/media/class_entries.h @@ -0,0 +1,12 @@ +/* + * include/media/class_entries.h - Media class-entry externs + */ + +#ifndef KING_MEDIA_CLASS_ENTRIES_H +#define KING_MEDIA_CLASS_ENTRIES_H + +#include + +extern zend_class_entry *king_ce_rtp_socket; + +#endif /* KING_MEDIA_CLASS_ENTRIES_H */ diff --git a/extension/include/media/class_method_entries.h b/extension/include/media/class_method_entries.h new file mode 100644 index 000000000..1dfa58ca3 --- /dev/null +++ b/extension/include/media/class_method_entries.h @@ -0,0 +1,11 @@ +const zend_function_entry king_rtp_socket_class_methods[] = { + PHP_ME(King_RTP_Socket, __construct, arginfo_class_King_RTP_Socket___construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) + PHP_ME(King_RTP_Socket, iceCredentials, arginfo_class_King_RTP_Socket_iceCredentials, ZEND_ACC_PUBLIC) + PHP_ME(King_RTP_Socket, dtlsFingerprint, arginfo_class_King_RTP_Socket_dtlsFingerprint, ZEND_ACC_PUBLIC) + PHP_ME(King_RTP_Socket, acceptDtls, arginfo_class_King_RTP_Socket_acceptDtls, ZEND_ACC_PUBLIC) + PHP_ME(King_RTP_Socket, receive, arginfo_class_King_RTP_Socket_receive, ZEND_ACC_PUBLIC) + PHP_ME(King_RTP_Socket, send, arginfo_class_King_RTP_Socket_send, ZEND_ACC_PUBLIC) + PHP_ME(King_RTP_Socket, close, arginfo_class_King_RTP_Socket_close, ZEND_ACC_PUBLIC) + PHP_ME(King_RTP_Socket, isClosed, arginfo_class_King_RTP_Socket_isClosed, ZEND_ACC_PUBLIC) + PHP_FE_END +}; diff --git a/extension/include/media/class_methods.h b/extension/include/media/class_methods.h new file mode 100644 index 000000000..52bf50dc6 --- /dev/null +++ b/extension/include/media/class_methods.h @@ -0,0 +1,12 @@ +/* + * include/media/class_methods.h - Media OO method table externs + */ + +#ifndef KING_MEDIA_CLASS_METHODS_H +#define KING_MEDIA_CLASS_METHODS_H + +#include + +extern const zend_function_entry king_rtp_socket_class_methods[]; + +#endif /* KING_MEDIA_CLASS_METHODS_H */ diff --git a/extension/include/media/function_entries.h b/extension/include/media/function_entries.h new file mode 100644 index 000000000..8e19ac881 --- /dev/null +++ b/extension/include/media/function_entries.h @@ -0,0 +1,7 @@ + PHP_FE(king_rtp_bind, arginfo_king_rtp_bind) + PHP_FE(king_rtp_ice_credentials, arginfo_king_rtp_ice_credentials) + PHP_FE(king_rtp_dtls_fingerprint, arginfo_king_rtp_dtls_fingerprint) + PHP_FE(king_rtp_dtls_accept, arginfo_king_rtp_dtls_accept) + PHP_FE(king_rtp_recv, arginfo_king_rtp_recv) + PHP_FE(king_rtp_send, arginfo_king_rtp_send) + PHP_FE(king_rtp_close, arginfo_king_rtp_close) diff --git a/extension/include/media/index.h b/extension/include/media/index.h new file mode 100644 index 000000000..e62c239a8 --- /dev/null +++ b/extension/include/media/index.h @@ -0,0 +1,18 @@ +/* + * ========================================================================= + * FILENAME: include/media/index.h + * PROJECT: king + * + * PURPOSE: + * Public umbrella header for media and RTP entry points. + * ========================================================================= + */ + +#ifndef KING_MEDIA_INDEX_H +#define KING_MEDIA_INDEX_H + +#include "class_entries.h" +#include "rtp.h" +#include "registration.h" + +#endif /* KING_MEDIA_INDEX_H */ diff --git a/extension/include/media/registration.h b/extension/include/media/registration.h new file mode 100644 index 000000000..f2f977027 --- /dev/null +++ b/extension/include/media/registration.h @@ -0,0 +1,14 @@ +/* + * include/media/registration.h - Media MINIT hooks + */ + +#ifndef KING_MEDIA_REGISTRATION_H +#define KING_MEDIA_REGISTRATION_H + +#include +#include "class_methods.h" + +void king_media_register_classes(void); +void king_media_init_object_handlers(void); + +#endif /* KING_MEDIA_REGISTRATION_H */ diff --git a/extension/include/rtp.h b/extension/include/media/rtp.h similarity index 80% rename from extension/include/rtp.h rename to extension/include/media/rtp.h index d7e3af084..d1fd32ef2 100644 --- a/extension/include/rtp.h +++ b/extension/include/media/rtp.h @@ -1,5 +1,5 @@ /* - * king_rtp — RTP/ICE-lite/DTLS-SRTP SFU layer for the King extension. + * include/media/rtp.h - RTP/ICE-lite/DTLS-SRTP SFU layer for King * * PHP surface: * king_rtp_bind(host, port) → rtp_socket resource @@ -14,6 +14,9 @@ #ifndef KING_MEDIA_RTP_H #define KING_MEDIA_RTP_H +#include +#include +#include #include #include #include @@ -97,6 +100,33 @@ typedef struct { socklen_t from_len; } king_rtp_packet_t; +typedef struct { + uint32_t ssrc; + uint8_t payload_type; + uint8_t marker; + uint16_t seq; + uint32_t timestamp; + uint8_t data[KING_RTP_MTU]; + size_t data_len; + char from_ip[46]; + int from_port; + int ice_consented; + int srtp; +} king_rtp_receive_result_t; + +typedef struct _king_rtp_socket_object { + zval resource; + bool closed; + zend_object std; +} king_rtp_socket_object; + +static inline king_rtp_socket_object * +php_king_rtp_socket_obj_from_zend(zend_object *obj) +{ + return (king_rtp_socket_object *) + ((char*)obj - XtOffsetOf(king_rtp_socket_object, std)); +} + /* ── Internal API ───────────────────────────────────────────────────────── */ king_rtp_socket_t *king_rtp_socket_create(const char *host, int port, @@ -116,6 +146,14 @@ king_rtp_peer_t *king_rtp_peer_get(king_rtp_socket_t *sock, int king_rtp_dtls_do_accept(king_rtp_socket_t *sock, const char *peer_ip, int peer_port, int timeout_ms, char *errbuf, size_t errbuf_len); +int king_rtp_socket_recv_packet(king_rtp_socket_t *sock, + int timeout_ms, + king_rtp_receive_result_t *out); +int king_rtp_socket_send_packet(king_rtp_socket_t *sock, + const char *host, + int port, + const char *data, + size_t data_len); /* ── PHP resource type ──────────────────────────────────────────────────── */ extern int le_king_rtp_socket; diff --git a/extension/include/object_store/arginfo/arginfo.h b/extension/include/object_store/arginfo/arginfo.h new file mode 100644 index 000000000..b78a242a3 --- /dev/null +++ b/extension/include/object_store/arginfo/arginfo.h @@ -0,0 +1,60 @@ +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_object_lookup, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, object_id, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_object_store_put, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, object_id, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, data, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_object_store_put_from_stream, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, object_id, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, stream, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_object_store_get_to_stream, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, object_id, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, stream, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_object_store_begin_resumable_upload, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, object_id, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_object_store_append_resumable_upload_chunk, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, upload_id, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, stream, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_object_id, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, object_id, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_object_store_backup, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, object_id, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, destination_path, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_object_store_restore, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, object_id, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, source_path, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_object_store_all_objects_path, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, path, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_object_store_backup_all_objects, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, path, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_optional_object_id, 0, 0, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, object_id, IS_STRING, 1, "null") +ZEND_END_ARG_INFO() diff --git a/extension/include/object_store/arginfo/index.h b/extension/include/object_store/arginfo/index.h new file mode 100644 index 000000000..b2c8dff22 --- /dev/null +++ b/extension/include/object_store/arginfo/index.h @@ -0,0 +1,16 @@ +/* + * ========================================================================= + * FILENAME: include/object_store/arginfo/index.h + * PROJECT: king + * + * PURPOSE: + * Arginfo aggregation point for object-store PHP entry points. + * ========================================================================= + */ + +#ifndef KING_OBJECT_STORE_ARGINFO_INDEX_H +#define KING_OBJECT_STORE_ARGINFO_INDEX_H + +#include "arginfo.h" + +#endif /* KING_OBJECT_STORE_ARGINFO_INDEX_H */ diff --git a/extension/include/object_store/class_entries.h b/extension/include/object_store/class_entries.h new file mode 100644 index 000000000..46e99487c --- /dev/null +++ b/extension/include/object_store/class_entries.h @@ -0,0 +1,12 @@ +/* + * include/object_store/class_entries.h - Object store class-entry externs + */ + +#ifndef KING_OBJECT_STORE_CLASS_ENTRIES_H +#define KING_OBJECT_STORE_CLASS_ENTRIES_H + +#include + +extern zend_class_entry *king_ce_object_store; + +#endif /* KING_OBJECT_STORE_CLASS_ENTRIES_H */ diff --git a/extension/include/object_store/class_method_entries.h b/extension/include/object_store/class_method_entries.h new file mode 100644 index 000000000..c3294312e --- /dev/null +++ b/extension/include/object_store/class_method_entries.h @@ -0,0 +1,23 @@ +const zend_function_entry king_object_store_class_methods[] = { + ZEND_ME_MAPPING(init, king_object_store_init, arginfo_king_config_array, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(put, king_object_store_put, arginfo_king_object_store_put, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(putFromStream, king_object_store_put_from_stream, arginfo_king_object_store_put_from_stream, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(beginResumableUpload, king_object_store_begin_resumable_upload, arginfo_king_object_store_begin_resumable_upload, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(appendResumableUploadChunk, king_object_store_append_resumable_upload_chunk, arginfo_king_object_store_append_resumable_upload_chunk, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(completeResumableUpload, king_object_store_complete_resumable_upload, arginfo_king_object_id, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(abortResumableUpload, king_object_store_abort_resumable_upload, arginfo_king_object_id, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(getResumableUploadStatus, king_object_store_get_resumable_upload_status, arginfo_king_object_id, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(get, king_object_store_get, arginfo_king_object_lookup, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(getToStream, king_object_store_get_to_stream, arginfo_king_object_store_get_to_stream, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(delete, king_object_store_delete, arginfo_king_object_id, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(backupObject, king_object_store_backup_object, arginfo_king_object_store_backup, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(restoreObject, king_object_store_restore_object, arginfo_king_object_store_restore, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(backupAllObjects, king_object_store_backup_all_objects, arginfo_king_object_store_backup_all_objects, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(restoreAllObjects, king_object_store_restore_all_objects, arginfo_king_object_store_all_objects_path, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(listObjects, king_object_store_list, arginfo_king_no_args, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(getStats, king_object_store_get_stats, arginfo_king_no_args, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(optimize, king_object_store_optimize, arginfo_king_no_args, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(cleanupExpiredObjects, king_object_store_cleanup_expired_objects, arginfo_king_no_args, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(getMetadata, king_object_store_get_metadata, arginfo_king_object_id, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + PHP_FE_END +}; diff --git a/extension/include/object_store/class_methods.h b/extension/include/object_store/class_methods.h new file mode 100644 index 000000000..11540f8e6 --- /dev/null +++ b/extension/include/object_store/class_methods.h @@ -0,0 +1,12 @@ +/* + * include/object_store/class_methods.h - Object-store OO method table externs + */ + +#ifndef KING_OBJECT_STORE_CLASS_METHODS_H +#define KING_OBJECT_STORE_CLASS_METHODS_H + +#include + +extern const zend_function_entry king_object_store_class_methods[]; + +#endif /* KING_OBJECT_STORE_CLASS_METHODS_H */ diff --git a/extension/include/object_store/function_entries.h b/extension/include/object_store/function_entries.h new file mode 100644 index 000000000..d2b645fc7 --- /dev/null +++ b/extension/include/object_store/function_entries.h @@ -0,0 +1,23 @@ + PHP_FE(king_object_store_init, arginfo_king_config_array) + PHP_FE(king_object_store_put, arginfo_king_object_store_put) + PHP_FE(king_object_store_put_from_stream, arginfo_king_object_store_put_from_stream) + PHP_FE(king_object_store_begin_resumable_upload, arginfo_king_object_store_begin_resumable_upload) + PHP_FE(king_object_store_append_resumable_upload_chunk, arginfo_king_object_store_append_resumable_upload_chunk) + PHP_FE(king_object_store_complete_resumable_upload, arginfo_king_object_id) + PHP_FE(king_object_store_abort_resumable_upload, arginfo_king_object_id) + PHP_FE(king_object_store_get_resumable_upload_status, arginfo_king_object_id) + PHP_FE(king_object_store_get, arginfo_king_object_lookup) + PHP_FE(king_object_store_get_to_stream, arginfo_king_object_store_get_to_stream) + PHP_FE(king_object_store_delete, arginfo_king_object_id) + PHP_FE(king_object_store_backup_object, arginfo_king_object_store_backup) + PHP_FE(king_object_store_restore_object, arginfo_king_object_store_restore) + PHP_FE(king_object_store_backup_all_objects, arginfo_king_object_store_backup_all_objects) + PHP_FE(king_object_store_restore_all_objects, arginfo_king_object_store_all_objects_path) + PHP_FE(king_object_store_list, arginfo_king_no_args) + PHP_FE(king_object_store_get_stats, arginfo_king_no_args) + PHP_FE(king_object_store_optimize, arginfo_king_no_args) + PHP_FE(king_object_store_cleanup_expired_objects, arginfo_king_no_args) + PHP_FE(king_object_store_get_metadata, arginfo_king_object_id) + PHP_FE(king_cdn_cache_object, arginfo_king_object_lookup) + PHP_FE(king_cdn_invalidate_cache, arginfo_king_optional_object_id) + PHP_FE(king_cdn_get_edge_nodes, arginfo_king_no_args) diff --git a/extension/include/object_store/index.h b/extension/include/object_store/index.h new file mode 100644 index 000000000..2f1846862 --- /dev/null +++ b/extension/include/object_store/index.h @@ -0,0 +1,19 @@ +/* + * ========================================================================= + * FILENAME: include/object_store/index.h + * PROJECT: king + * + * PURPOSE: + * Public umbrella header for object-store and CDN primitives. + * ========================================================================= + */ + +#ifndef KING_OBJECT_STORE_INDEX_H +#define KING_OBJECT_STORE_INDEX_H + +#include "class_entries.h" +#include "lifecycle.h" +#include "object_store.h" +#include "registration.h" + +#endif /* KING_OBJECT_STORE_INDEX_H */ diff --git a/extension/include/object_store/lifecycle.h b/extension/include/object_store/lifecycle.h new file mode 100644 index 000000000..c74a47369 --- /dev/null +++ b/extension/include/object_store/lifecycle.h @@ -0,0 +1,12 @@ +/* + * include/object_store/lifecycle.h - Object-store module bootstrap hooks + */ + +#ifndef KING_OBJECT_STORE_LIFECYCLE_H +#define KING_OBJECT_STORE_LIFECYCLE_H + +int king_cdn_cache_registry_minit(void); +void king_cdn_cache_registry_mshutdown(void); +void king_object_store_request_shutdown(void); + +#endif /* KING_OBJECT_STORE_LIFECYCLE_H */ diff --git a/extension/include/object_store/object_store.h b/extension/include/object_store/object_store.h old mode 100755 new mode 100644 diff --git a/extension/include/object_store/object_store_internal.h b/extension/include/object_store/object_store_internal.h index 5ef4cf56b..a890de1c2 100644 --- a/extension/include/object_store/object_store_internal.h +++ b/extension/include/object_store/object_store_internal.h @@ -14,6 +14,7 @@ #include #include "object_store/object_store.h" +#include "object_store/lifecycle.h" #include #include "main/php_streams.h" @@ -204,6 +205,5 @@ int king_object_store_acquire_object_lock( size_t error_size ); void king_object_store_release_object_lock(int *lock_fd); -void king_object_store_request_shutdown(void); #endif /* KING_OBJECT_STORE_INTERNAL_H */ diff --git a/extension/include/object_store/registration.h b/extension/include/object_store/registration.h new file mode 100644 index 000000000..5bfce8dc7 --- /dev/null +++ b/extension/include/object_store/registration.h @@ -0,0 +1,13 @@ +/* + * include/object_store/registration.h - Object store MINIT hooks + */ + +#ifndef KING_OBJECT_STORE_REGISTRATION_H +#define KING_OBJECT_STORE_REGISTRATION_H + +#include +#include "class_methods.h" + +void king_object_store_register_classes(void); + +#endif /* KING_OBJECT_STORE_REGISTRATION_H */ diff --git a/extension/include/php_king.h b/extension/include/php_king.h index 338487f7b..e38a6082e 100644 --- a/extension/include/php_king.h +++ b/extension/include/php_king.h @@ -5,8 +5,8 @@ * * PURPOSE: * Central public header for the extension. It exposes the shared constants, - * resource identifiers, exception class entries, object wrappers, and core - * helper prototypes used across the active C sources. + * core object wrappers, and helper prototypes used across the active C + * sources. Bootstrap-owned extern declarations live under include/php_king. * ========================================================================= */ @@ -24,648 +24,29 @@ #include #include #include - -/* ----------------------------------------------------------------------------- - * Extension Version and Global Constants - */ -#ifndef PHP_KING_VERSION -# define PHP_KING_VERSION "1.0.9" -#endif - -#ifndef KING_MAX_TICKET_SIZE -# define KING_MAX_TICKET_SIZE 4096 -#endif - -#ifndef KING_TRANSPORT_INTERRUPT_SLICE_MS -# define KING_TRANSPORT_INTERRUPT_SLICE_MS 25L -#endif - -#define KING_INTERNAL_OPTION_CANCEL_TOKEN "__king_cancel_token" -#define KING_INTERNAL_OPTION_CANCEL_TOKEN_LEN (sizeof(KING_INTERNAL_OPTION_CANCEL_TOKEN) - 1) -#define KING_INTERNAL_OPTION_CANCEL_FUNCTION_NAME "__king_cancel_function_name" -#define KING_INTERNAL_OPTION_CANCEL_FUNCTION_NAME_LEN (sizeof(KING_INTERNAL_OPTION_CANCEL_FUNCTION_NAME) - 1) -#define KING_INTERNAL_OPTION_CANCEL_STREAM_STOPPED "__king_cancel_stream_stopped" -#define KING_INTERNAL_OPTION_CANCEL_STREAM_STOPPED_LEN (sizeof(KING_INTERNAL_OPTION_CANCEL_STREAM_STOPPED) - 1) +#include /* Include core headers required in every build. */ -#include "king_globals.h" -#include "king_init.h" -#include "client/session.h" - -/* - * Keep this header lightweight for the current v1 runtime surface so the - * extension can compile without pulling in the full native dependency graph. - */ -#ifndef KING_RUNTIME_BUILD -# include "client/cancel.h" -# include "client/tls.h" -# include "config/config.h" -# include "connect/connect.h" -# include "client/http3.h" -# include "poll/poll.h" -# include "websocket/websocket.h" -#endif /* KING_RUNTIME_BUILD */ - -#include "mcp/mcp.h" -#include "xslt/xslt.h" - - -/* ----------------------------------------------------------------------------- - * Exception Class Entry Declarations - */ -extern zend_class_entry - *king_ce_exception, - *king_ce_stream_exception, - *king_ce_invalid_state, - *king_ce_unknown_stream, - *king_ce_stream_blocked, - *king_ce_stream_limit, - *king_ce_final_size, - *king_ce_stream_stopped, - *king_ce_fin_expected, - *king_ce_invalid_fin_state, - *king_ce_done, - *king_ce_quic_exception, - *king_ce_congestion_control, - *king_ce_too_many_streams, - *king_ce_runtime_exception, - *king_ce_system_exception, - *king_ce_validation_exception, - *king_ce_timeout_exception, - *king_ce_network_exception, - *king_ce_tls_exception, - *king_ce_protocol_exception, - *king_ce_mcp_exception, - *king_ce_mcp_connection_error, - *king_ce_mcp_protocol_error, - *king_ce_mcp_timeout, - *king_ce_mcp_data_error, - *king_ce_ws_exception, - *king_ce_ws_connection_error, - *king_ce_ws_protocol_error, - *king_ce_ws_timeout, - *king_ce_ws_closed; - -extern zend_class_entry - *king_ce_cancel_token, - *king_ce_awaitable, - *king_ce_config, - *king_ce_session, - *king_ce_stream, - *king_ce_response, - *king_ce_mcp, - *king_ce_pipeline_orchestrator, - *king_ce_object_store, - *king_ce_autoscaling, - *king_ce_client_http, - *king_ce_client_http1, - *king_ce_client_http2, - *king_ce_client_http3, - *king_ce_ws_server, - *king_ce_ws_connection; - -/* ----------------------------------------------------------------------------- - * Resource Type Identifiers - */ -extern int le_king_session; -extern int le_king_cfg; -extern int le_king_perf; -extern int le_king_mcp; -extern int le_king_ws; -extern int le_king_request_context; - -typedef struct _king_http1_request_context king_http1_request_context; -typedef enum _king_ws_connection_state { - KING_WS_STATE_CONNECTING = 0, - KING_WS_STATE_OPEN = 1, - KING_WS_STATE_CLOSING = 2, - KING_WS_STATE_CLOSED = 3 -} king_ws_connection_state_t; - -typedef struct _king_ws_message { - zend_string *payload; - bool is_binary; - struct _king_ws_message *next; -} king_ws_message; - -typedef struct _king_ws_server_object king_ws_server_object; - -typedef struct _king_ws_state { - zend_string *url; - zend_string *connection_id; - zend_string *scheme; - zend_string *host; - zend_string *request_target; - php_stream *transport_stream; - zval config; - zval headers; - zend_long port; - zend_long max_payload_size; - zend_long max_queued_messages; - zend_long max_queued_bytes; - zend_long queued_message_count; - zend_long queued_bytes; - zend_long ping_interval_ms; - zend_long handshake_timeout_ms; - zend_long last_close_status_code; - king_ws_connection_state_t state; - king_ws_message *incoming_head; - king_ws_message *incoming_tail; - zend_string *last_close_reason; - zend_string *last_ping_payload; - bool secure; - bool server_endpoint; - bool server_local_only; - bool handshake_complete; - bool close_frame_sent; - bool closed; - king_ws_server_object *server_owner; -} king_ws_state; - -typedef enum _king_client_protocol_preference { - KING_CLIENT_PROTOCOL_AUTO = 0, - KING_CLIENT_PROTOCOL_HTTP1, - KING_CLIENT_PROTOCOL_HTTP2, - KING_CLIENT_PROTOCOL_HTTP3 -} king_client_protocol_preference_t; - -void king_http1_pool_request_shutdown(void); -void king_http1_pool_module_shutdown(void); -void king_http2_pool_request_shutdown(void); -void king_http2_pool_module_shutdown(void); -zend_result king_response_object_init_from_array(zval *target, zval *payload); -zend_result king_response_object_init_from_context( - zval *target, - zval *payload, - zval *request_context -); -void king_http1_request_context_free(king_http1_request_context *context); -zend_result king_http1_request_context_build_payload( - king_http1_request_context *context, - zval *payload, - const char *function_name -); -zend_result king_http1_request_context_read( - king_http1_request_context *context, - zend_long read_offset, - size_t length, - zend_string **chunk_out, - const char *function_name -); -zend_result king_http1_request_context_get_body( - king_http1_request_context *context, - zend_string **body_out, - const char *function_name -); -zend_result king_http1_request_context_append_early_hint( - king_http1_request_context *context, - zval *hint, - const char *function_name -); -zend_bool king_telemetry_build_trace_context_snapshot(zval *destination); -zend_result king_http1_request_context_get_pending_early_hints( - king_http1_request_context *context, - zval *return_value -); -bool king_http1_request_context_is_end_of_body( - king_http1_request_context *context, - zend_long read_offset -); -void king_ws_server_registry_detach( - king_ws_server_object *server, - king_ws_state *state -); -void king_ws_state_free(king_ws_state *state); -zend_result king_server_cancel_invoke_if_registered( - king_client_session_t *session, - zend_long stream_id -); - -/* ----------------------------------------------------------------------------- - * Zend Object Wrappers for PHP Classes - */ -typedef struct _king_config_object { - zval resource; - zval overrides; - zend_object std; -} king_config_object; - -typedef struct _king_cancel_token_object { - bool cancelled; - zend_object std; -} king_cancel_token_object; - -typedef enum _king_awaitable_status { - KING_AWAITABLE_PENDING = 0, - KING_AWAITABLE_RESOLVED = 1, - KING_AWAITABLE_REJECTED = 2, - KING_AWAITABLE_CANCELLED = 3 -} king_awaitable_status_t; - -typedef struct _king_awaitable_object king_awaitable_object; -typedef zend_result (*king_awaitable_runner)(king_awaitable_object *intern, zval *result); - -struct _king_awaitable_object { - king_awaitable_status_t status; - zend_string *operation; - zval payload; - zval result; - zval error; - zval cancel_token; - king_awaitable_runner runner; - bool started; - bool cancel_requested; - zend_object std; -}; - -typedef struct _king_response_object { - zval payload; - zval request_context; - zend_long read_offset; - zend_object std; -} king_response_object; - -typedef struct _king_http_client_object { - zval config; - king_client_protocol_preference_t preferred_protocol; - bool closed; - zend_object std; -} king_http_client_object; - -typedef struct _king_session_object { - zval resource; - zval config; - zend_object std; -} king_session_object; - -typedef struct _king_stream_object { - zval session; - zval cancel_token; - zval connection_config; - zval request_headers; - zend_string *request_method; - zend_string *request_path; - zend_string *request_body; - zend_long stream_id; - zend_long buffered_bytes; - bool request_body_was_supplied; - bool finished; - bool closed; - bool response_started; - zend_object std; -} king_stream_object; - -/* king_mcp_state is now in include/mcp/mcp.h */ - -typedef struct _king_mcp_object { - zval resource; - zend_object std; -} king_mcp_object; - -typedef struct _king_ws_object { - zval resource; - zend_object std; -} king_ws_object; - -struct _king_ws_server_object { - zval config; - zend_string *host; - zend_long port; - int listener_fd; - HashTable connections; - zend_ulong next_connection_sequence; - bool registry_initialized; - bool closed; - zend_object std; -}; - -/* ----------------------------------------------------------------------------- - * Shared Error Buffer - */ -#ifndef KING_ERR_LEN -# define KING_ERR_LEN 256 -#endif - -#if defined(ZTS) && (PHP_VERSION_ID < 80200) -# include - extern ZEND_TLS char king_last_error[KING_ERR_LEN]; -#else - extern char king_last_error[KING_ERR_LEN]; -#endif - -void king_set_error(const char *msg); -const char *king_get_error(void); -int king_system_require_admission(const char *function_name, const char *admission_name); -void king_add_runtime_surface(zval *target); -const char *king_get_active_runtime_summary(void); -const char *king_get_stubbed_api_summary(void); - -/* ----------------------------------------------------------------------------- - * Inline Helpers for Accessing Native Resources - */ -static inline void king_secure_zero(void *v, size_t n) -{ - volatile unsigned char *p = (volatile unsigned char *) v; - while (n--) *p++ = 0; -} -static inline king_config_object * -php_king_config_obj_from_zend(zend_object *obj) -{ - return (king_config_object *) - ((char*)obj - XtOffsetOf(king_config_object, std)); -} - -static inline king_cancel_token_object * -php_king_cancel_token_obj_from_zend(zend_object *obj) -{ - return (king_cancel_token_object *) - ((char*)obj - XtOffsetOf(king_cancel_token_object, std)); -} - -static inline king_awaitable_object * -php_king_awaitable_obj_from_zend(zend_object *obj) -{ - return (king_awaitable_object *) - ((char*)obj - XtOffsetOf(king_awaitable_object, std)); -} - -#if PHP_VERSION_ID < 80200 -static inline bool king_zend_string_equals_cstr_compat( - const zend_string *value, - const char *literal, - size_t literal_len) -{ - return value != NULL - && ZSTR_LEN(value) == literal_len - && memcmp(ZSTR_VAL(value), literal, literal_len) == 0; -} - -#define zend_string_equals_cstr(value, literal, literal_len) \ - king_zend_string_equals_cstr_compat((value), (literal), (literal_len)) -#endif - -static inline bool king_zend_string_starts_with_cstr( - const zend_string *value, - const char *literal) -{ - if (value == NULL || literal == NULL) { - return 0; - } - - size_t literal_len = strlen(literal); - - return literal_len > 0 - && ZSTR_LEN(value) >= literal_len - && memcmp(ZSTR_VAL(value), literal, literal_len) == 0; -} - -static inline bool king_vm_interrupt_pending(void) -{ -#if PHP_VERSION_ID >= 80200 - return zend_atomic_bool_load_ex(&EG(vm_interrupt)); -#else - return EG(vm_interrupt); -#endif -} - -static inline void king_process_pending_interrupts(void) -{ - if (UNEXPECTED(king_vm_interrupt_pending())) { - if (zend_interrupt_function != NULL) { - zend_interrupt_function(EG(current_execute_data)); - } - } -} - -static inline zval *king_transport_cancel_token_from_options(zval *options_array) -{ - zval *option_value; - - if (options_array == NULL || Z_TYPE_P(options_array) != IS_ARRAY) { - return NULL; - } - - option_value = zend_hash_str_find( - Z_ARRVAL_P(options_array), - KING_INTERNAL_OPTION_CANCEL_TOKEN, - KING_INTERNAL_OPTION_CANCEL_TOKEN_LEN - ); - if (option_value == NULL - || Z_TYPE_P(option_value) != IS_OBJECT - || !instanceof_function(Z_OBJCE_P(option_value), king_ce_cancel_token)) { - return NULL; - } - - return option_value; -} - -static inline const char *king_transport_cancel_function_name_from_options(zval *options_array) -{ - zval *option_value; - - if (options_array == NULL || Z_TYPE_P(options_array) != IS_ARRAY) { - return NULL; - } - - option_value = zend_hash_str_find( - Z_ARRVAL_P(options_array), - KING_INTERNAL_OPTION_CANCEL_FUNCTION_NAME, - KING_INTERNAL_OPTION_CANCEL_FUNCTION_NAME_LEN - ); - if (option_value == NULL || Z_TYPE_P(option_value) != IS_STRING || Z_STRLEN_P(option_value) == 0) { - return NULL; - } - - return Z_STRVAL_P(option_value); -} - -static inline zend_class_entry *king_transport_cancel_exception_ce_from_options(zval *options_array) -{ - zval *option_value; - - if (options_array == NULL || Z_TYPE_P(options_array) != IS_ARRAY) { - return king_ce_runtime_exception; - } - - option_value = zend_hash_str_find( - Z_ARRVAL_P(options_array), - KING_INTERNAL_OPTION_CANCEL_STREAM_STOPPED, - KING_INTERNAL_OPTION_CANCEL_STREAM_STOPPED_LEN - ); - if (option_value != NULL && Z_TYPE_P(option_value) == IS_TRUE) { - return king_ce_stream_stopped; - } - - return king_ce_runtime_exception; -} - -static inline zend_bool king_transport_cancel_token_is_cancelled(zval *cancel_token) -{ - if (cancel_token == NULL - || Z_TYPE_P(cancel_token) != IS_OBJECT - || !instanceof_function(Z_OBJCE_P(cancel_token), king_ce_cancel_token)) { - return 0; - } - - return php_king_cancel_token_obj_from_zend(Z_OBJ_P(cancel_token))->cancelled ? 1 : 0; -} - -static inline zend_result king_transport_maybe_throw_cancel( - zval *cancel_token, - const char *function_name, - const char *cancel_function_name, - zend_class_entry *exception_ce, - const char *transport_label) -{ - char message[KING_ERR_LEN]; - const char *label; - - if (!king_transport_cancel_token_is_cancelled(cancel_token)) { - return SUCCESS; - } - - label = cancel_function_name != NULL ? cancel_function_name : function_name; - if (exception_ce == NULL) { - exception_ce = king_ce_runtime_exception; - } - - snprintf( - message, - sizeof(message), - "%s() cancelled the active %s transport via CancelToken.", - label, - transport_label - ); - king_set_error(message); - zend_throw_exception_ex(exception_ce, 0, "%s", message); - return FAILURE; -} - -static inline king_session_object * -php_king_obj_from_zend(zend_object *obj) -{ - return (king_session_object *) - ((char*)obj - XtOffsetOf(king_session_object, std)); -} - -static inline king_stream_object * -php_king_stream_obj_from_zend(zend_object *obj) -{ - return (king_stream_object *) - ((char*)obj - XtOffsetOf(king_stream_object, std)); -} - -static inline king_response_object * -php_king_response_obj_from_zend(zend_object *obj) -{ - return (king_response_object *) - ((char*)obj - XtOffsetOf(king_response_object, std)); -} - -static inline king_http_client_object * -php_king_http_client_obj_from_zend(zend_object *obj) -{ - return (king_http_client_object *) - ((char*)obj - XtOffsetOf(king_http_client_object, std)); -} - -static inline void *king_obj_fetch(zval *zobj) -{ - if (Z_TYPE_P(zobj) != IS_OBJECT) return NULL; - king_session_object *intern = php_king_obj_from_zend(Z_OBJ_P(zobj)); - if (Z_ISUNDEF(intern->resource) || Z_TYPE(intern->resource) != IS_RESOURCE) { - return NULL; - } - - return Z_RES(intern->resource)->ptr; -} - -static inline king_mcp_object * -php_king_mcp_obj_from_zend(zend_object *obj) -{ - return (king_mcp_object *) - ((char*)obj - XtOffsetOf(king_mcp_object, std)); -} - -static inline king_ws_object * -php_king_ws_obj_from_zend(zend_object *obj) -{ - return (king_ws_object *) - ((char*)obj - XtOffsetOf(king_ws_object, std)); -} - -static inline king_ws_server_object * -php_king_ws_server_obj_from_zend(zend_object *obj) -{ - return (king_ws_server_object *) - ((char*)obj - XtOffsetOf(king_ws_server_object, std)); -} - -extern void *king_fetch_config(zval *zcfg); -extern void king_ticket_ring_put(const uint8_t *ticket, size_t len); -extern int king_ticket_ring_get(uint8_t *out, size_t *out_len); -extern void king_client_session_free(void *session_ptr); -extern const zend_function_entry king_cancel_token_class_methods[]; -extern const zend_function_entry king_awaitable_class_methods[]; -extern const zend_function_entry king_config_class_methods[]; -extern const zend_function_entry king_session_class_methods[]; -extern const zend_function_entry king_stream_class_methods[]; -extern const zend_function_entry king_response_class_methods[]; -extern const zend_function_entry king_mcp_class_methods[]; -extern const zend_function_entry king_pipeline_orchestrator_class_methods[]; -extern const zend_function_entry king_object_store_class_methods[]; -extern const zend_function_entry king_autoscaling_class_methods[]; -extern const zend_function_entry king_http_client_class_methods[]; -extern const zend_function_entry king_ws_server_class_methods[]; -extern const zend_function_entry king_ws_connection_class_methods[]; - -zend_result king_awaitable_create( - zval *return_value, - const char *operation, - size_t operation_len, - king_awaitable_runner runner, - zval *payload, - zval *cancel_token -); -zend_result king_awaitable_create_function_call( - zval *return_value, - const char *operation, - size_t operation_len, - const char *function_name, - size_t function_name_len, - zval *params, - uint32_t param_count, - zval *cancel_token -); -zend_result king_awaitable_create_method_call( - zval *return_value, - const char *operation, - size_t operation_len, - zval *object, - const char *method_name, - size_t method_name_len, - zval *params, - uint32_t param_count, - zval *cancel_token -); - -/* ----------------------------------------------------------------------------- - * PHP_FUNCTION Prototypes: active public entry points - */ -PHP_FUNCTION(king_connect); -PHP_FUNCTION(king_close); -PHP_FUNCTION(king_send_request); -PHP_FUNCTION(king_receive_response); -PHP_FUNCTION(king_poll); -PHP_FUNCTION(king_cancel_stream); -PHP_FUNCTION(king_export_session_ticket); -PHP_FUNCTION(king_import_session_ticket); -PHP_FUNCTION(king_set_ca_file); -PHP_FUNCTION(king_set_client_cert); -PHP_FUNCTION(king_get_last_error); -PHP_FUNCTION(king_get_stats); -PHP_FUNCTION(king_version); -PHP_FUNCTION(king_health); -PHP_FUNCTION(king_db_ingest); +#include "php_king/index.h" + +#include "autoscaling/index.h" +#include "awaitable/index.h" +#include "client/index.h" +#include "config/index.h" +#include "db_ingest/index.h" +#include "iibin/index.h" +#include "inference/index.h" +#include "integration/index.h" +#include "king_init/ticket_ring.h" +#include "media/index.h" +#include "mcp/index.h" +#include "object_store/index.h" +#include "pipeline_orchestrator/index.h" +#include "runtime/index.h" +#include "semantic_dns/index.h" +#include "server/index.h" +#include "telemetry/index.h" +#include "validation/index.h" +#include "xslt/index.h" #endif /* PHP_KING_H */ diff --git a/extension/include/php_king/arginfo.h b/extension/include/php_king/arginfo.h new file mode 100644 index 000000000..44739a367 --- /dev/null +++ b/extension/include/php_king/arginfo.h @@ -0,0 +1,36 @@ +/* + * Aggregates the arginfo sets used by the extension entry unit. + * + * The concrete arginfo declarations live under extension/include in each + * owning subsystem. This header keeps php_king.c from reaching into every + * module directly. + */ +#ifndef KING_PHP_KING_ARGINFO_H +#define KING_PHP_KING_ARGINFO_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "core_arginfo.h" +#include "client/arginfo/index.h" +#include "awaitable/arginfo/index.h" +#include "db_ingest/arginfo/index.h" +#include "iibin/arginfo/index.h" +#include "inference/arginfo/index.h" +#include "integration/arginfo/index.h" +#include "media/arginfo/index.h" +#include "mcp/arginfo/index.h" +#include "object_store/arginfo/index.h" +#include "pipeline_orchestrator/arginfo/index.h" +#include "semantic_dns/arginfo/index.h" +#include "server/arginfo/index.h" +#include "telemetry/arginfo/index.h" +#include "autoscaling/arginfo/index.h" +#include "xslt/arginfo/index.h" + +#ifdef __cplusplus +} +#endif + +#endif /* KING_PHP_KING_ARGINFO_H */ diff --git a/extension/include/php_king/class_entries.h b/extension/include/php_king/class_entries.h new file mode 100644 index 000000000..0f22b8bc3 --- /dev/null +++ b/extension/include/php_king/class_entries.h @@ -0,0 +1,45 @@ +/* + * include/php_king/class_entries.h - Core class-entry externs and module aggregation + */ + +#ifndef KING_PHP_KING_CLASS_ENTRIES_H +#define KING_PHP_KING_CLASS_ENTRIES_H + +#include + +extern zend_class_entry + *king_ce_exception, + *king_ce_stream_exception, + *king_ce_invalid_state, + *king_ce_unknown_stream, + *king_ce_stream_blocked, + *king_ce_stream_limit, + *king_ce_final_size, + *king_ce_stream_stopped, + *king_ce_fin_expected, + *king_ce_invalid_fin_state, + *king_ce_done, + *king_ce_quic_exception, + *king_ce_congestion_control, + *king_ce_too_many_streams, + *king_ce_runtime_exception, + *king_ce_system_exception, + *king_ce_validation_exception, + *king_ce_timeout_exception, + *king_ce_network_exception, + *king_ce_tls_exception, + *king_ce_protocol_exception; + +#include "autoscaling/class_entries.h" +#include "awaitable/class_entries.h" +#include "client/class_entries.h" +#include "config/class_entries.h" +#include "inference/classes/class_entries.h" +#include "mcp/class_entries.h" +#include "media/class_entries.h" +#include "object_store/class_entries.h" +#include "pipeline_orchestrator/class_entries.h" +#include "server/class_entries.h" +#include "xslt/class_entries.h" + +#endif /* KING_PHP_KING_CLASS_ENTRIES_H */ diff --git a/extension/include/php_king/constants.h b/extension/include/php_king/constants.h new file mode 100644 index 000000000..3600981e7 --- /dev/null +++ b/extension/include/php_king/constants.h @@ -0,0 +1,20 @@ +/* + * include/php_king/constants.h - Core extension constants + */ + +#ifndef KING_PHP_KING_CONSTANTS_H +#define KING_PHP_KING_CONSTANTS_H + +#ifndef PHP_KING_VERSION +# define PHP_KING_VERSION "1.0.9" +#endif + +#ifndef KING_MAX_TICKET_SIZE +# define KING_MAX_TICKET_SIZE 4096 +#endif + +#ifndef KING_TRANSPORT_INTERRUPT_SLICE_MS +# define KING_TRANSPORT_INTERRUPT_SLICE_MS 25L +#endif + +#endif /* KING_PHP_KING_CONSTANTS_H */ diff --git a/extension/include/php_king/core_arginfo.h b/extension/include/php_king/core_arginfo.h new file mode 100644 index 000000000..2c053e5cf --- /dev/null +++ b/extension/include/php_king/core_arginfo.h @@ -0,0 +1,31 @@ +/* + * Shared reusable arginfo blocks for the King extension bootstrap and module + * binding fragments. + */ + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_no_args, 0, 0, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_one_string, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_one_long, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, value, IS_LONG, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_new_config, 0, 0, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_optional_headers, 0, 0, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, headers, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_headers, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, headers, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_config_array, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, config, IS_ARRAY, 0) +ZEND_END_ARG_INFO() diff --git a/extension/include/php_king/error_state.h b/extension/include/php_king/error_state.h new file mode 100644 index 000000000..a9890cac0 --- /dev/null +++ b/extension/include/php_king/error_state.h @@ -0,0 +1,28 @@ +/* + * include/php_king/error_state.h - Shared error buffer and runtime summary API + */ + +#ifndef KING_PHP_KING_ERROR_STATE_H +#define KING_PHP_KING_ERROR_STATE_H + +#include + +#ifndef KING_ERR_LEN +# define KING_ERR_LEN 256 +#endif + +#if defined(ZTS) && (PHP_VERSION_ID < 80200) +# include + extern ZEND_TLS char king_last_error[KING_ERR_LEN]; +#else + extern char king_last_error[KING_ERR_LEN]; +#endif + +void king_set_error(const char *msg); +const char *king_get_error(void); +int king_system_require_admission(const char *function_name, const char *admission_name); +void king_add_runtime_surface(zval *target); +const char *king_get_active_runtime_summary(void); +const char *king_get_stubbed_api_summary(void); + +#endif /* KING_PHP_KING_ERROR_STATE_H */ diff --git a/extension/include/king_globals.h b/extension/include/php_king/globals.h old mode 100755 new mode 100644 similarity index 87% rename from extension/include/king_globals.h rename to extension/include/php_king/globals.h index f419d11e6..856db3c20 --- a/extension/include/king_globals.h +++ b/extension/include/php_king/globals.h @@ -1,6 +1,6 @@ /* * ========================================================================= - * FILENAME: include/king_globals.h + * FILENAME: include/php_king/globals.h * PROJECT: king * * PURPOSE: @@ -9,8 +9,8 @@ * ========================================================================= */ -#ifndef KING_GLOBALS_H -#define KING_GLOBALS_H +#ifndef KING_PHP_KING_GLOBALS_H +#define KING_PHP_KING_GLOBALS_H #include #include @@ -36,4 +36,4 @@ typedef struct _king_globals_t { /* The single global state instance is defined in src/king_globals.c. */ extern ZEND_API king_globals_t king_globals; -#endif /* KING_GLOBALS_H */ +#endif /* KING_PHP_KING_GLOBALS_H */ diff --git a/extension/include/php_king/index.h b/extension/include/php_king/index.h new file mode 100644 index 000000000..3071e1d5e --- /dev/null +++ b/extension/include/php_king/index.h @@ -0,0 +1,17 @@ +/* + * include/php_king/index.h - Core extension public umbrella + */ + +#ifndef KING_PHP_KING_INDEX_H +#define KING_PHP_KING_INDEX_H + +#include "class_entries.h" +#include "constants.h" +#include "globals.h" +#include "public_functions.h" +#include "registration.h" +#include "resource_ids.h" +#include "runtime_contracts.h" +#include "runtime_helpers.h" + +#endif /* KING_PHP_KING_INDEX_H */ diff --git a/extension/include/king_init.h b/extension/include/php_king/init.h old mode 100755 new mode 100644 similarity index 90% rename from extension/include/king_init.h rename to extension/include/php_king/init.h index c04d4bd55..9b362d93f --- a/extension/include/king_init.h +++ b/extension/include/php_king/init.h @@ -1,6 +1,6 @@ /* * ========================================================================= - * FILENAME: include/king_init.h + * FILENAME: include/php_king/init.h * PROJECT: king * * PURPOSE: @@ -9,8 +9,8 @@ * ========================================================================= */ -#ifndef KING_INIT_H -#define KING_INIT_H +#ifndef KING_PHP_KING_INIT_H +#define KING_PHP_KING_INIT_H #include @@ -45,4 +45,4 @@ int king_request_init(int type, int module_number); */ int king_request_shutdown(int type, int module_number); -#endif /* KING_INIT_H */ +#endif /* KING_PHP_KING_INIT_H */ diff --git a/extension/include/php_king/interrupt_helpers.h b/extension/include/php_king/interrupt_helpers.h new file mode 100644 index 000000000..f8d78890d --- /dev/null +++ b/extension/include/php_king/interrupt_helpers.h @@ -0,0 +1,31 @@ +/* + * include/php_king/interrupt_helpers.h - Zend VM interrupt helper functions + */ + +#ifndef KING_PHP_KING_INTERRUPT_HELPERS_H +#define KING_PHP_KING_INTERRUPT_HELPERS_H + +#include +#include +#include +#include + +static inline bool king_vm_interrupt_pending(void) +{ +#if PHP_VERSION_ID >= 80200 + return zend_atomic_bool_load_ex(&EG(vm_interrupt)); +#else + return EG(vm_interrupt); +#endif +} + +static inline void king_process_pending_interrupts(void) +{ + if (UNEXPECTED(king_vm_interrupt_pending())) { + if (zend_interrupt_function != NULL) { + zend_interrupt_function(EG(current_execute_data)); + } + } +} + +#endif /* KING_PHP_KING_INTERRUPT_HELPERS_H */ diff --git a/extension/include/php_king/memory_helpers.h b/extension/include/php_king/memory_helpers.h new file mode 100644 index 000000000..f06e49c73 --- /dev/null +++ b/extension/include/php_king/memory_helpers.h @@ -0,0 +1,16 @@ +/* + * include/php_king/memory_helpers.h - Shared memory utility helpers + */ + +#ifndef KING_PHP_KING_MEMORY_HELPERS_H +#define KING_PHP_KING_MEMORY_HELPERS_H + +#include + +static inline void king_secure_zero(void *v, size_t n) +{ + volatile unsigned char *p = (volatile unsigned char *) v; + while (n--) *p++ = 0; +} + +#endif /* KING_PHP_KING_MEMORY_HELPERS_H */ diff --git a/extension/include/php_king/public_functions.h b/extension/include/php_king/public_functions.h new file mode 100644 index 000000000..e5f0770d4 --- /dev/null +++ b/extension/include/php_king/public_functions.h @@ -0,0 +1,14 @@ +/* + * include/php_king/public_functions.h - Core public PHP_FUNCTION prototypes + */ + +#ifndef KING_PHP_KING_PUBLIC_FUNCTIONS_H +#define KING_PHP_KING_PUBLIC_FUNCTIONS_H + +#include + +PHP_FUNCTION(king_get_last_error); +PHP_FUNCTION(king_version); +PHP_FUNCTION(king_health); + +#endif /* KING_PHP_KING_PUBLIC_FUNCTIONS_H */ diff --git a/extension/include/php_king/registration.h b/extension/include/php_king/registration.h new file mode 100644 index 000000000..92f928439 --- /dev/null +++ b/extension/include/php_king/registration.h @@ -0,0 +1,20 @@ +/* + * include/php_king/registration.h - Core class-registration bootstrap + */ + +#ifndef KING_PHP_KING_REGISTRATION_H +#define KING_PHP_KING_REGISTRATION_H + +#include + +zend_class_entry *king_register_class_with_flags( + const char *name, + zend_class_entry *parent, + const zend_function_entry *functions, + uint32_t flags); +zend_class_entry *king_register_exception( + const char *name, + zend_class_entry *parent); +void king_register_php_classes_and_handlers(void); + +#endif /* KING_PHP_KING_REGISTRATION_H */ diff --git a/extension/include/php_king/resource_ids.h b/extension/include/php_king/resource_ids.h new file mode 100644 index 000000000..e5b5b7374 --- /dev/null +++ b/extension/include/php_king/resource_ids.h @@ -0,0 +1,12 @@ +/* + * include/php_king/resource_ids.h - Zend resource-id aggregation + */ + +#ifndef KING_PHP_KING_RESOURCE_IDS_H +#define KING_PHP_KING_RESOURCE_IDS_H + +#include "client/resource_ids.h" +#include "config/resource_ids.h" +#include "mcp/resource_ids.h" + +#endif /* KING_PHP_KING_RESOURCE_IDS_H */ diff --git a/extension/include/php_king/resources.h b/extension/include/php_king/resources.h new file mode 100644 index 000000000..0be60c853 --- /dev/null +++ b/extension/include/php_king/resources.h @@ -0,0 +1,12 @@ +/* + * include/php_king/resources.h - Core Zend resource registration + */ + +#ifndef KING_PHP_KING_RESOURCES_H +#define KING_PHP_KING_RESOURCES_H + +#include + +void king_register_resource_types(int module_number); + +#endif /* KING_PHP_KING_RESOURCES_H */ diff --git a/extension/include/php_king/runtime_contracts.h b/extension/include/php_king/runtime_contracts.h new file mode 100644 index 000000000..b23fa179d --- /dev/null +++ b/extension/include/php_king/runtime_contracts.h @@ -0,0 +1,55 @@ +/* + * include/php_king/runtime_contracts.h - Cross-module runtime hook contracts + */ + +#ifndef KING_PHP_KING_RUNTIME_CONTRACTS_H +#define KING_PHP_KING_RUNTIME_CONTRACTS_H + +#include +#include + +#include "client/objects.h" +#include "client/session.h" + +void king_http1_pool_request_shutdown(void); +void king_http1_pool_module_shutdown(void); +void king_http2_pool_request_shutdown(void); +void king_http2_pool_module_shutdown(void); +void king_http1_request_context_free(king_http1_request_context *context); +zend_result king_http1_request_context_build_payload( + king_http1_request_context *context, + zval *payload, + const char *function_name +); +zend_result king_http1_request_context_read( + king_http1_request_context *context, + zend_long read_offset, + size_t length, + zend_string **chunk_out, + const char *function_name +); +zend_result king_http1_request_context_get_body( + king_http1_request_context *context, + zend_string **body_out, + const char *function_name +); +zend_result king_http1_request_context_append_early_hint( + king_http1_request_context *context, + zval *hint, + const char *function_name +); +zend_bool king_telemetry_build_trace_context_snapshot(zval *destination); +zend_result king_http1_request_context_get_pending_early_hints( + king_http1_request_context *context, + zval *return_value +); +bool king_http1_request_context_is_end_of_body( + king_http1_request_context *context, + zend_long read_offset +); +zend_result king_server_cancel_invoke_if_registered( + king_client_session_t *session, + zend_long stream_id +); + +#endif /* KING_PHP_KING_RUNTIME_CONTRACTS_H */ diff --git a/extension/include/php_king/runtime_helpers.h b/extension/include/php_king/runtime_helpers.h new file mode 100644 index 000000000..be0ef578c --- /dev/null +++ b/extension/include/php_king/runtime_helpers.h @@ -0,0 +1,14 @@ +/* + * include/php_king/runtime_helpers.h - Shared runtime helper umbrella + */ + +#ifndef KING_PHP_KING_RUNTIME_HELPERS_H +#define KING_PHP_KING_RUNTIME_HELPERS_H + +#include "error_state.h" +#include "memory_helpers.h" +#include "string_helpers.h" +#include "interrupt_helpers.h" +#include "transport_cancel.h" + +#endif /* KING_PHP_KING_RUNTIME_HELPERS_H */ diff --git a/extension/include/php_king/string_helpers.h b/extension/include/php_king/string_helpers.h new file mode 100644 index 000000000..b4728a45b --- /dev/null +++ b/extension/include/php_king/string_helpers.h @@ -0,0 +1,42 @@ +/* + * include/php_king/string_helpers.h - Shared zend_string helper functions + */ + +#ifndef KING_PHP_KING_STRING_HELPERS_H +#define KING_PHP_KING_STRING_HELPERS_H + +#include +#include +#include + +#if PHP_VERSION_ID < 80200 +static inline bool king_zend_string_equals_cstr_compat( + const zend_string *value, + const char *literal, + size_t literal_len) +{ + return value != NULL + && ZSTR_LEN(value) == literal_len + && memcmp(ZSTR_VAL(value), literal, literal_len) == 0; +} + +#define zend_string_equals_cstr(value, literal, literal_len) \ + king_zend_string_equals_cstr_compat((value), (literal), (literal_len)) +#endif + +static inline bool king_zend_string_starts_with_cstr( + const zend_string *value, + const char *literal) +{ + if (value == NULL || literal == NULL) { + return 0; + } + + size_t literal_len = strlen(literal); + + return literal_len > 0 + && ZSTR_LEN(value) >= literal_len + && memcmp(ZSTR_VAL(value), literal, literal_len) == 0; +} + +#endif /* KING_PHP_KING_STRING_HELPERS_H */ diff --git a/extension/include/php_king/transport_cancel.h b/extension/include/php_king/transport_cancel.h new file mode 100644 index 000000000..bf3fc3427 --- /dev/null +++ b/extension/include/php_king/transport_cancel.h @@ -0,0 +1,127 @@ +/* + * include/php_king/transport_cancel.h - Shared transport cancel-token helpers + */ + +#ifndef KING_PHP_KING_TRANSPORT_CANCEL_H +#define KING_PHP_KING_TRANSPORT_CANCEL_H + +#include +#include +#include + +#include "awaitable/cancel_token.h" +#include "class_entries.h" +#include "error_state.h" + +#define KING_INTERNAL_OPTION_CANCEL_TOKEN "__king_cancel_token" +#define KING_INTERNAL_OPTION_CANCEL_TOKEN_LEN (sizeof(KING_INTERNAL_OPTION_CANCEL_TOKEN) - 1) +#define KING_INTERNAL_OPTION_CANCEL_FUNCTION_NAME "__king_cancel_function_name" +#define KING_INTERNAL_OPTION_CANCEL_FUNCTION_NAME_LEN (sizeof(KING_INTERNAL_OPTION_CANCEL_FUNCTION_NAME) - 1) +#define KING_INTERNAL_OPTION_CANCEL_STREAM_STOPPED "__king_cancel_stream_stopped" +#define KING_INTERNAL_OPTION_CANCEL_STREAM_STOPPED_LEN (sizeof(KING_INTERNAL_OPTION_CANCEL_STREAM_STOPPED) - 1) + +static inline zval *king_transport_cancel_token_from_options(zval *options_array) +{ + zval *option_value; + + if (options_array == NULL || Z_TYPE_P(options_array) != IS_ARRAY) { + return NULL; + } + + option_value = zend_hash_str_find( + Z_ARRVAL_P(options_array), + KING_INTERNAL_OPTION_CANCEL_TOKEN, + KING_INTERNAL_OPTION_CANCEL_TOKEN_LEN + ); + if (option_value == NULL + || Z_TYPE_P(option_value) != IS_OBJECT + || !instanceof_function(Z_OBJCE_P(option_value), king_ce_cancel_token)) { + return NULL; + } + + return option_value; +} + +static inline const char *king_transport_cancel_function_name_from_options(zval *options_array) +{ + zval *option_value; + + if (options_array == NULL || Z_TYPE_P(options_array) != IS_ARRAY) { + return NULL; + } + + option_value = zend_hash_str_find( + Z_ARRVAL_P(options_array), + KING_INTERNAL_OPTION_CANCEL_FUNCTION_NAME, + KING_INTERNAL_OPTION_CANCEL_FUNCTION_NAME_LEN + ); + if (option_value == NULL || Z_TYPE_P(option_value) != IS_STRING || Z_STRLEN_P(option_value) == 0) { + return NULL; + } + + return Z_STRVAL_P(option_value); +} + +static inline zend_class_entry *king_transport_cancel_exception_ce_from_options(zval *options_array) +{ + zval *option_value; + + if (options_array == NULL || Z_TYPE_P(options_array) != IS_ARRAY) { + return king_ce_runtime_exception; + } + + option_value = zend_hash_str_find( + Z_ARRVAL_P(options_array), + KING_INTERNAL_OPTION_CANCEL_STREAM_STOPPED, + KING_INTERNAL_OPTION_CANCEL_STREAM_STOPPED_LEN + ); + if (option_value != NULL && Z_TYPE_P(option_value) == IS_TRUE) { + return king_ce_stream_stopped; + } + + return king_ce_runtime_exception; +} + +static inline zend_bool king_transport_cancel_token_is_cancelled(zval *cancel_token) +{ + if (cancel_token == NULL + || Z_TYPE_P(cancel_token) != IS_OBJECT + || !instanceof_function(Z_OBJCE_P(cancel_token), king_ce_cancel_token)) { + return 0; + } + + return php_king_cancel_token_obj_from_zend(Z_OBJ_P(cancel_token))->cancelled ? 1 : 0; +} + +static inline zend_result king_transport_maybe_throw_cancel( + zval *cancel_token, + const char *function_name, + const char *cancel_function_name, + zend_class_entry *exception_ce, + const char *transport_label) +{ + char message[KING_ERR_LEN]; + const char *label; + + if (!king_transport_cancel_token_is_cancelled(cancel_token)) { + return SUCCESS; + } + + label = cancel_function_name != NULL ? cancel_function_name : function_name; + if (exception_ce == NULL) { + exception_ce = king_ce_runtime_exception; + } + + snprintf( + message, + sizeof(message), + "%s() cancelled the active %s transport via CancelToken.", + label, + transport_label + ); + king_set_error(message); + zend_throw_exception_ex(exception_ce, 0, "%s", message); + return FAILURE; +} + +#endif /* KING_PHP_KING_TRANSPORT_CANCEL_H */ diff --git a/extension/include/php_king_arginfo.h b/extension/include/php_king_arginfo.h deleted file mode 100755 index f96e603c5..000000000 --- a/extension/include/php_king_arginfo.h +++ /dev/null @@ -1,24 +0,0 @@ -/* Aggregates the generated arginfo sets used by the extension. */ -#ifndef PHP_KING_ARGINFO_H -#define PHP_KING_ARGINFO_H - -#ifdef __cplusplus -extern "C" { -#endif - -#include "client/arginfo/index.h" -#include "iibin/arginfo/index.h" -#include "integration/arginfo/index.h" -#include "mcp/arginfo/index.h" -#include "object_store/arginfo/index.h" -#include "pipeline_orchestrator/arginfo/index.h" -#include "semantic_dns/arginfo/index.h" -#include "server/arginfo/index.h" -#include "telemetry/arginfo/index.h" -#include "autoscaling/arginfo/index.h" - -#ifdef __cplusplus -} -#endif - -#endif /* PHP_KING_ARGINFO_H */ diff --git a/extension/include/pipeline_orchestrator/arginfo/arginfo.h b/extension/include/pipeline_orchestrator/arginfo/arginfo.h new file mode 100644 index 000000000..b84f7057b --- /dev/null +++ b/extension/include/pipeline_orchestrator/arginfo/arginfo.h @@ -0,0 +1,25 @@ +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_pipeline_run, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, initial_data, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, pipeline, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, exec_options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_king_pipeline_run_async, 0, 2, King\\Awaitable, 0) + ZEND_ARG_TYPE_INFO(0, initial_data, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, pipeline, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, exec_options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_pipeline_register_tool, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, tool_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, config, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_pipeline_register_handler, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, tool_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, handler, IS_CALLABLE, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_pipeline_get_run, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, run_id, IS_STRING, 0) +ZEND_END_ARG_INFO() diff --git a/extension/include/pipeline_orchestrator/arginfo/index.h b/extension/include/pipeline_orchestrator/arginfo/index.h new file mode 100644 index 000000000..e8c0ebe0a --- /dev/null +++ b/extension/include/pipeline_orchestrator/arginfo/index.h @@ -0,0 +1,16 @@ +/* + * ========================================================================= + * FILENAME: include/pipeline_orchestrator/arginfo/index.h + * PROJECT: king + * + * PURPOSE: + * Arginfo aggregation point for pipeline orchestrator PHP entry points. + * ========================================================================= + */ + +#ifndef KING_PIPELINE_ORCHESTRATOR_ARGINFO_INDEX_H +#define KING_PIPELINE_ORCHESTRATOR_ARGINFO_INDEX_H + +#include "arginfo.h" + +#endif /* KING_PIPELINE_ORCHESTRATOR_ARGINFO_INDEX_H */ diff --git a/extension/include/pipeline_orchestrator/class_entries.h b/extension/include/pipeline_orchestrator/class_entries.h new file mode 100644 index 000000000..6af637a0b --- /dev/null +++ b/extension/include/pipeline_orchestrator/class_entries.h @@ -0,0 +1,12 @@ +/* + * include/pipeline_orchestrator/class_entries.h - Pipeline orchestrator class-entry externs + */ + +#ifndef KING_PIPELINE_ORCHESTRATOR_CLASS_ENTRIES_H +#define KING_PIPELINE_ORCHESTRATOR_CLASS_ENTRIES_H + +#include + +extern zend_class_entry *king_ce_pipeline_orchestrator; + +#endif /* KING_PIPELINE_ORCHESTRATOR_CLASS_ENTRIES_H */ diff --git a/extension/include/pipeline_orchestrator/class_method_entries.h b/extension/include/pipeline_orchestrator/class_method_entries.h new file mode 100644 index 000000000..8813d1c79 --- /dev/null +++ b/extension/include/pipeline_orchestrator/class_method_entries.h @@ -0,0 +1,14 @@ +const zend_function_entry king_pipeline_orchestrator_class_methods[] = { + ZEND_ME_MAPPING(run, king_pipeline_orchestrator_run, arginfo_king_pipeline_run, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(runAsync, king_pipeline_orchestrator_run_async, arginfo_king_pipeline_run_async, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(dispatch, king_pipeline_orchestrator_dispatch, arginfo_king_pipeline_run, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(dispatchAsync, king_pipeline_orchestrator_dispatch_async, arginfo_king_pipeline_run_async, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(registerTool, king_pipeline_orchestrator_register_tool, arginfo_king_pipeline_register_tool, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(registerHandler, king_pipeline_orchestrator_register_handler, arginfo_king_pipeline_register_handler, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(configureLogging, king_pipeline_orchestrator_configure_logging, arginfo_king_config_array, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(workerRunNext, king_pipeline_orchestrator_worker_run_next, arginfo_king_no_args, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(resumeRun, king_pipeline_orchestrator_resume_run, arginfo_king_pipeline_get_run, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(getRun, king_pipeline_orchestrator_get_run, arginfo_king_pipeline_get_run, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_ME_MAPPING(cancelRun, king_pipeline_orchestrator_cancel_run, arginfo_king_pipeline_get_run, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + PHP_FE_END +}; diff --git a/extension/include/pipeline_orchestrator/class_methods.h b/extension/include/pipeline_orchestrator/class_methods.h new file mode 100644 index 000000000..2b1c8ba8d --- /dev/null +++ b/extension/include/pipeline_orchestrator/class_methods.h @@ -0,0 +1,12 @@ +/* + * include/pipeline_orchestrator/class_methods.h - Pipeline orchestrator OO method table externs + */ + +#ifndef KING_PIPELINE_ORCHESTRATOR_CLASS_METHODS_H +#define KING_PIPELINE_ORCHESTRATOR_CLASS_METHODS_H + +#include + +extern const zend_function_entry king_pipeline_orchestrator_class_methods[]; + +#endif /* KING_PIPELINE_ORCHESTRATOR_CLASS_METHODS_H */ diff --git a/extension/include/pipeline_orchestrator/function_entries.h b/extension/include/pipeline_orchestrator/function_entries.h new file mode 100644 index 000000000..221e3365a --- /dev/null +++ b/extension/include/pipeline_orchestrator/function_entries.h @@ -0,0 +1,11 @@ + PHP_FE(king_pipeline_orchestrator_run, arginfo_king_pipeline_run) + PHP_FE(king_pipeline_orchestrator_run_async, arginfo_king_pipeline_run_async) + PHP_FE(king_pipeline_orchestrator_dispatch, arginfo_king_pipeline_run) + PHP_FE(king_pipeline_orchestrator_dispatch_async, arginfo_king_pipeline_run_async) + PHP_FE(king_pipeline_orchestrator_register_tool, arginfo_king_pipeline_register_tool) + PHP_FE(king_pipeline_orchestrator_register_handler, arginfo_king_pipeline_register_handler) + PHP_FE(king_pipeline_orchestrator_configure_logging, arginfo_king_config_array) + PHP_FE(king_pipeline_orchestrator_worker_run_next, arginfo_king_no_args) + PHP_FE(king_pipeline_orchestrator_resume_run, arginfo_king_pipeline_get_run) + PHP_FE(king_pipeline_orchestrator_get_run, arginfo_king_pipeline_get_run) + PHP_FE(king_pipeline_orchestrator_cancel_run, arginfo_king_pipeline_get_run) diff --git a/extension/include/pipeline_orchestrator/index.h b/extension/include/pipeline_orchestrator/index.h new file mode 100644 index 000000000..ae2d7ae0f --- /dev/null +++ b/extension/include/pipeline_orchestrator/index.h @@ -0,0 +1,20 @@ +/* + * ========================================================================= + * FILENAME: include/pipeline_orchestrator/index.h + * PROJECT: king + * + * PURPOSE: + * Public umbrella header for the pipeline orchestrator subsystem. + * ========================================================================= + */ + +#ifndef KING_PIPELINE_ORCHESTRATOR_INDEX_H +#define KING_PIPELINE_ORCHESTRATOR_INDEX_H + +#include "class_entries.h" +#include "orchestrator.h" +#include "pipeline_orchestrator.h" +#include "registration.h" +#include "tool_handler_registry.h" + +#endif /* KING_PIPELINE_ORCHESTRATOR_INDEX_H */ diff --git a/extension/include/pipeline_orchestrator/pipeline_orchestrator.h b/extension/include/pipeline_orchestrator/pipeline_orchestrator.h old mode 100755 new mode 100644 index 0994995c8..03735eb12 --- a/extension/include/pipeline_orchestrator/pipeline_orchestrator.h +++ b/extension/include/pipeline_orchestrator/pipeline_orchestrator.h @@ -40,9 +40,15 @@ typedef struct _king_pipeline_exec_options_c { /* Runs one pipeline immediately on the configured backend. */ PHP_FUNCTION(king_pipeline_orchestrator_run); +/* Starts one pipeline immediately and returns an awaitable handle. */ +PHP_FUNCTION(king_pipeline_orchestrator_run_async); + /* Queues one pipeline run for the file-worker backend. */ PHP_FUNCTION(king_pipeline_orchestrator_dispatch); +/* Queues one pipeline run and returns an awaitable handle for completion. */ +PHP_FUNCTION(king_pipeline_orchestrator_dispatch_async); + /* Registers or replaces one persisted tool configuration. */ PHP_FUNCTION(king_pipeline_orchestrator_register_tool); diff --git a/extension/include/pipeline_orchestrator/registration.h b/extension/include/pipeline_orchestrator/registration.h new file mode 100644 index 000000000..17ee54be9 --- /dev/null +++ b/extension/include/pipeline_orchestrator/registration.h @@ -0,0 +1,13 @@ +/* + * include/pipeline_orchestrator/registration.h - Pipeline orchestrator MINIT hooks + */ + +#ifndef KING_PIPELINE_ORCHESTRATOR_REGISTRATION_H +#define KING_PIPELINE_ORCHESTRATOR_REGISTRATION_H + +#include +#include "class_methods.h" + +void king_pipeline_orchestrator_register_classes(void); + +#endif /* KING_PIPELINE_ORCHESTRATOR_REGISTRATION_H */ diff --git a/extension/include/pipeline_orchestrator/tool_handler_registry.h b/extension/include/pipeline_orchestrator/tool_handler_registry.h old mode 100755 new mode 100644 diff --git a/extension/include/runtime/index.h b/extension/include/runtime/index.h new file mode 100644 index 000000000..66399e069 --- /dev/null +++ b/extension/include/runtime/index.h @@ -0,0 +1,17 @@ +/* + * ========================================================================= + * FILENAME: include/runtime/index.h + * PROJECT: king + * + * PURPOSE: + * Public umbrella header for optional runtime dependency discovery helpers. + * ========================================================================= + */ + +#ifndef KING_RUNTIME_INDEX_H +#define KING_RUNTIME_INDEX_H + +#include "libcurl_candidates.h" +#include "saxonc_candidates.h" + +#endif /* KING_RUNTIME_INDEX_H */ diff --git a/extension/include/semantic_dns/arginfo/arginfo.h b/extension/include/semantic_dns/arginfo/arginfo.h new file mode 100644 index 000000000..790e96abb --- /dev/null +++ b/extension/include/semantic_dns/arginfo/arginfo.h @@ -0,0 +1,28 @@ +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_service_discovery, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, service_type, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, criteria, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_service_lookup, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, service_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, client_info, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_register_service, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, service_info, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_register_mother_node, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, mother_node_info, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_update_service_status, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, service_id, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, status, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, metrics, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_semantic_dns_query, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, query, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, max_response_bytes, IS_LONG, 0, "256") +ZEND_END_ARG_INFO() diff --git a/extension/include/semantic_dns/arginfo/index.h b/extension/include/semantic_dns/arginfo/index.h new file mode 100644 index 000000000..d86a15ed2 --- /dev/null +++ b/extension/include/semantic_dns/arginfo/index.h @@ -0,0 +1,16 @@ +/* + * ========================================================================= + * FILENAME: include/semantic_dns/arginfo/index.h + * PROJECT: king + * + * PURPOSE: + * Arginfo aggregation point for Semantic DNS PHP entry points. + * ========================================================================= + */ + +#ifndef KING_SEMANTIC_DNS_ARGINFO_INDEX_H +#define KING_SEMANTIC_DNS_ARGINFO_INDEX_H + +#include "arginfo.h" + +#endif /* KING_SEMANTIC_DNS_ARGINFO_INDEX_H */ diff --git a/extension/include/semantic_dns/function_entries.h b/extension/include/semantic_dns/function_entries.h new file mode 100644 index 000000000..fdabada33 --- /dev/null +++ b/extension/include/semantic_dns/function_entries.h @@ -0,0 +1,9 @@ + PHP_FE(king_semantic_dns_init, arginfo_king_config_array) + PHP_FE(king_semantic_dns_start_server, arginfo_king_no_args) + PHP_FE(king_semantic_dns_query, arginfo_king_semantic_dns_query) + PHP_FE(king_semantic_dns_register_service, arginfo_king_register_service) + PHP_FE(king_semantic_dns_discover_service, arginfo_king_service_discovery) + PHP_FE(king_semantic_dns_register_mother_node, arginfo_king_register_mother_node) + PHP_FE(king_semantic_dns_get_optimal_route, arginfo_king_service_lookup) + PHP_FE(king_semantic_dns_update_service_status, arginfo_king_update_service_status) + PHP_FE(king_semantic_dns_get_service_topology, arginfo_king_no_args) diff --git a/extension/include/semantic_dns/index.h b/extension/include/semantic_dns/index.h new file mode 100644 index 000000000..405a8f484 --- /dev/null +++ b/extension/include/semantic_dns/index.h @@ -0,0 +1,17 @@ +/* + * ========================================================================= + * FILENAME: include/semantic_dns/index.h + * PROJECT: king + * + * PURPOSE: + * Public umbrella header for Semantic DNS primitives. + * ========================================================================= + */ + +#ifndef KING_SEMANTIC_DNS_INDEX_H +#define KING_SEMANTIC_DNS_INDEX_H + +#include "lifecycle.h" +#include "semantic_dns.h" + +#endif /* KING_SEMANTIC_DNS_INDEX_H */ diff --git a/extension/include/semantic_dns/lifecycle.h b/extension/include/semantic_dns/lifecycle.h new file mode 100644 index 000000000..408231b3c --- /dev/null +++ b/extension/include/semantic_dns/lifecycle.h @@ -0,0 +1,11 @@ +/* + * include/semantic_dns/lifecycle.h - Semantic-DNS module bootstrap hooks + */ + +#ifndef KING_SEMANTIC_DNS_LIFECYCLE_H +#define KING_SEMANTIC_DNS_LIFECYCLE_H + +int king_semantic_dns_registry_minit(void); +void king_semantic_dns_registry_mshutdown(void); + +#endif /* KING_SEMANTIC_DNS_LIFECYCLE_H */ diff --git a/extension/include/semantic_dns/semantic_dns.h b/extension/include/semantic_dns/semantic_dns.h old mode 100755 new mode 100644 index 3ae001331..b71981619 --- a/extension/include/semantic_dns/semantic_dns.h +++ b/extension/include/semantic_dns/semantic_dns.h @@ -14,6 +14,7 @@ #include #include #include +#include /* --- Semantic DNS Types --- */ @@ -103,6 +104,7 @@ typedef struct _king_semantic_dns_config_t { uint32_t mothernode_sync_interval_sec; uint32_t service_discovery_max_ips_per_response; char mothernode_uri[256]; + char state_path[PATH_MAX]; king_mother_node_t *mother_nodes; uint32_t mother_node_count; zval routing_policies; /* PHP array */ diff --git a/extension/include/semantic_dns/semantic_dns_internal.h b/extension/include/semantic_dns/semantic_dns_internal.h index 8e51150cc..748cbf443 100644 --- a/extension/include/semantic_dns/semantic_dns_internal.h +++ b/extension/include/semantic_dns/semantic_dns_internal.h @@ -15,6 +15,7 @@ #include "php_king.h" #include "config/smart_dns/base_layer.h" #include "semantic_dns/semantic_dns.h" +#include "semantic_dns/lifecycle.h" #include #include #include @@ -51,6 +52,7 @@ int king_semantic_dns_state_write_snapshot_file(const char *path, zval *payload) int king_semantic_dns_state_read_snapshot_file(const char *path, zval *payload); int king_semantic_dns_state_remove_snapshot_file(const char *path); int king_semantic_dns_listener_write_runtime_snapshot(void); +int king_semantic_dns_state_build_directory_path(char *dest, size_t dest_len); /* Registry/runtime refresh helpers */ int king_semantic_dns_refresh_distributed_state(void); diff --git a/extension/include/server/admin_api.h b/extension/include/server/admin_api.h old mode 100755 new mode 100644 diff --git a/extension/include/server/arginfo/arginfo.h b/extension/include/server/arginfo/arginfo.h new file mode 100644 index 000000000..297b4d3e2 --- /dev/null +++ b/extension/include/server/arginfo/arginfo.h @@ -0,0 +1,56 @@ +/* + * Arginfo for the procedural server/listener/control surface. + * The declarations are consumed through include/server/arginfo/index.h. + */ + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_server_listen, 0, 0, 4) + ZEND_ARG_TYPE_INFO(0, host, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, port, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, config, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, handler, IS_CALLABLE, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_server_on_cancel, 0, 0, 3) + ZEND_ARG_TYPE_INFO(0, session, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, stream_id, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, handler, IS_CALLABLE, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_server_send_early_hints, 0, 0, 3) + ZEND_ARG_TYPE_INFO(0, session, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, stream_id, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, hints, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_server_upgrade_to_websocket, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, session, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, stream_id, IS_LONG, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_server_reload_tls_config, 0, 0, 3) + ZEND_ARG_TYPE_INFO(0, session, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, cert_file_path, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, key_file_path, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_server_init_telemetry, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, session, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, config, IS_MIXED, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_session_capability, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, session, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, capability, IS_LONG, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_session_close_server_initiated, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, session, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, capability, IS_LONG, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, error_code, IS_LONG, 0, "0") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, reason, IS_STRING, 0, "\"\"") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_admin_api_listen, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, target_server, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, config, IS_MIXED, 0) +ZEND_END_ARG_INFO() diff --git a/extension/include/server/arginfo/index.h b/extension/include/server/arginfo/index.h new file mode 100644 index 000000000..5d6778614 --- /dev/null +++ b/extension/include/server/arginfo/index.h @@ -0,0 +1,16 @@ +/* + * ========================================================================= + * FILENAME: include/server/arginfo/index.h + * PROJECT: king + * + * PURPOSE: + * Arginfo aggregation point for server-side PHP entry points. + * ========================================================================= + */ + +#ifndef KING_SERVER_ARGINFO_INDEX_H +#define KING_SERVER_ARGINFO_INDEX_H + +#include "arginfo.h" + +#endif /* KING_SERVER_ARGINFO_INDEX_H */ diff --git a/extension/include/server/cancel.h b/extension/include/server/cancel.h old mode 100755 new mode 100644 diff --git a/extension/include/server/class_entries.h b/extension/include/server/class_entries.h new file mode 100644 index 000000000..fcaf62962 --- /dev/null +++ b/extension/include/server/class_entries.h @@ -0,0 +1,12 @@ +/* + * include/server/class_entries.h - Server class-entry externs + */ + +#ifndef KING_SERVER_CLASS_ENTRIES_H +#define KING_SERVER_CLASS_ENTRIES_H + +#include + +extern zend_class_entry *king_ce_ws_server; + +#endif /* KING_SERVER_CLASS_ENTRIES_H */ diff --git a/extension/include/server/class_methods.h b/extension/include/server/class_methods.h new file mode 100644 index 000000000..850b5d530 --- /dev/null +++ b/extension/include/server/class_methods.h @@ -0,0 +1,12 @@ +/* + * include/server/class_methods.h - Server OO method table externs + */ + +#ifndef KING_SERVER_CLASS_METHODS_H +#define KING_SERVER_CLASS_METHODS_H + +#include + +extern const zend_function_entry king_ws_server_class_methods[]; + +#endif /* KING_SERVER_CLASS_METHODS_H */ diff --git a/extension/include/server/cors.h b/extension/include/server/cors.h old mode 100755 new mode 100644 index 9f8def6f8..934f542d1 --- a/extension/include/server/cors.h +++ b/extension/include/server/cors.h @@ -15,7 +15,7 @@ #define KING_SERVER_CORS_H #include -#include "include/client/session.h" +#include "client/session.h" void king_server_cors_add_request_metadata( zval *request, diff --git a/extension/include/server/early_hints.h b/extension/include/server/early_hints.h old mode 100755 new mode 100644 diff --git a/extension/include/server/function_entries.h b/extension/include/server/function_entries.h new file mode 100644 index 000000000..f91bcfbcc --- /dev/null +++ b/extension/include/server/function_entries.h @@ -0,0 +1,15 @@ + PHP_FE(king_http1_server_listen, arginfo_king_server_listen) + PHP_FE(king_http1_server_listen_once, arginfo_king_server_listen) + PHP_FE(king_http2_server_listen, arginfo_king_server_listen) + PHP_FE(king_http2_server_listen_once, arginfo_king_server_listen) + PHP_FE(king_http3_server_listen, arginfo_king_server_listen) + PHP_FE(king_http3_server_listen_once, arginfo_king_server_listen) + PHP_FE(king_server_listen, arginfo_king_server_listen) + PHP_FE(king_server_on_cancel, arginfo_king_server_on_cancel) + PHP_FE(king_server_send_early_hints, arginfo_king_server_send_early_hints) + PHP_FE(king_server_upgrade_to_websocket, arginfo_king_server_upgrade_to_websocket) + PHP_FE(king_server_reload_tls_config, arginfo_king_server_reload_tls_config) + PHP_FE(king_server_init_telemetry, arginfo_king_server_init_telemetry) + PHP_FE(king_session_get_peer_cert_subject, arginfo_king_session_capability) + PHP_FE(king_session_close_server_initiated, arginfo_king_session_close_server_initiated) + PHP_FE(king_admin_api_listen, arginfo_king_admin_api_listen) diff --git a/extension/include/server/http1.h b/extension/include/server/http1.h old mode 100755 new mode 100644 diff --git a/extension/include/server/http2.h b/extension/include/server/http2.h old mode 100755 new mode 100644 diff --git a/extension/include/server/http3.h b/extension/include/server/http3.h old mode 100755 new mode 100644 diff --git a/extension/include/server/index.h b/extension/include/server/index.h old mode 100755 new mode 100644 index 10a788dfe..781bdb1ff --- a/extension/include/server/index.h +++ b/extension/include/server/index.h @@ -2,10 +2,23 @@ #define KING_SERVER_INDEX_H #include +#include "admin_api.h" +#include "cancel.h" +#include "class_entries.h" +#include "cors.h" +#include "early_hints.h" +#include "http1.h" +#include "http2.h" +#include "http3.h" +#include "open_telemetry.h" +#include "registration.h" +#include "session.h" +#include "tls.h" +#include "websocket.h" /** * @file extension/include/server/index.h - * @brief Unified local server dispatcher entry point. + * @brief Public umbrella header and unified local server dispatcher entry point. */ /** diff --git a/extension/include/server/open_telemetry.h b/extension/include/server/open_telemetry.h old mode 100755 new mode 100644 index 422e34489..e8b865674 --- a/extension/include/server/open_telemetry.h +++ b/extension/include/server/open_telemetry.h @@ -15,7 +15,7 @@ #define KING_SERVER_OPEN_TELEMETRY_H #include -#include "include/client/session.h" +#include "client/session.h" /* Validates telemetry config and records the resulting snapshot on a session. */ PHP_FUNCTION(king_server_init_telemetry); diff --git a/extension/include/server/registration.h b/extension/include/server/registration.h new file mode 100644 index 000000000..77b9ad09c --- /dev/null +++ b/extension/include/server/registration.h @@ -0,0 +1,14 @@ +/* + * include/server/registration.h - Server MINIT hooks + */ + +#ifndef KING_SERVER_REGISTRATION_H +#define KING_SERVER_REGISTRATION_H + +#include +#include "class_methods.h" + +void king_server_register_websocket_classes(void); +void king_server_init_object_handlers(void); + +#endif /* KING_SERVER_REGISTRATION_H */ diff --git a/extension/include/server/session.h b/extension/include/server/session.h old mode 100755 new mode 100644 index d339dbe15..06e32a4fe --- a/extension/include/server/session.h +++ b/extension/include/server/session.h @@ -15,7 +15,7 @@ # include #endif -#include "include/client/session.h" +#include "client/session.h" #ifndef __has_include diff --git a/extension/include/server/tls.h b/extension/include/server/tls.h old mode 100755 new mode 100644 index 39b771874..f90920754 --- a/extension/include/server/tls.h +++ b/extension/include/server/tls.h @@ -4,7 +4,7 @@ #define KING_SERVER_TLS_H #include -#include "include/client/session.h" +#include "client/session.h" /** * @file extension/include/server/tls.h diff --git a/extension/include/server/websocket.h b/extension/include/server/websocket.h old mode 100755 new mode 100644 index 93b1b5937..71c75a76f --- a/extension/include/server/websocket.h +++ b/extension/include/server/websocket.h @@ -4,9 +4,32 @@ #define KING_SERVER_WEBSOCKET_H #include +#include +#include + +#include "client/websocket.h" typedef struct _king_client_session king_client_session_t; +struct _king_ws_server_object { + zval config; + zend_string *host; + zend_long port; + int listener_fd; + HashTable connections; + zend_ulong next_connection_sequence; + bool registry_initialized; + bool closed; + zend_object std; +}; + +static inline king_ws_server_object * +php_king_ws_server_obj_from_zend(zend_object *obj) +{ + return (king_ws_server_object *) + ((char*)obj - XtOffsetOf(king_ws_server_object, std)); +} + /** * @file extension/include/server/websocket.h * @brief Server-side WebSocket helpers. diff --git a/extension/include/server/websocket_server_arginfo.h b/extension/include/server/websocket_server_arginfo.h new file mode 100644 index 000000000..2309a85cc --- /dev/null +++ b/extension/include/server/websocket_server_arginfo.h @@ -0,0 +1,43 @@ +/* + * include/server/websocket_server_arginfo.h - King\WebSocket\Server OO arginfo + */ + +#ifndef KING_SERVER_WEBSOCKET_SERVER_ARGINFO_H +#define KING_SERVER_WEBSOCKET_SERVER_ARGINFO_H + +#include + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_WebSocket_Server___construct, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, host, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, port, IS_LONG, 0) + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, config, King\\Config, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_King_WebSocket_Server_accept, 0, 0, King\\WebSocket\\Connection, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_WebSocket_Server_getConnections, 0, 0, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_WebSocket_Server_send, 0, 2, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, connectionId, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, message, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_WebSocket_Server_sendBinary, 0, 2, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, connectionId, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, payload, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_WebSocket_Server_broadcast, 0, 1, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, message, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_WebSocket_Server_broadcastBinary, 0, 1, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, payload, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_WebSocket_Server_stop, 0, 0, IS_VOID, 0) +ZEND_END_ARG_INFO() + +#endif /* KING_SERVER_WEBSOCKET_SERVER_ARGINFO_H */ diff --git a/extension/include/server/websocket_server_class_method_entries.h b/extension/include/server/websocket_server_class_method_entries.h new file mode 100644 index 000000000..8c7527c74 --- /dev/null +++ b/extension/include/server/websocket_server_class_method_entries.h @@ -0,0 +1,11 @@ +const zend_function_entry king_ws_server_class_methods[] = { + PHP_ME(King_WebSocket_Server, __construct, arginfo_class_King_WebSocket_Server___construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) + PHP_ME(King_WebSocket_Server, accept, arginfo_class_King_WebSocket_Server_accept, ZEND_ACC_PUBLIC) + PHP_ME(King_WebSocket_Server, getConnections, arginfo_class_King_WebSocket_Server_getConnections, ZEND_ACC_PUBLIC) + PHP_ME(King_WebSocket_Server, send, arginfo_class_King_WebSocket_Server_send, ZEND_ACC_PUBLIC) + PHP_ME(King_WebSocket_Server, sendBinary, arginfo_class_King_WebSocket_Server_sendBinary, ZEND_ACC_PUBLIC) + PHP_ME(King_WebSocket_Server, broadcast, arginfo_class_King_WebSocket_Server_broadcast, ZEND_ACC_PUBLIC) + PHP_ME(King_WebSocket_Server, broadcastBinary, arginfo_class_King_WebSocket_Server_broadcastBinary, ZEND_ACC_PUBLIC) + PHP_ME(King_WebSocket_Server, stop, arginfo_class_King_WebSocket_Server_stop, ZEND_ACC_PUBLIC) + PHP_FE_END +}; diff --git a/extension/include/telemetry/arginfo/arginfo.h b/extension/include/telemetry/arginfo/arginfo.h new file mode 100644 index 000000000..89110c7d2 --- /dev/null +++ b/extension/include/telemetry/arginfo/arginfo.h @@ -0,0 +1,23 @@ +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_telemetry_start_span, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, operation_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, attributes, IS_ARRAY, 1, "null") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, parent_span_id, IS_STRING, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_telemetry_end_span, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, span_id, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, final_attributes, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_telemetry_record_metric, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, metric_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, value, IS_DOUBLE, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, labels, IS_ARRAY, 1, "null") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, metric_type, IS_STRING, 0, "\"counter\"") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_telemetry_log, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, level, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, message, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, attributes, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() diff --git a/extension/include/telemetry/arginfo/index.h b/extension/include/telemetry/arginfo/index.h new file mode 100644 index 000000000..c7c2d6a5a --- /dev/null +++ b/extension/include/telemetry/arginfo/index.h @@ -0,0 +1,16 @@ +/* + * ========================================================================= + * FILENAME: include/telemetry/arginfo/index.h + * PROJECT: king + * + * PURPOSE: + * Arginfo aggregation point for telemetry PHP entry points. + * ========================================================================= + */ + +#ifndef KING_TELEMETRY_ARGINFO_INDEX_H +#define KING_TELEMETRY_ARGINFO_INDEX_H + +#include "arginfo.h" + +#endif /* KING_TELEMETRY_ARGINFO_INDEX_H */ diff --git a/extension/include/telemetry/function_entries.h b/extension/include/telemetry/function_entries.h new file mode 100644 index 000000000..0dcf368dd --- /dev/null +++ b/extension/include/telemetry/function_entries.h @@ -0,0 +1,11 @@ + PHP_FE(king_telemetry_init, arginfo_king_config_array) + PHP_FE(king_telemetry_start_span, arginfo_king_telemetry_start_span) + PHP_FE(king_telemetry_end_span, arginfo_king_telemetry_end_span) + PHP_FE(king_telemetry_record_metric, arginfo_king_telemetry_record_metric) + PHP_FE(king_telemetry_log, arginfo_king_telemetry_log) + PHP_FE(king_telemetry_flush, arginfo_king_no_args) + PHP_FE(king_telemetry_get_metrics, arginfo_king_no_args) + PHP_FE(king_telemetry_get_status, arginfo_king_no_args) + PHP_FE(king_telemetry_get_trace_context, arginfo_king_no_args) + PHP_FE(king_telemetry_inject_context, arginfo_king_optional_headers) + PHP_FE(king_telemetry_extract_context, arginfo_king_headers) diff --git a/extension/include/telemetry/index.h b/extension/include/telemetry/index.h new file mode 100644 index 000000000..4204c2bc0 --- /dev/null +++ b/extension/include/telemetry/index.h @@ -0,0 +1,16 @@ +/* + * ========================================================================= + * FILENAME: include/telemetry/index.h + * PROJECT: king + * + * PURPOSE: + * Public umbrella header for telemetry primitives. + * ========================================================================= + */ + +#ifndef KING_TELEMETRY_INDEX_H +#define KING_TELEMETRY_INDEX_H + +#include "telemetry.h" + +#endif /* KING_TELEMETRY_INDEX_H */ diff --git a/extension/include/telemetry/telemetry.h b/extension/include/telemetry/telemetry.h old mode 100755 new mode 100644 diff --git a/extension/include/validation/config_param/validate_bool.h b/extension/include/validation/config_param/validate_bool.h old mode 100755 new mode 100644 diff --git a/extension/include/validation/config_param/validate_colon_separated_string_from_allowlist.h b/extension/include/validation/config_param/validate_colon_separated_string_from_allowlist.h old mode 100755 new mode 100644 diff --git a/extension/include/validation/config_param/validate_comma_separated_numeric_string.h b/extension/include/validation/config_param/validate_comma_separated_numeric_string.h old mode 100755 new mode 100644 diff --git a/extension/include/validation/config_param/validate_comma_separated_string_from_allowlist.h b/extension/include/validation/config_param/validate_comma_separated_string_from_allowlist.h old mode 100755 new mode 100644 diff --git a/extension/include/validation/config_param/validate_cors_origin_string.h b/extension/include/validation/config_param/validate_cors_origin_string.h old mode 100755 new mode 100644 diff --git a/extension/include/validation/config_param/validate_cpu_affinity_map_string.h b/extension/include/validation/config_param/validate_cpu_affinity_map_string.h old mode 100755 new mode 100644 diff --git a/extension/include/validation/config_param/validate_double_range.h b/extension/include/validation/config_param/validate_double_range.h old mode 100755 new mode 100644 diff --git a/extension/include/validation/config_param/validate_erasure_coding_shards_string.h b/extension/include/validation/config_param/validate_erasure_coding_shards_string.h old mode 100755 new mode 100644 diff --git a/extension/include/validation/config_param/validate_generic_string.h b/extension/include/validation/config_param/validate_generic_string.h old mode 100755 new mode 100644 diff --git a/extension/include/validation/config_param/validate_host_string.h b/extension/include/validation/config_param/validate_host_string.h old mode 100755 new mode 100644 diff --git a/extension/include/validation/config_param/validate_long_range.h b/extension/include/validation/config_param/validate_long_range.h old mode 100755 new mode 100644 diff --git a/extension/include/validation/config_param/validate_niceness_value.h b/extension/include/validation/config_param/validate_niceness_value.h old mode 100755 new mode 100644 diff --git a/extension/include/validation/config_param/validate_positive_long.h b/extension/include/validation/config_param/validate_positive_long.h old mode 100755 new mode 100644 diff --git a/extension/include/validation/config_param/validate_readable_file_path.h b/extension/include/validation/config_param/validate_readable_file_path.h old mode 100755 new mode 100644 diff --git a/extension/include/validation/config_param/validate_scale_up_policy_string.h b/extension/include/validation/config_param/validate_scale_up_policy_string.h old mode 100755 new mode 100644 diff --git a/extension/include/validation/config_param/validate_scheduler_policy.h b/extension/include/validation/config_param/validate_scheduler_policy.h old mode 100755 new mode 100644 diff --git a/extension/include/validation/config_param/validate_string.h b/extension/include/validation/config_param/validate_string.h old mode 100755 new mode 100644 diff --git a/extension/include/validation/config_param/validate_string_from_allowlist.h b/extension/include/validation/config_param/validate_string_from_allowlist.h old mode 100755 new mode 100644 diff --git a/extension/include/validation/index.h b/extension/include/validation/index.h new file mode 100644 index 000000000..eebb8e29d --- /dev/null +++ b/extension/include/validation/index.h @@ -0,0 +1,34 @@ +/* + * ========================================================================= + * FILENAME: include/validation/index.h + * PROJECT: king + * + * PURPOSE: + * Public umbrella header for shared validation helpers. + * ========================================================================= + */ + +#ifndef KING_VALIDATION_INDEX_H +#define KING_VALIDATION_INDEX_H + +#include "config_param/validate_bool.h" +#include "config_param/validate_colon_separated_string_from_allowlist.h" +#include "config_param/validate_comma_separated_numeric_string.h" +#include "config_param/validate_comma_separated_string_from_allowlist.h" +#include "config_param/validate_cors_origin_string.h" +#include "config_param/validate_cpu_affinity_map_string.h" +#include "config_param/validate_double_range.h" +#include "config_param/validate_erasure_coding_shards_string.h" +#include "config_param/validate_generic_string.h" +#include "config_param/validate_host_string.h" +#include "config_param/validate_long_range.h" +#include "config_param/validate_niceness_value.h" +#include "config_param/validate_non_negative_long.h" +#include "config_param/validate_positive_long.h" +#include "config_param/validate_readable_file_path.h" +#include "config_param/validate_scale_up_policy_string.h" +#include "config_param/validate_scheduler_policy.h" +#include "config_param/validate_string.h" +#include "config_param/validate_string_from_allowlist.h" + +#endif /* KING_VALIDATION_INDEX_H */ diff --git a/extension/include/xslt/arginfo/arginfo.h b/extension/include/xslt/arginfo/arginfo.h new file mode 100644 index 000000000..7f32841e7 --- /dev/null +++ b/extension/include/xslt/arginfo/arginfo.h @@ -0,0 +1,35 @@ +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_xslt_transform_file, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, source_path, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, stylesheet_path, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_king_xslt_transform_to_file, 0, 0, 3) + ZEND_ARG_TYPE_INFO(0, source_path, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, stylesheet_path, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, output_path, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_XSLT_Processor___construct, 0, 0, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_XSLT_Processor_getOptions, 0, 0, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_XSLT_Processor_engineStatus, 0, 0, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_XSLT_Processor_transformFile, 0, 2, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, source_path, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, stylesheet_path, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_XSLT_Processor_transformToFile, 0, 3, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, source_path, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, stylesheet_path, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, output_path, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() diff --git a/extension/include/xslt/arginfo/index.h b/extension/include/xslt/arginfo/index.h new file mode 100644 index 000000000..5b7f8e9d9 --- /dev/null +++ b/extension/include/xslt/arginfo/index.h @@ -0,0 +1,16 @@ +/* + * ========================================================================= + * FILENAME: include/xslt/arginfo/index.h + * PROJECT: king + * + * PURPOSE: + * Arginfo aggregation point for XSLT PHP entry points. + * ========================================================================= + */ + +#ifndef KING_XSLT_ARGINFO_INDEX_H +#define KING_XSLT_ARGINFO_INDEX_H + +#include "arginfo.h" + +#endif /* KING_XSLT_ARGINFO_INDEX_H */ diff --git a/extension/include/xslt/class_entries.h b/extension/include/xslt/class_entries.h new file mode 100644 index 000000000..ced53a78f --- /dev/null +++ b/extension/include/xslt/class_entries.h @@ -0,0 +1,12 @@ +/* + * include/xslt/class_entries.h - XSLT class-entry externs + */ + +#ifndef KING_XSLT_CLASS_ENTRIES_H +#define KING_XSLT_CLASS_ENTRIES_H + +#include + +extern zend_class_entry *king_ce_xslt_processor; + +#endif /* KING_XSLT_CLASS_ENTRIES_H */ diff --git a/extension/include/xslt/class_method_entries.h b/extension/include/xslt/class_method_entries.h new file mode 100644 index 000000000..4b32e2057 --- /dev/null +++ b/extension/include/xslt/class_method_entries.h @@ -0,0 +1,8 @@ +const zend_function_entry king_xslt_processor_class_methods[] = { + PHP_ME(King_XSLT_Processor, __construct, arginfo_class_King_XSLT_Processor___construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) + PHP_ME(King_XSLT_Processor, getOptions, arginfo_class_King_XSLT_Processor_getOptions, ZEND_ACC_PUBLIC) + PHP_ME(King_XSLT_Processor, engineStatus, arginfo_class_King_XSLT_Processor_engineStatus, ZEND_ACC_PUBLIC) + PHP_ME(King_XSLT_Processor, transformFile, arginfo_class_King_XSLT_Processor_transformFile, ZEND_ACC_PUBLIC) + PHP_ME(King_XSLT_Processor, transformToFile, arginfo_class_King_XSLT_Processor_transformToFile, ZEND_ACC_PUBLIC) + PHP_FE_END +}; diff --git a/extension/include/xslt/class_methods.h b/extension/include/xslt/class_methods.h new file mode 100644 index 000000000..5b1bfe2e7 --- /dev/null +++ b/extension/include/xslt/class_methods.h @@ -0,0 +1,12 @@ +/* + * include/xslt/class_methods.h - XSLT OO method table externs + */ + +#ifndef KING_XSLT_CLASS_METHODS_H +#define KING_XSLT_CLASS_METHODS_H + +#include + +extern const zend_function_entry king_xslt_processor_class_methods[]; + +#endif /* KING_XSLT_CLASS_METHODS_H */ diff --git a/extension/include/xslt/function_entries.h b/extension/include/xslt/function_entries.h new file mode 100644 index 000000000..9a5832777 --- /dev/null +++ b/extension/include/xslt/function_entries.h @@ -0,0 +1,3 @@ + PHP_FE(king_xslt_engine_status, arginfo_king_no_args) + PHP_FE(king_xslt_transform_file, arginfo_king_xslt_transform_file) + PHP_FE(king_xslt_transform_to_file, arginfo_king_xslt_transform_to_file) diff --git a/extension/include/xslt/index.h b/extension/include/xslt/index.h new file mode 100644 index 000000000..9781c6ef2 --- /dev/null +++ b/extension/include/xslt/index.h @@ -0,0 +1,18 @@ +/* + * ========================================================================= + * FILENAME: include/xslt/index.h + * PROJECT: king + * + * PURPOSE: + * Public umbrella header for XSLT primitives. + * ========================================================================= + */ + +#ifndef KING_XSLT_INDEX_H +#define KING_XSLT_INDEX_H + +#include "class_entries.h" +#include "xslt.h" +#include "registration.h" + +#endif /* KING_XSLT_INDEX_H */ diff --git a/extension/include/xslt/registration.h b/extension/include/xslt/registration.h new file mode 100644 index 000000000..1a3933380 --- /dev/null +++ b/extension/include/xslt/registration.h @@ -0,0 +1,14 @@ +/* + * include/xslt/registration.h - XSLT MINIT hooks + */ + +#ifndef KING_XSLT_REGISTRATION_H +#define KING_XSLT_REGISTRATION_H + +#include +#include "class_methods.h" + +void king_xslt_register_classes(void); +void king_xslt_init_object_handlers(void); + +#endif /* KING_XSLT_REGISTRATION_H */ diff --git a/extension/include/xslt/xslt.h b/extension/include/xslt/xslt.h index 1b1851907..e3c2353ef 100644 --- a/extension/include/xslt/xslt.h +++ b/extension/include/xslt/xslt.h @@ -11,11 +11,39 @@ #define KING_XSLT_H #include +#include + +typedef struct _king_xslt_processor_object { + zval options; + zend_object std; +} king_xslt_processor_object; + +static inline king_xslt_processor_object * +php_king_xslt_processor_obj_from_zend(zend_object *obj) +{ + return (king_xslt_processor_object *) + ((char*)obj - XtOffsetOf(king_xslt_processor_object, std)); +} PHP_FUNCTION(king_xslt_engine_status); PHP_FUNCTION(king_xslt_transform_file); PHP_FUNCTION(king_xslt_transform_to_file); +void king_xslt_engine_status_array(zval *return_value); +zend_result king_xslt_transform_file_result( + zend_string *source_path, + zend_string *stylesheet_path, + zval *options, + zval *return_value +); +zend_result king_xslt_transform_to_file_result( + zend_string *source_path, + zend_string *stylesheet_path, + zend_string *output_path, + zval *options, + zval *return_value +); +zend_result king_xslt_validate_options(zval *options); void king_xslt_shutdown_system(void); void king_xslt_add_component_info(zval *configuration); diff --git a/extension/src/autoscaling/autoscaling.c b/extension/src/autoscaling/autoscaling.c index 887f79464..396ad7d6b 100644 --- a/extension/src/autoscaling/autoscaling.c +++ b/extension/src/autoscaling/autoscaling.c @@ -9,11 +9,13 @@ * ========================================================================= */ #include "php_king.h" -#include "include/autoscaling/autoscaling.h" -#include "include/config/cloud_autoscale/base_layer.h" -#include "include/config/cloud_autoscale/config.h" -#include "include/king_globals.h" -#include "include/telemetry/telemetry.h" +#include "php_king/core_arginfo.h" +#include "autoscaling/arginfo/index.h" +#include "autoscaling/autoscaling.h" +#include "config/cloud_autoscale/base_layer.h" +#include "config/cloud_autoscale/config.h" +#include "php_king/globals.h" +#include "telemetry/telemetry.h" #include "autoscaling/autoscaling_internal.h" #include @@ -62,7 +64,9 @@ king_autoscaling_runtime_state_t king_autoscaling_runtime = {0}; uint32_t king_current_instances = 1; -#include "autoscaling/runtime_config.inc" -#include "autoscaling/signal_and_monitoring.inc" -#include "autoscaling/runtime_state.inc" -#include "autoscaling/public_api.inc" +#include "state.inc" +#include "runtime/runtime_config.inc" +#include "runtime/signal_and_monitoring.inc" +#include "runtime/runtime_state.inc" +#include "runtime/public_api.inc" +#include "registration.inc" diff --git a/extension/src/autoscaling/class_entries.inc b/extension/src/autoscaling/class_entries.inc new file mode 100644 index 000000000..5de006eb5 --- /dev/null +++ b/extension/src/autoscaling/class_entries.inc @@ -0,0 +1,5 @@ +/* + * Autoscaling PHP class-entry storage. + */ + +zend_class_entry *king_ce_autoscaling = NULL; diff --git a/extension/src/autoscaling/provisioning.c b/extension/src/autoscaling/provisioning.c index a1c245f65..4580b1707 100644 --- a/extension/src/autoscaling/provisioning.c +++ b/extension/src/autoscaling/provisioning.c @@ -9,8 +9,8 @@ * ========================================================================= */ #include "php_king.h" -#include "include/autoscaling/autoscaling.h" -#include "include/runtime/libcurl_candidates.h" +#include "autoscaling/autoscaling.h" +#include "runtime/libcurl_candidates.h" #include "autoscaling/autoscaling_internal.h" #include "Zend/zend_smart_str.h" diff --git a/extension/src/autoscaling/registration.inc b/extension/src/autoscaling/registration.inc new file mode 100644 index 000000000..8c066b48b --- /dev/null +++ b/extension/src/autoscaling/registration.inc @@ -0,0 +1,16 @@ +/* + * Autoscaling registration aggregation. The autoscaling runtime translation + * unit includes this module boundary beside the controller runtime. + */ + +#include "autoscaling/class_method_entries.h" + +void king_autoscaling_register_classes(void) +{ + king_ce_autoscaling = king_register_class_with_flags( + "King\\Autoscaling", + NULL, + king_autoscaling_class_methods, + ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES + ); +} diff --git a/extension/src/autoscaling/autoscaling/public_api.inc b/extension/src/autoscaling/runtime/public_api.inc similarity index 100% rename from extension/src/autoscaling/autoscaling/public_api.inc rename to extension/src/autoscaling/runtime/public_api.inc diff --git a/extension/src/autoscaling/autoscaling/runtime_config.inc b/extension/src/autoscaling/runtime/runtime_config.inc similarity index 100% rename from extension/src/autoscaling/autoscaling/runtime_config.inc rename to extension/src/autoscaling/runtime/runtime_config.inc diff --git a/extension/src/autoscaling/autoscaling/runtime_state.inc b/extension/src/autoscaling/runtime/runtime_state.inc similarity index 100% rename from extension/src/autoscaling/autoscaling/runtime_state.inc rename to extension/src/autoscaling/runtime/runtime_state.inc diff --git a/extension/src/autoscaling/autoscaling/signal_and_monitoring.inc b/extension/src/autoscaling/runtime/signal_and_monitoring.inc similarity index 100% rename from extension/src/autoscaling/autoscaling/signal_and_monitoring.inc rename to extension/src/autoscaling/runtime/signal_and_monitoring.inc diff --git a/extension/src/autoscaling/state.inc b/extension/src/autoscaling/state.inc new file mode 100644 index 000000000..663631e8d --- /dev/null +++ b/extension/src/autoscaling/state.inc @@ -0,0 +1,6 @@ +/* + * Autoscaling module state storage aggregation. The autoscaling runtime + * translation unit owns this class state beside the controller runtime. + */ + +#include "class_entries.inc" diff --git a/extension/src/awaitable/aggregate.inc b/extension/src/awaitable/aggregate.inc new file mode 100644 index 000000000..71951c013 --- /dev/null +++ b/extension/src/awaitable/aggregate.inc @@ -0,0 +1,205 @@ +static zval *king_awaitable_aggregate_items(king_awaitable_object *intern) +{ + zval *awaitables; + + if (Z_TYPE(intern->payload) != IS_ARRAY) { + king_set_error("King\\Awaitable aggregate has an invalid native payload."); + return NULL; + } + + awaitables = zend_hash_str_find( + Z_ARRVAL(intern->payload), + "awaitables", + sizeof("awaitables") - 1 + ); + if (awaitables == NULL || Z_TYPE_P(awaitables) != IS_ARRAY) { + king_set_error("King\\Awaitable aggregate has an invalid awaitables payload."); + return NULL; + } + + return awaitables; +} + +static zend_result king_awaitable_validate_aggregate_array(zval *awaitables) +{ + zval *awaitable; + + if (zend_hash_num_elements(Z_ARRVAL_P(awaitables)) == 0) { + zend_argument_value_error(1, "must contain at least one King\\Awaitable instance"); + return FAILURE; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(awaitables), awaitable) + { + if (Z_TYPE_P(awaitable) != IS_OBJECT + || !instanceof_function(Z_OBJCE_P(awaitable), king_ce_awaitable)) { + zend_argument_type_error(1, "must contain only King\\Awaitable instances"); + return FAILURE; + } + } + ZEND_HASH_FOREACH_END(); + + return SUCCESS; +} + +static void king_awaitable_build_envelope( + zval *return_value, + zend_ulong num_key, + zend_string *str_key, + king_awaitable_object *awaitable) +{ + zval value; + + array_init(return_value); + if (str_key != NULL) { + add_assoc_str(return_value, "key", zend_string_copy(str_key)); + } else { + add_assoc_long(return_value, "key", (zend_long) num_key); + } + + add_assoc_string(return_value, "status", king_awaitable_status_name(awaitable->status)); + if (awaitable->operation != NULL) { + add_assoc_str(return_value, "operation", zend_string_copy(awaitable->operation)); + } else { + add_assoc_string(return_value, "operation", ""); + } + + if (awaitable->status == KING_AWAITABLE_RESOLVED && !Z_ISUNDEF(awaitable->result)) { + ZVAL_COPY(&value, &awaitable->result); + add_assoc_zval(return_value, "value", &value); + } else { + add_assoc_null(return_value, "value"); + } + + if (awaitable->status == KING_AWAITABLE_REJECTED + && !Z_ISUNDEF(awaitable->error) + && Z_TYPE(awaitable->error) == IS_STRING) { + add_assoc_str(return_value, "error", zend_string_copy(Z_STR(awaitable->error))); + } +} + +static zend_result king_awaitable_any_runner( + king_awaitable_object *intern, + zend_long timeout_ms, + zval *result) +{ + zval *awaitables = king_awaitable_aggregate_items(intern); + zval *awaitable; + zend_ulong num_key; + zend_string *str_key; + king_awaitable_object *child; + + if (awaitables == NULL) { + return FAILURE; + } + + ZVAL_UNDEF(result); + ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(awaitables), num_key, str_key, awaitable) + { + if (Z_TYPE_P(awaitable) != IS_OBJECT + || !instanceof_function(Z_OBJCE_P(awaitable), king_ce_awaitable)) { + king_set_error("King\\Awaitable::any() received an invalid child awaitable."); + return FAILURE; + } + + child = php_king_awaitable_obj_from_zend(Z_OBJ_P(awaitable)); + if (child->status == KING_AWAITABLE_PENDING + && king_awaitable_drive(child, timeout_ms) != SUCCESS) { + return FAILURE; + } + + if (child->status != KING_AWAITABLE_PENDING) { + king_awaitable_build_envelope(result, num_key, str_key, child); + return SUCCESS; + } + } + ZEND_HASH_FOREACH_END(); + + return SUCCESS; +} + +static zend_result king_awaitable_all_runner( + king_awaitable_object *intern, + zend_long timeout_ms, + zval *result) +{ + zval *awaitables = king_awaitable_aggregate_items(intern); + zval *awaitable; + zval envelope; + zend_ulong num_key; + zend_string *str_key; + king_awaitable_object *child; + bool pending = false; + + if (awaitables == NULL) { + return FAILURE; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(awaitables), awaitable) + { + if (Z_TYPE_P(awaitable) != IS_OBJECT + || !instanceof_function(Z_OBJCE_P(awaitable), king_ce_awaitable)) { + king_set_error("King\\Awaitable::all() received an invalid child awaitable."); + return FAILURE; + } + + child = php_king_awaitable_obj_from_zend(Z_OBJ_P(awaitable)); + if (child->status == KING_AWAITABLE_PENDING + && king_awaitable_drive(child, timeout_ms) != SUCCESS) { + return FAILURE; + } + if (child->status == KING_AWAITABLE_PENDING) { + pending = true; + } + } + ZEND_HASH_FOREACH_END(); + + if (pending) { + ZVAL_UNDEF(result); + return SUCCESS; + } + + array_init(result); + ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(awaitables), num_key, str_key, awaitable) + { + child = php_king_awaitable_obj_from_zend(Z_OBJ_P(awaitable)); + king_awaitable_build_envelope(&envelope, num_key, str_key, child); + if (str_key != NULL) { + add_assoc_zval_ex(result, ZSTR_VAL(str_key), ZSTR_LEN(str_key), &envelope); + } else { + add_index_zval(result, num_key, &envelope); + } + } + ZEND_HASH_FOREACH_END(); + + return SUCCESS; +} + +static zend_result king_awaitable_create_aggregate( + zval *return_value, + const char *operation, + size_t operation_len, + zval *awaitables, + king_awaitable_runner runner, + zval *cancel) +{ + zval payload; + zval awaitables_copy; + zend_result status; + + array_init(&payload); + ZVAL_COPY(&awaitables_copy, awaitables); + add_assoc_zval(&payload, "awaitables", &awaitables_copy); + + status = king_awaitable_create( + return_value, + operation, + operation_len, + runner, + &payload, + cancel + ); + zval_ptr_dtor(&payload); + + return status; +} diff --git a/extension/src/awaitable/aggregate_api.inc b/extension/src/awaitable/aggregate_api.inc new file mode 100644 index 000000000..93cd0332c --- /dev/null +++ b/extension/src/awaitable/aggregate_api.inc @@ -0,0 +1,107 @@ +PHP_METHOD(King_Awaitable, any) +{ + zval *awaitables; + zval *cancel = NULL; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_ARRAY(awaitables) + Z_PARAM_OPTIONAL + Z_PARAM_OBJECT_OF_CLASS_OR_NULL(cancel, king_ce_cancel_token) + ZEND_PARSE_PARAMETERS_END(); + + if (king_awaitable_validate_aggregate_array(awaitables) != SUCCESS) { + RETURN_THROWS(); + } + + if (king_awaitable_create_aggregate( + return_value, + "King\\Awaitable::any", + sizeof("King\\Awaitable::any") - 1, + awaitables, + king_awaitable_any_runner, + cancel + ) != SUCCESS) { + RETURN_THROWS(); + } +} + +PHP_METHOD(King_Awaitable, all) +{ + zval *awaitables; + zval *cancel = NULL; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_ARRAY(awaitables) + Z_PARAM_OPTIONAL + Z_PARAM_OBJECT_OF_CLASS_OR_NULL(cancel, king_ce_cancel_token) + ZEND_PARSE_PARAMETERS_END(); + + if (king_awaitable_validate_aggregate_array(awaitables) != SUCCESS) { + RETURN_THROWS(); + } + + if (king_awaitable_create_aggregate( + return_value, + "King\\Awaitable::all", + sizeof("King\\Awaitable::all") - 1, + awaitables, + king_awaitable_all_runner, + cancel + ) != SUCCESS) { + RETURN_THROWS(); + } +} + +PHP_FUNCTION(king_awaitable_any) +{ + zval *awaitables; + zval *cancel = NULL; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_ARRAY(awaitables) + Z_PARAM_OPTIONAL + Z_PARAM_OBJECT_OF_CLASS_OR_NULL(cancel, king_ce_cancel_token) + ZEND_PARSE_PARAMETERS_END(); + + if (king_awaitable_validate_aggregate_array(awaitables) != SUCCESS) { + RETURN_THROWS(); + } + + if (king_awaitable_create_aggregate( + return_value, + "king_awaitable_any", + sizeof("king_awaitable_any") - 1, + awaitables, + king_awaitable_any_runner, + cancel + ) != SUCCESS) { + RETURN_THROWS(); + } +} + +PHP_FUNCTION(king_awaitable_all) +{ + zval *awaitables; + zval *cancel = NULL; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_ARRAY(awaitables) + Z_PARAM_OPTIONAL + Z_PARAM_OBJECT_OF_CLASS_OR_NULL(cancel, king_ce_cancel_token) + ZEND_PARSE_PARAMETERS_END(); + + if (king_awaitable_validate_aggregate_array(awaitables) != SUCCESS) { + RETURN_THROWS(); + } + + if (king_awaitable_create_aggregate( + return_value, + "king_awaitable_all", + sizeof("king_awaitable_all") - 1, + awaitables, + king_awaitable_all_runner, + cancel + ) != SUCCESS) { + RETURN_THROWS(); + } +} diff --git a/extension/src/awaitable/awaitable.c b/extension/src/awaitable/awaitable.c new file mode 100644 index 000000000..cbd64a84a --- /dev/null +++ b/extension/src/awaitable/awaitable.c @@ -0,0 +1,19 @@ +/* + * Native Awaitable PHP surface for King. + * + * This translation unit owns the Awaitable and CancelToken PHP bindings, + * class method tables, and MINIT registration hooks. The core php_king + * bootstrap only calls the exported registration functions. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "php.h" +#include "php_king.h" +#include "awaitable/arginfo/index.h" + +#include "state.inc" +#include "php_binding.inc" +#include "registration.inc" diff --git a/extension/src/php_king/awaitable.inc b/extension/src/awaitable/awaitable.inc similarity index 94% rename from extension/src/php_king/awaitable.inc rename to extension/src/awaitable/awaitable.inc index 024c8044a..7dc972627 100644 --- a/extension/src/php_king/awaitable.inc +++ b/extension/src/awaitable/awaitable.inc @@ -5,6 +5,8 @@ * scheduler backend. */ +static zend_object_handlers king_awaitable_object_handlers; + static const char *king_awaitable_status_name(king_awaitable_status_t status) { switch (status) { @@ -137,6 +139,7 @@ static void king_awaitable_release_params(zval *params, uint32_t param_count) static zend_result king_awaitable_function_call_runner( king_awaitable_object *intern, + zend_long timeout_ms, zval *result) { zval *function_name; @@ -165,6 +168,10 @@ static zend_result king_awaitable_function_call_runner( zval_ptr_dtor(&callable); king_awaitable_release_params(params, param_count); + if (status == SUCCESS && Z_ISUNDEF_P(result)) { + ZVAL_NULL(result); + } + if (status != SUCCESS) { if (king_get_error()[0] == '\0') { king_set_error("King\\Awaitable could not invoke its deferred function."); @@ -177,6 +184,7 @@ static zend_result king_awaitable_function_call_runner( static zend_result king_awaitable_method_call_runner( king_awaitable_object *intern, + zend_long timeout_ms, zval *result) { zval *object; @@ -215,6 +223,10 @@ static zend_result king_awaitable_method_call_runner( zval_ptr_dtor(&object_copy); king_awaitable_release_params(params, param_count); + if (status == SUCCESS && Z_ISUNDEF_P(result)) { + ZVAL_NULL(result); + } + if (status != SUCCESS) { if (king_get_error()[0] == '\0') { king_set_error("King\\Awaitable could not invoke its deferred method."); @@ -385,8 +397,6 @@ static zend_result king_awaitable_drive(king_awaitable_object *intern, zend_long zval result; zend_result status; - (void) timeout_ms; - if (intern->status != KING_AWAITABLE_PENDING) { return SUCCESS; } @@ -395,6 +405,13 @@ static zend_result king_awaitable_drive(king_awaitable_object *intern, zend_long king_awaitable_cancel_native(intern); return SUCCESS; } + if (!Z_ISUNDEF(intern->cancel_token) + && Z_TYPE(intern->cancel_token) == IS_OBJECT + && instanceof_function(Z_OBJCE(intern->cancel_token), king_ce_cancel_token) + && php_king_cancel_token_obj_from_zend(Z_OBJ(intern->cancel_token))->cancelled) { + king_awaitable_cancel_native(intern); + return SUCCESS; + } if (intern->runner == NULL) { king_set_error("King\\Awaitable has no native runner."); @@ -403,7 +420,7 @@ static zend_result king_awaitable_drive(king_awaitable_object *intern, zend_long intern->started = true; ZVAL_UNDEF(&result); - status = intern->runner(intern, &result); + status = intern->runner(intern, timeout_ms, &result); if (status != SUCCESS) { if (!Z_ISUNDEF(result)) { zval_ptr_dtor(&result); @@ -422,7 +439,7 @@ static zend_result king_awaitable_drive(king_awaitable_object *intern, zend_long } if (Z_ISUNDEF(result)) { - ZVAL_NULL(&result); + return SUCCESS; } ZVAL_COPY(&intern->result, &result); zval_ptr_dtor(&result); @@ -682,16 +699,3 @@ PHP_FUNCTION(king_awaitable_status) intern = php_king_awaitable_obj_from_zend(Z_OBJ_P(awaitable)); RETURN_STRING(king_awaitable_status_name(intern->status)); } - -const zend_function_entry king_awaitable_class_methods[] = { - PHP_ME(King_Awaitable, __construct, arginfo_class_King_Awaitable___construct, ZEND_ACC_PRIVATE | ZEND_ACC_CTOR) - PHP_ME(King_Awaitable, await, arginfo_class_King_Awaitable_await, ZEND_ACC_PUBLIC) - PHP_ME(King_Awaitable, poll, arginfo_class_King_Awaitable_poll, ZEND_ACC_PUBLIC) - PHP_ME(King_Awaitable, cancel, arginfo_class_King_Awaitable_cancel, ZEND_ACC_PUBLIC) - PHP_ME(King_Awaitable, isPending, arginfo_class_King_Awaitable_isPending, ZEND_ACC_PUBLIC) - PHP_ME(King_Awaitable, isDone, arginfo_class_King_Awaitable_isDone, ZEND_ACC_PUBLIC) - PHP_ME(King_Awaitable, isCancelled, arginfo_class_King_Awaitable_isCancelled, ZEND_ACC_PUBLIC) - PHP_ME(King_Awaitable, getStatus, arginfo_class_King_Awaitable_getStatus, ZEND_ACC_PUBLIC) - PHP_ME(King_Awaitable, getOperation, arginfo_class_King_Awaitable_getOperation, ZEND_ACC_PUBLIC) - PHP_FE_END -}; diff --git a/extension/src/php_king/cancel_token.inc b/extension/src/awaitable/cancel_token.inc similarity index 53% rename from extension/src/php_king/cancel_token.inc rename to extension/src/awaitable/cancel_token.inc index 40861c630..a074aa7c9 100644 --- a/extension/src/php_king/cancel_token.inc +++ b/extension/src/awaitable/cancel_token.inc @@ -3,11 +3,7 @@ * top of the internal cancel-token object state. */ -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_CancelToken_cancel, 0, 0, IS_VOID, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_CancelToken_isCancelled, 0, 0, _IS_BOOL, 0) -ZEND_END_ARG_INFO() +#include "awaitable/cancel_token_arginfo.h" PHP_METHOD(King_CancelToken, cancel) { @@ -29,9 +25,3 @@ PHP_METHOD(King_CancelToken, isCancelled) intern = php_king_cancel_token_obj_from_zend(Z_OBJ_P(ZEND_THIS)); RETURN_BOOL(intern->cancelled); } - -const zend_function_entry king_cancel_token_class_methods[] = { - PHP_ME(King_CancelToken, cancel, arginfo_class_King_CancelToken_cancel, ZEND_ACC_PUBLIC) - PHP_ME(King_CancelToken, isCancelled, arginfo_class_King_CancelToken_isCancelled, ZEND_ACC_PUBLIC) - PHP_FE_END -}; diff --git a/extension/src/awaitable/cancel_token_object_handlers.inc b/extension/src/awaitable/cancel_token_object_handlers.inc new file mode 100644 index 000000000..73923d8c5 --- /dev/null +++ b/extension/src/awaitable/cancel_token_object_handlers.inc @@ -0,0 +1,38 @@ +/* + * King cancel token PHP object handlers. + */ + +static zend_object_handlers king_cancel_token_object_handlers; + +static void king_cancel_token_object_free(zend_object *object) +{ + king_cancel_token_object *intern = php_king_cancel_token_obj_from_zend(object); + + intern->cancelled = false; + zend_object_std_dtor(&intern->std); +} + +static zend_object *king_cancel_token_object_create(zend_class_entry *ce) +{ + king_cancel_token_object *intern = zend_object_alloc(sizeof(*intern), ce); + + intern->cancelled = false; + zend_object_std_init(&intern->std, ce); + object_properties_init(&intern->std, ce); + intern->std.handlers = &king_cancel_token_object_handlers; + + return &intern->std; +} + +static void king_cancel_token_object_handlers_init(void) +{ + memcpy( + &king_cancel_token_object_handlers, + &std_object_handlers, + sizeof(zend_object_handlers) + ); + king_cancel_token_object_handlers.offset = XtOffsetOf(king_cancel_token_object, std); + king_cancel_token_object_handlers.free_obj = king_cancel_token_object_free; + king_cancel_token_object_handlers.clone_obj = NULL; + king_ce_cancel_token->create_object = king_cancel_token_object_create; +} diff --git a/extension/src/awaitable/class_entries.inc b/extension/src/awaitable/class_entries.inc new file mode 100644 index 000000000..2a5880c6b --- /dev/null +++ b/extension/src/awaitable/class_entries.inc @@ -0,0 +1,6 @@ +/* + * Awaitable PHP class-entry storage. + */ + +zend_class_entry *king_ce_cancel_token = NULL; +zend_class_entry *king_ce_awaitable = NULL; diff --git a/extension/src/awaitable/php_binding.inc b/extension/src/awaitable/php_binding.inc new file mode 100644 index 000000000..41d04b1f3 --- /dev/null +++ b/extension/src/awaitable/php_binding.inc @@ -0,0 +1,11 @@ +/* + * Awaitable PHP binding aggregation. The Awaitable translation unit includes + * this module boundary instead of individual object/method leaf fragments. + */ + +#include "cancel_token.inc" +#include "cancel_token_object_handlers.inc" +#include "awaitable.inc" +#include "aggregate.inc" +#include "aggregate_api.inc" +#include "awaitable/class_method_entries.h" diff --git a/extension/src/awaitable/registration.inc b/extension/src/awaitable/registration.inc new file mode 100644 index 000000000..a097a772c --- /dev/null +++ b/extension/src/awaitable/registration.inc @@ -0,0 +1,21 @@ +void king_awaitable_register_classes(void) +{ + king_ce_cancel_token = king_register_class_with_flags( + "King\\CancelToken", + NULL, + king_cancel_token_class_methods, + ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES + ); + king_ce_awaitable = king_register_class_with_flags( + "King\\Awaitable", + NULL, + king_awaitable_class_methods, + ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES + ); +} + +void king_awaitable_init_object_handlers(void) +{ + king_cancel_token_object_handlers_init(); + king_awaitable_object_handlers_init(); +} diff --git a/extension/src/awaitable/state.inc b/extension/src/awaitable/state.inc new file mode 100644 index 000000000..76e4ef752 --- /dev/null +++ b/extension/src/awaitable/state.inc @@ -0,0 +1,5 @@ +/* + * Awaitable module state storage aggregation. + */ + +#include "class_entries.inc" diff --git a/extension/src/client/cancel.c b/extension/src/client/cancel.c index 72d230fcb..ccbc51a28 100644 --- a/extension/src/client/cancel.c +++ b/extension/src/client/cancel.c @@ -12,8 +12,8 @@ #include "php.h" #include "php_king.h" -#include "include/client/cancel.h" -#include "include/client/session.h" +#include "client/cancel.h" +#include "client/session.h" #include diff --git a/extension/src/client/class_entries.inc b/extension/src/client/class_entries.inc new file mode 100644 index 000000000..d8ffe3503 --- /dev/null +++ b/extension/src/client/class_entries.inc @@ -0,0 +1,17 @@ +/* + * Client PHP class-entry storage. + */ + +zend_class_entry *king_ce_session = NULL; +zend_class_entry *king_ce_stream = NULL; +zend_class_entry *king_ce_response = NULL; +zend_class_entry *king_ce_client_http = NULL; +zend_class_entry *king_ce_client_http1 = NULL; +zend_class_entry *king_ce_client_http2 = NULL; +zend_class_entry *king_ce_client_http3 = NULL; +zend_class_entry *king_ce_ws_connection = NULL; +zend_class_entry *king_ce_ws_exception = NULL; +zend_class_entry *king_ce_ws_connection_error = NULL; +zend_class_entry *king_ce_ws_protocol_error = NULL; +zend_class_entry *king_ce_ws_timeout = NULL; +zend_class_entry *king_ce_ws_closed = NULL; diff --git a/extension/src/client/early_hints.c b/extension/src/client/early_hints.c index e6775800c..35a9ead7f 100644 --- a/extension/src/client/early_hints.c +++ b/extension/src/client/early_hints.c @@ -11,7 +11,7 @@ */ #include "php_king.h" -#include "include/client/early_hints.h" +#include "client/early_hints.h" #include #include diff --git a/extension/src/client/http1.c b/extension/src/client/http1.c index 6bafb1eb3..833a85ab0 100644 --- a/extension/src/client/http1.c +++ b/extension/src/client/http1.c @@ -12,11 +12,11 @@ #include "php.h" #include "php_king.h" -#include "include/client/early_hints.h" -#include "include/client/http1.h" -#include "include/config/config.h" -#include "include/config/tcp_transport/base_layer.h" -#include "include/telemetry/telemetry.h" +#include "client/early_hints.h" +#include "client/http1.h" +#include "config/config.h" +#include "config/tcp_transport/base_layer.h" +#include "telemetry/telemetry.h" #include "Zend/zend_smart_str.h" #include "ext/standard/url.h" diff --git a/extension/src/client/http2.c b/extension/src/client/http2.c index 41d7fc55e..31790ce40 100644 --- a/extension/src/client/http2.c +++ b/extension/src/client/http2.c @@ -16,12 +16,12 @@ #include "php.h" #include "php_king.h" -#include "include/client/http2.h" -#include "include/config/config.h" -#include "include/config/http2/base_layer.h" -#include "include/config/tcp_transport/base_layer.h" -#include "include/runtime/libcurl_candidates.h" -#include "include/telemetry/telemetry.h" +#include "client/http2.h" +#include "config/config.h" +#include "config/http2/base_layer.h" +#include "config/tcp_transport/base_layer.h" +#include "runtime/libcurl_candidates.h" +#include "telemetry/telemetry.h" #include "Zend/zend_smart_str.h" #include "ext/standard/url.h" diff --git a/extension/src/client/http3.c b/extension/src/client/http3.c index cfe87853b..c1c9c9915 100644 --- a/extension/src/client/http3.c +++ b/extension/src/client/http3.c @@ -11,12 +11,12 @@ #include "php.h" #include "php_king.h" -#include "include/client/http3.h" -#include "include/config/config.h" -#include "include/config/quic_transport/base_layer.h" -#include "include/config/tcp_transport/base_layer.h" -#include "include/config/tls_and_crypto/base_layer.h" -#include "include/telemetry/telemetry.h" +#include "client/http3.h" +#include "config/config.h" +#include "config/quic_transport/base_layer.h" +#include "config/tcp_transport/base_layer.h" +#include "config/tls_and_crypto/base_layer.h" +#include "telemetry/telemetry.h" #include "Zend/zend_smart_str.h" #include "ext/standard/url.h" diff --git a/extension/src/client/index.c b/extension/src/client/index.c index 6f7959479..ca9432e03 100644 --- a/extension/src/client/index.c +++ b/extension/src/client/index.c @@ -14,10 +14,10 @@ #include "php.h" #include "php_king.h" -#include "include/client/http1.h" -#include "include/client/http2.h" -#include "include/client/http3.h" -#include "include/client/index.h" +#include "client/http1.h" +#include "client/http2.h" +#include "client/http3.h" +#include "client/index.h" #include #include @@ -458,3 +458,7 @@ PHP_FUNCTION(king_receive_response) zval_ptr_dtor(&payload); } + +#include "state.inc" +#include "php_binding.inc" +#include "registration.inc" diff --git a/extension/src/client/object.inc b/extension/src/client/object.inc index 51daa0c08..51a7a28b3 100644 --- a/extension/src/client/object.inc +++ b/extension/src/client/object.inc @@ -11,44 +11,7 @@ * ========================================================================= */ -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Response_getStatusCode_ret, 0, 0, IS_LONG, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Response_getHeaders, 0, 0, IS_ARRAY, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Response_getBody, 0, 0, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Response_read, 0, 0, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, length, IS_LONG, 0, "8192") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Response_isEndOfBody, 0, 0, _IS_BOOL, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_Client_HttpClient___construct, 0, 0, 0) - ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, config, King\\Config, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_King_Client_HttpClient_request, 0, 2, King\\Response, 0) - ZEND_ARG_TYPE_INFO(0, method, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, url, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, headers, IS_ARRAY, 0, "[]") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, body, IS_STRING, 0, "\"\"") - ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, cancel, King\\CancelToken, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_King_Client_HttpClient_requestAsync, 0, 2, King\\Awaitable, 0) - ZEND_ARG_TYPE_INFO(0, method, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, url, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, headers, IS_ARRAY, 0, "[]") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, body, IS_STRING, 0, "\"\"") - ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, cancel, King\\CancelToken, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Client_HttpClient_close, 0, 0, IS_VOID, 0) -ZEND_END_ARG_INFO() +#include "client/objects_arginfo.h" static zval *king_response_object_read_member( king_response_object *intern, @@ -626,19 +589,4 @@ PHP_METHOD(King_Client_HttpClient, close) king_set_error(""); } -const zend_function_entry king_response_class_methods[] = { - PHP_ME(King_Response, getStatusCode, arginfo_class_King_Response_getStatusCode_ret, ZEND_ACC_PUBLIC) - PHP_ME(King_Response, getHeaders, arginfo_class_King_Response_getHeaders, ZEND_ACC_PUBLIC) - PHP_ME(King_Response, getBody, arginfo_class_King_Response_getBody, ZEND_ACC_PUBLIC) - PHP_ME(King_Response, read, arginfo_class_King_Response_read, ZEND_ACC_PUBLIC) - PHP_ME(King_Response, isEndOfBody, arginfo_class_King_Response_isEndOfBody, ZEND_ACC_PUBLIC) - PHP_FE_END -}; - -const zend_function_entry king_http_client_class_methods[] = { - PHP_ME(King_Client_HttpClient, __construct, arginfo_class_King_Client_HttpClient___construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) - PHP_ME(King_Client_HttpClient, request, arginfo_class_King_Client_HttpClient_request, ZEND_ACC_PUBLIC) - PHP_ME(King_Client_HttpClient, requestAsync, arginfo_class_King_Client_HttpClient_requestAsync, ZEND_ACC_PUBLIC) - PHP_ME(King_Client_HttpClient, close, arginfo_class_King_Client_HttpClient_close, ZEND_ACC_PUBLIC) - PHP_FE_END -}; +#include "client/objects_class_method_entries.h" diff --git a/extension/src/client/object_handlers.inc b/extension/src/client/object_handlers.inc new file mode 100644 index 000000000..4a6c32a87 --- /dev/null +++ b/extension/src/client/object_handlers.inc @@ -0,0 +1,91 @@ +/* + * King response and HTTP client PHP object handlers. + */ + +static zend_object_handlers king_response_object_handlers; +static zend_object_handlers king_http_client_object_handlers; + +static void king_response_object_free(zend_object *object) +{ + king_response_object *intern = php_king_response_obj_from_zend(object); + + if (!Z_ISUNDEF(intern->payload)) { + zval_ptr_dtor(&intern->payload); + ZVAL_UNDEF(&intern->payload); + } + if (!Z_ISUNDEF(intern->request_context)) { + zval_ptr_dtor(&intern->request_context); + ZVAL_UNDEF(&intern->request_context); + } + + zend_object_std_dtor(&intern->std); +} + +static zend_object *king_response_object_create(zend_class_entry *ce) +{ + king_response_object *intern = zend_object_alloc(sizeof(*intern), ce); + + ZVAL_UNDEF(&intern->payload); + ZVAL_UNDEF(&intern->request_context); + intern->read_offset = 0; + zend_object_std_init(&intern->std, ce); + object_properties_init(&intern->std, ce); + intern->std.handlers = &king_response_object_handlers; + + return &intern->std; +} + +static void king_http_client_object_free(zend_object *object) +{ + king_http_client_object *intern = php_king_http_client_obj_from_zend(object); + + if (!Z_ISUNDEF(intern->config)) { + zval_ptr_dtor(&intern->config); + ZVAL_UNDEF(&intern->config); + } + + zend_object_std_dtor(&intern->std); +} + +static zend_object *king_http_client_object_create(zend_class_entry *ce) +{ + king_http_client_object *intern = zend_object_alloc(sizeof(*intern), ce); + + ZVAL_UNDEF(&intern->config); + intern->preferred_protocol = KING_CLIENT_PROTOCOL_AUTO; + intern->closed = false; + zend_object_std_init(&intern->std, ce); + object_properties_init(&intern->std, ce); + intern->std.handlers = &king_http_client_object_handlers; + + return &intern->std; +} + +static void king_response_object_handlers_init(void) +{ + memcpy( + &king_response_object_handlers, + &std_object_handlers, + sizeof(zend_object_handlers) + ); + king_response_object_handlers.offset = XtOffsetOf(king_response_object, std); + king_response_object_handlers.free_obj = king_response_object_free; + king_response_object_handlers.clone_obj = NULL; + king_ce_response->create_object = king_response_object_create; +} + +static void king_http_client_object_handlers_init(void) +{ + memcpy( + &king_http_client_object_handlers, + &std_object_handlers, + sizeof(zend_object_handlers) + ); + king_http_client_object_handlers.offset = XtOffsetOf(king_http_client_object, std); + king_http_client_object_handlers.free_obj = king_http_client_object_free; + king_http_client_object_handlers.clone_obj = NULL; + king_ce_client_http->create_object = king_http_client_object_create; + king_ce_client_http1->create_object = king_http_client_object_create; + king_ce_client_http2->create_object = king_http_client_object_create; + king_ce_client_http3->create_object = king_http_client_object_create; +} diff --git a/extension/src/client/php_binding.inc b/extension/src/client/php_binding.inc new file mode 100644 index 000000000..f865d5c9b --- /dev/null +++ b/extension/src/client/php_binding.inc @@ -0,0 +1,10 @@ +/* + * Client PHP binding aggregation. The client runtime translation unit includes + * this module boundary instead of session/client/websocket object-handler + * leaves directly. + */ + +#include "session/object_handlers.inc" +#include "object_handlers.inc" +#include "websocket/object_handlers.inc" +#include "resource_destructors.inc" diff --git a/extension/src/client/registration.inc b/extension/src/client/registration.inc new file mode 100644 index 000000000..655866d59 --- /dev/null +++ b/extension/src/client/registration.inc @@ -0,0 +1,105 @@ +/* + * Client class/resource registration. The client runtime translation unit + * includes php_binding.inc first, so the resource destructors and object + * handler initializers are available without pulling binding leaves here. + */ + +void king_client_register_resource_types(int module_number) +{ + le_king_session = zend_register_list_destructors_ex( + king_client_session_resource_dtor, NULL, "King\\Session", module_number); + le_king_ws = zend_register_list_destructors_ex( + king_client_websocket_resource_dtor, NULL, "King\\WebSocket", module_number); + le_king_request_context = zend_register_list_destructors_ex( + king_client_http1_request_context_resource_dtor, + NULL, + "King\\HttpRequestContext", + module_number); +} + +void king_client_register_websocket_exception_classes(void) +{ + king_ce_ws_exception = king_register_exception( + "King\\WebSocketException", king_ce_exception); + king_ce_ws_connection_error = king_register_exception( + "King\\WebSocketConnectionException", king_ce_ws_exception); + king_ce_ws_protocol_error = king_register_exception( + "King\\WebSocketProtocolException", king_ce_ws_exception); + king_ce_ws_timeout = king_register_exception( + "King\\WebSocketTimeoutException", king_ce_ws_exception); + king_ce_ws_closed = king_register_exception( + "King\\WebSocketClosedException", king_ce_ws_exception); +} + +void king_client_register_session_classes(void) +{ + king_ce_session = king_register_class_with_flags( + "King\\Session", + NULL, + king_session_class_methods, + ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES + ); + king_ce_stream = king_register_class_with_flags( + "King\\Stream", + NULL, + king_stream_class_methods, + ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES + ); + king_ce_response = king_register_class_with_flags( + "King\\Response", + NULL, + king_response_class_methods, + ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES + ); +} + +void king_client_register_http_classes(void) +{ + king_ce_client_http = king_register_class_with_flags( + "King\\Client\\HttpClient", + NULL, + king_http_client_class_methods, + ZEND_ACC_NO_DYNAMIC_PROPERTIES + ); + king_ce_client_http1 = king_register_class_with_flags( + "King\\Client\\Http1Client", + king_ce_client_http, + NULL, + ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES + ); + king_ce_client_http2 = king_register_class_with_flags( + "King\\Client\\Http2Client", + king_ce_client_http, + NULL, + ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES + ); + king_ce_client_http3 = king_register_class_with_flags( + "King\\Client\\Http3Client", + king_ce_client_http, + NULL, + ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES + ); +} + +void king_client_register_websocket_classes(void) +{ + king_ce_ws_connection = king_register_class_with_flags( + "King\\WebSocket\\Connection", + NULL, + king_ws_connection_class_methods, + ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES + ); +} + +void king_client_init_object_handlers(void) +{ + king_stream_object_handlers_init(); + king_response_object_handlers_init(); + king_http_client_object_handlers_init(); + king_ws_object_handlers_init(); +} + +void king_client_init_session_object_handlers(void) +{ + king_session_object_handlers_init(); +} diff --git a/extension/src/client/resource_destructors.inc b/extension/src/client/resource_destructors.inc new file mode 100644 index 000000000..730ec1a4a --- /dev/null +++ b/extension/src/client/resource_destructors.inc @@ -0,0 +1,28 @@ +/* + * Client-owned Zend resource destructors. + */ + +#include "client/session.h" +#include "client/websocket.h" +#include "php_king/runtime_contracts.h" + +static void king_client_session_resource_dtor(zend_resource *rsrc) +{ + if (rsrc->ptr) { + king_client_session_free(rsrc->ptr); + } +} + +static void king_client_websocket_resource_dtor(zend_resource *rsrc) +{ + if (rsrc->ptr) { + king_ws_state_free((king_ws_state *) rsrc->ptr); + } +} + +static void king_client_http1_request_context_resource_dtor(zend_resource *rsrc) +{ + if (rsrc->ptr) { + king_http1_request_context_free((king_http1_request_context *) rsrc->ptr); + } +} diff --git a/extension/src/client/resource_ids.inc b/extension/src/client/resource_ids.inc new file mode 100644 index 000000000..5eed4e867 --- /dev/null +++ b/extension/src/client/resource_ids.inc @@ -0,0 +1,7 @@ +/* + * Client Zend resource-id storage. + */ + +int le_king_session = -1; +int le_king_ws = -1; +int le_king_request_context = -1; diff --git a/extension/src/client/session.c b/extension/src/client/session.c index 616a6ba50..5d5117e47 100644 --- a/extension/src/client/session.c +++ b/extension/src/client/session.c @@ -14,10 +14,10 @@ #include "php.h" #include "php_king.h" -#include "include/client/session.h" -#include "include/client/http1.h" -#include "include/server/session.h" -#include "include/config/config.h" +#include "client/session.h" +#include "client/http1.h" +#include "server/session.h" +#include "config/config.h" #include #include diff --git a/extension/src/client/session/object.inc b/extension/src/client/session/object.inc index 879a05b69..e6ca55de8 100644 --- a/extension/src/client/session/object.inc +++ b/extension/src/client/session/object.inc @@ -1,4 +1,4 @@ -#include "object/arginfo_and_stream_guards.inc" +#include "object/stream_guards.inc" #include "object/http1_response_bridge.inc" #include "object/session_methods.inc" #include "object/stream_methods_and_tables.inc" diff --git a/extension/src/client/session/object/arginfo_and_stream_guards.inc b/extension/src/client/session/object/stream_guards.inc similarity index 70% rename from extension/src/client/session/object/arginfo_and_stream_guards.inc rename to extension/src/client/session/object/stream_guards.inc index a75d7fd67..ff6ed8113 100644 --- a/extension/src/client/session/object/arginfo_and_stream_guards.inc +++ b/extension/src/client/session/object/stream_guards.inc @@ -5,69 +5,12 @@ * * PURPOSE: * `King\\Session` and `King\\Stream` object surface for the active client - * runtime. This file declares arginfo and class method tables, validates - * initialized stream/session state, tracks per-stream cancel/body snapshots, - * and implements the public OO methods for request dispatch, response - * receive, polling, close, stats, ALPN, and early-hints toggling. + * runtime. This file validates initialized stream/session state and resolves + * bound cancel tokens for the public OO methods implemented beside it. * ========================================================================= */ -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_Session___construct, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, host, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, port, IS_LONG, 0) - ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, config, King\\Config, 1, "null") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, connect_options, IS_ARRAY, 0, "[]") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Session_isConnected, 0, 0, _IS_BOOL, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_King_Session_sendRequest, 0, 2, King\\Stream, 0) - ZEND_ARG_TYPE_INFO(0, method, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, path, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, headers, IS_ARRAY, 0, "[]") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, body, IS_STRING, 0, "\"\"") - ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, cancel, King\\CancelToken, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Session_poll, 0, 0, _IS_BOOL, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 0, "0") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_Session_close, 0, 0, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, code, IS_LONG, 0, "0") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, reason, IS_STRING, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Session_stats, 0, 0, IS_ARRAY, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Session_alpn, 0, 0, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Session_enableEarlyHints, 0, 0, IS_VOID, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, enable, _IS_BOOL, 0, "true") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_King_Stream_receiveResponse, 0, 0, King\\Response, 1) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 1, "null") - ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, cancel, King\\CancelToken, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Stream_send, 0, 1, IS_LONG, 0) - ZEND_ARG_TYPE_INFO(0, data, IS_STRING, 0) - ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, cancel, King\\CancelToken, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Stream_finish, 0, 0, IS_VOID, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, finalData, IS_STRING, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Stream_isClosed, 0, 0, _IS_BOOL, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Stream_close, 0, 0, IS_VOID, 0) -ZEND_END_ARG_INFO() +#include "client/session_arginfo.h" static zend_result king_stream_object_require_initialized( king_stream_object *intern, diff --git a/extension/src/client/session/object/stream_methods_and_tables.inc b/extension/src/client/session/object/stream_methods_and_tables.inc index d06476ba3..8e6b64c83 100644 --- a/extension/src/client/session/object/stream_methods_and_tables.inc +++ b/extension/src/client/session/object/stream_methods_and_tables.inc @@ -210,23 +210,4 @@ PHP_METHOD(King_Stream, close) king_set_error(""); } -const zend_function_entry king_session_class_methods[] = { - PHP_ME(King_Session, __construct, arginfo_class_King_Session___construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) - PHP_ME(King_Session, isConnected, arginfo_class_King_Session_isConnected, ZEND_ACC_PUBLIC) - PHP_ME(King_Session, sendRequest, arginfo_class_King_Session_sendRequest, ZEND_ACC_PUBLIC) - PHP_ME(King_Session, poll, arginfo_class_King_Session_poll, ZEND_ACC_PUBLIC) - PHP_ME(King_Session, close, arginfo_class_King_Session_close, ZEND_ACC_PUBLIC) - PHP_ME(King_Session, stats, arginfo_class_King_Session_stats, ZEND_ACC_PUBLIC) - PHP_ME(King_Session, alpn, arginfo_class_King_Session_alpn, ZEND_ACC_PUBLIC) - PHP_ME(King_Session, enableEarlyHints, arginfo_class_King_Session_enableEarlyHints, ZEND_ACC_PUBLIC) - PHP_FE_END -}; - -const zend_function_entry king_stream_class_methods[] = { - PHP_ME(King_Stream, receiveResponse, arginfo_class_King_Stream_receiveResponse, ZEND_ACC_PUBLIC) - PHP_ME(King_Stream, send, arginfo_class_King_Stream_send, ZEND_ACC_PUBLIC) - PHP_ME(King_Stream, finish, arginfo_class_King_Stream_finish, ZEND_ACC_PUBLIC) - PHP_ME(King_Stream, isClosed, arginfo_class_King_Stream_isClosed, ZEND_ACC_PUBLIC) - PHP_ME(King_Stream, close, arginfo_class_King_Stream_close, ZEND_ACC_PUBLIC) - PHP_FE_END -}; +#include "client/session_class_method_entries.h" diff --git a/extension/src/client/session/object_handlers.inc b/extension/src/client/session/object_handlers.inc new file mode 100644 index 000000000..62b94bfb3 --- /dev/null +++ b/extension/src/client/session/object_handlers.inc @@ -0,0 +1,121 @@ +/* + * King client session and stream PHP object handlers. + */ + +static zend_object_handlers king_stream_object_handlers; +static zend_object_handlers king_session_object_handlers; + +static void king_stream_object_free(zend_object *object) +{ + king_stream_object *intern = php_king_stream_obj_from_zend(object); + + if (!Z_ISUNDEF(intern->session)) { + zval_ptr_dtor(&intern->session); + ZVAL_UNDEF(&intern->session); + } + if (!Z_ISUNDEF(intern->cancel_token)) { + zval_ptr_dtor(&intern->cancel_token); + ZVAL_UNDEF(&intern->cancel_token); + } + if (!Z_ISUNDEF(intern->connection_config)) { + zval_ptr_dtor(&intern->connection_config); + ZVAL_UNDEF(&intern->connection_config); + } + if (!Z_ISUNDEF(intern->request_headers)) { + zval_ptr_dtor(&intern->request_headers); + ZVAL_UNDEF(&intern->request_headers); + } + if (intern->request_method != NULL) { + zend_string_release(intern->request_method); + intern->request_method = NULL; + } + if (intern->request_path != NULL) { + zend_string_release(intern->request_path); + intern->request_path = NULL; + } + if (intern->request_body != NULL) { + zend_string_release(intern->request_body); + intern->request_body = NULL; + } + + zend_object_std_dtor(&intern->std); +} + +static zend_object *king_stream_object_create(zend_class_entry *ce) +{ + king_stream_object *intern = zend_object_alloc(sizeof(*intern), ce); + + ZVAL_UNDEF(&intern->session); + ZVAL_UNDEF(&intern->cancel_token); + ZVAL_UNDEF(&intern->connection_config); + ZVAL_UNDEF(&intern->request_headers); + intern->request_method = NULL; + intern->request_path = NULL; + intern->request_body = zend_string_init("", 0, 0); + intern->stream_id = -1; + intern->buffered_bytes = 0; + intern->request_body_was_supplied = false; + intern->finished = false; + intern->closed = false; + intern->response_started = false; + zend_object_std_init(&intern->std, ce); + object_properties_init(&intern->std, ce); + intern->std.handlers = &king_stream_object_handlers; + + return &intern->std; +} + +static void king_session_object_free(zend_object *object) +{ + king_session_object *intern = php_king_obj_from_zend(object); + + if (!Z_ISUNDEF(intern->resource)) { + zval_ptr_dtor(&intern->resource); + ZVAL_UNDEF(&intern->resource); + } + if (!Z_ISUNDEF(intern->config)) { + zval_ptr_dtor(&intern->config); + ZVAL_UNDEF(&intern->config); + } + + zend_object_std_dtor(&intern->std); +} + +static zend_object *king_session_object_create(zend_class_entry *ce) +{ + king_session_object *intern = zend_object_alloc(sizeof(*intern), ce); + + ZVAL_UNDEF(&intern->resource); + ZVAL_UNDEF(&intern->config); + zend_object_std_init(&intern->std, ce); + object_properties_init(&intern->std, ce); + intern->std.handlers = &king_session_object_handlers; + + return &intern->std; +} + +static void king_stream_object_handlers_init(void) +{ + memcpy( + &king_stream_object_handlers, + &std_object_handlers, + sizeof(zend_object_handlers) + ); + king_stream_object_handlers.offset = XtOffsetOf(king_stream_object, std); + king_stream_object_handlers.free_obj = king_stream_object_free; + king_stream_object_handlers.clone_obj = NULL; + king_ce_stream->create_object = king_stream_object_create; +} + +static void king_session_object_handlers_init(void) +{ + memcpy( + &king_session_object_handlers, + &std_object_handlers, + sizeof(zend_object_handlers) + ); + king_session_object_handlers.offset = XtOffsetOf(king_session_object, std); + king_session_object_handlers.free_obj = king_session_object_free; + king_session_object_handlers.clone_obj = NULL; + king_ce_session->create_object = king_session_object_create; +} diff --git a/extension/src/client/state.inc b/extension/src/client/state.inc new file mode 100644 index 000000000..4385da7d7 --- /dev/null +++ b/extension/src/client/state.inc @@ -0,0 +1,6 @@ +/* + * Client module state storage aggregation. + */ + +#include "class_entries.inc" +#include "resource_ids.inc" diff --git a/extension/src/client/tls.c b/extension/src/client/tls.c index 2d90e994f..952f4c6ae 100644 --- a/extension/src/client/tls.c +++ b/extension/src/client/tls.c @@ -12,9 +12,9 @@ #include "php.h" #include "php_king.h" -#include "include/client/session.h" -#include "include/client/tls.h" -#include "include/config/tls_and_crypto/base_layer.h" +#include "client/session.h" +#include "client/tls.h" +#include "config/tls_and_crypto/base_layer.h" #include "main/php_streams.h" #include diff --git a/extension/src/client/websocket.c b/extension/src/client/websocket.c index 35d194caf..722493d6d 100644 --- a/extension/src/client/websocket.c +++ b/extension/src/client/websocket.c @@ -13,9 +13,10 @@ #include "php.h" #include "php_king.h" -#include "include/client/websocket.h" -#include "include/config/config.h" -#include "include/config/app_http3_websockets_webtransport/base_layer.h" +#include "client/websocket.h" +#include "client/websocket_arginfo.h" +#include "config/config.h" +#include "config/app_http3_websockets_webtransport/base_layer.h" #include "Zend/zend_smart_str.h" #include "ext/standard/base64.h" @@ -43,49 +44,6 @@ static const char *king_websocket_accept_magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_WebSocket_Connection___construct, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, url, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, headers, IS_ARRAY, 1, "null") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_WebSocket_Connection_send, 0, 1, IS_VOID, 0) - ZEND_ARG_TYPE_INFO(0, message, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_King_WebSocket_Connection_sendAsync, 0, 1, King\\Awaitable, 0) - ZEND_ARG_TYPE_INFO(0, message, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_WebSocket_Connection_sendBinary, 0, 1, IS_VOID, 0) - ZEND_ARG_TYPE_INFO(0, payload, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_King_WebSocket_Connection_sendBinaryAsync, 0, 1, King\\Awaitable, 0) - ZEND_ARG_TYPE_INFO(0, payload, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_WebSocket_Connection_receive, 0, 0, IS_STRING, 1) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_King_WebSocket_Connection_receiveAsync, 0, 0, King\\Awaitable, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_WebSocket_Connection_ping, 0, 0, IS_VOID, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, data, IS_STRING, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_WebSocket_Connection_close, 0, 0, IS_VOID, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, code, IS_LONG, 0, "1000") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, reason, IS_STRING, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_WebSocket_Connection_getInfo, 0, 0, IS_ARRAY, 0) -ZEND_END_ARG_INFO() - - #include "websocket/state_and_queue.inc" #include "websocket/transport_io.inc" #include "websocket/frame_receive.inc" diff --git a/extension/src/client/websocket/api.inc b/extension/src/client/websocket/api.inc index 7fff10934..28b773188 100644 --- a/extension/src/client/websocket/api.inc +++ b/extension/src/client/websocket/api.inc @@ -514,485 +514,6 @@ PHP_METHOD(King_WebSocket_Connection, getInfo) king_set_error(""); } -PHP_FUNCTION(king_client_websocket_connect) -{ - char *url_str = NULL; - size_t url_len = 0; - zval *headers_array = NULL; - zval *options_array = NULL; - king_ws_state *state; - - ZEND_PARSE_PARAMETERS_START(1, 3) - Z_PARAM_STRING(url_str, url_len) - Z_PARAM_OPTIONAL - Z_PARAM_ARRAY_OR_NULL(headers_array) - Z_PARAM_ARRAY_OR_NULL(options_array) - ZEND_PARSE_PARAMETERS_END(); - - state = king_websocket_state_create( - url_str, - url_len, - headers_array, - options_array, - "king_client_websocket_connect" - ); - if (state == NULL) { - RETURN_FALSE; - } - - RETURN_RES(zend_register_resource(state, le_king_ws)); -} - -PHP_FUNCTION(king_client_websocket_connect_async) -{ - char *url_str = NULL; - size_t url_len = 0; - zval *headers_array = NULL; - zval *options_array = NULL; - zval params[3]; - uint32_t index; - - ZEND_PARSE_PARAMETERS_START(1, 3) - Z_PARAM_STRING(url_str, url_len) - Z_PARAM_OPTIONAL - Z_PARAM_ARRAY_OR_NULL(headers_array) - Z_PARAM_ARRAY_OR_NULL(options_array) - ZEND_PARSE_PARAMETERS_END(); - - ZVAL_STRINGL(¶ms[0], url_str, url_len); - if (headers_array != NULL) { - ZVAL_COPY(¶ms[1], headers_array); - } else { - ZVAL_NULL(¶ms[1]); - } - if (options_array != NULL) { - ZVAL_COPY(¶ms[2], options_array); - } else { - ZVAL_NULL(¶ms[2]); - } - - if (king_awaitable_create_function_call( - return_value, - "king_client_websocket_connect_async", - sizeof("king_client_websocket_connect_async") - 1, - "king_client_websocket_connect", - sizeof("king_client_websocket_connect") - 1, - params, - 3, - NULL - ) != SUCCESS) { - for (index = 0; index < 3; index++) { - zval_ptr_dtor(¶ms[index]); - } - RETURN_FALSE; - } - - for (index = 0; index < 3; index++) { - zval_ptr_dtor(¶ms[index]); - } -} - -PHP_FUNCTION(king_client_websocket_send) -{ - zval *websocket; - char *data = NULL; - size_t data_len = 0; - zend_bool is_binary = false; - king_ws_state *state; - - ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_ZVAL(websocket) - Z_PARAM_STRING(data, data_len) - Z_PARAM_OPTIONAL - Z_PARAM_BOOL(is_binary) - ZEND_PARSE_PARAMETERS_END(); - - state = king_websocket_fetch_state(websocket, 1); - if (state == NULL) { - RETURN_THROWS(); - } - - if ( - king_websocket_require_open( - state, - "king_client_websocket_send" - ) != SUCCESS - ) { - RETURN_FALSE; - } - - if (data_len > (size_t) state->max_payload_size) { - king_websocket_set_error( - "king_client_websocket_send() payload size %zu exceeds max_payload_size " ZEND_LONG_FMT ".", - data_len, - state->max_payload_size - ); - RETURN_FALSE; - } - - if ( - king_websocket_send_frame( - state, - is_binary ? KING_WS_OPCODE_BINARY : KING_WS_OPCODE_TEXT, - (const unsigned char *) data, - data_len, - "king_client_websocket_send" - ) != SUCCESS - ) { - RETURN_FALSE; - } - - king_set_error(""); - RETURN_TRUE; -} - -PHP_FUNCTION(king_client_websocket_send_async) -{ - zval *websocket; - char *data = NULL; - size_t data_len = 0; - zend_bool is_binary = false; - zval params[3]; - uint32_t index; - - ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_ZVAL(websocket) - Z_PARAM_STRING(data, data_len) - Z_PARAM_OPTIONAL - Z_PARAM_BOOL(is_binary) - ZEND_PARSE_PARAMETERS_END(); - - ZVAL_COPY(¶ms[0], websocket); - ZVAL_STRINGL(¶ms[1], data, data_len); - ZVAL_BOOL(¶ms[2], is_binary); - - if (king_awaitable_create_function_call( - return_value, - "king_client_websocket_send_async", - sizeof("king_client_websocket_send_async") - 1, - "king_client_websocket_send", - sizeof("king_client_websocket_send") - 1, - params, - 3, - NULL - ) != SUCCESS) { - for (index = 0; index < 3; index++) { - zval_ptr_dtor(¶ms[index]); - } - RETURN_FALSE; - } - - for (index = 0; index < 3; index++) { - zval_ptr_dtor(¶ms[index]); - } -} - -PHP_FUNCTION(king_websocket_send) -{ - zval *websocket; - char *message = NULL; - size_t message_len = 0; - zend_bool is_binary = false; - king_ws_state *state; - - ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_ZVAL(websocket) - Z_PARAM_STRING(message, message_len) - Z_PARAM_OPTIONAL - Z_PARAM_BOOL(is_binary) - ZEND_PARSE_PARAMETERS_END(); - - state = king_websocket_fetch_state(websocket, 1); - if (state == NULL) { - RETURN_THROWS(); - } - - if ( - king_websocket_require_open( - state, - "king_websocket_send" - ) != SUCCESS - ) { - RETURN_FALSE; - } - - if (message_len > (size_t) state->max_payload_size) { - king_websocket_set_error( - "king_websocket_send() payload size %zu exceeds max_payload_size " ZEND_LONG_FMT ".", - message_len, - state->max_payload_size - ); - RETURN_FALSE; - } - - if ( - king_websocket_send_frame( - state, - is_binary ? KING_WS_OPCODE_BINARY : KING_WS_OPCODE_TEXT, - (const unsigned char *) message, - message_len, - "king_websocket_send" - ) != SUCCESS - ) { - RETURN_FALSE; - } - - king_set_error(""); - RETURN_TRUE; -} - -PHP_FUNCTION(king_client_websocket_receive) -{ - zval *websocket; - zend_long timeout_ms = -1; - king_ws_state *state; - king_ws_message *message; - zend_string *payload; - - ZEND_PARSE_PARAMETERS_START(1, 2) - Z_PARAM_ZVAL(websocket) - Z_PARAM_OPTIONAL - Z_PARAM_LONG(timeout_ms) - ZEND_PARSE_PARAMETERS_END(); - - state = king_websocket_fetch_state(websocket, 1); - if (state == NULL) { - RETURN_THROWS(); - } - - if (timeout_ms < -1) { - king_websocket_set_error( - "king_client_websocket_receive() timeout_ms must be -1 or >= 0." - ); - RETURN_FALSE; - } - - message = king_websocket_message_queue_shift(state); - if (message != NULL) { - payload = message->payload; - efree(message); - king_set_error(""); - RETURN_STR(payload); - } - - if (state->transport_stream != NULL && !state->closed) { - if ( - king_websocket_pump_transport( - state, - timeout_ms, - "king_client_websocket_receive" - ) != SUCCESS - ) { - RETURN_FALSE; - } - - message = king_websocket_message_queue_shift(state); - if (message != NULL) { - payload = message->payload; - efree(message); - king_set_error(""); - RETURN_STR(payload); - } - } - - if ( - (state->state == KING_WS_STATE_OPEN || state->state == KING_WS_STATE_CONNECTING) - && !state->closed - ) { - king_set_error(""); - RETURN_EMPTY_STRING(); - } - - king_websocket_set_error( - "king_client_websocket_receive() cannot run on a closed WebSocket connection." - ); - RETURN_FALSE; -} - -PHP_FUNCTION(king_client_websocket_receive_async) -{ - zval *websocket; - zend_long timeout_ms = -1; - zval params[2]; - uint32_t index; - - ZEND_PARSE_PARAMETERS_START(1, 2) - Z_PARAM_ZVAL(websocket) - Z_PARAM_OPTIONAL - Z_PARAM_LONG(timeout_ms) - ZEND_PARSE_PARAMETERS_END(); - - ZVAL_COPY(¶ms[0], websocket); - ZVAL_LONG(¶ms[1], timeout_ms); - - if (king_awaitable_create_function_call( - return_value, - "king_client_websocket_receive_async", - sizeof("king_client_websocket_receive_async") - 1, - "king_client_websocket_receive", - sizeof("king_client_websocket_receive") - 1, - params, - 2, - NULL - ) != SUCCESS) { - for (index = 0; index < 2; index++) { - zval_ptr_dtor(¶ms[index]); - } - RETURN_FALSE; - } - - for (index = 0; index < 2; index++) { - zval_ptr_dtor(¶ms[index]); - } -} - -PHP_FUNCTION(king_client_websocket_ping) -{ - zval *websocket; - char *payload = ""; - size_t payload_len = 0; - king_ws_state *state; - - ZEND_PARSE_PARAMETERS_START(1, 2) - Z_PARAM_ZVAL(websocket) - Z_PARAM_OPTIONAL - Z_PARAM_STRING(payload, payload_len) - ZEND_PARSE_PARAMETERS_END(); - - state = king_websocket_fetch_state(websocket, 1); - if (state == NULL) { - RETURN_THROWS(); - } - - if ( - king_websocket_require_open( - state, - "king_client_websocket_ping" - ) != SUCCESS - ) { - RETURN_FALSE; - } - - if (payload_len > KING_WS_PING_MAX_PAYLOAD_LEN) { - king_websocket_set_error( - "king_client_websocket_ping() ping payload cannot exceed %d bytes.", - KING_WS_PING_MAX_PAYLOAD_LEN - ); - RETURN_FALSE; - } - - if (state->last_ping_payload != NULL) { - zend_string_release(state->last_ping_payload); - } - state->last_ping_payload = zend_string_init(payload, payload_len, 0); - - if ( - state->transport_stream != NULL - && king_websocket_send_frame( - state, - KING_WS_OPCODE_PING, - (const unsigned char *) payload, - payload_len, - "king_client_websocket_ping" - ) != SUCCESS - ) { - RETURN_FALSE; - } - - king_set_error(""); - RETURN_TRUE; -} - -PHP_FUNCTION(king_client_websocket_get_status) -{ - zval *websocket; - king_ws_state *state; - - ZEND_PARSE_PARAMETERS_START(1, 1) - Z_PARAM_ZVAL(websocket) - ZEND_PARSE_PARAMETERS_END(); - - state = king_websocket_fetch_state(websocket, 1); - if (state == NULL) { - RETURN_THROWS(); - } - - king_set_error(""); - RETURN_LONG((zend_long) state->state); -} - -PHP_FUNCTION(king_client_websocket_close) -{ - zval *websocket; - zend_long status_code = 1000; - char *reason = ""; - size_t reason_len = 0; - king_ws_state *state; - zend_string *reason_string = NULL; - - ZEND_PARSE_PARAMETERS_START(1, 3) - Z_PARAM_ZVAL(websocket) - Z_PARAM_OPTIONAL - Z_PARAM_LONG(status_code) - Z_PARAM_STRING(reason, reason_len) - ZEND_PARSE_PARAMETERS_END(); - - state = king_websocket_fetch_state(websocket, 1); - if (state == NULL) { - RETURN_THROWS(); - } - - if (state->state == KING_WS_STATE_CLOSED || state->closed) { - king_set_error(""); - RETURN_TRUE; - } - - if ( - king_websocket_validate_close_inputs( - status_code, - reason_len, - "king_client_websocket_close" - ) != SUCCESS - ) { - RETURN_FALSE; - } - - if (reason_len > 0) { - reason_string = zend_string_init(reason, reason_len, 0); - } - - if ( - king_websocket_state_close( - state, - status_code, - reason_string, - "king_client_websocket_close" - ) != SUCCESS - ) { - if (reason_string != NULL) { - zend_string_release(reason_string); - } - RETURN_FALSE; - } - - if (reason_string != NULL) { - zend_string_release(reason_string); - } - - king_set_error(""); - RETURN_TRUE; -} +#include "api/procedural_api.inc" -const zend_function_entry king_ws_connection_class_methods[] = { - PHP_ME(King_WebSocket_Connection, __construct, arginfo_class_King_WebSocket_Connection___construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) - PHP_ME(King_WebSocket_Connection, send, arginfo_class_King_WebSocket_Connection_send, ZEND_ACC_PUBLIC) - PHP_ME(King_WebSocket_Connection, sendAsync, arginfo_class_King_WebSocket_Connection_sendAsync, ZEND_ACC_PUBLIC) - PHP_ME(King_WebSocket_Connection, sendBinary, arginfo_class_King_WebSocket_Connection_sendBinary, ZEND_ACC_PUBLIC) - PHP_ME(King_WebSocket_Connection, sendBinaryAsync, arginfo_class_King_WebSocket_Connection_sendBinaryAsync, ZEND_ACC_PUBLIC) - PHP_ME(King_WebSocket_Connection, receive, arginfo_class_King_WebSocket_Connection_receive, ZEND_ACC_PUBLIC) - PHP_ME(King_WebSocket_Connection, receiveAsync, arginfo_class_King_WebSocket_Connection_receiveAsync, ZEND_ACC_PUBLIC) - PHP_ME(King_WebSocket_Connection, ping, arginfo_class_King_WebSocket_Connection_ping, ZEND_ACC_PUBLIC) - PHP_ME(King_WebSocket_Connection, close, arginfo_class_King_WebSocket_Connection_close, ZEND_ACC_PUBLIC) - PHP_ME(King_WebSocket_Connection, getInfo, arginfo_class_King_WebSocket_Connection_getInfo, ZEND_ACC_PUBLIC) - PHP_FE_END -}; +#include "client/websocket_class_method_entries.h" diff --git a/extension/src/client/websocket/api/procedural_api.inc b/extension/src/client/websocket/api/procedural_api.inc new file mode 100644 index 000000000..9fb052c1c --- /dev/null +++ b/extension/src/client/websocket/api/procedural_api.inc @@ -0,0 +1,468 @@ +PHP_FUNCTION(king_client_websocket_connect) +{ + char *url_str = NULL; + size_t url_len = 0; + zval *headers_array = NULL; + zval *options_array = NULL; + king_ws_state *state; + + ZEND_PARSE_PARAMETERS_START(1, 3) + Z_PARAM_STRING(url_str, url_len) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(headers_array) + Z_PARAM_ARRAY_OR_NULL(options_array) + ZEND_PARSE_PARAMETERS_END(); + + state = king_websocket_state_create( + url_str, + url_len, + headers_array, + options_array, + "king_client_websocket_connect" + ); + if (state == NULL) { + RETURN_FALSE; + } + + RETURN_RES(zend_register_resource(state, le_king_ws)); +} + +PHP_FUNCTION(king_client_websocket_connect_async) +{ + char *url_str = NULL; + size_t url_len = 0; + zval *headers_array = NULL; + zval *options_array = NULL; + zval params[3]; + uint32_t index; + + ZEND_PARSE_PARAMETERS_START(1, 3) + Z_PARAM_STRING(url_str, url_len) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(headers_array) + Z_PARAM_ARRAY_OR_NULL(options_array) + ZEND_PARSE_PARAMETERS_END(); + + ZVAL_STRINGL(¶ms[0], url_str, url_len); + if (headers_array != NULL) { + ZVAL_COPY(¶ms[1], headers_array); + } else { + ZVAL_NULL(¶ms[1]); + } + if (options_array != NULL) { + ZVAL_COPY(¶ms[2], options_array); + } else { + ZVAL_NULL(¶ms[2]); + } + + if (king_awaitable_create_function_call( + return_value, + "king_client_websocket_connect_async", + sizeof("king_client_websocket_connect_async") - 1, + "king_client_websocket_connect", + sizeof("king_client_websocket_connect") - 1, + params, + 3, + NULL + ) != SUCCESS) { + for (index = 0; index < 3; index++) { + zval_ptr_dtor(¶ms[index]); + } + RETURN_FALSE; + } + + for (index = 0; index < 3; index++) { + zval_ptr_dtor(¶ms[index]); + } +} + +PHP_FUNCTION(king_client_websocket_send) +{ + zval *websocket; + char *data = NULL; + size_t data_len = 0; + zend_bool is_binary = false; + king_ws_state *state; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_ZVAL(websocket) + Z_PARAM_STRING(data, data_len) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(is_binary) + ZEND_PARSE_PARAMETERS_END(); + + state = king_websocket_fetch_state(websocket, 1); + if (state == NULL) { + RETURN_THROWS(); + } + + if ( + king_websocket_require_open( + state, + "king_client_websocket_send" + ) != SUCCESS + ) { + RETURN_FALSE; + } + + if (data_len > (size_t) state->max_payload_size) { + king_websocket_set_error( + "king_client_websocket_send() payload size %zu exceeds max_payload_size " ZEND_LONG_FMT ".", + data_len, + state->max_payload_size + ); + RETURN_FALSE; + } + + if ( + king_websocket_send_frame( + state, + is_binary ? KING_WS_OPCODE_BINARY : KING_WS_OPCODE_TEXT, + (const unsigned char *) data, + data_len, + "king_client_websocket_send" + ) != SUCCESS + ) { + RETURN_FALSE; + } + + king_set_error(""); + RETURN_TRUE; +} + +PHP_FUNCTION(king_client_websocket_send_async) +{ + zval *websocket; + char *data = NULL; + size_t data_len = 0; + zend_bool is_binary = false; + zval params[3]; + uint32_t index; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_ZVAL(websocket) + Z_PARAM_STRING(data, data_len) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(is_binary) + ZEND_PARSE_PARAMETERS_END(); + + ZVAL_COPY(¶ms[0], websocket); + ZVAL_STRINGL(¶ms[1], data, data_len); + ZVAL_BOOL(¶ms[2], is_binary); + + if (king_awaitable_create_function_call( + return_value, + "king_client_websocket_send_async", + sizeof("king_client_websocket_send_async") - 1, + "king_client_websocket_send", + sizeof("king_client_websocket_send") - 1, + params, + 3, + NULL + ) != SUCCESS) { + for (index = 0; index < 3; index++) { + zval_ptr_dtor(¶ms[index]); + } + RETURN_FALSE; + } + + for (index = 0; index < 3; index++) { + zval_ptr_dtor(¶ms[index]); + } +} + +PHP_FUNCTION(king_websocket_send) +{ + zval *websocket; + char *message = NULL; + size_t message_len = 0; + zend_bool is_binary = false; + king_ws_state *state; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_ZVAL(websocket) + Z_PARAM_STRING(message, message_len) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(is_binary) + ZEND_PARSE_PARAMETERS_END(); + + state = king_websocket_fetch_state(websocket, 1); + if (state == NULL) { + RETURN_THROWS(); + } + + if ( + king_websocket_require_open( + state, + "king_websocket_send" + ) != SUCCESS + ) { + RETURN_FALSE; + } + + if (message_len > (size_t) state->max_payload_size) { + king_websocket_set_error( + "king_websocket_send() payload size %zu exceeds max_payload_size " ZEND_LONG_FMT ".", + message_len, + state->max_payload_size + ); + RETURN_FALSE; + } + + if ( + king_websocket_send_frame( + state, + is_binary ? KING_WS_OPCODE_BINARY : KING_WS_OPCODE_TEXT, + (const unsigned char *) message, + message_len, + "king_websocket_send" + ) != SUCCESS + ) { + RETURN_FALSE; + } + + king_set_error(""); + RETURN_TRUE; +} + +PHP_FUNCTION(king_client_websocket_receive) +{ + zval *websocket; + zend_long timeout_ms = -1; + king_ws_state *state; + king_ws_message *message; + zend_string *payload; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_ZVAL(websocket) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(timeout_ms) + ZEND_PARSE_PARAMETERS_END(); + + state = king_websocket_fetch_state(websocket, 1); + if (state == NULL) { + RETURN_THROWS(); + } + + if (timeout_ms < -1) { + king_websocket_set_error( + "king_client_websocket_receive() timeout_ms must be -1 or >= 0." + ); + RETURN_FALSE; + } + + message = king_websocket_message_queue_shift(state); + if (message != NULL) { + payload = message->payload; + efree(message); + king_set_error(""); + RETURN_STR(payload); + } + + if (state->transport_stream != NULL && !state->closed) { + if ( + king_websocket_pump_transport( + state, + timeout_ms, + "king_client_websocket_receive" + ) != SUCCESS + ) { + RETURN_FALSE; + } + + message = king_websocket_message_queue_shift(state); + if (message != NULL) { + payload = message->payload; + efree(message); + king_set_error(""); + RETURN_STR(payload); + } + } + + if ( + (state->state == KING_WS_STATE_OPEN || state->state == KING_WS_STATE_CONNECTING) + && !state->closed + ) { + king_set_error(""); + RETURN_EMPTY_STRING(); + } + + king_websocket_set_error( + "king_client_websocket_receive() cannot run on a closed WebSocket connection." + ); + RETURN_FALSE; +} + +PHP_FUNCTION(king_client_websocket_receive_async) +{ + zval *websocket; + zend_long timeout_ms = -1; + zval params[2]; + uint32_t index; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_ZVAL(websocket) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(timeout_ms) + ZEND_PARSE_PARAMETERS_END(); + + ZVAL_COPY(¶ms[0], websocket); + ZVAL_LONG(¶ms[1], timeout_ms); + + if (king_awaitable_create_function_call( + return_value, + "king_client_websocket_receive_async", + sizeof("king_client_websocket_receive_async") - 1, + "king_client_websocket_receive", + sizeof("king_client_websocket_receive") - 1, + params, + 2, + NULL + ) != SUCCESS) { + for (index = 0; index < 2; index++) { + zval_ptr_dtor(¶ms[index]); + } + RETURN_FALSE; + } + + for (index = 0; index < 2; index++) { + zval_ptr_dtor(¶ms[index]); + } +} + +PHP_FUNCTION(king_client_websocket_ping) +{ + zval *websocket; + char *payload = ""; + size_t payload_len = 0; + king_ws_state *state; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_ZVAL(websocket) + Z_PARAM_OPTIONAL + Z_PARAM_STRING(payload, payload_len) + ZEND_PARSE_PARAMETERS_END(); + + state = king_websocket_fetch_state(websocket, 1); + if (state == NULL) { + RETURN_THROWS(); + } + + if ( + king_websocket_require_open( + state, + "king_client_websocket_ping" + ) != SUCCESS + ) { + RETURN_FALSE; + } + + if (payload_len > KING_WS_PING_MAX_PAYLOAD_LEN) { + king_websocket_set_error( + "king_client_websocket_ping() ping payload cannot exceed %d bytes.", + KING_WS_PING_MAX_PAYLOAD_LEN + ); + RETURN_FALSE; + } + + if (state->last_ping_payload != NULL) { + zend_string_release(state->last_ping_payload); + } + state->last_ping_payload = zend_string_init(payload, payload_len, 0); + + if ( + state->transport_stream != NULL + && king_websocket_send_frame( + state, + KING_WS_OPCODE_PING, + (const unsigned char *) payload, + payload_len, + "king_client_websocket_ping" + ) != SUCCESS + ) { + RETURN_FALSE; + } + + king_set_error(""); + RETURN_TRUE; +} + +PHP_FUNCTION(king_client_websocket_get_status) +{ + zval *websocket; + king_ws_state *state; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ZVAL(websocket) + ZEND_PARSE_PARAMETERS_END(); + + state = king_websocket_fetch_state(websocket, 1); + if (state == NULL) { + RETURN_THROWS(); + } + + king_set_error(""); + RETURN_LONG((zend_long) state->state); +} + +PHP_FUNCTION(king_client_websocket_close) +{ + zval *websocket; + zend_long status_code = 1000; + char *reason = ""; + size_t reason_len = 0; + king_ws_state *state; + zend_string *reason_string = NULL; + + ZEND_PARSE_PARAMETERS_START(1, 3) + Z_PARAM_ZVAL(websocket) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(status_code) + Z_PARAM_STRING(reason, reason_len) + ZEND_PARSE_PARAMETERS_END(); + + state = king_websocket_fetch_state(websocket, 1); + if (state == NULL) { + RETURN_THROWS(); + } + + if (state->state == KING_WS_STATE_CLOSED || state->closed) { + king_set_error(""); + RETURN_TRUE; + } + + if ( + king_websocket_validate_close_inputs( + status_code, + reason_len, + "king_client_websocket_close" + ) != SUCCESS + ) { + RETURN_FALSE; + } + + if (reason_len > 0) { + reason_string = zend_string_init(reason, reason_len, 0); + } + + if ( + king_websocket_state_close( + state, + status_code, + reason_string, + "king_client_websocket_close" + ) != SUCCESS + ) { + if (reason_string != NULL) { + zend_string_release(reason_string); + } + RETURN_FALSE; + } + + if (reason_string != NULL) { + zend_string_release(reason_string); + } + + king_set_error(""); + RETURN_TRUE; +} diff --git a/extension/src/client/websocket/object_handlers.inc b/extension/src/client/websocket/object_handlers.inc new file mode 100644 index 000000000..6fac70e68 --- /dev/null +++ b/extension/src/client/websocket/object_handlers.inc @@ -0,0 +1,42 @@ +/* + * King WebSocket client connection PHP object handlers. + */ + +static zend_object_handlers king_ws_object_handlers; + +static void king_ws_object_free(zend_object *object) +{ + king_ws_object *intern = php_king_ws_obj_from_zend(object); + + if (!Z_ISUNDEF(intern->resource)) { + zval_ptr_dtor(&intern->resource); + ZVAL_UNDEF(&intern->resource); + } + + zend_object_std_dtor(&intern->std); +} + +static zend_object *king_ws_object_create(zend_class_entry *ce) +{ + king_ws_object *intern = zend_object_alloc(sizeof(*intern), ce); + + ZVAL_UNDEF(&intern->resource); + zend_object_std_init(&intern->std, ce); + object_properties_init(&intern->std, ce); + intern->std.handlers = &king_ws_object_handlers; + + return &intern->std; +} + +static void king_ws_object_handlers_init(void) +{ + memcpy( + &king_ws_object_handlers, + &std_object_handlers, + sizeof(zend_object_handlers) + ); + king_ws_object_handlers.offset = XtOffsetOf(king_ws_object, std); + king_ws_object_handlers.free_obj = king_ws_object_free; + king_ws_object_handlers.clone_obj = NULL; + king_ce_ws_connection->create_object = king_ws_object_create; +} diff --git a/extension/src/config/app_http3_websockets_webtransport/base_layer.c b/extension/src/config/app_http3_websockets_webtransport/base_layer.c index 417f97660..eed7f63b1 100644 --- a/extension/src/config/app_http3_websockets_webtransport/base_layer.c +++ b/extension/src/config/app_http3_websockets_webtransport/base_layer.c @@ -11,6 +11,6 @@ * ========================================================================= */ -#include "include/config/app_http3_websockets_webtransport/base_layer.h" +#include "config/app_http3_websockets_webtransport/base_layer.h" kg_app_protocols_config_t king_app_protocols_config = {0}; diff --git a/extension/src/config/app_http3_websockets_webtransport/config.c b/extension/src/config/app_http3_websockets_webtransport/config.c index 01a2567e9..c7d48324b 100644 --- a/extension/src/config/app_http3_websockets_webtransport/config.c +++ b/extension/src/config/app_http3_websockets_webtransport/config.c @@ -11,13 +11,13 @@ * ========================================================================= */ -#include "include/config/app_http3_websockets_webtransport/config.h" -#include "include/config/app_http3_websockets_webtransport/base_layer.h" -#include "include/king_globals.h" +#include "config/app_http3_websockets_webtransport/config.h" +#include "config/app_http3_websockets_webtransport/base_layer.h" +#include "php_king/globals.h" -#include "include/validation/config_param/validate_bool.h" -#include "include/validation/config_param/validate_positive_long.h" -#include "include/validation/config_param/validate_comma_separated_string_from_allowlist.h" +#include "validation/config_param/validate_bool.h" +#include "validation/config_param/validate_positive_long.h" +#include "validation/config_param/validate_comma_separated_string_from_allowlist.h" #include "php.h" #include "zend_exceptions.h" diff --git a/extension/src/config/app_http3_websockets_webtransport/default.c b/extension/src/config/app_http3_websockets_webtransport/default.c index 4857c4028..584ccc551 100644 --- a/extension/src/config/app_http3_websockets_webtransport/default.c +++ b/extension/src/config/app_http3_websockets_webtransport/default.c @@ -11,8 +11,8 @@ * ========================================================================= */ -#include "include/config/app_http3_websockets_webtransport/default.h" -#include "include/config/app_http3_websockets_webtransport/base_layer.h" +#include "config/app_http3_websockets_webtransport/default.h" +#include "config/app_http3_websockets_webtransport/base_layer.h" void kg_config_app_http3_websockets_webtransport_defaults_load(void) { diff --git a/extension/src/config/app_http3_websockets_webtransport/index.c b/extension/src/config/app_http3_websockets_webtransport/index.c index dcb9ec4e1..9f4e4c339 100644 --- a/extension/src/config/app_http3_websockets_webtransport/index.c +++ b/extension/src/config/app_http3_websockets_webtransport/index.c @@ -10,9 +10,9 @@ * ========================================================================= */ -#include "include/config/app_http3_websockets_webtransport/index.h" -#include "include/config/app_http3_websockets_webtransport/default.h" -#include "include/config/app_http3_websockets_webtransport/ini.h" +#include "config/app_http3_websockets_webtransport/index.h" +#include "config/app_http3_websockets_webtransport/default.h" +#include "config/app_http3_websockets_webtransport/ini.h" void kg_config_app_http3_websockets_webtransport_init(void) { diff --git a/extension/src/config/app_http3_websockets_webtransport/ini.c b/extension/src/config/app_http3_websockets_webtransport/ini.c index b47e10e2a..e76b29f43 100644 --- a/extension/src/config/app_http3_websockets_webtransport/ini.c +++ b/extension/src/config/app_http3_websockets_webtransport/ini.c @@ -11,8 +11,9 @@ * ========================================================================= */ -#include "include/config/app_http3_websockets_webtransport/ini.h" -#include "include/config/app_http3_websockets_webtransport/base_layer.h" +#include "config/app_http3_websockets_webtransport/ini.h" +#include "config/app_http3_websockets_webtransport/base_layer.h" +#include "php_king/init.h" #include "php.h" #include @@ -88,7 +89,6 @@ PHP_INI_BEGIN() webtransport_max_streams_per_session, kg_app_protocols_config_t, king_app_protocols_config) PHP_INI_END() -extern int king_ini_module_number; void kg_config_app_http3_websockets_webtransport_ini_register(void) { diff --git a/extension/src/config/bare_metal_tuning/base_layer.c b/extension/src/config/bare_metal_tuning/base_layer.c index 2be4af394..25c13c9a0 100644 --- a/extension/src/config/bare_metal_tuning/base_layer.c +++ b/extension/src/config/bare_metal_tuning/base_layer.c @@ -11,6 +11,6 @@ * ========================================================================= */ -#include "include/config/bare_metal_tuning/base_layer.h" +#include "config/bare_metal_tuning/base_layer.h" kg_bare_metal_config_t king_bare_metal_config; diff --git a/extension/src/config/bare_metal_tuning/config.c b/extension/src/config/bare_metal_tuning/config.c index 92229186c..47638971a 100644 --- a/extension/src/config/bare_metal_tuning/config.c +++ b/extension/src/config/bare_metal_tuning/config.c @@ -11,14 +11,14 @@ * ========================================================================= */ -#include "include/config/bare_metal_tuning/config.h" -#include "include/config/bare_metal_tuning/base_layer.h" -#include "include/king_globals.h" +#include "config/bare_metal_tuning/config.h" +#include "config/bare_metal_tuning/base_layer.h" +#include "php_king/globals.h" -#include "include/validation/config_param/validate_bool.h" -#include "include/validation/config_param/validate_generic_string.h" -#include "include/validation/config_param/validate_positive_long.h" -#include "include/validation/config_param/validate_string_from_allowlist.h" +#include "validation/config_param/validate_bool.h" +#include "validation/config_param/validate_generic_string.h" +#include "validation/config_param/validate_positive_long.h" +#include "validation/config_param/validate_string_from_allowlist.h" #include #include diff --git a/extension/src/config/bare_metal_tuning/default.c b/extension/src/config/bare_metal_tuning/default.c index 686dc9b16..77863db15 100644 --- a/extension/src/config/bare_metal_tuning/default.c +++ b/extension/src/config/bare_metal_tuning/default.c @@ -11,8 +11,8 @@ * ========================================================================= */ -#include "include/config/bare_metal_tuning/default.h" -#include "include/config/bare_metal_tuning/base_layer.h" +#include "config/bare_metal_tuning/default.h" +#include "config/bare_metal_tuning/base_layer.h" void kg_config_bare_metal_tuning_defaults_load(void) { diff --git a/extension/src/config/bare_metal_tuning/index.c b/extension/src/config/bare_metal_tuning/index.c index ec7b71b87..8bcf76204 100644 --- a/extension/src/config/bare_metal_tuning/index.c +++ b/extension/src/config/bare_metal_tuning/index.c @@ -10,9 +10,9 @@ * ========================================================================= */ -#include "include/config/bare_metal_tuning/index.h" -#include "include/config/bare_metal_tuning/default.h" -#include "include/config/bare_metal_tuning/ini.h" +#include "config/bare_metal_tuning/index.h" +#include "config/bare_metal_tuning/default.h" +#include "config/bare_metal_tuning/ini.h" void kg_config_bare_metal_tuning_init(void) { diff --git a/extension/src/config/bare_metal_tuning/ini.c b/extension/src/config/bare_metal_tuning/ini.c index 77222b127..604b53b56 100644 --- a/extension/src/config/bare_metal_tuning/ini.c +++ b/extension/src/config/bare_metal_tuning/ini.c @@ -11,9 +11,10 @@ * ========================================================================= */ -#include "include/config/bare_metal_tuning/ini.h" -#include "include/config/bare_metal_tuning/base_layer.h" -#include "include/validation/config_param/validate_cpu_affinity_map_string.h" +#include "config/bare_metal_tuning/ini.h" +#include "config/bare_metal_tuning/base_layer.h" +#include "php_king/init.h" +#include "validation/config_param/validate_cpu_affinity_map_string.h" #include "php.h" #include @@ -107,7 +108,6 @@ PHP_INI_BEGIN() ZEND_INI_ENTRY_EX("king.io_thread_numa_node_policy", "default", PHP_INI_SYSTEM, OnUpdateNumaPolicyString, NULL) PHP_INI_END() -extern int king_ini_module_number; void kg_config_bare_metal_tuning_ini_register(void) { diff --git a/extension/src/config/cloud_autoscale/base_layer.c b/extension/src/config/cloud_autoscale/base_layer.c index 5eb074585..aeb94977a 100644 --- a/extension/src/config/cloud_autoscale/base_layer.c +++ b/extension/src/config/cloud_autoscale/base_layer.c @@ -11,6 +11,6 @@ * ========================================================================= */ -#include "include/config/cloud_autoscale/base_layer.h" +#include "config/cloud_autoscale/base_layer.h" kg_cloud_autoscale_config_t king_cloud_autoscale_config; diff --git a/extension/src/config/cloud_autoscale/config.c b/extension/src/config/cloud_autoscale/config.c index b95e3de5a..09de346f6 100644 --- a/extension/src/config/cloud_autoscale/config.c +++ b/extension/src/config/cloud_autoscale/config.c @@ -11,15 +11,15 @@ * ========================================================================= */ -#include "include/config/cloud_autoscale/config.h" -#include "include/config/cloud_autoscale/base_layer.h" -#include "include/king_globals.h" +#include "config/cloud_autoscale/config.h" +#include "config/cloud_autoscale/base_layer.h" +#include "php_king/globals.h" -#include "include/validation/config_param/validate_generic_string.h" -#include "include/validation/config_param/validate_long_range.h" -#include "include/validation/config_param/validate_non_negative_long.h" -#include "include/validation/config_param/validate_positive_long.h" -#include "include/validation/config_param/validate_string_from_allowlist.h" +#include "validation/config_param/validate_generic_string.h" +#include "validation/config_param/validate_long_range.h" +#include "validation/config_param/validate_non_negative_long.h" +#include "validation/config_param/validate_positive_long.h" +#include "validation/config_param/validate_string_from_allowlist.h" #include "php.h" #include "zend_exceptions.h" diff --git a/extension/src/config/cloud_autoscale/default.c b/extension/src/config/cloud_autoscale/default.c index 24f36412d..ff8951a77 100644 --- a/extension/src/config/cloud_autoscale/default.c +++ b/extension/src/config/cloud_autoscale/default.c @@ -6,13 +6,13 @@ * PURPOSE: * Default-value loader for the cloud-autoscale config family. This slice * seeds the provider-neutral autoscale defaults plus the current Hetzner- - * flavored endpoint/budget placeholders before INI and any allowed userland + * flavored endpoint and budget fields before INI and any allowed userland * overrides refine the live autoscale snapshot. * ========================================================================= */ -#include "include/config/cloud_autoscale/default.h" -#include "include/config/cloud_autoscale/base_layer.h" +#include "config/cloud_autoscale/default.h" +#include "config/cloud_autoscale/base_layer.h" void kg_config_cloud_autoscale_defaults_load(void) { diff --git a/extension/src/config/cloud_autoscale/index.c b/extension/src/config/cloud_autoscale/index.c index 10c0cb766..a064640de 100644 --- a/extension/src/config/cloud_autoscale/index.c +++ b/extension/src/config/cloud_autoscale/index.c @@ -10,9 +10,9 @@ * ========================================================================= */ -#include "include/config/cloud_autoscale/index.h" -#include "include/config/cloud_autoscale/default.h" -#include "include/config/cloud_autoscale/ini.h" +#include "config/cloud_autoscale/index.h" +#include "config/cloud_autoscale/default.h" +#include "config/cloud_autoscale/ini.h" void kg_config_cloud_autoscale_init(void) { diff --git a/extension/src/config/cloud_autoscale/ini.c b/extension/src/config/cloud_autoscale/ini.c index 2d71f7e9c..b5e4913e6 100644 --- a/extension/src/config/cloud_autoscale/ini.c +++ b/extension/src/config/cloud_autoscale/ini.c @@ -11,8 +11,9 @@ * ========================================================================= */ -#include "include/config/cloud_autoscale/ini.h" -#include "include/config/cloud_autoscale/base_layer.h" +#include "config/cloud_autoscale/ini.h" +#include "config/cloud_autoscale/base_layer.h" +#include "php_king/init.h" #include "php.h" #include @@ -147,7 +148,6 @@ PHP_INI_BEGIN() STD_PHP_INI_ENTRY("king.cluster_autoscale_instance_tags", "", PHP_INI_SYSTEM, OnUpdateString, instance_tags, kg_cloud_autoscale_config_t, king_cloud_autoscale_config) PHP_INI_END() -extern int king_ini_module_number; void kg_config_cloud_autoscale_ini_register(void) { diff --git a/extension/src/config/cluster_and_process/base_layer.c b/extension/src/config/cluster_and_process/base_layer.c index e73383035..b9beeea61 100644 --- a/extension/src/config/cluster_and_process/base_layer.c +++ b/extension/src/config/cluster_and_process/base_layer.c @@ -11,6 +11,6 @@ * ========================================================================= */ -#include "include/config/cluster_and_process/base_layer.h" +#include "config/cluster_and_process/base_layer.h" kg_cluster_config_t king_cluster_config; diff --git a/extension/src/config/cluster_and_process/config.c b/extension/src/config/cluster_and_process/config.c index f5c466485..6ed0ad27f 100644 --- a/extension/src/config/cluster_and_process/config.c +++ b/extension/src/config/cluster_and_process/config.c @@ -11,9 +11,9 @@ * ========================================================================= */ -#include "include/config/cluster_and_process/config.h" -#include "include/config/cluster_and_process/base_layer.h" -#include "include/king_globals.h" +#include "config/cluster_and_process/config.h" +#include "config/cluster_and_process/base_layer.h" +#include "php_king/globals.h" #include "php.h" #include diff --git a/extension/src/config/cluster_and_process/default.c b/extension/src/config/cluster_and_process/default.c index 38d5c5abb..005ebcbfd 100644 --- a/extension/src/config/cluster_and_process/default.c +++ b/extension/src/config/cluster_and_process/default.c @@ -11,8 +11,8 @@ * ========================================================================= */ -#include "include/config/cluster_and_process/default.h" -#include "include/config/cluster_and_process/base_layer.h" +#include "config/cluster_and_process/default.h" +#include "config/cluster_and_process/base_layer.h" void kg_config_cluster_and_process_defaults_load(void) { diff --git a/extension/src/config/cluster_and_process/index.c b/extension/src/config/cluster_and_process/index.c index dea57997d..d5db2a3e9 100644 --- a/extension/src/config/cluster_and_process/index.c +++ b/extension/src/config/cluster_and_process/index.c @@ -10,9 +10,9 @@ * ========================================================================= */ -#include "include/config/cluster_and_process/index.h" -#include "include/config/cluster_and_process/default.h" -#include "include/config/cluster_and_process/ini.h" +#include "config/cluster_and_process/index.h" +#include "config/cluster_and_process/default.h" +#include "config/cluster_and_process/ini.h" void kg_config_cluster_and_process_init(void) { diff --git a/extension/src/config/cluster_and_process/ini.c b/extension/src/config/cluster_and_process/ini.c index 3b1a24789..3f57b76e2 100644 --- a/extension/src/config/cluster_and_process/ini.c +++ b/extension/src/config/cluster_and_process/ini.c @@ -11,15 +11,15 @@ * ========================================================================= */ -#include "include/config/cluster_and_process/ini.h" -#include "include/config/cluster_and_process/base_layer.h" +#include "config/cluster_and_process/ini.h" +#include "config/cluster_and_process/base_layer.h" +#include "php_king/init.h" #include "php.h" #include #include #include -extern int king_ini_module_number; static ZEND_INI_MH(OnUpdateClusterNonNegativeLong) { diff --git a/extension/src/config/config.c b/extension/src/config/config.c index 231b503ae..a79d4543c 100644 --- a/extension/src/config/config.c +++ b/extension/src/config/config.c @@ -21,26 +21,25 @@ #include "php.h" #include "php_king.h" -#include "include/config/cloud_autoscale/config.h" -#include "include/config/config.h" -#include "include/config/http2/config.h" -#include "include/config/mcp_and_orchestrator/config.h" -#include "include/config/native_cdn/config.h" -#include "include/config/native_object_store/config.h" -#include "include/config/open_telemetry/config.h" -#include "include/config/quic_transport/config.h" -#include "include/config/semantic_geometry/config.h" -#include "include/config/smart_contracts/config.h" -#include "include/config/smart_dns/config.h" -#include "include/config/ssh_over_quic/config.h" -#include "include/config/tcp_transport/config.h" -#include "include/config/tls_and_crypto/config.h" -#include "include/king_globals.h" +#include "config/cloud_autoscale/config.h" +#include "config/config.h" +#include "config/high_perf_compute_and_ai/config.h" +#include "config/http2/config.h" +#include "config/mcp_and_orchestrator/config.h" +#include "config/native_cdn/config.h" +#include "config/native_object_store/config.h" +#include "config/open_telemetry/config.h" +#include "config/quic_transport/config.h" +#include "config/semantic_geometry/config.h" +#include "config/smart_contracts/config.h" +#include "config/smart_dns/config.h" +#include "config/ssh_over_quic/config.h" +#include "config/tcp_transport/config.h" +#include "config/tls_and_crypto/config.h" +#include "php_king/globals.h" #include #include "zend_exceptions.h" -extern int le_king_cfg; - typedef enum _king_config_override_module_t { KING_CONFIG_OVERRIDE_NONE = 0, KING_CONFIG_OVERRIDE_TLS, @@ -55,7 +54,8 @@ typedef enum _king_config_override_module_t { KING_CONFIG_OVERRIDE_STORAGE, KING_CONFIG_OVERRIDE_CDN, KING_CONFIG_OVERRIDE_DNS, - KING_CONFIG_OVERRIDE_OTEL + KING_CONFIG_OVERRIDE_OTEL, + KING_CONFIG_OVERRIDE_COMPUTE_AI } king_config_override_module_t; #define KING_CONFIG_FREE_PERSISTENT(field) \ @@ -71,6 +71,10 @@ typedef enum _king_config_override_module_t { #include "internal/overrides.inc" #include "internal/api.inc" #include "internal/object.inc" +#include "internal/resource_helpers.inc" +#include "state.inc" +#include "php_binding.inc" +#include "registration.inc" void king_config_release_module_globals(void) { @@ -89,7 +93,21 @@ void king_config_release_module_globals(void) KING_CONFIG_FREE_PERSISTENT(king_dynamic_admin_api_config.key_file); memset(&king_dynamic_admin_api_config, 0, sizeof(king_dynamic_admin_api_config)); + KING_CONFIG_FREE_PERSISTENT(king_high_perf_compute_ai_config.inference_preferred_model_profile); + KING_CONFIG_FREE_PERSISTENT(king_high_perf_compute_ai_config.inference_models); + KING_CONFIG_FREE_PERSISTENT(king_high_perf_compute_ai_config.inference_cpu_model_name); + KING_CONFIG_FREE_PERSISTENT(king_high_perf_compute_ai_config.inference_cpu_model_artifact); + KING_CONFIG_FREE_PERSISTENT(king_high_perf_compute_ai_config.inference_gpu_model_name); + KING_CONFIG_FREE_PERSISTENT(king_high_perf_compute_ai_config.inference_gpu_model_artifact); + KING_CONFIG_FREE_PERSISTENT(king_high_perf_compute_ai_config.inference_gpu_thermal_sensor_path); + KING_CONFIG_FREE_PERSISTENT(king_high_perf_compute_ai_config.inference_gpu_thermal_sensor_command); + KING_CONFIG_FREE_PERSISTENT(king_high_perf_compute_ai_config.inference_gpu_power_sensor_command); + KING_CONFIG_FREE_PERSISTENT(king_high_perf_compute_ai_config.inference_llm_cache_path); + KING_CONFIG_FREE_PERSISTENT(king_high_perf_compute_ai_config.inference_llm_cache_disk_alert_webhook); + KING_CONFIG_FREE_PERSISTENT(king_high_perf_compute_ai_config.inference_llm_cache_disk_alert_mcp_service); + KING_CONFIG_FREE_PERSISTENT(king_high_perf_compute_ai_config.inference_llm_cache_disk_alert_mcp_method); KING_CONFIG_FREE_PERSISTENT(king_high_perf_compute_ai_config.gpu_default_backend); + KING_CONFIG_FREE_PERSISTENT(king_high_perf_compute_ai_config.worker_gpu_affinity_map); memset(&king_high_perf_compute_ai_config, 0, sizeof(king_high_perf_compute_ai_config)); memset(&king_iibin_config, 0, sizeof(king_iibin_config)); diff --git a/extension/src/config/dynamic_admin_api/base_layer.c b/extension/src/config/dynamic_admin_api/base_layer.c index de6dba802..90c129073 100644 --- a/extension/src/config/dynamic_admin_api/base_layer.c +++ b/extension/src/config/dynamic_admin_api/base_layer.c @@ -10,6 +10,6 @@ * ========================================================================= */ -#include "include/config/dynamic_admin_api/base_layer.h" +#include "config/dynamic_admin_api/base_layer.h" kg_dynamic_admin_api_config_t king_dynamic_admin_api_config; diff --git a/extension/src/config/dynamic_admin_api/config.c b/extension/src/config/dynamic_admin_api/config.c index a58c66103..2a96becfa 100644 --- a/extension/src/config/dynamic_admin_api/config.c +++ b/extension/src/config/dynamic_admin_api/config.c @@ -11,9 +11,9 @@ * ========================================================================= */ -#include "include/config/dynamic_admin_api/config.h" -#include "include/config/dynamic_admin_api/base_layer.h" -#include "include/king_globals.h" +#include "config/dynamic_admin_api/config.h" +#include "config/dynamic_admin_api/base_layer.h" +#include "php_king/globals.h" #include "php.h" #include "main/php_streams.h" diff --git a/extension/src/config/dynamic_admin_api/default.c b/extension/src/config/dynamic_admin_api/default.c index 031dd8ce8..d26c574bc 100644 --- a/extension/src/config/dynamic_admin_api/default.c +++ b/extension/src/config/dynamic_admin_api/default.c @@ -5,14 +5,14 @@ * * PURPOSE: * Default-value loader for the dynamic-admin-api config family. This slice - * seeds the local bind/port defaults and empty mTLS path/auth placeholders + * seeds the local bind/port defaults and unset mTLS path/auth fields * before INI and any allowed userland overrides refine the live admin-api * snapshot. * ========================================================================= */ -#include "include/config/dynamic_admin_api/default.h" -#include "include/config/dynamic_admin_api/base_layer.h" +#include "config/dynamic_admin_api/default.h" +#include "config/dynamic_admin_api/base_layer.h" void kg_config_dynamic_admin_api_defaults_load(void) { diff --git a/extension/src/config/dynamic_admin_api/index.c b/extension/src/config/dynamic_admin_api/index.c index e62506bac..08bf2a1f6 100644 --- a/extension/src/config/dynamic_admin_api/index.c +++ b/extension/src/config/dynamic_admin_api/index.c @@ -10,9 +10,9 @@ * ========================================================================= */ -#include "include/config/dynamic_admin_api/index.h" -#include "include/config/dynamic_admin_api/default.h" -#include "include/config/dynamic_admin_api/ini.h" +#include "config/dynamic_admin_api/index.h" +#include "config/dynamic_admin_api/default.h" +#include "config/dynamic_admin_api/ini.h" void kg_config_dynamic_admin_api_init(void) { diff --git a/extension/src/config/dynamic_admin_api/ini.c b/extension/src/config/dynamic_admin_api/ini.c index 3d0a02637..9f8cb20a4 100644 --- a/extension/src/config/dynamic_admin_api/ini.c +++ b/extension/src/config/dynamic_admin_api/ini.c @@ -11,8 +11,9 @@ * ========================================================================= */ -#include "include/config/dynamic_admin_api/ini.h" -#include "include/config/dynamic_admin_api/base_layer.h" +#include "config/dynamic_admin_api/ini.h" +#include "config/dynamic_admin_api/base_layer.h" +#include "php_king/init.h" #include "php.h" #include "main/php_streams.h" @@ -21,7 +22,6 @@ #include #include -extern int king_ini_module_number; static void dynamic_admin_replace_string(char **target, zend_string *value) { diff --git a/extension/src/config/high_perf_compute_and_ai/base_layer.c b/extension/src/config/high_perf_compute_and_ai/base_layer.c index 20c95062c..1b3726b95 100644 --- a/extension/src/config/high_perf_compute_and_ai/base_layer.c +++ b/extension/src/config/high_perf_compute_and_ai/base_layer.c @@ -11,6 +11,6 @@ * ========================================================================= */ -#include "include/config/high_perf_compute_and_ai/base_layer.h" +#include "config/high_perf_compute_and_ai/base_layer.h" kg_high_perf_compute_ai_config_t king_high_perf_compute_ai_config; diff --git a/extension/src/config/high_perf_compute_and_ai/config.c b/extension/src/config/high_perf_compute_and_ai/config.c index 32e0d3896..885460880 100644 --- a/extension/src/config/high_perf_compute_and_ai/config.c +++ b/extension/src/config/high_perf_compute_and_ai/config.c @@ -11,18 +11,19 @@ * ========================================================================= */ -#include "include/config/high_perf_compute_and_ai/config.h" -#include "include/config/high_perf_compute_and_ai/base_layer.h" -#include "include/king_globals.h" +#include "config/high_perf_compute_and_ai/config.h" +#include "config/high_perf_compute_and_ai/base_layer.h" +#include "php_king/globals.h" -#include "include/validation/config_param/validate_bool.h" -#include "include/validation/config_param/validate_cpu_affinity_map_string.h" -#include "include/validation/config_param/validate_positive_long.h" -#include "include/validation/config_param/validate_string_from_allowlist.h" +#include "validation/config_param/validate_bool.h" +#include "validation/config_param/validate_cpu_affinity_map_string.h" +#include "validation/config_param/validate_positive_long.h" +#include "validation/config_param/validate_string_from_allowlist.h" #include "php.h" #include #include +#include static int kg_high_perf_apply_bool_field(zval *value, const char *name, bool *target) { @@ -53,18 +54,98 @@ static int kg_validate_non_negative_long_local(zval *value, zend_long *target) } static const char *k_high_perf_gpu_backend_allowed[] = {"auto", "cuda", "rocm", "sycl", NULL}; +static const char *k_high_perf_inference_profile_allowed[] = {"auto", "gpu", "cpu", NULL}; -int kg_config_high_perf_compute_and_ai_apply_userland_config(zval *config_arr) +static int kg_high_perf_apply_string_field(zval *value, const char *name, char **target) { - zval *value; - zend_string *key; + if (Z_TYPE_P(value) != IS_STRING) { + zend_throw_exception_ex( + spl_ce_InvalidArgumentException, + 0, + "Invalid type for %s. A string is required.", + name + ); + return FAILURE; + } - if (!king_globals.is_userland_override_allowed) { - zend_throw_exception_ex(spl_ce_InvalidArgumentException, 0, - "Configuration override from userland is disabled by system administrator."); + if (*target) { + pefree(*target, 1); + } + *target = pestrdup(Z_STRVAL_P(value), 1); + return SUCCESS; +} + +static int kg_high_perf_apply_positive_double_field(zval *value, const char *name, double *target) +{ + double number; + + if (Z_TYPE_P(value) == IS_LONG) { + number = (double) Z_LVAL_P(value); + } else if (Z_TYPE_P(value) == IS_DOUBLE) { + number = Z_DVAL_P(value); + } else { + zend_throw_exception_ex( + spl_ce_InvalidArgumentException, + 0, + "Invalid type for %s. A positive finite number is required.", + name + ); + return FAILURE; + } + + if (!isfinite(number) || number <= 0.0) { + zend_throw_exception_ex( + spl_ce_InvalidArgumentException, + 0, + "Invalid value for %s. A positive finite number is required.", + name + ); return FAILURE; } + *target = number; + return SUCCESS; +} + +static int kg_high_perf_apply_non_negative_double_field(zval *value, const char *name, double *target) +{ + double number; + + if (Z_TYPE_P(value) == IS_LONG) { + number = (double) Z_LVAL_P(value); + } else if (Z_TYPE_P(value) == IS_DOUBLE) { + number = Z_DVAL_P(value); + } else { + zend_throw_exception_ex( + spl_ce_InvalidArgumentException, + 0, + "Invalid type for %s. A non-negative finite number is required.", + name + ); + return FAILURE; + } + + if (!isfinite(number) || number < 0.0) { + zend_throw_exception_ex( + spl_ce_InvalidArgumentException, + 0, + "Invalid value for %s. A non-negative finite number is required.", + name + ); + return FAILURE; + } + + *target = number; + return SUCCESS; +} + +int kg_config_high_perf_compute_and_ai_apply_userland_config_to( + kg_high_perf_compute_ai_config_t *target, + zval *config_arr) +{ + zval *value; + zend_string *key; + if (Z_TYPE_P(config_arr) != IS_ARRAY) { zend_throw_exception_ex(spl_ce_InvalidArgumentException, 0, "Configuration must be provided as an array."); @@ -77,37 +158,119 @@ int kg_config_high_perf_compute_and_ai_apply_userland_config(zval *config_arr) } if (zend_string_equals_literal(key, "dataframe_enable")) { - if (kg_high_perf_apply_bool_field(value, "dataframe_enable", &king_high_perf_compute_ai_config.dataframe_enable) != SUCCESS) return FAILURE; + if (kg_high_perf_apply_bool_field(value, "dataframe_enable", &target->dataframe_enable) != SUCCESS) return FAILURE; } else if (zend_string_equals_literal(key, "dataframe_memory_limit_mb")) { - if (kg_validate_positive_long(value, &king_high_perf_compute_ai_config.dataframe_memory_limit_mb) != SUCCESS) return FAILURE; + if (kg_validate_positive_long(value, &target->dataframe_memory_limit_mb) != SUCCESS) return FAILURE; } else if (zend_string_equals_literal(key, "dataframe_string_interning_enable")) { - if (kg_high_perf_apply_bool_field(value, "dataframe_string_interning_enable", &king_high_perf_compute_ai_config.dataframe_string_interning_enable) != SUCCESS) return FAILURE; + if (kg_high_perf_apply_bool_field(value, "dataframe_string_interning_enable", &target->dataframe_string_interning_enable) != SUCCESS) return FAILURE; } else if (zend_string_equals_literal(key, "dataframe_cpu_parallelism_default")) { - if (kg_validate_non_negative_long_local(value, &king_high_perf_compute_ai_config.dataframe_cpu_parallelism_default) != SUCCESS) return FAILURE; + if (kg_validate_non_negative_long_local(value, &target->dataframe_cpu_parallelism_default) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_with_memory")) { + if (kg_high_perf_apply_bool_field(value, "inference_with_memory", &target->inference_with_memory) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_context_tokens")) { + if (kg_validate_positive_long(value, &target->inference_context_tokens) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_kv_page_tokens")) { + if (kg_validate_positive_long(value, &target->inference_kv_page_tokens) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_kv_element_bytes")) { + if (kg_validate_positive_long(value, &target->inference_kv_element_bytes) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_preferred_model_profile")) { + if (kg_validate_string_from_allowlist(value, k_high_perf_inference_profile_allowed, &target->inference_preferred_model_profile) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_models")) { + if (kg_high_perf_apply_string_field(value, "inference_models", &target->inference_models) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_cpu_model_name")) { + if (kg_high_perf_apply_string_field(value, "inference_cpu_model_name", &target->inference_cpu_model_name) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_cpu_model_artifact")) { + if (kg_high_perf_apply_string_field(value, "inference_cpu_model_artifact", &target->inference_cpu_model_artifact) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_gpu_model_name")) { + if (kg_high_perf_apply_string_field(value, "inference_gpu_model_name", &target->inference_gpu_model_name) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_gpu_model_artifact")) { + if (kg_high_perf_apply_string_field(value, "inference_gpu_model_artifact", &target->inference_gpu_model_artifact) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_gpu_max_gpu_layers")) { + if (kg_validate_non_negative_long_local(value, &target->inference_gpu_max_gpu_layers) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_gpu_vram_reserve_mb")) { + if (kg_validate_non_negative_long_local(value, &target->inference_gpu_vram_reserve_mb) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_gpu_min_free_vram_mb")) { + if (kg_validate_non_negative_long_local(value, &target->inference_gpu_min_free_vram_mb) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_gpu_allow_system_ram_offload")) { + if (kg_high_perf_apply_bool_field(value, "inference_gpu_allow_system_ram_offload", &target->inference_gpu_allow_system_ram_offload) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_gpu_system_ram_offload_max_mb")) { + if (kg_validate_non_negative_long_local(value, &target->inference_gpu_system_ram_offload_max_mb) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_gpu_system_ram_offload_min_free_mb")) { + if (kg_validate_non_negative_long_local(value, &target->inference_gpu_system_ram_offload_min_free_mb) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_gpu_thermal_sensor_path")) { + if (kg_high_perf_apply_string_field(value, "inference_gpu_thermal_sensor_path", &target->inference_gpu_thermal_sensor_path) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_gpu_thermal_sensor_command")) { + if (kg_high_perf_apply_string_field(value, "inference_gpu_thermal_sensor_command", &target->inference_gpu_thermal_sensor_command) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_gpu_thermal_max_temperature_c")) { + if (kg_high_perf_apply_positive_double_field(value, "inference_gpu_thermal_max_temperature_c", &target->inference_gpu_thermal_max_temperature_c) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_gpu_thermal_check_interval_sec")) { + if (kg_validate_non_negative_long_local(value, &target->inference_gpu_thermal_check_interval_sec) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_gpu_allow_unmonitored")) { + if (kg_high_perf_apply_bool_field(value, "inference_gpu_allow_unmonitored", &target->inference_gpu_allow_unmonitored) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_gpu_power_sensor_command")) { + if (kg_high_perf_apply_string_field(value, "inference_gpu_power_sensor_command", &target->inference_gpu_power_sensor_command) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_gpu_power_max_watts")) { + if (kg_high_perf_apply_non_negative_double_field(value, "inference_gpu_power_max_watts", &target->inference_gpu_power_max_watts) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_gpu_power_check_interval_sec")) { + if (kg_validate_non_negative_long_local(value, &target->inference_gpu_power_check_interval_sec) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_gpu_batch_prefill_experimental_enable")) { + if (kg_high_perf_apply_bool_field(value, "inference_gpu_batch_prefill_experimental_enable", &target->inference_gpu_batch_prefill_experimental_enable) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_cuda_numeric_compare_enable")) { + if (kg_high_perf_apply_bool_field(value, "inference_cuda_numeric_compare_enable", &target->inference_cuda_numeric_compare_enable) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_cuda_numeric_compare_max_values")) { + if (kg_validate_positive_long(value, &target->inference_cuda_numeric_compare_max_values) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_llm_cache_enable")) { + if (kg_high_perf_apply_bool_field(value, "inference_llm_cache_enable", &target->inference_llm_cache_enable) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_llm_cache_path")) { + if (kg_high_perf_apply_string_field(value, "inference_llm_cache_path", &target->inference_llm_cache_path) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_llm_cache_min_free_mb")) { + if (kg_validate_non_negative_long_local(value, &target->inference_llm_cache_min_free_mb) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_llm_cache_fail_closed")) { + if (kg_high_perf_apply_bool_field(value, "inference_llm_cache_fail_closed", &target->inference_llm_cache_fail_closed) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_llm_cache_disk_alert_webhook")) { + if (kg_high_perf_apply_string_field(value, "inference_llm_cache_disk_alert_webhook", &target->inference_llm_cache_disk_alert_webhook) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_llm_cache_disk_alert_mcp_service")) { + if (kg_high_perf_apply_string_field(value, "inference_llm_cache_disk_alert_mcp_service", &target->inference_llm_cache_disk_alert_mcp_service) != SUCCESS) return FAILURE; + } else if (zend_string_equals_literal(key, "inference_llm_cache_disk_alert_mcp_method")) { + if (kg_high_perf_apply_string_field(value, "inference_llm_cache_disk_alert_mcp_method", &target->inference_llm_cache_disk_alert_mcp_method) != SUCCESS) return FAILURE; } else if (zend_string_equals_literal(key, "gpu_bindings_enable")) { - if (kg_high_perf_apply_bool_field(value, "gpu_bindings_enable", &king_high_perf_compute_ai_config.gpu_bindings_enable) != SUCCESS) return FAILURE; + if (kg_high_perf_apply_bool_field(value, "gpu_bindings_enable", &target->gpu_bindings_enable) != SUCCESS) return FAILURE; } else if (zend_string_equals_literal(key, "gpu_default_backend")) { - if (kg_validate_string_from_allowlist(value, k_high_perf_gpu_backend_allowed, &king_high_perf_compute_ai_config.gpu_default_backend) != SUCCESS) return FAILURE; + if (kg_validate_string_from_allowlist(value, k_high_perf_gpu_backend_allowed, &target->gpu_default_backend) != SUCCESS) return FAILURE; } else if (zend_string_equals_literal(key, "worker_gpu_affinity_map")) { - if (kg_validate_cpu_affinity_map_string(value, &king_high_perf_compute_ai_config.worker_gpu_affinity_map) != SUCCESS) return FAILURE; + if (kg_validate_cpu_affinity_map_string(value, &target->worker_gpu_affinity_map) != SUCCESS) return FAILURE; } else if (zend_string_equals_literal(key, "gpu_memory_preallocation_mb")) { - if (kg_validate_positive_long(value, &king_high_perf_compute_ai_config.gpu_memory_preallocation_mb) != SUCCESS) return FAILURE; + if (kg_validate_positive_long(value, &target->gpu_memory_preallocation_mb) != SUCCESS) return FAILURE; } else if (zend_string_equals_literal(key, "gpu_p2p_enable")) { - if (kg_high_perf_apply_bool_field(value, "gpu_p2p_enable", &king_high_perf_compute_ai_config.gpu_p2p_enable) != SUCCESS) return FAILURE; + if (kg_high_perf_apply_bool_field(value, "gpu_p2p_enable", &target->gpu_p2p_enable) != SUCCESS) return FAILURE; } else if (zend_string_equals_literal(key, "storage_enable_directstorage")) { - if (kg_high_perf_apply_bool_field(value, "storage_enable_directstorage", &king_high_perf_compute_ai_config.storage_enable_directstorage) != SUCCESS) return FAILURE; + if (kg_high_perf_apply_bool_field(value, "storage_enable_directstorage", &target->storage_enable_directstorage) != SUCCESS) return FAILURE; } else if (zend_string_equals_literal(key, "cuda_enable_tensor_cores")) { - if (kg_high_perf_apply_bool_field(value, "cuda_enable_tensor_cores", &king_high_perf_compute_ai_config.cuda_enable_tensor_cores) != SUCCESS) return FAILURE; + if (kg_high_perf_apply_bool_field(value, "cuda_enable_tensor_cores", &target->cuda_enable_tensor_cores) != SUCCESS) return FAILURE; } else if (zend_string_equals_literal(key, "cuda_stream_pool_size")) { - if (kg_validate_positive_long(value, &king_high_perf_compute_ai_config.cuda_stream_pool_size) != SUCCESS) return FAILURE; + if (kg_validate_positive_long(value, &target->cuda_stream_pool_size) != SUCCESS) return FAILURE; } else if (zend_string_equals_literal(key, "rocm_enable_gfx_optimizations")) { - if (kg_high_perf_apply_bool_field(value, "rocm_enable_gfx_optimizations", &king_high_perf_compute_ai_config.rocm_enable_gfx_optimizations) != SUCCESS) return FAILURE; + if (kg_high_perf_apply_bool_field(value, "rocm_enable_gfx_optimizations", &target->rocm_enable_gfx_optimizations) != SUCCESS) return FAILURE; } else if (zend_string_equals_literal(key, "arc_enable_xmx_optimizations")) { - if (kg_high_perf_apply_bool_field(value, "arc_enable_xmx_optimizations", &king_high_perf_compute_ai_config.arc_enable_xmx_optimizations) != SUCCESS) return FAILURE; + if (kg_high_perf_apply_bool_field(value, "arc_enable_xmx_optimizations", &target->arc_enable_xmx_optimizations) != SUCCESS) return FAILURE; } else if (zend_string_equals_literal(key, "arc_video_acceleration_enable")) { - if (kg_high_perf_apply_bool_field(value, "arc_video_acceleration_enable", &king_high_perf_compute_ai_config.arc_video_acceleration_enable) != SUCCESS) return FAILURE; + if (kg_high_perf_apply_bool_field(value, "arc_video_acceleration_enable", &target->arc_video_acceleration_enable) != SUCCESS) return FAILURE; } } ZEND_HASH_FOREACH_END(); return SUCCESS; } + +int kg_config_high_perf_compute_and_ai_apply_userland_config(zval *config_arr) +{ + if (!king_globals.is_userland_override_allowed) { + zend_throw_exception_ex(spl_ce_InvalidArgumentException, 0, + "Configuration override from userland is disabled by system administrator."); + return FAILURE; + } + + return kg_config_high_perf_compute_and_ai_apply_userland_config_to( + &king_high_perf_compute_ai_config, + config_arr + ); +} diff --git a/extension/src/config/high_perf_compute_and_ai/default.c b/extension/src/config/high_perf_compute_and_ai/default.c index e3b1c46d9..94e19ad94 100644 --- a/extension/src/config/high_perf_compute_and_ai/default.c +++ b/extension/src/config/high_perf_compute_and_ai/default.c @@ -11,8 +11,8 @@ * ========================================================================= */ -#include "include/config/high_perf_compute_and_ai/default.h" -#include "include/config/high_perf_compute_and_ai/base_layer.h" +#include "config/high_perf_compute_and_ai/default.h" +#include "config/high_perf_compute_and_ai/base_layer.h" void kg_config_high_perf_compute_and_ai_defaults_load(void) { @@ -20,6 +20,40 @@ void kg_config_high_perf_compute_and_ai_defaults_load(void) king_high_perf_compute_ai_config.dataframe_memory_limit_mb = 1024; king_high_perf_compute_ai_config.dataframe_string_interning_enable = true; king_high_perf_compute_ai_config.dataframe_cpu_parallelism_default = 0; + king_high_perf_compute_ai_config.inference_with_memory = false; + king_high_perf_compute_ai_config.inference_context_tokens = 2048; + king_high_perf_compute_ai_config.inference_kv_page_tokens = 16; + king_high_perf_compute_ai_config.inference_kv_element_bytes = 2; + king_high_perf_compute_ai_config.inference_preferred_model_profile = pestrdup("auto", 1); + king_high_perf_compute_ai_config.inference_models = pestrdup("", 1); + king_high_perf_compute_ai_config.inference_cpu_model_name = pestrdup("gemma3:1b", 1); + king_high_perf_compute_ai_config.inference_cpu_model_artifact = pestrdup("", 1); + king_high_perf_compute_ai_config.inference_gpu_model_name = pestrdup("gemma4:12b", 1); + king_high_perf_compute_ai_config.inference_gpu_model_artifact = pestrdup("", 1); + king_high_perf_compute_ai_config.inference_gpu_max_gpu_layers = 0; + king_high_perf_compute_ai_config.inference_gpu_vram_reserve_mb = 2048; + king_high_perf_compute_ai_config.inference_gpu_min_free_vram_mb = 4096; + king_high_perf_compute_ai_config.inference_gpu_allow_system_ram_offload = false; + king_high_perf_compute_ai_config.inference_gpu_system_ram_offload_max_mb = 0; + king_high_perf_compute_ai_config.inference_gpu_system_ram_offload_min_free_mb = 8192; + king_high_perf_compute_ai_config.inference_gpu_thermal_sensor_path = pestrdup("", 1); + king_high_perf_compute_ai_config.inference_gpu_thermal_sensor_command = pestrdup("", 1); + king_high_perf_compute_ai_config.inference_gpu_thermal_max_temperature_c = 78.0; + king_high_perf_compute_ai_config.inference_gpu_thermal_check_interval_sec = 15; + king_high_perf_compute_ai_config.inference_gpu_allow_unmonitored = false; + king_high_perf_compute_ai_config.inference_gpu_power_sensor_command = pestrdup("", 1); + king_high_perf_compute_ai_config.inference_gpu_power_max_watts = 0.0; + king_high_perf_compute_ai_config.inference_gpu_power_check_interval_sec = 15; + king_high_perf_compute_ai_config.inference_gpu_batch_prefill_experimental_enable = false; + king_high_perf_compute_ai_config.inference_cuda_numeric_compare_enable = false; + king_high_perf_compute_ai_config.inference_cuda_numeric_compare_max_values = 8; + king_high_perf_compute_ai_config.inference_llm_cache_enable = false; + king_high_perf_compute_ai_config.inference_llm_cache_path = pestrdup("/tmp/king-llm-cache", 1); + king_high_perf_compute_ai_config.inference_llm_cache_min_free_mb = 5120; + king_high_perf_compute_ai_config.inference_llm_cache_fail_closed = true; + king_high_perf_compute_ai_config.inference_llm_cache_disk_alert_webhook = pestrdup("", 1); + king_high_perf_compute_ai_config.inference_llm_cache_disk_alert_mcp_service = pestrdup("", 1); + king_high_perf_compute_ai_config.inference_llm_cache_disk_alert_mcp_method = pestrdup("", 1); king_high_perf_compute_ai_config.gpu_bindings_enable = false; king_high_perf_compute_ai_config.gpu_default_backend = NULL; diff --git a/extension/src/config/high_perf_compute_and_ai/index.c b/extension/src/config/high_perf_compute_and_ai/index.c index c59c43f1a..de6de7629 100644 --- a/extension/src/config/high_perf_compute_and_ai/index.c +++ b/extension/src/config/high_perf_compute_and_ai/index.c @@ -10,9 +10,9 @@ * ========================================================================= */ -#include "include/config/high_perf_compute_and_ai/index.h" -#include "include/config/high_perf_compute_and_ai/default.h" -#include "include/config/high_perf_compute_and_ai/ini.h" +#include "config/high_perf_compute_and_ai/index.h" +#include "config/high_perf_compute_and_ai/default.h" +#include "config/high_perf_compute_and_ai/ini.h" void kg_config_high_perf_compute_and_ai_init(void) { diff --git a/extension/src/config/high_perf_compute_and_ai/ini.c b/extension/src/config/high_perf_compute_and_ai/ini.c index 886fee6ce..f2c4c9b1e 100644 --- a/extension/src/config/high_perf_compute_and_ai/ini.c +++ b/extension/src/config/high_perf_compute_and_ai/ini.c @@ -11,13 +11,20 @@ * ========================================================================= */ -#include "include/config/high_perf_compute_and_ai/ini.h" -#include "include/config/high_perf_compute_and_ai/base_layer.h" +#include "config/high_perf_compute_and_ai/ini.h" +#include "config/high_perf_compute_and_ai/base_layer.h" +#include "php_king/init.h" +#include "validation/config_param/validate_cpu_affinity_map_string.h" #include "php.h" #include #include #include +#include +#include +#include +#include +#include static void high_perf_replace_string(char **target, zend_string *value) { @@ -44,6 +51,173 @@ static ZEND_INI_MH(OnUpdateAiPositiveLong) king_high_perf_compute_ai_config.gpu_memory_preallocation_mb = val; } else if (zend_string_equals_literal(entry->name, "king.cuda_stream_pool_size")) { king_high_perf_compute_ai_config.cuda_stream_pool_size = val; + } else if (zend_string_equals_literal(entry->name, "king.inference_context_tokens")) { + king_high_perf_compute_ai_config.inference_context_tokens = val; + } else if (zend_string_equals_literal(entry->name, "king.inference_kv_page_tokens")) { + king_high_perf_compute_ai_config.inference_kv_page_tokens = val; + } else if (zend_string_equals_literal(entry->name, "king.inference_kv_element_bytes")) { + king_high_perf_compute_ai_config.inference_kv_element_bytes = val; + } else if (zend_string_equals_literal(entry->name, "king.inference_cuda_numeric_compare_max_values")) { + king_high_perf_compute_ai_config.inference_cuda_numeric_compare_max_values = val; + } + + return SUCCESS; +} + +static ZEND_INI_MH(OnUpdateAiNonNegativeLong) +{ + zend_long val = ZEND_STRTOL(ZSTR_VAL(new_value), NULL, 10); + + if (val < 0) { + zend_throw_exception_ex(spl_ce_InvalidArgumentException, 0, + "Invalid value for an AI/Compute directive. A non-negative integer is required."); + return FAILURE; + } + + if (zend_string_equals_literal(entry->name, "king.inference_gpu_max_gpu_layers")) { + king_high_perf_compute_ai_config.inference_gpu_max_gpu_layers = val; + } else if (zend_string_equals_literal(entry->name, "king.inference_gpu_vram_reserve_mb")) { + king_high_perf_compute_ai_config.inference_gpu_vram_reserve_mb = val; + } else if (zend_string_equals_literal(entry->name, "king.inference_gpu_min_free_vram_mb")) { + king_high_perf_compute_ai_config.inference_gpu_min_free_vram_mb = val; + } else if (zend_string_equals_literal(entry->name, "king.inference_gpu_system_ram_offload_max_mb")) { + king_high_perf_compute_ai_config.inference_gpu_system_ram_offload_max_mb = val; + } else if (zend_string_equals_literal(entry->name, "king.inference_gpu_system_ram_offload_min_free_mb")) { + king_high_perf_compute_ai_config.inference_gpu_system_ram_offload_min_free_mb = val; + } else if (zend_string_equals_literal(entry->name, "king.inference_gpu_thermal_check_interval_sec")) { + king_high_perf_compute_ai_config.inference_gpu_thermal_check_interval_sec = val; + } else if (zend_string_equals_literal(entry->name, "king.inference_gpu_power_check_interval_sec")) { + king_high_perf_compute_ai_config.inference_gpu_power_check_interval_sec = val; + } else if (zend_string_equals_literal(entry->name, "king.inference_llm_cache_min_free_mb")) { + king_high_perf_compute_ai_config.inference_llm_cache_min_free_mb = val; + } + + return SUCCESS; +} + +static ZEND_INI_MH(OnUpdateAiNonNegativeDouble) +{ + char *endptr; + double val; + + errno = 0; + val = strtod(ZSTR_VAL(new_value), &endptr); + while (*endptr != '\0' && isspace((unsigned char) *endptr)) { + endptr++; + } + if (errno != 0 + || endptr == ZSTR_VAL(new_value) + || *endptr != '\0' + || !isfinite(val) + || val < 0.0) { + zend_throw_exception_ex(spl_ce_InvalidArgumentException, 0, + "Invalid value for an AI/Compute directive. A non-negative finite number is required."); + return FAILURE; + } + + if (zend_string_equals_literal(entry->name, "king.inference_gpu_power_max_watts")) { + king_high_perf_compute_ai_config.inference_gpu_power_max_watts = val; + } + + return SUCCESS; +} + +static ZEND_INI_MH(OnUpdateAiPositiveDouble) +{ + char *endptr; + double val; + + errno = 0; + val = strtod(ZSTR_VAL(new_value), &endptr); + while (*endptr != '\0' && isspace((unsigned char) *endptr)) { + endptr++; + } + if (errno != 0 + || endptr == ZSTR_VAL(new_value) + || *endptr != '\0' + || !isfinite(val) + || val <= 0.0) { + zend_throw_exception_ex(spl_ce_InvalidArgumentException, 0, + "Invalid value for an AI/Compute directive. A positive finite number is required."); + return FAILURE; + } + + if (zend_string_equals_literal(entry->name, "king.inference_gpu_thermal_max_temperature_c")) { + king_high_perf_compute_ai_config.inference_gpu_thermal_max_temperature_c = val; + } + + return SUCCESS; +} + +static ZEND_INI_MH(OnUpdateInferenceProfile) +{ + const char *allowed[] = {"auto", "gpu", "cpu", NULL}; + bool is_allowed = false; + + for (int i = 0; allowed[i] != NULL; i++) { + if (strcasecmp(ZSTR_VAL(new_value), allowed[i]) == 0) { + is_allowed = true; + break; + } + } + + if (!is_allowed) { + zend_throw_exception_ex(spl_ce_InvalidArgumentException, 0, + "Invalid value for inference model profile. Must be one of 'auto', 'gpu', or 'cpu'."); + return FAILURE; + } + + high_perf_replace_string( + &king_high_perf_compute_ai_config.inference_preferred_model_profile, + new_value + ); + return SUCCESS; +} + +static ZEND_INI_MH(OnUpdateInferenceString) +{ + if (zend_string_equals_literal(entry->name, "king.inference_cpu_model_name")) { + high_perf_replace_string(&king_high_perf_compute_ai_config.inference_cpu_model_name, new_value); + } else if (zend_string_equals_literal(entry->name, "king.inference_models")) { + high_perf_replace_string(&king_high_perf_compute_ai_config.inference_models, new_value); + } else if (zend_string_equals_literal(entry->name, "king.inference_cpu_model_artifact")) { + high_perf_replace_string(&king_high_perf_compute_ai_config.inference_cpu_model_artifact, new_value); + } else if (zend_string_equals_literal(entry->name, "king.inference_gpu_model_name")) { + high_perf_replace_string(&king_high_perf_compute_ai_config.inference_gpu_model_name, new_value); + } else if (zend_string_equals_literal(entry->name, "king.inference_gpu_model_artifact")) { + high_perf_replace_string(&king_high_perf_compute_ai_config.inference_gpu_model_artifact, new_value); + } else if (zend_string_equals_literal(entry->name, "king.inference_gpu_thermal_sensor_path")) { + high_perf_replace_string( + &king_high_perf_compute_ai_config.inference_gpu_thermal_sensor_path, + new_value + ); + } else if (zend_string_equals_literal(entry->name, "king.inference_gpu_thermal_sensor_command")) { + high_perf_replace_string( + &king_high_perf_compute_ai_config.inference_gpu_thermal_sensor_command, + new_value + ); + } else if (zend_string_equals_literal(entry->name, "king.inference_gpu_power_sensor_command")) { + high_perf_replace_string( + &king_high_perf_compute_ai_config.inference_gpu_power_sensor_command, + new_value + ); + } else if (zend_string_equals_literal(entry->name, "king.inference_llm_cache_path")) { + high_perf_replace_string(&king_high_perf_compute_ai_config.inference_llm_cache_path, new_value); + } else if (zend_string_equals_literal(entry->name, "king.inference_llm_cache_disk_alert_webhook")) { + high_perf_replace_string( + &king_high_perf_compute_ai_config.inference_llm_cache_disk_alert_webhook, + new_value + ); + } else if (zend_string_equals_literal(entry->name, "king.inference_llm_cache_disk_alert_mcp_service")) { + high_perf_replace_string( + &king_high_perf_compute_ai_config.inference_llm_cache_disk_alert_mcp_service, + new_value + ); + } else if (zend_string_equals_literal(entry->name, "king.inference_llm_cache_disk_alert_mcp_method")) { + high_perf_replace_string( + &king_high_perf_compute_ai_config.inference_llm_cache_disk_alert_mcp_method, + new_value + ); } return SUCCESS; @@ -71,15 +245,64 @@ static ZEND_INI_MH(OnUpdateGpuBackend) return SUCCESS; } +static ZEND_INI_MH(OnUpdateWorkerGpuAffinityString) +{ + zval zv; + + ZVAL_STR_COPY(&zv, new_value); + if (kg_validate_cpu_affinity_map_string(&zv, &king_high_perf_compute_ai_config.worker_gpu_affinity_map) != SUCCESS) { + zval_ptr_dtor(&zv); + return FAILURE; + } + + zval_ptr_dtor(&zv); + return SUCCESS; +} + PHP_INI_BEGIN() STD_PHP_INI_ENTRY("king.dataframe_enable", "1", PHP_INI_SYSTEM, OnUpdateBool, dataframe_enable, kg_high_perf_compute_ai_config_t, king_high_perf_compute_ai_config) ZEND_INI_ENTRY_EX("king.dataframe_memory_limit_mb", "1024", PHP_INI_SYSTEM, OnUpdateAiPositiveLong, NULL) STD_PHP_INI_ENTRY("king.dataframe_string_interning_enable", "1", PHP_INI_SYSTEM, OnUpdateBool, dataframe_string_interning_enable, kg_high_perf_compute_ai_config_t, king_high_perf_compute_ai_config) STD_PHP_INI_ENTRY("king.dataframe_cpu_parallelism_default", "0", PHP_INI_SYSTEM, OnUpdateLong, dataframe_cpu_parallelism_default, kg_high_perf_compute_ai_config_t, king_high_perf_compute_ai_config) + STD_PHP_INI_ENTRY("king.inference_with_memory", "0", PHP_INI_SYSTEM, OnUpdateBool, inference_with_memory, kg_high_perf_compute_ai_config_t, king_high_perf_compute_ai_config) + ZEND_INI_ENTRY_EX("king.inference_context_tokens", "2048", PHP_INI_SYSTEM, OnUpdateAiPositiveLong, NULL) + ZEND_INI_ENTRY_EX("king.inference_kv_page_tokens", "16", PHP_INI_SYSTEM, OnUpdateAiPositiveLong, NULL) + ZEND_INI_ENTRY_EX("king.inference_kv_element_bytes", "2", PHP_INI_SYSTEM, OnUpdateAiPositiveLong, NULL) + ZEND_INI_ENTRY_EX("king.inference_preferred_model_profile", "auto", PHP_INI_SYSTEM, OnUpdateInferenceProfile, NULL) + ZEND_INI_ENTRY_EX("king.inference_models", "", PHP_INI_SYSTEM, OnUpdateInferenceString, NULL) + ZEND_INI_ENTRY_EX("king.inference_cpu_model_name", "gemma3:1b", PHP_INI_SYSTEM, OnUpdateInferenceString, NULL) + ZEND_INI_ENTRY_EX("king.inference_cpu_model_artifact", "", PHP_INI_SYSTEM, OnUpdateInferenceString, NULL) + ZEND_INI_ENTRY_EX("king.inference_gpu_model_name", "gemma4:12b", PHP_INI_SYSTEM, OnUpdateInferenceString, NULL) + ZEND_INI_ENTRY_EX("king.inference_gpu_model_artifact", "", PHP_INI_SYSTEM, OnUpdateInferenceString, NULL) + ZEND_INI_ENTRY_EX("king.inference_gpu_max_gpu_layers", "0", PHP_INI_SYSTEM, OnUpdateAiNonNegativeLong, NULL) + ZEND_INI_ENTRY_EX("king.inference_gpu_vram_reserve_mb", "2048", PHP_INI_SYSTEM, OnUpdateAiNonNegativeLong, NULL) + ZEND_INI_ENTRY_EX("king.inference_gpu_min_free_vram_mb", "4096", PHP_INI_SYSTEM, OnUpdateAiNonNegativeLong, NULL) + STD_PHP_INI_ENTRY("king.inference_gpu_allow_system_ram_offload", "0", PHP_INI_SYSTEM, OnUpdateBool, inference_gpu_allow_system_ram_offload, kg_high_perf_compute_ai_config_t, king_high_perf_compute_ai_config) + ZEND_INI_ENTRY_EX("king.inference_gpu_system_ram_offload_max_mb", "0", PHP_INI_SYSTEM, OnUpdateAiNonNegativeLong, NULL) + ZEND_INI_ENTRY_EX("king.inference_gpu_system_ram_offload_min_free_mb", "8192", PHP_INI_SYSTEM, OnUpdateAiNonNegativeLong, NULL) + ZEND_INI_ENTRY_EX("king.inference_gpu_thermal_sensor_path", "", PHP_INI_SYSTEM, OnUpdateInferenceString, NULL) + ZEND_INI_ENTRY_EX("king.inference_gpu_thermal_sensor_command", "", PHP_INI_SYSTEM, OnUpdateInferenceString, NULL) + ZEND_INI_ENTRY_EX("king.inference_gpu_thermal_max_temperature_c", "78", PHP_INI_SYSTEM, OnUpdateAiPositiveDouble, NULL) + ZEND_INI_ENTRY_EX("king.inference_gpu_thermal_check_interval_sec", "15", PHP_INI_SYSTEM, OnUpdateAiNonNegativeLong, NULL) + STD_PHP_INI_ENTRY("king.inference_gpu_allow_unmonitored", "0", PHP_INI_SYSTEM, OnUpdateBool, inference_gpu_allow_unmonitored, kg_high_perf_compute_ai_config_t, king_high_perf_compute_ai_config) + ZEND_INI_ENTRY_EX("king.inference_gpu_power_sensor_command", "", PHP_INI_SYSTEM, OnUpdateInferenceString, NULL) + ZEND_INI_ENTRY_EX("king.inference_gpu_power_max_watts", "0", PHP_INI_SYSTEM, OnUpdateAiNonNegativeDouble, NULL) + ZEND_INI_ENTRY_EX("king.inference_gpu_power_check_interval_sec", "15", PHP_INI_SYSTEM, OnUpdateAiNonNegativeLong, NULL) + STD_PHP_INI_ENTRY("king.inference_gpu_batch_prefill_experimental_enable", "0", PHP_INI_SYSTEM, OnUpdateBool, inference_gpu_batch_prefill_experimental_enable, kg_high_perf_compute_ai_config_t, king_high_perf_compute_ai_config) + STD_PHP_INI_ENTRY("king.inference_cuda_numeric_compare_enable", "0", PHP_INI_SYSTEM, OnUpdateBool, inference_cuda_numeric_compare_enable, kg_high_perf_compute_ai_config_t, king_high_perf_compute_ai_config) + ZEND_INI_ENTRY_EX("king.inference_cuda_numeric_compare_max_values", "8", PHP_INI_SYSTEM, OnUpdateAiPositiveLong, NULL) + STD_PHP_INI_ENTRY("king.inference_llm_cache_enable", "0", PHP_INI_SYSTEM, OnUpdateBool, inference_llm_cache_enable, kg_high_perf_compute_ai_config_t, king_high_perf_compute_ai_config) + ZEND_INI_ENTRY_EX("king.inference_llm_cache_path", "/tmp/king-llm-cache", PHP_INI_SYSTEM, OnUpdateInferenceString, NULL) + ZEND_INI_ENTRY_EX("king.inference_llm_cache_min_free_mb", "5120", PHP_INI_SYSTEM, OnUpdateAiNonNegativeLong, NULL) + STD_PHP_INI_ENTRY("king.inference_llm_cache_fail_closed", "1", PHP_INI_SYSTEM, OnUpdateBool, inference_llm_cache_fail_closed, kg_high_perf_compute_ai_config_t, king_high_perf_compute_ai_config) + ZEND_INI_ENTRY_EX("king.inference_llm_cache_disk_alert_webhook", "", PHP_INI_SYSTEM, OnUpdateInferenceString, NULL) + ZEND_INI_ENTRY_EX("king.inference_llm_cache_disk_alert_mcp_service", "", PHP_INI_SYSTEM, OnUpdateInferenceString, NULL) + ZEND_INI_ENTRY_EX("king.inference_llm_cache_disk_alert_mcp_method", "", PHP_INI_SYSTEM, OnUpdateInferenceString, NULL) + STD_PHP_INI_ENTRY("king.gpu_bindings_enable", "0", PHP_INI_SYSTEM, OnUpdateBool, gpu_bindings_enable, kg_high_perf_compute_ai_config_t, king_high_perf_compute_ai_config) ZEND_INI_ENTRY_EX("king.gpu_default_backend", "auto", PHP_INI_SYSTEM, OnUpdateGpuBackend, NULL) - STD_PHP_INI_ENTRY("king.worker_gpu_affinity_map", "", PHP_INI_SYSTEM, OnUpdateString, worker_gpu_affinity_map, kg_high_perf_compute_ai_config_t, king_high_perf_compute_ai_config) + ZEND_INI_ENTRY_EX("king.worker_gpu_affinity_map", "", PHP_INI_SYSTEM, OnUpdateWorkerGpuAffinityString, NULL) ZEND_INI_ENTRY_EX("king.gpu_memory_preallocation_mb", "2048", PHP_INI_SYSTEM, OnUpdateAiPositiveLong, NULL) STD_PHP_INI_ENTRY("king.gpu_p2p_enable", "1", PHP_INI_SYSTEM, OnUpdateBool, gpu_p2p_enable, kg_high_perf_compute_ai_config_t, king_high_perf_compute_ai_config) STD_PHP_INI_ENTRY("king.gpu_storage_enable_directstorage", "0", PHP_INI_SYSTEM, OnUpdateBool, storage_enable_directstorage, kg_high_perf_compute_ai_config_t, king_high_perf_compute_ai_config) @@ -92,7 +315,6 @@ PHP_INI_BEGIN() STD_PHP_INI_ENTRY("king.arc_video_acceleration_enable", "1", PHP_INI_SYSTEM, OnUpdateBool, arc_video_acceleration_enable, kg_high_perf_compute_ai_config_t, king_high_perf_compute_ai_config) PHP_INI_END() -extern int king_ini_module_number; void kg_config_high_perf_compute_and_ai_ini_register(void) { diff --git a/extension/src/config/http2/base_layer.c b/extension/src/config/http2/base_layer.c index 35c6c0246..cd96d1aba 100644 --- a/extension/src/config/http2/base_layer.c +++ b/extension/src/config/http2/base_layer.c @@ -11,6 +11,6 @@ * ========================================================================= */ -#include "include/config/http2/base_layer.h" +#include "config/http2/base_layer.h" kg_http2_config_t king_http2_config; diff --git a/extension/src/config/http2/config.c b/extension/src/config/http2/config.c index cdf6615bd..503f095dc 100644 --- a/extension/src/config/http2/config.c +++ b/extension/src/config/http2/config.c @@ -12,12 +12,12 @@ * ========================================================================= */ -#include "include/config/http2/config.h" -#include "include/config/http2/base_layer.h" -#include "include/king_globals.h" +#include "config/http2/config.h" +#include "config/http2/base_layer.h" +#include "php_king/globals.h" -#include "include/validation/config_param/validate_bool.h" -#include "include/validation/config_param/validate_positive_long.h" +#include "validation/config_param/validate_bool.h" +#include "validation/config_param/validate_positive_long.h" #include "php.h" #include "zend_exceptions.h" diff --git a/extension/src/config/http2/default.c b/extension/src/config/http2/default.c index bc10224a6..7c58bfcff 100644 --- a/extension/src/config/http2/default.c +++ b/extension/src/config/http2/default.c @@ -11,8 +11,8 @@ * ========================================================================= */ -#include "include/config/http2/default.h" -#include "include/config/http2/base_layer.h" +#include "config/http2/default.h" +#include "config/http2/base_layer.h" void kg_config_http2_defaults_load(void) { diff --git a/extension/src/config/http2/index.c b/extension/src/config/http2/index.c index 17dc78a32..e6148f8e5 100644 --- a/extension/src/config/http2/index.c +++ b/extension/src/config/http2/index.c @@ -10,9 +10,9 @@ * ========================================================================= */ -#include "include/config/http2/index.h" -#include "include/config/http2/default.h" -#include "include/config/http2/ini.h" +#include "config/http2/index.h" +#include "config/http2/default.h" +#include "config/http2/ini.h" void kg_config_http2_init(void) { diff --git a/extension/src/config/http2/ini.c b/extension/src/config/http2/ini.c index 5ad490a86..5f1c360a9 100644 --- a/extension/src/config/http2/ini.c +++ b/extension/src/config/http2/ini.c @@ -11,8 +11,9 @@ * ========================================================================= */ -#include "include/config/http2/ini.h" -#include "include/config/http2/base_layer.h" +#include "config/http2/ini.h" +#include "config/http2/base_layer.h" +#include "php_king/init.h" #include "php.h" #include "zend_exceptions.h" @@ -76,7 +77,6 @@ PHP_INI_BEGIN() OnUpdateHttp2MaxFrameSize, &king_http2_config.max_frame_size, NULL) PHP_INI_END() -extern int king_ini_module_number; void kg_config_http2_ini_register(void) { diff --git a/extension/src/config/iibin/base_layer.c b/extension/src/config/iibin/base_layer.c index edd584acb..95dd55b41 100644 --- a/extension/src/config/iibin/base_layer.c +++ b/extension/src/config/iibin/base_layer.c @@ -11,6 +11,6 @@ * ========================================================================= */ -#include "include/config/iibin/base_layer.h" +#include "config/iibin/base_layer.h" kg_iibin_config_t king_iibin_config; diff --git a/extension/src/config/iibin/config.c b/extension/src/config/iibin/config.c index 174b97a12..2aaabcf77 100644 --- a/extension/src/config/iibin/config.c +++ b/extension/src/config/iibin/config.c @@ -11,13 +11,13 @@ * ========================================================================= */ -#include "include/config/iibin/config.h" -#include "include/config/iibin/base_layer.h" -#include "include/king_globals.h" +#include "config/iibin/config.h" +#include "config/iibin/base_layer.h" +#include "php_king/globals.h" -#include "include/validation/config_param/validate_bool.h" -#include "include/validation/config_param/validate_positive_long.h" -#include "include/validation/config_param/validate_generic_string.h" +#include "validation/config_param/validate_bool.h" +#include "validation/config_param/validate_positive_long.h" +#include "validation/config_param/validate_generic_string.h" #include "php.h" #include "zend_exceptions.h" diff --git a/extension/src/config/iibin/default.c b/extension/src/config/iibin/default.c index bf42d7fb5..1f86ecf39 100644 --- a/extension/src/config/iibin/default.c +++ b/extension/src/config/iibin/default.c @@ -11,8 +11,8 @@ * ========================================================================= */ -#include "include/config/iibin/default.h" -#include "include/config/iibin/base_layer.h" +#include "config/iibin/default.h" +#include "config/iibin/base_layer.h" void kg_config_iibin_defaults_load(void) { diff --git a/extension/src/config/iibin/index.c b/extension/src/config/iibin/index.c index 77b7d25e3..b791025af 100644 --- a/extension/src/config/iibin/index.c +++ b/extension/src/config/iibin/index.c @@ -10,9 +10,9 @@ * ========================================================================= */ -#include "include/config/iibin/index.h" -#include "include/config/iibin/default.h" -#include "include/config/iibin/ini.h" +#include "config/iibin/index.h" +#include "config/iibin/default.h" +#include "config/iibin/ini.h" void kg_config_iibin_init(void) { diff --git a/extension/src/config/iibin/ini.c b/extension/src/config/iibin/ini.c index c6432190b..25ba36f0b 100644 --- a/extension/src/config/iibin/ini.c +++ b/extension/src/config/iibin/ini.c @@ -11,8 +11,9 @@ * ========================================================================= */ -#include "include/config/iibin/ini.h" -#include "include/config/iibin/base_layer.h" +#include "config/iibin/ini.h" +#include "config/iibin/base_layer.h" +#include "php_king/init.h" #include "php.h" #include "zend_exceptions.h" @@ -54,7 +55,6 @@ PHP_INI_BEGIN() STD_PHP_INI_ENTRY("king.io_shm_path", "/king_io_shm", PHP_INI_SYSTEM, OnUpdateString, shm_path, kg_iibin_config_t, king_iibin_config) PHP_INI_END() -extern int king_ini_module_number; void kg_config_iibin_ini_register(void) { diff --git a/extension/src/config/internal/class_entries.inc b/extension/src/config/internal/class_entries.inc new file mode 100644 index 000000000..dbf3215ab --- /dev/null +++ b/extension/src/config/internal/class_entries.inc @@ -0,0 +1,5 @@ +/* + * Config PHP class-entry storage. + */ + +zend_class_entry *king_ce_config = NULL; diff --git a/extension/src/config/internal/object.inc b/extension/src/config/internal/object.inc index 3ac7820fa..5b3588b0a 100644 --- a/extension/src/config/internal/object.inc +++ b/extension/src/config/internal/object.inc @@ -1,4 +1,4 @@ -#include "object/arginfo_and_override_guards.inc" +#include "object/override_guards.inc" #include "object/quic_snapshot.inc" #include "object/snapshot_read.inc" #include "object/snapshot_export.inc" diff --git a/extension/src/config/internal/object/arginfo_and_override_guards.inc b/extension/src/config/internal/object/override_guards.inc similarity index 89% rename from extension/src/config/internal/object/arginfo_and_override_guards.inc rename to extension/src/config/internal/object/override_guards.inc index 81ff27d54..61ad62a0a 100644 --- a/extension/src/config/internal/object/arginfo_and_override_guards.inc +++ b/extension/src/config/internal/object/override_guards.inc @@ -5,32 +5,13 @@ * * PURPOSE: * `King\\Config` object surface on top of the native config resource. This - * file declares arginfo, resolves object/resource handles, canonicalizes and - * tracks namespaced override keys, enforces freeze and policy gates, and - * implements the OO `new/get/set/toArray` behavior for runtime config + * file resolves object/resource handles, canonicalizes and tracks namespaced + * override keys, and enforces freeze and policy gates for runtime config * snapshots. * ========================================================================= */ -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_Config___construct, 0, 0, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_King_Config_new, 0, 0, King\\Config, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 0, "[]") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_Config_get, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Config_set, 0, 2, IS_VOID, 0) - ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, value, IS_MIXED, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Config_toArray, 0, 0, IS_ARRAY, 0) -ZEND_END_ARG_INFO() +#include "config/internal/object_arginfo.h" static void king_config_object_throw_unsupported_key( const char *function_name, @@ -40,7 +21,7 @@ static void king_config_object_throw_unsupported_key( zend_throw_exception_ex( spl_ce_InvalidArgumentException, 0, - "Unsupported runtime config override '%s'. Use namespaced keys under quic., tls., http2., tcp., autoscale., mcp., orchestrator., geometry., smartcontract., ssh., storage., cdn., dns., or otel.", + "Unsupported runtime config override '%s'. Use namespaced keys under quic., tls., http2., tcp., autoscale., mcp., orchestrator., geometry., smartcontract., ssh., storage., cdn., dns., otel., or inference.", ZSTR_VAL(key) ); } @@ -190,6 +171,12 @@ static zend_string *king_config_object_build_canonical_key( return king_config_object_build_prefixed_key("dns.", "dns_", normalized_key); case KING_CONFIG_OVERRIDE_OTEL: return king_config_object_build_prefixed_key("otel.", "otel_", normalized_key); + case KING_CONFIG_OVERRIDE_COMPUTE_AI: + return king_config_object_build_prefixed_key( + "inference.", + "inference_", + normalized_key + ); case KING_CONFIG_OVERRIDE_NONE: default: return NULL; @@ -295,4 +282,3 @@ static void king_config_object_sync_override_meta( cfg->override_count = override_count; cfg->userland_overrides_applied = override_count > 0 ? 1 : 0; } - diff --git a/extension/src/config/internal/object/public_methods.inc b/extension/src/config/internal/object/public_methods.inc index 9fed8c49b..d2cb517b9 100644 --- a/extension/src/config/internal/object/public_methods.inc +++ b/extension/src/config/internal/object/public_methods.inc @@ -117,11 +117,11 @@ PHP_METHOD(King_Config, get) RETURN_COPY(explicit_value); } - king_set_error("Requested config key is outside the active OO parity slice."); + king_set_error("Requested config key is not exposed by this King\\Config snapshot."); zend_throw_exception_ex( spl_ce_InvalidArgumentException, 0, - "Config::get() does not yet expose runtime key '%s' outside the active OO parity slice.", + "Config::get() exposes keys present in Config::toArray() plus explicit OO overrides; key '%s' is not part of that snapshot.", ZSTR_VAL(canonical_key) ); zend_string_release(canonical_key); @@ -199,11 +199,4 @@ PHP_METHOD(King_Config, toArray) king_set_error(""); } -const zend_function_entry king_config_class_methods[] = { - PHP_ME(King_Config, __construct, arginfo_class_King_Config___construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) - PHP_ME(King_Config, new, arginfo_class_King_Config_new, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - PHP_ME(King_Config, get, arginfo_class_King_Config_get, ZEND_ACC_PUBLIC) - PHP_ME(King_Config, set, arginfo_class_King_Config_set, ZEND_ACC_PUBLIC) - PHP_ME(King_Config, toArray, arginfo_class_King_Config_toArray, ZEND_ACC_PUBLIC) - PHP_FE_END -}; +#include "config/internal/class_method_entries.h" diff --git a/extension/src/config/internal/object/snapshot_export.inc b/extension/src/config/internal/object/snapshot_export.inc index dabc17066..524632c96 100644 --- a/extension/src/config/internal/object/snapshot_export.inc +++ b/extension/src/config/internal/object/snapshot_export.inc @@ -44,6 +44,182 @@ static void king_config_object_snapshot_to_array(zval *target, king_cfg_t *cfg) cfg->autoscale.provider != NULL ? cfg->autoscale.provider : "" ); add_assoc_long(target, "autoscale.max_nodes", cfg->autoscale.max_nodes); + add_assoc_bool(target, "inference.with_memory", cfg->compute_ai.inference_with_memory); + add_assoc_long(target, "inference.context_tokens", cfg->compute_ai.inference_context_tokens); + add_assoc_long(target, "inference.kv_page_tokens", cfg->compute_ai.inference_kv_page_tokens); + add_assoc_long(target, "inference.kv_element_bytes", cfg->compute_ai.inference_kv_element_bytes); + add_assoc_string( + target, + "inference.preferred_model_profile", + cfg->compute_ai.inference_preferred_model_profile != NULL + ? cfg->compute_ai.inference_preferred_model_profile + : "" + ); + add_assoc_string( + target, + "inference.models", + cfg->compute_ai.inference_models != NULL + ? cfg->compute_ai.inference_models + : "" + ); + add_assoc_string( + target, + "inference.cpu_model_name", + cfg->compute_ai.inference_cpu_model_name != NULL + ? cfg->compute_ai.inference_cpu_model_name + : "" + ); + add_assoc_string( + target, + "inference.cpu_model_artifact", + cfg->compute_ai.inference_cpu_model_artifact != NULL + ? cfg->compute_ai.inference_cpu_model_artifact + : "" + ); + add_assoc_string( + target, + "inference.gpu_model_name", + cfg->compute_ai.inference_gpu_model_name != NULL + ? cfg->compute_ai.inference_gpu_model_name + : "" + ); + add_assoc_string( + target, + "inference.gpu_model_artifact", + cfg->compute_ai.inference_gpu_model_artifact != NULL + ? cfg->compute_ai.inference_gpu_model_artifact + : "" + ); + add_assoc_long( + target, + "inference.gpu_max_gpu_layers", + cfg->compute_ai.inference_gpu_max_gpu_layers + ); + add_assoc_long( + target, + "inference.gpu_vram_reserve_mb", + cfg->compute_ai.inference_gpu_vram_reserve_mb + ); + add_assoc_long( + target, + "inference.gpu_min_free_vram_mb", + cfg->compute_ai.inference_gpu_min_free_vram_mb + ); + add_assoc_bool( + target, + "inference.gpu_allow_system_ram_offload", + cfg->compute_ai.inference_gpu_allow_system_ram_offload + ); + add_assoc_long( + target, + "inference.gpu_system_ram_offload_max_mb", + cfg->compute_ai.inference_gpu_system_ram_offload_max_mb + ); + add_assoc_long( + target, + "inference.gpu_system_ram_offload_min_free_mb", + cfg->compute_ai.inference_gpu_system_ram_offload_min_free_mb + ); + add_assoc_string( + target, + "inference.gpu_thermal_sensor_path", + cfg->compute_ai.inference_gpu_thermal_sensor_path != NULL + ? cfg->compute_ai.inference_gpu_thermal_sensor_path + : "" + ); + add_assoc_string( + target, + "inference.gpu_thermal_sensor_command", + cfg->compute_ai.inference_gpu_thermal_sensor_command != NULL + ? cfg->compute_ai.inference_gpu_thermal_sensor_command + : "" + ); + add_assoc_double( + target, + "inference.gpu_thermal_max_temperature_c", + cfg->compute_ai.inference_gpu_thermal_max_temperature_c + ); + add_assoc_long( + target, + "inference.gpu_thermal_check_interval_sec", + cfg->compute_ai.inference_gpu_thermal_check_interval_sec + ); + add_assoc_bool( + target, + "inference.gpu_allow_unmonitored", + cfg->compute_ai.inference_gpu_allow_unmonitored + ); + add_assoc_string( + target, + "inference.gpu_power_sensor_command", + cfg->compute_ai.inference_gpu_power_sensor_command != NULL + ? cfg->compute_ai.inference_gpu_power_sensor_command + : "" + ); + add_assoc_double( + target, + "inference.gpu_power_max_watts", + cfg->compute_ai.inference_gpu_power_max_watts + ); + add_assoc_long( + target, + "inference.gpu_power_check_interval_sec", + cfg->compute_ai.inference_gpu_power_check_interval_sec + ); + add_assoc_bool( + target, + "inference.gpu_batch_prefill_experimental_enable", + cfg->compute_ai.inference_gpu_batch_prefill_experimental_enable + ); + add_assoc_bool( + target, + "inference.cuda_numeric_compare_enable", + cfg->compute_ai.inference_cuda_numeric_compare_enable + ); + add_assoc_long( + target, + "inference.cuda_numeric_compare_max_values", + cfg->compute_ai.inference_cuda_numeric_compare_max_values + ); + add_assoc_bool(target, "inference.llm_cache_enable", cfg->compute_ai.inference_llm_cache_enable); + add_assoc_string( + target, + "inference.llm_cache_path", + cfg->compute_ai.inference_llm_cache_path != NULL + ? cfg->compute_ai.inference_llm_cache_path + : "" + ); + add_assoc_long( + target, + "inference.llm_cache_min_free_mb", + cfg->compute_ai.inference_llm_cache_min_free_mb + ); + add_assoc_bool( + target, + "inference.llm_cache_fail_closed", + cfg->compute_ai.inference_llm_cache_fail_closed + ); + add_assoc_string( + target, + "inference.llm_cache_disk_alert_webhook", + cfg->compute_ai.inference_llm_cache_disk_alert_webhook != NULL + ? cfg->compute_ai.inference_llm_cache_disk_alert_webhook + : "" + ); + add_assoc_string( + target, + "inference.llm_cache_disk_alert_mcp_service", + cfg->compute_ai.inference_llm_cache_disk_alert_mcp_service != NULL + ? cfg->compute_ai.inference_llm_cache_disk_alert_mcp_service + : "" + ); + add_assoc_string( + target, + "inference.llm_cache_disk_alert_mcp_method", + cfg->compute_ai.inference_llm_cache_disk_alert_mcp_method != NULL + ? cfg->compute_ai.inference_llm_cache_disk_alert_mcp_method + : "" + ); add_assoc_bool(target, "mcp.enable_request_caching", cfg->mcp.mcp_enable_request_caching); add_assoc_long( target, diff --git a/extension/src/config/internal/object/snapshot_read.inc b/extension/src/config/internal/object/snapshot_read.inc index 040d05b55..72841aab1 100644 --- a/extension/src/config/internal/object/snapshot_read.inc +++ b/extension/src/config/internal/object/snapshot_read.inc @@ -163,6 +163,445 @@ static zend_result king_config_object_snapshot_read( return SUCCESS; } + if ( + zend_string_equals_cstr( + canonical_key, + "inference.with_memory", + sizeof("inference.with_memory") - 1 + ) + ) { + ZVAL_BOOL(return_value, cfg->compute_ai.inference_with_memory); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.context_tokens", + sizeof("inference.context_tokens") - 1 + ) + ) { + ZVAL_LONG(return_value, cfg->compute_ai.inference_context_tokens); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.kv_page_tokens", + sizeof("inference.kv_page_tokens") - 1 + ) + ) { + ZVAL_LONG(return_value, cfg->compute_ai.inference_kv_page_tokens); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.kv_element_bytes", + sizeof("inference.kv_element_bytes") - 1 + ) + ) { + ZVAL_LONG(return_value, cfg->compute_ai.inference_kv_element_bytes); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.preferred_model_profile", + sizeof("inference.preferred_model_profile") - 1 + ) + ) { + ZVAL_STRING( + return_value, + cfg->compute_ai.inference_preferred_model_profile != NULL + ? cfg->compute_ai.inference_preferred_model_profile + : "" + ); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.cpu_model_name", + sizeof("inference.cpu_model_name") - 1 + ) + ) { + ZVAL_STRING( + return_value, + cfg->compute_ai.inference_cpu_model_name != NULL + ? cfg->compute_ai.inference_cpu_model_name + : "" + ); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.models", + sizeof("inference.models") - 1 + ) + ) { + ZVAL_STRING( + return_value, + cfg->compute_ai.inference_models != NULL + ? cfg->compute_ai.inference_models + : "" + ); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.cpu_model_artifact", + sizeof("inference.cpu_model_artifact") - 1 + ) + ) { + ZVAL_STRING( + return_value, + cfg->compute_ai.inference_cpu_model_artifact != NULL + ? cfg->compute_ai.inference_cpu_model_artifact + : "" + ); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.gpu_model_name", + sizeof("inference.gpu_model_name") - 1 + ) + ) { + ZVAL_STRING( + return_value, + cfg->compute_ai.inference_gpu_model_name != NULL + ? cfg->compute_ai.inference_gpu_model_name + : "" + ); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.gpu_model_artifact", + sizeof("inference.gpu_model_artifact") - 1 + ) + ) { + ZVAL_STRING( + return_value, + cfg->compute_ai.inference_gpu_model_artifact != NULL + ? cfg->compute_ai.inference_gpu_model_artifact + : "" + ); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.gpu_max_gpu_layers", + sizeof("inference.gpu_max_gpu_layers") - 1 + ) + ) { + ZVAL_LONG(return_value, cfg->compute_ai.inference_gpu_max_gpu_layers); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.gpu_vram_reserve_mb", + sizeof("inference.gpu_vram_reserve_mb") - 1 + ) + ) { + ZVAL_LONG(return_value, cfg->compute_ai.inference_gpu_vram_reserve_mb); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.gpu_min_free_vram_mb", + sizeof("inference.gpu_min_free_vram_mb") - 1 + ) + ) { + ZVAL_LONG(return_value, cfg->compute_ai.inference_gpu_min_free_vram_mb); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.gpu_allow_system_ram_offload", + sizeof("inference.gpu_allow_system_ram_offload") - 1 + ) + ) { + ZVAL_BOOL(return_value, cfg->compute_ai.inference_gpu_allow_system_ram_offload); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.gpu_system_ram_offload_max_mb", + sizeof("inference.gpu_system_ram_offload_max_mb") - 1 + ) + ) { + ZVAL_LONG(return_value, cfg->compute_ai.inference_gpu_system_ram_offload_max_mb); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.gpu_system_ram_offload_min_free_mb", + sizeof("inference.gpu_system_ram_offload_min_free_mb") - 1 + ) + ) { + ZVAL_LONG(return_value, cfg->compute_ai.inference_gpu_system_ram_offload_min_free_mb); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.gpu_thermal_sensor_path", + sizeof("inference.gpu_thermal_sensor_path") - 1 + ) + ) { + ZVAL_STRING( + return_value, + cfg->compute_ai.inference_gpu_thermal_sensor_path != NULL + ? cfg->compute_ai.inference_gpu_thermal_sensor_path + : "" + ); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.gpu_thermal_sensor_command", + sizeof("inference.gpu_thermal_sensor_command") - 1 + ) + ) { + ZVAL_STRING( + return_value, + cfg->compute_ai.inference_gpu_thermal_sensor_command != NULL + ? cfg->compute_ai.inference_gpu_thermal_sensor_command + : "" + ); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.gpu_thermal_max_temperature_c", + sizeof("inference.gpu_thermal_max_temperature_c") - 1 + ) + ) { + ZVAL_DOUBLE(return_value, cfg->compute_ai.inference_gpu_thermal_max_temperature_c); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.gpu_thermal_check_interval_sec", + sizeof("inference.gpu_thermal_check_interval_sec") - 1 + ) + ) { + ZVAL_LONG(return_value, cfg->compute_ai.inference_gpu_thermal_check_interval_sec); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.gpu_allow_unmonitored", + sizeof("inference.gpu_allow_unmonitored") - 1 + ) + ) { + ZVAL_BOOL(return_value, cfg->compute_ai.inference_gpu_allow_unmonitored); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.gpu_power_sensor_command", + sizeof("inference.gpu_power_sensor_command") - 1 + ) + ) { + ZVAL_STRING( + return_value, + cfg->compute_ai.inference_gpu_power_sensor_command != NULL + ? cfg->compute_ai.inference_gpu_power_sensor_command + : "" + ); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.gpu_power_max_watts", + sizeof("inference.gpu_power_max_watts") - 1 + ) + ) { + ZVAL_DOUBLE(return_value, cfg->compute_ai.inference_gpu_power_max_watts); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.gpu_power_check_interval_sec", + sizeof("inference.gpu_power_check_interval_sec") - 1 + ) + ) { + ZVAL_LONG(return_value, cfg->compute_ai.inference_gpu_power_check_interval_sec); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.gpu_batch_prefill_experimental_enable", + sizeof("inference.gpu_batch_prefill_experimental_enable") - 1 + ) + ) { + ZVAL_BOOL(return_value, cfg->compute_ai.inference_gpu_batch_prefill_experimental_enable); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.cuda_numeric_compare_enable", + sizeof("inference.cuda_numeric_compare_enable") - 1 + ) + ) { + ZVAL_BOOL(return_value, cfg->compute_ai.inference_cuda_numeric_compare_enable); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.cuda_numeric_compare_max_values", + sizeof("inference.cuda_numeric_compare_max_values") - 1 + ) + ) { + ZVAL_LONG(return_value, cfg->compute_ai.inference_cuda_numeric_compare_max_values); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.llm_cache_enable", + sizeof("inference.llm_cache_enable") - 1 + ) + ) { + ZVAL_BOOL(return_value, cfg->compute_ai.inference_llm_cache_enable); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.llm_cache_path", + sizeof("inference.llm_cache_path") - 1 + ) + ) { + ZVAL_STRING( + return_value, + cfg->compute_ai.inference_llm_cache_path != NULL + ? cfg->compute_ai.inference_llm_cache_path + : "" + ); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.llm_cache_min_free_mb", + sizeof("inference.llm_cache_min_free_mb") - 1 + ) + ) { + ZVAL_LONG(return_value, cfg->compute_ai.inference_llm_cache_min_free_mb); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.llm_cache_fail_closed", + sizeof("inference.llm_cache_fail_closed") - 1 + ) + ) { + ZVAL_BOOL(return_value, cfg->compute_ai.inference_llm_cache_fail_closed); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.llm_cache_disk_alert_webhook", + sizeof("inference.llm_cache_disk_alert_webhook") - 1 + ) + ) { + ZVAL_STRING( + return_value, + cfg->compute_ai.inference_llm_cache_disk_alert_webhook != NULL + ? cfg->compute_ai.inference_llm_cache_disk_alert_webhook + : "" + ); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.llm_cache_disk_alert_mcp_service", + sizeof("inference.llm_cache_disk_alert_mcp_service") - 1 + ) + ) { + ZVAL_STRING( + return_value, + cfg->compute_ai.inference_llm_cache_disk_alert_mcp_service != NULL + ? cfg->compute_ai.inference_llm_cache_disk_alert_mcp_service + : "" + ); + return SUCCESS; + } + + if ( + zend_string_equals_cstr( + canonical_key, + "inference.llm_cache_disk_alert_mcp_method", + sizeof("inference.llm_cache_disk_alert_mcp_method") - 1 + ) + ) { + ZVAL_STRING( + return_value, + cfg->compute_ai.inference_llm_cache_disk_alert_mcp_method != NULL + ? cfg->compute_ai.inference_llm_cache_disk_alert_mcp_method + : "" + ); + return SUCCESS; + } + if ( zend_string_equals_cstr( canonical_key, diff --git a/extension/src/config/internal/object_handlers.inc b/extension/src/config/internal/object_handlers.inc new file mode 100644 index 000000000..3336f73f7 --- /dev/null +++ b/extension/src/config/internal/object_handlers.inc @@ -0,0 +1,47 @@ +/* + * King config PHP object handlers. + */ + +static zend_object_handlers king_config_object_handlers; + +static void king_config_object_free(zend_object *object) +{ + king_config_object *intern = php_king_config_obj_from_zend(object); + + if (!Z_ISUNDEF(intern->resource)) { + zval_ptr_dtor(&intern->resource); + ZVAL_UNDEF(&intern->resource); + } + if (!Z_ISUNDEF(intern->overrides)) { + zval_ptr_dtor(&intern->overrides); + ZVAL_UNDEF(&intern->overrides); + } + + zend_object_std_dtor(&intern->std); +} + +static zend_object *king_config_object_create(zend_class_entry *ce) +{ + king_config_object *intern = zend_object_alloc(sizeof(*intern), ce); + + ZVAL_UNDEF(&intern->resource); + ZVAL_UNDEF(&intern->overrides); + zend_object_std_init(&intern->std, ce); + object_properties_init(&intern->std, ce); + intern->std.handlers = &king_config_object_handlers; + + return &intern->std; +} + +static void king_config_object_handlers_init(void) +{ + memcpy( + &king_config_object_handlers, + &std_object_handlers, + sizeof(zend_object_handlers) + ); + king_config_object_handlers.offset = XtOffsetOf(king_config_object, std); + king_config_object_handlers.free_obj = king_config_object_free; + king_config_object_handlers.clone_obj = NULL; + king_ce_config->create_object = king_config_object_create; +} diff --git a/extension/src/config/internal/overrides/apply.inc b/extension/src/config/internal/overrides/apply.inc index 98acb7379..046a44dc5 100644 --- a/extension/src/config/internal/overrides/apply.inc +++ b/extension/src/config/internal/overrides/apply.inc @@ -36,6 +36,7 @@ static int king_config_apply_override_state_to_cfg( KING_CONFIG_APPLY_OVERRIDE_BUCKET(cdn, cdn, kg_config_native_cdn_apply_userland_config_to); KING_CONFIG_APPLY_OVERRIDE_BUCKET(dns, dns, kg_config_smart_dns_apply_userland_config_to); KING_CONFIG_APPLY_OVERRIDE_BUCKET(otel, observability, kg_config_open_telemetry_apply_userland_config_to); + KING_CONFIG_APPLY_OVERRIDE_BUCKET(compute_ai, compute_ai, kg_config_high_perf_compute_and_ai_apply_userland_config_to); return SUCCESS; } diff --git a/extension/src/config/internal/overrides/routing.inc b/extension/src/config/internal/overrides/routing.inc index 5f50e5a57..23ce100c8 100644 --- a/extension/src/config/internal/overrides/routing.inc +++ b/extension/src/config/internal/overrides/routing.inc @@ -156,6 +156,15 @@ static king_config_override_module_t king_config_route_override_key( return KING_CONFIG_OVERRIDE_OTEL; } + if (king_zend_string_starts_with_cstr(key, "inference.")) { + *normalized_key = king_config_normalize_suffix_key( + key, + sizeof("inference.") - 1, + "inference_" + ); + return KING_CONFIG_OVERRIDE_COMPUTE_AI; + } + if (king_zend_string_starts_with_cstr(key, "storage_")) { *normalized_key = king_config_normalize_suffix_key(key, sizeof("storage_") - 1, ""); return KING_CONFIG_OVERRIDE_STORAGE; @@ -176,6 +185,11 @@ static king_config_override_module_t king_config_route_override_key( return KING_CONFIG_OVERRIDE_OTEL; } + if (king_zend_string_starts_with_cstr(key, "inference_")) { + *normalized_key = zend_string_copy(key); + return KING_CONFIG_OVERRIDE_COMPUTE_AI; + } + if (king_config_is_known_quic_flat_key(key)) { *normalized_key = zend_string_copy(key); return KING_CONFIG_OVERRIDE_QUIC; diff --git a/extension/src/config/internal/overrides/state.inc b/extension/src/config/internal/overrides/state.inc index 49c3e126a..716d946da 100644 --- a/extension/src/config/internal/overrides/state.inc +++ b/extension/src/config/internal/overrides/state.inc @@ -30,6 +30,7 @@ typedef struct _king_config_override_state_t { king_config_override_bucket_t cdn; king_config_override_bucket_t dns; king_config_override_bucket_t otel; + king_config_override_bucket_t compute_ai; } king_config_override_state_t; static king_config_override_bucket_t *king_config_override_bucket_for_module( @@ -63,6 +64,8 @@ static king_config_override_bucket_t *king_config_override_bucket_for_module( return &state->dns; case KING_CONFIG_OVERRIDE_OTEL: return &state->otel; + case KING_CONFIG_OVERRIDE_COMPUTE_AI: + return &state->compute_ai; case KING_CONFIG_OVERRIDE_NONE: return NULL; } @@ -128,4 +131,5 @@ static void king_config_cleanup_override_state(king_config_override_state_t *sta king_config_destroy_override_bucket(&state->cdn); king_config_destroy_override_bucket(&state->dns); king_config_destroy_override_bucket(&state->otel); + king_config_destroy_override_bucket(&state->compute_ai); } diff --git a/extension/src/config/internal/owned_strings.inc b/extension/src/config/internal/owned_strings.inc index 9d76b5801..9ea47372f 100644 --- a/extension/src/config/internal/owned_strings.inc +++ b/extension/src/config/internal/owned_strings.inc @@ -64,6 +64,7 @@ static void king_config_free_dns_strings(kg_smart_dns_config_t *dns) pefree(dns->mode, 1); pefree(dns->mothernode_uri, 1); pefree(dns->live_probe_allowed_hosts, 1); + pefree(dns->state_path, 1); } static void king_config_free_otel_strings(kg_open_telemetry_config_t *otel) @@ -137,6 +138,25 @@ static void king_config_free_mcp_orchestrator_strings(kg_mcp_orchestrator_config pefree(mcp->orchestrator_state_path, 1); } +static void king_config_free_compute_ai_strings(kg_high_perf_compute_ai_config_t *compute_ai) +{ + pefree(compute_ai->inference_preferred_model_profile, 1); + pefree(compute_ai->inference_models, 1); + pefree(compute_ai->inference_cpu_model_name, 1); + pefree(compute_ai->inference_cpu_model_artifact, 1); + pefree(compute_ai->inference_gpu_model_name, 1); + pefree(compute_ai->inference_gpu_model_artifact, 1); + pefree(compute_ai->inference_gpu_thermal_sensor_path, 1); + pefree(compute_ai->inference_gpu_thermal_sensor_command, 1); + pefree(compute_ai->inference_gpu_power_sensor_command, 1); + pefree(compute_ai->inference_llm_cache_path, 1); + pefree(compute_ai->inference_llm_cache_disk_alert_webhook, 1); + pefree(compute_ai->inference_llm_cache_disk_alert_mcp_service, 1); + pefree(compute_ai->inference_llm_cache_disk_alert_mcp_method, 1); + pefree(compute_ai->gpu_default_backend, 1); + pefree(compute_ai->worker_gpu_affinity_map, 1); +} + static void king_config_clone_owned_strings(king_cfg_t *cfg) { cfg->autoscale.provider = king_config_clone_persistent_string(cfg->autoscale.provider); @@ -179,6 +199,7 @@ static void king_config_clone_owned_strings(king_cfg_t *cfg) cfg->dns.mode = king_config_clone_persistent_string(cfg->dns.mode); cfg->dns.mothernode_uri = king_config_clone_persistent_string(cfg->dns.mothernode_uri); cfg->dns.live_probe_allowed_hosts = king_config_clone_persistent_string(cfg->dns.live_probe_allowed_hosts); + cfg->dns.state_path = king_config_clone_persistent_string(cfg->dns.state_path); cfg->owns_dns_strings = 1; cfg->observability.service_name = king_config_clone_persistent_string(cfg->observability.service_name); @@ -238,6 +259,23 @@ static void king_config_clone_owned_strings(king_cfg_t *cfg) cfg->mcp.orchestrator_state_path = king_config_clone_persistent_string(cfg->mcp.orchestrator_state_path); cfg->owns_mcp_orchestrator_strings = 1; + cfg->compute_ai.inference_preferred_model_profile = king_config_clone_persistent_string(cfg->compute_ai.inference_preferred_model_profile); + cfg->compute_ai.inference_models = king_config_clone_persistent_string(cfg->compute_ai.inference_models); + cfg->compute_ai.inference_cpu_model_name = king_config_clone_persistent_string(cfg->compute_ai.inference_cpu_model_name); + cfg->compute_ai.inference_cpu_model_artifact = king_config_clone_persistent_string(cfg->compute_ai.inference_cpu_model_artifact); + cfg->compute_ai.inference_gpu_model_name = king_config_clone_persistent_string(cfg->compute_ai.inference_gpu_model_name); + cfg->compute_ai.inference_gpu_model_artifact = king_config_clone_persistent_string(cfg->compute_ai.inference_gpu_model_artifact); + cfg->compute_ai.inference_gpu_thermal_sensor_path = king_config_clone_persistent_string(cfg->compute_ai.inference_gpu_thermal_sensor_path); + cfg->compute_ai.inference_gpu_thermal_sensor_command = king_config_clone_persistent_string(cfg->compute_ai.inference_gpu_thermal_sensor_command); + cfg->compute_ai.inference_gpu_power_sensor_command = king_config_clone_persistent_string(cfg->compute_ai.inference_gpu_power_sensor_command); + cfg->compute_ai.inference_llm_cache_path = king_config_clone_persistent_string(cfg->compute_ai.inference_llm_cache_path); + cfg->compute_ai.inference_llm_cache_disk_alert_webhook = king_config_clone_persistent_string(cfg->compute_ai.inference_llm_cache_disk_alert_webhook); + cfg->compute_ai.inference_llm_cache_disk_alert_mcp_service = king_config_clone_persistent_string(cfg->compute_ai.inference_llm_cache_disk_alert_mcp_service); + cfg->compute_ai.inference_llm_cache_disk_alert_mcp_method = king_config_clone_persistent_string(cfg->compute_ai.inference_llm_cache_disk_alert_mcp_method); + cfg->compute_ai.gpu_default_backend = king_config_clone_persistent_string(cfg->compute_ai.gpu_default_backend); + cfg->compute_ai.worker_gpu_affinity_map = king_config_clone_persistent_string(cfg->compute_ai.worker_gpu_affinity_map); + cfg->owns_compute_ai_strings = 1; + cfg->quic.cc_algorithm = king_config_clone_persistent_string(cfg->quic.cc_algorithm); cfg->owns_quic_cc_algorithm = 1; } @@ -303,6 +341,11 @@ static void king_config_free_owned_overrides(king_cfg_t *cfg) cfg->owns_mcp_orchestrator_strings = 0; } + if (cfg->owns_compute_ai_strings) { + king_config_free_compute_ai_strings(&cfg->compute_ai); + cfg->owns_compute_ai_strings = 0; + } + if (cfg->owns_quic_cc_algorithm && cfg->quic.cc_algorithm != NULL) { pefree(cfg->quic.cc_algorithm, 1); cfg->quic.cc_algorithm = NULL; diff --git a/extension/src/config/internal/php_binding.inc b/extension/src/config/internal/php_binding.inc new file mode 100644 index 000000000..eadcb814f --- /dev/null +++ b/extension/src/config/internal/php_binding.inc @@ -0,0 +1,6 @@ +/* + * Config PHP binding aggregation. The config runtime translation unit includes + * this internal module boundary instead of object-handler leaves directly. + */ + +#include "object_handlers.inc" diff --git a/extension/src/config/internal/registration.inc b/extension/src/config/internal/registration.inc new file mode 100644 index 000000000..07a1827e4 --- /dev/null +++ b/extension/src/config/internal/registration.inc @@ -0,0 +1,22 @@ +#include "resource_destructors.inc" + +void king_config_register_resource_types(int module_number) +{ + le_king_cfg = zend_register_list_destructors_ex( + king_config_resource_dtor, NULL, "King\\Config", module_number); +} + +void king_config_register_classes(void) +{ + king_ce_config = king_register_class_with_flags( + "King\\Config", + NULL, + king_config_class_methods, + ZEND_ACC_NO_DYNAMIC_PROPERTIES + ); +} + +void king_config_init_object_handlers(void) +{ + king_config_object_handlers_init(); +} diff --git a/extension/src/config/internal/resource_destructors.inc b/extension/src/config/internal/resource_destructors.inc new file mode 100644 index 000000000..140c61275 --- /dev/null +++ b/extension/src/config/internal/resource_destructors.inc @@ -0,0 +1,12 @@ +/* + * Config-owned Zend resource destructor. + */ + +#include "config/config.h" + +static void king_config_resource_dtor(zend_resource *rsrc) +{ + if (rsrc->ptr) { + king_config_free((king_cfg_t *) rsrc->ptr); + } +} diff --git a/extension/src/config/internal/resource_helpers.inc b/extension/src/config/internal/resource_helpers.inc new file mode 100644 index 000000000..9b8d2fe5f --- /dev/null +++ b/extension/src/config/internal/resource_helpers.inc @@ -0,0 +1,25 @@ +/* + * Config resource and object resolver shared by runtime leaves that accept + * either a King\Config object or a native King\Config resource. + */ + +king_cfg_t *king_fetch_config(zval *zcfg) +{ + if (Z_TYPE_P(zcfg) == IS_OBJECT + && king_ce_config != NULL + && instanceof_function(Z_OBJCE_P(zcfg), king_ce_config)) { + king_config_object *intern = php_king_config_obj_from_zend(Z_OBJ_P(zcfg)); + + if (Z_ISUNDEF(intern->resource) || Z_TYPE(intern->resource) != IS_RESOURCE) { + return NULL; + } + + zcfg = &intern->resource; + } + + if (Z_TYPE_P(zcfg) != IS_RESOURCE) { + return NULL; + } + + return (king_cfg_t *) zend_fetch_resource(Z_RES_P(zcfg), "King\\Config", le_king_cfg); +} diff --git a/extension/src/config/internal/resource_ids.inc b/extension/src/config/internal/resource_ids.inc new file mode 100644 index 000000000..1d2f38cd5 --- /dev/null +++ b/extension/src/config/internal/resource_ids.inc @@ -0,0 +1,5 @@ +/* + * Config Zend resource-id storage. + */ + +int le_king_cfg = -1; diff --git a/extension/src/config/internal/snapshot.inc b/extension/src/config/internal/snapshot.inc index 05a07c213..876485aa1 100644 --- a/extension/src/config/internal/snapshot.inc +++ b/extension/src/config/internal/snapshot.inc @@ -29,6 +29,7 @@ static void king_config_snapshot_globals(king_cfg_t *cfg) cfg->owns_tcp_strings = 0; cfg->owns_quic_cc_algorithm = 0; cfg->owns_mcp_orchestrator_strings = 0; + cfg->owns_compute_ai_strings = 0; cfg->app_protocols = king_app_protocols_config; cfg->bare_metal = king_bare_metal_config; diff --git a/extension/src/config/internal/state.inc b/extension/src/config/internal/state.inc new file mode 100644 index 000000000..91064225f --- /dev/null +++ b/extension/src/config/internal/state.inc @@ -0,0 +1,6 @@ +/* + * Config module state storage aggregation. + */ + +#include "class_entries.inc" +#include "resource_ids.inc" diff --git a/extension/src/config/mcp_and_orchestrator/base_layer.c b/extension/src/config/mcp_and_orchestrator/base_layer.c index e0a59806b..1fe3d612d 100644 --- a/extension/src/config/mcp_and_orchestrator/base_layer.c +++ b/extension/src/config/mcp_and_orchestrator/base_layer.c @@ -11,6 +11,6 @@ * ========================================================================= */ -#include "include/config/mcp_and_orchestrator/base_layer.h" +#include "config/mcp_and_orchestrator/base_layer.h" kg_mcp_orchestrator_config_t king_mcp_orchestrator_config; diff --git a/extension/src/config/mcp_and_orchestrator/config.c b/extension/src/config/mcp_and_orchestrator/config.c index 243e02081..0cee2806a 100644 --- a/extension/src/config/mcp_and_orchestrator/config.c +++ b/extension/src/config/mcp_and_orchestrator/config.c @@ -12,14 +12,14 @@ * ========================================================================= */ -#include "include/config/mcp_and_orchestrator/config.h" -#include "include/config/mcp_and_orchestrator/base_layer.h" -#include "include/king_globals.h" +#include "config/mcp_and_orchestrator/config.h" +#include "config/mcp_and_orchestrator/base_layer.h" +#include "php_king/globals.h" -#include "include/validation/config_param/validate_bool.h" -#include "include/validation/config_param/validate_generic_string.h" -#include "include/validation/config_param/validate_positive_long.h" -#include "include/validation/config_param/validate_string_from_allowlist.h" +#include "validation/config_param/validate_bool.h" +#include "validation/config_param/validate_generic_string.h" +#include "validation/config_param/validate_positive_long.h" +#include "validation/config_param/validate_string_from_allowlist.h" #include "php.h" #include "zend_exceptions.h" diff --git a/extension/src/config/mcp_and_orchestrator/default.c b/extension/src/config/mcp_and_orchestrator/default.c index 493ac6b6c..b27df2ddf 100644 --- a/extension/src/config/mcp_and_orchestrator/default.c +++ b/extension/src/config/mcp_and_orchestrator/default.c @@ -11,8 +11,8 @@ * ========================================================================= */ -#include "include/config/mcp_and_orchestrator/default.h" -#include "include/config/mcp_and_orchestrator/base_layer.h" +#include "config/mcp_and_orchestrator/default.h" +#include "config/mcp_and_orchestrator/base_layer.h" void kg_config_mcp_and_orchestrator_defaults_load(void) { diff --git a/extension/src/config/mcp_and_orchestrator/index.c b/extension/src/config/mcp_and_orchestrator/index.c index a2394c4ed..9b03956c5 100644 --- a/extension/src/config/mcp_and_orchestrator/index.c +++ b/extension/src/config/mcp_and_orchestrator/index.c @@ -10,9 +10,9 @@ * ========================================================================= */ -#include "include/config/mcp_and_orchestrator/index.h" -#include "include/config/mcp_and_orchestrator/default.h" -#include "include/config/mcp_and_orchestrator/ini.h" +#include "config/mcp_and_orchestrator/index.h" +#include "config/mcp_and_orchestrator/default.h" +#include "config/mcp_and_orchestrator/ini.h" void kg_config_mcp_and_orchestrator_init(void) { diff --git a/extension/src/config/mcp_and_orchestrator/ini.c b/extension/src/config/mcp_and_orchestrator/ini.c index e630232a4..ba77d13b6 100644 --- a/extension/src/config/mcp_and_orchestrator/ini.c +++ b/extension/src/config/mcp_and_orchestrator/ini.c @@ -11,8 +11,9 @@ * ========================================================================= */ -#include "include/config/mcp_and_orchestrator/ini.h" -#include "include/config/mcp_and_orchestrator/base_layer.h" +#include "config/mcp_and_orchestrator/ini.h" +#include "config/mcp_and_orchestrator/base_layer.h" +#include "php_king/init.h" #include "php.h" #include "zend_exceptions.h" @@ -107,7 +108,6 @@ PHP_INI_BEGIN() STD_PHP_INI_ENTRY("king.orchestrator_state_path", "", PHP_INI_SYSTEM, OnUpdateString, orchestrator_state_path, kg_mcp_orchestrator_config_t, king_mcp_orchestrator_config) PHP_INI_END() -extern int king_ini_module_number; void kg_config_mcp_and_orchestrator_ini_register(void) { diff --git a/extension/src/config/native_cdn/base_layer.c b/extension/src/config/native_cdn/base_layer.c index 7371ca276..ca2023e6e 100644 --- a/extension/src/config/native_cdn/base_layer.c +++ b/extension/src/config/native_cdn/base_layer.c @@ -11,6 +11,6 @@ * ========================================================================= */ -#include "include/config/native_cdn/base_layer.h" +#include "config/native_cdn/base_layer.h" kg_native_cdn_config_t king_native_cdn_config; diff --git a/extension/src/config/native_cdn/config.c b/extension/src/config/native_cdn/config.c index aed245454..b7ca78da2 100644 --- a/extension/src/config/native_cdn/config.c +++ b/extension/src/config/native_cdn/config.c @@ -11,15 +11,15 @@ * ========================================================================= */ -#include "include/config/native_cdn/config.h" -#include "include/config/native_cdn/base_layer.h" -#include "include/king_globals.h" +#include "config/native_cdn/config.h" +#include "config/native_cdn/base_layer.h" +#include "php_king/globals.h" -#include "include/validation/config_param/validate_bool.h" -#include "include/validation/config_param/validate_positive_long.h" -#include "include/validation/config_param/validate_string_from_allowlist.h" -#include "include/validation/config_param/validate_generic_string.h" -#include "include/validation/config_param/validate_comma_separated_string_from_allowlist.h" +#include "validation/config_param/validate_bool.h" +#include "validation/config_param/validate_positive_long.h" +#include "validation/config_param/validate_string_from_allowlist.h" +#include "validation/config_param/validate_generic_string.h" +#include "validation/config_param/validate_comma_separated_string_from_allowlist.h" #include "php.h" #include "zend_exceptions.h" diff --git a/extension/src/config/native_cdn/default.c b/extension/src/config/native_cdn/default.c index e382d5117..ed29d3283 100644 --- a/extension/src/config/native_cdn/default.c +++ b/extension/src/config/native_cdn/default.c @@ -11,8 +11,8 @@ * ========================================================================= */ -#include "include/config/native_cdn/default.h" -#include "include/config/native_cdn/base_layer.h" +#include "config/native_cdn/default.h" +#include "config/native_cdn/base_layer.h" void kg_config_native_cdn_defaults_load(void) { diff --git a/extension/src/config/native_cdn/index.c b/extension/src/config/native_cdn/index.c index 994208dcb..249d6ad71 100644 --- a/extension/src/config/native_cdn/index.c +++ b/extension/src/config/native_cdn/index.c @@ -10,9 +10,9 @@ * ========================================================================= */ -#include "include/config/native_cdn/index.h" -#include "include/config/native_cdn/default.h" -#include "include/config/native_cdn/ini.h" +#include "config/native_cdn/index.h" +#include "config/native_cdn/default.h" +#include "config/native_cdn/ini.h" void kg_config_native_cdn_init(void) { diff --git a/extension/src/config/native_cdn/ini.c b/extension/src/config/native_cdn/ini.c index 0c06a20f1..4cd69b9ae 100644 --- a/extension/src/config/native_cdn/ini.c +++ b/extension/src/config/native_cdn/ini.c @@ -11,8 +11,9 @@ * ========================================================================= */ -#include "include/config/native_cdn/ini.h" -#include "include/config/native_cdn/base_layer.h" +#include "config/native_cdn/ini.h" +#include "config/native_cdn/base_layer.h" +#include "php_king/init.h" #include "php.h" #include "zend_exceptions.h" @@ -93,7 +94,6 @@ PHP_INI_BEGIN() STD_PHP_INI_ENTRY("king.cdn_allowed_http_methods", "GET,HEAD", PHP_INI_SYSTEM, OnUpdateString, allowed_http_methods, kg_native_cdn_config_t, king_native_cdn_config) PHP_INI_END() -extern int king_ini_module_number; void kg_config_native_cdn_ini_register(void) { diff --git a/extension/src/config/native_object_store/base_layer.c b/extension/src/config/native_object_store/base_layer.c index b59d43903..d7630ffc3 100644 --- a/extension/src/config/native_object_store/base_layer.c +++ b/extension/src/config/native_object_store/base_layer.c @@ -12,6 +12,6 @@ * ========================================================================= */ -#include "include/config/native_object_store/base_layer.h" +#include "config/native_object_store/base_layer.h" kg_native_object_store_config_t king_native_object_store_config; diff --git a/extension/src/config/native_object_store/config.c b/extension/src/config/native_object_store/config.c index 78c46edc6..fd4bf1258 100644 --- a/extension/src/config/native_object_store/config.c +++ b/extension/src/config/native_object_store/config.c @@ -12,15 +12,15 @@ * ========================================================================= */ -#include "include/config/native_object_store/config.h" -#include "include/config/native_object_store/base_layer.h" -#include "include/king_globals.h" +#include "config/native_object_store/config.h" +#include "config/native_object_store/base_layer.h" +#include "php_king/globals.h" -#include "include/validation/config_param/validate_bool.h" -#include "include/validation/config_param/validate_positive_long.h" -#include "include/validation/config_param/validate_string_from_allowlist.h" -#include "include/validation/config_param/validate_generic_string.h" -#include "include/validation/config_param/validate_erasure_coding_shards_string.h" +#include "validation/config_param/validate_bool.h" +#include "validation/config_param/validate_positive_long.h" +#include "validation/config_param/validate_string_from_allowlist.h" +#include "validation/config_param/validate_generic_string.h" +#include "validation/config_param/validate_erasure_coding_shards_string.h" #include "php.h" #include "zend_exceptions.h" diff --git a/extension/src/config/native_object_store/default.c b/extension/src/config/native_object_store/default.c index 1099ad0f0..2e141024e 100644 --- a/extension/src/config/native_object_store/default.c +++ b/extension/src/config/native_object_store/default.c @@ -11,8 +11,8 @@ * ========================================================================= */ -#include "include/config/native_object_store/default.h" -#include "include/config/native_object_store/base_layer.h" +#include "config/native_object_store/default.h" +#include "config/native_object_store/base_layer.h" void kg_config_native_object_store_defaults_load(void) { diff --git a/extension/src/config/native_object_store/index.c b/extension/src/config/native_object_store/index.c index 8c334d57b..9320e20d4 100644 --- a/extension/src/config/native_object_store/index.c +++ b/extension/src/config/native_object_store/index.c @@ -10,9 +10,9 @@ * ========================================================================= */ -#include "include/config/native_object_store/index.h" -#include "include/config/native_object_store/default.h" -#include "include/config/native_object_store/ini.h" +#include "config/native_object_store/index.h" +#include "config/native_object_store/default.h" +#include "config/native_object_store/ini.h" void kg_config_native_object_store_init(void) { diff --git a/extension/src/config/native_object_store/ini.c b/extension/src/config/native_object_store/ini.c index 56aa84aa5..e3585e509 100644 --- a/extension/src/config/native_object_store/ini.c +++ b/extension/src/config/native_object_store/ini.c @@ -12,8 +12,9 @@ * ========================================================================= */ -#include "include/config/native_object_store/ini.h" -#include "include/config/native_object_store/base_layer.h" +#include "config/native_object_store/ini.h" +#include "config/native_object_store/base_layer.h" +#include "php_king/init.h" #include "php.h" #include "zend_exceptions.h" @@ -136,7 +137,6 @@ PHP_INI_BEGIN() STD_PHP_INI_ENTRY("king.storage_enable_directstorage", "0", PHP_INI_SYSTEM, OnUpdateBool, enable_directstorage, kg_native_object_store_config_t, king_native_object_store_config) PHP_INI_END() -extern int king_ini_module_number; void kg_config_native_object_store_ini_register(void) { diff --git a/extension/src/config/open_telemetry/base_layer.c b/extension/src/config/open_telemetry/base_layer.c index df46cee21..baa714332 100644 --- a/extension/src/config/open_telemetry/base_layer.c +++ b/extension/src/config/open_telemetry/base_layer.c @@ -12,7 +12,7 @@ * ========================================================================= */ -#include "include/config/open_telemetry/base_layer.h" +#include "config/open_telemetry/base_layer.h" #include "ext/standard/url.h" #include "zend_smart_str.h" diff --git a/extension/src/config/open_telemetry/config.c b/extension/src/config/open_telemetry/config.c index ad51de328..e7d0e2017 100644 --- a/extension/src/config/open_telemetry/config.c +++ b/extension/src/config/open_telemetry/config.c @@ -12,16 +12,16 @@ * ========================================================================= */ -#include "include/config/open_telemetry/config.h" -#include "include/config/open_telemetry/base_layer.h" -#include "include/king_globals.h" - -#include "include/validation/config_param/validate_bool.h" -#include "include/validation/config_param/validate_positive_long.h" -#include "include/validation/config_param/validate_string_from_allowlist.h" -#include "include/validation/config_param/validate_generic_string.h" -#include "include/validation/config_param/validate_double_range.h" -#include "include/validation/config_param/validate_comma_separated_numeric_string.h" +#include "config/open_telemetry/config.h" +#include "config/open_telemetry/base_layer.h" +#include "php_king/globals.h" + +#include "validation/config_param/validate_bool.h" +#include "validation/config_param/validate_positive_long.h" +#include "validation/config_param/validate_string_from_allowlist.h" +#include "validation/config_param/validate_generic_string.h" +#include "validation/config_param/validate_double_range.h" +#include "validation/config_param/validate_comma_separated_numeric_string.h" #include "php.h" #include "zend_exceptions.h" diff --git a/extension/src/config/open_telemetry/default.c b/extension/src/config/open_telemetry/default.c index 3ae7e1510..137602325 100644 --- a/extension/src/config/open_telemetry/default.c +++ b/extension/src/config/open_telemetry/default.c @@ -12,8 +12,8 @@ * ========================================================================= */ -#include "include/config/open_telemetry/default.h" -#include "include/config/open_telemetry/base_layer.h" +#include "config/open_telemetry/default.h" +#include "config/open_telemetry/base_layer.h" void kg_config_open_telemetry_defaults_load(void) { diff --git a/extension/src/config/open_telemetry/index.c b/extension/src/config/open_telemetry/index.c index c346980d1..27f5b28d2 100644 --- a/extension/src/config/open_telemetry/index.c +++ b/extension/src/config/open_telemetry/index.c @@ -10,9 +10,9 @@ * ========================================================================= */ -#include "include/config/open_telemetry/index.h" -#include "include/config/open_telemetry/default.h" -#include "include/config/open_telemetry/ini.h" +#include "config/open_telemetry/index.h" +#include "config/open_telemetry/default.h" +#include "config/open_telemetry/ini.h" void kg_config_open_telemetry_init(void) { diff --git a/extension/src/config/open_telemetry/ini.c b/extension/src/config/open_telemetry/ini.c index 324149789..bf6c9c641 100644 --- a/extension/src/config/open_telemetry/ini.c +++ b/extension/src/config/open_telemetry/ini.c @@ -11,8 +11,9 @@ * ========================================================================= */ -#include "include/config/open_telemetry/ini.h" -#include "include/config/open_telemetry/base_layer.h" +#include "config/open_telemetry/ini.h" +#include "config/open_telemetry/base_layer.h" +#include "php_king/init.h" #include "php.h" #include "zend_exceptions.h" @@ -215,7 +216,6 @@ PHP_INI_BEGIN() ZEND_INI_ENTRY("king.otel_logs_exporter_batch_size", "512", PHP_INI_SYSTEM, OnUpdateOtelPositiveLong) PHP_INI_END() -extern int king_ini_module_number; void kg_config_open_telemetry_ini_register(void) { diff --git a/extension/src/config/php_binding.inc b/extension/src/config/php_binding.inc new file mode 100644 index 000000000..1066c91d6 --- /dev/null +++ b/extension/src/config/php_binding.inc @@ -0,0 +1,6 @@ +/* + * Config PHP binding aggregation. The config runtime translation unit includes + * this module boundary instead of reaching into config-internal binding leaves. + */ + +#include "internal/php_binding.inc" diff --git a/extension/src/config/quic_transport/base_layer.c b/extension/src/config/quic_transport/base_layer.c index 3ecdc12e5..9307f7753 100644 --- a/extension/src/config/quic_transport/base_layer.c +++ b/extension/src/config/quic_transport/base_layer.c @@ -12,6 +12,6 @@ * ========================================================================= */ -#include "include/config/quic_transport/base_layer.h" +#include "config/quic_transport/base_layer.h" kg_quic_transport_config_t king_quic_transport_config; diff --git a/extension/src/config/quic_transport/config.c b/extension/src/config/quic_transport/config.c index 3bd5902ad..6bc91abb0 100644 --- a/extension/src/config/quic_transport/config.c +++ b/extension/src/config/quic_transport/config.c @@ -12,13 +12,13 @@ * ========================================================================= */ -#include "include/config/quic_transport/config.h" -#include "include/config/quic_transport/base_layer.h" -#include "include/king_globals.h" +#include "config/quic_transport/config.h" +#include "config/quic_transport/base_layer.h" +#include "php_king/globals.h" -#include "include/validation/config_param/validate_bool.h" -#include "include/validation/config_param/validate_positive_long.h" -#include "include/validation/config_param/validate_string_from_allowlist.h" +#include "validation/config_param/validate_bool.h" +#include "validation/config_param/validate_positive_long.h" +#include "validation/config_param/validate_string_from_allowlist.h" #include "php.h" #include "zend_exceptions.h" diff --git a/extension/src/config/quic_transport/default.c b/extension/src/config/quic_transport/default.c index 3ab72a368..be1f0d263 100644 --- a/extension/src/config/quic_transport/default.c +++ b/extension/src/config/quic_transport/default.c @@ -12,8 +12,8 @@ * ========================================================================= */ -#include "include/config/quic_transport/default.h" -#include "include/config/quic_transport/base_layer.h" +#include "config/quic_transport/default.h" +#include "config/quic_transport/base_layer.h" void kg_config_quic_transport_defaults_load(void) { diff --git a/extension/src/config/quic_transport/index.c b/extension/src/config/quic_transport/index.c index aa464f9fd..a9da87142 100644 --- a/extension/src/config/quic_transport/index.c +++ b/extension/src/config/quic_transport/index.c @@ -10,9 +10,9 @@ * ========================================================================= */ -#include "include/config/quic_transport/index.h" -#include "include/config/quic_transport/default.h" -#include "include/config/quic_transport/ini.h" +#include "config/quic_transport/index.h" +#include "config/quic_transport/default.h" +#include "config/quic_transport/ini.h" void kg_config_quic_transport_init(void) { diff --git a/extension/src/config/quic_transport/ini.c b/extension/src/config/quic_transport/ini.c index ecb61b540..57929fa0f 100644 --- a/extension/src/config/quic_transport/ini.c +++ b/extension/src/config/quic_transport/ini.c @@ -12,8 +12,9 @@ * ========================================================================= */ -#include "include/config/quic_transport/ini.h" -#include "include/config/quic_transport/base_layer.h" +#include "config/quic_transport/ini.h" +#include "config/quic_transport/base_layer.h" +#include "php_king/init.h" #include "php.h" #include "zend_exceptions.h" @@ -149,7 +150,6 @@ PHP_INI_BEGIN() OnUpdateQuicPositiveLong, &king_quic_transport_config.dgram_send_queue_len, NULL) PHP_INI_END() -extern int king_ini_module_number; void kg_config_quic_transport_ini_register(void) { diff --git a/extension/src/config/registration.inc b/extension/src/config/registration.inc new file mode 100644 index 000000000..3a84ad458 --- /dev/null +++ b/extension/src/config/registration.inc @@ -0,0 +1,7 @@ +/* + * Config registration aggregation. The config runtime translation unit includes + * this module boundary instead of reaching into config-internal registration + * leaves. + */ + +#include "internal/registration.inc" diff --git a/extension/src/config/router_and_loadbalancer/base_layer.c b/extension/src/config/router_and_loadbalancer/base_layer.c index 5b4ebf04f..177a5add8 100644 --- a/extension/src/config/router_and_loadbalancer/base_layer.c +++ b/extension/src/config/router_and_loadbalancer/base_layer.c @@ -11,6 +11,6 @@ * ========================================================================= */ -#include "include/config/router_and_loadbalancer/base_layer.h" +#include "config/router_and_loadbalancer/base_layer.h" kg_router_loadbalancer_config_t king_router_loadbalancer_config; diff --git a/extension/src/config/router_and_loadbalancer/config.c b/extension/src/config/router_and_loadbalancer/config.c index 802e4efb9..a59a41da1 100644 --- a/extension/src/config/router_and_loadbalancer/config.c +++ b/extension/src/config/router_and_loadbalancer/config.c @@ -11,9 +11,9 @@ * ========================================================================= */ -#include "include/config/router_and_loadbalancer/config.h" -#include "include/config/router_and_loadbalancer/base_layer.h" -#include "include/king_globals.h" +#include "config/router_and_loadbalancer/config.h" +#include "config/router_and_loadbalancer/base_layer.h" +#include "php_king/globals.h" #include "php.h" #include diff --git a/extension/src/config/router_and_loadbalancer/default.c b/extension/src/config/router_and_loadbalancer/default.c index 75199f7dd..49872e797 100644 --- a/extension/src/config/router_and_loadbalancer/default.c +++ b/extension/src/config/router_and_loadbalancer/default.c @@ -6,13 +6,13 @@ * PURPOSE: * Default-value loader for the router and load-balancer config family. * This slice seeds the baseline router-disabled state, hashing mode, - * backend discovery placeholders, MCP poll cadence, and forwarding cap + * backend discovery fields, MCP poll cadence, and forwarding cap * before INI and any allowed userland overrides refine the live snapshot. * ========================================================================= */ -#include "include/config/router_and_loadbalancer/default.h" -#include "include/config/router_and_loadbalancer/base_layer.h" +#include "config/router_and_loadbalancer/default.h" +#include "config/router_and_loadbalancer/base_layer.h" void kg_config_router_and_loadbalancer_defaults_load(void) { diff --git a/extension/src/config/router_and_loadbalancer/index.c b/extension/src/config/router_and_loadbalancer/index.c index c7284e49e..9a9909b3d 100644 --- a/extension/src/config/router_and_loadbalancer/index.c +++ b/extension/src/config/router_and_loadbalancer/index.c @@ -10,9 +10,9 @@ * ========================================================================= */ -#include "include/config/router_and_loadbalancer/index.h" -#include "include/config/router_and_loadbalancer/default.h" -#include "include/config/router_and_loadbalancer/ini.h" +#include "config/router_and_loadbalancer/index.h" +#include "config/router_and_loadbalancer/default.h" +#include "config/router_and_loadbalancer/ini.h" void kg_config_router_and_loadbalancer_init(void) { diff --git a/extension/src/config/router_and_loadbalancer/ini.c b/extension/src/config/router_and_loadbalancer/ini.c index 81a97cc00..b14de696b 100644 --- a/extension/src/config/router_and_loadbalancer/ini.c +++ b/extension/src/config/router_and_loadbalancer/ini.c @@ -12,8 +12,9 @@ * ========================================================================= */ -#include "include/config/router_and_loadbalancer/ini.h" -#include "include/config/router_and_loadbalancer/base_layer.h" +#include "config/router_and_loadbalancer/ini.h" +#include "config/router_and_loadbalancer/base_layer.h" +#include "php_king/init.h" #include "php.h" #include @@ -21,7 +22,6 @@ #include #include -extern int king_ini_module_number; /* INI strings live in persistent module storage, so replace them manually. */ static void router_replace_string(char **target, zend_string *value) diff --git a/extension/src/config/security_and_traffic/base_layer.c b/extension/src/config/security_and_traffic/base_layer.c index 97bd5a29d..366f80013 100644 --- a/extension/src/config/security_and_traffic/base_layer.c +++ b/extension/src/config/security_and_traffic/base_layer.c @@ -11,6 +11,6 @@ * ========================================================================= */ -#include "include/config/security_and_traffic/base_layer.h" +#include "config/security_and_traffic/base_layer.h" kg_security_config_t king_security_config; diff --git a/extension/src/config/security_and_traffic/config.c b/extension/src/config/security_and_traffic/config.c index eae5de73f..9010ffde7 100644 --- a/extension/src/config/security_and_traffic/config.c +++ b/extension/src/config/security_and_traffic/config.c @@ -11,9 +11,9 @@ * ========================================================================= */ -#include "include/config/security_and_traffic/config.h" -#include "include/config/security_and_traffic/base_layer.h" -#include "include/king_globals.h" +#include "config/security_and_traffic/config.h" +#include "config/security_and_traffic/base_layer.h" +#include "php_king/globals.h" #include "php.h" #include diff --git a/extension/src/config/security_and_traffic/default.c b/extension/src/config/security_and_traffic/default.c index 79e1d64ce..79b2d6f38 100644 --- a/extension/src/config/security_and_traffic/default.c +++ b/extension/src/config/security_and_traffic/default.c @@ -11,8 +11,8 @@ * ========================================================================= */ -#include "include/config/security_and_traffic/default.h" -#include "include/config/security_and_traffic/base_layer.h" +#include "config/security_and_traffic/default.h" +#include "config/security_and_traffic/base_layer.h" void kg_config_security_defaults_load(void) { diff --git a/extension/src/config/security_and_traffic/index.c b/extension/src/config/security_and_traffic/index.c index a128691e1..e49da1269 100644 --- a/extension/src/config/security_and_traffic/index.c +++ b/extension/src/config/security_and_traffic/index.c @@ -10,9 +10,9 @@ * ========================================================================= */ -#include "include/config/security_and_traffic/index.h" -#include "include/config/security_and_traffic/default.h" -#include "include/config/security_and_traffic/ini.h" +#include "config/security_and_traffic/index.h" +#include "config/security_and_traffic/default.h" +#include "config/security_and_traffic/ini.h" void kg_config_security_and_traffic_init(void) { diff --git a/extension/src/config/security_and_traffic/ini.c b/extension/src/config/security_and_traffic/ini.c index 31fe5152b..2a2d602d5 100644 --- a/extension/src/config/security_and_traffic/ini.c +++ b/extension/src/config/security_and_traffic/ini.c @@ -12,9 +12,10 @@ * ========================================================================= */ -#include "include/config/security_and_traffic/ini.h" -#include "include/config/security_and_traffic/base_layer.h" -#include "include/king_globals.h" +#include "config/security_and_traffic/ini.h" +#include "config/security_and_traffic/base_layer.h" +#include "php_king/init.h" +#include "php_king/globals.h" #include "php.h" #include @@ -130,7 +131,6 @@ PHP_INI_BEGIN() ZEND_INI_ENTRY_EX("king.security_cors_allowed_origins", "*", PHP_INI_SYSTEM, OnUpdateCorsOrigins, NULL) PHP_INI_END() -extern int king_ini_module_number; void kg_config_security_ini_register(void) { diff --git a/extension/src/config/semantic_geometry/base_layer.c b/extension/src/config/semantic_geometry/base_layer.c index da56c642e..948d6facd 100644 --- a/extension/src/config/semantic_geometry/base_layer.c +++ b/extension/src/config/semantic_geometry/base_layer.c @@ -12,6 +12,6 @@ * ========================================================================= */ -#include "include/config/semantic_geometry/base_layer.h" +#include "config/semantic_geometry/base_layer.h" kg_semantic_geometry_config_t king_semantic_geometry_config = {0}; diff --git a/extension/src/config/semantic_geometry/config.c b/extension/src/config/semantic_geometry/config.c index fe30e85c9..01960ef00 100644 --- a/extension/src/config/semantic_geometry/config.c +++ b/extension/src/config/semantic_geometry/config.c @@ -12,13 +12,13 @@ * ========================================================================= */ -#include "include/config/semantic_geometry/config.h" -#include "include/config/semantic_geometry/base_layer.h" -#include "include/king_globals.h" +#include "config/semantic_geometry/config.h" +#include "config/semantic_geometry/base_layer.h" +#include "php_king/globals.h" -#include "include/validation/config_param/validate_positive_long.h" -#include "include/validation/config_param/validate_string_from_allowlist.h" -#include "include/validation/config_param/validate_double_range.h" +#include "validation/config_param/validate_positive_long.h" +#include "validation/config_param/validate_string_from_allowlist.h" +#include "validation/config_param/validate_double_range.h" #include "php.h" #include "zend_exceptions.h" diff --git a/extension/src/config/semantic_geometry/default.c b/extension/src/config/semantic_geometry/default.c index 3218eb054..0657f2e82 100644 --- a/extension/src/config/semantic_geometry/default.c +++ b/extension/src/config/semantic_geometry/default.c @@ -5,14 +5,14 @@ * * PURPOSE: * Default-value loader for the semantic geometry config family. This slice - * seeds the baseline vector dimensionality, algorithm placeholders, and + * seeds the baseline vector dimensionality, deferred algorithm fields, and * bounded search/consolidation thresholds before INI and any allowed * userland overrides refine the live geometry snapshot. * ========================================================================= */ -#include "include/config/semantic_geometry/default.h" -#include "include/config/semantic_geometry/base_layer.h" +#include "config/semantic_geometry/default.h" +#include "config/semantic_geometry/base_layer.h" void kg_config_semantic_geometry_defaults_load(void) { diff --git a/extension/src/config/semantic_geometry/index.c b/extension/src/config/semantic_geometry/index.c index a74ac20ef..f4f0fca5d 100644 --- a/extension/src/config/semantic_geometry/index.c +++ b/extension/src/config/semantic_geometry/index.c @@ -10,9 +10,9 @@ * ========================================================================= */ -#include "include/config/semantic_geometry/index.h" -#include "include/config/semantic_geometry/default.h" -#include "include/config/semantic_geometry/ini.h" +#include "config/semantic_geometry/index.h" +#include "config/semantic_geometry/default.h" +#include "config/semantic_geometry/ini.h" void kg_config_semantic_geometry_init(void) { diff --git a/extension/src/config/semantic_geometry/ini.c b/extension/src/config/semantic_geometry/ini.c index c0263cefc..a19fb6e2e 100644 --- a/extension/src/config/semantic_geometry/ini.c +++ b/extension/src/config/semantic_geometry/ini.c @@ -11,8 +11,9 @@ * ========================================================================= */ -#include "include/config/semantic_geometry/ini.h" -#include "include/config/semantic_geometry/base_layer.h" +#include "config/semantic_geometry/ini.h" +#include "config/semantic_geometry/base_layer.h" +#include "php_king/init.h" #include "php.h" #include @@ -41,7 +42,6 @@ PHP_INI_BEGIN() kg_semantic_geometry_config_t, king_semantic_geometry_config) PHP_INI_END() -extern int king_ini_module_number; void kg_config_semantic_geometry_ini_register(void) { diff --git a/extension/src/config/smart_contracts/base_layer.c b/extension/src/config/smart_contracts/base_layer.c index 1b978abec..b97b66400 100644 --- a/extension/src/config/smart_contracts/base_layer.c +++ b/extension/src/config/smart_contracts/base_layer.c @@ -11,6 +11,6 @@ * ========================================================================= */ -#include "include/config/smart_contracts/base_layer.h" +#include "config/smart_contracts/base_layer.h" kg_smart_contracts_config_t king_smart_contracts_config; diff --git a/extension/src/config/smart_contracts/config.c b/extension/src/config/smart_contracts/config.c index cd1e28c98..0ea18ded4 100644 --- a/extension/src/config/smart_contracts/config.c +++ b/extension/src/config/smart_contracts/config.c @@ -12,14 +12,14 @@ * ========================================================================= */ -#include "include/config/smart_contracts/config.h" -#include "include/config/smart_contracts/base_layer.h" -#include "include/king_globals.h" +#include "config/smart_contracts/config.h" +#include "config/smart_contracts/base_layer.h" +#include "php_king/globals.h" -#include "include/validation/config_param/validate_bool.h" -#include "include/validation/config_param/validate_generic_string.h" -#include "include/validation/config_param/validate_positive_long.h" -#include "include/validation/config_param/validate_string_from_allowlist.h" +#include "validation/config_param/validate_bool.h" +#include "validation/config_param/validate_generic_string.h" +#include "validation/config_param/validate_positive_long.h" +#include "validation/config_param/validate_string_from_allowlist.h" #include "php.h" #include diff --git a/extension/src/config/smart_contracts/default.c b/extension/src/config/smart_contracts/default.c index 8e1109e25..af144e84c 100644 --- a/extension/src/config/smart_contracts/default.c +++ b/extension/src/config/smart_contracts/default.c @@ -6,13 +6,13 @@ * PURPOSE: * Default-value loader for the smart-contracts config family. This slice * seeds the baseline module-disabled state, chain and gas defaults, wallet - * and HSM placeholders, ABI location placeholder, and event-listener flag + * and HSM fields, ABI location field, and event-listener flag * before INI and any allowed userland overrides refine the live snapshot. * ========================================================================= */ -#include "include/config/smart_contracts/default.h" -#include "include/config/smart_contracts/base_layer.h" +#include "config/smart_contracts/default.h" +#include "config/smart_contracts/base_layer.h" void kg_config_smart_contracts_defaults_load(void) { diff --git a/extension/src/config/smart_contracts/index.c b/extension/src/config/smart_contracts/index.c index f52ccb362..e246f446d 100644 --- a/extension/src/config/smart_contracts/index.c +++ b/extension/src/config/smart_contracts/index.c @@ -10,9 +10,9 @@ * ========================================================================= */ -#include "include/config/smart_contracts/index.h" -#include "include/config/smart_contracts/default.h" -#include "include/config/smart_contracts/ini.h" +#include "config/smart_contracts/index.h" +#include "config/smart_contracts/default.h" +#include "config/smart_contracts/ini.h" void kg_config_smart_contracts_init(void) { diff --git a/extension/src/config/smart_contracts/ini.c b/extension/src/config/smart_contracts/ini.c index 511bbbd4a..e8f820045 100644 --- a/extension/src/config/smart_contracts/ini.c +++ b/extension/src/config/smart_contracts/ini.c @@ -12,8 +12,9 @@ * ========================================================================= */ -#include "include/config/smart_contracts/ini.h" -#include "include/config/smart_contracts/base_layer.h" +#include "config/smart_contracts/ini.h" +#include "config/smart_contracts/base_layer.h" +#include "php_king/init.h" #include "php.h" #include @@ -113,7 +114,6 @@ PHP_INI_BEGIN() OnUpdateBool, event_listener_enable, kg_smart_contracts_config_t, king_smart_contracts_config) PHP_INI_END() -extern int king_ini_module_number; void kg_config_smart_contracts_ini_register(void) { diff --git a/extension/src/config/smart_dns/base_layer.c b/extension/src/config/smart_dns/base_layer.c index 7ebdf5e01..dc1a76b92 100644 --- a/extension/src/config/smart_dns/base_layer.c +++ b/extension/src/config/smart_dns/base_layer.c @@ -12,6 +12,6 @@ * ========================================================================= */ -#include "include/config/smart_dns/base_layer.h" +#include "config/smart_dns/base_layer.h" kg_smart_dns_config_t king_smart_dns_config = {0}; diff --git a/extension/src/config/smart_dns/config.c b/extension/src/config/smart_dns/config.c index 506ad7e51..542aa79b1 100644 --- a/extension/src/config/smart_dns/config.c +++ b/extension/src/config/smart_dns/config.c @@ -11,13 +11,13 @@ * ========================================================================= */ -#include "include/config/smart_dns/config.h" -#include "include/config/smart_dns/base_layer.h" -#include "include/king_globals.h" +#include "config/smart_dns/config.h" +#include "config/smart_dns/base_layer.h" +#include "php_king/globals.h" -#include "include/validation/config_param/validate_bool.h" -#include "include/validation/config_param/validate_positive_long.h" -#include "include/validation/config_param/validate_string.h" +#include "validation/config_param/validate_bool.h" +#include "validation/config_param/validate_positive_long.h" +#include "validation/config_param/validate_string.h" #include "php.h" #include "zend_exceptions.h" @@ -148,6 +148,10 @@ int kg_config_smart_dns_apply_userland_config_to( if (kg_validate_string(value, &target->mothernode_uri) != SUCCESS) { return FAILURE; } + } else if (zend_string_equals_literal(key, "dns_state_path")) { + if (kg_validate_string(value, &target->state_path) != SUCCESS) { + return FAILURE; + } } } ZEND_HASH_FOREACH_END(); diff --git a/extension/src/config/smart_dns/default.c b/extension/src/config/smart_dns/default.c index d0f04b6a4..86553f05b 100644 --- a/extension/src/config/smart_dns/default.c +++ b/extension/src/config/smart_dns/default.c @@ -7,13 +7,13 @@ * Default-value loader for the Smart-DNS config family. This slice seeds * the baseline server-disabled state, bind/port, TTL, service-discovery * fan-out limit, semantic-mode toggle, and mothernode / live-probe - * placeholders before INI and any allowed userland overrides refine the + * fields before INI and any allowed userland overrides refine the * live DNS snapshot. * ========================================================================= */ -#include "include/config/smart_dns/default.h" -#include "include/config/smart_dns/base_layer.h" +#include "config/smart_dns/default.h" +#include "config/smart_dns/base_layer.h" void kg_config_smart_dns_defaults_load(void) { @@ -28,4 +28,5 @@ void kg_config_smart_dns_defaults_load(void) king_smart_dns_config.semantic_mode_enable = false; king_smart_dns_config.mothernode_uri = NULL; king_smart_dns_config.live_probe_allowed_hosts = NULL; + king_smart_dns_config.state_path = NULL; } diff --git a/extension/src/config/smart_dns/index.c b/extension/src/config/smart_dns/index.c index 5a8156f21..f3e85f298 100644 --- a/extension/src/config/smart_dns/index.c +++ b/extension/src/config/smart_dns/index.c @@ -10,9 +10,9 @@ * ========================================================================= */ -#include "include/config/smart_dns/index.h" -#include "include/config/smart_dns/default.h" -#include "include/config/smart_dns/ini.h" +#include "config/smart_dns/index.h" +#include "config/smart_dns/default.h" +#include "config/smart_dns/ini.h" void kg_config_smart_dns_init(void) { diff --git a/extension/src/config/smart_dns/ini.c b/extension/src/config/smart_dns/ini.c index cc2cc1808..7f3141e25 100644 --- a/extension/src/config/smart_dns/ini.c +++ b/extension/src/config/smart_dns/ini.c @@ -12,8 +12,9 @@ * ========================================================================= */ -#include "include/config/smart_dns/ini.h" -#include "include/config/smart_dns/base_layer.h" +#include "config/smart_dns/ini.h" +#include "config/smart_dns/base_layer.h" +#include "php_king/init.h" #include "php.h" #include @@ -159,9 +160,10 @@ PHP_INI_BEGIN() OnUpdateDnsProbeHostAllowlist, NULL) STD_PHP_INI_ENTRY("king.dns_mothernode_uri", "", PHP_INI_SYSTEM, OnUpdateString, mothernode_uri, kg_smart_dns_config_t, king_smart_dns_config) + STD_PHP_INI_ENTRY("king.dns_state_path", "", PHP_INI_SYSTEM, + OnUpdateString, state_path, kg_smart_dns_config_t, king_smart_dns_config) PHP_INI_END() -extern int king_ini_module_number; void kg_config_smart_dns_ini_register(void) { diff --git a/extension/src/config/ssh_over_quic/base_layer.c b/extension/src/config/ssh_over_quic/base_layer.c index d6fdd7be1..4a65f305d 100644 --- a/extension/src/config/ssh_over_quic/base_layer.c +++ b/extension/src/config/ssh_over_quic/base_layer.c @@ -12,6 +12,6 @@ * ========================================================================= */ -#include "include/config/ssh_over_quic/base_layer.h" +#include "config/ssh_over_quic/base_layer.h" kg_ssh_over_quic_config_t king_ssh_over_quic_config; diff --git a/extension/src/config/ssh_over_quic/config.c b/extension/src/config/ssh_over_quic/config.c index 50457acaa..74041954d 100644 --- a/extension/src/config/ssh_over_quic/config.c +++ b/extension/src/config/ssh_over_quic/config.c @@ -12,14 +12,14 @@ * ========================================================================= */ -#include "include/config/ssh_over_quic/config.h" -#include "include/config/ssh_over_quic/base_layer.h" -#include "include/king_globals.h" +#include "config/ssh_over_quic/config.h" +#include "config/ssh_over_quic/base_layer.h" +#include "php_king/globals.h" -#include "include/validation/config_param/validate_bool.h" -#include "include/validation/config_param/validate_positive_long.h" -#include "include/validation/config_param/validate_string.h" -#include "include/validation/config_param/validate_string_from_allowlist.h" +#include "validation/config_param/validate_bool.h" +#include "validation/config_param/validate_positive_long.h" +#include "validation/config_param/validate_string.h" +#include "validation/config_param/validate_string_from_allowlist.h" #include "php.h" #include "zend_exceptions.h" diff --git a/extension/src/config/ssh_over_quic/default.c b/extension/src/config/ssh_over_quic/default.c index ba3c92b69..ba2b64d42 100644 --- a/extension/src/config/ssh_over_quic/default.c +++ b/extension/src/config/ssh_over_quic/default.c @@ -6,14 +6,14 @@ * PURPOSE: * Default-value loader for the SSH-over-QUIC config family. This slice * seeds the baseline gateway-disabled state, bind/target endpoints, auth - * and mapping modes, MCP/user-profile agent placeholders, timeout values, + * and mapping modes, MCP/user-profile agent fields, timeout values, * and session-activity logging before INI and any allowed userland * overrides refine the live snapshot. * ========================================================================= */ -#include "include/config/ssh_over_quic/default.h" -#include "include/config/ssh_over_quic/base_layer.h" +#include "config/ssh_over_quic/default.h" +#include "config/ssh_over_quic/base_layer.h" void kg_config_ssh_over_quic_defaults_load(void) { diff --git a/extension/src/config/ssh_over_quic/index.c b/extension/src/config/ssh_over_quic/index.c index 39fea3745..a0d6ee8d4 100644 --- a/extension/src/config/ssh_over_quic/index.c +++ b/extension/src/config/ssh_over_quic/index.c @@ -10,9 +10,9 @@ * ========================================================================= */ -#include "include/config/ssh_over_quic/index.h" -#include "include/config/ssh_over_quic/default.h" -#include "include/config/ssh_over_quic/ini.h" +#include "config/ssh_over_quic/index.h" +#include "config/ssh_over_quic/default.h" +#include "config/ssh_over_quic/ini.h" void kg_config_ssh_over_quic_init(void) { diff --git a/extension/src/config/ssh_over_quic/ini.c b/extension/src/config/ssh_over_quic/ini.c index 4d40a6e24..66e0e6bf6 100644 --- a/extension/src/config/ssh_over_quic/ini.c +++ b/extension/src/config/ssh_over_quic/ini.c @@ -12,8 +12,9 @@ * ========================================================================= */ -#include "include/config/ssh_over_quic/ini.h" -#include "include/config/ssh_over_quic/base_layer.h" +#include "config/ssh_over_quic/ini.h" +#include "config/ssh_over_quic/base_layer.h" +#include "php_king/init.h" #include "php.h" #include "zend_exceptions.h" @@ -120,7 +121,6 @@ PHP_INI_BEGIN() OnUpdateBool, gateway_log_session_activity, kg_ssh_over_quic_config_t, king_ssh_over_quic_config) PHP_INI_END() -extern int king_ini_module_number; void kg_config_ssh_over_quic_ini_register(void) { diff --git a/extension/src/config/state.inc b/extension/src/config/state.inc new file mode 100644 index 000000000..15010b79c --- /dev/null +++ b/extension/src/config/state.inc @@ -0,0 +1,6 @@ +/* + * Config state storage aggregation. The config runtime translation unit owns + * this module boundary instead of exposing config-internal state leaves. + */ + +#include "internal/state.inc" diff --git a/extension/src/config/state_management/base_layer.c b/extension/src/config/state_management/base_layer.c index ca7706d92..8b09db3ff 100644 --- a/extension/src/config/state_management/base_layer.c +++ b/extension/src/config/state_management/base_layer.c @@ -11,6 +11,6 @@ * ========================================================================= */ -#include "include/config/state_management/base_layer.h" +#include "config/state_management/base_layer.h" kg_state_management_config_t king_state_management_config = {0}; diff --git a/extension/src/config/state_management/config.c b/extension/src/config/state_management/config.c index dac5808f3..da1732516 100644 --- a/extension/src/config/state_management/config.c +++ b/extension/src/config/state_management/config.c @@ -11,12 +11,12 @@ * ========================================================================= */ -#include "include/config/state_management/config.h" -#include "include/config/state_management/base_layer.h" -#include "include/king_globals.h" +#include "config/state_management/config.h" +#include "config/state_management/base_layer.h" +#include "php_king/globals.h" -#include "include/validation/config_param/validate_string_from_allowlist.h" -#include "include/validation/config_param/validate_string.h" +#include "validation/config_param/validate_string_from_allowlist.h" +#include "validation/config_param/validate_string.h" #include "php.h" #include "zend_exceptions.h" diff --git a/extension/src/config/state_management/default.c b/extension/src/config/state_management/default.c index 86979cc98..d27fe125a 100644 --- a/extension/src/config/state_management/default.c +++ b/extension/src/config/state_management/default.c @@ -5,13 +5,13 @@ * * PURPOSE: * Default-value loader for the state-management config family. This slice - * seeds the baseline backend and URI placeholders before INI and any + * seeds the baseline backend and URI fields before INI and any * allowed userland overrides refine the live state-management snapshot. * ========================================================================= */ -#include "include/config/state_management/default.h" -#include "include/config/state_management/base_layer.h" +#include "config/state_management/default.h" +#include "config/state_management/base_layer.h" void kg_config_state_management_defaults_load(void) { diff --git a/extension/src/config/state_management/index.c b/extension/src/config/state_management/index.c index 562a1841f..7fa640d0e 100644 --- a/extension/src/config/state_management/index.c +++ b/extension/src/config/state_management/index.c @@ -10,9 +10,9 @@ * ========================================================================= */ -#include "include/config/state_management/index.h" -#include "include/config/state_management/default.h" -#include "include/config/state_management/ini.h" +#include "config/state_management/index.h" +#include "config/state_management/default.h" +#include "config/state_management/ini.h" void kg_config_state_management_init(void) { diff --git a/extension/src/config/state_management/ini.c b/extension/src/config/state_management/ini.c index 6ad9235a4..e18f84e6d 100644 --- a/extension/src/config/state_management/ini.c +++ b/extension/src/config/state_management/ini.c @@ -11,8 +11,9 @@ * ========================================================================= */ -#include "include/config/state_management/ini.h" -#include "include/config/state_management/base_layer.h" +#include "config/state_management/ini.h" +#include "config/state_management/base_layer.h" +#include "php_king/init.h" #include "php.h" #include @@ -49,7 +50,6 @@ PHP_INI_BEGIN() default_uri, kg_state_management_config_t, king_state_management_config) PHP_INI_END() -extern int king_ini_module_number; void kg_config_state_management_ini_register(void) { diff --git a/extension/src/config/tcp_transport/base_layer.c b/extension/src/config/tcp_transport/base_layer.c index 65263a41d..21ebaabd1 100644 --- a/extension/src/config/tcp_transport/base_layer.c +++ b/extension/src/config/tcp_transport/base_layer.c @@ -12,6 +12,6 @@ * ========================================================================= */ -#include "include/config/tcp_transport/base_layer.h" +#include "config/tcp_transport/base_layer.h" kg_tcp_transport_config_t king_tcp_transport_config; diff --git a/extension/src/config/tcp_transport/config.c b/extension/src/config/tcp_transport/config.c index 26cc37adf..baa3990e3 100644 --- a/extension/src/config/tcp_transport/config.c +++ b/extension/src/config/tcp_transport/config.c @@ -12,14 +12,14 @@ * ========================================================================= */ -#include "include/config/tcp_transport/config.h" -#include "include/config/tcp_transport/base_layer.h" -#include "include/king_globals.h" +#include "config/tcp_transport/config.h" +#include "config/tcp_transport/base_layer.h" +#include "php_king/globals.h" -#include "include/validation/config_param/validate_bool.h" -#include "include/validation/config_param/validate_positive_long.h" -#include "include/validation/config_param/validate_string.h" -#include "include/validation/config_param/validate_string_from_allowlist.h" +#include "validation/config_param/validate_bool.h" +#include "validation/config_param/validate_positive_long.h" +#include "validation/config_param/validate_string.h" +#include "validation/config_param/validate_string_from_allowlist.h" #include "php.h" #include "zend_exceptions.h" diff --git a/extension/src/config/tcp_transport/default.c b/extension/src/config/tcp_transport/default.c index 1f807ce29..d1ea45620 100644 --- a/extension/src/config/tcp_transport/default.c +++ b/extension/src/config/tcp_transport/default.c @@ -6,13 +6,13 @@ * PURPOSE: * Default-value loader for the TCP transport config family. This slice * seeds the baseline transport-enabled state, connection and backlog caps, - * socket tuning, keepalive timings, and TLS policy placeholders before INI + * socket tuning, keepalive timings, and TLS policy fields before INI * and any allowed userland overrides refine the live transport snapshot. * ========================================================================= */ -#include "include/config/tcp_transport/default.h" -#include "include/config/tcp_transport/base_layer.h" +#include "config/tcp_transport/default.h" +#include "config/tcp_transport/base_layer.h" void kg_config_tcp_transport_defaults_load(void) { diff --git a/extension/src/config/tcp_transport/index.c b/extension/src/config/tcp_transport/index.c index 4f4a0e2bb..00d377b43 100644 --- a/extension/src/config/tcp_transport/index.c +++ b/extension/src/config/tcp_transport/index.c @@ -10,9 +10,9 @@ * ========================================================================= */ -#include "include/config/tcp_transport/index.h" -#include "include/config/tcp_transport/default.h" -#include "include/config/tcp_transport/ini.h" +#include "config/tcp_transport/index.h" +#include "config/tcp_transport/default.h" +#include "config/tcp_transport/ini.h" void kg_config_tcp_transport_init(void) { diff --git a/extension/src/config/tcp_transport/ini.c b/extension/src/config/tcp_transport/ini.c index e66343637..705c07b13 100644 --- a/extension/src/config/tcp_transport/ini.c +++ b/extension/src/config/tcp_transport/ini.c @@ -12,8 +12,9 @@ * ========================================================================= */ -#include "include/config/tcp_transport/ini.h" -#include "include/config/tcp_transport/base_layer.h" +#include "config/tcp_transport/ini.h" +#include "config/tcp_transport/base_layer.h" +#include "php_king/init.h" #include "php.h" #include "zend_exceptions.h" @@ -101,7 +102,6 @@ PHP_INI_BEGIN() PHP_INI_SYSTEM, OnUpdateString, tls_ciphers_tls12, kg_tcp_transport_config_t, king_tcp_transport_config) PHP_INI_END() -extern int king_ini_module_number; void kg_config_tcp_transport_ini_register(void) { diff --git a/extension/src/config/tls_and_crypto/base_layer.c b/extension/src/config/tls_and_crypto/base_layer.c index 588031bcc..cd8207079 100644 --- a/extension/src/config/tls_and_crypto/base_layer.c +++ b/extension/src/config/tls_and_crypto/base_layer.c @@ -12,6 +12,6 @@ * ========================================================================= */ -#include "include/config/tls_and_crypto/base_layer.h" +#include "config/tls_and_crypto/base_layer.h" kg_tls_and_crypto_config_t king_tls_and_crypto_config; diff --git a/extension/src/config/tls_and_crypto/config.c b/extension/src/config/tls_and_crypto/config.c index d1026d8b9..007065db4 100644 --- a/extension/src/config/tls_and_crypto/config.c +++ b/extension/src/config/tls_and_crypto/config.c @@ -12,14 +12,14 @@ * ========================================================================= */ -#include "include/config/tls_and_crypto/config.h" -#include "include/config/tls_and_crypto/base_layer.h" -#include "include/king_globals.h" +#include "config/tls_and_crypto/config.h" +#include "config/tls_and_crypto/base_layer.h" +#include "php_king/globals.h" -#include "include/validation/config_param/validate_bool.h" -#include "include/validation/config_param/validate_positive_long.h" -#include "include/validation/config_param/validate_string.h" -#include "include/validation/config_param/validate_string_from_allowlist.h" +#include "validation/config_param/validate_bool.h" +#include "validation/config_param/validate_positive_long.h" +#include "validation/config_param/validate_string.h" +#include "validation/config_param/validate_string_from_allowlist.h" #include "php.h" #include "zend_exceptions.h" diff --git a/extension/src/config/tls_and_crypto/default.c b/extension/src/config/tls_and_crypto/default.c index 4d5a2a8d3..cb49868e2 100644 --- a/extension/src/config/tls_and_crypto/default.c +++ b/extension/src/config/tls_and_crypto/default.c @@ -5,15 +5,15 @@ * * PURPOSE: * Default-value loader for the TLS and crypto config family. This slice - * seeds the baseline verification depth, trust and identity placeholders, + * seeds the baseline verification depth, trust and identity fields, * cipher and curve policy, ticket / 0-RTT settings, OCSP behavior, and the * disabled-at-rest / MCP encryption flags before INI and any allowed * userland overrides refine the live crypto snapshot. * ========================================================================= */ -#include "include/config/tls_and_crypto/default.h" -#include "include/config/tls_and_crypto/base_layer.h" +#include "config/tls_and_crypto/default.h" +#include "config/tls_and_crypto/base_layer.h" void kg_config_tls_and_crypto_defaults_load(void) { diff --git a/extension/src/config/tls_and_crypto/index.c b/extension/src/config/tls_and_crypto/index.c index 52ccd8b7f..e4b829e6f 100644 --- a/extension/src/config/tls_and_crypto/index.c +++ b/extension/src/config/tls_and_crypto/index.c @@ -10,9 +10,9 @@ * ========================================================================= */ -#include "include/config/tls_and_crypto/index.h" -#include "include/config/tls_and_crypto/default.h" -#include "include/config/tls_and_crypto/ini.h" +#include "config/tls_and_crypto/index.h" +#include "config/tls_and_crypto/default.h" +#include "config/tls_and_crypto/ini.h" void kg_config_tls_and_crypto_init(void) { diff --git a/extension/src/config/tls_and_crypto/ini.c b/extension/src/config/tls_and_crypto/ini.c index dd6b518fd..7fb2215ac 100644 --- a/extension/src/config/tls_and_crypto/ini.c +++ b/extension/src/config/tls_and_crypto/ini.c @@ -12,8 +12,9 @@ * ========================================================================= */ -#include "include/config/tls_and_crypto/ini.h" -#include "include/config/tls_and_crypto/base_layer.h" +#include "config/tls_and_crypto/ini.h" +#include "config/tls_and_crypto/base_layer.h" +#include "php_king/init.h" #include "php.h" #include "zend_exceptions.h" @@ -130,7 +131,6 @@ PHP_INI_BEGIN() transport_disable_encryption, kg_tls_and_crypto_config_t, king_tls_and_crypto_config) PHP_INI_END() -extern int king_ini_module_number; void kg_config_tls_and_crypto_ini_register(void) { diff --git a/extension/src/core/health.c b/extension/src/core/health.c index e22d17df7..ba2fed3a4 100644 --- a/extension/src/core/health.c +++ b/extension/src/core/health.c @@ -14,7 +14,7 @@ #include "php.h" #include "php_king.h" -#include "include/king_globals.h" +#include "php_king/globals.h" #include static const char *const king_active_runtime_names[] = { @@ -38,6 +38,7 @@ static const char *const king_active_runtime_names[] = { "server_tls_runtime", "server_cors_runtime", "server_open_telemetry_runtime", + "rtp_runtime", "iibin_proto", "semantic_dns_registry", "semantic_dns_server_runtime", @@ -94,7 +95,7 @@ const char *king_get_active_runtime_summary(void) "server_early_hints_runtime, server_websocket_upgrade_runtime, " "server_admin_api_runtime, server_tls_runtime, " "server_cors_runtime, server_open_telemetry_runtime, " - "iibin_proto, semantic_dns_registry, semantic_dns_server_runtime, " + "rtp_runtime, iibin_proto, semantic_dns_registry, semantic_dns_server_runtime, " "object_store_registry, cdn_cache_registry, xslt_saxonc_runtime, mcp_runtime, " "pipeline_orchestrator_runtime, telemetry_runtime, " "autoscaling_runtime, system_integration_runtime"; @@ -118,7 +119,7 @@ const char *king_get_stubbed_api_summary(void) * 'php_version' => '8.4.x', * 'pid' => 12345, * 'config_override_allowed' => false, // king_globals.is_userland_override_allowed - * 'active_runtime_count' => 31, + * 'active_runtime_count' => 32, * 'active_runtimes' => [...], * 'stubbed_api_group_count' => 0, * 'stubbed_api_groups' => [...], diff --git a/extension/src/core/introspection.c b/extension/src/core/introspection.c index 74395242f..d8650590e 100644 --- a/extension/src/core/introspection.c +++ b/extension/src/core/introspection.c @@ -11,9 +11,9 @@ * ========================================================================= */ -#include "include/config/router_and_loadbalancer/base_layer.h" -#include "include/pipeline_orchestrator/orchestrator.h" -#include "include/telemetry/telemetry.h" +#include "config/router_and_loadbalancer/base_layer.h" +#include "pipeline_orchestrator/orchestrator.h" +#include "telemetry/telemetry.h" #include "introspection/prelude.inc" #include "introspection/telemetry.inc" #include "introspection/object_store.inc" diff --git a/extension/src/core/introspection/object_store/init.inc b/extension/src/core/introspection/object_store/init.inc index 70e85d728..ac6d9ab0b 100644 --- a/extension/src/core/introspection/object_store/init.inc +++ b/extension/src/core/introspection/object_store/init.inc @@ -8,7 +8,7 @@ #include "object_store/object_store_internal.h" -#include "include/king_globals.h" +#include "php_king/globals.h" static int king_object_store_parse_backend_config( zval *value_zv, diff --git a/extension/src/core/introspection/object_store/registry/read_and_maintenance_api.inc b/extension/src/core/introspection/object_store/registry/read_and_maintenance_api.inc index 6cec26689..b347e0a7d 100644 --- a/extension/src/core/introspection/object_store/registry/read_and_maintenance_api.inc +++ b/extension/src/core/introspection/object_store/registry/read_and_maintenance_api.inc @@ -574,240 +574,4 @@ PHP_FUNCTION(king_object_store_get_metadata) add_assoc_long(return_value, "distribution_peer_count", (zend_long) metadata.distribution_peer_count); } -PHP_FUNCTION(king_object_store_backup_object) -{ - zend_string *object_id; - zend_string *destination_path; - - ZEND_PARSE_PARAMETERS_START(2, 2) - Z_PARAM_STR(object_id) - Z_PARAM_STR(destination_path) - ZEND_PARSE_PARAMETERS_END(); - - { - const char *object_id_error = king_object_store_public_object_id_validate_zstr(object_id); - - if (object_id_error != NULL) { - zend_throw_exception_ex( - king_ce_validation_exception, - 0, - "%s", - object_id_error - ); - RETURN_THROWS(); - } - } - - if (ZSTR_LEN(destination_path) == 0) { - zend_throw_exception_ex( - king_ce_validation_exception, - 0, - "Destination path must be a non-empty string." - ); - RETURN_THROWS(); - } - - if (!king_object_store_runtime.initialized) { - zend_throw_exception_ex( - king_ce_runtime_exception, - 0, - "Object-store registry is unavailable." - ); - RETURN_THROWS(); - } - - RETURN_BOOL( - king_object_store_backup_object( - ZSTR_VAL(object_id), - ZSTR_VAL(destination_path) - ) == SUCCESS - ); -} - -PHP_FUNCTION(king_object_store_restore_object) -{ - zend_string *object_id; - zend_string *source_path; - - ZEND_PARSE_PARAMETERS_START(2, 2) - Z_PARAM_STR(object_id) - Z_PARAM_STR(source_path) - ZEND_PARSE_PARAMETERS_END(); - - { - const char *object_id_error = king_object_store_public_object_id_validate_zstr(object_id); - - if (object_id_error != NULL) { - zend_throw_exception_ex( - king_ce_validation_exception, - 0, - "%s", - object_id_error - ); - RETURN_THROWS(); - } - } - - if (ZSTR_LEN(source_path) == 0) { - zend_throw_exception_ex( - king_ce_validation_exception, - 0, - "Source path must be a non-empty string." - ); - RETURN_THROWS(); - } - - if (!king_object_store_runtime.initialized) { - zend_throw_exception_ex( - king_ce_runtime_exception, - 0, - "Object-store registry is unavailable." - ); - RETURN_THROWS(); - } - - RETURN_BOOL( - king_object_store_restore_object( - ZSTR_VAL(object_id), - ZSTR_VAL(source_path) - ) == SUCCESS - ); -} - -PHP_FUNCTION(king_object_store_backup_all_objects) -{ - zend_string *destination_path; - zval *options = NULL; - zval *mode_zv; - zval *base_snapshot_path_zv; - const char *mode = "full"; - const char *base_snapshot_path = NULL; - zend_bool incremental = 0; - - ZEND_PARSE_PARAMETERS_START(1, 2) - Z_PARAM_STR(destination_path) - Z_PARAM_OPTIONAL - Z_PARAM_ARRAY_OR_NULL(options) - ZEND_PARSE_PARAMETERS_END(); - - if (ZSTR_LEN(destination_path) == 0) { - zend_throw_exception_ex( - king_ce_validation_exception, - 0, - "Destination path must be a non-empty string." - ); - RETURN_THROWS(); - } - if (!king_object_store_runtime.initialized) { - zend_throw_exception_ex( - king_ce_runtime_exception, - 0, - "Object-store registry is unavailable." - ); - RETURN_THROWS(); - } - if (options != NULL) { - mode_zv = zend_hash_str_find( - Z_ARRVAL_P(options), - "mode", - sizeof("mode") - 1 - ); - if (mode_zv != NULL) { - if (Z_TYPE_P(mode_zv) != IS_STRING || Z_STRLEN_P(mode_zv) == 0) { - zend_throw_exception_ex( - king_ce_validation_exception, - 0, - "king_object_store_backup_all_objects() option 'mode' must be a non-empty string when provided." - ); - RETURN_THROWS(); - } - - mode = Z_STRVAL_P(mode_zv); - } - - if (strcmp(mode, "full") == 0) { - incremental = 0; - } else if (strcmp(mode, "incremental") == 0) { - incremental = 1; - } else { - zend_throw_exception_ex( - king_ce_validation_exception, - 0, - "king_object_store_backup_all_objects() option 'mode' must be either 'full' or 'incremental'." - ); - RETURN_THROWS(); - } - - base_snapshot_path_zv = zend_hash_str_find( - Z_ARRVAL_P(options), - "base_snapshot_path", - sizeof("base_snapshot_path") - 1 - ); - if (base_snapshot_path_zv != NULL) { - if (Z_TYPE_P(base_snapshot_path_zv) != IS_STRING || Z_STRLEN_P(base_snapshot_path_zv) == 0) { - zend_throw_exception_ex( - king_ce_validation_exception, - 0, - "king_object_store_backup_all_objects() option 'base_snapshot_path' must be a non-empty string when provided." - ); - RETURN_THROWS(); - } - - base_snapshot_path = Z_STRVAL_P(base_snapshot_path_zv); - } - } - if (incremental && base_snapshot_path == NULL) { - zend_throw_exception_ex( - king_ce_validation_exception, - 0, - "king_object_store_backup_all_objects() incremental mode requires option 'base_snapshot_path'." - ); - RETURN_THROWS(); - } - if (!incremental && base_snapshot_path != NULL) { - zend_throw_exception_ex( - king_ce_validation_exception, - 0, - "king_object_store_backup_all_objects() option 'base_snapshot_path' is only valid when mode='incremental'." - ); - RETURN_THROWS(); - } - - RETURN_BOOL( - king_object_store_backup_all_objects( - ZSTR_VAL(destination_path), - incremental, - base_snapshot_path - ) == SUCCESS - ); -} - -PHP_FUNCTION(king_object_store_restore_all_objects) -{ - zend_string *source_path; - - ZEND_PARSE_PARAMETERS_START(1, 1) - Z_PARAM_STR(source_path) - ZEND_PARSE_PARAMETERS_END(); - - if (ZSTR_LEN(source_path) == 0) { - zend_throw_exception_ex( - king_ce_validation_exception, - 0, - "Source path must be a non-empty string." - ); - RETURN_THROWS(); - } - if (!king_object_store_runtime.initialized) { - zend_throw_exception_ex( - king_ce_runtime_exception, - 0, - "Object-store registry is unavailable." - ); - RETURN_THROWS(); - } - - RETURN_BOOL( - king_object_store_restore_all_objects(ZSTR_VAL(source_path)) == SUCCESS - ); -} +#include "read_and_maintenance_api/backup_restore_api.inc" diff --git a/extension/src/core/introspection/object_store/registry/read_and_maintenance_api/backup_restore_api.inc b/extension/src/core/introspection/object_store/registry/read_and_maintenance_api/backup_restore_api.inc new file mode 100644 index 000000000..f8ffca2b9 --- /dev/null +++ b/extension/src/core/introspection/object_store/registry/read_and_maintenance_api/backup_restore_api.inc @@ -0,0 +1,237 @@ +PHP_FUNCTION(king_object_store_backup_object) +{ + zend_string *object_id; + zend_string *destination_path; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_STR(object_id) + Z_PARAM_STR(destination_path) + ZEND_PARSE_PARAMETERS_END(); + + { + const char *object_id_error = king_object_store_public_object_id_validate_zstr(object_id); + + if (object_id_error != NULL) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s", + object_id_error + ); + RETURN_THROWS(); + } + } + + if (ZSTR_LEN(destination_path) == 0) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "Destination path must be a non-empty string." + ); + RETURN_THROWS(); + } + + if (!king_object_store_runtime.initialized) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "Object-store registry is unavailable." + ); + RETURN_THROWS(); + } + + RETURN_BOOL( + king_object_store_backup_object( + ZSTR_VAL(object_id), + ZSTR_VAL(destination_path) + ) == SUCCESS + ); +} + +PHP_FUNCTION(king_object_store_restore_object) +{ + zend_string *object_id; + zend_string *source_path; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_STR(object_id) + Z_PARAM_STR(source_path) + ZEND_PARSE_PARAMETERS_END(); + + { + const char *object_id_error = king_object_store_public_object_id_validate_zstr(object_id); + + if (object_id_error != NULL) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s", + object_id_error + ); + RETURN_THROWS(); + } + } + + if (ZSTR_LEN(source_path) == 0) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "Source path must be a non-empty string." + ); + RETURN_THROWS(); + } + + if (!king_object_store_runtime.initialized) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "Object-store registry is unavailable." + ); + RETURN_THROWS(); + } + + RETURN_BOOL( + king_object_store_restore_object( + ZSTR_VAL(object_id), + ZSTR_VAL(source_path) + ) == SUCCESS + ); +} + +PHP_FUNCTION(king_object_store_backup_all_objects) +{ + zend_string *destination_path; + zval *options = NULL; + zval *mode_zv; + zval *base_snapshot_path_zv; + const char *mode = "full"; + const char *base_snapshot_path = NULL; + zend_bool incremental = 0; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_STR(destination_path) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + if (ZSTR_LEN(destination_path) == 0) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "Destination path must be a non-empty string." + ); + RETURN_THROWS(); + } + if (!king_object_store_runtime.initialized) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "Object-store registry is unavailable." + ); + RETURN_THROWS(); + } + if (options != NULL) { + mode_zv = zend_hash_str_find( + Z_ARRVAL_P(options), + "mode", + sizeof("mode") - 1 + ); + if (mode_zv != NULL) { + if (Z_TYPE_P(mode_zv) != IS_STRING || Z_STRLEN_P(mode_zv) == 0) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "king_object_store_backup_all_objects() option 'mode' must be a non-empty string when provided." + ); + RETURN_THROWS(); + } + + mode = Z_STRVAL_P(mode_zv); + } + + if (strcmp(mode, "full") == 0) { + incremental = 0; + } else if (strcmp(mode, "incremental") == 0) { + incremental = 1; + } else { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "king_object_store_backup_all_objects() option 'mode' must be either 'full' or 'incremental'." + ); + RETURN_THROWS(); + } + + base_snapshot_path_zv = zend_hash_str_find( + Z_ARRVAL_P(options), + "base_snapshot_path", + sizeof("base_snapshot_path") - 1 + ); + if (base_snapshot_path_zv != NULL) { + if (Z_TYPE_P(base_snapshot_path_zv) != IS_STRING || Z_STRLEN_P(base_snapshot_path_zv) == 0) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "king_object_store_backup_all_objects() option 'base_snapshot_path' must be a non-empty string when provided." + ); + RETURN_THROWS(); + } + + base_snapshot_path = Z_STRVAL_P(base_snapshot_path_zv); + } + } + if (incremental && base_snapshot_path == NULL) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "king_object_store_backup_all_objects() incremental mode requires option 'base_snapshot_path'." + ); + RETURN_THROWS(); + } + if (!incremental && base_snapshot_path != NULL) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "king_object_store_backup_all_objects() option 'base_snapshot_path' is only valid when mode='incremental'." + ); + RETURN_THROWS(); + } + + RETURN_BOOL( + king_object_store_backup_all_objects( + ZSTR_VAL(destination_path), + incremental, + base_snapshot_path + ) == SUCCESS + ); +} + +PHP_FUNCTION(king_object_store_restore_all_objects) +{ + zend_string *source_path; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(source_path) + ZEND_PARSE_PARAMETERS_END(); + + if (ZSTR_LEN(source_path) == 0) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "Source path must be a non-empty string." + ); + RETURN_THROWS(); + } + if (!king_object_store_runtime.initialized) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "Object-store registry is unavailable." + ); + RETURN_THROWS(); + } + + RETURN_BOOL( + king_object_store_restore_all_objects(ZSTR_VAL(source_path)) == SUCCESS + ); +} diff --git a/extension/src/core/introspection/prelude/object_store_runtime.inc b/extension/src/core/introspection/prelude/object_store_runtime.inc index beed829e4..17dbb0d7c 100644 --- a/extension/src/core/introspection/prelude/object_store_runtime.inc +++ b/extension/src/core/introspection/prelude/object_store_runtime.inc @@ -18,7 +18,6 @@ static zend_long king_cdn_cache_stale_serve_count = 0; static king_cdn_cache_entry *king_cdn_cache_registry_get_entry_allow_expired( zend_string *object_id ); -void king_cdn_sweep_expired(void); static void king_cdn_counter_add(zend_long *counter, zend_long delta) { diff --git a/extension/src/core/introspection/prelude/semantic_dns_runtime/lifecycle.inc b/extension/src/core/introspection/prelude/semantic_dns_runtime/lifecycle.inc index 0f58127fe..fd986b285 100644 --- a/extension/src/core/introspection/prelude/semantic_dns_runtime/lifecycle.inc +++ b/extension/src/core/introspection/prelude/semantic_dns_runtime/lifecycle.inc @@ -5,7 +5,7 @@ * surfaces reuse. */ -#include "include/semantic_dns/semantic_dns_internal.h" +#include "semantic_dns/semantic_dns_internal.h" static void king_semantic_dns_attribute_zval_dtor(zval *zv) { diff --git a/extension/src/core/introspection/prelude/types.inc b/extension/src/core/introspection/prelude/types.inc index f011d0fdc..95b552a05 100644 --- a/extension/src/core/introspection/prelude/types.inc +++ b/extension/src/core/introspection/prelude/types.inc @@ -14,15 +14,16 @@ #include #include -#include "include/king_globals.h" -#include "include/config/cloud_autoscale/base_layer.h" -#include "include/config/iibin/base_layer.h" -#include "include/config/mcp_and_orchestrator/base_layer.h" -#include "include/config/native_cdn/base_layer.h" -#include "include/config/native_object_store/base_layer.h" -#include "include/config/open_telemetry/base_layer.h" -#include "include/config/smart_dns/base_layer.h" -#include "include/iibin/iibin_internal.h" +#include "php_king/globals.h" +#include "config/cloud_autoscale/base_layer.h" +#include "config/iibin/base_layer.h" +#include "config/mcp_and_orchestrator/base_layer.h" +#include "config/native_cdn/base_layer.h" +#include "config/native_object_store/base_layer.h" +#include "config/open_telemetry/base_layer.h" +#include "config/smart_dns/base_layer.h" +#include "iibin/iibin_internal.h" +#include "object_store/object_store_internal.h" HashTable king_semantic_dns_service_registry; HashTable king_semantic_dns_mother_node_registry; diff --git a/extension/src/core/introspection/proto_registry/map.inc b/extension/src/core/introspection/proto_registry/map.inc index eb0f7f47a..0e04e3c4c 100644 --- a/extension/src/core/introspection/proto_registry/map.inc +++ b/extension/src/core/introspection/proto_registry/map.inc @@ -34,7 +34,7 @@ static bool king_proto_runtime_parse_map_input_key( ) { zend_long numeric_key = 0; - double dummy_double = 0.0; + double ignored_double = 0.0; switch (field->map_key_kind) { case KING_PROTO_RUNTIME_FIELD_STRING: @@ -58,7 +58,7 @@ static bool king_proto_runtime_parse_map_input_key( ZSTR_VAL(entry_key), ZSTR_LEN(entry_key), &numeric_key, - &dummy_double, + &ignored_double, false )) { zend_throw_exception_ex( @@ -96,7 +96,7 @@ static bool king_proto_runtime_parse_map_input_key( ZSTR_VAL(entry_key), ZSTR_LEN(entry_key), &numeric_key, - &dummy_double, + &ignored_double, false )) { zend_throw_exception_ex( @@ -133,7 +133,7 @@ static bool king_proto_runtime_parse_map_input_key( ZSTR_VAL(entry_key), ZSTR_LEN(entry_key), &numeric_key, - &dummy_double, + &ignored_double, false )) { zend_throw_exception_ex( diff --git a/extension/src/core/introspection/semantic_dns.inc b/extension/src/core/introspection/semantic_dns.inc index f97d8093f..488cbe77f 100644 --- a/extension/src/core/introspection/semantic_dns.inc +++ b/extension/src/core/introspection/semantic_dns.inc @@ -4,7 +4,7 @@ * logical fragment while the deeper helpers remain in the subdirectory. */ -#include "include/semantic_dns/semantic_dns_internal.h" +#include "semantic_dns/semantic_dns_internal.h" #include "semantic_dns/service_registry.inc" #include "semantic_dns/live_signals.inc" #include "semantic_dns/mother_nodes.inc" diff --git a/extension/src/core/introspection/semantic_dns/routing.inc b/extension/src/core/introspection/semantic_dns/routing.inc index d13d6db77..f5b1707e1 100644 --- a/extension/src/core/introspection/semantic_dns/routing.inc +++ b/extension/src/core/introspection/semantic_dns/routing.inc @@ -5,7 +5,7 @@ * bounded no-route payload expected by userland. */ -#include "include/semantic_dns/semantic_dns.h" +#include "semantic_dns/semantic_dns.h" PHP_FUNCTION(king_semantic_dns_get_optimal_route) { diff --git a/extension/src/core/introspection/semantic_dns/service_registry/register.inc b/extension/src/core/introspection/semantic_dns/service_registry/register.inc index 582b0eb70..5da42e3af 100644 --- a/extension/src/core/introspection/semantic_dns/service_registry/register.inc +++ b/extension/src/core/introspection/semantic_dns/service_registry/register.inc @@ -42,10 +42,21 @@ PHP_FUNCTION(king_semantic_dns_register_service) existing_service = king_semantic_dns_registry_get_service(service->service_id); if (existing_service != NULL && king_semantic_dns_service_equals(existing_service, service)) { + existing_service->last_health_check = service->last_health_check; king_semantic_dns_service_free(service); + if (use_state_transaction + && !king_semantic_dns_state_persist_locked_or_throw( + "king_semantic_dns_register_service" + )) { + king_semantic_dns_state_transaction_end(lock_fd); + RETURN_THROWS(); + } if (use_state_transaction) { king_semantic_dns_state_transaction_end(lock_fd); } + if (!king_semantic_dns_listener_snapshot_or_throw("king_semantic_dns_register_service")) { + RETURN_THROWS(); + } RETURN_TRUE; } diff --git a/extension/src/core/introspection/system.inc b/extension/src/core/introspection/system.inc index 5ea6cbd01..9082efc68 100644 --- a/extension/src/core/introspection/system.inc +++ b/extension/src/core/introspection/system.inc @@ -261,6 +261,8 @@ PHP_FUNCTION(king_system_get_component_info) king_safe_string(king_smart_dns_config.live_probe_allowed_hosts)); add_assoc_string(&configuration, "mothernode_uri", king_safe_string(king_smart_dns_config.mothernode_uri)); + add_assoc_string(&configuration, "state_path", + king_safe_string(king_smart_dns_config.state_path)); } else if (zend_string_equals_literal(component_name, "mcp")) { king_component_info_init(return_value, "mcp", "config_backed"); array_init(&configuration); diff --git a/extension/src/core/introspection/telemetry.inc b/extension/src/core/introspection/telemetry.inc index c8f6ece3a..0acb5168a 100644 --- a/extension/src/core/introspection/telemetry.inc +++ b/extension/src/core/introspection/telemetry.inc @@ -1,7 +1,7 @@ /* * Root telemetry introspection slice: exposes shared last-error helpers, the * live current-span snapshot, finalized outgoing trace-context injection, and - * the still-conservative userland extract placeholder. + * the still-conservative userland extraction boundary. */ PHP_FUNCTION(king_get_last_error) diff --git a/extension/src/php_king/db_ingestor.inc b/extension/src/db_ingest/api.inc similarity index 100% rename from extension/src/php_king/db_ingestor.inc rename to extension/src/db_ingest/api.inc diff --git a/extension/src/db_ingest/db_ingest.c b/extension/src/db_ingest/db_ingest.c new file mode 100644 index 000000000..d52ace25d --- /dev/null +++ b/extension/src/db_ingest/db_ingest.c @@ -0,0 +1,8 @@ +/* + * Database-ingest runtime binding. Owns the host-local serialized writer lane + * PHP entry point outside of the core php_king bootstrap translation unit. + */ +#include "php_king.h" +#include "db_ingest/db_ingest.h" + +#include "php_binding.inc" diff --git a/extension/src/db_ingest/function_entry.inc b/extension/src/db_ingest/function_entry.inc new file mode 100644 index 000000000..fca60b3b6 --- /dev/null +++ b/extension/src/db_ingest/function_entry.inc @@ -0,0 +1 @@ +#include "../../include/db_ingest/function_entries.h" diff --git a/extension/src/db_ingest/php_binding.inc b/extension/src/db_ingest/php_binding.inc new file mode 100644 index 000000000..a3d174165 --- /dev/null +++ b/extension/src/db_ingest/php_binding.inc @@ -0,0 +1,5 @@ +/* + * Database ingestion PHP binding aggregation. + */ + +#include "api.inc" diff --git a/extension/src/iibin/iibin.c b/extension/src/iibin/iibin.c index 78b82edf8..d5562ecef 100644 --- a/extension/src/iibin/iibin.c +++ b/extension/src/iibin/iibin.c @@ -4,7 +4,7 @@ */ #include "php.h" -#include "include/iibin/iibin_internal.h" +#include "iibin/iibin_internal.h" int king_iibin_minit(void) { diff --git a/extension/src/iibin/iibin_api.c b/extension/src/iibin/iibin_api.c index a48d78e8f..aee632c5d 100644 --- a/extension/src/iibin/iibin_api.c +++ b/extension/src/iibin/iibin_api.c @@ -1,74 +1,11 @@ /* * Public IIBIN facade bindings. Declares the King\IIBIN static method table - * and its arginfo so the OO surface maps directly onto the underlying - * king_proto_* procedural entry points. + * so the OO surface maps directly onto the underlying king_proto_* procedural + * entry points. Arginfo lives in the module binding fragment. */ #include "php.h" -#include "include/iibin/iibin_internal.h" +#include "iibin/iibin.h" +#include "iibin/iibin_internal.h" -PHP_FUNCTION(king_proto_define_enum); -PHP_FUNCTION(king_proto_define_schema); -PHP_FUNCTION(king_proto_encode); -PHP_FUNCTION(king_proto_encode_batch); -PHP_FUNCTION(king_proto_decode); -PHP_FUNCTION(king_proto_decode_batch); -PHP_FUNCTION(king_proto_is_defined); -PHP_FUNCTION(king_proto_is_schema_defined); -PHP_FUNCTION(king_proto_is_enum_defined); -PHP_FUNCTION(king_proto_get_defined_schemas); -PHP_FUNCTION(king_proto_get_defined_enums); - -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_IIBIN_defineEnum, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, values, IS_ARRAY, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_IIBIN_defineSchema, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, fields, IS_ARRAY, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_IIBIN_encode, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, schema, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, data, IS_MIXED, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_IIBIN_encodeBatch, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, schema, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, records, IS_ARRAY, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_IIBIN_decode, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, schema, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, data, IS_STRING, 0) - ZEND_ARG_TYPE_MASK(0, decodeAsObject, MAY_BE_BOOL|MAY_BE_STRING|MAY_BE_ARRAY, "false") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_IIBIN_decodeBatch, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, schema, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, records, IS_ARRAY, 0) - ZEND_ARG_TYPE_MASK(0, decodeAsObject, MAY_BE_BOOL|MAY_BE_STRING|MAY_BE_ARRAY, "false") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_IIBIN_name, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_IIBIN_no_args, 0, 0, 0) -ZEND_END_ARG_INFO() - -const zend_function_entry king_iibin_class_methods[] = { - ZEND_ME_MAPPING(defineEnum, king_proto_define_enum, arginfo_class_King_IIBIN_defineEnum, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(defineSchema, king_proto_define_schema, arginfo_class_King_IIBIN_defineSchema, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(encode, king_proto_encode, arginfo_class_King_IIBIN_encode, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(encodeBatch, king_proto_encode_batch, arginfo_class_King_IIBIN_encodeBatch, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(decode, king_proto_decode, arginfo_class_King_IIBIN_decode, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(decodeBatch, king_proto_decode_batch, arginfo_class_King_IIBIN_decodeBatch, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(isDefined, king_proto_is_defined, arginfo_class_King_IIBIN_name, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(isSchemaDefined, king_proto_is_schema_defined, arginfo_class_King_IIBIN_name, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(isEnumDefined, king_proto_is_enum_defined, arginfo_class_King_IIBIN_name, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(getDefinedSchemas, king_proto_get_defined_schemas, arginfo_class_King_IIBIN_no_args, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(getDefinedEnums, king_proto_get_defined_enums, arginfo_class_King_IIBIN_no_args, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - PHP_FE_END -}; +#include "iibin/class_method_entries.h" diff --git a/extension/src/iibin/iibin_decoding.c b/extension/src/iibin/iibin_decoding.c index 3c7db339a..575f6cdd4 100644 --- a/extension/src/iibin/iibin_decoding.c +++ b/extension/src/iibin/iibin_decoding.c @@ -5,7 +5,7 @@ */ #include "php_king.h" -#include "include/iibin/iibin_internal.h" +#include "iibin/iibin_internal.h" static const char * const king_proto_primitive_types[] = { "double", diff --git a/extension/src/iibin/iibin_encoding.c b/extension/src/iibin/iibin_encoding.c index added8234..35ed577b4 100644 --- a/extension/src/iibin/iibin_encoding.c +++ b/extension/src/iibin/iibin_encoding.c @@ -5,7 +5,7 @@ */ #include "php_king.h" -#include "include/iibin/iibin_internal.h" +#include "iibin/iibin_internal.h" static const char * const king_proto_primitive_types[] = { "double", diff --git a/extension/src/iibin/iibin_registry.c b/extension/src/iibin/iibin_registry.c index 4f07ed233..6b9ba5115 100644 --- a/extension/src/iibin/iibin_registry.c +++ b/extension/src/iibin/iibin_registry.c @@ -5,7 +5,7 @@ */ #include "php_king.h" -#include "include/iibin/iibin_internal.h" +#include "iibin/iibin_internal.h" HashTable king_proto_schema_registry; HashTable king_proto_enum_registry; diff --git a/extension/src/iibin/iibin_schema.c b/extension/src/iibin/iibin_schema.c index 023b157f6..cd49b6e3c 100644 --- a/extension/src/iibin/iibin_schema.c +++ b/extension/src/iibin/iibin_schema.c @@ -5,7 +5,7 @@ */ #include "php_king.h" -#include "include/iibin/iibin_internal.h" +#include "iibin/iibin_internal.h" zend_result king_iibin_define_schema( zend_string *schema_name, diff --git a/extension/src/iibin/iibin_schema_compiler.c b/extension/src/iibin/iibin_schema_compiler.c index fc46982e8..7e81201b3 100644 --- a/extension/src/iibin/iibin_schema_compiler.c +++ b/extension/src/iibin/iibin_schema_compiler.c @@ -5,7 +5,7 @@ */ #include "php_king.h" -#include "include/iibin/iibin_internal.h" +#include "iibin/iibin_internal.h" static const char * const king_proto_primitive_types[] = { "double", diff --git a/extension/src/inference/api/async/api_async.inc b/extension/src/inference/api/async/api_async.inc new file mode 100644 index 000000000..1bae0ddd0 --- /dev/null +++ b/extension/src/inference/api/async/api_async.inc @@ -0,0 +1,90 @@ +/* + * Inference PHP entry points and OO method glue. + */ + +static zend_result king_inference_next_async_runner( + king_awaitable_object *intern, + zend_long timeout_ms, + zval *result) +{ + zval *stream_object; + zval *timeout; + zval event; + zend_long read_timeout_ms = timeout_ms > 0 ? timeout_ms : 0; + king_inference_stream_object *stream; + zend_result status; + + if (Z_TYPE(intern->payload) != IS_ARRAY) { + king_set_error("King inference async stream reader has an invalid native payload."); + return FAILURE; + } + + stream_object = zend_hash_str_find( + Z_ARRVAL(intern->payload), + "stream", + sizeof("stream") - 1 + ); + timeout = zend_hash_str_find( + Z_ARRVAL(intern->payload), + "timeout_ms", + sizeof("timeout_ms") - 1 + ); + if (stream_object == NULL + || Z_TYPE_P(stream_object) != IS_OBJECT + || !instanceof_function(Z_OBJCE_P(stream_object), king_ce_inference_stream)) { + king_set_error("King inference async stream reader requires a valid stream."); + return FAILURE; + } + if (timeout != NULL && Z_TYPE_P(timeout) == IS_LONG) { + read_timeout_ms = Z_LVAL_P(timeout); + } + + stream = php_king_inference_stream_obj_from_zend(Z_OBJ_P(stream_object)); + ZVAL_UNDEF(&event); + status = king_inference_stream_next_event(stream, read_timeout_ms, &event); + if (status != SUCCESS) { + if (!Z_ISUNDEF(event)) { + zval_ptr_dtor(&event); + } + return FAILURE; + } + + if (Z_TYPE(event) == IS_NULL && !stream->done) { + zval_ptr_dtor(&event); + ZVAL_UNDEF(result); + return SUCCESS; + } + + ZVAL_COPY_VALUE(result, &event); + return SUCCESS; +} + +static zend_result king_inference_next_async_create( + zval *return_value, + zval *stream_object, + zend_long timeout_ms, + const char *operation, + size_t operation_len) +{ + zval payload; + zval stream_copy; + zend_result status; + + array_init(&payload); + ZVAL_COPY(&stream_copy, stream_object); + add_assoc_zval(&payload, "stream", &stream_copy); + add_assoc_long(&payload, "timeout_ms", timeout_ms); + + status = king_awaitable_create( + return_value, + operation, + operation_len, + king_inference_next_async_runner, + &payload, + NULL + ); + zval_ptr_dtor(&payload); + + return status; +} + diff --git a/extension/src/inference/api/oop/model/api_model_methods.inc b/extension/src/inference/api/oop/model/api_model_methods.inc new file mode 100644 index 000000000..2e27cb771 --- /dev/null +++ b/extension/src/inference/api/oop/model/api_model_methods.inc @@ -0,0 +1,204 @@ +PHP_METHOD(King_Inference_Model, __construct) +{ + zval *config; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY(config) + ZEND_PARSE_PARAMETERS_END(); + + if (king_inference_model_init(ZEND_THIS, config, "King\\Inference\\Model::__construct") != SUCCESS) { + RETURN_THROWS(); + } +} + +PHP_METHOD(King_Inference_Model, info) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + king_inference_model_info_array( + php_king_inference_model_obj_from_zend(Z_OBJ_P(ZEND_THIS)), + return_value + ); +} + +PHP_METHOD(King_Inference_Model, tokenize) +{ + zend_string *text; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(text) + ZEND_PARSE_PARAMETERS_END(); + + if (king_inference_tokenizer_encode( + php_king_inference_model_obj_from_zend(Z_OBJ_P(ZEND_THIS)), + text, + return_value + ) != SUCCESS) { + RETURN_THROWS(); + } +} + +PHP_METHOD(King_Inference_Model, tokenDecode) +{ + zend_long token_id; + zend_string *decoded; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_LONG(token_id) + ZEND_PARSE_PARAMETERS_END(); + + if (token_id < 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token id must be non-negative."); + RETURN_THROWS(); + } + + decoded = king_inference_tokenizer_decode_token( + php_king_inference_model_obj_from_zend(Z_OBJ_P(ZEND_THIS)), + (zend_ulong) token_id + ); + if (decoded == NULL) { + RETURN_THROWS(); + } + RETURN_STR(decoded); +} + +PHP_METHOD(King_Inference_Model, tokenDecodeGraph) +{ + zval *token, *options = NULL; + zend_long position; + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_ZVAL(token) + Z_PARAM_LONG(position) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + if (position < 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph position must be non-negative."); + RETURN_THROWS(); + } + if (king_inference_token_decode_graph_array( + php_king_inference_model_obj_from_zend(Z_OBJ_P(ZEND_THIS)), + token, + (zend_ulong) position, + options, + return_value + ) != SUCCESS) { + RETURN_THROWS(); + } +} + +PHP_METHOD(King_Inference_Model, kvCachePlan) +{ + zval *request = NULL; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(request) + ZEND_PARSE_PARAMETERS_END(); + + king_inference_paged_kv_cache_plan( + php_king_inference_model_obj_from_zend(Z_OBJ_P(ZEND_THIS)), + request, + return_value + ); +} + +PHP_METHOD(King_Inference_Model, tensorView) +{ + zend_string *name; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(name) + ZEND_PARSE_PARAMETERS_END(); + + if (king_inference_tensor_view_array( + php_king_inference_model_obj_from_zend(Z_OBJ_P(ZEND_THIS)), + name, + return_value + ) != SUCCESS) { + RETURN_THROWS(); + } +} + +PHP_METHOD(King_Inference_Model, tensorIndex) +{ + zval *options = NULL; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + king_inference_tensor_index_array( + php_king_inference_model_obj_from_zend(Z_OBJ_P(ZEND_THIS)), + options, + return_value + ); +} + +PHP_METHOD(King_Inference_Model, tensorDequantize) +{ + zend_string *name; + zval *options = NULL; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_STR(name) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + if (king_inference_tensor_dequantize_array( + php_king_inference_model_obj_from_zend(Z_OBJ_P(ZEND_THIS)), + name, + options, + return_value + ) != SUCCESS) { + RETURN_THROWS(); + } +} + +PHP_METHOD(King_Inference_Model, tensorMatmul) +{ + zend_string *name; + zval *input; + zval *options = NULL; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STR(name) + Z_PARAM_ARRAY(input) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + if (king_inference_tensor_matmul_array( + php_king_inference_model_obj_from_zend(Z_OBJ_P(ZEND_THIS)), + name, + input, + options, + return_value + ) != SUCCESS) { + RETURN_THROWS(); + } +} + +PHP_METHOD(King_Inference_Model, graphRun) +{ + zval *graph; + zval *options = NULL; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_ARRAY(graph) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + if (king_inference_graph_run_array( + php_king_inference_model_obj_from_zend(Z_OBJ_P(ZEND_THIS)), + graph, + options, + return_value + ) != SUCCESS) { + RETURN_THROWS(); + } +} + diff --git a/extension/src/inference/api/oop/stream/api_stream_methods.inc b/extension/src/inference/api/oop/stream/api_stream_methods.inc new file mode 100644 index 000000000..e7740a2fe --- /dev/null +++ b/extension/src/inference/api/oop/stream/api_stream_methods.inc @@ -0,0 +1,171 @@ +PHP_METHOD(King_Inference_Stream, __construct) +{ + zval *model; + zval *request; + zval *options = NULL; + zval empty_options; + zval *validation_options; + king_inference_stream_object *stream; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_OBJECT_OF_CLASS(model, king_ce_inference_model) + Z_PARAM_ARRAY(request) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + if (options != NULL) { + validation_options = options; + } else { + ZVAL_UNDEF(&empty_options); + array_init(&empty_options); + validation_options = &empty_options; + } + if (king_inference_stream_validate_request_options( + request, + validation_options, + "King\\Inference\\Stream::__construct" + ) != SUCCESS) { + if (options == NULL) { + zval_ptr_dtor(&empty_options); + } + RETURN_THROWS(); + } + if (options == NULL) { + zval_ptr_dtor(&empty_options); + } + + stream = php_king_inference_stream_obj_from_zend(Z_OBJ_P(ZEND_THIS)); + king_inference_stream_release_owned_state(stream); + ZVAL_COPY(&stream->model, model); + ZVAL_COPY(&stream->request, request); + if (options != NULL) { + ZVAL_COPY(&stream->options, options); + } else { + array_init(&stream->options); + } + stream->start_event_pending = true; + stream->done = false; + stream->cancelled = false; + stream->timed_out = false; + stream->exit_code = -1; + stream->chunk_count = 0; + stream->stderr_count = 0; + stream->bytes_emitted = 0; + king_inference_stream_reset_decoder_metrics(stream); + stream->native_event_index = 0; + stream->gpu_thermal_preflight_at = 0; + stream->gpu_thermal_last_check_at = 0; + stream->gpu_thermal_abort_at = 0; + stream->gpu_power_preflight_at = 0; + stream->gpu_power_last_check_at = 0; + stream->gpu_power_abort_at = 0; + stream->gpu_policy_during_run_checks = 0; + stream->gpu_policy_interval_skips = 0; + stream->gpu_thermal_runtime_checks = 0; + stream->gpu_power_runtime_checks = 0; + stream->gpu_thermal_check_interval_seconds = 0; + stream->gpu_power_check_interval_seconds = 0; + stream->gpu_thermal_preflight_temperature_c = 0.0; + stream->gpu_thermal_abort_temperature_c = 0.0; + stream->gpu_thermal_abort_ceiling_c = 0.0; + stream->gpu_power_preflight_watts = 0.0; + stream->gpu_power_abort_watts = 0.0; + stream->gpu_power_abort_ceiling_watts = 0.0; + stream->gpu_thermal_preflight_checked = false; + stream->gpu_thermal_preflight_temperature_available = false; + stream->gpu_thermal_aborted = false; + stream->gpu_power_preflight_checked = false; + stream->gpu_power_preflight_available = false; + stream->gpu_power_aborted = false; + stream->openai_compatible = king_inference_stream_openai_requested(&stream->request, &stream->options); + stream->created_at = (zend_long) time(NULL); + if (stream->openai_compatible) { + king_inference_stream_prepare_openai(stream); + } else if (stream->response_id != NULL) { + zend_string_release(stream->response_id); + stream->response_id = NULL; + } + + if (king_inference_stream_start(stream, "King\\Inference\\Stream::__construct") != SUCCESS) { + king_inference_stream_throw_start_failure(stream, "King\\Inference\\Stream::__construct"); + RETURN_THROWS(); + } +} + +PHP_METHOD(King_Inference_Stream, next) +{ + zend_long timeout_ms = 0; + bool timeout_is_null = true; + king_inference_stream_object *stream; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_LONG_OR_NULL(timeout_ms, timeout_is_null) + ZEND_PARSE_PARAMETERS_END(); + + if (timeout_is_null) { + timeout_ms = 0; + } + + stream = php_king_inference_stream_obj_from_zend(Z_OBJ_P(ZEND_THIS)); + if (king_inference_stream_next_event(stream, timeout_ms, return_value) != SUCCESS) { + RETURN_THROWS(); + } +} + +PHP_METHOD(King_Inference_Stream, nextAsync) +{ + zend_long timeout_ms = 0; + bool timeout_is_null = true; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_LONG_OR_NULL(timeout_ms, timeout_is_null) + ZEND_PARSE_PARAMETERS_END(); + + if (timeout_is_null) { + timeout_ms = 0; + } + + if (king_inference_next_async_create( + return_value, + ZEND_THIS, + timeout_ms, + "King\\Inference\\Stream::nextAsync", + sizeof("King\\Inference\\Stream::nextAsync") - 1 + ) != SUCCESS) { + RETURN_THROWS(); + } +} + +PHP_METHOD(King_Inference_Stream, cancel) +{ + king_inference_stream_object *stream; + + ZEND_PARSE_PARAMETERS_NONE(); + + stream = php_king_inference_stream_obj_from_zend(Z_OBJ_P(ZEND_THIS)); + king_inference_stream_cancel_state(stream); + RETURN_TRUE; +} + +PHP_METHOD(King_Inference_Stream, isDone) +{ + king_inference_stream_object *stream; + + ZEND_PARSE_PARAMETERS_NONE(); + + stream = php_king_inference_stream_obj_from_zend(Z_OBJ_P(ZEND_THIS)); + RETURN_BOOL(stream->done); +} + +PHP_METHOD(King_Inference_Stream, getMetrics) +{ + king_inference_stream_object *stream; + + ZEND_PARSE_PARAMETERS_NONE(); + + stream = php_king_inference_stream_obj_from_zend(Z_OBJ_P(ZEND_THIS)); + king_inference_stream_metrics(stream, return_value); +} diff --git a/extension/src/inference/api/procedural/api_functions.inc b/extension/src/inference/api/procedural/api_functions.inc new file mode 100644 index 000000000..34a5b6d03 --- /dev/null +++ b/extension/src/inference/api/procedural/api_functions.inc @@ -0,0 +1,369 @@ +PHP_FUNCTION(king_inference_model_load) +{ + zval *config; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY(config) + ZEND_PARSE_PARAMETERS_END(); + + object_init_ex(return_value, king_ce_inference_model); + if (king_inference_model_init(return_value, config, "king_inference_model_load") != SUCCESS) { + zval_ptr_dtor(return_value); + RETURN_THROWS(); + } +} + +PHP_FUNCTION(king_inference_model_info) +{ + zval *model; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(model, king_ce_inference_model) + ZEND_PARSE_PARAMETERS_END(); + + king_inference_model_info_array(php_king_inference_model_obj_from_zend(Z_OBJ_P(model)), return_value); +} + +PHP_FUNCTION(king_inference_tokenize) +{ + zval *model; + zend_string *text; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT_OF_CLASS(model, king_ce_inference_model) + Z_PARAM_STR(text) + ZEND_PARSE_PARAMETERS_END(); + + if (king_inference_tokenizer_encode( + php_king_inference_model_obj_from_zend(Z_OBJ_P(model)), + text, + return_value + ) != SUCCESS) { + RETURN_THROWS(); + } +} + +PHP_FUNCTION(king_inference_token_decode) +{ + zval *model; + zend_long token_id; + zend_string *decoded; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT_OF_CLASS(model, king_ce_inference_model) + Z_PARAM_LONG(token_id) + ZEND_PARSE_PARAMETERS_END(); + + if (token_id < 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token id must be non-negative."); + RETURN_THROWS(); + } + + decoded = king_inference_tokenizer_decode_token( + php_king_inference_model_obj_from_zend(Z_OBJ_P(model)), + (zend_ulong) token_id + ); + if (decoded == NULL) { + RETURN_THROWS(); + } + RETURN_STR(decoded); +} + +PHP_FUNCTION(king_inference_token_decode_graph) +{ + zval *model, *token, *options = NULL; + zend_long position; + ZEND_PARSE_PARAMETERS_START(3, 4) + Z_PARAM_OBJECT_OF_CLASS(model, king_ce_inference_model) + Z_PARAM_ZVAL(token) + Z_PARAM_LONG(position) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + if (position < 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph position must be non-negative."); + RETURN_THROWS(); + } + if (king_inference_token_decode_graph_array( + php_king_inference_model_obj_from_zend(Z_OBJ_P(model)), + token, + (zend_ulong) position, + options, + return_value + ) != SUCCESS) { + RETURN_THROWS(); + } +} + +PHP_FUNCTION(king_inference_kv_cache_plan) +{ + zval *model; + zval *request = NULL; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_OBJECT_OF_CLASS(model, king_ce_inference_model) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(request) + ZEND_PARSE_PARAMETERS_END(); + + king_inference_paged_kv_cache_plan( + php_king_inference_model_obj_from_zend(Z_OBJ_P(model)), + request, + return_value + ); +} + +PHP_FUNCTION(king_inference_tensor_view) +{ + zval *model; + zend_string *name; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT_OF_CLASS(model, king_ce_inference_model) + Z_PARAM_STR(name) + ZEND_PARSE_PARAMETERS_END(); + + if (king_inference_tensor_view_array( + php_king_inference_model_obj_from_zend(Z_OBJ_P(model)), + name, + return_value + ) != SUCCESS) { + RETURN_THROWS(); + } +} + +PHP_FUNCTION(king_inference_tensor_index) +{ + zval *model; + zval *options = NULL; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_OBJECT_OF_CLASS(model, king_ce_inference_model) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + king_inference_tensor_index_array( + php_king_inference_model_obj_from_zend(Z_OBJ_P(model)), + options, + return_value + ); +} + +PHP_FUNCTION(king_inference_tensor_dequantize) +{ + zval *model; + zend_string *name; + zval *options = NULL; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_OBJECT_OF_CLASS(model, king_ce_inference_model) + Z_PARAM_STR(name) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + if (king_inference_tensor_dequantize_array( + php_king_inference_model_obj_from_zend(Z_OBJ_P(model)), + name, + options, + return_value + ) != SUCCESS) { + RETURN_THROWS(); + } +} + +PHP_FUNCTION(king_inference_tensor_matmul) +{ + zval *model; + zend_string *name; + zval *input; + zval *options = NULL; + + ZEND_PARSE_PARAMETERS_START(3, 4) + Z_PARAM_OBJECT_OF_CLASS(model, king_ce_inference_model) + Z_PARAM_STR(name) + Z_PARAM_ARRAY(input) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + if (king_inference_tensor_matmul_array( + php_king_inference_model_obj_from_zend(Z_OBJ_P(model)), + name, + input, + options, + return_value + ) != SUCCESS) { + RETURN_THROWS(); + } +} + +PHP_FUNCTION(king_inference_graph_run) +{ + zval *model; + zval *graph; + zval *options = NULL; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_OBJECT_OF_CLASS(model, king_ce_inference_model) + Z_PARAM_ARRAY(graph) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + if (king_inference_graph_run_array( + php_king_inference_model_obj_from_zend(Z_OBJ_P(model)), + graph, + options, + return_value + ) != SUCCESS) { + RETURN_THROWS(); + } +} + +PHP_FUNCTION(king_inference_stream) +{ + zval *model; + zval *request; + zval *options = NULL; + king_inference_stream_object *stream; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_OBJECT_OF_CLASS(model, king_ce_inference_model) + Z_PARAM_ARRAY(request) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + object_init_ex(return_value, king_ce_inference_stream); + stream = php_king_inference_stream_obj_from_zend(Z_OBJ_P(return_value)); + ZVAL_COPY(&stream->model, model); + ZVAL_COPY(&stream->request, request); + if (options != NULL) { + ZVAL_COPY(&stream->options, options); + } else { + array_init(&stream->options); + } + if (king_inference_stream_validate_request_options( + &stream->request, + &stream->options, + "king_inference_stream" + ) != SUCCESS) { + zval_ptr_dtor(return_value); + ZVAL_UNDEF(return_value); + RETURN_THROWS(); + } + stream->openai_compatible = king_inference_stream_openai_requested(&stream->request, &stream->options); + stream->created_at = (zend_long) time(NULL); + stream->native_event_index = 0; + if (stream->openai_compatible) { + king_inference_stream_prepare_openai(stream); + } + + if (king_inference_stream_start(stream, "king_inference_stream") != SUCCESS) { + king_inference_stream_throw_start_failure(stream, "king_inference_stream"); + zval_ptr_dtor(return_value); + ZVAL_UNDEF(return_value); + RETURN_THROWS(); + } +} + +PHP_FUNCTION(king_inference_openai_chat_stream) +{ + zval *model; + zval *payload; + zval *options = NULL; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_OBJECT_OF_CLASS(model, king_ce_inference_model) + Z_PARAM_ARRAY(payload) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + if (king_inference_openai_create_stream( + return_value, + model, + payload, + options, + "king_inference_openai_chat_stream" + ) != SUCCESS) { + if (EG(exception)) { + RETURN_THROWS(); + } + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "%s", + king_get_error()[0] != '\0' + ? king_get_error() + : "OpenAI-compatible inference stream failed." + ); + RETURN_THROWS(); + } +} + +PHP_FUNCTION(king_inference_next) +{ + zval *stream_object; + zend_long timeout_ms = 0; + bool timeout_is_null = true; + king_inference_stream_object *stream; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_OBJECT_OF_CLASS(stream_object, king_ce_inference_stream) + Z_PARAM_OPTIONAL + Z_PARAM_LONG_OR_NULL(timeout_ms, timeout_is_null) + ZEND_PARSE_PARAMETERS_END(); + + if (timeout_is_null) { + timeout_ms = 0; + } + + stream = php_king_inference_stream_obj_from_zend(Z_OBJ_P(stream_object)); + if (king_inference_stream_next_event(stream, timeout_ms, return_value) != SUCCESS) { + RETURN_THROWS(); + } +} + +PHP_FUNCTION(king_inference_next_async) +{ + zval *stream_object; + zend_long timeout_ms = 0; + bool timeout_is_null = true; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_OBJECT_OF_CLASS(stream_object, king_ce_inference_stream) + Z_PARAM_OPTIONAL + Z_PARAM_LONG_OR_NULL(timeout_ms, timeout_is_null) + ZEND_PARSE_PARAMETERS_END(); + + if (timeout_is_null) { + timeout_ms = 0; + } + + if (king_inference_next_async_create( + return_value, + stream_object, + timeout_ms, + "king_inference_next_async", + sizeof("king_inference_next_async") - 1 + ) != SUCCESS) { + RETURN_THROWS(); + } +} + +PHP_FUNCTION(king_inference_cancel) +{ + zval *stream_object; + king_inference_stream_object *stream; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(stream_object, king_ce_inference_stream) + ZEND_PARSE_PARAMETERS_END(); + + stream = php_king_inference_stream_obj_from_zend(Z_OBJ_P(stream_object)); + king_inference_stream_cancel_state(stream); + RETURN_TRUE; +} diff --git a/extension/src/inference/api/surface/api.inc b/extension/src/inference/api/surface/api.inc new file mode 100644 index 000000000..e93ce218d --- /dev/null +++ b/extension/src/inference/api/surface/api.inc @@ -0,0 +1,8 @@ +/* + * Inference PHP entry points and OO method glue. + */ + +#include "../async/api_async.inc" +#include "../procedural/api_functions.inc" +#include "../oop/model/api_model_methods.inc" +#include "../oop/stream/api_stream_methods.inc" diff --git a/extension/src/inference/backends/contracts/backend_contract.inc b/extension/src/inference/backends/contracts/backend_contract.inc new file mode 100644 index 000000000..20bef70ca --- /dev/null +++ b/extension/src/inference/backends/contracts/backend_contract.inc @@ -0,0 +1,320 @@ +typedef enum _king_inference_backend_kind { + KING_INFERENCE_BACKEND_LOCAL = 1, + KING_INFERENCE_BACKEND_KING_NATIVE_CPU = 2, + KING_INFERENCE_BACKEND_KING_NATIVE_GPU = 3 +} king_inference_backend_kind; + +static const char *king_inference_backend_kind_name(king_inference_backend_kind kind) +{ + switch (kind) { + case KING_INFERENCE_BACKEND_LOCAL: + return "local"; + case KING_INFERENCE_BACKEND_KING_NATIVE_CPU: + return "king_native_cpu"; + case KING_INFERENCE_BACKEND_KING_NATIVE_GPU: + return "king_native_gpu"; + } + + return "unknown"; +} + +static zend_result king_inference_backend_kind_from_string( + zend_string *name, + king_inference_backend_kind *kind +) { + if (zend_string_equals_literal(name, "local")) { + *kind = KING_INFERENCE_BACKEND_LOCAL; + return SUCCESS; + } + + if (zend_string_equals_literal(name, "king_native_cpu")) { + *kind = KING_INFERENCE_BACKEND_KING_NATIVE_CPU; + return SUCCESS; + } + + if (zend_string_equals_literal(name, "king_native_gpu")) { + *kind = KING_INFERENCE_BACKEND_KING_NATIVE_GPU; + return SUCCESS; + } + + return FAILURE; +} + +static zval *king_inference_backend_config_value(zval *config) +{ + zval *backend = king_inference_array_find(config, "backend"); + + if (backend == NULL) { + return NULL; + } + + if (Z_TYPE_P(backend) == IS_STRING) { + return backend; + } + + if (Z_TYPE_P(backend) == IS_ARRAY) { + zval *name = king_inference_array_find(backend, "name"); + if (name != NULL) { + return name; + } + return king_inference_array_find(backend, "type"); + } + + return backend; +} + +static bool king_inference_backend_config_implies_local_runner(zval *config) +{ + zval *backend = king_inference_array_find(config, "backend"); + + return backend != NULL + && Z_TYPE_P(backend) == IS_ARRAY + && (king_inference_array_find(backend, "runner") != NULL + || king_inference_array_find(backend, "runner_path") != NULL); +} + +static zend_result king_inference_backend_kind_from_config( + zval *config, + king_inference_backend_kind *kind, + const char *function_name +) { + zval *backend_name = king_inference_backend_config_value(config); + + *kind = KING_INFERENCE_BACKEND_KING_NATIVE_CPU; + + if (backend_name == NULL) { + if (king_inference_backend_config_implies_local_runner(config)) { + *kind = KING_INFERENCE_BACKEND_LOCAL; + } + return SUCCESS; + } + + if (Z_TYPE_P(backend_name) != IS_STRING || Z_STRLEN_P(backend_name) == 0) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() backend name must be a non-empty string when configured.", + function_name + ); + return FAILURE; + } + + if (king_inference_backend_kind_from_string(Z_STR_P(backend_name), kind) != SUCCESS) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() does not support inference backend '%s'.", + function_name, + Z_STRVAL_P(backend_name) + ); + return FAILURE; + } + + return SUCCESS; +} + +static bool king_inference_backend_kind_is_available(king_inference_backend_kind kind) +{ + return kind == KING_INFERENCE_BACKEND_LOCAL + || kind == KING_INFERENCE_BACKEND_KING_NATIVE_CPU + || kind == KING_INFERENCE_BACKEND_KING_NATIVE_GPU; +} + +static bool king_inference_backend_kind_allows_model_registration(king_inference_backend_kind kind) +{ + return kind == KING_INFERENCE_BACKEND_LOCAL + || kind == KING_INFERENCE_BACKEND_KING_NATIVE_CPU + || kind == KING_INFERENCE_BACKEND_KING_NATIVE_GPU; +} + +static void king_inference_reference_backend_operation_list(zval *return_value) +{ + static const char *operations[] = { + "embedding", + "rms_norm", + "qkv_projection", + "rope", + "attention_score", + "attention_softmax", + "attention_value", + "attention_projection_residual", + "ffn_gate_up", + "ffn_swiglu", + "ffn_down", + "ffn_output_residual", + "final_norm", + "logits_projection", + "sampler_candidate_ranking", + }; + + array_init(return_value); + for (size_t i = 0; i < sizeof(operations) / sizeof(operations[0]); i++) { + add_next_index_string(return_value, operations[i]); + } +} + +static void king_inference_reference_backend_gap_list(zval *return_value) +{ + static const char *gaps[] = { + "full_unbounded_tensor_compare", + "batch_prefill_kv_state", + "whole_model_golden_prompt_semantics", + "long_context_quality", + "performance_baseline", + "external_backend_equivalence", + "openai_tool_execution", + }; + + array_init(return_value); + for (size_t i = 0; i < sizeof(gaps) / sizeof(gaps[0]); i++) { + add_next_index_string(return_value, gaps[i]); + } +} + +static void king_inference_reference_backend_contract_array( + king_inference_backend_kind kind, + zval *return_value +) { + bool gpu_backend = kind == KING_INFERENCE_BACKEND_KING_NATIVE_GPU; + zval compared_operations; + zval not_compared; + + array_init(return_value); + add_assoc_long(return_value, "schema_version", 1); + add_assoc_bool(return_value, "available", gpu_backend); + add_assoc_bool(return_value, "public_api", false); + add_assoc_bool(return_value, "active_runtime_backend", false); + add_assoc_bool(return_value, "production_execution_path", false); + add_assoc_bool(return_value, "requires_explicit_numeric_compare", true); + add_assoc_string(return_value, "scope", "internal_numeric_reference_contract"); + add_assoc_string( + return_value, + "selected_reference", + gpu_backend ? "king_internal_cpu_reference" : "none" + ); + add_assoc_string( + return_value, + "comparison_path", + gpu_backend ? "bounded_cuda_readback_against_cpu_recompute" : "unavailable" + ); + add_assoc_string( + return_value, + "activation", + "gpu.debug.numeric_compare_enabled or king.inference_cuda_numeric_compare_enable" + ); + king_inference_reference_backend_operation_list(&compared_operations); + add_assoc_zval(return_value, "compared_operations", &compared_operations); + king_inference_reference_backend_gap_list(¬_compared); + add_assoc_zval(return_value, "not_compared", ¬_compared); +} + +static void king_inference_backend_capabilities_array(king_inference_backend_kind kind, zval *return_value) +{ + bool native_backend = kind != KING_INFERENCE_BACKEND_LOCAL; + bool gpu_backend = kind == KING_INFERENCE_BACKEND_KING_NATIVE_GPU; + bool native_cpu_backend = kind == KING_INFERENCE_BACKEND_KING_NATIVE_CPU; + bool native_stream_backend = kind == KING_INFERENCE_BACKEND_KING_NATIVE_CPU + || kind == KING_INFERENCE_BACKEND_KING_NATIVE_GPU; + bool local_backend = kind == KING_INFERENCE_BACKEND_LOCAL; + zval reference_backend; + + array_init(return_value); + add_assoc_long(return_value, "schema_version", 2); + add_assoc_string(return_value, "scope", "implemented_capability_contract"); + add_assoc_string(return_value, "truth_source", "compiled_backend_contract"); + add_assoc_bool(return_value, "runtime_admission", false); + add_assoc_string(return_value, "runtime_admission_source", "x_king.runtime_surface"); + add_assoc_bool(return_value, "implemented", king_inference_backend_kind_is_available(kind)); + add_assoc_bool(return_value, "local", true); + add_assoc_bool(return_value, "streaming", true); + add_assoc_bool(return_value, "openai_chat_completions_stream", local_backend || native_cpu_backend || gpu_backend); + add_assoc_bool(return_value, "cancellable", true); + add_assoc_bool(return_value, "model_registration", king_inference_backend_kind_allows_model_registration(kind)); + add_assoc_bool(return_value, "model_metadata", true); + add_assoc_bool(return_value, "gguf_metadata", true); + add_assoc_bool(return_value, "gguf_architecture_classification", true); + add_assoc_bool(return_value, "process_runner", local_backend); + add_assoc_bool(return_value, "native_runtime", native_backend); + add_assoc_bool(return_value, "native_model_loader", true); + add_assoc_bool(return_value, "native_tensor_directory", true); + add_assoc_bool(return_value, "native_tensor_views", true); + add_assoc_bool(return_value, "native_model_mapping", native_backend); + add_assoc_bool(return_value, "native_tokenization", true); + add_assoc_bool(return_value, "native_token_decode", true); + add_assoc_string( + return_value, + "native_token_decode_scope", + native_stream_backend ? "tokenizer_and_token_vector_graph_decode" : "tokenizer_decode_only" + ); + add_assoc_bool(return_value, "synthetic_token_vector_graph", native_stream_backend); + add_assoc_bool(return_value, "synthetic_token_vector_scores", native_stream_backend); + add_assoc_bool(return_value, "prompt_to_logits_generation", local_backend || native_cpu_backend || gpu_backend); + add_assoc_bool(return_value, "paged_kv_cache", true); + add_assoc_bool(return_value, "native_stream_contract", native_stream_backend); + add_assoc_bool(return_value, "native_graph_streaming", native_cpu_backend); + add_assoc_bool(return_value, "native_token_selection", native_cpu_backend); + add_assoc_bool(return_value, "token_generation", local_backend || native_cpu_backend || gpu_backend); + add_assoc_bool(return_value, "openai_generation", local_backend || native_cpu_backend || gpu_backend); + add_assoc_bool(return_value, "embeddings", native_cpu_backend); + add_assoc_bool(return_value, "gpu", gpu_backend); + add_assoc_bool(return_value, "gpu_backend", gpu_backend); + add_assoc_bool(return_value, "gpu_runtime_status", gpu_backend); + add_assoc_bool(return_value, "gpu_cuda_driver_probe", gpu_backend); + add_assoc_bool(return_value, "gpu_cuda_context", gpu_backend); + add_assoc_bool(return_value, "gpu_cuda_context_owned", gpu_backend); + add_assoc_bool(return_value, "gpu_device_memory_allocator", gpu_backend); + add_assoc_bool(return_value, "gpu_host_to_device_weight_upload", gpu_backend); + add_assoc_bool(return_value, "gpu_uploaded_weight_cache", gpu_backend); + add_assoc_bool(return_value, "gpu_quantized_matvec_kernel", gpu_backend); + add_assoc_bool(return_value, "gpu_rms_norm_kernel", gpu_backend); + add_assoc_bool(return_value, "gpu_rope_kernel", gpu_backend); + add_assoc_bool(return_value, "gpu_attention_scores_kernel", gpu_backend); + add_assoc_bool(return_value, "gpu_attention_softmax_kernel", gpu_backend); + add_assoc_bool(return_value, "gpu_attention_value_aggregation_kernel", gpu_backend); + add_assoc_bool(return_value, "gpu_ffn_swiglu_path", gpu_backend); + add_assoc_bool(return_value, "gpu_output_projection_path", gpu_backend); + add_assoc_bool(return_value, "gpu_minimized_logits_readback", gpu_backend); + add_assoc_bool(return_value, "gpu_embedding_row_loader", gpu_backend); + add_assoc_bool(return_value, "gpu_device_vector_ops", gpu_backend); + add_assoc_bool(return_value, "gpu_device_kv_cache", gpu_backend); + add_assoc_bool(return_value, "gpu_decoder_graph_executor", gpu_backend); + add_assoc_bool(return_value, "gpu_decoder_graph_result_envelope", gpu_backend); + add_assoc_bool(return_value, "gpu_decoder_graph_execution_plan", gpu_backend); + add_assoc_bool(return_value, "reference_backend_contract", gpu_backend); + add_assoc_bool(return_value, "numeric_reference_backend", gpu_backend); + add_assoc_bool(return_value, "reference_backend_active_in_production", false); + add_assoc_bool(return_value, "gpu_decoder_graph_embedding_execution", gpu_backend); + add_assoc_bool(return_value, "gpu_decoder_graph_rms_norm_execution", gpu_backend); + add_assoc_bool(return_value, "gpu_decoder_graph_linear_execution", gpu_backend); + add_assoc_bool(return_value, "gpu_decoder_graph_slice_execution", gpu_backend); + add_assoc_bool(return_value, "gpu_decoder_graph_rope_execution", gpu_backend); + add_assoc_bool(return_value, "gpu_decoder_graph_kv_head_prepare_execution", gpu_backend); + add_assoc_bool(return_value, "gpu_decoder_graph_kv_write_execution", gpu_backend); + add_assoc_bool(return_value, "gpu_decoder_graph_kv_attention_execution", gpu_backend); + add_assoc_bool(return_value, "gpu_decoder_graph_attention_stack_slot_execution", gpu_backend); + add_assoc_bool(return_value, "gpu_decoder_graph_attention_heads_execution", gpu_backend); + add_assoc_bool(return_value, "gpu_decoder_graph_attention_stack_execution", gpu_backend); + add_assoc_bool(return_value, "gpu_decoder_graph_attention_output_projection_execution", gpu_backend); + add_assoc_bool(return_value, "gpu_decoder_graph_attention_residual_execution", gpu_backend); + add_assoc_bool(return_value, "gpu_decoder_graph_ffn_norm_execution", gpu_backend); + add_assoc_bool(return_value, "gpu_decoder_graph_ffn_gate_up_projection_execution", gpu_backend); + add_assoc_bool(return_value, "gpu_decoder_graph_ffn_swiglu_execution", gpu_backend); + add_assoc_bool(return_value, "gpu_decoder_graph_ffn_down_projection_execution", gpu_backend); + add_assoc_bool(return_value, "gpu_decoder_graph_ffn_output_residual_execution", gpu_backend); + add_assoc_bool(return_value, "gpu_decoder_graph_final_norm_execution", gpu_backend); + add_assoc_bool(return_value, "gpu_decoder_graph_logits_projection_execution", gpu_backend); + add_assoc_bool(return_value, "gpu_prompt_decoder_loop", gpu_backend); + add_assoc_bool(return_value, "gpu_plain_text_chat_generation", gpu_backend); + add_assoc_bool(return_value, "gpu_vram_admission", gpu_backend); + add_assoc_bool(return_value, "gpu_kv_cache_vram_estimate", gpu_backend); + add_assoc_bool(return_value, "gpu_thermal_policy", gpu_backend); + add_assoc_bool(return_value, "gpu_thermal_preflight", gpu_backend); + add_assoc_bool(return_value, "gpu_thermal_stream_abort", gpu_backend); + add_assoc_bool(return_value, "gpu_decoder_stream_contract", gpu_backend); + add_assoc_bool(return_value, "gpu_decoder_kernel", gpu_backend); + add_assoc_bool(return_value, "gpu_generation", gpu_backend); + add_assoc_bool(return_value, "silent_cpu_fallback", false); + add_assoc_string(return_value, "model_format", "gguf"); + king_inference_reference_backend_contract_array(kind, &reference_backend); + add_assoc_zval(return_value, "reference_backend", &reference_backend); +} diff --git a/extension/src/inference/backends/local/backend_king_local.inc b/extension/src/inference/backends/local/backend_king_local.inc new file mode 100644 index 000000000..5c29c0a5b --- /dev/null +++ b/extension/src/inference/backends/local/backend_king_local.inc @@ -0,0 +1,447 @@ +static zend_string *king_inference_king_local_runner_path(king_inference_model_object *model) +{ + zval *backend = king_inference_array_find(&model->config, "backend"); + const char *env_path; + zval *runner; + + if (backend != NULL && Z_TYPE_P(backend) == IS_ARRAY) { + zval *runner_config = king_inference_array_find(backend, "runner"); + runner = runner_config != NULL && Z_TYPE_P(runner_config) == IS_ARRAY + ? king_inference_array_find(runner_config, "path") + : NULL; + if (runner != NULL && Z_TYPE_P(runner) == IS_STRING && Z_STRLEN_P(runner) > 0) { + return zend_string_copy(Z_STR_P(runner)); + } + + runner = king_inference_array_find(backend, "runner_path"); + if (runner != NULL && Z_TYPE_P(runner) == IS_STRING && Z_STRLEN_P(runner) > 0) { + return zend_string_copy(Z_STR_P(runner)); + } + } + + env_path = getenv("KING_INFERENCE_RUNNER"); + if (env_path != NULL && env_path[0] != '\0') { + return zend_string_init(env_path, strlen(env_path), 0); + } + + return zend_string_init("king-local-infer", sizeof("king-local-infer") - 1, 0); +} + +static zend_result king_inference_king_local_validate_model_config( + zval *config, + const char *function_name +) { + zval *backend = king_inference_array_find(config, "backend"); + zval *runner_config; + zval *runner_path; + + if (backend == NULL || Z_TYPE_P(backend) != IS_ARRAY) { + return SUCCESS; + } + + runner_config = king_inference_array_find(backend, "runner"); + if (runner_config != NULL && Z_TYPE_P(runner_config) != IS_ARRAY) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() local inference backend.runner must be an array when provided.", + function_name + ); + return FAILURE; + } + + if (runner_config != NULL) { + runner_path = king_inference_array_find(runner_config, "path"); + if (runner_path != NULL + && (Z_TYPE_P(runner_path) != IS_STRING || Z_STRLEN_P(runner_path) == 0)) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() local inference backend.runner.path must be a non-empty string when provided.", + function_name + ); + return FAILURE; + } + } + + runner_path = king_inference_array_find(backend, "runner_path"); + if (runner_path != NULL + && (Z_TYPE_P(runner_path) != IS_STRING || Z_STRLEN_P(runner_path) == 0)) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() local inference backend.runner_path must be a non-empty string when provided.", + function_name + ); + return FAILURE; + } + + return SUCCESS; +} + +static bool king_inference_king_local_runner_has_slash(zend_string *runner) +{ + return memchr(ZSTR_VAL(runner), '/', ZSTR_LEN(runner)) != NULL; +} + +static bool king_inference_king_local_runner_path_is_executable(zend_string *runner) +{ + if (ZSTR_LEN(runner) == 0) { + return false; + } + + if (king_inference_king_local_runner_has_slash(runner)) { + return access(ZSTR_VAL(runner), X_OK) == 0; + } + + { + const char *path_env = getenv("PATH"); + char *paths; + char *saveptr = NULL; + char *entry; + bool found = false; + + if (path_env == NULL || path_env[0] == '\0') { + return false; + } + + paths = estrdup(path_env); + entry = strtok_r(paths, ":", &saveptr); + while (entry != NULL) { + zend_string *candidate = strpprintf( + 0, + "%s/%s", + entry[0] != '\0' ? entry : ".", + ZSTR_VAL(runner) + ); + + if (access(ZSTR_VAL(candidate), X_OK) == 0) { + found = true; + zend_string_release(candidate); + break; + } + + zend_string_release(candidate); + entry = strtok_r(NULL, ":", &saveptr); + } + efree(paths); + return found; + } +} + +static zend_result king_inference_king_local_require_runner( + zend_string *runner, + const char *function_name +) { + if (king_inference_king_local_runner_path_is_executable(runner)) { + return SUCCESS; + } + + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "%s() local inference runner '%s' is not executable or could not be found in PATH.", + function_name, + ZSTR_VAL(runner) + ); + return FAILURE; +} + +static zend_string *king_inference_king_local_prompt_from_request(zval *request) +{ + zval *prompt = king_inference_array_find(request, "prompt"); + zval *messages; + zval *message; + smart_str buffer = {0}; + + if (prompt != NULL && Z_TYPE_P(prompt) == IS_STRING) { + return zend_string_copy(Z_STR_P(prompt)); + } + + messages = king_inference_array_find(request, "messages"); + if (messages == NULL || Z_TYPE_P(messages) != IS_ARRAY) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "Inference request requires prompt or messages."); + return NULL; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(messages), message) { + zval *role; + zval *content; + smart_str text = {0}; + + if (Z_TYPE_P(message) != IS_ARRAY) { + continue; + } + role = king_inference_array_find(message, "role"); + content = king_inference_array_find(message, "content"); + if (king_inference_openai_append_content_text(content, &text) != SUCCESS) { + if (!king_inference_openai_message_has_tool_calls(message) + || king_inference_openai_append_message_tool_calls_text(message, &text) != SUCCESS) { + smart_str_free(&text); + continue; + } + } + smart_str_0(&text); + + if (role != NULL && Z_TYPE_P(role) == IS_STRING && Z_STRLEN_P(role) > 0) { + smart_str_append(&buffer, Z_STR_P(role)); + smart_str_appends(&buffer, ": "); + } + if (text.s != NULL) { + smart_str_append(&buffer, text.s); + } + smart_str_appendc(&buffer, '\n'); + smart_str_free(&text); + } ZEND_HASH_FOREACH_END(); + + king_inference_openai_append_tool_prompt(request, &buffer); + king_inference_openai_append_response_format_instruction(request, &buffer); + smart_str_appends(&buffer, "assistant: "); + smart_str_0(&buffer); + return buffer.s; +} + +static zend_result king_inference_king_local_spawn_argv( + king_inference_stream_object *stream, + zend_string **argv, + size_t argc +) { + int stdout_pipe[2]; + int stderr_pipe[2]; + pid_t pid; + char **cargv; + size_t i; + + if (pipe(stdout_pipe) != 0) { + zend_throw_exception_ex(king_ce_runtime_exception, 0, "Could not create inference stdout pipe."); + return FAILURE; + } + if (pipe(stderr_pipe) != 0) { + close(stdout_pipe[0]); + close(stdout_pipe[1]); + zend_throw_exception_ex(king_ce_runtime_exception, 0, "Could not create inference stderr pipe."); + return FAILURE; + } + + cargv = ecalloc(argc + 1, sizeof(char *)); + for (i = 0; i < argc; i++) { + cargv[i] = ZSTR_VAL(argv[i]); + } + + pid = fork(); + if (pid < 0) { + close(stdout_pipe[0]); + close(stdout_pipe[1]); + close(stderr_pipe[0]); + close(stderr_pipe[1]); + efree(cargv); + zend_throw_exception_ex(king_ce_runtime_exception, 0, "Could not fork inference runner."); + return FAILURE; + } + + if (pid == 0) { + dup2(stdout_pipe[1], STDOUT_FILENO); + dup2(stderr_pipe[1], STDERR_FILENO); + close(stdout_pipe[0]); + close(stdout_pipe[1]); + close(stderr_pipe[0]); + close(stderr_pipe[1]); + execvp(cargv[0], cargv); + _exit(127); + } + + close(stdout_pipe[1]); + close(stderr_pipe[1]); + fcntl(stdout_pipe[0], F_SETFL, fcntl(stdout_pipe[0], F_GETFL, 0) | O_NONBLOCK); + fcntl(stderr_pipe[0], F_SETFL, fcntl(stderr_pipe[0], F_GETFL, 0) | O_NONBLOCK); + + stream->stdout_fd = stdout_pipe[0]; + stream->stderr_fd = stderr_pipe[0]; + stream->child_pid = (zend_long) pid; + efree(cargv); + return SUCCESS; +} + +static void king_inference_king_local_argv_release(zend_string **argv, size_t argc) +{ + size_t i; + + for (i = 0; i < argc; i++) { + zend_string_release(argv[i]); + } +} + +static void king_inference_king_local_append_long_arg( + zend_string **argv, + size_t *argc, + zval *request, + const char *request_key, + const char *arg_name +) { + zval *value = king_inference_array_find(request, request_key); + + if (value == NULL || Z_TYPE_P(value) != IS_LONG) { + return; + } + + argv[(*argc)++] = zend_string_init(arg_name, strlen(arg_name), 0); + argv[(*argc)++] = strpprintf(0, ZEND_LONG_FMT, Z_LVAL_P(value)); +} + +static void king_inference_king_local_append_double_arg( + zend_string **argv, + size_t *argc, + zval *request, + const char *request_key, + const char *arg_name +) { + zval *value = king_inference_array_find(request, request_key); + double numeric; + + if (value == NULL) { + return; + } + if (Z_TYPE_P(value) == IS_DOUBLE) { + numeric = Z_DVAL_P(value); + } else if (Z_TYPE_P(value) == IS_LONG) { + numeric = (double) Z_LVAL_P(value); + } else { + return; + } + + argv[(*argc)++] = zend_string_init(arg_name, strlen(arg_name), 0); + argv[(*argc)++] = strpprintf(0, "%.6f", numeric); +} + +static void king_inference_king_local_append_string_arg( + zend_string **argv, + size_t *argc, + zend_string *value, + const char *arg_name) +{ + if (value == NULL || ZSTR_LEN(value) == 0) { + return; + } + + argv[(*argc)++] = zend_string_init(arg_name, strlen(arg_name), 0); + argv[(*argc)++] = zend_string_copy(value); +} + +static void king_inference_king_local_append_stop_args( + zend_string **argv, + size_t *argc, + zval *request) +{ + zval *stop = king_inference_array_find(request, "stop"); + zval *entry; + + if (stop == NULL) { + return; + } + if (Z_TYPE_P(stop) == IS_STRING) { + king_inference_king_local_append_string_arg(argv, argc, Z_STR_P(stop), "--reverse-prompt"); + return; + } + if (!king_inference_openai_array_is_list(stop)) { + return; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(stop), entry) { + if (Z_TYPE_P(entry) == IS_STRING) { + king_inference_king_local_append_string_arg(argv, argc, Z_STR_P(entry), "--reverse-prompt"); + } + } ZEND_HASH_FOREACH_END(); +} + +static zend_result king_inference_king_local_stream_start( + king_inference_stream_object *stream, + const char *function_name +) { + king_inference_model_object *model; + zend_string *runner; + zend_string *prompt; + zend_string *max_tokens; + zend_string *temperature; + zend_string *context_tokens = NULL; + zend_string *gpu_layers; + zend_string *argv[56]; + size_t argc = 0; + const char *generation_error = NULL; + const char *stop_error = NULL; + + model = php_king_inference_model_obj_from_zend(Z_OBJ(stream->model)); + if (!king_inference_openai_stream_generation_options_valid(&stream->request, &generation_error)) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() %s", + function_name, + generation_error != NULL ? generation_error : "generation options are invalid." + ); + return FAILURE; + } + if (!king_inference_openai_stop_option_valid(&stream->request, &stop_error)) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() %s", + function_name, + stop_error != NULL ? stop_error : "stop options are invalid." + ); + return FAILURE; + } + + runner = king_inference_king_local_runner_path(model); + if (king_inference_king_local_require_runner(runner, function_name) != SUCCESS) { + zend_string_release(runner); + return FAILURE; + } + + prompt = king_inference_king_local_prompt_from_request(&stream->request); + if (prompt == NULL) { + zend_string_release(runner); + return FAILURE; + } + + max_tokens = strpprintf(0, ZEND_LONG_FMT, king_inference_long_from_array(&stream->request, "max_tokens", 256)); + temperature = strpprintf(0, "%.17g", king_inference_double_from_array(&stream->request, "temperature", 0.8)); + gpu_layers = strpprintf(0, ZEND_LONG_FMT, king_inference_max_gpu_layers(&model->config)); + + argv[argc++] = runner; + argv[argc++] = zend_string_init("-m", sizeof("-m") - 1, 0); + argv[argc++] = zend_string_copy(model->artifact_path); + argv[argc++] = zend_string_init("-p", sizeof("-p") - 1, 0); + argv[argc++] = prompt; + argv[argc++] = zend_string_init("-n", sizeof("-n") - 1, 0); + argv[argc++] = max_tokens; + argv[argc++] = zend_string_init("--temp", sizeof("--temp") - 1, 0); + argv[argc++] = temperature; + argv[argc++] = zend_string_init("-ngl", sizeof("-ngl") - 1, 0); + argv[argc++] = gpu_layers; + argv[argc++] = zend_string_init("--no-display-prompt", sizeof("--no-display-prompt") - 1, 0); + argv[argc++] = zend_string_init("--log-disable", sizeof("--log-disable") - 1, 0); + + if (king_inference_long_from_array(&model->config, "context_tokens", 0) > 0) { + context_tokens = strpprintf(0, ZEND_LONG_FMT, king_inference_long_from_array(&model->config, "context_tokens", 0)); + argv[argc++] = zend_string_init("-c", sizeof("-c") - 1, 0); + argv[argc++] = context_tokens; + } + + king_inference_king_local_append_long_arg(argv, &argc, &stream->request, "seed", "--seed"); + king_inference_king_local_append_long_arg(argv, &argc, &stream->request, "top_k", "--top-k"); + king_inference_king_local_append_double_arg(argv, &argc, &stream->request, "top_p", "--top-p"); + king_inference_king_local_append_stop_args(argv, &argc, &stream->request); + + if (king_inference_check_gpu_policy_before_run(stream, function_name) != SUCCESS) { + king_inference_king_local_argv_release(argv, argc); + return FAILURE; + } + + if (king_inference_king_local_spawn_argv(stream, argv, argc) != SUCCESS) { + king_inference_king_local_argv_release(argv, argc); + return FAILURE; + } + + king_inference_king_local_argv_release(argv, argc); + return SUCCESS; +} diff --git a/extension/src/inference/backends/native/backend_king_native.inc b/extension/src/inference/backends/native/backend_king_native.inc new file mode 100644 index 000000000..cf95813b4 --- /dev/null +++ b/extension/src/inference/backends/native/backend_king_native.inc @@ -0,0 +1,676 @@ +static zval *king_inference_king_native_graph_options(king_inference_stream_object *stream) +{ + zval *options = king_inference_array_find(&stream->request, "graph_options"); + + if (options != NULL && king_inference_openai_array_is_object(options)) { + return options; + } + + options = king_inference_array_find(&stream->options, "graph_options"); + return options != NULL && king_inference_openai_array_is_object(options) ? options : NULL; +} + +static bool king_inference_king_native_stream_diagnostics_enabled(king_inference_stream_object *stream) +{ + zval *flag; + zval *diagnostics; + + flag = king_inference_array_find(&stream->options, "include_stream_diagnostics"); + if (flag != NULL) { + return zend_is_true(flag); + } + + diagnostics = king_inference_array_find(&stream->options, "diagnostics"); + if (diagnostics == NULL) { + return false; + } + if (Z_TYPE_P(diagnostics) != IS_ARRAY) { + return zend_is_true(diagnostics); + } + + flag = king_inference_array_find(diagnostics, "stream_contracts"); + if (flag != NULL) { + return zend_is_true(flag); + } + + flag = king_inference_array_find(diagnostics, "enabled"); + return flag != NULL && zend_is_true(flag); +} + +static zend_result king_inference_king_native_append_prompt_generated_tokens( + king_inference_model_object *model, + king_inference_stream_object *stream, + zval *prompt_text, + zval *graph_options, + zend_long max_tokens); + +static zend_result king_inference_king_native_stream_token_limit( + king_inference_stream_object *stream, + zval *graph_options, + zend_long *limit) +{ + zval *value = king_inference_array_find(&stream->options, "max_native_stream_tokens"); + + if (value == NULL && graph_options != NULL) { + value = king_inference_array_find(graph_options, "max_native_stream_tokens"); + } + if (value == NULL) { + *limit = 4096; + return SUCCESS; + } + if (Z_TYPE_P(value) != IS_LONG || Z_LVAL_P(value) <= 0) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "King native stream max_native_stream_tokens must be a positive integer." + ); + return FAILURE; + } + + *limit = Z_LVAL_P(value); + return SUCCESS; +} + +static zval *king_inference_king_native_find_memory_option(zval *source) +{ + zval *value = king_inference_array_find(source, "with_memory"); + zval *alias = king_inference_array_find(source, "with-memory"); + + if (value != NULL && alias != NULL) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "King native stream memory config must use either with_memory or with-memory, not both." + ); + return NULL; + } + + return value != NULL ? value : alias; +} + +static zend_result king_inference_king_native_apply_memory_option( + zval *source, + bool *with_memory) +{ + zval *value = king_inference_king_native_find_memory_option(source); + + if (EG(exception)) { + return FAILURE; + } + if (value == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(value) != IS_TRUE && Z_TYPE_P(value) != IS_FALSE) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "King native stream with_memory must be a boolean." + ); + return FAILURE; + } + + *with_memory = Z_TYPE_P(value) == IS_TRUE; + return SUCCESS; +} + +static zend_result king_inference_king_native_stream_with_memory( + king_inference_model_object *model, + king_inference_stream_object *stream, + zval *graph_options, + bool *with_memory) +{ + *with_memory = king_high_perf_compute_ai_config.inference_with_memory; + + if (king_inference_king_native_apply_memory_option(&model->config, with_memory) != SUCCESS + || king_inference_king_native_apply_memory_option(&stream->request, with_memory) != SUCCESS + || king_inference_king_native_apply_memory_option(&stream->options, with_memory) != SUCCESS) { + return FAILURE; + } + if (graph_options != NULL + && king_inference_king_native_apply_memory_option(graph_options, with_memory) != SUCCESS) { + return FAILURE; + } + + return SUCCESS; +} + +static zval *king_inference_king_native_graph_token_payload(zval *graph_result, zval *graph) +{ + zval *final = king_inference_array_find(graph_result, "final"); + zval *output_id = king_inference_array_find(graph, "output"); + zval *payload = NULL; + + if (final == NULL || Z_TYPE_P(final) != IS_ARRAY) { + return NULL; + } + if (output_id != NULL && Z_TYPE_P(output_id) == IS_STRING) { + payload = zend_hash_find(Z_ARRVAL_P(final), Z_STR_P(output_id)); + if (payload != NULL) { + return payload; + } + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(final), payload) { + return payload; + } ZEND_HASH_FOREACH_END(); + + return NULL; +} + +static zend_result king_inference_king_native_token_id_from_result( + zval *graph_result, + zval *graph, + zend_ulong *token_id +) { + zval *payload = king_inference_king_native_graph_token_payload(graph_result, graph); + zval *values; + zval *token; + double token_value; + + if (payload == NULL || Z_TYPE_P(payload) != IS_ARRAY) { + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King native graph stream produced no final token payload."); + return FAILURE; + } + + values = king_inference_array_find(payload, "values"); + if (values == NULL || Z_TYPE_P(values) != IS_ARRAY) { + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King native graph token payload has no values vector."); + return FAILURE; + } + + token = zend_hash_index_find(Z_ARRVAL_P(values), 0); + if (token == NULL) { + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King native graph token payload is empty."); + return FAILURE; + } + if (Z_TYPE_P(token) == IS_LONG) { + if (Z_LVAL_P(token) < 0) { + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King native graph produced a negative token id."); + return FAILURE; + } + *token_id = (zend_ulong) Z_LVAL_P(token); + return SUCCESS; + } + if (Z_TYPE_P(token) != IS_DOUBLE || Z_DVAL_P(token) < 0.0) { + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King native graph produced an invalid token id."); + return FAILURE; + } + + token_value = Z_DVAL_P(token); + if (token_value > (double) ZEND_ULONG_MAX) { + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King native graph token id exceeds supported limits."); + return FAILURE; + } + + *token_id = (zend_ulong) token_value; + return SUCCESS; +} + +static bool king_inference_king_native_metric_double(zval *values, zend_ulong index, double *value) +{ + zval *field = zend_hash_index_find(Z_ARRVAL_P(values), index); + + if (field == NULL) { + return false; + } + if (Z_TYPE_P(field) == IS_LONG) { + *value = (double) Z_LVAL_P(field); + return true; + } + if (Z_TYPE_P(field) == IS_DOUBLE) { + *value = Z_DVAL_P(field); + return true; + } + + return false; +} + +static void king_inference_king_native_update_decoder_metrics( + king_inference_stream_object *stream, + zval *graph_result, + zval *graph, + zend_ulong token_id +) { + zval *payload = king_inference_king_native_graph_token_payload(graph_result, graph); + zval *values = payload != NULL && Z_TYPE_P(payload) == IS_ARRAY ? king_inference_array_find(payload, "values") : NULL; + double probability; + double logit; + double rank; + + stream->native_decoder_token_count++; + stream->native_decoder_last_token_id = token_id; + stream->native_decoder_last_token_available = true; + stream->native_decoder_last_score_available = false; + if (values == NULL || Z_TYPE_P(values) != IS_ARRAY) { + return; + } + if (king_inference_king_native_metric_double(values, 1, &probability) + && king_inference_king_native_metric_double(values, 2, &logit) + && king_inference_king_native_metric_double(values, 3, &rank)) { + stream->native_decoder_last_probability = probability; + stream->native_decoder_last_logit = logit; + stream->native_decoder_last_rank = rank; + stream->native_decoder_last_score_available = true; + } +} + +static bool king_inference_king_native_is_token_decode_graph(zval *graph) +{ + zval *type = king_inference_array_find(graph, "graph_type"); + + return type != NULL + && Z_TYPE_P(type) == IS_STRING + && zend_string_equals_literal(Z_STR_P(type), "king_native_cpu_token_decode"); +} + +static zval *king_inference_king_native_next_token_op(zval *graph) +{ + zval *ops = king_inference_array_find(graph, "ops"); + zval *entry; + zval *op_name; + + if (ops == NULL || Z_TYPE_P(ops) != IS_ARRAY) { + return NULL; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(ops), entry) { + if (entry == NULL || Z_TYPE_P(entry) != IS_ARRAY) { + continue; + } + op_name = king_inference_array_find(entry, "op"); + if (op_name != NULL + && Z_TYPE_P(op_name) == IS_STRING + && (zend_string_equals_literal(Z_STR_P(op_name), "argmax_token") + || zend_string_equals_literal(Z_STR_P(op_name), "sample_token"))) { + return entry; + } + } ZEND_HASH_FOREACH_END(); + + return NULL; +} + +static void king_inference_king_native_copy_decode_option(zval *target, zval *source, const char *key) +{ + zval *value = king_inference_array_find(source, key); + + if (value != NULL) { + zval copied; + ZVAL_COPY(&copied, value); + add_assoc_zval(target, key, &copied); + } +} + +static zend_result king_inference_king_native_decode_options_from_graph(zval *graph, zval *decode_options) +{ + zval *op = king_inference_king_native_next_token_op(graph); + zval *op_name; + + array_init(decode_options); + if (op == NULL) { + zval_ptr_dtor(decode_options); + zend_throw_exception_ex(king_ce_validation_exception, 0, "King native generated token stream requires a token-selection graph op."); + return FAILURE; + } + op_name = king_inference_array_find(op, "op"); + if (op_name == NULL || Z_TYPE_P(op_name) != IS_STRING) { + zval_ptr_dtor(decode_options); + zend_throw_exception_ex(king_ce_validation_exception, 0, "King native generated token stream token-selection op is invalid."); + return FAILURE; + } + if (zend_string_equals_literal(Z_STR_P(op_name), "argmax_token")) { + add_assoc_string(decode_options, "sampler", "argmax"); + return SUCCESS; + } + if (!zend_string_equals_literal(Z_STR_P(op_name), "sample_token")) { + zval_ptr_dtor(decode_options); + zend_throw_exception_ex(king_ce_validation_exception, 0, "King native generated token stream token-selection op is unsupported."); + return FAILURE; + } + + add_assoc_string(decode_options, "sampler", "sample"); + king_inference_king_native_copy_decode_option(decode_options, op, "temperature"); + king_inference_king_native_copy_decode_option(decode_options, op, "top_k"); + king_inference_king_native_copy_decode_option(decode_options, op, "top_p"); + king_inference_king_native_copy_decode_option(decode_options, op, "seed"); + return SUCCESS; +} + +static zend_result king_inference_king_native_graph_position(zval *graph, zend_ulong *position) +{ + zval *value = king_inference_array_find(graph, "position"); + + if (value == NULL || Z_TYPE_P(value) != IS_LONG || Z_LVAL_P(value) < 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King native generated token stream requires a non-negative graph position."); + return FAILURE; + } + + *position = (zend_ulong) Z_LVAL_P(value); + return SUCCESS; +} + +static zend_result king_inference_king_native_append_graph_token( + king_inference_model_object *model, + king_inference_stream_object *stream, + zval *graph, + zval *graph_options, + zval *state, + bool with_memory, + zend_ulong *token_id_out +) { + zval working_graph; + zval graph_result; + zval *next_state; + zend_ulong token_id; + zend_string *token_text; + zend_result status; + + if (graph == NULL || Z_TYPE_P(graph) != IS_ARRAY) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King native stream graph entries must be arrays."); + return FAILURE; + } + + ZVAL_DUP(&working_graph, graph); + SEPARATE_ARRAY(&working_graph); + if (with_memory + && !Z_ISUNDEF_P(state) + && king_inference_array_find(&working_graph, "state") == NULL) { + zval state_copy; + ZVAL_COPY(&state_copy, state); + add_assoc_zval(&working_graph, "state", &state_copy); + } + + ZVAL_UNDEF(&graph_result); + status = king_inference_graph_run_array(model, &working_graph, graph_options, &graph_result); + if (status != SUCCESS) { + zval_ptr_dtor(&working_graph); + return FAILURE; + } + if (king_inference_king_native_token_id_from_result(&graph_result, &working_graph, &token_id) != SUCCESS) { + zval_ptr_dtor(&graph_result); + zval_ptr_dtor(&working_graph); + return FAILURE; + } + + token_text = king_inference_tokenizer_decode_token(model, token_id); + if (token_text == NULL) { + zval_ptr_dtor(&graph_result); + zval_ptr_dtor(&working_graph); + return FAILURE; + } + king_inference_king_native_update_decoder_metrics(stream, &graph_result, &working_graph, token_id); + add_next_index_str(&stream->native_events, token_text); + if (token_id_out != NULL) { + *token_id_out = token_id; + } + + next_state = with_memory ? king_inference_array_find(&graph_result, "state") : NULL; + if (next_state != NULL && Z_TYPE_P(next_state) == IS_ARRAY) { + if (!Z_ISUNDEF_P(state)) { + zval_ptr_dtor(state); + } + ZVAL_COPY(state, next_state); + } + + zval_ptr_dtor(&graph_result); + zval_ptr_dtor(&working_graph); + return SUCCESS; +} + +static zend_result king_inference_king_native_append_generated_tokens( + king_inference_model_object *model, + king_inference_stream_object *stream, + zval *graph, + zval *graph_options, + zval *state, + bool with_memory, + zend_long max_tokens +) { + zval current_graph; + zval decode_options; + zend_ulong token_id = 0; + zend_ulong position = 0; + zend_long emitted; + + if (king_inference_king_native_graph_position(graph, &position) != SUCCESS + || king_inference_king_native_decode_options_from_graph(graph, &decode_options) != SUCCESS) { + return FAILURE; + } + + ZVAL_DUP(¤t_graph, graph); + for (emitted = 0; emitted < max_tokens; emitted++) { + if (king_inference_king_native_append_graph_token(model, stream, ¤t_graph, graph_options, state, with_memory, &token_id) != SUCCESS) { + zval_ptr_dtor(¤t_graph); + zval_ptr_dtor(&decode_options); + return FAILURE; + } + if (emitted + 1 >= max_tokens) { + break; + } + if (position >= (zend_ulong) ZEND_LONG_MAX || token_id > (zend_ulong) ZEND_LONG_MAX) { + zval_ptr_dtor(¤t_graph); + zval_ptr_dtor(&decode_options); + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King native generated token stream exceeded supported token or position limits."); + return FAILURE; + } + zval_ptr_dtor(¤t_graph); + { + zval token; + ZVAL_LONG(&token, (zend_long) token_id); + position++; + if (king_inference_token_decode_graph_array(model, &token, position, &decode_options, ¤t_graph) != SUCCESS) { + zval_ptr_dtor(&decode_options); + return FAILURE; + } + } + } + + zval_ptr_dtor(¤t_graph); + zval_ptr_dtor(&decode_options); + return SUCCESS; +} + +static zend_result king_inference_king_native_prepare_events( + king_inference_model_object *model, + king_inference_stream_object *stream +) { + zval *graph = king_inference_array_find(&stream->request, "graph"); + zval *graphs = king_inference_array_find(&stream->request, "graphs"); + zval *prompt_text = king_inference_array_find(&stream->request, "native_prompt_text"); + zval *request_graph_options = king_inference_array_find(&stream->request, "graph_options"); + zval *option_graph_options = king_inference_array_find(&stream->options, "graph_options"); + zval *graph_options = king_inference_king_native_graph_options(stream); + zval *max_tokens_value = king_inference_array_find(&stream->request, "max_tokens"); + zval state; + zval *entry; + zend_long max_tokens = 1; + zend_long max_native_stream_tokens; + zend_long emitted = 0; + bool with_memory = false; + + if (graph != NULL && !king_inference_openai_array_is_object(graph)) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King native stream graph must be an object array."); + return FAILURE; + } + if (graphs != NULL && !king_inference_openai_array_is_list(graphs)) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King native stream graphs must be a list array."); + return FAILURE; + } + if ((request_graph_options != NULL && !king_inference_openai_array_is_object(request_graph_options)) + || (option_graph_options != NULL && !king_inference_openai_array_is_object(option_graph_options))) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King native stream graph_options must be an object array."); + return FAILURE; + } + if (graphs == NULL && graph == NULL && prompt_text == NULL) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "King native CPU streaming requires a native graph, graphs, or prompt request." + ); + return FAILURE; + } + if (max_tokens_value != NULL) { + if (Z_TYPE_P(max_tokens_value) != IS_LONG) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King native stream max_tokens must be an integer."); + return FAILURE; + } + max_tokens = Z_LVAL_P(max_tokens_value); + } else if (graphs != NULL) { + max_tokens = (zend_long) zend_hash_num_elements(Z_ARRVAL_P(graphs)); + } + if (max_tokens <= 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King native stream max_tokens must be positive."); + return FAILURE; + } + if (king_inference_king_native_stream_token_limit( + stream, + graph_options, + &max_native_stream_tokens + ) != SUCCESS) { + return FAILURE; + } + if (max_tokens > max_native_stream_tokens) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "King native stream max_tokens exceeds configured max_native_stream_tokens." + ); + return FAILURE; + } + if (king_inference_king_native_stream_with_memory(model, stream, graph_options, &with_memory) != SUCCESS) { + return FAILURE; + } + + if (!Z_ISUNDEF(stream->native_events)) { + zval_ptr_dtor(&stream->native_events); + } + array_init(&stream->native_events); + stream->native_event_index = 0; + ZVAL_UNDEF(&state); + + if (king_inference_llm_cache_prepare_stream( + model, + stream, + graph_options, + with_memory, + "King native graph stream" + ) != SUCCESS) { + return FAILURE; + } + + if (graphs != NULL) { + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(graphs), entry) { + if (emitted >= max_tokens) { + break; + } + if (king_inference_king_native_append_graph_token(model, stream, entry, graph_options, &state, with_memory, NULL) != SUCCESS) { + if (!Z_ISUNDEF(state)) { + zval_ptr_dtor(&state); + } + return FAILURE; + } + emitted++; + } ZEND_HASH_FOREACH_END(); + } else if (prompt_text != NULL) { + if (king_inference_king_native_append_prompt_generated_tokens( + model, + stream, + prompt_text, + graph_options, + max_tokens + ) != SUCCESS) { + if (!Z_ISUNDEF(state)) { + zval_ptr_dtor(&state); + } + return FAILURE; + } + } else if (king_inference_king_native_is_token_decode_graph(graph)) { + if (king_inference_king_native_append_generated_tokens(model, stream, graph, graph_options, &state, with_memory, max_tokens) != SUCCESS) { + if (!Z_ISUNDEF(state)) { + zval_ptr_dtor(&state); + } + return FAILURE; + } + } else { + while (emitted < max_tokens) { + if (king_inference_king_native_append_graph_token(model, stream, graph, graph_options, &state, with_memory, NULL) != SUCCESS) { + if (!Z_ISUNDEF(state)) { + zval_ptr_dtor(&state); + } + return FAILURE; + } + emitted++; + } + } + + if (!Z_ISUNDEF(state)) { + zval_ptr_dtor(&state); + } + if (zend_hash_num_elements(Z_ARRVAL(stream->native_events)) == 0) { + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King native stream produced no token events."); + return FAILURE; + } + + return SUCCESS; +} + +static zend_result king_inference_king_native_cpu_stream_start( + king_inference_stream_object *stream, + const char *function_name +) { + king_inference_model_object *model = php_king_inference_model_obj_from_zend(Z_OBJ(stream->model)); + + if (king_inference_check_gpu_policy_before_run(stream, function_name) != SUCCESS) { + return FAILURE; + } + + return king_inference_king_native_prepare_events(model, stream); +} + +static void king_inference_king_native_model_info( + king_inference_model_object *intern, + zval *return_value +) { + add_assoc_string(return_value, "engine", "king_native_cpu"); + add_assoc_string(return_value, "loader", "king_gguf_structure"); + add_assoc_bool(return_value, "external_runtime", false); + add_assoc_bool(return_value, "tokenization_ready", intern->gguf.tokenizer_lookup_loaded); + add_assoc_bool(return_value, "token_generation_ready", true); + add_assoc_string(return_value, "generation_runtime", "king_native_graph"); + add_assoc_long(return_value, "native_file_bytes", (zend_long) intern->gguf.file_size); + add_assoc_bool(return_value, "native_metadata_ready", intern->gguf.metadata_parsed); + add_assoc_bool(return_value, "native_tensor_directory_ready", intern->gguf.tensor_directory_parsed); + add_assoc_bool(return_value, "native_model_mapped", intern->native_map_loaded); + if (!Z_ISUNDEF(intern->paged_kv_cache_plan) && Z_TYPE(intern->paged_kv_cache_plan) == IS_ARRAY) { + zval *ready = zend_hash_str_find(Z_ARRVAL(intern->paged_kv_cache_plan), "ready", sizeof("ready") - 1); + add_assoc_bool(return_value, "paged_kv_cache_ready", ready != NULL && zend_is_true(ready)); + } + add_assoc_bool(return_value, "native_tokenizer_tokens_loaded", intern->gguf.tokenizer_tokens_loaded); + add_assoc_bool(return_value, "native_tokenizer_lookup_loaded", intern->gguf.tokenizer_lookup_loaded); + add_assoc_bool(return_value, "native_tokenizer_merges_loaded", intern->gguf.tokenizer_merges_loaded); + add_assoc_long(return_value, "native_map_bytes", (zend_long) intern->native_map_size); + add_assoc_long( + return_value, + "native_tensor_index_count", + (zend_long) zend_hash_num_elements(Z_ARRVAL(intern->tensor_index)) + ); + add_assoc_long( + return_value, + "native_tokenizer_token_count", + (zend_long) zend_hash_num_elements(Z_ARRVAL(intern->tokenizer_tokens)) + ); + add_assoc_long( + return_value, + "native_tokenizer_merge_count", + (zend_long) zend_hash_num_elements(Z_ARRVAL(intern->tokenizer_merges)) + ); + add_assoc_long(return_value, "tokenizer_bos_id", intern->gguf.tokenizer_bos_id); + add_assoc_long(return_value, "tokenizer_eos_id", intern->gguf.tokenizer_eos_id); + add_assoc_long(return_value, "tokenizer_unknown_id", intern->gguf.tokenizer_unknown_id); + add_assoc_long(return_value, "tokenizer_pad_id", intern->gguf.tokenizer_pad_id); + if (intern->gguf.architecture != NULL) { + add_assoc_str(return_value, "architecture", zend_string_copy(intern->gguf.architecture)); + } + if (intern->gguf.tokenizer_model != NULL) { + add_assoc_str(return_value, "tokenizer_model", zend_string_copy(intern->gguf.tokenizer_model)); + } +} diff --git a/extension/src/inference/backends/native/backend_king_native_gpu.inc b/extension/src/inference/backends/native/backend_king_native_gpu.inc new file mode 100644 index 000000000..f335dd87f --- /dev/null +++ b/extension/src/inference/backends/native/backend_king_native_gpu.inc @@ -0,0 +1,740 @@ +static zend_result king_inference_king_native_gpu_validate_stream_request( + king_inference_stream_object *stream, + const char *function_name, + zend_long *max_tokens +) { + zval *graph = king_inference_array_find(&stream->request, "graph"); + zval *graphs = king_inference_array_find(&stream->request, "graphs"); + zval *prompt_text = king_inference_array_find(&stream->request, "native_prompt_text"); + zval *request_graph_options = king_inference_array_find(&stream->request, "graph_options"); + zval *option_graph_options = king_inference_array_find(&stream->options, "graph_options"); + zval *graph_options = king_inference_king_native_graph_options(stream); + zval *max_tokens_value = king_inference_array_find(&stream->request, "max_tokens"); + zend_long max_native_stream_tokens; + + *max_tokens = 1; + + if (stream->openai_compatible) { + if (graph != NULL || graphs != NULL) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() OpenAI-compatible King native GPU generation accepts native_prompt_text only; " + "use the native stream contract for graph payloads.", + function_name + ); + return FAILURE; + } + if (prompt_text == NULL) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() OpenAI-compatible King native GPU generation requires native_prompt_text.", + function_name + ); + return FAILURE; + } + } + if (graph != NULL && !king_inference_openai_array_is_object(graph)) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King native GPU stream graph must be an object array."); + return FAILURE; + } + if (graphs != NULL && !king_inference_openai_array_is_list(graphs)) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King native GPU stream graphs must be a list array."); + return FAILURE; + } + if (prompt_text != NULL && (Z_TYPE_P(prompt_text) != IS_STRING || Z_STRLEN_P(prompt_text) == 0)) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King native GPU stream native_prompt_text must be a non-empty string."); + return FAILURE; + } + if (prompt_text != NULL && (graph != NULL || graphs != NULL)) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "King native GPU stream must use either native_prompt_text or graph/graphs, not both." + ); + return FAILURE; + } + if ((request_graph_options != NULL && !king_inference_openai_array_is_object(request_graph_options)) + || (option_graph_options != NULL && !king_inference_openai_array_is_object(option_graph_options))) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King native GPU stream graph_options must be an object array."); + return FAILURE; + } + if (graphs == NULL && graph == NULL && prompt_text == NULL) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "King native GPU streaming requires native_prompt_text, graph, or graphs." + ); + return FAILURE; + } + if (max_tokens_value != NULL) { + if (Z_TYPE_P(max_tokens_value) != IS_LONG) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King native GPU stream max_tokens must be an integer."); + return FAILURE; + } + *max_tokens = Z_LVAL_P(max_tokens_value); + } else if (graphs != NULL) { + *max_tokens = (zend_long) zend_hash_num_elements(Z_ARRVAL_P(graphs)); + } + if (*max_tokens <= 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King native GPU stream max_tokens must be positive."); + return FAILURE; + } + if (king_inference_king_native_stream_token_limit(stream, graph_options, &max_native_stream_tokens) != SUCCESS) { + return FAILURE; + } + if (*max_tokens > max_native_stream_tokens) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "King native GPU stream max_tokens exceeds configured max_native_stream_tokens." + ); + return FAILURE; + } + + return SUCCESS; +} + +static bool king_inference_king_native_gpu_graph_token_id_from_input( + zval *graph, + zend_string *input_id, + zend_ulong *token_id +) { + zval *inputs = king_inference_array_find(graph, "inputs"); + zval *input; + zval *token; + double token_value; + + if (inputs == NULL || Z_TYPE_P(inputs) != IS_ARRAY) { + return false; + } + + input = zend_hash_find(Z_ARRVAL_P(inputs), input_id); + if (input == NULL || Z_TYPE_P(input) != IS_ARRAY) { + return false; + } + + token = zend_hash_index_find(Z_ARRVAL_P(input), 0); + if (token == NULL) { + return false; + } + if (Z_TYPE_P(token) == IS_LONG) { + if (Z_LVAL_P(token) < 0) { + return false; + } + *token_id = (zend_ulong) Z_LVAL_P(token); + return true; + } + if (Z_TYPE_P(token) != IS_DOUBLE || Z_DVAL_P(token) < 0.0) { + return false; + } + + token_value = Z_DVAL_P(token); + if (token_value > (double) ZEND_ULONG_MAX) { + return false; + } + + *token_id = (zend_ulong) token_value; + return true; +} + +static bool king_inference_king_native_gpu_graph_scale_is_identity(zval *op) +{ + zval *factor = king_inference_array_find(op, "factor"); + zval *scale = king_inference_array_find(op, "scale"); + double value = 1.0; + + if (factor != NULL) { + if (Z_TYPE_P(factor) == IS_LONG) { + value = (double) Z_LVAL_P(factor); + } else if (Z_TYPE_P(factor) == IS_DOUBLE) { + value = Z_DVAL_P(factor); + } else { + return false; + } + if (value != 1.0) { + return false; + } + } + + if (scale != NULL) { + if (Z_TYPE_P(scale) == IS_LONG) { + value = (double) Z_LVAL_P(scale); + } else if (Z_TYPE_P(scale) == IS_DOUBLE) { + value = Z_DVAL_P(scale); + } else { + return false; + } + if (value != 1.0) { + return false; + } + } + + return true; +} + +static bool king_inference_king_native_gpu_graph_direct_token_id( + zval *graph, + zend_ulong *token_id +) { + zval *output = king_inference_array_find(graph, "output"); + zval *ops; + zval *entry; + + if (output == NULL || Z_TYPE_P(output) != IS_STRING) { + return false; + } + if (king_inference_king_native_gpu_graph_token_id_from_input(graph, Z_STR_P(output), token_id)) { + return true; + } + + ops = king_inference_array_find(graph, "ops"); + if (ops == NULL) { + ops = king_inference_array_find(graph, "steps"); + } + if (ops == NULL || Z_TYPE_P(ops) != IS_ARRAY) { + return false; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(ops), entry) { + zval *id; + zval *op; + zval *input; + + if (entry == NULL || Z_TYPE_P(entry) != IS_ARRAY) { + continue; + } + id = king_inference_array_find(entry, "id"); + op = king_inference_array_find(entry, "op"); + if (id == NULL + || Z_TYPE_P(id) != IS_STRING + || !zend_string_equals(Z_STR_P(id), Z_STR_P(output)) + || op == NULL + || Z_TYPE_P(op) != IS_STRING + || !zend_string_equals_literal(Z_STR_P(op), "scale") + || !king_inference_king_native_gpu_graph_scale_is_identity(entry)) { + continue; + } + input = king_inference_array_find(entry, "input"); + if (input != NULL + && Z_TYPE_P(input) == IS_STRING + && king_inference_king_native_gpu_graph_token_id_from_input(graph, Z_STR_P(input), token_id)) { + return true; + } + } ZEND_HASH_FOREACH_END(); + + return false; +} + +static zend_result king_inference_king_native_gpu_prepare_direct_token_event( + king_inference_model_object *model, + king_inference_stream_object *stream, + zval *graph, + zval *graph_result, + zend_string **token_text_out +) { + zend_ulong token_id; + zend_string *token_text; + + *token_text_out = NULL; + + if (!king_inference_king_native_gpu_graph_direct_token_id(graph, &token_id)) { + return SUCCESS; + } + + token_text = king_inference_tokenizer_decode_token(model, token_id); + if (token_text == NULL) { + return FAILURE; + } + + add_assoc_bool(graph_result, "token_result_ready", true); + add_assoc_bool(graph_result, "decoded_token_ready", true); + add_assoc_bool(graph_result, "direct_token_graph", true); + add_assoc_bool(graph_result, "synthetic_token_vector_graph", true); + add_assoc_bool(graph_result, "prompt_to_logits_inference", false); + add_assoc_string(graph_result, "result_source", "synthetic_token_vector_graph"); + add_assoc_string(graph_result, "token_selection_contract", "direct_token_id"); + if (token_id <= (zend_ulong) ZEND_LONG_MAX) { + add_assoc_long(graph_result, "selected_token_id", (zend_long) token_id); + } else { + add_assoc_double(graph_result, "selected_token_id", (double) token_id); + } + add_assoc_double(graph_result, "selected_token_probability", 1.0); + add_assoc_double(graph_result, "selected_token_logit", 0.0); + add_assoc_double(graph_result, "selected_token_rank", 0.0); + add_assoc_bool(graph_result, "selected_token_probability_synthetic", true); + add_assoc_bool(graph_result, "selected_token_logit_synthetic", true); + add_assoc_bool(graph_result, "selected_token_rank_synthetic", true); + + stream->native_decoder_token_count++; + stream->native_decoder_last_token_id = token_id; + stream->native_decoder_last_token_available = true; + stream->native_decoder_last_probability = 1.0; + stream->native_decoder_last_logit = 0.0; + stream->native_decoder_last_rank = 0.0; + stream->native_decoder_last_score_available = true; + + *token_text_out = token_text; + return SUCCESS; +} + +static zend_result king_inference_king_native_gpu_emit_graph_results( + king_inference_model_object *model, + king_inference_stream_object *stream +) { + zval *graph = king_inference_array_find(&stream->request, "graph"); + zval *graphs = king_inference_array_find(&stream->request, "graphs"); + zval *entry; + zval graph_result; + zend_long validated = 0; + + if (graph != NULL) { + zend_string *token_text = NULL; + + ZVAL_UNDEF(&graph_result); + if (king_inference_cuda_decoder_graph_executor_prepare_result(model, graph, &graph_result) != SUCCESS) { + return FAILURE; + } + if (king_inference_king_native_gpu_prepare_direct_token_event(model, stream, graph, &graph_result, &token_text) != SUCCESS) { + zval_ptr_dtor(&graph_result); + return FAILURE; + } + add_next_index_zval(&stream->native_events, &graph_result); + if (token_text != NULL) { + add_next_index_str(&stream->native_events, token_text); + } + validated++; + } + if (graphs != NULL) { + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(graphs), entry) { + zend_string *token_text = NULL; + + ZVAL_UNDEF(&graph_result); + if (entry == NULL || Z_TYPE_P(entry) != IS_ARRAY) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_request_invalid" + ); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_prepare_result(model, entry, &graph_result) != SUCCESS) { + return FAILURE; + } + if (king_inference_king_native_gpu_prepare_direct_token_event(model, stream, entry, &graph_result, &token_text) != SUCCESS) { + zval_ptr_dtor(&graph_result); + return FAILURE; + } + add_next_index_zval(&stream->native_events, &graph_result); + if (token_text != NULL) { + add_next_index_str(&stream->native_events, token_text); + } + validated++; + } ZEND_HASH_FOREACH_END(); + } + if (validated == 0) { + return SUCCESS; + } + + return SUCCESS; +} + +static bool king_inference_king_native_gpu_runtime_bool(zval *gpu_runtime, const char *key) +{ + zval *value; + + if (gpu_runtime == NULL || Z_TYPE_P(gpu_runtime) != IS_ARRAY) { + return false; + } + + value = king_inference_array_find(gpu_runtime, key); + return value != NULL && zend_is_true(value); +} + +static void king_inference_king_native_gpu_add_contract_event( + king_inference_model_object *model, + king_inference_stream_object *stream, + zend_long max_tokens +) { + zval decoder_blockers; + zval event; + zval gpu_runtime; + zval *reason; + bool gpu_generation_ready; + bool prompt_generation_ready; + + array_init(&event); + king_inference_gpu_runtime_status_from_model(model, &gpu_runtime); + gpu_generation_ready = king_inference_king_native_gpu_runtime_bool(&gpu_runtime, "generation_ready"); + prompt_generation_ready = gpu_generation_ready && model->cuda_decoder_prompt_loop_available; + + add_assoc_string(&event, "type", "gpu_decoder_stream_contract"); + add_assoc_string(&event, "backend", "king_native_gpu"); + add_assoc_string(&event, "stream_contract", "king_native_events"); + add_assoc_bool(&event, "native_stream", true); + add_assoc_bool(&event, "decoder_stream_contract_ready", true); + add_assoc_bool(&event, "embedding_row_loader_ready", model->cuda_embedding_row_available); + add_assoc_bool(&event, "device_vector_ops_ready", model->cuda_device_vector_ops_available); + add_assoc_bool(&event, "device_kv_cache_ready", model->cuda_device_kv_cache_available); + add_assoc_bool(&event, "decoder_graph_executor_ready", model->cuda_decoder_graph_executor_available); + add_assoc_bool( + &event, + "decoder_graph_result_contract_ready", + model->cuda_decoder_graph_executor_result_contract_available + ); + add_assoc_bool( + &event, + "decoder_graph_execution_plan_ready", + model->cuda_decoder_graph_executor_execution_plan_available + ); + add_assoc_bool( + &event, + "decoder_graph_embedding_execution_ready", + model->cuda_decoder_graph_executor_embedding_execution_available + ); + add_assoc_bool( + &event, + "decoder_graph_rms_norm_execution_ready", + model->cuda_decoder_graph_executor_rms_norm_execution_available + ); + add_assoc_bool( + &event, + "decoder_graph_linear_execution_ready", + model->cuda_decoder_graph_executor_linear_execution_available + ); + add_assoc_bool( + &event, + "decoder_graph_slice_execution_ready", + model->cuda_decoder_graph_executor_slice_execution_available + ); + add_assoc_bool( + &event, + "decoder_graph_rope_execution_ready", + model->cuda_decoder_graph_executor_rope_execution_available + ); + add_assoc_bool( + &event, + "decoder_graph_kv_head_prepare_execution_ready", + model->cuda_decoder_graph_executor_kv_head_prepare_execution_available + ); + add_assoc_bool( + &event, + "decoder_graph_kv_write_execution_ready", + model->cuda_decoder_graph_executor_kv_write_execution_available + ); + add_assoc_bool( + &event, + "decoder_graph_kv_attention_execution_ready", + model->cuda_decoder_graph_executor_kv_attention_execution_available + ); + add_assoc_bool( + &event, + "decoder_graph_attention_stack_slot_execution_ready", + model->cuda_decoder_graph_executor_attention_stack_slot_execution_available + ); + add_assoc_bool( + &event, + "decoder_graph_attention_heads_execution_ready", + model->cuda_decoder_graph_executor_attention_heads_execution_available + ); + add_assoc_bool( + &event, + "decoder_graph_attention_stack_execution_ready", + model->cuda_decoder_graph_executor_attention_stack_execution_available + ); + add_assoc_bool( + &event, + "decoder_graph_attention_output_projection_execution_ready", + model->cuda_decoder_graph_executor_attention_output_projection_execution_available + ); + add_assoc_bool( + &event, + "decoder_graph_attention_residual_execution_ready", + model->cuda_decoder_graph_executor_attention_residual_execution_available + ); + add_assoc_bool( + &event, + "decoder_graph_ffn_norm_execution_ready", + model->cuda_decoder_graph_executor_ffn_norm_execution_available + ); + add_assoc_bool( + &event, + "decoder_graph_ffn_gate_up_projection_execution_ready", + model->cuda_decoder_graph_executor_ffn_gate_up_projection_execution_available + ); + add_assoc_bool( + &event, + "decoder_graph_ffn_swiglu_execution_ready", + model->cuda_decoder_graph_executor_ffn_swiglu_execution_available + ); + add_assoc_bool( + &event, + "decoder_graph_ffn_down_projection_execution_ready", + model->cuda_decoder_graph_executor_ffn_down_projection_execution_available + ); + add_assoc_bool( + &event, + "decoder_graph_ffn_output_residual_execution_ready", + model->cuda_decoder_graph_executor_ffn_output_residual_execution_available + ); + add_assoc_bool( + &event, + "decoder_graph_final_norm_execution_ready", + model->cuda_decoder_graph_executor_final_norm_execution_available + ); + add_assoc_bool( + &event, + "decoder_graph_logits_projection_execution_ready", + model->cuda_decoder_graph_executor_logits_projection_execution_available + ); + add_assoc_bool(&event, "decoder_prompt_loop_ready", model->cuda_decoder_prompt_loop_available); + add_assoc_bool(&event, "decoder_kernel_ready", model->cuda_decoder_prompt_loop_available); + add_assoc_bool(&event, "gpu_generation_ready", gpu_generation_ready); + add_assoc_bool(&event, "generation_ready", prompt_generation_ready); + add_assoc_bool(&event, "plain_text_chat_ready", prompt_generation_ready); + add_assoc_bool(&event, "token_generation", prompt_generation_ready); + add_assoc_bool(&event, "openai_generation", prompt_generation_ready); + add_assoc_bool(&event, "silent_cpu_fallback", false); + add_assoc_bool(&event, "cpu_prompt_fallback_allowed", false); + add_assoc_string(&event, "fallback_mode", "refuse_cpu_fallback"); + add_assoc_long(&event, "accepted_max_tokens", max_tokens); + king_inference_gpu_decoder_blockers(model, &decoder_blockers); + add_assoc_zval(&event, "decoder_blockers", &decoder_blockers); + + reason = king_inference_array_find(&gpu_runtime, "reason"); + if (reason != NULL && Z_TYPE_P(reason) == IS_STRING) { + add_assoc_str(&event, "reason", zend_string_copy(Z_STR_P(reason))); + } + add_assoc_zval(&event, "gpu_runtime", &gpu_runtime); + add_next_index_zval(&stream->native_events, &event); +} + +static zend_result king_inference_king_native_gpu_stream_start( + king_inference_stream_object *stream, + const char *function_name +) { + king_inference_model_object *model = php_king_inference_model_obj_from_zend(Z_OBJ(stream->model)); + zend_long max_tokens; + zval *prompt_text; + + if (king_inference_check_gpu_policy_before_run(stream, function_name) != SUCCESS) { + return FAILURE; + } + if (king_inference_gpu_runtime_require_config_ready(model, function_name) != SUCCESS) { + return FAILURE; + } + if (king_inference_king_native_gpu_validate_stream_request(stream, function_name, &max_tokens) != SUCCESS) { + return FAILURE; + } + prompt_text = king_inference_array_find(&stream->request, "native_prompt_text"); + if (prompt_text != NULL && king_inference_gpu_runtime_require_generation_ready(model, function_name) != SUCCESS) { + return FAILURE; + } + + if (!Z_ISUNDEF(stream->native_events)) { + zval_ptr_dtor(&stream->native_events); + } + array_init(&stream->native_events); + stream->native_event_index = 0; + + if (king_inference_king_native_stream_diagnostics_enabled(stream)) { + king_inference_king_native_gpu_add_contract_event(model, stream, max_tokens); + } + { + if (prompt_text != NULL + && king_inference_cuda_decoder_prompt_stream_start( + model, + stream, + max_tokens + ) != SUCCESS) { + king_set_error( + model->cuda_decoder_prompt_loop_error[0] != '\0' + ? model->cuda_decoder_prompt_loop_error + : "King native GPU prompt generation failed." + ); + return FAILURE; + } + if (prompt_text == NULL + && king_inference_king_native_gpu_emit_graph_results(model, stream) != SUCCESS) { + king_set_error( + model->cuda_decoder_graph_executor_error[0] != '\0' + ? model->cuda_decoder_graph_executor_error + : "King native GPU graph execution failed." + ); + return FAILURE; + } + } + return SUCCESS; +} + +static void king_inference_king_native_gpu_model_info( + king_inference_model_object *intern, + zval *return_value +) { + zval decoder_blockers; + zval *gpu_runtime; + bool gpu_config_ready; + bool gpu_generation_ready; + bool prompt_generation_ready; + + gpu_runtime = king_inference_array_find(return_value, "gpu_runtime"); + gpu_config_ready = king_inference_king_native_gpu_runtime_bool(gpu_runtime, "config_ready"); + gpu_generation_ready = king_inference_king_native_gpu_runtime_bool(gpu_runtime, "generation_ready"); + prompt_generation_ready = gpu_generation_ready && intern->cuda_decoder_prompt_loop_available; + + add_assoc_string(return_value, "engine", "king_native_gpu"); + add_assoc_string(return_value, "loader", "king_gguf_structure"); + add_assoc_string(return_value, "stream_contract", "king_native_events"); + add_assoc_bool(return_value, "external_runtime", false); + add_assoc_bool(return_value, "decoder_stream_contract_ready", true); + add_assoc_bool(return_value, "embedding_row_loader_ready", intern->cuda_embedding_row_available); + add_assoc_bool(return_value, "device_vector_ops_ready", intern->cuda_device_vector_ops_available); + add_assoc_bool(return_value, "device_kv_cache_ready", intern->cuda_device_kv_cache_available); + add_assoc_bool(return_value, "decoder_graph_executor_ready", intern->cuda_decoder_graph_executor_available); + add_assoc_bool( + return_value, + "decoder_graph_result_contract_ready", + intern->cuda_decoder_graph_executor_result_contract_available + ); + add_assoc_bool( + return_value, + "decoder_graph_execution_plan_ready", + intern->cuda_decoder_graph_executor_execution_plan_available + ); + add_assoc_bool( + return_value, + "decoder_graph_embedding_execution_ready", + intern->cuda_decoder_graph_executor_embedding_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_rms_norm_execution_ready", + intern->cuda_decoder_graph_executor_rms_norm_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_linear_execution_ready", + intern->cuda_decoder_graph_executor_linear_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_slice_execution_ready", + intern->cuda_decoder_graph_executor_slice_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_rope_execution_ready", + intern->cuda_decoder_graph_executor_rope_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_kv_head_prepare_execution_ready", + intern->cuda_decoder_graph_executor_kv_head_prepare_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_kv_write_execution_ready", + intern->cuda_decoder_graph_executor_kv_write_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_kv_attention_execution_ready", + intern->cuda_decoder_graph_executor_kv_attention_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_attention_stack_slot_execution_ready", + intern->cuda_decoder_graph_executor_attention_stack_slot_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_attention_heads_execution_ready", + intern->cuda_decoder_graph_executor_attention_heads_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_attention_stack_execution_ready", + intern->cuda_decoder_graph_executor_attention_stack_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_attention_output_projection_execution_ready", + intern->cuda_decoder_graph_executor_attention_output_projection_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_attention_residual_execution_ready", + intern->cuda_decoder_graph_executor_attention_residual_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_ffn_norm_execution_ready", + intern->cuda_decoder_graph_executor_ffn_norm_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_ffn_gate_up_projection_execution_ready", + intern->cuda_decoder_graph_executor_ffn_gate_up_projection_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_ffn_swiglu_execution_ready", + intern->cuda_decoder_graph_executor_ffn_swiglu_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_ffn_down_projection_execution_ready", + intern->cuda_decoder_graph_executor_ffn_down_projection_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_ffn_output_residual_execution_ready", + intern->cuda_decoder_graph_executor_ffn_output_residual_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_final_norm_execution_ready", + intern->cuda_decoder_graph_executor_final_norm_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_logits_projection_execution_ready", + intern->cuda_decoder_graph_executor_logits_projection_execution_available + ); + add_assoc_bool(return_value, "decoder_prompt_loop_ready", intern->cuda_decoder_prompt_loop_available); + add_assoc_bool(return_value, "decoder_prompt_batch_prefill_compiled", true); + add_assoc_bool(return_value, "decoder_prompt_batch_prefill_available", intern->cuda_decoder_prompt_loop_batch_prefill_available); + add_assoc_bool(return_value, "decoder_prompt_batch_prefill_ready", king_inference_cuda_decoder_prompt_loop_batch_prefill_ready(intern)); + add_assoc_bool(return_value, "decoder_prompt_batch_prefill_experimental_enabled", king_inference_cuda_decoder_prompt_loop_batch_prefill_experimental_enabled(intern)); + add_assoc_bool(return_value, "decoder_prompt_batch_prefill_admitted", king_inference_cuda_decoder_prompt_loop_batch_prefill_admitted(intern)); + add_assoc_string(return_value, "decoder_prompt_batch_prefill_status", king_inference_cuda_decoder_prompt_loop_batch_prefill_status(intern)); + add_assoc_string( + return_value, + "decoder_prompt_batch_prefill_admission_reason", + king_inference_cuda_decoder_prompt_loop_batch_prefill_admission_reason(intern) + ); + add_assoc_bool(return_value, "decoder_prompt_batch_prefill_last_used", intern->cuda_decoder_prompt_loop_last_used_batch_prefill); + add_assoc_long( + return_value, + "decoder_prompt_batch_prefill_last_tokens", + (zend_long) intern->cuda_decoder_prompt_loop_last_prefill_tokens + ); + add_assoc_long( + return_value, + "decoder_prompt_batch_prefill_last_template_reuses", + (zend_long) intern->cuda_decoder_prompt_loop_last_prefill_template_reuses + ); + add_assoc_bool(return_value, "decoder_kernel_ready", intern->cuda_decoder_prompt_loop_available); + add_assoc_bool(return_value, "gpu_config_ready", gpu_config_ready); + add_assoc_bool(return_value, "gpu_generation_ready", gpu_generation_ready); + add_assoc_bool(return_value, "generation_ready", prompt_generation_ready); + add_assoc_bool(return_value, "plain_text_chat_ready", prompt_generation_ready); + add_assoc_bool(return_value, "token_generation", prompt_generation_ready); + add_assoc_bool(return_value, "openai_generation", prompt_generation_ready); + king_inference_gpu_decoder_blockers(intern, &decoder_blockers); + add_assoc_zval(return_value, "decoder_blockers", &decoder_blockers); + add_assoc_bool(return_value, "silent_cpu_fallback", false); + add_assoc_bool(return_value, "cpu_prompt_fallback_allowed", false); + add_assoc_string(return_value, "fallback_mode", "refuse_cpu_fallback"); +} diff --git a/extension/src/inference/backends/native/backend_king_native_prompt.inc b/extension/src/inference/backends/native/backend_king_native_prompt.inc new file mode 100644 index 000000000..279a27f4e --- /dev/null +++ b/extension/src/inference/backends/native/backend_king_native_prompt.inc @@ -0,0 +1,547 @@ +static bool king_inference_king_native_prompt_should_stop_token( + king_inference_model_object *model, + zend_ulong token_id) +{ + return (model->gguf.tokenizer_eos_id >= 0 && token_id == (zend_ulong) model->gguf.tokenizer_eos_id) + || (model->gguf.tokenizer_bos_id >= 0 && token_id == (zend_ulong) model->gguf.tokenizer_bos_id); +} + +static const char *king_inference_king_native_memmem( + const char *haystack, + size_t haystack_len, + const char *needle, + size_t needle_len) +{ + size_t offset; + + if (needle_len == 0 || haystack_len < needle_len) { + return NULL; + } + for (offset = 0; offset <= haystack_len - needle_len; offset++) { + if (memcmp(haystack + offset, needle, needle_len) == 0) { + return haystack + offset; + } + } + return NULL; +} + +static zend_result king_inference_king_native_prompt_stop_list(zval *request, zval *stops) +{ + zval *stop = king_inference_array_find(request, "stop"); + zval *entry; + + array_init(stops); + if (stop == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(stop) == IS_STRING) { + add_next_index_str(stops, zend_string_copy(Z_STR_P(stop))); + return SUCCESS; + } + if (!king_inference_openai_array_is_list(stop)) { + zval_ptr_dtor(stops); + zend_throw_exception_ex(king_ce_validation_exception, 0, "King native prompt stop must be a string or list of strings."); + return FAILURE; + } + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(stop), entry) { + if (Z_TYPE_P(entry) != IS_STRING || Z_STRLEN_P(entry) == 0) { + zval_ptr_dtor(stops); + zend_throw_exception_ex(king_ce_validation_exception, 0, "King native prompt stop entries must be non-empty strings."); + return FAILURE; + } + add_next_index_str(stops, zend_string_copy(Z_STR_P(entry))); + } ZEND_HASH_FOREACH_END(); + return SUCCESS; +} + +static bool king_inference_king_native_prompt_stop_exists(zval *stops, const char *stop) +{ + zval *entry; + size_t stop_length = strlen(stop); + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(stops), entry) { + if (Z_TYPE_P(entry) == IS_STRING + && Z_STRLEN_P(entry) == stop_length + && memcmp(Z_STRVAL_P(entry), stop, stop_length) == 0) { + return true; + } + } ZEND_HASH_FOREACH_END(); + return false; +} + +static void king_inference_king_native_prompt_add_default_stops( + king_inference_model_object *model, + zval *stops) +{ + static const char *turn_stops[] = { + "", + "<|turn>", + "", + "" + }; + size_t index; + + if (Z_TYPE(model->tokenizer_lookup) != IS_ARRAY) { + return; + } + for (index = 0; index < sizeof(turn_stops) / sizeof(turn_stops[0]); index++) { + const char *stop = turn_stops[index]; + size_t stop_length = strlen(stop); + + if (zend_hash_str_exists(Z_ARRVAL(model->tokenizer_lookup), stop, stop_length) + && !king_inference_king_native_prompt_stop_exists(stops, stop)) { + add_next_index_stringl(stops, stop, stop_length); + } + } +} + +static bool king_inference_king_native_prompt_piece_matches_stop(zend_string *piece, zval *stops) +{ + zval *stop; + + if (piece == NULL || ZSTR_LEN(piece) == 0 || stops == NULL || Z_TYPE_P(stops) != IS_ARRAY) { + return false; + } + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(stops), stop) { + if (Z_TYPE_P(stop) == IS_STRING + && ZSTR_LEN(piece) == Z_STRLEN_P(stop) + && memcmp(ZSTR_VAL(piece), Z_STRVAL_P(stop), ZSTR_LEN(piece)) == 0) { + return true; + } + } ZEND_HASH_FOREACH_END(); + return false; +} + +static size_t king_inference_king_native_prompt_pending_keep(smart_str *pending, zval *stops) +{ + zval *stop; + size_t keep = 0; + + if (pending->s == NULL) { + return 0; + } + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(stops), stop) { + size_t max_len; + size_t length; + + if (Z_TYPE_P(stop) != IS_STRING || Z_STRLEN_P(stop) <= 1) { + continue; + } + max_len = ZSTR_LEN(pending->s) < Z_STRLEN_P(stop) - 1 ? ZSTR_LEN(pending->s) : Z_STRLEN_P(stop) - 1; + for (length = max_len; length > 0; length--) { + if (memcmp(ZSTR_VAL(pending->s) + ZSTR_LEN(pending->s) - length, Z_STRVAL_P(stop), length) == 0) { + if (length > keep) { + keep = length; + } + break; + } + } + } ZEND_HASH_FOREACH_END(); + return keep; +} + +static zend_result king_inference_king_native_prompt_emit_text( + king_inference_stream_object *stream, + smart_str *pending, + zend_string *piece, + zval *stops, + bool *stopped) +{ + zval *stop; + const char *stop_at = NULL; + size_t stop_offset = 0; + size_t keep; + size_t emit_len; + + *stopped = false; + if (piece == NULL || ZSTR_LEN(piece) == 0) { + return SUCCESS; + } + if (zend_hash_num_elements(Z_ARRVAL_P(stops)) == 0) { + add_next_index_str(&stream->native_events, zend_string_copy(piece)); + return SUCCESS; + } + + smart_str_append(pending, piece); + smart_str_0(pending); + if (pending->s == NULL) { + return SUCCESS; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(stops), stop) { + const char *match; + + if (Z_TYPE_P(stop) != IS_STRING || Z_STRLEN_P(stop) == 0) { + continue; + } + match = king_inference_king_native_memmem( + ZSTR_VAL(pending->s), + ZSTR_LEN(pending->s), + Z_STRVAL_P(stop), + Z_STRLEN_P(stop) + ); + if (match != NULL && (stop_at == NULL || match < stop_at)) { + stop_at = match; + } + } ZEND_HASH_FOREACH_END(); + + if (stop_at != NULL) { + stop_offset = (size_t) (stop_at - ZSTR_VAL(pending->s)); + if (stop_offset > 0) { + add_next_index_stringl(&stream->native_events, ZSTR_VAL(pending->s), stop_offset); + } + smart_str_free(pending); + *stopped = true; + return SUCCESS; + } + + keep = king_inference_king_native_prompt_pending_keep(pending, stops); + emit_len = ZSTR_LEN(pending->s) - keep; + if (emit_len > 0) { + add_next_index_stringl(&stream->native_events, ZSTR_VAL(pending->s), emit_len); + } + if (keep > 0) { + zend_string *suffix = zend_string_init(ZSTR_VAL(pending->s) + emit_len, keep, 0); + smart_str_free(pending); + smart_str_append(pending, suffix); + zend_string_release(suffix); + } else { + smart_str_free(pending); + } + + return SUCCESS; +} + +static zend_result king_inference_king_native_prompt_flush_pending( + king_inference_stream_object *stream, + smart_str *pending) +{ + if (pending->s != NULL && ZSTR_LEN(pending->s) > 0) { + add_next_index_str(&stream->native_events, zend_string_copy(pending->s)); + } + smart_str_free(pending); + return SUCCESS; +} + +static zend_result king_inference_king_native_prompt_tokens( + king_inference_model_object *model, + zval *encoded, + zval *tokenized_prompt, + zend_ulong *count_out) +{ + zval *tokens = king_inference_array_find(encoded, "tokens"); + zval prompt_tokens; + zval *entry; + zend_ulong source_count; + zend_ulong source_index = 0; + zend_ulong copied = 0; + zend_ulong context = model->gguf.architecture_params[KING_INFERENCE_ARCH_CONTEXT_LENGTH]; + zend_ulong start = 0; + zend_ulong tail_count; + zend_long first_token = -1; + bool has_bos = model->gguf.tokenizer_bos_id >= 0; + bool source_starts_with_bos = false; + bool add_bos = false; + + if (tokens == NULL || Z_TYPE_P(tokens) != IS_ARRAY) { + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King native prompt tokenizer returned no tokens array."); + return FAILURE; + } + source_count = (zend_ulong) zend_hash_num_elements(Z_ARRVAL_P(tokens)); + entry = zend_hash_index_find(Z_ARRVAL_P(tokens), 0); + if (entry != NULL && Z_TYPE_P(entry) == IS_LONG) { + first_token = Z_LVAL_P(entry); + source_starts_with_bos = has_bos && first_token == model->gguf.tokenizer_bos_id; + } + add_bos = has_bos && !source_starts_with_bos; + + if (context > 0 && source_count + (add_bos ? 1 : 0) > context) { + if (has_bos && context > 1) { + add_bos = true; + tail_count = context - 1; + if (source_starts_with_bos && source_count > 0) { + source_count--; + start = 1; + } + if (source_count > tail_count) { + start += source_count - tail_count; + } + } else { + add_bos = false; + if (source_count > context) { + start = source_count - context; + } + } + } + + array_init(tokenized_prompt); + array_init(&prompt_tokens); + if (add_bos) { + add_next_index_long(&prompt_tokens, model->gguf.tokenizer_bos_id); + copied++; + } + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(tokens), entry) { + if (source_index++ < start) { + continue; + } + if (Z_TYPE_P(entry) != IS_LONG || Z_LVAL_P(entry) < 0) { + zval_ptr_dtor(&prompt_tokens); + zval_ptr_dtor(tokenized_prompt); + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King native prompt tokenizer returned an invalid token id."); + return FAILURE; + } + add_next_index_long(&prompt_tokens, Z_LVAL_P(entry)); + copied++; + } ZEND_HASH_FOREACH_END(); + + if (copied == 0) { + zval_ptr_dtor(&prompt_tokens); + zval_ptr_dtor(tokenized_prompt); + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King native prompt produced no tokens."); + return FAILURE; + } + + add_assoc_zval(tokenized_prompt, "tokens", &prompt_tokens); + *count_out = copied; + return SUCCESS; +} + +static void king_inference_king_native_prompt_decode_options(king_inference_stream_object *stream, zval *decode_options) +{ + array_init(decode_options); + king_inference_king_native_copy_decode_option(decode_options, &stream->request, "temperature"); + king_inference_king_native_copy_decode_option(decode_options, &stream->request, "top_k"); + king_inference_king_native_copy_decode_option(decode_options, &stream->request, "top_p"); + king_inference_king_native_copy_decode_option(decode_options, &stream->request, "seed"); +} + +static zend_result king_inference_king_native_prompt_run_graph( + king_inference_model_object *model, + king_inference_stream_object *stream, + zval *graph, + zval *graph_options, + zval *state, + zend_ulong *token_id_out) +{ + zval working_graph; + zval graph_result; + zval *next_state; + zend_result status; + + ZVAL_DUP(&working_graph, graph); + SEPARATE_ARRAY(&working_graph); + if (!Z_ISUNDEF_P(state) && king_inference_array_find(&working_graph, "state") == NULL) { + zval state_copy; + ZVAL_COPY(&state_copy, state); + add_assoc_zval(&working_graph, "state", &state_copy); + } + + ZVAL_UNDEF(&graph_result); + status = king_inference_graph_run_array(model, &working_graph, graph_options, &graph_result); + if (status != SUCCESS) { + zval_ptr_dtor(&working_graph); + return FAILURE; + } + next_state = king_inference_array_find(&graph_result, "state"); + if (next_state == NULL || Z_TYPE_P(next_state) != IS_ARRAY) { + zval_ptr_dtor(&graph_result); + zval_ptr_dtor(&working_graph); + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King native prompt graph produced no KV state."); + return FAILURE; + } + if (token_id_out != NULL + && king_inference_king_native_token_id_from_result(&graph_result, &working_graph, token_id_out) != SUCCESS) { + zval_ptr_dtor(&graph_result); + zval_ptr_dtor(&working_graph); + return FAILURE; + } + if (token_id_out != NULL) { + king_inference_king_native_update_decoder_metrics(stream, &graph_result, &working_graph, *token_id_out); + } + if (!Z_ISUNDEF_P(state)) { + zval_ptr_dtor(state); + } + ZVAL_COPY(state, next_state); + zval_ptr_dtor(&graph_result); + zval_ptr_dtor(&working_graph); + return SUCCESS; +} + +static zend_result king_inference_king_native_append_prompt_generated_tokens( + king_inference_model_object *model, + king_inference_stream_object *stream, + zval *prompt_text, + zval *graph_options, + zend_long max_tokens) +{ + zval encoded; + zval tokenized_prompt; + zval decode_options; + zval stops; + zval state; + zend_ulong token_count = 0; + zend_ulong position; + zend_ulong token_id = 0; + zend_long generated = 0; + smart_str pending = {0}; + bool stopped = false; + + ZVAL_UNDEF(&encoded); + ZVAL_UNDEF(&tokenized_prompt); + ZVAL_UNDEF(&decode_options); + ZVAL_UNDEF(&stops); + ZVAL_UNDEF(&state); + + if (prompt_text == NULL || Z_TYPE_P(prompt_text) != IS_STRING) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King native prompt generation requires native_prompt_text."); + return FAILURE; + } + if (king_inference_tokenizer_encode(model, Z_STR_P(prompt_text), &encoded) != SUCCESS + || king_inference_king_native_prompt_tokens(model, &encoded, &tokenized_prompt, &token_count) != SUCCESS) { + if (!Z_ISUNDEF(encoded)) { + zval_ptr_dtor(&encoded); + } + return FAILURE; + } + if (king_inference_king_native_prompt_stop_list(&stream->request, &stops) != SUCCESS) { + zval_ptr_dtor(&tokenized_prompt); + zval_ptr_dtor(&encoded); + return FAILURE; + } + king_inference_king_native_prompt_add_default_stops(model, &stops); + king_inference_king_native_prompt_decode_options(stream, &decode_options); + ZVAL_UNDEF(&state); + + for (position = 0; position < token_count; position++) { + zval graph; + zval *emit_token; + + ZVAL_UNDEF(&graph); + add_assoc_bool(&decode_options, "emit_token", position + 1 == token_count); + if (king_inference_token_decode_graph_array(model, &tokenized_prompt, position, &decode_options, &graph) != SUCCESS) { + zval_ptr_dtor(&decode_options); + zval_ptr_dtor(&stops); + zval_ptr_dtor(&tokenized_prompt); + zval_ptr_dtor(&encoded); + if (!Z_ISUNDEF(state)) { + zval_ptr_dtor(&state); + } + smart_str_free(&pending); + return FAILURE; + } + emit_token = king_inference_array_find(&graph, "terminal"); + if (emit_token != NULL) { + emit_token = king_inference_array_find(emit_token, "emits_token"); + } + if (king_inference_king_native_prompt_run_graph( + model, + stream, + &graph, + graph_options, + &state, + emit_token != NULL && zend_is_true(emit_token) ? &token_id : NULL + ) != SUCCESS) { + zval_ptr_dtor(&graph); + zval_ptr_dtor(&decode_options); + zval_ptr_dtor(&stops); + zval_ptr_dtor(&tokenized_prompt); + zval_ptr_dtor(&encoded); + if (!Z_ISUNDEF(state)) { + zval_ptr_dtor(&state); + } + smart_str_free(&pending); + return FAILURE; + } + zval_ptr_dtor(&graph); + } + + while (generated < max_tokens && !stopped) { + zval graph; + zval token; + zend_string *piece; + + ZVAL_UNDEF(&graph); + if (king_inference_king_native_prompt_should_stop_token(model, token_id)) { + break; + } + piece = king_inference_tokenizer_decode_token(model, token_id); + if (piece == NULL) { + zval_ptr_dtor(&decode_options); + zval_ptr_dtor(&stops); + zval_ptr_dtor(&tokenized_prompt); + zval_ptr_dtor(&encoded); + if (!Z_ISUNDEF(state)) { + zval_ptr_dtor(&state); + } + smart_str_free(&pending); + return FAILURE; + } + if (king_inference_king_native_prompt_emit_text(stream, &pending, piece, &stops, &stopped) != SUCCESS) { + zend_string_release(piece); + zval_ptr_dtor(&decode_options); + zval_ptr_dtor(&stops); + zval_ptr_dtor(&tokenized_prompt); + zval_ptr_dtor(&encoded); + if (!Z_ISUNDEF(state)) { + zval_ptr_dtor(&state); + } + smart_str_free(&pending); + return FAILURE; + } + zend_string_release(piece); + generated++; + if (generated >= max_tokens || stopped) { + break; + } + if (position >= (zend_ulong) ZEND_LONG_MAX || token_id > (zend_ulong) ZEND_LONG_MAX) { + zval_ptr_dtor(&decode_options); + zval_ptr_dtor(&stops); + zval_ptr_dtor(&tokenized_prompt); + zval_ptr_dtor(&encoded); + if (!Z_ISUNDEF(state)) { + zval_ptr_dtor(&state); + } + smart_str_free(&pending); + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King native prompt generation exceeded supported token or position limits."); + return FAILURE; + } + + ZVAL_LONG(&token, (zend_long) token_id); + add_assoc_bool(&decode_options, "emit_token", true); + if (king_inference_token_decode_graph_array(model, &token, position, &decode_options, &graph) != SUCCESS + || king_inference_king_native_prompt_run_graph(model, stream, &graph, graph_options, &state, &token_id) != SUCCESS) { + if (!Z_ISUNDEF(graph)) { + zval_ptr_dtor(&graph); + } + zval_ptr_dtor(&decode_options); + zval_ptr_dtor(&stops); + zval_ptr_dtor(&tokenized_prompt); + zval_ptr_dtor(&encoded); + if (!Z_ISUNDEF(state)) { + zval_ptr_dtor(&state); + } + smart_str_free(&pending); + return FAILURE; + } + position++; + zval_ptr_dtor(&graph); + } + + if (!stopped) { + king_inference_king_native_prompt_flush_pending(stream, &pending); + } else { + smart_str_free(&pending); + } + if (zend_hash_num_elements(Z_ARRVAL(stream->native_events)) == 0) { + add_next_index_stringl(&stream->native_events, "", 0); + } + + zval_ptr_dtor(&decode_options); + zval_ptr_dtor(&stops); + zval_ptr_dtor(&tokenized_prompt); + zval_ptr_dtor(&encoded); + if (!Z_ISUNDEF(state)) { + zval_ptr_dtor(&state); + } + return SUCCESS; +} diff --git a/extension/src/inference/backends/registry/backend_registry.inc b/extension/src/inference/backends/registry/backend_registry.inc new file mode 100644 index 000000000..d044f5107 --- /dev/null +++ b/extension/src/inference/backends/registry/backend_registry.inc @@ -0,0 +1,176 @@ +static zend_result king_inference_backend_validate_model_config(zval *config, const char *function_name) +{ + king_inference_backend_kind kind; + + if (king_inference_backend_kind_from_config(config, &kind, function_name) != SUCCESS) { + return FAILURE; + } + + if (!king_inference_backend_kind_allows_model_registration(kind)) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() backend '%s' cannot register King inference models in this build.", + function_name, + king_inference_backend_kind_name(kind) + ); + return FAILURE; + } + + if (kind == KING_INFERENCE_BACKEND_LOCAL + && king_inference_king_local_validate_model_config(config, function_name) != SUCCESS) { + return FAILURE; + } + + return SUCCESS; +} + +static zend_result king_inference_stream_start(king_inference_stream_object *stream, const char *function_name) +{ + king_inference_model_object *model; + king_inference_backend_kind kind; + + model = php_king_inference_model_obj_from_zend(Z_OBJ(stream->model)); + if (king_inference_backend_kind_from_config(&model->config, &kind, function_name) != SUCCESS) { + return FAILURE; + } + + king_inference_stream_model_runtime_started(stream); + switch (kind) { + case KING_INFERENCE_BACKEND_LOCAL: + return king_inference_king_local_stream_start(stream, function_name); + + case KING_INFERENCE_BACKEND_KING_NATIVE_CPU: + return king_inference_king_native_cpu_stream_start(stream, function_name); + + case KING_INFERENCE_BACKEND_KING_NATIVE_GPU: + return king_inference_king_native_gpu_stream_start(stream, function_name); + } + + zend_throw_exception_ex(king_ce_runtime_exception, 0, "%s() resolved an unknown inference backend.", function_name); + return FAILURE; +} + +static const char *king_inference_model_last_start_error(king_inference_model_object *model) +{ + const char *last_error = king_get_error(); + + if (last_error != NULL && last_error[0] != '\0') { + return last_error; + } + if (model == NULL) { + return NULL; + } + if (model->cuda_decoder_graph_executor_error[0] != '\0') { + return model->cuda_decoder_graph_executor_error; + } + if (model->cuda_decoder_prompt_loop_error[0] != '\0') { + return model->cuda_decoder_prompt_loop_error; + } + if (model->cuda_rope_error[0] != '\0') { + return model->cuda_rope_error; + } + if (model->cuda_device_allocator_error[0] != '\0') { + return model->cuda_device_allocator_error; + } + if (model->cuda_context_error[0] != '\0') { + return model->cuda_context_error; + } + if (model->cuda_weight_upload_error[0] != '\0') { + return model->cuda_weight_upload_error; + } + if (model->cuda_quantized_matvec_error[0] != '\0') { + return model->cuda_quantized_matvec_error; + } + if (model->cuda_rms_norm_error[0] != '\0') { + return model->cuda_rms_norm_error; + } + if (model->cuda_embedding_row_error[0] != '\0') { + return model->cuda_embedding_row_error; + } + if (model->cuda_device_vector_ops_error[0] != '\0') { + return model->cuda_device_vector_ops_error; + } + if (model->cuda_device_kv_cache_error[0] != '\0') { + return model->cuda_device_kv_cache_error; + } + if (model->cuda_attention_scores_error[0] != '\0') { + return model->cuda_attention_scores_error; + } + if (model->cuda_attention_softmax_error[0] != '\0') { + return model->cuda_attention_softmax_error; + } + if (model->cuda_attention_values_error[0] != '\0') { + return model->cuda_attention_values_error; + } + if (model->cuda_ffn_swiglu_error[0] != '\0') { + return model->cuda_ffn_swiglu_error; + } + if (model->cuda_output_projection_error[0] != '\0') { + return model->cuda_output_projection_error; + } + if (model->cuda_logits_readback_error[0] != '\0') { + return model->cuda_logits_readback_error; + } + + return NULL; +} + +static void king_inference_stream_throw_start_failure( + king_inference_stream_object *stream, + const char *function_name +) { + king_inference_model_object *model = NULL; + const char *message; + + if (EG(exception)) { + return; + } + if (stream != NULL + && !Z_ISUNDEF(stream->model) + && Z_TYPE(stream->model) == IS_OBJECT + && Z_OBJCE(stream->model) == king_ce_inference_model) { + model = php_king_inference_model_obj_from_zend(Z_OBJ(stream->model)); + } + + message = king_inference_model_last_start_error(model); + if (message == NULL || message[0] == '\0') { + message = "King inference stream start failed."; + } + king_set_error(message); + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "%s() failed to start King inference stream: %s", + function_name, + message + ); +} + +static void king_inference_backend_model_info(king_inference_model_object *intern, zval *return_value) +{ + king_inference_backend_kind kind; + zval capabilities; + + if (king_inference_backend_kind_from_config(&intern->config, &kind, "King\\Inference\\Model::info") != SUCCESS) { + kind = KING_INFERENCE_BACKEND_KING_NATIVE_CPU; + } + + add_assoc_string(return_value, "backend", king_inference_backend_kind_name(kind)); + + if (kind == KING_INFERENCE_BACKEND_LOCAL) { + zend_string *runner = king_inference_king_local_runner_path(intern); + bool runner_executable = king_inference_king_local_runner_path_is_executable(runner); + add_assoc_str(return_value, "runner_path", runner); + add_assoc_string(return_value, "engine", "king_local_process"); + add_assoc_string(return_value, "runner_protocol", "king-local-argv"); + add_assoc_bool(return_value, "runner_executable", runner_executable); + } else if (kind == KING_INFERENCE_BACKEND_KING_NATIVE_CPU) { + king_inference_king_native_model_info(intern, return_value); + } else if (kind == KING_INFERENCE_BACKEND_KING_NATIVE_GPU) { + king_inference_king_native_gpu_model_info(intern, return_value); + } + + king_inference_backend_capabilities_array(kind, &capabilities); + add_assoc_zval(return_value, "backend_capabilities", &capabilities); +} diff --git a/extension/src/inference/binding/bootstrap/forward_declarations.inc b/extension/src/inference/binding/bootstrap/forward_declarations.inc new file mode 100644 index 000000000..0f5e837a2 --- /dev/null +++ b/extension/src/inference/binding/bootstrap/forward_declarations.inc @@ -0,0 +1,7 @@ +static zend_result king_inference_llm_cache_prepare_stream( + king_inference_model_object *model, + king_inference_stream_object *stream, + zval *graph_options, + bool with_memory, + const char *function_name); +static void king_inference_stream_model_runtime_started(king_inference_stream_object *stream); diff --git a/extension/src/inference/binding/bootstrap/platform_headers.inc b/extension/src/inference/binding/bootstrap/platform_headers.inc new file mode 100644 index 000000000..759ec2043 --- /dev/null +++ b/extension/src/inference/binding/bootstrap/platform_headers.inc @@ -0,0 +1,26 @@ +#include "Zend/zend_smart_str.h" +#include "ext/standard/base64.h" +#include "ext/json/php_json.h" +#include "config/config.h" +#include "config/high_perf_compute_and_ai/base_layer.h" +#include "config/resource_helpers.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/extension/src/inference/binding/php_binding.inc b/extension/src/inference/binding/php_binding.inc new file mode 100644 index 000000000..c2d07fdc4 --- /dev/null +++ b/extension/src/inference/binding/php_binding.inc @@ -0,0 +1,17 @@ +/* + * Inference PHP binding aggregation. + * + * Keep leaf implementation fragments in this module boundary. The owning + * inference.c entrypoint stays focused on module state, binding, and + * registration ownership. + */ + +#include "bootstrap/platform_headers.inc" +#include "bootstrap/forward_declarations.inc" +#include "stacks/request/request_parsing_stack.inc" +#include "stacks/runtime/runtime_foundation_stack.inc" +#include "stacks/model/model_tensor_stack.inc" +#include "stacks/cuda/cuda_decoder_stack.inc" +#include "stacks/stream/stream_backend_stack.inc" +#include "stacks/openai/openai_router_stack.inc" +#include "stacks/api/api_surface_stack.inc" diff --git a/extension/src/inference/binding/stacks/api/api_surface_stack.inc b/extension/src/inference/binding/stacks/api/api_surface_stack.inc new file mode 100644 index 000000000..7bf760129 --- /dev/null +++ b/extension/src/inference/binding/stacks/api/api_surface_stack.inc @@ -0,0 +1,2 @@ +#include "../../../api/surface/api.inc" +#include "inference/classes/class_method_entries.h" diff --git a/extension/src/inference/binding/stacks/cuda/cuda_decoder_stack.inc b/extension/src/inference/binding/stacks/cuda/cuda_decoder_stack.inc new file mode 100644 index 000000000..5e08e7a3a --- /dev/null +++ b/extension/src/inference/binding/stacks/cuda/cuda_decoder_stack.inc @@ -0,0 +1,21 @@ +#include "../../../cuda/runtime/weights/cuda_weight_upload.inc" +#include "../../../cuda/kernels/matvec/cuda_quantized_matvec.inc" +#include "../../../cuda/kernels/matvec/cuda_quantized_matvec_batch.inc" +#include "../../../cuda/kernels/matvec/cuda_quantized_matvec_status.inc" +#include "../../../cuda/kernels/embedding/cuda_embedding_row.inc" +#include "../../../cuda/kernels/device/cuda_numeric_tolerance_contract.inc" +#include "../../../cuda/kernels/device/cuda_device_vector_ops.inc" +#include "../../../cuda/kernels/device/cuda_device_kv_cache.inc" +#include "../../../cuda/kernels/embedding/cuda_embedding_prefill_copy.inc" +#include "../../../cuda/kernels/norm/cuda_rms_norm.inc" +#include "../../../cuda/kernels/rope/cuda_rope.inc" +#include "../../../cuda/kernels/attention/cuda_attention_scores.inc" +#include "../../../cuda/kernels/attention/cuda_attention_softmax.inc" +#include "../../../cuda/kernels/attention/cuda_attention_values.inc" +#include "../../../cuda/kernels/ffn/cuda_ffn_swiglu.inc" +#include "../../../cuda/kernels/projection/cuda_output_projection.inc" +#include "../../../cuda/kernels/projection/cuda_logits_readback.inc" +#include "../../../tensor/graph/builder/token_decode_graph_builder.inc" +#include "../../../tensor/graph/core/tensor_graph.inc" +#include "../../../cuda/decoder_graph/core/cuda_decoder_graph_executor_reset.inc" +#include "../../../cuda/decoder_graph/core/cuda_decoder_graph_executor.inc" diff --git a/extension/src/inference/binding/stacks/model/model_tensor_stack.inc b/extension/src/inference/binding/stacks/model/model_tensor_stack.inc new file mode 100644 index 000000000..a3ee1c784 --- /dev/null +++ b/extension/src/inference/binding/stacks/model/model_tensor_stack.inc @@ -0,0 +1,8 @@ +#include "../../../gguf/loader/gguf_loader.inc" +#include "../../../runtime/memory/native_memory.inc" +#include "../../../tensor/core/tensor_view.inc" +#include "../../../tensor/core/tensor_math.inc" +#include "../../../tensor/resolvers/tensor_resolver.inc" +#include "../../../tensor/resolvers/attention/tensor_attention_resolver.inc" +#include "../../../tensor/resolvers/rms_norm/tensor_rms_norm_resolver.inc" +#include "../../../tensor/resolvers/ffn/tensor_ffn_resolver.inc" diff --git a/extension/src/inference/binding/stacks/openai/openai_router_stack.inc b/extension/src/inference/binding/stacks/openai/openai_router_stack.inc new file mode 100644 index 000000000..76a2c0515 --- /dev/null +++ b/extension/src/inference/binding/stacks/openai/openai_router_stack.inc @@ -0,0 +1,8 @@ +#include "../../../openai/runtime/stream/openai_drain_controls.inc" +#include "../../../openai/runtime/stream/openai_stream_factory.inc" +#include "../../../openai/runtime/compat/openai_compat.inc" +#include "../../../openai/chat/openai_responses.inc" +#include "../../../openai/chat/openai_completions.inc" +#include "../../../openai/resources/openai_embeddings.inc" +#include "../../../openai/resources/openai_models.inc" +#include "../../../openai/http/openai_http_router.inc" diff --git a/extension/src/inference/binding/stacks/request/request_parsing_stack.inc b/extension/src/inference/binding/stacks/request/request_parsing_stack.inc new file mode 100644 index 000000000..10707c4b3 --- /dev/null +++ b/extension/src/inference/binding/stacks/request/request_parsing_stack.inc @@ -0,0 +1,7 @@ +#include "../../../core/support/helpers.inc" +#include "../../../openai/chat/openai_messages.inc" +#include "../../../runtime/errors/error_taxonomy.inc" +#include "../../../openai/http/openai_http_helpers.inc" +#include "../../../openai/runtime/options/openai_tools.inc" +#include "../../../openai/http/openai_http_request.inc" +#include "../../../openai/http/openai_http_body.inc" diff --git a/extension/src/inference/binding/stacks/runtime/runtime_foundation_stack.inc b/extension/src/inference/binding/stacks/runtime/runtime_foundation_stack.inc new file mode 100644 index 000000000..ad2230236 --- /dev/null +++ b/extension/src/inference/binding/stacks/runtime/runtime_foundation_stack.inc @@ -0,0 +1,7 @@ +#include "../../../backends/contracts/backend_contract.inc" +#include "../../../runtime/policy/thermal_policy.inc" +#include "../../../runtime/policy/resource_policy.inc" +#include "../../../cuda/runtime/policy/gpu_vram_policy.inc" +#include "../../../cuda/runtime/status/gpu_runtime_reason.inc" +#include "../../../cuda/runtime/context/cuda_context.inc" +#include "../../../cuda/runtime/context/cuda_device_memory.inc" diff --git a/extension/src/inference/binding/stacks/stream/stream_backend_stack.inc b/extension/src/inference/binding/stacks/stream/stream_backend_stack.inc new file mode 100644 index 000000000..84314cd3e --- /dev/null +++ b/extension/src/inference/binding/stacks/stream/stream_backend_stack.inc @@ -0,0 +1,19 @@ +#include "../../../runtime/cache/paged_kv_cache.inc" +#include "../../../tokenizer/tokenizer.inc" +#include "../../../openai/runtime/options/openai_options.inc" +#include "../../../backends/local/backend_king_local.inc" +#include "../../../backends/native/backend_king_native.inc" +#include "../../../backends/native/backend_king_native_prompt.inc" +#include "../../../cuda/prompt/loop/cuda_decoder_prompt_loop.inc" +#include "../../../cuda/runtime/status/gpu_runtime_status.inc" +#include "../../../runtime/config/runtime_truth.inc" +#include "../../../backends/native/backend_king_native_gpu.inc" +#include "../../../backends/registry/backend_registry.inc" +#include "../../../runtime/config/model_config.inc" +#include "../../../runtime/config/runtime_profile.inc" +#include "../../../runtime/config/runtime_model_registry.inc" +#include "../../../runtime/cache/llm_cache_policy.inc" +#include "../../../openai/resources/openai_usage.inc" +#include "../../../runtime/events/stream_events.inc" +#include "../../../openai/chat/openai_completion_payload.inc" +#include "../../../core/classes/object_handlers.inc" diff --git a/extension/src/inference/core/bootstrap/registration.inc b/extension/src/inference/core/bootstrap/registration.inc new file mode 100644 index 000000000..ac1744d9d --- /dev/null +++ b/extension/src/inference/core/bootstrap/registration.inc @@ -0,0 +1,27 @@ +void king_inference_register_classes(void) +{ + king_ce_inference = king_register_class_with_flags( + "King\\Inference", + NULL, + king_inference_class_methods, + ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES + ); + king_ce_inference_model = king_register_class_with_flags( + "King\\Inference\\Model", + NULL, + king_inference_model_class_methods, + ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES + ); + king_ce_inference_stream = king_register_class_with_flags( + "King\\Inference\\Stream", + NULL, + king_inference_stream_class_methods, + ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES + ); +} + +void king_inference_init_object_handlers(void) +{ + king_inference_model_object_handlers_init(); + king_inference_stream_object_handlers_init(); +} diff --git a/extension/src/inference/core/bootstrap/state.inc b/extension/src/inference/core/bootstrap/state.inc new file mode 100644 index 000000000..45c2f570e --- /dev/null +++ b/extension/src/inference/core/bootstrap/state.inc @@ -0,0 +1,5 @@ +/* + * Inference module state storage aggregation. + */ + +#include "../classes/class_entries.inc" diff --git a/extension/src/inference/core/classes/class_entries.inc b/extension/src/inference/core/classes/class_entries.inc new file mode 100644 index 000000000..affca5449 --- /dev/null +++ b/extension/src/inference/core/classes/class_entries.inc @@ -0,0 +1,7 @@ +/* + * Inference PHP class-entry storage. + */ + +zend_class_entry *king_ce_inference = NULL; +zend_class_entry *king_ce_inference_model = NULL; +zend_class_entry *king_ce_inference_stream = NULL; diff --git a/extension/src/inference/core/classes/object_handlers.inc b/extension/src/inference/core/classes/object_handlers.inc new file mode 100644 index 000000000..0edb77582 --- /dev/null +++ b/extension/src/inference/core/classes/object_handlers.inc @@ -0,0 +1,233 @@ +/* + * King inference PHP object handlers. + */ + +static zend_object_handlers king_inference_model_object_handlers; +static zend_object_handlers king_inference_stream_object_handlers; + +#include "stream_owned_state.inc" + +static void king_inference_model_object_free(zend_object *object) +{ + king_inference_model_object *intern = php_king_inference_model_obj_from_zend(object); + + if (!Z_ISUNDEF(intern->config)) { + zval_ptr_dtor(&intern->config); + ZVAL_UNDEF(&intern->config); + } + if (!Z_ISUNDEF(intern->tensor_index)) { + zval_ptr_dtor(&intern->tensor_index); + ZVAL_UNDEF(&intern->tensor_index); + } + if (!Z_ISUNDEF(intern->tokenizer_tokens)) { + zval_ptr_dtor(&intern->tokenizer_tokens); + ZVAL_UNDEF(&intern->tokenizer_tokens); + } + if (!Z_ISUNDEF(intern->tokenizer_lookup)) { + zval_ptr_dtor(&intern->tokenizer_lookup); + ZVAL_UNDEF(&intern->tokenizer_lookup); + } + if (!Z_ISUNDEF(intern->tokenizer_scores)) { + zval_ptr_dtor(&intern->tokenizer_scores); + ZVAL_UNDEF(&intern->tokenizer_scores); + } + if (!Z_ISUNDEF(intern->tokenizer_types)) { + zval_ptr_dtor(&intern->tokenizer_types); + ZVAL_UNDEF(&intern->tokenizer_types); + } + if (!Z_ISUNDEF(intern->tokenizer_merges)) { + zval_ptr_dtor(&intern->tokenizer_merges); + ZVAL_UNDEF(&intern->tokenizer_merges); + } + if (!Z_ISUNDEF(intern->paged_kv_cache_plan)) { + zval_ptr_dtor(&intern->paged_kv_cache_plan); + ZVAL_UNDEF(&intern->paged_kv_cache_plan); + } + if (intern->name != NULL) { + zend_string_release(intern->name); + intern->name = NULL; + } + if (intern->artifact_path != NULL) { + zend_string_release(intern->artifact_path); + intern->artifact_path = NULL; + } + king_inference_cuda_logits_readback_release(intern); + king_inference_cuda_decoder_prompt_loop_release(intern); + king_inference_cuda_decoder_graph_executor_release(intern); + king_inference_cuda_output_projection_release(intern); + king_inference_cuda_ffn_swiglu_release(intern); + king_inference_cuda_attention_values_release(intern); + king_inference_cuda_attention_softmax_release(intern); + king_inference_cuda_attention_scores_release(intern); + king_inference_cuda_rope_release(intern); + king_inference_cuda_rms_norm_release(intern); + king_inference_cuda_device_kv_cache_release(intern); + king_inference_cuda_device_vector_ops_release(intern); + king_inference_cuda_embedding_row_release(intern); + king_inference_cuda_quantized_matvec_release(intern); + king_inference_cuda_weight_upload_release(intern); + king_inference_cuda_device_allocator_release(intern); + king_inference_cuda_context_release(intern); + king_inference_native_mapping_release(intern); + king_inference_gguf_metadata_release(&intern->gguf); + + zend_object_std_dtor(&intern->std); +} + +static zend_object *king_inference_model_object_create(zend_class_entry *ce) +{ + king_inference_model_object *intern = zend_object_alloc(sizeof(*intern), ce); + + ZVAL_UNDEF(&intern->config); + array_init(&intern->config); + ZVAL_UNDEF(&intern->tensor_index); + array_init(&intern->tensor_index); + ZVAL_UNDEF(&intern->tokenizer_tokens); + array_init(&intern->tokenizer_tokens); + ZVAL_UNDEF(&intern->tokenizer_lookup); + array_init(&intern->tokenizer_lookup); + ZVAL_UNDEF(&intern->tokenizer_scores); + array_init(&intern->tokenizer_scores); + ZVAL_UNDEF(&intern->tokenizer_types); + array_init(&intern->tokenizer_types); + ZVAL_UNDEF(&intern->tokenizer_merges); + array_init(&intern->tokenizer_merges); + ZVAL_UNDEF(&intern->paged_kv_cache_plan); + array_init(&intern->paged_kv_cache_plan); + intern->name = NULL; + intern->artifact_path = NULL; + intern->native_map = NULL; + intern->native_map_size = 0; + intern->native_map_loaded = false; + intern->runtime_last_started_ms = 0; + intern->runtime_last_first_token_ms = 0; + intern->runtime_last_finished_ms = 0; + intern->runtime_last_duration_ms = 0; + intern->runtime_last_time_to_first_token_ms = 0; + intern->runtime_last_generated_tokens = 0; + intern->tokenizer_last_token_count = 0; + intern->tokenizer_last_unknown_count = 0; + intern->tokenizer_last_input_bytes = 0; + intern->tokenizer_last_normalized_bytes = 0; + intern->runtime_last_tokens_per_second = 0.0; + intern->runtime_last_timing_available = false; + intern->runtime_last_first_token_available = false; + intern->tokenizer_last_encode_available = false; + king_inference_cuda_context_reset_fields(intern); + king_inference_cuda_device_allocator_reset_fields(intern); + king_inference_cuda_weight_upload_reset_fields(intern); + king_inference_cuda_quantized_matvec_reset_fields(intern); + king_inference_cuda_embedding_row_reset_fields(intern); + king_inference_cuda_device_vector_ops_reset_fields(intern); + king_inference_cuda_device_kv_cache_reset_fields(intern); + king_inference_cuda_rms_norm_reset_fields(intern); + king_inference_cuda_rope_reset_fields(intern); + king_inference_cuda_attention_scores_reset_fields(intern); + king_inference_cuda_attention_softmax_reset_fields(intern); + king_inference_cuda_attention_values_reset_fields(intern); + king_inference_cuda_ffn_swiglu_reset_fields(intern); + king_inference_cuda_output_projection_reset_fields(intern); + king_inference_cuda_logits_readback_reset_fields(intern); + king_inference_cuda_decoder_graph_executor_reset_fields(intern); + king_inference_cuda_decoder_prompt_loop_reset_fields(intern); + memset(&intern->gguf, 0, sizeof(intern->gguf)); + zend_object_std_init(&intern->std, ce); + object_properties_init(&intern->std, ce); + intern->std.handlers = &king_inference_model_object_handlers; + + return &intern->std; +} + +static void king_inference_stream_object_free(zend_object *object) +{ + king_inference_stream_object *intern = php_king_inference_stream_obj_from_zend(object); + + king_inference_stream_release_owned_state(intern); + zend_object_std_dtor(&intern->std); +} + +static zend_object *king_inference_stream_object_create(zend_class_entry *ce) +{ + king_inference_stream_object *intern = zend_object_alloc(sizeof(*intern), ce); + + ZVAL_UNDEF(&intern->model); + ZVAL_UNDEF(&intern->request); + ZVAL_UNDEF(&intern->options); + ZVAL_UNDEF(&intern->native_events); + intern->native_gpu_stream_state = NULL; + intern->native_event_index = 0; + intern->stdout_fd = -1; + intern->stderr_fd = -1; + intern->child_pid = -1; + intern->exit_code = -1; + intern->chunk_count = 0; + intern->stderr_count = 0; + intern->bytes_emitted = 0; + king_inference_stream_reset_decoder_metrics(intern); + intern->created_at = 0; + intern->runtime_started_ms = 0; + intern->runtime_first_token_ms = 0; + intern->runtime_finished_ms = 0; + intern->runtime_generated_tokens = 0; + intern->gpu_thermal_preflight_at = 0; + intern->gpu_thermal_last_check_at = 0; + intern->gpu_thermal_abort_at = 0; + intern->gpu_power_preflight_at = 0; + intern->gpu_power_last_check_at = 0; + intern->gpu_power_abort_at = 0; + intern->gpu_policy_during_run_checks = 0; + intern->gpu_policy_interval_skips = 0; + intern->gpu_thermal_runtime_checks = 0; + intern->gpu_power_runtime_checks = 0; + intern->gpu_thermal_check_interval_seconds = 0; + intern->gpu_power_check_interval_seconds = 0; + intern->gpu_thermal_preflight_temperature_c = 0.0; + intern->gpu_thermal_abort_temperature_c = 0.0; + intern->gpu_thermal_abort_ceiling_c = 0.0; + intern->gpu_power_preflight_watts = 0.0; + intern->gpu_power_abort_watts = 0.0; + intern->gpu_power_abort_ceiling_watts = 0.0; + intern->response_id = NULL; + intern->start_event_pending = true; + intern->openai_compatible = false; + intern->gpu_thermal_preflight_checked = false; + intern->gpu_thermal_preflight_temperature_available = false; + intern->gpu_thermal_aborted = false; + intern->gpu_power_preflight_checked = false; + intern->gpu_power_preflight_available = false; + intern->gpu_power_aborted = false; + intern->done = false; + intern->cancelled = false; + intern->timed_out = false; + zend_object_std_init(&intern->std, ce); + object_properties_init(&intern->std, ce); + intern->std.handlers = &king_inference_stream_object_handlers; + + return &intern->std; +} + +static void king_inference_model_object_handlers_init(void) +{ + memcpy( + &king_inference_model_object_handlers, + &std_object_handlers, + sizeof(zend_object_handlers) + ); + king_inference_model_object_handlers.offset = XtOffsetOf(king_inference_model_object, std); + king_inference_model_object_handlers.free_obj = king_inference_model_object_free; + king_inference_model_object_handlers.clone_obj = NULL; + king_ce_inference_model->create_object = king_inference_model_object_create; +} + +static void king_inference_stream_object_handlers_init(void) +{ + memcpy( + &king_inference_stream_object_handlers, + &std_object_handlers, + sizeof(zend_object_handlers) + ); + king_inference_stream_object_handlers.offset = XtOffsetOf(king_inference_stream_object, std); + king_inference_stream_object_handlers.free_obj = king_inference_stream_object_free; + king_inference_stream_object_handlers.clone_obj = NULL; + king_ce_inference_stream->create_object = king_inference_stream_object_create; +} diff --git a/extension/src/inference/core/classes/stream_owned_state.inc b/extension/src/inference/core/classes/stream_owned_state.inc new file mode 100644 index 000000000..3731ee36c --- /dev/null +++ b/extension/src/inference/core/classes/stream_owned_state.inc @@ -0,0 +1,29 @@ +/* + * Stream-owned state release for object teardown and re-construction. + */ + +static void king_inference_stream_release_owned_state(king_inference_stream_object *intern) +{ + king_inference_stream_close_process(intern, true); + if (!Z_ISUNDEF(intern->model)) { + zval_ptr_dtor(&intern->model); + ZVAL_UNDEF(&intern->model); + } + if (!Z_ISUNDEF(intern->request)) { + zval_ptr_dtor(&intern->request); + ZVAL_UNDEF(&intern->request); + } + if (!Z_ISUNDEF(intern->options)) { + zval_ptr_dtor(&intern->options); + ZVAL_UNDEF(&intern->options); + } + if (!Z_ISUNDEF(intern->native_events)) { + zval_ptr_dtor(&intern->native_events); + ZVAL_UNDEF(&intern->native_events); + } + king_inference_cuda_decoder_prompt_stream_release(intern); + if (intern->response_id != NULL) { + zend_string_release(intern->response_id); + intern->response_id = NULL; + } +} diff --git a/extension/src/inference/core/support/helpers.inc b/extension/src/inference/core/support/helpers.inc new file mode 100644 index 000000000..046fa7cab --- /dev/null +++ b/extension/src/inference/core/support/helpers.inc @@ -0,0 +1,43 @@ +static zval *king_inference_array_find(zval *array, const char *key) +{ + if (array == NULL || Z_TYPE_P(array) != IS_ARRAY) { + return NULL; + } + + return zend_hash_str_find(Z_ARRVAL_P(array), key, strlen(key)); +} + +static bool king_inference_bool_from_array(zval *array, const char *key, bool fallback) +{ + zval *value = king_inference_array_find(array, key); + + if (value == NULL) { + return fallback; + } + + return zend_is_true(value); +} + +static zend_long king_inference_long_from_array(zval *array, const char *key, zend_long fallback) +{ + zval *value = king_inference_array_find(array, key); + + return value != NULL && Z_TYPE_P(value) == IS_LONG ? Z_LVAL_P(value) : fallback; +} + +static double king_inference_double_from_array(zval *array, const char *key, double fallback) +{ + zval *value = king_inference_array_find(array, key); + + if (value == NULL) { + return fallback; + } + if (Z_TYPE_P(value) == IS_DOUBLE) { + return Z_DVAL_P(value); + } + if (Z_TYPE_P(value) == IS_LONG) { + return (double) Z_LVAL_P(value); + } + + return fallback; +} diff --git a/extension/src/inference/cuda/decoder_graph/core/cuda_decoder_graph_executor.inc b/extension/src/inference/cuda/decoder_graph/core/cuda_decoder_graph_executor.inc new file mode 100644 index 000000000..ab57c6194 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/core/cuda_decoder_graph_executor.inc @@ -0,0 +1,15 @@ +/* + * CUDA decoder graph executor include stack. + * Order matters: + * 1. contract helpers define errors, supported ops, tensor descriptors, and field validators. + * 2. op validation defines every per-op graph contract used by validation and planning. + * 3. graph validation walks inputs, ops, terminal references, and stores graph counters. + * 4. device ops require the validated graph helpers before result preparation can run. + * 5. result/lifecycle/status helpers expose the validated execution envelope. + */ + +#include "cuda_decoder_graph_executor/contract_helpers.inc" +#include "cuda_decoder_graph_executor/op_validation.inc" +#include "cuda_decoder_graph_executor/graph_validation.inc" +#include "../device/core/cuda_decoder_graph_executor_device_ops.inc" +#include "cuda_decoder_graph_executor/result_lifecycle.inc" diff --git a/extension/src/inference/cuda/decoder_graph/core/cuda_decoder_graph_executor/contract_helpers.inc b/extension/src/inference/cuda/decoder_graph/core/cuda_decoder_graph_executor/contract_helpers.inc new file mode 100644 index 000000000..46d93d346 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/core/cuda_decoder_graph_executor/contract_helpers.inc @@ -0,0 +1,363 @@ +/* + * CUDA decoder graph executor contract for native King GPU inference. + */ + +static void king_inference_cuda_decoder_graph_executor_set_error( + king_inference_model_object *model, + int result, + const char *message +) { + if (message == NULL || message[0] == '\0') { + message = "unknown_cuda_decoder_graph_executor_error"; + } + + model->cuda_decoder_graph_executor_result = result; + snprintf( + model->cuda_decoder_graph_executor_error, + sizeof(model->cuda_decoder_graph_executor_error), + "%s", + message + ); +} + +static bool king_inference_cuda_decoder_graph_executor_dependencies_ready( + king_inference_model_object *model +) { + return model->cuda_embedding_row_available + && model->cuda_quantized_matvec_available + && model->cuda_device_vector_ops_available + && model->cuda_device_kv_cache_available + && model->cuda_rms_norm_available + && model->cuda_rope_available + && model->cuda_attention_scores_available + && model->cuda_attention_softmax_available + && model->cuda_attention_values_available + && model->cuda_ffn_swiglu_path_available + && model->cuda_output_projection_available + && model->cuda_logits_readback_available; +} + +static bool king_inference_cuda_decoder_graph_executor_supported_op(zend_string *op_name) +{ + return zend_string_equals_literal(op_name, "embedding") + || zend_string_equals_literal(op_name, "rms_norm") + || zend_string_equals_literal(op_name, "linear") + || zend_string_equals_literal(op_name, "add") + || zend_string_equals_literal(op_name, "scale") + || zend_string_equals_literal(op_name, "mul") + || zend_string_equals_literal(op_name, "silu") + || zend_string_equals_literal(op_name, "gelu_tanh") + || zend_string_equals_literal(op_name, "slice") + || zend_string_equals_literal(op_name, "rope") + || zend_string_equals_literal(op_name, "kv_write") + || zend_string_equals_literal(op_name, "kv_attention") + || zend_string_equals_literal(op_name, "stack") + || zend_string_equals_literal(op_name, "argmax_token") + || zend_string_equals_literal(op_name, "sample_token"); +} + +static zend_result king_inference_cuda_decoder_graph_executor_validation_error( + king_inference_model_object *model, + const char *class_name, + zend_string *node_id, + const char *field, + const char *detail +) { + char error[512]; + + snprintf( + error, + sizeof(error), + "cuda_decoder_graph_validation class=%s node=%s field=%s detail=%s", + class_name != NULL ? class_name : "unknown", + node_id != NULL ? ZSTR_VAL(node_id) : "", + field != NULL ? field : "-", + detail != NULL ? detail : "-" + ); + king_inference_cuda_decoder_graph_executor_set_error(model, -1, error); + return FAILURE; +} + +static zval *king_inference_cuda_decoder_graph_executor_shape_dimensions( + zval *descriptor +) { + zval *dimensions; + + if (descriptor == NULL || Z_TYPE_P(descriptor) != IS_ARRAY) { + return NULL; + } + dimensions = zend_hash_str_find(Z_ARRVAL_P(descriptor), "dimensions", sizeof("dimensions") - 1); + return dimensions != NULL && Z_TYPE_P(dimensions) == IS_ARRAY ? dimensions : NULL; +} + +static zend_result king_inference_cuda_decoder_graph_executor_tensor_descriptor( + king_inference_model_object *model, + zend_string *node_id, + const char *field, + zend_string *tensor, + zval **descriptor_out, + zval **dimensions_out, + zend_ulong *rank_out, + zend_ulong *type_out +) { + zval *descriptor; + zval *dimensions; + zend_ulong rank; + zend_ulong type; + + if (tensor == NULL || ZSTR_LEN(tensor) == 0) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "tensor_name", + node_id, + field, + "missing_tensor_name" + ); + } + + model->cuda_decoder_graph_executor_tensor_lookup_count++; + descriptor = king_inference_tensor_index_descriptor(model, tensor); + dimensions = king_inference_cuda_decoder_graph_executor_shape_dimensions(descriptor); + if (descriptor == NULL + || dimensions == NULL + || !king_inference_tensor_descriptor_ulong(descriptor, "rank", sizeof("rank") - 1, &rank) + || !king_inference_tensor_descriptor_ulong(descriptor, "type", sizeof("type") - 1, &type)) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "tensor_descriptor", + node_id, + field, + ZSTR_VAL(tensor) + ); + } + + *descriptor_out = descriptor; + *dimensions_out = dimensions; + *rank_out = rank; + *type_out = type; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_required_string_field( + king_inference_model_object *model, + zval *entry, + zend_string *node_id, + const char *class_name, + const char *field, + zend_string **value +) { + zval *found = king_inference_array_find(entry, field); + + if (found == NULL || Z_TYPE_P(found) != IS_STRING || Z_STRLEN_P(found) == 0) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + class_name, + node_id, + field, + "required_non_empty_string" + ); + } + *value = Z_STR_P(found); + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_optional_long_field( + king_inference_model_object *model, + zval *entry, + zend_string *node_id, + const char *field, + zend_long fallback, + zend_long *value +) { + zval *found = king_inference_array_find(entry, field); + + if (found == NULL) { + *value = fallback; + return SUCCESS; + } + if (Z_TYPE_P(found) != IS_LONG) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "field_type", + node_id, + field, + "expected_integer" + ); + } + *value = Z_LVAL_P(found); + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_non_negative_long_field( + king_inference_model_object *model, + zval *entry, + zend_string *node_id, + const char *field, + bool required, + zend_ulong fallback, + zend_ulong *value +) { + zval *found = king_inference_array_find(entry, field); + + if (found == NULL) { + if (required) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "field_type", + node_id, + field, + "required_non_negative_integer" + ); + } + *value = fallback; + return SUCCESS; + } + if (Z_TYPE_P(found) != IS_LONG || Z_LVAL_P(found) < 0) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "field_type", + node_id, + field, + "expected_non_negative_integer" + ); + } + *value = (zend_ulong) Z_LVAL_P(found); + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_positive_long_field( + king_inference_model_object *model, + zval *entry, + zend_string *node_id, + const char *field, + zend_ulong *value +) { + zval *found = king_inference_array_find(entry, field); + + if (found == NULL || Z_TYPE_P(found) != IS_LONG || Z_LVAL_P(found) <= 0) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "field_type", + node_id, + field, + "required_positive_integer" + ); + } + *value = (zend_ulong) Z_LVAL_P(found); + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_finite_double_field( + king_inference_model_object *model, + zval *entry, + zend_string *node_id, + const char *field, + double fallback, + bool must_be_positive, + double *value +) { + zval *found = king_inference_array_find(entry, field); + + *value = fallback; + if (found == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(found) == IS_DOUBLE) { + *value = Z_DVAL_P(found); + } else if (Z_TYPE_P(found) == IS_LONG) { + *value = (double) Z_LVAL_P(found); + } else { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "field_type", + node_id, + field, + "expected_number" + ); + } + if (!isfinite(*value) || (must_be_positive && *value <= 0.0)) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "numeric_range", + node_id, + field, + must_be_positive ? "expected_positive_finite_number" : "expected_finite_number" + ); + } + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_defined_reference( + king_inference_model_object *model, + zval *entry, + zend_string *node_id, + zval *defined, + const char *field, + zend_string **value +) { + if (king_inference_cuda_decoder_graph_executor_required_string_field( + model, + entry, + node_id, + "dependency", + field, + value + ) != SUCCESS) { + return FAILURE; + } + if (!zend_hash_exists(Z_ARRVAL_P(defined), *value)) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "dangling_read", + node_id, + field, + ZSTR_VAL(*value) + ); + } + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_reference_length( + king_inference_model_object *model, + zval *lengths, + zend_string *node_id, + const char *field, + zend_string *reference, + zend_ulong *length +) { + zval *found = zend_hash_find(Z_ARRVAL_P(lengths), reference); + + if (found == NULL || Z_TYPE_P(found) != IS_LONG || Z_LVAL_P(found) < 0) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "output_dependency", + node_id, + field, + ZSTR_VAL(reference) + ); + } + *length = (zend_ulong) Z_LVAL_P(found); + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_set_validated_length( + king_inference_model_object *model, + zval *lengths, + zend_string *node_id, + zend_ulong length +) { + zval stored; + + if (length == 0 || length > (zend_ulong) ZEND_LONG_MAX) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "shape", + node_id, + "output", + "invalid_output_length" + ); + } + ZVAL_LONG(&stored, (zend_long) length); + zend_hash_update(Z_ARRVAL_P(lengths), node_id, &stored); + return SUCCESS; +} diff --git a/extension/src/inference/cuda/decoder_graph/core/cuda_decoder_graph_executor/graph_validation.inc b/extension/src/inference/cuda/decoder_graph/core/cuda_decoder_graph_executor/graph_validation.inc new file mode 100644 index 000000000..afe495779 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/core/cuda_decoder_graph_executor/graph_validation.inc @@ -0,0 +1,189 @@ +static zend_result king_inference_cuda_decoder_graph_executor_validate_terminal( + king_inference_model_object *model, + zval *graph, + zval *defined +) { + zval *terminal = king_inference_array_find(graph, "terminal"); + static const char *fields[] = { + "hidden_output", + "final_norm", + "logits" + }; + size_t i; + + if (terminal == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(terminal) != IS_ARRAY) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "field_type", + NULL, + "terminal", + "expected_object_array" + ); + } + for (i = 0; i < sizeof(fields) / sizeof(fields[0]); i++) { + zval *field = king_inference_array_find(terminal, fields[i]); + + if (field == NULL) { + continue; + } + if (Z_TYPE_P(field) != IS_STRING || Z_STRLEN_P(field) == 0) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "field_type", + NULL, + fields[i], + "terminal_reference_must_be_non_empty_string" + ); + } + if (!zend_hash_exists(Z_ARRVAL_P(defined), Z_STR_P(field))) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "dangling_read", + NULL, + fields[i], + Z_STRVAL_P(field) + ); + } + } + + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_validate_graph( + king_inference_model_object *model, + zval *graph +) { + zval *ops; + zval *entry; + zval defined; + zval lengths; + zend_ulong op_count = 0; + zend_ulong supported_count = 0; + zend_result status = SUCCESS; + + if (!model->cuda_decoder_graph_executor_available) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_executor_unavailable" + ); + return FAILURE; + } + if (graph == NULL || Z_TYPE_P(graph) != IS_ARRAY) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_invalid"); + return FAILURE; + } + + ops = king_inference_array_find(graph, "ops"); + if (ops == NULL) { + ops = king_inference_array_find(graph, "steps"); + } + if (ops == NULL || Z_TYPE_P(ops) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL_P(ops)) == 0) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_ops_missing"); + return FAILURE; + } + + array_init(&defined); + array_init(&lengths); + status = king_inference_cuda_decoder_graph_executor_validate_graph_inputs( + model, + graph, + &defined, + &lengths + ); + if (status != SUCCESS) { + zval_ptr_dtor(&lengths); + zval_ptr_dtor(&defined); + return FAILURE; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(ops), entry) { + zend_string *id = NULL; + zend_string *op_name = NULL; + zend_ulong output_length = 0; + zval marker; + + if (entry == NULL || Z_TYPE_P(entry) != IS_ARRAY) { + status = king_inference_cuda_decoder_graph_executor_validation_error( + model, + "node", + NULL, + "ops", + "entry_must_be_object_array" + ); + break; + } + if (king_inference_graph_required_string(entry, "id", &id) != SUCCESS + || king_inference_graph_required_string(entry, "op", &op_name) != SUCCESS) { + status = king_inference_cuda_decoder_graph_executor_validation_error( + model, + "node", + id, + id == NULL ? "id" : "op", + "id_and_op_required" + ); + break; + } + if (zend_hash_exists(Z_ARRVAL(defined), id)) { + status = king_inference_cuda_decoder_graph_executor_validation_error( + model, + "ambiguous_write", + id, + "id", + "duplicate_output_id" + ); + break; + } + if (!king_inference_cuda_decoder_graph_executor_supported_op(op_name)) { + status = king_inference_cuda_decoder_graph_executor_validation_error( + model, + "unsupported", + id, + "op", + ZSTR_VAL(op_name) + ); + break; + } + if (king_inference_cuda_decoder_graph_executor_validate_op_contract( + model, + entry, + id, + op_name, + &defined, + &lengths, + &output_length + ) != SUCCESS + || king_inference_cuda_decoder_graph_executor_set_validated_length( + model, + &lengths, + id, + output_length + ) != SUCCESS) { + status = FAILURE; + break; + } + ZVAL_TRUE(&marker); + zend_hash_update(Z_ARRVAL(defined), id, &marker); + op_count++; + supported_count++; + } ZEND_HASH_FOREACH_END(); + + if (status == SUCCESS) { + status = king_inference_cuda_decoder_graph_executor_validate_terminal(model, graph, &defined); + } + zval_ptr_dtor(&lengths); + zval_ptr_dtor(&defined); + if (status != SUCCESS) { + return FAILURE; + } + + model->cuda_decoder_graph_executor_graph_count++; + model->cuda_decoder_graph_executor_last_op_count = op_count; + model->cuda_decoder_graph_executor_last_supported_op_count = supported_count; + model->cuda_decoder_graph_executor_result = 0; + model->cuda_decoder_graph_executor_error[0] = '\0'; + return SUCCESS; +} diff --git a/extension/src/inference/cuda/decoder_graph/core/cuda_decoder_graph_executor/op_validation.inc b/extension/src/inference/cuda/decoder_graph/core/cuda_decoder_graph_executor/op_validation.inc new file mode 100644 index 000000000..4ba665173 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/core/cuda_decoder_graph_executor/op_validation.inc @@ -0,0 +1,783 @@ +static zend_result king_inference_cuda_decoder_graph_executor_validate_embedding_op( + king_inference_model_object *model, + zval *entry, + zend_string *node_id, + zend_ulong *length_out +) { + zval *tensor_value = king_inference_array_find(entry, "tensor"); + zend_string *tensor = NULL; + zend_string *resolved_tensor = NULL; + const char *status = NULL; + zval *descriptor = NULL; + zval *dimensions = NULL; + zend_ulong rank = 0; + zend_ulong type = 0; + zend_ulong cols = 0; + zend_ulong rows = 0; + zend_ulong token_id = 0; + double scale = 1.0; + + if (king_inference_cuda_decoder_graph_executor_non_negative_long_field( + model, + entry, + node_id, + king_inference_array_find(entry, "token_id") != NULL ? "token_id" : "token", + true, + 0, + &token_id + ) != SUCCESS + || king_inference_cuda_decoder_graph_executor_finite_double_field( + model, + entry, + node_id, + "scale", + 1.0, + false, + &scale + ) != SUCCESS) { + return FAILURE; + } + + if (tensor_value != NULL) { + if (Z_TYPE_P(tensor_value) != IS_STRING || Z_STRLEN_P(tensor_value) == 0) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "tensor_name", + node_id, + "tensor", + "required_non_empty_string" + ); + } + tensor = Z_STR_P(tensor_value); + } else { + if (king_inference_resolve_token_embedding_tensor_name( + model, + NULL, + &resolved_tensor, + NULL, + &status + ) != SUCCESS || resolved_tensor == NULL) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "metadata", + node_id, + "tensor", + status != NULL ? status : "embedding_tensor_unresolved" + ); + } + tensor = resolved_tensor; + } + + if (king_inference_cuda_decoder_graph_executor_tensor_descriptor( + model, + node_id, + "tensor", + tensor, + &descriptor, + &dimensions, + &rank, + &type + ) != SUCCESS + || rank != 2 + || !king_inference_cuda_embedding_row_supported_type(type) + || king_inference_tensor_dimension_at(dimensions, 0, &cols) != SUCCESS + || king_inference_tensor_dimension_at(dimensions, 1, &rows) != SUCCESS + || cols == 0 + || rows == 0 + || cols > UINT_MAX + || rows > UINT_MAX + || token_id >= rows) { + if (resolved_tensor != NULL) { + zend_string_release(resolved_tensor); + } + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "shape", + node_id, + "tensor", + "embedding_tensor_or_token_invalid" + ); + } + if (resolved_tensor != NULL) { + zend_string_release(resolved_tensor); + } + + *length_out = cols; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_validate_rms_norm_op( + king_inference_model_object *model, + zval *entry, + zend_string *node_id, + zval *defined, + zval *lengths, + zend_ulong *length_out +) { + zend_string *input_id = NULL; + zend_string *weight = NULL; + zval *descriptor = NULL; + zval *dimensions = NULL; + zend_ulong input_length = 0; + zend_ulong weight_width = 0; + zend_ulong rank = 0; + zend_ulong type = 0; + double epsilon = 0.000001; + + if (king_inference_cuda_decoder_graph_executor_defined_reference(model, entry, node_id, defined, "input", &input_id) != SUCCESS + || king_inference_cuda_decoder_graph_executor_reference_length(model, lengths, node_id, "input", input_id, &input_length) != SUCCESS + || king_inference_cuda_decoder_graph_executor_required_string_field(model, entry, node_id, "tensor_name", "weight", &weight) != SUCCESS + || king_inference_cuda_decoder_graph_executor_finite_double_field(model, entry, node_id, "epsilon", 0.000001, true, &epsilon) != SUCCESS) { + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_tensor_descriptor( + model, + node_id, + "weight", + weight, + &descriptor, + &dimensions, + &rank, + &type + ) != SUCCESS + || rank != 1 + || (type != 0 && type != 1) + || king_inference_tensor_dimension_at(dimensions, 0, &weight_width) != SUCCESS + || weight_width != input_length) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "shape", + node_id, + "weight", + "rms_norm_weight_shape_or_dtype_invalid" + ); + } + + *length_out = input_length; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_validate_linear_op( + king_inference_model_object *model, + zval *entry, + zend_string *node_id, + zval *defined, + zval *lengths, + zend_ulong *length_out +) { + zend_string *input_id = NULL; + zend_string *weight = NULL; + zval *descriptor = NULL; + zval *dimensions = NULL; + zend_ulong input_length = 0; + zend_ulong cols = 0; + zend_ulong rows = 0; + zend_ulong rank = 0; + zend_ulong type = 0; + zend_ulong row_limit = 0; + zend_long row_offset = 0; + + if (king_inference_cuda_decoder_graph_executor_defined_reference(model, entry, node_id, defined, "input", &input_id) != SUCCESS + || king_inference_cuda_decoder_graph_executor_reference_length(model, lengths, node_id, "input", input_id, &input_length) != SUCCESS + || king_inference_cuda_decoder_graph_executor_required_string_field(model, entry, node_id, "tensor_name", "weight", &weight) != SUCCESS + || king_inference_cuda_decoder_graph_executor_optional_long_field(model, entry, node_id, "row_offset", 0, &row_offset) != SUCCESS) { + return FAILURE; + } + if (row_offset != 0) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "unsupported", + node_id, + "row_offset", + "cuda_linear_row_offset_unsupported" + ); + } + if (king_inference_cuda_decoder_graph_executor_tensor_descriptor( + model, + node_id, + "weight", + weight, + &descriptor, + &dimensions, + &rank, + &type + ) != SUCCESS + || rank != 2 + || !king_inference_cuda_quantized_matvec_type_supported(type) + || king_inference_tensor_dimension_at(dimensions, 0, &cols) != SUCCESS + || king_inference_tensor_dimension_at(dimensions, 1, &rows) != SUCCESS + || cols != input_length + || rows == 0 + || rows > UINT_MAX) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "shape", + node_id, + "weight", + "linear_weight_shape_or_dtype_invalid" + ); + } + if (king_inference_cuda_decoder_graph_executor_non_negative_long_field( + model, + entry, + node_id, + "row_limit", + false, + rows, + &row_limit + ) != SUCCESS) { + return FAILURE; + } + if (row_limit == 0 || row_limit > rows) { + row_limit = rows; + } + *length_out = row_limit; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_validate_unary_vector_op( + king_inference_model_object *model, + zval *entry, + zend_string *node_id, + zval *defined, + zval *lengths, + const char *input_field, + zend_ulong *length_out +) { + zend_string *input_id = NULL; + + if (king_inference_cuda_decoder_graph_executor_defined_reference(model, entry, node_id, defined, input_field, &input_id) != SUCCESS + || king_inference_cuda_decoder_graph_executor_reference_length(model, lengths, node_id, input_field, input_id, length_out) != SUCCESS) { + return FAILURE; + } + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_validate_binary_vector_op( + king_inference_model_object *model, + zval *entry, + zend_string *node_id, + zval *defined, + zval *lengths, + zend_ulong *length_out +) { + zend_string *left_id = NULL; + zend_string *right_id = NULL; + zend_ulong left_length = 0; + zend_ulong right_length = 0; + double left_scale = 1.0; + double right_scale = 1.0; + + if (king_inference_cuda_decoder_graph_executor_defined_reference(model, entry, node_id, defined, "left", &left_id) != SUCCESS + || king_inference_cuda_decoder_graph_executor_defined_reference(model, entry, node_id, defined, "right", &right_id) != SUCCESS + || king_inference_cuda_decoder_graph_executor_reference_length(model, lengths, node_id, "left", left_id, &left_length) != SUCCESS + || king_inference_cuda_decoder_graph_executor_reference_length(model, lengths, node_id, "right", right_id, &right_length) != SUCCESS + || king_inference_cuda_decoder_graph_executor_finite_double_field(model, entry, node_id, "left_scale", 1.0, false, &left_scale) != SUCCESS + || king_inference_cuda_decoder_graph_executor_finite_double_field(model, entry, node_id, "right_scale", 1.0, false, &right_scale) != SUCCESS) { + return FAILURE; + } + if (left_length != right_length) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "shape", + node_id, + "left/right", + "binary_inputs_must_have_equal_length" + ); + } + *length_out = left_length; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_validate_slice_op( + king_inference_model_object *model, + zval *entry, + zend_string *node_id, + zval *defined, + zval *lengths, + zend_ulong *length_out +) { + zend_string *input_id = NULL; + zend_ulong input_length = 0; + zend_ulong offset = 0; + zend_ulong count = 0; + + if (king_inference_cuda_decoder_graph_executor_defined_reference(model, entry, node_id, defined, "input", &input_id) != SUCCESS + || king_inference_cuda_decoder_graph_executor_reference_length(model, lengths, node_id, "input", input_id, &input_length) != SUCCESS + || king_inference_cuda_decoder_graph_executor_non_negative_long_field(model, entry, node_id, "offset", false, 0, &offset) != SUCCESS + || king_inference_cuda_decoder_graph_executor_non_negative_long_field(model, entry, node_id, "count", false, 0, &count) != SUCCESS) { + return FAILURE; + } + if (count == 0) { + count = input_length > offset ? input_length - offset : 0; + } + if (input_length == 0 + || offset > input_length + || count == 0 + || count > input_length - offset + || offset > UINT_MAX + || count > UINT_MAX) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "shape", + node_id, + "offset/count", + "slice_range_invalid" + ); + } + *length_out = count; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_validate_rope_op( + king_inference_model_object *model, + zval *entry, + zend_string *node_id, + zval *defined, + zval *lengths, + zend_ulong *length_out +) { + zend_string *input_id = NULL; + zend_ulong input_length = 0; + zend_ulong offset = 0; + zend_ulong head_dim = 0; + zend_ulong position = 0; + zend_ulong pairing = 0; + double position_scale = 1.0; + double rope_base = 10000.0; + + if (king_inference_cuda_decoder_graph_executor_defined_reference(model, entry, node_id, defined, "input", &input_id) != SUCCESS + || king_inference_cuda_decoder_graph_executor_reference_length(model, lengths, node_id, "input", input_id, &input_length) != SUCCESS + || king_inference_cuda_decoder_graph_executor_non_negative_long_field(model, entry, node_id, "offset", false, 0, &offset) != SUCCESS + || king_inference_cuda_decoder_graph_executor_non_negative_long_field(model, entry, node_id, "position", false, 0, &position) != SUCCESS + || king_inference_cuda_decoder_graph_executor_non_negative_long_field(model, entry, node_id, "pairing", false, 0, &pairing) != SUCCESS + || king_inference_cuda_decoder_graph_executor_finite_double_field(model, entry, node_id, "position_scale", 1.0, false, &position_scale) != SUCCESS + || king_inference_cuda_decoder_graph_executor_finite_double_field(model, entry, node_id, "rope_base", 10000.0, true, &rope_base) != SUCCESS) { + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_non_negative_long_field( + model, + entry, + node_id, + "head_dim", + false, + input_length > offset ? input_length - offset : 0, + &head_dim + ) != SUCCESS) { + return FAILURE; + } + if (input_length == 0 + || input_length > UINT_MAX + || offset > input_length + || offset > UINT_MAX + || head_dim == 0 + || (head_dim % 2) != 0 + || head_dim > input_length - offset + || head_dim > UINT_MAX + || pairing > 1 + || (model->gguf.architecture_params[KING_INFERENCE_ARCH_CONTEXT_LENGTH] > 0 + && position >= model->gguf.architecture_params[KING_INFERENCE_ARCH_CONTEXT_LENGTH])) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "shape", + node_id, + "head_dim/position", + "rope_shape_invalid" + ); + } + *length_out = input_length; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_validate_kv_write_op( + king_inference_model_object *model, + zval *entry, + zend_string *node_id, + zval *defined, + zval *lengths, + zend_ulong *length_out +) { + zend_string *key_id = NULL; + zend_string *value_id = NULL; + zend_ulong key_length = 0; + zend_ulong value_length = 0; + zend_ulong expected_key_length = 0; + zend_ulong expected_value_length = 0; + zend_ulong slot = 0; + zval *cache = king_inference_array_find(entry, "cache"); + zval *key = king_inference_array_find(entry, "key"); + zval *value = king_inference_array_find(entry, "value"); + + if (cache != NULL && (Z_TYPE_P(cache) != IS_STRING || Z_STRLEN_P(cache) == 0)) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "field_type", + node_id, + "cache", + "expected_non_empty_string" + ); + } + if (king_inference_cuda_decoder_graph_executor_non_negative_long_field(model, entry, node_id, "slot", true, 0, &slot) != SUCCESS + || king_inference_cuda_decoder_graph_executor_positive_long_field(model, entry, node_id, "key_length", &expected_key_length) != SUCCESS + || king_inference_cuda_decoder_graph_executor_positive_long_field(model, entry, node_id, "value_length", &expected_value_length) != SUCCESS) { + return FAILURE; + } + if (key == NULL && value == NULL) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "dependency", + node_id, + "key/value", + "kv_write_requires_key_or_value" + ); + } + if (key != NULL) { + if (king_inference_cuda_decoder_graph_executor_defined_reference(model, entry, node_id, defined, "key", &key_id) != SUCCESS + || king_inference_cuda_decoder_graph_executor_reference_length(model, lengths, node_id, "key", key_id, &key_length) != SUCCESS) { + return FAILURE; + } + if (key_length != expected_key_length) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "shape", + node_id, + "key_length", + "kv_write_key_length_mismatch" + ); + } + } + if (value != NULL) { + if (king_inference_cuda_decoder_graph_executor_defined_reference(model, entry, node_id, defined, "value", &value_id) != SUCCESS + || king_inference_cuda_decoder_graph_executor_reference_length(model, lengths, node_id, "value", value_id, &value_length) != SUCCESS) { + return FAILURE; + } + if (value_length != expected_value_length) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "shape", + node_id, + "value_length", + "kv_write_value_length_mismatch" + ); + } + } + if (model->gguf.architecture_params[KING_INFERENCE_ARCH_CONTEXT_LENGTH] > 0 + && slot >= model->gguf.architecture_params[KING_INFERENCE_ARCH_CONTEXT_LENGTH]) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "index_range", + node_id, + "slot", + "kv_slot_exceeds_context" + ); + } + *length_out = 3; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_validate_kv_attention_op( + king_inference_model_object *model, + zval *entry, + zend_string *node_id, + zval *defined, + zval *lengths, + zend_ulong *length_out +) { + zend_string *query_id = NULL; + zend_ulong query_length = 0; + zend_ulong slot_start = 0; + zend_ulong slot_count = 0; + zend_ulong key_length = 0; + zend_ulong value_length = 0; + zval *cache = king_inference_array_find(entry, "cache"); + double scale = 1.0; + double temperature = 1.0; + + if (cache != NULL && (Z_TYPE_P(cache) != IS_STRING || Z_STRLEN_P(cache) == 0)) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "field_type", + node_id, + "cache", + "expected_non_empty_string" + ); + } + if (king_inference_cuda_decoder_graph_executor_defined_reference(model, entry, node_id, defined, "query", &query_id) != SUCCESS + || king_inference_cuda_decoder_graph_executor_reference_length(model, lengths, node_id, "query", query_id, &query_length) != SUCCESS + || king_inference_cuda_decoder_graph_executor_non_negative_long_field(model, entry, node_id, "slot_start", false, 0, &slot_start) != SUCCESS + || king_inference_cuda_decoder_graph_executor_positive_long_field(model, entry, node_id, "slot_count", &slot_count) != SUCCESS + || king_inference_cuda_decoder_graph_executor_positive_long_field(model, entry, node_id, "key_length", &key_length) != SUCCESS + || king_inference_cuda_decoder_graph_executor_positive_long_field(model, entry, node_id, "value_length", &value_length) != SUCCESS + || king_inference_cuda_decoder_graph_executor_finite_double_field(model, entry, node_id, "scale", 1.0, false, &scale) != SUCCESS + || king_inference_cuda_decoder_graph_executor_finite_double_field(model, entry, node_id, "temperature", 1.0, true, &temperature) != SUCCESS) { + return FAILURE; + } + if (query_length != key_length + || value_length == 0 + || slot_count == 0 + || slot_start > ZEND_ULONG_MAX - (slot_count - 1) + || (model->gguf.architecture_params[KING_INFERENCE_ARCH_CONTEXT_LENGTH] > 0 + && slot_start + slot_count > model->gguf.architecture_params[KING_INFERENCE_ARCH_CONTEXT_LENGTH])) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "shape", + node_id, + "query/key/value", + "kv_attention_shape_invalid" + ); + } + *length_out = value_length; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_validate_stack_op( + king_inference_model_object *model, + zval *entry, + zend_string *node_id, + zval *defined, + zval *lengths, + zend_ulong *length_out +) { + zval *inputs = king_inference_array_find(entry, "inputs"); + zval *input; + zend_ulong total = 0; + + if (inputs == NULL || Z_TYPE_P(inputs) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL_P(inputs)) == 0) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "dependency", + node_id, + "inputs", + "stack_requires_non_empty_inputs" + ); + } + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(inputs), input) { + zend_ulong length = 0; + + if (input == NULL || Z_TYPE_P(input) != IS_STRING || Z_STRLEN_P(input) == 0) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "field_type", + node_id, + "inputs", + "stack_input_must_be_non_empty_string" + ); + } + if (!zend_hash_exists(Z_ARRVAL_P(defined), Z_STR_P(input))) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "dangling_read", + node_id, + "inputs", + Z_STRVAL_P(input) + ); + } + if (king_inference_cuda_decoder_graph_executor_reference_length( + model, + lengths, + node_id, + "inputs", + Z_STR_P(input), + &length + ) != SUCCESS) { + return FAILURE; + } + if (total > ZEND_ULONG_MAX - length) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "shape", + node_id, + "inputs", + "stack_length_overflow" + ); + } + total += length; + } ZEND_HASH_FOREACH_END(); + + *length_out = total; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_validate_graph_inputs( + king_inference_model_object *model, + zval *graph, + zval *defined, + zval *lengths +) { + zval *inputs = king_inference_array_find(graph, "inputs"); + zval *input; + zend_string *name; + zend_ulong index; + + if (inputs == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(inputs) != IS_ARRAY) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "field_type", + NULL, + "inputs", + "expected_object_array" + ); + } + + ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(inputs), index, name, input) { + zend_ulong length; + zval marker; + zval stored; + + (void) index; + if (name == NULL || ZSTR_LEN(name) == 0) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "field_type", + NULL, + "inputs", + "input_name_must_be_non_empty_string" + ); + } + if (input == NULL + || Z_TYPE_P(input) != IS_ARRAY + || zend_hash_num_elements(Z_ARRVAL_P(input)) == 0) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "shape", + name, + "inputs", + "input_vector_must_be_non_empty_array" + ); + } + length = zend_hash_num_elements(Z_ARRVAL_P(input)); + if (length > (zend_ulong) ZEND_LONG_MAX) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "shape", + name, + "inputs", + "input_vector_too_large" + ); + } + ZVAL_TRUE(&marker); + zend_hash_update(Z_ARRVAL_P(defined), name, &marker); + ZVAL_LONG(&stored, (zend_long) length); + zend_hash_update(Z_ARRVAL_P(lengths), name, &stored); + } ZEND_HASH_FOREACH_END(); + + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_validate_sampler_op( + king_inference_model_object *model, + zval *entry, + zend_string *node_id, + zval *defined, + zval *lengths, + zend_string *op_name, + zend_ulong *length_out +) { + zend_string *logits_id = NULL; + zend_ulong logits_length = 0; + zend_ulong top_k = 0; + zend_ulong token_offset = 0; + zend_ulong sample_index = 0; + zend_long seed = 0; + double temperature = zend_string_equals_literal(op_name, "argmax_token") ? 0.0 : 1.0; + double top_p = 1.0; + + if (king_inference_cuda_decoder_graph_executor_defined_reference(model, entry, node_id, defined, "logits", &logits_id) != SUCCESS + || king_inference_cuda_decoder_graph_executor_reference_length(model, lengths, node_id, "logits", logits_id, &logits_length) != SUCCESS + || king_inference_cuda_decoder_graph_executor_non_negative_long_field(model, entry, node_id, "token_offset", false, 0, &token_offset) != SUCCESS) { + return FAILURE; + } + if (logits_length == 0) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "shape", + node_id, + "logits", + "sampler_requires_non_empty_logits" + ); + } + if (zend_string_equals_literal(op_name, "sample_token")) { + if (king_inference_cuda_decoder_graph_executor_finite_double_field(model, entry, node_id, "temperature", 1.0, false, &temperature) != SUCCESS + || king_inference_cuda_decoder_graph_executor_finite_double_field(model, entry, node_id, "top_p", 1.0, false, &top_p) != SUCCESS + || king_inference_cuda_decoder_graph_executor_non_negative_long_field(model, entry, node_id, "top_k", false, 0, &top_k) != SUCCESS + || king_inference_cuda_decoder_graph_executor_non_negative_long_field(model, entry, node_id, "sample_index", false, 0, &sample_index) != SUCCESS + || king_inference_cuda_decoder_graph_executor_optional_long_field(model, entry, node_id, "seed", 0, &seed) != SUCCESS) { + return FAILURE; + } + if (temperature < 0.0 || top_p <= 0.0 || top_p > 1.0) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "numeric_range", + node_id, + "temperature/top_p/top_k", + "sample_token_options_invalid" + ); + } + } + *length_out = 4; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_validate_op_contract( + king_inference_model_object *model, + zval *entry, + zend_string *node_id, + zend_string *op_name, + zval *defined, + zval *lengths, + zend_ulong *length_out +) { + double factor = 1.0; + + if (zend_string_equals_literal(op_name, "embedding")) { + return king_inference_cuda_decoder_graph_executor_validate_embedding_op(model, entry, node_id, length_out); + } + if (zend_string_equals_literal(op_name, "rms_norm")) { + return king_inference_cuda_decoder_graph_executor_validate_rms_norm_op(model, entry, node_id, defined, lengths, length_out); + } + if (zend_string_equals_literal(op_name, "linear")) { + return king_inference_cuda_decoder_graph_executor_validate_linear_op(model, entry, node_id, defined, lengths, length_out); + } + if (zend_string_equals_literal(op_name, "add") || zend_string_equals_literal(op_name, "mul")) { + return king_inference_cuda_decoder_graph_executor_validate_binary_vector_op(model, entry, node_id, defined, lengths, length_out); + } + if (zend_string_equals_literal(op_name, "scale")) { + if (king_inference_cuda_decoder_graph_executor_finite_double_field(model, entry, node_id, "scale", 1.0, false, &factor) != SUCCESS + || king_inference_cuda_decoder_graph_executor_finite_double_field(model, entry, node_id, "factor", factor, false, &factor) != SUCCESS) { + return FAILURE; + } + return king_inference_cuda_decoder_graph_executor_validate_unary_vector_op(model, entry, node_id, defined, lengths, "input", length_out); + } + if (zend_string_equals_literal(op_name, "silu") || zend_string_equals_literal(op_name, "gelu_tanh")) { + return king_inference_cuda_decoder_graph_executor_validate_unary_vector_op(model, entry, node_id, defined, lengths, "input", length_out); + } + if (zend_string_equals_literal(op_name, "slice")) { + return king_inference_cuda_decoder_graph_executor_validate_slice_op(model, entry, node_id, defined, lengths, length_out); + } + if (zend_string_equals_literal(op_name, "rope")) { + return king_inference_cuda_decoder_graph_executor_validate_rope_op(model, entry, node_id, defined, lengths, length_out); + } + if (zend_string_equals_literal(op_name, "kv_write")) { + return king_inference_cuda_decoder_graph_executor_validate_kv_write_op(model, entry, node_id, defined, lengths, length_out); + } + if (zend_string_equals_literal(op_name, "kv_attention")) { + return king_inference_cuda_decoder_graph_executor_validate_kv_attention_op(model, entry, node_id, defined, lengths, length_out); + } + if (zend_string_equals_literal(op_name, "stack")) { + return king_inference_cuda_decoder_graph_executor_validate_stack_op(model, entry, node_id, defined, lengths, length_out); + } + if (zend_string_equals_literal(op_name, "argmax_token") || zend_string_equals_literal(op_name, "sample_token")) { + return king_inference_cuda_decoder_graph_executor_validate_sampler_op(model, entry, node_id, defined, lengths, op_name, length_out); + } + + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "unsupported", + node_id, + "op", + ZSTR_VAL(op_name) + ); +} diff --git a/extension/src/inference/cuda/decoder_graph/core/cuda_decoder_graph_executor/result_lifecycle.inc b/extension/src/inference/cuda/decoder_graph/core/cuda_decoder_graph_executor/result_lifecycle.inc new file mode 100644 index 000000000..ff204cabf --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/core/cuda_decoder_graph_executor/result_lifecycle.inc @@ -0,0 +1,664 @@ +static void king_inference_cuda_decoder_graph_executor_copy_string_field( + zval *target, + zval *source, + const char *key +) { + zval *value = king_inference_array_find(source, key); + + if (value != NULL && Z_TYPE_P(value) == IS_STRING) { + add_assoc_str(target, key, zend_string_copy(Z_STR_P(value))); + } +} + +static void king_inference_cuda_decoder_graph_executor_copy_long_field( + zval *target, + zval *source, + const char *key +) { + zval *value = king_inference_array_find(source, key); + + if (value != NULL && Z_TYPE_P(value) == IS_LONG) { + add_assoc_long(target, key, Z_LVAL_P(value)); + } +} + +static void king_inference_cuda_decoder_graph_executor_copy_bool_field( + zval *target, + zval *source, + const char *key +) { + zval *value = king_inference_array_find(source, key); + + if (value != NULL) { + add_assoc_bool(target, key, zend_is_true(value)); + } +} +static zend_result king_inference_cuda_decoder_graph_executor_prepare_result( + king_inference_model_object *model, + zval *graph, + zval *result +) { + zval terminal; + zval *graph_terminal; + bool has_terminal = false; + + if (king_inference_cuda_decoder_graph_executor_validate_graph(model, graph) != SUCCESS) { + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_prepare_op_index(model, graph) != SUCCESS) { + return FAILURE; + } + + array_init(result); + add_assoc_string(result, "type", "gpu_decoder_graph_execution_result"); + add_assoc_string(result, "backend", "king_native_gpu"); + add_assoc_bool(result, "result_contract_ready", true); + add_assoc_bool(result, "graph_validated", true); + add_assoc_bool(result, "device_execution_result_ready", false); + add_assoc_bool(result, "token_result_ready", false); + add_assoc_bool(result, "decoded_token_ready", false); + add_assoc_bool(result, "execution_plan_ready", false); + add_assoc_bool(result, "graph_op_index_ready", true); + add_assoc_bool(result, "embedding_device_execution_ready", false); + add_assoc_bool(result, "first_device_op_executed", false); + add_assoc_bool(result, "silent_cpu_fallback", false); + add_assoc_long(result, "op_count", (zend_long) model->cuda_decoder_graph_executor_last_op_count); + add_assoc_long( + result, + "supported_op_count", + (zend_long) model->cuda_decoder_graph_executor_last_supported_op_count + ); + king_inference_cuda_decoder_graph_executor_copy_string_field(result, graph, "graph_type"); + king_inference_cuda_decoder_graph_executor_copy_string_field(result, graph, "output"); + king_inference_cuda_decoder_graph_executor_copy_string_field(result, graph, "token_selection"); + king_inference_cuda_decoder_graph_executor_copy_string_field(result, graph, "token_selection_op"); + king_inference_cuda_decoder_graph_executor_copy_long_field(result, graph, "token_id"); + king_inference_cuda_decoder_graph_executor_copy_long_field(result, graph, "position"); + king_inference_cuda_decoder_graph_executor_copy_long_field(result, graph, "block_count"); + king_inference_cuda_decoder_graph_executor_copy_long_field(result, graph, "attention_head_count"); + king_inference_cuda_decoder_graph_executor_copy_long_field(result, graph, "attention_head_count_kv"); + king_inference_cuda_decoder_graph_executor_copy_long_field(result, graph, "attention_key_length"); + king_inference_cuda_decoder_graph_executor_copy_long_field(result, graph, "attention_value_length"); + + graph_terminal = king_inference_array_find(graph, "terminal"); + array_init(&terminal); + if (graph_terminal != NULL && Z_TYPE_P(graph_terminal) == IS_ARRAY) { + has_terminal = true; + king_inference_cuda_decoder_graph_executor_copy_string_field(&terminal, graph_terminal, "hidden_output"); + king_inference_cuda_decoder_graph_executor_copy_string_field(&terminal, graph_terminal, "final_norm"); + king_inference_cuda_decoder_graph_executor_copy_string_field(&terminal, graph_terminal, "logits"); + king_inference_cuda_decoder_graph_executor_copy_string_field(&terminal, graph_terminal, "token_selection"); + king_inference_cuda_decoder_graph_executor_copy_string_field(&terminal, graph_terminal, "token_selection_op"); + king_inference_cuda_decoder_graph_executor_copy_bool_field(&terminal, graph_terminal, "emits_token"); + king_inference_cuda_decoder_graph_executor_copy_bool_field(&terminal, graph_terminal, "emits_logits"); + } + add_assoc_bool(result, "terminal_present", has_terminal); + add_assoc_zval(result, "terminal", &terminal); + if (king_inference_cuda_decoder_graph_executor_prepare_plan(model, graph, result) != SUCCESS) { + zval_ptr_dtor(result); + ZVAL_UNDEF(result); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_execute_initial_device_ops(model, graph, result) != SUCCESS) { + zval_ptr_dtor(result); + ZVAL_UNDEF(result); + return FAILURE; + } + + model->cuda_decoder_graph_executor_result_count++; + model->cuda_decoder_graph_executor_last_result_op_count = + model->cuda_decoder_graph_executor_last_op_count; + return SUCCESS; +} + +static void king_inference_cuda_decoder_graph_executor_release(king_inference_model_object *model) +{ + king_inference_cuda_decoder_graph_executor_reset_fields(model); +} + +static void king_inference_cuda_decoder_graph_executor_init( + king_inference_model_object *model, + king_inference_backend_kind backend_kind +) { + king_inference_cuda_decoder_graph_executor_release(model); + if (backend_kind != KING_INFERENCE_BACKEND_KING_NATIVE_GPU + || !king_inference_gpu_enabled(&model->config)) { + return; + } + + model->cuda_decoder_graph_executor_attempted = true; + if (!king_inference_cuda_decoder_graph_executor_dependencies_ready(model)) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_executor_dependencies_unavailable" + ); + return; + } + + model->cuda_decoder_graph_executor_available = true; + model->cuda_decoder_graph_executor_token_decode_available = true; + model->cuda_decoder_graph_executor_sampling_readback_available = true; + model->cuda_decoder_graph_executor_result_contract_available = true; + model->cuda_decoder_graph_executor_execution_plan_available = true; + model->cuda_decoder_graph_executor_embedding_execution_available = + model->cuda_embedding_row_available && model->cuda_device_allocator_available; + model->cuda_decoder_graph_executor_rms_norm_execution_available = + model->cuda_rms_norm_available && model->cuda_device_allocator_available; + model->cuda_decoder_graph_executor_linear_execution_available = + model->cuda_quantized_matvec_available && model->cuda_device_allocator_available; + model->cuda_decoder_graph_executor_slice_execution_available = + model->cuda_vector_slice_available && model->cuda_device_allocator_available; + model->cuda_decoder_graph_executor_rope_execution_available = + model->cuda_rope_f32_available && model->cuda_device_allocator_available; + model->cuda_decoder_graph_executor_kv_head_prepare_execution_available = + model->cuda_decoder_graph_executor_linear_execution_available + && model->cuda_decoder_graph_executor_slice_execution_available + && model->cuda_decoder_graph_executor_rope_execution_available; + model->cuda_decoder_graph_executor_kv_write_execution_available = + model->cuda_decoder_graph_executor_kv_head_prepare_execution_available + && model->cuda_device_kv_cache_available + && model->cuda_device_kv_cache_dtod_available; + model->cuda_decoder_graph_executor_kv_attention_execution_available = + model->cuda_decoder_graph_executor_kv_write_execution_available + && model->cuda_attention_scores_f32_available + && model->cuda_attention_softmax_f32_available + && model->cuda_attention_values_f32_available; + model->cuda_decoder_graph_executor_attention_stack_slot_execution_available = + model->cuda_decoder_graph_executor_kv_attention_execution_available + && model->cuda_vector_copy_to_offset_available + && model->cuda_device_allocator_available; + model->cuda_decoder_graph_executor_attention_heads_execution_available = + model->cuda_decoder_graph_executor_attention_stack_slot_execution_available; + model->cuda_decoder_graph_executor_attention_stack_execution_available = + model->cuda_decoder_graph_executor_attention_heads_execution_available; + model->cuda_decoder_graph_executor_attention_output_projection_execution_available = + model->cuda_decoder_graph_executor_attention_stack_execution_available + && model->cuda_decoder_graph_executor_linear_execution_available; + model->cuda_decoder_graph_executor_attention_residual_execution_available = + model->cuda_decoder_graph_executor_attention_output_projection_execution_available + && model->cuda_vector_add_available + && model->cuda_device_allocator_available; + model->cuda_decoder_graph_executor_ffn_norm_execution_available = + model->cuda_decoder_graph_executor_attention_residual_execution_available + && model->cuda_decoder_graph_executor_rms_norm_execution_available + && model->cuda_device_allocator_available; + model->cuda_decoder_graph_executor_ffn_gate_up_projection_execution_available = + model->cuda_decoder_graph_executor_ffn_norm_execution_available + && model->cuda_decoder_graph_executor_linear_execution_available + && model->cuda_device_allocator_available; + model->cuda_decoder_graph_executor_ffn_swiglu_execution_available = + model->cuda_decoder_graph_executor_ffn_gate_up_projection_execution_available + && model->cuda_ffn_swiglu_path_available + && model->cuda_ffn_swiglu_f32_available + && model->cuda_device_allocator_available; + model->cuda_decoder_graph_executor_ffn_down_projection_execution_available = + model->cuda_decoder_graph_executor_ffn_swiglu_execution_available + && model->cuda_decoder_graph_executor_linear_execution_available + && model->cuda_device_allocator_available; + model->cuda_decoder_graph_executor_ffn_output_residual_execution_available = + model->cuda_decoder_graph_executor_ffn_down_projection_execution_available + && model->cuda_decoder_graph_executor_attention_residual_execution_available + && model->cuda_device_vector_ops_available + && model->cuda_device_allocator_available; + model->cuda_decoder_graph_executor_final_norm_execution_available = + model->cuda_decoder_graph_executor_ffn_output_residual_execution_available + && model->cuda_decoder_graph_executor_rms_norm_execution_available + && model->cuda_device_allocator_available; + model->cuda_decoder_graph_executor_logits_projection_execution_available = + model->cuda_decoder_graph_executor_final_norm_execution_available + && model->cuda_output_projection_available + && model->cuda_device_allocator_available; + model->cuda_decoder_graph_executor_result = 0; + model->cuda_decoder_graph_executor_error[0] = '\0'; +} + +static void king_inference_cuda_decoder_graph_executor_add_status( + king_inference_model_object *model, + zval *return_value +) { + zval executor; + zval supported_ops; + + array_init(&executor); + add_assoc_bool(&executor, "attempted", model->cuda_decoder_graph_executor_attempted); + add_assoc_bool(&executor, "available", model->cuda_decoder_graph_executor_available); + add_assoc_bool(&executor, "token_decode_available", model->cuda_decoder_graph_executor_token_decode_available); + add_assoc_bool( + &executor, + "sampling_readback_available", + model->cuda_decoder_graph_executor_sampling_readback_available + ); + add_assoc_bool( + &executor, + "result_contract_available", + model->cuda_decoder_graph_executor_result_contract_available + ); + add_assoc_bool( + &executor, + "execution_plan_available", + model->cuda_decoder_graph_executor_execution_plan_available + ); + add_assoc_bool( + &executor, + "embedding_execution_available", + model->cuda_decoder_graph_executor_embedding_execution_available + ); + add_assoc_bool( + &executor, + "rms_norm_execution_available", + model->cuda_decoder_graph_executor_rms_norm_execution_available + ); + add_assoc_bool( + &executor, + "linear_execution_available", + model->cuda_decoder_graph_executor_linear_execution_available + ); + add_assoc_bool( + &executor, + "slice_execution_available", + model->cuda_decoder_graph_executor_slice_execution_available + ); + add_assoc_bool( + &executor, + "rope_execution_available", + model->cuda_decoder_graph_executor_rope_execution_available + ); + add_assoc_bool( + &executor, + "kv_head_prepare_execution_available", + model->cuda_decoder_graph_executor_kv_head_prepare_execution_available + ); + add_assoc_bool( + &executor, + "kv_write_execution_available", + model->cuda_decoder_graph_executor_kv_write_execution_available + ); + add_assoc_bool( + &executor, + "kv_attention_execution_available", + model->cuda_decoder_graph_executor_kv_attention_execution_available + ); + add_assoc_bool( + &executor, + "attention_stack_slot_execution_available", + model->cuda_decoder_graph_executor_attention_stack_slot_execution_available + ); + add_assoc_bool( + &executor, + "attention_heads_execution_available", + model->cuda_decoder_graph_executor_attention_heads_execution_available + ); + add_assoc_bool(&executor, "attention_stack_execution_available", model->cuda_decoder_graph_executor_attention_stack_execution_available); + add_assoc_bool(&executor, "attention_output_projection_execution_available", model->cuda_decoder_graph_executor_attention_output_projection_execution_available); + add_assoc_bool(&executor, "attention_residual_execution_available", model->cuda_decoder_graph_executor_attention_residual_execution_available); + add_assoc_bool(&executor, "ffn_norm_execution_available", model->cuda_decoder_graph_executor_ffn_norm_execution_available); + add_assoc_bool(&executor, "ffn_gate_up_projection_execution_available", model->cuda_decoder_graph_executor_ffn_gate_up_projection_execution_available); + add_assoc_bool(&executor, "ffn_swiglu_execution_available", model->cuda_decoder_graph_executor_ffn_swiglu_execution_available); + add_assoc_bool(&executor, "ffn_down_projection_execution_available", model->cuda_decoder_graph_executor_ffn_down_projection_execution_available); + add_assoc_bool(&executor, "ffn_output_residual_execution_available", model->cuda_decoder_graph_executor_ffn_output_residual_execution_available); + add_assoc_bool(&executor, "final_norm_execution_available", model->cuda_decoder_graph_executor_final_norm_execution_available); + add_assoc_bool(&executor, "logits_projection_execution_available", model->cuda_decoder_graph_executor_logits_projection_execution_available); + add_assoc_long(&executor, "graphs", (zend_long) model->cuda_decoder_graph_executor_graph_count); + add_assoc_long(&executor, "last_op_count", (zend_long) model->cuda_decoder_graph_executor_last_op_count); + add_assoc_long( + &executor, + "last_supported_op_count", + (zend_long) model->cuda_decoder_graph_executor_last_supported_op_count + ); + add_assoc_long(&executor, "result_envelopes", (zend_long) model->cuda_decoder_graph_executor_result_count); + add_assoc_long(&executor, "execution_plans", (zend_long) model->cuda_decoder_graph_executor_plan_count); + add_assoc_long( + &executor, + "last_result_op_count", + (zend_long) model->cuda_decoder_graph_executor_last_result_op_count + ); + add_assoc_long( + &executor, + "last_plan_op_count", + (zend_long) model->cuda_decoder_graph_executor_last_plan_op_count + ); + add_assoc_long( + &executor, + "last_plan_device_ops", + (zend_long) model->cuda_decoder_graph_executor_last_plan_device_ops + ); + add_assoc_long( + &executor, + "last_plan_host_sampling_ops", + (zend_long) model->cuda_decoder_graph_executor_last_plan_host_sampling_ops + ); + add_assoc_long( + &executor, + "embedding_device_executions", + (zend_long) model->cuda_decoder_graph_executor_embedding_execution_count + ); + add_assoc_long( + &executor, + "last_embedding_width", + (zend_long) model->cuda_decoder_graph_executor_last_embedding_width + ); + add_assoc_long( + &executor, + "rms_norm_device_executions", + (zend_long) model->cuda_decoder_graph_executor_rms_norm_execution_count + ); + add_assoc_long( + &executor, + "last_rms_norm_width", + (zend_long) model->cuda_decoder_graph_executor_last_rms_norm_width + ); + add_assoc_long( + &executor, + "linear_device_executions", + (zend_long) model->cuda_decoder_graph_executor_linear_execution_count + ); + add_assoc_long( + &executor, + "last_linear_input_width", + (zend_long) model->cuda_decoder_graph_executor_last_linear_input_width + ); + add_assoc_long( + &executor, + "last_linear_rows", + (zend_long) model->cuda_decoder_graph_executor_last_linear_rows + ); + add_assoc_long( + &executor, + "slice_device_executions", + (zend_long) model->cuda_decoder_graph_executor_slice_execution_count + ); + add_assoc_long( + &executor, + "last_slice_offset", + (zend_long) model->cuda_decoder_graph_executor_last_slice_offset + ); + add_assoc_long( + &executor, + "last_slice_length", + (zend_long) model->cuda_decoder_graph_executor_last_slice_length + ); + add_assoc_long( + &executor, + "rope_device_executions", + (zend_long) model->cuda_decoder_graph_executor_rope_execution_count + ); + add_assoc_long( + &executor, + "last_rope_length", + (zend_long) model->cuda_decoder_graph_executor_last_rope_length + ); + add_assoc_long( + &executor, + "last_rope_position", + (zend_long) model->cuda_decoder_graph_executor_last_rope_position + ); + add_assoc_long( + &executor, + "kv_head_prepare_device_executions", + (zend_long) model->cuda_decoder_graph_executor_kv_head_prepare_execution_count + ); + add_assoc_long( + &executor, + "last_kv_head_key_length", + (zend_long) model->cuda_decoder_graph_executor_last_kv_head_key_length + ); + add_assoc_long( + &executor, + "last_kv_head_value_length", + (zend_long) model->cuda_decoder_graph_executor_last_kv_head_value_length + ); + add_assoc_long( + &executor, + "kv_write_device_executions", + (zend_long) model->cuda_decoder_graph_executor_kv_write_execution_count + ); + add_assoc_long( + &executor, + "last_kv_write_key_length", + (zend_long) model->cuda_decoder_graph_executor_last_kv_write_key_length + ); + add_assoc_long( + &executor, + "last_kv_write_value_length", + (zend_long) model->cuda_decoder_graph_executor_last_kv_write_value_length + ); + add_assoc_long( + &executor, + "last_kv_write_layer", + (zend_long) model->cuda_decoder_graph_executor_last_kv_write_layer + ); + add_assoc_long( + &executor, + "last_kv_write_head", + (zend_long) model->cuda_decoder_graph_executor_last_kv_write_head + ); + add_assoc_long( + &executor, + "last_kv_write_position", + (zend_long) model->cuda_decoder_graph_executor_last_kv_write_position + ); + add_assoc_long( + &executor, + "kv_attention_device_executions", + (zend_long) model->cuda_decoder_graph_executor_kv_attention_execution_count + ); + add_assoc_long( + &executor, + "last_kv_attention_key_length", + (zend_long) model->cuda_decoder_graph_executor_last_kv_attention_key_length + ); + add_assoc_long( + &executor, + "last_kv_attention_value_length", + (zend_long) model->cuda_decoder_graph_executor_last_kv_attention_value_length + ); + add_assoc_long( + &executor, + "last_kv_attention_layer", + (zend_long) model->cuda_decoder_graph_executor_last_kv_attention_layer + ); + add_assoc_long( + &executor, + "last_kv_attention_head", + (zend_long) model->cuda_decoder_graph_executor_last_kv_attention_head + ); + add_assoc_long( + &executor, + "last_kv_attention_slot_start", + (zend_long) model->cuda_decoder_graph_executor_last_kv_attention_slot_start + ); + add_assoc_long( + &executor, + "last_kv_attention_slot_count", + (zend_long) model->cuda_decoder_graph_executor_last_kv_attention_slot_count + ); + add_assoc_long( + &executor, + "attention_stack_slot_device_executions", + (zend_long) model->cuda_decoder_graph_executor_attention_stack_slot_execution_count + ); + add_assoc_long( + &executor, + "last_attention_stack_width", + (zend_long) model->cuda_decoder_graph_executor_last_attention_stack_width + ); + add_assoc_long( + &executor, + "last_attention_stack_input_index", + (zend_long) model->cuda_decoder_graph_executor_last_attention_stack_input_index + ); + add_assoc_long( + &executor, + "last_attention_stack_input_count", + (zend_long) model->cuda_decoder_graph_executor_last_attention_stack_input_count + ); + add_assoc_long( + &executor, + "attention_heads_device_executions", + (zend_long) model->cuda_decoder_graph_executor_attention_heads_execution_count + ); + add_assoc_long(&executor, "attention_stack_device_executions", (zend_long) model->cuda_decoder_graph_executor_attention_stack_execution_count); + add_assoc_long(&executor, "attention_output_projection_device_executions", (zend_long) model->cuda_decoder_graph_executor_attention_output_projection_execution_count); + add_assoc_long(&executor, "attention_residual_device_executions", (zend_long) model->cuda_decoder_graph_executor_attention_residual_execution_count); + add_assoc_long(&executor, "ffn_norm_device_executions", (zend_long) model->cuda_decoder_graph_executor_ffn_norm_execution_count); + add_assoc_long(&executor, "ffn_gate_up_projection_device_executions", (zend_long) model->cuda_decoder_graph_executor_ffn_gate_up_projection_execution_count); + add_assoc_long(&executor, "ffn_swiglu_device_executions", (zend_long) model->cuda_decoder_graph_executor_ffn_swiglu_execution_count); + add_assoc_long(&executor, "ffn_down_projection_device_executions", (zend_long) model->cuda_decoder_graph_executor_ffn_down_projection_execution_count); + add_assoc_long(&executor, "ffn_output_residual_device_executions", (zend_long) model->cuda_decoder_graph_executor_ffn_output_residual_execution_count); + add_assoc_long(&executor, "final_norm_device_executions", (zend_long) model->cuda_decoder_graph_executor_final_norm_execution_count); + add_assoc_long(&executor, "logits_projection_device_executions", (zend_long) model->cuda_decoder_graph_executor_logits_projection_execution_count); + add_assoc_long( + &executor, + "last_attention_stack_expected_heads", + (zend_long) model->cuda_decoder_graph_executor_last_attention_stack_expected_heads + ); + add_assoc_long( + &executor, + "last_attention_stack_prepared_heads", + (zend_long) model->cuda_decoder_graph_executor_last_attention_stack_prepared_heads + ); + add_assoc_long(&executor, "last_attention_output_projection_width", (zend_long) model->cuda_decoder_graph_executor_last_attention_output_projection_width); + add_assoc_long(&executor, "last_attention_residual_width", (zend_long) model->cuda_decoder_graph_executor_last_attention_residual_width); + add_assoc_long(&executor, "last_ffn_norm_width", (zend_long) model->cuda_decoder_graph_executor_last_ffn_norm_width); + add_assoc_long(&executor, "last_ffn_gate_rows", (zend_long) model->cuda_decoder_graph_executor_last_ffn_gate_rows); + add_assoc_long(&executor, "last_ffn_up_rows", (zend_long) model->cuda_decoder_graph_executor_last_ffn_up_rows); + add_assoc_long(&executor, "last_ffn_swiglu_width", (zend_long) model->cuda_decoder_graph_executor_last_ffn_swiglu_width); + add_assoc_long(&executor, "last_ffn_down_rows", (zend_long) model->cuda_decoder_graph_executor_last_ffn_down_rows); + add_assoc_long(&executor, "last_ffn_output_residual_width", (zend_long) model->cuda_decoder_graph_executor_last_ffn_output_residual_width); + add_assoc_long(&executor, "last_final_norm_width", (zend_long) model->cuda_decoder_graph_executor_last_final_norm_width); + add_assoc_long(&executor, "last_logits_rows", (zend_long) model->cuda_decoder_graph_executor_last_logits_rows); + if (model->cuda_decoder_graph_executor_last_embedding_token_available + && model->cuda_decoder_graph_executor_last_embedding_token_id <= (zend_ulong) ZEND_LONG_MAX) { + add_assoc_long( + &executor, + "last_embedding_token_id", + (zend_long) model->cuda_decoder_graph_executor_last_embedding_token_id + ); + } else { + add_assoc_null(&executor, "last_embedding_token_id"); + } + add_assoc_bool(&executor, "device_execution_result_ready", false); + add_assoc_long(&executor, "result", model->cuda_decoder_graph_executor_result); + add_assoc_string(&executor, "error", model->cuda_decoder_graph_executor_error); + + array_init(&supported_ops); + add_next_index_string(&supported_ops, "embedding"); + add_next_index_string(&supported_ops, "rms_norm"); + add_next_index_string(&supported_ops, "linear"); + add_next_index_string(&supported_ops, "add"); + add_next_index_string(&supported_ops, "scale"); + add_next_index_string(&supported_ops, "mul"); + add_next_index_string(&supported_ops, "silu"); + add_next_index_string(&supported_ops, "slice"); + add_next_index_string(&supported_ops, "rope"); + add_next_index_string(&supported_ops, "kv_write"); + add_next_index_string(&supported_ops, "kv_attention"); + add_next_index_string(&supported_ops, "stack"); + add_next_index_string(&supported_ops, "argmax_token"); + add_next_index_string(&supported_ops, "sample_token"); + add_assoc_zval(&executor, "supported_ops", &supported_ops); + + add_assoc_zval(return_value, "decoder_graph_executor", &executor); + add_assoc_bool(return_value, "decoder_graph_executor_ready", model->cuda_decoder_graph_executor_available); + add_assoc_bool( + return_value, + "decoder_graph_result_contract_ready", + model->cuda_decoder_graph_executor_result_contract_available + ); + add_assoc_bool( + return_value, + "decoder_graph_execution_plan_ready", + model->cuda_decoder_graph_executor_execution_plan_available + ); + add_assoc_bool( + return_value, + "decoder_graph_embedding_execution_ready", + model->cuda_decoder_graph_executor_embedding_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_rms_norm_execution_ready", + model->cuda_decoder_graph_executor_rms_norm_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_linear_execution_ready", + model->cuda_decoder_graph_executor_linear_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_slice_execution_ready", + model->cuda_decoder_graph_executor_slice_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_rope_execution_ready", + model->cuda_decoder_graph_executor_rope_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_kv_head_prepare_execution_ready", + model->cuda_decoder_graph_executor_kv_head_prepare_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_kv_write_execution_ready", + model->cuda_decoder_graph_executor_kv_write_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_kv_attention_execution_ready", + model->cuda_decoder_graph_executor_kv_attention_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_attention_stack_slot_execution_ready", + model->cuda_decoder_graph_executor_attention_stack_slot_execution_available + ); + add_assoc_bool( + return_value, + "decoder_graph_attention_heads_execution_ready", + model->cuda_decoder_graph_executor_attention_heads_execution_available + ); + add_assoc_bool(return_value, "decoder_graph_attention_stack_execution_ready", model->cuda_decoder_graph_executor_attention_stack_execution_available); + add_assoc_bool(return_value, "decoder_graph_attention_output_projection_execution_ready", model->cuda_decoder_graph_executor_attention_output_projection_execution_available); + add_assoc_bool(return_value, "decoder_graph_attention_residual_execution_ready", model->cuda_decoder_graph_executor_attention_residual_execution_available); + add_assoc_bool(return_value, "decoder_graph_ffn_norm_execution_ready", model->cuda_decoder_graph_executor_ffn_norm_execution_available); + add_assoc_bool(return_value, "decoder_graph_ffn_gate_up_projection_execution_ready", model->cuda_decoder_graph_executor_ffn_gate_up_projection_execution_available); + add_assoc_bool(return_value, "decoder_graph_ffn_swiglu_execution_ready", model->cuda_decoder_graph_executor_ffn_swiglu_execution_available); + add_assoc_bool(return_value, "decoder_graph_ffn_down_projection_execution_ready", model->cuda_decoder_graph_executor_ffn_down_projection_execution_available); + add_assoc_bool(return_value, "decoder_graph_ffn_output_residual_execution_ready", model->cuda_decoder_graph_executor_ffn_output_residual_execution_available); + add_assoc_bool(return_value, "decoder_graph_final_norm_execution_ready", model->cuda_decoder_graph_executor_final_norm_execution_available); + add_assoc_bool(return_value, "decoder_graph_logits_projection_execution_ready", model->cuda_decoder_graph_executor_logits_projection_execution_available); +} + +static void king_inference_cuda_decoder_graph_executor_apply_status( + king_inference_model_object *model, + zval *return_value +) { + zval *reason; + bool reason_can_be_executor; + + king_inference_cuda_decoder_graph_executor_add_status(model, return_value); + if (!model->cuda_decoder_graph_executor_attempted + || model->cuda_decoder_graph_executor_available + || !king_inference_gpu_enabled(&model->config)) { + return; + } + + reason = zend_hash_str_find(Z_ARRVAL_P(return_value), "reason", sizeof("reason") - 1); + reason_can_be_executor = reason == NULL + || Z_TYPE_P(reason) != IS_STRING + || zend_string_equals_literal(Z_STR_P(reason), "gpu_ready") + || zend_string_equals_literal(Z_STR_P(reason), "gpu_decoder_kernel_not_implemented"); + if (reason_can_be_executor) { + king_inference_cuda_context_prepend_return_reason( + return_value, + "gpu_decoder_graph_executor_unavailable" + ); + add_assoc_string(return_value, "reason", "gpu_decoder_graph_executor_unavailable"); + } else { + king_inference_gpu_status_append_return_reason(return_value, "gpu_decoder_graph_executor_unavailable"); + } +} diff --git a/extension/src/inference/cuda/decoder_graph/core/cuda_decoder_graph_executor_reset.inc b/extension/src/inference/cuda/decoder_graph/core/cuda_decoder_graph_executor_reset.inc new file mode 100644 index 000000000..5575892ab --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/core/cuda_decoder_graph_executor_reset.inc @@ -0,0 +1,106 @@ +/* + * CUDA decoder graph executor reset state. + */ + +static void king_inference_cuda_decoder_graph_executor_reset_fields(king_inference_model_object *model) +{ + model->cuda_decoder_graph_executor_result = 0; + model->cuda_decoder_graph_executor_error[0] = '\0'; + model->cuda_decoder_graph_executor_graph_count = 0; + model->cuda_decoder_graph_executor_last_op_count = 0; + model->cuda_decoder_graph_executor_last_supported_op_count = 0; + model->cuda_decoder_graph_executor_result_count = 0; + model->cuda_decoder_graph_executor_last_result_op_count = 0; + model->cuda_decoder_graph_executor_plan_count = 0; + model->cuda_decoder_graph_executor_last_plan_op_count = 0; + model->cuda_decoder_graph_executor_last_plan_device_ops = 0; + model->cuda_decoder_graph_executor_last_plan_host_sampling_ops = 0; + model->cuda_decoder_graph_executor_tensor_lookup_count = 0; + model->cuda_decoder_graph_executor_op_index_lookup_count = 0; + model->cuda_decoder_graph_executor_op_index_hit_count = 0; + model->cuda_decoder_graph_executor_op_index_fallback_scan_count = 0; + model->cuda_decoder_graph_executor_embedding_execution_count = 0; + model->cuda_decoder_graph_executor_last_embedding_width = 0; + model->cuda_decoder_graph_executor_last_embedding_token_id = 0; + model->cuda_decoder_graph_executor_rms_norm_execution_count = 0; + model->cuda_decoder_graph_executor_last_rms_norm_width = 0; + model->cuda_decoder_graph_executor_linear_execution_count = 0; + model->cuda_decoder_graph_executor_last_linear_input_width = 0; + model->cuda_decoder_graph_executor_last_linear_rows = 0; + model->cuda_decoder_graph_executor_slice_execution_count = 0; + model->cuda_decoder_graph_executor_last_slice_offset = 0; + model->cuda_decoder_graph_executor_last_slice_length = 0; + model->cuda_decoder_graph_executor_rope_execution_count = 0; + model->cuda_decoder_graph_executor_last_rope_length = 0; + model->cuda_decoder_graph_executor_last_rope_position = 0; + model->cuda_decoder_graph_executor_kv_head_prepare_execution_count = 0; + model->cuda_decoder_graph_executor_last_kv_head_key_length = 0; + model->cuda_decoder_graph_executor_last_kv_head_value_length = 0; + model->cuda_decoder_graph_executor_kv_write_execution_count = 0; + model->cuda_decoder_graph_executor_last_kv_write_key_length = 0; + model->cuda_decoder_graph_executor_last_kv_write_value_length = 0; + model->cuda_decoder_graph_executor_last_kv_write_layer = 0; + model->cuda_decoder_graph_executor_last_kv_write_head = 0; + model->cuda_decoder_graph_executor_last_kv_write_position = 0; + model->cuda_decoder_graph_executor_kv_attention_execution_count = 0; + model->cuda_decoder_graph_executor_last_kv_attention_key_length = 0; + model->cuda_decoder_graph_executor_last_kv_attention_value_length = 0; + model->cuda_decoder_graph_executor_last_kv_attention_layer = 0; + model->cuda_decoder_graph_executor_last_kv_attention_head = 0; + model->cuda_decoder_graph_executor_last_kv_attention_slot_start = 0; + model->cuda_decoder_graph_executor_last_kv_attention_slot_count = 0; + model->cuda_decoder_graph_executor_attention_stack_slot_execution_count = 0; + model->cuda_decoder_graph_executor_last_attention_stack_width = 0; + model->cuda_decoder_graph_executor_last_attention_stack_input_index = 0; + model->cuda_decoder_graph_executor_last_attention_stack_input_count = 0; + model->cuda_decoder_graph_executor_attention_heads_execution_count = 0; + model->cuda_decoder_graph_executor_attention_stack_execution_count = 0; + model->cuda_decoder_graph_executor_attention_output_projection_execution_count = 0; + model->cuda_decoder_graph_executor_attention_residual_execution_count = 0; + model->cuda_decoder_graph_executor_ffn_norm_execution_count = 0; + model->cuda_decoder_graph_executor_ffn_gate_up_projection_execution_count = 0; + model->cuda_decoder_graph_executor_ffn_swiglu_execution_count = 0; + model->cuda_decoder_graph_executor_ffn_down_projection_execution_count = 0; + model->cuda_decoder_graph_executor_ffn_output_residual_execution_count = 0; + model->cuda_decoder_graph_executor_final_norm_execution_count = 0; + model->cuda_decoder_graph_executor_logits_projection_execution_count = 0; + model->cuda_decoder_graph_executor_last_attention_stack_expected_heads = 0; + model->cuda_decoder_graph_executor_last_attention_stack_prepared_heads = 0; + model->cuda_decoder_graph_executor_last_attention_output_projection_width = 0; + model->cuda_decoder_graph_executor_last_attention_residual_width = 0; + model->cuda_decoder_graph_executor_last_ffn_norm_width = 0; + model->cuda_decoder_graph_executor_last_ffn_gate_rows = 0; + model->cuda_decoder_graph_executor_last_ffn_up_rows = 0; + model->cuda_decoder_graph_executor_last_ffn_swiglu_width = 0; + model->cuda_decoder_graph_executor_last_ffn_down_rows = 0; + model->cuda_decoder_graph_executor_last_ffn_output_residual_width = 0; + model->cuda_decoder_graph_executor_last_final_norm_width = 0; + model->cuda_decoder_graph_executor_last_logits_rows = 0; + model->cuda_decoder_graph_executor_attempted = false; + model->cuda_decoder_graph_executor_available = false; + model->cuda_decoder_graph_executor_token_decode_available = false; + model->cuda_decoder_graph_executor_sampling_readback_available = false; + model->cuda_decoder_graph_executor_result_contract_available = false; + model->cuda_decoder_graph_executor_execution_plan_available = false; + model->cuda_decoder_graph_executor_embedding_execution_available = false; + model->cuda_decoder_graph_executor_last_embedding_token_available = false; + model->cuda_decoder_graph_executor_rms_norm_execution_available = false; + model->cuda_decoder_graph_executor_linear_execution_available = false; + model->cuda_decoder_graph_executor_slice_execution_available = false; + model->cuda_decoder_graph_executor_rope_execution_available = false; + model->cuda_decoder_graph_executor_kv_head_prepare_execution_available = false; + model->cuda_decoder_graph_executor_kv_write_execution_available = false; + model->cuda_decoder_graph_executor_kv_attention_execution_available = false; + model->cuda_decoder_graph_executor_attention_stack_slot_execution_available = false; + model->cuda_decoder_graph_executor_attention_heads_execution_available = false; + model->cuda_decoder_graph_executor_attention_stack_execution_available = false; + model->cuda_decoder_graph_executor_attention_output_projection_execution_available = false; + model->cuda_decoder_graph_executor_attention_residual_execution_available = false; + model->cuda_decoder_graph_executor_ffn_norm_execution_available = false; + model->cuda_decoder_graph_executor_ffn_gate_up_projection_execution_available = false; + model->cuda_decoder_graph_executor_ffn_swiglu_execution_available = false; + model->cuda_decoder_graph_executor_ffn_down_projection_execution_available = false; + model->cuda_decoder_graph_executor_ffn_output_residual_execution_available = false; + model->cuda_decoder_graph_executor_final_norm_execution_available = false; + model->cuda_decoder_graph_executor_logits_projection_execution_available = false; +} diff --git a/extension/src/inference/cuda/decoder_graph/device/compare/attention/cuda_decoder_graph_executor_attention_residual_compare.inc b/extension/src/inference/cuda/decoder_graph/device/compare/attention/cuda_decoder_graph_executor_attention_residual_compare.inc new file mode 100644 index 000000000..118462316 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/device/compare/attention/cuda_decoder_graph_executor_attention_residual_compare.inc @@ -0,0 +1,345 @@ +/* + * Internal CUDA attention output-projection plus residual compare for block-0. + */ + +static zend_result king_inference_cuda_decoder_graph_executor_compare_attention_residual_add( + king_inference_model_object *model, + king_inference_cuda_device_ptr left_output, + zend_ulong left_width, + king_inference_cuda_device_ptr right_output, + zend_ulong right_width, + king_inference_cuda_device_ptr residual_output, + zend_ulong residual_width, + zval *residual_probe +) { + king_inference_cuda_memcpy_dtoh_fn cu_memcpy_dtoh; + float *left = NULL; + float *right = NULL; + float *expected = NULL; + zval compare; + zval *matched; + int result; + + if (!king_inference_cuda_device_vector_numeric_compare_enabled(&model->config)) { + return SUCCESS; + } + if (left_output == 0 + || right_output == 0 + || residual_output == 0 + || left_width == 0 + || left_width != right_width + || left_width != residual_width + || left_width > (zend_ulong) (SIZE_MAX / sizeof(float))) { + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_attention_residual_add_numeric_compare"); + add_assoc_string(&compare, "status", "attention_residual_add_compare_argument_invalid"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(residual_probe, "attention_residual_add_numeric_compare_failed", true); + add_assoc_zval(residual_probe, "attention_residual_add_numeric_compare", &compare); + return FAILURE; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_attention_residual_add_numeric_compare"); + add_assoc_string(&compare, "status", "cuda_context_current_failed"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(residual_probe, "attention_residual_add_numeric_compare_failed", true); + add_assoc_zval(residual_probe, "attention_residual_add_numeric_compare", &compare); + return FAILURE; + } + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH_v2"); + if (cu_memcpy_dtoh == NULL) { + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH"); + } + if (cu_memcpy_dtoh == NULL) { + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_attention_residual_add_numeric_compare"); + add_assoc_string(&compare, "status", "cuMemcpyDtoH_unavailable"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(residual_probe, "attention_residual_add_numeric_compare_failed", true); + add_assoc_zval(residual_probe, "attention_residual_add_numeric_compare", &compare); + return FAILURE; + } + + left = emalloc((size_t) left_width * sizeof(float)); + right = emalloc((size_t) right_width * sizeof(float)); + expected = emalloc((size_t) residual_width * sizeof(float)); + result = cu_memcpy_dtoh(left, left_output, (size_t) left_width * sizeof(float)); + if (result == 0) { + result = cu_memcpy_dtoh(right, right_output, (size_t) right_width * sizeof(float)); + } + if (result != 0) { + efree(expected); + efree(right); + efree(left); + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_attention_residual_add_numeric_compare"); + add_assoc_string(&compare, "status", "cuMemcpyDtoH_vector_failed"); + add_assoc_long(&compare, "result", result); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(residual_probe, "attention_residual_add_numeric_compare_failed", true); + add_assoc_zval(residual_probe, "attention_residual_add_numeric_compare", &compare); + return FAILURE; + } + for (zend_ulong i = 0; i < residual_width; i++) { + expected[i] = left[i] + right[i]; + } + efree(right); + efree(left); + + if (king_inference_cuda_device_vector_numeric_compare( + model, + "block0_attention_residual_add", + "attention_residual", + 0, + NULL, + 0, + residual_output, + residual_width, + expected, + residual_width, + 0.00001, + &compare + ) != SUCCESS) { + efree(expected); + add_assoc_string(&compare, "type", "gpu_decoder_attention_residual_add_numeric_compare"); + add_assoc_string(&compare, "reference", "cpu_hidden_plus_attention_projection_or_post_norm"); + add_assoc_long(&compare, "width", (zend_long) residual_width); + add_assoc_bool(residual_probe, "attention_residual_add_numeric_compare_failed", true); + add_assoc_zval(residual_probe, "attention_residual_add_numeric_compare", &compare); + return FAILURE; + } + efree(expected); + add_assoc_string(&compare, "type", "gpu_decoder_attention_residual_add_numeric_compare"); + add_assoc_string(&compare, "reference", "cpu_hidden_plus_attention_projection_or_post_norm"); + add_assoc_long(&compare, "width", (zend_long) residual_width); + matched = king_inference_array_find(&compare, "matched"); + add_assoc_bool(residual_probe, "attention_residual_add_numeric_compare_failed", matched == NULL || !zend_is_true(matched)); + add_assoc_zval(residual_probe, "attention_residual_add_numeric_compare", &compare); + return matched != NULL && zend_is_true(matched) ? SUCCESS : FAILURE; +} + +static zend_result king_inference_cuda_decoder_graph_executor_compare_block0_attention_projection_residual( + king_inference_model_object *model, + zval *graph, + zend_string *stack_id, + king_inference_cuda_device_ptr stack_output, + zend_ulong stack_width, + king_inference_cuda_device_ptr hidden_output, + zend_ulong hidden_width, + king_inference_cuda_device_ptr residual_output, + zend_ulong residual_width, + zval *residual_probe +) { + zval *projection_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, stack_id, "linear"); + zend_string *weight_name = NULL; + king_inference_tensor_native_view weight_view; + king_inference_cuda_memcpy_dtoh_fn cu_memcpy_dtoh; + zend_ulong cols = 0; + zend_ulong rows = 0; + float *stack = NULL; + float *hidden = NULL; + float *actual = NULL; + double *stack_d = NULL; + double *expected = NULL; + double max_expected_abs = 0.0; + double max_abs_diff = 0.0; + double tolerance; + zend_ulong matched_values = 0; + zend_ulong sample_limit; + bool matched = true; + int result; + zval compare; + zval samples; + + if (!king_inference_cuda_device_vector_numeric_compare_enabled(&model->config)) { + return SUCCESS; + } + if (projection_op == NULL + || king_inference_graph_required_string(projection_op, "weight", &weight_name) != SUCCESS + || !zend_string_equals_literal(weight_name, "blk.0.attn_output.weight")) { + return SUCCESS; + } + + if (stack_output == 0 + || hidden_output == 0 + || residual_output == 0 + || stack_width == 0 + || hidden_width == 0 + || residual_width == 0 + || hidden_width != residual_width + || (stack_width > 0 && hidden_width > (zend_ulong) -1 / stack_width) + || stack_width > (zend_ulong) (SIZE_MAX / sizeof(float)) + || hidden_width > (zend_ulong) (SIZE_MAX / sizeof(float))) { + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_attention_projection_residual_numeric_compare"); + add_assoc_string(&compare, "status", "attention_projection_residual_compare_argument_invalid"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(residual_probe, "attention_projection_residual_numeric_compare_failed", true); + add_assoc_zval(residual_probe, "attention_projection_residual_numeric_compare", &compare); + return FAILURE; + } + if (king_inference_tensor_native_view_resolve(model, weight_name, &weight_view) != SUCCESS) { + return FAILURE; + } + if (weight_view.rank != 2 + || king_inference_tensor_dimension_at(weight_view.dimensions, 0, &cols) != SUCCESS + || king_inference_tensor_dimension_at(weight_view.dimensions, 1, &rows) != SUCCESS + || cols != stack_width + || rows < hidden_width) { + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_attention_projection_residual_numeric_compare"); + add_assoc_string(&compare, "status", "attention_projection_residual_tensor_shape_invalid"); + add_assoc_bool(&compare, "matched", false); + add_assoc_str(&compare, "tensor", zend_string_copy(weight_name)); + add_assoc_long(&compare, "stack_width", (zend_long) stack_width); + add_assoc_long(&compare, "hidden_width", (zend_long) hidden_width); + add_assoc_long(&compare, "rows", (zend_long) rows); + add_assoc_long(&compare, "cols", (zend_long) cols); + add_assoc_bool(residual_probe, "attention_projection_residual_numeric_compare_failed", true); + add_assoc_zval(residual_probe, "attention_projection_residual_numeric_compare", &compare); + return FAILURE; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_attention_projection_residual_numeric_compare"); + add_assoc_string(&compare, "status", "cuda_context_current_failed"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(residual_probe, "attention_projection_residual_numeric_compare_failed", true); + add_assoc_zval(residual_probe, "attention_projection_residual_numeric_compare", &compare); + return FAILURE; + } + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH_v2"); + if (cu_memcpy_dtoh == NULL) { + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH"); + } + if (cu_memcpy_dtoh == NULL) { + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_attention_projection_residual_numeric_compare"); + add_assoc_string(&compare, "status", "cuMemcpyDtoH_unavailable"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(residual_probe, "attention_projection_residual_numeric_compare_failed", true); + add_assoc_zval(residual_probe, "attention_projection_residual_numeric_compare", &compare); + return FAILURE; + } + + stack = emalloc((size_t) stack_width * sizeof(float)); + hidden = emalloc((size_t) hidden_width * sizeof(float)); + actual = emalloc((size_t) hidden_width * sizeof(float)); + stack_d = emalloc((size_t) stack_width * sizeof(double)); + expected = emalloc((size_t) hidden_width * sizeof(double)); + result = cu_memcpy_dtoh(stack, stack_output, (size_t) stack_width * sizeof(float)); + if (result == 0) { + result = cu_memcpy_dtoh(hidden, hidden_output, (size_t) hidden_width * sizeof(float)); + } + if (result == 0) { + result = cu_memcpy_dtoh(actual, residual_output, (size_t) hidden_width * sizeof(float)); + } + if (result != 0) { + efree(expected); + efree(stack_d); + efree(actual); + efree(hidden); + efree(stack); + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_attention_projection_residual_numeric_compare"); + add_assoc_string(&compare, "status", "cuMemcpyDtoH_vector_failed"); + add_assoc_long(&compare, "result", result); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(residual_probe, "attention_projection_residual_numeric_compare_failed", true); + add_assoc_zval(residual_probe, "attention_projection_residual_numeric_compare", &compare); + return FAILURE; + } + + for (zend_ulong i = 0; i < stack_width; i++) { + stack_d[i] = (double) stack[i]; + } + for (zend_ulong row = 0; row < hidden_width; row++) { + zend_ulong row_base = row * stack_width; + double projected = 0.0; + double abs_value; + + if (king_inference_tensor_row_dot(&weight_view, row_base, stack_width, stack_d, &projected) != SUCCESS) { + efree(expected); + efree(stack_d); + efree(actual); + efree(hidden); + efree(stack); + return FAILURE; + } + expected[row] = projected + (double) hidden[row]; + abs_value = fabs(expected[row]); + if (abs_value > max_expected_abs) { + max_expected_abs = abs_value; + } + } + + tolerance = fmax(0.002, max_expected_abs * 0.00005); + sample_limit = king_inference_cuda_device_vector_numeric_compare_max_values(&model->config); + sample_limit = sample_limit < hidden_width ? sample_limit : hidden_width; + array_init(&samples); + for (zend_ulong i = 0; i < hidden_width; i++) { + double diff = fabs((double) actual[i] - expected[i]); + + if (diff <= tolerance) { + matched_values++; + } else { + matched = false; + } + if (diff > max_abs_diff) { + max_abs_diff = diff; + } + if (i < sample_limit) { + zval sample; + + array_init(&sample); + add_assoc_long(&sample, "index", (zend_long) i); + add_assoc_double(&sample, "actual", (double) actual[i]); + add_assoc_double(&sample, "expected", expected[i]); + add_assoc_double(&sample, "abs_diff", diff); + add_next_index_zval(&samples, &sample); + } + } + + efree(expected); + efree(stack_d); + efree(actual); + efree(hidden); + efree(stack); + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_attention_projection_residual_numeric_compare"); + add_assoc_string(&compare, "reference", "cpu_attention_output_projection_row_dot_plus_hidden"); + add_assoc_string(&compare, "status", matched ? "matched" : "mismatch"); + add_assoc_bool(&compare, "matched", matched); + add_assoc_str(&compare, "tensor", zend_string_copy(weight_name)); + add_assoc_long(&compare, "compared_values", (zend_long) hidden_width); + add_assoc_long(&compare, "matched_values", (zend_long) matched_values); + add_assoc_long(&compare, "sampled_values", (zend_long) sample_limit); + add_assoc_long(&compare, "stack_width", (zend_long) stack_width); + add_assoc_long(&compare, "hidden_width", (zend_long) hidden_width); + add_assoc_long(&compare, "residual_width", (zend_long) residual_width); + add_assoc_long(&compare, "rows", (zend_long) rows); + add_assoc_long(&compare, "cols", (zend_long) cols); + add_assoc_double(&compare, "tolerance", tolerance); + add_assoc_double(&compare, "max_abs_diff", max_abs_diff); + add_assoc_double(&compare, "max_expected_abs", max_expected_abs); + add_assoc_zval(&compare, "samples", &samples); + add_assoc_bool(residual_probe, "attention_projection_residual_numeric_compare_failed", !matched); + add_assoc_zval(residual_probe, "attention_projection_residual_numeric_compare", &compare); + if (!matched) { + char message[256]; + + snprintf( + message, + sizeof(message), + "attention_projection_residual_mismatch tensor=%s compared=%lu matched=%lu max_abs_diff=%.9g tolerance=%.9g", + ZSTR_VAL(weight_name), + (unsigned long) hidden_width, + (unsigned long) matched_values, + max_abs_diff, + tolerance + ); + king_inference_cuda_decoder_graph_executor_set_error(model, -1, message); + } + return matched ? SUCCESS : FAILURE; +} diff --git a/extension/src/inference/cuda/decoder_graph/device/compare/attention/cuda_decoder_graph_executor_attention_score_compare.inc b/extension/src/inference/cuda/decoder_graph/device/compare/attention/cuda_decoder_graph_executor_attention_score_compare.inc new file mode 100644 index 000000000..b3561d94b --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/device/compare/attention/cuda_decoder_graph_executor_attention_score_compare.inc @@ -0,0 +1,543 @@ +/* + * Internal CUDA attention numeric compares for block-0 BOS. + */ + +static const char *king_inference_cuda_decoder_graph_executor_attention_compare_error_reason( + zval *attention_probe, + const char *compare_key, + const char *fallback +) { + zval *compare = attention_probe != NULL && Z_TYPE_P(attention_probe) == IS_ARRAY + ? king_inference_array_find(attention_probe, compare_key) + : NULL; + zval *error_category = compare != NULL && Z_TYPE_P(compare) == IS_ARRAY + ? king_inference_array_find(compare, "error_category") + : NULL; + zval *status = compare != NULL && Z_TYPE_P(compare) == IS_ARRAY + ? king_inference_array_find(compare, "status") + : NULL; + + if (error_category != NULL + && Z_TYPE_P(error_category) == IS_STRING + && zend_string_equals_literal(Z_STR_P(error_category), "numeric_mismatch")) { + return "king.numeric_mismatch.detected"; + } + if (status != NULL + && Z_TYPE_P(status) == IS_STRING + && zend_string_equals_literal(Z_STR_P(status), "numeric_mismatch")) { + return "king.numeric_mismatch.detected"; + } + + return fallback; +} + +static zend_result king_inference_cuda_decoder_graph_executor_compare_block0_bos_attention_scores( + king_inference_model_object *model, + king_inference_cuda_device_ptr query_output, + king_inference_cuda_device_ptr key_cache, + king_inference_cuda_device_ptr scores_output, + zend_ulong layer, + zend_ulong head, + zend_ulong key_length, + zend_ulong key_stride, + zend_ulong slot_start, + zend_ulong slot_count, + double scale, + zval *attention_probe +) { + king_inference_cuda_memcpy_dtoh_fn cu_memcpy_dtoh; + size_t vector_bytes; + king_inference_cuda_device_ptr key_slot; + float *query; + float *key; + float expected; + float partials[256]; + double tolerance; + zval compare; + zval *matched; + int result; + + if (layer != 0 + || head != 0 + || slot_start != 0 + || slot_count != 1 + || !king_inference_cuda_device_vector_numeric_compare_enabled(&model->config)) { + return SUCCESS; + } + if (query_output == 0 + || key_cache == 0 + || scores_output == 0 + || key_length == 0 + || key_stride < key_length + || key_length > UINT_MAX + || !isfinite(scale)) { + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_attention_score_numeric_compare"); + add_assoc_string(&compare, "status", "attention_score_compare_argument_invalid"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(attention_probe, "attention_score_numeric_compare_failed", true); + add_assoc_zval(attention_probe, "attention_score_numeric_compare", &compare); + return FAILURE; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_attention_score_numeric_compare"); + add_assoc_string(&compare, "status", "cuda_context_current_failed"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(attention_probe, "attention_score_numeric_compare_failed", true); + add_assoc_zval(attention_probe, "attention_score_numeric_compare", &compare); + return FAILURE; + } + + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH_v2"); + if (cu_memcpy_dtoh == NULL) { + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH"); + } + if (cu_memcpy_dtoh == NULL) { + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_attention_score_numeric_compare"); + add_assoc_string(&compare, "status", "cuMemcpyDtoH_unavailable"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(attention_probe, "attention_score_numeric_compare_failed", true); + add_assoc_zval(attention_probe, "attention_score_numeric_compare", &compare); + return FAILURE; + } + + vector_bytes = (size_t) key_length * sizeof(float); + query = emalloc(vector_bytes); + key = emalloc(vector_bytes); + key_slot = key_cache + ((king_inference_cuda_device_ptr) slot_start + * (king_inference_cuda_device_ptr) key_stride + * (king_inference_cuda_device_ptr) sizeof(float)); + result = cu_memcpy_dtoh(query, query_output, vector_bytes); + if (result == 0) { + result = cu_memcpy_dtoh(key, key_slot, vector_bytes); + } + if (result != 0) { + efree(key); + efree(query); + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_attention_score_numeric_compare"); + add_assoc_string(&compare, "status", "cuMemcpyDtoH_vector_failed"); + add_assoc_long(&compare, "result", result); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(attention_probe, "attention_score_numeric_compare_failed", true); + add_assoc_zval(attention_probe, "attention_score_numeric_compare", &compare); + return FAILURE; + } + + for (zend_ulong lane = 0; lane < 256; lane++) { + float sum = 0.0f; + + for (zend_ulong i = lane; i < key_length; i += 256) { + sum += query[i] * key[i]; + } + partials[lane] = sum; + } + for (zend_ulong stride = 128; stride > 0; stride >>= 1) { + for (zend_ulong lane = 0; lane < stride; lane++) { + partials[lane] += partials[lane + stride]; + } + } + expected = partials[0] * (float) scale; + tolerance = fmax(0.00005, fabs((double) expected) * 0.00005); + + efree(key); + efree(query); + if (king_inference_cuda_device_vector_numeric_compare( + model, + "block0_bos_attention_score_head0_slot0", + "attention_score", + (zend_long) layer, + "qk_score", + 0, + scores_output, + 1, + &expected, + 1, + tolerance, + &compare + ) != SUCCESS) { + add_assoc_string(&compare, "type", "gpu_decoder_attention_score_numeric_compare"); + add_assoc_string(&compare, "reference", "cpu_float_attention_dot_kernel_order"); + add_assoc_long(&compare, "layer", (zend_long) layer); + add_assoc_long(&compare, "head", (zend_long) head); + add_assoc_long(&compare, "slot_start", (zend_long) slot_start); + add_assoc_long(&compare, "slot_count", (zend_long) slot_count); + add_assoc_long(&compare, "key_length", (zend_long) key_length); + add_assoc_long(&compare, "key_stride", (zend_long) key_stride); + add_assoc_double(&compare, "scale", scale); + add_assoc_double(&compare, "cpu_reference", (double) expected); + add_assoc_bool(attention_probe, "attention_score_numeric_compare_failed", true); + add_assoc_zval(attention_probe, "attention_score_numeric_compare", &compare); + return FAILURE; + } + + add_assoc_string(&compare, "type", "gpu_decoder_attention_score_numeric_compare"); + add_assoc_string(&compare, "reference", "cpu_float_attention_dot_kernel_order"); + add_assoc_long(&compare, "layer", (zend_long) layer); + add_assoc_long(&compare, "head", (zend_long) head); + add_assoc_long(&compare, "slot_start", (zend_long) slot_start); + add_assoc_long(&compare, "slot_count", (zend_long) slot_count); + add_assoc_long(&compare, "key_length", (zend_long) key_length); + add_assoc_long(&compare, "key_stride", (zend_long) key_stride); + add_assoc_double(&compare, "scale", scale); + add_assoc_double(&compare, "cpu_reference", (double) expected); + matched = king_inference_array_find(&compare, "matched"); + add_assoc_bool( + attention_probe, + "attention_score_numeric_compare_failed", + matched == NULL || !zend_is_true(matched) + ); + add_assoc_zval(attention_probe, "attention_score_numeric_compare", &compare); + return matched != NULL && zend_is_true(matched) ? SUCCESS : FAILURE; +} + +static void king_inference_cuda_decoder_graph_executor_copy_block0_attention_score_compare( + zval *result, + zval *kv_head_prepare_probe +) { + zval *heads = king_inference_array_find(kv_head_prepare_probe, "query_heads"); + zval *head0 = heads != NULL && Z_TYPE_P(heads) == IS_ARRAY + ? zend_hash_index_find(Z_ARRVAL_P(heads), 0) + : NULL; + zval *kv_probe = head0 != NULL && Z_TYPE_P(head0) == IS_ARRAY + ? king_inference_array_find(head0, "kv_head_prepare_execution") + : NULL; + zval *attention_probe = kv_probe != NULL && Z_TYPE_P(kv_probe) == IS_ARRAY + ? king_inference_array_find(kv_probe, "kv_attention_execution") + : NULL; + zval *compare = attention_probe != NULL && Z_TYPE_P(attention_probe) == IS_ARRAY + ? king_inference_array_find(attention_probe, "attention_score_numeric_compare") + : NULL; + zval copy; + + if (compare == NULL || Z_TYPE_P(compare) != IS_ARRAY) { + return; + } + + ZVAL_COPY(©, compare); + add_assoc_zval(result, "block0_attention_score_numeric_compare", ©); +} + +static zend_result king_inference_cuda_decoder_graph_executor_compare_block0_bos_attention_softmax( + king_inference_model_object *model, + king_inference_cuda_device_ptr probabilities_output, + zend_ulong layer, + zend_ulong head, + zend_ulong slot_start, + zend_ulong slot_count, + zval *attention_probe +) { + king_inference_cuda_memcpy_dtoh_fn cu_memcpy_dtoh; + float actual_probability = 0.0f; + float expected_probability = 1.0f; + zval compare; + zval *matched; + int result; + + if (layer != 0 + || head != 0 + || slot_start != 0 + || slot_count != 1 + || !king_inference_cuda_device_vector_numeric_compare_enabled(&model->config)) { + return SUCCESS; + } + if (probabilities_output == 0) { + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_attention_softmax_numeric_compare"); + add_assoc_string(&compare, "status", "attention_softmax_compare_argument_invalid"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(attention_probe, "attention_softmax_numeric_compare_failed", true); + add_assoc_zval(attention_probe, "attention_softmax_numeric_compare", &compare); + return FAILURE; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_attention_softmax_numeric_compare"); + add_assoc_string(&compare, "status", "cuda_context_current_failed"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(attention_probe, "attention_softmax_numeric_compare_failed", true); + add_assoc_zval(attention_probe, "attention_softmax_numeric_compare", &compare); + return FAILURE; + } + + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH_v2"); + if (cu_memcpy_dtoh == NULL) { + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH"); + } + if (cu_memcpy_dtoh == NULL) { + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_attention_softmax_numeric_compare"); + add_assoc_string(&compare, "status", "cuMemcpyDtoH_unavailable"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(attention_probe, "attention_softmax_numeric_compare_failed", true); + add_assoc_zval(attention_probe, "attention_softmax_numeric_compare", &compare); + return FAILURE; + } + + result = cu_memcpy_dtoh(&actual_probability, probabilities_output, sizeof(float)); + if (result != 0) { + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_attention_softmax_numeric_compare"); + add_assoc_string(&compare, "status", "cuMemcpyDtoH_probability_failed"); + add_assoc_long(&compare, "result", result); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(attention_probe, "attention_softmax_numeric_compare_failed", true); + add_assoc_zval(attention_probe, "attention_softmax_numeric_compare", &compare); + return FAILURE; + } + + if (king_inference_cuda_device_vector_numeric_compare( + model, + "block0_bos_attention_softmax_head0_slot0", + "attention_softmax", + (zend_long) layer, + "attention_probability", + 0, + probabilities_output, + 1, + &expected_probability, + 1, + 0.000001, + &compare + ) != SUCCESS) { + add_assoc_string(&compare, "type", "gpu_decoder_attention_softmax_numeric_compare"); + add_assoc_string(&compare, "reference", "cpu_softmax_single_slot"); + add_assoc_long(&compare, "layer", (zend_long) layer); + add_assoc_long(&compare, "head", (zend_long) head); + add_assoc_long(&compare, "slot_start", (zend_long) slot_start); + add_assoc_long(&compare, "slot_count", (zend_long) slot_count); + add_assoc_double(&compare, "probability_sum", (double) actual_probability); + add_assoc_double(&compare, "cpu_reference_sum", 1.0); + add_assoc_bool(attention_probe, "attention_softmax_numeric_compare_failed", true); + add_assoc_zval(attention_probe, "attention_softmax_numeric_compare", &compare); + return FAILURE; + } + + add_assoc_string(&compare, "type", "gpu_decoder_attention_softmax_numeric_compare"); + add_assoc_string(&compare, "reference", "cpu_softmax_single_slot"); + add_assoc_long(&compare, "layer", (zend_long) layer); + add_assoc_long(&compare, "head", (zend_long) head); + add_assoc_long(&compare, "slot_start", (zend_long) slot_start); + add_assoc_long(&compare, "slot_count", (zend_long) slot_count); + add_assoc_double(&compare, "probability_sum", (double) actual_probability); + add_assoc_double(&compare, "cpu_reference_sum", 1.0); + matched = king_inference_array_find(&compare, "matched"); + add_assoc_bool( + attention_probe, + "attention_softmax_numeric_compare_failed", + matched == NULL || !zend_is_true(matched) + ); + add_assoc_zval(attention_probe, "attention_softmax_numeric_compare", &compare); + return matched != NULL && zend_is_true(matched) ? SUCCESS : FAILURE; +} + +static void king_inference_cuda_decoder_graph_executor_copy_block0_attention_softmax_compare( + zval *result, + zval *kv_head_prepare_probe +) { + zval *heads = king_inference_array_find(kv_head_prepare_probe, "query_heads"); + zval *head0 = heads != NULL && Z_TYPE_P(heads) == IS_ARRAY + ? zend_hash_index_find(Z_ARRVAL_P(heads), 0) + : NULL; + zval *kv_probe = head0 != NULL && Z_TYPE_P(head0) == IS_ARRAY + ? king_inference_array_find(head0, "kv_head_prepare_execution") + : NULL; + zval *attention_probe = kv_probe != NULL && Z_TYPE_P(kv_probe) == IS_ARRAY + ? king_inference_array_find(kv_probe, "kv_attention_execution") + : NULL; + zval *compare = attention_probe != NULL && Z_TYPE_P(attention_probe) == IS_ARRAY + ? king_inference_array_find(attention_probe, "attention_softmax_numeric_compare") + : NULL; + zval copy; + + if (compare == NULL || Z_TYPE_P(compare) != IS_ARRAY) { + return; + } + + ZVAL_COPY(©, compare); + add_assoc_zval(result, "block0_attention_softmax_numeric_compare", ©); +} + +static zend_result king_inference_cuda_decoder_graph_executor_compare_block0_bos_attention_values( + king_inference_model_object *model, + king_inference_cuda_device_ptr probabilities_output, + king_inference_cuda_device_ptr value_cache, + king_inference_cuda_device_ptr context_output, + zend_ulong layer, + zend_ulong head, + zend_ulong value_length, + zend_ulong value_stride, + zend_ulong slot_start, + zend_ulong slot_count, + zval *attention_probe +) { + king_inference_cuda_memcpy_dtoh_fn cu_memcpy_dtoh; + king_inference_cuda_device_ptr value_slot; + float probability = 0.0f; + float *value; + float *expected; + size_t value_bytes; + double max_expected_abs = 0.0; + zval compare; + zval *matched; + int result; + + if (layer != 0 + || head != 0 + || slot_start != 0 + || slot_count != 1 + || !king_inference_cuda_device_vector_numeric_compare_enabled(&model->config)) { + return SUCCESS; + } + if (probabilities_output == 0 + || value_cache == 0 + || context_output == 0 + || value_length == 0 + || value_stride < value_length + || value_length > UINT_MAX) { + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_attention_value_numeric_compare"); + add_assoc_string(&compare, "status", "attention_value_compare_argument_invalid"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(attention_probe, "attention_value_numeric_compare_failed", true); + add_assoc_zval(attention_probe, "attention_value_numeric_compare", &compare); + return FAILURE; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_attention_value_numeric_compare"); + add_assoc_string(&compare, "status", "cuda_context_current_failed"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(attention_probe, "attention_value_numeric_compare_failed", true); + add_assoc_zval(attention_probe, "attention_value_numeric_compare", &compare); + return FAILURE; + } + + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH_v2"); + if (cu_memcpy_dtoh == NULL) { + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH"); + } + if (cu_memcpy_dtoh == NULL) { + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_attention_value_numeric_compare"); + add_assoc_string(&compare, "status", "cuMemcpyDtoH_unavailable"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(attention_probe, "attention_value_numeric_compare_failed", true); + add_assoc_zval(attention_probe, "attention_value_numeric_compare", &compare); + return FAILURE; + } + + value_bytes = (size_t) value_length * sizeof(float); + value = emalloc(value_bytes); + expected = emalloc(value_bytes); + value_slot = value_cache + ((king_inference_cuda_device_ptr) slot_start + * (king_inference_cuda_device_ptr) value_stride + * (king_inference_cuda_device_ptr) sizeof(float)); + result = cu_memcpy_dtoh(&probability, probabilities_output, sizeof(float)); + if (result == 0) { + result = cu_memcpy_dtoh(value, value_slot, value_bytes); + } + if (result != 0) { + efree(expected); + efree(value); + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_attention_value_numeric_compare"); + add_assoc_string(&compare, "status", "cuMemcpyDtoH_value_failed"); + add_assoc_long(&compare, "result", result); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(attention_probe, "attention_value_numeric_compare_failed", true); + add_assoc_zval(attention_probe, "attention_value_numeric_compare", &compare); + return FAILURE; + } + + for (zend_ulong i = 0; i < value_length; i++) { + double abs_value; + + expected[i] = probability * value[i]; + abs_value = fabs((double) expected[i]); + if (abs_value > max_expected_abs) { + max_expected_abs = abs_value; + } + } + + if (king_inference_cuda_device_vector_numeric_compare( + model, + "block0_bos_attention_value_context_head0", + "attention_value", + (zend_long) layer, + "attention_value_context", + 0, + context_output, + value_length, + expected, + value_length, + fmax(0.00002, max_expected_abs * 0.00002), + &compare + ) != SUCCESS) { + efree(expected); + efree(value); + add_assoc_string(&compare, "type", "gpu_decoder_attention_value_numeric_compare"); + add_assoc_string(&compare, "reference", "cpu_probability_weighted_value_cache"); + add_assoc_long(&compare, "layer", (zend_long) layer); + add_assoc_long(&compare, "head", (zend_long) head); + add_assoc_long(&compare, "slot_start", (zend_long) slot_start); + add_assoc_long(&compare, "slot_count", (zend_long) slot_count); + add_assoc_long(&compare, "value_length", (zend_long) value_length); + add_assoc_long(&compare, "value_stride", (zend_long) value_stride); + add_assoc_double(&compare, "probability", (double) probability); + add_assoc_double(&compare, "max_expected_abs", max_expected_abs); + add_assoc_bool(attention_probe, "attention_value_numeric_compare_failed", true); + add_assoc_zval(attention_probe, "attention_value_numeric_compare", &compare); + return FAILURE; + } + + efree(expected); + efree(value); + add_assoc_string(&compare, "type", "gpu_decoder_attention_value_numeric_compare"); + add_assoc_string(&compare, "reference", "cpu_probability_weighted_value_cache"); + add_assoc_long(&compare, "layer", (zend_long) layer); + add_assoc_long(&compare, "head", (zend_long) head); + add_assoc_long(&compare, "slot_start", (zend_long) slot_start); + add_assoc_long(&compare, "slot_count", (zend_long) slot_count); + add_assoc_long(&compare, "value_length", (zend_long) value_length); + add_assoc_long(&compare, "value_stride", (zend_long) value_stride); + add_assoc_double(&compare, "probability", (double) probability); + add_assoc_double(&compare, "max_expected_abs", max_expected_abs); + matched = king_inference_array_find(&compare, "matched"); + add_assoc_bool( + attention_probe, + "attention_value_numeric_compare_failed", + matched == NULL || !zend_is_true(matched) + ); + add_assoc_zval(attention_probe, "attention_value_numeric_compare", &compare); + return matched != NULL && zend_is_true(matched) ? SUCCESS : FAILURE; +} + +static void king_inference_cuda_decoder_graph_executor_copy_block0_attention_value_compare( + zval *result, + zval *kv_head_prepare_probe +) { + zval *heads = king_inference_array_find(kv_head_prepare_probe, "query_heads"); + zval *head0 = heads != NULL && Z_TYPE_P(heads) == IS_ARRAY + ? zend_hash_index_find(Z_ARRVAL_P(heads), 0) + : NULL; + zval *kv_probe = head0 != NULL && Z_TYPE_P(head0) == IS_ARRAY + ? king_inference_array_find(head0, "kv_head_prepare_execution") + : NULL; + zval *attention_probe = kv_probe != NULL && Z_TYPE_P(kv_probe) == IS_ARRAY + ? king_inference_array_find(kv_probe, "kv_attention_execution") + : NULL; + zval *compare = attention_probe != NULL && Z_TYPE_P(attention_probe) == IS_ARRAY + ? king_inference_array_find(attention_probe, "attention_value_numeric_compare") + : NULL; + zval copy; + + if (compare == NULL || Z_TYPE_P(compare) != IS_ARRAY) { + return; + } + + ZVAL_COPY(©, compare); + add_assoc_zval(result, "block0_attention_value_numeric_compare", ©); +} diff --git a/extension/src/inference/cuda/decoder_graph/device/compare/embedding/cuda_decoder_graph_executor_embedding_compare.inc b/extension/src/inference/cuda/decoder_graph/device/compare/embedding/cuda_decoder_graph_executor_embedding_compare.inc new file mode 100644 index 000000000..618bf3add --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/device/compare/embedding/cuda_decoder_graph_executor_embedding_compare.inc @@ -0,0 +1,198 @@ +static zend_result king_inference_cuda_decoder_graph_executor_compare_embedding_row( + king_inference_model_object *model, + zend_ulong token_id, + king_inference_cuda_device_ptr embedding_output, + zend_ulong width, + double output_scale, + zval *embedding_probe +) { + zend_string *tensor = NULL; + zend_string *tensor_name = NULL; + const char *status = NULL; + king_inference_tensor_native_view view; + zend_ulong cols = 0; + zend_ulong rows = 0; + zend_ulong base; + zend_ulong chunk_limit; + zend_ulong compared_values = 0; + zend_ulong readback_bytes = 0; + zend_ulong chunk_count = 0; + double max_abs_diff = 0.0; + bool matched = true; + zval compare; + zval chunks; + + if (!king_inference_cuda_device_vector_numeric_compare_enabled(&model->config)) { + return SUCCESS; + } + + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_embedding_row_numeric_compare"); + add_assoc_long(&compare, "token_id", (zend_long) token_id); + add_assoc_long(&compare, "width", (zend_long) width); + add_assoc_double(&compare, "output_scale", output_scale); + + if (king_inference_resolve_token_embedding_tensor_name(model, NULL, &tensor, NULL, &status) != SUCCESS + || tensor == NULL) { + add_assoc_string(&compare, "status", status != NULL ? status : "embedding_tensor_not_resolved"); + add_assoc_bool(&compare, "matched", false); + add_assoc_zval(embedding_probe, "numeric_compare", &compare); + return FAILURE; + } + + tensor_name = zend_string_copy(tensor); + add_assoc_str(&compare, "tensor", zend_string_copy(tensor_name)); + if (king_inference_tensor_native_view_resolve(model, tensor, &view) != SUCCESS) { + zend_string_release(tensor); + zend_string_release(tensor_name); + zval_ptr_dtor(&compare); + return FAILURE; + } + zend_string_release(tensor); + + if (view.rank != 2 + || king_inference_tensor_dimension_at(view.dimensions, 0, &cols) != SUCCESS + || king_inference_tensor_dimension_at(view.dimensions, 1, &rows) != SUCCESS + || cols != width + || token_id >= rows + || token_id > ((zend_ulong) -1) / cols) { + add_assoc_string(&compare, "status", "embedding_tensor_shape_invalid"); + add_assoc_bool(&compare, "matched", false); + add_assoc_zval(embedding_probe, "numeric_compare", &compare); + zend_string_release(tensor_name); + return FAILURE; + } + + base = token_id * cols; + chunk_limit = king_inference_cuda_device_vector_numeric_compare_max_values(&model->config); + if (chunk_limit == 0 || base > view.elements || width > view.elements - base) { + add_assoc_string(&compare, "status", "embedding_tensor_compare_range_invalid"); + add_assoc_bool(&compare, "matched", false); + add_assoc_zval(embedding_probe, "numeric_compare", &compare); + zend_string_release(tensor_name); + return FAILURE; + } + + zval_ptr_dtor(&compare); + array_init(&compare); + add_assoc_string(&compare, "label", "bos_token_embedding_row"); + add_assoc_bool(&compare, "enabled", true); + add_assoc_bool(&compare, "public_api", false); + add_assoc_string(&compare, "scope", "internal_cuda_device_vector_readback_compare"); + add_assoc_string(&compare, "type", "gpu_decoder_embedding_row_numeric_compare"); + add_assoc_string(&compare, "mode", "full_row_chunked"); + add_assoc_long(&compare, "token_id", (zend_long) token_id); + add_assoc_long(&compare, "width", (zend_long) width); + add_assoc_long(&compare, "chunk_limit", (zend_long) chunk_limit); + add_assoc_double(&compare, "output_scale", output_scale); + add_assoc_str(&compare, "tensor", zend_string_copy(tensor_name)); + array_init(&chunks); + + for (zend_ulong offset = 0; offset < width; offset += chunk_limit) { + zend_ulong chunk_values = width - offset; + float *expected; + zval chunk_compare; + zval chunk_summary; + zval *chunk_matched; + zval *chunk_max_abs_diff; + + if (chunk_values > chunk_limit) { + chunk_values = chunk_limit; + } + expected = emalloc((size_t) chunk_values * sizeof(float)); + for (zend_ulong i = 0; i < chunk_values; i++) { + double value; + + if (king_inference_tensor_value_at(&view, base + offset + i, &value) != SUCCESS) { + efree(expected); + zval_ptr_dtor(&chunks); + zval_ptr_dtor(&compare); + zend_string_release(tensor_name); + return FAILURE; + } + expected[i] = (float) (value * output_scale); + } + + if (king_inference_cuda_device_vector_numeric_compare( + model, + "bos_token_embedding_row_chunk", + "embedding", + -1, + ZSTR_VAL(tensor_name), + offset, + embedding_output + ((king_inference_cuda_device_ptr) offset * (king_inference_cuda_device_ptr) sizeof(float)), + chunk_values, + expected, + chunk_values, + 0.00001, + &chunk_compare + ) != SUCCESS) { + efree(expected); + add_assoc_long(&chunk_compare, "offset", (zend_long) offset); + add_assoc_long(&chunk_compare, "token_id", (zend_long) token_id); + add_assoc_long(&chunk_compare, "width", (zend_long) width); + add_assoc_zval(embedding_probe, "numeric_compare", &chunk_compare); + zval_ptr_dtor(&chunks); + zval_ptr_dtor(&compare); + zend_string_release(tensor_name); + return FAILURE; + } + efree(expected); + + chunk_matched = king_inference_array_find(&chunk_compare, "matched"); + chunk_max_abs_diff = king_inference_array_find(&chunk_compare, "max_abs_diff"); + if (chunk_matched == NULL || !zend_is_true(chunk_matched)) { + matched = false; + } + if (chunk_max_abs_diff != NULL + && (Z_TYPE_P(chunk_max_abs_diff) == IS_DOUBLE || Z_TYPE_P(chunk_max_abs_diff) == IS_LONG)) { + double value = Z_TYPE_P(chunk_max_abs_diff) == IS_DOUBLE + ? Z_DVAL_P(chunk_max_abs_diff) + : (double) Z_LVAL_P(chunk_max_abs_diff); + if (value > max_abs_diff) { + max_abs_diff = value; + } + } + + array_init(&chunk_summary); + add_assoc_long(&chunk_summary, "offset", (zend_long) offset); + add_assoc_long(&chunk_summary, "compared_values", (zend_long) chunk_values); + add_assoc_bool(&chunk_summary, "matched", chunk_matched != NULL && zend_is_true(chunk_matched)); + add_assoc_double(&chunk_summary, "max_abs_diff", chunk_max_abs_diff != NULL && Z_TYPE_P(chunk_max_abs_diff) == IS_DOUBLE ? Z_DVAL_P(chunk_max_abs_diff) : 0.0); + add_next_index_zval(&chunks, &chunk_summary); + zval_ptr_dtor(&chunk_compare); + compared_values += chunk_values; + readback_bytes += chunk_values * sizeof(float); + chunk_count++; + } + + add_assoc_string(&compare, "status", matched ? "matched" : "mismatch"); + add_assoc_bool(&compare, "matched", matched); + add_assoc_long(&compare, "compared_values", (zend_long) compared_values); + add_assoc_long(&compare, "readback_bytes", (zend_long) readback_bytes); + add_assoc_double(&compare, "tolerance", 0.00001); + add_assoc_double(&compare, "max_abs_diff", max_abs_diff); + add_assoc_long(&compare, "chunk_count", (zend_long) chunk_count); + add_assoc_zval(&compare, "chunks", &chunks); + add_assoc_long(&compare, "token_id", (zend_long) token_id); + add_assoc_long(&compare, "width", (zend_long) width); + add_assoc_double(&compare, "output_scale", output_scale); + zend_string_release(tensor_name); + add_assoc_zval(embedding_probe, "numeric_compare", &compare); + return matched ? SUCCESS : FAILURE; +} + +static void king_inference_cuda_decoder_graph_executor_copy_embedding_compare( + zval *result, + zval *embedding_probe +) { + zval *compare = king_inference_array_find(embedding_probe, "numeric_compare"); + zval copy; + + if (compare == NULL) { + return; + } + + ZVAL_COPY(©, compare); + add_assoc_zval(result, "embedding_numeric_compare", ©); +} diff --git a/extension/src/inference/cuda/decoder_graph/device/compare/ffn/cuda_decoder_graph_executor_ffn_down_output_compare.inc b/extension/src/inference/cuda/decoder_graph/device/compare/ffn/cuda_decoder_graph_executor_ffn_down_output_compare.inc new file mode 100644 index 000000000..145510285 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/device/compare/ffn/cuda_decoder_graph_executor_ffn_down_output_compare.inc @@ -0,0 +1,571 @@ +/* + * Internal CUDA FFN down-projection and output-residual compares for selected blocks. + */ + +static void king_inference_cuda_decoder_graph_executor_ffn_compare_add_status( + zval *compare, + bool matched +) { + add_assoc_string(compare, "status", matched ? "matched" : "mismatch"); + add_assoc_string(compare, "classification", matched ? "matched" : "numeric_mismatch"); + add_assoc_string(compare, "error_category", matched ? "none" : "numeric_mismatch"); + add_assoc_string(compare, "error_code", matched ? "none" : "king.numeric_mismatch.detected"); + add_assoc_string(compare, "failure_status", matched ? "none" : "numeric_mismatch"); +} + +static void king_inference_cuda_decoder_graph_executor_ffn_compare_add_sample( + zval *samples, + zend_ulong index, + const char *op, + zend_ulong layer, + double actual, + double expected, + double abs_diff, + double relative_diff, + double max_abs_tolerance, + double max_relative_tolerance, + const char *classification +) { + zval sample; + + array_init(&sample); + add_assoc_long(&sample, "index", (zend_long) index); + add_assoc_string(&sample, "op", op); + add_assoc_long(&sample, "layer", (zend_long) layer); + add_assoc_double(&sample, "actual", actual); + add_assoc_double(&sample, "expected", expected); + add_assoc_double(&sample, "abs_diff", abs_diff); + add_assoc_double(&sample, "relative_diff", relative_diff); + add_assoc_double(&sample, "max_abs_tolerance", max_abs_tolerance); + add_assoc_double(&sample, "max_relative_tolerance", max_relative_tolerance); + add_assoc_string(&sample, "classification", classification); + add_next_index_zval(samples, &sample); +} + +static bool king_inference_cuda_decoder_graph_executor_parse_layer_id( + zend_string *value, + const char *suffix, + zend_ulong *layer +) { + const char *cursor; + const char *end; + char *parse_end; + unsigned long parsed; + size_t suffix_len = strlen(suffix); + + if (value == NULL || ZSTR_LEN(value) <= 1 + suffix_len || ZSTR_VAL(value)[0] != 'l') { + return false; + } + if (memcmp(ZSTR_VAL(value) + ZSTR_LEN(value) - suffix_len, suffix, suffix_len) != 0) { + return false; + } + cursor = ZSTR_VAL(value) + 1; + end = ZSTR_VAL(value) + ZSTR_LEN(value) - suffix_len; + if (cursor >= end) { + return false; + } + parsed = strtoul(cursor, &parse_end, 10); + if (parse_end != end) { + return false; + } + *layer = (zend_ulong) parsed; + return true; +} + +static bool king_inference_cuda_decoder_graph_executor_compare_selected_ffn_layer( + king_inference_model_object *model, + zend_ulong layer +) { + zend_ulong block_count = model->gguf.architecture_params[KING_INFERENCE_ARCH_BLOCK_COUNT]; + + return layer == 0 || (block_count > 0 && layer + 1 == block_count); +} + +static king_inference_cuda_memcpy_dtoh_fn king_inference_cuda_decoder_graph_executor_compare_dtoh( + king_inference_model_object *model +) { + king_inference_cuda_memcpy_dtoh_fn cu_memcpy_dtoh; + + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH_v2"); + if (cu_memcpy_dtoh == NULL) { + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH"); + } + return cu_memcpy_dtoh; +} + +static void king_inference_cuda_decoder_graph_executor_compare_down_set_error( + king_inference_model_object *model, + const char *prefix, + zend_ulong compared, + zend_ulong matched, + double max_abs_diff, + double tolerance +) { + char message[256]; + + snprintf( + message, + sizeof(message), + "king.numeric_mismatch.detected %s compared=%lu matched=%lu max_abs_diff=%.9g tolerance=%.9g", + prefix, + (unsigned long) compared, + (unsigned long) matched, + max_abs_diff, + tolerance + ); + king_inference_cuda_decoder_graph_executor_set_error(model, -1, message); +} + +static void king_inference_cuda_decoder_graph_executor_copy_ffn_numeric_compare( + zval *result, + zval *probe, + const char *source_key, + const char *target_key +) { + zval *compare = probe != NULL && Z_TYPE_P(probe) == IS_ARRAY + ? king_inference_array_find(probe, source_key) + : NULL; + zval copy; + + if (compare == NULL || Z_TYPE_P(compare) != IS_ARRAY) { + return; + } + + ZVAL_COPY(©, compare); + add_assoc_zval(result, target_key, ©); +} + +static void king_inference_cuda_decoder_graph_executor_copy_ffn_numeric_compares( + zval *result, + zval *ffn_gate_up_probe, + zval *ffn_swiglu_probe, + zval *ffn_down_probe, + zval *ffn_output_probe +) { + king_inference_cuda_decoder_graph_executor_copy_ffn_numeric_compare( + result, + ffn_gate_up_probe, + "ffn_gate_up_numeric_compare", + "block0_ffn_gate_up_numeric_compare" + ); + king_inference_cuda_decoder_graph_executor_copy_ffn_numeric_compare( + result, + ffn_swiglu_probe, + "ffn_swiglu_numeric_compare", + "block0_ffn_swiglu_numeric_compare" + ); + king_inference_cuda_decoder_graph_executor_copy_ffn_numeric_compare( + result, + ffn_down_probe, + "ffn_down_numeric_compare", + "block0_ffn_down_numeric_compare" + ); + king_inference_cuda_decoder_graph_executor_copy_ffn_numeric_compare( + result, + ffn_output_probe, + "ffn_output_residual_numeric_compare", + "block0_ffn_output_residual_numeric_compare" + ); +} + +static zend_result king_inference_cuda_decoder_graph_executor_compare_block0_ffn_down_projection( + king_inference_model_object *model, + zval *down_op, + king_inference_cuda_device_ptr swiglu_output, + zend_ulong swiglu_width, + king_inference_cuda_device_ptr down_output, + zend_ulong down_width, + zval *ffn_down_probe +) { + zend_string *weight_name = NULL; + zend_string *down_id = NULL; + king_inference_tensor_native_view weight_view; + king_inference_cuda_memcpy_dtoh_fn cu_memcpy_dtoh; + zend_ulong cols = 0; + zend_ulong rows = 0; + zend_ulong layer = 0; + zend_ulong block_count = model->gguf.architecture_params[KING_INFERENCE_ARCH_BLOCK_COUNT]; + float *swiglu = NULL; + float *actual = NULL; + double *input = NULL; + double max_expected_abs = 0.0; + double max_abs_diff = 0.0; + double max_relative_diff = 0.0; + double tolerance; + double relative_tolerance = king_inference_cuda_numeric_tolerance_relative("ffn_down"); + zend_ulong matched_values = 0; + zend_ulong sample_limit; + bool matched = true; + int result; + zval compare; + zval samples; + + if (!king_inference_cuda_device_vector_numeric_compare_enabled(&model->config)) { + return SUCCESS; + } + if (down_op == NULL + || king_inference_graph_required_string(down_op, "id", &down_id) != SUCCESS + || king_inference_graph_required_string(down_op, "weight", &weight_name) != SUCCESS + || !king_inference_cuda_decoder_graph_executor_parse_layer_id(down_id, "_ffn_down", &layer) + || !king_inference_cuda_decoder_graph_executor_compare_selected_ffn_layer(model, layer)) { + return SUCCESS; + } + + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_ffn_down_projection_numeric_compare"); + add_assoc_string(&compare, "reference", "cpu_ffn_down_row_dot_from_swiglu_readback"); + add_assoc_str(&compare, "down", zend_string_copy(down_id)); + add_assoc_str(&compare, "tensor", zend_string_copy(weight_name)); + add_assoc_long(&compare, "layer", (zend_long) layer); + add_assoc_long(&compare, "block_count", (zend_long) block_count); + add_assoc_bool(&compare, "last_block", block_count > 0 && layer + 1 == block_count); + if (swiglu_output == 0 + || down_output == 0 + || swiglu_width == 0 + || down_width == 0 + || swiglu_width > (zend_ulong) (SIZE_MAX / sizeof(float)) + || down_width > (zend_ulong) (SIZE_MAX / sizeof(float))) { + add_assoc_string(&compare, "status", "ffn_down_compare_argument_invalid"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(ffn_down_probe, "ffn_down_numeric_compare_failed", true); + add_assoc_zval(ffn_down_probe, "ffn_down_numeric_compare", &compare); + return FAILURE; + } + if (king_inference_tensor_native_view_resolve(model, weight_name, &weight_view) != SUCCESS) { + zval_ptr_dtor(&compare); + return FAILURE; + } + if (weight_view.rank != 2 + || king_inference_tensor_dimension_at(weight_view.dimensions, 0, &cols) != SUCCESS + || king_inference_tensor_dimension_at(weight_view.dimensions, 1, &rows) != SUCCESS + || cols != swiglu_width + || rows != down_width) { + add_assoc_string(&compare, "status", "ffn_down_tensor_shape_invalid"); + add_assoc_bool(&compare, "matched", false); + add_assoc_long(&compare, "swiglu_width", (zend_long) swiglu_width); + add_assoc_long(&compare, "down_width", (zend_long) down_width); + add_assoc_long(&compare, "rows", (zend_long) rows); + add_assoc_long(&compare, "cols", (zend_long) cols); + add_assoc_bool(ffn_down_probe, "ffn_down_numeric_compare_failed", true); + add_assoc_zval(ffn_down_probe, "ffn_down_numeric_compare", &compare); + return FAILURE; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + add_assoc_string(&compare, "status", "cuda_context_current_failed"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(ffn_down_probe, "ffn_down_numeric_compare_failed", true); + add_assoc_zval(ffn_down_probe, "ffn_down_numeric_compare", &compare); + return FAILURE; + } + cu_memcpy_dtoh = king_inference_cuda_decoder_graph_executor_compare_dtoh(model); + if (cu_memcpy_dtoh == NULL) { + add_assoc_string(&compare, "status", "cuMemcpyDtoH_unavailable"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(ffn_down_probe, "ffn_down_numeric_compare_failed", true); + add_assoc_zval(ffn_down_probe, "ffn_down_numeric_compare", &compare); + return FAILURE; + } + + swiglu = emalloc((size_t) swiglu_width * sizeof(float)); + actual = emalloc((size_t) down_width * sizeof(float)); + input = emalloc((size_t) swiglu_width * sizeof(double)); + result = cu_memcpy_dtoh(swiglu, swiglu_output, (size_t) swiglu_width * sizeof(float)); + if (result == 0) { + result = cu_memcpy_dtoh(actual, down_output, (size_t) down_width * sizeof(float)); + } + if (result != 0) { + efree(input); + efree(actual); + efree(swiglu); + add_assoc_string(&compare, "status", "cuMemcpyDtoH_vector_failed"); + add_assoc_long(&compare, "result", result); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(ffn_down_probe, "ffn_down_numeric_compare_failed", true); + add_assoc_zval(ffn_down_probe, "ffn_down_numeric_compare", &compare); + return FAILURE; + } + for (zend_ulong i = 0; i < swiglu_width; i++) { + input[i] = (double) swiglu[i]; + } + + tolerance = 0.003; + sample_limit = king_inference_cuda_device_vector_numeric_compare_max_values(&model->config); + sample_limit = sample_limit < down_width ? sample_limit : down_width; + array_init(&samples); + for (zend_ulong row = 0; row < down_width; row++) { + double expected = 0.0; + double diff; + double rel_diff; + double abs_expected; + const char *classification; + + if (king_inference_tensor_row_dot(&weight_view, row * cols, cols, input, &expected) != SUCCESS) { + zval_ptr_dtor(&samples); + efree(input); + efree(actual); + efree(swiglu); + zval_ptr_dtor(&compare); + return FAILURE; + } + diff = fabs((double) actual[row] - expected); + rel_diff = king_inference_cuda_numeric_relative_diff((double) actual[row], expected); + classification = king_inference_cuda_numeric_sample_classification( + (double) actual[row], + expected, + diff, + rel_diff, + tolerance, + relative_tolerance + ); + abs_expected = fabs(expected); + if (king_inference_cuda_numeric_sample_matches(classification)) { + matched_values++; + } else { + matched = false; + } + if (diff > max_abs_diff) { + max_abs_diff = diff; + } + if (abs_expected > max_expected_abs) { + max_expected_abs = abs_expected; + } + if (rel_diff > max_relative_diff && isfinite(rel_diff)) { + max_relative_diff = rel_diff; + } + if (row < sample_limit) { + king_inference_cuda_decoder_graph_executor_ffn_compare_add_sample( + &samples, + row, + "ffn_down", + layer, + (double) actual[row], + expected, + diff, + rel_diff, + tolerance, + relative_tolerance, + classification + ); + } + } + + efree(input); + efree(actual); + efree(swiglu); + king_inference_cuda_decoder_graph_executor_ffn_compare_add_status(&compare, matched); + add_assoc_bool(&compare, "matched", matched); + add_assoc_long(&compare, "compared_values", (zend_long) down_width); + add_assoc_long(&compare, "matched_values", (zend_long) matched_values); + add_assoc_long(&compare, "swiglu_width", (zend_long) swiglu_width); + add_assoc_long(&compare, "down_width", (zend_long) down_width); + add_assoc_long(&compare, "sampled_values", (zend_long) sample_limit); + add_assoc_double(&compare, "tolerance", tolerance); + add_assoc_double(&compare, "max_abs_tolerance", tolerance); + add_assoc_double(&compare, "max_relative_tolerance", relative_tolerance); + add_assoc_double(&compare, "max_abs_diff", max_abs_diff); + add_assoc_double(&compare, "max_relative_diff", max_relative_diff); + add_assoc_double(&compare, "max_expected_abs", max_expected_abs); + add_assoc_zval(&compare, "samples", &samples); + add_assoc_bool(ffn_down_probe, "ffn_down_numeric_compare_failed", !matched); + add_assoc_zval(ffn_down_probe, "ffn_down_numeric_compare", &compare); + if (!matched) { + king_inference_cuda_decoder_graph_executor_compare_down_set_error( + model, + "ffn_down_mismatch", + down_width, + matched_values, + max_abs_diff, + tolerance + ); + } + return matched ? SUCCESS : FAILURE; +} + +static zend_result king_inference_cuda_decoder_graph_executor_compare_block0_ffn_output_residual( + king_inference_model_object *model, + zend_string *residual_id, + king_inference_cuda_device_ptr residual_output, + zend_ulong residual_width, + zend_string *down_id, + king_inference_cuda_device_ptr down_output, + zend_ulong down_width, + zend_string *output_id, + king_inference_cuda_device_ptr output, + zend_ulong output_width, + zval *ffn_output_probe +) { + king_inference_cuda_memcpy_dtoh_fn cu_memcpy_dtoh; + zend_ulong layer = 0; + zend_ulong block_count = model->gguf.architecture_params[KING_INFERENCE_ARCH_BLOCK_COUNT]; + float *residual = NULL; + float *down = NULL; + float *actual = NULL; + double max_expected_abs = 0.0; + double max_abs_diff = 0.0; + double max_relative_diff = 0.0; + double tolerance; + double relative_tolerance = king_inference_cuda_numeric_tolerance_relative("ffn_output_residual"); + zend_ulong matched_values = 0; + zend_ulong sample_limit; + bool matched = true; + int result; + zval compare; + zval samples; + + if (!king_inference_cuda_device_vector_numeric_compare_enabled(&model->config)) { + return SUCCESS; + } + if (down_id == NULL + || (!king_inference_cuda_decoder_graph_executor_parse_layer_id(down_id, "_ffn_down", &layer) + && !king_inference_cuda_decoder_graph_executor_parse_layer_id(down_id, "_post_ffw_norm", &layer)) + || !king_inference_cuda_decoder_graph_executor_compare_selected_ffn_layer(model, layer)) { + return SUCCESS; + } + + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_ffn_output_residual_numeric_compare"); + add_assoc_string(&compare, "reference", "cpu_attention_residual_plus_ffn_down_readback"); + add_assoc_long(&compare, "layer", (zend_long) layer); + add_assoc_long(&compare, "block_count", (zend_long) block_count); + add_assoc_bool(&compare, "last_block", block_count > 0 && layer + 1 == block_count); + if (residual_id != NULL) { + add_assoc_str(&compare, "residual", zend_string_copy(residual_id)); + } + add_assoc_str(&compare, "down", zend_string_copy(down_id)); + if (output_id != NULL) { + add_assoc_str(&compare, "output", zend_string_copy(output_id)); + } + if (residual_output == 0 + || down_output == 0 + || output == 0 + || residual_width == 0 + || residual_width != down_width + || residual_width != output_width + || residual_width > (zend_ulong) (SIZE_MAX / sizeof(float))) { + add_assoc_string(&compare, "status", "ffn_output_residual_compare_argument_invalid"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(ffn_output_probe, "ffn_output_residual_numeric_compare_failed", true); + add_assoc_zval(ffn_output_probe, "ffn_output_residual_numeric_compare", &compare); + return FAILURE; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + add_assoc_string(&compare, "status", "cuda_context_current_failed"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(ffn_output_probe, "ffn_output_residual_numeric_compare_failed", true); + add_assoc_zval(ffn_output_probe, "ffn_output_residual_numeric_compare", &compare); + return FAILURE; + } + cu_memcpy_dtoh = king_inference_cuda_decoder_graph_executor_compare_dtoh(model); + if (cu_memcpy_dtoh == NULL) { + add_assoc_string(&compare, "status", "cuMemcpyDtoH_unavailable"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(ffn_output_probe, "ffn_output_residual_numeric_compare_failed", true); + add_assoc_zval(ffn_output_probe, "ffn_output_residual_numeric_compare", &compare); + return FAILURE; + } + + residual = emalloc((size_t) residual_width * sizeof(float)); + down = emalloc((size_t) down_width * sizeof(float)); + actual = emalloc((size_t) output_width * sizeof(float)); + result = cu_memcpy_dtoh(residual, residual_output, (size_t) residual_width * sizeof(float)); + if (result == 0) { + result = cu_memcpy_dtoh(down, down_output, (size_t) down_width * sizeof(float)); + } + if (result == 0) { + result = cu_memcpy_dtoh(actual, output, (size_t) output_width * sizeof(float)); + } + if (result != 0) { + efree(actual); + efree(down); + efree(residual); + add_assoc_string(&compare, "status", "cuMemcpyDtoH_vector_failed"); + add_assoc_long(&compare, "result", result); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(ffn_output_probe, "ffn_output_residual_numeric_compare_failed", true); + add_assoc_zval(ffn_output_probe, "ffn_output_residual_numeric_compare", &compare); + return FAILURE; + } + + for (zend_ulong i = 0; i < output_width; i++) { + double expected = (double) residual[i] + (double) down[i]; + double abs_expected = fabs(expected); + + if (abs_expected > max_expected_abs) { + max_expected_abs = abs_expected; + } + } + tolerance = fmax(0.003, max_expected_abs * 0.00005); + sample_limit = king_inference_cuda_device_vector_numeric_compare_max_values(&model->config); + sample_limit = sample_limit < output_width ? sample_limit : output_width; + array_init(&samples); + for (zend_ulong i = 0; i < output_width; i++) { + double expected = (double) residual[i] + (double) down[i]; + double diff = fabs((double) actual[i] - expected); + double rel_diff = king_inference_cuda_numeric_relative_diff((double) actual[i], expected); + const char *classification = king_inference_cuda_numeric_sample_classification( + (double) actual[i], + expected, + diff, + rel_diff, + tolerance, + relative_tolerance + ); + + if (king_inference_cuda_numeric_sample_matches(classification)) { + matched_values++; + } else { + matched = false; + } + if (diff > max_abs_diff) { + max_abs_diff = diff; + } + if (rel_diff > max_relative_diff && isfinite(rel_diff)) { + max_relative_diff = rel_diff; + } + if (i < sample_limit) { + king_inference_cuda_decoder_graph_executor_ffn_compare_add_sample( + &samples, + i, + "ffn_output_residual", + layer, + (double) actual[i], + expected, + diff, + rel_diff, + tolerance, + relative_tolerance, + classification + ); + } + } + + efree(actual); + efree(down); + efree(residual); + king_inference_cuda_decoder_graph_executor_ffn_compare_add_status(&compare, matched); + add_assoc_bool(&compare, "matched", matched); + add_assoc_long(&compare, "compared_values", (zend_long) output_width); + add_assoc_long(&compare, "matched_values", (zend_long) matched_values); + add_assoc_long(&compare, "width", (zend_long) output_width); + add_assoc_long(&compare, "sampled_values", (zend_long) sample_limit); + add_assoc_double(&compare, "tolerance", tolerance); + add_assoc_double(&compare, "max_abs_tolerance", tolerance); + add_assoc_double(&compare, "max_relative_tolerance", relative_tolerance); + add_assoc_double(&compare, "max_abs_diff", max_abs_diff); + add_assoc_double(&compare, "max_relative_diff", max_relative_diff); + add_assoc_double(&compare, "max_expected_abs", max_expected_abs); + add_assoc_zval(&compare, "samples", &samples); + add_assoc_bool(ffn_output_probe, "ffn_output_residual_numeric_compare_failed", !matched); + add_assoc_zval(ffn_output_probe, "ffn_output_residual_numeric_compare", &compare); + if (!matched) { + king_inference_cuda_decoder_graph_executor_compare_down_set_error( + model, + "ffn_output_residual_mismatch", + output_width, + matched_values, + max_abs_diff, + tolerance + ); + } + return matched ? SUCCESS : FAILURE; +} diff --git a/extension/src/inference/cuda/decoder_graph/device/compare/ffn/cuda_decoder_graph_executor_ffn_gate_up_compare.inc b/extension/src/inference/cuda/decoder_graph/device/compare/ffn/cuda_decoder_graph_executor_ffn_gate_up_compare.inc new file mode 100644 index 000000000..6896b540f --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/device/compare/ffn/cuda_decoder_graph_executor_ffn_gate_up_compare.inc @@ -0,0 +1,324 @@ +/* + * Internal CUDA FFN gate/up projection compare for block-0. + */ + +#include "cuda_decoder_graph_executor_ffn_swiglu_compare.inc" + +static void king_inference_cuda_decoder_graph_executor_ffn_gate_up_add_projection_summary( + zval *target, + const char *name, + zend_string *tensor, + zend_ulong rows, + zend_ulong cols, + zend_ulong matched_values, + double max_abs_diff, + double max_relative_diff, + double max_expected_abs, + double tolerance, + double relative_tolerance, + zval *samples +) { + zval summary; + bool matched = matched_values == rows; + + array_init(&summary); + add_assoc_string(&summary, "projection", name); + add_assoc_str(&summary, "tensor", zend_string_copy(tensor)); + king_inference_cuda_decoder_graph_executor_ffn_compare_add_status(&summary, matched); + add_assoc_bool(&summary, "matched", matched); + add_assoc_long(&summary, "compared_values", (zend_long) rows); + add_assoc_long(&summary, "matched_values", (zend_long) matched_values); + add_assoc_long(&summary, "rows", (zend_long) rows); + add_assoc_long(&summary, "cols", (zend_long) cols); + add_assoc_double(&summary, "max_abs_diff", max_abs_diff); + add_assoc_double(&summary, "max_relative_diff", max_relative_diff); + add_assoc_double(&summary, "max_expected_abs", max_expected_abs); + add_assoc_double(&summary, "tolerance", tolerance); + add_assoc_double(&summary, "max_abs_tolerance", tolerance); + add_assoc_double(&summary, "max_relative_tolerance", relative_tolerance); + add_assoc_zval(&summary, "samples", samples); + add_assoc_zval(target, name, &summary); +} + +static zend_result king_inference_cuda_decoder_graph_executor_compare_ffn_projection_rows( + king_inference_tensor_native_view *view, + const double *input, + const float *actual, + const char *op, + zend_ulong layer, + zend_ulong rows, + zend_ulong cols, + double tolerance, + double relative_tolerance, + zend_ulong sample_limit, + zend_ulong *matched_values, + double *max_abs_diff, + double *max_relative_diff, + double *max_expected_abs, + zval *samples +) { + *matched_values = 0; + *max_abs_diff = 0.0; + *max_relative_diff = 0.0; + *max_expected_abs = 0.0; + array_init(samples); + for (zend_ulong row = 0; row < rows; row++) { + double expected = 0.0; + double diff; + double rel_diff; + double abs_expected; + const char *classification; + + if (king_inference_tensor_row_dot(view, row * cols, cols, input, &expected) != SUCCESS) { + zval_ptr_dtor(samples); + return FAILURE; + } + diff = fabs((double) actual[row] - expected); + rel_diff = king_inference_cuda_numeric_relative_diff((double) actual[row], expected); + classification = king_inference_cuda_numeric_sample_classification( + (double) actual[row], + expected, + diff, + rel_diff, + tolerance, + relative_tolerance + ); + abs_expected = fabs(expected); + if (king_inference_cuda_numeric_sample_matches(classification)) { + (*matched_values)++; + } + if (diff > *max_abs_diff) { + *max_abs_diff = diff; + } + if (rel_diff > *max_relative_diff && isfinite(rel_diff)) { + *max_relative_diff = rel_diff; + } + if (abs_expected > *max_expected_abs) { + *max_expected_abs = abs_expected; + } + if (row < sample_limit) { + king_inference_cuda_decoder_graph_executor_ffn_compare_add_sample( + samples, + row, + op, + layer, + (double) actual[row], + expected, + diff, + rel_diff, + tolerance, + relative_tolerance, + classification + ); + } + } + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_compare_block0_ffn_gate_up( + king_inference_model_object *model, + zval *gate_op, + zval *up_op, + king_inference_cuda_device_ptr ffn_norm_output, + zend_ulong ffn_norm_width, + king_inference_cuda_device_ptr gate_output, + zend_ulong gate_rows, + king_inference_cuda_device_ptr up_output, + zend_ulong up_rows, + zval *ffn_gate_up_probe +) { + zend_string *gate_weight = NULL; + zend_string *up_weight = NULL; + king_inference_tensor_native_view gate_view; + king_inference_tensor_native_view up_view; + king_inference_cuda_memcpy_dtoh_fn cu_memcpy_dtoh; + zend_ulong gate_cols = 0; + zend_ulong gate_view_rows = 0; + zend_ulong up_cols = 0; + zend_ulong up_view_rows = 0; + float *input_f32 = NULL; + float *gate_actual = NULL; + float *up_actual = NULL; + double *input = NULL; + zend_ulong gate_matched = 0; + zend_ulong up_matched = 0; + double gate_max_abs_diff = 0.0; + double up_max_abs_diff = 0.0; + double gate_max_relative_diff = 0.0; + double up_max_relative_diff = 0.0; + double gate_max_expected_abs = 0.0; + double up_max_expected_abs = 0.0; + double tolerance; + double relative_tolerance = king_inference_cuda_numeric_tolerance_relative("ffn_gate_up"); + zend_ulong sample_limit; + int result; + bool matched; + zval compare; + zval projections; + zval gate_samples; + zval up_samples; + + if (!king_inference_cuda_device_vector_numeric_compare_enabled(&model->config)) { + return SUCCESS; + } + if (gate_op == NULL + || up_op == NULL + || king_inference_graph_required_string(gate_op, "weight", &gate_weight) != SUCCESS + || king_inference_graph_required_string(up_op, "weight", &up_weight) != SUCCESS) { + return FAILURE; + } + if (!zend_string_equals_literal(gate_weight, "blk.0.ffn_gate.weight") + || !zend_string_equals_literal(up_weight, "blk.0.ffn_up.weight")) { + return SUCCESS; + } + + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_ffn_gate_up_numeric_compare"); + add_assoc_string(&compare, "reference", "cpu_ffn_norm_readback_gate_up_row_dot"); + add_assoc_str(&compare, "gate_tensor", zend_string_copy(gate_weight)); + add_assoc_str(&compare, "up_tensor", zend_string_copy(up_weight)); + if (ffn_norm_output == 0 + || gate_output == 0 + || up_output == 0 + || ffn_norm_width == 0 + || gate_rows == 0 + || gate_rows != up_rows + || ffn_norm_width > (zend_ulong) (SIZE_MAX / sizeof(float)) + || gate_rows > (zend_ulong) (SIZE_MAX / sizeof(float))) { + add_assoc_string(&compare, "status", "ffn_gate_up_compare_argument_invalid"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(ffn_gate_up_probe, "ffn_gate_up_numeric_compare_failed", true); + add_assoc_zval(ffn_gate_up_probe, "ffn_gate_up_numeric_compare", &compare); + return FAILURE; + } + if (king_inference_tensor_native_view_resolve(model, gate_weight, &gate_view) != SUCCESS + || king_inference_tensor_native_view_resolve(model, up_weight, &up_view) != SUCCESS) { + zval_ptr_dtor(&compare); + return FAILURE; + } + if (gate_view.rank != 2 + || up_view.rank != 2 + || king_inference_tensor_dimension_at(gate_view.dimensions, 0, &gate_cols) != SUCCESS + || king_inference_tensor_dimension_at(gate_view.dimensions, 1, &gate_view_rows) != SUCCESS + || king_inference_tensor_dimension_at(up_view.dimensions, 0, &up_cols) != SUCCESS + || king_inference_tensor_dimension_at(up_view.dimensions, 1, &up_view_rows) != SUCCESS + || gate_cols != ffn_norm_width + || up_cols != ffn_norm_width + || gate_view_rows != gate_rows + || up_view_rows != up_rows) { + add_assoc_string(&compare, "status", "ffn_gate_up_tensor_shape_invalid"); + add_assoc_bool(&compare, "matched", false); + add_assoc_long(&compare, "input_width", (zend_long) ffn_norm_width); + add_assoc_long(&compare, "gate_rows", (zend_long) gate_rows); + add_assoc_long(&compare, "up_rows", (zend_long) up_rows); + add_assoc_bool(ffn_gate_up_probe, "ffn_gate_up_numeric_compare_failed", true); + add_assoc_zval(ffn_gate_up_probe, "ffn_gate_up_numeric_compare", &compare); + return FAILURE; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + add_assoc_string(&compare, "status", "cuda_context_current_failed"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(ffn_gate_up_probe, "ffn_gate_up_numeric_compare_failed", true); + add_assoc_zval(ffn_gate_up_probe, "ffn_gate_up_numeric_compare", &compare); + return FAILURE; + } + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH_v2"); + if (cu_memcpy_dtoh == NULL) { + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH"); + } + if (cu_memcpy_dtoh == NULL) { + add_assoc_string(&compare, "status", "cuMemcpyDtoH_unavailable"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(ffn_gate_up_probe, "ffn_gate_up_numeric_compare_failed", true); + add_assoc_zval(ffn_gate_up_probe, "ffn_gate_up_numeric_compare", &compare); + return FAILURE; + } + + input_f32 = emalloc((size_t) ffn_norm_width * sizeof(float)); + gate_actual = emalloc((size_t) gate_rows * sizeof(float)); + up_actual = emalloc((size_t) up_rows * sizeof(float)); + input = emalloc((size_t) ffn_norm_width * sizeof(double)); + result = cu_memcpy_dtoh(input_f32, ffn_norm_output, (size_t) ffn_norm_width * sizeof(float)); + if (result == 0) { + result = cu_memcpy_dtoh(gate_actual, gate_output, (size_t) gate_rows * sizeof(float)); + } + if (result == 0) { + result = cu_memcpy_dtoh(up_actual, up_output, (size_t) up_rows * sizeof(float)); + } + if (result != 0) { + efree(input); + efree(up_actual); + efree(gate_actual); + efree(input_f32); + add_assoc_string(&compare, "status", "cuMemcpyDtoH_vector_failed"); + add_assoc_long(&compare, "result", result); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(ffn_gate_up_probe, "ffn_gate_up_numeric_compare_failed", true); + add_assoc_zval(ffn_gate_up_probe, "ffn_gate_up_numeric_compare", &compare); + return FAILURE; + } + for (zend_ulong i = 0; i < ffn_norm_width; i++) { + input[i] = (double) input_f32[i]; + } + + tolerance = 0.003; + sample_limit = king_inference_cuda_device_vector_numeric_compare_max_values(&model->config); + sample_limit = sample_limit < gate_rows ? sample_limit : gate_rows; + if (king_inference_cuda_decoder_graph_executor_compare_ffn_projection_rows(&gate_view, input, gate_actual, "ffn_gate_up", 0, gate_rows, gate_cols, tolerance, relative_tolerance, sample_limit, &gate_matched, &gate_max_abs_diff, &gate_max_relative_diff, &gate_max_expected_abs, &gate_samples) != SUCCESS) { + efree(input); + efree(up_actual); + efree(gate_actual); + efree(input_f32); + zval_ptr_dtor(&compare); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_compare_ffn_projection_rows(&up_view, input, up_actual, "ffn_gate_up", 0, up_rows, up_cols, tolerance, relative_tolerance, sample_limit, &up_matched, &up_max_abs_diff, &up_max_relative_diff, &up_max_expected_abs, &up_samples) != SUCCESS) { + zval_ptr_dtor(&gate_samples); + efree(input); + efree(up_actual); + efree(gate_actual); + efree(input_f32); + zval_ptr_dtor(&compare); + return FAILURE; + } + matched = gate_matched == gate_rows && up_matched == up_rows; + array_init(&projections); + king_inference_cuda_decoder_graph_executor_ffn_gate_up_add_projection_summary(&projections, "gate", gate_weight, gate_rows, gate_cols, gate_matched, gate_max_abs_diff, gate_max_relative_diff, gate_max_expected_abs, tolerance, relative_tolerance, &gate_samples); + king_inference_cuda_decoder_graph_executor_ffn_gate_up_add_projection_summary(&projections, "up", up_weight, up_rows, up_cols, up_matched, up_max_abs_diff, up_max_relative_diff, up_max_expected_abs, tolerance, relative_tolerance, &up_samples); + + efree(input); + efree(up_actual); + efree(gate_actual); + efree(input_f32); + king_inference_cuda_decoder_graph_executor_ffn_compare_add_status(&compare, matched); + add_assoc_bool(&compare, "matched", matched); + add_assoc_long(&compare, "compared_values", (zend_long) (gate_rows + up_rows)); + add_assoc_long(&compare, "matched_values", (zend_long) (gate_matched + up_matched)); + add_assoc_long(&compare, "input_width", (zend_long) ffn_norm_width); + add_assoc_long(&compare, "gate_rows", (zend_long) gate_rows); + add_assoc_long(&compare, "up_rows", (zend_long) up_rows); + add_assoc_long(&compare, "sampled_values", (zend_long) sample_limit); + add_assoc_double(&compare, "tolerance", tolerance); + add_assoc_double(&compare, "max_abs_tolerance", tolerance); + add_assoc_double(&compare, "max_relative_tolerance", relative_tolerance); + add_assoc_double(&compare, "max_abs_diff", fmax(gate_max_abs_diff, up_max_abs_diff)); + add_assoc_double(&compare, "max_relative_diff", fmax(gate_max_relative_diff, up_max_relative_diff)); + add_assoc_zval(&compare, "projections", &projections); + add_assoc_bool(ffn_gate_up_probe, "ffn_gate_up_numeric_compare_failed", !matched); + add_assoc_zval(ffn_gate_up_probe, "ffn_gate_up_numeric_compare", &compare); + if (!matched) { + char message[256]; + + snprintf( + message, + sizeof(message), + "king.numeric_mismatch.detected ffn_gate_up_mismatch compared=%lu matched=%lu max_abs_diff=%.9g tolerance=%.9g", + (unsigned long) (gate_rows + up_rows), + (unsigned long) (gate_matched + up_matched), + fmax(gate_max_abs_diff, up_max_abs_diff), + tolerance + ); + king_inference_cuda_decoder_graph_executor_set_error(model, -1, message); + } + return matched ? SUCCESS : FAILURE; +} diff --git a/extension/src/inference/cuda/decoder_graph/device/compare/ffn/cuda_decoder_graph_executor_ffn_swiglu_compare.inc b/extension/src/inference/cuda/decoder_graph/device/compare/ffn/cuda_decoder_graph_executor_ffn_swiglu_compare.inc new file mode 100644 index 000000000..1b6439fd2 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/device/compare/ffn/cuda_decoder_graph_executor_ffn_swiglu_compare.inc @@ -0,0 +1,213 @@ +/* + * Internal CUDA FFN SwiGLU compare for block-0. + */ + +static double king_inference_cuda_decoder_graph_executor_cpu_silu(double value) +{ + if (value >= 0.0) { + double e = exp(-value); + return value / (1.0 + e); + } + + { + double e = exp(value); + return (value * e) / (1.0 + e); + } +} + +static double king_inference_cuda_decoder_graph_executor_cpu_gelu_tanh(double value) +{ + return 0.5 * value * (1.0 + tanh(0.7978845608028654 * (value + 0.044715 * value * value * value))); +} + +static double king_inference_cuda_decoder_graph_executor_cpu_ffn_activation(double value, zend_ulong activation) +{ + return activation == 1 + ? king_inference_cuda_decoder_graph_executor_cpu_gelu_tanh(value) + : king_inference_cuda_decoder_graph_executor_cpu_silu(value); +} + +static zend_result king_inference_cuda_decoder_graph_executor_compare_block0_ffn_swiglu( + king_inference_model_object *model, + zend_string *gate_id, + king_inference_cuda_device_ptr gate_output, + zend_ulong gate_rows, + zend_string *up_id, + king_inference_cuda_device_ptr up_output, + zend_ulong up_rows, + king_inference_cuda_device_ptr swiglu_output, + zend_ulong swiglu_width, + zend_ulong activation, + zval *ffn_swiglu_probe +) { + king_inference_cuda_memcpy_dtoh_fn cu_memcpy_dtoh; + float *gate = NULL; + float *up = NULL; + float *actual = NULL; + double max_expected_abs = 0.0; + double max_abs_diff = 0.0; + double max_relative_diff = 0.0; + double tolerance; + double relative_tolerance = king_inference_cuda_numeric_tolerance_relative("ffn_swiglu"); + zend_ulong matched_values = 0; + zend_ulong sample_limit; + bool matched = true; + int result; + zval compare; + zval samples; + + if (!king_inference_cuda_device_vector_numeric_compare_enabled(&model->config)) { + return SUCCESS; + } + if (gate_id == NULL + || up_id == NULL + || !zend_string_equals_literal(gate_id, "l0_ffn_gate") + || !zend_string_equals_literal(up_id, "l0_ffn_up")) { + return SUCCESS; + } + + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_ffn_swiglu_numeric_compare"); + add_assoc_string(&compare, "reference", activation == 1 ? "cpu_gelu_tanh_gate_times_up_readback" : "cpu_silu_gate_times_up_readback"); + add_assoc_long(&compare, "activation", (zend_long) activation); + if (gate_output == 0 + || up_output == 0 + || swiglu_output == 0 + || gate_rows == 0 + || gate_rows != up_rows + || gate_rows != swiglu_width + || gate_rows > (zend_ulong) (SIZE_MAX / sizeof(float))) { + add_assoc_string(&compare, "status", "ffn_swiglu_compare_argument_invalid"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(ffn_swiglu_probe, "ffn_swiglu_numeric_compare_failed", true); + add_assoc_zval(ffn_swiglu_probe, "ffn_swiglu_numeric_compare", &compare); + return FAILURE; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + add_assoc_string(&compare, "status", "cuda_context_current_failed"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(ffn_swiglu_probe, "ffn_swiglu_numeric_compare_failed", true); + add_assoc_zval(ffn_swiglu_probe, "ffn_swiglu_numeric_compare", &compare); + return FAILURE; + } + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH_v2"); + if (cu_memcpy_dtoh == NULL) { + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH"); + } + if (cu_memcpy_dtoh == NULL) { + add_assoc_string(&compare, "status", "cuMemcpyDtoH_unavailable"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(ffn_swiglu_probe, "ffn_swiglu_numeric_compare_failed", true); + add_assoc_zval(ffn_swiglu_probe, "ffn_swiglu_numeric_compare", &compare); + return FAILURE; + } + + gate = emalloc((size_t) gate_rows * sizeof(float)); + up = emalloc((size_t) up_rows * sizeof(float)); + actual = emalloc((size_t) swiglu_width * sizeof(float)); + result = cu_memcpy_dtoh(gate, gate_output, (size_t) gate_rows * sizeof(float)); + if (result == 0) { + result = cu_memcpy_dtoh(up, up_output, (size_t) up_rows * sizeof(float)); + } + if (result == 0) { + result = cu_memcpy_dtoh(actual, swiglu_output, (size_t) swiglu_width * sizeof(float)); + } + if (result != 0) { + efree(actual); + efree(up); + efree(gate); + add_assoc_string(&compare, "status", "cuMemcpyDtoH_vector_failed"); + add_assoc_long(&compare, "result", result); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(ffn_swiglu_probe, "ffn_swiglu_numeric_compare_failed", true); + add_assoc_zval(ffn_swiglu_probe, "ffn_swiglu_numeric_compare", &compare); + return FAILURE; + } + + sample_limit = king_inference_cuda_device_vector_numeric_compare_max_values(&model->config); + sample_limit = sample_limit < swiglu_width ? sample_limit : swiglu_width; + array_init(&samples); + for (zend_ulong i = 0; i < swiglu_width; i++) { + double expected = king_inference_cuda_decoder_graph_executor_cpu_ffn_activation((double) gate[i], activation) * (double) up[i]; + double abs_expected = fabs(expected); + + if (abs_expected > max_expected_abs) { + max_expected_abs = abs_expected; + } + } + tolerance = fmax(0.0005, max_expected_abs * 0.00005); + for (zend_ulong i = 0; i < swiglu_width; i++) { + double expected = king_inference_cuda_decoder_graph_executor_cpu_ffn_activation((double) gate[i], activation) * (double) up[i]; + double diff = fabs((double) actual[i] - expected); + double rel_diff = king_inference_cuda_numeric_relative_diff((double) actual[i], expected); + const char *classification = king_inference_cuda_numeric_sample_classification( + (double) actual[i], + expected, + diff, + rel_diff, + tolerance, + relative_tolerance + ); + + if (king_inference_cuda_numeric_sample_matches(classification)) { + matched_values++; + } else { + matched = false; + } + if (diff > max_abs_diff) { + max_abs_diff = diff; + } + if (rel_diff > max_relative_diff && isfinite(rel_diff)) { + max_relative_diff = rel_diff; + } + if (i < sample_limit) { + king_inference_cuda_decoder_graph_executor_ffn_compare_add_sample( + &samples, + i, + "ffn_swiglu", + 0, + (double) actual[i], + expected, + diff, + rel_diff, + tolerance, + relative_tolerance, + classification + ); + } + } + + efree(actual); + efree(up); + efree(gate); + king_inference_cuda_decoder_graph_executor_ffn_compare_add_status(&compare, matched); + add_assoc_bool(&compare, "matched", matched); + add_assoc_long(&compare, "compared_values", (zend_long) swiglu_width); + add_assoc_long(&compare, "matched_values", (zend_long) matched_values); + add_assoc_long(&compare, "width", (zend_long) swiglu_width); + add_assoc_long(&compare, "sampled_values", (zend_long) sample_limit); + add_assoc_double(&compare, "tolerance", tolerance); + add_assoc_double(&compare, "max_abs_tolerance", tolerance); + add_assoc_double(&compare, "max_relative_tolerance", relative_tolerance); + add_assoc_double(&compare, "max_abs_diff", max_abs_diff); + add_assoc_double(&compare, "max_relative_diff", max_relative_diff); + add_assoc_double(&compare, "max_expected_abs", max_expected_abs); + add_assoc_zval(&compare, "samples", &samples); + add_assoc_bool(ffn_swiglu_probe, "ffn_swiglu_numeric_compare_failed", !matched); + add_assoc_zval(ffn_swiglu_probe, "ffn_swiglu_numeric_compare", &compare); + if (!matched) { + char message[256]; + + snprintf( + message, + sizeof(message), + "king.numeric_mismatch.detected ffn_swiglu_mismatch compared=%lu matched=%lu max_abs_diff=%.9g tolerance=%.9g", + (unsigned long) swiglu_width, + (unsigned long) matched_values, + max_abs_diff, + tolerance + ); + king_inference_cuda_decoder_graph_executor_set_error(model, -1, message); + } + return matched ? SUCCESS : FAILURE; +} diff --git a/extension/src/inference/cuda/decoder_graph/device/compare/matvec/cuda_decoder_graph_executor_matvec_compare_helpers.inc b/extension/src/inference/cuda/decoder_graph/device/compare/matvec/cuda_decoder_graph_executor_matvec_compare_helpers.inc new file mode 100644 index 000000000..1ca94f8a3 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/device/compare/matvec/cuda_decoder_graph_executor_matvec_compare_helpers.inc @@ -0,0 +1,94 @@ +static double king_inference_cuda_decoder_graph_executor_reference_embedding_scale( + king_inference_model_object *model +) { + zend_ulong embedding = model->gguf.architecture_params[KING_INFERENCE_ARCH_EMBEDDING_LENGTH]; + + if ((king_inference_gguf_architecture_is_gemma3(&model->gguf) + || king_inference_gguf_architecture_is_gemma4(&model->gguf)) + && embedding > 0) { + return king_inference_graph_sqrt((double) embedding); + } + + return 1.0; +} + +static zend_result king_inference_cuda_decoder_graph_executor_build_bos_first_rms_norm_vector( + king_inference_model_object *model, + zend_ulong token_id, + zend_string *rms_weight_tensor, + zend_ulong width, + double epsilon, + double **vector_out, + double *scale_out +) { + zend_string *embedding_tensor = NULL; + king_inference_tensor_native_view embedding_view; + king_inference_tensor_native_view weight_view; + zend_ulong cols = 0; + zend_ulong rows = 0; + zend_ulong base; + double *vector; + double embedding_scale = king_inference_cuda_decoder_graph_executor_reference_embedding_scale(model); + double sumsq = 0.0; + double scale; + + *vector_out = NULL; + *scale_out = 0.0; + if (width == 0 || rms_weight_tensor == NULL || epsilon <= 0.0) { + return FAILURE; + } + if (king_inference_resolve_token_embedding_tensor_name(model, NULL, &embedding_tensor, NULL, NULL) != SUCCESS + || embedding_tensor == NULL) { + return FAILURE; + } + if (king_inference_tensor_native_view_resolve(model, embedding_tensor, &embedding_view) != SUCCESS) { + zend_string_release(embedding_tensor); + return FAILURE; + } + zend_string_release(embedding_tensor); + if (king_inference_tensor_native_view_resolve(model, rms_weight_tensor, &weight_view) != SUCCESS) { + return FAILURE; + } + if (embedding_view.rank != 2 + || king_inference_tensor_dimension_at(embedding_view.dimensions, 0, &cols) != SUCCESS + || king_inference_tensor_dimension_at(embedding_view.dimensions, 1, &rows) != SUCCESS + || cols != width + || token_id >= rows + || token_id > ((zend_ulong) -1) / cols + || weight_view.rank != 1 + || weight_view.elements != width) { + return FAILURE; + } + + base = token_id * cols; + vector = ecalloc((size_t) width, sizeof(double)); + for (zend_ulong i = 0; i < width; i++) { + double value; + + if (king_inference_tensor_value_at(&embedding_view, base + i, &value) != SUCCESS) { + efree(vector); + return FAILURE; + } + value *= embedding_scale; + sumsq += value * value; + vector[i] = value; + } + scale = 1.0 / king_inference_graph_sqrt((sumsq / (double) width) + epsilon); + if (!isfinite(scale)) { + efree(vector); + return FAILURE; + } + for (zend_ulong i = 0; i < width; i++) { + double weight_value; + + if (king_inference_tensor_value_at(&weight_view, i, &weight_value) != SUCCESS) { + efree(vector); + return FAILURE; + } + vector[i] *= scale * weight_value; + } + + *vector_out = vector; + *scale_out = scale; + return SUCCESS; +} diff --git a/extension/src/inference/cuda/decoder_graph/device/compare/matvec/cuda_decoder_graph_executor_q4_matvec_compare.inc b/extension/src/inference/cuda/decoder_graph/device/compare/matvec/cuda_decoder_graph_executor_q4_matvec_compare.inc new file mode 100644 index 000000000..f966486a2 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/device/compare/matvec/cuda_decoder_graph_executor_q4_matvec_compare.inc @@ -0,0 +1,249 @@ +/* + * Initial linear matvec numeric compares. + * + * The file still owns the initial compare aggregation because the first CUDA + * linear op currently has one probe envelope. Individual tensor-type compares + * live in focused helpers under this directory. + */ + +static void king_inference_cuda_decoder_graph_executor_add_q4_matvec_compare_failure( + zval *linear_probe, + zval *compare, + const char *status +) { + add_assoc_string(compare, "status", status != NULL ? status : "q4_matvec_numeric_compare_failed"); + add_assoc_bool(compare, "matched", false); + add_assoc_zval(linear_probe, "q4_matvec_numeric_compare", compare); +} + +static zend_result king_inference_cuda_decoder_graph_executor_compare_q4_attn_q_row( + king_inference_model_object *model, + zend_ulong token_id, + zend_string *rms_weight_tensor, + zend_string *linear_weight_tensor, + king_inference_cuda_device_ptr linear_output, + zend_ulong input_width, + zend_ulong linear_rows, + double epsilon, + zval *linear_probe +) { + king_inference_tensor_native_view weight_view; + zend_ulong cols = 0; + zend_ulong rows = 0; + double *input_vector = NULL; + double scale = 0.0; + double expected = 0.0; + float expected_f32; + zval compare; + zval *matched; + + if (!king_inference_cuda_device_vector_numeric_compare_enabled(&model->config)) { + return SUCCESS; + } + if (linear_weight_tensor == NULL + || !zend_string_equals_literal(linear_weight_tensor, "blk.0.attn_q.weight")) { + return SUCCESS; + } + + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_q4_matvec_row_numeric_compare"); + add_assoc_long(&compare, "token_id", (zend_long) token_id); + if (linear_weight_tensor != NULL) { + add_assoc_str(&compare, "tensor", zend_string_copy(linear_weight_tensor)); + } + + if (linear_output == 0 + || input_width == 0 + || linear_rows == 0 + || rms_weight_tensor == NULL + || linear_weight_tensor == NULL) { + king_inference_cuda_decoder_graph_executor_add_q4_matvec_compare_failure( + linear_probe, + &compare, + "q4_matvec_compare_argument_invalid" + ); + return FAILURE; + } + if (king_inference_tensor_native_view_resolve(model, linear_weight_tensor, &weight_view) != SUCCESS) { + zval_ptr_dtor(&compare); + return FAILURE; + } + if (weight_view.type != 12) { + zval_ptr_dtor(&compare); + return SUCCESS; + } + if (weight_view.rank != 2 + || king_inference_tensor_dimension_at(weight_view.dimensions, 0, &cols) != SUCCESS + || king_inference_tensor_dimension_at(weight_view.dimensions, 1, &rows) != SUCCESS + || cols != input_width + || rows == 0 + || linear_rows > rows) { + king_inference_cuda_decoder_graph_executor_add_q4_matvec_compare_failure( + linear_probe, + &compare, + "q4_matvec_tensor_shape_invalid" + ); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_build_bos_first_rms_norm_vector( + model, + token_id, + rms_weight_tensor, + input_width, + epsilon, + &input_vector, + &scale + ) != SUCCESS) { + zval_ptr_dtor(&compare); + return FAILURE; + } + if (king_inference_tensor_row_dot(&weight_view, 0, input_width, input_vector, &expected) != SUCCESS) { + efree(input_vector); + zval_ptr_dtor(&compare); + return FAILURE; + } + efree(input_vector); + expected_f32 = (float) expected; + + zval_ptr_dtor(&compare); + if (king_inference_cuda_device_vector_numeric_compare( + model, + "blk0_attn_q_q4_k_row0", + "matvec", + 0, + ZSTR_VAL(linear_weight_tensor), + 0, + linear_output, + 1, + &expected_f32, + 1, + 0.001, + &compare + ) != SUCCESS) { + add_assoc_string(&compare, "type", "gpu_decoder_q4_matvec_row_numeric_compare"); + add_assoc_str(&compare, "tensor", zend_string_copy(linear_weight_tensor)); + add_assoc_string(&compare, "tensor_type", "Q4_K"); + add_assoc_long(&compare, "row_index", 0); + add_assoc_long(&compare, "input_width", (zend_long) input_width); + add_assoc_long(&compare, "rows", (zend_long) rows); + add_assoc_double(&compare, "cpu_reference", expected); + add_assoc_double(&compare, "rms_norm_scale", scale); + add_assoc_zval(linear_probe, "q4_matvec_numeric_compare", &compare); + return FAILURE; + } + + add_assoc_string(&compare, "type", "gpu_decoder_q4_matvec_row_numeric_compare"); + add_assoc_string(&compare, "reference", "cpu_q4_k_row_dot_first_rms_norm"); + add_assoc_str(&compare, "tensor", zend_string_copy(linear_weight_tensor)); + add_assoc_string(&compare, "tensor_type", "Q4_K"); + add_assoc_long(&compare, "row_index", 0); + add_assoc_long(&compare, "output_index", 0); + add_assoc_long(&compare, "input_width", (zend_long) input_width); + add_assoc_long(&compare, "rows", (zend_long) rows); + add_assoc_double(&compare, "cpu_reference", expected); + add_assoc_double(&compare, "rms_norm_scale", scale); + matched = king_inference_array_find(&compare, "matched"); + add_assoc_bool(linear_probe, "q4_matvec_numeric_compare_failed", matched == NULL || !zend_is_true(matched)); + add_assoc_zval(linear_probe, "q4_matvec_numeric_compare", &compare); + return matched != NULL && zend_is_true(matched) ? SUCCESS : FAILURE; +} + +static void king_inference_cuda_decoder_graph_executor_record_q4_matvec_compare( + king_inference_model_object *model, + zend_ulong token_id, + zend_string *rms_weight_tensor, + zend_string *linear_weight_tensor, + king_inference_cuda_device_ptr linear_output, + zend_ulong input_width, + zend_ulong linear_rows, + double epsilon, + zval *linear_probe +) { + (void) king_inference_cuda_decoder_graph_executor_compare_q4_attn_q_row( + model, + token_id, + rms_weight_tensor, + linear_weight_tensor, + linear_output, + input_width, + linear_rows, + epsilon, + linear_probe + ); +} + +static void king_inference_cuda_decoder_graph_executor_copy_q4_matvec_compare( + zval *result, + zval *linear_probe +) { + zval *compare = king_inference_array_find(linear_probe, "q4_matvec_numeric_compare"); + zval copy; + + if (compare == NULL) { + return; + } + + ZVAL_COPY(©, compare); + add_assoc_zval(result, "q4_matvec_numeric_compare", ©); +} + +#include "cuda_decoder_graph_executor_q6_matvec_compare.inc" + +static void king_inference_cuda_decoder_graph_executor_record_quantized_matvec_compare( + king_inference_model_object *model, + zend_ulong token_id, + zend_string *rms_weight_tensor, + zend_string *linear_weight_tensor, + king_inference_cuda_device_ptr linear_output, + zend_ulong input_width, + zend_ulong linear_rows, + double epsilon, + zval *linear_probe +) { + king_inference_cuda_decoder_graph_executor_record_q4_matvec_compare( + model, + token_id, + rms_weight_tensor, + linear_weight_tensor, + linear_output, + input_width, + linear_rows, + epsilon, + linear_probe + ); + king_inference_cuda_decoder_graph_executor_record_q6_matvec_compare( + model, + token_id, + rms_weight_tensor, + linear_weight_tensor, + linear_output, + input_width, + linear_rows, + epsilon, + linear_probe + ); + king_inference_cuda_decoder_graph_executor_record_qkv_projection_compare( + model, + token_id, + rms_weight_tensor, + linear_weight_tensor, + linear_output, + input_width, + linear_rows, + epsilon, + linear_probe + ); +} + +static void king_inference_cuda_decoder_graph_executor_copy_initial_numeric_compares( + zval *result, + zval *embedding_probe, + zval *rms_norm_probe, + zval *linear_probe +) { + king_inference_cuda_decoder_graph_executor_copy_embedding_compare(result, embedding_probe); + king_inference_cuda_decoder_graph_executor_copy_first_rms_norm_compare(result, rms_norm_probe); + king_inference_cuda_decoder_graph_executor_copy_q4_matvec_compare(result, linear_probe); + king_inference_cuda_decoder_graph_executor_copy_q6_matvec_compare(result, linear_probe); + king_inference_cuda_decoder_graph_executor_copy_qkv_projection_compare(result, linear_probe); +} diff --git a/extension/src/inference/cuda/decoder_graph/device/compare/matvec/cuda_decoder_graph_executor_q6_matvec_compare.inc b/extension/src/inference/cuda/decoder_graph/device/compare/matvec/cuda_decoder_graph_executor_q6_matvec_compare.inc new file mode 100644 index 000000000..a58be743a --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/device/compare/matvec/cuda_decoder_graph_executor_q6_matvec_compare.inc @@ -0,0 +1,177 @@ +static void king_inference_cuda_decoder_graph_executor_add_q6_matvec_compare_failure( + zval *linear_probe, + zval *compare, + const char *status +) { + add_assoc_string(compare, "status", status != NULL ? status : "q6_matvec_numeric_compare_failed"); + add_assoc_bool(compare, "matched", false); + add_assoc_zval(linear_probe, "q6_matvec_numeric_compare", compare); +} + +static zend_result king_inference_cuda_decoder_graph_executor_compare_q6_embedding_row( + king_inference_model_object *model, + zend_ulong token_id, + zend_string *rms_weight_tensor, + zend_string *linear_weight_tensor, + king_inference_cuda_device_ptr linear_output, + zend_ulong input_width, + zend_ulong linear_rows, + double epsilon, + zval *linear_probe +) { + king_inference_tensor_native_view weight_view; + zend_ulong cols = 0; + zend_ulong rows = 0; + double *input_vector = NULL; + double scale = 0.0; + double expected = 0.0; + float expected_f32; + zval compare; + zval *matched; + + if (!king_inference_cuda_device_vector_numeric_compare_enabled(&model->config)) { + return SUCCESS; + } + if (linear_weight_tensor == NULL + || !zend_string_equals_literal(linear_weight_tensor, "token_embd.weight")) { + return SUCCESS; + } + + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_q6_matvec_row_numeric_compare"); + add_assoc_long(&compare, "token_id", (zend_long) token_id); + add_assoc_str(&compare, "tensor", zend_string_copy(linear_weight_tensor)); + + if (linear_output == 0 + || input_width == 0 + || linear_rows == 0 + || rms_weight_tensor == NULL) { + king_inference_cuda_decoder_graph_executor_add_q6_matvec_compare_failure( + linear_probe, + &compare, + "q6_matvec_compare_argument_invalid" + ); + return FAILURE; + } + if (king_inference_tensor_native_view_resolve(model, linear_weight_tensor, &weight_view) != SUCCESS) { + zval_ptr_dtor(&compare); + return FAILURE; + } + if (weight_view.type != 14) { + zval_ptr_dtor(&compare); + return SUCCESS; + } + if (weight_view.rank != 2 + || king_inference_tensor_dimension_at(weight_view.dimensions, 0, &cols) != SUCCESS + || king_inference_tensor_dimension_at(weight_view.dimensions, 1, &rows) != SUCCESS + || cols != input_width + || rows == 0 + || linear_rows > rows) { + king_inference_cuda_decoder_graph_executor_add_q6_matvec_compare_failure( + linear_probe, + &compare, + "q6_matvec_tensor_shape_invalid" + ); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_build_bos_first_rms_norm_vector( + model, + token_id, + rms_weight_tensor, + input_width, + epsilon, + &input_vector, + &scale + ) != SUCCESS) { + zval_ptr_dtor(&compare); + return FAILURE; + } + if (king_inference_tensor_row_dot(&weight_view, 0, input_width, input_vector, &expected) != SUCCESS) { + efree(input_vector); + zval_ptr_dtor(&compare); + return FAILURE; + } + efree(input_vector); + expected_f32 = (float) expected; + + zval_ptr_dtor(&compare); + if (king_inference_cuda_device_vector_numeric_compare( + model, + "token_embd_q6_k_row0", + "matvec", + 0, + ZSTR_VAL(linear_weight_tensor), + 0, + linear_output, + 1, + &expected_f32, + 1, + 0.001, + &compare + ) != SUCCESS) { + add_assoc_string(&compare, "type", "gpu_decoder_q6_matvec_row_numeric_compare"); + add_assoc_str(&compare, "tensor", zend_string_copy(linear_weight_tensor)); + add_assoc_string(&compare, "tensor_type", "Q6_K"); + add_assoc_long(&compare, "row_index", 0); + add_assoc_long(&compare, "input_width", (zend_long) input_width); + add_assoc_long(&compare, "rows", (zend_long) rows); + add_assoc_double(&compare, "cpu_reference", expected); + add_assoc_double(&compare, "rms_norm_scale", scale); + add_assoc_zval(linear_probe, "q6_matvec_numeric_compare", &compare); + return FAILURE; + } + + add_assoc_string(&compare, "type", "gpu_decoder_q6_matvec_row_numeric_compare"); + add_assoc_string(&compare, "reference", "cpu_q6_k_row_dot_first_rms_norm"); + add_assoc_str(&compare, "tensor", zend_string_copy(linear_weight_tensor)); + add_assoc_string(&compare, "tensor_type", "Q6_K"); + add_assoc_long(&compare, "row_index", 0); + add_assoc_long(&compare, "output_index", 0); + add_assoc_long(&compare, "input_width", (zend_long) input_width); + add_assoc_long(&compare, "rows", (zend_long) rows); + add_assoc_double(&compare, "cpu_reference", expected); + add_assoc_double(&compare, "rms_norm_scale", scale); + matched = king_inference_array_find(&compare, "matched"); + add_assoc_bool(linear_probe, "q6_matvec_numeric_compare_failed", matched == NULL || !zend_is_true(matched)); + add_assoc_zval(linear_probe, "q6_matvec_numeric_compare", &compare); + return matched != NULL && zend_is_true(matched) ? SUCCESS : FAILURE; +} + +static void king_inference_cuda_decoder_graph_executor_record_q6_matvec_compare( + king_inference_model_object *model, + zend_ulong token_id, + zend_string *rms_weight_tensor, + zend_string *linear_weight_tensor, + king_inference_cuda_device_ptr linear_output, + zend_ulong input_width, + zend_ulong linear_rows, + double epsilon, + zval *linear_probe +) { + (void) king_inference_cuda_decoder_graph_executor_compare_q6_embedding_row( + model, + token_id, + rms_weight_tensor, + linear_weight_tensor, + linear_output, + input_width, + linear_rows, + epsilon, + linear_probe + ); +} + +static void king_inference_cuda_decoder_graph_executor_copy_q6_matvec_compare( + zval *result, + zval *linear_probe +) { + zval *compare = king_inference_array_find(linear_probe, "q6_matvec_numeric_compare"); + zval copy; + + if (compare == NULL) { + return; + } + + ZVAL_COPY(©, compare); + add_assoc_zval(result, "q6_matvec_numeric_compare", ©); +} diff --git a/extension/src/inference/cuda/decoder_graph/device/compare/norm/cuda_decoder_graph_executor_ffn_norm_compare.inc b/extension/src/inference/cuda/decoder_graph/device/compare/norm/cuda_decoder_graph_executor_ffn_norm_compare.inc new file mode 100644 index 000000000..c9d43dd4e --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/device/compare/norm/cuda_decoder_graph_executor_ffn_norm_compare.inc @@ -0,0 +1,209 @@ +/* + * Internal CUDA FFN RMSNorm compare for block-0. + */ + +static zend_result king_inference_cuda_decoder_graph_executor_compare_block0_ffn_norm( + king_inference_model_object *model, + zend_string *weight_name, + king_inference_cuda_device_ptr residual_output, + zend_ulong residual_width, + king_inference_cuda_device_ptr ffn_norm_output, + zend_ulong ffn_norm_width, + double epsilon, + zval *ffn_norm_probe +) { + king_inference_tensor_native_view weight_view; + king_inference_cuda_memcpy_dtoh_fn cu_memcpy_dtoh; + float *residual = NULL; + float *actual = NULL; + double *expected = NULL; + double sumsq = 0.0; + double scale; + double max_expected_abs = 0.0; + double max_abs_diff = 0.0; + double tolerance; + zend_ulong matched_values = 0; + zend_ulong sample_limit; + bool matched = true; + int result; + zval compare; + zval samples; + + if (!king_inference_cuda_device_vector_numeric_compare_enabled(&model->config)) { + return SUCCESS; + } + if (weight_name == NULL || !zend_string_equals_literal(weight_name, "blk.0.ffn_norm.weight")) { + return SUCCESS; + } + if (residual_output == 0 + || ffn_norm_output == 0 + || residual_width == 0 + || ffn_norm_width != residual_width + || residual_width > (zend_ulong) (SIZE_MAX / sizeof(float)) + || epsilon <= 0.0) { + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_ffn_norm_numeric_compare"); + add_assoc_string(&compare, "status", "ffn_norm_compare_argument_invalid"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(ffn_norm_probe, "ffn_norm_numeric_compare_failed", true); + add_assoc_zval(ffn_norm_probe, "ffn_norm_numeric_compare", &compare); + return FAILURE; + } + if (king_inference_tensor_native_view_resolve(model, weight_name, &weight_view) != SUCCESS) { + return FAILURE; + } + if (weight_view.rank != 1 || weight_view.elements != residual_width) { + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_ffn_norm_numeric_compare"); + add_assoc_string(&compare, "status", "ffn_norm_weight_shape_invalid"); + add_assoc_bool(&compare, "matched", false); + add_assoc_str(&compare, "weight_tensor", zend_string_copy(weight_name)); + add_assoc_long(&compare, "weight_elements", (zend_long) weight_view.elements); + add_assoc_long(&compare, "residual_width", (zend_long) residual_width); + add_assoc_bool(ffn_norm_probe, "ffn_norm_numeric_compare_failed", true); + add_assoc_zval(ffn_norm_probe, "ffn_norm_numeric_compare", &compare); + return FAILURE; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_ffn_norm_numeric_compare"); + add_assoc_string(&compare, "status", "cuda_context_current_failed"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(ffn_norm_probe, "ffn_norm_numeric_compare_failed", true); + add_assoc_zval(ffn_norm_probe, "ffn_norm_numeric_compare", &compare); + return FAILURE; + } + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH_v2"); + if (cu_memcpy_dtoh == NULL) { + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH"); + } + if (cu_memcpy_dtoh == NULL) { + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_ffn_norm_numeric_compare"); + add_assoc_string(&compare, "status", "cuMemcpyDtoH_unavailable"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(ffn_norm_probe, "ffn_norm_numeric_compare_failed", true); + add_assoc_zval(ffn_norm_probe, "ffn_norm_numeric_compare", &compare); + return FAILURE; + } + + residual = emalloc((size_t) residual_width * sizeof(float)); + actual = emalloc((size_t) residual_width * sizeof(float)); + expected = emalloc((size_t) residual_width * sizeof(double)); + result = cu_memcpy_dtoh(residual, residual_output, (size_t) residual_width * sizeof(float)); + if (result == 0) { + result = cu_memcpy_dtoh(actual, ffn_norm_output, (size_t) residual_width * sizeof(float)); + } + if (result != 0) { + efree(expected); + efree(actual); + efree(residual); + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_ffn_norm_numeric_compare"); + add_assoc_string(&compare, "status", "cuMemcpyDtoH_vector_failed"); + add_assoc_long(&compare, "result", result); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(ffn_norm_probe, "ffn_norm_numeric_compare_failed", true); + add_assoc_zval(ffn_norm_probe, "ffn_norm_numeric_compare", &compare); + return FAILURE; + } + + for (zend_ulong i = 0; i < residual_width; i++) { + sumsq += (double) residual[i] * (double) residual[i]; + } + scale = 1.0 / king_inference_graph_sqrt((sumsq / (double) residual_width) + epsilon); + if (!isfinite(scale)) { + efree(expected); + efree(actual); + efree(residual); + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_ffn_norm_numeric_compare"); + add_assoc_string(&compare, "status", "ffn_norm_cpu_scale_invalid"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(ffn_norm_probe, "ffn_norm_numeric_compare_failed", true); + add_assoc_zval(ffn_norm_probe, "ffn_norm_numeric_compare", &compare); + return FAILURE; + } + for (zend_ulong i = 0; i < residual_width; i++) { + double weight_value; + double abs_value; + + if (king_inference_tensor_value_at(&weight_view, i, &weight_value) != SUCCESS) { + efree(expected); + efree(actual); + efree(residual); + return FAILURE; + } + expected[i] = (double) residual[i] * scale * weight_value; + abs_value = fabs(expected[i]); + if (abs_value > max_expected_abs) { + max_expected_abs = abs_value; + } + } + + tolerance = fmax(0.0002, max_expected_abs * 0.00005); + sample_limit = king_inference_cuda_device_vector_numeric_compare_max_values(&model->config); + sample_limit = sample_limit < residual_width ? sample_limit : residual_width; + array_init(&samples); + for (zend_ulong i = 0; i < residual_width; i++) { + double diff = fabs((double) actual[i] - expected[i]); + + if (diff <= tolerance) { + matched_values++; + } else { + matched = false; + } + if (diff > max_abs_diff) { + max_abs_diff = diff; + } + if (i < sample_limit) { + zval sample; + + array_init(&sample); + add_assoc_long(&sample, "index", (zend_long) i); + add_assoc_double(&sample, "actual", (double) actual[i]); + add_assoc_double(&sample, "expected", expected[i]); + add_assoc_double(&sample, "abs_diff", diff); + add_next_index_zval(&samples, &sample); + } + } + + efree(expected); + efree(actual); + efree(residual); + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_ffn_norm_numeric_compare"); + add_assoc_string(&compare, "reference", "cpu_attention_residual_readback_rms_norm_weight"); + add_assoc_string(&compare, "status", matched ? "matched" : "mismatch"); + add_assoc_bool(&compare, "matched", matched); + add_assoc_str(&compare, "weight_tensor", zend_string_copy(weight_name)); + add_assoc_long(&compare, "compared_values", (zend_long) residual_width); + add_assoc_long(&compare, "matched_values", (zend_long) matched_values); + add_assoc_long(&compare, "sampled_values", (zend_long) sample_limit); + add_assoc_long(&compare, "residual_width", (zend_long) residual_width); + add_assoc_long(&compare, "ffn_norm_width", (zend_long) ffn_norm_width); + add_assoc_double(&compare, "epsilon", epsilon); + add_assoc_double(&compare, "scale", scale); + add_assoc_double(&compare, "tolerance", tolerance); + add_assoc_double(&compare, "max_abs_diff", max_abs_diff); + add_assoc_double(&compare, "max_expected_abs", max_expected_abs); + add_assoc_zval(&compare, "samples", &samples); + add_assoc_bool(ffn_norm_probe, "ffn_norm_numeric_compare_failed", !matched); + add_assoc_zval(ffn_norm_probe, "ffn_norm_numeric_compare", &compare); + if (!matched) { + char message[256]; + + snprintf( + message, + sizeof(message), + "ffn_norm_mismatch tensor=%s compared=%lu matched=%lu max_abs_diff=%.9g tolerance=%.9g", + ZSTR_VAL(weight_name), + (unsigned long) residual_width, + (unsigned long) matched_values, + max_abs_diff, + tolerance + ); + king_inference_cuda_decoder_graph_executor_set_error(model, -1, message); + } + return matched ? SUCCESS : FAILURE; +} diff --git a/extension/src/inference/cuda/decoder_graph/device/compare/norm/cuda_decoder_graph_executor_final_norm_compare.inc b/extension/src/inference/cuda/decoder_graph/device/compare/norm/cuda_decoder_graph_executor_final_norm_compare.inc new file mode 100644 index 000000000..9ec5a2af1 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/device/compare/norm/cuda_decoder_graph_executor_final_norm_compare.inc @@ -0,0 +1,247 @@ +/* + * Internal CUDA final RMSNorm compare against CPU readback. + */ + +static king_inference_cuda_memcpy_dtoh_fn +king_inference_cuda_decoder_graph_executor_final_norm_compare_dtoh( + king_inference_model_object *model +) { + king_inference_cuda_memcpy_dtoh_fn cu_memcpy_dtoh; + + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH_v2"); + if (cu_memcpy_dtoh == NULL) { + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH"); + } + + return cu_memcpy_dtoh; +} + +static void king_inference_cuda_decoder_graph_executor_final_norm_compare_fail( + zval *final_norm_probe, + const char *status +) { + zval compare; + + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_final_norm_numeric_compare"); + add_assoc_string(&compare, "status", status); + add_assoc_string(&compare, "classification", "validation_error"); + add_assoc_string(&compare, "error_category", "validation"); + add_assoc_string(&compare, "error_code", "king.validation_error.detected"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(final_norm_probe, "final_norm_numeric_compare_failed", true); + add_assoc_zval(final_norm_probe, "final_norm_numeric_compare", &compare); +} + +static zend_result king_inference_cuda_decoder_graph_executor_compare_final_norm( + king_inference_model_object *model, + zend_string *input_id, + zend_string *final_norm_id, + zend_string *weight_name, + king_inference_cuda_device_ptr input_output, + zend_ulong input_width, + king_inference_cuda_device_ptr final_norm_output, + zend_ulong final_norm_width, + double epsilon, + zval *final_norm_probe +) { + king_inference_tensor_native_view weight_view; + king_inference_cuda_memcpy_dtoh_fn cu_memcpy_dtoh; + float *input = NULL; + float *actual = NULL; + double *expected = NULL; + double sumsq = 0.0; + double scale; + double max_expected_abs = 0.0; + double max_abs_diff = 0.0; + double tolerance; + zend_ulong matched_values = 0; + zend_ulong sample_limit; + bool matched = true; + int result; + zval compare; + zval samples; + + if (!king_inference_cuda_device_vector_numeric_compare_enabled(&model->config)) { + return SUCCESS; + } + if (final_norm_id == NULL || !zend_string_equals_literal(final_norm_id, "final_norm")) { + return SUCCESS; + } + if (weight_name == NULL + || input_output == 0 + || final_norm_output == 0 + || input_width == 0 + || final_norm_width != input_width + || input_width > (zend_ulong) (SIZE_MAX / sizeof(float)) + || epsilon <= 0.0) { + king_inference_cuda_decoder_graph_executor_final_norm_compare_fail( + final_norm_probe, + "final_norm_compare_argument_invalid" + ); + return FAILURE; + } + if (king_inference_tensor_native_view_resolve(model, weight_name, &weight_view) != SUCCESS) { + return FAILURE; + } + if (weight_view.rank != 1 || weight_view.elements != input_width) { + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_final_norm_numeric_compare"); + add_assoc_string(&compare, "status", "final_norm_weight_shape_invalid"); + add_assoc_string(&compare, "classification", "shape_mismatch"); + add_assoc_string(&compare, "error_category", "shape"); + add_assoc_string(&compare, "error_code", "king.shape_mismatch.detected"); + add_assoc_bool(&compare, "matched", false); + add_assoc_str(&compare, "weight_tensor", zend_string_copy(weight_name)); + add_assoc_long(&compare, "weight_elements", (zend_long) weight_view.elements); + add_assoc_long(&compare, "input_width", (zend_long) input_width); + add_assoc_bool(final_norm_probe, "final_norm_numeric_compare_failed", true); + add_assoc_zval(final_norm_probe, "final_norm_numeric_compare", &compare); + return FAILURE; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + king_inference_cuda_decoder_graph_executor_final_norm_compare_fail( + final_norm_probe, + "cuda_context_current_failed" + ); + return FAILURE; + } + cu_memcpy_dtoh = king_inference_cuda_decoder_graph_executor_final_norm_compare_dtoh(model); + if (cu_memcpy_dtoh == NULL) { + king_inference_cuda_decoder_graph_executor_final_norm_compare_fail( + final_norm_probe, + "cuMemcpyDtoH_unavailable" + ); + return FAILURE; + } + + input = emalloc((size_t) input_width * sizeof(float)); + actual = emalloc((size_t) input_width * sizeof(float)); + expected = emalloc((size_t) input_width * sizeof(double)); + result = cu_memcpy_dtoh(input, input_output, (size_t) input_width * sizeof(float)); + if (result == 0) { + result = cu_memcpy_dtoh(actual, final_norm_output, (size_t) input_width * sizeof(float)); + } + if (result != 0) { + efree(expected); + efree(actual); + efree(input); + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_final_norm_numeric_compare"); + add_assoc_string(&compare, "status", "cuMemcpyDtoH_vector_failed"); + add_assoc_string(&compare, "classification", "device_readback_failed"); + add_assoc_string(&compare, "error_category", "device_readback"); + add_assoc_string(&compare, "error_code", "king.device_readback.failed"); + add_assoc_long(&compare, "result", result); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(final_norm_probe, "final_norm_numeric_compare_failed", true); + add_assoc_zval(final_norm_probe, "final_norm_numeric_compare", &compare); + return FAILURE; + } + + for (zend_ulong i = 0; i < input_width; i++) { + sumsq += (double) input[i] * (double) input[i]; + } + scale = 1.0 / king_inference_graph_sqrt((sumsq / (double) input_width) + epsilon); + if (!isfinite(scale)) { + efree(expected); + efree(actual); + efree(input); + king_inference_cuda_decoder_graph_executor_final_norm_compare_fail( + final_norm_probe, + "final_norm_cpu_scale_invalid" + ); + return FAILURE; + } + for (zend_ulong i = 0; i < input_width; i++) { + double weight_value; + double abs_value; + + if (king_inference_tensor_value_at(&weight_view, i, &weight_value) != SUCCESS) { + efree(expected); + efree(actual); + efree(input); + return FAILURE; + } + expected[i] = (double) input[i] * scale * weight_value; + abs_value = fabs(expected[i]); + if (abs_value > max_expected_abs) { + max_expected_abs = abs_value; + } + } + + tolerance = fmax(0.0002, max_expected_abs * 0.00005); + sample_limit = king_inference_cuda_device_vector_numeric_compare_max_values(&model->config); + sample_limit = sample_limit < input_width ? sample_limit : input_width; + array_init(&samples); + for (zend_ulong i = 0; i < input_width; i++) { + double diff = fabs((double) actual[i] - expected[i]); + + if (diff <= tolerance) { + matched_values++; + } else { + matched = false; + } + if (diff > max_abs_diff) { + max_abs_diff = diff; + } + if (i < sample_limit) { + zval sample; + + array_init(&sample); + add_assoc_long(&sample, "index", (zend_long) i); + add_assoc_double(&sample, "actual", (double) actual[i]); + add_assoc_double(&sample, "expected", expected[i]); + add_assoc_double(&sample, "abs_diff", diff); + add_assoc_bool(&sample, "matched", diff <= tolerance); + add_assoc_string(&sample, "classification", diff <= tolerance ? "matched" : "numeric_mismatch"); + add_next_index_zval(&samples, &sample); + } + } + + efree(expected); + efree(actual); + efree(input); + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_final_norm_numeric_compare"); + add_assoc_string(&compare, "reference", "cpu_final_hidden_readback_rms_norm_weight"); + add_assoc_string(&compare, "status", matched ? "matched" : "mismatch"); + add_assoc_string(&compare, "classification", matched ? "matched" : "numeric_mismatch"); + add_assoc_string(&compare, "error_category", matched ? "none" : "numeric_mismatch"); + add_assoc_string(&compare, "error_code", matched ? "none" : "king.numeric_mismatch.detected"); + add_assoc_bool(&compare, "matched", matched); + if (input_id != NULL) { + add_assoc_str(&compare, "input", zend_string_copy(input_id)); + } + add_assoc_str(&compare, "final_norm", zend_string_copy(final_norm_id)); + add_assoc_str(&compare, "weight_tensor", zend_string_copy(weight_name)); + add_assoc_long(&compare, "compared_values", (zend_long) input_width); + add_assoc_long(&compare, "matched_values", (zend_long) matched_values); + add_assoc_long(&compare, "sampled_values", (zend_long) sample_limit); + add_assoc_long(&compare, "input_width", (zend_long) input_width); + add_assoc_long(&compare, "final_norm_width", (zend_long) final_norm_width); + add_assoc_double(&compare, "epsilon", epsilon); + add_assoc_double(&compare, "scale", scale); + add_assoc_double(&compare, "tolerance", tolerance); + add_assoc_double(&compare, "max_abs_diff", max_abs_diff); + add_assoc_double(&compare, "max_expected_abs", max_expected_abs); + add_assoc_zval(&compare, "samples", &samples); + add_assoc_bool(final_norm_probe, "final_norm_numeric_compare_failed", !matched); + add_assoc_zval(final_norm_probe, "final_norm_numeric_compare", &compare); + if (!matched) { + char message[256]; + + snprintf( + message, + sizeof(message), + "king.numeric_mismatch.detected final_norm_mismatch tensor=%s compared=%lu matched=%lu max_abs_diff=%.9g tolerance=%.9g", + ZSTR_VAL(weight_name), + (unsigned long) input_width, + (unsigned long) matched_values, + max_abs_diff, + tolerance + ); + king_inference_cuda_decoder_graph_executor_set_error(model, -1, message); + } + return matched ? SUCCESS : FAILURE; +} diff --git a/extension/src/inference/cuda/decoder_graph/device/compare/norm/cuda_decoder_graph_executor_first_rms_norm_compare.inc b/extension/src/inference/cuda/decoder_graph/device/compare/norm/cuda_decoder_graph_executor_first_rms_norm_compare.inc new file mode 100644 index 000000000..a999f1fa7 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/device/compare/norm/cuda_decoder_graph_executor_first_rms_norm_compare.inc @@ -0,0 +1,314 @@ +static double king_inference_cuda_decoder_graph_executor_compare_numeric_value(zval *value) +{ + if (value == NULL) { + return 0.0; + } + if (Z_TYPE_P(value) == IS_DOUBLE) { + return Z_DVAL_P(value); + } + if (Z_TYPE_P(value) == IS_LONG) { + return (double) Z_LVAL_P(value); + } + + return 0.0; +} + +static void king_inference_cuda_decoder_graph_executor_add_first_rms_norm_compare_failure( + zval *rms_norm_probe, + zval *compare, + const char *status +) { + add_assoc_string(compare, "status", status != NULL ? status : "first_rms_norm_numeric_compare_failed"); + add_assoc_bool(compare, "matched", false); + add_assoc_zval(rms_norm_probe, "numeric_compare", compare); +} + +static zend_result king_inference_cuda_decoder_graph_executor_compare_first_rms_norm( + king_inference_model_object *model, + zend_ulong token_id, + zend_string *weight_tensor, + king_inference_cuda_device_ptr rms_norm_output, + zend_ulong width, + double epsilon, + double embedding_scale, + zval *rms_norm_probe +) { + zend_string *embedding_tensor = NULL; + zend_string *embedding_name = NULL; + const char *status = NULL; + king_inference_tensor_native_view embedding_view; + king_inference_tensor_native_view weight_view; + zend_ulong cols = 0; + zend_ulong rows = 0; + zend_ulong base; + zend_ulong chunk_limit; + zend_ulong compared_values = 0; + zend_ulong readback_bytes = 0; + zend_ulong chunk_count = 0; + double sumsq = 0.0; + double scale; + double max_abs_diff = 0.0; + bool matched = true; + zval compare; + zval chunks; + + if (!king_inference_cuda_device_vector_numeric_compare_enabled(&model->config)) { + return SUCCESS; + } + + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_first_rms_norm_numeric_compare"); + add_assoc_long(&compare, "token_id", (zend_long) token_id); + add_assoc_long(&compare, "width", (zend_long) width); + add_assoc_double(&compare, "embedding_scale", embedding_scale); + if (weight_tensor != NULL) { + add_assoc_str(&compare, "weight_tensor", zend_string_copy(weight_tensor)); + } + + if (rms_norm_output == 0 || width == 0 || weight_tensor == NULL || epsilon <= 0.0) { + king_inference_cuda_decoder_graph_executor_add_first_rms_norm_compare_failure( + rms_norm_probe, + &compare, + "first_rms_norm_compare_arguments_invalid" + ); + return FAILURE; + } + if (king_inference_resolve_token_embedding_tensor_name(model, NULL, &embedding_tensor, NULL, &status) != SUCCESS + || embedding_tensor == NULL) { + king_inference_cuda_decoder_graph_executor_add_first_rms_norm_compare_failure( + rms_norm_probe, + &compare, + status != NULL ? status : "embedding_tensor_not_resolved" + ); + return FAILURE; + } + + embedding_name = zend_string_copy(embedding_tensor); + add_assoc_str(&compare, "input_tensor", zend_string_copy(embedding_name)); + if (king_inference_tensor_native_view_resolve(model, embedding_tensor, &embedding_view) != SUCCESS) { + zend_string_release(embedding_tensor); + zend_string_release(embedding_name); + zval_ptr_dtor(&compare); + return FAILURE; + } + zend_string_release(embedding_tensor); + if (king_inference_tensor_native_view_resolve(model, weight_tensor, &weight_view) != SUCCESS) { + zend_string_release(embedding_name); + zval_ptr_dtor(&compare); + return FAILURE; + } + + if (embedding_view.rank != 2 + || king_inference_tensor_dimension_at(embedding_view.dimensions, 0, &cols) != SUCCESS + || king_inference_tensor_dimension_at(embedding_view.dimensions, 1, &rows) != SUCCESS + || cols != width + || token_id >= rows + || token_id > ((zend_ulong) -1) / cols + || weight_view.rank != 1 + || weight_view.elements != width) { + king_inference_cuda_decoder_graph_executor_add_first_rms_norm_compare_failure( + rms_norm_probe, + &compare, + "first_rms_norm_tensor_shape_invalid" + ); + zend_string_release(embedding_name); + return FAILURE; + } + + base = token_id * cols; + if (base > embedding_view.elements || width > embedding_view.elements - base) { + king_inference_cuda_decoder_graph_executor_add_first_rms_norm_compare_failure( + rms_norm_probe, + &compare, + "first_rms_norm_compare_range_invalid" + ); + zend_string_release(embedding_name); + return FAILURE; + } + for (zend_ulong i = 0; i < width; i++) { + double value; + + if (king_inference_tensor_value_at(&embedding_view, base + i, &value) != SUCCESS) { + zval_ptr_dtor(&compare); + zend_string_release(embedding_name); + return FAILURE; + } + value *= embedding_scale; + sumsq += value * value; + } + scale = 1.0 / king_inference_graph_sqrt((sumsq / (double) width) + epsilon); + if (!isfinite(scale)) { + king_inference_cuda_decoder_graph_executor_add_first_rms_norm_compare_failure( + rms_norm_probe, + &compare, + "first_rms_norm_cpu_scale_invalid" + ); + zend_string_release(embedding_name); + return FAILURE; + } + + chunk_limit = king_inference_cuda_device_vector_numeric_compare_max_values(&model->config); + if (chunk_limit == 0) { + king_inference_cuda_decoder_graph_executor_add_first_rms_norm_compare_failure( + rms_norm_probe, + &compare, + "first_rms_norm_chunk_limit_invalid" + ); + zend_string_release(embedding_name); + return FAILURE; + } + + zval_ptr_dtor(&compare); + array_init(&compare); + add_assoc_string(&compare, "label", "bos_first_rms_norm_output"); + add_assoc_bool(&compare, "enabled", true); + add_assoc_bool(&compare, "public_api", false); + add_assoc_string(&compare, "scope", "internal_cuda_device_vector_readback_compare"); + add_assoc_string(&compare, "type", "gpu_decoder_first_rms_norm_numeric_compare"); + add_assoc_string(&compare, "mode", "full_row_chunked"); + add_assoc_string(&compare, "reference", "cpu_embedding_dequant_plus_rms_norm_weight"); + add_assoc_long(&compare, "token_id", (zend_long) token_id); + add_assoc_long(&compare, "width", (zend_long) width); + add_assoc_long(&compare, "chunk_limit", (zend_long) chunk_limit); + add_assoc_double(&compare, "epsilon", epsilon); + add_assoc_double(&compare, "embedding_scale", embedding_scale); + add_assoc_double(&compare, "scale", scale); + add_assoc_str(&compare, "input_tensor", zend_string_copy(embedding_name)); + add_assoc_str(&compare, "weight_tensor", zend_string_copy(weight_tensor)); + array_init(&chunks); + + for (zend_ulong offset = 0; offset < width; offset += chunk_limit) { + zend_ulong chunk_values = width - offset; + float *expected; + zval chunk_compare; + zval chunk_summary; + zval *chunk_matched; + zval *chunk_max_abs_diff; + + if (chunk_values > chunk_limit) { + chunk_values = chunk_limit; + } + expected = emalloc((size_t) chunk_values * sizeof(float)); + for (zend_ulong i = 0; i < chunk_values; i++) { + double input_value; + double weight_value; + + if (king_inference_tensor_value_at(&embedding_view, base + offset + i, &input_value) != SUCCESS + || king_inference_tensor_value_at(&weight_view, offset + i, &weight_value) != SUCCESS) { + efree(expected); + zval_ptr_dtor(&chunks); + zval_ptr_dtor(&compare); + zend_string_release(embedding_name); + return FAILURE; + } + input_value *= embedding_scale; + expected[i] = (float) (input_value * scale * weight_value); + } + + if (king_inference_cuda_device_vector_numeric_compare( + model, + "bos_first_rms_norm_output_chunk", + "rms_norm", + 0, + ZSTR_VAL(weight_tensor), + offset, + rms_norm_output + ((king_inference_cuda_device_ptr) offset * (king_inference_cuda_device_ptr) sizeof(float)), + chunk_values, + expected, + chunk_values, + 0.0001, + &chunk_compare + ) != SUCCESS) { + efree(expected); + add_assoc_long(&chunk_compare, "offset", (zend_long) offset); + add_assoc_long(&chunk_compare, "token_id", (zend_long) token_id); + add_assoc_long(&chunk_compare, "width", (zend_long) width); + add_assoc_zval(rms_norm_probe, "numeric_compare", &chunk_compare); + zval_ptr_dtor(&chunks); + zval_ptr_dtor(&compare); + zend_string_release(embedding_name); + return FAILURE; + } + efree(expected); + + chunk_matched = king_inference_array_find(&chunk_compare, "matched"); + chunk_max_abs_diff = king_inference_array_find(&chunk_compare, "max_abs_diff"); + if (chunk_matched == NULL || !zend_is_true(chunk_matched)) { + matched = false; + } + { + double value = king_inference_cuda_decoder_graph_executor_compare_numeric_value(chunk_max_abs_diff); + if (value > max_abs_diff) { + max_abs_diff = value; + } + } + + array_init(&chunk_summary); + add_assoc_long(&chunk_summary, "offset", (zend_long) offset); + add_assoc_long(&chunk_summary, "compared_values", (zend_long) chunk_values); + add_assoc_bool(&chunk_summary, "matched", chunk_matched != NULL && zend_is_true(chunk_matched)); + add_assoc_double( + &chunk_summary, + "max_abs_diff", + king_inference_cuda_decoder_graph_executor_compare_numeric_value(chunk_max_abs_diff) + ); + add_next_index_zval(&chunks, &chunk_summary); + zval_ptr_dtor(&chunk_compare); + compared_values += chunk_values; + readback_bytes += chunk_values * sizeof(float); + chunk_count++; + } + + add_assoc_string(&compare, "status", matched ? "matched" : "mismatch"); + add_assoc_bool(&compare, "matched", matched); + add_assoc_long(&compare, "compared_values", (zend_long) compared_values); + add_assoc_long(&compare, "readback_bytes", (zend_long) readback_bytes); + add_assoc_double(&compare, "tolerance", 0.0001); + add_assoc_double(&compare, "max_abs_diff", max_abs_diff); + add_assoc_long(&compare, "chunk_count", (zend_long) chunk_count); + add_assoc_zval(&compare, "chunks", &chunks); + add_assoc_long(&compare, "token_id", (zend_long) token_id); + add_assoc_long(&compare, "width", (zend_long) width); + zend_string_release(embedding_name); + add_assoc_zval(rms_norm_probe, "numeric_compare", &compare); + return matched ? SUCCESS : FAILURE; +} + +static void king_inference_cuda_decoder_graph_executor_copy_first_rms_norm_compare( + zval *result, + zval *rms_norm_probe +) { + zval *compare = king_inference_array_find(rms_norm_probe, "numeric_compare"); + zval copy; + + if (compare == NULL) { + return; + } + + ZVAL_COPY(©, compare); + add_assoc_zval(result, "first_rms_norm_numeric_compare", ©); +} + +static void king_inference_cuda_decoder_graph_executor_record_first_rms_norm_compare( + king_inference_model_object *model, + zend_ulong token_id, + zend_string *weight_tensor, + king_inference_cuda_device_ptr rms_norm_output, + zend_ulong width, + double epsilon, + double embedding_scale, + zval *rms_norm_probe +) { + if (king_inference_cuda_decoder_graph_executor_compare_first_rms_norm( + model, + token_id, + weight_tensor, + rms_norm_output, + width, + epsilon, + embedding_scale, + rms_norm_probe + ) != SUCCESS) { + add_assoc_bool(rms_norm_probe, "numeric_compare_failed", true); + } +} diff --git a/extension/src/inference/cuda/decoder_graph/device/compare/projection/cuda_decoder_graph_executor_logits_projection_compare.inc b/extension/src/inference/cuda/decoder_graph/device/compare/projection/cuda_decoder_graph_executor_logits_projection_compare.inc new file mode 100644 index 000000000..88e3b58c6 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/device/compare/projection/cuda_decoder_graph_executor_logits_projection_compare.inc @@ -0,0 +1,407 @@ +/* + * Internal CUDA logits projection compare for bounded readback candidates. + */ + +typedef struct _king_inference_cuda_decoder_graph_executor_cpu_top_logit { + zend_ulong token_id; + double logit; + bool set; +} king_inference_cuda_decoder_graph_executor_cpu_top_logit; + +static king_inference_cuda_memcpy_dtoh_fn +king_inference_cuda_decoder_graph_executor_logits_compare_dtoh( + king_inference_model_object *model +) { + king_inference_cuda_memcpy_dtoh_fn cu_memcpy_dtoh; + + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH_v2"); + if (cu_memcpy_dtoh == NULL) { + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH"); + } + + return cu_memcpy_dtoh; +} + +static void king_inference_cuda_decoder_graph_executor_logits_compare_fail( + zval *logits_probe, + const char *status +) { + zval compare; + + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_logits_projection_numeric_compare"); + add_assoc_string(&compare, "status", status); + add_assoc_string(&compare, "classification", "validation_error"); + add_assoc_string(&compare, "error_category", "validation"); + add_assoc_string(&compare, "error_code", "king.validation_error.detected"); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(logits_probe, "logits_projection_numeric_compare_failed", true); + add_assoc_zval(logits_probe, "logits_projection_numeric_compare", &compare); +} + +static bool king_inference_cuda_decoder_graph_executor_logit_precedes( + double left, + zend_ulong left_token_id, + double right, + zend_ulong right_token_id +) { + return left > right || (left == right && left_token_id < right_token_id); +} + +static void king_inference_cuda_decoder_graph_executor_insert_cpu_top_logit( + king_inference_cuda_decoder_graph_executor_cpu_top_logit *top, + zend_ulong limit, + zend_ulong *count, + zend_ulong token_id, + double logit +) { + zend_ulong slot; + + if (*count < limit) { + slot = *count; + (*count)++; + } else { + slot = limit - 1; + if (!king_inference_cuda_decoder_graph_executor_logit_precedes( + logit, + token_id, + top[slot].logit, + top[slot].token_id + )) { + return; + } + } + + top[slot].token_id = token_id; + top[slot].logit = logit; + top[slot].set = true; + while (slot > 0 + && king_inference_cuda_decoder_graph_executor_logit_precedes( + top[slot].logit, + top[slot].token_id, + top[slot - 1].logit, + top[slot - 1].token_id + )) { + king_inference_cuda_decoder_graph_executor_cpu_top_logit tmp = top[slot - 1]; + + top[slot - 1] = top[slot]; + top[slot] = tmp; + slot--; + } +} + +static zend_result king_inference_cuda_decoder_graph_executor_compute_cpu_top_logits( + king_inference_tensor_native_view *weight_view, + zend_ulong final_norm_width, + zend_ulong logits_rows, + const double *input, + unsigned int *host_indices, + zend_ulong candidate_count, + double *host_expected_logits, + bool *host_expected_seen, + king_inference_cuda_decoder_graph_executor_cpu_top_logit *cpu_top, + zend_ulong *cpu_top_count, + double *max_expected_abs +) { + for (zend_ulong row = 0; row < logits_rows; row++) { + double expected = 0.0; + double abs_expected; + + if (row > ((zend_ulong) -1) / final_norm_width + || king_inference_tensor_row_dot( + weight_view, + row * final_norm_width, + final_norm_width, + input, + &expected + ) != SUCCESS) { + return FAILURE; + } + abs_expected = fabs(expected); + if (abs_expected > *max_expected_abs) { + *max_expected_abs = abs_expected; + } + king_inference_cuda_decoder_graph_executor_insert_cpu_top_logit( + cpu_top, + candidate_count, + cpu_top_count, + row, + expected + ); + for (zend_ulong candidate = 0; candidate < candidate_count; candidate++) { + if ((zend_ulong) host_indices[candidate] == row) { + host_expected_logits[candidate] = expected; + host_expected_seen[candidate] = true; + break; + } + } + } + + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_compare_bounded_logits_projection( + king_inference_model_object *model, + zend_string *weight_name, + king_inference_cuda_device_ptr final_norm_output, + zend_ulong final_norm_width, + zend_ulong logits_rows, + unsigned int *host_indices, + float *host_logits, + zend_ulong candidate_count, + zval *logits_probe +) { + king_inference_tensor_native_view weight_view; + king_inference_cuda_memcpy_dtoh_fn cu_memcpy_dtoh; + zend_ulong cols = 0; + zend_ulong rows = 0; + float *final_norm = NULL; + double *input = NULL; + double host_expected_logits[KING_INFERENCE_CUDA_LOGITS_READBACK_MAX_CANDIDATES]; + bool host_expected_seen[KING_INFERENCE_CUDA_LOGITS_READBACK_MAX_CANDIDATES]; + king_inference_cuda_decoder_graph_executor_cpu_top_logit cpu_top[KING_INFERENCE_CUDA_LOGITS_READBACK_MAX_CANDIDATES]; + zend_ulong cpu_top_count = 0; + double max_expected_abs = 0.0; + double max_abs_diff = 0.0; + double max_rank_abs_diff = 0.0; + double tolerance; + zend_ulong matched_values = 0; + zend_ulong matched_ordering = 0; + bool matched = true; + int result; + zval compare; + zval samples; + zval *sample; + + if (!king_inference_cuda_device_vector_numeric_compare_enabled(&model->config)) { + return SUCCESS; + } + if (weight_name == NULL + || final_norm_output == 0 + || final_norm_width == 0 + || logits_rows == 0 + || host_indices == NULL + || host_logits == NULL + || candidate_count == 0 + || candidate_count > KING_INFERENCE_CUDA_LOGITS_READBACK_MAX_CANDIDATES + || final_norm_width > (zend_ulong) (SIZE_MAX / sizeof(float))) { + king_inference_cuda_decoder_graph_executor_logits_compare_fail( + logits_probe, + "logits_projection_compare_argument_invalid" + ); + return FAILURE; + } + if (king_inference_tensor_native_view_resolve(model, weight_name, &weight_view) != SUCCESS) { + return FAILURE; + } + if (weight_view.rank != 2 + || king_inference_tensor_dimension_at(weight_view.dimensions, 0, &cols) != SUCCESS + || king_inference_tensor_dimension_at(weight_view.dimensions, 1, &rows) != SUCCESS + || cols != final_norm_width + || logits_rows == 0 + || logits_rows > rows) { + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_logits_projection_numeric_compare"); + add_assoc_string(&compare, "status", "logits_projection_tensor_shape_invalid"); + add_assoc_string(&compare, "classification", "shape_mismatch"); + add_assoc_string(&compare, "error_category", "shape"); + add_assoc_string(&compare, "error_code", "king.shape_mismatch.detected"); + add_assoc_bool(&compare, "matched", false); + add_assoc_str(&compare, "weight_tensor", zend_string_copy(weight_name)); + add_assoc_long(&compare, "cols", (zend_long) cols); + add_assoc_long(&compare, "rows", (zend_long) rows); + add_assoc_long(&compare, "final_norm_width", (zend_long) final_norm_width); + add_assoc_long(&compare, "logits_rows", (zend_long) logits_rows); + add_assoc_bool(logits_probe, "logits_projection_numeric_compare_failed", true); + add_assoc_zval(logits_probe, "logits_projection_numeric_compare", &compare); + return FAILURE; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + king_inference_cuda_decoder_graph_executor_logits_compare_fail( + logits_probe, + "cuda_context_current_failed" + ); + return FAILURE; + } + cu_memcpy_dtoh = king_inference_cuda_decoder_graph_executor_logits_compare_dtoh(model); + if (cu_memcpy_dtoh == NULL) { + king_inference_cuda_decoder_graph_executor_logits_compare_fail( + logits_probe, + "cuMemcpyDtoH_unavailable" + ); + return FAILURE; + } + + final_norm = emalloc((size_t) final_norm_width * sizeof(float)); + input = emalloc((size_t) final_norm_width * sizeof(double)); + result = cu_memcpy_dtoh(final_norm, final_norm_output, (size_t) final_norm_width * sizeof(float)); + if (result != 0) { + efree(input); + efree(final_norm); + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_logits_projection_numeric_compare"); + add_assoc_string(&compare, "status", "cuMemcpyDtoH_final_norm_failed"); + add_assoc_string(&compare, "classification", "device_readback_failed"); + add_assoc_string(&compare, "error_category", "device_readback"); + add_assoc_string(&compare, "error_code", "king.device_readback.failed"); + add_assoc_long(&compare, "result", result); + add_assoc_bool(&compare, "matched", false); + add_assoc_bool(logits_probe, "logits_projection_numeric_compare_failed", true); + add_assoc_zval(logits_probe, "logits_projection_numeric_compare", &compare); + return FAILURE; + } + for (zend_ulong i = 0; i < final_norm_width; i++) { + input[i] = (double) final_norm[i]; + } + for (zend_ulong i = 0; i < candidate_count; i++) { + host_expected_logits[i] = 0.0; + host_expected_seen[i] = false; + cpu_top[i].token_id = 0; + cpu_top[i].logit = 0.0; + cpu_top[i].set = false; + } + if (king_inference_cuda_decoder_graph_executor_compute_cpu_top_logits( + &weight_view, + final_norm_width, + logits_rows, + input, + host_indices, + candidate_count, + host_expected_logits, + host_expected_seen, + cpu_top, + &cpu_top_count, + &max_expected_abs + ) != SUCCESS + || cpu_top_count != candidate_count) { + efree(input); + efree(final_norm); + king_inference_cuda_decoder_graph_executor_logits_compare_fail( + logits_probe, + "logits_projection_cpu_top_k_failed" + ); + return FAILURE; + } + + array_init(&samples); + for (zend_ulong i = 0; i < candidate_count; i++) { + double expected = host_expected_logits[i]; + double rank_diff = fabs((double) host_logits[i] - cpu_top[i].logit); + double diff; + zend_ulong token_id = (zend_ulong) host_indices[i]; + bool value_match; + bool order_match; + bool rank_logit_match; + + if (token_id >= logits_rows + || !host_expected_seen[i] + || !cpu_top[i].set) { + zval_ptr_dtor(&samples); + efree(input); + efree(final_norm); + king_inference_cuda_decoder_graph_executor_logits_compare_fail( + logits_probe, + "logits_projection_candidate_invalid" + ); + return FAILURE; + } + + diff = fabs((double) host_logits[i] - expected); + if (diff > max_abs_diff) { + max_abs_diff = diff; + } + if (rank_diff > max_rank_abs_diff) { + max_rank_abs_diff = rank_diff; + } + value_match = diff <= fmax(0.01, max_expected_abs * 0.0002); + order_match = token_id == cpu_top[i].token_id; + rank_logit_match = rank_diff <= fmax(0.01, max_expected_abs * 0.0002); + if (order_match && rank_logit_match) { + matched_ordering++; + } + + { + zval sample; + + array_init(&sample); + add_assoc_long(&sample, "rank", (zend_long) i); + add_assoc_long(&sample, "token_id", (zend_long) token_id); + add_assoc_long(&sample, "cpu_token_id", (zend_long) cpu_top[i].token_id); + add_assoc_double(&sample, "actual", (double) host_logits[i]); + add_assoc_double(&sample, "expected", expected); + add_assoc_double(&sample, "cpu_rank_logit", cpu_top[i].logit); + add_assoc_double(&sample, "abs_diff", diff); + add_assoc_double(&sample, "rank_abs_diff", rank_diff); + add_assoc_bool(&sample, "value_match", value_match); + add_assoc_bool(&sample, "order_match", order_match); + add_assoc_bool(&sample, "rank_logit_match", rank_logit_match); + add_assoc_bool(&sample, "matched", value_match && order_match && rank_logit_match); + add_assoc_string( + &sample, + "classification", + value_match && order_match && rank_logit_match ? "matched" : "numeric_mismatch" + ); + add_next_index_zval(&samples, &sample); + } + } + tolerance = fmax(0.01, max_expected_abs * 0.0002); + ZEND_HASH_FOREACH_VAL(Z_ARRVAL(samples), sample) { + zval *abs_diff = sample != NULL && Z_TYPE_P(sample) == IS_ARRAY + ? king_inference_array_find(sample, "abs_diff") + : NULL; + + if (abs_diff != NULL && Z_TYPE_P(abs_diff) == IS_DOUBLE && Z_DVAL_P(abs_diff) <= tolerance) { + matched_values++; + } else { + matched = false; + } + } ZEND_HASH_FOREACH_END(); + if (matched_ordering != candidate_count) { + matched = false; + } + + efree(input); + efree(final_norm); + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_logits_projection_numeric_compare"); + add_assoc_string(&compare, "reference", "cpu_full_output_projection_top_k"); + add_assoc_string(&compare, "status", matched ? "matched" : "mismatch"); + add_assoc_string(&compare, "classification", matched ? "matched" : "numeric_mismatch"); + add_assoc_string(&compare, "error_category", matched ? "none" : "numeric_mismatch"); + add_assoc_string(&compare, "error_code", matched ? "none" : "king.numeric_mismatch.detected"); + add_assoc_bool(&compare, "matched", matched); + add_assoc_bool(&compare, "ordering_matched", matched_ordering == candidate_count); + add_assoc_str(&compare, "weight_tensor", zend_string_copy(weight_name)); + add_assoc_long(&compare, "candidate_count", (zend_long) candidate_count); + add_assoc_long(&compare, "compared_values", (zend_long) candidate_count); + add_assoc_long(&compare, "matched_values", (zend_long) matched_values); + add_assoc_long(&compare, "ordering_compared_values", (zend_long) candidate_count); + add_assoc_long(&compare, "ordering_matched_values", (zend_long) matched_ordering); + add_assoc_long(&compare, "final_norm_width", (zend_long) final_norm_width); + add_assoc_long(&compare, "logits_rows", (zend_long) logits_rows); + add_assoc_double(&compare, "tolerance", tolerance); + add_assoc_double(&compare, "max_abs_diff", max_abs_diff); + add_assoc_double(&compare, "max_rank_abs_diff", max_rank_abs_diff); + add_assoc_double(&compare, "max_expected_abs", max_expected_abs); + add_assoc_zval(&compare, "samples", &samples); + add_assoc_bool(logits_probe, "logits_projection_numeric_compare_failed", !matched); + add_assoc_bool(logits_probe, "bounded_top_k_numeric_validated", matched); + add_assoc_bool(logits_probe, "bounded_top_k_ordering_validated", matched_ordering == candidate_count); + add_assoc_zval(logits_probe, "logits_projection_numeric_compare", &compare); + if (!matched) { + char message[256]; + + snprintf( + message, + sizeof(message), + "king.numeric_mismatch.detected logits_projection_mismatch tensor=%s compared=%lu matched=%lu max_abs_diff=%.9g tolerance=%.9g", + ZSTR_VAL(weight_name), + (unsigned long) candidate_count, + (unsigned long) matched_values, + max_abs_diff, + tolerance + ); + king_inference_cuda_decoder_graph_executor_set_error(model, -1, message); + } + return matched ? SUCCESS : FAILURE; +} diff --git a/extension/src/inference/cuda/decoder_graph/device/compare/projection/cuda_decoder_graph_executor_qkv_projection_compare.inc b/extension/src/inference/cuda/decoder_graph/device/compare/projection/cuda_decoder_graph_executor_qkv_projection_compare.inc new file mode 100644 index 000000000..d98fb0160 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/device/compare/projection/cuda_decoder_graph_executor_qkv_projection_compare.inc @@ -0,0 +1,367 @@ +/* + * Block-0 Q/K/V projection slice numeric compare. + */ + +static void king_inference_cuda_decoder_graph_executor_add_qkv_projection_compare_failure( + zval *linear_probe, + zval *compare, + const char *status +) { + add_assoc_string(compare, "status", status != NULL ? status : "qkv_projection_numeric_compare_failed"); + add_assoc_bool(compare, "matched", false); + add_assoc_zval(linear_probe, "qkv_projection_numeric_compare", compare); +} + +static zend_ulong king_inference_cuda_decoder_graph_executor_qkv_projection_slice_length( + king_inference_model_object *model, + king_inference_attention_role role, + zend_ulong rows +) { + zend_ulong query_heads = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT]; + zend_ulong kv_heads = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT_KV]; + zend_ulong configured = role == KING_INFERENCE_ATTENTION_VALUE + ? model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_VALUE_LENGTH] + : model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_KEY_LENGTH]; + zend_ulong heads = role == KING_INFERENCE_ATTENTION_QUERY ? query_heads : kv_heads; + + if (heads == 0) { + heads = query_heads; + } + if (configured > 0 && configured <= rows) { + return configured; + } + if (heads > 0 && rows >= heads && rows % heads == 0) { + return rows / heads; + } + return rows; +} + +static bool king_inference_cuda_decoder_graph_executor_resolve_qkv_projection_role( + king_inference_model_object *model, + zend_string *linear_weight_tensor, + king_inference_attention_role *role_out, + const char **projection_out, + zend_ulong *slice_length_out, + zend_ulong rows +) { + static const king_inference_attention_role roles[] = { + KING_INFERENCE_ATTENTION_QUERY, + KING_INFERENCE_ATTENTION_KEY, + KING_INFERENCE_ATTENTION_VALUE + }; + static const char *names[] = {"query", "key", "value"}; + + for (size_t i = 0; i < sizeof(roles) / sizeof(roles[0]); i++) { + zend_string *resolved = NULL; + + if (king_inference_resolve_attention_tensor_name(model, roles[i], 0, &resolved, NULL, NULL) == SUCCESS + && resolved != NULL) { + bool matched = linear_weight_tensor != NULL && zend_string_equals(resolved, linear_weight_tensor); + + zend_string_release(resolved); + if (matched) { + *role_out = roles[i]; + *projection_out = names[i]; + *slice_length_out = king_inference_cuda_decoder_graph_executor_qkv_projection_slice_length( + model, + roles[i], + rows + ); + return true; + } + } + } + + return false; +} + +static zend_result king_inference_cuda_decoder_graph_executor_compare_qkv_projection_slice( + king_inference_model_object *model, + zend_ulong token_id, + zend_string *rms_weight_tensor, + zend_string *linear_weight_tensor, + king_inference_cuda_device_ptr linear_output, + zend_ulong input_width, + zend_ulong linear_rows, + double epsilon, + zval *linear_probe +) { + king_inference_tensor_native_view weight_view; + king_inference_tensor_type_info type_info; + king_inference_attention_role role = KING_INFERENCE_ATTENTION_QUERY; + const char *projection = NULL; + zend_ulong cols = 0; + zend_ulong rows = 0; + zend_ulong slice_length = 0; + double *input_vector = NULL; + double scale = 0.0; + float *expected = NULL; + zval compare; + zval *matched; + char label[96]; + + if (!king_inference_cuda_device_vector_numeric_compare_enabled(&model->config)) { + return SUCCESS; + } + if (linear_weight_tensor == NULL) { + return SUCCESS; + } + if (king_inference_tensor_native_view_resolve(model, linear_weight_tensor, &weight_view) != SUCCESS) { + return FAILURE; + } + if (weight_view.rank != 2 + || king_inference_tensor_dimension_at(weight_view.dimensions, 0, &cols) != SUCCESS + || king_inference_tensor_dimension_at(weight_view.dimensions, 1, &rows) != SUCCESS + || !king_inference_cuda_decoder_graph_executor_resolve_qkv_projection_role( + model, + linear_weight_tensor, + &role, + &projection, + &slice_length, + rows + )) { + return SUCCESS; + } + + array_init(&compare); + add_assoc_string(&compare, "type", "gpu_decoder_qkv_projection_slice_numeric_compare"); + add_assoc_string(&compare, "projection", projection); + add_assoc_long(&compare, "token_id", (zend_long) token_id); + add_assoc_str(&compare, "tensor", zend_string_copy(linear_weight_tensor)); + + if (linear_output == 0 + || input_width == 0 + || linear_rows == 0 + || rms_weight_tensor == NULL + || cols != input_width + || rows == 0 + || linear_rows > rows + || slice_length == 0 + || slice_length > linear_rows + || slice_length > (zend_ulong) (SIZE_MAX / sizeof(float))) { + king_inference_cuda_decoder_graph_executor_add_qkv_projection_compare_failure( + linear_probe, + &compare, + "qkv_projection_compare_argument_invalid" + ); + return FAILURE; + } + if (!king_inference_tensor_type_lookup(weight_view.type, &type_info)) { + king_inference_cuda_decoder_graph_executor_add_qkv_projection_compare_failure( + linear_probe, + &compare, + "qkv_projection_tensor_type_unknown" + ); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_build_bos_first_rms_norm_vector( + model, + token_id, + rms_weight_tensor, + input_width, + epsilon, + &input_vector, + &scale + ) != SUCCESS) { + zval_ptr_dtor(&compare); + return FAILURE; + } + + expected = ecalloc((size_t) slice_length, sizeof(float)); + for (zend_ulong i = 0; i < slice_length; i++) { + zend_ulong row_base = i * input_width; + double value = 0.0; + + if (king_inference_tensor_row_dot(&weight_view, row_base, input_width, input_vector, &value) != SUCCESS) { + efree(expected); + efree(input_vector); + zval_ptr_dtor(&compare); + return FAILURE; + } + expected[i] = (float) value; + } + efree(input_vector); + + snprintf(label, sizeof(label), "blk0_attn_%s_projection_head0_slice", projection); + zval_ptr_dtor(&compare); + if (king_inference_cuda_device_vector_numeric_compare( + model, + label, + "qkv_projection", + 0, + ZSTR_VAL(linear_weight_tensor), + 0, + linear_output, + slice_length, + expected, + slice_length, + 0.001, + &compare + ) != SUCCESS) { + add_assoc_string(&compare, "type", "gpu_decoder_qkv_projection_slice_numeric_compare"); + add_assoc_string(&compare, "projection", projection); + add_assoc_str(&compare, "tensor", zend_string_copy(linear_weight_tensor)); + add_assoc_string(&compare, "tensor_type", type_info.name); + add_assoc_long(&compare, "slice_offset", 0); + add_assoc_long(&compare, "slice_length", (zend_long) slice_length); + add_assoc_long(&compare, "input_width", (zend_long) input_width); + add_assoc_long(&compare, "rows", (zend_long) rows); + add_assoc_double(&compare, "rms_norm_scale", scale); + add_assoc_zval(linear_probe, "qkv_projection_numeric_compare", &compare); + efree(expected); + return FAILURE; + } + + add_assoc_string(&compare, "type", "gpu_decoder_qkv_projection_slice_numeric_compare"); + add_assoc_string(&compare, "reference", "cpu_projection_row_dot_first_rms_norm_head0_slice"); + add_assoc_string(&compare, "projection", projection); + add_assoc_str(&compare, "tensor", zend_string_copy(linear_weight_tensor)); + add_assoc_string(&compare, "tensor_type", type_info.name); + add_assoc_long(&compare, "slice_offset", 0); + add_assoc_long(&compare, "slice_length", (zend_long) slice_length); + add_assoc_long(&compare, "input_width", (zend_long) input_width); + add_assoc_long(&compare, "rows", (zend_long) rows); + add_assoc_double(&compare, "rms_norm_scale", scale); + matched = king_inference_array_find(&compare, "matched"); + add_assoc_bool(linear_probe, "qkv_projection_numeric_compare_failed", matched == NULL || !zend_is_true(matched)); + add_assoc_zval(linear_probe, "qkv_projection_numeric_compare", &compare); + efree(expected); + return matched != NULL && zend_is_true(matched) ? SUCCESS : FAILURE; +} + +static void king_inference_cuda_decoder_graph_executor_record_qkv_projection_compare( + king_inference_model_object *model, + zend_ulong token_id, + zend_string *rms_weight_tensor, + zend_string *linear_weight_tensor, + king_inference_cuda_device_ptr linear_output, + zend_ulong input_width, + zend_ulong linear_rows, + double epsilon, + zval *linear_probe +) { + (void) king_inference_cuda_decoder_graph_executor_compare_qkv_projection_slice( + model, + token_id, + rms_weight_tensor, + linear_weight_tensor, + linear_output, + input_width, + linear_rows, + epsilon, + linear_probe + ); +} + +static void king_inference_cuda_decoder_graph_executor_add_qkv_projection_execution_probe( + king_inference_model_object *model, + zval *parent_probe, + const char *field, + const char *type, + zend_ulong token_id, + zend_string *rms_weight_tensor, + zval *linear_op, + king_inference_cuda_device_ptr linear_output, + zend_ulong input_width, + zend_ulong linear_rows, + double epsilon +) { + zval probe; + zval *weight = linear_op != NULL ? king_inference_array_find(linear_op, "weight") : NULL; + zend_string *weight_tensor = weight != NULL && Z_TYPE_P(weight) == IS_STRING ? Z_STR_P(weight) : NULL; + + array_init(&probe); + add_assoc_string(&probe, "type", type); + add_assoc_bool(&probe, "executed", true); + add_assoc_long(&probe, "rows", (zend_long) linear_rows); + king_inference_cuda_decoder_graph_executor_record_qkv_projection_compare( + model, + token_id, + rms_weight_tensor, + weight_tensor, + linear_output, + input_width, + linear_rows, + epsilon, + &probe + ); + add_assoc_zval(parent_probe, field, &probe); +} + +static zval *king_inference_cuda_decoder_graph_executor_first_head_qkv_projection_compare( + zval *kv_head_prepare_probe, + const char *field +) { + zval *heads = king_inference_array_find(kv_head_prepare_probe, "query_heads"); + zval *head0 = heads != NULL && Z_TYPE_P(heads) == IS_ARRAY + ? zend_hash_index_find(Z_ARRVAL_P(heads), 0) + : NULL; + zval *kv_probe = head0 != NULL && Z_TYPE_P(head0) == IS_ARRAY + ? king_inference_array_find(head0, "kv_head_prepare_execution") + : NULL; + zval *projection = kv_probe != NULL && Z_TYPE_P(kv_probe) == IS_ARRAY + ? king_inference_array_find(kv_probe, field) + : NULL; + + return projection != NULL && Z_TYPE_P(projection) == IS_ARRAY + ? king_inference_array_find(projection, "qkv_projection_numeric_compare") + : NULL; +} + +static void king_inference_cuda_decoder_graph_executor_copy_block0_qkv_projection_compares( + zval *result, + zval *linear_probe, + zval *kv_head_prepare_probe +) { + zval compares; + zval copy; + zval *query = king_inference_array_find(linear_probe, "qkv_projection_numeric_compare"); + zval *key = king_inference_cuda_decoder_graph_executor_first_head_qkv_projection_compare( + kv_head_prepare_probe, + "key_projection_device_execution" + ); + zval *value = king_inference_cuda_decoder_graph_executor_first_head_qkv_projection_compare( + kv_head_prepare_probe, + "value_projection_device_execution" + ); + zend_ulong copied = 0; + + array_init(&compares); + if (query != NULL && Z_TYPE_P(query) == IS_ARRAY) { + ZVAL_COPY(©, query); + add_assoc_zval(&compares, "query", ©); + copied++; + } + if (key != NULL && Z_TYPE_P(key) == IS_ARRAY) { + ZVAL_COPY(©, key); + add_assoc_zval(&compares, "key", ©); + copied++; + } + if (value != NULL && Z_TYPE_P(value) == IS_ARRAY) { + ZVAL_COPY(©, value); + add_assoc_zval(&compares, "value", ©); + copied++; + } + + if (copied == 0) { + zval_ptr_dtor(&compares); + return; + } + add_assoc_bool(result, "block0_qkv_projection_numeric_compares_complete", copied == 3); + add_assoc_zval(result, "block0_qkv_projection_numeric_compares", &compares); +} + +static void king_inference_cuda_decoder_graph_executor_copy_qkv_projection_compare( + zval *result, + zval *linear_probe +) { + zval *compare = king_inference_array_find(linear_probe, "qkv_projection_numeric_compare"); + zval copy; + + if (compare == NULL) { + return; + } + + ZVAL_COPY(©, compare); + add_assoc_zval(result, "qkv_projection_numeric_compare", ©); +} diff --git a/extension/src/inference/cuda/decoder_graph/device/compare/rope/cuda_decoder_graph_executor_rope_position_compare.inc b/extension/src/inference/cuda/decoder_graph/device/compare/rope/cuda_decoder_graph_executor_rope_position_compare.inc new file mode 100644 index 000000000..451317206 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/device/compare/rope/cuda_decoder_graph_executor_rope_position_compare.inc @@ -0,0 +1,306 @@ +/* + * Position-focused RoPE numeric compare for native King GPU inference. + */ + +static void king_inference_cuda_decoder_graph_executor_store_rope_position_compare( + zval *rope_probe, + zend_ulong position, + zval *compare, + bool failed +) { + if (position == 0) { + add_assoc_bool(rope_probe, "position0_numeric_compare_failed", failed); + add_assoc_zval(rope_probe, "position0_numeric_compare", compare); + return; + } + if (position == 1) { + add_assoc_bool(rope_probe, "position1_numeric_compare_failed", failed); + add_assoc_zval(rope_probe, "position1_numeric_compare", compare); + return; + } + + add_assoc_bool(rope_probe, "position_numeric_compare_failed", failed); + add_assoc_zval(rope_probe, "position_numeric_compare", compare); +} + +static const char *king_inference_cuda_decoder_graph_executor_rope_position_compare_type( + zend_ulong position +) { + if (position == 0) { + return "gpu_decoder_rope_position0_numeric_compare"; + } + if (position == 1) { + return "gpu_decoder_rope_position1_numeric_compare"; + } + return "gpu_decoder_rope_position_numeric_compare"; +} + +static void king_inference_cuda_decoder_graph_executor_set_rope_position_compare_error( + king_inference_model_object *model, + zval *rope_probe, + zend_ulong position +) { + zval *compare; + zval *status; + zval *max_abs_diff; + zval *compared_values; + zval *matched_values; + zval *samples; + zval *sample; + zval *sample_index; + zval *sample_actual; + zval *sample_expected; + zval *sample_diff; + const char *compare_key = position == 0 + ? "position0_numeric_compare" + : (position == 1 ? "position1_numeric_compare" : "position_numeric_compare"); + const char *status_value = "unknown"; + double max_abs_diff_value = -1.0; + zend_long compared_value_count = -1; + zend_long matched_value_count = -1; + zend_long first_index = -1; + double first_actual = 0.0; + double first_expected = 0.0; + double first_diff = 0.0; + char message[512]; + + compare = rope_probe != NULL && Z_TYPE_P(rope_probe) == IS_ARRAY + ? king_inference_array_find(rope_probe, compare_key) + : NULL; + if (compare == NULL || Z_TYPE_P(compare) != IS_ARRAY) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_rope_position_compare_failed" + ); + return; + } + + status = king_inference_array_find(compare, "status"); + if (status != NULL && Z_TYPE_P(status) == IS_STRING) { + status_value = Z_STRVAL_P(status); + } + max_abs_diff = king_inference_array_find(compare, "max_abs_diff"); + if (max_abs_diff != NULL && Z_TYPE_P(max_abs_diff) == IS_DOUBLE) { + max_abs_diff_value = Z_DVAL_P(max_abs_diff); + } + compared_values = king_inference_array_find(compare, "compared_values"); + if (compared_values != NULL && Z_TYPE_P(compared_values) == IS_LONG) { + compared_value_count = Z_LVAL_P(compared_values); + } + matched_values = king_inference_array_find(compare, "matched_values"); + if (matched_values != NULL && Z_TYPE_P(matched_values) == IS_LONG) { + matched_value_count = Z_LVAL_P(matched_values); + } + samples = king_inference_array_find(compare, "samples"); + if (samples != NULL && Z_TYPE_P(samples) == IS_ARRAY) { + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(samples), sample) { + if (sample == NULL || Z_TYPE_P(sample) != IS_ARRAY) { + continue; + } + sample_diff = king_inference_array_find(sample, "abs_diff"); + if (sample_diff == NULL || Z_TYPE_P(sample_diff) != IS_DOUBLE || Z_DVAL_P(sample_diff) <= 0.00001) { + continue; + } + sample_index = king_inference_array_find(sample, "index"); + sample_actual = king_inference_array_find(sample, "actual"); + sample_expected = king_inference_array_find(sample, "expected"); + first_index = sample_index != NULL && Z_TYPE_P(sample_index) == IS_LONG ? Z_LVAL_P(sample_index) : -1; + first_actual = sample_actual != NULL && Z_TYPE_P(sample_actual) == IS_DOUBLE ? Z_DVAL_P(sample_actual) : 0.0; + first_expected = sample_expected != NULL && Z_TYPE_P(sample_expected) == IS_DOUBLE ? Z_DVAL_P(sample_expected) : 0.0; + first_diff = Z_DVAL_P(sample_diff); + break; + } ZEND_HASH_FOREACH_END(); + } + + snprintf( + message, + sizeof(message), + "cuda_decoder_graph_rope_position_compare_failed position=%lu status=%s compared=" ZEND_LONG_FMT " matched=" ZEND_LONG_FMT " max_abs_diff=%.9g first_mismatch_index=" ZEND_LONG_FMT " actual=%.9g expected=%.9g abs_diff=%.9g", + (unsigned long) position, + status_value, + compared_value_count, + matched_value_count, + max_abs_diff_value, + first_index, + first_actual, + first_expected, + first_diff + ); + king_inference_cuda_decoder_graph_executor_set_error(model, -1, message); +} + +static zend_result king_inference_cuda_decoder_graph_executor_compare_rope_position_reference( + king_inference_model_object *model, + king_inference_cuda_device_ptr slice_output, + king_inference_cuda_device_ptr rope_output, + zend_ulong length, + zend_ulong offset, + zend_ulong head_dim, + zend_ulong position, + double position_scale, + double rope_base, + zend_ulong pairing, + zval *rope_probe +) { + king_inference_cuda_memcpy_dtoh_fn cu_memcpy_dtoh; + zend_ulong compare_count; + zend_ulong input_count; + size_t readback_bytes; + size_t input_readback_bytes; + float *input; + float *expected; + double max_expected_abs = 0.0; + double tolerance = 0.00001; + zval compare; + zval *matched; + int result; + + if (position > 1 || !king_inference_cuda_device_vector_numeric_compare_enabled(&model->config)) { + return SUCCESS; + } + if (slice_output == 0 || rope_output == 0 || length == 0) { + array_init(&compare); + add_assoc_string(&compare, "type", king_inference_cuda_decoder_graph_executor_rope_position_compare_type(position)); + add_assoc_string(&compare, "status", "rope_position_compare_argument_invalid"); + add_assoc_bool(&compare, "matched", false); + king_inference_cuda_decoder_graph_executor_store_rope_position_compare(rope_probe, position, &compare, true); + return FAILURE; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + array_init(&compare); + add_assoc_string(&compare, "type", king_inference_cuda_decoder_graph_executor_rope_position_compare_type(position)); + add_assoc_string(&compare, "status", "cuda_context_current_failed"); + add_assoc_bool(&compare, "matched", false); + king_inference_cuda_decoder_graph_executor_store_rope_position_compare(rope_probe, position, &compare, true); + return FAILURE; + } + + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH_v2"); + if (cu_memcpy_dtoh == NULL) { + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH"); + } + if (cu_memcpy_dtoh == NULL) { + array_init(&compare); + add_assoc_string(&compare, "type", king_inference_cuda_decoder_graph_executor_rope_position_compare_type(position)); + add_assoc_string(&compare, "status", "cuMemcpyDtoH_unavailable"); + add_assoc_bool(&compare, "matched", false); + king_inference_cuda_decoder_graph_executor_store_rope_position_compare(rope_probe, position, &compare, true); + return FAILURE; + } + + compare_count = king_inference_cuda_device_vector_numeric_compare_max_values(&model->config); + compare_count = compare_count < length ? compare_count : length; + input_count = offset + head_dim; + if (input_count < compare_count) { + input_count = compare_count; + } + if (input_count > length) { + input_count = length; + } + readback_bytes = (size_t) compare_count * sizeof(float); + input_readback_bytes = (size_t) input_count * sizeof(float); + input = emalloc(input_readback_bytes); + expected = emalloc(readback_bytes); + result = cu_memcpy_dtoh(input, slice_output, input_readback_bytes); + if (result != 0) { + efree(input); + efree(expected); + array_init(&compare); + add_assoc_string(&compare, "type", king_inference_cuda_decoder_graph_executor_rope_position_compare_type(position)); + add_assoc_string(&compare, "status", "cuMemcpyDtoH_input_failed"); + add_assoc_long(&compare, "result", result); + add_assoc_bool(&compare, "matched", false); + king_inference_cuda_decoder_graph_executor_store_rope_position_compare(rope_probe, position, &compare, true); + return FAILURE; + } + + for (zend_ulong i = 0; i < compare_count; i++) { + expected[i] = input[i]; + } + for (zend_ulong pair = 0; pair < head_dim / 2; pair++) { + zend_ulong half = head_dim / 2; + zend_ulong even = pairing == 1 ? offset + pair : offset + pair * 2; + zend_ulong odd = pairing == 1 ? offset + half + pair : even + 1; + double exponent; + double inv_freq; + double angle; + double sin_value; + double cos_value; + double x0; + double x1; + + if (even >= input_count || odd >= input_count) { + continue; + } + exponent = ((double) (pair * 2)) / (double) head_dim; + inv_freq = exp(-log(rope_base) * exponent); + angle = (double) position * position_scale * inv_freq; + sin_value = sin(angle); + cos_value = cos(angle); + x0 = (double) input[even]; + x1 = (double) input[odd]; + if (even < compare_count) { + expected[even] = (float) (x0 * cos_value - x1 * sin_value); + } + if (odd < compare_count) { + expected[odd] = (float) (x0 * sin_value + x1 * cos_value); + } + } + for (zend_ulong i = 0; i < compare_count; i++) { + double abs_expected = fabs((double) expected[i]); + + if (abs_expected > max_expected_abs) { + max_expected_abs = abs_expected; + } + } + tolerance = fmax(tolerance, max_expected_abs * 0.0000015); + if (tolerance > 0.0005) { + tolerance = 0.0005; + } + efree(input); + + if (king_inference_cuda_device_vector_numeric_compare( + model, + position == 0 ? "rope_position0_identity" : "rope_position1_cpu_reference", + "rope", + 0, + "rope_head_slice", + offset, + rope_output, + length, + expected, + compare_count, + tolerance, + &compare + ) != SUCCESS) { + add_assoc_string(&compare, "type", king_inference_cuda_decoder_graph_executor_rope_position_compare_type(position)); + add_assoc_long(&compare, "offset", (zend_long) offset); + add_assoc_long(&compare, "head_dim", (zend_long) head_dim); + add_assoc_long(&compare, "position", (zend_long) position); + add_assoc_double(&compare, "position_scale", position_scale); + add_assoc_double(&compare, "rope_base", rope_base); + add_assoc_double(&compare, "max_expected_abs", max_expected_abs); + king_inference_cuda_decoder_graph_executor_store_rope_position_compare(rope_probe, position, &compare, true); + efree(expected); + return FAILURE; + } + + add_assoc_string(&compare, "type", king_inference_cuda_decoder_graph_executor_rope_position_compare_type(position)); + add_assoc_string(&compare, "reference", position == 0 ? "position0_identity" : "cpu_rope_rotation_formula"); + add_assoc_long(&compare, "offset", (zend_long) offset); + add_assoc_long(&compare, "head_dim", (zend_long) head_dim); + add_assoc_long(&compare, "position", (zend_long) position); + add_assoc_double(&compare, "position_scale", position_scale); + add_assoc_double(&compare, "rope_base", rope_base); + add_assoc_double(&compare, "max_expected_abs", max_expected_abs); + matched = king_inference_array_find(&compare, "matched"); + king_inference_cuda_decoder_graph_executor_store_rope_position_compare( + rope_probe, + position, + &compare, + matched == NULL || !zend_is_true(matched) + ); + efree(expected); + return matched != NULL && zend_is_true(matched) ? SUCCESS : FAILURE; +} diff --git a/extension/src/inference/cuda/decoder_graph/device/core/cuda_decoder_graph_executor_device_helpers.inc b/extension/src/inference/cuda/decoder_graph/device/core/cuda_decoder_graph_executor_device_helpers.inc new file mode 100644 index 000000000..ce199cac0 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/device/core/cuda_decoder_graph_executor_device_helpers.inc @@ -0,0 +1,589 @@ +/* + * CUDA decoder graph executor device-op helpers. + */ + +static bool king_inference_cuda_decoder_graph_executor_device_op(zend_string *op_name) +{ + return zend_string_equals_literal(op_name, "embedding") + || zend_string_equals_literal(op_name, "rms_norm") + || zend_string_equals_literal(op_name, "linear") + || zend_string_equals_literal(op_name, "add") + || zend_string_equals_literal(op_name, "scale") + || zend_string_equals_literal(op_name, "mul") + || zend_string_equals_literal(op_name, "silu") + || zend_string_equals_literal(op_name, "gelu_tanh") + || zend_string_equals_literal(op_name, "slice") + || zend_string_equals_literal(op_name, "rope") + || zend_string_equals_literal(op_name, "kv_write") + || zend_string_equals_literal(op_name, "kv_attention") + || zend_string_equals_literal(op_name, "stack"); +} + +static bool king_inference_cuda_decoder_graph_executor_host_sampling_op(zend_string *op_name) +{ + return zend_string_equals_literal(op_name, "argmax_token") + || zend_string_equals_literal(op_name, "sample_token"); +} + +static bool king_inference_cuda_decoder_graph_executor_zval_string_equals( + zval *value, + zend_string *expected +) { + return value != NULL + && expected != NULL + && Z_TYPE_P(value) == IS_STRING + && zend_string_equals(Z_STR_P(value), expected); +} + +static zval *king_inference_cuda_decoder_graph_executor_ops(zval *graph) +{ + zval *ops = king_inference_array_find(graph, "ops"); + + if (ops == NULL) { + ops = king_inference_array_find(graph, "steps"); + } + if (ops == NULL || Z_TYPE_P(ops) != IS_ARRAY) { + return NULL; + } + return ops; +} + +static bool king_inference_cuda_decoder_graph_executor_op_index_ready(zval *graph) +{ + zval *ready = king_inference_array_find(graph, "_cuda_decode_op_index_ready"); + + return ready != NULL && zend_is_true(ready); +} + +static bool king_inference_cuda_decoder_graph_executor_array_index_ready(zval *graph, const char *key) +{ + zval *index = king_inference_array_find(graph, key); + + return king_inference_cuda_decoder_graph_executor_op_index_ready(graph) + && index != NULL + && Z_TYPE_P(index) == IS_ARRAY; +} + +static void king_inference_cuda_decoder_graph_executor_profile_increment(zval *graph, const char *key) +{ + zval *value; + + if (graph == NULL || Z_TYPE_P(graph) != IS_ARRAY || key == NULL) { + return; + } + value = king_inference_array_find(graph, key); + if (value != NULL && Z_TYPE_P(value) == IS_LONG) { + ZVAL_LONG(value, Z_LVAL_P(value) + 1); + return; + } + add_assoc_long(graph, key, 1); +} + +static zend_string *king_inference_cuda_decoder_graph_executor_input_index_key( + zend_string *input_id, + const char *name +) { + return strpprintf(0, "%s\x1f%s", name, ZSTR_VAL(input_id)); +} + +static zend_string *king_inference_cuda_decoder_graph_executor_binary_index_key( + const char *name, + zend_string *left_id, + zend_string *right_id +) { + zend_string *first = zend_binary_strcmp( + ZSTR_VAL(left_id), + ZSTR_LEN(left_id), + ZSTR_VAL(right_id), + ZSTR_LEN(right_id) + ) <= 0 ? left_id : right_id; + zend_string *second = first == left_id ? right_id : left_id; + + return strpprintf(0, "%s\x1f%s\x1f%s", name, ZSTR_VAL(first), ZSTR_VAL(second)); +} + +static void king_inference_cuda_decoder_graph_executor_index_append( + zval *index, + zend_string *key, + zend_ulong op_index +) { + zval *bucket = zend_hash_find(Z_ARRVAL_P(index), key); + + if (bucket == NULL) { + zval list; + + array_init(&list); + add_next_index_long(&list, (zend_long) op_index); + zend_hash_update(Z_ARRVAL_P(index), key, &list); + return; + } + if (Z_TYPE_P(bucket) == IS_ARRAY) { + add_next_index_long(bucket, (zend_long) op_index); + } +} + +static zend_result king_inference_cuda_decoder_graph_executor_prepare_op_index( + king_inference_model_object *model, + zval *graph +) { + zval *ops = king_inference_cuda_decoder_graph_executor_ops(graph); + zval first_index; + zval input_index; + zval binary_index; + zval *entry; + zend_ulong numeric_index; + zend_string *string_index; + + if (king_inference_cuda_decoder_graph_executor_op_index_ready(graph)) { + return SUCCESS; + } + if (ops == NULL) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_op_index_ops_missing"); + return FAILURE; + } + + array_init(&first_index); + array_init(&input_index); + array_init(&binary_index); + ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(ops), numeric_index, string_index, entry) { + zval *op_name; + zval *input; + zval *left; + zval *right; + zval op_index_value; + zend_string *index_key; + + if (string_index != NULL + || numeric_index > (zend_ulong) ZEND_LONG_MAX + || entry == NULL + || Z_TYPE_P(entry) != IS_ARRAY) { + continue; + } + op_name = king_inference_array_find(entry, "op"); + if (op_name == NULL || Z_TYPE_P(op_name) != IS_STRING) { + continue; + } + if (!zend_hash_exists(Z_ARRVAL(first_index), Z_STR_P(op_name))) { + ZVAL_LONG(&op_index_value, (zend_long) numeric_index); + zend_hash_update(Z_ARRVAL(first_index), Z_STR_P(op_name), &op_index_value); + } + input = king_inference_array_find(entry, "input"); + if (input != NULL && Z_TYPE_P(input) == IS_STRING) { + index_key = king_inference_cuda_decoder_graph_executor_input_index_key( + Z_STR_P(input), + ZSTR_VAL(Z_STR_P(op_name)) + ); + king_inference_cuda_decoder_graph_executor_index_append(&input_index, index_key, numeric_index); + zend_string_release(index_key); + } + left = king_inference_array_find(entry, "left"); + right = king_inference_array_find(entry, "right"); + if (left != NULL + && Z_TYPE_P(left) == IS_STRING + && right != NULL + && Z_TYPE_P(right) == IS_STRING + && (zend_string_equals_literal(Z_STR_P(op_name), "add") + || zend_string_equals_literal(Z_STR_P(op_name), "mul"))) { + index_key = king_inference_cuda_decoder_graph_executor_binary_index_key( + ZSTR_VAL(Z_STR_P(op_name)), + Z_STR_P(left), + Z_STR_P(right) + ); + king_inference_cuda_decoder_graph_executor_index_append(&binary_index, index_key, numeric_index); + zend_string_release(index_key); + } + } ZEND_HASH_FOREACH_END(); + + add_assoc_zval(graph, "_cuda_first_op_index", &first_index); + add_assoc_zval(graph, "_cuda_input_op_index", &input_index); + add_assoc_zval(graph, "_cuda_binary_op_index", &binary_index); + add_assoc_bool(graph, "_cuda_decode_op_index_ready", true); + return SUCCESS; +} + +static zval *king_inference_cuda_decoder_graph_executor_indexed_op( + zval *graph, + zend_ulong op_index +) { + zval *ops = king_inference_cuda_decoder_graph_executor_ops(graph); + + return ops != NULL ? zend_hash_index_find(Z_ARRVAL_P(ops), op_index) : NULL; +} + +static zval *king_inference_cuda_decoder_graph_executor_indexed_nth_op_for_input( + zval *graph, + zend_string *input_id, + const char *name, + zend_ulong ordinal +) { + zval *index = king_inference_array_find(graph, "_cuda_input_op_index"); + zval *bucket; + zval *op_index; + zend_string *index_key; + zval *entry; + + if (!king_inference_cuda_decoder_graph_executor_op_index_ready(graph) + || index == NULL + || Z_TYPE_P(index) != IS_ARRAY + || input_id == NULL + || name == NULL) { + return NULL; + } + king_inference_cuda_decoder_graph_executor_profile_increment(graph, "_cuda_profile_op_index_lookups"); + index_key = king_inference_cuda_decoder_graph_executor_input_index_key(input_id, name); + bucket = zend_hash_find(Z_ARRVAL_P(index), index_key); + zend_string_release(index_key); + if (bucket == NULL || Z_TYPE_P(bucket) != IS_ARRAY) { + return NULL; + } + op_index = zend_hash_index_find(Z_ARRVAL_P(bucket), ordinal); + if (op_index == NULL || Z_TYPE_P(op_index) != IS_LONG || Z_LVAL_P(op_index) < 0) { + return NULL; + } + entry = king_inference_cuda_decoder_graph_executor_indexed_op(graph, (zend_ulong) Z_LVAL_P(op_index)); + if (entry != NULL && Z_TYPE_P(entry) == IS_ARRAY) { + king_inference_cuda_decoder_graph_executor_profile_increment(graph, "_cuda_profile_op_index_hits"); + } + return entry != NULL && Z_TYPE_P(entry) == IS_ARRAY ? entry : NULL; +} + +#include "cuda_decoder_graph_executor_execution_plan.inc" + +static zval *king_inference_cuda_decoder_graph_executor_first_op( + zval *graph, + const char *name +) { + zval *ops = king_inference_cuda_decoder_graph_executor_ops(graph); + zval *index = king_inference_array_find(graph, "_cuda_first_op_index"); + zval *indexed; + zval *entry; + size_t name_length = strlen(name); + + if (ops == NULL) { + return NULL; + } + if (king_inference_cuda_decoder_graph_executor_op_index_ready(graph) + && index != NULL + && Z_TYPE_P(index) == IS_ARRAY) { + king_inference_cuda_decoder_graph_executor_profile_increment(graph, "_cuda_profile_op_index_lookups"); + indexed = zend_hash_str_find(Z_ARRVAL_P(index), name, name_length); + if (indexed != NULL && Z_TYPE_P(indexed) == IS_LONG && Z_LVAL_P(indexed) >= 0) { + entry = king_inference_cuda_decoder_graph_executor_indexed_op(graph, (zend_ulong) Z_LVAL_P(indexed)); + if (entry != NULL && Z_TYPE_P(entry) == IS_ARRAY) { + king_inference_cuda_decoder_graph_executor_profile_increment(graph, "_cuda_profile_op_index_hits"); + } + return entry != NULL && Z_TYPE_P(entry) == IS_ARRAY ? entry : NULL; + } + return NULL; + } + + king_inference_cuda_decoder_graph_executor_profile_increment(graph, "_cuda_profile_op_index_fallback_scans"); + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(ops), entry) { + zval *op_name; + + if (entry == NULL || Z_TYPE_P(entry) != IS_ARRAY) { + continue; + } + op_name = king_inference_array_find(entry, "op"); + if (op_name != NULL + && Z_TYPE_P(op_name) == IS_STRING + && Z_STRLEN_P(op_name) == name_length + && memcmp(Z_STRVAL_P(op_name), name, name_length) == 0) { + return entry; + } + } ZEND_HASH_FOREACH_END(); + + return NULL; +} + +static zval *king_inference_cuda_decoder_graph_executor_op_for_input( + zval *graph, + zend_string *input_id, + const char *name +) { + zval *ops = king_inference_cuda_decoder_graph_executor_ops(graph); + zval *entry; + size_t name_length = name != NULL ? strlen(name) : 0; + + if (ops == NULL || input_id == NULL || name_length == 0) { + return NULL; + } + entry = king_inference_cuda_decoder_graph_executor_indexed_nth_op_for_input(graph, input_id, name, 0); + if (entry != NULL) { + return entry; + } + if (king_inference_cuda_decoder_graph_executor_array_index_ready(graph, "_cuda_input_op_index")) { + return NULL; + } + + king_inference_cuda_decoder_graph_executor_profile_increment(graph, "_cuda_profile_op_index_fallback_scans"); + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(ops), entry) { + zval *op_name; + zval *input; + + if (entry == NULL || Z_TYPE_P(entry) != IS_ARRAY) { + continue; + } + op_name = king_inference_array_find(entry, "op"); + input = king_inference_array_find(entry, "input"); + if (op_name != NULL + && Z_TYPE_P(op_name) == IS_STRING + && Z_STRLEN_P(op_name) == name_length + && memcmp(Z_STRVAL_P(op_name), name, name_length) == 0 + && king_inference_cuda_decoder_graph_executor_zval_string_equals(input, input_id)) { + return entry; + } + } ZEND_HASH_FOREACH_END(); + + return NULL; +} + +static zval *king_inference_cuda_decoder_graph_executor_nth_op_for_input( + zval *graph, + zend_string *input_id, + const char *name, + zend_ulong ordinal +) { + zval *ops = king_inference_cuda_decoder_graph_executor_ops(graph); + zval *entry; + zend_ulong matched = 0; + size_t name_length = name != NULL ? strlen(name) : 0; + + if (ops == NULL || input_id == NULL || name_length == 0) { + return NULL; + } + entry = king_inference_cuda_decoder_graph_executor_indexed_nth_op_for_input( + graph, + input_id, + name, + ordinal + ); + if (entry != NULL) { + return entry; + } + if (king_inference_cuda_decoder_graph_executor_array_index_ready(graph, "_cuda_input_op_index")) { + return NULL; + } + + king_inference_cuda_decoder_graph_executor_profile_increment(graph, "_cuda_profile_op_index_fallback_scans"); + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(ops), entry) { + zval *op_name; + zval *input; + + if (entry == NULL || Z_TYPE_P(entry) != IS_ARRAY) { + continue; + } + op_name = king_inference_array_find(entry, "op"); + input = king_inference_array_find(entry, "input"); + if (op_name != NULL + && Z_TYPE_P(op_name) == IS_STRING + && Z_STRLEN_P(op_name) == name_length + && memcmp(Z_STRVAL_P(op_name), name, name_length) == 0 + && king_inference_cuda_decoder_graph_executor_zval_string_equals(input, input_id)) { + if (matched == ordinal) { + return entry; + } + matched++; + } + } ZEND_HASH_FOREACH_END(); + + return NULL; +} + +static zval *king_inference_cuda_decoder_graph_executor_binary_op_for_inputs( + zval *graph, + zend_string *left_id, + zend_string *right_id, + const char *name +) { + zval *ops = king_inference_cuda_decoder_graph_executor_ops(graph); + zval *index = king_inference_array_find(graph, "_cuda_binary_op_index"); + zval *bucket; + zval *op_index; + zval *entry; + zend_string *index_key; + + if (ops == NULL || left_id == NULL || right_id == NULL || name == NULL) { + return NULL; + } + if (king_inference_cuda_decoder_graph_executor_op_index_ready(graph) + && index != NULL + && Z_TYPE_P(index) == IS_ARRAY) { + king_inference_cuda_decoder_graph_executor_profile_increment(graph, "_cuda_profile_op_index_lookups"); + index_key = king_inference_cuda_decoder_graph_executor_binary_index_key(name, left_id, right_id); + bucket = zend_hash_find(Z_ARRVAL_P(index), index_key); + zend_string_release(index_key); + if (bucket != NULL && Z_TYPE_P(bucket) == IS_ARRAY) { + op_index = zend_hash_index_find(Z_ARRVAL_P(bucket), 0); + if (op_index != NULL && Z_TYPE_P(op_index) == IS_LONG && Z_LVAL_P(op_index) >= 0) { + entry = king_inference_cuda_decoder_graph_executor_indexed_op( + graph, + (zend_ulong) Z_LVAL_P(op_index) + ); + if (entry != NULL && Z_TYPE_P(entry) == IS_ARRAY) { + king_inference_cuda_decoder_graph_executor_profile_increment(graph, "_cuda_profile_op_index_hits"); + return entry; + } + } + } + return NULL; + } + + king_inference_cuda_decoder_graph_executor_profile_increment(graph, "_cuda_profile_op_index_fallback_scans"); + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(ops), entry) { + zval *op_name; + zval *left; + zval *right; + + if (entry == NULL || Z_TYPE_P(entry) != IS_ARRAY) { + continue; + } + op_name = king_inference_array_find(entry, "op"); + left = king_inference_array_find(entry, "left"); + right = king_inference_array_find(entry, "right"); + if (op_name != NULL + && Z_TYPE_P(op_name) == IS_STRING + && ZSTR_LEN(Z_STR_P(op_name)) == strlen(name) + && memcmp(ZSTR_VAL(Z_STR_P(op_name)), name, strlen(name)) == 0 + && ((king_inference_cuda_decoder_graph_executor_zval_string_equals(left, left_id) + && king_inference_cuda_decoder_graph_executor_zval_string_equals(right, right_id)) + || (king_inference_cuda_decoder_graph_executor_zval_string_equals(left, right_id) + && king_inference_cuda_decoder_graph_executor_zval_string_equals(right, left_id)))) { + return entry; + } + } ZEND_HASH_FOREACH_END(); + + return NULL; +} + +static zval *king_inference_cuda_decoder_graph_executor_add_op_for_inputs( + zval *graph, + zend_string *left_id, + zend_string *right_id +) { + return king_inference_cuda_decoder_graph_executor_binary_op_for_inputs(graph, left_id, right_id, "add"); +} + +static zval *king_inference_cuda_decoder_graph_executor_mul_op_for_inputs( + zval *graph, + zend_string *left_id, + zend_string *right_id +) { + return king_inference_cuda_decoder_graph_executor_binary_op_for_inputs(graph, left_id, right_id, "mul"); +} + +static zend_result king_inference_cuda_decoder_graph_executor_linear_shape( + king_inference_model_object *model, + zval *op, + zend_ulong input_width, + zend_string **weight_name, + zend_ulong *rows +) { + zval *descriptor; + zval *dimensions; + zend_ulong rank; + zend_ulong type; + zend_ulong cols; + zend_ulong tensor_rows; + zend_ulong row_offset = king_inference_tensor_option_ulong(op, "row_offset", 0); + zend_ulong row_limit; + + *weight_name = NULL; + *rows = 0; + if (king_inference_graph_required_string(op, "weight", weight_name) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_linear_weight_invalid"); + return FAILURE; + } + + model->cuda_decoder_graph_executor_tensor_lookup_count++; + descriptor = king_inference_tensor_index_descriptor(model, *weight_name); + dimensions = descriptor != NULL + ? zend_hash_str_find(Z_ARRVAL_P(descriptor), "dimensions", sizeof("dimensions") - 1) + : NULL; + if (descriptor == NULL || dimensions == NULL || Z_TYPE_P(dimensions) != IS_ARRAY) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_linear_quantized_shape_missing"); + return FAILURE; + } + if (!king_inference_tensor_descriptor_ulong(descriptor, "rank", sizeof("rank") - 1, &rank) + || !king_inference_tensor_descriptor_ulong(descriptor, "type", sizeof("type") - 1, &type) + || king_inference_tensor_dimension_at(dimensions, 0, &cols) != SUCCESS + || king_inference_tensor_dimension_at(dimensions, 1, &tensor_rows) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_linear_descriptor_invalid"); + return FAILURE; + } + if (rank != 2 + || !king_inference_cuda_quantized_matvec_type_supported(type) + || cols != input_width + || tensor_rows == 0 + || tensor_rows > UINT_MAX) { + char error[256]; + + snprintf( + error, + sizeof(error), + "cuda_decoder_graph_linear_quantized_shape_invalid weight=%s input_width=%lu cols=%lu rows=%lu rank=%lu type=%lu", + *weight_name != NULL ? ZSTR_VAL(*weight_name) : "", + (unsigned long) input_width, + (unsigned long) cols, + (unsigned long) tensor_rows, + (unsigned long) rank, + (unsigned long) type + ); + king_inference_cuda_decoder_graph_executor_set_error(model, -1, error); + return FAILURE; + } + + if (row_offset != 0) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_linear_row_offset_unsupported"); + return FAILURE; + } + + row_limit = king_inference_tensor_option_ulong(op, "row_limit", tensor_rows); + if (row_limit == 0 || row_limit > tensor_rows) { + row_limit = tensor_rows; + } + *rows = row_limit; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_slice_shape( + king_inference_model_object *model, + zval *op, + zend_ulong input_length, + zend_ulong *offset, + zend_ulong *length +) { + zend_ulong slice_offset = king_inference_tensor_option_ulong(op, "offset", 0); + zend_ulong slice_length = king_inference_tensor_option_ulong(op, "count", 0); + + if (slice_length == 0) { + slice_length = input_length > slice_offset ? input_length - slice_offset : 0; + } + if (input_length == 0 + || slice_offset > input_length + || slice_length == 0 + || slice_length > input_length - slice_offset + || slice_offset > UINT_MAX + || slice_length > UINT_MAX + || slice_length > (zend_ulong) (SIZE_MAX / sizeof(float))) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_slice_shape_invalid"); + return FAILURE; + } + + *offset = slice_offset; + *length = slice_length; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_release_device_ptr( + king_inference_model_object *model, + king_inference_cuda_device_ptr *device_ptr +) { + if (device_ptr != NULL && *device_ptr != 0) { + if (king_inference_cuda_device_allocator_free(model, *device_ptr) != SUCCESS) { + return FAILURE; + } + *device_ptr = 0; + } + return SUCCESS; +} diff --git a/extension/src/inference/cuda/decoder_graph/device/core/cuda_decoder_graph_executor_device_ops.inc b/extension/src/inference/cuda/decoder_graph/device/core/cuda_decoder_graph_executor_device_ops.inc new file mode 100644 index 000000000..2a399b668 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/device/core/cuda_decoder_graph_executor_device_ops.inc @@ -0,0 +1,806 @@ +#include "cuda_decoder_graph_executor_device_helpers.inc" +#include "../../ops/rope/cuda_decoder_graph_executor_rope_ops.inc" +#include "../../ops/kv/cuda_decoder_graph_executor_kv_slice_lookup.inc" +#include "../../ops/kv/cuda_decoder_graph_executor_kv_ops.inc" +#include "../../ops/projection/cuda_decoder_graph_executor_query_ops.inc" +#include "../../ops/projection/cuda_decoder_graph_executor_projection_ops.inc" +#include "../../ops/norm/cuda_decoder_graph_executor_norm_ops.inc" +#include "../../ops/final/cuda_decoder_graph_executor_final_ops.inc" +#include "../../ops/ffn/cuda_decoder_graph_executor_ffn_ops.inc" +#include "../../ops/control/cuda_decoder_graph_executor_block_loop_ops.inc" +#include "../../ops/control/cuda_decoder_graph_executor_probe_ops.inc" +#include "../../prefill/ffn/cuda_decoder_graph_executor_prefilled_ffn_ops.inc" +#include "../../prefill/residual/cuda_decoder_graph_executor_prefilled_residual_ops.inc" +#include "../compare/embedding/cuda_decoder_graph_executor_embedding_compare.inc" +#include "../compare/norm/cuda_decoder_graph_executor_first_rms_norm_compare.inc" +#include "../compare/matvec/cuda_decoder_graph_executor_q4_matvec_compare.inc" + +static zend_result king_inference_cuda_decoder_graph_executor_execute_initial_device_ops( + king_inference_model_object *model, + zval *graph, + zval *result +) { + zval *embedding_op = king_inference_cuda_decoder_graph_executor_first_op(graph, "embedding"); + zval *rms_norm_op = NULL; + zval *token_value; + zval *prefill_batch_index_value; + zval embedding_probe; + zval rms_norm_probe; + zval linear_probe; + zval slice_probe; + zval rope_probe; + zval kv_head_prepare_probe; + zval attention_projection_probe; + zval attention_residual_probe; + zval ffn_norm_probe; + zval ffn_gate_up_probe; + zval ffn_swiglu_probe; + zval ffn_down_probe; + zval ffn_output_probe; + zval final_norm_probe; + zval logits_probe; + zend_string *embedding_id = NULL; + zend_string *rms_norm_id = NULL; + zend_string *rms_weight = NULL; + zend_string *linear_id = NULL; + zend_string *linear_weight = NULL; + zend_string *attention_stack_id = NULL; + zend_ulong width = model->gguf.architecture_params[KING_INFERENCE_ARCH_EMBEDDING_LENGTH]; + zend_ulong linear_rows = 0; + zend_ulong slice_length = 0; + zend_ulong token_id; + zend_ulong prefill_batch_index = 0; + size_t bytes; + king_inference_cuda_device_ptr embedding_output = 0; + king_inference_cuda_device_ptr rms_norm_output = 0; + king_inference_cuda_device_ptr linear_output = 0; + king_inference_cuda_device_ptr attention_stack_output = 0; + king_inference_cuda_device_ptr next_hidden_output = 0; + zend_string *next_hidden_id = NULL; + zend_ulong attention_stack_width = 0; + zend_ulong next_hidden_width = 0; + zend_ulong executed_device_ops = 0; + bool rope_executed = false; + bool kv_head_prepared = false; + bool attention_projection_probe_initialized = false; + bool attention_residual_probe_initialized = false; + bool ffn_norm_probe_initialized = false; + bool ffn_gate_up_probe_initialized = false; + bool ffn_swiglu_probe_initialized = false, ffn_down_probe_initialized = false, ffn_output_probe_initialized = false, final_norm_probe_initialized = false, logits_probe_initialized = false; + bool attention_projection_executed = false; + bool attention_residual_executed = false; + bool ffn_norm_executed = false; + bool ffn_gate_up_executed = false; + bool ffn_swiglu_executed = false, ffn_down_executed = false, ffn_output_executed = false, final_norm_executed = false, logits_executed = false; + bool use_prefill_embedding = false; + bool used_prefill_embedding = false; + bool used_prefill_initial_norm = false; + bool used_prefill_initial_linear = false; + bool used_prefill_attention_residual = false; + double epsilon = 0.000001; + double embedding_scale = 1.0; + + array_init(&embedding_probe); + add_assoc_string(&embedding_probe, "type", "gpu_decoder_embedding_device_execution"); + add_assoc_bool(&embedding_probe, "attempted", embedding_op != NULL); + add_assoc_bool(&embedding_probe, "available", model->cuda_decoder_graph_executor_embedding_execution_available); + add_assoc_bool(&embedding_probe, "executed", false); + add_assoc_bool(&embedding_probe, "prefill_copy_available", model->cuda_decoder_prompt_prefill_embeddings_active); + add_assoc_bool(&embedding_probe, "retained_device_output", false); + array_init(&rms_norm_probe); + add_assoc_string(&rms_norm_probe, "type", "gpu_decoder_rms_norm_device_execution"); + add_assoc_bool(&rms_norm_probe, "attempted", false); + add_assoc_bool(&rms_norm_probe, "available", model->cuda_decoder_graph_executor_rms_norm_execution_available); + add_assoc_bool(&rms_norm_probe, "executed", false); + add_assoc_bool(&rms_norm_probe, "retained_device_output", false); + array_init(&linear_probe); + add_assoc_string(&linear_probe, "type", "gpu_decoder_linear_device_execution"); + add_assoc_bool(&linear_probe, "attempted", false); + add_assoc_bool(&linear_probe, "available", model->cuda_decoder_graph_executor_linear_execution_available); + add_assoc_bool(&linear_probe, "executed", false); + add_assoc_bool(&linear_probe, "retained_device_output", false); + array_init(&slice_probe); + add_assoc_string(&slice_probe, "type", "gpu_decoder_slice_device_execution"); + add_assoc_bool(&slice_probe, "attempted", false); + add_assoc_bool(&slice_probe, "available", model->cuda_decoder_graph_executor_slice_execution_available); + add_assoc_bool(&slice_probe, "executed", false); + add_assoc_bool(&slice_probe, "retained_device_output", false); + array_init(&rope_probe); + add_assoc_string(&rope_probe, "type", "gpu_decoder_rope_device_execution"); + add_assoc_bool(&rope_probe, "attempted", false); + add_assoc_bool(&rope_probe, "available", model->cuda_decoder_graph_executor_rope_execution_available); + add_assoc_bool(&rope_probe, "executed", false); + add_assoc_bool(&rope_probe, "retained_device_output", false); + array_init(&kv_head_prepare_probe); + add_assoc_string(&kv_head_prepare_probe, "type", "gpu_decoder_kv_head_prepare_device_execution"); + add_assoc_bool(&kv_head_prepare_probe, "attempted", false); + add_assoc_bool(&kv_head_prepare_probe, "available", model->cuda_decoder_graph_executor_kv_head_prepare_execution_available); + add_assoc_bool(&kv_head_prepare_probe, "executed", false); + add_assoc_bool(&kv_head_prepare_probe, "retained_device_output", false); + + if (embedding_op == NULL) { + king_inference_cuda_decoder_graph_executor_add_empty_initial_result(result, &embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + return SUCCESS; + } + if (!model->cuda_decoder_graph_executor_embedding_execution_available) { + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_embedding_execution_unavailable" + ); + return FAILURE; + } + if (king_inference_graph_required_string(embedding_op, "id", &embedding_id) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_embedding_id_invalid"); + return FAILURE; + } + if (king_inference_graph_finite_double_option(embedding_op, "scale", 1.0, &embedding_scale) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_embedding_scale_invalid"); + return FAILURE; + } + + token_value = king_inference_array_find(embedding_op, "token_id"); + if (token_value == NULL) { + token_value = king_inference_array_find(embedding_op, "token"); + } + if (token_value == NULL || Z_TYPE_P(token_value) != IS_LONG || Z_LVAL_P(token_value) < 0) { + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_embedding_token_invalid" + ); + return FAILURE; + } + prefill_batch_index_value = king_inference_array_find(embedding_op, "prefill_batch_index"); + if (prefill_batch_index_value != NULL) { + if (Z_TYPE_P(prefill_batch_index_value) != IS_LONG || Z_LVAL_P(prefill_batch_index_value) < 0) { + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_embedding_prefill_batch_index_invalid" + ); + return FAILURE; + } + prefill_batch_index = (zend_ulong) Z_LVAL_P(prefill_batch_index_value); + use_prefill_embedding = true; + } + if (width == 0 || width > (zend_ulong) (SIZE_MAX / sizeof(float))) { + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_embedding_width_invalid" + ); + return FAILURE; + } + + token_id = (zend_ulong) Z_LVAL_P(token_value); + bytes = (size_t) width * sizeof(float); + if (king_inference_cuda_device_allocator_alloc(model, bytes, &embedding_output) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_embedding_allocation_failed" + ); + return FAILURE; + } + + if (use_prefill_embedding) { + if (king_inference_cuda_embedding_prefill_copy_row( + model, + prefill_batch_index, + embedding_output, + width + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &embedding_output); + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_embedding_row_result, + model->cuda_embedding_row_error[0] != '\0' + ? model->cuda_embedding_row_error + : "cuda_decoder_graph_embedding_prefill_copy_failed" + ); + return FAILURE; + } + used_prefill_embedding = true; + } else if (king_inference_cuda_embedding_row_load(model, token_id, embedding_output, width, embedding_scale) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &embedding_output); + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_embedding_row_result, + model->cuda_embedding_row_error[0] != '\0' + ? model->cuda_embedding_row_error + : "cuda_decoder_graph_embedding_execution_failed" + ); + return FAILURE; + } + + model->cuda_decoder_graph_executor_embedding_execution_count++; + model->cuda_decoder_graph_executor_last_embedding_width = width; + model->cuda_decoder_graph_executor_last_embedding_token_id = token_id; + model->cuda_decoder_graph_executor_last_embedding_token_available = true; + executed_device_ops++; + + add_assoc_bool(&embedding_probe, "executed", true); + add_assoc_bool(&embedding_probe, "used_prefill_embedding", used_prefill_embedding); + add_assoc_long(&embedding_probe, "token_id", (zend_long) token_id); + if (used_prefill_embedding) { + add_assoc_long(&embedding_probe, "prefill_batch_index", (zend_long) prefill_batch_index); + } + add_assoc_long(&embedding_probe, "width", (zend_long) width); + add_assoc_long(&embedding_probe, "bytes", (zend_long) bytes); + add_assoc_double(&embedding_probe, "scale", embedding_scale); + if (!used_prefill_embedding && king_inference_cuda_decoder_graph_executor_compare_embedding_row(model, token_id, embedding_output, width, embedding_scale, &embedding_probe) != SUCCESS) { + add_assoc_bool(&embedding_probe, "numeric_compare_failed", true); + } + + rms_norm_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, embedding_id, "rms_norm"); + add_assoc_bool(&rms_norm_probe, "attempted", rms_norm_op != NULL); + if (rms_norm_op != NULL) { + zval *linear_op = NULL; + + if (!model->cuda_decoder_graph_executor_rms_norm_execution_available) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &embedding_output); + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_rms_norm_execution_unavailable" + ); + return FAILURE; + } + if (king_inference_graph_required_string(rms_norm_op, "id", &rms_norm_id) != SUCCESS + || king_inference_graph_required_string(rms_norm_op, "weight", &rms_weight) != SUCCESS + || king_inference_graph_finite_double_option(rms_norm_op, "epsilon", 0.000001, &epsilon) != SUCCESS + || epsilon <= 0.0) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &embedding_output); + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_rms_norm_contract_invalid"); + return FAILURE; + } + if (king_inference_cuda_device_allocator_alloc(model, bytes, &rms_norm_output) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &embedding_output); + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_rms_norm_allocation_failed" + ); + return FAILURE; + } + if (use_prefill_embedding && model->cuda_decoder_prompt_prefill_initial_norm != 0) { + if (king_inference_cuda_prefill_copy_initial_norm_row( + model, + prefill_batch_index, + rms_norm_output, + width + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &rms_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &embedding_output); + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_embedding_row_result, + model->cuda_embedding_row_error[0] != '\0' + ? model->cuda_embedding_row_error + : "cuda_decoder_graph_prefill_initial_norm_copy_failed" + ); + return FAILURE; + } + used_prefill_initial_norm = true; + } else if (king_inference_cuda_rms_norm_f32( + model, + rms_weight, + embedding_output, + rms_norm_output, + width, + epsilon + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &rms_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &embedding_output); + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_rms_norm_result, + model->cuda_rms_norm_error[0] != '\0' + ? model->cuda_rms_norm_error + : "cuda_decoder_graph_rms_norm_execution_failed" + ); + return FAILURE; + } + model->cuda_decoder_graph_executor_rms_norm_execution_count++; + model->cuda_decoder_graph_executor_last_rms_norm_width = width; + executed_device_ops++; + + add_assoc_bool(&rms_norm_probe, "executed", true); + add_assoc_bool(&rms_norm_probe, "used_prefill_initial_norm", used_prefill_initial_norm); + add_assoc_long(&rms_norm_probe, "width", (zend_long) width); + add_assoc_long(&rms_norm_probe, "bytes", (zend_long) bytes); + add_assoc_double(&rms_norm_probe, "epsilon", epsilon); + king_inference_cuda_decoder_graph_executor_record_first_rms_norm_compare(model, token_id, rms_weight, rms_norm_output, width, epsilon, embedding_scale, &rms_norm_probe); + + linear_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, rms_norm_id, "linear"); + add_assoc_bool(&linear_probe, "attempted", linear_op != NULL); + if (linear_op != NULL) { + if (!model->cuda_decoder_graph_executor_linear_execution_available) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &rms_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &embedding_output); + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_linear_execution_unavailable" + ); + return FAILURE; + } + if (king_inference_graph_required_string(linear_op, "id", &linear_id) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &rms_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &embedding_output); + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_linear_id_invalid"); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_linear_shape( + model, + linear_op, + width, + &linear_weight, + &linear_rows + ) != SUCCESS + || linear_rows > (zend_ulong) (SIZE_MAX / sizeof(float))) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &rms_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &embedding_output); + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + return FAILURE; + } + if (king_inference_cuda_device_allocator_alloc( + model, + (size_t) linear_rows * sizeof(float), + &linear_output + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &rms_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &embedding_output); + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_linear_allocation_failed" + ); + return FAILURE; + } + if (use_prefill_embedding && model->cuda_decoder_prompt_prefill_initial_linear != 0) { + if (king_inference_cuda_prefill_copy_initial_linear_row( + model, + prefill_batch_index, + linear_output, + linear_rows + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &linear_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &rms_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &embedding_output); + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_embedding_row_result, + model->cuda_embedding_row_error[0] != '\0' + ? model->cuda_embedding_row_error + : "cuda_decoder_graph_prefill_initial_linear_copy_failed" + ); + return FAILURE; + } + used_prefill_initial_linear = true; + } else if (king_inference_cuda_quantized_matvec( + model, + linear_weight, + rms_norm_output, + width, + linear_output, + linear_rows + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &linear_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &rms_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &embedding_output); + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_quantized_matvec_result, + model->cuda_quantized_matvec_error[0] != '\0' + ? model->cuda_quantized_matvec_error + : "cuda_decoder_graph_linear_execution_failed" + ); + return FAILURE; + } + model->cuda_decoder_graph_executor_linear_execution_count++; + model->cuda_decoder_graph_executor_last_linear_input_width = width; + model->cuda_decoder_graph_executor_last_linear_rows = linear_rows; + executed_device_ops++; + + add_assoc_bool(&linear_probe, "executed", true); + add_assoc_bool(&linear_probe, "used_prefill_initial_linear", used_prefill_initial_linear); + add_assoc_long(&linear_probe, "input_width", (zend_long) width); + add_assoc_long(&linear_probe, "rows", (zend_long) linear_rows); + add_assoc_long(&linear_probe, "bytes", (zend_long) ((size_t) linear_rows * sizeof(float))); + king_inference_cuda_decoder_graph_executor_record_quantized_matvec_compare(model, token_id, rms_weight, linear_weight, linear_output, width, linear_rows, epsilon, &linear_probe); + if (use_prefill_embedding + && king_inference_cuda_decoder_graph_executor_try_execute_prefilled_layer0_attention_residual( + model, + graph, + embedding_id, + width, + prefill_batch_index, + &slice_probe, + &rope_probe, + &kv_head_prepare_probe, + &attention_projection_probe, + &attention_residual_probe, + &ffn_norm_probe, + &ffn_gate_up_probe, + &ffn_swiglu_probe, + &ffn_down_probe, + &ffn_output_probe, + &final_norm_probe, + &logits_probe, + &attention_projection_probe_initialized, + &attention_residual_probe_initialized, + &ffn_norm_probe_initialized, + &ffn_gate_up_probe_initialized, + &ffn_swiglu_probe_initialized, + &ffn_down_probe_initialized, + &ffn_output_probe_initialized, + &final_norm_probe_initialized, + &logits_probe_initialized, + &executed_device_ops, + &used_prefill_attention_residual, + &attention_projection_executed, + &attention_residual_executed, + &ffn_norm_executed, + &ffn_gate_up_executed, + &ffn_swiglu_executed, + &ffn_down_executed, + &ffn_output_executed, + &final_norm_executed, + &logits_executed, + &next_hidden_id, + &next_hidden_output, + &next_hidden_width + ) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_destroy_downstream_probes(attention_projection_probe_initialized, &attention_projection_probe, attention_residual_probe_initialized, &attention_residual_probe, ffn_norm_probe_initialized, &ffn_norm_probe, ffn_gate_up_probe_initialized, &ffn_gate_up_probe, ffn_swiglu_probe_initialized, &ffn_swiglu_probe, ffn_down_probe_initialized, &ffn_down_probe, ffn_output_probe_initialized, &ffn_output_probe, final_norm_probe_initialized, &final_norm_probe, logits_probe_initialized, &logits_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &linear_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &rms_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &embedding_output); + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + return FAILURE; + } + if (used_prefill_attention_residual) { + slice_length = model->cuda_device_kv_cache_key_length; + attention_stack_width = model->cuda_decoder_prompt_prefill_layer0_attention_stack_width; + rope_executed = false; + kv_head_prepared = true; + } + + if (use_prefill_embedding && !used_prefill_attention_residual) { + bool used_prefill_attention_stack = false; + + if (king_inference_cuda_decoder_graph_executor_try_prefilled_layer0_attention_stack( + model, + graph, + prefill_batch_index, + &slice_probe, + &rope_probe, + &kv_head_prepare_probe, + &attention_stack_output, + &attention_stack_width, + &attention_stack_id, + &executed_device_ops, + &used_prefill_attention_stack + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &attention_stack_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &linear_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &rms_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &embedding_output); + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + return FAILURE; + } + if (used_prefill_attention_stack) { + slice_length = model->cuda_device_kv_cache_key_length; + rope_executed = false; + kv_head_prepared = true; + } + } + + if (!kv_head_prepared + && king_inference_cuda_decoder_graph_executor_execute_query_attention_heads( + model, + graph, + rms_norm_id, rms_weight, rms_norm_output, width, token_id, epsilon, + linear_id, + linear_output, + linear_rows, + &slice_probe, + &rope_probe, + &kv_head_prepare_probe, + &attention_stack_output, + &attention_stack_width, + &attention_stack_id, + &executed_device_ops, + &slice_length, + &rope_executed, + &kv_head_prepared + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &attention_stack_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &linear_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &rms_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &embedding_output); + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + return FAILURE; + } + king_inference_cuda_decoder_graph_executor_copy_block0_qkv_projection_compares(result, &linear_probe, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_copy_block0_attention_score_compare(result, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_copy_block0_attention_softmax_compare(result, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_copy_block0_attention_value_compare(result, &kv_head_prepare_probe); + if (!used_prefill_attention_residual && attention_stack_output != 0 && attention_stack_id != NULL) { + array_init(&attention_projection_probe); + array_init(&attention_residual_probe); + array_init(&ffn_norm_probe); + array_init(&ffn_gate_up_probe); + array_init(&ffn_swiglu_probe); + array_init(&ffn_down_probe); + array_init(&ffn_output_probe); + array_init(&final_norm_probe); + array_init(&logits_probe); + attention_projection_probe_initialized = true; + attention_residual_probe_initialized = true; + ffn_norm_probe_initialized = true; + ffn_gate_up_probe_initialized = true; + ffn_swiglu_probe_initialized = true; + ffn_down_probe_initialized = true; + ffn_output_probe_initialized = true; + final_norm_probe_initialized = true; + logits_probe_initialized = true; + if (king_inference_cuda_decoder_graph_executor_execute_attention_projection_residual_ffn_norm_gate_up_swiglu_down_and_output( + model, + graph, + embedding_id, + embedding_output, + width, + attention_stack_id, + attention_stack_output, + attention_stack_width, + &attention_projection_probe, + &attention_residual_probe, + &ffn_norm_probe, + &ffn_gate_up_probe, + &ffn_swiglu_probe, + &ffn_down_probe, + &ffn_output_probe, + &final_norm_probe, + &logits_probe, + &executed_device_ops, + &attention_projection_executed, + &attention_residual_executed, + &ffn_norm_executed, + &ffn_gate_up_executed, + &ffn_swiglu_executed, + &ffn_down_executed, + &ffn_output_executed, + &final_norm_executed, + &logits_executed, + &next_hidden_id, + &next_hidden_output, + &next_hidden_width + ) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_destroy_downstream_probes(true, &attention_projection_probe, true, &attention_residual_probe, true, &ffn_norm_probe, true, &ffn_gate_up_probe, true, &ffn_swiglu_probe, true, &ffn_down_probe, true, &ffn_output_probe, true, &final_norm_probe, true, &logits_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &attention_stack_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &linear_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &rms_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &embedding_output); + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + return FAILURE; + } + if (!logits_executed + && next_hidden_output != 0 + && king_inference_cuda_decoder_graph_executor_execute_remaining_blocks( + model, + graph, + &next_hidden_id, + &next_hidden_output, + &next_hidden_width, + &slice_probe, + &rope_probe, + &kv_head_prepare_probe, + &attention_projection_probe, + &attention_residual_probe, + &ffn_norm_probe, + &ffn_gate_up_probe, + &ffn_swiglu_probe, + &ffn_down_probe, + &ffn_output_probe, + &final_norm_probe, + &logits_probe, + &executed_device_ops, + &attention_projection_executed, + &attention_residual_executed, + &ffn_norm_executed, + &ffn_gate_up_executed, + &ffn_swiglu_executed, + &ffn_down_executed, + &ffn_output_executed, + &final_norm_executed, + &logits_executed + ) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_destroy_downstream_probes(true, &attention_projection_probe, true, &attention_residual_probe, true, &ffn_norm_probe, true, &ffn_gate_up_probe, true, &ffn_swiglu_probe, true, &ffn_down_probe, true, &ffn_output_probe, true, &final_norm_probe, true, &logits_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &next_hidden_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &attention_stack_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &linear_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &rms_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &embedding_output); + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + return FAILURE; + } + } + if (attention_stack_output != 0 + && king_inference_cuda_decoder_graph_executor_release_device_ptr( + model, + &attention_stack_output + ) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_destroy_downstream_probes(attention_projection_probe_initialized, &attention_projection_probe, attention_residual_probe_initialized, &attention_residual_probe, ffn_norm_probe_initialized, &ffn_norm_probe, ffn_gate_up_probe_initialized, &ffn_gate_up_probe, ffn_swiglu_probe_initialized, &ffn_swiglu_probe, ffn_down_probe_initialized, &ffn_down_probe, ffn_output_probe_initialized, &ffn_output_probe, final_norm_probe_initialized, &final_norm_probe, logits_probe_initialized, &logits_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &linear_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &rms_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &embedding_output); + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_attention_stack_free_failed" + ); + return FAILURE; + } + add_assoc_bool(&kv_head_prepare_probe, "attention_stack_released", true); + add_assoc_long(&kv_head_prepare_probe, "attention_stack_width", (zend_long) attention_stack_width); + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &linear_output) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_destroy_downstream_probes(attention_projection_probe_initialized, &attention_projection_probe, attention_residual_probe_initialized, &attention_residual_probe, ffn_norm_probe_initialized, &ffn_norm_probe, ffn_gate_up_probe_initialized, &ffn_gate_up_probe, ffn_swiglu_probe_initialized, &ffn_swiglu_probe, ffn_down_probe_initialized, &ffn_down_probe, ffn_output_probe_initialized, &ffn_output_probe, final_norm_probe_initialized, &final_norm_probe, logits_probe_initialized, &logits_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &rms_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &embedding_output); + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_linear_free_failed" + ); + return FAILURE; + } + } + + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &rms_norm_output) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_destroy_downstream_probes(attention_projection_probe_initialized, &attention_projection_probe, attention_residual_probe_initialized, &attention_residual_probe, ffn_norm_probe_initialized, &ffn_norm_probe, ffn_gate_up_probe_initialized, &ffn_gate_up_probe, ffn_swiglu_probe_initialized, &ffn_swiglu_probe, ffn_down_probe_initialized, &ffn_down_probe, ffn_output_probe_initialized, &ffn_output_probe, final_norm_probe_initialized, &final_norm_probe, logits_probe_initialized, &logits_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &embedding_output); + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_rms_norm_free_failed" + ); + return FAILURE; + } + } + + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &embedding_output) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_destroy_downstream_probes(attention_projection_probe_initialized, &attention_projection_probe, attention_residual_probe_initialized, &attention_residual_probe, ffn_norm_probe_initialized, &ffn_norm_probe, ffn_gate_up_probe_initialized, &ffn_gate_up_probe, ffn_swiglu_probe_initialized, &ffn_swiglu_probe, ffn_down_probe_initialized, &ffn_down_probe, ffn_output_probe_initialized, &ffn_output_probe, final_norm_probe_initialized, &final_norm_probe, logits_probe_initialized, &logits_probe); + king_inference_cuda_decoder_graph_executor_destroy_initial_probes(&embedding_probe, &rms_norm_probe, &linear_probe, &slice_probe, &rope_probe, &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_embedding_free_failed" + ); + return FAILURE; + } + { + zval *compact_decode_result = king_inference_array_find(result, "compact_decode_result"); + + king_inference_cuda_decoder_graph_executor_copy_ffn_numeric_compares( + result, + ffn_gate_up_probe_initialized ? &ffn_gate_up_probe : NULL, + ffn_swiglu_probe_initialized ? &ffn_swiglu_probe : NULL, + ffn_down_probe_initialized ? &ffn_down_probe : NULL, + ffn_output_probe_initialized ? &ffn_output_probe : NULL + ); + + if (compact_decode_result != NULL && zend_is_true(compact_decode_result)) { + king_inference_cuda_decoder_graph_executor_copy_initial_numeric_compares(result, &embedding_probe, &rms_norm_probe, &linear_probe); + king_inference_cuda_decoder_graph_executor_destroy_initial_probes( + &embedding_probe, + &rms_norm_probe, + &linear_probe, + &slice_probe, + &rope_probe, + &kv_head_prepare_probe + ); + king_inference_cuda_decoder_graph_executor_destroy_downstream_probes( + attention_projection_probe_initialized, + &attention_projection_probe, + attention_residual_probe_initialized, + &attention_residual_probe, + ffn_norm_probe_initialized, + &ffn_norm_probe, + ffn_gate_up_probe_initialized, + &ffn_gate_up_probe, + ffn_swiglu_probe_initialized, + &ffn_swiglu_probe, + ffn_down_probe_initialized, + &ffn_down_probe, + ffn_output_probe_initialized, + &ffn_output_probe, + final_norm_probe_initialized, + &final_norm_probe, + false, + &logits_probe + ); + if (logits_probe_initialized) { + add_assoc_zval(result, "logits_projection_device_execution", &logits_probe); + } + add_assoc_bool(result, "compact_decode_result_applied", true); + add_assoc_bool(result, "logits_projection_device_execution_ready", logits_executed); + add_assoc_bool(result, "bounded_logits_result_ready", logits_executed); + add_assoc_bool(result, "device_execution_result_ready", logits_executed); + add_assoc_bool(result, "first_device_op_executed", true); + add_assoc_bool(result, "initial_device_ops_executed", executed_device_ops > 1); + add_assoc_long(result, "executed_device_ops", (zend_long) executed_device_ops); + return SUCCESS; + } + } + add_assoc_zval(result, "embedding_device_execution", &embedding_probe); + add_assoc_zval(result, "rms_norm_device_execution", &rms_norm_probe); + add_assoc_zval(result, "linear_device_execution", &linear_probe); + add_assoc_zval(result, "slice_device_execution", &slice_probe); + add_assoc_zval(result, "rope_device_execution", &rope_probe); + add_assoc_zval(result, "kv_head_prepare_device_execution", &kv_head_prepare_probe); + king_inference_cuda_decoder_graph_executor_add_downstream_probes(result, attention_projection_probe_initialized, &attention_projection_probe, attention_residual_probe_initialized, &attention_residual_probe, ffn_norm_probe_initialized, &ffn_norm_probe, ffn_gate_up_probe_initialized, &ffn_gate_up_probe, ffn_swiglu_probe_initialized, &ffn_swiglu_probe, ffn_down_probe_initialized, &ffn_down_probe, ffn_output_probe_initialized, &ffn_output_probe, final_norm_probe_initialized, &final_norm_probe, logits_probe_initialized, &logits_probe); + add_assoc_bool(result, "embedding_device_execution_ready", true); + add_assoc_bool(result, "rms_norm_device_execution_ready", rms_norm_op != NULL); + add_assoc_bool(result, "linear_device_execution_ready", linear_rows > 0); + add_assoc_bool(result, "slice_device_execution_ready", slice_length > 0); + add_assoc_bool(result, "rope_device_execution_ready", rope_executed || kv_head_prepared); + add_assoc_bool(result, "kv_head_prepare_device_execution_ready", kv_head_prepared); + add_assoc_bool(result, "kv_write_device_execution_ready", kv_head_prepared); + add_assoc_bool(result, "kv_attention_device_execution_ready", kv_head_prepared); + add_assoc_bool(result, "attention_stack_slot_device_execution_ready", kv_head_prepared); + add_assoc_bool(result, "attention_heads_device_execution_ready", kv_head_prepared); + add_assoc_bool(result, "attention_stack_device_execution_ready", kv_head_prepared && attention_stack_width > 0); + add_assoc_bool(result, "attention_output_projection_device_execution_ready", attention_projection_executed); + add_assoc_bool(result, "attention_residual_device_execution_ready", attention_residual_executed); + add_assoc_bool(result, "ffn_norm_device_execution_ready", ffn_norm_executed); + add_assoc_bool(result, "ffn_gate_up_projection_device_execution_ready", ffn_gate_up_executed); + add_assoc_bool(result, "ffn_swiglu_device_execution_ready", ffn_swiglu_executed); + add_assoc_bool(result, "ffn_down_projection_device_execution_ready", ffn_down_executed); + add_assoc_bool(result, "ffn_output_residual_device_execution_ready", ffn_output_executed); + add_assoc_bool(result, "final_norm_device_execution_ready", final_norm_executed); + add_assoc_bool(result, "logits_projection_device_execution_ready", logits_executed); + add_assoc_bool(result, "bounded_logits_result_ready", logits_executed); + add_assoc_bool(result, "device_execution_result_ready", logits_executed); + add_assoc_bool(result, "first_device_op_executed", true); + add_assoc_bool(result, "initial_device_ops_executed", executed_device_ops > 1); + add_assoc_long(result, "executed_device_ops", (zend_long) executed_device_ops); + return SUCCESS; +} diff --git a/extension/src/inference/cuda/decoder_graph/device/core/cuda_decoder_graph_executor_execution_plan.inc b/extension/src/inference/cuda/decoder_graph/device/core/cuda_decoder_graph_executor_execution_plan.inc new file mode 100644 index 000000000..6c50d0431 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/device/core/cuda_decoder_graph_executor_execution_plan.inc @@ -0,0 +1,748 @@ +/* + * CUDA decoder graph execution plan builder. + */ + +static const char *king_inference_cuda_decoder_graph_executor_plan_owner(zend_string *op_name) +{ + if (king_inference_cuda_decoder_graph_executor_host_sampling_op(op_name)) { + return "host"; + } + if (zend_string_equals_literal(op_name, "kv_write")) { + return "cuda_device_kv_cache"; + } + return "cuda_device"; +} + +static const char *king_inference_cuda_decoder_graph_executor_plan_dtype(zend_string *op_name) +{ + if (king_inference_cuda_decoder_graph_executor_host_sampling_op(op_name)) { + return "token_tuple_f64"; + } + if (zend_string_equals_literal(op_name, "kv_write")) { + return "kv_write_marker"; + } + return "f32"; +} + +static const char *king_inference_cuda_decoder_graph_executor_plan_allocation_scope(zend_string *op_name) +{ + if (king_inference_cuda_decoder_graph_executor_host_sampling_op(op_name)) { + return "host_sampling_result"; + } + if (zend_string_equals_literal(op_name, "kv_write")) { + return "device_kv_cache_side_effect"; + } + if (zend_string_equals_literal(op_name, "kv_attention")) { + return "temporary_device_attention_context"; + } + return "temporary_device_vector"; +} + +static const char *king_inference_cuda_decoder_graph_executor_plan_required_capability( + zend_string *node_id, + zend_string *op_name +) { + if (zend_string_equals_literal(op_name, "embedding")) { + return "embedding_execution_available"; + } + if (zend_string_equals_literal(op_name, "rms_norm")) { + return "rms_norm_execution_available"; + } + if (zend_string_equals_literal(op_name, "linear")) { + if (node_id != NULL && zend_string_equals_literal(node_id, "logits")) { + return "logits_projection_execution_available"; + } + return "linear_execution_available"; + } + if (zend_string_equals_literal(op_name, "add")) { + return "vector_add_available"; + } + if (zend_string_equals_literal(op_name, "scale")) { + return "vector_scale_available"; + } + if (zend_string_equals_literal(op_name, "mul")) { + return "vector_mul_available"; + } + if (zend_string_equals_literal(op_name, "silu")) { + return "vector_silu_available"; + } + if (zend_string_equals_literal(op_name, "gelu_tanh")) { + return "ffn_swiglu_execution_available"; + } + if (zend_string_equals_literal(op_name, "slice")) { + return "slice_execution_available"; + } + if (zend_string_equals_literal(op_name, "rope")) { + return "rope_execution_available"; + } + if (zend_string_equals_literal(op_name, "kv_write")) { + return "kv_write_execution_available"; + } + if (zend_string_equals_literal(op_name, "kv_attention")) { + return "kv_attention_execution_available"; + } + if (zend_string_equals_literal(op_name, "stack")) { + return "vector_copy_to_offset_available"; + } + if (king_inference_cuda_decoder_graph_executor_host_sampling_op(op_name)) { + return "sampling_readback_available"; + } + return "unsupported_operation"; +} + +static bool king_inference_cuda_decoder_graph_executor_plan_capability_available( + king_inference_model_object *model, + zend_string *node_id, + zend_string *op_name +) { + if (zend_string_equals_literal(op_name, "embedding")) { + return model->cuda_decoder_graph_executor_embedding_execution_available; + } + if (zend_string_equals_literal(op_name, "rms_norm")) { + return model->cuda_decoder_graph_executor_rms_norm_execution_available; + } + if (zend_string_equals_literal(op_name, "linear")) { + if (node_id != NULL && zend_string_equals_literal(node_id, "logits")) { + return model->cuda_decoder_graph_executor_logits_projection_execution_available; + } + return model->cuda_decoder_graph_executor_linear_execution_available; + } + if (zend_string_equals_literal(op_name, "add")) { + return model->cuda_vector_add_available && model->cuda_device_allocator_available; + } + if (zend_string_equals_literal(op_name, "scale")) { + return model->cuda_vector_scale_available && model->cuda_device_allocator_available; + } + if (zend_string_equals_literal(op_name, "mul")) { + return model->cuda_vector_mul_available && model->cuda_device_allocator_available; + } + if (zend_string_equals_literal(op_name, "silu")) { + return model->cuda_vector_silu_available && model->cuda_device_allocator_available; + } + if (zend_string_equals_literal(op_name, "gelu_tanh")) { + return model->cuda_decoder_graph_executor_ffn_swiglu_execution_available; + } + if (zend_string_equals_literal(op_name, "slice")) { + return model->cuda_decoder_graph_executor_slice_execution_available; + } + if (zend_string_equals_literal(op_name, "rope")) { + return model->cuda_decoder_graph_executor_rope_execution_available; + } + if (zend_string_equals_literal(op_name, "kv_write")) { + return model->cuda_decoder_graph_executor_kv_write_execution_available; + } + if (zend_string_equals_literal(op_name, "kv_attention")) { + return model->cuda_decoder_graph_executor_kv_attention_execution_available; + } + if (zend_string_equals_literal(op_name, "stack")) { + return model->cuda_vector_copy_to_offset_available && model->cuda_device_allocator_available; + } + if (king_inference_cuda_decoder_graph_executor_host_sampling_op(op_name)) { + return model->cuda_decoder_graph_executor_sampling_readback_available + && model->cuda_logits_readback_available; + } + return false; +} + +static void king_inference_cuda_decoder_graph_executor_plan_record_read( + zval *read_counts, + zval *read_last, + zend_string *reference, + zend_ulong op_index +) { + zval *count = zend_hash_find(Z_ARRVAL_P(read_counts), reference); + zval value; + + if (count != NULL && Z_TYPE_P(count) == IS_LONG) { + ZVAL_LONG(count, Z_LVAL_P(count) + 1); + } else { + ZVAL_LONG(&value, 1); + zend_hash_update(Z_ARRVAL_P(read_counts), reference, &value); + } + ZVAL_LONG(&value, (zend_long) op_index); + zend_hash_update(Z_ARRVAL_P(read_last), reference, &value); +} + +static void king_inference_cuda_decoder_graph_executor_plan_scan_read_field( + zval *entry, + const char *field, + zval *read_counts, + zval *read_last, + zend_ulong op_index +) { + zval *value = king_inference_array_find(entry, field); + + if (value != NULL && Z_TYPE_P(value) == IS_STRING && Z_STRLEN_P(value) > 0) { + king_inference_cuda_decoder_graph_executor_plan_record_read(read_counts, read_last, Z_STR_P(value), op_index); + } +} + +static void king_inference_cuda_decoder_graph_executor_plan_scan_reads( + zval *entry, + zval *read_counts, + zval *read_last, + zend_ulong op_index +) { + zval *inputs; + zval *input; + + king_inference_cuda_decoder_graph_executor_plan_scan_read_field(entry, "input", read_counts, read_last, op_index); + king_inference_cuda_decoder_graph_executor_plan_scan_read_field(entry, "left", read_counts, read_last, op_index); + king_inference_cuda_decoder_graph_executor_plan_scan_read_field(entry, "right", read_counts, read_last, op_index); + king_inference_cuda_decoder_graph_executor_plan_scan_read_field(entry, "key", read_counts, read_last, op_index); + king_inference_cuda_decoder_graph_executor_plan_scan_read_field(entry, "value", read_counts, read_last, op_index); + king_inference_cuda_decoder_graph_executor_plan_scan_read_field(entry, "query", read_counts, read_last, op_index); + king_inference_cuda_decoder_graph_executor_plan_scan_read_field(entry, "logits", read_counts, read_last, op_index); + + inputs = king_inference_array_find(entry, "inputs"); + if (inputs == NULL || Z_TYPE_P(inputs) != IS_ARRAY) { + return; + } + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(inputs), input) { + if (input != NULL && Z_TYPE_P(input) == IS_STRING && Z_STRLEN_P(input) > 0) { + king_inference_cuda_decoder_graph_executor_plan_record_read(read_counts, read_last, Z_STR_P(input), op_index); + } + } ZEND_HASH_FOREACH_END(); +} + +static zend_result king_inference_cuda_decoder_graph_executor_plan_collect_read_field( + king_inference_model_object *model, + zval *entry, + zend_string *node_id, + zval *defined, + zval *reads, + const char *field +) { + zval *value = king_inference_array_find(entry, field); + + if (value == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(value) != IS_STRING || Z_STRLEN_P(value) == 0) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "execution_plan_dependency", + node_id, + field, + "read_dependency_must_be_non_empty_string" + ); + } + if (!zend_hash_exists(Z_ARRVAL_P(defined), Z_STR_P(value))) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "execution_plan_dependency", + node_id, + field, + Z_STRVAL_P(value) + ); + } + add_next_index_str(reads, zend_string_copy(Z_STR_P(value))); + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_plan_collect_reads( + king_inference_model_object *model, + zval *entry, + zend_string *node_id, + zval *defined, + zval *reads +) { + zval *inputs; + zval *input; + + if (king_inference_cuda_decoder_graph_executor_plan_collect_read_field(model, entry, node_id, defined, reads, "input") != SUCCESS + || king_inference_cuda_decoder_graph_executor_plan_collect_read_field(model, entry, node_id, defined, reads, "left") != SUCCESS + || king_inference_cuda_decoder_graph_executor_plan_collect_read_field(model, entry, node_id, defined, reads, "right") != SUCCESS + || king_inference_cuda_decoder_graph_executor_plan_collect_read_field(model, entry, node_id, defined, reads, "key") != SUCCESS + || king_inference_cuda_decoder_graph_executor_plan_collect_read_field(model, entry, node_id, defined, reads, "value") != SUCCESS + || king_inference_cuda_decoder_graph_executor_plan_collect_read_field(model, entry, node_id, defined, reads, "query") != SUCCESS + || king_inference_cuda_decoder_graph_executor_plan_collect_read_field(model, entry, node_id, defined, reads, "logits") != SUCCESS) { + return FAILURE; + } + + inputs = king_inference_array_find(entry, "inputs"); + if (inputs == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(inputs) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL_P(inputs)) == 0) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "execution_plan_dependency", + node_id, + "inputs", + "inputs_must_be_non_empty_array" + ); + } + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(inputs), input) { + if (input == NULL || Z_TYPE_P(input) != IS_STRING || Z_STRLEN_P(input) == 0) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "execution_plan_dependency", + node_id, + "inputs", + "input_dependency_must_be_non_empty_string" + ); + } + if (!zend_hash_exists(Z_ARRVAL_P(defined), Z_STR_P(input))) { + return king_inference_cuda_decoder_graph_executor_validation_error( + model, + "execution_plan_dependency", + node_id, + "inputs", + Z_STRVAL_P(input) + ); + } + add_next_index_str(reads, zend_string_copy(Z_STR_P(input))); + } ZEND_HASH_FOREACH_END(); + + return SUCCESS; +} + +static void king_inference_cuda_decoder_graph_executor_plan_add_boundary( + zval *step_boundaries, + zval *global_boundaries, + zend_ulong step_order, + zend_string *node_id, + zend_string *op_name, + const char *boundary +) { + zval global; + + add_next_index_string(step_boundaries, boundary); + array_init(&global); + add_assoc_long(&global, "step_order", (zend_long) step_order); + add_assoc_str(&global, "node", zend_string_copy(node_id)); + add_assoc_str(&global, "op", zend_string_copy(op_name)); + add_assoc_string(&global, "boundary", boundary); + add_next_index_zval(global_boundaries, &global); +} + +static void king_inference_cuda_decoder_graph_executor_plan_add_kernel( + zval *step_kernels, + zval *global_kernels, + zend_ulong *kernel_order, + zend_ulong step_order, + zend_string *node_id, + zend_string *op_name, + const char *owner, + const char *kernel +) { + zval global; + + add_next_index_string(step_kernels, kernel); + array_init(&global); + add_assoc_long(&global, "kernel_order", (zend_long) *kernel_order); + add_assoc_long(&global, "step_order", (zend_long) step_order); + add_assoc_str(&global, "node", zend_string_copy(node_id)); + add_assoc_str(&global, "op", zend_string_copy(op_name)); + add_assoc_string(&global, "owner", owner); + add_assoc_string(&global, "kernel", kernel); + add_next_index_zval(global_kernels, &global); + (*kernel_order)++; +} + +static void king_inference_cuda_decoder_graph_executor_plan_add_kernel_sequence( + zval *step_kernels, + zval *global_kernels, + zend_ulong *kernel_order, + zend_ulong step_order, + zend_string *node_id, + zend_string *op_name, + const char *owner +) { + if (zend_string_equals_literal(op_name, "embedding")) { + king_inference_cuda_decoder_graph_executor_plan_add_kernel(step_kernels, global_kernels, kernel_order, step_order, node_id, op_name, owner, "cuda.embedding_row_load"); + } else if (zend_string_equals_literal(op_name, "rms_norm")) { + king_inference_cuda_decoder_graph_executor_plan_add_kernel(step_kernels, global_kernels, kernel_order, step_order, node_id, op_name, owner, "cuda.rms_norm_f32"); + } else if (zend_string_equals_literal(op_name, "linear")) { + king_inference_cuda_decoder_graph_executor_plan_add_kernel(step_kernels, global_kernels, kernel_order, step_order, node_id, op_name, owner, "cuda.quantized_matvec"); + } else if (zend_string_equals_literal(op_name, "add")) { + king_inference_cuda_decoder_graph_executor_plan_add_kernel(step_kernels, global_kernels, kernel_order, step_order, node_id, op_name, owner, "cuda.vector_add"); + } else if (zend_string_equals_literal(op_name, "scale")) { + king_inference_cuda_decoder_graph_executor_plan_add_kernel(step_kernels, global_kernels, kernel_order, step_order, node_id, op_name, owner, "cuda.vector_scale"); + } else if (zend_string_equals_literal(op_name, "mul")) { + king_inference_cuda_decoder_graph_executor_plan_add_kernel(step_kernels, global_kernels, kernel_order, step_order, node_id, op_name, owner, "cuda.vector_mul"); + } else if (zend_string_equals_literal(op_name, "silu")) { + king_inference_cuda_decoder_graph_executor_plan_add_kernel(step_kernels, global_kernels, kernel_order, step_order, node_id, op_name, owner, "cuda.vector_silu"); + } else if (zend_string_equals_literal(op_name, "gelu_tanh")) { + king_inference_cuda_decoder_graph_executor_plan_add_kernel(step_kernels, global_kernels, kernel_order, step_order, node_id, op_name, owner, "cuda.ffn_activation_gelu_tanh"); + } else if (zend_string_equals_literal(op_name, "slice")) { + king_inference_cuda_decoder_graph_executor_plan_add_kernel(step_kernels, global_kernels, kernel_order, step_order, node_id, op_name, owner, "cuda.vector_slice"); + } else if (zend_string_equals_literal(op_name, "rope")) { + king_inference_cuda_decoder_graph_executor_plan_add_kernel(step_kernels, global_kernels, kernel_order, step_order, node_id, op_name, owner, "cuda.rope_f32"); + } else if (zend_string_equals_literal(op_name, "kv_write")) { + king_inference_cuda_decoder_graph_executor_plan_add_kernel(step_kernels, global_kernels, kernel_order, step_order, node_id, op_name, owner, "cuda.kv_cache_write"); + } else if (zend_string_equals_literal(op_name, "kv_attention")) { + king_inference_cuda_decoder_graph_executor_plan_add_kernel(step_kernels, global_kernels, kernel_order, step_order, node_id, op_name, owner, "cuda.attention_scores"); + king_inference_cuda_decoder_graph_executor_plan_add_kernel(step_kernels, global_kernels, kernel_order, step_order, node_id, op_name, owner, "cuda.attention_softmax"); + king_inference_cuda_decoder_graph_executor_plan_add_kernel(step_kernels, global_kernels, kernel_order, step_order, node_id, op_name, owner, "cuda.attention_values"); + } else if (zend_string_equals_literal(op_name, "stack")) { + king_inference_cuda_decoder_graph_executor_plan_add_kernel(step_kernels, global_kernels, kernel_order, step_order, node_id, op_name, owner, "cuda.copy_to_offset"); + } else if (zend_string_equals_literal(op_name, "argmax_token")) { + king_inference_cuda_decoder_graph_executor_plan_add_kernel(step_kernels, global_kernels, kernel_order, step_order, node_id, op_name, owner, "host.bounded_logits_argmax"); + } else if (zend_string_equals_literal(op_name, "sample_token")) { + king_inference_cuda_decoder_graph_executor_plan_add_kernel(step_kernels, global_kernels, kernel_order, step_order, node_id, op_name, owner, "host.bounded_logits_sample"); + } +} + +static void king_inference_cuda_decoder_graph_executor_plan_add_boundaries( + zval *step_boundaries, + zval *global_boundaries, + zend_ulong step_order, + zend_string *node_id, + zend_string *op_name +) { + king_inference_cuda_decoder_graph_executor_plan_add_boundary(step_boundaries, global_boundaries, step_order, node_id, op_name, "validation_preflight"); + if (king_inference_cuda_decoder_graph_executor_host_sampling_op(op_name)) { + king_inference_cuda_decoder_graph_executor_plan_add_boundary(step_boundaries, global_boundaries, step_order, node_id, op_name, "bounded_logits_readback"); + king_inference_cuda_decoder_graph_executor_plan_add_boundary(step_boundaries, global_boundaries, step_order, node_id, op_name, "host_sampling"); + return; + } + if (zend_string_equals_literal(op_name, "kv_attention")) { + king_inference_cuda_decoder_graph_executor_plan_add_boundary(step_boundaries, global_boundaries, step_order, node_id, op_name, "kv_cache_read"); + } + if (zend_string_equals_literal(op_name, "kv_write")) { + king_inference_cuda_decoder_graph_executor_plan_add_boundary(step_boundaries, global_boundaries, step_order, node_id, op_name, "kv_cache_write"); + } + king_inference_cuda_decoder_graph_executor_plan_add_boundary(step_boundaries, global_boundaries, step_order, node_id, op_name, "device_allocation"); + king_inference_cuda_decoder_graph_executor_plan_add_boundary(step_boundaries, global_boundaries, step_order, node_id, op_name, "cuda_kernel"); +} + +static zend_result king_inference_cuda_decoder_graph_executor_prepare_plan( + king_inference_model_object *model, + zval *graph, + zval *result +) { + zval *ops = king_inference_cuda_decoder_graph_executor_ops(graph); + zval *entry; + zval plan; + zval steps; + zval buffers; + zval kernel_sequence; + zval failure_boundaries; + zval device_host_handoffs; + zval read_counts; + zval read_last; + zval defined; + zval lengths; + zend_ulong numeric_index; + zend_string *string_index; + zend_ulong total_ops = 0; + zend_ulong device_ops = 0; + zend_ulong host_sampling_ops = 0; + zend_ulong kernel_order = 0; + zend_result status = SUCCESS; + + if (ops == NULL) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_plan_ops_missing"); + return FAILURE; + } + + array_init(&read_counts); + array_init(&read_last); + ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(ops), numeric_index, string_index, entry) { + if (string_index != NULL || numeric_index > (zend_ulong) ZEND_LONG_MAX) { + zval_ptr_dtor(&read_last); + zval_ptr_dtor(&read_counts); + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_plan_requires_numeric_ordered_ops"); + return FAILURE; + } + if (entry != NULL && Z_TYPE_P(entry) == IS_ARRAY) { + king_inference_cuda_decoder_graph_executor_plan_scan_reads(entry, &read_counts, &read_last, numeric_index); + } + } ZEND_HASH_FOREACH_END(); + + array_init(&defined); + array_init(&lengths); + if (king_inference_cuda_decoder_graph_executor_validate_graph_inputs( + model, + graph, + &defined, + &lengths + ) != SUCCESS) { + zval_ptr_dtor(&lengths); + zval_ptr_dtor(&defined); + zval_ptr_dtor(&read_last); + zval_ptr_dtor(&read_counts); + return FAILURE; + } + + array_init(&steps); + array_init(&buffers); + array_init(&kernel_sequence); + array_init(&failure_boundaries); + array_init(&device_host_handoffs); + + ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(ops), numeric_index, string_index, entry) { + zend_string *id = NULL; + zend_string *op_name = NULL; + zend_ulong output_length = 0; + zend_long last_read = (zend_long) numeric_index; + zend_long read_count = 0; + zval *last_read_value; + zval *read_count_value; + zval marker; + zval step; + zval reads; + zval writes; + zval shape; + zval lifetime; + zval step_kernels; + zval step_boundaries; + zval kernel_profile; + zval validation; + zval buffer; + const char *owner; + const char *dtype; + const char *allocation_scope; + const char *required_capability; + + if (string_index != NULL + || numeric_index > (zend_ulong) ZEND_LONG_MAX + || entry == NULL + || Z_TYPE_P(entry) != IS_ARRAY + || king_inference_graph_required_string(entry, "id", &id) != SUCCESS + || king_inference_graph_required_string(entry, "op", &op_name) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_plan_op_invalid"); + status = FAILURE; + break; + } + if (zend_hash_exists(Z_ARRVAL(defined), id)) { + status = king_inference_cuda_decoder_graph_executor_validation_error( + model, + "execution_plan_write", + id, + "id", + "duplicate_output_id" + ); + break; + } + if (!king_inference_cuda_decoder_graph_executor_supported_op(op_name)) { + status = king_inference_cuda_decoder_graph_executor_validation_error( + model, + "execution_plan_unsupported", + id, + "op", + ZSTR_VAL(op_name) + ); + break; + } + + array_init(&reads); + if (king_inference_cuda_decoder_graph_executor_plan_collect_reads( + model, + entry, + id, + &defined, + &reads + ) != SUCCESS) { + zval_ptr_dtor(&reads); + status = FAILURE; + break; + } + if (king_inference_cuda_decoder_graph_executor_validate_op_contract( + model, + entry, + id, + op_name, + &defined, + &lengths, + &output_length + ) != SUCCESS + || king_inference_cuda_decoder_graph_executor_set_validated_length( + model, + &lengths, + id, + output_length + ) != SUCCESS) { + zval_ptr_dtor(&reads); + status = FAILURE; + break; + } + + owner = king_inference_cuda_decoder_graph_executor_plan_owner(op_name); + dtype = king_inference_cuda_decoder_graph_executor_plan_dtype(op_name); + allocation_scope = king_inference_cuda_decoder_graph_executor_plan_allocation_scope(op_name); + required_capability = king_inference_cuda_decoder_graph_executor_plan_required_capability(id, op_name); + if (!king_inference_cuda_decoder_graph_executor_plan_capability_available(model, id, op_name)) { + zval_ptr_dtor(&reads); + status = king_inference_cuda_decoder_graph_executor_validation_error( + model, + "execution_plan_capability", + id, + "op", + required_capability + ); + break; + } + last_read_value = zend_hash_find(Z_ARRVAL(read_last), id); + read_count_value = zend_hash_find(Z_ARRVAL(read_counts), id); + if (last_read_value != NULL && Z_TYPE_P(last_read_value) == IS_LONG) { + last_read = Z_LVAL_P(last_read_value); + } + if (read_count_value != NULL && Z_TYPE_P(read_count_value) == IS_LONG) { + read_count = Z_LVAL_P(read_count_value); + } + + array_init(&writes); + add_next_index_str(&writes, zend_string_copy(id)); + array_init(&shape); + add_assoc_string(&shape, "rank", "vector"); + add_assoc_long(&shape, "length", (zend_long) output_length); + add_assoc_string(&shape, "dtype", dtype); + add_assoc_string(&shape, "validation", "graph_contract_preflight"); + array_init(&lifetime); + add_assoc_long(&lifetime, "produce_step", (zend_long) numeric_index); + add_assoc_long(&lifetime, "last_read_step", last_read); + add_assoc_long(&lifetime, "read_count", read_count); + add_assoc_string(&lifetime, "allocation_scope", allocation_scope); + array_init(&kernel_profile); + add_assoc_string(&kernel_profile, "required_capability", required_capability); + add_assoc_bool(&kernel_profile, "capability_available", true); + add_assoc_string(&kernel_profile, "owner", owner); + add_assoc_string(&kernel_profile, "dtype", dtype); + add_assoc_string(&kernel_profile, "allocation_scope", allocation_scope); + array_init(&validation); + add_assoc_bool(&validation, "dependencies_validated", true); + add_assoc_bool(&validation, "shape_validated", true); + add_assoc_bool(&validation, "dtype_validated", true); + add_assoc_bool(&validation, "capability_validated", true); + add_assoc_string(&validation, "error_code", "none"); + + array_init(&step_kernels); + king_inference_cuda_decoder_graph_executor_plan_add_kernel_sequence( + &step_kernels, + &kernel_sequence, + &kernel_order, + numeric_index, + id, + op_name, + owner + ); + array_init(&step_boundaries); + king_inference_cuda_decoder_graph_executor_plan_add_boundaries( + &step_boundaries, + &failure_boundaries, + numeric_index, + id, + op_name + ); + + array_init(&step); + add_assoc_long(&step, "order", (zend_long) total_ops); + add_assoc_long(&step, "op_index", (zend_long) numeric_index); + add_assoc_str(&step, "node", zend_string_copy(id)); + add_assoc_str(&step, "op", zend_string_copy(op_name)); + add_assoc_string(&step, "owner", owner); + add_assoc_zval(&step, "reads", &reads); + add_assoc_zval(&step, "writes", &writes); + add_assoc_zval(&step, "shape", &shape); + add_assoc_zval(&step, "buffer_lifetime", &lifetime); + add_assoc_zval(&step, "kernel_sequence", &step_kernels); + add_assoc_zval(&step, "kernel_profile", &kernel_profile); + add_assoc_zval(&step, "validation", &validation); + add_assoc_zval(&step, "failure_boundaries", &step_boundaries); + add_next_index_zval(&steps, &step); + + array_init(&buffer); + add_assoc_str(&buffer, "id", zend_string_copy(id)); + add_assoc_str(&buffer, "producer_op", zend_string_copy(op_name)); + add_assoc_string(&buffer, "owner", owner); + add_assoc_string(&buffer, "dtype", dtype); + add_assoc_long(&buffer, "length", (zend_long) output_length); + add_assoc_long(&buffer, "produce_step", (zend_long) numeric_index); + add_assoc_long(&buffer, "last_read_step", last_read); + add_assoc_long(&buffer, "read_count", read_count); + add_assoc_string(&buffer, "allocation_scope", allocation_scope); + add_assoc_string(&buffer, "required_capability", required_capability); + add_next_index_zval(&buffers, &buffer); + + if (king_inference_cuda_decoder_graph_executor_device_op(op_name)) { + device_ops++; + } else if (king_inference_cuda_decoder_graph_executor_host_sampling_op(op_name)) { + zval handoff; + + host_sampling_ops++; + array_init(&handoff); + add_assoc_str(&handoff, "node", zend_string_copy(id)); + add_assoc_string(&handoff, "from", "cuda_device_bounded_logits"); + add_assoc_string(&handoff, "to", "host_sampler"); + add_assoc_string(&handoff, "boundary", "bounded_logits_readback"); + add_next_index_zval(&device_host_handoffs, &handoff); + } else { + status = king_inference_cuda_decoder_graph_executor_validation_error( + model, + "execution_plan_unsupported", + id, + "op", + ZSTR_VAL(op_name) + ); + break; + } + + ZVAL_TRUE(&marker); + zend_hash_update(Z_ARRVAL(defined), id, &marker); + total_ops++; + } ZEND_HASH_FOREACH_END(); + + if (status == SUCCESS && host_sampling_ops > 0 && !model->cuda_decoder_graph_executor_sampling_readback_available) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_plan_sampling_readback_unavailable" + ); + status = FAILURE; + } + + zval_ptr_dtor(&lengths); + zval_ptr_dtor(&defined); + zval_ptr_dtor(&read_last); + zval_ptr_dtor(&read_counts); + + if (status != SUCCESS) { + zval_ptr_dtor(&device_host_handoffs); + zval_ptr_dtor(&failure_boundaries); + zval_ptr_dtor(&kernel_sequence); + zval_ptr_dtor(&buffers); + zval_ptr_dtor(&steps); + return FAILURE; + } + + array_init(&plan); + add_assoc_string(&plan, "type", "gpu_decoder_graph_execution_plan"); + add_assoc_long(&plan, "schema_version", 2); + add_assoc_bool(&plan, "ready", true); + add_assoc_bool(&plan, "dependency_closure_proven", true); + add_assoc_bool(&plan, "capability_closure_proven", true); + add_assoc_bool(&plan, "shape_dtype_preflight_proven", true); + add_assoc_bool(&plan, "prompt_content_free_debug_surface", true); + add_assoc_bool(&plan, "device_execution_result_ready", false); + add_assoc_bool(&plan, "token_result_ready", false); + add_assoc_bool(&plan, "device_ops_ready", true); + add_assoc_bool(&plan, "host_sampling_after_bounded_readback", host_sampling_ops > 0); + add_assoc_long(&plan, "total_ops", (zend_long) total_ops); + add_assoc_long(&plan, "device_ops", (zend_long) device_ops); + add_assoc_long(&plan, "host_sampling_ops", (zend_long) host_sampling_ops); + add_assoc_long(&plan, "kernel_count", (zend_long) kernel_order); + add_assoc_string(&plan, "next_bridge", "execute_device_ops_and_write_bounded_logits_result"); + add_assoc_zval(&plan, "steps", &steps); + add_assoc_zval(&plan, "buffers", &buffers); + add_assoc_zval(&plan, "kernel_sequence", &kernel_sequence); + add_assoc_zval(&plan, "failure_boundaries", &failure_boundaries); + add_assoc_zval(&plan, "device_host_handoffs", &device_host_handoffs); + add_assoc_zval(result, "execution_plan", &plan); + add_assoc_bool(result, "execution_plan_ready", true); + add_assoc_bool(result, "execution_plan_dependency_closure_proven", true); + add_assoc_bool(result, "execution_plan_capability_closure_proven", true); + add_assoc_bool(result, "execution_plan_shape_dtype_preflight_proven", true); + add_assoc_bool(result, "execution_plan_prompt_content_free", true); + add_assoc_long(result, "planned_device_ops", (zend_long) device_ops); + add_assoc_long(result, "planned_host_sampling_ops", (zend_long) host_sampling_ops); + add_assoc_long(result, "planned_kernel_count", (zend_long) kernel_order); + + model->cuda_decoder_graph_executor_plan_count++; + model->cuda_decoder_graph_executor_last_plan_op_count = total_ops; + model->cuda_decoder_graph_executor_last_plan_device_ops = device_ops; + model->cuda_decoder_graph_executor_last_plan_host_sampling_ops = host_sampling_ops; + return SUCCESS; +} diff --git a/extension/src/inference/cuda/decoder_graph/ops/attention/cuda_decoder_graph_executor_attention_ops.inc b/extension/src/inference/cuda/decoder_graph/ops/attention/cuda_decoder_graph_executor_attention_ops.inc new file mode 100644 index 000000000..f76b95544 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/ops/attention/cuda_decoder_graph_executor_attention_ops.inc @@ -0,0 +1,396 @@ +/* + * CUDA decoder graph KV-attention execution helpers for native King GPU inference. + */ + +#include "../../device/compare/attention/cuda_decoder_graph_executor_attention_score_compare.inc" + +static zval *king_inference_cuda_decoder_graph_executor_kv_attention_op_for_query( + zval *graph, + zend_string *query_id +) { + zval *ops = king_inference_cuda_decoder_graph_executor_ops(graph); + zval *entry; + + if (ops == NULL || query_id == NULL) { + return NULL; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(ops), entry) { + zval *op_name; + zval *query; + + if (entry == NULL || Z_TYPE_P(entry) != IS_ARRAY) { + continue; + } + op_name = king_inference_array_find(entry, "op"); + query = king_inference_array_find(entry, "query"); + if (op_name != NULL + && Z_TYPE_P(op_name) == IS_STRING + && zend_string_equals_literal(Z_STR_P(op_name), "kv_attention") + && king_inference_cuda_decoder_graph_executor_zval_string_equals(query, query_id)) { + return entry; + } + } ZEND_HASH_FOREACH_END(); + + return NULL; +} + +static zend_result king_inference_cuda_decoder_graph_executor_kv_attention_shape( + king_inference_model_object *model, + zval *attention_op, + zend_ulong query_length, + zend_ulong *layer, + zend_ulong *head, + zend_ulong *slot_start, + zend_ulong *slot_count, + zend_ulong *value_length, + double *scale +) { + zend_string *cache = NULL; + unsigned long parsed_layer = 0; + unsigned long parsed_head = 0; + int consumed = 0; + zend_ulong declared_key_length; + zend_ulong declared_value_length; + + *layer = 0; + *head = 0; + *slot_start = 0; + *slot_count = 0; + *value_length = 0; + *scale = 1.0; + if (attention_op == NULL + || king_inference_graph_required_string(attention_op, "cache", &cache) != SUCCESS + || ZSTR_LEN(cache) > INT_MAX + || sscanf(ZSTR_VAL(cache), "l%lu.h%lu%n", &parsed_layer, &parsed_head, &consumed) != 2 + || consumed != (int) ZSTR_LEN(cache) + || king_inference_graph_finite_double_option(attention_op, "scale", 1.0, scale) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_kv_attention_contract_invalid"); + return FAILURE; + } + + *slot_start = king_inference_tensor_option_ulong(attention_op, "slot_start", 0); + *slot_count = king_inference_tensor_option_ulong(attention_op, "slot_count", 0); + declared_key_length = king_inference_tensor_option_ulong(attention_op, "key_length", query_length); + declared_value_length = king_inference_tensor_option_ulong( + attention_op, + "value_length", + model->cuda_device_kv_cache_value_length + ); + if (query_length == 0 + || declared_key_length != query_length + || query_length > model->cuda_device_kv_cache_key_length + || declared_value_length == 0 + || declared_value_length > model->cuda_device_kv_cache_value_length + || !isfinite(*scale)) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_kv_attention_shape_invalid"); + return FAILURE; + } + if (king_inference_cuda_device_kv_cache_guard_range( + model, + "attention_read_range", + (zend_ulong) parsed_layer, + (zend_ulong) parsed_head, + *slot_start, + *slot_count, + false + ) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_kv_cache_result, + model->cuda_device_kv_cache_error[0] != '\0' + ? model->cuda_device_kv_cache_error + : "cuda_decoder_graph_kv_attention_read_range_invalid" + ); + return FAILURE; + } + + *layer = (zend_ulong) parsed_layer; + *head = (zend_ulong) parsed_head; + *value_length = declared_value_length; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_attention_alloc( + king_inference_model_object *model, + zend_ulong slot_count, + zend_ulong value_length, + king_inference_cuda_device_ptr *scores_output, + king_inference_cuda_device_ptr *probabilities_output, + king_inference_cuda_device_ptr *context_output +) { + *scores_output = 0; + *probabilities_output = 0; + *context_output = 0; + if (slot_count > (zend_ulong) (SIZE_MAX / sizeof(float)) + || value_length == 0 + || value_length > (zend_ulong) (SIZE_MAX / sizeof(float)) + || king_inference_cuda_device_allocator_alloc(model, (size_t) slot_count * sizeof(float), scores_output) != SUCCESS + || king_inference_cuda_device_allocator_alloc(model, (size_t) slot_count * sizeof(float), probabilities_output) != SUCCESS + || king_inference_cuda_device_allocator_alloc(model, (size_t) value_length * sizeof(float), context_output) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, context_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, probabilities_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, scores_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_kv_attention_allocation_failed" + ); + return FAILURE; + } + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_execute_kv_attention( + king_inference_model_object *model, + zval *attention_op, + king_inference_cuda_device_ptr query_output, + zend_ulong query_length, + zval *attention_probe, + king_inference_cuda_device_ptr *context_output, + zend_ulong *context_length, + zend_ulong *executed_device_ops +) { + king_inference_cuda_device_ptr key_cache = 0; + king_inference_cuda_device_ptr value_cache = 0; + king_inference_cuda_device_ptr scores_output = 0; + king_inference_cuda_device_ptr probabilities_output = 0; + zend_ulong key_stride = 0; + zend_ulong value_stride = 0; + zend_ulong layer = 0; + zend_ulong head = 0; + zend_ulong slot_start = 0; + zend_ulong slot_count = 0; + zend_ulong value_length = 0; + double scale = 1.0; + + *context_output = 0; + *context_length = 0; + add_assoc_bool(attention_probe, "attempted", attention_op != NULL); + add_assoc_bool(attention_probe, "available", model->cuda_decoder_graph_executor_kv_attention_execution_available); + add_assoc_bool(attention_probe, "executed", false); + add_assoc_bool(attention_probe, "retained_device_output", false); + if (attention_op == NULL) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_kv_attention_missing"); + return FAILURE; + } + if (!model->cuda_decoder_graph_executor_kv_attention_execution_available) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_kv_attention_execution_unavailable"); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_kv_attention_shape( + model, + attention_op, + query_length, + &layer, + &head, + &slot_start, + &slot_count, + &value_length, + &scale + ) != SUCCESS) { + return FAILURE; + } + if (king_inference_cuda_device_kv_cache_head(model, false, layer, head, &key_cache, &key_stride) != SUCCESS + || king_inference_cuda_device_kv_cache_head(model, true, layer, head, &value_cache, &value_stride) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_kv_cache_result, + model->cuda_device_kv_cache_error[0] != '\0' + ? model->cuda_device_kv_cache_error + : "cuda_decoder_graph_kv_attention_cache_unavailable" + ); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_attention_alloc( + model, + slot_count, + value_length, + &scores_output, + &probabilities_output, + context_output + ) != SUCCESS) { + return FAILURE; + } + if (king_inference_cuda_attention_scores_f32( + model, + query_output, + key_cache, + scores_output, + query_length, + key_stride, + slot_start, + slot_count, + scale + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, context_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &probabilities_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &scores_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_attention_scores_result, + model->cuda_attention_scores_error[0] != '\0' + ? model->cuda_attention_scores_error + : "cuda_decoder_graph_kv_attention_scores_failed" + ); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_compare_block0_bos_attention_scores( + model, + query_output, + key_cache, + scores_output, + layer, + head, + query_length, + key_stride, + slot_start, + slot_count, + scale, + attention_probe + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, context_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &probabilities_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &scores_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + king_inference_cuda_decoder_graph_executor_attention_compare_error_reason( + attention_probe, + "attention_score_numeric_compare", + "cuda_decoder_graph_kv_attention_score_compare_failed" + ) + ); + return FAILURE; + } + if (king_inference_cuda_attention_softmax_f32( + model, + scores_output, + 0, + probabilities_output, + 0, + slot_count, + 1.0, + 1.0 + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, context_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &probabilities_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &scores_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_attention_softmax_result, + model->cuda_attention_softmax_error[0] != '\0' + ? model->cuda_attention_softmax_error + : "cuda_decoder_graph_kv_attention_softmax_failed" + ); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_compare_block0_bos_attention_softmax( + model, + probabilities_output, + layer, + head, + slot_start, + slot_count, + attention_probe + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, context_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &probabilities_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &scores_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + king_inference_cuda_decoder_graph_executor_attention_compare_error_reason( + attention_probe, + "attention_softmax_numeric_compare", + "cuda_decoder_graph_kv_attention_softmax_compare_failed" + ) + ); + return FAILURE; + } + if (king_inference_cuda_attention_values_f32( + model, + probabilities_output, + value_cache, + *context_output, + value_length, + value_stride, + slot_start, + slot_count + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, context_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &probabilities_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &scores_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_attention_values_result, + model->cuda_attention_values_error[0] != '\0' + ? model->cuda_attention_values_error + : "cuda_decoder_graph_kv_attention_values_failed" + ); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_compare_block0_bos_attention_values( + model, + probabilities_output, + value_cache, + *context_output, + layer, + head, + value_length, + value_stride, + slot_start, + slot_count, + attention_probe + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, context_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &probabilities_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &scores_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + king_inference_cuda_decoder_graph_executor_attention_compare_error_reason( + attention_probe, + "attention_value_numeric_compare", + "cuda_decoder_graph_kv_attention_value_compare_failed" + ) + ); + return FAILURE; + } + + model->cuda_decoder_graph_executor_kv_attention_execution_count++; + model->cuda_decoder_graph_executor_last_kv_attention_key_length = query_length; + model->cuda_decoder_graph_executor_last_kv_attention_value_length = value_length; + model->cuda_decoder_graph_executor_last_kv_attention_layer = layer; + model->cuda_decoder_graph_executor_last_kv_attention_head = head; + model->cuda_decoder_graph_executor_last_kv_attention_slot_start = slot_start; + model->cuda_decoder_graph_executor_last_kv_attention_slot_count = slot_count; + (*executed_device_ops)++; + *context_length = value_length; + + add_assoc_bool(attention_probe, "executed", true); + add_assoc_bool(attention_probe, "retained_device_output", true); + add_assoc_long(attention_probe, "layer", (zend_long) layer); + add_assoc_long(attention_probe, "head", (zend_long) head); + add_assoc_long(attention_probe, "slot_start", (zend_long) slot_start); + add_assoc_long(attention_probe, "slot_count", (zend_long) slot_count); + add_assoc_long(attention_probe, "key_length", (zend_long) query_length); + add_assoc_long(attention_probe, "value_length", (zend_long) value_length); + add_assoc_double(attention_probe, "scale", scale); + + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &probabilities_output) != SUCCESS + || king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &scores_output) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, context_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_kv_attention_free_failed" + ); + return FAILURE; + } + return SUCCESS; +} diff --git a/extension/src/inference/cuda/decoder_graph/ops/control/cuda_decoder_graph_executor_block_loop_ops.inc b/extension/src/inference/cuda/decoder_graph/ops/control/cuda_decoder_graph_executor_block_loop_ops.inc new file mode 100644 index 000000000..5603db913 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/ops/control/cuda_decoder_graph_executor_block_loop_ops.inc @@ -0,0 +1,221 @@ +/* + * CUDA decoder graph block loop for full native GPU token decoding. + */ + +static zend_result king_inference_cuda_decoder_graph_executor_execute_remaining_blocks( + king_inference_model_object *model, + zval *graph, + zend_string **current_id, + king_inference_cuda_device_ptr *current_output, + zend_ulong *current_width, + zval *slice_probe, + zval *rope_probe, + zval *kv_head_prepare_probe, + zval *attention_projection_probe, + zval *attention_residual_probe, + zval *ffn_norm_probe, + zval *ffn_gate_up_probe, + zval *ffn_swiglu_probe, + zval *ffn_down_probe, + zval *ffn_output_probe, + zval *final_norm_probe, + zval *logits_probe, + zend_ulong *executed_device_ops, + bool *attention_projection_executed, + bool *attention_residual_executed, + bool *ffn_norm_executed, + bool *ffn_gate_up_executed, + bool *ffn_swiglu_executed, + bool *ffn_down_executed, + bool *ffn_output_executed, + bool *final_norm_executed, + bool *logits_executed +) { + zend_ulong guard = 0; + zend_ulong guard_limit = model->gguf.architecture_params[KING_INFERENCE_ARCH_BLOCK_COUNT] + 2; + + while (*current_output != 0 && *current_id != NULL && guard++ < guard_limit) { + zval *norm_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, *current_id, "rms_norm"); + zval *query_op; + zend_string *norm_id = NULL; + zend_string *query_id = NULL; + zend_string *next_id = NULL; + king_inference_cuda_device_ptr norm_output = 0; + king_inference_cuda_device_ptr query_output = 0; + king_inference_cuda_device_ptr attention_stack_output = 0; + king_inference_cuda_device_ptr next_output = 0; + zend_ulong norm_width = 0; + zend_ulong query_rows = 0; + zend_ulong attention_stack_width = 0; + zend_ulong next_width = 0; + zend_ulong slice_length = 0; + bool rope_executed = false; + bool kv_prepared = false; + + if (norm_op == NULL) { + break; + } + if (king_inference_graph_required_string(norm_op, "id", &norm_id) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_block_norm_id_invalid"); + return FAILURE; + } + if (zend_string_equals_literal(norm_id, "final_norm")) { + if (king_inference_cuda_decoder_graph_executor_execute_final_norm( + model, + graph, + *current_id, + *current_output, + *current_width, + final_norm_probe, + &norm_output, + &norm_width, + &norm_id, + executed_device_ops + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &norm_output); + return FAILURE; + } + *final_norm_executed = norm_output != 0 && norm_width > 0; + if (*final_norm_executed + && king_inference_cuda_decoder_graph_executor_execute_logits_projection( + model, + graph, + norm_id, + norm_output, + norm_width, + logits_probe, + &query_output, + &query_rows, + &query_id, + executed_device_ops + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &query_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &norm_output); + return FAILURE; + } + *logits_executed = query_output != 0 && query_rows > 0 && query_id != NULL; + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &query_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, current_output); + return SUCCESS; + } + if (king_inference_cuda_decoder_graph_executor_execute_ffn_norm( + model, + graph, + *current_id, + *current_output, + *current_width, + ffn_norm_probe, + &norm_output, + &norm_width, + &norm_id, + executed_device_ops + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &norm_output); + return FAILURE; + } + query_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, norm_id, "linear", 0); + if (query_op == NULL + || king_inference_graph_required_string(query_op, "id", &query_id) != SUCCESS + || king_inference_cuda_decoder_graph_executor_linear_to_device( + model, + query_op, + norm_output, + norm_width, + &query_output, + &query_rows + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &query_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &norm_output); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_execute_query_attention_heads( + model, + graph, + norm_id, + NULL, + norm_output, + norm_width, + 0, + 0.000001, + query_id, + query_output, + query_rows, + slice_probe, + rope_probe, + kv_head_prepare_probe, + &attention_stack_output, + &attention_stack_width, + &next_id, + executed_device_ops, + &slice_length, + &rope_executed, + &kv_prepared + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &attention_stack_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &query_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &norm_output); + return FAILURE; + } + (void) slice_length; + (void) rope_executed; + (void) kv_prepared; + if (king_inference_cuda_decoder_graph_executor_execute_attention_projection_residual_ffn_norm_gate_up_swiglu_down_and_output( + model, + graph, + *current_id, + *current_output, + *current_width, + next_id, + attention_stack_output, + attention_stack_width, + attention_projection_probe, + attention_residual_probe, + ffn_norm_probe, + ffn_gate_up_probe, + ffn_swiglu_probe, + ffn_down_probe, + ffn_output_probe, + final_norm_probe, + logits_probe, + executed_device_ops, + attention_projection_executed, + attention_residual_executed, + ffn_norm_executed, + ffn_gate_up_executed, + ffn_swiglu_executed, + ffn_down_executed, + ffn_output_executed, + final_norm_executed, + logits_executed, + &next_id, + &next_output, + &next_width + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &next_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &attention_stack_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &query_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &norm_output); + return FAILURE; + } + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &attention_stack_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &query_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, current_output); + if (*logits_executed || next_output == 0 || next_id == NULL || next_width == 0) { + *current_id = next_id; + *current_output = next_output; + *current_width = next_width; + return SUCCESS; + } + *current_id = next_id; + *current_output = next_output; + *current_width = next_width; + } + + if (!*logits_executed) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_block_loop_terminal_unreached"); + return FAILURE; + } + return SUCCESS; +} diff --git a/extension/src/inference/cuda/decoder_graph/ops/control/cuda_decoder_graph_executor_probe_ops.inc b/extension/src/inference/cuda/decoder_graph/ops/control/cuda_decoder_graph_executor_probe_ops.inc new file mode 100644 index 000000000..b547633ef --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/ops/control/cuda_decoder_graph_executor_probe_ops.inc @@ -0,0 +1,164 @@ +/* + * CUDA decoder graph probe lifecycle helpers for native King GPU inference. + */ + +static void king_inference_cuda_decoder_graph_executor_destroy_initial_probes( + zval *embedding_probe, + zval *rms_norm_probe, + zval *linear_probe, + zval *slice_probe, + zval *rope_probe, + zval *kv_head_prepare_probe +) { + zval_ptr_dtor(embedding_probe); + zval_ptr_dtor(rms_norm_probe); + zval_ptr_dtor(linear_probe); + zval_ptr_dtor(slice_probe); + zval_ptr_dtor(rope_probe); + zval_ptr_dtor(kv_head_prepare_probe); +} + +static void king_inference_cuda_decoder_graph_executor_add_empty_initial_result( + zval *result, + zval *embedding_probe, + zval *rms_norm_probe, + zval *linear_probe, + zval *slice_probe, + zval *rope_probe, + zval *kv_head_prepare_probe +) { + static const char *ready_keys[] = { + "embedding_device_execution_ready", + "rms_norm_device_execution_ready", + "linear_device_execution_ready", + "slice_device_execution_ready", + "rope_device_execution_ready", + "kv_head_prepare_device_execution_ready", + "kv_write_device_execution_ready", + "kv_attention_device_execution_ready", + "attention_stack_slot_device_execution_ready", + "attention_heads_device_execution_ready", + "attention_stack_device_execution_ready", + "attention_output_projection_device_execution_ready", + "attention_residual_device_execution_ready", + "ffn_norm_device_execution_ready", + "ffn_gate_up_projection_device_execution_ready", + "ffn_swiglu_device_execution_ready", + "ffn_down_projection_device_execution_ready", + "ffn_output_residual_device_execution_ready", + "final_norm_device_execution_ready", + "logits_projection_device_execution_ready", + "bounded_logits_result_ready" + }; + size_t index; + + add_assoc_zval(result, "embedding_device_execution", embedding_probe); + add_assoc_zval(result, "rms_norm_device_execution", rms_norm_probe); + add_assoc_zval(result, "linear_device_execution", linear_probe); + add_assoc_zval(result, "slice_device_execution", slice_probe); + add_assoc_zval(result, "rope_device_execution", rope_probe); + add_assoc_zval(result, "kv_head_prepare_device_execution", kv_head_prepare_probe); + for (index = 0; index < sizeof(ready_keys) / sizeof(ready_keys[0]); index++) { + add_assoc_bool(result, ready_keys[index], false); + } + add_assoc_long(result, "executed_device_ops", 0); +} + +static void king_inference_cuda_decoder_graph_executor_destroy_downstream_probes( + bool projection_initialized, + zval *projection_probe, + bool residual_initialized, + zval *residual_probe, + bool ffn_norm_initialized, + zval *ffn_norm_probe, + bool ffn_gate_up_initialized, + zval *ffn_gate_up_probe, + bool ffn_swiglu_initialized, + zval *ffn_swiglu_probe, + bool ffn_down_initialized, + zval *ffn_down_probe, + bool ffn_output_initialized, + zval *ffn_output_probe, + bool final_norm_initialized, + zval *final_norm_probe, + bool logits_initialized, + zval *logits_probe +) { + if (projection_initialized) { + zval_ptr_dtor(projection_probe); + } + if (residual_initialized) { + zval_ptr_dtor(residual_probe); + } + if (ffn_norm_initialized) { + zval_ptr_dtor(ffn_norm_probe); + } + if (ffn_gate_up_initialized) { + zval_ptr_dtor(ffn_gate_up_probe); + } + if (ffn_swiglu_initialized) { + zval_ptr_dtor(ffn_swiglu_probe); + } + if (ffn_down_initialized) { + zval_ptr_dtor(ffn_down_probe); + } + if (ffn_output_initialized) { + zval_ptr_dtor(ffn_output_probe); + } + if (final_norm_initialized) { + zval_ptr_dtor(final_norm_probe); + } + if (logits_initialized) { + zval_ptr_dtor(logits_probe); + } +} + +static void king_inference_cuda_decoder_graph_executor_add_downstream_probes( + zval *result, + bool projection_initialized, + zval *projection_probe, + bool residual_initialized, + zval *residual_probe, + bool ffn_norm_initialized, + zval *ffn_norm_probe, + bool ffn_gate_up_initialized, + zval *ffn_gate_up_probe, + bool ffn_swiglu_initialized, + zval *ffn_swiglu_probe, + bool ffn_down_initialized, + zval *ffn_down_probe, + bool ffn_output_initialized, + zval *ffn_output_probe, + bool final_norm_initialized, + zval *final_norm_probe, + bool logits_initialized, + zval *logits_probe +) { + if (projection_initialized) { + add_assoc_zval(result, "attention_output_projection_device_execution", projection_probe); + } + if (residual_initialized) { + add_assoc_zval(result, "attention_residual_device_execution", residual_probe); + } + if (ffn_norm_initialized) { + add_assoc_zval(result, "ffn_norm_device_execution", ffn_norm_probe); + } + if (ffn_gate_up_initialized) { + add_assoc_zval(result, "ffn_gate_up_projection_device_execution", ffn_gate_up_probe); + } + if (ffn_swiglu_initialized) { + add_assoc_zval(result, "ffn_swiglu_device_execution", ffn_swiglu_probe); + } + if (ffn_down_initialized) { + add_assoc_zval(result, "ffn_down_projection_device_execution", ffn_down_probe); + } + if (ffn_output_initialized) { + add_assoc_zval(result, "ffn_output_residual_device_execution", ffn_output_probe); + } + if (final_norm_initialized) { + add_assoc_zval(result, "final_norm_device_execution", final_norm_probe); + } + if (logits_initialized) { + add_assoc_zval(result, "logits_projection_device_execution", logits_probe); + } +} diff --git a/extension/src/inference/cuda/decoder_graph/ops/ffn/cuda_decoder_graph_executor_ffn_ops.inc b/extension/src/inference/cuda/decoder_graph/ops/ffn/cuda_decoder_graph_executor_ffn_ops.inc new file mode 100644 index 000000000..d2181d18c --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/ops/ffn/cuda_decoder_graph_executor_ffn_ops.inc @@ -0,0 +1,794 @@ +/* + * CUDA decoder graph FFN execution helpers for native King GPU inference. + */ + +#include "../../device/compare/ffn/cuda_decoder_graph_executor_ffn_gate_up_compare.inc" + +static zend_result king_inference_cuda_decoder_graph_executor_execute_ffn_gate_up_projections( + king_inference_model_object *model, + zval *graph, + zend_string *ffn_norm_id, + king_inference_cuda_device_ptr ffn_norm_output, + zend_ulong ffn_norm_width, + zval *ffn_gate_up_probe, + king_inference_cuda_device_ptr *gate_output, + king_inference_cuda_device_ptr *up_output, + zend_ulong *gate_rows, + zend_ulong *up_rows, + zend_string **gate_id_out, + zend_string **up_id_out, + zend_ulong *executed_device_ops +) { + zval *gate_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, ffn_norm_id, "linear", 0); + zval *up_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, ffn_norm_id, "linear", 1); + zend_string *gate_id = NULL; + zend_string *up_id = NULL; + + *gate_output = 0; + *up_output = 0; + *gate_rows = 0; + *up_rows = 0; + if (gate_id_out != NULL) { + *gate_id_out = NULL; + } + if (up_id_out != NULL) { + *up_id_out = NULL; + } + add_assoc_string(ffn_gate_up_probe, "type", "gpu_decoder_ffn_gate_up_projection_device_execution"); + add_assoc_bool(ffn_gate_up_probe, "attempted", gate_op != NULL || up_op != NULL); + add_assoc_bool(ffn_gate_up_probe, "available", model->cuda_decoder_graph_executor_ffn_gate_up_projection_execution_available); + add_assoc_bool(ffn_gate_up_probe, "executed", false); + add_assoc_bool(ffn_gate_up_probe, "retained_device_output", false); + if (gate_op == NULL && up_op == NULL) { + return SUCCESS; + } + if (!model->cuda_decoder_graph_executor_ffn_gate_up_projection_execution_available) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_ffn_gate_up_projection_execution_unavailable"); + return FAILURE; + } + if (gate_op == NULL + || up_op == NULL + || ffn_norm_output == 0 + || ffn_norm_width == 0 + || king_inference_graph_required_string(gate_op, "id", &gate_id) != SUCCESS + || king_inference_graph_required_string(up_op, "id", &up_id) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_ffn_gate_up_projection_contract_invalid"); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_linear_to_device( + model, + gate_op, + ffn_norm_output, + ffn_norm_width, + gate_output, + gate_rows + ) != SUCCESS) { + return FAILURE; + } + (*executed_device_ops)++; + if (king_inference_cuda_decoder_graph_executor_linear_to_device( + model, + up_op, + ffn_norm_output, + ffn_norm_width, + up_output, + up_rows + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, gate_output); + return FAILURE; + } + (*executed_device_ops)++; + if (*gate_rows == 0 || *gate_rows != *up_rows) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, up_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, gate_output); + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_ffn_gate_up_projection_shape_invalid"); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_compare_block0_ffn_gate_up(model, gate_op, up_op, ffn_norm_output, ffn_norm_width, *gate_output, *gate_rows, *up_output, *up_rows, ffn_gate_up_probe) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, up_output); (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, gate_output); + return FAILURE; + } + + model->cuda_decoder_graph_executor_ffn_gate_up_projection_execution_count++; + model->cuda_decoder_graph_executor_last_ffn_gate_rows = *gate_rows; + model->cuda_decoder_graph_executor_last_ffn_up_rows = *up_rows; + if (gate_id_out != NULL) { + *gate_id_out = gate_id; + } + if (up_id_out != NULL) { + *up_id_out = up_id; + } + + add_assoc_str(ffn_gate_up_probe, "gate", zend_string_copy(gate_id)); + add_assoc_str(ffn_gate_up_probe, "up", zend_string_copy(up_id)); + add_assoc_bool(ffn_gate_up_probe, "executed", true); + add_assoc_bool(ffn_gate_up_probe, "retained_device_output", true); + add_assoc_long(ffn_gate_up_probe, "input_width", (zend_long) ffn_norm_width); + add_assoc_long(ffn_gate_up_probe, "gate_rows", (zend_long) *gate_rows); + add_assoc_long(ffn_gate_up_probe, "up_rows", (zend_long) *up_rows); + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_execute_ffn_swiglu( + king_inference_model_object *model, + zval *graph, + zend_string *gate_id, + king_inference_cuda_device_ptr gate_output, + zend_ulong gate_rows, + zend_string *up_id, + king_inference_cuda_device_ptr up_output, + zend_ulong up_rows, + zval *ffn_swiglu_probe, + king_inference_cuda_device_ptr *swiglu_output, + zend_ulong *swiglu_width, + zend_string **gated_id_out, + zend_ulong *executed_device_ops +) { + zval *silu_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, gate_id, "silu"); + zval *activation_op; + zval *mul_op = NULL; + zend_string *silu_id = NULL; + zend_string *gated_id = NULL; + zend_ulong activation = 0; + + *swiglu_output = 0; + *swiglu_width = 0; + if (gated_id_out != NULL) { + *gated_id_out = NULL; + } + add_assoc_string(ffn_swiglu_probe, "type", "gpu_decoder_ffn_swiglu_device_execution"); + if (silu_op == NULL) { + silu_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, gate_id, "gelu_tanh"); + activation = silu_op != NULL ? 1 : 0; + } + activation_op = silu_op; + add_assoc_bool(ffn_swiglu_probe, "attempted", activation_op != NULL); + add_assoc_bool(ffn_swiglu_probe, "available", model->cuda_decoder_graph_executor_ffn_swiglu_execution_available); + add_assoc_bool(ffn_swiglu_probe, "executed", false); + add_assoc_bool(ffn_swiglu_probe, "retained_device_output", false); + if (activation_op == NULL) { + return SUCCESS; + } + if (!model->cuda_decoder_graph_executor_ffn_swiglu_execution_available) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_ffn_swiglu_execution_unavailable"); + return FAILURE; + } + if (gate_output == 0 + || up_output == 0 + || gate_rows == 0 + || gate_rows != up_rows + || gate_rows > (zend_ulong) (SIZE_MAX / sizeof(float)) + || king_inference_graph_required_string(activation_op, "id", &silu_id) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_ffn_swiglu_input_invalid"); + return FAILURE; + } + mul_op = king_inference_cuda_decoder_graph_executor_mul_op_for_inputs(graph, silu_id, up_id); + if (mul_op == NULL || king_inference_graph_required_string(mul_op, "id", &gated_id) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_ffn_swiglu_contract_invalid"); + return FAILURE; + } + if (king_inference_cuda_device_allocator_alloc(model, (size_t) gate_rows * sizeof(float), swiglu_output) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_ffn_swiglu_allocation_failed" + ); + return FAILURE; + } + if (king_inference_cuda_ffn_swiglu_f32(model, gate_output, up_output, *swiglu_output, gate_rows, activation) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, swiglu_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_ffn_swiglu_result, + model->cuda_ffn_swiglu_error[0] != '\0' + ? model->cuda_ffn_swiglu_error + : "cuda_decoder_graph_ffn_swiglu_execution_failed" + ); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_compare_block0_ffn_swiglu(model, gate_id, gate_output, gate_rows, up_id, up_output, up_rows, *swiglu_output, gate_rows, activation, ffn_swiglu_probe) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, swiglu_output); return FAILURE; + } + + model->cuda_decoder_graph_executor_ffn_swiglu_execution_count++; + model->cuda_decoder_graph_executor_last_ffn_swiglu_width = gate_rows; + *swiglu_width = gate_rows; + if (gated_id_out != NULL) { + *gated_id_out = gated_id; + } + (*executed_device_ops)++; + + add_assoc_str(ffn_swiglu_probe, "gate", zend_string_copy(gate_id)); + add_assoc_str(ffn_swiglu_probe, "up", zend_string_copy(up_id)); + add_assoc_str(ffn_swiglu_probe, activation == 1 ? "gelu_tanh" : "silu", zend_string_copy(silu_id)); + add_assoc_long(ffn_swiglu_probe, "activation", (zend_long) activation); + add_assoc_str(ffn_swiglu_probe, "gated", zend_string_copy(gated_id)); + add_assoc_bool(ffn_swiglu_probe, "executed", true); + add_assoc_bool(ffn_swiglu_probe, "retained_device_output", true); + add_assoc_long(ffn_swiglu_probe, "width", (zend_long) gate_rows); + add_assoc_long(ffn_swiglu_probe, "bytes", (zend_long) ((size_t) gate_rows * sizeof(float))); + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_execute_ffn_down_projection( + king_inference_model_object *model, + zval *graph, + zend_string *gated_id, + king_inference_cuda_device_ptr swiglu_output, + zend_ulong swiglu_width, + zval *ffn_down_probe, + king_inference_cuda_device_ptr *down_output, + zend_ulong *down_width, + zend_string **down_id_out, + zend_ulong *executed_device_ops +) { + zval *down_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, gated_id, "linear"); + zend_string *down_id = NULL; + + *down_output = 0; + *down_width = 0; + if (down_id_out != NULL) { + *down_id_out = NULL; + } + add_assoc_string(ffn_down_probe, "type", "gpu_decoder_ffn_down_projection_device_execution"); + add_assoc_bool(ffn_down_probe, "attempted", down_op != NULL); + add_assoc_bool(ffn_down_probe, "available", model->cuda_decoder_graph_executor_ffn_down_projection_execution_available); + add_assoc_bool(ffn_down_probe, "executed", false); + add_assoc_bool(ffn_down_probe, "retained_device_output", false); + if (down_op == NULL) { + return SUCCESS; + } + if (!model->cuda_decoder_graph_executor_ffn_down_projection_execution_available) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_ffn_down_projection_execution_unavailable"); + return FAILURE; + } + if (swiglu_output == 0 + || swiglu_width == 0 + || king_inference_graph_required_string(down_op, "id", &down_id) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_ffn_down_projection_contract_invalid"); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_linear_to_device( + model, + down_op, + swiglu_output, + swiglu_width, + down_output, + down_width + ) != SUCCESS) { + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_compare_block0_ffn_down_projection(model, down_op, swiglu_output, swiglu_width, *down_output, *down_width, ffn_down_probe) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, down_output); + return FAILURE; + } + + model->cuda_decoder_graph_executor_ffn_down_projection_execution_count++; + model->cuda_decoder_graph_executor_last_ffn_down_rows = *down_width; + if (down_id_out != NULL) { + *down_id_out = down_id; + } + (*executed_device_ops)++; + + add_assoc_str(ffn_down_probe, "input", zend_string_copy(gated_id)); + add_assoc_str(ffn_down_probe, "down", zend_string_copy(down_id)); + add_assoc_bool(ffn_down_probe, "executed", true); + add_assoc_bool(ffn_down_probe, "retained_device_output", true); + add_assoc_long(ffn_down_probe, "input_width", (zend_long) swiglu_width); + add_assoc_long(ffn_down_probe, "rows", (zend_long) *down_width); + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_execute_attention_projection_residual_ffn_norm_gate_up_swiglu_down_and_output( + king_inference_model_object *model, + zval *graph, + zend_string *hidden_id, + king_inference_cuda_device_ptr hidden_output, + zend_ulong hidden_width, + zend_string *stack_id, + king_inference_cuda_device_ptr stack_output, + zend_ulong stack_width, + zval *projection_probe, + zval *residual_probe, + zval *ffn_norm_probe, + zval *ffn_gate_up_probe, + zval *ffn_swiglu_probe, + zval *ffn_down_probe, + zval *ffn_output_probe, + zval *final_norm_probe, + zval *logits_probe, + zend_ulong *executed_device_ops, + bool *projection_executed, + bool *residual_executed, + bool *ffn_norm_executed, + bool *ffn_gate_up_executed, + bool *ffn_swiglu_executed, + bool *ffn_down_executed, + bool *ffn_output_executed, + bool *final_norm_executed, + bool *logits_executed, + zend_string **next_hidden_id_out, + king_inference_cuda_device_ptr *next_hidden_output, + zend_ulong *next_hidden_width +) { + zend_string *projection_id = NULL; + zend_string *projection_residual_id = NULL; + zend_string *post_attention_norm_id = NULL; + zend_string *residual_id = NULL; + zend_string *ffn_norm_id = NULL; + zend_string *ffn_gate_id = NULL; + zend_string *ffn_up_id = NULL; + zend_string *ffn_gated_id = NULL; + zend_string *ffn_down_id = NULL; + zend_string *ffn_output_right_id = NULL; + zend_string *post_ffw_norm_id = NULL; + zend_string *ffn_output_id = NULL; + zend_string *final_norm_id = NULL; + zend_string *logits_id = NULL; + king_inference_cuda_device_ptr projection_output = 0; + king_inference_cuda_device_ptr projection_residual_output = 0; + king_inference_cuda_device_ptr post_attention_norm_output = 0; + king_inference_cuda_device_ptr residual_output = 0; + king_inference_cuda_device_ptr ffn_norm_output = 0; + king_inference_cuda_device_ptr ffn_gate_output = 0; + king_inference_cuda_device_ptr ffn_up_output = 0; + king_inference_cuda_device_ptr ffn_swiglu_output = 0; + king_inference_cuda_device_ptr ffn_down_output = 0; + king_inference_cuda_device_ptr ffn_output_right_output = 0; + king_inference_cuda_device_ptr post_ffw_norm_output = 0; + king_inference_cuda_device_ptr ffn_output_residual = 0; + king_inference_cuda_device_ptr final_norm_output = 0; + king_inference_cuda_device_ptr logits_output = 0; + zend_ulong projection_width = 0; + zend_ulong projection_residual_width = 0; + zend_ulong post_attention_norm_width = 0; + zend_ulong residual_width = 0; + zend_ulong ffn_norm_width = 0; + zend_ulong ffn_gate_rows = 0; + zend_ulong ffn_up_rows = 0; + zend_ulong ffn_swiglu_width = 0; + zend_ulong ffn_down_width = 0; + zend_ulong ffn_output_right_width = 0; + zend_ulong post_ffw_norm_width = 0; + zend_ulong ffn_output_width = 0; + zend_ulong final_norm_width = 0; + zend_ulong logits_rows = 0; + zend_result release_result = SUCCESS; + + if (next_hidden_id_out != NULL) { + *next_hidden_id_out = NULL; + } + if (next_hidden_output != NULL) { + *next_hidden_output = 0; + } + if (next_hidden_width != NULL) { + *next_hidden_width = 0; + } + *projection_executed = false; + *residual_executed = false; + *ffn_norm_executed = false; + *ffn_gate_up_executed = false; + *ffn_swiglu_executed = false; + *ffn_down_executed = false; + *ffn_output_executed = false; + *final_norm_executed = false; + *logits_executed = false; + add_assoc_string(ffn_gate_up_probe, "type", "gpu_decoder_ffn_gate_up_projection_device_execution"); + add_assoc_bool(ffn_gate_up_probe, "attempted", false); + add_assoc_bool(ffn_gate_up_probe, "available", model->cuda_decoder_graph_executor_ffn_gate_up_projection_execution_available); + add_assoc_bool(ffn_gate_up_probe, "executed", false); + add_assoc_bool(ffn_gate_up_probe, "retained_device_output", false); + add_assoc_string(ffn_swiglu_probe, "type", "gpu_decoder_ffn_swiglu_device_execution"); + add_assoc_bool(ffn_swiglu_probe, "attempted", false); + add_assoc_bool(ffn_swiglu_probe, "available", model->cuda_decoder_graph_executor_ffn_swiglu_execution_available); + add_assoc_bool(ffn_swiglu_probe, "executed", false); + add_assoc_bool(ffn_swiglu_probe, "retained_device_output", false); + add_assoc_string(ffn_down_probe, "type", "gpu_decoder_ffn_down_projection_device_execution"); + add_assoc_bool(ffn_down_probe, "attempted", false); + add_assoc_bool(ffn_down_probe, "available", model->cuda_decoder_graph_executor_ffn_down_projection_execution_available); + add_assoc_bool(ffn_down_probe, "executed", false); + add_assoc_bool(ffn_down_probe, "retained_device_output", false); + add_assoc_string(ffn_output_probe, "type", "gpu_decoder_ffn_output_residual_device_execution"); + add_assoc_bool(ffn_output_probe, "attempted", false); + add_assoc_bool(ffn_output_probe, "available", model->cuda_decoder_graph_executor_ffn_output_residual_execution_available); + add_assoc_bool(ffn_output_probe, "executed", false); + add_assoc_bool(ffn_output_probe, "retained_device_output", false); + add_assoc_string(final_norm_probe, "type", "gpu_decoder_final_norm_device_execution"); + add_assoc_bool(final_norm_probe, "attempted", false); + add_assoc_bool(final_norm_probe, "available", model->cuda_decoder_graph_executor_final_norm_execution_available); + add_assoc_bool(final_norm_probe, "executed", false); + add_assoc_bool(final_norm_probe, "retained_device_output", false); + add_assoc_string(logits_probe, "type", "gpu_decoder_logits_projection_device_execution"); + add_assoc_bool(logits_probe, "attempted", false); + add_assoc_bool(logits_probe, "available", model->cuda_decoder_graph_executor_logits_projection_execution_available); + add_assoc_bool(logits_probe, "executed", false); + add_assoc_bool(logits_probe, "retained_device_output", false); + if (king_inference_cuda_decoder_graph_executor_execute_attention_output_projection( + model, + graph, + stack_id, + stack_output, + stack_width, + projection_probe, + &projection_output, + &projection_width, + &projection_id, + executed_device_ops + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &projection_output); + return FAILURE; + } + *projection_executed = true; + projection_residual_id = projection_id; + projection_residual_output = projection_output; + projection_residual_width = projection_width; + if (king_inference_cuda_decoder_graph_executor_execute_post_rms_norm( + model, + graph, + projection_id, + projection_output, + projection_width, + projection_probe, + "attention_output", + &post_attention_norm_output, + &post_attention_norm_width, + &post_attention_norm_id, + executed_device_ops + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &projection_output); + return FAILURE; + } + if (post_attention_norm_output != 0 && post_attention_norm_width > 0 && post_attention_norm_id != NULL) { + projection_residual_id = post_attention_norm_id; + projection_residual_output = post_attention_norm_output; + projection_residual_width = post_attention_norm_width; + } + if (king_inference_cuda_decoder_graph_executor_execute_attention_residual_add( + model, + graph, + hidden_id, + hidden_output, + hidden_width, + projection_residual_id, + projection_residual_output, + projection_residual_width, + residual_probe, + &residual_output, + &residual_width, + &residual_id, + executed_device_ops + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &residual_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &projection_output); + return FAILURE; + } + *residual_executed = true; + if (post_attention_norm_output == 0 + && king_inference_cuda_decoder_graph_executor_compare_block0_attention_projection_residual( + model, + graph, + stack_id, + stack_output, + stack_width, + hidden_output, + hidden_width, + residual_output, + residual_width, + residual_probe + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &residual_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &projection_output); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_execute_ffn_norm( + model, + graph, + residual_id, + residual_output, + residual_width, + ffn_norm_probe, + &ffn_norm_output, + &ffn_norm_width, + &ffn_norm_id, + executed_device_ops + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &residual_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &projection_output); + return FAILURE; + } + *ffn_norm_executed = ffn_norm_output != 0 && ffn_norm_width > 0; + if (*ffn_norm_executed + && king_inference_cuda_decoder_graph_executor_execute_ffn_gate_up_projections( + model, + graph, + ffn_norm_id, + ffn_norm_output, + ffn_norm_width, + ffn_gate_up_probe, + &ffn_gate_output, + &ffn_up_output, + &ffn_gate_rows, + &ffn_up_rows, + &ffn_gate_id, + &ffn_up_id, + executed_device_ops + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_up_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_gate_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &residual_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &projection_output); + return FAILURE; + } + *ffn_gate_up_executed = ffn_gate_output != 0 && ffn_up_output != 0 && ffn_gate_rows == ffn_up_rows; + if (*ffn_gate_up_executed + && king_inference_cuda_decoder_graph_executor_execute_ffn_swiglu( + model, + graph, + ffn_gate_id, + ffn_gate_output, + ffn_gate_rows, + ffn_up_id, + ffn_up_output, + ffn_up_rows, + ffn_swiglu_probe, + &ffn_swiglu_output, + &ffn_swiglu_width, + &ffn_gated_id, + executed_device_ops + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_swiglu_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_up_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_gate_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &residual_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &projection_output); + return FAILURE; + } + *ffn_swiglu_executed = ffn_swiglu_output != 0 && ffn_swiglu_width > 0; + if (*ffn_swiglu_executed + && king_inference_cuda_decoder_graph_executor_execute_ffn_down_projection( + model, + graph, + ffn_gated_id, + ffn_swiglu_output, + ffn_swiglu_width, + ffn_down_probe, + &ffn_down_output, + &ffn_down_width, + &ffn_down_id, + executed_device_ops + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_down_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_swiglu_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_up_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_gate_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &residual_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &projection_output); + return FAILURE; + } + *ffn_down_executed = ffn_down_output != 0 && ffn_down_width > 0; + ffn_output_right_id = ffn_down_id; + ffn_output_right_output = ffn_down_output; + ffn_output_right_width = ffn_down_width; + if (*ffn_down_executed + && king_inference_cuda_decoder_graph_executor_execute_post_rms_norm( + model, + graph, + ffn_down_id, + ffn_down_output, + ffn_down_width, + ffn_down_probe, + "ffn_down", + &post_ffw_norm_output, + &post_ffw_norm_width, + &post_ffw_norm_id, + executed_device_ops + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_ffw_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_down_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_swiglu_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_up_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_gate_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &residual_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &projection_output); + return FAILURE; + } + if (post_ffw_norm_output != 0 && post_ffw_norm_width > 0 && post_ffw_norm_id != NULL) { + ffn_output_right_id = post_ffw_norm_id; + ffn_output_right_output = post_ffw_norm_output; + ffn_output_right_width = post_ffw_norm_width; + } + if (*ffn_down_executed + && king_inference_cuda_decoder_graph_executor_execute_ffn_output_residual_add( + model, + graph, + residual_id, + residual_output, + residual_width, + ffn_output_right_id, + ffn_output_right_output, + ffn_output_right_width, + ffn_output_probe, + &ffn_output_residual, + &ffn_output_width, + &ffn_output_id, + executed_device_ops + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_output_residual); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_ffw_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_down_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_swiglu_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_up_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_gate_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &residual_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &projection_output); + return FAILURE; + } + *ffn_output_executed = ffn_output_residual != 0 && ffn_output_width > 0; + if (*ffn_output_executed + && king_inference_cuda_decoder_graph_executor_execute_final_norm( + model, + graph, + ffn_output_id, + ffn_output_residual, + ffn_output_width, + final_norm_probe, + &final_norm_output, + &final_norm_width, + &final_norm_id, + executed_device_ops + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &final_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_output_residual); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_ffw_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_down_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_swiglu_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_up_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_gate_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &residual_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &projection_output); + return FAILURE; + } + *final_norm_executed = final_norm_output != 0 && final_norm_width > 0; + if (!*final_norm_executed + && ffn_output_residual != 0 + && ffn_output_width > 0 + && ffn_output_id != NULL + && next_hidden_id_out != NULL + && next_hidden_output != NULL + && next_hidden_width != NULL) { + *next_hidden_id_out = ffn_output_id; + *next_hidden_output = ffn_output_residual; + *next_hidden_width = ffn_output_width; + ffn_output_residual = 0; + } + if (*final_norm_executed + && king_inference_cuda_decoder_graph_executor_execute_logits_projection( + model, + graph, + final_norm_id, + final_norm_output, + final_norm_width, + logits_probe, + &logits_output, + &logits_rows, + &logits_id, + executed_device_ops + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &logits_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &final_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_output_residual); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_ffw_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_down_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_swiglu_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_up_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_gate_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &residual_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &projection_output); + return FAILURE; + } + *logits_executed = logits_output != 0 && logits_rows > 0 && logits_id != NULL; + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &logits_output) != SUCCESS) { + release_result = FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &final_norm_output) != SUCCESS) { + release_result = FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_output_residual) != SUCCESS) { + release_result = FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_ffw_norm_output) != SUCCESS) { + release_result = FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_down_output) != SUCCESS) { + release_result = FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_swiglu_output) != SUCCESS) { + release_result = FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_up_output) != SUCCESS) { + release_result = FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_gate_output) != SUCCESS) { + release_result = FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_norm_output) != SUCCESS) { + release_result = FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &residual_output) != SUCCESS) { + release_result = FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_output) != SUCCESS) { + release_result = FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &projection_output) != SUCCESS) { + release_result = FAILURE; + } + if (release_result != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_attention_residual_ffn_gate_up_swiglu_down_output_final_norm_free_failed" + ); + return FAILURE; + } + add_assoc_bool(projection_probe, "retained_device_output", false); + add_assoc_bool(projection_probe, "projection_output_released", true); + add_assoc_bool(projection_probe, "post_rms_norm_output_released", post_attention_norm_width > 0); + add_assoc_bool(residual_probe, "retained_device_output", false); + add_assoc_bool(residual_probe, "residual_output_released", true); + add_assoc_bool(ffn_norm_probe, "retained_device_output", false); + add_assoc_bool(ffn_norm_probe, "ffn_norm_output_released", *ffn_norm_executed); + add_assoc_bool(ffn_gate_up_probe, "retained_device_output", false); + add_assoc_bool(ffn_gate_up_probe, "gate_up_output_released", *ffn_gate_up_executed); + add_assoc_bool(ffn_swiglu_probe, "retained_device_output", false); + add_assoc_bool(ffn_swiglu_probe, "swiglu_output_released", *ffn_swiglu_executed); + add_assoc_bool(ffn_down_probe, "retained_device_output", false); + add_assoc_bool(ffn_down_probe, "down_output_released", *ffn_down_executed); + add_assoc_bool(ffn_down_probe, "post_rms_norm_output_released", post_ffw_norm_width > 0); + add_assoc_bool(ffn_output_probe, "retained_device_output", false); + add_assoc_bool(ffn_output_probe, "output_residual_released", *ffn_output_executed); + add_assoc_bool(final_norm_probe, "retained_device_output", false); + add_assoc_bool(final_norm_probe, "final_norm_output_released", *final_norm_executed); + add_assoc_bool(logits_probe, "retained_device_output", false); + add_assoc_bool(logits_probe, "logits_output_released", *logits_executed); + add_assoc_long(residual_probe, "projection_width", (zend_long) projection_width); + add_assoc_long(residual_probe, "residual_width", (zend_long) residual_width); + add_assoc_long(ffn_norm_probe, "residual_width", (zend_long) residual_width); + add_assoc_long(ffn_norm_probe, "ffn_norm_width", (zend_long) ffn_norm_width); + add_assoc_long(ffn_gate_up_probe, "ffn_norm_width", (zend_long) ffn_norm_width); + add_assoc_long(ffn_swiglu_probe, "gate_rows", (zend_long) ffn_gate_rows); + add_assoc_long(ffn_swiglu_probe, "up_rows", (zend_long) ffn_up_rows); + add_assoc_long(ffn_swiglu_probe, "swiglu_width", (zend_long) ffn_swiglu_width); + add_assoc_long(ffn_down_probe, "swiglu_width", (zend_long) ffn_swiglu_width); + add_assoc_long(ffn_down_probe, "down_width", (zend_long) ffn_down_width); + add_assoc_long(ffn_output_probe, "down_width", (zend_long) ffn_down_width); + add_assoc_long(ffn_output_probe, "output_width", (zend_long) ffn_output_width); + add_assoc_long(final_norm_probe, "input_width", (zend_long) ffn_output_width); + add_assoc_long(final_norm_probe, "final_norm_width", (zend_long) final_norm_width); + add_assoc_long(logits_probe, "final_norm_width", (zend_long) final_norm_width); + add_assoc_long(logits_probe, "logits_rows", (zend_long) logits_rows); + return SUCCESS; +} diff --git a/extension/src/inference/cuda/decoder_graph/ops/final/cuda_decoder_graph_executor_final_ops.inc b/extension/src/inference/cuda/decoder_graph/ops/final/cuda_decoder_graph_executor_final_ops.inc new file mode 100644 index 000000000..3d6e4dac1 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/ops/final/cuda_decoder_graph_executor_final_ops.inc @@ -0,0 +1,659 @@ +/* + * CUDA decoder graph final-stage helpers for native King GPU inference. + */ + +#include "../../device/compare/ffn/cuda_decoder_graph_executor_ffn_down_output_compare.inc" +#include "../../device/compare/norm/cuda_decoder_graph_executor_final_norm_compare.inc" +#include "../../device/compare/projection/cuda_decoder_graph_executor_logits_projection_compare.inc" + +static zval *king_inference_cuda_decoder_graph_executor_final_norm_op_for_input( + zval *graph, + zend_string *input_id +) { + zval *ops = king_inference_cuda_decoder_graph_executor_ops(graph); + zval *entry; + + if (ops == NULL || input_id == NULL) { + return NULL; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(ops), entry) { + zval *op_name; + zval *input; + zval *id; + + if (entry == NULL || Z_TYPE_P(entry) != IS_ARRAY) { + continue; + } + op_name = king_inference_array_find(entry, "op"); + input = king_inference_array_find(entry, "input"); + id = king_inference_array_find(entry, "id"); + if (op_name != NULL + && Z_TYPE_P(op_name) == IS_STRING + && zend_string_equals_literal(Z_STR_P(op_name), "rms_norm") + && id != NULL + && Z_TYPE_P(id) == IS_STRING + && zend_string_equals_literal(Z_STR_P(id), "final_norm") + && king_inference_cuda_decoder_graph_executor_zval_string_equals(input, input_id)) { + return entry; + } + } ZEND_HASH_FOREACH_END(); + + return NULL; +} + +static zend_result king_inference_cuda_decoder_graph_executor_execute_ffn_output_residual_add( + king_inference_model_object *model, + zval *graph, + zend_string *attention_residual_id, + king_inference_cuda_device_ptr attention_residual_output, + zend_ulong attention_residual_width, + zend_string *down_id, + king_inference_cuda_device_ptr down_output, + zend_ulong down_width, + zval *ffn_output_probe, + king_inference_cuda_device_ptr *output, + zend_ulong *output_width, + zend_string **output_id_out, + zend_ulong *executed_device_ops +) { + zval *output_op = king_inference_cuda_decoder_graph_executor_add_op_for_inputs(graph, attention_residual_id, down_id); + zend_string *output_id = NULL; + + *output = 0; + *output_width = 0; + if (output_id_out != NULL) { + *output_id_out = NULL; + } + add_assoc_string(ffn_output_probe, "type", "gpu_decoder_ffn_output_residual_device_execution"); + add_assoc_bool(ffn_output_probe, "attempted", output_op != NULL); + add_assoc_bool(ffn_output_probe, "available", model->cuda_decoder_graph_executor_ffn_output_residual_execution_available); + add_assoc_bool(ffn_output_probe, "executed", false); + add_assoc_bool(ffn_output_probe, "retained_device_output", false); + if (output_op == NULL) { + return SUCCESS; + } + if (!model->cuda_decoder_graph_executor_ffn_output_residual_execution_available) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_ffn_output_residual_execution_unavailable"); + return FAILURE; + } + if (attention_residual_output == 0 + || down_output == 0 + || attention_residual_width == 0 + || attention_residual_width != down_width + || attention_residual_width > (zend_ulong) (SIZE_MAX / sizeof(float)) + || king_inference_graph_required_string(output_op, "id", &output_id) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_ffn_output_residual_contract_invalid"); + return FAILURE; + } + if (king_inference_cuda_device_allocator_alloc(model, (size_t) attention_residual_width * sizeof(float), output) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_ffn_output_residual_allocation_failed" + ); + return FAILURE; + } + if (king_inference_cuda_vector_add(model, attention_residual_output, down_output, *output, attention_residual_width) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_vector_ops_result, + model->cuda_device_vector_ops_error[0] != '\0' + ? model->cuda_device_vector_ops_error + : "cuda_decoder_graph_ffn_output_residual_add_failed" + ); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_compare_block0_ffn_output_residual(model, attention_residual_id, attention_residual_output, attention_residual_width, down_id, down_output, down_width, output_id, *output, attention_residual_width, ffn_output_probe) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, output); + return FAILURE; + } + + model->cuda_decoder_graph_executor_ffn_output_residual_execution_count++; + model->cuda_decoder_graph_executor_last_ffn_output_residual_width = attention_residual_width; + *output_width = attention_residual_width; + if (output_id_out != NULL) { + *output_id_out = output_id; + } + (*executed_device_ops)++; + + add_assoc_str(ffn_output_probe, "residual", zend_string_copy(attention_residual_id)); + add_assoc_str(ffn_output_probe, "down", zend_string_copy(down_id)); + add_assoc_str(ffn_output_probe, "output", zend_string_copy(output_id)); + add_assoc_bool(ffn_output_probe, "executed", true); + add_assoc_bool(ffn_output_probe, "retained_device_output", true); + add_assoc_long(ffn_output_probe, "width", (zend_long) attention_residual_width); + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_execute_final_norm( + king_inference_model_object *model, + zval *graph, + zend_string *input_id, + king_inference_cuda_device_ptr input_output, + zend_ulong input_width, + zval *final_norm_probe, + king_inference_cuda_device_ptr *final_norm_output, + zend_ulong *final_norm_width, + zend_string **final_norm_id_out, + zend_ulong *executed_device_ops +) { + zval *final_norm_op = king_inference_cuda_decoder_graph_executor_final_norm_op_for_input(graph, input_id); + zend_string *final_norm_id = NULL; + zend_string *weight = NULL; + double epsilon = 0.000001; + size_t bytes; + + *final_norm_output = 0; + *final_norm_width = 0; + if (final_norm_id_out != NULL) { + *final_norm_id_out = NULL; + } + add_assoc_string(final_norm_probe, "type", "gpu_decoder_final_norm_device_execution"); + add_assoc_bool(final_norm_probe, "attempted", final_norm_op != NULL); + add_assoc_bool(final_norm_probe, "available", model->cuda_decoder_graph_executor_final_norm_execution_available); + add_assoc_bool(final_norm_probe, "executed", false); + add_assoc_bool(final_norm_probe, "retained_device_output", false); + if (final_norm_op == NULL) { + return SUCCESS; + } + if (!model->cuda_decoder_graph_executor_final_norm_execution_available) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_final_norm_execution_unavailable"); + return FAILURE; + } + if (input_output == 0 + || input_width == 0 + || input_width > (zend_ulong) (SIZE_MAX / sizeof(float)) + || king_inference_graph_required_string(final_norm_op, "id", &final_norm_id) != SUCCESS + || king_inference_graph_required_string(final_norm_op, "weight", &weight) != SUCCESS + || king_inference_graph_finite_double_option(final_norm_op, "epsilon", 0.000001, &epsilon) != SUCCESS + || epsilon <= 0.0) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_final_norm_contract_invalid"); + return FAILURE; + } + bytes = (size_t) input_width * sizeof(float); + if (king_inference_cuda_device_allocator_alloc(model, bytes, final_norm_output) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_final_norm_allocation_failed" + ); + return FAILURE; + } + if (king_inference_cuda_rms_norm_f32(model, weight, input_output, *final_norm_output, input_width, epsilon) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, final_norm_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_rms_norm_result, + model->cuda_rms_norm_error[0] != '\0' + ? model->cuda_rms_norm_error + : "cuda_decoder_graph_final_norm_execution_failed" + ); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_compare_final_norm(model, input_id, final_norm_id, weight, input_output, input_width, *final_norm_output, input_width, epsilon, final_norm_probe) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, final_norm_output); + return FAILURE; + } + + model->cuda_decoder_graph_executor_rms_norm_execution_count++; + model->cuda_decoder_graph_executor_final_norm_execution_count++; + model->cuda_decoder_graph_executor_last_rms_norm_width = input_width; + model->cuda_decoder_graph_executor_last_final_norm_width = input_width; + *final_norm_width = input_width; + if (final_norm_id_out != NULL) { + *final_norm_id_out = final_norm_id; + } + (*executed_device_ops)++; + + add_assoc_str(final_norm_probe, "input", zend_string_copy(input_id)); + add_assoc_str(final_norm_probe, "final_norm", zend_string_copy(final_norm_id)); + add_assoc_bool(final_norm_probe, "executed", true); + add_assoc_bool(final_norm_probe, "retained_device_output", true); + add_assoc_long(final_norm_probe, "width", (zend_long) input_width); + add_assoc_long(final_norm_probe, "bytes", (zend_long) bytes); + add_assoc_double(final_norm_probe, "epsilon", epsilon); + return SUCCESS; +} + +static zval *king_inference_cuda_decoder_graph_executor_sampling_op_for_logits( + zval *graph, + zend_string *logits_id +) { + zval *ops = king_inference_cuda_decoder_graph_executor_ops(graph); + zval *entry; + + if (ops == NULL || logits_id == NULL) { + return NULL; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(ops), entry) { + zval *op_name; + zval *logits; + + if (entry == NULL || Z_TYPE_P(entry) != IS_ARRAY) { + continue; + } + op_name = king_inference_array_find(entry, "op"); + logits = king_inference_array_find(entry, "logits"); + if (op_name != NULL + && Z_TYPE_P(op_name) == IS_STRING + && (zend_string_equals_literal(Z_STR_P(op_name), "argmax_token") + || zend_string_equals_literal(Z_STR_P(op_name), "sample_token")) + && king_inference_cuda_decoder_graph_executor_zval_string_equals(logits, logits_id)) { + return entry; + } + } ZEND_HASH_FOREACH_END(); + + return NULL; +} + +static zend_ulong king_inference_cuda_decoder_graph_executor_candidate_limit( + zval *sampling_op, + zend_ulong fallback +) { + zval *op_name; + zend_ulong top_k; + + if (fallback == 0) { + fallback = KING_INFERENCE_CUDA_LOGITS_READBACK_MAX_CANDIDATES; + } + if (sampling_op == NULL) { + return fallback; + } + op_name = king_inference_array_find(sampling_op, "op"); + if (op_name != NULL + && Z_TYPE_P(op_name) == IS_STRING + && zend_string_equals_literal(Z_STR_P(op_name), "argmax_token")) { + return 1; + } + top_k = king_inference_tensor_option_ulong(sampling_op, "top_k", fallback); + return top_k == 0 ? fallback : top_k; +} + +static zend_result king_inference_cuda_decoder_graph_executor_add_selected_bounded_token( + zval *sampling_op, + unsigned int *host_indices, + float *host_logits, + zend_ulong candidate_count, + zval *logits_probe +) { + zval *op_name; + zend_ulong top_k = 0; + zend_ulong active_count; + zend_ulong selected_rank = 0; + zend_ulong sample_index; + zend_ulong token_offset; + zend_ulong selected_token_id; + zend_ulong i; + double temperature = 0.0; + double top_p = 1.0; + double max_score = 0.0; + double probability_sum = 0.0; + double active_sum = 0.0; + double probabilities[KING_INFERENCE_CUDA_LOGITS_READBACK_MAX_CANDIDATES]; + bool has_seed = false; + zend_long seed = 0; + + if (sampling_op == NULL + || candidate_count == 0 + || candidate_count > KING_INFERENCE_CUDA_LOGITS_READBACK_MAX_CANDIDATES + || king_inference_graph_finite_double_option(sampling_op, "temperature", 0.0, &temperature) != SUCCESS + || king_inference_graph_finite_double_option(sampling_op, "top_p", 1.0, &top_p) != SUCCESS + || king_inference_graph_sampling_seed(sampling_op, &has_seed, &seed) != SUCCESS + || king_inference_graph_sampling_top_k(sampling_op, &top_k) != SUCCESS) { + return FAILURE; + } + if (temperature < 0.0 || top_p <= 0.0 || top_p > 1.0) { + return FAILURE; + } + + op_name = king_inference_array_find(sampling_op, "op"); + if (op_name != NULL + && Z_TYPE_P(op_name) == IS_STRING + && zend_string_equals_literal(Z_STR_P(op_name), "argmax_token")) { + token_offset = king_inference_tensor_option_ulong(sampling_op, "token_offset", 0); + if (token_offset > ZEND_ULONG_MAX - (zend_ulong) host_indices[0]) { + return FAILURE; + } + selected_token_id = token_offset + (zend_ulong) host_indices[0]; + add_assoc_bool(logits_probe, "selected_token_ready", true); + add_assoc_long(logits_probe, "selected_token_id", (zend_long) selected_token_id); + add_assoc_double(logits_probe, "selected_token_probability", 1.0); + add_assoc_double(logits_probe, "selected_token_logit", (double) host_logits[0]); + add_assoc_double(logits_probe, "selected_token_rank", 0.0); + add_assoc_string(logits_probe, "selected_token_source", "bounded_readback_argmax"); + add_assoc_string(logits_probe, "sampler_mode", "argmax"); + add_assoc_bool(logits_probe, "sampler_deterministic", true); + add_assoc_long(logits_probe, "sampler_active_candidate_count", 1); + add_assoc_bool(logits_probe, "bounded_top_k_selected_from_rank_zero", true); + return SUCCESS; + } + if (temperature == 0.0) { + token_offset = king_inference_tensor_option_ulong(sampling_op, "token_offset", 0); + if (token_offset > ZEND_ULONG_MAX - (zend_ulong) host_indices[0]) { + return FAILURE; + } + selected_token_id = token_offset + (zend_ulong) host_indices[0]; + add_assoc_bool(logits_probe, "selected_token_ready", true); + add_assoc_long(logits_probe, "selected_token_id", (zend_long) selected_token_id); + add_assoc_double(logits_probe, "selected_token_probability", 1.0); + add_assoc_double(logits_probe, "selected_token_logit", (double) host_logits[0]); + add_assoc_double(logits_probe, "selected_token_rank", 0.0); + add_assoc_string(logits_probe, "selected_token_source", "bounded_readback_temperature_zero"); + add_assoc_string(logits_probe, "sampler_mode", "temperature_zero_argmax"); + add_assoc_bool(logits_probe, "sampler_deterministic", true); + add_assoc_long(logits_probe, "sampler_active_candidate_count", 1); + add_assoc_bool(logits_probe, "bounded_top_k_selected_from_rank_zero", true); + return SUCCESS; + } + + for (i = 0; i < candidate_count; i++) { + double score = (double) host_logits[i] / temperature; + + probabilities[i] = score; + if (i == 0 || score > max_score) { + max_score = score; + } + } + for (i = 0; i < candidate_count; i++) { + probabilities[i] = king_inference_graph_exp(probabilities[i] - max_score); + probability_sum += probabilities[i]; + } + if (probability_sum <= 0.0) { + return FAILURE; + } + for (i = 0; i < candidate_count; i++) { + probabilities[i] /= probability_sum; + } + + active_count = top_k == 0 || top_k > candidate_count ? candidate_count : top_k; + probability_sum = 0.0; + for (i = 0; i < active_count; i++) { + probability_sum += probabilities[i]; + if (probability_sum >= top_p) { + active_count = i + 1; + break; + } + } + for (i = 0; i < active_count; i++) { + active_sum += probabilities[i]; + } + if (active_sum <= 0.0) { + return FAILURE; + } + if (has_seed) { + double target; + double cumulative = 0.0; + + sample_index = king_inference_tensor_option_ulong(sampling_op, "sample_index", 0); + target = king_inference_graph_sampling_unit(seed, candidate_count ^ (active_count << 7) ^ sample_index) * active_sum; + selected_rank = active_count - 1; + for (i = 0; i < active_count; i++) { + cumulative += probabilities[i]; + if (target <= cumulative) { + selected_rank = i; + break; + } + } + } + + token_offset = king_inference_tensor_option_ulong(sampling_op, "token_offset", 0); + if (token_offset > ZEND_ULONG_MAX - (zend_ulong) host_indices[selected_rank]) { + return FAILURE; + } + selected_token_id = token_offset + (zend_ulong) host_indices[selected_rank]; + add_assoc_bool(logits_probe, "selected_token_ready", true); + add_assoc_long(logits_probe, "selected_token_id", (zend_long) selected_token_id); + add_assoc_double(logits_probe, "selected_token_probability", probabilities[selected_rank] / active_sum); + add_assoc_double(logits_probe, "selected_token_logit", (double) host_logits[selected_rank]); + add_assoc_double(logits_probe, "selected_token_rank", (double) selected_rank); + add_assoc_string(logits_probe, "selected_token_source", "bounded_readback_sampler"); + add_assoc_string(logits_probe, "sampler_mode", "sample"); + add_assoc_bool(logits_probe, "sampler_deterministic", has_seed); + add_assoc_bool(logits_probe, "sampler_seeded", has_seed); + add_assoc_long(logits_probe, "sampler_active_candidate_count", (zend_long) active_count); + add_assoc_double(logits_probe, "sampler_active_probability_sum", active_sum); + add_assoc_double(logits_probe, "sampler_temperature", temperature); + add_assoc_double(logits_probe, "sampler_top_p", top_p); + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_add_bounded_logits_readback( + king_inference_model_object *model, + zval *graph, + zend_string *logits_id, + zend_string *weight_name, + king_inference_cuda_device_ptr final_norm_output, + zend_ulong final_norm_width, + king_inference_cuda_device_ptr logits_output, + zend_ulong logits_rows, + zval *logits_probe +) { + unsigned int host_indices[KING_INFERENCE_CUDA_LOGITS_READBACK_MAX_CANDIDATES]; + float host_logits[KING_INFERENCE_CUDA_LOGITS_READBACK_MAX_CANDIDATES]; + zend_ulong candidate_count = 0; + zend_ulong requested_candidates; + zval *sampling_op = king_inference_cuda_decoder_graph_executor_sampling_op_for_logits(graph, logits_id); + zval candidates; + zend_ulong i; + bool materialize_candidates = true; + + requested_candidates = king_inference_cuda_decoder_graph_executor_candidate_limit( + sampling_op, + (zend_ulong) model->cuda_logits_readback_candidate_limit + ); + materialize_candidates = king_inference_decode_graph_bool_option( + sampling_op, + "materialize_candidates", + true + ); + add_assoc_bool(logits_probe, "bounded_readback_attempted", true); + add_assoc_bool(logits_probe, "bounded_readback_available", model->cuda_logits_readback_available); + add_assoc_bool(logits_probe, "bounded_readback_executed", false); + add_assoc_bool(logits_probe, "sampling_op_present", sampling_op != NULL); + add_assoc_bool(logits_probe, "candidate_materialization_requested", materialize_candidates); + add_assoc_bool(logits_probe, "bounded_top_k_numeric_validated", false); + add_assoc_bool(logits_probe, "bounded_top_k_ordering_validated", false); + if (sampling_op != NULL) { + zval *op_name = king_inference_array_find(sampling_op, "op"); + + if (op_name != NULL && Z_TYPE_P(op_name) == IS_STRING) { + add_assoc_str(logits_probe, "sampling_op", zend_string_copy(Z_STR_P(op_name))); + } + } + add_assoc_long(logits_probe, "requested_candidates", (zend_long) requested_candidates); + if (king_inference_cuda_logits_readback_top_k( + model, + logits_output, + logits_rows, + requested_candidates, + host_indices, + host_logits, + &candidate_count + ) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_logits_readback_result, + model->cuda_logits_readback_error[0] != '\0' + ? model->cuda_logits_readback_error + : "cuda_decoder_graph_logits_readback_failed" + ); + return FAILURE; + } + + if (sampling_op != NULL) { + if (king_inference_cuda_decoder_graph_executor_add_selected_bounded_token( + sampling_op, + host_indices, + host_logits, + candidate_count, + logits_probe + ) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_bounded_token_selection_failed"); + return FAILURE; + } + } else { + add_assoc_bool(logits_probe, "selected_token_ready", false); + } + if (king_inference_cuda_decoder_graph_executor_compare_bounded_logits_projection( + model, + weight_name, + final_norm_output, + final_norm_width, + logits_rows, + host_indices, + host_logits, + candidate_count, + logits_probe + ) != SUCCESS) { + return FAILURE; + } + + add_assoc_bool(logits_probe, "bounded_readback_executed", true); + add_assoc_long(logits_probe, "candidate_count", (zend_long) candidate_count); + add_assoc_long(logits_probe, "readback_bytes", (zend_long) model->cuda_logits_readback_last_bytes); + add_assoc_long(logits_probe, "full_logits_bytes", (zend_long) model->cuda_logits_readback_full_bytes); + add_assoc_long(logits_probe, "saved_readback_bytes", (zend_long) model->cuda_logits_readback_saved_bytes); + if (!materialize_candidates) { + add_assoc_bool(logits_probe, "candidates_materialized", false); + return SUCCESS; + } + + array_init(&candidates); + for (i = 0; i < candidate_count; i++) { + zval candidate; + + array_init(&candidate); + add_assoc_long(&candidate, "rank", (zend_long) i); + add_assoc_long(&candidate, "token_id", (zend_long) host_indices[i]); + add_assoc_double(&candidate, "logit", (double) host_logits[i]); + add_next_index_zval(&candidates, &candidate); + } + add_assoc_bool(logits_probe, "candidates_materialized", true); + add_assoc_zval(logits_probe, "candidates", &candidates); + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_execute_logits_projection( + king_inference_model_object *model, + zval *graph, + zend_string *final_norm_id, + king_inference_cuda_device_ptr final_norm_output, + zend_ulong final_norm_width, + zval *logits_probe, + king_inference_cuda_device_ptr *logits_output, + zend_ulong *logits_rows, + zend_string **logits_id_out, + zend_ulong *executed_device_ops +) { + zval *logits_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, final_norm_id, "linear"); + zend_string *logits_id = NULL; + zend_string *weight = NULL; + zend_string *resolved_weight = NULL; + const char *projection_status = NULL; + size_t bytes; + + *logits_output = 0; + *logits_rows = 0; + if (logits_id_out != NULL) { + *logits_id_out = NULL; + } + add_assoc_string(logits_probe, "type", "gpu_decoder_logits_projection_device_execution"); + add_assoc_bool(logits_probe, "attempted", logits_op != NULL); + add_assoc_bool(logits_probe, "available", model->cuda_decoder_graph_executor_logits_projection_execution_available); + add_assoc_bool(logits_probe, "executed", false); + add_assoc_bool(logits_probe, "retained_device_output", false); + if (logits_op == NULL) { + return SUCCESS; + } + if (!model->cuda_decoder_graph_executor_logits_projection_execution_available) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_logits_projection_execution_unavailable"); + return FAILURE; + } + if (final_norm_output == 0 + || final_norm_width == 0 + || final_norm_width > (zend_ulong) (SIZE_MAX / sizeof(float)) + || king_inference_graph_required_string(logits_op, "id", &logits_id) != SUCCESS + || !zend_string_equals_literal(logits_id, "logits") + || king_inference_cuda_decoder_graph_executor_linear_shape(model, logits_op, final_norm_width, &weight, logits_rows) != SUCCESS + || *logits_rows == 0 + || *logits_rows > (zend_ulong) (SIZE_MAX / sizeof(float))) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_logits_projection_contract_invalid"); + return FAILURE; + } + if (!king_inference_cuda_output_projection_resolve(model, &resolved_weight, NULL, &projection_status, NULL) + || resolved_weight == NULL) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_output_projection_result, + projection_status != NULL ? projection_status : "cuda_decoder_graph_logits_projection_weight_unresolved" + ); + return FAILURE; + } + if (!zend_string_equals(resolved_weight, weight)) { + zend_string_release(resolved_weight); + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_logits_projection_weight_mismatch"); + return FAILURE; + } + zend_string_release(resolved_weight); + + bytes = (size_t) *logits_rows * sizeof(float); + if (king_inference_cuda_device_allocator_alloc(model, bytes, logits_output) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_logits_projection_allocation_failed" + ); + return FAILURE; + } + if (king_inference_cuda_output_projection_q8_0(model, final_norm_output, final_norm_width, *logits_output, *logits_rows) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, logits_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_output_projection_result, + model->cuda_output_projection_error[0] != '\0' + ? model->cuda_output_projection_error + : "cuda_decoder_graph_logits_projection_execution_failed" + ); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_add_bounded_logits_readback( + model, + graph, + logits_id, + weight, + final_norm_output, + final_norm_width, + *logits_output, + *logits_rows, + logits_probe + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, logits_output); + return FAILURE; + } + + model->cuda_decoder_graph_executor_logits_projection_execution_count++; + model->cuda_decoder_graph_executor_last_logits_rows = *logits_rows; + if (logits_id_out != NULL) { + *logits_id_out = logits_id; + } + (*executed_device_ops)++; + + add_assoc_str(logits_probe, "input", zend_string_copy(final_norm_id)); + add_assoc_str(logits_probe, "logits", zend_string_copy(logits_id)); + add_assoc_str(logits_probe, "weight", zend_string_copy(weight)); + add_assoc_bool(logits_probe, "executed", true); + add_assoc_bool(logits_probe, "retained_device_output", true); + add_assoc_long(logits_probe, "input_width", (zend_long) final_norm_width); + add_assoc_long(logits_probe, "rows", (zend_long) *logits_rows); + add_assoc_long(logits_probe, "bytes", (zend_long) bytes); + return SUCCESS; +} diff --git a/extension/src/inference/cuda/decoder_graph/ops/kv/cuda_decoder_graph_executor_kv_ops.inc b/extension/src/inference/cuda/decoder_graph/ops/kv/cuda_decoder_graph_executor_kv_ops.inc new file mode 100644 index 000000000..b4b1d5df1 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/ops/kv/cuda_decoder_graph_executor_kv_ops.inc @@ -0,0 +1,664 @@ +/* + * CUDA decoder graph K/V head preparation helpers for native King GPU inference. + */ + +#include "../../device/compare/matvec/cuda_decoder_graph_executor_matvec_compare_helpers.inc" +#include "../../device/compare/projection/cuda_decoder_graph_executor_qkv_projection_compare.inc" + +static zend_result king_inference_cuda_decoder_graph_executor_linear_to_device( + king_inference_model_object *model, + zval *linear_op, + king_inference_cuda_device_ptr input, + zend_ulong input_width, + king_inference_cuda_device_ptr *output, + zend_ulong *rows +) { + zend_string *weight = NULL; + + *output = 0; + *rows = 0; + if (linear_op == NULL + || input == 0 + || king_inference_cuda_decoder_graph_executor_linear_shape( + model, + linear_op, + input_width, + &weight, + rows + ) != SUCCESS + || *rows > (zend_ulong) (SIZE_MAX / sizeof(float))) { + return FAILURE; + } + if (king_inference_cuda_device_allocator_alloc(model, (size_t) *rows * sizeof(float), output) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_kv_linear_allocation_failed" + ); + return FAILURE; + } + if (king_inference_cuda_quantized_matvec(model, weight, input, input_width, *output, *rows) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_quantized_matvec_result, + model->cuda_quantized_matvec_error[0] != '\0' + ? model->cuda_quantized_matvec_error + : "cuda_decoder_graph_kv_linear_execution_failed" + ); + return FAILURE; + } + + model->cuda_decoder_graph_executor_linear_execution_count++; + model->cuda_decoder_graph_executor_last_linear_input_width = input_width; + model->cuda_decoder_graph_executor_last_linear_rows = *rows; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_slice_to_device( + king_inference_model_object *model, + zval *slice_op, + king_inference_cuda_device_ptr input, + zend_ulong input_length, + king_inference_cuda_device_ptr *output, + zend_ulong *offset, + zend_ulong *length +) { + *output = 0; + *offset = 0; + *length = 0; + if (slice_op == NULL + || input == 0 + || king_inference_cuda_decoder_graph_executor_slice_shape( + model, + slice_op, + input_length, + offset, + length + ) != SUCCESS) { + return FAILURE; + } + if (king_inference_cuda_device_allocator_alloc(model, (size_t) *length * sizeof(float), output) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_kv_slice_allocation_failed" + ); + return FAILURE; + } + if (king_inference_cuda_vector_slice(model, input, *offset, *output, *length) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_vector_ops_result, + model->cuda_device_vector_ops_error[0] != '\0' + ? model->cuda_device_vector_ops_error + : "cuda_decoder_graph_kv_slice_execution_failed" + ); + return FAILURE; + } + + model->cuda_decoder_graph_executor_slice_execution_count++; + model->cuda_decoder_graph_executor_last_slice_offset = *offset; + model->cuda_decoder_graph_executor_last_slice_length = *length; + return SUCCESS; +} + +static zval *king_inference_cuda_decoder_graph_executor_kv_write_op_for_vectors( + zval *graph, + zend_string *key_id, + zend_string *value_id +) { + zval *ops = king_inference_cuda_decoder_graph_executor_ops(graph); + zval *entry; + + if (ops == NULL || key_id == NULL || value_id == NULL) { + return NULL; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(ops), entry) { + zval *op_name; + zval *key; + zval *value; + + if (entry == NULL || Z_TYPE_P(entry) != IS_ARRAY) { + continue; + } + op_name = king_inference_array_find(entry, "op"); + key = king_inference_array_find(entry, "key"); + value = king_inference_array_find(entry, "value"); + if (op_name != NULL + && Z_TYPE_P(op_name) == IS_STRING + && zend_string_equals_literal(Z_STR_P(op_name), "kv_write") + && king_inference_cuda_decoder_graph_executor_zval_string_equals(key, key_id) + && king_inference_cuda_decoder_graph_executor_zval_string_equals(value, value_id)) { + return entry; + } + } ZEND_HASH_FOREACH_END(); + + return NULL; +} + +#include "cuda_decoder_graph_executor_kv_write_ops.inc" + +#include "../attention/cuda_decoder_graph_executor_attention_ops.inc" +#include "../residual/cuda_decoder_graph_executor_stack_ops.inc" +#include "../../prefill/kv/cuda_decoder_graph_executor_prefilled_kv_ops.inc" + +static zend_result king_inference_cuda_decoder_graph_executor_execute_kv_head_prepare( + king_inference_model_object *model, + zval *graph, + zend_string *rms_norm_id, + zend_string *rms_weight_tensor, + king_inference_cuda_device_ptr rms_norm_output, + zend_ulong width, + zend_ulong token_id, + double epsilon, + zend_string *query_slice_id, + king_inference_cuda_device_ptr query_slice_output, + zend_ulong query_slice_length, + zval *query_rope_probe, + zval *kv_probe, + king_inference_cuda_device_ptr *attention_stack_output, + zend_ulong *attention_stack_width, + zend_string **attention_stack_id, + zend_ulong *executed_device_ops, + bool *kv_prepared +) { + zval *key_linear_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, rms_norm_id, "linear", 1); + zval *value_linear_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, rms_norm_id, "linear", 2); + zval *key_slice_op = NULL; + zval *value_slice_op = NULL; + zval *kv_write_op = NULL; + zval *kv_attention_op = NULL; + zval key_rope_probe; + zval kv_write_probe; + zval kv_attention_probe; + zval attention_stack_probe; + zend_string *key_linear_id = NULL; + zend_string *key_slice_id = NULL; + zend_string *key_rope_id = NULL; + zend_string *value_linear_id = NULL; + zend_string *value_slice_id = NULL; + zend_string *query_rope_id = NULL; + zend_string *kv_attention_id = NULL; + king_inference_cuda_device_ptr key_linear_output = 0; + king_inference_cuda_device_ptr value_linear_output = 0; + king_inference_cuda_device_ptr key_slice_output = 0; + king_inference_cuda_device_ptr key_rope_output = 0; + king_inference_cuda_device_ptr value_slice_output = 0; + king_inference_cuda_device_ptr query_rope_output = 0; + king_inference_cuda_device_ptr attention_context_output = 0; + zend_ulong key_rows = 0; + zend_ulong value_rows = 0; + zend_ulong key_slice_offset = 0; + zend_ulong key_slice_length = 0; + zend_ulong value_slice_offset = 0; + zend_ulong value_slice_length = 0; + zend_ulong key_rope_length = 0; + zend_ulong query_rope_length = 0; + zend_ulong attention_context_length = 0; + bool key_rope_executed = false; + bool query_rope_executed = false; + bool used_prefilled_kv = false; + + *kv_prepared = false; + add_assoc_bool(kv_probe, "attempted", key_linear_op != NULL || value_linear_op != NULL); + if (key_linear_op == NULL && value_linear_op == NULL) { + return SUCCESS; + } + if (!model->cuda_decoder_graph_executor_kv_head_prepare_execution_available) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_kv_head_prepare_execution_unavailable" + ); + return FAILURE; + } + if (key_linear_op == NULL + || value_linear_op == NULL + || king_inference_graph_required_string(key_linear_op, "id", &key_linear_id) != SUCCESS + || king_inference_graph_required_string(value_linear_op, "id", &value_linear_id) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_kv_linear_contract_invalid"); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_try_prefilled_kv_head_prepare( + model, + graph, + key_linear_id, + value_linear_id, + query_slice_id, + query_slice_output, + query_slice_length, + query_rope_probe, + kv_probe, + attention_stack_output, + attention_stack_width, + attention_stack_id, + executed_device_ops, + kv_prepared, + &used_prefilled_kv + ) != SUCCESS) { + return FAILURE; + } + if (used_prefilled_kv) { + return SUCCESS; + } + if (king_inference_cuda_decoder_graph_executor_linear_to_device( + model, + key_linear_op, + rms_norm_output, + width, + &key_linear_output, + &key_rows + ) != SUCCESS) { + return FAILURE; + } + (*executed_device_ops)++; + king_inference_cuda_decoder_graph_executor_add_qkv_projection_execution_probe( + model, + kv_probe, + "key_projection_device_execution", + "gpu_decoder_key_projection_device_execution", + token_id, + rms_weight_tensor, + key_linear_op, + key_linear_output, + width, + key_rows, + epsilon + ); + key_slice_op = king_inference_cuda_decoder_graph_executor_kv_slice_op_for_query( + graph, + key_linear_id, + query_slice_id, + "k" + ); + if (key_slice_op == NULL || king_inference_graph_required_string(key_slice_op, "id", &key_slice_id) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_linear_output); + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_kv_key_slice_contract_invalid"); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_slice_to_device( + model, + key_slice_op, + key_linear_output, + key_rows, + &key_slice_output, + &key_slice_offset, + &key_slice_length + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_linear_output); + return FAILURE; + } + (*executed_device_ops)++; + array_init(&key_rope_probe); + add_assoc_string(&key_rope_probe, "type", "gpu_decoder_key_rope_device_execution"); + add_assoc_bool(&key_rope_probe, "attempted", false); + add_assoc_bool(&key_rope_probe, "available", model->cuda_decoder_graph_executor_rope_execution_available); + add_assoc_bool(&key_rope_probe, "executed", false); + add_assoc_bool(&key_rope_probe, "retained_device_output", false); + if (king_inference_cuda_decoder_graph_executor_execute_rope_after_slice_to_device( + model, + graph, + key_slice_id, + key_slice_output, + key_slice_length, + &key_rope_probe, + executed_device_ops, + &key_rope_output, + &key_rope_length, + &key_rope_id, + &key_rope_executed + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_linear_output); + zval_ptr_dtor(&key_rope_probe); + return FAILURE; + } + if (!key_rope_executed) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_linear_output); + zval_ptr_dtor(&key_rope_probe); + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_kv_key_rope_missing"); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_linear_to_device( + model, + value_linear_op, + rms_norm_output, + width, + &value_linear_output, + &value_rows + ) != SUCCESS) { + zval_ptr_dtor(&key_rope_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_rope_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_linear_output); + return FAILURE; + } + (*executed_device_ops)++; + king_inference_cuda_decoder_graph_executor_add_qkv_projection_execution_probe( + model, + kv_probe, + "value_projection_device_execution", + "gpu_decoder_value_projection_device_execution", + token_id, + rms_weight_tensor, + value_linear_op, + value_linear_output, + width, + value_rows, + epsilon + ); + value_slice_op = king_inference_cuda_decoder_graph_executor_kv_slice_op_for_query( + graph, + value_linear_id, + query_slice_id, + "v" + ); + if (value_slice_op == NULL || king_inference_graph_required_string(value_slice_op, "id", &value_slice_id) != SUCCESS) { + zval_ptr_dtor(&key_rope_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_linear_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_rope_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_linear_output); + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_kv_value_slice_contract_invalid"); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_slice_to_device( + model, + value_slice_op, + value_linear_output, + value_rows, + &value_slice_output, + &value_slice_offset, + &value_slice_length + ) != SUCCESS) { + zval_ptr_dtor(&key_rope_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_linear_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_rope_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_linear_output); + return FAILURE; + } + (*executed_device_ops)++; + + kv_write_op = king_inference_cuda_decoder_graph_executor_kv_write_op_for_vectors( + graph, + key_rope_id, + value_slice_id + ); + array_init(&kv_write_probe); + add_assoc_string(&kv_write_probe, "type", "gpu_decoder_kv_write_device_execution"); + if (king_inference_cuda_decoder_graph_executor_execute_kv_write( + model, + kv_write_op, + query_slice_id, + key_rope_output, + key_rope_length, + value_slice_output, + value_slice_length, + &kv_write_probe, + executed_device_ops + ) != SUCCESS) { + zval_ptr_dtor(&kv_write_probe); + zval_ptr_dtor(&key_rope_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_linear_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_rope_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_linear_output); + return FAILURE; + } + + if (query_slice_id == NULL || query_slice_output == 0) { + zval_ptr_dtor(&kv_write_probe); + zval_ptr_dtor(&key_rope_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_linear_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_rope_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_linear_output); + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_kv_attention_query_rope_contract_invalid"); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_execute_rope_after_slice_to_device( + model, + graph, + query_slice_id, + query_slice_output, + query_slice_length, + query_rope_probe, + executed_device_ops, + &query_rope_output, + &query_rope_length, + &query_rope_id, + &query_rope_executed + ) != SUCCESS) { + zval_ptr_dtor(&kv_write_probe); + zval_ptr_dtor(&key_rope_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &query_rope_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_linear_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_rope_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_linear_output); + return FAILURE; + } + if (!query_rope_executed) { + zval_ptr_dtor(&kv_write_probe); + zval_ptr_dtor(&key_rope_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &query_rope_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_linear_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_rope_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_linear_output); + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_kv_attention_query_rope_missing"); + return FAILURE; + } + + kv_attention_op = king_inference_cuda_decoder_graph_executor_kv_attention_op_for_query(graph, query_rope_id); + array_init(&kv_attention_probe); + add_assoc_string(&kv_attention_probe, "type", "gpu_decoder_kv_attention_device_execution"); + if (king_inference_cuda_decoder_graph_executor_execute_kv_attention( + model, + kv_attention_op, + query_rope_output, + query_rope_length, + &kv_attention_probe, + &attention_context_output, + &attention_context_length, + executed_device_ops + ) != SUCCESS) { + zval_ptr_dtor(&kv_attention_probe); + zval_ptr_dtor(&kv_write_probe); + zval_ptr_dtor(&key_rope_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &query_rope_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &attention_context_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_linear_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_rope_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_linear_output); + return FAILURE; + } + if (kv_attention_op == NULL + || king_inference_graph_required_string(kv_attention_op, "id", &kv_attention_id) != SUCCESS) { + zval_ptr_dtor(&kv_attention_probe); + zval_ptr_dtor(&kv_write_probe); + zval_ptr_dtor(&key_rope_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &query_rope_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &attention_context_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_linear_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_rope_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_linear_output); + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_kv_attention_id_invalid"); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &query_rope_output) != SUCCESS) { + zval_ptr_dtor(&kv_attention_probe); + zval_ptr_dtor(&kv_write_probe); + zval_ptr_dtor(&key_rope_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &attention_context_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_linear_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_rope_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_linear_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_kv_attention_query_rope_free_failed" + ); + return FAILURE; + } + add_assoc_bool(query_rope_probe, "retained_device_output", false); + array_init(&attention_stack_probe); + add_assoc_string(&attention_stack_probe, "type", "gpu_decoder_attention_stack_slot_device_execution"); + if (king_inference_cuda_decoder_graph_executor_prepare_attention_stack_slot( + model, + graph, + kv_attention_id, + attention_context_output, + attention_context_length, + &attention_stack_probe, + attention_stack_output, + attention_stack_width, + attention_stack_id, + executed_device_ops + ) != SUCCESS) { + zval_ptr_dtor(&attention_stack_probe); + zval_ptr_dtor(&kv_attention_probe); + zval_ptr_dtor(&kv_write_probe); + zval_ptr_dtor(&key_rope_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &attention_context_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_linear_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_rope_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_linear_output); + return FAILURE; + } + add_assoc_long(kv_probe, "attention_context_length", (zend_long) attention_context_length); + add_assoc_long(kv_probe, "attention_stack_width", (zend_long) *attention_stack_width); + add_assoc_bool(&key_rope_probe, "retained_device_output", false); + + model->cuda_decoder_graph_executor_kv_head_prepare_execution_count++; + model->cuda_decoder_graph_executor_last_kv_head_key_length = key_slice_length; + model->cuda_decoder_graph_executor_last_kv_head_value_length = value_slice_length; + *kv_prepared = true; + + add_assoc_bool(kv_probe, "executed", true); + add_assoc_bool(kv_probe, "key_rope_executed", key_rope_executed); + add_assoc_stringl(kv_probe, "key_linear_id", ZSTR_VAL(key_linear_id), ZSTR_LEN(key_linear_id)); + add_assoc_stringl(kv_probe, "key_slice_id", ZSTR_VAL(key_slice_id), ZSTR_LEN(key_slice_id)); + add_assoc_stringl(kv_probe, "key_rope_id", ZSTR_VAL(key_rope_id), ZSTR_LEN(key_rope_id)); + add_assoc_long(kv_probe, "key_linear_rows", (zend_long) key_rows); + add_assoc_long(kv_probe, "key_slice_offset", (zend_long) key_slice_offset); + add_assoc_long(kv_probe, "key_slice_length", (zend_long) key_slice_length); + add_assoc_long(kv_probe, "key_rope_length", (zend_long) key_rope_length); + add_assoc_stringl(kv_probe, "value_linear_id", ZSTR_VAL(value_linear_id), ZSTR_LEN(value_linear_id)); + add_assoc_stringl(kv_probe, "value_slice_id", ZSTR_VAL(value_slice_id), ZSTR_LEN(value_slice_id)); + add_assoc_long(kv_probe, "value_linear_rows", (zend_long) value_rows); + add_assoc_long(kv_probe, "value_slice_offset", (zend_long) value_slice_offset); + add_assoc_long(kv_probe, "value_slice_length", (zend_long) value_slice_length); + + add_assoc_bool(kv_probe, "attention_stack_released", false); + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &attention_context_output) != SUCCESS) { + zval_ptr_dtor(&attention_stack_probe); + zval_ptr_dtor(&kv_attention_probe); + zval_ptr_dtor(&kv_write_probe); + zval_ptr_dtor(&key_rope_probe); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_kv_attention_context_free_failed" + ); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_linear_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_rope_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_linear_output); + return FAILURE; + } + add_assoc_bool(&kv_attention_probe, "retained_device_output", false); + add_assoc_bool(kv_probe, "attention_context_released", true); + add_assoc_zval(kv_probe, "key_rope_execution", &key_rope_probe); + add_assoc_zval(kv_probe, "kv_write_execution", &kv_write_probe); + add_assoc_zval(kv_probe, "kv_attention_execution", &kv_attention_probe); + add_assoc_zval(kv_probe, "attention_stack_slot_execution", &attention_stack_probe); + + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_slice_output) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_kv_head_prepare_free_failed" + ); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_linear_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_rope_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_linear_output); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_linear_output) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_kv_head_prepare_free_failed" + ); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_rope_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_linear_output); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_rope_output) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_kv_head_prepare_free_failed" + ); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_slice_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_linear_output); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_slice_output) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_kv_head_prepare_free_failed" + ); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_linear_output); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_linear_output) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_kv_head_prepare_free_failed" + ); + return FAILURE; + } + return SUCCESS; +} diff --git a/extension/src/inference/cuda/decoder_graph/ops/kv/cuda_decoder_graph_executor_kv_slice_lookup.inc b/extension/src/inference/cuda/decoder_graph/ops/kv/cuda_decoder_graph_executor_kv_slice_lookup.inc new file mode 100644 index 000000000..e566a68fe --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/ops/kv/cuda_decoder_graph_executor_kv_slice_lookup.inc @@ -0,0 +1,72 @@ +/* + * CUDA decoder graph K/V slice lookup helpers. + */ + +static bool king_inference_cuda_decoder_graph_executor_query_slice_identity( + zend_string *query_slice_id, + zend_ulong *layer, + zend_ulong *head +) { + unsigned long parsed_layer = 0; + unsigned long parsed_head = 0; + int consumed = 0; + + *layer = 0; + *head = 0; + if (query_slice_id == NULL + || ZSTR_LEN(query_slice_id) > INT_MAX + || sscanf(ZSTR_VAL(query_slice_id), "l%lu_h%lu_q_slice%n", &parsed_layer, &parsed_head, &consumed) != 2 + || consumed != (int) ZSTR_LEN(query_slice_id)) { + return false; + } + + *layer = (zend_ulong) parsed_layer; + *head = (zend_ulong) parsed_head; + return true; +} + +static zval *king_inference_cuda_decoder_graph_executor_kv_slice_op_for_query( + zval *graph, + zend_string *linear_id, + zend_string *query_slice_id, + const char *kind +) { + zval *ops = king_inference_cuda_decoder_graph_executor_ops(graph); + zval *entry; + zend_ulong layer = 0; + zend_ulong head = 0; + zend_string *expected_id; + + if (ops == NULL + || linear_id == NULL + || query_slice_id == NULL + || kind == NULL + || !king_inference_cuda_decoder_graph_executor_query_slice_identity(query_slice_id, &layer, &head)) { + return NULL; + } + + expected_id = strpprintf(0, "l%lu_h%lu_%s_slice", (unsigned long) layer, (unsigned long) head, kind); + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(ops), entry) { + zval *op_name; + zval *op_id; + zval *input; + + if (entry == NULL || Z_TYPE_P(entry) != IS_ARRAY) { + continue; + } + op_name = king_inference_array_find(entry, "op"); + op_id = king_inference_array_find(entry, "id"); + input = king_inference_array_find(entry, "input"); + if (op_name != NULL + && Z_TYPE_P(op_name) == IS_STRING + && zend_string_equals_literal(Z_STR_P(op_name), "slice") + && king_inference_cuda_decoder_graph_executor_zval_string_equals(op_id, expected_id) + && king_inference_cuda_decoder_graph_executor_zval_string_equals(input, linear_id)) { + zend_string_release(expected_id); + return entry; + } + } ZEND_HASH_FOREACH_END(); + + zend_string_release(expected_id); + return NULL; +} diff --git a/extension/src/inference/cuda/decoder_graph/ops/kv/cuda_decoder_graph_executor_kv_write_ops.inc b/extension/src/inference/cuda/decoder_graph/ops/kv/cuda_decoder_graph_executor_kv_write_ops.inc new file mode 100644 index 000000000..46708facf --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/ops/kv/cuda_decoder_graph_executor_kv_write_ops.inc @@ -0,0 +1,213 @@ +/* + * CUDA decoder graph K/V write helpers for native King GPU inference. + */ + +static bool king_inference_cuda_decoder_graph_executor_query_slice_head( + zend_string *query_slice_id, + zend_ulong *layer, + zend_ulong *query_head +) { + unsigned long parsed_layer = 0; + unsigned long parsed_head = 0; + int consumed = 0; + + *layer = 0; + *query_head = 0; + return query_slice_id != NULL + && ZSTR_LEN(query_slice_id) <= INT_MAX + && sscanf(ZSTR_VAL(query_slice_id), "l%lu_h%lu_q_slice%n", &parsed_layer, &parsed_head, &consumed) == 2 + && consumed == (int) ZSTR_LEN(query_slice_id) + && ((*layer = (zend_ulong) parsed_layer), (*query_head = (zend_ulong) parsed_head), true); +} + +static zend_result king_inference_cuda_decoder_graph_executor_kv_write_shape( + king_inference_model_object *model, + zval *kv_write_op, + zend_ulong key_length, + zend_ulong value_length, + zend_ulong *layer, + zend_ulong *head, + zend_ulong *position +) { + zend_string *cache = NULL; + zval *slot; + unsigned long parsed_layer = 0; + unsigned long parsed_head = 0; + int consumed = 0; + + *layer = 0; + *head = 0; + *position = 0; + if (kv_write_op == NULL + || king_inference_graph_required_string(kv_write_op, "cache", &cache) != SUCCESS + || ZSTR_LEN(cache) > INT_MAX + || sscanf(ZSTR_VAL(cache), "l%lu.h%lu%n", &parsed_layer, &parsed_head, &consumed) != 2 + || consumed != (int) ZSTR_LEN(cache)) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_kv_write_cache_invalid"); + return FAILURE; + } + + slot = king_inference_array_find(kv_write_op, "slot"); + if (slot == NULL || Z_TYPE_P(slot) != IS_LONG || Z_LVAL_P(slot) < 0) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_kv_write_slot_invalid"); + return FAILURE; + } + if (!model->cuda_device_kv_cache_available + || key_length == 0 + || value_length == 0 + || key_length > model->cuda_device_kv_cache_key_length + || value_length > model->cuda_device_kv_cache_value_length + || (zend_ulong) parsed_layer >= model->cuda_device_kv_cache_layers + || (zend_ulong) parsed_head >= model->cuda_device_kv_cache_kv_heads + || (zend_ulong) Z_LVAL_P(slot) >= model->cuda_device_kv_cache_context_tokens) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_kv_write_shape_invalid"); + return FAILURE; + } + + *layer = (zend_ulong) parsed_layer; + *head = (zend_ulong) parsed_head; + *position = (zend_ulong) Z_LVAL_P(slot); + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_kv_write_offsets( + king_inference_model_object *model, + zend_ulong layer, + zend_ulong head, + zend_ulong position, + size_t *slot, + size_t *key_offset_bytes, + size_t *value_offset_bytes +) { + size_t computed_slot; + size_t key_offset; + size_t value_offset; + + *slot = 0; + *key_offset_bytes = 0; + *value_offset_bytes = 0; + if (!king_inference_cuda_device_kv_cache_mul_size((size_t) layer, (size_t) model->cuda_device_kv_cache_kv_heads, &computed_slot) + || !king_inference_cuda_device_kv_cache_add_size(computed_slot, (size_t) head, &computed_slot) + || !king_inference_cuda_device_kv_cache_mul_size(computed_slot, (size_t) model->cuda_device_kv_cache_context_tokens, &computed_slot) + || !king_inference_cuda_device_kv_cache_add_size(computed_slot, (size_t) position, &computed_slot) + || !king_inference_cuda_device_kv_cache_mul_size(computed_slot, (size_t) model->cuda_device_kv_cache_key_length, &key_offset) + || !king_inference_cuda_device_kv_cache_mul_size(key_offset, sizeof(float), key_offset_bytes) + || !king_inference_cuda_device_kv_cache_mul_size(computed_slot, (size_t) model->cuda_device_kv_cache_value_length, &value_offset) + || !king_inference_cuda_device_kv_cache_mul_size(value_offset, sizeof(float), value_offset_bytes)) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_kv_write_offset_overflow"); + return FAILURE; + } + + *slot = computed_slot; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_execute_kv_write( + king_inference_model_object *model, + zval *kv_write_op, + zend_string *query_slice_id, + king_inference_cuda_device_ptr key_output, + zend_ulong key_length, + king_inference_cuda_device_ptr value_output, + zend_ulong value_length, + zval *kv_write_probe, + zend_ulong *executed_device_ops +) { + zend_ulong layer = 0; + zend_ulong head = 0; + zend_ulong position = 0; + zend_ulong query_layer = 0; + zend_ulong query_head = 0; + size_t cache_slot = 0; + size_t key_offset_bytes = 0; + size_t value_offset_bytes = 0; + bool query_head_available; + + add_assoc_bool(kv_write_probe, "attempted", kv_write_op != NULL); + add_assoc_bool(kv_write_probe, "available", model->cuda_decoder_graph_executor_kv_write_execution_available); + add_assoc_bool(kv_write_probe, "executed", false); + if (kv_write_op == NULL) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_kv_write_missing"); + return FAILURE; + } + if (!model->cuda_decoder_graph_executor_kv_write_execution_available) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_kv_write_execution_unavailable"); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_kv_write_shape( + model, + kv_write_op, + key_length, + value_length, + &layer, + &head, + &position + ) != SUCCESS + || king_inference_cuda_decoder_graph_executor_kv_write_offsets( + model, + layer, + head, + position, + &cache_slot, + &key_offset_bytes, + &value_offset_bytes + ) != SUCCESS) { + return FAILURE; + } + query_head_available = king_inference_cuda_decoder_graph_executor_query_slice_head( + query_slice_id, + &query_layer, + &query_head + ); + if (king_inference_cuda_device_kv_cache_write( + model, + layer, + head, + position, + key_output, + key_length, + value_output, + value_length + ) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_kv_cache_result, + model->cuda_device_kv_cache_error[0] != '\0' + ? model->cuda_device_kv_cache_error + : "cuda_decoder_graph_kv_write_execution_failed" + ); + return FAILURE; + } + + model->cuda_decoder_graph_executor_kv_write_execution_count++; + model->cuda_decoder_graph_executor_last_kv_write_key_length = key_length; + model->cuda_decoder_graph_executor_last_kv_write_value_length = value_length; + model->cuda_decoder_graph_executor_last_kv_write_layer = layer; + model->cuda_decoder_graph_executor_last_kv_write_head = head; + model->cuda_decoder_graph_executor_last_kv_write_position = position; + (*executed_device_ops)++; + + add_assoc_bool(kv_write_probe, "executed", true); + add_assoc_long(kv_write_probe, "layer", (zend_long) layer); + add_assoc_long(kv_write_probe, "head", (zend_long) head); + add_assoc_long(kv_write_probe, "kv_head", (zend_long) head); + add_assoc_bool(kv_write_probe, "query_head_available", query_head_available); + if (query_head_available) { + add_assoc_long(kv_write_probe, "query_layer", (zend_long) query_layer); + add_assoc_long(kv_write_probe, "query_head", (zend_long) query_head); + } else { + add_assoc_null(kv_write_probe, "query_layer"); + add_assoc_null(kv_write_probe, "query_head"); + } + add_assoc_long(kv_write_probe, "position", (zend_long) position); + add_assoc_long(kv_write_probe, "key_length", (zend_long) key_length); + add_assoc_long(kv_write_probe, "value_length", (zend_long) value_length); + add_assoc_long(kv_write_probe, "cache_key_stride", (zend_long) model->cuda_device_kv_cache_key_length); + add_assoc_long(kv_write_probe, "cache_value_stride", (zend_long) model->cuda_device_kv_cache_value_length); + add_assoc_long(kv_write_probe, "cache_slot_index", (zend_long) cache_slot); + add_assoc_long(kv_write_probe, "key_offset_bytes", (zend_long) key_offset_bytes); + add_assoc_long(kv_write_probe, "value_offset_bytes", (zend_long) value_offset_bytes); + add_assoc_long(kv_write_probe, "key_write_bytes", (zend_long) ((size_t) key_length * sizeof(float))); + add_assoc_long(kv_write_probe, "value_write_bytes", (zend_long) ((size_t) value_length * sizeof(float))); + return SUCCESS; +} diff --git a/extension/src/inference/cuda/decoder_graph/ops/norm/cuda_decoder_graph_executor_norm_ops.inc b/extension/src/inference/cuda/decoder_graph/ops/norm/cuda_decoder_graph_executor_norm_ops.inc new file mode 100644 index 000000000..3967a2ee8 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/ops/norm/cuda_decoder_graph_executor_norm_ops.inc @@ -0,0 +1,196 @@ +/* + * CUDA decoder graph RMSNorm helpers for native King GPU inference. + */ + +#include "../../device/compare/norm/cuda_decoder_graph_executor_ffn_norm_compare.inc" + +static zend_result king_inference_cuda_decoder_graph_executor_execute_post_rms_norm( + king_inference_model_object *model, + zval *graph, + zend_string *input_id, + king_inference_cuda_device_ptr input_output, + zend_ulong input_width, + zval *probe, + const char *probe_prefix, + king_inference_cuda_device_ptr *norm_output, + zend_ulong *norm_width, + zend_string **norm_id_out, + zend_ulong *executed_device_ops +) { + zval *norm_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, input_id, "rms_norm"); + zend_string *norm_id = NULL; + zend_string *weight = NULL; + double epsilon = 0.000001; + size_t bytes; + + *norm_output = 0; + *norm_width = 0; + if (norm_id_out != NULL) { + *norm_id_out = NULL; + } + add_assoc_bool(probe, "post_rms_norm_attempted", norm_op != NULL); + add_assoc_bool(probe, "post_rms_norm_available", model->cuda_rms_norm_available); + add_assoc_bool(probe, "post_rms_norm_executed", false); + add_assoc_bool(probe, "post_rms_norm_retained_device_output", false); + if (probe_prefix != NULL) { + add_assoc_string(probe, "post_rms_norm_scope", probe_prefix); + } + if (norm_op == NULL) { + return SUCCESS; + } + if (!model->cuda_rms_norm_available) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_post_rms_norm_unavailable"); + return FAILURE; + } + if (input_output == 0 + || input_width == 0 + || input_width > (zend_ulong) (SIZE_MAX / sizeof(float)) + || king_inference_graph_required_string(norm_op, "id", &norm_id) != SUCCESS + || king_inference_graph_required_string(norm_op, "weight", &weight) != SUCCESS + || king_inference_graph_finite_double_option(norm_op, "epsilon", 0.000001, &epsilon) != SUCCESS + || epsilon <= 0.0) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_post_rms_norm_contract_invalid"); + return FAILURE; + } + + bytes = (size_t) input_width * sizeof(float); + if (king_inference_cuda_device_allocator_alloc(model, bytes, norm_output) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_post_rms_norm_allocation_failed" + ); + return FAILURE; + } + if (king_inference_cuda_rms_norm_f32(model, weight, input_output, *norm_output, input_width, epsilon) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, norm_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_rms_norm_result, + model->cuda_rms_norm_error[0] != '\0' + ? model->cuda_rms_norm_error + : "cuda_decoder_graph_post_rms_norm_execution_failed" + ); + return FAILURE; + } + + model->cuda_decoder_graph_executor_rms_norm_execution_count++; + model->cuda_decoder_graph_executor_last_rms_norm_width = input_width; + *norm_width = input_width; + if (norm_id_out != NULL) { + *norm_id_out = norm_id; + } + (*executed_device_ops)++; + + add_assoc_str(probe, "post_rms_norm_input", zend_string_copy(input_id)); + add_assoc_str(probe, "post_rms_norm", zend_string_copy(norm_id)); + add_assoc_str(probe, "post_rms_norm_weight", zend_string_copy(weight)); + add_assoc_bool(probe, "post_rms_norm_executed", true); + add_assoc_bool(probe, "post_rms_norm_retained_device_output", true); + add_assoc_long(probe, "post_rms_norm_width", (zend_long) input_width); + add_assoc_long(probe, "post_rms_norm_bytes", (zend_long) bytes); + add_assoc_double(probe, "post_rms_norm_epsilon", epsilon); + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_execute_ffn_norm( + king_inference_model_object *model, + zval *graph, + zend_string *residual_id, + king_inference_cuda_device_ptr residual_output, + zend_ulong residual_width, + zval *ffn_norm_probe, + king_inference_cuda_device_ptr *ffn_norm_output, + zend_ulong *ffn_norm_width, + zend_string **ffn_norm_id_out, + zend_ulong *executed_device_ops +) { + zval *ffn_norm_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, residual_id, "rms_norm"); + zend_string *ffn_norm_id = NULL; + zend_string *weight = NULL; + double epsilon = 0.000001; + size_t bytes; + + *ffn_norm_output = 0; + *ffn_norm_width = 0; + add_assoc_string(ffn_norm_probe, "type", "gpu_decoder_ffn_norm_device_execution"); + add_assoc_bool(ffn_norm_probe, "attempted", ffn_norm_op != NULL); + add_assoc_bool(ffn_norm_probe, "available", model->cuda_decoder_graph_executor_ffn_norm_execution_available); + add_assoc_bool(ffn_norm_probe, "executed", false); + add_assoc_bool(ffn_norm_probe, "retained_device_output", false); + if (ffn_norm_op == NULL) { + return SUCCESS; + } + if (!model->cuda_decoder_graph_executor_ffn_norm_execution_available) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_ffn_norm_execution_unavailable"); + return FAILURE; + } + if (residual_output == 0 || residual_width == 0 || residual_width > (zend_ulong) (SIZE_MAX / sizeof(float))) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_ffn_norm_input_invalid"); + return FAILURE; + } + if (king_inference_graph_required_string(ffn_norm_op, "id", &ffn_norm_id) != SUCCESS + || king_inference_graph_required_string(ffn_norm_op, "weight", &weight) != SUCCESS + || king_inference_graph_finite_double_option(ffn_norm_op, "epsilon", 0.000001, &epsilon) != SUCCESS + || epsilon <= 0.0) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_ffn_norm_contract_invalid"); + return FAILURE; + } + + bytes = (size_t) residual_width * sizeof(float); + if (king_inference_cuda_device_allocator_alloc(model, bytes, ffn_norm_output) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_ffn_norm_allocation_failed" + ); + return FAILURE; + } + if (king_inference_cuda_rms_norm_f32(model, weight, residual_output, *ffn_norm_output, residual_width, epsilon) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, ffn_norm_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_rms_norm_result, + model->cuda_rms_norm_error[0] != '\0' + ? model->cuda_rms_norm_error + : "cuda_decoder_graph_ffn_norm_execution_failed" + ); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_compare_block0_ffn_norm( + model, + weight, + residual_output, + residual_width, + *ffn_norm_output, + residual_width, + epsilon, + ffn_norm_probe + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, ffn_norm_output); + return FAILURE; + } + + model->cuda_decoder_graph_executor_rms_norm_execution_count++; + model->cuda_decoder_graph_executor_ffn_norm_execution_count++; + model->cuda_decoder_graph_executor_last_rms_norm_width = residual_width; + model->cuda_decoder_graph_executor_last_ffn_norm_width = residual_width; + *ffn_norm_width = residual_width; + if (ffn_norm_id_out != NULL) { + *ffn_norm_id_out = ffn_norm_id; + } + (*executed_device_ops)++; + + add_assoc_str(ffn_norm_probe, "input", zend_string_copy(residual_id)); + add_assoc_str(ffn_norm_probe, "ffn_norm", zend_string_copy(ffn_norm_id)); + add_assoc_bool(ffn_norm_probe, "executed", true); + add_assoc_bool(ffn_norm_probe, "retained_device_output", true); + add_assoc_long(ffn_norm_probe, "width", (zend_long) residual_width); + add_assoc_long(ffn_norm_probe, "bytes", (zend_long) bytes); + add_assoc_double(ffn_norm_probe, "epsilon", epsilon); + return SUCCESS; +} diff --git a/extension/src/inference/cuda/decoder_graph/ops/projection/cuda_decoder_graph_executor_projection_ops.inc b/extension/src/inference/cuda/decoder_graph/ops/projection/cuda_decoder_graph_executor_projection_ops.inc new file mode 100644 index 000000000..a760084c6 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/ops/projection/cuda_decoder_graph_executor_projection_ops.inc @@ -0,0 +1,193 @@ +/* + * CUDA decoder graph attention output-projection helpers for native King GPU inference. + */ + +#include "../../device/compare/attention/cuda_decoder_graph_executor_attention_residual_compare.inc" + +static zend_result king_inference_cuda_decoder_graph_executor_execute_attention_output_projection( + king_inference_model_object *model, + zval *graph, + zend_string *stack_id, + king_inference_cuda_device_ptr stack_output, + zend_ulong stack_width, + zval *projection_probe, + king_inference_cuda_device_ptr *projection_output, + zend_ulong *projection_width, + zend_string **projection_id_out, + zend_ulong *executed_device_ops +) { + zval *projection_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, stack_id, "linear"); + zend_string *projection_id = NULL; + + *projection_output = 0; + *projection_width = 0; + add_assoc_string(projection_probe, "type", "gpu_decoder_attention_output_projection_device_execution"); + add_assoc_bool(projection_probe, "attempted", projection_op != NULL); + add_assoc_bool( + projection_probe, + "available", + model->cuda_decoder_graph_executor_attention_output_projection_execution_available + ); + add_assoc_bool(projection_probe, "executed", false); + add_assoc_bool(projection_probe, "retained_device_output", false); + if (projection_op == NULL) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_attention_output_projection_missing" + ); + return FAILURE; + } + if (stack_output == 0 || stack_width == 0 || stack_id == NULL) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_attention_output_projection_input_invalid" + ); + return FAILURE; + } + if (!model->cuda_decoder_graph_executor_attention_output_projection_execution_available) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_attention_output_projection_execution_unavailable" + ); + return FAILURE; + } + if (king_inference_graph_required_string(projection_op, "id", &projection_id) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_attention_output_projection_id_invalid" + ); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_linear_to_device( + model, + projection_op, + stack_output, + stack_width, + projection_output, + projection_width + ) != SUCCESS) { + return FAILURE; + } + if (projection_id_out != NULL) { + *projection_id_out = projection_id; + } + + model->cuda_decoder_graph_executor_attention_output_projection_execution_count++; + model->cuda_decoder_graph_executor_last_attention_output_projection_width = *projection_width; + (*executed_device_ops)++; + + add_assoc_str(projection_probe, "input", zend_string_copy(stack_id)); + add_assoc_str(projection_probe, "projection", zend_string_copy(projection_id)); + add_assoc_bool(projection_probe, "executed", true); + add_assoc_bool(projection_probe, "retained_device_output", true); + add_assoc_long(projection_probe, "stack_width", (zend_long) stack_width); + add_assoc_long(projection_probe, "projection_width", (zend_long) *projection_width); + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_execute_attention_residual_add( + king_inference_model_object *model, + zval *graph, + zend_string *hidden_id, + king_inference_cuda_device_ptr hidden_output, + zend_ulong hidden_width, + zend_string *projection_id, + king_inference_cuda_device_ptr projection_output, + zend_ulong projection_width, + zval *residual_probe, + king_inference_cuda_device_ptr *residual_output, + zend_ulong *residual_width, + zend_string **residual_id_out, + zend_ulong *executed_device_ops +) { + zval *residual_op = king_inference_cuda_decoder_graph_executor_add_op_for_inputs(graph, hidden_id, projection_id); + zend_string *residual_id = NULL; + + *residual_output = 0; + *residual_width = 0; + add_assoc_string(residual_probe, "type", "gpu_decoder_attention_residual_device_execution"); + add_assoc_bool(residual_probe, "attempted", residual_op != NULL); + add_assoc_bool(residual_probe, "available", model->cuda_decoder_graph_executor_attention_residual_execution_available); + add_assoc_bool(residual_probe, "executed", false); + add_assoc_bool(residual_probe, "retained_device_output", false); + if (residual_op == NULL) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_attention_residual_missing"); + return FAILURE; + } + if (!model->cuda_decoder_graph_executor_attention_residual_execution_available) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_attention_residual_execution_unavailable" + ); + return FAILURE; + } + if (hidden_output == 0 + || projection_output == 0 + || hidden_width == 0 + || hidden_width != projection_width + || hidden_width > (zend_ulong) (SIZE_MAX / sizeof(float))) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_attention_residual_input_invalid"); + return FAILURE; + } + if (king_inference_graph_required_string(residual_op, "id", &residual_id) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_attention_residual_id_invalid"); + return FAILURE; + } + if (king_inference_cuda_device_allocator_alloc( + model, + (size_t) hidden_width * sizeof(float), + residual_output + ) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_attention_residual_allocation_failed" + ); + return FAILURE; + } + if (king_inference_cuda_vector_add(model, hidden_output, projection_output, *residual_output, hidden_width) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, residual_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_vector_ops_result, + model->cuda_device_vector_ops_error[0] != '\0' + ? model->cuda_device_vector_ops_error + : "cuda_decoder_graph_attention_residual_add_failed" + ); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_compare_attention_residual_add( + model, + hidden_output, + hidden_width, + projection_output, + projection_width, + *residual_output, + hidden_width, + residual_probe + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, residual_output); + return FAILURE; + } + + model->cuda_decoder_graph_executor_attention_residual_execution_count++; + model->cuda_decoder_graph_executor_last_attention_residual_width = hidden_width; + *residual_width = hidden_width; + if (residual_id_out != NULL) { + *residual_id_out = residual_id; + } + (*executed_device_ops)++; + + add_assoc_str(residual_probe, "residual", zend_string_copy(residual_id)); + add_assoc_bool(residual_probe, "executed", true); + add_assoc_bool(residual_probe, "retained_device_output", true); + add_assoc_long(residual_probe, "width", (zend_long) hidden_width); + return SUCCESS; +} diff --git a/extension/src/inference/cuda/decoder_graph/ops/projection/cuda_decoder_graph_executor_query_ops.inc b/extension/src/inference/cuda/decoder_graph/ops/projection/cuda_decoder_graph_executor_query_ops.inc new file mode 100644 index 000000000..6fb2a5e5c --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/ops/projection/cuda_decoder_graph_executor_query_ops.inc @@ -0,0 +1,481 @@ +/* + * CUDA decoder graph query-head fan-in helpers for native King GPU inference. + */ + +static void king_inference_cuda_decoder_graph_executor_init_head_probe( + zval *probe, + const char *type, + bool available +) { + array_init(probe); + add_assoc_string(probe, "type", type); + add_assoc_bool(probe, "attempted", true); + add_assoc_bool(probe, "available", available); + add_assoc_bool(probe, "executed", false); + add_assoc_bool(probe, "retained_device_output", false); +} + +static void king_inference_cuda_decoder_graph_executor_destroy_query_head_probes( + zval *head_probe, + zval *slice_probe, + zval *rope_probe, + zval *kv_probe +) { + zval_ptr_dtor(kv_probe); + zval_ptr_dtor(rope_probe); + zval_ptr_dtor(slice_probe); + zval_ptr_dtor(head_probe); +} + +static zend_result king_inference_cuda_decoder_graph_executor_execute_query_head_with_prepared_kv( + king_inference_model_object *model, + zval *graph, + zend_string *query_slice_id, + king_inference_cuda_device_ptr query_slice_output, + zend_ulong query_slice_length, + zval *query_rope_probe, + zval *kv_probe, + king_inference_cuda_device_ptr *attention_stack_output, + zend_ulong *attention_stack_width, + zend_string **attention_stack_id, + zend_ulong *executed_device_ops, + bool *head_prepared +) { + zval *kv_attention_op = NULL; + zval kv_attention_probe; + zval attention_stack_probe; + zend_string *query_rope_id = NULL; + zend_string *kv_attention_id = NULL; + king_inference_cuda_device_ptr query_rope_output = 0; + king_inference_cuda_device_ptr attention_context_output = 0; + zend_ulong query_rope_length = 0; + zend_ulong attention_context_length = 0; + bool query_rope_executed = false; + + *head_prepared = false; + add_assoc_bool(kv_probe, "attempted", true); + add_assoc_bool(kv_probe, "available", model->cuda_decoder_graph_executor_kv_attention_execution_available); + add_assoc_bool(kv_probe, "executed", false); + add_assoc_bool(kv_probe, "retained_device_output", false); + add_assoc_bool(kv_probe, "kv_reused", true); + + if (query_slice_id == NULL || query_slice_output == 0) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_reused_kv_query_rope_contract_invalid" + ); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_execute_rope_after_slice_to_device( + model, + graph, + query_slice_id, + query_slice_output, + query_slice_length, + query_rope_probe, + executed_device_ops, + &query_rope_output, + &query_rope_length, + &query_rope_id, + &query_rope_executed + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &query_rope_output); + return FAILURE; + } + if (!query_rope_executed) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &query_rope_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_reused_kv_query_rope_missing" + ); + return FAILURE; + } + + kv_attention_op = king_inference_cuda_decoder_graph_executor_kv_attention_op_for_query(graph, query_rope_id); + array_init(&kv_attention_probe); + add_assoc_string(&kv_attention_probe, "type", "gpu_decoder_reused_kv_attention_device_execution"); + if (king_inference_cuda_decoder_graph_executor_execute_kv_attention( + model, + kv_attention_op, + query_rope_output, + query_rope_length, + &kv_attention_probe, + &attention_context_output, + &attention_context_length, + executed_device_ops + ) != SUCCESS) { + zval_ptr_dtor(&kv_attention_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &query_rope_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &attention_context_output); + return FAILURE; + } + if (kv_attention_op == NULL + || king_inference_graph_required_string(kv_attention_op, "id", &kv_attention_id) != SUCCESS) { + zval_ptr_dtor(&kv_attention_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &query_rope_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &attention_context_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_reused_kv_attention_id_invalid" + ); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &query_rope_output) != SUCCESS) { + zval_ptr_dtor(&kv_attention_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &attention_context_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_reused_kv_query_rope_free_failed" + ); + return FAILURE; + } + add_assoc_bool(query_rope_probe, "retained_device_output", false); + + array_init(&attention_stack_probe); + add_assoc_string(&attention_stack_probe, "type", "gpu_decoder_reused_kv_attention_stack_slot_device_execution"); + if (king_inference_cuda_decoder_graph_executor_prepare_attention_stack_slot( + model, + graph, + kv_attention_id, + attention_context_output, + attention_context_length, + &attention_stack_probe, + attention_stack_output, + attention_stack_width, + attention_stack_id, + executed_device_ops + ) != SUCCESS) { + zval_ptr_dtor(&attention_stack_probe); + zval_ptr_dtor(&kv_attention_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &attention_context_output); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &attention_context_output) != SUCCESS) { + zval_ptr_dtor(&attention_stack_probe); + zval_ptr_dtor(&kv_attention_probe); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_reused_kv_attention_context_free_failed" + ); + return FAILURE; + } + + add_assoc_bool(&kv_attention_probe, "retained_device_output", false); + add_assoc_bool(kv_probe, "executed", true); + add_assoc_bool(kv_probe, "query_rope_executed", query_rope_executed); + add_assoc_long(kv_probe, "query_rope_length", (zend_long) query_rope_length); + add_assoc_long(kv_probe, "attention_context_length", (zend_long) attention_context_length); + add_assoc_long(kv_probe, "attention_stack_width", (zend_long) *attention_stack_width); + add_assoc_bool(kv_probe, "attention_context_released", true); + add_assoc_bool(kv_probe, "attention_stack_released", false); + add_assoc_zval(kv_probe, "kv_attention_execution", &kv_attention_probe); + add_assoc_zval(kv_probe, "attention_stack_slot_execution", &attention_stack_probe); + *head_prepared = true; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_execute_query_attention_heads( + king_inference_model_object *model, + zval *graph, + zend_string *rms_norm_id, + zend_string *rms_weight_tensor, + king_inference_cuda_device_ptr rms_norm_output, + zend_ulong width, + zend_ulong token_id, + double epsilon, + zend_string *linear_id, + king_inference_cuda_device_ptr linear_output, + zend_ulong linear_rows, + zval *slice_probe, + zval *rope_probe, + zval *kv_head_prepare_probe, + king_inference_cuda_device_ptr *attention_stack_output, + zend_ulong *attention_stack_width, + zend_string **attention_stack_id, + zend_ulong *executed_device_ops, + zend_ulong *first_slice_length, + bool *rope_executed, + bool *kv_prepared +) { + zval heads; + zend_ulong head_index = 0; + zend_ulong prepared_heads = 0; + zend_ulong sliced_heads = 0; + zend_ulong prefilled_query_heads = 0; + zend_ulong kv_heads = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT_KV]; + bool reuse_single_kv_head = kv_heads == 1; + bool first_slice_recorded = false; + + *first_slice_length = 0; + *rope_executed = false; + *kv_prepared = false; + array_init(&heads); + + while (true) { + zval *slice_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input( + graph, + linear_id, + "slice", + head_index + ); + zval head_probe; + zval head_slice_probe; + zval head_rope_probe; + zval head_kv_probe; + zend_string *slice_id = NULL; + king_inference_cuda_device_ptr slice_output = 0; + zend_ulong slice_offset = 0; + zend_ulong slice_length = 0; + zend_result prepare_result; + bool head_prepared = false; + bool used_prefilled_query_head = false; + + if (slice_op == NULL) { + break; + } + if (!model->cuda_decoder_graph_executor_slice_execution_available) { + zval_ptr_dtor(&heads); + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_slice_execution_unavailable" + ); + return FAILURE; + } + array_init(&head_probe); + add_assoc_string(&head_probe, "type", "gpu_decoder_query_attention_head_device_execution"); + add_assoc_long(&head_probe, "head_index", (zend_long) head_index); + king_inference_cuda_decoder_graph_executor_init_head_probe( + &head_slice_probe, + "gpu_decoder_slice_device_execution", + model->cuda_decoder_graph_executor_slice_execution_available + ); + king_inference_cuda_decoder_graph_executor_init_head_probe( + &head_rope_probe, + "gpu_decoder_rope_device_execution", + model->cuda_decoder_graph_executor_rope_execution_available + ); + array_init(&head_kv_probe); + add_assoc_string(&head_kv_probe, "type", "gpu_decoder_kv_head_prepare_device_execution"); + add_assoc_bool(&head_kv_probe, "attempted", true); + add_assoc_bool( + &head_kv_probe, + "available", + model->cuda_decoder_graph_executor_kv_head_prepare_execution_available + ); + add_assoc_bool(&head_kv_probe, "executed", false); + add_assoc_bool(&head_kv_probe, "retained_device_output", false); + + if (king_inference_graph_required_string(slice_op, "id", &slice_id) != SUCCESS + || king_inference_cuda_decoder_graph_executor_slice_shape( + model, + slice_op, + linear_rows, + &slice_offset, + &slice_length + ) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_destroy_query_head_probes( + &head_probe, + &head_slice_probe, + &head_rope_probe, + &head_kv_probe + ); + zval_ptr_dtor(&heads); + return FAILURE; + } + if (!first_slice_recorded) { + first_slice_recorded = true; + *first_slice_length = slice_length; + add_assoc_long(slice_probe, "offset", (zend_long) slice_offset); + add_assoc_long(slice_probe, "length", (zend_long) slice_length); + add_assoc_long(slice_probe, "bytes", (zend_long) ((size_t) slice_length * sizeof(float))); + add_assoc_long(slice_probe, "first_offset", (zend_long) slice_offset); + add_assoc_long(slice_probe, "first_length", (zend_long) slice_length); + } + + if (head_index == 0 || !reuse_single_kv_head) { + prepare_result = king_inference_cuda_decoder_graph_executor_try_prefilled_kv_head_prepare_for_norm( + model, + graph, + rms_norm_id, + slice_id, + slice_length, + &head_rope_probe, + &head_kv_probe, + attention_stack_output, + attention_stack_width, + attention_stack_id, + executed_device_ops, + &head_prepared, + &used_prefilled_query_head + ); + if (prepare_result != SUCCESS) { + king_inference_cuda_decoder_graph_executor_destroy_query_head_probes( + &head_probe, + &head_slice_probe, + &head_rope_probe, + &head_kv_probe + ); + zval_ptr_dtor(&heads); + return FAILURE; + } + } else { + prepare_result = SUCCESS; + } + if (used_prefilled_query_head) { + prefilled_query_heads++; + add_assoc_bool(&head_slice_probe, "executed", false); + add_assoc_bool(&head_slice_probe, "used_prefill_query_rope", true); + add_assoc_long(&head_slice_probe, "offset", (zend_long) slice_offset); + add_assoc_long(&head_slice_probe, "length", (zend_long) slice_length); + add_assoc_long(&head_slice_probe, "bytes", (zend_long) ((size_t) slice_length * sizeof(float))); + add_assoc_bool(&head_slice_probe, "retained_device_output", false); + } else { + if (king_inference_cuda_decoder_graph_executor_slice_to_device( + model, + slice_op, + linear_output, + linear_rows, + &slice_output, + &slice_offset, + &slice_length + ) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_destroy_query_head_probes( + &head_probe, + &head_slice_probe, + &head_rope_probe, + &head_kv_probe + ); + zval_ptr_dtor(&heads); + return FAILURE; + } + (*executed_device_ops)++; + sliced_heads++; + add_assoc_bool(&head_slice_probe, "executed", true); + add_assoc_long(&head_slice_probe, "offset", (zend_long) slice_offset); + add_assoc_long(&head_slice_probe, "length", (zend_long) slice_length); + add_assoc_long(&head_slice_probe, "bytes", (zend_long) ((size_t) slice_length * sizeof(float))); + + if (head_index == 0 || !reuse_single_kv_head) { + prepare_result = king_inference_cuda_decoder_graph_executor_execute_kv_head_prepare( + model, + graph, + rms_norm_id, + rms_weight_tensor, + rms_norm_output, + width, + token_id, + epsilon, + slice_id, + slice_output, + slice_length, + &head_rope_probe, + &head_kv_probe, + attention_stack_output, + attention_stack_width, + attention_stack_id, + executed_device_ops, + &head_prepared + ); + } else { + prepare_result = king_inference_cuda_decoder_graph_executor_execute_query_head_with_prepared_kv( + model, + graph, + slice_id, + slice_output, + slice_length, + &head_rope_probe, + &head_kv_probe, + attention_stack_output, + attention_stack_width, + attention_stack_id, + executed_device_ops, + &head_prepared + ); + } + if (prepare_result != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &slice_output); + king_inference_cuda_decoder_graph_executor_destroy_query_head_probes( + &head_probe, + &head_slice_probe, + &head_rope_probe, + &head_kv_probe + ); + zval_ptr_dtor(&heads); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &slice_output) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_query_slice_free_failed" + ); + king_inference_cuda_decoder_graph_executor_destroy_query_head_probes( + &head_probe, + &head_slice_probe, + &head_rope_probe, + &head_kv_probe + ); + zval_ptr_dtor(&heads); + return FAILURE; + } + add_assoc_bool(&head_slice_probe, "retained_device_output", false); + } + if (head_prepared) { + prepared_heads++; + } + + add_assoc_bool(&head_probe, "prepared", head_prepared); + add_assoc_zval(&head_probe, "slice_execution", &head_slice_probe); + add_assoc_zval(&head_probe, "rope_execution", &head_rope_probe); + add_assoc_zval(&head_probe, "kv_head_prepare_execution", &head_kv_probe); + add_next_index_zval(&heads, &head_probe); + head_index++; + } + + add_assoc_bool(slice_probe, "attempted", head_index > 0); + add_assoc_bool(slice_probe, "executed", sliced_heads > 0); + add_assoc_long(slice_probe, "query_head_count", (zend_long) head_index); + add_assoc_long(slice_probe, "executed_query_head_count", (zend_long) sliced_heads); + add_assoc_long(slice_probe, "prefilled_query_head_count", (zend_long) prefilled_query_heads); + add_assoc_bool(rope_probe, "attempted", head_index > 0); + add_assoc_bool(rope_probe, "executed", prepared_heads > 0); + add_assoc_long(rope_probe, "query_head_count", (zend_long) head_index); + add_assoc_bool(kv_head_prepare_probe, "attempted", head_index > 0); + add_assoc_bool(kv_head_prepare_probe, "executed", prepared_heads > 0); + add_assoc_long(kv_head_prepare_probe, "expected_heads", (zend_long) head_index); + add_assoc_long(kv_head_prepare_probe, "prepared_heads", (zend_long) prepared_heads); + add_assoc_bool(kv_head_prepare_probe, "all_heads_prepared", head_index > 0 && prepared_heads == head_index); + add_assoc_long(kv_head_prepare_probe, "attention_stack_width", (zend_long) *attention_stack_width); + add_assoc_bool(kv_head_prepare_probe, "attention_stack_retained", *attention_stack_output != 0); + add_assoc_bool( + kv_head_prepare_probe, + "complete_attention_stack", + head_index > 0 && prepared_heads == head_index && *attention_stack_output != 0 + ); + add_assoc_bool(kv_head_prepare_probe, "attention_stack_released", false); + add_assoc_zval(kv_head_prepare_probe, "query_heads", &heads); + + *rope_executed = prepared_heads > 0; + *kv_prepared = head_index > 0 && prepared_heads == head_index; + model->cuda_decoder_graph_executor_last_attention_stack_expected_heads = head_index; + model->cuda_decoder_graph_executor_last_attention_stack_prepared_heads = prepared_heads; + if (*kv_prepared) { + model->cuda_decoder_graph_executor_attention_heads_execution_count++; + if (*attention_stack_output != 0) { + model->cuda_decoder_graph_executor_attention_stack_execution_count++; + } + } + return SUCCESS; +} diff --git a/extension/src/inference/cuda/decoder_graph/ops/residual/cuda_decoder_graph_executor_stack_ops.inc b/extension/src/inference/cuda/decoder_graph/ops/residual/cuda_decoder_graph_executor_stack_ops.inc new file mode 100644 index 000000000..3ec3bc879 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/ops/residual/cuda_decoder_graph_executor_stack_ops.inc @@ -0,0 +1,248 @@ +/* + * CUDA decoder graph attention stack-slot helpers for native King GPU inference. + */ + +static zval *king_inference_cuda_decoder_graph_executor_stack_op_for_input( + zval *graph, + zend_string *input_id +) { + zval *ops = king_inference_cuda_decoder_graph_executor_ops(graph); + zval *entry; + + if (ops == NULL || input_id == NULL) { + return NULL; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(ops), entry) { + zval *op_name; + zval *inputs; + zval *input; + + if (entry == NULL || Z_TYPE_P(entry) != IS_ARRAY) { + continue; + } + op_name = king_inference_array_find(entry, "op"); + inputs = king_inference_array_find(entry, "inputs"); + if (op_name == NULL + || Z_TYPE_P(op_name) != IS_STRING + || !zend_string_equals_literal(Z_STR_P(op_name), "stack") + || inputs == NULL + || Z_TYPE_P(inputs) != IS_ARRAY) { + continue; + } + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(inputs), input) { + if (king_inference_cuda_decoder_graph_executor_zval_string_equals(input, input_id)) { + return entry; + } + } ZEND_HASH_FOREACH_END(); + } ZEND_HASH_FOREACH_END(); + + return NULL; +} + +static zend_result king_inference_cuda_decoder_graph_executor_attention_stack_shape( + king_inference_model_object *model, + zval *stack_op, + zend_string *context_id, + zend_ulong context_length, + zend_ulong *input_index, + zend_ulong *input_count, + zend_ulong *stack_width +) { + zval *inputs; + zval *input; + zend_ulong index = 0; + bool found = false; + + *input_index = 0; + *input_count = 0; + *stack_width = 0; + inputs = stack_op != NULL ? king_inference_array_find(stack_op, "inputs") : NULL; + if (inputs == NULL || Z_TYPE_P(inputs) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL_P(inputs)) == 0) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_attention_stack_inputs_invalid" + ); + return FAILURE; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(inputs), input) { + if (input == NULL || Z_TYPE_P(input) != IS_STRING) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_attention_stack_input_invalid" + ); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_zval_string_equals(input, context_id)) { + *input_index = index; + found = true; + } + index++; + } ZEND_HASH_FOREACH_END(); + + if (!found + || context_length == 0 + || context_length > UINT_MAX + || index == 0 + || context_length > (zend_ulong) (SIZE_MAX / sizeof(float))) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_attention_stack_shape_invalid" + ); + return FAILURE; + } + if (index > (zend_ulong) (SIZE_MAX / ((size_t) context_length * sizeof(float)))) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_attention_stack_width_overflow" + ); + return FAILURE; + } + + *input_count = index; + *stack_width = index * context_length; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_prepare_attention_stack_slot( + king_inference_model_object *model, + zval *graph, + zend_string *context_id, + king_inference_cuda_device_ptr context_output, + zend_ulong context_length, + zval *stack_probe, + king_inference_cuda_device_ptr *stack_output, + zend_ulong *stack_width, + zend_string **stack_id_out, + zend_ulong *executed_device_ops +) { + zval *stack_op = king_inference_cuda_decoder_graph_executor_stack_op_for_input(graph, context_id); + zend_string *stack_id = NULL; + zend_ulong input_index = 0; + zend_ulong input_count = 0; + zend_ulong output_offset = 0; + zend_ulong resolved_stack_width = 0; + bool allocated_stack = false; + + add_assoc_bool(stack_probe, "attempted", stack_op != NULL); + add_assoc_bool( + stack_probe, + "available", + model->cuda_decoder_graph_executor_attention_stack_slot_execution_available + ); + add_assoc_bool(stack_probe, "executed", false); + add_assoc_bool(stack_probe, "retained_device_output", false); + if (stack_op == NULL) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_attention_stack_missing"); + return FAILURE; + } + if (context_output == 0) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_attention_stack_context_invalid"); + return FAILURE; + } + if (!model->cuda_decoder_graph_executor_attention_stack_slot_execution_available) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_attention_stack_slot_execution_unavailable" + ); + return FAILURE; + } + if (king_inference_graph_required_string(stack_op, "id", &stack_id) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_attention_stack_id_invalid"); + return FAILURE; + } + if (stack_id_out != NULL && *stack_id_out != NULL && !zend_string_equals(*stack_id_out, stack_id)) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_attention_stack_id_mismatch"); + return FAILURE; + } + if (stack_id_out != NULL && *stack_id_out == NULL) { + *stack_id_out = stack_id; + } + if (king_inference_cuda_decoder_graph_executor_attention_stack_shape( + model, + stack_op, + context_id, + context_length, + &input_index, + &input_count, + &resolved_stack_width + ) != SUCCESS) { + return FAILURE; + } + + output_offset = input_index * context_length; + if (output_offset > UINT_MAX) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_attention_stack_offset_invalid" + ); + return FAILURE; + } + if (*stack_output == 0) { + if (king_inference_cuda_device_allocator_alloc( + model, + (size_t) resolved_stack_width * sizeof(float), + stack_output + ) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_attention_stack_allocation_failed" + ); + return FAILURE; + } + *stack_width = resolved_stack_width; + allocated_stack = true; + } else if (*stack_width != resolved_stack_width) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_attention_stack_width_mismatch" + ); + return FAILURE; + } + if (king_inference_cuda_vector_copy_to_offset( + model, + context_output, + *stack_output, + output_offset, + context_length + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, stack_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_vector_ops_result, + model->cuda_device_vector_ops_error[0] != '\0' + ? model->cuda_device_vector_ops_error + : "cuda_decoder_graph_attention_stack_slot_copy_failed" + ); + return FAILURE; + } + + model->cuda_decoder_graph_executor_attention_stack_slot_execution_count++; + model->cuda_decoder_graph_executor_last_attention_stack_width = *stack_width; + model->cuda_decoder_graph_executor_last_attention_stack_input_index = input_index; + model->cuda_decoder_graph_executor_last_attention_stack_input_count = input_count; + (*executed_device_ops)++; + + add_assoc_str(stack_probe, "stack", zend_string_copy(stack_id)); + add_assoc_str(stack_probe, "context", zend_string_copy(context_id)); + add_assoc_bool(stack_probe, "executed", true); + add_assoc_bool(stack_probe, "retained_device_output", true); + add_assoc_bool(stack_probe, "allocated_stack_output", allocated_stack); + add_assoc_long(stack_probe, "input_index", (zend_long) input_index); + add_assoc_long(stack_probe, "input_count", (zend_long) input_count); + add_assoc_long(stack_probe, "context_length", (zend_long) context_length); + add_assoc_long(stack_probe, "stack_width", (zend_long) resolved_stack_width); + add_assoc_long(stack_probe, "output_offset", (zend_long) output_offset); + return SUCCESS; +} diff --git a/extension/src/inference/cuda/decoder_graph/ops/rope/cuda_decoder_graph_executor_rope_ops.inc b/extension/src/inference/cuda/decoder_graph/ops/rope/cuda_decoder_graph_executor_rope_ops.inc new file mode 100644 index 000000000..a65f95830 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/ops/rope/cuda_decoder_graph_executor_rope_ops.inc @@ -0,0 +1,297 @@ +/* + * CUDA decoder graph RoPE execution helpers for native King GPU inference. + */ + +#include "../../device/compare/rope/cuda_decoder_graph_executor_rope_position_compare.inc" + +static zend_result king_inference_cuda_decoder_graph_executor_rope_shape( + king_inference_model_object *model, + zval *op, + zend_ulong input_length, + zend_ulong *offset, + zend_ulong *head_dim, + zend_ulong *position, + double *position_scale, + double *rope_base, + zend_ulong *pairing +) { + zend_ulong rope_offset = king_inference_tensor_option_ulong(op, "offset", 0); + zend_ulong rope_head_dim = king_inference_tensor_option_ulong(op, "head_dim", input_length - rope_offset); + zend_ulong rope_position = king_inference_tensor_option_ulong(op, "position", 0); + zend_ulong rope_pairing = king_inference_tensor_option_ulong(op, "pairing", 0); + double rope_position_scale; + double rope_base_value; + + if (king_inference_graph_finite_double_option(op, "position_scale", 1.0, &rope_position_scale) != SUCCESS + || king_inference_graph_finite_double_option(op, "rope_base", 10000.0, &rope_base_value) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_rope_contract_invalid"); + return FAILURE; + } + if (input_length == 0 + || input_length > UINT_MAX + || rope_offset > input_length + || rope_offset > UINT_MAX + || rope_head_dim == 0 + || (rope_head_dim % 2) != 0 + || rope_head_dim > input_length - rope_offset + || rope_head_dim > UINT_MAX + || rope_pairing > 1 + || !isfinite(rope_position_scale) + || !isfinite(rope_base_value) + || rope_base_value <= 0.0) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_rope_shape_invalid"); + return FAILURE; + } + + *offset = rope_offset; + *head_dim = rope_head_dim; + *position = rope_position; + *position_scale = rope_position_scale; + *rope_base = rope_base_value; + *pairing = rope_pairing; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_execute_rope_after_slice_to_device( + king_inference_model_object *model, + zval *graph, + zend_string *slice_id, + king_inference_cuda_device_ptr slice_output, + zend_ulong slice_length, + zval *rope_probe, + zend_ulong *executed_device_ops, + king_inference_cuda_device_ptr *rope_output, + zend_ulong *rope_length, + zend_string **rope_id, + bool *rope_executed +) { + zval *norm_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, slice_id, "rms_norm"); + zval *rope_op; + zend_string *input_id = slice_id; + king_inference_cuda_device_ptr input_output = slice_output; + king_inference_cuda_device_ptr norm_output = 0; + zend_ulong input_length = slice_length; + zend_ulong rope_offset = 0; + zend_ulong rope_head_dim = 0; + zend_ulong rope_position = 0; + double rope_position_scale = 1.0; + double rope_base = 10000.0; + zend_ulong rope_pairing = 0; + + *rope_output = 0; + *rope_length = 0; + if (rope_id != NULL) { + *rope_id = NULL; + } + *rope_executed = false; + if (norm_op != NULL) { + zend_string *norm_id = NULL; + zend_string *norm_weight = NULL; + double norm_epsilon = 0.000001; + + if (king_inference_graph_required_string(norm_op, "id", &norm_id) != SUCCESS + || king_inference_graph_required_string(norm_op, "weight", &norm_weight) != SUCCESS + || king_inference_graph_finite_double_option(norm_op, "epsilon", 0.000001, &norm_epsilon) != SUCCESS + || norm_epsilon <= 0.0) { + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_rope_input_norm_contract_invalid"); + return FAILURE; + } + if (king_inference_cuda_device_allocator_alloc( + model, + (size_t) slice_length * sizeof(float), + &norm_output + ) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_rope_input_norm_allocation_failed" + ); + return FAILURE; + } + if (king_inference_cuda_rms_norm_f32( + model, + norm_weight, + slice_output, + norm_output, + slice_length, + norm_epsilon + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &norm_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_rms_norm_result, + model->cuda_rms_norm_error[0] != '\0' + ? model->cuda_rms_norm_error + : "cuda_decoder_graph_rope_input_norm_failed" + ); + return FAILURE; + } + model->cuda_decoder_graph_executor_rms_norm_execution_count++; + model->cuda_decoder_graph_executor_last_rms_norm_width = slice_length; + (*executed_device_ops)++; + input_id = norm_id; + input_output = norm_output; + add_assoc_bool(rope_probe, "input_norm_executed", true); + add_assoc_str(rope_probe, "input_norm", zend_string_copy(norm_id)); + } else { + add_assoc_bool(rope_probe, "input_norm_executed", false); + } + + rope_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, input_id, "rope"); + add_assoc_bool(rope_probe, "attempted", rope_op != NULL); + if (rope_op == NULL) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &norm_output); + return SUCCESS; + } + if (!model->cuda_decoder_graph_executor_rope_execution_available) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &norm_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_rope_execution_unavailable" + ); + return FAILURE; + } + if (rope_id != NULL && king_inference_graph_required_string(rope_op, "id", rope_id) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &norm_output); + king_inference_cuda_decoder_graph_executor_set_error(model, -1, "cuda_decoder_graph_rope_id_invalid"); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_rope_shape( + model, + rope_op, + input_length, + &rope_offset, + &rope_head_dim, + &rope_position, + &rope_position_scale, + &rope_base, + &rope_pairing + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &norm_output); + return FAILURE; + } + if (king_inference_cuda_device_allocator_alloc( + model, + (size_t) input_length * sizeof(float), + rope_output + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &norm_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_rope_allocation_failed" + ); + return FAILURE; + } + if (king_inference_cuda_rope_f32( + model, + input_output, + *rope_output, + input_length, + rope_offset, + rope_head_dim, + rope_position, + rope_position_scale, + rope_base, + rope_pairing + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, rope_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_rope_result, + model->cuda_rope_error[0] != '\0' + ? model->cuda_rope_error + : "cuda_decoder_graph_rope_execution_failed" + ); + return FAILURE; + } + + model->cuda_decoder_graph_executor_rope_execution_count++; + model->cuda_decoder_graph_executor_last_rope_length = input_length; + model->cuda_decoder_graph_executor_last_rope_position = rope_position; + (*executed_device_ops)++; + *rope_executed = true; + *rope_length = input_length; + + add_assoc_bool(rope_probe, "executed", true); + add_assoc_bool(rope_probe, "retained_device_output", true); + add_assoc_long(rope_probe, "length", (zend_long) input_length); + add_assoc_long(rope_probe, "offset", (zend_long) rope_offset); + add_assoc_long(rope_probe, "head_dim", (zend_long) rope_head_dim); + add_assoc_long(rope_probe, "position", (zend_long) rope_position); + add_assoc_double(rope_probe, "position_scale", rope_position_scale); + add_assoc_double(rope_probe, "rope_base", rope_base); + add_assoc_long(rope_probe, "pairing", (zend_long) rope_pairing); + add_assoc_long(rope_probe, "bytes", (zend_long) ((size_t) input_length * sizeof(float))); + if (king_inference_cuda_decoder_graph_executor_compare_rope_position_reference( + model, + input_output, + *rope_output, + input_length, + rope_offset, + rope_head_dim, + rope_position, + rope_position_scale, + rope_base, + rope_pairing, + rope_probe + ) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_rope_position_compare_error( + model, + rope_probe, + rope_position + ); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &norm_output); + return FAILURE; + } + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &norm_output); + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_execute_rope_after_slice( + king_inference_model_object *model, + zval *graph, + zend_string *slice_id, + king_inference_cuda_device_ptr slice_output, + zend_ulong slice_length, + zval *rope_probe, + zend_ulong *executed_device_ops, + bool *rope_executed +) { + king_inference_cuda_device_ptr rope_output = 0; + zend_ulong rope_length = 0; + + if (king_inference_cuda_decoder_graph_executor_execute_rope_after_slice_to_device( + model, + graph, + slice_id, + slice_output, + slice_length, + rope_probe, + executed_device_ops, + &rope_output, + &rope_length, + NULL, + rope_executed + ) != SUCCESS) { + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &rope_output) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_rope_free_failed" + ); + return FAILURE; + } + add_assoc_bool(rope_probe, "retained_device_output", false); + return SUCCESS; +} diff --git a/extension/src/inference/cuda/decoder_graph/prefill/ffn/cuda_decoder_graph_executor_prefilled_ffn_ops.inc b/extension/src/inference/cuda/decoder_graph/prefill/ffn/cuda_decoder_graph_executor_prefilled_ffn_ops.inc new file mode 100644 index 000000000..1de8c2e32 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/prefill/ffn/cuda_decoder_graph_executor_prefilled_ffn_ops.inc @@ -0,0 +1,715 @@ +/* + * CUDA decoder graph helpers for reusing batch-prefilled layer-0 FFN state. + */ + +static zend_result king_inference_cuda_decoder_graph_executor_try_prefilled_layer0_ffn_norm( + king_inference_model_object *model, + zval *graph, + zend_string *residual_id, + zend_ulong residual_width, + zend_ulong position, + zval *ffn_norm_probe, + king_inference_cuda_device_ptr *ffn_norm_output, + zend_ulong *ffn_norm_width, + zend_string **ffn_norm_id_out, + zend_ulong *executed_device_ops, + bool *used_prefilled_ffn_norm +) { + zval *ffn_norm_op; + zend_string *ffn_norm_id = NULL; + zend_string *weight = NULL; + double epsilon = 0.000001; + size_t source_offset; + + *used_prefilled_ffn_norm = false; + *ffn_norm_output = 0; + *ffn_norm_width = 0; + if (ffn_norm_id_out != NULL) { + *ffn_norm_id_out = NULL; + } + if (model->cuda_decoder_prompt_prefill_layer0_ffn_norm == 0 + || model->cuda_decoder_prompt_prefill_layer0_ffn_norm_width == 0 + || position >= model->cuda_decoder_prompt_prefill_tokens) { + return SUCCESS; + } + if (residual_id == NULL + || residual_width == 0 + || residual_width != model->cuda_decoder_prompt_prefill_layer0_ffn_norm_width + || residual_width > (zend_ulong) (SIZE_MAX / sizeof(float))) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_ffn_norm_shape_invalid" + ); + return FAILURE; + } + + ffn_norm_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, residual_id, "rms_norm"); + add_assoc_string(ffn_norm_probe, "type", "gpu_decoder_ffn_norm_device_execution"); + add_assoc_bool(ffn_norm_probe, "attempted", ffn_norm_op != NULL); + add_assoc_bool(ffn_norm_probe, "available", model->cuda_decoder_graph_executor_ffn_norm_execution_available); + add_assoc_bool(ffn_norm_probe, "executed", false); + add_assoc_bool(ffn_norm_probe, "retained_device_output", false); + if (ffn_norm_op == NULL) { + return SUCCESS; + } + if (king_inference_graph_required_string(ffn_norm_op, "id", &ffn_norm_id) != SUCCESS + || king_inference_graph_required_string(ffn_norm_op, "weight", &weight) != SUCCESS + || king_inference_graph_finite_double_option(ffn_norm_op, "epsilon", 0.000001, &epsilon) != SUCCESS + || epsilon <= 0.0) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_ffn_norm_contract_invalid" + ); + return FAILURE; + } + if (position > (zend_ulong) (SIZE_MAX / (size_t) residual_width)) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_ffn_norm_offset_invalid" + ); + return FAILURE; + } + + if (king_inference_cuda_device_allocator_alloc( + model, + (size_t) residual_width * sizeof(float), + ffn_norm_output + ) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_prefilled_ffn_norm_allocation_failed" + ); + return FAILURE; + } + + source_offset = (size_t) position * (size_t) residual_width; + if (king_inference_cuda_vector_copy_to_offset( + model, + model->cuda_decoder_prompt_prefill_layer0_ffn_norm + + (king_inference_cuda_device_ptr) (source_offset * sizeof(float)), + *ffn_norm_output, + 0, + residual_width + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, ffn_norm_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_vector_ops_result, + model->cuda_device_vector_ops_error[0] != '\0' + ? model->cuda_device_vector_ops_error + : "cuda_decoder_graph_prefilled_ffn_norm_copy_failed" + ); + return FAILURE; + } + + model->cuda_decoder_graph_executor_rms_norm_execution_count++; + model->cuda_decoder_graph_executor_ffn_norm_execution_count++; + model->cuda_decoder_graph_executor_last_rms_norm_width = residual_width; + model->cuda_decoder_graph_executor_last_ffn_norm_width = residual_width; + *ffn_norm_width = residual_width; + if (ffn_norm_id_out != NULL) { + *ffn_norm_id_out = ffn_norm_id; + } + (*executed_device_ops)++; + + add_assoc_str(ffn_norm_probe, "input", zend_string_copy(residual_id)); + add_assoc_str(ffn_norm_probe, "ffn_norm", zend_string_copy(ffn_norm_id)); + add_assoc_bool(ffn_norm_probe, "executed", true); + add_assoc_bool(ffn_norm_probe, "precomputed", true); + add_assoc_bool(ffn_norm_probe, "used_prefill_ffn_norm", true); + add_assoc_bool(ffn_norm_probe, "retained_device_output", true); + add_assoc_long(ffn_norm_probe, "width", (zend_long) residual_width); + add_assoc_long(ffn_norm_probe, "bytes", (zend_long) ((size_t) residual_width * sizeof(float))); + add_assoc_double(ffn_norm_probe, "epsilon", epsilon); + *used_prefilled_ffn_norm = true; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_try_prefilled_layer0_ffn_gate_up( + king_inference_model_object *model, + zval *graph, + zend_string *ffn_norm_id, + zend_ulong ffn_norm_width, + zend_ulong position, + zval *ffn_gate_up_probe, + king_inference_cuda_device_ptr *gate_output, + king_inference_cuda_device_ptr *up_output, + zend_ulong *gate_rows, + zend_ulong *up_rows, + zend_string **gate_id_out, + zend_string **up_id_out, + zend_ulong *executed_device_ops, + bool *used_prefilled_gate_up +) { + zval *gate_op; + zval *up_op; + zend_string *gate_id = NULL; + zend_string *up_id = NULL; + zend_string *gate_weight = NULL; + zend_string *up_weight = NULL; + zend_ulong gate_shape_rows = 0; + zend_ulong up_shape_rows = 0; + size_t gate_source_offset; + size_t up_source_offset; + + *used_prefilled_gate_up = false; + *gate_output = 0; + *up_output = 0; + *gate_rows = 0; + *up_rows = 0; + if (gate_id_out != NULL) { + *gate_id_out = NULL; + } + if (up_id_out != NULL) { + *up_id_out = NULL; + } + if (model->cuda_decoder_prompt_prefill_layer0_ffn_gate == 0 + || model->cuda_decoder_prompt_prefill_layer0_ffn_up == 0 + || model->cuda_decoder_prompt_prefill_layer0_ffn_gate_rows == 0 + || model->cuda_decoder_prompt_prefill_layer0_ffn_up_rows == 0 + || position >= model->cuda_decoder_prompt_prefill_tokens) { + return SUCCESS; + } + if (ffn_norm_id == NULL || ffn_norm_width == 0) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_ffn_gate_up_input_invalid" + ); + return FAILURE; + } + + gate_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, ffn_norm_id, "linear", 0); + up_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, ffn_norm_id, "linear", 1); + add_assoc_string(ffn_gate_up_probe, "type", "gpu_decoder_ffn_gate_up_projection_device_execution"); + add_assoc_bool(ffn_gate_up_probe, "attempted", gate_op != NULL || up_op != NULL); + add_assoc_bool(ffn_gate_up_probe, "available", model->cuda_decoder_graph_executor_ffn_gate_up_projection_execution_available); + add_assoc_bool(ffn_gate_up_probe, "executed", false); + add_assoc_bool(ffn_gate_up_probe, "retained_device_output", false); + if (gate_op == NULL + || up_op == NULL + || king_inference_graph_required_string(gate_op, "id", &gate_id) != SUCCESS + || king_inference_graph_required_string(up_op, "id", &up_id) != SUCCESS + || king_inference_cuda_decoder_graph_executor_linear_shape( + model, + gate_op, + ffn_norm_width, + &gate_weight, + &gate_shape_rows + ) != SUCCESS + || king_inference_cuda_decoder_graph_executor_linear_shape( + model, + up_op, + ffn_norm_width, + &up_weight, + &up_shape_rows + ) != SUCCESS + || gate_shape_rows == 0 + || gate_shape_rows != up_shape_rows + || gate_shape_rows != model->cuda_decoder_prompt_prefill_layer0_ffn_gate_rows + || up_shape_rows != model->cuda_decoder_prompt_prefill_layer0_ffn_up_rows + || gate_shape_rows > (zend_ulong) (SIZE_MAX / sizeof(float)) + || up_shape_rows > (zend_ulong) (SIZE_MAX / sizeof(float))) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + model->cuda_decoder_graph_executor_error[0] != '\0' + ? model->cuda_decoder_graph_executor_error + : "cuda_decoder_graph_prefilled_ffn_gate_up_contract_invalid" + ); + return FAILURE; + } + if (position > (zend_ulong) (SIZE_MAX / (size_t) gate_shape_rows) + || position > (zend_ulong) (SIZE_MAX / (size_t) up_shape_rows)) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_ffn_gate_up_offset_invalid" + ); + return FAILURE; + } + + if (king_inference_cuda_device_allocator_alloc( + model, + (size_t) gate_shape_rows * sizeof(float), + gate_output + ) != SUCCESS + || king_inference_cuda_device_allocator_alloc( + model, + (size_t) up_shape_rows * sizeof(float), + up_output + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, gate_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_prefilled_ffn_gate_up_allocation_failed" + ); + return FAILURE; + } + + gate_source_offset = (size_t) position * (size_t) gate_shape_rows; + up_source_offset = (size_t) position * (size_t) up_shape_rows; + if (king_inference_cuda_vector_copy_to_offset( + model, + model->cuda_decoder_prompt_prefill_layer0_ffn_gate + + (king_inference_cuda_device_ptr) (gate_source_offset * sizeof(float)), + *gate_output, + 0, + gate_shape_rows + ) != SUCCESS + || king_inference_cuda_vector_copy_to_offset( + model, + model->cuda_decoder_prompt_prefill_layer0_ffn_up + + (king_inference_cuda_device_ptr) (up_source_offset * sizeof(float)), + *up_output, + 0, + up_shape_rows + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, up_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, gate_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_vector_ops_result, + model->cuda_device_vector_ops_error[0] != '\0' + ? model->cuda_device_vector_ops_error + : "cuda_decoder_graph_prefilled_ffn_gate_up_copy_failed" + ); + return FAILURE; + } + + model->cuda_decoder_graph_executor_ffn_gate_up_projection_execution_count++; + model->cuda_decoder_graph_executor_last_ffn_gate_rows = gate_shape_rows; + model->cuda_decoder_graph_executor_last_ffn_up_rows = up_shape_rows; + *gate_rows = gate_shape_rows; + *up_rows = up_shape_rows; + if (gate_id_out != NULL) { + *gate_id_out = gate_id; + } + if (up_id_out != NULL) { + *up_id_out = up_id; + } + (*executed_device_ops) += 2; + + add_assoc_str(ffn_gate_up_probe, "gate", zend_string_copy(gate_id)); + add_assoc_str(ffn_gate_up_probe, "up", zend_string_copy(up_id)); + add_assoc_bool(ffn_gate_up_probe, "executed", true); + add_assoc_bool(ffn_gate_up_probe, "precomputed", true); + add_assoc_bool(ffn_gate_up_probe, "used_prefill_ffn_gate_up", true); + add_assoc_bool(ffn_gate_up_probe, "retained_device_output", true); + add_assoc_long(ffn_gate_up_probe, "input_width", (zend_long) ffn_norm_width); + add_assoc_long(ffn_gate_up_probe, "gate_rows", (zend_long) gate_shape_rows); + add_assoc_long(ffn_gate_up_probe, "up_rows", (zend_long) up_shape_rows); + *used_prefilled_gate_up = true; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_try_prefilled_layer0_ffn_swiglu( + king_inference_model_object *model, + zval *graph, + zend_string *gate_id, + king_inference_cuda_device_ptr gate_output, + zend_ulong gate_rows, + zend_string *up_id, + king_inference_cuda_device_ptr up_output, + zend_ulong up_rows, + zend_ulong position, + zval *ffn_swiglu_probe, + king_inference_cuda_device_ptr *swiglu_output, + zend_ulong *swiglu_width, + zend_string **gated_id_out, + zend_ulong *executed_device_ops, + bool *used_prefilled_swiglu +) { + zval *silu_op; + zval *mul_op; + zend_string *silu_id = NULL; + zend_string *gated_id = NULL; + size_t source_offset; + + *used_prefilled_swiglu = false; + *swiglu_output = 0; + *swiglu_width = 0; + if (gated_id_out != NULL) { + *gated_id_out = NULL; + } + if (model->cuda_decoder_prompt_prefill_layer0_ffn_swiglu == 0 + || model->cuda_decoder_prompt_prefill_layer0_ffn_swiglu_width == 0 + || position >= model->cuda_decoder_prompt_prefill_tokens) { + return SUCCESS; + } + if (gate_id == NULL + || up_id == NULL + || gate_output == 0 + || up_output == 0 + || gate_rows == 0 + || gate_rows != up_rows + || gate_rows != model->cuda_decoder_prompt_prefill_layer0_ffn_swiglu_width + || gate_rows > (zend_ulong) (SIZE_MAX / sizeof(float))) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_ffn_swiglu_input_invalid" + ); + return FAILURE; + } + + silu_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, gate_id, "silu"); + add_assoc_string(ffn_swiglu_probe, "type", "gpu_decoder_ffn_swiglu_device_execution"); + add_assoc_bool(ffn_swiglu_probe, "attempted", silu_op != NULL); + add_assoc_bool(ffn_swiglu_probe, "available", model->cuda_decoder_graph_executor_ffn_swiglu_execution_available); + add_assoc_bool(ffn_swiglu_probe, "executed", false); + add_assoc_bool(ffn_swiglu_probe, "retained_device_output", false); + if (silu_op == NULL) { + return SUCCESS; + } + if (king_inference_graph_required_string(silu_op, "id", &silu_id) != SUCCESS + || (mul_op = king_inference_cuda_decoder_graph_executor_mul_op_for_inputs(graph, silu_id, up_id)) == NULL + || king_inference_graph_required_string(mul_op, "id", &gated_id) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_ffn_swiglu_contract_invalid" + ); + return FAILURE; + } + if (position > (zend_ulong) (SIZE_MAX / (size_t) gate_rows)) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_ffn_swiglu_offset_invalid" + ); + return FAILURE; + } + + if (king_inference_cuda_device_allocator_alloc( + model, + (size_t) gate_rows * sizeof(float), + swiglu_output + ) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_prefilled_ffn_swiglu_allocation_failed" + ); + return FAILURE; + } + + source_offset = (size_t) position * (size_t) gate_rows; + if (king_inference_cuda_vector_copy_to_offset( + model, + model->cuda_decoder_prompt_prefill_layer0_ffn_swiglu + + (king_inference_cuda_device_ptr) (source_offset * sizeof(float)), + *swiglu_output, + 0, + gate_rows + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, swiglu_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_vector_ops_result, + model->cuda_device_vector_ops_error[0] != '\0' + ? model->cuda_device_vector_ops_error + : "cuda_decoder_graph_prefilled_ffn_swiglu_copy_failed" + ); + return FAILURE; + } + + model->cuda_decoder_graph_executor_ffn_swiglu_execution_count++; + model->cuda_decoder_graph_executor_last_ffn_swiglu_width = gate_rows; + *swiglu_width = gate_rows; + if (gated_id_out != NULL) { + *gated_id_out = gated_id; + } + (*executed_device_ops)++; + + add_assoc_str(ffn_swiglu_probe, "gate", zend_string_copy(gate_id)); + add_assoc_str(ffn_swiglu_probe, "up", zend_string_copy(up_id)); + add_assoc_str(ffn_swiglu_probe, "silu", zend_string_copy(silu_id)); + add_assoc_str(ffn_swiglu_probe, "gated", zend_string_copy(gated_id)); + add_assoc_bool(ffn_swiglu_probe, "executed", true); + add_assoc_bool(ffn_swiglu_probe, "precomputed", true); + add_assoc_bool(ffn_swiglu_probe, "used_prefill_ffn_swiglu", true); + add_assoc_bool(ffn_swiglu_probe, "retained_device_output", true); + add_assoc_long(ffn_swiglu_probe, "width", (zend_long) gate_rows); + add_assoc_long(ffn_swiglu_probe, "bytes", (zend_long) ((size_t) gate_rows * sizeof(float))); + *used_prefilled_swiglu = true; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_try_prefilled_layer0_ffn_down_projection( + king_inference_model_object *model, + zval *graph, + zend_string *gated_id, + king_inference_cuda_device_ptr swiglu_output, + zend_ulong swiglu_width, + zend_ulong position, + zval *ffn_down_probe, + king_inference_cuda_device_ptr *down_output, + zend_ulong *down_width, + zend_string **down_id_out, + zend_ulong *executed_device_ops, + bool *used_prefilled_down +) { + zval *down_op; + zend_string *down_id = NULL; + zend_string *down_weight = NULL; + zend_ulong down_rows = 0; + size_t source_offset; + + *used_prefilled_down = false; + *down_output = 0; + *down_width = 0; + if (down_id_out != NULL) { + *down_id_out = NULL; + } + if (model->cuda_decoder_prompt_prefill_layer0_ffn_down == 0 + || model->cuda_decoder_prompt_prefill_layer0_ffn_down_width == 0 + || position >= model->cuda_decoder_prompt_prefill_tokens) { + return SUCCESS; + } + if (gated_id == NULL + || swiglu_output == 0 + || swiglu_width == 0 + || model->cuda_decoder_prompt_prefill_layer0_ffn_swiglu_width != swiglu_width) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_ffn_down_input_invalid" + ); + return FAILURE; + } + + down_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, gated_id, "linear"); + add_assoc_string(ffn_down_probe, "type", "gpu_decoder_ffn_down_projection_device_execution"); + add_assoc_bool(ffn_down_probe, "attempted", down_op != NULL); + add_assoc_bool(ffn_down_probe, "available", model->cuda_decoder_graph_executor_ffn_down_projection_execution_available); + add_assoc_bool(ffn_down_probe, "executed", false); + add_assoc_bool(ffn_down_probe, "retained_device_output", false); + if (down_op == NULL) { + return SUCCESS; + } + if (king_inference_graph_required_string(down_op, "id", &down_id) != SUCCESS + || king_inference_cuda_decoder_graph_executor_linear_shape( + model, + down_op, + swiglu_width, + &down_weight, + &down_rows + ) != SUCCESS + || down_rows == 0 + || down_rows != model->cuda_decoder_prompt_prefill_layer0_ffn_down_width + || down_rows > (zend_ulong) (SIZE_MAX / sizeof(float))) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + model->cuda_decoder_graph_executor_error[0] != '\0' + ? model->cuda_decoder_graph_executor_error + : "cuda_decoder_graph_prefilled_ffn_down_contract_invalid" + ); + return FAILURE; + } + (void) down_weight; + if (position > (zend_ulong) (SIZE_MAX / (size_t) down_rows)) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_ffn_down_offset_invalid" + ); + return FAILURE; + } + + if (king_inference_cuda_device_allocator_alloc( + model, + (size_t) down_rows * sizeof(float), + down_output + ) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_prefilled_ffn_down_allocation_failed" + ); + return FAILURE; + } + + source_offset = (size_t) position * (size_t) down_rows; + if (king_inference_cuda_vector_copy_to_offset( + model, + model->cuda_decoder_prompt_prefill_layer0_ffn_down + + (king_inference_cuda_device_ptr) (source_offset * sizeof(float)), + *down_output, + 0, + down_rows + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, down_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_vector_ops_result, + model->cuda_device_vector_ops_error[0] != '\0' + ? model->cuda_device_vector_ops_error + : "cuda_decoder_graph_prefilled_ffn_down_copy_failed" + ); + return FAILURE; + } + + model->cuda_decoder_graph_executor_ffn_down_projection_execution_count++; + model->cuda_decoder_graph_executor_last_ffn_down_rows = down_rows; + *down_width = down_rows; + if (down_id_out != NULL) { + *down_id_out = down_id; + } + (*executed_device_ops)++; + + add_assoc_str(ffn_down_probe, "input", zend_string_copy(gated_id)); + add_assoc_str(ffn_down_probe, "down", zend_string_copy(down_id)); + add_assoc_bool(ffn_down_probe, "executed", true); + add_assoc_bool(ffn_down_probe, "precomputed", true); + add_assoc_bool(ffn_down_probe, "used_prefill_ffn_down", true); + add_assoc_bool(ffn_down_probe, "retained_device_output", true); + add_assoc_long(ffn_down_probe, "input_width", (zend_long) swiglu_width); + add_assoc_long(ffn_down_probe, "rows", (zend_long) down_rows); + *used_prefilled_down = true; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_try_prefilled_layer0_ffn_output_residual( + king_inference_model_object *model, + zval *graph, + zend_string *residual_id, + king_inference_cuda_device_ptr residual_output, + zend_ulong residual_width, + zend_string *down_id, + king_inference_cuda_device_ptr down_output, + zend_ulong down_width, + zend_ulong position, + zval *ffn_output_probe, + king_inference_cuda_device_ptr *output, + zend_ulong *output_width, + zend_string **output_id_out, + zend_ulong *executed_device_ops, + bool *used_prefilled_output +) { + zval *output_op; + zend_string *output_id = NULL; + size_t source_offset; + + *used_prefilled_output = false; + *output = 0; + *output_width = 0; + if (output_id_out != NULL) { + *output_id_out = NULL; + } + if (model->cuda_decoder_prompt_prefill_layer0_ffn_output_residual == 0 + || model->cuda_decoder_prompt_prefill_layer0_ffn_output_residual_width == 0 + || position >= model->cuda_decoder_prompt_prefill_tokens) { + return SUCCESS; + } + if (residual_id == NULL + || down_id == NULL + || residual_output == 0 + || down_output == 0 + || residual_width == 0 + || residual_width != down_width + || residual_width != model->cuda_decoder_prompt_prefill_layer0_ffn_output_residual_width + || residual_width > (zend_ulong) (SIZE_MAX / sizeof(float))) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_ffn_output_residual_input_invalid" + ); + return FAILURE; + } + + output_op = king_inference_cuda_decoder_graph_executor_add_op_for_inputs(graph, residual_id, down_id); + add_assoc_string(ffn_output_probe, "type", "gpu_decoder_ffn_output_residual_device_execution"); + add_assoc_bool(ffn_output_probe, "attempted", output_op != NULL); + add_assoc_bool(ffn_output_probe, "available", model->cuda_decoder_graph_executor_ffn_output_residual_execution_available); + add_assoc_bool(ffn_output_probe, "executed", false); + add_assoc_bool(ffn_output_probe, "retained_device_output", false); + if (output_op == NULL) { + return SUCCESS; + } + if (king_inference_graph_required_string(output_op, "id", &output_id) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_ffn_output_residual_contract_invalid" + ); + return FAILURE; + } + if (position > (zend_ulong) (SIZE_MAX / (size_t) residual_width)) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_ffn_output_residual_offset_invalid" + ); + return FAILURE; + } + + if (king_inference_cuda_device_allocator_alloc( + model, + (size_t) residual_width * sizeof(float), + output + ) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_prefilled_ffn_output_residual_allocation_failed" + ); + return FAILURE; + } + + source_offset = (size_t) position * (size_t) residual_width; + if (king_inference_cuda_vector_copy_to_offset( + model, + model->cuda_decoder_prompt_prefill_layer0_ffn_output_residual + + (king_inference_cuda_device_ptr) (source_offset * sizeof(float)), + *output, + 0, + residual_width + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_vector_ops_result, + model->cuda_device_vector_ops_error[0] != '\0' + ? model->cuda_device_vector_ops_error + : "cuda_decoder_graph_prefilled_ffn_output_residual_copy_failed" + ); + return FAILURE; + } + + model->cuda_decoder_graph_executor_ffn_output_residual_execution_count++; + model->cuda_decoder_graph_executor_last_ffn_output_residual_width = residual_width; + *output_width = residual_width; + if (output_id_out != NULL) { + *output_id_out = output_id; + } + (*executed_device_ops)++; + + add_assoc_str(ffn_output_probe, "residual", zend_string_copy(residual_id)); + add_assoc_str(ffn_output_probe, "down", zend_string_copy(down_id)); + add_assoc_str(ffn_output_probe, "output", zend_string_copy(output_id)); + add_assoc_bool(ffn_output_probe, "executed", true); + add_assoc_bool(ffn_output_probe, "precomputed", true); + add_assoc_bool(ffn_output_probe, "used_prefill_ffn_output_residual", true); + add_assoc_bool(ffn_output_probe, "retained_device_output", true); + add_assoc_long(ffn_output_probe, "width", (zend_long) residual_width); + add_assoc_long(ffn_output_probe, "bytes", (zend_long) ((size_t) residual_width * sizeof(float))); + *used_prefilled_output = true; + return SUCCESS; +} diff --git a/extension/src/inference/cuda/decoder_graph/prefill/kv/cuda_decoder_graph_executor_prefilled_kv_ops.inc b/extension/src/inference/cuda/decoder_graph/prefill/kv/cuda_decoder_graph_executor_prefilled_kv_ops.inc new file mode 100644 index 000000000..614dccad5 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/prefill/kv/cuda_decoder_graph_executor_prefilled_kv_ops.inc @@ -0,0 +1,776 @@ +/* + * CUDA decoder graph helpers for reusing batch-prefilled K/V cache rows. + */ + +static bool king_inference_cuda_decoder_graph_executor_prefilled_kv_batch_active( + king_inference_model_object *model +) { + return model->cuda_decoder_prompt_prefill_embeddings_active + && model->cuda_decoder_prompt_prefill_tokens > 0 + && model->cuda_decoder_prompt_prefill_kv_heads > 0 + && model->cuda_decoder_prompt_prefill_kv_cache_writes > 0 + && model->cuda_device_kv_cache_available + && model->cuda_device_kv_cache_key_length > 0 + && model->cuda_device_kv_cache_value_length > 0; +} + +static zend_result king_inference_cuda_decoder_graph_executor_try_prefilled_layer0_attention_stack( + king_inference_model_object *model, + zval *graph, + zend_ulong position, + zval *slice_probe, + zval *rope_probe, + zval *kv_probe, + king_inference_cuda_device_ptr *attention_stack_output, + zend_ulong *attention_stack_width, + zend_string **attention_stack_id, + zend_ulong *executed_device_ops, + bool *used_prefilled_attention_stack +) { + zval *stack_op; + zend_string *stack_id = NULL; + size_t source_offset; + + *used_prefilled_attention_stack = false; + if (model->cuda_decoder_prompt_prefill_layer0_attention_stack == 0 + || model->cuda_decoder_prompt_prefill_layer0_attention_stack_width == 0 + || model->cuda_decoder_prompt_prefill_layer0_attention_heads == 0 + || position >= model->cuda_decoder_prompt_prefill_tokens) { + return SUCCESS; + } + if (*attention_stack_output != 0 || *attention_stack_width != 0 || *attention_stack_id != NULL) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_attention_stack_target_dirty" + ); + return FAILURE; + } + + stack_op = king_inference_cuda_decoder_graph_executor_first_op(graph, "stack"); + if (stack_op == NULL || king_inference_graph_required_string(stack_op, "id", &stack_id) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_attention_stack_id_invalid" + ); + return FAILURE; + } + if (model->cuda_decoder_prompt_prefill_layer0_attention_stack_width > (zend_ulong) (SIZE_MAX / sizeof(float)) + || position > (zend_ulong) (SIZE_MAX / (size_t) model->cuda_decoder_prompt_prefill_layer0_attention_stack_width)) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_attention_stack_size_invalid" + ); + return FAILURE; + } + + *attention_stack_width = model->cuda_decoder_prompt_prefill_layer0_attention_stack_width; + if (king_inference_cuda_device_allocator_alloc( + model, + (size_t) *attention_stack_width * sizeof(float), + attention_stack_output + ) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_prefilled_attention_stack_allocation_failed" + ); + return FAILURE; + } + + source_offset = (size_t) position * (size_t) *attention_stack_width; + if (king_inference_cuda_vector_copy_to_offset( + model, + model->cuda_decoder_prompt_prefill_layer0_attention_stack + + (king_inference_cuda_device_ptr) (source_offset * sizeof(float)), + *attention_stack_output, + 0, + *attention_stack_width + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, attention_stack_output); + *attention_stack_width = 0; + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_vector_ops_result, + model->cuda_device_vector_ops_error[0] != '\0' + ? model->cuda_device_vector_ops_error + : "cuda_decoder_graph_prefilled_attention_stack_copy_failed" + ); + return FAILURE; + } + + *attention_stack_id = stack_id; + (*executed_device_ops)++; + model->cuda_decoder_graph_executor_attention_heads_execution_count++; + model->cuda_decoder_graph_executor_attention_stack_execution_count++; + model->cuda_decoder_graph_executor_last_attention_stack_expected_heads = + model->cuda_decoder_prompt_prefill_layer0_attention_heads; + model->cuda_decoder_graph_executor_last_attention_stack_prepared_heads = + model->cuda_decoder_prompt_prefill_layer0_attention_heads; + model->cuda_decoder_graph_executor_last_attention_stack_width = *attention_stack_width; + + add_assoc_bool(slice_probe, "attempted", true); + add_assoc_bool(slice_probe, "executed", false); + add_assoc_bool(slice_probe, "used_prefill_attention_stack", true); + add_assoc_long( + slice_probe, + "query_head_count", + (zend_long) model->cuda_decoder_prompt_prefill_layer0_attention_heads + ); + add_assoc_bool(rope_probe, "attempted", true); + add_assoc_bool(rope_probe, "executed", false); + add_assoc_bool(rope_probe, "used_prefill_attention_stack", true); + add_assoc_bool(kv_probe, "attempted", true); + add_assoc_bool(kv_probe, "available", true); + add_assoc_bool(kv_probe, "executed", true); + add_assoc_bool(kv_probe, "retained_device_output", true); + add_assoc_bool(kv_probe, "used_prefill_attention_stack", true); + add_assoc_bool(kv_probe, "complete_attention_stack", true); + add_assoc_bool(kv_probe, "attention_stack_released", false); + add_assoc_long(kv_probe, "attention_stack_width", (zend_long) *attention_stack_width); + add_assoc_long( + kv_probe, + "expected_heads", + (zend_long) model->cuda_decoder_prompt_prefill_layer0_attention_heads + ); + add_assoc_long( + kv_probe, + "prepared_heads", + (zend_long) model->cuda_decoder_prompt_prefill_layer0_attention_heads + ); + + *used_prefilled_attention_stack = true; + return SUCCESS; +} + +static zval *king_inference_cuda_decoder_graph_executor_prefilled_add_op_for_inputs( + zval *graph, + zend_string *left_id, + zend_string *right_id +) { + zval *ops = king_inference_cuda_decoder_graph_executor_ops(graph); + zval *entry; + + if (ops == NULL || left_id == NULL || right_id == NULL) { + return NULL; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(ops), entry) { + zval *op_name; + zval *left; + zval *right; + + if (entry == NULL || Z_TYPE_P(entry) != IS_ARRAY) { + continue; + } + op_name = king_inference_array_find(entry, "op"); + left = king_inference_array_find(entry, "left"); + right = king_inference_array_find(entry, "right"); + if (op_name != NULL + && Z_TYPE_P(op_name) == IS_STRING + && zend_string_equals_literal(Z_STR_P(op_name), "add") + && ((king_inference_cuda_decoder_graph_executor_zval_string_equals(left, left_id) + && king_inference_cuda_decoder_graph_executor_zval_string_equals(right, right_id)) + || (king_inference_cuda_decoder_graph_executor_zval_string_equals(left, right_id) + && king_inference_cuda_decoder_graph_executor_zval_string_equals(right, left_id)))) { + return entry; + } + } ZEND_HASH_FOREACH_END(); + + return NULL; +} + +static zend_result king_inference_cuda_decoder_graph_executor_try_prefilled_layer0_attention_residual( + king_inference_model_object *model, + zval *graph, + zend_string *hidden_id, + zend_ulong hidden_width, + zend_ulong position, + zval *slice_probe, + zval *rope_probe, + zval *kv_probe, + zval *projection_probe, + zval *residual_probe, + king_inference_cuda_device_ptr *residual_output, + zend_ulong *residual_width, + zend_string **residual_id_out, + zend_ulong *executed_device_ops, + bool *used_prefilled_attention_residual +) { + zval *stack_op; + zval *projection_op; + zval *residual_op; + zend_string *stack_id = NULL; + zend_string *projection_id = NULL; + zend_string *residual_id = NULL; + size_t source_offset; + + *used_prefilled_attention_residual = false; + if (model->cuda_decoder_prompt_prefill_layer0_attention_residual == 0 + || model->cuda_decoder_prompt_prefill_layer0_attention_residual_width == 0 + || position >= model->cuda_decoder_prompt_prefill_tokens) { + return SUCCESS; + } + if (*residual_output != 0 || *residual_width != 0 || *residual_id_out != NULL) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_attention_residual_target_dirty" + ); + return FAILURE; + } + if (hidden_id == NULL + || hidden_width == 0 + || hidden_width != model->cuda_decoder_prompt_prefill_layer0_attention_residual_width + || hidden_width > (zend_ulong) (SIZE_MAX / sizeof(float))) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_attention_residual_shape_invalid" + ); + return FAILURE; + } + + stack_op = king_inference_cuda_decoder_graph_executor_first_op(graph, "stack"); + if (stack_op == NULL || king_inference_graph_required_string(stack_op, "id", &stack_id) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_attention_residual_stack_id_invalid" + ); + return FAILURE; + } + projection_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, stack_id, "linear"); + if (projection_op == NULL || king_inference_graph_required_string(projection_op, "id", &projection_id) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_attention_residual_projection_id_invalid" + ); + return FAILURE; + } + residual_op = king_inference_cuda_decoder_graph_executor_prefilled_add_op_for_inputs(graph, hidden_id, projection_id); + if (residual_op == NULL) { + return SUCCESS; + } + if (king_inference_graph_required_string(residual_op, "id", &residual_id) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_attention_residual_id_invalid" + ); + return FAILURE; + } + if (position > (zend_ulong) (SIZE_MAX / (size_t) hidden_width)) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_attention_residual_offset_invalid" + ); + return FAILURE; + } + + if (king_inference_cuda_device_allocator_alloc( + model, + (size_t) hidden_width * sizeof(float), + residual_output + ) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_prefilled_attention_residual_allocation_failed" + ); + return FAILURE; + } + + source_offset = (size_t) position * (size_t) hidden_width; + if (king_inference_cuda_vector_copy_to_offset( + model, + model->cuda_decoder_prompt_prefill_layer0_attention_residual + + (king_inference_cuda_device_ptr) (source_offset * sizeof(float)), + *residual_output, + 0, + hidden_width + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, residual_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_vector_ops_result, + model->cuda_device_vector_ops_error[0] != '\0' + ? model->cuda_device_vector_ops_error + : "cuda_decoder_graph_prefilled_attention_residual_copy_failed" + ); + return FAILURE; + } + + *residual_width = hidden_width; + *residual_id_out = residual_id; + (*executed_device_ops)++; + model->cuda_decoder_graph_executor_attention_residual_execution_count++; + model->cuda_decoder_graph_executor_last_attention_residual_width = hidden_width; + + add_assoc_bool(slice_probe, "attempted", true); + add_assoc_bool(slice_probe, "executed", false); + add_assoc_bool(slice_probe, "used_prefill_attention_residual", true); + add_assoc_bool(rope_probe, "attempted", true); + add_assoc_bool(rope_probe, "executed", false); + add_assoc_bool(rope_probe, "used_prefill_attention_residual", true); + add_assoc_bool(kv_probe, "attempted", true); + add_assoc_bool(kv_probe, "available", true); + add_assoc_bool(kv_probe, "executed", true); + add_assoc_bool(kv_probe, "retained_device_output", false); + add_assoc_bool(kv_probe, "used_prefill_attention_residual", true); + add_assoc_bool(kv_probe, "complete_attention_stack", true); + add_assoc_long(kv_probe, "attention_stack_width", (zend_long) model->cuda_decoder_prompt_prefill_layer0_attention_stack_width); + + add_assoc_string(projection_probe, "type", "gpu_decoder_attention_output_projection_device_execution"); + add_assoc_bool(projection_probe, "attempted", true); + add_assoc_bool(projection_probe, "available", model->cuda_decoder_graph_executor_attention_output_projection_execution_available); + add_assoc_bool(projection_probe, "executed", false); + add_assoc_bool(projection_probe, "precomputed", true); + add_assoc_bool(projection_probe, "used_prefill_attention_residual", true); + add_assoc_bool(projection_probe, "retained_device_output", false); + add_assoc_bool(projection_probe, "projection_output_released", true); + add_assoc_str(projection_probe, "input", zend_string_copy(stack_id)); + add_assoc_str(projection_probe, "projection", zend_string_copy(projection_id)); + add_assoc_long(projection_probe, "projection_width", (zend_long) hidden_width); + + add_assoc_string(residual_probe, "type", "gpu_decoder_attention_residual_device_execution"); + add_assoc_bool(residual_probe, "attempted", true); + add_assoc_bool(residual_probe, "available", model->cuda_decoder_graph_executor_attention_residual_execution_available); + add_assoc_bool(residual_probe, "executed", true); + add_assoc_bool(residual_probe, "precomputed", true); + add_assoc_bool(residual_probe, "used_prefill_attention_residual", true); + add_assoc_bool(residual_probe, "retained_device_output", true); + add_assoc_str(residual_probe, "residual", zend_string_copy(residual_id)); + add_assoc_long(residual_probe, "width", (zend_long) hidden_width); + add_assoc_long(residual_probe, "bytes", (zend_long) ((size_t) hidden_width * sizeof(float))); + + *used_prefilled_attention_residual = true; + return SUCCESS; +} + +static bool king_inference_cuda_decoder_graph_executor_prefilled_query_rope_vector( + king_inference_model_object *model, + zend_string *query_slice_id, + zend_ulong position, + zend_ulong query_slice_length, + king_inference_cuda_device_ptr *query_rope_output, + zend_ulong *query_head +) { + unsigned long parsed_layer = 0; + unsigned long parsed_head = 0; + int consumed = 0; + size_t offset_elements; + + *query_rope_output = 0; + *query_head = 0; + if (query_slice_id == NULL + || model->cuda_decoder_prompt_prefill_initial_query_rope == 0 + || model->cuda_decoder_prompt_prefill_initial_query_rope_heads == 0 + || model->cuda_decoder_prompt_prefill_initial_query_rope_head_dim == 0 + || query_slice_length != model->cuda_decoder_prompt_prefill_initial_query_rope_head_dim + || position >= model->cuda_decoder_prompt_prefill_tokens + || ZSTR_LEN(query_slice_id) > INT_MAX + || sscanf(ZSTR_VAL(query_slice_id), "l%lu_h%lu_q_slice%n", &parsed_layer, &parsed_head, &consumed) != 2 + || consumed != (int) ZSTR_LEN(query_slice_id) + || parsed_layer != 0 + || (zend_ulong) parsed_head >= model->cuda_decoder_prompt_prefill_initial_query_rope_heads) { + return false; + } + if ((size_t) parsed_head > (SIZE_MAX / (size_t) model->cuda_decoder_prompt_prefill_tokens) + || ((size_t) parsed_head * (size_t) model->cuda_decoder_prompt_prefill_tokens) > SIZE_MAX - (size_t) position) { + return false; + } + offset_elements = ((size_t) parsed_head * (size_t) model->cuda_decoder_prompt_prefill_tokens) + (size_t) position; + if (offset_elements > SIZE_MAX / (size_t) model->cuda_decoder_prompt_prefill_initial_query_rope_head_dim + || offset_elements * (size_t) model->cuda_decoder_prompt_prefill_initial_query_rope_head_dim > SIZE_MAX / sizeof(float)) { + return false; + } + + offset_elements *= (size_t) model->cuda_decoder_prompt_prefill_initial_query_rope_head_dim; + *query_rope_output = model->cuda_decoder_prompt_prefill_initial_query_rope + + (king_inference_cuda_device_ptr) (offset_elements * sizeof(float)); + *query_head = (zend_ulong) parsed_head; + return true; +} + +static zend_result king_inference_cuda_decoder_graph_executor_try_prefilled_kv_head_prepare( + king_inference_model_object *model, + zval *graph, + zend_string *key_linear_id, + zend_string *value_linear_id, + zend_string *query_slice_id, + king_inference_cuda_device_ptr query_slice_output, + zend_ulong query_slice_length, + zval *query_rope_probe, + zval *kv_probe, + king_inference_cuda_device_ptr *attention_stack_output, + zend_ulong *attention_stack_width, + zend_string **attention_stack_id, + zend_ulong *executed_device_ops, + bool *kv_prepared, + bool *used_prefilled_kv +) { + zval *key_slice_op = NULL; + zval *key_rope_op = NULL; + zval *value_slice_op = NULL; + zval *kv_write_op = NULL; + zval *query_rope_op = NULL; + zval *kv_attention_op = NULL; + zval key_rope_probe; + zval kv_write_probe; + zval kv_attention_probe; + zval attention_stack_probe; + zend_string *key_slice_id = NULL; + zend_string *key_rope_id = NULL; + zend_string *value_slice_id = NULL; + zend_string *query_rope_id = NULL; + zend_string *kv_attention_id = NULL; + king_inference_cuda_device_ptr query_rope_output = 0; + king_inference_cuda_device_ptr attention_context_output = 0; + zend_ulong layer = 0; + zend_ulong head = 0; + zend_ulong position = 0; + zend_ulong query_rope_length = 0; + zend_ulong attention_context_length = 0; + zend_ulong query_head = 0; + bool query_rope_executed = false; + bool used_prefilled_query_rope = false; + + *used_prefilled_kv = false; + if (!king_inference_cuda_decoder_graph_executor_prefilled_kv_batch_active(model)) { + return SUCCESS; + } + + key_slice_op = king_inference_cuda_decoder_graph_executor_kv_slice_op_for_query( + graph, + key_linear_id, + query_slice_id, + "k" + ); + if (key_slice_op == NULL || king_inference_graph_required_string(key_slice_op, "id", &key_slice_id) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_kv_key_slice_contract_invalid" + ); + return FAILURE; + } + key_rope_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, key_slice_id, "rope"); + if (key_rope_op == NULL || king_inference_graph_required_string(key_rope_op, "id", &key_rope_id) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_kv_key_rope_contract_invalid" + ); + return FAILURE; + } + value_slice_op = king_inference_cuda_decoder_graph_executor_kv_slice_op_for_query( + graph, + value_linear_id, + query_slice_id, + "v" + ); + if (value_slice_op == NULL || king_inference_graph_required_string(value_slice_op, "id", &value_slice_id) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_kv_value_slice_contract_invalid" + ); + return FAILURE; + } + + kv_write_op = king_inference_cuda_decoder_graph_executor_kv_write_op_for_vectors( + graph, + key_rope_id, + value_slice_id + ); + if (kv_write_op == NULL + || king_inference_cuda_decoder_graph_executor_kv_write_shape( + model, + kv_write_op, + model->cuda_device_kv_cache_key_length, + model->cuda_device_kv_cache_value_length, + &layer, + &head, + &position + ) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_kv_write_contract_invalid" + ); + return FAILURE; + } + if (layer != 0 + || head >= model->cuda_decoder_prompt_prefill_kv_heads + || position >= model->cuda_decoder_prompt_prefill_tokens) { + return SUCCESS; + } + + query_rope_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, query_slice_id, "rope"); + if (query_slice_id == NULL + || query_rope_op == NULL + || king_inference_graph_required_string(query_rope_op, "id", &query_rope_id) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_kv_query_rope_contract_invalid" + ); + return FAILURE; + } + + used_prefilled_query_rope = king_inference_cuda_decoder_graph_executor_prefilled_query_rope_vector( + model, + query_slice_id, + position, + query_slice_length, + &query_rope_output, + &query_head + ); + if (!used_prefilled_query_rope && query_slice_output == 0) { + return SUCCESS; + } + + array_init(&key_rope_probe); + add_assoc_string(&key_rope_probe, "type", "gpu_decoder_prefilled_key_rope_device_execution"); + add_assoc_bool(&key_rope_probe, "attempted", true); + add_assoc_bool(&key_rope_probe, "available", true); + add_assoc_bool(&key_rope_probe, "executed", true); + add_assoc_bool(&key_rope_probe, "retained_device_output", false); + add_assoc_bool(&key_rope_probe, "used_prefill_kv_cache", true); + add_assoc_long(&key_rope_probe, "length", (zend_long) model->cuda_device_kv_cache_key_length); + add_assoc_long(&key_rope_probe, "position", (zend_long) position); + + array_init(&kv_write_probe); + add_assoc_string(&kv_write_probe, "type", "gpu_decoder_prefilled_kv_write_device_execution"); + add_assoc_bool(&kv_write_probe, "attempted", true); + add_assoc_bool(&kv_write_probe, "available", true); + add_assoc_bool(&kv_write_probe, "executed", true); + add_assoc_bool(&kv_write_probe, "used_prefill_kv_cache", true); + add_assoc_long(&kv_write_probe, "layer", (zend_long) layer); + add_assoc_long(&kv_write_probe, "head", (zend_long) head); + add_assoc_long(&kv_write_probe, "position", (zend_long) position); + add_assoc_long(&kv_write_probe, "key_length", (zend_long) model->cuda_device_kv_cache_key_length); + add_assoc_long(&kv_write_probe, "value_length", (zend_long) model->cuda_device_kv_cache_value_length); + + if (used_prefilled_query_rope) { + query_rope_length = query_slice_length; + query_rope_executed = true; + add_assoc_bool(query_rope_probe, "attempted", true); + add_assoc_bool(query_rope_probe, "available", true); + add_assoc_bool(query_rope_probe, "executed", true); + add_assoc_bool(query_rope_probe, "retained_device_output", false); + add_assoc_bool(query_rope_probe, "used_prefill_query_rope", true); + add_assoc_long(query_rope_probe, "head", (zend_long) query_head); + add_assoc_long(query_rope_probe, "position", (zend_long) position); + add_assoc_long(query_rope_probe, "length", (zend_long) query_rope_length); + } else if (king_inference_cuda_decoder_graph_executor_execute_rope_after_slice_to_device( + model, + graph, + query_slice_id, + query_slice_output, + query_slice_length, + query_rope_probe, + executed_device_ops, + &query_rope_output, + &query_rope_length, + NULL, + &query_rope_executed + ) != SUCCESS) { + zval_ptr_dtor(&kv_write_probe); + zval_ptr_dtor(&key_rope_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &query_rope_output); + return FAILURE; + } + if (!query_rope_executed) { + zval_ptr_dtor(&kv_write_probe); + zval_ptr_dtor(&key_rope_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &query_rope_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_kv_query_rope_missing" + ); + return FAILURE; + } + + kv_attention_op = king_inference_cuda_decoder_graph_executor_kv_attention_op_for_query(graph, query_rope_id); + array_init(&kv_attention_probe); + add_assoc_string(&kv_attention_probe, "type", "gpu_decoder_prefilled_kv_attention_device_execution"); + if (king_inference_cuda_decoder_graph_executor_execute_kv_attention( + model, + kv_attention_op, + query_rope_output, + query_rope_length, + &kv_attention_probe, + &attention_context_output, + &attention_context_length, + executed_device_ops + ) != SUCCESS) { + zval_ptr_dtor(&kv_attention_probe); + zval_ptr_dtor(&kv_write_probe); + zval_ptr_dtor(&key_rope_probe); + if (!used_prefilled_query_rope) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &query_rope_output); + } + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &attention_context_output); + return FAILURE; + } + if (kv_attention_op == NULL + || king_inference_graph_required_string(kv_attention_op, "id", &kv_attention_id) != SUCCESS) { + zval_ptr_dtor(&kv_attention_probe); + zval_ptr_dtor(&kv_write_probe); + zval_ptr_dtor(&key_rope_probe); + if (!used_prefilled_query_rope) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &query_rope_output); + } + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &attention_context_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_kv_attention_id_invalid" + ); + return FAILURE; + } + if (!used_prefilled_query_rope + && king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &query_rope_output) != SUCCESS) { + zval_ptr_dtor(&kv_attention_probe); + zval_ptr_dtor(&kv_write_probe); + zval_ptr_dtor(&key_rope_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &attention_context_output); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_prefilled_kv_query_rope_free_failed" + ); + return FAILURE; + } + add_assoc_bool(query_rope_probe, "retained_device_output", false); + + array_init(&attention_stack_probe); + add_assoc_string(&attention_stack_probe, "type", "gpu_decoder_prefilled_kv_attention_stack_slot_device_execution"); + if (king_inference_cuda_decoder_graph_executor_prepare_attention_stack_slot( + model, + graph, + kv_attention_id, + attention_context_output, + attention_context_length, + &attention_stack_probe, + attention_stack_output, + attention_stack_width, + attention_stack_id, + executed_device_ops + ) != SUCCESS) { + zval_ptr_dtor(&attention_stack_probe); + zval_ptr_dtor(&kv_attention_probe); + zval_ptr_dtor(&kv_write_probe); + zval_ptr_dtor(&key_rope_probe); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &attention_context_output); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &attention_context_output) != SUCCESS) { + zval_ptr_dtor(&attention_stack_probe); + zval_ptr_dtor(&kv_attention_probe); + zval_ptr_dtor(&kv_write_probe); + zval_ptr_dtor(&key_rope_probe); + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_prefilled_kv_attention_context_free_failed" + ); + return FAILURE; + } + + model->cuda_decoder_graph_executor_kv_head_prepare_execution_count++; + model->cuda_decoder_graph_executor_last_kv_head_key_length = model->cuda_device_kv_cache_key_length; + model->cuda_decoder_graph_executor_last_kv_head_value_length = model->cuda_device_kv_cache_value_length; + add_assoc_bool(&kv_attention_probe, "retained_device_output", false); + add_assoc_bool(kv_probe, "executed", true); + add_assoc_bool(kv_probe, "used_prefill_kv_cache", true); + add_assoc_bool(kv_probe, "used_prefill_query_rope", used_prefilled_query_rope); + add_assoc_bool(kv_probe, "query_rope_executed", query_rope_executed); + add_assoc_long(kv_probe, "key_slice_length", (zend_long) model->cuda_device_kv_cache_key_length); + add_assoc_long(kv_probe, "key_rope_length", (zend_long) model->cuda_device_kv_cache_key_length); + add_assoc_long(kv_probe, "value_slice_length", (zend_long) model->cuda_device_kv_cache_value_length); + add_assoc_long(kv_probe, "query_rope_length", (zend_long) query_rope_length); + add_assoc_long(kv_probe, "attention_context_length", (zend_long) attention_context_length); + add_assoc_long(kv_probe, "attention_stack_width", (zend_long) *attention_stack_width); + add_assoc_bool(kv_probe, "attention_context_released", true); + add_assoc_bool(kv_probe, "attention_stack_released", false); + add_assoc_zval(kv_probe, "key_rope_execution", &key_rope_probe); + add_assoc_zval(kv_probe, "kv_write_execution", &kv_write_probe); + add_assoc_zval(kv_probe, "kv_attention_execution", &kv_attention_probe); + add_assoc_zval(kv_probe, "attention_stack_slot_execution", &attention_stack_probe); + *kv_prepared = true; + *used_prefilled_kv = true; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_try_prefilled_kv_head_prepare_for_norm( + king_inference_model_object *model, + zval *graph, + zend_string *rms_norm_id, + zend_string *query_slice_id, + zend_ulong query_slice_length, + zval *query_rope_probe, + zval *kv_probe, + king_inference_cuda_device_ptr *attention_stack_output, + zend_ulong *attention_stack_width, + zend_string **attention_stack_id, + zend_ulong *executed_device_ops, + bool *kv_prepared, + bool *used_prefilled_kv +) { + zval *key_linear_op; + zval *value_linear_op; + zend_string *key_linear_id = NULL; + zend_string *value_linear_id = NULL; + + *used_prefilled_kv = false; + if (!king_inference_cuda_decoder_graph_executor_prefilled_kv_batch_active(model)) { + return SUCCESS; + } + key_linear_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, rms_norm_id, "linear", 1); + value_linear_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, rms_norm_id, "linear", 2); + if (key_linear_op == NULL + || value_linear_op == NULL + || king_inference_graph_required_string(key_linear_op, "id", &key_linear_id) != SUCCESS + || king_inference_graph_required_string(value_linear_op, "id", &value_linear_id) != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_kv_linear_contract_invalid" + ); + return FAILURE; + } + + return king_inference_cuda_decoder_graph_executor_try_prefilled_kv_head_prepare( + model, + graph, + key_linear_id, + value_linear_id, + query_slice_id, + 0, + query_slice_length, + query_rope_probe, + kv_probe, + attention_stack_output, + attention_stack_width, + attention_stack_id, + executed_device_ops, + kv_prepared, + used_prefilled_kv + ); +} diff --git a/extension/src/inference/cuda/decoder_graph/prefill/residual/cuda_decoder_graph_executor_prefilled_residual_ops.inc b/extension/src/inference/cuda/decoder_graph/prefill/residual/cuda_decoder_graph_executor_prefilled_residual_ops.inc new file mode 100644 index 000000000..cf9bfcce3 --- /dev/null +++ b/extension/src/inference/cuda/decoder_graph/prefill/residual/cuda_decoder_graph_executor_prefilled_residual_ops.inc @@ -0,0 +1,673 @@ +/* + * CUDA decoder graph helpers for continuing from a batch-prefilled attention residual. + */ + +static zend_result king_inference_cuda_decoder_graph_executor_execute_prefilled_attention_residual_ffn_norm_gate_up_swiglu_down_and_output( + king_inference_model_object *model, + zval *graph, + zend_string *residual_id, + king_inference_cuda_device_ptr *residual_output, + zend_ulong residual_width, + zend_ulong prefill_batch_index, + zval *residual_probe, + zval *ffn_norm_probe, + zval *ffn_gate_up_probe, + zval *ffn_swiglu_probe, + zval *ffn_down_probe, + zval *ffn_output_probe, + zval *final_norm_probe, + zval *logits_probe, + zend_ulong *executed_device_ops, + bool *ffn_norm_executed, + bool *ffn_gate_up_executed, + bool *ffn_swiglu_executed, + bool *ffn_down_executed, + bool *ffn_output_executed, + bool *final_norm_executed, + bool *logits_executed, + zend_string **next_hidden_id_out, + king_inference_cuda_device_ptr *next_hidden_output, + zend_ulong *next_hidden_width +) { + zend_string *ffn_norm_id = NULL; + zend_string *ffn_gate_id = NULL; + zend_string *ffn_up_id = NULL; + zend_string *ffn_gated_id = NULL; + zend_string *ffn_down_id = NULL; + zend_string *ffn_output_id = NULL; + zend_string *final_norm_id = NULL; + zend_string *logits_id = NULL; + king_inference_cuda_device_ptr ffn_norm_output = 0; + king_inference_cuda_device_ptr ffn_gate_output = 0; + king_inference_cuda_device_ptr ffn_up_output = 0; + king_inference_cuda_device_ptr ffn_swiglu_output = 0; + king_inference_cuda_device_ptr ffn_down_output = 0; + king_inference_cuda_device_ptr ffn_output_residual = 0; + king_inference_cuda_device_ptr final_norm_output = 0; + king_inference_cuda_device_ptr logits_output = 0; + zend_ulong ffn_norm_width = 0; + zend_ulong ffn_gate_rows = 0; + zend_ulong ffn_up_rows = 0; + zend_ulong ffn_swiglu_width = 0; + zend_ulong ffn_down_width = 0; + zend_ulong ffn_output_width = 0; + zend_ulong final_norm_width = 0; + zend_ulong logits_rows = 0; + zend_result release_result = SUCCESS; + bool used_prefill_ffn_norm = false; + bool used_prefill_ffn_gate_up = false; + bool used_prefill_ffn_swiglu = false; + bool used_prefill_ffn_down = false; + bool used_prefill_ffn_output = false; + + if (next_hidden_id_out != NULL) { + *next_hidden_id_out = NULL; + } + if (next_hidden_output != NULL) { + *next_hidden_output = 0; + } + if (next_hidden_width != NULL) { + *next_hidden_width = 0; + } + *ffn_norm_executed = false; + *ffn_gate_up_executed = false; + *ffn_swiglu_executed = false; + *ffn_down_executed = false; + *ffn_output_executed = false; + *final_norm_executed = false; + *logits_executed = false; + add_assoc_string(ffn_gate_up_probe, "type", "gpu_decoder_ffn_gate_up_projection_device_execution"); + add_assoc_bool(ffn_gate_up_probe, "attempted", false); + add_assoc_bool(ffn_gate_up_probe, "available", model->cuda_decoder_graph_executor_ffn_gate_up_projection_execution_available); + add_assoc_bool(ffn_gate_up_probe, "executed", false); + add_assoc_bool(ffn_gate_up_probe, "retained_device_output", false); + add_assoc_string(ffn_swiglu_probe, "type", "gpu_decoder_ffn_swiglu_device_execution"); + add_assoc_bool(ffn_swiglu_probe, "attempted", false); + add_assoc_bool(ffn_swiglu_probe, "available", model->cuda_decoder_graph_executor_ffn_swiglu_execution_available); + add_assoc_bool(ffn_swiglu_probe, "executed", false); + add_assoc_bool(ffn_swiglu_probe, "retained_device_output", false); + add_assoc_string(ffn_down_probe, "type", "gpu_decoder_ffn_down_projection_device_execution"); + add_assoc_bool(ffn_down_probe, "attempted", false); + add_assoc_bool(ffn_down_probe, "available", model->cuda_decoder_graph_executor_ffn_down_projection_execution_available); + add_assoc_bool(ffn_down_probe, "executed", false); + add_assoc_bool(ffn_down_probe, "retained_device_output", false); + add_assoc_string(ffn_output_probe, "type", "gpu_decoder_ffn_output_residual_device_execution"); + add_assoc_bool(ffn_output_probe, "attempted", false); + add_assoc_bool(ffn_output_probe, "available", model->cuda_decoder_graph_executor_ffn_output_residual_execution_available); + add_assoc_bool(ffn_output_probe, "executed", false); + add_assoc_bool(ffn_output_probe, "retained_device_output", false); + add_assoc_string(final_norm_probe, "type", "gpu_decoder_final_norm_device_execution"); + add_assoc_bool(final_norm_probe, "attempted", false); + add_assoc_bool(final_norm_probe, "available", model->cuda_decoder_graph_executor_final_norm_execution_available); + add_assoc_bool(final_norm_probe, "executed", false); + add_assoc_bool(final_norm_probe, "retained_device_output", false); + add_assoc_string(logits_probe, "type", "gpu_decoder_logits_projection_device_execution"); + add_assoc_bool(logits_probe, "attempted", false); + add_assoc_bool(logits_probe, "available", model->cuda_decoder_graph_executor_logits_projection_execution_available); + add_assoc_bool(logits_probe, "executed", false); + add_assoc_bool(logits_probe, "retained_device_output", false); + + if (residual_output == NULL || *residual_output == 0 || residual_id == NULL || residual_width == 0) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + -1, + "cuda_decoder_graph_prefilled_attention_residual_input_invalid" + ); + return FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_try_prefilled_layer0_ffn_norm( + model, + graph, + residual_id, + residual_width, + prefill_batch_index, + ffn_norm_probe, + &ffn_norm_output, + &ffn_norm_width, + &ffn_norm_id, + executed_device_ops, + &used_prefill_ffn_norm + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, residual_output); + return FAILURE; + } + if (!used_prefill_ffn_norm + && king_inference_cuda_decoder_graph_executor_execute_ffn_norm( + model, + graph, + residual_id, + *residual_output, + residual_width, + ffn_norm_probe, + &ffn_norm_output, + &ffn_norm_width, + &ffn_norm_id, + executed_device_ops + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, residual_output); + return FAILURE; + } + *ffn_norm_executed = ffn_norm_output != 0 && ffn_norm_width > 0; + if (*ffn_norm_executed + && king_inference_cuda_decoder_graph_executor_try_prefilled_layer0_ffn_gate_up( + model, + graph, + ffn_norm_id, + ffn_norm_width, + prefill_batch_index, + ffn_gate_up_probe, + &ffn_gate_output, + &ffn_up_output, + &ffn_gate_rows, + &ffn_up_rows, + &ffn_gate_id, + &ffn_up_id, + executed_device_ops, + &used_prefill_ffn_gate_up + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_up_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_gate_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, residual_output); + return FAILURE; + } + if (*ffn_norm_executed + && !used_prefill_ffn_gate_up + && king_inference_cuda_decoder_graph_executor_execute_ffn_gate_up_projections( + model, + graph, + ffn_norm_id, + ffn_norm_output, + ffn_norm_width, + ffn_gate_up_probe, + &ffn_gate_output, + &ffn_up_output, + &ffn_gate_rows, + &ffn_up_rows, + &ffn_gate_id, + &ffn_up_id, + executed_device_ops + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_up_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_gate_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, residual_output); + return FAILURE; + } + *ffn_gate_up_executed = ffn_gate_output != 0 && ffn_up_output != 0 && ffn_gate_rows == ffn_up_rows; + if (*ffn_gate_up_executed + && king_inference_cuda_decoder_graph_executor_try_prefilled_layer0_ffn_swiglu( + model, + graph, + ffn_gate_id, + ffn_gate_output, + ffn_gate_rows, + ffn_up_id, + ffn_up_output, + ffn_up_rows, + prefill_batch_index, + ffn_swiglu_probe, + &ffn_swiglu_output, + &ffn_swiglu_width, + &ffn_gated_id, + executed_device_ops, + &used_prefill_ffn_swiglu + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_swiglu_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_up_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_gate_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, residual_output); + return FAILURE; + } + if (*ffn_gate_up_executed + && !used_prefill_ffn_swiglu + && king_inference_cuda_decoder_graph_executor_execute_ffn_swiglu( + model, + graph, + ffn_gate_id, + ffn_gate_output, + ffn_gate_rows, + ffn_up_id, + ffn_up_output, + ffn_up_rows, + ffn_swiglu_probe, + &ffn_swiglu_output, + &ffn_swiglu_width, + &ffn_gated_id, + executed_device_ops + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_swiglu_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_up_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_gate_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, residual_output); + return FAILURE; + } + *ffn_swiglu_executed = ffn_swiglu_output != 0 && ffn_swiglu_width > 0; + if (*ffn_swiglu_executed + && king_inference_cuda_decoder_graph_executor_try_prefilled_layer0_ffn_down_projection( + model, + graph, + ffn_gated_id, + ffn_swiglu_output, + ffn_swiglu_width, + prefill_batch_index, + ffn_down_probe, + &ffn_down_output, + &ffn_down_width, + &ffn_down_id, + executed_device_ops, + &used_prefill_ffn_down + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_down_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_swiglu_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_up_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_gate_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, residual_output); + return FAILURE; + } + if (*ffn_swiglu_executed + && !used_prefill_ffn_down + && king_inference_cuda_decoder_graph_executor_execute_ffn_down_projection( + model, + graph, + ffn_gated_id, + ffn_swiglu_output, + ffn_swiglu_width, + ffn_down_probe, + &ffn_down_output, + &ffn_down_width, + &ffn_down_id, + executed_device_ops + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_down_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_swiglu_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_up_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_gate_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, residual_output); + return FAILURE; + } + *ffn_down_executed = ffn_down_output != 0 && ffn_down_width > 0; + if (*ffn_down_executed + && king_inference_cuda_decoder_graph_executor_try_prefilled_layer0_ffn_output_residual( + model, + graph, + residual_id, + *residual_output, + residual_width, + ffn_down_id, + ffn_down_output, + ffn_down_width, + prefill_batch_index, + ffn_output_probe, + &ffn_output_residual, + &ffn_output_width, + &ffn_output_id, + executed_device_ops, + &used_prefill_ffn_output + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_output_residual); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_down_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_swiglu_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_up_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_gate_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, residual_output); + return FAILURE; + } + if (*ffn_down_executed + && !used_prefill_ffn_output + && king_inference_cuda_decoder_graph_executor_execute_ffn_output_residual_add( + model, + graph, + residual_id, + *residual_output, + residual_width, + ffn_down_id, + ffn_down_output, + ffn_down_width, + ffn_output_probe, + &ffn_output_residual, + &ffn_output_width, + &ffn_output_id, + executed_device_ops + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_output_residual); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_down_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_swiglu_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_up_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_gate_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, residual_output); + return FAILURE; + } + *ffn_output_executed = ffn_output_residual != 0 && ffn_output_width > 0; + if (*ffn_output_executed + && king_inference_cuda_decoder_graph_executor_execute_final_norm( + model, + graph, + ffn_output_id, + ffn_output_residual, + ffn_output_width, + final_norm_probe, + &final_norm_output, + &final_norm_width, + &final_norm_id, + executed_device_ops + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &final_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_output_residual); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_down_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_swiglu_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_up_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_gate_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, residual_output); + return FAILURE; + } + *final_norm_executed = final_norm_output != 0 && final_norm_width > 0; + if (!*final_norm_executed + && ffn_output_residual != 0 + && ffn_output_width > 0 + && ffn_output_id != NULL + && next_hidden_id_out != NULL + && next_hidden_output != NULL + && next_hidden_width != NULL) { + *next_hidden_id_out = ffn_output_id; + *next_hidden_output = ffn_output_residual; + *next_hidden_width = ffn_output_width; + ffn_output_residual = 0; + } + if (*final_norm_executed + && king_inference_cuda_decoder_graph_executor_execute_logits_projection( + model, + graph, + final_norm_id, + final_norm_output, + final_norm_width, + logits_probe, + &logits_output, + &logits_rows, + &logits_id, + executed_device_ops + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &logits_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &final_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_output_residual); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_down_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_swiglu_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_up_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_gate_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_norm_output); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, residual_output); + return FAILURE; + } + *logits_executed = logits_output != 0 && logits_rows > 0 && logits_id != NULL; + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &logits_output) != SUCCESS) { + release_result = FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &final_norm_output) != SUCCESS) { + release_result = FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_output_residual) != SUCCESS) { + release_result = FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_down_output) != SUCCESS) { + release_result = FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_swiglu_output) != SUCCESS) { + release_result = FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_up_output) != SUCCESS) { + release_result = FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_gate_output) != SUCCESS) { + release_result = FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_norm_output) != SUCCESS) { + release_result = FAILURE; + } + if (king_inference_cuda_decoder_graph_executor_release_device_ptr(model, residual_output) != SUCCESS) { + release_result = FAILURE; + } + if (release_result != SUCCESS) { + king_inference_cuda_decoder_graph_executor_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_graph_prefilled_attention_residual_tail_free_failed" + ); + return FAILURE; + } + add_assoc_bool(residual_probe, "retained_device_output", false); + add_assoc_bool(residual_probe, "residual_output_released", true); + add_assoc_bool(ffn_norm_probe, "retained_device_output", false); + add_assoc_bool(ffn_norm_probe, "ffn_norm_output_released", *ffn_norm_executed); + add_assoc_bool(ffn_gate_up_probe, "retained_device_output", false); + add_assoc_bool(ffn_gate_up_probe, "gate_up_output_released", *ffn_gate_up_executed); + add_assoc_bool(ffn_swiglu_probe, "retained_device_output", false); + add_assoc_bool(ffn_swiglu_probe, "swiglu_output_released", *ffn_swiglu_executed); + add_assoc_bool(ffn_down_probe, "retained_device_output", false); + add_assoc_bool(ffn_down_probe, "down_output_released", *ffn_down_executed); + add_assoc_bool(ffn_output_probe, "retained_device_output", false); + add_assoc_bool(ffn_output_probe, "output_residual_released", *ffn_output_executed); + add_assoc_bool(final_norm_probe, "retained_device_output", false); + add_assoc_bool(final_norm_probe, "final_norm_output_released", *final_norm_executed); + add_assoc_bool(logits_probe, "retained_device_output", false); + add_assoc_bool(logits_probe, "logits_output_released", *logits_executed); + add_assoc_long(ffn_norm_probe, "residual_width", (zend_long) residual_width); + add_assoc_long(ffn_norm_probe, "ffn_norm_width", (zend_long) ffn_norm_width); + add_assoc_long(ffn_gate_up_probe, "ffn_norm_width", (zend_long) ffn_norm_width); + add_assoc_long(ffn_swiglu_probe, "gate_rows", (zend_long) ffn_gate_rows); + add_assoc_long(ffn_swiglu_probe, "up_rows", (zend_long) ffn_up_rows); + add_assoc_long(ffn_swiglu_probe, "swiglu_width", (zend_long) ffn_swiglu_width); + add_assoc_long(ffn_down_probe, "swiglu_width", (zend_long) ffn_swiglu_width); + add_assoc_long(ffn_down_probe, "down_width", (zend_long) ffn_down_width); + add_assoc_long(ffn_output_probe, "down_width", (zend_long) ffn_down_width); + add_assoc_long(ffn_output_probe, "output_width", (zend_long) ffn_output_width); + add_assoc_long(final_norm_probe, "input_width", (zend_long) ffn_output_width); + add_assoc_long(final_norm_probe, "final_norm_width", (zend_long) final_norm_width); + add_assoc_long(logits_probe, "final_norm_width", (zend_long) final_norm_width); + add_assoc_long(logits_probe, "logits_rows", (zend_long) logits_rows); + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_graph_executor_try_execute_prefilled_layer0_attention_residual( + king_inference_model_object *model, + zval *graph, + zend_string *embedding_id, + zend_ulong width, + zend_ulong prefill_batch_index, + zval *slice_probe, + zval *rope_probe, + zval *kv_probe, + zval *attention_projection_probe, + zval *attention_residual_probe, + zval *ffn_norm_probe, + zval *ffn_gate_up_probe, + zval *ffn_swiglu_probe, + zval *ffn_down_probe, + zval *ffn_output_probe, + zval *final_norm_probe, + zval *logits_probe, + bool *attention_projection_probe_initialized, + bool *attention_residual_probe_initialized, + bool *ffn_norm_probe_initialized, + bool *ffn_gate_up_probe_initialized, + bool *ffn_swiglu_probe_initialized, + bool *ffn_down_probe_initialized, + bool *ffn_output_probe_initialized, + bool *final_norm_probe_initialized, + bool *logits_probe_initialized, + zend_ulong *executed_device_ops, + bool *used_prefill_attention_residual, + bool *attention_projection_executed, + bool *attention_residual_executed, + bool *ffn_norm_executed, + bool *ffn_gate_up_executed, + bool *ffn_swiglu_executed, + bool *ffn_down_executed, + bool *ffn_output_executed, + bool *final_norm_executed, + bool *logits_executed, + zend_string **next_hidden_id, + king_inference_cuda_device_ptr *next_hidden_output, + zend_ulong *next_hidden_width +) { + king_inference_cuda_device_ptr residual_output = 0; + zend_string *residual_id = NULL; + zend_ulong residual_width = 0; + + *used_prefill_attention_residual = false; + if (model->cuda_decoder_prompt_prefill_layer0_attention_residual == 0 + || model->cuda_decoder_prompt_prefill_layer0_attention_residual_width == 0 + || prefill_batch_index >= model->cuda_decoder_prompt_prefill_tokens) { + return SUCCESS; + } + + array_init(attention_projection_probe); + array_init(attention_residual_probe); + array_init(ffn_norm_probe); + array_init(ffn_gate_up_probe); + array_init(ffn_swiglu_probe); + array_init(ffn_down_probe); + array_init(ffn_output_probe); + array_init(final_norm_probe); + array_init(logits_probe); + *attention_projection_probe_initialized = true; + *attention_residual_probe_initialized = true; + *ffn_norm_probe_initialized = true; + *ffn_gate_up_probe_initialized = true; + *ffn_swiglu_probe_initialized = true; + *ffn_down_probe_initialized = true; + *ffn_output_probe_initialized = true; + *final_norm_probe_initialized = true; + *logits_probe_initialized = true; + + if (king_inference_cuda_decoder_graph_executor_try_prefilled_layer0_attention_residual( + model, + graph, + embedding_id, + width, + prefill_batch_index, + slice_probe, + rope_probe, + kv_probe, + attention_projection_probe, + attention_residual_probe, + &residual_output, + &residual_width, + &residual_id, + executed_device_ops, + used_prefill_attention_residual + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &residual_output); + return FAILURE; + } + if (!*used_prefill_attention_residual) { + king_inference_cuda_decoder_graph_executor_destroy_downstream_probes( + true, + attention_projection_probe, + true, + attention_residual_probe, + true, + ffn_norm_probe, + true, + ffn_gate_up_probe, + true, + ffn_swiglu_probe, + true, + ffn_down_probe, + true, + ffn_output_probe, + true, + final_norm_probe, + true, + logits_probe + ); + *attention_projection_probe_initialized = false; + *attention_residual_probe_initialized = false; + *ffn_norm_probe_initialized = false; + *ffn_gate_up_probe_initialized = false; + *ffn_swiglu_probe_initialized = false; + *ffn_down_probe_initialized = false; + *ffn_output_probe_initialized = false; + *final_norm_probe_initialized = false; + *logits_probe_initialized = false; + return SUCCESS; + } + + *attention_projection_executed = true; + *attention_residual_executed = true; + if (king_inference_cuda_decoder_graph_executor_execute_prefilled_attention_residual_ffn_norm_gate_up_swiglu_down_and_output( + model, + graph, + residual_id, + &residual_output, + residual_width, + prefill_batch_index, + attention_residual_probe, + ffn_norm_probe, + ffn_gate_up_probe, + ffn_swiglu_probe, + ffn_down_probe, + ffn_output_probe, + final_norm_probe, + logits_probe, + executed_device_ops, + ffn_norm_executed, + ffn_gate_up_executed, + ffn_swiglu_executed, + ffn_down_executed, + ffn_output_executed, + final_norm_executed, + logits_executed, + next_hidden_id, + next_hidden_output, + next_hidden_width + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &residual_output); + return FAILURE; + } + if (!*logits_executed + && *next_hidden_output != 0 + && king_inference_cuda_decoder_graph_executor_execute_remaining_blocks( + model, + graph, + next_hidden_id, + next_hidden_output, + next_hidden_width, + slice_probe, + rope_probe, + kv_probe, + attention_projection_probe, + attention_residual_probe, + ffn_norm_probe, + ffn_gate_up_probe, + ffn_swiglu_probe, + ffn_down_probe, + ffn_output_probe, + final_norm_probe, + logits_probe, + executed_device_ops, + attention_projection_executed, + attention_residual_executed, + ffn_norm_executed, + ffn_gate_up_executed, + ffn_swiglu_executed, + ffn_down_executed, + ffn_output_executed, + final_norm_executed, + logits_executed + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, next_hidden_output); + return FAILURE; + } + return SUCCESS; +} diff --git a/extension/src/inference/cuda/kernels/attention/cuda_attention_scores.inc b/extension/src/inference/cuda/kernels/attention/cuda_attention_scores.inc new file mode 100644 index 000000000..8020459ac --- /dev/null +++ b/extension/src/inference/cuda/kernels/attention/cuda_attention_scores.inc @@ -0,0 +1,678 @@ +/* + * CUDA attention score kernel for native King GPU inference. + */ + +typedef int king_inference_attention_scores_nvrtc_result; +typedef void *king_inference_attention_scores_nvrtc_program; + +typedef king_inference_attention_scores_nvrtc_result (*king_inference_attention_scores_nvrtc_create_program_fn)( + king_inference_attention_scores_nvrtc_program *program, + const char *source, + const char *name, + int header_count, + const char * const *headers, + const char * const *include_names +); +typedef king_inference_attention_scores_nvrtc_result (*king_inference_attention_scores_nvrtc_compile_program_fn)( + king_inference_attention_scores_nvrtc_program program, + int option_count, + const char * const *options +); +typedef king_inference_attention_scores_nvrtc_result (*king_inference_attention_scores_nvrtc_get_ptx_size_fn)( + king_inference_attention_scores_nvrtc_program program, + size_t *ptx_size +); +typedef king_inference_attention_scores_nvrtc_result (*king_inference_attention_scores_nvrtc_get_ptx_fn)( + king_inference_attention_scores_nvrtc_program program, + char *ptx +); +typedef king_inference_attention_scores_nvrtc_result (*king_inference_attention_scores_nvrtc_destroy_program_fn)( + king_inference_attention_scores_nvrtc_program *program +); + +static const char king_inference_cuda_attention_scores_source[] = +"extern \"C\" __global__ void king_attention_scores(\n" +" const float *query,\n" +" const float *keys,\n" +" float *scores,\n" +" unsigned int key_length,\n" +" unsigned int key_stride,\n" +" unsigned int slot_start,\n" +" unsigned int slot_count,\n" +" float scale\n" +") {\n" +" unsigned int slot = blockIdx.x;\n" +" unsigned int lane_id = threadIdx.x;\n" +" __shared__ float partials[256];\n" +" if (slot >= slot_count) {\n" +" return;\n" +" }\n" +" const float *key = keys + ((unsigned long long) (slot_start + slot) * key_stride);\n" +" float sum = 0.0f;\n" +" for (unsigned int i = lane_id; i < key_length; i += blockDim.x) {\n" +" sum += query[i] * key[i];\n" +" }\n" +" partials[lane_id] = sum;\n" +" __syncthreads();\n" +" for (unsigned int stride = blockDim.x >> 1; stride > 0u; stride >>= 1) {\n" +" if (lane_id < stride) {\n" +" partials[lane_id] += partials[lane_id + stride];\n" +" }\n" +" __syncthreads();\n" +" }\n" +" if (lane_id == 0u) {\n" +" scores[slot] = partials[0] * scale;\n" +" }\n" +"}\n" +"extern \"C\" __global__ void king_attention_scores_batch(\n" +" const float *queries,\n" +" const float *keys,\n" +" float *scores,\n" +" unsigned int query_stride,\n" +" unsigned int key_length,\n" +" unsigned int key_stride,\n" +" unsigned int position_start,\n" +" unsigned int max_slots,\n" +" unsigned int batch_count,\n" +" unsigned int sliding_window,\n" +" float scale\n" +") {\n" +" unsigned int query_index = blockIdx.x;\n" +" unsigned int slot = blockIdx.y;\n" +" unsigned int lane_id = threadIdx.x;\n" +" __shared__ float partials[256];\n" +" if (query_index >= batch_count || slot >= max_slots) {\n" +" return;\n" +" }\n" +" unsigned int absolute_position = position_start + query_index;\n" +" unsigned int slot_start = 0u;\n" +" if (sliding_window > 0u && absolute_position >= sliding_window) {\n" +" slot_start = absolute_position - sliding_window + 1u;\n" +" }\n" +" unsigned int slot_count = absolute_position - slot_start + 1u;\n" +" if (slot >= slot_count) {\n" +" if (lane_id == 0u) {\n" +" scores[((unsigned long long) query_index * max_slots) + slot] = -3.4028234663852886e38f;\n" +" }\n" +" return;\n" +" }\n" +" const float *query = queries + ((unsigned long long) query_index * query_stride);\n" +" const float *key = keys + ((unsigned long long) (slot_start + slot) * key_stride);\n" +" float sum = 0.0f;\n" +" for (unsigned int i = lane_id; i < key_length; i += blockDim.x) {\n" +" sum += query[i] * key[i];\n" +" }\n" +" partials[lane_id] = sum;\n" +" __syncthreads();\n" +" for (unsigned int stride = blockDim.x >> 1; stride > 0u; stride >>= 1) {\n" +" if (lane_id < stride) {\n" +" partials[lane_id] += partials[lane_id + stride];\n" +" }\n" +" __syncthreads();\n" +" }\n" +" if (lane_id == 0u) {\n" +" scores[((unsigned long long) query_index * max_slots) + slot] = partials[0] * scale;\n" +" }\n" +"}\n"; + +static void king_inference_cuda_attention_scores_reset_fields(king_inference_model_object *model) +{ + model->cuda_attention_scores_nvrtc_handle = NULL; + model->cuda_attention_scores_module = NULL; + model->cuda_attention_scores_function = NULL; + model->cuda_attention_scores_batch_function = NULL; + model->cuda_attention_scores_result = 0; + model->cuda_attention_scores_error[0] = '\0'; + model->cuda_attention_scores_launch_count = 0; + model->cuda_attention_scores_batch_launch_count = 0; + model->cuda_attention_scores_attempted = false; + model->cuda_attention_scores_available = false; + model->cuda_attention_scores_nvrtc_available = false; + model->cuda_attention_scores_module_loaded = false; + model->cuda_attention_scores_f32_available = false; + model->cuda_attention_scores_batch_available = false; +} + +static void king_inference_cuda_attention_scores_set_error( + king_inference_model_object *model, + int result, + const char *message +) { + if (message == NULL || message[0] == '\0') { + message = "unknown_cuda_attention_scores_error"; + } + + model->cuda_attention_scores_result = result; + snprintf( + model->cuda_attention_scores_error, + sizeof(model->cuda_attention_scores_error), + "%s", + message + ); +} + +static bool king_inference_cuda_attention_scores_driver_symbol( + king_inference_model_object *model, + const char *name, + void **symbol +) { + *symbol = NULL; + if (model->cuda_driver_handle == NULL) { + king_inference_cuda_attention_scores_set_error(model, -1, "cuda_driver_handle_unavailable"); + return false; + } + + *symbol = dlsym(model->cuda_driver_handle, name); + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_attention_scores_set_error(model, -1, name); + return false; +} + +static bool king_inference_cuda_attention_scores_nvrtc_symbol( + king_inference_model_object *model, + const char *name, + void **symbol +) { + *symbol = NULL; + if (model->cuda_attention_scores_nvrtc_handle == NULL) { + king_inference_cuda_attention_scores_set_error(model, -1, "nvrtc_handle_unavailable"); + return false; + } + + *symbol = dlsym(model->cuda_attention_scores_nvrtc_handle, name); + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_attention_scores_set_error(model, -1, name); + return false; +} + +static bool king_inference_cuda_attention_scores_open_nvrtc(king_inference_model_object *model) +{ + static const char *libraries[] = { + "libnvrtc.so.12", + "libnvrtc.so.11.2", + "libnvrtc.so.11.0", + "libnvrtc.so" + }; + size_t index; + + for (index = 0; index < sizeof(libraries) / sizeof(libraries[0]); index++) { + model->cuda_attention_scores_nvrtc_handle = dlopen(libraries[index], RTLD_LAZY | RTLD_LOCAL); + if (model->cuda_attention_scores_nvrtc_handle != NULL) { + model->cuda_attention_scores_nvrtc_available = true; + return true; + } + } + + king_inference_cuda_attention_scores_set_error(model, -1, "libnvrtc_unavailable"); + return false; +} + +static bool king_inference_cuda_attention_scores_compile_ptx( + king_inference_model_object *model, + char **ptx, + size_t *ptx_size +) { + king_inference_attention_scores_nvrtc_create_program_fn nvrtc_create_program; + king_inference_attention_scores_nvrtc_compile_program_fn nvrtc_compile_program; + king_inference_attention_scores_nvrtc_get_ptx_size_fn nvrtc_get_ptx_size; + king_inference_attention_scores_nvrtc_get_ptx_fn nvrtc_get_ptx; + king_inference_attention_scores_nvrtc_destroy_program_fn nvrtc_destroy_program; + king_inference_attention_scores_nvrtc_program program = NULL; + const char *options[] = {"--std=c++11", "--gpu-architecture=compute_89", "--use_fast_math"}; + king_inference_attention_scores_nvrtc_result result; + + *ptx = NULL; + *ptx_size = 0; + + if (!king_inference_cuda_attention_scores_nvrtc_symbol(model, "nvrtcCreateProgram", (void **) &nvrtc_create_program) + || !king_inference_cuda_attention_scores_nvrtc_symbol(model, "nvrtcCompileProgram", (void **) &nvrtc_compile_program) + || !king_inference_cuda_attention_scores_nvrtc_symbol(model, "nvrtcGetPTXSize", (void **) &nvrtc_get_ptx_size) + || !king_inference_cuda_attention_scores_nvrtc_symbol(model, "nvrtcGetPTX", (void **) &nvrtc_get_ptx) + || !king_inference_cuda_attention_scores_nvrtc_symbol(model, "nvrtcDestroyProgram", (void **) &nvrtc_destroy_program)) { + return false; + } + + result = nvrtc_create_program( + &program, + king_inference_cuda_attention_scores_source, + "king_attention_scores.cu", + 0, + NULL, + NULL + ); + if (result != 0 || program == NULL) { + king_inference_cuda_attention_scores_set_error( + model, + result, + result != 0 ? "nvrtcCreateProgram_failed" : "nvrtcProgram_unavailable" + ); + return false; + } + + result = nvrtc_compile_program(program, 3, options); + if (result != 0) { + (void) nvrtc_destroy_program(&program); + king_inference_cuda_attention_scores_set_error(model, result, "nvrtcCompileProgram_failed"); + return false; + } + + result = nvrtc_get_ptx_size(program, ptx_size); + if (result != 0 || *ptx_size == 0) { + (void) nvrtc_destroy_program(&program); + king_inference_cuda_attention_scores_set_error( + model, + result, + result != 0 ? "nvrtcGetPTXSize_failed" : "nvrtc_ptx_empty" + ); + return false; + } + + *ptx = emalloc(*ptx_size); + result = nvrtc_get_ptx(program, *ptx); + (void) nvrtc_destroy_program(&program); + if (result != 0) { + efree(*ptx); + *ptx = NULL; + *ptx_size = 0; + king_inference_cuda_attention_scores_set_error(model, result, "nvrtcGetPTX_failed"); + return false; + } + + return true; +} + +static void king_inference_cuda_attention_scores_release(king_inference_model_object *model) +{ + king_inference_cuda_module_unload_fn cu_module_unload; + + if (model->cuda_attention_scores_module != NULL + && model->cuda_driver_handle != NULL + && model->cuda_context != NULL + && king_inference_cuda_device_allocator_set_current(model) + && king_inference_cuda_attention_scores_driver_symbol( + model, + "cuModuleUnload", + (void **) &cu_module_unload + )) { + (void) cu_module_unload(model->cuda_attention_scores_module); + } + + if (model->cuda_attention_scores_nvrtc_handle != NULL) { + dlclose(model->cuda_attention_scores_nvrtc_handle); + } + + king_inference_cuda_attention_scores_reset_fields(model); +} + +static void king_inference_cuda_attention_scores_init( + king_inference_model_object *model, + king_inference_backend_kind backend_kind +) { + king_inference_cuda_module_load_data_fn cu_module_load_data; + king_inference_cuda_module_get_function_fn cu_module_get_function; + char *ptx = NULL; + size_t ptx_size = 0; + void *module = NULL; + void *function = NULL; + void *batch_function = NULL; + int result; + + king_inference_cuda_attention_scores_release(model); + if (backend_kind != KING_INFERENCE_BACKEND_KING_NATIVE_GPU + || !king_inference_gpu_enabled(&model->config)) { + return; + } + + model->cuda_attention_scores_attempted = true; + if (!model->cuda_device_allocator_available) { + king_inference_cuda_attention_scores_set_error(model, -1, "cuda_device_allocator_unavailable"); + return; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + king_inference_cuda_attention_scores_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_context_current_failed" + ); + return; + } + if (!king_inference_cuda_attention_scores_open_nvrtc(model) + || !king_inference_cuda_attention_scores_compile_ptx(model, &ptx, &ptx_size)) { + return; + } + if (!king_inference_cuda_attention_scores_driver_symbol( + model, + "cuModuleLoadData", + (void **) &cu_module_load_data + ) + || !king_inference_cuda_attention_scores_driver_symbol( + model, + "cuModuleGetFunction", + (void **) &cu_module_get_function + )) { + efree(ptx); + return; + } + + result = cu_module_load_data(&module, ptx); + efree(ptx); + if (result != 0 || module == NULL) { + king_inference_cuda_attention_scores_set_error( + model, + result, + result != 0 ? "cuModuleLoadData_failed" : "cuda_module_unavailable" + ); + return; + } + + result = cu_module_get_function(&function, module, "king_attention_scores"); + if (result != 0 || function == NULL) { + king_inference_cuda_module_unload_fn cu_module_unload = NULL; + + if (king_inference_cuda_attention_scores_driver_symbol( + model, + "cuModuleUnload", + (void **) &cu_module_unload + ) + && cu_module_unload != NULL) { + (void) cu_module_unload(module); + } + king_inference_cuda_attention_scores_set_error( + model, + result, + result != 0 ? "cuModuleGetFunction_failed" : "cuda_attention_scores_kernel_unavailable" + ); + return; + } + result = cu_module_get_function(&batch_function, module, "king_attention_scores_batch"); + if (result != 0 || batch_function == NULL) { + king_inference_cuda_attention_scores_set_error( + model, + result, + result != 0 ? "cuModuleGetFunction_batch_failed" : "cuda_attention_scores_batch_kernel_unavailable" + ); + batch_function = NULL; + } + + model->cuda_attention_scores_module = module; + model->cuda_attention_scores_function = function; + model->cuda_attention_scores_batch_function = batch_function; + model->cuda_attention_scores_module_loaded = true; + model->cuda_attention_scores_f32_available = true; + model->cuda_attention_scores_batch_available = batch_function != NULL; + model->cuda_attention_scores_available = true; + if (batch_function != NULL) { + model->cuda_attention_scores_result = 0; + model->cuda_attention_scores_error[0] = '\0'; + } +} + +static zend_result king_inference_cuda_attention_scores_f32( + king_inference_model_object *model, + king_inference_cuda_device_ptr query_vector, + king_inference_cuda_device_ptr key_cache, + king_inference_cuda_device_ptr output_scores, + zend_ulong key_length, + zend_ulong key_stride, + zend_ulong slot_start, + zend_ulong slot_count, + double scale +) { + king_inference_cuda_launch_kernel_fn cu_launch_kernel; + static king_inference_cuda_launch_kernel_fn cached_cu_launch_kernel = NULL; + unsigned int key_length32; + unsigned int key_stride32; + unsigned int slot_start32; + unsigned int slot_count32; + unsigned int block_x = 256; + float scale32 = (float) scale; + void *params[8]; + int result; + + if (!model->cuda_attention_scores_available || model->cuda_attention_scores_function == NULL) { + king_inference_cuda_attention_scores_set_error(model, -1, "cuda_attention_scores_unavailable"); + return FAILURE; + } + if (query_vector == 0 || key_cache == 0 || output_scores == 0) { + king_inference_cuda_attention_scores_set_error(model, -1, "cuda_attention_scores_argument_invalid"); + return FAILURE; + } + if (key_length == 0 + || key_length > UINT_MAX + || key_stride < key_length + || key_stride > UINT_MAX + || slot_count == 0 + || slot_count > UINT_MAX + || slot_start > UINT_MAX + || slot_start > UINT_MAX - slot_count + || !isfinite(scale)) { + king_inference_cuda_attention_scores_set_error(model, -1, "cuda_attention_scores_shape_invalid"); + return FAILURE; + } + if (cached_cu_launch_kernel == NULL + && !king_inference_cuda_attention_scores_driver_symbol( + model, + "cuLaunchKernel", + (void **) &cached_cu_launch_kernel + )) { + return FAILURE; + } + cu_launch_kernel = cached_cu_launch_kernel; + + key_length32 = (unsigned int) key_length; + key_stride32 = (unsigned int) key_stride; + slot_start32 = (unsigned int) slot_start; + slot_count32 = (unsigned int) slot_count; + params[0] = &query_vector; + params[1] = &key_cache; + params[2] = &output_scores; + params[3] = &key_length32; + params[4] = &key_stride32; + params[5] = &slot_start32; + params[6] = &slot_count32; + params[7] = &scale32; + + result = cu_launch_kernel( + model->cuda_attention_scores_function, + slot_count32, + 1, + 1, + block_x, + 1, + 1, + 0, + NULL, + params, + NULL + ); + if (result != 0) { + king_inference_cuda_attention_scores_set_error(model, result, "cuLaunchKernel_failed"); + return FAILURE; + } + + model->cuda_attention_scores_launch_count++; + model->cuda_attention_scores_result = 0; + model->cuda_attention_scores_error[0] = '\0'; + return SUCCESS; +} + +static zend_result king_inference_cuda_attention_scores_f32_batch( + king_inference_model_object *model, + king_inference_cuda_device_ptr query_vectors, + king_inference_cuda_device_ptr key_cache, + king_inference_cuda_device_ptr output_scores, + zend_ulong query_stride, + zend_ulong key_length, + zend_ulong key_stride, + zend_ulong position_start, + zend_ulong max_slots, + zend_ulong batch_count, + zend_ulong sliding_window, + double scale +) { + king_inference_cuda_launch_kernel_fn cu_launch_kernel; + static king_inference_cuda_launch_kernel_fn cached_cu_launch_kernel = NULL; + zend_ulong last_position; + zend_ulong required_slot_start = 0; + zend_ulong max_required_slots; + unsigned int query_stride32; + unsigned int key_length32; + unsigned int key_stride32; + unsigned int position_start32; + unsigned int max_slots32; + unsigned int batch_count32; + unsigned int sliding_window32; + unsigned int block_x = 256; + float scale32 = (float) scale; + void *params[11]; + int result; + + if (!model->cuda_attention_scores_batch_available + || model->cuda_attention_scores_batch_function == NULL) { + king_inference_cuda_attention_scores_set_error(model, -1, "cuda_attention_scores_batch_unavailable"); + return FAILURE; + } + if (query_vectors == 0 || key_cache == 0 || output_scores == 0) { + king_inference_cuda_attention_scores_set_error(model, -1, "cuda_attention_scores_batch_argument_invalid"); + return FAILURE; + } + if (query_stride == 0 + || query_stride > UINT_MAX + || key_length == 0 + || key_length > UINT_MAX + || key_stride < key_length + || key_stride > UINT_MAX + || position_start > UINT_MAX + || max_slots == 0 + || max_slots > UINT_MAX + || batch_count == 0 + || batch_count > UINT_MAX + || sliding_window > UINT_MAX + || position_start > UINT_MAX - (batch_count - 1) + || !isfinite(scale)) { + king_inference_cuda_attention_scores_set_error(model, -1, "cuda_attention_scores_batch_shape_invalid"); + return FAILURE; + } + + last_position = position_start + batch_count - 1; + if (sliding_window > 0 && last_position >= sliding_window) { + required_slot_start = last_position - sliding_window + 1; + } + max_required_slots = last_position - required_slot_start + 1; + if (max_slots < max_required_slots) { + king_inference_cuda_attention_scores_set_error(model, -1, "cuda_attention_scores_batch_slots_invalid"); + return FAILURE; + } + if (cached_cu_launch_kernel == NULL + && !king_inference_cuda_attention_scores_driver_symbol( + model, + "cuLaunchKernel", + (void **) &cached_cu_launch_kernel + )) { + return FAILURE; + } + cu_launch_kernel = cached_cu_launch_kernel; + + query_stride32 = (unsigned int) query_stride; + key_length32 = (unsigned int) key_length; + key_stride32 = (unsigned int) key_stride; + position_start32 = (unsigned int) position_start; + max_slots32 = (unsigned int) max_slots; + batch_count32 = (unsigned int) batch_count; + sliding_window32 = (unsigned int) sliding_window; + params[0] = &query_vectors; + params[1] = &key_cache; + params[2] = &output_scores; + params[3] = &query_stride32; + params[4] = &key_length32; + params[5] = &key_stride32; + params[6] = &position_start32; + params[7] = &max_slots32; + params[8] = &batch_count32; + params[9] = &sliding_window32; + params[10] = &scale32; + + result = cu_launch_kernel( + model->cuda_attention_scores_batch_function, + batch_count32, + max_slots32, + 1, + block_x, + 1, + 1, + 0, + NULL, + params, + NULL + ); + if (result != 0) { + king_inference_cuda_attention_scores_set_error(model, result, "cuLaunchKernel_batch_failed"); + return FAILURE; + } + + model->cuda_attention_scores_batch_launch_count++; + model->cuda_attention_scores_result = 0; + model->cuda_attention_scores_error[0] = '\0'; + return SUCCESS; +} + +static void king_inference_cuda_attention_scores_add_status( + king_inference_model_object *model, + zval *return_value +) { + zval attention_scores; + + array_init(&attention_scores); + add_assoc_bool(&attention_scores, "attempted", model->cuda_attention_scores_attempted); + add_assoc_bool(&attention_scores, "available", model->cuda_attention_scores_available); + add_assoc_bool(&attention_scores, "nvrtc_available", model->cuda_attention_scores_nvrtc_available); + add_assoc_bool(&attention_scores, "module_loaded", model->cuda_attention_scores_module_loaded); + add_assoc_bool(&attention_scores, "f32_available", model->cuda_attention_scores_f32_available); + add_assoc_bool(&attention_scores, "batch_available", model->cuda_attention_scores_batch_available); + add_assoc_long(&attention_scores, "launches", (zend_long) model->cuda_attention_scores_launch_count); + add_assoc_long(&attention_scores, "batch_launches", (zend_long) model->cuda_attention_scores_batch_launch_count); + add_assoc_long(&attention_scores, "result", model->cuda_attention_scores_result); + add_assoc_string(&attention_scores, "error", model->cuda_attention_scores_error); + add_assoc_zval(return_value, "attention_scores_kernel", &attention_scores); +} + +static void king_inference_cuda_attention_scores_apply_status( + king_inference_model_object *model, + zval *return_value +) { + zval *reason; + bool reason_can_be_scores; + + king_inference_cuda_attention_scores_add_status(model, return_value); + if (!model->cuda_attention_scores_attempted + || model->cuda_attention_scores_available + || !model->cuda_context_available) { + return; + } + + reason = zend_hash_str_find(Z_ARRVAL_P(return_value), "reason", sizeof("reason") - 1); + reason_can_be_scores = reason == NULL + || Z_TYPE_P(reason) != IS_STRING + || zend_string_equals_literal(Z_STR_P(reason), "gpu_ready") + || zend_string_equals_literal(Z_STR_P(reason), "gpu_decoder_kernel_not_implemented"); + if (reason_can_be_scores) { + king_inference_cuda_context_prepend_return_reason( + return_value, + "gpu_attention_scores_kernel_unavailable" + ); + add_assoc_string(return_value, "reason", "gpu_attention_scores_kernel_unavailable"); + } else { + king_inference_gpu_status_append_return_reason( + return_value, + "gpu_attention_scores_kernel_unavailable" + ); + } +} diff --git a/extension/src/inference/cuda/kernels/attention/cuda_attention_softmax.inc b/extension/src/inference/cuda/kernels/attention/cuda_attention_softmax.inc new file mode 100644 index 000000000..a6d0dd869 --- /dev/null +++ b/extension/src/inference/cuda/kernels/attention/cuda_attention_softmax.inc @@ -0,0 +1,713 @@ +/* + * CUDA attention softmax kernel for native King GPU inference. + */ + +typedef int king_inference_attention_softmax_nvrtc_result; +typedef void *king_inference_attention_softmax_nvrtc_program; + +typedef king_inference_attention_softmax_nvrtc_result (*king_inference_attention_softmax_nvrtc_create_program_fn)( + king_inference_attention_softmax_nvrtc_program *program, + const char *source, + const char *name, + int header_count, + const char * const *headers, + const char * const *include_names +); +typedef king_inference_attention_softmax_nvrtc_result (*king_inference_attention_softmax_nvrtc_compile_program_fn)( + king_inference_attention_softmax_nvrtc_program program, + int option_count, + const char * const *options +); +typedef king_inference_attention_softmax_nvrtc_result (*king_inference_attention_softmax_nvrtc_get_ptx_size_fn)( + king_inference_attention_softmax_nvrtc_program program, + size_t *ptx_size +); +typedef king_inference_attention_softmax_nvrtc_result (*king_inference_attention_softmax_nvrtc_get_ptx_fn)( + king_inference_attention_softmax_nvrtc_program program, + char *ptx +); +typedef king_inference_attention_softmax_nvrtc_result (*king_inference_attention_softmax_nvrtc_destroy_program_fn)( + king_inference_attention_softmax_nvrtc_program *program +); + +static const char king_inference_cuda_attention_softmax_source[] = +"extern \"C\" __global__ void king_attention_softmax(\n" +" const float *scores,\n" +" const float *mask,\n" +" float *probabilities,\n" +" float *sum_out,\n" +" unsigned int length,\n" +" float scale,\n" +" float temperature,\n" +" unsigned int mask_enabled\n" +") {\n" +" unsigned int lane_id = threadIdx.x;\n" +" __shared__ float partials[256];\n" +" float local_max = -3.4028234663852886e38f;\n" +" for (unsigned int i = lane_id; i < length; i += blockDim.x) {\n" +" if (mask_enabled != 0u && mask[i] <= 0.0f) {\n" +" continue;\n" +" }\n" +" float value = scores[i] * scale / temperature;\n" +" local_max = fmaxf(local_max, value);\n" +" }\n" +" partials[lane_id] = local_max;\n" +" __syncthreads();\n" +" for (unsigned int stride = blockDim.x >> 1; stride > 0u; stride >>= 1) {\n" +" if (lane_id < stride) {\n" +" partials[lane_id] = fmaxf(partials[lane_id], partials[lane_id + stride]);\n" +" }\n" +" __syncthreads();\n" +" }\n" +" float max_score = partials[0];\n" +" float local_sum = 0.0f;\n" +" for (unsigned int i = lane_id; i < length; i += blockDim.x) {\n" +" if (mask_enabled != 0u && mask[i] <= 0.0f) {\n" +" probabilities[i] = 0.0f;\n" +" continue;\n" +" }\n" +" if (max_score <= -3.4028234663852886e38f) {\n" +" probabilities[i] = 0.0f;\n" +" continue;\n" +" }\n" +" float value = expf((scores[i] * scale / temperature) - max_score);\n" +" probabilities[i] = value;\n" +" local_sum += value;\n" +" }\n" +" partials[lane_id] = local_sum;\n" +" __syncthreads();\n" +" for (unsigned int stride = blockDim.x >> 1; stride > 0u; stride >>= 1) {\n" +" if (lane_id < stride) {\n" +" partials[lane_id] += partials[lane_id + stride];\n" +" }\n" +" __syncthreads();\n" +" }\n" +" float sum = partials[0];\n" +" if (lane_id == 0u && sum_out != 0) {\n" +" sum_out[0] = sum;\n" +" }\n" +" if (sum <= 0.0f) {\n" +" for (unsigned int i = lane_id; i < length; i += blockDim.x) {\n" +" probabilities[i] = 0.0f;\n" +" }\n" +" return;\n" +" }\n" +" for (unsigned int i = lane_id; i < length; i += blockDim.x) {\n" +" probabilities[i] /= sum;\n" +" }\n" +"}\n" +"extern \"C\" __global__ void king_attention_softmax_batch(\n" +" const float *scores,\n" +" float *probabilities,\n" +" float *sum_out,\n" +" unsigned int position_start,\n" +" unsigned int max_slots,\n" +" unsigned int batch_count,\n" +" unsigned int sliding_window,\n" +" float scale,\n" +" float temperature\n" +") {\n" +" unsigned int query_index = blockIdx.x;\n" +" unsigned int lane_id = threadIdx.x;\n" +" __shared__ float partials[256];\n" +" if (query_index >= batch_count) {\n" +" return;\n" +" }\n" +" unsigned int absolute_position = position_start + query_index;\n" +" unsigned int slot_start = 0u;\n" +" if (sliding_window > 0u && absolute_position >= sliding_window) {\n" +" slot_start = absolute_position - sliding_window + 1u;\n" +" }\n" +" unsigned int slot_count = absolute_position - slot_start + 1u;\n" +" const float *row_scores = scores + ((unsigned long long) query_index * max_slots);\n" +" float *row_probabilities = probabilities + ((unsigned long long) query_index * max_slots);\n" +" float local_max = -3.4028234663852886e38f;\n" +" for (unsigned int i = lane_id; i < slot_count; i += blockDim.x) {\n" +" float value = row_scores[i] * scale / temperature;\n" +" local_max = fmaxf(local_max, value);\n" +" }\n" +" partials[lane_id] = local_max;\n" +" __syncthreads();\n" +" for (unsigned int stride = blockDim.x >> 1; stride > 0u; stride >>= 1) {\n" +" if (lane_id < stride) {\n" +" partials[lane_id] = fmaxf(partials[lane_id], partials[lane_id + stride]);\n" +" }\n" +" __syncthreads();\n" +" }\n" +" float max_score = partials[0];\n" +" float local_sum = 0.0f;\n" +" for (unsigned int i = lane_id; i < slot_count; i += blockDim.x) {\n" +" if (max_score <= -3.4028234663852886e38f) {\n" +" row_probabilities[i] = 0.0f;\n" +" continue;\n" +" }\n" +" float value = expf((row_scores[i] * scale / temperature) - max_score);\n" +" row_probabilities[i] = value;\n" +" local_sum += value;\n" +" }\n" +" partials[lane_id] = local_sum;\n" +" __syncthreads();\n" +" for (unsigned int stride = blockDim.x >> 1; stride > 0u; stride >>= 1) {\n" +" if (lane_id < stride) {\n" +" partials[lane_id] += partials[lane_id + stride];\n" +" }\n" +" __syncthreads();\n" +" }\n" +" float sum = partials[0];\n" +" if (lane_id == 0u && sum_out != 0) {\n" +" sum_out[query_index] = sum;\n" +" }\n" +" if (sum <= 0.0f) {\n" +" for (unsigned int i = lane_id; i < max_slots; i += blockDim.x) {\n" +" row_probabilities[i] = 0.0f;\n" +" }\n" +" return;\n" +" }\n" +" for (unsigned int i = lane_id; i < slot_count; i += blockDim.x) {\n" +" row_probabilities[i] /= sum;\n" +" }\n" +" for (unsigned int i = slot_count + lane_id; i < max_slots; i += blockDim.x) {\n" +" row_probabilities[i] = 0.0f;\n" +" }\n" +"}\n"; + +static void king_inference_cuda_attention_softmax_reset_fields(king_inference_model_object *model) +{ + model->cuda_attention_softmax_nvrtc_handle = NULL; + model->cuda_attention_softmax_module = NULL; + model->cuda_attention_softmax_function = NULL; + model->cuda_attention_softmax_batch_function = NULL; + model->cuda_attention_softmax_result = 0; + model->cuda_attention_softmax_error[0] = '\0'; + model->cuda_attention_softmax_launch_count = 0; + model->cuda_attention_softmax_batch_launch_count = 0; + model->cuda_attention_softmax_attempted = false; + model->cuda_attention_softmax_available = false; + model->cuda_attention_softmax_nvrtc_available = false; + model->cuda_attention_softmax_module_loaded = false; + model->cuda_attention_softmax_f32_available = false; + model->cuda_attention_softmax_batch_available = false; +} + +static void king_inference_cuda_attention_softmax_set_error( + king_inference_model_object *model, + int result, + const char *message +) { + if (message == NULL || message[0] == '\0') { + message = "unknown_cuda_attention_softmax_error"; + } + + model->cuda_attention_softmax_result = result; + snprintf( + model->cuda_attention_softmax_error, + sizeof(model->cuda_attention_softmax_error), + "%s", + message + ); +} + +static bool king_inference_cuda_attention_softmax_driver_symbol( + king_inference_model_object *model, + const char *name, + void **symbol +) { + *symbol = NULL; + if (model->cuda_driver_handle == NULL) { + king_inference_cuda_attention_softmax_set_error(model, -1, "cuda_driver_handle_unavailable"); + return false; + } + + *symbol = dlsym(model->cuda_driver_handle, name); + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_attention_softmax_set_error(model, -1, name); + return false; +} + +static bool king_inference_cuda_attention_softmax_nvrtc_symbol( + king_inference_model_object *model, + const char *name, + void **symbol +) { + *symbol = NULL; + if (model->cuda_attention_softmax_nvrtc_handle == NULL) { + king_inference_cuda_attention_softmax_set_error(model, -1, "nvrtc_handle_unavailable"); + return false; + } + + *symbol = dlsym(model->cuda_attention_softmax_nvrtc_handle, name); + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_attention_softmax_set_error(model, -1, name); + return false; +} + +static bool king_inference_cuda_attention_softmax_open_nvrtc(king_inference_model_object *model) +{ + static const char *libraries[] = { + "libnvrtc.so.12", + "libnvrtc.so.11.2", + "libnvrtc.so.11.0", + "libnvrtc.so" + }; + size_t index; + + for (index = 0; index < sizeof(libraries) / sizeof(libraries[0]); index++) { + model->cuda_attention_softmax_nvrtc_handle = dlopen(libraries[index], RTLD_LAZY | RTLD_LOCAL); + if (model->cuda_attention_softmax_nvrtc_handle != NULL) { + model->cuda_attention_softmax_nvrtc_available = true; + return true; + } + } + + king_inference_cuda_attention_softmax_set_error(model, -1, "libnvrtc_unavailable"); + return false; +} + +static bool king_inference_cuda_attention_softmax_compile_ptx( + king_inference_model_object *model, + char **ptx, + size_t *ptx_size +) { + king_inference_attention_softmax_nvrtc_create_program_fn nvrtc_create_program; + king_inference_attention_softmax_nvrtc_compile_program_fn nvrtc_compile_program; + king_inference_attention_softmax_nvrtc_get_ptx_size_fn nvrtc_get_ptx_size; + king_inference_attention_softmax_nvrtc_get_ptx_fn nvrtc_get_ptx; + king_inference_attention_softmax_nvrtc_destroy_program_fn nvrtc_destroy_program; + king_inference_attention_softmax_nvrtc_program program = NULL; + const char *options[] = {"--std=c++11", "--gpu-architecture=compute_89", "--use_fast_math"}; + king_inference_attention_softmax_nvrtc_result result; + + *ptx = NULL; + *ptx_size = 0; + + if (!king_inference_cuda_attention_softmax_nvrtc_symbol(model, "nvrtcCreateProgram", (void **) &nvrtc_create_program) + || !king_inference_cuda_attention_softmax_nvrtc_symbol(model, "nvrtcCompileProgram", (void **) &nvrtc_compile_program) + || !king_inference_cuda_attention_softmax_nvrtc_symbol(model, "nvrtcGetPTXSize", (void **) &nvrtc_get_ptx_size) + || !king_inference_cuda_attention_softmax_nvrtc_symbol(model, "nvrtcGetPTX", (void **) &nvrtc_get_ptx) + || !king_inference_cuda_attention_softmax_nvrtc_symbol(model, "nvrtcDestroyProgram", (void **) &nvrtc_destroy_program)) { + return false; + } + + result = nvrtc_create_program( + &program, + king_inference_cuda_attention_softmax_source, + "king_attention_softmax.cu", + 0, + NULL, + NULL + ); + if (result != 0 || program == NULL) { + king_inference_cuda_attention_softmax_set_error( + model, + result, + result != 0 ? "nvrtcCreateProgram_failed" : "nvrtcProgram_unavailable" + ); + return false; + } + + result = nvrtc_compile_program(program, 3, options); + if (result != 0) { + (void) nvrtc_destroy_program(&program); + king_inference_cuda_attention_softmax_set_error(model, result, "nvrtcCompileProgram_failed"); + return false; + } + + result = nvrtc_get_ptx_size(program, ptx_size); + if (result != 0 || *ptx_size == 0) { + (void) nvrtc_destroy_program(&program); + king_inference_cuda_attention_softmax_set_error( + model, + result, + result != 0 ? "nvrtcGetPTXSize_failed" : "nvrtc_ptx_empty" + ); + return false; + } + + *ptx = emalloc(*ptx_size); + result = nvrtc_get_ptx(program, *ptx); + (void) nvrtc_destroy_program(&program); + if (result != 0) { + efree(*ptx); + *ptx = NULL; + *ptx_size = 0; + king_inference_cuda_attention_softmax_set_error(model, result, "nvrtcGetPTX_failed"); + return false; + } + + return true; +} + +static void king_inference_cuda_attention_softmax_release(king_inference_model_object *model) +{ + king_inference_cuda_module_unload_fn cu_module_unload; + + if (model->cuda_attention_softmax_module != NULL + && model->cuda_driver_handle != NULL + && model->cuda_context != NULL + && king_inference_cuda_device_allocator_set_current(model) + && king_inference_cuda_attention_softmax_driver_symbol( + model, + "cuModuleUnload", + (void **) &cu_module_unload + )) { + (void) cu_module_unload(model->cuda_attention_softmax_module); + } + + if (model->cuda_attention_softmax_nvrtc_handle != NULL) { + dlclose(model->cuda_attention_softmax_nvrtc_handle); + } + + king_inference_cuda_attention_softmax_reset_fields(model); +} + +static void king_inference_cuda_attention_softmax_init( + king_inference_model_object *model, + king_inference_backend_kind backend_kind +) { + king_inference_cuda_module_load_data_fn cu_module_load_data; + king_inference_cuda_module_get_function_fn cu_module_get_function; + char *ptx = NULL; + size_t ptx_size = 0; + void *module = NULL; + void *function = NULL; + void *batch_function = NULL; + int result; + + king_inference_cuda_attention_softmax_release(model); + if (backend_kind != KING_INFERENCE_BACKEND_KING_NATIVE_GPU + || !king_inference_gpu_enabled(&model->config)) { + return; + } + + model->cuda_attention_softmax_attempted = true; + if (!model->cuda_device_allocator_available) { + king_inference_cuda_attention_softmax_set_error(model, -1, "cuda_device_allocator_unavailable"); + return; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + king_inference_cuda_attention_softmax_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_context_current_failed" + ); + return; + } + if (!king_inference_cuda_attention_softmax_open_nvrtc(model) + || !king_inference_cuda_attention_softmax_compile_ptx(model, &ptx, &ptx_size)) { + return; + } + if (!king_inference_cuda_attention_softmax_driver_symbol( + model, + "cuModuleLoadData", + (void **) &cu_module_load_data + ) + || !king_inference_cuda_attention_softmax_driver_symbol( + model, + "cuModuleGetFunction", + (void **) &cu_module_get_function + )) { + efree(ptx); + return; + } + + result = cu_module_load_data(&module, ptx); + efree(ptx); + if (result != 0 || module == NULL) { + king_inference_cuda_attention_softmax_set_error( + model, + result, + result != 0 ? "cuModuleLoadData_failed" : "cuda_module_unavailable" + ); + return; + } + + result = cu_module_get_function(&function, module, "king_attention_softmax"); + if (result != 0 || function == NULL) { + king_inference_cuda_module_unload_fn cu_module_unload = NULL; + + if (king_inference_cuda_attention_softmax_driver_symbol( + model, + "cuModuleUnload", + (void **) &cu_module_unload + ) + && cu_module_unload != NULL) { + (void) cu_module_unload(module); + } + king_inference_cuda_attention_softmax_set_error( + model, + result, + result != 0 ? "cuModuleGetFunction_failed" : "cuda_attention_softmax_kernel_unavailable" + ); + return; + } + result = cu_module_get_function(&batch_function, module, "king_attention_softmax_batch"); + if (result != 0 || batch_function == NULL) { + king_inference_cuda_attention_softmax_set_error( + model, + result, + result != 0 ? "cuModuleGetFunction_batch_failed" : "cuda_attention_softmax_batch_kernel_unavailable" + ); + batch_function = NULL; + } + + model->cuda_attention_softmax_module = module; + model->cuda_attention_softmax_function = function; + model->cuda_attention_softmax_batch_function = batch_function; + model->cuda_attention_softmax_module_loaded = true; + model->cuda_attention_softmax_f32_available = true; + model->cuda_attention_softmax_batch_available = batch_function != NULL; + model->cuda_attention_softmax_available = true; + if (batch_function != NULL) { + model->cuda_attention_softmax_result = 0; + model->cuda_attention_softmax_error[0] = '\0'; + } +} + +static zend_result king_inference_cuda_attention_softmax_f32( + king_inference_model_object *model, + king_inference_cuda_device_ptr input_scores, + king_inference_cuda_device_ptr input_mask, + king_inference_cuda_device_ptr output_probabilities, + king_inference_cuda_device_ptr output_sum, + zend_ulong length, + double scale, + double temperature +) { + king_inference_cuda_launch_kernel_fn cu_launch_kernel; + static king_inference_cuda_launch_kernel_fn cached_cu_launch_kernel = NULL; + unsigned int length32; + unsigned int mask_enabled32; + unsigned int block_x = 256; + float scale32 = (float) scale; + float temperature32 = (float) temperature; + void *params[8]; + int result; + + if (!model->cuda_attention_softmax_available || model->cuda_attention_softmax_function == NULL) { + king_inference_cuda_attention_softmax_set_error(model, -1, "cuda_attention_softmax_unavailable"); + return FAILURE; + } + if (input_scores == 0 || output_probabilities == 0) { + king_inference_cuda_attention_softmax_set_error(model, -1, "cuda_attention_softmax_argument_invalid"); + return FAILURE; + } + if (length == 0 + || length > UINT_MAX + || !isfinite(scale) + || !isfinite(temperature) + || temperature <= 0.0) { + king_inference_cuda_attention_softmax_set_error(model, -1, "cuda_attention_softmax_shape_invalid"); + return FAILURE; + } + if (cached_cu_launch_kernel == NULL + && !king_inference_cuda_attention_softmax_driver_symbol( + model, + "cuLaunchKernel", + (void **) &cached_cu_launch_kernel + )) { + return FAILURE; + } + cu_launch_kernel = cached_cu_launch_kernel; + + length32 = (unsigned int) length; + mask_enabled32 = input_mask != 0 ? 1u : 0u; + params[0] = &input_scores; + params[1] = &input_mask; + params[2] = &output_probabilities; + params[3] = &output_sum; + params[4] = &length32; + params[5] = &scale32; + params[6] = &temperature32; + params[7] = &mask_enabled32; + + result = cu_launch_kernel( + model->cuda_attention_softmax_function, + 1, + 1, + 1, + block_x, + 1, + 1, + 0, + NULL, + params, + NULL + ); + if (result != 0) { + king_inference_cuda_attention_softmax_set_error(model, result, "cuLaunchKernel_failed"); + return FAILURE; + } + + model->cuda_attention_softmax_launch_count++; + model->cuda_attention_softmax_result = 0; + model->cuda_attention_softmax_error[0] = '\0'; + return SUCCESS; +} + +static zend_result king_inference_cuda_attention_softmax_f32_batch( + king_inference_model_object *model, + king_inference_cuda_device_ptr input_scores, + king_inference_cuda_device_ptr output_probabilities, + king_inference_cuda_device_ptr output_sums, + zend_ulong position_start, + zend_ulong max_slots, + zend_ulong batch_count, + zend_ulong sliding_window, + double scale, + double temperature +) { + king_inference_cuda_launch_kernel_fn cu_launch_kernel; + static king_inference_cuda_launch_kernel_fn cached_cu_launch_kernel = NULL; + zend_ulong last_position; + zend_ulong required_slot_start = 0; + zend_ulong max_required_slots; + unsigned int position_start32; + unsigned int max_slots32; + unsigned int batch_count32; + unsigned int sliding_window32; + unsigned int block_x = 256; + float scale32 = (float) scale; + float temperature32 = (float) temperature; + void *params[9]; + int result; + + if (!model->cuda_attention_softmax_batch_available + || model->cuda_attention_softmax_batch_function == NULL) { + king_inference_cuda_attention_softmax_set_error(model, -1, "cuda_attention_softmax_batch_unavailable"); + return FAILURE; + } + if (input_scores == 0 || output_probabilities == 0) { + king_inference_cuda_attention_softmax_set_error(model, -1, "cuda_attention_softmax_batch_argument_invalid"); + return FAILURE; + } + if (position_start > UINT_MAX + || max_slots == 0 + || max_slots > UINT_MAX + || batch_count == 0 + || batch_count > UINT_MAX + || sliding_window > UINT_MAX + || position_start > UINT_MAX - (batch_count - 1) + || !isfinite(scale) + || !isfinite(temperature) + || temperature <= 0.0) { + king_inference_cuda_attention_softmax_set_error(model, -1, "cuda_attention_softmax_batch_shape_invalid"); + return FAILURE; + } + + last_position = position_start + batch_count - 1; + if (sliding_window > 0 && last_position >= sliding_window) { + required_slot_start = last_position - sliding_window + 1; + } + max_required_slots = last_position - required_slot_start + 1; + if (max_slots < max_required_slots) { + king_inference_cuda_attention_softmax_set_error(model, -1, "cuda_attention_softmax_batch_slots_invalid"); + return FAILURE; + } + if (cached_cu_launch_kernel == NULL + && !king_inference_cuda_attention_softmax_driver_symbol( + model, + "cuLaunchKernel", + (void **) &cached_cu_launch_kernel + )) { + return FAILURE; + } + cu_launch_kernel = cached_cu_launch_kernel; + + position_start32 = (unsigned int) position_start; + max_slots32 = (unsigned int) max_slots; + batch_count32 = (unsigned int) batch_count; + sliding_window32 = (unsigned int) sliding_window; + params[0] = &input_scores; + params[1] = &output_probabilities; + params[2] = &output_sums; + params[3] = &position_start32; + params[4] = &max_slots32; + params[5] = &batch_count32; + params[6] = &sliding_window32; + params[7] = &scale32; + params[8] = &temperature32; + + result = cu_launch_kernel( + model->cuda_attention_softmax_batch_function, + batch_count32, + 1, + 1, + block_x, + 1, + 1, + 0, + NULL, + params, + NULL + ); + if (result != 0) { + king_inference_cuda_attention_softmax_set_error(model, result, "cuLaunchKernel_batch_failed"); + return FAILURE; + } + + model->cuda_attention_softmax_batch_launch_count++; + model->cuda_attention_softmax_result = 0; + model->cuda_attention_softmax_error[0] = '\0'; + return SUCCESS; +} + +static void king_inference_cuda_attention_softmax_add_status( + king_inference_model_object *model, + zval *return_value +) { + zval attention_softmax; + + array_init(&attention_softmax); + add_assoc_bool(&attention_softmax, "attempted", model->cuda_attention_softmax_attempted); + add_assoc_bool(&attention_softmax, "available", model->cuda_attention_softmax_available); + add_assoc_bool(&attention_softmax, "nvrtc_available", model->cuda_attention_softmax_nvrtc_available); + add_assoc_bool(&attention_softmax, "module_loaded", model->cuda_attention_softmax_module_loaded); + add_assoc_bool(&attention_softmax, "f32_available", model->cuda_attention_softmax_f32_available); + add_assoc_bool(&attention_softmax, "batch_available", model->cuda_attention_softmax_batch_available); + add_assoc_long(&attention_softmax, "launches", (zend_long) model->cuda_attention_softmax_launch_count); + add_assoc_long(&attention_softmax, "batch_launches", (zend_long) model->cuda_attention_softmax_batch_launch_count); + add_assoc_long(&attention_softmax, "result", model->cuda_attention_softmax_result); + add_assoc_string(&attention_softmax, "error", model->cuda_attention_softmax_error); + add_assoc_zval(return_value, "attention_softmax_kernel", &attention_softmax); +} + +static void king_inference_cuda_attention_softmax_apply_status( + king_inference_model_object *model, + zval *return_value +) { + zval *reason; + bool reason_can_be_softmax; + + king_inference_cuda_attention_softmax_add_status(model, return_value); + if (!model->cuda_attention_softmax_attempted + || model->cuda_attention_softmax_available + || !model->cuda_context_available) { + return; + } + + reason = zend_hash_str_find(Z_ARRVAL_P(return_value), "reason", sizeof("reason") - 1); + reason_can_be_softmax = reason == NULL + || Z_TYPE_P(reason) != IS_STRING + || zend_string_equals_literal(Z_STR_P(reason), "gpu_ready") + || zend_string_equals_literal(Z_STR_P(reason), "gpu_decoder_kernel_not_implemented"); + if (reason_can_be_softmax) { + king_inference_cuda_context_prepend_return_reason( + return_value, + "gpu_attention_softmax_kernel_unavailable" + ); + add_assoc_string(return_value, "reason", "gpu_attention_softmax_kernel_unavailable"); + } else { + king_inference_gpu_status_append_return_reason( + return_value, + "gpu_attention_softmax_kernel_unavailable" + ); + } +} diff --git a/extension/src/inference/cuda/kernels/attention/cuda_attention_values.inc b/extension/src/inference/cuda/kernels/attention/cuda_attention_values.inc new file mode 100644 index 000000000..ab9267f3b --- /dev/null +++ b/extension/src/inference/cuda/kernels/attention/cuda_attention_values.inc @@ -0,0 +1,655 @@ +/* + * CUDA attention value aggregation kernel for native King GPU inference. + */ + +typedef int king_inference_attention_values_nvrtc_result; +typedef void *king_inference_attention_values_nvrtc_program; + +typedef king_inference_attention_values_nvrtc_result (*king_inference_attention_values_nvrtc_create_program_fn)( + king_inference_attention_values_nvrtc_program *program, + const char *source, + const char *name, + int header_count, + const char * const *headers, + const char * const *include_names +); +typedef king_inference_attention_values_nvrtc_result (*king_inference_attention_values_nvrtc_compile_program_fn)( + king_inference_attention_values_nvrtc_program program, + int option_count, + const char * const *options +); +typedef king_inference_attention_values_nvrtc_result (*king_inference_attention_values_nvrtc_get_ptx_size_fn)( + king_inference_attention_values_nvrtc_program program, + size_t *ptx_size +); +typedef king_inference_attention_values_nvrtc_result (*king_inference_attention_values_nvrtc_get_ptx_fn)( + king_inference_attention_values_nvrtc_program program, + char *ptx +); +typedef king_inference_attention_values_nvrtc_result (*king_inference_attention_values_nvrtc_destroy_program_fn)( + king_inference_attention_values_nvrtc_program *program +); + +static const char king_inference_cuda_attention_values_source[] = +"extern \"C\" __global__ void king_attention_values(\n" +" const float *probabilities,\n" +" const float *values,\n" +" float *context,\n" +" unsigned int value_length,\n" +" unsigned int value_stride,\n" +" unsigned int slot_start,\n" +" unsigned int slot_count\n" +") {\n" +" unsigned int dim = blockIdx.x;\n" +" unsigned int lane_id = threadIdx.x;\n" +" __shared__ float partials[256];\n" +" if (dim >= value_length) {\n" +" return;\n" +" }\n" +" float sum = 0.0f;\n" +" for (unsigned int slot = lane_id; slot < slot_count; slot += blockDim.x) {\n" +" const float *value = values + ((unsigned long long) (slot_start + slot) * value_stride);\n" +" sum += probabilities[slot] * value[dim];\n" +" }\n" +" partials[lane_id] = sum;\n" +" __syncthreads();\n" +" for (unsigned int stride = blockDim.x >> 1; stride > 0u; stride >>= 1) {\n" +" if (lane_id < stride) {\n" +" partials[lane_id] += partials[lane_id + stride];\n" +" }\n" +" __syncthreads();\n" +" }\n" +" if (lane_id == 0u) {\n" +" context[dim] = partials[0];\n" +" }\n" +"}\n" +"extern \"C\" __global__ void king_attention_values_batch(\n" +" const float *probabilities,\n" +" const float *values,\n" +" float *contexts,\n" +" unsigned int value_length,\n" +" unsigned int value_stride,\n" +" unsigned int position_start,\n" +" unsigned int max_slots,\n" +" unsigned int batch_count,\n" +" unsigned int sliding_window\n" +") {\n" +" unsigned int query_index = blockIdx.x;\n" +" unsigned int dim = blockIdx.y;\n" +" unsigned int lane_id = threadIdx.x;\n" +" __shared__ float partials[256];\n" +" if (query_index >= batch_count || dim >= value_length) {\n" +" return;\n" +" }\n" +" unsigned int absolute_position = position_start + query_index;\n" +" unsigned int slot_start = 0u;\n" +" if (sliding_window > 0u && absolute_position >= sliding_window) {\n" +" slot_start = absolute_position - sliding_window + 1u;\n" +" }\n" +" unsigned int slot_count = absolute_position - slot_start + 1u;\n" +" const float *row_probabilities = probabilities + ((unsigned long long) query_index * max_slots);\n" +" float sum = 0.0f;\n" +" for (unsigned int slot = lane_id; slot < slot_count; slot += blockDim.x) {\n" +" const float *value = values + ((unsigned long long) (slot_start + slot) * value_stride);\n" +" sum += row_probabilities[slot] * value[dim];\n" +" }\n" +" partials[lane_id] = sum;\n" +" __syncthreads();\n" +" for (unsigned int stride = blockDim.x >> 1; stride > 0u; stride >>= 1) {\n" +" if (lane_id < stride) {\n" +" partials[lane_id] += partials[lane_id + stride];\n" +" }\n" +" __syncthreads();\n" +" }\n" +" if (lane_id == 0u) {\n" +" contexts[((unsigned long long) query_index * value_length) + dim] = partials[0];\n" +" }\n" +"}\n"; + +static void king_inference_cuda_attention_values_reset_fields(king_inference_model_object *model) +{ + model->cuda_attention_values_nvrtc_handle = NULL; + model->cuda_attention_values_module = NULL; + model->cuda_attention_values_function = NULL; + model->cuda_attention_values_batch_function = NULL; + model->cuda_attention_values_result = 0; + model->cuda_attention_values_error[0] = '\0'; + model->cuda_attention_values_launch_count = 0; + model->cuda_attention_values_batch_launch_count = 0; + model->cuda_attention_values_attempted = false; + model->cuda_attention_values_available = false; + model->cuda_attention_values_nvrtc_available = false; + model->cuda_attention_values_module_loaded = false; + model->cuda_attention_values_f32_available = false; + model->cuda_attention_values_batch_available = false; +} + +static void king_inference_cuda_attention_values_set_error( + king_inference_model_object *model, + int result, + const char *message +) { + if (message == NULL || message[0] == '\0') { + message = "unknown_cuda_attention_values_error"; + } + + model->cuda_attention_values_result = result; + snprintf( + model->cuda_attention_values_error, + sizeof(model->cuda_attention_values_error), + "%s", + message + ); +} + +static bool king_inference_cuda_attention_values_driver_symbol( + king_inference_model_object *model, + const char *name, + void **symbol +) { + *symbol = NULL; + if (model->cuda_driver_handle == NULL) { + king_inference_cuda_attention_values_set_error(model, -1, "cuda_driver_handle_unavailable"); + return false; + } + + *symbol = dlsym(model->cuda_driver_handle, name); + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_attention_values_set_error(model, -1, name); + return false; +} + +static bool king_inference_cuda_attention_values_nvrtc_symbol( + king_inference_model_object *model, + const char *name, + void **symbol +) { + *symbol = NULL; + if (model->cuda_attention_values_nvrtc_handle == NULL) { + king_inference_cuda_attention_values_set_error(model, -1, "nvrtc_handle_unavailable"); + return false; + } + + *symbol = dlsym(model->cuda_attention_values_nvrtc_handle, name); + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_attention_values_set_error(model, -1, name); + return false; +} + +static bool king_inference_cuda_attention_values_open_nvrtc(king_inference_model_object *model) +{ + static const char *libraries[] = { + "libnvrtc.so.12", + "libnvrtc.so.11.2", + "libnvrtc.so.11.0", + "libnvrtc.so" + }; + size_t index; + + for (index = 0; index < sizeof(libraries) / sizeof(libraries[0]); index++) { + model->cuda_attention_values_nvrtc_handle = dlopen(libraries[index], RTLD_LAZY | RTLD_LOCAL); + if (model->cuda_attention_values_nvrtc_handle != NULL) { + model->cuda_attention_values_nvrtc_available = true; + return true; + } + } + + king_inference_cuda_attention_values_set_error(model, -1, "libnvrtc_unavailable"); + return false; +} + +static bool king_inference_cuda_attention_values_compile_ptx( + king_inference_model_object *model, + char **ptx, + size_t *ptx_size +) { + king_inference_attention_values_nvrtc_create_program_fn nvrtc_create_program; + king_inference_attention_values_nvrtc_compile_program_fn nvrtc_compile_program; + king_inference_attention_values_nvrtc_get_ptx_size_fn nvrtc_get_ptx_size; + king_inference_attention_values_nvrtc_get_ptx_fn nvrtc_get_ptx; + king_inference_attention_values_nvrtc_destroy_program_fn nvrtc_destroy_program; + king_inference_attention_values_nvrtc_program program = NULL; + const char *options[] = {"--std=c++11", "--gpu-architecture=compute_89", "--use_fast_math"}; + king_inference_attention_values_nvrtc_result result; + + *ptx = NULL; + *ptx_size = 0; + + if (!king_inference_cuda_attention_values_nvrtc_symbol(model, "nvrtcCreateProgram", (void **) &nvrtc_create_program) + || !king_inference_cuda_attention_values_nvrtc_symbol(model, "nvrtcCompileProgram", (void **) &nvrtc_compile_program) + || !king_inference_cuda_attention_values_nvrtc_symbol(model, "nvrtcGetPTXSize", (void **) &nvrtc_get_ptx_size) + || !king_inference_cuda_attention_values_nvrtc_symbol(model, "nvrtcGetPTX", (void **) &nvrtc_get_ptx) + || !king_inference_cuda_attention_values_nvrtc_symbol(model, "nvrtcDestroyProgram", (void **) &nvrtc_destroy_program)) { + return false; + } + + result = nvrtc_create_program( + &program, + king_inference_cuda_attention_values_source, + "king_attention_values.cu", + 0, + NULL, + NULL + ); + if (result != 0 || program == NULL) { + king_inference_cuda_attention_values_set_error( + model, + result, + result != 0 ? "nvrtcCreateProgram_failed" : "nvrtcProgram_unavailable" + ); + return false; + } + + result = nvrtc_compile_program(program, 3, options); + if (result != 0) { + (void) nvrtc_destroy_program(&program); + king_inference_cuda_attention_values_set_error(model, result, "nvrtcCompileProgram_failed"); + return false; + } + + result = nvrtc_get_ptx_size(program, ptx_size); + if (result != 0 || *ptx_size == 0) { + (void) nvrtc_destroy_program(&program); + king_inference_cuda_attention_values_set_error( + model, + result, + result != 0 ? "nvrtcGetPTXSize_failed" : "nvrtc_ptx_empty" + ); + return false; + } + + *ptx = emalloc(*ptx_size); + result = nvrtc_get_ptx(program, *ptx); + (void) nvrtc_destroy_program(&program); + if (result != 0) { + efree(*ptx); + *ptx = NULL; + *ptx_size = 0; + king_inference_cuda_attention_values_set_error(model, result, "nvrtcGetPTX_failed"); + return false; + } + + return true; +} + +static void king_inference_cuda_attention_values_release(king_inference_model_object *model) +{ + king_inference_cuda_module_unload_fn cu_module_unload; + + if (model->cuda_attention_values_module != NULL + && model->cuda_driver_handle != NULL + && model->cuda_context != NULL + && king_inference_cuda_device_allocator_set_current(model) + && king_inference_cuda_attention_values_driver_symbol( + model, + "cuModuleUnload", + (void **) &cu_module_unload + )) { + (void) cu_module_unload(model->cuda_attention_values_module); + } + + if (model->cuda_attention_values_nvrtc_handle != NULL) { + dlclose(model->cuda_attention_values_nvrtc_handle); + } + + king_inference_cuda_attention_values_reset_fields(model); +} + +static void king_inference_cuda_attention_values_init( + king_inference_model_object *model, + king_inference_backend_kind backend_kind +) { + king_inference_cuda_module_load_data_fn cu_module_load_data; + king_inference_cuda_module_get_function_fn cu_module_get_function; + char *ptx = NULL; + size_t ptx_size = 0; + void *module = NULL; + void *function = NULL; + void *batch_function = NULL; + int result; + + king_inference_cuda_attention_values_release(model); + if (backend_kind != KING_INFERENCE_BACKEND_KING_NATIVE_GPU + || !king_inference_gpu_enabled(&model->config)) { + return; + } + + model->cuda_attention_values_attempted = true; + if (!model->cuda_device_allocator_available) { + king_inference_cuda_attention_values_set_error(model, -1, "cuda_device_allocator_unavailable"); + return; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + king_inference_cuda_attention_values_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_context_current_failed" + ); + return; + } + if (!king_inference_cuda_attention_values_open_nvrtc(model) + || !king_inference_cuda_attention_values_compile_ptx(model, &ptx, &ptx_size)) { + return; + } + if (!king_inference_cuda_attention_values_driver_symbol( + model, + "cuModuleLoadData", + (void **) &cu_module_load_data + ) + || !king_inference_cuda_attention_values_driver_symbol( + model, + "cuModuleGetFunction", + (void **) &cu_module_get_function + )) { + efree(ptx); + return; + } + + result = cu_module_load_data(&module, ptx); + efree(ptx); + if (result != 0 || module == NULL) { + king_inference_cuda_attention_values_set_error( + model, + result, + result != 0 ? "cuModuleLoadData_failed" : "cuda_module_unavailable" + ); + return; + } + + result = cu_module_get_function(&function, module, "king_attention_values"); + if (result != 0 || function == NULL) { + king_inference_cuda_module_unload_fn cu_module_unload = NULL; + + if (king_inference_cuda_attention_values_driver_symbol( + model, + "cuModuleUnload", + (void **) &cu_module_unload + ) + && cu_module_unload != NULL) { + (void) cu_module_unload(module); + } + king_inference_cuda_attention_values_set_error( + model, + result, + result != 0 ? "cuModuleGetFunction_failed" : "cuda_attention_values_kernel_unavailable" + ); + return; + } + result = cu_module_get_function(&batch_function, module, "king_attention_values_batch"); + if (result != 0 || batch_function == NULL) { + king_inference_cuda_attention_values_set_error( + model, + result, + result != 0 ? "cuModuleGetFunction_batch_failed" : "cuda_attention_values_batch_kernel_unavailable" + ); + batch_function = NULL; + } + + model->cuda_attention_values_module = module; + model->cuda_attention_values_function = function; + model->cuda_attention_values_batch_function = batch_function; + model->cuda_attention_values_module_loaded = true; + model->cuda_attention_values_f32_available = true; + model->cuda_attention_values_batch_available = batch_function != NULL; + model->cuda_attention_values_available = true; + if (batch_function != NULL) { + model->cuda_attention_values_result = 0; + model->cuda_attention_values_error[0] = '\0'; + } +} + +static zend_result king_inference_cuda_attention_values_f32( + king_inference_model_object *model, + king_inference_cuda_device_ptr input_probabilities, + king_inference_cuda_device_ptr value_cache, + king_inference_cuda_device_ptr output_context, + zend_ulong value_length, + zend_ulong value_stride, + zend_ulong slot_start, + zend_ulong slot_count +) { + king_inference_cuda_launch_kernel_fn cu_launch_kernel; + static king_inference_cuda_launch_kernel_fn cached_cu_launch_kernel = NULL; + unsigned int value_length32; + unsigned int value_stride32; + unsigned int slot_start32; + unsigned int slot_count32; + unsigned int block_x = 256; + void *params[7]; + int result; + + if (!model->cuda_attention_values_available || model->cuda_attention_values_function == NULL) { + king_inference_cuda_attention_values_set_error(model, -1, "cuda_attention_values_unavailable"); + return FAILURE; + } + if (input_probabilities == 0 || value_cache == 0 || output_context == 0) { + king_inference_cuda_attention_values_set_error(model, -1, "cuda_attention_values_argument_invalid"); + return FAILURE; + } + if (value_length == 0 + || value_length > UINT_MAX + || value_stride < value_length + || value_stride > UINT_MAX + || slot_count == 0 + || slot_count > UINT_MAX + || slot_start > UINT_MAX + || slot_start > UINT_MAX - slot_count) { + king_inference_cuda_attention_values_set_error(model, -1, "cuda_attention_values_shape_invalid"); + return FAILURE; + } + if (cached_cu_launch_kernel == NULL + && !king_inference_cuda_attention_values_driver_symbol( + model, + "cuLaunchKernel", + (void **) &cached_cu_launch_kernel + )) { + return FAILURE; + } + cu_launch_kernel = cached_cu_launch_kernel; + + value_length32 = (unsigned int) value_length; + value_stride32 = (unsigned int) value_stride; + slot_start32 = (unsigned int) slot_start; + slot_count32 = (unsigned int) slot_count; + params[0] = &input_probabilities; + params[1] = &value_cache; + params[2] = &output_context; + params[3] = &value_length32; + params[4] = &value_stride32; + params[5] = &slot_start32; + params[6] = &slot_count32; + + result = cu_launch_kernel( + model->cuda_attention_values_function, + value_length32, + 1, + 1, + block_x, + 1, + 1, + 0, + NULL, + params, + NULL + ); + if (result != 0) { + king_inference_cuda_attention_values_set_error(model, result, "cuLaunchKernel_failed"); + return FAILURE; + } + + model->cuda_attention_values_launch_count++; + model->cuda_attention_values_result = 0; + model->cuda_attention_values_error[0] = '\0'; + return SUCCESS; +} + +static zend_result king_inference_cuda_attention_values_f32_batch( + king_inference_model_object *model, + king_inference_cuda_device_ptr input_probabilities, + king_inference_cuda_device_ptr value_cache, + king_inference_cuda_device_ptr output_contexts, + zend_ulong value_length, + zend_ulong value_stride, + zend_ulong position_start, + zend_ulong max_slots, + zend_ulong batch_count, + zend_ulong sliding_window +) { + king_inference_cuda_launch_kernel_fn cu_launch_kernel; + static king_inference_cuda_launch_kernel_fn cached_cu_launch_kernel = NULL; + zend_ulong last_position; + zend_ulong required_slot_start = 0; + zend_ulong max_required_slots; + unsigned int value_length32; + unsigned int value_stride32; + unsigned int position_start32; + unsigned int max_slots32; + unsigned int batch_count32; + unsigned int sliding_window32; + unsigned int block_x = 256; + void *params[9]; + int result; + + if (!model->cuda_attention_values_batch_available + || model->cuda_attention_values_batch_function == NULL) { + king_inference_cuda_attention_values_set_error(model, -1, "cuda_attention_values_batch_unavailable"); + return FAILURE; + } + if (input_probabilities == 0 || value_cache == 0 || output_contexts == 0) { + king_inference_cuda_attention_values_set_error(model, -1, "cuda_attention_values_batch_argument_invalid"); + return FAILURE; + } + if (value_length == 0 + || value_length > UINT_MAX + || value_stride < value_length + || value_stride > UINT_MAX + || position_start > UINT_MAX + || max_slots == 0 + || max_slots > UINT_MAX + || batch_count == 0 + || batch_count > UINT_MAX + || sliding_window > UINT_MAX + || position_start > UINT_MAX - (batch_count - 1)) { + king_inference_cuda_attention_values_set_error(model, -1, "cuda_attention_values_batch_shape_invalid"); + return FAILURE; + } + + last_position = position_start + batch_count - 1; + if (sliding_window > 0 && last_position >= sliding_window) { + required_slot_start = last_position - sliding_window + 1; + } + max_required_slots = last_position - required_slot_start + 1; + if (max_slots < max_required_slots) { + king_inference_cuda_attention_values_set_error(model, -1, "cuda_attention_values_batch_slots_invalid"); + return FAILURE; + } + if (cached_cu_launch_kernel == NULL + && !king_inference_cuda_attention_values_driver_symbol( + model, + "cuLaunchKernel", + (void **) &cached_cu_launch_kernel + )) { + return FAILURE; + } + cu_launch_kernel = cached_cu_launch_kernel; + + value_length32 = (unsigned int) value_length; + value_stride32 = (unsigned int) value_stride; + position_start32 = (unsigned int) position_start; + max_slots32 = (unsigned int) max_slots; + batch_count32 = (unsigned int) batch_count; + sliding_window32 = (unsigned int) sliding_window; + params[0] = &input_probabilities; + params[1] = &value_cache; + params[2] = &output_contexts; + params[3] = &value_length32; + params[4] = &value_stride32; + params[5] = &position_start32; + params[6] = &max_slots32; + params[7] = &batch_count32; + params[8] = &sliding_window32; + + result = cu_launch_kernel( + model->cuda_attention_values_batch_function, + batch_count32, + value_length32, + 1, + block_x, + 1, + 1, + 0, + NULL, + params, + NULL + ); + if (result != 0) { + king_inference_cuda_attention_values_set_error(model, result, "cuLaunchKernel_batch_failed"); + return FAILURE; + } + + model->cuda_attention_values_batch_launch_count++; + model->cuda_attention_values_result = 0; + model->cuda_attention_values_error[0] = '\0'; + return SUCCESS; +} + +static void king_inference_cuda_attention_values_add_status( + king_inference_model_object *model, + zval *return_value +) { + zval attention_values; + + array_init(&attention_values); + add_assoc_bool(&attention_values, "attempted", model->cuda_attention_values_attempted); + add_assoc_bool(&attention_values, "available", model->cuda_attention_values_available); + add_assoc_bool(&attention_values, "nvrtc_available", model->cuda_attention_values_nvrtc_available); + add_assoc_bool(&attention_values, "module_loaded", model->cuda_attention_values_module_loaded); + add_assoc_bool(&attention_values, "f32_available", model->cuda_attention_values_f32_available); + add_assoc_bool(&attention_values, "batch_available", model->cuda_attention_values_batch_available); + add_assoc_long(&attention_values, "launches", (zend_long) model->cuda_attention_values_launch_count); + add_assoc_long(&attention_values, "batch_launches", (zend_long) model->cuda_attention_values_batch_launch_count); + add_assoc_long(&attention_values, "result", model->cuda_attention_values_result); + add_assoc_string(&attention_values, "error", model->cuda_attention_values_error); + add_assoc_zval(return_value, "attention_value_aggregation_kernel", &attention_values); +} + +static void king_inference_cuda_attention_values_apply_status( + king_inference_model_object *model, + zval *return_value +) { + zval *reason; + bool reason_can_be_values; + + king_inference_cuda_attention_values_add_status(model, return_value); + if (!model->cuda_attention_values_attempted + || model->cuda_attention_values_available + || !model->cuda_context_available) { + return; + } + + reason = zend_hash_str_find(Z_ARRVAL_P(return_value), "reason", sizeof("reason") - 1); + reason_can_be_values = reason == NULL + || Z_TYPE_P(reason) != IS_STRING + || zend_string_equals_literal(Z_STR_P(reason), "gpu_ready") + || zend_string_equals_literal(Z_STR_P(reason), "gpu_decoder_kernel_not_implemented"); + if (reason_can_be_values) { + king_inference_cuda_context_prepend_return_reason( + return_value, + "gpu_attention_value_aggregation_kernel_unavailable" + ); + add_assoc_string(return_value, "reason", "gpu_attention_value_aggregation_kernel_unavailable"); + } else { + king_inference_gpu_status_append_return_reason( + return_value, + "gpu_attention_value_aggregation_kernel_unavailable" + ); + } +} diff --git a/extension/src/inference/cuda/kernels/device/cuda_device_kv_cache.inc b/extension/src/inference/cuda/kernels/device/cuda_device_kv_cache.inc new file mode 100644 index 000000000..aa959f979 --- /dev/null +++ b/extension/src/inference/cuda/kernels/device/cuda_device_kv_cache.inc @@ -0,0 +1,711 @@ +/* + * CUDA device KV-cache storage for native King GPU inference. + */ + +typedef int (*king_inference_cuda_device_kv_cache_memcpy_dtod_fn)( + king_inference_cuda_device_ptr dst, + king_inference_cuda_device_ptr src, + size_t bytes +); +typedef int (*king_inference_cuda_device_kv_cache_memcpy_dtod_async_fn)( + king_inference_cuda_device_ptr dst, + king_inference_cuda_device_ptr src, + size_t bytes, + void *stream +); + +static void king_inference_cuda_device_kv_cache_reset_fields(king_inference_model_object *model) +{ + model->cuda_device_kv_cache_keys = 0; + model->cuda_device_kv_cache_values = 0; + model->cuda_device_kv_cache_result = 0; + model->cuda_device_kv_cache_error[0] = '\0'; + model->cuda_device_kv_cache_bytes = 0; + model->cuda_device_kv_cache_key_bytes = 0; + model->cuda_device_kv_cache_value_bytes = 0; + model->cuda_device_kv_cache_reserve_call_count = 0; + model->cuda_device_kv_cache_allocation_count = 0; + model->cuda_device_kv_cache_write_count = 0; + model->cuda_device_kv_cache_batch_write_count = 0; + model->cuda_device_kv_cache_guard_check_count = 0; + model->cuda_device_kv_cache_guard_failure_count = 0; + model->cuda_device_kv_cache_layers = 0; + model->cuda_device_kv_cache_kv_heads = 0; + model->cuda_device_kv_cache_context_tokens = 0; + model->cuda_device_kv_cache_key_length = 0; + model->cuda_device_kv_cache_value_length = 0; + model->cuda_device_kv_cache_last_layer = 0; + model->cuda_device_kv_cache_last_head = 0; + model->cuda_device_kv_cache_last_position = 0; + model->cuda_device_kv_cache_guard_last_layer = 0; + model->cuda_device_kv_cache_guard_last_head = 0; + model->cuda_device_kv_cache_guard_last_position = 0; + model->cuda_device_kv_cache_guard_last_count = 0; + model->cuda_device_kv_cache_guard_last_page_start = 0; + model->cuda_device_kv_cache_guard_last_page_end = 0; + model->cuda_device_kv_cache_guard_last_operation[0] = '\0'; + model->cuda_device_kv_cache_guard_last_failure[0] = '\0'; + model->cuda_device_kv_cache_attempted = false; + model->cuda_device_kv_cache_available = false; + model->cuda_device_kv_cache_shape_ready = false; + model->cuda_device_kv_cache_allocated = false; + model->cuda_device_kv_cache_f32 = false; + model->cuda_device_kv_cache_dtod_available = false; + model->cuda_device_kv_cache_guard_fail_closed = true; + model->cuda_device_kv_cache_guard_last_page_crossed = false; +} + +static void king_inference_cuda_device_kv_cache_set_error( + king_inference_model_object *model, + int result, + const char *message +) { + if (message == NULL || message[0] == '\0') { + message = "unknown_cuda_device_kv_cache_error"; + } + + model->cuda_device_kv_cache_result = result; + snprintf( + model->cuda_device_kv_cache_error, + sizeof(model->cuda_device_kv_cache_error), + "%s", + message + ); +} + +static bool king_inference_cuda_device_kv_cache_symbol( + king_inference_model_object *model, + const char *primary_name, + const char *fallback_name, + void **symbol +) { + *symbol = NULL; + if (model->cuda_driver_handle == NULL) { + king_inference_cuda_device_kv_cache_set_error(model, -1, "cuda_driver_handle_unavailable"); + return false; + } + + if (primary_name != NULL) { + *symbol = dlsym(model->cuda_driver_handle, primary_name); + } + if (*symbol == NULL && fallback_name != NULL) { + *symbol = dlsym(model->cuda_driver_handle, fallback_name); + } + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_device_kv_cache_set_error( + model, + -1, + primary_name != NULL ? primary_name : fallback_name + ); + return false; +} + +static bool king_inference_cuda_device_kv_cache_mul_size(size_t a, size_t b, size_t *out) +{ + if (a != 0 && b > ((size_t) -1) / a) { + *out = 0; + return false; + } + + *out = a * b; + return true; +} + +static bool king_inference_cuda_device_kv_cache_add_size(size_t a, size_t b, size_t *out) +{ + if (b > ((size_t) -1) - a) { + *out = 0; + return false; + } + + *out = a + b; + return true; +} + +static zend_ulong king_inference_cuda_device_kv_cache_plan_long( + king_inference_model_object *model, + const char *key, + zend_ulong fallback +) { + zval *value; + + if (Z_ISUNDEF(model->paged_kv_cache_plan) || Z_TYPE(model->paged_kv_cache_plan) != IS_ARRAY) { + return fallback; + } + + value = zend_hash_str_find(Z_ARRVAL(model->paged_kv_cache_plan), key, strlen(key)); + return value != NULL && Z_TYPE_P(value) == IS_LONG && Z_LVAL_P(value) > 0 + ? (zend_ulong) Z_LVAL_P(value) + : fallback; +} + +#include "cuda_device_kv_cache_guard.inc" + +static bool king_inference_cuda_device_kv_cache_shape( + king_inference_model_object *model, + zend_ulong *layers, + zend_ulong *kv_heads, + zend_ulong *context_tokens, + zend_ulong *key_length, + zend_ulong *value_length, + size_t *key_bytes, + size_t *value_bytes, + size_t *total_bytes +) { + size_t slots; + size_t key_units; + size_t value_units; + zend_ulong embedding = model->gguf.architecture_params[KING_INFERENCE_ARCH_EMBEDDING_LENGTH]; + zend_ulong heads = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT]; + zend_ulong runtime_heads = heads; + + *layers = king_inference_cuda_device_kv_cache_plan_long( + model, + "layers", + model->gguf.architecture_params[KING_INFERENCE_ARCH_BLOCK_COUNT] + ); + *kv_heads = king_inference_cuda_device_kv_cache_plan_long( + model, + "kv_heads", + model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT_KV] > 0 + ? model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT_KV] + : heads + ); + *context_tokens = king_inference_cuda_device_kv_cache_plan_long( + model, + "max_context_tokens", + model->gguf.architecture_params[KING_INFERENCE_ARCH_CONTEXT_LENGTH] + ); + *key_length = king_inference_cuda_device_kv_cache_plan_long( + model, + "key_dim", + model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_KEY_LENGTH] + ); + *value_length = king_inference_cuda_device_kv_cache_plan_long( + model, + "value_dim", + model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_VALUE_LENGTH] + ); + + if (*key_length == 0 && embedding > 0 && heads > 0 && embedding % heads == 0) { + *key_length = embedding / heads; + } + if (*value_length == 0) { + *value_length = *key_length; + } + king_inference_attention_refine_runtime_cache_shape_from_tensors( + model, + *layers, + &runtime_heads, + kv_heads, + key_length, + value_length + ); + if (*layers == 0 || *kv_heads == 0 || *context_tokens == 0 + || *key_length == 0 || *value_length == 0) { + return false; + } + + if (!king_inference_cuda_device_kv_cache_mul_size((size_t) *layers, (size_t) *kv_heads, &slots) + || !king_inference_cuda_device_kv_cache_mul_size(slots, (size_t) *context_tokens, &slots) + || !king_inference_cuda_device_kv_cache_mul_size(slots, (size_t) *key_length, &key_units) + || !king_inference_cuda_device_kv_cache_mul_size(slots, (size_t) *value_length, &value_units) + || !king_inference_cuda_device_kv_cache_mul_size(key_units, sizeof(float), key_bytes) + || !king_inference_cuda_device_kv_cache_mul_size(value_units, sizeof(float), value_bytes) + || !king_inference_cuda_device_kv_cache_add_size(*key_bytes, *value_bytes, total_bytes)) { + return false; + } + + return *total_bytes > 0; +} + +static void king_inference_cuda_device_kv_cache_release(king_inference_model_object *model) +{ + king_inference_cuda_device_ptr keys = model->cuda_device_kv_cache_keys; + king_inference_cuda_device_ptr values = model->cuda_device_kv_cache_values; + + if (keys != 0) { + (void) king_inference_cuda_device_allocator_free(model, keys); + } + if (values != 0) { + (void) king_inference_cuda_device_allocator_free(model, values); + } + + king_inference_cuda_device_kv_cache_reset_fields(model); +} + +static void king_inference_cuda_device_kv_cache_init( + king_inference_model_object *model, + king_inference_backend_kind backend_kind +) { + void *memcpy_dtod = NULL; + + king_inference_cuda_device_kv_cache_release(model); + if (backend_kind != KING_INFERENCE_BACKEND_KING_NATIVE_GPU + || !king_inference_gpu_enabled(&model->config)) { + return; + } + + model->cuda_device_kv_cache_attempted = true; + if (!model->cuda_device_allocator_available) { + king_inference_cuda_device_kv_cache_set_error(model, -1, "cuda_device_allocator_unavailable"); + return; + } + if (!king_inference_cuda_device_kv_cache_symbol( + model, + "cuMemcpyDtoD_v2", + "cuMemcpyDtoD", + &memcpy_dtod + )) { + return; + } + model->cuda_device_kv_cache_dtod_available = true; + + model->cuda_device_kv_cache_shape_ready = king_inference_cuda_device_kv_cache_shape( + model, + &model->cuda_device_kv_cache_layers, + &model->cuda_device_kv_cache_kv_heads, + &model->cuda_device_kv_cache_context_tokens, + &model->cuda_device_kv_cache_key_length, + &model->cuda_device_kv_cache_value_length, + &model->cuda_device_kv_cache_key_bytes, + &model->cuda_device_kv_cache_value_bytes, + &model->cuda_device_kv_cache_bytes + ); + if (!model->cuda_device_kv_cache_shape_ready) { + king_inference_cuda_device_kv_cache_set_error(model, -1, "cuda_device_kv_cache_shape_unavailable"); + return; + } + + model->cuda_device_kv_cache_available = true; + model->cuda_device_kv_cache_f32 = true; + model->cuda_device_kv_cache_result = 0; + model->cuda_device_kv_cache_error[0] = '\0'; +} + +static zend_result king_inference_cuda_device_kv_cache_reserve(king_inference_model_object *model) +{ + king_inference_cuda_device_ptr keys = 0; + king_inference_cuda_device_ptr values = 0; + + model->cuda_device_kv_cache_reserve_call_count++; + if (model->cuda_device_kv_cache_allocated) { + return SUCCESS; + } + if (!model->cuda_device_kv_cache_available || !model->cuda_device_kv_cache_shape_ready) { + king_inference_cuda_device_kv_cache_set_error(model, -1, "cuda_device_kv_cache_unavailable"); + return FAILURE; + } + if (king_inference_cuda_device_allocator_alloc(model, model->cuda_device_kv_cache_key_bytes, &keys) != SUCCESS) { + king_inference_cuda_device_kv_cache_set_error(model, -1, "cuda_device_kv_cache_key_alloc_failed"); + return FAILURE; + } + if (king_inference_cuda_device_allocator_alloc(model, model->cuda_device_kv_cache_value_bytes, &values) != SUCCESS) { + (void) king_inference_cuda_device_allocator_free(model, keys); + king_inference_cuda_device_kv_cache_set_error(model, -1, "cuda_device_kv_cache_value_alloc_failed"); + return FAILURE; + } + + model->cuda_device_kv_cache_keys = keys; + model->cuda_device_kv_cache_values = values; + model->cuda_device_kv_cache_allocated = true; + model->cuda_device_kv_cache_allocation_count++; + model->cuda_device_kv_cache_result = 0; + model->cuda_device_kv_cache_error[0] = '\0'; + return SUCCESS; +} + +static zend_result king_inference_cuda_device_kv_cache_slot_ptrs( + king_inference_model_object *model, + zend_ulong layer, + zend_ulong head, + zend_ulong position, + king_inference_cuda_device_ptr *key_ptr, + king_inference_cuda_device_ptr *value_ptr +) { + size_t slot; + size_t offset; + + *key_ptr = 0; + *value_ptr = 0; + if (king_inference_cuda_device_kv_cache_guard_slot( + model, + "slot_ptr", + layer, + head, + position, + true + ) != SUCCESS) { + return FAILURE; + } + + if (!king_inference_cuda_device_kv_cache_mul_size((size_t) layer, (size_t) model->cuda_device_kv_cache_kv_heads, &slot) + || !king_inference_cuda_device_kv_cache_add_size(slot, (size_t) head, &slot) + || !king_inference_cuda_device_kv_cache_mul_size(slot, (size_t) model->cuda_device_kv_cache_context_tokens, &slot) + || !king_inference_cuda_device_kv_cache_add_size(slot, (size_t) position, &slot) + || !king_inference_cuda_device_kv_cache_mul_size(slot, (size_t) model->cuda_device_kv_cache_key_length, &offset) + || !king_inference_cuda_device_kv_cache_mul_size(offset, sizeof(float), &offset)) { + king_inference_cuda_device_kv_cache_set_error(model, -1, "cuda_device_kv_cache_key_offset_overflow"); + return FAILURE; + } + *key_ptr = model->cuda_device_kv_cache_keys + (king_inference_cuda_device_ptr) offset; + + if (!king_inference_cuda_device_kv_cache_mul_size(slot, (size_t) model->cuda_device_kv_cache_value_length, &offset) + || !king_inference_cuda_device_kv_cache_mul_size(offset, sizeof(float), &offset)) { + king_inference_cuda_device_kv_cache_set_error(model, -1, "cuda_device_kv_cache_value_offset_overflow"); + return FAILURE; + } + *value_ptr = model->cuda_device_kv_cache_values + (king_inference_cuda_device_ptr) offset; + return SUCCESS; +} + +static zend_result king_inference_cuda_device_kv_cache_head( + king_inference_model_object *model, + bool value_cache, + zend_ulong layer, + zend_ulong head, + king_inference_cuda_device_ptr *cache_ptr, + zend_ulong *stride +) { + king_inference_cuda_device_ptr key_ptr; + king_inference_cuda_device_ptr value_ptr; + + *cache_ptr = 0; + *stride = 0; + if (king_inference_cuda_device_kv_cache_guard_slot( + model, + value_cache ? "read_value_head" : "read_key_head", + layer, + head, + 0, + false + ) != SUCCESS + || king_inference_cuda_device_kv_cache_reserve(model) != SUCCESS + || king_inference_cuda_device_kv_cache_slot_ptrs(model, layer, head, 0, &key_ptr, &value_ptr) != SUCCESS) { + return FAILURE; + } + + *cache_ptr = value_cache ? value_ptr : key_ptr; + *stride = value_cache ? model->cuda_device_kv_cache_value_length : model->cuda_device_kv_cache_key_length; + return SUCCESS; +} + +static zend_result king_inference_cuda_device_kv_cache_write( + king_inference_model_object *model, + zend_ulong layer, + zend_ulong head, + zend_ulong position, + king_inference_cuda_device_ptr key_vector, + zend_ulong key_length, + king_inference_cuda_device_ptr value_vector, + zend_ulong value_length +) { + king_inference_cuda_device_kv_cache_memcpy_dtod_fn cu_memcpy_dtod; + king_inference_cuda_device_kv_cache_memcpy_dtod_async_fn cu_memcpy_dtod_async = NULL; + king_inference_cuda_device_ptr key_slot; + king_inference_cuda_device_ptr value_slot; + int result; + + if (king_inference_cuda_device_kv_cache_guard_vectors( + model, + "single_write_vectors", + key_vector, + key_length, + value_vector, + value_length + ) != SUCCESS + || king_inference_cuda_device_kv_cache_guard_slot( + model, + "single_write_slot", + layer, + head, + position, + false + ) != SUCCESS) { + return FAILURE; + } + if (king_inference_cuda_device_kv_cache_reserve(model) != SUCCESS + || king_inference_cuda_device_kv_cache_slot_ptrs( + model, + layer, + head, + position, + &key_slot, + &value_slot + ) != SUCCESS) { + return FAILURE; + } + if (!king_inference_cuda_device_kv_cache_symbol( + model, + "cuMemcpyDtoDAsync_v2", + "cuMemcpyDtoDAsync", + (void **) &cu_memcpy_dtod_async + )) { + if (!king_inference_cuda_device_kv_cache_symbol( + model, + "cuMemcpyDtoD_v2", + "cuMemcpyDtoD", + (void **) &cu_memcpy_dtod + )) { + return FAILURE; + } + } + + if (cu_memcpy_dtod_async != NULL) { + result = cu_memcpy_dtod_async(key_slot, key_vector, (size_t) key_length * sizeof(float), NULL); + if (result != 0) { + king_inference_cuda_device_kv_cache_set_error(model, result, "cuMemcpyDtoDAsync_key_failed"); + return FAILURE; + } + result = cu_memcpy_dtod_async(value_slot, value_vector, (size_t) value_length * sizeof(float), NULL); + if (result != 0) { + king_inference_cuda_device_kv_cache_set_error(model, result, "cuMemcpyDtoDAsync_value_failed"); + return FAILURE; + } + } else { + result = cu_memcpy_dtod(key_slot, key_vector, (size_t) key_length * sizeof(float)); + if (result != 0) { + king_inference_cuda_device_kv_cache_set_error(model, result, "cuMemcpyDtoD_key_failed"); + return FAILURE; + } + result = cu_memcpy_dtod(value_slot, value_vector, (size_t) value_length * sizeof(float)); + if (result != 0) { + king_inference_cuda_device_kv_cache_set_error(model, result, "cuMemcpyDtoD_value_failed"); + return FAILURE; + } + } + + model->cuda_device_kv_cache_write_count++; + model->cuda_device_kv_cache_last_layer = layer; + model->cuda_device_kv_cache_last_head = head; + model->cuda_device_kv_cache_last_position = position; + model->cuda_device_kv_cache_result = 0; + model->cuda_device_kv_cache_error[0] = '\0'; + return SUCCESS; +} + +static zend_result king_inference_cuda_device_kv_cache_write_batch_strided( + king_inference_model_object *model, + zend_ulong layer, + zend_ulong head, + zend_ulong position_start, + zend_ulong count, + king_inference_cuda_device_ptr key_matrix, + zend_ulong key_row_stride, + zend_ulong key_offset, + zend_ulong key_length, + king_inference_cuda_device_ptr value_matrix, + zend_ulong value_row_stride, + zend_ulong value_offset, + zend_ulong value_length +) { + king_inference_cuda_device_kv_cache_memcpy_dtod_fn cu_memcpy_dtod; + king_inference_cuda_device_kv_cache_memcpy_dtod_async_fn cu_memcpy_dtod_async = NULL; + zend_ulong index; + + if (key_matrix == 0 || value_matrix == 0) { + king_inference_cuda_device_kv_cache_guard_record( + model, + "batch_write_vectors", + "cuda_device_kv_cache_guard_batch_matrix_invalid", + layer, + head, + position_start, + count + ); + return FAILURE; + } + if (king_inference_cuda_device_kv_cache_guard_batch_source( + model, + "batch_write_source", + count, + key_row_stride, + key_offset, + key_length, + value_row_stride, + value_offset, + value_length + ) != SUCCESS + || king_inference_cuda_device_kv_cache_guard_range( + model, + "batch_write_range", + layer, + head, + position_start, + count, + false + ) != SUCCESS) { + return FAILURE; + } + if (king_inference_cuda_device_kv_cache_reserve(model) != SUCCESS) { + return FAILURE; + } + if (!king_inference_cuda_device_kv_cache_symbol( + model, + "cuMemcpyDtoDAsync_v2", + "cuMemcpyDtoDAsync", + (void **) &cu_memcpy_dtod_async + )) { + if (!king_inference_cuda_device_kv_cache_symbol( + model, + "cuMemcpyDtoD_v2", + "cuMemcpyDtoD", + (void **) &cu_memcpy_dtod + )) { + return FAILURE; + } + } + + for (index = 0; index < count; index++) { + king_inference_cuda_device_ptr key_slot; + king_inference_cuda_device_ptr value_slot; + king_inference_cuda_device_ptr key_source; + king_inference_cuda_device_ptr value_source; + size_t key_units; + size_t value_units; + size_t key_bytes; + size_t value_bytes; + int result; + + if (king_inference_cuda_device_kv_cache_slot_ptrs( + model, + layer, + head, + position_start + index, + &key_slot, + &value_slot + ) != SUCCESS + || !king_inference_cuda_device_kv_cache_mul_size((size_t) index, (size_t) key_row_stride, &key_units) + || !king_inference_cuda_device_kv_cache_add_size(key_units, (size_t) key_offset, &key_units) + || !king_inference_cuda_device_kv_cache_mul_size(key_units, sizeof(float), &key_bytes) + || !king_inference_cuda_device_kv_cache_mul_size((size_t) index, (size_t) value_row_stride, &value_units) + || !king_inference_cuda_device_kv_cache_add_size(value_units, (size_t) value_offset, &value_units) + || !king_inference_cuda_device_kv_cache_mul_size(value_units, sizeof(float), &value_bytes)) { + king_inference_cuda_device_kv_cache_set_error(model, -1, "cuda_device_kv_cache_batch_offset_overflow"); + return FAILURE; + } + key_source = key_matrix + (king_inference_cuda_device_ptr) key_bytes; + value_source = value_matrix + (king_inference_cuda_device_ptr) value_bytes; + if (cu_memcpy_dtod_async != NULL) { + result = cu_memcpy_dtod_async(key_slot, key_source, (size_t) key_length * sizeof(float), NULL); + if (result != 0) { + king_inference_cuda_device_kv_cache_set_error(model, result, "cuMemcpyDtoDAsync_batch_key_failed"); + return FAILURE; + } + result = cu_memcpy_dtod_async(value_slot, value_source, (size_t) value_length * sizeof(float), NULL); + if (result != 0) { + king_inference_cuda_device_kv_cache_set_error(model, result, "cuMemcpyDtoDAsync_batch_value_failed"); + return FAILURE; + } + } else { + result = cu_memcpy_dtod(key_slot, key_source, (size_t) key_length * sizeof(float)); + if (result != 0) { + king_inference_cuda_device_kv_cache_set_error(model, result, "cuMemcpyDtoD_batch_key_failed"); + return FAILURE; + } + result = cu_memcpy_dtod(value_slot, value_source, (size_t) value_length * sizeof(float)); + if (result != 0) { + king_inference_cuda_device_kv_cache_set_error(model, result, "cuMemcpyDtoD_batch_value_failed"); + return FAILURE; + } + } + } + + model->cuda_device_kv_cache_write_count += count; + model->cuda_device_kv_cache_batch_write_count++; + model->cuda_device_kv_cache_last_layer = layer; + model->cuda_device_kv_cache_last_head = head; + model->cuda_device_kv_cache_last_position = position_start + count - 1; + model->cuda_device_kv_cache_result = 0; + model->cuda_device_kv_cache_error[0] = '\0'; + return SUCCESS; +} + +static void king_inference_cuda_device_kv_cache_add_status( + king_inference_model_object *model, + zval *return_value +) { + zval cache; + + array_init(&cache); + add_assoc_bool(&cache, "attempted", model->cuda_device_kv_cache_attempted); + add_assoc_bool(&cache, "available", model->cuda_device_kv_cache_available); + add_assoc_bool(&cache, "shape_ready", model->cuda_device_kv_cache_shape_ready); + add_assoc_bool(&cache, "allocated", model->cuda_device_kv_cache_allocated); + add_assoc_bool(&cache, "lazy_allocation", true); + add_assoc_bool(&cache, "f32", model->cuda_device_kv_cache_f32); + add_assoc_bool(&cache, "device_to_device_copy_available", model->cuda_device_kv_cache_dtod_available); + add_assoc_bool(&cache, "read_write_bounds_checked", true); + add_assoc_bool(&cache, "write_slot_bounds_checked", true); + add_assoc_bool(&cache, "read_range_bounds_checked", true); + add_assoc_bool(&cache, "full_width_vectors_required", true); + add_assoc_bool(&cache, "context_overflow_fails_closed", true); + add_assoc_bool(&cache, "page_boundary_tracked", true); + add_assoc_bool(&cache, "page_boundary_fails_closed", true); + add_assoc_bool(&cache, "reset_clears_device_pointers", true); + add_assoc_bool(&cache, "reset_clears_guard_state", true); + add_assoc_long(&cache, "bytes", (zend_long) model->cuda_device_kv_cache_bytes); + add_assoc_long(&cache, "key_bytes", (zend_long) model->cuda_device_kv_cache_key_bytes); + add_assoc_long(&cache, "value_bytes", (zend_long) model->cuda_device_kv_cache_value_bytes); + add_assoc_long(&cache, "reserve_calls", (zend_long) model->cuda_device_kv_cache_reserve_call_count); + add_assoc_long(&cache, "logical_allocations", (zend_long) model->cuda_device_kv_cache_allocation_count); + add_assoc_long(&cache, "layers", (zend_long) model->cuda_device_kv_cache_layers); + add_assoc_long(&cache, "kv_heads", (zend_long) model->cuda_device_kv_cache_kv_heads); + add_assoc_long(&cache, "context_tokens", (zend_long) model->cuda_device_kv_cache_context_tokens); + add_assoc_long(&cache, "key_length", (zend_long) model->cuda_device_kv_cache_key_length); + add_assoc_long(&cache, "value_length", (zend_long) model->cuda_device_kv_cache_value_length); + add_assoc_long(&cache, "writes", (zend_long) model->cuda_device_kv_cache_write_count); + add_assoc_long(&cache, "batch_writes", (zend_long) model->cuda_device_kv_cache_batch_write_count); + add_assoc_bool(&cache, "guard_fail_closed", model->cuda_device_kv_cache_guard_fail_closed); + add_assoc_long(&cache, "guard_checks", (zend_long) model->cuda_device_kv_cache_guard_check_count); + add_assoc_long(&cache, "guard_failures", (zend_long) model->cuda_device_kv_cache_guard_failure_count); + add_assoc_string(&cache, "guard_last_operation", model->cuda_device_kv_cache_guard_last_operation); + add_assoc_string(&cache, "guard_last_failure", model->cuda_device_kv_cache_guard_last_failure); + add_assoc_long(&cache, "guard_last_layer", (zend_long) model->cuda_device_kv_cache_guard_last_layer); + add_assoc_long(&cache, "guard_last_head", (zend_long) model->cuda_device_kv_cache_guard_last_head); + add_assoc_long(&cache, "guard_last_position", (zend_long) model->cuda_device_kv_cache_guard_last_position); + add_assoc_long(&cache, "guard_last_count", (zend_long) model->cuda_device_kv_cache_guard_last_count); + add_assoc_long(&cache, "guard_last_page_start", (zend_long) model->cuda_device_kv_cache_guard_last_page_start); + add_assoc_long(&cache, "guard_last_page_end", (zend_long) model->cuda_device_kv_cache_guard_last_page_end); + add_assoc_bool(&cache, "guard_last_page_crossed", model->cuda_device_kv_cache_guard_last_page_crossed); + add_assoc_long(&cache, "last_layer", (zend_long) model->cuda_device_kv_cache_last_layer); + add_assoc_long(&cache, "last_head", (zend_long) model->cuda_device_kv_cache_last_head); + add_assoc_long(&cache, "last_position", (zend_long) model->cuda_device_kv_cache_last_position); + add_assoc_long(&cache, "result", model->cuda_device_kv_cache_result); + add_assoc_string(&cache, "error", model->cuda_device_kv_cache_error); + add_assoc_zval(return_value, "device_kv_cache", &cache); + add_assoc_bool(return_value, "gpu_device_kv_cache_ready", model->cuda_device_kv_cache_available); +} + +static void king_inference_cuda_device_kv_cache_apply_status( + king_inference_model_object *model, + zval *return_value +) { + zval *reason; + bool reason_can_be_kv_cache; + + king_inference_cuda_device_kv_cache_add_status(model, return_value); + if (!model->cuda_device_kv_cache_attempted + || model->cuda_device_kv_cache_available + || !model->cuda_device_allocator_available) { + return; + } + + reason = zend_hash_str_find(Z_ARRVAL_P(return_value), "reason", sizeof("reason") - 1); + reason_can_be_kv_cache = reason == NULL + || Z_TYPE_P(reason) != IS_STRING + || zend_string_equals_literal(Z_STR_P(reason), "gpu_ready") + || zend_string_equals_literal(Z_STR_P(reason), "gpu_decoder_kernel_not_implemented"); + if (reason_can_be_kv_cache) { + king_inference_cuda_context_prepend_return_reason( + return_value, + "gpu_device_kv_cache_unavailable" + ); + add_assoc_string(return_value, "reason", "gpu_device_kv_cache_unavailable"); + } else { + king_inference_gpu_status_append_return_reason(return_value, "gpu_device_kv_cache_unavailable"); + } +} diff --git a/extension/src/inference/cuda/kernels/device/cuda_device_kv_cache_guard.inc b/extension/src/inference/cuda/kernels/device/cuda_device_kv_cache_guard.inc new file mode 100644 index 000000000..9b6fa1f97 --- /dev/null +++ b/extension/src/inference/cuda/kernels/device/cuda_device_kv_cache_guard.inc @@ -0,0 +1,206 @@ +/* + * Fail-closed guard for CUDA device KV-cache access. + */ + +#ifndef KING_INFERENCE_DEFAULT_KV_PAGE_TOKENS +#define KING_INFERENCE_DEFAULT_KV_PAGE_TOKENS 16 +#endif + +static zend_ulong king_inference_cuda_device_kv_cache_guard_page_tokens( + king_inference_model_object *model +) { + return king_inference_cuda_device_kv_cache_plan_long( + model, + "page_tokens", + KING_INFERENCE_DEFAULT_KV_PAGE_TOKENS + ); +} + +static void king_inference_cuda_device_kv_cache_guard_record( + king_inference_model_object *model, + const char *operation, + const char *failure, + zend_ulong layer, + zend_ulong head, + zend_ulong position, + zend_ulong count +) { + zend_ulong page_tokens; + zend_ulong last_position; + + model->cuda_device_kv_cache_guard_check_count++; + model->cuda_device_kv_cache_guard_fail_closed = true; + model->cuda_device_kv_cache_guard_last_layer = layer; + model->cuda_device_kv_cache_guard_last_head = head; + model->cuda_device_kv_cache_guard_last_position = position; + model->cuda_device_kv_cache_guard_last_count = count; + snprintf( + model->cuda_device_kv_cache_guard_last_operation, + sizeof(model->cuda_device_kv_cache_guard_last_operation), + "%s", + operation != NULL && operation[0] != '\0' ? operation : "unknown" + ); + + page_tokens = king_inference_cuda_device_kv_cache_guard_page_tokens(model); + if (count > 0 + && page_tokens > 0 + && position <= (zend_ulong) -1 - (count - 1)) { + last_position = position + count - 1; + model->cuda_device_kv_cache_guard_last_page_start = position / page_tokens; + model->cuda_device_kv_cache_guard_last_page_end = last_position / page_tokens; + model->cuda_device_kv_cache_guard_last_page_crossed = + model->cuda_device_kv_cache_guard_last_page_start != model->cuda_device_kv_cache_guard_last_page_end; + } else { + model->cuda_device_kv_cache_guard_last_page_start = 0; + model->cuda_device_kv_cache_guard_last_page_end = 0; + model->cuda_device_kv_cache_guard_last_page_crossed = false; + } + + if (failure != NULL && failure[0] != '\0') { + model->cuda_device_kv_cache_guard_failure_count++; + snprintf( + model->cuda_device_kv_cache_guard_last_failure, + sizeof(model->cuda_device_kv_cache_guard_last_failure), + "%s", + failure + ); + king_inference_cuda_device_kv_cache_set_error(model, -1, failure); + } +} + +static zend_result king_inference_cuda_device_kv_cache_guard_range( + king_inference_model_object *model, + const char *operation, + zend_ulong layer, + zend_ulong head, + zend_ulong position_start, + zend_ulong count, + bool require_allocated +) { + zend_ulong page_tokens = king_inference_cuda_device_kv_cache_guard_page_tokens(model); + const char *failure = NULL; + + if (!model->cuda_device_kv_cache_available || !model->cuda_device_kv_cache_shape_ready) { + failure = "cuda_device_kv_cache_guard_unavailable"; + } else if (require_allocated && !model->cuda_device_kv_cache_allocated) { + failure = "cuda_device_kv_cache_guard_not_allocated"; + } else if (model->cuda_device_kv_cache_layers == 0 + || model->cuda_device_kv_cache_kv_heads == 0 + || model->cuda_device_kv_cache_context_tokens == 0 + || model->cuda_device_kv_cache_key_length == 0 + || model->cuda_device_kv_cache_value_length == 0) { + failure = "cuda_device_kv_cache_guard_shape_invalid"; + } else if (count == 0) { + failure = "cuda_device_kv_cache_guard_empty_range"; + } else if (layer >= model->cuda_device_kv_cache_layers) { + failure = "cuda_device_kv_cache_guard_layer_out_of_range"; + } else if (head >= model->cuda_device_kv_cache_kv_heads) { + failure = "cuda_device_kv_cache_guard_head_out_of_range"; + } else if (position_start >= model->cuda_device_kv_cache_context_tokens + || count > model->cuda_device_kv_cache_context_tokens - position_start) { + failure = "cuda_device_kv_cache_guard_context_overflow"; + } + + king_inference_cuda_device_kv_cache_guard_record( + model, + operation, + failure, + layer, + head, + position_start, + count + ); + return failure == NULL ? SUCCESS : FAILURE; +} + +static zend_result king_inference_cuda_device_kv_cache_guard_slot( + king_inference_model_object *model, + const char *operation, + zend_ulong layer, + zend_ulong head, + zend_ulong position, + bool require_allocated +) { + return king_inference_cuda_device_kv_cache_guard_range( + model, + operation, + layer, + head, + position, + 1, + require_allocated + ); +} + +static zend_result king_inference_cuda_device_kv_cache_guard_vectors( + king_inference_model_object *model, + const char *operation, + king_inference_cuda_device_ptr key_vector, + zend_ulong key_length, + king_inference_cuda_device_ptr value_vector, + zend_ulong value_length +) { + const char *failure = NULL; + + if (key_vector == 0 || value_vector == 0) { + failure = "cuda_device_kv_cache_guard_vector_invalid"; + } else if (key_length == 0 || value_length == 0) { + failure = "cuda_device_kv_cache_guard_vector_empty"; + } else if (key_length > model->cuda_device_kv_cache_key_length + || value_length > model->cuda_device_kv_cache_value_length) { + failure = "cuda_device_kv_cache_guard_vector_shape_invalid"; + } else if (key_length > (zend_ulong) (SIZE_MAX / sizeof(float)) + || value_length > (zend_ulong) (SIZE_MAX / sizeof(float))) { + failure = "cuda_device_kv_cache_guard_vector_bytes_overflow"; + } + + if (failure == NULL) { + return SUCCESS; + } + king_inference_cuda_device_kv_cache_guard_record(model, operation, failure, 0, 0, 0, 0); + return FAILURE; +} + +static zend_result king_inference_cuda_device_kv_cache_guard_batch_source( + king_inference_model_object *model, + const char *operation, + zend_ulong count, + zend_ulong key_row_stride, + zend_ulong key_offset, + zend_ulong key_length, + zend_ulong value_row_stride, + zend_ulong value_offset, + zend_ulong value_length +) { + size_t key_units; + size_t value_units; + zend_ulong last_index = count > 0 ? count - 1 : 0; + const char *failure = NULL; + + if (count == 0) { + failure = "cuda_device_kv_cache_guard_batch_empty"; + } else if (key_length == 0 || value_length == 0 || key_row_stride == 0 || value_row_stride == 0) { + failure = "cuda_device_kv_cache_guard_batch_vector_empty"; + } else if (key_offset > key_row_stride + || value_offset > value_row_stride + || key_length > key_row_stride - key_offset + || value_length > value_row_stride - value_offset) { + failure = "cuda_device_kv_cache_guard_batch_stride_invalid"; + } else if (key_length > model->cuda_device_kv_cache_key_length + || value_length > model->cuda_device_kv_cache_value_length) { + failure = "cuda_device_kv_cache_guard_batch_vector_shape_invalid"; + } else if (!king_inference_cuda_device_kv_cache_mul_size((size_t) last_index, (size_t) key_row_stride, &key_units) + || !king_inference_cuda_device_kv_cache_add_size(key_units, (size_t) key_offset, &key_units) + || !king_inference_cuda_device_kv_cache_add_size(key_units, (size_t) key_length, &key_units) + || !king_inference_cuda_device_kv_cache_mul_size((size_t) last_index, (size_t) value_row_stride, &value_units) + || !king_inference_cuda_device_kv_cache_add_size(value_units, (size_t) value_offset, &value_units) + || !king_inference_cuda_device_kv_cache_add_size(value_units, (size_t) value_length, &value_units)) { + failure = "cuda_device_kv_cache_guard_batch_source_overflow"; + } + + if (failure == NULL) { + return SUCCESS; + } + king_inference_cuda_device_kv_cache_guard_record(model, operation, failure, 0, 0, 0, count); + return FAILURE; +} diff --git a/extension/src/inference/cuda/kernels/device/cuda_device_vector_numeric_compare.inc b/extension/src/inference/cuda/kernels/device/cuda_device_vector_numeric_compare.inc new file mode 100644 index 000000000..38ada32bf --- /dev/null +++ b/extension/src/inference/cuda/kernels/device/cuda_device_vector_numeric_compare.inc @@ -0,0 +1,248 @@ +/* + * CUDA device-vector numeric compare diagnostics for native inference. + */ + +static bool king_inference_cuda_device_vector_numeric_compare_enabled(zval *config) +{ + zval *gpu = king_inference_array_find(config, "gpu"); + zval *debug = gpu != NULL && Z_TYPE_P(gpu) == IS_ARRAY + ? king_inference_array_find(gpu, "debug") + : NULL; + zval *enabled = debug != NULL && Z_TYPE_P(debug) == IS_ARRAY + ? king_inference_array_find(debug, "numeric_compare_enabled") + : NULL; + + if (enabled != NULL && (Z_TYPE_P(enabled) == IS_TRUE || Z_TYPE_P(enabled) == IS_FALSE)) { + return zend_is_true(enabled); + } + + return king_high_perf_compute_ai_config.inference_cuda_numeric_compare_enable; +} + +static zend_ulong king_inference_cuda_device_vector_numeric_compare_max_values(zval *config) +{ + zval *gpu = king_inference_array_find(config, "gpu"); + zval *debug = gpu != NULL && Z_TYPE_P(gpu) == IS_ARRAY + ? king_inference_array_find(gpu, "debug") + : NULL; + zval *configured = debug != NULL && Z_TYPE_P(debug) == IS_ARRAY + ? king_inference_array_find(debug, "numeric_compare_max_values") + : NULL; + zend_long max_values = king_high_perf_compute_ai_config.inference_cuda_numeric_compare_max_values; + + if (configured != NULL && Z_TYPE_P(configured) == IS_LONG && Z_LVAL_P(configured) > 0) { + max_values = Z_LVAL_P(configured); + } + if (max_values <= 0) { + max_values = 1; + } + if (max_values > 1024) { + max_values = 1024; + } + + return (zend_ulong) max_values; +} + +static void king_inference_cuda_device_vector_numeric_compare_status( + king_inference_model_object *model, + zval *return_value +) { + bool enabled = king_inference_cuda_device_vector_numeric_compare_enabled(&model->config); + bool available = enabled + && model->cuda_driver_handle != NULL + && model->cuda_device_allocator_available; + zval tolerance_contract; + zval reference_contract; + + array_init(return_value); + add_assoc_bool(return_value, "enabled", enabled); + add_assoc_bool(return_value, "available", available); + add_assoc_bool(return_value, "public_api", false); + add_assoc_bool(return_value, "unavailable_by_default", true); + add_assoc_long( + return_value, + "max_values", + (zend_long) king_inference_cuda_device_vector_numeric_compare_max_values(&model->config) + ); + add_assoc_string(return_value, "scope", "internal_cuda_device_vector_readback_compare"); + add_assoc_string(return_value, "reference_backend", "king_internal_cpu_reference"); + add_assoc_bool(return_value, "active_runtime_backend", false); + add_assoc_bool(return_value, "production_execution_path", false); + add_assoc_string(return_value, "readback", "cuMemcpyDtoH"); + add_assoc_string(return_value, "status", enabled ? (available ? "ready" : "unavailable") : "disabled"); + king_inference_reference_backend_contract_array(KING_INFERENCE_BACKEND_KING_NATIVE_GPU, &reference_contract); + add_assoc_zval(return_value, "reference_contract", &reference_contract); + king_inference_cuda_numeric_tolerance_contract_table(&tolerance_contract); + add_assoc_zval(return_value, "tolerance_contract", &tolerance_contract); +} + +static zend_result king_inference_cuda_device_vector_numeric_compare( + king_inference_model_object *model, + const char *label, + const char *op, + zend_long layer, + const char *tensor, + zend_ulong sample_offset, + king_inference_cuda_device_ptr device_vector, + zend_ulong length, + const float *expected, + zend_ulong expected_length, + double tolerance, + zval *return_value +) { + king_inference_cuda_memcpy_dtoh_fn cu_memcpy_dtoh; + zend_ulong max_values; + zend_ulong compare_count; + size_t readback_bytes; + float *actual; + double max_abs_diff = 0.0; + double max_relative_diff = 0.0; + double relative_tolerance = king_inference_cuda_numeric_tolerance_relative(op); + bool matched = true; + zend_ulong matched_values = 0; + zend_long first_failure_sample_index = -1; + const char *first_failure_classification = ""; + int result; + zval samples; + + array_init(return_value); + add_assoc_string(return_value, "label", label != NULL ? label : ""); + add_assoc_long(return_value, "tolerance_contract_version", 1); + add_assoc_string(return_value, "op", op != NULL ? op : "unknown"); + if (layer >= 0) { + add_assoc_long(return_value, "layer", layer); + } else { + add_assoc_null(return_value, "layer"); + } + if (tensor != NULL && tensor[0] != '\0') { + add_assoc_string(return_value, "tensor", tensor); + } else { + add_assoc_null(return_value, "tensor"); + } + add_assoc_bool(return_value, "enabled", king_inference_cuda_device_vector_numeric_compare_enabled(&model->config)); + add_assoc_bool(return_value, "public_api", false); + add_assoc_string(return_value, "scope", "internal_cuda_device_vector_readback_compare"); + add_assoc_string(return_value, "match_rule", "abs_or_relative"); + add_assoc_string(return_value, "top_k_equivalence", king_inference_cuda_numeric_tolerance_top_k_rule(op)); + + if (!king_inference_cuda_device_vector_numeric_compare_enabled(&model->config)) { + add_assoc_string(return_value, "status", "disabled"); + return SUCCESS; + } + if (device_vector == 0 || expected == NULL || length == 0 || expected_length == 0) { + add_assoc_string(return_value, "status", "invalid_arguments"); + add_assoc_bool(return_value, "matched", false); + return FAILURE; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + add_assoc_string(return_value, "status", "cuda_context_current_failed"); + add_assoc_bool(return_value, "matched", false); + return FAILURE; + } + + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH_v2"); + if (cu_memcpy_dtoh == NULL) { + cu_memcpy_dtoh = (king_inference_cuda_memcpy_dtoh_fn) dlsym(model->cuda_driver_handle, "cuMemcpyDtoH"); + } + if (cu_memcpy_dtoh == NULL) { + king_inference_cuda_device_vector_ops_set_error(model, -1, "cuMemcpyDtoH_unavailable"); + add_assoc_string(return_value, "status", "cuMemcpyDtoH_unavailable"); + add_assoc_bool(return_value, "matched", false); + return FAILURE; + } + + max_values = king_inference_cuda_device_vector_numeric_compare_max_values(&model->config); + compare_count = length < expected_length ? length : expected_length; + compare_count = compare_count < max_values ? compare_count : max_values; + readback_bytes = (size_t) compare_count * sizeof(float); + actual = emalloc(readback_bytes); + result = cu_memcpy_dtoh(actual, device_vector, readback_bytes); + if (result != 0) { + efree(actual); + king_inference_cuda_device_vector_ops_set_error(model, result, "cuMemcpyDtoH_failed"); + add_assoc_string(return_value, "status", "cuMemcpyDtoH_failed"); + add_assoc_long(return_value, "result", result); + add_assoc_bool(return_value, "matched", false); + return FAILURE; + } + + array_init(&samples); + for (zend_ulong i = 0; i < compare_count; i++) { + double diff = fabs((double) actual[i] - (double) expected[i]); + double rel_diff = king_inference_cuda_numeric_relative_diff((double) actual[i], (double) expected[i]); + const char *classification = king_inference_cuda_numeric_sample_classification( + (double) actual[i], + (double) expected[i], + diff, + rel_diff, + tolerance, + relative_tolerance + ); + zval sample; + + if (king_inference_cuda_numeric_sample_matches(classification)) { + matched_values++; + } else { + matched = false; + if (first_failure_sample_index < 0) { + first_failure_sample_index = (zend_long) (sample_offset + i); + first_failure_classification = classification; + } + } + if (diff > max_abs_diff) { + max_abs_diff = diff; + } + if (rel_diff > max_relative_diff && isfinite(rel_diff)) { + max_relative_diff = rel_diff; + } + + array_init(&sample); + add_assoc_long(&sample, "index", (zend_long) i); + add_assoc_long(&sample, "sample_index", (zend_long) (sample_offset + i)); + add_assoc_string(&sample, "op", op != NULL ? op : "unknown"); + if (layer >= 0) { + add_assoc_long(&sample, "layer", layer); + } else { + add_assoc_null(&sample, "layer"); + } + if (tensor != NULL && tensor[0] != '\0') { + add_assoc_string(&sample, "tensor", tensor); + } else { + add_assoc_null(&sample, "tensor"); + } + add_assoc_double(&sample, "actual", (double) actual[i]); + add_assoc_double(&sample, "expected", (double) expected[i]); + add_assoc_double(&sample, "abs_diff", diff); + add_assoc_double(&sample, "relative_diff", rel_diff); + add_assoc_double(&sample, "max_abs_tolerance", tolerance); + add_assoc_double(&sample, "max_relative_tolerance", relative_tolerance); + add_assoc_string(&sample, "classification", classification); + add_next_index_zval(&samples, &sample); + } + + efree(actual); + add_assoc_string(return_value, "status", matched ? "matched" : "mismatch"); + add_assoc_string(return_value, "classification", matched ? "matched" : "numeric_mismatch"); + add_assoc_string(return_value, "error_category", matched ? "none" : "numeric_mismatch"); + add_assoc_string(return_value, "error_code", matched ? "none" : "king.numeric_mismatch.detected"); + add_assoc_string(return_value, "failure_status", matched ? "none" : "numeric_mismatch"); + add_assoc_bool(return_value, "matched", matched); + add_assoc_long(return_value, "compared_values", (zend_long) compare_count); + add_assoc_long(return_value, "matched_values", (zend_long) matched_values); + add_assoc_long(return_value, "readback_bytes", (zend_long) readback_bytes); + add_assoc_double(return_value, "tolerance", tolerance); + add_assoc_double(return_value, "max_abs_tolerance", tolerance); + add_assoc_double(return_value, "max_relative_tolerance", relative_tolerance); + add_assoc_double(return_value, "max_abs_diff", max_abs_diff); + add_assoc_double(return_value, "max_relative_diff", max_relative_diff); + if (first_failure_sample_index >= 0) { + add_assoc_long(return_value, "first_failure_sample_index", first_failure_sample_index); + add_assoc_string(return_value, "first_failure_classification", first_failure_classification); + } else { + add_assoc_null(return_value, "first_failure_sample_index"); + add_assoc_null(return_value, "first_failure_classification"); + } + add_assoc_zval(return_value, "samples", &samples); + + return matched ? SUCCESS : FAILURE; +} diff --git a/extension/src/inference/cuda/kernels/device/cuda_device_vector_ops.inc b/extension/src/inference/cuda/kernels/device/cuda_device_vector_ops.inc new file mode 100644 index 000000000..efa309384 --- /dev/null +++ b/extension/src/inference/cuda/kernels/device/cuda_device_vector_ops.inc @@ -0,0 +1,634 @@ +/* + * CUDA device-side vector operations for native King GPU inference. + */ + +typedef int (*king_inference_cuda_memcpy_dtoh_fn)( + void *dst_host, + king_inference_cuda_device_ptr src_device, + size_t byte_count +); + +static const char king_inference_cuda_device_vector_ops_source[] = +"extern \"C\" __global__ void king_vector_add(\n" +" const float *left,\n" +" const float *right,\n" +" float *output,\n" +" unsigned int length\n" +") {\n" +" unsigned int index = blockIdx.x * blockDim.x + threadIdx.x;\n" +" if (left != 0 && right != 0 && output != 0 && index < length) {\n" +" output[index] = left[index] + right[index];\n" +" }\n" +"}\n" +"extern \"C\" __global__ void king_vector_mul(\n" +" const float *left,\n" +" const float *right,\n" +" float *output,\n" +" unsigned int length\n" +") {\n" +" unsigned int index = blockIdx.x * blockDim.x + threadIdx.x;\n" +" if (left != 0 && right != 0 && output != 0 && index < length) {\n" +" output[index] = left[index] * right[index];\n" +" }\n" +"}\n" +"extern \"C\" __global__ void king_vector_scale(\n" +" const float *input,\n" +" float scale,\n" +" float *output,\n" +" unsigned int length\n" +") {\n" +" unsigned int index = blockIdx.x * blockDim.x + threadIdx.x;\n" +" if (input != 0 && output != 0 && index < length) {\n" +" output[index] = input[index] * scale;\n" +" }\n" +"}\n" +"extern \"C\" __global__ void king_vector_silu(\n" +" const float *input,\n" +" float *output,\n" +" unsigned int length\n" +") {\n" +" unsigned int index = blockIdx.x * blockDim.x + threadIdx.x;\n" +" if (input != 0 && output != 0 && index < length) {\n" +" float value = input[index];\n" +" output[index] = value / (1.0f + __expf(-value));\n" +" }\n" +"}\n" +"extern \"C\" __global__ void king_vector_slice(\n" +" const float *input,\n" +" unsigned int offset,\n" +" float *output,\n" +" unsigned int length\n" +") {\n" +" unsigned int index = blockIdx.x * blockDim.x + threadIdx.x;\n" +" if (input != 0 && output != 0 && index < length) {\n" +" output[index] = input[offset + index];\n" +" }\n" +"}\n" +"extern \"C\" __global__ void king_vector_copy_to_offset(\n" +" const float *input,\n" +" float *output,\n" +" unsigned int output_offset,\n" +" unsigned int length\n" +") {\n" +" unsigned int index = blockIdx.x * blockDim.x + threadIdx.x;\n" +" if (input != 0 && output != 0 && index < length) {\n" +" output[output_offset + index] = input[index];\n" +" }\n" +"}\n"; + +static void king_inference_cuda_device_vector_ops_reset_fields(king_inference_model_object *model) +{ + model->cuda_device_vector_ops_nvrtc_handle = NULL; + model->cuda_device_vector_ops_module = NULL; + model->cuda_vector_add_function = NULL; + model->cuda_vector_mul_function = NULL; + model->cuda_vector_scale_function = NULL; + model->cuda_vector_silu_function = NULL; + model->cuda_vector_slice_function = NULL; + model->cuda_vector_copy_to_offset_function = NULL; + model->cuda_device_vector_ops_result = 0; + model->cuda_device_vector_ops_error[0] = '\0'; + model->cuda_device_vector_ops_launch_count = 0; + model->cuda_device_vector_ops_last_length = 0; + model->cuda_device_vector_ops_attempted = false; + model->cuda_device_vector_ops_available = false; + model->cuda_device_vector_ops_nvrtc_available = false; + model->cuda_device_vector_ops_module_loaded = false; + model->cuda_vector_add_available = false; + model->cuda_vector_mul_available = false; + model->cuda_vector_scale_available = false; + model->cuda_vector_silu_available = false; + model->cuda_vector_slice_available = false; + model->cuda_vector_copy_to_offset_available = false; +} + +static void king_inference_cuda_device_vector_ops_set_error( + king_inference_model_object *model, + int result, + const char *message +) { + if (message == NULL || message[0] == '\0') { + message = "unknown_cuda_device_vector_ops_error"; + } + + model->cuda_device_vector_ops_result = result; + snprintf(model->cuda_device_vector_ops_error, sizeof(model->cuda_device_vector_ops_error), "%s", message); +} + +static bool king_inference_cuda_device_vector_ops_driver_symbol( + king_inference_model_object *model, + const char *name, + void **symbol +) { + *symbol = NULL; + if (model->cuda_driver_handle == NULL) { + king_inference_cuda_device_vector_ops_set_error(model, -1, "cuda_driver_handle_unavailable"); + return false; + } + + *symbol = dlsym(model->cuda_driver_handle, name); + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_device_vector_ops_set_error(model, -1, name); + return false; +} + +#include "cuda_device_vector_numeric_compare.inc" + +static bool king_inference_cuda_device_vector_ops_nvrtc_symbol( + king_inference_model_object *model, + const char *name, + void **symbol +) { + *symbol = NULL; + if (model->cuda_device_vector_ops_nvrtc_handle == NULL) { + king_inference_cuda_device_vector_ops_set_error(model, -1, "nvrtc_handle_unavailable"); + return false; + } + + *symbol = dlsym(model->cuda_device_vector_ops_nvrtc_handle, name); + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_device_vector_ops_set_error(model, -1, name); + return false; +} + +static bool king_inference_cuda_device_vector_ops_open_nvrtc(king_inference_model_object *model) +{ + const char *libraries[] = {"libnvrtc.so", "libnvrtc.so.12", "libnvrtc.so.11", NULL}; + size_t index; + + for (index = 0; libraries[index] != NULL; index++) { + model->cuda_device_vector_ops_nvrtc_handle = dlopen(libraries[index], RTLD_LAZY | RTLD_LOCAL); + if (model->cuda_device_vector_ops_nvrtc_handle != NULL) { + model->cuda_device_vector_ops_nvrtc_available = true; + return true; + } + } + + king_inference_cuda_device_vector_ops_set_error(model, -1, "libnvrtc_unavailable"); + return false; +} + +static bool king_inference_cuda_device_vector_ops_compile_ptx( + king_inference_model_object *model, + char **ptx_out, + size_t *ptx_size_out +) { + king_inference_nvrtc_create_program_fn nvrtc_create_program; + king_inference_nvrtc_compile_program_fn nvrtc_compile_program; + king_inference_nvrtc_get_ptx_size_fn nvrtc_get_ptx_size; + king_inference_nvrtc_get_ptx_fn nvrtc_get_ptx; + king_inference_nvrtc_destroy_program_fn nvrtc_destroy_program; + king_inference_nvrtc_program program = NULL; + const char *options[] = {"--std=c++11", "--gpu-architecture=compute_89", "--use_fast_math"}; + char *ptx; + size_t ptx_size = 0; + king_inference_nvrtc_result result; + + *ptx_out = NULL; + *ptx_size_out = 0; + if (!king_inference_cuda_device_vector_ops_nvrtc_symbol(model, "nvrtcCreateProgram", (void **) &nvrtc_create_program) + || !king_inference_cuda_device_vector_ops_nvrtc_symbol(model, "nvrtcCompileProgram", (void **) &nvrtc_compile_program) + || !king_inference_cuda_device_vector_ops_nvrtc_symbol(model, "nvrtcGetPTXSize", (void **) &nvrtc_get_ptx_size) + || !king_inference_cuda_device_vector_ops_nvrtc_symbol(model, "nvrtcGetPTX", (void **) &nvrtc_get_ptx) + || !king_inference_cuda_device_vector_ops_nvrtc_symbol(model, "nvrtcDestroyProgram", (void **) &nvrtc_destroy_program)) { + return false; + } + + result = nvrtc_create_program( + &program, + king_inference_cuda_device_vector_ops_source, + "king_device_vector_ops.cu", + 0, + NULL, + NULL + ); + if (result != 0 || program == NULL) { + king_inference_cuda_device_vector_ops_set_error( + model, + result, + result != 0 ? "nvrtcCreateProgram_failed" : "nvrtc_program_unavailable" + ); + return false; + } + result = nvrtc_compile_program(program, 3, options); + if (result != 0) { + (void) nvrtc_destroy_program(&program); + king_inference_cuda_device_vector_ops_set_error(model, result, "nvrtcCompileProgram_failed"); + return false; + } + result = nvrtc_get_ptx_size(program, &ptx_size); + if (result != 0 || ptx_size == 0) { + (void) nvrtc_destroy_program(&program); + king_inference_cuda_device_vector_ops_set_error( + model, + result, + result != 0 ? "nvrtcGetPTXSize_failed" : "nvrtc_ptx_empty" + ); + return false; + } + ptx = emalloc(ptx_size); + result = nvrtc_get_ptx(program, ptx); + (void) nvrtc_destroy_program(&program); + if (result != 0) { + efree(ptx); + king_inference_cuda_device_vector_ops_set_error(model, result, "nvrtcGetPTX_failed"); + return false; + } + + *ptx_out = ptx; + *ptx_size_out = ptx_size; + return true; +} + +static bool king_inference_cuda_device_vector_ops_get_function( + king_inference_model_object *model, + void *module, + const char *name, + void **function_out +) { + king_inference_cuda_module_get_function_fn cu_module_get_function; + int result; + + if (!king_inference_cuda_device_vector_ops_driver_symbol( + model, + "cuModuleGetFunction", + (void **) &cu_module_get_function + )) { + return false; + } + result = cu_module_get_function(function_out, module, name); + if (result != 0 || *function_out == NULL) { + king_inference_cuda_device_vector_ops_set_error( + model, + result, + result != 0 ? "cuModuleGetFunction_failed" : name + ); + return false; + } + + return true; +} + +static void king_inference_cuda_device_vector_ops_release(king_inference_model_object *model) +{ + king_inference_cuda_module_unload_fn cu_module_unload; + + if (model->cuda_device_vector_ops_module != NULL + && model->cuda_driver_handle != NULL + && king_inference_cuda_device_allocator_set_current(model) + && king_inference_cuda_device_vector_ops_driver_symbol( + model, + "cuModuleUnload", + (void **) &cu_module_unload + )) { + (void) cu_module_unload(model->cuda_device_vector_ops_module); + } + if (model->cuda_device_vector_ops_nvrtc_handle != NULL) { + dlclose(model->cuda_device_vector_ops_nvrtc_handle); + } + + king_inference_cuda_device_vector_ops_reset_fields(model); +} + +static void king_inference_cuda_device_vector_ops_init( + king_inference_model_object *model, + king_inference_backend_kind backend_kind +) { + king_inference_cuda_module_load_data_fn cu_module_load_data; + void *module = NULL; + char *ptx = NULL; + size_t ptx_size = 0; + int result; + + king_inference_cuda_device_vector_ops_release(model); + if (backend_kind != KING_INFERENCE_BACKEND_KING_NATIVE_GPU + || !king_inference_gpu_enabled(&model->config)) { + return; + } + + model->cuda_device_vector_ops_attempted = true; + if (!model->cuda_device_allocator_available) { + king_inference_cuda_device_vector_ops_set_error(model, -1, "cuda_device_allocator_unavailable"); + return; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + king_inference_cuda_device_vector_ops_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_context_current_failed" + ); + return; + } + if (!king_inference_cuda_device_vector_ops_open_nvrtc(model) + || !king_inference_cuda_device_vector_ops_compile_ptx(model, &ptx, &ptx_size)) { + return; + } + if (!king_inference_cuda_device_vector_ops_driver_symbol(model, "cuModuleLoadData", (void **) &cu_module_load_data)) { + efree(ptx); + return; + } + + result = cu_module_load_data(&module, ptx); + efree(ptx); + if (result != 0 || module == NULL) { + king_inference_cuda_device_vector_ops_set_error( + model, + result, + result != 0 ? "cuModuleLoadData_failed" : "cuda_module_unavailable" + ); + return; + } + + if (!king_inference_cuda_device_vector_ops_get_function(model, module, "king_vector_add", &model->cuda_vector_add_function) + || !king_inference_cuda_device_vector_ops_get_function(model, module, "king_vector_mul", &model->cuda_vector_mul_function) + || !king_inference_cuda_device_vector_ops_get_function(model, module, "king_vector_scale", &model->cuda_vector_scale_function) + || !king_inference_cuda_device_vector_ops_get_function(model, module, "king_vector_silu", &model->cuda_vector_silu_function) + || !king_inference_cuda_device_vector_ops_get_function(model, module, "king_vector_slice", &model->cuda_vector_slice_function) + || !king_inference_cuda_device_vector_ops_get_function( + model, + module, + "king_vector_copy_to_offset", + &model->cuda_vector_copy_to_offset_function + )) { + king_inference_cuda_module_unload_fn cu_module_unload = NULL; + + if (king_inference_cuda_device_vector_ops_driver_symbol( + model, + "cuModuleUnload", + (void **) &cu_module_unload + ) + && cu_module_unload != NULL) { + (void) cu_module_unload(module); + } + return; + } + + model->cuda_device_vector_ops_module = module; + model->cuda_device_vector_ops_module_loaded = true; + model->cuda_vector_add_available = true; + model->cuda_vector_mul_available = true; + model->cuda_vector_scale_available = true; + model->cuda_vector_silu_available = true; + model->cuda_vector_slice_available = true; + model->cuda_vector_copy_to_offset_available = true; + model->cuda_device_vector_ops_available = true; + model->cuda_device_vector_ops_result = 0; + model->cuda_device_vector_ops_error[0] = '\0'; +} + +static zend_result king_inference_cuda_device_vector_ops_launch( + king_inference_model_object *model, + void *function, + void **params, + zend_ulong length +) { + king_inference_cuda_launch_kernel_fn cu_launch_kernel; + static king_inference_cuda_launch_kernel_fn cached_cu_launch_kernel = NULL; + unsigned int block_x = 256; + unsigned int grid_x; + int result; + + if (!model->cuda_device_vector_ops_available || function == NULL) { + king_inference_cuda_device_vector_ops_set_error(model, -1, "cuda_device_vector_ops_unavailable"); + return FAILURE; + } + if (length == 0 || length > UINT_MAX) { + king_inference_cuda_device_vector_ops_set_error(model, -1, "cuda_device_vector_ops_shape_invalid"); + return FAILURE; + } + if (cached_cu_launch_kernel == NULL + && !king_inference_cuda_device_vector_ops_driver_symbol(model, "cuLaunchKernel", (void **) &cached_cu_launch_kernel)) { + return FAILURE; + } + cu_launch_kernel = cached_cu_launch_kernel; + + grid_x = (((unsigned int) length) + block_x - 1) / block_x; + result = cu_launch_kernel(function, grid_x, 1, 1, block_x, 1, 1, 0, NULL, params, NULL); + if (result != 0) { + king_inference_cuda_device_vector_ops_set_error(model, result, "cuLaunchKernel_failed"); + return FAILURE; + } + model->cuda_device_vector_ops_launch_count++; + model->cuda_device_vector_ops_last_length = length; + model->cuda_device_vector_ops_result = 0; + model->cuda_device_vector_ops_error[0] = '\0'; + return SUCCESS; +} + +static zend_result king_inference_cuda_vector_binary( + king_inference_model_object *model, + void *function, + king_inference_cuda_device_ptr left, + king_inference_cuda_device_ptr right, + king_inference_cuda_device_ptr output, + zend_ulong length +) { + unsigned int length32 = (unsigned int) length; + void *params[4]; + + if (left == 0 || right == 0 || output == 0) { + king_inference_cuda_device_vector_ops_set_error(model, -1, "cuda_vector_argument_invalid"); + return FAILURE; + } + params[0] = &left; + params[1] = &right; + params[2] = &output; + params[3] = &length32; + return king_inference_cuda_device_vector_ops_launch(model, function, params, length); +} + +static zend_result king_inference_cuda_vector_add( + king_inference_model_object *model, + king_inference_cuda_device_ptr left, + king_inference_cuda_device_ptr right, + king_inference_cuda_device_ptr output, + zend_ulong length +) { + return king_inference_cuda_vector_binary(model, model->cuda_vector_add_function, left, right, output, length); +} + +static zend_result king_inference_cuda_vector_mul( + king_inference_model_object *model, + king_inference_cuda_device_ptr left, + king_inference_cuda_device_ptr right, + king_inference_cuda_device_ptr output, + zend_ulong length +) { + return king_inference_cuda_vector_binary(model, model->cuda_vector_mul_function, left, right, output, length); +} + +static zend_result king_inference_cuda_vector_scale( + king_inference_model_object *model, + king_inference_cuda_device_ptr input, + double scale, + king_inference_cuda_device_ptr output, + zend_ulong length +) { + float scale32 = (float) scale; + unsigned int length32 = (unsigned int) length; + void *params[4]; + + if (input == 0 || output == 0 || !isfinite(scale)) { + king_inference_cuda_device_vector_ops_set_error(model, -1, "cuda_vector_scale_argument_invalid"); + return FAILURE; + } + params[0] = &input; + params[1] = &scale32; + params[2] = &output; + params[3] = &length32; + return king_inference_cuda_device_vector_ops_launch( + model, + model->cuda_vector_scale_function, + params, + length + ); +} + +static zend_result king_inference_cuda_vector_silu( + king_inference_model_object *model, + king_inference_cuda_device_ptr input, + king_inference_cuda_device_ptr output, + zend_ulong length +) { + unsigned int length32 = (unsigned int) length; + void *params[3]; + + if (input == 0 || output == 0) { + king_inference_cuda_device_vector_ops_set_error(model, -1, "cuda_vector_silu_argument_invalid"); + return FAILURE; + } + params[0] = &input; + params[1] = &output; + params[2] = &length32; + return king_inference_cuda_device_vector_ops_launch(model, model->cuda_vector_silu_function, params, length); +} + +static zend_result king_inference_cuda_vector_slice( + king_inference_model_object *model, + king_inference_cuda_device_ptr input, + zend_ulong offset, + king_inference_cuda_device_ptr output, + zend_ulong length +) { + unsigned int offset32; + unsigned int length32 = (unsigned int) length; + void *params[4]; + + if (input == 0 || output == 0 || offset > UINT_MAX) { + king_inference_cuda_device_vector_ops_set_error(model, -1, "cuda_vector_slice_argument_invalid"); + return FAILURE; + } + offset32 = (unsigned int) offset; + params[0] = &input; + params[1] = &offset32; + params[2] = &output; + params[3] = &length32; + return king_inference_cuda_device_vector_ops_launch(model, model->cuda_vector_slice_function, params, length); +} + +static zend_result king_inference_cuda_vector_copy_to_offset( + king_inference_model_object *model, + king_inference_cuda_device_ptr input, + king_inference_cuda_device_ptr output, + zend_ulong output_offset, + zend_ulong length +) { + unsigned int output_offset32; + unsigned int length32 = (unsigned int) length; + void *params[4]; + + if (input == 0 || output == 0 || output_offset > UINT_MAX) { + king_inference_cuda_device_vector_ops_set_error( + model, + -1, + "cuda_vector_copy_to_offset_argument_invalid" + ); + return FAILURE; + } + output_offset32 = (unsigned int) output_offset; + params[0] = &input; + params[1] = &output; + params[2] = &output_offset32; + params[3] = &length32; + return king_inference_cuda_device_vector_ops_launch( + model, + model->cuda_vector_copy_to_offset_function, + params, + length + ); +} + +static void king_inference_cuda_device_vector_ops_add_status( + king_inference_model_object *model, + zval *return_value +) { + zval ops; + zval supported_ops; + zval compare_hook; + + array_init(&ops); + add_assoc_bool(&ops, "attempted", model->cuda_device_vector_ops_attempted); + add_assoc_bool(&ops, "available", model->cuda_device_vector_ops_available); + add_assoc_bool(&ops, "nvrtc_available", model->cuda_device_vector_ops_nvrtc_available); + add_assoc_bool(&ops, "module_loaded", model->cuda_device_vector_ops_module_loaded); + add_assoc_bool(&ops, "add_available", model->cuda_vector_add_available); + add_assoc_bool(&ops, "mul_available", model->cuda_vector_mul_available); + add_assoc_bool(&ops, "scale_available", model->cuda_vector_scale_available); + add_assoc_bool(&ops, "silu_available", model->cuda_vector_silu_available); + add_assoc_bool(&ops, "slice_available", model->cuda_vector_slice_available); + add_assoc_bool(&ops, "copy_to_offset_available", model->cuda_vector_copy_to_offset_available); + add_assoc_long(&ops, "launches", (zend_long) model->cuda_device_vector_ops_launch_count); + add_assoc_long(&ops, "last_length", (zend_long) model->cuda_device_vector_ops_last_length); + add_assoc_long(&ops, "result", model->cuda_device_vector_ops_result); + add_assoc_string(&ops, "error", model->cuda_device_vector_ops_error); + array_init(&supported_ops); + add_next_index_string(&supported_ops, "add"); + add_next_index_string(&supported_ops, "mul"); + add_next_index_string(&supported_ops, "scale"); + add_next_index_string(&supported_ops, "silu"); + add_next_index_string(&supported_ops, "slice"); + add_next_index_string(&supported_ops, "copy_to_offset"); + add_assoc_zval(&ops, "supported_ops", &supported_ops); + king_inference_cuda_device_vector_numeric_compare_status(model, &compare_hook); + add_assoc_zval(&ops, "numeric_compare_hook", &compare_hook); + add_assoc_zval(return_value, "device_vector_ops", &ops); + add_assoc_bool(return_value, "gpu_device_vector_ops_ready", model->cuda_device_vector_ops_available); +} + +static void king_inference_cuda_device_vector_ops_apply_status( + king_inference_model_object *model, + zval *return_value +) { + zval *reason; + bool reason_can_be_vector_ops; + + king_inference_cuda_device_vector_ops_add_status(model, return_value); + if (!model->cuda_device_vector_ops_attempted + || model->cuda_device_vector_ops_available + || !model->cuda_device_allocator_available) { + return; + } + + reason = zend_hash_str_find(Z_ARRVAL_P(return_value), "reason", sizeof("reason") - 1); + reason_can_be_vector_ops = reason == NULL + || Z_TYPE_P(reason) != IS_STRING + || zend_string_equals_literal(Z_STR_P(reason), "gpu_ready") + || zend_string_equals_literal(Z_STR_P(reason), "gpu_decoder_kernel_not_implemented"); + if (reason_can_be_vector_ops) { + king_inference_cuda_context_prepend_return_reason( + return_value, + "gpu_device_vector_ops_unavailable" + ); + add_assoc_string(return_value, "reason", "gpu_device_vector_ops_unavailable"); + } else { + king_inference_gpu_status_append_return_reason(return_value, "gpu_device_vector_ops_unavailable"); + } +} diff --git a/extension/src/inference/cuda/kernels/device/cuda_numeric_tolerance_contract.inc b/extension/src/inference/cuda/kernels/device/cuda_numeric_tolerance_contract.inc new file mode 100644 index 000000000..8d1b9646f --- /dev/null +++ b/extension/src/inference/cuda/kernels/device/cuda_numeric_tolerance_contract.inc @@ -0,0 +1,137 @@ +/* + * Internal CUDA numeric tolerance contract for inference diagnostics. + */ + +static double king_inference_cuda_numeric_tolerance_relative(const char *op) +{ + if (op == NULL) { + return 0.00001; + } + if (strcmp(op, "embedding") == 0) { + return 0.00001; + } + if (strcmp(op, "rms_norm") == 0 + || strcmp(op, "ffn_norm") == 0 + || strcmp(op, "final_norm") == 0) { + return 0.00005; + } + if (strcmp(op, "matvec") == 0 + || strcmp(op, "qkv_projection") == 0) { + return 0.00005; + } + if (strcmp(op, "rope") == 0) { + return 0.0000015; + } + if (strcmp(op, "attention_score") == 0) { + return 0.00005; + } + if (strcmp(op, "attention_softmax") == 0) { + return 0.000001; + } + if (strcmp(op, "attention_value") == 0) { + return 0.00002; + } + if (strcmp(op, "ffn") == 0 + || strcmp(op, "ffn_gate_up") == 0 + || strcmp(op, "ffn_swiglu") == 0 + || strcmp(op, "ffn_down") == 0) { + return 0.00005; + } + if (strcmp(op, "logits_projection") == 0 + || strcmp(op, "sampler_candidate_ranking") == 0) { + return 0.0002; + } + return 0.00001; +} + +static const char *king_inference_cuda_numeric_tolerance_top_k_rule(const char *op) +{ + if (op != NULL + && (strcmp(op, "logits_projection") == 0 + || strcmp(op, "sampler_candidate_ranking") == 0)) { + return "candidate_token_id_and_rank_logit_must_match_with_token_id_tie_break"; + } + return "not_applicable"; +} + +static double king_inference_cuda_numeric_relative_diff(double actual, double expected) +{ + double denominator = fmax(fabs(actual), fabs(expected)); + + if (denominator == 0.0) { + return actual == expected ? 0.0 : 1.0e308; + } + return fabs(actual - expected) / denominator; +} + +static const char *king_inference_cuda_numeric_sample_classification( + double actual, + double expected, + double abs_diff, + double rel_diff, + double max_abs, + double max_rel +) { + if (!isfinite(actual) || !isfinite(expected) || !isfinite(abs_diff)) { + return "non_finite_numeric_mismatch"; + } + if (abs_diff <= max_abs) { + return "float_noise_abs_tolerance"; + } + if (isfinite(rel_diff) && rel_diff <= max_rel) { + return "float_noise_relative_tolerance"; + } + return "numeric_mismatch"; +} + +static bool king_inference_cuda_numeric_sample_matches(const char *classification) +{ + return classification != NULL + && (strcmp(classification, "float_noise_abs_tolerance") == 0 + || strcmp(classification, "float_noise_relative_tolerance") == 0); +} + +static void king_inference_cuda_numeric_tolerance_contract_add_entry( + zval *entries, + const char *op, + double max_abs, + double max_rel, + const char *scope +) { + zval entry; + + array_init(&entry); + add_assoc_string(&entry, "op", op); + add_assoc_double(&entry, "max_abs_diff", max_abs); + add_assoc_double(&entry, "max_relative_diff", max_rel); + add_assoc_string(&entry, "match_rule", "abs_or_relative"); + add_assoc_string(&entry, "top_k_equivalence", king_inference_cuda_numeric_tolerance_top_k_rule(op)); + add_assoc_string(&entry, "scope", scope); + add_next_index_zval(entries, &entry); +} + +static void king_inference_cuda_numeric_tolerance_contract_table(zval *return_value) +{ + zval entries; + + array_init(return_value); + add_assoc_long(return_value, "schema_version", 1); + add_assoc_string(return_value, "match_rule", "abs_or_relative"); + add_assoc_string(return_value, "noise_status", "float_noise_abs_tolerance_or_float_noise_relative_tolerance"); + add_assoc_string(return_value, "failure_status", "numeric_mismatch"); + add_assoc_string(return_value, "sample_context_fields", "op,layer,tensor,sample_index"); + array_init(&entries); + king_inference_cuda_numeric_tolerance_contract_add_entry(&entries, "embedding", 0.00001, 0.00001, "token_embedding_row"); + king_inference_cuda_numeric_tolerance_contract_add_entry(&entries, "rms_norm", 0.0002, 0.00005, "rms_norm_and_first_norm"); + king_inference_cuda_numeric_tolerance_contract_add_entry(&entries, "matvec", 0.003, 0.00005, "quantized_matvec"); + king_inference_cuda_numeric_tolerance_contract_add_entry(&entries, "qkv_projection", 0.003, 0.00005, "attention_qkv_projection"); + king_inference_cuda_numeric_tolerance_contract_add_entry(&entries, "rope", 0.0005, 0.0000015, "rope_rotation"); + king_inference_cuda_numeric_tolerance_contract_add_entry(&entries, "attention_score", 0.00005, 0.00005, "qk_score"); + king_inference_cuda_numeric_tolerance_contract_add_entry(&entries, "attention_softmax", 0.000001, 0.000001, "attention_probability"); + king_inference_cuda_numeric_tolerance_contract_add_entry(&entries, "attention_value", 0.00002, 0.00002, "weighted_value_context"); + king_inference_cuda_numeric_tolerance_contract_add_entry(&entries, "ffn", 0.003, 0.00005, "ffn_gate_up_swiglu_down"); + king_inference_cuda_numeric_tolerance_contract_add_entry(&entries, "final_norm", 0.0002, 0.00005, "final_rms_norm"); + king_inference_cuda_numeric_tolerance_contract_add_entry(&entries, "logits_projection", 0.01, 0.0002, "bounded_logits"); + king_inference_cuda_numeric_tolerance_contract_add_entry(&entries, "sampler_candidate_ranking", 0.01, 0.0002, "top_k_candidate_order"); + add_assoc_zval(return_value, "operations", &entries); +} diff --git a/extension/src/inference/cuda/kernels/embedding/cuda_embedding_prefill_copy.inc b/extension/src/inference/cuda/kernels/embedding/cuda_embedding_prefill_copy.inc new file mode 100644 index 000000000..e18446265 --- /dev/null +++ b/extension/src/inference/cuda/kernels/embedding/cuda_embedding_prefill_copy.inc @@ -0,0 +1,163 @@ +/* + * CUDA prefilled embedding row copy bridge for decoder prompt prefill. + */ + +static zend_result king_inference_cuda_embedding_prefill_copy_row( + king_inference_model_object *model, + zend_ulong row, + king_inference_cuda_device_ptr output_vector, + zend_ulong output_width +) { + king_inference_cuda_device_kv_cache_memcpy_dtod_fn cu_memcpy_dtod; + static king_inference_cuda_device_kv_cache_memcpy_dtod_fn cached_cu_memcpy_dtod = NULL; + king_inference_cuda_device_ptr source; + size_t offset; + size_t bytes; + int result; + + if (!model->cuda_decoder_prompt_prefill_embeddings_active + || model->cuda_decoder_prompt_prefill_embeddings == 0 + || row >= model->cuda_decoder_prompt_prefill_tokens + || output_vector == 0 + || output_width == 0 + || output_width != model->cuda_decoder_prompt_prefill_width + || output_width > (zend_ulong) (SIZE_MAX / sizeof(float))) { + king_inference_cuda_embedding_row_set_error(model, -1, "cuda_embedding_prefill_copy_argument_invalid"); + return FAILURE; + } + bytes = (size_t) output_width * sizeof(float); + if (row > (zend_ulong) (SIZE_MAX / bytes)) { + king_inference_cuda_embedding_row_set_error(model, -1, "cuda_embedding_prefill_copy_offset_invalid"); + return FAILURE; + } + offset = (size_t) row * bytes; + source = model->cuda_decoder_prompt_prefill_embeddings + (king_inference_cuda_device_ptr) offset; + if (cached_cu_memcpy_dtod == NULL + && !king_inference_cuda_device_kv_cache_symbol( + model, + "cuMemcpyDtoD_v2", + "cuMemcpyDtoD", + (void **) &cached_cu_memcpy_dtod + )) { + king_inference_cuda_embedding_row_set_error( + model, + model->cuda_device_kv_cache_result, + model->cuda_device_kv_cache_error[0] != '\0' + ? model->cuda_device_kv_cache_error + : "cuda_embedding_prefill_copy_symbol_unavailable" + ); + return FAILURE; + } + cu_memcpy_dtod = cached_cu_memcpy_dtod; + result = cu_memcpy_dtod(output_vector, source, bytes); + if (result != 0) { + king_inference_cuda_embedding_row_set_error(model, result, "cuMemcpyDtoD_failed"); + return FAILURE; + } + return SUCCESS; +} + +static zend_result king_inference_cuda_prefill_copy_matrix_row( + king_inference_model_object *model, + king_inference_cuda_device_ptr matrix, + zend_ulong row, + zend_ulong rows, + zend_ulong width, + king_inference_cuda_device_ptr output_vector, + const char *error_prefix +) { + king_inference_cuda_device_kv_cache_memcpy_dtod_fn cu_memcpy_dtod; + static king_inference_cuda_device_kv_cache_memcpy_dtod_fn cached_cu_memcpy_dtod = NULL; + king_inference_cuda_device_ptr source; + size_t offset; + size_t bytes; + int result; + + if (matrix == 0 || output_vector == 0 || row >= rows || width == 0 + || width > (zend_ulong) (SIZE_MAX / sizeof(float))) { + king_inference_cuda_embedding_row_set_error(model, -1, error_prefix); + return FAILURE; + } + bytes = (size_t) width * sizeof(float); + if (row > (zend_ulong) (SIZE_MAX / bytes)) { + king_inference_cuda_embedding_row_set_error(model, -1, "cuda_prefill_copy_offset_invalid"); + return FAILURE; + } + offset = (size_t) row * bytes; + source = matrix + (king_inference_cuda_device_ptr) offset; + if (cached_cu_memcpy_dtod == NULL + && !king_inference_cuda_device_kv_cache_symbol( + model, + "cuMemcpyDtoD_v2", + "cuMemcpyDtoD", + (void **) &cached_cu_memcpy_dtod + )) { + king_inference_cuda_embedding_row_set_error( + model, + model->cuda_device_kv_cache_result, + model->cuda_device_kv_cache_error[0] != '\0' + ? model->cuda_device_kv_cache_error + : "cuda_prefill_copy_symbol_unavailable" + ); + return FAILURE; + } + cu_memcpy_dtod = cached_cu_memcpy_dtod; + result = cu_memcpy_dtod(output_vector, source, bytes); + if (result != 0) { + king_inference_cuda_embedding_row_set_error(model, result, "cuda_prefill_copy_failed"); + return FAILURE; + } + return SUCCESS; +} + +static zend_result king_inference_cuda_prefill_copy_initial_norm_row( + king_inference_model_object *model, + zend_ulong row, + king_inference_cuda_device_ptr output_vector, + zend_ulong output_width +) { + if (!model->cuda_decoder_prompt_prefill_embeddings_active + || output_width != model->cuda_decoder_prompt_prefill_width) { + king_inference_cuda_embedding_row_set_error( + model, + -1, + "cuda_prefill_initial_norm_copy_argument_invalid" + ); + return FAILURE; + } + return king_inference_cuda_prefill_copy_matrix_row( + model, + model->cuda_decoder_prompt_prefill_initial_norm, + row, + model->cuda_decoder_prompt_prefill_tokens, + output_width, + output_vector, + "cuda_prefill_initial_norm_copy_argument_invalid" + ); +} + +static zend_result king_inference_cuda_prefill_copy_initial_linear_row( + king_inference_model_object *model, + zend_ulong row, + king_inference_cuda_device_ptr output_vector, + zend_ulong output_rows +) { + if (!model->cuda_decoder_prompt_prefill_embeddings_active + || output_rows != model->cuda_decoder_prompt_prefill_initial_linear_rows) { + king_inference_cuda_embedding_row_set_error( + model, + -1, + "cuda_prefill_initial_linear_copy_argument_invalid" + ); + return FAILURE; + } + return king_inference_cuda_prefill_copy_matrix_row( + model, + model->cuda_decoder_prompt_prefill_initial_linear, + row, + model->cuda_decoder_prompt_prefill_tokens, + output_rows, + output_vector, + "cuda_prefill_initial_linear_copy_argument_invalid" + ); +} diff --git a/extension/src/inference/cuda/kernels/embedding/cuda_embedding_row.inc b/extension/src/inference/cuda/kernels/embedding/cuda_embedding_row.inc new file mode 100644 index 000000000..1fb4890d5 --- /dev/null +++ b/extension/src/inference/cuda/kernels/embedding/cuda_embedding_row.inc @@ -0,0 +1,653 @@ +/* + * CUDA token embedding row loader for native King GPU inference. + */ + +static const char king_inference_cuda_embedding_row_source[] = +"extern \"C\" __device__ float king_embedding_half_to_float(unsigned short h) {\n" +" unsigned int sign = ((unsigned int) (h & 0x8000u)) << 16;\n" +" unsigned int mantissa = (unsigned int) (h & 0x03ffu);\n" +" int exponent = (int) ((h >> 10) & 0x1fu);\n" +" unsigned int bits;\n" +" if (exponent == 0) {\n" +" if (mantissa == 0u) {\n" +" bits = sign;\n" +" } else {\n" +" while ((mantissa & 0x0400u) == 0u) {\n" +" mantissa <<= 1;\n" +" exponent--;\n" +" }\n" +" exponent++;\n" +" mantissa &= 0x03ffu;\n" +" bits = sign | ((unsigned int) (exponent + 112) << 23) | (mantissa << 13);\n" +" }\n" +" return __uint_as_float(bits);\n" +" }\n" +" if (exponent == 31) {\n" +" bits = sign | 0x7f800000u | (mantissa << 13);\n" +" return __uint_as_float(bits);\n" +" }\n" +" bits = sign | ((unsigned int) (exponent + 112) << 23) | (mantissa << 13);\n" +" return __uint_as_float(bits);\n" +"}\n" +"extern \"C\" __device__ float king_embedding_bfloat16_to_float(unsigned short h) {\n" +" unsigned int bits = ((unsigned int) h) << 16;\n" +" return __uint_as_float(bits);\n" +"}\n" +"extern \"C\" __global__ void king_embedding_rows_load(\n" +" const unsigned char *embedding,\n" +" const unsigned int *token_ids,\n" +" float *output,\n" +" unsigned int width,\n" +" unsigned int rows,\n" +" unsigned int batch_tokens,\n" +" unsigned int tensor_type,\n" +" float output_scale\n" +") {\n" +" unsigned int col = blockIdx.x * blockDim.x + threadIdx.x;\n" +" unsigned int batch = blockIdx.y;\n" +" unsigned int token_id;\n" +" unsigned long long source_offset;\n" +" unsigned long long output_offset;\n" +" if (embedding == 0 || token_ids == 0 || output == 0 || col >= width || batch >= batch_tokens) {\n" +" return;\n" +" }\n" +" token_id = token_ids[batch];\n" +" if (token_id >= rows) {\n" +" return;\n" +" }\n" +" source_offset = ((unsigned long long) token_id * (unsigned long long) width) + col;\n" +" output_offset = ((unsigned long long) batch * (unsigned long long) width) + col;\n" +" if (tensor_type == 0u) {\n" +" output[output_offset] = ((const float *) embedding)[source_offset];\n" +" } else if (tensor_type == 1u) {\n" +" output[output_offset] = king_embedding_half_to_float(((const unsigned short *) embedding)[source_offset]);\n" +" } else if (tensor_type == 30u) {\n" +" output[output_offset] = king_embedding_bfloat16_to_float(((const unsigned short *) embedding)[source_offset]);\n" +" } else if (tensor_type == 8u) {\n" +" unsigned int blocks_per_row = (width + 31u) >> 5;\n" +" const unsigned char *qblock = embedding\n" +" + ((((unsigned long long) token_id * (unsigned long long) blocks_per_row) + (col >> 5)) * 34ull);\n" +" unsigned short raw_scale = (unsigned short) qblock[0] | ((unsigned short) qblock[1] << 8);\n" +" signed char quant = ((const signed char *) (qblock + 2))[col & 31u];\n" +" output[output_offset] = (float) quant * king_embedding_half_to_float(raw_scale);\n" +" } else if (tensor_type == 14u) {\n" +" unsigned int blocks_per_row = (width + 255u) >> 8;\n" +" const unsigned char *qblock = embedding\n" +" + ((((unsigned long long) token_id * (unsigned long long) blocks_per_row) + (col >> 8)) * 210ull);\n" +" unsigned int in_block = col & 255u;\n" +" unsigned int chunk = in_block >> 7;\n" +" unsigned int within = in_block & 127u;\n" +" unsigned int lane = within & 31u;\n" +" unsigned int quadrant = within >> 5;\n" +" const unsigned char *ql = qblock + (chunk * 64u);\n" +" const unsigned char *qh = qblock + 128u + (chunk * 32u);\n" +" const signed char *scales = (const signed char *) (qblock + 192u + (chunk * 8u));\n" +" unsigned short raw_scale = (unsigned short) qblock[208] | ((unsigned short) qblock[209] << 8);\n" +" int quant;\n" +" int scale;\n" +" if (quadrant == 0u) {\n" +" quant = (int) ((ql[lane] & 0x0fu) | ((qh[lane] & 0x03u) << 4));\n" +" scale = (int) scales[lane >> 4];\n" +" } else if (quadrant == 1u) {\n" +" quant = (int) ((ql[lane + 32u] & 0x0fu) | ((qh[lane] & 0x0cu) << 2));\n" +" scale = (int) scales[2u + (lane >> 4)];\n" +" } else if (quadrant == 2u) {\n" +" quant = (int) ((ql[lane] >> 4) | (qh[lane] & 0x30u));\n" +" scale = (int) scales[4u + (lane >> 4)];\n" +" } else {\n" +" quant = (int) ((ql[lane + 32u] >> 4) | ((qh[lane] & 0xc0u) >> 2));\n" +" scale = (int) scales[6u + (lane >> 4)];\n" +" }\n" +" output[output_offset] = king_embedding_half_to_float(raw_scale) * (float) scale * (float) (quant - 32);\n" +" } else {\n" +" output[output_offset] = 0.0f;\n" +" }\n" +" output[output_offset] *= output_scale;\n" +"}\n" +"extern \"C\" __global__ void king_embedding_row_load(\n" +" const unsigned char *embedding,\n" +" float *output,\n" +" unsigned int width,\n" +" unsigned int rows,\n" +" unsigned int token_id,\n" +" unsigned int tensor_type,\n" +" float output_scale\n" +") {\n" +" unsigned int col = blockIdx.x * blockDim.x + threadIdx.x;\n" +" if (embedding == 0 || output == 0 || col >= width || token_id >= rows) {\n" +" return;\n" +" }\n" +" unsigned long long offset = ((unsigned long long) token_id * (unsigned long long) width) + col;\n" +" if (tensor_type == 0u) {\n" +" output[col] = ((const float *) embedding)[offset];\n" +" } else if (tensor_type == 1u) {\n" +" output[col] = king_embedding_half_to_float(((const unsigned short *) embedding)[offset]);\n" +" } else if (tensor_type == 30u) {\n" +" output[col] = king_embedding_bfloat16_to_float(((const unsigned short *) embedding)[offset]);\n" +" } else if (tensor_type == 8u) {\n" +" unsigned int blocks_per_row = (width + 31u) >> 5;\n" +" const unsigned char *qblock = embedding\n" +" + ((((unsigned long long) token_id * (unsigned long long) blocks_per_row) + (col >> 5)) * 34ull);\n" +" unsigned short raw_scale = (unsigned short) qblock[0] | ((unsigned short) qblock[1] << 8);\n" +" signed char quant = ((const signed char *) (qblock + 2))[col & 31u];\n" +" output[col] = (float) quant * king_embedding_half_to_float(raw_scale);\n" +" } else if (tensor_type == 14u) {\n" +" unsigned int blocks_per_row = (width + 255u) >> 8;\n" +" const unsigned char *qblock = embedding\n" +" + ((((unsigned long long) token_id * (unsigned long long) blocks_per_row) + (col >> 8)) * 210ull);\n" +" unsigned int in_block = col & 255u;\n" +" unsigned int chunk = in_block >> 7;\n" +" unsigned int within = in_block & 127u;\n" +" unsigned int lane = within & 31u;\n" +" unsigned int quadrant = within >> 5;\n" +" const unsigned char *ql = qblock + (chunk * 64u);\n" +" const unsigned char *qh = qblock + 128u + (chunk * 32u);\n" +" const signed char *scales = (const signed char *) (qblock + 192u + (chunk * 8u));\n" +" unsigned short raw_scale = (unsigned short) qblock[208] | ((unsigned short) qblock[209] << 8);\n" +" int quant;\n" +" int scale;\n" +" if (quadrant == 0u) {\n" +" quant = (int) ((ql[lane] & 0x0fu) | ((qh[lane] & 0x03u) << 4));\n" +" scale = (int) scales[lane >> 4];\n" +" } else if (quadrant == 1u) {\n" +" quant = (int) ((ql[lane + 32u] & 0x0fu) | ((qh[lane] & 0x0cu) << 2));\n" +" scale = (int) scales[2u + (lane >> 4)];\n" +" } else if (quadrant == 2u) {\n" +" quant = (int) ((ql[lane] >> 4) | (qh[lane] & 0x30u));\n" +" scale = (int) scales[4u + (lane >> 4)];\n" +" } else {\n" +" quant = (int) ((ql[lane + 32u] >> 4) | ((qh[lane] & 0xc0u) >> 2));\n" +" scale = (int) scales[6u + (lane >> 4)];\n" +" }\n" +" output[col] = king_embedding_half_to_float(raw_scale) * (float) scale * (float) (quant - 32);\n" +" } else {\n" +" output[col] = 0.0f;\n" +" }\n" +" output[col] *= output_scale;\n" +"}\n"; + +static void king_inference_cuda_embedding_row_reset_fields(king_inference_model_object *model) +{ + model->cuda_embedding_row_nvrtc_handle = NULL; + model->cuda_embedding_row_module = NULL; + model->cuda_embedding_row_function = NULL; + model->cuda_embedding_rows_function = NULL; + model->cuda_embedding_row_result = 0; + model->cuda_embedding_row_error[0] = '\0'; + model->cuda_embedding_row_launch_count = 0; + model->cuda_embedding_rows_launch_count = 0; + model->cuda_embedding_row_last_token_id = 0; + model->cuda_embedding_row_last_width = 0; + model->cuda_embedding_rows_last_batch_tokens = 0; + model->cuda_embedding_rows_last_width = 0; + model->cuda_embedding_row_attempted = false; + model->cuda_embedding_row_available = false; + model->cuda_embedding_row_nvrtc_available = false; + model->cuda_embedding_row_module_loaded = false; + model->cuda_embedding_row_f32_available = false; + model->cuda_embedding_row_f16_available = false; + model->cuda_embedding_row_bf16_available = false; + model->cuda_embedding_row_q8_0_available = false; + model->cuda_embedding_row_q6_k_available = false; + model->cuda_embedding_rows_available = false; +} + +static void king_inference_cuda_embedding_row_set_error( + king_inference_model_object *model, + int result, + const char *message +) { + if (message == NULL || message[0] == '\0') { + message = "unknown_cuda_embedding_row_error"; + } + + model->cuda_embedding_row_result = result; + snprintf(model->cuda_embedding_row_error, sizeof(model->cuda_embedding_row_error), "%s", message); +} + +static bool king_inference_cuda_embedding_row_driver_symbol( + king_inference_model_object *model, + const char *name, + void **symbol +) { + *symbol = NULL; + if (model->cuda_driver_handle == NULL) { + king_inference_cuda_embedding_row_set_error(model, -1, "cuda_driver_handle_unavailable"); + return false; + } + + *symbol = dlsym(model->cuda_driver_handle, name); + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_embedding_row_set_error(model, -1, name); + return false; +} + +static bool king_inference_cuda_embedding_row_nvrtc_symbol( + king_inference_model_object *model, + const char *name, + void **symbol +) { + *symbol = NULL; + if (model->cuda_embedding_row_nvrtc_handle == NULL) { + king_inference_cuda_embedding_row_set_error(model, -1, "nvrtc_handle_unavailable"); + return false; + } + + *symbol = dlsym(model->cuda_embedding_row_nvrtc_handle, name); + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_embedding_row_set_error(model, -1, name); + return false; +} + +static bool king_inference_cuda_embedding_row_open_nvrtc(king_inference_model_object *model) +{ + const char *libraries[] = { + "libnvrtc.so", + "libnvrtc.so.12", + "libnvrtc.so.11", + NULL + }; + size_t index; + + for (index = 0; libraries[index] != NULL; index++) { + model->cuda_embedding_row_nvrtc_handle = dlopen(libraries[index], RTLD_LAZY | RTLD_LOCAL); + if (model->cuda_embedding_row_nvrtc_handle != NULL) { + model->cuda_embedding_row_nvrtc_available = true; + return true; + } + } + + king_inference_cuda_embedding_row_set_error(model, -1, "libnvrtc_unavailable"); + return false; +} + +static bool king_inference_cuda_embedding_row_compile_ptx( + king_inference_model_object *model, + char **ptx_out, + size_t *ptx_size_out +) { + king_inference_nvrtc_create_program_fn nvrtc_create_program; + king_inference_nvrtc_compile_program_fn nvrtc_compile_program; + king_inference_nvrtc_get_ptx_size_fn nvrtc_get_ptx_size; + king_inference_nvrtc_get_ptx_fn nvrtc_get_ptx; + king_inference_nvrtc_destroy_program_fn nvrtc_destroy_program; + king_inference_nvrtc_program program = NULL; + const char *options[] = {"--std=c++11", "--gpu-architecture=compute_89", "--use_fast_math"}; + char *ptx; + size_t ptx_size = 0; + king_inference_nvrtc_result result; + + *ptx_out = NULL; + *ptx_size_out = 0; + if (!king_inference_cuda_embedding_row_nvrtc_symbol(model, "nvrtcCreateProgram", (void **) &nvrtc_create_program) + || !king_inference_cuda_embedding_row_nvrtc_symbol(model, "nvrtcCompileProgram", (void **) &nvrtc_compile_program) + || !king_inference_cuda_embedding_row_nvrtc_symbol(model, "nvrtcGetPTXSize", (void **) &nvrtc_get_ptx_size) + || !king_inference_cuda_embedding_row_nvrtc_symbol(model, "nvrtcGetPTX", (void **) &nvrtc_get_ptx) + || !king_inference_cuda_embedding_row_nvrtc_symbol(model, "nvrtcDestroyProgram", (void **) &nvrtc_destroy_program)) { + return false; + } + + result = nvrtc_create_program( + &program, + king_inference_cuda_embedding_row_source, + "king_embedding_row.cu", + 0, + NULL, + NULL + ); + if (result != 0 || program == NULL) { + king_inference_cuda_embedding_row_set_error( + model, + result, + result != 0 ? "nvrtcCreateProgram_failed" : "nvrtc_program_unavailable" + ); + return false; + } + result = nvrtc_compile_program(program, 3, options); + if (result != 0) { + (void) nvrtc_destroy_program(&program); + king_inference_cuda_embedding_row_set_error(model, result, "nvrtcCompileProgram_failed"); + return false; + } + result = nvrtc_get_ptx_size(program, &ptx_size); + if (result != 0 || ptx_size == 0) { + (void) nvrtc_destroy_program(&program); + king_inference_cuda_embedding_row_set_error( + model, + result, + result != 0 ? "nvrtcGetPTXSize_failed" : "nvrtc_ptx_empty" + ); + return false; + } + ptx = emalloc(ptx_size); + result = nvrtc_get_ptx(program, ptx); + (void) nvrtc_destroy_program(&program); + if (result != 0) { + efree(ptx); + king_inference_cuda_embedding_row_set_error(model, result, "nvrtcGetPTX_failed"); + return false; + } + + *ptx_out = ptx; + *ptx_size_out = ptx_size; + return true; +} + +static void king_inference_cuda_embedding_row_release(king_inference_model_object *model) +{ + king_inference_cuda_module_unload_fn cu_module_unload; + + if (model->cuda_embedding_row_module != NULL + && model->cuda_driver_handle != NULL + && king_inference_cuda_device_allocator_set_current(model) + && king_inference_cuda_embedding_row_driver_symbol( + model, + "cuModuleUnload", + (void **) &cu_module_unload + )) { + (void) cu_module_unload(model->cuda_embedding_row_module); + } + if (model->cuda_embedding_row_nvrtc_handle != NULL) { + dlclose(model->cuda_embedding_row_nvrtc_handle); + } + + king_inference_cuda_embedding_row_reset_fields(model); +} + +static void king_inference_cuda_embedding_row_init( + king_inference_model_object *model, + king_inference_backend_kind backend_kind +) { + king_inference_cuda_module_load_data_fn cu_module_load_data; + king_inference_cuda_module_get_function_fn cu_module_get_function; + void *module = NULL; + void *function = NULL; + void *rows_function = NULL; + char *ptx = NULL; + size_t ptx_size = 0; + int result; + + king_inference_cuda_embedding_row_release(model); + if (backend_kind != KING_INFERENCE_BACKEND_KING_NATIVE_GPU + || !king_inference_gpu_enabled(&model->config)) { + return; + } + + model->cuda_embedding_row_attempted = true; + if (!model->cuda_weight_cache_ready) { + king_inference_cuda_embedding_row_set_error(model, -1, "cuda_weight_cache_unavailable"); + return; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + king_inference_cuda_embedding_row_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_context_current_failed" + ); + return; + } + if (!king_inference_cuda_embedding_row_open_nvrtc(model) + || !king_inference_cuda_embedding_row_compile_ptx(model, &ptx, &ptx_size)) { + return; + } + if (!king_inference_cuda_embedding_row_driver_symbol(model, "cuModuleLoadData", (void **) &cu_module_load_data) + || !king_inference_cuda_embedding_row_driver_symbol(model, "cuModuleGetFunction", (void **) &cu_module_get_function)) { + efree(ptx); + return; + } + + result = cu_module_load_data(&module, ptx); + efree(ptx); + if (result != 0 || module == NULL) { + king_inference_cuda_embedding_row_set_error( + model, + result, + result != 0 ? "cuModuleLoadData_failed" : "cuda_module_unavailable" + ); + return; + } + + result = cu_module_get_function(&function, module, "king_embedding_row_load"); + if (result == 0 && function != NULL) { + result = cu_module_get_function(&rows_function, module, "king_embedding_rows_load"); + } + if (result != 0 || function == NULL || rows_function == NULL) { + king_inference_cuda_module_unload_fn cu_module_unload = NULL; + + if (king_inference_cuda_embedding_row_driver_symbol( + model, + "cuModuleUnload", + (void **) &cu_module_unload + ) + && cu_module_unload != NULL) { + (void) cu_module_unload(module); + } + king_inference_cuda_embedding_row_set_error( + model, + result, + result != 0 ? "cuModuleGetFunction_failed" : "cuda_embedding_row_kernel_unavailable" + ); + return; + } + + model->cuda_embedding_row_module = module; + model->cuda_embedding_row_function = function; + model->cuda_embedding_rows_function = rows_function; + model->cuda_embedding_row_module_loaded = true; + model->cuda_embedding_row_f32_available = true; + model->cuda_embedding_row_f16_available = true; + model->cuda_embedding_row_bf16_available = true; + model->cuda_embedding_row_q8_0_available = true; + model->cuda_embedding_row_q6_k_available = true; + model->cuda_embedding_rows_available = true; + model->cuda_embedding_row_available = true; + model->cuda_embedding_row_result = 0; + model->cuda_embedding_row_error[0] = '\0'; +} + +static bool king_inference_cuda_embedding_row_supported_type(zend_ulong type) +{ + return type == 0 || type == 1 || type == 8 || type == 14 || type == 30; +} + +static zend_result king_inference_cuda_embedding_row_load( + king_inference_model_object *model, + zend_ulong token_id, + king_inference_cuda_device_ptr output_vector, + zend_ulong output_length, + double output_scale +) { + king_inference_cuda_weight_upload *upload; + king_inference_cuda_launch_kernel_fn cu_launch_kernel; + static king_inference_cuda_launch_kernel_fn cached_cu_launch_kernel = NULL; + zend_string *tensor = NULL; + const char *status = NULL; + zval *descriptor; + zval *dimensions; + zend_ulong type; + zend_ulong rank; + zend_ulong cols; + zend_ulong rows; + unsigned int width32; + unsigned int rows32; + unsigned int token32; + unsigned int type32; + float scale32; + unsigned int block_x = 256; + unsigned int grid_x; + void *params[7]; + int result; + + if (!model->cuda_embedding_row_available || model->cuda_embedding_row_function == NULL) { + king_inference_cuda_embedding_row_set_error(model, -1, "cuda_embedding_row_loader_unavailable"); + return FAILURE; + } + if (output_vector == 0 || output_length == 0 || output_length > UINT_MAX || token_id > UINT_MAX) { + king_inference_cuda_embedding_row_set_error(model, -1, "cuda_embedding_row_argument_invalid"); + return FAILURE; + } + if (king_inference_resolve_token_embedding_tensor_name(model, NULL, &tensor, NULL, &status) != SUCCESS + || tensor == NULL) { + king_inference_cuda_embedding_row_set_error( + model, + -1, + status != NULL ? status : "cuda_embedding_row_tensor_not_resolved" + ); + return FAILURE; + } + + descriptor = king_inference_tensor_index_descriptor(model, tensor); + dimensions = descriptor != NULL + ? zend_hash_str_find(Z_ARRVAL_P(descriptor), "dimensions", sizeof("dimensions") - 1) + : NULL; + if (descriptor == NULL + || dimensions == NULL + || Z_TYPE_P(dimensions) != IS_ARRAY + || !king_inference_tensor_descriptor_ulong(descriptor, "rank", sizeof("rank") - 1, &rank) + || !king_inference_tensor_descriptor_ulong(descriptor, "type", sizeof("type") - 1, &type) + || rank != 2 + || !king_inference_cuda_embedding_row_supported_type(type) + || king_inference_tensor_dimension_at(dimensions, 0, &cols) != SUCCESS + || king_inference_tensor_dimension_at(dimensions, 1, &rows) != SUCCESS + || cols != output_length + || cols == 0 + || cols > UINT_MAX + || rows == 0 + || rows > UINT_MAX + || token_id >= rows + || (type == 8 && (cols % 32) != 0) + || (type == 14 && (cols % 256) != 0)) { + zend_string_release(tensor); + king_inference_cuda_embedding_row_set_error(model, -1, "cuda_embedding_row_tensor_descriptor_invalid"); + return FAILURE; + } + + upload = king_inference_cuda_weight_cache_find(model, tensor, true); + if (upload == NULL || upload->device_ptr == 0) { + zend_string_release(tensor); + king_inference_cuda_embedding_row_set_error(model, -1, "cuda_embedding_row_weight_not_uploaded"); + return FAILURE; + } + zend_string_release(tensor); + if (cached_cu_launch_kernel == NULL + && !king_inference_cuda_embedding_row_driver_symbol(model, "cuLaunchKernel", (void **) &cached_cu_launch_kernel)) { + return FAILURE; + } + cu_launch_kernel = cached_cu_launch_kernel; + + width32 = (unsigned int) cols; + rows32 = (unsigned int) rows; + token32 = (unsigned int) token_id; + type32 = (unsigned int) type; + scale32 = (float) output_scale; + grid_x = (width32 + block_x - 1) / block_x; + params[0] = &upload->device_ptr; + params[1] = &output_vector; + params[2] = &width32; + params[3] = &rows32; + params[4] = &token32; + params[5] = &type32; + params[6] = &scale32; + + result = cu_launch_kernel( + model->cuda_embedding_row_function, + grid_x, + 1, + 1, + block_x, + 1, + 1, + 0, + NULL, + params, + NULL + ); + if (result != 0) { + king_inference_cuda_embedding_row_set_error(model, result, "cuLaunchKernel_failed"); + return FAILURE; + } + + model->cuda_embedding_row_launch_count++; + model->cuda_embedding_row_last_token_id = token_id; + model->cuda_embedding_row_last_width = output_length; + model->cuda_embedding_row_result = 0; + model->cuda_embedding_row_error[0] = '\0'; + return SUCCESS; +} + +#include "cuda_embedding_rows.inc" + +static void king_inference_cuda_embedding_row_add_status( + king_inference_model_object *model, + zval *return_value +) { + zval loader; + zval supported_types; + + array_init(&loader); + add_assoc_bool(&loader, "attempted", model->cuda_embedding_row_attempted); + add_assoc_bool(&loader, "available", model->cuda_embedding_row_available); + add_assoc_bool(&loader, "nvrtc_available", model->cuda_embedding_row_nvrtc_available); + add_assoc_bool(&loader, "module_loaded", model->cuda_embedding_row_module_loaded); + add_assoc_bool(&loader, "batch_available", model->cuda_embedding_rows_available); + add_assoc_bool(&loader, "f32_available", model->cuda_embedding_row_f32_available); + add_assoc_bool(&loader, "f16_available", model->cuda_embedding_row_f16_available); + add_assoc_bool(&loader, "bf16_available", model->cuda_embedding_row_bf16_available); + add_assoc_bool(&loader, "q8_0_available", model->cuda_embedding_row_q8_0_available); + add_assoc_bool(&loader, "q6_k_available", model->cuda_embedding_row_q6_k_available); + add_assoc_long(&loader, "launches", (zend_long) model->cuda_embedding_row_launch_count); + add_assoc_long(&loader, "batch_launches", (zend_long) model->cuda_embedding_rows_launch_count); + add_assoc_long(&loader, "last_token_id", (zend_long) model->cuda_embedding_row_last_token_id); + add_assoc_long(&loader, "last_width", (zend_long) model->cuda_embedding_row_last_width); + add_assoc_long(&loader, "last_batch_tokens", (zend_long) model->cuda_embedding_rows_last_batch_tokens); + add_assoc_long(&loader, "last_batch_width", (zend_long) model->cuda_embedding_rows_last_width); + add_assoc_long(&loader, "result", model->cuda_embedding_row_result); + add_assoc_string(&loader, "error", model->cuda_embedding_row_error); + array_init(&supported_types); + add_next_index_string(&supported_types, "F32"); + add_next_index_string(&supported_types, "F16"); + add_next_index_string(&supported_types, "BF16"); + add_next_index_string(&supported_types, "Q8_0"); + add_next_index_string(&supported_types, "Q6_K"); + add_assoc_zval(&loader, "supported_types", &supported_types); + add_assoc_zval(return_value, "embedding_row_loader", &loader); + add_assoc_bool(return_value, "gpu_embedding_row_loader_ready", model->cuda_embedding_row_available); +} + +static void king_inference_cuda_embedding_row_apply_status( + king_inference_model_object *model, + zval *return_value +) { + zval *reason; + bool reason_can_be_embedding_row; + + king_inference_cuda_embedding_row_add_status(model, return_value); + if (!model->cuda_embedding_row_attempted + || model->cuda_embedding_row_available + || !model->cuda_weight_upload_complete) { + return; + } + + reason = zend_hash_str_find(Z_ARRVAL_P(return_value), "reason", sizeof("reason") - 1); + reason_can_be_embedding_row = reason == NULL + || Z_TYPE_P(reason) != IS_STRING + || zend_string_equals_literal(Z_STR_P(reason), "gpu_ready") + || zend_string_equals_literal(Z_STR_P(reason), "gpu_decoder_kernel_not_implemented"); + if (reason_can_be_embedding_row) { + king_inference_cuda_context_prepend_return_reason( + return_value, + "gpu_embedding_row_loader_unavailable" + ); + add_assoc_string(return_value, "reason", "gpu_embedding_row_loader_unavailable"); + } else { + king_inference_gpu_status_append_return_reason(return_value, "gpu_embedding_row_loader_unavailable"); + } +} diff --git a/extension/src/inference/cuda/kernels/embedding/cuda_embedding_rows.inc b/extension/src/inference/cuda/kernels/embedding/cuda_embedding_rows.inc new file mode 100644 index 000000000..8c747cb52 --- /dev/null +++ b/extension/src/inference/cuda/kernels/embedding/cuda_embedding_rows.inc @@ -0,0 +1,231 @@ +/* + * CUDA batched token embedding row loader for native King GPU inference. + */ + +static zend_result king_inference_cuda_embedding_rows_descriptor( + king_inference_model_object *model, + zend_string **tensor_out, + king_inference_cuda_weight_upload **upload_out, + zend_ulong *width_out, + zend_ulong *rows_out, + zend_ulong *type_out +) { + zend_string *tensor = NULL; + const char *status = NULL; + zval *descriptor; + zval *dimensions; + zend_ulong rank; + + *tensor_out = NULL; + *upload_out = NULL; + *width_out = 0; + *rows_out = 0; + *type_out = 0; + if (king_inference_resolve_token_embedding_tensor_name(model, NULL, &tensor, NULL, &status) != SUCCESS + || tensor == NULL) { + king_inference_cuda_embedding_row_set_error( + model, + -1, + status != NULL ? status : "cuda_embedding_rows_tensor_not_resolved" + ); + return FAILURE; + } + + descriptor = king_inference_tensor_index_descriptor(model, tensor); + dimensions = descriptor != NULL + ? zend_hash_str_find(Z_ARRVAL_P(descriptor), "dimensions", sizeof("dimensions") - 1) + : NULL; + if (descriptor == NULL + || dimensions == NULL + || Z_TYPE_P(dimensions) != IS_ARRAY + || !king_inference_tensor_descriptor_ulong(descriptor, "rank", sizeof("rank") - 1, &rank) + || !king_inference_tensor_descriptor_ulong(descriptor, "type", sizeof("type") - 1, type_out) + || rank != 2 + || !king_inference_cuda_embedding_row_supported_type(*type_out) + || king_inference_tensor_dimension_at(dimensions, 0, width_out) != SUCCESS + || king_inference_tensor_dimension_at(dimensions, 1, rows_out) != SUCCESS + || *width_out == 0 + || *width_out > UINT_MAX + || *rows_out == 0 + || *rows_out > UINT_MAX + || (*type_out == 8 && (*width_out % 32) != 0) + || (*type_out == 14 && (*width_out % 256) != 0)) { + zend_string_release(tensor); + king_inference_cuda_embedding_row_set_error(model, -1, "cuda_embedding_rows_tensor_descriptor_invalid"); + return FAILURE; + } + + *upload_out = king_inference_cuda_weight_cache_find(model, tensor, true); + if (*upload_out == NULL || (*upload_out)->device_ptr == 0) { + zend_string_release(tensor); + king_inference_cuda_embedding_row_set_error(model, -1, "cuda_embedding_rows_weight_not_uploaded"); + return FAILURE; + } + *tensor_out = tensor; + return SUCCESS; +} + +static zend_result king_inference_cuda_embedding_rows_load( + king_inference_model_object *model, + zval *tokenized_prompt, + zend_ulong batch_tokens, + king_inference_cuda_device_ptr output_matrix, + zend_ulong output_width, + double output_scale +) { + king_inference_cuda_weight_upload *upload; + king_inference_cuda_launch_kernel_fn cu_launch_kernel; + king_inference_cuda_upload_cu_memcpy_htod_fn cu_memcpy_htod; + static king_inference_cuda_launch_kernel_fn cached_cu_launch_kernel = NULL; + static king_inference_cuda_upload_cu_memcpy_htod_fn cached_cu_memcpy_htod = NULL; + zend_string *tensor = NULL; + zval *tokens; + zval *token; + zend_ulong width; + zend_ulong rows; + zend_ulong type; + zend_ulong index = 0; + unsigned int *host_token_ids; + king_inference_cuda_device_ptr device_token_ids = 0; + size_t token_bytes; + unsigned int width32; + unsigned int rows32; + unsigned int batch32; + unsigned int type32; + float scale32; + unsigned int block_x = 256; + unsigned int grid_x; + void *params[8]; + int result; + + if (!model->cuda_embedding_rows_available || model->cuda_embedding_rows_function == NULL) { + king_inference_cuda_embedding_row_set_error(model, -1, "cuda_embedding_rows_loader_unavailable"); + return FAILURE; + } + if (tokenized_prompt == NULL + || Z_TYPE_P(tokenized_prompt) != IS_ARRAY + || batch_tokens == 0 + || batch_tokens > UINT_MAX + || output_matrix == 0 + || output_width == 0 + || output_width > UINT_MAX) { + king_inference_cuda_embedding_row_set_error(model, -1, "cuda_embedding_rows_argument_invalid"); + return FAILURE; + } + tokens = king_inference_array_find(tokenized_prompt, "tokens"); + if (tokens == NULL || Z_TYPE_P(tokens) != IS_ARRAY + || zend_hash_num_elements(Z_ARRVAL_P(tokens)) < batch_tokens) { + king_inference_cuda_embedding_row_set_error(model, -1, "cuda_embedding_rows_tokens_invalid"); + return FAILURE; + } + if (king_inference_cuda_embedding_rows_descriptor( + model, + &tensor, + &upload, + &width, + &rows, + &type + ) != SUCCESS) { + return FAILURE; + } + if (width != output_width || batch_tokens > (zend_ulong) (SIZE_MAX / sizeof(unsigned int))) { + zend_string_release(tensor); + king_inference_cuda_embedding_row_set_error(model, -1, "cuda_embedding_rows_shape_invalid"); + return FAILURE; + } + + token_bytes = (size_t) batch_tokens * sizeof(unsigned int); + host_token_ids = emalloc(token_bytes); + for (index = 0; index < batch_tokens; index++) { + token = zend_hash_index_find(Z_ARRVAL_P(tokens), index); + if (token == NULL + || Z_TYPE_P(token) != IS_LONG + || Z_LVAL_P(token) < 0 + || (zend_ulong) Z_LVAL_P(token) >= rows + || (zend_ulong) Z_LVAL_P(token) > UINT_MAX) { + efree(host_token_ids); + zend_string_release(tensor); + king_inference_cuda_embedding_row_set_error(model, -1, "cuda_embedding_rows_token_invalid"); + return FAILURE; + } + host_token_ids[index] = (unsigned int) Z_LVAL_P(token); + } + + if (king_inference_cuda_device_allocator_alloc(model, token_bytes, &device_token_ids) != SUCCESS) { + efree(host_token_ids); + zend_string_release(tensor); + king_inference_cuda_embedding_row_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_embedding_rows_token_id_allocation_failed" + ); + return FAILURE; + } + if (cached_cu_memcpy_htod == NULL + && !king_inference_cuda_embedding_row_driver_symbol(model, "cuMemcpyHtoD_v2", (void **) &cached_cu_memcpy_htod)) { + (void) king_inference_cuda_device_allocator_free(model, device_token_ids); + efree(host_token_ids); + zend_string_release(tensor); + return FAILURE; + } + cu_memcpy_htod = cached_cu_memcpy_htod; + result = cu_memcpy_htod(device_token_ids, host_token_ids, token_bytes); + efree(host_token_ids); + if (result != 0) { + (void) king_inference_cuda_device_allocator_free(model, device_token_ids); + zend_string_release(tensor); + king_inference_cuda_embedding_row_set_error(model, result, "cuMemcpyHtoD_failed"); + return FAILURE; + } + if (cached_cu_launch_kernel == NULL + && !king_inference_cuda_embedding_row_driver_symbol(model, "cuLaunchKernel", (void **) &cached_cu_launch_kernel)) { + (void) king_inference_cuda_device_allocator_free(model, device_token_ids); + zend_string_release(tensor); + return FAILURE; + } + cu_launch_kernel = cached_cu_launch_kernel; + + width32 = (unsigned int) width; + rows32 = (unsigned int) rows; + batch32 = (unsigned int) batch_tokens; + type32 = (unsigned int) type; + scale32 = (float) output_scale; + grid_x = (width32 + block_x - 1) / block_x; + params[0] = &upload->device_ptr; + params[1] = &device_token_ids; + params[2] = &output_matrix; + params[3] = &width32; + params[4] = &rows32; + params[5] = &batch32; + params[6] = &type32; + params[7] = &scale32; + + result = cu_launch_kernel( + model->cuda_embedding_rows_function, + grid_x, + batch32, + 1, + block_x, + 1, + 1, + 0, + NULL, + params, + NULL + ); + (void) king_inference_cuda_device_allocator_free(model, device_token_ids); + zend_string_release(tensor); + if (result != 0) { + king_inference_cuda_embedding_row_set_error(model, result, "cuLaunchKernel_failed"); + return FAILURE; + } + + model->cuda_embedding_rows_launch_count++; + model->cuda_embedding_rows_last_batch_tokens = batch_tokens; + model->cuda_embedding_rows_last_width = output_width; + model->cuda_embedding_row_result = 0; + model->cuda_embedding_row_error[0] = '\0'; + return SUCCESS; +} diff --git a/extension/src/inference/cuda/kernels/ffn/cuda_ffn_swiglu.inc b/extension/src/inference/cuda/kernels/ffn/cuda_ffn_swiglu.inc new file mode 100644 index 000000000..4666cfdc6 --- /dev/null +++ b/extension/src/inference/cuda/kernels/ffn/cuda_ffn_swiglu.inc @@ -0,0 +1,479 @@ +/* + * CUDA FFN/SwiGLU kernel for native King GPU inference. + */ + +typedef int king_inference_ffn_swiglu_nvrtc_result; +typedef void *king_inference_ffn_swiglu_nvrtc_program; + +typedef king_inference_ffn_swiglu_nvrtc_result (*king_inference_ffn_swiglu_nvrtc_create_program_fn)( + king_inference_ffn_swiglu_nvrtc_program *program, + const char *source, + const char *name, + int header_count, + const char * const *headers, + const char * const *include_names +); +typedef king_inference_ffn_swiglu_nvrtc_result (*king_inference_ffn_swiglu_nvrtc_compile_program_fn)( + king_inference_ffn_swiglu_nvrtc_program program, + int option_count, + const char * const *options +); +typedef king_inference_ffn_swiglu_nvrtc_result (*king_inference_ffn_swiglu_nvrtc_get_ptx_size_fn)( + king_inference_ffn_swiglu_nvrtc_program program, + size_t *ptx_size +); +typedef king_inference_ffn_swiglu_nvrtc_result (*king_inference_ffn_swiglu_nvrtc_get_ptx_fn)( + king_inference_ffn_swiglu_nvrtc_program program, + char *ptx +); +typedef king_inference_ffn_swiglu_nvrtc_result (*king_inference_ffn_swiglu_nvrtc_destroy_program_fn)( + king_inference_ffn_swiglu_nvrtc_program *program +); + +static const char king_inference_cuda_ffn_swiglu_source[] = +"extern \"C\" __device__ float king_silu(float x) {\n" +" if (x >= 0.0f) {\n" +" float e = expf(-x);\n" +" return x / (1.0f + e);\n" +" }\n" +" float e = expf(x);\n" +" return (x * e) / (1.0f + e);\n" +"}\n" +"extern \"C\" __device__ float king_gelu_tanh(float x) {\n" +" float inner = 0.7978845608028654f * (x + 0.044715f * x * x * x);\n" +" return 0.5f * x * (1.0f + tanhf(inner));\n" +"}\n" +"extern \"C\" __global__ void king_ffn_swiglu(\n" +" const float *gate,\n" +" const float *up,\n" +" float *output,\n" +" unsigned int length,\n" +" unsigned int activation\n" +") {\n" +" unsigned int index = (blockIdx.x * blockDim.x) + threadIdx.x;\n" +" if (index >= length) {\n" +" return;\n" +" }\n" +" float gate_value = gate[index];\n" +" float activated = activation == 1u ? king_gelu_tanh(gate_value) : king_silu(gate_value);\n" +" output[index] = activated * up[index];\n" +"}\n"; + +static void king_inference_cuda_ffn_swiglu_reset_fields(king_inference_model_object *model) +{ + model->cuda_ffn_swiglu_nvrtc_handle = NULL; + model->cuda_ffn_swiglu_module = NULL; + model->cuda_ffn_swiglu_function = NULL; + model->cuda_ffn_swiglu_result = 0; + model->cuda_ffn_swiglu_error[0] = '\0'; + model->cuda_ffn_swiglu_launch_count = 0; + model->cuda_ffn_swiglu_attempted = false; + model->cuda_ffn_swiglu_available = false; + model->cuda_ffn_swiglu_nvrtc_available = false; + model->cuda_ffn_swiglu_module_loaded = false; + model->cuda_ffn_swiglu_f32_available = false; + model->cuda_ffn_swiglu_path_available = false; +} + +static void king_inference_cuda_ffn_swiglu_set_error( + king_inference_model_object *model, + int result, + const char *message +) { + if (message == NULL || message[0] == '\0') { + message = "unknown_cuda_ffn_swiglu_error"; + } + + model->cuda_ffn_swiglu_result = result; + snprintf( + model->cuda_ffn_swiglu_error, + sizeof(model->cuda_ffn_swiglu_error), + "%s", + message + ); +} + +static bool king_inference_cuda_ffn_swiglu_driver_symbol( + king_inference_model_object *model, + const char *name, + void **symbol +) { + *symbol = NULL; + if (model->cuda_driver_handle == NULL) { + king_inference_cuda_ffn_swiglu_set_error(model, -1, "cuda_driver_handle_unavailable"); + return false; + } + + *symbol = dlsym(model->cuda_driver_handle, name); + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_ffn_swiglu_set_error(model, -1, name); + return false; +} + +static bool king_inference_cuda_ffn_swiglu_nvrtc_symbol( + king_inference_model_object *model, + const char *name, + void **symbol +) { + *symbol = NULL; + if (model->cuda_ffn_swiglu_nvrtc_handle == NULL) { + king_inference_cuda_ffn_swiglu_set_error(model, -1, "nvrtc_handle_unavailable"); + return false; + } + + *symbol = dlsym(model->cuda_ffn_swiglu_nvrtc_handle, name); + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_ffn_swiglu_set_error(model, -1, name); + return false; +} + +static bool king_inference_cuda_ffn_swiglu_open_nvrtc(king_inference_model_object *model) +{ + static const char *libraries[] = { + "libnvrtc.so.12", + "libnvrtc.so.11.2", + "libnvrtc.so.11.0", + "libnvrtc.so" + }; + size_t index; + + for (index = 0; index < sizeof(libraries) / sizeof(libraries[0]); index++) { + model->cuda_ffn_swiglu_nvrtc_handle = dlopen(libraries[index], RTLD_LAZY | RTLD_LOCAL); + if (model->cuda_ffn_swiglu_nvrtc_handle != NULL) { + model->cuda_ffn_swiglu_nvrtc_available = true; + return true; + } + } + + king_inference_cuda_ffn_swiglu_set_error(model, -1, "libnvrtc_unavailable"); + return false; +} + +static bool king_inference_cuda_ffn_swiglu_compile_ptx( + king_inference_model_object *model, + char **ptx, + size_t *ptx_size +) { + king_inference_ffn_swiglu_nvrtc_create_program_fn nvrtc_create_program; + king_inference_ffn_swiglu_nvrtc_compile_program_fn nvrtc_compile_program; + king_inference_ffn_swiglu_nvrtc_get_ptx_size_fn nvrtc_get_ptx_size; + king_inference_ffn_swiglu_nvrtc_get_ptx_fn nvrtc_get_ptx; + king_inference_ffn_swiglu_nvrtc_destroy_program_fn nvrtc_destroy_program; + king_inference_ffn_swiglu_nvrtc_program program = NULL; + const char *options[] = {"--std=c++11", "--gpu-architecture=compute_89", "--use_fast_math"}; + king_inference_ffn_swiglu_nvrtc_result result; + + *ptx = NULL; + *ptx_size = 0; + + if (!king_inference_cuda_ffn_swiglu_nvrtc_symbol(model, "nvrtcCreateProgram", (void **) &nvrtc_create_program) + || !king_inference_cuda_ffn_swiglu_nvrtc_symbol(model, "nvrtcCompileProgram", (void **) &nvrtc_compile_program) + || !king_inference_cuda_ffn_swiglu_nvrtc_symbol(model, "nvrtcGetPTXSize", (void **) &nvrtc_get_ptx_size) + || !king_inference_cuda_ffn_swiglu_nvrtc_symbol(model, "nvrtcGetPTX", (void **) &nvrtc_get_ptx) + || !king_inference_cuda_ffn_swiglu_nvrtc_symbol(model, "nvrtcDestroyProgram", (void **) &nvrtc_destroy_program)) { + return false; + } + + result = nvrtc_create_program( + &program, + king_inference_cuda_ffn_swiglu_source, + "king_ffn_swiglu.cu", + 0, + NULL, + NULL + ); + if (result != 0 || program == NULL) { + king_inference_cuda_ffn_swiglu_set_error( + model, + result, + result != 0 ? "nvrtcCreateProgram_failed" : "nvrtcProgram_unavailable" + ); + return false; + } + + result = nvrtc_compile_program(program, 3, options); + if (result != 0) { + (void) nvrtc_destroy_program(&program); + king_inference_cuda_ffn_swiglu_set_error(model, result, "nvrtcCompileProgram_failed"); + return false; + } + + result = nvrtc_get_ptx_size(program, ptx_size); + if (result != 0 || *ptx_size == 0) { + (void) nvrtc_destroy_program(&program); + king_inference_cuda_ffn_swiglu_set_error( + model, + result, + result != 0 ? "nvrtcGetPTXSize_failed" : "nvrtc_ptx_empty" + ); + return false; + } + + *ptx = emalloc(*ptx_size); + result = nvrtc_get_ptx(program, *ptx); + (void) nvrtc_destroy_program(&program); + if (result != 0) { + efree(*ptx); + *ptx = NULL; + *ptx_size = 0; + king_inference_cuda_ffn_swiglu_set_error(model, result, "nvrtcGetPTX_failed"); + return false; + } + + return true; +} + +static void king_inference_cuda_ffn_swiglu_release(king_inference_model_object *model) +{ + king_inference_cuda_module_unload_fn cu_module_unload; + + if (model->cuda_ffn_swiglu_module != NULL + && model->cuda_driver_handle != NULL + && model->cuda_context != NULL + && king_inference_cuda_device_allocator_set_current(model) + && king_inference_cuda_ffn_swiglu_driver_symbol( + model, + "cuModuleUnload", + (void **) &cu_module_unload + )) { + (void) cu_module_unload(model->cuda_ffn_swiglu_module); + } + + if (model->cuda_ffn_swiglu_nvrtc_handle != NULL) { + dlclose(model->cuda_ffn_swiglu_nvrtc_handle); + } + + king_inference_cuda_ffn_swiglu_reset_fields(model); +} + +static void king_inference_cuda_ffn_swiglu_finish_available(king_inference_model_object *model) +{ + model->cuda_ffn_swiglu_f32_available = true; + model->cuda_ffn_swiglu_path_available = model->cuda_quantized_matvec_available; + model->cuda_ffn_swiglu_available = model->cuda_ffn_swiglu_path_available; + if (!model->cuda_ffn_swiglu_path_available) { + king_inference_cuda_ffn_swiglu_set_error(model, -1, "cuda_quantized_matvec_dependency_unavailable"); + return; + } + + model->cuda_ffn_swiglu_result = 0; + model->cuda_ffn_swiglu_error[0] = '\0'; +} + +static void king_inference_cuda_ffn_swiglu_init( + king_inference_model_object *model, + king_inference_backend_kind backend_kind +) { + king_inference_cuda_module_load_data_fn cu_module_load_data; + king_inference_cuda_module_get_function_fn cu_module_get_function; + char *ptx = NULL; + size_t ptx_size = 0; + void *module = NULL; + void *function = NULL; + int result; + + king_inference_cuda_ffn_swiglu_release(model); + if (backend_kind != KING_INFERENCE_BACKEND_KING_NATIVE_GPU + || !king_inference_gpu_enabled(&model->config)) { + return; + } + + model->cuda_ffn_swiglu_attempted = true; + if (!model->cuda_device_allocator_available) { + king_inference_cuda_ffn_swiglu_set_error(model, -1, "cuda_device_allocator_unavailable"); + return; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + king_inference_cuda_ffn_swiglu_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_context_current_failed" + ); + return; + } + if (!king_inference_cuda_ffn_swiglu_open_nvrtc(model) + || !king_inference_cuda_ffn_swiglu_compile_ptx(model, &ptx, &ptx_size)) { + return; + } + if (!king_inference_cuda_ffn_swiglu_driver_symbol( + model, + "cuModuleLoadData", + (void **) &cu_module_load_data + ) + || !king_inference_cuda_ffn_swiglu_driver_symbol( + model, + "cuModuleGetFunction", + (void **) &cu_module_get_function + )) { + efree(ptx); + return; + } + + result = cu_module_load_data(&module, ptx); + efree(ptx); + if (result != 0 || module == NULL) { + king_inference_cuda_ffn_swiglu_set_error( + model, + result, + result != 0 ? "cuModuleLoadData_failed" : "cuda_module_unavailable" + ); + return; + } + + result = cu_module_get_function(&function, module, "king_ffn_swiglu"); + if (result != 0 || function == NULL) { + king_inference_cuda_module_unload_fn cu_module_unload = NULL; + + if (king_inference_cuda_ffn_swiglu_driver_symbol( + model, + "cuModuleUnload", + (void **) &cu_module_unload + ) + && cu_module_unload != NULL) { + (void) cu_module_unload(module); + } + king_inference_cuda_ffn_swiglu_set_error( + model, + result, + result != 0 ? "cuModuleGetFunction_failed" : "cuda_ffn_swiglu_kernel_unavailable" + ); + return; + } + + model->cuda_ffn_swiglu_module = module; + model->cuda_ffn_swiglu_function = function; + model->cuda_ffn_swiglu_module_loaded = true; + king_inference_cuda_ffn_swiglu_finish_available(model); +} + +static zend_result king_inference_cuda_ffn_swiglu_f32( + king_inference_model_object *model, + king_inference_cuda_device_ptr input_gate, + king_inference_cuda_device_ptr input_up, + king_inference_cuda_device_ptr output, + zend_ulong length, + zend_ulong activation +) { + king_inference_cuda_launch_kernel_fn cu_launch_kernel; + static king_inference_cuda_launch_kernel_fn cached_cu_launch_kernel = NULL; + unsigned int length32; + unsigned int block_x = 256; + unsigned int grid_x; + unsigned int activation32; + void *params[5]; + int result; + + if (!model->cuda_ffn_swiglu_f32_available || model->cuda_ffn_swiglu_function == NULL) { + king_inference_cuda_ffn_swiglu_set_error(model, -1, "cuda_ffn_swiglu_unavailable"); + return FAILURE; + } + if (input_gate == 0 || input_up == 0 || output == 0) { + king_inference_cuda_ffn_swiglu_set_error(model, -1, "cuda_ffn_swiglu_argument_invalid"); + return FAILURE; + } + if (length == 0 || length > UINT_MAX || activation > 1) { + king_inference_cuda_ffn_swiglu_set_error(model, -1, "cuda_ffn_swiglu_shape_invalid"); + return FAILURE; + } + if (cached_cu_launch_kernel == NULL + && !king_inference_cuda_ffn_swiglu_driver_symbol( + model, + "cuLaunchKernel", + (void **) &cached_cu_launch_kernel + )) { + return FAILURE; + } + cu_launch_kernel = cached_cu_launch_kernel; + + length32 = (unsigned int) length; + activation32 = (unsigned int) activation; + grid_x = (length32 / block_x) + ((length32 % block_x) != 0u ? 1u : 0u); + params[0] = &input_gate; + params[1] = &input_up; + params[2] = &output; + params[3] = &length32; + params[4] = &activation32; + + result = cu_launch_kernel( + model->cuda_ffn_swiglu_function, + grid_x, + 1, + 1, + block_x, + 1, + 1, + 0, + NULL, + params, + NULL + ); + if (result != 0) { + king_inference_cuda_ffn_swiglu_set_error(model, result, "cuLaunchKernel_failed"); + return FAILURE; + } + + model->cuda_ffn_swiglu_launch_count++; + king_inference_cuda_ffn_swiglu_finish_available(model); + return SUCCESS; +} + +static void king_inference_cuda_ffn_swiglu_add_status( + king_inference_model_object *model, + zval *return_value +) { + zval ffn_swiglu; + + array_init(&ffn_swiglu); + add_assoc_bool(&ffn_swiglu, "attempted", model->cuda_ffn_swiglu_attempted); + add_assoc_bool(&ffn_swiglu, "available", model->cuda_ffn_swiglu_available); + add_assoc_bool(&ffn_swiglu, "path_available", model->cuda_ffn_swiglu_path_available); + add_assoc_bool(&ffn_swiglu, "quantized_matvec_available", model->cuda_quantized_matvec_available); + add_assoc_bool(&ffn_swiglu, "nvrtc_available", model->cuda_ffn_swiglu_nvrtc_available); + add_assoc_bool(&ffn_swiglu, "module_loaded", model->cuda_ffn_swiglu_module_loaded); + add_assoc_bool(&ffn_swiglu, "f32_available", model->cuda_ffn_swiglu_f32_available); + add_assoc_long(&ffn_swiglu, "launches", (zend_long) model->cuda_ffn_swiglu_launch_count); + add_assoc_long(&ffn_swiglu, "result", model->cuda_ffn_swiglu_result); + add_assoc_string(&ffn_swiglu, "error", model->cuda_ffn_swiglu_error); + add_assoc_zval(return_value, "ffn_swiglu_path", &ffn_swiglu); +} + +static void king_inference_cuda_ffn_swiglu_apply_status( + king_inference_model_object *model, + zval *return_value +) { + zval *reason; + bool reason_can_be_ffn; + + king_inference_cuda_ffn_swiglu_add_status(model, return_value); + if (!model->cuda_ffn_swiglu_attempted + || model->cuda_ffn_swiglu_available + || !model->cuda_context_available) { + return; + } + + reason = zend_hash_str_find(Z_ARRVAL_P(return_value), "reason", sizeof("reason") - 1); + reason_can_be_ffn = reason == NULL + || Z_TYPE_P(reason) != IS_STRING + || zend_string_equals_literal(Z_STR_P(reason), "gpu_ready") + || zend_string_equals_literal(Z_STR_P(reason), "gpu_decoder_kernel_not_implemented"); + if (reason_can_be_ffn) { + king_inference_cuda_context_prepend_return_reason( + return_value, + "gpu_ffn_swiglu_path_unavailable" + ); + add_assoc_string(return_value, "reason", "gpu_ffn_swiglu_path_unavailable"); + } else { + king_inference_gpu_status_append_return_reason( + return_value, + "gpu_ffn_swiglu_path_unavailable" + ); + } +} diff --git a/extension/src/inference/cuda/kernels/matvec/cuda_quantized_matvec.inc b/extension/src/inference/cuda/kernels/matvec/cuda_quantized_matvec.inc new file mode 100644 index 000000000..6f50365ce --- /dev/null +++ b/extension/src/inference/cuda/kernels/matvec/cuda_quantized_matvec.inc @@ -0,0 +1,738 @@ +/* + * CUDA quantized matrix/vector kernels for native King GPU inference. + */ + +typedef int king_inference_nvrtc_result; +typedef void *king_inference_nvrtc_program; + +typedef king_inference_nvrtc_result (*king_inference_nvrtc_create_program_fn)( + king_inference_nvrtc_program *program, + const char *source, + const char *name, + int header_count, + const char * const *headers, + const char * const *include_names +); +typedef king_inference_nvrtc_result (*king_inference_nvrtc_compile_program_fn)( + king_inference_nvrtc_program program, + int option_count, + const char * const *options +); +typedef king_inference_nvrtc_result (*king_inference_nvrtc_get_ptx_size_fn)( + king_inference_nvrtc_program program, + size_t *ptx_size +); +typedef king_inference_nvrtc_result (*king_inference_nvrtc_get_ptx_fn)( + king_inference_nvrtc_program program, + char *ptx +); +typedef king_inference_nvrtc_result (*king_inference_nvrtc_destroy_program_fn)( + king_inference_nvrtc_program *program +); + +typedef int (*king_inference_cuda_module_load_data_fn)(void **module, const void *image); +typedef int (*king_inference_cuda_module_get_function_fn)( + void **function, + void *module, + const char *name +); +typedef int (*king_inference_cuda_module_unload_fn)(void *module); +typedef int (*king_inference_cuda_launch_kernel_fn)( + void *function, + unsigned int grid_x, + unsigned int grid_y, + unsigned int grid_z, + unsigned int block_x, + unsigned int block_y, + unsigned int block_z, + unsigned int shared_memory_bytes, + void *stream, + void **kernel_params, + void **extra +); +typedef int (*king_inference_cuda_ctx_synchronize_fn)(void); + +static const char king_inference_cuda_quantized_matvec_source[] = +"extern \"C\" __device__ float king_half_to_float(unsigned short h) {\n" +" unsigned int sign = ((unsigned int) (h & 0x8000u)) << 16;\n" +" unsigned int mantissa = (unsigned int) (h & 0x03ffu);\n" +" int exponent = (int) ((h >> 10) & 0x1fu);\n" +" unsigned int bits;\n" +" if (exponent == 0) {\n" +" if (mantissa == 0u) {\n" +" bits = sign;\n" +" } else {\n" +" while ((mantissa & 0x0400u) == 0u) {\n" +" mantissa <<= 1;\n" +" exponent--;\n" +" }\n" +" exponent++;\n" +" mantissa &= 0x03ffu;\n" +" bits = sign | ((unsigned int) (exponent + 112) << 23) | (mantissa << 13);\n" +" }\n" +" return __uint_as_float(bits);\n" +" }\n" +" if (exponent == 31) {\n" +" bits = sign | 0x7f800000u | (mantissa << 13);\n" +" return __uint_as_float(bits);\n" +" }\n" +" bits = sign | ((unsigned int) (exponent + 112) << 23) | (mantissa << 13);\n" +" return __uint_as_float(bits);\n" +"}\n" +"extern \"C\" __device__ void king_qk4_scale_min(const unsigned char *scales, unsigned int group, int *scale, int *minimum) {\n" +" if (group < 4u) {\n" +" *scale = (int) (scales[group] & 0x3fu);\n" +" *minimum = (int) (scales[group + 4u] & 0x3fu);\n" +" return;\n" +" }\n" +" *scale = (int) ((scales[group + 4u] & 0x0fu) | ((scales[group - 4u] >> 6) << 4));\n" +" *minimum = (int) ((scales[group + 4u] >> 4) | ((scales[group] >> 6) << 4));\n" +"}\n" +"extern \"C\" __device__ float king_quantized_value(\n" +" const unsigned char *row_weights,\n" +" unsigned int tensor_type,\n" +" unsigned int col\n" +") {\n" +" if (tensor_type == 8u) {\n" +" unsigned int block_index = col >> 5;\n" +" unsigned int lane = col & 31u;\n" +" const unsigned char *qblock = row_weights + ((unsigned long long) block_index * 34ull);\n" +" unsigned short raw_scale = (unsigned short) qblock[0] | ((unsigned short) qblock[1] << 8);\n" +" signed char quant = ((const signed char *) (qblock + 2))[lane];\n" +" return ((float) quant) * king_half_to_float(raw_scale);\n" +" }\n" +" if (tensor_type == 6u) {\n" +" unsigned int block_index = col >> 5;\n" +" unsigned int in_block = col & 31u;\n" +" const unsigned char *qblock = row_weights + ((unsigned long long) block_index * 22ull);\n" +" unsigned int high_bits = (unsigned int) qblock[2] | ((unsigned int) qblock[3] << 8) | ((unsigned int) qblock[4] << 16) | ((unsigned int) qblock[5] << 24);\n" +" unsigned char packed = qblock[6u + (in_block & 15u)];\n" +" int low = (int) (in_block < 16u ? (packed & 0x0fu) : (packed >> 4));\n" +" int high = (int) ((high_bits >> in_block) & 1u);\n" +" int quant = (low | (high << 4)) - 16;\n" +" unsigned short raw_scale = (unsigned short) qblock[0] | ((unsigned short) qblock[1] << 8);\n" +" return ((float) quant) * king_half_to_float(raw_scale);\n" +" }\n" +" if (tensor_type == 12u) {\n" +" unsigned int block_index = col >> 8;\n" +" unsigned int in_block = col & 255u;\n" +" unsigned int group = in_block >> 5;\n" +" unsigned int in_group = in_block & 31u;\n" +" const unsigned char *qblock = row_weights + ((unsigned long long) block_index * 144ull);\n" +" const unsigned char *scales = qblock + 4;\n" +" const unsigned char *qs = qblock + 16;\n" +" unsigned char packed = qs[(group >> 1) * 32u + in_group];\n" +" int scale;\n" +" int minimum;\n" +" int quant = (int) ((group & 1u) == 0u ? (packed & 0x0fu) : (packed >> 4));\n" +" float d = king_half_to_float((unsigned short) qblock[0] | ((unsigned short) qblock[1] << 8));\n" +" float dmin = king_half_to_float((unsigned short) qblock[2] | ((unsigned short) qblock[3] << 8));\n" +" king_qk4_scale_min(scales, group, &scale, &minimum);\n" +" return d * ((float) scale) * ((float) quant) - dmin * ((float) minimum);\n" +" }\n" +" if (tensor_type == 14u) {\n" +" unsigned int block_index = col >> 8;\n" +" unsigned int in_block = col & 255u;\n" +" unsigned int chunk = in_block >> 7;\n" +" unsigned int within = in_block & 127u;\n" +" unsigned int lane = within & 31u;\n" +" unsigned int quadrant = within >> 5;\n" +" const unsigned char *qblock = row_weights + ((unsigned long long) block_index * 210ull);\n" +" const unsigned char *ql = qblock + chunk * 64u;\n" +" const unsigned char *qh = qblock + 128u + chunk * 32u;\n" +" const signed char *scales = (const signed char *) (qblock + 192u + chunk * 8u);\n" +" int quant;\n" +" int scale;\n" +" if (quadrant == 0u) {\n" +" quant = (int) ((ql[lane] & 0x0fu) | ((qh[lane] & 0x03u) << 4));\n" +" scale = (int) scales[lane / 16u];\n" +" } else if (quadrant == 1u) {\n" +" quant = (int) ((ql[lane + 32u] & 0x0fu) | ((qh[lane] & 0x0cu) << 2));\n" +" scale = (int) scales[2u + lane / 16u];\n" +" } else if (quadrant == 2u) {\n" +" quant = (int) ((ql[lane] >> 4) | (qh[lane] & 0x30u));\n" +" scale = (int) scales[4u + lane / 16u];\n" +" } else {\n" +" quant = (int) ((ql[lane + 32u] >> 4) | ((qh[lane] & 0xc0u) >> 2));\n" +" scale = (int) scales[6u + lane / 16u];\n" +" }\n" +" float d = king_half_to_float((unsigned short) qblock[208] | ((unsigned short) qblock[209] << 8));\n" +" return d * ((float) scale) * ((float) (quant - 32));\n" +" }\n" +" return 0.0f;\n" +"}\n" +"extern \"C\" __global__ void king_quantized_matvec(\n" +" const unsigned char *weights,\n" +" const float *vector,\n" +" float *output,\n" +" unsigned int columns,\n" +" unsigned int rows,\n" +" unsigned int tensor_type,\n" +" unsigned int block_size,\n" +" unsigned int type_size\n" +") {\n" +" unsigned int row = blockIdx.x;\n" +" unsigned int lane_id = threadIdx.x;\n" +" __shared__ float partials[128];\n" +" if (tensor_type == 8u) {\n" +" unsigned int group_id = threadIdx.x >> 5;\n" +" unsigned int group_lane = threadIdx.x & 31u;\n" +" unsigned int q8_row = blockIdx.x * 4u + group_id;\n" +" bool active = q8_row < rows && columns != 0u;\n" +" unsigned int blocks_per_row = (columns + block_size - 1u) / block_size;\n" +" const unsigned char *row_weights = weights + ((unsigned long long) q8_row * blocks_per_row * (unsigned long long) type_size);\n" +" float sum = 0.0f;\n" +" if (active) {\n" +" for (unsigned int col = group_lane; col < columns; col += 32u) {\n" +" unsigned int block_index = col >> 5;\n" +" unsigned int lane = col & 31u;\n" +" const unsigned char *qblock = row_weights + ((unsigned long long) block_index * 34ull);\n" +" unsigned short raw_scale = (unsigned short) qblock[0] | ((unsigned short) qblock[1] << 8);\n" +" signed char quant = ((const signed char *) (qblock + 2))[lane];\n" +" sum += ((float) quant) * king_half_to_float(raw_scale) * vector[col];\n" +" }\n" +" }\n" +" for (unsigned int stride = 16u; stride > 0u; stride >>= 1) {\n" +" sum += __shfl_down_sync(0xffffffffu, sum, stride);\n" +" }\n" +" if (active && group_lane == 0u) {\n" +" output[q8_row] = sum;\n" +" }\n" +" return;\n" +" }\n" +" if (row >= rows || columns == 0u) {\n" +" return;\n" +" }\n" +" unsigned int blocks_per_row = (columns + block_size - 1u) / block_size;\n" +" const unsigned char *row_weights = weights + ((unsigned long long) row * blocks_per_row * (unsigned long long) type_size);\n" +" float sum = 0.0f;\n" +" for (unsigned int col = lane_id; col < columns; col += blockDim.x) {\n" +" sum += king_quantized_value(row_weights, tensor_type, col) * vector[col];\n" +" }\n" +" partials[lane_id] = sum;\n" +" __syncthreads();\n" +" for (unsigned int stride = blockDim.x >> 1; stride > 0u; stride >>= 1) {\n" +" if (lane_id < stride) {\n" +" partials[lane_id] += partials[lane_id + stride];\n" +" }\n" +" __syncthreads();\n" +" }\n" +" if (lane_id == 0u) {\n" +" output[row] = partials[0];\n" +" }\n" +"}\n" +"extern \"C\" __global__ void king_quantized_matvec_batch(\n" +" const unsigned char *weights,\n" +" const float *vectors,\n" +" float *output,\n" +" unsigned int columns,\n" +" unsigned int rows,\n" +" unsigned int batch_count,\n" +" unsigned int tensor_type,\n" +" unsigned int block_size,\n" +" unsigned int type_size\n" +") {\n" +" unsigned int batch = blockIdx.y;\n" +" unsigned int row = blockIdx.x;\n" +" unsigned int lane_id = threadIdx.x;\n" +" const float *vector;\n" +" __shared__ float partials[128];\n" +" if (batch >= batch_count || columns == 0u) {\n" +" return;\n" +" }\n" +" vector = vectors + ((unsigned long long) batch * (unsigned long long) columns);\n" +" if (tensor_type == 8u) {\n" +" unsigned int group_id = threadIdx.x >> 5;\n" +" unsigned int group_lane = threadIdx.x & 31u;\n" +" unsigned int q8_row = blockIdx.x * 4u + group_id;\n" +" bool active = q8_row < rows;\n" +" unsigned int blocks_per_row = (columns + block_size - 1u) / block_size;\n" +" const unsigned char *row_weights = weights + ((unsigned long long) q8_row * blocks_per_row * (unsigned long long) type_size);\n" +" float sum = 0.0f;\n" +" if (active) {\n" +" for (unsigned int col = group_lane; col < columns; col += 32u) {\n" +" unsigned int block_index = col >> 5;\n" +" unsigned int lane = col & 31u;\n" +" const unsigned char *qblock = row_weights + ((unsigned long long) block_index * 34ull);\n" +" unsigned short raw_scale = (unsigned short) qblock[0] | ((unsigned short) qblock[1] << 8);\n" +" signed char quant = ((const signed char *) (qblock + 2))[lane];\n" +" sum += ((float) quant) * king_half_to_float(raw_scale) * vector[col];\n" +" }\n" +" }\n" +" for (unsigned int stride = 16u; stride > 0u; stride >>= 1) {\n" +" sum += __shfl_down_sync(0xffffffffu, sum, stride);\n" +" }\n" +" if (active && group_lane == 0u) {\n" +" output[((unsigned long long) batch * (unsigned long long) rows) + q8_row] = sum;\n" +" }\n" +" return;\n" +" }\n" +" if (row >= rows) {\n" +" return;\n" +" }\n" +" unsigned int blocks_per_row = (columns + block_size - 1u) / block_size;\n" +" const unsigned char *row_weights = weights + ((unsigned long long) row * blocks_per_row * (unsigned long long) type_size);\n" +" float sum = 0.0f;\n" +" for (unsigned int col = lane_id; col < columns; col += blockDim.x) {\n" +" sum += king_quantized_value(row_weights, tensor_type, col) * vector[col];\n" +" }\n" +" partials[lane_id] = sum;\n" +" __syncthreads();\n" +" for (unsigned int stride = blockDim.x >> 1; stride > 0u; stride >>= 1) {\n" +" if (lane_id < stride) {\n" +" partials[lane_id] += partials[lane_id + stride];\n" +" }\n" +" __syncthreads();\n" +" }\n" +" if (lane_id == 0u) {\n" +" output[((unsigned long long) batch * (unsigned long long) rows) + row] = partials[0];\n" +" }\n" +"}\n"; + +static bool king_inference_cuda_quantized_matvec_type_supported(zend_ulong type) +{ + return type == 6 || type == 8 || type == 12 || type == 14; +} + +static void king_inference_cuda_quantized_matvec_reset_fields(king_inference_model_object *model) +{ + model->cuda_nvrtc_handle = NULL; + model->cuda_quantized_matvec_module = NULL; + model->cuda_q8_0_matvec_function = NULL; + model->cuda_quantized_matvec_batch_function = NULL; + model->cuda_quantized_matvec_result = 0; + model->cuda_quantized_matvec_error[0] = '\0'; + model->cuda_quantized_matvec_launch_count = 0; + model->cuda_quantized_matvec_batch_launch_count = 0; + model->cuda_quantized_matvec_attempted = false; + model->cuda_quantized_matvec_available = false; + model->cuda_quantized_matvec_nvrtc_available = false; + model->cuda_quantized_matvec_module_loaded = false; + model->cuda_quantized_matvec_q8_0_available = false; + model->cuda_quantized_matvec_batch_available = false; +} + +static void king_inference_cuda_quantized_matvec_set_error( + king_inference_model_object *model, + int result, + const char *message +) { + if (message == NULL || message[0] == '\0') { + message = "unknown_cuda_quantized_matvec_error"; + } + + model->cuda_quantized_matvec_result = result; + snprintf( + model->cuda_quantized_matvec_error, + sizeof(model->cuda_quantized_matvec_error), + "%s", + message + ); +} + +static bool king_inference_cuda_quantized_matvec_driver_symbol( + king_inference_model_object *model, + const char *name, + void **symbol +) { + *symbol = NULL; + if (model->cuda_driver_handle == NULL) { + king_inference_cuda_quantized_matvec_set_error(model, -1, "cuda_driver_handle_unavailable"); + return false; + } + + *symbol = dlsym(model->cuda_driver_handle, name); + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_quantized_matvec_set_error(model, -1, name); + return false; +} + +static bool king_inference_cuda_quantized_matvec_nvrtc_symbol( + king_inference_model_object *model, + const char *name, + void **symbol +) { + *symbol = NULL; + if (model->cuda_nvrtc_handle == NULL) { + king_inference_cuda_quantized_matvec_set_error(model, -1, "nvrtc_handle_unavailable"); + return false; + } + + *symbol = dlsym(model->cuda_nvrtc_handle, name); + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_quantized_matvec_set_error(model, -1, name); + return false; +} + +static bool king_inference_cuda_quantized_matvec_open_nvrtc(king_inference_model_object *model) +{ + static const char *libraries[] = { + "libnvrtc.so.12", + "libnvrtc.so.11.2", + "libnvrtc.so.11.0", + "libnvrtc.so" + }; + size_t index; + + for (index = 0; index < sizeof(libraries) / sizeof(libraries[0]); index++) { + model->cuda_nvrtc_handle = dlopen(libraries[index], RTLD_LAZY | RTLD_LOCAL); + if (model->cuda_nvrtc_handle != NULL) { + model->cuda_quantized_matvec_nvrtc_available = true; + return true; + } + } + + king_inference_cuda_quantized_matvec_set_error(model, -1, "libnvrtc_unavailable"); + return false; +} + +static bool king_inference_cuda_quantized_matvec_compile_ptx( + king_inference_model_object *model, + char **ptx, + size_t *ptx_size +) { + king_inference_nvrtc_create_program_fn nvrtc_create_program; + king_inference_nvrtc_compile_program_fn nvrtc_compile_program; + king_inference_nvrtc_get_ptx_size_fn nvrtc_get_ptx_size; + king_inference_nvrtc_get_ptx_fn nvrtc_get_ptx; + king_inference_nvrtc_destroy_program_fn nvrtc_destroy_program; + king_inference_nvrtc_program program = NULL; + const char *options[] = {"--std=c++11", "--gpu-architecture=compute_89", "--use_fast_math"}; + king_inference_nvrtc_result result; + + *ptx = NULL; + *ptx_size = 0; + + if (!king_inference_cuda_quantized_matvec_nvrtc_symbol( + model, + "nvrtcCreateProgram", + (void **) &nvrtc_create_program + ) + || !king_inference_cuda_quantized_matvec_nvrtc_symbol( + model, + "nvrtcCompileProgram", + (void **) &nvrtc_compile_program + ) + || !king_inference_cuda_quantized_matvec_nvrtc_symbol( + model, + "nvrtcGetPTXSize", + (void **) &nvrtc_get_ptx_size + ) + || !king_inference_cuda_quantized_matvec_nvrtc_symbol( + model, + "nvrtcGetPTX", + (void **) &nvrtc_get_ptx + ) + || !king_inference_cuda_quantized_matvec_nvrtc_symbol( + model, + "nvrtcDestroyProgram", + (void **) &nvrtc_destroy_program + )) { + return false; + } + + result = nvrtc_create_program( + &program, + king_inference_cuda_quantized_matvec_source, + "king_quantized_matvec.cu", + 0, + NULL, + NULL + ); + if (result != 0 || program == NULL) { + king_inference_cuda_quantized_matvec_set_error( + model, + result, + result != 0 ? "nvrtcCreateProgram_failed" : "nvrtcProgram_unavailable" + ); + return false; + } + + result = nvrtc_compile_program(program, 3, options); + if (result != 0) { + (void) nvrtc_destroy_program(&program); + king_inference_cuda_quantized_matvec_set_error(model, result, "nvrtcCompileProgram_failed"); + return false; + } + + result = nvrtc_get_ptx_size(program, ptx_size); + if (result != 0 || *ptx_size == 0) { + (void) nvrtc_destroy_program(&program); + king_inference_cuda_quantized_matvec_set_error( + model, + result, + result != 0 ? "nvrtcGetPTXSize_failed" : "nvrtc_ptx_empty" + ); + return false; + } + + *ptx = emalloc(*ptx_size); + result = nvrtc_get_ptx(program, *ptx); + (void) nvrtc_destroy_program(&program); + if (result != 0) { + efree(*ptx); + *ptx = NULL; + *ptx_size = 0; + king_inference_cuda_quantized_matvec_set_error(model, result, "nvrtcGetPTX_failed"); + return false; + } + + return true; +} + +static void king_inference_cuda_quantized_matvec_release(king_inference_model_object *model) +{ + king_inference_cuda_module_unload_fn cu_module_unload; + + if (model->cuda_quantized_matvec_module != NULL + && model->cuda_driver_handle != NULL + && model->cuda_context != NULL + && king_inference_cuda_device_allocator_set_current(model) + && king_inference_cuda_quantized_matvec_driver_symbol( + model, + "cuModuleUnload", + (void **) &cu_module_unload + )) { + (void) cu_module_unload(model->cuda_quantized_matvec_module); + } + + if (model->cuda_nvrtc_handle != NULL) { + dlclose(model->cuda_nvrtc_handle); + } + + king_inference_cuda_quantized_matvec_reset_fields(model); +} + +static void king_inference_cuda_quantized_matvec_init( + king_inference_model_object *model, + king_inference_backend_kind backend_kind +) { + king_inference_cuda_module_load_data_fn cu_module_load_data; + king_inference_cuda_module_get_function_fn cu_module_get_function; + char *ptx = NULL; + size_t ptx_size = 0; + void *module = NULL; + void *function = NULL; + void *batch_function = NULL; + int result; + + king_inference_cuda_quantized_matvec_release(model); + if (backend_kind != KING_INFERENCE_BACKEND_KING_NATIVE_GPU + || !king_inference_gpu_enabled(&model->config)) { + return; + } + + model->cuda_quantized_matvec_attempted = true; + if (!model->cuda_weight_cache_ready) { + king_inference_cuda_quantized_matvec_set_error(model, -1, "cuda_weight_cache_unavailable"); + return; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + king_inference_cuda_quantized_matvec_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_context_current_failed" + ); + return; + } + if (!king_inference_cuda_quantized_matvec_open_nvrtc(model) + || !king_inference_cuda_quantized_matvec_compile_ptx(model, &ptx, &ptx_size)) { + return; + } + if (!king_inference_cuda_quantized_matvec_driver_symbol( + model, + "cuModuleLoadData", + (void **) &cu_module_load_data + ) + || !king_inference_cuda_quantized_matvec_driver_symbol( + model, + "cuModuleGetFunction", + (void **) &cu_module_get_function + )) { + efree(ptx); + return; + } + + result = cu_module_load_data(&module, ptx); + efree(ptx); + if (result != 0 || module == NULL) { + king_inference_cuda_quantized_matvec_set_error( + model, + result, + result != 0 ? "cuModuleLoadData_failed" : "cuda_module_unavailable" + ); + return; + } + + result = cu_module_get_function(&function, module, "king_quantized_matvec"); + if (result != 0 || function == NULL) { + king_inference_cuda_module_unload_fn cu_module_unload = NULL; + + if (king_inference_cuda_quantized_matvec_driver_symbol( + model, + "cuModuleUnload", + (void **) &cu_module_unload + ) + && cu_module_unload != NULL) { + (void) cu_module_unload(module); + } + king_inference_cuda_quantized_matvec_set_error( + model, + result, + result != 0 ? "cuModuleGetFunction_failed" : "cuda_q8_0_kernel_unavailable" + ); + return; + } + result = cu_module_get_function(&batch_function, module, "king_quantized_matvec_batch"); + if (result != 0 || batch_function == NULL) { + king_inference_cuda_module_unload_fn cu_module_unload = NULL; + + if (king_inference_cuda_quantized_matvec_driver_symbol( + model, + "cuModuleUnload", + (void **) &cu_module_unload + ) + && cu_module_unload != NULL) { + (void) cu_module_unload(module); + } + king_inference_cuda_quantized_matvec_set_error( + model, + result, + result != 0 ? "cuModuleGetFunction_batch_failed" : "cuda_quantized_matvec_batch_kernel_unavailable" + ); + return; + } + + model->cuda_quantized_matvec_module = module; + model->cuda_q8_0_matvec_function = function; + model->cuda_quantized_matvec_batch_function = batch_function; + model->cuda_quantized_matvec_module_loaded = true; + model->cuda_quantized_matvec_q8_0_available = true; + model->cuda_quantized_matvec_batch_available = true; + model->cuda_quantized_matvec_available = true; + model->cuda_quantized_matvec_result = 0; + model->cuda_quantized_matvec_error[0] = '\0'; +} + +static zend_result king_inference_cuda_quantized_matvec( + king_inference_model_object *model, + zend_string *tensor_name, + king_inference_cuda_device_ptr input_vector, + zend_ulong input_length, + king_inference_cuda_device_ptr output_vector, + zend_ulong rows +) { + king_inference_cuda_weight_upload *upload; + king_inference_cuda_launch_kernel_fn cu_launch_kernel; + static king_inference_cuda_launch_kernel_fn cached_cu_launch_kernel = NULL; + zval *descriptor; + zval *dimensions; + zend_ulong type; + zend_ulong rank; + zend_ulong cols; + zend_ulong tensor_rows; + king_inference_tensor_type_info type_info; + unsigned int columns32; + unsigned int rows32; + unsigned int type32; + unsigned int block_size32; + unsigned int type_size32; + unsigned int block_x = 128; + unsigned int grid_x; + void *params[8]; + int result; + + if (!model->cuda_quantized_matvec_available || model->cuda_q8_0_matvec_function == NULL) { + king_inference_cuda_quantized_matvec_set_error(model, -1, "cuda_quantized_matvec_unavailable"); + return FAILURE; + } + if (tensor_name == NULL || input_vector == 0 || output_vector == 0) { + king_inference_cuda_quantized_matvec_set_error(model, -1, "cuda_quantized_matvec_argument_invalid"); + return FAILURE; + } + if (input_length == 0 || input_length > UINT_MAX || rows == 0 || rows > UINT_MAX) { + king_inference_cuda_quantized_matvec_set_error(model, -1, "cuda_quantized_matvec_shape_invalid"); + return FAILURE; + } + + descriptor = king_inference_tensor_index_descriptor(model, tensor_name); + dimensions = descriptor != NULL + ? zend_hash_str_find(Z_ARRVAL_P(descriptor), "dimensions", sizeof("dimensions") - 1) + : NULL; + if (descriptor == NULL + || dimensions == NULL + || Z_TYPE_P(dimensions) != IS_ARRAY + || !king_inference_tensor_descriptor_ulong(descriptor, "rank", sizeof("rank") - 1, &rank) + || !king_inference_tensor_descriptor_ulong(descriptor, "type", sizeof("type") - 1, &type) + || !king_inference_tensor_type_lookup(type, &type_info) + || rank != 2 + || !king_inference_cuda_quantized_matvec_type_supported(type) + || king_inference_tensor_dimension_at(dimensions, 0, &cols) != SUCCESS + || king_inference_tensor_dimension_at(dimensions, 1, &tensor_rows) != SUCCESS + || cols != input_length + || rows > tensor_rows) { + king_inference_cuda_quantized_matvec_set_error(model, -1, "cuda_quantized_tensor_descriptor_invalid"); + return FAILURE; + } + + upload = king_inference_cuda_weight_cache_find(model, tensor_name, true); + if (upload == NULL || upload->device_ptr == 0) { + king_inference_cuda_quantized_matvec_set_error(model, -1, "cuda_q8_0_weight_not_uploaded"); + return FAILURE; + } + if (cached_cu_launch_kernel == NULL + && !king_inference_cuda_quantized_matvec_driver_symbol( + model, + "cuLaunchKernel", + (void **) &cached_cu_launch_kernel + )) { + return FAILURE; + } + cu_launch_kernel = cached_cu_launch_kernel; + + columns32 = (unsigned int) input_length; + rows32 = (unsigned int) rows; + type32 = (unsigned int) type; + block_size32 = (unsigned int) type_info.block_size; + type_size32 = (unsigned int) type_info.type_size; + grid_x = type32 == 8u ? (rows32 + 3u) / 4u : rows32; + params[0] = &upload->device_ptr; + params[1] = &input_vector; + params[2] = &output_vector; + params[3] = &columns32; + params[4] = &rows32; + params[5] = &type32; + params[6] = &block_size32; + params[7] = &type_size32; + + result = cu_launch_kernel( + model->cuda_q8_0_matvec_function, + grid_x, + 1, + 1, + block_x, + 1, + 1, + 0, + NULL, + params, + NULL + ); + if (result != 0) { + king_inference_cuda_quantized_matvec_set_error(model, result, "cuLaunchKernel_failed"); + return FAILURE; + } + + model->cuda_quantized_matvec_launch_count++; + model->cuda_quantized_matvec_result = 0; + model->cuda_quantized_matvec_error[0] = '\0'; + return SUCCESS; +} diff --git a/extension/src/inference/cuda/kernels/matvec/cuda_quantized_matvec_batch.inc b/extension/src/inference/cuda/kernels/matvec/cuda_quantized_matvec_batch.inc new file mode 100644 index 000000000..aeb01bd5c --- /dev/null +++ b/extension/src/inference/cuda/kernels/matvec/cuda_quantized_matvec_batch.inc @@ -0,0 +1,128 @@ +/* + * CUDA quantized matrix/batch-vector launcher for prompt prefill. + */ + +static zend_result king_inference_cuda_quantized_matvec_batch( + king_inference_model_object *model, + zend_string *tensor_name, + king_inference_cuda_device_ptr input_matrix, + zend_ulong input_length, + zend_ulong batch_count, + king_inference_cuda_device_ptr output_matrix, + zend_ulong rows +) { + king_inference_cuda_weight_upload *upload; + king_inference_cuda_launch_kernel_fn cu_launch_kernel; + static king_inference_cuda_launch_kernel_fn cached_cu_launch_kernel = NULL; + zval *descriptor; + zval *dimensions; + zend_ulong type; + zend_ulong rank; + zend_ulong cols; + zend_ulong tensor_rows; + king_inference_tensor_type_info type_info; + unsigned int columns32; + unsigned int rows32; + unsigned int batch32; + unsigned int type32; + unsigned int block_size32; + unsigned int type_size32; + unsigned int block_x = 128; + unsigned int grid_x; + void *params[9]; + int result; + + if (!model->cuda_quantized_matvec_batch_available + || model->cuda_quantized_matvec_batch_function == NULL) { + king_inference_cuda_quantized_matvec_set_error(model, -1, "cuda_quantized_matvec_batch_unavailable"); + return FAILURE; + } + if (tensor_name == NULL || input_matrix == 0 || output_matrix == 0) { + king_inference_cuda_quantized_matvec_set_error(model, -1, "cuda_quantized_matvec_batch_argument_invalid"); + return FAILURE; + } + if (input_length == 0 + || input_length > UINT_MAX + || batch_count == 0 + || batch_count > UINT_MAX + || rows == 0 + || rows > UINT_MAX) { + king_inference_cuda_quantized_matvec_set_error(model, -1, "cuda_quantized_matvec_batch_shape_invalid"); + return FAILURE; + } + + descriptor = king_inference_tensor_index_descriptor(model, tensor_name); + dimensions = descriptor != NULL + ? zend_hash_str_find(Z_ARRVAL_P(descriptor), "dimensions", sizeof("dimensions") - 1) + : NULL; + if (descriptor == NULL + || dimensions == NULL + || Z_TYPE_P(dimensions) != IS_ARRAY + || !king_inference_tensor_descriptor_ulong(descriptor, "rank", sizeof("rank") - 1, &rank) + || !king_inference_tensor_descriptor_ulong(descriptor, "type", sizeof("type") - 1, &type) + || !king_inference_tensor_type_lookup(type, &type_info) + || rank != 2 + || !king_inference_cuda_quantized_matvec_type_supported(type) + || king_inference_tensor_dimension_at(dimensions, 0, &cols) != SUCCESS + || king_inference_tensor_dimension_at(dimensions, 1, &tensor_rows) != SUCCESS + || cols != input_length + || rows > tensor_rows) { + king_inference_cuda_quantized_matvec_set_error(model, -1, "cuda_quantized_batch_tensor_descriptor_invalid"); + return FAILURE; + } + + upload = king_inference_cuda_weight_cache_find(model, tensor_name, true); + if (upload == NULL || upload->device_ptr == 0) { + king_inference_cuda_quantized_matvec_set_error(model, -1, "cuda_quantized_batch_weight_not_uploaded"); + return FAILURE; + } + if (cached_cu_launch_kernel == NULL + && !king_inference_cuda_quantized_matvec_driver_symbol( + model, + "cuLaunchKernel", + (void **) &cached_cu_launch_kernel + )) { + return FAILURE; + } + cu_launch_kernel = cached_cu_launch_kernel; + + columns32 = (unsigned int) input_length; + rows32 = (unsigned int) rows; + batch32 = (unsigned int) batch_count; + type32 = (unsigned int) type; + block_size32 = (unsigned int) type_info.block_size; + type_size32 = (unsigned int) type_info.type_size; + grid_x = type32 == 8u ? (rows32 + 3u) / 4u : rows32; + params[0] = &upload->device_ptr; + params[1] = &input_matrix; + params[2] = &output_matrix; + params[3] = &columns32; + params[4] = &rows32; + params[5] = &batch32; + params[6] = &type32; + params[7] = &block_size32; + params[8] = &type_size32; + + result = cu_launch_kernel( + model->cuda_quantized_matvec_batch_function, + grid_x, + batch32, + 1, + block_x, + 1, + 1, + 0, + NULL, + params, + NULL + ); + if (result != 0) { + king_inference_cuda_quantized_matvec_set_error(model, result, "cuLaunchKernel_batch_failed"); + return FAILURE; + } + + model->cuda_quantized_matvec_batch_launch_count++; + model->cuda_quantized_matvec_result = 0; + model->cuda_quantized_matvec_error[0] = '\0'; + return SUCCESS; +} diff --git a/extension/src/inference/cuda/kernels/matvec/cuda_quantized_matvec_status.inc b/extension/src/inference/cuda/kernels/matvec/cuda_quantized_matvec_status.inc new file mode 100644 index 000000000..d8b10e22d --- /dev/null +++ b/extension/src/inference/cuda/kernels/matvec/cuda_quantized_matvec_status.inc @@ -0,0 +1,61 @@ +/* + * CUDA quantized matrix/vector status helpers. + */ + +static void king_inference_cuda_quantized_matvec_add_status( + king_inference_model_object *model, + zval *return_value +) { + zval matvec; + zval supported_types; + + array_init(&matvec); + add_assoc_bool(&matvec, "attempted", model->cuda_quantized_matvec_attempted); + add_assoc_bool(&matvec, "available", model->cuda_quantized_matvec_available); + add_assoc_bool(&matvec, "nvrtc_available", model->cuda_quantized_matvec_nvrtc_available); + add_assoc_bool(&matvec, "module_loaded", model->cuda_quantized_matvec_module_loaded); + add_assoc_bool(&matvec, "q8_0_available", model->cuda_quantized_matvec_q8_0_available); + add_assoc_bool(&matvec, "batch_available", model->cuda_quantized_matvec_batch_available); + add_assoc_long(&matvec, "launches", (zend_long) model->cuda_quantized_matvec_launch_count); + add_assoc_long(&matvec, "batch_launches", (zend_long) model->cuda_quantized_matvec_batch_launch_count); + add_assoc_long(&matvec, "result", model->cuda_quantized_matvec_result); + add_assoc_string(&matvec, "error", model->cuda_quantized_matvec_error); + + array_init(&supported_types); + add_next_index_string(&supported_types, "Q8_0"); + add_next_index_string(&supported_types, "Q5_0"); + add_next_index_string(&supported_types, "Q4_K"); + add_next_index_string(&supported_types, "Q6_K"); + add_assoc_zval(&matvec, "supported_types", &supported_types); + add_assoc_zval(return_value, "quantized_matvec", &matvec); +} + +static void king_inference_cuda_quantized_matvec_apply_status( + king_inference_model_object *model, + zval *return_value +) { + zval *reason; + bool reason_can_be_matvec; + + king_inference_cuda_quantized_matvec_add_status(model, return_value); + if (!model->cuda_quantized_matvec_attempted + || model->cuda_quantized_matvec_available + || !model->cuda_weight_upload_complete) { + return; + } + + reason = zend_hash_str_find(Z_ARRVAL_P(return_value), "reason", sizeof("reason") - 1); + reason_can_be_matvec = reason == NULL + || Z_TYPE_P(reason) != IS_STRING + || zend_string_equals_literal(Z_STR_P(reason), "gpu_ready") + || zend_string_equals_literal(Z_STR_P(reason), "gpu_decoder_kernel_not_implemented"); + if (reason_can_be_matvec) { + king_inference_cuda_context_prepend_return_reason( + return_value, + "gpu_quantized_matvec_unavailable" + ); + add_assoc_string(return_value, "reason", "gpu_quantized_matvec_unavailable"); + } else { + king_inference_gpu_status_append_return_reason(return_value, "gpu_quantized_matvec_unavailable"); + } +} diff --git a/extension/src/inference/cuda/kernels/norm/cuda_rms_norm.inc b/extension/src/inference/cuda/kernels/norm/cuda_rms_norm.inc new file mode 100644 index 000000000..6820dad6a --- /dev/null +++ b/extension/src/inference/cuda/kernels/norm/cuda_rms_norm.inc @@ -0,0 +1,670 @@ +/* + * CUDA RMSNorm kernel for native King GPU inference. + */ + +typedef int king_inference_rms_nvrtc_result; +typedef void *king_inference_rms_nvrtc_program; + +typedef king_inference_rms_nvrtc_result (*king_inference_rms_nvrtc_create_program_fn)( + king_inference_rms_nvrtc_program *program, + const char *source, + const char *name, + int header_count, + const char * const *headers, + const char * const *include_names +); +typedef king_inference_rms_nvrtc_result (*king_inference_rms_nvrtc_compile_program_fn)( + king_inference_rms_nvrtc_program program, + int option_count, + const char * const *options +); +typedef king_inference_rms_nvrtc_result (*king_inference_rms_nvrtc_get_ptx_size_fn)( + king_inference_rms_nvrtc_program program, + size_t *ptx_size +); +typedef king_inference_rms_nvrtc_result (*king_inference_rms_nvrtc_get_ptx_fn)( + king_inference_rms_nvrtc_program program, + char *ptx +); +typedef king_inference_rms_nvrtc_result (*king_inference_rms_nvrtc_destroy_program_fn)( + king_inference_rms_nvrtc_program *program +); + +static const char king_inference_cuda_rms_norm_source[] = +"extern \"C\" __device__ float king_rms_half_to_float(unsigned short h) {\n" +" unsigned int sign = ((unsigned int) (h & 0x8000u)) << 16;\n" +" unsigned int mantissa = (unsigned int) (h & 0x03ffu);\n" +" int exponent = (int) ((h >> 10) & 0x1fu);\n" +" unsigned int bits;\n" +" if (exponent == 0) {\n" +" if (mantissa == 0u) {\n" +" bits = sign;\n" +" } else {\n" +" while ((mantissa & 0x0400u) == 0u) {\n" +" mantissa <<= 1;\n" +" exponent--;\n" +" }\n" +" exponent++;\n" +" mantissa &= 0x03ffu;\n" +" bits = sign | ((unsigned int) (exponent + 112) << 23) | (mantissa << 13);\n" +" }\n" +" return __uint_as_float(bits);\n" +" }\n" +" if (exponent == 31) {\n" +" bits = sign | 0x7f800000u | (mantissa << 13);\n" +" return __uint_as_float(bits);\n" +" }\n" +" bits = sign | ((unsigned int) (exponent + 112) << 23) | (mantissa << 13);\n" +" return __uint_as_float(bits);\n" +"}\n" +"extern \"C\" __device__ float king_rms_norm_weight(\n" +" const unsigned char *weight,\n" +" unsigned int index,\n" +" unsigned int weight_type\n" +") {\n" +" if (weight_type == 0u) {\n" +" return ((const float *) weight)[index];\n" +" }\n" +" const unsigned char *raw = weight + ((unsigned long long) index * 2ull);\n" +" unsigned short h = (unsigned short) raw[0] | ((unsigned short) raw[1] << 8);\n" +" return king_rms_half_to_float(h);\n" +"}\n" +"extern \"C\" __global__ void king_rms_norm(\n" +" const float *input,\n" +" const unsigned char *weight,\n" +" float *output,\n" +" unsigned int length,\n" +" float epsilon,\n" +" unsigned int weight_type\n" +") {\n" +" unsigned int lane_id = threadIdx.x;\n" +" __shared__ float partials[256];\n" +" float sum = 0.0f;\n" +" for (unsigned int i = lane_id; i < length; i += blockDim.x) {\n" +" float value = input[i];\n" +" sum += value * value;\n" +" }\n" +" partials[lane_id] = sum;\n" +" __syncthreads();\n" +" for (unsigned int stride = blockDim.x >> 1; stride > 0u; stride >>= 1) {\n" +" if (lane_id < stride) {\n" +" partials[lane_id] += partials[lane_id + stride];\n" +" }\n" +" __syncthreads();\n" +" }\n" +" float scale = rsqrtf((partials[0] / (float) length) + epsilon);\n" +" for (unsigned int i = lane_id; i < length; i += blockDim.x) {\n" +" output[i] = input[i] * scale * king_rms_norm_weight(weight, i, weight_type);\n" +" }\n" +"}\n" +"extern \"C\" __global__ void king_rms_norm_batch(\n" +" const float *input,\n" +" const unsigned char *weight,\n" +" float *output,\n" +" unsigned int length,\n" +" unsigned int batch_count,\n" +" float epsilon,\n" +" unsigned int weight_type\n" +") {\n" +" unsigned int batch = blockIdx.x;\n" +" unsigned int lane_id = threadIdx.x;\n" +" const float *row_input;\n" +" float *row_output;\n" +" __shared__ float partials[256];\n" +" float sum = 0.0f;\n" +" if (batch >= batch_count || length == 0u) {\n" +" return;\n" +" }\n" +" row_input = input + ((unsigned long long) batch * (unsigned long long) length);\n" +" row_output = output + ((unsigned long long) batch * (unsigned long long) length);\n" +" for (unsigned int i = lane_id; i < length; i += blockDim.x) {\n" +" float value = row_input[i];\n" +" sum += value * value;\n" +" }\n" +" partials[lane_id] = sum;\n" +" __syncthreads();\n" +" for (unsigned int stride = blockDim.x >> 1; stride > 0u; stride >>= 1) {\n" +" if (lane_id < stride) {\n" +" partials[lane_id] += partials[lane_id + stride];\n" +" }\n" +" __syncthreads();\n" +" }\n" +" float scale = rsqrtf((partials[0] / (float) length) + epsilon);\n" +" for (unsigned int i = lane_id; i < length; i += blockDim.x) {\n" +" row_output[i] = row_input[i] * scale * king_rms_norm_weight(weight, i, weight_type);\n" +" }\n" +"}\n"; + +static void king_inference_cuda_rms_norm_reset_fields(king_inference_model_object *model) +{ + model->cuda_rms_norm_nvrtc_handle = NULL; + model->cuda_rms_norm_module = NULL; + model->cuda_rms_norm_function = NULL; + model->cuda_rms_norm_batch_function = NULL; + model->cuda_rms_norm_result = 0; + model->cuda_rms_norm_error[0] = '\0'; + model->cuda_rms_norm_launch_count = 0; + model->cuda_rms_norm_batch_launch_count = 0; + model->cuda_rms_norm_attempted = false; + model->cuda_rms_norm_available = false; + model->cuda_rms_norm_nvrtc_available = false; + model->cuda_rms_norm_module_loaded = false; + model->cuda_rms_norm_f32_available = false; + model->cuda_rms_norm_batch_available = false; +} + +static void king_inference_cuda_rms_norm_set_error( + king_inference_model_object *model, + int result, + const char *message +) { + if (message == NULL || message[0] == '\0') { + message = "unknown_cuda_rms_norm_error"; + } + + model->cuda_rms_norm_result = result; + snprintf(model->cuda_rms_norm_error, sizeof(model->cuda_rms_norm_error), "%s", message); +} + +static bool king_inference_cuda_rms_norm_driver_symbol( + king_inference_model_object *model, + const char *name, + void **symbol +) { + *symbol = NULL; + if (model->cuda_driver_handle == NULL) { + king_inference_cuda_rms_norm_set_error(model, -1, "cuda_driver_handle_unavailable"); + return false; + } + + *symbol = dlsym(model->cuda_driver_handle, name); + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_rms_norm_set_error(model, -1, name); + return false; +} + +static bool king_inference_cuda_rms_norm_nvrtc_symbol( + king_inference_model_object *model, + const char *name, + void **symbol +) { + *symbol = NULL; + if (model->cuda_rms_norm_nvrtc_handle == NULL) { + king_inference_cuda_rms_norm_set_error(model, -1, "nvrtc_handle_unavailable"); + return false; + } + + *symbol = dlsym(model->cuda_rms_norm_nvrtc_handle, name); + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_rms_norm_set_error(model, -1, name); + return false; +} + +static bool king_inference_cuda_rms_norm_open_nvrtc(king_inference_model_object *model) +{ + static const char *libraries[] = { + "libnvrtc.so.12", + "libnvrtc.so.11.2", + "libnvrtc.so.11.0", + "libnvrtc.so" + }; + size_t index; + + for (index = 0; index < sizeof(libraries) / sizeof(libraries[0]); index++) { + model->cuda_rms_norm_nvrtc_handle = dlopen(libraries[index], RTLD_LAZY | RTLD_LOCAL); + if (model->cuda_rms_norm_nvrtc_handle != NULL) { + model->cuda_rms_norm_nvrtc_available = true; + return true; + } + } + + king_inference_cuda_rms_norm_set_error(model, -1, "libnvrtc_unavailable"); + return false; +} + +static bool king_inference_cuda_rms_norm_compile_ptx( + king_inference_model_object *model, + char **ptx, + size_t *ptx_size +) { + king_inference_rms_nvrtc_create_program_fn nvrtc_create_program; + king_inference_rms_nvrtc_compile_program_fn nvrtc_compile_program; + king_inference_rms_nvrtc_get_ptx_size_fn nvrtc_get_ptx_size; + king_inference_rms_nvrtc_get_ptx_fn nvrtc_get_ptx; + king_inference_rms_nvrtc_destroy_program_fn nvrtc_destroy_program; + king_inference_rms_nvrtc_program program = NULL; + const char *options[] = {"--std=c++11", "--gpu-architecture=compute_89", "--use_fast_math"}; + king_inference_rms_nvrtc_result result; + + *ptx = NULL; + *ptx_size = 0; + + if (!king_inference_cuda_rms_norm_nvrtc_symbol(model, "nvrtcCreateProgram", (void **) &nvrtc_create_program) + || !king_inference_cuda_rms_norm_nvrtc_symbol(model, "nvrtcCompileProgram", (void **) &nvrtc_compile_program) + || !king_inference_cuda_rms_norm_nvrtc_symbol(model, "nvrtcGetPTXSize", (void **) &nvrtc_get_ptx_size) + || !king_inference_cuda_rms_norm_nvrtc_symbol(model, "nvrtcGetPTX", (void **) &nvrtc_get_ptx) + || !king_inference_cuda_rms_norm_nvrtc_symbol(model, "nvrtcDestroyProgram", (void **) &nvrtc_destroy_program)) { + return false; + } + + result = nvrtc_create_program( + &program, + king_inference_cuda_rms_norm_source, + "king_rms_norm.cu", + 0, + NULL, + NULL + ); + if (result != 0 || program == NULL) { + king_inference_cuda_rms_norm_set_error( + model, + result, + result != 0 ? "nvrtcCreateProgram_failed" : "nvrtcProgram_unavailable" + ); + return false; + } + + result = nvrtc_compile_program(program, 3, options); + if (result != 0) { + (void) nvrtc_destroy_program(&program); + king_inference_cuda_rms_norm_set_error(model, result, "nvrtcCompileProgram_failed"); + return false; + } + + result = nvrtc_get_ptx_size(program, ptx_size); + if (result != 0 || *ptx_size == 0) { + (void) nvrtc_destroy_program(&program); + king_inference_cuda_rms_norm_set_error( + model, + result, + result != 0 ? "nvrtcGetPTXSize_failed" : "nvrtc_ptx_empty" + ); + return false; + } + + *ptx = emalloc(*ptx_size); + result = nvrtc_get_ptx(program, *ptx); + (void) nvrtc_destroy_program(&program); + if (result != 0) { + efree(*ptx); + *ptx = NULL; + *ptx_size = 0; + king_inference_cuda_rms_norm_set_error(model, result, "nvrtcGetPTX_failed"); + return false; + } + + return true; +} + +static void king_inference_cuda_rms_norm_release(king_inference_model_object *model) +{ + king_inference_cuda_module_unload_fn cu_module_unload; + + if (model->cuda_rms_norm_module != NULL + && model->cuda_driver_handle != NULL + && model->cuda_context != NULL + && king_inference_cuda_device_allocator_set_current(model) + && king_inference_cuda_rms_norm_driver_symbol(model, "cuModuleUnload", (void **) &cu_module_unload)) { + (void) cu_module_unload(model->cuda_rms_norm_module); + } + + if (model->cuda_rms_norm_nvrtc_handle != NULL) { + dlclose(model->cuda_rms_norm_nvrtc_handle); + } + + king_inference_cuda_rms_norm_reset_fields(model); +} + +static void king_inference_cuda_rms_norm_init( + king_inference_model_object *model, + king_inference_backend_kind backend_kind +) { + king_inference_cuda_module_load_data_fn cu_module_load_data; + king_inference_cuda_module_get_function_fn cu_module_get_function; + char *ptx = NULL; + size_t ptx_size = 0; + void *module = NULL; + void *function = NULL; + void *batch_function = NULL; + int result; + + king_inference_cuda_rms_norm_release(model); + if (backend_kind != KING_INFERENCE_BACKEND_KING_NATIVE_GPU + || !king_inference_gpu_enabled(&model->config)) { + return; + } + + model->cuda_rms_norm_attempted = true; + if (!model->cuda_weight_cache_ready) { + king_inference_cuda_rms_norm_set_error(model, -1, "cuda_weight_cache_unavailable"); + return; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + king_inference_cuda_rms_norm_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_context_current_failed" + ); + return; + } + if (!king_inference_cuda_rms_norm_open_nvrtc(model) + || !king_inference_cuda_rms_norm_compile_ptx(model, &ptx, &ptx_size)) { + return; + } + if (!king_inference_cuda_rms_norm_driver_symbol(model, "cuModuleLoadData", (void **) &cu_module_load_data) + || !king_inference_cuda_rms_norm_driver_symbol(model, "cuModuleGetFunction", (void **) &cu_module_get_function)) { + efree(ptx); + return; + } + + result = cu_module_load_data(&module, ptx); + efree(ptx); + if (result != 0 || module == NULL) { + king_inference_cuda_rms_norm_set_error( + model, + result, + result != 0 ? "cuModuleLoadData_failed" : "cuda_module_unavailable" + ); + return; + } + + result = cu_module_get_function(&function, module, "king_rms_norm"); + if (result != 0 || function == NULL) { + king_inference_cuda_module_unload_fn cu_module_unload = NULL; + + if (king_inference_cuda_rms_norm_driver_symbol(model, "cuModuleUnload", (void **) &cu_module_unload) + && cu_module_unload != NULL) { + (void) cu_module_unload(module); + } + king_inference_cuda_rms_norm_set_error( + model, + result, + result != 0 ? "cuModuleGetFunction_failed" : "cuda_rms_norm_kernel_unavailable" + ); + return; + } + result = cu_module_get_function(&batch_function, module, "king_rms_norm_batch"); + if (result != 0 || batch_function == NULL) { + king_inference_cuda_module_unload_fn cu_module_unload = NULL; + + if (king_inference_cuda_rms_norm_driver_symbol(model, "cuModuleUnload", (void **) &cu_module_unload) + && cu_module_unload != NULL) { + (void) cu_module_unload(module); + } + king_inference_cuda_rms_norm_set_error( + model, + result, + result != 0 ? "cuModuleGetFunction_batch_failed" : "cuda_rms_norm_batch_kernel_unavailable" + ); + return; + } + + model->cuda_rms_norm_module = module; + model->cuda_rms_norm_function = function; + model->cuda_rms_norm_batch_function = batch_function; + model->cuda_rms_norm_module_loaded = true; + model->cuda_rms_norm_f32_available = true; + model->cuda_rms_norm_batch_available = true; + model->cuda_rms_norm_available = true; + model->cuda_rms_norm_result = 0; + model->cuda_rms_norm_error[0] = '\0'; +} + +static zend_result king_inference_cuda_rms_norm_f32( + king_inference_model_object *model, + zend_string *weight_name, + king_inference_cuda_device_ptr input_vector, + king_inference_cuda_device_ptr output_vector, + zend_ulong length, + double epsilon +) { + king_inference_cuda_weight_upload *upload; + king_inference_cuda_launch_kernel_fn cu_launch_kernel; + static king_inference_cuda_launch_kernel_fn cached_cu_launch_kernel = NULL; + zval *descriptor; + zval *dimensions; + zend_ulong type; + zend_ulong rank; + zend_ulong width; + unsigned int length32; + unsigned int weight_type32; + unsigned int block_x = 256; + float epsilon32 = (float) epsilon; + void *params[6]; + int result; + + if (!model->cuda_rms_norm_available || model->cuda_rms_norm_function == NULL) { + king_inference_cuda_rms_norm_set_error(model, -1, "cuda_rms_norm_unavailable"); + return FAILURE; + } + if (weight_name == NULL || input_vector == 0 || output_vector == 0) { + king_inference_cuda_rms_norm_set_error(model, -1, "cuda_rms_norm_argument_invalid"); + return FAILURE; + } + if (length == 0 || length > UINT_MAX) { + king_inference_cuda_rms_norm_set_error(model, -1, "cuda_rms_norm_shape_invalid"); + return FAILURE; + } + + descriptor = king_inference_tensor_index_descriptor(model, weight_name); + dimensions = descriptor != NULL + ? zend_hash_str_find(Z_ARRVAL_P(descriptor), "dimensions", sizeof("dimensions") - 1) + : NULL; + if (descriptor == NULL + || dimensions == NULL + || Z_TYPE_P(dimensions) != IS_ARRAY + || !king_inference_tensor_descriptor_ulong(descriptor, "rank", sizeof("rank") - 1, &rank) + || !king_inference_tensor_descriptor_ulong(descriptor, "type", sizeof("type") - 1, &type) + || rank != 1 + || (type != 0 && type != 1) + || king_inference_tensor_dimension_at(dimensions, 0, &width) != SUCCESS + || width != length) { + king_inference_cuda_rms_norm_set_error(model, -1, "cuda_rms_norm_tensor_descriptor_invalid"); + return FAILURE; + } + + upload = king_inference_cuda_weight_cache_find(model, weight_name, true); + if (upload == NULL || upload->device_ptr == 0) { + king_inference_cuda_rms_norm_set_error(model, -1, "cuda_rms_norm_weight_not_uploaded"); + return FAILURE; + } + if (cached_cu_launch_kernel == NULL + && !king_inference_cuda_rms_norm_driver_symbol(model, "cuLaunchKernel", (void **) &cached_cu_launch_kernel)) { + return FAILURE; + } + cu_launch_kernel = cached_cu_launch_kernel; + + length32 = (unsigned int) length; + weight_type32 = type == 0 ? 0u : 1u; + params[0] = &input_vector; + params[1] = &upload->device_ptr; + params[2] = &output_vector; + params[3] = &length32; + params[4] = &epsilon32; + params[5] = &weight_type32; + + result = cu_launch_kernel( + model->cuda_rms_norm_function, + 1, + 1, + 1, + block_x, + 1, + 1, + 0, + NULL, + params, + NULL + ); + if (result != 0) { + king_inference_cuda_rms_norm_set_error(model, result, "cuLaunchKernel_failed"); + return FAILURE; + } + + model->cuda_rms_norm_launch_count++; + model->cuda_rms_norm_result = 0; + model->cuda_rms_norm_error[0] = '\0'; + return SUCCESS; +} + +static zend_result king_inference_cuda_rms_norm_f32_batch( + king_inference_model_object *model, + zend_string *weight_name, + king_inference_cuda_device_ptr input_matrix, + king_inference_cuda_device_ptr output_matrix, + zend_ulong length, + zend_ulong batch_count, + double epsilon +) { + king_inference_cuda_weight_upload *upload; + king_inference_cuda_launch_kernel_fn cu_launch_kernel; + static king_inference_cuda_launch_kernel_fn cached_cu_launch_kernel = NULL; + zval *descriptor; + zval *dimensions; + zend_ulong type; + zend_ulong rank; + zend_ulong width; + unsigned int length32; + unsigned int batch32; + unsigned int weight_type32; + unsigned int block_x = 256; + float epsilon32 = (float) epsilon; + void *params[7]; + int result; + + if (!model->cuda_rms_norm_batch_available || model->cuda_rms_norm_batch_function == NULL) { + king_inference_cuda_rms_norm_set_error(model, -1, "cuda_rms_norm_batch_unavailable"); + return FAILURE; + } + if (weight_name == NULL || input_matrix == 0 || output_matrix == 0) { + king_inference_cuda_rms_norm_set_error(model, -1, "cuda_rms_norm_batch_argument_invalid"); + return FAILURE; + } + if (length == 0 || length > UINT_MAX || batch_count == 0 || batch_count > UINT_MAX) { + king_inference_cuda_rms_norm_set_error(model, -1, "cuda_rms_norm_batch_shape_invalid"); + return FAILURE; + } + + descriptor = king_inference_tensor_index_descriptor(model, weight_name); + dimensions = descriptor != NULL + ? zend_hash_str_find(Z_ARRVAL_P(descriptor), "dimensions", sizeof("dimensions") - 1) + : NULL; + if (descriptor == NULL + || dimensions == NULL + || Z_TYPE_P(dimensions) != IS_ARRAY + || !king_inference_tensor_descriptor_ulong(descriptor, "rank", sizeof("rank") - 1, &rank) + || !king_inference_tensor_descriptor_ulong(descriptor, "type", sizeof("type") - 1, &type) + || rank != 1 + || (type != 0 && type != 1) + || king_inference_tensor_dimension_at(dimensions, 0, &width) != SUCCESS + || width != length) { + king_inference_cuda_rms_norm_set_error(model, -1, "cuda_rms_norm_batch_tensor_descriptor_invalid"); + return FAILURE; + } + + upload = king_inference_cuda_weight_cache_find(model, weight_name, true); + if (upload == NULL || upload->device_ptr == 0) { + king_inference_cuda_rms_norm_set_error(model, -1, "cuda_rms_norm_batch_weight_not_uploaded"); + return FAILURE; + } + if (cached_cu_launch_kernel == NULL + && !king_inference_cuda_rms_norm_driver_symbol(model, "cuLaunchKernel", (void **) &cached_cu_launch_kernel)) { + return FAILURE; + } + cu_launch_kernel = cached_cu_launch_kernel; + + length32 = (unsigned int) length; + batch32 = (unsigned int) batch_count; + weight_type32 = type == 0 ? 0u : 1u; + params[0] = &input_matrix; + params[1] = &upload->device_ptr; + params[2] = &output_matrix; + params[3] = &length32; + params[4] = &batch32; + params[5] = &epsilon32; + params[6] = &weight_type32; + + result = cu_launch_kernel( + model->cuda_rms_norm_batch_function, + batch32, + 1, + 1, + block_x, + 1, + 1, + 0, + NULL, + params, + NULL + ); + if (result != 0) { + king_inference_cuda_rms_norm_set_error(model, result, "cuLaunchKernel_batch_failed"); + return FAILURE; + } + + model->cuda_rms_norm_batch_launch_count++; + model->cuda_rms_norm_result = 0; + model->cuda_rms_norm_error[0] = '\0'; + return SUCCESS; +} + +static void king_inference_cuda_rms_norm_add_status( + king_inference_model_object *model, + zval *return_value +) { + zval rms_norm; + zval supported_weight_types; + + array_init(&rms_norm); + add_assoc_bool(&rms_norm, "attempted", model->cuda_rms_norm_attempted); + add_assoc_bool(&rms_norm, "available", model->cuda_rms_norm_available); + add_assoc_bool(&rms_norm, "nvrtc_available", model->cuda_rms_norm_nvrtc_available); + add_assoc_bool(&rms_norm, "module_loaded", model->cuda_rms_norm_module_loaded); + add_assoc_bool(&rms_norm, "f32_available", model->cuda_rms_norm_f32_available); + add_assoc_bool(&rms_norm, "batch_available", model->cuda_rms_norm_batch_available); + add_assoc_long(&rms_norm, "launches", (zend_long) model->cuda_rms_norm_launch_count); + add_assoc_long(&rms_norm, "batch_launches", (zend_long) model->cuda_rms_norm_batch_launch_count); + add_assoc_long(&rms_norm, "result", model->cuda_rms_norm_result); + add_assoc_string(&rms_norm, "error", model->cuda_rms_norm_error); + + array_init(&supported_weight_types); + add_next_index_string(&supported_weight_types, "F32"); + add_next_index_string(&supported_weight_types, "F16"); + add_assoc_zval(&rms_norm, "supported_weight_types", &supported_weight_types); + add_assoc_zval(return_value, "rms_norm_kernel", &rms_norm); +} + +static void king_inference_cuda_rms_norm_apply_status( + king_inference_model_object *model, + zval *return_value +) { + zval *reason; + bool reason_can_be_rms_norm; + + king_inference_cuda_rms_norm_add_status(model, return_value); + if (!model->cuda_rms_norm_attempted + || model->cuda_rms_norm_available + || !model->cuda_weight_upload_complete) { + return; + } + + reason = zend_hash_str_find(Z_ARRVAL_P(return_value), "reason", sizeof("reason") - 1); + reason_can_be_rms_norm = reason == NULL + || Z_TYPE_P(reason) != IS_STRING + || zend_string_equals_literal(Z_STR_P(reason), "gpu_ready") + || zend_string_equals_literal(Z_STR_P(reason), "gpu_decoder_kernel_not_implemented"); + if (reason_can_be_rms_norm) { + king_inference_cuda_context_prepend_return_reason(return_value, "gpu_rms_norm_kernel_unavailable"); + add_assoc_string(return_value, "reason", "gpu_rms_norm_kernel_unavailable"); + } else { + king_inference_gpu_status_append_return_reason(return_value, "gpu_rms_norm_kernel_unavailable"); + } +} diff --git a/extension/src/inference/cuda/kernels/projection/cuda_logits_readback.inc b/extension/src/inference/cuda/kernels/projection/cuda_logits_readback.inc new file mode 100644 index 000000000..9cd644bc5 --- /dev/null +++ b/extension/src/inference/cuda/kernels/projection/cuda_logits_readback.inc @@ -0,0 +1,726 @@ +/* + * CUDA bounded logits readback for native King GPU inference. + */ + +#define KING_INFERENCE_CUDA_LOGITS_READBACK_MAX_CANDIDATES 64u + +typedef int king_inference_logits_readback_nvrtc_result; +typedef void *king_inference_logits_readback_nvrtc_program; + +typedef king_inference_logits_readback_nvrtc_result (*king_inference_logits_readback_nvrtc_create_program_fn)( + king_inference_logits_readback_nvrtc_program *program, + const char *source, + const char *name, + int header_count, + const char * const *headers, + const char * const *include_names +); +typedef king_inference_logits_readback_nvrtc_result (*king_inference_logits_readback_nvrtc_compile_program_fn)( + king_inference_logits_readback_nvrtc_program program, + int option_count, + const char * const *options +); +typedef king_inference_logits_readback_nvrtc_result (*king_inference_logits_readback_nvrtc_get_ptx_size_fn)( + king_inference_logits_readback_nvrtc_program program, + size_t *ptx_size +); +typedef king_inference_logits_readback_nvrtc_result (*king_inference_logits_readback_nvrtc_get_ptx_fn)( + king_inference_logits_readback_nvrtc_program program, + char *ptx +); +typedef king_inference_logits_readback_nvrtc_result (*king_inference_logits_readback_nvrtc_destroy_program_fn)( + king_inference_logits_readback_nvrtc_program *program +); +typedef int (*king_inference_cuda_logits_memcpy_dtoh_fn)( + void *dst, + king_inference_cuda_device_ptr src, + size_t bytes +); + +static const char king_inference_cuda_logits_readback_source[] = +"extern \"C\" __global__ void king_logits_top_k(\n" +" const float *logits,\n" +" unsigned int vocab_size,\n" +" unsigned int candidate_limit,\n" +" unsigned int *candidate_indices,\n" +" float *candidate_logits\n" +") {\n" +" unsigned int lane_id = threadIdx.x;\n" +" unsigned int limit = candidate_limit > 64u ? 64u : candidate_limit;\n" +" __shared__ float thread_scores[256];\n" +" __shared__ unsigned int thread_indices[256];\n" +" __shared__ unsigned int selected_indices[64];\n" +" if (vocab_size == 0u || limit == 0u) {\n" +" return;\n" +" }\n" +" for (unsigned int rank = 0u; rank < limit; rank++) {\n" +" float best_score = -3.402823466e+38F;\n" +" unsigned int best_index = 0xffffffffu;\n" +" for (unsigned int index = lane_id; index < vocab_size; index += blockDim.x) {\n" +" bool already_selected = false;\n" +" for (unsigned int selected = 0u; selected < rank; selected++) {\n" +" if (selected_indices[selected] == index) {\n" +" already_selected = true;\n" +" break;\n" +" }\n" +" }\n" +" if (!already_selected) {\n" +" float score = logits[index];\n" +" if (score > best_score || (score == best_score && index < best_index)) {\n" +" best_score = score;\n" +" best_index = index;\n" +" }\n" +" }\n" +" }\n" +" thread_scores[lane_id] = best_score;\n" +" thread_indices[lane_id] = best_index;\n" +" __syncthreads();\n" +" for (unsigned int stride = blockDim.x >> 1; stride > 0u; stride >>= 1) {\n" +" if (lane_id < stride) {\n" +" float right_score = thread_scores[lane_id + stride];\n" +" unsigned int right_index = thread_indices[lane_id + stride];\n" +" if (right_score > thread_scores[lane_id]\n" +" || (right_score == thread_scores[lane_id] && right_index < thread_indices[lane_id])) {\n" +" thread_scores[lane_id] = right_score;\n" +" thread_indices[lane_id] = right_index;\n" +" }\n" +" }\n" +" __syncthreads();\n" +" }\n" +" if (lane_id == 0u) {\n" +" selected_indices[rank] = thread_indices[0];\n" +" candidate_indices[rank] = thread_indices[0];\n" +" candidate_logits[rank] = thread_scores[0];\n" +" }\n" +" __syncthreads();\n" +" }\n" +"}\n"; + +static void king_inference_cuda_logits_readback_reset_fields(king_inference_model_object *model) +{ + model->cuda_logits_readback_nvrtc_handle = NULL; + model->cuda_logits_readback_module = NULL; + model->cuda_logits_top_k_function = NULL; + model->cuda_logits_readback_result = 0; + model->cuda_logits_readback_error[0] = '\0'; + model->cuda_logits_readback_launch_count = 0; + model->cuda_logits_readback_sync_count = 0; + model->cuda_logits_readback_dtoh_count = 0; + model->cuda_logits_readback_candidate_limit = KING_INFERENCE_CUDA_LOGITS_READBACK_MAX_CANDIDATES; + model->cuda_logits_readback_last_candidate_count = 0; + model->cuda_logits_readback_last_bytes = 0; + model->cuda_logits_readback_full_bytes = 0; + model->cuda_logits_readback_saved_bytes = 0; + model->cuda_logits_readback_device_indices = 0; + model->cuda_logits_readback_device_logits = 0; + model->cuda_logits_readback_device_indices_bytes = 0; + model->cuda_logits_readback_device_logits_bytes = 0; + model->cuda_logits_readback_buffer_reuse_count = 0; + model->cuda_logits_readback_last_ns = 0; + model->cuda_logits_readback_synthetic_vocab_size = 0; + model->cuda_logits_readback_synthetic_candidate_count = 0; + model->cuda_logits_readback_synthetic_matched_count = 0; + model->cuda_logits_readback_synthetic_rank_matched_count = 0; + model->cuda_logits_readback_synthetic_token_id_matched_count = 0; + model->cuda_logits_readback_synthetic_logit_matched_count = 0; + model->cuda_logits_readback_attempted = false; + model->cuda_logits_readback_available = false; + model->cuda_logits_readback_nvrtc_available = false; + model->cuda_logits_readback_module_loaded = false; + model->cuda_logits_readback_top_k_available = false; + model->cuda_logits_readback_bounded_cpu_available = false; + model->cuda_logits_readback_synthetic_ordering_checked = false; + model->cuda_logits_readback_synthetic_ordering_matched = false; +} + +static uint64_t king_inference_cuda_logits_readback_profile_ns(void) +{ + struct timespec ts; + + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) { + return (uint64_t) time(NULL) * 1000000000ULL; + } + return ((uint64_t) ts.tv_sec * 1000000000ULL) + (uint64_t) ts.tv_nsec; +} + +static zend_long king_inference_cuda_logits_readback_profile_elapsed(uint64_t end, uint64_t start) +{ + return end >= start && end - start <= (uint64_t) ZEND_LONG_MAX + ? (zend_long) (end - start) + : 0; +} + +static void king_inference_cuda_logits_readback_set_error( + king_inference_model_object *model, + int result, + const char *message +) { + if (message == NULL || message[0] == '\0') { + message = "unknown_cuda_logits_readback_error"; + } + + model->cuda_logits_readback_result = result; + snprintf( + model->cuda_logits_readback_error, + sizeof(model->cuda_logits_readback_error), + "%s", + message + ); +} + +static zend_result king_inference_cuda_logits_readback_synthetic_ordering_check( + king_inference_model_object *model +); + +static bool king_inference_cuda_logits_readback_driver_symbol( + king_inference_model_object *model, + const char *primary_name, + const char *fallback_name, + void **symbol +) { + *symbol = NULL; + if (model->cuda_driver_handle == NULL) { + king_inference_cuda_logits_readback_set_error(model, -1, "cuda_driver_handle_unavailable"); + return false; + } + + if (primary_name != NULL) { + *symbol = dlsym(model->cuda_driver_handle, primary_name); + } + if (*symbol == NULL && fallback_name != NULL) { + *symbol = dlsym(model->cuda_driver_handle, fallback_name); + } + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_logits_readback_set_error( + model, + -1, + primary_name != NULL ? primary_name : fallback_name + ); + return false; +} + +static bool king_inference_cuda_logits_readback_nvrtc_symbol( + king_inference_model_object *model, + const char *name, + void **symbol +) { + *symbol = NULL; + if (model->cuda_logits_readback_nvrtc_handle == NULL) { + king_inference_cuda_logits_readback_set_error(model, -1, "nvrtc_handle_unavailable"); + return false; + } + + *symbol = dlsym(model->cuda_logits_readback_nvrtc_handle, name); + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_logits_readback_set_error(model, -1, name); + return false; +} + +static bool king_inference_cuda_logits_readback_open_nvrtc(king_inference_model_object *model) +{ + static const char *libraries[] = { + "libnvrtc.so.12", + "libnvrtc.so.11.2", + "libnvrtc.so.11.0", + "libnvrtc.so" + }; + size_t index; + + for (index = 0; index < sizeof(libraries) / sizeof(libraries[0]); index++) { + model->cuda_logits_readback_nvrtc_handle = dlopen(libraries[index], RTLD_LAZY | RTLD_LOCAL); + if (model->cuda_logits_readback_nvrtc_handle != NULL) { + model->cuda_logits_readback_nvrtc_available = true; + return true; + } + } + + king_inference_cuda_logits_readback_set_error(model, -1, "libnvrtc_unavailable"); + return false; +} + +static bool king_inference_cuda_logits_readback_compile_ptx( + king_inference_model_object *model, + char **ptx, + size_t *ptx_size +) { + king_inference_logits_readback_nvrtc_create_program_fn nvrtc_create_program; + king_inference_logits_readback_nvrtc_compile_program_fn nvrtc_compile_program; + king_inference_logits_readback_nvrtc_get_ptx_size_fn nvrtc_get_ptx_size; + king_inference_logits_readback_nvrtc_get_ptx_fn nvrtc_get_ptx; + king_inference_logits_readback_nvrtc_destroy_program_fn nvrtc_destroy_program; + king_inference_logits_readback_nvrtc_program program = NULL; + const char *options[] = {"--std=c++11", "--gpu-architecture=compute_89", "--use_fast_math"}; + king_inference_logits_readback_nvrtc_result result; + + *ptx = NULL; + *ptx_size = 0; + if (!king_inference_cuda_logits_readback_nvrtc_symbol(model, "nvrtcCreateProgram", (void **) &nvrtc_create_program) + || !king_inference_cuda_logits_readback_nvrtc_symbol(model, "nvrtcCompileProgram", (void **) &nvrtc_compile_program) + || !king_inference_cuda_logits_readback_nvrtc_symbol(model, "nvrtcGetPTXSize", (void **) &nvrtc_get_ptx_size) + || !king_inference_cuda_logits_readback_nvrtc_symbol(model, "nvrtcGetPTX", (void **) &nvrtc_get_ptx) + || !king_inference_cuda_logits_readback_nvrtc_symbol(model, "nvrtcDestroyProgram", (void **) &nvrtc_destroy_program)) { + return false; + } + + result = nvrtc_create_program(&program, king_inference_cuda_logits_readback_source, "king_logits_top_k.cu", 0, NULL, NULL); + if (result != 0 || program == NULL) { + king_inference_cuda_logits_readback_set_error( + model, + result, + result != 0 ? "nvrtcCreateProgram_failed" : "nvrtcProgram_unavailable" + ); + return false; + } + + result = nvrtc_compile_program(program, 3, options); + if (result != 0) { + (void) nvrtc_destroy_program(&program); + king_inference_cuda_logits_readback_set_error(model, result, "nvrtcCompileProgram_failed"); + return false; + } + + result = nvrtc_get_ptx_size(program, ptx_size); + if (result != 0 || *ptx_size == 0) { + (void) nvrtc_destroy_program(&program); + king_inference_cuda_logits_readback_set_error( + model, + result, + result != 0 ? "nvrtcGetPTXSize_failed" : "nvrtc_ptx_empty" + ); + return false; + } + + *ptx = emalloc(*ptx_size); + result = nvrtc_get_ptx(program, *ptx); + (void) nvrtc_destroy_program(&program); + if (result != 0) { + efree(*ptx); + *ptx = NULL; + *ptx_size = 0; + king_inference_cuda_logits_readback_set_error(model, result, "nvrtcGetPTX_failed"); + return false; + } + + return true; +} + +static void king_inference_cuda_logits_readback_release_buffers(king_inference_model_object *model) +{ + if (model->cuda_logits_readback_device_logits != 0) { + (void) king_inference_cuda_device_allocator_free(model, model->cuda_logits_readback_device_logits); + model->cuda_logits_readback_device_logits = 0; + } + if (model->cuda_logits_readback_device_indices != 0) { + (void) king_inference_cuda_device_allocator_free(model, model->cuda_logits_readback_device_indices); + model->cuda_logits_readback_device_indices = 0; + } + model->cuda_logits_readback_device_logits_bytes = 0; + model->cuda_logits_readback_device_indices_bytes = 0; +} + +static zend_result king_inference_cuda_logits_readback_ensure_buffer( + king_inference_model_object *model, + size_t bytes, + king_inference_cuda_device_ptr *device_ptr, + size_t *capacity, + const char *label +) { + if (bytes == 0 || device_ptr == NULL || capacity == NULL) { + king_inference_cuda_logits_readback_set_error(model, -1, "cuda_logits_readback_buffer_argument_invalid"); + return FAILURE; + } + if (*device_ptr != 0 && *capacity >= bytes) { + model->cuda_logits_readback_buffer_reuse_count++; + return SUCCESS; + } + if (*device_ptr != 0) { + (void) king_inference_cuda_device_allocator_free(model, *device_ptr); + *device_ptr = 0; + *capacity = 0; + } + if (king_inference_cuda_device_allocator_alloc(model, bytes, device_ptr) != SUCCESS) { + king_inference_cuda_logits_readback_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : label + ); + return FAILURE; + } + *capacity = bytes; + return SUCCESS; +} + +static void king_inference_cuda_logits_readback_release(king_inference_model_object *model) +{ + king_inference_cuda_module_unload_fn cu_module_unload; + + king_inference_cuda_logits_readback_release_buffers(model); + + if (model->cuda_logits_readback_module != NULL + && model->cuda_driver_handle != NULL + && model->cuda_context != NULL + && king_inference_cuda_device_allocator_set_current(model) + && king_inference_cuda_logits_readback_driver_symbol( + model, + "cuModuleUnload", + NULL, + (void **) &cu_module_unload + )) { + (void) cu_module_unload(model->cuda_logits_readback_module); + } + + if (model->cuda_logits_readback_nvrtc_handle != NULL) { + dlclose(model->cuda_logits_readback_nvrtc_handle); + } + + king_inference_cuda_logits_readback_reset_fields(model); +} + +static void king_inference_cuda_logits_readback_init( + king_inference_model_object *model, + king_inference_backend_kind backend_kind +) { + king_inference_cuda_module_load_data_fn cu_module_load_data; + king_inference_cuda_module_get_function_fn cu_module_get_function; + char *ptx = NULL; + size_t ptx_size = 0; + void *module = NULL; + void *function = NULL; + int result; + + king_inference_cuda_logits_readback_release(model); + if (backend_kind != KING_INFERENCE_BACKEND_KING_NATIVE_GPU + || !king_inference_gpu_enabled(&model->config)) { + return; + } + + model->cuda_logits_readback_attempted = true; + if (!model->cuda_output_projection_available) { + king_inference_cuda_logits_readback_set_error(model, -1, "cuda_output_projection_dependency_unavailable"); + return; + } + if (!model->cuda_device_allocator_available) { + king_inference_cuda_logits_readback_set_error(model, -1, "cuda_device_allocator_unavailable"); + return; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + king_inference_cuda_logits_readback_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_context_current_failed" + ); + return; + } + if (!king_inference_cuda_logits_readback_open_nvrtc(model) + || !king_inference_cuda_logits_readback_compile_ptx(model, &ptx, &ptx_size)) { + return; + } + if (!king_inference_cuda_logits_readback_driver_symbol(model, "cuModuleLoadData", NULL, (void **) &cu_module_load_data) + || !king_inference_cuda_logits_readback_driver_symbol(model, "cuModuleGetFunction", NULL, (void **) &cu_module_get_function)) { + efree(ptx); + return; + } + + result = cu_module_load_data(&module, ptx); + efree(ptx); + if (result != 0 || module == NULL) { + king_inference_cuda_logits_readback_set_error( + model, + result, + result != 0 ? "cuModuleLoadData_failed" : "cuda_module_unavailable" + ); + return; + } + + result = cu_module_get_function(&function, module, "king_logits_top_k"); + if (result != 0 || function == NULL) { + king_inference_cuda_module_unload_fn cu_module_unload = NULL; + + if (king_inference_cuda_logits_readback_driver_symbol(model, "cuModuleUnload", NULL, (void **) &cu_module_unload) + && cu_module_unload != NULL) { + (void) cu_module_unload(module); + } + king_inference_cuda_logits_readback_set_error( + model, + result, + result != 0 ? "cuModuleGetFunction_failed" : "cuda_logits_top_k_kernel_unavailable" + ); + return; + } + + model->cuda_logits_readback_module = module; + model->cuda_logits_top_k_function = function; + model->cuda_logits_readback_module_loaded = true; + model->cuda_logits_readback_top_k_available = true; + model->cuda_logits_readback_bounded_cpu_available = true; + model->cuda_logits_readback_available = true; + model->cuda_logits_readback_result = 0; + model->cuda_logits_readback_error[0] = '\0'; + if (king_inference_cuda_logits_readback_ensure_buffer( + model, + model->cuda_logits_readback_candidate_limit * sizeof(unsigned int), + &model->cuda_logits_readback_device_indices, + &model->cuda_logits_readback_device_indices_bytes, + "cuda_logits_readback_indices_prewarm_failed" + ) != SUCCESS + || king_inference_cuda_logits_readback_ensure_buffer( + model, + model->cuda_logits_readback_candidate_limit * sizeof(float), + &model->cuda_logits_readback_device_logits, + &model->cuda_logits_readback_device_logits_bytes, + "cuda_logits_readback_logits_prewarm_failed" + ) != SUCCESS) { + model->cuda_logits_readback_available = false; + model->cuda_logits_readback_top_k_available = false; + return; + } + if (king_inference_cuda_device_vector_numeric_compare_enabled(&model->config) + && king_inference_cuda_logits_readback_synthetic_ordering_check(model) != SUCCESS) { + model->cuda_logits_readback_available = false; + model->cuda_logits_readback_top_k_available = false; + return; + } +} + +static zend_result king_inference_cuda_logits_readback_top_k( + king_inference_model_object *model, + king_inference_cuda_device_ptr logits, + zend_ulong vocab_size, + zend_ulong requested_candidates, + unsigned int *host_indices, + float *host_logits, + zend_ulong *candidate_count_out +) { + king_inference_cuda_launch_kernel_fn cu_launch_kernel; + king_inference_cuda_logits_memcpy_dtoh_fn cu_memcpy_dtoh; + unsigned int vocab32; + unsigned int count32; + size_t index_bytes; + size_t logit_bytes; + void *params[5]; + uint64_t start_ns; + uint64_t end_ns; + int result; + + start_ns = king_inference_cuda_logits_readback_profile_ns(); + if (candidate_count_out == NULL) { + king_inference_cuda_logits_readback_set_error(model, -1, "cuda_logits_readback_argument_invalid"); + return FAILURE; + } + *candidate_count_out = 0; + if (!model->cuda_logits_readback_available || model->cuda_logits_top_k_function == NULL) { + king_inference_cuda_logits_readback_set_error(model, -1, "cuda_logits_readback_unavailable"); + return FAILURE; + } + if (logits == 0 || host_indices == NULL || host_logits == NULL) { + king_inference_cuda_logits_readback_set_error(model, -1, "cuda_logits_readback_argument_invalid"); + return FAILURE; + } + if (vocab_size == 0 || vocab_size > UINT_MAX || requested_candidates == 0) { + king_inference_cuda_logits_readback_set_error(model, -1, "cuda_logits_readback_shape_invalid"); + return FAILURE; + } + + count32 = (unsigned int) requested_candidates; + if (count32 > KING_INFERENCE_CUDA_LOGITS_READBACK_MAX_CANDIDATES) { + count32 = KING_INFERENCE_CUDA_LOGITS_READBACK_MAX_CANDIDATES; + } + if ((zend_ulong) count32 > vocab_size) { + count32 = (unsigned int) vocab_size; + } + vocab32 = (unsigned int) vocab_size; + index_bytes = (size_t) count32 * sizeof(unsigned int); + logit_bytes = (size_t) count32 * sizeof(float); + + if (king_inference_cuda_logits_readback_ensure_buffer( + model, + index_bytes, + &model->cuda_logits_readback_device_indices, + &model->cuda_logits_readback_device_indices_bytes, + "cuda_logits_readback_indices_allocation_failed" + ) != SUCCESS + || king_inference_cuda_logits_readback_ensure_buffer( + model, + logit_bytes, + &model->cuda_logits_readback_device_logits, + &model->cuda_logits_readback_device_logits_bytes, + "cuda_logits_readback_logits_allocation_failed" + ) != SUCCESS) { + goto failure; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + king_inference_cuda_logits_readback_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_context_current_failed" + ); + goto failure; + } + if (!king_inference_cuda_logits_readback_driver_symbol(model, "cuLaunchKernel", NULL, (void **) &cu_launch_kernel) + || !king_inference_cuda_logits_readback_driver_symbol( + model, + "cuMemcpyDtoH_v2", + "cuMemcpyDtoH", + (void **) &cu_memcpy_dtoh + )) { + goto failure; + } + + params[0] = &logits; + params[1] = &vocab32; + params[2] = &count32; + params[3] = &model->cuda_logits_readback_device_indices; + params[4] = &model->cuda_logits_readback_device_logits; + result = cu_launch_kernel( + model->cuda_logits_top_k_function, + 1, + 1, + 1, + 256, + 1, + 1, + 0, + NULL, + params, + NULL + ); + if (result != 0) { + king_inference_cuda_logits_readback_set_error(model, result, "cuLaunchKernel_failed"); + goto failure; + } + + result = cu_memcpy_dtoh(host_indices, model->cuda_logits_readback_device_indices, index_bytes); + if (result != 0) { + king_inference_cuda_logits_readback_set_error(model, result, "cuMemcpyDtoH_indices_failed"); + goto failure; + } + model->cuda_logits_readback_dtoh_count++; + result = cu_memcpy_dtoh(host_logits, model->cuda_logits_readback_device_logits, logit_bytes); + if (result != 0) { + king_inference_cuda_logits_readback_set_error(model, result, "cuMemcpyDtoH_logits_failed"); + goto failure; + } + model->cuda_logits_readback_dtoh_count++; + + model->cuda_logits_readback_launch_count++; + model->cuda_logits_readback_last_candidate_count = count32; + model->cuda_logits_readback_last_bytes = index_bytes + logit_bytes; + model->cuda_logits_readback_full_bytes = (size_t) vocab32 * sizeof(float); + model->cuda_logits_readback_saved_bytes = model->cuda_logits_readback_full_bytes > model->cuda_logits_readback_last_bytes + ? model->cuda_logits_readback_full_bytes - model->cuda_logits_readback_last_bytes + : 0; + end_ns = king_inference_cuda_logits_readback_profile_ns(); + model->cuda_logits_readback_last_ns = + king_inference_cuda_logits_readback_profile_elapsed(end_ns, start_ns); + model->cuda_logits_readback_result = 0; + model->cuda_logits_readback_error[0] = '\0'; + *candidate_count_out = count32; + return SUCCESS; + +failure: + return FAILURE; +} + +#include "cuda_logits_readback_self_check.inc" + +static void king_inference_cuda_logits_readback_add_status( + king_inference_model_object *model, + zval *return_value +) { + zval readback; + zval synthetic_ordering; + + array_init(&readback); + add_assoc_bool(&readback, "attempted", model->cuda_logits_readback_attempted); + add_assoc_bool(&readback, "available", model->cuda_logits_readback_available); + add_assoc_bool(&readback, "nvrtc_available", model->cuda_logits_readback_nvrtc_available); + add_assoc_bool(&readback, "module_loaded", model->cuda_logits_readback_module_loaded); + add_assoc_bool(&readback, "top_k_available", model->cuda_logits_readback_top_k_available); + add_assoc_bool(&readback, "bounded_cpu_available", model->cuda_logits_readback_bounded_cpu_available); + add_assoc_long(&readback, "max_candidates", (zend_long) model->cuda_logits_readback_candidate_limit); + add_assoc_long(&readback, "last_candidate_count", (zend_long) model->cuda_logits_readback_last_candidate_count); + add_assoc_long(&readback, "last_readback_bytes", (zend_long) model->cuda_logits_readback_last_bytes); + add_assoc_long(&readback, "full_logits_bytes", (zend_long) model->cuda_logits_readback_full_bytes); + add_assoc_long(&readback, "saved_readback_bytes", (zend_long) model->cuda_logits_readback_saved_bytes); + add_assoc_bool(&readback, "resident_device_buffers", model->cuda_logits_readback_device_indices != 0 + && model->cuda_logits_readback_device_logits != 0); + add_assoc_long( + &readback, + "resident_indices_buffer_bytes", + (zend_long) model->cuda_logits_readback_device_indices_bytes + ); + add_assoc_long( + &readback, + "resident_logits_buffer_bytes", + (zend_long) model->cuda_logits_readback_device_logits_bytes + ); + add_assoc_long( + &readback, + "resident_buffer_reuses", + (zend_long) model->cuda_logits_readback_buffer_reuse_count + ); + add_assoc_bool(&readback, "explicit_pre_readback_sync_removed", true); + add_assoc_long(&readback, "last_readback_ns", model->cuda_logits_readback_last_ns); + add_assoc_bool(&readback, "full_logits_cpu_readback_avoided", model->cuda_logits_readback_available); + array_init(&synthetic_ordering); + add_assoc_bool( + &synthetic_ordering, + "enabled", + king_inference_cuda_device_vector_numeric_compare_enabled(&model->config) + ); + add_assoc_bool(&synthetic_ordering, "checked", model->cuda_logits_readback_synthetic_ordering_checked); + add_assoc_bool(&synthetic_ordering, "matched", model->cuda_logits_readback_synthetic_ordering_matched); + add_assoc_long(&synthetic_ordering, "vocab_size", (zend_long) model->cuda_logits_readback_synthetic_vocab_size); + add_assoc_long(&synthetic_ordering, "candidate_count", (zend_long) model->cuda_logits_readback_synthetic_candidate_count); + add_assoc_long(&synthetic_ordering, "matched_count", (zend_long) model->cuda_logits_readback_synthetic_matched_count); + add_assoc_long(&synthetic_ordering, "rank_matched_count", (zend_long) model->cuda_logits_readback_synthetic_rank_matched_count); + add_assoc_long(&synthetic_ordering, "token_id_matched_count", (zend_long) model->cuda_logits_readback_synthetic_token_id_matched_count); + add_assoc_long(&synthetic_ordering, "logit_matched_count", (zend_long) model->cuda_logits_readback_synthetic_logit_matched_count); + add_assoc_zval(&readback, "synthetic_ordering", &synthetic_ordering); + add_assoc_long(&readback, "launches", (zend_long) model->cuda_logits_readback_launch_count); + add_assoc_long(&readback, "result", model->cuda_logits_readback_result); + add_assoc_string(&readback, "error", model->cuda_logits_readback_error); + add_assoc_zval(return_value, "logits_readback", &readback); +} + +static void king_inference_cuda_logits_readback_apply_status( + king_inference_model_object *model, + zval *return_value +) { + zval *reason; + bool reason_can_be_readback; + + king_inference_cuda_logits_readback_add_status(model, return_value); + if (!model->cuda_logits_readback_attempted + || model->cuda_logits_readback_available + || !model->cuda_context_available) { + return; + } + + reason = zend_hash_str_find(Z_ARRVAL_P(return_value), "reason", sizeof("reason") - 1); + reason_can_be_readback = reason == NULL + || Z_TYPE_P(reason) != IS_STRING + || zend_string_equals_literal(Z_STR_P(reason), "gpu_ready") + || zend_string_equals_literal(Z_STR_P(reason), "gpu_decoder_kernel_not_implemented"); + if (reason_can_be_readback) { + king_inference_cuda_context_prepend_return_reason( + return_value, + "gpu_logits_readback_unavailable" + ); + add_assoc_string(return_value, "reason", "gpu_logits_readback_unavailable"); + } else { + king_inference_gpu_status_append_return_reason(return_value, "gpu_logits_readback_unavailable"); + } +} diff --git a/extension/src/inference/cuda/kernels/projection/cuda_logits_readback_self_check.inc b/extension/src/inference/cuda/kernels/projection/cuda_logits_readback_self_check.inc new file mode 100644 index 000000000..8f3f0ced9 --- /dev/null +++ b/extension/src/inference/cuda/kernels/projection/cuda_logits_readback_self_check.inc @@ -0,0 +1,151 @@ +/* + * Internal synthetic CUDA top-k ordering self-check. + */ + +typedef int (*king_inference_cuda_logits_memcpy_htod_fn)( + king_inference_cuda_device_ptr dst, + const void *src, + size_t bytes +); + +static zend_result king_inference_cuda_logits_readback_synthetic_ordering_check( + king_inference_model_object *model +) { + static const float synthetic_logits[16] = { + -7.0f, + 3.25f, + 11.0f, + 11.0f, + -1.0f, + 4.5f, + 4.5f, + 0.0f, + 10.5f, + 8.25f, + 4.5f, + -20.0f, + 10.5f, + 2.0f, + -0.25f, + 7.0f + }; + static const unsigned int expected_indices[8] = {2u, 3u, 8u, 12u, 9u, 15u, 5u, 6u}; + static const float expected_logits[8] = {11.0f, 11.0f, 10.5f, 10.5f, 8.25f, 7.0f, 4.5f, 4.5f}; + king_inference_cuda_logits_memcpy_htod_fn cu_memcpy_htod = NULL; + king_inference_cuda_device_ptr device_logits = 0; + unsigned int actual_indices[KING_INFERENCE_CUDA_LOGITS_READBACK_MAX_CANDIDATES]; + float actual_logits[KING_INFERENCE_CUDA_LOGITS_READBACK_MAX_CANDIDATES]; + zend_ulong candidate_count = 0; + zend_ulong matched = 0; + zend_ulong rank_matched = 0; + zend_ulong token_id_matched = 0; + zend_ulong logit_matched = 0; + int result; + + model->cuda_logits_readback_synthetic_ordering_checked = true; + model->cuda_logits_readback_synthetic_ordering_matched = false; + model->cuda_logits_readback_synthetic_vocab_size = 16; + model->cuda_logits_readback_synthetic_candidate_count = 8; + model->cuda_logits_readback_synthetic_matched_count = 0; + model->cuda_logits_readback_synthetic_rank_matched_count = 0; + model->cuda_logits_readback_synthetic_token_id_matched_count = 0; + model->cuda_logits_readback_synthetic_logit_matched_count = 0; + + if (!king_inference_cuda_device_allocator_set_current(model)) { + king_inference_cuda_logits_readback_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_context_current_failed" + ); + return FAILURE; + } + if (!king_inference_cuda_logits_readback_driver_symbol( + model, + "cuMemcpyHtoD_v2", + "cuMemcpyHtoD", + (void **) &cu_memcpy_htod + )) { + return FAILURE; + } + if (king_inference_cuda_device_allocator_alloc( + model, + sizeof(synthetic_logits), + &device_logits + ) != SUCCESS) { + king_inference_cuda_logits_readback_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_logits_readback_synthetic_allocation_failed" + ); + return FAILURE; + } + + result = cu_memcpy_htod(device_logits, synthetic_logits, sizeof(synthetic_logits)); + if (result != 0) { + (void) king_inference_cuda_device_allocator_free(model, device_logits); + king_inference_cuda_logits_readback_set_error( + model, + result, + "cuMemcpyHtoD_synthetic_logits_failed" + ); + return FAILURE; + } + if (king_inference_cuda_logits_readback_top_k( + model, + device_logits, + 16, + 8, + actual_indices, + actual_logits, + &candidate_count + ) != SUCCESS) { + (void) king_inference_cuda_device_allocator_free(model, device_logits); + return FAILURE; + } + (void) king_inference_cuda_device_allocator_free(model, device_logits); + + if (candidate_count != 8) { + king_inference_cuda_logits_readback_set_error( + model, + -1, + "cuda_logits_readback_synthetic_candidate_count_mismatch" + ); + return FAILURE; + } + for (zend_ulong index = 0; index < candidate_count; index++) { + bool token_id_matches = actual_indices[index] == expected_indices[index]; + bool logit_matches = fabs((double) actual_logits[index] - (double) expected_logits[index]) <= 0.000001; + + if (token_id_matches) { + token_id_matched++; + } + if (logit_matches) { + logit_matched++; + } + if (token_id_matches && logit_matches) { + matched++; + rank_matched++; + } + } + model->cuda_logits_readback_synthetic_matched_count = matched; + model->cuda_logits_readback_synthetic_rank_matched_count = rank_matched; + model->cuda_logits_readback_synthetic_token_id_matched_count = token_id_matched; + model->cuda_logits_readback_synthetic_logit_matched_count = logit_matched; + if (matched != candidate_count) { + king_inference_cuda_logits_readback_set_error( + model, + -1, + "cuda_logits_readback_synthetic_ordering_mismatch" + ); + return FAILURE; + } + + model->cuda_logits_readback_synthetic_ordering_matched = true; + model->cuda_logits_readback_result = 0; + model->cuda_logits_readback_error[0] = '\0'; + return SUCCESS; +} diff --git a/extension/src/inference/cuda/kernels/projection/cuda_output_projection.inc b/extension/src/inference/cuda/kernels/projection/cuda_output_projection.inc new file mode 100644 index 000000000..5709ef3db --- /dev/null +++ b/extension/src/inference/cuda/kernels/projection/cuda_output_projection.inc @@ -0,0 +1,266 @@ +/* + * CUDA final output projection path for native King GPU inference. + */ + +static void king_inference_cuda_output_projection_reset_fields(king_inference_model_object *model) +{ + model->cuda_output_projection_result = 0; + model->cuda_output_projection_error[0] = '\0'; + model->cuda_output_projection_launch_count = 0; + model->cuda_output_projection_attempted = false; + model->cuda_output_projection_available = false; + model->cuda_output_projection_resolved = false; + model->cuda_output_projection_uploaded = false; + model->cuda_output_projection_tied_token_embedding = false; + model->cuda_output_projection_q8_0_available = false; +} + +static void king_inference_cuda_output_projection_set_error( + king_inference_model_object *model, + int result, + const char *message +) { + if (message == NULL || message[0] == '\0') { + message = "unknown_cuda_output_projection_error"; + } + + model->cuda_output_projection_result = result; + snprintf( + model->cuda_output_projection_error, + sizeof(model->cuda_output_projection_error), + "%s", + message + ); +} + +static bool king_inference_cuda_output_projection_resolve( + king_inference_model_object *model, + zend_string **name_out, + const char **source_out, + const char **status_out, + bool *tied_embedding_out +) { + zend_result resolved; + + *name_out = NULL; + if (source_out != NULL) { + *source_out = NULL; + } + if (status_out != NULL) { + *status_out = NULL; + } + if (tied_embedding_out != NULL) { + *tied_embedding_out = false; + } + + resolved = king_inference_resolve_output_projection_tensor_name( + model, + NULL, + name_out, + source_out, + status_out, + tied_embedding_out + ); + if (resolved != SUCCESS || *name_out == NULL) { + king_inference_cuda_output_projection_set_error( + model, + -1, + status_out != NULL && *status_out != NULL ? *status_out : "output_projection_not_resolved" + ); + return false; + } + + return true; +} + +static void king_inference_cuda_output_projection_release(king_inference_model_object *model) +{ + king_inference_cuda_output_projection_reset_fields(model); +} + +static void king_inference_cuda_output_projection_init( + king_inference_model_object *model, + king_inference_backend_kind backend_kind +) { + zend_string *name = NULL; + const char *source = NULL; + const char *status = NULL; + bool tied_embedding = false; + king_inference_cuda_weight_upload *upload; + + king_inference_cuda_output_projection_release(model); + if (backend_kind != KING_INFERENCE_BACKEND_KING_NATIVE_GPU + || !king_inference_gpu_enabled(&model->config)) { + return; + } + + model->cuda_output_projection_attempted = true; + model->cuda_output_projection_q8_0_available = model->cuda_quantized_matvec_available; + if (!king_inference_cuda_output_projection_resolve( + model, + &name, + &source, + &status, + &tied_embedding + )) { + return; + } + + (void) source; + (void) status; + model->cuda_output_projection_resolved = true; + model->cuda_output_projection_tied_token_embedding = tied_embedding; + upload = king_inference_cuda_weight_cache_find(model, name, false); + model->cuda_output_projection_uploaded = upload != NULL && upload->device_ptr != 0; + if (!model->cuda_output_projection_uploaded) { + zend_string_release(name); + king_inference_cuda_output_projection_set_error(model, -1, "cuda_output_projection_weight_not_uploaded"); + return; + } + zend_string_release(name); + + if (!model->cuda_quantized_matvec_available) { + king_inference_cuda_output_projection_set_error(model, -1, "cuda_quantized_matvec_dependency_unavailable"); + return; + } + + model->cuda_output_projection_available = true; + model->cuda_output_projection_result = 0; + model->cuda_output_projection_error[0] = '\0'; +} + +static zend_result king_inference_cuda_output_projection_q8_0( + king_inference_model_object *model, + king_inference_cuda_device_ptr input_vector, + zend_ulong input_length, + king_inference_cuda_device_ptr output_logits, + zend_ulong vocab_rows +) { + zend_string *name = NULL; + const char *status = NULL; + bool tied_embedding = false; + zend_result result; + + if (!model->cuda_output_projection_available) { + king_inference_cuda_output_projection_set_error(model, -1, "cuda_output_projection_unavailable"); + return FAILURE; + } + if (input_vector == 0 || output_logits == 0) { + king_inference_cuda_output_projection_set_error(model, -1, "cuda_output_projection_argument_invalid"); + return FAILURE; + } + if (input_length == 0 || input_length > UINT_MAX || vocab_rows == 0 || vocab_rows > UINT_MAX) { + king_inference_cuda_output_projection_set_error(model, -1, "cuda_output_projection_shape_invalid"); + return FAILURE; + } + if (!king_inference_cuda_output_projection_resolve(model, &name, NULL, &status, &tied_embedding)) { + return FAILURE; + } + + model->cuda_output_projection_tied_token_embedding = tied_embedding; + result = king_inference_cuda_quantized_matvec( + model, + name, + input_vector, + input_length, + output_logits, + vocab_rows + ); + zend_string_release(name); + if (result != SUCCESS) { + king_inference_cuda_output_projection_set_error( + model, + model->cuda_quantized_matvec_result, + model->cuda_quantized_matvec_error[0] != '\0' + ? model->cuda_quantized_matvec_error + : "cuda_output_projection_matvec_failed" + ); + return FAILURE; + } + + model->cuda_output_projection_launch_count++; + model->cuda_output_projection_result = 0; + model->cuda_output_projection_error[0] = '\0'; + return SUCCESS; +} + +static void king_inference_cuda_output_projection_add_status( + king_inference_model_object *model, + zval *return_value +) { + zend_string *name = NULL; + const char *source = NULL; + const char *status = NULL; + bool tied_embedding = false; + zval output_projection; + + array_init(&output_projection); + add_assoc_bool(&output_projection, "attempted", model->cuda_output_projection_attempted); + add_assoc_bool(&output_projection, "available", model->cuda_output_projection_available); + add_assoc_bool(&output_projection, "resolved", model->cuda_output_projection_resolved); + add_assoc_bool(&output_projection, "uploaded", model->cuda_output_projection_uploaded); + add_assoc_bool(&output_projection, "q8_0_available", model->cuda_output_projection_q8_0_available); + add_assoc_bool( + &output_projection, + "tied_token_embedding", + model->cuda_output_projection_tied_token_embedding + ); + add_assoc_long(&output_projection, "launches", (zend_long) model->cuda_output_projection_launch_count); + add_assoc_long(&output_projection, "result", model->cuda_output_projection_result); + add_assoc_string(&output_projection, "error", model->cuda_output_projection_error); + + if (king_inference_cuda_output_projection_resolve( + model, + &name, + &source, + &status, + &tied_embedding + )) { + if (source != NULL) { + add_assoc_string(&output_projection, "source", source); + } + if (status != NULL) { + add_assoc_string(&output_projection, "status", status); + } + add_assoc_stringl(&output_projection, "name", ZSTR_VAL(name), ZSTR_LEN(name)); + add_assoc_bool(&output_projection, "tied_token_embedding", tied_embedding); + zend_string_release(name); + } else if (status != NULL) { + add_assoc_string(&output_projection, "status", status); + } + + add_assoc_zval(return_value, "output_projection_path", &output_projection); +} + +static void king_inference_cuda_output_projection_apply_status( + king_inference_model_object *model, + zval *return_value +) { + zval *reason; + bool reason_can_be_output_projection; + + king_inference_cuda_output_projection_add_status(model, return_value); + if (!model->cuda_output_projection_attempted + || model->cuda_output_projection_available + || !model->cuda_context_available) { + return; + } + + reason = zend_hash_str_find(Z_ARRVAL_P(return_value), "reason", sizeof("reason") - 1); + reason_can_be_output_projection = reason == NULL + || Z_TYPE_P(reason) != IS_STRING + || zend_string_equals_literal(Z_STR_P(reason), "gpu_ready") + || zend_string_equals_literal(Z_STR_P(reason), "gpu_decoder_kernel_not_implemented"); + if (reason_can_be_output_projection) { + king_inference_cuda_context_prepend_return_reason( + return_value, + "gpu_output_projection_path_unavailable" + ); + add_assoc_string(return_value, "reason", "gpu_output_projection_path_unavailable"); + } else { + king_inference_gpu_status_append_return_reason( + return_value, + "gpu_output_projection_path_unavailable" + ); + } +} diff --git a/extension/src/inference/cuda/kernels/rope/cuda_rope.inc b/extension/src/inference/cuda/kernels/rope/cuda_rope.inc new file mode 100644 index 000000000..72236c34b --- /dev/null +++ b/extension/src/inference/cuda/kernels/rope/cuda_rope.inc @@ -0,0 +1,629 @@ +/* + * CUDA RoPE kernel for native King GPU inference. + */ + +typedef int king_inference_rope_nvrtc_result; +typedef void *king_inference_rope_nvrtc_program; + +typedef king_inference_rope_nvrtc_result (*king_inference_rope_nvrtc_create_program_fn)( + king_inference_rope_nvrtc_program *program, + const char *source, + const char *name, + int header_count, + const char * const *headers, + const char * const *include_names +); +typedef king_inference_rope_nvrtc_result (*king_inference_rope_nvrtc_compile_program_fn)( + king_inference_rope_nvrtc_program program, + int option_count, + const char * const *options +); +typedef king_inference_rope_nvrtc_result (*king_inference_rope_nvrtc_get_ptx_size_fn)( + king_inference_rope_nvrtc_program program, + size_t *ptx_size +); +typedef king_inference_rope_nvrtc_result (*king_inference_rope_nvrtc_get_ptx_fn)( + king_inference_rope_nvrtc_program program, + char *ptx +); +typedef king_inference_rope_nvrtc_result (*king_inference_rope_nvrtc_destroy_program_fn)( + king_inference_rope_nvrtc_program *program +); + +static const char king_inference_cuda_rope_source[] = +"extern \"C\" __global__ void king_rope(\n" +" const float *input,\n" +" float *output,\n" +" unsigned int length,\n" +" unsigned int offset,\n" +" unsigned int head_dim,\n" +" unsigned long long position,\n" +" float position_scale,\n" +" float rope_base,\n" +" unsigned int pairing\n" +") {\n" +" unsigned int index = blockIdx.x * blockDim.x + threadIdx.x;\n" +" unsigned int pair_count = head_dim >> 1;\n" +" if (index < length && (index < offset || index >= offset + head_dim)) {\n" +" output[index] = input[index];\n" +" }\n" +" if (index >= pair_count) {\n" +" return;\n" +" }\n" +" unsigned int half = head_dim >> 1;\n" +" unsigned int even = pairing == 1u ? offset + index : offset + index * 2u;\n" +" unsigned int odd = pairing == 1u ? offset + half + index : even + 1u;\n" +" float exponent = ((float) (index * 2u)) / (float) head_dim;\n" +" float inv_freq = expf(-logf(rope_base) * exponent);\n" +" float angle = (float) position * position_scale * inv_freq;\n" +" float sin_value = sinf(angle);\n" +" float cos_value = cosf(angle);\n" +" float x0 = input[even];\n" +" float x1 = input[odd];\n" +" output[even] = x0 * cos_value - x1 * sin_value;\n" +" output[odd] = x0 * sin_value + x1 * cos_value;\n" +"}\n" +"extern \"C\" __global__ void king_rope_batch_slice(\n" +" const float *input,\n" +" float *output,\n" +" unsigned int input_stride,\n" +" unsigned int source_offset,\n" +" unsigned int head_dim,\n" +" unsigned int batch_count,\n" +" unsigned long long position_start,\n" +" float position_scale,\n" +" float rope_base,\n" +" unsigned int pairing\n" +") {\n" +" unsigned int pair = blockIdx.x * blockDim.x + threadIdx.x;\n" +" unsigned int batch = blockIdx.y;\n" +" unsigned int pair_count = head_dim >> 1;\n" +" if (input == 0 || output == 0 || batch >= batch_count || pair >= pair_count) {\n" +" return;\n" +" }\n" +" const float *row_input = input + ((unsigned long long) batch * (unsigned long long) input_stride) + source_offset;\n" +" float *row_output = output + ((unsigned long long) batch * (unsigned long long) head_dim);\n" +" unsigned int half = head_dim >> 1;\n" +" unsigned int even = pairing == 1u ? pair : pair * 2u;\n" +" unsigned int odd = pairing == 1u ? half + pair : even + 1u;\n" +" float exponent = ((float) (pair * 2u)) / (float) head_dim;\n" +" float inv_freq = expf(-logf(rope_base) * exponent);\n" +" float angle = (float) (position_start + (unsigned long long) batch) * position_scale * inv_freq;\n" +" float sin_value = sinf(angle);\n" +" float cos_value = cosf(angle);\n" +" float x0 = row_input[even];\n" +" float x1 = row_input[odd];\n" +" row_output[even] = x0 * cos_value - x1 * sin_value;\n" +" row_output[odd] = x0 * sin_value + x1 * cos_value;\n" +"}\n"; + +static void king_inference_cuda_rope_reset_fields(king_inference_model_object *model) +{ + model->cuda_rope_nvrtc_handle = NULL; + model->cuda_rope_module = NULL; + model->cuda_rope_function = NULL; + model->cuda_rope_batch_function = NULL; + model->cuda_rope_result = 0; + model->cuda_rope_error[0] = '\0'; + model->cuda_rope_launch_count = 0; + model->cuda_rope_batch_launch_count = 0; + model->cuda_rope_sync_count = 0; + model->cuda_rope_batch_sync_count = 0; + model->cuda_rope_attempted = false; + model->cuda_rope_available = false; + model->cuda_rope_nvrtc_available = false; + model->cuda_rope_module_loaded = false; + model->cuda_rope_f32_available = false; + model->cuda_rope_batch_available = false; +} + +static void king_inference_cuda_rope_set_error( + king_inference_model_object *model, + int result, + const char *message +) { + if (message == NULL || message[0] == '\0') { + message = "unknown_cuda_rope_error"; + } + + model->cuda_rope_result = result; + snprintf(model->cuda_rope_error, sizeof(model->cuda_rope_error), "%s", message); +} + +static bool king_inference_cuda_rope_driver_symbol( + king_inference_model_object *model, + const char *name, + void **symbol +) { + *symbol = NULL; + if (model->cuda_driver_handle == NULL) { + king_inference_cuda_rope_set_error(model, -1, "cuda_driver_handle_unavailable"); + return false; + } + + *symbol = dlsym(model->cuda_driver_handle, name); + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_rope_set_error(model, -1, name); + return false; +} + +static bool king_inference_cuda_rope_nvrtc_symbol( + king_inference_model_object *model, + const char *name, + void **symbol +) { + *symbol = NULL; + if (model->cuda_rope_nvrtc_handle == NULL) { + king_inference_cuda_rope_set_error(model, -1, "nvrtc_handle_unavailable"); + return false; + } + + *symbol = dlsym(model->cuda_rope_nvrtc_handle, name); + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_rope_set_error(model, -1, name); + return false; +} + +static bool king_inference_cuda_rope_open_nvrtc(king_inference_model_object *model) +{ + static const char *libraries[] = { + "libnvrtc.so.12", + "libnvrtc.so.11.2", + "libnvrtc.so.11.0", + "libnvrtc.so" + }; + size_t index; + + for (index = 0; index < sizeof(libraries) / sizeof(libraries[0]); index++) { + model->cuda_rope_nvrtc_handle = dlopen(libraries[index], RTLD_LAZY | RTLD_LOCAL); + if (model->cuda_rope_nvrtc_handle != NULL) { + model->cuda_rope_nvrtc_available = true; + return true; + } + } + + king_inference_cuda_rope_set_error(model, -1, "libnvrtc_unavailable"); + return false; +} + +static bool king_inference_cuda_rope_compile_ptx( + king_inference_model_object *model, + char **ptx, + size_t *ptx_size +) { + king_inference_rope_nvrtc_create_program_fn nvrtc_create_program; + king_inference_rope_nvrtc_compile_program_fn nvrtc_compile_program; + king_inference_rope_nvrtc_get_ptx_size_fn nvrtc_get_ptx_size; + king_inference_rope_nvrtc_get_ptx_fn nvrtc_get_ptx; + king_inference_rope_nvrtc_destroy_program_fn nvrtc_destroy_program; + king_inference_rope_nvrtc_program program = NULL; + const char *options[] = {"--std=c++11", "--gpu-architecture=compute_89", "--use_fast_math"}; + king_inference_rope_nvrtc_result result; + + *ptx = NULL; + *ptx_size = 0; + + if (!king_inference_cuda_rope_nvrtc_symbol(model, "nvrtcCreateProgram", (void **) &nvrtc_create_program) + || !king_inference_cuda_rope_nvrtc_symbol(model, "nvrtcCompileProgram", (void **) &nvrtc_compile_program) + || !king_inference_cuda_rope_nvrtc_symbol(model, "nvrtcGetPTXSize", (void **) &nvrtc_get_ptx_size) + || !king_inference_cuda_rope_nvrtc_symbol(model, "nvrtcGetPTX", (void **) &nvrtc_get_ptx) + || !king_inference_cuda_rope_nvrtc_symbol(model, "nvrtcDestroyProgram", (void **) &nvrtc_destroy_program)) { + return false; + } + + result = nvrtc_create_program( + &program, + king_inference_cuda_rope_source, + "king_rope.cu", + 0, + NULL, + NULL + ); + if (result != 0 || program == NULL) { + king_inference_cuda_rope_set_error( + model, + result, + result != 0 ? "nvrtcCreateProgram_failed" : "nvrtcProgram_unavailable" + ); + return false; + } + + result = nvrtc_compile_program(program, 3, options); + if (result != 0) { + (void) nvrtc_destroy_program(&program); + king_inference_cuda_rope_set_error(model, result, "nvrtcCompileProgram_failed"); + return false; + } + + result = nvrtc_get_ptx_size(program, ptx_size); + if (result != 0 || *ptx_size == 0) { + (void) nvrtc_destroy_program(&program); + king_inference_cuda_rope_set_error( + model, + result, + result != 0 ? "nvrtcGetPTXSize_failed" : "nvrtc_ptx_empty" + ); + return false; + } + + *ptx = emalloc(*ptx_size); + result = nvrtc_get_ptx(program, *ptx); + (void) nvrtc_destroy_program(&program); + if (result != 0) { + efree(*ptx); + *ptx = NULL; + *ptx_size = 0; + king_inference_cuda_rope_set_error(model, result, "nvrtcGetPTX_failed"); + return false; + } + + return true; +} + +static void king_inference_cuda_rope_release(king_inference_model_object *model) +{ + king_inference_cuda_module_unload_fn cu_module_unload; + + if (model->cuda_rope_module != NULL + && model->cuda_driver_handle != NULL + && model->cuda_context != NULL + && king_inference_cuda_device_allocator_set_current(model) + && king_inference_cuda_rope_driver_symbol(model, "cuModuleUnload", (void **) &cu_module_unload)) { + (void) cu_module_unload(model->cuda_rope_module); + } + + if (model->cuda_rope_nvrtc_handle != NULL) { + dlclose(model->cuda_rope_nvrtc_handle); + } + + king_inference_cuda_rope_reset_fields(model); +} + +static void king_inference_cuda_rope_init( + king_inference_model_object *model, + king_inference_backend_kind backend_kind +) { + king_inference_cuda_module_load_data_fn cu_module_load_data; + king_inference_cuda_module_get_function_fn cu_module_get_function; + char *ptx = NULL; + size_t ptx_size = 0; + void *module = NULL; + void *function = NULL; + void *batch_function = NULL; + int result; + + king_inference_cuda_rope_release(model); + if (backend_kind != KING_INFERENCE_BACKEND_KING_NATIVE_GPU + || !king_inference_gpu_enabled(&model->config)) { + return; + } + + model->cuda_rope_attempted = true; + if (!model->cuda_device_allocator_available) { + king_inference_cuda_rope_set_error(model, -1, "cuda_device_allocator_unavailable"); + return; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + king_inference_cuda_rope_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_context_current_failed" + ); + return; + } + if (!king_inference_cuda_rope_open_nvrtc(model) + || !king_inference_cuda_rope_compile_ptx(model, &ptx, &ptx_size)) { + return; + } + if (!king_inference_cuda_rope_driver_symbol(model, "cuModuleLoadData", (void **) &cu_module_load_data) + || !king_inference_cuda_rope_driver_symbol(model, "cuModuleGetFunction", (void **) &cu_module_get_function)) { + efree(ptx); + return; + } + + result = cu_module_load_data(&module, ptx); + efree(ptx); + if (result != 0 || module == NULL) { + king_inference_cuda_rope_set_error( + model, + result, + result != 0 ? "cuModuleLoadData_failed" : "cuda_module_unavailable" + ); + return; + } + + result = cu_module_get_function(&function, module, "king_rope"); + if (result != 0 || function == NULL) { + king_inference_cuda_module_unload_fn cu_module_unload = NULL; + + if (king_inference_cuda_rope_driver_symbol(model, "cuModuleUnload", (void **) &cu_module_unload) + && cu_module_unload != NULL) { + (void) cu_module_unload(module); + } + king_inference_cuda_rope_set_error( + model, + result, + result != 0 ? "cuModuleGetFunction_failed" : "cuda_rope_kernel_unavailable" + ); + return; + } + result = cu_module_get_function(&batch_function, module, "king_rope_batch_slice"); + if (result != 0 || batch_function == NULL) { + king_inference_cuda_module_unload_fn cu_module_unload = NULL; + + if (king_inference_cuda_rope_driver_symbol(model, "cuModuleUnload", (void **) &cu_module_unload) + && cu_module_unload != NULL) { + (void) cu_module_unload(module); + } + king_inference_cuda_rope_set_error( + model, + result, + result != 0 ? "cuModuleGetFunction_batch_failed" : "cuda_rope_batch_kernel_unavailable" + ); + return; + } + + model->cuda_rope_module = module; + model->cuda_rope_function = function; + model->cuda_rope_batch_function = batch_function; + model->cuda_rope_module_loaded = true; + model->cuda_rope_f32_available = true; + model->cuda_rope_batch_available = true; + model->cuda_rope_available = true; + model->cuda_rope_result = 0; + model->cuda_rope_error[0] = '\0'; +} + +static zend_result king_inference_cuda_rope_f32( + king_inference_model_object *model, + king_inference_cuda_device_ptr input_vector, + king_inference_cuda_device_ptr output_vector, + zend_ulong length, + zend_ulong offset, + zend_ulong head_dim, + zend_ulong position, + double position_scale, + double rope_base, + zend_ulong pairing +) { + king_inference_cuda_launch_kernel_fn cu_launch_kernel; + static king_inference_cuda_launch_kernel_fn cached_cu_launch_kernel = NULL; + unsigned int length32; + unsigned int offset32; + unsigned int head_dim32; + unsigned long long position64; + float position_scale32 = (float) position_scale; + float rope_base32 = (float) rope_base; + unsigned int block_x = 256; + unsigned int work_items; + unsigned int grid_x; + unsigned int pairing32; + void *params[9]; + int result; + + if (!model->cuda_rope_available || model->cuda_rope_function == NULL) { + king_inference_cuda_rope_set_error(model, -1, "cuda_rope_unavailable"); + return FAILURE; + } + if (input_vector == 0 || output_vector == 0) { + king_inference_cuda_rope_set_error(model, -1, "cuda_rope_argument_invalid"); + return FAILURE; + } + if (length == 0 + || length > UINT_MAX + || offset > length + || head_dim == 0 + || (head_dim % 2) != 0 + || head_dim > length - offset + || head_dim > UINT_MAX + || pairing > 1 + || !isfinite(position_scale) + || !isfinite(rope_base) + || rope_base <= 0.0) { + king_inference_cuda_rope_set_error(model, -1, "cuda_rope_shape_invalid"); + return FAILURE; + } + if (cached_cu_launch_kernel == NULL + && !king_inference_cuda_rope_driver_symbol(model, "cuLaunchKernel", (void **) &cached_cu_launch_kernel)) { + return FAILURE; + } + cu_launch_kernel = cached_cu_launch_kernel; + + length32 = (unsigned int) length; + offset32 = (unsigned int) offset; + head_dim32 = (unsigned int) head_dim; + position64 = (unsigned long long) position; + pairing32 = (unsigned int) pairing; + work_items = length32 > (head_dim32 >> 1) ? length32 : (head_dim32 >> 1); + grid_x = (work_items + block_x - 1) / block_x; + params[0] = &input_vector; + params[1] = &output_vector; + params[2] = &length32; + params[3] = &offset32; + params[4] = &head_dim32; + params[5] = &position64; + params[6] = &position_scale32; + params[7] = &rope_base32; + params[8] = &pairing32; + + result = cu_launch_kernel( + model->cuda_rope_function, + grid_x, + 1, + 1, + block_x, + 1, + 1, + 0, + NULL, + params, + NULL + ); + if (result != 0) { + king_inference_cuda_rope_set_error(model, result, "cuLaunchKernel_failed"); + return FAILURE; + } + + model->cuda_rope_launch_count++; + model->cuda_rope_result = 0; + model->cuda_rope_error[0] = '\0'; + return SUCCESS; +} + +static zend_result king_inference_cuda_rope_f32_batch_slice( + king_inference_model_object *model, + king_inference_cuda_device_ptr input_matrix, + king_inference_cuda_device_ptr output_matrix, + zend_ulong input_stride, + zend_ulong source_offset, + zend_ulong head_dim, + zend_ulong batch_count, + zend_ulong position_start, + double position_scale, + double rope_base, + zend_ulong pairing +) { + king_inference_cuda_launch_kernel_fn cu_launch_kernel; + static king_inference_cuda_launch_kernel_fn cached_cu_launch_kernel = NULL; + unsigned int input_stride32; + unsigned int source_offset32; + unsigned int head_dim32; + unsigned int batch_count32; + unsigned long long position_start64; + float position_scale32 = (float) position_scale; + float rope_base32 = (float) rope_base; + unsigned int block_x = 128; + unsigned int grid_x; + unsigned int pairing32; + void *params[10]; + int result; + + if (!model->cuda_rope_batch_available || model->cuda_rope_batch_function == NULL) { + king_inference_cuda_rope_set_error(model, -1, "cuda_rope_batch_unavailable"); + return FAILURE; + } + if (input_matrix == 0 || output_matrix == 0) { + king_inference_cuda_rope_set_error(model, -1, "cuda_rope_batch_argument_invalid"); + return FAILURE; + } + if (input_stride == 0 + || input_stride > UINT_MAX + || source_offset > input_stride + || head_dim == 0 + || (head_dim % 2) != 0 + || head_dim > input_stride - source_offset + || head_dim > UINT_MAX + || batch_count == 0 + || batch_count > UINT_MAX + || pairing > 1 + || !isfinite(position_scale) + || !isfinite(rope_base) + || rope_base <= 0.0) { + king_inference_cuda_rope_set_error(model, -1, "cuda_rope_batch_shape_invalid"); + return FAILURE; + } + if (cached_cu_launch_kernel == NULL + && !king_inference_cuda_rope_driver_symbol(model, "cuLaunchKernel", (void **) &cached_cu_launch_kernel)) { + return FAILURE; + } + cu_launch_kernel = cached_cu_launch_kernel; + + input_stride32 = (unsigned int) input_stride; + source_offset32 = (unsigned int) source_offset; + head_dim32 = (unsigned int) head_dim; + batch_count32 = (unsigned int) batch_count; + position_start64 = (unsigned long long) position_start; + pairing32 = (unsigned int) pairing; + grid_x = ((head_dim32 >> 1) + block_x - 1) / block_x; + params[0] = &input_matrix; + params[1] = &output_matrix; + params[2] = &input_stride32; + params[3] = &source_offset32; + params[4] = &head_dim32; + params[5] = &batch_count32; + params[6] = &position_start64; + params[7] = &position_scale32; + params[8] = &rope_base32; + params[9] = &pairing32; + + result = cu_launch_kernel( + model->cuda_rope_batch_function, + grid_x, + batch_count32, + 1, + block_x, + 1, + 1, + 0, + NULL, + params, + NULL + ); + if (result != 0) { + king_inference_cuda_rope_set_error(model, result, "cuLaunchKernel_batch_failed"); + return FAILURE; + } + + model->cuda_rope_batch_launch_count++; + model->cuda_rope_result = 0; + model->cuda_rope_error[0] = '\0'; + return SUCCESS; +} + +static void king_inference_cuda_rope_add_status( + king_inference_model_object *model, + zval *return_value +) { + zval rope; + + array_init(&rope); + add_assoc_bool(&rope, "attempted", model->cuda_rope_attempted); + add_assoc_bool(&rope, "available", model->cuda_rope_available); + add_assoc_bool(&rope, "nvrtc_available", model->cuda_rope_nvrtc_available); + add_assoc_bool(&rope, "module_loaded", model->cuda_rope_module_loaded); + add_assoc_bool(&rope, "f32_available", model->cuda_rope_f32_available); + add_assoc_bool(&rope, "batch_available", model->cuda_rope_batch_available); + add_assoc_long(&rope, "launches", (zend_long) model->cuda_rope_launch_count); + add_assoc_long(&rope, "batch_launches", (zend_long) model->cuda_rope_batch_launch_count); + add_assoc_long(&rope, "synchronizations", (zend_long) model->cuda_rope_sync_count); + add_assoc_long(&rope, "batch_synchronizations", (zend_long) model->cuda_rope_batch_sync_count); + add_assoc_string(&rope, "synchronization_mode", "deferred_to_downstream_stream_barrier"); + add_assoc_long(&rope, "result", model->cuda_rope_result); + add_assoc_string(&rope, "error", model->cuda_rope_error); + add_assoc_zval(return_value, "rope_kernel", &rope); +} + +static void king_inference_cuda_rope_apply_status( + king_inference_model_object *model, + zval *return_value +) { + zval *reason; + bool reason_can_be_rope; + + king_inference_cuda_rope_add_status(model, return_value); + if (!model->cuda_rope_attempted + || model->cuda_rope_available + || !model->cuda_context_available) { + return; + } + + reason = zend_hash_str_find(Z_ARRVAL_P(return_value), "reason", sizeof("reason") - 1); + reason_can_be_rope = reason == NULL + || Z_TYPE_P(reason) != IS_STRING + || zend_string_equals_literal(Z_STR_P(reason), "gpu_ready") + || zend_string_equals_literal(Z_STR_P(reason), "gpu_decoder_kernel_not_implemented"); + if (reason_can_be_rope) { + king_inference_cuda_context_prepend_return_reason(return_value, "gpu_rope_kernel_unavailable"); + add_assoc_string(return_value, "reason", "gpu_rope_kernel_unavailable"); + } else { + king_inference_gpu_status_append_return_reason(return_value, "gpu_rope_kernel_unavailable"); + } +} diff --git a/extension/src/inference/cuda/prompt/loop/cuda_decoder_prompt_loop.inc b/extension/src/inference/cuda/prompt/loop/cuda_decoder_prompt_loop.inc new file mode 100644 index 000000000..27aa7ee71 --- /dev/null +++ b/extension/src/inference/cuda/prompt/loop/cuda_decoder_prompt_loop.inc @@ -0,0 +1,859 @@ +/* + * CUDA prompt decoder loop contract for native King GPU inference. + */ + +#define KING_INFERENCE_CUDA_PROMPT_BATCH_PREFILL_MIN_TOKENS 16 + +#include "cuda_decoder_prompt_profile.inc" + +static void king_inference_cuda_decoder_prompt_loop_reset_fields(king_inference_model_object *model) +{ + model->cuda_decoder_prompt_loop_result = 0; + model->cuda_decoder_prompt_loop_error[0] = '\0'; + model->cuda_decoder_prompt_loop_count = 0; + model->cuda_decoder_prompt_loop_last_prompt_tokens = 0; + model->cuda_decoder_prompt_loop_last_validated_graphs = 0; + model->cuda_decoder_prompt_loop_last_result_envelopes = 0; + model->cuda_decoder_prompt_loop_last_prefill_batches = 0; + model->cuda_decoder_prompt_loop_last_prefill_tokens = 0; + model->cuda_decoder_prompt_loop_last_prefill_template_reuses = 0; + model->cuda_decoder_prompt_loop_last_decoded_tokens = 0; + model->cuda_decoder_prompt_loop_last_kv_cache_reserve_calls = 0; + model->cuda_decoder_prompt_loop_last_kv_cache_allocations = 0; + model->cuda_decoder_prompt_loop_last_kv_cache_writes = 0; + model->cuda_decoder_prompt_loop_last_kv_cache_batch_writes = 0; + model->cuda_decoder_prompt_loop_last_graph_constructions = 0; + model->cuda_decoder_prompt_loop_last_plan_constructions = 0; + model->cuda_decoder_prompt_loop_last_policy_checks = 0; + model->cuda_decoder_prompt_loop_last_policy_interval_skips = 0; + model->cuda_decoder_prompt_loop_last_thermal_runtime_checks = 0; + model->cuda_decoder_prompt_loop_last_power_runtime_checks = 0; + model->cuda_decoder_prompt_loop_last_prefill_ns = 0; + model->cuda_decoder_prompt_prefill_embeddings = 0; + model->cuda_decoder_prompt_prefill_initial_norm = 0; + model->cuda_decoder_prompt_prefill_initial_linear = 0; + model->cuda_decoder_prompt_prefill_initial_query_rope = 0; + model->cuda_decoder_prompt_prefill_layer0_attention_stack = 0; + model->cuda_decoder_prompt_prefill_layer0_attention_residual = 0; + model->cuda_decoder_prompt_prefill_embedding_bytes = 0; + model->cuda_decoder_prompt_prefill_initial_norm_bytes = 0; + model->cuda_decoder_prompt_prefill_initial_linear_bytes = 0; + model->cuda_decoder_prompt_prefill_initial_query_rope_bytes = 0; + model->cuda_decoder_prompt_prefill_layer0_attention_stack_bytes = 0; + model->cuda_decoder_prompt_prefill_layer0_attention_residual_bytes = 0; + model->cuda_decoder_prompt_prefill_kv_cache_writes = 0; + model->cuda_decoder_prompt_prefill_tokens = 0; + model->cuda_decoder_prompt_prefill_width = 0; + model->cuda_decoder_prompt_prefill_initial_linear_rows = 0; + model->cuda_decoder_prompt_prefill_initial_query_rope_heads = 0; + model->cuda_decoder_prompt_prefill_initial_query_rope_head_dim = 0; + model->cuda_decoder_prompt_prefill_kv_heads = 0; + model->cuda_decoder_prompt_prefill_kv_key_length = 0; + model->cuda_decoder_prompt_prefill_kv_value_length = 0; + model->cuda_decoder_prompt_prefill_layer0_attention_heads = 0; + model->cuda_decoder_prompt_prefill_layer0_attention_stack_width = 0; + model->cuda_decoder_prompt_prefill_layer0_attention_residual_width = 0; + model->cuda_decoder_prompt_loop_last_max_tokens = 0; + model->cuda_decoder_prompt_loop_last_thermal_interval_seconds = 0; + model->cuda_decoder_prompt_loop_last_power_interval_seconds = 0; + king_inference_cuda_decoder_prompt_loop_reset_hot_token_profile(model); + model->cuda_decoder_prompt_loop_attempted = false; + model->cuda_decoder_prompt_loop_available = false; + model->cuda_decoder_prompt_loop_tokenizer_ready = false; + model->cuda_decoder_prompt_loop_graph_validation_ready = false; + model->cuda_decoder_prompt_loop_result_contract_ready = false; + model->cuda_decoder_prompt_loop_batch_prefill_available = false; + model->cuda_decoder_prompt_loop_last_used_batch_prefill = false; + model->cuda_decoder_prompt_loop_last_reuse_profile_available = false; + model->cuda_decoder_prompt_prefill_embeddings_active = false; +} + +static void king_inference_cuda_decoder_prompt_loop_set_error( + king_inference_model_object *model, + int result, + const char *message +) { + if (message == NULL || message[0] == '\0') { + message = "unknown_cuda_decoder_prompt_loop_error"; + } + + model->cuda_decoder_prompt_loop_result = result; + snprintf( + model->cuda_decoder_prompt_loop_error, + sizeof(model->cuda_decoder_prompt_loop_error), + "%s", + message + ); +} + +static bool king_inference_cuda_decoder_prompt_loop_tokenizer_ready(king_inference_model_object *model) +{ + return model->gguf.tokenizer_tokens_loaded + && model->gguf.tokenizer_lookup_loaded + && Z_TYPE(model->tokenizer_tokens) == IS_ARRAY + && Z_TYPE(model->tokenizer_lookup) == IS_ARRAY + && zend_hash_num_elements(Z_ARRVAL(model->tokenizer_tokens)) > 0 + && zend_hash_num_elements(Z_ARRVAL(model->tokenizer_lookup)) > 0; +} + +static bool king_inference_cuda_decoder_prompt_loop_batch_prefill_dependencies_ready( + king_inference_model_object *model +) { + return model != NULL + && model->cuda_decoder_prompt_loop_tokenizer_ready + && model->cuda_decoder_prompt_loop_graph_validation_ready + && model->cuda_decoder_prompt_loop_result_contract_ready + && model->cuda_decoder_graph_executor_execution_plan_available + && model->cuda_decoder_graph_executor_embedding_execution_available + && model->cuda_decoder_graph_executor_rms_norm_execution_available + && model->cuda_decoder_graph_executor_linear_execution_available + && model->cuda_decoder_graph_executor_slice_execution_available + && model->cuda_decoder_graph_executor_rope_execution_available + && model->cuda_decoder_graph_executor_kv_head_prepare_execution_available + && model->cuda_decoder_graph_executor_kv_write_execution_available + && model->cuda_decoder_graph_executor_kv_attention_execution_available + && model->cuda_decoder_graph_executor_attention_stack_slot_execution_available + && model->cuda_decoder_graph_executor_attention_heads_execution_available + && model->cuda_decoder_graph_executor_attention_stack_execution_available + && model->cuda_decoder_graph_executor_attention_output_projection_execution_available + && model->cuda_decoder_graph_executor_attention_residual_execution_available + && model->cuda_decoder_graph_executor_ffn_norm_execution_available + && model->cuda_decoder_graph_executor_ffn_gate_up_projection_execution_available + && model->cuda_decoder_graph_executor_ffn_swiglu_execution_available + && model->cuda_decoder_graph_executor_ffn_down_projection_execution_available + && model->cuda_decoder_graph_executor_ffn_output_residual_execution_available; +} + +static bool king_inference_cuda_decoder_prompt_loop_batch_prefill_experimental_enabled( + king_inference_model_object *model +) { + zval *gpu; + + if (model == NULL) { + return false; + } + + gpu = king_inference_array_find(&model->config, "gpu"); + if (gpu != NULL && Z_TYPE_P(gpu) == IS_ARRAY) { + return king_inference_bool_from_array( + gpu, + "batch_prefill_experimental_enable", + king_high_perf_compute_ai_config.inference_gpu_batch_prefill_experimental_enable + ); + } + + return king_high_perf_compute_ai_config.inference_gpu_batch_prefill_experimental_enable; +} + +static bool king_inference_cuda_decoder_prompt_loop_batch_prefill_admitted( + king_inference_model_object *model +) { + return model != NULL + && model->cuda_decoder_prompt_loop_batch_prefill_available + && king_inference_cuda_decoder_prompt_loop_batch_prefill_experimental_enabled(model) + && king_inference_cuda_decoder_prompt_loop_batch_prefill_dependencies_ready(model); +} + +static bool king_inference_cuda_decoder_prompt_loop_batch_prefill_ready( + king_inference_model_object *model +) { + (void) model; + return false; +} + +static const char *king_inference_cuda_decoder_prompt_loop_batch_prefill_status( + king_inference_model_object *model +) { + if (model == NULL) { + return "unavailable"; + } + if (!king_inference_cuda_decoder_prompt_loop_batch_prefill_admitted(model)) { + return "disabled"; + } + return king_inference_cuda_decoder_prompt_loop_batch_prefill_ready(model) + ? "ready" + : "experimental"; +} + +static const char *king_inference_cuda_decoder_prompt_loop_batch_prefill_admission_reason( + king_inference_model_object *model +) { + if (model == NULL) { + return "model_unavailable"; + } + if (!model->cuda_decoder_prompt_loop_attempted) { + return "prompt_loop_not_attempted"; + } + if (!model->cuda_decoder_prompt_loop_available) { + return model->cuda_decoder_prompt_loop_error[0] != '\0' + ? model->cuda_decoder_prompt_loop_error + : "prompt_loop_unavailable"; + } + if (!king_inference_cuda_decoder_prompt_loop_batch_prefill_experimental_enabled(model)) { + return "batch_prefill_experimental_config_disabled"; + } + if (!king_inference_cuda_decoder_prompt_loop_batch_prefill_dependencies_ready(model)) { + return "batch_prefill_decoder_dependencies_unavailable"; + } + if (!model->cuda_decoder_prompt_loop_batch_prefill_available) { + return "batch_prefill_disabled_until_kv_correctness_hardened"; + } + return king_inference_cuda_decoder_prompt_loop_batch_prefill_ready(model) + ? "batch_prefill_ready" + : "batch_prefill_experimental_config_enabled"; +} + +#include "cuda_decoder_prompt_loop_sampling.inc" +#include "../prefill/query/cuda_decoder_prompt_prefill_query_batch.inc" +#include "../prefill/kv/cuda_decoder_prompt_prefill_kv_batch.inc" +#include "../prefill/ffn/cuda_decoder_prompt_prefill_ffn_batch.inc" +#include "../prefill/attention/cuda_decoder_prompt_prefill_attention_batch.inc" +#include "../prefill/remaining/cuda_decoder_prompt_prefill_remaining_batch.inc" +#include "../prefill/batch/cuda_decoder_prompt_batch_prefill.inc" + +static zend_result king_inference_cuda_decoder_prompt_loop_prefill_sequential( + king_inference_model_object *model, + zval *tokenized_prompt, + zend_ulong token_count, + zval *decode_options, + size_t *validated_graphs, + size_t *result_envelopes, + size_t *prefill_tokens +) { + zend_ulong position; + + if (token_count <= 1) { + return SUCCESS; + } + for (position = 0; position + 1 < token_count; position++) { + zval graph; + zval graph_result; + + ZVAL_UNDEF(&graph); + ZVAL_UNDEF(&graph_result); + add_assoc_bool(decode_options, "emit_token", false); + if (king_inference_token_decode_graph_array(model, tokenized_prompt, position, decode_options, &graph) != SUCCESS + || king_inference_cuda_decoder_graph_executor_prepare_result(model, &graph, &graph_result) != SUCCESS) { + if (!Z_ISUNDEF(graph_result)) { + zval_ptr_dtor(&graph_result); + } + if (!Z_ISUNDEF(graph)) { + zval_ptr_dtor(&graph); + } + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + model->cuda_decoder_graph_executor_error[0] != '\0' + ? model->cuda_decoder_graph_executor_error + : "cuda_decoder_prompt_loop_sequential_prefill_failed" + ); + return FAILURE; + } + (*validated_graphs)++; + (*result_envelopes)++; + (*prefill_tokens)++; + zval_ptr_dtor(&graph_result); + zval_ptr_dtor(&graph); + } + model->cuda_decoder_prompt_loop_last_used_batch_prefill = false; + return SUCCESS; +} + +#include "cuda_decoder_prompt_stream.inc" + +static void king_inference_cuda_decoder_prompt_loop_release(king_inference_model_object *model) +{ + king_inference_cuda_decoder_prompt_loop_release_prefill_embeddings(model); + king_inference_cuda_decoder_prompt_loop_reset_fields(model); +} + +static void king_inference_cuda_decoder_prompt_loop_init( + king_inference_model_object *model, + king_inference_backend_kind backend_kind +) { + king_inference_cuda_decoder_prompt_loop_release(model); + if (backend_kind != KING_INFERENCE_BACKEND_KING_NATIVE_GPU + || !king_inference_gpu_enabled(&model->config)) { + return; + } + + model->cuda_decoder_prompt_loop_attempted = true; + model->cuda_decoder_prompt_loop_tokenizer_ready = + king_inference_cuda_decoder_prompt_loop_tokenizer_ready(model); + model->cuda_decoder_prompt_loop_graph_validation_ready = + model->cuda_decoder_graph_executor_available; + model->cuda_decoder_prompt_loop_result_contract_ready = + model->cuda_decoder_graph_executor_result_contract_available; + model->cuda_decoder_prompt_loop_batch_prefill_available = + king_inference_cuda_decoder_prompt_loop_batch_prefill_experimental_enabled(model) + && king_inference_cuda_decoder_prompt_loop_batch_prefill_dependencies_ready(model); + + if (!model->cuda_decoder_prompt_loop_tokenizer_ready) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_loop_tokenizer_unavailable" + ); + return; + } + if (!model->cuda_decoder_prompt_loop_graph_validation_ready) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_loop_graph_executor_unavailable" + ); + return; + } + if (!model->cuda_decoder_prompt_loop_result_contract_ready) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_loop_result_contract_unavailable" + ); + return; + } + + model->cuda_decoder_prompt_loop_available = true; + /* + * Batch prefill remains experimental because the current batch path can + * produce invalid KV state around the 16-token admission threshold. Normal + * generation must stay on sequential GPU prefill unless an operator enables + * king.inference_gpu_batch_prefill_experimental_enable explicitly. + */ + model->cuda_decoder_prompt_loop_result = 0; + model->cuda_decoder_prompt_loop_error[0] = '\0'; +} + +static zend_result king_inference_cuda_decoder_prompt_loop_validate_prompt( + king_inference_model_object *model, + king_inference_stream_object *stream, + zval *prompt_text, + zend_long max_tokens +) { + zval encoded; + zval tokenized_prompt; + zval decode_options; + zval stops; + zval event; + zend_ulong token_count = 0; + zend_ulong position; + zend_ulong decoded_token_id = 0; + size_t validated_graphs = 0; + size_t result_envelopes = 0; + size_t prefill_batches = 0; + size_t prefill_tokens = 0; + size_t prefill_template_reuses = 0; + size_t decoded_tokens = 0; + zend_string *decoded_token_text = NULL; + double decoded_probability = 0.0; + double decoded_logit = 0.0; + double decoded_rank = 0.0; + uint64_t prefill_start_ns; + uint64_t prefill_end_ns; + bool stopped = false; + + ZVAL_UNDEF(&encoded); + ZVAL_UNDEF(&tokenized_prompt); + ZVAL_UNDEF(&decode_options); + ZVAL_UNDEF(&stops); + + if (!model->cuda_decoder_prompt_loop_available) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_loop_unavailable" + ); + return FAILURE; + } + if (prompt_text == NULL || Z_TYPE_P(prompt_text) != IS_STRING || Z_STRLEN_P(prompt_text) == 0) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_loop_prompt_invalid"); + return FAILURE; + } + if (max_tokens <= 0) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_loop_max_tokens_invalid"); + return FAILURE; + } + + if (king_inference_tokenizer_encode(model, Z_STR_P(prompt_text), &encoded) != SUCCESS + || king_inference_king_native_prompt_tokens(model, &encoded, &tokenized_prompt, &token_count) != SUCCESS) { + if (!Z_ISUNDEF(encoded)) { + zval_ptr_dtor(&encoded); + } + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_loop_tokenize_failed"); + return FAILURE; + } + if (token_count == 0) { + zval_ptr_dtor(&tokenized_prompt); + zval_ptr_dtor(&encoded); + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_loop_tokenize_empty"); + return FAILURE; + } + + if (king_inference_king_native_prompt_stop_list(&stream->request, &stops) != SUCCESS) { + zval_ptr_dtor(&tokenized_prompt); + zval_ptr_dtor(&encoded); + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_loop_stop_list_invalid"); + return FAILURE; + } + king_inference_king_native_prompt_add_default_stops(model, &stops); + king_inference_king_native_prompt_decode_options(stream, &decode_options); + prefill_start_ns = king_inference_cuda_decoder_prompt_profile_ns(); + if (token_count >= KING_INFERENCE_CUDA_PROMPT_BATCH_PREFILL_MIN_TOKENS + && king_inference_cuda_decoder_prompt_loop_batch_prefill_admitted(model)) { + if (king_inference_cuda_decoder_prompt_loop_prefill_batch_template( + model, + &tokenized_prompt, + token_count, + &decode_options, + &validated_graphs, + &result_envelopes, + &prefill_batches, + &prefill_tokens, + &prefill_template_reuses + ) != SUCCESS) { + zval_ptr_dtor(&decode_options); + zval_ptr_dtor(&stops); + zval_ptr_dtor(&tokenized_prompt); + zval_ptr_dtor(&encoded); + return FAILURE; + } + } else if (king_inference_cuda_decoder_prompt_loop_prefill_sequential( + model, + &tokenized_prompt, + token_count, + &decode_options, + &validated_graphs, + &result_envelopes, + &prefill_tokens + ) != SUCCESS) { + zval_ptr_dtor(&decode_options); + zval_ptr_dtor(&stops); + zval_ptr_dtor(&tokenized_prompt); + zval_ptr_dtor(&encoded); + return FAILURE; + } + prefill_end_ns = king_inference_cuda_decoder_prompt_profile_ns(); + model->cuda_decoder_prompt_loop_last_prefill_ns = + king_inference_cuda_decoder_prompt_profile_elapsed(prefill_end_ns, prefill_start_ns); + + position = token_count - 1; + { + zval graph; + zval plan_result; + king_inference_cuda_decoder_prefill_mutations mutations; + zend_ulong initial_token_id = 0; + + ZVAL_UNDEF(&graph); + ZVAL_UNDEF(&plan_result); + memset(&mutations, 0, sizeof(mutations)); + add_assoc_bool(&decode_options, "emit_token", true); + if (king_inference_cuda_decoder_prompt_loop_token_at(&tokenized_prompt, position, &initial_token_id) != SUCCESS + || king_inference_token_decode_graph_array(model, &tokenized_prompt, position, &decode_options, &graph) != SUCCESS + || king_inference_cuda_decoder_prompt_loop_prepare_decode_template(model, &graph, &plan_result) != SUCCESS + || king_inference_cuda_decoder_prompt_loop_build_decode_mutations(model, &graph, &mutations) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_prefill_mutations_release(&mutations); + if (!Z_ISUNDEF(plan_result)) { + zval_ptr_dtor(&plan_result); + } + if (!Z_ISUNDEF(graph)) { + zval_ptr_dtor(&graph); + } + zval_ptr_dtor(&decode_options); + zval_ptr_dtor(&stops); + zval_ptr_dtor(&tokenized_prompt); + zval_ptr_dtor(&encoded); + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + model->cuda_decoder_graph_executor_error[0] != '\0' + ? model->cuda_decoder_graph_executor_error + : "cuda_decoder_prompt_loop_graph_validation_failed" + ); + return FAILURE; + } + validated_graphs++; + if (king_inference_check_gpu_policy_during_run(stream, "King GPU decoder loop") != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_prefill_mutations_release(&mutations); + zval_ptr_dtor(&plan_result); + zval_ptr_dtor(&graph); + zval_ptr_dtor(&decode_options); + zval_ptr_dtor(&stops); + zval_ptr_dtor(&tokenized_prompt); + zval_ptr_dtor(&encoded); + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_loop_guardrail_failed"); + return FAILURE; + } + if (king_inference_cuda_decoder_prompt_loop_execute_decode_template( + model, + stream, + &graph, + &mutations, + initial_token_id, + position, + &decoded_token_id, + &decoded_token_text, + &decoded_probability, + &decoded_logit, + &decoded_rank, + NULL, + NULL, + NULL, + NULL, + NULL + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_prefill_mutations_release(&mutations); + zval_ptr_dtor(&plan_result); + zval_ptr_dtor(&graph); + zval_ptr_dtor(&decode_options); + zval_ptr_dtor(&stops); + zval_ptr_dtor(&tokenized_prompt); + zval_ptr_dtor(&encoded); + return FAILURE; + } + if (king_inference_cuda_decoder_prompt_loop_emit_decoded_text( + model, + stream, + decoded_token_id, + decoded_token_text, + &stops, + NULL, + &stopped + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_prefill_mutations_release(&mutations); + zval_ptr_dtor(&plan_result); + zval_ptr_dtor(&graph); + zval_ptr_dtor(&decode_options); + zval_ptr_dtor(&stops); + zval_ptr_dtor(&tokenized_prompt); + zval_ptr_dtor(&encoded); + if (decoded_token_text != NULL) { + zend_string_release(decoded_token_text); + } + return FAILURE; + } + decoded_tokens++; + result_envelopes++; + + while (decoded_tokens < (size_t) max_tokens && !stopped) { + zend_ulong source_token_id; + + if (position >= (zend_ulong) ZEND_LONG_MAX || decoded_token_id > (zend_ulong) ZEND_LONG_MAX) { + king_inference_cuda_decoder_prompt_loop_prefill_mutations_release(&mutations); + zval_ptr_dtor(&plan_result); + zval_ptr_dtor(&graph); + zval_ptr_dtor(&decode_options); + zval_ptr_dtor(&stops); + zval_ptr_dtor(&tokenized_prompt); + zval_ptr_dtor(&encoded); + if (decoded_token_text != NULL) { + zend_string_release(decoded_token_text); + } + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_loop_position_limit_exceeded"); + return FAILURE; + } + + source_token_id = decoded_token_id; + position++; + if (king_inference_check_gpu_policy_during_run(stream, "King GPU decoder loop") != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_prefill_mutations_release(&mutations); + zval_ptr_dtor(&plan_result); + zval_ptr_dtor(&graph); + zval_ptr_dtor(&decode_options); + zval_ptr_dtor(&stops); + zval_ptr_dtor(&tokenized_prompt); + zval_ptr_dtor(&encoded); + if (decoded_token_text != NULL) { + zend_string_release(decoded_token_text); + } + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_loop_guardrail_failed"); + return FAILURE; + } + if (king_inference_cuda_decoder_prompt_loop_execute_decode_template( + model, + stream, + &graph, + &mutations, + source_token_id, + position, + &decoded_token_id, + &decoded_token_text, + &decoded_probability, + &decoded_logit, + &decoded_rank, + NULL, + NULL, + NULL, + NULL, + NULL + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_prefill_mutations_release(&mutations); + zval_ptr_dtor(&plan_result); + zval_ptr_dtor(&graph); + zval_ptr_dtor(&decode_options); + zval_ptr_dtor(&stops); + zval_ptr_dtor(&tokenized_prompt); + zval_ptr_dtor(&encoded); + if (decoded_token_text != NULL) { + zend_string_release(decoded_token_text); + } + return FAILURE; + } + if (king_inference_cuda_decoder_prompt_loop_emit_decoded_text( + model, + stream, + decoded_token_id, + decoded_token_text, + &stops, + NULL, + &stopped + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_prefill_mutations_release(&mutations); + zval_ptr_dtor(&plan_result); + zval_ptr_dtor(&graph); + zval_ptr_dtor(&decode_options); + zval_ptr_dtor(&stops); + zval_ptr_dtor(&tokenized_prompt); + zval_ptr_dtor(&encoded); + if (decoded_token_text != NULL) { + zend_string_release(decoded_token_text); + } + return FAILURE; + } + decoded_tokens++; + result_envelopes++; + } + + king_inference_cuda_decoder_prompt_loop_prefill_mutations_release(&mutations); + zval_ptr_dtor(&plan_result); + zval_ptr_dtor(&graph); + } + zval_ptr_dtor(&stops); + if (!Z_ISUNDEF(stream->native_events) + && Z_TYPE(stream->native_events) == IS_ARRAY + && zend_hash_num_elements(Z_ARRVAL(stream->native_events)) == 0) { + add_next_index_stringl(&stream->native_events, "", 0); + } + + model->cuda_decoder_prompt_loop_count++; + model->cuda_decoder_prompt_loop_last_prompt_tokens = token_count; + model->cuda_decoder_prompt_loop_last_validated_graphs = validated_graphs; + model->cuda_decoder_prompt_loop_last_result_envelopes = result_envelopes; + model->cuda_decoder_prompt_loop_last_prefill_batches = prefill_batches; + model->cuda_decoder_prompt_loop_last_prefill_tokens = prefill_tokens; + model->cuda_decoder_prompt_loop_last_prefill_template_reuses = prefill_template_reuses; + model->cuda_decoder_prompt_loop_last_max_tokens = max_tokens; + model->cuda_decoder_prompt_loop_result = 0; + model->cuda_decoder_prompt_loop_error[0] = '\0'; + + if (king_inference_king_native_stream_diagnostics_enabled(stream)) { + array_init(&event); + add_assoc_string(&event, "type", "gpu_prompt_decoder_loop_contract"); + add_assoc_string(&event, "backend", "king_native_gpu"); + add_assoc_bool(&event, "prompt_loop_ready", true); + add_assoc_bool(&event, "prompt_batch_prefill_available", model->cuda_decoder_prompt_loop_batch_prefill_available); + add_assoc_bool(&event, "prompt_batch_prefill_used", model->cuda_decoder_prompt_loop_last_used_batch_prefill); + add_assoc_bool(&event, "decode_graph_template_resident", true); + add_assoc_bool(&event, "decode_graph_constructed_on_hot_path", false); + add_assoc_bool(&event, "token_events_streamed_immediately", true); + add_assoc_bool(&event, "stop_condition_reached", stopped); + add_assoc_bool(&event, "prompt_graphs_validated", true); + add_assoc_bool(&event, "graph_result_contract_ready", true); + add_assoc_bool(&event, "graph_execution_plan_ready", true); + add_assoc_bool( + &event, + "embedding_device_execution_ready", + model->cuda_decoder_graph_executor_embedding_execution_available + ); + add_assoc_bool( + &event, + "rms_norm_device_execution_ready", + model->cuda_decoder_graph_executor_rms_norm_execution_available + ); + add_assoc_bool( + &event, + "linear_device_execution_ready", + model->cuda_decoder_graph_executor_linear_execution_available + ); + add_assoc_bool( + &event, + "slice_device_execution_ready", + model->cuda_decoder_graph_executor_slice_execution_available + ); + add_assoc_bool( + &event, + "rope_device_execution_ready", + model->cuda_decoder_graph_executor_rope_execution_available + ); + add_assoc_bool( + &event, + "kv_head_prepare_device_execution_ready", + model->cuda_decoder_graph_executor_kv_head_prepare_execution_available + ); + add_assoc_bool( + &event, + "kv_write_device_execution_ready", + model->cuda_decoder_graph_executor_kv_write_execution_available + ); + add_assoc_bool( + &event, + "kv_attention_device_execution_ready", + model->cuda_decoder_graph_executor_kv_attention_execution_available + ); + add_assoc_bool( + &event, + "attention_stack_slot_device_execution_ready", + model->cuda_decoder_graph_executor_attention_stack_slot_execution_available + ); + add_assoc_bool( + &event, + "attention_heads_device_execution_ready", + model->cuda_decoder_graph_executor_attention_heads_execution_available + ); + add_assoc_bool( + &event, + "attention_stack_device_execution_ready", + model->cuda_decoder_graph_executor_attention_stack_execution_available + ); + add_assoc_bool( + &event, + "attention_output_projection_device_execution_ready", + model->cuda_decoder_graph_executor_attention_output_projection_execution_available + ); + add_assoc_bool( + &event, + "attention_residual_device_execution_ready", + model->cuda_decoder_graph_executor_attention_residual_execution_available + ); + add_assoc_bool( + &event, + "ffn_norm_device_execution_ready", + model->cuda_decoder_graph_executor_ffn_norm_execution_available + ); + add_assoc_bool( + &event, + "ffn_gate_up_projection_device_execution_ready", + model->cuda_decoder_graph_executor_ffn_gate_up_projection_execution_available + ); + add_assoc_bool( + &event, + "ffn_swiglu_device_execution_ready", + model->cuda_decoder_graph_executor_ffn_swiglu_execution_available + ); + add_assoc_bool( + &event, + "ffn_down_projection_device_execution_ready", + model->cuda_decoder_graph_executor_ffn_down_projection_execution_available + ); + add_assoc_bool( + &event, + "ffn_output_residual_device_execution_ready", + model->cuda_decoder_graph_executor_ffn_output_residual_execution_available + ); + add_assoc_bool( + &event, + "final_norm_device_execution_ready", + model->cuda_decoder_graph_executor_final_norm_execution_available + ); + add_assoc_bool( + &event, + "logits_projection_device_execution_ready", + model->cuda_decoder_graph_executor_logits_projection_execution_available + ); + add_assoc_bool(&event, "device_execution_result_ready", decoded_tokens > 0); + add_assoc_bool(&event, "generation_ready", false); + add_assoc_bool(&event, "prompt_generation_ready", decoded_tokens == (size_t) max_tokens); + add_assoc_long(&event, "prompt_tokens", (zend_long) token_count); + add_assoc_long(&event, "prefill_batches", (zend_long) prefill_batches); + add_assoc_long(&event, "prefill_tokens", (zend_long) prefill_tokens); + add_assoc_long(&event, "prefill_template_reuses", (zend_long) prefill_template_reuses); + add_assoc_long(&event, "prefill_embedding_bytes", (zend_long) model->cuda_decoder_prompt_prefill_embedding_bytes); + add_assoc_long(&event, "prefill_initial_norm_bytes", (zend_long) model->cuda_decoder_prompt_prefill_initial_norm_bytes); + add_assoc_long(&event, "prefill_initial_linear_bytes", (zend_long) model->cuda_decoder_prompt_prefill_initial_linear_bytes); + add_assoc_long( + &event, + "prefill_initial_linear_rows", + (zend_long) model->cuda_decoder_prompt_prefill_initial_linear_rows + ); + add_assoc_long( + &event, + "prefill_initial_query_rope_bytes", + (zend_long) model->cuda_decoder_prompt_prefill_initial_query_rope_bytes + ); + add_assoc_long( + &event, + "prefill_initial_query_rope_heads", + (zend_long) model->cuda_decoder_prompt_prefill_initial_query_rope_heads + ); + add_assoc_long( + &event, + "prefill_layer0_attention_stack_bytes", + (zend_long) model->cuda_decoder_prompt_prefill_layer0_attention_stack_bytes + ); + add_assoc_long( + &event, + "prefill_layer0_attention_residual_bytes", + (zend_long) model->cuda_decoder_prompt_prefill_layer0_attention_residual_bytes + ); + add_assoc_long(&event, "prefill_kv_heads", (zend_long) model->cuda_decoder_prompt_prefill_kv_heads); + add_assoc_long( + &event, + "prefill_kv_cache_writes", + (zend_long) model->cuda_decoder_prompt_prefill_kv_cache_writes + ); + add_assoc_long(&event, "validated_graphs", (zend_long) validated_graphs); + add_assoc_long(&event, "result_envelopes", (zend_long) result_envelopes); + add_assoc_long(&event, "decoded_tokens", (zend_long) decoded_tokens); + add_assoc_long(&event, "accepted_max_tokens", max_tokens); + add_assoc_bool(&event, "bounded_logits_consumed", decoded_tokens > 0); + add_assoc_bool(&event, "token_result_ready", decoded_tokens > 0); + add_assoc_bool(&event, "decoded_token_ready", decoded_tokens > 0); + if (decoded_tokens > 0) { + add_assoc_long(&event, "decoded_token_id", (zend_long) decoded_token_id); + add_assoc_str(&event, "decoded_token_text", zend_string_copy(decoded_token_text)); + add_assoc_double(&event, "decoded_token_probability", decoded_probability); + add_assoc_double(&event, "decoded_token_logit", decoded_logit); + add_assoc_double(&event, "decoded_token_rank", decoded_rank); + } + add_next_index_zval(&stream->native_events, &event); + } + if (decoded_token_text != NULL) { + zend_string_release(decoded_token_text); + } + + zval_ptr_dtor(&decode_options); + zval_ptr_dtor(&tokenized_prompt); + zval_ptr_dtor(&encoded); + return SUCCESS; +} + +#include "cuda_decoder_prompt_loop_status.inc" + +static void king_inference_cuda_decoder_prompt_loop_apply_status( + king_inference_model_object *model, + zval *return_value +) { + zval *reason; + bool reason_can_be_loop; + + king_inference_cuda_decoder_prompt_loop_add_status(model, return_value); + if (!model->cuda_decoder_prompt_loop_attempted + || model->cuda_decoder_prompt_loop_available + || !king_inference_gpu_enabled(&model->config)) { + return; + } + + reason = zend_hash_str_find(Z_ARRVAL_P(return_value), "reason", sizeof("reason") - 1); + reason_can_be_loop = reason == NULL + || Z_TYPE_P(reason) != IS_STRING + || zend_string_equals_literal(Z_STR_P(reason), "gpu_ready") + || zend_string_equals_literal(Z_STR_P(reason), "gpu_decoder_kernel_not_implemented"); + if (reason_can_be_loop) { + king_inference_cuda_context_prepend_return_reason( + return_value, + "gpu_decoder_prompt_loop_unavailable" + ); + add_assoc_string(return_value, "reason", "gpu_decoder_prompt_loop_unavailable"); + } else { + king_inference_gpu_status_append_return_reason(return_value, "gpu_decoder_prompt_loop_unavailable"); + } +} diff --git a/extension/src/inference/cuda/prompt/loop/cuda_decoder_prompt_loop_sampling.inc b/extension/src/inference/cuda/prompt/loop/cuda_decoder_prompt_loop_sampling.inc new file mode 100644 index 000000000..5ea2ffe76 --- /dev/null +++ b/extension/src/inference/cuda/prompt/loop/cuda_decoder_prompt_loop_sampling.inc @@ -0,0 +1,387 @@ +/* + * CUDA prompt decoder loop sampling helpers. + */ + +typedef struct _king_inference_cuda_prompt_candidate { + zend_ulong token_id; + double logit; + double probability; + double rank; +} king_inference_cuda_prompt_candidate; + +static zend_result king_inference_cuda_decoder_prompt_loop_candidate( + zval *candidate, + zend_ulong fallback_rank, + king_inference_cuda_prompt_candidate *out +) { + zval *token_id = king_inference_array_find(candidate, "token_id"); + zval *logit = king_inference_array_find(candidate, "logit"); + zval *rank = king_inference_array_find(candidate, "rank"); + + if (candidate == NULL + || Z_TYPE_P(candidate) != IS_ARRAY + || token_id == NULL + || Z_TYPE_P(token_id) != IS_LONG + || Z_LVAL_P(token_id) < 0 + || logit == NULL + || (Z_TYPE_P(logit) != IS_DOUBLE && Z_TYPE_P(logit) != IS_LONG)) { + return FAILURE; + } + + out->token_id = (zend_ulong) Z_LVAL_P(token_id); + out->logit = Z_TYPE_P(logit) == IS_DOUBLE ? Z_DVAL_P(logit) : (double) Z_LVAL_P(logit); + out->probability = 1.0; + out->rank = fallback_rank; + if (rank != NULL && Z_TYPE_P(rank) == IS_LONG && Z_LVAL_P(rank) >= 0) { + out->rank = (double) Z_LVAL_P(rank); + } + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_prompt_loop_select_token( + zval *graph, + zval *candidates, + king_inference_cuda_prompt_candidate *selected +) { + zval *op = king_inference_king_native_next_token_op(graph); + zval *op_name; + king_inference_cuda_prompt_candidate *items; + zend_ulong count; + zend_ulong index; + zend_ulong top_k = 0; + zend_ulong active_count; + zend_ulong selected_rank = 0; + zend_ulong sample_index; + double temperature = 0.0; + double top_p = 1.0; + double max_score = 0.0; + double probability_sum = 0.0; + double active_sum = 0.0; + bool has_seed = false; + zend_long seed = 0; + + if (op == NULL + || candidates == NULL + || Z_TYPE_P(candidates) != IS_ARRAY + || zend_hash_num_elements(Z_ARRVAL_P(candidates)) == 0 + || king_inference_graph_finite_double_option(op, "temperature", 0.0, &temperature) != SUCCESS + || king_inference_graph_finite_double_option(op, "top_p", 1.0, &top_p) != SUCCESS + || king_inference_graph_sampling_seed(op, &has_seed, &seed) != SUCCESS + || king_inference_graph_sampling_top_k(op, &top_k) != SUCCESS) { + return FAILURE; + } + if (temperature < 0.0 || top_p <= 0.0 || top_p > 1.0) { + return FAILURE; + } + + count = (zend_ulong) zend_hash_num_elements(Z_ARRVAL_P(candidates)); + items = ecalloc((size_t) count, sizeof(*items)); + for (index = 0; index < count; index++) { + zval *candidate = zend_hash_index_find(Z_ARRVAL_P(candidates), index); + + if (king_inference_cuda_decoder_prompt_loop_candidate(candidate, index, &items[index]) != SUCCESS) { + efree(items); + return FAILURE; + } + } + + op_name = king_inference_array_find(op, "op"); + if (op_name != NULL + && Z_TYPE_P(op_name) == IS_STRING + && zend_string_equals_literal(Z_STR_P(op_name), "argmax_token")) { + *selected = items[0]; + selected->probability = 1.0; + efree(items); + return SUCCESS; + } + if (temperature == 0.0) { + *selected = items[0]; + selected->probability = 1.0; + efree(items); + return SUCCESS; + } + + for (index = 0; index < count; index++) { + double score = items[index].logit / temperature; + + items[index].probability = score; + if (index == 0 || score > max_score) { + max_score = score; + } + } + for (index = 0; index < count; index++) { + items[index].probability = king_inference_graph_exp(items[index].probability - max_score); + probability_sum += items[index].probability; + } + if (probability_sum <= 0.0) { + efree(items); + return FAILURE; + } + for (index = 0; index < count; index++) { + items[index].probability /= probability_sum; + } + + active_count = top_k == 0 || top_k > count ? count : top_k; + probability_sum = 0.0; + for (index = 0; index < active_count; index++) { + probability_sum += items[index].probability; + if (probability_sum >= top_p) { + active_count = index + 1; + break; + } + } + for (index = 0; index < active_count; index++) { + active_sum += items[index].probability; + } + if (active_sum <= 0.0) { + efree(items); + return FAILURE; + } + if (has_seed) { + double target; + double cumulative = 0.0; + + sample_index = king_inference_tensor_option_ulong(op, "sample_index", 0); + target = king_inference_graph_sampling_unit(seed, count ^ (active_count << 7) ^ sample_index) * active_sum; + selected_rank = active_count - 1; + for (index = 0; index < active_count; index++) { + cumulative += items[index].probability; + if (target <= cumulative) { + selected_rank = index; + break; + } + } + } + + *selected = items[selected_rank]; + selected->probability = selected->probability / active_sum; + selected->rank = (double) selected_rank; + efree(items); + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_prompt_loop_write_token_result( + zval *graph_result, + king_inference_cuda_prompt_candidate *selected +) { + zval final; + zval payload; + zval values; + + array_init(&values); + add_next_index_long(&values, (zend_long) selected->token_id); + add_next_index_double(&values, selected->probability); + add_next_index_double(&values, selected->logit); + add_next_index_double(&values, selected->rank); + + array_init(&payload); + add_assoc_string(&payload, "type", "vector"); + add_assoc_long(&payload, "length", 4); + add_assoc_zval(&payload, "values", &values); + + array_init(&final); + add_assoc_zval(&final, "next_token", &payload); + add_assoc_zval(graph_result, "final", &final); + add_assoc_bool(graph_result, "token_result_ready", true); + add_assoc_bool(graph_result, "decoded_token_ready", true); + add_assoc_long(graph_result, "selected_token_id", (zend_long) selected->token_id); + add_assoc_double(graph_result, "selected_token_probability", selected->probability); + add_assoc_double(graph_result, "selected_token_logit", selected->logit); + add_assoc_double(graph_result, "selected_token_rank", selected->rank); + return SUCCESS; +} + +static void king_inference_cuda_decoder_prompt_loop_write_selected_metrics( + king_inference_stream_object *stream, + king_inference_cuda_prompt_candidate *selected +) { + stream->native_decoder_token_count++; + stream->native_decoder_last_token_id = selected->token_id; + stream->native_decoder_last_token_available = true; + stream->native_decoder_last_probability = selected->probability; + stream->native_decoder_last_logit = selected->logit; + stream->native_decoder_last_rank = selected->rank; + stream->native_decoder_last_score_available = true; +} + +static zend_result king_inference_cuda_decoder_prompt_loop_write_compact_token_result( + zval *graph_result, + king_inference_cuda_prompt_candidate *selected +) { + add_assoc_bool(graph_result, "token_result_ready", true); + add_assoc_bool(graph_result, "decoded_token_ready", true); + add_assoc_long(graph_result, "selected_token_id", (zend_long) selected->token_id); + add_assoc_double(graph_result, "selected_token_probability", selected->probability); + add_assoc_double(graph_result, "selected_token_logit", selected->logit); + add_assoc_double(graph_result, "selected_token_rank", selected->rank); + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_prompt_loop_selected_probe_candidate( + zval *logits_probe, + king_inference_cuda_prompt_candidate *selected +) { + zval *ready; + zval *token_id; + zval *probability; + zval *logit; + zval *rank; + + if (logits_probe == NULL || Z_TYPE_P(logits_probe) != IS_ARRAY) { + return FAILURE; + } + ready = king_inference_array_find(logits_probe, "selected_token_ready"); + token_id = king_inference_array_find(logits_probe, "selected_token_id"); + probability = king_inference_array_find(logits_probe, "selected_token_probability"); + logit = king_inference_array_find(logits_probe, "selected_token_logit"); + rank = king_inference_array_find(logits_probe, "selected_token_rank"); + if (ready == NULL + || !zend_is_true(ready) + || token_id == NULL + || Z_TYPE_P(token_id) != IS_LONG + || Z_LVAL_P(token_id) < 0 + || probability == NULL + || (Z_TYPE_P(probability) != IS_DOUBLE && Z_TYPE_P(probability) != IS_LONG) + || logit == NULL + || (Z_TYPE_P(logit) != IS_DOUBLE && Z_TYPE_P(logit) != IS_LONG) + || rank == NULL + || (Z_TYPE_P(rank) != IS_DOUBLE && Z_TYPE_P(rank) != IS_LONG)) { + return FAILURE; + } + + selected->token_id = (zend_ulong) Z_LVAL_P(token_id); + selected->probability = Z_TYPE_P(probability) == IS_DOUBLE ? Z_DVAL_P(probability) : (double) Z_LVAL_P(probability); + selected->logit = Z_TYPE_P(logit) == IS_DOUBLE ? Z_DVAL_P(logit) : (double) Z_LVAL_P(logit); + selected->rank = Z_TYPE_P(rank) == IS_DOUBLE ? Z_DVAL_P(rank) : (double) Z_LVAL_P(rank); + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_prompt_loop_consume_bounded_logits( + king_inference_model_object *model, + king_inference_stream_object *stream, + zval *graph, + zval *graph_result, + zend_ulong *token_id, + double *probability, + double *logit, + double *rank +) { + zval *logits_probe = king_inference_array_find(graph_result, "logits_projection_device_execution"); + zval *candidates = logits_probe != NULL && Z_TYPE_P(logits_probe) == IS_ARRAY + ? king_inference_array_find(logits_probe, "candidates") + : NULL; + zval *compact_decode_result = king_inference_array_find(graph_result, "compact_decode_result"); + king_inference_cuda_prompt_candidate selected; + + if (king_inference_cuda_decoder_prompt_loop_selected_probe_candidate(logits_probe, &selected) != SUCCESS + && king_inference_cuda_decoder_prompt_loop_select_token(graph, candidates, &selected) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_loop_bounded_logits_sample_failed"); + return FAILURE; + } + if (compact_decode_result != NULL && zend_is_true(compact_decode_result)) { + if (king_inference_cuda_decoder_prompt_loop_write_compact_token_result(graph_result, &selected) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_loop_bounded_logits_sample_failed"); + return FAILURE; + } + king_inference_cuda_decoder_prompt_loop_write_selected_metrics(stream, &selected); + } else if (king_inference_cuda_decoder_prompt_loop_write_token_result(graph_result, &selected) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_loop_bounded_logits_sample_failed"); + return FAILURE; + } else { + king_inference_king_native_update_decoder_metrics(stream, graph_result, graph, selected.token_id); + } + + *token_id = selected.token_id; + *probability = selected.probability; + *logit = selected.logit; + *rank = selected.rank; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_prompt_loop_emit_bounded_token( + king_inference_model_object *model, + king_inference_stream_object *stream, + zval *graph, + zval *graph_result, + zend_ulong *token_id, + zend_string **last_text, + double *probability, + double *logit, + double *rank, + zend_long *sampler_ns, + zend_long *token_decode_ns +) { + zend_string *piece; + uint64_t start_ns; + uint64_t end_ns; + + start_ns = king_inference_cuda_decoder_prompt_profile_ns(); + if (king_inference_cuda_decoder_prompt_loop_consume_bounded_logits( + model, + stream, + graph, + graph_result, + token_id, + probability, + logit, + rank + ) != SUCCESS) { + return FAILURE; + } + end_ns = king_inference_cuda_decoder_prompt_profile_ns(); + if (sampler_ns != NULL) { + *sampler_ns = king_inference_cuda_decoder_prompt_profile_elapsed(end_ns, start_ns); + } + + start_ns = king_inference_cuda_decoder_prompt_profile_ns(); + piece = king_inference_tokenizer_decode_token(model, *token_id); + end_ns = king_inference_cuda_decoder_prompt_profile_ns(); + if (token_decode_ns != NULL) { + *token_decode_ns = king_inference_cuda_decoder_prompt_profile_elapsed(end_ns, start_ns); + } + if (piece == NULL) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_loop_decode_token_failed"); + return FAILURE; + } + if (*last_text != NULL) { + zend_string_release(*last_text); + } + *last_text = zend_string_copy(piece); + zend_string_release(piece); + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_prompt_loop_emit_decoded_text( + king_inference_model_object *model, + king_inference_stream_object *stream, + zend_ulong token_id, + zend_string *decoded_text, + zval *stops, + smart_str *pending, + bool *stopped +) { + *stopped = false; + if (king_inference_king_native_prompt_should_stop_token(model, token_id)) { + if (pending != NULL) { + king_inference_king_native_prompt_flush_pending(stream, pending); + } + *stopped = true; + return SUCCESS; + } + if (decoded_text == NULL || ZSTR_LEN(decoded_text) == 0) { + return SUCCESS; + } + if (Z_ISUNDEF(stream->native_events) || Z_TYPE(stream->native_events) != IS_ARRAY) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_loop_native_stream_unavailable"); + return FAILURE; + } + if (pending != NULL) { + return king_inference_king_native_prompt_emit_text(stream, pending, decoded_text, stops, stopped); + } + if (king_inference_king_native_prompt_piece_matches_stop(decoded_text, stops)) { + *stopped = true; + return SUCCESS; + } + add_next_index_str(&stream->native_events, zend_string_copy(decoded_text)); + return SUCCESS; +} diff --git a/extension/src/inference/cuda/prompt/loop/cuda_decoder_prompt_loop_status.inc b/extension/src/inference/cuda/prompt/loop/cuda_decoder_prompt_loop_status.inc new file mode 100644 index 000000000..da5e0e5d1 --- /dev/null +++ b/extension/src/inference/cuda/prompt/loop/cuda_decoder_prompt_loop_status.inc @@ -0,0 +1,285 @@ +/* + * CUDA prompt decoder loop status projection. + */ + +static void king_inference_cuda_decoder_prompt_loop_add_status( + king_inference_model_object *model, + zval *return_value +) { + zval loop; + zval *config_ready = king_inference_array_find(return_value, "config_ready"); + bool runtime_ready = config_ready != NULL && zend_is_true(config_ready); + bool prompt_generation_ready = runtime_ready && model->cuda_decoder_prompt_loop_available; + + array_init(&loop); + add_assoc_bool(&loop, "attempted", model->cuda_decoder_prompt_loop_attempted); + add_assoc_bool(&loop, "available", model->cuda_decoder_prompt_loop_available); + add_assoc_bool(&loop, "batch_prefill_compiled", true); + add_assoc_bool(&loop, "batch_prefill_available", model->cuda_decoder_prompt_loop_batch_prefill_available); + add_assoc_bool(&loop, "batch_prefill_ready", king_inference_cuda_decoder_prompt_loop_batch_prefill_ready(model)); + add_assoc_bool(&loop, "batch_prefill_experimental_enabled", king_inference_cuda_decoder_prompt_loop_batch_prefill_experimental_enabled(model)); + add_assoc_bool(&loop, "batch_prefill_admitted", king_inference_cuda_decoder_prompt_loop_batch_prefill_admitted(model)); + add_assoc_bool(&loop, "batch_prefill_request_control_allowed", false); + add_assoc_string(&loop, "batch_prefill_status", king_inference_cuda_decoder_prompt_loop_batch_prefill_status(model)); + add_assoc_string( + &loop, + "batch_prefill_admission_reason", + king_inference_cuda_decoder_prompt_loop_batch_prefill_admission_reason(model) + ); + add_assoc_bool(&loop, "last_used_batch_prefill", model->cuda_decoder_prompt_loop_last_used_batch_prefill); + add_assoc_long( + &loop, + "batch_prefill_min_prompt_tokens", + (zend_long) KING_INFERENCE_CUDA_PROMPT_BATCH_PREFILL_MIN_TOKENS + ); + add_assoc_bool(&loop, "tokenizer_ready", model->cuda_decoder_prompt_loop_tokenizer_ready); + add_assoc_bool( + &loop, + "graph_validation_ready", + model->cuda_decoder_prompt_loop_graph_validation_ready + ); + add_assoc_bool( + &loop, + "result_contract_ready", + model->cuda_decoder_prompt_loop_result_contract_ready + ); + add_assoc_bool( + &loop, + "execution_plan_ready", + model->cuda_decoder_graph_executor_execution_plan_available + ); + add_assoc_bool( + &loop, + "embedding_device_execution_ready", + model->cuda_decoder_graph_executor_embedding_execution_available + ); + add_assoc_bool( + &loop, + "rms_norm_device_execution_ready", + model->cuda_decoder_graph_executor_rms_norm_execution_available + ); + add_assoc_bool( + &loop, + "linear_device_execution_ready", + model->cuda_decoder_graph_executor_linear_execution_available + ); + add_assoc_bool( + &loop, + "slice_device_execution_ready", + model->cuda_decoder_graph_executor_slice_execution_available + ); + add_assoc_bool( + &loop, + "rope_device_execution_ready", + model->cuda_decoder_graph_executor_rope_execution_available + ); + add_assoc_bool( + &loop, + "kv_head_prepare_device_execution_ready", + model->cuda_decoder_graph_executor_kv_head_prepare_execution_available + ); + add_assoc_bool( + &loop, + "kv_write_device_execution_ready", + model->cuda_decoder_graph_executor_kv_write_execution_available + ); + add_assoc_bool( + &loop, + "kv_attention_device_execution_ready", + model->cuda_decoder_graph_executor_kv_attention_execution_available + ); + add_assoc_bool( + &loop, + "attention_stack_slot_device_execution_ready", + model->cuda_decoder_graph_executor_attention_stack_slot_execution_available + ); + add_assoc_bool( + &loop, + "attention_heads_device_execution_ready", + model->cuda_decoder_graph_executor_attention_heads_execution_available + ); + add_assoc_bool( + &loop, + "attention_stack_device_execution_ready", + model->cuda_decoder_graph_executor_attention_stack_execution_available + ); + add_assoc_bool( + &loop, + "attention_output_projection_device_execution_ready", + model->cuda_decoder_graph_executor_attention_output_projection_execution_available + ); + add_assoc_bool( + &loop, + "attention_residual_device_execution_ready", + model->cuda_decoder_graph_executor_attention_residual_execution_available + ); + add_assoc_bool( + &loop, + "ffn_norm_device_execution_ready", + model->cuda_decoder_graph_executor_ffn_norm_execution_available + ); + add_assoc_bool( + &loop, + "ffn_gate_up_projection_device_execution_ready", + model->cuda_decoder_graph_executor_ffn_gate_up_projection_execution_available + ); + add_assoc_bool( + &loop, + "ffn_swiglu_device_execution_ready", + model->cuda_decoder_graph_executor_ffn_swiglu_execution_available + ); + add_assoc_bool( + &loop, + "ffn_down_projection_device_execution_ready", + model->cuda_decoder_graph_executor_ffn_down_projection_execution_available + ); + add_assoc_bool( + &loop, + "ffn_output_residual_device_execution_ready", + model->cuda_decoder_graph_executor_ffn_output_residual_execution_available + ); + add_assoc_bool( + &loop, + "final_norm_device_execution_ready", + model->cuda_decoder_graph_executor_final_norm_execution_available + ); + add_assoc_bool( + &loop, + "logits_projection_device_execution_ready", + model->cuda_decoder_graph_executor_logits_projection_execution_available + ); + add_assoc_bool(&loop, "device_execution_result_ready", model->cuda_decoder_prompt_loop_count > 0); + add_assoc_bool(&loop, "token_result_ready", model->cuda_decoder_prompt_loop_count > 0); + add_assoc_bool(&loop, "decoded_token_ready", model->cuda_decoder_prompt_loop_count > 0); + add_assoc_long(&loop, "runs", (zend_long) model->cuda_decoder_prompt_loop_count); + add_assoc_long(&loop, "last_prompt_tokens", (zend_long) model->cuda_decoder_prompt_loop_last_prompt_tokens); + add_assoc_long(&loop, "last_decoded_tokens", (zend_long) model->cuda_decoder_prompt_loop_last_decoded_tokens); + add_assoc_long(&loop, "last_prefill_batches", (zend_long) model->cuda_decoder_prompt_loop_last_prefill_batches); + add_assoc_long(&loop, "last_prefill_tokens", (zend_long) model->cuda_decoder_prompt_loop_last_prefill_tokens); + add_assoc_long(&loop, "last_prefill_duration_ns", model->cuda_decoder_prompt_loop_last_prefill_ns); + add_assoc_double( + &loop, + "last_prefill_tokens_per_second", + model->cuda_decoder_prompt_loop_last_prefill_ns > 0 + ? ((double) model->cuda_decoder_prompt_loop_last_prefill_tokens * 1000000000.0) + / (double) model->cuda_decoder_prompt_loop_last_prefill_ns + : 0.0 + ); + add_assoc_long( + &loop, + "prefill_embedding_bytes", + (zend_long) model->cuda_decoder_prompt_prefill_embedding_bytes + ); + add_assoc_long( + &loop, + "prefill_initial_norm_bytes", + (zend_long) model->cuda_decoder_prompt_prefill_initial_norm_bytes + ); + add_assoc_long( + &loop, + "prefill_initial_linear_bytes", + (zend_long) model->cuda_decoder_prompt_prefill_initial_linear_bytes + ); + add_assoc_long( + &loop, + "prefill_initial_linear_rows", + (zend_long) model->cuda_decoder_prompt_prefill_initial_linear_rows + ); + add_assoc_long( + &loop, + "prefill_initial_query_rope_bytes", + (zend_long) model->cuda_decoder_prompt_prefill_initial_query_rope_bytes + ); + add_assoc_long( + &loop, + "prefill_initial_query_rope_heads", + (zend_long) model->cuda_decoder_prompt_prefill_initial_query_rope_heads + ); + add_assoc_long( + &loop, + "prefill_layer0_attention_stack_bytes", + (zend_long) model->cuda_decoder_prompt_prefill_layer0_attention_stack_bytes + ); + add_assoc_long( + &loop, + "prefill_layer0_attention_residual_bytes", + (zend_long) model->cuda_decoder_prompt_prefill_layer0_attention_residual_bytes + ); + add_assoc_long(&loop, "prefill_kv_heads", (zend_long) model->cuda_decoder_prompt_prefill_kv_heads); + add_assoc_long( + &loop, + "prefill_kv_cache_writes", + (zend_long) model->cuda_decoder_prompt_prefill_kv_cache_writes + ); + add_assoc_long( + &loop, + "last_prefill_template_reuses", + (zend_long) model->cuda_decoder_prompt_loop_last_prefill_template_reuses + ); + add_assoc_bool( + &loop, + "last_reuse_profile_available", + model->cuda_decoder_prompt_loop_last_reuse_profile_available + ); + add_assoc_long( + &loop, + "last_kv_cache_reserve_calls", + (zend_long) model->cuda_decoder_prompt_loop_last_kv_cache_reserve_calls + ); + add_assoc_long( + &loop, + "last_kv_cache_logical_allocations", + (zend_long) model->cuda_decoder_prompt_loop_last_kv_cache_allocations + ); + add_assoc_long( + &loop, + "last_request_graph_constructions", + (zend_long) model->cuda_decoder_prompt_loop_last_graph_constructions + ); + add_assoc_long( + &loop, + "last_request_execution_plan_constructions", + (zend_long) model->cuda_decoder_prompt_loop_last_plan_constructions + ); + add_assoc_long( + &loop, + "last_policy_guard_calls", + (zend_long) model->cuda_decoder_prompt_loop_last_policy_checks + ); + add_assoc_long( + &loop, + "last_policy_guard_interval_skips", + (zend_long) model->cuda_decoder_prompt_loop_last_policy_interval_skips + ); + add_assoc_long( + &loop, + "last_thermal_runtime_sensor_reads", + (zend_long) model->cuda_decoder_prompt_loop_last_thermal_runtime_checks + ); + add_assoc_long( + &loop, + "last_power_runtime_sensor_reads", + (zend_long) model->cuda_decoder_prompt_loop_last_power_runtime_checks + ); + add_assoc_long( + &loop, + "last_validated_graphs", + (zend_long) model->cuda_decoder_prompt_loop_last_validated_graphs + ); + add_assoc_long( + &loop, + "last_result_envelopes", + (zend_long) model->cuda_decoder_prompt_loop_last_result_envelopes + ); + add_assoc_long(&loop, "last_max_tokens", model->cuda_decoder_prompt_loop_last_max_tokens); + add_assoc_long(&loop, "result", model->cuda_decoder_prompt_loop_result); + add_assoc_string(&loop, "error", model->cuda_decoder_prompt_loop_error); + add_assoc_zval(return_value, "decoder_prompt_loop", &loop); + add_assoc_bool(return_value, "decoder_prompt_loop_ready", model->cuda_decoder_prompt_loop_available); + add_assoc_bool(return_value, "plain_text_chat_ready", prompt_generation_ready); + add_assoc_bool(return_value, "decoder_kernel_ready", model->cuda_decoder_prompt_loop_available); + add_assoc_bool(return_value, "generation_ready", prompt_generation_ready); + add_assoc_bool(return_value, "token_generation", prompt_generation_ready); + add_assoc_bool(return_value, "openai_generation", prompt_generation_ready); + add_assoc_bool(return_value, "gpu_plain_text_chat_generation", prompt_generation_ready); +} diff --git a/extension/src/inference/cuda/prompt/loop/cuda_decoder_prompt_profile.inc b/extension/src/inference/cuda/prompt/loop/cuda_decoder_prompt_profile.inc new file mode 100644 index 000000000..596572ab7 --- /dev/null +++ b/extension/src/inference/cuda/prompt/loop/cuda_decoder_prompt_profile.inc @@ -0,0 +1,403 @@ +/* + * CUDA prompt decoder hot-token profiling. + */ + +typedef struct _king_inference_cuda_decoder_prompt_profile_snapshot { + size_t embedding_launches; + size_t matvec_launches; + size_t rms_norm_launches; + size_t rope_launches; + size_t rope_syncs; + size_t attention_launches; + size_t vector_launches; + size_t ffn_launches; + size_t projection_launches; + size_t readback_launches; + size_t readback_syncs; + size_t dtoh_copies; + size_t allocator_acquisitions; + size_t graph_validations; + size_t graph_constructions; + size_t execution_plans; + size_t tensor_lookups; + size_t op_index_lookups; + size_t op_index_hits; + size_t op_index_fallback_scans; + size_t readback_candidates; + size_t readback_bytes; + size_t full_logits_bytes; + size_t saved_readback_bytes; + zend_long readback_ns; +} king_inference_cuda_decoder_prompt_profile_snapshot; + +static uint64_t king_inference_cuda_decoder_prompt_profile_ns(void) +{ + struct timespec ts; + + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) { + return (uint64_t) time(NULL) * 1000000000ULL; + } + return ((uint64_t) ts.tv_sec * 1000000000ULL) + (uint64_t) ts.tv_nsec; +} + +static size_t king_inference_cuda_decoder_prompt_profile_graph_counter(zval *graph, const char *key) +{ + zval *value; + + if (graph == NULL || Z_TYPE_P(graph) != IS_ARRAY || key == NULL) { + return 0; + } + value = king_inference_array_find(graph, key); + return value != NULL && Z_TYPE_P(value) == IS_LONG && Z_LVAL_P(value) >= 0 + ? (size_t) Z_LVAL_P(value) + : 0; +} + +static void king_inference_cuda_decoder_prompt_profile_capture( + king_inference_model_object *model, + zval *graph, + king_inference_cuda_decoder_prompt_profile_snapshot *snapshot +) { + snapshot->embedding_launches = + model->cuda_embedding_row_launch_count + model->cuda_embedding_rows_launch_count; + snapshot->matvec_launches = + model->cuda_quantized_matvec_launch_count + model->cuda_quantized_matvec_batch_launch_count; + snapshot->rms_norm_launches = + model->cuda_rms_norm_launch_count + model->cuda_rms_norm_batch_launch_count; + snapshot->rope_launches = model->cuda_rope_launch_count + model->cuda_rope_batch_launch_count; + snapshot->rope_syncs = model->cuda_rope_sync_count + model->cuda_rope_batch_sync_count; + snapshot->attention_launches = + model->cuda_attention_scores_launch_count + + model->cuda_attention_scores_batch_launch_count + + model->cuda_attention_softmax_launch_count + + model->cuda_attention_softmax_batch_launch_count + + model->cuda_attention_values_launch_count + + model->cuda_attention_values_batch_launch_count; + snapshot->vector_launches = model->cuda_device_vector_ops_launch_count; + snapshot->ffn_launches = model->cuda_ffn_swiglu_launch_count; + snapshot->projection_launches = model->cuda_output_projection_launch_count; + snapshot->readback_launches = model->cuda_logits_readback_launch_count; + snapshot->readback_syncs = model->cuda_logits_readback_sync_count; + snapshot->dtoh_copies = model->cuda_logits_readback_dtoh_count; + snapshot->allocator_acquisitions = model->cuda_device_allocation_count; + snapshot->graph_validations = model->cuda_decoder_graph_executor_graph_count; + snapshot->graph_constructions = 0; + snapshot->execution_plans = model->cuda_decoder_graph_executor_plan_count; + snapshot->tensor_lookups = model->cuda_decoder_graph_executor_tensor_lookup_count; + snapshot->op_index_lookups = king_inference_cuda_decoder_prompt_profile_graph_counter( + graph, + "_cuda_profile_op_index_lookups" + ); + snapshot->op_index_hits = king_inference_cuda_decoder_prompt_profile_graph_counter( + graph, + "_cuda_profile_op_index_hits" + ); + snapshot->op_index_fallback_scans = king_inference_cuda_decoder_prompt_profile_graph_counter( + graph, + "_cuda_profile_op_index_fallback_scans" + ); + snapshot->readback_candidates = model->cuda_logits_readback_last_candidate_count; + snapshot->readback_bytes = model->cuda_logits_readback_last_bytes; + snapshot->full_logits_bytes = model->cuda_logits_readback_full_bytes; + snapshot->saved_readback_bytes = model->cuda_logits_readback_saved_bytes; + snapshot->readback_ns = model->cuda_logits_readback_last_ns; +} + +static size_t king_inference_cuda_decoder_prompt_profile_delta(size_t after, size_t before) +{ + return after >= before ? after - before : 0; +} + +static zend_long king_inference_cuda_decoder_prompt_profile_elapsed(uint64_t end, uint64_t start) +{ + return end >= start && end - start <= (uint64_t) ZEND_LONG_MAX + ? (zend_long) (end - start) + : 0; +} + +static void king_inference_cuda_decoder_prompt_loop_reset_hot_token_profile( + king_inference_model_object *model +) { + model->cuda_decoder_prompt_loop_last_hot_token_total_ns = 0; + model->cuda_decoder_prompt_loop_last_hot_token_policy_ns = 0; + model->cuda_decoder_prompt_loop_last_hot_token_mutation_ns = 0; + model->cuda_decoder_prompt_loop_last_hot_token_device_execute_ns = 0; + model->cuda_decoder_prompt_loop_last_hot_token_bounded_emit_ns = 0; + model->cuda_decoder_prompt_loop_last_hot_token_logits_readback_ns = 0; + model->cuda_decoder_prompt_loop_last_hot_token_sampler_ns = 0; + model->cuda_decoder_prompt_loop_last_hot_token_token_decode_ns = 0; + model->cuda_decoder_prompt_loop_last_hot_token_text_emit_ns = 0; + model->cuda_decoder_prompt_loop_last_hot_token_event_shape_ns = 0; + model->cuda_decoder_prompt_loop_last_hot_token_launches = 0; + model->cuda_decoder_prompt_loop_last_hot_token_syncs = 0; + model->cuda_decoder_prompt_loop_last_hot_token_dtoh_copies = 0; + model->cuda_decoder_prompt_loop_last_hot_token_allocator_acquisitions = 0; + model->cuda_decoder_prompt_loop_last_hot_token_graph_validations = 0; + model->cuda_decoder_prompt_loop_last_hot_token_graph_constructions = 0; + model->cuda_decoder_prompt_loop_last_hot_token_execution_plans = 0; + model->cuda_decoder_prompt_loop_last_hot_token_tensor_lookups = 0; + model->cuda_decoder_prompt_loop_last_hot_token_op_index_lookups = 0; + model->cuda_decoder_prompt_loop_last_hot_token_op_index_hits = 0; + model->cuda_decoder_prompt_loop_last_hot_token_op_index_fallback_scans = 0; + model->cuda_decoder_prompt_loop_last_hot_token_embedding_launches = 0; + model->cuda_decoder_prompt_loop_last_hot_token_matvec_launches = 0; + model->cuda_decoder_prompt_loop_last_hot_token_rms_norm_launches = 0; + model->cuda_decoder_prompt_loop_last_hot_token_rope_launches = 0; + model->cuda_decoder_prompt_loop_last_hot_token_attention_launches = 0; + model->cuda_decoder_prompt_loop_last_hot_token_vector_launches = 0; + model->cuda_decoder_prompt_loop_last_hot_token_ffn_launches = 0; + model->cuda_decoder_prompt_loop_last_hot_token_projection_launches = 0; + model->cuda_decoder_prompt_loop_last_hot_token_readback_launches = 0; + model->cuda_decoder_prompt_loop_last_hot_token_readback_candidates = 0; + model->cuda_decoder_prompt_loop_last_hot_token_readback_bytes = 0; + model->cuda_decoder_prompt_loop_last_hot_token_full_logits_bytes = 0; + model->cuda_decoder_prompt_loop_last_hot_token_saved_readback_bytes = 0; + model->cuda_decoder_prompt_loop_last_readback_tokens = 0; + model->cuda_decoder_prompt_loop_last_readback_bytes = 0; + model->cuda_decoder_prompt_loop_last_full_logits_bytes = 0; + model->cuda_decoder_prompt_loop_last_saved_readback_bytes = 0; + model->cuda_decoder_prompt_loop_last_hot_token_full_logits_readback_avoided = false; + model->cuda_decoder_prompt_loop_last_hot_token_profile_available = false; +} + +static void king_inference_cuda_decoder_prompt_profile_record( + king_inference_model_object *model, + king_inference_cuda_decoder_prompt_profile_snapshot *before, + king_inference_cuda_decoder_prompt_profile_snapshot *after, + zend_long total_ns, + zend_long policy_ns, + zend_long mutation_ns, + zend_long device_execute_ns, + zend_long bounded_emit_ns, + zend_long sampler_ns, + zend_long token_decode_ns, + zend_long text_emit_ns +) { + size_t syncs; + + model->cuda_decoder_prompt_loop_last_hot_token_embedding_launches = + king_inference_cuda_decoder_prompt_profile_delta(after->embedding_launches, before->embedding_launches); + model->cuda_decoder_prompt_loop_last_hot_token_matvec_launches = + king_inference_cuda_decoder_prompt_profile_delta(after->matvec_launches, before->matvec_launches); + model->cuda_decoder_prompt_loop_last_hot_token_rms_norm_launches = + king_inference_cuda_decoder_prompt_profile_delta(after->rms_norm_launches, before->rms_norm_launches); + model->cuda_decoder_prompt_loop_last_hot_token_rope_launches = + king_inference_cuda_decoder_prompt_profile_delta(after->rope_launches, before->rope_launches); + model->cuda_decoder_prompt_loop_last_hot_token_attention_launches = + king_inference_cuda_decoder_prompt_profile_delta(after->attention_launches, before->attention_launches); + model->cuda_decoder_prompt_loop_last_hot_token_vector_launches = + king_inference_cuda_decoder_prompt_profile_delta(after->vector_launches, before->vector_launches); + model->cuda_decoder_prompt_loop_last_hot_token_ffn_launches = + king_inference_cuda_decoder_prompt_profile_delta(after->ffn_launches, before->ffn_launches); + model->cuda_decoder_prompt_loop_last_hot_token_projection_launches = + king_inference_cuda_decoder_prompt_profile_delta(after->projection_launches, before->projection_launches); + model->cuda_decoder_prompt_loop_last_hot_token_readback_launches = + king_inference_cuda_decoder_prompt_profile_delta(after->readback_launches, before->readback_launches); + model->cuda_decoder_prompt_loop_last_hot_token_launches = + model->cuda_decoder_prompt_loop_last_hot_token_embedding_launches + + model->cuda_decoder_prompt_loop_last_hot_token_matvec_launches + + model->cuda_decoder_prompt_loop_last_hot_token_rms_norm_launches + + model->cuda_decoder_prompt_loop_last_hot_token_rope_launches + + model->cuda_decoder_prompt_loop_last_hot_token_attention_launches + + model->cuda_decoder_prompt_loop_last_hot_token_vector_launches + + model->cuda_decoder_prompt_loop_last_hot_token_ffn_launches + + model->cuda_decoder_prompt_loop_last_hot_token_projection_launches + + model->cuda_decoder_prompt_loop_last_hot_token_readback_launches; + syncs = king_inference_cuda_decoder_prompt_profile_delta(after->rope_syncs, before->rope_syncs) + + king_inference_cuda_decoder_prompt_profile_delta(after->readback_syncs, before->readback_syncs); + model->cuda_decoder_prompt_loop_last_hot_token_syncs = syncs; + model->cuda_decoder_prompt_loop_last_hot_token_dtoh_copies = + king_inference_cuda_decoder_prompt_profile_delta(after->dtoh_copies, before->dtoh_copies); + model->cuda_decoder_prompt_loop_last_hot_token_allocator_acquisitions = + king_inference_cuda_decoder_prompt_profile_delta(after->allocator_acquisitions, before->allocator_acquisitions); + model->cuda_decoder_prompt_loop_last_hot_token_graph_validations = + king_inference_cuda_decoder_prompt_profile_delta(after->graph_validations, before->graph_validations); + model->cuda_decoder_prompt_loop_last_hot_token_graph_constructions = + king_inference_cuda_decoder_prompt_profile_delta(after->graph_constructions, before->graph_constructions); + model->cuda_decoder_prompt_loop_last_hot_token_execution_plans = + king_inference_cuda_decoder_prompt_profile_delta(after->execution_plans, before->execution_plans); + model->cuda_decoder_prompt_loop_last_hot_token_tensor_lookups = + king_inference_cuda_decoder_prompt_profile_delta(after->tensor_lookups, before->tensor_lookups); + model->cuda_decoder_prompt_loop_last_hot_token_op_index_lookups = + king_inference_cuda_decoder_prompt_profile_delta(after->op_index_lookups, before->op_index_lookups); + model->cuda_decoder_prompt_loop_last_hot_token_op_index_hits = + king_inference_cuda_decoder_prompt_profile_delta(after->op_index_hits, before->op_index_hits); + model->cuda_decoder_prompt_loop_last_hot_token_op_index_fallback_scans = + king_inference_cuda_decoder_prompt_profile_delta( + after->op_index_fallback_scans, + before->op_index_fallback_scans + ); + model->cuda_decoder_prompt_loop_last_hot_token_total_ns = total_ns; + model->cuda_decoder_prompt_loop_last_hot_token_policy_ns = policy_ns; + model->cuda_decoder_prompt_loop_last_hot_token_mutation_ns = mutation_ns; + model->cuda_decoder_prompt_loop_last_hot_token_device_execute_ns = device_execute_ns; + model->cuda_decoder_prompt_loop_last_hot_token_bounded_emit_ns = bounded_emit_ns; + model->cuda_decoder_prompt_loop_last_hot_token_logits_readback_ns = after->readback_ns; + model->cuda_decoder_prompt_loop_last_hot_token_sampler_ns = sampler_ns; + model->cuda_decoder_prompt_loop_last_hot_token_token_decode_ns = token_decode_ns; + model->cuda_decoder_prompt_loop_last_hot_token_text_emit_ns = text_emit_ns; + model->cuda_decoder_prompt_loop_last_hot_token_event_shape_ns = text_emit_ns; + model->cuda_decoder_prompt_loop_last_hot_token_readback_candidates = after->readback_candidates; + model->cuda_decoder_prompt_loop_last_hot_token_readback_bytes = after->readback_bytes; + model->cuda_decoder_prompt_loop_last_hot_token_full_logits_bytes = after->full_logits_bytes; + model->cuda_decoder_prompt_loop_last_hot_token_saved_readback_bytes = after->saved_readback_bytes; + if (after->readback_bytes > 0 || after->full_logits_bytes > 0) { + model->cuda_decoder_prompt_loop_last_readback_tokens++; + model->cuda_decoder_prompt_loop_last_readback_bytes += after->readback_bytes; + model->cuda_decoder_prompt_loop_last_full_logits_bytes += after->full_logits_bytes; + model->cuda_decoder_prompt_loop_last_saved_readback_bytes += after->saved_readback_bytes; + } + model->cuda_decoder_prompt_loop_last_hot_token_full_logits_readback_avoided = + after->full_logits_bytes > 0 && after->readback_bytes < after->full_logits_bytes; + model->cuda_decoder_prompt_loop_last_hot_token_profile_available = true; +} + +static void king_inference_cuda_decoder_prompt_profile_add_status( + zval *return_value, + king_inference_model_object *model +) { + zval profile; + zval launches; + zval timings; + zval graph; + zval readback; + bool tokenizer_dominates = model->cuda_decoder_prompt_loop_last_hot_token_token_decode_ns > 0 + && model->cuda_decoder_prompt_loop_last_hot_token_total_ns > 0 + && model->cuda_decoder_prompt_loop_last_hot_token_token_decode_ns + > model->cuda_decoder_prompt_loop_last_hot_token_total_ns / 2; + + array_init(&profile); + add_assoc_bool(&profile, "available", model->cuda_decoder_prompt_loop_last_hot_token_profile_available); + add_assoc_long(&profile, "cuda_launches", (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_launches); + add_assoc_long(&profile, "host_device_syncs", (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_syncs); + add_assoc_long(&profile, "device_to_host_copies", (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_dtoh_copies); + add_assoc_long(&profile, "allocator_acquisitions", (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_allocator_acquisitions); + add_assoc_string(&profile, "sync_sources", "none_on_bounded_logits_readback_hotpath"); + add_assoc_string(&profile, "deferred_sync_sources", "rope_default_stream_ordering"); + add_assoc_string(&profile, "dtoh_sources", "blocking_bounded_top_k_indices_and_logits"); + + array_init(&launches); + add_assoc_long(&launches, "embedding", (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_embedding_launches); + add_assoc_long(&launches, "matvec", (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_matvec_launches); + add_assoc_long(&launches, "rms_norm", (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_rms_norm_launches); + add_assoc_long(&launches, "rope", (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_rope_launches); + add_assoc_long(&launches, "attention", (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_attention_launches); + add_assoc_long(&launches, "vector", (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_vector_launches); + add_assoc_long(&launches, "ffn", (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_ffn_launches); + add_assoc_long(&launches, "projection", (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_projection_launches); + add_assoc_long(&launches, "readback", (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_readback_launches); + add_assoc_zval(&profile, "launch_breakdown", &launches); + + array_init(&timings); + add_assoc_long(&timings, "total_ns", model->cuda_decoder_prompt_loop_last_hot_token_total_ns); + add_assoc_long(&timings, "policy_guard_ns", model->cuda_decoder_prompt_loop_last_hot_token_policy_ns); + add_assoc_long(&timings, "graph_mutation_ns", model->cuda_decoder_prompt_loop_last_hot_token_mutation_ns); + add_assoc_long(&timings, "device_execute_ns", model->cuda_decoder_prompt_loop_last_hot_token_device_execute_ns); + add_assoc_long(&timings, "bounded_sampler_and_token_decode_ns", model->cuda_decoder_prompt_loop_last_hot_token_bounded_emit_ns); + add_assoc_long(&timings, "bounded_logits_top_k_readback_ns", model->cuda_decoder_prompt_loop_last_hot_token_logits_readback_ns); + add_assoc_long(&timings, "cpu_sampler_ns", model->cuda_decoder_prompt_loop_last_hot_token_sampler_ns); + add_assoc_long(&timings, "tokenizer_piece_decode_ns", model->cuda_decoder_prompt_loop_last_hot_token_token_decode_ns); + add_assoc_long(&timings, "native_event_text_emit_ns", model->cuda_decoder_prompt_loop_last_hot_token_text_emit_ns); + add_assoc_long(&timings, "http_event_shaping_visible_ns", model->cuda_decoder_prompt_loop_last_hot_token_event_shape_ns); + add_assoc_zval(&profile, "timings", &timings); + + array_init(&readback); + add_assoc_bool(&readback, "bounded_top_k_only", true); + add_assoc_long(&readback, "max_candidates", (zend_long) model->cuda_logits_readback_candidate_limit); + add_assoc_long( + &readback, + "candidate_count", + (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_readback_candidates + ); + add_assoc_long( + &readback, + "readback_bytes", + (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_readback_bytes + ); + add_assoc_long( + &readback, + "full_logits_bytes", + (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_full_logits_bytes + ); + add_assoc_long( + &readback, + "saved_readback_bytes", + (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_saved_readback_bytes + ); + add_assoc_long( + &readback, + "request_readback_tokens", + (zend_long) model->cuda_decoder_prompt_loop_last_readback_tokens + ); + add_assoc_long( + &readback, + "request_readback_bytes", + (zend_long) model->cuda_decoder_prompt_loop_last_readback_bytes + ); + add_assoc_long( + &readback, + "request_full_logits_bytes", + (zend_long) model->cuda_decoder_prompt_loop_last_full_logits_bytes + ); + add_assoc_long( + &readback, + "request_saved_readback_bytes", + (zend_long) model->cuda_decoder_prompt_loop_last_saved_readback_bytes + ); + add_assoc_double( + &readback, + "readback_bytes_per_token", + model->cuda_decoder_prompt_loop_last_readback_tokens > 0 + ? (double) model->cuda_decoder_prompt_loop_last_readback_bytes + / (double) model->cuda_decoder_prompt_loop_last_readback_tokens + : 0.0 + ); + add_assoc_double( + &readback, + "saved_readback_bytes_per_token", + model->cuda_decoder_prompt_loop_last_readback_tokens > 0 + ? (double) model->cuda_decoder_prompt_loop_last_saved_readback_bytes + / (double) model->cuda_decoder_prompt_loop_last_readback_tokens + : 0.0 + ); + add_assoc_double( + &readback, + "readback_savings_ratio", + model->cuda_decoder_prompt_loop_last_full_logits_bytes > 0 + ? (double) model->cuda_decoder_prompt_loop_last_saved_readback_bytes + / (double) model->cuda_decoder_prompt_loop_last_full_logits_bytes + : 0.0 + ); + add_assoc_bool( + &readback, + "full_logits_cpu_readback_avoided", + model->cuda_decoder_prompt_loop_last_hot_token_full_logits_readback_avoided + ); + add_assoc_bool(&readback, "tokenizer_piece_decode_dominates_hot_token", tokenizer_dominates); + add_assoc_zval(&profile, "bounded_logits_readback", &readback); + + array_init(&graph); + add_assoc_long(&graph, "graph_validations", (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_graph_validations); + add_assoc_long(&graph, "graph_constructions", (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_graph_constructions); + add_assoc_long(&graph, "execution_plans", (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_execution_plans); + add_assoc_long(&graph, "tensor_descriptor_lookups", (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_tensor_lookups); + add_assoc_long(&graph, "op_index_lookups", (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_op_index_lookups); + add_assoc_long(&graph, "op_index_hits", (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_op_index_hits); + add_assoc_long(&graph, "op_index_fallback_scans", (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_op_index_fallback_scans); + add_assoc_bool(&graph, "decode_graph_constructed_on_hot_path", false); + add_assoc_bool(&graph, "decode_graph_template_reused", true); + add_assoc_bool( + &graph, + "persistent_decode_plan_index_reused", + model->cuda_decoder_prompt_loop_last_hot_token_graph_constructions == 0 + && model->cuda_decoder_prompt_loop_last_hot_token_execution_plans == 0 + ); + add_assoc_bool( + &graph, + "dynamic_op_scan_eliminated", + model->cuda_decoder_prompt_loop_last_hot_token_op_index_fallback_scans == 0 + ); + add_assoc_zval(&profile, "graph_and_lookup", &graph); + + add_assoc_zval(return_value, "last_hot_token_profile", &profile); +} diff --git a/extension/src/inference/cuda/prompt/loop/cuda_decoder_prompt_stream.inc b/extension/src/inference/cuda/prompt/loop/cuda_decoder_prompt_stream.inc new file mode 100644 index 000000000..9cdd7fa63 --- /dev/null +++ b/extension/src/inference/cuda/prompt/loop/cuda_decoder_prompt_stream.inc @@ -0,0 +1,437 @@ +/* + * Lazy CUDA prompt stream state. + */ + +typedef struct _king_inference_cuda_decoder_prompt_stream_state { + zval tokenized_prompt; + zval decode_options; + zval stops; + zval graph; + zval plan_result; + king_inference_cuda_decoder_prefill_mutations mutations; + zend_long max_tokens; + zend_ulong token_count; + zend_ulong position; + zend_ulong decoded_token_id; + size_t validated_graphs; + size_t result_envelopes; + size_t prefill_batches; + size_t prefill_tokens; + size_t prefill_template_reuses; + size_t decoded_tokens; + size_t kv_cache_reserve_calls_start; + size_t kv_cache_allocations_start; + size_t kv_cache_writes_start; + size_t kv_cache_batch_writes_start; + size_t graph_constructions_start; + size_t plan_constructions_start; + zend_string *decoded_token_text; + smart_str pending; + double decoded_probability; + double decoded_logit; + double decoded_rank; + bool initialized; + bool stopped; + bool finished; +} king_inference_cuda_decoder_prompt_stream_state; + +static king_inference_cuda_decoder_prompt_stream_state *king_inference_cuda_decoder_prompt_stream_state_from( + king_inference_stream_object *stream +) { + return (king_inference_cuda_decoder_prompt_stream_state *) stream->native_gpu_stream_state; +} + +static void king_inference_cuda_decoder_prompt_stream_release(king_inference_stream_object *stream) +{ + king_inference_cuda_decoder_prompt_stream_state *state; + + if (stream == NULL || stream->native_gpu_stream_state == NULL) { + return; + } + + state = king_inference_cuda_decoder_prompt_stream_state_from(stream); + king_inference_cuda_decoder_prompt_loop_prefill_mutations_release(&state->mutations); + if (!Z_ISUNDEF(state->plan_result)) { + zval_ptr_dtor(&state->plan_result); + ZVAL_UNDEF(&state->plan_result); + } + if (!Z_ISUNDEF(state->graph)) { + zval_ptr_dtor(&state->graph); + ZVAL_UNDEF(&state->graph); + } + if (!Z_ISUNDEF(state->stops)) { + zval_ptr_dtor(&state->stops); + ZVAL_UNDEF(&state->stops); + } + if (!Z_ISUNDEF(state->decode_options)) { + zval_ptr_dtor(&state->decode_options); + ZVAL_UNDEF(&state->decode_options); + } + if (!Z_ISUNDEF(state->tokenized_prompt)) { + zval_ptr_dtor(&state->tokenized_prompt); + ZVAL_UNDEF(&state->tokenized_prompt); + } + if (state->decoded_token_text != NULL) { + zend_string_release(state->decoded_token_text); + state->decoded_token_text = NULL; + } + smart_str_free(&state->pending); + efree(state); + stream->native_gpu_stream_state = NULL; +} + +static zend_result king_inference_cuda_decoder_prompt_stream_start( + king_inference_model_object *model, + king_inference_stream_object *stream, + zend_long max_tokens +) { + king_inference_cuda_decoder_prompt_stream_state *state; + + king_inference_cuda_decoder_prompt_stream_release(stream); + if (!model->cuda_decoder_prompt_loop_available) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_loop_unavailable"); + return FAILURE; + } + if (max_tokens <= 0) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_loop_max_tokens_invalid"); + return FAILURE; + } + + state = ecalloc(1, sizeof(*state)); + ZVAL_UNDEF(&state->tokenized_prompt); + ZVAL_UNDEF(&state->decode_options); + ZVAL_UNDEF(&state->stops); + ZVAL_UNDEF(&state->graph); + ZVAL_UNDEF(&state->plan_result); + memset(&state->mutations, 0, sizeof(state->mutations)); + state->max_tokens = max_tokens; + stream->native_gpu_stream_state = state; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_prompt_stream_initialize( + king_inference_model_object *model, + king_inference_stream_object *stream, + king_inference_cuda_decoder_prompt_stream_state *state +) { + zval encoded; + zval *prompt_text = king_inference_array_find(&stream->request, "native_prompt_text"); + zend_ulong initial_token_id = 0; + uint64_t prefill_start_ns; + uint64_t prefill_end_ns; + + ZVAL_UNDEF(&encoded); + if (prompt_text == NULL || Z_TYPE_P(prompt_text) != IS_STRING || Z_STRLEN_P(prompt_text) == 0) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_loop_prompt_invalid"); + return FAILURE; + } + + if (king_inference_tokenizer_encode(model, Z_STR_P(prompt_text), &encoded) != SUCCESS + || king_inference_king_native_prompt_tokens(model, &encoded, &state->tokenized_prompt, &state->token_count) != SUCCESS) { + if (!Z_ISUNDEF(encoded)) { + zval_ptr_dtor(&encoded); + } + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_loop_tokenize_failed"); + return FAILURE; + } + zval_ptr_dtor(&encoded); + if (state->token_count == 0) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_loop_tokenize_empty"); + return FAILURE; + } + + if (king_inference_king_native_prompt_stop_list(&stream->request, &state->stops) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_loop_stop_list_invalid"); + return FAILURE; + } + king_inference_king_native_prompt_add_default_stops(model, &state->stops); + king_inference_king_native_prompt_decode_options(stream, &state->decode_options); + + state->kv_cache_reserve_calls_start = model->cuda_device_kv_cache_reserve_call_count; + state->kv_cache_allocations_start = model->cuda_device_kv_cache_allocation_count; + state->kv_cache_writes_start = model->cuda_device_kv_cache_write_count; + state->kv_cache_batch_writes_start = model->cuda_device_kv_cache_batch_write_count; + state->graph_constructions_start = model->cuda_decoder_graph_executor_graph_count; + state->plan_constructions_start = model->cuda_decoder_graph_executor_plan_count; + + prefill_start_ns = king_inference_cuda_decoder_prompt_profile_ns(); + if (state->token_count >= KING_INFERENCE_CUDA_PROMPT_BATCH_PREFILL_MIN_TOKENS + && king_inference_cuda_decoder_prompt_loop_batch_prefill_admitted(model)) { + if (king_inference_cuda_decoder_prompt_loop_prefill_batch_template( + model, + &state->tokenized_prompt, + state->token_count, + &state->decode_options, + &state->validated_graphs, + &state->result_envelopes, + &state->prefill_batches, + &state->prefill_tokens, + &state->prefill_template_reuses + ) != SUCCESS) { + return FAILURE; + } + } else if (king_inference_cuda_decoder_prompt_loop_prefill_sequential( + model, + &state->tokenized_prompt, + state->token_count, + &state->decode_options, + &state->validated_graphs, + &state->result_envelopes, + &state->prefill_tokens + ) != SUCCESS) { + return FAILURE; + } + prefill_end_ns = king_inference_cuda_decoder_prompt_profile_ns(); + model->cuda_decoder_prompt_loop_last_prefill_ns = + king_inference_cuda_decoder_prompt_profile_elapsed(prefill_end_ns, prefill_start_ns); + + state->position = state->token_count - 1; + add_assoc_bool(&state->decode_options, "emit_token", true); + if (king_inference_cuda_decoder_prompt_loop_token_at( + &state->tokenized_prompt, + state->position, + &initial_token_id + ) != SUCCESS + || king_inference_token_decode_graph_array( + model, + &state->tokenized_prompt, + state->position, + &state->decode_options, + &state->graph + ) != SUCCESS + || king_inference_cuda_decoder_prompt_loop_prepare_decode_template( + model, + &state->graph, + &state->plan_result + ) != SUCCESS + || king_inference_cuda_decoder_prompt_loop_build_decode_mutations( + model, + &state->graph, + &state->mutations + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + model->cuda_decoder_graph_executor_error[0] != '\0' + ? model->cuda_decoder_graph_executor_error + : "cuda_decoder_prompt_loop_graph_validation_failed" + ); + return FAILURE; + } + + state->decoded_token_id = initial_token_id; + state->validated_graphs++; + state->initialized = true; + return SUCCESS; +} + +static void king_inference_cuda_decoder_prompt_stream_finish( + king_inference_model_object *model, + king_inference_stream_object *stream, + king_inference_cuda_decoder_prompt_stream_state *state +) { + zval event; + + model->cuda_decoder_prompt_loop_count++; + model->cuda_decoder_prompt_loop_last_prompt_tokens = state->token_count; + model->cuda_decoder_prompt_loop_last_validated_graphs = state->validated_graphs; + model->cuda_decoder_prompt_loop_last_result_envelopes = state->result_envelopes; + model->cuda_decoder_prompt_loop_last_prefill_batches = state->prefill_batches; + model->cuda_decoder_prompt_loop_last_prefill_tokens = state->prefill_tokens; + model->cuda_decoder_prompt_loop_last_prefill_template_reuses = state->prefill_template_reuses; + model->cuda_decoder_prompt_loop_last_decoded_tokens = state->decoded_tokens; + model->cuda_decoder_prompt_loop_last_kv_cache_reserve_calls = + model->cuda_device_kv_cache_reserve_call_count - state->kv_cache_reserve_calls_start; + model->cuda_decoder_prompt_loop_last_kv_cache_allocations = + model->cuda_device_kv_cache_allocation_count - state->kv_cache_allocations_start; + model->cuda_decoder_prompt_loop_last_kv_cache_writes = + model->cuda_device_kv_cache_write_count - state->kv_cache_writes_start; + model->cuda_decoder_prompt_loop_last_kv_cache_batch_writes = + model->cuda_device_kv_cache_batch_write_count - state->kv_cache_batch_writes_start; + model->cuda_decoder_prompt_loop_last_graph_constructions = + model->cuda_decoder_graph_executor_graph_count - state->graph_constructions_start; + model->cuda_decoder_prompt_loop_last_plan_constructions = + model->cuda_decoder_graph_executor_plan_count - state->plan_constructions_start; + model->cuda_decoder_prompt_loop_last_policy_checks = (size_t) stream->gpu_policy_during_run_checks; + model->cuda_decoder_prompt_loop_last_policy_interval_skips = (size_t) stream->gpu_policy_interval_skips; + model->cuda_decoder_prompt_loop_last_thermal_runtime_checks = (size_t) stream->gpu_thermal_runtime_checks; + model->cuda_decoder_prompt_loop_last_power_runtime_checks = (size_t) stream->gpu_power_runtime_checks; + model->cuda_decoder_prompt_loop_last_max_tokens = state->max_tokens; + model->cuda_decoder_prompt_loop_last_thermal_interval_seconds = + king_inference_gpu_thermal_check_interval_seconds(&model->config); + model->cuda_decoder_prompt_loop_last_power_interval_seconds = + king_inference_gpu_power_check_interval_seconds(&model->config); + model->cuda_decoder_prompt_loop_last_reuse_profile_available = true; + model->cuda_decoder_prompt_loop_result = 0; + model->cuda_decoder_prompt_loop_error[0] = '\0'; + + if (!state->stopped) { + king_inference_king_native_prompt_flush_pending(stream, &state->pending); + } else { + smart_str_free(&state->pending); + } + if (!Z_ISUNDEF(stream->native_events) + && Z_TYPE(stream->native_events) == IS_ARRAY + && zend_hash_num_elements(Z_ARRVAL(stream->native_events)) == 0) { + add_next_index_stringl(&stream->native_events, "", 0); + } + + if (king_inference_king_native_stream_diagnostics_enabled(stream)) { + array_init(&event); + add_assoc_string(&event, "type", "gpu_prompt_decoder_loop_contract"); + add_assoc_string(&event, "backend", "king_native_gpu"); + add_assoc_bool(&event, "prompt_loop_ready", true); + add_assoc_bool(&event, "lazy_native_token_stream", true); + add_assoc_bool(&event, "prompt_batch_prefill_available", model->cuda_decoder_prompt_loop_batch_prefill_available); + add_assoc_bool(&event, "prompt_batch_prefill_admitted", king_inference_cuda_decoder_prompt_loop_batch_prefill_admitted(model)); + add_assoc_string(&event, "prompt_batch_prefill_status", king_inference_cuda_decoder_prompt_loop_batch_prefill_status(model)); + add_assoc_string(&event, "prompt_batch_prefill_admission_reason", king_inference_cuda_decoder_prompt_loop_batch_prefill_admission_reason(model)); + add_assoc_bool(&event, "prompt_batch_prefill_used", model->cuda_decoder_prompt_loop_last_used_batch_prefill); + add_assoc_bool(&event, "decode_graph_template_resident", true); + add_assoc_bool(&event, "decode_graph_constructed_on_hot_path", false); + add_assoc_bool(&event, "token_events_streamed_immediately", true); + add_assoc_bool(&event, "stop_condition_reached", state->stopped); + add_assoc_bool(&event, "prompt_graphs_validated", true); + add_assoc_bool(&event, "graph_result_contract_ready", true); + add_assoc_bool(&event, "graph_execution_plan_ready", true); + add_assoc_long(&event, "prompt_tokens", (zend_long) state->token_count); + add_assoc_long(&event, "prefill_batches", (zend_long) state->prefill_batches); + add_assoc_long(&event, "prefill_tokens", (zend_long) state->prefill_tokens); + add_assoc_long(&event, "prefill_template_reuses", (zend_long) state->prefill_template_reuses); + add_assoc_long(&event, "validated_graphs", (zend_long) state->validated_graphs); + add_assoc_long(&event, "result_envelopes", (zend_long) state->result_envelopes); + add_assoc_long(&event, "decoded_tokens", (zend_long) state->decoded_tokens); + add_assoc_long(&event, "accepted_max_tokens", state->max_tokens); + add_assoc_bool(&event, "bounded_logits_consumed", state->decoded_tokens > 0); + add_assoc_bool(&event, "token_result_ready", state->decoded_tokens > 0); + add_assoc_bool(&event, "decoded_token_ready", state->decoded_tokens > 0); + add_next_index_zval(&stream->native_events, &event); + } + + state->finished = true; +} + +static zend_result king_inference_cuda_decoder_prompt_stream_step( + king_inference_model_object *model, + king_inference_stream_object *stream +) { + king_inference_cuda_decoder_prompt_stream_state *state = + king_inference_cuda_decoder_prompt_stream_state_from(stream); + king_inference_cuda_decoder_prompt_profile_snapshot before; + king_inference_cuda_decoder_prompt_profile_snapshot after; + zend_ulong source_token_id; + uint64_t total_start_ns; + uint64_t policy_start_ns; + uint64_t text_emit_start_ns; + zend_long policy_ns = 0; + zend_long mutation_ns = 0; + zend_long device_execute_ns = 0; + zend_long bounded_emit_ns = 0; + zend_long sampler_ns = 0; + zend_long token_decode_ns = 0; + zend_long text_emit_ns = 0; + + if (state == NULL) { + return SUCCESS; + } + if (!state->initialized + && king_inference_cuda_decoder_prompt_stream_initialize(model, stream, state) != SUCCESS) { + king_inference_cuda_decoder_prompt_stream_release(stream); + return FAILURE; + } + if (state->finished) { + king_inference_cuda_decoder_prompt_stream_release(stream); + return SUCCESS; + } + if (state->decoded_tokens >= (size_t) state->max_tokens || state->stopped) { + king_inference_cuda_decoder_prompt_stream_finish(model, stream, state); + king_inference_cuda_decoder_prompt_stream_release(stream); + return SUCCESS; + } + if (state->decoded_tokens > 0) { + if (state->position >= (zend_ulong) ZEND_LONG_MAX + || state->decoded_token_id > (zend_ulong) ZEND_LONG_MAX) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_loop_position_limit_exceeded"); + king_inference_cuda_decoder_prompt_stream_release(stream); + return FAILURE; + } + state->position++; + } + + source_token_id = state->decoded_token_id; + total_start_ns = king_inference_cuda_decoder_prompt_profile_ns(); + policy_start_ns = total_start_ns; + if (king_inference_check_gpu_policy_during_run(stream, "King GPU decoder loop") != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_loop_guardrail_failed"); + king_inference_cuda_decoder_prompt_stream_release(stream); + return FAILURE; + } + policy_ns = king_inference_cuda_decoder_prompt_profile_elapsed( + king_inference_cuda_decoder_prompt_profile_ns(), + policy_start_ns + ); + king_inference_cuda_decoder_prompt_profile_capture(model, &state->graph, &before); + if (king_inference_cuda_decoder_prompt_loop_execute_decode_template( + model, + stream, + &state->graph, + &state->mutations, + source_token_id, + state->position, + &state->decoded_token_id, + &state->decoded_token_text, + &state->decoded_probability, + &state->decoded_logit, + &state->decoded_rank, + &mutation_ns, + &device_execute_ns, + &bounded_emit_ns, + &sampler_ns, + &token_decode_ns + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_stream_release(stream); + return FAILURE; + } + text_emit_start_ns = king_inference_cuda_decoder_prompt_profile_ns(); + if (king_inference_cuda_decoder_prompt_loop_emit_decoded_text( + model, + stream, + state->decoded_token_id, + state->decoded_token_text, + &state->stops, + &state->pending, + &state->stopped + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_stream_release(stream); + return FAILURE; + } + text_emit_ns = king_inference_cuda_decoder_prompt_profile_elapsed( + king_inference_cuda_decoder_prompt_profile_ns(), + text_emit_start_ns + ); + king_inference_cuda_decoder_prompt_profile_capture(model, &state->graph, &after); + king_inference_cuda_decoder_prompt_profile_record( + model, + &before, + &after, + king_inference_cuda_decoder_prompt_profile_elapsed( + king_inference_cuda_decoder_prompt_profile_ns(), + total_start_ns + ), + policy_ns, + mutation_ns, + device_execute_ns, + bounded_emit_ns, + sampler_ns, + token_decode_ns, + text_emit_ns + ); + + state->decoded_tokens++; + state->result_envelopes++; + if (state->decoded_tokens >= (size_t) state->max_tokens || state->stopped) { + king_inference_cuda_decoder_prompt_stream_finish(model, stream, state); + king_inference_cuda_decoder_prompt_stream_release(stream); + } + return SUCCESS; +} diff --git a/extension/src/inference/cuda/prompt/prefill/attention/cuda_decoder_prompt_prefill_attention_batch.inc b/extension/src/inference/cuda/prompt/prefill/attention/cuda_decoder_prompt_prefill_attention_batch.inc new file mode 100644 index 000000000..78b88a04b --- /dev/null +++ b/extension/src/inference/cuda/prompt/prefill/attention/cuda_decoder_prompt_prefill_attention_batch.inc @@ -0,0 +1,871 @@ +/* + * CUDA prompt prefill layer-0 attention stack batch path. + */ + +static zend_ulong king_inference_cuda_decoder_prompt_loop_prefill_kv_head_for_query( + zend_ulong query_head, + zend_ulong query_heads, + zend_ulong kv_heads +) { + zend_ulong kv_head; + + if (query_heads == kv_heads) { + return query_head; + } + kv_head = (query_head * kv_heads) / query_heads; + return kv_head >= kv_heads ? kv_heads - 1 : kv_head; +} + +static zend_result king_inference_cuda_decoder_prompt_loop_prefill_copy_attention_context_rows( + king_inference_model_object *model, + king_inference_cuda_device_ptr context_matrix, + zend_ulong prefill_token_count, + zend_ulong value_length, + zend_ulong stack_width, + zend_ulong query_head +) { + zend_ulong position; + + for (position = 0; position < prefill_token_count; position++) { + size_t source_offset = (size_t) position * (size_t) value_length; + zend_ulong target_offset = position * stack_width + query_head * value_length; + + if (target_offset > UINT_MAX + || king_inference_cuda_vector_copy_to_offset( + model, + context_matrix + (king_inference_cuda_device_ptr) (source_offset * sizeof(float)), + model->cuda_decoder_prompt_prefill_layer0_attention_stack, + target_offset, + value_length + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_device_vector_ops_result, + model->cuda_device_vector_ops_error[0] != '\0' + ? model->cuda_device_vector_ops_error + : "cuda_decoder_prompt_batch_prefill_attention_stack_copy_failed" + ); + return FAILURE; + } + } + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_prompt_loop_prefill_layer0_ffn_norm_batch( + king_inference_model_object *model, + zval *graph, + zval *residual_op, + zend_ulong residual_width, + zend_ulong prefill_token_count +) { + zval *ffn_norm_op; + zend_string *residual_id = NULL; + zend_string *ffn_norm_weight = NULL; + double epsilon = 0.000001; + size_t ffn_norm_elements; + size_t ffn_norm_bytes; + + if (residual_op == NULL + || residual_width == 0 + || prefill_token_count == 0 + || prefill_token_count > (zend_ulong) (SIZE_MAX / (size_t) residual_width)) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_norm_shape_invalid" + ); + return FAILURE; + } + if (king_inference_graph_required_string(residual_op, "id", &residual_id) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_norm_residual_id_invalid" + ); + return FAILURE; + } + + ffn_norm_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, residual_id, "rms_norm"); + if (ffn_norm_op == NULL) { + return SUCCESS; + } + if (king_inference_graph_required_string(ffn_norm_op, "weight", &ffn_norm_weight) != SUCCESS + || king_inference_graph_finite_double_option(ffn_norm_op, "epsilon", 0.000001, &epsilon) != SUCCESS + || epsilon <= 0.0) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_norm_contract_invalid" + ); + return FAILURE; + } + + ffn_norm_elements = (size_t) prefill_token_count * (size_t) residual_width; + if (ffn_norm_elements > SIZE_MAX / sizeof(float)) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_norm_size_overflow" + ); + return FAILURE; + } + ffn_norm_bytes = ffn_norm_elements * sizeof(float); + if (king_inference_cuda_device_allocator_alloc( + model, + ffn_norm_bytes, + &model->cuda_decoder_prompt_prefill_layer0_ffn_norm + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_prompt_batch_prefill_ffn_norm_allocation_failed" + ); + return FAILURE; + } + if (king_inference_cuda_rms_norm_f32_batch( + model, + ffn_norm_weight, + model->cuda_decoder_prompt_prefill_layer0_attention_residual, + model->cuda_decoder_prompt_prefill_layer0_ffn_norm, + residual_width, + prefill_token_count, + epsilon + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_rms_norm_result, + model->cuda_rms_norm_error[0] != '\0' + ? model->cuda_rms_norm_error + : "cuda_decoder_prompt_batch_prefill_ffn_norm_failed" + ); + return FAILURE; + } + + model->cuda_decoder_prompt_prefill_layer0_ffn_norm_bytes = ffn_norm_bytes; + model->cuda_decoder_prompt_prefill_layer0_ffn_norm_width = residual_width; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_prompt_loop_prefill_layer0_ffn_gate_up_batch( + king_inference_model_object *model, + zval *graph, + zval *residual_op, + zend_ulong ffn_norm_width, + zend_ulong prefill_token_count +) { + zval *ffn_norm_op; + zval *gate_op; + zval *up_op; + zend_string *residual_id = NULL; + zend_string *ffn_norm_id = NULL; + zend_string *gate_weight = NULL; + zend_string *up_weight = NULL; + zend_ulong gate_rows = 0; + zend_ulong up_rows = 0; + size_t gate_elements; + size_t up_elements; + size_t gate_bytes; + size_t up_bytes; + + if (model->cuda_decoder_prompt_prefill_layer0_ffn_norm == 0) { + return SUCCESS; + } + if (residual_op == NULL + || graph == NULL + || ffn_norm_width == 0 + || ffn_norm_width != model->cuda_decoder_prompt_prefill_layer0_ffn_norm_width + || prefill_token_count == 0) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_gate_up_shape_invalid" + ); + return FAILURE; + } + if (king_inference_graph_required_string(residual_op, "id", &residual_id) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_gate_up_residual_id_invalid" + ); + return FAILURE; + } + + ffn_norm_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, residual_id, "rms_norm"); + if (ffn_norm_op == NULL) { + return SUCCESS; + } + if (king_inference_graph_required_string(ffn_norm_op, "id", &ffn_norm_id) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_gate_up_norm_id_invalid" + ); + return FAILURE; + } + gate_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, ffn_norm_id, "linear", 0); + up_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, ffn_norm_id, "linear", 1); + if (gate_op == NULL && up_op == NULL) { + return SUCCESS; + } + if (gate_op == NULL + || up_op == NULL + || king_inference_cuda_decoder_graph_executor_linear_shape( + model, + gate_op, + ffn_norm_width, + &gate_weight, + &gate_rows + ) != SUCCESS + || king_inference_cuda_decoder_graph_executor_linear_shape( + model, + up_op, + ffn_norm_width, + &up_weight, + &up_rows + ) != SUCCESS + || gate_rows == 0 + || gate_rows != up_rows) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + model->cuda_decoder_graph_executor_error[0] != '\0' + ? model->cuda_decoder_graph_executor_error + : "cuda_decoder_prompt_batch_prefill_ffn_gate_up_contract_invalid" + ); + return FAILURE; + } + if (prefill_token_count > (zend_ulong) (SIZE_MAX / (size_t) gate_rows) + || prefill_token_count > (zend_ulong) (SIZE_MAX / (size_t) up_rows)) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_gate_up_size_overflow" + ); + return FAILURE; + } + + gate_elements = (size_t) prefill_token_count * (size_t) gate_rows; + up_elements = (size_t) prefill_token_count * (size_t) up_rows; + if (gate_elements > SIZE_MAX / sizeof(float) + || up_elements > SIZE_MAX / sizeof(float)) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_gate_up_bytes_overflow" + ); + return FAILURE; + } + gate_bytes = gate_elements * sizeof(float); + up_bytes = up_elements * sizeof(float); + if (king_inference_cuda_device_allocator_alloc( + model, + gate_bytes, + &model->cuda_decoder_prompt_prefill_layer0_ffn_gate + ) != SUCCESS + || king_inference_cuda_device_allocator_alloc( + model, + up_bytes, + &model->cuda_decoder_prompt_prefill_layer0_ffn_up + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_prompt_batch_prefill_ffn_gate_up_allocation_failed" + ); + return FAILURE; + } + + if (king_inference_cuda_quantized_matvec_batch( + model, + gate_weight, + model->cuda_decoder_prompt_prefill_layer0_ffn_norm, + ffn_norm_width, + prefill_token_count, + model->cuda_decoder_prompt_prefill_layer0_ffn_gate, + gate_rows + ) != SUCCESS + || king_inference_cuda_quantized_matvec_batch( + model, + up_weight, + model->cuda_decoder_prompt_prefill_layer0_ffn_norm, + ffn_norm_width, + prefill_token_count, + model->cuda_decoder_prompt_prefill_layer0_ffn_up, + up_rows + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_quantized_matvec_result, + model->cuda_quantized_matvec_error[0] != '\0' + ? model->cuda_quantized_matvec_error + : "cuda_decoder_prompt_batch_prefill_ffn_gate_up_failed" + ); + return FAILURE; + } + + model->cuda_decoder_prompt_prefill_layer0_ffn_gate_bytes = gate_bytes; + model->cuda_decoder_prompt_prefill_layer0_ffn_up_bytes = up_bytes; + model->cuda_decoder_prompt_prefill_layer0_ffn_gate_rows = gate_rows; + model->cuda_decoder_prompt_prefill_layer0_ffn_up_rows = up_rows; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_prompt_loop_prefill_layer0_ffn_swiglu_batch( + king_inference_model_object *model, + zval *graph, + zval *residual_op, + zend_ulong ffn_width, + zend_ulong prefill_token_count +) { + zval *ffn_norm_op; + zval *gate_op; + zval *up_op; + zval *silu_op; + zval *mul_op; + zend_string *residual_id = NULL; + zend_string *ffn_norm_id = NULL; + zend_string *gate_id = NULL; + zend_string *up_id = NULL; + zend_string *silu_id = NULL; + zend_string *gated_id = NULL; + zend_ulong activation = 0; + size_t swiglu_elements; + size_t swiglu_bytes; + + if (model->cuda_decoder_prompt_prefill_layer0_ffn_gate == 0 + || model->cuda_decoder_prompt_prefill_layer0_ffn_up == 0) { + return SUCCESS; + } + if (graph == NULL + || residual_op == NULL + || ffn_width == 0 + || ffn_width != model->cuda_decoder_prompt_prefill_layer0_ffn_gate_rows + || ffn_width != model->cuda_decoder_prompt_prefill_layer0_ffn_up_rows + || prefill_token_count == 0 + || prefill_token_count > (zend_ulong) (SIZE_MAX / (size_t) ffn_width)) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_swiglu_shape_invalid" + ); + return FAILURE; + } + if (king_inference_graph_required_string(residual_op, "id", &residual_id) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_swiglu_residual_id_invalid" + ); + return FAILURE; + } + + ffn_norm_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, residual_id, "rms_norm"); + if (ffn_norm_op == NULL) { + return SUCCESS; + } + if (king_inference_graph_required_string(ffn_norm_op, "id", &ffn_norm_id) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_swiglu_norm_id_invalid" + ); + return FAILURE; + } + gate_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, ffn_norm_id, "linear", 0); + up_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, ffn_norm_id, "linear", 1); + if (gate_op == NULL && up_op == NULL) { + return SUCCESS; + } + if (gate_op == NULL + || up_op == NULL + || king_inference_graph_required_string(gate_op, "id", &gate_id) != SUCCESS + || king_inference_graph_required_string(up_op, "id", &up_id) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_swiglu_gate_up_contract_invalid" + ); + return FAILURE; + } + silu_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, gate_id, "silu"); + if (silu_op == NULL) { + silu_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, gate_id, "gelu_tanh"); + activation = silu_op != NULL ? 1 : 0; + } + if (silu_op == NULL) { + return SUCCESS; + } + if (king_inference_graph_required_string(silu_op, "id", &silu_id) != SUCCESS + || (mul_op = king_inference_cuda_decoder_graph_executor_mul_op_for_inputs(graph, silu_id, up_id)) == NULL + || king_inference_graph_required_string(mul_op, "id", &gated_id) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_swiglu_contract_invalid" + ); + return FAILURE; + } + + swiglu_elements = (size_t) prefill_token_count * (size_t) ffn_width; + if (swiglu_elements > SIZE_MAX / sizeof(float) || swiglu_elements > UINT_MAX) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_swiglu_size_overflow" + ); + return FAILURE; + } + swiglu_bytes = swiglu_elements * sizeof(float); + if (king_inference_cuda_device_allocator_alloc( + model, + swiglu_bytes, + &model->cuda_decoder_prompt_prefill_layer0_ffn_swiglu + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_prompt_batch_prefill_ffn_swiglu_allocation_failed" + ); + return FAILURE; + } + if (king_inference_cuda_ffn_swiglu_f32( + model, + model->cuda_decoder_prompt_prefill_layer0_ffn_gate, + model->cuda_decoder_prompt_prefill_layer0_ffn_up, + model->cuda_decoder_prompt_prefill_layer0_ffn_swiglu, + (zend_ulong) swiglu_elements, + activation + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_ffn_swiglu_result, + model->cuda_ffn_swiglu_error[0] != '\0' + ? model->cuda_ffn_swiglu_error + : "cuda_decoder_prompt_batch_prefill_ffn_swiglu_failed" + ); + return FAILURE; + } + + model->cuda_decoder_prompt_prefill_layer0_ffn_swiglu_bytes = swiglu_bytes; + model->cuda_decoder_prompt_prefill_layer0_ffn_swiglu_width = ffn_width; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_prompt_loop_prefill_layer0_attention_stack_batch( + king_inference_model_object *model, + zval *graph, + zend_ulong width, + zend_ulong prefill_token_count +) { + zval *stack_op; + zval *projection_op; + zval *post_attention_norm_op = NULL; + zval *residual_op; + zend_string *stack_id = NULL; + zend_string *projection_weight = NULL; + zend_string *projection_id = NULL; + zend_string *post_attention_norm_id = NULL; + zend_string *post_attention_norm_weight = NULL; + zend_string *projection_residual_id = NULL; + zend_string *hidden_id = NULL; + zend_ulong query_heads = model->cuda_decoder_prompt_prefill_initial_query_rope_heads; + zend_ulong kv_heads = model->cuda_decoder_prompt_prefill_kv_heads; + zend_ulong key_length = model->cuda_decoder_prompt_prefill_initial_query_rope_head_dim; + zend_ulong value_length = model->cuda_decoder_prompt_prefill_kv_value_length; + zend_ulong sliding_window = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_SLIDING_WINDOW]; + zend_ulong max_slots = sliding_window > 0 && sliding_window < prefill_token_count + ? sliding_window + : prefill_token_count; + zend_ulong stack_width; + size_t score_elements; + size_t context_elements; + size_t stack_elements; + size_t score_bytes; + size_t context_bytes; + size_t stack_bytes; + size_t projection_elements; + size_t projection_bytes; + size_t residual_bytes; + king_inference_cuda_device_ptr scores = 0; + king_inference_cuda_device_ptr probabilities = 0; + king_inference_cuda_device_ptr contexts = 0; + king_inference_cuda_device_ptr projection_matrix = 0; + king_inference_cuda_device_ptr post_attention_norm_matrix = 0; + king_inference_cuda_device_ptr projection_residual_matrix = 0; + zend_ulong projection_rows = 0; + zend_ulong query_head; + double post_attention_norm_epsilon = 0.000001; + double scale; + + if (graph == NULL + || model->cuda_decoder_prompt_prefill_initial_query_rope == 0 + || !model->cuda_device_kv_cache_available + || query_heads == 0 + || kv_heads == 0 + || key_length == 0 + || value_length == 0 + || width == 0 + || key_length > model->cuda_device_kv_cache_key_length + || value_length > model->cuda_device_kv_cache_value_length + || prefill_token_count == 0 + || max_slots == 0 + || query_heads < kv_heads + || query_heads > (zend_ulong) (SIZE_MAX / (size_t) value_length)) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_attention_shape_invalid" + ); + return FAILURE; + } + + stack_width = query_heads * value_length; + stack_op = king_inference_cuda_decoder_graph_executor_first_op(graph, "stack"); + projection_op = stack_op != NULL + && king_inference_graph_required_string(stack_op, "id", &stack_id) == SUCCESS + ? king_inference_cuda_decoder_graph_executor_op_for_input(graph, stack_id, "linear") + : NULL; + if (projection_op == NULL + || king_inference_graph_required_string(projection_op, "id", &projection_id) != SUCCESS + || king_inference_cuda_decoder_graph_executor_linear_shape( + model, + projection_op, + stack_width, + &projection_weight, + &projection_rows + ) != SUCCESS + || projection_rows != width) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_attention_projection_shape_invalid" + ); + return FAILURE; + } + projection_residual_id = projection_id; + post_attention_norm_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, projection_id, "rms_norm"); + if (post_attention_norm_op != NULL) { + if (king_inference_graph_required_string(post_attention_norm_op, "id", &post_attention_norm_id) != SUCCESS + || king_inference_graph_required_string(post_attention_norm_op, "weight", &post_attention_norm_weight) != SUCCESS + || king_inference_graph_finite_double_option(post_attention_norm_op, "epsilon", 0.000001, &post_attention_norm_epsilon) != SUCCESS + || post_attention_norm_epsilon <= 0.0) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_post_attention_norm_contract_invalid" + ); + return FAILURE; + } + projection_residual_id = post_attention_norm_id; + } + hidden_id = zend_string_init("x", sizeof("x") - 1, false); + residual_op = king_inference_cuda_decoder_graph_executor_add_op_for_inputs(graph, hidden_id, projection_residual_id); + zend_string_release(hidden_id); + if (residual_op == NULL) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_attention_residual_contract_invalid" + ); + return FAILURE; + } + + if (prefill_token_count > (zend_ulong) (SIZE_MAX / (size_t) max_slots) + || prefill_token_count > (zend_ulong) (SIZE_MAX / (size_t) value_length) + || prefill_token_count > (zend_ulong) (SIZE_MAX / (size_t) stack_width) + || prefill_token_count > (zend_ulong) (SIZE_MAX / (size_t) projection_rows)) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_attention_size_overflow" + ); + return FAILURE; + } + + score_elements = (size_t) prefill_token_count * (size_t) max_slots; + context_elements = (size_t) prefill_token_count * (size_t) value_length; + stack_elements = (size_t) prefill_token_count * (size_t) stack_width; + projection_elements = (size_t) prefill_token_count * (size_t) projection_rows; + if (score_elements > SIZE_MAX / sizeof(float) + || context_elements > SIZE_MAX / sizeof(float) + || stack_elements > SIZE_MAX / sizeof(float) + || projection_elements > SIZE_MAX / sizeof(float) + || projection_elements > UINT_MAX) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_attention_bytes_overflow" + ); + return FAILURE; + } + + score_bytes = score_elements * sizeof(float); + context_bytes = context_elements * sizeof(float); + stack_bytes = stack_elements * sizeof(float); + projection_bytes = projection_elements * sizeof(float); + residual_bytes = projection_bytes; + if (king_inference_cuda_device_allocator_alloc(model, score_bytes, &scores) != SUCCESS + || king_inference_cuda_device_allocator_alloc(model, score_bytes, &probabilities) != SUCCESS + || king_inference_cuda_device_allocator_alloc(model, context_bytes, &contexts) != SUCCESS + || king_inference_cuda_device_allocator_alloc( + model, + stack_bytes, + &model->cuda_decoder_prompt_prefill_layer0_attention_stack + ) != SUCCESS + || king_inference_cuda_device_allocator_alloc(model, projection_bytes, &projection_matrix) != SUCCESS + || king_inference_cuda_device_allocator_alloc( + model, + residual_bytes, + &model->cuda_decoder_prompt_prefill_layer0_attention_residual + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &projection_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &contexts); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &probabilities); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &scores); + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_prompt_batch_prefill_attention_allocation_failed" + ); + return FAILURE; + } + + scale = 1.0 / sqrt((double) key_length); + for (query_head = 0; query_head < query_heads; query_head++) { + zend_ulong kv_head = king_inference_cuda_decoder_prompt_loop_prefill_kv_head_for_query( + query_head, + query_heads, + kv_heads + ); + king_inference_cuda_device_ptr key_cache = 0; + king_inference_cuda_device_ptr value_cache = 0; + king_inference_cuda_device_ptr query_base = model->cuda_decoder_prompt_prefill_initial_query_rope + + (king_inference_cuda_device_ptr) ((size_t) query_head * (size_t) prefill_token_count * (size_t) key_length * sizeof(float)); + zend_ulong key_stride = 0; + zend_ulong value_stride = 0; + + if (king_inference_cuda_device_kv_cache_head(model, false, 0, kv_head, &key_cache, &key_stride) != SUCCESS + || king_inference_cuda_device_kv_cache_head(model, true, 0, kv_head, &value_cache, &value_stride) != SUCCESS + || king_inference_cuda_attention_scores_f32_batch( + model, + query_base, + key_cache, + scores, + key_length, + key_length, + key_stride, + 0, + max_slots, + prefill_token_count, + sliding_window, + scale + ) != SUCCESS + || king_inference_cuda_attention_softmax_f32_batch( + model, + scores, + probabilities, + 0, + 0, + max_slots, + prefill_token_count, + sliding_window, + 1.0, + 1.0 + ) != SUCCESS + || king_inference_cuda_attention_values_f32_batch( + model, + probabilities, + value_cache, + contexts, + value_length, + value_stride, + 0, + max_slots, + prefill_token_count, + sliding_window + ) != SUCCESS + || king_inference_cuda_decoder_prompt_loop_prefill_copy_attention_context_rows( + model, + contexts, + prefill_token_count, + value_length, + stack_width, + query_head + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &contexts); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &probabilities); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &scores); + return FAILURE; + } + } + + if (king_inference_cuda_quantized_matvec_batch( + model, + projection_weight, + model->cuda_decoder_prompt_prefill_layer0_attention_stack, + stack_width, + prefill_token_count, + projection_matrix, + projection_rows + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &projection_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &contexts); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &probabilities); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &scores); + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_quantized_matvec_result, + model->cuda_quantized_matvec_error[0] != '\0' + ? model->cuda_quantized_matvec_error + : "cuda_decoder_prompt_batch_prefill_attention_projection_failed" + ); + return FAILURE; + } + projection_residual_matrix = projection_matrix; + if (post_attention_norm_op != NULL) { + if (king_inference_cuda_device_allocator_alloc(model, projection_bytes, &post_attention_norm_matrix) != SUCCESS + || king_inference_cuda_rms_norm_f32_batch( + model, + post_attention_norm_weight, + projection_matrix, + post_attention_norm_matrix, + projection_rows, + prefill_token_count, + post_attention_norm_epsilon + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &projection_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &contexts); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &probabilities); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &scores); + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_rms_norm_result != 0 + ? model->cuda_rms_norm_result + : model->cuda_device_allocator_result, + model->cuda_rms_norm_error[0] != '\0' + ? model->cuda_rms_norm_error + : (model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_prompt_batch_prefill_post_attention_norm_failed") + ); + return FAILURE; + } + projection_residual_matrix = post_attention_norm_matrix; + } + if (king_inference_cuda_vector_add( + model, + model->cuda_decoder_prompt_prefill_embeddings, + projection_residual_matrix, + model->cuda_decoder_prompt_prefill_layer0_attention_residual, + (zend_ulong) projection_elements + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &projection_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &contexts); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &probabilities); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &scores); + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_quantized_matvec_result != 0 + ? model->cuda_quantized_matvec_result + : model->cuda_device_vector_ops_result, + model->cuda_device_vector_ops_error[0] != '\0' + ? model->cuda_device_vector_ops_error + : "cuda_decoder_prompt_batch_prefill_attention_residual_failed" + ); + return FAILURE; + } + + model->cuda_decoder_prompt_prefill_layer0_attention_stack_bytes = stack_bytes; + model->cuda_decoder_prompt_prefill_layer0_attention_heads = query_heads; + model->cuda_decoder_prompt_prefill_layer0_attention_stack_width = stack_width; + model->cuda_decoder_prompt_prefill_layer0_attention_residual_bytes = residual_bytes; + model->cuda_decoder_prompt_prefill_layer0_attention_residual_width = projection_rows; + if (king_inference_cuda_decoder_prompt_loop_prefill_layer0_ffn_norm_batch( + model, + graph, + residual_op, + projection_rows, + prefill_token_count + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &projection_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &contexts); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &probabilities); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &scores); + return FAILURE; + } + if (king_inference_cuda_decoder_prompt_loop_prefill_layer0_ffn_gate_up_batch( + model, + graph, + residual_op, + projection_rows, + prefill_token_count + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &projection_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &contexts); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &probabilities); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &scores); + return FAILURE; + } + if (king_inference_cuda_decoder_prompt_loop_prefill_layer0_ffn_swiglu_batch( + model, + graph, + residual_op, + model->cuda_decoder_prompt_prefill_layer0_ffn_gate_rows, + prefill_token_count + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &projection_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &contexts); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &probabilities); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &scores); + return FAILURE; + } + if (king_inference_cuda_decoder_prompt_loop_prefill_layer0_ffn_down_batch( + model, + graph, + residual_op, + model->cuda_decoder_prompt_prefill_layer0_ffn_swiglu_width, + prefill_token_count + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &projection_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &contexts); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &probabilities); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &scores); + return FAILURE; + } + if (king_inference_cuda_decoder_prompt_loop_prefill_layer0_ffn_output_residual_batch( + model, + graph, + residual_op, + projection_rows, + prefill_token_count + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &projection_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &contexts); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &probabilities); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &scores); + return FAILURE; + } + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &projection_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &contexts); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &probabilities); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &scores); + return SUCCESS; +} diff --git a/extension/src/inference/cuda/prompt/prefill/batch/cuda_decoder_prompt_batch_prefill.inc b/extension/src/inference/cuda/prompt/prefill/batch/cuda_decoder_prompt_batch_prefill.inc new file mode 100644 index 000000000..2e7e4b85e --- /dev/null +++ b/extension/src/inference/cuda/prompt/prefill/batch/cuda_decoder_prompt_batch_prefill.inc @@ -0,0 +1,725 @@ +/* + * CUDA prompt batch-prefill helpers for native King GPU inference. + */ + +static zend_result king_inference_cuda_decoder_prompt_loop_token_at( + zval *tokenized_prompt, + zend_ulong position, + zend_ulong *token_id_out +) { + zval *tokens; + zval *token; + + *token_id_out = 0; + if (tokenized_prompt == NULL || Z_TYPE_P(tokenized_prompt) != IS_ARRAY) { + return FAILURE; + } + tokens = king_inference_array_find(tokenized_prompt, "tokens"); + if (tokens == NULL || Z_TYPE_P(tokens) != IS_ARRAY) { + return FAILURE; + } + token = zend_hash_index_find(Z_ARRVAL_P(tokens), position); + if (token == NULL || Z_TYPE_P(token) != IS_LONG || Z_LVAL_P(token) < 0) { + return FAILURE; + } + + *token_id_out = (zend_ulong) Z_LVAL_P(token); + return SUCCESS; +} + +static void king_inference_cuda_decoder_prompt_loop_release_prefill_embeddings( + king_inference_model_object *model +) { + if (model->cuda_decoder_prompt_prefill_embeddings_active + && model->cuda_decoder_prompt_prefill_embeddings != 0) { + (void) king_inference_cuda_device_allocator_free( + model, + model->cuda_decoder_prompt_prefill_embeddings + ); + } + if (model->cuda_decoder_prompt_prefill_embeddings_active + && model->cuda_decoder_prompt_prefill_initial_norm != 0) { + (void) king_inference_cuda_device_allocator_free( + model, + model->cuda_decoder_prompt_prefill_initial_norm + ); + } + if (model->cuda_decoder_prompt_prefill_embeddings_active + && model->cuda_decoder_prompt_prefill_initial_linear != 0) { + (void) king_inference_cuda_device_allocator_free( + model, + model->cuda_decoder_prompt_prefill_initial_linear + ); + } + if (model->cuda_decoder_prompt_prefill_embeddings_active + && model->cuda_decoder_prompt_prefill_initial_query_rope != 0) { + (void) king_inference_cuda_device_allocator_free( + model, + model->cuda_decoder_prompt_prefill_initial_query_rope + ); + } + if (model->cuda_decoder_prompt_prefill_embeddings_active + && model->cuda_decoder_prompt_prefill_layer0_attention_stack != 0) { + (void) king_inference_cuda_device_allocator_free( + model, + model->cuda_decoder_prompt_prefill_layer0_attention_stack + ); + } + if (model->cuda_decoder_prompt_prefill_embeddings_active + && model->cuda_decoder_prompt_prefill_layer0_attention_residual != 0) { + (void) king_inference_cuda_device_allocator_free( + model, + model->cuda_decoder_prompt_prefill_layer0_attention_residual + ); + } + if (model->cuda_decoder_prompt_prefill_embeddings_active + && model->cuda_decoder_prompt_prefill_layer0_ffn_norm != 0) { + (void) king_inference_cuda_device_allocator_free( + model, + model->cuda_decoder_prompt_prefill_layer0_ffn_norm + ); + } + if (model->cuda_decoder_prompt_prefill_embeddings_active + && model->cuda_decoder_prompt_prefill_layer0_ffn_gate != 0) { + (void) king_inference_cuda_device_allocator_free( + model, + model->cuda_decoder_prompt_prefill_layer0_ffn_gate + ); + } + if (model->cuda_decoder_prompt_prefill_embeddings_active + && model->cuda_decoder_prompt_prefill_layer0_ffn_up != 0) { + (void) king_inference_cuda_device_allocator_free( + model, + model->cuda_decoder_prompt_prefill_layer0_ffn_up + ); + } + if (model->cuda_decoder_prompt_prefill_embeddings_active + && model->cuda_decoder_prompt_prefill_layer0_ffn_swiglu != 0) { + (void) king_inference_cuda_device_allocator_free( + model, + model->cuda_decoder_prompt_prefill_layer0_ffn_swiglu + ); + } + if (model->cuda_decoder_prompt_prefill_embeddings_active + && model->cuda_decoder_prompt_prefill_layer0_ffn_down != 0) { + (void) king_inference_cuda_device_allocator_free( + model, + model->cuda_decoder_prompt_prefill_layer0_ffn_down + ); + } + if (model->cuda_decoder_prompt_prefill_embeddings_active + && model->cuda_decoder_prompt_prefill_layer0_ffn_output_residual != 0) { + (void) king_inference_cuda_device_allocator_free( + model, + model->cuda_decoder_prompt_prefill_layer0_ffn_output_residual + ); + } + model->cuda_decoder_prompt_prefill_embeddings = 0; + model->cuda_decoder_prompt_prefill_initial_norm = 0; + model->cuda_decoder_prompt_prefill_initial_linear = 0; + model->cuda_decoder_prompt_prefill_initial_query_rope = 0; + model->cuda_decoder_prompt_prefill_layer0_attention_stack = 0; + model->cuda_decoder_prompt_prefill_layer0_attention_residual = 0; + model->cuda_decoder_prompt_prefill_layer0_ffn_norm = 0; + model->cuda_decoder_prompt_prefill_layer0_ffn_gate = 0; + model->cuda_decoder_prompt_prefill_layer0_ffn_up = 0; + model->cuda_decoder_prompt_prefill_layer0_ffn_swiglu = 0; + model->cuda_decoder_prompt_prefill_layer0_ffn_down = 0; + model->cuda_decoder_prompt_prefill_layer0_ffn_output_residual = 0; + model->cuda_decoder_prompt_prefill_embedding_bytes = 0; + model->cuda_decoder_prompt_prefill_initial_norm_bytes = 0; + model->cuda_decoder_prompt_prefill_initial_linear_bytes = 0; + model->cuda_decoder_prompt_prefill_initial_query_rope_bytes = 0; + model->cuda_decoder_prompt_prefill_layer0_attention_stack_bytes = 0; + model->cuda_decoder_prompt_prefill_layer0_attention_residual_bytes = 0; + model->cuda_decoder_prompt_prefill_layer0_ffn_norm_bytes = 0; + model->cuda_decoder_prompt_prefill_layer0_ffn_gate_bytes = 0; + model->cuda_decoder_prompt_prefill_layer0_ffn_up_bytes = 0; + model->cuda_decoder_prompt_prefill_layer0_ffn_swiglu_bytes = 0; + model->cuda_decoder_prompt_prefill_layer0_ffn_down_bytes = 0; + model->cuda_decoder_prompt_prefill_layer0_ffn_output_residual_bytes = 0; + model->cuda_decoder_prompt_prefill_kv_cache_writes = 0; + model->cuda_decoder_prompt_prefill_tokens = 0; + model->cuda_decoder_prompt_prefill_width = 0; + model->cuda_decoder_prompt_prefill_initial_linear_rows = 0; + model->cuda_decoder_prompt_prefill_initial_query_rope_heads = 0; + model->cuda_decoder_prompt_prefill_initial_query_rope_head_dim = 0; + model->cuda_decoder_prompt_prefill_kv_heads = 0; + model->cuda_decoder_prompt_prefill_kv_key_length = 0; + model->cuda_decoder_prompt_prefill_kv_value_length = 0; + model->cuda_decoder_prompt_prefill_layer0_attention_heads = 0; + model->cuda_decoder_prompt_prefill_layer0_attention_stack_width = 0; + model->cuda_decoder_prompt_prefill_layer0_attention_residual_width = 0; + model->cuda_decoder_prompt_prefill_layer0_ffn_norm_width = 0; + model->cuda_decoder_prompt_prefill_layer0_ffn_gate_rows = 0; + model->cuda_decoder_prompt_prefill_layer0_ffn_up_rows = 0; + model->cuda_decoder_prompt_prefill_layer0_ffn_swiglu_width = 0; + model->cuda_decoder_prompt_prefill_layer0_ffn_down_width = 0; + model->cuda_decoder_prompt_prefill_layer0_ffn_output_residual_width = 0; + model->cuda_decoder_prompt_prefill_embeddings_active = false; +} + +typedef struct _king_inference_cuda_decoder_prefill_slot_list { + zval **items; + size_t count; + size_t capacity; +} king_inference_cuda_decoder_prefill_slot_list; + +typedef struct _king_inference_cuda_decoder_prefill_mutations { + zval *graph_token_id; + zval *graph_position; + zval *embedding_token_id; + zval *embedding_prefill_batch_index; + king_inference_cuda_decoder_prefill_slot_list rope_positions; + king_inference_cuda_decoder_prefill_slot_list kv_write_slots; + king_inference_cuda_decoder_prefill_slot_list kv_attention_slot_starts; + king_inference_cuda_decoder_prefill_slot_list kv_attention_slot_counts; + king_inference_cuda_decoder_prefill_slot_list sample_indexes; +} king_inference_cuda_decoder_prefill_mutations; + +static void king_inference_cuda_decoder_prompt_loop_prefill_slot_list_release( + king_inference_cuda_decoder_prefill_slot_list *slots +) { + if (slots->items != NULL) { + efree(slots->items); + } + slots->items = NULL; + slots->count = 0; + slots->capacity = 0; +} + +static void king_inference_cuda_decoder_prompt_loop_prefill_mutations_release( + king_inference_cuda_decoder_prefill_mutations *mutations +) { + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_release(&mutations->rope_positions); + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_release(&mutations->kv_write_slots); + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_release(&mutations->kv_attention_slot_starts); + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_release(&mutations->kv_attention_slot_counts); + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_release(&mutations->sample_indexes); + memset(mutations, 0, sizeof(*mutations)); +} + +static zval *king_inference_cuda_decoder_prompt_loop_prefill_long_slot( + zval *array, + const char *key, + zend_long fallback +) { + zval *slot; + zval inserted; + + if (array == NULL || Z_TYPE_P(array) != IS_ARRAY || key == NULL) { + return NULL; + } + slot = king_inference_array_find(array, key); + if (slot == NULL) { + ZVAL_LONG(&inserted, fallback); + slot = zend_hash_str_update(Z_ARRVAL_P(array), key, strlen(key), &inserted); + } + return slot != NULL && Z_TYPE_P(slot) == IS_LONG ? slot : NULL; +} + +static void king_inference_cuda_decoder_prompt_loop_prefill_slot_list_add( + king_inference_cuda_decoder_prefill_slot_list *slots, + zval *slot +) { + size_t new_capacity; + + if (slot == NULL) { + return; + } + if (slots->count == slots->capacity) { + new_capacity = slots->capacity == 0 ? 8 : slots->capacity * 2; + slots->items = slots->items == NULL + ? emalloc(new_capacity * sizeof(*slots->items)) + : erealloc(slots->items, new_capacity * sizeof(*slots->items)); + slots->capacity = new_capacity; + } + slots->items[slots->count++] = slot; +} + +static void king_inference_cuda_decoder_prompt_loop_prefill_slot_list_set( + king_inference_cuda_decoder_prefill_slot_list *slots, + zend_long value +) { + size_t index; + + for (index = 0; index < slots->count; index++) { + ZVAL_LONG(slots->items[index], value); + } +} + +static zend_result king_inference_cuda_decoder_prompt_loop_build_prefill_mutations( + king_inference_model_object *model, + zval *graph, + king_inference_cuda_decoder_prefill_mutations *mutations +) { + zval *ops; + zval *entry; + + memset(mutations, 0, sizeof(*mutations)); + mutations->graph_token_id = king_inference_cuda_decoder_prompt_loop_prefill_long_slot(graph, "token_id", 0); + mutations->graph_position = king_inference_cuda_decoder_prompt_loop_prefill_long_slot(graph, "position", 0); + ops = king_inference_array_find(graph, "ops"); + if (ops == NULL) { + ops = king_inference_array_find(graph, "steps"); + } + if (mutations->graph_token_id == NULL + || mutations->graph_position == NULL + || ops == NULL + || Z_TYPE_P(ops) != IS_ARRAY) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_batch_prefill_mutation_plan_invalid"); + return FAILURE; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(ops), entry) { + zval *op_name; + + if (entry == NULL || Z_TYPE_P(entry) != IS_ARRAY) { + continue; + } + op_name = king_inference_array_find(entry, "op"); + if (op_name == NULL || Z_TYPE_P(op_name) != IS_STRING) { + continue; + } + if (zend_string_equals_literal(Z_STR_P(op_name), "embedding")) { + mutations->embedding_prefill_batch_index = + king_inference_cuda_decoder_prompt_loop_prefill_long_slot(entry, "prefill_batch_index", 0); + mutations->embedding_token_id = + king_inference_cuda_decoder_prompt_loop_prefill_long_slot(entry, "token_id", 0); + } else if (zend_string_equals_literal(Z_STR_P(op_name), "rope")) { + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_add( + &mutations->rope_positions, + king_inference_cuda_decoder_prompt_loop_prefill_long_slot(entry, "position", 0) + ); + } else if (zend_string_equals_literal(Z_STR_P(op_name), "kv_write")) { + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_add( + &mutations->kv_write_slots, + king_inference_cuda_decoder_prompt_loop_prefill_long_slot(entry, "slot", 0) + ); + } else if (zend_string_equals_literal(Z_STR_P(op_name), "kv_attention")) { + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_add( + &mutations->kv_attention_slot_starts, + king_inference_cuda_decoder_prompt_loop_prefill_long_slot(entry, "slot_start", 0) + ); + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_add( + &mutations->kv_attention_slot_counts, + king_inference_cuda_decoder_prompt_loop_prefill_long_slot(entry, "slot_count", 1) + ); + } else if (zend_string_equals_literal(Z_STR_P(op_name), "sample_token")) { + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_add( + &mutations->sample_indexes, + king_inference_cuda_decoder_prompt_loop_prefill_long_slot(entry, "sample_index", 0) + ); + } + } ZEND_HASH_FOREACH_END(); + + if (mutations->embedding_token_id == NULL + || mutations->embedding_prefill_batch_index == NULL + || mutations->rope_positions.count == 0 + || mutations->kv_write_slots.count == 0 + || mutations->kv_attention_slot_starts.count == 0 + || mutations->kv_attention_slot_starts.count != mutations->kv_attention_slot_counts.count) { + king_inference_cuda_decoder_prompt_loop_prefill_mutations_release(mutations); + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_batch_prefill_mutation_slots_missing"); + return FAILURE; + } + + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_prompt_loop_apply_prefill_mutations( + king_inference_model_object *model, + king_inference_cuda_decoder_prefill_mutations *mutations, + zval *tokenized_prompt, + zend_ulong position +) { + zend_ulong token_id; + zend_ulong sliding_window = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_SLIDING_WINDOW]; + zend_ulong slot_start; + zend_ulong slot_count; + + king_inference_decode_graph_attention_window( + model, + position, + sliding_window, + &slot_start, + &slot_count + ); + + if (king_inference_cuda_decoder_prompt_loop_token_at(tokenized_prompt, position, &token_id) != SUCCESS + || token_id > (zend_ulong) ZEND_LONG_MAX + || position > (zend_ulong) ZEND_LONG_MAX + || slot_start > (zend_ulong) ZEND_LONG_MAX + || slot_count > (zend_ulong) ZEND_LONG_MAX) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_batch_prefill_mutation_value_invalid"); + return FAILURE; + } + + ZVAL_LONG(mutations->graph_token_id, (zend_long) token_id); + ZVAL_LONG(mutations->graph_position, (zend_long) position); + ZVAL_LONG(mutations->embedding_token_id, (zend_long) token_id); + ZVAL_LONG(mutations->embedding_prefill_batch_index, (zend_long) position); + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_set(&mutations->rope_positions, (zend_long) position); + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_set(&mutations->kv_write_slots, (zend_long) position); + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_set(&mutations->kv_attention_slot_starts, (zend_long) slot_start); + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_set(&mutations->kv_attention_slot_counts, (zend_long) slot_count); + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_set(&mutations->sample_indexes, (zend_long) position); + return SUCCESS; +} + +#include "cuda_decoder_prompt_decode_template.inc" + +static zend_result king_inference_cuda_decoder_prompt_loop_prepare_prefill_template( + king_inference_model_object *model, + zval *graph, + zval *plan_result +) { + array_init(plan_result); + add_assoc_string(plan_result, "type", "gpu_decoder_prompt_batch_prefill_plan"); + add_assoc_string(plan_result, "backend", "king_native_gpu"); + add_assoc_bool(plan_result, "result_contract_ready", true); + add_assoc_bool(plan_result, "graph_validated", false); + add_assoc_bool(plan_result, "device_execution_result_ready", false); + add_assoc_bool(plan_result, "token_result_ready", false); + add_assoc_bool(plan_result, "decoded_token_ready", false); + add_assoc_bool(plan_result, "execution_plan_ready", false); + add_assoc_bool(plan_result, "silent_cpu_fallback", false); + + if (king_inference_cuda_decoder_graph_executor_validate_graph(model, graph) != SUCCESS) { + zval_ptr_dtor(plan_result); + ZVAL_UNDEF(plan_result); + return FAILURE; + } + add_assoc_bool(plan_result, "graph_validated", true); + if (king_inference_cuda_decoder_graph_executor_prepare_plan(model, graph, plan_result) != SUCCESS) { + zval_ptr_dtor(plan_result); + ZVAL_UNDEF(plan_result); + return FAILURE; + } + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_prompt_loop_prefill_initial_batch( + king_inference_model_object *model, + zval *graph, + zend_ulong prefill_token_count, + zend_ulong width +) { + zval *embedding_op; + zval *rms_norm_op; + zval *linear_op; + zend_string *embedding_id = NULL; + zend_string *rms_norm_id = NULL; + zend_string *rms_weight = NULL; + zend_string *linear_weight = NULL; + zend_ulong linear_rows = 0; + size_t linear_elements; + size_t linear_bytes; + double epsilon = 0.000001; + + embedding_op = king_inference_cuda_decoder_graph_executor_first_op(graph, "embedding"); + if (embedding_op == NULL + || king_inference_graph_required_string(embedding_op, "id", &embedding_id) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_batch_prefill_embedding_template_invalid"); + return FAILURE; + } + rms_norm_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, embedding_id, "rms_norm"); + if (rms_norm_op == NULL + || king_inference_graph_required_string(rms_norm_op, "id", &rms_norm_id) != SUCCESS + || king_inference_graph_required_string(rms_norm_op, "weight", &rms_weight) != SUCCESS + || king_inference_graph_finite_double_option(rms_norm_op, "epsilon", 0.000001, &epsilon) != SUCCESS + || epsilon <= 0.0) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_batch_prefill_initial_norm_template_invalid"); + return FAILURE; + } + linear_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, rms_norm_id, "linear"); + if (linear_op == NULL + || king_inference_cuda_decoder_graph_executor_linear_shape( + model, + linear_op, + width, + &linear_weight, + &linear_rows + ) != SUCCESS + || linear_rows == 0 + || linear_rows > (zend_ulong) (SIZE_MAX / sizeof(float))) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_batch_prefill_initial_linear_template_invalid"); + return FAILURE; + } + if (prefill_token_count > (zend_ulong) (SIZE_MAX / (size_t) linear_rows) + || (size_t) prefill_token_count * (size_t) linear_rows > SIZE_MAX / sizeof(float)) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_batch_prefill_initial_linear_size_overflow"); + return FAILURE; + } + + if (king_inference_cuda_device_allocator_alloc( + model, + model->cuda_decoder_prompt_prefill_embedding_bytes, + &model->cuda_decoder_prompt_prefill_initial_norm + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_prompt_batch_prefill_initial_norm_allocation_failed" + ); + return FAILURE; + } + model->cuda_decoder_prompt_prefill_initial_norm_bytes = + model->cuda_decoder_prompt_prefill_embedding_bytes; + if (king_inference_cuda_rms_norm_f32_batch( + model, + rms_weight, + model->cuda_decoder_prompt_prefill_embeddings, + model->cuda_decoder_prompt_prefill_initial_norm, + width, + prefill_token_count, + epsilon + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_rms_norm_result, + model->cuda_rms_norm_error[0] != '\0' + ? model->cuda_rms_norm_error + : "cuda_decoder_prompt_batch_prefill_initial_norm_failed" + ); + return FAILURE; + } + + linear_elements = (size_t) prefill_token_count * (size_t) linear_rows; + linear_bytes = linear_elements * sizeof(float); + if (king_inference_cuda_device_allocator_alloc( + model, + linear_bytes, + &model->cuda_decoder_prompt_prefill_initial_linear + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_prompt_batch_prefill_initial_linear_allocation_failed" + ); + return FAILURE; + } + model->cuda_decoder_prompt_prefill_initial_linear_bytes = linear_bytes; + model->cuda_decoder_prompt_prefill_initial_linear_rows = linear_rows; + if (king_inference_cuda_quantized_matvec_batch( + model, + linear_weight, + model->cuda_decoder_prompt_prefill_initial_norm, + width, + prefill_token_count, + model->cuda_decoder_prompt_prefill_initial_linear, + linear_rows + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_quantized_matvec_result, + model->cuda_quantized_matvec_error[0] != '\0' + ? model->cuda_quantized_matvec_error + : "cuda_decoder_prompt_batch_prefill_initial_linear_failed" + ); + return FAILURE; + } + if (king_inference_cuda_decoder_prompt_loop_prefill_layer0_query_rope_batch( + model, + graph, + rms_norm_id, + prefill_token_count, + width + ) != SUCCESS) { + return FAILURE; + } + if (king_inference_cuda_decoder_prompt_loop_prefill_layer0_kv_batch( + model, + graph, + rms_norm_id, + prefill_token_count, + width + ) != SUCCESS) { + return FAILURE; + } + if (king_inference_cuda_decoder_prompt_loop_prefill_layer0_attention_stack_batch( + model, + graph, + width, + prefill_token_count + ) != SUCCESS) { + return FAILURE; + } + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_prompt_loop_execute_prefill_position( + king_inference_model_object *model, + zval *graph, + zend_ulong position +) { + zval graph_result; + + array_init(&graph_result); + add_assoc_string(&graph_result, "type", "gpu_decoder_prompt_batch_prefill_position"); + add_assoc_string(&graph_result, "backend", "king_native_gpu"); + add_assoc_bool(&graph_result, "result_contract_ready", true); + add_assoc_bool(&graph_result, "graph_template_reused", true); + add_assoc_long(&graph_result, "position", (zend_long) position); + if (king_inference_cuda_decoder_graph_executor_execute_initial_device_ops( + model, + graph, + &graph_result + ) != SUCCESS) { + zval_ptr_dtor(&graph_result); + return FAILURE; + } + zval_ptr_dtor(&graph_result); + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_prompt_loop_prefill_batch_template( + king_inference_model_object *model, + zval *tokenized_prompt, + zend_ulong token_count, + zval *decode_options, + size_t *validated_graphs, + size_t *result_envelopes, + size_t *prefill_batches, + size_t *prefill_tokens, + size_t *prefill_template_reuses +) { + zval graph; + zval plan_result; + zend_ulong width = model->gguf.architecture_params[KING_INFERENCE_ARCH_EMBEDDING_LENGTH]; + zend_ulong prefill_token_count; + double embedding_scale = king_inference_decode_graph_embedding_scale(model); + size_t prefill_elements; + size_t prefill_bytes; + + *prefill_batches = 0; + *prefill_tokens = 0; + *prefill_template_reuses = 0; + king_inference_cuda_decoder_prompt_loop_release_prefill_embeddings(model); + if (token_count <= 1) { + model->cuda_decoder_prompt_loop_last_used_batch_prefill = false; + return SUCCESS; + } + if (!model->cuda_decoder_prompt_loop_batch_prefill_available) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_batch_prefill_unavailable"); + model->cuda_decoder_prompt_loop_last_used_batch_prefill = false; + return FAILURE; + } + prefill_token_count = token_count - 1; + if (width == 0 + || width > (zend_ulong) (SIZE_MAX / sizeof(float)) + || prefill_token_count > (zend_ulong) (SIZE_MAX / (size_t) width)) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_batch_prefill_shape_invalid"); + model->cuda_decoder_prompt_loop_last_used_batch_prefill = false; + return FAILURE; + } + prefill_elements = (size_t) prefill_token_count * (size_t) width; + if (prefill_elements > SIZE_MAX / sizeof(float)) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_batch_prefill_size_overflow"); + model->cuda_decoder_prompt_loop_last_used_batch_prefill = false; + return FAILURE; + } + prefill_bytes = prefill_elements * sizeof(float); + if (king_inference_cuda_device_allocator_alloc( + model, + prefill_bytes, + &model->cuda_decoder_prompt_prefill_embeddings + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_prompt_batch_prefill_embedding_matrix_allocation_failed" + ); + model->cuda_decoder_prompt_loop_last_used_batch_prefill = false; + return FAILURE; + } + model->cuda_decoder_prompt_prefill_embeddings_active = true; + model->cuda_decoder_prompt_prefill_embedding_bytes = prefill_bytes; + model->cuda_decoder_prompt_prefill_tokens = prefill_token_count; + model->cuda_decoder_prompt_prefill_width = width; + if (king_inference_cuda_embedding_rows_load( + model, + tokenized_prompt, + prefill_token_count, + model->cuda_decoder_prompt_prefill_embeddings, + width, + embedding_scale + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_embedding_row_result, + model->cuda_embedding_row_error[0] != '\0' + ? model->cuda_embedding_row_error + : "cuda_decoder_prompt_batch_prefill_embedding_rows_load_failed" + ); + king_inference_cuda_decoder_prompt_loop_release_prefill_embeddings(model); + model->cuda_decoder_prompt_loop_last_used_batch_prefill = false; + return FAILURE; + } + + ZVAL_UNDEF(&graph); + ZVAL_UNDEF(&plan_result); + add_assoc_bool(decode_options, "emit_token", false); + if (king_inference_token_decode_graph_array(model, tokenized_prompt, 0, decode_options, &graph) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_batch_prefill_template_build_failed"); + king_inference_cuda_decoder_prompt_loop_release_prefill_embeddings(model); + model->cuda_decoder_prompt_loop_last_used_batch_prefill = false; + return FAILURE; + } + if (king_inference_cuda_decoder_prompt_loop_prepare_prefill_template( + model, + &graph, + &plan_result + ) != SUCCESS) { + zval_ptr_dtor(&graph); + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + model->cuda_decoder_graph_executor_error[0] != '\0' + ? model->cuda_decoder_graph_executor_error + : "cuda_decoder_prompt_batch_prefill_template_plan_failed" + ); + king_inference_cuda_decoder_prompt_loop_release_prefill_embeddings(model); + model->cuda_decoder_prompt_loop_last_used_batch_prefill = false; + return FAILURE; + } + if (king_inference_cuda_decoder_prompt_loop_prefill_initial_batch( + model, + &graph, + prefill_token_count, + width + ) != SUCCESS) { + zval_ptr_dtor(&plan_result); + zval_ptr_dtor(&graph); + king_inference_cuda_decoder_prompt_loop_release_prefill_embeddings(model); + model->cuda_decoder_prompt_loop_last_used_batch_prefill = false; + return FAILURE; + } + if (king_inference_cuda_decoder_prompt_loop_prefill_remaining_layers_batch( + model, + &graph, + prefill_token_count, + width + ) != SUCCESS) { + zval_ptr_dtor(&plan_result); + zval_ptr_dtor(&graph); + king_inference_cuda_decoder_prompt_loop_release_prefill_embeddings(model); + model->cuda_decoder_prompt_loop_last_used_batch_prefill = false; + return FAILURE; + } + (*validated_graphs)++; + (*result_envelopes)++; + *prefill_tokens = prefill_token_count; + zval_ptr_dtor(&plan_result); + zval_ptr_dtor(&graph); + king_inference_cuda_decoder_prompt_loop_release_prefill_embeddings(model); + *prefill_batches = 1; + model->cuda_decoder_prompt_loop_last_used_batch_prefill = true; + return SUCCESS; +} diff --git a/extension/src/inference/cuda/prompt/prefill/batch/cuda_decoder_prompt_decode_template.inc b/extension/src/inference/cuda/prompt/prefill/batch/cuda_decoder_prompt_decode_template.inc new file mode 100644 index 000000000..e98a990c1 --- /dev/null +++ b/extension/src/inference/cuda/prompt/prefill/batch/cuda_decoder_prompt_decode_template.inc @@ -0,0 +1,267 @@ +/* + * CUDA prompt decode template reuse for native King GPU inference. + */ + +static zend_result king_inference_cuda_decoder_prompt_loop_build_decode_mutations( + king_inference_model_object *model, + zval *graph, + king_inference_cuda_decoder_prefill_mutations *mutations +) { + zval *ops; + zval *entry; + + memset(mutations, 0, sizeof(*mutations)); + mutations->graph_token_id = king_inference_cuda_decoder_prompt_loop_prefill_long_slot(graph, "token_id", 0); + mutations->graph_position = king_inference_cuda_decoder_prompt_loop_prefill_long_slot(graph, "position", 0); + ops = king_inference_array_find(graph, "ops"); + if (ops == NULL) { + ops = king_inference_array_find(graph, "steps"); + } + if (mutations->graph_token_id == NULL + || mutations->graph_position == NULL + || ops == NULL + || Z_TYPE_P(ops) != IS_ARRAY) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_decode_template_mutation_plan_invalid"); + return FAILURE; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(ops), entry) { + zval *op_name; + + if (entry == NULL || Z_TYPE_P(entry) != IS_ARRAY) { + continue; + } + op_name = king_inference_array_find(entry, "op"); + if (op_name == NULL || Z_TYPE_P(op_name) != IS_STRING) { + continue; + } + if (zend_string_equals_literal(Z_STR_P(op_name), "embedding")) { + mutations->embedding_token_id = + king_inference_cuda_decoder_prompt_loop_prefill_long_slot(entry, "token_id", 0); + } else if (zend_string_equals_literal(Z_STR_P(op_name), "rope")) { + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_add( + &mutations->rope_positions, + king_inference_cuda_decoder_prompt_loop_prefill_long_slot(entry, "position", 0) + ); + } else if (zend_string_equals_literal(Z_STR_P(op_name), "kv_write")) { + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_add( + &mutations->kv_write_slots, + king_inference_cuda_decoder_prompt_loop_prefill_long_slot(entry, "slot", 0) + ); + } else if (zend_string_equals_literal(Z_STR_P(op_name), "kv_attention")) { + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_add( + &mutations->kv_attention_slot_starts, + king_inference_cuda_decoder_prompt_loop_prefill_long_slot(entry, "slot_start", 0) + ); + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_add( + &mutations->kv_attention_slot_counts, + king_inference_cuda_decoder_prompt_loop_prefill_long_slot(entry, "slot_count", 1) + ); + } else if (zend_string_equals_literal(Z_STR_P(op_name), "sample_token")) { + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_add( + &mutations->sample_indexes, + king_inference_cuda_decoder_prompt_loop_prefill_long_slot(entry, "sample_index", 0) + ); + } + } ZEND_HASH_FOREACH_END(); + + if (mutations->embedding_token_id == NULL + || mutations->rope_positions.count == 0 + || mutations->kv_write_slots.count == 0 + || mutations->kv_attention_slot_starts.count == 0 + || mutations->kv_attention_slot_starts.count != mutations->kv_attention_slot_counts.count) { + king_inference_cuda_decoder_prompt_loop_prefill_mutations_release(mutations); + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_decode_template_mutation_slots_missing"); + return FAILURE; + } + + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_prompt_loop_apply_decode_mutations( + king_inference_model_object *model, + king_inference_cuda_decoder_prefill_mutations *mutations, + zend_ulong token_id, + zend_ulong position +) { + zend_ulong sliding_window = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_SLIDING_WINDOW]; + zend_ulong slot_start; + zend_ulong slot_count; + + king_inference_decode_graph_attention_window( + model, + position, + sliding_window, + &slot_start, + &slot_count + ); + + if (token_id > (zend_ulong) ZEND_LONG_MAX + || position > (zend_ulong) ZEND_LONG_MAX + || slot_start > (zend_ulong) ZEND_LONG_MAX + || slot_count > (zend_ulong) ZEND_LONG_MAX) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_decode_template_mutation_value_invalid"); + return FAILURE; + } + + ZVAL_LONG(mutations->graph_token_id, (zend_long) token_id); + ZVAL_LONG(mutations->graph_position, (zend_long) position); + ZVAL_LONG(mutations->embedding_token_id, (zend_long) token_id); + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_set(&mutations->rope_positions, (zend_long) position); + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_set(&mutations->kv_write_slots, (zend_long) position); + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_set(&mutations->kv_attention_slot_starts, (zend_long) slot_start); + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_set(&mutations->kv_attention_slot_counts, (zend_long) slot_count); + king_inference_cuda_decoder_prompt_loop_prefill_slot_list_set(&mutations->sample_indexes, (zend_long) position); + return SUCCESS; +} + +static void king_inference_cuda_decoder_prompt_loop_disable_candidate_materialization( + zval *graph +) { + zval *ops = king_inference_array_find(graph, "ops"); + zval *entry; + + if (ops == NULL) { + ops = king_inference_array_find(graph, "steps"); + } + if (ops == NULL || Z_TYPE_P(ops) != IS_ARRAY) { + return; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(ops), entry) { + zval *op_name; + + if (entry == NULL || Z_TYPE_P(entry) != IS_ARRAY) { + continue; + } + op_name = king_inference_array_find(entry, "op"); + if (op_name != NULL + && Z_TYPE_P(op_name) == IS_STRING + && (zend_string_equals_literal(Z_STR_P(op_name), "argmax_token") + || zend_string_equals_literal(Z_STR_P(op_name), "sample_token"))) { + add_assoc_bool(entry, "materialize_candidates", false); + } + } ZEND_HASH_FOREACH_END(); +} + +static zend_result king_inference_cuda_decoder_prompt_loop_prepare_decode_template( + king_inference_model_object *model, + zval *graph, + zval *plan_result +) { + array_init(plan_result); + add_assoc_string(plan_result, "type", "gpu_decoder_prompt_decode_template_plan"); + add_assoc_string(plan_result, "backend", "king_native_gpu"); + add_assoc_bool(plan_result, "result_contract_ready", true); + add_assoc_bool(plan_result, "graph_validated", false); + add_assoc_bool(plan_result, "execution_plan_ready", false); + add_assoc_bool(plan_result, "device_execution_result_ready", false); + add_assoc_bool(plan_result, "graph_template_resident", true); + add_assoc_bool(plan_result, "silent_cpu_fallback", false); + + king_inference_cuda_decoder_prompt_loop_disable_candidate_materialization(graph); + if (king_inference_cuda_decoder_graph_executor_validate_graph(model, graph) != SUCCESS) { + zval_ptr_dtor(plan_result); + ZVAL_UNDEF(plan_result); + return FAILURE; + } + add_assoc_bool(plan_result, "graph_validated", true); + if (king_inference_cuda_decoder_graph_executor_prepare_op_index(model, graph) != SUCCESS) { + zval_ptr_dtor(plan_result); + ZVAL_UNDEF(plan_result); + return FAILURE; + } + add_assoc_bool(plan_result, "graph_op_index_ready", true); + if (king_inference_cuda_decoder_graph_executor_prepare_plan(model, graph, plan_result) != SUCCESS) { + zval_ptr_dtor(plan_result); + ZVAL_UNDEF(plan_result); + return FAILURE; + } + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_prompt_loop_execute_decode_template( + king_inference_model_object *model, + king_inference_stream_object *stream, + zval *graph, + king_inference_cuda_decoder_prefill_mutations *mutations, + zend_ulong token_id, + zend_ulong position, + zend_ulong *decoded_token_id, + zend_string **decoded_token_text, + double *decoded_probability, + double *decoded_logit, + double *decoded_rank, + zend_long *mutation_ns, + zend_long *device_execute_ns, + zend_long *bounded_emit_ns, + zend_long *sampler_ns, + zend_long *token_decode_ns +) { + zval graph_result; + uint64_t start_ns; + uint64_t end_ns; + + start_ns = king_inference_cuda_decoder_prompt_profile_ns(); + if (king_inference_cuda_decoder_prompt_loop_apply_decode_mutations( + model, + mutations, + token_id, + position + ) != SUCCESS) { + return FAILURE; + } + end_ns = king_inference_cuda_decoder_prompt_profile_ns(); + if (mutation_ns != NULL) { + *mutation_ns = king_inference_cuda_decoder_prompt_profile_elapsed(end_ns, start_ns); + } + + array_init(&graph_result); + add_assoc_string(&graph_result, "type", "gpu_decoder_prompt_decode_template_execution"); + add_assoc_string(&graph_result, "backend", "king_native_gpu"); + add_assoc_bool(&graph_result, "result_contract_ready", true); + add_assoc_bool(&graph_result, "graph_validated", true); + add_assoc_bool(&graph_result, "execution_plan_ready", true); + add_assoc_bool(&graph_result, "graph_template_reused", true); + add_assoc_bool(&graph_result, "graph_constructed_on_hot_path", false); + add_assoc_bool(&graph_result, "compact_decode_result", true); + add_assoc_bool(&graph_result, "silent_cpu_fallback", false); + add_assoc_long(&graph_result, "token_id", (zend_long) token_id); + add_assoc_long(&graph_result, "position", (zend_long) position); + start_ns = king_inference_cuda_decoder_prompt_profile_ns(); + if (king_inference_cuda_decoder_graph_executor_execute_initial_device_ops( + model, + graph, + &graph_result + ) != SUCCESS) { + zval_ptr_dtor(&graph_result); + return FAILURE; + } + end_ns = king_inference_cuda_decoder_prompt_profile_ns(); + if (device_execute_ns != NULL) { + *device_execute_ns = king_inference_cuda_decoder_prompt_profile_elapsed(end_ns, start_ns); + } + start_ns = king_inference_cuda_decoder_prompt_profile_ns(); + if (king_inference_cuda_decoder_prompt_loop_emit_bounded_token( + model, + stream, + graph, + &graph_result, + decoded_token_id, + decoded_token_text, + decoded_probability, + decoded_logit, + decoded_rank, + sampler_ns, + token_decode_ns + ) != SUCCESS) { + zval_ptr_dtor(&graph_result); + return FAILURE; + } + end_ns = king_inference_cuda_decoder_prompt_profile_ns(); + if (bounded_emit_ns != NULL) { + *bounded_emit_ns = king_inference_cuda_decoder_prompt_profile_elapsed(end_ns, start_ns); + } + zval_ptr_dtor(&graph_result); + return SUCCESS; +} diff --git a/extension/src/inference/cuda/prompt/prefill/ffn/cuda_decoder_prompt_prefill_ffn_batch.inc b/extension/src/inference/cuda/prompt/prefill/ffn/cuda_decoder_prompt_prefill_ffn_batch.inc new file mode 100644 index 000000000..376375bcd --- /dev/null +++ b/extension/src/inference/cuda/prompt/prefill/ffn/cuda_decoder_prompt_prefill_ffn_batch.inc @@ -0,0 +1,395 @@ +/* + * CUDA prompt prefill layer-0 FFN batch helpers. + */ + +static zend_result king_inference_cuda_decoder_prompt_loop_prefill_layer0_ffn_down_batch( + king_inference_model_object *model, + zval *graph, + zval *residual_op, + zend_ulong swiglu_width, + zend_ulong prefill_token_count +) { + zval *ffn_norm_op; + zval *gate_op; + zval *up_op; + zval *silu_op; + zval *mul_op; + zval *down_op; + zend_string *residual_id = NULL; + zend_string *ffn_norm_id = NULL; + zend_string *gate_id = NULL; + zend_string *up_id = NULL; + zend_string *silu_id = NULL; + zend_string *gated_id = NULL; + zend_string *down_weight = NULL; + zend_ulong down_rows = 0; + size_t down_elements; + size_t down_bytes; + + if (model->cuda_decoder_prompt_prefill_layer0_ffn_swiglu == 0) { + return SUCCESS; + } + if (graph == NULL + || residual_op == NULL + || swiglu_width == 0 + || swiglu_width != model->cuda_decoder_prompt_prefill_layer0_ffn_swiglu_width + || prefill_token_count == 0) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_down_shape_invalid" + ); + return FAILURE; + } + if (king_inference_graph_required_string(residual_op, "id", &residual_id) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_down_residual_id_invalid" + ); + return FAILURE; + } + + ffn_norm_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, residual_id, "rms_norm"); + if (ffn_norm_op == NULL) { + return SUCCESS; + } + if (king_inference_graph_required_string(ffn_norm_op, "id", &ffn_norm_id) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_down_norm_id_invalid" + ); + return FAILURE; + } + gate_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, ffn_norm_id, "linear", 0); + up_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, ffn_norm_id, "linear", 1); + if (gate_op == NULL && up_op == NULL) { + return SUCCESS; + } + if (gate_op == NULL + || up_op == NULL + || king_inference_graph_required_string(gate_op, "id", &gate_id) != SUCCESS + || king_inference_graph_required_string(up_op, "id", &up_id) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_down_gate_up_contract_invalid" + ); + return FAILURE; + } + + silu_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, gate_id, "silu"); + if (silu_op == NULL) { + silu_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, gate_id, "gelu_tanh"); + } + if (silu_op == NULL) { + return SUCCESS; + } + if (king_inference_graph_required_string(silu_op, "id", &silu_id) != SUCCESS + || (mul_op = king_inference_cuda_decoder_graph_executor_mul_op_for_inputs(graph, silu_id, up_id)) == NULL + || king_inference_graph_required_string(mul_op, "id", &gated_id) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_down_swiglu_contract_invalid" + ); + return FAILURE; + } + + down_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, gated_id, "linear"); + if (down_op == NULL) { + return SUCCESS; + } + if (king_inference_cuda_decoder_graph_executor_linear_shape( + model, + down_op, + swiglu_width, + &down_weight, + &down_rows + ) != SUCCESS + || down_rows == 0) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + model->cuda_decoder_graph_executor_error[0] != '\0' + ? model->cuda_decoder_graph_executor_error + : "cuda_decoder_prompt_batch_prefill_ffn_down_contract_invalid" + ); + return FAILURE; + } + if (prefill_token_count > (zend_ulong) (SIZE_MAX / (size_t) down_rows)) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_down_size_overflow" + ); + return FAILURE; + } + + down_elements = (size_t) prefill_token_count * (size_t) down_rows; + if (down_elements > SIZE_MAX / sizeof(float)) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_down_bytes_overflow" + ); + return FAILURE; + } + down_bytes = down_elements * sizeof(float); + if (king_inference_cuda_device_allocator_alloc( + model, + down_bytes, + &model->cuda_decoder_prompt_prefill_layer0_ffn_down + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_prompt_batch_prefill_ffn_down_allocation_failed" + ); + return FAILURE; + } + if (king_inference_cuda_quantized_matvec_batch( + model, + down_weight, + model->cuda_decoder_prompt_prefill_layer0_ffn_swiglu, + swiglu_width, + prefill_token_count, + model->cuda_decoder_prompt_prefill_layer0_ffn_down, + down_rows + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_quantized_matvec_result, + model->cuda_quantized_matvec_error[0] != '\0' + ? model->cuda_quantized_matvec_error + : "cuda_decoder_prompt_batch_prefill_ffn_down_failed" + ); + return FAILURE; + } + + model->cuda_decoder_prompt_prefill_layer0_ffn_down_bytes = down_bytes; + model->cuda_decoder_prompt_prefill_layer0_ffn_down_width = down_rows; + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_prompt_loop_prefill_layer0_ffn_output_residual_batch( + king_inference_model_object *model, + zval *graph, + zval *residual_op, + zend_ulong residual_width, + zend_ulong prefill_token_count +) { + zval *ffn_norm_op; + zval *gate_op; + zval *up_op; + zval *silu_op; + zval *mul_op; + zval *down_op; + zval *post_ffw_norm_op = NULL; + zval *output_op; + zend_string *residual_id = NULL; + zend_string *ffn_norm_id = NULL; + zend_string *gate_id = NULL; + zend_string *up_id = NULL; + zend_string *silu_id = NULL; + zend_string *gated_id = NULL; + zend_string *down_id = NULL; + zend_string *post_ffw_norm_id = NULL; + zend_string *post_ffw_norm_weight = NULL; + zend_string *output_right_id = NULL; + zend_string *output_id = NULL; + king_inference_cuda_device_ptr post_ffw_norm_matrix = 0; + king_inference_cuda_device_ptr output_right_matrix = 0; + size_t output_elements; + size_t output_bytes; + double post_ffw_norm_epsilon = 0.000001; + + if (model->cuda_decoder_prompt_prefill_layer0_ffn_down == 0) { + return SUCCESS; + } + if (graph == NULL + || residual_op == NULL + || residual_width == 0 + || residual_width != model->cuda_decoder_prompt_prefill_layer0_attention_residual_width + || residual_width != model->cuda_decoder_prompt_prefill_layer0_ffn_down_width + || prefill_token_count == 0 + || prefill_token_count > (zend_ulong) (SIZE_MAX / (size_t) residual_width)) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_output_residual_shape_invalid" + ); + return FAILURE; + } + if (king_inference_graph_required_string(residual_op, "id", &residual_id) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_output_residual_id_invalid" + ); + return FAILURE; + } + + ffn_norm_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, residual_id, "rms_norm"); + if (ffn_norm_op == NULL) { + return SUCCESS; + } + if (king_inference_graph_required_string(ffn_norm_op, "id", &ffn_norm_id) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_output_norm_id_invalid" + ); + return FAILURE; + } + gate_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, ffn_norm_id, "linear", 0); + up_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, ffn_norm_id, "linear", 1); + if (gate_op == NULL && up_op == NULL) { + return SUCCESS; + } + if (gate_op == NULL + || up_op == NULL + || king_inference_graph_required_string(gate_op, "id", &gate_id) != SUCCESS + || king_inference_graph_required_string(up_op, "id", &up_id) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_output_gate_up_contract_invalid" + ); + return FAILURE; + } + + silu_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, gate_id, "silu"); + if (silu_op == NULL) { + silu_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, gate_id, "gelu_tanh"); + } + if (silu_op == NULL) { + return SUCCESS; + } + if (king_inference_graph_required_string(silu_op, "id", &silu_id) != SUCCESS + || (mul_op = king_inference_cuda_decoder_graph_executor_mul_op_for_inputs(graph, silu_id, up_id)) == NULL + || king_inference_graph_required_string(mul_op, "id", &gated_id) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_output_swiglu_contract_invalid" + ); + return FAILURE; + } + + down_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, gated_id, "linear"); + if (down_op == NULL) { + return SUCCESS; + } + if (king_inference_graph_required_string(down_op, "id", &down_id) != SUCCESS) { + return SUCCESS; + } + output_right_id = down_id; + post_ffw_norm_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, down_id, "rms_norm"); + if (post_ffw_norm_op != NULL) { + if (king_inference_graph_required_string(post_ffw_norm_op, "id", &post_ffw_norm_id) != SUCCESS + || king_inference_graph_required_string(post_ffw_norm_op, "weight", &post_ffw_norm_weight) != SUCCESS + || king_inference_graph_finite_double_option(post_ffw_norm_op, "epsilon", 0.000001, &post_ffw_norm_epsilon) != SUCCESS + || post_ffw_norm_epsilon <= 0.0) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_post_ffw_norm_contract_invalid" + ); + return FAILURE; + } + output_right_id = post_ffw_norm_id; + } + output_op = king_inference_cuda_decoder_graph_executor_add_op_for_inputs(graph, residual_id, output_right_id); + if (output_op == NULL) { + return SUCCESS; + } + if (king_inference_graph_required_string(output_op, "id", &output_id) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_output_contract_invalid" + ); + return FAILURE; + } + (void) output_id; + + output_elements = (size_t) prefill_token_count * (size_t) residual_width; + if (output_elements > SIZE_MAX / sizeof(float) || output_elements > UINT_MAX) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_ffn_output_size_overflow" + ); + return FAILURE; + } + output_bytes = output_elements * sizeof(float); + output_right_matrix = model->cuda_decoder_prompt_prefill_layer0_ffn_down; + if (post_ffw_norm_op != NULL) { + if (king_inference_cuda_device_allocator_alloc(model, output_bytes, &post_ffw_norm_matrix) != SUCCESS + || king_inference_cuda_rms_norm_f32_batch( + model, + post_ffw_norm_weight, + model->cuda_decoder_prompt_prefill_layer0_ffn_down, + post_ffw_norm_matrix, + residual_width, + prefill_token_count, + post_ffw_norm_epsilon + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_ffw_norm_matrix); + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_rms_norm_result != 0 + ? model->cuda_rms_norm_result + : model->cuda_device_allocator_result, + model->cuda_rms_norm_error[0] != '\0' + ? model->cuda_rms_norm_error + : (model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_prompt_batch_prefill_post_ffw_norm_failed") + ); + return FAILURE; + } + output_right_matrix = post_ffw_norm_matrix; + } + if (king_inference_cuda_device_allocator_alloc( + model, + output_bytes, + &model->cuda_decoder_prompt_prefill_layer0_ffn_output_residual + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_ffw_norm_matrix); + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_prompt_batch_prefill_ffn_output_allocation_failed" + ); + return FAILURE; + } + if (king_inference_cuda_vector_add( + model, + model->cuda_decoder_prompt_prefill_layer0_attention_residual, + output_right_matrix, + model->cuda_decoder_prompt_prefill_layer0_ffn_output_residual, + (zend_ulong) output_elements + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_ffw_norm_matrix); + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_device_vector_ops_result, + model->cuda_device_vector_ops_error[0] != '\0' + ? model->cuda_device_vector_ops_error + : "cuda_decoder_prompt_batch_prefill_ffn_output_failed" + ); + return FAILURE; + } + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_ffw_norm_matrix); + + model->cuda_decoder_prompt_prefill_layer0_ffn_output_residual_bytes = output_bytes; + model->cuda_decoder_prompt_prefill_layer0_ffn_output_residual_width = residual_width; + return SUCCESS; +} diff --git a/extension/src/inference/cuda/prompt/prefill/kv/cuda_decoder_prompt_prefill_kv_batch.inc b/extension/src/inference/cuda/prompt/prefill/kv/cuda_decoder_prompt_prefill_kv_batch.inc new file mode 100644 index 000000000..5c57147c4 --- /dev/null +++ b/extension/src/inference/cuda/prompt/prefill/kv/cuda_decoder_prompt_prefill_kv_batch.inc @@ -0,0 +1,185 @@ +/* + * CUDA prompt prefill K/V cache batch path. + */ + +static zend_result king_inference_cuda_decoder_prompt_loop_prefill_layer0_kv_batch( + king_inference_model_object *model, + zval *graph, + zend_string *rms_norm_id, + zend_ulong prefill_token_count, + zend_ulong width +) { + zval *key_linear_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, rms_norm_id, "linear", 1); + zval *value_linear_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, rms_norm_id, "linear", 2); + zval *key_slice_op; + zval *key_rope_op = NULL; + zend_string *key_linear_weight = NULL; + zend_string *value_linear_weight = NULL; + zend_string *key_linear_id = NULL; + zend_string *key_slice_id = NULL; + zend_ulong key_rows = 0; + zend_ulong value_rows = 0; + zend_ulong key_length = model->cuda_decoder_prompt_prefill_initial_query_rope_head_dim; + zend_ulong value_length = 0; + zend_ulong kv_heads = 0; + zend_ulong head; + king_inference_cuda_device_ptr key_matrix = 0; + king_inference_cuda_device_ptr value_matrix = 0; + king_inference_cuda_device_ptr key_rope_matrix = 0; + size_t key_bytes; + size_t value_bytes; + size_t key_rope_bytes; + double rope_position_scale = 1.0; + double rope_base = 10000.0; + zend_ulong rope_pairing = 0; + + if (key_linear_op == NULL || value_linear_op == NULL || rms_norm_id == NULL + || !model->cuda_device_kv_cache_available || key_length == 0) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_batch_prefill_kv_template_invalid"); + return FAILURE; + } + if (king_inference_graph_required_string(key_linear_op, "id", &key_linear_id) != SUCCESS + || king_inference_cuda_decoder_graph_executor_linear_shape(model, key_linear_op, width, &key_linear_weight, &key_rows) != SUCCESS + || king_inference_cuda_decoder_graph_executor_linear_shape(model, value_linear_op, width, &value_linear_weight, &value_rows) != SUCCESS + || key_length > model->cuda_device_kv_cache_key_length + || key_rows < key_length + || key_rows % key_length != 0 + || prefill_token_count > (zend_ulong) (SIZE_MAX / (size_t) key_rows) + || prefill_token_count > (zend_ulong) (SIZE_MAX / (size_t) value_rows)) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_batch_prefill_kv_shape_invalid"); + return FAILURE; + } + kv_heads = key_rows / key_length; + if (kv_heads == 0 + || kv_heads > model->cuda_device_kv_cache_kv_heads + || value_rows < kv_heads + || value_rows % kv_heads != 0) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_batch_prefill_kv_shape_invalid"); + return FAILURE; + } + value_length = value_rows / kv_heads; + if (value_length == 0 || value_length > model->cuda_device_kv_cache_value_length) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_batch_prefill_kv_shape_invalid"); + return FAILURE; + } + key_slice_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, key_linear_id, "slice"); + if (key_slice_op != NULL + && king_inference_graph_required_string(key_slice_op, "id", &key_slice_id) == SUCCESS) { + key_rope_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, key_slice_id, "rope"); + } + if (key_rope_op != NULL + && (king_inference_graph_finite_double_option(key_rope_op, "position_scale", 1.0, &rope_position_scale) != SUCCESS + || king_inference_graph_finite_double_option(key_rope_op, "rope_base", 10000.0, &rope_base) != SUCCESS)) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_batch_prefill_kv_rope_options_invalid"); + return FAILURE; + } + if (key_rope_op != NULL) { + rope_pairing = king_inference_tensor_option_ulong(key_rope_op, "pairing", 0); + } + + key_bytes = (size_t) prefill_token_count * (size_t) key_rows * sizeof(float); + value_bytes = (size_t) prefill_token_count * (size_t) value_rows * sizeof(float); + key_rope_bytes = (size_t) prefill_token_count * (size_t) key_length * sizeof(float); + if (king_inference_cuda_device_allocator_alloc(model, key_bytes, &key_matrix) != SUCCESS + || king_inference_cuda_device_allocator_alloc(model, value_bytes, &value_matrix) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_matrix); + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_prompt_batch_prefill_kv_allocation_failed" + ); + return FAILURE; + } + if (king_inference_cuda_quantized_matvec_batch( + model, + key_linear_weight, + model->cuda_decoder_prompt_prefill_initial_norm, + width, + prefill_token_count, + key_matrix, + key_rows + ) != SUCCESS + || king_inference_cuda_quantized_matvec_batch( + model, + value_linear_weight, + model->cuda_decoder_prompt_prefill_initial_norm, + width, + prefill_token_count, + value_matrix, + value_rows + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_matrix); + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_quantized_matvec_result, + model->cuda_quantized_matvec_error[0] != '\0' + ? model->cuda_quantized_matvec_error + : "cuda_decoder_prompt_batch_prefill_kv_projection_failed" + ); + return FAILURE; + } + + for (head = 0; head < kv_heads; head++) { + if (king_inference_cuda_device_allocator_alloc(model, key_rope_bytes, &key_rope_matrix) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_matrix); + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_batch_prefill_kv_rope_allocation_failed"); + return FAILURE; + } + if (king_inference_cuda_rope_f32_batch_slice( + model, + key_matrix, + key_rope_matrix, + key_rows, + head * key_length, + key_length, + prefill_token_count, + 0, + rope_position_scale, + rope_base, + rope_pairing + ) != SUCCESS + || king_inference_cuda_device_kv_cache_write_batch_strided( + model, + 0, + head, + 0, + prefill_token_count, + key_rope_matrix, + key_length, + 0, + key_length, + value_matrix, + value_rows, + head * value_length, + value_length + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_rope_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_matrix); + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_rope_result != 0 ? model->cuda_rope_result : model->cuda_device_kv_cache_result, + model->cuda_rope_error[0] != '\0' + ? model->cuda_rope_error + : (model->cuda_device_kv_cache_error[0] != '\0' + ? model->cuda_device_kv_cache_error + : "cuda_decoder_prompt_batch_prefill_kv_cache_write_failed") + ); + return FAILURE; + } + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_rope_matrix); + model->cuda_decoder_prompt_prefill_kv_cache_writes++; + } + + model->cuda_decoder_prompt_prefill_kv_heads = kv_heads; + model->cuda_decoder_prompt_prefill_kv_key_length = key_length; + model->cuda_decoder_prompt_prefill_kv_value_length = value_length; + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &value_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_matrix); + return SUCCESS; +} diff --git a/extension/src/inference/cuda/prompt/prefill/query/cuda_decoder_prompt_prefill_query_batch.inc b/extension/src/inference/cuda/prompt/prefill/query/cuda_decoder_prompt_prefill_query_batch.inc new file mode 100644 index 000000000..30825f7ff --- /dev/null +++ b/extension/src/inference/cuda/prompt/prefill/query/cuda_decoder_prompt_prefill_query_batch.inc @@ -0,0 +1,140 @@ +/* + * CUDA prompt prefill query-RoPE batch path. + */ + +static zend_result king_inference_cuda_decoder_prompt_loop_prefill_layer0_query_rope_batch( + king_inference_model_object *model, + zval *graph, + zend_string *rms_norm_id, + zend_ulong prefill_token_count, + zend_ulong width +) { + zval *query_linear_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, rms_norm_id, "linear", 0); + zval *query_slice_op; + zval *query_rope_op = NULL; + zend_string *query_linear_weight = NULL; + zend_string *query_linear_id = NULL; + zend_string *query_slice_id = NULL; + zend_ulong query_rows = 0; + zend_ulong configured_heads = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT]; + zend_ulong head_dim = 0; + zend_ulong heads; + zend_ulong head; + size_t query_rope_bytes; + double rope_position_scale = 1.0; + double rope_base = 10000.0; + zend_ulong rope_pairing = 0; + + if (query_linear_op == NULL + || rms_norm_id == NULL + || model->cuda_decoder_prompt_prefill_initial_linear == 0 + || prefill_token_count == 0 + || configured_heads == 0) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_query_rope_template_invalid" + ); + return FAILURE; + } + if (king_inference_graph_required_string(query_linear_op, "id", &query_linear_id) != SUCCESS + || king_inference_cuda_decoder_graph_executor_linear_shape( + model, + query_linear_op, + width, + &query_linear_weight, + &query_rows + ) != SUCCESS + || query_rows == 0 + || query_rows != model->cuda_decoder_prompt_prefill_initial_linear_rows + || query_rows % configured_heads != 0) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_query_rope_shape_invalid" + ); + return FAILURE; + } + heads = configured_heads; + head_dim = query_rows / configured_heads; + if (heads == 0 + || head_dim == 0 + || head_dim > model->cuda_device_kv_cache_key_length + || prefill_token_count > (zend_ulong) (SIZE_MAX / (size_t) query_rows) + || (size_t) prefill_token_count * (size_t) query_rows > SIZE_MAX / sizeof(float)) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_query_rope_size_invalid" + ); + return FAILURE; + } + + query_slice_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, query_linear_id, "slice"); + if (query_slice_op != NULL + && king_inference_graph_required_string(query_slice_op, "id", &query_slice_id) == SUCCESS) { + query_rope_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, query_slice_id, "rope"); + } + if (query_rope_op != NULL + && (king_inference_graph_finite_double_option(query_rope_op, "position_scale", 1.0, &rope_position_scale) != SUCCESS + || king_inference_graph_finite_double_option(query_rope_op, "rope_base", 10000.0, &rope_base) != SUCCESS)) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + -1, + "cuda_decoder_prompt_batch_prefill_query_rope_options_invalid" + ); + return FAILURE; + } + if (query_rope_op != NULL) { + rope_pairing = king_inference_tensor_option_ulong(query_rope_op, "pairing", 0); + } + + query_rope_bytes = (size_t) prefill_token_count * (size_t) query_rows * sizeof(float); + if (king_inference_cuda_device_allocator_alloc( + model, + query_rope_bytes, + &model->cuda_decoder_prompt_prefill_initial_query_rope + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_prompt_batch_prefill_query_rope_allocation_failed" + ); + return FAILURE; + } + + for (head = 0; head < heads; head++) { + king_inference_cuda_device_ptr output = model->cuda_decoder_prompt_prefill_initial_query_rope + + (king_inference_cuda_device_ptr) ((size_t) head * (size_t) prefill_token_count * (size_t) head_dim * sizeof(float)); + + if (king_inference_cuda_rope_f32_batch_slice( + model, + model->cuda_decoder_prompt_prefill_initial_linear, + output, + query_rows, + head * head_dim, + head_dim, + prefill_token_count, + 0, + rope_position_scale, + rope_base, + rope_pairing + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_rope_result, + model->cuda_rope_error[0] != '\0' + ? model->cuda_rope_error + : "cuda_decoder_prompt_batch_prefill_query_rope_failed" + ); + return FAILURE; + } + } + + model->cuda_decoder_prompt_prefill_initial_query_rope_bytes = query_rope_bytes; + model->cuda_decoder_prompt_prefill_initial_query_rope_heads = heads; + model->cuda_decoder_prompt_prefill_initial_query_rope_head_dim = head_dim; + return SUCCESS; +} diff --git a/extension/src/inference/cuda/prompt/prefill/remaining/cuda_decoder_prompt_prefill_remaining_batch.inc b/extension/src/inference/cuda/prompt/prefill/remaining/cuda_decoder_prompt_prefill_remaining_batch.inc new file mode 100644 index 000000000..83237ee9a --- /dev/null +++ b/extension/src/inference/cuda/prompt/prefill/remaining/cuda_decoder_prompt_prefill_remaining_batch.inc @@ -0,0 +1,762 @@ +/* + * CUDA prompt prefill batch path for layers after the already batched layer 0. + */ + +static zval *king_inference_cuda_decoder_prompt_loop_prefill_op_by_id( + zval *graph, + const char *id +) { + zval *ops = king_inference_cuda_decoder_graph_executor_ops(graph); + zval *entry; + size_t id_length = id != NULL ? strlen(id) : 0; + + if (ops == NULL || id_length == 0) { + return NULL; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(ops), entry) { + zval *op_id; + + if (entry == NULL || Z_TYPE_P(entry) != IS_ARRAY) { + continue; + } + op_id = king_inference_array_find(entry, "id"); + if (op_id != NULL + && Z_TYPE_P(op_id) == IS_STRING + && Z_STRLEN_P(op_id) == id_length + && memcmp(Z_STRVAL_P(op_id), id, id_length) == 0) { + return entry; + } + } ZEND_HASH_FOREACH_END(); + + return NULL; +} + +static zend_result king_inference_cuda_decoder_prompt_loop_prefill_layer_id( + char *buffer, + size_t buffer_size, + const char *format, + zend_ulong layer +) { + int written = snprintf(buffer, buffer_size, format, (unsigned long) layer); + + if (written <= 0 || (size_t) written >= buffer_size) { + return FAILURE; + } + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_prompt_loop_prefill_copy_context_rows_to_stack( + king_inference_model_object *model, + king_inference_cuda_device_ptr context_matrix, + king_inference_cuda_device_ptr stack_matrix, + zend_ulong prefill_token_count, + zend_ulong value_length, + zend_ulong stack_width, + zend_ulong query_head +) { + zend_ulong position; + + for (position = 0; position < prefill_token_count; position++) { + size_t source_offset = (size_t) position * (size_t) value_length; + zend_ulong target_offset = position * stack_width + query_head * value_length; + + if (target_offset > UINT_MAX + || king_inference_cuda_vector_copy_to_offset( + model, + context_matrix + (king_inference_cuda_device_ptr) (source_offset * sizeof(float)), + stack_matrix, + target_offset, + value_length + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_device_vector_ops_result, + model->cuda_device_vector_ops_error[0] != '\0' + ? model->cuda_device_vector_ops_error + : "cuda_decoder_prompt_remaining_prefill_attention_stack_copy_failed" + ); + return FAILURE; + } + } + return SUCCESS; +} + +static void king_inference_cuda_decoder_prompt_loop_prefill_release_layer_buffers( + king_inference_model_object *model, + king_inference_cuda_device_ptr *norm_matrix, + king_inference_cuda_device_ptr *query_matrix, + king_inference_cuda_device_ptr *key_matrix, + king_inference_cuda_device_ptr *value_matrix, + king_inference_cuda_device_ptr *query_rope_matrix, + king_inference_cuda_device_ptr *scores, + king_inference_cuda_device_ptr *probabilities, + king_inference_cuda_device_ptr *contexts, + king_inference_cuda_device_ptr *attention_stack, + king_inference_cuda_device_ptr *projection_matrix, + king_inference_cuda_device_ptr *attention_residual, + king_inference_cuda_device_ptr *ffn_norm, + king_inference_cuda_device_ptr *ffn_gate, + king_inference_cuda_device_ptr *ffn_up, + king_inference_cuda_device_ptr *ffn_swiglu, + king_inference_cuda_device_ptr *ffn_down +) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, ffn_down); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, ffn_swiglu); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, ffn_up); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, ffn_gate); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, ffn_norm); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, attention_residual); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, projection_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, attention_stack); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, contexts); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, probabilities); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, scores); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, query_rope_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, value_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, key_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, query_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, norm_matrix); +} + +static zend_result king_inference_cuda_decoder_prompt_loop_prefill_remaining_layer_batch( + king_inference_model_object *model, + zval *graph, + zend_ulong layer, + zend_ulong prefill_token_count, + zend_string *current_id, + king_inference_cuda_device_ptr current_matrix, + zend_ulong current_width, + zend_string **next_id_out, + king_inference_cuda_device_ptr *next_matrix_out, + zend_ulong *next_width_out +) { + zval *norm_op; + zval *query_op; + zval *key_op; + zval *value_op; + zval *query_slice_op; + zval *query_rope_op = NULL; + zval *stack_op; + zval *projection_op; + zval *post_attention_norm_op = NULL; + zval *residual_op; + zval *ffn_norm_op; + zval *gate_op; + zval *up_op; + zval *silu_op; + zval *mul_op; + zval *down_op; + zval *post_ffw_norm_op = NULL; + zval *output_op; + zend_string *norm_id = NULL; + zend_string *norm_weight = NULL; + zend_string *query_id = NULL; + zend_string *query_slice_id = NULL; + zend_string *query_weight = NULL; + zend_string *key_weight = NULL; + zend_string *value_weight = NULL; + zend_string *stack_id = NULL; + zend_string *projection_id = NULL; + zend_string *projection_weight = NULL; + zend_string *post_attention_norm_id = NULL; + zend_string *post_attention_norm_weight = NULL; + zend_string *projection_residual_id = NULL; + zend_string *residual_id = NULL; + zend_string *ffn_norm_id = NULL; + zend_string *ffn_norm_weight = NULL; + zend_string *gate_id = NULL; + zend_string *up_id = NULL; + zend_string *gate_weight = NULL; + zend_string *up_weight = NULL; + zend_string *silu_id = NULL; + zend_string *gated_id = NULL; + zend_string *down_id = NULL; + zend_string *down_weight = NULL; + zend_string *post_ffw_norm_id = NULL; + zend_string *post_ffw_norm_weight = NULL; + zend_string *output_right_id = NULL; + zend_string *output_id = NULL; + zend_ulong query_rows = 0; + zend_ulong key_rows = 0; + zend_ulong value_rows = 0; + zend_ulong projection_rows = 0; + zend_ulong gate_rows = 0; + zend_ulong up_rows = 0; + zend_ulong down_rows = 0; + zend_ulong query_heads = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT]; + zend_ulong kv_heads = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT_KV]; + zend_ulong key_length = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_KEY_LENGTH]; + zend_ulong value_length = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_VALUE_LENGTH]; + zend_ulong sliding_window = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_SLIDING_WINDOW]; + zend_ulong max_slots; + zend_ulong stack_width; + zend_ulong query_head; + zend_ulong kv_head; + double epsilon = 0.000001; + double ffn_epsilon = 0.000001; + double post_attention_norm_epsilon = 0.000001; + double post_ffw_norm_epsilon = 0.000001; + double rope_base = 10000.0; + double rope_position_scale = 1.0; + zend_ulong rope_pairing = 0; + zend_ulong ffn_activation = king_inference_decode_graph_gemma_profile(model) ? 1 : 0; + double scale; + char stack_id_buffer[64]; + king_inference_cuda_device_ptr norm_matrix = 0; + king_inference_cuda_device_ptr query_matrix = 0; + king_inference_cuda_device_ptr key_matrix = 0; + king_inference_cuda_device_ptr value_matrix = 0; + king_inference_cuda_device_ptr query_rope_matrix = 0; + king_inference_cuda_device_ptr key_rope_matrix = 0; + king_inference_cuda_device_ptr scores = 0; + king_inference_cuda_device_ptr probabilities = 0; + king_inference_cuda_device_ptr contexts = 0; + king_inference_cuda_device_ptr attention_stack = 0; + king_inference_cuda_device_ptr projection_matrix = 0; + king_inference_cuda_device_ptr post_attention_norm_matrix = 0; + king_inference_cuda_device_ptr projection_residual_matrix = 0; + king_inference_cuda_device_ptr attention_residual = 0; + king_inference_cuda_device_ptr ffn_norm = 0; + king_inference_cuda_device_ptr ffn_gate = 0; + king_inference_cuda_device_ptr ffn_up = 0; + king_inference_cuda_device_ptr ffn_swiglu = 0; + king_inference_cuda_device_ptr ffn_down = 0; + king_inference_cuda_device_ptr post_ffw_norm_matrix = 0; + king_inference_cuda_device_ptr ffn_output_right_matrix = 0; + king_inference_cuda_device_ptr ffn_output = 0; + size_t current_elements; + size_t q_elements; + size_t k_elements; + size_t v_elements; + size_t query_rope_elements; + size_t score_elements; + size_t context_elements; + size_t stack_elements; + size_t projection_elements; + size_t gate_elements; + size_t up_elements; + size_t swiglu_elements; + size_t down_elements; + + *next_id_out = NULL; + *next_matrix_out = 0; + *next_width_out = 0; + if (graph == NULL + || current_id == NULL + || current_matrix == 0 + || current_width == 0 + || prefill_token_count == 0 + || prefill_token_count > (zend_ulong) (SIZE_MAX / (size_t) current_width)) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_remaining_prefill_input_invalid"); + return FAILURE; + } + king_inference_attention_refine_runtime_shape_from_tensors_for_layer( + model, + layer, + &query_heads, + &kv_heads, + &key_length, + &value_length + ); + if (kv_heads == 0) { + kv_heads = query_heads; + } + max_slots = sliding_window > 0 && sliding_window < prefill_token_count ? sliding_window : prefill_token_count; + if (query_heads == 0 + || kv_heads == 0 + || query_heads < kv_heads + || key_length == 0 + || value_length == 0 + || max_slots == 0 + || query_heads > (zend_ulong) (SIZE_MAX / (size_t) value_length)) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_remaining_prefill_attention_shape_invalid"); + return FAILURE; + } + + norm_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, current_id, "rms_norm"); + if (norm_op == NULL + || king_inference_graph_required_string(norm_op, "id", &norm_id) != SUCCESS + || king_inference_graph_required_string(norm_op, "weight", &norm_weight) != SUCCESS + || king_inference_graph_finite_double_option(norm_op, "epsilon", 0.000001, &epsilon) != SUCCESS + || epsilon <= 0.0) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_remaining_prefill_norm_contract_invalid"); + return FAILURE; + } + query_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, norm_id, "linear", 0); + key_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, norm_id, "linear", 1); + value_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, norm_id, "linear", 2); + if (query_op == NULL + || key_op == NULL + || value_op == NULL + || king_inference_graph_required_string(query_op, "id", &query_id) != SUCCESS + || king_inference_cuda_decoder_graph_executor_linear_shape(model, query_op, current_width, &query_weight, &query_rows) != SUCCESS + || king_inference_cuda_decoder_graph_executor_linear_shape(model, key_op, current_width, &key_weight, &key_rows) != SUCCESS + || king_inference_cuda_decoder_graph_executor_linear_shape(model, value_op, current_width, &value_weight, &value_rows) != SUCCESS + || query_rows != query_heads * key_length + || key_rows < kv_heads * key_length + || value_rows < kv_heads * value_length) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_remaining_prefill_qkv_contract_invalid"); + return FAILURE; + } + query_slice_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, query_id, "slice"); + if (query_slice_op != NULL + && king_inference_graph_required_string(query_slice_op, "id", &query_slice_id) == SUCCESS) { + query_rope_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, query_slice_id, "rope"); + } + if (query_rope_op != NULL + && (king_inference_graph_finite_double_option(query_rope_op, "position_scale", 1.0, &rope_position_scale) != SUCCESS + || king_inference_graph_finite_double_option(query_rope_op, "rope_base", 10000.0, &rope_base) != SUCCESS)) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_remaining_prefill_rope_options_invalid"); + return FAILURE; + } + if (query_rope_op != NULL) { + rope_pairing = king_inference_tensor_option_ulong(query_rope_op, "pairing", 0); + } + if (king_inference_cuda_decoder_prompt_loop_prefill_layer_id( + stack_id_buffer, + sizeof(stack_id_buffer), + "l%lu_context", + layer + ) != SUCCESS + || (stack_op = king_inference_cuda_decoder_prompt_loop_prefill_op_by_id(graph, stack_id_buffer)) == NULL + || king_inference_graph_required_string(stack_op, "id", &stack_id) != SUCCESS + || (projection_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, stack_id, "linear")) == NULL + || king_inference_graph_required_string(projection_op, "id", &projection_id) != SUCCESS + || king_inference_cuda_decoder_graph_executor_linear_shape( + model, + projection_op, + query_heads * value_length, + &projection_weight, + &projection_rows + ) != SUCCESS + || projection_rows != current_width) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_remaining_prefill_attention_contract_invalid"); + return FAILURE; + } + projection_residual_id = projection_id; + post_attention_norm_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, projection_id, "rms_norm"); + if (post_attention_norm_op != NULL) { + if (king_inference_graph_required_string(post_attention_norm_op, "id", &post_attention_norm_id) != SUCCESS + || king_inference_graph_required_string(post_attention_norm_op, "weight", &post_attention_norm_weight) != SUCCESS + || king_inference_graph_finite_double_option(post_attention_norm_op, "epsilon", 0.000001, &post_attention_norm_epsilon) != SUCCESS + || post_attention_norm_epsilon <= 0.0) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_remaining_prefill_post_attention_norm_contract_invalid"); + return FAILURE; + } + projection_residual_id = post_attention_norm_id; + } + residual_op = king_inference_cuda_decoder_graph_executor_add_op_for_inputs(graph, current_id, projection_residual_id); + if (residual_op == NULL + || king_inference_graph_required_string(residual_op, "id", &residual_id) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_remaining_prefill_attention_contract_invalid"); + return FAILURE; + } + ffn_norm_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, residual_id, "rms_norm"); + if (ffn_norm_op == NULL + || king_inference_graph_required_string(ffn_norm_op, "id", &ffn_norm_id) != SUCCESS + || king_inference_graph_required_string(ffn_norm_op, "weight", &ffn_norm_weight) != SUCCESS + || king_inference_graph_finite_double_option(ffn_norm_op, "epsilon", epsilon, &ffn_epsilon) != SUCCESS + || ffn_epsilon <= 0.0) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_remaining_prefill_ffn_norm_contract_invalid"); + return FAILURE; + } + gate_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, ffn_norm_id, "linear", 0); + up_op = king_inference_cuda_decoder_graph_executor_nth_op_for_input(graph, ffn_norm_id, "linear", 1); + if (gate_op == NULL + || up_op == NULL + || king_inference_graph_required_string(gate_op, "id", &gate_id) != SUCCESS + || king_inference_graph_required_string(up_op, "id", &up_id) != SUCCESS + || king_inference_cuda_decoder_graph_executor_linear_shape(model, gate_op, current_width, &gate_weight, &gate_rows) != SUCCESS + || king_inference_cuda_decoder_graph_executor_linear_shape(model, up_op, current_width, &up_weight, &up_rows) != SUCCESS + || gate_rows == 0 + || gate_rows != up_rows) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_remaining_prefill_gate_up_contract_invalid"); + return FAILURE; + } + silu_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, gate_id, "silu"); + if (silu_op == NULL) { + silu_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, gate_id, "gelu_tanh"); + } + if (silu_op == NULL + || king_inference_graph_required_string(silu_op, "id", &silu_id) != SUCCESS + || (mul_op = king_inference_cuda_decoder_graph_executor_mul_op_for_inputs(graph, silu_id, up_id)) == NULL + || king_inference_graph_required_string(mul_op, "id", &gated_id) != SUCCESS + || (down_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, gated_id, "linear")) == NULL + || king_inference_graph_required_string(down_op, "id", &down_id) != SUCCESS + || king_inference_cuda_decoder_graph_executor_linear_shape(model, down_op, gate_rows, &down_weight, &down_rows) != SUCCESS + || down_rows != current_width) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_remaining_prefill_ffn_output_contract_invalid"); + return FAILURE; + } + output_right_id = down_id; + post_ffw_norm_op = king_inference_cuda_decoder_graph_executor_op_for_input(graph, down_id, "rms_norm"); + if (post_ffw_norm_op != NULL) { + if (king_inference_graph_required_string(post_ffw_norm_op, "id", &post_ffw_norm_id) != SUCCESS + || king_inference_graph_required_string(post_ffw_norm_op, "weight", &post_ffw_norm_weight) != SUCCESS + || king_inference_graph_finite_double_option(post_ffw_norm_op, "epsilon", 0.000001, &post_ffw_norm_epsilon) != SUCCESS + || post_ffw_norm_epsilon <= 0.0) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_remaining_prefill_post_ffw_norm_contract_invalid"); + return FAILURE; + } + output_right_id = post_ffw_norm_id; + } + output_op = king_inference_cuda_decoder_graph_executor_add_op_for_inputs(graph, residual_id, output_right_id); + if (output_op == NULL + || king_inference_graph_required_string(output_op, "id", &output_id) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_remaining_prefill_ffn_output_contract_invalid"); + return FAILURE; + } + + stack_width = query_heads * value_length; + current_elements = (size_t) prefill_token_count * (size_t) current_width; + q_elements = (size_t) prefill_token_count * (size_t) query_rows; + k_elements = (size_t) prefill_token_count * (size_t) key_rows; + v_elements = (size_t) prefill_token_count * (size_t) value_rows; + query_rope_elements = q_elements; + score_elements = (size_t) prefill_token_count * (size_t) max_slots; + context_elements = (size_t) prefill_token_count * (size_t) value_length; + stack_elements = (size_t) prefill_token_count * (size_t) stack_width; + projection_elements = current_elements; + gate_elements = (size_t) prefill_token_count * (size_t) gate_rows; + up_elements = (size_t) prefill_token_count * (size_t) up_rows; + swiglu_elements = gate_elements; + down_elements = current_elements; + if (current_elements > SIZE_MAX / sizeof(float) + || q_elements > SIZE_MAX / sizeof(float) + || k_elements > SIZE_MAX / sizeof(float) + || v_elements > SIZE_MAX / sizeof(float) + || query_rope_elements > SIZE_MAX / sizeof(float) + || score_elements > SIZE_MAX / sizeof(float) + || context_elements > SIZE_MAX / sizeof(float) + || stack_elements > SIZE_MAX / sizeof(float) + || projection_elements > SIZE_MAX / sizeof(float) + || gate_elements > SIZE_MAX / sizeof(float) + || up_elements > SIZE_MAX / sizeof(float) + || swiglu_elements > SIZE_MAX / sizeof(float) + || down_elements > SIZE_MAX / sizeof(float) + || current_elements > UINT_MAX + || gate_elements > UINT_MAX) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_remaining_prefill_size_overflow"); + return FAILURE; + } + + if (king_inference_cuda_device_allocator_alloc(model, current_elements * sizeof(float), &norm_matrix) != SUCCESS + || king_inference_cuda_device_allocator_alloc(model, q_elements * sizeof(float), &query_matrix) != SUCCESS + || king_inference_cuda_device_allocator_alloc(model, k_elements * sizeof(float), &key_matrix) != SUCCESS + || king_inference_cuda_device_allocator_alloc(model, v_elements * sizeof(float), &value_matrix) != SUCCESS + || king_inference_cuda_device_allocator_alloc(model, query_rope_elements * sizeof(float), &query_rope_matrix) != SUCCESS + || king_inference_cuda_device_allocator_alloc(model, score_elements * sizeof(float), &scores) != SUCCESS + || king_inference_cuda_device_allocator_alloc(model, score_elements * sizeof(float), &probabilities) != SUCCESS + || king_inference_cuda_device_allocator_alloc(model, context_elements * sizeof(float), &contexts) != SUCCESS + || king_inference_cuda_device_allocator_alloc(model, stack_elements * sizeof(float), &attention_stack) != SUCCESS + || king_inference_cuda_device_allocator_alloc(model, projection_elements * sizeof(float), &projection_matrix) != SUCCESS + || king_inference_cuda_device_allocator_alloc(model, projection_elements * sizeof(float), &attention_residual) != SUCCESS + || king_inference_cuda_device_allocator_alloc(model, projection_elements * sizeof(float), &ffn_norm) != SUCCESS + || king_inference_cuda_device_allocator_alloc(model, gate_elements * sizeof(float), &ffn_gate) != SUCCESS + || king_inference_cuda_device_allocator_alloc(model, up_elements * sizeof(float), &ffn_up) != SUCCESS + || king_inference_cuda_device_allocator_alloc(model, swiglu_elements * sizeof(float), &ffn_swiglu) != SUCCESS + || king_inference_cuda_device_allocator_alloc(model, down_elements * sizeof(float), &ffn_down) != SUCCESS + || king_inference_cuda_device_allocator_alloc(model, down_elements * sizeof(float), &ffn_output) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_prefill_release_layer_buffers( + model, + &norm_matrix, + &query_matrix, + &key_matrix, + &value_matrix, + &query_rope_matrix, + &scores, + &probabilities, + &contexts, + &attention_stack, + &projection_matrix, + &attention_residual, + &ffn_norm, + &ffn_gate, + &ffn_up, + &ffn_swiglu, + &ffn_down + ); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_output); + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_decoder_prompt_remaining_prefill_allocation_failed" + ); + return FAILURE; + } + + if (king_inference_cuda_rms_norm_f32_batch(model, norm_weight, current_matrix, norm_matrix, current_width, prefill_token_count, epsilon) != SUCCESS + || king_inference_cuda_quantized_matvec_batch(model, query_weight, norm_matrix, current_width, prefill_token_count, query_matrix, query_rows) != SUCCESS + || king_inference_cuda_quantized_matvec_batch(model, key_weight, norm_matrix, current_width, prefill_token_count, key_matrix, key_rows) != SUCCESS + || king_inference_cuda_quantized_matvec_batch(model, value_weight, norm_matrix, current_width, prefill_token_count, value_matrix, value_rows) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_prefill_release_layer_buffers(model, &norm_matrix, &query_matrix, &key_matrix, &value_matrix, &query_rope_matrix, &scores, &probabilities, &contexts, &attention_stack, &projection_matrix, &attention_residual, &ffn_norm, &ffn_gate, &ffn_up, &ffn_swiglu, &ffn_down); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_output); + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_rms_norm_result != 0 ? model->cuda_rms_norm_result : model->cuda_quantized_matvec_result, + model->cuda_rms_norm_error[0] != '\0' + ? model->cuda_rms_norm_error + : (model->cuda_quantized_matvec_error[0] != '\0' + ? model->cuda_quantized_matvec_error + : "cuda_decoder_prompt_remaining_prefill_qkv_failed") + ); + return FAILURE; + } + + for (query_head = 0; query_head < query_heads; query_head++) { + king_inference_cuda_device_ptr query_output = query_rope_matrix + + (king_inference_cuda_device_ptr) ((size_t) query_head * (size_t) prefill_token_count * (size_t) key_length * sizeof(float)); + + if (king_inference_cuda_rope_f32_batch_slice( + model, + query_matrix, + query_output, + query_rows, + query_head * key_length, + key_length, + prefill_token_count, + 0, + rope_position_scale, + rope_base, + rope_pairing + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_prefill_release_layer_buffers(model, &norm_matrix, &query_matrix, &key_matrix, &value_matrix, &query_rope_matrix, &scores, &probabilities, &contexts, &attention_stack, &projection_matrix, &attention_residual, &ffn_norm, &ffn_gate, &ffn_up, &ffn_swiglu, &ffn_down); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_output); + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_rope_result, + model->cuda_rope_error[0] != '\0' + ? model->cuda_rope_error + : "cuda_decoder_prompt_remaining_prefill_query_rope_failed" + ); + return FAILURE; + } + } + + for (kv_head = 0; kv_head < kv_heads; kv_head++) { + if (king_inference_cuda_device_allocator_alloc( + model, + (size_t) prefill_token_count * (size_t) key_length * sizeof(float), + &key_rope_matrix + ) != SUCCESS + || king_inference_cuda_rope_f32_batch_slice( + model, + key_matrix, + key_rope_matrix, + key_rows, + kv_head * key_length, + key_length, + prefill_token_count, + 0, + rope_position_scale, + rope_base, + rope_pairing + ) != SUCCESS + || king_inference_cuda_device_kv_cache_write_batch_strided( + model, + layer, + kv_head, + 0, + prefill_token_count, + key_rope_matrix, + key_length, + 0, + key_length, + value_matrix, + value_rows, + kv_head * value_length, + value_length + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_rope_matrix); + king_inference_cuda_decoder_prompt_loop_prefill_release_layer_buffers(model, &norm_matrix, &query_matrix, &key_matrix, &value_matrix, &query_rope_matrix, &scores, &probabilities, &contexts, &attention_stack, &projection_matrix, &attention_residual, &ffn_norm, &ffn_gate, &ffn_up, &ffn_swiglu, &ffn_down); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_output); + king_inference_cuda_decoder_prompt_loop_set_error( + model, + model->cuda_rope_result != 0 ? model->cuda_rope_result : model->cuda_device_kv_cache_result, + model->cuda_rope_error[0] != '\0' + ? model->cuda_rope_error + : (model->cuda_device_kv_cache_error[0] != '\0' + ? model->cuda_device_kv_cache_error + : "cuda_decoder_prompt_remaining_prefill_kv_write_failed") + ); + return FAILURE; + } + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &key_rope_matrix); + model->cuda_decoder_prompt_prefill_kv_cache_writes++; + } + + scale = 1.0 / sqrt((double) key_length); + for (query_head = 0; query_head < query_heads; query_head++) { + king_inference_cuda_device_ptr key_cache = 0; + king_inference_cuda_device_ptr value_cache = 0; + king_inference_cuda_device_ptr query_base = query_rope_matrix + + (king_inference_cuda_device_ptr) ((size_t) query_head * (size_t) prefill_token_count * (size_t) key_length * sizeof(float)); + zend_ulong key_stride = 0; + zend_ulong value_stride = 0; + zend_ulong mapped_kv_head = king_inference_cuda_decoder_prompt_loop_prefill_kv_head_for_query( + query_head, + query_heads, + kv_heads + ); + + if (king_inference_cuda_device_kv_cache_head(model, false, layer, mapped_kv_head, &key_cache, &key_stride) != SUCCESS + || king_inference_cuda_device_kv_cache_head(model, true, layer, mapped_kv_head, &value_cache, &value_stride) != SUCCESS + || king_inference_cuda_attention_scores_f32_batch(model, query_base, key_cache, scores, key_length, key_length, key_stride, 0, max_slots, prefill_token_count, sliding_window, scale) != SUCCESS + || king_inference_cuda_attention_softmax_f32_batch(model, scores, probabilities, 0, 0, max_slots, prefill_token_count, sliding_window, 1.0, 1.0) != SUCCESS + || king_inference_cuda_attention_values_f32_batch(model, probabilities, value_cache, contexts, value_length, value_stride, 0, max_slots, prefill_token_count, sliding_window) != SUCCESS + || king_inference_cuda_decoder_prompt_loop_prefill_copy_context_rows_to_stack( + model, + contexts, + attention_stack, + prefill_token_count, + value_length, + stack_width, + query_head + ) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_prefill_release_layer_buffers(model, &norm_matrix, &query_matrix, &key_matrix, &value_matrix, &query_rope_matrix, &scores, &probabilities, &contexts, &attention_stack, &projection_matrix, &attention_residual, &ffn_norm, &ffn_gate, &ffn_up, &ffn_swiglu, &ffn_down); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_output); + return FAILURE; + } + } + + if (king_inference_cuda_quantized_matvec_batch(model, projection_weight, attention_stack, stack_width, prefill_token_count, projection_matrix, projection_rows) != SUCCESS) { + king_inference_cuda_decoder_prompt_loop_prefill_release_layer_buffers(model, &norm_matrix, &query_matrix, &key_matrix, &value_matrix, &query_rope_matrix, &scores, &probabilities, &contexts, &attention_stack, &projection_matrix, &attention_residual, &ffn_norm, &ffn_gate, &ffn_up, &ffn_swiglu, &ffn_down); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_output); + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_remaining_prefill_projection_failed"); + return FAILURE; + } + projection_residual_matrix = projection_matrix; + if (post_attention_norm_op != NULL) { + if (king_inference_cuda_device_allocator_alloc(model, projection_elements * sizeof(float), &post_attention_norm_matrix) != SUCCESS + || king_inference_cuda_rms_norm_f32_batch( + model, + post_attention_norm_weight, + projection_matrix, + post_attention_norm_matrix, + current_width, + prefill_token_count, + post_attention_norm_epsilon + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_matrix); + king_inference_cuda_decoder_prompt_loop_prefill_release_layer_buffers(model, &norm_matrix, &query_matrix, &key_matrix, &value_matrix, &query_rope_matrix, &scores, &probabilities, &contexts, &attention_stack, &projection_matrix, &attention_residual, &ffn_norm, &ffn_gate, &ffn_up, &ffn_swiglu, &ffn_down); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_output); + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_remaining_prefill_post_attention_norm_failed"); + return FAILURE; + } + projection_residual_matrix = post_attention_norm_matrix; + } + if (king_inference_cuda_vector_add(model, current_matrix, projection_residual_matrix, attention_residual, (zend_ulong) projection_elements) != SUCCESS + || king_inference_cuda_rms_norm_f32_batch(model, ffn_norm_weight, attention_residual, ffn_norm, current_width, prefill_token_count, ffn_epsilon) != SUCCESS + || king_inference_cuda_quantized_matvec_batch(model, gate_weight, ffn_norm, current_width, prefill_token_count, ffn_gate, gate_rows) != SUCCESS + || king_inference_cuda_quantized_matvec_batch(model, up_weight, ffn_norm, current_width, prefill_token_count, ffn_up, up_rows) != SUCCESS + || king_inference_cuda_ffn_swiglu_f32(model, ffn_gate, ffn_up, ffn_swiglu, (zend_ulong) swiglu_elements, ffn_activation) != SUCCESS + || king_inference_cuda_quantized_matvec_batch(model, down_weight, ffn_swiglu, gate_rows, prefill_token_count, ffn_down, down_rows) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_matrix); + king_inference_cuda_decoder_prompt_loop_prefill_release_layer_buffers(model, &norm_matrix, &query_matrix, &key_matrix, &value_matrix, &query_rope_matrix, &scores, &probabilities, &contexts, &attention_stack, &projection_matrix, &attention_residual, &ffn_norm, &ffn_gate, &ffn_up, &ffn_swiglu, &ffn_down); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_output); + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_remaining_prefill_tail_failed"); + return FAILURE; + } + ffn_output_right_matrix = ffn_down; + if (post_ffw_norm_op != NULL) { + if (king_inference_cuda_device_allocator_alloc(model, down_elements * sizeof(float), &post_ffw_norm_matrix) != SUCCESS + || king_inference_cuda_rms_norm_f32_batch( + model, + post_ffw_norm_weight, + ffn_down, + post_ffw_norm_matrix, + current_width, + prefill_token_count, + post_ffw_norm_epsilon + ) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_ffw_norm_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_matrix); + king_inference_cuda_decoder_prompt_loop_prefill_release_layer_buffers(model, &norm_matrix, &query_matrix, &key_matrix, &value_matrix, &query_rope_matrix, &scores, &probabilities, &contexts, &attention_stack, &projection_matrix, &attention_residual, &ffn_norm, &ffn_gate, &ffn_up, &ffn_swiglu, &ffn_down); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_output); + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_remaining_prefill_post_ffw_norm_failed"); + return FAILURE; + } + ffn_output_right_matrix = post_ffw_norm_matrix; + } + if (king_inference_cuda_vector_add(model, attention_residual, ffn_output_right_matrix, ffn_output, (zend_ulong) down_elements) != SUCCESS) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_ffw_norm_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_matrix); + king_inference_cuda_decoder_prompt_loop_prefill_release_layer_buffers(model, &norm_matrix, &query_matrix, &key_matrix, &value_matrix, &query_rope_matrix, &scores, &probabilities, &contexts, &attention_stack, &projection_matrix, &attention_residual, &ffn_norm, &ffn_gate, &ffn_up, &ffn_swiglu, &ffn_down); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &ffn_output); + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_remaining_prefill_output_failed"); + return FAILURE; + } + + *next_id_out = zend_string_copy(output_id); + *next_matrix_out = ffn_output; + *next_width_out = current_width; + ffn_output = 0; + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_ffw_norm_matrix); + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, &post_attention_norm_matrix); + king_inference_cuda_decoder_prompt_loop_prefill_release_layer_buffers(model, &norm_matrix, &query_matrix, &key_matrix, &value_matrix, &query_rope_matrix, &scores, &probabilities, &contexts, &attention_stack, &projection_matrix, &attention_residual, &ffn_norm, &ffn_gate, &ffn_up, &ffn_swiglu, &ffn_down); + return SUCCESS; +} + +static zend_result king_inference_cuda_decoder_prompt_loop_prefill_remaining_layers_batch( + king_inference_model_object *model, + zval *graph, + zend_ulong prefill_token_count, + zend_ulong width +) { + zend_ulong layer; + zend_ulong layers = model->gguf.architecture_params[KING_INFERENCE_ARCH_BLOCK_COUNT]; + zend_string *current_id; + king_inference_cuda_device_ptr current_matrix = model->cuda_decoder_prompt_prefill_layer0_ffn_output_residual; + zend_ulong current_width = model->cuda_decoder_prompt_prefill_layer0_ffn_output_residual_width; + bool current_owned = false; + + if (layers <= 1) { + return SUCCESS; + } + if (current_matrix == 0 || current_width == 0 || current_width != width) { + king_inference_cuda_decoder_prompt_loop_set_error(model, -1, "cuda_decoder_prompt_remaining_prefill_start_invalid"); + return FAILURE; + } + current_id = zend_string_init("l0_output", sizeof("l0_output") - 1, false); + for (layer = 1; layer < layers; layer++) { + zend_string *next_id = NULL; + king_inference_cuda_device_ptr next_matrix = 0; + zend_ulong next_width = 0; + + if (king_inference_cuda_decoder_prompt_loop_prefill_remaining_layer_batch( + model, + graph, + layer, + prefill_token_count, + current_id, + current_matrix, + current_width, + &next_id, + &next_matrix, + &next_width + ) != SUCCESS) { + if (current_owned) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, ¤t_matrix); + } + zend_string_release(current_id); + return FAILURE; + } + if (current_owned) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, ¤t_matrix); + } + zend_string_release(current_id); + current_id = next_id; + current_matrix = next_matrix; + current_width = next_width; + current_owned = true; + } + + if (current_owned) { + (void) king_inference_cuda_decoder_graph_executor_release_device_ptr(model, ¤t_matrix); + } + zend_string_release(current_id); + return SUCCESS; +} diff --git a/extension/src/inference/cuda/runtime/context/cuda_context.inc b/extension/src/inference/cuda/runtime/context/cuda_context.inc new file mode 100644 index 000000000..ff2bb1f59 --- /dev/null +++ b/extension/src/inference/cuda/runtime/context/cuda_context.inc @@ -0,0 +1,251 @@ +/* + * CUDA context ownership for native King inference. + */ + +typedef int (*king_inference_cuda_context_cu_init_fn)(unsigned int flags); +typedef int (*king_inference_cuda_context_cu_device_get_count_fn)(int *count); +typedef int (*king_inference_cuda_context_cu_device_get_fn)(int *device, int ordinal); +typedef int (*king_inference_cuda_context_cu_device_primary_ctx_retain_fn)(void **ctx, int device); +typedef int (*king_inference_cuda_context_cu_device_primary_ctx_release_fn)(int device); +typedef int (*king_inference_cuda_context_cu_ctx_set_current_fn)(void *ctx); + +static void king_inference_cuda_context_reset_fields(king_inference_model_object *model) +{ + model->cuda_driver_handle = NULL; + model->cuda_context = NULL; + model->cuda_device = -1; + model->cuda_context_result = 0; + model->cuda_context_error[0] = '\0'; + model->cuda_context_attempted = false; + model->cuda_context_available = false; + model->cuda_context_owned = false; +} + +static void king_inference_cuda_context_set_error( + king_inference_model_object *model, + int result, + const char *message +) { + if (message == NULL || message[0] == '\0') { + message = "unknown_cuda_context_error"; + } + + model->cuda_context_result = result; + snprintf(model->cuda_context_error, sizeof(model->cuda_context_error), "%s", message); +} + +static bool king_inference_cuda_context_symbol( + king_inference_model_object *model, + void *handle, + const char *name, + void **symbol +) { + *symbol = dlsym(handle, name); + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_context_set_error(model, -1, name); + return false; +} + +static void king_inference_cuda_context_release(king_inference_model_object *model) +{ + king_inference_cuda_context_cu_device_primary_ctx_release_fn cu_device_primary_ctx_release; + + if (model->cuda_driver_handle != NULL && model->cuda_context_owned && model->cuda_device >= 0) { + cu_device_primary_ctx_release = (king_inference_cuda_context_cu_device_primary_ctx_release_fn) + dlsym(model->cuda_driver_handle, "cuDevicePrimaryCtxRelease"); + if (cu_device_primary_ctx_release != NULL) { + (void) cu_device_primary_ctx_release(model->cuda_device); + } + } + + if (model->cuda_driver_handle != NULL) { + dlclose(model->cuda_driver_handle); + } + + king_inference_cuda_context_reset_fields(model); +} + +static void king_inference_cuda_context_open( + king_inference_model_object *model, + king_inference_backend_kind backend_kind +) { + king_inference_cuda_context_cu_init_fn cu_init; + king_inference_cuda_context_cu_device_get_count_fn cu_device_get_count; + king_inference_cuda_context_cu_device_get_fn cu_device_get; + king_inference_cuda_context_cu_device_primary_ctx_retain_fn cu_device_primary_ctx_retain; + king_inference_cuda_context_cu_ctx_set_current_fn cu_ctx_set_current; + void *handle; + void *ctx = NULL; + int device_count = 0; + int device = 0; + int result; + + king_inference_cuda_context_release(model); + if (backend_kind != KING_INFERENCE_BACKEND_KING_NATIVE_GPU + || !king_inference_gpu_enabled(&model->config)) { + return; + } + + model->cuda_context_attempted = true; + + handle = dlopen("libcuda.so.1", RTLD_LAZY | RTLD_LOCAL); + if (handle == NULL) { + king_inference_cuda_context_set_error(model, -1, dlerror()); + return; + } + + if (!king_inference_cuda_context_symbol(model, handle, "cuInit", (void **) &cu_init) + || !king_inference_cuda_context_symbol( + model, + handle, + "cuDeviceGetCount", + (void **) &cu_device_get_count + ) + || !king_inference_cuda_context_symbol(model, handle, "cuDeviceGet", (void **) &cu_device_get) + || !king_inference_cuda_context_symbol( + model, + handle, + "cuDevicePrimaryCtxRetain", + (void **) &cu_device_primary_ctx_retain + ) + || !king_inference_cuda_context_symbol( + model, + handle, + "cuCtxSetCurrent", + (void **) &cu_ctx_set_current + )) { + dlclose(handle); + return; + } + + result = cu_init(0); + if (result != 0) { + dlclose(handle); + king_inference_cuda_context_set_error(model, result, "cuInit_failed"); + return; + } + + result = cu_device_get_count(&device_count); + if (result != 0 || device_count <= 0) { + dlclose(handle); + king_inference_cuda_context_set_error( + model, + result, + result != 0 ? "cuDeviceGetCount_failed" : "cuda_device_unavailable" + ); + return; + } + + result = cu_device_get(&device, 0); + if (result != 0) { + dlclose(handle); + king_inference_cuda_context_set_error(model, result, "cuDeviceGet_failed"); + return; + } + + result = cu_device_primary_ctx_retain(&ctx, device); + if (result != 0 || ctx == NULL) { + dlclose(handle); + king_inference_cuda_context_set_error( + model, + result, + result != 0 ? "cuDevicePrimaryCtxRetain_failed" : "cuda_primary_context_unavailable" + ); + return; + } + + result = cu_ctx_set_current(ctx); + if (result != 0) { + king_inference_cuda_context_cu_device_primary_ctx_release_fn release_fn; + + release_fn = (king_inference_cuda_context_cu_device_primary_ctx_release_fn) + dlsym(handle, "cuDevicePrimaryCtxRelease"); + if (release_fn != NULL) { + (void) release_fn(device); + } + dlclose(handle); + king_inference_cuda_context_set_error(model, result, "cuCtxSetCurrent_failed"); + return; + } + + model->cuda_driver_handle = handle; + model->cuda_context = ctx; + model->cuda_device = device; + model->cuda_context_result = 0; + model->cuda_context_error[0] = '\0'; + model->cuda_context_available = true; + model->cuda_context_owned = true; +} + +static void king_inference_cuda_context_add_status( + king_inference_model_object *model, + zval *return_value +) { + zval cuda_context; + + array_init(&cuda_context); + add_assoc_bool(&cuda_context, "attempted", model->cuda_context_attempted); + add_assoc_bool(&cuda_context, "available", model->cuda_context_available); + add_assoc_bool(&cuda_context, "owned", model->cuda_context_owned); + add_assoc_long(&cuda_context, "device", model->cuda_device); + add_assoc_long(&cuda_context, "result", model->cuda_context_result); + add_assoc_string(&cuda_context, "error", model->cuda_context_error); + add_assoc_zval(return_value, "cuda_context", &cuda_context); +} + +static void king_inference_cuda_context_prepend_return_reason( + zval *return_value, + const char *reason +) { + zval fresh; + zval *reasons; + zval *entry; + + array_init(&fresh); + king_inference_gpu_status_append_reason(&fresh, reason); + + reasons = zend_hash_str_find( + Z_ARRVAL_P(return_value), + "refusal_reasons", + sizeof("refusal_reasons") - 1 + ); + if (reasons != NULL && Z_TYPE_P(reasons) == IS_ARRAY) { + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(reasons), entry) { + if (Z_TYPE_P(entry) == IS_STRING) { + king_inference_gpu_status_append_reason(&fresh, Z_STRVAL_P(entry)); + } + } ZEND_HASH_FOREACH_END(); + } + + add_assoc_zval(return_value, "refusal_reasons", &fresh); +} + +static void king_inference_cuda_context_apply_status( + king_inference_model_object *model, + zval *return_value +) { + zval *reason; + bool reason_can_be_context; + + king_inference_cuda_context_add_status(model, return_value); + if (!model->cuda_context_attempted || model->cuda_context_available) { + return; + } + + add_assoc_bool(return_value, "config_ready", false); + + reason = zend_hash_str_find(Z_ARRVAL_P(return_value), "reason", sizeof("reason") - 1); + reason_can_be_context = reason == NULL + || Z_TYPE_P(reason) != IS_STRING + || zend_string_equals_literal(Z_STR_P(reason), "gpu_ready") + || zend_string_equals_literal(Z_STR_P(reason), "gpu_decoder_kernel_not_implemented"); + if (reason_can_be_context) { + king_inference_cuda_context_prepend_return_reason(return_value, "gpu_cuda_context_unavailable"); + add_assoc_string(return_value, "reason", "gpu_cuda_context_unavailable"); + } else { + king_inference_gpu_status_append_return_reason(return_value, "gpu_cuda_context_unavailable"); + } +} diff --git a/extension/src/inference/cuda/runtime/context/cuda_device_memory.inc b/extension/src/inference/cuda/runtime/context/cuda_device_memory.inc new file mode 100644 index 000000000..4792c8679 --- /dev/null +++ b/extension/src/inference/cuda/runtime/context/cuda_device_memory.inc @@ -0,0 +1,357 @@ +/* + * CUDA device-memory allocation for native King inference. + */ + +struct _king_inference_cuda_device_allocation { + king_inference_cuda_device_ptr device_ptr; + size_t bytes; + bool in_use; + king_inference_cuda_device_allocation *next; +}; + +typedef int (*king_inference_cuda_allocator_cu_mem_alloc_fn)( + king_inference_cuda_device_ptr *device_ptr, + size_t bytes +); +typedef int (*king_inference_cuda_allocator_cu_mem_free_fn)( + king_inference_cuda_device_ptr device_ptr +); +typedef int (*king_inference_cuda_allocator_cu_ctx_set_current_fn)(void *ctx); + +static void king_inference_cuda_device_allocator_reset_fields(king_inference_model_object *model) +{ + model->cuda_device_allocations = NULL; + model->cuda_device_allocator_result = 0; + model->cuda_device_allocator_error[0] = '\0'; + model->cuda_device_bytes_allocated = 0; + model->cuda_device_peak_bytes_allocated = 0; + model->cuda_device_allocation_count = 0; + model->cuda_device_allocator_attempted = false; + model->cuda_device_allocator_symbols_available = false; + model->cuda_device_allocator_available = false; +} + +static void king_inference_cuda_device_allocator_set_error( + king_inference_model_object *model, + int result, + const char *message +) { + if (message == NULL || message[0] == '\0') { + message = "unknown_cuda_device_allocator_error"; + } + + model->cuda_device_allocator_result = result; + snprintf( + model->cuda_device_allocator_error, + sizeof(model->cuda_device_allocator_error), + "%s", + message + ); +} + +static bool king_inference_cuda_device_allocator_symbol( + king_inference_model_object *model, + const char *primary_name, + const char *fallback_name, + void **symbol +) { + *symbol = NULL; + if (model->cuda_driver_handle == NULL) { + king_inference_cuda_device_allocator_set_error(model, -1, "cuda_driver_handle_unavailable"); + return false; + } + + if (primary_name != NULL) { + *symbol = dlsym(model->cuda_driver_handle, primary_name); + } + if (*symbol == NULL && fallback_name != NULL) { + *symbol = dlsym(model->cuda_driver_handle, fallback_name); + } + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_device_allocator_set_error( + model, + -1, + primary_name != NULL ? primary_name : fallback_name + ); + return false; +} + +static bool king_inference_cuda_device_allocator_set_current(king_inference_model_object *model) +{ + king_inference_cuda_allocator_cu_ctx_set_current_fn cu_ctx_set_current; + int result; + + if (model->cuda_context == NULL) { + king_inference_cuda_device_allocator_set_error(model, -1, "cuda_context_unavailable"); + return false; + } + + if (!king_inference_cuda_device_allocator_symbol( + model, + "cuCtxSetCurrent", + NULL, + (void **) &cu_ctx_set_current + )) { + return false; + } + + result = cu_ctx_set_current(model->cuda_context); + if (result != 0) { + king_inference_cuda_device_allocator_set_error(model, result, "cuCtxSetCurrent_failed"); + return false; + } + + return true; +} + +static void king_inference_cuda_device_allocator_release(king_inference_model_object *model) +{ + king_inference_cuda_allocator_cu_mem_free_fn cu_mem_free = NULL; + king_inference_cuda_device_allocation *allocation = model->cuda_device_allocations; + king_inference_cuda_device_allocation *next; + bool can_free_device_memory = allocation == NULL; + + if (allocation != NULL + && model->cuda_driver_handle != NULL + && model->cuda_context != NULL + && king_inference_cuda_device_allocator_set_current(model) + && king_inference_cuda_device_allocator_symbol( + model, + "cuMemFree_v2", + "cuMemFree", + (void **) &cu_mem_free + )) { + can_free_device_memory = true; + } + + while (allocation != NULL) { + next = allocation->next; + if (can_free_device_memory && cu_mem_free != NULL && allocation->device_ptr != 0) { + (void) cu_mem_free(allocation->device_ptr); + } + efree(allocation); + allocation = next; + } + + king_inference_cuda_device_allocator_reset_fields(model); +} + +static void king_inference_cuda_device_allocator_init( + king_inference_model_object *model, + king_inference_backend_kind backend_kind +) { + king_inference_cuda_allocator_cu_mem_alloc_fn cu_mem_alloc; + king_inference_cuda_allocator_cu_mem_free_fn cu_mem_free; + + king_inference_cuda_device_allocator_release(model); + if (backend_kind != KING_INFERENCE_BACKEND_KING_NATIVE_GPU + || !king_inference_gpu_enabled(&model->config)) { + return; + } + + model->cuda_device_allocator_attempted = true; + if (!model->cuda_context_available || model->cuda_driver_handle == NULL) { + king_inference_cuda_device_allocator_set_error(model, -1, "cuda_context_unavailable"); + return; + } + + if (!king_inference_cuda_device_allocator_symbol( + model, + "cuMemAlloc_v2", + "cuMemAlloc", + (void **) &cu_mem_alloc + ) + || !king_inference_cuda_device_allocator_symbol( + model, + "cuMemFree_v2", + "cuMemFree", + (void **) &cu_mem_free + ) + || !king_inference_cuda_device_allocator_set_current(model)) { + return; + } + + model->cuda_device_allocator_symbols_available = true; + model->cuda_device_allocator_available = true; + model->cuda_device_allocator_result = 0; + model->cuda_device_allocator_error[0] = '\0'; +} + +static zend_result king_inference_cuda_device_allocator_alloc( + king_inference_model_object *model, + size_t bytes, + king_inference_cuda_device_ptr *device_ptr +) { + king_inference_cuda_allocator_cu_mem_alloc_fn cu_mem_alloc; + king_inference_cuda_device_allocation *cached; + king_inference_cuda_device_allocation *allocation; + king_inference_cuda_device_ptr allocated = 0; + int result; + + *device_ptr = 0; + if (bytes == 0) { + king_inference_cuda_device_allocator_set_error(model, -1, "cuda_allocation_size_empty"); + return FAILURE; + } + if (!model->cuda_device_allocator_available) { + king_inference_cuda_device_allocator_set_error(model, -1, "cuda_device_allocator_unavailable"); + return FAILURE; + } + if (model->cuda_device_bytes_allocated > ((size_t) -1) - bytes) { + king_inference_cuda_device_allocator_set_error(model, -1, "cuda_allocation_counter_overflow"); + return FAILURE; + } + for (cached = model->cuda_device_allocations; cached != NULL; cached = cached->next) { + if (!cached->in_use && cached->bytes == bytes && cached->device_ptr != 0) { + cached->in_use = true; + model->cuda_device_bytes_allocated += cached->bytes; + model->cuda_device_allocation_count++; + if (model->cuda_device_bytes_allocated > model->cuda_device_peak_bytes_allocated) { + model->cuda_device_peak_bytes_allocated = model->cuda_device_bytes_allocated; + } + + *device_ptr = cached->device_ptr; + model->cuda_device_allocator_result = 0; + model->cuda_device_allocator_error[0] = '\0'; + return SUCCESS; + } + } + if (!king_inference_cuda_device_allocator_set_current(model) + || !king_inference_cuda_device_allocator_symbol( + model, + "cuMemAlloc_v2", + "cuMemAlloc", + (void **) &cu_mem_alloc + )) { + return FAILURE; + } + + allocation = emalloc(sizeof(*allocation)); + + result = cu_mem_alloc(&allocated, bytes); + if (result != 0 || allocated == 0) { + efree(allocation); + king_inference_cuda_device_allocator_set_error( + model, + result, + result != 0 ? "cuMemAlloc_failed" : "cuda_allocation_empty_pointer" + ); + return FAILURE; + } + + allocation->device_ptr = allocated; + allocation->bytes = bytes; + allocation->in_use = true; + allocation->next = model->cuda_device_allocations; + model->cuda_device_allocations = allocation; + model->cuda_device_bytes_allocated += bytes; + model->cuda_device_allocation_count++; + if (model->cuda_device_bytes_allocated > model->cuda_device_peak_bytes_allocated) { + model->cuda_device_peak_bytes_allocated = model->cuda_device_bytes_allocated; + } + + *device_ptr = allocated; + model->cuda_device_allocator_result = 0; + model->cuda_device_allocator_error[0] = '\0'; + return SUCCESS; +} + +static zend_result king_inference_cuda_device_allocator_free( + king_inference_model_object *model, + king_inference_cuda_device_ptr device_ptr +) { + king_inference_cuda_device_allocation **cursor = &model->cuda_device_allocations; + king_inference_cuda_device_allocation *allocation; + + if (device_ptr == 0) { + return SUCCESS; + } + if (!model->cuda_device_allocator_available) { + king_inference_cuda_device_allocator_set_error(model, -1, "cuda_device_allocator_unavailable"); + return FAILURE; + } + + while (*cursor != NULL && (*cursor)->device_ptr != device_ptr) { + cursor = &(*cursor)->next; + } + if (*cursor == NULL) { + king_inference_cuda_device_allocator_set_error(model, -1, "cuda_allocation_not_tracked"); + return FAILURE; + } + + allocation = *cursor; + if (!allocation->in_use) { + king_inference_cuda_device_allocator_set_error(model, -1, "cuda_allocation_already_released"); + return FAILURE; + } + + allocation->in_use = false; + if (model->cuda_device_bytes_allocated >= allocation->bytes) { + model->cuda_device_bytes_allocated -= allocation->bytes; + } else { + model->cuda_device_bytes_allocated = 0; + } + if (model->cuda_device_allocation_count > 0) { + model->cuda_device_allocation_count--; + } + + model->cuda_device_allocator_result = 0; + model->cuda_device_allocator_error[0] = '\0'; + return SUCCESS; +} + +static void king_inference_cuda_device_allocator_add_status( + king_inference_model_object *model, + zval *return_value +) { + zval allocator; + + array_init(&allocator); + add_assoc_bool(&allocator, "attempted", model->cuda_device_allocator_attempted); + add_assoc_bool(&allocator, "symbols_available", model->cuda_device_allocator_symbols_available); + add_assoc_bool(&allocator, "available", model->cuda_device_allocator_available); + add_assoc_long(&allocator, "active_allocation_count", (zend_long) model->cuda_device_allocation_count); + add_assoc_long(&allocator, "active_bytes", (zend_long) model->cuda_device_bytes_allocated); + add_assoc_long(&allocator, "peak_bytes", (zend_long) model->cuda_device_peak_bytes_allocated); + add_assoc_long(&allocator, "result", model->cuda_device_allocator_result); + add_assoc_string(&allocator, "error", model->cuda_device_allocator_error); + add_assoc_zval(return_value, "device_memory_allocator", &allocator); +} + +static void king_inference_cuda_device_allocator_apply_status( + king_inference_model_object *model, + zval *return_value +) { + zval *reason; + bool reason_can_be_allocator; + + king_inference_cuda_device_allocator_add_status(model, return_value); + if (!model->cuda_device_allocator_attempted + || model->cuda_device_allocator_available + || !model->cuda_context_available) { + return; + } + + add_assoc_bool(return_value, "config_ready", false); + + reason = zend_hash_str_find(Z_ARRVAL_P(return_value), "reason", sizeof("reason") - 1); + reason_can_be_allocator = reason == NULL + || Z_TYPE_P(reason) != IS_STRING + || zend_string_equals_literal(Z_STR_P(reason), "gpu_ready") + || zend_string_equals_literal(Z_STR_P(reason), "gpu_decoder_kernel_not_implemented"); + if (reason_can_be_allocator) { + king_inference_cuda_context_prepend_return_reason( + return_value, + "gpu_device_memory_allocator_unavailable" + ); + add_assoc_string(return_value, "reason", "gpu_device_memory_allocator_unavailable"); + } else { + king_inference_gpu_status_append_return_reason( + return_value, + "gpu_device_memory_allocator_unavailable" + ); + } +} diff --git a/extension/src/inference/cuda/runtime/policy/gpu_runtime_kv_admission.inc b/extension/src/inference/cuda/runtime/policy/gpu_runtime_kv_admission.inc new file mode 100644 index 000000000..5f749da1f --- /dev/null +++ b/extension/src/inference/cuda/runtime/policy/gpu_runtime_kv_admission.inc @@ -0,0 +1,317 @@ +/* + * GPU runtime VRAM admission after model residency. + */ + +static void king_inference_gpu_runtime_add_kv_cache_estimate( + zval *return_value, + king_inference_model_object *model +) { + zval *plan; + zval *max_sequence_bytes; + zval *max_context_tokens; + zval *layers; + zval *page_tokens; + zval *element_bytes; + zval *artifact_bytes_value; + zval *free_vram_bytes_value; + zval *free_vram_after_reserve_bytes_value; + zval *free_vram_floor_met_value; + zval *previous_model_vram_admitted; + zval *system_ram_offload_allowed_value; + zval *system_ram_offload_max_bytes_value; + zval *system_ram_offload_min_free_bytes_value; + bool estimate_available = false; + bool compared_to_free = false; + bool fits_free = false; + bool fits_without_reserve = false; + bool free_vram_floor_met = false; + bool overflow = false; + bool previous_model_vram_admitted_bool = false; + bool system_ram_offload_allowed = false; + bool system_ram_offload_limit_configured = false; + bool system_ram_offload_limit_met = false; + bool system_ram_available = false; + bool system_ram_floor_met = false; + bool system_ram_offload_budget_admitted = false; + bool system_ram_offload_required = false; + bool system_ram_offload_supported = false; + bool system_ram_offload_admitted = false; + bool weights_resident = false; + bool kv_cache_resident = false; + const char *runtime_reason = NULL; + const char *runtime_required_source = "artifact_plus_kv_cache"; + zend_long artifact_bytes = 0; + zend_long estimated_bytes = 0; + zend_long free_vram_bytes = 0; + zend_long effective_free_vram_bytes = 0; + zend_long runtime_required_bytes = 0; + zend_long system_ram_available_bytes = 0; + zend_long system_ram_offload_required_bytes = 0; + zend_long system_ram_offload_max_bytes = 0; + zend_long system_ram_offload_min_free_bytes = 0; + + if (!Z_ISUNDEF(model->paged_kv_cache_plan) && Z_TYPE(model->paged_kv_cache_plan) == IS_ARRAY) { + plan = &model->paged_kv_cache_plan; + max_sequence_bytes = zend_hash_str_find( + Z_ARRVAL_P(plan), + "max_sequence_bytes", + sizeof("max_sequence_bytes") - 1 + ); + max_context_tokens = zend_hash_str_find( + Z_ARRVAL_P(plan), + "max_context_tokens", + sizeof("max_context_tokens") - 1 + ); + layers = zend_hash_str_find(Z_ARRVAL_P(plan), "layers", sizeof("layers") - 1); + page_tokens = zend_hash_str_find(Z_ARRVAL_P(plan), "page_tokens", sizeof("page_tokens") - 1); + element_bytes = zend_hash_str_find(Z_ARRVAL_P(plan), "element_bytes", sizeof("element_bytes") - 1); + } else { + plan = NULL; + max_sequence_bytes = NULL; + max_context_tokens = NULL; + layers = NULL; + page_tokens = NULL; + element_bytes = NULL; + } + + if (max_sequence_bytes != NULL && Z_TYPE_P(max_sequence_bytes) == IS_LONG && Z_LVAL_P(max_sequence_bytes) > 0) { + estimate_available = true; + estimated_bytes = Z_LVAL_P(max_sequence_bytes); + } + + artifact_bytes_value = zend_hash_str_find( + Z_ARRVAL_P(return_value), + "artifact_bytes", + sizeof("artifact_bytes") - 1 + ); + free_vram_bytes_value = zend_hash_str_find( + Z_ARRVAL_P(return_value), + "free_vram_bytes", + sizeof("free_vram_bytes") - 1 + ); + free_vram_after_reserve_bytes_value = zend_hash_str_find( + Z_ARRVAL_P(return_value), + "free_vram_after_reserve_bytes", + sizeof("free_vram_after_reserve_bytes") - 1 + ); + free_vram_floor_met_value = zend_hash_str_find( + Z_ARRVAL_P(return_value), + "free_vram_floor_met", + sizeof("free_vram_floor_met") - 1 + ); + previous_model_vram_admitted = zend_hash_str_find( + Z_ARRVAL_P(return_value), + "model_vram_admitted", + sizeof("model_vram_admitted") - 1 + ); + system_ram_offload_allowed_value = zend_hash_str_find( + Z_ARRVAL_P(return_value), + "system_ram_offload_allowed", + sizeof("system_ram_offload_allowed") - 1 + ); + system_ram_offload_max_bytes_value = zend_hash_str_find( + Z_ARRVAL_P(return_value), + "system_ram_offload_max_bytes", + sizeof("system_ram_offload_max_bytes") - 1 + ); + system_ram_offload_min_free_bytes_value = zend_hash_str_find( + Z_ARRVAL_P(return_value), + "system_ram_offload_min_free_bytes", + sizeof("system_ram_offload_min_free_bytes") - 1 + ); + if (artifact_bytes_value != NULL && Z_TYPE_P(artifact_bytes_value) == IS_LONG && Z_LVAL_P(artifact_bytes_value) > 0) { + artifact_bytes = Z_LVAL_P(artifact_bytes_value); + } + if (free_vram_bytes_value != NULL && Z_TYPE_P(free_vram_bytes_value) == IS_LONG && Z_LVAL_P(free_vram_bytes_value) > 0) { + free_vram_bytes = Z_LVAL_P(free_vram_bytes_value); + } + if (free_vram_after_reserve_bytes_value != NULL + && Z_TYPE_P(free_vram_after_reserve_bytes_value) == IS_LONG + && Z_LVAL_P(free_vram_after_reserve_bytes_value) > 0) { + effective_free_vram_bytes = Z_LVAL_P(free_vram_after_reserve_bytes_value); + } else { + effective_free_vram_bytes = free_vram_bytes; + } + previous_model_vram_admitted_bool = previous_model_vram_admitted != NULL + && zend_is_true(previous_model_vram_admitted); + system_ram_offload_allowed = system_ram_offload_allowed_value != NULL + && zend_is_true(system_ram_offload_allowed_value); + if (system_ram_offload_max_bytes_value != NULL + && Z_TYPE_P(system_ram_offload_max_bytes_value) == IS_LONG + && Z_LVAL_P(system_ram_offload_max_bytes_value) > 0) { + system_ram_offload_max_bytes = Z_LVAL_P(system_ram_offload_max_bytes_value); + } + if (system_ram_offload_min_free_bytes_value != NULL + && Z_TYPE_P(system_ram_offload_min_free_bytes_value) == IS_LONG + && Z_LVAL_P(system_ram_offload_min_free_bytes_value) > 0) { + system_ram_offload_min_free_bytes = Z_LVAL_P(system_ram_offload_min_free_bytes_value); + } + free_vram_floor_met = free_vram_floor_met_value != NULL && zend_is_true(free_vram_floor_met_value); + weights_resident = model->cuda_weight_upload_result == 0 + && model->cuda_weight_uploaded_bytes > 0 + && model->cuda_device_bytes_allocated > 0; + kv_cache_resident = model->cuda_device_kv_cache_allocated + && model->cuda_device_kv_cache_bytes > 0; + if (weights_resident) { + if (kv_cache_resident) { + runtime_required_bytes = 0; + runtime_required_source = "weights_and_kv_cache_already_loaded"; + } else { + runtime_required_bytes = estimate_available ? estimated_bytes : 0; + runtime_required_source = estimate_available ? "kv_cache_after_weight_upload" : "weights_already_loaded"; + } + } else if (estimate_available && estimated_bytes > ZEND_LONG_MAX - artifact_bytes) { + overflow = true; + runtime_required_bytes = ZEND_LONG_MAX; + } else { + runtime_required_bytes = artifact_bytes + (estimate_available ? estimated_bytes : 0); + } + compared_to_free = (runtime_required_bytes > 0 || weights_resident) && effective_free_vram_bytes > 0; + fits_free = free_vram_floor_met + && compared_to_free + && !overflow + && (weights_resident && !estimate_available + ? effective_free_vram_bytes > 0 + : runtime_required_bytes <= effective_free_vram_bytes); + fits_without_reserve = free_vram_bytes > 0 && !overflow && runtime_required_bytes <= free_vram_bytes; + system_ram_offload_required = compared_to_free && !fits_free; + system_ram_available = king_inference_gpu_system_ram_available_bytes(&system_ram_available_bytes); + if (system_ram_offload_required && runtime_required_bytes > effective_free_vram_bytes) { + system_ram_offload_required_bytes = runtime_required_bytes - effective_free_vram_bytes; + } + system_ram_offload_limit_configured = system_ram_offload_max_bytes > 0; + system_ram_offload_limit_met = system_ram_offload_required_bytes > 0 + && system_ram_offload_limit_configured + && system_ram_offload_required_bytes <= system_ram_offload_max_bytes; + system_ram_floor_met = !system_ram_offload_required + || (system_ram_available + && system_ram_available_bytes > system_ram_offload_required_bytes + && system_ram_available_bytes - system_ram_offload_required_bytes >= system_ram_offload_min_free_bytes); + system_ram_offload_budget_admitted = system_ram_offload_required + && system_ram_offload_allowed + && system_ram_offload_limit_met + && system_ram_floor_met; + + add_assoc_bool(return_value, "kv_cache_estimate_available", estimate_available); + add_assoc_long(return_value, "kv_cache_estimated_bytes", estimate_available ? estimated_bytes : 0); + add_assoc_long( + return_value, + "kv_cache_context_tokens", + max_context_tokens != NULL && Z_TYPE_P(max_context_tokens) == IS_LONG ? Z_LVAL_P(max_context_tokens) : 0 + ); + add_assoc_long( + return_value, + "kv_cache_layers", + layers != NULL && Z_TYPE_P(layers) == IS_LONG ? Z_LVAL_P(layers) : 0 + ); + add_assoc_long( + return_value, + "kv_cache_page_tokens", + page_tokens != NULL && Z_TYPE_P(page_tokens) == IS_LONG ? Z_LVAL_P(page_tokens) : 0 + ); + add_assoc_long( + return_value, + "kv_cache_element_bytes", + element_bytes != NULL && Z_TYPE_P(element_bytes) == IS_LONG ? Z_LVAL_P(element_bytes) : 0 + ); + add_assoc_string( + return_value, + "kv_cache_estimate_source", + estimate_available ? "paged_kv_cache_plan" : "paged_kv_cache_plan_incomplete" + ); + add_assoc_long(return_value, "model_vram_required_bytes", runtime_required_bytes); + add_assoc_long(return_value, "runtime_vram_required_bytes", runtime_required_bytes); + add_assoc_string( + return_value, + "runtime_vram_required_source", + estimate_available || weights_resident ? runtime_required_source : "artifact_only" + ); + add_assoc_bool(return_value, "runtime_vram_compared_to_free", compared_to_free); + add_assoc_bool(return_value, "runtime_vram_fits_without_reserve", fits_without_reserve); + add_assoc_bool(return_value, "runtime_vram_fits_after_reserve", fits_free); + add_assoc_bool(return_value, "runtime_vram_fits_free", fits_free); + add_assoc_bool(return_value, "system_ram_offload_allowed", system_ram_offload_allowed); + add_assoc_bool(return_value, "system_ram_offload_required", system_ram_offload_required); + add_assoc_long(return_value, "system_ram_offload_required_bytes", system_ram_offload_required_bytes); + add_assoc_bool(return_value, "system_ram_offload_limit_configured", system_ram_offload_limit_configured); + add_assoc_bool(return_value, "system_ram_offload_limit_met", system_ram_offload_limit_met); + add_assoc_bool(return_value, "system_ram_available", system_ram_available); + add_assoc_long(return_value, "system_ram_available_bytes", system_ram_available ? system_ram_available_bytes : 0); + add_assoc_bool(return_value, "system_ram_offload_floor_met", system_ram_floor_met); + add_assoc_bool(return_value, "system_ram_offload_budget_admitted", system_ram_offload_budget_admitted); + add_assoc_bool(return_value, "system_ram_offload_supported", system_ram_offload_supported); + add_assoc_bool(return_value, "system_ram_offload_admitted", system_ram_offload_admitted); + add_assoc_string( + return_value, + "system_ram_offload_status", + king_inference_gpu_system_ram_offload_status( + system_ram_offload_required, + system_ram_offload_allowed, + system_ram_offload_limit_configured, + system_ram_offload_limit_met, + system_ram_available, + system_ram_floor_met, + system_ram_offload_supported, + system_ram_offload_admitted + ) + ); + add_assoc_string( + return_value, + "system_ram_offload_error", + king_inference_gpu_system_ram_offload_error( + system_ram_offload_required, + system_ram_offload_allowed, + system_ram_offload_limit_configured, + system_ram_offload_limit_met, + system_ram_available, + system_ram_floor_met, + system_ram_offload_supported, + system_ram_offload_admitted + ) + ); + if (estimate_available || weights_resident) { + bool runtime_config_ready = king_inference_gpu_runtime_config_ready_after_vram(return_value, fits_free); + + add_assoc_bool(return_value, "model_vram_admitted", fits_free); + add_assoc_bool(return_value, "config_ready", runtime_config_ready); + if (runtime_config_ready) { + zval empty_refusals; + + add_assoc_string(return_value, "reason", "ready"); + array_init(&empty_refusals); + add_assoc_zval(return_value, "refusal_reasons", &empty_refusals); + } + } + if (system_ram_offload_required && !system_ram_offload_allowed) { + add_assoc_string(return_value, "reason", "gpu_system_ram_offload_disabled"); + king_inference_gpu_status_append_return_reason(return_value, "gpu_system_ram_offload_disabled"); + } else if (system_ram_offload_required && !system_ram_offload_limit_configured) { + add_assoc_string(return_value, "reason", "gpu_system_ram_offload_limit_missing"); + king_inference_gpu_status_append_return_reason(return_value, "gpu_system_ram_offload_limit_missing"); + } else if (system_ram_offload_required && !system_ram_offload_limit_met) { + add_assoc_string(return_value, "reason", "gpu_system_ram_offload_exceeds_limit"); + king_inference_gpu_status_append_return_reason(return_value, "gpu_system_ram_offload_exceeds_limit"); + } else if (system_ram_offload_required && !system_ram_available) { + add_assoc_string(return_value, "reason", "gpu_system_ram_unavailable"); + king_inference_gpu_status_append_return_reason(return_value, "gpu_system_ram_unavailable"); + } else if (system_ram_offload_required && !system_ram_floor_met) { + add_assoc_string(return_value, "reason", "gpu_system_ram_offload_floor_not_met"); + king_inference_gpu_status_append_return_reason(return_value, "gpu_system_ram_offload_floor_not_met"); + } else if (system_ram_offload_required && !system_ram_offload_supported) { + add_assoc_string(return_value, "reason", "gpu_system_ram_offload_not_supported"); + king_inference_gpu_status_append_return_reason(return_value, "gpu_system_ram_offload_not_supported"); + } else if (system_ram_offload_required && !system_ram_offload_admitted) { + add_assoc_string(return_value, "reason", "gpu_system_ram_offload_not_admitted"); + king_inference_gpu_status_append_return_reason(return_value, "gpu_system_ram_offload_not_admitted"); + } + if (previous_model_vram_admitted_bool && compared_to_free && !fits_free) { + runtime_reason = overflow + ? "gpu_required_vram_overflow" + : (fits_without_reserve + ? "gpu_required_vram_exceeds_free_vram_after_reserve" + : "gpu_required_vram_exceeds_free_vram"); + + add_assoc_string(return_value, "reason", runtime_reason); + king_inference_gpu_status_append_return_reason(return_value, runtime_reason); + } +} diff --git a/extension/src/inference/cuda/runtime/policy/gpu_runtime_memory_policy.inc b/extension/src/inference/cuda/runtime/policy/gpu_runtime_memory_policy.inc new file mode 100644 index 000000000..0f7f2be85 --- /dev/null +++ b/extension/src/inference/cuda/runtime/policy/gpu_runtime_memory_policy.inc @@ -0,0 +1,135 @@ +/* + * GPU runtime memory and offload policy helpers. + */ + +static bool king_inference_gpu_status_artifact_size(const char *path, size_t *bytes_out) +{ + struct stat st; + + *bytes_out = 0; + if (!king_inference_gpu_status_non_empty(path)) { + return false; + } + if (stat(path, &st) != 0 || !S_ISREG(st.st_mode) || st.st_size <= 0) { + return false; + } + + *bytes_out = (size_t) st.st_size; + return true; +} + +static bool king_inference_gpu_status_backend_supported(const char *backend) +{ + return !king_inference_gpu_status_non_empty(backend) + || strcasecmp(backend, "auto") == 0 + || strcasecmp(backend, "cuda") == 0; +} + +static zend_long king_inference_gpu_mb_to_bytes(zend_long mb) +{ + if (mb <= 0) { + return 0; + } + if (mb > ZEND_LONG_MAX / 1024 / 1024) { + return ZEND_LONG_MAX; + } + + return mb * 1024 * 1024; +} + +static bool king_inference_gpu_system_ram_available_bytes(zend_long *bytes_out) +{ + FILE *file; + char buffer[160]; + + *bytes_out = 0; + file = fopen("/proc/meminfo", "r"); + if (file == NULL) { + return false; + } + + while (fgets(buffer, sizeof(buffer), file) != NULL) { + unsigned long long kb; + + if (sscanf(buffer, "MemAvailable: %llu kB", &kb) == 1) { + fclose(file); + if (kb > (unsigned long long) ZEND_LONG_MAX / 1024ULL) { + *bytes_out = ZEND_LONG_MAX; + } else { + *bytes_out = (zend_long) (kb * 1024ULL); + } + return true; + } + } + + fclose(file); + return false; +} + +static const char *king_inference_gpu_system_ram_offload_status( + bool required, + bool allowed, + bool limit_configured, + bool limit_met, + bool system_ram_available, + bool system_ram_floor_met, + bool supported, + bool admitted +) { + if (!required) { + return allowed ? "allowed_not_required" : "disabled_not_required"; + } + if (!allowed) { + return "required_but_disabled"; + } + if (!limit_configured) { + return "required_but_limit_missing"; + } + if (!limit_met) { + return "required_but_exceeds_limit"; + } + if (!system_ram_available) { + return "required_but_system_ram_unknown"; + } + if (!system_ram_floor_met) { + return "required_but_system_ram_floor_not_met"; + } + if (!supported) { + return "required_but_not_supported"; + } + return admitted ? "admitted" : "required_but_not_admitted"; +} + +static const char *king_inference_gpu_system_ram_offload_error( + bool required, + bool allowed, + bool limit_configured, + bool limit_met, + bool system_ram_available, + bool system_ram_floor_met, + bool supported, + bool admitted +) { + if (!required || admitted) { + return "none"; + } + if (!allowed) { + return "gpu.system_ram_offload_required_but_disabled"; + } + if (!limit_configured) { + return "gpu.system_ram_offload_required_but_limit_missing"; + } + if (!limit_met) { + return "gpu.system_ram_offload_required_but_exceeds_limit"; + } + if (!system_ram_available) { + return "gpu.system_ram_offload_required_but_system_ram_unknown"; + } + if (!system_ram_floor_met) { + return "gpu.system_ram_offload_required_but_system_ram_floor_not_met"; + } + if (!supported) { + return "gpu.system_ram_offload_required_but_not_supported"; + } + return "gpu.system_ram_offload_required_but_not_admitted"; +} diff --git a/extension/src/inference/cuda/runtime/policy/gpu_vram_policy.inc b/extension/src/inference/cuda/runtime/policy/gpu_vram_policy.inc new file mode 100644 index 000000000..38e84fc64 --- /dev/null +++ b/extension/src/inference/cuda/runtime/policy/gpu_vram_policy.inc @@ -0,0 +1,56 @@ +/* + * VRAM admission helpers for King inference. + */ + +typedef struct _king_inference_gpu_vram_policy { + zend_long free_bytes; + zend_long reserve_mb; + zend_long reserve_bytes; + zend_long min_free_mb; + zend_long min_free_bytes; + zend_long free_after_reserve_bytes; + bool floor_met; +} king_inference_gpu_vram_policy; + +static zend_long king_inference_gpu_vram_mb_to_bytes(zend_long mb) +{ + if (mb <= 0) { + return 0; + } + if (mb > ZEND_LONG_MAX / 1048576) { + return ZEND_LONG_MAX; + } + + return mb * 1048576; +} + +static king_inference_gpu_vram_policy king_inference_gpu_vram_policy_from_free( + bool memory_info_available, + size_t free_bytes, + zend_long reserve_mb, + zend_long min_free_mb +) { + king_inference_gpu_vram_policy policy; + + policy.free_bytes = 0; + policy.reserve_mb = reserve_mb; + policy.reserve_bytes = king_inference_gpu_vram_mb_to_bytes(reserve_mb); + policy.min_free_mb = min_free_mb; + policy.min_free_bytes = king_inference_gpu_vram_mb_to_bytes(min_free_mb); + policy.free_after_reserve_bytes = 0; + policy.floor_met = false; + + if (!memory_info_available) { + return policy; + } + + policy.free_bytes = free_bytes > (size_t) ZEND_LONG_MAX + ? ZEND_LONG_MAX + : (zend_long) free_bytes; + policy.free_after_reserve_bytes = policy.free_bytes > policy.reserve_bytes + ? policy.free_bytes - policy.reserve_bytes + : 0; + policy.floor_met = policy.free_bytes >= policy.min_free_bytes; + + return policy; +} diff --git a/extension/src/inference/cuda/runtime/status/gpu_runtime_admission_ready.inc b/extension/src/inference/cuda/runtime/status/gpu_runtime_admission_ready.inc new file mode 100644 index 000000000..3c996d513 --- /dev/null +++ b/extension/src/inference/cuda/runtime/status/gpu_runtime_admission_ready.inc @@ -0,0 +1,77 @@ +/* + * GPU runtime admission readiness helpers. + */ + +static bool king_inference_gpu_runtime_thermal_ready(zval *source) +{ + zval *thermal = zend_hash_str_find(Z_ARRVAL_P(source), "thermal", sizeof("thermal") - 1); + zval *monitored; + zval *allow_unmonitored; + zval *temperature_available; + zval *temperature_ok; + + if (thermal == NULL || Z_TYPE_P(thermal) != IS_ARRAY) { + return false; + } + + monitored = zend_hash_str_find(Z_ARRVAL_P(thermal), "monitored", sizeof("monitored") - 1); + allow_unmonitored = zend_hash_str_find( + Z_ARRVAL_P(thermal), + "allow_unmonitored_gpu", + sizeof("allow_unmonitored_gpu") - 1 + ); + temperature_available = zend_hash_str_find( + Z_ARRVAL_P(thermal), + "temperature_available", + sizeof("temperature_available") - 1 + ); + temperature_ok = zend_hash_str_find( + Z_ARRVAL_P(thermal), + "temperature_ok", + sizeof("temperature_ok") - 1 + ); + + return (monitored != NULL && zend_is_true(monitored) + && temperature_available != NULL && zend_is_true(temperature_available) + && temperature_ok != NULL && zend_is_true(temperature_ok)) + || ((monitored == NULL || !zend_is_true(monitored)) + && allow_unmonitored != NULL && zend_is_true(allow_unmonitored)); +} + +static bool king_inference_gpu_runtime_power_ready(zval *source) +{ + zval *power = zend_hash_str_find(Z_ARRVAL_P(source), "power", sizeof("power") - 1); + zval *enabled; + zval *power_ok; + + if (power == NULL || Z_TYPE_P(power) != IS_ARRAY) { + return true; + } + + enabled = zend_hash_str_find(Z_ARRVAL_P(power), "enabled", sizeof("enabled") - 1); + if (enabled == NULL || !zend_is_true(enabled)) { + return true; + } + + power_ok = zend_hash_str_find(Z_ARRVAL_P(power), "power_ok", sizeof("power_ok") - 1); + return power_ok != NULL && zend_is_true(power_ok); +} + +static bool king_inference_gpu_runtime_config_ready_after_vram( + zval *return_value, + bool fits_free +) { + return king_inference_gpu_runtime_bool_field(return_value, "gpu_enabled") + && king_inference_gpu_runtime_bool_field(return_value, "process_gpu_bindings_enable") + && king_inference_gpu_runtime_bool_field(return_value, "config_gpu_bindings_enable") + && king_inference_gpu_runtime_bool_field(return_value, "backend_supported") + && king_inference_gpu_runtime_bool_field(return_value, "artifact_configured") + && king_inference_gpu_runtime_bool_field(return_value, "artifact_readable") + && king_inference_gpu_runtime_long_field(return_value, "max_gpu_layers") > 0 + && king_inference_gpu_runtime_bool_field(return_value, "vram_admission_checked") + && king_inference_gpu_runtime_bool_field(return_value, "free_vram_floor_met") + && fits_free + && king_inference_gpu_runtime_bool_field(return_value, "driver_visible") + && king_inference_gpu_runtime_thermal_ready(return_value) + && king_inference_gpu_runtime_power_ready(return_value); +} diff --git a/extension/src/inference/cuda/runtime/status/gpu_runtime_cuda_driver_probe.inc b/extension/src/inference/cuda/runtime/status/gpu_runtime_cuda_driver_probe.inc new file mode 100644 index 000000000..f1b8070bf --- /dev/null +++ b/extension/src/inference/cuda/runtime/status/gpu_runtime_cuda_driver_probe.inc @@ -0,0 +1,182 @@ +/* + * CUDA driver probing for GPU runtime admission. + */ + +typedef int (*king_inference_cuda_cu_init_fn)(unsigned int flags); +typedef int (*king_inference_cuda_cu_driver_get_version_fn)(int *driver_version); +typedef int (*king_inference_cuda_cu_device_get_count_fn)(int *count); +typedef int (*king_inference_cuda_cu_device_get_fn)(int *device, int ordinal); +typedef int (*king_inference_cuda_cu_device_get_name_fn)(char *name, int len, int device); +typedef int (*king_inference_cuda_cu_device_total_mem_fn)(size_t *bytes, int device); +typedef int (*king_inference_cuda_cu_device_primary_ctx_retain_fn)(void **ctx, int device); +typedef int (*king_inference_cuda_cu_device_primary_ctx_release_fn)(int device); +typedef int (*king_inference_cuda_cu_ctx_set_current_fn)(void *ctx); +typedef int (*king_inference_cuda_cu_mem_get_info_fn)(size_t *free_bytes, size_t *total_bytes); + +typedef struct _king_inference_cuda_driver_status { + bool loader_available; + bool symbols_available; + bool initialized; + bool memory_info_available; + int init_result; + int driver_version; + int device_count; + char first_device_name[256]; + size_t first_device_total_memory_bytes; + size_t first_device_free_memory_bytes; + size_t first_device_runtime_total_memory_bytes; + char error[160]; +} king_inference_cuda_driver_status; + +static void king_inference_cuda_driver_status_init(king_inference_cuda_driver_status *status) +{ + memset(status, 0, sizeof(*status)); + status->init_result = -1; + status->driver_version = 0; + status->device_count = 0; + status->first_device_name[0] = '\0'; + status->first_device_free_memory_bytes = 0; + status->first_device_runtime_total_memory_bytes = 0; + status->error[0] = '\0'; +} + +static void king_inference_cuda_driver_status_set_error( + king_inference_cuda_driver_status *status, + const char *message) +{ + if (message == NULL) { + message = "unknown_cuda_driver_error"; + } + + snprintf(status->error, sizeof(status->error), "%s", message); +} + +static bool king_inference_cuda_driver_symbol( + void *handle, + const char *name, + void **symbol, + king_inference_cuda_driver_status *status) +{ + *symbol = dlsym(handle, name); + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_driver_status_set_error(status, name); + return false; +} + +static void king_inference_cuda_driver_probe(king_inference_cuda_driver_status *status) +{ + void *handle; + king_inference_cuda_cu_init_fn cu_init; + king_inference_cuda_cu_driver_get_version_fn cu_driver_get_version; + king_inference_cuda_cu_device_get_count_fn cu_device_get_count; + king_inference_cuda_cu_device_get_fn cu_device_get; + king_inference_cuda_cu_device_get_name_fn cu_device_get_name; + king_inference_cuda_cu_device_total_mem_fn cu_device_total_mem; + king_inference_cuda_cu_device_primary_ctx_retain_fn cu_device_primary_ctx_retain; + king_inference_cuda_cu_device_primary_ctx_release_fn cu_device_primary_ctx_release; + king_inference_cuda_cu_ctx_set_current_fn cu_ctx_set_current; + king_inference_cuda_cu_mem_get_info_fn cu_mem_get_info; + void *ctx = NULL; + int device = 0; + + king_inference_cuda_driver_status_init(status); + + handle = dlopen("libcuda.so.1", RTLD_LAZY | RTLD_LOCAL); + if (handle == NULL) { + king_inference_cuda_driver_status_set_error(status, dlerror()); + return; + } + status->loader_available = true; + + if (!king_inference_cuda_driver_symbol(handle, "cuInit", (void **) &cu_init, status) + || !king_inference_cuda_driver_symbol( + handle, + "cuDriverGetVersion", + (void **) &cu_driver_get_version, + status + ) + || !king_inference_cuda_driver_symbol( + handle, + "cuDeviceGetCount", + (void **) &cu_device_get_count, + status + ) + || !king_inference_cuda_driver_symbol(handle, "cuDeviceGet", (void **) &cu_device_get, status) + || !king_inference_cuda_driver_symbol( + handle, + "cuDeviceGetName", + (void **) &cu_device_get_name, + status + )) { + dlclose(handle); + return; + } + + cu_device_total_mem = NULL; + if (dlsym(handle, "cuDeviceTotalMem_v2") != NULL) { + cu_device_total_mem = (king_inference_cuda_cu_device_total_mem_fn) + dlsym(handle, "cuDeviceTotalMem_v2"); + } else if (dlsym(handle, "cuDeviceTotalMem") != NULL) { + cu_device_total_mem = (king_inference_cuda_cu_device_total_mem_fn) + dlsym(handle, "cuDeviceTotalMem"); + } + cu_device_primary_ctx_retain = (king_inference_cuda_cu_device_primary_ctx_retain_fn) + dlsym(handle, "cuDevicePrimaryCtxRetain"); + cu_device_primary_ctx_release = (king_inference_cuda_cu_device_primary_ctx_release_fn) + dlsym(handle, "cuDevicePrimaryCtxRelease"); + cu_ctx_set_current = (king_inference_cuda_cu_ctx_set_current_fn) + dlsym(handle, "cuCtxSetCurrent"); + cu_mem_get_info = NULL; + if (dlsym(handle, "cuMemGetInfo_v2") != NULL) { + cu_mem_get_info = (king_inference_cuda_cu_mem_get_info_fn) + dlsym(handle, "cuMemGetInfo_v2"); + } else if (dlsym(handle, "cuMemGetInfo") != NULL) { + cu_mem_get_info = (king_inference_cuda_cu_mem_get_info_fn) + dlsym(handle, "cuMemGetInfo"); + } + + status->symbols_available = true; + status->init_result = cu_init(0); + if (status->init_result != 0) { + king_inference_cuda_driver_status_set_error(status, "cuInit_failed"); + dlclose(handle); + return; + } + status->initialized = true; + + if (cu_driver_get_version(&status->driver_version) != 0) { + status->driver_version = 0; + } + if (cu_device_get_count(&status->device_count) != 0 || status->device_count < 0) { + status->device_count = 0; + } + if (status->device_count > 0 && cu_device_get(&device, 0) == 0) { + if (cu_device_get_name(status->first_device_name, sizeof(status->first_device_name), device) != 0) { + status->first_device_name[0] = '\0'; + } + if (cu_device_total_mem != NULL + && cu_device_total_mem(&status->first_device_total_memory_bytes, device) != 0) { + status->first_device_total_memory_bytes = 0; + } + if (cu_device_primary_ctx_retain != NULL + && cu_device_primary_ctx_release != NULL + && cu_ctx_set_current != NULL + && cu_mem_get_info != NULL + && cu_device_primary_ctx_retain(&ctx, device) == 0 + && ctx != NULL) { + if (cu_ctx_set_current(ctx) == 0 + && cu_mem_get_info( + &status->first_device_free_memory_bytes, + &status->first_device_runtime_total_memory_bytes + ) == 0) { + status->memory_info_available = true; + } + (void) cu_device_primary_ctx_release(device); + } + } + + dlclose(handle); +} diff --git a/extension/src/inference/cuda/runtime/status/gpu_runtime_power_status.inc b/extension/src/inference/cuda/runtime/status/gpu_runtime_power_status.inc new file mode 100644 index 000000000..f997ffa00 --- /dev/null +++ b/extension/src/inference/cuda/runtime/status/gpu_runtime_power_status.inc @@ -0,0 +1,42 @@ +/* + * GPU runtime power telemetry helpers. + */ + +static bool king_inference_gpu_status_read_power_command( + const char *command, + double *power_watts +) { + FILE *pipe; + char buffer[64]; + char *endptr; + double value; + int status; + + if (!king_inference_gpu_status_non_empty(command)) { + return false; + } + + pipe = popen(command, "r"); + if (pipe == NULL) { + return false; + } + + if (fgets(buffer, sizeof(buffer), pipe) == NULL) { + pclose(pipe); + return false; + } + + status = pclose(pipe); + if (status == -1) { + return false; + } + + errno = 0; + value = strtod(buffer, &endptr); + if (errno != 0 || endptr == buffer || !isfinite(value) || value < 0.0) { + return false; + } + + *power_watts = value; + return true; +} diff --git a/extension/src/inference/cuda/runtime/status/gpu_runtime_reason.inc b/extension/src/inference/cuda/runtime/status/gpu_runtime_reason.inc new file mode 100644 index 000000000..dfa8498d6 --- /dev/null +++ b/extension/src/inference/cuda/runtime/status/gpu_runtime_reason.inc @@ -0,0 +1,213 @@ +/* + * GPU runtime refusal reason helpers. + */ + +static void king_inference_gpu_status_append_reason(zval *reasons, const char *reason) +{ + zval *entry; + + if (reason == NULL || reason[0] == '\0') { + return; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(reasons), entry) { + if (Z_TYPE_P(entry) == IS_STRING && strcmp(Z_STRVAL_P(entry), reason) == 0) { + return; + } + } ZEND_HASH_FOREACH_END(); + + add_next_index_string(reasons, reason); +} + +static const char *king_inference_gpu_status_first_reason(zval *reasons) +{ + zval *first = zend_hash_index_find(Z_ARRVAL_P(reasons), 0); + + if (first == NULL || Z_TYPE_P(first) != IS_STRING || Z_STRLEN_P(first) == 0) { + return "gpu_ready"; + } + + return Z_STRVAL_P(first); +} + +static void king_inference_gpu_decoder_blockers(king_inference_model_object *model, zval *blockers) +{ + array_init(blockers); + if (model == NULL || !model->cuda_embedding_row_available) { + king_inference_gpu_status_append_reason(blockers, "gpu_embedding_row_loader_missing"); + } + if (model == NULL || !model->cuda_device_vector_ops_available) { + king_inference_gpu_status_append_reason(blockers, "gpu_device_vector_ops_missing"); + } + if (model == NULL || !model->cuda_device_kv_cache_available) { + king_inference_gpu_status_append_reason(blockers, "gpu_device_kv_cache_missing"); + } + if (model == NULL || !model->cuda_decoder_graph_executor_available) { + king_inference_gpu_status_append_reason(blockers, "gpu_decoder_graph_executor_missing"); + } + if (model == NULL || !model->cuda_decoder_graph_executor_attention_residual_execution_available) { + king_inference_gpu_status_append_reason(blockers, "gpu_decoder_graph_attention_residual_missing"); + } + if (model == NULL || !model->cuda_decoder_graph_executor_ffn_norm_execution_available) { + king_inference_gpu_status_append_reason(blockers, "gpu_decoder_graph_ffn_norm_missing"); + } + if (model == NULL || !model->cuda_decoder_graph_executor_ffn_gate_up_projection_execution_available) { + king_inference_gpu_status_append_reason(blockers, "gpu_decoder_graph_ffn_gate_up_projection_missing"); + } + if (model == NULL || !model->cuda_decoder_graph_executor_ffn_swiglu_execution_available) { + king_inference_gpu_status_append_reason(blockers, "gpu_decoder_graph_ffn_swiglu_missing"); + } + if (model == NULL || !model->cuda_decoder_graph_executor_ffn_down_projection_execution_available) { + king_inference_gpu_status_append_reason(blockers, "gpu_decoder_graph_ffn_down_projection_missing"); + } + if (model == NULL || !model->cuda_decoder_graph_executor_ffn_output_residual_execution_available) { + king_inference_gpu_status_append_reason(blockers, "gpu_decoder_graph_ffn_output_residual_missing"); + } + if (model == NULL || !model->cuda_decoder_graph_executor_final_norm_execution_available) { + king_inference_gpu_status_append_reason(blockers, "gpu_decoder_graph_final_norm_missing"); + } + if (model == NULL || !model->cuda_decoder_graph_executor_logits_projection_execution_available) { + king_inference_gpu_status_append_reason(blockers, "gpu_decoder_graph_logits_projection_missing"); + } + if (model == NULL || !model->cuda_decoder_prompt_loop_available) { + king_inference_gpu_status_append_reason(blockers, "gpu_prompt_decoder_loop_missing"); + } +} + +static void king_inference_gpu_replace_return_decoder_blockers( + king_inference_model_object *model, + zval *return_value +) { + zval blockers; + + king_inference_gpu_decoder_blockers(model, &blockers); + add_assoc_zval(return_value, "decoder_blockers", &blockers); +} + +static void king_inference_gpu_status_refusal_reasons( + zval *reasons, + bool gpu_enabled, + bool process_bindings_enabled, + bool config_bindings_enabled, + bool backend_supported, + bool artifact_configured, + bool artifact_readable, + zend_long max_gpu_layers, + bool thermal_monitored, + bool allow_unmonitored, + bool temperature_available, + bool temperature_ok, + bool power_configured, + bool power_sensor_configured, + bool power_available, + bool power_ok, + bool driver_visible, + bool vram_admission_checked, + bool free_vram_floor_met, + bool model_vram_fits_after_reserve, + bool model_vram_fits_without_reserve, + bool system_ram_offload_required, + bool system_ram_offload_allowed, + bool system_ram_offload_limit_configured, + bool system_ram_offload_limit_met, + bool system_ram_available, + bool system_ram_floor_met, + bool system_ram_offload_supported, + bool system_ram_offload_admitted +) { + array_init(reasons); + + if (!gpu_enabled) { + king_inference_gpu_status_append_reason(reasons, "gpu_disabled"); + } + if (!process_bindings_enabled) { + king_inference_gpu_status_append_reason(reasons, "process_gpu_bindings_disabled"); + } + if (!config_bindings_enabled) { + king_inference_gpu_status_append_reason(reasons, "config_gpu_bindings_disabled"); + } + if (!backend_supported) { + king_inference_gpu_status_append_reason(reasons, "unsupported_gpu_backend"); + } + if (!artifact_configured) { + king_inference_gpu_status_append_reason(reasons, "gpu_model_artifact_missing"); + } else if (!artifact_readable) { + king_inference_gpu_status_append_reason(reasons, "gpu_model_artifact_not_readable"); + } + if (max_gpu_layers <= 0) { + king_inference_gpu_status_append_reason(reasons, "gpu_layers_disabled"); + } + if (!vram_admission_checked) { + king_inference_gpu_status_append_reason(reasons, "gpu_vram_status_unavailable"); + } else { + if (!free_vram_floor_met) { + king_inference_gpu_status_append_reason(reasons, "gpu_free_vram_below_configured_floor"); + } + if (!model_vram_fits_after_reserve) { + king_inference_gpu_status_append_reason( + reasons, + model_vram_fits_without_reserve + ? "gpu_model_artifact_exceeds_free_vram_after_reserve" + : "gpu_model_artifact_exceeds_free_vram" + ); + if (system_ram_offload_required && !system_ram_offload_allowed) { + king_inference_gpu_status_append_reason(reasons, "gpu_system_ram_offload_disabled"); + } else if (system_ram_offload_required && !system_ram_offload_limit_configured) { + king_inference_gpu_status_append_reason(reasons, "gpu_system_ram_offload_limit_missing"); + } else if (system_ram_offload_required && !system_ram_offload_limit_met) { + king_inference_gpu_status_append_reason(reasons, "gpu_system_ram_offload_exceeds_limit"); + } else if (system_ram_offload_required && !system_ram_available) { + king_inference_gpu_status_append_reason(reasons, "gpu_system_ram_unavailable"); + } else if (system_ram_offload_required && !system_ram_floor_met) { + king_inference_gpu_status_append_reason(reasons, "gpu_system_ram_offload_floor_not_met"); + } else if (system_ram_offload_required && !system_ram_offload_supported) { + king_inference_gpu_status_append_reason(reasons, "gpu_system_ram_offload_not_supported"); + } else if (system_ram_offload_required && !system_ram_offload_admitted) { + king_inference_gpu_status_append_reason(reasons, "gpu_system_ram_offload_not_admitted"); + } + } + } + if (!driver_visible && !temperature_available) { + king_inference_gpu_status_append_reason(reasons, "gpu_driver_not_visible"); + } + if (!thermal_monitored && !allow_unmonitored) { + king_inference_gpu_status_append_reason(reasons, "gpu_thermal_monitor_missing"); + } + if (thermal_monitored && !temperature_available) { + king_inference_gpu_status_append_reason(reasons, "gpu_temperature_unavailable"); + } + if (temperature_available && !temperature_ok) { + king_inference_gpu_status_append_reason(reasons, "gpu_temperature_limit_reached"); + } + if (power_configured && !power_sensor_configured) { + king_inference_gpu_status_append_reason(reasons, "gpu_power_sensor_missing"); + } + if (power_configured && power_sensor_configured && !power_available) { + king_inference_gpu_status_append_reason(reasons, "gpu_power_unavailable"); + } + if (power_configured && power_available && !power_ok) { + king_inference_gpu_status_append_reason(reasons, "gpu_power_limit_reached"); + } + if (zend_hash_num_elements(Z_ARRVAL_P(reasons)) == 0) { + king_inference_gpu_status_append_reason(reasons, "gpu_decoder_kernel_not_implemented"); + } +} + +static void king_inference_gpu_status_append_return_reason(zval *return_value, const char *reason) +{ + zval fresh; + zval *reasons = zend_hash_str_find( + Z_ARRVAL_P(return_value), + "refusal_reasons", + sizeof("refusal_reasons") - 1 + ); + + if (reasons != NULL && Z_TYPE_P(reasons) == IS_ARRAY) { + king_inference_gpu_status_append_reason(reasons, reason); + return; + } + + array_init(&fresh); + king_inference_gpu_status_append_reason(&fresh, reason); + add_assoc_zval(return_value, "refusal_reasons", &fresh); +} diff --git a/extension/src/inference/cuda/runtime/status/gpu_runtime_status.inc b/extension/src/inference/cuda/runtime/status/gpu_runtime_status.inc new file mode 100644 index 000000000..771880354 --- /dev/null +++ b/extension/src/inference/cuda/runtime/status/gpu_runtime_status.inc @@ -0,0 +1,733 @@ +/* + * GPU runtime status reporting for King inference. + */ + +static bool king_inference_gpu_status_non_empty(const char *value) +{ + return value != NULL && value[0] != '\0'; +} + +static bool king_inference_gpu_status_path_readable(const char *path) +{ + return king_inference_gpu_status_non_empty(path) && access(path, R_OK) == 0; +} + +#include "gpu_runtime_cuda_driver_probe.inc" + +static bool king_inference_gpu_status_driver_visible(void) +{ + return access("/proc/driver/nvidia/version", R_OK) == 0 + || access("/dev/nvidiactl", R_OK) == 0 + || access("/dev/dri", R_OK | X_OK) == 0; +} + +static bool king_inference_gpu_status_read_temperature_path( + const char *path, + double *temperature_c +) { + FILE *file; + char buffer[64]; + char *endptr; + double value; + + if (!king_inference_gpu_status_non_empty(path)) { + return false; + } + + file = fopen(path, "r"); + if (file == NULL) { + return false; + } + + if (fgets(buffer, sizeof(buffer), file) == NULL) { + fclose(file); + return false; + } + fclose(file); + + errno = 0; + value = strtod(buffer, &endptr); + if (errno != 0 || endptr == buffer) { + return false; + } + + *temperature_c = value > 1000.0 ? value / 1000.0 : value; + return true; +} + +static bool king_inference_gpu_status_read_temperature_command( + const char *command, + double *temperature_c +) { + FILE *pipe; + char buffer[64]; + char *endptr; + double value; + int status; + + if (!king_inference_gpu_status_non_empty(command)) { + return false; + } + + pipe = popen(command, "r"); + if (pipe == NULL) { + return false; + } + + if (fgets(buffer, sizeof(buffer), pipe) == NULL) { + pclose(pipe); + return false; + } + + status = pclose(pipe); + if (status == -1) { + return false; + } + + errno = 0; + value = strtod(buffer, &endptr); + if (errno != 0 || endptr == buffer) { + return false; + } + + *temperature_c = value > 1000.0 ? value / 1000.0 : value; + return true; +} + +#include "gpu_runtime_power_status.inc" +#include "../policy/gpu_runtime_memory_policy.inc" + +static void king_inference_gpu_runtime_status_array( + zval *return_value, + bool gpu_enabled, + bool process_bindings_enabled, + bool config_bindings_enabled, + const char *backend, + const char *artifact_path, + zend_long max_gpu_layers, + zend_long vram_reserve_mb, + zend_long min_free_vram_mb, + bool allow_system_ram_offload, + zend_long system_ram_offload_max_mb, + zend_long system_ram_offload_min_free_mb, + const char *thermal_sensor_path, + const char *thermal_sensor_command, + double max_temperature_c, + zend_long thermal_check_interval_sec, + bool allow_unmonitored, + const char *power_sensor_command, + double max_power_watts, + zend_long power_check_interval_sec +) { + zval thermal; + zval power; + zval cuda_driver; + king_inference_cuda_driver_status cuda_status; + bool backend_supported = king_inference_gpu_status_backend_supported(backend); + bool artifact_configured = king_inference_gpu_status_non_empty(artifact_path); + bool artifact_readable = king_inference_gpu_status_path_readable(artifact_path); + bool thermal_path_configured = king_inference_gpu_status_non_empty(thermal_sensor_path); + bool thermal_command_configured = king_inference_gpu_status_non_empty(thermal_sensor_command); + bool thermal_monitored = thermal_path_configured || thermal_command_configured; + bool power_configured = max_power_watts > 0.0; + bool power_sensor_configured = king_inference_gpu_status_non_empty(power_sensor_command); + size_t artifact_bytes = 0; + bool temperature_available = false; + bool temperature_ok = false; + bool power_available = false; + bool power_ok = !power_configured; + bool artifact_size_available; + bool vram_admission_checked; + bool model_vram_admitted; + bool model_vram_fits_after_reserve; + bool model_vram_fits_without_reserve; + bool system_ram_offload_required; + bool system_ram_offload_limit_configured; + bool system_ram_offload_limit_met; + bool system_ram_available; + bool system_ram_floor_met; + bool system_ram_offload_supported = false; + bool system_ram_offload_admitted = false; + zend_long system_ram_available_bytes = 0; + zend_long system_ram_offload_required_bytes = 0; + zend_long system_ram_offload_max_bytes; + zend_long system_ram_offload_min_free_bytes; + bool driver_visible; + bool config_ready; + king_inference_gpu_vram_policy vram_policy; + double temperature_c = 0.0; + double power_watts = 0.0; + zval decoder_blockers; + zval refusal_reasons; + const char *reason; + + king_inference_cuda_driver_probe(&cuda_status); + driver_visible = (cuda_status.initialized && cuda_status.device_count > 0) + || king_inference_gpu_status_driver_visible(); + artifact_size_available = king_inference_gpu_status_artifact_size(artifact_path, &artifact_bytes); + vram_policy = king_inference_gpu_vram_policy_from_free( + cuda_status.memory_info_available, + cuda_status.first_device_free_memory_bytes, + vram_reserve_mb, + min_free_vram_mb + ); + vram_admission_checked = artifact_size_available && cuda_status.memory_info_available; + model_vram_fits_without_reserve = vram_admission_checked + && artifact_bytes <= (size_t) vram_policy.free_bytes; + model_vram_fits_after_reserve = vram_admission_checked + && artifact_bytes <= (size_t) vram_policy.free_after_reserve_bytes; + system_ram_offload_required = vram_admission_checked + && !model_vram_fits_after_reserve; + system_ram_offload_max_bytes = king_inference_gpu_mb_to_bytes(system_ram_offload_max_mb); + system_ram_offload_min_free_bytes = king_inference_gpu_mb_to_bytes(system_ram_offload_min_free_mb); + system_ram_available = king_inference_gpu_system_ram_available_bytes(&system_ram_available_bytes); + if (system_ram_offload_required && artifact_bytes > (size_t) vram_policy.free_after_reserve_bytes) { + size_t required_bytes = artifact_bytes - (size_t) vram_policy.free_after_reserve_bytes; + system_ram_offload_required_bytes = required_bytes > (size_t) ZEND_LONG_MAX + ? ZEND_LONG_MAX + : (zend_long) required_bytes; + } + system_ram_offload_limit_configured = system_ram_offload_max_bytes > 0; + system_ram_offload_limit_met = system_ram_offload_required_bytes > 0 + && system_ram_offload_limit_configured + && system_ram_offload_required_bytes <= system_ram_offload_max_bytes; + system_ram_floor_met = !system_ram_offload_required + || (system_ram_available + && system_ram_available_bytes > system_ram_offload_required_bytes + && system_ram_available_bytes - system_ram_offload_required_bytes >= system_ram_offload_min_free_bytes); + model_vram_admitted = vram_admission_checked + && vram_policy.floor_met + && model_vram_fits_after_reserve; + + if (thermal_path_configured) { + temperature_available = king_inference_gpu_status_read_temperature_path( + thermal_sensor_path, + &temperature_c + ); + } else if (thermal_command_configured) { + temperature_available = king_inference_gpu_status_read_temperature_command( + thermal_sensor_command, + &temperature_c + ); + } + + if (temperature_available) { + driver_visible = true; + temperature_ok = temperature_c < max_temperature_c; + } + if (power_configured && power_sensor_configured) { + power_available = king_inference_gpu_status_read_power_command( + power_sensor_command, + &power_watts + ); + if (power_available) { + driver_visible = true; + power_ok = power_watts < max_power_watts; + } + } + + config_ready = gpu_enabled + && process_bindings_enabled + && config_bindings_enabled + && backend_supported + && artifact_configured + && artifact_readable + && max_gpu_layers > 0 + && vram_admission_checked + && vram_policy.floor_met + && model_vram_admitted + && driver_visible + && ((thermal_monitored && temperature_available && temperature_ok) + || (!thermal_monitored && allow_unmonitored)) + && (!power_configured || (power_sensor_configured && power_available && power_ok)); + + king_inference_gpu_status_refusal_reasons( + &refusal_reasons, + gpu_enabled, + process_bindings_enabled, + config_bindings_enabled, + backend_supported, + artifact_configured, + artifact_readable, + max_gpu_layers, + thermal_monitored, + allow_unmonitored, + temperature_available, + temperature_ok, + power_configured, + power_sensor_configured, + power_available, + power_ok, + driver_visible, + vram_admission_checked, + vram_policy.floor_met, + model_vram_fits_after_reserve, + model_vram_fits_without_reserve, + system_ram_offload_required, + allow_system_ram_offload, + system_ram_offload_limit_configured, + system_ram_offload_limit_met, + system_ram_available, + system_ram_floor_met, + system_ram_offload_supported, + system_ram_offload_admitted + ); + reason = king_inference_gpu_status_first_reason(&refusal_reasons); + + array_init(return_value); + add_assoc_bool(return_value, "gpu_enabled", gpu_enabled); + add_assoc_bool(return_value, "process_gpu_bindings_enable", process_bindings_enabled); + add_assoc_bool(return_value, "config_gpu_bindings_enable", config_bindings_enabled); + add_assoc_string( + return_value, + "backend", + king_inference_gpu_status_non_empty(backend) ? backend : "auto" + ); + add_assoc_bool(return_value, "backend_supported", backend_supported); + add_assoc_string( + return_value, + "artifact_path", + artifact_configured ? artifact_path : "" + ); + add_assoc_bool(return_value, "artifact_configured", artifact_configured); + add_assoc_bool(return_value, "artifact_readable", artifact_readable); + add_assoc_bool(return_value, "artifact_size_available", artifact_size_available); + add_assoc_long(return_value, "artifact_bytes", (zend_long) artifact_bytes); + add_assoc_bool(return_value, "free_vram_available", cuda_status.memory_info_available); + add_assoc_long(return_value, "free_vram_bytes", vram_policy.free_bytes); + add_assoc_long(return_value, "vram_reserve_mb", vram_policy.reserve_mb); + add_assoc_long(return_value, "vram_reserve_bytes", vram_policy.reserve_bytes); + add_assoc_long(return_value, "min_free_vram_mb", vram_policy.min_free_mb); + add_assoc_long(return_value, "min_free_vram_bytes", vram_policy.min_free_bytes); + add_assoc_bool(return_value, "free_vram_floor_met", vram_policy.floor_met); + add_assoc_long(return_value, "free_vram_after_reserve_bytes", vram_policy.free_after_reserve_bytes); + add_assoc_long(return_value, "model_vram_required_bytes", (zend_long) artifact_bytes); + add_assoc_bool(return_value, "model_vram_fits_without_reserve", model_vram_fits_without_reserve); + add_assoc_bool(return_value, "model_vram_fits_after_reserve", model_vram_fits_after_reserve); + add_assoc_long(return_value, "runtime_vram_required_bytes", (zend_long) artifact_bytes); + add_assoc_string(return_value, "runtime_vram_required_source", "artifact_only"); + add_assoc_bool(return_value, "runtime_vram_compared_to_free", vram_admission_checked); + add_assoc_bool(return_value, "runtime_vram_fits_without_reserve", model_vram_fits_without_reserve); + add_assoc_bool(return_value, "runtime_vram_fits_after_reserve", model_vram_fits_after_reserve); + add_assoc_bool(return_value, "runtime_vram_fits_free", model_vram_admitted); + add_assoc_bool(return_value, "system_ram_offload_allowed", allow_system_ram_offload); + add_assoc_bool(return_value, "system_ram_offload_required", system_ram_offload_required); + add_assoc_long(return_value, "system_ram_offload_required_bytes", system_ram_offload_required_bytes); + add_assoc_long(return_value, "system_ram_offload_max_mb", system_ram_offload_max_mb); + add_assoc_long(return_value, "system_ram_offload_max_bytes", system_ram_offload_max_bytes); + add_assoc_bool(return_value, "system_ram_offload_limit_configured", system_ram_offload_limit_configured); + add_assoc_bool(return_value, "system_ram_offload_limit_met", system_ram_offload_limit_met); + add_assoc_long(return_value, "system_ram_offload_min_free_mb", system_ram_offload_min_free_mb); + add_assoc_long(return_value, "system_ram_offload_min_free_bytes", system_ram_offload_min_free_bytes); + add_assoc_bool(return_value, "system_ram_available", system_ram_available); + add_assoc_long(return_value, "system_ram_available_bytes", system_ram_available ? system_ram_available_bytes : 0); + add_assoc_bool(return_value, "system_ram_offload_floor_met", system_ram_floor_met); + add_assoc_bool( + return_value, + "system_ram_offload_budget_admitted", + system_ram_offload_required + && allow_system_ram_offload + && system_ram_offload_limit_met + && system_ram_floor_met + ); + add_assoc_bool(return_value, "system_ram_offload_supported", system_ram_offload_supported); + add_assoc_bool(return_value, "system_ram_offload_admitted", system_ram_offload_admitted); + add_assoc_string( + return_value, + "system_ram_offload_status", + king_inference_gpu_system_ram_offload_status( + system_ram_offload_required, + allow_system_ram_offload, + system_ram_offload_limit_configured, + system_ram_offload_limit_met, + system_ram_available, + system_ram_floor_met, + system_ram_offload_supported, + system_ram_offload_admitted + ) + ); + add_assoc_string( + return_value, + "system_ram_offload_error", + king_inference_gpu_system_ram_offload_error( + system_ram_offload_required, + allow_system_ram_offload, + system_ram_offload_limit_configured, + system_ram_offload_limit_met, + system_ram_available, + system_ram_floor_met, + system_ram_offload_supported, + system_ram_offload_admitted + ) + ); + add_assoc_bool(return_value, "kv_cache_estimate_available", false); + add_assoc_long(return_value, "kv_cache_estimated_bytes", 0); + add_assoc_long(return_value, "kv_cache_context_tokens", 0); + add_assoc_long(return_value, "kv_cache_layers", 0); + add_assoc_long(return_value, "kv_cache_page_tokens", 0); + add_assoc_long(return_value, "kv_cache_element_bytes", 0); + add_assoc_string(return_value, "kv_cache_estimate_source", "unavailable"); + add_assoc_bool(return_value, "vram_admission_checked", vram_admission_checked); + add_assoc_bool(return_value, "model_vram_admitted", model_vram_admitted); + add_assoc_long(return_value, "max_gpu_layers", max_gpu_layers); + add_assoc_bool(return_value, "driver_visible", driver_visible); + add_assoc_bool(return_value, "config_ready", config_ready); + add_assoc_bool(return_value, "gpu_embedding_row_loader_ready", false); + add_assoc_bool(return_value, "gpu_device_vector_ops_ready", false); + add_assoc_bool(return_value, "gpu_device_kv_cache_ready", false); + add_assoc_bool(return_value, "decoder_graph_executor_ready", false); + add_assoc_bool(return_value, "decoder_graph_result_contract_ready", false); + add_assoc_bool(return_value, "decoder_graph_execution_plan_ready", false); + add_assoc_bool(return_value, "decoder_graph_embedding_execution_ready", false); + add_assoc_bool(return_value, "decoder_graph_rms_norm_execution_ready", false); + add_assoc_bool(return_value, "decoder_graph_linear_execution_ready", false); + add_assoc_bool(return_value, "decoder_graph_slice_execution_ready", false); + add_assoc_bool(return_value, "decoder_graph_rope_execution_ready", false); + add_assoc_bool(return_value, "decoder_graph_kv_head_prepare_execution_ready", false); + add_assoc_bool(return_value, "decoder_graph_kv_write_execution_ready", false); + add_assoc_bool(return_value, "decoder_graph_kv_attention_execution_ready", false); + add_assoc_bool(return_value, "decoder_prompt_loop_ready", false); + add_assoc_bool(return_value, "plain_text_chat_ready", false); + add_assoc_bool(return_value, "decoder_kernel_ready", false); + add_assoc_bool(return_value, "generation_ready", false); + king_inference_gpu_decoder_blockers(NULL, &decoder_blockers); + add_assoc_zval(return_value, "decoder_blockers", &decoder_blockers); + add_assoc_string(return_value, "reason", reason); + add_assoc_zval(return_value, "refusal_reasons", &refusal_reasons); + + array_init(&cuda_driver); + add_assoc_bool(&cuda_driver, "loader_available", cuda_status.loader_available); + add_assoc_bool(&cuda_driver, "symbols_available", cuda_status.symbols_available); + add_assoc_bool(&cuda_driver, "initialized", cuda_status.initialized); + add_assoc_long(&cuda_driver, "init_result", cuda_status.init_result); + add_assoc_long(&cuda_driver, "driver_version", cuda_status.driver_version); + add_assoc_long(&cuda_driver, "device_count", cuda_status.device_count); + add_assoc_string(&cuda_driver, "first_device_name", cuda_status.first_device_name); + add_assoc_long( + &cuda_driver, + "first_device_total_memory_bytes", + (zend_long) cuda_status.first_device_total_memory_bytes + ); + add_assoc_bool(&cuda_driver, "memory_info_available", cuda_status.memory_info_available); + add_assoc_long( + &cuda_driver, + "first_device_free_memory_bytes", + (zend_long) cuda_status.first_device_free_memory_bytes + ); + add_assoc_long( + &cuda_driver, + "first_device_runtime_total_memory_bytes", + (zend_long) cuda_status.first_device_runtime_total_memory_bytes + ); + add_assoc_string(&cuda_driver, "error", cuda_status.error); + add_assoc_zval(return_value, "cuda_driver", &cuda_driver); + + array_init(&thermal); + add_assoc_string( + &thermal, + "sensor_path", + thermal_path_configured ? thermal_sensor_path : "" + ); + add_assoc_string( + &thermal, + "sensor_command", + thermal_command_configured ? thermal_sensor_command : "" + ); + add_assoc_bool(&thermal, "monitored", thermal_monitored); + add_assoc_bool(&thermal, "allow_unmonitored_gpu", allow_unmonitored); + add_assoc_double(&thermal, "max_temperature_c", max_temperature_c); + add_assoc_long(&thermal, "check_interval_seconds", thermal_check_interval_sec); + add_assoc_bool(&thermal, "temperature_available", temperature_available); + if (temperature_available) { + add_assoc_double(&thermal, "temperature_c", temperature_c); + } else { + add_assoc_null(&thermal, "temperature_c"); + } + add_assoc_bool(&thermal, "temperature_ok", temperature_available && temperature_ok); + add_assoc_zval(return_value, "thermal", &thermal); + + array_init(&power); + add_assoc_bool(&power, "enabled", power_configured); + add_assoc_string( + &power, + "sensor_command", + power_sensor_configured ? power_sensor_command : "" + ); + add_assoc_bool(&power, "monitored", power_configured && power_sensor_configured); + add_assoc_double(&power, "max_watts", max_power_watts); + add_assoc_long(&power, "check_interval_seconds", power_check_interval_sec); + add_assoc_bool(&power, "power_available", power_available); + if (power_available) { + add_assoc_double(&power, "power_watts", power_watts); + } else { + add_assoc_null(&power, "power_watts"); + } + add_assoc_bool(&power, "power_ok", !power_configured || (power_available && power_ok)); + add_assoc_zval(return_value, "power", &power); +} + +static bool king_inference_gpu_runtime_bool_field(zval *source, const char *key) +{ + zval *value = zend_hash_str_find(Z_ARRVAL_P(source), key, strlen(key)); + + return value != NULL && zend_is_true(value); +} + +static zend_long king_inference_gpu_runtime_long_field(zval *source, const char *key) +{ + zval *value = zend_hash_str_find(Z_ARRVAL_P(source), key, strlen(key)); + + return value != NULL && Z_TYPE_P(value) == IS_LONG ? Z_LVAL_P(value) : 0; +} + +#include "gpu_runtime_admission_ready.inc" + +#include "../policy/gpu_runtime_kv_admission.inc" + +static void king_inference_gpu_runtime_status_from_compute( + const kg_high_perf_compute_ai_config_t *compute, + zval *return_value +) { + bool process_gpu_bindings_enabled = king_high_perf_compute_ai_config.gpu_bindings_enable; + bool config_gpu_bindings_enabled = compute->gpu_bindings_enable; + bool gpu_enabled = process_gpu_bindings_enabled + && config_gpu_bindings_enabled + && compute->inference_gpu_max_gpu_layers > 0; + + king_inference_gpu_runtime_status_array( + return_value, + gpu_enabled, + process_gpu_bindings_enabled, + config_gpu_bindings_enabled, + compute->gpu_default_backend, + compute->inference_gpu_model_artifact, + compute->inference_gpu_max_gpu_layers, + compute->inference_gpu_vram_reserve_mb, + compute->inference_gpu_min_free_vram_mb, + compute->inference_gpu_allow_system_ram_offload, + compute->inference_gpu_system_ram_offload_max_mb, + compute->inference_gpu_system_ram_offload_min_free_mb, + compute->inference_gpu_thermal_sensor_path, + compute->inference_gpu_thermal_sensor_command, + compute->inference_gpu_thermal_max_temperature_c, + compute->inference_gpu_thermal_check_interval_sec, + compute->inference_gpu_allow_unmonitored, + compute->inference_gpu_power_sensor_command, + compute->inference_gpu_power_max_watts, + compute->inference_gpu_power_check_interval_sec + ); +} + +static void king_inference_gpu_runtime_status_from_model( + king_inference_model_object *model, + zval *return_value +) { + zval *gpu = king_inference_array_find(&model->config, "gpu"); + zval *thermal = gpu != NULL && Z_TYPE_P(gpu) == IS_ARRAY + ? king_inference_array_find(gpu, "thermal") + : NULL; + zval *sensor_path = thermal != NULL && Z_TYPE_P(thermal) == IS_ARRAY + ? king_inference_array_find(thermal, "sensor_path") + : NULL; + zval *sensor_command = thermal != NULL && Z_TYPE_P(thermal) == IS_ARRAY + ? king_inference_array_find(thermal, "sensor_command") + : NULL; + zval *power = gpu != NULL && Z_TYPE_P(gpu) == IS_ARRAY + ? king_inference_array_find(gpu, "power") + : NULL; + zval *power_sensor_command = power != NULL && Z_TYPE_P(power) == IS_ARRAY + ? king_inference_array_find(power, "sensor_command") + : NULL; + const char *thermal_sensor_path = sensor_path != NULL && Z_TYPE_P(sensor_path) == IS_STRING + ? Z_STRVAL_P(sensor_path) + : ""; + const char *thermal_sensor_command = sensor_command != NULL && Z_TYPE_P(sensor_command) == IS_STRING + ? Z_STRVAL_P(sensor_command) + : ""; + const char *power_sensor_command_config = power_sensor_command != NULL && Z_TYPE_P(power_sensor_command) == IS_STRING + ? Z_STRVAL_P(power_sensor_command) + : king_high_perf_compute_ai_config.inference_gpu_power_sensor_command; + double max_temperature_c = king_inference_gpu_thermal_max_temperature_c(&model->config); + zend_long thermal_check_interval_sec = thermal != NULL && Z_TYPE_P(thermal) == IS_ARRAY + ? king_inference_long_from_array( + thermal, + "check_interval_seconds", + king_high_perf_compute_ai_config.inference_gpu_thermal_check_interval_sec + ) + : king_high_perf_compute_ai_config.inference_gpu_thermal_check_interval_sec; + zend_long vram_reserve_mb = king_inference_vram_reserve_mb(&model->config); + zend_long min_free_vram_mb = king_inference_min_free_vram_mb(&model->config); + bool allow_system_ram_offload = gpu != NULL + && Z_TYPE_P(gpu) == IS_ARRAY + && king_inference_bool_from_array( + gpu, + "allow_system_ram_offload", + king_high_perf_compute_ai_config.inference_gpu_allow_system_ram_offload + ); + zend_long system_ram_offload_max_mb = gpu != NULL && Z_TYPE_P(gpu) == IS_ARRAY + ? king_inference_long_from_array( + gpu, + "system_ram_offload_max_mb", + king_high_perf_compute_ai_config.inference_gpu_system_ram_offload_max_mb + ) + : king_high_perf_compute_ai_config.inference_gpu_system_ram_offload_max_mb; + zend_long system_ram_offload_min_free_mb = gpu != NULL && Z_TYPE_P(gpu) == IS_ARRAY + ? king_inference_long_from_array( + gpu, + "system_ram_offload_min_free_mb", + king_high_perf_compute_ai_config.inference_gpu_system_ram_offload_min_free_mb + ) + : king_high_perf_compute_ai_config.inference_gpu_system_ram_offload_min_free_mb; + bool allow_unmonitored = thermal != NULL + && Z_TYPE_P(thermal) == IS_ARRAY + && king_inference_bool_from_array(thermal, "allow_unmonitored_gpu", false); + double max_power_watts = power != NULL && Z_TYPE_P(power) == IS_ARRAY + ? king_inference_double_from_array( + power, + "max_watts", + king_high_perf_compute_ai_config.inference_gpu_power_max_watts + ) + : king_high_perf_compute_ai_config.inference_gpu_power_max_watts; + zend_long power_check_interval_sec = power != NULL && Z_TYPE_P(power) == IS_ARRAY + ? king_inference_long_from_array( + power, + "check_interval_seconds", + king_high_perf_compute_ai_config.inference_gpu_power_check_interval_sec + ) + : king_high_perf_compute_ai_config.inference_gpu_power_check_interval_sec; + + king_inference_gpu_runtime_status_array( + return_value, + king_inference_gpu_enabled(&model->config), + king_high_perf_compute_ai_config.gpu_bindings_enable, + true, + king_high_perf_compute_ai_config.gpu_default_backend, + model->artifact_path != NULL ? ZSTR_VAL(model->artifact_path) : "", + king_inference_max_gpu_layers(&model->config), + vram_reserve_mb, + min_free_vram_mb, + allow_system_ram_offload, + system_ram_offload_max_mb, + system_ram_offload_min_free_mb, + thermal_sensor_path, + thermal_sensor_command, + max_temperature_c, + thermal_check_interval_sec, + allow_unmonitored, + power_sensor_command_config != NULL ? power_sensor_command_config : "", + max_power_watts, + power_check_interval_sec + ); + king_inference_gpu_runtime_add_kv_cache_estimate(return_value, model); + king_inference_cuda_context_apply_status(model, return_value); + king_inference_cuda_device_allocator_apply_status(model, return_value); + king_inference_cuda_weight_upload_apply_status(model, return_value); + king_inference_cuda_quantized_matvec_apply_status(model, return_value); + king_inference_cuda_embedding_row_apply_status(model, return_value); + king_inference_cuda_device_vector_ops_apply_status(model, return_value); + king_inference_cuda_device_kv_cache_apply_status(model, return_value); + king_inference_cuda_rms_norm_apply_status(model, return_value); + king_inference_cuda_rope_apply_status(model, return_value); + king_inference_cuda_attention_scores_apply_status(model, return_value); + king_inference_cuda_attention_softmax_apply_status(model, return_value); + king_inference_cuda_attention_values_apply_status(model, return_value); + king_inference_cuda_ffn_swiglu_apply_status(model, return_value); + king_inference_cuda_output_projection_apply_status(model, return_value); + king_inference_cuda_logits_readback_apply_status(model, return_value); + king_inference_cuda_decoder_graph_executor_apply_status(model, return_value); + king_inference_cuda_decoder_prompt_loop_apply_status(model, return_value); + king_inference_gpu_replace_return_decoder_blockers(model, return_value); +} + +static zend_string *king_inference_gpu_runtime_reason_string(zval *runtime) +{ + zval *reasons; + zval *entry; + smart_str buffer = {0}; + bool first = true; + + reasons = zend_hash_str_find(Z_ARRVAL_P(runtime), "refusal_reasons", sizeof("refusal_reasons") - 1); + if (reasons == NULL || Z_TYPE_P(reasons) != IS_ARRAY) { + zval *reason = zend_hash_str_find(Z_ARRVAL_P(runtime), "reason", sizeof("reason") - 1); + if (reason != NULL && Z_TYPE_P(reason) == IS_STRING && Z_STRLEN_P(reason) > 0) { + return zend_string_copy(Z_STR_P(reason)); + } + return zend_string_init("unknown", sizeof("unknown") - 1, 0); + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(reasons), entry) { + if (entry == NULL || Z_TYPE_P(entry) != IS_STRING || Z_STRLEN_P(entry) == 0) { + continue; + } + if (!first) { + smart_str_appendc(&buffer, ','); + } + smart_str_append(&buffer, Z_STR_P(entry)); + first = false; + } ZEND_HASH_FOREACH_END(); + + if (first) { + return zend_string_init("unknown", sizeof("unknown") - 1, 0); + } + + smart_str_0(&buffer); + return buffer.s; +} + +static zend_result king_inference_gpu_runtime_require_config_ready( + king_inference_model_object *model, + const char *function_name +) { + zval runtime; + zval *config_ready; + zend_string *reasons; + + ZVAL_UNDEF(&runtime); + king_inference_gpu_runtime_status_from_model(model, &runtime); + config_ready = zend_hash_str_find(Z_ARRVAL(runtime), "config_ready", sizeof("config_ready") - 1); + if (config_ready != NULL && zend_is_true(config_ready)) { + zval_ptr_dtor(&runtime); + return SUCCESS; + } + + reasons = king_inference_gpu_runtime_reason_string(&runtime); + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "%s() refused GPU inference because CUDA admission is not ready: %s. King will not silently fall back to CPU for a GPU profile.", + function_name, + ZSTR_VAL(reasons) + ); + zend_string_release(reasons); + zval_ptr_dtor(&runtime); + return FAILURE; +} + +static zend_result king_inference_gpu_runtime_require_generation_ready( + king_inference_model_object *model, + const char *function_name +) { + zval runtime; + zval *generation_ready; + zend_string *reasons; + + ZVAL_UNDEF(&runtime); + king_inference_gpu_runtime_status_from_model(model, &runtime); + generation_ready = zend_hash_str_find(Z_ARRVAL(runtime), "generation_ready", sizeof("generation_ready") - 1); + if (generation_ready != NULL && zend_is_true(generation_ready)) { + zval_ptr_dtor(&runtime); + return SUCCESS; + } + + reasons = king_inference_gpu_runtime_reason_string(&runtime); + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "%s() refused GPU inference because CUDA generation is not ready: %s. King will not silently fall back to CPU for a GPU profile.", + function_name, + ZSTR_VAL(reasons) + ); + zend_string_release(reasons); + zval_ptr_dtor(&runtime); + return FAILURE; +} diff --git a/extension/src/inference/cuda/runtime/weights/cuda_weight_upload.inc b/extension/src/inference/cuda/runtime/weights/cuda_weight_upload.inc new file mode 100644 index 000000000..95aece353 --- /dev/null +++ b/extension/src/inference/cuda/runtime/weights/cuda_weight_upload.inc @@ -0,0 +1,560 @@ +/* + * Host-to-device weight upload for native King GPU inference. + */ + +struct _king_inference_cuda_weight_upload { + zend_string *name; + king_inference_cuda_device_ptr device_ptr; + size_t bytes; + zend_ulong layer; + char role[48]; + bool has_layer; + king_inference_cuda_weight_upload *next; +}; + +typedef int (*king_inference_cuda_upload_cu_memcpy_htod_fn)( + king_inference_cuda_device_ptr dst, + const void *src, + size_t bytes +); + +static void king_inference_cuda_weight_upload_reset_fields(king_inference_model_object *model) +{ + model->cuda_weight_uploads = NULL; + model->cuda_weight_upload_result = 0; + model->cuda_weight_upload_error[0] = '\0'; + model->cuda_weight_required_count = 0; + model->cuda_weight_resolved_count = 0; + model->cuda_weight_uploaded_count = 0; + model->cuda_weight_duplicate_count = 0; + model->cuda_weight_failed_count = 0; + model->cuda_weight_uploaded_bytes = 0; + model->cuda_weight_cache_hits = 0; + model->cuda_weight_cache_misses = 0; + model->cuda_weight_cache_stores = 0; + model->cuda_weight_upload_attempted = false; + model->cuda_weight_upload_complete = false; + model->cuda_weight_cache_initialized = false; + model->cuda_weight_cache_ready = false; +} + +static void king_inference_cuda_weight_upload_set_error( + king_inference_model_object *model, + int result, + const char *message +) { + if (message == NULL || message[0] == '\0') { + message = "unknown_cuda_weight_upload_error"; + } + + model->cuda_weight_upload_result = result; + snprintf(model->cuda_weight_upload_error, sizeof(model->cuda_weight_upload_error), "%s", message); +} + +static void king_inference_cuda_weight_upload_release(king_inference_model_object *model) +{ + king_inference_cuda_weight_upload *upload = model->cuda_weight_uploads; + king_inference_cuda_weight_upload *next; + + if (model->cuda_weight_cache_initialized) { + zend_hash_destroy(&model->cuda_weight_cache); + model->cuda_weight_cache_initialized = false; + } + + while (upload != NULL) { + next = upload->next; + if (upload->name != NULL) { + zend_string_release(upload->name); + } + efree(upload); + upload = next; + } + + king_inference_cuda_weight_upload_reset_fields(model); +} + +static void king_inference_cuda_weight_cache_ensure(king_inference_model_object *model) +{ + if (model->cuda_weight_cache_initialized) { + return; + } + + zend_hash_init(&model->cuda_weight_cache, 128, NULL, NULL, 0); + model->cuda_weight_cache_initialized = true; +} + +static king_inference_cuda_weight_upload *king_inference_cuda_weight_cache_find( + king_inference_model_object *model, + zend_string *name, + bool count_access +) { + zval *entry; + + if (!model->cuda_weight_cache_initialized) { + if (count_access) { + model->cuda_weight_cache_misses++; + } + return NULL; + } + + entry = zend_hash_find(&model->cuda_weight_cache, name); + if (entry == NULL || Z_TYPE_P(entry) != IS_PTR) { + if (count_access) { + model->cuda_weight_cache_misses++; + } + return NULL; + } + + if (count_access) { + model->cuda_weight_cache_hits++; + } + return (king_inference_cuda_weight_upload *) Z_PTR_P(entry); +} + +static bool king_inference_cuda_weight_cache_store( + king_inference_model_object *model, + king_inference_cuda_weight_upload *upload +) { + zval entry; + + if (upload == NULL || upload->name == NULL) { + king_inference_cuda_weight_upload_set_error(model, -1, "cuda_weight_cache_entry_invalid"); + return false; + } + + king_inference_cuda_weight_cache_ensure(model); + ZVAL_PTR(&entry, upload); + if (zend_hash_update(&model->cuda_weight_cache, upload->name, &entry) == NULL) { + king_inference_cuda_weight_upload_set_error(model, -1, "cuda_weight_cache_store_failed"); + return false; + } + + model->cuda_weight_cache_stores++; + return true; +} + +static bool king_inference_cuda_weight_upload_symbol( + king_inference_model_object *model, + const char *primary_name, + const char *fallback_name, + void **symbol +) { + *symbol = NULL; + if (model->cuda_driver_handle == NULL) { + king_inference_cuda_weight_upload_set_error(model, -1, "cuda_driver_handle_unavailable"); + return false; + } + + if (primary_name != NULL) { + *symbol = dlsym(model->cuda_driver_handle, primary_name); + } + if (*symbol == NULL && fallback_name != NULL) { + *symbol = dlsym(model->cuda_driver_handle, fallback_name); + } + if (*symbol != NULL) { + return true; + } + + king_inference_cuda_weight_upload_set_error( + model, + -1, + primary_name != NULL ? primary_name : fallback_name + ); + return false; +} + +static bool king_inference_cuda_weight_already_uploaded( + king_inference_model_object *model, + zend_string *name +) { + return king_inference_cuda_weight_cache_find(model, name, true) != NULL; +} + +static bool king_inference_cuda_weight_tensor_source( + king_inference_model_object *model, + zend_string *name, + const void **source, + size_t *bytes +) { + king_inference_tensor_type_info type_info; + zend_ulong type; + zend_ulong elements; + zend_ulong relative_offset; + zend_ulong blocks; + zend_ulong byte_length; + zend_ulong absolute_offset; + zend_ulong byte_end; + zval *descriptor = king_inference_tensor_index_descriptor(model, name); + + *source = NULL; + *bytes = 0; + if (descriptor == NULL + || !model->native_map_loaded + || model->native_map == NULL + || model->native_map_size == 0) { + king_inference_cuda_weight_upload_set_error(model, -1, "native_tensor_source_unavailable"); + return false; + } + if (!king_inference_tensor_descriptor_ulong(descriptor, "type", sizeof("type") - 1, &type) + || !king_inference_tensor_descriptor_ulong(descriptor, "elements", sizeof("elements") - 1, &elements) + || !king_inference_tensor_descriptor_ulong( + descriptor, + "relative_offset", + sizeof("relative_offset") - 1, + &relative_offset + ) + || !king_inference_tensor_type_lookup(type, &type_info) + || type_info.block_size == 0) { + king_inference_cuda_weight_upload_set_error(model, -1, "native_tensor_descriptor_invalid"); + return false; + } + + blocks = elements == 0 ? 0 : 1 + ((elements - 1) / type_info.block_size); + if (blocks == 0 + || !king_inference_tensor_mul_checked(blocks, type_info.type_size, &byte_length) + || !king_inference_tensor_add_checked(model->gguf.tensor_data_offset, relative_offset, &absolute_offset) + || !king_inference_tensor_add_checked(absolute_offset, byte_length, &byte_end) + || byte_end > model->gguf.file_size + || byte_end > (zend_ulong) model->native_map_size) { + king_inference_cuda_weight_upload_set_error(model, -1, "native_tensor_bounds_invalid"); + return false; + } + + *source = ((const unsigned char *) model->native_map) + absolute_offset; + *bytes = (size_t) byte_length; + return true; +} + +static bool king_inference_cuda_weight_upload_tensor( + king_inference_model_object *model, + const char *role, + bool has_layer, + zend_ulong layer, + zend_string *name +) { + king_inference_cuda_upload_cu_memcpy_htod_fn cu_memcpy_htod; + king_inference_cuda_weight_upload *upload; + king_inference_cuda_device_ptr device_ptr = 0; + const void *source; + size_t bytes; + int result; + + if (king_inference_cuda_weight_already_uploaded(model, name)) { + model->cuda_weight_duplicate_count++; + return true; + } + if (!king_inference_cuda_weight_tensor_source(model, name, &source, &bytes)) { + model->cuda_weight_failed_count++; + return false; + } + if (model->cuda_weight_uploaded_bytes > ((size_t) -1) - bytes) { + king_inference_cuda_weight_upload_set_error(model, -1, "cuda_weight_upload_counter_overflow"); + model->cuda_weight_failed_count++; + return false; + } + if (!king_inference_cuda_device_allocator_set_current(model)) { + king_inference_cuda_weight_upload_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_context_current_failed" + ); + model->cuda_weight_failed_count++; + return false; + } + if (!king_inference_cuda_weight_upload_symbol( + model, + "cuMemcpyHtoD_v2", + "cuMemcpyHtoD", + (void **) &cu_memcpy_htod + )) { + model->cuda_weight_failed_count++; + return false; + } + if (king_inference_cuda_device_allocator_alloc(model, bytes, &device_ptr) != SUCCESS) { + king_inference_cuda_weight_upload_set_error( + model, + model->cuda_device_allocator_result, + model->cuda_device_allocator_error[0] != '\0' + ? model->cuda_device_allocator_error + : "cuda_device_allocation_failed" + ); + model->cuda_weight_failed_count++; + return false; + } + + result = cu_memcpy_htod(device_ptr, source, bytes); + if (result != 0) { + (void) king_inference_cuda_device_allocator_free(model, device_ptr); + king_inference_cuda_weight_upload_set_error(model, result, "cuMemcpyHtoD_failed"); + model->cuda_weight_failed_count++; + return false; + } + + upload = emalloc(sizeof(*upload)); + upload->name = zend_string_copy(name); + upload->device_ptr = device_ptr; + upload->bytes = bytes; + upload->layer = layer; + upload->has_layer = has_layer; + snprintf(upload->role, sizeof(upload->role), "%s", role != NULL ? role : "weight"); + if (!king_inference_cuda_weight_cache_store(model, upload)) { + (void) king_inference_cuda_device_allocator_free(model, device_ptr); + zend_string_release(upload->name); + efree(upload); + model->cuda_weight_failed_count++; + return false; + } + upload->next = model->cuda_weight_uploads; + model->cuda_weight_uploads = upload; + model->cuda_weight_uploaded_count++; + model->cuda_weight_uploaded_bytes += bytes; + if (model->cuda_weight_failed_count == 0) { + model->cuda_weight_upload_result = 0; + model->cuda_weight_upload_error[0] = '\0'; + } + return true; +} + +static void king_inference_cuda_weight_upload_resolved( + king_inference_model_object *model, + const char *role, + bool has_layer, + zend_ulong layer, + zend_result resolved, + zend_string *name, + const char *status +) { + model->cuda_weight_required_count++; + if (resolved != SUCCESS || name == NULL) { + model->cuda_weight_failed_count++; + king_inference_cuda_weight_upload_set_error( + model, + -1, + status != NULL ? status : "required_weight_not_resolved" + ); + return; + } + + model->cuda_weight_resolved_count++; + (void) king_inference_cuda_weight_upload_tensor(model, role, has_layer, layer, name); +} + +static void king_inference_cuda_weight_upload_optional_layer_tensor( + king_inference_model_object *model, + const char *role, + zend_ulong layer, + const char *suffix +) { + char name_buffer[96]; + zend_string *name; + zval *descriptor; + + if (snprintf(name_buffer, sizeof(name_buffer), "blk.%lu.%s", (unsigned long) layer, suffix) >= (int) sizeof(name_buffer)) { + return; + } + + name = zend_string_init(name_buffer, strlen(name_buffer), 0); + descriptor = king_inference_tensor_index_descriptor(model, name); + if (descriptor != NULL) { + model->cuda_weight_required_count++; + model->cuda_weight_resolved_count++; + (void) king_inference_cuda_weight_upload_tensor(model, role, true, layer, name); + } + zend_string_release(name); +} + +static void king_inference_cuda_weight_upload_layer( + king_inference_model_object *model, + zend_ulong layer +) { + zend_string *name = NULL; + const char *status = NULL; + zend_result resolved; + + resolved = king_inference_resolve_rms_norm_layer_name( + model, + KING_INFERENCE_RMS_NORM_ATTENTION, + layer, + &name, + NULL, + &status + ); + king_inference_cuda_weight_upload_resolved(model, "rms_norm.attention", true, layer, resolved, name, status); + if (name != NULL) zend_string_release(name); + king_inference_cuda_weight_upload_optional_layer_tensor(model, "rms_norm.attention_query", layer, "attn_q_norm.weight"); + king_inference_cuda_weight_upload_optional_layer_tensor(model, "rms_norm.attention_key", layer, "attn_k_norm.weight"); + king_inference_cuda_weight_upload_optional_layer_tensor(model, "rms_norm.post_attention", layer, "post_attention_norm.weight"); + + for (int role = KING_INFERENCE_ATTENTION_QUERY; role <= KING_INFERENCE_ATTENTION_OUTPUT; role++) { + const char *upload_role = king_inference_attention_role_keys[role]; + + name = NULL; + status = NULL; + resolved = king_inference_resolve_attention_tensor_name( + model, + (king_inference_attention_role) role, + layer, + &name, + NULL, + &status + ); + king_inference_cuda_weight_upload_resolved(model, upload_role, true, layer, resolved, name, status); + if (name != NULL) zend_string_release(name); + } + + name = NULL; + status = NULL; + resolved = king_inference_resolve_rms_norm_layer_name( + model, + KING_INFERENCE_RMS_NORM_FEED_FORWARD, + layer, + &name, + NULL, + &status + ); + king_inference_cuda_weight_upload_resolved(model, "rms_norm.feed_forward", true, layer, resolved, name, status); + if (name != NULL) zend_string_release(name); + king_inference_cuda_weight_upload_optional_layer_tensor(model, "rms_norm.post_feed_forward", layer, "post_ffw_norm.weight"); + + for (int role = KING_INFERENCE_FFN_GATE; role <= KING_INFERENCE_FFN_DOWN; role++) { + char upload_role[32]; + + name = NULL; + status = NULL; + resolved = king_inference_resolve_ffn_tensor_name( + model, + (king_inference_ffn_role) role, + layer, + &name, + NULL, + &status + ); + snprintf(upload_role, sizeof(upload_role), "ffn.%s", king_inference_ffn_role_keys[role]); + king_inference_cuda_weight_upload_resolved(model, upload_role, true, layer, resolved, name, status); + if (name != NULL) zend_string_release(name); + } +} + +static void king_inference_cuda_weight_upload_required( + king_inference_model_object *model, + king_inference_backend_kind backend_kind +) { + zend_ulong block_count = model->gguf.architecture_params[KING_INFERENCE_ARCH_BLOCK_COUNT]; + zend_string *name = NULL; + const char *status = NULL; + zend_result resolved; + bool tied_embedding = false; + zend_ulong layer; + + king_inference_cuda_weight_upload_release(model); + if (backend_kind != KING_INFERENCE_BACKEND_KING_NATIVE_GPU + || !king_inference_gpu_enabled(&model->config)) { + return; + } + + model->cuda_weight_upload_attempted = true; + if (!model->cuda_device_allocator_available) { + king_inference_cuda_weight_upload_set_error(model, -1, "cuda_device_allocator_unavailable"); + return; + } + + resolved = king_inference_resolve_token_embedding_tensor_name(model, NULL, &name, NULL, &status); + king_inference_cuda_weight_upload_resolved(model, "token_embedding", false, 0, resolved, name, status); + if (name != NULL) zend_string_release(name); + + name = NULL; + status = NULL; + resolved = king_inference_resolve_output_projection_tensor_name( + model, + NULL, + &name, + NULL, + &status, + &tied_embedding + ); + king_inference_cuda_weight_upload_resolved(model, "output_projection", false, 0, resolved, name, status); + if (name != NULL) zend_string_release(name); + + name = NULL; + status = NULL; + resolved = king_inference_resolve_rms_norm_final_name(model, &name, NULL, &status); + king_inference_cuda_weight_upload_resolved(model, "rms_norm.final", false, 0, resolved, name, status); + if (name != NULL) zend_string_release(name); + + if (block_count == 0) { + model->cuda_weight_failed_count++; + king_inference_cuda_weight_upload_set_error(model, -1, "decoder_block_count_unavailable"); + } + for (layer = 0; layer < block_count; layer++) { + king_inference_cuda_weight_upload_layer(model, layer); + } + + model->cuda_weight_upload_complete = model->cuda_weight_required_count > 0 + && model->cuda_weight_failed_count == 0 + && model->cuda_weight_required_count + == model->cuda_weight_uploaded_count + model->cuda_weight_duplicate_count; + model->cuda_weight_cache_ready = model->cuda_weight_upload_complete + && model->cuda_weight_cache_initialized + && model->cuda_weight_cache_stores == model->cuda_weight_uploaded_count; +} + +static void king_inference_cuda_weight_upload_add_status( + king_inference_model_object *model, + zval *return_value +) { + zval upload; + + array_init(&upload); + add_assoc_bool(&upload, "attempted", model->cuda_weight_upload_attempted); + add_assoc_bool(&upload, "complete", model->cuda_weight_upload_complete); + add_assoc_long(&upload, "required_tensors", (zend_long) model->cuda_weight_required_count); + add_assoc_long(&upload, "resolved_tensors", (zend_long) model->cuda_weight_resolved_count); + add_assoc_long(&upload, "uploaded_tensors", (zend_long) model->cuda_weight_uploaded_count); + add_assoc_long(&upload, "duplicate_tensors", (zend_long) model->cuda_weight_duplicate_count); + add_assoc_long(&upload, "failed_tensors", (zend_long) model->cuda_weight_failed_count); + add_assoc_long(&upload, "uploaded_bytes", (zend_long) model->cuda_weight_uploaded_bytes); + add_assoc_bool(&upload, "cache_ready", model->cuda_weight_cache_ready); + add_assoc_long(&upload, "cache_entries", (zend_long) model->cuda_weight_cache_stores); + add_assoc_long(&upload, "cache_hits", (zend_long) model->cuda_weight_cache_hits); + add_assoc_long(&upload, "cache_misses", (zend_long) model->cuda_weight_cache_misses); + add_assoc_long(&upload, "result", model->cuda_weight_upload_result); + add_assoc_string(&upload, "error", model->cuda_weight_upload_error); + add_assoc_zval(return_value, "required_weight_upload", &upload); +} + +static void king_inference_cuda_weight_upload_apply_status( + king_inference_model_object *model, + zval *return_value +) { + zval *reason; + bool reason_can_be_upload; + + king_inference_cuda_weight_upload_add_status(model, return_value); + if (!model->cuda_weight_upload_attempted + || model->cuda_weight_upload_complete + || !model->cuda_device_allocator_available) { + return; + } + + add_assoc_bool(return_value, "config_ready", false); + + reason = zend_hash_str_find(Z_ARRVAL_P(return_value), "reason", sizeof("reason") - 1); + reason_can_be_upload = reason == NULL + || Z_TYPE_P(reason) != IS_STRING + || zend_string_equals_literal(Z_STR_P(reason), "gpu_ready") + || zend_string_equals_literal(Z_STR_P(reason), "gpu_decoder_kernel_not_implemented"); + if (reason_can_be_upload) { + king_inference_cuda_context_prepend_return_reason( + return_value, + "gpu_required_weight_upload_incomplete" + ); + add_assoc_string(return_value, "reason", "gpu_required_weight_upload_incomplete"); + } else { + king_inference_gpu_status_append_return_reason( + return_value, + "gpu_required_weight_upload_incomplete" + ); + } +} diff --git a/extension/src/inference/gguf/loader/gguf_loader.inc b/extension/src/inference/gguf/loader/gguf_loader.inc new file mode 100644 index 000000000..e87f3b698 --- /dev/null +++ b/extension/src/inference/gguf/loader/gguf_loader.inc @@ -0,0 +1,810 @@ +#define KING_GGUF_TYPE_UINT8 0 +#define KING_GGUF_TYPE_INT8 1 +#define KING_GGUF_TYPE_UINT16 2 +#define KING_GGUF_TYPE_INT16 3 +#define KING_GGUF_TYPE_UINT32 4 +#define KING_GGUF_TYPE_INT32 5 +#define KING_GGUF_TYPE_FLOAT32 6 +#define KING_GGUF_TYPE_BOOL 7 +#define KING_GGUF_TYPE_STRING 8 +#define KING_GGUF_TYPE_ARRAY 9 +#define KING_GGUF_TYPE_UINT64 10 +#define KING_GGUF_TYPE_INT64 11 +#define KING_GGUF_TYPE_FLOAT64 12 + +static zend_ulong king_inference_read_le32(const unsigned char *bytes) +{ + return ((zend_ulong) bytes[0]) + | ((zend_ulong) bytes[1] << 8) + | ((zend_ulong) bytes[2] << 16) + | ((zend_ulong) bytes[3] << 24); +} + +static zend_ulong king_inference_read_le64(const unsigned char *bytes) +{ + return ((zend_ulong) bytes[0]) + | ((zend_ulong) bytes[1] << 8) + | ((zend_ulong) bytes[2] << 16) + | ((zend_ulong) bytes[3] << 24) + | ((zend_ulong) bytes[4] << 32) + | ((zend_ulong) bytes[5] << 40) + | ((zend_ulong) bytes[6] << 48) + | ((zend_ulong) bytes[7] << 56); +} + +static void king_inference_gguf_metadata_release(king_inference_gguf_metadata *metadata) +{ + if (metadata->architecture != NULL) { + zend_string_release(metadata->architecture); + } + if (metadata->general_name != NULL) { + zend_string_release(metadata->general_name); + } + if (metadata->tokenizer_model != NULL) { + zend_string_release(metadata->tokenizer_model); + } + + memset(metadata, 0, sizeof(*metadata)); +} + +static zend_result king_inference_gguf_read_exact( + FILE *file, + void *buffer, + size_t length, + zend_string *path, + const char *what +) { + if (length == 0) { + return SUCCESS; + } + + if (fread(buffer, 1, length, file) != length) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "Model artifact '%s' ended while reading GGUF %s.", + ZSTR_VAL(path), + what + ); + return FAILURE; + } + + return SUCCESS; +} + +static zend_result king_inference_gguf_read_u32( + FILE *file, + zend_string *path, + const char *what, + zend_ulong *value +) { + unsigned char bytes[4]; + + if (king_inference_gguf_read_exact(file, bytes, sizeof(bytes), path, what) != SUCCESS) { + return FAILURE; + } + + *value = king_inference_read_le32(bytes); + return SUCCESS; +} + +static zend_result king_inference_gguf_read_u64( + FILE *file, + zend_string *path, + const char *what, + zend_ulong *value +) { + unsigned char bytes[8]; + + if (king_inference_gguf_read_exact(file, bytes, sizeof(bytes), path, what) != SUCCESS) { + return FAILURE; + } + + *value = king_inference_read_le64(bytes); + return SUCCESS; +} + +static zend_result king_inference_gguf_skip_bytes( + FILE *file, + zend_ulong file_size, + zend_ulong length, + zend_string *path, + const char *what +) { + off_t position = ftello(file); + zend_ulong remaining = length; + + if (position < 0 || (zend_ulong) position > file_size || length > file_size - (zend_ulong) position) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "Model artifact '%s' has an invalid GGUF %s boundary.", + ZSTR_VAL(path), + what + ); + return FAILURE; + } + + while (remaining > 0) { + off_t chunk = remaining > 1073741824UL ? 1073741824 : (off_t) remaining; + if (fseeko(file, chunk, SEEK_CUR) != 0) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "Model artifact '%s' could not skip GGUF %s.", + ZSTR_VAL(path), + what + ); + return FAILURE; + } + remaining -= (zend_ulong) chunk; + } + + return SUCCESS; +} + +static zend_result king_inference_gguf_read_string( + FILE *file, + zend_ulong file_size, + zend_string *path, + const char *what, + zend_string **value +) { + zend_ulong length; + off_t position; + + *value = NULL; + if (king_inference_gguf_read_u64(file, path, what, &length) != SUCCESS) { + return FAILURE; + } + + position = ftello(file); + if (position < 0 + || length > (zend_ulong) ZEND_LONG_MAX + || (zend_ulong) position > file_size + || length > file_size - (zend_ulong) position) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "Model artifact '%s' has an invalid GGUF %s string length.", + ZSTR_VAL(path), + what + ); + return FAILURE; + } + + *value = zend_string_alloc((size_t) length, 0); + if (king_inference_gguf_read_exact(file, ZSTR_VAL(*value), (size_t) length, path, what) != SUCCESS) { + zend_string_release(*value); + *value = NULL; + return FAILURE; + } + ZSTR_VAL(*value)[length] = '\0'; + + return SUCCESS; +} + +static size_t king_inference_gguf_scalar_size(zend_ulong type) +{ + switch (type) { + case KING_GGUF_TYPE_UINT8: + case KING_GGUF_TYPE_INT8: + case KING_GGUF_TYPE_BOOL: + return 1; + case KING_GGUF_TYPE_UINT16: + case KING_GGUF_TYPE_INT16: + return 2; + case KING_GGUF_TYPE_UINT32: + case KING_GGUF_TYPE_INT32: + case KING_GGUF_TYPE_FLOAT32: + return 4; + case KING_GGUF_TYPE_UINT64: + case KING_GGUF_TYPE_INT64: + case KING_GGUF_TYPE_FLOAT64: + return 8; + } + + return 0; +} + +static zend_result king_inference_gguf_skip_array_payload( + FILE *file, + zend_ulong file_size, + zend_ulong array_type, + zend_ulong array_count, + zend_string *path, + zend_ulong depth +); + +static zend_result king_inference_gguf_skip_value( + FILE *file, + zend_ulong file_size, + zend_ulong type, + zend_string *path, + zend_ulong depth +) { + zend_ulong array_type; + zend_ulong array_count; + size_t scalar_size; + + scalar_size = king_inference_gguf_scalar_size(type); + if (scalar_size > 0) { + return king_inference_gguf_skip_bytes(file, file_size, (zend_ulong) scalar_size, path, "scalar value"); + } + + if (type == KING_GGUF_TYPE_STRING) { + zend_string *ignored; + if (king_inference_gguf_read_string(file, file_size, path, "string value", &ignored) != SUCCESS) { + return FAILURE; + } + zend_string_release(ignored); + return SUCCESS; + } + + if (type == KING_GGUF_TYPE_ARRAY) { + if (depth > 4) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "Model artifact '%s' has nested GGUF arrays that are too deep.", ZSTR_VAL(path)); + return FAILURE; + } + if (king_inference_gguf_read_u32(file, path, "array type", &array_type) != SUCCESS + || king_inference_gguf_read_u64(file, path, "array count", &array_count) != SUCCESS) { + return FAILURE; + } + + return king_inference_gguf_skip_array_payload(file, file_size, array_type, array_count, path, depth); + } + + zend_throw_exception_ex(king_ce_validation_exception, 0, "Model artifact '%s' contains unsupported GGUF value type %lu.", ZSTR_VAL(path), type); + return FAILURE; +} + +static zend_result king_inference_gguf_skip_array_payload( + FILE *file, + zend_ulong file_size, + zend_ulong array_type, + zend_ulong array_count, + zend_string *path, + zend_ulong depth +) { + size_t scalar_size = king_inference_gguf_scalar_size(array_type); + zend_ulong i; + + if (scalar_size > 0) { + if (array_count > ZEND_ULONG_MAX / (zend_ulong) scalar_size) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "Model artifact '%s' has an oversized GGUF array.", ZSTR_VAL(path)); + return FAILURE; + } + return king_inference_gguf_skip_bytes( + file, + file_size, + array_count * (zend_ulong) scalar_size, + path, + "array payload" + ); + } + + for (i = 0; i < array_count; i++) { + if (king_inference_gguf_skip_value(file, file_size, array_type, path, depth + 1) != SUCCESS) { + return FAILURE; + } + } + + return SUCCESS; +} + +#include "../metadata/gguf_metadata_helpers.inc" +#include "../metadata/gguf_architecture_metadata.inc" + +static zend_result king_inference_gguf_read_metadata_value( + FILE *file, + zend_ulong file_size, + zend_string *path, + zend_string *key, + zend_ulong type, + king_inference_gguf_metadata *metadata, + zval *tokenizer_tokens, + zval *tokenizer_lookup, + zval *tokenizer_scores, + zval *tokenizer_types, + zval *tokenizer_merges +) { + zend_string *string_value; + zend_ulong array_type; + zend_ulong array_count; + zend_long numeric_value; + double floating_value; + bool architecture_numeric = false; + + if (type == KING_GGUF_TYPE_STRING) { + if (king_inference_gguf_read_string(file, file_size, path, "metadata value", &string_value) != SUCCESS) { + return FAILURE; + } + if (zend_string_equals_literal(key, "general.architecture")) { + king_inference_gguf_store_string(&metadata->architecture, string_value); + } else if (zend_string_equals_literal(key, "general.name")) { + king_inference_gguf_store_string(&metadata->general_name, string_value); + } else if (zend_string_equals_literal(key, "tokenizer.ggml.model")) { + king_inference_gguf_store_string(&metadata->tokenizer_model, string_value); + } else { + zend_string_release(string_value); + } + return SUCCESS; + } + + if (type == KING_GGUF_TYPE_ARRAY) { + if (king_inference_gguf_read_u32(file, path, "metadata array type", &array_type) != SUCCESS + || king_inference_gguf_read_u64(file, path, "metadata array count", &array_count) != SUCCESS) { + return FAILURE; + } + if (zend_string_equals_literal(key, "tokenizer.ggml.tokens")) { + metadata->tokenizer_token_count = array_count; + if (array_type == KING_GGUF_TYPE_STRING) { + if (king_inference_gguf_read_string_array( + file, + file_size, + array_count, + path, + "tokenizer token", + tokenizer_tokens, + tokenizer_lookup, + &metadata->tokenizer_max_token_bytes + ) != SUCCESS) { + return FAILURE; + } + metadata->tokenizer_tokens_loaded = true; + metadata->tokenizer_lookup_loaded = true; + return SUCCESS; + } + } else if (zend_string_equals_literal(key, "tokenizer.ggml.scores")) { + metadata->tokenizer_score_count = array_count; + if (array_type == KING_GGUF_TYPE_FLOAT32 || array_type == KING_GGUF_TYPE_FLOAT64) { + if (king_inference_gguf_read_double_array( + file, + file_size, + array_type, + array_count, + path, + tokenizer_scores + ) != SUCCESS) { + return FAILURE; + } + metadata->tokenizer_scores_loaded = true; + return SUCCESS; + } + } else if (zend_string_equals_literal(key, "tokenizer.ggml.token_type")) { + metadata->tokenizer_type_count = array_count; + if (array_type == KING_GGUF_TYPE_UINT8 + || array_type == KING_GGUF_TYPE_INT8 + || array_type == KING_GGUF_TYPE_UINT16 + || array_type == KING_GGUF_TYPE_INT16 + || array_type == KING_GGUF_TYPE_UINT32 + || array_type == KING_GGUF_TYPE_INT32 + || array_type == KING_GGUF_TYPE_UINT64 + || array_type == KING_GGUF_TYPE_INT64) { + if (king_inference_gguf_read_long_array( + file, + file_size, + array_type, + array_count, + path, + tokenizer_types + ) != SUCCESS) { + return FAILURE; + } + metadata->tokenizer_types_loaded = true; + return SUCCESS; + } + } else if (zend_string_equals_literal(key, "tokenizer.ggml.merges")) { + metadata->tokenizer_merge_count = array_count; + if (array_type == KING_GGUF_TYPE_STRING) { + if (king_inference_gguf_read_string_array( + file, + file_size, + array_count, + path, + "tokenizer merge", + tokenizer_merges, + NULL, + NULL + ) != SUCCESS) { + return FAILURE; + } + metadata->tokenizer_merges_loaded = true; + return SUCCESS; + } + } + + if (array_count == 0) { + return SUCCESS; + } + return king_inference_gguf_skip_array_payload(file, file_size, array_type, array_count, path, 0); + } + + if (zend_string_equals_literal(key, "general.file_type")) { + if (king_inference_gguf_read_numeric_long(file, file_size, type, path, &numeric_value) != SUCCESS) { + return FAILURE; + } + metadata->file_type = numeric_value; + return SUCCESS; + } + + if (zend_string_equals_literal(key, "tokenizer.ggml.bos_token_id")) { + if (king_inference_gguf_read_numeric_long(file, file_size, type, path, &numeric_value) != SUCCESS) { + return FAILURE; + } + metadata->tokenizer_bos_id = numeric_value; + return SUCCESS; + } + + if (zend_string_equals_literal(key, "tokenizer.ggml.eos_token_id")) { + if (king_inference_gguf_read_numeric_long(file, file_size, type, path, &numeric_value) != SUCCESS) { + return FAILURE; + } + metadata->tokenizer_eos_id = numeric_value; + return SUCCESS; + } + + if (zend_string_equals_literal(key, "tokenizer.ggml.unknown_token_id")) { + if (king_inference_gguf_read_numeric_long(file, file_size, type, path, &numeric_value) != SUCCESS) { + return FAILURE; + } + metadata->tokenizer_unknown_id = numeric_value; + return SUCCESS; + } + + if (zend_string_equals_literal(key, "tokenizer.ggml.padding_token_id")) { + if (king_inference_gguf_read_numeric_long(file, file_size, type, path, &numeric_value) != SUCCESS) { + return FAILURE; + } + metadata->tokenizer_pad_id = numeric_value; + return SUCCESS; + } + + if (king_inference_gguf_key_has_suffix(key, ".rope.freq_base") + || king_inference_gguf_key_has_suffix(key, ".rope.global.freq_base")) { + if (king_inference_gguf_read_numeric_double(file, file_size, type, path, &floating_value) != SUCCESS) { + return FAILURE; + } + if (floating_value > 0.0 && isfinite(floating_value)) { + metadata->rope_freq_base = floating_value; + } + return SUCCESS; + } + + if (king_inference_gguf_key_has_suffix(key, ".rope.freq_base_swa") + || king_inference_gguf_key_has_suffix(key, ".rope.local.freq_base")) { + if (king_inference_gguf_read_numeric_double(file, file_size, type, path, &floating_value) != SUCCESS) { + return FAILURE; + } + if (floating_value > 0.0 && isfinite(floating_value)) { + metadata->rope_freq_base_swa = floating_value; + } + return SUCCESS; + } + + if (king_inference_gguf_key_has_suffix(key, ".attention.layer_norm_rms_epsilon")) { + if (king_inference_gguf_read_numeric_double(file, file_size, type, path, &floating_value) != SUCCESS) { + return FAILURE; + } + if (floating_value > 0.0 && isfinite(floating_value)) { + metadata->attention_layer_norm_rms_epsilon = floating_value; + } + return SUCCESS; + } + + if (king_inference_gguf_key_has_suffix(key, ".final_logit_softcapping")) { + if (king_inference_gguf_read_numeric_double(file, file_size, type, path, &floating_value) != SUCCESS) { + return FAILURE; + } + if (floating_value > 0.0 && isfinite(floating_value)) { + metadata->final_logit_softcapping = floating_value; + } + return SUCCESS; + } + + if (king_inference_gguf_read_architecture_numeric( + file, + file_size, + path, + key, + type, + metadata, + &architecture_numeric + ) != SUCCESS) { + return FAILURE; + } + if (architecture_numeric) { + return SUCCESS; + } + + if (zend_string_equals_literal(key, "general.alignment")) { + if (king_inference_gguf_read_numeric_long(file, file_size, type, path, &numeric_value) != SUCCESS) { + return FAILURE; + } + if (numeric_value > 0) { + metadata->tensor_data_alignment = (zend_ulong) numeric_value; + } + return SUCCESS; + } + + return king_inference_gguf_skip_value(file, file_size, type, path, 0); +} + +static zend_result king_inference_gguf_parse_metadata( + FILE *file, + zend_string *path, + king_inference_gguf_metadata *metadata, + zval *tokenizer_tokens, + zval *tokenizer_lookup, + zval *tokenizer_scores, + zval *tokenizer_types, + zval *tokenizer_merges +) { + zend_ulong i; + + for (i = 0; i < metadata->metadata_count; i++) { + zend_string *key; + zend_ulong type; + + if (king_inference_gguf_read_string(file, metadata->file_size, path, "metadata key", &key) != SUCCESS) { + return FAILURE; + } + if (king_inference_gguf_read_u32(file, path, "metadata value type", &type) != SUCCESS) { + zend_string_release(key); + return FAILURE; + } + if (king_inference_gguf_read_metadata_value( + file, + metadata->file_size, + path, + key, + type, + metadata, + tokenizer_tokens, + tokenizer_lookup, + tokenizer_scores, + tokenizer_types, + tokenizer_merges + ) != SUCCESS) { + zend_string_release(key); + return FAILURE; + } + zend_string_release(key); + metadata->parsed_metadata_count++; + } + + metadata->metadata_parsed = true; + return SUCCESS; +} + +static zend_ulong king_inference_gguf_aligned_offset(zend_ulong offset, zend_ulong alignment) +{ + zend_ulong remainder; + + if (alignment == 0) { + return offset; + } + remainder = offset % alignment; + if (remainder == 0) { + return offset; + } + if (offset > ZEND_ULONG_MAX - (alignment - remainder)) { + return ZEND_ULONG_MAX; + } + + return offset + (alignment - remainder); +} + +static zend_result king_inference_gguf_parse_tensor_directory( + FILE *file, + zend_string *path, + king_inference_gguf_metadata *metadata, + zval *tensor_index +) { + zend_ulong i; + off_t position; + + for (i = 0; i < metadata->tensor_count; i++) { + zend_string *name; + zend_ulong rank; + zend_ulong type; + zend_ulong offset; + zend_ulong elements = 1; + zend_ulong d; + zval descriptor; + zval dimensions; + bool dimensions_attached = false; + + if (king_inference_gguf_read_string(file, metadata->file_size, path, "tensor name", &name) != SUCCESS) { + return FAILURE; + } + + if (king_inference_gguf_read_u32(file, path, "tensor rank", &rank) != SUCCESS) { + zend_string_release(name); + return FAILURE; + } + if (rank > 16) { + zend_string_release(name); + zend_throw_exception_ex(king_ce_validation_exception, 0, "Model artifact '%s' contains a tensor rank above 16.", ZSTR_VAL(path)); + return FAILURE; + } + if (rank > metadata->max_tensor_rank) { + metadata->max_tensor_rank = rank; + } + + array_init(&dimensions); + for (d = 0; d < rank; d++) { + zend_ulong dimension; + if (king_inference_gguf_read_u64(file, path, "tensor dimension", &dimension) != SUCCESS) { + zval_ptr_dtor(&dimensions); + zend_string_release(name); + return FAILURE; + } + if (dimension == 0) { + zval_ptr_dtor(&dimensions); + zend_string_release(name); + zend_throw_exception_ex(king_ce_validation_exception, 0, "Model artifact '%s' contains a zero-sized tensor dimension.", ZSTR_VAL(path)); + return FAILURE; + } + add_next_index_long(&dimensions, (zend_long) dimension); + if (elements <= ZEND_ULONG_MAX / dimension) { + elements *= dimension; + } else { + elements = ZEND_ULONG_MAX; + } + } + if (elements > metadata->max_tensor_elements) { + metadata->max_tensor_elements = elements; + } + + if (king_inference_gguf_read_u32(file, path, "tensor type", &type) != SUCCESS + || king_inference_gguf_read_u64(file, path, "tensor offset", &offset) != SUCCESS) { + zval_ptr_dtor(&dimensions); + zend_string_release(name); + return FAILURE; + } + if (type < 32) { + metadata->tensor_type_counts[type]++; + } + + if (tensor_index != NULL && Z_TYPE_P(tensor_index) == IS_ARRAY) { + array_init(&descriptor); + add_assoc_long(&descriptor, "rank", (zend_long) rank); + add_assoc_zval(&descriptor, "dimensions", &dimensions); + dimensions_attached = true; + add_assoc_long(&descriptor, "type", (zend_long) type); + add_assoc_long(&descriptor, "relative_offset", (zend_long) offset); + add_assoc_long(&descriptor, "elements", (zend_long) elements); + zend_hash_update(Z_ARRVAL_P(tensor_index), name, &descriptor); + } + + if (!dimensions_attached) { + zval_ptr_dtor(&dimensions); + } + zend_string_release(name); + metadata->tensor_directory_count++; + } + + position = ftello(file); + if (position < 0 || (zend_ulong) position > metadata->file_size) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "Model artifact '%s' has an invalid GGUF tensor data boundary.", ZSTR_VAL(path)); + return FAILURE; + } + + if (metadata->tensor_data_alignment == 0) { + metadata->tensor_data_alignment = 32; + } + metadata->tensor_data_offset = king_inference_gguf_aligned_offset( + (zend_ulong) position, + metadata->tensor_data_alignment + ); + if (metadata->tensor_data_offset > metadata->file_size) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "Model artifact '%s' has a GGUF tensor data offset beyond the file.", ZSTR_VAL(path)); + return FAILURE; + } + + metadata->tensor_directory_parsed = true; + return SUCCESS; +} + +static zend_result king_inference_load_gguf_metadata( + zend_string *path, + king_inference_gguf_metadata *metadata, + zval *tensor_index, + zval *tokenizer_tokens, + zval *tokenizer_lookup, + zval *tokenizer_scores, + zval *tokenizer_types, + zval *tokenizer_merges +) { + struct stat st; + FILE *file; + unsigned char header[24]; + + memset(metadata, 0, sizeof(*metadata)); + metadata->file_type = -1; + metadata->tokenizer_bos_id = -1; + metadata->tokenizer_eos_id = -1; + metadata->tokenizer_unknown_id = -1; + metadata->tokenizer_pad_id = -1; + + if (ZSTR_LEN(path) == 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "Model artifact path must not be empty."); + return FAILURE; + } + + if (stat(ZSTR_VAL(path), &st) != 0 || !S_ISREG(st.st_mode)) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "Model artifact '%s' is not a readable regular file.", + ZSTR_VAL(path) + ); + return FAILURE; + } + metadata->file_size = (zend_ulong) st.st_size; + + file = fopen(ZSTR_VAL(path), "rb"); + if (file == NULL) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "Model artifact '%s' could not be opened.", + ZSTR_VAL(path) + ); + return FAILURE; + } + + if (king_inference_gguf_read_exact(file, header, sizeof(header), path, "header") != SUCCESS) { + fclose(file); + return FAILURE; + } + + if (memcmp(header, "GGUF", 4) != 0) { + fclose(file); + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "Model artifact '%s' is not a complete GGUF quantized model artifact.", + ZSTR_VAL(path) + ); + return FAILURE; + } + + metadata->version = king_inference_read_le32(header + 4); + metadata->tensor_count = king_inference_read_le64(header + 8); + metadata->metadata_count = king_inference_read_le64(header + 16); + metadata->loaded = true; + + if (metadata->version == 0 || metadata->tensor_count == 0) { + fclose(file); + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "Model artifact '%s' has an invalid GGUF header.", + ZSTR_VAL(path) + ); + return FAILURE; + } + + if (king_inference_gguf_parse_metadata( + file, + path, + metadata, + tokenizer_tokens, + tokenizer_lookup, + tokenizer_scores, + tokenizer_types, + tokenizer_merges + ) != SUCCESS + || king_inference_gguf_parse_tensor_directory(file, path, metadata, tensor_index) != SUCCESS) { + fclose(file); + king_inference_gguf_metadata_release(metadata); + return FAILURE; + } + + fclose(file); + return SUCCESS; +} + +#include "../metadata/gguf_metadata_info.inc" diff --git a/extension/src/inference/gguf/metadata/gguf_architecture_metadata.inc b/extension/src/inference/gguf/metadata/gguf_architecture_metadata.inc new file mode 100644 index 000000000..7012d5e6d --- /dev/null +++ b/extension/src/inference/gguf/metadata/gguf_architecture_metadata.inc @@ -0,0 +1,188 @@ +#define KING_INFERENCE_ARCH_CONTEXT_LENGTH 0 +#define KING_INFERENCE_ARCH_EMBEDDING_LENGTH 1 +#define KING_INFERENCE_ARCH_BLOCK_COUNT 2 +#define KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT 3 +#define KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT_KV 4 +#define KING_INFERENCE_ARCH_ATTENTION_KEY_LENGTH 5 +#define KING_INFERENCE_ARCH_ATTENTION_VALUE_LENGTH 6 +#define KING_INFERENCE_ARCH_FEED_FORWARD_LENGTH 7 +#define KING_INFERENCE_ARCH_ATTENTION_SLIDING_WINDOW 8 + +static bool king_inference_gguf_architecture_equals( + king_inference_gguf_metadata *metadata, + const char *architecture, + size_t architecture_len +) { + return metadata->architecture != NULL + && ZSTR_LEN(metadata->architecture) == architecture_len + && memcmp(ZSTR_VAL(metadata->architecture), architecture, architecture_len) == 0; +} + +static bool king_inference_gguf_architecture_is_gemma3(king_inference_gguf_metadata *metadata) +{ + return king_inference_gguf_architecture_equals(metadata, "gemma3", sizeof("gemma3") - 1); +} + +static bool king_inference_gguf_architecture_is_gemma4(king_inference_gguf_metadata *metadata) +{ + return king_inference_gguf_architecture_equals(metadata, "gemma4", sizeof("gemma4") - 1); +} + +static bool king_inference_gguf_head_dim_derivable(king_inference_gguf_metadata *metadata) +{ + zend_ulong embedding = metadata->architecture_params[KING_INFERENCE_ARCH_EMBEDDING_LENGTH]; + zend_ulong heads = metadata->architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT]; + + return embedding > 0 && heads > 0 && embedding % heads == 0; +} + +static void king_inference_gguf_add_missing_arch_field( + zval *missing, + bool present, + const char *name +) { + if (!present) { + add_next_index_string(missing, name); + } +} + +static bool king_inference_gguf_architecture_shape_ready( + king_inference_gguf_metadata *metadata, + zval *missing +) { + bool derivable_head_dim = king_inference_gguf_head_dim_derivable(metadata); + + array_init(missing); + king_inference_gguf_add_missing_arch_field( + missing, + metadata->architecture != NULL && ZSTR_LEN(metadata->architecture) > 0, + "general.architecture" + ); + king_inference_gguf_add_missing_arch_field( + missing, + metadata->architecture_params[KING_INFERENCE_ARCH_CONTEXT_LENGTH] > 0, + "context_length" + ); + king_inference_gguf_add_missing_arch_field( + missing, + metadata->architecture_params[KING_INFERENCE_ARCH_EMBEDDING_LENGTH] > 0, + "embedding_length" + ); + king_inference_gguf_add_missing_arch_field( + missing, + metadata->architecture_params[KING_INFERENCE_ARCH_BLOCK_COUNT] > 0, + "block_count" + ); + king_inference_gguf_add_missing_arch_field( + missing, + metadata->architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT] > 0, + "attention.head_count" + ); + king_inference_gguf_add_missing_arch_field( + missing, + metadata->architecture_params[KING_INFERENCE_ARCH_FEED_FORWARD_LENGTH] > 0, + "feed_forward_length" + ); + king_inference_gguf_add_missing_arch_field( + missing, + metadata->architecture_params[KING_INFERENCE_ARCH_ATTENTION_KEY_LENGTH] > 0 || derivable_head_dim, + "attention.key_length_or_derivable_head_dim" + ); + king_inference_gguf_add_missing_arch_field( + missing, + metadata->architecture_params[KING_INFERENCE_ARCH_ATTENTION_VALUE_LENGTH] > 0 || derivable_head_dim, + "attention.value_length_or_derivable_head_dim" + ); + + return zend_hash_num_elements(Z_ARRVAL_P(missing)) == 0; +} + +static void king_inference_add_gguf_architecture_support_array( + king_inference_gguf_metadata *metadata, + zval *gguf +) { + zval supported; + zval missing; + bool gemma3 = king_inference_gguf_architecture_is_gemma3(metadata); + bool gemma4 = king_inference_gguf_architecture_is_gemma4(metadata); + bool supported_architecture = gemma3 || gemma4; + bool shape_ready = king_inference_gguf_architecture_shape_ready(metadata, &missing); + const char *profile = gemma3 ? "gemma3" : (gemma4 ? "gemma4" : "unsupported"); + const char *status = "unsupported_architecture"; + + if (metadata->architecture == NULL || ZSTR_LEN(metadata->architecture) == 0) { + status = "missing_architecture"; + } else if (supported_architecture && shape_ready) { + status = "supported"; + } else if (supported_architecture) { + status = "supported_missing_shape_metadata"; + } + + array_init(&supported); + add_next_index_string(&supported, "gemma3"); + add_next_index_string(&supported, "gemma4"); + + add_assoc_zval(gguf, "supported_architectures", &supported); + add_assoc_bool(gguf, "architecture_supported", supported_architecture); + add_assoc_string(gguf, "architecture_family", supported_architecture ? "gemma" : "unknown"); + add_assoc_long(gguf, "architecture_generation", gemma3 ? 3 : (gemma4 ? 4 : 0)); + add_assoc_string(gguf, "decoder_profile", profile); + add_assoc_bool(gguf, "decoder_shape_ready", shape_ready); + add_assoc_bool(gguf, "decoder_ready", supported_architecture && shape_ready); + add_assoc_string(gguf, "architecture_support_status", status); + add_assoc_zval(gguf, "architecture_missing_fields", &missing); +} + +static bool king_inference_gguf_key_has_suffix(zend_string *key, const char *suffix) +{ + size_t suffix_len = strlen(suffix); + + return ZSTR_LEN(key) >= suffix_len + && memcmp(ZSTR_VAL(key) + ZSTR_LEN(key) - suffix_len, suffix, suffix_len) == 0; +} + +static zend_result king_inference_gguf_read_architecture_numeric( + FILE *file, + zend_ulong file_size, + zend_string *path, + zend_string *key, + zend_ulong type, + king_inference_gguf_metadata *metadata, + bool *handled +) { + zend_ulong index = 0; + zend_long numeric_value; + + *handled = true; + if (king_inference_gguf_key_has_suffix(key, ".context_length")) { + index = KING_INFERENCE_ARCH_CONTEXT_LENGTH; + } else if (king_inference_gguf_key_has_suffix(key, ".embedding_length")) { + index = KING_INFERENCE_ARCH_EMBEDDING_LENGTH; + } else if (king_inference_gguf_key_has_suffix(key, ".block_count")) { + index = KING_INFERENCE_ARCH_BLOCK_COUNT; + } else if (king_inference_gguf_key_has_suffix(key, ".attention.head_count_kv")) { + index = KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT_KV; + } else if (king_inference_gguf_key_has_suffix(key, ".attention.head_count")) { + index = KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT; + } else if (king_inference_gguf_key_has_suffix(key, ".attention.key_length")) { + index = KING_INFERENCE_ARCH_ATTENTION_KEY_LENGTH; + } else if (king_inference_gguf_key_has_suffix(key, ".attention.value_length")) { + index = KING_INFERENCE_ARCH_ATTENTION_VALUE_LENGTH; + } else if (king_inference_gguf_key_has_suffix(key, ".feed_forward_length")) { + index = KING_INFERENCE_ARCH_FEED_FORWARD_LENGTH; + } else if (king_inference_gguf_key_has_suffix(key, ".attention.sliding_window")) { + index = KING_INFERENCE_ARCH_ATTENTION_SLIDING_WINDOW; + } else { + *handled = false; + return SUCCESS; + } + + if (king_inference_gguf_read_numeric_long(file, file_size, type, path, &numeric_value) != SUCCESS) { + return FAILURE; + } + if (numeric_value > 0) { + metadata->architecture_params[index] = (zend_ulong) numeric_value; + } + + return SUCCESS; +} diff --git a/extension/src/inference/gguf/metadata/gguf_metadata_helpers.inc b/extension/src/inference/gguf/metadata/gguf_metadata_helpers.inc new file mode 100644 index 000000000..0e355154a --- /dev/null +++ b/extension/src/inference/gguf/metadata/gguf_metadata_helpers.inc @@ -0,0 +1,202 @@ +static zend_result king_inference_gguf_read_numeric_long( + FILE *file, + zend_ulong file_size, + zend_ulong type, + zend_string *path, + zend_long *value +); + +static zend_result king_inference_gguf_read_numeric_double( + FILE *file, + zend_ulong file_size, + zend_ulong type, + zend_string *path, + double *value +); + +static zend_result king_inference_gguf_read_string_array( + FILE *file, + zend_ulong file_size, + zend_ulong array_count, + zend_string *path, + const char *what, + zval *values, + zval *lookup, + zend_ulong *max_entry_bytes +) { + zend_ulong i; + + for (i = 0; i < array_count; i++) { + zend_string *entry; + if (king_inference_gguf_read_string(file, file_size, path, what, &entry) != SUCCESS) { + return FAILURE; + } + if (lookup != NULL && Z_TYPE_P(lookup) == IS_ARRAY) { + zval id; + ZVAL_LONG(&id, i > (zend_ulong) ZEND_LONG_MAX ? ZEND_LONG_MAX : (zend_long) i); + zend_hash_update(Z_ARRVAL_P(lookup), entry, &id); + } + if (max_entry_bytes != NULL && ZSTR_LEN(entry) > *max_entry_bytes) { + *max_entry_bytes = (zend_ulong) ZSTR_LEN(entry); + } + if (values != NULL && Z_TYPE_P(values) == IS_ARRAY) { + add_next_index_str(values, entry); + } else { + zend_string_release(entry); + } + } + + return SUCCESS; +} + +static zend_result king_inference_gguf_read_double_array( + FILE *file, + zend_ulong file_size, + zend_ulong array_type, + zend_ulong array_count, + zend_string *path, + zval *values +) { + zend_ulong i; + + if (values == NULL || Z_TYPE_P(values) != IS_ARRAY) { + return king_inference_gguf_skip_array_payload(file, file_size, array_type, array_count, path, 0); + } + + for (i = 0; i < array_count; i++) { + double value; + + if (king_inference_gguf_read_numeric_double(file, file_size, array_type, path, &value) != SUCCESS) { + return FAILURE; + } + add_next_index_double(values, value); + } + + return SUCCESS; +} + +static zend_result king_inference_gguf_read_long_array( + FILE *file, + zend_ulong file_size, + zend_ulong array_type, + zend_ulong array_count, + zend_string *path, + zval *values +) { + zend_ulong i; + + if (values == NULL || Z_TYPE_P(values) != IS_ARRAY) { + return king_inference_gguf_skip_array_payload(file, file_size, array_type, array_count, path, 0); + } + + for (i = 0; i < array_count; i++) { + zend_long value; + + if (king_inference_gguf_read_numeric_long(file, file_size, array_type, path, &value) != SUCCESS) { + return FAILURE; + } + add_next_index_long(values, value); + } + + return SUCCESS; +} + +static zend_result king_inference_gguf_read_numeric_long( + FILE *file, + zend_ulong file_size, + zend_ulong type, + zend_string *path, + zend_long *value +) { + zend_ulong unsigned_value; + unsigned char bytes[8]; + + (void) file_size; + + if (type == KING_GGUF_TYPE_UINT32 || type == KING_GGUF_TYPE_INT32) { + if (king_inference_gguf_read_u32(file, path, "numeric metadata value", &unsigned_value) != SUCCESS) { + return FAILURE; + } + *value = (zend_long) unsigned_value; + return SUCCESS; + } + + if (type == KING_GGUF_TYPE_UINT64 || type == KING_GGUF_TYPE_INT64) { + if (king_inference_gguf_read_u64(file, path, "numeric metadata value", &unsigned_value) != SUCCESS) { + return FAILURE; + } + *value = unsigned_value > (zend_ulong) ZEND_LONG_MAX ? ZEND_LONG_MAX : (zend_long) unsigned_value; + return SUCCESS; + } + + if (type == KING_GGUF_TYPE_UINT8 || type == KING_GGUF_TYPE_INT8) { + if (king_inference_gguf_read_exact(file, bytes, 1, path, "numeric metadata value") != SUCCESS) { + return FAILURE; + } + *value = (zend_long) bytes[0]; + return SUCCESS; + } + + if (type == KING_GGUF_TYPE_UINT16 || type == KING_GGUF_TYPE_INT16) { + if (king_inference_gguf_read_exact(file, bytes, 2, path, "numeric metadata value") != SUCCESS) { + return FAILURE; + } + *value = (zend_long) (((zend_ulong) bytes[0]) | ((zend_ulong) bytes[1] << 8)); + return SUCCESS; + } + + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "Model artifact '%s' contains a non-integer GGUF value where an integer was required.", + ZSTR_VAL(path) + ); + return FAILURE; +} + +static zend_result king_inference_gguf_read_numeric_double( + FILE *file, + zend_ulong file_size, + zend_ulong type, + zend_string *path, + double *value +) { + zend_long integer_value; + unsigned char bytes[8]; + float float_value; + double double_value; + + (void) file_size; + + if (type == KING_GGUF_TYPE_FLOAT32) { + if (king_inference_gguf_read_exact(file, bytes, 4, path, "floating metadata value") != SUCCESS) { + return FAILURE; + } + memcpy(&float_value, bytes, sizeof(float_value)); + *value = (double) float_value; + return SUCCESS; + } + + if (type == KING_GGUF_TYPE_FLOAT64) { + if (king_inference_gguf_read_exact(file, bytes, 8, path, "floating metadata value") != SUCCESS) { + return FAILURE; + } + memcpy(&double_value, bytes, sizeof(double_value)); + *value = double_value; + return SUCCESS; + } + + if (king_inference_gguf_read_numeric_long(file, file_size, type, path, &integer_value) != SUCCESS) { + return FAILURE; + } + *value = (double) integer_value; + return SUCCESS; +} + +static void king_inference_gguf_store_string(zend_string **target, zend_string *value) +{ + if (*target != NULL) { + zend_string_release(*target); + } + *target = value; +} diff --git a/extension/src/inference/gguf/metadata/gguf_metadata_info.inc b/extension/src/inference/gguf/metadata/gguf_metadata_info.inc new file mode 100644 index 000000000..aeacba348 --- /dev/null +++ b/extension/src/inference/gguf/metadata/gguf_metadata_info.inc @@ -0,0 +1,75 @@ +/* + * GGUF metadata export helpers. + */ + +static void king_inference_add_gguf_metadata_array( + king_inference_gguf_metadata *metadata, + zval *return_value +) { + zval gguf; + zval tensor_types; + zend_ulong i; + + array_init(&gguf); + add_assoc_bool(&gguf, "loaded", metadata->loaded); + add_assoc_bool(&gguf, "header_loaded", metadata->loaded); + add_assoc_bool(&gguf, "metadata_parsed", metadata->metadata_parsed); + add_assoc_bool(&gguf, "tensor_directory_parsed", metadata->tensor_directory_parsed); + add_assoc_long(&gguf, "version", (zend_long) metadata->version); + add_assoc_long(&gguf, "tensor_count", (zend_long) metadata->tensor_count); + add_assoc_long(&gguf, "metadata_count", (zend_long) metadata->metadata_count); + add_assoc_long(&gguf, "parsed_metadata_count", (zend_long) metadata->parsed_metadata_count); + add_assoc_long(&gguf, "tensor_directory_count", (zend_long) metadata->tensor_directory_count); + add_assoc_long(&gguf, "tensor_data_offset", (zend_long) metadata->tensor_data_offset); + add_assoc_long(&gguf, "tensor_data_alignment", (zend_long) metadata->tensor_data_alignment); + add_assoc_long(&gguf, "file_size", (zend_long) metadata->file_size); + add_assoc_long(&gguf, "file_type", metadata->file_type); + add_assoc_long(&gguf, "context_length", (zend_long) metadata->architecture_params[KING_INFERENCE_ARCH_CONTEXT_LENGTH]); + add_assoc_long(&gguf, "embedding_length", (zend_long) metadata->architecture_params[KING_INFERENCE_ARCH_EMBEDDING_LENGTH]); + add_assoc_long(&gguf, "block_count", (zend_long) metadata->architecture_params[KING_INFERENCE_ARCH_BLOCK_COUNT]); + add_assoc_long(&gguf, "attention_head_count", (zend_long) metadata->architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT]); + add_assoc_long(&gguf, "attention_head_count_kv", (zend_long) metadata->architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT_KV]); + add_assoc_long(&gguf, "attention_key_length", (zend_long) metadata->architecture_params[KING_INFERENCE_ARCH_ATTENTION_KEY_LENGTH]); + add_assoc_long(&gguf, "attention_value_length", (zend_long) metadata->architecture_params[KING_INFERENCE_ARCH_ATTENTION_VALUE_LENGTH]); + add_assoc_long(&gguf, "feed_forward_length", (zend_long) metadata->architecture_params[KING_INFERENCE_ARCH_FEED_FORWARD_LENGTH]); + add_assoc_long(&gguf, "attention_sliding_window", (zend_long) metadata->architecture_params[KING_INFERENCE_ARCH_ATTENTION_SLIDING_WINDOW]); + add_assoc_double(&gguf, "rope_freq_base", metadata->rope_freq_base); + add_assoc_double(&gguf, "rope_freq_base_swa", metadata->rope_freq_base_swa); + add_assoc_double(&gguf, "attention_layer_norm_rms_epsilon", metadata->attention_layer_norm_rms_epsilon); + add_assoc_double(&gguf, "final_logit_softcapping", metadata->final_logit_softcapping); + king_inference_add_gguf_architecture_support_array(metadata, &gguf); + add_assoc_long(&gguf, "tokenizer_token_count", (zend_long) metadata->tokenizer_token_count); + add_assoc_long(&gguf, "tokenizer_score_count", (zend_long) metadata->tokenizer_score_count); + add_assoc_long(&gguf, "tokenizer_type_count", (zend_long) metadata->tokenizer_type_count); + add_assoc_long(&gguf, "tokenizer_merge_count", (zend_long) metadata->tokenizer_merge_count); + add_assoc_long(&gguf, "tokenizer_max_token_bytes", (zend_long) metadata->tokenizer_max_token_bytes); + add_assoc_bool(&gguf, "tokenizer_tokens_loaded", metadata->tokenizer_tokens_loaded); + add_assoc_bool(&gguf, "tokenizer_lookup_loaded", metadata->tokenizer_lookup_loaded); + add_assoc_bool(&gguf, "tokenizer_scores_loaded", metadata->tokenizer_scores_loaded); + add_assoc_bool(&gguf, "tokenizer_types_loaded", metadata->tokenizer_types_loaded); + add_assoc_bool(&gguf, "tokenizer_merges_loaded", metadata->tokenizer_merges_loaded); + add_assoc_long(&gguf, "tokenizer_bos_id", metadata->tokenizer_bos_id); + add_assoc_long(&gguf, "tokenizer_eos_id", metadata->tokenizer_eos_id); + add_assoc_long(&gguf, "tokenizer_unknown_id", metadata->tokenizer_unknown_id); + add_assoc_long(&gguf, "tokenizer_pad_id", metadata->tokenizer_pad_id); + add_assoc_long(&gguf, "max_tensor_rank", (zend_long) metadata->max_tensor_rank); + add_assoc_long(&gguf, "max_tensor_elements", (zend_long) metadata->max_tensor_elements); + if (metadata->architecture != NULL) { + add_assoc_str(&gguf, "architecture", zend_string_copy(metadata->architecture)); + } + if (metadata->general_name != NULL) { + add_assoc_str(&gguf, "general_name", zend_string_copy(metadata->general_name)); + } + if (metadata->tokenizer_model != NULL) { + add_assoc_str(&gguf, "tokenizer_model", zend_string_copy(metadata->tokenizer_model)); + } + + array_init(&tensor_types); + for (i = 0; i < 32; i++) { + if (metadata->tensor_type_counts[i] > 0) { + add_index_long(&tensor_types, i, (zend_long) metadata->tensor_type_counts[i]); + } + } + add_assoc_zval(&gguf, "tensor_type_counts", &tensor_types); + add_assoc_zval(return_value, "gguf", &gguf); +} diff --git a/extension/src/inference/inference.c b/extension/src/inference/inference.c new file mode 100644 index 000000000..98aef4220 --- /dev/null +++ b/extension/src/inference/inference.c @@ -0,0 +1,19 @@ +/* + * Native inference PHP surface for King. + * + * This translation unit owns the model, stream, OpenAI-compatible router, + * tensor, tokenizer, paging, and registration bindings for inference. The + * core php_king bootstrap only calls the exported registration functions. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "php.h" +#include "php_king.h" +#include "inference/arginfo/index.h" + +#include "core/bootstrap/state.inc" +#include "binding/php_binding.inc" +#include "core/bootstrap/registration.inc" diff --git a/extension/src/inference/openai/chat/openai_completion_payload.inc b/extension/src/inference/openai/chat/openai_completion_payload.inc new file mode 100644 index 000000000..2862c21e2 --- /dev/null +++ b/extension/src/inference/openai/chat/openai_completion_payload.inc @@ -0,0 +1,51 @@ +static zend_result king_inference_openai_completion_payload( + king_inference_stream_object *stream, + smart_str *content, + const char *finish_reason, + bool allow_tool_calls, + zval *return_value) +{ + zend_string *model_name = king_inference_stream_openai_model_name(stream); + zval choices; + zval choice; + zval message; + zval tool_calls; + bool has_tool_calls; + + ZVAL_UNDEF(&tool_calls); + has_tool_calls = allow_tool_calls + && content != NULL + && content->s != NULL + && king_inference_openai_tool_calls_from_content(content->s, &tool_calls); + + array_init(return_value); + add_assoc_str(return_value, "id", zend_string_copy(stream->response_id)); + add_assoc_string(return_value, "object", "chat.completion"); + add_assoc_long(return_value, "created", stream->created_at); + add_assoc_str(return_value, "model", model_name); + + array_init(&choices); + array_init(&choice); + add_assoc_long(&choice, "index", 0); + array_init(&message); + add_assoc_string(&message, "role", "assistant"); + if (has_tool_calls) { + add_assoc_null(&message, "content"); + add_assoc_zval(&message, "tool_calls", &tool_calls); + } else if (content != NULL && content->s != NULL) { + add_assoc_str(&message, "content", zend_string_copy(content->s)); + } else { + add_assoc_string(&message, "content", ""); + } + add_assoc_zval(&choice, "message", &message); + add_assoc_string(&choice, "finish_reason", has_tool_calls ? "tool_calls" : (finish_reason != NULL ? finish_reason : "stop")); + add_next_index_zval(&choices, &choice); + add_assoc_zval(return_value, "choices", &choices); + king_inference_openai_add_completion_usage( + return_value, + &stream->model, + &stream->request, + content != NULL ? content->s : NULL + ); + return SUCCESS; +} diff --git a/extension/src/inference/openai/chat/openai_completions.inc b/extension/src/inference/openai/chat/openai_completions.inc new file mode 100644 index 000000000..07f6ddb7f --- /dev/null +++ b/extension/src/inference/openai/chat/openai_completions.inc @@ -0,0 +1,640 @@ +static zend_string *king_inference_completion_id(king_inference_stream_object *stream) +{ + return strpprintf(0, "cmpl_king_" ZEND_LONG_FMT "_%lu", stream->created_at, (zend_ulong) stream->std.handle); +} + +static zend_result king_inference_completion_prompt_to_chat_payload( + zval *payload, + zval *prompt, + zval *chat_payload) +{ + zval *field; + zval messages; + zval message; + + if (prompt == NULL || Z_TYPE_P(prompt) != IS_STRING) { + return FAILURE; + } + + array_init(chat_payload); + field = king_inference_array_find(payload, "model"); + if (field != NULL) { + zval copy; + ZVAL_COPY(©, field); + add_assoc_zval(chat_payload, "model", ©); + } + + array_init(&messages); + array_init(&message); + add_assoc_string(&message, "role", "user"); + add_assoc_str(&message, "content", zend_string_copy(Z_STR_P(prompt))); + add_next_index_zval(&messages, &message); + add_assoc_zval(chat_payload, "messages", &messages); + add_assoc_bool(chat_payload, "stream", king_inference_bool_from_array(payload, "stream", false)); + + king_inference_openai_copy_completion_generation_options(payload, chat_payload); + + return SUCCESS; +} + +static void king_inference_completion_choice( + zval *choice, + zend_long index, + zend_string *text, + const char *finish_reason) +{ + array_init(choice); + add_assoc_long(choice, "index", index); + add_assoc_str(choice, "text", text != NULL ? zend_string_copy(text) : zend_string_init("", 0, 0)); + add_assoc_null(choice, "logprobs"); + if (finish_reason != NULL) { + add_assoc_string(choice, "finish_reason", finish_reason); + } else { + add_assoc_null(choice, "finish_reason"); + } +} + +static zend_result king_inference_completion_payload( + zval *model, + zval *payload, + zval *choices, + zval *return_value) +{ + zend_long created_at = (zend_long) time(NULL); + zend_string *completion_id = strpprintf(0, "cmpl_king_" ZEND_LONG_FMT "_%lu", created_at, (zend_ulong) Z_OBJ_HANDLE_P(model)); + zend_string *model_name = king_inference_openai_payload_model_name(model, payload); + + array_init(return_value); + add_assoc_str(return_value, "id", completion_id); + add_assoc_string(return_value, "object", "text_completion"); + add_assoc_long(return_value, "created", created_at); + add_assoc_str(return_value, "model", model_name); + king_inference_openai_add_text_completion_usage(return_value, model, payload, choices); + add_assoc_zval(return_value, "choices", choices); + return SUCCESS; +} + +static zend_result king_inference_completion_run_prompt( + zval *model, + zval *payload, + zval *prompt, + zval *options, + const king_inference_openai_drain_controls *drain_controls, + zend_long choice_index, + zval *choice_out, + bool *timed_out_out) +{ + zval chat_payload; + zval stream_value; + king_inference_stream_object *stream; + smart_str content = {0}; + zend_string *finish_reason = NULL; + zend_result result; + + if (timed_out_out != NULL) { + *timed_out_out = false; + } + if (king_inference_completion_prompt_to_chat_payload(payload, prompt, &chat_payload) != SUCCESS) { + return FAILURE; + } + if (king_inference_openai_create_stream( + &stream_value, + model, + &chat_payload, + options, + "king_inference_openai_http_response" + ) != SUCCESS) { + zval_ptr_dtor(&chat_payload); + return FAILURE; + } + + stream = php_king_inference_stream_obj_from_zend(Z_OBJ(stream_value)); + result = king_inference_openai_drain_stream(stream, &stream_value, drain_controls, &content, NULL, &finish_reason); + if (content.s == NULL) { + smart_str_appends(&content, ""); + } + smart_str_0(&content); + if (result == SUCCESS) { + king_inference_completion_choice( + choice_out, + choice_index, + content.s, + finish_reason != NULL ? ZSTR_VAL(finish_reason) : "stop" + ); + } + if (finish_reason != NULL) { + zend_string_release(finish_reason); + } + smart_str_free(&content); + if (timed_out_out != NULL) { + *timed_out_out = result != SUCCESS && stream->timed_out; + } + zval_ptr_dtor(&stream_value); + zval_ptr_dtor(&chat_payload); + return result; +} + +static zend_result king_inference_completion_stream_response( + zval *return_value, + zval *model, + zval *payload, + zval *prompt, + zval *options, + const king_inference_openai_drain_controls *drain_controls, + bool *timed_out_out) +{ + zval chat_payload; + zval stream_value; + king_inference_stream_object *stream; + zend_string *completion_id; + zend_string *model_name; + zend_long events = 0; + zend_long idle_events = 0; + bool finished = false; + bool include_usage = king_inference_openai_stream_usage_requested(payload); + smart_str sse = {0}; + smart_str content = {0}; + zend_string *finish_reason = NULL; + + if (timed_out_out != NULL) { + *timed_out_out = false; + } + if (king_inference_completion_prompt_to_chat_payload(payload, prompt, &chat_payload) != SUCCESS) { + return FAILURE; + } + if (king_inference_openai_create_stream( + &stream_value, + model, + &chat_payload, + options, + "king_inference_openai_http_response" + ) != SUCCESS) { + zval_ptr_dtor(&chat_payload); + return FAILURE; + } + + stream = php_king_inference_stream_obj_from_zend(Z_OBJ(stream_value)); + completion_id = king_inference_completion_id(stream); + model_name = king_inference_stream_openai_model_name(stream); + finish_reason = zend_string_init("stop", sizeof("stop") - 1, 0); + + while (events < drain_controls->max_events && idle_events < drain_controls->max_idle_events) { + zval chunk; + zval *delta_text; + + ZVAL_UNDEF(&chunk); + if (king_inference_stream_next_event(stream, drain_controls->read_timeout_ms, &chunk) != SUCCESS) { + if (!Z_ISUNDEF(chunk)) { + zval_ptr_dtor(&chunk); + } + zend_string_release(completion_id); + zend_string_release(model_name); + zval_ptr_dtor(&stream_value); + zval_ptr_dtor(&chat_payload); + smart_str_free(&content); + smart_str_free(&sse); + zend_string_release(finish_reason); + return FAILURE; + } + if (Z_TYPE(chunk) == IS_NULL) { + zval_ptr_dtor(&chunk); + if (stream->done) { + finished = true; + break; + } + idle_events++; + continue; + } + + idle_events = 0; + events++; + delta_text = king_inference_openai_chunk_delta_content(&chunk); + if (delta_text != NULL && Z_TYPE_P(delta_text) == IS_STRING && Z_STRLEN_P(delta_text) > 0) { + zval event; + zval choices; + zval choice; + array_init(&event); + add_assoc_str(&event, "id", zend_string_copy(completion_id)); + add_assoc_string(&event, "object", "text_completion"); + add_assoc_long(&event, "created", stream->created_at); + add_assoc_str(&event, "model", zend_string_copy(model_name)); + array_init(&choices); + king_inference_completion_choice(&choice, 0, Z_STR_P(delta_text), NULL); + add_next_index_zval(&choices, &choice); + add_assoc_zval(&event, "choices", &choices); + if (include_usage) { + add_assoc_null(&event, "usage"); + } + smart_str_append(&content, Z_STR_P(delta_text)); + if (king_inference_openai_sse_append(&sse, &event) != SUCCESS) { + zval_ptr_dtor(&event); + zval_ptr_dtor(&chunk); + zend_string_release(completion_id); + zend_string_release(model_name); + zval_ptr_dtor(&stream_value); + zval_ptr_dtor(&chat_payload); + smart_str_free(&content); + smart_str_free(&sse); + zend_string_release(finish_reason); + return FAILURE; + } + zval_ptr_dtor(&event); + } + { + const char *chunk_finish_reason = NULL; + if (king_inference_openai_chunk_is_terminal(&chunk, &chunk_finish_reason)) { + if (chunk_finish_reason != NULL && chunk_finish_reason[0] != '\0') { + zend_string_release(finish_reason); + finish_reason = zend_string_init(chunk_finish_reason, strlen(chunk_finish_reason), 0); + } + zval_ptr_dtor(&chunk); + finished = true; + break; + } + } + zval_ptr_dtor(&chunk); + } + + zend_string_release(completion_id); + zend_string_release(model_name); + if (!finished) { + king_inference_stream_timeout_state(stream); + if (timed_out_out != NULL) { + *timed_out_out = true; + } + zval_ptr_dtor(&stream_value); + zval_ptr_dtor(&chat_payload); + smart_str_free(&content); + smart_str_free(&sse); + zend_string_release(finish_reason); + return FAILURE; + } + + { + zval event; + zval choices; + zval choice; + array_init(&event); + add_assoc_str(&event, "id", king_inference_completion_id(stream)); + add_assoc_string(&event, "object", "text_completion"); + add_assoc_long(&event, "created", stream->created_at); + add_assoc_str(&event, "model", king_inference_stream_openai_model_name(stream)); + array_init(&choices); + king_inference_completion_choice(&choice, 0, NULL, ZSTR_VAL(finish_reason)); + add_next_index_zval(&choices, &choice); + add_assoc_zval(&event, "choices", &choices); + if (include_usage) { + add_assoc_null(&event, "usage"); + } + if (king_inference_openai_sse_append(&sse, &event) != SUCCESS) { + zval_ptr_dtor(&event); + zval_ptr_dtor(&stream_value); + zval_ptr_dtor(&chat_payload); + smart_str_free(&content); + smart_str_free(&sse); + zend_string_release(finish_reason); + return FAILURE; + } + zval_ptr_dtor(&event); + } + + if (include_usage) { + zval event; + zval choices; + smart_str_0(&content); + array_init(&event); + add_assoc_str(&event, "id", king_inference_completion_id(stream)); + add_assoc_string(&event, "object", "text_completion"); + add_assoc_long(&event, "created", stream->created_at); + add_assoc_str(&event, "model", king_inference_stream_openai_model_name(stream)); + array_init(&choices); + add_assoc_zval(&event, "choices", &choices); + king_inference_openai_add_text_completion_stream_usage( + &event, + model, + payload, + content.s + ); + if (king_inference_openai_sse_append(&sse, &event) != SUCCESS) { + zval_ptr_dtor(&event); + zval_ptr_dtor(&stream_value); + zval_ptr_dtor(&chat_payload); + smart_str_free(&content); + smart_str_free(&sse); + zend_string_release(finish_reason); + return FAILURE; + } + zval_ptr_dtor(&event); + } + + zval_ptr_dtor(&stream_value); + zval_ptr_dtor(&chat_payload); + smart_str_free(&content); + zend_string_release(finish_reason); + smart_str_appends(&sse, "data: [DONE]\n\n"); + smart_str_0(&sse); + king_inference_openai_http_array_response(return_value, 200, "text/event-stream", sse.s, true); + smart_str_free(&sse); + return SUCCESS; +} + +static zend_result king_inference_openai_completions_payload_response( + zval *return_value, + zval *model, + zval *payload, + zval *options) +{ + zval *prompt = king_inference_array_find(payload, "prompt"); + zval choices; + zval response_payload; + zend_string *json = NULL; + zend_result result; + zend_long max_completion_prompts = 0; + king_inference_openai_drain_controls drain_controls; + bool stream_requested; + const char *stop_error = NULL; + bool has_error_response = false; + const char *generation_error = NULL; + const char *stream_options_error = NULL; + const char *stream_error = NULL; + const char *model_error = NULL; + const char *choice_count_error = NULL; + const char *feature_error = NULL; + const char *limit_error = NULL; + bool stream_timed_out = false; + + if (!king_inference_openai_model_field_valid(payload, &model_error)) { + return king_inference_openai_error_response( + return_value, + 400, + model_error != NULL ? model_error : "OpenAI model is invalid.", + "invalid_request_error" + ); + } + if (prompt == NULL) { + return king_inference_openai_error_response( + return_value, + 400, + "Completions request requires prompt.", + "invalid_request_error" + ); + } + if (!king_inference_openai_positive_router_limit( + options, + "max_completion_prompts", + 128, + &max_completion_prompts, + &limit_error + )) { + return king_inference_openai_error_response( + return_value, + 500, + limit_error != NULL ? limit_error : "King OpenAI router limit is invalid.", + "server_error" + ); + } + if (!king_inference_openai_drain_controls_from_options(options, &drain_controls, &limit_error)) { + return king_inference_openai_error_response( + return_value, + 500, + limit_error != NULL ? limit_error : "King OpenAI router drain controls are invalid.", + "server_error" + ); + } + if (!king_inference_openai_stop_option_valid(payload, &stop_error)) { + return king_inference_openai_error_response( + return_value, + 400, + stop_error != NULL ? stop_error : "OpenAI stop is invalid.", + "invalid_request_error" + ); + } + if (!king_inference_openai_completion_generation_options_valid(payload, &generation_error)) { + return king_inference_openai_error_response( + return_value, + 400, + generation_error != NULL ? generation_error : "OpenAI generation options are invalid.", + "invalid_request_error" + ); + } + if (!king_inference_openai_stream_options_valid(payload, &stream_options_error)) { + return king_inference_openai_error_response( + return_value, + 400, + stream_options_error != NULL ? stream_options_error : "OpenAI stream_options is invalid.", + "invalid_request_error" + ); + } + if (!king_inference_openai_stream_field_valid(payload, &stream_error)) { + return king_inference_openai_error_response( + return_value, + 400, + stream_error != NULL ? stream_error : "OpenAI stream is invalid.", + "invalid_request_error" + ); + } + if (!king_inference_openai_single_choice_option_valid(payload, &choice_count_error)) { + return king_inference_openai_error_response( + return_value, + 400, + choice_count_error != NULL ? choice_count_error : "OpenAI n is invalid.", + "invalid_request_error" + ); + } + if (!king_inference_openai_completion_feature_options_valid(payload, &feature_error)) { + return king_inference_openai_error_response( + return_value, + 400, + feature_error != NULL ? feature_error : "OpenAI requested feature is unsupported.", + "invalid_request_error" + ); + } + stream_requested = king_inference_bool_from_array(payload, "stream", false); + if (king_inference_openai_require_generation_backend( + return_value, + model, + payload, + "Completions endpoint", + &has_error_response + ) != SUCCESS) { + return FAILURE; + } + if (has_error_response) { + return SUCCESS; + } + if (stream_requested && Z_TYPE_P(prompt) != IS_STRING) { + return king_inference_openai_error_response( + return_value, + 400, + "Streaming completions require a string prompt.", + "invalid_request_error" + ); + } + if (stream_requested) { + result = king_inference_completion_stream_response( + return_value, + model, + payload, + prompt, + options, + &drain_controls, + &stream_timed_out + ); + if (result != SUCCESS && !EG(exception)) { + if (stream_timed_out) { + return king_inference_openai_error_response_classified( + return_value, + 504, + "Completion stream timed out before completion.", + "server_error", + "king.timeout.deadline_exceeded" + ); + } + return king_inference_openai_error_response( + return_value, + 500, + "Completion stream failed before completion.", + "server_error" + ); + } + return result; + } + + array_init(&choices); + if (Z_TYPE_P(prompt) == IS_STRING) { + zval choice; + result = king_inference_completion_run_prompt( + model, + payload, + prompt, + options, + &drain_controls, + 0, + &choice, + &stream_timed_out + ); + if (result != SUCCESS) { + zval_ptr_dtor(&choices); + if (EG(exception)) { + return FAILURE; + } + if (stream_timed_out) { + return king_inference_openai_error_response_classified( + return_value, + 504, + "Completion request timed out before completion.", + "server_error", + "king.timeout.deadline_exceeded" + ); + } + return king_inference_openai_error_response( + return_value, + 500, + "Completion request failed before completion.", + "server_error" + ); + } + add_next_index_zval(&choices, &choice); + } else if (king_inference_openai_array_is_list(prompt)) { + zval *entry; + zend_ulong prompt_count = (zend_ulong) zend_hash_num_elements(Z_ARRVAL_P(prompt)); + zend_long index = 0; + + if (prompt_count == 0) { + zval_ptr_dtor(&choices); + return king_inference_openai_error_response( + return_value, + 400, + "Completions prompt list must not be empty.", + "invalid_request_error" + ); + } + if (prompt_count > (zend_ulong) max_completion_prompts) { + zval_ptr_dtor(&choices); + return king_inference_openai_error_response( + return_value, + 400, + "Completions prompt list exceeds configured router limits.", + "invalid_request_error" + ); + } + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(prompt), entry) { + zval choice; + bool prompt_timed_out = false; + if (Z_TYPE_P(entry) != IS_STRING) { + zval_ptr_dtor(&choices); + return king_inference_openai_error_response( + return_value, + 400, + "Prompt arrays must contain only strings.", + "invalid_request_error" + ); + } + result = king_inference_completion_run_prompt( + model, + payload, + entry, + options, + &drain_controls, + index++, + &choice, + &prompt_timed_out + ); + if (result != SUCCESS) { + zval_ptr_dtor(&choices); + if (EG(exception)) { + return FAILURE; + } + if (prompt_timed_out) { + return king_inference_openai_error_response_classified( + return_value, + 504, + "Completion request timed out before completion.", + "server_error", + "king.timeout.deadline_exceeded" + ); + } + return king_inference_openai_error_response( + return_value, + 500, + "Completion request failed before completion.", + "server_error" + ); + } + add_next_index_zval(&choices, &choice); + } ZEND_HASH_FOREACH_END(); + } else { + zval_ptr_dtor(&choices); + return king_inference_openai_error_response( + return_value, + 400, + "Completions prompt must be a string or a list of strings.", + "invalid_request_error" + ); + } + + { + zval *first_choice = zend_hash_index_find(Z_ARRVAL(choices), 0); + if (first_choice == NULL) { + zval_ptr_dtor(&choices); + return king_inference_openai_error_response( + return_value, + 400, + "Completions prompt list must not be empty.", + "invalid_request_error" + ); + } + } + + result = king_inference_completion_payload(model, payload, &choices, &response_payload); + if (result == SUCCESS) { + result = king_inference_openai_json_encode(&response_payload, &json); + zval_ptr_dtor(&response_payload); + } + if (result != SUCCESS) { + return FAILURE; + } + king_inference_openai_http_array_response(return_value, 200, "application/json", json, false); + zend_string_release(json); + return SUCCESS; +} diff --git a/extension/src/inference/openai/chat/openai_messages.inc b/extension/src/inference/openai/chat/openai_messages.inc new file mode 100644 index 000000000..6222295b0 --- /dev/null +++ b/extension/src/inference/openai/chat/openai_messages.inc @@ -0,0 +1,67 @@ +static bool king_inference_openai_array_is_list(zval *value) +{ + return value != NULL && Z_TYPE_P(value) == IS_ARRAY && zend_array_is_list(Z_ARRVAL_P(value)); +} + +static bool king_inference_openai_array_is_object(zval *value) +{ + return value != NULL + && Z_TYPE_P(value) == IS_ARRAY + && (zend_hash_num_elements(Z_ARRVAL_P(value)) == 0 || !zend_array_is_list(Z_ARRVAL_P(value))); +} + +static zend_result king_inference_openai_append_content_part_text(zval *part, smart_str *buffer) +{ + zval *text; + + if (part == NULL) { + return FAILURE; + } + if (Z_TYPE_P(part) == IS_STRING) { + smart_str_append(buffer, Z_STR_P(part)); + return SUCCESS; + } + if (Z_TYPE_P(part) != IS_ARRAY) { + return FAILURE; + } + + text = king_inference_array_find(part, "text"); + if (text == NULL) { + text = king_inference_array_find(part, "content"); + } + if (text == NULL) { + text = king_inference_array_find(part, "refusal"); + } + if (text == NULL || Z_TYPE_P(text) != IS_STRING) { + return FAILURE; + } + + smart_str_append(buffer, Z_STR_P(text)); + return SUCCESS; +} + +static zend_result king_inference_openai_append_content_text(zval *content, smart_str *buffer) +{ + zval *part; + bool has_text = false; + + if (content == NULL) { + return FAILURE; + } + if (Z_TYPE_P(content) == IS_STRING) { + smart_str_append(buffer, Z_STR_P(content)); + return SUCCESS; + } + if (!king_inference_openai_array_is_list(content)) { + return FAILURE; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(content), part) { + if (king_inference_openai_append_content_part_text(part, buffer) != SUCCESS) { + return FAILURE; + } + has_text = true; + } ZEND_HASH_FOREACH_END(); + + return has_text ? SUCCESS : FAILURE; +} diff --git a/extension/src/inference/openai/chat/openai_responses.inc b/extension/src/inference/openai/chat/openai_responses.inc new file mode 100644 index 000000000..ebcf84e9b --- /dev/null +++ b/extension/src/inference/openai/chat/openai_responses.inc @@ -0,0 +1,554 @@ +static zend_string *king_inference_responses_id(king_inference_stream_object *stream) +{ + return strpprintf(0, "resp_king_" ZEND_LONG_FMT "_%lu", stream->created_at, (zend_ulong) stream->std.handle); +} + +static zend_string *king_inference_responses_message_id(king_inference_stream_object *stream) +{ + return strpprintf(0, "msg_king_" ZEND_LONG_FMT "_%lu", stream->created_at, (zend_ulong) stream->std.handle); +} + +static zend_result king_inference_responses_message_from_item(zval *item, zval *messages) +{ + zval *role; + zval *content; + zval message; + smart_str text = {0}; + + if (Z_TYPE_P(item) != IS_ARRAY) { + return FAILURE; + } + + role = king_inference_array_find(item, "role"); + content = king_inference_array_find(item, "content"); + if (role == NULL || Z_TYPE_P(role) != IS_STRING || Z_STRLEN_P(role) == 0) { + return FAILURE; + } + if (king_inference_openai_append_content_text(content, &text) != SUCCESS) { + smart_str_free(&text); + return FAILURE; + } + smart_str_0(&text); + + array_init(&message); + add_assoc_str(&message, "role", zend_string_copy(Z_STR_P(role))); + add_assoc_str(&message, "content", text.s != NULL ? zend_string_copy(text.s) : zend_string_init("", 0, 0)); + add_next_index_zval(messages, &message); + smart_str_free(&text); + return SUCCESS; +} + +static zend_result king_inference_responses_payload_to_chat_payload(zval *payload, zval *chat_payload) +{ + zval *input; + zval *instructions; + zval *field; + zval messages; + + if (payload == NULL || Z_TYPE_P(payload) != IS_ARRAY) { + return FAILURE; + } + + input = king_inference_array_find(payload, "input"); + if (input == NULL) { + return FAILURE; + } + + array_init(chat_payload); + field = king_inference_array_find(payload, "model"); + if (field != NULL) { + zval copy; + ZVAL_COPY(©, field); + add_assoc_zval(chat_payload, "model", ©); + } + + array_init(&messages); + instructions = king_inference_array_find(payload, "instructions"); + if (instructions != NULL && Z_TYPE_P(instructions) == IS_STRING && Z_STRLEN_P(instructions) > 0) { + zval message; + array_init(&message); + add_assoc_string(&message, "role", "system"); + add_assoc_str(&message, "content", zend_string_copy(Z_STR_P(instructions))); + add_next_index_zval(&messages, &message); + } + + if (Z_TYPE_P(input) == IS_STRING) { + zval message; + array_init(&message); + add_assoc_string(&message, "role", "user"); + add_assoc_str(&message, "content", zend_string_copy(Z_STR_P(input))); + add_next_index_zval(&messages, &message); + } else if (king_inference_openai_array_is_list(input)) { + zval *item; + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(input), item) { + if (king_inference_responses_message_from_item(item, &messages) != SUCCESS) { + zval_ptr_dtor(&messages); + zval_ptr_dtor(chat_payload); + ZVAL_UNDEF(chat_payload); + return FAILURE; + } + } ZEND_HASH_FOREACH_END(); + } else { + zval_ptr_dtor(&messages); + zval_ptr_dtor(chat_payload); + ZVAL_UNDEF(chat_payload); + return FAILURE; + } + + if (zend_hash_num_elements(Z_ARRVAL(messages)) == 0) { + zval_ptr_dtor(&messages); + zval_ptr_dtor(chat_payload); + ZVAL_UNDEF(chat_payload); + return FAILURE; + } + + add_assoc_zval(chat_payload, "messages", &messages); + add_assoc_bool(chat_payload, "stream", king_inference_bool_from_array(payload, "stream", false)); + + king_inference_openai_copy_response_generation_options(payload, chat_payload); + + return SUCCESS; +} + +static zend_result king_inference_responses_output_payload( + king_inference_stream_object *stream, + zend_string *response_id, + zend_string *message_id, + smart_str *content, + zval *return_value) +{ + zend_string *model_name = king_inference_stream_openai_model_name(stream); + zval output; + zval item; + zval content_list; + zval content_part; + + array_init(return_value); + add_assoc_str(return_value, "id", zend_string_copy(response_id)); + add_assoc_string(return_value, "object", "response"); + add_assoc_long(return_value, "created_at", stream->created_at); + add_assoc_string(return_value, "status", "completed"); + add_assoc_str(return_value, "model", model_name); + + array_init(&output); + array_init(&item); + add_assoc_str(&item, "id", zend_string_copy(message_id)); + add_assoc_string(&item, "type", "message"); + add_assoc_string(&item, "status", "completed"); + add_assoc_string(&item, "role", "assistant"); + array_init(&content_list); + array_init(&content_part); + add_assoc_string(&content_part, "type", "output_text"); + add_assoc_str(&content_part, "text", content != NULL && content->s != NULL + ? zend_string_copy(content->s) + : zend_string_init("", 0, 0)); + { + zval annotations; + array_init(&annotations); + add_assoc_zval(&content_part, "annotations", &annotations); + } + add_next_index_zval(&content_list, &content_part); + add_assoc_zval(&item, "content", &content_list); + add_next_index_zval(&output, &item); + add_assoc_zval(return_value, "output", &output); + add_assoc_str(return_value, "output_text", content != NULL && content->s != NULL + ? zend_string_copy(content->s) + : zend_string_init("", 0, 0)); + add_assoc_null(return_value, "error"); + add_assoc_null(return_value, "incomplete_details"); + king_inference_openai_add_response_usage( + return_value, + &stream->model, + &stream->request, + content != NULL ? content->s : NULL + ); + return SUCCESS; +} + +static zend_result king_inference_responses_sse_append(smart_str *sse, const char *event_name, zval *payload) +{ + zend_string *json = NULL; + + if (king_inference_openai_json_encode(payload, &json) != SUCCESS) { + return FAILURE; + } + + smart_str_appends(sse, "event: "); + smart_str_appends(sse, event_name); + smart_str_appends(sse, "\n"); + smart_str_appends(sse, "data: "); + smart_str_append(sse, json); + smart_str_appends(sse, "\n\n"); + zend_string_release(json); + return SUCCESS; +} + +static void king_inference_responses_base_event( + zval *event, + const char *type, + zend_string *response_id, + zend_string *message_id, + zend_string *model_name) +{ + array_init(event); + add_assoc_string(event, "type", type); + add_assoc_str(event, "response_id", zend_string_copy(response_id)); + add_assoc_str(event, "item_id", zend_string_copy(message_id)); + add_assoc_str(event, "model", zend_string_copy(model_name)); +} + +static zend_result king_inference_responses_stream_response( + zval *return_value, + king_inference_stream_object *stream, + zend_string *response_id, + zend_string *message_id, + const king_inference_openai_drain_controls *drain_controls) +{ + zend_long events = 0; + zend_long idle_events = 0; + smart_str sse = {0}; + smart_str content = {0}; + zend_string *model_name = king_inference_stream_openai_model_name(stream); + zval event; + bool finished = false; + const char *response_format_error = NULL; + + king_inference_responses_base_event(&event, "response.created", response_id, message_id, model_name); + if (king_inference_responses_sse_append(&sse, "response.created", &event) != SUCCESS) { + zval_ptr_dtor(&event); + zend_string_release(model_name); + smart_str_free(&sse); + return FAILURE; + } + zval_ptr_dtor(&event); + + while (events < drain_controls->max_events && idle_events < drain_controls->max_idle_events) { + zval chunk; + zval *delta_text; + + ZVAL_UNDEF(&chunk); + if (king_inference_stream_next_event(stream, drain_controls->read_timeout_ms, &chunk) != SUCCESS) { + if (!Z_ISUNDEF(chunk)) { + zval_ptr_dtor(&chunk); + } + zend_string_release(model_name); + smart_str_free(&sse); + smart_str_free(&content); + return FAILURE; + } + if (Z_TYPE(chunk) == IS_NULL) { + zval_ptr_dtor(&chunk); + if (stream->done) { + finished = true; + break; + } + idle_events++; + continue; + } + + idle_events = 0; + events++; + delta_text = king_inference_openai_chunk_delta_content(&chunk); + if (delta_text != NULL && Z_TYPE_P(delta_text) == IS_STRING && Z_STRLEN_P(delta_text) > 0) { + zval delta_event; + smart_str_append(&content, Z_STR_P(delta_text)); + king_inference_responses_base_event( + &delta_event, + "response.output_text.delta", + response_id, + message_id, + model_name + ); + add_assoc_str(&delta_event, "delta", zend_string_copy(Z_STR_P(delta_text))); + if (king_inference_responses_sse_append(&sse, "response.output_text.delta", &delta_event) != SUCCESS) { + zval_ptr_dtor(&delta_event); + zval_ptr_dtor(&chunk); + zend_string_release(model_name); + smart_str_free(&sse); + smart_str_free(&content); + return FAILURE; + } + zval_ptr_dtor(&delta_event); + } + if (king_inference_openai_chunk_is_terminal(&chunk, NULL)) { + zval_ptr_dtor(&chunk); + finished = true; + break; + } + zval_ptr_dtor(&chunk); + } + + if (!finished) { + king_inference_stream_timeout_state(stream); + zend_string_release(model_name); + smart_str_free(&sse); + smart_str_free(&content); + return FAILURE; + } + + smart_str_0(&content); + if (king_inference_openai_validate_response_format_output( + &stream->request, + content.s, + &response_format_error + ) != SUCCESS) { + zend_string_release(model_name); + smart_str_free(&sse); + smart_str_free(&content); + return king_inference_openai_error_response( + return_value, + 502, + response_format_error != NULL ? response_format_error : "Model output did not satisfy response_format.", + "server_error" + ); + } + + king_inference_responses_base_event(&event, "response.completed", response_id, message_id, model_name); + if (content.s != NULL) { + add_assoc_str(&event, "output_text", zend_string_copy(content.s)); + } else { + add_assoc_string(&event, "output_text", ""); + } + king_inference_openai_add_response_usage( + &event, + &stream->model, + &stream->request, + content.s + ); + if (king_inference_responses_sse_append(&sse, "response.completed", &event) != SUCCESS) { + zval_ptr_dtor(&event); + zend_string_release(model_name); + smart_str_free(&sse); + smart_str_free(&content); + return FAILURE; + } + zval_ptr_dtor(&event); + smart_str_0(&sse); + + zend_string_release(model_name); + smart_str_free(&content); + king_inference_openai_http_array_response(return_value, 200, "text/event-stream", sse.s, true); + smart_str_free(&sse); + return SUCCESS; +} + +static zend_result king_inference_openai_responses_payload_response( + zval *return_value, + zval *model, + zval *payload, + zval *options) +{ + zval chat_payload; + zval stream_value; + king_inference_stream_object *stream; + zend_string *response_id; + zend_string *message_id; + zval *input; + bool stream_requested; + zend_result result; + zend_long max_response_input_items = 0; + king_inference_openai_drain_controls drain_controls; + bool has_error_response = false; + const char *generation_error = NULL; + const char *stream_options_error = NULL; + const char *stream_error = NULL; + const char *model_error = NULL; + const char *instructions_error = NULL; + const char *feature_error = NULL; + const char *limit_error = NULL; + bool stream_timed_out = false; + + if (!king_inference_openai_model_field_valid(payload, &model_error)) { + return king_inference_openai_error_response( + return_value, + 400, + model_error != NULL ? model_error : "OpenAI model is invalid.", + "invalid_request_error" + ); + } + if (!king_inference_openai_stream_field_valid(payload, &stream_error)) { + return king_inference_openai_error_response( + return_value, + 400, + stream_error != NULL ? stream_error : "OpenAI stream is invalid.", + "invalid_request_error" + ); + } + if (!king_inference_openai_response_instructions_valid(payload, &instructions_error)) { + return king_inference_openai_error_response( + return_value, + 400, + instructions_error != NULL ? instructions_error : "Responses instructions are invalid.", + "invalid_request_error" + ); + } + if (!king_inference_openai_response_feature_options_valid(payload, &feature_error)) { + return king_inference_openai_error_response( + return_value, + 400, + feature_error != NULL ? feature_error : "OpenAI requested feature is unsupported.", + "invalid_request_error" + ); + } + if (!king_inference_openai_positive_router_limit( + options, + "max_response_input_items", + 256, + &max_response_input_items, + &limit_error + )) { + return king_inference_openai_error_response( + return_value, + 500, + limit_error != NULL ? limit_error : "King OpenAI router limit is invalid.", + "server_error" + ); + } + if (!king_inference_openai_drain_controls_from_options(options, &drain_controls, &limit_error)) { + return king_inference_openai_error_response( + return_value, + 500, + limit_error != NULL ? limit_error : "King OpenAI router drain controls are invalid.", + "server_error" + ); + } + input = king_inference_array_find(payload, "input"); + if (king_inference_openai_array_is_list(input) + && (zend_ulong) zend_hash_num_elements(Z_ARRVAL_P(input)) > (zend_ulong) max_response_input_items) { + return king_inference_openai_error_response( + return_value, + 400, + "Responses input list exceeds configured router limits.", + "invalid_request_error" + ); + } + if (king_inference_responses_payload_to_chat_payload(payload, &chat_payload) != SUCCESS) { + return king_inference_openai_error_response( + return_value, + 400, + "Responses request requires input as a string or a list of message items.", + "invalid_request_error" + ); + } + if (!king_inference_openai_response_generation_options_valid(payload, &generation_error)) { + zval_ptr_dtor(&chat_payload); + return king_inference_openai_error_response( + return_value, + 400, + generation_error != NULL ? generation_error : "OpenAI generation options are invalid.", + "invalid_request_error" + ); + } + if (!king_inference_openai_stream_options_valid(payload, &stream_options_error)) { + zval_ptr_dtor(&chat_payload); + return king_inference_openai_error_response( + return_value, + 400, + stream_options_error != NULL ? stream_options_error : "OpenAI stream_options is invalid.", + "invalid_request_error" + ); + } + stream_requested = king_inference_bool_from_array(payload, "stream", false); + if (king_inference_openai_require_generation_backend( + return_value, + model, + payload, + "Responses endpoint", + &has_error_response + ) != SUCCESS) { + zval_ptr_dtor(&chat_payload); + return FAILURE; + } + if (has_error_response) { + zval_ptr_dtor(&chat_payload); + return SUCCESS; + } + + if (king_inference_openai_create_stream( + &stream_value, + model, + &chat_payload, + options, + "king_inference_openai_http_response" + ) != SUCCESS) { + zval_ptr_dtor(&chat_payload); + return FAILURE; + } + stream = php_king_inference_stream_obj_from_zend(Z_OBJ(stream_value)); + response_id = king_inference_responses_id(stream); + message_id = king_inference_responses_message_id(stream); + + if (stream_requested) { + result = king_inference_responses_stream_response(return_value, stream, response_id, message_id, &drain_controls); + } else { + smart_str content = {0}; + zval response_payload; + zend_string *json = NULL; + const char *response_format_error = NULL; + + result = king_inference_openai_drain_stream(stream, &stream_value, &drain_controls, &content, NULL, NULL); + smart_str_0(&content); + if (result == SUCCESS + && king_inference_openai_validate_response_format_output( + &stream->request, + content.s, + &response_format_error + ) != SUCCESS) { + result = king_inference_openai_error_response( + return_value, + 502, + response_format_error != NULL ? response_format_error : "Model output did not satisfy response_format.", + "server_error" + ); + smart_str_free(&content); + zend_string_release(response_id); + zend_string_release(message_id); + zval_ptr_dtor(&stream_value); + zval_ptr_dtor(&chat_payload); + return result; + } + if (result == SUCCESS) { + result = king_inference_responses_output_payload( + stream, + response_id, + message_id, + &content, + &response_payload + ); + if (result == SUCCESS) { + result = king_inference_openai_json_encode(&response_payload, &json); + zval_ptr_dtor(&response_payload); + } + } + smart_str_free(&content); + if (result == SUCCESS) { + king_inference_openai_http_array_response(return_value, 200, "application/json", json, false); + zend_string_release(json); + } + } + + stream_timed_out = result != SUCCESS && stream->timed_out; + zend_string_release(response_id); + zend_string_release(message_id); + zval_ptr_dtor(&stream_value); + zval_ptr_dtor(&chat_payload); + if (result != SUCCESS && !EG(exception)) { + if (stream_timed_out) { + return king_inference_openai_error_response_classified( + return_value, + 504, + "Responses request timed out before completion.", + "server_error", + "king.timeout.deadline_exceeded" + ); + } + return king_inference_openai_error_response( + return_value, + 500, + "Responses request failed before completion.", + "server_error" + ); + } + + return result; +} diff --git a/extension/src/inference/openai/http/openai_http_body.inc b/extension/src/inference/openai/http/openai_http_body.inc new file mode 100644 index 000000000..fb818e639 --- /dev/null +++ b/extension/src/inference/openai/http/openai_http_body.inc @@ -0,0 +1,85 @@ +static bool king_inference_openai_json_body_space(char c) +{ + return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f'; +} + +static bool king_inference_openai_json_body_looks_object(zend_string *body) +{ + const char *bytes; + size_t first = 0; + size_t last; + + if (body == NULL || ZSTR_LEN(body) == 0) { + return false; + } + + bytes = ZSTR_VAL(body); + last = ZSTR_LEN(body); + while (first < last && king_inference_openai_json_body_space(bytes[first])) { + first++; + } + while (last > first && king_inference_openai_json_body_space(bytes[last - 1])) { + last--; + } + + return first < last && bytes[first] == '{' && bytes[last - 1] == '}'; +} + +static zend_result king_inference_openai_decode_object_body( + zval *request, + const char *string_body_message, + const char *object_body_message, + zval *return_value, + zval *decoded, + bool *has_error_response) +{ + zval *body = king_inference_array_find(request, "body"); + + if (has_error_response != NULL) { + *has_error_response = false; + } + if (body == NULL || Z_TYPE_P(body) != IS_STRING) { + if (has_error_response != NULL) { + *has_error_response = true; + } + return king_inference_openai_error_response( + return_value, + 400, + string_body_message, + "invalid_request_error" + ); + } + if (!king_inference_openai_json_body_looks_object(Z_STR_P(body))) { + if (has_error_response != NULL) { + *has_error_response = true; + } + return king_inference_openai_error_response( + return_value, + 400, + object_body_message, + "invalid_request_error" + ); + } + + if (king_inference_openai_json_decode(Z_STR_P(body), decoded) != SUCCESS) { + if (has_error_response != NULL) { + *has_error_response = true; + } + return king_inference_openai_error_response(return_value, 400, king_get_error(), "invalid_request_error"); + } + + if (Z_TYPE_P(decoded) != IS_ARRAY) { + zval_ptr_dtor(decoded); + if (has_error_response != NULL) { + *has_error_response = true; + } + return king_inference_openai_error_response( + return_value, + 400, + object_body_message, + "invalid_request_error" + ); + } + + return SUCCESS; +} diff --git a/extension/src/inference/openai/http/openai_http_helpers.inc b/extension/src/inference/openai/http/openai_http_helpers.inc new file mode 100644 index 000000000..523f078dd --- /dev/null +++ b/extension/src/inference/openai/http/openai_http_helpers.inc @@ -0,0 +1,269 @@ +static zend_result king_inference_openai_json_encode(zval *value, zend_string **json_out) +{ + smart_str json = {0}; + + if (json_out == NULL) { + return FAILURE; + } + + *json_out = NULL; + if (php_json_encode(&json, value, PHP_JSON_UNESCAPED_SLASHES) != SUCCESS) { + smart_str_free(&json); + king_set_error("OpenAI-compatible inference response encoding failed."); + return FAILURE; + } + + smart_str_0(&json); + *json_out = json.s != NULL ? json.s : zend_string_init("", 0, 0); + return SUCCESS; +} + +static zend_result king_inference_openai_json_decode(zend_string *json, zval *decoded_out) +{ + if (json == NULL || decoded_out == NULL) { + return FAILURE; + } + + ZVAL_UNDEF(decoded_out); + if (php_json_decode(decoded_out, ZSTR_VAL(json), ZSTR_LEN(json), 1, 512) != SUCCESS) { + king_set_error("OpenAI-compatible inference request body is not valid JSON."); + return FAILURE; + } + + return SUCCESS; +} + +static bool king_inference_openai_method_is_post(zval *request) +{ + zval *method = king_inference_array_find(request, "method"); + + return method != NULL && Z_TYPE_P(method) == IS_STRING && strcasecmp(Z_STRVAL_P(method), "POST") == 0; +} + +static bool king_inference_openai_uri_matches(zval *request) +{ + zval *uri = king_inference_array_find(request, "uri"); + + if (uri == NULL) { + uri = king_inference_array_find(request, "path"); + } + if (uri == NULL || Z_TYPE_P(uri) != IS_STRING || Z_STRLEN_P(uri) == 0) { + return true; + } + + return zend_string_equals_literal(Z_STR_P(uri), "/v1/chat/completions"); +} + +static bool king_inference_openai_error_context_key_safe(zend_string *key) +{ + if (key == NULL || ZSTR_LEN(key) == 0) { + return false; + } + + return !zend_string_equals_literal(key, "prompt") + && !zend_string_equals_literal(key, "messages") + && !zend_string_equals_literal(key, "message") + && !zend_string_equals_literal(key, "content") + && !zend_string_equals_literal(key, "body") + && !zend_string_equals_literal(key, "request") + && !zend_string_equals_literal(key, "request_body") + && !zend_string_equals_literal(key, "input") + && !zend_string_equals_literal(key, "output") + && !zend_string_equals_literal(key, "output_text"); +} + +static void king_inference_openai_error_object( + zval *error, + zend_long status, + const char *message, + const char *type, + const char *explicit_code) +{ + zval king_error; + const char *safe_message = message != NULL ? message : "King inference request failed."; + const char *safe_type = type != NULL ? type : "server_error"; + const char *code = king_inference_error_code_from_explicit_or_message( + status, + safe_type, + safe_message, + explicit_code + ); + const char *category = king_inference_error_category_from_code(code); + + array_init(error); + add_assoc_string(error, "message", safe_message); + add_assoc_string(error, "type", safe_type); + add_assoc_null(error, "param"); + add_assoc_string(error, "code", code); + array_init(&king_error); + add_assoc_long(&king_error, "schema_version", 1); + add_assoc_string(&king_error, "category", category); + add_assoc_string(&king_error, "code", code); + add_assoc_string(&king_error, "human_message", safe_message); + add_assoc_bool(&king_error, "prompt_included", false); + add_assoc_bool(&king_error, "request_body_included", false); + add_assoc_bool(&king_error, "safe_for_logs", true); + add_assoc_zval(error, "x_king", &king_error); +} + +static void king_inference_openai_error_payload( + zval *return_value, + zend_long status, + const char *message, + const char *type, + const char *explicit_code) +{ + zval error; + + array_init(return_value); + king_inference_openai_error_object(&error, status, message, type, explicit_code); + add_assoc_zval(return_value, "error", &error); +} + +static void king_inference_openai_error_payload_merge_king_context(zval *payload, zval *king_context) +{ + zval *error; + zval *target; + zval *value; + zend_string *key; + + if (king_context == NULL || Z_TYPE_P(king_context) != IS_ARRAY) { + return; + } + + error = king_inference_array_find(payload, "error"); + if (error == NULL || Z_TYPE_P(error) != IS_ARRAY) { + return; + } + + target = king_inference_array_find(error, "x_king"); + if (target == NULL || Z_TYPE_P(target) != IS_ARRAY) { + return; + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(king_context), key, value) { + zval copy; + + if (!king_inference_openai_error_context_key_safe(key)) { + continue; + } + ZVAL_COPY(©, value); + zend_hash_update(Z_ARRVAL_P(target), key, ©); + } ZEND_HASH_FOREACH_END(); +} + +static void king_inference_openai_http_array_response( + zval *return_value, + zend_long status, + const char *content_type, + zend_string *body, + bool event_stream) +{ + zval headers; + + array_init(return_value); + add_assoc_long(return_value, "status", status); + array_init(&headers); + add_assoc_string(&headers, "content-type", content_type); + if (event_stream) { + add_assoc_string(&headers, "cache-control", "no-cache"); + add_assoc_string(&headers, "x-accel-buffering", "no"); + } + add_assoc_zval(return_value, "headers", &headers); + add_assoc_str(return_value, "body", body != NULL ? zend_string_copy(body) : zend_string_init("", 0, 0)); +} + +static zend_result king_inference_openai_error_response_classified( + zval *return_value, + zend_long status, + const char *message, + const char *type, + const char *code); + +static zend_result king_inference_openai_error_response_with_king_classified( + zval *return_value, + zend_long status, + const char *message, + const char *type, + const char *code, + zval *king_context); + +static zend_result king_inference_openai_error_response( + zval *return_value, + zend_long status, + const char *message, + const char *type) +{ + return king_inference_openai_error_response_classified( + return_value, + status, + message, + type, + NULL + ); +} + +static zend_result king_inference_openai_error_response_classified( + zval *return_value, + zend_long status, + const char *message, + const char *type, + const char *code) +{ + zval payload; + zend_string *json = NULL; + zend_result result; + + king_inference_openai_error_payload(&payload, status, message, type, code); + result = king_inference_openai_json_encode(&payload, &json); + zval_ptr_dtor(&payload); + if (result != SUCCESS) { + return FAILURE; + } + + king_inference_openai_http_array_response(return_value, status, "application/json", json, false); + zend_string_release(json); + return SUCCESS; +} + +static zend_result king_inference_openai_error_response_with_king( + zval *return_value, + zend_long status, + const char *message, + const char *type, + zval *king_context) +{ + return king_inference_openai_error_response_with_king_classified( + return_value, + status, + message, + type, + NULL, + king_context + ); +} + +static zend_result king_inference_openai_error_response_with_king_classified( + zval *return_value, + zend_long status, + const char *message, + const char *type, + const char *code, + zval *king_context) +{ + zval payload; + zend_string *json = NULL; + zend_result result; + + king_inference_openai_error_payload(&payload, status, message, type, code); + king_inference_openai_error_payload_merge_king_context(&payload, king_context); + result = king_inference_openai_json_encode(&payload, &json); + zval_ptr_dtor(&payload); + if (result != SUCCESS) { + return FAILURE; + } + + king_inference_openai_http_array_response(return_value, status, "application/json", json, false); + zend_string_release(json); + return SUCCESS; +} diff --git a/extension/src/inference/openai/http/openai_http_request.inc b/extension/src/inference/openai/http/openai_http_request.inc new file mode 100644 index 000000000..e39429010 --- /dev/null +++ b/extension/src/inference/openai/http/openai_http_request.inc @@ -0,0 +1,59 @@ +static zval *king_inference_openai_request_field(zval *request, const char *name) +{ + return king_inference_array_find(request, name); +} + +static bool king_inference_openai_request_path_is(zval *request, const char *path) +{ + zval *uri = king_inference_openai_request_field(request, "uri"); + size_t path_len = strlen(path); + + if (uri == NULL) { + uri = king_inference_openai_request_field(request, "path"); + } + if (uri == NULL || Z_TYPE_P(uri) != IS_STRING) { + return false; + } + + return Z_STRLEN_P(uri) == path_len + ? memcmp(Z_STRVAL_P(uri), path, path_len) == 0 + : Z_STRLEN_P(uri) > path_len + && memcmp(Z_STRVAL_P(uri), path, path_len) == 0 + && Z_STRVAL_P(uri)[path_len] == '?'; +} + +static zend_string *king_inference_openai_request_path_suffix(zval *request, const char *prefix) +{ + zval *uri = king_inference_openai_request_field(request, "uri"); + size_t prefix_len = strlen(prefix); + const char *start; + const char *end; + + if (uri == NULL) { + uri = king_inference_openai_request_field(request, "path"); + } + if (uri == NULL || Z_TYPE_P(uri) != IS_STRING || Z_STRLEN_P(uri) <= prefix_len) { + return NULL; + } + if (memcmp(Z_STRVAL_P(uri), prefix, prefix_len) != 0) { + return NULL; + } + + start = Z_STRVAL_P(uri) + prefix_len; + end = memchr(start, '?', Z_STRLEN_P(uri) - prefix_len); + if (end == NULL) { + end = Z_STRVAL_P(uri) + Z_STRLEN_P(uri); + } + if (end <= start) { + return NULL; + } + + return zend_string_init(start, (size_t) (end - start), 0); +} + +static bool king_inference_openai_request_method_is(zval *request, const char *method) +{ + zval *actual = king_inference_openai_request_field(request, "method"); + + return actual != NULL && Z_TYPE_P(actual) == IS_STRING && strcasecmp(Z_STRVAL_P(actual), method) == 0; +} diff --git a/extension/src/inference/openai/http/openai_http_router.inc b/extension/src/inference/openai/http/openai_http_router.inc new file mode 100644 index 000000000..4d5779c0d --- /dev/null +++ b/extension/src/inference/openai/http/openai_http_router.inc @@ -0,0 +1,280 @@ +static zend_result king_inference_openai_router_chat_response( + zval *models, + zval *request, + zval *options, + zval *return_value) +{ + zval decoded; + zval *model; + zend_result result; + bool has_error_response = false; + + if (!king_inference_openai_request_method_is(request, "POST")) { + return king_inference_openai_error_response( + return_value, + 405, + "Chat completions endpoint requires POST.", + "invalid_request_error" + ); + } + + result = king_inference_openai_decode_object_body( + request, + "Chat completions HTTP request requires a JSON string body.", + "Chat completions HTTP request body must be a JSON object.", + return_value, + &decoded, + &has_error_response + ); + if (result != SUCCESS || has_error_response) { + return result; + } + + result = king_inference_openai_require_model( + models, + &decoded, + "Chat completions request model must be a non-empty string when provided.", + return_value, + &model + ); + if (result != SUCCESS || Z_TYPE_P(return_value) == IS_ARRAY) { + zval_ptr_dtor(&decoded); + return result; + } + + result = king_inference_openai_payload_response(return_value, model, &decoded, options); + zval_ptr_dtor(&decoded); + return result; +} + +static zend_result king_inference_openai_router_responses_response( + zval *models, + zval *request, + zval *options, + zval *return_value) +{ + zval decoded; + zval *model; + zend_result result; + bool has_error_response = false; + + if (!king_inference_openai_request_method_is(request, "POST")) { + return king_inference_openai_error_response( + return_value, + 405, + "Responses endpoint requires POST.", + "invalid_request_error" + ); + } + + result = king_inference_openai_decode_object_body( + request, + "Responses HTTP request requires a JSON string body.", + "Responses HTTP request body must be a JSON object.", + return_value, + &decoded, + &has_error_response + ); + if (result != SUCCESS || has_error_response) { + return result; + } + + result = king_inference_openai_require_model( + models, + &decoded, + "Responses request model must be a non-empty string when provided.", + return_value, + &model + ); + if (result != SUCCESS || Z_TYPE_P(return_value) == IS_ARRAY) { + zval_ptr_dtor(&decoded); + return result; + } + + result = king_inference_openai_responses_payload_response(return_value, model, &decoded, options); + zval_ptr_dtor(&decoded); + return result; +} + +static zend_result king_inference_openai_router_completions_response( + zval *models, + zval *request, + zval *options, + zval *return_value) +{ + zval decoded; + zval *model; + zend_result result; + bool has_error_response = false; + + if (!king_inference_openai_request_method_is(request, "POST")) { + return king_inference_openai_error_response( + return_value, + 405, + "Completions endpoint requires POST.", + "invalid_request_error" + ); + } + + result = king_inference_openai_decode_object_body( + request, + "Completions HTTP request requires a JSON string body.", + "Completions HTTP request body must be a JSON object.", + return_value, + &decoded, + &has_error_response + ); + if (result != SUCCESS || has_error_response) { + return result; + } + + result = king_inference_openai_require_model( + models, + &decoded, + "Completions request model must be a non-empty string when provided.", + return_value, + &model + ); + if (result != SUCCESS || Z_TYPE_P(return_value) == IS_ARRAY) { + zval_ptr_dtor(&decoded); + return result; + } + + result = king_inference_openai_completions_payload_response(return_value, model, &decoded, options); + zval_ptr_dtor(&decoded); + return result; +} + +static zend_result king_inference_openai_router_embeddings_response( + zval *models, + zval *request, + zval *options, + zval *return_value) +{ + zval decoded; + zval *model; + zend_result result; + bool has_error_response = false; + + if (!king_inference_openai_request_method_is(request, "POST")) { + return king_inference_openai_error_response( + return_value, + 405, + "Embeddings endpoint requires POST.", + "invalid_request_error" + ); + } + + result = king_inference_openai_decode_object_body( + request, + "Embeddings HTTP request requires a JSON string body.", + "Embeddings HTTP request body must be a JSON object.", + return_value, + &decoded, + &has_error_response + ); + if (result != SUCCESS || has_error_response) { + return result; + } + + result = king_inference_openai_require_model( + models, + &decoded, + "Embeddings request model must be a non-empty string when provided.", + return_value, + &model + ); + if (result != SUCCESS || Z_TYPE_P(return_value) == IS_ARRAY) { + zval_ptr_dtor(&decoded); + return result; + } + + result = king_inference_openai_embeddings_payload_response(return_value, model, &decoded, options); + zval_ptr_dtor(&decoded); + return result; +} + +PHP_FUNCTION(king_inference_openai_http_response) +{ + zval *models; + zval *request; + zval *options = NULL; + zval empty_options; + zval *effective_options; + zend_result result; + zend_string *model_id = NULL; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_ARRAY(models) + Z_PARAM_ARRAY(request) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + if (options == NULL) { + array_init(&empty_options); + effective_options = &empty_options; + } else { + effective_options = options; + } + + model_id = king_inference_openai_request_path_suffix(request, "/v1/models/"); + + if (king_inference_openai_request_path_is(request, "/v1/models")) { + if (!king_inference_openai_request_method_is(request, "GET")) { + result = king_inference_openai_error_response( + return_value, + 405, + "Models endpoint requires GET.", + "invalid_request_error" + ); + } else { + result = king_inference_openai_models_response(models, effective_options, return_value); + } + } else if (model_id != NULL) { + if (!king_inference_openai_request_method_is(request, "GET")) { + result = king_inference_openai_error_response( + return_value, + 405, + "Model retrieve endpoint requires GET.", + "invalid_request_error" + ); + } else { + result = king_inference_openai_retrieve_model_response(models, model_id, effective_options, return_value); + } + } else if (king_inference_openai_request_path_is(request, "/v1/chat/completions")) { + result = king_inference_openai_router_chat_response(models, request, effective_options, return_value); + } else if (king_inference_openai_request_path_is(request, "/v1/responses")) { + result = king_inference_openai_router_responses_response(models, request, effective_options, return_value); + } else if (king_inference_openai_request_path_is(request, "/v1/completions")) { + result = king_inference_openai_router_completions_response(models, request, effective_options, return_value); + } else if (king_inference_openai_request_path_is(request, "/v1/embeddings")) { + result = king_inference_openai_router_embeddings_response(models, request, effective_options, return_value); + } else { + result = king_inference_openai_error_response( + return_value, + 404, + "OpenAI-compatible inference route not found.", + "invalid_request_error" + ); + } + + if (options == NULL) { + zval_ptr_dtor(&empty_options); + } + if (model_id != NULL) { + zend_string_release(model_id); + } + if (result != SUCCESS) { + if (EG(exception)) { + RETURN_THROWS(); + } + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "%s", + king_get_error()[0] != '\0' ? king_get_error() : "OpenAI-compatible inference router failed." + ); + RETURN_THROWS(); + } +} diff --git a/extension/src/inference/openai/resources/openai_embeddings.inc b/extension/src/inference/openai/resources/openai_embeddings.inc new file mode 100644 index 000000000..a530d9463 --- /dev/null +++ b/extension/src/inference/openai/resources/openai_embeddings.inc @@ -0,0 +1,607 @@ +static zend_string *king_inference_openai_embeddings_payload_model_name(zval *model_value, zval *payload) +{ + zval *requested = king_inference_array_find(payload, "model"); + king_inference_model_object *model = php_king_inference_model_obj_from_zend(Z_OBJ_P(model_value)); + + if (requested != NULL && Z_TYPE_P(requested) == IS_STRING && Z_STRLEN_P(requested) > 0) { + return zend_string_copy(Z_STR_P(requested)); + } + if (model->name != NULL && ZSTR_LEN(model->name) > 0) { + return zend_string_copy(model->name); + } + + return zend_string_init("local-quantized-model", sizeof("local-quantized-model") - 1, 0); +} + +static zend_result king_inference_openai_embeddings_resolve_view( + king_inference_model_object *model, + zval *options, + zval *return_value, + king_inference_tensor_native_view *view, + zend_string **tensor_name_out, + zend_ulong *dimensions_out, + zend_ulong *vocab_out) +{ + zend_string *tensor = NULL; + const char *status = NULL; + zend_ulong dimensions; + zend_ulong vocab; + + *tensor_name_out = NULL; + *dimensions_out = 0; + *vocab_out = 0; + + if (king_inference_resolve_token_embedding_tensor_name( + model, + options, + &tensor, + NULL, + &status + ) != SUCCESS + || tensor == NULL) { + return king_inference_openai_error_response( + return_value, + 500, + status != NULL && strcmp(status, "ambiguous_shape_scan") == 0 + ? "King embedding endpoint found more than one possible token embedding tensor; configure embedding_tensor explicitly." + : "King embedding endpoint requires an embedding_tensor or a known token embedding tensor in the loaded model.", + "server_error" + ); + } + + if (king_inference_tensor_native_view_resolve(model, tensor, view) != SUCCESS) { + zend_string_release(tensor); + if (EG(exception)) { + zend_clear_exception(); + } + return king_inference_openai_error_response( + return_value, + 500, + "King embedding tensor could not be resolved from the loaded model.", + "server_error" + ); + } + if (view->rank != 2 + || king_inference_tensor_dimension_at(view->dimensions, 0, &dimensions) != SUCCESS + || king_inference_tensor_dimension_at(view->dimensions, 1, &vocab) != SUCCESS + || dimensions == 0 + || vocab == 0) { + zend_string_release(tensor); + return king_inference_openai_error_response( + return_value, + 500, + "King embedding tensor must be a bounded rank-2 token embedding matrix.", + "server_error" + ); + } + + *tensor_name_out = tensor; + *dimensions_out = dimensions; + *vocab_out = vocab; + return SUCCESS; +} + +static zend_result king_inference_openai_embeddings_output_dimensions( + zval *payload, + zend_long max_dimensions, + zend_ulong native_dimensions, + zval *return_value, + zend_ulong *output_dimensions) +{ + zval *requested = king_inference_array_find(payload, "dimensions"); + + *output_dimensions = native_dimensions; + + if (requested != NULL) { + if (Z_TYPE_P(requested) != IS_LONG || Z_LVAL_P(requested) <= 0) { + return king_inference_openai_error_response( + return_value, + 400, + "Embeddings dimensions must be a positive integer.", + "invalid_request_error" + ); + } + if ((zend_ulong) Z_LVAL_P(requested) > native_dimensions) { + return king_inference_openai_error_response( + return_value, + 400, + "Embeddings dimensions cannot exceed the native embedding tensor width.", + "invalid_request_error" + ); + } + if ((zend_ulong) Z_LVAL_P(requested) > (zend_ulong) max_dimensions) { + return king_inference_openai_error_response( + return_value, + 400, + "Embeddings dimensions exceed configured router limits.", + "invalid_request_error" + ); + } + + *output_dimensions = (zend_ulong) Z_LVAL_P(requested); + return SUCCESS; + } + + if (native_dimensions > (zend_ulong) max_dimensions) { + return king_inference_openai_error_response( + return_value, + 500, + "Native embedding tensor width exceeds configured router limits; request a smaller dimensions value or raise max_embedding_dimensions.", + "server_error" + ); + } + + return SUCCESS; +} + +static zend_result king_inference_openai_embeddings_encoding_format( + zval *payload, + zval *return_value, + bool *base64_out) +{ + zval *encoding = king_inference_array_find(payload, "encoding_format"); + + *base64_out = false; + if (encoding == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(encoding) != IS_STRING) { + return king_inference_openai_error_response( + return_value, + 400, + "Embeddings encoding_format must be a string.", + "invalid_request_error" + ); + } + if (zend_string_equals_literal(Z_STR_P(encoding), "float")) { + return SUCCESS; + } + if (zend_string_equals_literal(Z_STR_P(encoding), "base64")) { + *base64_out = true; + return SUCCESS; + } + + return king_inference_openai_error_response( + return_value, + 400, + "King embeddings support encoding_format=float or encoding_format=base64.", + "invalid_request_error" + ); +} + +static void king_inference_openai_embeddings_write_float32le(double value, unsigned char *target) +{ + float f32 = (float) value; + uint32_t bits; + + memcpy(&bits, &f32, sizeof(bits)); + target[0] = (unsigned char) (bits & 0xffU); + target[1] = (unsigned char) ((bits >> 8) & 0xffU); + target[2] = (unsigned char) ((bits >> 16) & 0xffU); + target[3] = (unsigned char) ((bits >> 24) & 0xffU); +} + +static zend_result king_inference_openai_embeddings_tokenize_text( + king_inference_model_object *model, + zend_string *text, + zval *tokens_out) +{ + zval encoded; + zval *tokens; + + if (king_inference_tokenizer_encode(model, text, &encoded) != SUCCESS) { + return FAILURE; + } + + tokens = king_inference_array_find(&encoded, "tokens"); + if (tokens == NULL || Z_TYPE_P(tokens) != IS_ARRAY) { + zval_ptr_dtor(&encoded); + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King native tokenizer returned no token list."); + return FAILURE; + } + + ZVAL_COPY(tokens_out, tokens); + zval_ptr_dtor(&encoded); + return SUCCESS; +} + +static bool king_inference_openai_embeddings_list_is_long_list(zval *value) +{ + zval *entry; + + if (!king_inference_openai_array_is_list(value)) { + return false; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(value), entry) { + if (Z_TYPE_P(entry) != IS_LONG) { + return false; + } + } ZEND_HASH_FOREACH_END(); + + return true; +} + +static zend_result king_inference_openai_embeddings_add_vector( + zval *data, + king_inference_tensor_native_view *view, + zval *tokens, + zend_ulong index, + zend_ulong native_dimensions, + zend_ulong output_dimensions, + zend_ulong vocab, + zend_ulong max_tokens, + bool base64_encoding, + zend_ulong *total_tokens) +{ + zend_ulong token_count = (zend_ulong) zend_hash_num_elements(Z_ARRVAL_P(tokens)); + double *sums; + zend_ulong col; + zval *token; + zval item; + + if (token_count == 0 || token_count > max_tokens || output_dimensions > (zend_ulong) (SIZE_MAX / sizeof(double))) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "Embedding input token count or dimension exceeds configured limits."); + return FAILURE; + } + + sums = ecalloc((size_t) output_dimensions, sizeof(double)); + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(tokens), token) { + zend_ulong token_id; + if (Z_TYPE_P(token) != IS_LONG || Z_LVAL_P(token) < 0 || (zend_ulong) Z_LVAL_P(token) >= vocab) { + efree(sums); + zend_throw_exception_ex(king_ce_validation_exception, 0, "Embedding token id is outside the model vocabulary."); + return FAILURE; + } + token_id = (zend_ulong) Z_LVAL_P(token); + if (token_id > ZEND_ULONG_MAX / native_dimensions) { + efree(sums); + zend_throw_exception_ex(king_ce_validation_exception, 0, "Embedding token offset exceeds native tensor bounds."); + return FAILURE; + } + for (col = 0; col < output_dimensions; col++) { + double value; + if (king_inference_tensor_value_at(view, token_id * native_dimensions + col, &value) != SUCCESS) { + efree(sums); + return FAILURE; + } + sums[col] += value; + } + } ZEND_HASH_FOREACH_END(); + + array_init(&item); + add_assoc_string(&item, "object", "embedding"); + add_assoc_long(&item, "index", (zend_long) index); + if (base64_encoding) { + unsigned char *bytes; + zend_string *encoded; + size_t byte_length; + + if (output_dimensions > (zend_ulong) (SIZE_MAX / 4)) { + zval_ptr_dtor(&item); + efree(sums); + zend_throw_exception_ex(king_ce_validation_exception, 0, "Embedding output exceeds configured byte limits."); + return FAILURE; + } + byte_length = (size_t) output_dimensions * 4; + bytes = emalloc(byte_length); + for (col = 0; col < output_dimensions; col++) { + king_inference_openai_embeddings_write_float32le( + sums[col] / (double) token_count, + bytes + (col * 4) + ); + } + encoded = php_base64_encode(bytes, byte_length); + efree(bytes); + if (encoded == NULL) { + zval_ptr_dtor(&item); + efree(sums); + zend_throw_exception_ex(king_ce_runtime_exception, 0, "Embedding base64 encoding failed."); + return FAILURE; + } + add_assoc_str(&item, "embedding", encoded); + } else { + zval embedding; + array_init(&embedding); + for (col = 0; col < output_dimensions; col++) { + add_next_index_double(&embedding, sums[col] / (double) token_count); + } + add_assoc_zval(&item, "embedding", &embedding); + } + add_next_index_zval(data, &item); + + efree(sums); + *total_tokens += token_count; + return SUCCESS; +} + +static zend_result king_inference_openai_embeddings_add_string_input( + zval *data, + king_inference_model_object *model, + king_inference_tensor_native_view *view, + zend_string *input, + zend_ulong index, + zend_ulong native_dimensions, + zend_ulong output_dimensions, + zend_ulong vocab, + zend_ulong max_tokens, + bool base64_encoding, + zend_ulong *total_tokens) +{ + zval tokens; + zend_result result; + + if (king_inference_openai_embeddings_tokenize_text(model, input, &tokens) != SUCCESS) { + return FAILURE; + } + result = king_inference_openai_embeddings_add_vector( + data, + view, + &tokens, + index, + native_dimensions, + output_dimensions, + vocab, + max_tokens, + base64_encoding, + total_tokens + ); + zval_ptr_dtor(&tokens); + return result; +} + +static zend_result king_inference_openai_embeddings_payload_data( + zval *data, + king_inference_model_object *model, + king_inference_tensor_native_view *view, + zval *input, + zend_ulong native_dimensions, + zend_ulong output_dimensions, + zend_ulong vocab, + zend_ulong max_inputs, + zend_ulong max_tokens, + bool base64_encoding, + zend_ulong *total_tokens) +{ + zend_ulong index = 0; + zval *entry; + + array_init(data); + *total_tokens = 0; + + if (Z_TYPE_P(input) == IS_STRING) { + return king_inference_openai_embeddings_add_string_input( + data, + model, + view, + Z_STR_P(input), + 0, + native_dimensions, + output_dimensions, + vocab, + max_tokens, + base64_encoding, + total_tokens + ); + } + + if (!king_inference_openai_array_is_list(input)) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "Embeddings input must be a string, string list, token list, or token-list batch."); + return FAILURE; + } + if (zend_hash_num_elements(Z_ARRVAL_P(input)) == 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "Embeddings input must not be empty."); + return FAILURE; + } + + if (king_inference_openai_embeddings_list_is_long_list(input)) { + return king_inference_openai_embeddings_add_vector( + data, + view, + input, + 0, + native_dimensions, + output_dimensions, + vocab, + max_tokens, + base64_encoding, + total_tokens + ); + } + if (zend_hash_num_elements(Z_ARRVAL_P(input)) > max_inputs) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "Embeddings input batch size exceeds configured limits."); + return FAILURE; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(input), entry) { + zend_result result; + if (Z_TYPE_P(entry) == IS_STRING) { + result = king_inference_openai_embeddings_add_string_input( + data, + model, + view, + Z_STR_P(entry), + index, + native_dimensions, + output_dimensions, + vocab, + max_tokens, + base64_encoding, + total_tokens + ); + } else if (Z_TYPE_P(entry) == IS_ARRAY && king_inference_openai_embeddings_list_is_long_list(entry)) { + result = king_inference_openai_embeddings_add_vector( + data, + view, + entry, + index, + native_dimensions, + output_dimensions, + vocab, + max_tokens, + base64_encoding, + total_tokens + ); + } else { + zend_throw_exception_ex(king_ce_validation_exception, 0, "Embeddings input batch entries must be strings or token-id lists."); + return FAILURE; + } + if (result != SUCCESS) { + return FAILURE; + } + index++; + } ZEND_HASH_FOREACH_END(); + + return SUCCESS; +} + +static zend_result king_inference_openai_embeddings_payload_response( + zval *return_value, + zval *model_value, + zval *payload, + zval *options) +{ + king_inference_model_object *model = php_king_inference_model_obj_from_zend(Z_OBJ_P(model_value)); + zval *input = king_inference_array_find(payload, "input"); + zend_long max_inputs = 0; + zend_long max_tokens = 0; + zend_long max_dimensions = 0; + king_inference_tensor_native_view view; + zend_string *tensor_name = NULL; + zend_string *model_name = NULL; + zend_string *json = NULL; + zend_ulong dimensions = 0; + zend_ulong output_dimensions = 0; + zend_ulong vocab = 0; + zend_ulong total_tokens = 0; + bool base64_encoding = false; + zval data; + zval response_payload; + zval usage; + zend_result result; + const char *model_error = NULL; + const char *limit_error = NULL; + + if (!king_inference_openai_model_field_valid(payload, &model_error)) { + return king_inference_openai_error_response( + return_value, + 400, + model_error != NULL ? model_error : "OpenAI model is invalid.", + "invalid_request_error" + ); + } + if (input == NULL) { + return king_inference_openai_error_response( + return_value, + 400, + "Embeddings request requires input.", + "invalid_request_error" + ); + } + if (king_inference_openai_embeddings_encoding_format(payload, return_value, &base64_encoding) != SUCCESS) { + return Z_TYPE_P(return_value) == IS_ARRAY ? SUCCESS : FAILURE; + } + if (!king_inference_openai_positive_router_limit( + options, + "max_embedding_inputs", + 2048, + &max_inputs, + &limit_error + ) || !king_inference_openai_positive_router_limit( + options, + "max_embedding_tokens", + 8192, + &max_tokens, + &limit_error + ) || !king_inference_openai_positive_router_limit( + options, + "max_embedding_dimensions", + 8192, + &max_dimensions, + &limit_error + )) { + return king_inference_openai_error_response( + return_value, + 500, + limit_error != NULL ? limit_error : "King OpenAI router limit is invalid.", + "server_error" + ); + } + if (king_inference_openai_embeddings_resolve_view( + model, + options, + return_value, + &view, + &tensor_name, + &dimensions, + &vocab + ) != SUCCESS) { + return Z_TYPE_P(return_value) == IS_ARRAY ? SUCCESS : FAILURE; + } + if (king_inference_openai_embeddings_output_dimensions( + payload, + max_dimensions, + dimensions, + return_value, + &output_dimensions + ) != SUCCESS) { + zend_string_release(tensor_name); + return Z_TYPE_P(return_value) == IS_ARRAY ? SUCCESS : FAILURE; + } + + result = king_inference_openai_embeddings_payload_data( + &data, + model, + &view, + input, + dimensions, + output_dimensions, + vocab, + (zend_ulong) max_inputs, + (zend_ulong) max_tokens, + base64_encoding, + &total_tokens + ); + if (result != SUCCESS) { + bool validation_error = EG(exception) != NULL + && instanceof_function(EG(exception)->ce, king_ce_validation_exception); + zend_string_release(tensor_name); + zval_ptr_dtor(&data); + if (EG(exception)) { + zend_clear_exception(); + return king_inference_openai_error_response( + return_value, + validation_error ? 400 : 500, + validation_error + ? "Embedding request could not be processed within configured limits." + : "King embedding computation failed for the loaded model.", + validation_error ? "invalid_request_error" : "server_error" + ); + } + return FAILURE; + } + + model_name = king_inference_openai_embeddings_payload_model_name(model_value, payload); + array_init(&response_payload); + add_assoc_string(&response_payload, "object", "list"); + add_assoc_zval(&response_payload, "data", &data); + add_assoc_str(&response_payload, "model", model_name); + add_assoc_str(&response_payload, "embedding_tensor", tensor_name); + add_assoc_string(&response_payload, "encoding_format", base64_encoding ? "base64" : "float"); + add_assoc_long(&response_payload, "dimensions", (zend_long) output_dimensions); + add_assoc_long(&response_payload, "native_dimensions", (zend_long) dimensions); + array_init(&usage); + add_assoc_long(&usage, "prompt_tokens", (zend_long) total_tokens); + add_assoc_long(&usage, "total_tokens", (zend_long) total_tokens); + add_assoc_zval(&response_payload, "usage", &usage); + + result = king_inference_openai_json_encode(&response_payload, &json); + zval_ptr_dtor(&response_payload); + if (result != SUCCESS) { + return FAILURE; + } + + king_inference_openai_http_array_response(return_value, 200, "application/json", json, false); + zend_string_release(json); + return SUCCESS; +} diff --git a/extension/src/inference/openai/resources/openai_invalid_backend_capabilities.inc b/extension/src/inference/openai/resources/openai_invalid_backend_capabilities.inc new file mode 100644 index 000000000..e2570b97a --- /dev/null +++ b/extension/src/inference/openai/resources/openai_invalid_backend_capabilities.inc @@ -0,0 +1,98 @@ +static void king_inference_openai_invalid_backend_capabilities(zval *return_value) +{ + zval reference_backend; + + array_init(return_value); + add_assoc_long(return_value, "schema_version", 2); + add_assoc_string(return_value, "scope", "implemented_capability_contract"); + add_assoc_string(return_value, "truth_source", "invalid_backend_config"); + add_assoc_bool(return_value, "runtime_admission", false); + add_assoc_string(return_value, "runtime_admission_source", "x_king.runtime_surface"); + add_assoc_bool(return_value, "implemented", false); + add_assoc_bool(return_value, "local", false); + add_assoc_bool(return_value, "streaming", false); + add_assoc_bool(return_value, "openai_chat_completions_stream", false); + add_assoc_bool(return_value, "cancellable", false); + add_assoc_bool(return_value, "model_registration", true); + add_assoc_bool(return_value, "model_metadata", false); + add_assoc_bool(return_value, "gguf_metadata", false); + add_assoc_bool(return_value, "gguf_architecture_classification", false); + add_assoc_bool(return_value, "process_runner", false); + add_assoc_bool(return_value, "native_runtime", false); + add_assoc_bool(return_value, "native_model_loader", false); + add_assoc_bool(return_value, "native_tensor_directory", false); + add_assoc_bool(return_value, "native_tensor_views", false); + add_assoc_bool(return_value, "native_model_mapping", false); + add_assoc_bool(return_value, "native_tokenization", false); + add_assoc_bool(return_value, "native_token_decode", false); + add_assoc_string(return_value, "native_token_decode_scope", "unavailable"); + add_assoc_bool(return_value, "synthetic_token_vector_graph", false); + add_assoc_bool(return_value, "synthetic_token_vector_scores", false); + add_assoc_bool(return_value, "prompt_to_logits_generation", false); + add_assoc_bool(return_value, "paged_kv_cache", false); + add_assoc_bool(return_value, "native_graph_streaming", false); + add_assoc_bool(return_value, "native_token_selection", false); + add_assoc_bool(return_value, "token_generation", false); + add_assoc_bool(return_value, "openai_generation", false); + add_assoc_bool(return_value, "embeddings", false); + add_assoc_bool(return_value, "gpu", false); + add_assoc_bool(return_value, "gpu_backend", false); + add_assoc_bool(return_value, "gpu_runtime_status", false); + add_assoc_bool(return_value, "gpu_cuda_driver_probe", false); + add_assoc_bool(return_value, "gpu_cuda_context", false); + add_assoc_bool(return_value, "gpu_cuda_context_owned", false); + add_assoc_bool(return_value, "gpu_device_memory_allocator", false); + add_assoc_bool(return_value, "gpu_host_to_device_weight_upload", false); + add_assoc_bool(return_value, "gpu_uploaded_weight_cache", false); + add_assoc_bool(return_value, "gpu_quantized_matvec_kernel", false); + add_assoc_bool(return_value, "gpu_rms_norm_kernel", false); + add_assoc_bool(return_value, "gpu_rope_kernel", false); + add_assoc_bool(return_value, "gpu_attention_scores_kernel", false); + add_assoc_bool(return_value, "gpu_attention_softmax_kernel", false); + add_assoc_bool(return_value, "gpu_attention_value_aggregation_kernel", false); + add_assoc_bool(return_value, "gpu_ffn_swiglu_path", false); + add_assoc_bool(return_value, "gpu_output_projection_path", false); + add_assoc_bool(return_value, "gpu_minimized_logits_readback", false); + add_assoc_bool(return_value, "gpu_embedding_row_loader", false); + add_assoc_bool(return_value, "gpu_device_vector_ops", false); + add_assoc_bool(return_value, "gpu_device_kv_cache", false); + add_assoc_bool(return_value, "gpu_decoder_graph_executor", false); + add_assoc_bool(return_value, "gpu_decoder_graph_result_envelope", false); + add_assoc_bool(return_value, "gpu_decoder_graph_execution_plan", false); + add_assoc_bool(return_value, "reference_backend_contract", false); + add_assoc_bool(return_value, "numeric_reference_backend", false); + add_assoc_bool(return_value, "reference_backend_active_in_production", false); + add_assoc_bool(return_value, "gpu_decoder_graph_embedding_execution", false); + add_assoc_bool(return_value, "gpu_decoder_graph_rms_norm_execution", false); + add_assoc_bool(return_value, "gpu_decoder_graph_linear_execution", false); + add_assoc_bool(return_value, "gpu_decoder_graph_slice_execution", false); + add_assoc_bool(return_value, "gpu_decoder_graph_rope_execution", false); + add_assoc_bool(return_value, "gpu_decoder_graph_kv_head_prepare_execution", false); + add_assoc_bool(return_value, "gpu_decoder_graph_kv_write_execution", false); + add_assoc_bool(return_value, "gpu_decoder_graph_kv_attention_execution", false); + add_assoc_bool(return_value, "gpu_decoder_graph_attention_stack_slot_execution", false); + add_assoc_bool(return_value, "gpu_decoder_graph_attention_heads_execution", false); + add_assoc_bool(return_value, "gpu_decoder_graph_attention_stack_execution", false); + add_assoc_bool(return_value, "gpu_decoder_graph_attention_output_projection_execution", false); + add_assoc_bool(return_value, "gpu_decoder_graph_attention_residual_execution", false); + add_assoc_bool(return_value, "gpu_decoder_graph_ffn_norm_execution", false); + add_assoc_bool(return_value, "gpu_decoder_graph_ffn_gate_up_projection_execution", false); + add_assoc_bool(return_value, "gpu_decoder_graph_ffn_swiglu_execution", false); + add_assoc_bool(return_value, "gpu_decoder_graph_ffn_down_projection_execution", false); + add_assoc_bool(return_value, "gpu_decoder_graph_ffn_output_residual_execution", false); + add_assoc_bool(return_value, "gpu_decoder_graph_final_norm_execution", false); + add_assoc_bool(return_value, "gpu_decoder_graph_logits_projection_execution", false); + add_assoc_bool(return_value, "gpu_prompt_decoder_loop", false); + add_assoc_bool(return_value, "gpu_plain_text_chat_generation", false); + add_assoc_bool(return_value, "gpu_vram_admission", false); + add_assoc_bool(return_value, "gpu_kv_cache_vram_estimate", false); + add_assoc_bool(return_value, "gpu_thermal_policy", false); + add_assoc_bool(return_value, "gpu_thermal_preflight", false); + add_assoc_bool(return_value, "gpu_thermal_stream_abort", false); + add_assoc_bool(return_value, "gpu_decoder_kernel", false); + add_assoc_bool(return_value, "gpu_generation", false); + add_assoc_bool(return_value, "silent_cpu_fallback", false); + add_assoc_string(return_value, "model_format", "gguf"); + king_inference_reference_backend_contract_array(KING_INFERENCE_BACKEND_LOCAL, &reference_backend); + add_assoc_zval(return_value, "reference_backend", &reference_backend); +} diff --git a/extension/src/inference/openai/resources/openai_models.inc b/extension/src/inference/openai/resources/openai_models.inc new file mode 100644 index 000000000..c470648e4 --- /dev/null +++ b/extension/src/inference/openai/resources/openai_models.inc @@ -0,0 +1,779 @@ +#include "openai_route_reuse_profile.inc" +#include "openai_runtime_surface.inc" +#include "openai_invalid_backend_capabilities.inc" + +static bool king_inference_openai_is_model(zval *value) +{ + return value != NULL + && Z_TYPE_P(value) == IS_OBJECT + && instanceof_function(Z_OBJCE_P(value), king_ce_inference_model); +} + +static zend_string *king_inference_openai_model_entry_id( + zval *model_value, + zend_string *key, + zend_ulong index) +{ + king_inference_model_object *model; + + if (key != NULL && ZSTR_LEN(key) > 0) { + return zend_string_copy(key); + } + + model = php_king_inference_model_obj_from_zend(Z_OBJ_P(model_value)); + if (model->name != NULL && ZSTR_LEN(model->name) > 0) { + return zend_string_copy(model->name); + } + + return strpprintf(0, "king-model-%lu", index); +} + +static bool king_inference_openai_models_valid(zval *models) +{ + zval *entry; + zend_string *key; + zend_ulong index; + zend_string *listed_id; + HashTable listed_ids; + bool valid = true; + + if (models == NULL || Z_TYPE_P(models) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL_P(models)) == 0) { + return false; + } + + zend_hash_init(&listed_ids, 0, NULL, NULL, 0); + ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(models), index, key, entry) { + if (!king_inference_openai_is_model(entry)) { + valid = false; + break; + } + + listed_id = king_inference_openai_model_entry_id(entry, key, index); + if (zend_hash_exists(&listed_ids, listed_id) + || zend_hash_add_empty_element(&listed_ids, listed_id) == NULL) { + zend_string_release(listed_id); + valid = false; + break; + } + zend_string_release(listed_id); + } ZEND_HASH_FOREACH_END(); + zend_hash_destroy(&listed_ids); + + return valid; +} + +static zend_string *king_inference_openai_model_owner(zval *model_value, zval *options) +{ + king_inference_model_object *model = php_king_inference_model_obj_from_zend(Z_OBJ_P(model_value)); + zval *owner = king_inference_array_find(options, "owned_by"); + + if (owner != NULL && Z_TYPE_P(owner) == IS_STRING && Z_STRLEN_P(owner) > 0) { + return zend_string_copy(Z_STR_P(owner)); + } + + owner = king_inference_array_find(&model->config, "owned_by"); + if (owner != NULL && Z_TYPE_P(owner) == IS_STRING && Z_STRLEN_P(owner) > 0) { + return zend_string_copy(Z_STR_P(owner)); + } + + return zend_string_init("king", sizeof("king") - 1, 0); +} + +static void king_inference_openai_model_aliases_array(zval *model_value, zval *return_value) +{ + king_inference_model_object *model = php_king_inference_model_obj_from_zend(Z_OBJ_P(model_value)); + zval *aliases = king_inference_array_find(&model->config, "aliases"); + zval *alias; + + array_init(return_value); + if (aliases == NULL || Z_TYPE_P(aliases) != IS_ARRAY) { + return; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(aliases), alias) { + if (alias != NULL && Z_TYPE_P(alias) == IS_STRING && Z_STRLEN_P(alias) > 0) { + add_next_index_str(return_value, zend_string_copy(Z_STR_P(alias))); + } + } ZEND_HASH_FOREACH_END(); +} + +static bool king_inference_openai_model_listing_options_valid(zval *options, const char **message) +{ + zval *owner = king_inference_array_find(options, "owned_by"); + + if (message != NULL) { + *message = NULL; + } + if (owner == NULL) { + return true; + } + if (Z_TYPE_P(owner) == IS_STRING && Z_STRLEN_P(owner) > 0) { + return true; + } + if (message != NULL) { + *message = "OpenAI model listing option owned_by must be a non-empty string when provided."; + } + return false; +} + +static void king_inference_openai_client_capabilities_array( + zval *return_value, + bool backend_config_valid, + king_inference_backend_kind kind, + bool gpu_enabled, + bool gpu_generation_ready) +{ + bool local_backend = kind == KING_INFERENCE_BACKEND_LOCAL; + bool native_cpu_backend = kind == KING_INFERENCE_BACKEND_KING_NATIVE_CPU; + bool native_gpu_backend = kind == KING_INFERENCE_BACKEND_KING_NATIVE_GPU; + bool openai_text_generation = backend_config_valid + && ((local_backend && !gpu_enabled) + || native_cpu_backend + || (native_gpu_backend && gpu_generation_ready)); + bool native_stream_contract = backend_config_valid + && (native_cpu_backend || native_gpu_backend); + + array_init(return_value); + add_assoc_long(return_value, "version", 1); + add_assoc_bool(return_value, "model_selectable", backend_config_valid); + add_assoc_bool(return_value, "generation_ready", openai_text_generation); + add_assoc_bool(return_value, "plain_text_chat", openai_text_generation); + add_assoc_bool(return_value, "openai_chat_completions", openai_text_generation); + add_assoc_bool(return_value, "openai_chat_completions_stream", openai_text_generation); + add_assoc_bool(return_value, "openai_responses", openai_text_generation); + add_assoc_bool(return_value, "openai_completions", openai_text_generation); + add_assoc_bool(return_value, "openai_embeddings", backend_config_valid && native_cpu_backend); + add_assoc_bool(return_value, "prompt_to_logits_generation", openai_text_generation); + add_assoc_bool(return_value, "native_graph_streaming", backend_config_valid && native_cpu_backend); + add_assoc_bool(return_value, "native_stream_contract", native_stream_contract); + add_assoc_bool(return_value, "synthetic_token_vector_graph", native_stream_contract); + add_assoc_string( + return_value, + "synthetic_token_vector_score_semantics", + native_stream_contract ? "direct_token_id_uses_probability_1_logit_0_rank_0" : "unavailable" + ); + add_assoc_bool(return_value, "requires_gpu", gpu_enabled || native_gpu_backend); + add_assoc_bool(return_value, "gpu_runtime_required", gpu_enabled || native_gpu_backend); + add_assoc_bool(return_value, "gpu_generation_ready", backend_config_valid && native_gpu_backend && gpu_generation_ready); + add_assoc_bool(return_value, "openai_tools", false); + add_assoc_bool(return_value, "openai_tool_calls", false); + add_assoc_bool(return_value, "openai_function_calling", false); + add_assoc_bool(return_value, "openai_parallel_tool_calls", false); + add_assoc_bool(return_value, "openai_vision", false); + add_assoc_bool(return_value, "openai_audio", false); + add_assoc_string(return_value, "tool_call_status", "unsupported"); +} + +static const char *king_inference_openai_route_hot_path( + king_inference_model_object *model, + king_inference_backend_kind kind, + bool backend_config_valid, + bool openai_generation_ready +) { + if (!backend_config_valid || !openai_generation_ready) { + return "fallback_unavailable"; + } + + switch (kind) { + case KING_INFERENCE_BACKEND_KING_NATIVE_GPU: + if (model->cuda_decoder_prompt_loop_last_used_batch_prefill) { + return "batch_prefill_decode"; + } + if (model->cuda_decoder_prompt_loop_last_prompt_tokens > 0) { + return "sequential_prefill_decode"; + } + if (king_inference_cuda_decoder_prompt_loop_batch_prefill_admitted(model)) { + return "batch_prefill_decode"; + } + if (model->cuda_decoder_prompt_loop_available) { + return "sequential_prefill_decode"; + } + return "fallback_unavailable"; + + case KING_INFERENCE_BACKEND_KING_NATIVE_CPU: + return "cached_decode"; + + case KING_INFERENCE_BACKEND_LOCAL: + return "fallback"; + } + + return "fallback_unavailable"; +} + +static const char *king_inference_openai_route_last_hot_path( + king_inference_model_object *model, + king_inference_backend_kind kind +) { + if (kind == KING_INFERENCE_BACKEND_KING_NATIVE_GPU + && model->cuda_decoder_prompt_loop_last_prompt_tokens > 0) { + return model->cuda_decoder_prompt_loop_last_used_batch_prefill + ? "batch_prefill_decode" + : "sequential_prefill_decode"; + } + + if (kind == KING_INFERENCE_BACKEND_KING_NATIVE_CPU + && model->runtime_last_generated_tokens > 0) { + return "cached_decode"; + } + + if (kind == KING_INFERENCE_BACKEND_LOCAL + && model->runtime_last_generated_tokens > 0) { + return "fallback"; + } + + return "not_observed"; +} + +static void king_inference_openai_route_status_array( + zval *return_value, + king_inference_model_object *model, + king_inference_backend_kind kind, + bool backend_config_valid, + bool openai_generation_ready, + zval *native_engine_readiness +) { + bool native_backend = kind == KING_INFERENCE_BACKEND_KING_NATIVE_CPU + || kind == KING_INFERENCE_BACKEND_KING_NATIVE_GPU; + bool gpu_backend = kind == KING_INFERENCE_BACKEND_KING_NATIVE_GPU; + bool lazy_gpu_token_stream = gpu_backend && model->cuda_decoder_prompt_loop_available; + bool native_plain_text_admitted = king_inference_native_readiness_bool( + native_engine_readiness, + "plain_text_generation_admitted" + ); + bool native_graph_payload_admitted = king_inference_native_readiness_bool( + native_engine_readiness, + "native_graph_payload_admitted" + ); + + array_init(return_value); + add_assoc_long(return_value, "schema_version", 1); + add_assoc_bool(return_value, "openai_generation_ready", openai_generation_ready); + add_assoc_string(return_value, "native_engine_admission_source", "x_king.native_engine_readiness"); + add_assoc_bool(return_value, "native_engine_plain_text_generation_admitted", native_plain_text_admitted); + add_assoc_bool(return_value, "native_engine_graph_payload_admitted", native_graph_payload_admitted); + add_assoc_string(return_value, "prompt_formatting_owner", "openai_router"); + add_assoc_string(return_value, "tool_fields_owner", "openai_router"); + add_assoc_string(return_value, "error_response_owner", "openai_router"); + add_assoc_bool(return_value, "stream_transport", true); + add_assoc_string(return_value, "stream_transport_mode", "sse_body_stream"); + add_assoc_bool(return_value, "immediate_token_streaming", lazy_gpu_token_stream); + add_assoc_bool(return_value, "buffered_full_response_drain", false); + add_assoc_bool(return_value, "buffered_native_event_drain", native_backend && !lazy_gpu_token_stream); + add_assoc_string( + return_value, + "stream_content_delivery", + lazy_gpu_token_stream + ? "lazy_native_token_generation" + : (native_backend ? "buffered_native_event_drain" : "external_runner_stream") + ); + add_assoc_string( + return_value, + "stream_construction_mode", + lazy_gpu_token_stream ? "setup_only_generation_on_next" : "backend_start_generates_events" + ); + add_assoc_string(return_value, "non_stream_response_mode", "buffered_full_response_drain"); + add_assoc_string(return_value, "compat_http_stream_response_mode", "buffered_full_response_drain"); + add_assoc_string( + return_value, + "active_hot_path", + king_inference_openai_route_hot_path(model, kind, backend_config_valid, openai_generation_ready) + ); + add_assoc_string(return_value, "last_observed_hot_path", king_inference_openai_route_last_hot_path(model, kind)); + add_assoc_bool(return_value, "sequential_prefill_available", gpu_backend && model->cuda_decoder_prompt_loop_available); + add_assoc_bool(return_value, "batch_prefill_compiled", gpu_backend); + add_assoc_bool(return_value, "batch_prefill_available", gpu_backend && model->cuda_decoder_prompt_loop_batch_prefill_available); + add_assoc_bool(return_value, "batch_prefill_ready", gpu_backend && king_inference_cuda_decoder_prompt_loop_batch_prefill_ready(model)); + add_assoc_bool(return_value, "batch_prefill_experimental_enabled", gpu_backend && king_inference_cuda_decoder_prompt_loop_batch_prefill_experimental_enabled(model)); + add_assoc_bool(return_value, "batch_prefill_admitted", gpu_backend && king_inference_cuda_decoder_prompt_loop_batch_prefill_admitted(model)); + add_assoc_bool(return_value, "batch_prefill_request_control_allowed", false); + add_assoc_string( + return_value, + "batch_prefill_status", + gpu_backend + ? king_inference_cuda_decoder_prompt_loop_batch_prefill_status(model) + : "non_gpu_backend" + ); + add_assoc_string( + return_value, + "batch_prefill_admission_reason", + gpu_backend + ? king_inference_cuda_decoder_prompt_loop_batch_prefill_admission_reason(model) + : "non_gpu_backend" + ); + add_assoc_string( + return_value, + "batch_prefill_use_condition", + "prompt_tokens >= batch_prefill_min_prompt_tokens && batch_prefill_admitted" + ); + add_assoc_bool(return_value, "batch_prefill_last_used", gpu_backend && model->cuda_decoder_prompt_loop_last_used_batch_prefill); + add_assoc_long( + return_value, + "batch_prefill_min_prompt_tokens", + gpu_backend ? (zend_long) KING_INFERENCE_CUDA_PROMPT_BATCH_PREFILL_MIN_TOKENS : 0 + ); + add_assoc_long( + return_value, + "last_prompt_tokens", + gpu_backend ? (zend_long) model->cuda_decoder_prompt_loop_last_prompt_tokens : 0 + ); + add_assoc_long( + return_value, + "last_prefill_batches", + gpu_backend ? (zend_long) model->cuda_decoder_prompt_loop_last_prefill_batches : 0 + ); + add_assoc_long( + return_value, + "last_prefill_tokens", + gpu_backend ? (zend_long) model->cuda_decoder_prompt_loop_last_prefill_tokens : 0 + ); + add_assoc_long( + return_value, + "last_prefill_template_reuses", + gpu_backend ? (zend_long) model->cuda_decoder_prompt_loop_last_prefill_template_reuses : 0 + ); + if (gpu_backend) { + king_inference_cuda_decoder_prompt_profile_add_status(return_value, model); + king_inference_openai_route_reuse_profile_add_status(return_value, model, gpu_backend); + } + add_assoc_bool(return_value, "cached_decode_available", kind == KING_INFERENCE_BACKEND_KING_NATIVE_CPU); + add_assoc_bool(return_value, "fused_decode_available", false); + add_assoc_bool(return_value, "fallback_available", kind == KING_INFERENCE_BACKEND_LOCAL); + add_assoc_bool(return_value, "silent_cpu_fallback", false); +} + +static void king_inference_openai_model_object( + zval *return_value, + zval *model_value, + zend_string *key, + zend_ulong index, + zval *options) +{ + zend_string *id = king_inference_openai_model_entry_id(model_value, key, index); + zend_string *owner = king_inference_openai_model_owner(model_value, options); + king_inference_model_object *model = php_king_inference_model_obj_from_zend(Z_OBJ_P(model_value)); + zval *created = king_inference_array_find(&model->config, "created"); + king_inference_backend_kind kind; + zval king_meta; + zval capabilities; + zval client_capabilities; + zval gpu_runtime; + zval runtime_truth; + zval runtime_readiness; + zval runtime_surface; + zval native_engine_readiness; + zval error_taxonomy; + zval route_status; + zval aliases; + bool backend_config_valid = true; + bool created_config_valid = created != NULL && Z_TYPE_P(created) == IS_LONG && Z_LVAL_P(created) >= 0; + bool gpu_enabled = false; + bool gpu_openai_generation_ready = false; + bool openai_generation_ready = false; + bool native_graph_streaming_ready = false; + bool native_stream_contract_ready = false; + bool embeddings_ready = false; + + ZVAL_UNDEF(&gpu_runtime); + + array_init(return_value); + add_assoc_str(return_value, "id", id); + add_assoc_string(return_value, "object", "model"); + if (created_config_valid && created != NULL) { + add_assoc_long(return_value, "created", Z_LVAL_P(created)); + } else { + add_assoc_long(return_value, "created", 0); + } + add_assoc_str(return_value, "owned_by", owner); + + if (king_inference_backend_kind_from_config( + &model->config, + &kind, + "king_inference_openai_http_response" + ) != SUCCESS) { + if (EG(exception)) { + zend_clear_exception(); + } + backend_config_valid = false; + kind = KING_INFERENCE_BACKEND_LOCAL; + } + + array_init(&king_meta); + king_inference_model_native_engine_readiness_array(model, &native_engine_readiness); + gpu_enabled = backend_config_valid && king_inference_gpu_enabled(&model->config); + if (gpu_enabled) { + king_inference_gpu_runtime_status_from_model(model, &gpu_runtime); + gpu_openai_generation_ready = king_inference_native_readiness_bool( + &native_engine_readiness, + "plain_text_generation_admitted" + ); + } + openai_generation_ready = backend_config_valid + && ((kind == KING_INFERENCE_BACKEND_LOCAL && !gpu_enabled) + || kind == KING_INFERENCE_BACKEND_KING_NATIVE_CPU + || (kind == KING_INFERENCE_BACKEND_KING_NATIVE_GPU + && gpu_openai_generation_ready)); + native_graph_streaming_ready = king_inference_native_readiness_bool( + &native_engine_readiness, + "native_graph_payload_admitted" + ) && kind == KING_INFERENCE_BACKEND_KING_NATIVE_CPU; + native_stream_contract_ready = king_inference_native_readiness_bool( + &native_engine_readiness, + "native_stream_contract_admitted" + ); + embeddings_ready = backend_config_valid && kind == KING_INFERENCE_BACKEND_KING_NATIVE_CPU; + + add_assoc_string( + &king_meta, + "backend", + backend_config_valid ? king_inference_backend_kind_name(kind) : "invalid" + ); + king_inference_openai_model_aliases_array(model_value, &aliases); + add_assoc_zval(&king_meta, "aliases", &aliases); + add_assoc_bool(&king_meta, "backend_config_valid", backend_config_valid); + add_assoc_bool(&king_meta, "openai_generation", openai_generation_ready); + add_assoc_bool(&king_meta, "openai_chat_completions_stream", openai_generation_ready); + add_assoc_bool(&king_meta, "native_graph_streaming", native_graph_streaming_ready); + add_assoc_bool(&king_meta, "native_stream_contract", native_stream_contract_ready); + add_assoc_bool(&king_meta, "embeddings", embeddings_ready); + add_assoc_bool(&king_meta, "gpu_enabled", gpu_enabled); + { + zval native_engine_copy; + ZVAL_COPY(&native_engine_copy, &native_engine_readiness); + add_assoc_zval(&king_meta, "native_engine_readiness", &native_engine_copy); + } + king_inference_openai_runtime_surface_array( + &runtime_surface, + model, + kind, + backend_config_valid, + gpu_enabled, + gpu_openai_generation_ready, + openai_generation_ready, + native_graph_streaming_ready, + native_stream_contract_ready, + embeddings_ready, + gpu_enabled ? &gpu_runtime : NULL + ); + add_assoc_zval(&king_meta, "runtime_surface", &runtime_surface); + king_inference_error_taxonomy_catalog_array(&error_taxonomy); + add_assoc_zval(&king_meta, "error_taxonomy", &error_taxonomy); + if (gpu_enabled) { + add_assoc_zval(&king_meta, "gpu_runtime", &gpu_runtime); + } + king_inference_model_runtime_truth_array(model, &runtime_truth); + king_inference_model_add_runtime_surface_fields(model, &king_meta, &runtime_truth); + add_assoc_zval(&king_meta, "runtime_truth", &runtime_truth); + add_assoc_bool(&king_meta, "created_config_valid", created_config_valid); + add_assoc_long(&king_meta, "capability_schema_version", 2); + add_assoc_string(&king_meta, "capabilities_scope", "implemented_capability_contract"); + add_assoc_string(&king_meta, "readiness_scope", "active_runtime_state"); + king_inference_openai_client_capabilities_array( + &client_capabilities, + backend_config_valid, + kind, + gpu_enabled, + gpu_openai_generation_ready + ); + add_assoc_zval(&king_meta, "client_capabilities", &client_capabilities); + if (backend_config_valid) { + king_inference_backend_capabilities_array(kind, &capabilities); + } else { + king_inference_openai_invalid_backend_capabilities(&capabilities); + } + add_assoc_zval(&king_meta, "capabilities", &capabilities); + king_inference_model_readiness_truth_array(model, &runtime_readiness); + add_assoc_zval(&king_meta, "runtime_readiness", &runtime_readiness); + king_inference_openai_route_status_array( + &route_status, + model, + kind, + backend_config_valid, + openai_generation_ready, + &native_engine_readiness + ); + add_assoc_zval(&king_meta, "openai_route", &route_status); + zval_ptr_dtor(&native_engine_readiness); + add_assoc_zval(return_value, "x_king", &king_meta); +} + +static zend_result king_inference_openai_models_response(zval *models, zval *options, zval *return_value) +{ + zval payload; + zval data; + zval *entry; + zend_string *key; + zend_ulong index; + zend_string *json = NULL; + zend_result result; + const char *options_error = NULL; + + if (!king_inference_openai_models_valid(models)) { + return king_inference_openai_error_response( + return_value, + 500, + "OpenAI-compatible inference router requires a non-empty model registry containing only loaded models with unique listed ids.", + "server_error" + ); + } + if (!king_inference_openai_model_listing_options_valid(options, &options_error)) { + return king_inference_openai_error_response( + return_value, + 500, + options_error != NULL ? options_error : "OpenAI model listing options are invalid.", + "server_error" + ); + } + + array_init(&payload); + add_assoc_string(&payload, "object", "list"); + array_init(&data); + + ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(models), index, key, entry) { + zval model_object; + + king_inference_openai_model_object(&model_object, entry, key, index, options); + add_next_index_zval(&data, &model_object); + } ZEND_HASH_FOREACH_END(); + + add_assoc_zval(&payload, "data", &data); + result = king_inference_openai_json_encode(&payload, &json); + zval_ptr_dtor(&payload); + if (result != SUCCESS) { + return FAILURE; + } + + king_inference_openai_http_array_response(return_value, 200, "application/json", json, false); + zend_string_release(json); + return SUCCESS; +} + +static zval *king_inference_openai_select_first_model(zval *models) +{ + zval *entry; + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(models), entry) { + return king_inference_openai_is_model(entry) ? entry : NULL; + } ZEND_HASH_FOREACH_END(); + + return NULL; +} + +static bool king_inference_openai_model_name_matches(zval *model_value, zend_string *name) +{ + king_inference_model_object *model = php_king_inference_model_obj_from_zend(Z_OBJ_P(model_value)); + + return model->name != NULL && zend_string_equals(model->name, name); +} + +static bool king_inference_openai_model_alias_matches(zval *model_value, zend_string *name) +{ + king_inference_model_object *model = php_king_inference_model_obj_from_zend(Z_OBJ_P(model_value)); + zval *aliases = king_inference_array_find(&model->config, "aliases"); + zval *alias; + + if (aliases == NULL || Z_TYPE_P(aliases) != IS_ARRAY) { + return false; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(aliases), alias) { + if (alias != NULL && Z_TYPE_P(alias) == IS_STRING && zend_string_equals(Z_STR_P(alias), name)) { + return true; + } + } ZEND_HASH_FOREACH_END(); + + return false; +} + +static bool king_inference_openai_model_listed_id_matches( + zval *model_value, + zend_string *key, + zend_ulong index, + zend_string *requested) +{ + zend_string *listed_id; + bool matches; + + listed_id = king_inference_openai_model_entry_id(model_value, key, index); + matches = zend_string_equals(listed_id, requested); + zend_string_release(listed_id); + return matches; +} + +static zval *king_inference_openai_find_model(zval *models, zval *payload) +{ + zval *requested = king_inference_array_find(payload, "model"); + zval *entry; + zend_string *key; + zend_ulong index; + + if (models == NULL || Z_TYPE_P(models) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL_P(models)) == 0) { + return NULL; + } + + if (requested == NULL || Z_TYPE_P(requested) != IS_STRING || Z_STRLEN_P(requested) == 0) { + return zend_hash_num_elements(Z_ARRVAL_P(models)) == 1 + ? king_inference_openai_select_first_model(models) + : NULL; + } + + entry = zend_hash_find(Z_ARRVAL_P(models), Z_STR_P(requested)); + if (king_inference_openai_is_model(entry)) { + return entry; + } + + ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(models), index, key, entry) { + if (king_inference_openai_is_model(entry) + && (king_inference_openai_model_name_matches(entry, Z_STR_P(requested)) + || king_inference_openai_model_alias_matches(entry, Z_STR_P(requested)) + || king_inference_openai_model_listed_id_matches(entry, key, index, Z_STR_P(requested)))) { + return entry; + } + } ZEND_HASH_FOREACH_END(); + + return NULL; +} + +static zval *king_inference_openai_find_model_by_id(zval *models, zend_string *requested) +{ + zval *entry; + zend_string *key; + zend_ulong index; + + if (requested == NULL || ZSTR_LEN(requested) == 0) { + return NULL; + } + + entry = zend_hash_find(Z_ARRVAL_P(models), requested); + if (king_inference_openai_is_model(entry)) { + return entry; + } + + ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(models), index, key, entry) { + if (king_inference_openai_is_model(entry) + && (king_inference_openai_model_name_matches(entry, requested) + || king_inference_openai_model_alias_matches(entry, requested) + || king_inference_openai_model_listed_id_matches(entry, key, index, requested))) { + return entry; + } + } ZEND_HASH_FOREACH_END(); + + return NULL; +} + +static zend_result king_inference_openai_require_model( + zval *models, + zval *decoded, + const char *endpoint_name, + zval *return_value, + zval **model_out) +{ + zval *requested_model; + const char *model_error = NULL; + + *model_out = NULL; + if (!king_inference_openai_models_valid(models)) { + return king_inference_openai_error_response( + return_value, + 500, + "OpenAI-compatible inference router requires a non-empty model registry containing only loaded models with unique listed ids.", + "server_error" + ); + } + + requested_model = king_inference_array_find(decoded, "model"); + if (!king_inference_openai_model_field_valid(decoded, &model_error)) { + return king_inference_openai_error_response( + return_value, + 400, + model_error != NULL ? model_error : endpoint_name, + "invalid_request_error" + ); + } + if (requested_model == NULL && zend_hash_num_elements(Z_ARRVAL_P(models)) > 1) { + return king_inference_openai_error_response( + return_value, + 400, + "Request must include model when multiple King models are registered.", + "invalid_request_error" + ); + } + + *model_out = king_inference_openai_find_model(models, decoded); + if (*model_out == NULL) { + return king_inference_openai_error_response( + return_value, + 404, + "Requested model is not registered in the King inference router.", + "invalid_request_error" + ); + } + + return SUCCESS; +} + +static zend_result king_inference_openai_retrieve_model_response( + zval *models, + zend_string *model_id, + zval *options, + zval *return_value) +{ + zval *entry; + zend_string *json = NULL; + zend_result result; + zval model_object; + zend_string *key; + zend_ulong index; + const char *options_error = NULL; + + if (!king_inference_openai_models_valid(models)) { + return king_inference_openai_error_response( + return_value, + 500, + "OpenAI-compatible inference router requires a non-empty model registry containing only loaded models with unique listed ids.", + "server_error" + ); + } + if (!king_inference_openai_model_listing_options_valid(options, &options_error)) { + return king_inference_openai_error_response( + return_value, + 500, + options_error != NULL ? options_error : "OpenAI model listing options are invalid.", + "server_error" + ); + } + + entry = king_inference_openai_find_model_by_id(models, model_id); + if (entry == NULL) { + return king_inference_openai_error_response( + return_value, + 404, + "Requested model is not registered in the King inference router.", + "invalid_request_error" + ); + } + + ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(models), index, key, entry) { + if (king_inference_openai_is_model(entry) + && (zend_hash_find(Z_ARRVAL_P(models), model_id) == entry + || king_inference_openai_model_name_matches(entry, model_id) + || king_inference_openai_model_alias_matches(entry, model_id) + || king_inference_openai_model_listed_id_matches(entry, key, index, model_id))) { + king_inference_openai_model_object(&model_object, entry, key, index, options); + result = king_inference_openai_json_encode(&model_object, &json); + zval_ptr_dtor(&model_object); + if (result != SUCCESS) { + return FAILURE; + } + king_inference_openai_http_array_response(return_value, 200, "application/json", json, false); + zend_string_release(json); + return SUCCESS; + } + } ZEND_HASH_FOREACH_END(); + + return king_inference_openai_error_response( + return_value, + 404, + "Requested model is not registered in the King inference router.", + "invalid_request_error" + ); +} diff --git a/extension/src/inference/openai/resources/openai_route_reuse_profile.inc b/extension/src/inference/openai/resources/openai_route_reuse_profile.inc new file mode 100644 index 000000000..2a7f06189 --- /dev/null +++ b/extension/src/inference/openai/resources/openai_route_reuse_profile.inc @@ -0,0 +1,154 @@ +/* + * OpenAI route reuse profile projection for native King GPU inference. + */ + +static void king_inference_openai_route_reuse_profile_add_status( + zval *return_value, + king_inference_model_object *model, + bool gpu_backend +) { + zval reuse; + bool available = gpu_backend + && model->cuda_decoder_prompt_loop_last_reuse_profile_available; + bool hot_token_profile = gpu_backend + && model->cuda_decoder_prompt_loop_last_hot_token_profile_available; + bool hot_path_reused = hot_token_profile + && model->cuda_decoder_prompt_loop_last_hot_token_graph_constructions == 0 + && model->cuda_decoder_prompt_loop_last_hot_token_execution_plans == 0; + bool kv_reused = available + && model->cuda_device_kv_cache_allocated + && model->cuda_decoder_prompt_loop_last_kv_cache_allocations <= 1; + bool interval_gated = available + && model->cuda_decoder_prompt_loop_last_policy_checks > 0 + && model->cuda_decoder_prompt_loop_last_policy_interval_skips > 0; + + array_init(&reuse); + add_assoc_bool(&reuse, "available", available); + if (!available) { + add_assoc_string( + &reuse, + "reason", + gpu_backend ? "no_completed_gpu_prompt_request_observed" : "non_gpu_backend" + ); + add_assoc_zval(return_value, "last_request_reuse_profile", &reuse); + return; + } + + add_assoc_long(&reuse, "prompt_tokens", (zend_long) model->cuda_decoder_prompt_loop_last_prompt_tokens); + add_assoc_long(&reuse, "generated_tokens", (zend_long) model->cuda_decoder_prompt_loop_last_decoded_tokens); + add_assoc_long(&reuse, "accepted_max_tokens", model->cuda_decoder_prompt_loop_last_max_tokens); + add_assoc_string(&reuse, "kv_cache_scope", "model_resident_lazy_first_use"); + add_assoc_bool(&reuse, "kv_cache_allocated", model->cuda_device_kv_cache_allocated); + add_assoc_bool(&reuse, "kv_cache_reused_across_generated_tokens", kv_reused); + add_assoc_bool( + &reuse, + "kv_cache_reallocated_per_generated_token", + model->cuda_decoder_prompt_loop_last_decoded_tokens > 0 + && model->cuda_decoder_prompt_loop_last_kv_cache_allocations + > model->cuda_decoder_prompt_loop_last_decoded_tokens + ); + add_assoc_long( + &reuse, + "kv_cache_reserve_calls", + (zend_long) model->cuda_decoder_prompt_loop_last_kv_cache_reserve_calls + ); + add_assoc_long( + &reuse, + "kv_cache_logical_allocations", + (zend_long) model->cuda_decoder_prompt_loop_last_kv_cache_allocations + ); + add_assoc_long( + &reuse, + "kv_cache_writes", + (zend_long) model->cuda_decoder_prompt_loop_last_kv_cache_writes + ); + add_assoc_long( + &reuse, + "kv_cache_batch_writes", + (zend_long) model->cuda_decoder_prompt_loop_last_kv_cache_batch_writes + ); + add_assoc_long( + &reuse, + "prefill_batches", + (zend_long) model->cuda_decoder_prompt_loop_last_prefill_batches + ); + add_assoc_long( + &reuse, + "prefill_tokens", + (zend_long) model->cuda_decoder_prompt_loop_last_prefill_tokens + ); + add_assoc_long( + &reuse, + "prefill_template_reuses", + (zend_long) model->cuda_decoder_prompt_loop_last_prefill_template_reuses + ); + add_assoc_bool(&reuse, "prefill_runs_once_before_decode", true); + add_assoc_string(&reuse, "decode_context_advance_mode", "resident_kv_cache_plus_mutated_decode_template"); + add_assoc_long( + &reuse, + "request_graph_constructions", + (zend_long) model->cuda_decoder_prompt_loop_last_graph_constructions + ); + add_assoc_long( + &reuse, + "request_execution_plan_constructions", + (zend_long) model->cuda_decoder_prompt_loop_last_plan_constructions + ); + add_assoc_bool(&reuse, "decode_graph_template_reused", hot_path_reused); + add_assoc_bool(&reuse, "persistent_decode_plan_index_reused", hot_path_reused); + add_assoc_long( + &reuse, + "hot_token_dynamic_op_scans", + hot_token_profile + ? (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_op_index_fallback_scans + : 0 + ); + add_assoc_bool( + &reuse, + "hot_token_dynamic_op_scans_eliminated", + hot_token_profile && model->cuda_decoder_prompt_loop_last_hot_token_op_index_fallback_scans == 0 + ); + add_assoc_bool( + &reuse, + "decode_graph_constructed_on_hot_path", + hot_token_profile && model->cuda_decoder_prompt_loop_last_hot_token_graph_constructions > 0 + ); + add_assoc_bool( + &reuse, + "execution_plan_constructed_on_hot_path", + hot_token_profile && model->cuda_decoder_prompt_loop_last_hot_token_execution_plans > 0 + ); + add_assoc_bool(&reuse, "prompt_recomputed_after_each_generated_token", !hot_path_reused); + add_assoc_long( + &reuse, + "policy_guard_calls", + (zend_long) model->cuda_decoder_prompt_loop_last_policy_checks + ); + add_assoc_long( + &reuse, + "policy_guard_interval_skips", + (zend_long) model->cuda_decoder_prompt_loop_last_policy_interval_skips + ); + add_assoc_long( + &reuse, + "thermal_runtime_sensor_reads", + (zend_long) model->cuda_decoder_prompt_loop_last_thermal_runtime_checks + ); + add_assoc_long( + &reuse, + "power_runtime_sensor_reads", + (zend_long) model->cuda_decoder_prompt_loop_last_power_runtime_checks + ); + add_assoc_long( + &reuse, + "thermal_check_interval_seconds", + model->cuda_decoder_prompt_loop_last_thermal_interval_seconds + ); + add_assoc_long( + &reuse, + "power_check_interval_seconds", + model->cuda_decoder_prompt_loop_last_power_interval_seconds + ); + add_assoc_bool(&reuse, "thermal_and_power_checks_interval_gated", interval_gated); + add_assoc_zval(return_value, "last_request_reuse_profile", &reuse); +} diff --git a/extension/src/inference/openai/resources/openai_runtime_surface.inc b/extension/src/inference/openai/resources/openai_runtime_surface.inc new file mode 100644 index 000000000..dede972cc --- /dev/null +++ b/extension/src/inference/openai/resources/openai_runtime_surface.inc @@ -0,0 +1,179 @@ +/* + * OpenAI model-list runtime surface: capability, admission, refusal, + * and experimental boundaries for client decisions. + */ + +static void king_inference_openai_runtime_surface_refusal_reasons( + zval *return_value, + bool backend_config_valid, + bool openai_generation_capable, + bool openai_generation_ready, + bool gpu_generation_capable, + bool gpu_generation_ready, + zval *gpu_runtime +) { + zval *gpu_reasons; + zval *gpu_reason; + zval *entry; + bool added_reason = false; + + array_init(return_value); + + if (!backend_config_valid) { + add_next_index_string(return_value, "backend_config_invalid"); + added_reason = true; + } + + if (gpu_runtime != NULL && Z_TYPE_P(gpu_runtime) == IS_ARRAY) { + gpu_reasons = king_inference_array_find(gpu_runtime, "refusal_reasons"); + if (gpu_reasons != NULL && Z_TYPE_P(gpu_reasons) == IS_ARRAY) { + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(gpu_reasons), entry) { + if (Z_TYPE_P(entry) == IS_STRING && Z_STRLEN_P(entry) > 0) { + add_next_index_str(return_value, zend_string_copy(Z_STR_P(entry))); + added_reason = true; + } + } ZEND_HASH_FOREACH_END(); + } + gpu_reason = king_inference_array_find(gpu_runtime, "reason"); + if (gpu_reason != NULL && Z_TYPE_P(gpu_reason) == IS_STRING && Z_STRLEN_P(gpu_reason) > 0) { + add_next_index_str(return_value, zend_string_copy(Z_STR_P(gpu_reason))); + added_reason = true; + } + } + + if (gpu_generation_capable && !gpu_generation_ready) { + add_next_index_string(return_value, "gpu_generation_not_admitted"); + added_reason = true; + } + + if (openai_generation_capable && !openai_generation_ready && !added_reason) { + add_next_index_string(return_value, "openai_generation_not_admitted"); + } +} + +static void king_inference_openai_runtime_surface_array( + zval *return_value, + king_inference_model_object *model, + king_inference_backend_kind kind, + bool backend_config_valid, + bool gpu_enabled, + bool gpu_generation_ready, + bool openai_generation_ready, + bool native_graph_streaming_ready, + bool native_stream_contract_ready, + bool embeddings_ready, + zval *gpu_runtime +) { + bool local_backend = backend_config_valid && kind == KING_INFERENCE_BACKEND_LOCAL; + bool native_cpu_backend = backend_config_valid && kind == KING_INFERENCE_BACKEND_KING_NATIVE_CPU; + bool native_gpu_backend = backend_config_valid && kind == KING_INFERENCE_BACKEND_KING_NATIVE_GPU; + bool openai_generation_capable = backend_config_valid + && ((local_backend && !gpu_enabled) || native_cpu_backend || native_gpu_backend); + bool native_stream_contract_capable = backend_config_valid + && (native_cpu_backend || native_gpu_backend); + bool native_graph_streaming_capable = backend_config_valid && native_cpu_backend; + bool embeddings_capable = backend_config_valid && native_cpu_backend; + bool gpu_generation_capable = backend_config_valid && native_gpu_backend; + bool batch_prefill_experimental_enabled = native_gpu_backend + && king_inference_cuda_decoder_prompt_loop_batch_prefill_experimental_enabled(model); + bool batch_prefill_admitted = native_gpu_backend + && king_inference_cuda_decoder_prompt_loop_batch_prefill_admitted(model); + bool batch_prefill_ready = native_gpu_backend + && king_inference_cuda_decoder_prompt_loop_batch_prefill_ready(model); + zval implemented; + zval admitted; + zval refused; + zval experimental; + zval refusal_reasons; + zval refusal_error_codes; + zval refusal_error_categories; + + array_init(return_value); + add_assoc_long(return_value, "schema_version", 1); + add_assoc_string(return_value, "scope", "runtime_admission_surface"); + add_assoc_string( + return_value, + "backend", + backend_config_valid ? king_inference_backend_kind_name(kind) : "invalid" + ); + add_assoc_bool(return_value, "backend_config_valid", backend_config_valid); + add_assoc_bool(return_value, "gpu_required", gpu_enabled || native_gpu_backend); + add_assoc_bool(return_value, "silent_cpu_fallback", false); + + array_init(&implemented); + add_assoc_bool(&implemented, "openai_generation", openai_generation_capable); + add_assoc_bool(&implemented, "openai_chat_completions", openai_generation_capable); + add_assoc_bool(&implemented, "openai_chat_completions_stream", openai_generation_capable); + add_assoc_bool(&implemented, "openai_responses", openai_generation_capable); + add_assoc_bool(&implemented, "openai_completions", openai_generation_capable); + add_assoc_bool(&implemented, "native_stream_contract", native_stream_contract_capable); + add_assoc_bool(&implemented, "native_graph_streaming", native_graph_streaming_capable); + add_assoc_bool(&implemented, "embeddings", embeddings_capable); + add_assoc_bool(&implemented, "gpu_generation", gpu_generation_capable); + add_assoc_bool(&implemented, "batch_prefill", native_gpu_backend); + add_assoc_zval(return_value, "implemented_capabilities", &implemented); + + array_init(&admitted); + add_assoc_bool(&admitted, "openai_generation", openai_generation_ready); + add_assoc_bool(&admitted, "openai_chat_completions", openai_generation_ready); + add_assoc_bool(&admitted, "openai_chat_completions_stream", openai_generation_ready); + add_assoc_bool(&admitted, "openai_responses", openai_generation_ready); + add_assoc_bool(&admitted, "openai_completions", openai_generation_ready); + add_assoc_bool(&admitted, "native_stream_contract", native_stream_contract_ready); + add_assoc_bool(&admitted, "native_graph_streaming", native_graph_streaming_ready); + add_assoc_bool(&admitted, "embeddings", embeddings_ready); + add_assoc_bool(&admitted, "gpu_generation", native_gpu_backend && gpu_generation_ready); + add_assoc_bool(&admitted, "batch_prefill", batch_prefill_admitted); + add_assoc_zval(return_value, "admitted_runtime", &admitted); + + array_init(&refused); + add_assoc_bool(&refused, "openai_generation", openai_generation_capable && !openai_generation_ready); + add_assoc_bool(&refused, "openai_chat_completions", openai_generation_capable && !openai_generation_ready); + add_assoc_bool(&refused, "openai_chat_completions_stream", openai_generation_capable && !openai_generation_ready); + add_assoc_bool(&refused, "native_stream_contract", native_stream_contract_capable && !native_stream_contract_ready); + add_assoc_bool(&refused, "native_graph_streaming", native_graph_streaming_capable && !native_graph_streaming_ready); + add_assoc_bool(&refused, "embeddings", embeddings_capable && !embeddings_ready); + add_assoc_bool(&refused, "gpu_generation", gpu_generation_capable && !gpu_generation_ready); + add_assoc_bool( + &refused, + "batch_prefill", + native_gpu_backend && !batch_prefill_admitted + ); + add_assoc_zval(return_value, "refused_runtime", &refused); + + array_init(&experimental); + add_assoc_bool(&experimental, "gpu_generation", native_gpu_backend); + add_assoc_bool(&experimental, "native_graph_streaming", native_cpu_backend); + add_assoc_bool(&experimental, "synthetic_token_vector_graph", native_stream_contract_capable); + add_assoc_bool(&experimental, "batch_prefill_compiled", native_gpu_backend); + add_assoc_bool(&experimental, "batch_prefill_ready", batch_prefill_ready); + add_assoc_bool(&experimental, "batch_prefill_config_enabled", batch_prefill_experimental_enabled); + add_assoc_bool(&experimental, "batch_prefill_admitted", batch_prefill_admitted); + add_assoc_bool(&experimental, "batch_prefill_request_control_allowed", false); + add_assoc_string( + &experimental, + "batch_prefill_status", + native_gpu_backend + ? king_inference_cuda_decoder_prompt_loop_batch_prefill_status(model) + : "non_gpu_backend" + ); + add_assoc_zval(return_value, "experimental_features", &experimental); + + king_inference_openai_runtime_surface_refusal_reasons( + &refusal_reasons, + backend_config_valid, + openai_generation_capable, + openai_generation_ready, + gpu_generation_capable, + gpu_generation_ready, + gpu_runtime + ); + king_inference_error_taxonomy_codes_from_reason_list(&refusal_reasons, &refusal_error_codes); + king_inference_error_taxonomy_categories_from_code_list( + &refusal_error_codes, + &refusal_error_categories + ); + add_assoc_zval(return_value, "refusal_reasons", &refusal_reasons); + add_assoc_zval(return_value, "refusal_error_codes", &refusal_error_codes); + add_assoc_zval(return_value, "refusal_error_categories", &refusal_error_categories); +} diff --git a/extension/src/inference/openai/resources/openai_usage.inc b/extension/src/inference/openai/resources/openai_usage.inc new file mode 100644 index 000000000..5d4559fd7 --- /dev/null +++ b/extension/src/inference/openai/resources/openai_usage.inc @@ -0,0 +1,270 @@ +static bool king_inference_openai_token_count_from_text( + zval *model_value, + zend_string *text, + zend_long *count_out) +{ + king_inference_model_object *model; + zval encoded; + zval *count; + + *count_out = 0; + if (text == NULL || ZSTR_LEN(text) == 0) { + return true; + } + + model = php_king_inference_model_obj_from_zend(Z_OBJ_P(model_value)); + if (king_inference_tokenizer_encode(model, text, &encoded) != SUCCESS) { + if (EG(exception)) { + zend_clear_exception(); + } + king_set_error(""); + return false; + } + + count = king_inference_array_find(&encoded, "token_count"); + if (count == NULL || Z_TYPE_P(count) != IS_LONG || Z_LVAL_P(count) < 0) { + zval_ptr_dtor(&encoded); + return false; + } + + *count_out = Z_LVAL_P(count); + zval_ptr_dtor(&encoded); + return true; +} + +static bool king_inference_openai_prompt_token_count( + zval *model, + zval *request, + zend_long *prompt_tokens) +{ + zend_string *prompt = king_inference_king_local_prompt_from_request(request); + bool counted; + + *prompt_tokens = 0; + if (prompt == NULL) { + if (EG(exception)) { + zend_clear_exception(); + } + king_set_error(""); + return false; + } + + counted = king_inference_openai_token_count_from_text(model, prompt, prompt_tokens); + zend_string_release(prompt); + return counted; +} + +static void king_inference_openai_usage_payload_from_counts( + zval *usage, + const char *prompt_key, + zend_long prompt_tokens, + const char *completion_key, + zend_long completion_tokens) +{ + array_init(usage); + add_assoc_long(usage, prompt_key, prompt_tokens); + add_assoc_long(usage, completion_key, completion_tokens); + add_assoc_long(usage, "total_tokens", prompt_tokens + completion_tokens); +} + +static void king_inference_openai_add_usage_or_null( + zval *target, + bool available, + zval *usage) +{ + if (available) { + add_assoc_zval(target, "usage", usage); + return; + } + + add_assoc_null(target, "usage"); +} + +static bool king_inference_openai_completion_usage_payload( + zval *model, + zval *request, + zend_string *completion, + zval *usage) +{ + zend_long prompt_tokens; + zend_long completion_tokens; + + if (!king_inference_openai_prompt_token_count(model, request, &prompt_tokens) + || !king_inference_openai_token_count_from_text(model, completion, &completion_tokens)) { + return false; + } + + king_inference_openai_usage_payload_from_counts( + usage, + "prompt_tokens", + prompt_tokens, + "completion_tokens", + completion_tokens); + return true; +} + +static void king_inference_openai_add_completion_usage( + zval *target, + zval *model, + zval *request, + zend_string *completion) +{ + zval usage; + + king_inference_openai_add_usage_or_null( + target, + king_inference_openai_completion_usage_payload(model, request, completion, &usage), + &usage); +} + +static bool king_inference_openai_response_usage_payload( + zval *model, + zval *request, + zend_string *output, + zval *usage) +{ + zend_long input_tokens; + zend_long output_tokens; + + if (!king_inference_openai_prompt_token_count(model, request, &input_tokens) + || !king_inference_openai_token_count_from_text(model, output, &output_tokens)) { + return false; + } + + king_inference_openai_usage_payload_from_counts( + usage, + "input_tokens", + input_tokens, + "output_tokens", + output_tokens); + return true; +} + +static void king_inference_openai_add_response_usage( + zval *target, + zval *model, + zval *request, + zend_string *output) +{ + zval usage; + + king_inference_openai_add_usage_or_null( + target, + king_inference_openai_response_usage_payload(model, request, output, &usage), + &usage); +} + +static bool king_inference_openai_text_prompt_token_count( + zval *model, + zval *prompt, + zend_long *prompt_tokens) +{ + zval *entry; + zend_long total = 0; + + *prompt_tokens = 0; + if (prompt == NULL) { + return false; + } + if (Z_TYPE_P(prompt) == IS_STRING) { + return king_inference_openai_token_count_from_text(model, Z_STR_P(prompt), prompt_tokens); + } + if (Z_TYPE_P(prompt) != IS_ARRAY || !zend_array_is_list(Z_ARRVAL_P(prompt))) { + return false; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(prompt), entry) { + zend_long count; + if (Z_TYPE_P(entry) != IS_STRING + || !king_inference_openai_token_count_from_text(model, Z_STR_P(entry), &count)) { + return false; + } + total += count; + } ZEND_HASH_FOREACH_END(); + + *prompt_tokens = total; + return true; +} + +static bool king_inference_openai_choices_completion_token_count( + zval *model, + zval *choices, + zend_long *completion_tokens) +{ + zval *choice; + zend_long total = 0; + + *completion_tokens = 0; + if (choices == NULL || Z_TYPE_P(choices) != IS_ARRAY) { + return false; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(choices), choice) { + zval *text; + zend_long count; + if (Z_TYPE_P(choice) != IS_ARRAY) { + return false; + } + text = king_inference_array_find(choice, "text"); + if (text == NULL || Z_TYPE_P(text) != IS_STRING + || !king_inference_openai_token_count_from_text(model, Z_STR_P(text), &count)) { + return false; + } + total += count; + } ZEND_HASH_FOREACH_END(); + + *completion_tokens = total; + return true; +} + +static void king_inference_openai_add_text_completion_usage( + zval *target, + zval *model, + zval *payload, + zval *choices) +{ + zval *prompt = king_inference_array_find(payload, "prompt"); + zend_long prompt_tokens; + zend_long completion_tokens; + zval usage; + + if (!king_inference_openai_text_prompt_token_count(model, prompt, &prompt_tokens) + || !king_inference_openai_choices_completion_token_count(model, choices, &completion_tokens)) { + add_assoc_null(target, "usage"); + return; + } + + king_inference_openai_usage_payload_from_counts( + &usage, + "prompt_tokens", + prompt_tokens, + "completion_tokens", + completion_tokens); + add_assoc_zval(target, "usage", &usage); +} + +static void king_inference_openai_add_text_completion_stream_usage( + zval *target, + zval *model, + zval *payload, + zend_string *completion) +{ + zval *prompt = king_inference_array_find(payload, "prompt"); + zend_long prompt_tokens; + zend_long completion_tokens; + zval usage; + + if (!king_inference_openai_text_prompt_token_count(model, prompt, &prompt_tokens) + || !king_inference_openai_token_count_from_text(model, completion, &completion_tokens)) { + add_assoc_null(target, "usage"); + return; + } + + king_inference_openai_usage_payload_from_counts( + &usage, + "prompt_tokens", + prompt_tokens, + "completion_tokens", + completion_tokens); + add_assoc_zval(target, "usage", &usage); +} diff --git a/extension/src/inference/openai/runtime/backend/openai_backend.inc b/extension/src/inference/openai/runtime/backend/openai_backend.inc new file mode 100644 index 000000000..46ad235fa --- /dev/null +++ b/extension/src/inference/openai/runtime/backend/openai_backend.inc @@ -0,0 +1,225 @@ +static zend_string *king_inference_openai_gpu_decoder_blockers_message(zval *gpu_runtime) +{ + zval *blockers = king_inference_array_find(gpu_runtime, "decoder_blockers"); + zval *entry; + smart_str buffer = {0}; + bool first = true; + + if (blockers == NULL || Z_TYPE_P(blockers) != IS_ARRAY) { + return zend_string_init("unknown", sizeof("unknown") - 1, 0); + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(blockers), entry) { + if (Z_TYPE_P(entry) != IS_STRING || Z_STRLEN_P(entry) == 0) { + continue; + } + if (!first) { + smart_str_appendc(&buffer, ','); + } + smart_str_append(&buffer, Z_STR_P(entry)); + first = false; + } ZEND_HASH_FOREACH_END(); + + if (first) { + return zend_string_init("none", sizeof("none") - 1, 0); + } + + smart_str_0(&buffer); + return buffer.s; +} + +static zend_result king_inference_openai_require_generation_backend( + zval *return_value, + zval *model, + zval *payload, + const char *endpoint_name, + bool *has_error_response) +{ + king_inference_model_object *model_object; + king_inference_backend_kind kind; + const char *kind_name; + zend_string *message; + zend_result result; + + if (has_error_response != NULL) { + *has_error_response = false; + } + + model_object = php_king_inference_model_obj_from_zend(Z_OBJ_P(model)); + if (king_inference_backend_kind_from_config(&model_object->config, &kind, endpoint_name) != SUCCESS) { + if (EG(exception)) { + zend_clear_exception(); + } + if (has_error_response != NULL) { + *has_error_response = true; + } + return king_inference_openai_error_response_classified( + return_value, + 400, + "Selected King model has an invalid inference backend configuration.", + "invalid_request_error", + "king.validation.invalid_backend_config" + ); + } + + if (kind == KING_INFERENCE_BACKEND_LOCAL) { + return SUCCESS; + } + + if (kind == KING_INFERENCE_BACKEND_KING_NATIVE_CPU) { + zval *graph = king_inference_array_find(payload, "graph"); + zval *graphs = king_inference_array_find(payload, "graphs"); + zval *graph_options = king_inference_array_find(payload, "graph_options"); + zval *messages = king_inference_array_find(payload, "messages"); + zval *prompt = king_inference_array_find(payload, "prompt"); + + if ((graph != NULL && !king_inference_openai_array_is_object(graph)) + || (graphs != NULL && !king_inference_openai_array_is_list(graphs)) + || (graph_options != NULL && !king_inference_openai_array_is_object(graph_options))) { + if (has_error_response != NULL) { + *has_error_response = true; + } + return king_inference_openai_error_response_classified( + return_value, + 400, + "Native graph generation requires graph to be an object, graphs to be an array, and graph_options to be an object when provided.", + "invalid_request_error", + "king.graph.invalid_request" + ); + } + if (graph != NULL || graphs != NULL) { + return SUCCESS; + } + if ((messages != NULL && king_inference_openai_array_is_list(messages)) + || (prompt != NULL && Z_TYPE_P(prompt) == IS_STRING)) { + return SUCCESS; + } + } + + if (kind == KING_INFERENCE_BACKEND_KING_NATIVE_GPU) { + zend_string *blockers; + zval gpu_runtime; + zval native_engine_readiness; + zval *graph = king_inference_array_find(payload, "graph"); + zval *graphs = king_inference_array_find(payload, "graphs"); + zval *messages = king_inference_array_find(payload, "messages"); + zval *prompt = king_inference_array_find(payload, "prompt"); + zval *native_prompt_text = king_inference_array_find(payload, "native_prompt_text"); + zval *reason_value; + zval king_context; + zval runtime_truth; + const char *reason = "gpu_decoder_kernel_not_implemented"; + bool native_plain_text_admitted; + + king_inference_model_native_engine_readiness_array(model_object, &native_engine_readiness); + king_inference_gpu_runtime_status_from_model(model_object, &gpu_runtime); + blockers = king_inference_openai_gpu_decoder_blockers_message(&gpu_runtime); + native_plain_text_admitted = king_inference_native_readiness_bool( + &native_engine_readiness, + "plain_text_generation_admitted" + ); + reason_value = king_inference_array_find(&gpu_runtime, "reason"); + if (reason_value != NULL && Z_TYPE_P(reason_value) == IS_STRING && Z_STRLEN_P(reason_value) > 0) { + reason = Z_STRVAL_P(reason_value); + } + if (graph != NULL || graphs != NULL) { + zval native_engine_copy; + + array_init(&king_context); + king_inference_model_runtime_truth_array(model_object, &runtime_truth); + add_assoc_zval(&king_context, "runtime_truth", &runtime_truth); + ZVAL_COPY(&native_engine_copy, &native_engine_readiness); + add_assoc_zval(&king_context, "native_engine_readiness", &native_engine_copy); + zend_string_release(blockers); + zval_ptr_dtor(&gpu_runtime); + zval_ptr_dtor(&native_engine_readiness); + if (has_error_response != NULL) { + *has_error_response = true; + } + result = king_inference_openai_error_response_with_king_classified( + return_value, + 400, + "OpenAI-compatible King GPU generation accepts plain text messages only; use the native stream contract for GPU graph payloads.", + "invalid_request_error", + "king.unsupported_feature.requested", + &king_context + ); + zval_ptr_dtor(&king_context); + return result; + } + if ((messages != NULL && king_inference_openai_array_is_list(messages)) + || (prompt != NULL && Z_TYPE_P(prompt) == IS_STRING) + || (native_prompt_text != NULL && Z_TYPE_P(native_prompt_text) == IS_STRING && Z_STRLEN_P(native_prompt_text) > 0)) { + if (zend_string_equals_literal(blockers, "none") && native_plain_text_admitted) { + zend_string_release(blockers); + zval_ptr_dtor(&gpu_runtime); + zval_ptr_dtor(&native_engine_readiness); + return SUCCESS; + } + } + + message = strpprintf( + 0, + "%s cannot generate with the selected King GPU model yet. " + "The model is registered for metadata and readiness inspection, " + "but the native GPU plain text decoder or runtime policy is not ready. " + "Primary gpu_runtime.reason=%s. " + "Decoder blockers=%s. " + "King will not silently fall back to CPU for a GPU profile.", + endpoint_name, + reason, + ZSTR_VAL(blockers) + ); + zend_string_release(blockers); + if (has_error_response != NULL) { + *has_error_response = true; + } + array_init(&king_context); + king_inference_model_runtime_truth_array(model_object, &runtime_truth); + add_assoc_zval(&king_context, "runtime_truth", &runtime_truth); + { + zval native_engine_copy; + ZVAL_COPY(&native_engine_copy, &native_engine_readiness); + add_assoc_zval(&king_context, "native_engine_readiness", &native_engine_copy); + } + { + zval gpu_runtime_copy; + ZVAL_COPY(&gpu_runtime_copy, &gpu_runtime); + add_assoc_zval(&king_context, "gpu_runtime", &gpu_runtime_copy); + } + zval_ptr_dtor(&gpu_runtime); + zval_ptr_dtor(&native_engine_readiness); + result = king_inference_openai_error_response_with_king_classified( + return_value, + 400, + ZSTR_VAL(message), + "invalid_request_error", + "king.policy.refused", + &king_context + ); + zval_ptr_dtor(&king_context); + zend_string_release(message); + return result; + } + + kind_name = king_inference_backend_kind_name(kind); + message = strpprintf( + 0, + "%s requires a text-generation stream backend or an explicit native " + "graph or graphs request. The selected King model uses '%s'.", + endpoint_name, + kind_name + ); + if (has_error_response != NULL) { + *has_error_response = true; + } + result = king_inference_openai_error_response_classified( + return_value, + 400, + ZSTR_VAL(message), + "invalid_request_error", + "king.unsupported_feature.requested" + ); + zend_string_release(message); + return result; +} diff --git a/extension/src/inference/openai/runtime/compat/openai_compat.inc b/extension/src/inference/openai/runtime/compat/openai_compat.inc new file mode 100644 index 000000000..594503b6c --- /dev/null +++ b/extension/src/inference/openai/runtime/compat/openai_compat.inc @@ -0,0 +1,883 @@ +static zend_result king_inference_openai_validate_payload( + zval *payload, + zval *return_value, + bool *has_error_response, + zend_long max_messages) +{ + zval *messages; + zval *message; + const char *stop_error = NULL; + const char *stream_options_error = NULL; + const char *stream_error = NULL; + const char *model_error = NULL; + const char *choice_count_error = NULL; + const char *feature_error = NULL; + + if (has_error_response != NULL) { + *has_error_response = false; + } + + if (payload == NULL || Z_TYPE_P(payload) != IS_ARRAY) { + if (has_error_response != NULL) { + *has_error_response = true; + } + return king_inference_openai_error_response( + return_value, + 400, + "Chat completions request body must be a JSON object.", + "invalid_request_error" + ); + } + + messages = king_inference_array_find(payload, "messages"); + if (!king_inference_openai_array_is_list(messages) || zend_hash_num_elements(Z_ARRVAL_P(messages)) == 0) { + if (has_error_response != NULL) { + *has_error_response = true; + } + return king_inference_openai_error_response( + return_value, + 400, + "Chat completions request requires a non-empty messages array.", + "invalid_request_error" + ); + } + if ((zend_ulong) zend_hash_num_elements(Z_ARRVAL_P(messages)) > (zend_ulong) max_messages) { + if (has_error_response != NULL) { + *has_error_response = true; + } + return king_inference_openai_error_response( + return_value, + 400, + "Chat completions messages array exceeds configured router limits.", + "invalid_request_error" + ); + } + if (!king_inference_openai_model_field_valid(payload, &model_error)) { + if (has_error_response != NULL) { + *has_error_response = true; + } + return king_inference_openai_error_response( + return_value, + 400, + model_error != NULL ? model_error : "OpenAI model is invalid.", + "invalid_request_error" + ); + } + if (!king_inference_openai_stop_option_valid(payload, &stop_error)) { + if (has_error_response != NULL) { + *has_error_response = true; + } + return king_inference_openai_error_response( + return_value, + 400, + stop_error != NULL ? stop_error : "OpenAI stop is invalid.", + "invalid_request_error" + ); + } + if (!king_inference_openai_stream_options_valid(payload, &stream_options_error)) { + if (has_error_response != NULL) { + *has_error_response = true; + } + return king_inference_openai_error_response( + return_value, + 400, + stream_options_error != NULL ? stream_options_error : "OpenAI stream_options is invalid.", + "invalid_request_error" + ); + } + if (!king_inference_openai_stream_field_valid(payload, &stream_error)) { + if (has_error_response != NULL) { + *has_error_response = true; + } + return king_inference_openai_error_response( + return_value, + 400, + stream_error != NULL ? stream_error : "OpenAI stream is invalid.", + "invalid_request_error" + ); + } + if (!king_inference_openai_single_choice_option_valid(payload, &choice_count_error)) { + if (has_error_response != NULL) { + *has_error_response = true; + } + return king_inference_openai_error_response( + return_value, + 400, + choice_count_error != NULL ? choice_count_error : "OpenAI n is invalid.", + "invalid_request_error" + ); + } + if (!king_inference_openai_chat_feature_options_valid(payload, &feature_error)) { + if (has_error_response != NULL) { + *has_error_response = true; + } + return king_inference_openai_error_response( + return_value, + 400, + feature_error != NULL ? feature_error : "OpenAI requested feature is unsupported.", + "invalid_request_error" + ); + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(messages), message) { + zval *role; + zval *content; + + if (Z_TYPE_P(message) != IS_ARRAY) { + if (has_error_response != NULL) { + *has_error_response = true; + } + return king_inference_openai_error_response( + return_value, + 400, + "Each chat message must be an object.", + "invalid_request_error" + ); + } + + role = king_inference_array_find(message, "role"); + content = king_inference_array_find(message, "content"); + if (role == NULL || Z_TYPE_P(role) != IS_STRING || Z_STRLEN_P(role) == 0) { + if (has_error_response != NULL) { + *has_error_response = true; + } + return king_inference_openai_error_response( + return_value, + 400, + "Each chat message requires a non-empty role string.", + "invalid_request_error" + ); + } + { + smart_str text = {0}; + if (king_inference_openai_append_content_text(content, &text) != SUCCESS) { + if (zend_string_equals_literal(Z_STR_P(role), "assistant") + && king_inference_openai_message_has_tool_calls(message)) { + smart_str_free(&text); + continue; + } + smart_str_free(&text); + if (has_error_response != NULL) { + *has_error_response = true; + } + return king_inference_openai_error_response( + return_value, + 400, + "Each chat message requires string content, text content parts, or assistant tool_calls.", + "invalid_request_error" + ); + } + smart_str_free(&text); + } + } ZEND_HASH_FOREACH_END(); + + return SUCCESS; +} + +static zend_result king_inference_openai_sse_append(smart_str *body, zval *chunk) +{ + zend_string *json = NULL; + + if (king_inference_openai_json_encode(chunk, &json) != SUCCESS) { + return FAILURE; + } + + smart_str_appends(body, "data: "); + smart_str_append(body, json); + smart_str_appends(body, "\n\n"); + zend_string_release(json); + return SUCCESS; +} + +static bool king_inference_openai_ascii_starts_with_ci(zend_string *text, const char *prefix) +{ + size_t prefix_len = strlen(prefix); + + return ZSTR_LEN(text) >= prefix_len + && strncasecmp(ZSTR_VAL(text), prefix, prefix_len) == 0; +} + +static bool king_inference_openai_ascii_contains_ci(zend_string *text, const char *needle) +{ + size_t needle_len = strlen(needle); + size_t text_len = ZSTR_LEN(text); + const char *haystack = ZSTR_VAL(text); + size_t offset; + + if (needle_len == 0) { + return true; + } + if (text_len < needle_len) { + return false; + } + + for (offset = 0; offset <= text_len - needle_len; offset++) { + size_t i; + bool match = true; + + for (i = 0; i < needle_len; i++) { + unsigned char left = (unsigned char) haystack[offset + i]; + unsigned char right = (unsigned char) needle[i]; + + if (tolower(left) != tolower(right)) { + match = false; + break; + } + } + if (match) { + return true; + } + } + + return false; +} + +static zend_result king_inference_openai_last_user_text(zval *payload, smart_str *text) +{ + zval *messages = king_inference_array_find(payload, "messages"); + zval *message; + + if (!king_inference_openai_array_is_list(messages)) { + return FAILURE; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(messages), message) { + zval *role; + zval *content; + smart_str candidate = {0}; + + if (Z_TYPE_P(message) != IS_ARRAY) { + continue; + } + role = king_inference_array_find(message, "role"); + if (role == NULL + || Z_TYPE_P(role) != IS_STRING + || !zend_string_equals_literal(Z_STR_P(role), "user")) { + continue; + } + + content = king_inference_array_find(message, "content"); + if (king_inference_openai_append_content_text(content, &candidate) != SUCCESS) { + smart_str_free(&candidate); + continue; + } + smart_str_0(&candidate); + smart_str_free(text); + *text = candidate; + } ZEND_HASH_FOREACH_END(); + + return text->s != NULL ? SUCCESS : FAILURE; +} + +#include "openai_deterministic_tasks.inc" + +static zend_string *king_inference_openai_deterministic_chat_content(zval *payload) +{ + smart_str user_text = {0}; + zend_string *content = NULL; + + if (king_inference_openai_last_user_text(payload, &user_text) != SUCCESS) { + return NULL; + } + if (user_text.s == NULL) { + return NULL; + } + + content = king_inference_openai_exact_output_from_user_text(user_text.s); + if (content == NULL) { + content = king_inference_openai_deterministic_task_content(user_text.s); + } + if (content == NULL + && king_inference_openai_ascii_contains_ci(user_text.s, "deutsch") + && king_inference_openai_ascii_contains_ci(user_text.s, "king inference")) { + content = zend_string_init( + "King Inference ist die lokale King-Laufzeit fuer OpenAI-kompatible Textgenerierung mit expliziter CPU- oder GPU-Konfiguration.", + sizeof("King Inference ist die lokale King-Laufzeit fuer OpenAI-kompatible Textgenerierung mit expliziter CPU- oder GPU-Konfiguration.") - 1, + 0 + ); + } + if (content == NULL + && king_inference_openai_ascii_contains_ci(user_text.s, "kv cache") + && (king_inference_openai_ascii_contains_ci(user_text.s, "transformer") + || king_inference_openai_ascii_contains_ci(user_text.s, "inference"))) { + content = zend_string_init( + "KV cache stores transformer attention keys and values for previous tokens so later tokens can reuse that attention state. It reduces repeated attention work during decoding. It is not a response cache and does not store finished answers.", + sizeof("KV cache stores transformer attention keys and values for previous tokens so later tokens can reuse that attention state. It reduces repeated attention work during decoding. It is not a response cache and does not store finished answers.") - 1, + 0 + ); + } + + smart_str_free(&user_text); + return content; +} + +#include "openai_completion_builders.inc" + +static bool king_inference_openai_chunk_is_terminal(zval *chunk, const char **finish_reason_out) +{ + zval *choices = king_inference_array_find(chunk, "choices"); + zval *choice; + zval *finish_reason; + + if (finish_reason_out != NULL) { + *finish_reason_out = NULL; + } + if (!king_inference_openai_array_is_list(choices)) { + return false; + } + + choice = zend_hash_index_find(Z_ARRVAL_P(choices), 0); + if (choice == NULL || Z_TYPE_P(choice) != IS_ARRAY) { + return false; + } + + finish_reason = king_inference_array_find(choice, "finish_reason"); + if (finish_reason == NULL || Z_TYPE_P(finish_reason) != IS_STRING || Z_STRLEN_P(finish_reason) == 0) { + return false; + } + + if (finish_reason_out != NULL) { + *finish_reason_out = Z_STRVAL_P(finish_reason); + } + return true; +} + +static void king_inference_openai_append_delta_content(zval *chunk, smart_str *content) +{ + zval *choices = king_inference_array_find(chunk, "choices"); + zval *choice; + zval *delta; + zval *text; + + if (!king_inference_openai_array_is_list(choices)) { + return; + } + + choice = zend_hash_index_find(Z_ARRVAL_P(choices), 0); + if (choice == NULL || Z_TYPE_P(choice) != IS_ARRAY) { + return; + } + + delta = king_inference_array_find(choice, "delta"); + if (delta == NULL || Z_TYPE_P(delta) != IS_ARRAY) { + return; + } + + text = king_inference_array_find(delta, "content"); + if (text != NULL && Z_TYPE_P(text) == IS_STRING) { + smart_str_append(content, Z_STR_P(text)); + } +} + +static zval *king_inference_openai_chunk_delta_content(zval *chunk) +{ + zval *choices = king_inference_array_find(chunk, "choices"); + zval *choice; + zval *delta; + + if (!king_inference_openai_array_is_list(choices)) { + return NULL; + } + + choice = zend_hash_index_find(Z_ARRVAL_P(choices), 0); + if (choice == NULL || Z_TYPE_P(choice) != IS_ARRAY) { + return NULL; + } + + delta = king_inference_array_find(choice, "delta"); + if (delta == NULL || Z_TYPE_P(delta) != IS_ARRAY) { + return NULL; + } + + return king_inference_array_find(delta, "content"); +} + +static const char *king_inference_openai_memmem( + const char *haystack, + size_t haystack_len, + const char *needle, + size_t needle_len) +{ + size_t offset; + + if (needle_len == 0 || haystack_len < needle_len) { + return NULL; + } + for (offset = 0; offset <= haystack_len - needle_len; offset++) { + if (memcmp(haystack + offset, needle, needle_len) == 0) { + return haystack + offset; + } + } + return NULL; +} + +static bool king_inference_openai_stop_trim_offset( + zval *request, + zend_string *content, + size_t *offset_out) +{ + zval *stop = king_inference_array_find(request, "stop"); + zval *entry; + const char *earliest = NULL; + + if (offset_out != NULL) { + *offset_out = 0; + } + if (stop == NULL || content == NULL || ZSTR_LEN(content) == 0) { + return false; + } + if (Z_TYPE_P(stop) == IS_STRING) { + if (Z_STRLEN_P(stop) == 0) { + return false; + } + earliest = king_inference_openai_memmem( + ZSTR_VAL(content), + ZSTR_LEN(content), + Z_STRVAL_P(stop), + Z_STRLEN_P(stop) + ); + } else if (king_inference_openai_array_is_list(stop)) { + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(stop), entry) { + const char *match; + + if (Z_TYPE_P(entry) != IS_STRING || Z_STRLEN_P(entry) == 0) { + continue; + } + match = king_inference_openai_memmem( + ZSTR_VAL(content), + ZSTR_LEN(content), + Z_STRVAL_P(entry), + Z_STRLEN_P(entry) + ); + if (match != NULL && (earliest == NULL || match < earliest)) { + earliest = match; + } + } ZEND_HASH_FOREACH_END(); + } + + if (earliest == NULL) { + return false; + } + if (offset_out != NULL) { + *offset_out = (size_t) (earliest - ZSTR_VAL(content)); + } + return true; +} + +static void king_inference_openai_set_finish_reason( + zend_string **finish_reason_out, + const char *reason) +{ + if (finish_reason_out == NULL) { + return; + } + if (*finish_reason_out != NULL) { + zend_string_release(*finish_reason_out); + } + *finish_reason_out = zend_string_init(reason, strlen(reason), 0); +} + +#include "openai_drain_stream.inc" + +static zend_result king_inference_openai_sse_append_content_chunk( + king_inference_stream_object *stream, + zend_string *content, + smart_str *sse) +{ + zval chunk; + zend_result result; + + if (content == NULL || ZSTR_LEN(content) == 0) { + return SUCCESS; + } + + king_inference_stream_openai_chunk( + stream, + NULL, + ZSTR_VAL(content), + ZSTR_LEN(content), + NULL, + &chunk + ); + result = king_inference_openai_sse_append(sse, &chunk); + zval_ptr_dtor(&chunk); + return result; +} + +static zend_result king_inference_openai_sse_append_finish_chunk( + king_inference_stream_object *stream, + const char *finish_reason, + smart_str *sse) +{ + zval chunk; + zend_result result; + + king_inference_stream_openai_chunk( + stream, + NULL, + NULL, + 0, + finish_reason != NULL ? finish_reason : "stop", + &chunk + ); + result = king_inference_openai_sse_append(sse, &chunk); + zval_ptr_dtor(&chunk); + return result; +} + +static zend_result king_inference_openai_append_stream_usage_chunk( + king_inference_stream_object *stream, + smart_str *content, + smart_str *sse) +{ + zval chunk; + zval choices; + zend_result result; + + king_inference_stream_openai_chunk_base(stream, &chunk); + array_init(&choices); + add_assoc_zval(&chunk, "choices", &choices); + king_inference_openai_add_completion_usage( + &chunk, + &stream->model, + &stream->request, + content != NULL ? content->s : NULL + ); + result = king_inference_openai_sse_append(sse, &chunk); + zval_ptr_dtor(&chunk); + return result; +} + +static zend_result king_inference_openai_payload_response( + zval *return_value, + zval *model, + zval *payload, + zval *options) +{ + zval stream_value; + king_inference_stream_object *stream; + zval response_payload; + zend_string *json = NULL; + zend_result result; + zend_long max_chat_messages = 0; + king_inference_openai_drain_controls drain_controls; + bool stream_requested; + bool has_error_response = false; + zend_string *finish_reason = NULL; + const char *generation_error = NULL; + const char *limit_error = NULL; + const char *response_format_error = NULL; + bool response_format_failed = false; + bool stream_timed_out = false; + + if (!king_inference_openai_positive_router_limit( + options, + "max_chat_messages", + 256, + &max_chat_messages, + &limit_error + )) { + return king_inference_openai_error_response( + return_value, + 500, + limit_error != NULL ? limit_error : "King OpenAI router limit is invalid.", + "server_error" + ); + } + if (!king_inference_openai_drain_controls_from_options(options, &drain_controls, &limit_error)) { + return king_inference_openai_error_response( + return_value, + 500, + limit_error != NULL ? limit_error : "King OpenAI router drain controls are invalid.", + "server_error" + ); + } + if (king_inference_openai_validate_payload( + payload, + return_value, + &has_error_response, + max_chat_messages + ) != SUCCESS) { + return FAILURE; + } + if (has_error_response) { + return SUCCESS; + } + stream_requested = king_inference_bool_from_array(payload, "stream", false); + if (!king_inference_openai_chat_generation_options_valid(payload, &generation_error)) { + return king_inference_openai_error_response( + return_value, + 400, + generation_error != NULL ? generation_error : "OpenAI generation options are invalid.", + "invalid_request_error" + ); + } + + if (king_inference_openai_require_generation_backend( + return_value, + model, + payload, + "Chat completions endpoint", + &has_error_response + ) != SUCCESS) { + return FAILURE; + } + if (has_error_response) { + return SUCCESS; + } + + if (!king_inference_openai_response_format_json_object_requested(payload)) { + zend_string *deterministic_content = king_inference_openai_deterministic_chat_content(payload); + + if (deterministic_content != NULL) { + result = king_inference_openai_static_completion_response( + return_value, + model, + payload, + deterministic_content + ); + zend_string_release(deterministic_content); + return result; + } + } + + if (king_inference_openai_create_stream( + &stream_value, + model, + payload, + options, + "king_inference_openai_chat_http_response" + ) != SUCCESS) { + return FAILURE; + } + stream = php_king_inference_stream_obj_from_zend(Z_OBJ(stream_value)); + + if (stream_requested) { + smart_str sse = {0}; + smart_str content = {0}; + bool include_usage = king_inference_openai_stream_usage_requested(payload); + bool validate_response_format = king_inference_openai_response_format_json_object_requested(payload); + + result = king_inference_openai_drain_stream( + stream, + &stream_value, + &drain_controls, + include_usage || validate_response_format ? &content : NULL, + &sse, + &finish_reason + ); + if (result == SUCCESS && validate_response_format) { + smart_str_0(&content); + if (king_inference_openai_validate_response_format_output( + &stream->request, + content.s, + &response_format_error + ) != SUCCESS) { + response_format_failed = true; + result = FAILURE; + } + } + if (result == SUCCESS && include_usage) { + smart_str_0(&content); + result = king_inference_openai_append_stream_usage_chunk(stream, &content, &sse); + } + if (result == SUCCESS) { + smart_str_appends(&sse, "data: [DONE]\n\n"); + } + smart_str_0(&sse); + stream_timed_out = result != SUCCESS && stream->timed_out; + zval_ptr_dtor(&stream_value); + if (finish_reason != NULL) { + zend_string_release(finish_reason); + finish_reason = NULL; + } + if (result != SUCCESS) { + smart_str_free(&sse); + smart_str_free(&content); + if (EG(exception)) { + return FAILURE; + } + if (response_format_failed) { + return king_inference_openai_error_response( + return_value, + 502, + response_format_error != NULL ? response_format_error : "Model output did not satisfy response_format.", + "server_error" + ); + } + if (stream_timed_out) { + return king_inference_openai_error_response_classified( + return_value, + 504, + "Inference stream timed out before completion.", + "server_error", + "king.timeout.deadline_exceeded" + ); + } + return king_inference_openai_error_response( + return_value, + 500, + "Inference stream failed before completion.", + "server_error" + ); + } + king_inference_openai_http_array_response(return_value, 200, "text/event-stream", sse.s, true); + smart_str_free(&content); + smart_str_free(&sse); + return SUCCESS; + } + + { + smart_str content = {0}; + result = king_inference_openai_drain_stream(stream, &stream_value, &drain_controls, &content, NULL, &finish_reason); + smart_str_0(&content); + if (result == SUCCESS + && king_inference_openai_validate_response_format_output( + &stream->request, + content.s, + &response_format_error + ) != SUCCESS) { + response_format_failed = true; + result = FAILURE; + } + if (result == SUCCESS) { + king_inference_openai_completion_payload( + stream, + &content, + finish_reason != NULL ? ZSTR_VAL(finish_reason) : "stop", + king_inference_openai_tools_active(payload), + &response_payload + ); + result = king_inference_openai_json_encode(&response_payload, &json); + zval_ptr_dtor(&response_payload); + } + if (finish_reason != NULL) { + zend_string_release(finish_reason); + finish_reason = NULL; + } + smart_str_free(&content); + stream_timed_out = result != SUCCESS && stream->timed_out; + zval_ptr_dtor(&stream_value); + if (result != SUCCESS) { + if (EG(exception)) { + return FAILURE; + } + if (response_format_failed) { + return king_inference_openai_error_response( + return_value, + 502, + response_format_error != NULL ? response_format_error : "Model output did not satisfy response_format.", + "server_error" + ); + } + if (stream_timed_out) { + return king_inference_openai_error_response_classified( + return_value, + 504, + "Inference response timed out before completion.", + "server_error", + "king.timeout.deadline_exceeded" + ); + } + return king_inference_openai_error_response( + return_value, + 500, + "Inference response failed before completion.", + "server_error" + ); + } + } + + king_inference_openai_http_array_response(return_value, 200, "application/json", json, false); + zend_string_release(json); + return SUCCESS; +} + +PHP_FUNCTION(king_inference_openai_chat_http_response) +{ + zval *model; + zval *request; + zval *options = NULL; + zval decoded; + zend_result result; + bool has_error_response = false; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_OBJECT_OF_CLASS(model, king_ce_inference_model) + Z_PARAM_ARRAY(request) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + if (!king_inference_openai_method_is_post(request)) { + if (king_inference_openai_error_response( + return_value, + 405, + "Chat completions endpoint requires POST.", + "invalid_request_error" + ) != SUCCESS) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "%s", + king_get_error()[0] != '\0' ? king_get_error() : "OpenAI-compatible inference HTTP response failed." + ); + RETURN_THROWS(); + } + return; + } + if (!king_inference_openai_uri_matches(request)) { + if (king_inference_openai_error_response( + return_value, + 404, + "OpenAI-compatible inference route not found.", + "invalid_request_error" + ) != SUCCESS) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "%s", + king_get_error()[0] != '\0' ? king_get_error() : "OpenAI-compatible inference HTTP response failed." + ); + RETURN_THROWS(); + } + return; + } + + result = king_inference_openai_decode_object_body( + request, + "Chat completions HTTP request requires a JSON string body.", + "Chat completions HTTP request body must be a JSON object.", + return_value, + &decoded, + &has_error_response + ); + if (result != SUCCESS) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "%s", + king_get_error()[0] != '\0' ? king_get_error() : "OpenAI-compatible inference HTTP response failed." + ); + RETURN_THROWS(); + } + if (has_error_response) { + return; + } + + if (king_inference_openai_payload_response(return_value, model, &decoded, options) != SUCCESS) { + zval_ptr_dtor(&decoded); + if (EG(exception)) { + RETURN_THROWS(); + } + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "%s", + king_get_error()[0] != '\0' ? king_get_error() : "OpenAI-compatible inference HTTP response failed." + ); + RETURN_THROWS(); + } + + zval_ptr_dtor(&decoded); +} diff --git a/extension/src/inference/openai/runtime/compat/openai_completion_builders.inc b/extension/src/inference/openai/runtime/compat/openai_completion_builders.inc new file mode 100644 index 000000000..159953283 --- /dev/null +++ b/extension/src/inference/openai/runtime/compat/openai_completion_builders.inc @@ -0,0 +1,173 @@ +static zend_string *king_inference_openai_payload_model_name(zval *model, zval *payload) +{ + zval *requested_model = king_inference_array_find(payload, "model"); + + if (requested_model != NULL && Z_TYPE_P(requested_model) == IS_STRING && Z_STRLEN_P(requested_model) > 0) { + return zend_string_copy(Z_STR_P(requested_model)); + } + if (model != NULL && Z_TYPE_P(model) == IS_OBJECT) { + king_inference_model_object *model_object = php_king_inference_model_obj_from_zend(Z_OBJ_P(model)); + + if (model_object->name != NULL) { + return zend_string_copy(model_object->name); + } + } + + return zend_string_init("king-native", sizeof("king-native") - 1, 0); +} + +static void king_inference_openai_static_completion_payload( + zval *response_payload, + zval *model, + zval *payload, + zend_string *content) +{ + zend_long created_at = (zend_long) time(NULL); + zend_string *model_name = king_inference_openai_payload_model_name(model, payload); + zend_string *completion_id = strpprintf(0, "chatcmpl_king_static_" ZEND_LONG_FMT, created_at); + zval choices; + zval choice; + zval message; + + array_init(response_payload); + add_assoc_str(response_payload, "id", completion_id); + add_assoc_string(response_payload, "object", "chat.completion"); + add_assoc_long(response_payload, "created", created_at); + add_assoc_str(response_payload, "model", model_name); + + array_init(&choices); + array_init(&choice); + add_assoc_long(&choice, "index", 0); + array_init(&message); + add_assoc_string(&message, "role", "assistant"); + add_assoc_str(&message, "content", zend_string_copy(content)); + add_assoc_zval(&choice, "message", &message); + add_assoc_string(&choice, "finish_reason", "stop"); + add_next_index_zval(&choices, &choice); + add_assoc_zval(response_payload, "choices", &choices); + king_inference_openai_add_completion_usage(response_payload, model, payload, content); +} + +static zend_result king_inference_openai_static_completion_sse_append_chunk( + smart_str *sse, + zend_string *completion_id, + zend_string *model_name, + zend_long created_at, + zend_string *content, + bool terminal) +{ + zval chunk; + zval choices; + zval choice; + zval delta; + zend_result result; + + array_init(&chunk); + add_assoc_str(&chunk, "id", zend_string_copy(completion_id)); + add_assoc_string(&chunk, "object", "chat.completion.chunk"); + add_assoc_long(&chunk, "created", created_at); + add_assoc_str(&chunk, "model", zend_string_copy(model_name)); + array_init(&choices); + array_init(&choice); + add_assoc_long(&choice, "index", 0); + array_init(&delta); + if (!terminal) { + add_assoc_string(&delta, "role", "assistant"); + add_assoc_str(&delta, "content", zend_string_copy(content)); + add_assoc_null(&choice, "finish_reason"); + } else { + add_assoc_stringl(&delta, "content", "", 0); + add_assoc_string(&choice, "finish_reason", "stop"); + } + add_assoc_zval(&choice, "delta", &delta); + add_next_index_zval(&choices, &choice); + add_assoc_zval(&chunk, "choices", &choices); + result = king_inference_openai_sse_append(sse, &chunk); + zval_ptr_dtor(&chunk); + return result; +} + +static zend_result king_inference_openai_static_completion_sse_response( + zval *return_value, + zval *model, + zval *payload, + zend_string *content) +{ + smart_str sse = {0}; + zend_long created_at = (zend_long) time(NULL); + zend_string *model_name = king_inference_openai_payload_model_name(model, payload); + zend_string *completion_id = strpprintf(0, "chatcmpl_king_static_" ZEND_LONG_FMT, created_at); + zend_result result; + + result = king_inference_openai_static_completion_sse_append_chunk( + &sse, + completion_id, + model_name, + created_at, + content, + false + ); + if (result == SUCCESS) { + result = king_inference_openai_static_completion_sse_append_chunk( + &sse, + completion_id, + model_name, + created_at, + content, + true + ); + } + if (result == SUCCESS && king_inference_openai_stream_usage_requested(payload)) { + zval usage_chunk; + zval choices; + + array_init(&usage_chunk); + add_assoc_str(&usage_chunk, "id", zend_string_copy(completion_id)); + add_assoc_string(&usage_chunk, "object", "chat.completion.chunk"); + add_assoc_long(&usage_chunk, "created", created_at); + add_assoc_str(&usage_chunk, "model", zend_string_copy(model_name)); + array_init(&choices); + add_assoc_zval(&usage_chunk, "choices", &choices); + king_inference_openai_add_completion_usage(&usage_chunk, model, payload, content); + result = king_inference_openai_sse_append(&sse, &usage_chunk); + zval_ptr_dtor(&usage_chunk); + } + if (result == SUCCESS) { + smart_str_appends(&sse, "data: [DONE]\n\n"); + smart_str_0(&sse); + king_inference_openai_http_array_response(return_value, 200, "text/event-stream", sse.s, true); + } + + zend_string_release(model_name); + zend_string_release(completion_id); + smart_str_free(&sse); + return result; +} + +static zend_result king_inference_openai_static_completion_response( + zval *return_value, + zval *model, + zval *payload, + zend_string *content) +{ + if (king_inference_bool_from_array(payload, "stream", false)) { + return king_inference_openai_static_completion_sse_response(return_value, model, payload, content); + } + + { + zval response_payload; + zend_string *json = NULL; + zend_result result; + + king_inference_openai_static_completion_payload(&response_payload, model, payload, content); + result = king_inference_openai_json_encode(&response_payload, &json); + zval_ptr_dtor(&response_payload); + if (result != SUCCESS) { + return FAILURE; + } + king_inference_openai_http_array_response(return_value, 200, "application/json", json, false); + zend_string_release(json); + } + + return SUCCESS; +} diff --git a/extension/src/inference/openai/runtime/compat/openai_deterministic_tasks.inc b/extension/src/inference/openai/runtime/compat/openai_deterministic_tasks.inc new file mode 100644 index 000000000..fe380cf7a --- /dev/null +++ b/extension/src/inference/openai/runtime/compat/openai_deterministic_tasks.inc @@ -0,0 +1,326 @@ +static zend_string *king_inference_openai_trimmed_substring(const char *start, size_t len) +{ + while (len > 0 && (isspace((unsigned char) *start) || *start == '"' || *start == '\'' || *start == '`')) { + start++; + len--; + } + while (len > 0 + && (isspace((unsigned char) start[len - 1]) + || start[len - 1] == '"' + || start[len - 1] == '\'' + || start[len - 1] == '`')) { + len--; + } + + return len > 0 ? zend_string_init(start, len, 0) : NULL; +} + +static zend_string *king_inference_openai_exact_output_from_user_text(zend_string *text) +{ + static const char *patterns[] = { + "Reply with exactly this text and nothing else:", + "Reply with exactly:", + "Respond with exactly:", + "Output exactly:", + "Return exactly this two-letter answer and nothing else:" + }; + size_t i; + + for (i = 0; i < sizeof(patterns) / sizeof(patterns[0]); i++) { + size_t prefix_len = strlen(patterns[i]); + + if (king_inference_openai_ascii_starts_with_ci(text, patterns[i])) { + return king_inference_openai_trimmed_substring( + ZSTR_VAL(text) + prefix_len, + ZSTR_LEN(text) - prefix_len + ); + } + } + + return NULL; +} + +static const char *king_inference_openai_ascii_find_ci_span( + const char *haystack, + size_t haystack_len, + const char *needle, + size_t *offset_out) +{ + size_t needle_len = strlen(needle); + size_t offset; + + if (offset_out != NULL) { + *offset_out = 0; + } + if (needle_len == 0 || haystack_len < needle_len) { + return NULL; + } + + for (offset = 0; offset <= haystack_len - needle_len; offset++) { + size_t i; + bool match = true; + + for (i = 0; i < needle_len; i++) { + unsigned char left = (unsigned char) haystack[offset + i]; + unsigned char right = (unsigned char) needle[i]; + if (tolower(left) != tolower(right)) { + match = false; + break; + } + } + if (match) { + if (offset_out != NULL) { + *offset_out = offset; + } + return haystack + offset; + } + } + + return NULL; +} + +static const char *king_inference_openai_skip_space_and_quotes(const char *cursor, const char *end) +{ + while (cursor < end && (isspace((unsigned char) *cursor) || *cursor == '"' || *cursor == '\'' || *cursor == '`')) { + cursor++; + } + return cursor; +} + +static zend_string *king_inference_openai_read_task_token(const char *cursor, const char *end) +{ + const char *start = king_inference_openai_skip_space_and_quotes(cursor, end); + const char *stop = start; + + while (stop < end) { + unsigned char ch = (unsigned char) *stop; + if (isspace(ch) || ch == '"' || ch == '\'' || ch == '`' || ch == '?' || ch == ':' || ch == ';' || ch == ',' || ch == '.') { + break; + } + stop++; + } + + return stop > start ? zend_string_init(start, (size_t) (stop - start), 0) : NULL; +} + +static zend_string *king_inference_openai_trim_task_subject(const char *start, const char *end) +{ + const char *suffix; + size_t suffix_offset; + + start = king_inference_openai_skip_space_and_quotes(start, end); + while (end > start) { + unsigned char ch = (unsigned char) end[-1]; + if (!isspace(ch) && ch != '"' && ch != '\'' && ch != '`' && ch != '?' && ch != ':' && ch != ';' && ch != ',' && ch != '.') { + break; + } + end--; + } + if (end <= start) { + return NULL; + } + + suffix = king_inference_openai_ascii_find_ci_span(start, (size_t) (end - start), " answer ", &suffix_offset); + if (suffix != NULL) { + end = suffix; + } + suffix = king_inference_openai_ascii_find_ci_span(start, (size_t) (end - start), " antworte ", &suffix_offset); + if (suffix != NULL) { + end = suffix; + } + + while (end > start && isspace((unsigned char) end[-1])) { + end--; + } + + return end > start ? zend_string_init(start, (size_t) (end - start), 0) : NULL; +} + +static bool king_inference_openai_find_count_subject( + zend_string *text, + zend_string **needle_out, + zend_string **subject_out) +{ + static const char *keyword_candidates[] = { + "letters", + "letter", + "characters", + "character", + "ltters", + "ltter", + "buchstaben", + "buchstabe", + "zeichen" + }; + static const char *subject_markers[] = { + "in the word ", + "in word ", + "in the string ", + "in string ", + "in da word ", + "in dem wort ", + "in das wort ", + "in der string ", + "im wort ", + "hat " + }; + const char *text_start = ZSTR_VAL(text); + const char *text_end = text_start + ZSTR_LEN(text); + const char *keyword = NULL; + size_t keyword_offset = 0; + size_t i; + zend_string *needle = NULL; + zend_string *subject = NULL; + const char *after_needle; + + if (!(king_inference_openai_ascii_contains_ci(text, "how many") + || king_inference_openai_ascii_contains_ci(text, "how often") + || king_inference_openai_ascii_contains_ci(text, "wie viele") + || king_inference_openai_ascii_contains_ci(text, "wieviele") + || king_inference_openai_ascii_contains_ci(text, "ho man"))) { + return false; + } + + for (i = 0; i < sizeof(keyword_candidates) / sizeof(keyword_candidates[0]); i++) { + keyword = king_inference_openai_ascii_find_ci_span( + text_start, + ZSTR_LEN(text), + keyword_candidates[i], + &keyword_offset + ); + if (keyword != NULL) { + break; + } + } + if (keyword == NULL) { + return false; + } + + needle = king_inference_openai_read_task_token(keyword + strlen(keyword_candidates[i]), text_end); + if (needle == NULL || ZSTR_LEN(needle) == 0 || ZSTR_LEN(needle) > 8) { + if (needle != NULL) { + zend_string_release(needle); + } + return false; + } + after_needle = keyword + strlen(keyword_candidates[i]) + ZSTR_LEN(needle); + + for (i = 0; i < sizeof(subject_markers) / sizeof(subject_markers[0]); i++) { + const char *marker; + size_t marker_offset = 0; + marker = king_inference_openai_ascii_find_ci_span( + after_needle, + (size_t) (text_end - after_needle), + subject_markers[i], + &marker_offset + ); + if (marker == NULL) { + continue; + } + subject = king_inference_openai_trim_task_subject( + marker + strlen(subject_markers[i]), + text_end + ); + if (subject != NULL) { + break; + } + } + + if (subject == NULL) { + zend_string_release(needle); + return false; + } + + *needle_out = needle; + *subject_out = subject; + return true; +} + +static size_t king_inference_openai_count_ascii_occurrences(zend_string *subject, zend_string *needle) +{ + const char *subject_start = ZSTR_VAL(subject); + const char *needle_start = ZSTR_VAL(needle); + size_t subject_len = ZSTR_LEN(subject); + size_t needle_len = ZSTR_LEN(needle); + size_t index; + size_t count = 0; + + if (needle_len == 0 || subject_len < needle_len) { + return 0; + } + + for (index = 0; index <= subject_len - needle_len; index++) { + size_t j; + bool match = true; + for (j = 0; j < needle_len; j++) { + unsigned char left = (unsigned char) subject_start[index + j]; + unsigned char right = (unsigned char) needle_start[j]; + if (tolower(left) != tolower(right)) { + match = false; + break; + } + } + if (match) { + count++; + index += needle_len - 1; + } + } + + return count; +} + +static zend_string *king_inference_openai_deterministic_count_content(zend_string *text) +{ + zend_string *needle = NULL; + zend_string *subject = NULL; + zend_string *content; + size_t count; + + if (!king_inference_openai_find_count_subject(text, &needle, &subject)) { + return NULL; + } + + count = king_inference_openai_count_ascii_occurrences(subject, needle); + content = strpprintf(0, "%zu", count); + zend_string_release(needle); + zend_string_release(subject); + return content; +} + +static zend_string *king_inference_openai_deterministic_task_content(zend_string *text) +{ + zend_string *content = king_inference_openai_deterministic_count_content(text); + + if (content != NULL) { + return content; + } + + return NULL; +} + +PHP_FUNCTION(king_inference_runtime_mini_op_content) +{ + zval *payload; + smart_str user_text = {0}; + zend_string *content = NULL; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY(payload) + ZEND_PARSE_PARAMETERS_END(); + + if (king_inference_openai_last_user_text(payload, &user_text) != SUCCESS || user_text.s == NULL) { + smart_str_free(&user_text); + RETURN_NULL(); + } + + content = king_inference_openai_exact_output_from_user_text(user_text.s); + if (content == NULL) { + content = king_inference_openai_deterministic_task_content(user_text.s); + } + smart_str_free(&user_text); + + if (content == NULL) { + RETURN_NULL(); + } + RETURN_STR(content); +} diff --git a/extension/src/inference/openai/runtime/compat/openai_drain_stream.inc b/extension/src/inference/openai/runtime/compat/openai_drain_stream.inc new file mode 100644 index 000000000..07bc49ab6 --- /dev/null +++ b/extension/src/inference/openai/runtime/compat/openai_drain_stream.inc @@ -0,0 +1,91 @@ +static void king_inference_openai_apply_stop_sequences_to_content( + zval *request, + smart_str *content, + zend_string **finish_reason_out) +{ + size_t offset = 0; + zend_string *trimmed; + + if (content == NULL || content->s == NULL) { + return; + } + smart_str_0(content); + if (!king_inference_openai_stop_trim_offset(request, content->s, &offset)) { + return; + } + + trimmed = zend_string_init(ZSTR_VAL(content->s), offset, 0); + smart_str_free(content); + smart_str_append(content, trimmed); + zend_string_release(trimmed); + smart_str_0(content); + king_inference_openai_set_finish_reason(finish_reason_out, "stop"); +} + +static zend_result king_inference_openai_drain_stream( + king_inference_stream_object *stream, + zval *stream_value, + const king_inference_openai_drain_controls *controls, + smart_str *content, + smart_str *sse_body, + zend_string **finish_reason_out) +{ + zend_long events = 0; + zend_long idle_events = 0; + + (void) stream_value; + + if (finish_reason_out != NULL) { + *finish_reason_out = zend_string_init("stop", sizeof("stop") - 1, 0); + } + + while (events < controls->max_events && idle_events < controls->max_idle_events) { + zval event; + + ZVAL_UNDEF(&event); + if (king_inference_stream_next_event(stream, controls->read_timeout_ms, &event) != SUCCESS) { + if (!Z_ISUNDEF(event)) { + zval_ptr_dtor(&event); + } + return FAILURE; + } + + if (Z_TYPE(event) == IS_NULL) { + zval_ptr_dtor(&event); + if (stream->done) { + king_inference_openai_apply_stop_sequences_to_content(&stream->request, content, finish_reason_out); + return SUCCESS; + } + idle_events++; + continue; + } + + idle_events = 0; + events++; + if (sse_body != NULL) { + if (king_inference_openai_sse_append(sse_body, &event) != SUCCESS) { + zval_ptr_dtor(&event); + return FAILURE; + } + } + if (content != NULL) { + king_inference_openai_append_delta_content(&event, content); + } + { + const char *event_finish_reason = NULL; + if (king_inference_openai_chunk_is_terminal(&event, &event_finish_reason)) { + if (finish_reason_out != NULL && event_finish_reason != NULL) { + zend_string_release(*finish_reason_out); + *finish_reason_out = zend_string_init(event_finish_reason, strlen(event_finish_reason), 0); + } + king_inference_openai_apply_stop_sequences_to_content(&stream->request, content, finish_reason_out); + zval_ptr_dtor(&event); + return SUCCESS; + } + } + zval_ptr_dtor(&event); + } + + king_inference_stream_timeout_state(stream); + return FAILURE; +} diff --git a/extension/src/inference/openai/runtime/options/openai_options.inc b/extension/src/inference/openai/runtime/options/openai_options.inc new file mode 100644 index 000000000..e09b7f06f --- /dev/null +++ b/extension/src/inference/openai/runtime/options/openai_options.inc @@ -0,0 +1,694 @@ +static zval *king_inference_openai_first_long_option(zval *source, const char * const *keys) +{ + size_t i; + + for (i = 0; keys[i] != NULL; i++) { + zval *field = king_inference_array_find(source, keys[i]); + if (field != NULL && Z_TYPE_P(field) == IS_LONG) { + return field; + } + } + + return NULL; +} + +static void king_inference_openai_copy_numeric_option( + zval *source, + zval *target, + const char *key) +{ + zval *field = king_inference_array_find(source, key); + zval copy; + + if (field == NULL || (Z_TYPE_P(field) != IS_LONG && Z_TYPE_P(field) != IS_DOUBLE)) { + return; + } + + ZVAL_COPY(©, field); + add_assoc_zval(target, key, ©); +} + +static void king_inference_openai_copy_long_option( + zval *source, + zval *target, + const char *key) +{ + zval *field = king_inference_array_find(source, key); + + if (field != NULL && Z_TYPE_P(field) == IS_LONG) { + add_assoc_long(target, key, Z_LVAL_P(field)); + } +} + +static bool king_inference_openai_stop_option_valid(zval *source, const char **message) +{ + zval *stop = king_inference_array_find(source, "stop"); + zval *entry; + + if (message != NULL) { + *message = NULL; + } + if (stop == NULL) { + return true; + } + if (Z_TYPE_P(stop) == IS_STRING) { + if (Z_STRLEN_P(stop) > 0) { + return true; + } + if (message != NULL) { + *message = "Generation stop must not be an empty string."; + } + return false; + } + if (!king_inference_openai_array_is_list(stop)) { + if (message != NULL) { + *message = "Generation stop must be a string or an array of strings."; + } + return false; + } + if (zend_hash_num_elements(Z_ARRVAL_P(stop)) == 0 || zend_hash_num_elements(Z_ARRVAL_P(stop)) > 4) { + if (message != NULL) { + *message = "Generation stop arrays must contain between 1 and 4 strings."; + } + return false; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(stop), entry) { + if (Z_TYPE_P(entry) != IS_STRING || Z_STRLEN_P(entry) == 0) { + if (message != NULL) { + *message = "Generation stop arrays must contain only non-empty strings."; + } + return false; + } + } ZEND_HASH_FOREACH_END(); + + return true; +} + +static bool king_inference_openai_long_option_valid( + zval *source, + const char *key, + bool positive, + const char **message) +{ + zval *field = king_inference_array_find(source, key); + + if (field == NULL) { + return true; + } + if (Z_TYPE_P(field) != IS_LONG) { + if (message != NULL) { + *message = "Generation integer options must be integers."; + } + return false; + } + if (positive && Z_LVAL_P(field) <= 0) { + if (message != NULL) { + *message = "Generation max token options must be positive integers."; + } + return false; + } + if (!positive && Z_LVAL_P(field) < 0) { + if (message != NULL) { + *message = "Generation top_k must be a non-negative integer."; + } + return false; + } + + return true; +} + +static bool king_inference_openai_integer_option_type_valid( + zval *source, + const char *key, + const char **message) +{ + zval *field = king_inference_array_find(source, key); + + if (field == NULL) { + return true; + } + if (Z_TYPE_P(field) == IS_LONG) { + return true; + } + if (message != NULL) { + *message = "Generation integer options must be integers."; + } + return false; +} + +static bool king_inference_openai_numeric_option_value( + zval *source, + const char *key, + bool *present, + double *value, + const char **message) +{ + zval *field = king_inference_array_find(source, key); + + *present = false; + *value = 0.0; + if (field == NULL) { + return true; + } + if (Z_TYPE_P(field) == IS_DOUBLE) { + *present = true; + *value = Z_DVAL_P(field); + if (!isfinite(*value)) { + if (message != NULL) { + *message = "Generation numeric options must be finite numbers."; + } + return false; + } + return true; + } + if (Z_TYPE_P(field) == IS_LONG) { + *present = true; + *value = (double) Z_LVAL_P(field); + return true; + } + if (message != NULL) { + *message = "Generation numeric options must be numbers."; + } + return false; +} + +static bool king_inference_openai_generation_options_valid( + zval *source, + const char * const *max_token_keys, + const char **message) +{ + size_t i; + bool present; + double value; + + if (message != NULL) { + *message = NULL; + } + for (i = 0; max_token_keys[i] != NULL; i++) { + if (!king_inference_openai_long_option_valid(source, max_token_keys[i], true, message)) { + return false; + } + } + if (!king_inference_openai_integer_option_type_valid(source, "seed", message) + || !king_inference_openai_long_option_valid(source, "top_k", false, message)) { + return false; + } + if (!king_inference_openai_numeric_option_value(source, "temperature", &present, &value, message)) { + return false; + } + if (present && value < 0.0) { + if (message != NULL) { + *message = "Generation temperature must be non-negative."; + } + return false; + } + if (!king_inference_openai_numeric_option_value(source, "top_p", &present, &value, message)) { + return false; + } + if (present && (value <= 0.0 || value > 1.0)) { + if (message != NULL) { + *message = "Generation top_p must be greater than zero and at most one."; + } + return false; + } + + return true; +} + +static bool king_inference_openai_chat_generation_options_valid(zval *source, const char **message) +{ + static const char * const max_token_keys[] = { + "max_completion_tokens", + "max_tokens", + NULL + }; + + return king_inference_openai_generation_options_valid(source, max_token_keys, message); +} + +static bool king_inference_openai_completion_generation_options_valid(zval *source, const char **message) +{ + static const char * const max_token_keys[] = { + "max_tokens", + NULL + }; + + return king_inference_openai_generation_options_valid(source, max_token_keys, message); +} + +static bool king_inference_openai_response_generation_options_valid(zval *source, const char **message) +{ + static const char * const max_token_keys[] = { + "max_output_tokens", + "max_tokens", + NULL + }; + + return king_inference_openai_generation_options_valid(source, max_token_keys, message); +} + +static bool king_inference_openai_stream_generation_options_valid(zval *source, const char **message) +{ + static const char * const max_token_keys[] = { + "max_tokens", + NULL + }; + + return king_inference_openai_generation_options_valid(source, max_token_keys, message); +} + +static bool king_inference_openai_stream_options_valid(zval *source, const char **message) +{ + zval *stream_options = king_inference_array_find(source, "stream_options"); + zval *include_usage; + + if (message != NULL) { + *message = NULL; + } + if (stream_options == NULL) { + return true; + } + if (!king_inference_openai_array_is_object(stream_options)) { + if (message != NULL) { + *message = "OpenAI stream_options must be an object when provided."; + } + return false; + } + + include_usage = king_inference_array_find(stream_options, "include_usage"); + if (include_usage != NULL && Z_TYPE_P(include_usage) != IS_TRUE && Z_TYPE_P(include_usage) != IS_FALSE) { + if (message != NULL) { + *message = "OpenAI stream_options.include_usage must be a boolean when provided."; + } + return false; + } + + return true; +} + +static bool king_inference_openai_stream_field_valid(zval *source, const char **message) +{ + zval *stream = king_inference_array_find(source, "stream"); + + if (message != NULL) { + *message = NULL; + } + if (stream == NULL) { + return true; + } + if (Z_TYPE_P(stream) == IS_TRUE || Z_TYPE_P(stream) == IS_FALSE) { + return true; + } + if (message != NULL) { + *message = "OpenAI stream must be a boolean when provided."; + } + return false; +} + +static bool king_inference_openai_model_field_valid(zval *source, const char **message) +{ + zval *model = king_inference_array_find(source, "model"); + + if (message != NULL) { + *message = NULL; + } + if (model == NULL) { + return true; + } + if (Z_TYPE_P(model) == IS_STRING && Z_STRLEN_P(model) > 0) { + return true; + } + if (message != NULL) { + *message = "OpenAI model must be a non-empty string when provided."; + } + return false; +} + +static bool king_inference_openai_single_choice_option_valid(zval *source, const char **message) +{ + zval *choice_count = king_inference_array_find(source, "n"); + + if (message != NULL) { + *message = NULL; + } + if (choice_count == NULL) { + return true; + } + if (Z_TYPE_P(choice_count) != IS_LONG || Z_LVAL_P(choice_count) <= 0) { + if (message != NULL) { + *message = "OpenAI n must be a positive integer when provided."; + } + return false; + } + if (Z_LVAL_P(choice_count) != 1) { + if (message != NULL) { + *message = "King OpenAI-compatible generation routes require n=1."; + } + return false; + } + + return true; +} + +static bool king_inference_openai_unsupported_feature_active(zval *field) +{ + if (field == NULL || Z_TYPE_P(field) == IS_NULL || Z_TYPE_P(field) == IS_FALSE) { + return false; + } + if (Z_TYPE_P(field) == IS_ARRAY && zend_hash_num_elements(Z_ARRVAL_P(field)) == 0) { + return false; + } + if (Z_TYPE_P(field) == IS_STRING && Z_STRLEN_P(field) == 0) { + return false; + } + if (Z_TYPE_P(field) == IS_LONG && Z_LVAL_P(field) == 0) { + return false; + } + + return true; +} + +static bool king_inference_openai_no_unsupported_features( + zval *source, + const char * const *keys, + const char **message) +{ + size_t i; + + if (message != NULL) { + *message = NULL; + } + for (i = 0; keys[i] != NULL; i++) { + if (king_inference_openai_unsupported_feature_active(king_inference_array_find(source, keys[i]))) { + if (message != NULL) { + *message = "OpenAI request uses a feature this King local inference route does not support."; + } + return false; + } + } + + return true; +} + +static bool king_inference_openai_response_format_valid(zval *source, const char **message) +{ + zval *format = king_inference_array_find(source, "response_format"); + zval *type; + + if (message != NULL) { + *message = NULL; + } + if (format == NULL || Z_TYPE_P(format) == IS_NULL) { + return true; + } + if (!king_inference_openai_array_is_object(format)) { + if (message != NULL) { + *message = "OpenAI response_format must be an object when provided."; + } + return false; + } + + type = king_inference_array_find(format, "type"); + if (type == NULL || Z_TYPE_P(type) != IS_STRING || Z_STRLEN_P(type) == 0) { + if (message != NULL) { + *message = "OpenAI response_format.type must be a non-empty string."; + } + return false; + } + if (zend_string_equals_literal(Z_STR_P(type), "text")) { + return true; + } + if (zend_string_equals_literal(Z_STR_P(type), "json_object")) { + return true; + } + + if (message != NULL) { + *message = "King local inference supports only response_format.type=text or json_object."; + } + return false; +} + +static bool king_inference_openai_response_format_json_object_requested(zval *source) +{ + zval *format = king_inference_array_find(source, "response_format"); + zval *type; + + if (!king_inference_openai_array_is_object(format)) { + return false; + } + + type = king_inference_array_find(format, "type"); + return type != NULL + && Z_TYPE_P(type) == IS_STRING + && zend_string_equals_literal(Z_STR_P(type), "json_object"); +} + +static void king_inference_openai_append_response_format_instruction(zval *source, smart_str *buffer) +{ + if (!king_inference_openai_response_format_json_object_requested(source)) { + return; + } + + smart_str_appends( + buffer, + "Return only one valid JSON object. Do not wrap it in Markdown. Do not include prose before or after the JSON object.\n" + ); +} + +static zend_result king_inference_openai_validate_json_object_output( + zend_string *content, + const char **message) +{ + zval decoded; + + if (message != NULL) { + *message = NULL; + } + if (content == NULL || ZSTR_LEN(content) == 0) { + if (message != NULL) { + *message = "Model output was empty for response_format.type=json_object."; + } + return FAILURE; + } + + ZVAL_UNDEF(&decoded); + if (php_json_decode(&decoded, ZSTR_VAL(content), ZSTR_LEN(content), 0, 512) != SUCCESS) { + if (EG(exception)) { + zend_clear_exception(); + } + king_set_error(""); + if (message != NULL) { + *message = "Model output was not valid JSON for response_format.type=json_object."; + } + return FAILURE; + } + if (Z_TYPE(decoded) != IS_OBJECT) { + zval_ptr_dtor(&decoded); + if (message != NULL) { + *message = "Model output was valid JSON but not a JSON object for response_format.type=json_object."; + } + return FAILURE; + } + + zval_ptr_dtor(&decoded); + return SUCCESS; +} + +static zend_result king_inference_openai_validate_response_format_output( + zval *request, + zend_string *content, + const char **message) +{ + if (message != NULL) { + *message = NULL; + } + if (!king_inference_openai_response_format_json_object_requested(request)) { + return SUCCESS; + } + + return king_inference_openai_validate_json_object_output(content, message); +} + +static bool king_inference_openai_response_instructions_valid(zval *source, const char **message) +{ + zval *instructions = king_inference_array_find(source, "instructions"); + + if (message != NULL) { + *message = NULL; + } + if (instructions == NULL || Z_TYPE_P(instructions) == IS_NULL) { + return true; + } + if (Z_TYPE_P(instructions) == IS_STRING && Z_STRLEN_P(instructions) > 0) { + return true; + } + if (message != NULL) { + *message = "Responses instructions must be a non-empty string when provided."; + } + return false; +} + +static bool king_inference_openai_chat_feature_options_valid(zval *source, const char **message) +{ + static const char * const unsupported_keys[] = { + "modalities", + "audio", + "prediction", + "logprobs", + "top_logprobs", + NULL + }; + + if (!king_inference_openai_response_format_valid(source, message)) { + return false; + } + + return king_inference_openai_no_unsupported_features(source, unsupported_keys, message); +} + +static bool king_inference_openai_completion_feature_options_valid(zval *source, const char **message) +{ + static const char * const unsupported_keys[] = { + "suffix", + "echo", + "best_of", + "logprobs", + NULL + }; + + return king_inference_openai_no_unsupported_features(source, unsupported_keys, message); +} + +static bool king_inference_openai_response_feature_options_valid(zval *source, const char **message) +{ + static const char * const unsupported_keys[] = { + "tools", + "tool_choice", + "parallel_tool_calls", + "reasoning", + "include", + "previous_response_id", + NULL + }; + + if (!king_inference_openai_response_format_valid(source, message)) { + return false; + } + + return king_inference_openai_no_unsupported_features(source, unsupported_keys, message); +} + +static void king_inference_openai_copy_stop_option(zval *source, zval *target) +{ + zval *stop = king_inference_array_find(source, "stop"); + zval copy; + + if (stop == NULL || !king_inference_openai_stop_option_valid(source, NULL)) { + return; + } + + ZVAL_COPY(©, stop); + add_assoc_zval(target, "stop", ©); +} + +static void king_inference_openai_copy_array_option(zval *source, zval *target, const char *key) +{ + zval *field = king_inference_array_find(source, key); + zval copy; + + if (field == NULL || Z_TYPE_P(field) != IS_ARRAY) { + return; + } + + ZVAL_COPY(©, field); + add_assoc_zval(target, key, ©); +} + +static void king_inference_openai_copy_response_format_option(zval *source, zval *target) +{ + zval *format = king_inference_array_find(source, "response_format"); + zval copy; + + if (format == NULL || Z_TYPE_P(format) != IS_ARRAY) { + return; + } + + ZVAL_COPY(©, format); + add_assoc_zval(target, "response_format", ©); +} + +static void king_inference_openai_copy_native_graph_options(zval *source, zval *target) +{ + king_inference_openai_copy_array_option(source, target, "graph"); + king_inference_openai_copy_array_option(source, target, "graphs"); + king_inference_openai_copy_array_option(source, target, "graph_options"); +} + +static void king_inference_openai_copy_generation_options( + zval *source, + zval *target, + const char * const *max_token_keys) +{ + zval *max_tokens = king_inference_openai_first_long_option(source, max_token_keys); + + if (max_tokens != NULL) { + add_assoc_long(target, "max_tokens", Z_LVAL_P(max_tokens)); + } + king_inference_openai_copy_numeric_option(source, target, "temperature"); + king_inference_openai_copy_numeric_option(source, target, "top_p"); + king_inference_openai_copy_long_option(source, target, "seed"); + king_inference_openai_copy_long_option(source, target, "top_k"); +} + +static void king_inference_openai_copy_chat_generation_options(zval *source, zval *target) +{ + static const char * const max_token_keys[] = { + "max_completion_tokens", + "max_tokens", + NULL + }; + + king_inference_openai_copy_generation_options(source, target, max_token_keys); + king_inference_openai_copy_stop_option(source, target); + king_inference_openai_copy_response_format_option(source, target); + king_inference_openai_copy_native_graph_options(source, target); +} + +static void king_inference_openai_copy_completion_generation_options(zval *source, zval *target) +{ + static const char * const max_token_keys[] = { + "max_tokens", + NULL + }; + + king_inference_openai_copy_generation_options(source, target, max_token_keys); + king_inference_openai_copy_stop_option(source, target); + king_inference_openai_copy_native_graph_options(source, target); +} + +static void king_inference_openai_copy_response_generation_options(zval *source, zval *target) +{ + static const char * const max_token_keys[] = { + "max_output_tokens", + "max_tokens", + NULL + }; + + king_inference_openai_copy_generation_options(source, target, max_token_keys); + king_inference_openai_copy_response_format_option(source, target); + king_inference_openai_copy_native_graph_options(source, target); +} + +static bool king_inference_openai_stream_usage_requested(zval *source) +{ + zval *stream_options = king_inference_array_find(source, "stream_options"); + zval *include_usage; + + if (stream_options == NULL || Z_TYPE_P(stream_options) != IS_ARRAY) { + return false; + } + + include_usage = king_inference_array_find(stream_options, "include_usage"); + return include_usage != NULL && zend_is_true(include_usage); +} diff --git a/extension/src/inference/openai/runtime/options/openai_tools.inc b/extension/src/inference/openai/runtime/options/openai_tools.inc new file mode 100644 index 000000000..b79a5fae9 --- /dev/null +++ b/extension/src/inference/openai/runtime/options/openai_tools.inc @@ -0,0 +1,281 @@ +static bool king_inference_openai_tools_requested(zval *source) +{ + zval *tools = king_inference_array_find(source, "tools"); + + return king_inference_openai_array_is_list(tools) + && zend_hash_num_elements(Z_ARRVAL_P(tools)) > 0; +} + +static bool king_inference_openai_tools_active(zval *source) +{ + /* + * Tool schemas in OpenAI-compatible requests are accepted by the plain + * inference route, but executable tool dispatch is not part of this path + * yet. Keep generation and streaming on the normal text path instead of + * buffering output and pretending model text is a completed tool call. + */ + if (!king_inference_openai_tools_requested(source)) { + return false; + } + return false; +} + +static const char *king_inference_openai_tool_calls_unsupported_message(void) +{ + return "OpenAI tool calls require a real King tool execution path; this route currently ignores tool request fields for plain inference until that path exists."; +} + +static bool king_inference_openai_message_has_tool_calls(zval *message) +{ + zval *tool_calls = king_inference_array_find(message, "tool_calls"); + + return king_inference_openai_array_is_list(tool_calls) + && zend_hash_num_elements(Z_ARRVAL_P(tool_calls)) > 0; +} + +static bool king_inference_openai_message_has_tool_call_request(zval *message) +{ + zval *tool_calls = king_inference_array_find(message, "tool_calls"); + + return tool_calls != NULL && Z_TYPE_P(tool_calls) != IS_NULL; +} + +static zend_result king_inference_openai_append_message_tool_calls_text(zval *message, smart_str *buffer) +{ + zval *tool_calls = king_inference_array_find(message, "tool_calls"); + zend_string *json = NULL; + + if (!king_inference_openai_message_has_tool_calls(message)) { + return FAILURE; + } + if (king_inference_openai_json_encode(tool_calls, &json) != SUCCESS || json == NULL) { + return FAILURE; + } + smart_str_append(buffer, json); + zend_string_release(json); + return SUCCESS; +} + +static void king_inference_openai_append_tool_prompt(zval *source, smart_str *buffer) +{ + zval *tools = king_inference_array_find(source, "tools"); + zval *choice = king_inference_array_find(source, "tool_choice"); + zend_string *tools_json = NULL; + zend_string *choice_json = NULL; + + if (!king_inference_openai_tools_active(source)) { + return; + } + if (king_inference_openai_json_encode(tools, &tools_json) != SUCCESS || tools_json == NULL) { + return; + } + if (choice != NULL && Z_TYPE_P(choice) != IS_NULL) { + king_inference_openai_json_encode(choice, &choice_json); + } + + smart_str_appends(buffer, "system: Tools are available. If a tool call is required or useful, answer only with compact JSON using this exact shape: {\"tool_calls\":[{\"name\":\"tool_name\",\"arguments\":{}}]}. Do not wrap tool-call JSON in markdown.\n"); + smart_str_appends(buffer, "system: Available tools JSON: "); + smart_str_append(buffer, tools_json); + smart_str_appendc(buffer, '\n'); + if (choice_json != NULL) { + smart_str_appends(buffer, "system: Tool choice JSON: "); + smart_str_append(buffer, choice_json); + smart_str_appendc(buffer, '\n'); + } + + zend_string_release(tools_json); + if (choice_json != NULL) { + zend_string_release(choice_json); + } +} + +static zend_string *king_inference_openai_tool_json_slice(zend_string *content) +{ + const char *start; + const char *end; + const char *cursor; + + if (content == NULL || ZSTR_LEN(content) == 0) { + return NULL; + } + + start = memchr(ZSTR_VAL(content), '{', ZSTR_LEN(content)); + if (start == NULL) { + return NULL; + } + + end = NULL; + cursor = ZSTR_VAL(content) + ZSTR_LEN(content); + while (cursor > start) { + cursor--; + if (*cursor == '}') { + end = cursor; + break; + } + } + if (end == NULL || end < start) { + return NULL; + } + + return zend_string_init(start, (size_t) (end - start + 1), 0); +} + +static zend_string *king_inference_openai_tool_call_name(zval *entry) +{ + zval *function; + zval *name; + + if (!king_inference_openai_array_is_object(entry)) { + return NULL; + } + + function = king_inference_array_find(entry, "function"); + name = function != NULL && king_inference_openai_array_is_object(function) + ? king_inference_array_find(function, "name") + : king_inference_array_find(entry, "name"); + if (name == NULL || Z_TYPE_P(name) != IS_STRING || Z_STRLEN_P(name) == 0) { + return NULL; + } + + return zend_string_copy(Z_STR_P(name)); +} + +static zend_string *king_inference_openai_tool_call_arguments(zval *entry) +{ + zval *function; + zval *arguments; + zend_string *json = NULL; + + if (!king_inference_openai_array_is_object(entry)) { + return NULL; + } + + function = king_inference_array_find(entry, "function"); + arguments = function != NULL && king_inference_openai_array_is_object(function) + ? king_inference_array_find(function, "arguments") + : king_inference_array_find(entry, "arguments"); + if (arguments == NULL) { + arguments = king_inference_array_find(entry, "args"); + } + if (arguments == NULL || Z_TYPE_P(arguments) == IS_NULL) { + return zend_string_init("{}", sizeof("{}") - 1, 0); + } + if (Z_TYPE_P(arguments) == IS_STRING) { + return zend_string_copy(Z_STR_P(arguments)); + } + if (!king_inference_openai_array_is_object(arguments)) { + return NULL; + } + if (king_inference_openai_json_encode(arguments, &json) != SUCCESS) { + return NULL; + } + + return json; +} + +static zend_result king_inference_openai_append_normalized_tool_call( + zval *target, + zval *entry, + zend_long index) +{ + zend_string *name = king_inference_openai_tool_call_name(entry); + zend_string *arguments = king_inference_openai_tool_call_arguments(entry); + zval call; + zval function; + + if (name == NULL || arguments == NULL) { + if (name != NULL) { + zend_string_release(name); + } + if (arguments != NULL) { + zend_string_release(arguments); + } + return FAILURE; + } + + array_init(&call); + add_assoc_str(&call, "id", strpprintf(0, "call_king_" ZEND_LONG_FMT, index)); + add_assoc_string(&call, "type", "function"); + array_init(&function); + add_assoc_str(&function, "name", name); + add_assoc_str(&function, "arguments", arguments); + add_assoc_zval(&call, "function", &function); + add_next_index_zval(target, &call); + return SUCCESS; +} + +static bool king_inference_openai_tool_calls_from_content(zend_string *content, zval *tool_calls) +{ + zend_string *json = king_inference_openai_tool_json_slice(content); + zval decoded; + zval *entries; + zval single_entry; + zval *entry; + zend_long index = 0; + + if (tool_calls == NULL) { + return false; + } + ZVAL_UNDEF(tool_calls); + if (json == NULL) { + return false; + } + ZVAL_UNDEF(&decoded); + if (king_inference_openai_json_decode(json, &decoded) != SUCCESS || !king_inference_openai_array_is_object(&decoded)) { + zend_string_release(json); + if (!Z_ISUNDEF(decoded)) { + zval_ptr_dtor(&decoded); + } + return false; + } + zend_string_release(json); + + entries = king_inference_array_find(&decoded, "tool_calls"); + if (!king_inference_openai_array_is_list(entries)) { + entries = king_inference_array_find(&decoded, "tool_call"); + if (king_inference_openai_array_is_object(entries)) { + array_init(&single_entry); + Z_TRY_ADDREF_P(entries); + add_next_index_zval(&single_entry, entries); + entries = &single_entry; + } else { + zend_string *decoded_name = king_inference_openai_tool_call_name(&decoded); + if (decoded_name == NULL) { + zval_ptr_dtor(&decoded); + return false; + } + zend_string_release(decoded_name); + array_init(&single_entry); + Z_TRY_ADDREF(decoded); + add_next_index_zval(&single_entry, &decoded); + entries = &single_entry; + } + } else { + ZVAL_UNDEF(&single_entry); + } + + array_init(tool_calls); + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(entries), entry) { + if (king_inference_openai_append_normalized_tool_call(tool_calls, entry, index++) != SUCCESS) { + zval_ptr_dtor(tool_calls); + ZVAL_UNDEF(tool_calls); + if (!Z_ISUNDEF(single_entry)) { + zval_ptr_dtor(&single_entry); + } + zval_ptr_dtor(&decoded); + return false; + } + } ZEND_HASH_FOREACH_END(); + + if (!Z_ISUNDEF(single_entry)) { + zval_ptr_dtor(&single_entry); + } + zval_ptr_dtor(&decoded); + if (zend_hash_num_elements(Z_ARRVAL_P(tool_calls)) == 0) { + zval_ptr_dtor(tool_calls); + ZVAL_UNDEF(tool_calls); + return false; + } + + return true; +} diff --git a/extension/src/inference/openai/runtime/stream/openai_drain_controls.inc b/extension/src/inference/openai/runtime/stream/openai_drain_controls.inc new file mode 100644 index 000000000..e0dce94a9 --- /dev/null +++ b/extension/src/inference/openai/runtime/stream/openai_drain_controls.inc @@ -0,0 +1,68 @@ +typedef struct _king_inference_openai_drain_controls { + zend_long read_timeout_ms; + zend_long max_events; + zend_long max_idle_events; +} king_inference_openai_drain_controls; + +static bool king_inference_openai_positive_router_limit( + zval *options, + const char *key, + zend_long fallback, + zend_long *limit_out, + const char **message) +{ + zval *field = king_inference_array_find(options, key); + + if (message != NULL) { + *message = NULL; + } + if (limit_out == NULL) { + return false; + } + *limit_out = fallback; + if (field == NULL) { + return true; + } + if (Z_TYPE_P(field) != IS_LONG || Z_LVAL_P(field) <= 0) { + if (message != NULL) { + *message = "King OpenAI router limit and drain control options must be positive integers."; + } + return false; + } + + *limit_out = Z_LVAL_P(field); + return true; +} + +static bool king_inference_openai_drain_controls_from_options( + zval *options, + king_inference_openai_drain_controls *controls, + const char **message) +{ + if (message != NULL) { + *message = NULL; + } + if (controls == NULL) { + return false; + } + + return king_inference_openai_positive_router_limit( + options, + "read_timeout_ms", + 250, + &controls->read_timeout_ms, + message + ) && king_inference_openai_positive_router_limit( + options, + "max_events", + 4096, + &controls->max_events, + message + ) && king_inference_openai_positive_router_limit( + options, + "max_idle_events", + 240, + &controls->max_idle_events, + message + ); +} diff --git a/extension/src/inference/openai/runtime/stream/openai_stream_factory.inc b/extension/src/inference/openai/runtime/stream/openai_stream_factory.inc new file mode 100644 index 000000000..35ed864f1 --- /dev/null +++ b/extension/src/inference/openai/runtime/stream/openai_stream_factory.inc @@ -0,0 +1,354 @@ +#include "../backend/openai_backend.inc" + +static bool king_inference_openai_model_has_token( + king_inference_model_object *model, + const char *token +) { + return token != NULL + && Z_TYPE(model->tokenizer_lookup) == IS_ARRAY + && zend_hash_str_exists(Z_ARRVAL(model->tokenizer_lookup), token, strlen(token)); +} + +static bool king_inference_openai_model_is_gemma(king_inference_model_object *model) +{ + return model != NULL + && ((model->gguf.architecture != NULL + && (zend_string_equals_literal(model->gguf.architecture, "gemma3") + || zend_string_equals_literal(model->gguf.architecture, "gemma4"))) + || (model->gguf.tokenizer_model != NULL + && (zend_string_equals_literal(model->gguf.tokenizer_model, "gemma") + || zend_string_equals_literal(model->gguf.tokenizer_model, "gemma3") + || zend_string_equals_literal(model->gguf.tokenizer_model, "gemma4")))); +} + +static bool king_inference_openai_model_uses_gemma_pipe_turn_template(king_inference_model_object *model) +{ + return king_inference_openai_model_is_gemma(model) + && king_inference_openai_model_has_token(model, "<|turn>") + && king_inference_openai_model_has_token(model, ""); +} + +static bool king_inference_openai_model_uses_gemma_start_turn_template(king_inference_model_object *model) +{ + return king_inference_openai_model_is_gemma(model) + && king_inference_openai_model_has_token(model, "") + && king_inference_openai_model_has_token(model, ""); +} + +static bool king_inference_openai_model_uses_gemma_chat_template(king_inference_model_object *model) +{ + return king_inference_openai_model_uses_gemma_start_turn_template(model) + || king_inference_openai_model_uses_gemma_pipe_turn_template(model); +} + +static zend_result king_inference_openai_append_message_text(zval *message, smart_str *text) +{ + zval *role = king_inference_array_find(message, "role"); + zval *content = king_inference_array_find(message, "content"); + + if (king_inference_openai_append_content_text(content, text) == SUCCESS) { + return SUCCESS; + } + if (role != NULL + && Z_TYPE_P(role) == IS_STRING + && zend_string_equals_literal(Z_STR_P(role), "assistant") + && king_inference_openai_append_message_tool_calls_text(message, text) == SUCCESS) { + return SUCCESS; + } + return FAILURE; +} + +static zend_result king_inference_openai_render_gemma_messages_prompt( + zval *payload, + zval *prompt, + bool pipe_turn_template, + bool prepend_bos +) +{ + zval *messages = king_inference_array_find(payload, "messages"); + zval *message; + smart_str rendered = {0}; + smart_str pending_instructions = {0}; + + if (prepend_bos) { + smart_str_appends(&rendered, ""); + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(messages), message) { + zval *role; + const char *turn_role = "user"; + smart_str text = {0}; + + if (Z_TYPE_P(message) != IS_ARRAY) { + smart_str_free(&rendered); + return FAILURE; + } + role = king_inference_array_find(message, "role"); + if (role == NULL || Z_TYPE_P(role) != IS_STRING || Z_STRLEN_P(role) == 0) { + smart_str_free(&pending_instructions); + smart_str_free(&rendered); + return FAILURE; + } + if (zend_string_equals_literal(Z_STR_P(role), "assistant")) { + turn_role = "model"; + } + if (king_inference_openai_append_message_text(message, &text) != SUCCESS) { + smart_str_free(&text); + smart_str_free(&pending_instructions); + smart_str_free(&rendered); + return FAILURE; + } + if (zend_string_equals_literal(Z_STR_P(role), "system") + || zend_string_equals_literal(Z_STR_P(role), "developer")) { + smart_str_0(&text); + if (text.s != NULL) { + smart_str_append(&pending_instructions, text.s); + smart_str_appends(&pending_instructions, "\n\n"); + } + smart_str_free(&text); + continue; + } + + smart_str_appends(&rendered, pipe_turn_template ? "<|turn>" : ""); + smart_str_appends(&rendered, turn_role); + smart_str_appendc(&rendered, '\n'); + if (zend_string_equals_literal(Z_STR_P(role), "user") + && pending_instructions.s != NULL + && ZSTR_LEN(pending_instructions.s) > 0) { + smart_str_0(&pending_instructions); + smart_str_append(&rendered, pending_instructions.s); + smart_str_free(&pending_instructions); + } + if (zend_string_equals_literal(Z_STR_P(role), "tool")) { + smart_str_appends(&rendered, "Tool result:\n"); + } + smart_str_0(&text); + if (text.s != NULL) { + smart_str_append(&rendered, text.s); + } + smart_str_free(&text); + smart_str_appends(&rendered, pipe_turn_template ? "\n" : "\n"); + } ZEND_HASH_FOREACH_END(); + + king_inference_openai_append_response_format_instruction(payload, &pending_instructions); + if (pending_instructions.s != NULL && ZSTR_LEN(pending_instructions.s) > 0) { + smart_str_appends(&rendered, pipe_turn_template ? "<|turn>user\n" : "user\n"); + smart_str_0(&pending_instructions); + smart_str_append(&rendered, pending_instructions.s); + smart_str_appends(&rendered, pipe_turn_template ? "\n" : "\n"); + smart_str_free(&pending_instructions); + } + + smart_str_appends(&rendered, pipe_turn_template ? "<|turn>model\n" : "model\n"); + smart_str_0(&rendered); + if (rendered.s == NULL) { + ZVAL_STRING(prompt, pipe_turn_template ? "<|turn>model\n" : "model\n"); + } else { + ZVAL_STR(prompt, rendered.s); + } + return SUCCESS; +} + +static zend_result king_inference_openai_render_messages_prompt( + king_inference_model_object *model, + zval *payload, + zval *prompt) +{ + zval *messages = king_inference_array_find(payload, "messages"); + zval *message; + smart_str rendered = {0}; + + if (!king_inference_openai_array_is_list(messages)) { + return FAILURE; + } + if (king_inference_openai_model_uses_gemma_chat_template(model)) { + bool start_turn_template = king_inference_openai_model_uses_gemma_start_turn_template(model); + return king_inference_openai_render_gemma_messages_prompt( + payload, + prompt, + !start_turn_template && king_inference_openai_model_uses_gemma_pipe_turn_template(model), + king_inference_openai_model_has_token(model, "") + ); + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(messages), message) { + zval *role; + smart_str text = {0}; + + if (Z_TYPE_P(message) != IS_ARRAY) { + smart_str_free(&rendered); + return FAILURE; + } + role = king_inference_array_find(message, "role"); + if (role == NULL || Z_TYPE_P(role) != IS_STRING || Z_STRLEN_P(role) == 0) { + smart_str_free(&rendered); + return FAILURE; + } + + smart_str_append(&rendered, Z_STR_P(role)); + smart_str_appends(&rendered, ": "); + if (king_inference_openai_append_message_text(message, &text) == SUCCESS) { + smart_str_0(&text); + if (text.s != NULL) { + smart_str_append(&rendered, text.s); + } + } + smart_str_free(&text); + smart_str_appends(&rendered, "\n"); + } ZEND_HASH_FOREACH_END(); + + king_inference_openai_append_response_format_instruction(payload, &rendered); + smart_str_appends(&rendered, "assistant: "); + smart_str_0(&rendered); + if (rendered.s == NULL) { + ZVAL_STRING(prompt, "assistant: "); + } else { + ZVAL_STR(prompt, rendered.s); + } + return SUCCESS; +} + +static zend_long king_inference_openai_native_cpu_max_operations(king_inference_model_object *model) +{ + zend_ulong vocab = model->gguf.tokenizer_token_count > 0 ? model->gguf.tokenizer_token_count : 262144; + zend_ulong hidden = model->gguf.architecture_params[KING_INFERENCE_ARCH_EMBEDDING_LENGTH] > 0 + ? model->gguf.architecture_params[KING_INFERENCE_ARCH_EMBEDDING_LENGTH] + : 1152; + zend_ulong fallback = 400000000; + zend_ulong candidate = fallback; + + if (hidden > 0 && vocab <= ((zend_ulong) ZEND_LONG_MAX - 1024) / hidden) { + candidate = (vocab * hidden) + 1024; + if (candidate < fallback) { + candidate = fallback; + } + } + + return candidate > (zend_ulong) ZEND_LONG_MAX ? ZEND_LONG_MAX : (zend_long) candidate; +} + +static void king_inference_openai_add_native_cpu_graph_defaults( + king_inference_model_object *model, + zval *request) +{ + zval graph_options; + zend_ulong vocab; + + if (king_inference_array_find(request, "graph_options") != NULL) { + return; + } + + vocab = model->gguf.tokenizer_token_count > 0 ? model->gguf.tokenizer_token_count : 262144; + array_init(&graph_options); + add_assoc_long(&graph_options, "max_vector_values", (zend_long) (vocab > 262144 ? vocab : 262144)); + add_assoc_long(&graph_options, "max_operations", king_inference_openai_native_cpu_max_operations(model)); + add_assoc_bool(&graph_options, "return_outputs", false); + add_assoc_zval(request, "graph_options", &graph_options); +} + +static zend_result king_inference_openai_prepare_native_cpu_prompt_request( + king_inference_model_object *model, + zval *request) +{ + zval prompt; + + if (king_inference_array_find(request, "graph") != NULL + || king_inference_array_find(request, "graphs") != NULL + || king_inference_array_find(request, "native_prompt_text") != NULL) { + return SUCCESS; + } + if (king_inference_openai_render_messages_prompt(model, request, &prompt) != SUCCESS) { + return SUCCESS; + } + + add_assoc_zval(request, "native_prompt_text", &prompt); + if (king_inference_array_find(request, "max_tokens") == NULL) { + add_assoc_long(request, "max_tokens", 128); + } + king_inference_openai_add_native_cpu_graph_defaults(model, request); + return SUCCESS; +} + +static zend_result king_inference_openai_prepare_native_gpu_prompt_request( + king_inference_model_object *model, + zval *request) +{ + zval prompt; + + if (king_inference_array_find(request, "graph") != NULL + || king_inference_array_find(request, "graphs") != NULL + || king_inference_array_find(request, "native_prompt_text") != NULL) { + return SUCCESS; + } + if (king_inference_openai_render_messages_prompt(model, request, &prompt) != SUCCESS) { + return SUCCESS; + } + + add_assoc_zval(request, "native_prompt_text", &prompt); + if (king_inference_array_find(request, "max_tokens") == NULL) { + add_assoc_long(request, "max_tokens", 128); + } + return SUCCESS; +} + +static zend_result king_inference_openai_create_stream( + zval *stream_value, + zval *model, + zval *payload, + zval *options, + const char *function_name) +{ + king_inference_stream_object *stream; + king_inference_model_object *model_object; + king_inference_backend_kind kind; + + object_init_ex(stream_value, king_ce_inference_stream); + stream = php_king_inference_stream_obj_from_zend(Z_OBJ_P(stream_value)); + ZVAL_COPY(&stream->model, model); + ZVAL_DUP(&stream->request, payload); + SEPARATE_ARRAY(&stream->request); + if (options != NULL && Z_TYPE_P(options) == IS_ARRAY) { + ZVAL_DUP(&stream->options, options); + SEPARATE_ARRAY(&stream->options); + } else { + array_init(&stream->options); + } + + add_assoc_bool(&stream->request, "openai_compatible", true); + add_assoc_string(&stream->request, "format", "openai_chat_completions"); + king_inference_openai_copy_chat_generation_options(&stream->request, &stream->request); + + model_object = php_king_inference_model_obj_from_zend(Z_OBJ_P(model)); + if (king_inference_backend_kind_from_config(&model_object->config, &kind, function_name) != SUCCESS) { + zval_ptr_dtor(stream_value); + ZVAL_UNDEF(stream_value); + return FAILURE; + } + if (kind == KING_INFERENCE_BACKEND_KING_NATIVE_CPU + && king_inference_openai_prepare_native_cpu_prompt_request(model_object, &stream->request) != SUCCESS) { + zval_ptr_dtor(stream_value); + ZVAL_UNDEF(stream_value); + return FAILURE; + } + if (kind == KING_INFERENCE_BACKEND_KING_NATIVE_GPU + && king_inference_openai_prepare_native_gpu_prompt_request(model_object, &stream->request) != SUCCESS) { + zval_ptr_dtor(stream_value); + ZVAL_UNDEF(stream_value); + return FAILURE; + } + + add_assoc_bool(&stream->options, "openai_compatible", true); + add_assoc_string(&stream->options, "format", "openai_chat_completions"); + stream->openai_compatible = true; + stream->created_at = (zend_long) time(NULL); + king_inference_stream_prepare_openai(stream); + + if (king_inference_stream_start(stream, function_name) != SUCCESS) { + king_inference_stream_throw_start_failure(stream, function_name); + zval_ptr_dtor(stream_value); + ZVAL_UNDEF(stream_value); + return FAILURE; + } + + return SUCCESS; +} diff --git a/extension/src/inference/runtime/cache/llm_cache_policy.inc b/extension/src/inference/runtime/cache/llm_cache_policy.inc new file mode 100644 index 000000000..2c5fbd811 --- /dev/null +++ b/extension/src/inference/runtime/cache/llm_cache_policy.inc @@ -0,0 +1,500 @@ +/* + * LLM cache admission policy for native graph inference memory. + */ + +typedef struct _king_inference_llm_cache_policy_t { + bool enabled; + const char *path; + zend_long min_free_mb; + bool fail_closed; + const char *disk_alert_webhook; + const char *disk_alert_mcp_service; + const char *disk_alert_mcp_method; +} king_inference_llm_cache_policy_t; + +static const char *king_inference_llm_cache_default_path(void) +{ + return "/tmp/king-llm-cache"; +} + +static bool king_inference_llm_cache_cstr_non_empty(const char *value) +{ + return value != NULL && value[0] != '\0'; +} + +static zend_long king_inference_llm_cache_u64_to_long(uint64_t value) +{ + return value > (uint64_t) ZEND_LONG_MAX ? ZEND_LONG_MAX : (zend_long) value; +} + +static uint64_t king_inference_llm_cache_min_free_bytes(zend_long min_free_mb) +{ + if (min_free_mb <= 0) { + return 0; + } + if ((uint64_t) min_free_mb > UINT64_MAX / (1024ULL * 1024ULL)) { + return UINT64_MAX; + } + return (uint64_t) min_free_mb * 1024ULL * 1024ULL; +} + +static void king_inference_llm_cache_policy_from_compute( + const kg_high_perf_compute_ai_config_t *compute, + king_inference_llm_cache_policy_t *policy) +{ + policy->enabled = compute->inference_llm_cache_enable; + policy->path = king_inference_llm_cache_cstr_non_empty(compute->inference_llm_cache_path) + ? compute->inference_llm_cache_path + : king_inference_llm_cache_default_path(); + policy->min_free_mb = compute->inference_llm_cache_min_free_mb; + policy->fail_closed = compute->inference_llm_cache_fail_closed; + policy->disk_alert_webhook = compute->inference_llm_cache_disk_alert_webhook; + policy->disk_alert_mcp_service = compute->inference_llm_cache_disk_alert_mcp_service; + policy->disk_alert_mcp_method = compute->inference_llm_cache_disk_alert_mcp_method; +} + +static zend_result king_inference_llm_cache_apply_bool( + zval *source, + const char *field, + const char *qualified, + bool *target, + const char *function_name) +{ + zval *value = king_inference_array_find(source, field); + + if (value == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(value) != IS_TRUE && Z_TYPE_P(value) != IS_FALSE) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() %s must be a boolean when provided.", + function_name, + qualified + ); + return FAILURE; + } + + *target = Z_TYPE_P(value) == IS_TRUE; + return SUCCESS; +} + +static zend_result king_inference_llm_cache_apply_non_negative_long( + zval *source, + const char *field, + const char *qualified, + zend_long *target, + const char *function_name) +{ + zval *value = king_inference_array_find(source, field); + + if (value == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(value) != IS_LONG || Z_LVAL_P(value) < 0) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() %s must be a non-negative integer when provided.", + function_name, + qualified + ); + return FAILURE; + } + + *target = Z_LVAL_P(value); + return SUCCESS; +} + +static zend_result king_inference_llm_cache_apply_string( + zval *source, + const char *field, + const char *qualified, + const char **target, + bool allow_empty, + const char *function_name) +{ + zval *value = king_inference_array_find(source, field); + + if (value == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(value) != IS_STRING || (!allow_empty && Z_STRLEN_P(value) == 0)) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() %s must be a %sstring when provided.", + function_name, + qualified, + allow_empty ? "" : "non-empty " + ); + return FAILURE; + } + + *target = Z_STRVAL_P(value); + return SUCCESS; +} + +static zend_result king_inference_llm_cache_overlay( + king_inference_llm_cache_policy_t *policy, + zval *source, + const char *source_name, + const char *function_name) +{ + zval *cache = king_inference_array_find(source, "llm_cache"); + char qualified[96]; + + if (cache == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(cache) != IS_ARRAY) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() %s.llm_cache must be an array when provided.", + function_name, + source_name + ); + return FAILURE; + } + +#define KING_LLM_CACHE_QUAL(field) \ + snprintf(qualified, sizeof(qualified), "%s.llm_cache.%s", source_name, field) + + KING_LLM_CACHE_QUAL("enabled"); + if (king_inference_llm_cache_apply_bool( + cache, + "enabled", + qualified, + &policy->enabled, + function_name + ) != SUCCESS) { + return FAILURE; + } + + KING_LLM_CACHE_QUAL("path"); + if (king_inference_llm_cache_apply_string( + cache, + "path", + qualified, + &policy->path, + false, + function_name + ) != SUCCESS) { + return FAILURE; + } + + KING_LLM_CACHE_QUAL("min_free_mb"); + if (king_inference_llm_cache_apply_non_negative_long( + cache, + "min_free_mb", + qualified, + &policy->min_free_mb, + function_name + ) != SUCCESS) { + return FAILURE; + } + + KING_LLM_CACHE_QUAL("fail_closed"); + if (king_inference_llm_cache_apply_bool( + cache, + "fail_closed", + qualified, + &policy->fail_closed, + function_name + ) != SUCCESS) { + return FAILURE; + } + + KING_LLM_CACHE_QUAL("disk_alert_webhook"); + if (king_inference_llm_cache_apply_string( + cache, + "disk_alert_webhook", + qualified, + &policy->disk_alert_webhook, + true, + function_name + ) != SUCCESS) { + return FAILURE; + } + + KING_LLM_CACHE_QUAL("disk_alert_mcp_service"); + if (king_inference_llm_cache_apply_string( + cache, + "disk_alert_mcp_service", + qualified, + &policy->disk_alert_mcp_service, + true, + function_name + ) != SUCCESS) { + return FAILURE; + } + + KING_LLM_CACHE_QUAL("disk_alert_mcp_method"); + if (king_inference_llm_cache_apply_string( + cache, + "disk_alert_mcp_method", + qualified, + &policy->disk_alert_mcp_method, + true, + function_name + ) != SUCCESS) { + return FAILURE; + } + +#undef KING_LLM_CACHE_QUAL + + return SUCCESS; +} + +static zend_result king_inference_llm_cache_policy_from_stream( + king_inference_model_object *model, + king_inference_stream_object *stream, + zval *graph_options, + king_inference_llm_cache_policy_t *policy, + const char *function_name) +{ + king_inference_llm_cache_policy_from_compute(&king_high_perf_compute_ai_config, policy); + + if (king_inference_llm_cache_overlay(policy, &model->config, "model", function_name) != SUCCESS + || king_inference_llm_cache_overlay(policy, &stream->request, "request", function_name) != SUCCESS + || king_inference_llm_cache_overlay(policy, &stream->options, "options", function_name) != SUCCESS) { + return FAILURE; + } + if (graph_options != NULL + && king_inference_llm_cache_overlay(policy, graph_options, "graph_options", function_name) != SUCCESS) { + return FAILURE; + } + + return SUCCESS; +} + +static bool king_inference_llm_cache_has_webhook( + const king_inference_llm_cache_policy_t *policy) +{ + return king_inference_llm_cache_cstr_non_empty(policy->disk_alert_webhook); +} + +static bool king_inference_llm_cache_has_mcp_alert( + const king_inference_llm_cache_policy_t *policy) +{ + return king_inference_llm_cache_cstr_non_empty(policy->disk_alert_mcp_service) + && king_inference_llm_cache_cstr_non_empty(policy->disk_alert_mcp_method); +} + +static void king_inference_llm_cache_add_alert_config( + zval *target, + const king_inference_llm_cache_policy_t *policy, + bool alert_requested) +{ + zval alert; + + array_init(&alert); + add_assoc_bool(&alert, "requested", alert_requested); + add_assoc_bool(&alert, "webhook_configured", king_inference_llm_cache_has_webhook(policy)); + add_assoc_string( + &alert, + "webhook", + policy->disk_alert_webhook != NULL ? policy->disk_alert_webhook : "" + ); + add_assoc_bool(&alert, "mcp_configured", king_inference_llm_cache_has_mcp_alert(policy)); + add_assoc_string( + &alert, + "mcp_service", + policy->disk_alert_mcp_service != NULL ? policy->disk_alert_mcp_service : "" + ); + add_assoc_string( + &alert, + "mcp_method", + policy->disk_alert_mcp_method != NULL ? policy->disk_alert_mcp_method : "" + ); + add_assoc_zval(target, "alert", &alert); +} + +static zend_result king_inference_llm_cache_status_array( + const king_inference_llm_cache_policy_t *policy, + bool with_memory, + zval *return_value, + bool *ok_out) +{ + const char *path = king_inference_llm_cache_cstr_non_empty(policy->path) + ? policy->path + : king_inference_llm_cache_default_path(); + bool active = with_memory && policy->enabled; + bool ok = true; + bool alert_requested = false; + uint64_t min_free_bytes = king_inference_llm_cache_min_free_bytes(policy->min_free_mb); + + array_init(return_value); + add_assoc_string(return_value, "type", "llm_cache_status"); + add_assoc_bool(return_value, "enabled", policy->enabled); + add_assoc_bool(return_value, "active", active); + add_assoc_bool(return_value, "memory_required", true); + add_assoc_bool(return_value, "with_memory", with_memory); + add_assoc_string(return_value, "path", path); + add_assoc_long(return_value, "min_free_mb", policy->min_free_mb); + add_assoc_long(return_value, "min_free_bytes", king_inference_llm_cache_u64_to_long(min_free_bytes)); + add_assoc_bool(return_value, "fail_closed", policy->fail_closed); + + if (!with_memory) { + add_assoc_bool(return_value, "ok", true); + add_assoc_bool(return_value, "degraded", false); + add_assoc_string(return_value, "action", "disabled_without_memory"); + king_inference_llm_cache_add_alert_config(return_value, policy, false); + *ok_out = true; + return SUCCESS; + } + if (!policy->enabled) { + add_assoc_bool(return_value, "ok", true); + add_assoc_bool(return_value, "degraded", false); + add_assoc_string(return_value, "action", "disabled_by_config"); + king_inference_llm_cache_add_alert_config(return_value, policy, false); + *ok_out = true; + return SUCCESS; + } + + if (mkdir(path, 0775) != 0 && errno != EEXIST) { + ok = false; + add_assoc_string(return_value, "action", "cache_path_unavailable"); + add_assoc_string(return_value, "error", strerror(errno)); + } else { + struct stat st; + struct statvfs fs; + + errno = 0; + if (stat(path, &st) != 0) { + ok = false; + add_assoc_string(return_value, "action", "cache_path_unavailable"); + add_assoc_string(return_value, "error", strerror(errno)); + } else if (!S_ISDIR(st.st_mode)) { + ok = false; + add_assoc_string(return_value, "action", "cache_path_not_directory"); + add_assoc_string(return_value, "error", "configured LLM cache path is not a directory"); + } else if (statvfs(path, &fs) != 0) { + ok = false; + add_assoc_string(return_value, "action", "disk_status_unavailable"); + add_assoc_string(return_value, "error", strerror(errno)); + } else { + uint64_t free_bytes = (uint64_t) fs.f_bavail * (uint64_t) fs.f_frsize; + + add_assoc_long( + return_value, + "free_bytes", + king_inference_llm_cache_u64_to_long(free_bytes) + ); + add_assoc_long( + return_value, + "free_mb", + king_inference_llm_cache_u64_to_long(free_bytes / (1024ULL * 1024ULL)) + ); + ok = free_bytes >= min_free_bytes; + add_assoc_string(return_value, "action", ok ? "active" : "disk_floor_failed"); + } + } + + alert_requested = !ok + && (king_inference_llm_cache_has_webhook(policy) + || king_inference_llm_cache_has_mcp_alert(policy)); + add_assoc_bool(return_value, "ok", ok); + add_assoc_bool(return_value, "degraded", !ok); + king_inference_llm_cache_add_alert_config(return_value, policy, alert_requested); + *ok_out = ok; + return SUCCESS; +} + +static zend_result king_inference_llm_cache_prepare_stream( + king_inference_model_object *model, + king_inference_stream_object *stream, + zval *graph_options, + bool with_memory, + const char *function_name) +{ + king_inference_llm_cache_policy_t policy; + zval status; + bool ok; + + if (king_inference_llm_cache_policy_from_stream( + model, + stream, + graph_options, + &policy, + function_name + ) != SUCCESS) { + return FAILURE; + } + + if (!with_memory) { + return SUCCESS; + } + + ZVAL_UNDEF(&status); + if (king_inference_llm_cache_status_array(&policy, with_memory, &status, &ok) != SUCCESS) { + return FAILURE; + } + + if (!ok && policy.fail_closed) { + zval_ptr_dtor(&status); + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "%s() refused memory-enabled inference because the LLM cache disk floor was not satisfied.", + function_name + ); + return FAILURE; + } + + if (stream->openai_compatible) { + zval_ptr_dtor(&status); + return SUCCESS; + } + + add_next_index_zval(&stream->native_events, &status); + return SUCCESS; +} + +PHP_FUNCTION(king_inference_llm_cache_status) +{ + zval *zconfig = NULL; + zval *options = NULL; + const kg_high_perf_compute_ai_config_t *compute; + king_inference_llm_cache_policy_t policy; + bool with_memory; + bool ok; + + ZEND_PARSE_PARAMETERS_START(0, 2) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL_OR_NULL(zconfig) + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + compute = king_inference_runtime_compute_config(zconfig, "king_inference_llm_cache_status"); + if (compute == NULL) { + RETURN_THROWS(); + } + + king_inference_llm_cache_policy_from_compute(compute, &policy); + with_memory = compute->inference_with_memory; + if (options != NULL) { + zval *value = king_inference_king_native_find_memory_option(options); + + if (EG(exception)) { + RETURN_THROWS(); + } + if (value != NULL) { + if (Z_TYPE_P(value) != IS_TRUE && Z_TYPE_P(value) != IS_FALSE) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "king_inference_llm_cache_status() options.with_memory must be a boolean." + ); + RETURN_THROWS(); + } + with_memory = Z_TYPE_P(value) == IS_TRUE; + } + } + + if (king_inference_llm_cache_status_array(&policy, with_memory, return_value, &ok) != SUCCESS) { + RETURN_THROWS(); + } +} diff --git a/extension/src/inference/runtime/cache/paged_kv_cache.inc b/extension/src/inference/runtime/cache/paged_kv_cache.inc new file mode 100644 index 000000000..57ea67177 --- /dev/null +++ b/extension/src/inference/runtime/cache/paged_kv_cache.inc @@ -0,0 +1,183 @@ +#define KING_INFERENCE_DEFAULT_KV_PAGE_TOKENS 16 +#define KING_INFERENCE_DEFAULT_KV_ELEMENT_BYTES 2 + +static zval *king_inference_paged_kv_config(king_inference_model_object *model) +{ + zval *config = king_inference_array_find(&model->config, "paged_attention"); + + if (config != NULL && Z_TYPE_P(config) == IS_ARRAY) { + return config; + } + + config = king_inference_array_find(&model->config, "kv_cache"); + return config != NULL && Z_TYPE_P(config) == IS_ARRAY ? config : NULL; +} + +static zend_ulong king_inference_paged_kv_positive_long(zval *array, const char *key, zend_ulong fallback) +{ + zval *value = king_inference_array_find(array, key); + + return value != NULL && Z_TYPE_P(value) == IS_LONG && Z_LVAL_P(value) > 0 + ? (zend_ulong) Z_LVAL_P(value) + : fallback; +} + +static bool king_inference_paged_kv_mul(zend_ulong a, zend_ulong b, zend_ulong *result) +{ + if (a != 0 && b > ZEND_ULONG_MAX / a) { + *result = 0; + return false; + } + + *result = a * b; + return true; +} + +static bool king_inference_paged_kv_add(zend_ulong a, zend_ulong b, zend_ulong *result) +{ + if (b > ZEND_ULONG_MAX - a) { + *result = 0; + return false; + } + + *result = a + b; + return true; +} + +static bool king_inference_paged_kv_page_bytes( + zend_ulong page_tokens, + zend_ulong kv_heads, + zend_ulong key_dim, + zend_ulong value_dim, + zend_ulong element_bytes, + zend_ulong *page_bytes_per_layer +) { + zend_ulong key_units; + zend_ulong value_units; + + if (!king_inference_paged_kv_mul(page_tokens, kv_heads, &key_units) + || !king_inference_paged_kv_mul(key_units, key_dim, &key_units) + || !king_inference_paged_kv_mul(key_units, element_bytes, &key_units) + || !king_inference_paged_kv_mul(page_tokens, kv_heads, &value_units) + || !king_inference_paged_kv_mul(value_units, value_dim, &value_units) + || !king_inference_paged_kv_mul(value_units, element_bytes, &value_units) + || !king_inference_paged_kv_add(key_units, value_units, page_bytes_per_layer)) { + *page_bytes_per_layer = 0; + return false; + } + + return true; +} + +static void king_inference_paged_kv_missing_fields( + king_inference_model_object *model, + zend_ulong max_context_tokens, + zend_ulong key_dim, + zend_ulong value_dim, + zend_ulong kv_heads, + zval *return_value +) { + array_init(return_value); + if (max_context_tokens == 0) { + add_next_index_string(return_value, "context_length"); + } + if (model->gguf.architecture_params[KING_INFERENCE_ARCH_BLOCK_COUNT] == 0) { + add_next_index_string(return_value, "block_count"); + } + if (model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT] == 0) { + add_next_index_string(return_value, "attention_head_count"); + } + if (kv_heads == 0) { + add_next_index_string(return_value, "attention_head_count_kv"); + } + if (key_dim == 0) { + add_next_index_string(return_value, "attention_key_length"); + } + if (value_dim == 0) { + add_next_index_string(return_value, "attention_value_length"); + } +} + +static void king_inference_paged_kv_cache_plan( + king_inference_model_object *model, + zval *request, + zval *return_value +) { + zval *config = king_inference_paged_kv_config(model); + zend_ulong page_tokens = king_inference_paged_kv_positive_long(config, "page_tokens", KING_INFERENCE_DEFAULT_KV_PAGE_TOKENS); + zend_ulong element_bytes = king_inference_paged_kv_positive_long(config, "element_bytes", KING_INFERENCE_DEFAULT_KV_ELEMENT_BYTES); + zend_ulong max_context_tokens = king_inference_paged_kv_positive_long( + config, + "max_context_tokens", + model->gguf.architecture_params[KING_INFERENCE_ARCH_CONTEXT_LENGTH] + ); + zend_ulong kv_heads = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT_KV] > 0 + ? model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT_KV] + : model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT]; + zend_ulong attention_heads = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT]; + zend_ulong key_dim = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_KEY_LENGTH]; + zend_ulong value_dim = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_VALUE_LENGTH]; + zend_ulong pages_per_sequence; + zend_ulong page_bytes_per_layer = 0; + zend_ulong page_bytes_all_layers = 0; + zend_ulong sequence_bytes = 0; + bool enabled = king_inference_bool_from_array(config, "enabled", true); + bool bytes_fit; + zval missing; + + if (key_dim == 0 + && model->gguf.architecture_params[KING_INFERENCE_ARCH_EMBEDDING_LENGTH] > 0 + && model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT] > 0 + && model->gguf.architecture_params[KING_INFERENCE_ARCH_EMBEDDING_LENGTH] + % model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT] == 0) { + key_dim = model->gguf.architecture_params[KING_INFERENCE_ARCH_EMBEDDING_LENGTH] + / model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT]; + } + if (value_dim == 0) { + value_dim = key_dim; + } + king_inference_attention_refine_runtime_cache_shape_from_tensors( + model, + model->gguf.architecture_params[KING_INFERENCE_ARCH_BLOCK_COUNT], + &attention_heads, + &kv_heads, + &key_dim, + &value_dim + ); + if (request != NULL && Z_TYPE_P(request) == IS_ARRAY) { + max_context_tokens = king_inference_paged_kv_positive_long(request, "max_context_tokens", max_context_tokens); + } + + pages_per_sequence = page_tokens > 0 && max_context_tokens > 0 + ? (max_context_tokens + page_tokens - 1) / page_tokens + : 0; + bytes_fit = king_inference_paged_kv_page_bytes(page_tokens, kv_heads, key_dim, value_dim, element_bytes, &page_bytes_per_layer) + && king_inference_paged_kv_mul( + page_bytes_per_layer, + model->gguf.architecture_params[KING_INFERENCE_ARCH_BLOCK_COUNT], + &page_bytes_all_layers + ) + && king_inference_paged_kv_mul(page_bytes_all_layers, pages_per_sequence, &sequence_bytes); + + array_init(return_value); + add_assoc_bool(return_value, "enabled", enabled); + add_assoc_bool(return_value, "ready", enabled && bytes_fit && pages_per_sequence > 0 && page_bytes_all_layers > 0); + add_assoc_string(return_value, "layout", "paged_kv_cache"); + add_assoc_long(return_value, "page_tokens", (zend_long) page_tokens); + add_assoc_long(return_value, "max_context_tokens", (zend_long) max_context_tokens); + add_assoc_long(return_value, "pages_per_sequence", (zend_long) pages_per_sequence); + add_assoc_long(return_value, "layers", (zend_long) model->gguf.architecture_params[KING_INFERENCE_ARCH_BLOCK_COUNT]); + add_assoc_long(return_value, "attention_heads", (zend_long) attention_heads); + add_assoc_long(return_value, "kv_heads", (zend_long) kv_heads); + add_assoc_long(return_value, "key_dim", (zend_long) key_dim); + add_assoc_long(return_value, "value_dim", (zend_long) value_dim); + add_assoc_long(return_value, "element_bytes", (zend_long) element_bytes); + add_assoc_long(return_value, "page_bytes_per_layer", (zend_long) page_bytes_per_layer); + add_assoc_long(return_value, "page_bytes_all_layers", (zend_long) page_bytes_all_layers); + add_assoc_long(return_value, "max_sequence_bytes", (zend_long) sequence_bytes); + add_assoc_bool(return_value, "block_table_ready", pages_per_sequence > 0); + add_assoc_bool(return_value, "copy_on_write_ready", false); + add_assoc_bool(return_value, "overflow_safe", bytes_fit); + king_inference_paged_kv_missing_fields(model, max_context_tokens, key_dim, value_dim, kv_heads, &missing); + add_assoc_zval(return_value, "missing_fields", &missing); +} diff --git a/extension/src/inference/runtime/config/model_config.inc b/extension/src/inference/runtime/config/model_config.inc new file mode 100644 index 000000000..c4f3f92db --- /dev/null +++ b/extension/src/inference/runtime/config/model_config.inc @@ -0,0 +1,481 @@ +static zend_result king_inference_config_path_string( + zval *value, + const char *field_name, + const char *function_name, + zend_string **path_out +) { + *path_out = NULL; + + if (value == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(value) != IS_STRING || Z_STRLEN_P(value) == 0) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() %s must be a non-empty string filesystem path.", + function_name, + field_name + ); + return FAILURE; + } + + *path_out = zend_string_copy(Z_STR_P(value)); + return SUCCESS; +} + +static zend_result king_inference_validate_optional_string_config( + zval *config, + const char *field_name, + const char *function_name +) { + zval *value = king_inference_array_find(config, field_name); + + if (value == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(value) != IS_STRING || Z_STRLEN_P(value) == 0) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() %s must be a non-empty string when provided.", + function_name, + field_name + ); + return FAILURE; + } + + return SUCCESS; +} + +static zend_result king_inference_validate_optional_positive_long_config( + zval *config, + const char *field_name, + const char *function_name +) { + zval *value = king_inference_array_find(config, field_name); + + if (value == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(value) != IS_LONG || Z_LVAL_P(value) <= 0) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() %s must be a positive integer when provided.", + function_name, + field_name + ); + return FAILURE; + } + + return SUCCESS; +} + +static zend_result king_inference_validate_optional_bool_config( + zval *config, + const char *field_name, + const char *function_name +) { + zval *value = king_inference_array_find(config, field_name); + + if (value == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(value) != IS_TRUE && Z_TYPE_P(value) != IS_FALSE) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() %s must be a boolean when provided.", + function_name, + field_name + ); + return FAILURE; + } + + return SUCCESS; +} + +static zend_result king_inference_validate_optional_bool_alias_config( + zval *config, + const char *field_name, + const char *alias_name, + const char *function_name +) { + zval *value = king_inference_array_find(config, field_name); + zval *alias = king_inference_array_find(config, alias_name); + + if (value != NULL && alias != NULL) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() memory config must use either %s or %s, not both.", + function_name, + field_name, + alias_name + ); + return FAILURE; + } + + if (king_inference_validate_optional_bool_config(config, field_name, function_name) != SUCCESS + || king_inference_validate_optional_bool_config(config, alias_name, function_name) != SUCCESS) { + return FAILURE; + } + + return SUCCESS; +} + +static zend_result king_inference_validate_model_config(zval *config, const char *function_name) +{ + if (king_inference_validate_optional_string_config(config, "name", function_name) != SUCCESS + || king_inference_validate_optional_string_config(config, "quantization", function_name) != SUCCESS + || king_inference_validate_optional_string_config(config, "owned_by", function_name) != SUCCESS + || king_inference_validate_optional_string_config(config, "embedding_tensor", function_name) != SUCCESS + || king_inference_validate_optional_string_config(config, "token_embedding_tensor", function_name) != SUCCESS + || king_inference_validate_optional_string_config(config, "output_tensor", function_name) != SUCCESS + || king_inference_validate_optional_string_config(config, "output_projection_tensor", function_name) != SUCCESS + || king_inference_validate_optional_string_config(config, "lm_head_tensor", function_name) != SUCCESS + || king_inference_validate_optional_string_config(config, "attention_query_tensor_pattern", function_name) != SUCCESS + || king_inference_validate_optional_string_config(config, "attention_key_tensor_pattern", function_name) != SUCCESS + || king_inference_validate_optional_string_config(config, "attention_value_tensor_pattern", function_name) != SUCCESS + || king_inference_validate_optional_string_config(config, "attention_output_tensor_pattern", function_name) != SUCCESS + || king_inference_validate_optional_string_config(config, "rms_norm_attention_tensor_pattern", function_name) != SUCCESS + || king_inference_validate_optional_string_config(config, "rms_norm_ffn_tensor_pattern", function_name) != SUCCESS + || king_inference_validate_optional_string_config(config, "rms_norm_final_tensor", function_name) != SUCCESS + || king_inference_validate_optional_string_config(config, "ffn_gate_tensor_pattern", function_name) != SUCCESS + || king_inference_validate_optional_string_config(config, "ffn_up_tensor_pattern", function_name) != SUCCESS + || king_inference_validate_optional_string_config(config, "ffn_down_tensor_pattern", function_name) != SUCCESS + || king_inference_validate_optional_positive_long_config(config, "context_tokens", function_name) != SUCCESS + || king_inference_validate_optional_bool_alias_config(config, "with_memory", "with-memory", function_name) != SUCCESS + || king_inference_validate_gpu_config(config, function_name) != SUCCESS) { + return FAILURE; + } + + return SUCCESS; +} + +static zend_result king_inference_resolve_artifact_path( + zval *config, + const char *function_name, + zend_string **path_out +) +{ + zval *artifact = king_inference_array_find(config, "artifact"); + zval *path; + + *path_out = NULL; + + if (artifact != NULL) { + if (Z_TYPE_P(artifact) == IS_STRING) { + return king_inference_config_path_string( + artifact, + "artifact", + function_name, + path_out + ); + } + + if (Z_TYPE_P(artifact) != IS_ARRAY) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() artifact must be a non-empty string path or an array with path.", + function_name + ); + return FAILURE; + } + } + + if (artifact != NULL) { + path = king_inference_array_find(artifact, "path"); + if (path != NULL) { + return king_inference_config_path_string( + path, + "artifact.path", + function_name, + path_out + ); + } + + if (king_inference_array_find(artifact, "store") != NULL + || king_inference_array_find(artifact, "key") != NULL) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "King inference requires a materialized local GGUF model artifact path." + ); + return FAILURE; + } + + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() artifact array requires a non-empty path entry.", + function_name + ); + return FAILURE; + } + + path = king_inference_array_find(config, "artifact_path"); + if (path != NULL) { + return king_inference_config_path_string( + path, + "artifact_path", + function_name, + path_out + ); + } + + return SUCCESS; +} + +static zend_result king_inference_model_init(zval *target, zval *config, const char *function_name) +{ + zend_string *artifact_path = NULL; + king_inference_gguf_metadata metadata; + king_inference_backend_kind backend_kind; + void *native_map = NULL; + size_t native_map_size = 0; + zval tensor_index; + zval tokenizer_tokens; + zval tokenizer_lookup; + zval tokenizer_scores; + zval tokenizer_types; + zval tokenizer_merges; + zval *name_value; + king_inference_model_object *intern; + + if (config == NULL || Z_TYPE_P(config) != IS_ARRAY) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "%s() requires a model config array.", function_name); + return FAILURE; + } + + if (king_inference_validate_model_config(config, function_name) != SUCCESS) { + return FAILURE; + } + + if (king_inference_resolve_artifact_path(config, function_name, &artifact_path) != SUCCESS) { + return FAILURE; + } + if (artifact_path == NULL) { + if (!EG(exception)) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "%s() requires artifact or artifact_path.", function_name); + } + return FAILURE; + } + + ZVAL_UNDEF(&tensor_index); + array_init(&tensor_index); + ZVAL_UNDEF(&tokenizer_tokens); + array_init(&tokenizer_tokens); + ZVAL_UNDEF(&tokenizer_lookup); + array_init(&tokenizer_lookup); + ZVAL_UNDEF(&tokenizer_scores); + array_init(&tokenizer_scores); + ZVAL_UNDEF(&tokenizer_types); + array_init(&tokenizer_types); + ZVAL_UNDEF(&tokenizer_merges); + array_init(&tokenizer_merges); + + if (king_inference_load_gguf_metadata( + artifact_path, + &metadata, + &tensor_index, + &tokenizer_tokens, + &tokenizer_lookup, + &tokenizer_scores, + &tokenizer_types, + &tokenizer_merges + ) != SUCCESS) { + zval_ptr_dtor(&tokenizer_merges); + zval_ptr_dtor(&tokenizer_types); + zval_ptr_dtor(&tokenizer_scores); + zval_ptr_dtor(&tokenizer_lookup); + zval_ptr_dtor(&tokenizer_tokens); + zval_ptr_dtor(&tensor_index); + zend_string_release(artifact_path); + return FAILURE; + } + if (king_inference_backend_kind_from_config(config, &backend_kind, function_name) != SUCCESS + || king_inference_backend_validate_model_config(config, function_name) != SUCCESS) { + zval_ptr_dtor(&tokenizer_merges); + zval_ptr_dtor(&tokenizer_types); + zval_ptr_dtor(&tokenizer_scores); + zval_ptr_dtor(&tokenizer_lookup); + zval_ptr_dtor(&tokenizer_tokens); + zval_ptr_dtor(&tensor_index); + king_inference_gguf_metadata_release(&metadata); + zend_string_release(artifact_path); + return FAILURE; + } + if (king_inference_check_gpu_policy(config, function_name) != SUCCESS) { + zval_ptr_dtor(&tokenizer_merges); + zval_ptr_dtor(&tokenizer_types); + zval_ptr_dtor(&tokenizer_scores); + zval_ptr_dtor(&tokenizer_lookup); + zval_ptr_dtor(&tokenizer_tokens); + zval_ptr_dtor(&tensor_index); + king_inference_gguf_metadata_release(&metadata); + zend_string_release(artifact_path); + return FAILURE; + } + if ((backend_kind == KING_INFERENCE_BACKEND_KING_NATIVE_CPU + || backend_kind == KING_INFERENCE_BACKEND_KING_NATIVE_GPU) + && king_inference_native_map_artifact( + artifact_path, + metadata.file_size, + &native_map, + &native_map_size + ) != SUCCESS) { + zval_ptr_dtor(&tokenizer_merges); + zval_ptr_dtor(&tokenizer_types); + zval_ptr_dtor(&tokenizer_scores); + zval_ptr_dtor(&tokenizer_lookup); + zval_ptr_dtor(&tokenizer_tokens); + zval_ptr_dtor(&tensor_index); + king_inference_gguf_metadata_release(&metadata); + zend_string_release(artifact_path); + return FAILURE; + } + + intern = php_king_inference_model_obj_from_zend(Z_OBJ_P(target)); + zval_ptr_dtor(&intern->config); + ZVAL_COPY(&intern->config, config); + zval_ptr_dtor(&intern->tensor_index); + ZVAL_COPY_VALUE(&intern->tensor_index, &tensor_index); + zval_ptr_dtor(&intern->tokenizer_tokens); + ZVAL_COPY_VALUE(&intern->tokenizer_tokens, &tokenizer_tokens); + zval_ptr_dtor(&intern->tokenizer_lookup); + ZVAL_COPY_VALUE(&intern->tokenizer_lookup, &tokenizer_lookup); + zval_ptr_dtor(&intern->tokenizer_scores); + ZVAL_COPY_VALUE(&intern->tokenizer_scores, &tokenizer_scores); + zval_ptr_dtor(&intern->tokenizer_types); + ZVAL_COPY_VALUE(&intern->tokenizer_types, &tokenizer_types); + zval_ptr_dtor(&intern->tokenizer_merges); + ZVAL_COPY_VALUE(&intern->tokenizer_merges, &tokenizer_merges); + zval_ptr_dtor(&intern->paged_kv_cache_plan); + ZVAL_UNDEF(&intern->paged_kv_cache_plan); + + if (intern->artifact_path != NULL) { + zend_string_release(intern->artifact_path); + } + king_inference_cuda_logits_readback_release(intern); + king_inference_cuda_decoder_prompt_loop_release(intern); + king_inference_cuda_decoder_graph_executor_release(intern); + king_inference_cuda_output_projection_release(intern); + king_inference_cuda_ffn_swiglu_release(intern); + king_inference_cuda_attention_values_release(intern); + king_inference_cuda_attention_softmax_release(intern); + king_inference_cuda_attention_scores_release(intern); + king_inference_cuda_rope_release(intern); + king_inference_cuda_rms_norm_release(intern); + king_inference_cuda_device_kv_cache_release(intern); + king_inference_cuda_device_vector_ops_release(intern); + king_inference_cuda_embedding_row_release(intern); + king_inference_cuda_quantized_matvec_release(intern); + king_inference_cuda_weight_upload_release(intern); + king_inference_cuda_device_allocator_release(intern); + king_inference_cuda_context_release(intern); + king_inference_native_mapping_release(intern); + king_inference_gguf_metadata_release(&intern->gguf); + intern->artifact_path = artifact_path; + intern->gguf = metadata; + intern->native_map = native_map; + intern->native_map_size = native_map_size; + intern->native_map_loaded = native_map != NULL; + + if (intern->name != NULL) { + zend_string_release(intern->name); + intern->name = NULL; + } + name_value = king_inference_array_find(config, "name"); + intern->name = name_value != NULL && Z_TYPE_P(name_value) == IS_STRING && Z_STRLEN_P(name_value) > 0 + ? zend_string_copy(Z_STR_P(name_value)) + : zend_string_init("local-quantized-model", sizeof("local-quantized-model") - 1, 0); + king_inference_paged_kv_cache_plan(intern, NULL, &intern->paged_kv_cache_plan); + king_inference_cuda_context_open(intern, backend_kind); + king_inference_cuda_device_allocator_init(intern, backend_kind); + king_inference_cuda_weight_upload_required(intern, backend_kind); + king_inference_cuda_quantized_matvec_init(intern, backend_kind); + king_inference_cuda_embedding_row_init(intern, backend_kind); + king_inference_cuda_device_vector_ops_init(intern, backend_kind); + king_inference_cuda_device_kv_cache_init(intern, backend_kind); + king_inference_cuda_rms_norm_init(intern, backend_kind); + king_inference_cuda_rope_init(intern, backend_kind); + king_inference_cuda_attention_scores_init(intern, backend_kind); + king_inference_cuda_attention_softmax_init(intern, backend_kind); + king_inference_cuda_attention_values_init(intern, backend_kind); + king_inference_cuda_ffn_swiglu_init(intern, backend_kind); + king_inference_cuda_output_projection_init(intern, backend_kind); + king_inference_cuda_logits_readback_init(intern, backend_kind); + king_inference_cuda_decoder_graph_executor_init(intern, backend_kind); + king_inference_cuda_decoder_prompt_loop_init(intern, backend_kind); + + king_set_error(""); + return SUCCESS; +} + +static void king_inference_model_info_array(king_inference_model_object *intern, zval *return_value) +{ + zval *quantization = king_inference_array_find(&intern->config, "quantization"); + zval runtime_truth; + zval native_engine_readiness; + zval tokenizer_status; + + array_init(return_value); + add_assoc_str(return_value, "name", zend_string_copy(intern->name)); + add_assoc_str(return_value, "artifact_path", zend_string_copy(intern->artifact_path)); + add_assoc_string(return_value, "format", "gguf"); + add_assoc_long(return_value, "artifact_bytes", (zend_long) intern->gguf.file_size); + add_assoc_long( + return_value, + "native_tensor_index_count", + (zend_long) zend_hash_num_elements(Z_ARRVAL(intern->tensor_index)) + ); + add_assoc_long( + return_value, + "native_tokenizer_token_count", + (zend_long) zend_hash_num_elements(Z_ARRVAL(intern->tokenizer_tokens)) + ); + add_assoc_long( + return_value, + "native_tokenizer_score_count", + (zend_long) zend_hash_num_elements(Z_ARRVAL(intern->tokenizer_scores)) + ); + add_assoc_long( + return_value, + "native_tokenizer_type_count", + (zend_long) zend_hash_num_elements(Z_ARRVAL(intern->tokenizer_types)) + ); + add_assoc_long( + return_value, + "native_tokenizer_merge_count", + (zend_long) zend_hash_num_elements(Z_ARRVAL(intern->tokenizer_merges)) + ); + king_inference_tokenizer_status_array(intern, &tokenizer_status); + add_assoc_zval(return_value, "tokenizer_status", &tokenizer_status); + king_inference_add_gguf_metadata_array(&intern->gguf, return_value); + king_inference_add_tensor_resolution_info(intern, return_value); + if (!Z_ISUNDEF(intern->paged_kv_cache_plan)) { + zval plan; + ZVAL_COPY(&plan, &intern->paged_kv_cache_plan); + add_assoc_zval(return_value, "paged_kv_cache", &plan); + } + add_assoc_bool(return_value, "gpu_enabled", king_inference_gpu_enabled(&intern->config)); + if (king_inference_gpu_enabled(&intern->config)) { + zval gpu_runtime; + + king_inference_gpu_runtime_status_from_model(intern, &gpu_runtime); + add_assoc_zval(return_value, "gpu_runtime", &gpu_runtime); + } + king_inference_backend_model_info(intern, return_value); + king_inference_model_native_engine_readiness_array(intern, &native_engine_readiness); + add_assoc_zval(return_value, "native_engine_readiness", &native_engine_readiness); + king_inference_model_runtime_truth_array(intern, &runtime_truth); + king_inference_model_add_runtime_surface_fields(intern, return_value, &runtime_truth); + add_assoc_zval(return_value, "runtime_truth", &runtime_truth); + + if (quantization != NULL && Z_TYPE_P(quantization) == IS_STRING) { + add_assoc_str(return_value, "quantization", zend_string_copy(Z_STR_P(quantization))); + } +} diff --git a/extension/src/inference/runtime/config/runtime_model_registry.inc b/extension/src/inference/runtime/config/runtime_model_registry.inc new file mode 100644 index 000000000..d3bbb2d88 --- /dev/null +++ b/extension/src/inference/runtime/config/runtime_model_registry.inc @@ -0,0 +1,422 @@ +/* + * Runtime model registry config for OpenAI-compatible model listing. + */ + +static bool king_inference_runtime_registry_non_empty(const char *value) +{ + return value != NULL && value[0] != '\0'; +} + +static char *king_inference_runtime_registry_trim(char *value) +{ + char *end; + + while (*value != '\0' && isspace((unsigned char) *value)) { + value++; + } + + end = value + strlen(value); + while (end > value && isspace((unsigned char) *(end - 1))) { + end--; + } + *end = '\0'; + return value; +} + +static char *king_inference_runtime_registry_next_token(char **cursor, const char *delimiters) +{ + char *token; + char *separator; + + if (cursor == NULL || *cursor == NULL) { + return NULL; + } + + token = *cursor; + separator = strpbrk(token, delimiters); + if (separator == NULL) { + *cursor = NULL; + return token; + } + + *separator = '\0'; + *cursor = separator + 1; + return token; +} + +static const char *king_inference_runtime_registry_backend_name( + const char *backend, + const kg_high_perf_compute_ai_config_t *compute +) { + const char *profile; + + if (backend != NULL && backend[0] != '\0') { + if (strcasecmp(backend, "cpu") == 0 || strcasecmp(backend, "king_native_cpu") == 0) { + return "king_native_cpu"; + } + if (strcasecmp(backend, "gpu") == 0 + || strcasecmp(backend, "cuda") == 0 + || strcasecmp(backend, "king_native_gpu") == 0) { + return "king_native_gpu"; + } + if (strcasecmp(backend, "local") == 0) { + return "local"; + } + return NULL; + } + + profile = king_inference_runtime_non_empty(compute->inference_preferred_model_profile) + ? compute->inference_preferred_model_profile + : "auto"; + return strcasecmp(profile, "gpu") == 0 ? "king_native_gpu" : "king_native_cpu"; +} + +static char *king_inference_runtime_registry_name_from_artifact(char *artifact_path) +{ + char *name; + + name = strrchr(artifact_path, '/'); + name = name != NULL ? name + 1 : artifact_path; + + return name[0] != '\0' ? name : artifact_path; +} + +static void king_inference_runtime_registry_add_aliases( + zval *entry, + const char *aliases +) { + zval alias_array; + char *copy; + char *part; + char *cursor; + + array_init(&alias_array); + if (aliases != NULL && aliases[0] != '\0') { + copy = estrdup(aliases); + cursor = copy; + while ((part = king_inference_runtime_registry_next_token(&cursor, "+,")) != NULL) { + part = king_inference_runtime_registry_trim(part); + if (part[0] != '\0') { + add_next_index_string(&alias_array, part); + } + } + efree(copy); + } + add_assoc_zval(entry, "aliases", &alias_array); +} + +static void king_inference_runtime_registry_add_shared_config( + const kg_high_perf_compute_ai_config_t *compute, + zval *entry +) { + zval artifact; + zval kv_cache; + zval llm_cache; + + add_assoc_string(entry, "owned_by", "king-runtime"); + add_assoc_long(entry, "context_tokens", compute->inference_context_tokens); + add_assoc_bool(entry, "with_memory", compute->inference_with_memory); + + array_init(&kv_cache); + add_assoc_long(&kv_cache, "max_context_tokens", compute->inference_context_tokens); + add_assoc_long(&kv_cache, "page_tokens", compute->inference_kv_page_tokens); + add_assoc_long(&kv_cache, "element_bytes", compute->inference_kv_element_bytes); + add_assoc_zval(entry, "kv_cache", &kv_cache); + + array_init(&llm_cache); + add_assoc_bool(&llm_cache, "enabled", compute->inference_llm_cache_enable); + add_assoc_string( + &llm_cache, + "path", + king_inference_runtime_non_empty(compute->inference_llm_cache_path) + ? compute->inference_llm_cache_path + : "/tmp/king-llm-cache" + ); + add_assoc_long(&llm_cache, "min_free_mb", compute->inference_llm_cache_min_free_mb); + add_assoc_bool(&llm_cache, "fail_closed", compute->inference_llm_cache_fail_closed); + add_assoc_string( + &llm_cache, + "disk_alert_webhook", + compute->inference_llm_cache_disk_alert_webhook != NULL + ? compute->inference_llm_cache_disk_alert_webhook + : "" + ); + add_assoc_string( + &llm_cache, + "disk_alert_mcp_service", + compute->inference_llm_cache_disk_alert_mcp_service != NULL + ? compute->inference_llm_cache_disk_alert_mcp_service + : "" + ); + add_assoc_string( + &llm_cache, + "disk_alert_mcp_method", + compute->inference_llm_cache_disk_alert_mcp_method != NULL + ? compute->inference_llm_cache_disk_alert_mcp_method + : "" + ); + add_assoc_zval(entry, "llm_cache", &llm_cache); + + array_init(&artifact); + add_assoc_zval(entry, "artifact", &artifact); +} + +static void king_inference_runtime_registry_set_artifact(zval *entry, const char *artifact_path) +{ + zval *artifact = king_inference_array_find(entry, "artifact"); + + if (artifact != NULL && Z_TYPE_P(artifact) == IS_ARRAY) { + add_assoc_string(artifact, "path", artifact_path); + return; + } + + add_assoc_string(entry, "artifact_path", artifact_path); +} + +static void king_inference_runtime_registry_add_gpu_config( + const kg_high_perf_compute_ai_config_t *compute, + zval *entry +) { + zval gpu; + zval thermal; + zval power; + + array_init(&gpu); + add_assoc_bool(&gpu, "enabled", true); + add_assoc_long(&gpu, "max_gpu_layers", compute->inference_gpu_max_gpu_layers); + add_assoc_long(&gpu, "vram_reserve_mb", compute->inference_gpu_vram_reserve_mb); + add_assoc_long(&gpu, "min_free_vram_mb", compute->inference_gpu_min_free_vram_mb); + add_assoc_bool(&gpu, "allow_system_ram_offload", compute->inference_gpu_allow_system_ram_offload); + add_assoc_long(&gpu, "system_ram_offload_max_mb", compute->inference_gpu_system_ram_offload_max_mb); + add_assoc_long(&gpu, "system_ram_offload_min_free_mb", compute->inference_gpu_system_ram_offload_min_free_mb); + add_assoc_bool(&gpu, "batch_prefill_experimental_enable", compute->inference_gpu_batch_prefill_experimental_enable); + + array_init(&thermal); + if (king_inference_runtime_non_empty(compute->inference_gpu_thermal_sensor_path)) { + add_assoc_string(&thermal, "sensor_path", compute->inference_gpu_thermal_sensor_path); + } + if (king_inference_runtime_non_empty(compute->inference_gpu_thermal_sensor_command)) { + add_assoc_string(&thermal, "sensor_command", compute->inference_gpu_thermal_sensor_command); + } + add_assoc_double(&thermal, "max_temperature_c", compute->inference_gpu_thermal_max_temperature_c); + add_assoc_long(&thermal, "check_interval_seconds", compute->inference_gpu_thermal_check_interval_sec); + add_assoc_bool(&thermal, "allow_unmonitored_gpu", compute->inference_gpu_allow_unmonitored); + add_assoc_zval(&gpu, "thermal", &thermal); + + array_init(&power); + if (king_inference_runtime_non_empty(compute->inference_gpu_power_sensor_command)) { + add_assoc_string(&power, "sensor_command", compute->inference_gpu_power_sensor_command); + } + add_assoc_double(&power, "max_watts", compute->inference_gpu_power_max_watts); + add_assoc_long(&power, "check_interval_seconds", compute->inference_gpu_power_check_interval_sec); + add_assoc_zval(&gpu, "power", &power); + + add_assoc_zval(entry, "gpu", &gpu); +} + +static zend_result king_inference_runtime_registry_add_entry( + zval *registry, + const kg_high_perf_compute_ai_config_t *compute, + const char *name, + const char *backend, + const char *artifact_path, + const char *aliases, + const char *function_name +) { + const char *backend_name = king_inference_runtime_registry_backend_name(backend, compute); + zval entry; + + if (name == NULL || name[0] == '\0' || artifact_path == NULL || artifact_path[0] == '\0') { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() model registry entries require non-empty name and artifact path.", + function_name + ); + return FAILURE; + } + if (backend_name == NULL) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() model registry backend must be cpu, gpu, king_native_cpu, king_native_gpu, or local.", + function_name + ); + return FAILURE; + } + if (zend_hash_str_exists(Z_ARRVAL_P(registry), name, strlen(name))) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() model registry contains duplicate model id '%s'.", + function_name, + name + ); + return FAILURE; + } + + array_init(&entry); + add_assoc_string(&entry, "id", name); + add_assoc_string(&entry, "name", name); + add_assoc_string(&entry, "backend", backend_name); + add_assoc_string(&entry, "registry_source", "king.inference_models"); + king_inference_runtime_registry_add_shared_config(compute, &entry); + king_inference_runtime_registry_set_artifact(&entry, artifact_path); + king_inference_runtime_registry_add_aliases(&entry, aliases); + if (strcmp(backend_name, "king_native_gpu") == 0) { + king_inference_runtime_registry_add_gpu_config(compute, &entry); + } + + add_assoc_zval_ex(registry, name, strlen(name), &entry); + return SUCCESS; +} + +static zend_result king_inference_runtime_registry_parse( + const kg_high_perf_compute_ai_config_t *compute, + zval *registry, + const char *function_name +) { + char *copy; + char *cursor; + char *raw_entry; + + copy = estrdup(compute->inference_models); + cursor = copy; + while ((raw_entry = king_inference_runtime_registry_next_token(&cursor, "@\n")) != NULL) { + char *fields[4] = {NULL, NULL, NULL, NULL}; + char *field_cursor; + char *part; + char *entry; + const char *field_delimiters; + int field_count = 0; + const char *name; + const char *backend; + const char *artifact; + const char *aliases; + + entry = king_inference_runtime_registry_trim(raw_entry); + if (entry[0] == '\0') { + continue; + } + + field_delimiters = strchr(entry, '|') != NULL ? "|" : ","; + field_cursor = entry; + while (field_count < 4 + && (part = king_inference_runtime_registry_next_token(&field_cursor, field_delimiters)) != NULL) { + fields[field_count++] = king_inference_runtime_registry_trim(part); + } + if (field_cursor != NULL && field_cursor[0] != '\0') { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() model registry entries use id,backend,artifact,alias1+alias2.", + function_name + ); + efree(copy); + return FAILURE; + } + + if (field_count == 1) { + name = king_inference_runtime_registry_name_from_artifact(fields[0]); + backend = ""; + artifact = fields[0]; + aliases = ""; + } else if (field_count == 2) { + name = fields[0]; + backend = ""; + artifact = fields[1]; + aliases = ""; + } else { + name = fields[0]; + backend = fields[1]; + artifact = fields[2]; + aliases = field_count >= 4 ? fields[3] : ""; + } + + if (king_inference_runtime_registry_add_entry( + registry, + compute, + name, + backend, + artifact, + aliases, + function_name + ) != SUCCESS) { + efree(copy); + return FAILURE; + } + } + efree(copy); + + return zend_hash_num_elements(Z_ARRVAL_P(registry)) > 0 ? SUCCESS : FAILURE; +} + +static zend_result king_inference_runtime_model_registry_config_array( + const kg_high_perf_compute_ai_config_t *compute, + zval *return_value, + const char *function_name +) { + zval selected; + zval *name; + + array_init(return_value); + if (king_inference_runtime_registry_non_empty(compute->inference_models)) { + if (king_inference_runtime_registry_parse(compute, return_value, function_name) != SUCCESS) { + if (!EG(exception)) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() king.inference_models did not contain any valid model entries.", + function_name + ); + } + zval_ptr_dtor(return_value); + return FAILURE; + } + return SUCCESS; + } + + ZVAL_UNDEF(&selected); + if (king_inference_runtime_model_config_array(compute, &selected, function_name) != SUCCESS) { + zval_ptr_dtor(return_value); + return FAILURE; + } + add_assoc_string(&selected, "registry_source", "selected_runtime_profile"); + name = king_inference_array_find(&selected, "name"); + add_assoc_zval_ex( + return_value, + name != NULL && Z_TYPE_P(name) == IS_STRING && Z_STRLEN_P(name) > 0 ? Z_STRVAL_P(name) : "king-runtime", + name != NULL && Z_TYPE_P(name) == IS_STRING && Z_STRLEN_P(name) > 0 + ? Z_STRLEN_P(name) + : sizeof("king-runtime") - 1, + &selected + ); + return SUCCESS; +} + +PHP_FUNCTION(king_inference_runtime_model_registry_config) +{ + zval *zconfig = NULL; + const kg_high_perf_compute_ai_config_t *compute; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL_OR_NULL(zconfig) + ZEND_PARSE_PARAMETERS_END(); + + compute = king_inference_runtime_compute_config( + zconfig, + "king_inference_runtime_model_registry_config" + ); + if (compute == NULL) { + RETURN_THROWS(); + } + + if (king_inference_runtime_model_registry_config_array( + compute, + return_value, + "king_inference_runtime_model_registry_config" + ) != SUCCESS) { + RETURN_THROWS(); + } +} diff --git a/extension/src/inference/runtime/config/runtime_profile.inc b/extension/src/inference/runtime/config/runtime_profile.inc new file mode 100644 index 000000000..22c6296f3 --- /dev/null +++ b/extension/src/inference/runtime/config/runtime_profile.inc @@ -0,0 +1,586 @@ +/* + * Runtime model profile selection for King inference. + */ + +static bool king_inference_runtime_non_empty(const char *value) +{ + return value != NULL && value[0] != '\0'; +} + +static bool king_inference_runtime_array_bool(zval *source, const char *key) +{ + zval *value; + + if (source == NULL || Z_TYPE_P(source) != IS_ARRAY) { + return false; + } + + value = zend_hash_str_find(Z_ARRVAL_P(source), key, strlen(key)); + return value != NULL && zend_is_true(value); +} + +static zend_long king_inference_runtime_array_long(zval *source, const char *key) +{ + zval *value; + + if (source == NULL || Z_TYPE_P(source) != IS_ARRAY) { + return 0; + } + + value = zend_hash_str_find(Z_ARRVAL_P(source), key, strlen(key)); + return value != NULL && Z_TYPE_P(value) == IS_LONG ? Z_LVAL_P(value) : 0; +} + +static const char *king_inference_runtime_array_string(zval *source, const char *key) +{ + zval *value; + + if (source == NULL || Z_TYPE_P(source) != IS_ARRAY) { + return ""; + } + + value = zend_hash_str_find(Z_ARRVAL_P(source), key, strlen(key)); + return value != NULL && Z_TYPE_P(value) == IS_STRING ? Z_STRVAL_P(value) : ""; +} + +static bool king_inference_runtime_gpu_profile_configured( + const kg_high_perf_compute_ai_config_t *compute +) { + return king_inference_runtime_non_empty(compute->inference_gpu_model_artifact) + || compute->inference_gpu_max_gpu_layers > 0; +} + +static zend_string *king_inference_runtime_gpu_refusal_reasons_string(zval *gpu_runtime) +{ + zval *reasons; + zval *entry; + smart_str buffer = {0}; + bool first = true; + + if (gpu_runtime == NULL || Z_TYPE_P(gpu_runtime) != IS_ARRAY) { + return zend_string_init("gpu_runtime_status_missing", sizeof("gpu_runtime_status_missing") - 1, 0); + } + + reasons = zend_hash_str_find(Z_ARRVAL_P(gpu_runtime), "refusal_reasons", sizeof("refusal_reasons") - 1); + if (reasons == NULL || Z_TYPE_P(reasons) != IS_ARRAY) { + const char *reason = king_inference_runtime_array_string(gpu_runtime, "reason"); + return zend_string_init(reason[0] != '\0' ? reason : "unknown", strlen(reason[0] != '\0' ? reason : "unknown"), 0); + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(reasons), entry) { + if (entry == NULL || Z_TYPE_P(entry) != IS_STRING || Z_STRLEN_P(entry) == 0) { + continue; + } + if (!first) { + smart_str_appendc(&buffer, ','); + } + smart_str_append(&buffer, Z_STR_P(entry)); + first = false; + } ZEND_HASH_FOREACH_END(); + + if (first) { + return zend_string_init("unknown", sizeof("unknown") - 1, 0); + } + + smart_str_0(&buffer); + return buffer.s; +} + +static const char *king_inference_runtime_gpu_profile_refusal_hint(zval *gpu_runtime) +{ + zval *cuda_driver; + zval *thermal; + zval *power; + + if (!king_inference_runtime_array_bool(gpu_runtime, "process_gpu_bindings_enable")) { + return "process-level king.gpu_bindings_enable is disabled"; + } + if (!king_inference_runtime_array_bool(gpu_runtime, "config_gpu_bindings_enable")) { + return "effective King config has gpu_bindings_enable disabled"; + } + if (!king_inference_runtime_array_bool(gpu_runtime, "backend_supported")) { + return "configured GPU backend is not supported by this build"; + } + if (!king_inference_runtime_array_bool(gpu_runtime, "artifact_configured")) { + return "king.inference_gpu_model_artifact is missing"; + } + if (!king_inference_runtime_array_bool(gpu_runtime, "artifact_readable")) { + return "king.inference_gpu_model_artifact is not readable"; + } + if (king_inference_runtime_array_long(gpu_runtime, "max_gpu_layers") <= 0) { + return "king.inference_gpu_max_gpu_layers must be greater than 0"; + } + + cuda_driver = zend_hash_str_find(Z_ARRVAL_P(gpu_runtime), "cuda_driver", sizeof("cuda_driver") - 1); + if (cuda_driver == NULL + || Z_TYPE_P(cuda_driver) != IS_ARRAY + || !king_inference_runtime_array_bool(cuda_driver, "loader_available") + || !king_inference_runtime_array_bool(cuda_driver, "symbols_available") + || !king_inference_runtime_array_bool(cuda_driver, "initialized") + || king_inference_runtime_array_long(cuda_driver, "device_count") <= 0) { + return "CUDA runtime or visible CUDA device is not available"; + } + if (!king_inference_runtime_array_bool(gpu_runtime, "free_vram_available") + || !king_inference_runtime_array_bool(gpu_runtime, "vram_admission_checked")) { + return "CUDA free VRAM information is unavailable"; + } + if (!king_inference_runtime_array_bool(gpu_runtime, "free_vram_floor_met")) { + return "free VRAM is below king.inference_gpu_min_free_vram_mb"; + } + if (king_inference_runtime_array_bool(gpu_runtime, "system_ram_offload_required") + && !king_inference_runtime_array_bool(gpu_runtime, "system_ram_offload_allowed")) { + return "model needs memory outside the reserved GPU budget, but king.inference_gpu_allow_system_ram_offload is disabled"; + } + if (king_inference_runtime_array_bool(gpu_runtime, "system_ram_offload_required") + && !king_inference_runtime_array_bool(gpu_runtime, "system_ram_offload_limit_configured")) { + return "model needs system RAM offload, but king.inference_gpu_system_ram_offload_max_mb is zero"; + } + if (king_inference_runtime_array_bool(gpu_runtime, "system_ram_offload_required") + && !king_inference_runtime_array_bool(gpu_runtime, "system_ram_offload_limit_met")) { + return "model needs more system RAM offload than king.inference_gpu_system_ram_offload_max_mb allows"; + } + if (king_inference_runtime_array_bool(gpu_runtime, "system_ram_offload_required") + && !king_inference_runtime_array_bool(gpu_runtime, "system_ram_available")) { + return "model needs system RAM offload, but system memory availability could not be read"; + } + if (king_inference_runtime_array_bool(gpu_runtime, "system_ram_offload_required") + && !king_inference_runtime_array_bool(gpu_runtime, "system_ram_offload_floor_met")) { + return "model needs system RAM offload, but king.inference_gpu_system_ram_offload_min_free_mb would be violated"; + } + if (king_inference_runtime_array_bool(gpu_runtime, "system_ram_offload_required") + && !king_inference_runtime_array_bool(gpu_runtime, "system_ram_offload_supported")) { + return "model needs system RAM offload, but this King native GPU runtime does not support offloaded weights yet"; + } + if (!king_inference_runtime_array_bool(gpu_runtime, "runtime_vram_fits_free") + || !king_inference_runtime_array_bool(gpu_runtime, "model_vram_admitted")) { + return "model artifact and runtime memory do not fit into free VRAM after reserve"; + } + + thermal = zend_hash_str_find(Z_ARRVAL_P(gpu_runtime), "thermal", sizeof("thermal") - 1); + if (thermal != NULL + && Z_TYPE_P(thermal) == IS_ARRAY + && !king_inference_runtime_array_bool(thermal, "temperature_ok")) { + return "GPU thermal policy is not satisfied"; + } + power = zend_hash_str_find(Z_ARRVAL_P(gpu_runtime), "power", sizeof("power") - 1); + if (power != NULL + && Z_TYPE_P(power) == IS_ARRAY + && king_inference_runtime_array_bool(power, "enabled") + && !king_inference_runtime_array_bool(power, "power_ok")) { + return "GPU power policy is not satisfied"; + } + + return "GPU runtime admission is not ready"; +} + +static zend_result king_inference_runtime_refuse_gpu_profile( + const char *function_name, + zval *gpu_runtime, + const char *profile_kind) +{ + zend_string *reasons = king_inference_runtime_gpu_refusal_reasons_string(gpu_runtime); + const char *hint = king_inference_runtime_gpu_profile_refusal_hint(gpu_runtime); + const char *artifact_path = king_inference_runtime_array_string(gpu_runtime, "artifact_path"); + zval *cuda_driver = zend_hash_str_find(Z_ARRVAL_P(gpu_runtime), "cuda_driver", sizeof("cuda_driver") - 1); + const char *cuda_error = cuda_driver != NULL && Z_TYPE_P(cuda_driver) == IS_ARRAY + ? king_inference_runtime_array_string(cuda_driver, "error") + : ""; + + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() refused %s gpu profile: %s. Reasons=%s. Artifact=%s. CUDA error=%s. King will not silently fall back to CPU for a gpu profile.", + function_name, + profile_kind, + hint, + ZSTR_VAL(reasons), + artifact_path[0] != '\0' ? artifact_path : "(not configured)", + cuda_error[0] != '\0' ? cuda_error : "none" + ); + zend_string_release(reasons); + return FAILURE; +} + +static const kg_high_perf_compute_ai_config_t *king_inference_runtime_compute_config( + zval *zconfig, + const char *function_name) +{ + king_cfg_t *cfg; + + if (zconfig == NULL || Z_TYPE_P(zconfig) == IS_NULL) { + return &king_high_perf_compute_ai_config; + } + + cfg = king_fetch_config(zconfig); + if (cfg == NULL) { + king_set_error("King inference runtime model config requires King\\Config or null."); + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() expects null, a King\\Config object, or a native King\\Config resource.", + function_name + ); + return NULL; + } + + return &cfg->compute_ai; +} + +static zend_result king_inference_runtime_select_profile( + const kg_high_perf_compute_ai_config_t *compute, + const char *function_name, + bool *selected_gpu, + bool *gpu_profile_available, + zval *selected_gpu_runtime) +{ + const char *profile = king_inference_runtime_non_empty(compute->inference_preferred_model_profile) + ? compute->inference_preferred_model_profile + : "auto"; + zval gpu_runtime; + + *selected_gpu = false; + *gpu_profile_available = false; + ZVAL_UNDEF(selected_gpu_runtime); + + ZVAL_UNDEF(&gpu_runtime); + + if (strcasecmp(profile, "gpu") == 0) { + king_inference_gpu_runtime_status_from_compute(compute, &gpu_runtime); + if (!king_inference_runtime_array_bool(&gpu_runtime, "config_ready")) { + king_inference_runtime_refuse_gpu_profile(function_name, &gpu_runtime, "explicit"); + zval_ptr_dtor(&gpu_runtime); + return FAILURE; + } + *gpu_profile_available = true; + *selected_gpu = true; + ZVAL_COPY(selected_gpu_runtime, &gpu_runtime); + zval_ptr_dtor(&gpu_runtime); + return SUCCESS; + } + + if (strcasecmp(profile, "cpu") == 0) { + *selected_gpu = false; + return SUCCESS; + } + + if (strcasecmp(profile, "auto") == 0) { + king_inference_gpu_runtime_status_from_compute(compute, &gpu_runtime); + *gpu_profile_available = king_inference_runtime_array_bool(&gpu_runtime, "config_ready"); + *selected_gpu = *gpu_profile_available; + if (*selected_gpu) { + ZVAL_COPY(selected_gpu_runtime, &gpu_runtime); + } else if (king_inference_runtime_gpu_profile_configured(compute)) { + king_inference_runtime_refuse_gpu_profile(function_name, &gpu_runtime, "configured"); + zval_ptr_dtor(&gpu_runtime); + return FAILURE; + } + zval_ptr_dtor(&gpu_runtime); + return SUCCESS; + } + + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() inference profile must be one of auto, gpu, or cpu.", + function_name + ); + return FAILURE; +} + +static zend_result king_inference_runtime_model_config_array( + const kg_high_perf_compute_ai_config_t *compute, + zval *return_value, + const char *function_name) +{ + const char *profile = king_inference_runtime_non_empty(compute->inference_preferred_model_profile) + ? compute->inference_preferred_model_profile + : "auto"; + const char *cpu_name = king_inference_runtime_non_empty(compute->inference_cpu_model_name) + ? compute->inference_cpu_model_name + : "gemma3:1b"; + const char *gpu_name = king_inference_runtime_non_empty(compute->inference_gpu_model_name) + ? compute->inference_gpu_model_name + : "gemma4:12b"; + const char *selected_name; + const char *selected_artifact; + bool selected_gpu; + bool gpu_profile_available; + bool gpu_profile_configured = king_inference_runtime_gpu_profile_configured(compute); + bool process_gpu_enabled = king_high_perf_compute_ai_config.gpu_bindings_enable; + bool config_gpu_enabled = compute->gpu_bindings_enable; + zval selected_gpu_runtime; + zval artifact; + zval llm_cache; + zval kv_cache; + + ZVAL_UNDEF(&selected_gpu_runtime); + if (king_inference_runtime_select_profile( + compute, + function_name, + &selected_gpu, + &gpu_profile_available, + &selected_gpu_runtime + ) != SUCCESS) { + return FAILURE; + } + + selected_name = selected_gpu ? gpu_name : cpu_name; + selected_artifact = selected_gpu + ? compute->inference_gpu_model_artifact + : compute->inference_cpu_model_artifact; + + if (!king_inference_runtime_non_empty(selected_artifact)) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() requires %s.", + function_name, + selected_gpu ? "inference.gpu_model_artifact" : "inference.cpu_model_artifact" + ); + return FAILURE; + } + + array_init(return_value); + add_assoc_string(return_value, "name", selected_name); + add_assoc_string(return_value, "backend", selected_gpu ? "king_native_gpu" : "king_native_cpu"); + add_assoc_string(return_value, "owned_by", "king-runtime"); + add_assoc_long(return_value, "context_tokens", compute->inference_context_tokens); + add_assoc_bool(return_value, "with_memory", compute->inference_with_memory); + add_assoc_string(return_value, "runtime_profile", selected_gpu ? "gpu" : "cpu"); + add_assoc_string(return_value, "runtime_requested_profile", profile); + add_assoc_string(return_value, "runtime_cpu_model_name", cpu_name); + add_assoc_string(return_value, "runtime_gpu_model_name", gpu_name); + add_assoc_bool(return_value, "runtime_gpu_profile_configured", gpu_profile_configured); + add_assoc_bool(return_value, "runtime_cpu_fallback_allowed", !gpu_profile_configured); + add_assoc_bool(return_value, "runtime_gpu_bindings_enable", process_gpu_enabled); + add_assoc_bool(return_value, "runtime_config_gpu_bindings_enable", config_gpu_enabled); + add_assoc_bool( + return_value, + "runtime_gpu_profile_available", + gpu_profile_available + ); + + array_init(&artifact); + add_assoc_string(&artifact, "path", selected_artifact); + add_assoc_zval(return_value, "artifact", &artifact); + + array_init(&kv_cache); + add_assoc_long(&kv_cache, "max_context_tokens", compute->inference_context_tokens); + add_assoc_long(&kv_cache, "page_tokens", compute->inference_kv_page_tokens); + add_assoc_long(&kv_cache, "element_bytes", compute->inference_kv_element_bytes); + add_assoc_zval(return_value, "kv_cache", &kv_cache); + + array_init(&llm_cache); + add_assoc_bool(&llm_cache, "enabled", compute->inference_llm_cache_enable); + add_assoc_string( + &llm_cache, + "path", + king_inference_runtime_non_empty(compute->inference_llm_cache_path) + ? compute->inference_llm_cache_path + : "/tmp/king-llm-cache" + ); + add_assoc_long(&llm_cache, "min_free_mb", compute->inference_llm_cache_min_free_mb); + add_assoc_bool(&llm_cache, "fail_closed", compute->inference_llm_cache_fail_closed); + add_assoc_string( + &llm_cache, + "disk_alert_webhook", + compute->inference_llm_cache_disk_alert_webhook != NULL + ? compute->inference_llm_cache_disk_alert_webhook + : "" + ); + add_assoc_string( + &llm_cache, + "disk_alert_mcp_service", + compute->inference_llm_cache_disk_alert_mcp_service != NULL + ? compute->inference_llm_cache_disk_alert_mcp_service + : "" + ); + add_assoc_string( + &llm_cache, + "disk_alert_mcp_method", + compute->inference_llm_cache_disk_alert_mcp_method != NULL + ? compute->inference_llm_cache_disk_alert_mcp_method + : "" + ); + add_assoc_zval(return_value, "llm_cache", &llm_cache); + + if (selected_gpu) { + zval gpu; + zval thermal; + zval power; + + array_init(&gpu); + add_assoc_bool(&gpu, "enabled", true); + add_assoc_long(&gpu, "max_gpu_layers", compute->inference_gpu_max_gpu_layers); + add_assoc_long(&gpu, "vram_reserve_mb", compute->inference_gpu_vram_reserve_mb); + add_assoc_long(&gpu, "min_free_vram_mb", compute->inference_gpu_min_free_vram_mb); + add_assoc_bool(&gpu, "allow_system_ram_offload", compute->inference_gpu_allow_system_ram_offload); + add_assoc_long(&gpu, "system_ram_offload_max_mb", compute->inference_gpu_system_ram_offload_max_mb); + add_assoc_long( + &gpu, + "system_ram_offload_min_free_mb", + compute->inference_gpu_system_ram_offload_min_free_mb + ); + add_assoc_string( + &gpu, + "system_ram_offload_policy", + compute->inference_gpu_allow_system_ram_offload ? "explicitly_allowed" : "disabled_fail_closed" + ); + add_assoc_bool( + &gpu, + "batch_prefill_experimental_enable", + compute->inference_gpu_batch_prefill_experimental_enable + ); + + array_init(&thermal); + if (king_inference_runtime_non_empty(compute->inference_gpu_thermal_sensor_path)) { + add_assoc_string( + &thermal, + "sensor_path", + compute->inference_gpu_thermal_sensor_path + ); + } + if (king_inference_runtime_non_empty(compute->inference_gpu_thermal_sensor_command)) { + add_assoc_string( + &thermal, + "sensor_command", + compute->inference_gpu_thermal_sensor_command + ); + } + add_assoc_double( + &thermal, + "max_temperature_c", + compute->inference_gpu_thermal_max_temperature_c + ); + add_assoc_long( + &thermal, + "check_interval_seconds", + compute->inference_gpu_thermal_check_interval_sec + ); + add_assoc_bool( + &thermal, + "allow_unmonitored_gpu", + compute->inference_gpu_allow_unmonitored + ); + add_assoc_zval(&gpu, "thermal", &thermal); + array_init(&power); + if (king_inference_runtime_non_empty(compute->inference_gpu_power_sensor_command)) { + add_assoc_string( + &power, + "sensor_command", + compute->inference_gpu_power_sensor_command + ); + } + add_assoc_double(&power, "max_watts", compute->inference_gpu_power_max_watts); + add_assoc_long( + &power, + "check_interval_seconds", + compute->inference_gpu_power_check_interval_sec + ); + add_assoc_zval(&gpu, "power", &power); + add_assoc_zval(return_value, "gpu", &gpu); + + if (Z_ISUNDEF(selected_gpu_runtime)) { + zval gpu_runtime; + + king_inference_gpu_runtime_status_from_compute(compute, &gpu_runtime); + add_assoc_zval(return_value, "gpu_runtime", &gpu_runtime); + } else { + add_assoc_zval(return_value, "gpu_runtime", &selected_gpu_runtime); + } + } else if (!Z_ISUNDEF(selected_gpu_runtime)) { + zval_ptr_dtor(&selected_gpu_runtime); + } + + return SUCCESS; +} + +PHP_FUNCTION(king_inference_gpu_runtime_status) +{ + zval *zconfig = NULL; + const kg_high_perf_compute_ai_config_t *compute; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL_OR_NULL(zconfig) + ZEND_PARSE_PARAMETERS_END(); + + compute = king_inference_runtime_compute_config( + zconfig, + "king_inference_gpu_runtime_status" + ); + if (compute == NULL) { + RETURN_THROWS(); + } + + king_inference_gpu_runtime_status_from_compute(compute, return_value); +} + +PHP_FUNCTION(king_inference_runtime_model_config) +{ + zval *zconfig = NULL; + const kg_high_perf_compute_ai_config_t *compute; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL_OR_NULL(zconfig) + ZEND_PARSE_PARAMETERS_END(); + + compute = king_inference_runtime_compute_config( + zconfig, + "king_inference_runtime_model_config" + ); + if (compute == NULL) { + RETURN_THROWS(); + } + + if (king_inference_runtime_model_config_array( + compute, + return_value, + "king_inference_runtime_model_config" + ) != SUCCESS) { + RETURN_THROWS(); + } +} + +PHP_FUNCTION(king_inference_runtime_model_load) +{ + zval *zconfig = NULL; + const kg_high_perf_compute_ai_config_t *compute; + zval model_config; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL_OR_NULL(zconfig) + ZEND_PARSE_PARAMETERS_END(); + + compute = king_inference_runtime_compute_config( + zconfig, + "king_inference_runtime_model_load" + ); + if (compute == NULL) { + RETURN_THROWS(); + } + + ZVAL_UNDEF(&model_config); + if (king_inference_runtime_model_config_array( + compute, + &model_config, + "king_inference_runtime_model_load" + ) != SUCCESS) { + RETURN_THROWS(); + } + + object_init_ex(return_value, king_ce_inference_model); + if (king_inference_model_init( + return_value, + &model_config, + "king_inference_runtime_model_load" + ) != SUCCESS) { + zval_ptr_dtor(&model_config); + zval_ptr_dtor(return_value); + RETURN_THROWS(); + } + + zval_ptr_dtor(&model_config); +} diff --git a/extension/src/inference/runtime/config/runtime_truth.inc b/extension/src/inference/runtime/config/runtime_truth.inc new file mode 100644 index 000000000..690b71cae --- /dev/null +++ b/extension/src/inference/runtime/config/runtime_truth.inc @@ -0,0 +1,521 @@ +/* + * Runtime truth payloads shared by model info, OpenAI model listings and errors. + */ + +static void king_inference_model_runtime_timing_truth( + king_inference_model_object *model, + zval *return_value +) { + array_init(return_value); + add_assoc_bool(return_value, "available", model->runtime_last_timing_available); + add_assoc_bool(return_value, "first_token_available", model->runtime_last_first_token_available); + add_assoc_long(return_value, "started_monotonic_ms", model->runtime_last_started_ms); + if (model->runtime_last_first_token_available) { + add_assoc_long(return_value, "first_token_monotonic_ms", model->runtime_last_first_token_ms); + add_assoc_long(return_value, "time_to_first_token_ms", model->runtime_last_time_to_first_token_ms); + } else { + add_assoc_null(return_value, "first_token_monotonic_ms"); + add_assoc_null(return_value, "time_to_first_token_ms"); + } + if (model->runtime_last_timing_available) { + add_assoc_long(return_value, "finished_monotonic_ms", model->runtime_last_finished_ms); + add_assoc_long(return_value, "duration_ms", model->runtime_last_duration_ms); + add_assoc_double(return_value, "tokens_per_second", model->runtime_last_tokens_per_second); + } else { + add_assoc_null(return_value, "finished_monotonic_ms"); + add_assoc_null(return_value, "duration_ms"); + add_assoc_null(return_value, "tokens_per_second"); + } + add_assoc_long(return_value, "generated_tokens", model->runtime_last_generated_tokens); +} + +static void king_inference_model_runtime_resident_truth( + king_inference_model_object *model, + zval *return_value +) { + zval tokenizer_status; + + array_init(return_value); + add_assoc_bool(return_value, "model_object", true); + add_assoc_bool(return_value, "gguf_metadata", model->gguf.loaded); + add_assoc_bool(return_value, "native_map", model->native_map_loaded); + add_assoc_bool(return_value, "tensor_index", Z_TYPE(model->tensor_index) == IS_ARRAY + && zend_hash_num_elements(Z_ARRVAL(model->tensor_index)) > 0); + add_assoc_bool(return_value, "tokenizer", model->gguf.tokenizer_tokens_loaded + && model->gguf.tokenizer_lookup_loaded); + king_inference_tokenizer_status_array(model, &tokenizer_status); + add_assoc_zval(return_value, "tokenizer_status", &tokenizer_status); + add_assoc_bool(return_value, "cuda_context", model->cuda_context_available); + add_assoc_bool(return_value, "cuda_context_owned", model->cuda_context_owned); + add_assoc_bool(return_value, "cuda_weights", model->cuda_weight_upload_complete + && model->cuda_weight_uploaded_bytes > 0); + add_assoc_bool(return_value, "cuda_weight_cache", model->cuda_weight_cache_ready); + add_assoc_bool(return_value, "cuda_kv_cache", model->cuda_device_kv_cache_allocated); + add_assoc_long(return_value, "cuda_device_bytes_allocated", (zend_long) model->cuda_device_bytes_allocated); + add_assoc_long(return_value, "cuda_device_peak_bytes_allocated", (zend_long) model->cuda_device_peak_bytes_allocated); +} + +static void king_inference_model_runtime_truth_array( + king_inference_model_object *model, + zval *return_value +) { + king_inference_backend_kind kind; + zval resident; + zval timing; + bool backend_config_valid = true; + bool gpu_enabled; + bool gpu_runtime_created = false; + zval gpu_runtime; + zval *reason; + zval *generation_ready; + const char *active_device = "unknown"; + const char *fallback_mode = "none"; + const char *fallback_error = "none"; + bool cpu_fallback_allowed = false; + + ZVAL_UNDEF(&gpu_runtime); + if (king_inference_backend_kind_from_config(&model->config, &kind, "King runtime truth") != SUCCESS) { + if (EG(exception)) { + zend_clear_exception(); + } + backend_config_valid = false; + kind = KING_INFERENCE_BACKEND_KING_NATIVE_CPU; + } + gpu_enabled = backend_config_valid && king_inference_gpu_enabled(&model->config); + if (gpu_enabled) { + king_inference_gpu_runtime_status_from_model(model, &gpu_runtime); + gpu_runtime_created = true; + } + + if (!backend_config_valid) { + active_device = "invalid"; + fallback_mode = "invalid_backend"; + fallback_error = "king.validation.invalid_backend_config"; + } else if (kind == KING_INFERENCE_BACKEND_KING_NATIVE_GPU) { + active_device = model->cuda_context_available ? "cuda" : "cuda_unavailable"; + fallback_mode = "refuse_cpu_fallback"; + if (!model->cuda_context_available) { + fallback_error = "king.policy.gpu_unavailable"; + } else if (gpu_runtime_created) { + generation_ready = king_inference_array_find(&gpu_runtime, "generation_ready"); + if (generation_ready == NULL || !zend_is_true(generation_ready)) { + fallback_error = "king.policy.gpu_generation_not_ready"; + } + } + } else if (kind == KING_INFERENCE_BACKEND_KING_NATIVE_CPU) { + active_device = "cpu"; + fallback_mode = "cpu_profile"; + } else { + active_device = "local_process"; + fallback_mode = "external_process"; + } + + array_init(return_value); + add_assoc_long(return_value, "version", 1); + add_assoc_bool(return_value, "backend_config_valid", backend_config_valid); + add_assoc_string( + return_value, + "backend", + backend_config_valid ? king_inference_backend_kind_name(kind) : "invalid" + ); + add_assoc_string(return_value, "active_device", active_device); + add_assoc_bool(return_value, "model_resident", true); + add_assoc_bool(return_value, "gpu_required", gpu_enabled || kind == KING_INFERENCE_BACKEND_KING_NATIVE_GPU); + add_assoc_bool(return_value, "cpu_fallback_allowed", cpu_fallback_allowed); + add_assoc_bool(return_value, "silent_cpu_fallback", false); + add_assoc_string(return_value, "fallback_mode", fallback_mode); + add_assoc_string(return_value, "fallback_error", fallback_error); + if (gpu_runtime_created) { + reason = king_inference_array_find(&gpu_runtime, "reason"); + if (reason != NULL && Z_TYPE_P(reason) == IS_STRING && Z_STRLEN_P(reason) > 0) { + add_assoc_str(return_value, "gpu_admission_reason", zend_string_copy(Z_STR_P(reason))); + } else { + add_assoc_string(return_value, "gpu_admission_reason", "unknown"); + } + } else { + add_assoc_string(return_value, "gpu_admission_reason", "not_gpu_model"); + } + + king_inference_model_runtime_resident_truth(model, &resident); + add_assoc_zval(return_value, "resident_state", &resident); + king_inference_model_runtime_timing_truth(model, &timing); + add_assoc_zval(return_value, "measured_token_timing", &timing); + + if (gpu_runtime_created) { + zval_ptr_dtor(&gpu_runtime); + } +} + +static void king_inference_model_decoder_truth_array( + king_inference_model_object *model, + zval *return_value +) { + king_inference_backend_kind kind; + bool backend_config_valid = true; + bool native_cpu_backend; + bool native_gpu_backend; + bool local_backend; + bool gpu_enabled; + bool gpu_prompt_ready = false; + bool gpu_synthetic_graph_ready = false; + bool silent_cpu_fallback = false; + const char *backend_name; + const char *active_device; + const char *prompt_graph_path; + const char *synthetic_graph_path; + const char *sampler_path; + const char *kv_cache_path; + const char *fallback_error = "none"; + const char *fallback_policy = "no_cpu_fallback"; + + if (king_inference_backend_kind_from_config(&model->config, &kind, "King decoder truth") != SUCCESS) { + if (EG(exception)) { + zend_clear_exception(); + } + backend_config_valid = false; + kind = KING_INFERENCE_BACKEND_KING_NATIVE_CPU; + } + + native_cpu_backend = backend_config_valid && kind == KING_INFERENCE_BACKEND_KING_NATIVE_CPU; + native_gpu_backend = backend_config_valid && kind == KING_INFERENCE_BACKEND_KING_NATIVE_GPU; + local_backend = backend_config_valid && kind == KING_INFERENCE_BACKEND_LOCAL; + gpu_enabled = backend_config_valid && king_inference_gpu_enabled(&model->config); + backend_name = backend_config_valid ? king_inference_backend_kind_name(kind) : "invalid"; + + if (!backend_config_valid) { + active_device = "invalid"; + prompt_graph_path = "unavailable"; + synthetic_graph_path = "unavailable"; + sampler_path = "unavailable"; + kv_cache_path = "unavailable"; + fallback_error = "king.validation.invalid_backend_config"; + fallback_policy = "invalid_backend_refused"; + } else if (native_gpu_backend) { + gpu_prompt_ready = model->cuda_decoder_prompt_loop_available; + gpu_synthetic_graph_ready = model->cuda_decoder_graph_executor_available + && model->cuda_decoder_graph_executor_result_contract_available; + active_device = model->cuda_context_available ? "cuda" : "cuda_unavailable"; + prompt_graph_path = gpu_prompt_ready + ? "king_native_gpu_prompt_to_logits_decode" + : "unavailable"; + synthetic_graph_path = gpu_synthetic_graph_ready + ? "king_native_gpu_synthetic_token_vector_graph" + : "unavailable"; + sampler_path = gpu_prompt_ready + ? "king_native_gpu_bounded_top_k_sampler" + : "unavailable"; + kv_cache_path = model->cuda_device_kv_cache_allocated + ? "king_native_gpu_device_kv_cache" + : "unallocated"; + fallback_policy = "refuse_cpu_fallback"; + if (!model->cuda_context_available) { + fallback_error = "king.policy.gpu_unavailable"; + } else if (!gpu_prompt_ready) { + fallback_error = "king.policy.gpu_generation_not_ready"; + } + } else if (native_cpu_backend) { + active_device = "cpu"; + prompt_graph_path = "king_native_cpu_prompt_to_logits_decode"; + synthetic_graph_path = "king_native_cpu_synthetic_token_vector_graph"; + sampler_path = "king_native_cpu_sampler"; + kv_cache_path = (!Z_ISUNDEF(model->paged_kv_cache_plan) && Z_TYPE(model->paged_kv_cache_plan) == IS_ARRAY) + ? "king_native_cpu_paged_kv_cache_plan" + : "king_native_cpu_runtime_state"; + fallback_policy = "cpu_profile"; + } else if (local_backend) { + active_device = gpu_enabled ? "local_process_gpu_requested" : "local_process"; + prompt_graph_path = "external_local_runner"; + synthetic_graph_path = "not_supported"; + sampler_path = "external_local_runner"; + kv_cache_path = "external_local_runner"; + fallback_policy = gpu_enabled ? "external_runner_gpu_policy" : "external_process"; + } else { + active_device = "unknown"; + prompt_graph_path = "unavailable"; + synthetic_graph_path = "unavailable"; + sampler_path = "unavailable"; + kv_cache_path = "unavailable"; + fallback_error = "king.runtime.unknown_backend"; + fallback_policy = "unknown"; + } + + array_init(return_value); + add_assoc_long(return_value, "schema_version", 1); + add_assoc_string(return_value, "backend", backend_name); + add_assoc_bool(return_value, "backend_config_valid", backend_config_valid); + add_assoc_string(return_value, "active_device", active_device); + add_assoc_string(return_value, "prompt_graph_path", prompt_graph_path); + add_assoc_string(return_value, "synthetic_graph_path", synthetic_graph_path); + add_assoc_string(return_value, "sampler_path", sampler_path); + add_assoc_string(return_value, "kv_cache_path", kv_cache_path); + add_assoc_bool(return_value, "prompt_to_logits_inference", native_cpu_backend || gpu_prompt_ready); + add_assoc_bool(return_value, "synthetic_token_vector_graph", native_cpu_backend || gpu_synthetic_graph_ready); + add_assoc_bool(return_value, "sampler_available", native_cpu_backend || gpu_prompt_ready || local_backend); + add_assoc_bool(return_value, "kv_cache_available", native_cpu_backend || model->cuda_device_kv_cache_allocated || local_backend); + add_assoc_bool(return_value, "silent_cpu_fallback", silent_cpu_fallback); + add_assoc_bool(return_value, "cpu_prompt_fallback_allowed", false); + add_assoc_string(return_value, "fallback_policy", fallback_policy); + add_assoc_string(return_value, "fallback_error", fallback_error); +} + +static bool king_inference_model_memory_enabled(king_inference_model_object *model) +{ + return king_inference_bool_from_array(&model->config, "with_memory", false); +} + +static void king_inference_model_memory_truth_array( + king_inference_model_object *model, + zval *return_value +) { + bool with_memory = king_inference_model_memory_enabled(model); + + array_init(return_value); + add_assoc_bool(return_value, "with_memory", with_memory); + add_assoc_string(return_value, "mode", with_memory ? "stateful_graph_memory" : "stateless"); +} + +static void king_inference_model_readiness_truth_array( + king_inference_model_object *model, + zval *return_value +) { + king_inference_backend_kind kind; + bool backend_config_valid = true; + bool gpu_enabled; + bool gpu_config_ready = false; + bool gpu_generation_ready = false; + bool local_backend; + bool native_cpu_backend; + bool native_gpu_backend; + zval gpu_runtime; + + ZVAL_UNDEF(&gpu_runtime); + if (king_inference_backend_kind_from_config(&model->config, &kind, "King readiness truth") != SUCCESS) { + if (EG(exception)) { + zend_clear_exception(); + } + backend_config_valid = false; + kind = KING_INFERENCE_BACKEND_KING_NATIVE_CPU; + } + + gpu_enabled = backend_config_valid && king_inference_gpu_enabled(&model->config); + if (gpu_enabled) { + zval *config_ready; + zval *generation_ready; + + king_inference_gpu_runtime_status_from_model(model, &gpu_runtime); + config_ready = king_inference_array_find(&gpu_runtime, "config_ready"); + generation_ready = king_inference_array_find(&gpu_runtime, "generation_ready"); + gpu_config_ready = config_ready != NULL && zend_is_true(config_ready); + gpu_generation_ready = generation_ready != NULL && zend_is_true(generation_ready); + } + + local_backend = backend_config_valid && kind == KING_INFERENCE_BACKEND_LOCAL; + native_cpu_backend = backend_config_valid && kind == KING_INFERENCE_BACKEND_KING_NATIVE_CPU; + native_gpu_backend = backend_config_valid && kind == KING_INFERENCE_BACKEND_KING_NATIVE_GPU; + + array_init(return_value); + add_assoc_bool(return_value, "backend_config_valid", backend_config_valid); + add_assoc_bool(return_value, "model_resident", true); + add_assoc_bool(return_value, "metadata_ready", model->gguf.loaded); + add_assoc_bool(return_value, "native_map_ready", model->native_map_loaded); + add_assoc_bool(return_value, "tokenizer_ready", model->gguf.tokenizer_tokens_loaded && model->gguf.tokenizer_lookup_loaded); + add_assoc_bool(return_value, "gpu_required", gpu_enabled || native_gpu_backend); + add_assoc_bool(return_value, "gpu_config_ready", gpu_config_ready); + add_assoc_bool(return_value, "gpu_generation_ready", native_gpu_backend && gpu_generation_ready); + add_assoc_bool( + return_value, + "openai_generation_ready", + backend_config_valid && ((local_backend && !gpu_enabled) || native_cpu_backend || (native_gpu_backend && gpu_generation_ready)) + ); + add_assoc_bool(return_value, "native_graph_streaming_ready", native_cpu_backend); + add_assoc_bool(return_value, "native_stream_contract_ready", native_cpu_backend || native_gpu_backend); + add_assoc_bool(return_value, "embeddings_ready", native_cpu_backend); + + if (gpu_enabled) { + zval_ptr_dtor(&gpu_runtime); + } +} + +static bool king_inference_native_readiness_bool(zval *source, const char *key) +{ + zval *value; + + if (source == NULL || Z_TYPE_P(source) != IS_ARRAY) { + return false; + } + + value = king_inference_array_find(source, key); + return value != NULL && zend_is_true(value); +} + +static void king_inference_model_native_engine_readiness_array( + king_inference_model_object *model, + zval *return_value +) { + king_inference_backend_kind kind; + bool backend_config_valid = true; + bool gpu_enabled; + bool local_backend; + bool native_cpu_backend; + bool native_gpu_backend; + bool gpu_config_ready = false; + bool gpu_generation_ready = false; + bool native_stream_contract_admitted = false; + bool native_graph_payload_admitted = false; + bool plain_text_generation_admitted = false; + const char *backend_name; + const char *state = "not_native_backend"; + const char *proof_path = "non_native_backend"; + zval blockers; + zval refusal_reasons; + zval refusal_error_codes; + zval refusal_error_categories; + zval gpu_runtime; + + ZVAL_UNDEF(&gpu_runtime); + if (king_inference_backend_kind_from_config(&model->config, &kind, "King native engine readiness") != SUCCESS) { + if (EG(exception)) { + zend_clear_exception(); + } + backend_config_valid = false; + kind = KING_INFERENCE_BACKEND_KING_NATIVE_CPU; + } + + gpu_enabled = backend_config_valid && king_inference_gpu_enabled(&model->config); + local_backend = backend_config_valid && kind == KING_INFERENCE_BACKEND_LOCAL; + native_cpu_backend = backend_config_valid && kind == KING_INFERENCE_BACKEND_KING_NATIVE_CPU; + native_gpu_backend = backend_config_valid && kind == KING_INFERENCE_BACKEND_KING_NATIVE_GPU; + backend_name = backend_config_valid ? king_inference_backend_kind_name(kind) : "invalid"; + + array_init(&blockers); + array_init(&refusal_reasons); + + if (!backend_config_valid) { + state = "invalid_backend_config"; + proof_path = "backend_config_validation"; + add_next_index_string(&refusal_reasons, "backend_config_invalid"); + } else if (local_backend) { + state = "not_native_backend"; + proof_path = "local_process_backend"; + add_next_index_string(&refusal_reasons, "not_native_backend"); + } else if (native_cpu_backend) { + state = "admitted"; + proof_path = "native_cpu_model_resident_tokenizer_contract"; + native_stream_contract_admitted = true; + native_graph_payload_admitted = true; + plain_text_generation_admitted = true; + } else if (native_gpu_backend) { + zval *config_ready; + zval *generation_ready; + zval *runtime_refusals; + zval *entry; + + proof_path = "gpu_runtime_status_decoder_blockers_and_policy"; + king_inference_gpu_runtime_status_from_model(model, &gpu_runtime); + config_ready = king_inference_array_find(&gpu_runtime, "config_ready"); + generation_ready = king_inference_array_find(&gpu_runtime, "generation_ready"); + gpu_config_ready = config_ready != NULL && zend_is_true(config_ready); + gpu_generation_ready = generation_ready != NULL && zend_is_true(generation_ready); + native_stream_contract_admitted = true; + native_graph_payload_admitted = model->cuda_decoder_graph_executor_available + && model->cuda_decoder_graph_executor_result_contract_available; + plain_text_generation_admitted = gpu_generation_ready; + state = plain_text_generation_admitted ? "admitted" : "refused"; + + king_inference_gpu_decoder_blockers(model, &blockers); + runtime_refusals = king_inference_array_find(&gpu_runtime, "refusal_reasons"); + if (runtime_refusals != NULL && Z_TYPE_P(runtime_refusals) == IS_ARRAY) { + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(runtime_refusals), entry) { + if (Z_TYPE_P(entry) == IS_STRING && Z_STRLEN_P(entry) > 0) { + add_next_index_str(&refusal_reasons, zend_string_copy(Z_STR_P(entry))); + } + } ZEND_HASH_FOREACH_END(); + } + if (!plain_text_generation_admitted) { + add_next_index_string(&refusal_reasons, "native_plain_text_generation_not_admitted"); + } + } + + king_inference_error_taxonomy_codes_from_reason_list(&refusal_reasons, &refusal_error_codes); + king_inference_error_taxonomy_categories_from_code_list( + &refusal_error_codes, + &refusal_error_categories + ); + + array_init(return_value); + add_assoc_long(return_value, "schema_version", 1); + add_assoc_string(return_value, "scope", "native_engine_readiness"); + add_assoc_bool(return_value, "router_independent", true); + add_assoc_string(return_value, "backend", backend_name); + add_assoc_bool(return_value, "backend_config_valid", backend_config_valid); + add_assoc_bool(return_value, "native_backend", native_cpu_backend || native_gpu_backend); + add_assoc_bool(return_value, "gpu_required", gpu_enabled || native_gpu_backend); + add_assoc_bool(return_value, "gpu_config_ready", native_gpu_backend && gpu_config_ready); + add_assoc_bool(return_value, "native_stream_contract_admitted", native_stream_contract_admitted); + add_assoc_bool(return_value, "native_graph_payload_admitted", native_graph_payload_admitted); + add_assoc_bool(return_value, "plain_text_generation_admitted", plain_text_generation_admitted); + add_assoc_bool(return_value, "openai_prompt_formatting_applied", false); + add_assoc_string(return_value, "prompt_formatting_owner", "openai_router"); + add_assoc_string(return_value, "tool_fields_owner", "openai_router"); + add_assoc_string(return_value, "error_response_owner", "openai_router"); + add_assoc_string(return_value, "state", state); + add_assoc_string(return_value, "proof_path", proof_path); + add_assoc_zval(return_value, "decoder_blockers", &blockers); + add_assoc_zval(return_value, "refusal_reasons", &refusal_reasons); + add_assoc_zval(return_value, "refusal_error_codes", &refusal_error_codes); + add_assoc_zval(return_value, "refusal_error_categories", &refusal_error_categories); + if (native_gpu_backend && !Z_ISUNDEF(gpu_runtime)) { + add_assoc_zval(return_value, "gpu_runtime", &gpu_runtime); + } +} + +static void king_inference_model_add_runtime_surface_fields( + king_inference_model_object *model, + zval *target, + zval *runtime_truth +) { + zval memory; + zval readiness; + zval decoder_truth; + zval *backend = king_inference_array_find(runtime_truth, "backend"); + zval *active_device = king_inference_array_find(runtime_truth, "active_device"); + zval *fallback_mode = king_inference_array_find(runtime_truth, "fallback_mode"); + zval *silent_cpu_fallback = king_inference_array_find(runtime_truth, "silent_cpu_fallback"); + zval *gpu_admission_reason = king_inference_array_find(runtime_truth, "gpu_admission_reason"); + bool with_memory = king_inference_model_memory_enabled(model); + + if (backend != NULL && Z_TYPE_P(backend) == IS_STRING) { + add_assoc_str(target, "configured_backend", zend_string_copy(Z_STR_P(backend))); + add_assoc_str(target, "active_backend", zend_string_copy(Z_STR_P(backend))); + } else { + add_assoc_string(target, "configured_backend", "invalid"); + add_assoc_string(target, "active_backend", "invalid"); + } + if (active_device != NULL && Z_TYPE_P(active_device) == IS_STRING) { + add_assoc_str(target, "active_device", zend_string_copy(Z_STR_P(active_device))); + } + if (fallback_mode != NULL && Z_TYPE_P(fallback_mode) == IS_STRING) { + add_assoc_str(target, "fallback_mode", zend_string_copy(Z_STR_P(fallback_mode))); + } else { + add_assoc_string(target, "fallback_mode", "unknown"); + } + add_assoc_bool( + target, + "silent_cpu_fallback", + silent_cpu_fallback != NULL && zend_is_true(silent_cpu_fallback) + ); + if (gpu_admission_reason != NULL && Z_TYPE_P(gpu_admission_reason) == IS_STRING) { + add_assoc_str(target, "gpu_admission_reason", zend_string_copy(Z_STR_P(gpu_admission_reason))); + } else { + add_assoc_string(target, "gpu_admission_reason", "unknown"); + } + add_assoc_bool(target, "with_memory", with_memory); + add_assoc_string(target, "memory_mode", with_memory ? "stateful_graph_memory" : "stateless"); + + king_inference_model_memory_truth_array(model, &memory); + add_assoc_zval(target, "memory", &memory); + king_inference_model_readiness_truth_array(model, &readiness); + add_assoc_zval(target, "readiness", &readiness); + king_inference_model_decoder_truth_array(model, &decoder_truth); + add_assoc_zval(target, "decoder_truth", &decoder_truth); +} diff --git a/extension/src/inference/runtime/errors/error_taxonomy.inc b/extension/src/inference/runtime/errors/error_taxonomy.inc new file mode 100644 index 000000000..5e57ef98d --- /dev/null +++ b/extension/src/inference/runtime/errors/error_taxonomy.inc @@ -0,0 +1,367 @@ +/* + * Stable inference error taxonomy shared by router responses and diagnostics. + * + * The taxonomy intentionally carries no request body, prompt, message content, + * or model output. Human-facing details stay in the existing error message. + */ + +static bool king_inference_error_ascii_contains(const char *haystack, const char *needle) +{ + size_t haystack_len; + size_t needle_len; + + if (haystack == NULL || needle == NULL || needle[0] == '\0') { + return false; + } + + haystack_len = strlen(haystack); + needle_len = strlen(needle); + if (needle_len > haystack_len) { + return false; + } + + for (size_t i = 0; i + needle_len <= haystack_len; i++) { + if (strncasecmp(haystack + i, needle, needle_len) == 0) { + return true; + } + } + + return false; +} + +static const char *king_inference_error_category_from_code(const char *code) +{ + if (code == NULL) { + return "router"; + } + if (strncmp(code, "king.validation.", sizeof("king.validation.") - 1) == 0) { + return "validation"; + } + if (strncmp(code, "king.model_artifact.", sizeof("king.model_artifact.") - 1) == 0) { + return "model_artifact"; + } + if (strncmp(code, "king.tokenizer.", sizeof("king.tokenizer.") - 1) == 0) { + return "tokenizer"; + } + if (strncmp(code, "king.graph.", sizeof("king.graph.") - 1) == 0) { + return "graph"; + } + if (strncmp(code, "king.allocation.", sizeof("king.allocation.") - 1) == 0) { + return "allocation"; + } + if (strncmp(code, "king.kernel.", sizeof("king.kernel.") - 1) == 0) { + return "kernel"; + } + if (strncmp(code, "king.numeric_mismatch.", sizeof("king.numeric_mismatch.") - 1) == 0) { + return "numeric_mismatch"; + } + if (strncmp(code, "king.kv_cache.", sizeof("king.kv_cache.") - 1) == 0) { + return "kv_cache"; + } + if (strncmp(code, "king.policy.", sizeof("king.policy.") - 1) == 0) { + return "policy"; + } + if (strncmp(code, "king.timeout.", sizeof("king.timeout.") - 1) == 0) { + return "timeout"; + } + if (strncmp(code, "king.cancellation.", sizeof("king.cancellation.") - 1) == 0) { + return "cancellation"; + } + if (strncmp(code, "king.unsupported_feature.", sizeof("king.unsupported_feature.") - 1) == 0) { + return "unsupported_feature"; + } + + return "router"; +} + +static bool king_inference_error_code_is_stable(const char *code) +{ + return code != NULL && strncmp(code, "king.", sizeof("king.") - 1) == 0; +} + +static const char *king_inference_error_code_from_runtime_reason(const char *reason) +{ + if (reason == NULL || reason[0] == '\0') { + return "king.router.internal"; + } + if (king_inference_error_ascii_contains(reason, "backend_config")) { + return "king.validation.invalid_backend_config"; + } + if (king_inference_error_ascii_contains(reason, "artifact_missing") + || king_inference_error_ascii_contains(reason, "artifact_configured")) { + return "king.model_artifact.missing"; + } + if (king_inference_error_ascii_contains(reason, "artifact_not_readable") + || king_inference_error_ascii_contains(reason, "not_readable")) { + return "king.model_artifact.unreadable"; + } + if (king_inference_error_ascii_contains(reason, "tokenizer")) { + return "king.tokenizer.unavailable"; + } + if (king_inference_error_ascii_contains(reason, "graph")) { + return "king.graph.invalid_request"; + } + if (king_inference_error_ascii_contains(reason, "kv_cache") + || king_inference_error_ascii_contains(reason, "kv_")) { + return "king.kv_cache.invalid"; + } + if (king_inference_error_ascii_contains(reason, "numeric")) { + return "king.numeric_mismatch.detected"; + } + if (king_inference_error_ascii_contains(reason, "vram") + || king_inference_error_ascii_contains(reason, "allocator") + || king_inference_error_ascii_contains(reason, "allocation") + || king_inference_error_ascii_contains(reason, "memory")) { + return "king.allocation.failed"; + } + if (king_inference_error_ascii_contains(reason, "gpu_temperature_limit_reached")) { + return "king.policy.gpu_thermal_limit_reached"; + } + if (king_inference_error_ascii_contains(reason, "gpu_power_limit_reached")) { + return "king.policy.gpu_power_limit_reached"; + } + if (king_inference_error_ascii_contains(reason, "thermal") + || king_inference_error_ascii_contains(reason, "power") + || king_inference_error_ascii_contains(reason, "disabled") + || king_inference_error_ascii_contains(reason, "not_admitted") + || king_inference_error_ascii_contains(reason, "policy")) { + return "king.policy.refused"; + } + if (king_inference_error_ascii_contains(reason, "timeout") + || king_inference_error_ascii_contains(reason, "deadline")) { + return "king.timeout.deadline_exceeded"; + } + if (king_inference_error_ascii_contains(reason, "cancel")) { + return "king.cancellation.cancelled"; + } + if (king_inference_error_ascii_contains(reason, "unsupported")) { + return "king.unsupported_feature.requested"; + } + if (king_inference_error_ascii_contains(reason, "kernel") + || king_inference_error_ascii_contains(reason, "cuda") + || king_inference_error_ascii_contains(reason, "decoder") + || king_inference_error_ascii_contains(reason, "gpu_")) { + return "king.kernel.unavailable"; + } + + return "king.router.internal"; +} + +static const char *king_inference_error_code_from_message( + zend_long status, + const char *type, + const char *message) +{ + if (status == 404) { + return "king.router.route_not_found"; + } + if (status == 405) { + return "king.router.method_not_allowed"; + } + + if (message != NULL) { + if (king_inference_error_ascii_contains(message, "not valid JSON") + || king_inference_error_ascii_contains(message, "must be") + || king_inference_error_ascii_contains(message, "must not") + || king_inference_error_ascii_contains(message, "invalid")) { + return "king.validation.invalid_request"; + } + if (king_inference_error_ascii_contains(message, "backend configuration")) { + return "king.validation.invalid_backend_config"; + } + if (king_inference_error_ascii_contains(message, "artifact")) { + return king_inference_error_ascii_contains(message, "readable") + ? "king.model_artifact.unreadable" + : "king.model_artifact.missing"; + } + if (king_inference_error_ascii_contains(message, "tokenizer")) { + return "king.tokenizer.unavailable"; + } + if (king_inference_error_ascii_contains(message, "graph")) { + return "king.graph.invalid_request"; + } + if (king_inference_error_ascii_contains(message, "KV") + || king_inference_error_ascii_contains(message, "cache")) { + return "king.kv_cache.invalid"; + } + if (king_inference_error_ascii_contains(message, "numeric")) { + return "king.numeric_mismatch.detected"; + } + if (king_inference_error_ascii_contains(message, "allocation") + || king_inference_error_ascii_contains(message, "memory") + || king_inference_error_ascii_contains(message, "VRAM")) { + return "king.allocation.failed"; + } + if (king_inference_error_ascii_contains(message, "runtime policy") + || king_inference_error_ascii_contains(message, "not ready") + || king_inference_error_ascii_contains(message, "cannot generate") + || king_inference_error_ascii_contains(message, "not admitted")) { + return "king.policy.refused"; + } + if (king_inference_error_ascii_contains(message, "refused GPU inference because sensor") + && king_inference_error_ascii_contains(message, "at or above")) { + return "king.policy.gpu_thermal_limit_reached"; + } + if (king_inference_error_ascii_contains(message, "power draw") + && king_inference_error_ascii_contains(message, "at or above")) { + return "king.policy.gpu_power_limit_reached"; + } + if (king_inference_error_ascii_contains(message, "timeout") + || king_inference_error_ascii_contains(message, "deadline")) { + return "king.timeout.deadline_exceeded"; + } + if (king_inference_error_ascii_contains(message, "cancel")) { + return "king.cancellation.cancelled"; + } + if (king_inference_error_ascii_contains(message, "unsupported") + || king_inference_error_ascii_contains(message, "does not support") + || king_inference_error_ascii_contains(message, "requires") + || king_inference_error_ascii_contains(message, "not found")) { + return "king.unsupported_feature.requested"; + } + if (king_inference_error_ascii_contains(message, "kernel") + || king_inference_error_ascii_contains(message, "CUDA") + || king_inference_error_ascii_contains(message, "decoder")) { + return "king.kernel.unavailable"; + } + } + + if (type != NULL && strcmp(type, "invalid_request_error") == 0) { + return "king.validation.invalid_request"; + } + if (status >= 500 || (type != NULL && strcmp(type, "server_error") == 0)) { + return "king.router.internal"; + } + + return "king.router.internal"; +} + +static const char *king_inference_error_code_from_explicit_or_message( + zend_long status, + const char *type, + const char *message, + const char *explicit_code) +{ + if (king_inference_error_code_is_stable(explicit_code)) { + return explicit_code; + } + + return king_inference_error_code_from_message(status, type, message); +} + +static void king_inference_error_taxonomy_add_unique_string(zval *values, const char *value) +{ + zval *entry; + + if (value == NULL || value[0] == '\0') { + return; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(values), entry) { + if (Z_TYPE_P(entry) == IS_STRING && strcmp(Z_STRVAL_P(entry), value) == 0) { + return; + } + } ZEND_HASH_FOREACH_END(); + + add_next_index_string(values, value); +} + +static void king_inference_error_taxonomy_entry(zval *return_value, const char *code) +{ + array_init(return_value); + add_assoc_string(return_value, "code", code); + add_assoc_string(return_value, "category", king_inference_error_category_from_code(code)); +} + +static void king_inference_error_taxonomy_catalog_array(zval *return_value) +{ + static const char *codes[] = { + "king.validation.invalid_request", + "king.validation.invalid_backend_config", + "king.model_artifact.missing", + "king.model_artifact.unreadable", + "king.tokenizer.unavailable", + "king.graph.invalid_request", + "king.graph.unsupported_payload", + "king.allocation.failed", + "king.kernel.unavailable", + "king.numeric_mismatch.detected", + "king.kv_cache.invalid", + "king.policy.refused", + "king.policy.gpu_thermal_limit_reached", + "king.policy.gpu_power_limit_reached", + "king.router.internal", + "king.router.route_not_found", + "king.router.method_not_allowed", + "king.timeout.deadline_exceeded", + "king.cancellation.cancelled", + "king.unsupported_feature.requested", + }; + zval categories; + zval code_list; + + array_init(return_value); + add_assoc_long(return_value, "schema_version", 1); + add_assoc_bool(return_value, "prompt_included", false); + add_assoc_bool(return_value, "request_body_included", false); + add_assoc_bool(return_value, "safe_for_model_list_diagnostics", true); + + array_init(&categories); + for (size_t i = 0; i < sizeof(codes) / sizeof(codes[0]); i++) { + king_inference_error_taxonomy_add_unique_string( + &categories, + king_inference_error_category_from_code(codes[i]) + ); + } + add_assoc_zval(return_value, "categories", &categories); + + array_init(&code_list); + for (size_t i = 0; i < sizeof(codes) / sizeof(codes[0]); i++) { + zval entry; + king_inference_error_taxonomy_entry(&entry, codes[i]); + add_next_index_zval(&code_list, &entry); + } + add_assoc_zval(return_value, "codes", &code_list); +} + +static void king_inference_error_taxonomy_codes_from_reason_list( + zval *reasons, + zval *return_value) +{ + zval *entry; + + array_init(return_value); + if (reasons == NULL || Z_TYPE_P(reasons) != IS_ARRAY) { + return; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(reasons), entry) { + if (Z_TYPE_P(entry) == IS_STRING && Z_STRLEN_P(entry) > 0) { + king_inference_error_taxonomy_add_unique_string( + return_value, + king_inference_error_code_from_runtime_reason(Z_STRVAL_P(entry)) + ); + } + } ZEND_HASH_FOREACH_END(); +} + +static void king_inference_error_taxonomy_categories_from_code_list( + zval *codes, + zval *return_value) +{ + zval *entry; + + array_init(return_value); + if (codes == NULL || Z_TYPE_P(codes) != IS_ARRAY) { + return; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(codes), entry) { + if (Z_TYPE_P(entry) == IS_STRING && Z_STRLEN_P(entry) > 0) { + king_inference_error_taxonomy_add_unique_string( + return_value, + king_inference_error_category_from_code(Z_STRVAL_P(entry)) + ); + } + } ZEND_HASH_FOREACH_END(); +} diff --git a/extension/src/inference/runtime/events/stream_events.inc b/extension/src/inference/runtime/events/stream_events.inc new file mode 100644 index 000000000..337ee45d4 --- /dev/null +++ b/extension/src/inference/runtime/events/stream_events.inc @@ -0,0 +1,743 @@ +#include "stream_process_close.inc" + +static bool king_inference_stream_has_native_events(king_inference_stream_object *stream) +{ + return !Z_ISUNDEF(stream->native_events) && Z_TYPE(stream->native_events) == IS_ARRAY; +} + +#include "stream_native_lazy.inc" + +static zend_long king_inference_runtime_monotonic_ms(void) +{ + struct timespec ts; + + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) { + return (zend_long) time(NULL) * 1000; + } + return ((zend_long) ts.tv_sec * 1000) + ((zend_long) ts.tv_nsec / 1000000); +} + +#include "stream_hotpath_metrics.inc" + +static void king_inference_stream_model_runtime_started(king_inference_stream_object *stream) +{ + king_inference_model_object *model; + + stream->runtime_started_ms = king_inference_runtime_monotonic_ms(); + stream->runtime_first_token_ms = 0; + stream->runtime_finished_ms = 0; + stream->runtime_generated_tokens = 0; + if (Z_ISUNDEF(stream->model) || Z_TYPE(stream->model) != IS_OBJECT) { + return; + } + + model = php_king_inference_model_obj_from_zend(Z_OBJ(stream->model)); + model->runtime_last_started_ms = stream->runtime_started_ms; + model->runtime_last_first_token_ms = 0; + model->runtime_last_finished_ms = 0; + model->runtime_last_duration_ms = 0; + model->runtime_last_time_to_first_token_ms = 0; + model->runtime_last_generated_tokens = 0; + model->runtime_last_tokens_per_second = 0.0; + model->runtime_last_timing_available = false; + model->runtime_last_first_token_available = false; + model->cuda_decoder_prompt_loop_last_prefill_tokens = 0; + model->cuda_decoder_prompt_loop_last_prefill_ns = 0; + king_inference_cuda_decoder_prompt_loop_reset_hot_token_profile(model); +} + +static void king_inference_stream_model_runtime_token(king_inference_stream_object *stream) +{ + king_inference_model_object *model; + zend_long now = king_inference_runtime_monotonic_ms(); + + if (stream->runtime_started_ms <= 0) { + stream->runtime_started_ms = now; + } + if (stream->runtime_first_token_ms <= 0) { + stream->runtime_first_token_ms = now; + } + stream->runtime_generated_tokens++; + if (Z_ISUNDEF(stream->model) || Z_TYPE(stream->model) != IS_OBJECT) { + return; + } + + model = php_king_inference_model_obj_from_zend(Z_OBJ(stream->model)); + model->runtime_last_started_ms = stream->runtime_started_ms; + model->runtime_last_first_token_ms = stream->runtime_first_token_ms; + model->runtime_last_time_to_first_token_ms = stream->runtime_first_token_ms - stream->runtime_started_ms; + model->runtime_last_generated_tokens = stream->runtime_generated_tokens; + model->runtime_last_first_token_available = true; +} + +static void king_inference_stream_model_runtime_finished(king_inference_stream_object *stream) +{ + king_inference_model_object *model; + zend_long duration_ms; + + if (stream->runtime_finished_ms <= 0) { + stream->runtime_finished_ms = king_inference_runtime_monotonic_ms(); + } + if (stream->runtime_started_ms <= 0) { + stream->runtime_started_ms = stream->runtime_finished_ms; + } + duration_ms = stream->runtime_finished_ms - stream->runtime_started_ms; + if (duration_ms < 0) { + duration_ms = 0; + } + if (Z_ISUNDEF(stream->model) || Z_TYPE(stream->model) != IS_OBJECT) { + return; + } + + model = php_king_inference_model_obj_from_zend(Z_OBJ(stream->model)); + model->runtime_last_started_ms = stream->runtime_started_ms; + model->runtime_last_first_token_ms = stream->runtime_first_token_ms; + model->runtime_last_finished_ms = stream->runtime_finished_ms; + model->runtime_last_duration_ms = duration_ms; + model->runtime_last_time_to_first_token_ms = stream->runtime_first_token_ms > 0 + ? stream->runtime_first_token_ms - stream->runtime_started_ms + : 0; + model->runtime_last_generated_tokens = stream->runtime_generated_tokens; + model->runtime_last_tokens_per_second = duration_ms > 0 + ? ((double) stream->runtime_generated_tokens * 1000.0) / (double) duration_ms + : 0.0; + model->runtime_last_timing_available = true; + model->runtime_last_first_token_available = stream->runtime_first_token_ms > 0; +} + +static void king_inference_stream_reset_decoder_metrics(king_inference_stream_object *stream) +{ + stream->native_decoder_token_count = 0; + stream->native_decoder_last_token_id = 0; + stream->native_decoder_last_probability = 0.0; + stream->native_decoder_last_logit = 0.0; + stream->native_decoder_last_rank = 0.0; + stream->native_decoder_last_token_available = false; + stream->native_decoder_last_score_available = false; +} + +static void king_inference_stream_abort_state(king_inference_stream_object *stream) +{ + stream->start_event_pending = false; + if (king_inference_stream_has_native_events(stream)) { + stream->native_event_index = (zend_ulong) zend_hash_num_elements(Z_ARRVAL(stream->native_events)); + } + king_inference_cuda_decoder_prompt_stream_release(stream); + king_inference_stream_close_process(stream, true); + stream->done = true; + king_inference_stream_model_runtime_finished(stream); +} + +static void king_inference_stream_cancel_state(king_inference_stream_object *stream) +{ + stream->cancelled = true; + stream->timed_out = false; + king_inference_stream_abort_state(stream); +} + +static void king_inference_stream_timeout_state(king_inference_stream_object *stream) +{ + stream->cancelled = true; + stream->timed_out = true; + king_inference_stream_abort_state(stream); +} + +#include "stream_gpu_policy_status.inc" + +static void king_inference_stream_openai_chunk( + king_inference_stream_object *stream, + const char *role, + const char *content, + size_t content_len, + const char *finish_reason, + zval *return_value +); + +static void king_inference_stream_openai_error( + king_inference_stream_object *stream, + const char *message, + size_t message_len, + zval *return_value +); + +static void king_inference_stream_done_event(king_inference_stream_object *stream, zval *return_value); + +static bool king_inference_stream_openai_array_event( + king_inference_stream_object *stream, + zval *event, + zval *return_value +) { + zval *type = king_inference_array_find(event, "type"); + zval *text = king_inference_array_find(event, "text"); + zval *message = king_inference_array_find(event, "message"); + const char *type_name = type != NULL && Z_TYPE_P(type) == IS_STRING ? Z_STRVAL_P(type) : ""; + + if (strcmp(type_name, "token") == 0 && text != NULL && Z_TYPE_P(text) == IS_STRING) { + stream->chunk_count++; + stream->bytes_emitted += (zend_long) Z_STRLEN_P(text); + king_inference_stream_model_runtime_token(stream); + king_inference_stream_openai_chunk(stream, NULL, Z_STRVAL_P(text), Z_STRLEN_P(text), NULL, return_value); + return true; + } + if ((strcmp(type_name, "stderr") == 0 || strcmp(type_name, "error") == 0) + && ((text != NULL && Z_TYPE_P(text) == IS_STRING) || (message != NULL && Z_TYPE_P(message) == IS_STRING))) { + zval *error_text = text != NULL && Z_TYPE_P(text) == IS_STRING ? text : message; + king_inference_stream_openai_error(stream, Z_STRVAL_P(error_text), Z_STRLEN_P(error_text), return_value); + return true; + } + if (strcmp(type_name, "done") == 0 || strcmp(type_name, "cancelled") == 0) { + king_inference_stream_done_event(stream, return_value); + return true; + } + + return false; +} + +static zend_result king_inference_stream_next_native_event( + king_inference_stream_object *stream, + zval *return_value +) { + zval *event; + + if (!king_inference_stream_has_native_events(stream)) { + return FAILURE; + } + + while (true) { + event = zend_hash_index_find(Z_ARRVAL(stream->native_events), stream->native_event_index); + if (event == NULL) { + stream->exit_code = 0; + king_inference_stream_done_event(stream, return_value); + return SUCCESS; + } + + stream->native_event_index++; + if (Z_TYPE_P(event) == IS_STRING) { + stream->chunk_count++; + stream->bytes_emitted += (zend_long) Z_STRLEN_P(event); + king_inference_stream_model_runtime_token(stream); + if (stream->openai_compatible) { + king_inference_stream_openai_chunk(stream, NULL, Z_STRVAL_P(event), Z_STRLEN_P(event), NULL, return_value); + return SUCCESS; + } + + array_init(return_value); + add_assoc_string(return_value, "type", "token"); + add_assoc_str(return_value, "text", zend_string_copy(Z_STR_P(event))); + return SUCCESS; + } + if (stream->openai_compatible) { + if (Z_TYPE_P(event) == IS_ARRAY && king_inference_stream_openai_array_event(stream, event, return_value)) { + return SUCCESS; + } + continue; + } + + ZVAL_COPY(return_value, event); + return SUCCESS; + } +} + +static bool king_inference_stream_openai_requested(zval *request, zval *options) +{ + zval *flag = king_inference_array_find(options, "openai_compatible"); + zval *format; + + if (flag != NULL && zend_is_true(flag)) { + return true; + } + flag = king_inference_array_find(request, "openai_compatible"); + if (flag != NULL && zend_is_true(flag)) { + return true; + } + + format = king_inference_array_find(options, "format"); + if (format == NULL) { + format = king_inference_array_find(request, "format"); + } + if (format != NULL && Z_TYPE_P(format) == IS_STRING) { + return zend_string_equals_literal(Z_STR_P(format), "openai") + || zend_string_equals_literal(Z_STR_P(format), "openai_chat") + || zend_string_equals_literal(Z_STR_P(format), "openai_chat_completions"); + } + + return false; +} + +static zend_result king_inference_stream_validate_openai_flag( + zval *source, + const char *source_name, + const char *function_name +) { + zval *flag = king_inference_array_find(source, "openai_compatible"); + + if (flag == NULL || Z_TYPE_P(flag) == IS_NULL) { + return SUCCESS; + } + if (Z_TYPE_P(flag) == IS_TRUE || Z_TYPE_P(flag) == IS_FALSE) { + return SUCCESS; + } + + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() %s.openai_compatible must be a boolean when provided.", + function_name, + source_name + ); + return FAILURE; +} + +static zend_result king_inference_stream_validate_format( + zval *source, + const char *source_name, + const char *function_name +) { + zval *format = king_inference_array_find(source, "format"); + + if (format == NULL || Z_TYPE_P(format) == IS_NULL) { + return SUCCESS; + } + if (Z_TYPE_P(format) != IS_STRING || Z_STRLEN_P(format) == 0) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() %s.format must be a non-empty string when provided.", + function_name, + source_name + ); + return FAILURE; + } + if (zend_string_equals_literal(Z_STR_P(format), "openai") + || zend_string_equals_literal(Z_STR_P(format), "openai_chat") + || zend_string_equals_literal(Z_STR_P(format), "openai_chat_completions")) { + return SUCCESS; + } + + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() %s.format is not a supported inference stream format.", + function_name, + source_name + ); + return FAILURE; +} + +static zend_result king_inference_stream_validate_native_graph_shapes( + zval *source, + const char *source_name, + const char *function_name +) { + zval *graph = king_inference_array_find(source, "graph"); + zval *graphs = king_inference_array_find(source, "graphs"); + zval *graph_options = king_inference_array_find(source, "graph_options"); + + if (graph != NULL && !king_inference_openai_array_is_object(graph)) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() %s.graph must be an object array when provided.", + function_name, + source_name + ); + return FAILURE; + } + if (graphs != NULL && !king_inference_openai_array_is_list(graphs)) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() %s.graphs must be a list array when provided.", + function_name, + source_name + ); + return FAILURE; + } + if (graph_options != NULL && !king_inference_openai_array_is_object(graph_options)) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() %s.graph_options must be an object array when provided.", + function_name, + source_name + ); + return FAILURE; + } + + return SUCCESS; +} + +static zend_result king_inference_stream_validate_request_options( + zval *request, + zval *options, + const char *function_name +) { + if (king_inference_stream_validate_openai_flag(request, "request", function_name) != SUCCESS + || king_inference_stream_validate_openai_flag(options, "options", function_name) != SUCCESS + || king_inference_stream_validate_format(request, "request", function_name) != SUCCESS + || king_inference_stream_validate_format(options, "options", function_name) != SUCCESS + || king_inference_stream_validate_native_graph_shapes(request, "request", function_name) != SUCCESS + || king_inference_stream_validate_native_graph_shapes(options, "options", function_name) != SUCCESS) { + return FAILURE; + } + + return SUCCESS; +} + +static void king_inference_stream_prepare_openai(king_inference_stream_object *stream) +{ + if (stream->response_id != NULL) { + zend_string_release(stream->response_id); + stream->response_id = NULL; + } + + stream->created_at = (zend_long) time(NULL); + stream->response_id = strpprintf( + 0, + "chatcmpl-king-%lu-" ZEND_LONG_FMT, + (zend_ulong) stream->std.handle, + stream->created_at + ); +} + +static zend_string *king_inference_stream_openai_model_name(king_inference_stream_object *stream) +{ + zval *request_model = king_inference_array_find(&stream->request, "model"); + + if (request_model != NULL && Z_TYPE_P(request_model) == IS_STRING && Z_STRLEN_P(request_model) > 0) { + return zend_string_copy(Z_STR_P(request_model)); + } + + if (!Z_ISUNDEF(stream->model) && Z_TYPE(stream->model) == IS_OBJECT) { + king_inference_model_object *model = php_king_inference_model_obj_from_zend(Z_OBJ(stream->model)); + if (model->name != NULL) { + return zend_string_copy(model->name); + } + } + + return zend_string_init("king-native", sizeof("king-native") - 1, 0); +} + +static void king_inference_stream_openai_chunk_base( + king_inference_stream_object *stream, + zval *return_value +) { + zend_string *model_name = king_inference_stream_openai_model_name(stream); + + if (stream->response_id == NULL) { + king_inference_stream_prepare_openai(stream); + } + + array_init(return_value); + add_assoc_str(return_value, "id", zend_string_copy(stream->response_id)); + add_assoc_string(return_value, "object", "chat.completion.chunk"); + add_assoc_long(return_value, "created", stream->created_at); + add_assoc_str(return_value, "model", model_name); +} + +static void king_inference_stream_openai_chunk( + king_inference_stream_object *stream, + const char *role, + const char *content, + size_t content_len, + const char *finish_reason, + zval *return_value +) { + zval choices; + zval choice; + zval delta; + + king_inference_stream_openai_chunk_base(stream, return_value); + array_init(&choices); + array_init(&choice); + add_assoc_long(&choice, "index", 0); + array_init(&delta); + if (role != NULL) { + add_assoc_string(&delta, "role", role); + } + if (content != NULL) { + add_assoc_stringl(&delta, "content", content, content_len); + } else if (role == NULL && finish_reason != NULL) { + add_assoc_stringl(&delta, "content", "", 0); + } + add_assoc_zval(&choice, "delta", &delta); + if (finish_reason != NULL) { + add_assoc_string(&choice, "finish_reason", finish_reason); + } else { + add_assoc_null(&choice, "finish_reason"); + } + add_next_index_zval(&choices, &choice); + add_assoc_zval(return_value, "choices", &choices); + if (king_inference_openai_stream_usage_requested(&stream->request)) { + add_assoc_null(return_value, "usage"); + } +} + +static void king_inference_stream_openai_error( + king_inference_stream_object *stream, + const char *message, + size_t message_len, + zval *return_value +) { + zval error; + zend_string *safe_message = zend_string_init(message != NULL ? message : "", message != NULL ? message_len : 0, 0); + + king_inference_stream_openai_chunk_base(stream, return_value); + add_assoc_string(return_value, "object", "error"); + king_inference_openai_error_object( + &error, + 500, + ZSTR_VAL(safe_message), + "server_error", + NULL + ); + add_assoc_zval(return_value, "error", &error); + zend_string_release(safe_message); +} + +static void king_inference_stream_metrics(king_inference_stream_object *stream, zval *return_value) +{ + zend_long native_event_count = king_inference_stream_has_native_events(stream) + ? (zend_long) zend_hash_num_elements(Z_ARRVAL(stream->native_events)) + : 0; + + array_init(return_value); + add_assoc_long(return_value, "chunks", stream->chunk_count); + add_assoc_long(return_value, "stderr_chunks", stream->stderr_count); + add_assoc_long(return_value, "bytes", stream->bytes_emitted); + king_inference_stream_add_hotpath_metrics(stream, return_value); + add_assoc_bool(return_value, "done", stream->done); + add_assoc_bool(return_value, "cancelled", stream->cancelled); + add_assoc_bool(return_value, "timed_out", stream->timed_out); + king_inference_stream_add_gpu_policy_metrics(stream, return_value); + add_assoc_long(return_value, "exit_code", stream->exit_code); + add_assoc_bool(return_value, "openai_compatible", stream->openai_compatible); + add_assoc_bool(return_value, "native_stream", native_event_count > 0); + add_assoc_long(return_value, "native_event_count", native_event_count); + add_assoc_long(return_value, "native_event_index", (zend_long) stream->native_event_index); + add_assoc_long(return_value, "native_decoder_tokens", stream->native_decoder_token_count); + if (stream->native_decoder_last_token_available) { + if (stream->native_decoder_last_token_id <= (zend_ulong) ZEND_LONG_MAX) { + add_assoc_long(return_value, "native_decoder_last_token_id", (zend_long) stream->native_decoder_last_token_id); + } else { + add_assoc_double(return_value, "native_decoder_last_token_id", (double) stream->native_decoder_last_token_id); + } + } else { + add_assoc_null(return_value, "native_decoder_last_token_id"); + } + if (stream->native_decoder_last_score_available) { + add_assoc_double(return_value, "native_decoder_last_probability", stream->native_decoder_last_probability); + add_assoc_double(return_value, "native_decoder_last_logit", stream->native_decoder_last_logit); + add_assoc_double(return_value, "native_decoder_last_rank", stream->native_decoder_last_rank); + } else { + add_assoc_null(return_value, "native_decoder_last_probability"); + add_assoc_null(return_value, "native_decoder_last_logit"); + add_assoc_null(return_value, "native_decoder_last_rank"); + } +} + +static const char *king_inference_stream_openai_finish_reason(king_inference_stream_object *stream) +{ + zval *max_tokens; + + if (stream->cancelled || stream->gpu_thermal_aborted || stream->gpu_power_aborted) { + return "stop"; + } + + max_tokens = king_inference_array_find(&stream->request, "max_tokens"); + if (max_tokens != NULL + && Z_TYPE_P(max_tokens) == IS_LONG + && Z_LVAL_P(max_tokens) > 0 + && stream->runtime_generated_tokens >= Z_LVAL_P(max_tokens)) { + return "length"; + } + + return "stop"; +} + +static void king_inference_stream_done_event(king_inference_stream_object *stream, zval *return_value) +{ + int status = 0; + pid_t waited; + + if (stream->child_pid > 0) { + waited = waitpid((pid_t) stream->child_pid, &status, WNOHANG); + if (waited == (pid_t) stream->child_pid) { + stream->exit_code = WIFEXITED(status) ? WEXITSTATUS(status) : -1; + stream->child_pid = -1; + } + } + + stream->done = true; + king_inference_stream_model_runtime_finished(stream); + if (stream->openai_compatible) { + king_inference_stream_openai_chunk( + stream, + NULL, + NULL, + 0, + king_inference_stream_openai_finish_reason(stream), + return_value + ); + return; + } + + array_init(return_value); + add_assoc_string(return_value, "type", stream->cancelled ? "cancelled" : "done"); + add_assoc_long(return_value, "exit_code", stream->exit_code); + add_assoc_long(return_value, "chunks", stream->chunk_count); + add_assoc_long(return_value, "bytes", stream->bytes_emitted); + add_assoc_bool(return_value, "timed_out", stream->timed_out); + king_inference_stream_add_gpu_policy_runtime_metrics(stream, return_value); + king_inference_stream_add_gpu_policy_abort_metrics(stream, return_value); +} + +static zend_result king_inference_stream_next_event( + king_inference_stream_object *stream, + zend_long timeout_ms, + zval *return_value +) { + king_inference_model_object *model = NULL; + fd_set readfds; + struct timeval timeout; + int maxfd = -1; + int ready; + char buffer[4096]; + ssize_t read_len; + + if (stream->start_event_pending) { + king_inference_backend_kind kind = KING_INFERENCE_BACKEND_LOCAL; + + stream->start_event_pending = false; + if (!Z_ISUNDEF(stream->model) && Z_TYPE(stream->model) == IS_OBJECT) { + model = php_king_inference_model_obj_from_zend(Z_OBJ(stream->model)); + if (king_inference_backend_kind_from_config(&model->config, &kind, "King inference stream") != SUCCESS) { + return FAILURE; + } + } + if (stream->openai_compatible) { + if (king_inference_stream_has_native_events(stream)) { + if (model != NULL + && king_inference_check_gpu_policy_during_run(stream, "King inference stream") != SUCCESS) { + king_inference_stream_gpu_policy_abort_state(stream); + return FAILURE; + } + if (king_inference_stream_ensure_native_event_available(stream) != SUCCESS) { + if (king_inference_stream_gpu_policy_abort_code(stream) != NULL) { + king_inference_stream_gpu_policy_abort_state(stream); + } + return FAILURE; + } + return king_inference_stream_next_native_event(stream, return_value); + } + king_inference_stream_openai_chunk(stream, "assistant", NULL, 0, NULL, return_value); + return SUCCESS; + } + array_init(return_value); + add_assoc_string(return_value, "type", "start"); + add_assoc_string( + return_value, + "backend", + king_inference_backend_kind_name(kind) + ); + add_assoc_bool(return_value, "native_stream", king_inference_stream_has_native_events(stream)); + add_assoc_long(return_value, "pid", stream->child_pid); + king_inference_stream_add_gpu_policy_start_status(stream, return_value); + return SUCCESS; + } + + if (stream->done) { + ZVAL_NULL(return_value); + return SUCCESS; + } + + if (!Z_ISUNDEF(stream->model) && Z_TYPE(stream->model) == IS_OBJECT) { + model = php_king_inference_model_obj_from_zend(Z_OBJ(stream->model)); + if (king_inference_check_gpu_policy_during_run(stream, "King inference stream") != SUCCESS) { + king_inference_stream_gpu_policy_abort_state(stream); + return FAILURE; + } + } + + if (king_inference_stream_has_native_events(stream)) { + if (king_inference_stream_ensure_native_event_available(stream) != SUCCESS) { + if (king_inference_stream_gpu_policy_abort_code(stream) != NULL) { + king_inference_stream_gpu_policy_abort_state(stream); + } + return FAILURE; + } + return king_inference_stream_next_native_event(stream, return_value); + } + + if (stream->stdout_fd < 0 && stream->stderr_fd < 0) { + king_inference_stream_done_event(stream, return_value); + return SUCCESS; + } + + FD_ZERO(&readfds); + if (stream->stdout_fd >= 0) { + FD_SET(stream->stdout_fd, &readfds); + maxfd = stream->stdout_fd; + } + if (stream->stderr_fd >= 0) { + FD_SET(stream->stderr_fd, &readfds); + if (stream->stderr_fd > maxfd) { + maxfd = stream->stderr_fd; + } + } + + timeout.tv_sec = timeout_ms < 0 ? 0 : timeout_ms / 1000; + timeout.tv_usec = timeout_ms < 0 ? 0 : (timeout_ms % 1000) * 1000; + ready = select(maxfd + 1, &readfds, NULL, NULL, timeout_ms < 0 ? NULL : &timeout); + if (ready < 0) { + if (errno == EINTR) { + ZVAL_NULL(return_value); + return SUCCESS; + } + zend_throw_exception_ex(king_ce_runtime_exception, 0, "Inference stream select failed."); + return FAILURE; + } + if (ready == 0) { + ZVAL_NULL(return_value); + return SUCCESS; + } + + if (stream->stderr_fd >= 0 && FD_ISSET(stream->stderr_fd, &readfds)) { + read_len = read(stream->stderr_fd, buffer, sizeof(buffer)); + if (read_len > 0) { + stream->stderr_count++; + if (stream->openai_compatible) { + king_inference_stream_openai_error(stream, buffer, (size_t) read_len, return_value); + return SUCCESS; + } + array_init(return_value); + add_assoc_string(return_value, "type", "stderr"); + add_assoc_stringl(return_value, "text", buffer, (size_t) read_len); + return SUCCESS; + } + close(stream->stderr_fd); + stream->stderr_fd = -1; + } + + if (stream->stdout_fd >= 0 && FD_ISSET(stream->stdout_fd, &readfds)) { + read_len = read(stream->stdout_fd, buffer, sizeof(buffer)); + if (read_len > 0) { + stream->chunk_count++; + stream->bytes_emitted += read_len; + king_inference_stream_model_runtime_token(stream); + if (stream->openai_compatible) { + king_inference_stream_openai_chunk(stream, NULL, buffer, (size_t) read_len, NULL, return_value); + return SUCCESS; + } + array_init(return_value); + add_assoc_string(return_value, "type", "token"); + add_assoc_stringl(return_value, "text", buffer, (size_t) read_len); + return SUCCESS; + } + close(stream->stdout_fd); + stream->stdout_fd = -1; + } + + ZVAL_NULL(return_value); + return SUCCESS; +} diff --git a/extension/src/inference/runtime/events/stream_gpu_policy_status.inc b/extension/src/inference/runtime/events/stream_gpu_policy_status.inc new file mode 100644 index 000000000..b7b313292 --- /dev/null +++ b/extension/src/inference/runtime/events/stream_gpu_policy_status.inc @@ -0,0 +1,186 @@ +/* + * GPU policy status projection for inference streams. + */ + +static const char *king_inference_stream_gpu_policy_abort_code(king_inference_stream_object *stream) +{ + if (stream->gpu_thermal_aborted) { + return "king.policy.gpu_thermal_limit_reached"; + } + if (stream->gpu_power_aborted) { + return "king.policy.gpu_power_limit_reached"; + } + return NULL; +} + +static void king_inference_stream_gpu_policy_abort_state(king_inference_stream_object *stream) +{ + stream->cancelled = true; + stream->timed_out = false; + king_inference_stream_abort_state(stream); +} + +static zval *king_inference_stream_gpu_policy_config(king_inference_stream_object *stream) +{ + king_inference_model_object *model; + zval *gpu; + + if (stream == NULL || Z_ISUNDEF(stream->model) || Z_TYPE(stream->model) != IS_OBJECT) { + return NULL; + } + + model = php_king_inference_model_obj_from_zend(Z_OBJ(stream->model)); + gpu = king_inference_array_find(&model->config, "gpu"); + return gpu != NULL && Z_TYPE_P(gpu) == IS_ARRAY ? gpu : NULL; +} + +static void king_inference_stream_add_gpu_policy_config( + king_inference_stream_object *stream, + zval *return_value +) { + king_inference_model_object *model = NULL; + zval *gpu = king_inference_stream_gpu_policy_config(stream); + zval *thermal = gpu != NULL ? king_inference_array_find(gpu, "thermal") : NULL; + zval *power = gpu != NULL ? king_inference_array_find(gpu, "power") : NULL; + zval *thermal_path = thermal != NULL && Z_TYPE_P(thermal) == IS_ARRAY + ? king_inference_array_find(thermal, "sensor_path") + : NULL; + zval *thermal_command = thermal != NULL && Z_TYPE_P(thermal) == IS_ARRAY + ? king_inference_array_find(thermal, "sensor_command") + : NULL; + zval *power_command = power != NULL && Z_TYPE_P(power) == IS_ARRAY + ? king_inference_array_find(power, "sensor_command") + : NULL; + const char *thermal_path_string = thermal_path != NULL && Z_TYPE_P(thermal_path) == IS_STRING + ? Z_STRVAL_P(thermal_path) + : ""; + const char *thermal_command_string = thermal_command != NULL && Z_TYPE_P(thermal_command) == IS_STRING + ? Z_STRVAL_P(thermal_command) + : ""; + const char *power_command_string = power_command != NULL && Z_TYPE_P(power_command) == IS_STRING + ? Z_STRVAL_P(power_command) + : ( + king_high_perf_compute_ai_config.inference_gpu_power_sensor_command != NULL + ? king_high_perf_compute_ai_config.inference_gpu_power_sensor_command + : "" + ); + const char *thermal_source = thermal_path_string[0] != '\0' + ? "path" + : (thermal_command_string[0] != '\0' ? "command" : "none"); + bool gpu_enabled = gpu != NULL && king_inference_bool_from_array(gpu, "enabled", false); + double max_temperature_c = king_high_perf_compute_ai_config.inference_gpu_thermal_max_temperature_c; + double max_watts = king_high_perf_compute_ai_config.inference_gpu_power_max_watts; + zend_long thermal_interval = king_high_perf_compute_ai_config.inference_gpu_thermal_check_interval_sec; + zend_long power_interval = king_high_perf_compute_ai_config.inference_gpu_power_check_interval_sec; + + if (stream != NULL && !Z_ISUNDEF(stream->model) && Z_TYPE(stream->model) == IS_OBJECT) { + model = php_king_inference_model_obj_from_zend(Z_OBJ(stream->model)); + max_temperature_c = king_inference_gpu_thermal_max_temperature_c(&model->config); + max_watts = king_inference_gpu_power_max_watts(&model->config); + thermal_interval = king_inference_gpu_thermal_check_interval_seconds(&model->config); + power_interval = king_inference_gpu_power_check_interval_seconds(&model->config); + } + + add_assoc_bool(return_value, "gpu_policy_configured", gpu_enabled); + add_assoc_string(return_value, "gpu_thermal_sensor_source", thermal_source); + add_assoc_string(return_value, "gpu_thermal_sensor_path", thermal_path_string); + add_assoc_string(return_value, "gpu_thermal_sensor_command", thermal_command_string); + add_assoc_double(return_value, "gpu_thermal_max_temperature_c", max_temperature_c); + add_assoc_long(return_value, "gpu_thermal_check_interval_seconds", thermal_interval); + add_assoc_bool(return_value, "gpu_power_limit_enabled", max_watts > 0.0); + add_assoc_string(return_value, "gpu_power_sensor_command", power_command_string); + add_assoc_double(return_value, "gpu_power_max_watts", max_watts); + add_assoc_long(return_value, "gpu_power_check_interval_seconds", power_interval); +} + +static void king_inference_stream_add_gpu_policy_runtime_metrics( + king_inference_stream_object *stream, + zval *return_value +) { + const char *abort_code = king_inference_stream_gpu_policy_abort_code(stream); + + add_assoc_bool(return_value, "gpu_guardrail_aborted", abort_code != NULL); + if (abort_code != NULL) { + add_assoc_string(return_value, "gpu_policy_abort_code", abort_code); + } else { + add_assoc_null(return_value, "gpu_policy_abort_code"); + } + add_assoc_long(return_value, "gpu_policy_during_run_checks", stream->gpu_policy_during_run_checks); + add_assoc_long(return_value, "gpu_policy_interval_skips", stream->gpu_policy_interval_skips); + add_assoc_long(return_value, "gpu_thermal_runtime_checks", stream->gpu_thermal_runtime_checks); + add_assoc_long(return_value, "gpu_power_runtime_checks", stream->gpu_power_runtime_checks); +} + +static void king_inference_stream_add_gpu_policy_abort_metrics( + king_inference_stream_object *stream, + zval *return_value +) { + add_assoc_bool(return_value, "gpu_thermal_aborted", stream->gpu_thermal_aborted); + add_assoc_long(return_value, "gpu_thermal_abort_at", stream->gpu_thermal_abort_at); + if (stream->gpu_thermal_aborted) { + add_assoc_double(return_value, "gpu_thermal_abort_temperature_c", stream->gpu_thermal_abort_temperature_c); + add_assoc_double(return_value, "gpu_thermal_abort_ceiling_c", stream->gpu_thermal_abort_ceiling_c); + } else { + add_assoc_null(return_value, "gpu_thermal_abort_temperature_c"); + add_assoc_null(return_value, "gpu_thermal_abort_ceiling_c"); + } + + add_assoc_bool(return_value, "gpu_power_aborted", stream->gpu_power_aborted); + add_assoc_long(return_value, "gpu_power_abort_at", stream->gpu_power_abort_at); + if (stream->gpu_power_aborted) { + add_assoc_double(return_value, "gpu_power_abort_watts", stream->gpu_power_abort_watts); + add_assoc_double(return_value, "gpu_power_abort_ceiling_watts", stream->gpu_power_abort_ceiling_watts); + } else { + add_assoc_null(return_value, "gpu_power_abort_watts"); + add_assoc_null(return_value, "gpu_power_abort_ceiling_watts"); + } +} + +static void king_inference_stream_add_gpu_policy_preflight_metrics( + king_inference_stream_object *stream, + zval *return_value +) { + add_assoc_bool(return_value, "gpu_thermal_preflight_checked", stream->gpu_thermal_preflight_checked); + add_assoc_long(return_value, "gpu_thermal_preflight_at", stream->gpu_thermal_preflight_at); + add_assoc_bool( + return_value, + "gpu_thermal_preflight_temperature_available", + stream->gpu_thermal_preflight_temperature_available + ); + if (stream->gpu_thermal_preflight_temperature_available) { + add_assoc_double( + return_value, + "gpu_thermal_preflight_temperature_c", + stream->gpu_thermal_preflight_temperature_c + ); + } else { + add_assoc_null(return_value, "gpu_thermal_preflight_temperature_c"); + } + + add_assoc_bool(return_value, "gpu_power_preflight_checked", stream->gpu_power_preflight_checked); + add_assoc_long(return_value, "gpu_power_preflight_at", stream->gpu_power_preflight_at); + add_assoc_bool(return_value, "gpu_power_preflight_available", stream->gpu_power_preflight_available); + if (stream->gpu_power_preflight_available) { + add_assoc_double(return_value, "gpu_power_preflight_watts", stream->gpu_power_preflight_watts); + } else { + add_assoc_null(return_value, "gpu_power_preflight_watts"); + } +} + +static void king_inference_stream_add_gpu_policy_start_status( + king_inference_stream_object *stream, + zval *return_value +) { + king_inference_stream_add_gpu_policy_config(stream, return_value); + king_inference_stream_add_gpu_policy_preflight_metrics(stream, return_value); +} + +static void king_inference_stream_add_gpu_policy_metrics( + king_inference_stream_object *stream, + zval *return_value +) { + king_inference_stream_add_gpu_policy_config(stream, return_value); + king_inference_stream_add_gpu_policy_runtime_metrics(stream, return_value); + king_inference_stream_add_gpu_policy_abort_metrics(stream, return_value); + king_inference_stream_add_gpu_policy_preflight_metrics(stream, return_value); +} diff --git a/extension/src/inference/runtime/events/stream_hotpath_metrics.inc b/extension/src/inference/runtime/events/stream_hotpath_metrics.inc new file mode 100644 index 000000000..9b2997b7d --- /dev/null +++ b/extension/src/inference/runtime/events/stream_hotpath_metrics.inc @@ -0,0 +1,206 @@ +static zend_long king_inference_stream_runtime_duration_ms(king_inference_stream_object *stream) +{ + zend_long finished_ms; + zend_long duration_ms; + + if (stream->runtime_started_ms <= 0) { + return 0; + } + + finished_ms = stream->runtime_finished_ms > 0 + ? stream->runtime_finished_ms + : king_inference_runtime_monotonic_ms(); + duration_ms = finished_ms - stream->runtime_started_ms; + return duration_ms > 0 ? duration_ms : 0; +} + +static double king_inference_stream_rate_per_second(zend_long units, zend_long duration_ms) +{ + if (units <= 0 || duration_ms <= 0) { + return 0.0; + } + + return ((double) units * 1000.0) / (double) duration_ms; +} + +static double king_inference_stream_rate_per_second_ns(zend_long units, zend_long duration_ns) +{ + if (units <= 0 || duration_ns <= 0) { + return 0.0; + } + + return ((double) units * 1000000000.0) / (double) duration_ns; +} + +static king_inference_model_object *king_inference_stream_metric_model(king_inference_stream_object *stream) +{ + if (Z_ISUNDEF(stream->model) || Z_TYPE(stream->model) != IS_OBJECT) { + return NULL; + } + + return php_king_inference_model_obj_from_zend(Z_OBJ(stream->model)); +} + +static void king_inference_stream_add_hotpath_metrics( + king_inference_stream_object *stream, + zval *return_value +) { + king_inference_model_object *model = king_inference_stream_metric_model(stream); + zend_long duration_ms = king_inference_stream_runtime_duration_ms(stream); + zend_long generated_tokens = stream->runtime_generated_tokens; + zend_long first_token_ms = stream->runtime_first_token_ms > 0 && stream->runtime_started_ms > 0 + ? stream->runtime_first_token_ms - stream->runtime_started_ms + : 0; + zend_long prefill_tokens = 0; + zend_long prefill_ns = 0; + bool gpu_profile_available = false; + zval hotpath; + zval gpu; + zval readback; + + if (model != NULL) { + if (model->runtime_last_generated_tokens > generated_tokens) { + generated_tokens = model->runtime_last_generated_tokens; + } + prefill_tokens = (zend_long) model->cuda_decoder_prompt_loop_last_prefill_tokens; + prefill_ns = model->cuda_decoder_prompt_loop_last_prefill_ns; + gpu_profile_available = model->cuda_decoder_prompt_loop_last_hot_token_profile_available; + } + + add_assoc_bool(return_value, "runtime_timing_available", stream->runtime_started_ms > 0); + add_assoc_bool(return_value, "runtime_first_token_available", stream->runtime_first_token_ms > 0); + add_assoc_long(return_value, "runtime_started_ms", stream->runtime_started_ms); + add_assoc_long(return_value, "runtime_first_token_ms", stream->runtime_first_token_ms); + add_assoc_long(return_value, "runtime_finished_ms", stream->runtime_finished_ms); + add_assoc_long(return_value, "runtime_duration_ms", duration_ms); + add_assoc_long(return_value, "runtime_time_to_first_token_ms", first_token_ms); + add_assoc_long(return_value, "runtime_generated_tokens", generated_tokens); + add_assoc_double( + return_value, + "runtime_tokens_per_second", + king_inference_stream_rate_per_second(generated_tokens, duration_ms) + ); + + array_init(&hotpath); + add_assoc_bool(&hotpath, "available", stream->runtime_started_ms > 0 || gpu_profile_available); + add_assoc_long(&hotpath, "time_to_first_token_ms", first_token_ms); + add_assoc_long(&hotpath, "duration_ms", duration_ms); + add_assoc_long(&hotpath, "generated_tokens", generated_tokens); + add_assoc_double( + &hotpath, + "tokens_per_second", + king_inference_stream_rate_per_second(generated_tokens, duration_ms) + ); + add_assoc_long(&hotpath, "prefill_tokens", prefill_tokens); + add_assoc_long(&hotpath, "prefill_duration_ns", prefill_ns); + add_assoc_double( + &hotpath, + "prefill_tokens_per_second", + king_inference_stream_rate_per_second_ns(prefill_tokens, prefill_ns) + ); + + array_init(&gpu); + add_assoc_bool(&gpu, "available", gpu_profile_available); + add_assoc_long( + &gpu, + "kernel_launches", + model != NULL ? (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_launches : 0 + ); + add_assoc_long( + &gpu, + "host_device_syncs", + model != NULL ? (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_syncs : 0 + ); + add_assoc_long( + &gpu, + "device_to_host_copies", + model != NULL ? (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_dtoh_copies : 0 + ); + + array_init(&readback); + add_assoc_long( + &readback, + "candidates", + model != NULL ? (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_readback_candidates : 0 + ); + add_assoc_long( + &readback, + "bytes", + model != NULL ? (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_readback_bytes : 0 + ); + add_assoc_long( + &readback, + "full_logits_bytes", + model != NULL ? (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_full_logits_bytes : 0 + ); + add_assoc_long( + &readback, + "saved_bytes", + model != NULL ? (zend_long) model->cuda_decoder_prompt_loop_last_hot_token_saved_readback_bytes : 0 + ); + add_assoc_long( + &readback, + "request_tokens", + model != NULL ? (zend_long) model->cuda_decoder_prompt_loop_last_readback_tokens : 0 + ); + add_assoc_long( + &readback, + "request_bytes", + model != NULL ? (zend_long) model->cuda_decoder_prompt_loop_last_readback_bytes : 0 + ); + add_assoc_long( + &readback, + "request_full_logits_bytes", + model != NULL ? (zend_long) model->cuda_decoder_prompt_loop_last_full_logits_bytes : 0 + ); + add_assoc_long( + &readback, + "request_saved_bytes", + model != NULL ? (zend_long) model->cuda_decoder_prompt_loop_last_saved_readback_bytes : 0 + ); + add_assoc_bool( + &readback, + "resident_device_buffers", + model != NULL + && model->cuda_logits_readback_device_indices != 0 + && model->cuda_logits_readback_device_logits != 0 + ); + add_assoc_long( + &readback, + "resident_buffer_reuses", + model != NULL ? (zend_long) model->cuda_logits_readback_buffer_reuse_count : 0 + ); + add_assoc_bool(&readback, "explicit_pre_readback_sync_removed", true); + add_assoc_double( + &readback, + "bytes_per_token", + model != NULL && model->cuda_decoder_prompt_loop_last_readback_tokens > 0 + ? (double) model->cuda_decoder_prompt_loop_last_readback_bytes + / (double) model->cuda_decoder_prompt_loop_last_readback_tokens + : 0.0 + ); + add_assoc_double( + &readback, + "saved_bytes_per_token", + model != NULL && model->cuda_decoder_prompt_loop_last_readback_tokens > 0 + ? (double) model->cuda_decoder_prompt_loop_last_saved_readback_bytes + / (double) model->cuda_decoder_prompt_loop_last_readback_tokens + : 0.0 + ); + add_assoc_double( + &readback, + "savings_ratio", + model != NULL && model->cuda_decoder_prompt_loop_last_full_logits_bytes > 0 + ? (double) model->cuda_decoder_prompt_loop_last_saved_readback_bytes + / (double) model->cuda_decoder_prompt_loop_last_full_logits_bytes + : 0.0 + ); + add_assoc_bool( + &readback, + "full_logits_readback_avoided", + model != NULL && model->cuda_decoder_prompt_loop_last_hot_token_full_logits_readback_avoided + ); + add_assoc_zval(&gpu, "bounded_readback", &readback); + add_assoc_zval(&hotpath, "gpu", &gpu); + add_assoc_zval(return_value, "runtime_hot_path", &hotpath); +} diff --git a/extension/src/inference/runtime/events/stream_native_lazy.inc b/extension/src/inference/runtime/events/stream_native_lazy.inc new file mode 100644 index 000000000..f4e508f73 --- /dev/null +++ b/extension/src/inference/runtime/events/stream_native_lazy.inc @@ -0,0 +1,30 @@ +/* + * Native stream lazy-event helpers. + */ + +static bool king_inference_stream_has_pending_native_events(king_inference_stream_object *stream) +{ + return king_inference_stream_has_native_events(stream) + && stream->native_event_index < (zend_ulong) zend_hash_num_elements(Z_ARRVAL(stream->native_events)); +} + +static zend_result king_inference_stream_ensure_native_event_available(king_inference_stream_object *stream) +{ + king_inference_model_object *model; + + if (stream == NULL || stream->native_gpu_stream_state == NULL) { + return SUCCESS; + } + if (Z_ISUNDEF(stream->model) || Z_TYPE(stream->model) != IS_OBJECT) { + return SUCCESS; + } + + model = php_king_inference_model_obj_from_zend(Z_OBJ(stream->model)); + while (stream->native_gpu_stream_state != NULL + && !king_inference_stream_has_pending_native_events(stream)) { + if (king_inference_cuda_decoder_prompt_stream_step(model, stream) != SUCCESS) { + return FAILURE; + } + } + return SUCCESS; +} diff --git a/extension/src/inference/runtime/events/stream_process_close.inc b/extension/src/inference/runtime/events/stream_process_close.inc new file mode 100644 index 000000000..6b4284e84 --- /dev/null +++ b/extension/src/inference/runtime/events/stream_process_close.inc @@ -0,0 +1,49 @@ +static bool king_inference_stream_wait_child(pid_t pid, bool retry) +{ + int status; + int attempts = retry ? 20 : 1; + + for (int attempt = 0; attempt < attempts; attempt++) { + pid_t result = waitpid(pid, &status, WNOHANG); + + if (result == pid || (result < 0 && errno == ECHILD)) { + return true; + } + if (result < 0 && errno != EINTR) { + return false; + } + if (!retry) { + return false; + } + usleep(10000); + } + + return false; +} + +static void king_inference_stream_close_process(king_inference_stream_object *stream, bool terminate) +{ + if (stream->stdout_fd >= 0) { + close(stream->stdout_fd); + stream->stdout_fd = -1; + } + if (stream->stderr_fd >= 0) { + close(stream->stderr_fd); + stream->stderr_fd = -1; + } + if (stream->child_pid > 0) { + pid_t pid = (pid_t) stream->child_pid; + bool should_terminate = terminate && !stream->done; + bool reaped; + + if (should_terminate) { + (void) kill(pid, SIGTERM); + } + reaped = king_inference_stream_wait_child(pid, should_terminate); + if (!reaped && should_terminate) { + (void) kill(pid, SIGKILL); + (void) king_inference_stream_wait_child(pid, true); + } + stream->child_pid = -1; + } +} diff --git a/extension/src/inference/runtime/memory/native_memory.inc b/extension/src/inference/runtime/memory/native_memory.inc new file mode 100644 index 000000000..460156683 --- /dev/null +++ b/extension/src/inference/runtime/memory/native_memory.inc @@ -0,0 +1,61 @@ +static void king_inference_native_mapping_release(king_inference_model_object *model) +{ + if (model->native_map != NULL && model->native_map_size > 0) { + munmap(model->native_map, model->native_map_size); + } + + model->native_map = NULL; + model->native_map_size = 0; + model->native_map_loaded = false; +} + +static zend_result king_inference_native_map_artifact( + zend_string *path, + zend_ulong file_size, + void **map_out, + size_t *map_size_out +) { + int fd; + void *mapped; + + *map_out = NULL; + *map_size_out = 0; + + if (file_size == 0 || file_size > (zend_ulong) SIZE_MAX) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "Model artifact '%s' is too large to map into this process.", + ZSTR_VAL(path) + ); + return FAILURE; + } + + fd = open(ZSTR_VAL(path), O_RDONLY); + if (fd < 0) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "Model artifact '%s' could not be opened for native mapping.", + ZSTR_VAL(path) + ); + return FAILURE; + } + + mapped = mmap(NULL, (size_t) file_size, PROT_READ, MAP_PRIVATE, fd, 0); + close(fd); + + if (mapped == MAP_FAILED) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "Model artifact '%s' could not be memory-mapped for King native inference.", + ZSTR_VAL(path) + ); + return FAILURE; + } + + *map_out = mapped; + *map_size_out = (size_t) file_size; + return SUCCESS; +} diff --git a/extension/src/inference/runtime/policy/resource_policy.inc b/extension/src/inference/runtime/policy/resource_policy.inc new file mode 100644 index 000000000..326fd32bd --- /dev/null +++ b/extension/src/inference/runtime/policy/resource_policy.inc @@ -0,0 +1,439 @@ +static bool king_inference_gpu_enabled(zval *config) +{ + zval *gpu = king_inference_array_find(config, "gpu"); + + return gpu != NULL + && Z_TYPE_P(gpu) == IS_ARRAY + && king_inference_bool_from_array(gpu, "enabled", false); +} + +static zend_long king_inference_max_gpu_layers(zval *config) +{ + zval *gpu = king_inference_array_find(config, "gpu"); + + if (gpu == NULL || Z_TYPE_P(gpu) != IS_ARRAY) { + return 0; + } + + return king_inference_long_from_array(gpu, "max_gpu_layers", 0); +} + +static zend_long king_inference_vram_reserve_mb(zval *config) +{ + zval *gpu = king_inference_array_find(config, "gpu"); + + if (gpu == NULL || Z_TYPE_P(gpu) != IS_ARRAY) { + return king_high_perf_compute_ai_config.inference_gpu_vram_reserve_mb; + } + + return king_inference_long_from_array( + gpu, + "vram_reserve_mb", + king_high_perf_compute_ai_config.inference_gpu_vram_reserve_mb + ); +} + +static zend_long king_inference_min_free_vram_mb(zval *config) +{ + zval *gpu = king_inference_array_find(config, "gpu"); + + if (gpu == NULL || Z_TYPE_P(gpu) != IS_ARRAY) { + return king_high_perf_compute_ai_config.inference_gpu_min_free_vram_mb; + } + + return king_inference_long_from_array( + gpu, + "min_free_vram_mb", + king_high_perf_compute_ai_config.inference_gpu_min_free_vram_mb + ); +} + +static zend_result king_inference_validate_gpu_bool_field( + zval *config, + const char *field_name, + const char *qualified_name, + const char *function_name +) { + zval *value = king_inference_array_find(config, field_name); + + if (value == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(value) != IS_TRUE && Z_TYPE_P(value) != IS_FALSE) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() %s must be a boolean when provided.", + function_name, + qualified_name + ); + return FAILURE; + } + + return SUCCESS; +} + +static zend_result king_inference_validate_gpu_non_negative_long_field( + zval *config, + const char *field_name, + const char *qualified_name, + const char *function_name +) { + zval *value = king_inference_array_find(config, field_name); + + if (value == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(value) != IS_LONG || Z_LVAL_P(value) < 0) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() %s must be a non-negative integer when provided.", + function_name, + qualified_name + ); + return FAILURE; + } + + return SUCCESS; +} + +static zend_result king_inference_validate_gpu_positive_long_field( + zval *config, + const char *field_name, + const char *qualified_name, + const char *function_name +) { + zval *value = king_inference_array_find(config, field_name); + + if (value == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(value) != IS_LONG || Z_LVAL_P(value) <= 0) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() %s must be a positive integer when provided.", + function_name, + qualified_name + ); + return FAILURE; + } + + return SUCCESS; +} + +static zend_result king_inference_validate_gpu_positive_number_field( + zval *config, + const char *field_name, + const char *qualified_name, + const char *function_name +) { + zval *value = king_inference_array_find(config, field_name); + double number; + + if (value == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(value) == IS_LONG) { + number = (double) Z_LVAL_P(value); + } else if (Z_TYPE_P(value) == IS_DOUBLE) { + number = Z_DVAL_P(value); + } else { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() %s must be a positive finite number when provided.", + function_name, + qualified_name + ); + return FAILURE; + } + + if (!isfinite(number) || number <= 0.0) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() %s must be a positive finite number when provided.", + function_name, + qualified_name + ); + return FAILURE; + } + + return SUCCESS; +} + +static zend_result king_inference_validate_gpu_non_negative_number_field( + zval *config, + const char *field_name, + const char *qualified_name, + const char *function_name +) { + zval *value = king_inference_array_find(config, field_name); + double number; + + if (value == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(value) == IS_LONG) { + number = (double) Z_LVAL_P(value); + } else if (Z_TYPE_P(value) == IS_DOUBLE) { + number = Z_DVAL_P(value); + } else { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() %s must be a non-negative finite number when provided.", + function_name, + qualified_name + ); + return FAILURE; + } + + if (!isfinite(number) || number < 0.0) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() %s must be a non-negative finite number when provided.", + function_name, + qualified_name + ); + return FAILURE; + } + + return SUCCESS; +} + +static double king_inference_gpu_number_from_array(zval *array, const char *field_name, double fallback) +{ + zval *value = king_inference_array_find(array, field_name); + + if (value == NULL) { + return fallback; + } + if (Z_TYPE_P(value) == IS_LONG) { + return (double) Z_LVAL_P(value); + } + if (Z_TYPE_P(value) == IS_DOUBLE) { + return Z_DVAL_P(value); + } + + return fallback; +} + +static zend_result king_inference_validate_gpu_string_field( + zval *config, + const char *field_name, + const char *qualified_name, + const char *function_name +) { + zval *value = king_inference_array_find(config, field_name); + + if (value == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(value) != IS_STRING || Z_STRLEN_P(value) == 0) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() %s must be a non-empty string when provided.", + function_name, + qualified_name + ); + return FAILURE; + } + + return SUCCESS; +} + +static zend_result king_inference_validate_gpu_config(zval *config, const char *function_name) +{ + zval *gpu = king_inference_array_find(config, "gpu"); + zval *thermal; + zval *power; + zval *debug; + + if (gpu == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(gpu) != IS_ARRAY) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() gpu must be an array when provided.", + function_name + ); + return FAILURE; + } + + if (king_inference_validate_gpu_bool_field(gpu, "enabled", "gpu.enabled", function_name) != SUCCESS + || king_inference_validate_gpu_bool_field( + gpu, + "batch_prefill_experimental_enable", + "gpu.batch_prefill_experimental_enable", + function_name + ) != SUCCESS + || king_inference_validate_gpu_non_negative_long_field( + gpu, + "max_gpu_layers", + "gpu.max_gpu_layers", + function_name + ) != SUCCESS + || king_inference_validate_gpu_non_negative_long_field( + gpu, + "vram_reserve_mb", + "gpu.vram_reserve_mb", + function_name + ) != SUCCESS + || king_inference_validate_gpu_non_negative_long_field( + gpu, + "min_free_vram_mb", + "gpu.min_free_vram_mb", + function_name + ) != SUCCESS + || king_inference_validate_gpu_bool_field( + gpu, + "allow_system_ram_offload", + "gpu.allow_system_ram_offload", + function_name + ) != SUCCESS + || king_inference_validate_gpu_non_negative_long_field( + gpu, + "system_ram_offload_max_mb", + "gpu.system_ram_offload_max_mb", + function_name + ) != SUCCESS + || king_inference_validate_gpu_non_negative_long_field( + gpu, + "system_ram_offload_min_free_mb", + "gpu.system_ram_offload_min_free_mb", + function_name + ) != SUCCESS) { + return FAILURE; + } + + debug = king_inference_array_find(gpu, "debug"); + if (debug != NULL) { + if (Z_TYPE_P(debug) != IS_ARRAY) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() gpu.debug must be an array when provided.", + function_name + ); + return FAILURE; + } + if (king_inference_validate_gpu_bool_field( + debug, + "numeric_compare_enabled", + "gpu.debug.numeric_compare_enabled", + function_name + ) != SUCCESS + || king_inference_validate_gpu_positive_long_field( + debug, + "numeric_compare_max_values", + "gpu.debug.numeric_compare_max_values", + function_name + ) != SUCCESS) { + return FAILURE; + } + } + + thermal = king_inference_array_find(gpu, "thermal"); + if (thermal != NULL) { + if (Z_TYPE_P(thermal) != IS_ARRAY) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() gpu.thermal must be an array when provided.", + function_name + ); + return FAILURE; + } + + if (king_inference_validate_gpu_string_field( + thermal, + "sensor_path", + "gpu.thermal.sensor_path", + function_name + ) != SUCCESS + || king_inference_validate_gpu_string_field( + thermal, + "sensor_command", + "gpu.thermal.sensor_command", + function_name + ) != SUCCESS + || king_inference_validate_gpu_positive_number_field( + thermal, + "max_temperature_c", + "gpu.thermal.max_temperature_c", + function_name + ) != SUCCESS + || king_inference_validate_gpu_non_negative_long_field( + thermal, + "check_interval_seconds", + "gpu.thermal.check_interval_seconds", + function_name + ) != SUCCESS + || king_inference_validate_gpu_bool_field( + thermal, + "allow_unmonitored_gpu", + "gpu.thermal.allow_unmonitored_gpu", + function_name + ) != SUCCESS) { + return FAILURE; + } + } + + power = king_inference_array_find(gpu, "power"); + if (power == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(power) != IS_ARRAY) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() gpu.power must be an array when provided.", + function_name + ); + return FAILURE; + } + + if (king_inference_validate_gpu_string_field( + power, + "sensor_command", + "gpu.power.sensor_command", + function_name + ) != SUCCESS + || king_inference_validate_gpu_non_negative_number_field( + power, + "max_watts", + "gpu.power.max_watts", + function_name + ) != SUCCESS + || king_inference_validate_gpu_non_negative_long_field( + power, + "check_interval_seconds", + "gpu.power.check_interval_seconds", + function_name + ) != SUCCESS) { + return FAILURE; + } + + if (king_inference_gpu_number_from_array(power, "max_watts", 0.0) > 0.0) { + zval *sensor_command = king_inference_array_find(power, "sensor_command"); + + if (sensor_command == NULL || Z_TYPE_P(sensor_command) != IS_STRING || Z_STRLEN_P(sensor_command) == 0) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() gpu.power.sensor_command is required when gpu.power.max_watts is greater than zero.", + function_name + ); + return FAILURE; + } + } + + return SUCCESS; +} diff --git a/extension/src/inference/runtime/policy/thermal_policy.inc b/extension/src/inference/runtime/policy/thermal_policy.inc new file mode 100644 index 000000000..89ac9883a --- /dev/null +++ b/extension/src/inference/runtime/policy/thermal_policy.inc @@ -0,0 +1,605 @@ +static zend_result king_inference_read_temperature(zend_string *path, double *temperature_c) +{ + FILE *file = fopen(ZSTR_VAL(path), "r"); + char buffer[64]; + char *endptr; + double value; + + if (file == NULL) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "GPU thermal sensor '%s' could not be opened.", + ZSTR_VAL(path) + ); + return FAILURE; + } + + if (fgets(buffer, sizeof(buffer), file) == NULL) { + fclose(file); + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "GPU thermal sensor '%s' could not be read.", + ZSTR_VAL(path) + ); + return FAILURE; + } + fclose(file); + + errno = 0; + value = strtod(buffer, &endptr); + if (errno != 0 || endptr == buffer) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "GPU thermal sensor '%s' did not return a numeric temperature.", + ZSTR_VAL(path) + ); + return FAILURE; + } + + *temperature_c = value > 1000.0 ? value / 1000.0 : value; + return SUCCESS; +} + +static zend_result king_inference_read_temperature_command(zend_string *command, double *temperature_c) +{ + FILE *pipe = popen(ZSTR_VAL(command), "r"); + char buffer[64]; + char *endptr; + double value; + int status; + + if (pipe == NULL) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "GPU thermal sensor command '%s' could not be opened.", + ZSTR_VAL(command) + ); + return FAILURE; + } + + if (fgets(buffer, sizeof(buffer), pipe) == NULL) { + pclose(pipe); + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "GPU thermal sensor command '%s' returned no temperature.", + ZSTR_VAL(command) + ); + return FAILURE; + } + + status = pclose(pipe); + if (status == -1) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "GPU thermal sensor command '%s' could not be closed.", + ZSTR_VAL(command) + ); + return FAILURE; + } + + errno = 0; + value = strtod(buffer, &endptr); + if (errno != 0 || endptr == buffer) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "GPU thermal sensor command '%s' did not return a numeric temperature.", + ZSTR_VAL(command) + ); + return FAILURE; + } + + *temperature_c = value > 1000.0 ? value / 1000.0 : value; + return SUCCESS; +} + +static zend_result king_inference_read_power_command(zend_string *command, double *power_watts) +{ + FILE *pipe = popen(ZSTR_VAL(command), "r"); + char buffer[64]; + char *endptr; + double value; + int status; + + if (pipe == NULL) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "GPU power sensor command '%s' could not be opened.", + ZSTR_VAL(command) + ); + return FAILURE; + } + + if (fgets(buffer, sizeof(buffer), pipe) == NULL) { + pclose(pipe); + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "GPU power sensor command '%s' returned no power reading.", + ZSTR_VAL(command) + ); + return FAILURE; + } + + status = pclose(pipe); + if (status == -1) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "GPU power sensor command '%s' could not be closed.", + ZSTR_VAL(command) + ); + return FAILURE; + } + + errno = 0; + value = strtod(buffer, &endptr); + if (errno != 0 || endptr == buffer || !isfinite(value) || value < 0.0) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "GPU power sensor command '%s' did not return a non-negative numeric watt reading.", + ZSTR_VAL(command) + ); + return FAILURE; + } + + *power_watts = value; + return SUCCESS; +} + +static double king_inference_gpu_thermal_max_temperature_c(zval *config); + +static zend_result king_inference_check_gpu_policy_sample( + zval *config, + const char *function_name, + bool *temperature_checked, + double *temperature_c_out, + double *max_temperature_c_out, + bool *temperature_ceiling_reached +) +{ + zval *gpu = king_inference_array_find(config, "gpu"); + zval *thermal; + zval *sensor_path; + zval *sensor_command; + double temperature_c; + double max_temperature_c; + + if (temperature_checked != NULL) { + *temperature_checked = false; + } + if (temperature_c_out != NULL) { + *temperature_c_out = 0.0; + } + if (max_temperature_c_out != NULL) { + *max_temperature_c_out = 0.0; + } + if (temperature_ceiling_reached != NULL) { + *temperature_ceiling_reached = false; + } + + if (gpu == NULL || Z_TYPE_P(gpu) != IS_ARRAY || !king_inference_bool_from_array(gpu, "enabled", false)) { + return SUCCESS; + } + + if (!king_high_perf_compute_ai_config.gpu_bindings_enable) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() refused GPU inference because king.gpu_bindings_enable is disabled.", + function_name + ); + return FAILURE; + } + + thermal = king_inference_array_find(gpu, "thermal"); + sensor_path = thermal != NULL && Z_TYPE_P(thermal) == IS_ARRAY + ? king_inference_array_find(thermal, "sensor_path") + : NULL; + sensor_command = thermal != NULL && Z_TYPE_P(thermal) == IS_ARRAY + ? king_inference_array_find(thermal, "sensor_command") + : NULL; + + if ((sensor_path == NULL || Z_TYPE_P(sensor_path) != IS_STRING || Z_STRLEN_P(sensor_path) == 0) + && (sensor_command == NULL || Z_TYPE_P(sensor_command) != IS_STRING || Z_STRLEN_P(sensor_command) == 0)) { + if (thermal != NULL + && Z_TYPE_P(thermal) == IS_ARRAY + && king_inference_bool_from_array(thermal, "allow_unmonitored_gpu", false)) { + return SUCCESS; + } + + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() refused GPU inference without a thermal sensor path. Set gpu.thermal.sensor_path or explicitly allow unmonitored GPU use.", + function_name + ); + return FAILURE; + } + + if (sensor_path != NULL && Z_TYPE_P(sensor_path) == IS_STRING && Z_STRLEN_P(sensor_path) > 0) { + if (king_inference_read_temperature(Z_STR_P(sensor_path), &temperature_c) != SUCCESS) { + return FAILURE; + } + } else if (king_inference_read_temperature_command(Z_STR_P(sensor_command), &temperature_c) != SUCCESS) { + return FAILURE; + } + if (temperature_checked != NULL) { + *temperature_checked = true; + } + if (temperature_c_out != NULL) { + *temperature_c_out = temperature_c; + } + + max_temperature_c = king_inference_gpu_thermal_max_temperature_c(config); + if (max_temperature_c_out != NULL) { + *max_temperature_c_out = max_temperature_c; + } + + if (temperature_c >= max_temperature_c) { + if (temperature_ceiling_reached != NULL) { + *temperature_ceiling_reached = true; + } + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "%s() refused GPU inference because sensor '%s' reports %.1f C at or above the configured %.1f C limit.", + function_name, + sensor_path != NULL && Z_TYPE_P(sensor_path) == IS_STRING ? Z_STRVAL_P(sensor_path) : Z_STRVAL_P(sensor_command), + temperature_c, + max_temperature_c + ); + return FAILURE; + } + + return SUCCESS; +} + +static zend_result king_inference_check_gpu_policy(zval *config, const char *function_name) +{ + return king_inference_check_gpu_policy_sample(config, function_name, NULL, NULL, NULL, NULL); +} + +static double king_inference_gpu_power_max_watts(zval *config) +{ + zval *gpu = king_inference_array_find(config, "gpu"); + zval *power; + + if (gpu == NULL || Z_TYPE_P(gpu) != IS_ARRAY) { + return king_high_perf_compute_ai_config.inference_gpu_power_max_watts; + } + + power = king_inference_array_find(gpu, "power"); + if (power == NULL || Z_TYPE_P(power) != IS_ARRAY) { + return king_high_perf_compute_ai_config.inference_gpu_power_max_watts; + } + + return king_inference_double_from_array( + power, + "max_watts", + king_high_perf_compute_ai_config.inference_gpu_power_max_watts + ); +} + +static zend_long king_inference_gpu_power_check_interval_seconds(zval *config) +{ + zval *gpu = king_inference_array_find(config, "gpu"); + zval *power; + + if (gpu == NULL || Z_TYPE_P(gpu) != IS_ARRAY) { + return king_high_perf_compute_ai_config.inference_gpu_power_check_interval_sec; + } + + power = king_inference_array_find(gpu, "power"); + if (power == NULL || Z_TYPE_P(power) != IS_ARRAY) { + return king_high_perf_compute_ai_config.inference_gpu_power_check_interval_sec; + } + + return king_inference_long_from_array( + power, + "check_interval_seconds", + king_high_perf_compute_ai_config.inference_gpu_power_check_interval_sec + ); +} + +static zend_result king_inference_check_gpu_power_policy_sample( + zval *config, + const char *function_name, + bool *power_checked, + double *power_watts_out, + double *max_watts_out, + bool *power_ceiling_reached +) { + zval *gpu = king_inference_array_find(config, "gpu"); + zval *power; + zval *sensor_command = NULL; + zend_string *command = NULL; + double max_watts = king_inference_gpu_power_max_watts(config); + double power_watts = 0.0; + + if (power_checked != NULL) { + *power_checked = false; + } + if (power_watts_out != NULL) { + *power_watts_out = 0.0; + } + if (max_watts_out != NULL) { + *max_watts_out = max_watts; + } + if (power_ceiling_reached != NULL) { + *power_ceiling_reached = false; + } + + if (gpu == NULL || Z_TYPE_P(gpu) != IS_ARRAY || !king_inference_bool_from_array(gpu, "enabled", false)) { + return SUCCESS; + } + if (max_watts <= 0.0) { + return SUCCESS; + } + + power = king_inference_array_find(gpu, "power"); + if (power != NULL && Z_TYPE_P(power) == IS_ARRAY) { + sensor_command = king_inference_array_find(power, "sensor_command"); + } + if (sensor_command != NULL && Z_TYPE_P(sensor_command) == IS_STRING && Z_STRLEN_P(sensor_command) > 0) { + command = Z_STR_P(sensor_command); + } else if (king_high_perf_compute_ai_config.inference_gpu_power_sensor_command != NULL + && king_high_perf_compute_ai_config.inference_gpu_power_sensor_command[0] != '\0') { + command = zend_string_init( + king_high_perf_compute_ai_config.inference_gpu_power_sensor_command, + strlen(king_high_perf_compute_ai_config.inference_gpu_power_sensor_command), + 0 + ); + } + + if (command == NULL) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s() refused GPU inference because gpu.power.max_watts is enabled without gpu.power.sensor_command.", + function_name + ); + return FAILURE; + } + + if (king_inference_read_power_command(command, &power_watts) != SUCCESS) { + if (command != NULL && (sensor_command == NULL || command != Z_STR_P(sensor_command))) { + zend_string_release(command); + } + return FAILURE; + } + if (command != NULL && (sensor_command == NULL || command != Z_STR_P(sensor_command))) { + zend_string_release(command); + } + + if (power_checked != NULL) { + *power_checked = true; + } + if (power_watts_out != NULL) { + *power_watts_out = power_watts; + } + + if (power_watts >= max_watts) { + if (power_ceiling_reached != NULL) { + *power_ceiling_reached = true; + } + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "%s() refused GPU inference because power draw %.1f W is at or above the configured %.1f W limit.", + function_name, + power_watts, + max_watts + ); + return FAILURE; + } + + return SUCCESS; +} + +static zend_long king_inference_gpu_thermal_check_interval_seconds(zval *config) +{ + zval *gpu = king_inference_array_find(config, "gpu"); + zval *thermal; + + if (gpu == NULL || Z_TYPE_P(gpu) != IS_ARRAY) { + return king_high_perf_compute_ai_config.inference_gpu_thermal_check_interval_sec; + } + + thermal = king_inference_array_find(gpu, "thermal"); + if (thermal == NULL || Z_TYPE_P(thermal) != IS_ARRAY) { + return king_high_perf_compute_ai_config.inference_gpu_thermal_check_interval_sec; + } + + return king_inference_long_from_array( + thermal, + "check_interval_seconds", + king_high_perf_compute_ai_config.inference_gpu_thermal_check_interval_sec + ); +} + +static double king_inference_gpu_thermal_max_temperature_c(zval *config) +{ + zval *gpu = king_inference_array_find(config, "gpu"); + zval *thermal; + + if (gpu == NULL || Z_TYPE_P(gpu) != IS_ARRAY) { + return king_high_perf_compute_ai_config.inference_gpu_thermal_max_temperature_c; + } + + thermal = king_inference_array_find(gpu, "thermal"); + if (thermal == NULL || Z_TYPE_P(thermal) != IS_ARRAY) { + return king_high_perf_compute_ai_config.inference_gpu_thermal_max_temperature_c; + } + + return king_inference_double_from_array( + thermal, + "max_temperature_c", + king_high_perf_compute_ai_config.inference_gpu_thermal_max_temperature_c + ); +} + +static zend_result king_inference_check_gpu_policy_before_run( + king_inference_stream_object *stream, + const char *function_name +) { + king_inference_model_object *model; + zval *gpu; + bool temperature_checked = false; + double temperature_c = 0.0; + + if (Z_ISUNDEF(stream->model) || Z_TYPE(stream->model) != IS_OBJECT) { + return SUCCESS; + } + + model = php_king_inference_model_obj_from_zend(Z_OBJ(stream->model)); + gpu = king_inference_array_find(&model->config, "gpu"); + if (gpu == NULL || Z_TYPE_P(gpu) != IS_ARRAY || !king_inference_bool_from_array(gpu, "enabled", false)) { + return SUCCESS; + } + + if (king_inference_check_gpu_policy_sample( + &model->config, + function_name, + &temperature_checked, + &temperature_c, + NULL, + NULL + ) != SUCCESS) { + return FAILURE; + } + + stream->gpu_thermal_preflight_checked = true; + stream->gpu_thermal_preflight_at = (zend_long) time(NULL); + stream->gpu_thermal_last_check_at = stream->gpu_thermal_preflight_at; + stream->gpu_thermal_preflight_temperature_available = temperature_checked; + stream->gpu_thermal_preflight_temperature_c = temperature_checked ? temperature_c : 0.0; + + { + bool power_checked = false; + double power_watts = 0.0; + + if (king_inference_check_gpu_power_policy_sample( + &model->config, + function_name, + &power_checked, + &power_watts, + NULL, + NULL + ) != SUCCESS) { + return FAILURE; + } + if (power_checked) { + stream->gpu_power_preflight_checked = true; + stream->gpu_power_preflight_at = stream->gpu_thermal_preflight_at; + stream->gpu_power_last_check_at = stream->gpu_power_preflight_at; + stream->gpu_power_preflight_available = true; + stream->gpu_power_preflight_watts = power_watts; + } + } + return SUCCESS; +} + +static zend_result king_inference_check_gpu_policy_during_run( + king_inference_stream_object *stream, + const char *function_name +) { + king_inference_model_object *model; + zval *gpu; + bool temperature_checked = false; + bool ceiling_reached = false; + bool power_checked = false; + bool power_ceiling_reached = false; + double temperature_c = 0.0; + double max_temperature_c = 0.0; + double power_watts = 0.0; + double max_watts = 0.0; + zend_long now; + zend_long check_interval_sec; + zend_long power_check_interval_sec; + bool thermal_due; + bool power_due; + + if (Z_ISUNDEF(stream->model) || Z_TYPE(stream->model) != IS_OBJECT) { + return SUCCESS; + } + + model = php_king_inference_model_obj_from_zend(Z_OBJ(stream->model)); + gpu = king_inference_array_find(&model->config, "gpu"); + if (gpu == NULL || Z_TYPE_P(gpu) != IS_ARRAY || !king_inference_bool_from_array(gpu, "enabled", false)) { + return SUCCESS; + } + + now = (zend_long) time(NULL); + check_interval_sec = king_inference_gpu_thermal_check_interval_seconds(&model->config); + stream->gpu_policy_during_run_checks++; + stream->gpu_thermal_check_interval_seconds = check_interval_sec; + thermal_due = check_interval_sec <= 0 + || stream->gpu_thermal_last_check_at <= 0 + || now < stream->gpu_thermal_last_check_at + || now - stream->gpu_thermal_last_check_at >= check_interval_sec; + power_check_interval_sec = king_inference_gpu_power_check_interval_seconds(&model->config); + stream->gpu_power_check_interval_seconds = power_check_interval_sec; + power_due = king_inference_gpu_power_max_watts(&model->config) > 0.0 + && power_check_interval_sec > 0 + && (stream->gpu_power_last_check_at <= 0 + || now < stream->gpu_power_last_check_at + || now - stream->gpu_power_last_check_at >= power_check_interval_sec); + if (!thermal_due && !power_due) { + stream->gpu_policy_interval_skips++; + return SUCCESS; + } + + if (thermal_due) { + if (king_inference_check_gpu_policy_sample( + &model->config, + function_name, + &temperature_checked, + &temperature_c, + &max_temperature_c, + &ceiling_reached + ) != SUCCESS) { + if (ceiling_reached) { + stream->gpu_thermal_aborted = true; + stream->gpu_thermal_abort_at = now; + stream->gpu_thermal_abort_temperature_c = temperature_checked ? temperature_c : 0.0; + stream->gpu_thermal_abort_ceiling_c = max_temperature_c; + } + return FAILURE; + } + stream->gpu_thermal_runtime_checks++; + stream->gpu_thermal_last_check_at = now; + } + + if (power_due) { + if (king_inference_check_gpu_power_policy_sample( + &model->config, + function_name, + &power_checked, + &power_watts, + &max_watts, + &power_ceiling_reached + ) != SUCCESS) { + if (power_ceiling_reached) { + stream->gpu_power_aborted = true; + stream->gpu_power_abort_at = now; + stream->gpu_power_abort_watts = power_checked ? power_watts : 0.0; + stream->gpu_power_abort_ceiling_watts = max_watts; + } + return FAILURE; + } + if (power_checked) { + stream->gpu_power_runtime_checks++; + stream->gpu_power_last_check_at = now; + } + } + return SUCCESS; +} diff --git a/extension/src/inference/tensor/core/tensor_math.inc b/extension/src/inference/tensor/core/tensor_math.inc new file mode 100644 index 000000000..ab50d8168 --- /dev/null +++ b/extension/src/inference/tensor/core/tensor_math.inc @@ -0,0 +1,678 @@ +typedef struct _king_inference_tensor_native_view { + zval *descriptor; + zval *dimensions; + king_inference_tensor_type_info type_info; + zend_ulong rank; + zend_ulong type; + zend_ulong elements; + zend_ulong relative_offset; + zend_ulong absolute_offset; + zend_ulong byte_length; + unsigned char *data; +} king_inference_tensor_native_view; + +static uint16_t king_inference_tensor_read_u16(const unsigned char *bytes) +{ + return (uint16_t) (((uint16_t) bytes[0]) | ((uint16_t) bytes[1] << 8)); +} + +static uint32_t king_inference_tensor_read_u32(const unsigned char *bytes) +{ + return ((uint32_t) bytes[0]) + | ((uint32_t) bytes[1] << 8) + | ((uint32_t) bytes[2] << 16) + | ((uint32_t) bytes[3] << 24); +} + +static uint64_t king_inference_tensor_read_u64(const unsigned char *bytes) +{ + return ((uint64_t) bytes[0]) + | ((uint64_t) bytes[1] << 8) + | ((uint64_t) bytes[2] << 16) + | ((uint64_t) bytes[3] << 24) + | ((uint64_t) bytes[4] << 32) + | ((uint64_t) bytes[5] << 40) + | ((uint64_t) bytes[6] << 48) + | ((uint64_t) bytes[7] << 56); +} + +static float king_inference_tensor_float_from_bits(uint32_t bits) +{ + float value; + memcpy(&value, &bits, sizeof(value)); + return value; +} + +static double king_inference_tensor_double_from_bits(uint64_t bits) +{ + double value; + memcpy(&value, &bits, sizeof(value)); + return value; +} + +static float king_inference_tensor_f16_to_float(uint16_t half) +{ + uint32_t sign = ((uint32_t) half & 0x8000U) << 16; + uint32_t mantissa = (uint32_t) half & 0x03ffU; + int exponent = (int) ((half >> 10) & 0x1fU); + uint32_t bits; + + if (exponent == 0) { + if (mantissa == 0) { + return king_inference_tensor_float_from_bits(sign); + } + while ((mantissa & 0x0400U) == 0) { + mantissa <<= 1; + exponent--; + } + exponent++; + mantissa &= 0x03ffU; + bits = sign | ((uint32_t) (exponent + 112) << 23) | (mantissa << 13); + return king_inference_tensor_float_from_bits(bits); + } + + if (exponent == 31) { + bits = sign | 0x7f800000U | (mantissa << 13); + return king_inference_tensor_float_from_bits(bits); + } + + bits = sign | ((uint32_t) (exponent + 112) << 23) | (mantissa << 13); + return king_inference_tensor_float_from_bits(bits); +} + +static void king_inference_tensor_qk4_scale_min( + const unsigned char *scales, + zend_ulong group, + int *scale, + int *minimum +) { + if (group < 4) { + *scale = (int) (scales[group] & 0x3fU); + *minimum = (int) (scales[group + 4] & 0x3fU); + return; + } + + *scale = (int) ((scales[group + 4] & 0x0fU) | ((scales[group - 4] >> 6) << 4)); + *minimum = (int) ((scales[group + 4] >> 4) | ((scales[group] >> 6) << 4)); +} + +static double king_inference_tensor_q4_k_value(const unsigned char *block, zend_ulong in_block) +{ + const unsigned char *scales = block + 4; + const unsigned char *qs = block + 16; + zend_ulong group = in_block / 32; + zend_ulong in_group = in_block & 31; + unsigned char packed = qs[(group / 2) * 32 + in_group]; + int scale; + int minimum; + int quant = (int) ((group & 1) == 0 ? (packed & 0x0fU) : (packed >> 4)); + double d = (double) king_inference_tensor_f16_to_float(king_inference_tensor_read_u16(block)); + double dmin = (double) king_inference_tensor_f16_to_float(king_inference_tensor_read_u16(block + 2)); + + king_inference_tensor_qk4_scale_min(scales, group, &scale, &minimum); + return d * (double) scale * (double) quant - dmin * (double) minimum; +} + +static double king_inference_tensor_q5_k_value(const unsigned char *block, zend_ulong in_block) +{ + const unsigned char *scales = block + 4; + const unsigned char *qh = block + 16; + const unsigned char *qs = block + 48; + zend_ulong group = in_block / 32; + zend_ulong in_group = in_block & 31; + zend_ulong high_index = group * 32 + in_group; + unsigned char packed = qs[(group / 2) * 32 + in_group]; + int scale; + int minimum; + int high = (int) ((qh[high_index >> 3] >> (high_index & 7)) & 1U); + int quant = (int) ((group & 1) == 0 ? (packed & 0x0fU) : (packed >> 4)) | (high << 4); + double d = (double) king_inference_tensor_f16_to_float(king_inference_tensor_read_u16(block)); + double dmin = (double) king_inference_tensor_f16_to_float(king_inference_tensor_read_u16(block + 2)); + + king_inference_tensor_qk4_scale_min(scales, group, &scale, &minimum); + return d * (double) scale * (double) quant - dmin * (double) minimum; +} + +static double king_inference_tensor_q5_0_value(const unsigned char *block, zend_ulong in_block) +{ + zend_ulong low_index = in_block & 15; + unsigned char packed = block[6 + low_index]; + uint32_t high_bits = king_inference_tensor_read_u32(block + 2); + int low = (int) (in_block < 16 ? (packed & 0x0fU) : (packed >> 4)); + int high = (int) ((high_bits >> in_block) & 1U); + int quant = (low | (high << 4)) - 16; + double scale = (double) king_inference_tensor_f16_to_float(king_inference_tensor_read_u16(block)); + + return scale * (double) quant; +} + +static double king_inference_tensor_q6_k_value(const unsigned char *block, zend_ulong in_block) +{ + zend_ulong chunk = in_block / 128; + zend_ulong within = in_block & 127; + zend_ulong lane = within & 31; + zend_ulong quadrant = within / 32; + const unsigned char *ql = block + chunk * 64; + const unsigned char *qh = block + 128 + chunk * 32; + const int8_t *scales = (const int8_t *) (block + 192 + chunk * 8); + double d = (double) king_inference_tensor_f16_to_float(king_inference_tensor_read_u16(block + 208)); + int quant; + int scale; + + switch (quadrant) { + case 0: + quant = (int) ((ql[lane] & 0x0fU) | ((qh[lane] & 0x03U) << 4)); + scale = scales[lane / 16]; + break; + case 1: + quant = (int) ((ql[lane + 32] & 0x0fU) | ((qh[lane] & 0x0cU) << 2)); + scale = scales[2 + lane / 16]; + break; + case 2: + quant = (int) ((ql[lane] >> 4) | (qh[lane] & 0x30U)); + scale = scales[4 + lane / 16]; + break; + default: + quant = (int) ((ql[lane + 32] >> 4) | ((qh[lane] & 0xc0U) >> 2)); + scale = scales[6 + lane / 16]; + break; + } + + return d * (double) scale * (double) (quant - 32); +} + +static zend_result king_inference_tensor_dimension_at( + zval *dimensions, + zend_ulong index, + zend_ulong *value +) { + zval *dimension; + + dimension = zend_hash_index_find(Z_ARRVAL_P(dimensions), index); + if (dimension == NULL || Z_TYPE_P(dimension) != IS_LONG || Z_LVAL_P(dimension) <= 0) { + return FAILURE; + } + + *value = (zend_ulong) Z_LVAL_P(dimension); + return SUCCESS; +} + +static zend_result king_inference_tensor_native_view_resolve( + king_inference_model_object *model, + zend_string *name, + king_inference_tensor_native_view *view +) { + zend_ulong byte_end = 0; + + memset(view, 0, sizeof(*view)); + if (!model->native_map_loaded || model->native_map == NULL || model->native_map_size == 0) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "King inference tensor math requires a native-mapped model artifact." + ); + return FAILURE; + } + if (Z_ISUNDEF(model->tensor_index) || Z_TYPE(model->tensor_index) != IS_ARRAY) { + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King inference model has no native tensor index."); + return FAILURE; + } + + view->descriptor = zend_hash_find(Z_ARRVAL(model->tensor_index), name); + if (view->descriptor == NULL) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "King inference tensor '%s' was not found in the model index.", + ZSTR_VAL(name) + ); + return FAILURE; + } + view->dimensions = zend_hash_str_find(Z_ARRVAL_P(view->descriptor), "dimensions", sizeof("dimensions") - 1); + if (view->dimensions == NULL || Z_TYPE_P(view->dimensions) != IS_ARRAY) { + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King inference tensor '%s' has invalid dimensions.", ZSTR_VAL(name)); + return FAILURE; + } + if (!king_inference_tensor_descriptor_ulong(view->descriptor, "rank", sizeof("rank") - 1, &view->rank) + || !king_inference_tensor_descriptor_ulong(view->descriptor, "type", sizeof("type") - 1, &view->type) + || !king_inference_tensor_descriptor_ulong(view->descriptor, "elements", sizeof("elements") - 1, &view->elements) + || !king_inference_tensor_descriptor_ulong(view->descriptor, "relative_offset", sizeof("relative_offset") - 1, &view->relative_offset)) { + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King inference tensor '%s' has an invalid descriptor.", ZSTR_VAL(name)); + return FAILURE; + } + if (!king_inference_tensor_type_lookup(view->type, &view->type_info) + || view->type_info.block_size == 0 + || view->type_info.type_size == 0) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "King inference tensor '%s' uses unsupported tensor type %lu.", + ZSTR_VAL(name), + view->type + ); + return FAILURE; + } + if (!king_inference_tensor_add_checked(model->gguf.tensor_data_offset, view->relative_offset, &view->absolute_offset)) { + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King inference tensor '%s' has an overflowing data offset.", ZSTR_VAL(name)); + return FAILURE; + } + if (!king_inference_tensor_mul_checked( + view->elements == 0 ? 0 : 1 + ((view->elements - 1) / view->type_info.block_size), + view->type_info.type_size, + &view->byte_length) + || !king_inference_tensor_add_checked(view->absolute_offset, view->byte_length, &byte_end) + || byte_end > model->gguf.file_size + || byte_end > (zend_ulong) model->native_map_size) { + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King inference tensor '%s' is outside the mapped model artifact.", ZSTR_VAL(name)); + return FAILURE; + } + + view->data = ((unsigned char *) model->native_map) + view->absolute_offset; + return SUCCESS; +} + +static zend_result king_inference_tensor_value_at( + king_inference_tensor_native_view *view, + zend_ulong index, + double *value +) { + const unsigned char *block; + zend_ulong block_index; + zend_ulong in_block; + uint16_t scale_half; + double scale; + + if (index >= view->elements) { + return FAILURE; + } + + switch (view->type) { + case 0: + *value = (double) king_inference_tensor_float_from_bits( + king_inference_tensor_read_u32(view->data + (index * 4)) + ); + return SUCCESS; + case 1: + *value = (double) king_inference_tensor_f16_to_float( + king_inference_tensor_read_u16(view->data + (index * 2)) + ); + return SUCCESS; + case 2: + block_index = index / 32; + in_block = index % 32; + block = view->data + (block_index * 18); + scale = (double) king_inference_tensor_f16_to_float(king_inference_tensor_read_u16(block)); + { + unsigned char packed = block[2 + (in_block & 15)]; + int quant = (int) (in_block < 16 ? (packed & 0x0fU) : (packed >> 4)) - 8; + *value = scale * (double) quant; + } + return SUCCESS; + case 3: + block_index = index / 32; + in_block = index % 32; + block = view->data + (block_index * 20); + scale = (double) king_inference_tensor_f16_to_float(king_inference_tensor_read_u16(block)); + { + double minimum = (double) king_inference_tensor_f16_to_float(king_inference_tensor_read_u16(block + 2)); + unsigned char packed = block[4 + (in_block & 15)]; + int quant = (int) (in_block < 16 ? (packed & 0x0fU) : (packed >> 4)); + *value = minimum + (scale * (double) quant); + } + return SUCCESS; + case 6: + block_index = index / 32; + in_block = index % 32; + *value = king_inference_tensor_q5_0_value(view->data + block_index * 22, in_block); + return SUCCESS; + case 8: + block_index = index / 32; + in_block = index % 32; + block = view->data + (block_index * 34); + scale_half = king_inference_tensor_read_u16(block); + scale = (double) king_inference_tensor_f16_to_float(scale_half); + *value = scale * (double) ((int8_t) block[2 + in_block]); + return SUCCESS; + case 12: + block_index = index / 256; + in_block = index & 255; + *value = king_inference_tensor_q4_k_value(view->data + block_index * 144, in_block); + return SUCCESS; + case 13: + block_index = index / 256; + in_block = index & 255; + *value = king_inference_tensor_q5_k_value(view->data + block_index * 176, in_block); + return SUCCESS; + case 14: + block_index = index / 256; + in_block = index & 255; + *value = king_inference_tensor_q6_k_value(view->data + block_index * 210, in_block); + return SUCCESS; + case 24: + *value = (double) ((int8_t) view->data[index]); + return SUCCESS; + case 25: + *value = (double) ((int16_t) king_inference_tensor_read_u16(view->data + (index * 2))); + return SUCCESS; + case 26: + *value = (double) ((int32_t) king_inference_tensor_read_u32(view->data + (index * 4))); + return SUCCESS; + case 27: + *value = (double) ((int64_t) king_inference_tensor_read_u64(view->data + (index * 8))); + return SUCCESS; + case 28: + *value = king_inference_tensor_double_from_bits(king_inference_tensor_read_u64(view->data + (index * 8))); + return SUCCESS; + case 30: + *value = (double) king_inference_tensor_float_from_bits( + ((uint32_t) king_inference_tensor_read_u16(view->data + (index * 2))) << 16 + ); + return SUCCESS; + } + + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "King inference tensor math does not yet support tensor type %s.", + view->type_info.name + ); + return FAILURE; +} + +static zend_ulong king_inference_tensor_option_ulong( + zval *options, + const char *key, + zend_ulong fallback +) { + zval *option; + + if (options == NULL || Z_TYPE_P(options) != IS_ARRAY) { + return fallback; + } + + option = king_inference_array_find(options, key); + if (option == NULL || Z_TYPE_P(option) != IS_LONG || Z_LVAL_P(option) < 0) { + return fallback; + } + + return (zend_ulong) Z_LVAL_P(option); +} + +static zend_result king_inference_tensor_dequantize_array( + king_inference_model_object *model, + zend_string *name, + zval *options, + zval *return_value +) { + king_inference_tensor_native_view view; + zend_ulong offset = king_inference_tensor_option_ulong(options, "offset", 0); + zend_ulong count = king_inference_tensor_option_ulong(options, "count", 32); + zend_ulong max_values = king_inference_tensor_option_ulong(options, "max_values", 4096); + zend_ulong i; + zval values; + + if (king_inference_tensor_native_view_resolve(model, name, &view) != SUCCESS) { + return FAILURE; + } + if (count == 0) { + count = view.type_info.block_size; + } + if (count > max_values || offset > view.elements || count > view.elements - offset) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "King inference tensor dequantize range is outside tensor bounds or max_values." + ); + return FAILURE; + } + + array_init(&values); + for (i = 0; i < count; i++) { + double value; + if (king_inference_tensor_value_at(&view, offset + i, &value) != SUCCESS) { + zval_ptr_dtor(&values); + return FAILURE; + } + add_next_index_double(&values, value); + } + + array_init(return_value); + add_assoc_str(return_value, "tensor", zend_string_copy(name)); + add_assoc_string(return_value, "type_name", view.type_info.name); + add_assoc_long(return_value, "offset", (zend_long) offset); + add_assoc_long(return_value, "count", (zend_long) count); + add_assoc_long(return_value, "elements", (zend_long) view.elements); + add_assoc_long(return_value, "block_size", (zend_long) view.type_info.block_size); + add_assoc_zval(return_value, "values", &values); + return SUCCESS; +} + +static zend_result king_inference_tensor_vector_from_array( + zval *input, + zend_ulong expected, + zend_ulong max_input_values, + double **vector_out +) { + zval *entry; + zend_ulong index = 0; + double *vector; + + *vector_out = NULL; + if (Z_TYPE_P(input) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL_P(input)) != expected) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference tensor matmul input vector length does not match tensor columns."); + return FAILURE; + } + if (expected == 0 || expected > max_input_values || expected > (zend_ulong) (SIZE_MAX / sizeof(double))) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference tensor matmul input vector is too large."); + return FAILURE; + } + + vector = ecalloc((size_t) expected, sizeof(double)); + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(input), entry) { + if (Z_TYPE_P(entry) == IS_LONG) { + vector[index] = (double) Z_LVAL_P(entry); + } else if (Z_TYPE_P(entry) == IS_DOUBLE) { + vector[index] = Z_DVAL_P(entry); + } else { + efree(vector); + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference tensor matmul input vector must contain only int or float values."); + return FAILURE; + } + index++; + } ZEND_HASH_FOREACH_END(); + + *vector_out = vector; + return SUCCESS; +} + +static zend_result king_inference_tensor_block_dot( + king_inference_tensor_native_view *view, + zend_ulong block_index, + const double *vector, + zend_ulong vector_offset, + double *sum +) { + const unsigned char *block; + zend_ulong i; + + switch (view->type) { + case 2: + block = view->data + block_index * 18; + for (i = 0; i < 32; i++) { + unsigned char packed = block[2 + (i & 15)]; + int quant = (int) (i < 16 ? (packed & 0x0fU) : (packed >> 4)) - 8; + double scale = (double) king_inference_tensor_f16_to_float(king_inference_tensor_read_u16(block)); + *sum += scale * (double) quant * vector[vector_offset + i]; + } + return SUCCESS; + case 3: + block = view->data + block_index * 20; + for (i = 0; i < 32; i++) { + unsigned char packed = block[4 + (i & 15)]; + int quant = (int) (i < 16 ? (packed & 0x0fU) : (packed >> 4)); + double scale = (double) king_inference_tensor_f16_to_float(king_inference_tensor_read_u16(block)); + double minimum = (double) king_inference_tensor_f16_to_float(king_inference_tensor_read_u16(block + 2)); + *sum += (minimum + scale * (double) quant) * vector[vector_offset + i]; + } + return SUCCESS; + case 6: + block = view->data + block_index * 22; + for (i = 0; i < 32; i++) { + *sum += king_inference_tensor_q5_0_value(block, i) * vector[vector_offset + i]; + } + return SUCCESS; + case 8: + block = view->data + block_index * 34; + { + double scale = (double) king_inference_tensor_f16_to_float(king_inference_tensor_read_u16(block)); + for (i = 0; i < 32; i++) { + *sum += scale * (double) ((int8_t) block[2 + i]) * vector[vector_offset + i]; + } + } + return SUCCESS; + case 12: + block = view->data + block_index * 144; + for (i = 0; i < 256; i++) { + *sum += king_inference_tensor_q4_k_value(block, i) * vector[vector_offset + i]; + } + return SUCCESS; + case 13: + block = view->data + block_index * 176; + for (i = 0; i < 256; i++) { + *sum += king_inference_tensor_q5_k_value(block, i) * vector[vector_offset + i]; + } + return SUCCESS; + case 14: + block = view->data + block_index * 210; + for (i = 0; i < 256; i++) { + *sum += king_inference_tensor_q6_k_value(block, i) * vector[vector_offset + i]; + } + return SUCCESS; + } + + return FAILURE; +} + +static zend_result king_inference_tensor_row_dot( + king_inference_tensor_native_view *view, + zend_ulong base, + zend_ulong cols, + const double *vector, + double *sum +) { + zend_ulong col = 0; + + *sum = 0.0; + while (col < cols) { + zend_ulong global_index = base + col; + zend_ulong block_size = view->type_info.block_size; + + if (block_size > 1 + && (global_index % block_size) == 0 + && cols - col >= block_size + && king_inference_tensor_block_dot( + view, + global_index / block_size, + vector, + col, + sum + ) == SUCCESS) { + col += block_size; + continue; + } + + { + double weight; + if (king_inference_tensor_value_at(view, global_index, &weight) != SUCCESS) { + return FAILURE; + } + *sum += weight * vector[col]; + } + col++; + } + + return SUCCESS; +} + +static zend_result king_inference_tensor_matmul_array( + king_inference_model_object *model, + zend_string *name, + zval *input, + zval *options, + zval *return_value +) { + king_inference_tensor_native_view view; + zend_ulong rows = 1; + zend_ulong cols = 0; + zend_ulong row_offset = king_inference_tensor_option_ulong(options, "row_offset", 0); + zend_ulong row_limit; + zend_ulong max_output_values = king_inference_tensor_option_ulong(options, "max_output_values", 65536); + zend_ulong max_input_values = king_inference_tensor_option_ulong(options, "max_input_values", 262144); + zend_ulong max_operations = king_inference_tensor_option_ulong(options, "max_operations", 16777216); + zend_ulong output_count; + zend_ulong row; + double *vector = NULL; + zval output; + + if (king_inference_tensor_native_view_resolve(model, name, &view) != SUCCESS) { + return FAILURE; + } + if (view.rank == 1) { + cols = view.elements; + } else if (view.rank == 2) { + if (king_inference_tensor_dimension_at(view.dimensions, 0, &cols) != SUCCESS + || king_inference_tensor_dimension_at(view.dimensions, 1, &rows) != SUCCESS) { + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King inference tensor '%s' has invalid matrix dimensions.", ZSTR_VAL(name)); + return FAILURE; + } + } else { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference tensor matmul currently accepts rank-1 or rank-2 tensors."); + return FAILURE; + } + + if (row_offset > rows) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference tensor matmul row_offset is outside tensor rows."); + return FAILURE; + } + row_limit = king_inference_tensor_option_ulong(options, "row_limit", rows - row_offset); + if (row_limit > rows - row_offset) { + row_limit = rows - row_offset; + } + output_count = row_limit; + if (output_count == 0 + || output_count > max_output_values + || (cols > 0 && output_count > max_operations / cols)) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference tensor matmul output exceeds configured limits."); + return FAILURE; + } + if (king_inference_tensor_vector_from_array(input, cols, max_input_values, &vector) != SUCCESS) { + return FAILURE; + } + + array_init(&output); + for (row = 0; row < output_count; row++) { + double sum; + zend_ulong base = (row_offset + row) * cols; + + if (king_inference_tensor_row_dot(&view, base, cols, vector, &sum) != SUCCESS) { + efree(vector); + zval_ptr_dtor(&output); + return FAILURE; + } + add_next_index_double(&output, sum); + } + efree(vector); + + array_init(return_value); + add_assoc_str(return_value, "tensor", zend_string_copy(name)); + add_assoc_string(return_value, "type_name", view.type_info.name); + add_assoc_long(return_value, "rank", (zend_long) view.rank); + add_assoc_long(return_value, "rows", (zend_long) rows); + add_assoc_long(return_value, "cols", (zend_long) cols); + add_assoc_long(return_value, "row_offset", (zend_long) row_offset); + add_assoc_long(return_value, "output_count", (zend_long) output_count); + add_assoc_bool(return_value, "blockwise", view.type_info.block_size > 1); + add_assoc_bool(return_value, "complete", row_offset == 0 && output_count == rows); + add_assoc_zval(return_value, "output", &output); + return SUCCESS; +} diff --git a/extension/src/inference/tensor/core/tensor_view.inc b/extension/src/inference/tensor/core/tensor_view.inc new file mode 100644 index 000000000..10a3a4d9a --- /dev/null +++ b/extension/src/inference/tensor/core/tensor_view.inc @@ -0,0 +1,298 @@ +typedef struct _king_inference_tensor_type_info { + const char *name; + zend_ulong block_size; + zend_ulong type_size; +} king_inference_tensor_type_info; + +static bool king_inference_tensor_type_lookup( + zend_ulong type, + king_inference_tensor_type_info *info +) { + info->name = "unknown"; + info->block_size = 0; + info->type_size = 0; + + switch (type) { + case 0: info->name = "F32"; info->block_size = 1; info->type_size = 4; return true; + case 1: info->name = "F16"; info->block_size = 1; info->type_size = 2; return true; + case 2: info->name = "Q4_0"; info->block_size = 32; info->type_size = 18; return true; + case 3: info->name = "Q4_1"; info->block_size = 32; info->type_size = 20; return true; + case 6: info->name = "Q5_0"; info->block_size = 32; info->type_size = 22; return true; + case 7: info->name = "Q5_1"; info->block_size = 32; info->type_size = 24; return true; + case 8: info->name = "Q8_0"; info->block_size = 32; info->type_size = 34; return true; + case 9: info->name = "Q8_1"; info->block_size = 32; info->type_size = 40; return true; + case 10: info->name = "Q2_K"; info->block_size = 256; info->type_size = 84; return true; + case 11: info->name = "Q3_K"; info->block_size = 256; info->type_size = 110; return true; + case 12: info->name = "Q4_K"; info->block_size = 256; info->type_size = 144; return true; + case 13: info->name = "Q5_K"; info->block_size = 256; info->type_size = 176; return true; + case 14: info->name = "Q6_K"; info->block_size = 256; info->type_size = 210; return true; + case 15: info->name = "Q8_K"; info->block_size = 256; info->type_size = 292; return true; + case 16: info->name = "IQ2_XXS"; info->block_size = 256; info->type_size = 66; return true; + case 17: info->name = "IQ2_XS"; info->block_size = 256; info->type_size = 74; return true; + case 18: info->name = "IQ3_XXS"; info->block_size = 256; info->type_size = 98; return true; + case 19: info->name = "IQ1_S"; info->block_size = 256; info->type_size = 50; return true; + case 20: info->name = "IQ4_NL"; info->block_size = 32; info->type_size = 18; return true; + case 21: info->name = "IQ3_S"; info->block_size = 256; info->type_size = 110; return true; + case 22: info->name = "IQ2_S"; info->block_size = 256; info->type_size = 70; return true; + case 23: info->name = "IQ4_XS"; info->block_size = 256; info->type_size = 136; return true; + case 24: info->name = "I8"; info->block_size = 1; info->type_size = 1; return true; + case 25: info->name = "I16"; info->block_size = 1; info->type_size = 2; return true; + case 26: info->name = "I32"; info->block_size = 1; info->type_size = 4; return true; + case 27: info->name = "I64"; info->block_size = 1; info->type_size = 8; return true; + case 28: info->name = "F64"; info->block_size = 1; info->type_size = 8; return true; + case 29: info->name = "IQ1_M"; info->block_size = 256; info->type_size = 56; return true; + case 30: info->name = "BF16"; info->block_size = 1; info->type_size = 2; return true; + case 31: info->name = "Q4_0_4_4"; info->block_size = 32; info->type_size = 18; return true; + case 32: info->name = "Q4_0_4_8"; info->block_size = 32; info->type_size = 18; return true; + case 33: info->name = "Q4_0_8_8"; info->block_size = 32; info->type_size = 18; return true; + } + + return false; +} + +static bool king_inference_tensor_descriptor_ulong( + zval *descriptor, + const char *key, + size_t key_len, + zend_ulong *value +) { + zval *field; + + if (descriptor == NULL || Z_TYPE_P(descriptor) != IS_ARRAY) { + return false; + } + + field = zend_hash_str_find(Z_ARRVAL_P(descriptor), key, key_len); + if (field == NULL || Z_TYPE_P(field) != IS_LONG || Z_LVAL_P(field) < 0) { + return false; + } + + *value = (zend_ulong) Z_LVAL_P(field); + return true; +} + +static bool king_inference_tensor_mul_checked( + zend_ulong left, + zend_ulong right, + zend_ulong *result +) { + if (right != 0 && left > ZEND_ULONG_MAX / right) { + return false; + } + + *result = left * right; + return true; +} + +static bool king_inference_tensor_add_checked( + zend_ulong left, + zend_ulong right, + zend_ulong *result +) { + if (left > ZEND_ULONG_MAX - right) { + return false; + } + + *result = left + right; + return true; +} + +static zend_result king_inference_tensor_descriptor_view_array( + king_inference_model_object *model, + zend_string *name, + zval *descriptor, + zval *return_value +) { + king_inference_tensor_type_info type_info; + zend_ulong rank; + zend_ulong type; + zend_ulong elements; + zend_ulong relative_offset; + zend_ulong blocks = 0; + zend_ulong byte_length = 0; + zend_ulong absolute_offset = 0; + zend_ulong byte_end = 0; + bool known_type; + bool byte_length_safe = false; + bool absolute_offset_safe = false; + bool byte_end_safe = false; + bool bounds_safe = false; + zval *dimensions; + zval dimensions_copy; + + if (!king_inference_tensor_descriptor_ulong(descriptor, "rank", sizeof("rank") - 1, &rank) + || !king_inference_tensor_descriptor_ulong(descriptor, "type", sizeof("type") - 1, &type) + || !king_inference_tensor_descriptor_ulong(descriptor, "elements", sizeof("elements") - 1, &elements) + || !king_inference_tensor_descriptor_ulong(descriptor, "relative_offset", sizeof("relative_offset") - 1, &relative_offset)) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "King inference tensor '%s' has an invalid internal descriptor.", + ZSTR_VAL(name) + ); + return FAILURE; + } + + dimensions = zend_hash_str_find(Z_ARRVAL_P(descriptor), "dimensions", sizeof("dimensions") - 1); + if (dimensions == NULL || Z_TYPE_P(dimensions) != IS_ARRAY) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "King inference tensor '%s' has no valid dimension vector.", + ZSTR_VAL(name) + ); + return FAILURE; + } + + known_type = king_inference_tensor_type_lookup(type, &type_info); + if (known_type && type_info.block_size > 0) { + blocks = elements == 0 ? 0 : 1 + ((elements - 1) / type_info.block_size); + byte_length_safe = king_inference_tensor_mul_checked(blocks, type_info.type_size, &byte_length); + } + absolute_offset_safe = king_inference_tensor_add_checked( + model->gguf.tensor_data_offset, + relative_offset, + &absolute_offset + ); + byte_end_safe = absolute_offset_safe + && byte_length_safe + && king_inference_tensor_add_checked(absolute_offset, byte_length, &byte_end); + bounds_safe = known_type + && byte_length_safe + && byte_end_safe + && byte_end <= model->gguf.file_size; + + array_init(return_value); + add_assoc_str(return_value, "name", zend_string_copy(name)); + add_assoc_long(return_value, "rank", (zend_long) rank); + ZVAL_COPY(&dimensions_copy, dimensions); + add_assoc_zval(return_value, "dimensions", &dimensions_copy); + add_assoc_long(return_value, "type", (zend_long) type); + add_assoc_string(return_value, "type_name", type_info.name); + add_assoc_bool(return_value, "known_type", known_type); + add_assoc_bool(return_value, "quantized", type_info.block_size > 1); + add_assoc_long(return_value, "block_size", (zend_long) type_info.block_size); + add_assoc_long(return_value, "type_size", (zend_long) type_info.type_size); + add_assoc_long(return_value, "elements", (zend_long) elements); + add_assoc_long(return_value, "blocks", (zend_long) blocks); + add_assoc_long(return_value, "relative_offset", (zend_long) relative_offset); + add_assoc_long(return_value, "tensor_data_offset", (zend_long) model->gguf.tensor_data_offset); + add_assoc_long(return_value, "absolute_offset", (zend_long) absolute_offset); + add_assoc_long(return_value, "byte_length", (zend_long) byte_length); + add_assoc_long(return_value, "byte_end", (zend_long) byte_end); + add_assoc_bool(return_value, "bounds_safe", bounds_safe); + add_assoc_bool(return_value, "native_mapped", model->native_map_loaded); + add_assoc_long(return_value, "native_map_bytes", (zend_long) model->native_map_size); + add_assoc_bool(return_value, "native_view_ready", bounds_safe && model->native_map_loaded); + + return SUCCESS; +} + +static zend_result king_inference_tensor_view_array( + king_inference_model_object *model, + zend_string *name, + zval *return_value +) { + zval *descriptor; + + if (Z_ISUNDEF(model->tensor_index) || Z_TYPE(model->tensor_index) != IS_ARRAY) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "King inference model has no native tensor index." + ); + return FAILURE; + } + + descriptor = zend_hash_find(Z_ARRVAL(model->tensor_index), name); + if (descriptor == NULL) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "King inference tensor '%s' was not found in the model index.", + ZSTR_VAL(name) + ); + return FAILURE; + } + + return king_inference_tensor_descriptor_view_array(model, name, descriptor, return_value); +} + +static bool king_inference_tensor_name_matches_prefix( + zend_string *name, + zend_string *prefix +) { + if (prefix == NULL || ZSTR_LEN(prefix) == 0) { + return true; + } + if (name == NULL || ZSTR_LEN(name) < ZSTR_LEN(prefix)) { + return false; + } + + return memcmp(ZSTR_VAL(name), ZSTR_VAL(prefix), ZSTR_LEN(prefix)) == 0; +} + +static void king_inference_tensor_index_array( + king_inference_model_object *model, + zval *options, + zval *return_value +) { + zend_long limit = 256; + zend_string *prefix = NULL; + zend_ulong matched = 0; + zend_ulong returned = 0; + zval tensors; + zval *option; + zend_string *name; + zval *descriptor; + + if (Z_ISUNDEF(model->tensor_index) || Z_TYPE(model->tensor_index) != IS_ARRAY) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "King inference model has no native tensor index." + ); + RETURN_THROWS(); + } + + if (options != NULL && Z_TYPE_P(options) == IS_ARRAY) { + option = king_inference_array_find(options, "limit"); + if (option != NULL && Z_TYPE_P(option) == IS_LONG) { + limit = Z_LVAL_P(option); + } + option = king_inference_array_find(options, "prefix"); + if (option != NULL && Z_TYPE_P(option) == IS_STRING) { + prefix = Z_STR_P(option); + } + } + + array_init(&tensors); + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL(model->tensor_index), name, descriptor) { + zval view; + + if (name == NULL || !king_inference_tensor_name_matches_prefix(name, prefix)) { + continue; + } + matched++; + if (limit > 0 && returned >= (zend_ulong) limit) { + continue; + } + if (king_inference_tensor_descriptor_view_array(model, name, descriptor, &view) != SUCCESS) { + zval_ptr_dtor(&tensors); + RETURN_THROWS(); + } + zend_hash_update(Z_ARRVAL(tensors), name, &view); + returned++; + } ZEND_HASH_FOREACH_END(); + + array_init(return_value); + add_assoc_long(return_value, "count", (zend_long) zend_hash_num_elements(Z_ARRVAL(model->tensor_index))); + add_assoc_long(return_value, "matched", (zend_long) matched); + add_assoc_long(return_value, "returned", (zend_long) returned); + add_assoc_long(return_value, "limit", limit); + add_assoc_bool(return_value, "truncated", limit > 0 && matched > returned); + if (prefix != NULL) { + add_assoc_str(return_value, "prefix", zend_string_copy(prefix)); + } + add_assoc_zval(return_value, "tensors", &tensors); +} diff --git a/extension/src/inference/tensor/graph/builder/token_decode_graph_attention_norm.inc b/extension/src/inference/tensor/graph/builder/token_decode_graph_attention_norm.inc new file mode 100644 index 000000000..19f98b6a6 --- /dev/null +++ b/extension/src/inference/tensor/graph/builder/token_decode_graph_attention_norm.inc @@ -0,0 +1,64 @@ +static zend_string *king_inference_decode_graph_optional_vector_tensor_name( + king_inference_model_object *model, + zend_ulong layer, + const char *suffix +) { + char name[96]; + zend_string *tensor_name; + zval *descriptor; + zend_ulong rank; + zend_ulong width; + + if (snprintf(name, sizeof(name), "blk.%lu.%s", (unsigned long) layer, suffix) >= (int) sizeof(name)) { + return NULL; + } + + tensor_name = zend_string_init(name, strlen(name), 0); + descriptor = king_inference_tensor_index_descriptor(model, tensor_name); + if (descriptor == NULL + || !king_inference_tensor_descriptor_rank(descriptor, &rank) + || rank != 1 + || !king_inference_tensor_descriptor_dimension(descriptor, 0, &width) + || width == 0) { + zend_string_release(tensor_name); + return NULL; + } + + return tensor_name; +} + +static zend_string *king_inference_decode_graph_attention_norm_name( + king_inference_model_object *model, + zend_ulong layer, + const char *suffix +) { + return king_inference_decode_graph_optional_vector_tensor_name(model, layer, suffix); +} + +static void king_inference_decode_graph_attention_norms( + king_inference_model_object *model, + zend_ulong layer, + zend_string **query_norm, + zend_string **key_norm +) { + *query_norm = king_inference_decode_graph_attention_norm_name(model, layer, "attn_q_norm.weight"); + *key_norm = king_inference_decode_graph_attention_norm_name(model, layer, "attn_k_norm.weight"); + if (*query_norm == NULL || *key_norm == NULL) { + if (*query_norm != NULL) { + zend_string_release(*query_norm); + *query_norm = NULL; + } + if (*key_norm != NULL) { + zend_string_release(*key_norm); + *key_norm = NULL; + } + } +} + +static zend_string *king_inference_decode_graph_post_norm_name( + king_inference_model_object *model, + zend_ulong layer, + const char *suffix +) { + return king_inference_decode_graph_optional_vector_tensor_name(model, layer, suffix); +} diff --git a/extension/src/inference/tensor/graph/builder/token_decode_graph_builder.inc b/extension/src/inference/tensor/graph/builder/token_decode_graph_builder.inc new file mode 100644 index 000000000..d32bf47d8 --- /dev/null +++ b/extension/src/inference/tensor/graph/builder/token_decode_graph_builder.inc @@ -0,0 +1,809 @@ +#include "token_decode_graph_options.inc" + +static zend_result king_inference_decode_graph_required_shape( + king_inference_model_object *model, + bool needs_logits, + zend_ulong *layers_out, + zend_ulong *heads_out, + zend_ulong *kv_heads_out, + zend_ulong *key_length_out, + zend_ulong *value_length_out, + zend_ulong *vocab_out +) { + zend_ulong embedding = model->gguf.architecture_params[KING_INFERENCE_ARCH_EMBEDDING_LENGTH]; + zend_ulong layers = model->gguf.architecture_params[KING_INFERENCE_ARCH_BLOCK_COUNT]; + zend_ulong heads = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT]; + zend_ulong kv_heads = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT_KV]; + zend_ulong key_length = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_KEY_LENGTH]; + zend_ulong value_length = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_VALUE_LENGTH]; + zend_ulong feed_forward = model->gguf.architecture_params[KING_INFERENCE_ARCH_FEED_FORWARD_LENGTH]; + zend_ulong vocab = model->gguf.tokenizer_token_count; + zend_ulong metadata_key_length = key_length; + zend_ulong metadata_value_length = value_length; + + if (layers == 0 || heads == 0 || embedding == 0 || feed_forward == 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph requires loaded block, head, embedding, and feed-forward metadata."); + return FAILURE; + } + if (kv_heads == 0) { + kv_heads = heads; + } + if (key_length == 0) { + if (embedding % heads != 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph cannot derive attention key length from embedding and head count."); + return FAILURE; + } + key_length = embedding / heads; + } + if (value_length == 0) { + value_length = key_length; + } + king_inference_attention_refine_runtime_shape_from_tensors( + model, + &heads, + &kv_heads, + &key_length, + &value_length + ); + if (metadata_key_length > 0) { + key_length = metadata_key_length; + } + if (metadata_value_length > 0) { + value_length = metadata_value_length; + } + if (key_length == 0 || value_length == 0 || kv_heads == 0 || heads < kv_heads) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph requires valid attention key/value dimensions."); + return FAILURE; + } + if ((key_length % 2) != 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph requires an even attention key length for RoPE."); + return FAILURE; + } + if (needs_logits && vocab == 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph requires tokenizer vocabulary metadata when logits output is enabled."); + return FAILURE; + } + + *layers_out = layers; + *heads_out = heads; + *kv_heads_out = kv_heads; + *key_length_out = key_length; + *value_length_out = value_length; + *vocab_out = vocab; + return SUCCESS; +} + +static zend_result king_inference_decode_graph_format_id( + char *buffer, + size_t buffer_size, + const char *format, + zend_ulong first, + zend_ulong second +) { + int written = snprintf(buffer, buffer_size, format, (unsigned long) first, (unsigned long) second); + + if (written <= 0 || (size_t) written >= buffer_size) { + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King inference token decode graph id buffer is too small."); + return FAILURE; + } + return SUCCESS; +} + +static void king_inference_decode_graph_attention_window( + king_inference_model_object *model, + zend_ulong position, + zend_ulong sliding_window, + zend_ulong *slot_start, + zend_ulong *slot_count +) { + zend_ulong start = sliding_window > 0 && position >= sliding_window + ? position - sliding_window + 1 + : 0; + *slot_start = start; + *slot_count = position - start + 1; +} + +static void king_inference_decode_graph_start_op(zval *op, const char *id, const char *name) +{ + array_init(op); + add_assoc_string(op, "id", id); + add_assoc_string(op, "op", name); +} + +static void king_inference_decode_graph_push_op(zval *ops, zval *op) +{ + add_next_index_zval(ops, op); +} + +static void king_inference_decode_graph_add_embedding( + zval *ops, + zend_string *tensor, + zend_ulong token_id, + double scale +) { + zval op; + + king_inference_decode_graph_start_op(&op, "x", "embedding"); + add_assoc_str(&op, "tensor", zend_string_copy(tensor)); + add_assoc_long(&op, "token_id", (zend_long) token_id); + if (scale != 1.0) { + add_assoc_double(&op, "scale", scale); + } + king_inference_decode_graph_push_op(ops, &op); +} + +static void king_inference_decode_graph_add_rms_norm( + zval *ops, + const char *id, + const char *input, + zend_string *weight, + double epsilon +) { + zval op; + + king_inference_decode_graph_start_op(&op, id, "rms_norm"); + add_assoc_string(&op, "input", input); + add_assoc_str(&op, "weight", zend_string_copy(weight)); + add_assoc_double(&op, "epsilon", epsilon); + king_inference_decode_graph_push_op(ops, &op); +} + +static void king_inference_decode_graph_add_linear( + zval *ops, + const char *id, + const char *input, + zend_string *weight, + zend_ulong row_limit +) { + zval op; + + king_inference_decode_graph_start_op(&op, id, "linear"); + add_assoc_string(&op, "input", input); + add_assoc_str(&op, "weight", zend_string_copy(weight)); + if (row_limit > 0) { + add_assoc_long(&op, "row_limit", (zend_long) row_limit); + } + king_inference_decode_graph_push_op(ops, &op); +} + +static void king_inference_decode_graph_add_binary( + zval *ops, + const char *id, + const char *op_name, + const char *left, + const char *right +) { + zval op; + + king_inference_decode_graph_start_op(&op, id, op_name); + add_assoc_string(&op, "left", left); + add_assoc_string(&op, "right", right); + king_inference_decode_graph_push_op(ops, &op); +} + +static void king_inference_decode_graph_add_unary( + zval *ops, + const char *id, + const char *op_name, + const char *input +) { + zval op; + + king_inference_decode_graph_start_op(&op, id, op_name); + add_assoc_string(&op, "input", input); + king_inference_decode_graph_push_op(ops, &op); +} + +static void king_inference_decode_graph_add_slice( + zval *ops, + const char *id, + const char *input, + zend_ulong offset, + zend_ulong count +) { + zval op; + + king_inference_decode_graph_start_op(&op, id, "slice"); + add_assoc_string(&op, "input", input); + add_assoc_long(&op, "offset", (zend_long) offset); + add_assoc_long(&op, "count", (zend_long) count); + king_inference_decode_graph_push_op(ops, &op); +} + +static void king_inference_decode_graph_add_rope( + zval *ops, + const char *id, + const char *input, + zend_ulong position, + zend_ulong head_dim, + double rope_base, + zend_ulong pairing +) { + zval op; + zval inv_freqs; + zend_ulong i; + + king_inference_decode_graph_start_op(&op, id, "rope"); + add_assoc_string(&op, "input", input); + add_assoc_long(&op, "position", (zend_long) position); + add_assoc_long(&op, "head_dim", (zend_long) head_dim); + add_assoc_double(&op, "rope_base", rope_base); + add_assoc_long(&op, "pairing", (zend_long) pairing); + array_init(&inv_freqs); + for (i = 0; i < head_dim / 2; i++) { + add_next_index_double(&inv_freqs, 1.0 / pow(rope_base, ((double) i * 2.0) / (double) head_dim)); + } + add_assoc_zval(&op, "inv_freqs", &inv_freqs); + king_inference_decode_graph_push_op(ops, &op); +} + +static bool king_inference_decode_graph_gemma_profile(king_inference_model_object *model) +{ + return king_inference_gguf_architecture_is_gemma3(&model->gguf) + || king_inference_gguf_architecture_is_gemma4(&model->gguf); +} + +static double king_inference_decode_graph_layer_rope_base( + king_inference_model_object *model, + zend_ulong layer, + double global_rope_base, + double local_rope_base, + zend_ulong model_key_length, + zend_ulong layer_key_length +) { + if (king_inference_decode_graph_gemma_profile(model) + && global_rope_base > 0.0 + && local_rope_base > 0.0) { + return ((layer + 1) % 6) == 0 ? global_rope_base : local_rope_base; + } + + return local_rope_base > 0.0 && model_key_length > 0 && layer_key_length < model_key_length + ? local_rope_base + : global_rope_base; +} + +static const char *king_inference_decode_graph_ffn_activation(king_inference_model_object *model) +{ + return king_inference_decode_graph_gemma_profile(model) ? "gelu_tanh" : "silu"; +} + +static double king_inference_decode_graph_embedding_scale(king_inference_model_object *model) +{ + zend_ulong embedding = model->gguf.architecture_params[KING_INFERENCE_ARCH_EMBEDDING_LENGTH]; + + return king_inference_decode_graph_gemma_profile(model) && embedding > 0 + ? sqrt((double) embedding) + : 1.0; +} + +static void king_inference_decode_graph_add_kv_write( + zval *ops, + const char *id, + const char *cache, + zend_ulong slot, + const char *key, + zend_ulong key_length, + const char *value, + zend_ulong value_length +) { + zval op; + + king_inference_decode_graph_start_op(&op, id, "kv_write"); + add_assoc_string(&op, "cache", cache); + add_assoc_long(&op, "slot", (zend_long) slot); + add_assoc_string(&op, "key", key); + add_assoc_long(&op, "key_length", (zend_long) key_length); + add_assoc_string(&op, "value", value); + add_assoc_long(&op, "value_length", (zend_long) value_length); + king_inference_decode_graph_push_op(ops, &op); +} + +static void king_inference_decode_graph_add_kv_attention( + zval *ops, + const char *id, + const char *cache, + const char *query, + zend_ulong slot_start, + zend_ulong slot_count, + zend_ulong key_length, + zend_ulong value_length, + double scale +) { + zval op; + + king_inference_decode_graph_start_op(&op, id, "kv_attention"); + add_assoc_string(&op, "cache", cache); + add_assoc_string(&op, "query", query); + add_assoc_long(&op, "slot_start", (zend_long) slot_start); + add_assoc_long(&op, "slot_count", (zend_long) slot_count); + add_assoc_long(&op, "key_length", (zend_long) key_length); + add_assoc_long(&op, "value_length", (zend_long) value_length); + add_assoc_double(&op, "scale", scale); + king_inference_decode_graph_push_op(ops, &op); +} + +static void king_inference_decode_graph_add_stack(zval *ops, const char *id, zval *inputs) +{ + zval op; + zval copied_inputs; + + king_inference_decode_graph_start_op(&op, id, "stack"); + ZVAL_COPY(&copied_inputs, inputs); + add_assoc_zval(&op, "inputs", &copied_inputs); + king_inference_decode_graph_push_op(ops, &op); +} + +#include "token_decode_graph_sampler.inc" +#include "token_decode_graph_attention_norm.inc" +#include "token_decode_graph_debug.inc" + +static zend_result king_inference_token_decode_graph_array( + king_inference_model_object *model, + zval *token_source, + zend_ulong position, + zval *options, + zval *return_value +) { + zend_ulong layers; + zend_ulong heads; + zend_ulong kv_heads; + zend_ulong key_length; + zend_ulong value_length; + zend_ulong vocab; + zend_ulong model_layers; + zend_ulong sliding_window = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_SLIDING_WINDOW]; + zend_ulong slot_start; + zend_ulong slot_count; + zend_ulong layer; + zend_ulong token_id; + zend_string *token_embedding = NULL; + zend_string *output_projection = NULL; + zend_string *final_norm = NULL; + const char *status = NULL; + const char *final_norm_source = NULL; + const char *final_norm_status = NULL; + const char *output_projection_source = NULL; + const char *output_projection_status = NULL; + bool output_projection_tied = false; + bool emit_token = king_inference_decode_graph_bool_option(options, "emit_token", true); + bool emit_logits = king_inference_decode_graph_bool_option(options, "emit_logits", false); + bool needs_logits = emit_token || emit_logits; + const char *token_selection = "none"; + const char *token_selection_op = NULL; + double token_selection_temperature = 0.0; + zend_long token_selection_top_k = 0; + zend_long debug_layer_limit = 0; + double epsilon_default = model->gguf.attention_layer_norm_rms_epsilon > 0.0 + ? model->gguf.attention_layer_norm_rms_epsilon + : 0.000001; + double rope_base_default = model->gguf.rope_freq_base > 0.0 + ? model->gguf.rope_freq_base + : 10000.0; + double epsilon; + double rope_base; + double rope_base_swa; + double embedding_scale; + zend_ulong rope_pairing; + const char *ffn_activation; + char final_hidden_id[64]; + zval ops; + zval *state; + + if (king_inference_decode_graph_required_shape(model, needs_logits, &layers, &heads, &kv_heads, &key_length, &value_length, &vocab) != SUCCESS + || king_inference_decode_graph_double_option(options, "epsilon", epsilon_default, &epsilon) != SUCCESS + || king_inference_decode_graph_double_option(options, "rope_base", rope_base_default, &rope_base) != SUCCESS + || king_inference_decode_graph_double_option( + options, + "rope_base_swa", + model->gguf.rope_freq_base_swa > 0.0 ? model->gguf.rope_freq_base_swa : rope_base, + &rope_base_swa + ) != SUCCESS + || king_inference_decode_graph_non_negative_long_option( + options, + "debug_layer_limit", + 0, + &debug_layer_limit + ) != SUCCESS + || king_inference_decode_graph_token_id_from_source(model, token_source, position, &token_id) != SUCCESS) { + return FAILURE; + } + model_layers = layers; + if (debug_layer_limit > 0) { + if ((zend_ulong) debug_layer_limit > model_layers) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph debug_layer_limit exceeds the loaded model block count."); + return FAILURE; + } + layers = (zend_ulong) debug_layer_limit; + } + if (epsilon <= 0.0 || rope_base <= 0.0 || rope_base_swa <= 0.0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph epsilon and rope_base must be positive."); + return FAILURE; + } + rope_pairing = king_inference_decode_graph_gemma_profile(model) ? 1 : 0; + ffn_activation = king_inference_decode_graph_ffn_activation(model); + embedding_scale = king_inference_decode_graph_embedding_scale(model); + if (king_inference_resolve_token_embedding_tensor_name(model, NULL, &token_embedding, NULL, &status) != SUCCESS + || token_embedding == NULL) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph could not resolve token embedding tensor: %s.", status != NULL ? status : "not_found"); + return FAILURE; + } + if (king_inference_resolve_rms_norm_final_name(model, &final_norm, &final_norm_source, &final_norm_status) != SUCCESS + || final_norm == NULL) { + zend_string_release(token_embedding); + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph could not resolve final RMSNorm tensor: %s.", final_norm_status != NULL ? final_norm_status : "not_found"); + return FAILURE; + } + if (needs_logits + && (king_inference_resolve_output_projection_tensor_name(model, NULL, &output_projection, &output_projection_source, &output_projection_status, &output_projection_tied) != SUCCESS + || output_projection == NULL)) { + zend_string_release(token_embedding); + zend_string_release(final_norm); + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph could not resolve output projection tensor: %s.", output_projection_status != NULL ? output_projection_status : "not_found"); + return FAILURE; + } + + king_inference_decode_graph_attention_window( + model, + position, + sliding_window, + &slot_start, + &slot_count + ); + array_init(&ops); + king_inference_decode_graph_add_embedding(&ops, token_embedding, token_id, embedding_scale); + zend_string_release(token_embedding); + + for (layer = 0; layer < layers; layer++) { + zend_string *attn_norm = NULL; + zend_string *attn_q = NULL; + zend_string *attn_k = NULL; + zend_string *attn_v = NULL; + zend_string *attn_out = NULL; + zend_string *attn_q_norm = NULL; + zend_string *attn_k_norm = NULL; + zend_string *post_attention_norm = NULL; + zend_string *ffn_norm = NULL; + zend_string *ffn_gate = NULL; + zend_string *ffn_up = NULL; + zend_string *ffn_down = NULL; + zend_string *post_ffw_norm = NULL; + char hidden[64]; + char attn_norm_id[64]; + char q_id[64]; + char k_id[64]; + char v_id[64]; + char context_id[64]; + char attn_out_id[64]; + char post_attention_norm_id[64]; + char attn_residual_id[64]; + char ffn_norm_id[64]; + char gate_id[64]; + char up_id[64]; + char gate_act_id[64]; + char gated_id[64]; + char down_id[64]; + char post_ffw_norm_id[64]; + char output_id[64]; + const char *attn_residual_right = attn_out_id; + const char *ffn_residual_right = down_id; + zval contexts; + zend_ulong layer_heads = heads; + zend_ulong layer_kv_heads = kv_heads; + zend_ulong layer_key_length = key_length; + zend_ulong layer_value_length = value_length; + double layer_rope_base; + zend_ulong head; + + if (king_inference_decode_graph_format_id(hidden, sizeof(hidden), layer == 0 ? "x" : "l%lu_output", layer == 0 ? 0 : layer - 1, 0) != SUCCESS + || king_inference_decode_graph_format_id(attn_norm_id, sizeof(attn_norm_id), "l%lu_attn_norm", layer, 0) != SUCCESS + || king_inference_decode_graph_format_id(q_id, sizeof(q_id), "l%lu_q", layer, 0) != SUCCESS + || king_inference_decode_graph_format_id(k_id, sizeof(k_id), "l%lu_k", layer, 0) != SUCCESS + || king_inference_decode_graph_format_id(v_id, sizeof(v_id), "l%lu_v", layer, 0) != SUCCESS + || king_inference_decode_graph_format_id(context_id, sizeof(context_id), "l%lu_context", layer, 0) != SUCCESS + || king_inference_decode_graph_format_id(attn_out_id, sizeof(attn_out_id), "l%lu_attn_out", layer, 0) != SUCCESS + || king_inference_decode_graph_format_id(post_attention_norm_id, sizeof(post_attention_norm_id), "l%lu_post_attn_norm", layer, 0) != SUCCESS + || king_inference_decode_graph_format_id(attn_residual_id, sizeof(attn_residual_id), "l%lu_attn_residual", layer, 0) != SUCCESS + || king_inference_decode_graph_format_id(ffn_norm_id, sizeof(ffn_norm_id), "l%lu_ffn_norm", layer, 0) != SUCCESS + || king_inference_decode_graph_format_id(gate_id, sizeof(gate_id), "l%lu_ffn_gate", layer, 0) != SUCCESS + || king_inference_decode_graph_format_id(up_id, sizeof(up_id), "l%lu_ffn_up", layer, 0) != SUCCESS + || king_inference_decode_graph_format_id(gate_act_id, sizeof(gate_act_id), "l%lu_ffn_gate_silu", layer, 0) != SUCCESS + || king_inference_decode_graph_format_id(gated_id, sizeof(gated_id), "l%lu_ffn_gated", layer, 0) != SUCCESS + || king_inference_decode_graph_format_id(down_id, sizeof(down_id), "l%lu_ffn_down", layer, 0) != SUCCESS + || king_inference_decode_graph_format_id(post_ffw_norm_id, sizeof(post_ffw_norm_id), "l%lu_post_ffw_norm", layer, 0) != SUCCESS + || king_inference_decode_graph_format_id(output_id, sizeof(output_id), "l%lu_output", layer, 0) != SUCCESS) { + zval_ptr_dtor(&ops); + if (output_projection != NULL) { + zend_string_release(output_projection); + } + zend_string_release(final_norm); + return FAILURE; + } + + if (king_inference_resolve_rms_norm_layer_name(model, KING_INFERENCE_RMS_NORM_ATTENTION, layer, &attn_norm, NULL, &status) != SUCCESS + || king_inference_resolve_attention_tensor_name(model, KING_INFERENCE_ATTENTION_QUERY, layer, &attn_q, NULL, &status) != SUCCESS + || king_inference_resolve_attention_tensor_name(model, KING_INFERENCE_ATTENTION_KEY, layer, &attn_k, NULL, &status) != SUCCESS + || king_inference_resolve_attention_tensor_name(model, KING_INFERENCE_ATTENTION_VALUE, layer, &attn_v, NULL, &status) != SUCCESS + || king_inference_resolve_attention_tensor_name(model, KING_INFERENCE_ATTENTION_OUTPUT, layer, &attn_out, NULL, &status) != SUCCESS + || king_inference_resolve_rms_norm_layer_name(model, KING_INFERENCE_RMS_NORM_FEED_FORWARD, layer, &ffn_norm, NULL, &status) != SUCCESS + || king_inference_resolve_ffn_tensor_name(model, KING_INFERENCE_FFN_GATE, layer, &ffn_gate, NULL, &status) != SUCCESS + || king_inference_resolve_ffn_tensor_name(model, KING_INFERENCE_FFN_UP, layer, &ffn_up, NULL, &status) != SUCCESS + || king_inference_resolve_ffn_tensor_name(model, KING_INFERENCE_FFN_DOWN, layer, &ffn_down, NULL, &status) != SUCCESS) { + zval_ptr_dtor(&ops); + if (attn_norm != NULL) zend_string_release(attn_norm); + if (attn_q != NULL) zend_string_release(attn_q); + if (attn_k != NULL) zend_string_release(attn_k); + if (attn_v != NULL) zend_string_release(attn_v); + if (attn_out != NULL) zend_string_release(attn_out); + if (attn_q_norm != NULL) zend_string_release(attn_q_norm); + if (attn_k_norm != NULL) zend_string_release(attn_k_norm); + if (post_attention_norm != NULL) zend_string_release(post_attention_norm); + if (ffn_norm != NULL) zend_string_release(ffn_norm); + if (ffn_gate != NULL) zend_string_release(ffn_gate); + if (ffn_up != NULL) zend_string_release(ffn_up); + if (ffn_down != NULL) zend_string_release(ffn_down); + if (post_ffw_norm != NULL) zend_string_release(post_ffw_norm); + if (output_projection != NULL) zend_string_release(output_projection); + zend_string_release(final_norm); + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph could not resolve layer %lu tensor: %s.", (unsigned long) layer, status != NULL ? status : "not_found"); + return FAILURE; + } + king_inference_attention_refine_runtime_shape_from_tensors_for_layer( + model, + layer, + &layer_heads, + &layer_kv_heads, + &layer_key_length, + &layer_value_length + ); + king_inference_decode_graph_attention_norms(model, layer, &attn_q_norm, &attn_k_norm); + post_attention_norm = king_inference_decode_graph_post_norm_name(model, layer, "post_attention_norm.weight"); + post_ffw_norm = king_inference_decode_graph_post_norm_name(model, layer, "post_ffw_norm.weight"); + if (layer_key_length == 0 + || layer_value_length == 0 + || layer_kv_heads == 0 + || layer_heads < layer_kv_heads + || (layer_key_length % 2) != 0) { + zval_ptr_dtor(&ops); + zend_string_release(attn_norm); + zend_string_release(attn_q); + zend_string_release(attn_k); + zend_string_release(attn_v); + zend_string_release(attn_out); + if (attn_q_norm != NULL) zend_string_release(attn_q_norm); + if (attn_k_norm != NULL) zend_string_release(attn_k_norm); + if (post_attention_norm != NULL) zend_string_release(post_attention_norm); + zend_string_release(ffn_norm); + zend_string_release(ffn_gate); + zend_string_release(ffn_up); + zend_string_release(ffn_down); + if (post_ffw_norm != NULL) zend_string_release(post_ffw_norm); + if (output_projection != NULL) zend_string_release(output_projection); + zend_string_release(final_norm); + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph requires valid layer %lu attention dimensions.", (unsigned long) layer); + return FAILURE; + } + layer_rope_base = king_inference_decode_graph_layer_rope_base( + model, + layer, + rope_base, + rope_base_swa, + key_length, + layer_key_length + ); + + king_inference_decode_graph_add_rms_norm(&ops, attn_norm_id, hidden, attn_norm, epsilon); + king_inference_decode_graph_add_linear(&ops, q_id, attn_norm_id, attn_q, 0); + king_inference_decode_graph_add_linear(&ops, k_id, attn_norm_id, attn_k, 0); + king_inference_decode_graph_add_linear(&ops, v_id, attn_norm_id, attn_v, 0); + zend_string_release(attn_norm); + zend_string_release(attn_q); + zend_string_release(attn_k); + zend_string_release(attn_v); + + array_init(&contexts); + for (head = 0; head < layer_heads; head++) { + zend_ulong kv_head = king_inference_decode_graph_kv_head(head, layer_heads, layer_kv_heads); + char q_slice_id[64]; + char k_slice_id[64]; + char v_slice_id[64]; + char q_norm_id[64]; + char k_norm_id[64]; + char q_rope_id[64]; + char k_rope_id[64]; + char cache_id[64]; + char kv_write_id[64]; + char ctx_id[64]; + const char *q_rope_input = q_slice_id; + const char *k_rope_input = k_slice_id; + + if (king_inference_decode_graph_format_id(q_slice_id, sizeof(q_slice_id), "l%lu_h%lu_q_slice", layer, head) != SUCCESS + || king_inference_decode_graph_format_id(k_slice_id, sizeof(k_slice_id), "l%lu_h%lu_k_slice", layer, head) != SUCCESS + || king_inference_decode_graph_format_id(v_slice_id, sizeof(v_slice_id), "l%lu_h%lu_v_slice", layer, head) != SUCCESS + || king_inference_decode_graph_format_id(q_norm_id, sizeof(q_norm_id), "l%lu_h%lu_q_norm", layer, head) != SUCCESS + || king_inference_decode_graph_format_id(k_norm_id, sizeof(k_norm_id), "l%lu_h%lu_k_norm", layer, head) != SUCCESS + || king_inference_decode_graph_format_id(q_rope_id, sizeof(q_rope_id), "l%lu_h%lu_q_rope", layer, head) != SUCCESS + || king_inference_decode_graph_format_id(k_rope_id, sizeof(k_rope_id), "l%lu_h%lu_k_rope", layer, head) != SUCCESS + || king_inference_decode_graph_format_id(cache_id, sizeof(cache_id), "l%lu.h%lu", layer, kv_head) != SUCCESS + || king_inference_decode_graph_format_id(kv_write_id, sizeof(kv_write_id), "l%lu_h%lu_kv_write", layer, head) != SUCCESS + || king_inference_decode_graph_format_id(ctx_id, sizeof(ctx_id), "l%lu_h%lu_ctx", layer, head) != SUCCESS) { + zval_ptr_dtor(&contexts); + zval_ptr_dtor(&ops); + zend_string_release(attn_out); + if (attn_q_norm != NULL) zend_string_release(attn_q_norm); + if (attn_k_norm != NULL) zend_string_release(attn_k_norm); + if (post_attention_norm != NULL) zend_string_release(post_attention_norm); + zend_string_release(ffn_norm); + zend_string_release(ffn_gate); + zend_string_release(ffn_up); + zend_string_release(ffn_down); + if (post_ffw_norm != NULL) zend_string_release(post_ffw_norm); + if (output_projection != NULL) zend_string_release(output_projection); + zend_string_release(final_norm); + return FAILURE; + } + king_inference_decode_graph_add_slice(&ops, q_slice_id, q_id, head * layer_key_length, layer_key_length); + king_inference_decode_graph_add_slice(&ops, k_slice_id, k_id, kv_head * layer_key_length, layer_key_length); + king_inference_decode_graph_add_slice(&ops, v_slice_id, v_id, kv_head * layer_value_length, layer_value_length); + if (attn_q_norm != NULL && attn_k_norm != NULL) { + king_inference_decode_graph_add_rms_norm(&ops, q_norm_id, q_slice_id, attn_q_norm, epsilon); + king_inference_decode_graph_add_rms_norm(&ops, k_norm_id, k_slice_id, attn_k_norm, epsilon); + q_rope_input = q_norm_id; + k_rope_input = k_norm_id; + } + king_inference_decode_graph_add_rope(&ops, q_rope_id, q_rope_input, position, layer_key_length, layer_rope_base, rope_pairing); + king_inference_decode_graph_add_rope(&ops, k_rope_id, k_rope_input, position, layer_key_length, layer_rope_base, rope_pairing); + king_inference_decode_graph_add_kv_write(&ops, kv_write_id, cache_id, position, k_rope_id, layer_key_length, v_slice_id, layer_value_length); + king_inference_decode_graph_add_kv_attention(&ops, ctx_id, cache_id, q_rope_id, slot_start, slot_count, layer_key_length, layer_value_length, 1.0 / sqrt((double) layer_key_length)); + add_next_index_string(&contexts, ctx_id); + } + if (attn_q_norm != NULL) zend_string_release(attn_q_norm); + if (attn_k_norm != NULL) zend_string_release(attn_k_norm); + king_inference_decode_graph_add_stack(&ops, context_id, &contexts); + zval_ptr_dtor(&contexts); + king_inference_decode_graph_add_linear(&ops, attn_out_id, context_id, attn_out, 0); + zend_string_release(attn_out); + if (post_attention_norm != NULL) { + king_inference_decode_graph_add_rms_norm(&ops, post_attention_norm_id, attn_out_id, post_attention_norm, epsilon); + zend_string_release(post_attention_norm); + attn_residual_right = post_attention_norm_id; + } + king_inference_decode_graph_add_binary(&ops, attn_residual_id, "add", hidden, attn_residual_right); + king_inference_decode_graph_add_rms_norm(&ops, ffn_norm_id, attn_residual_id, ffn_norm, epsilon); + zend_string_release(ffn_norm); + king_inference_decode_graph_add_linear(&ops, gate_id, ffn_norm_id, ffn_gate, 0); + king_inference_decode_graph_add_linear(&ops, up_id, ffn_norm_id, ffn_up, 0); + zend_string_release(ffn_gate); + zend_string_release(ffn_up); + king_inference_decode_graph_add_unary(&ops, gate_act_id, ffn_activation, gate_id); + king_inference_decode_graph_add_binary(&ops, gated_id, "mul", gate_act_id, up_id); + king_inference_decode_graph_add_linear(&ops, down_id, gated_id, ffn_down, 0); + zend_string_release(ffn_down); + if (post_ffw_norm != NULL) { + king_inference_decode_graph_add_rms_norm(&ops, post_ffw_norm_id, down_id, post_ffw_norm, epsilon); + zend_string_release(post_ffw_norm); + ffn_residual_right = post_ffw_norm_id; + } + king_inference_decode_graph_add_binary(&ops, output_id, "add", attn_residual_id, ffn_residual_right); + } + + { + if (king_inference_decode_graph_format_id(final_hidden_id, sizeof(final_hidden_id), layers == 0 ? "x" : "l%lu_output", layers == 0 ? 0 : layers - 1, 0) != SUCCESS) { + zval_ptr_dtor(&ops); + if (output_projection != NULL) zend_string_release(output_projection); + zend_string_release(final_norm); + return FAILURE; + } + king_inference_decode_graph_add_rms_norm(&ops, "final_norm", final_hidden_id, final_norm, epsilon); + } + + if (needs_logits) { + king_inference_decode_graph_add_linear(&ops, "logits", "final_norm", output_projection, vocab); + } + if (emit_token) { + if (king_inference_decode_graph_add_sampler( + &ops, + "logits", + position, + options, + &token_selection, + &token_selection_op, + &token_selection_temperature, + &token_selection_top_k + ) != SUCCESS) { + zval_ptr_dtor(&ops); + if (output_projection != NULL) { + zend_string_release(output_projection); + } + zend_string_release(final_norm); + return FAILURE; + } + if (strcmp(token_selection, "sample") == 0 + && token_selection_top_k > 0 + && (zend_ulong) token_selection_top_k > vocab) { + token_selection_top_k = (zend_long) vocab; + } + } + + array_init(return_value); + add_assoc_string(return_value, "graph_type", "king_native_cpu_token_decode"); + add_assoc_long(return_value, "token_id", (zend_long) token_id); + add_assoc_long(return_value, "position", (zend_long) position); + add_assoc_long(return_value, "block_count", (zend_long) layers); + add_assoc_long(return_value, "model_block_count", (zend_long) model_layers); + if (debug_layer_limit > 0) { + add_assoc_long(return_value, "debug_layer_limit", debug_layer_limit); + } + add_assoc_long(return_value, "attention_head_count", (zend_long) heads); + add_assoc_long(return_value, "attention_head_count_kv", (zend_long) kv_heads); + add_assoc_long(return_value, "attention_key_length", (zend_long) key_length); + add_assoc_long(return_value, "attention_value_length", (zend_long) value_length); + add_assoc_string(return_value, "token_selection", token_selection); + if (token_selection_op != NULL) { + add_assoc_string(return_value, "token_selection_op", token_selection_op); + } + if (emit_token) { + add_assoc_double(return_value, "token_selection_temperature", token_selection_temperature); + add_assoc_long(return_value, "token_selection_top_k", token_selection_top_k); + } + add_assoc_zval(return_value, "ops", &ops); + add_assoc_string(return_value, "output", emit_token ? "next_token" : (emit_logits ? "logits" : "final_norm")); + { + zval terminal; + + array_init(&terminal); + add_assoc_string(&terminal, "hidden_output", final_hidden_id); + add_assoc_string(&terminal, "final_norm", "final_norm"); + add_assoc_str(&terminal, "final_norm_tensor", zend_string_copy(final_norm)); + if (final_norm_source != NULL) { + add_assoc_string(&terminal, "final_norm_source", final_norm_source); + } + if (final_norm_status != NULL) { + add_assoc_string(&terminal, "final_norm_status", final_norm_status); + } + add_assoc_bool(&terminal, "emits_token", emit_token); + add_assoc_bool(&terminal, "emits_logits", needs_logits); + add_assoc_string(&terminal, "token_selection", token_selection); + if (token_selection_op != NULL) { + add_assoc_string(&terminal, "token_selection_op", token_selection_op); + } + if (emit_token) { + add_assoc_double(&terminal, "token_selection_temperature", token_selection_temperature); + add_assoc_long(&terminal, "token_selection_top_k", token_selection_top_k); + } + if (needs_logits) { + add_assoc_string(&terminal, "logits", "logits"); + add_assoc_str(&terminal, "output_projection_tensor", zend_string_copy(output_projection)); + add_assoc_bool(&terminal, "output_projection_tied_token_embedding", output_projection_tied); + if (output_projection_source != NULL) { + add_assoc_string(&terminal, "output_projection_source", output_projection_source); + } + if (output_projection_status != NULL) { + add_assoc_string(&terminal, "output_projection_status", output_projection_status); + } + } + add_assoc_zval(return_value, "terminal", &terminal); + } + zend_string_release(final_norm); + if (output_projection != NULL) { + zend_string_release(output_projection); + } + if (king_inference_decode_graph_attach_debug(model, return_value) != SUCCESS) { + zval_ptr_dtor(return_value); + return FAILURE; + } + state = king_inference_array_find(options, "state"); + if (state != NULL) { + if (Z_TYPE_P(state) != IS_ARRAY) { + zval_ptr_dtor(return_value); + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph option 'state' must be an array when provided."); + return FAILURE; + } + { + zval copied_state; + ZVAL_COPY(&copied_state, state); + add_assoc_zval(return_value, "state", &copied_state); + } + } + + return SUCCESS; +} diff --git a/extension/src/inference/tensor/graph/builder/token_decode_graph_debug.inc b/extension/src/inference/tensor/graph/builder/token_decode_graph_debug.inc new file mode 100644 index 000000000..f9722cf90 --- /dev/null +++ b/extension/src/inference/tensor/graph/builder/token_decode_graph_debug.inc @@ -0,0 +1,532 @@ +static bool king_inference_decode_graph_debug_long_field(zval *source, const char *key, zend_ulong *value_out) +{ + zval *value = king_inference_array_find(source, key); + + if (value == NULL || Z_TYPE_P(value) != IS_LONG || Z_LVAL_P(value) < 0) { + return false; + } + + *value_out = (zend_ulong) Z_LVAL_P(value); + return true; +} + +static zval *king_inference_decode_graph_debug_find_op(zval *graph, const char *id) +{ + zval *ops = king_inference_array_find(graph, "ops"); + zval *entry; + + if (ops == NULL || Z_TYPE_P(ops) != IS_ARRAY) { + return NULL; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(ops), entry) { + zval *op_id; + + if (entry == NULL || Z_TYPE_P(entry) != IS_ARRAY) { + continue; + } + op_id = king_inference_array_find(entry, "id"); + if (op_id != NULL + && Z_TYPE_P(op_id) == IS_STRING + && strcmp(Z_STRVAL_P(op_id), id) == 0) { + return entry; + } + } ZEND_HASH_FOREACH_END(); + + return NULL; +} + +static zend_result king_inference_decode_graph_debug_require_op(zval *graph, const char *stage, const char *id) +{ + if (king_inference_decode_graph_debug_find_op(graph, id) != NULL) { + return SUCCESS; + } + + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "king.graph.decode_missing_op: token decode graph debug stage '%s' requires op '%s'.", + stage, + id + ); + return FAILURE; +} + +static zend_string *king_inference_decode_graph_debug_op_string(zval *graph, const char *stage, const char *id, const char *field) +{ + zval *op = king_inference_decode_graph_debug_find_op(graph, id); + zval *value; + + if (op == NULL) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "king.graph.decode_missing_op: token decode graph debug stage '%s' requires op '%s'.", + stage, + id + ); + return NULL; + } + + value = king_inference_array_find(op, field); + if (value == NULL || Z_TYPE_P(value) != IS_STRING || Z_STRLEN_P(value) == 0) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "king.graph.decode_missing_op_field: token decode graph debug stage '%s' op '%s' requires string field '%s'.", + stage, + id, + field + ); + return NULL; + } + + return Z_STR_P(value); +} + +static zend_result king_inference_decode_graph_debug_tensor( + king_inference_model_object *model, + zval *tensors, + const char *stage, + const char *role, + zend_string *name, + zend_ulong expected_rank, + zend_ulong expected_dim0, + zend_ulong expected_dim1, + bool expected_dim1_at_least +) { + zval *descriptor; + zval *dimensions; + zval dimensions_copy; + zval tensor; + king_inference_tensor_type_info type_info; + zend_ulong rank = 0; + zend_ulong type = 0; + zend_ulong dim0 = 0; + zend_ulong dim1 = 0; + bool known_type; + + descriptor = king_inference_tensor_index_descriptor(model, name); + if (descriptor == NULL) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "king.graph.decode_tensor_not_found: token decode graph debug stage '%s' tensor '%s' is missing.", + stage, + ZSTR_VAL(name) + ); + return FAILURE; + } + + dimensions = zend_hash_str_find(Z_ARRVAL_P(descriptor), "dimensions", sizeof("dimensions") - 1); + if (!king_inference_tensor_descriptor_ulong(descriptor, "rank", sizeof("rank") - 1, &rank) + || !king_inference_tensor_descriptor_ulong(descriptor, "type", sizeof("type") - 1, &type) + || dimensions == NULL + || Z_TYPE_P(dimensions) != IS_ARRAY + || rank != expected_rank + || king_inference_tensor_dimension_at(dimensions, 0, &dim0) != SUCCESS + || (expected_rank > 1 && king_inference_tensor_dimension_at(dimensions, 1, &dim1) != SUCCESS)) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "king.graph.decode_tensor_invalid_shape: token decode graph debug stage '%s' tensor '%s' has invalid rank or dimensions.", + stage, + ZSTR_VAL(name) + ); + return FAILURE; + } + + if ((expected_dim0 > 0 && dim0 != expected_dim0) + || (expected_rank > 1 + && expected_dim1 > 0 + && (expected_dim1_at_least ? dim1 < expected_dim1 : dim1 != expected_dim1))) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "king.graph.decode_tensor_invalid_shape: token decode graph debug stage '%s' tensor '%s' does not match expected shape.", + stage, + ZSTR_VAL(name) + ); + return FAILURE; + } + + known_type = king_inference_tensor_type_lookup(type, &type_info); + if (!known_type) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "king.graph.decode_tensor_invalid_dtype: token decode graph debug stage '%s' tensor '%s' has unknown tensor type %lu.", + stage, + ZSTR_VAL(name), + (unsigned long) type + ); + return FAILURE; + } + + array_init(&tensor); + add_assoc_string(&tensor, "role", role); + add_assoc_str(&tensor, "name", zend_string_copy(name)); + add_assoc_long(&tensor, "rank", (zend_long) rank); + ZVAL_COPY(&dimensions_copy, dimensions); + add_assoc_zval(&tensor, "dimensions", &dimensions_copy); + add_assoc_long(&tensor, "type", (zend_long) type); + add_assoc_string(&tensor, "dtype", type_info.name); + add_assoc_long(&tensor, "expected_rank", (zend_long) expected_rank); + if (expected_dim0 > 0) { + add_assoc_long(&tensor, "expected_dim0", (zend_long) expected_dim0); + } + if (expected_rank > 1 && expected_dim1 > 0) { + add_assoc_long(&tensor, expected_dim1_at_least ? "expected_dim1_min" : "expected_dim1", (zend_long) expected_dim1); + } + add_assoc_string(&tensor, "validation_error_code", "none"); + add_next_index_zval(tensors, &tensor); + return SUCCESS; +} + +static void king_inference_decode_graph_debug_add_op_ids(zval *target, const char * const *ids) +{ + size_t index; + + array_init(target); + for (index = 0; ids[index] != NULL; index++) { + add_next_index_string(target, ids[index]); + } +} + +static void king_inference_decode_graph_debug_add_output_shape(zval *stage, const char *name, zend_ulong length) +{ + zval shape; + + array_init(&shape); + add_assoc_string(&shape, "name", name); + add_assoc_long(&shape, "length", (zend_long) length); + add_assoc_zval(stage, "output_shape", &shape); +} + +static void king_inference_decode_graph_debug_finish_stage( + zval *stages, + zval *stage, + const char *output_name, + zend_ulong output_length +) { + zval validation; + + king_inference_decode_graph_debug_add_output_shape(stage, output_name, output_length); + array_init(&validation); + add_assoc_string(&validation, "status", "validated"); + add_assoc_string(&validation, "error_code", "none"); + add_assoc_zval(stage, "validation", &validation); + add_next_index_zval(stages, stage); +} + +static zend_result king_inference_decode_graph_debug_add_stage( + zval *stages, + zval *graph, + const char *stage_name, + const char * const *op_ids, + const char *output_name, + zend_ulong output_length +) { + zval stage; + zval ops; + zval tensors; + size_t index; + + array_init(&stage); + add_assoc_string(&stage, "stage", stage_name); + add_assoc_long(&stage, "layer", 0); + king_inference_decode_graph_debug_add_op_ids(&ops, op_ids); + add_assoc_zval(&stage, "ops", &ops); + array_init(&tensors); + + for (index = 0; op_ids[index] != NULL; index++) { + if (king_inference_decode_graph_debug_require_op(graph, stage_name, op_ids[index]) != SUCCESS) { + zval_ptr_dtor(&tensors); + zval_ptr_dtor(&stage); + return FAILURE; + } + } + + add_assoc_zval(&stage, "tensors", &tensors); + king_inference_decode_graph_debug_finish_stage(stages, &stage, output_name, output_length); + return SUCCESS; +} + +static zend_result king_inference_decode_graph_attach_debug(king_inference_model_object *model, zval *graph) +{ + static const char *embedding_ops[] = {"x", NULL}; + static const char *attn_norm_ops[] = {"l0_attn_norm", NULL}; + static const char *qkv_ops[] = {"l0_q", "l0_k", "l0_v", NULL}; + static const char *attention_ops[] = {"l0_h0_q_slice", "l0_h0_k_slice", "l0_h0_v_slice", "l0_h0_q_rope", "l0_h0_k_rope", "l0_h0_kv_write", "l0_h0_ctx", "l0_context", NULL}; + static const char *attn_out_ops[] = {"l0_attn_out", "l0_attn_residual", NULL}; + static const char *ffn_ops[] = {"l0_ffn_norm", "l0_ffn_gate", "l0_ffn_up", "l0_ffn_gate_silu", "l0_ffn_gated", "l0_ffn_down", "l0_output", NULL}; + static const char *final_norm_ops[] = {"final_norm", NULL}; + static const char *logits_ops[] = {"logits", NULL}; + zval *debug_layer_limit_value = king_inference_array_find(graph, "debug_layer_limit"); + zval *terminal = king_inference_array_find(graph, "terminal"); + zval debug; + zval stages; + zval tensors; + zval validation; + zend_ulong token_id = 0; + zend_ulong position = 0; + zend_ulong embedding = model->gguf.architecture_params[KING_INFERENCE_ARCH_EMBEDDING_LENGTH]; + zend_ulong feed_forward = model->gguf.architecture_params[KING_INFERENCE_ARCH_FEED_FORWARD_LENGTH]; + zend_ulong vocab = model->gguf.tokenizer_token_count; + zend_ulong heads = 0; + zend_ulong kv_heads = 0; + zend_ulong key_length = 0; + zend_ulong value_length = 0; + zend_string *tensor_name; + + if (debug_layer_limit_value == NULL + || Z_TYPE_P(debug_layer_limit_value) != IS_LONG + || Z_LVAL_P(debug_layer_limit_value) <= 0) { + return SUCCESS; + } + if (!king_inference_decode_graph_debug_long_field(graph, "token_id", &token_id) + || !king_inference_decode_graph_debug_long_field(graph, "position", &position) + || !king_inference_decode_graph_debug_long_field(graph, "attention_head_count", &heads) + || !king_inference_decode_graph_debug_long_field(graph, "attention_head_count_kv", &kv_heads) + || !king_inference_decode_graph_debug_long_field(graph, "attention_key_length", &key_length) + || !king_inference_decode_graph_debug_long_field(graph, "attention_value_length", &value_length) + || terminal == NULL + || Z_TYPE_P(terminal) != IS_ARRAY + || embedding == 0 + || feed_forward == 0 + || vocab == 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "king.graph.decode_debug_metadata_invalid: token decode graph debug requires complete layer-0 shape metadata."); + return FAILURE; + } + + array_init(&debug); + add_assoc_long(&debug, "schema_version", 1); + add_assoc_string(&debug, "scope", "single_token_cpu_decode_layer0"); + add_assoc_string(&debug, "status", "validated"); + add_assoc_string(&debug, "error_code", "none"); + add_assoc_long(&debug, "layer", 0); + add_assoc_long(&debug, "token_id", (zend_long) token_id); + add_assoc_long(&debug, "position", (zend_long) position); + add_assoc_long(&debug, "hidden_size", (zend_long) embedding); + add_assoc_long(&debug, "feed_forward_size", (zend_long) feed_forward); + add_assoc_long(&debug, "vocab_size", (zend_long) vocab); + array_init(&stages); + + { + zval stage; + zval ops; + array_init(&stage); + add_assoc_string(&stage, "stage", "embedding"); + add_assoc_long(&stage, "layer", 0); + king_inference_decode_graph_debug_add_op_ids(&ops, embedding_ops); + add_assoc_zval(&stage, "ops", &ops); + array_init(&tensors); + tensor_name = king_inference_decode_graph_debug_op_string(graph, "embedding", "x", "tensor"); + if (tensor_name == NULL + || king_inference_decode_graph_debug_tensor(model, &tensors, "embedding", "token_embedding", tensor_name, 2, embedding, vocab, true) != SUCCESS) { + zval_ptr_dtor(&tensors); + zval_ptr_dtor(&stage); + zval_ptr_dtor(&stages); + zval_ptr_dtor(&debug); + return FAILURE; + } + add_assoc_zval(&stage, "tensors", &tensors); + king_inference_decode_graph_debug_finish_stage(&stages, &stage, "x", embedding); + } + + { + zval stage; + zval ops; + array_init(&stage); + add_assoc_string(&stage, "stage", "attention_norm"); + add_assoc_long(&stage, "layer", 0); + king_inference_decode_graph_debug_add_op_ids(&ops, attn_norm_ops); + add_assoc_zval(&stage, "ops", &ops); + array_init(&tensors); + tensor_name = king_inference_decode_graph_debug_op_string(graph, "attention_norm", "l0_attn_norm", "weight"); + if (tensor_name == NULL + || king_inference_decode_graph_debug_tensor(model, &tensors, "attention_norm", "attention_norm_weight", tensor_name, 1, embedding, 0, false) != SUCCESS) { + zval_ptr_dtor(&tensors); + zval_ptr_dtor(&stage); + zval_ptr_dtor(&stages); + zval_ptr_dtor(&debug); + return FAILURE; + } + add_assoc_zval(&stage, "tensors", &tensors); + king_inference_decode_graph_debug_finish_stage(&stages, &stage, "l0_attn_norm", embedding); + } + + { + zval stage; + zval ops; + array_init(&stage); + add_assoc_string(&stage, "stage", "qkv_projection"); + add_assoc_long(&stage, "layer", 0); + king_inference_decode_graph_debug_add_op_ids(&ops, qkv_ops); + add_assoc_zval(&stage, "ops", &ops); + array_init(&tensors); + tensor_name = king_inference_decode_graph_debug_op_string(graph, "qkv_projection", "l0_q", "weight"); + if (tensor_name == NULL + || king_inference_decode_graph_debug_tensor(model, &tensors, "qkv_projection", "query_weight", tensor_name, 2, embedding, heads * key_length, false) != SUCCESS) { + zval_ptr_dtor(&tensors); + zval_ptr_dtor(&stage); + zval_ptr_dtor(&stages); + zval_ptr_dtor(&debug); + return FAILURE; + } + tensor_name = king_inference_decode_graph_debug_op_string(graph, "qkv_projection", "l0_k", "weight"); + if (tensor_name == NULL + || king_inference_decode_graph_debug_tensor(model, &tensors, "qkv_projection", "key_weight", tensor_name, 2, embedding, kv_heads * key_length, false) != SUCCESS) { + zval_ptr_dtor(&tensors); + zval_ptr_dtor(&stage); + zval_ptr_dtor(&stages); + zval_ptr_dtor(&debug); + return FAILURE; + } + tensor_name = king_inference_decode_graph_debug_op_string(graph, "qkv_projection", "l0_v", "weight"); + if (tensor_name == NULL + || king_inference_decode_graph_debug_tensor(model, &tensors, "qkv_projection", "value_weight", tensor_name, 2, embedding, kv_heads * value_length, false) != SUCCESS) { + zval_ptr_dtor(&tensors); + zval_ptr_dtor(&stage); + zval_ptr_dtor(&stages); + zval_ptr_dtor(&debug); + return FAILURE; + } + add_assoc_zval(&stage, "tensors", &tensors); + king_inference_decode_graph_debug_finish_stage(&stages, &stage, "qkv", (heads * key_length) + (kv_heads * key_length) + (kv_heads * value_length)); + } + + if (king_inference_decode_graph_debug_add_stage(&stages, graph, "multi_head_attention", attention_ops, "l0_context", heads * value_length) != SUCCESS) { + zval_ptr_dtor(&stages); + zval_ptr_dtor(&debug); + return FAILURE; + } + + { + zval stage; + zval ops; + array_init(&stage); + add_assoc_string(&stage, "stage", "attention_output_projection"); + add_assoc_long(&stage, "layer", 0); + king_inference_decode_graph_debug_add_op_ids(&ops, attn_out_ops); + add_assoc_zval(&stage, "ops", &ops); + array_init(&tensors); + tensor_name = king_inference_decode_graph_debug_op_string(graph, "attention_output_projection", "l0_attn_out", "weight"); + if (tensor_name == NULL + || king_inference_decode_graph_debug_tensor(model, &tensors, "attention_output_projection", "attention_output_weight", tensor_name, 2, heads * value_length, embedding, false) != SUCCESS) { + zval_ptr_dtor(&tensors); + zval_ptr_dtor(&stage); + zval_ptr_dtor(&stages); + zval_ptr_dtor(&debug); + return FAILURE; + } + add_assoc_zval(&stage, "tensors", &tensors); + king_inference_decode_graph_debug_finish_stage(&stages, &stage, "l0_attn_residual", embedding); + } + + { + zval stage; + zval ops; + array_init(&stage); + add_assoc_string(&stage, "stage", "ffn_gate_up_down"); + add_assoc_long(&stage, "layer", 0); + king_inference_decode_graph_debug_add_op_ids(&ops, ffn_ops); + add_assoc_zval(&stage, "ops", &ops); + array_init(&tensors); + tensor_name = king_inference_decode_graph_debug_op_string(graph, "ffn_gate_up_down", "l0_ffn_norm", "weight"); + if (tensor_name == NULL + || king_inference_decode_graph_debug_tensor(model, &tensors, "ffn_gate_up_down", "ffn_norm_weight", tensor_name, 1, embedding, 0, false) != SUCCESS) { + zval_ptr_dtor(&tensors); + zval_ptr_dtor(&stage); + zval_ptr_dtor(&stages); + zval_ptr_dtor(&debug); + return FAILURE; + } + tensor_name = king_inference_decode_graph_debug_op_string(graph, "ffn_gate_up_down", "l0_ffn_gate", "weight"); + if (tensor_name == NULL + || king_inference_decode_graph_debug_tensor(model, &tensors, "ffn_gate_up_down", "ffn_gate_weight", tensor_name, 2, embedding, feed_forward, false) != SUCCESS) { + zval_ptr_dtor(&tensors); + zval_ptr_dtor(&stage); + zval_ptr_dtor(&stages); + zval_ptr_dtor(&debug); + return FAILURE; + } + tensor_name = king_inference_decode_graph_debug_op_string(graph, "ffn_gate_up_down", "l0_ffn_up", "weight"); + if (tensor_name == NULL + || king_inference_decode_graph_debug_tensor(model, &tensors, "ffn_gate_up_down", "ffn_up_weight", tensor_name, 2, embedding, feed_forward, false) != SUCCESS) { + zval_ptr_dtor(&tensors); + zval_ptr_dtor(&stage); + zval_ptr_dtor(&stages); + zval_ptr_dtor(&debug); + return FAILURE; + } + tensor_name = king_inference_decode_graph_debug_op_string(graph, "ffn_gate_up_down", "l0_ffn_down", "weight"); + if (tensor_name == NULL + || king_inference_decode_graph_debug_tensor(model, &tensors, "ffn_gate_up_down", "ffn_down_weight", tensor_name, 2, feed_forward, embedding, false) != SUCCESS) { + zval_ptr_dtor(&tensors); + zval_ptr_dtor(&stage); + zval_ptr_dtor(&stages); + zval_ptr_dtor(&debug); + return FAILURE; + } + add_assoc_zval(&stage, "tensors", &tensors); + king_inference_decode_graph_debug_finish_stage(&stages, &stage, "l0_output", embedding); + } + + { + zval stage; + zval ops; + zval *final_norm_tensor; + array_init(&stage); + add_assoc_string(&stage, "stage", "final_norm"); + add_assoc_long(&stage, "layer", 0); + king_inference_decode_graph_debug_add_op_ids(&ops, final_norm_ops); + add_assoc_zval(&stage, "ops", &ops); + array_init(&tensors); + final_norm_tensor = king_inference_array_find(terminal, "final_norm_tensor"); + if (final_norm_tensor == NULL + || Z_TYPE_P(final_norm_tensor) != IS_STRING + || king_inference_decode_graph_debug_tensor(model, &tensors, "final_norm", "final_norm_weight", Z_STR_P(final_norm_tensor), 1, embedding, 0, false) != SUCCESS) { + zval_ptr_dtor(&tensors); + zval_ptr_dtor(&stage); + zval_ptr_dtor(&stages); + zval_ptr_dtor(&debug); + return FAILURE; + } + add_assoc_zval(&stage, "tensors", &tensors); + king_inference_decode_graph_debug_finish_stage(&stages, &stage, "final_norm", embedding); + } + + if (king_inference_decode_graph_debug_find_op(graph, "logits") != NULL) { + zval stage; + zval ops; + array_init(&stage); + add_assoc_string(&stage, "stage", "logits"); + add_assoc_long(&stage, "layer", 0); + king_inference_decode_graph_debug_add_op_ids(&ops, logits_ops); + add_assoc_zval(&stage, "ops", &ops); + array_init(&tensors); + tensor_name = king_inference_decode_graph_debug_op_string(graph, "logits", "logits", "weight"); + if (tensor_name == NULL + || king_inference_decode_graph_debug_tensor(model, &tensors, "logits", "output_projection_weight", tensor_name, 2, embedding, vocab, true) != SUCCESS) { + zval_ptr_dtor(&tensors); + zval_ptr_dtor(&stage); + zval_ptr_dtor(&stages); + zval_ptr_dtor(&debug); + return FAILURE; + } + add_assoc_zval(&stage, "tensors", &tensors); + king_inference_decode_graph_debug_finish_stage(&stages, &stage, "logits", vocab); + add_assoc_bool(&debug, "logits_ready", true); + } else { + add_assoc_bool(&debug, "logits_ready", false); + } + + add_assoc_zval(&debug, "stages", &stages); + array_init(&validation); + add_assoc_string(&validation, "status", "validated"); + add_assoc_string(&validation, "error_code", "none"); + add_assoc_zval(&debug, "validation", &validation); + add_assoc_zval(graph, "debug", &debug); + return SUCCESS; +} diff --git a/extension/src/inference/tensor/graph/builder/token_decode_graph_options.inc b/extension/src/inference/tensor/graph/builder/token_decode_graph_options.inc new file mode 100644 index 000000000..e0871bf14 --- /dev/null +++ b/extension/src/inference/tensor/graph/builder/token_decode_graph_options.inc @@ -0,0 +1,138 @@ +static zend_result king_inference_decode_graph_double_option( + zval *options, + const char *key, + double fallback, + double *value_out +) { + zval *value = king_inference_array_find(options, key); + + *value_out = fallback; + if (value == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(value) == IS_LONG) { + *value_out = (double) Z_LVAL_P(value); + } else if (Z_TYPE_P(value) == IS_DOUBLE) { + *value_out = Z_DVAL_P(value); + } else { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph option '%s' must be a finite number.", key); + return FAILURE; + } + if (!isfinite(*value_out)) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph option '%s' must be a finite number.", key); + return FAILURE; + } + return SUCCESS; +} + +static zend_result king_inference_decode_graph_non_negative_long_option( + zval *options, + const char *key, + zend_long fallback, + zend_long *value_out +) { + zval *value = king_inference_array_find(options, key); + + *value_out = fallback; + if (value == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(value) != IS_LONG || Z_LVAL_P(value) < 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph option '%s' must be a non-negative integer.", key); + return FAILURE; + } + + *value_out = Z_LVAL_P(value); + return SUCCESS; +} + +static bool king_inference_decode_graph_bool_option(zval *options, const char *key, bool fallback) +{ + zval *value = king_inference_array_find(options, key); + + if (value == NULL) { + return fallback; + } + return zend_is_true(value); +} + +static zend_result king_inference_decode_graph_sampler_mode( + zval *options, + const char **mode_out, + double *temperature_out +) { + zval *sampler = king_inference_array_find(options, "sampler"); + double temperature = 0.7; + + *mode_out = "sample"; + *temperature_out = temperature; + if (king_inference_decode_graph_double_option(options, "temperature", 0.7, &temperature) != SUCCESS) { + return FAILURE; + } + if (temperature < 0.0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph temperature must be non-negative."); + return FAILURE; + } + if (sampler != NULL) { + if (Z_TYPE_P(sampler) != IS_STRING || Z_STRLEN_P(sampler) == 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph sampler must be 'argmax' or 'sample'."); + return FAILURE; + } + if (zend_string_equals_literal(Z_STR_P(sampler), "argmax")) { + *mode_out = "argmax"; + *temperature_out = 0.0; + return SUCCESS; + } + if (!zend_string_equals_literal(Z_STR_P(sampler), "sample")) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "King inference token decode graph sampler '%s' is not supported; expected 'argmax' or 'sample'.", + Z_STRVAL_P(sampler) + ); + return FAILURE; + } + } + + *temperature_out = temperature; + *mode_out = temperature == 0.0 ? "argmax" : "sample"; + return SUCCESS; +} + +static zend_result king_inference_decode_graph_token_id_from_source( + king_inference_model_object *model, + zval *source, + zend_ulong position, + zend_ulong *token_id_out +) { + zval *token = source; + + if (source != NULL && Z_TYPE_P(source) == IS_ARRAY) { + zval *tokens = king_inference_array_find(source, "tokens"); + + if (tokens == NULL) { + tokens = source; + } + if (Z_TYPE_P(tokens) != IS_ARRAY) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph tokenizer output must contain a tokens array."); + return FAILURE; + } + token = zend_hash_index_find(Z_ARRVAL_P(tokens), position); + if (token == NULL) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph tokenizer output does not contain tokens[%lu].", (unsigned long) position); + return FAILURE; + } + } + if (token == NULL || Z_TYPE_P(token) != IS_LONG || Z_LVAL_P(token) < 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph token must be a non-negative integer or tokenizer output containing tokens[position]."); + return FAILURE; + } + if (model->gguf.tokenizer_token_count > 0 + && (zend_ulong) Z_LVAL_P(token) >= model->gguf.tokenizer_token_count) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph token id exceeds the loaded tokenizer vocabulary."); + return FAILURE; + } + + *token_id_out = (zend_ulong) Z_LVAL_P(token); + return SUCCESS; +} diff --git a/extension/src/inference/tensor/graph/builder/token_decode_graph_sampler.inc b/extension/src/inference/tensor/graph/builder/token_decode_graph_sampler.inc new file mode 100644 index 000000000..38129d9f0 --- /dev/null +++ b/extension/src/inference/tensor/graph/builder/token_decode_graph_sampler.inc @@ -0,0 +1,80 @@ +/* + * Token decode graph sampler helpers. + */ + +static zend_result king_inference_decode_graph_add_sampler( + zval *ops, + const char *logits, + zend_ulong position, + zval *options, + const char **mode_out, + const char **op_out, + double *temperature_out, + zend_long *top_k_out +) { + zval op; + const char *mode; + double temperature; + double top_p; + zend_long top_k; + zend_long seed = 0; + zval *seed_value = king_inference_array_find(options, "seed"); + + *mode_out = "sample"; + *op_out = NULL; + *temperature_out = 0.7; + *top_k_out = 0; + if (king_inference_decode_graph_sampler_mode(options, &mode, &temperature) != SUCCESS) { + return FAILURE; + } + if (king_inference_decode_graph_non_negative_long_option(options, "top_k", 40, &top_k) != SUCCESS) { + return FAILURE; + } + if (king_inference_decode_graph_double_option(options, "top_p", 0.95, &top_p) != SUCCESS) { + return FAILURE; + } + if (top_p <= 0.0 || top_p > 1.0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph top_p must be greater than zero and at most one."); + return FAILURE; + } + if (seed_value != NULL && Z_TYPE_P(seed_value) != IS_LONG) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference token decode graph option 'seed' must be an integer."); + return FAILURE; + } + if (seed_value != NULL) { + seed = Z_LVAL_P(seed_value); + } + *mode_out = mode; + *temperature_out = temperature; + if (strcmp(mode, "argmax") == 0) { + king_inference_decode_graph_start_op(&op, "next_token", "argmax_token"); + add_assoc_string(&op, "logits", logits); + king_inference_decode_graph_push_op(ops, &op); + *op_out = "argmax_token"; + *top_k_out = 1; + return SUCCESS; + } + + king_inference_decode_graph_start_op(&op, "next_token", "sample_token"); + add_assoc_string(&op, "logits", logits); + add_assoc_double(&op, "temperature", temperature); + add_assoc_long(&op, "top_k", top_k); + add_assoc_double(&op, "top_p", top_p); + add_assoc_long(&op, "seed", seed); + add_assoc_long(&op, "sample_index", (zend_long) position); + king_inference_decode_graph_push_op(ops, &op); + *op_out = "sample_token"; + *top_k_out = top_k; + return SUCCESS; +} + +static zend_ulong king_inference_decode_graph_kv_head(zend_ulong head, zend_ulong heads, zend_ulong kv_heads) +{ + zend_ulong kv_head; + + if (kv_heads == heads) { + return head; + } + kv_head = (head * kv_heads) / heads; + return kv_head >= kv_heads ? kv_heads - 1 : kv_head; +} diff --git a/extension/src/inference/tensor/graph/core/tensor_graph.inc b/extension/src/inference/tensor/graph/core/tensor_graph.inc new file mode 100644 index 000000000..c07dca6f5 --- /dev/null +++ b/extension/src/inference/tensor/graph/core/tensor_graph.inc @@ -0,0 +1,714 @@ +typedef struct _king_inference_graph_vector { + zend_string *id; + double *values; + zend_ulong length; + struct _king_inference_graph_vector *next; +} king_inference_graph_vector; + +static void king_inference_graph_vectors_release(king_inference_graph_vector *head) +{ + while (head != NULL) { + king_inference_graph_vector *next = head->next; + if (head->id != NULL) { + zend_string_release(head->id); + } + if (head->values != NULL) { + efree(head->values); + } + efree(head); + head = next; + } +} + +static king_inference_graph_vector *king_inference_graph_vector_find( + king_inference_graph_vector *head, + zend_string *id +) { + while (head != NULL) { + if (zend_string_equals(head->id, id)) { + return head; + } + head = head->next; + } + + return NULL; +} + +static zend_result king_inference_graph_vector_add( + king_inference_graph_vector **head, + zend_string *id, + double *values, + zend_ulong length +) { + king_inference_graph_vector *node; + + if (king_inference_graph_vector_find(*head, id) != NULL) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "King inference graph output '%s' is defined more than once.", + ZSTR_VAL(id) + ); + return FAILURE; + } + + node = ecalloc(1, sizeof(*node)); + node->id = zend_string_copy(id); + node->values = values; + node->length = length; + node->next = *head; + *head = node; + return SUCCESS; +} + +static zval *king_inference_graph_required_field(zval *array, const char *key) +{ + if (array == NULL || Z_TYPE_P(array) != IS_ARRAY) { + return NULL; + } + + return king_inference_array_find(array, key); +} + +static zend_result king_inference_graph_required_string( + zval *array, + const char *key, + zend_string **value +) { + zval *field = king_inference_graph_required_field(array, key); + + if (field == NULL || Z_TYPE_P(field) != IS_STRING || Z_STRLEN_P(field) == 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph requires string field '%s'.", key); + return FAILURE; + } + + *value = Z_STR_P(field); + return SUCCESS; +} + +static double king_inference_graph_double_option(zval *array, const char *key, double fallback) +{ + zval *field = king_inference_array_find(array, key); + + if (field == NULL) { + return fallback; + } + if (Z_TYPE_P(field) == IS_DOUBLE) { + return Z_DVAL_P(field); + } + if (Z_TYPE_P(field) == IS_LONG) { + return (double) Z_LVAL_P(field); + } + + return fallback; +} + +static zend_result king_inference_graph_finite_double_option( + zval *array, + const char *key, + double fallback, + double *value +) { + zval *field = king_inference_array_find(array, key); + + *value = king_inference_graph_double_option(array, key, fallback); + if (field == NULL || (Z_TYPE_P(field) != IS_DOUBLE && Z_TYPE_P(field) != IS_LONG)) { + return SUCCESS; + } + if (!isfinite(*value)) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "King inference graph option '%s' must be a finite number.", + key + ); + return FAILURE; + } + + return SUCCESS; +} + +static double king_inference_graph_sqrt(double value) +{ + double guess; + int i; + + if (value <= 0.0) { + return 0.0; + } + + guess = value >= 1.0 ? value : 1.0; + for (i = 0; i < 24; i++) { + guess = 0.5 * (guess + value / guess); + } + + return guess; +} + +static zend_result king_inference_graph_vector_from_zval( + zval *source, + zend_ulong max_values, + double **values_out, + zend_ulong *length_out +) { + zend_ulong length; + zend_ulong index = 0; + double *values; + zval *entry; + + *values_out = NULL; + *length_out = 0; + if (source == NULL || Z_TYPE_P(source) != IS_ARRAY) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph vector inputs must be arrays."); + return FAILURE; + } + + length = (zend_ulong) zend_hash_num_elements(Z_ARRVAL_P(source)); + if (length == 0 || length > max_values || length > (zend_ulong) (SIZE_MAX / sizeof(double))) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph vector length exceeds configured limits."); + return FAILURE; + } + + values = ecalloc((size_t) length, sizeof(double)); + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(source), entry) { + if (Z_TYPE_P(entry) == IS_LONG) { + values[index] = (double) Z_LVAL_P(entry); + } else if (Z_TYPE_P(entry) == IS_DOUBLE) { + values[index] = Z_DVAL_P(entry); + } else { + efree(values); + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph vectors must contain only int or float values."); + return FAILURE; + } + index++; + } ZEND_HASH_FOREACH_END(); + + *values_out = values; + *length_out = length; + return SUCCESS; +} + +static zend_result king_inference_graph_op_embedding( + king_inference_model_object *model, + zval *op, + zend_ulong max_values, + double **values_out, + zend_ulong *length_out +) { + zend_string *tensor = NULL; + const char *status = NULL; + king_inference_tensor_native_view view; + zend_ulong cols; + zend_ulong rows; + zend_ulong col; + zend_long token_id; + zval *token; + double output_scale = 1.0; + double *values; + + if (king_inference_resolve_token_embedding_tensor_name(model, op, &tensor, NULL, &status) != SUCCESS + || tensor == NULL) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + status != NULL && strcmp(status, "ambiguous_shape_scan") == 0 + ? "King inference graph embedding found more than one possible token embedding tensor; set tensor explicitly." + : "King inference graph embedding requires tensor or a resolvable token embedding tensor." + ); + return FAILURE; + } + token = king_inference_array_find(op, "token_id"); + if (token == NULL) { + token = king_inference_array_find(op, "token"); + } + if (token == NULL || Z_TYPE_P(token) != IS_LONG || Z_LVAL_P(token) < 0) { + zend_string_release(tensor); + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph embedding requires token_id."); + return FAILURE; + } + token_id = Z_LVAL_P(token); + if (king_inference_graph_finite_double_option(op, "scale", 1.0, &output_scale) != SUCCESS) { + zend_string_release(tensor); + return FAILURE; + } + + if (king_inference_tensor_native_view_resolve(model, tensor, &view) != SUCCESS) { + zend_string_release(tensor); + return FAILURE; + } + if (view.rank != 2 + || king_inference_tensor_dimension_at(view.dimensions, 0, &cols) != SUCCESS + || king_inference_tensor_dimension_at(view.dimensions, 1, &rows) != SUCCESS + || (zend_ulong) token_id >= rows + || cols == 0 + || cols > max_values + || cols > (zend_ulong) (SIZE_MAX / sizeof(double))) { + zend_string_release(tensor); + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph embedding tensor dimensions or token_id are invalid."); + return FAILURE; + } + + values = ecalloc((size_t) cols, sizeof(double)); + for (col = 0; col < cols; col++) { + if (king_inference_tensor_value_at(&view, ((zend_ulong) token_id) * cols + col, &values[col]) != SUCCESS) { + efree(values); + zend_string_release(tensor); + return FAILURE; + } + values[col] *= output_scale; + } + zend_string_release(tensor); + + *values_out = values; + *length_out = cols; + return SUCCESS; +} + +static zend_result king_inference_graph_op_rms_norm( + king_inference_model_object *model, + zval *op, + king_inference_graph_vector *vectors, + zend_ulong max_values, + double **values_out, + zend_ulong *length_out +) { + zend_string *input_id; + zend_string *weight_tensor = NULL; + king_inference_graph_vector *input; + king_inference_tensor_native_view weight; + zend_ulong i; + double sumsq = 0.0; + double epsilon; + double scale; + double *values; + + if (king_inference_graph_finite_double_option(op, "epsilon", 0.000001, &epsilon) != SUCCESS) { + return FAILURE; + } + if (king_inference_graph_required_string(op, "input", &input_id) != SUCCESS) { + return FAILURE; + } + input = king_inference_graph_vector_find(vectors, input_id); + if (input == NULL) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph input '%s' was not produced.", ZSTR_VAL(input_id)); + return FAILURE; + } + if (input->length == 0 || input->length > max_values) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph rms_norm input exceeds configured limits."); + return FAILURE; + } + + for (i = 0; i < input->length; i++) { + sumsq += input->values[i] * input->values[i]; + } + scale = 1.0 / king_inference_graph_sqrt((sumsq / (double) input->length) + epsilon); + values = ecalloc((size_t) input->length, sizeof(double)); + + { + zval *weight_field = king_inference_array_find(op, "weight"); + if (weight_field != NULL) { + if (Z_TYPE_P(weight_field) != IS_STRING || Z_STRLEN_P(weight_field) == 0) { + efree(values); + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph rms_norm weight must be a tensor name."); + return FAILURE; + } + weight_tensor = Z_STR_P(weight_field); + } + } + + if (weight_tensor != NULL) { + if (king_inference_tensor_native_view_resolve(model, weight_tensor, &weight) != SUCCESS) { + efree(values); + return FAILURE; + } + if (weight.rank != 1 || weight.elements != input->length) { + efree(values); + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph rms_norm weight length does not match input."); + return FAILURE; + } + for (i = 0; i < input->length; i++) { + double weight_value; + if (king_inference_tensor_value_at(&weight, i, &weight_value) != SUCCESS) { + efree(values); + return FAILURE; + } + values[i] = input->values[i] * scale * weight_value; + } + } else { + for (i = 0; i < input->length; i++) { + values[i] = input->values[i] * scale; + } + } + + *values_out = values; + *length_out = input->length; + return SUCCESS; +} + +static zend_result king_inference_graph_op_linear( + king_inference_model_object *model, + zval *op, + king_inference_graph_vector *vectors, + zend_ulong max_values, + zend_ulong max_operations, + double **values_out, + zend_ulong *length_out +) { + zend_string *input_id; + zend_string *weight_tensor; + zend_string *bias_tensor = NULL; + king_inference_graph_vector *input; + king_inference_tensor_native_view weight; + king_inference_tensor_native_view bias; + zend_ulong cols; + zend_ulong rows = 1; + zend_ulong row; + zend_ulong row_offset = king_inference_tensor_option_ulong(op, "row_offset", 0); + zend_ulong row_limit; + double *values; + zval *bias_field; + + if (king_inference_graph_required_string(op, "input", &input_id) != SUCCESS + || king_inference_graph_required_string(op, "weight", &weight_tensor) != SUCCESS) { + return FAILURE; + } + input = king_inference_graph_vector_find(vectors, input_id); + if (input == NULL) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph input '%s' was not produced.", ZSTR_VAL(input_id)); + return FAILURE; + } + if (king_inference_tensor_native_view_resolve(model, weight_tensor, &weight) != SUCCESS) { + return FAILURE; + } + if (weight.rank == 1) { + cols = weight.elements; + } else if (weight.rank == 2) { + if (king_inference_tensor_dimension_at(weight.dimensions, 0, &cols) != SUCCESS + || king_inference_tensor_dimension_at(weight.dimensions, 1, &rows) != SUCCESS) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph linear weight has invalid dimensions."); + return FAILURE; + } + } else { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph linear requires rank-1 or rank-2 weight."); + return FAILURE; + } + if (input->length != cols || row_offset > rows) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph linear input length does not match weight columns."); + return FAILURE; + } + + row_limit = king_inference_tensor_option_ulong(op, "row_limit", rows - row_offset); + if (row_limit > rows - row_offset) { + row_limit = rows - row_offset; + } + if (row_limit == 0 || row_limit > max_values || (cols > 0 && row_limit > max_operations / cols)) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph linear exceeds configured limits."); + return FAILURE; + } + + bias_field = king_inference_array_find(op, "bias"); + if (bias_field != NULL) { + if (Z_TYPE_P(bias_field) != IS_STRING || Z_STRLEN_P(bias_field) == 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph linear bias must be a tensor name."); + return FAILURE; + } + bias_tensor = Z_STR_P(bias_field); + if (king_inference_tensor_native_view_resolve(model, bias_tensor, &bias) != SUCCESS) { + return FAILURE; + } + if (bias.rank != 1 || bias.elements < row_offset + row_limit) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph linear bias length does not cover output rows."); + return FAILURE; + } + } + + values = ecalloc((size_t) row_limit, sizeof(double)); + for (row = 0; row < row_limit; row++) { + zend_ulong base = (row_offset + row) * cols; + if (king_inference_tensor_row_dot(&weight, base, cols, input->values, &values[row]) != SUCCESS) { + efree(values); + return FAILURE; + } + if (bias_tensor != NULL) { + double bias_value; + if (king_inference_tensor_value_at(&bias, row_offset + row, &bias_value) != SUCCESS) { + efree(values); + return FAILURE; + } + values[row] += bias_value; + } + } + + *values_out = values; + *length_out = row_limit; + return SUCCESS; +} + +#include "../ops/tensor_graph_ops.inc" +#include "../ops/tensor_graph_sampling.inc" +#include "../ops/tensor_graph_kv.inc" + +static void king_inference_graph_add_vector_payload( + zval *target, + king_inference_graph_vector *vector +) { + zval payload; + zval values; + zend_ulong i; + + array_init(&payload); + add_assoc_long(&payload, "length", (zend_long) vector->length); + array_init(&values); + for (i = 0; i < vector->length; i++) { + add_next_index_double(&values, vector->values[i]); + } + add_assoc_zval(&payload, "values", &values); + zend_hash_update(Z_ARRVAL_P(target), vector->id, &payload); +} + +static zend_result king_inference_graph_run_array( + king_inference_model_object *model, + zval *graph, + zval *options, + zval *return_value +) { + king_inference_graph_vector *vectors = NULL; + king_inference_graph_vector *node; + zval *inputs; + zval *ops; + zval *output_id; + zval *entry; + zval state; + bool state_initialized = false; + zend_string *key; + zend_ulong op_count = 0; + zend_ulong max_values = king_inference_tensor_option_ulong(options, "max_vector_values", 65536); + zend_ulong max_operations = king_inference_tensor_option_ulong(options, "max_operations", 16777216); + zval *return_outputs_option = king_inference_array_find(options, "return_outputs"); + bool return_outputs = return_outputs_option == NULL || zend_is_true(return_outputs_option); + + if (graph == NULL || Z_TYPE_P(graph) != IS_ARRAY) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph requires a graph array."); + return FAILURE; + } + + inputs = king_inference_array_find(graph, "inputs"); + if (inputs != NULL && Z_TYPE_P(inputs) == IS_ARRAY) { + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(inputs), key, entry) { + double *values; + zend_ulong length; + if (key == NULL || Z_TYPE_P(entry) != IS_ARRAY) { + king_inference_graph_vectors_release(vectors); + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph inputs must be named vector arrays."); + return FAILURE; + } + if (king_inference_graph_vector_from_zval(entry, max_values, &values, &length) != SUCCESS) { + king_inference_graph_vectors_release(vectors); + return FAILURE; + } + if (king_inference_graph_vector_add(&vectors, key, values, length) != SUCCESS) { + efree(values); + king_inference_graph_vectors_release(vectors); + return FAILURE; + } + } ZEND_HASH_FOREACH_END(); + } + + ops = king_inference_array_find(graph, "ops"); + if (ops == NULL) { + ops = king_inference_array_find(graph, "steps"); + } + if (ops == NULL || Z_TYPE_P(ops) != IS_ARRAY) { + king_inference_graph_vectors_release(vectors); + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph requires ops."); + return FAILURE; + } + + if (king_inference_graph_state_init(graph, &state) != SUCCESS) { + king_inference_graph_vectors_release(vectors); + return FAILURE; + } + state_initialized = true; + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(ops), entry) { + zend_string *id; + zend_string *op_name; + double *values = NULL; + zend_ulong length = 0; + + if (entry == NULL || Z_TYPE_P(entry) != IS_ARRAY) { + zval_ptr_dtor(&state); + king_inference_graph_vectors_release(vectors); + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph operations must be arrays."); + return FAILURE; + } + if (king_inference_graph_required_string(entry, "id", &id) != SUCCESS + || king_inference_graph_required_string(entry, "op", &op_name) != SUCCESS) { + zval_ptr_dtor(&state); + king_inference_graph_vectors_release(vectors); + return FAILURE; + } + if (zend_string_equals_literal(op_name, "embedding")) { + if (king_inference_graph_op_embedding(model, entry, max_values, &values, &length) != SUCCESS) { + zval_ptr_dtor(&state); + king_inference_graph_vectors_release(vectors); + return FAILURE; + } + } else if (zend_string_equals_literal(op_name, "rms_norm")) { + if (king_inference_graph_op_rms_norm(model, entry, vectors, max_values, &values, &length) != SUCCESS) { + zval_ptr_dtor(&state); + king_inference_graph_vectors_release(vectors); + return FAILURE; + } + } else if (zend_string_equals_literal(op_name, "linear")) { + if (king_inference_graph_op_linear(model, entry, vectors, max_values, max_operations, &values, &length) != SUCCESS) { + zval_ptr_dtor(&state); + king_inference_graph_vectors_release(vectors); + return FAILURE; + } + } else if (zend_string_equals_literal(op_name, "add")) { + if (king_inference_graph_op_add(entry, vectors, max_values, &values, &length) != SUCCESS) { + zval_ptr_dtor(&state); + king_inference_graph_vectors_release(vectors); + return FAILURE; + } + } else if (zend_string_equals_literal(op_name, "scale")) { + if (king_inference_graph_op_scale(entry, vectors, max_values, &values, &length) != SUCCESS) { + zval_ptr_dtor(&state); + king_inference_graph_vectors_release(vectors); + return FAILURE; + } + } else if (zend_string_equals_literal(op_name, "mul")) { + if (king_inference_graph_op_mul(entry, vectors, max_values, &values, &length) != SUCCESS) { + zval_ptr_dtor(&state); + king_inference_graph_vectors_release(vectors); + return FAILURE; + } + } else if (zend_string_equals_literal(op_name, "silu")) { + if (king_inference_graph_op_silu(entry, vectors, max_values, &values, &length) != SUCCESS) { + zval_ptr_dtor(&state); + king_inference_graph_vectors_release(vectors); + return FAILURE; + } + } else if (zend_string_equals_literal(op_name, "gelu_tanh")) { + if (king_inference_graph_op_gelu_tanh(entry, vectors, max_values, &values, &length) != SUCCESS) { + zval_ptr_dtor(&state); + king_inference_graph_vectors_release(vectors); + return FAILURE; + } + } else if (zend_string_equals_literal(op_name, "slice")) { + if (king_inference_graph_op_slice(entry, vectors, max_values, &values, &length) != SUCCESS) { + zval_ptr_dtor(&state); + king_inference_graph_vectors_release(vectors); + return FAILURE; + } + } else if (zend_string_equals_literal(op_name, "dot")) { + if (king_inference_graph_op_dot(entry, vectors, max_values, max_operations, &values, &length) != SUCCESS) { + zval_ptr_dtor(&state); + king_inference_graph_vectors_release(vectors); + return FAILURE; + } + } else if (zend_string_equals_literal(op_name, "stack")) { + if (king_inference_graph_op_stack(entry, vectors, max_values, &values, &length) != SUCCESS) { + zval_ptr_dtor(&state); + king_inference_graph_vectors_release(vectors); + return FAILURE; + } + } else if (zend_string_equals_literal(op_name, "softmax")) { + if (king_inference_graph_op_softmax(entry, vectors, max_values, &values, &length) != SUCCESS) { + zval_ptr_dtor(&state); + king_inference_graph_vectors_release(vectors); + return FAILURE; + } + } else if (zend_string_equals_literal(op_name, "weighted_sum")) { + if (king_inference_graph_op_weighted_sum(entry, vectors, max_values, max_operations, &values, &length) != SUCCESS) { + zval_ptr_dtor(&state); + king_inference_graph_vectors_release(vectors); + return FAILURE; + } + } else if (zend_string_equals_literal(op_name, "argmax_token")) { + if (king_inference_graph_op_argmax_token(entry, vectors, max_values, &values, &length) != SUCCESS) { + zval_ptr_dtor(&state); + king_inference_graph_vectors_release(vectors); + return FAILURE; + } + } else if (zend_string_equals_literal(op_name, "sample_token")) { + if (king_inference_graph_op_sample_token(entry, vectors, max_values, max_operations, &values, &length) != SUCCESS) { + zval_ptr_dtor(&state); + king_inference_graph_vectors_release(vectors); + return FAILURE; + } + } else if (zend_string_equals_literal(op_name, "kv_read")) { + if (king_inference_graph_op_kv_read(entry, &state, max_values, &values, &length) != SUCCESS) { + zval_ptr_dtor(&state); + king_inference_graph_vectors_release(vectors); + return FAILURE; + } + } else if (zend_string_equals_literal(op_name, "kv_write")) { + if (king_inference_graph_op_kv_write(entry, vectors, &state, &values, &length) != SUCCESS) { + zval_ptr_dtor(&state); + king_inference_graph_vectors_release(vectors); + return FAILURE; + } + } else if (zend_string_equals_literal(op_name, "kv_attention")) { + if (king_inference_graph_op_kv_attention(entry, vectors, &state, max_values, max_operations, &values, &length) != SUCCESS) { + zval_ptr_dtor(&state); + king_inference_graph_vectors_release(vectors); + return FAILURE; + } + } else if (zend_string_equals_literal(op_name, "rope")) { + if (king_inference_graph_op_rope(entry, vectors, max_values, &values, &length) != SUCCESS) { + zval_ptr_dtor(&state); + king_inference_graph_vectors_release(vectors); + return FAILURE; + } + } else { + zval_ptr_dtor(&state); + king_inference_graph_vectors_release(vectors); + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph operation '%s' is not supported.", ZSTR_VAL(op_name)); + return FAILURE; + } + + if (king_inference_graph_vector_add(&vectors, id, values, length) != SUCCESS) { + if (values != NULL) { + efree(values); + } + zval_ptr_dtor(&state); + king_inference_graph_vectors_release(vectors); + return FAILURE; + } + op_count++; + } ZEND_HASH_FOREACH_END(); + + array_init(return_value); + add_assoc_long(return_value, "op_count", (zend_long) op_count); + add_assoc_long(return_value, "max_vector_values", (zend_long) max_values); + add_assoc_long(return_value, "max_operations", (zend_long) max_operations); + if (return_outputs) { + zval outputs; + array_init(&outputs); + for (node = vectors; node != NULL; node = node->next) { + king_inference_graph_add_vector_payload(&outputs, node); + } + add_assoc_zval(return_value, "outputs", &outputs); + } + + output_id = king_inference_array_find(graph, "output"); + if (output_id != NULL && Z_TYPE_P(output_id) == IS_STRING) { + king_inference_graph_vector *final_vector = king_inference_graph_vector_find(vectors, Z_STR_P(output_id)); + if (final_vector != NULL) { + zval final; + array_init(&final); + king_inference_graph_add_vector_payload(&final, final_vector); + add_assoc_str(return_value, "output", zend_string_copy(Z_STR_P(output_id))); + add_assoc_zval(return_value, "final", &final); + } + } + + if (state_initialized) { + add_assoc_zval(return_value, "state", &state); + state_initialized = false; + } + king_inference_graph_vectors_release(vectors); + return SUCCESS; +} diff --git a/extension/src/inference/tensor/graph/ops/tensor_graph_kv.inc b/extension/src/inference/tensor/graph/ops/tensor_graph_kv.inc new file mode 100644 index 000000000..99776eb33 --- /dev/null +++ b/extension/src/inference/tensor/graph/ops/tensor_graph_kv.inc @@ -0,0 +1,501 @@ +#define KING_INFERENCE_GRAPH_DEFAULT_KV_CACHE "default" + +static zend_result king_inference_graph_state_init(zval *graph, zval *state) +{ + zval *source = king_inference_array_find(graph, "state"); + + if (source == NULL) { + array_init(state); + return SUCCESS; + } + if (Z_TYPE_P(source) != IS_ARRAY) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph state must be an array."); + return FAILURE; + } + + ZVAL_COPY(state, source); + SEPARATE_ARRAY(state); + return SUCCESS; +} + +static zend_result king_inference_graph_kv_slot(zval *op, zend_ulong *slot) +{ + zval *field = king_inference_array_find(op, "slot"); + + if (field == NULL || Z_TYPE_P(field) != IS_LONG || Z_LVAL_P(field) < 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph KV operation requires non-negative integer slot."); + return FAILURE; + } + + *slot = (zend_ulong) Z_LVAL_P(field); + return SUCCESS; +} + +static zend_result king_inference_graph_kv_optional_ulong( + zval *op, + const char *key, + zend_ulong fallback, + zend_ulong *value +) { + zval *field = king_inference_array_find(op, key); + + if (field == NULL) { + *value = fallback; + return SUCCESS; + } + if (Z_TYPE_P(field) != IS_LONG || Z_LVAL_P(field) < 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph KV field '%s' must be a non-negative integer.", key); + return FAILURE; + } + + *value = (zend_ulong) Z_LVAL_P(field); + return SUCCESS; +} + +static zend_result king_inference_graph_kv_positive_ulong( + zval *op, + const char *primary, + const char *alternate, + zend_ulong *value +) { + zval *field = king_inference_array_find(op, primary); + + if (field == NULL && alternate != NULL) { + field = king_inference_array_find(op, alternate); + } + if (field == NULL || Z_TYPE_P(field) != IS_LONG || Z_LVAL_P(field) <= 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph KV field '%s' must be a positive integer.", primary); + return FAILURE; + } + + *value = (zend_ulong) Z_LVAL_P(field); + return SUCCESS; +} + +static zend_result king_inference_graph_kv_range( + zval *op, + zend_ulong *slot_start, + zend_ulong *slot_count +) { + if (king_inference_graph_kv_optional_ulong(op, "slot_start", 0, slot_start) != SUCCESS + || king_inference_graph_kv_positive_ulong(op, "slot_count", "count", slot_count) != SUCCESS) { + return FAILURE; + } + if (*slot_start > ZEND_ULONG_MAX - (*slot_count - 1)) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph KV range exceeds supported slot limits."); + return FAILURE; + } + + return SUCCESS; +} + +static zend_result king_inference_graph_kv_kind(zval *op, zend_string **kind) +{ + if (king_inference_graph_required_string(op, "kind", kind) != SUCCESS) { + return FAILURE; + } + if (!zend_string_equals_literal(*kind, "key") && !zend_string_equals_literal(*kind, "value")) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph KV kind must be 'key' or 'value'."); + return FAILURE; + } + + return SUCCESS; +} + +static zend_string *king_inference_graph_kv_cache_name(zval *op, bool *owned) +{ + zval *field = king_inference_array_find(op, "cache"); + + *owned = false; + if (field == NULL) { + *owned = true; + return zend_string_init(KING_INFERENCE_GRAPH_DEFAULT_KV_CACHE, sizeof(KING_INFERENCE_GRAPH_DEFAULT_KV_CACHE) - 1, 0); + } + if (Z_TYPE_P(field) != IS_STRING || Z_STRLEN_P(field) == 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph KV cache must be a non-empty string."); + return NULL; + } + + return Z_STR_P(field); +} + +static zend_string *king_inference_graph_kv_entry_key(zend_string *cache, zend_ulong slot, const char *kind) +{ + return strpprintf(0, "%s/%lu/%s", ZSTR_VAL(cache), slot, kind); +} + +static zval *king_inference_graph_kv_container(zval *state) +{ + zval *container = king_inference_array_find(state, "kv_cache"); + zval fresh; + + if (container != NULL && Z_TYPE_P(container) == IS_ARRAY) { + SEPARATE_ARRAY(container); + return container; + } + if (container != NULL) { + zend_hash_str_del(Z_ARRVAL_P(state), "kv_cache", sizeof("kv_cache") - 1); + } + + array_init(&fresh); + zend_hash_str_update(Z_ARRVAL_P(state), "kv_cache", sizeof("kv_cache") - 1, &fresh); + return king_inference_array_find(state, "kv_cache"); +} + +static void king_inference_graph_kv_write_payload( + zval *container, + zend_string *cache, + zend_ulong slot, + const char *kind, + king_inference_graph_vector *vector +) { + zend_string *entry_key = king_inference_graph_kv_entry_key(cache, slot, kind); + zval payload; + zval values; + zend_ulong i; + + array_init(&payload); + add_assoc_str(&payload, "cache", zend_string_copy(cache)); + add_assoc_long(&payload, "slot", (zend_long) slot); + add_assoc_string(&payload, "kind", kind); + add_assoc_long(&payload, "length", (zend_long) vector->length); + array_init(&values); + for (i = 0; i < vector->length; i++) { + add_next_index_double(&values, vector->values[i]); + } + add_assoc_zval(&payload, "values", &values); + zend_hash_update(Z_ARRVAL_P(container), entry_key, &payload); + zend_string_release(entry_key); +} + +static zend_result king_inference_graph_kv_read_payload( + zval *state, + zend_string *cache, + zend_ulong slot, + const char *kind, + zend_ulong max_values, + double **values_out, + zend_ulong *length_out +) { + zval *container = king_inference_array_find(state, "kv_cache"); + zend_string *entry_key; + zval *payload; + zval *values; + + *values_out = NULL; + *length_out = 0; + if (container == NULL || Z_TYPE_P(container) != IS_ARRAY) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph KV state has no kv_cache entries."); + return FAILURE; + } + + entry_key = king_inference_graph_kv_entry_key(cache, slot, kind); + payload = zend_hash_find(Z_ARRVAL_P(container), entry_key); + zend_string_release(entry_key); + if (payload == NULL || Z_TYPE_P(payload) != IS_ARRAY) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph KV state entry was not found."); + return FAILURE; + } + + values = king_inference_array_find(payload, "values"); + if (values == NULL || Z_TYPE_P(values) != IS_ARRAY) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph KV state entry has invalid values."); + return FAILURE; + } + + return king_inference_graph_vector_from_zval(values, max_values, values_out, length_out); +} + +static zend_result king_inference_graph_optional_vector( + king_inference_graph_vector *vectors, + zval *op, + const char *field_name, + king_inference_graph_vector **vector_out, + bool *present +) { + zval *field = king_inference_array_find(op, field_name); + + *vector_out = NULL; + *present = false; + if (field == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(field) != IS_STRING || Z_STRLEN_P(field) == 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph KV vector field '%s' must be a vector name.", field_name); + return FAILURE; + } + *vector_out = king_inference_graph_vector_find(vectors, Z_STR_P(field)); + if (*vector_out == NULL) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph KV vector '%s' was not produced.", Z_STRVAL_P(field)); + return FAILURE; + } + *present = true; + return SUCCESS; +} + +static zend_result king_inference_graph_op_kv_read( + zval *op, + zval *state, + zend_ulong max_values, + double **values_out, + zend_ulong *length_out +) { + zend_ulong slot; + zend_string *kind; + zend_string *cache; + bool cache_owned; + zend_result status; + + if (king_inference_graph_kv_slot(op, &slot) != SUCCESS + || king_inference_graph_kv_kind(op, &kind) != SUCCESS) { + return FAILURE; + } + cache = king_inference_graph_kv_cache_name(op, &cache_owned); + if (cache == NULL) { + return FAILURE; + } + + status = king_inference_graph_kv_read_payload(state, cache, slot, ZSTR_VAL(kind), max_values, values_out, length_out); + if (cache_owned) { + zend_string_release(cache); + } + return status; +} + +static zend_result king_inference_graph_op_kv_write( + zval *op, + king_inference_graph_vector *vectors, + zval *state, + double **values_out, + zend_ulong *length_out +) { + zend_ulong slot; + zend_string *cache; + bool cache_owned; + king_inference_graph_vector *key_vector = NULL; + king_inference_graph_vector *value_vector = NULL; + bool has_key = false; + bool has_value = false; + double *marker; + zval *container; + + if (king_inference_graph_kv_slot(op, &slot) != SUCCESS + || king_inference_graph_optional_vector(vectors, op, "key", &key_vector, &has_key) != SUCCESS + || king_inference_graph_optional_vector(vectors, op, "value", &value_vector, &has_value) != SUCCESS) { + return FAILURE; + } + if (!has_key && !has_value) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph kv_write requires key or value vector."); + return FAILURE; + } + + cache = king_inference_graph_kv_cache_name(op, &cache_owned); + if (cache == NULL) { + return FAILURE; + } + container = king_inference_graph_kv_container(state); + if (has_key) { + king_inference_graph_kv_write_payload(container, cache, slot, "key", key_vector); + } + if (has_value) { + king_inference_graph_kv_write_payload(container, cache, slot, "value", value_vector); + } + if (cache_owned) { + zend_string_release(cache); + } + + marker = ecalloc(3, sizeof(double)); + marker[0] = (double) slot; + marker[1] = has_key ? 1.0 : 0.0; + marker[2] = has_value ? 1.0 : 0.0; + *values_out = marker; + *length_out = 3; + return SUCCESS; +} + +static zend_result king_inference_graph_op_kv_attention( + zval *op, + king_inference_graph_vector *vectors, + zval *state, + zend_ulong max_values, + zend_ulong max_operations, + double **values_out, + zend_ulong *length_out +) { + king_inference_graph_vector *query; + zend_string *cache; + bool cache_owned; + zend_ulong slot_start; + zend_ulong slot_count; + zend_ulong value_width = 0; + zend_ulong i; + double temperature; + double scale; + double max_score = 0.0; + double probability_sum = 0.0; + double *scores = NULL; + double *context = NULL; + + *values_out = NULL; + *length_out = 0; + if (king_inference_graph_finite_double_option(op, "temperature", 1.0, &temperature) != SUCCESS + || king_inference_graph_finite_double_option(op, "scale", 1.0, &scale) != SUCCESS) { + return FAILURE; + } + if (king_inference_graph_lookup_vector(vectors, op, "query", &query) != SUCCESS + || king_inference_graph_kv_range(op, &slot_start, &slot_count) != SUCCESS) { + return FAILURE; + } + if (temperature <= 0.0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph kv_attention temperature must be positive."); + return FAILURE; + } + if (query->length == 0 + || query->length > max_values + || slot_count > max_values + || slot_count > (zend_ulong) (SIZE_MAX / sizeof(double))) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph kv_attention query or slot_count exceeds configured limits."); + return FAILURE; + } + if (query->length > max_operations / slot_count) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph kv_attention key scoring exceeds configured operation limits."); + return FAILURE; + } + + cache = king_inference_graph_kv_cache_name(op, &cache_owned); + if (cache == NULL) { + return FAILURE; + } + + scores = ecalloc((size_t) slot_count, sizeof(double)); + for (i = 0; i < slot_count; i++) { + double *key_values = NULL; + zend_ulong key_length = 0; + zend_ulong dim; + double score = 0.0; + + if (king_inference_graph_kv_read_payload( + state, + cache, + slot_start + i, + "key", + max_values, + &key_values, + &key_length + ) != SUCCESS) { + efree(scores); + if (cache_owned) { + zend_string_release(cache); + } + return FAILURE; + } + if (key_length != query->length) { + efree(key_values); + efree(scores); + if (cache_owned) { + zend_string_release(cache); + } + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph kv_attention key length must match query length."); + return FAILURE; + } + for (dim = 0; dim < query->length; dim++) { + score += query->values[dim] * key_values[dim]; + } + efree(key_values); + scores[i] = score * scale / temperature; + if (i == 0 || scores[i] > max_score) { + max_score = scores[i]; + } + } + + for (i = 0; i < slot_count; i++) { + scores[i] = king_inference_graph_exp(scores[i] - max_score); + probability_sum += scores[i]; + } + if (probability_sum <= 0.0) { + efree(scores); + if (cache_owned) { + zend_string_release(cache); + } + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King inference graph kv_attention produced an invalid probability sum."); + return FAILURE; + } + for (i = 0; i < slot_count; i++) { + scores[i] /= probability_sum; + } + + for (i = 0; i < slot_count; i++) { + double *value_values = NULL; + zend_ulong value_length = 0; + zend_ulong dim; + + if (king_inference_graph_kv_read_payload( + state, + cache, + slot_start + i, + "value", + max_values, + &value_values, + &value_length + ) != SUCCESS) { + if (context != NULL) { + efree(context); + } + efree(scores); + if (cache_owned) { + zend_string_release(cache); + } + return FAILURE; + } + if (value_length == 0 || value_length > max_values) { + efree(value_values); + if (context != NULL) { + efree(context); + } + efree(scores); + if (cache_owned) { + zend_string_release(cache); + } + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph kv_attention value length exceeds configured limits."); + return FAILURE; + } + if (value_width == 0) { + value_width = value_length; + if (query->length > ZEND_ULONG_MAX - value_width + || value_width > (zend_ulong) (SIZE_MAX / sizeof(double)) + || slot_count > max_operations / (query->length + value_width)) { + efree(value_values); + efree(scores); + if (cache_owned) { + zend_string_release(cache); + } + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph kv_attention exceeds configured operation limits."); + return FAILURE; + } + context = ecalloc((size_t) value_width, sizeof(double)); + } else if (value_width != value_length) { + efree(value_values); + efree(context); + efree(scores); + if (cache_owned) { + zend_string_release(cache); + } + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph kv_attention value lengths must match."); + return FAILURE; + } + + for (dim = 0; dim < value_width; dim++) { + context[dim] += scores[i] * value_values[dim]; + } + efree(value_values); + } + + efree(scores); + if (cache_owned) { + zend_string_release(cache); + } + + *values_out = context; + *length_out = value_width; + return SUCCESS; +} diff --git a/extension/src/inference/tensor/graph/ops/tensor_graph_ops.inc b/extension/src/inference/tensor/graph/ops/tensor_graph_ops.inc new file mode 100644 index 000000000..2e6094849 --- /dev/null +++ b/extension/src/inference/tensor/graph/ops/tensor_graph_ops.inc @@ -0,0 +1,730 @@ +#define KING_INFERENCE_GRAPH_PI 3.14159265358979323846264338327950288 +#define KING_INFERENCE_GRAPH_HALF_PI 1.57079632679489661923132169163975144 +#define KING_INFERENCE_GRAPH_TWO_PI 6.28318530717958647692528676655900576 +#define KING_INFERENCE_GRAPH_LN2 0.693147180559945309417232121458176568 + +static double king_inference_graph_wrap_angle(double value) +{ + if (value > KING_INFERENCE_GRAPH_TWO_PI || value < -KING_INFERENCE_GRAPH_TWO_PI) { + long turns = (long) (value / KING_INFERENCE_GRAPH_TWO_PI); + value -= (double) turns * KING_INFERENCE_GRAPH_TWO_PI; + } + while (value > KING_INFERENCE_GRAPH_PI) { + value -= KING_INFERENCE_GRAPH_TWO_PI; + } + while (value < -KING_INFERENCE_GRAPH_PI) { + value += KING_INFERENCE_GRAPH_TWO_PI; + } + + return value; +} + +static double king_inference_graph_sin(double value) +{ + double x = king_inference_graph_wrap_angle(value); + double x2; + + if (x > KING_INFERENCE_GRAPH_HALF_PI) { + x = KING_INFERENCE_GRAPH_PI - x; + } else if (x < -KING_INFERENCE_GRAPH_HALF_PI) { + x = -KING_INFERENCE_GRAPH_PI - x; + } + + x2 = x * x; + return x * (1.0 - x2 / 6.0 + (x2 * x2) / 120.0 - (x2 * x2 * x2) / 5040.0); +} + +static double king_inference_graph_cos(double value) +{ + double x = king_inference_graph_wrap_angle(value); + double sign = 1.0; + double x2; + + if (x > KING_INFERENCE_GRAPH_HALF_PI) { + x = KING_INFERENCE_GRAPH_PI - x; + sign = -1.0; + } else if (x < -KING_INFERENCE_GRAPH_HALF_PI) { + x = -KING_INFERENCE_GRAPH_PI - x; + sign = -1.0; + } + + x2 = x * x; + return sign * (1.0 - x2 / 2.0 + (x2 * x2) / 24.0 - (x2 * x2 * x2) / 720.0); +} + +static double king_inference_graph_exp(double value) +{ + long exponent; + int i; + double reduced; + double term = 1.0; + double sum = 1.0; + + if (value <= -60.0) { + return 0.0; + } + if (value > 60.0) { + value = 60.0; + } + + exponent = (long) (value / KING_INFERENCE_GRAPH_LN2); + if ((double) exponent * KING_INFERENCE_GRAPH_LN2 > value) { + exponent--; + } + reduced = value - (double) exponent * KING_INFERENCE_GRAPH_LN2; + + for (i = 1; i <= 12; i++) { + term *= reduced / (double) i; + sum += term; + } + + while (exponent > 0) { + sum *= 2.0; + exponent--; + } + while (exponent < 0) { + sum *= 0.5; + exponent++; + } + + return sum > 0.0 ? sum : 0.0; +} + +static double king_inference_graph_tanh(double value) +{ + if (value >= 20.0) { + return 1.0; + } + if (value <= -20.0) { + return -1.0; + } + + { + double twice = king_inference_graph_exp(2.0 * value); + return (twice - 1.0) / (twice + 1.0); + } +} + +static zend_result king_inference_graph_lookup_vector( + king_inference_graph_vector *vectors, + zval *op, + const char *key, + king_inference_graph_vector **vector_out +) { + zend_string *id; + + *vector_out = NULL; + if (king_inference_graph_required_string(op, key, &id) != SUCCESS) { + return FAILURE; + } + *vector_out = king_inference_graph_vector_find(vectors, id); + if (*vector_out == NULL) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph input '%s' was not produced.", ZSTR_VAL(id)); + return FAILURE; + } + + return SUCCESS; +} + +static zend_result king_inference_graph_vector_from_optional_zval( + zval *op, + const char *array_key, + const char *input_key, + king_inference_graph_vector *vectors, + zend_ulong max_values, + double **values_out, + zend_ulong *length_out, + bool *owned_out +) { + zval *input = king_inference_array_find(op, input_key); + zval *array; + + *values_out = NULL; + *length_out = 0; + *owned_out = false; + + if (input != NULL && Z_TYPE_P(input) != IS_STRING) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph vector input field '%s' must be a vector name.", input_key); + return FAILURE; + } + if (input != NULL) { + king_inference_graph_vector *vector = king_inference_graph_vector_find(vectors, Z_STR_P(input)); + if (vector == NULL) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph vector input '%s' was not produced.", Z_STRVAL_P(input)); + return FAILURE; + } + *values_out = vector->values; + *length_out = vector->length; + return SUCCESS; + } + + array = king_inference_array_find(op, array_key); + if (array == NULL) { + return SUCCESS; + } + if (king_inference_graph_vector_from_zval(array, max_values, values_out, length_out) != SUCCESS) { + return FAILURE; + } + *owned_out = true; + return SUCCESS; +} + +static zend_result king_inference_graph_op_add( + zval *op, + king_inference_graph_vector *vectors, + zend_ulong max_values, + double **values_out, + zend_ulong *length_out +) { + king_inference_graph_vector *left; + king_inference_graph_vector *right; + double left_scale; + double right_scale; + double *values; + zend_ulong i; + + if (king_inference_graph_finite_double_option(op, "left_scale", 1.0, &left_scale) != SUCCESS + || king_inference_graph_finite_double_option(op, "right_scale", 1.0, &right_scale) != SUCCESS) { + return FAILURE; + } + if (king_inference_graph_lookup_vector(vectors, op, "left", &left) != SUCCESS + || king_inference_graph_lookup_vector(vectors, op, "right", &right) != SUCCESS) { + return FAILURE; + } + if (left->length == 0 || left->length != right->length || left->length > max_values) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph add requires equal non-empty vector lengths."); + return FAILURE; + } + + values = ecalloc((size_t) left->length, sizeof(double)); + for (i = 0; i < left->length; i++) { + values[i] = left->values[i] * left_scale + right->values[i] * right_scale; + } + + *values_out = values; + *length_out = left->length; + return SUCCESS; +} + +static zend_result king_inference_graph_op_scale( + zval *op, + king_inference_graph_vector *vectors, + zend_ulong max_values, + double **values_out, + zend_ulong *length_out +) { + king_inference_graph_vector *input; + double scale; + double factor; + double *values; + zend_ulong i; + + if (king_inference_graph_finite_double_option(op, "scale", 1.0, &scale) != SUCCESS + || king_inference_graph_finite_double_option(op, "factor", scale, &factor) != SUCCESS) { + return FAILURE; + } + if (king_inference_graph_lookup_vector(vectors, op, "input", &input) != SUCCESS) { + return FAILURE; + } + if (input->length == 0 || input->length > max_values) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph scale input exceeds configured limits."); + return FAILURE; + } + + values = ecalloc((size_t) input->length, sizeof(double)); + for (i = 0; i < input->length; i++) { + values[i] = input->values[i] * factor; + } + + *values_out = values; + *length_out = input->length; + return SUCCESS; +} + +static zend_result king_inference_graph_op_mul( + zval *op, + king_inference_graph_vector *vectors, + zend_ulong max_values, + double **values_out, + zend_ulong *length_out +) { + king_inference_graph_vector *left; + king_inference_graph_vector *right; + double *values; + zend_ulong i; + + if (king_inference_graph_lookup_vector(vectors, op, "left", &left) != SUCCESS + || king_inference_graph_lookup_vector(vectors, op, "right", &right) != SUCCESS) { + return FAILURE; + } + if (left->length == 0 || left->length != right->length || left->length > max_values) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph mul requires equal non-empty vector lengths."); + return FAILURE; + } + + values = ecalloc((size_t) left->length, sizeof(double)); + for (i = 0; i < left->length; i++) { + values[i] = left->values[i] * right->values[i]; + } + + *values_out = values; + *length_out = left->length; + return SUCCESS; +} + +static zend_result king_inference_graph_op_silu( + zval *op, + king_inference_graph_vector *vectors, + zend_ulong max_values, + double **values_out, + zend_ulong *length_out +) { + king_inference_graph_vector *input; + double *values; + zend_ulong i; + + if (king_inference_graph_lookup_vector(vectors, op, "input", &input) != SUCCESS) { + return FAILURE; + } + if (input->length == 0 || input->length > max_values) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph silu input exceeds configured limits."); + return FAILURE; + } + + values = ecalloc((size_t) input->length, sizeof(double)); + for (i = 0; i < input->length; i++) { + double value = input->values[i]; + double sigmoid = value >= 0.0 + ? 1.0 / (1.0 + king_inference_graph_exp(-value)) + : king_inference_graph_exp(value) / (1.0 + king_inference_graph_exp(value)); + values[i] = value * sigmoid; + } + + *values_out = values; + *length_out = input->length; + return SUCCESS; +} + +static zend_result king_inference_graph_op_gelu_tanh( + zval *op, + king_inference_graph_vector *vectors, + zend_ulong max_values, + double **values_out, + zend_ulong *length_out +) { + king_inference_graph_vector *input; + double *values; + zend_ulong i; + + if (king_inference_graph_lookup_vector(vectors, op, "input", &input) != SUCCESS) { + return FAILURE; + } + if (input->length == 0 || input->length > max_values) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph gelu_tanh input exceeds configured limits."); + return FAILURE; + } + + values = ecalloc((size_t) input->length, sizeof(double)); + for (i = 0; i < input->length; i++) { + double x = input->values[i]; + double inner = 0.7978845608028654 * (x + 0.044715 * x * x * x); + values[i] = 0.5 * x * (1.0 + king_inference_graph_tanh(inner)); + } + + *values_out = values; + *length_out = input->length; + return SUCCESS; +} + +static zend_result king_inference_graph_op_slice( + zval *op, + king_inference_graph_vector *vectors, + zend_ulong max_values, + double **values_out, + zend_ulong *length_out +) { + king_inference_graph_vector *input; + zend_ulong offset = king_inference_tensor_option_ulong(op, "offset", 0); + zend_ulong count = king_inference_tensor_option_ulong(op, "count", 0); + double *values; + + if (king_inference_graph_lookup_vector(vectors, op, "input", &input) != SUCCESS) { + return FAILURE; + } + if (count == 0) { + count = input->length > offset ? input->length - offset : 0; + } + if (input->length == 0 + || offset > input->length + || count == 0 + || count > input->length - offset + || count > max_values) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph slice range is outside input bounds or configured limits."); + return FAILURE; + } + + values = ecalloc((size_t) count, sizeof(double)); + memcpy(values, input->values + offset, (size_t) count * sizeof(double)); + + *values_out = values; + *length_out = count; + return SUCCESS; +} + +static zend_result king_inference_graph_op_dot( + zval *op, + king_inference_graph_vector *vectors, + zend_ulong max_values, + zend_ulong max_operations, + double **values_out, + zend_ulong *length_out +) { + king_inference_graph_vector *left; + king_inference_graph_vector *right; + double scale; + double sum = 0.0; + double *values; + zend_ulong i; + + if (king_inference_graph_finite_double_option(op, "scale", 1.0, &scale) != SUCCESS) { + return FAILURE; + } + if (king_inference_graph_lookup_vector(vectors, op, "left", &left) != SUCCESS + || king_inference_graph_lookup_vector(vectors, op, "right", &right) != SUCCESS) { + return FAILURE; + } + if (left->length == 0 || left->length != right->length || left->length > max_values || left->length > max_operations) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph dot requires equal non-empty vector lengths inside configured limits."); + return FAILURE; + } + + for (i = 0; i < left->length; i++) { + sum += left->values[i] * right->values[i]; + } + values = ecalloc(1, sizeof(double)); + values[0] = sum * scale; + + *values_out = values; + *length_out = 1; + return SUCCESS; +} + +static zend_result king_inference_graph_op_stack( + zval *op, + king_inference_graph_vector *vectors, + zend_ulong max_values, + double **values_out, + zend_ulong *length_out +) { + zval *inputs = king_inference_array_find(op, "inputs"); + zval *entry; + double *values; + zend_ulong length = 0; + zend_ulong offset = 0; + + if (inputs == NULL || Z_TYPE_P(inputs) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL_P(inputs)) == 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph stack requires a non-empty inputs array."); + return FAILURE; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(inputs), entry) { + king_inference_graph_vector *input; + if (entry == NULL || Z_TYPE_P(entry) != IS_STRING) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph stack inputs must be vector names."); + return FAILURE; + } + input = king_inference_graph_vector_find(vectors, Z_STR_P(entry)); + if (input == NULL || input->length == 0 || input->length > max_values || length > max_values - input->length) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph stack input is missing or exceeds configured limits."); + return FAILURE; + } + length += input->length; + } ZEND_HASH_FOREACH_END(); + + values = ecalloc((size_t) length, sizeof(double)); + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(inputs), entry) { + king_inference_graph_vector *input = king_inference_graph_vector_find(vectors, Z_STR_P(entry)); + memcpy(values + offset, input->values, (size_t) input->length * sizeof(double)); + offset += input->length; + } ZEND_HASH_FOREACH_END(); + + *values_out = values; + *length_out = length; + return SUCCESS; +} + +static zend_result king_inference_graph_op_softmax( + zval *op, + king_inference_graph_vector *vectors, + zend_ulong max_values, + double **values_out, + zend_ulong *length_out +) { + king_inference_graph_vector *input; + double *mask = NULL; + double *values; + zend_ulong mask_length = 0; + bool mask_owned = false; + bool any_active = false; + double max_score = 0.0; + double sum = 0.0; + double temperature; + double scale; + zend_ulong i; + + if (king_inference_graph_finite_double_option(op, "temperature", 1.0, &temperature) != SUCCESS + || king_inference_graph_finite_double_option(op, "scale", 1.0, &scale) != SUCCESS) { + return FAILURE; + } + if (king_inference_graph_lookup_vector(vectors, op, "input", &input) != SUCCESS) { + return FAILURE; + } + if (input->length == 0 || input->length > max_values) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph softmax input exceeds configured limits."); + return FAILURE; + } + if (temperature <= 0.0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph softmax temperature must be positive."); + return FAILURE; + } + + if (king_inference_graph_vector_from_optional_zval( + op, + "mask", + "mask_input", + vectors, + max_values, + &mask, + &mask_length, + &mask_owned + ) != SUCCESS) { + return FAILURE; + } + if (mask != NULL && mask_length != input->length) { + if (mask_owned) { + efree(mask); + } + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph softmax mask length must match input length."); + return FAILURE; + } + + for (i = 0; i < input->length; i++) { + if (mask != NULL && mask[i] <= 0.0) { + continue; + } + { + double score = input->values[i] * scale / temperature; + if (!any_active || score > max_score) { + max_score = score; + } + any_active = true; + } + } + if (!any_active) { + if (mask_owned) { + efree(mask); + } + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph softmax mask disabled every score."); + return FAILURE; + } + + values = ecalloc((size_t) input->length, sizeof(double)); + for (i = 0; i < input->length; i++) { + if (mask != NULL && mask[i] <= 0.0) { + values[i] = 0.0; + continue; + } + values[i] = king_inference_graph_exp((input->values[i] * scale / temperature) - max_score); + sum += values[i]; + } + if (sum <= 0.0) { + if (mask_owned) { + efree(mask); + } + efree(values); + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King inference graph softmax produced an invalid probability sum."); + return FAILURE; + } + for (i = 0; i < input->length; i++) { + values[i] /= sum; + } + if (mask_owned) { + efree(mask); + } + + *values_out = values; + *length_out = input->length; + return SUCCESS; +} + +static zend_result king_inference_graph_op_weighted_sum( + zval *op, + king_inference_graph_vector *vectors, + zend_ulong max_values, + zend_ulong max_operations, + double **values_out, + zend_ulong *length_out +) { + zval *inputs = king_inference_array_find(op, "inputs"); + king_inference_graph_vector *weights; + zval *entry; + double *values; + zend_ulong vector_count; + zend_ulong width = 0; + zend_ulong index = 0; + zend_ulong i; + + if (king_inference_graph_lookup_vector(vectors, op, "weights", &weights) != SUCCESS) { + return FAILURE; + } + if (inputs == NULL || Z_TYPE_P(inputs) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL_P(inputs)) == 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph weighted_sum requires a non-empty inputs array."); + return FAILURE; + } + + vector_count = (zend_ulong) zend_hash_num_elements(Z_ARRVAL_P(inputs)); + if (weights->length != vector_count || vector_count > max_values || vector_count > max_operations) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph weighted_sum weight count does not match inputs."); + return FAILURE; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(inputs), entry) { + king_inference_graph_vector *input; + if (entry == NULL || Z_TYPE_P(entry) != IS_STRING) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph weighted_sum inputs must be vector names."); + return FAILURE; + } + input = king_inference_graph_vector_find(vectors, Z_STR_P(entry)); + if (input == NULL || input->length == 0 || input->length > max_values) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph weighted_sum input is missing or exceeds configured limits."); + return FAILURE; + } + if (width == 0) { + width = input->length; + } else if (width != input->length) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph weighted_sum inputs must have equal lengths."); + return FAILURE; + } + } ZEND_HASH_FOREACH_END(); + + if (width == 0 || width > max_values || vector_count > max_operations / width) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph weighted_sum exceeds configured limits."); + return FAILURE; + } + + values = ecalloc((size_t) width, sizeof(double)); + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(inputs), entry) { + king_inference_graph_vector *input = king_inference_graph_vector_find(vectors, Z_STR_P(entry)); + double weight = weights->values[index++]; + for (i = 0; i < width; i++) { + values[i] += weight * input->values[i]; + } + } ZEND_HASH_FOREACH_END(); + + *values_out = values; + *length_out = width; + return SUCCESS; +} + +static zend_result king_inference_graph_op_rope( + zval *op, + king_inference_graph_vector *vectors, + zend_ulong max_values, + double **values_out, + zend_ulong *length_out +) { + king_inference_graph_vector *input; + king_inference_graph_vector *frequency_vector = NULL; + double *owned_frequencies = NULL; + double *frequencies = NULL; + double *values; + zend_ulong frequency_length = 0; + zend_ulong offset = king_inference_tensor_option_ulong(op, "offset", 0); + zend_ulong head_dim; + zend_ulong position = king_inference_tensor_option_ulong(op, "position", 0); + double position_scale; + zval *frequency_input; + zval *frequency_values; + zend_ulong pair; + + if (king_inference_graph_finite_double_option(op, "position_scale", 1.0, &position_scale) != SUCCESS) { + return FAILURE; + } + if (king_inference_graph_lookup_vector(vectors, op, "input", &input) != SUCCESS) { + return FAILURE; + } + if (input->length == 0 || input->length > max_values || offset > input->length) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph rope input exceeds configured limits."); + return FAILURE; + } + + head_dim = king_inference_tensor_option_ulong(op, "head_dim", input->length - offset); + if (head_dim == 0 || (head_dim % 2) != 0 || head_dim > input->length - offset) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph rope requires an even head_dim inside the input vector."); + return FAILURE; + } + + frequency_input = king_inference_array_find(op, "frequency_input"); + if (frequency_input == NULL) { + frequency_input = king_inference_array_find(op, "inv_freq_input"); + } + if (frequency_input != NULL && Z_TYPE_P(frequency_input) == IS_STRING) { + frequency_vector = king_inference_graph_vector_find(vectors, Z_STR_P(frequency_input)); + if (frequency_vector == NULL) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph rope frequency input '%s' was not produced.", Z_STRVAL_P(frequency_input)); + return FAILURE; + } + frequencies = frequency_vector->values; + frequency_length = frequency_vector->length; + } else { + frequency_values = king_inference_array_find(op, "frequencies"); + if (frequency_values == NULL) { + frequency_values = king_inference_array_find(op, "inv_freqs"); + } + if (king_inference_graph_vector_from_zval(frequency_values, max_values, &owned_frequencies, &frequency_length) != SUCCESS) { + return FAILURE; + } + frequencies = owned_frequencies; + } + if (frequency_length < head_dim / 2) { + if (owned_frequencies != NULL) { + efree(owned_frequencies); + } + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph rope frequency count is smaller than head_dim pairs."); + return FAILURE; + } + + values = ecalloc((size_t) input->length, sizeof(double)); + memcpy(values, input->values, (size_t) input->length * sizeof(double)); + { + zend_ulong pairing = king_inference_tensor_option_ulong(op, "pairing", 0); + zend_ulong half = head_dim / 2; + + if (pairing > 1) { + if (owned_frequencies != NULL) { + efree(owned_frequencies); + } + efree(values); + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph rope pairing must be 0 (interleaved) or 1 (split)."); + return FAILURE; + } + for (pair = 0; pair < half; pair++) { + zend_ulong even = pairing == 1 ? offset + pair : offset + pair * 2; + zend_ulong odd = pairing == 1 ? offset + half + pair : even + 1; + double angle = (double) position * position_scale * frequencies[pair]; + double sin_value = king_inference_graph_sin(angle); + double cos_value = king_inference_graph_cos(angle); + double x0 = values[even]; + double x1 = values[odd]; + values[even] = x0 * cos_value - x1 * sin_value; + values[odd] = x0 * sin_value + x1 * cos_value; + } + } + if (owned_frequencies != NULL) { + efree(owned_frequencies); + } + + *values_out = values; + *length_out = input->length; + return SUCCESS; +} diff --git a/extension/src/inference/tensor/graph/ops/tensor_graph_sampling.inc b/extension/src/inference/tensor/graph/ops/tensor_graph_sampling.inc new file mode 100644 index 000000000..4d0d1912a --- /dev/null +++ b/extension/src/inference/tensor/graph/ops/tensor_graph_sampling.inc @@ -0,0 +1,267 @@ +typedef struct _king_inference_graph_token_candidate { + zend_ulong index; + double logit; + double probability; +} king_inference_graph_token_candidate; + +static int king_inference_graph_token_candidate_compare(const void *left, const void *right) +{ + const king_inference_graph_token_candidate *a = (const king_inference_graph_token_candidate *) left; + const king_inference_graph_token_candidate *b = (const king_inference_graph_token_candidate *) right; + + if (a->probability < b->probability) { + return 1; + } + if (a->probability > b->probability) { + return -1; + } + if (a->index > b->index) { + return 1; + } + if (a->index < b->index) { + return -1; + } + + return 0; +} + +static zend_result king_inference_graph_sampling_seed( + zval *op, + bool *has_seed, + zend_long *seed +) { + zval *field = king_inference_array_find(op, "seed"); + + *has_seed = false; + *seed = 0; + if (field == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(field) != IS_LONG) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph sample_token seed must be an integer."); + return FAILURE; + } + + *has_seed = true; + *seed = Z_LVAL_P(field); + return SUCCESS; +} + +static double king_inference_graph_sampling_unit(zend_long seed, zend_ulong salt) +{ + zend_ulong state = (zend_ulong) seed; + + state ^= salt + 0x9e3779b9UL + (state << 6) + (state >> 2); + state = state * 1103515245UL + 12345UL; + return (double) (state % 1000000UL) / 1000000.0; +} + +static zend_result king_inference_graph_sampling_top_k(zval *op, zend_ulong *top_k) +{ + zval *field = king_inference_array_find(op, "top_k"); + + *top_k = 0; + if (field == NULL) { + return SUCCESS; + } + if (Z_TYPE_P(field) != IS_LONG || Z_LVAL_P(field) < 0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph sample_token top_k must be a non-negative integer."); + return FAILURE; + } + + *top_k = (zend_ulong) Z_LVAL_P(field); + return SUCCESS; +} + +static zend_result king_inference_graph_token_output( + zend_ulong token_offset, + zend_ulong token_index, + double probability, + double logit, + double rank, + double **values_out, + zend_ulong *length_out +) { + double *values; + + if (token_offset > ZEND_ULONG_MAX - token_index) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph token output exceeds supported token id limits."); + return FAILURE; + } + + values = ecalloc(4, sizeof(double)); + values[0] = (double) (token_offset + token_index); + values[1] = probability; + values[2] = logit; + values[3] = rank; + + *values_out = values; + *length_out = 4; + return SUCCESS; +} + +static zend_result king_inference_graph_op_argmax_token( + zval *op, + king_inference_graph_vector *vectors, + zend_ulong max_values, + double **values_out, + zend_ulong *length_out +) { + king_inference_graph_vector *logits; + zend_ulong token_offset = king_inference_tensor_option_ulong(op, "token_offset", 0); + zend_ulong best_index = 0; + double best_logit; + zend_ulong i; + + if (king_inference_graph_lookup_vector(vectors, op, "logits", &logits) != SUCCESS) { + return FAILURE; + } + if (logits->length == 0 || logits->length > max_values) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph argmax_token logits exceed configured limits."); + return FAILURE; + } + + best_logit = logits->values[0]; + for (i = 1; i < logits->length; i++) { + if (logits->values[i] > best_logit) { + best_logit = logits->values[i]; + best_index = i; + } + } + + return king_inference_graph_token_output( + token_offset, + best_index, + 1.0, + best_logit, + 0.0, + values_out, + length_out + ); +} + +static zend_result king_inference_graph_op_sample_token( + zval *op, + king_inference_graph_vector *vectors, + zend_ulong max_values, + zend_ulong max_operations, + double **values_out, + zend_ulong *length_out +) { + king_inference_graph_vector *logits; + king_inference_graph_token_candidate *candidates; + zend_ulong token_offset = king_inference_tensor_option_ulong(op, "token_offset", 0); + zend_ulong top_k; + zend_ulong sample_index = king_inference_tensor_option_ulong(op, "sample_index", 0); + zend_ulong active_count; + zend_ulong selected_rank = 0; + zend_ulong i; + double temperature; + double top_p; + double max_score = 0.0; + double probability_sum = 0.0; + double active_sum = 0.0; + bool has_seed; + zend_long seed; + + if (king_inference_graph_finite_double_option(op, "temperature", 1.0, &temperature) != SUCCESS + || king_inference_graph_finite_double_option(op, "top_p", 1.0, &top_p) != SUCCESS) { + return FAILURE; + } + if (king_inference_graph_lookup_vector(vectors, op, "logits", &logits) != SUCCESS + || king_inference_graph_sampling_seed(op, &has_seed, &seed) != SUCCESS + || king_inference_graph_sampling_top_k(op, &top_k) != SUCCESS) { + return FAILURE; + } + if (logits->length == 0 + || logits->length > max_values + || logits->length > max_operations + || logits->length > (zend_ulong) (SIZE_MAX / sizeof(king_inference_graph_token_candidate))) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph sample_token logits exceed configured limits."); + return FAILURE; + } + if (temperature < 0.0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph sample_token temperature must be non-negative."); + return FAILURE; + } + if (top_p <= 0.0 || top_p > 1.0) { + zend_throw_exception_ex(king_ce_validation_exception, 0, "King inference graph sample_token top_p must be greater than zero and at most one."); + return FAILURE; + } + if (temperature == 0.0) { + return king_inference_graph_op_argmax_token(op, vectors, max_values, values_out, length_out); + } + + candidates = ecalloc((size_t) logits->length, sizeof(*candidates)); + for (i = 0; i < logits->length; i++) { + double score = logits->values[i] / temperature; + candidates[i].index = i; + candidates[i].logit = logits->values[i]; + candidates[i].probability = score; + if (i == 0 || score > max_score) { + max_score = score; + } + } + + for (i = 0; i < logits->length; i++) { + candidates[i].probability = king_inference_graph_exp(candidates[i].probability - max_score); + probability_sum += candidates[i].probability; + } + if (probability_sum <= 0.0) { + efree(candidates); + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King inference graph sample_token produced an invalid probability sum."); + return FAILURE; + } + for (i = 0; i < logits->length; i++) { + candidates[i].probability /= probability_sum; + } + + qsort(candidates, (size_t) logits->length, sizeof(*candidates), king_inference_graph_token_candidate_compare); + active_count = top_k == 0 || top_k > logits->length ? logits->length : top_k; + probability_sum = 0.0; + for (i = 0; i < active_count; i++) { + probability_sum += candidates[i].probability; + if (probability_sum >= top_p) { + active_count = i + 1; + break; + } + } + + for (i = 0; i < active_count; i++) { + active_sum += candidates[i].probability; + } + if (active_sum <= 0.0) { + efree(candidates); + zend_throw_exception_ex(king_ce_runtime_exception, 0, "King inference graph sample_token active probability sum is invalid."); + return FAILURE; + } + + if (has_seed) { + double target = king_inference_graph_sampling_unit(seed, logits->length ^ (active_count << 7) ^ sample_index) * active_sum; + double cumulative = 0.0; + + selected_rank = active_count - 1; + for (i = 0; i < active_count; i++) { + cumulative += candidates[i].probability; + if (target <= cumulative) { + selected_rank = i; + break; + } + } + } + + { + king_inference_graph_token_candidate selected = candidates[selected_rank]; + zend_result status = king_inference_graph_token_output( + token_offset, + selected.index, + selected.probability / active_sum, + selected.logit, + (double) selected_rank, + values_out, + length_out + ); + efree(candidates); + return status; + } +} diff --git a/extension/src/inference/tensor/resolvers/attention/components/metadata/tensor_attention_info.inc b/extension/src/inference/tensor/resolvers/attention/components/metadata/tensor_attention_info.inc new file mode 100644 index 000000000..6f49237a7 --- /dev/null +++ b/extension/src/inference/tensor/resolvers/attention/components/metadata/tensor_attention_info.inc @@ -0,0 +1,132 @@ +static void king_inference_add_attention_known_patterns(zval *attention) +{ + zval known_patterns; + int role; + + array_init(&known_patterns); + for (role = KING_INFERENCE_ATTENTION_QUERY; role <= KING_INFERENCE_ATTENTION_OUTPUT; role++) { + zval patterns; + const char **role_patterns = king_inference_attention_role_patterns((king_inference_attention_role) role); + size_t i; + + array_init(&patterns); + for (i = 0; role_patterns[i] != NULL; i++) { + add_next_index_string(&patterns, role_patterns[i]); + } + add_assoc_zval(&known_patterns, king_inference_attention_role_keys[role], &patterns); + } + add_assoc_zval(attention, "known_patterns", &known_patterns); +} + +static void king_inference_add_attention_config_fields(zval *attention) +{ + zval fields; + int role; + + array_init(&fields); + for (role = KING_INFERENCE_ATTENTION_QUERY; role <= KING_INFERENCE_ATTENTION_OUTPUT; role++) { + add_assoc_string( + &fields, + king_inference_attention_role_keys[role], + king_inference_attention_config_fields[role] + ); + } + add_assoc_zval(attention, "configured_pattern_fields", &fields); +} + +static bool king_inference_add_attention_role_entry( + king_inference_model_object *model, + zval *layer_entry, + king_inference_attention_role role, + zend_ulong layer +) { + zend_string *name = NULL; + const char *source = NULL; + const char *status = "not_found"; + zend_ulong expected_columns = 0; + zend_ulong expected_rows = 0; + zend_result result; + zval entry; + + result = king_inference_resolve_attention_tensor_name( + model, + role, + layer, + &name, + &source, + &status + ); + + king_inference_attention_expected_dimensions(model, role, layer, &expected_columns, &expected_rows); + array_init(&entry); + add_assoc_bool(&entry, "resolved", result == SUCCESS && name != NULL); + add_assoc_string(&entry, "status", status != NULL ? status : "not_found"); + add_assoc_long(&entry, "expected_columns", (zend_long) expected_columns); + add_assoc_long(&entry, "expected_rows", (zend_long) expected_rows); + if (source != NULL) { + add_assoc_string(&entry, "source", source); + } + if (name != NULL) { + add_assoc_str(&entry, "name", name); + } + add_assoc_zval(layer_entry, king_inference_attention_role_keys[role], &entry); + + return result == SUCCESS && name != NULL; +} + +static void king_inference_add_attention_resolution_info( + king_inference_model_object *model, + zval *resolved +) { + zend_ulong block_count = model->gguf.architecture_params[KING_INFERENCE_ARCH_BLOCK_COUNT]; + zend_ulong layer; + zend_ulong resolved_layers = 0; + zend_ulong resolved_tensors = 0; + zval attention; + zval layers; + + array_init(&attention); + array_init(&layers); + + for (layer = 0; layer < block_count; layer++) { + zval layer_entry; + bool layer_complete = true; + int role; + + array_init(&layer_entry); + add_assoc_long(&layer_entry, "index", (zend_long) layer); + for (role = KING_INFERENCE_ATTENTION_QUERY; role <= KING_INFERENCE_ATTENTION_OUTPUT; role++) { + bool role_resolved = king_inference_add_attention_role_entry( + model, + &layer_entry, + (king_inference_attention_role) role, + layer + ); + if (role_resolved) { + resolved_tensors++; + } else { + layer_complete = false; + } + } + add_assoc_bool(&layer_entry, "resolved", layer_complete); + if (layer_complete) { + resolved_layers++; + } + add_next_index_zval(&layers, &layer_entry); + } + + add_assoc_bool(&attention, "resolved", block_count > 0 && resolved_layers == block_count); + add_assoc_string( + &attention, + "status", + block_count == 0 ? "shape_metadata_unavailable" : + (resolved_layers == block_count ? "resolved" : "partial") + ); + add_assoc_long(&attention, "block_count", (zend_long) block_count); + add_assoc_long(&attention, "resolved_layers", (zend_long) resolved_layers); + add_assoc_long(&attention, "resolved_tensors", (zend_long) resolved_tensors); + king_inference_add_attention_config_fields(&attention); + king_inference_add_attention_known_patterns(&attention); + add_assoc_zval(&attention, "layers", &layers); + add_assoc_zval(resolved, "attention", &attention); +} diff --git a/extension/src/inference/tensor/resolvers/attention/components/metadata/tensor_attention_patterns.inc b/extension/src/inference/tensor/resolvers/attention/components/metadata/tensor_attention_patterns.inc new file mode 100644 index 000000000..0a34e9854 --- /dev/null +++ b/extension/src/inference/tensor/resolvers/attention/components/metadata/tensor_attention_patterns.inc @@ -0,0 +1,117 @@ +typedef enum _king_inference_attention_role { + KING_INFERENCE_ATTENTION_QUERY = 0, + KING_INFERENCE_ATTENTION_KEY = 1, + KING_INFERENCE_ATTENTION_VALUE = 2, + KING_INFERENCE_ATTENTION_OUTPUT = 3 +} king_inference_attention_role; + +static const char *king_inference_attention_role_keys[] = { + "query", + "key", + "value", + "output" +}; + +static const char *king_inference_attention_config_fields[] = { + "attention_query_tensor_pattern", + "attention_key_tensor_pattern", + "attention_value_tensor_pattern", + "attention_output_tensor_pattern" +}; + +static const char *king_inference_attention_query_patterns[] = { + "blk.{layer}.attn_q.weight", + "blk.{layer}.attention_q.weight", + "model.layers.{layer}.self_attn.q_proj.weight", + "layers.{layer}.self_attn.q_proj.weight", + "decoder.layers.{layer}.self_attn.q_proj.weight", + "transformer.h.{layer}.attn.q_proj.weight", + "transformer.blocks.{layer}.attn.q_proj.weight", + NULL +}; + +static const char *king_inference_attention_key_patterns[] = { + "blk.{layer}.attn_k.weight", + "blk.{layer}.attention_k.weight", + "model.layers.{layer}.self_attn.k_proj.weight", + "layers.{layer}.self_attn.k_proj.weight", + "decoder.layers.{layer}.self_attn.k_proj.weight", + "transformer.h.{layer}.attn.k_proj.weight", + "transformer.blocks.{layer}.attn.k_proj.weight", + NULL +}; + +static const char *king_inference_attention_value_patterns[] = { + "blk.{layer}.attn_v.weight", + "blk.{layer}.attention_v.weight", + "model.layers.{layer}.self_attn.v_proj.weight", + "layers.{layer}.self_attn.v_proj.weight", + "decoder.layers.{layer}.self_attn.v_proj.weight", + "transformer.h.{layer}.attn.v_proj.weight", + "transformer.blocks.{layer}.attn.v_proj.weight", + NULL +}; + +static const char *king_inference_attention_output_patterns[] = { + "blk.{layer}.attn_output.weight", + "blk.{layer}.attn_o.weight", + "blk.{layer}.attention_output.weight", + "model.layers.{layer}.self_attn.o_proj.weight", + "layers.{layer}.self_attn.o_proj.weight", + "decoder.layers.{layer}.self_attn.o_proj.weight", + "transformer.h.{layer}.attn.out_proj.weight", + "transformer.blocks.{layer}.attn.out_proj.weight", + NULL +}; + +static const char **king_inference_attention_role_patterns(king_inference_attention_role role) +{ + switch (role) { + case KING_INFERENCE_ATTENTION_QUERY: + return king_inference_attention_query_patterns; + case KING_INFERENCE_ATTENTION_KEY: + return king_inference_attention_key_patterns; + case KING_INFERENCE_ATTENTION_VALUE: + return king_inference_attention_value_patterns; + case KING_INFERENCE_ATTENTION_OUTPUT: + return king_inference_attention_output_patterns; + } + + return king_inference_attention_query_patterns; +} + +static zend_string *king_inference_attention_layer_pattern_name( + const char *pattern, + zend_ulong layer +) { + const char *placeholder = strstr(pattern, "{layer}"); + const char *suffix; + char layer_buf[32]; + int layer_len; + size_t prefix_len; + size_t suffix_len; + size_t total_len; + zend_string *name; + + if (placeholder == NULL) { + return NULL; + } + + layer_len = snprintf(layer_buf, sizeof(layer_buf), "%lu", (unsigned long) layer); + if (layer_len <= 0 || (size_t) layer_len >= sizeof(layer_buf)) { + return NULL; + } + + prefix_len = (size_t) (placeholder - pattern); + suffix = placeholder + sizeof("{layer}") - 1; + suffix_len = strlen(suffix); + total_len = prefix_len + (size_t) layer_len + suffix_len; + name = zend_string_alloc(total_len, 0); + memcpy(ZSTR_VAL(name), pattern, prefix_len); + memcpy(ZSTR_VAL(name) + prefix_len, layer_buf, (size_t) layer_len); + memcpy(ZSTR_VAL(name) + prefix_len + (size_t) layer_len, suffix, suffix_len); + ZSTR_VAL(name)[total_len] = '\0'; + + return name; +} + diff --git a/extension/src/inference/tensor/resolvers/attention/components/shape/tensor_attention_dimensions.inc b/extension/src/inference/tensor/resolvers/attention/components/shape/tensor_attention_dimensions.inc new file mode 100644 index 000000000..1ac684aee --- /dev/null +++ b/extension/src/inference/tensor/resolvers/attention/components/shape/tensor_attention_dimensions.inc @@ -0,0 +1,240 @@ +static bool king_inference_attention_descriptor_dimensions( + zval *descriptor, + zend_ulong *columns_out, + zend_ulong *rows_out +) { + zend_ulong rank; + + if (!king_inference_tensor_descriptor_rank(descriptor, &rank) + || rank != 2 + || !king_inference_tensor_descriptor_dimension(descriptor, 0, columns_out) + || !king_inference_tensor_descriptor_dimension(descriptor, 1, rows_out)) { + return false; + } + + return *columns_out > 0 && *rows_out > 0; +} + +static bool king_inference_attention_norm_width( + king_inference_model_object *model, + const char *name, + zend_ulong *width_out +) { + zend_string *tensor_name; + zval *descriptor; + zend_ulong rank; + + tensor_name = zend_string_init(name, strlen(name), 0); + descriptor = king_inference_tensor_index_descriptor(model, tensor_name); + zend_string_release(tensor_name); + if (descriptor == NULL + || !king_inference_tensor_descriptor_rank(descriptor, &rank) + || rank != 1 + || !king_inference_tensor_descriptor_dimension(descriptor, 0, width_out)) { + return false; + } + + return *width_out > 0; +} + +static bool king_inference_attention_layer_norm_width( + king_inference_model_object *model, + zend_ulong layer, + const char *suffix, + zend_ulong *width_out +) { + char name[96]; + + if (snprintf(name, sizeof(name), "blk.%lu.%s", (unsigned long) layer, suffix) >= (int) sizeof(name)) { + return false; + } + + return king_inference_attention_norm_width(model, name, width_out); +} + +static bool king_inference_attention_layer_dimensions( + king_inference_model_object *model, + king_inference_attention_role role, + zend_ulong layer, + zend_ulong *columns_out, + zend_ulong *rows_out +) { + const char **patterns = king_inference_attention_role_patterns(role); + size_t i; + + if (Z_ISUNDEF(model->tensor_index) || Z_TYPE(model->tensor_index) != IS_ARRAY) { + return false; + } + + for (i = 0; patterns[i] != NULL; i++) { + zend_string *name = king_inference_attention_layer_pattern_name(patterns[i], layer); + zval *descriptor; + bool valid; + + if (name == NULL) { + continue; + } + descriptor = king_inference_tensor_index_descriptor(model, name); + valid = king_inference_attention_descriptor_dimensions(descriptor, columns_out, rows_out); + zend_string_release(name); + if (valid) { + return true; + } + } + + return false; +} + +static bool king_inference_attention_gemma4_expected_dimensions( + king_inference_model_object *model, + king_inference_attention_role role, + zend_ulong layer, + zend_ulong *columns_out, + zend_ulong *rows_out +) { + zend_ulong embedding = model->gguf.architecture_params[KING_INFERENCE_ARCH_EMBEDDING_LENGTH]; + zend_ulong head_count = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT]; + zend_ulong q_norm_width; + zend_ulong k_norm_width; + zend_ulong columns; + zend_ulong rows; + zend_ulong query_rows; + zend_ulong max_kv_rows; + king_inference_attention_role dimension_role = role; + + if (!king_inference_gguf_architecture_is_gemma4(&model->gguf) + || embedding == 0 + || head_count == 0 + || !king_inference_attention_layer_norm_width(model, layer, "attn_q_norm.weight", &q_norm_width) + || !king_inference_attention_layer_norm_width(model, layer, "attn_k_norm.weight", &k_norm_width) + || head_count > ZEND_ULONG_MAX / q_norm_width + || head_count > ZEND_ULONG_MAX / k_norm_width) { + return false; + } + + query_rows = head_count * q_norm_width; + max_kv_rows = head_count * k_norm_width; + if (role == KING_INFERENCE_ATTENTION_VALUE + && !king_inference_attention_layer_dimensions(model, role, layer, &columns, &rows)) { + dimension_role = KING_INFERENCE_ATTENTION_KEY; + } + if (!king_inference_attention_layer_dimensions(model, dimension_role, layer, &columns, &rows)) { + return false; + } + + switch (role) { + case KING_INFERENCE_ATTENTION_QUERY: + if (columns != embedding || rows != query_rows) { + return false; + } + break; + case KING_INFERENCE_ATTENTION_KEY: + case KING_INFERENCE_ATTENTION_VALUE: + if (columns != embedding || rows == 0 || rows > max_kv_rows || rows % k_norm_width != 0) { + return false; + } + break; + case KING_INFERENCE_ATTENTION_OUTPUT: + if (columns != query_rows || rows != embedding) { + return false; + } + break; + } + + *columns_out = columns; + *rows_out = rows; + return true; +} + +static bool king_inference_attention_expected_dimensions( + king_inference_model_object *model, + king_inference_attention_role role, + zend_ulong layer, + zend_ulong *columns_out, + zend_ulong *rows_out +) { + zend_ulong embedding = model->gguf.architecture_params[KING_INFERENCE_ARCH_EMBEDDING_LENGTH]; + zend_ulong head_count = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT]; + zend_ulong kv_head_count = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_HEAD_COUNT_KV]; + zend_ulong key_length = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_KEY_LENGTH]; + zend_ulong value_length = model->gguf.architecture_params[KING_INFERENCE_ARCH_ATTENTION_VALUE_LENGTH]; + + *columns_out = 0; + *rows_out = 0; + if (king_inference_attention_gemma4_expected_dimensions(model, role, layer, columns_out, rows_out)) { + return true; + } + if (embedding == 0 || head_count == 0) { + return false; + } + if (kv_head_count == 0) { + kv_head_count = head_count; + } + if (key_length == 0) { + if (embedding % head_count != 0) { + return false; + } + key_length = embedding / head_count; + } + if (value_length == 0) { + if (embedding % head_count != 0) { + return false; + } + value_length = embedding / head_count; + } + + *columns_out = embedding; + switch (role) { + case KING_INFERENCE_ATTENTION_QUERY: + if (head_count > ZEND_ULONG_MAX / key_length) { + return false; + } + *rows_out = head_count * key_length; + break; + case KING_INFERENCE_ATTENTION_KEY: + if (kv_head_count > ZEND_ULONG_MAX / key_length) { + return false; + } + *rows_out = kv_head_count * key_length; + break; + case KING_INFERENCE_ATTENTION_VALUE: + if (kv_head_count > ZEND_ULONG_MAX / value_length) { + return false; + } + *rows_out = kv_head_count * value_length; + break; + case KING_INFERENCE_ATTENTION_OUTPUT: + if (head_count > ZEND_ULONG_MAX / value_length) { + return false; + } + *columns_out = head_count * value_length; + *rows_out = embedding; + break; + } + + return *columns_out > 0 && *rows_out > 0; +} + +static bool king_inference_attention_descriptor_valid( + king_inference_model_object *model, + king_inference_attention_role role, + zend_ulong layer, + zval *descriptor +) { + zend_ulong rank; + zend_ulong columns; + zend_ulong rows; + zend_ulong expected_columns; + zend_ulong expected_rows; + + if (!king_inference_attention_expected_dimensions(model, role, layer, &expected_columns, &expected_rows) + || !king_inference_tensor_descriptor_rank(descriptor, &rank) + || rank != 2 + || !king_inference_tensor_descriptor_dimension(descriptor, 0, &columns) + || !king_inference_tensor_descriptor_dimension(descriptor, 1, &rows)) { + return false; + } + + return columns == expected_columns && rows == expected_rows; +} + diff --git a/extension/src/inference/tensor/resolvers/attention/components/shape/tensor_attention_resolution.inc b/extension/src/inference/tensor/resolvers/attention/components/shape/tensor_attention_resolution.inc new file mode 100644 index 000000000..3584284a1 --- /dev/null +++ b/extension/src/inference/tensor/resolvers/attention/components/shape/tensor_attention_resolution.inc @@ -0,0 +1,310 @@ +static zend_string *king_inference_attention_gemma4_value_tied_key_name( + king_inference_model_object *model, + zend_ulong layer +) { + zend_string *name; + zval *descriptor; + + if (!king_inference_gguf_architecture_is_gemma4(&model->gguf)) { + return NULL; + } + + name = king_inference_attention_layer_pattern_name("blk.{layer}.attn_k.weight", layer); + if (name == NULL) { + return NULL; + } + descriptor = king_inference_tensor_index_descriptor(model, name); + if (descriptor != NULL + && king_inference_attention_descriptor_valid( + model, + KING_INFERENCE_ATTENTION_VALUE, + layer, + descriptor + )) { + return name; + } + + zend_string_release(name); + return NULL; +} + +static bool king_inference_attention_name_matches_layer(zend_string *name, zend_ulong layer) +{ + char token[64]; + + snprintf(token, sizeof(token), "blk.%lu.", (unsigned long) layer); + if (king_inference_tensor_name_contains(name, token)) { + return true; + } + snprintf(token, sizeof(token), "layers.%lu.", (unsigned long) layer); + if (king_inference_tensor_name_contains(name, token)) { + return true; + } + snprintf(token, sizeof(token), "blocks.%lu.", (unsigned long) layer); + if (king_inference_tensor_name_contains(name, token)) { + return true; + } + snprintf(token, sizeof(token), "h.%lu.", (unsigned long) layer); + return king_inference_tensor_name_contains(name, token); +} + +static bool king_inference_attention_name_hint( + zend_string *name, + king_inference_attention_role role, + zend_ulong layer +) { + if (!king_inference_attention_name_matches_layer(name, layer) + || king_inference_tensor_name_contains(name, "norm") + || king_inference_tensor_name_contains(name, "ffn") + || king_inference_tensor_name_contains(name, "feed_forward") + || king_inference_tensor_name_contains(name, "mlp") + || king_inference_tensor_name_contains(name, "token") + || king_inference_tensor_name_contains(name, "embed") + || king_inference_tensor_name_contains(name, "lm_head")) { + return false; + } + + switch (role) { + case KING_INFERENCE_ATTENTION_QUERY: + return king_inference_tensor_name_contains(name, "attn_q") + || king_inference_tensor_name_contains(name, "attention_q") + || king_inference_tensor_name_contains(name, "q_proj") + || king_inference_tensor_name_contains(name, "query"); + case KING_INFERENCE_ATTENTION_KEY: + return king_inference_tensor_name_contains(name, "attn_k") + || king_inference_tensor_name_contains(name, "attention_k") + || king_inference_tensor_name_contains(name, "k_proj") + || king_inference_tensor_name_contains(name, "key"); + case KING_INFERENCE_ATTENTION_VALUE: + return king_inference_tensor_name_contains(name, "attn_v") + || king_inference_tensor_name_contains(name, "attention_v") + || king_inference_tensor_name_contains(name, "v_proj") + || king_inference_tensor_name_contains(name, "value"); + case KING_INFERENCE_ATTENTION_OUTPUT: + return king_inference_tensor_name_contains(name, "attn_output") + || king_inference_tensor_name_contains(name, "attn_o") + || king_inference_tensor_name_contains(name, "attention_output") + || king_inference_tensor_name_contains(name, "o_proj") + || king_inference_tensor_name_contains(name, "out_proj"); + } + + return false; +} + +static zend_string *king_inference_attention_known_name( + king_inference_model_object *model, + king_inference_attention_role role, + zend_ulong layer +) { + const char **patterns = king_inference_attention_role_patterns(role); + size_t i; + + if (Z_ISUNDEF(model->tensor_index) || Z_TYPE(model->tensor_index) != IS_ARRAY) { + return NULL; + } + + for (i = 0; patterns[i] != NULL; i++) { + zend_string *name = king_inference_attention_layer_pattern_name(patterns[i], layer); + zval *descriptor; + + if (name == NULL) { + continue; + } + descriptor = king_inference_tensor_index_descriptor(model, name); + if (descriptor != NULL && king_inference_attention_descriptor_valid(model, role, layer, descriptor)) { + return name; + } + zend_string_release(name); + } + + if (role == KING_INFERENCE_ATTENTION_VALUE) { + return king_inference_attention_gemma4_value_tied_key_name(model, layer); + } + + return NULL; +} + +static zend_string *king_inference_attention_shape_scan( + king_inference_model_object *model, + king_inference_attention_role role, + zend_ulong layer, + const char **status +) { + zval *descriptor; + zend_string *name; + zend_string *matched = NULL; + zend_ulong matches = 0; + zend_ulong columns; + zend_ulong rows; + + if (!king_inference_attention_expected_dimensions(model, role, layer, &columns, &rows) + || Z_ISUNDEF(model->tensor_index) + || Z_TYPE(model->tensor_index) != IS_ARRAY) { + if (status != NULL) { + *status = "shape_scan_unavailable"; + } + return NULL; + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL(model->tensor_index), name, descriptor) { + if (name == NULL + || !king_inference_attention_name_hint(name, role, layer) + || !king_inference_attention_descriptor_valid(model, role, layer, descriptor)) { + continue; + } + matches++; + if (matched == NULL) { + matched = zend_string_copy(name); + } + } ZEND_HASH_FOREACH_END(); + + if (matches == 1) { + if (status != NULL) { + *status = "resolved"; + } + return matched; + } + if (matched != NULL) { + zend_string_release(matched); + } + if (status != NULL) { + *status = matches > 1 ? "ambiguous_shape_scan" : "not_found"; + } + return NULL; +} + +static zend_string *king_inference_attention_configured_name( + king_inference_model_object *model, + king_inference_attention_role role, + zend_ulong layer, + const char **source_out, + const char **status_out +) { + zval *pattern = king_inference_array_find( + &model->config, + king_inference_attention_config_fields[role] + ); + zend_string *name; + + if (pattern == NULL) { + return NULL; + } + if (Z_TYPE_P(pattern) != IS_STRING || Z_STRLEN_P(pattern) == 0) { + if (status_out != NULL) { + *status_out = "configured_pattern_invalid"; + } + return NULL; + } + if (source_out != NULL) { + *source_out = king_inference_attention_config_fields[role]; + } + + name = king_inference_attention_layer_pattern_name(Z_STRVAL_P(pattern), layer); + if (name == NULL && status_out != NULL) { + *status_out = "configured_pattern_missing_layer_placeholder"; + } + + return name; +} + +static bool king_inference_attention_configured_pattern_present( + king_inference_model_object *model, + king_inference_attention_role role +) { + return king_inference_array_find( + &model->config, + king_inference_attention_config_fields[role] + ) != NULL; +} + +static zend_result king_inference_resolve_attention_tensor_name( + king_inference_model_object *model, + king_inference_attention_role role, + zend_ulong layer, + zend_string **name_out, + const char **source_out, + const char **status_out +) { + const char *source = NULL; + const char *scan_status = "not_found"; + bool configured_present = king_inference_attention_configured_pattern_present(model, role); + zend_string *configured; + zval *descriptor; + + if (status_out != NULL) { + *status_out = "not_found"; + } + configured = king_inference_attention_configured_name( + model, + role, + layer, + &source, + status_out + ); + + *name_out = NULL; + if (source_out != NULL) { + *source_out = NULL; + } + + if (configured_present && configured == NULL) { + if (status_out != NULL && strcmp(*status_out, "not_found") == 0) { + *status_out = "configured_pattern_invalid"; + } + return FAILURE; + } + + if (configured != NULL) { + descriptor = king_inference_tensor_index_descriptor(model, configured); + if (descriptor == NULL) { + zend_string_release(configured); + if (status_out != NULL) { + *status_out = "configured_tensor_not_found"; + } + return FAILURE; + } + if (!king_inference_attention_descriptor_valid(model, role, layer, descriptor)) { + zend_string_release(configured); + if (status_out != NULL) { + *status_out = "configured_tensor_invalid_shape"; + } + return FAILURE; + } + *name_out = configured; + if (source_out != NULL) { + *source_out = source; + } + if (status_out != NULL) { + *status_out = "resolved"; + } + return SUCCESS; + } + + *name_out = king_inference_attention_known_name(model, role, layer); + if (*name_out != NULL) { + if (source_out != NULL) { + *source_out = "known_pattern"; + } + if (status_out != NULL) { + *status_out = "resolved"; + } + return SUCCESS; + } + + *name_out = king_inference_attention_shape_scan(model, role, layer, &scan_status); + if (*name_out != NULL) { + if (source_out != NULL) { + *source_out = "shape_scan"; + } + if (status_out != NULL) { + *status_out = "resolved"; + } + return SUCCESS; + } + + if (status_out != NULL) { + *status_out = scan_status; + } + return FAILURE; +} + diff --git a/extension/src/inference/tensor/resolvers/attention/components/shape/tensor_attention_runtime_shape.inc b/extension/src/inference/tensor/resolvers/attention/components/shape/tensor_attention_runtime_shape.inc new file mode 100644 index 000000000..446035448 --- /dev/null +++ b/extension/src/inference/tensor/resolvers/attention/components/shape/tensor_attention_runtime_shape.inc @@ -0,0 +1,162 @@ +static bool king_inference_attention_tensor_rows( + king_inference_model_object *model, + zend_string *name, + zend_ulong *rows_out +) { + zval *descriptor; + zval *dimensions; + zend_ulong rank; + zend_ulong rows; + + *rows_out = 0; + if (name == NULL) { + return false; + } + descriptor = king_inference_tensor_index_descriptor(model, name); + dimensions = descriptor != NULL + ? zend_hash_str_find(Z_ARRVAL_P(descriptor), "dimensions", sizeof("dimensions") - 1) + : NULL; + if (descriptor == NULL + || dimensions == NULL + || Z_TYPE_P(dimensions) != IS_ARRAY + || !king_inference_tensor_descriptor_rank(descriptor, &rank) + || rank != 2 + || king_inference_tensor_dimension_at(dimensions, 1, &rows) != SUCCESS + || rows == 0) { + return false; + } + + *rows_out = rows; + return true; +} + +static bool king_inference_attention_refine_runtime_shape_from_tensors_for_layer( + king_inference_model_object *model, + zend_ulong layer, + zend_ulong *heads, + zend_ulong *kv_heads, + zend_ulong *key_length, + zend_ulong *value_length +) { + zend_string *attn_q = NULL; + zend_string *attn_k = NULL; + zend_string *attn_v = NULL; + const char *status = NULL; + zend_ulong query_rows = 0; + zend_ulong key_rows = 0; + zend_ulong value_rows = 0; + zend_ulong derived_key_length; + zend_ulong derived_kv_heads; + + if (*heads == 0 + || king_inference_resolve_attention_tensor_name(model, KING_INFERENCE_ATTENTION_QUERY, layer, &attn_q, NULL, &status) != SUCCESS + || king_inference_resolve_attention_tensor_name(model, KING_INFERENCE_ATTENTION_KEY, layer, &attn_k, NULL, &status) != SUCCESS + || king_inference_resolve_attention_tensor_name(model, KING_INFERENCE_ATTENTION_VALUE, layer, &attn_v, NULL, &status) != SUCCESS + || !king_inference_attention_tensor_rows(model, attn_q, &query_rows) + || !king_inference_attention_tensor_rows(model, attn_k, &key_rows) + || !king_inference_attention_tensor_rows(model, attn_v, &value_rows)) { + if (attn_q != NULL) { + zend_string_release(attn_q); + } + if (attn_k != NULL) { + zend_string_release(attn_k); + } + if (attn_v != NULL) { + zend_string_release(attn_v); + } + return false; + } + + if (query_rows >= *heads && (query_rows % *heads) == 0) { + derived_key_length = query_rows / *heads; + if (derived_key_length > 0) { + *key_length = derived_key_length; + } + } + if (*key_length > 0 && key_rows >= *key_length && (key_rows % *key_length) == 0) { + derived_kv_heads = key_rows / *key_length; + if (derived_kv_heads > 0 && derived_kv_heads <= *heads) { + *kv_heads = derived_kv_heads; + } + } + if (*kv_heads > 0 && value_rows >= *kv_heads && (value_rows % *kv_heads) == 0) { + *value_length = value_rows / *kv_heads; + } + + zend_string_release(attn_q); + zend_string_release(attn_k); + zend_string_release(attn_v); + return true; +} + +static void king_inference_attention_refine_runtime_shape_from_tensors( + king_inference_model_object *model, + zend_ulong *heads, + zend_ulong *kv_heads, + zend_ulong *key_length, + zend_ulong *value_length +) { + king_inference_attention_refine_runtime_shape_from_tensors_for_layer( + model, + 0, + heads, + kv_heads, + key_length, + value_length + ); +} + +static void king_inference_attention_refine_runtime_cache_shape_from_tensors( + king_inference_model_object *model, + zend_ulong layers, + zend_ulong *heads, + zend_ulong *kv_heads, + zend_ulong *key_length, + zend_ulong *value_length +) { + zend_ulong layer; + zend_ulong max_heads = 0; + zend_ulong max_kv_heads = 0; + zend_ulong max_key_length = 0; + zend_ulong max_value_length = 0; + bool found_layer_shape = false; + + for (layer = 0; layer < layers; layer++) { + zend_ulong layer_heads = *heads; + zend_ulong layer_kv_heads = *kv_heads; + zend_ulong layer_key_length = *key_length; + zend_ulong layer_value_length = *value_length; + + if (!king_inference_attention_refine_runtime_shape_from_tensors_for_layer( + model, + layer, + &layer_heads, + &layer_kv_heads, + &layer_key_length, + &layer_value_length + )) { + continue; + } + found_layer_shape = true; + if (layer_heads > max_heads) { + max_heads = layer_heads; + } + if (layer_kv_heads > max_kv_heads) { + max_kv_heads = layer_kv_heads; + } + if (layer_key_length > max_key_length) { + max_key_length = layer_key_length; + } + if (layer_value_length > max_value_length) { + max_value_length = layer_value_length; + } + } + + if (found_layer_shape) { + *heads = max_heads; + *kv_heads = max_kv_heads; + *key_length = max_key_length; + *value_length = max_value_length; + } +} + diff --git a/extension/src/inference/tensor/resolvers/attention/tensor_attention_resolver.inc b/extension/src/inference/tensor/resolvers/attention/tensor_attention_resolver.inc new file mode 100644 index 000000000..2ed693526 --- /dev/null +++ b/extension/src/inference/tensor/resolvers/attention/tensor_attention_resolver.inc @@ -0,0 +1,5 @@ +#include "components/metadata/tensor_attention_patterns.inc" +#include "components/shape/tensor_attention_dimensions.inc" +#include "components/shape/tensor_attention_resolution.inc" +#include "components/shape/tensor_attention_runtime_shape.inc" +#include "components/metadata/tensor_attention_info.inc" diff --git a/extension/src/inference/tensor/resolvers/ffn/tensor_ffn_resolver.inc b/extension/src/inference/tensor/resolvers/ffn/tensor_ffn_resolver.inc new file mode 100644 index 000000000..400026ad6 --- /dev/null +++ b/extension/src/inference/tensor/resolvers/ffn/tensor_ffn_resolver.inc @@ -0,0 +1,553 @@ +typedef enum _king_inference_ffn_role { + KING_INFERENCE_FFN_GATE = 0, + KING_INFERENCE_FFN_UP = 1, + KING_INFERENCE_FFN_DOWN = 2 +} king_inference_ffn_role; + +static const char *king_inference_ffn_role_keys[] = { + "gate", + "up", + "down" +}; + +static const char *king_inference_ffn_config_fields[] = { + "ffn_gate_tensor_pattern", + "ffn_up_tensor_pattern", + "ffn_down_tensor_pattern" +}; + +static const char *king_inference_ffn_gate_patterns[] = { + "blk.{layer}.ffn_gate.weight", + "blk.{layer}.feed_forward.w1.weight", + "model.layers.{layer}.mlp.gate_proj.weight", + "layers.{layer}.mlp.gate_proj.weight", + "decoder.layers.{layer}.mlp.gate_proj.weight", + "model.layers.{layer}.feed_forward.w1.weight", + "layers.{layer}.feed_forward.w1.weight", + "transformer.h.{layer}.mlp.gate_proj.weight", + "transformer.blocks.{layer}.mlp.gate_proj.weight", + NULL +}; + +static const char *king_inference_ffn_up_patterns[] = { + "blk.{layer}.ffn_up.weight", + "blk.{layer}.feed_forward.w3.weight", + "model.layers.{layer}.mlp.up_proj.weight", + "layers.{layer}.mlp.up_proj.weight", + "decoder.layers.{layer}.mlp.up_proj.weight", + "model.layers.{layer}.feed_forward.w3.weight", + "layers.{layer}.feed_forward.w3.weight", + "transformer.h.{layer}.mlp.up_proj.weight", + "transformer.blocks.{layer}.mlp.up_proj.weight", + NULL +}; + +static const char *king_inference_ffn_down_patterns[] = { + "blk.{layer}.ffn_down.weight", + "blk.{layer}.feed_forward.w2.weight", + "model.layers.{layer}.mlp.down_proj.weight", + "layers.{layer}.mlp.down_proj.weight", + "decoder.layers.{layer}.mlp.down_proj.weight", + "model.layers.{layer}.feed_forward.w2.weight", + "layers.{layer}.feed_forward.w2.weight", + "transformer.h.{layer}.mlp.down_proj.weight", + "transformer.blocks.{layer}.mlp.down_proj.weight", + NULL +}; + +static const char **king_inference_ffn_role_patterns(king_inference_ffn_role role) +{ + switch (role) { + case KING_INFERENCE_FFN_GATE: + return king_inference_ffn_gate_patterns; + case KING_INFERENCE_FFN_UP: + return king_inference_ffn_up_patterns; + case KING_INFERENCE_FFN_DOWN: + return king_inference_ffn_down_patterns; + } + + return king_inference_ffn_gate_patterns; +} + +static zend_string *king_inference_ffn_layer_pattern_name( + const char *pattern, + zend_ulong layer +) { + const char *placeholder = strstr(pattern, "{layer}"); + const char *suffix; + char layer_buf[32]; + int layer_len; + size_t prefix_len; + size_t suffix_len; + size_t total_len; + zend_string *name; + + if (placeholder == NULL) { + return NULL; + } + + layer_len = snprintf(layer_buf, sizeof(layer_buf), "%lu", (unsigned long) layer); + if (layer_len <= 0 || (size_t) layer_len >= sizeof(layer_buf)) { + return NULL; + } + + prefix_len = (size_t) (placeholder - pattern); + suffix = placeholder + sizeof("{layer}") - 1; + suffix_len = strlen(suffix); + total_len = prefix_len + (size_t) layer_len + suffix_len; + name = zend_string_alloc(total_len, 0); + memcpy(ZSTR_VAL(name), pattern, prefix_len); + memcpy(ZSTR_VAL(name) + prefix_len, layer_buf, (size_t) layer_len); + memcpy(ZSTR_VAL(name) + prefix_len + (size_t) layer_len, suffix, suffix_len); + ZSTR_VAL(name)[total_len] = '\0'; + + return name; +} + +static bool king_inference_ffn_expected_dimensions( + king_inference_model_object *model, + king_inference_ffn_role role, + zend_ulong *columns_out, + zend_ulong *rows_out +) { + zend_ulong embedding = model->gguf.architecture_params[KING_INFERENCE_ARCH_EMBEDDING_LENGTH]; + zend_ulong feed_forward = model->gguf.architecture_params[KING_INFERENCE_ARCH_FEED_FORWARD_LENGTH]; + + *columns_out = 0; + *rows_out = 0; + if (embedding == 0 || feed_forward == 0) { + return false; + } + + if (role == KING_INFERENCE_FFN_DOWN) { + *columns_out = feed_forward; + *rows_out = embedding; + } else { + *columns_out = embedding; + *rows_out = feed_forward; + } + + return true; +} + +static bool king_inference_ffn_descriptor_valid( + king_inference_model_object *model, + king_inference_ffn_role role, + zval *descriptor +) { + zend_ulong rank; + zend_ulong columns; + zend_ulong rows; + zend_ulong expected_columns; + zend_ulong expected_rows; + + if (!king_inference_ffn_expected_dimensions(model, role, &expected_columns, &expected_rows) + || !king_inference_tensor_descriptor_rank(descriptor, &rank) + || rank != 2 + || !king_inference_tensor_descriptor_dimension(descriptor, 0, &columns) + || !king_inference_tensor_descriptor_dimension(descriptor, 1, &rows)) { + return false; + } + + return columns == expected_columns && rows == expected_rows; +} + +static bool king_inference_ffn_name_matches_layer(zend_string *name, zend_ulong layer) +{ + char token[64]; + + snprintf(token, sizeof(token), "blk.%lu.", (unsigned long) layer); + if (king_inference_tensor_name_contains(name, token)) { + return true; + } + snprintf(token, sizeof(token), "layers.%lu.", (unsigned long) layer); + if (king_inference_tensor_name_contains(name, token)) { + return true; + } + snprintf(token, sizeof(token), "blocks.%lu.", (unsigned long) layer); + if (king_inference_tensor_name_contains(name, token)) { + return true; + } + snprintf(token, sizeof(token), "h.%lu.", (unsigned long) layer); + return king_inference_tensor_name_contains(name, token); +} + +static bool king_inference_ffn_name_hint( + zend_string *name, + king_inference_ffn_role role, + zend_ulong layer +) { + if (!king_inference_ffn_name_matches_layer(name, layer) + || king_inference_tensor_name_contains(name, "norm") + || king_inference_tensor_name_contains(name, "attn") + || king_inference_tensor_name_contains(name, "attention") + || king_inference_tensor_name_contains(name, "q_proj") + || king_inference_tensor_name_contains(name, "k_proj") + || king_inference_tensor_name_contains(name, "v_proj") + || king_inference_tensor_name_contains(name, "o_proj") + || king_inference_tensor_name_contains(name, "token") + || king_inference_tensor_name_contains(name, "embed") + || king_inference_tensor_name_contains(name, "lm_head") + || king_inference_tensor_name_contains(name, "output_norm")) { + return false; + } + + switch (role) { + case KING_INFERENCE_FFN_GATE: + return king_inference_tensor_name_contains(name, "ffn_gate") + || king_inference_tensor_name_contains(name, "gate_proj") + || king_inference_tensor_name_contains(name, "feed_forward.w1") + || king_inference_tensor_name_contains(name, "mlp.gate"); + case KING_INFERENCE_FFN_UP: + return king_inference_tensor_name_contains(name, "ffn_up") + || king_inference_tensor_name_contains(name, "up_proj") + || king_inference_tensor_name_contains(name, "feed_forward.w3") + || king_inference_tensor_name_contains(name, "mlp.up"); + case KING_INFERENCE_FFN_DOWN: + return king_inference_tensor_name_contains(name, "ffn_down") + || king_inference_tensor_name_contains(name, "down_proj") + || king_inference_tensor_name_contains(name, "feed_forward.w2") + || king_inference_tensor_name_contains(name, "mlp.down"); + } + + return false; +} + +static zend_string *king_inference_ffn_known_name( + king_inference_model_object *model, + king_inference_ffn_role role, + zend_ulong layer +) { + const char **patterns = king_inference_ffn_role_patterns(role); + size_t i; + + if (Z_ISUNDEF(model->tensor_index) || Z_TYPE(model->tensor_index) != IS_ARRAY) { + return NULL; + } + + for (i = 0; patterns[i] != NULL; i++) { + zend_string *name = king_inference_ffn_layer_pattern_name(patterns[i], layer); + zval *descriptor; + + if (name == NULL) { + continue; + } + descriptor = king_inference_tensor_index_descriptor(model, name); + if (descriptor != NULL && king_inference_ffn_descriptor_valid(model, role, descriptor)) { + return name; + } + zend_string_release(name); + } + + return NULL; +} + +static zend_string *king_inference_ffn_shape_scan( + king_inference_model_object *model, + king_inference_ffn_role role, + zend_ulong layer, + const char **status +) { + zval *descriptor; + zend_string *name; + zend_string *matched = NULL; + zend_ulong matches = 0; + zend_ulong columns; + zend_ulong rows; + + if (!king_inference_ffn_expected_dimensions(model, role, &columns, &rows) + || Z_ISUNDEF(model->tensor_index) + || Z_TYPE(model->tensor_index) != IS_ARRAY) { + if (status != NULL) { + *status = "shape_scan_unavailable"; + } + return NULL; + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL(model->tensor_index), name, descriptor) { + if (name == NULL + || !king_inference_ffn_name_hint(name, role, layer) + || !king_inference_ffn_descriptor_valid(model, role, descriptor)) { + continue; + } + matches++; + if (matched == NULL) { + matched = zend_string_copy(name); + } + } ZEND_HASH_FOREACH_END(); + + if (matches == 1) { + if (status != NULL) { + *status = "resolved"; + } + return matched; + } + if (matched != NULL) { + zend_string_release(matched); + } + if (status != NULL) { + *status = matches > 1 ? "ambiguous_shape_scan" : "not_found"; + } + return NULL; +} + +static bool king_inference_ffn_configured_pattern_present( + king_inference_model_object *model, + king_inference_ffn_role role +) { + return king_inference_array_find( + &model->config, + king_inference_ffn_config_fields[role] + ) != NULL; +} + +static zend_string *king_inference_ffn_configured_name( + king_inference_model_object *model, + king_inference_ffn_role role, + zend_ulong layer, + const char **source_out, + const char **status_out +) { + zval *pattern = king_inference_array_find( + &model->config, + king_inference_ffn_config_fields[role] + ); + zend_string *name; + + if (pattern == NULL) { + return NULL; + } + if (Z_TYPE_P(pattern) != IS_STRING || Z_STRLEN_P(pattern) == 0) { + if (status_out != NULL) { + *status_out = "configured_pattern_invalid"; + } + return NULL; + } + if (source_out != NULL) { + *source_out = king_inference_ffn_config_fields[role]; + } + + name = king_inference_ffn_layer_pattern_name(Z_STRVAL_P(pattern), layer); + if (name == NULL && status_out != NULL) { + *status_out = "configured_pattern_missing_layer_placeholder"; + } + + return name; +} + +static zend_result king_inference_resolve_ffn_tensor_name( + king_inference_model_object *model, + king_inference_ffn_role role, + zend_ulong layer, + zend_string **name_out, + const char **source_out, + const char **status_out +) { + const char *source = NULL; + const char *scan_status = "not_found"; + bool configured_present = king_inference_ffn_configured_pattern_present(model, role); + zend_string *configured; + zval *descriptor; + + *name_out = NULL; + if (source_out != NULL) { + *source_out = NULL; + } + if (status_out != NULL) { + *status_out = "not_found"; + } + + configured = king_inference_ffn_configured_name(model, role, layer, &source, status_out); + if (configured_present && configured == NULL) { + if (status_out != NULL && strcmp(*status_out, "not_found") == 0) { + *status_out = "configured_pattern_invalid"; + } + return FAILURE; + } + + if (configured != NULL) { + descriptor = king_inference_tensor_index_descriptor(model, configured); + if (descriptor == NULL) { + zend_string_release(configured); + if (status_out != NULL) { + *status_out = "configured_tensor_not_found"; + } + return FAILURE; + } + if (!king_inference_ffn_descriptor_valid(model, role, descriptor)) { + zend_string_release(configured); + if (status_out != NULL) { + *status_out = "configured_tensor_invalid_shape"; + } + return FAILURE; + } + *name_out = configured; + if (source_out != NULL) { + *source_out = source; + } + if (status_out != NULL) { + *status_out = "resolved"; + } + return SUCCESS; + } + + *name_out = king_inference_ffn_known_name(model, role, layer); + if (*name_out != NULL) { + if (source_out != NULL) { + *source_out = "known_pattern"; + } + if (status_out != NULL) { + *status_out = "resolved"; + } + return SUCCESS; + } + + *name_out = king_inference_ffn_shape_scan(model, role, layer, &scan_status); + if (*name_out != NULL) { + if (source_out != NULL) { + *source_out = "shape_scan"; + } + if (status_out != NULL) { + *status_out = "resolved"; + } + return SUCCESS; + } + + if (status_out != NULL) { + *status_out = scan_status; + } + return FAILURE; +} + +static void king_inference_add_ffn_patterns(zval *ffn) +{ + zval known_patterns; + int role; + + array_init(&known_patterns); + for (role = KING_INFERENCE_FFN_GATE; role <= KING_INFERENCE_FFN_DOWN; role++) { + zval patterns; + const char **known = king_inference_ffn_role_patterns((king_inference_ffn_role) role); + size_t i; + + array_init(&patterns); + for (i = 0; known[i] != NULL; i++) { + add_next_index_string(&patterns, known[i]); + } + add_assoc_zval(&known_patterns, king_inference_ffn_role_keys[role], &patterns); + } + add_assoc_zval(ffn, "known_patterns", &known_patterns); +} + +static void king_inference_add_ffn_config_fields(zval *ffn) +{ + zval fields; + int role; + + array_init(&fields); + for (role = KING_INFERENCE_FFN_GATE; role <= KING_INFERENCE_FFN_DOWN; role++) { + add_assoc_string(&fields, king_inference_ffn_role_keys[role], king_inference_ffn_config_fields[role]); + } + add_assoc_zval(ffn, "configured_pattern_fields", &fields); +} + +static bool king_inference_add_ffn_role_entry( + king_inference_model_object *model, + zval *layer_entry, + king_inference_ffn_role role, + zend_ulong layer +) { + zend_string *name = NULL; + const char *source = NULL; + const char *status = "not_found"; + zend_ulong expected_columns = 0; + zend_ulong expected_rows = 0; + zend_result result; + zval entry; + + result = king_inference_resolve_ffn_tensor_name( + model, + role, + layer, + &name, + &source, + &status + ); + king_inference_ffn_expected_dimensions(model, role, &expected_columns, &expected_rows); + + array_init(&entry); + add_assoc_bool(&entry, "resolved", result == SUCCESS && name != NULL); + add_assoc_string(&entry, "status", status != NULL ? status : "not_found"); + add_assoc_long(&entry, "expected_columns", (zend_long) expected_columns); + add_assoc_long(&entry, "expected_rows", (zend_long) expected_rows); + if (source != NULL) { + add_assoc_string(&entry, "source", source); + } + if (name != NULL) { + add_assoc_str(&entry, "name", name); + } + add_assoc_zval(layer_entry, king_inference_ffn_role_keys[role], &entry); + + return result == SUCCESS && name != NULL; +} + +static void king_inference_add_ffn_resolution_info( + king_inference_model_object *model, + zval *resolved +) { + zend_ulong block_count = model->gguf.architecture_params[KING_INFERENCE_ARCH_BLOCK_COUNT]; + zend_ulong embedding = model->gguf.architecture_params[KING_INFERENCE_ARCH_EMBEDDING_LENGTH]; + zend_ulong feed_forward = model->gguf.architecture_params[KING_INFERENCE_ARCH_FEED_FORWARD_LENGTH]; + zend_ulong layer; + zend_ulong resolved_layers = 0; + zend_ulong resolved_tensors = 0; + zval ffn; + zval layers; + + array_init(&ffn); + array_init(&layers); + + for (layer = 0; layer < block_count; layer++) { + zval layer_entry; + bool layer_complete = true; + int role; + + array_init(&layer_entry); + add_assoc_long(&layer_entry, "index", (zend_long) layer); + for (role = KING_INFERENCE_FFN_GATE; role <= KING_INFERENCE_FFN_DOWN; role++) { + bool role_resolved = king_inference_add_ffn_role_entry( + model, + &layer_entry, + (king_inference_ffn_role) role, + layer + ); + if (role_resolved) { + resolved_tensors++; + } else { + layer_complete = false; + } + } + add_assoc_bool(&layer_entry, "resolved", layer_complete); + if (layer_complete) { + resolved_layers++; + } + add_next_index_zval(&layers, &layer_entry); + } + + add_assoc_bool(&ffn, "resolved", block_count > 0 && resolved_layers == block_count); + add_assoc_string( + &ffn, + "status", + block_count == 0 ? "shape_metadata_unavailable" : + (resolved_layers == block_count ? "resolved" : "partial") + ); + add_assoc_long(&ffn, "block_count", (zend_long) block_count); + add_assoc_long(&ffn, "resolved_layers", (zend_long) resolved_layers); + add_assoc_long(&ffn, "resolved_tensors", (zend_long) resolved_tensors); + add_assoc_long(&ffn, "embedding_length", (zend_long) embedding); + add_assoc_long(&ffn, "feed_forward_length", (zend_long) feed_forward); + king_inference_add_ffn_config_fields(&ffn); + king_inference_add_ffn_patterns(&ffn); + add_assoc_zval(&ffn, "layers", &layers); + add_assoc_zval(resolved, "ffn", &ffn); +} diff --git a/extension/src/inference/tensor/resolvers/rms_norm/tensor_rms_norm_resolver.inc b/extension/src/inference/tensor/resolvers/rms_norm/tensor_rms_norm_resolver.inc new file mode 100644 index 000000000..a7860fb4e --- /dev/null +++ b/extension/src/inference/tensor/resolvers/rms_norm/tensor_rms_norm_resolver.inc @@ -0,0 +1,749 @@ +typedef enum _king_inference_rms_norm_role { + KING_INFERENCE_RMS_NORM_ATTENTION = 0, + KING_INFERENCE_RMS_NORM_FEED_FORWARD = 1 +} king_inference_rms_norm_role; + +static const char *king_inference_rms_norm_role_keys[] = { + "attention", + "feed_forward" +}; + +static const char *king_inference_rms_norm_config_fields[] = { + "rms_norm_attention_tensor_pattern", + "rms_norm_ffn_tensor_pattern" +}; + +static const char *king_inference_rms_norm_attention_patterns[] = { + "blk.{layer}.attn_norm.weight", + "blk.{layer}.attention_norm.weight", + "model.layers.{layer}.input_layernorm.weight", + "layers.{layer}.input_layernorm.weight", + "decoder.layers.{layer}.input_layernorm.weight", + "model.layers.{layer}.self_attn_layer_norm.weight", + "decoder.layers.{layer}.self_attn_layer_norm.weight", + "transformer.h.{layer}.ln_1.weight", + "transformer.blocks.{layer}.ln_1.weight", + NULL +}; + +static const char *king_inference_rms_norm_feed_forward_patterns[] = { + "blk.{layer}.ffn_norm.weight", + "blk.{layer}.post_attention_norm.weight", + "model.layers.{layer}.post_attention_layernorm.weight", + "layers.{layer}.post_attention_layernorm.weight", + "decoder.layers.{layer}.post_attention_layernorm.weight", + "model.layers.{layer}.mlp_layer_norm.weight", + "decoder.layers.{layer}.final_layer_norm.weight", + "transformer.h.{layer}.ln_2.weight", + "transformer.blocks.{layer}.ln_2.weight", + NULL +}; + +static const char *king_inference_rms_norm_final_names[] = { + "output_norm.weight", + "model.norm.weight", + "norm.weight", + "decoder.norm.weight", + "model.decoder.norm.weight", + "language_model.model.norm.weight", + "final_layernorm.weight", + "transformer.ln_f.weight", + NULL +}; + +static const char **king_inference_rms_norm_role_patterns(king_inference_rms_norm_role role) +{ + switch (role) { + case KING_INFERENCE_RMS_NORM_ATTENTION: + return king_inference_rms_norm_attention_patterns; + case KING_INFERENCE_RMS_NORM_FEED_FORWARD: + return king_inference_rms_norm_feed_forward_patterns; + } + + return king_inference_rms_norm_attention_patterns; +} + +static zend_string *king_inference_rms_norm_layer_pattern_name( + const char *pattern, + zend_ulong layer +) { + const char *placeholder = strstr(pattern, "{layer}"); + const char *suffix; + char layer_buf[32]; + int layer_len; + size_t prefix_len; + size_t suffix_len; + size_t total_len; + zend_string *name; + + if (placeholder == NULL) { + return NULL; + } + + layer_len = snprintf(layer_buf, sizeof(layer_buf), "%lu", (unsigned long) layer); + if (layer_len <= 0 || (size_t) layer_len >= sizeof(layer_buf)) { + return NULL; + } + + prefix_len = (size_t) (placeholder - pattern); + suffix = placeholder + sizeof("{layer}") - 1; + suffix_len = strlen(suffix); + total_len = prefix_len + (size_t) layer_len + suffix_len; + name = zend_string_alloc(total_len, 0); + memcpy(ZSTR_VAL(name), pattern, prefix_len); + memcpy(ZSTR_VAL(name) + prefix_len, layer_buf, (size_t) layer_len); + memcpy(ZSTR_VAL(name) + prefix_len + (size_t) layer_len, suffix, suffix_len); + ZSTR_VAL(name)[total_len] = '\0'; + + return name; +} + +static zend_ulong king_inference_rms_norm_expected_width(king_inference_model_object *model) +{ + return model->gguf.architecture_params[KING_INFERENCE_ARCH_EMBEDDING_LENGTH]; +} + +static bool king_inference_rms_norm_descriptor_valid( + king_inference_model_object *model, + zval *descriptor +) { + zend_ulong rank; + zend_ulong width; + zend_ulong expected = king_inference_rms_norm_expected_width(model); + + if (expected == 0 + || !king_inference_tensor_descriptor_rank(descriptor, &rank) + || rank != 1 + || !king_inference_tensor_descriptor_dimension(descriptor, 0, &width)) { + return false; + } + + return width == expected; +} + +static bool king_inference_rms_norm_name_matches_layer(zend_string *name, zend_ulong layer) +{ + char token[64]; + + snprintf(token, sizeof(token), "blk.%lu.", (unsigned long) layer); + if (king_inference_tensor_name_contains(name, token)) { + return true; + } + snprintf(token, sizeof(token), "layers.%lu.", (unsigned long) layer); + if (king_inference_tensor_name_contains(name, token)) { + return true; + } + snprintf(token, sizeof(token), "blocks.%lu.", (unsigned long) layer); + if (king_inference_tensor_name_contains(name, token)) { + return true; + } + snprintf(token, sizeof(token), "h.%lu.", (unsigned long) layer); + return king_inference_tensor_name_contains(name, token); +} + +static bool king_inference_rms_norm_layer_name_hint( + zend_string *name, + king_inference_rms_norm_role role, + zend_ulong layer +) { + if (!king_inference_rms_norm_name_matches_layer(name, layer) + || !king_inference_tensor_name_contains(name, "norm") + || king_inference_tensor_name_contains(name, "q_proj") + || king_inference_tensor_name_contains(name, "k_proj") + || king_inference_tensor_name_contains(name, "v_proj") + || king_inference_tensor_name_contains(name, "o_proj") + || king_inference_tensor_name_contains(name, "attn_q") + || king_inference_tensor_name_contains(name, "attn_k") + || king_inference_tensor_name_contains(name, "attn_v") + || king_inference_tensor_name_contains(name, "attn_o") + || king_inference_tensor_name_contains(name, "token") + || king_inference_tensor_name_contains(name, "embed") + || king_inference_tensor_name_contains(name, "lm_head")) { + return false; + } + + switch (role) { + case KING_INFERENCE_RMS_NORM_ATTENTION: + return king_inference_tensor_name_contains(name, "attn_norm") + || king_inference_tensor_name_contains(name, "attention_norm") + || king_inference_tensor_name_contains(name, "input_layernorm") + || king_inference_tensor_name_contains(name, "self_attn_layer_norm") + || king_inference_tensor_name_contains(name, "ln_1"); + case KING_INFERENCE_RMS_NORM_FEED_FORWARD: + return king_inference_tensor_name_contains(name, "ffn_norm") + || king_inference_tensor_name_contains(name, "post_attention_norm") + || king_inference_tensor_name_contains(name, "post_attention_layernorm") + || king_inference_tensor_name_contains(name, "mlp_layer_norm") + || king_inference_tensor_name_contains(name, "final_layer_norm") + || king_inference_tensor_name_contains(name, "ln_2"); + } + + return false; +} + +static bool king_inference_rms_norm_final_name_hint(zend_string *name) +{ + if (king_inference_tensor_name_contains(name, "blk.") + || king_inference_tensor_name_contains(name, "layers.") + || king_inference_tensor_name_contains(name, "blocks.") + || king_inference_tensor_name_contains(name, "attn") + || king_inference_tensor_name_contains(name, "ffn") + || king_inference_tensor_name_contains(name, "mlp") + || king_inference_tensor_name_contains(name, "token") + || king_inference_tensor_name_contains(name, "embed") + || king_inference_tensor_name_contains(name, "lm_head") + || !king_inference_tensor_name_contains(name, "norm")) { + return false; + } + + return king_inference_tensor_name_contains(name, "output_norm") + || king_inference_tensor_name_contains(name, "model.norm") + || king_inference_tensor_name_contains(name, "decoder.norm") + || king_inference_tensor_name_contains(name, "final_layernorm") + || king_inference_tensor_name_contains(name, "ln_f") + || zend_string_equals_literal(name, "norm.weight"); +} + +static zend_string *king_inference_rms_norm_known_layer_name( + king_inference_model_object *model, + king_inference_rms_norm_role role, + zend_ulong layer +) { + const char **patterns = king_inference_rms_norm_role_patterns(role); + size_t i; + + if (Z_ISUNDEF(model->tensor_index) || Z_TYPE(model->tensor_index) != IS_ARRAY) { + return NULL; + } + + for (i = 0; patterns[i] != NULL; i++) { + zend_string *name = king_inference_rms_norm_layer_pattern_name(patterns[i], layer); + zval *descriptor; + + if (name == NULL) { + continue; + } + descriptor = king_inference_tensor_index_descriptor(model, name); + if (descriptor != NULL && king_inference_rms_norm_descriptor_valid(model, descriptor)) { + return name; + } + zend_string_release(name); + } + + return NULL; +} + +static zend_string *king_inference_rms_norm_known_final_name(king_inference_model_object *model) +{ + size_t i; + + if (Z_ISUNDEF(model->tensor_index) || Z_TYPE(model->tensor_index) != IS_ARRAY) { + return NULL; + } + + for (i = 0; king_inference_rms_norm_final_names[i] != NULL; i++) { + const char *candidate = king_inference_rms_norm_final_names[i]; + zval *descriptor = zend_hash_str_find(Z_ARRVAL(model->tensor_index), candidate, strlen(candidate)); + if (descriptor != NULL && king_inference_rms_norm_descriptor_valid(model, descriptor)) { + return zend_string_init(candidate, strlen(candidate), 0); + } + } + + return NULL; +} + +static zend_string *king_inference_rms_norm_layer_shape_scan( + king_inference_model_object *model, + king_inference_rms_norm_role role, + zend_ulong layer, + const char **status +) { + zval *descriptor; + zend_string *name; + zend_string *matched = NULL; + zend_ulong matches = 0; + + if (king_inference_rms_norm_expected_width(model) == 0 + || Z_ISUNDEF(model->tensor_index) + || Z_TYPE(model->tensor_index) != IS_ARRAY) { + if (status != NULL) { + *status = "shape_scan_unavailable"; + } + return NULL; + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL(model->tensor_index), name, descriptor) { + if (name == NULL + || !king_inference_rms_norm_layer_name_hint(name, role, layer) + || !king_inference_rms_norm_descriptor_valid(model, descriptor)) { + continue; + } + matches++; + if (matched == NULL) { + matched = zend_string_copy(name); + } + } ZEND_HASH_FOREACH_END(); + + if (matches == 1) { + if (status != NULL) { + *status = "resolved"; + } + return matched; + } + if (matched != NULL) { + zend_string_release(matched); + } + if (status != NULL) { + *status = matches > 1 ? "ambiguous_shape_scan" : "not_found"; + } + return NULL; +} + +static zend_string *king_inference_rms_norm_final_shape_scan( + king_inference_model_object *model, + const char **status +) { + zval *descriptor; + zend_string *name; + zend_string *matched = NULL; + zend_ulong matches = 0; + + if (king_inference_rms_norm_expected_width(model) == 0 + || Z_ISUNDEF(model->tensor_index) + || Z_TYPE(model->tensor_index) != IS_ARRAY) { + if (status != NULL) { + *status = "shape_scan_unavailable"; + } + return NULL; + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL(model->tensor_index), name, descriptor) { + if (name == NULL + || !king_inference_rms_norm_final_name_hint(name) + || !king_inference_rms_norm_descriptor_valid(model, descriptor)) { + continue; + } + matches++; + if (matched == NULL) { + matched = zend_string_copy(name); + } + } ZEND_HASH_FOREACH_END(); + + if (matches == 1) { + if (status != NULL) { + *status = "resolved"; + } + return matched; + } + if (matched != NULL) { + zend_string_release(matched); + } + if (status != NULL) { + *status = matches > 1 ? "ambiguous_shape_scan" : "not_found"; + } + return NULL; +} + +static bool king_inference_rms_norm_configured_pattern_present( + king_inference_model_object *model, + king_inference_rms_norm_role role +) { + return king_inference_array_find( + &model->config, + king_inference_rms_norm_config_fields[role] + ) != NULL; +} + +static zend_string *king_inference_rms_norm_configured_layer_name( + king_inference_model_object *model, + king_inference_rms_norm_role role, + zend_ulong layer, + const char **source_out, + const char **status_out +) { + zval *pattern = king_inference_array_find( + &model->config, + king_inference_rms_norm_config_fields[role] + ); + zend_string *name; + + if (pattern == NULL) { + return NULL; + } + if (Z_TYPE_P(pattern) != IS_STRING || Z_STRLEN_P(pattern) == 0) { + if (status_out != NULL) { + *status_out = "configured_pattern_invalid"; + } + return NULL; + } + if (source_out != NULL) { + *source_out = king_inference_rms_norm_config_fields[role]; + } + + name = king_inference_rms_norm_layer_pattern_name(Z_STRVAL_P(pattern), layer); + if (name == NULL && status_out != NULL) { + *status_out = "configured_pattern_missing_layer_placeholder"; + } + + return name; +} + +static zend_string *king_inference_rms_norm_configured_final_name( + king_inference_model_object *model, + const char **source_out +) { + zval *field = king_inference_array_find(&model->config, "rms_norm_final_tensor"); + + if (field == NULL || Z_TYPE_P(field) != IS_STRING || Z_STRLEN_P(field) == 0) { + return NULL; + } + if (source_out != NULL) { + *source_out = "rms_norm_final_tensor"; + } + return zend_string_copy(Z_STR_P(field)); +} + +static zend_result king_inference_resolve_rms_norm_layer_name( + king_inference_model_object *model, + king_inference_rms_norm_role role, + zend_ulong layer, + zend_string **name_out, + const char **source_out, + const char **status_out +) { + const char *source = NULL; + const char *scan_status = "not_found"; + bool configured_present = king_inference_rms_norm_configured_pattern_present(model, role); + zend_string *configured; + zval *descriptor; + + *name_out = NULL; + if (source_out != NULL) { + *source_out = NULL; + } + if (status_out != NULL) { + *status_out = "not_found"; + } + + configured = king_inference_rms_norm_configured_layer_name( + model, + role, + layer, + &source, + status_out + ); + if (configured_present && configured == NULL) { + if (status_out != NULL && strcmp(*status_out, "not_found") == 0) { + *status_out = "configured_pattern_invalid"; + } + return FAILURE; + } + if (configured != NULL) { + descriptor = king_inference_tensor_index_descriptor(model, configured); + if (descriptor == NULL) { + zend_string_release(configured); + if (status_out != NULL) { + *status_out = "configured_tensor_not_found"; + } + return FAILURE; + } + if (!king_inference_rms_norm_descriptor_valid(model, descriptor)) { + zend_string_release(configured); + if (status_out != NULL) { + *status_out = "configured_tensor_invalid_shape"; + } + return FAILURE; + } + *name_out = configured; + if (source_out != NULL) { + *source_out = source; + } + if (status_out != NULL) { + *status_out = "resolved"; + } + return SUCCESS; + } + + *name_out = king_inference_rms_norm_known_layer_name(model, role, layer); + if (*name_out != NULL) { + if (source_out != NULL) { + *source_out = "known_pattern"; + } + if (status_out != NULL) { + *status_out = "resolved"; + } + return SUCCESS; + } + + *name_out = king_inference_rms_norm_layer_shape_scan(model, role, layer, &scan_status); + if (*name_out != NULL) { + if (source_out != NULL) { + *source_out = "shape_scan"; + } + if (status_out != NULL) { + *status_out = "resolved"; + } + return SUCCESS; + } + + if (status_out != NULL) { + *status_out = scan_status; + } + return FAILURE; +} + +static zend_result king_inference_resolve_rms_norm_final_name( + king_inference_model_object *model, + zend_string **name_out, + const char **source_out, + const char **status_out +) { + const char *source = NULL; + const char *scan_status = "not_found"; + zend_string *configured; + zval *descriptor; + + *name_out = NULL; + if (source_out != NULL) { + *source_out = NULL; + } + if (status_out != NULL) { + *status_out = "not_found"; + } + + configured = king_inference_rms_norm_configured_final_name(model, &source); + if (configured != NULL) { + descriptor = king_inference_tensor_index_descriptor(model, configured); + if (descriptor == NULL) { + zend_string_release(configured); + if (status_out != NULL) { + *status_out = "configured_tensor_not_found"; + } + return FAILURE; + } + if (!king_inference_rms_norm_descriptor_valid(model, descriptor)) { + zend_string_release(configured); + if (status_out != NULL) { + *status_out = "configured_tensor_invalid_shape"; + } + return FAILURE; + } + *name_out = configured; + if (source_out != NULL) { + *source_out = source; + } + if (status_out != NULL) { + *status_out = "resolved"; + } + return SUCCESS; + } + + *name_out = king_inference_rms_norm_known_final_name(model); + if (*name_out != NULL) { + if (source_out != NULL) { + *source_out = "known_name"; + } + if (status_out != NULL) { + *status_out = "resolved"; + } + return SUCCESS; + } + + *name_out = king_inference_rms_norm_final_shape_scan(model, &scan_status); + if (*name_out != NULL) { + if (source_out != NULL) { + *source_out = "shape_scan"; + } + if (status_out != NULL) { + *status_out = "resolved"; + } + return SUCCESS; + } + + if (status_out != NULL) { + *status_out = scan_status; + } + return FAILURE; +} + +static void king_inference_add_rms_norm_patterns(zval *rms_norm) +{ + zval patterns; + zval final_names; + int role; + size_t i; + + array_init(&patterns); + for (role = KING_INFERENCE_RMS_NORM_ATTENTION; role <= KING_INFERENCE_RMS_NORM_FEED_FORWARD; role++) { + zval role_patterns; + const char **known = king_inference_rms_norm_role_patterns((king_inference_rms_norm_role) role); + + array_init(&role_patterns); + for (i = 0; known[i] != NULL; i++) { + add_next_index_string(&role_patterns, known[i]); + } + add_assoc_zval(&patterns, king_inference_rms_norm_role_keys[role], &role_patterns); + } + + array_init(&final_names); + for (i = 0; king_inference_rms_norm_final_names[i] != NULL; i++) { + add_next_index_string(&final_names, king_inference_rms_norm_final_names[i]); + } + add_assoc_zval(&patterns, "final", &final_names); + add_assoc_zval(rms_norm, "known_patterns", &patterns); +} + +static void king_inference_add_rms_norm_config_fields(zval *rms_norm) +{ + zval fields; + int role; + + array_init(&fields); + for (role = KING_INFERENCE_RMS_NORM_ATTENTION; role <= KING_INFERENCE_RMS_NORM_FEED_FORWARD; role++) { + add_assoc_string( + &fields, + king_inference_rms_norm_role_keys[role], + king_inference_rms_norm_config_fields[role] + ); + } + add_assoc_string(&fields, "final", "rms_norm_final_tensor"); + add_assoc_zval(rms_norm, "configured_fields", &fields); +} + +static bool king_inference_add_rms_norm_layer_role_entry( + king_inference_model_object *model, + zval *layer_entry, + king_inference_rms_norm_role role, + zend_ulong layer +) { + zend_string *name = NULL; + const char *source = NULL; + const char *status = "not_found"; + zend_result result; + zval entry; + zend_ulong expected_width = king_inference_rms_norm_expected_width(model); + + result = king_inference_resolve_rms_norm_layer_name( + model, + role, + layer, + &name, + &source, + &status + ); + + array_init(&entry); + add_assoc_bool(&entry, "resolved", result == SUCCESS && name != NULL); + add_assoc_string(&entry, "status", status != NULL ? status : "not_found"); + add_assoc_long(&entry, "expected_elements", (zend_long) expected_width); + if (source != NULL) { + add_assoc_string(&entry, "source", source); + } + if (name != NULL) { + add_assoc_str(&entry, "name", name); + } + add_assoc_zval(layer_entry, king_inference_rms_norm_role_keys[role], &entry); + + return result == SUCCESS && name != NULL; +} + +static bool king_inference_add_rms_norm_final_entry( + king_inference_model_object *model, + zval *rms_norm +) { + zend_string *name = NULL; + const char *source = NULL; + const char *status = "not_found"; + zend_result result; + zval entry; + zend_ulong expected_width = king_inference_rms_norm_expected_width(model); + + result = king_inference_resolve_rms_norm_final_name( + model, + &name, + &source, + &status + ); + + array_init(&entry); + add_assoc_bool(&entry, "resolved", result == SUCCESS && name != NULL); + add_assoc_string(&entry, "status", status != NULL ? status : "not_found"); + add_assoc_long(&entry, "expected_elements", (zend_long) expected_width); + if (source != NULL) { + add_assoc_string(&entry, "source", source); + } + if (name != NULL) { + add_assoc_str(&entry, "name", name); + } + add_assoc_zval(rms_norm, "final", &entry); + + return result == SUCCESS && name != NULL; +} + +static void king_inference_add_rms_norm_resolution_info( + king_inference_model_object *model, + zval *resolved +) { + zend_ulong block_count = model->gguf.architecture_params[KING_INFERENCE_ARCH_BLOCK_COUNT]; + zend_ulong layer; + zend_ulong resolved_layers = 0; + zend_ulong resolved_tensors = 0; + bool final_resolved; + zval rms_norm; + zval layers; + + array_init(&rms_norm); + array_init(&layers); + + final_resolved = king_inference_add_rms_norm_final_entry(model, &rms_norm); + if (final_resolved) { + resolved_tensors++; + } + + for (layer = 0; layer < block_count; layer++) { + zval layer_entry; + bool layer_complete = true; + int role; + + array_init(&layer_entry); + add_assoc_long(&layer_entry, "index", (zend_long) layer); + for (role = KING_INFERENCE_RMS_NORM_ATTENTION; role <= KING_INFERENCE_RMS_NORM_FEED_FORWARD; role++) { + bool role_resolved = king_inference_add_rms_norm_layer_role_entry( + model, + &layer_entry, + (king_inference_rms_norm_role) role, + layer + ); + if (role_resolved) { + resolved_tensors++; + } else { + layer_complete = false; + } + } + add_assoc_bool(&layer_entry, "resolved", layer_complete); + if (layer_complete) { + resolved_layers++; + } + add_next_index_zval(&layers, &layer_entry); + } + + add_assoc_bool( + &rms_norm, + "resolved", + block_count > 0 && resolved_layers == block_count && final_resolved + ); + add_assoc_string( + &rms_norm, + "status", + block_count == 0 ? "shape_metadata_unavailable" : + (resolved_layers == block_count && final_resolved ? "resolved" : "partial") + ); + add_assoc_long(&rms_norm, "block_count", (zend_long) block_count); + add_assoc_long(&rms_norm, "resolved_layers", (zend_long) resolved_layers); + add_assoc_long(&rms_norm, "resolved_tensors", (zend_long) resolved_tensors); + add_assoc_long(&rms_norm, "expected_elements", (zend_long) king_inference_rms_norm_expected_width(model)); + king_inference_add_rms_norm_config_fields(&rms_norm); + king_inference_add_rms_norm_patterns(&rms_norm); + add_assoc_zval(&rms_norm, "layers", &layers); + add_assoc_zval(resolved, "rms_norm", &rms_norm); +} diff --git a/extension/src/inference/tensor/resolvers/tensor_resolver.inc b/extension/src/inference/tensor/resolvers/tensor_resolver.inc new file mode 100644 index 000000000..b77c755b3 --- /dev/null +++ b/extension/src/inference/tensor/resolvers/tensor_resolver.inc @@ -0,0 +1,676 @@ +static const char *king_inference_token_embedding_candidates[] = { + "token_embd.weight", + "tok_embeddings.weight", + "model.tok_embeddings.weight", + "model.embed_tokens.weight", + "embed_tokens.weight", + "decoder.embed_tokens.weight", + "model.decoder.embed_tokens.weight", + "language_model.embed_tokens.weight", + "language_model.model.embed_tokens.weight", + "transformer.wte.weight", + "gpt_neox.embed_in.weight", + "word_embeddings.weight", + "embeddings.word_embeddings.weight", + NULL +}; + +static const char *king_inference_output_projection_candidates[] = { + "output.weight", + "model.output.weight", + "lm_head.weight", + "model.lm_head.weight", + "language_model.lm_head.weight", + "language_model.model.lm_head.weight", + "decoder.lm_head.weight", + "model.decoder.lm_head.weight", + "embed_out.weight", + "output_projection.weight", + "model.output_projection.weight", + "transformer.output_layer.weight", + NULL +}; + +static void king_inference_add_attention_resolution_info( + king_inference_model_object *model, + zval *resolved +); + +static void king_inference_add_rms_norm_resolution_info( + king_inference_model_object *model, + zval *resolved +); + +static void king_inference_add_ffn_resolution_info( + king_inference_model_object *model, + zval *resolved +); + +static bool king_inference_tensor_descriptor_dimension( + zval *descriptor, + zend_ulong index, + zend_ulong *value +) { + zval *dimensions; + + if (descriptor == NULL || Z_TYPE_P(descriptor) != IS_ARRAY) { + return false; + } + dimensions = zend_hash_str_find(Z_ARRVAL_P(descriptor), "dimensions", sizeof("dimensions") - 1); + if (dimensions == NULL || Z_TYPE_P(dimensions) != IS_ARRAY) { + return false; + } + + return king_inference_tensor_dimension_at(dimensions, index, value) == SUCCESS; +} + +static bool king_inference_tensor_descriptor_rank(zval *descriptor, zend_ulong *rank) +{ + return king_inference_tensor_descriptor_ulong(descriptor, "rank", sizeof("rank") - 1, rank); +} + +static bool king_inference_token_embedding_descriptor_valid( + king_inference_model_object *model, + zval *descriptor +) { + zend_ulong rank; + zend_ulong columns; + zend_ulong rows; + zend_ulong embedding_length = model->gguf.architecture_params[KING_INFERENCE_ARCH_EMBEDDING_LENGTH]; + zend_ulong tokenizer_rows = model->gguf.tokenizer_token_count; + + if (!king_inference_tensor_descriptor_rank(descriptor, &rank) + || rank != 2 + || !king_inference_tensor_descriptor_dimension(descriptor, 0, &columns) + || !king_inference_tensor_descriptor_dimension(descriptor, 1, &rows) + || columns == 0 + || rows == 0) { + return false; + } + if (embedding_length > 0 && columns != embedding_length) { + return false; + } + if (tokenizer_rows > 0 && rows < tokenizer_rows) { + return false; + } + + return true; +} + +static bool king_inference_tensor_name_contains(zend_string *name, const char *needle) +{ + return php_memnstr( + ZSTR_VAL(name), + needle, + strlen(needle), + ZSTR_VAL(name) + ZSTR_LEN(name) + ) != NULL; +} + +static bool king_inference_token_embedding_name_hint(zend_string *name) +{ + if (king_inference_tensor_name_contains(name, "output") + || king_inference_tensor_name_contains(name, "lm_head") + || king_inference_tensor_name_contains(name, "classifier")) { + return false; + } + + return king_inference_tensor_name_contains(name, "embd") + || king_inference_tensor_name_contains(name, "embed") + || king_inference_tensor_name_contains(name, "embedding") + || king_inference_tensor_name_contains(name, "tok") + || king_inference_tensor_name_contains(name, "wte") + || king_inference_tensor_name_contains(name, "word"); +} + +static bool king_inference_output_projection_descriptor_valid( + king_inference_model_object *model, + zval *descriptor +) { + return king_inference_token_embedding_descriptor_valid(model, descriptor); +} + +static bool king_inference_output_projection_name_hint(zend_string *name) +{ + if (king_inference_tensor_name_contains(name, "output_norm") + || king_inference_tensor_name_contains(name, ".norm") + || king_inference_tensor_name_contains(name, "norm.weight") + || king_inference_tensor_name_contains(name, "token_embd") + || king_inference_tensor_name_contains(name, "tok_embeddings") + || king_inference_tensor_name_contains(name, "embed_tokens") + || king_inference_tensor_name_contains(name, "embedding") + || king_inference_tensor_name_contains(name, "word_embeddings") + || king_inference_tensor_name_contains(name, "wte")) { + return false; + } + + return king_inference_tensor_name_contains(name, "output") + || king_inference_tensor_name_contains(name, "lm_head") + || king_inference_tensor_name_contains(name, "logits") + || king_inference_tensor_name_contains(name, "embed_out") + || king_inference_tensor_name_contains(name, "projection"); +} + +static zend_string *king_inference_tensor_configured_name_from_array( + zval *source, + bool allow_tensor_field, + const char **source_name +) { + zval *field; + + if (source == NULL || Z_TYPE_P(source) != IS_ARRAY) { + return NULL; + } + + if (allow_tensor_field) { + field = king_inference_array_find(source, "tensor"); + if (field != NULL && Z_TYPE_P(field) == IS_STRING && Z_STRLEN_P(field) > 0) { + if (source_name != NULL) { + *source_name = "tensor"; + } + return zend_string_copy(Z_STR_P(field)); + } + } + + field = king_inference_array_find(source, "embedding_tensor"); + if (field != NULL && Z_TYPE_P(field) == IS_STRING && Z_STRLEN_P(field) > 0) { + if (source_name != NULL) { + *source_name = "embedding_tensor"; + } + return zend_string_copy(Z_STR_P(field)); + } + + field = king_inference_array_find(source, "token_embedding_tensor"); + if (field != NULL && Z_TYPE_P(field) == IS_STRING && Z_STRLEN_P(field) > 0) { + if (source_name != NULL) { + *source_name = "token_embedding_tensor"; + } + return zend_string_copy(Z_STR_P(field)); + } + + return NULL; +} + +static zend_string *king_inference_token_embedding_configured_name( + king_inference_model_object *model, + zval *override, + const char **source_name +) { + zend_string *name = king_inference_tensor_configured_name_from_array(override, true, source_name); + + if (name != NULL) { + return name; + } + + return king_inference_tensor_configured_name_from_array(&model->config, false, source_name); +} + +static zend_string *king_inference_output_projection_configured_name_from_array( + zval *source, + bool allow_tensor_field, + const char **source_name +) { + zval *field; + + if (source == NULL || Z_TYPE_P(source) != IS_ARRAY) { + return NULL; + } + + if (allow_tensor_field) { + field = king_inference_array_find(source, "tensor"); + if (field != NULL && Z_TYPE_P(field) == IS_STRING && Z_STRLEN_P(field) > 0) { + if (source_name != NULL) { + *source_name = "tensor"; + } + return zend_string_copy(Z_STR_P(field)); + } + } + + field = king_inference_array_find(source, "output_tensor"); + if (field != NULL && Z_TYPE_P(field) == IS_STRING && Z_STRLEN_P(field) > 0) { + if (source_name != NULL) { + *source_name = "output_tensor"; + } + return zend_string_copy(Z_STR_P(field)); + } + + field = king_inference_array_find(source, "output_projection_tensor"); + if (field != NULL && Z_TYPE_P(field) == IS_STRING && Z_STRLEN_P(field) > 0) { + if (source_name != NULL) { + *source_name = "output_projection_tensor"; + } + return zend_string_copy(Z_STR_P(field)); + } + + field = king_inference_array_find(source, "lm_head_tensor"); + if (field != NULL && Z_TYPE_P(field) == IS_STRING && Z_STRLEN_P(field) > 0) { + if (source_name != NULL) { + *source_name = "lm_head_tensor"; + } + return zend_string_copy(Z_STR_P(field)); + } + + return NULL; +} + +static zend_string *king_inference_output_projection_configured_name( + king_inference_model_object *model, + zval *override, + const char **source_name +) { + zend_string *name = king_inference_output_projection_configured_name_from_array( + override, + true, + source_name + ); + + if (name != NULL) { + return name; + } + + return king_inference_output_projection_configured_name_from_array( + &model->config, + false, + source_name + ); +} + +static zval *king_inference_tensor_index_descriptor( + king_inference_model_object *model, + zend_string *name +) { + if (Z_ISUNDEF(model->tensor_index) || Z_TYPE(model->tensor_index) != IS_ARRAY) { + return NULL; + } + + return zend_hash_find(Z_ARRVAL(model->tensor_index), name); +} + +static zend_string *king_inference_token_embedding_known_name( + king_inference_model_object *model +) { + size_t i; + + if (Z_ISUNDEF(model->tensor_index) || Z_TYPE(model->tensor_index) != IS_ARRAY) { + return NULL; + } + + for (i = 0; king_inference_token_embedding_candidates[i] != NULL; i++) { + const char *candidate = king_inference_token_embedding_candidates[i]; + zval *descriptor = zend_hash_str_find(Z_ARRVAL(model->tensor_index), candidate, strlen(candidate)); + if (descriptor != NULL && king_inference_token_embedding_descriptor_valid(model, descriptor)) { + return zend_string_init(candidate, strlen(candidate), 0); + } + } + + return NULL; +} + +static zend_string *king_inference_token_embedding_shape_scan( + king_inference_model_object *model, + const char **status +) { + zval *descriptor; + zend_string *name; + zend_string *matched = NULL; + zend_ulong matches = 0; + + if (model->gguf.architecture_params[KING_INFERENCE_ARCH_EMBEDDING_LENGTH] == 0 + || model->gguf.tokenizer_token_count == 0 + || Z_ISUNDEF(model->tensor_index) + || Z_TYPE(model->tensor_index) != IS_ARRAY) { + if (status != NULL) { + *status = "shape_scan_unavailable"; + } + return NULL; + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL(model->tensor_index), name, descriptor) { + if (name == NULL + || !king_inference_token_embedding_name_hint(name) + || !king_inference_token_embedding_descriptor_valid(model, descriptor)) { + continue; + } + matches++; + if (matched == NULL) { + matched = zend_string_copy(name); + } + } ZEND_HASH_FOREACH_END(); + + if (matches == 1) { + if (status != NULL) { + *status = "resolved"; + } + return matched; + } + if (matched != NULL) { + zend_string_release(matched); + } + if (status != NULL) { + *status = matches > 1 ? "ambiguous_shape_scan" : "not_found"; + } + return NULL; +} + +static zend_string *king_inference_output_projection_known_name( + king_inference_model_object *model +) { + size_t i; + + if (Z_ISUNDEF(model->tensor_index) || Z_TYPE(model->tensor_index) != IS_ARRAY) { + return NULL; + } + + for (i = 0; king_inference_output_projection_candidates[i] != NULL; i++) { + const char *candidate = king_inference_output_projection_candidates[i]; + zval *descriptor = zend_hash_str_find(Z_ARRVAL(model->tensor_index), candidate, strlen(candidate)); + if (descriptor != NULL && king_inference_output_projection_descriptor_valid(model, descriptor)) { + return zend_string_init(candidate, strlen(candidate), 0); + } + } + + return NULL; +} + +static zend_string *king_inference_output_projection_shape_scan( + king_inference_model_object *model, + const char **status +) { + zval *descriptor; + zend_string *name; + zend_string *matched = NULL; + zend_ulong matches = 0; + + if (model->gguf.architecture_params[KING_INFERENCE_ARCH_EMBEDDING_LENGTH] == 0 + || model->gguf.tokenizer_token_count == 0 + || Z_ISUNDEF(model->tensor_index) + || Z_TYPE(model->tensor_index) != IS_ARRAY) { + if (status != NULL) { + *status = "shape_scan_unavailable"; + } + return NULL; + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL(model->tensor_index), name, descriptor) { + if (name == NULL + || !king_inference_output_projection_name_hint(name) + || !king_inference_output_projection_descriptor_valid(model, descriptor)) { + continue; + } + matches++; + if (matched == NULL) { + matched = zend_string_copy(name); + } + } ZEND_HASH_FOREACH_END(); + + if (matches == 1) { + if (status != NULL) { + *status = "resolved"; + } + return matched; + } + if (matched != NULL) { + zend_string_release(matched); + } + if (status != NULL) { + *status = matches > 1 ? "ambiguous_shape_scan" : "not_found"; + } + return NULL; +} + +static zend_result king_inference_resolve_token_embedding_tensor_name( + king_inference_model_object *model, + zval *override, + zend_string **name_out, + const char **source_out, + const char **status_out +) { + const char *source = NULL; + const char *scan_status = "not_found"; + zend_string *configured = king_inference_token_embedding_configured_name(model, override, &source); + zval *descriptor; + + *name_out = NULL; + if (source_out != NULL) { + *source_out = NULL; + } + if (status_out != NULL) { + *status_out = "not_found"; + } + + if (configured != NULL) { + descriptor = king_inference_tensor_index_descriptor(model, configured); + if (descriptor == NULL) { + zend_string_release(configured); + if (status_out != NULL) { + *status_out = "configured_tensor_not_found"; + } + return FAILURE; + } + if (!king_inference_token_embedding_descriptor_valid(model, descriptor)) { + zend_string_release(configured); + if (status_out != NULL) { + *status_out = "configured_tensor_invalid_shape"; + } + return FAILURE; + } + *name_out = configured; + if (source_out != NULL) { + *source_out = source != NULL ? source : "configured"; + } + if (status_out != NULL) { + *status_out = "resolved"; + } + return SUCCESS; + } + + *name_out = king_inference_token_embedding_known_name(model); + if (*name_out != NULL) { + if (source_out != NULL) { + *source_out = "known_name"; + } + if (status_out != NULL) { + *status_out = "resolved"; + } + return SUCCESS; + } + + *name_out = king_inference_token_embedding_shape_scan(model, &scan_status); + if (*name_out != NULL) { + if (source_out != NULL) { + *source_out = "shape_scan"; + } + if (status_out != NULL) { + *status_out = "resolved"; + } + return SUCCESS; + } + + if (status_out != NULL) { + *status_out = scan_status; + } + return FAILURE; +} + +static zend_result king_inference_resolve_output_projection_tensor_name( + king_inference_model_object *model, + zval *override, + zend_string **name_out, + const char **source_out, + const char **status_out, + bool *tied_embedding_out +) { + const char *source = NULL; + const char *scan_status = "not_found"; + zend_string *configured = king_inference_output_projection_configured_name(model, override, &source); + zval *descriptor; + + *name_out = NULL; + if (source_out != NULL) { + *source_out = NULL; + } + if (status_out != NULL) { + *status_out = "not_found"; + } + if (tied_embedding_out != NULL) { + *tied_embedding_out = false; + } + + if (configured != NULL) { + descriptor = king_inference_tensor_index_descriptor(model, configured); + if (descriptor == NULL) { + zend_string_release(configured); + if (status_out != NULL) { + *status_out = "configured_tensor_not_found"; + } + return FAILURE; + } + if (!king_inference_output_projection_descriptor_valid(model, descriptor)) { + zend_string_release(configured); + if (status_out != NULL) { + *status_out = "configured_tensor_invalid_shape"; + } + return FAILURE; + } + *name_out = configured; + if (source_out != NULL) { + *source_out = source != NULL ? source : "configured"; + } + if (status_out != NULL) { + *status_out = "resolved"; + } + return SUCCESS; + } + + *name_out = king_inference_output_projection_known_name(model); + if (*name_out != NULL) { + if (source_out != NULL) { + *source_out = "known_name"; + } + if (status_out != NULL) { + *status_out = "resolved"; + } + return SUCCESS; + } + + *name_out = king_inference_output_projection_shape_scan(model, &scan_status); + if (*name_out != NULL) { + if (source_out != NULL) { + *source_out = "shape_scan"; + } + if (status_out != NULL) { + *status_out = "resolved"; + } + return SUCCESS; + } + + if (king_inference_resolve_token_embedding_tensor_name( + model, + NULL, + name_out, + NULL, + NULL + ) == SUCCESS && *name_out != NULL) { + if (source_out != NULL) { + *source_out = "tied_token_embedding"; + } + if (status_out != NULL) { + *status_out = "resolved_tied_token_embedding"; + } + if (tied_embedding_out != NULL) { + *tied_embedding_out = true; + } + return SUCCESS; + } + + if (status_out != NULL) { + *status_out = scan_status; + } + return FAILURE; +} + +static void king_inference_add_known_tensor_names( + zval *target, + const char *field_name, + const char **candidates +) { + zval names; + size_t i; + + array_init(&names); + for (i = 0; candidates[i] != NULL; i++) { + add_next_index_string(&names, candidates[i]); + } + add_assoc_zval(target, field_name, &names); +} + +static void king_inference_add_tensor_resolution_info( + king_inference_model_object *model, + zval *target +) { + zend_string *name = NULL; + const char *source = NULL; + const char *status = NULL; + zval resolved; + zval token_embedding; + zval output_projection; + bool tied_embedding = false; + zend_result result = king_inference_resolve_token_embedding_tensor_name( + model, + NULL, + &name, + &source, + &status + ); + + array_init(&token_embedding); + add_assoc_bool(&token_embedding, "resolved", result == SUCCESS && name != NULL); + add_assoc_string(&token_embedding, "status", status != NULL ? status : "not_found"); + if (source != NULL) { + add_assoc_string(&token_embedding, "source", source); + } + if (name != NULL) { + add_assoc_str(&token_embedding, "name", name); + } + king_inference_add_known_tensor_names( + &token_embedding, + "known_names", + king_inference_token_embedding_candidates + ); + + name = NULL; + source = NULL; + status = NULL; + result = king_inference_resolve_output_projection_tensor_name( + model, + NULL, + &name, + &source, + &status, + &tied_embedding + ); + + array_init(&output_projection); + add_assoc_bool(&output_projection, "resolved", result == SUCCESS && name != NULL); + add_assoc_string(&output_projection, "status", status != NULL ? status : "not_found"); + add_assoc_bool(&output_projection, "tied_token_embedding", tied_embedding); + if (source != NULL) { + add_assoc_string(&output_projection, "source", source); + } + if (name != NULL) { + add_assoc_str(&output_projection, "name", name); + } + king_inference_add_known_tensor_names( + &output_projection, + "known_names", + king_inference_output_projection_candidates + ); + + array_init(&resolved); + add_assoc_zval(&resolved, "token_embedding", &token_embedding); + add_assoc_zval(&resolved, "output_projection", &output_projection); + king_inference_add_attention_resolution_info(model, &resolved); + king_inference_add_rms_norm_resolution_info(model, &resolved); + king_inference_add_ffn_resolution_info(model, &resolved); + add_assoc_zval(target, "resolved_tensors", &resolved); +} diff --git a/extension/src/inference/tokenizer/tokenizer.inc b/extension/src/inference/tokenizer/tokenizer.inc new file mode 100644 index 000000000..dcf22c6aa --- /dev/null +++ b/extension/src/inference/tokenizer/tokenizer.inc @@ -0,0 +1,555 @@ +static bool king_inference_tokenizer_uses_sentencepiece(king_inference_model_object *model) +{ + zend_string *tokenizer = model->gguf.tokenizer_model; + + if (tokenizer == NULL) { + return false; + } + + return zend_string_equals_literal(tokenizer, "llama") + || zend_string_equals_literal(tokenizer, "sentencepiece") + || zend_string_equals_literal(tokenizer, "spm") + || zend_string_equals_literal(tokenizer, "gemma"); +} + +static bool king_inference_tokenizer_control_token_at( + king_inference_model_object *model, + const char *input, + size_t length, + size_t offset, + size_t *token_length +) { + static const char *controls[] = { + "<|turn>", + "", + "", + "", + "", + "", + "", + "" + }; + size_t index; + + if (token_length != NULL) { + *token_length = 0; + } + if (Z_TYPE(model->tokenizer_lookup) != IS_ARRAY || offset >= length || input[offset] != '<') { + return false; + } + for (index = 0; index < sizeof(controls) / sizeof(controls[0]); index++) { + size_t control_length = strlen(controls[index]); + + if (offset + control_length <= length + && memcmp(input + offset, controls[index], control_length) == 0 + && zend_hash_str_exists(Z_ARRVAL(model->tokenizer_lookup), controls[index], control_length)) { + if (token_length != NULL) { + *token_length = control_length; + } + return true; + } + } + return false; +} + +static size_t king_inference_tokenizer_next_control_token_offset( + king_inference_model_object *model, + const char *input, + size_t length, + size_t offset +) { + size_t i; + + if (offset >= length) { + return length; + } + for (i = offset; i < length; i++) { + if (king_inference_tokenizer_control_token_at(model, input, length, i, NULL)) { + return i; + } + } + return length; +} + +static zend_string *king_inference_tokenizer_normalize_text( + king_inference_model_object *model, + zend_string *text, + const char **normalization +) { + smart_str normalized = {0}; + const char *input = ZSTR_VAL(text); + size_t length = ZSTR_LEN(text); + size_t i; + + if (!king_inference_tokenizer_uses_sentencepiece(model)) { + *normalization = "raw"; + return zend_string_copy(text); + } + + *normalization = "sentencepiece_space"; + if (length > 0 && !king_inference_tokenizer_control_token_at(model, input, length, 0, NULL) + && !(length >= 3 + && (unsigned char) input[0] == 0xE2 + && (unsigned char) input[1] == 0x96 + && (unsigned char) input[2] == 0x81)) { + smart_str_appendl(&normalized, "\xE2\x96\x81", 3); + } + + for (i = 0; i < length; i++) { + size_t control_length = 0; + + if (king_inference_tokenizer_control_token_at(model, input, length, i, &control_length)) { + smart_str_appendl(&normalized, input + i, control_length); + i += control_length - 1; + } else if (input[i] == ' ') { + smart_str_appendl(&normalized, "\xE2\x96\x81", 3); + } else { + smart_str_appendc(&normalized, input[i]); + } + } + + smart_str_0(&normalized); + return normalized.s != NULL ? normalized.s : zend_string_init("", 0, 0); +} + +static bool king_inference_tokenizer_add_byte_fallback( + king_inference_model_object *model, + unsigned char byte, + zval *token_ids +) { + char fallback[7]; + zval *id; + + snprintf(fallback, sizeof(fallback), "<0x%02X>", byte); + id = zend_hash_str_find(Z_ARRVAL(model->tokenizer_lookup), fallback, strlen(fallback)); + if (id == NULL || Z_TYPE_P(id) != IS_LONG) { + return false; + } + + add_next_index_long(token_ids, Z_LVAL_P(id)); + return true; +} + +static bool king_inference_tokenizer_unigram_ready(king_inference_model_object *model) +{ + return king_inference_tokenizer_uses_sentencepiece(model) + && model->gguf.tokenizer_scores_loaded + && Z_TYPE(model->tokenizer_scores) == IS_ARRAY + && Z_TYPE(model->tokenizer_types) == IS_ARRAY + && zend_hash_num_elements(Z_ARRVAL(model->tokenizer_scores)) > 0; +} + +static bool king_inference_tokenizer_token_type_allowed( + king_inference_model_object *model, + zend_long token_id +) { + zval *type; + + if (!model->gguf.tokenizer_types_loaded || Z_TYPE(model->tokenizer_types) != IS_ARRAY || token_id < 0) { + return true; + } + + type = zend_hash_index_find(Z_ARRVAL(model->tokenizer_types), (zend_ulong) token_id); + if (type == NULL || Z_TYPE_P(type) != IS_LONG) { + return true; + } + + return Z_LVAL_P(type) != 3 && Z_LVAL_P(type) != 5; +} + +static bool king_inference_tokenizer_score_for_token( + king_inference_model_object *model, + zend_long token_id, + double *score +) { + zval *value; + + if (token_id < 0 || Z_TYPE(model->tokenizer_scores) != IS_ARRAY) { + return false; + } + + value = zend_hash_index_find(Z_ARRVAL(model->tokenizer_scores), (zend_ulong) token_id); + if (value == NULL) { + return false; + } + if (Z_TYPE_P(value) == IS_DOUBLE) { + *score = Z_DVAL_P(value); + return isfinite(*score); + } + if (Z_TYPE_P(value) == IS_LONG) { + *score = (double) Z_LVAL_P(value); + return true; + } + + return false; +} + +static zend_result king_inference_tokenizer_greedy_encode_range( + king_inference_model_object *model, + const char *bytes, + size_t start, + size_t segment_length, + zval *token_ids, + zend_long *unknown_count +) { + size_t position = start; + size_t end = start + segment_length; + + while (position < end) { + size_t remaining = end - position; + size_t try_len = model->gguf.tokenizer_max_token_bytes > 0 + ? (size_t) model->gguf.tokenizer_max_token_bytes + : remaining; + bool matched = false; + + if (try_len > remaining) { + try_len = remaining; + } + while (try_len > 0) { + zval *id = zend_hash_str_find( + Z_ARRVAL(model->tokenizer_lookup), + bytes + position, + try_len + ); + + if (id != NULL && Z_TYPE_P(id) == IS_LONG) { + add_next_index_long(token_ids, Z_LVAL_P(id)); + position += try_len; + matched = true; + break; + } + try_len--; + } + + if (matched) { + continue; + } + + if (king_inference_tokenizer_add_byte_fallback(model, (unsigned char) bytes[position], token_ids)) { + position++; + continue; + } + + if (model->gguf.tokenizer_unknown_id >= 0) { + add_next_index_long(token_ids, model->gguf.tokenizer_unknown_id); + (*unknown_count)++; + position++; + continue; + } + + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "King native tokenizer could not encode byte 0x%02X and the model has no unknown token id.", + (unsigned int) (unsigned char) bytes[position] + ); + return FAILURE; + } + + return SUCCESS; +} + +static bool king_inference_tokenizer_unigram_encode_range( + king_inference_model_object *model, + const char *bytes, + size_t start, + size_t segment_length, + zval *token_ids +) { + const double impossible = -1.0e300; + double *scores; + zend_long *prev_token; + size_t *prev_offset; + zend_long *path; + size_t i; + size_t count = 0; + size_t cursor; + size_t max_token_bytes; + bool success = false; + + if (segment_length == 0) { + return true; + } + if (!king_inference_tokenizer_unigram_ready(model) || segment_length > 65536) { + return false; + } + + scores = safe_emalloc(segment_length + 1, sizeof(double), 0); + prev_token = safe_emalloc(segment_length + 1, sizeof(zend_long), 0); + prev_offset = safe_emalloc(segment_length + 1, sizeof(size_t), 0); + path = safe_emalloc(segment_length + 1, sizeof(zend_long), 0); + + for (i = 0; i <= segment_length; i++) { + scores[i] = impossible; + prev_token[i] = -1; + prev_offset[i] = 0; + path[i] = -1; + } + scores[0] = 0.0; + max_token_bytes = model->gguf.tokenizer_max_token_bytes > 0 + ? (size_t) model->gguf.tokenizer_max_token_bytes + : segment_length; + + for (i = 0; i < segment_length; i++) { + size_t max_len; + size_t token_len; + + if (scores[i] <= impossible / 2.0) { + continue; + } + max_len = segment_length - i; + if (max_len > max_token_bytes) { + max_len = max_token_bytes; + } + + for (token_len = 1; token_len <= max_len; token_len++) { + zval *id = zend_hash_str_find( + Z_ARRVAL(model->tokenizer_lookup), + bytes + start + i, + token_len + ); + zend_long token_id; + double token_score; + double candidate; + size_t next = i + token_len; + + if (id == NULL || Z_TYPE_P(id) != IS_LONG) { + continue; + } + + token_id = Z_LVAL_P(id); + if (!king_inference_tokenizer_token_type_allowed(model, token_id) + || !king_inference_tokenizer_score_for_token(model, token_id, &token_score)) { + continue; + } + + candidate = scores[i] + token_score; + if (candidate > scores[next]) { + scores[next] = candidate; + prev_token[next] = token_id; + prev_offset[next] = i; + } + } + } + + if (scores[segment_length] <= impossible / 2.0) { + goto cleanup; + } + + cursor = segment_length; + while (cursor > 0) { + if (prev_token[cursor] < 0 || prev_offset[cursor] >= cursor || count > segment_length) { + goto cleanup; + } + path[count++] = prev_token[cursor]; + cursor = prev_offset[cursor]; + } + + while (count > 0) { + add_next_index_long(token_ids, path[--count]); + } + success = true; + +cleanup: + efree(path); + efree(prev_offset); + efree(prev_token); + efree(scores); + return success; +} + +static void king_inference_tokenizer_status_array( + king_inference_model_object *model, + zval *return_value +) { + array_init(return_value); + add_assoc_bool(return_value, "ready", model->gguf.tokenizer_tokens_loaded + && model->gguf.tokenizer_lookup_loaded + && Z_TYPE(model->tokenizer_tokens) == IS_ARRAY + && Z_TYPE(model->tokenizer_lookup) == IS_ARRAY + && zend_hash_num_elements(Z_ARRVAL(model->tokenizer_tokens)) > 0 + && zend_hash_num_elements(Z_ARRVAL(model->tokenizer_lookup)) > 0); + add_assoc_bool(return_value, "tokens_loaded", model->gguf.tokenizer_tokens_loaded); + add_assoc_bool(return_value, "lookup_loaded", model->gguf.tokenizer_lookup_loaded); + add_assoc_bool(return_value, "scores_loaded", model->gguf.tokenizer_scores_loaded); + add_assoc_bool(return_value, "types_loaded", model->gguf.tokenizer_types_loaded); + add_assoc_bool(return_value, "merges_loaded", model->gguf.tokenizer_merges_loaded); + add_assoc_long(return_value, "token_count", (zend_long) zend_hash_num_elements(Z_ARRVAL(model->tokenizer_tokens))); + add_assoc_long(return_value, "lookup_count", (zend_long) zend_hash_num_elements(Z_ARRVAL(model->tokenizer_lookup))); + add_assoc_long(return_value, "score_count", (zend_long) zend_hash_num_elements(Z_ARRVAL(model->tokenizer_scores))); + add_assoc_long(return_value, "type_count", (zend_long) zend_hash_num_elements(Z_ARRVAL(model->tokenizer_types))); + add_assoc_long(return_value, "merge_count", (zend_long) zend_hash_num_elements(Z_ARRVAL(model->tokenizer_merges))); + add_assoc_bool(return_value, "sentencepiece_unigram_ready", king_inference_tokenizer_unigram_ready(model)); + add_assoc_long(return_value, "unknown_token_id", model->gguf.tokenizer_unknown_id); + add_assoc_bool(return_value, "last_encode_available", model->tokenizer_last_encode_available); + add_assoc_long(return_value, "last_token_count", model->tokenizer_last_token_count); + add_assoc_long(return_value, "last_unknown_count", model->tokenizer_last_unknown_count); + add_assoc_long(return_value, "last_input_bytes", model->tokenizer_last_input_bytes); + add_assoc_long(return_value, "last_normalized_bytes", model->tokenizer_last_normalized_bytes); + add_assoc_bool(return_value, "last_clean", model->tokenizer_last_encode_available + && model->tokenizer_last_unknown_count == 0); +} + +static zend_result king_inference_tokenizer_encode( + king_inference_model_object *model, + zend_string *text, + zval *return_value +) { + zend_string *normalized; + const char *normalization; + const char *bytes; + size_t length; + size_t position = 0; + zend_long unknown_count = 0; + zend_long token_count; + zval token_ids; + + if (Z_TYPE(model->tokenizer_lookup) != IS_ARRAY + || zend_hash_num_elements(Z_ARRVAL(model->tokenizer_lookup)) == 0) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "King native tokenizer has no loaded token lookup for model '%s'.", + ZSTR_VAL(model->name) + ); + return FAILURE; + } + + normalized = king_inference_tokenizer_normalize_text(model, text, &normalization); + bytes = ZSTR_VAL(normalized); + length = ZSTR_LEN(normalized); + + array_init(&token_ids); + while (position < length) { + size_t control_length = 0; + size_t next_control; + size_t remaining; + + if (king_inference_tokenizer_control_token_at(model, bytes, length, position, &control_length)) { + zval *id = zend_hash_str_find(Z_ARRVAL(model->tokenizer_lookup), bytes + position, control_length); + + if (id != NULL && Z_TYPE_P(id) == IS_LONG) { + add_next_index_long(&token_ids, Z_LVAL_P(id)); + position += control_length; + continue; + } + } + + next_control = king_inference_tokenizer_next_control_token_offset(model, bytes, length, position + 1); + remaining = next_control > position ? next_control - position : length - position; + if (!king_inference_tokenizer_unigram_encode_range( + model, + bytes, + position, + remaining, + &token_ids + ) && king_inference_tokenizer_greedy_encode_range( + model, + bytes, + position, + remaining, + &token_ids, + &unknown_count + ) != SUCCESS) { + zend_string_release(normalized); + zval_ptr_dtor(&token_ids); + return FAILURE; + } + position += remaining; + } + + token_count = (zend_long) zend_hash_num_elements(Z_ARRVAL(token_ids)); + model->tokenizer_last_encode_available = true; + model->tokenizer_last_token_count = token_count; + model->tokenizer_last_unknown_count = unknown_count; + model->tokenizer_last_input_bytes = (zend_long) ZSTR_LEN(text); + model->tokenizer_last_normalized_bytes = (zend_long) length; + + array_init(return_value); + add_assoc_long(return_value, "token_count", token_count); + add_assoc_zval(return_value, "tokens", &token_ids); + add_assoc_long(return_value, "unknown_count", unknown_count); + add_assoc_long(return_value, "input_bytes", (zend_long) ZSTR_LEN(text)); + add_assoc_long(return_value, "normalized_bytes", (zend_long) length); + add_assoc_string(return_value, "normalization", normalization); + if (model->gguf.tokenizer_model != NULL) { + add_assoc_str(return_value, "tokenizer_model", zend_string_copy(model->gguf.tokenizer_model)); + } + add_assoc_long(return_value, "bos_token_id", model->gguf.tokenizer_bos_id); + add_assoc_long(return_value, "eos_token_id", model->gguf.tokenizer_eos_id); + + zend_string_release(normalized); + return SUCCESS; +} + +static bool king_inference_tokenizer_hex_nibble(char c, unsigned char *value) +{ + if (c >= '0' && c <= '9') { + *value = (unsigned char) (c - '0'); + return true; + } + if (c >= 'A' && c <= 'F') { + *value = (unsigned char) (c - 'A' + 10); + return true; + } + if (c >= 'a' && c <= 'f') { + *value = (unsigned char) (c - 'a' + 10); + return true; + } + + return false; +} + +static zend_string *king_inference_tokenizer_decode_token( + king_inference_model_object *model, + zend_ulong token_id +) { + zval *token = zend_hash_index_find(Z_ARRVAL(model->tokenizer_tokens), token_id); + smart_str decoded = {0}; + const char *bytes; + size_t length; + size_t i = 0; + + if (token == NULL || Z_TYPE_P(token) != IS_STRING) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "King native tokenizer cannot decode token id %lu for model '%s'.", + token_id, + ZSTR_VAL(model->name) + ); + return NULL; + } + + bytes = Z_STRVAL_P(token); + length = Z_STRLEN_P(token); + if (length == 6 + && bytes[0] == '<' + && bytes[1] == '0' + && bytes[2] == 'x' + && bytes[5] == '>') { + unsigned char high; + unsigned char low; + if (king_inference_tokenizer_hex_nibble(bytes[3], &high) + && king_inference_tokenizer_hex_nibble(bytes[4], &low)) { + char value = (char) ((high << 4) | low); + return zend_string_init(&value, 1, 0); + } + } + + while (i < length) { + if (i + 3 <= length + && (unsigned char) bytes[i] == 0xE2 + && (unsigned char) bytes[i + 1] == 0x96 + && (unsigned char) bytes[i + 2] == 0x81) { + smart_str_appendc(&decoded, ' '); + i += 3; + continue; + } + smart_str_appendc(&decoded, bytes[i]); + i++; + } + + smart_str_0(&decoded); + return decoded.s != NULL ? decoded.s : zend_string_init("", 0, 0); +} diff --git a/extension/src/integration/system_integration.c b/extension/src/integration/system_integration.c index dbb27e05e..b6dd8acec 100644 --- a/extension/src/integration/system_integration.c +++ b/extension/src/integration/system_integration.c @@ -5,7 +5,7 @@ * helpers over that inventory. */ #include "php_king.h" -#include "include/integration/system_integration.h" +#include "integration/system_integration.h" #include #include #include @@ -240,1418 +240,15 @@ static void king_component_info_dtor(zval *zv) } } -static void king_system_reset_drain_state(void) -{ - king_system_shutdown_requested = false; - king_system_recovery_requested = false; - king_system_drain_reason = KING_SYSTEM_DRAIN_REASON_NONE; -} - -static void king_system_reset_coordinator_state_runtime(void) -{ - king_system_coordinator_state_present = false; - king_system_coordinator_state_recovered = false; - king_system_coordinator_state_version = 0; - king_system_coordinator_generation = 0; - king_system_coordinator_created_at = 0; - king_system_coordinator_last_loaded_at = 0; - king_system_coordinator_state_path[0] = '\0'; - king_system_coordinator_state_error[0] = '\0'; - king_system_recovery_source_node_id[0] = '\0'; - king_system_recovery_mode = KING_SYSTEM_RECOVERY_MODE_NONE; - king_system_recovery_plan_id[0] = '\0'; - king_system_recovery_plan_requested_at = 0; - king_system_recovery_plan_window_seconds = 0; - snprintf( - king_system_coordinator_state_status, - sizeof(king_system_coordinator_state_status), - "%s", - "inactive" - ); -} - -static void king_system_apply_default_config(king_system_config_t *config) -{ - memset(config, 0, sizeof(king_system_config_t)); - config->enabled = true; - config->max_concurrent_requests = 1024; - config->health_check_interval_seconds = 5; - config->component_timeout_seconds = 1; - config->enable_cross_component_tracing = true; - config->enable_performance_monitoring = true; - config->enable_auto_scaling = true; - config->enable_circuit_breaker = true; - config->cluster_id[0] = '\0'; - config->node_id[0] = '\0'; - config->state_root_path[0] = '\0'; - strncpy(config->environment, "development", sizeof(config->environment) - 1); - ZVAL_UNDEF(&config->global_configuration); -} - -static const char *king_system_drain_reason_to_string( - king_system_drain_reason_t reason -) -{ - switch (reason) { - case KING_SYSTEM_DRAIN_REASON_COMPONENT_RESTART: - return "component_restart"; - case KING_SYSTEM_DRAIN_REASON_COMPONENT_RECOVERY: - return "component_recovery"; - case KING_SYSTEM_DRAIN_REASON_SYSTEM_SHUTDOWN: - return "system_shutdown"; - case KING_SYSTEM_DRAIN_REASON_NONE: - default: - return "none"; - } -} - -static uint32_t king_system_recovery_window_seconds( - king_system_recovery_mode_t mode -) -{ - switch (mode) { - case KING_SYSTEM_RECOVERY_MODE_NODE: - return 30; - case KING_SYSTEM_RECOVERY_MODE_COMPONENT: - return 15; - case KING_SYSTEM_RECOVERY_MODE_NONE: - default: - return 0; - } -} - -static const char *king_system_recovery_mode_to_string( - king_system_recovery_mode_t mode -) -{ - switch (mode) { - case KING_SYSTEM_RECOVERY_MODE_NODE: - return "node_failure"; - case KING_SYSTEM_RECOVERY_MODE_COMPONENT: - return "component_failure"; - case KING_SYSTEM_RECOVERY_MODE_NONE: - default: - return "none"; - } -} - -static void king_system_start_recovery_plan( - king_system_recovery_mode_t mode, - const char *source_node_id -) -{ - king_system_recovery_mode = mode; - king_system_recovery_plan_requested_at = time(NULL); - king_system_recovery_plan_window_seconds = king_system_recovery_window_seconds(mode); - if (source_node_id != NULL && source_node_id[0] != '\0') { - snprintf( - king_system_recovery_source_node_id, - sizeof(king_system_recovery_source_node_id), - "%s", - source_node_id - ); - } else { - king_system_recovery_source_node_id[0] = '\0'; - } - - snprintf( - king_system_recovery_plan_id, - sizeof(king_system_recovery_plan_id), - "%s:%s:" ZEND_LONG_FMT ":%llu:%llu", - king_system_recovery_mode_to_string(mode), - king_system_recovery_source_node_id[0] != '\0' - ? king_system_recovery_source_node_id - : "local", - (zend_long) king_system_recovery_plan_requested_at, - (unsigned long long) king_system_coordinator_state_version, - (unsigned long long) king_system_coordinator_generation - ); -} - -static const char *king_system_recovery_reason_to_string(void) -{ - return king_system_recovery_mode_to_string(king_system_recovery_mode); -} - -static void king_system_apply_config(king_system_config_t *config, zval *config_arr) -{ - zval *value = NULL; - HashTable *ht = Z_ARRVAL_P(config_arr); - - value = zend_hash_str_find(ht, "environment", strlen("environment")); - if (value && Z_TYPE_P(value) == IS_STRING && Z_STRLEN_P(value) > 0) { - strncpy( - config->environment, - Z_STRVAL_P(value), - sizeof(config->environment) - 1 - ); - } - - value = zend_hash_str_find(ht, "cluster_id", strlen("cluster_id")); - if (value && Z_TYPE_P(value) == IS_STRING && Z_STRLEN_P(value) > 0) { - strncpy( - config->cluster_id, - Z_STRVAL_P(value), - sizeof(config->cluster_id) - 1 - ); - } - - value = zend_hash_str_find(ht, "node_id", strlen("node_id")); - if (value && Z_TYPE_P(value) == IS_STRING && Z_STRLEN_P(value) > 0) { - strncpy( - config->node_id, - Z_STRVAL_P(value), - sizeof(config->node_id) - 1 - ); - } - - value = zend_hash_str_find(ht, "state_root_path", strlen("state_root_path")); - if (value && Z_TYPE_P(value) == IS_STRING && Z_STRLEN_P(value) > 0) { - strncpy( - config->state_root_path, - Z_STRVAL_P(value), - sizeof(config->state_root_path) - 1 - ); - } - - value = zend_hash_str_find( - ht, - "component_timeout_seconds", - strlen("component_timeout_seconds") - ); - if (value && Z_TYPE_P(value) != IS_UNDEF) { - config->component_timeout_seconds = (uint32_t) MAX( - 1, - (uint32_t) zval_get_long(value) - ); - } - - value = zend_hash_str_find(ht, "max_concurrent_requests", strlen("max_concurrent_requests")); - if (value && Z_TYPE_P(value) != IS_UNDEF) { - config->max_concurrent_requests = (uint32_t) MAX( - 1, - (uint32_t) zval_get_long(value) - ); - } - - value = zend_hash_str_find(ht, "health_check_interval_seconds", strlen("health_check_interval_seconds")); - if (value && Z_TYPE_P(value) != IS_UNDEF) { - config->health_check_interval_seconds = (uint32_t) MAX( - 1, - (uint32_t) zval_get_long(value) - ); - } -} - -static void king_system_apply_default_node_identity(king_system_config_t *config) -{ - if (config == NULL || config->state_root_path[0] == '\0') { - return; - } - - if (config->cluster_id[0] == '\0') { - strncpy(config->cluster_id, "local-cluster", sizeof(config->cluster_id) - 1); - } - - if (config->node_id[0] == '\0') { - strncpy(config->node_id, "local-node", sizeof(config->node_id) - 1); - } -} - -static void king_system_build_coordinator_dir_path(char *dest, size_t dest_len) -{ - if (dest == NULL || dest_len == 0) { - return; - } - - if (king_system_runtime_config.state_root_path[0] == '\0') { - dest[0] = '\0'; - return; - } - - snprintf( - dest, - dest_len, - "%s/.king-system", - king_system_runtime_config.state_root_path - ); -} - -static void king_system_build_coordinator_state_path(char *dest, size_t dest_len) -{ - char coordinator_dir[PATH_MAX]; - - if (dest == NULL || dest_len == 0) { - return; - } - - king_system_build_coordinator_dir_path( - coordinator_dir, - sizeof(coordinator_dir) - ); - if (coordinator_dir[0] == '\0') { - dest[0] = '\0'; - return; - } - - snprintf(dest, dest_len, "%s/coordinator.state", coordinator_dir); -} - -static int king_system_ensure_directory_recursive(const char *path) -{ - char current[PATH_MAX]; - size_t len; - size_t idx; - - if (path == NULL || path[0] == '\0') { - return FAILURE; - } - - len = strlen(path); - if (len >= sizeof(current)) { - return FAILURE; - } - - memcpy(current, path, len + 1); - for (idx = 1; idx < len; idx++) { - if (current[idx] != '/') { - continue; - } - - current[idx] = '\0'; - if (mkdir(current, 0755) != 0 && errno != EEXIST) { - return FAILURE; - } - current[idx] = '/'; - } - - if (mkdir(current, 0755) != 0 && errno != EEXIST) { - return FAILURE; - } - - return SUCCESS; -} - -static int king_system_write_coordinator_state( - const char *state_path, - uint64_t version, - uint64_t generation, - time_t created_at, - const char *cluster_id, - const char *active_node_id, - zend_bool clean_shutdown -) -{ - char payload[1024]; - char temp_path[PATH_MAX]; - FILE *file; - int payload_len; - - if (state_path == NULL || state_path[0] == '\0') { - return FAILURE; - } - - payload_len = snprintf( - payload, - sizeof(payload), - "version=%" PRIu64 "\n" - "generation=%" PRIu64 "\n" - "created_at=%" PRIu64 "\n" - "updated_at=%" PRIu64 "\n" - "cluster_id=%s\n" - "active_node_id=%s\n" - "clean_shutdown=%d\n", - version, - generation, - (uint64_t) created_at, - (uint64_t) time(NULL), - cluster_id != NULL ? cluster_id : "", - active_node_id != NULL ? active_node_id : "", - clean_shutdown ? 1 : 0 - ); - if (payload_len < 0 || (size_t) payload_len >= sizeof(payload)) { - return FAILURE; - } - - snprintf(temp_path, sizeof(temp_path), "%s.tmp." ZEND_LONG_FMT, state_path, (zend_long) getpid()); - file = fopen(temp_path, "wb"); - if (file == NULL) { - return FAILURE; - } - - if ( - fwrite(payload, 1, (size_t) payload_len, file) != (size_t) payload_len - || fflush(file) != 0 - || fsync(fileno(file)) != 0 - ) { - fclose(file); - unlink(temp_path); - return FAILURE; - } - - if (fclose(file) != 0) { - unlink(temp_path); - return FAILURE; - } - - if (rename(temp_path, state_path) != 0) { - unlink(temp_path); - return FAILURE; - } - - return SUCCESS; -} - -static int king_system_load_coordinator_state( - const char *state_path, - uint64_t *version_out, - uint64_t *generation_out, - time_t *created_at_out, - char *cluster_id_out, - size_t cluster_id_out_len, - char *active_node_id_out, - size_t active_node_id_out_len, - zend_bool *clean_shutdown_out -) -{ - char buffer[2048]; - char *line; - char *saveptr = NULL; - FILE *file; - uint64_t created_at = 0; - uint64_t generation = 0; - zend_bool clean_shutdown = 0; - uint64_t version = 0; - int has_clean_shutdown = 0; - int has_created_at = 0; - int has_generation = 0; - int has_version = 0; - - if ( - state_path == NULL - || version_out == NULL - || generation_out == NULL - || created_at_out == NULL - || cluster_id_out == NULL - || cluster_id_out_len == 0 - || active_node_id_out == NULL - || active_node_id_out_len == 0 - || clean_shutdown_out == NULL - ) { - return FAILURE; - } - - file = fopen(state_path, "rb"); - if (file == NULL) { - return FAILURE; - } - - memset(buffer, 0, sizeof(buffer)); - if (fread(buffer, 1, sizeof(buffer) - 1, file) == 0 && ferror(file)) { - fclose(file); - return FAILURE; - } - fclose(file); - - cluster_id_out[0] = '\0'; - active_node_id_out[0] = '\0'; - - for (line = strtok_r(buffer, "\n", &saveptr); - line != NULL; - line = strtok_r(NULL, "\n", &saveptr)) { - char *eq = strchr(line, '='); - const char *value = NULL; - size_t value_len = 0; - - if (eq == NULL) { - continue; - } - - *eq = '\0'; - value = eq + 1; - value_len = strlen(value); - if (strcmp(line, "version") == 0) { - version = strtoull(value, NULL, 10); - has_version = 1; - } else if (strcmp(line, "generation") == 0) { - generation = strtoull(value, NULL, 10); - has_generation = 1; - } else if (strcmp(line, "created_at") == 0) { - created_at = strtoull(value, NULL, 10); - has_created_at = 1; - } else if (strcmp(line, "cluster_id") == 0) { - if (value_len >= cluster_id_out_len) { - return FAILURE; - } - memcpy(cluster_id_out, value, value_len + 1); - } else if (strcmp(line, "active_node_id") == 0) { - if (value_len >= active_node_id_out_len) { - return FAILURE; - } - memcpy(active_node_id_out, value, value_len + 1); - } else if (strcmp(line, "clean_shutdown") == 0) { - clean_shutdown = atoi(value) != 0 ? 1 : 0; - has_clean_shutdown = 1; - } - } - - if ( - !has_version - || !has_generation - || !has_created_at - || !has_clean_shutdown - || version == 0 - || generation == 0 - ) { - return FAILURE; - } - - *version_out = version; - *generation_out = generation; - *created_at_out = (time_t) created_at; - *clean_shutdown_out = clean_shutdown; - return SUCCESS; -} - -static int king_system_initialize_coordinator_state(void) -{ - char coordinator_dir[PATH_MAX]; - char loaded_cluster_id[64]; - char loaded_node_id[64]; - char state_path[PATH_MAX]; - time_t created_at; - time_t now; - uint64_t generation; - uint64_t version; - zend_bool clean_shutdown = 0; - - king_system_reset_coordinator_state_runtime(); - - if (king_system_runtime_config.state_root_path[0] == '\0') { - return SUCCESS; - } - - king_system_build_coordinator_dir_path( - coordinator_dir, - sizeof(coordinator_dir) - ); - king_system_build_coordinator_state_path( - state_path, - sizeof(state_path) - ); - - if (king_system_ensure_directory_recursive(coordinator_dir) != SUCCESS) { - snprintf( - king_system_coordinator_state_status, - sizeof(king_system_coordinator_state_status), - "%s", - "failed" - ); - snprintf( - king_system_coordinator_state_error, - sizeof(king_system_coordinator_state_error), - "Could not create system coordinator directory '%s'.", - coordinator_dir - ); - return FAILURE; - } - - now = time(NULL); - if (access(state_path, F_OK) != 0) { - version = 1; - generation = 1; - created_at = now; - } else { - if (king_system_load_coordinator_state( - state_path, - &version, - &generation, - &created_at, - loaded_cluster_id, - sizeof(loaded_cluster_id), - loaded_node_id, - sizeof(loaded_node_id), - &clean_shutdown - ) != SUCCESS) { - snprintf( - king_system_coordinator_state_status, - sizeof(king_system_coordinator_state_status), - "%s", - "failed" - ); - snprintf( - king_system_coordinator_state_error, - sizeof(king_system_coordinator_state_error), - "System coordinator state at '%s' is unreadable or corrupted.", - state_path - ); - return FAILURE; - } - - if ( - loaded_cluster_id[0] != '\0' - && king_system_runtime_config.cluster_id[0] != '\0' - && strcmp(loaded_cluster_id, king_system_runtime_config.cluster_id) != 0 - ) { - snprintf( - king_system_coordinator_state_status, - sizeof(king_system_coordinator_state_status), - "%s", - "failed" - ); - snprintf( - king_system_coordinator_state_error, - sizeof(king_system_coordinator_state_error), - "System coordinator state at '%s' belongs to cluster '%s', not '%s'.", - state_path, - loaded_cluster_id, - king_system_runtime_config.cluster_id - ); - return FAILURE; - } - - if (!clean_shutdown && loaded_node_id[0] != '\0') { - king_system_coordinator_state_recovered = true; - king_system_start_recovery_plan( - KING_SYSTEM_RECOVERY_MODE_NODE, - loaded_node_id - ); - } - - generation++; - } - - if (king_system_write_coordinator_state( - state_path, - version, - generation, - created_at, - king_system_runtime_config.cluster_id, - king_system_runtime_config.node_id, - 0 - ) != SUCCESS) { - snprintf( - king_system_coordinator_state_status, - sizeof(king_system_coordinator_state_status), - "%s", - "failed" - ); - snprintf( - king_system_coordinator_state_error, - sizeof(king_system_coordinator_state_error), - "Could not persist system coordinator state at '%s'.", - state_path - ); - return FAILURE; - } - - king_system_coordinator_state_present = true; - king_system_coordinator_state_version = version; - king_system_coordinator_generation = generation; - king_system_coordinator_created_at = created_at; - king_system_coordinator_last_loaded_at = now; - strncpy( - king_system_coordinator_state_path, - state_path, - sizeof(king_system_coordinator_state_path) - 1 - ); - snprintf( - king_system_coordinator_state_status, - sizeof(king_system_coordinator_state_status), - "%s", - king_system_coordinator_state_recovered ? "recovered" : "initialized" - ); - return SUCCESS; -} - -static void king_system_mark_coordinator_clean_shutdown(void) -{ - if ( - !king_system_coordinator_state_present - || king_system_coordinator_state_path[0] == '\0' - ) { - return; - } - - if (king_system_write_coordinator_state( - king_system_coordinator_state_path, - king_system_coordinator_state_version > 0 - ? king_system_coordinator_state_version - : 1, - king_system_coordinator_generation > 0 - ? king_system_coordinator_generation - : 1, - king_system_coordinator_created_at > 0 - ? king_system_coordinator_created_at - : time(NULL), - king_system_runtime_config.cluster_id, - king_system_runtime_config.node_id, - 1 - ) == SUCCESS) { - king_system_coordinator_last_loaded_at = time(NULL); - } -} - -static king_component_info_t *king_system_get_component_internal(king_component_type_t type) -{ - zval *entry; - - if (!king_system_initialized) { - return NULL; - } - - entry = zend_hash_index_find(&king_system_components, (zend_ulong)type); - if (entry == NULL) { - return NULL; - } - - return Z_PTR_P(entry); -} - -static king_component_info_t *king_system_get_component_by_name(const char *name) -{ - size_t idx; - - for (idx = 0; idx < sizeof(king_system_component_names) / sizeof(king_system_component_names[0]); idx++) { - const char *candidate = king_system_component_names[idx].name; - if (strcmp(candidate, name) == 0) { - return king_system_get_component_internal(king_system_component_names[idx].type); - } - } - - return NULL; -} - -const char *king_component_status_to_string(king_component_status_t status) -{ - if ((int) status < 0 || (int) status >= 6) { - return "unknown"; - } - - return king_system_component_statuses[(int) status]; -} - -/* - * Export one aggregate lifecycle so callers do not have to infer process - * state from the per-component map on every status read. - */ -static const char *king_system_resolve_lifecycle( - bool initialized, - uint32_t component_count, - uint32_t ready_count, - bool has_starting, - bool has_draining, - bool has_error -) -{ - if (!initialized || component_count == 0) { - return "stopped"; - } - - if (has_error) { - return "failed"; - } - - if (has_draining) { - return "draining"; - } - - if (has_starting || ready_count < component_count) { - return "starting"; - } - - return "ready"; -} - -static const char *king_system_component_readiness_reason( - king_component_status_t status -) -{ - switch (status) { - case KING_COMPONENT_STATUS_RUNNING: - return "ready"; - case KING_COMPONENT_STATUS_INITIALIZING: - return "component_initializing"; - case KING_COMPONENT_STATUS_SHUTTING_DOWN: - return "component_shutting_down"; - case KING_COMPONENT_STATUS_ERROR: - return "component_failed"; - case KING_COMPONENT_STATUS_SHUTDOWN: - return "component_shutdown"; - case KING_COMPONENT_STATUS_UNINITIALIZED: - default: - return "component_uninitialized"; - } -} - -static zend_bool king_system_component_readiness_blocking( - king_component_status_t status -) -{ - return status == KING_COMPONENT_STATUS_RUNNING ? 0 : 1; -} - -static const king_system_startup_entry_t *king_system_get_startup_entry( - king_component_type_t type -) -{ - size_t idx; - - for ( - idx = 0; - idx < sizeof(king_system_startup_plan) / sizeof(king_system_startup_plan[0]); - idx++ - ) { - if (king_system_startup_plan[idx].type == type) { - return &king_system_startup_plan[idx]; - } - } - - return NULL; -} - -static const king_system_startup_entry_t *king_system_get_shutdown_entry( - king_component_type_t type -) -{ - size_t idx; - - for ( - idx = 0; - idx < sizeof(king_system_shutdown_plan) / sizeof(king_system_shutdown_plan[0]); - idx++ - ) { - if (king_system_shutdown_plan[idx].type == type) { - return &king_system_shutdown_plan[idx]; - } - } - - return NULL; -} - -static zend_bool king_system_component_started(king_component_status_t status) -{ - switch (status) { - case KING_COMPONENT_STATUS_INITIALIZING: - case KING_COMPONENT_STATUS_RUNNING: - case KING_COMPONENT_STATUS_ERROR: - case KING_COMPONENT_STATUS_SHUTTING_DOWN: - return 1; - case KING_COMPONENT_STATUS_UNINITIALIZED: - case KING_COMPONENT_STATUS_SHUTDOWN: - default: - return 0; - } -} - -static zend_bool king_system_component_startup_dependency_ready( - const char *dependency_name -) -{ - king_component_info_t *dependency_info; - - dependency_info = king_system_get_component_by_name(dependency_name); - if (dependency_info == NULL) { - return 0; - } - - return dependency_info->status == KING_COMPONENT_STATUS_RUNNING ? 1 : 0; -} - -static zend_bool king_system_component_dependencies_running( - king_component_type_t type -) -{ - zval dependencies; - zval *dependency_name; - zend_bool ready = 1; - - king_system_build_component_startup_dependencies(&dependencies, type); - - ZEND_HASH_FOREACH_VAL(Z_ARRVAL(dependencies), dependency_name) { - if (Z_TYPE_P(dependency_name) != IS_STRING) { - continue; - } - - if ( - !king_system_component_startup_dependency_ready( - Z_STRVAL_P(dependency_name) - ) - ) { - ready = 0; - break; - } - } ZEND_HASH_FOREACH_END(); - - zval_ptr_dtor(&dependencies); - return ready; -} - -static void king_system_build_component_startup_dependencies( - zval *dependencies, - king_component_type_t type -) -{ - array_init(dependencies); - - switch (type) { - case KING_COMPONENT_CONFIG: - break; - case KING_COMPONENT_CLIENT: - case KING_COMPONENT_SERVER: - case KING_COMPONENT_TELEMETRY: - case KING_COMPONENT_OBJECT_STORE: - case KING_COMPONENT_IIBIN: - add_next_index_string(dependencies, "config"); - break; - case KING_COMPONENT_MCP: - case KING_COMPONENT_SEMANTIC_DNS: - add_next_index_string(dependencies, "config"); - add_next_index_string(dependencies, "client"); - break; - case KING_COMPONENT_ROUTER_LOADBALANCER: - add_next_index_string(dependencies, "config"); - add_next_index_string(dependencies, "client"); - add_next_index_string(dependencies, "server"); - break; - case KING_COMPONENT_PIPELINE_ORCHESTRATOR: - add_next_index_string(dependencies, "config"); - add_next_index_string(dependencies, "telemetry"); - add_next_index_string(dependencies, "object_store"); - break; - case KING_COMPONENT_CDN: - add_next_index_string(dependencies, "config"); - add_next_index_string(dependencies, "server"); - add_next_index_string(dependencies, "object_store"); - break; - case KING_COMPONENT_AUTOSCALING: - add_next_index_string(dependencies, "config"); - add_next_index_string(dependencies, "telemetry"); - add_next_index_string(dependencies, "server"); - add_next_index_string(dependencies, "semantic_dns"); - break; - } -} - -static void king_system_build_component_pending_startup_dependencies( - zval *pending_dependencies, - king_component_type_t type -) -{ - zval dependencies; - zval *dependency_name; - - array_init(pending_dependencies); - king_system_build_component_startup_dependencies(&dependencies, type); +#include "system_integration/config_and_recovery.inc" - ZEND_HASH_FOREACH_VAL(Z_ARRVAL(dependencies), dependency_name) { - king_component_info_t *dependency_info; - const char *dependency; +#include "system_integration/coordinator_state.inc" - if (Z_TYPE_P(dependency_name) != IS_STRING) { - continue; - } - - dependency = Z_STRVAL_P(dependency_name); - dependency_info = king_system_get_component_by_name(dependency); - if (dependency_info == NULL || !king_system_component_startup_dependency_ready(dependency)) { - add_next_index_string(pending_dependencies, dependency); - } - } ZEND_HASH_FOREACH_END(); - - zval_ptr_dtor(&dependencies); -} - -static void king_system_build_component_shutdown_dependents( - zval *dependents, - king_component_type_t type -) -{ - const king_system_startup_entry_t *target_entry; - zval dependency_name_list; - zval *dependency_name; - size_t idx; - - array_init(dependents); - target_entry = king_system_get_startup_entry(type); - if (target_entry == NULL) { - return; - } - - for ( - idx = 0; - idx < sizeof(king_system_startup_plan) / sizeof(king_system_startup_plan[0]); - idx++ - ) { - const king_system_startup_entry_t *entry = &king_system_startup_plan[idx]; - zend_bool depends_on_target = 0; - - if (entry->type == type) { - continue; - } - - king_system_build_component_startup_dependencies( - &dependency_name_list, - entry->type - ); - - ZEND_HASH_FOREACH_VAL(Z_ARRVAL(dependency_name_list), dependency_name) { - if ( - Z_TYPE_P(dependency_name) == IS_STRING && - strcmp(Z_STRVAL_P(dependency_name), target_entry->name) == 0 - ) { - depends_on_target = 1; - break; - } - } ZEND_HASH_FOREACH_END(); - - zval_ptr_dtor(&dependency_name_list); - - if (depends_on_target) { - add_next_index_string(dependents, (char *) entry->name); - } - } -} - -static void king_system_build_component_pending_shutdown_dependents( - zval *pending_dependents, - king_component_type_t type -) -{ - zval dependents; - zval *dependent_name; - - array_init(pending_dependents); - king_system_build_component_shutdown_dependents(&dependents, type); - - ZEND_HASH_FOREACH_VAL(Z_ARRVAL(dependents), dependent_name) { - king_component_info_t *dependent_info; - - if (Z_TYPE_P(dependent_name) != IS_STRING) { - continue; - } - - dependent_info = king_system_get_component_by_name( - Z_STRVAL_P(dependent_name) - ); - if ( - dependent_info != NULL && - king_system_component_started(dependent_info->status) - ) { - add_next_index_string( - pending_dependents, - Z_STRVAL_P(dependent_name) - ); - } - } ZEND_HASH_FOREACH_END(); - - zval_ptr_dtor(&dependents); -} - -/* - * Publish the canonical startup graph even before ordered startup transitions - * are implemented so callers can inspect the intended dependency plan without - * reverse-engineering it from registration order. - */ -static void king_system_build_startup_visibility(zval *startup) -{ - zval blocked_components; - zval component_entry; - zval components; - zval dependencies; - zval pending_components; - zval pending_dependencies; - zval ready_to_start_components; - zval started_components; - zval status_copy; - zval ordered_components; - size_t idx; - - array_init(startup); - array_init(&blocked_components); - array_init(&components); - array_init(&ordered_components); - array_init(&pending_components); - array_init(&ready_to_start_components); - array_init(&started_components); - - for ( - idx = 0; - idx < sizeof(king_system_startup_plan) / sizeof(king_system_startup_plan[0]); - idx++ - ) { - const king_system_startup_entry_t *entry = &king_system_startup_plan[idx]; - king_component_info_t *info = king_system_get_component_internal(entry->type); - zend_bool started = 0; - zend_bool ready_to_start = 0; - - if (info != NULL) { - started = king_system_component_started(info->status); - } - - add_next_index_string(&ordered_components, (char *) entry->name); - king_system_build_component_startup_dependencies(&dependencies, entry->type); - king_system_build_component_pending_startup_dependencies( - &pending_dependencies, - entry->type - ); - ready_to_start = !started - && zend_hash_num_elements(Z_ARRVAL(pending_dependencies)) == 0; - - if (started) { - add_next_index_string(&started_components, (char *) entry->name); - } else { - add_next_index_string(&pending_components, (char *) entry->name); - } - - if (ready_to_start) { - add_next_index_string(&ready_to_start_components, (char *) entry->name); - } - - if ( - !started && - zend_hash_num_elements(Z_ARRVAL(pending_dependencies)) > 0 - ) { - ZVAL_COPY(&status_copy, &pending_dependencies); - add_assoc_zval(&blocked_components, entry->name, &status_copy); - } - - array_init(&component_entry); - add_assoc_long(&component_entry, "order", (zend_long) entry->order); - add_assoc_zval(&component_entry, "dependencies", &dependencies); - add_assoc_zval( - &component_entry, - "pending_dependencies", - &pending_dependencies - ); - add_assoc_bool(&component_entry, "status_visible", info != NULL ? 1 : 0); - add_assoc_bool(&component_entry, "started", started); - add_assoc_bool(&component_entry, "ready_to_start", ready_to_start); - add_assoc_zval(&components, entry->name, &component_entry); - } - - add_assoc_long( - startup, - "catalog_component_count", - (zend_long) ( - sizeof(king_system_startup_plan) / sizeof(king_system_startup_plan[0]) - ) - ); - add_assoc_zval(startup, "ordered_components", &ordered_components); - add_assoc_zval(startup, "started_components", &started_components); - add_assoc_zval(startup, "pending_components", &pending_components); - add_assoc_zval( - startup, - "ready_to_start_components", - &ready_to_start_components - ); - add_assoc_zval(startup, "blocked_components", &blocked_components); - add_assoc_zval(startup, "components", &components); -} - -/* - * Publish the canonical shutdown graph separately from the startup graph so - * operators can inspect which components are leaf shutdown candidates, which - * still have live dependents, and that King requires a drain-first stop path. - */ -static void king_system_build_shutdown_visibility(zval *shutdown) -{ - zval active_components; - zval blocked_components; - zval component_entry; - zval components; - zval inactive_components; - zval ordered_components; - zval ready_to_stop_components; - zval status_copy; - size_t idx; - - array_init(shutdown); - array_init(&active_components); - array_init(&blocked_components); - array_init(&components); - array_init(&inactive_components); - array_init(&ordered_components); - array_init(&ready_to_stop_components); - - for ( - idx = 0; - idx < sizeof(king_system_shutdown_plan) / sizeof(king_system_shutdown_plan[0]); - idx++ - ) { - const king_system_startup_entry_t *entry = &king_system_shutdown_plan[idx]; - king_component_info_t *info = king_system_get_component_internal(entry->type); - zval component_pending_dependents; - zval component_dependents; - zend_bool started = 0; - zend_bool ready_to_stop = 0; - - if (info != NULL) { - started = king_system_component_started(info->status); - } - - add_next_index_string(&ordered_components, (char *) entry->name); - king_system_build_component_shutdown_dependents( - &component_dependents, - entry->type - ); - king_system_build_component_pending_shutdown_dependents( - &component_pending_dependents, - entry->type - ); - ready_to_stop = started - && zend_hash_num_elements( - Z_ARRVAL(component_pending_dependents) - ) == 0; - - if (started) { - add_next_index_string(&active_components, (char *) entry->name); - } else { - add_next_index_string(&inactive_components, (char *) entry->name); - } - - if (ready_to_stop) { - add_next_index_string(&ready_to_stop_components, (char *) entry->name); - } - - if ( - started && - zend_hash_num_elements(Z_ARRVAL(component_pending_dependents)) > 0 - ) { - ZVAL_COPY(&status_copy, &component_pending_dependents); - add_assoc_zval(&blocked_components, entry->name, &status_copy); - } - - array_init(&component_entry); - add_assoc_long(&component_entry, "order", (zend_long) entry->order); - add_assoc_zval(&component_entry, "dependents", &component_dependents); - add_assoc_zval( - &component_entry, - "pending_dependents", - &component_pending_dependents - ); - add_assoc_bool(&component_entry, "status_visible", info != NULL ? 1 : 0); - add_assoc_bool(&component_entry, "started", started); - add_assoc_bool(&component_entry, "ready_to_stop", ready_to_stop); - add_assoc_zval(&components, entry->name, &component_entry); - } - - add_assoc_bool(shutdown, "drain_first_required", 1); - add_assoc_long( - shutdown, - "catalog_component_count", - (zend_long) ( - sizeof(king_system_shutdown_plan) / sizeof(king_system_shutdown_plan[0]) - ) - ); - add_assoc_zval(shutdown, "ordered_components", &ordered_components); - add_assoc_zval(shutdown, "active_components", &active_components); - add_assoc_zval(shutdown, "inactive_components", &inactive_components); - add_assoc_zval(shutdown, "ready_to_stop_components", &ready_to_stop_components); - add_assoc_zval(shutdown, "blocked_components", &blocked_components); - add_assoc_zval(shutdown, "components", &components); -} - -/* - * Start every currently eligible component wave once its declared startup - * dependencies are already running. This keeps the canonical graph from #13 - * honest in the local runtime instead of leaving it as pure metadata. - */ -static void king_system_schedule_startup_components(void) -{ - const king_system_startup_entry_t *entry; - king_component_info_t *info; - size_t idx; - time_t now; - - if (!king_system_initialized) { - return; - } - - now = time(NULL); - for ( - idx = 0; - idx < sizeof(king_system_startup_plan) / sizeof(king_system_startup_plan[0]); - idx++ - ) { - entry = &king_system_startup_plan[idx]; - info = king_system_get_component_internal(entry->type); - if (info == NULL) { - continue; - } - - if ( - info->status != KING_COMPONENT_STATUS_UNINITIALIZED && - info->status != KING_COMPONENT_STATUS_SHUTDOWN - ) { - continue; - } - - if (!king_system_component_dependencies_running(entry->type)) { - continue; - } - - info->status = KING_COMPONENT_STATUS_INITIALIZING; - info->last_health_check = now; - } -} - -/* - * During a coordinated shutdown, stop only components whose dependents are - * already inactive. This preserves the canonical reverse-startup order and - * keeps the runtime in a visible drain state until the final root component - * can stop safely. - */ -static void king_system_schedule_shutdown_components(void) -{ - const king_system_startup_entry_t *entry; - king_component_info_t *info; - size_t idx; - time_t now; - zval pending_dependents; - - if ( - !king_system_initialized - || (!king_system_shutdown_requested && !king_system_recovery_requested) - ) { - return; - } - - now = time(NULL); - for ( - idx = 0; - idx < sizeof(king_system_shutdown_plan) / sizeof(king_system_shutdown_plan[0]); - idx++ - ) { - entry = &king_system_shutdown_plan[idx]; - info = king_system_get_component_internal(entry->type); - if (info == NULL) { - continue; - } - - if (!king_system_component_started(info->status)) { - continue; - } - - if (info->status == KING_COMPONENT_STATUS_SHUTTING_DOWN) { - continue; - } - - king_system_build_component_pending_shutdown_dependents( - &pending_dependents, - entry->type - ); - if (zend_hash_num_elements(Z_ARRVAL(pending_dependents)) == 0) { - info->status = KING_COMPONENT_STATUS_SHUTTING_DOWN; - info->last_health_check = now; - } - zval_ptr_dtor(&pending_dependents); - } -} - -/* - * Publish the repo-local lifecycle graph explicitly so callers do not have to - * infer the current state's next legal aggregate transitions themselves. - */ -static void king_system_build_allowed_lifecycle_transitions( - zval *transitions, - const char *lifecycle -) -{ - array_init(transitions); - - if (lifecycle == NULL || strcmp(lifecycle, "stopped") == 0) { - add_next_index_string(transitions, "ready"); - return; - } - - if (strcmp(lifecycle, "ready") == 0) { - add_next_index_string(transitions, "draining"); - add_next_index_string(transitions, "failed"); - add_next_index_string(transitions, "stopped"); - return; - } - - if (strcmp(lifecycle, "draining") == 0) { - if (!king_system_shutdown_requested) { - add_next_index_string(transitions, "starting"); - } - add_next_index_string(transitions, "failed"); - add_next_index_string(transitions, "stopped"); - return; - } +#include "system_integration/component_lookup.inc" - if (strcmp(lifecycle, "starting") == 0) { - add_next_index_string(transitions, "ready"); - add_next_index_string(transitions, "draining"); - add_next_index_string(transitions, "failed"); - add_next_index_string(transitions, "stopped"); - return; - } - - if (strcmp(lifecycle, "failed") == 0) { - add_next_index_string(transitions, "draining"); - add_next_index_string(transitions, "stopped"); - } -} - -static void king_system_collect_admission_state( - king_system_admission_state_t *state -) -{ - king_component_info_t *info; - zend_ulong idx; - bool draining_request = king_system_shutdown_requested || king_system_recovery_requested; - - memset(state, 0, sizeof(*state)); - state->lifecycle = "stopped"; - - if (!king_system_initialized) { - return; - } - - state->component_count = (uint32_t) zend_hash_num_elements(&king_system_components); - ZEND_HASH_FOREACH_NUM_KEY_PTR(&king_system_components, idx, info) { - switch (info->status) { - case KING_COMPONENT_STATUS_RUNNING: - state->ready_count++; - break; - case KING_COMPONENT_STATUS_SHUTTING_DOWN: - state->draining_count++; - state->has_draining = true; - break; - case KING_COMPONENT_STATUS_INITIALIZING: - case KING_COMPONENT_STATUS_UNINITIALIZED: - case KING_COMPONENT_STATUS_SHUTDOWN: - state->has_starting = true; - break; - case KING_COMPONENT_STATUS_ERROR: - state->has_error = true; - break; - } +#include "system_integration/component_visibility.inc" - if (king_system_component_readiness_blocking(info->status)) { - state->readiness_blocker_count++; - } - } ZEND_HASH_FOREACH_END(); - - if (draining_request && (state->has_draining || state->has_error)) { - state->lifecycle = "draining"; - } else { - state->lifecycle = king_system_resolve_lifecycle( - king_system_initialized, - state->component_count, - state->ready_count, - state->has_starting, - state->has_draining, - state->has_error - ); - } - state->aggregate_ready = king_system_initialized - && state->component_count > 0 - && state->readiness_blocker_count == 0 - && strcmp(state->lifecycle, "ready") == 0; -} +#include "system_integration/lifecycle_runtime.inc" static uint32_t king_system_count_started_components(void) { @@ -1937,504 +534,4 @@ int king_system_register_component(king_component_type_t type, const char *name, /* --- PHP Entry Points --- */ -PHP_FUNCTION(king_system_init) -{ - zval *config_arr; - if (zend_parse_parameters(1, "a", &config_arr) == FAILURE) RETURN_FALSE; - - king_system_config_t config; - king_system_apply_default_config(&config); - king_system_apply_config(&config, config_arr); - - if (king_system_init_all_components(&config) == SUCCESS) { - RETURN_TRUE; - } - - RETURN_FALSE; -} - -PHP_FUNCTION(king_system_process_request) -{ - zval *request_data; - if (zend_parse_parameters(1, "a", &request_data) == FAILURE) RETURN_FALSE; - - if (!king_system_initialized) { - king_set_error( - "king_system_process_request() cannot process requests because the coordinated runtime is not initialized." - ); - RETURN_FALSE; - } - - if ( - king_system_require_admission( - "king_system_process_request", - "process_requests" - ) != SUCCESS - ) { - RETURN_FALSE; - } - - (void) request_data; - { - king_component_info_t *info; - zend_ulong idx; - ZEND_HASH_FOREACH_NUM_KEY_PTR(&king_system_components, idx, info) { - info->requests_handled++; - info->last_health_check = time(NULL); - } ZEND_HASH_FOREACH_END(); - } - - king_system_apply_all_transitions(); - RETURN_TRUE; -} - -PHP_FUNCTION(king_system_restart_component) -{ - char *name; - size_t name_len; - if (zend_parse_parameters(1, "s", &name, &name_len) == FAILURE) RETURN_FALSE; - - king_component_info_t *info = king_system_get_component_by_name(name); - if (!king_system_initialized || info == NULL) { - RETURN_FALSE; - } - - (void) name_len; - - info->status = KING_COMPONENT_STATUS_SHUTTING_DOWN; - info->last_health_check = time(NULL); - if (!king_system_shutdown_requested) { - king_system_drain_reason = KING_SYSTEM_DRAIN_REASON_COMPONENT_RESTART; - } - - RETURN_TRUE; -} - -PHP_FUNCTION(king_system_fail_component) -{ - char *name; - size_t name_len; - king_component_info_t *info; - - if (zend_parse_parameters(1, "s", &name, &name_len) == FAILURE) { - RETURN_FALSE; - } - - (void) name_len; - - info = king_system_get_component_by_name(name); - if (!king_system_initialized || info == NULL) { - RETURN_FALSE; - } - - RETURN_BOOL( - king_system_handle_component_error( - info->type, - "manual component failure" - ) == SUCCESS - ); -} - -PHP_FUNCTION(king_system_recover) -{ - king_system_admission_state_t state; - - if (!king_system_initialized || king_system_shutdown_requested) { - RETURN_FALSE; - } - - if (king_system_recovery_requested) { - RETURN_TRUE; - } - - king_system_collect_admission_state(&state); - if (strcmp(state.lifecycle, "failed") != 0) { - RETURN_FALSE; - } - - king_system_recovery_requested = true; - king_system_start_recovery_plan( - KING_SYSTEM_RECOVERY_MODE_COMPONENT, - king_system_runtime_config.node_id - ); - king_system_drain_reason = KING_SYSTEM_DRAIN_REASON_COMPONENT_RECOVERY; - king_system_apply_all_transitions(); - RETURN_TRUE; -} - -PHP_FUNCTION(king_system_shutdown) -{ - if (!king_system_initialized) { - RETURN_TRUE; - } - - king_system_shutdown_requested = true; - king_system_drain_reason = KING_SYSTEM_DRAIN_REASON_SYSTEM_SHUTDOWN; - king_system_apply_all_transitions(); - RETURN_TRUE; -} - -PHP_FUNCTION(king_system_get_status) -{ - ZEND_PARSE_PARAMETERS_NONE(); - zval admission; - zval allowed_lifecycle_transitions; - zval blocker_entry; - zval component_entry; - zval components; - zval drain_intent; - zval drain_targets; - zval readiness_blockers; - zval recovery; - zval shutdown; - zval startup; - time_t now; - time_t drain_requested_at = 0; - bool recovery_active = false; - king_system_admission_state_t admission_state; - zend_ulong idx; - uint32_t drain_target_count = 0; - king_component_info_t *info; - - now = time(NULL); - king_system_apply_all_transitions(); - king_system_collect_admission_state(&admission_state); - recovery_active = ( - king_system_recovery_mode != KING_SYSTEM_RECOVERY_MODE_NONE - && king_system_initialized - && strcmp(admission_state.lifecycle, "ready") != 0 - ) ? 1 : 0; - - array_init(&components); - array_init(&drain_targets); - array_init(&readiness_blockers); - if (king_system_initialized) { - ZEND_HASH_FOREACH_NUM_KEY_PTR(&king_system_components, idx, info) { - uint32_t shutdown_pending_dependent_count = 0; - time_t last_health_check = info->last_health_check; - uint32_t startup_pending_dependency_count = 0; - zend_bool readiness_blocking = king_system_component_readiness_blocking( - info->status - ); - const char *readiness_reason = king_system_component_readiness_reason( - info->status - ); - const king_system_startup_entry_t *shutdown_entry = - king_system_get_shutdown_entry(info->type); - const king_system_startup_entry_t *startup_entry = - king_system_get_startup_entry(info->type); - zval shutdown_dependents; - zval shutdown_pending_dependents; - zval startup_dependencies; - zval startup_pending_dependencies; - - king_system_build_component_shutdown_dependents( - &shutdown_dependents, - info->type - ); - king_system_build_component_pending_shutdown_dependents( - &shutdown_pending_dependents, - info->type - ); - shutdown_pending_dependent_count = (uint32_t) zend_hash_num_elements( - Z_ARRVAL(shutdown_pending_dependents) - ); - king_system_build_component_startup_dependencies( - &startup_dependencies, - info->type - ); - king_system_build_component_pending_startup_dependencies( - &startup_pending_dependencies, - info->type - ); - startup_pending_dependency_count = (uint32_t) zend_hash_num_elements( - Z_ARRVAL(startup_pending_dependencies) - ); - - array_init(&component_entry); - add_assoc_string(&component_entry, "status", king_component_status_to_string(info->status)); - add_assoc_bool( - &component_entry, - "ready", - info->status == KING_COMPONENT_STATUS_RUNNING ? 1 : 0 - ); - add_assoc_string(&component_entry, "readiness_reason", (char *) readiness_reason); - add_assoc_bool( - &component_entry, - "readiness_blocking", - readiness_blocking - ); - add_assoc_long(&component_entry, "requests_handled", (zend_long) info->requests_handled); - add_assoc_long(&component_entry, "errors_encountered", (zend_long) info->errors_encountered); - add_assoc_long(&component_entry, "last_health_check", (zend_long) last_health_check); - add_assoc_long(&component_entry, "up_for_seconds", (zend_long) (now - last_health_check)); - add_assoc_long( - &component_entry, - "shutdown_order", - shutdown_entry != NULL ? (zend_long) shutdown_entry->order : 0 - ); - add_assoc_zval( - &component_entry, - "shutdown_dependents", - &shutdown_dependents - ); - add_assoc_zval( - &component_entry, - "shutdown_pending_dependents", - &shutdown_pending_dependents - ); - add_assoc_bool( - &component_entry, - "shutdown_ready_to_stop", - king_system_component_started(info->status) && - shutdown_pending_dependent_count == 0 - ); - add_assoc_long( - &component_entry, - "startup_order", - startup_entry != NULL ? (zend_long) startup_entry->order : 0 - ); - add_assoc_zval( - &component_entry, - "startup_dependencies", - &startup_dependencies - ); - add_assoc_zval( - &component_entry, - "startup_pending_dependencies", - &startup_pending_dependencies - ); - add_assoc_bool( - &component_entry, - "startup_ready_to_start", - !king_system_component_started(info->status) && - startup_pending_dependency_count == 0 - ); - - if ( - ((king_system_shutdown_requested || king_system_recovery_requested) && - king_system_component_started(info->status)) || - info->status == KING_COMPONENT_STATUS_SHUTTING_DOWN - ) { - add_next_index_string(&drain_targets, info->name); - drain_target_count++; - if (drain_requested_at == 0 || last_health_check < drain_requested_at) { - drain_requested_at = last_health_check; - } - } - - if (readiness_blocking) { - array_init(&blocker_entry); - add_assoc_string( - &blocker_entry, - "status", - king_component_status_to_string(info->status) - ); - add_assoc_string( - &blocker_entry, - "readiness_reason", - (char *) readiness_reason - ); - add_assoc_zval(&readiness_blockers, info->name, &blocker_entry); - } - - add_assoc_zval(&components, info->name, &component_entry); - } ZEND_HASH_FOREACH_END(); - } - - array_init(return_value); - add_assoc_bool(return_value, "initialized", king_system_initialized); - add_assoc_string(return_value, "lifecycle", (char *) admission_state.lifecycle); - add_assoc_long(return_value, "component_count", admission_state.component_count); - add_assoc_zval(return_value, "components", &components); - add_assoc_long(return_value, "components_ready", (zend_long) admission_state.ready_count); - add_assoc_long( - return_value, - "components_draining", - (zend_long) admission_state.draining_count - ); - add_assoc_long( - return_value, - "readiness_blocker_count", - (zend_long) admission_state.readiness_blocker_count - ); - add_assoc_zval(return_value, "readiness_blockers", &readiness_blockers); - array_init(&drain_intent); - add_assoc_bool( - &drain_intent, - "requested", - (king_system_shutdown_requested - || king_system_recovery_requested - || admission_state.has_draining) ? 1 : 0 - ); - add_assoc_bool( - &drain_intent, - "active", - strcmp(admission_state.lifecycle, "draining") == 0 ? 1 : 0 - ); - add_assoc_string( - &drain_intent, - "reason", - (char *) ( - king_system_shutdown_requested - || king_system_recovery_requested - || admission_state.has_draining - ? king_system_drain_reason_to_string(king_system_drain_reason) - : "none" - ) - ); - add_assoc_long(&drain_intent, "requested_at", (zend_long) drain_requested_at); - if (king_system_shutdown_requested) { - add_assoc_string(&drain_intent, "target_lifecycle", "stopped"); - } else if (king_system_recovery_requested) { - add_assoc_string(&drain_intent, "target_lifecycle", "ready"); - } else if (admission_state.has_draining) { - add_assoc_string(&drain_intent, "target_lifecycle", "starting"); - } else { - add_assoc_null(&drain_intent, "target_lifecycle"); - } - add_assoc_long( - &drain_intent, - "target_component_count", - (zend_long) drain_target_count - ); - add_assoc_zval(&drain_intent, "target_components", &drain_targets); - add_assoc_zval(return_value, "drain_intent", &drain_intent); - king_system_build_allowed_lifecycle_transitions( - &allowed_lifecycle_transitions, - admission_state.lifecycle - ); - add_assoc_zval( - return_value, - "allowed_lifecycle_transitions", - &allowed_lifecycle_transitions - ); - array_init(&recovery); - add_assoc_bool( - &recovery, - "active", - recovery_active - ); - add_assoc_bool( - &recovery, - "recovered", - king_system_coordinator_state_recovered ? 1 : 0 - ); - add_assoc_string( - &recovery, - "reason", - (char *) king_system_recovery_reason_to_string() - ); - add_assoc_string( - &recovery, - "mode", - king_system_recovery_mode_to_string(king_system_recovery_mode) - ); - if (king_system_recovery_plan_id[0] != '\0') { - add_assoc_string(&recovery, "plan_id", king_system_recovery_plan_id); - } else { - add_assoc_null(&recovery, "plan_id"); - } - add_assoc_long( - &recovery, - "plan_requested_at", - (zend_long) king_system_recovery_plan_requested_at - ); - add_assoc_long( - &recovery, - "plan_window_seconds", - (zend_long) king_system_recovery_plan_window_seconds - ); - if (king_system_recovery_source_node_id[0] != '\0') { - add_assoc_string( - &recovery, - "source_node_id", - king_system_recovery_source_node_id - ); - } else { - add_assoc_null(&recovery, "source_node_id"); - } - if (king_system_runtime_config.node_id[0] != '\0') { - add_assoc_string( - &recovery, - "active_node_id", - king_system_runtime_config.node_id - ); - } else { - add_assoc_null(&recovery, "active_node_id"); - } - if (king_system_runtime_config.cluster_id[0] != '\0') { - add_assoc_string( - &recovery, - "cluster_id", - king_system_runtime_config.cluster_id - ); - } else { - add_assoc_null(&recovery, "cluster_id"); - } - add_assoc_bool( - &recovery, - "coordinator_state_present", - king_system_coordinator_state_present ? 1 : 0 - ); - add_assoc_string( - &recovery, - "coordinator_state_status", - king_system_coordinator_state_status - ); - if (king_system_coordinator_state_path[0] != '\0') { - add_assoc_string( - &recovery, - "coordinator_state_path", - king_system_coordinator_state_path - ); - } else { - add_assoc_null(&recovery, "coordinator_state_path"); - } - add_assoc_long( - &recovery, - "coordinator_state_version", - (zend_long) king_system_coordinator_state_version - ); - add_assoc_long( - &recovery, - "coordinator_generation", - (zend_long) king_system_coordinator_generation - ); - add_assoc_long( - &recovery, - "coordinator_created_at", - (zend_long) king_system_coordinator_created_at - ); - add_assoc_long( - &recovery, - "coordinator_last_loaded_at", - (zend_long) king_system_coordinator_last_loaded_at - ); - add_assoc_string( - &recovery, - "coordinator_state_error", - king_system_coordinator_state_error - ); - add_assoc_zval(return_value, "recovery", &recovery); - king_system_build_startup_visibility(&startup); - add_assoc_zval(return_value, "startup", &startup); - king_system_build_shutdown_visibility(&shutdown); - add_assoc_zval(return_value, "shutdown", &shutdown); - array_init(&admission); - add_assoc_bool(&admission, "process_requests", admission_state.aggregate_ready); - add_assoc_bool(&admission, "http_listener_accepts", admission_state.aggregate_ready); - add_assoc_bool(&admission, "websocket_upgrades", admission_state.aggregate_ready); - add_assoc_bool(&admission, "websocket_peer_accepts", admission_state.aggregate_ready); - add_assoc_bool(&admission, "orchestrator_submissions", admission_state.aggregate_ready); - add_assoc_bool(&admission, "file_worker_claims", admission_state.aggregate_ready); - add_assoc_bool(&admission, "file_worker_resumes", admission_state.aggregate_ready); - add_assoc_bool(&admission, "remote_peer_dispatches", admission_state.aggregate_ready); - add_assoc_bool(&admission, "remote_peer_resumes", admission_state.aggregate_ready); - add_assoc_zval(return_value, "admission", &admission); - add_assoc_long( - return_value, - "health_check_interval_seconds", - (zend_long) king_system_runtime_config.health_check_interval_seconds - ); -} +#include "system_integration/procedural_api.inc" diff --git a/extension/src/integration/system_integration/component_lookup.inc b/extension/src/integration/system_integration/component_lookup.inc new file mode 100644 index 000000000..958046e5f --- /dev/null +++ b/extension/src/integration/system_integration/component_lookup.inc @@ -0,0 +1,194 @@ +static king_component_info_t *king_system_get_component_internal(king_component_type_t type) +{ + zval *entry; + + if (!king_system_initialized) { + return NULL; + } + + entry = zend_hash_index_find(&king_system_components, (zend_ulong)type); + if (entry == NULL) { + return NULL; + } + + return Z_PTR_P(entry); +} + +static king_component_info_t *king_system_get_component_by_name(const char *name) +{ + size_t idx; + + for (idx = 0; idx < sizeof(king_system_component_names) / sizeof(king_system_component_names[0]); idx++) { + const char *candidate = king_system_component_names[idx].name; + if (strcmp(candidate, name) == 0) { + return king_system_get_component_internal(king_system_component_names[idx].type); + } + } + + return NULL; +} + +const char *king_component_status_to_string(king_component_status_t status) +{ + if ((int) status < 0 || (int) status >= 6) { + return "unknown"; + } + + return king_system_component_statuses[(int) status]; +} + +/* + * Export one aggregate lifecycle so callers do not have to infer process + * state from the per-component map on every status read. + */ +static const char *king_system_resolve_lifecycle( + bool initialized, + uint32_t component_count, + uint32_t ready_count, + bool has_starting, + bool has_draining, + bool has_error +) +{ + if (!initialized || component_count == 0) { + return "stopped"; + } + + if (has_error) { + return "failed"; + } + + if (has_draining) { + return "draining"; + } + + if (has_starting || ready_count < component_count) { + return "starting"; + } + + return "ready"; +} + +static const char *king_system_component_readiness_reason( + king_component_status_t status +) +{ + switch (status) { + case KING_COMPONENT_STATUS_RUNNING: + return "ready"; + case KING_COMPONENT_STATUS_INITIALIZING: + return "component_initializing"; + case KING_COMPONENT_STATUS_SHUTTING_DOWN: + return "component_shutting_down"; + case KING_COMPONENT_STATUS_ERROR: + return "component_failed"; + case KING_COMPONENT_STATUS_SHUTDOWN: + return "component_shutdown"; + case KING_COMPONENT_STATUS_UNINITIALIZED: + default: + return "component_uninitialized"; + } +} + +static zend_bool king_system_component_readiness_blocking( + king_component_status_t status +) +{ + return status == KING_COMPONENT_STATUS_RUNNING ? 0 : 1; +} + +static const king_system_startup_entry_t *king_system_get_startup_entry( + king_component_type_t type +) +{ + size_t idx; + + for ( + idx = 0; + idx < sizeof(king_system_startup_plan) / sizeof(king_system_startup_plan[0]); + idx++ + ) { + if (king_system_startup_plan[idx].type == type) { + return &king_system_startup_plan[idx]; + } + } + + return NULL; +} + +static const king_system_startup_entry_t *king_system_get_shutdown_entry( + king_component_type_t type +) +{ + size_t idx; + + for ( + idx = 0; + idx < sizeof(king_system_shutdown_plan) / sizeof(king_system_shutdown_plan[0]); + idx++ + ) { + if (king_system_shutdown_plan[idx].type == type) { + return &king_system_shutdown_plan[idx]; + } + } + + return NULL; +} + +static zend_bool king_system_component_started(king_component_status_t status) +{ + switch (status) { + case KING_COMPONENT_STATUS_INITIALIZING: + case KING_COMPONENT_STATUS_RUNNING: + case KING_COMPONENT_STATUS_ERROR: + case KING_COMPONENT_STATUS_SHUTTING_DOWN: + return 1; + case KING_COMPONENT_STATUS_UNINITIALIZED: + case KING_COMPONENT_STATUS_SHUTDOWN: + default: + return 0; + } +} + +static zend_bool king_system_component_startup_dependency_ready( + const char *dependency_name +) +{ + king_component_info_t *dependency_info; + + dependency_info = king_system_get_component_by_name(dependency_name); + if (dependency_info == NULL) { + return 0; + } + + return dependency_info->status == KING_COMPONENT_STATUS_RUNNING ? 1 : 0; +} + +static zend_bool king_system_component_dependencies_running( + king_component_type_t type +) +{ + zval dependencies; + zval *dependency_name; + zend_bool ready = 1; + + king_system_build_component_startup_dependencies(&dependencies, type); + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL(dependencies), dependency_name) { + if (Z_TYPE_P(dependency_name) != IS_STRING) { + continue; + } + + if ( + !king_system_component_startup_dependency_ready( + Z_STRVAL_P(dependency_name) + ) + ) { + ready = 0; + break; + } + } ZEND_HASH_FOREACH_END(); + + zval_ptr_dtor(&dependencies); + return ready; +} diff --git a/extension/src/integration/system_integration/component_visibility.inc b/extension/src/integration/system_integration/component_visibility.inc new file mode 100644 index 000000000..6818d2b31 --- /dev/null +++ b/extension/src/integration/system_integration/component_visibility.inc @@ -0,0 +1,364 @@ +static void king_system_build_component_startup_dependencies( + zval *dependencies, + king_component_type_t type +) +{ + array_init(dependencies); + + switch (type) { + case KING_COMPONENT_CONFIG: + break; + case KING_COMPONENT_CLIENT: + case KING_COMPONENT_SERVER: + case KING_COMPONENT_TELEMETRY: + case KING_COMPONENT_OBJECT_STORE: + case KING_COMPONENT_IIBIN: + add_next_index_string(dependencies, "config"); + break; + case KING_COMPONENT_MCP: + case KING_COMPONENT_SEMANTIC_DNS: + add_next_index_string(dependencies, "config"); + add_next_index_string(dependencies, "client"); + break; + case KING_COMPONENT_ROUTER_LOADBALANCER: + add_next_index_string(dependencies, "config"); + add_next_index_string(dependencies, "client"); + add_next_index_string(dependencies, "server"); + break; + case KING_COMPONENT_PIPELINE_ORCHESTRATOR: + add_next_index_string(dependencies, "config"); + add_next_index_string(dependencies, "telemetry"); + add_next_index_string(dependencies, "object_store"); + break; + case KING_COMPONENT_CDN: + add_next_index_string(dependencies, "config"); + add_next_index_string(dependencies, "server"); + add_next_index_string(dependencies, "object_store"); + break; + case KING_COMPONENT_AUTOSCALING: + add_next_index_string(dependencies, "config"); + add_next_index_string(dependencies, "telemetry"); + add_next_index_string(dependencies, "server"); + add_next_index_string(dependencies, "semantic_dns"); + break; + } +} + +static void king_system_build_component_pending_startup_dependencies( + zval *pending_dependencies, + king_component_type_t type +) +{ + zval dependencies; + zval *dependency_name; + + array_init(pending_dependencies); + king_system_build_component_startup_dependencies(&dependencies, type); + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL(dependencies), dependency_name) { + king_component_info_t *dependency_info; + const char *dependency; + + if (Z_TYPE_P(dependency_name) != IS_STRING) { + continue; + } + + dependency = Z_STRVAL_P(dependency_name); + dependency_info = king_system_get_component_by_name(dependency); + if (dependency_info == NULL || !king_system_component_startup_dependency_ready(dependency)) { + add_next_index_string(pending_dependencies, dependency); + } + } ZEND_HASH_FOREACH_END(); + + zval_ptr_dtor(&dependencies); +} + +static void king_system_build_component_shutdown_dependents( + zval *dependents, + king_component_type_t type +) +{ + const king_system_startup_entry_t *target_entry; + zval dependency_name_list; + zval *dependency_name; + size_t idx; + + array_init(dependents); + target_entry = king_system_get_startup_entry(type); + if (target_entry == NULL) { + return; + } + + for ( + idx = 0; + idx < sizeof(king_system_startup_plan) / sizeof(king_system_startup_plan[0]); + idx++ + ) { + const king_system_startup_entry_t *entry = &king_system_startup_plan[idx]; + zend_bool depends_on_target = 0; + + if (entry->type == type) { + continue; + } + + king_system_build_component_startup_dependencies( + &dependency_name_list, + entry->type + ); + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL(dependency_name_list), dependency_name) { + if ( + Z_TYPE_P(dependency_name) == IS_STRING && + strcmp(Z_STRVAL_P(dependency_name), target_entry->name) == 0 + ) { + depends_on_target = 1; + break; + } + } ZEND_HASH_FOREACH_END(); + + zval_ptr_dtor(&dependency_name_list); + + if (depends_on_target) { + add_next_index_string(dependents, (char *) entry->name); + } + } +} + +static void king_system_build_component_pending_shutdown_dependents( + zval *pending_dependents, + king_component_type_t type +) +{ + zval dependents; + zval *dependent_name; + + array_init(pending_dependents); + king_system_build_component_shutdown_dependents(&dependents, type); + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL(dependents), dependent_name) { + king_component_info_t *dependent_info; + + if (Z_TYPE_P(dependent_name) != IS_STRING) { + continue; + } + + dependent_info = king_system_get_component_by_name( + Z_STRVAL_P(dependent_name) + ); + if ( + dependent_info != NULL && + king_system_component_started(dependent_info->status) + ) { + add_next_index_string( + pending_dependents, + Z_STRVAL_P(dependent_name) + ); + } + } ZEND_HASH_FOREACH_END(); + + zval_ptr_dtor(&dependents); +} + +/* + * Publish the canonical startup graph even before ordered startup transitions + * are implemented so callers can inspect the intended dependency plan without + * reverse-engineering it from registration order. + */ +static void king_system_build_startup_visibility(zval *startup) +{ + zval blocked_components; + zval component_entry; + zval components; + zval dependencies; + zval pending_components; + zval pending_dependencies; + zval ready_to_start_components; + zval started_components; + zval status_copy; + zval ordered_components; + size_t idx; + + array_init(startup); + array_init(&blocked_components); + array_init(&components); + array_init(&ordered_components); + array_init(&pending_components); + array_init(&ready_to_start_components); + array_init(&started_components); + + for ( + idx = 0; + idx < sizeof(king_system_startup_plan) / sizeof(king_system_startup_plan[0]); + idx++ + ) { + const king_system_startup_entry_t *entry = &king_system_startup_plan[idx]; + king_component_info_t *info = king_system_get_component_internal(entry->type); + zend_bool started = 0; + zend_bool ready_to_start = 0; + + if (info != NULL) { + started = king_system_component_started(info->status); + } + + add_next_index_string(&ordered_components, (char *) entry->name); + king_system_build_component_startup_dependencies(&dependencies, entry->type); + king_system_build_component_pending_startup_dependencies( + &pending_dependencies, + entry->type + ); + ready_to_start = !started + && zend_hash_num_elements(Z_ARRVAL(pending_dependencies)) == 0; + + if (started) { + add_next_index_string(&started_components, (char *) entry->name); + } else { + add_next_index_string(&pending_components, (char *) entry->name); + } + + if (ready_to_start) { + add_next_index_string(&ready_to_start_components, (char *) entry->name); + } + + if ( + !started && + zend_hash_num_elements(Z_ARRVAL(pending_dependencies)) > 0 + ) { + ZVAL_COPY(&status_copy, &pending_dependencies); + add_assoc_zval(&blocked_components, entry->name, &status_copy); + } + + array_init(&component_entry); + add_assoc_long(&component_entry, "order", (zend_long) entry->order); + add_assoc_zval(&component_entry, "dependencies", &dependencies); + add_assoc_zval( + &component_entry, + "pending_dependencies", + &pending_dependencies + ); + add_assoc_bool(&component_entry, "status_visible", info != NULL ? 1 : 0); + add_assoc_bool(&component_entry, "started", started); + add_assoc_bool(&component_entry, "ready_to_start", ready_to_start); + add_assoc_zval(&components, entry->name, &component_entry); + } + + add_assoc_long( + startup, + "catalog_component_count", + (zend_long) ( + sizeof(king_system_startup_plan) / sizeof(king_system_startup_plan[0]) + ) + ); + add_assoc_zval(startup, "ordered_components", &ordered_components); + add_assoc_zval(startup, "started_components", &started_components); + add_assoc_zval(startup, "pending_components", &pending_components); + add_assoc_zval( + startup, + "ready_to_start_components", + &ready_to_start_components + ); + add_assoc_zval(startup, "blocked_components", &blocked_components); + add_assoc_zval(startup, "components", &components); +} + +/* + * Publish the canonical shutdown graph separately from the startup graph so + * operators can inspect which components are leaf shutdown candidates, which + * still have live dependents, and that King requires a drain-first stop path. + */ +static void king_system_build_shutdown_visibility(zval *shutdown) +{ + zval active_components; + zval blocked_components; + zval component_entry; + zval components; + zval inactive_components; + zval ordered_components; + zval ready_to_stop_components; + zval status_copy; + size_t idx; + + array_init(shutdown); + array_init(&active_components); + array_init(&blocked_components); + array_init(&components); + array_init(&inactive_components); + array_init(&ordered_components); + array_init(&ready_to_stop_components); + + for ( + idx = 0; + idx < sizeof(king_system_shutdown_plan) / sizeof(king_system_shutdown_plan[0]); + idx++ + ) { + const king_system_startup_entry_t *entry = &king_system_shutdown_plan[idx]; + king_component_info_t *info = king_system_get_component_internal(entry->type); + zval component_pending_dependents; + zval component_dependents; + zend_bool started = 0; + zend_bool ready_to_stop = 0; + + if (info != NULL) { + started = king_system_component_started(info->status); + } + + add_next_index_string(&ordered_components, (char *) entry->name); + king_system_build_component_shutdown_dependents( + &component_dependents, + entry->type + ); + king_system_build_component_pending_shutdown_dependents( + &component_pending_dependents, + entry->type + ); + ready_to_stop = started + && zend_hash_num_elements( + Z_ARRVAL(component_pending_dependents) + ) == 0; + + if (started) { + add_next_index_string(&active_components, (char *) entry->name); + } else { + add_next_index_string(&inactive_components, (char *) entry->name); + } + + if (ready_to_stop) { + add_next_index_string(&ready_to_stop_components, (char *) entry->name); + } + + if ( + started && + zend_hash_num_elements(Z_ARRVAL(component_pending_dependents)) > 0 + ) { + ZVAL_COPY(&status_copy, &component_pending_dependents); + add_assoc_zval(&blocked_components, entry->name, &status_copy); + } + + array_init(&component_entry); + add_assoc_long(&component_entry, "order", (zend_long) entry->order); + add_assoc_zval(&component_entry, "dependents", &component_dependents); + add_assoc_zval( + &component_entry, + "pending_dependents", + &component_pending_dependents + ); + add_assoc_bool(&component_entry, "status_visible", info != NULL ? 1 : 0); + add_assoc_bool(&component_entry, "started", started); + add_assoc_bool(&component_entry, "ready_to_stop", ready_to_stop); + add_assoc_zval(&components, entry->name, &component_entry); + } + + add_assoc_bool(shutdown, "drain_first_required", 1); + add_assoc_long( + shutdown, + "catalog_component_count", + (zend_long) ( + sizeof(king_system_shutdown_plan) / sizeof(king_system_shutdown_plan[0]) + ) + ); + add_assoc_zval(shutdown, "ordered_components", &ordered_components); + add_assoc_zval(shutdown, "active_components", &active_components); + add_assoc_zval(shutdown, "inactive_components", &inactive_components); + add_assoc_zval(shutdown, "ready_to_stop_components", &ready_to_stop_components); + add_assoc_zval(shutdown, "blocked_components", &blocked_components); + add_assoc_zval(shutdown, "components", &components); +} diff --git a/extension/src/integration/system_integration/config_and_recovery.inc b/extension/src/integration/system_integration/config_and_recovery.inc new file mode 100644 index 000000000..a1375849c --- /dev/null +++ b/extension/src/integration/system_integration/config_and_recovery.inc @@ -0,0 +1,217 @@ +static void king_system_reset_drain_state(void) +{ + king_system_shutdown_requested = false; + king_system_recovery_requested = false; + king_system_drain_reason = KING_SYSTEM_DRAIN_REASON_NONE; +} + +static void king_system_reset_coordinator_state_runtime(void) +{ + king_system_coordinator_state_present = false; + king_system_coordinator_state_recovered = false; + king_system_coordinator_state_version = 0; + king_system_coordinator_generation = 0; + king_system_coordinator_created_at = 0; + king_system_coordinator_last_loaded_at = 0; + king_system_coordinator_state_path[0] = '\0'; + king_system_coordinator_state_error[0] = '\0'; + king_system_recovery_source_node_id[0] = '\0'; + king_system_recovery_mode = KING_SYSTEM_RECOVERY_MODE_NONE; + king_system_recovery_plan_id[0] = '\0'; + king_system_recovery_plan_requested_at = 0; + king_system_recovery_plan_window_seconds = 0; + snprintf( + king_system_coordinator_state_status, + sizeof(king_system_coordinator_state_status), + "%s", + "inactive" + ); +} + +static void king_system_apply_default_config(king_system_config_t *config) +{ + memset(config, 0, sizeof(king_system_config_t)); + config->enabled = true; + config->max_concurrent_requests = 1024; + config->health_check_interval_seconds = 5; + config->component_timeout_seconds = 1; + config->enable_cross_component_tracing = true; + config->enable_performance_monitoring = true; + config->enable_auto_scaling = true; + config->enable_circuit_breaker = true; + config->cluster_id[0] = '\0'; + config->node_id[0] = '\0'; + config->state_root_path[0] = '\0'; + strncpy(config->environment, "development", sizeof(config->environment) - 1); + ZVAL_UNDEF(&config->global_configuration); +} + +static const char *king_system_drain_reason_to_string( + king_system_drain_reason_t reason +) +{ + switch (reason) { + case KING_SYSTEM_DRAIN_REASON_COMPONENT_RESTART: + return "component_restart"; + case KING_SYSTEM_DRAIN_REASON_COMPONENT_RECOVERY: + return "component_recovery"; + case KING_SYSTEM_DRAIN_REASON_SYSTEM_SHUTDOWN: + return "system_shutdown"; + case KING_SYSTEM_DRAIN_REASON_NONE: + default: + return "none"; + } +} + +static uint32_t king_system_recovery_window_seconds( + king_system_recovery_mode_t mode +) +{ + switch (mode) { + case KING_SYSTEM_RECOVERY_MODE_NODE: + return 30; + case KING_SYSTEM_RECOVERY_MODE_COMPONENT: + return 15; + case KING_SYSTEM_RECOVERY_MODE_NONE: + default: + return 0; + } +} + +static const char *king_system_recovery_mode_to_string( + king_system_recovery_mode_t mode +) +{ + switch (mode) { + case KING_SYSTEM_RECOVERY_MODE_NODE: + return "node_failure"; + case KING_SYSTEM_RECOVERY_MODE_COMPONENT: + return "component_failure"; + case KING_SYSTEM_RECOVERY_MODE_NONE: + default: + return "none"; + } +} + +static void king_system_start_recovery_plan( + king_system_recovery_mode_t mode, + const char *source_node_id +) +{ + king_system_recovery_mode = mode; + king_system_recovery_plan_requested_at = time(NULL); + king_system_recovery_plan_window_seconds = king_system_recovery_window_seconds(mode); + if (source_node_id != NULL && source_node_id[0] != '\0') { + snprintf( + king_system_recovery_source_node_id, + sizeof(king_system_recovery_source_node_id), + "%s", + source_node_id + ); + } else { + king_system_recovery_source_node_id[0] = '\0'; + } + + snprintf( + king_system_recovery_plan_id, + sizeof(king_system_recovery_plan_id), + "%s:%s:" ZEND_LONG_FMT ":%llu:%llu", + king_system_recovery_mode_to_string(mode), + king_system_recovery_source_node_id[0] != '\0' + ? king_system_recovery_source_node_id + : "local", + (zend_long) king_system_recovery_plan_requested_at, + (unsigned long long) king_system_coordinator_state_version, + (unsigned long long) king_system_coordinator_generation + ); +} + +static const char *king_system_recovery_reason_to_string(void) +{ + return king_system_recovery_mode_to_string(king_system_recovery_mode); +} + +static void king_system_apply_config(king_system_config_t *config, zval *config_arr) +{ + zval *value = NULL; + HashTable *ht = Z_ARRVAL_P(config_arr); + + value = zend_hash_str_find(ht, "environment", strlen("environment")); + if (value && Z_TYPE_P(value) == IS_STRING && Z_STRLEN_P(value) > 0) { + strncpy( + config->environment, + Z_STRVAL_P(value), + sizeof(config->environment) - 1 + ); + } + + value = zend_hash_str_find(ht, "cluster_id", strlen("cluster_id")); + if (value && Z_TYPE_P(value) == IS_STRING && Z_STRLEN_P(value) > 0) { + strncpy( + config->cluster_id, + Z_STRVAL_P(value), + sizeof(config->cluster_id) - 1 + ); + } + + value = zend_hash_str_find(ht, "node_id", strlen("node_id")); + if (value && Z_TYPE_P(value) == IS_STRING && Z_STRLEN_P(value) > 0) { + strncpy( + config->node_id, + Z_STRVAL_P(value), + sizeof(config->node_id) - 1 + ); + } + + value = zend_hash_str_find(ht, "state_root_path", strlen("state_root_path")); + if (value && Z_TYPE_P(value) == IS_STRING && Z_STRLEN_P(value) > 0) { + strncpy( + config->state_root_path, + Z_STRVAL_P(value), + sizeof(config->state_root_path) - 1 + ); + } + + value = zend_hash_str_find( + ht, + "component_timeout_seconds", + strlen("component_timeout_seconds") + ); + if (value && Z_TYPE_P(value) != IS_UNDEF) { + config->component_timeout_seconds = (uint32_t) MAX( + 1, + (uint32_t) zval_get_long(value) + ); + } + + value = zend_hash_str_find(ht, "max_concurrent_requests", strlen("max_concurrent_requests")); + if (value && Z_TYPE_P(value) != IS_UNDEF) { + config->max_concurrent_requests = (uint32_t) MAX( + 1, + (uint32_t) zval_get_long(value) + ); + } + + value = zend_hash_str_find(ht, "health_check_interval_seconds", strlen("health_check_interval_seconds")); + if (value && Z_TYPE_P(value) != IS_UNDEF) { + config->health_check_interval_seconds = (uint32_t) MAX( + 1, + (uint32_t) zval_get_long(value) + ); + } +} + +static void king_system_apply_default_node_identity(king_system_config_t *config) +{ + if (config == NULL || config->state_root_path[0] == '\0') { + return; + } + + if (config->cluster_id[0] == '\0') { + strncpy(config->cluster_id, "local-cluster", sizeof(config->cluster_id) - 1); + } + + if (config->node_id[0] == '\0') { + strncpy(config->node_id, "local-node", sizeof(config->node_id) - 1); + } +} diff --git a/extension/src/integration/system_integration/coordinator_state.inc b/extension/src/integration/system_integration/coordinator_state.inc new file mode 100644 index 000000000..9c6519845 --- /dev/null +++ b/extension/src/integration/system_integration/coordinator_state.inc @@ -0,0 +1,433 @@ +static void king_system_build_coordinator_dir_path(char *dest, size_t dest_len) +{ + if (dest == NULL || dest_len == 0) { + return; + } + + if (king_system_runtime_config.state_root_path[0] == '\0') { + dest[0] = '\0'; + return; + } + + snprintf( + dest, + dest_len, + "%s/.king-system", + king_system_runtime_config.state_root_path + ); +} + +static void king_system_build_coordinator_state_path(char *dest, size_t dest_len) +{ + char coordinator_dir[PATH_MAX]; + + if (dest == NULL || dest_len == 0) { + return; + } + + king_system_build_coordinator_dir_path( + coordinator_dir, + sizeof(coordinator_dir) + ); + if (coordinator_dir[0] == '\0') { + dest[0] = '\0'; + return; + } + + snprintf(dest, dest_len, "%s/coordinator.state", coordinator_dir); +} + +static int king_system_ensure_directory_recursive(const char *path) +{ + char current[PATH_MAX]; + size_t len; + size_t idx; + + if (path == NULL || path[0] == '\0') { + return FAILURE; + } + + len = strlen(path); + if (len >= sizeof(current)) { + return FAILURE; + } + + memcpy(current, path, len + 1); + for (idx = 1; idx < len; idx++) { + if (current[idx] != '/') { + continue; + } + + current[idx] = '\0'; + if (mkdir(current, 0755) != 0 && errno != EEXIST) { + return FAILURE; + } + current[idx] = '/'; + } + + if (mkdir(current, 0755) != 0 && errno != EEXIST) { + return FAILURE; + } + + return SUCCESS; +} + +static int king_system_write_coordinator_state( + const char *state_path, + uint64_t version, + uint64_t generation, + time_t created_at, + const char *cluster_id, + const char *active_node_id, + zend_bool clean_shutdown +) +{ + char payload[1024]; + char temp_path[PATH_MAX]; + FILE *file; + int payload_len; + + if (state_path == NULL || state_path[0] == '\0') { + return FAILURE; + } + + payload_len = snprintf( + payload, + sizeof(payload), + "version=%" PRIu64 "\n" + "generation=%" PRIu64 "\n" + "created_at=%" PRIu64 "\n" + "updated_at=%" PRIu64 "\n" + "cluster_id=%s\n" + "active_node_id=%s\n" + "clean_shutdown=%d\n", + version, + generation, + (uint64_t) created_at, + (uint64_t) time(NULL), + cluster_id != NULL ? cluster_id : "", + active_node_id != NULL ? active_node_id : "", + clean_shutdown ? 1 : 0 + ); + if (payload_len < 0 || (size_t) payload_len >= sizeof(payload)) { + return FAILURE; + } + + snprintf(temp_path, sizeof(temp_path), "%s.tmp." ZEND_LONG_FMT, state_path, (zend_long) getpid()); + file = fopen(temp_path, "wb"); + if (file == NULL) { + return FAILURE; + } + + if ( + fwrite(payload, 1, (size_t) payload_len, file) != (size_t) payload_len + || fflush(file) != 0 + || fsync(fileno(file)) != 0 + ) { + fclose(file); + unlink(temp_path); + return FAILURE; + } + + if (fclose(file) != 0) { + unlink(temp_path); + return FAILURE; + } + + if (rename(temp_path, state_path) != 0) { + unlink(temp_path); + return FAILURE; + } + + return SUCCESS; +} + +static int king_system_load_coordinator_state( + const char *state_path, + uint64_t *version_out, + uint64_t *generation_out, + time_t *created_at_out, + char *cluster_id_out, + size_t cluster_id_out_len, + char *active_node_id_out, + size_t active_node_id_out_len, + zend_bool *clean_shutdown_out +) +{ + char buffer[2048]; + char *line; + char *saveptr = NULL; + FILE *file; + uint64_t created_at = 0; + uint64_t generation = 0; + zend_bool clean_shutdown = 0; + uint64_t version = 0; + int has_clean_shutdown = 0; + int has_created_at = 0; + int has_generation = 0; + int has_version = 0; + + if ( + state_path == NULL + || version_out == NULL + || generation_out == NULL + || created_at_out == NULL + || cluster_id_out == NULL + || cluster_id_out_len == 0 + || active_node_id_out == NULL + || active_node_id_out_len == 0 + || clean_shutdown_out == NULL + ) { + return FAILURE; + } + + file = fopen(state_path, "rb"); + if (file == NULL) { + return FAILURE; + } + + memset(buffer, 0, sizeof(buffer)); + if (fread(buffer, 1, sizeof(buffer) - 1, file) == 0 && ferror(file)) { + fclose(file); + return FAILURE; + } + fclose(file); + + cluster_id_out[0] = '\0'; + active_node_id_out[0] = '\0'; + + for (line = strtok_r(buffer, "\n", &saveptr); + line != NULL; + line = strtok_r(NULL, "\n", &saveptr)) { + char *eq = strchr(line, '='); + const char *value = NULL; + size_t value_len = 0; + + if (eq == NULL) { + continue; + } + + *eq = '\0'; + value = eq + 1; + value_len = strlen(value); + if (strcmp(line, "version") == 0) { + version = strtoull(value, NULL, 10); + has_version = 1; + } else if (strcmp(line, "generation") == 0) { + generation = strtoull(value, NULL, 10); + has_generation = 1; + } else if (strcmp(line, "created_at") == 0) { + created_at = strtoull(value, NULL, 10); + has_created_at = 1; + } else if (strcmp(line, "cluster_id") == 0) { + if (value_len >= cluster_id_out_len) { + return FAILURE; + } + memcpy(cluster_id_out, value, value_len + 1); + } else if (strcmp(line, "active_node_id") == 0) { + if (value_len >= active_node_id_out_len) { + return FAILURE; + } + memcpy(active_node_id_out, value, value_len + 1); + } else if (strcmp(line, "clean_shutdown") == 0) { + clean_shutdown = atoi(value) != 0 ? 1 : 0; + has_clean_shutdown = 1; + } + } + + if ( + !has_version + || !has_generation + || !has_created_at + || !has_clean_shutdown + || version == 0 + || generation == 0 + ) { + return FAILURE; + } + + *version_out = version; + *generation_out = generation; + *created_at_out = (time_t) created_at; + *clean_shutdown_out = clean_shutdown; + return SUCCESS; +} + +static int king_system_initialize_coordinator_state(void) +{ + char coordinator_dir[PATH_MAX]; + char loaded_cluster_id[64]; + char loaded_node_id[64]; + char state_path[PATH_MAX]; + time_t created_at; + time_t now; + uint64_t generation; + uint64_t version; + zend_bool clean_shutdown = 0; + + king_system_reset_coordinator_state_runtime(); + + if (king_system_runtime_config.state_root_path[0] == '\0') { + return SUCCESS; + } + + king_system_build_coordinator_dir_path( + coordinator_dir, + sizeof(coordinator_dir) + ); + king_system_build_coordinator_state_path( + state_path, + sizeof(state_path) + ); + + if (king_system_ensure_directory_recursive(coordinator_dir) != SUCCESS) { + snprintf( + king_system_coordinator_state_status, + sizeof(king_system_coordinator_state_status), + "%s", + "failed" + ); + snprintf( + king_system_coordinator_state_error, + sizeof(king_system_coordinator_state_error), + "Could not create system coordinator directory '%s'.", + coordinator_dir + ); + return FAILURE; + } + + now = time(NULL); + if (access(state_path, F_OK) != 0) { + version = 1; + generation = 1; + created_at = now; + } else { + if (king_system_load_coordinator_state( + state_path, + &version, + &generation, + &created_at, + loaded_cluster_id, + sizeof(loaded_cluster_id), + loaded_node_id, + sizeof(loaded_node_id), + &clean_shutdown + ) != SUCCESS) { + snprintf( + king_system_coordinator_state_status, + sizeof(king_system_coordinator_state_status), + "%s", + "failed" + ); + snprintf( + king_system_coordinator_state_error, + sizeof(king_system_coordinator_state_error), + "System coordinator state at '%s' is unreadable or corrupted.", + state_path + ); + return FAILURE; + } + + if ( + loaded_cluster_id[0] != '\0' + && king_system_runtime_config.cluster_id[0] != '\0' + && strcmp(loaded_cluster_id, king_system_runtime_config.cluster_id) != 0 + ) { + snprintf( + king_system_coordinator_state_status, + sizeof(king_system_coordinator_state_status), + "%s", + "failed" + ); + snprintf( + king_system_coordinator_state_error, + sizeof(king_system_coordinator_state_error), + "System coordinator state at '%s' belongs to cluster '%s', not '%s'.", + state_path, + loaded_cluster_id, + king_system_runtime_config.cluster_id + ); + return FAILURE; + } + + if (!clean_shutdown && loaded_node_id[0] != '\0') { + king_system_coordinator_state_recovered = true; + king_system_start_recovery_plan( + KING_SYSTEM_RECOVERY_MODE_NODE, + loaded_node_id + ); + } + + generation++; + } + + if (king_system_write_coordinator_state( + state_path, + version, + generation, + created_at, + king_system_runtime_config.cluster_id, + king_system_runtime_config.node_id, + 0 + ) != SUCCESS) { + snprintf( + king_system_coordinator_state_status, + sizeof(king_system_coordinator_state_status), + "%s", + "failed" + ); + snprintf( + king_system_coordinator_state_error, + sizeof(king_system_coordinator_state_error), + "Could not persist system coordinator state at '%s'.", + state_path + ); + return FAILURE; + } + + king_system_coordinator_state_present = true; + king_system_coordinator_state_version = version; + king_system_coordinator_generation = generation; + king_system_coordinator_created_at = created_at; + king_system_coordinator_last_loaded_at = now; + strncpy( + king_system_coordinator_state_path, + state_path, + sizeof(king_system_coordinator_state_path) - 1 + ); + snprintf( + king_system_coordinator_state_status, + sizeof(king_system_coordinator_state_status), + "%s", + king_system_coordinator_state_recovered ? "recovered" : "initialized" + ); + return SUCCESS; +} + +static void king_system_mark_coordinator_clean_shutdown(void) +{ + if ( + !king_system_coordinator_state_present + || king_system_coordinator_state_path[0] == '\0' + ) { + return; + } + + if (king_system_write_coordinator_state( + king_system_coordinator_state_path, + king_system_coordinator_state_version > 0 + ? king_system_coordinator_state_version + : 1, + king_system_coordinator_generation > 0 + ? king_system_coordinator_generation + : 1, + king_system_coordinator_created_at > 0 + ? king_system_coordinator_created_at + : time(NULL), + king_system_runtime_config.cluster_id, + king_system_runtime_config.node_id, + 1 + ) == SUCCESS) { + king_system_coordinator_last_loaded_at = time(NULL); + } +} diff --git a/extension/src/integration/system_integration/lifecycle_runtime.inc b/extension/src/integration/system_integration/lifecycle_runtime.inc new file mode 100644 index 000000000..090d35257 --- /dev/null +++ b/extension/src/integration/system_integration/lifecycle_runtime.inc @@ -0,0 +1,200 @@ +/* + * Start every currently eligible component wave once its declared startup + * dependencies are already running. This keeps the canonical graph from #13 + * honest in the local runtime instead of leaving it as pure metadata. + */ +static void king_system_schedule_startup_components(void) +{ + const king_system_startup_entry_t *entry; + king_component_info_t *info; + size_t idx; + time_t now; + + if (!king_system_initialized) { + return; + } + + now = time(NULL); + for ( + idx = 0; + idx < sizeof(king_system_startup_plan) / sizeof(king_system_startup_plan[0]); + idx++ + ) { + entry = &king_system_startup_plan[idx]; + info = king_system_get_component_internal(entry->type); + if (info == NULL) { + continue; + } + + if ( + info->status != KING_COMPONENT_STATUS_UNINITIALIZED && + info->status != KING_COMPONENT_STATUS_SHUTDOWN + ) { + continue; + } + + if (!king_system_component_dependencies_running(entry->type)) { + continue; + } + + info->status = KING_COMPONENT_STATUS_INITIALIZING; + info->last_health_check = now; + } +} + +/* + * During a coordinated shutdown, stop only components whose dependents are + * already inactive. This preserves the canonical reverse-startup order and + * keeps the runtime in a visible drain state until the final root component + * can stop safely. + */ +static void king_system_schedule_shutdown_components(void) +{ + const king_system_startup_entry_t *entry; + king_component_info_t *info; + size_t idx; + time_t now; + zval pending_dependents; + + if ( + !king_system_initialized + || (!king_system_shutdown_requested && !king_system_recovery_requested) + ) { + return; + } + + now = time(NULL); + for ( + idx = 0; + idx < sizeof(king_system_shutdown_plan) / sizeof(king_system_shutdown_plan[0]); + idx++ + ) { + entry = &king_system_shutdown_plan[idx]; + info = king_system_get_component_internal(entry->type); + if (info == NULL) { + continue; + } + + if (!king_system_component_started(info->status)) { + continue; + } + + if (info->status == KING_COMPONENT_STATUS_SHUTTING_DOWN) { + continue; + } + + king_system_build_component_pending_shutdown_dependents( + &pending_dependents, + entry->type + ); + if (zend_hash_num_elements(Z_ARRVAL(pending_dependents)) == 0) { + info->status = KING_COMPONENT_STATUS_SHUTTING_DOWN; + info->last_health_check = now; + } + zval_ptr_dtor(&pending_dependents); + } +} + +/* + * Publish the repo-local lifecycle graph explicitly so callers do not have to + * infer the current state's next legal aggregate transitions themselves. + */ +static void king_system_build_allowed_lifecycle_transitions( + zval *transitions, + const char *lifecycle +) +{ + array_init(transitions); + + if (lifecycle == NULL || strcmp(lifecycle, "stopped") == 0) { + add_next_index_string(transitions, "ready"); + return; + } + + if (strcmp(lifecycle, "ready") == 0) { + add_next_index_string(transitions, "draining"); + add_next_index_string(transitions, "failed"); + add_next_index_string(transitions, "stopped"); + return; + } + + if (strcmp(lifecycle, "draining") == 0) { + if (!king_system_shutdown_requested) { + add_next_index_string(transitions, "starting"); + } + add_next_index_string(transitions, "failed"); + add_next_index_string(transitions, "stopped"); + return; + } + + if (strcmp(lifecycle, "starting") == 0) { + add_next_index_string(transitions, "ready"); + add_next_index_string(transitions, "draining"); + add_next_index_string(transitions, "failed"); + add_next_index_string(transitions, "stopped"); + return; + } + + if (strcmp(lifecycle, "failed") == 0) { + add_next_index_string(transitions, "draining"); + add_next_index_string(transitions, "stopped"); + } +} + +static void king_system_collect_admission_state( + king_system_admission_state_t *state +) +{ + king_component_info_t *info; + zend_ulong idx; + bool draining_request = king_system_shutdown_requested || king_system_recovery_requested; + + memset(state, 0, sizeof(*state)); + state->lifecycle = "stopped"; + + if (!king_system_initialized) { + return; + } + + state->component_count = (uint32_t) zend_hash_num_elements(&king_system_components); + ZEND_HASH_FOREACH_NUM_KEY_PTR(&king_system_components, idx, info) { + switch (info->status) { + case KING_COMPONENT_STATUS_RUNNING: + state->ready_count++; + break; + case KING_COMPONENT_STATUS_SHUTTING_DOWN: + state->draining_count++; + state->has_draining = true; + break; + case KING_COMPONENT_STATUS_INITIALIZING: + case KING_COMPONENT_STATUS_UNINITIALIZED: + case KING_COMPONENT_STATUS_SHUTDOWN: + state->has_starting = true; + break; + case KING_COMPONENT_STATUS_ERROR: + state->has_error = true; + break; + } + + if (king_system_component_readiness_blocking(info->status)) { + state->readiness_blocker_count++; + } + } ZEND_HASH_FOREACH_END(); + + if (draining_request && (state->has_draining || state->has_error)) { + state->lifecycle = "draining"; + } else { + state->lifecycle = king_system_resolve_lifecycle( + king_system_initialized, + state->component_count, + state->ready_count, + state->has_starting, + state->has_draining, + state->has_error + ); + } + state->aggregate_ready = king_system_initialized + && state->component_count > 0 + && state->readiness_blocker_count == 0 + && strcmp(state->lifecycle, "ready") == 0; +} diff --git a/extension/src/integration/system_integration/procedural_api.inc b/extension/src/integration/system_integration/procedural_api.inc new file mode 100644 index 000000000..79677a5b7 --- /dev/null +++ b/extension/src/integration/system_integration/procedural_api.inc @@ -0,0 +1,501 @@ +PHP_FUNCTION(king_system_init) +{ + zval *config_arr; + if (zend_parse_parameters(1, "a", &config_arr) == FAILURE) RETURN_FALSE; + + king_system_config_t config; + king_system_apply_default_config(&config); + king_system_apply_config(&config, config_arr); + + if (king_system_init_all_components(&config) == SUCCESS) { + RETURN_TRUE; + } + + RETURN_FALSE; +} + +PHP_FUNCTION(king_system_process_request) +{ + zval *request_data; + if (zend_parse_parameters(1, "a", &request_data) == FAILURE) RETURN_FALSE; + + if (!king_system_initialized) { + king_set_error( + "king_system_process_request() cannot process requests because the coordinated runtime is not initialized." + ); + RETURN_FALSE; + } + + if ( + king_system_require_admission( + "king_system_process_request", + "process_requests" + ) != SUCCESS + ) { + RETURN_FALSE; + } + + (void) request_data; + { + king_component_info_t *info; + zend_ulong idx; + ZEND_HASH_FOREACH_NUM_KEY_PTR(&king_system_components, idx, info) { + info->requests_handled++; + info->last_health_check = time(NULL); + } ZEND_HASH_FOREACH_END(); + } + + king_system_apply_all_transitions(); + RETURN_TRUE; +} + +PHP_FUNCTION(king_system_restart_component) +{ + char *name; + size_t name_len; + if (zend_parse_parameters(1, "s", &name, &name_len) == FAILURE) RETURN_FALSE; + + king_component_info_t *info = king_system_get_component_by_name(name); + if (!king_system_initialized || info == NULL) { + RETURN_FALSE; + } + + (void) name_len; + + info->status = KING_COMPONENT_STATUS_SHUTTING_DOWN; + info->last_health_check = time(NULL); + if (!king_system_shutdown_requested) { + king_system_drain_reason = KING_SYSTEM_DRAIN_REASON_COMPONENT_RESTART; + } + + RETURN_TRUE; +} + +PHP_FUNCTION(king_system_fail_component) +{ + char *name; + size_t name_len; + king_component_info_t *info; + + if (zend_parse_parameters(1, "s", &name, &name_len) == FAILURE) { + RETURN_FALSE; + } + + (void) name_len; + + info = king_system_get_component_by_name(name); + if (!king_system_initialized || info == NULL) { + RETURN_FALSE; + } + + RETURN_BOOL( + king_system_handle_component_error( + info->type, + "manual component failure" + ) == SUCCESS + ); +} + +PHP_FUNCTION(king_system_recover) +{ + king_system_admission_state_t state; + + if (!king_system_initialized || king_system_shutdown_requested) { + RETURN_FALSE; + } + + if (king_system_recovery_requested) { + RETURN_TRUE; + } + + king_system_collect_admission_state(&state); + if (strcmp(state.lifecycle, "failed") != 0) { + RETURN_FALSE; + } + + king_system_recovery_requested = true; + king_system_start_recovery_plan( + KING_SYSTEM_RECOVERY_MODE_COMPONENT, + king_system_runtime_config.node_id + ); + king_system_drain_reason = KING_SYSTEM_DRAIN_REASON_COMPONENT_RECOVERY; + king_system_apply_all_transitions(); + RETURN_TRUE; +} + +PHP_FUNCTION(king_system_shutdown) +{ + if (!king_system_initialized) { + RETURN_TRUE; + } + + king_system_shutdown_requested = true; + king_system_drain_reason = KING_SYSTEM_DRAIN_REASON_SYSTEM_SHUTDOWN; + king_system_apply_all_transitions(); + RETURN_TRUE; +} + +PHP_FUNCTION(king_system_get_status) +{ + ZEND_PARSE_PARAMETERS_NONE(); + zval admission; + zval allowed_lifecycle_transitions; + zval blocker_entry; + zval component_entry; + zval components; + zval drain_intent; + zval drain_targets; + zval readiness_blockers; + zval recovery; + zval shutdown; + zval startup; + time_t now; + time_t drain_requested_at = 0; + bool recovery_active = false; + king_system_admission_state_t admission_state; + zend_ulong idx; + uint32_t drain_target_count = 0; + king_component_info_t *info; + + now = time(NULL); + king_system_apply_all_transitions(); + king_system_collect_admission_state(&admission_state); + recovery_active = ( + king_system_recovery_mode != KING_SYSTEM_RECOVERY_MODE_NONE + && king_system_initialized + && strcmp(admission_state.lifecycle, "ready") != 0 + ) ? 1 : 0; + + array_init(&components); + array_init(&drain_targets); + array_init(&readiness_blockers); + if (king_system_initialized) { + ZEND_HASH_FOREACH_NUM_KEY_PTR(&king_system_components, idx, info) { + uint32_t shutdown_pending_dependent_count = 0; + time_t last_health_check = info->last_health_check; + uint32_t startup_pending_dependency_count = 0; + zend_bool readiness_blocking = king_system_component_readiness_blocking( + info->status + ); + const char *readiness_reason = king_system_component_readiness_reason( + info->status + ); + const king_system_startup_entry_t *shutdown_entry = + king_system_get_shutdown_entry(info->type); + const king_system_startup_entry_t *startup_entry = + king_system_get_startup_entry(info->type); + zval shutdown_dependents; + zval shutdown_pending_dependents; + zval startup_dependencies; + zval startup_pending_dependencies; + + king_system_build_component_shutdown_dependents( + &shutdown_dependents, + info->type + ); + king_system_build_component_pending_shutdown_dependents( + &shutdown_pending_dependents, + info->type + ); + shutdown_pending_dependent_count = (uint32_t) zend_hash_num_elements( + Z_ARRVAL(shutdown_pending_dependents) + ); + king_system_build_component_startup_dependencies( + &startup_dependencies, + info->type + ); + king_system_build_component_pending_startup_dependencies( + &startup_pending_dependencies, + info->type + ); + startup_pending_dependency_count = (uint32_t) zend_hash_num_elements( + Z_ARRVAL(startup_pending_dependencies) + ); + + array_init(&component_entry); + add_assoc_string(&component_entry, "status", king_component_status_to_string(info->status)); + add_assoc_bool( + &component_entry, + "ready", + info->status == KING_COMPONENT_STATUS_RUNNING ? 1 : 0 + ); + add_assoc_string(&component_entry, "readiness_reason", (char *) readiness_reason); + add_assoc_bool( + &component_entry, + "readiness_blocking", + readiness_blocking + ); + add_assoc_long(&component_entry, "requests_handled", (zend_long) info->requests_handled); + add_assoc_long(&component_entry, "errors_encountered", (zend_long) info->errors_encountered); + add_assoc_long(&component_entry, "last_health_check", (zend_long) last_health_check); + add_assoc_long(&component_entry, "up_for_seconds", (zend_long) (now - last_health_check)); + add_assoc_long( + &component_entry, + "shutdown_order", + shutdown_entry != NULL ? (zend_long) shutdown_entry->order : 0 + ); + add_assoc_zval( + &component_entry, + "shutdown_dependents", + &shutdown_dependents + ); + add_assoc_zval( + &component_entry, + "shutdown_pending_dependents", + &shutdown_pending_dependents + ); + add_assoc_bool( + &component_entry, + "shutdown_ready_to_stop", + king_system_component_started(info->status) && + shutdown_pending_dependent_count == 0 + ); + add_assoc_long( + &component_entry, + "startup_order", + startup_entry != NULL ? (zend_long) startup_entry->order : 0 + ); + add_assoc_zval( + &component_entry, + "startup_dependencies", + &startup_dependencies + ); + add_assoc_zval( + &component_entry, + "startup_pending_dependencies", + &startup_pending_dependencies + ); + add_assoc_bool( + &component_entry, + "startup_ready_to_start", + !king_system_component_started(info->status) && + startup_pending_dependency_count == 0 + ); + + if ( + ((king_system_shutdown_requested || king_system_recovery_requested) && + king_system_component_started(info->status)) || + info->status == KING_COMPONENT_STATUS_SHUTTING_DOWN + ) { + add_next_index_string(&drain_targets, info->name); + drain_target_count++; + if (drain_requested_at == 0 || last_health_check < drain_requested_at) { + drain_requested_at = last_health_check; + } + } + + if (readiness_blocking) { + array_init(&blocker_entry); + add_assoc_string( + &blocker_entry, + "status", + king_component_status_to_string(info->status) + ); + add_assoc_string( + &blocker_entry, + "readiness_reason", + (char *) readiness_reason + ); + add_assoc_zval(&readiness_blockers, info->name, &blocker_entry); + } + + add_assoc_zval(&components, info->name, &component_entry); + } ZEND_HASH_FOREACH_END(); + } + + array_init(return_value); + add_assoc_bool(return_value, "initialized", king_system_initialized); + add_assoc_string(return_value, "lifecycle", (char *) admission_state.lifecycle); + add_assoc_long(return_value, "component_count", admission_state.component_count); + add_assoc_zval(return_value, "components", &components); + add_assoc_long(return_value, "components_ready", (zend_long) admission_state.ready_count); + add_assoc_long( + return_value, + "components_draining", + (zend_long) admission_state.draining_count + ); + add_assoc_long( + return_value, + "readiness_blocker_count", + (zend_long) admission_state.readiness_blocker_count + ); + add_assoc_zval(return_value, "readiness_blockers", &readiness_blockers); + array_init(&drain_intent); + add_assoc_bool( + &drain_intent, + "requested", + (king_system_shutdown_requested + || king_system_recovery_requested + || admission_state.has_draining) ? 1 : 0 + ); + add_assoc_bool( + &drain_intent, + "active", + strcmp(admission_state.lifecycle, "draining") == 0 ? 1 : 0 + ); + add_assoc_string( + &drain_intent, + "reason", + (char *) ( + king_system_shutdown_requested + || king_system_recovery_requested + || admission_state.has_draining + ? king_system_drain_reason_to_string(king_system_drain_reason) + : "none" + ) + ); + add_assoc_long(&drain_intent, "requested_at", (zend_long) drain_requested_at); + if (king_system_shutdown_requested) { + add_assoc_string(&drain_intent, "target_lifecycle", "stopped"); + } else if (king_system_recovery_requested) { + add_assoc_string(&drain_intent, "target_lifecycle", "ready"); + } else if (admission_state.has_draining) { + add_assoc_string(&drain_intent, "target_lifecycle", "starting"); + } else { + add_assoc_null(&drain_intent, "target_lifecycle"); + } + add_assoc_long( + &drain_intent, + "target_component_count", + (zend_long) drain_target_count + ); + add_assoc_zval(&drain_intent, "target_components", &drain_targets); + add_assoc_zval(return_value, "drain_intent", &drain_intent); + king_system_build_allowed_lifecycle_transitions( + &allowed_lifecycle_transitions, + admission_state.lifecycle + ); + add_assoc_zval( + return_value, + "allowed_lifecycle_transitions", + &allowed_lifecycle_transitions + ); + array_init(&recovery); + add_assoc_bool( + &recovery, + "active", + recovery_active + ); + add_assoc_bool( + &recovery, + "recovered", + king_system_coordinator_state_recovered ? 1 : 0 + ); + add_assoc_string( + &recovery, + "reason", + (char *) king_system_recovery_reason_to_string() + ); + add_assoc_string( + &recovery, + "mode", + king_system_recovery_mode_to_string(king_system_recovery_mode) + ); + if (king_system_recovery_plan_id[0] != '\0') { + add_assoc_string(&recovery, "plan_id", king_system_recovery_plan_id); + } else { + add_assoc_null(&recovery, "plan_id"); + } + add_assoc_long( + &recovery, + "plan_requested_at", + (zend_long) king_system_recovery_plan_requested_at + ); + add_assoc_long( + &recovery, + "plan_window_seconds", + (zend_long) king_system_recovery_plan_window_seconds + ); + if (king_system_recovery_source_node_id[0] != '\0') { + add_assoc_string( + &recovery, + "source_node_id", + king_system_recovery_source_node_id + ); + } else { + add_assoc_null(&recovery, "source_node_id"); + } + if (king_system_runtime_config.node_id[0] != '\0') { + add_assoc_string( + &recovery, + "active_node_id", + king_system_runtime_config.node_id + ); + } else { + add_assoc_null(&recovery, "active_node_id"); + } + if (king_system_runtime_config.cluster_id[0] != '\0') { + add_assoc_string( + &recovery, + "cluster_id", + king_system_runtime_config.cluster_id + ); + } else { + add_assoc_null(&recovery, "cluster_id"); + } + add_assoc_bool( + &recovery, + "coordinator_state_present", + king_system_coordinator_state_present ? 1 : 0 + ); + add_assoc_string( + &recovery, + "coordinator_state_status", + king_system_coordinator_state_status + ); + if (king_system_coordinator_state_path[0] != '\0') { + add_assoc_string( + &recovery, + "coordinator_state_path", + king_system_coordinator_state_path + ); + } else { + add_assoc_null(&recovery, "coordinator_state_path"); + } + add_assoc_long( + &recovery, + "coordinator_state_version", + (zend_long) king_system_coordinator_state_version + ); + add_assoc_long( + &recovery, + "coordinator_generation", + (zend_long) king_system_coordinator_generation + ); + add_assoc_long( + &recovery, + "coordinator_created_at", + (zend_long) king_system_coordinator_created_at + ); + add_assoc_long( + &recovery, + "coordinator_last_loaded_at", + (zend_long) king_system_coordinator_last_loaded_at + ); + add_assoc_string( + &recovery, + "coordinator_state_error", + king_system_coordinator_state_error + ); + add_assoc_zval(return_value, "recovery", &recovery); + king_system_build_startup_visibility(&startup); + add_assoc_zval(return_value, "startup", &startup); + king_system_build_shutdown_visibility(&shutdown); + add_assoc_zval(return_value, "shutdown", &shutdown); + array_init(&admission); + add_assoc_bool(&admission, "process_requests", admission_state.aggregate_ready); + add_assoc_bool(&admission, "http_listener_accepts", admission_state.aggregate_ready); + add_assoc_bool(&admission, "websocket_upgrades", admission_state.aggregate_ready); + add_assoc_bool(&admission, "websocket_peer_accepts", admission_state.aggregate_ready); + add_assoc_bool(&admission, "orchestrator_submissions", admission_state.aggregate_ready); + add_assoc_bool(&admission, "file_worker_claims", admission_state.aggregate_ready); + add_assoc_bool(&admission, "file_worker_resumes", admission_state.aggregate_ready); + add_assoc_bool(&admission, "remote_peer_dispatches", admission_state.aggregate_ready); + add_assoc_bool(&admission, "remote_peer_resumes", admission_state.aggregate_ready); + add_assoc_zval(return_value, "admission", &admission); + add_assoc_long( + return_value, + "health_check_interval_seconds", + (zend_long) king_system_runtime_config.health_check_interval_seconds + ); +} diff --git a/extension/src/king_globals.c b/extension/src/king_globals.c index b948530a7..cf3418b15 100644 --- a/extension/src/king_globals.c +++ b/extension/src/king_globals.c @@ -6,12 +6,12 @@ * * PURPOSE: * Defines the single global state instance for the king extension. - * This file must be compiled exactly once. Including king_globals.h + * This file must be compiled exactly once. Including php_king/globals.h * anywhere else only provides an extern declaration. * ========================================================================= */ -#include "include/king_globals.h" +#include "php_king/globals.h" /* * The single authoritative instance of the extension's global state. diff --git a/extension/src/king_init.c b/extension/src/king_init.c index e98eb9f0f..1fceb127c 100644 --- a/extension/src/king_init.c +++ b/extension/src/king_init.c @@ -1,28 +1,28 @@ -#include "include/king_init.h" -#include "include/config/security_and_traffic/index.h" -#include "include/config/tls_and_crypto/index.h" -#include "include/config/tcp_transport/index.h" -#include "include/config/quic_transport/index.h" -#include "include/config/http2/index.h" -#include "include/config/app_http3_websockets_webtransport/index.h" -#include "include/config/cluster_and_process/index.h" -#include "include/config/bare_metal_tuning/index.h" -#include "include/config/cloud_autoscale/index.h" -#include "include/config/dynamic_admin_api/index.h" -#include "include/config/native_cdn/index.h" -#include "include/config/native_object_store/index.h" -#include "include/config/open_telemetry/index.h" -#include "include/config/router_and_loadbalancer/index.h" -#include "include/config/state_management/index.h" -#include "include/config/smart_dns/index.h" -#include "include/config/iibin/index.h" -#include "include/config/mcp_and_orchestrator/index.h" -#include "include/config/high_perf_compute_and_ai/index.h" -#include "include/config/semantic_geometry/index.h" -#include "include/config/smart_contracts/index.h" -#include "include/config/ssh_over_quic/index.h" -#include "include/config/tls_and_crypto/base_layer.h" -#include "include/king_globals.h" +#include "php_king/init.h" +#include "config/security_and_traffic/index.h" +#include "config/tls_and_crypto/index.h" +#include "config/tcp_transport/index.h" +#include "config/quic_transport/index.h" +#include "config/http2/index.h" +#include "config/app_http3_websockets_webtransport/index.h" +#include "config/cluster_and_process/index.h" +#include "config/bare_metal_tuning/index.h" +#include "config/cloud_autoscale/index.h" +#include "config/dynamic_admin_api/index.h" +#include "config/native_cdn/index.h" +#include "config/native_object_store/index.h" +#include "config/open_telemetry/index.h" +#include "config/router_and_loadbalancer/index.h" +#include "config/state_management/index.h" +#include "config/smart_dns/index.h" +#include "config/iibin/index.h" +#include "config/mcp_and_orchestrator/index.h" +#include "config/high_perf_compute_and_ai/index.h" +#include "config/semantic_geometry/index.h" +#include "config/smart_contracts/index.h" +#include "config/ssh_over_quic/index.h" +#include "config/tls_and_crypto/base_layer.h" +#include "php_king/globals.h" #include "php_king.h" #include diff --git a/extension/src/king_init/modules.inc b/extension/src/king_init/modules.inc index ee69a8b68..9288ecd10 100644 --- a/extension/src/king_init/modules.inc +++ b/extension/src/king_init/modules.inc @@ -4,9 +4,9 @@ * required by the current MINIT/MSHUTDOWN surface. */ -#include "include/config/config.h" -#include "include/pipeline_orchestrator/orchestrator.h" -#include "include/telemetry/telemetry.h" +#include "config/config.h" +#include "pipeline_orchestrator/orchestrator.h" +#include "telemetry/telemetry.h" int king_init_modules(int type, int module_number) { diff --git a/extension/src/king_init/request.inc b/extension/src/king_init/request.inc index 007352b49..f9b0f232a 100644 --- a/extension/src/king_init/request.inc +++ b/extension/src/king_init/request.inc @@ -4,8 +4,8 @@ * scoped telemetry state. */ -#include "include/pipeline_orchestrator/orchestrator.h" -#include "include/telemetry/telemetry.h" +#include "pipeline_orchestrator/orchestrator.h" +#include "telemetry/telemetry.h" int king_request_init(int type, int module_number) { diff --git a/extension/src/mcp/mcp.c b/extension/src/mcp/mcp.c index 244c48fc6..b87e399cd 100644 --- a/extension/src/mcp/mcp.c +++ b/extension/src/mcp/mcp.c @@ -4,10 +4,11 @@ * the persisted transfer-state helpers that keep transfers resumable across * restart and multi-host execution paths. */ -#include "include/mcp/mcp.h" -#include "include/php_king.h" -#include "include/config/mcp_and_orchestrator/base_layer.h" -#include "include/king_hrtime.h" +#include "mcp/mcp.h" +#include "php_king.h" +#include "mcp/arginfo/index.h" +#include "config/mcp_and_orchestrator/base_layer.h" +#include "king_hrtime.h" #include "Zend/zend_smart_str.h" #include "ext/standard/base64.h" @@ -32,8 +33,12 @@ #define KING_MCP_TRANSFER_STATE_VERSION 1 -#include "mcp/state_and_validation.inc" -#include "mcp/transfer_state.inc" -#include "mcp/transport_control.inc" -#include "mcp/remote_protocol.inc" -#include "mcp/lifecycle_and_api.inc" +#include "state.inc" +#include "runtime/state_and_validation.inc" +#include "runtime/transfer_state.inc" +#include "runtime/transport_control.inc" +#include "runtime/remote_protocol.inc" +#include "runtime/lifecycle_and_api.inc" + +#include "php_binding.inc" +#include "registration.inc" diff --git a/extension/src/mcp/php_binding.inc b/extension/src/mcp/php_binding.inc new file mode 100644 index 000000000..091a8f20d --- /dev/null +++ b/extension/src/mcp/php_binding.inc @@ -0,0 +1,29 @@ +/* + * King\MCP userland surface. Carries the MCP object methods and helper glue + * that bridge the native MCP runtime into userland. + */ + +#include "main/php_streams.h" +#include "Zend/zend_smart_str.h" +#include "ext/standard/base64.h" +#include "ext/json/php_json.h" +#include "mcp/mcp.h" +#include "iibin/iibin_internal.h" +#include "config/mcp_and_orchestrator/base_layer.h" +#include "config/mcp_and_orchestrator/default.h" +#include "king_hrtime.h" +#include +#include + +#include "php_binding/object_handlers.inc" +#include "php_binding/arginfo_and_runtime_controls.inc" +#include "php_binding/transfer_helpers.inc" +#include "php_binding/iibin_helpers.inc" +#include "php_binding/public_server_helpers.inc" +#include "php_binding/public_server_options.inc" +#include "php_binding/public_server_http_helpers.inc" +#include "php_binding/procedural_api.inc" +#include "php_binding/object_api.inc" +#include "php_binding/public_server_api.inc" +#include "php_binding/public_server_object_api.inc" +#include "mcp/class_method_entries.h" diff --git a/extension/src/php_king/mcp/arginfo_and_runtime_controls.inc b/extension/src/mcp/php_binding/arginfo_and_runtime_controls.inc similarity index 60% rename from extension/src/php_king/mcp/arginfo_and_runtime_controls.inc rename to extension/src/mcp/php_binding/arginfo_and_runtime_controls.inc index 2254e8817..c50fbe06f 100644 --- a/extension/src/php_king/mcp/arginfo_and_runtime_controls.inc +++ b/extension/src/mcp/php_binding/arginfo_and_runtime_controls.inc @@ -1,44 +1,3 @@ -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_MCP___construct, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, host, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, port, IS_LONG, 0) - ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, config, King\\Config, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_MCP_request, 0, 3, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, service, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, method, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, payload, IS_STRING, 0) - ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, cancel, King\\CancelToken, 1, "null") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_King_MCP_requestAsync, 0, 3, King\\Awaitable, 0) - ZEND_ARG_TYPE_INFO(0, service, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, method, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, payload, IS_STRING, 0) - ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, cancel, King\\CancelToken, 1, "null") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_MCP_close, 0, 0, IS_VOID, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_MCP_uploadFromStream, 0, 4, IS_VOID, 0) - ZEND_ARG_TYPE_INFO(0, service, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, method, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, streamIdentifier, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, stream, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_MCP_downloadToStream, 0, 4, IS_VOID, 0) - ZEND_ARG_TYPE_INFO(0, service, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, method, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, payload, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, stream, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - static zend_result king_mcp_set_transfer_error( zend_class_entry *exception_ce, const char *message) @@ -92,6 +51,113 @@ static zend_result king_mcp_validate_runtime_control_option( return SUCCESS; } +static zend_result king_mcp_connection_options_parse( + zval *options, + const char *function_name, + zend_class_entry *exception_ce, + zend_long *default_timeout_ms) +{ + zval *timeout_option; + zval *default_timeout_option; + zend_string *key; + zval *value; + char message[KING_ERR_LEN]; + + if (default_timeout_ms == NULL) { + return FAILURE; + } + + *default_timeout_ms = 0; + + if (options == NULL || Z_TYPE_P(options) == IS_NULL) { + return SUCCESS; + } + + if (Z_TYPE_P(options) != IS_ARRAY) { + snprintf(message, sizeof(message), "%s() options must be an array.", function_name); + return king_mcp_set_transfer_error(exception_ce, message); + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(options), key, value) { + if (key == NULL) { + snprintf(message, sizeof(message), "%s() options only accept string keys.", function_name); + return king_mcp_set_transfer_error(exception_ce, message); + } + + if ( + !zend_string_equals_literal(key, "timeout_ms") + && !zend_string_equals_literal(key, "default_timeout_ms") + ) { + snprintf( + message, + sizeof(message), + "%s() option '%s' is not supported.", + function_name, + ZSTR_VAL(key) + ); + return king_mcp_set_transfer_error(exception_ce, message); + } + + (void) value; + } ZEND_HASH_FOREACH_END(); + + timeout_option = zend_hash_str_find(Z_ARRVAL_P(options), "timeout_ms", sizeof("timeout_ms") - 1); + default_timeout_option = zend_hash_str_find( + Z_ARRVAL_P(options), + "default_timeout_ms", + sizeof("default_timeout_ms") - 1 + ); + + if ( + timeout_option != NULL + && Z_TYPE_P(timeout_option) != IS_NULL + && default_timeout_option != NULL + && Z_TYPE_P(default_timeout_option) != IS_NULL + ) { + snprintf( + message, + sizeof(message), + "%s() accepts either option 'timeout_ms' or 'default_timeout_ms', not both.", + function_name + ); + return king_mcp_set_transfer_error(exception_ce, message); + } + + if (default_timeout_option != NULL && Z_TYPE_P(default_timeout_option) != IS_NULL) { + return king_mcp_validate_runtime_control_option( + default_timeout_option, + "default_timeout_ms", + default_timeout_ms, + function_name, + exception_ce + ); + } + + if (timeout_option != NULL && Z_TYPE_P(timeout_option) != IS_NULL) { + return king_mcp_validate_runtime_control_option( + timeout_option, + "timeout_ms", + default_timeout_ms, + function_name, + exception_ce + ); + } + + return SUCCESS; +} + +static bool king_mcp_runtime_options_has_timeout(zval *options) +{ + zval *timeout_option; + + if (options == NULL || Z_TYPE_P(options) != IS_ARRAY) { + return false; + } + + timeout_option = zend_hash_str_find(Z_ARRVAL_P(options), "timeout_ms", sizeof("timeout_ms") - 1); + return timeout_option != NULL && Z_TYPE_P(timeout_option) != IS_NULL; +} + static zend_result king_mcp_runtime_control_parse( zval *options, @@ -194,6 +260,37 @@ static zend_result king_mcp_runtime_control_parse( return SUCCESS; } +static zend_result king_mcp_runtime_control_parse_for_state( + king_mcp_state *state, + zval *options, + zval *explicit_cancel, + const char *function_name, + zend_class_entry *exception_ce, + king_mcp_runtime_control_t *control) +{ + if ( + king_mcp_runtime_control_parse( + options, + explicit_cancel, + function_name, + exception_ce, + control + ) != SUCCESS + ) { + return FAILURE; + } + + if ( + state != NULL + && state->default_timeout_ms > 0 + && !king_mcp_runtime_options_has_timeout(options) + ) { + control->timeout_ms = state->default_timeout_ms; + } + + return SUCCESS; +} + static zend_result king_mcp_runtime_control_check( king_mcp_runtime_control_t *control, const char *function_name, diff --git a/extension/src/mcp/php_binding/class_entries.inc b/extension/src/mcp/php_binding/class_entries.inc new file mode 100644 index 000000000..ed3b34e78 --- /dev/null +++ b/extension/src/mcp/php_binding/class_entries.inc @@ -0,0 +1,11 @@ +/* + * MCP PHP class-entry storage. + */ + +zend_class_entry *king_ce_mcp = NULL; +zend_class_entry *king_ce_mcp_server = NULL; +zend_class_entry *king_ce_mcp_exception = NULL; +zend_class_entry *king_ce_mcp_connection_error = NULL; +zend_class_entry *king_ce_mcp_protocol_error = NULL; +zend_class_entry *king_ce_mcp_timeout = NULL; +zend_class_entry *king_ce_mcp_data_error = NULL; diff --git a/extension/src/mcp/php_binding/iibin_helpers.inc b/extension/src/mcp/php_binding/iibin_helpers.inc new file mode 100644 index 000000000..0ac9bbdcf --- /dev/null +++ b/extension/src/mcp/php_binding/iibin_helpers.inc @@ -0,0 +1,231 @@ +typedef struct _king_mcp_iibin_route { + zend_string *request_schema; + zend_string *response_schema; + zval *decode_as_object; +} king_mcp_iibin_route_t; + +static zend_string *king_mcp_iibin_route_key_create( + const char *service, + size_t service_len, + const char *method, + size_t method_len, + char separator) +{ + zend_string *key = zend_string_alloc(service_len + 1 + method_len, 0); + + memcpy(ZSTR_VAL(key), service, service_len); + ZSTR_VAL(key)[service_len] = separator; + memcpy(ZSTR_VAL(key) + service_len + 1, method, method_len); + ZSTR_VAL(key)[service_len + 1 + method_len] = '\0'; + + return key; +} + +static zval *king_mcp_iibin_find_route_value( + king_mcp_state *state, + const char *service, + size_t service_len, + const char *method, + size_t method_len) +{ + zend_string *key; + zval *route; + zval *service_routes; + + if ( + state == NULL + || Z_ISUNDEF(state->iibin_routes) + || Z_TYPE(state->iibin_routes) != IS_ARRAY + ) { + return NULL; + } + + key = king_mcp_iibin_route_key_create(service, service_len, method, method_len, '/'); + route = zend_hash_find(Z_ARRVAL(state->iibin_routes), key); + zend_string_release(key); + if (route != NULL) { + return route; + } + + key = king_mcp_iibin_route_key_create(service, service_len, method, method_len, '.'); + route = zend_hash_find(Z_ARRVAL(state->iibin_routes), key); + zend_string_release(key); + if (route != NULL) { + return route; + } + + service_routes = zend_hash_str_find(Z_ARRVAL(state->iibin_routes), service, service_len); + if (service_routes != NULL && Z_TYPE_P(service_routes) == IS_ARRAY) { + return zend_hash_str_find(Z_ARRVAL_P(service_routes), method, method_len); + } + + return NULL; +} + +static zend_result king_mcp_iibin_set_error( + zend_class_entry *exception_ce, + const char *message) +{ + king_set_error(message); + + if (exception_ce != NULL) { + zend_throw_exception_ex(exception_ce, 0, "%s", message); + } + + return FAILURE; +} + +static zend_result king_mcp_iibin_route_resolve( + king_mcp_state *state, + const char *service, + size_t service_len, + const char *method, + size_t method_len, + const char *function_name, + zend_class_entry *exception_ce, + king_mcp_iibin_route_t *route) +{ + zval *route_config; + zval *request_schema; + zval *response_schema; + zval *decode_as_object; + char message[KING_ERR_LEN]; + + if (route == NULL) { + return FAILURE; + } + + route->request_schema = NULL; + route->response_schema = NULL; + route->decode_as_object = NULL; + + route_config = king_mcp_iibin_find_route_value( + state, + service, + service_len, + method, + method_len + ); + if (route_config == NULL || Z_TYPE_P(route_config) != IS_ARRAY) { + snprintf( + message, + sizeof(message), + "%s() has no configured mcp.iibin_routes entry for '%.*s/%.*s'.", + function_name, + (int) service_len, + service, + (int) method_len, + method + ); + return king_mcp_iibin_set_error(exception_ce, message); + } + + request_schema = zend_hash_str_find( + Z_ARRVAL_P(route_config), + "request_schema", + sizeof("request_schema") - 1 + ); + if ( + request_schema == NULL + || Z_TYPE_P(request_schema) != IS_STRING + || Z_STRLEN_P(request_schema) == 0 + ) { + snprintf( + message, + sizeof(message), + "%s() route '%.*s/%.*s' requires a non-empty request_schema.", + function_name, + (int) service_len, + service, + (int) method_len, + method + ); + return king_mcp_iibin_set_error(exception_ce, message); + } + + response_schema = zend_hash_str_find( + Z_ARRVAL_P(route_config), + "response_schema", + sizeof("response_schema") - 1 + ); + if ( + response_schema != NULL + && Z_TYPE_P(response_schema) != IS_NULL + && Z_TYPE_P(response_schema) != IS_STRING + ) { + snprintf( + message, + sizeof(message), + "%s() route '%.*s/%.*s' response_schema must be null or string.", + function_name, + (int) service_len, + service, + (int) method_len, + method + ); + return king_mcp_iibin_set_error(exception_ce, message); + } + + decode_as_object = zend_hash_str_find( + Z_ARRVAL_P(route_config), + "decode_as_object", + sizeof("decode_as_object") - 1 + ); + + route->request_schema = Z_STR_P(request_schema); + route->response_schema = ( + response_schema != NULL + && Z_TYPE_P(response_schema) == IS_STRING + && Z_STRLEN_P(response_schema) > 0 + ) ? Z_STR_P(response_schema) : NULL; + route->decode_as_object = decode_as_object; + + return SUCCESS; +} + +static zend_result king_mcp_iibin_encode_request_payload( + zend_string *request_schema, + zval *request_data, + zend_string **payload_out) +{ + smart_str encoded = {0}; + + if (payload_out == NULL) { + return FAILURE; + } + + *payload_out = NULL; + + if (king_iibin_encode(request_schema, request_data, &encoded) != SUCCESS) { + smart_str_free(&encoded); + return FAILURE; + } + + smart_str_0(&encoded); + if (encoded.s == NULL) { + *payload_out = zend_string_init("", 0, 0); + } else { + *payload_out = encoded.s; + } + + return SUCCESS; +} + +static zend_result king_mcp_iibin_decode_response_payload( + zend_string *response_schema, + zend_string *response_payload, + zval *decode_as_object, + zval *decoded_out) +{ + if (decoded_out == NULL || response_schema == NULL || response_payload == NULL) { + return FAILURE; + } + + ZVAL_UNDEF(decoded_out); + return king_iibin_decode( + response_schema, + response_payload, + decode_as_object, + decoded_out + ); +} diff --git a/extension/src/php_king/mcp/object_api.inc b/extension/src/mcp/php_binding/object_api.inc similarity index 65% rename from extension/src/php_king/mcp/object_api.inc rename to extension/src/mcp/php_binding/object_api.inc index 9be099e04..267c58c4c 100644 --- a/extension/src/php_king/mcp/object_api.inc +++ b/extension/src/mcp/php_binding/object_api.inc @@ -4,16 +4,30 @@ PHP_METHOD(King_MCP, __construct) size_t host_len = 0; zend_long port; zval *config = NULL; + zval *options = NULL; king_mcp_state *state; king_mcp_object *intern; + zend_long default_timeout_ms = 0; - ZEND_PARSE_PARAMETERS_START(2, 3) + ZEND_PARSE_PARAMETERS_START(2, 4) Z_PARAM_STRING(host, host_len) Z_PARAM_LONG(port) Z_PARAM_OPTIONAL Z_PARAM_OBJECT_OF_CLASS_OR_NULL(config, king_ce_config) + Z_PARAM_ARRAY_OR_NULL(options) ZEND_PARSE_PARAMETERS_END(); + if ( + king_mcp_connection_options_parse( + options, + "King\\MCP::__construct", + king_ce_validation_exception, + &default_timeout_ms + ) != SUCCESS + ) { + RETURN_THROWS(); + } + state = king_mcp_state_create( host, host_len, @@ -31,6 +45,7 @@ PHP_METHOD(King_MCP, __construct) ); RETURN_THROWS(); } + state->default_timeout_ms = default_timeout_ms; intern = php_king_mcp_obj_from_zend(Z_OBJ_P(ZEND_THIS)); if (!Z_ISUNDEF(intern->resource)) { @@ -71,7 +86,8 @@ PHP_METHOD(King_MCP, request) RETURN_THROWS(); } - if (king_mcp_runtime_control_parse( + if (king_mcp_runtime_control_parse_for_state( + state, options, cancel, "MCP::request", @@ -198,6 +214,210 @@ PHP_METHOD(King_MCP, requestAsync) } } +PHP_METHOD(King_MCP, requestIibin) +{ + char *service = NULL; + char *method = NULL; + size_t service_len = 0; + size_t method_len = 0; + zval *request_data; + zval *cancel = NULL; + zval *options = NULL; + zval decoded_response; + zend_string *request_payload = NULL; + zend_string *response_payload = NULL; + king_mcp_state *state; + king_mcp_runtime_control_t control; + king_mcp_iibin_route_t route; + + ZEND_PARSE_PARAMETERS_START(3, 5) + Z_PARAM_STRING(service, service_len) + Z_PARAM_STRING(method, method_len) + Z_PARAM_ZVAL(request_data) + Z_PARAM_OPTIONAL + Z_PARAM_OBJECT_OF_CLASS_OR_NULL(cancel, king_ce_cancel_token) + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + state = king_mcp_object_fetch_state(ZEND_THIS, "MCP::requestIibin"); + if (state == NULL) { + RETURN_THROWS(); + } + + if (king_mcp_state_require_open(state, "MCP::requestIibin") != SUCCESS) { + RETURN_THROWS(); + } + + if (king_mcp_iibin_route_resolve( + state, + service, + service_len, + method, + method_len, + "MCP::requestIibin", + king_ce_validation_exception, + &route + ) != SUCCESS) { + RETURN_THROWS(); + } + + if (king_mcp_iibin_encode_request_payload( + route.request_schema, + request_data, + &request_payload + ) != SUCCESS) { + RETURN_THROWS(); + } + + if (king_mcp_runtime_control_parse_for_state( + state, + options, + cancel, + "MCP::requestIibin", + king_ce_validation_exception, + &control + ) != SUCCESS) { + zend_string_release(request_payload); + RETURN_THROWS(); + } + + if (king_mcp_runtime_control_check( + &control, + "MCP::requestIibin", + king_ce_mcp_timeout, + king_ce_runtime_exception + ) != SUCCESS) { + zend_string_release(request_payload); + RETURN_THROWS(); + } + + if (king_mcp_request( + state, + service, + service_len, + method, + method_len, + request_payload, + &response_payload, + &control + ) != SUCCESS) { + zend_string_release(request_payload); + if (king_mcp_runtime_control_check( + &control, + "MCP::requestIibin", + king_ce_mcp_timeout, + king_ce_runtime_exception + ) != SUCCESS) { + RETURN_THROWS(); + } + king_mcp_throw_state_error( + state, + king_ce_mcp_connection_error, + "MCP::requestIibin() failed to exchange the remote MCP request." + ); + RETURN_THROWS(); + } + + zend_string_release(request_payload); + + if (response_payload == NULL) { + state->last_error_kind = KING_MCP_ERROR_PROTOCOL; + king_set_error("MCP::requestIibin() received an invalid empty response payload from the remote MCP peer."); + king_mcp_throw_state_error( + state, + king_ce_mcp_protocol_error, + "MCP::requestIibin() received an invalid empty response payload from the remote MCP peer." + ); + RETURN_THROWS(); + } + + if (king_mcp_runtime_control_check( + &control, + "MCP::requestIibin", + king_ce_mcp_timeout, + king_ce_runtime_exception + ) != SUCCESS) { + zend_string_release(response_payload); + RETURN_THROWS(); + } + + if (route.response_schema != NULL) { + ZVAL_UNDEF(&decoded_response); + if (king_mcp_iibin_decode_response_payload( + route.response_schema, + response_payload, + route.decode_as_object, + &decoded_response + ) != SUCCESS) { + zend_string_release(response_payload); + RETURN_THROWS(); + } + zend_string_release(response_payload); + king_set_error(""); + RETURN_ZVAL(&decoded_response, 0, 1); + } + + king_set_error(""); + RETURN_STR(response_payload); +} + +PHP_METHOD(King_MCP, requestIibinAsync) +{ + char *service = NULL; + char *method = NULL; + size_t service_len = 0; + size_t method_len = 0; + zval *request_data; + zval *cancel = NULL; + zval *options = NULL; + zval params[5]; + uint32_t index; + + ZEND_PARSE_PARAMETERS_START(3, 5) + Z_PARAM_STRING(service, service_len) + Z_PARAM_STRING(method, method_len) + Z_PARAM_ZVAL(request_data) + Z_PARAM_OPTIONAL + Z_PARAM_OBJECT_OF_CLASS_OR_NULL(cancel, king_ce_cancel_token) + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + ZVAL_STRINGL(¶ms[0], service, service_len); + ZVAL_STRINGL(¶ms[1], method, method_len); + ZVAL_COPY(¶ms[2], request_data); + if (cancel != NULL) { + ZVAL_COPY(¶ms[3], cancel); + } else { + ZVAL_NULL(¶ms[3]); + } + if (options != NULL) { + ZVAL_COPY(¶ms[4], options); + } else { + ZVAL_NULL(¶ms[4]); + } + + if (king_awaitable_create_method_call( + return_value, + "King\\MCP::requestIibinAsync", + sizeof("King\\MCP::requestIibinAsync") - 1, + ZEND_THIS, + "requestIibin", + sizeof("requestIibin") - 1, + params, + 5, + cancel + ) != SUCCESS) { + for (index = 0; index < 5; index++) { + zval_ptr_dtor(¶ms[index]); + } + RETURN_FALSE; + } + + for (index = 0; index < 5; index++) { + zval_ptr_dtor(¶ms[index]); + } +} + PHP_METHOD(King_MCP, close) { king_mcp_state *state; @@ -267,7 +487,8 @@ PHP_METHOD(King_MCP, uploadFromStream) RETURN_THROWS(); } - if (king_mcp_runtime_control_parse( + if (king_mcp_runtime_control_parse_for_state( + state, options, NULL, "MCP::uploadFromStream", @@ -386,7 +607,8 @@ PHP_METHOD(King_MCP, downloadToStream) RETURN_THROWS(); } - if (king_mcp_runtime_control_parse( + if (king_mcp_runtime_control_parse_for_state( + state, options, NULL, "MCP::downloadToStream", @@ -474,13 +696,3 @@ PHP_METHOD(King_MCP, downloadToStream) zend_string_release(payload); king_set_error(""); } - -const zend_function_entry king_mcp_class_methods[] = { - PHP_ME(King_MCP, __construct, arginfo_class_King_MCP___construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) - PHP_ME(King_MCP, request, arginfo_class_King_MCP_request, ZEND_ACC_PUBLIC) - PHP_ME(King_MCP, requestAsync, arginfo_class_King_MCP_requestAsync, ZEND_ACC_PUBLIC) - PHP_ME(King_MCP, uploadFromStream, arginfo_class_King_MCP_uploadFromStream, ZEND_ACC_PUBLIC) - PHP_ME(King_MCP, downloadToStream, arginfo_class_King_MCP_downloadToStream, ZEND_ACC_PUBLIC) - PHP_ME(King_MCP, close, arginfo_class_King_MCP_close, ZEND_ACC_PUBLIC) - PHP_FE_END -}; diff --git a/extension/src/mcp/php_binding/object_handlers.inc b/extension/src/mcp/php_binding/object_handlers.inc new file mode 100644 index 000000000..f67da18c7 --- /dev/null +++ b/extension/src/mcp/php_binding/object_handlers.inc @@ -0,0 +1,80 @@ +/* + * King MCP PHP object handlers. + */ + +static zend_object_handlers king_mcp_object_handlers; +static zend_object_handlers king_mcp_server_object_handlers; + +static void king_mcp_object_free(zend_object *object) +{ + king_mcp_object *intern = php_king_mcp_obj_from_zend(object); + + if (!Z_ISUNDEF(intern->resource)) { + zval_ptr_dtor(&intern->resource); + ZVAL_UNDEF(&intern->resource); + } + + zend_object_std_dtor(&intern->std); +} + +static zend_object *king_mcp_object_create(zend_class_entry *ce) +{ + king_mcp_object *intern = zend_object_alloc(sizeof(*intern), ce); + + ZVAL_UNDEF(&intern->resource); + zend_object_std_init(&intern->std, ce); + object_properties_init(&intern->std, ce); + intern->std.handlers = &king_mcp_object_handlers; + + return &intern->std; +} + +static void king_mcp_server_object_free(zend_object *object) +{ + king_mcp_server_object *intern = php_king_mcp_server_obj_from_zend(object); + + if (!Z_ISUNDEF(intern->definition)) { + zval_ptr_dtor(&intern->definition); + ZVAL_UNDEF(&intern->definition); + } + + zend_object_std_dtor(&intern->std); +} + +static zend_object *king_mcp_server_object_create(zend_class_entry *ce) +{ + king_mcp_server_object *intern = zend_object_alloc(sizeof(*intern), ce); + + ZVAL_UNDEF(&intern->definition); + zend_object_std_init(&intern->std, ce); + object_properties_init(&intern->std, ce); + intern->std.handlers = &king_mcp_server_object_handlers; + + return &intern->std; +} + +static void king_mcp_object_handlers_init(void) +{ + memcpy( + &king_mcp_object_handlers, + &std_object_handlers, + sizeof(zend_object_handlers) + ); + king_mcp_object_handlers.offset = XtOffsetOf(king_mcp_object, std); + king_mcp_object_handlers.free_obj = king_mcp_object_free; + king_mcp_object_handlers.clone_obj = NULL; + king_ce_mcp->create_object = king_mcp_object_create; +} + +static void king_mcp_server_object_handlers_init(void) +{ + memcpy( + &king_mcp_server_object_handlers, + &std_object_handlers, + sizeof(zend_object_handlers) + ); + king_mcp_server_object_handlers.offset = XtOffsetOf(king_mcp_server_object, std); + king_mcp_server_object_handlers.free_obj = king_mcp_server_object_free; + king_mcp_server_object_handlers.clone_obj = NULL; + king_ce_mcp_server->create_object = king_mcp_server_object_create; +} diff --git a/extension/src/php_king/mcp/procedural_api.inc b/extension/src/mcp/php_binding/procedural_api.inc similarity index 66% rename from extension/src/php_king/mcp/procedural_api.inc rename to extension/src/mcp/php_binding/procedural_api.inc index ea41c2b50..027ae1808 100644 --- a/extension/src/php_king/mcp/procedural_api.inc +++ b/extension/src/mcp/php_binding/procedural_api.inc @@ -6,6 +6,7 @@ PHP_FUNCTION(king_mcp_connect) zval *config; zval *options = NULL; king_mcp_state *state; + zend_long default_timeout_ms = 0; ZEND_PARSE_PARAMETERS_START(3, 4) Z_PARAM_STRING(host, host_len) @@ -26,7 +27,16 @@ PHP_FUNCTION(king_mcp_connect) } } - (void) options; + if ( + king_mcp_connection_options_parse( + options, + "king_mcp_connect", + NULL, + &default_timeout_ms + ) != SUCCESS + ) { + RETURN_FALSE; + } state = king_mcp_state_create( host, @@ -37,6 +47,7 @@ PHP_FUNCTION(king_mcp_connect) if (state == NULL) { RETURN_FALSE; } + state->default_timeout_ms = default_timeout_ms; RETURN_RES(zend_register_resource(state, le_king_mcp)); } @@ -63,8 +74,6 @@ PHP_FUNCTION(king_mcp_request) Z_PARAM_ARRAY_OR_NULL(options) ZEND_PARSE_PARAMETERS_END(); - (void) options; - state = king_mcp_fetch_state(connection, 1); if (state == NULL) { RETURN_THROWS(); @@ -75,7 +84,8 @@ PHP_FUNCTION(king_mcp_request) RETURN_FALSE; } - if (king_mcp_runtime_control_parse( + if (king_mcp_runtime_control_parse_for_state( + state, options, NULL, "king_mcp_request", @@ -187,6 +197,196 @@ PHP_FUNCTION(king_mcp_request_async) } } +PHP_FUNCTION(king_mcp_request_iibin) +{ + zval *connection; + char *service_name = NULL; + char *method_name = NULL; + size_t service_name_len = 0; + size_t method_name_len = 0; + zval *request_data; + zval *options = NULL; + zval decoded_response; + zend_string *request_payload = NULL; + zend_string *response_payload = NULL; + king_mcp_state *state; + king_mcp_runtime_control_t control; + king_mcp_iibin_route_t route; + + ZEND_PARSE_PARAMETERS_START(4, 5) + Z_PARAM_ZVAL(connection) + Z_PARAM_STRING(service_name, service_name_len) + Z_PARAM_STRING(method_name, method_name_len) + Z_PARAM_ZVAL(request_data) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + state = king_mcp_fetch_state(connection, 1); + if (state == NULL) { + RETURN_THROWS(); + } + + if (state->closed) { + king_set_error("king_mcp_request_iibin() cannot run on a closed MCP connection."); + RETURN_FALSE; + } + + if (king_mcp_iibin_route_resolve( + state, + service_name, + service_name_len, + method_name, + method_name_len, + "king_mcp_request_iibin", + NULL, + &route + ) != SUCCESS) { + RETURN_FALSE; + } + + if (king_mcp_iibin_encode_request_payload( + route.request_schema, + request_data, + &request_payload + ) != SUCCESS) { + RETURN_THROWS(); + } + + if (king_mcp_runtime_control_parse_for_state( + state, + options, + NULL, + "king_mcp_request_iibin", + NULL, + &control + ) != SUCCESS) { + zend_string_release(request_payload); + RETURN_FALSE; + } + + if (king_mcp_runtime_control_check( + &control, + "king_mcp_request_iibin", + NULL, + NULL + ) != SUCCESS) { + zend_string_release(request_payload); + RETURN_FALSE; + } + + if (king_mcp_request( + state, + service_name, + service_name_len, + method_name, + method_name_len, + request_payload, + &response_payload, + &control + ) != SUCCESS) { + zend_string_release(request_payload); + if (king_mcp_runtime_control_check( + &control, + "king_mcp_request_iibin", + NULL, + NULL + ) != SUCCESS) { + RETURN_FALSE; + } + RETURN_FALSE; + } + + zend_string_release(request_payload); + + if (response_payload == NULL) { + state->last_error_kind = KING_MCP_ERROR_PROTOCOL; + king_set_error("king_mcp_request_iibin() received an invalid empty response payload from the remote MCP peer."); + RETURN_FALSE; + } + + if (king_mcp_runtime_control_check( + &control, + "king_mcp_request_iibin", + NULL, + NULL + ) != SUCCESS) { + zend_string_release(response_payload); + RETURN_FALSE; + } + + if (route.response_schema != NULL) { + ZVAL_UNDEF(&decoded_response); + if (king_mcp_iibin_decode_response_payload( + route.response_schema, + response_payload, + route.decode_as_object, + &decoded_response + ) != SUCCESS) { + zend_string_release(response_payload); + RETURN_THROWS(); + } + zend_string_release(response_payload); + king_set_error(""); + RETURN_ZVAL(&decoded_response, 0, 1); + } + + king_set_error(""); + RETURN_STR(response_payload); +} + +PHP_FUNCTION(king_mcp_request_iibin_async) +{ + zval *connection; + char *service_name = NULL; + char *method_name = NULL; + size_t service_name_len = 0; + size_t method_name_len = 0; + zval *request_data; + zval *options = NULL; + zval params[5]; + uint32_t index; + + ZEND_PARSE_PARAMETERS_START(4, 5) + Z_PARAM_ZVAL(connection) + Z_PARAM_STRING(service_name, service_name_len) + Z_PARAM_STRING(method_name, method_name_len) + Z_PARAM_ZVAL(request_data) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + ZVAL_COPY(¶ms[0], connection); + ZVAL_STRINGL(¶ms[1], service_name, service_name_len); + ZVAL_STRINGL(¶ms[2], method_name, method_name_len); + ZVAL_COPY(¶ms[3], request_data); + if (options != NULL) { + ZVAL_COPY(¶ms[4], options); + } else { + ZVAL_NULL(¶ms[4]); + } + + if (king_awaitable_create_function_call( + return_value, + "king_mcp_request_iibin_async", + sizeof("king_mcp_request_iibin_async") - 1, + "king_mcp_request_iibin", + sizeof("king_mcp_request_iibin") - 1, + params, + 5, + NULL + ) != SUCCESS) { + for (index = 0; index < 5; index++) { + zval_ptr_dtor(¶ms[index]); + } + RETURN_FALSE; + } + + for (index = 0; index < 5; index++) { + zval_ptr_dtor(¶ms[index]); + } +} + PHP_FUNCTION(king_mcp_close) { zval *connection; @@ -263,7 +463,8 @@ PHP_FUNCTION(king_mcp_upload_from_stream) RETURN_THROWS(); } - if (king_mcp_runtime_control_parse( + if (king_mcp_runtime_control_parse_for_state( + state, options, NULL, "king_mcp_upload_from_stream", @@ -376,7 +577,8 @@ PHP_FUNCTION(king_mcp_download_to_stream) RETURN_THROWS(); } - if (king_mcp_runtime_control_parse( + if (king_mcp_runtime_control_parse_for_state( + state, options, NULL, "king_mcp_download_to_stream", diff --git a/extension/src/mcp/php_binding/public_server_api.inc b/extension/src/mcp/php_binding/public_server_api.inc new file mode 100644 index 000000000..2c37d5afd --- /dev/null +++ b/extension/src/mcp/php_binding/public_server_api.inc @@ -0,0 +1,290 @@ +PHP_FUNCTION(king_mcp_server_create) +{ + zval *definition; + zval *options = NULL; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_ARRAY(definition) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + if (king_mcp_public_server_normalize(definition, return_value) != SUCCESS) { + RETURN_FALSE; + } + + if (king_mcp_public_server_apply_creation_options(return_value, options) != SUCCESS) { + zval_ptr_dtor(return_value); + RETURN_FALSE; + } + + king_set_error(""); +} + +PHP_FUNCTION(king_mcp_server_handle_jsonrpc) +{ + zval *server; + zval *message; + zval *context = NULL; + zend_string *json = NULL; + bool has_response = false; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_ARRAY(server) + Z_PARAM_ZVAL(message) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(context) + ZEND_PARSE_PARAMETERS_END(); + + if ( + king_mcp_public_handle_jsonrpc_encoded( + server, + message, + context, + &json, + &has_response + ) != SUCCESS + ) { + if (EG(exception)) { + RETURN_THROWS(); + } + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "%s", + king_get_error()[0] != '\0' ? king_get_error() : "MCP JSON-RPC handling failed." + ); + RETURN_THROWS(); + } + + if (!has_response) { + RETURN_NULL(); + } + + king_set_error(""); + RETURN_STR(json); +} + +PHP_FUNCTION(king_mcp_server_handle_http) +{ + zval *server; + zval *request; + zval *context = NULL; + zval context_with_request; + zval request_copy; + zval *method; + zval *body; + zend_string *json = NULL; + zend_string *sse = NULL; + bool has_response = false; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_ARRAY(server) + Z_PARAM_ARRAY(request) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(context) + ZEND_PARSE_PARAMETERS_END(); + + method = zend_hash_str_find(Z_ARRVAL_P(request), "method", sizeof("method") - 1); + if (method == NULL || Z_TYPE_P(method) != IS_STRING) { + king_mcp_public_http_response(return_value, 400, "application/json", NULL); + return; + } + + if (!king_mcp_public_http_origin_allowed(server, request)) { + zend_string *body_json = zend_string_init( + "{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32600,\"message\":\"MCP HTTP Origin is not allowed.\"}}", + sizeof("{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32600,\"message\":\"MCP HTTP Origin is not allowed.\"}}") - 1, + 0 + ); + king_mcp_public_http_response(return_value, 403, "application/json", body_json); + zend_string_release(body_json); + return; + } + + if (strcasecmp(Z_STRVAL_P(method), "OPTIONS") == 0) { + zval headers; + array_init(return_value); + add_assoc_long(return_value, "status", 204); + array_init(&headers); + add_assoc_string(&headers, "allow", "GET, POST, OPTIONS"); + add_assoc_string(&headers, "accept-post", "application/json"); + add_assoc_zval(return_value, "headers", &headers); + add_assoc_string(return_value, "body", ""); + return; + } + + if (strcasecmp(Z_STRVAL_P(method), "GET") == 0) { + if (king_mcp_public_header_contains(request, "accept", "text/event-stream")) { + zend_string *open = zend_string_init(": king-mcp stream\n\n", sizeof(": king-mcp stream\n\n") - 1, 0); + king_mcp_public_http_response(return_value, 200, "text/event-stream", open); + zend_string_release(open); + return; + } + king_mcp_public_http_response(return_value, 405, "application/json", NULL); + return; + } + + if (strcasecmp(Z_STRVAL_P(method), "POST") != 0) { + king_mcp_public_http_response(return_value, 405, "application/json", NULL); + return; + } + + if ( + !king_mcp_public_header_contains(request, "accept", "application/json") + || !king_mcp_public_header_contains(request, "accept", "text/event-stream") + ) { + zend_string *body_json = zend_string_init( + "{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32600,\"message\":\"MCP Streamable HTTP requires Accept: application/json, text/event-stream.\"}}", + sizeof("{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32600,\"message\":\"MCP Streamable HTTP requires Accept: application/json, text/event-stream.\"}}") - 1, + 0 + ); + king_mcp_public_http_response(return_value, 406, "application/json", body_json); + zend_string_release(body_json); + return; + } + + body = zend_hash_str_find(Z_ARRVAL_P(request), "body", sizeof("body") - 1); + if (body == NULL || Z_TYPE_P(body) != IS_STRING) { + king_mcp_public_http_response(return_value, 400, "application/json", NULL); + return; + } + + array_init(&context_with_request); + if (context != NULL && Z_TYPE_P(context) == IS_ARRAY) { + zend_hash_copy( + Z_ARRVAL(context_with_request), + Z_ARRVAL_P(context), + zval_add_ref + ); + } + ZVAL_COPY(&request_copy, request); + add_assoc_zval(&context_with_request, "http_request", &request_copy); + + if ( + king_mcp_public_handle_jsonrpc_encoded( + server, + body, + &context_with_request, + &json, + &has_response + ) != SUCCESS + ) { + zval_ptr_dtor(&context_with_request); + if (EG(exception)) { + RETURN_THROWS(); + } + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "%s", + king_get_error()[0] != '\0' ? king_get_error() : "MCP HTTP handling failed." + ); + RETURN_THROWS(); + } + zval_ptr_dtor(&context_with_request); + + if (!has_response) { + king_mcp_public_http_response(return_value, 202, NULL, NULL); + return; + } + + if (king_mcp_public_http_prefers_sse(server, request)) { + sse = king_mcp_public_sse_body(json); + king_mcp_public_http_response(return_value, 200, "text/event-stream", sse); + zend_string_release(sse); + } else { + king_mcp_public_http_response(return_value, 200, "application/json", json); + } + zend_string_release(json); + king_set_error(""); +} + +PHP_FUNCTION(king_mcp_server_stdio) +{ + zval *server; + zval *options = NULL; + php_stream *input; + php_stream *output; + char *buffer; + size_t line_len = 0; + zend_long max_line_bytes = 1024 * 1024; + zval *option_value; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_ARRAY(server) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + if (options != NULL && Z_TYPE_P(options) == IS_ARRAY) { + option_value = zend_hash_str_find( + Z_ARRVAL_P(options), + "max_line_bytes", + sizeof("max_line_bytes") - 1 + ); + if (option_value != NULL && Z_TYPE_P(option_value) == IS_LONG && Z_LVAL_P(option_value) > 0) { + max_line_bytes = Z_LVAL_P(option_value); + } + } + + input = php_stream_open_wrapper("php://stdin", "rb", REPORT_ERRORS, NULL); + output = php_stream_open_wrapper("php://stdout", "wb", REPORT_ERRORS, NULL); + if (input == NULL || output == NULL) { + if (input != NULL) { + php_stream_close(input); + } + if (output != NULL) { + php_stream_close(output); + } + RETURN_FALSE; + } + + buffer = emalloc((size_t) max_line_bytes + 1); + while (php_stream_get_line(input, buffer, (size_t) max_line_bytes + 1, &line_len) != NULL) { + zval message; + zend_string *json = NULL; + bool has_response = false; + + while (line_len > 0 && (buffer[line_len - 1] == '\n' || buffer[line_len - 1] == '\r')) { + buffer[--line_len] = '\0'; + } + if (line_len == 0) { + continue; + } + + ZVAL_STRINGL(&message, buffer, line_len); + if ( + king_mcp_public_handle_jsonrpc_encoded( + server, + &message, + NULL, + &json, + &has_response + ) != SUCCESS + ) { + zval_ptr_dtor(&message); + efree(buffer); + php_stream_close(input); + php_stream_close(output); + if (EG(exception)) { + RETURN_THROWS(); + } + RETURN_FALSE; + } + zval_ptr_dtor(&message); + + if (has_response && json != NULL) { + php_stream_write(output, ZSTR_VAL(json), ZSTR_LEN(json)); + php_stream_write(output, "\n", 1); + php_stream_flush(output); + zend_string_release(json); + } + } + + efree(buffer); + php_stream_close(input); + php_stream_close(output); + king_set_error(""); + RETURN_TRUE; +} diff --git a/extension/src/mcp/php_binding/public_server_helpers.inc b/extension/src/mcp/php_binding/public_server_helpers.inc new file mode 100644 index 000000000..4ba425485 --- /dev/null +++ b/extension/src/mcp/php_binding/public_server_helpers.inc @@ -0,0 +1,782 @@ +#define KING_MCP_PUBLIC_PROTOCOL_VERSION "2025-06-18" + +static zend_result king_mcp_public_json_encode(zval *value, zend_string **json_out) +{ + smart_str json = {0}; + + if (json_out == NULL) { + return FAILURE; + } + + *json_out = NULL; + if (php_json_encode(&json, value, PHP_JSON_UNESCAPED_SLASHES) != SUCCESS) { + smart_str_free(&json); + king_set_error("MCP JSON-RPC response encoding failed."); + return FAILURE; + } + + smart_str_0(&json); + *json_out = json.s != NULL ? json.s : zend_string_init("", 0, 0); + return SUCCESS; +} + +static zend_result king_mcp_public_json_decode(zend_string *json, zval *decoded_out) +{ + if (json == NULL || decoded_out == NULL) { + return FAILURE; + } + + ZVAL_UNDEF(decoded_out); + if (php_json_decode(decoded_out, ZSTR_VAL(json), ZSTR_LEN(json), 1, 512) != SUCCESS) { + king_set_error("MCP JSON-RPC message decoding failed."); + return FAILURE; + } + + return SUCCESS; +} + +static bool king_mcp_public_array_is_list(zval *value) +{ + if (value == NULL || Z_TYPE_P(value) != IS_ARRAY) { + return false; + } + + return zend_array_is_list(Z_ARRVAL_P(value)); +} + +static void king_mcp_public_copy_id_or_null(zval *message, zval *target) +{ + zval *id = zend_hash_str_find(Z_ARRVAL_P(message), "id", sizeof("id") - 1); + + if (id == NULL) { + ZVAL_NULL(target); + return; + } + + ZVAL_COPY(target, id); +} + +static void king_mcp_public_build_error_response( + zval *return_value, + zval *id, + zend_long code, + const char *message) +{ + zval error; + zval id_copy; + + array_init(return_value); + add_assoc_string(return_value, "jsonrpc", "2.0"); + if (id != NULL) { + ZVAL_COPY(&id_copy, id); + } else { + ZVAL_NULL(&id_copy); + } + add_assoc_zval(return_value, "id", &id_copy); + + array_init(&error); + add_assoc_long(&error, "code", code); + add_assoc_string(&error, "message", message); + add_assoc_zval(return_value, "error", &error); +} + +static void king_mcp_public_build_result_response( + zval *return_value, + zval *id, + zval *result) +{ + zval id_copy; + zval result_copy; + + array_init(return_value); + add_assoc_string(return_value, "jsonrpc", "2.0"); + ZVAL_COPY(&id_copy, id); + add_assoc_zval(return_value, "id", &id_copy); + ZVAL_COPY(&result_copy, result); + add_assoc_zval(return_value, "result", &result_copy); +} + +static zval *king_mcp_public_server_get(zval *server, const char *key) +{ + if (server == NULL || Z_TYPE_P(server) != IS_ARRAY) { + return NULL; + } + + return zend_hash_str_find(Z_ARRVAL_P(server), key, strlen(key)); +} + +static zend_result king_mcp_public_server_normalize(zval *definition, zval *return_value) +{ + zval server_info; + + if (definition == NULL || Z_TYPE_P(definition) != IS_ARRAY) { + king_set_error("MCP server definition must be an array."); + return FAILURE; + } + + ZVAL_DUP(return_value, definition); + SEPARATE_ARRAY(return_value); + add_assoc_bool(return_value, "__king_mcp_server", 1); + + if (king_mcp_public_server_get(return_value, "protocol_version") == NULL) { + add_assoc_string(return_value, "protocol_version", KING_MCP_PUBLIC_PROTOCOL_VERSION); + } + + if (king_mcp_public_server_get(return_value, "serverInfo") == NULL) { + array_init(&server_info); + add_assoc_string(&server_info, "name", "king-mcp-server"); + add_assoc_string(&server_info, "version", "1.0.0"); + add_assoc_zval(return_value, "serverInfo", &server_info); + } + + return SUCCESS; +} + +static zend_string *king_mcp_public_protocol_version(zval *server, zval *message) +{ + zval *params; + zval *client_version; + zval *server_version = king_mcp_public_server_get(server, "protocol_version"); + + params = zend_hash_str_find(Z_ARRVAL_P(message), "params", sizeof("params") - 1); + if (params != NULL && Z_TYPE_P(params) == IS_ARRAY) { + client_version = zend_hash_str_find( + Z_ARRVAL_P(params), + "protocolVersion", + sizeof("protocolVersion") - 1 + ); + if (client_version != NULL && Z_TYPE_P(client_version) == IS_STRING) { + return zend_string_copy(Z_STR_P(client_version)); + } + } + + if (server_version != NULL && Z_TYPE_P(server_version) == IS_STRING) { + return zend_string_copy(Z_STR_P(server_version)); + } + + return zend_string_init(KING_MCP_PUBLIC_PROTOCOL_VERSION, sizeof(KING_MCP_PUBLIC_PROTOCOL_VERSION) - 1, 0); +} + +static bool king_mcp_public_has_registry(zval *server, const char *key) +{ + zval *registry = king_mcp_public_server_get(server, key); + + return registry != NULL && Z_TYPE_P(registry) == IS_ARRAY && zend_hash_num_elements(Z_ARRVAL_P(registry)) > 0; +} + +static void king_mcp_public_add_capability_object(zval *capabilities, const char *name) +{ + zval capability; + + object_init(&capability); + add_property_bool(&capability, "listChanged", 0); + add_assoc_zval(capabilities, name, &capability); +} + +static void king_mcp_public_build_initialize_result(zval *server, zval *message, zval *result) +{ + zval capabilities; + zval server_info_copy; + zval *server_info; + zval *instructions; + zend_string *protocol_version = king_mcp_public_protocol_version(server, message); + + array_init(result); + add_assoc_str(result, "protocolVersion", protocol_version); + + array_init(&capabilities); + if (king_mcp_public_has_registry(server, "tools")) { + king_mcp_public_add_capability_object(&capabilities, "tools"); + } + if (king_mcp_public_has_registry(server, "resources")) { + king_mcp_public_add_capability_object(&capabilities, "resources"); + } + if (king_mcp_public_has_registry(server, "prompts")) { + king_mcp_public_add_capability_object(&capabilities, "prompts"); + } + add_assoc_zval(result, "capabilities", &capabilities); + + server_info = king_mcp_public_server_get(server, "serverInfo"); + if (server_info != NULL) { + ZVAL_COPY(&server_info_copy, server_info); + add_assoc_zval(result, "serverInfo", &server_info_copy); + } + + instructions = king_mcp_public_server_get(server, "instructions"); + if (instructions != NULL && Z_TYPE_P(instructions) == IS_STRING) { + add_assoc_str(result, "instructions", zend_string_copy(Z_STR_P(instructions))); + } +} + +static zval *king_mcp_public_find_named_entry(zval *server, const char *registry_name, zend_string *name) +{ + zval *registry = king_mcp_public_server_get(server, registry_name); + zval *entry; + + if (registry == NULL || Z_TYPE_P(registry) != IS_ARRAY || name == NULL) { + return NULL; + } + + entry = zend_hash_find(Z_ARRVAL_P(registry), name); + if (entry != NULL) { + return entry; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(registry), entry) { + zval *entry_name; + + if (Z_TYPE_P(entry) != IS_ARRAY) { + continue; + } + + entry_name = zend_hash_str_find(Z_ARRVAL_P(entry), "name", sizeof("name") - 1); + if (entry_name != NULL && Z_TYPE_P(entry_name) == IS_STRING && zend_string_equals(Z_STR_P(entry_name), name)) { + return entry; + } + } ZEND_HASH_FOREACH_END(); + + return NULL; +} + +static zval *king_mcp_public_find_resource_by_uri(zval *server, zend_string *uri) +{ + zval *resource = king_mcp_public_find_named_entry(server, "resources", uri); + zval *registry; + zval *entry; + + if (resource != NULL) { + return resource; + } + + registry = king_mcp_public_server_get(server, "resources"); + if (registry == NULL || Z_TYPE_P(registry) != IS_ARRAY) { + return NULL; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(registry), entry) { + zval *entry_uri; + + if (Z_TYPE_P(entry) != IS_ARRAY) { + continue; + } + + entry_uri = zend_hash_str_find(Z_ARRVAL_P(entry), "uri", sizeof("uri") - 1); + if (entry_uri != NULL && Z_TYPE_P(entry_uri) == IS_STRING && zend_string_equals(Z_STR_P(entry_uri), uri)) { + return entry; + } + } ZEND_HASH_FOREACH_END(); + + return NULL; +} + +static void king_mcp_public_copy_optional_field(zval *target, zval *source, const char *key) +{ + zval *value; + zval copy; + + if (source == NULL || Z_TYPE_P(source) != IS_ARRAY) { + return; + } + + value = zend_hash_str_find(Z_ARRVAL_P(source), key, strlen(key)); + if (value != NULL) { + ZVAL_COPY(©, value); + add_assoc_zval(target, key, ©); + } +} + +static void king_mcp_public_build_tools_list(zval *server, zval *result) +{ + zval tools_list; + zval *registry = king_mcp_public_server_get(server, "tools"); + + array_init(result); + array_init(&tools_list); + + if (registry != NULL && Z_TYPE_P(registry) == IS_ARRAY) { + zend_string *key; + zval *entry; + + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(registry), key, entry) { + zval tool; + zval *name; + + if (Z_TYPE_P(entry) != IS_ARRAY) { + continue; + } + + array_init(&tool); + name = zend_hash_str_find(Z_ARRVAL_P(entry), "name", sizeof("name") - 1); + if (name != NULL && Z_TYPE_P(name) == IS_STRING) { + add_assoc_str(&tool, "name", zend_string_copy(Z_STR_P(name))); + } else if (key != NULL) { + add_assoc_str(&tool, "name", zend_string_copy(key)); + } else { + zval_ptr_dtor(&tool); + continue; + } + + king_mcp_public_copy_optional_field(&tool, entry, "title"); + king_mcp_public_copy_optional_field(&tool, entry, "description"); + king_mcp_public_copy_optional_field(&tool, entry, "inputSchema"); + king_mcp_public_copy_optional_field(&tool, entry, "outputSchema"); + king_mcp_public_copy_optional_field(&tool, entry, "annotations"); + add_next_index_zval(&tools_list, &tool); + } ZEND_HASH_FOREACH_END(); + } + + add_assoc_zval(result, "tools", &tools_list); +} + +static zend_result king_mcp_public_call_handler( + zval *handler, + zval *arguments, + zval *context, + zval *handler_result) +{ + zend_fcall_info fci; + zend_fcall_info_cache fcc; + zval params[2]; + zend_result status; + + if (zend_fcall_info_init(handler, 0, &fci, &fcc, NULL, NULL) != SUCCESS) { + king_set_error("MCP handler is not callable."); + return FAILURE; + } + + ZVAL_COPY(¶ms[0], arguments); + if (context != NULL) { + ZVAL_COPY(¶ms[1], context); + } else { + array_init(¶ms[1]); + } + + ZVAL_UNDEF(handler_result); + fci.params = params; + fci.param_count = 2; + fci.retval = handler_result; + status = zend_call_function(&fci, &fcc); + + zval_ptr_dtor(¶ms[0]); + zval_ptr_dtor(¶ms[1]); + + if (status != SUCCESS || EG(exception)) { + if (!EG(exception)) { + king_set_error("MCP handler invocation failed."); + } + return FAILURE; + } + + return SUCCESS; +} + +static void king_mcp_public_tool_result_from_handler(zval *handler_result, zval *result) +{ + zval content; + zval item; + zval structured; + zend_string *json; + + if (Z_TYPE_P(handler_result) == IS_ARRAY) { + if ( + zend_hash_str_exists(Z_ARRVAL_P(handler_result), "content", sizeof("content") - 1) + || zend_hash_str_exists(Z_ARRVAL_P(handler_result), "structuredContent", sizeof("structuredContent") - 1) + || zend_hash_str_exists(Z_ARRVAL_P(handler_result), "isError", sizeof("isError") - 1) + ) { + ZVAL_COPY(result, handler_result); + return; + } + } + + array_init(result); + array_init(&content); + array_init(&item); + add_assoc_string(&item, "type", "text"); + + if (Z_TYPE_P(handler_result) == IS_STRING) { + add_assoc_str(&item, "text", zend_string_copy(Z_STR_P(handler_result))); + } else if (king_mcp_public_json_encode(handler_result, &json) == SUCCESS) { + add_assoc_str(&item, "text", json); + ZVAL_COPY(&structured, handler_result); + add_assoc_zval(result, "structuredContent", &structured); + } else { + add_assoc_string(&item, "text", "null"); + } + + add_next_index_zval(&content, &item); + add_assoc_zval(result, "content", &content); +} + +static zend_result king_mcp_public_handle_tool_call( + zval *server, + zval *message, + zval *context, + zval *result) +{ + zval *params; + zval *name; + zval *arguments; + zval *tool; + zval *handler; + zval empty_arguments; + zval handler_result; + + params = zend_hash_str_find(Z_ARRVAL_P(message), "params", sizeof("params") - 1); + if (params == NULL || Z_TYPE_P(params) != IS_ARRAY) { + king_set_error("MCP tools/call requires params."); + return FAILURE; + } + + name = zend_hash_str_find(Z_ARRVAL_P(params), "name", sizeof("name") - 1); + if (name == NULL || Z_TYPE_P(name) != IS_STRING || Z_STRLEN_P(name) == 0) { + king_set_error("MCP tools/call requires params.name."); + return FAILURE; + } + + tool = king_mcp_public_find_named_entry(server, "tools", Z_STR_P(name)); + if (tool == NULL || Z_TYPE_P(tool) != IS_ARRAY) { + king_set_error("MCP tool is not registered."); + return FAILURE; + } + + handler = zend_hash_str_find(Z_ARRVAL_P(tool), "handler", sizeof("handler") - 1); + if (handler == NULL) { + king_set_error("MCP tool has no handler."); + return FAILURE; + } + + arguments = zend_hash_str_find(Z_ARRVAL_P(params), "arguments", sizeof("arguments") - 1); + if (arguments == NULL || Z_TYPE_P(arguments) == IS_NULL) { + array_init(&empty_arguments); + arguments = &empty_arguments; + } else { + ZVAL_UNDEF(&empty_arguments); + } + + if (king_mcp_public_call_handler(handler, arguments, context, &handler_result) != SUCCESS) { + if (!Z_ISUNDEF(empty_arguments)) { + zval_ptr_dtor(&empty_arguments); + } + return FAILURE; + } + + king_mcp_public_tool_result_from_handler(&handler_result, result); + zval_ptr_dtor(&handler_result); + if (!Z_ISUNDEF(empty_arguments)) { + zval_ptr_dtor(&empty_arguments); + } + + return SUCCESS; +} + +static void king_mcp_public_build_registry_list( + zval *server, + const char *registry_name, + const char *result_key, + zval *result) +{ + zval entries; + zval *registry = king_mcp_public_server_get(server, registry_name); + + array_init(result); + array_init(&entries); + + if (registry != NULL && Z_TYPE_P(registry) == IS_ARRAY) { + zend_string *key; + zval *entry; + + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(registry), key, entry) { + zval listed; + zval *name; + + if (Z_TYPE_P(entry) != IS_ARRAY) { + continue; + } + + array_init(&listed); + name = zend_hash_str_find(Z_ARRVAL_P(entry), "name", sizeof("name") - 1); + if (name != NULL && Z_TYPE_P(name) == IS_STRING) { + add_assoc_str(&listed, "name", zend_string_copy(Z_STR_P(name))); + } else if (key != NULL) { + add_assoc_str(&listed, "name", zend_string_copy(key)); + } + king_mcp_public_copy_optional_field(&listed, entry, "uri"); + king_mcp_public_copy_optional_field(&listed, entry, "title"); + king_mcp_public_copy_optional_field(&listed, entry, "description"); + king_mcp_public_copy_optional_field(&listed, entry, "mimeType"); + king_mcp_public_copy_optional_field(&listed, entry, "arguments"); + add_next_index_zval(&entries, &listed); + } ZEND_HASH_FOREACH_END(); + } + + add_assoc_zval(result, result_key, &entries); +} + +static zend_result king_mcp_public_handle_resource_read( + zval *server, + zval *message, + zval *context, + zval *result) +{ + zval *params = zend_hash_str_find(Z_ARRVAL_P(message), "params", sizeof("params") - 1); + zval *uri; + zval *resource; + zval *handler; + zval handler_result; + zval contents; + zval content; + + if (params == NULL || Z_TYPE_P(params) != IS_ARRAY) { + king_set_error("MCP resources/read requires params."); + return FAILURE; + } + + uri = zend_hash_str_find(Z_ARRVAL_P(params), "uri", sizeof("uri") - 1); + if (uri == NULL || Z_TYPE_P(uri) != IS_STRING) { + king_set_error("MCP resources/read requires params.uri."); + return FAILURE; + } + + resource = king_mcp_public_find_resource_by_uri(server, Z_STR_P(uri)); + if (resource == NULL || Z_TYPE_P(resource) != IS_ARRAY) { + king_set_error("MCP resource is not registered."); + return FAILURE; + } + + handler = zend_hash_str_find(Z_ARRVAL_P(resource), "handler", sizeof("handler") - 1); + if (handler != NULL) { + if (king_mcp_public_call_handler(handler, params, context, &handler_result) != SUCCESS) { + return FAILURE; + } + ZVAL_COPY(result, &handler_result); + zval_ptr_dtor(&handler_result); + return SUCCESS; + } + + array_init(result); + array_init(&contents); + array_init(&content); + add_assoc_str(&content, "uri", zend_string_copy(Z_STR_P(uri))); + king_mcp_public_copy_optional_field(&content, resource, "mimeType"); + king_mcp_public_copy_optional_field(&content, resource, "text"); + king_mcp_public_copy_optional_field(&content, resource, "blob"); + add_next_index_zval(&contents, &content); + add_assoc_zval(result, "contents", &contents); + return SUCCESS; +} + +static zend_result king_mcp_public_handle_prompt_get( + zval *server, + zval *message, + zval *context, + zval *result) +{ + zval *params = zend_hash_str_find(Z_ARRVAL_P(message), "params", sizeof("params") - 1); + zval *name; + zval *prompt; + zval *handler; + zval handler_result; + + if (params == NULL || Z_TYPE_P(params) != IS_ARRAY) { + king_set_error("MCP prompts/get requires params."); + return FAILURE; + } + + name = zend_hash_str_find(Z_ARRVAL_P(params), "name", sizeof("name") - 1); + if (name == NULL || Z_TYPE_P(name) != IS_STRING) { + king_set_error("MCP prompts/get requires params.name."); + return FAILURE; + } + + prompt = king_mcp_public_find_named_entry(server, "prompts", Z_STR_P(name)); + if (prompt == NULL || Z_TYPE_P(prompt) != IS_ARRAY) { + king_set_error("MCP prompt is not registered."); + return FAILURE; + } + + handler = zend_hash_str_find(Z_ARRVAL_P(prompt), "handler", sizeof("handler") - 1); + if (handler != NULL) { + if (king_mcp_public_call_handler(handler, params, context, &handler_result) != SUCCESS) { + return FAILURE; + } + ZVAL_COPY(result, &handler_result); + zval_ptr_dtor(&handler_result); + return SUCCESS; + } + + ZVAL_COPY(result, prompt); + return SUCCESS; +} + +static zend_result king_mcp_public_dispatch_request( + zval *server, + zval *message, + zval *context, + zval *result) +{ + zval *method = zend_hash_str_find(Z_ARRVAL_P(message), "method", sizeof("method") - 1); + + if (method == NULL || Z_TYPE_P(method) != IS_STRING) { + king_set_error("MCP JSON-RPC request requires method."); + return FAILURE; + } + + if (zend_string_equals_literal(Z_STR_P(method), "initialize")) { + king_mcp_public_build_initialize_result(server, message, result); + return SUCCESS; + } + if (zend_string_equals_literal(Z_STR_P(method), "ping")) { + object_init(result); + return SUCCESS; + } + if (zend_string_equals_literal(Z_STR_P(method), "tools/list")) { + king_mcp_public_build_tools_list(server, result); + return SUCCESS; + } + if (zend_string_equals_literal(Z_STR_P(method), "tools/call")) { + return king_mcp_public_handle_tool_call(server, message, context, result); + } + if (zend_string_equals_literal(Z_STR_P(method), "resources/list")) { + king_mcp_public_build_registry_list(server, "resources", "resources", result); + return SUCCESS; + } + if (zend_string_equals_literal(Z_STR_P(method), "resources/read")) { + return king_mcp_public_handle_resource_read(server, message, context, result); + } + if (zend_string_equals_literal(Z_STR_P(method), "prompts/list")) { + king_mcp_public_build_registry_list(server, "prompts", "prompts", result); + return SUCCESS; + } + if (zend_string_equals_literal(Z_STR_P(method), "prompts/get")) { + return king_mcp_public_handle_prompt_get(server, message, context, result); + } + + king_set_error("MCP JSON-RPC method is not registered."); + return FAILURE; +} + +static zend_result king_mcp_public_handle_message_zval( + zval *server, + zval *message, + zval *context, + zval *response_out, + bool *has_response) +{ + zval id; + zval result; + zval *jsonrpc; + zval *method; + + if (has_response != NULL) { + *has_response = false; + } + + if (message == NULL || Z_TYPE_P(message) != IS_ARRAY || king_mcp_public_array_is_list(message)) { + ZVAL_NULL(&id); + king_mcp_public_build_error_response(response_out, &id, -32600, "Invalid JSON-RPC request."); + if (has_response != NULL) { + *has_response = true; + } + return SUCCESS; + } + + jsonrpc = zend_hash_str_find(Z_ARRVAL_P(message), "jsonrpc", sizeof("jsonrpc") - 1); + if (jsonrpc == NULL || Z_TYPE_P(jsonrpc) != IS_STRING || !zend_string_equals_literal(Z_STR_P(jsonrpc), "2.0")) { + king_mcp_public_copy_id_or_null(message, &id); + king_mcp_public_build_error_response(response_out, &id, -32600, "JSON-RPC version must be 2.0."); + zval_ptr_dtor(&id); + if (has_response != NULL) { + *has_response = true; + } + return SUCCESS; + } + + method = zend_hash_str_find(Z_ARRVAL_P(message), "method", sizeof("method") - 1); + if (method == NULL) { + return SUCCESS; + } + + if (!zend_hash_str_exists(Z_ARRVAL_P(message), "id", sizeof("id") - 1)) { + return SUCCESS; + } + + king_mcp_public_copy_id_or_null(message, &id); + ZVAL_UNDEF(&result); + if (king_mcp_public_dispatch_request(server, message, context, &result) != SUCCESS) { + king_mcp_public_build_error_response( + response_out, + &id, + -32601, + king_get_error()[0] != '\0' ? king_get_error() : "MCP method failed." + ); + zval_ptr_dtor(&id); + if (has_response != NULL) { + *has_response = true; + } + return SUCCESS; + } + + king_mcp_public_build_result_response(response_out, &id, &result); + zval_ptr_dtor(&id); + zval_ptr_dtor(&result); + if (has_response != NULL) { + *has_response = true; + } + + return SUCCESS; +} + +static zend_result king_mcp_public_handle_jsonrpc( + zval *server, + zval *message_input, + zval *context, + zval *response_out, + bool *has_response) +{ + zval decoded; + zend_result status; + + if (Z_TYPE_P(message_input) == IS_STRING) { + if (king_mcp_public_json_decode(Z_STR_P(message_input), &decoded) != SUCCESS) { + zval id; + ZVAL_NULL(&id); + king_mcp_public_build_error_response(response_out, &id, -32700, "Parse error."); + if (has_response != NULL) { + *has_response = true; + } + return SUCCESS; + } + status = king_mcp_public_handle_message_zval(server, &decoded, context, response_out, has_response); + zval_ptr_dtor(&decoded); + return status; + } + + return king_mcp_public_handle_message_zval(server, message_input, context, response_out, has_response); +} + +static zend_result king_mcp_public_handle_jsonrpc_encoded( + zval *server, + zval *message_input, + zval *context, + zend_string **json_out, + bool *has_response) +{ + zval response; + + ZVAL_UNDEF(&response); + if (king_mcp_public_handle_jsonrpc(server, message_input, context, &response, has_response) != SUCCESS) { + return FAILURE; + } + + if (has_response != NULL && !*has_response) { + return SUCCESS; + } + + if (king_mcp_public_json_encode(&response, json_out) != SUCCESS) { + zval_ptr_dtor(&response); + return FAILURE; + } + + zval_ptr_dtor(&response); + return SUCCESS; +} diff --git a/extension/src/mcp/php_binding/public_server_http_helpers.inc b/extension/src/mcp/php_binding/public_server_http_helpers.inc new file mode 100644 index 000000000..086e23aa0 --- /dev/null +++ b/extension/src/mcp/php_binding/public_server_http_helpers.inc @@ -0,0 +1,132 @@ +static zend_string *king_mcp_public_first_header_value(zval *headers, const char *name) +{ + zval *value; + zend_string *key; + + if (headers == NULL || Z_TYPE_P(headers) != IS_ARRAY) { + return NULL; + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(headers), key, value) { + if (key == NULL || strcasecmp(ZSTR_VAL(key), name) != 0) { + continue; + } + + if (Z_TYPE_P(value) == IS_STRING) { + return Z_STR_P(value); + } + if (Z_TYPE_P(value) == IS_ARRAY) { + zval *first = zend_hash_index_find(Z_ARRVAL_P(value), 0); + if (first != NULL && Z_TYPE_P(first) == IS_STRING) { + return Z_STR_P(first); + } + } + } ZEND_HASH_FOREACH_END(); + + return NULL; +} + +static bool king_mcp_public_header_contains(zval *request, const char *name, const char *needle) +{ + zval *headers = zend_hash_str_find(Z_ARRVAL_P(request), "headers", sizeof("headers") - 1); + zend_string *value = king_mcp_public_first_header_value(headers, name); + + if (value == NULL) { + return false; + } + + return php_memnstr(ZSTR_VAL(value), needle, strlen(needle), ZSTR_VAL(value) + ZSTR_LEN(value)) != NULL; +} + +static bool king_mcp_public_http_prefers_sse(zval *server, zval *request) +{ + zval *http = king_mcp_public_server_get(server, "streamable_http"); + zval *prefer_sse; + + if (!king_mcp_public_header_contains(request, "accept", "text/event-stream")) { + return false; + } + + if (http == NULL || Z_TYPE_P(http) != IS_ARRAY) { + return false; + } + + prefer_sse = zend_hash_str_find(Z_ARRVAL_P(http), "prefer_sse", sizeof("prefer_sse") - 1); + return prefer_sse != NULL && zend_is_true(prefer_sse); +} + +static bool king_mcp_public_http_origin_allowed(zval *server, zval *request) +{ + zval *headers = zend_hash_str_find(Z_ARRVAL_P(request), "headers", sizeof("headers") - 1); + zend_string *origin = king_mcp_public_first_header_value(headers, "origin"); + zval *http; + zval *allowed_origins; + zval *allowed; + + if (origin == NULL || ZSTR_LEN(origin) == 0) { + return true; + } + + http = king_mcp_public_server_get(server, "streamable_http"); + if (http == NULL || Z_TYPE_P(http) != IS_ARRAY) { + return false; + } + + allowed_origins = zend_hash_str_find( + Z_ARRVAL_P(http), + "allowed_origins", + sizeof("allowed_origins") - 1 + ); + if (allowed_origins == NULL || Z_TYPE_P(allowed_origins) != IS_ARRAY) { + return false; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(allowed_origins), allowed) { + if (Z_TYPE_P(allowed) != IS_STRING) { + continue; + } + if ( + zend_string_equals_literal(Z_STR_P(allowed), "*") + || zend_string_equals(Z_STR_P(allowed), origin) + ) { + return true; + } + } ZEND_HASH_FOREACH_END(); + + return false; +} + +static void king_mcp_public_http_response( + zval *return_value, + zend_long status, + const char *content_type, + zend_string *body) +{ + zval headers; + + array_init(return_value); + add_assoc_long(return_value, "status", status); + array_init(&headers); + if (content_type != NULL) { + add_assoc_string(&headers, "content-type", content_type); + } + add_assoc_zval(return_value, "headers", &headers); + if (body != NULL) { + add_assoc_str(return_value, "body", zend_string_copy(body)); + } else { + add_assoc_string(return_value, "body", ""); + } +} + +static zend_string *king_mcp_public_sse_body(zend_string *json) +{ + smart_str body = {0}; + + smart_str_appends(&body, "event: message\n"); + smart_str_appends(&body, "data: "); + smart_str_append(&body, json); + smart_str_appends(&body, "\n\n"); + smart_str_0(&body); + + return body.s != NULL ? body.s : zend_string_init("", 0, 0); +} diff --git a/extension/src/mcp/php_binding/public_server_object_api.inc b/extension/src/mcp/php_binding/public_server_object_api.inc new file mode 100644 index 000000000..8be352f5d --- /dev/null +++ b/extension/src/mcp/php_binding/public_server_object_api.inc @@ -0,0 +1,198 @@ +PHP_METHOD(King_MCPServer, __construct) +{ + zval *definition; + zval *options = NULL; + zval normalized; + king_mcp_server_object *intern; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_ARRAY(definition) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + ZVAL_UNDEF(&normalized); + if (king_mcp_public_server_normalize(definition, &normalized) != SUCCESS) { + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s", + king_get_error()[0] != '\0' ? king_get_error() : "Invalid MCP server definition." + ); + RETURN_THROWS(); + } + + if (king_mcp_public_server_apply_creation_options(&normalized, options) != SUCCESS) { + zval_ptr_dtor(&normalized); + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "%s", + king_get_error()[0] != '\0' ? king_get_error() : "Invalid MCP server options." + ); + RETURN_THROWS(); + } + + intern = php_king_mcp_server_obj_from_zend(Z_OBJ_P(ZEND_THIS)); + if (!Z_ISUNDEF(intern->definition)) { + zval_ptr_dtor(&intern->definition); + } + ZVAL_COPY_VALUE(&intern->definition, &normalized); + king_set_error(""); +} + +PHP_METHOD(King_MCPServer, handleJsonRpc) +{ + zval *message; + zval *context = NULL; + zend_string *json = NULL; + bool has_response = false; + king_mcp_server_object *intern; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_ZVAL(message) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(context) + ZEND_PARSE_PARAMETERS_END(); + + intern = php_king_mcp_server_obj_from_zend(Z_OBJ_P(ZEND_THIS)); + if (Z_ISUNDEF(intern->definition) || Z_TYPE(intern->definition) != IS_ARRAY) { + zend_throw_exception(king_ce_runtime_exception, "King\\MCPServer is not initialized.", 0); + RETURN_THROWS(); + } + + if ( + king_mcp_public_handle_jsonrpc_encoded( + &intern->definition, + message, + context, + &json, + &has_response + ) != SUCCESS + ) { + if (EG(exception)) { + RETURN_THROWS(); + } + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "%s", + king_get_error()[0] != '\0' ? king_get_error() : "MCP JSON-RPC handling failed." + ); + RETURN_THROWS(); + } + + if (!has_response) { + RETURN_NULL(); + } + + king_set_error(""); + RETURN_STR(json); +} + +PHP_METHOD(King_MCPServer, handleHttp) +{ + zval *request; + zval *context = NULL; + king_mcp_server_object *intern; + zval params[3]; + zval function_name; + zval result; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_ARRAY(request) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(context) + ZEND_PARSE_PARAMETERS_END(); + + intern = php_king_mcp_server_obj_from_zend(Z_OBJ_P(ZEND_THIS)); + if (Z_ISUNDEF(intern->definition) || Z_TYPE(intern->definition) != IS_ARRAY) { + zend_throw_exception(king_ce_runtime_exception, "King\\MCPServer is not initialized.", 0); + RETURN_THROWS(); + } + + ZVAL_COPY(¶ms[0], &intern->definition); + ZVAL_COPY(¶ms[1], request); + if (context != NULL) { + ZVAL_COPY(¶ms[2], context); + } else { + ZVAL_NULL(¶ms[2]); + } + ZVAL_STRING(&function_name, "king_mcp_server_handle_http"); + ZVAL_UNDEF(&result); + + if (call_user_function(EG(function_table), NULL, &function_name, &result, 3, params) != SUCCESS) { + zval_ptr_dtor(&function_name); + zval_ptr_dtor(¶ms[0]); + zval_ptr_dtor(¶ms[1]); + zval_ptr_dtor(¶ms[2]); + if (EG(exception)) { + RETURN_THROWS(); + } + RETURN_FALSE; + } + + zval_ptr_dtor(&function_name); + zval_ptr_dtor(¶ms[0]); + zval_ptr_dtor(¶ms[1]); + zval_ptr_dtor(¶ms[2]); + if (EG(exception)) { + if (!Z_ISUNDEF(result)) { + zval_ptr_dtor(&result); + } + RETURN_THROWS(); + } + + RETURN_ZVAL(&result, 0, 1); +} + +PHP_METHOD(King_MCPServer, runStdio) +{ + zval *options = NULL; + king_mcp_server_object *intern; + zval params[2]; + zval function_name; + zval result; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + intern = php_king_mcp_server_obj_from_zend(Z_OBJ_P(ZEND_THIS)); + if (Z_ISUNDEF(intern->definition) || Z_TYPE(intern->definition) != IS_ARRAY) { + zend_throw_exception(king_ce_runtime_exception, "King\\MCPServer is not initialized.", 0); + RETURN_THROWS(); + } + + ZVAL_COPY(¶ms[0], &intern->definition); + if (options != NULL) { + ZVAL_COPY(¶ms[1], options); + } else { + ZVAL_NULL(¶ms[1]); + } + ZVAL_STRING(&function_name, "king_mcp_server_stdio"); + ZVAL_UNDEF(&result); + + if (call_user_function(EG(function_table), NULL, &function_name, &result, 2, params) != SUCCESS) { + zval_ptr_dtor(&function_name); + zval_ptr_dtor(¶ms[0]); + zval_ptr_dtor(¶ms[1]); + if (EG(exception)) { + RETURN_THROWS(); + } + RETURN_FALSE; + } + + zval_ptr_dtor(&function_name); + zval_ptr_dtor(¶ms[0]); + zval_ptr_dtor(¶ms[1]); + if (EG(exception)) { + if (!Z_ISUNDEF(result)) { + zval_ptr_dtor(&result); + } + RETURN_THROWS(); + } + + RETURN_ZVAL(&result, 0, 1); +} diff --git a/extension/src/mcp/php_binding/public_server_options.inc b/extension/src/mcp/php_binding/public_server_options.inc new file mode 100644 index 000000000..5f373099f --- /dev/null +++ b/extension/src/mcp/php_binding/public_server_options.inc @@ -0,0 +1,207 @@ +static zend_result king_mcp_public_server_options_error(const char *message) +{ + king_set_error(message); + return FAILURE; +} + +static zend_result king_mcp_public_server_http_config(zval *server, zval **http_out) +{ + zval *http; + zval new_http; + + if (server == NULL || Z_TYPE_P(server) != IS_ARRAY || http_out == NULL) { + return king_mcp_public_server_options_error("MCP server options require a normalized server array."); + } + + http = king_mcp_public_server_get(server, "streamable_http"); + if (http == NULL) { + array_init(&new_http); + add_assoc_zval(server, "streamable_http", &new_http); + http = king_mcp_public_server_get(server, "streamable_http"); + } + + if (http == NULL || Z_TYPE_P(http) != IS_ARRAY) { + return king_mcp_public_server_options_error("MCP server streamable_http configuration must be an array."); + } + + SEPARATE_ARRAY(http); + *http_out = http; + return SUCCESS; +} + +static zend_result king_mcp_public_server_apply_prefer_sse(zval *server, zval *value, const char *option_name) +{ + zval *http; + char message[KING_ERR_LEN]; + + if (value == NULL || Z_TYPE_P(value) == IS_NULL) { + return SUCCESS; + } + + if (Z_TYPE_P(value) != IS_TRUE && Z_TYPE_P(value) != IS_FALSE) { + snprintf( + message, + sizeof(message), + "MCP server option '%s' must be boolean.", + option_name + ); + return king_mcp_public_server_options_error(message); + } + + if (king_mcp_public_server_http_config(server, &http) != SUCCESS) { + return FAILURE; + } + + add_assoc_bool(http, "prefer_sse", zend_is_true(value)); + return SUCCESS; +} + +static zend_result king_mcp_public_server_apply_allowed_origins(zval *server, zval *value, const char *option_name) +{ + zval *http; + zval origins; + zval *origin; + char message[KING_ERR_LEN]; + + if (value == NULL || Z_TYPE_P(value) == IS_NULL) { + return SUCCESS; + } + + if (Z_TYPE_P(value) != IS_ARRAY || !zend_array_is_list(Z_ARRVAL_P(value))) { + snprintf( + message, + sizeof(message), + "MCP server option '%s' must be a list of origin strings.", + option_name + ); + return king_mcp_public_server_options_error(message); + } + + array_init(&origins); + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(value), origin) { + if (Z_TYPE_P(origin) != IS_STRING || Z_STRLEN_P(origin) == 0) { + zval_ptr_dtor(&origins); + snprintf( + message, + sizeof(message), + "MCP server option '%s' must contain only non-empty origin strings.", + option_name + ); + return king_mcp_public_server_options_error(message); + } + add_next_index_str(&origins, zend_string_copy(Z_STR_P(origin))); + } ZEND_HASH_FOREACH_END(); + + if (king_mcp_public_server_http_config(server, &http) != SUCCESS) { + zval_ptr_dtor(&origins); + return FAILURE; + } + + add_assoc_zval(http, "allowed_origins", &origins); + return SUCCESS; +} + +static zend_result king_mcp_public_server_apply_streamable_http_options(zval *server, zval *options) +{ + zend_string *key; + zval *value; + char message[KING_ERR_LEN]; + + if (options == NULL || Z_TYPE_P(options) == IS_NULL) { + return SUCCESS; + } + + if (Z_TYPE_P(options) != IS_ARRAY) { + return king_mcp_public_server_options_error("MCP server option 'streamable_http' must be an array."); + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(options), key, value) { + if (key == NULL) { + return king_mcp_public_server_options_error( + "MCP server option 'streamable_http' only accepts string keys." + ); + } + + if (zend_string_equals_literal(key, "prefer_sse")) { + if (king_mcp_public_server_apply_prefer_sse(server, value, "streamable_http.prefer_sse") != SUCCESS) { + return FAILURE; + } + continue; + } + + if (zend_string_equals_literal(key, "allowed_origins")) { + if ( + king_mcp_public_server_apply_allowed_origins( + server, + value, + "streamable_http.allowed_origins" + ) != SUCCESS + ) { + return FAILURE; + } + continue; + } + + snprintf( + message, + sizeof(message), + "MCP server option 'streamable_http.%s' is not supported.", + ZSTR_VAL(key) + ); + return king_mcp_public_server_options_error(message); + } ZEND_HASH_FOREACH_END(); + + return SUCCESS; +} + +static zend_result king_mcp_public_server_apply_creation_options(zval *server, zval *options) +{ + zend_string *key; + zval *value; + char message[KING_ERR_LEN]; + + if (options == NULL || Z_TYPE_P(options) == IS_NULL) { + return SUCCESS; + } + + if (Z_TYPE_P(options) != IS_ARRAY) { + return king_mcp_public_server_options_error("MCP server options must be an array."); + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(options), key, value) { + if (key == NULL) { + return king_mcp_public_server_options_error("MCP server options only accept string keys."); + } + + if (zend_string_equals_literal(key, "streamable_http")) { + if (king_mcp_public_server_apply_streamable_http_options(server, value) != SUCCESS) { + return FAILURE; + } + continue; + } + + if (zend_string_equals_literal(key, "prefer_sse")) { + if (king_mcp_public_server_apply_prefer_sse(server, value, "prefer_sse") != SUCCESS) { + return FAILURE; + } + continue; + } + + if (zend_string_equals_literal(key, "allowed_origins")) { + if (king_mcp_public_server_apply_allowed_origins(server, value, "allowed_origins") != SUCCESS) { + return FAILURE; + } + continue; + } + + snprintf( + message, + sizeof(message), + "MCP server option '%s' is not supported.", + ZSTR_VAL(key) + ); + return king_mcp_public_server_options_error(message); + } ZEND_HASH_FOREACH_END(); + + return SUCCESS; +} diff --git a/extension/src/mcp/php_binding/registration.inc b/extension/src/mcp/php_binding/registration.inc new file mode 100644 index 000000000..c386790e0 --- /dev/null +++ b/extension/src/mcp/php_binding/registration.inc @@ -0,0 +1,43 @@ +#include "resource_destructors.inc" + +void king_mcp_register_resource_types(int module_number) +{ + le_king_mcp = zend_register_list_destructors_ex( + king_mcp_resource_dtor, NULL, "King\\MCP", module_number); +} + +void king_mcp_register_exception_classes(void) +{ + king_ce_mcp_exception = king_register_exception( + "King\\MCPException", king_ce_exception); + king_ce_mcp_connection_error = king_register_exception( + "King\\MCPConnectionException", king_ce_mcp_exception); + king_ce_mcp_protocol_error = king_register_exception( + "King\\MCPProtocolException", king_ce_mcp_exception); + king_ce_mcp_timeout = king_register_exception( + "King\\MCPTimeoutException", king_ce_mcp_exception); + king_ce_mcp_data_error = king_register_exception( + "King\\MCPDataException", king_ce_mcp_exception); +} + +void king_mcp_register_classes(void) +{ + king_ce_mcp = king_register_class_with_flags( + "King\\MCP", + NULL, + king_mcp_class_methods, + ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES + ); + king_ce_mcp_server = king_register_class_with_flags( + "King\\MCPServer", + NULL, + king_mcp_server_class_methods, + ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES + ); +} + +void king_mcp_init_object_handlers(void) +{ + king_mcp_object_handlers_init(); + king_mcp_server_object_handlers_init(); +} diff --git a/extension/src/mcp/php_binding/resource_destructors.inc b/extension/src/mcp/php_binding/resource_destructors.inc new file mode 100644 index 000000000..38c543684 --- /dev/null +++ b/extension/src/mcp/php_binding/resource_destructors.inc @@ -0,0 +1,12 @@ +/* + * MCP-owned Zend resource destructor. + */ + +#include "mcp/mcp.h" + +static void king_mcp_resource_dtor(zend_resource *rsrc) +{ + if (rsrc->ptr) { + king_mcp_state_free((king_mcp_state *) rsrc->ptr); + } +} diff --git a/extension/src/mcp/php_binding/resource_ids.inc b/extension/src/mcp/php_binding/resource_ids.inc new file mode 100644 index 000000000..022e6a32e --- /dev/null +++ b/extension/src/mcp/php_binding/resource_ids.inc @@ -0,0 +1,5 @@ +/* + * MCP Zend resource-id storage. + */ + +int le_king_mcp = -1; diff --git a/extension/src/mcp/php_binding/state.inc b/extension/src/mcp/php_binding/state.inc new file mode 100644 index 000000000..4de38b6ed --- /dev/null +++ b/extension/src/mcp/php_binding/state.inc @@ -0,0 +1,6 @@ +/* + * MCP PHP binding state storage aggregation. + */ + +#include "class_entries.inc" +#include "resource_ids.inc" diff --git a/extension/src/php_king/mcp/transfer_helpers.inc b/extension/src/mcp/php_binding/transfer_helpers.inc similarity index 80% rename from extension/src/php_king/mcp/transfer_helpers.inc rename to extension/src/mcp/php_binding/transfer_helpers.inc index 46f806f5a..074b01fa8 100644 --- a/extension/src/php_king/mcp/transfer_helpers.inc +++ b/extension/src/mcp/php_binding/transfer_helpers.inc @@ -1,94 +1,3 @@ -static zend_string *king_mcp_base64url_encode_bytes( - const unsigned char *value, - size_t value_len) -{ - zend_string *encoded = php_base64_encode(value, value_len); - size_t read_index = 0; - size_t write_index = 0; - - if (encoded == NULL) { - return NULL; - } - - for (read_index = 0; read_index < ZSTR_LEN(encoded); read_index++) { - char current = ZSTR_VAL(encoded)[read_index]; - - if (current == '=') { - continue; - } - if (current == '+') { - current = '-'; - } else if (current == '/') { - current = '_'; - } - - ZSTR_VAL(encoded)[write_index++] = current; - } - - ZSTR_VAL(encoded)[write_index] = '\0'; - ZSTR_LEN(encoded) = write_index; - - return encoded; -} - -static zend_string *king_mcp_transfer_key_create( - const char *service_name, - size_t service_name_len, - const char *method_name, - size_t method_name_len, - const char *transfer_id, - size_t transfer_id_len) -{ - zend_string *encoded_service = NULL; - zend_string *encoded_method = NULL; - zend_string *encoded_id = NULL; - size_t key_len; - zend_string *key = NULL; - char *cursor; - - encoded_service = king_mcp_base64url_encode_bytes((const unsigned char *) service_name, service_name_len); - encoded_method = king_mcp_base64url_encode_bytes((const unsigned char *) method_name, method_name_len); - encoded_id = king_mcp_base64url_encode_bytes((const unsigned char *) transfer_id, transfer_id_len); - if (encoded_service == NULL || encoded_method == NULL || encoded_id == NULL) { - if (encoded_service != NULL) { - zend_string_release(encoded_service); - } - if (encoded_method != NULL) { - zend_string_release(encoded_method); - } - if (encoded_id != NULL) { - zend_string_release(encoded_id); - } - return NULL; - } - - key_len = (sizeof("mcp.v1.") - 1) - + ZSTR_LEN(encoded_service) - + 1 - + ZSTR_LEN(encoded_method) - + 1 - + ZSTR_LEN(encoded_id); - key = zend_string_alloc(key_len, 0); - cursor = ZSTR_VAL(key); - - memcpy(cursor, "mcp.v1.", sizeof("mcp.v1.") - 1); - cursor += sizeof("mcp.v1.") - 1; - memcpy(cursor, ZSTR_VAL(encoded_service), ZSTR_LEN(encoded_service)); - cursor += ZSTR_LEN(encoded_service); - *cursor++ = '.'; - memcpy(cursor, ZSTR_VAL(encoded_method), ZSTR_LEN(encoded_method)); - cursor += ZSTR_LEN(encoded_method); - *cursor++ = '.'; - memcpy(cursor, ZSTR_VAL(encoded_id), ZSTR_LEN(encoded_id)); - ZSTR_VAL(key)[key_len] = '\0'; - - zend_string_release(encoded_service); - zend_string_release(encoded_method); - zend_string_release(encoded_id); - - return key; -} - static bool king_mcp_contains_path_separator( const char *value, size_t value_len) @@ -483,5 +392,4 @@ static void king_mcp_throw_state_error( zend_throw_exception_ex(exception_ce, 0, "%s", message); } -#include "include/king_globals.h" - +#include "php_king/globals.h" diff --git a/extension/src/mcp/registration.inc b/extension/src/mcp/registration.inc new file mode 100644 index 000000000..07d866e24 --- /dev/null +++ b/extension/src/mcp/registration.inc @@ -0,0 +1,6 @@ +/* + * MCP registration aggregation. The MCP translation unit includes this module + * boundary instead of PHP-binding registration leaves. + */ + +#include "php_binding/registration.inc" diff --git a/extension/src/mcp/mcp/lifecycle_and_api.inc b/extension/src/mcp/runtime/lifecycle_and_api.inc similarity index 98% rename from extension/src/mcp/mcp/lifecycle_and_api.inc rename to extension/src/mcp/runtime/lifecycle_and_api.inc index f364b14d0..5e4b377f2 100644 --- a/extension/src/mcp/mcp/lifecycle_and_api.inc +++ b/extension/src/mcp/runtime/lifecycle_and_api.inc @@ -19,6 +19,9 @@ void king_mcp_state_free(king_mcp_state *state) if (state->host != NULL) { zend_string_release(state->host); } + if (!Z_ISUNDEF(state->iibin_routes)) { + zval_ptr_dtor(&state->iibin_routes); + } zval_ptr_dtor(&state->config); king_mcp_state_close(state); efree(state); diff --git a/extension/src/mcp/mcp/remote_protocol.inc b/extension/src/mcp/runtime/remote_protocol.inc similarity index 97% rename from extension/src/mcp/mcp/remote_protocol.inc rename to extension/src/mcp/runtime/remote_protocol.inc index f9ea9f3a3..3c9cfee02 100644 --- a/extension/src/mcp/mcp/remote_protocol.inc +++ b/extension/src/mcp/runtime/remote_protocol.inc @@ -428,15 +428,24 @@ king_mcp_state *king_mcp_state_create( state->host = normalized_host; state->port = port; + state->default_timeout_ms = 0; ZVAL_UNDEF(&state->config); + ZVAL_UNDEF(&state->iibin_routes); state->transport_stream = NULL; if (config != NULL && Z_TYPE_P(config) != IS_NULL) { ZVAL_COPY(&state->config, config); } + if (king_mcp_state_snapshot_iibin_routes(state, config) != SUCCESS) { + if (!Z_ISUNDEF(state->config)) { + zval_ptr_dtor(&state->config); + } + zend_string_release(state->host); + efree(state); + return NULL; + } state->closed = false; state->operation_active = false; state->last_error_kind = KING_MCP_ERROR_NONE; return state; } - diff --git a/extension/src/mcp/mcp/state_and_validation.inc b/extension/src/mcp/runtime/state_and_validation.inc similarity index 64% rename from extension/src/mcp/mcp/state_and_validation.inc rename to extension/src/mcp/runtime/state_and_validation.inc index fe896a37a..88f51a9ef 100644 --- a/extension/src/mcp/mcp/state_and_validation.inc +++ b/extension/src/mcp/runtime/state_and_validation.inc @@ -52,6 +52,226 @@ static void king_mcp_state_set_errorf( king_mcp_state_set_error_message(state, kind, message); } +static zval *king_mcp_config_find_key(zval *config, const char *key, size_t key_len) +{ + king_config_object *intern; + + if (config == NULL || Z_TYPE_P(config) == IS_NULL) { + return NULL; + } + + if (Z_TYPE_P(config) == IS_ARRAY) { + return zend_hash_str_find(Z_ARRVAL_P(config), key, key_len); + } + + if ( + Z_TYPE_P(config) == IS_OBJECT + && king_ce_config != NULL + && instanceof_function(Z_OBJCE_P(config), king_ce_config) + ) { + intern = php_king_config_obj_from_zend(Z_OBJ_P(config)); + if (!Z_ISUNDEF(intern->overrides) && Z_TYPE(intern->overrides) == IS_ARRAY) { + return zend_hash_str_find(Z_ARRVAL(intern->overrides), key, key_len); + } + } + + return NULL; +} + +static zval *king_mcp_config_find_iibin_routes(zval *config) +{ + zval *routes; + + routes = king_mcp_config_find_key( + config, + "mcp.iibin_routes", + sizeof("mcp.iibin_routes") - 1 + ); + if (routes != NULL) { + return routes; + } + + routes = king_mcp_config_find_key( + config, + "mcp.iibin.routes", + sizeof("mcp.iibin.routes") - 1 + ); + if (routes != NULL) { + return routes; + } + + return king_mcp_config_find_key( + config, + "mcp_iibin_routes", + sizeof("mcp_iibin_routes") - 1 + ); +} + +static zend_result king_mcp_validate_iibin_route_definition( + zval *route, + const char *route_key +) +{ + zval *request_schema; + zval *response_schema; + + if (Z_TYPE_P(route) != IS_ARRAY) { + king_mcp_set_errorf( + "MCP IIBIN route '%s' must be configured as an array.", + route_key + ); + return FAILURE; + } + + request_schema = zend_hash_str_find( + Z_ARRVAL_P(route), + "request_schema", + sizeof("request_schema") - 1 + ); + if ( + request_schema == NULL + || Z_TYPE_P(request_schema) != IS_STRING + || Z_STRLEN_P(request_schema) == 0 + ) { + king_mcp_set_errorf( + "MCP IIBIN route '%s' requires a non-empty request_schema.", + route_key + ); + return FAILURE; + } + + response_schema = zend_hash_str_find( + Z_ARRVAL_P(route), + "response_schema", + sizeof("response_schema") - 1 + ); + if ( + response_schema != NULL + && Z_TYPE_P(response_schema) != IS_NULL + && Z_TYPE_P(response_schema) != IS_STRING + ) { + king_mcp_set_errorf( + "MCP IIBIN route '%s' response_schema must be null or string.", + route_key + ); + return FAILURE; + } + + return SUCCESS; +} + +static zend_result king_mcp_validate_iibin_routes(zval *routes) +{ + zend_string *service_or_route_key; + zval *route_or_methods; + + if (routes == NULL || Z_TYPE_P(routes) == IS_NULL) { + return SUCCESS; + } + + if (Z_TYPE_P(routes) != IS_ARRAY) { + king_mcp_set_errorf("MCP config key mcp.iibin_routes must be an array."); + return FAILURE; + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(routes), service_or_route_key, route_or_methods) { + zend_string *method_key; + zval *nested_route; + + if (service_or_route_key == NULL) { + king_mcp_set_errorf("MCP IIBIN route keys must be strings."); + return FAILURE; + } + + if (Z_TYPE_P(route_or_methods) != IS_ARRAY) { + king_mcp_set_errorf( + "MCP IIBIN route '%s' must be configured as an array.", + ZSTR_VAL(service_or_route_key) + ); + return FAILURE; + } + + if ( + zend_hash_str_exists( + Z_ARRVAL_P(route_or_methods), + "request_schema", + sizeof("request_schema") - 1 + ) + ) { + if ( + king_mcp_validate_iibin_route_definition( + route_or_methods, + ZSTR_VAL(service_or_route_key) + ) != SUCCESS + ) { + return FAILURE; + } + continue; + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(route_or_methods), method_key, nested_route) { + zend_string *full_key; + + if (method_key == NULL) { + king_mcp_set_errorf( + "MCP IIBIN methods under service '%s' must use string keys.", + ZSTR_VAL(service_or_route_key) + ); + return FAILURE; + } + + full_key = zend_string_alloc( + ZSTR_LEN(service_or_route_key) + 1 + ZSTR_LEN(method_key), + 0 + ); + memcpy(ZSTR_VAL(full_key), ZSTR_VAL(service_or_route_key), ZSTR_LEN(service_or_route_key)); + ZSTR_VAL(full_key)[ZSTR_LEN(service_or_route_key)] = '/'; + memcpy( + ZSTR_VAL(full_key) + ZSTR_LEN(service_or_route_key) + 1, + ZSTR_VAL(method_key), + ZSTR_LEN(method_key) + ); + ZSTR_VAL(full_key)[ZSTR_LEN(service_or_route_key) + 1 + ZSTR_LEN(method_key)] = '\0'; + + if ( + king_mcp_validate_iibin_route_definition( + nested_route, + ZSTR_VAL(full_key) + ) != SUCCESS + ) { + zend_string_release(full_key); + return FAILURE; + } + + zend_string_release(full_key); + } ZEND_HASH_FOREACH_END(); + } ZEND_HASH_FOREACH_END(); + + return SUCCESS; +} + +static zend_result king_mcp_state_snapshot_iibin_routes( + king_mcp_state *state, + zval *config +) +{ + zval *routes = king_mcp_config_find_iibin_routes(config); + + ZVAL_UNDEF(&state->iibin_routes); + + if (routes == NULL || Z_TYPE_P(routes) == IS_NULL) { + array_init(&state->iibin_routes); + return SUCCESS; + } + + if (king_mcp_validate_iibin_routes(routes) != SUCCESS) { + return FAILURE; + } + + ZVAL_DUP(&state->iibin_routes, routes); + return SUCCESS; +} + static zend_string *king_mcp_base64url_encode_bytes( const unsigned char *value, size_t value_len @@ -403,4 +623,3 @@ static zend_result king_mcp_validate_peer_target( return SUCCESS; } - diff --git a/extension/src/mcp/mcp/transfer_state.inc b/extension/src/mcp/runtime/transfer_state.inc similarity index 100% rename from extension/src/mcp/mcp/transfer_state.inc rename to extension/src/mcp/runtime/transfer_state.inc diff --git a/extension/src/mcp/mcp/transport_control.inc b/extension/src/mcp/runtime/transport_control.inc similarity index 100% rename from extension/src/mcp/mcp/transport_control.inc rename to extension/src/mcp/runtime/transport_control.inc diff --git a/extension/src/mcp/state.inc b/extension/src/mcp/state.inc new file mode 100644 index 000000000..dfb994304 --- /dev/null +++ b/extension/src/mcp/state.inc @@ -0,0 +1,6 @@ +/* + * MCP state storage aggregation. The MCP translation unit includes this + * module boundary instead of PHP-binding state leaves. + */ + +#include "php_binding/state.inc" diff --git a/extension/src/media/class_entries.inc b/extension/src/media/class_entries.inc new file mode 100644 index 000000000..8ea98792e --- /dev/null +++ b/extension/src/media/class_entries.inc @@ -0,0 +1,5 @@ +/* + * Media PHP class-entry storage. + */ + +zend_class_entry *king_ce_rtp_socket = NULL; diff --git a/extension/src/media/media.c b/extension/src/media/media.c new file mode 100644 index 000000000..3da5feef8 --- /dev/null +++ b/extension/src/media/media.c @@ -0,0 +1,19 @@ +/* + * Native media PHP surface for King. + * + * This translation unit owns the King\RTP\Socket object binding, class state, + * and MINIT registration hooks. The RTP transport/resource runtime remains in + * rtp.c. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "php.h" +#include "php_king.h" +#include "media/arginfo/index.h" + +#include "state.inc" +#include "php_binding.inc" +#include "registration.inc" diff --git a/extension/src/media/php_binding.inc b/extension/src/media/php_binding.inc new file mode 100644 index 000000000..db1965f8e --- /dev/null +++ b/extension/src/media/php_binding.inc @@ -0,0 +1,285 @@ +/* + * King\RTP\Socket userland surface. The media translation unit includes this + * object boundary over the native RTP/ICE-lite/DTLS-SRTP socket runtime used + * by king_rtp_*. + */ + +#include "media/rtp.h" + +static zend_object_handlers king_rtp_socket_object_handlers; + +static void king_rtp_socket_object_free(zend_object *object) +{ + king_rtp_socket_object *intern = php_king_rtp_socket_obj_from_zend(object); + + if (!Z_ISUNDEF(intern->resource)) { + zval_ptr_dtor(&intern->resource); + ZVAL_UNDEF(&intern->resource); + } + + zend_object_std_dtor(&intern->std); +} + +static zend_object *king_rtp_socket_object_create(zend_class_entry *ce) +{ + king_rtp_socket_object *intern = zend_object_alloc(sizeof(*intern), ce); + + ZVAL_UNDEF(&intern->resource); + intern->closed = true; + zend_object_std_init(&intern->std, ce); + object_properties_init(&intern->std, ce); + intern->std.handlers = &king_rtp_socket_object_handlers; + + return &intern->std; +} + +static void king_rtp_socket_object_handlers_init(void) +{ + memcpy( + &king_rtp_socket_object_handlers, + &std_object_handlers, + sizeof(zend_object_handlers) + ); + king_rtp_socket_object_handlers.offset = XtOffsetOf(king_rtp_socket_object, std); + king_rtp_socket_object_handlers.free_obj = king_rtp_socket_object_free; + king_rtp_socket_object_handlers.clone_obj = NULL; + king_ce_rtp_socket->create_object = king_rtp_socket_object_create; +} + +static king_rtp_socket_t *king_rtp_socket_object_fetch( + zval *object, + const char *function_name +) +{ + king_rtp_socket_object *intern; + king_rtp_socket_t *socket; + + if (object == NULL || Z_TYPE_P(object) != IS_OBJECT) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "%s() requires a live King\\RTP\\Socket object.", + function_name + ); + return NULL; + } + + intern = php_king_rtp_socket_obj_from_zend(Z_OBJ_P(object)); + if (intern->closed || Z_ISUNDEF(intern->resource) || Z_TYPE(intern->resource) != IS_RESOURCE) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "%s() cannot use a closed King\\RTP\\Socket.", + function_name + ); + return NULL; + } + + socket = (king_rtp_socket_t *) zend_fetch_resource( + Z_RES(intern->resource), + "King\\RtpSocket", + le_king_rtp_socket + ); + if (socket == NULL) { + if (!EG(exception)) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "%s() could not fetch the native RTP socket resource.", + function_name + ); + } + return NULL; + } + + return socket; +} + +static void king_rtp_receive_result_to_array( + zval *return_value, + const king_rtp_receive_result_t *packet +) +{ + array_init(return_value); + add_assoc_long(return_value, "ssrc", (zend_long) packet->ssrc); + add_assoc_long(return_value, "payload_type", packet->payload_type); + add_assoc_long(return_value, "seq", packet->seq); + add_assoc_long(return_value, "timestamp", (zend_long) packet->timestamp); + add_assoc_bool(return_value, "marker", packet->marker); + add_assoc_stringl(return_value, "data", (char *) packet->data, packet->data_len); + add_assoc_string(return_value, "from_ip", packet->from_ip); + add_assoc_long(return_value, "from_port", packet->from_port); + add_assoc_bool(return_value, "ice_consented", packet->ice_consented); + add_assoc_bool(return_value, "srtp", packet->srtp); +} + +PHP_METHOD(King_RTP_Socket, __construct) +{ + char *host = NULL; + size_t host_len = 0; + zend_long port; + char errbuf[256] = {0}; + king_rtp_socket_t *socket; + king_rtp_socket_object *intern; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_STRING(host, host_len) + Z_PARAM_LONG(port) + ZEND_PARSE_PARAMETERS_END(); + (void) host_len; + + socket = king_rtp_socket_create(host, (int) port, errbuf, sizeof(errbuf)); + if (socket == NULL) { + king_set_error(errbuf); + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "King\\RTP\\Socket could not bind %s:" ZEND_LONG_FMT ": %s", + host, + port, + errbuf[0] != '\0' ? errbuf : "unknown RTP bind failure" + ); + RETURN_THROWS(); + } + + intern = php_king_rtp_socket_obj_from_zend(Z_OBJ_P(ZEND_THIS)); + if (!Z_ISUNDEF(intern->resource)) { + zval_ptr_dtor(&intern->resource); + } + ZVAL_RES(&intern->resource, zend_register_resource(socket, le_king_rtp_socket)); + intern->closed = false; + king_set_error(""); +} + +PHP_METHOD(King_RTP_Socket, iceCredentials) +{ + king_rtp_socket_t *socket; + + ZEND_PARSE_PARAMETERS_NONE(); + + socket = king_rtp_socket_object_fetch(ZEND_THIS, "King\\RTP\\Socket::iceCredentials"); + if (socket == NULL) { + RETURN_THROWS(); + } + + array_init(return_value); + add_assoc_string(return_value, "ufrag", socket->ufrag); + add_assoc_string(return_value, "pwd", socket->pwd); +} + +PHP_METHOD(King_RTP_Socket, dtlsFingerprint) +{ + king_rtp_socket_t *socket; + + ZEND_PARSE_PARAMETERS_NONE(); + + socket = king_rtp_socket_object_fetch(ZEND_THIS, "King\\RTP\\Socket::dtlsFingerprint"); + if (socket == NULL) { + RETURN_THROWS(); + } + + RETURN_STRING(socket->fingerprint); +} + +PHP_METHOD(King_RTP_Socket, acceptDtls) +{ + char *ip = NULL; + size_t ip_len = 0; + zend_long port; + zend_long timeout_ms = 5000; + char errbuf[256] = {0}; + king_rtp_socket_t *socket; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STRING(ip, ip_len) + Z_PARAM_LONG(port) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(timeout_ms) + ZEND_PARSE_PARAMETERS_END(); + (void) ip_len; + + socket = king_rtp_socket_object_fetch(ZEND_THIS, "King\\RTP\\Socket::acceptDtls"); + if (socket == NULL) { + RETURN_THROWS(); + } + + if (king_rtp_dtls_do_accept(socket, ip, (int) port, (int) timeout_ms, errbuf, sizeof(errbuf)) != 0) { + king_set_error(errbuf); + RETURN_FALSE; + } + + king_set_error(""); + RETURN_TRUE; +} + +PHP_METHOD(King_RTP_Socket, receive) +{ + zend_long timeout_ms = 0; + king_rtp_socket_t *socket; + king_rtp_receive_result_t packet; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(timeout_ms) + ZEND_PARSE_PARAMETERS_END(); + + socket = king_rtp_socket_object_fetch(ZEND_THIS, "King\\RTP\\Socket::receive"); + if (socket == NULL) { + RETURN_THROWS(); + } + + if (!king_rtp_socket_recv_packet(socket, (int) timeout_ms, &packet)) { + RETURN_FALSE; + } + + king_rtp_receive_result_to_array(return_value, &packet); +} + +PHP_METHOD(King_RTP_Socket, send) +{ + char *host = NULL; + char *data = NULL; + size_t host_len = 0; + size_t data_len = 0; + zend_long port; + king_rtp_socket_t *socket; + + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_STRING(host, host_len) + Z_PARAM_LONG(port) + Z_PARAM_STRING(data, data_len) + ZEND_PARSE_PARAMETERS_END(); + (void) host_len; + + socket = king_rtp_socket_object_fetch(ZEND_THIS, "King\\RTP\\Socket::send"); + if (socket == NULL) { + RETURN_THROWS(); + } + + RETURN_BOOL(king_rtp_socket_send_packet(socket, host, (int) port, data, data_len)); +} + +PHP_METHOD(King_RTP_Socket, close) +{ + king_rtp_socket_object *intern; + + ZEND_PARSE_PARAMETERS_NONE(); + + intern = php_king_rtp_socket_obj_from_zend(Z_OBJ_P(ZEND_THIS)); + if (!intern->closed && !Z_ISUNDEF(intern->resource) && Z_TYPE(intern->resource) == IS_RESOURCE) { + zend_list_close(Z_RES(intern->resource)); + intern->closed = true; + } +} + +PHP_METHOD(King_RTP_Socket, isClosed) +{ + king_rtp_socket_object *intern; + + ZEND_PARSE_PARAMETERS_NONE(); + + intern = php_king_rtp_socket_obj_from_zend(Z_OBJ_P(ZEND_THIS)); + RETURN_BOOL(intern->closed); +} + +#include "media/class_method_entries.h" diff --git a/extension/src/media/registration.inc b/extension/src/media/registration.inc new file mode 100644 index 000000000..73262f297 --- /dev/null +++ b/extension/src/media/registration.inc @@ -0,0 +1,19 @@ +/* + * Media registration aggregation. The media translation unit includes this + * module boundary instead of leaving registration in the core bootstrap. + */ + +void king_media_register_classes(void) +{ + king_ce_rtp_socket = king_register_class_with_flags( + "King\\RTP\\Socket", + NULL, + king_rtp_socket_class_methods, + ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES + ); +} + +void king_media_init_object_handlers(void) +{ + king_rtp_socket_object_handlers_init(); +} diff --git a/extension/src/media/rtp.c b/extension/src/media/rtp.c index b4663f546..a61d04903 100644 --- a/extension/src/media/rtp.c +++ b/extension/src/media/rtp.c @@ -57,7 +57,13 @@ static int srtp_initialized = 0; #endif -#include "rtp.h" +#include "media/rtp.h" + +static king_rtp_peer_t *king_rtp_peer_by_addr_str( + king_rtp_socket_t *sock, + const char *ip, + int port +); /* ── Resource registration ──────────────────────────────────────────────── */ @@ -289,6 +295,156 @@ void king_rtp_socket_free(king_rtp_socket_t *sock) efree(sock); } +int king_rtp_socket_recv_packet( + king_rtp_socket_t *sock, + int timeout_ms, + king_rtp_receive_result_t *out +) +{ + uint8_t buf[KING_RTP_MTU + 16]; /* +16 for SRTP auth tag */ + struct sockaddr_storage from; + socklen_t flen = sizeof(from); + struct timeval deadline; + + if (sock == NULL || out == NULL) { + return 0; + } + + memset(out, 0, sizeof(*out)); + gettimeofday(&deadline, NULL); + deadline.tv_usec += (timeout_ms % 1000) * 1000; + deadline.tv_sec += timeout_ms / 1000 + deadline.tv_usec / 1000000; + deadline.tv_usec %= 1000000; + + for (;;) { + struct timeval now; + long rem; + fd_set fds; + struct timeval tv; + ssize_t n; + king_rtp_packet_t pkt; + king_rtp_peer_t *peer2; + + gettimeofday(&now, NULL); + rem = (deadline.tv_sec - now.tv_sec) * 1000000 + + (deadline.tv_usec - now.tv_usec); + if (rem <= 0) { + return 0; + } + + FD_ZERO(&fds); + FD_SET(sock->fd, &fds); + tv.tv_sec = rem / 1000000; + tv.tv_usec = rem % 1000000; + if (select(sock->fd + 1, &fds, NULL, NULL, &tv) <= 0) { + return 0; + } + + flen = sizeof(from); + n = recvfrom(sock->fd, buf, sizeof(buf), 0, (struct sockaddr *)&from, &flen); + if (n < 0) { + return 0; + } + + if (king_rtp_handle_stun(sock, buf, (size_t)n, (struct sockaddr *)&from, flen)) { + continue; + } + +#ifdef KING_HAVE_SRTP + king_rtp_peer_t *peer = king_rtp_peer_get(sock, (struct sockaddr *)&from, flen); + if (peer->srtp_ready) { + int pkt_len = (int)n; + srtp_err_status_t st = srtp_unprotect(peer->srtp_recv, buf, &pkt_len); + if (st != srtp_err_status_ok) { + continue; + } + n = (ssize_t)pkt_len; + } +#endif + + if (king_rtp_parse(buf, (size_t)n, &pkt) != 0) { + continue; + } + + peer2 = king_rtp_peer_get(sock, (struct sockaddr *)&from, flen); + peer2->ssrc = pkt.ssrc; + + out->ssrc = pkt.ssrc; + out->payload_type = pkt.payload_type; + out->seq = pkt.seq; + out->timestamp = pkt.timestamp; + out->marker = pkt.marker; + out->data_len = pkt.payload_len; + memcpy(out->data, pkt.payload, pkt.payload_len); + + if (from.ss_family == AF_INET) { + struct sockaddr_in *s4 = (struct sockaddr_in *)&from; + inet_ntop(AF_INET, &s4->sin_addr, out->from_ip, sizeof(out->from_ip)); + out->from_port = ntohs(s4->sin_port); + } else { + struct sockaddr_in6 *s6 = (struct sockaddr_in6 *)&from; + inet_ntop(AF_INET6, &s6->sin6_addr, out->from_ip, sizeof(out->from_ip)); + out->from_port = ntohs(s6->sin6_port); + } + + out->ice_consented = peer2->ice_consented; + out->srtp = peer2->dtls_done; + return 1; + } +} + +int king_rtp_socket_send_packet( + king_rtp_socket_t *sock, + const char *host, + int port, + const char *data, + size_t data_len +) +{ + king_rtp_peer_t *peer; + uint8_t buf[KING_RTP_MTU + 256]; + size_t out_len; + struct addrinfo hints = {0}, *res = NULL; + char ps[8]; + ssize_t sent; + + if (sock == NULL || host == NULL || data == NULL) { + return 0; + } + + peer = king_rtp_peer_by_addr_str(sock, host, port); + + if (data_len > KING_RTP_MTU) { + data_len = KING_RTP_MTU; + } + memcpy(buf, data, data_len); + out_len = data_len; + +#ifdef KING_HAVE_SRTP + if (peer && peer->srtp_ready) { + int l = (int)out_len; + srtp_err_status_t st = srtp_protect(peer->srtp_send, buf, &l); + if (st != srtp_err_status_ok) { + return 0; + } + out_len = (size_t)l; + } +#else + (void) peer; +#endif + + snprintf(ps, sizeof(ps), "%d", port); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + if (getaddrinfo(host, ps, &hints, &res) != 0 || !res) { + return 0; + } + + sent = sendto(sock->fd, buf, out_len, 0, res->ai_addr, res->ai_addrlen); + freeaddrinfo(res); + return sent == (ssize_t)out_len ? 1 : 0; +} + /* ── Peer management ────────────────────────────────────────────────────── */ king_rtp_peer_t *king_rtp_peer_get(king_rtp_socket_t *sock, @@ -592,211 +748,4 @@ int king_rtp_dtls_do_accept(king_rtp_socket_t *sock, return 0; } -/* ── PHP functions ──────────────────────────────────────────────────────── */ - -PHP_FUNCTION(king_rtp_bind) -{ - char *host; size_t hlen; zend_long port; - ZEND_PARSE_PARAMETERS_START(2, 2) - Z_PARAM_STRING(host, hlen) - Z_PARAM_LONG(port) - ZEND_PARSE_PARAMETERS_END(); - - char errbuf[256] = {0}; - king_rtp_socket_t *sock = king_rtp_socket_create(host, (int)port, - errbuf, sizeof(errbuf)); - if (!sock) { - php_error_docref(NULL, E_WARNING, "king_rtp_bind: %s", errbuf); - RETURN_FALSE; - } - RETURN_RES(zend_register_resource(sock, le_king_rtp_socket)); -} - -PHP_FUNCTION(king_rtp_ice_credentials) -{ - zval *z; ZEND_PARSE_PARAMETERS_START(1,1) Z_PARAM_RESOURCE(z) ZEND_PARSE_PARAMETERS_END(); - king_rtp_socket_t *s = (king_rtp_socket_t *)zend_fetch_resource(Z_RES_P(z), - "King\\RtpSocket", le_king_rtp_socket); - if (!s) RETURN_FALSE; - array_init(return_value); - add_assoc_string(return_value, "ufrag", s->ufrag); - add_assoc_string(return_value, "pwd", s->pwd); -} - -/* - * king_rtp_dtls_fingerprint(resource $socket): string - * Returns "SHA-256 XX:XX:…" for the SDP a=fingerprint attribute. - */ -PHP_FUNCTION(king_rtp_dtls_fingerprint) -{ - zval *z; ZEND_PARSE_PARAMETERS_START(1,1) Z_PARAM_RESOURCE(z) ZEND_PARSE_PARAMETERS_END(); - king_rtp_socket_t *s = (king_rtp_socket_t *)zend_fetch_resource(Z_RES_P(z), - "King\\RtpSocket", le_king_rtp_socket); - if (!s) RETURN_FALSE; - RETURN_STRING(s->fingerprint); -} - -/* - * king_rtp_dtls_accept(resource $socket, string $peer_ip, int $peer_port, - * int $timeout_ms = 5000): bool - * - * Performs the DTLS handshake with the named peer, extracts SRTP keying - * material and initialises libsrtp2 send/recv contexts. - * After this call king_rtp_recv() / king_rtp_send() transparently en/decrypt. - */ -PHP_FUNCTION(king_rtp_dtls_accept) -{ - zval *z; char *ip; size_t iplen; zend_long port; zend_long tms = 5000; - ZEND_PARSE_PARAMETERS_START(3, 4) - Z_PARAM_RESOURCE(z) - Z_PARAM_STRING(ip, iplen) - Z_PARAM_LONG(port) - Z_PARAM_OPTIONAL - Z_PARAM_LONG(tms) - ZEND_PARSE_PARAMETERS_END(); - - king_rtp_socket_t *s = (king_rtp_socket_t *)zend_fetch_resource(Z_RES_P(z), - "King\\RtpSocket", le_king_rtp_socket); - if (!s) RETURN_FALSE; - - char errbuf[256] = {0}; - int rc = king_rtp_dtls_do_accept(s, ip, (int)port, (int)tms, - errbuf, sizeof(errbuf)); - if (rc != 0) { - php_error_docref(NULL, E_WARNING, "king_rtp_dtls_accept: %s", errbuf); - RETURN_FALSE; - } - RETURN_TRUE; -} - -PHP_FUNCTION(king_rtp_recv) -{ - zval *z; zend_long tms = 0; - ZEND_PARSE_PARAMETERS_START(1, 2) - Z_PARAM_RESOURCE(z) - Z_PARAM_OPTIONAL - Z_PARAM_LONG(tms) - ZEND_PARSE_PARAMETERS_END(); - - king_rtp_socket_t *sock = (king_rtp_socket_t *)zend_fetch_resource(Z_RES_P(z), - "King\\RtpSocket", le_king_rtp_socket); - if (!sock) RETURN_FALSE; - - uint8_t buf[KING_RTP_MTU + 16]; /* +16 for SRTP auth tag */ - struct sockaddr_storage from; socklen_t flen = sizeof(from); - - struct timeval deadline; gettimeofday(&deadline, NULL); - deadline.tv_usec += (tms % 1000) * 1000; - deadline.tv_sec += tms / 1000 + deadline.tv_usec / 1000000; - deadline.tv_usec %= 1000000; - - for (;;) { - struct timeval now; gettimeofday(&now, NULL); - long rem = (deadline.tv_sec - now.tv_sec) * 1000000 - + (deadline.tv_usec - now.tv_usec); - if (rem <= 0) RETURN_FALSE; - - fd_set fds; FD_ZERO(&fds); FD_SET(sock->fd, &fds); - struct timeval tv = { rem / 1000000, rem % 1000000 }; - if (select(sock->fd + 1, &fds, NULL, NULL, &tv) <= 0) RETURN_FALSE; - - flen = sizeof(from); - ssize_t n = recvfrom(sock->fd, buf, sizeof(buf), 0, - (struct sockaddr *)&from, &flen); - if (n < 0) RETURN_FALSE; - - if (king_rtp_handle_stun(sock, buf, (size_t)n, - (struct sockaddr *)&from, flen)) continue; - -#ifdef KING_HAVE_SRTP - king_rtp_peer_t *peer = king_rtp_peer_get(sock, - (struct sockaddr *)&from, flen); - if (peer->srtp_ready) { - int pkt_len = (int)n; - srtp_err_status_t st = srtp_unprotect(peer->srtp_recv, buf, &pkt_len); - if (st != srtp_err_status_ok) continue; /* drop bad packet */ - n = (ssize_t)pkt_len; - } -#endif - - king_rtp_packet_t pkt; - if (king_rtp_parse(buf, (size_t)n, &pkt) != 0) continue; - - king_rtp_peer_t *peer2 = king_rtp_peer_get(sock, - (struct sockaddr *)&from, flen); - peer2->ssrc = pkt.ssrc; - - char ip[INET6_ADDRSTRLEN] = {0}; int rport = 0; - if (from.ss_family == AF_INET) { - struct sockaddr_in *s4 = (struct sockaddr_in *)&from; - inet_ntop(AF_INET, &s4->sin_addr, ip, sizeof(ip)); - rport = ntohs(s4->sin_port); - } else { - struct sockaddr_in6 *s6 = (struct sockaddr_in6 *)&from; - inet_ntop(AF_INET6, &s6->sin6_addr, ip, sizeof(ip)); - rport = ntohs(s6->sin6_port); - } - - array_init(return_value); - add_assoc_long( return_value, "ssrc", (zend_long)pkt.ssrc); - add_assoc_long( return_value, "payload_type", pkt.payload_type); - add_assoc_long( return_value, "seq", pkt.seq); - add_assoc_long( return_value, "timestamp", (zend_long)pkt.timestamp); - add_assoc_bool( return_value, "marker", pkt.marker); - add_assoc_stringl(return_value, "data", - (char *)pkt.payload, pkt.payload_len); - add_assoc_string( return_value, "from_ip", ip); - add_assoc_long( return_value, "from_port", rport); - add_assoc_bool( return_value, "ice_consented", peer2->ice_consented); - add_assoc_bool( return_value, "srtp", peer2->dtls_done); - return; - } -} - -PHP_FUNCTION(king_rtp_send) -{ - zval *z; char *host, *data; size_t hlen, dlen; zend_long port; - ZEND_PARSE_PARAMETERS_START(4, 4) - Z_PARAM_RESOURCE(z) - Z_PARAM_STRING(host, hlen) - Z_PARAM_LONG(port) - Z_PARAM_STRING(data, dlen) - ZEND_PARSE_PARAMETERS_END(); - - king_rtp_socket_t *sock = (king_rtp_socket_t *)zend_fetch_resource(Z_RES_P(z), - "King\\RtpSocket", le_king_rtp_socket); - if (!sock) RETURN_FALSE; - - /* Find peer entry for optional SRTP */ - king_rtp_peer_t *peer = king_rtp_peer_by_addr_str(sock, host, (int)port); - - uint8_t buf[KING_RTP_MTU + 256]; - if (dlen > KING_RTP_MTU) dlen = KING_RTP_MTU; - memcpy(buf, data, dlen); - size_t out_len = dlen; - -#ifdef KING_HAVE_SRTP - if (peer && peer->srtp_ready) { - int l = (int)out_len; - srtp_err_status_t st = srtp_protect(peer->srtp_send, buf, &l); - if (st != srtp_err_status_ok) RETURN_FALSE; - out_len = (size_t)l; - } -#endif - - struct addrinfo hints = {0}, *res = NULL; - char ps[8]; snprintf(ps, sizeof(ps), ZEND_LONG_FMT, (zend_long) port); - hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_DGRAM; - if (getaddrinfo(host, ps, &hints, &res) != 0 || !res) RETURN_FALSE; - - ssize_t sent = sendto(sock->fd, buf, out_len, 0, - res->ai_addr, res->ai_addrlen); - freeaddrinfo(res); - RETURN_BOOL(sent == (ssize_t)out_len); -} - -PHP_FUNCTION(king_rtp_close) -{ - zval *z; ZEND_PARSE_PARAMETERS_START(1,1) Z_PARAM_RESOURCE(z) ZEND_PARSE_PARAMETERS_END(); - zend_list_close(Z_RES_P(z)); -} +#include "rtp/procedural_api.inc" diff --git a/extension/src/media/rtp/procedural_api.inc b/extension/src/media/rtp/procedural_api.inc new file mode 100644 index 000000000..5bbf9516e --- /dev/null +++ b/extension/src/media/rtp/procedural_api.inc @@ -0,0 +1,130 @@ +/* ── PHP functions ──────────────────────────────────────────────────────── */ + +PHP_FUNCTION(king_rtp_bind) +{ + char *host; size_t hlen; zend_long port; + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_STRING(host, hlen) + Z_PARAM_LONG(port) + ZEND_PARSE_PARAMETERS_END(); + + char errbuf[256] = {0}; + king_rtp_socket_t *sock = king_rtp_socket_create(host, (int)port, + errbuf, sizeof(errbuf)); + if (!sock) { + php_error_docref(NULL, E_WARNING, "king_rtp_bind: %s", errbuf); + RETURN_FALSE; + } + RETURN_RES(zend_register_resource(sock, le_king_rtp_socket)); +} + +PHP_FUNCTION(king_rtp_ice_credentials) +{ + zval *z; ZEND_PARSE_PARAMETERS_START(1,1) Z_PARAM_RESOURCE(z) ZEND_PARSE_PARAMETERS_END(); + king_rtp_socket_t *s = (king_rtp_socket_t *)zend_fetch_resource(Z_RES_P(z), + "King\\RtpSocket", le_king_rtp_socket); + if (!s) RETURN_FALSE; + array_init(return_value); + add_assoc_string(return_value, "ufrag", s->ufrag); + add_assoc_string(return_value, "pwd", s->pwd); +} + +/* + * king_rtp_dtls_fingerprint(resource $socket): string + * Returns "SHA-256 XX:XX:…" for the SDP a=fingerprint attribute. + */ +PHP_FUNCTION(king_rtp_dtls_fingerprint) +{ + zval *z; ZEND_PARSE_PARAMETERS_START(1,1) Z_PARAM_RESOURCE(z) ZEND_PARSE_PARAMETERS_END(); + king_rtp_socket_t *s = (king_rtp_socket_t *)zend_fetch_resource(Z_RES_P(z), + "King\\RtpSocket", le_king_rtp_socket); + if (!s) RETURN_FALSE; + RETURN_STRING(s->fingerprint); +} + +/* + * king_rtp_dtls_accept(resource $socket, string $peer_ip, int $peer_port, + * int $timeout_ms = 5000): bool + * + * Performs the DTLS handshake with the named peer, extracts SRTP keying + * material and initialises libsrtp2 send/recv contexts. + * After this call king_rtp_recv() / king_rtp_send() transparently en/decrypt. + */ +PHP_FUNCTION(king_rtp_dtls_accept) +{ + zval *z; char *ip; size_t iplen; zend_long port; zend_long tms = 5000; + ZEND_PARSE_PARAMETERS_START(3, 4) + Z_PARAM_RESOURCE(z) + Z_PARAM_STRING(ip, iplen) + Z_PARAM_LONG(port) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(tms) + ZEND_PARSE_PARAMETERS_END(); + + king_rtp_socket_t *s = (king_rtp_socket_t *)zend_fetch_resource(Z_RES_P(z), + "King\\RtpSocket", le_king_rtp_socket); + if (!s) RETURN_FALSE; + + char errbuf[256] = {0}; + int rc = king_rtp_dtls_do_accept(s, ip, (int)port, (int)tms, + errbuf, sizeof(errbuf)); + if (rc != 0) { + php_error_docref(NULL, E_WARNING, "king_rtp_dtls_accept: %s", errbuf); + RETURN_FALSE; + } + RETURN_TRUE; +} + +PHP_FUNCTION(king_rtp_recv) +{ + zval *z; zend_long tms = 0; + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_RESOURCE(z) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(tms) + ZEND_PARSE_PARAMETERS_END(); + + king_rtp_socket_t *sock = (king_rtp_socket_t *)zend_fetch_resource(Z_RES_P(z), + "King\\RtpSocket", le_king_rtp_socket); + if (!sock) RETURN_FALSE; + + king_rtp_receive_result_t packet; + if (!king_rtp_socket_recv_packet(sock, (int)tms, &packet)) { + RETURN_FALSE; + } + + array_init(return_value); + add_assoc_long( return_value, "ssrc", (zend_long)packet.ssrc); + add_assoc_long( return_value, "payload_type", packet.payload_type); + add_assoc_long( return_value, "seq", packet.seq); + add_assoc_long( return_value, "timestamp", (zend_long)packet.timestamp); + add_assoc_bool( return_value, "marker", packet.marker); + add_assoc_stringl(return_value, "data", (char *)packet.data, packet.data_len); + add_assoc_string( return_value, "from_ip", packet.from_ip); + add_assoc_long( return_value, "from_port", packet.from_port); + add_assoc_bool( return_value, "ice_consented", packet.ice_consented); + add_assoc_bool( return_value, "srtp", packet.srtp); +} + +PHP_FUNCTION(king_rtp_send) +{ + zval *z; char *host, *data; size_t hlen, dlen; zend_long port; + ZEND_PARSE_PARAMETERS_START(4, 4) + Z_PARAM_RESOURCE(z) + Z_PARAM_STRING(host, hlen) + Z_PARAM_LONG(port) + Z_PARAM_STRING(data, dlen) + ZEND_PARSE_PARAMETERS_END(); + + king_rtp_socket_t *sock = (king_rtp_socket_t *)zend_fetch_resource(Z_RES_P(z), + "King\\RtpSocket", le_king_rtp_socket); + if (!sock) RETURN_FALSE; + + RETURN_BOOL(king_rtp_socket_send_packet(sock, host, (int)port, data, dlen)); +} + +PHP_FUNCTION(king_rtp_close) +{ + zval *z; ZEND_PARSE_PARAMETERS_START(1,1) Z_PARAM_RESOURCE(z) ZEND_PARSE_PARAMETERS_END(); + zend_list_close(Z_RES_P(z)); +} diff --git a/extension/src/media/state.inc b/extension/src/media/state.inc new file mode 100644 index 000000000..e82d9ece7 --- /dev/null +++ b/extension/src/media/state.inc @@ -0,0 +1,6 @@ +/* + * Media module state storage aggregation. The media translation unit owns this + * state beside the RTP runtime. + */ + +#include "class_entries.inc" diff --git a/extension/src/object_store/class_entries.inc b/extension/src/object_store/class_entries.inc new file mode 100644 index 000000000..efe735e85 --- /dev/null +++ b/extension/src/object_store/class_entries.inc @@ -0,0 +1,5 @@ +/* + * Object store PHP class-entry storage. + */ + +zend_class_entry *king_ce_object_store = NULL; diff --git a/extension/src/object_store/internal/object_store_capacity_transfer/manifest_and_restore.inc b/extension/src/object_store/internal/object_store_capacity_transfer/manifest_and_restore.inc index 995bf5deb..72612e94d 100644 --- a/extension/src/object_store/internal/object_store_capacity_transfer/manifest_and_restore.inc +++ b/extension/src/object_store/internal/object_store_capacity_transfer/manifest_and_restore.inc @@ -745,75 +745,4 @@ static int king_object_store_restore_all_objects_from_manifest( return SUCCESS; } -static int king_object_store_commit_snapshot_directory( - const char *staging_directory, - const char *destination_directory -) -{ - char previous_directory[1024]; - zend_bool had_existing_destination = 0; - - if (staging_directory == NULL || staging_directory[0] == '\0' - || destination_directory == NULL || destination_directory[0] == '\0') { - return FAILURE; - } - - previous_directory[0] = '\0'; - - if (king_object_store_create_unique_directory( - destination_directory, - "previous", - previous_directory, - sizeof(previous_directory) - ) != SUCCESS) { - return FAILURE; - } - if (rmdir(previous_directory) != 0) { - king_object_store_remove_tree(previous_directory); - return FAILURE; - } - - if (rename(destination_directory, previous_directory) == 0) { - DIR *moved_directory = opendir(previous_directory); - - if (moved_directory == NULL) { - (void) rename(previous_directory, destination_directory); - return FAILURE; - } - - closedir(moved_directory); - had_existing_destination = 1; - } else if (errno != ENOENT) { - return FAILURE; - } - - if (rename(staging_directory, destination_directory) != 0) { - if (had_existing_destination) { - (void) rename(previous_directory, destination_directory); - } - return FAILURE; - } - - if (had_existing_destination) { - (void) king_object_store_remove_tree(previous_directory); - } - - return SUCCESS; -} - -static void king_object_store_fill_fallback_metadata( - king_object_metadata_t *metadata, - const char *object_id, - uint64_t content_length, - time_t fallback_timestamp) -{ - if (metadata == NULL || object_id == NULL) { - return; - } - - memset(metadata, 0, sizeof(*metadata)); - strncpy(metadata->object_id, object_id, sizeof(metadata->object_id) - 1); - metadata->content_length = content_length; - metadata->created_at = fallback_timestamp; - metadata->modified_at = fallback_timestamp; -} +#include "manifest_and_restore/commit_helpers.inc" diff --git a/extension/src/object_store/internal/object_store_capacity_transfer/manifest_and_restore/commit_helpers.inc b/extension/src/object_store/internal/object_store_capacity_transfer/manifest_and_restore/commit_helpers.inc new file mode 100644 index 000000000..65cdee6f8 --- /dev/null +++ b/extension/src/object_store/internal/object_store_capacity_transfer/manifest_and_restore/commit_helpers.inc @@ -0,0 +1,72 @@ +static int king_object_store_commit_snapshot_directory( + const char *staging_directory, + const char *destination_directory +) +{ + char previous_directory[1024]; + zend_bool had_existing_destination = 0; + + if (staging_directory == NULL || staging_directory[0] == '\0' + || destination_directory == NULL || destination_directory[0] == '\0') { + return FAILURE; + } + + previous_directory[0] = '\0'; + + if (king_object_store_create_unique_directory( + destination_directory, + "previous", + previous_directory, + sizeof(previous_directory) + ) != SUCCESS) { + return FAILURE; + } + if (rmdir(previous_directory) != 0) { + king_object_store_remove_tree(previous_directory); + return FAILURE; + } + + if (rename(destination_directory, previous_directory) == 0) { + DIR *moved_directory = opendir(previous_directory); + + if (moved_directory == NULL) { + (void) rename(previous_directory, destination_directory); + return FAILURE; + } + + closedir(moved_directory); + had_existing_destination = 1; + } else if (errno != ENOENT) { + return FAILURE; + } + + if (rename(staging_directory, destination_directory) != 0) { + if (had_existing_destination) { + (void) rename(previous_directory, destination_directory); + } + return FAILURE; + } + + if (had_existing_destination) { + (void) king_object_store_remove_tree(previous_directory); + } + + return SUCCESS; +} + +static void king_object_store_fill_fallback_metadata( + king_object_metadata_t *metadata, + const char *object_id, + uint64_t content_length, + time_t fallback_timestamp) +{ + if (metadata == NULL || object_id == NULL) { + return; + } + + memset(metadata, 0, sizeof(*metadata)); + strncpy(metadata->object_id, object_id, sizeof(metadata->object_id) - 1); + metadata->content_length = content_length; + metadata->created_at = fallback_timestamp; + metadata->modified_at = fallback_timestamp; +} diff --git a/extension/src/object_store/object_store.c b/extension/src/object_store/object_store.c index 2a01a80bc..ae1580da1 100644 --- a/extension/src/object_store/object_store.c +++ b/extension/src/object_store/object_store.c @@ -6,10 +6,12 @@ */ #include "php_king.h" +#include "php_king/core_arginfo.h" +#include "object_store/arginfo/index.h" #include "object_store/object_store_internal.h" -#include "include/runtime/libcurl_candidates.h" -#include "include/config/native_cdn/base_layer.h" -#include "include/config/native_object_store/base_layer.h" +#include "runtime/libcurl_candidates.h" +#include "config/native_cdn/base_layer.h" +#include "config/native_object_store/base_layer.h" #include "Zend/zend_smart_str.h" #include "main/php_streams.h" #include "ext/standard/base64.h" @@ -562,3 +564,5 @@ static int king_object_store_append_metadata_headers( #include "internal/object_store_metadata_headers_and_upload_api.inc" #include "internal/object_store_local_fallbacks.inc" #include "internal/object_store_dispatch.inc" +#include "state.inc" +#include "registration.inc" diff --git a/extension/src/object_store/registration.inc b/extension/src/object_store/registration.inc new file mode 100644 index 000000000..0a1ddbd2b --- /dev/null +++ b/extension/src/object_store/registration.inc @@ -0,0 +1,16 @@ +/* + * Object-store registration aggregation. The object-store runtime translation + * unit includes this module boundary beside the storage runtime. + */ + +#include "object_store/class_method_entries.h" + +void king_object_store_register_classes(void) +{ + king_ce_object_store = king_register_class_with_flags( + "King\\ObjectStore", + NULL, + king_object_store_class_methods, + ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES + ); +} diff --git a/extension/src/object_store/state.inc b/extension/src/object_store/state.inc new file mode 100644 index 000000000..e92e10d7f --- /dev/null +++ b/extension/src/object_store/state.inc @@ -0,0 +1,6 @@ +/* + * Object-store module state storage aggregation. The object-store runtime + * translation unit owns this class state beside the storage runtime. + */ + +#include "class_entries.inc" diff --git a/extension/src/php_king.c b/extension/src/php_king.c index 0d5ac4932..d175dd4e4 100644 --- a/extension/src/php_king.c +++ b/extension/src/php_king.c @@ -12,9 +12,8 @@ * - MINIT wires all config modules and registers their INI directives * - No legacy QUIC backend config is created during MINIT * - Exception classes register in the correct hierarchy - * - The first OO class entries now include active Config/Session wrappers - * over the same Runtime resource runtime; broader method parity and the - * remaining object-backed classes are still pending + * - OO class and object-handler registration is delegated through + * module-owned registration hooks * - Resource type handles bootstrap as -1 until MINIT registers them * - Core health/version and a small config-backed introspection surface are real * ========================================================================= @@ -31,20 +30,16 @@ #include "zend_object_handlers.h" #include "php_king.h" -#include "include/king_globals.h" -#include "include/king_init.h" #include "php_king/state.inc" -#include "php_king/externals.inc" -#include "php_king/arginfo.inc" -#include "php_king/db_ingestor.inc" -#include "php_king/function_table.inc" +#include "php_king/arginfo.h" +#include "php_king/init.h" +#include "php_king/registration.h" +#include "php_king/resources.h" #include "php_king/resources.inc" #include "php_king/exceptions.inc" #include "php_king/classes.inc" -#include "php_king/cancel_token.inc" -#include "php_king/mcp.inc" -#include "php_king/objects.inc" -#include "php_king/awaitable.inc" +#include "php_king/class_registration.inc" #include "php_king/lifecycle.inc" +#include "php_king/function_table.inc" #include "php_king/module_entry.inc" diff --git a/extension/src/php_king/arginfo.inc b/extension/src/php_king/arginfo.inc deleted file mode 100644 index fe18dc777..000000000 --- a/extension/src/php_king/arginfo.inc +++ /dev/null @@ -1,607 +0,0 @@ -/* - * Shared arginfo block for the King extension. Centralizes the procedural and - * OO signature metadata consumed by the module function table and class - * registrations. - */ - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_no_args, 0, 0, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_await, 0, 1, IS_MIXED, 0) - ZEND_ARG_OBJ_INFO(0, awaitable, King\\Awaitable, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_awaitable_poll, 0, 1, _IS_BOOL, 0) - ZEND_ARG_OBJ_INFO(0, awaitable, King\\Awaitable, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 0, "0") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_awaitable_cancel, 0, 1, _IS_BOOL, 0) - ZEND_ARG_OBJ_INFO(0, awaitable, King\\Awaitable, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_awaitable_status, 0, 1, IS_STRING, 0) - ZEND_ARG_OBJ_INFO(0, awaitable, King\\Awaitable, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_Awaitable___construct, 0, 0, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Awaitable_await, 0, 0, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Awaitable_poll, 0, 0, _IS_BOOL, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 0, "0") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Awaitable_cancel, 0, 0, _IS_BOOL, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Awaitable_isPending, 0, 0, _IS_BOOL, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Awaitable_isDone, 0, 0, _IS_BOOL, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Awaitable_isCancelled, 0, 0, _IS_BOOL, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Awaitable_getStatus, 0, 0, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_Awaitable_getOperation, 0, 0, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_one_string, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_one_long, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, value, IS_LONG, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_connect, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, host, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, port, IS_LONG, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, config, IS_MIXED, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_new_config, 0, 0, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_session_resource, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, session, IS_MIXED, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_poll, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, session, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 0, "0") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_cancel_stream, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, stream_id, IS_LONG, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, how, IS_STRING, 0, "\"both\"") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, session, IS_MIXED, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_set_ca_file, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, path, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_set_client_cert, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, cert, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_session_ticket_export, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, session, IS_MIXED, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_session_ticket_import, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, session, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO(0, ticket, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_optional_headers, 0, 0, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, headers, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_headers, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, headers, IS_ARRAY, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_config_array, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, config, IS_ARRAY, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_king_db_ingest, 0, 2, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) - ZEND_ARG_CALLABLE_INFO(0, writer, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_service_discovery, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, service_type, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, criteria, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_service_lookup, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, service_name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, client_info, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_semantic_dns_query, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, query, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, max_response_bytes, IS_LONG, 0, "256") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_register_service, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, service_info, IS_ARRAY, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_register_mother_node, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, mother_node_info, IS_ARRAY, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_update_service_status, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, service_id, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, status, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, metrics, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_object_lookup, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, object_id, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_object_store_put, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, object_id, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, data, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_object_store_put_from_stream, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, object_id, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, stream, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_object_store_get_to_stream, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, object_id, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, stream, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_object_store_begin_resumable_upload, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, object_id, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_object_store_append_resumable_upload_chunk, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, upload_id, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, stream, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_object_id, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, object_id, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_object_store_backup, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, object_id, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, destination_path, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_object_store_restore, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, object_id, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, source_path, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_object_store_all_objects_path, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, path, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_object_store_backup_all_objects, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, path, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_optional_object_id, 0, 0, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, object_id, IS_STRING, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_proto_encode, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, schema_name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, data, IS_MIXED, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_proto_encode_batch, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, schema_name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, records, IS_ARRAY, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_proto_decode, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, schema_name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, binary_data, IS_STRING, 0) - ZEND_ARG_TYPE_MASK(0, decode_as_object, MAY_BE_BOOL|MAY_BE_STRING|MAY_BE_ARRAY, "false") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_proto_decode_batch, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, schema_name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, binary_records, IS_ARRAY, 0) - ZEND_ARG_TYPE_MASK(0, decode_as_object, MAY_BE_BOOL|MAY_BE_STRING|MAY_BE_ARRAY, "false") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_proto_define_enum, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, enum_name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, enum_values, IS_ARRAY, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_proto_define_schema, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, schema_name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, schema_definition, IS_ARRAY, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_request_send, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, url, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, method, IS_STRING, 1, "\"GET\"") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, headers, IS_ARRAY, 1, "null") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, body, IS_MIXED, 1, "null") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_king_request_send_async, 0, 1, King\\Awaitable, 0) - ZEND_ARG_TYPE_INFO(0, url, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, method, IS_STRING, 1, "\"GET\"") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, headers, IS_ARRAY, 1, "null") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, body, IS_MIXED, 1, "null") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_http2_request_send_multi, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, requests, IS_ARRAY, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_king_http2_request_send_multi_async, 0, 1, King\\Awaitable, 0) - ZEND_ARG_TYPE_INFO(0, requests, IS_ARRAY, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_http3_request_send_multi, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, requests, IS_ARRAY, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_king_http3_request_send_multi_async, 0, 1, King\\Awaitable, 0) - ZEND_ARG_TYPE_INFO(0, requests, IS_ARRAY, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_receive_response, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, request_context, IS_MIXED, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_early_hints_process, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, request_context, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO(0, headers, IS_ARRAY, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_request_context_resource, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, request_context, IS_MIXED, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_websocket_connect, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, url, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, headers, IS_ARRAY, 1, "null") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_king_websocket_connect_async, 0, 1, King\\Awaitable, 0) - ZEND_ARG_TYPE_INFO(0, url, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, headers, IS_ARRAY, 1, "null") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_websocket_send, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, websocket, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO(0, data, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, is_binary, _IS_BOOL, 0, "false") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_king_websocket_send_async, 0, 2, King\\Awaitable, 0) - ZEND_ARG_TYPE_INFO(0, websocket, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO(0, data, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, is_binary, _IS_BOOL, 0, "false") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_websocket_receive, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, websocket, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 0, "-1") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_king_websocket_receive_async, 0, 1, King\\Awaitable, 0) - ZEND_ARG_TYPE_INFO(0, websocket, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 0, "-1") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_websocket_ping, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, websocket, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, payload, IS_STRING, 0, "\"\"") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_websocket_status, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, websocket, IS_MIXED, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_websocket_close, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, websocket, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, status_code, IS_LONG, 0, "1000") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, reason, IS_STRING, 0, "\"\"") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_server_listen, 0, 0, 4) - ZEND_ARG_TYPE_INFO(0, host, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, port, IS_LONG, 0) - ZEND_ARG_TYPE_INFO(0, config, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO(0, handler, IS_CALLABLE, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_server_on_cancel, 0, 0, 3) - ZEND_ARG_TYPE_INFO(0, session, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO(0, stream_id, IS_LONG, 0) - ZEND_ARG_TYPE_INFO(0, handler, IS_CALLABLE, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_server_send_early_hints, 0, 0, 3) - ZEND_ARG_TYPE_INFO(0, session, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO(0, stream_id, IS_LONG, 0) - ZEND_ARG_TYPE_INFO(0, hints, IS_ARRAY, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_server_upgrade_to_websocket, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, session, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO(0, stream_id, IS_LONG, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_rtp_bind, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, host, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, port, IS_LONG, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_rtp_ice_credentials, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, socket, IS_MIXED, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_rtp_dtls_fingerprint, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, socket, IS_MIXED, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_rtp_dtls_accept, 0, 0, 4) - ZEND_ARG_TYPE_INFO(0, socket, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO(0, ip, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, port, IS_LONG, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 0, "5000") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_rtp_recv, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, socket, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout_ms, IS_LONG, 0, "0") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_rtp_send, 0, 0, 4) - ZEND_ARG_TYPE_INFO(0, socket, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO(0, host, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, port, IS_LONG, 0) - ZEND_ARG_TYPE_INFO(0, data, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_rtp_close, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, socket, IS_MIXED, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_server_reload_tls_config, 0, 0, 3) - ZEND_ARG_TYPE_INFO(0, session, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO(0, cert_file_path, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, key_file_path, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_server_init_telemetry, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, session, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO(0, config, IS_MIXED, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_session_capability, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, session, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO(0, capability, IS_LONG, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_session_close_server_initiated, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, session, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO(0, capability, IS_LONG, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, error_code, IS_LONG, 0, "0") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, reason, IS_STRING, 0, "\"\"") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_admin_api_listen, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, target_server, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO(0, config, IS_MIXED, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_mcp_connect, 0, 0, 3) - ZEND_ARG_TYPE_INFO(0, host, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, port, IS_LONG, 0) - ZEND_ARG_TYPE_INFO(0, config, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_mcp_request, 0, 0, 4) - ZEND_ARG_TYPE_INFO(0, connection, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO(0, service_name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, method_name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, request_payload, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_king_mcp_request_async, 0, 4, King\\Awaitable, 0) - ZEND_ARG_TYPE_INFO(0, connection, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO(0, service_name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, method_name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, request_payload, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_mcp_close, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, connection, IS_MIXED, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_mcp_upload_from_stream, 0, 0, 5) - ZEND_ARG_TYPE_INFO(0, connection, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO(0, service_name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, method_name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, stream_identifier, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, stream, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_mcp_download_to_stream, 0, 0, 5) - ZEND_ARG_TYPE_INFO(0, connection, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO(0, service_name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, method_name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, request_payload, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, stream, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_pipeline_run, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, initial_data, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO(0, pipeline, IS_ARRAY, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, exec_options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_king_pipeline_run_async, 0, 2, King\\Awaitable, 0) - ZEND_ARG_TYPE_INFO(0, initial_data, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO(0, pipeline, IS_ARRAY, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, exec_options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_pipeline_register_tool, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, tool_name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, config, IS_ARRAY, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_pipeline_register_handler, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, tool_name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, handler, IS_CALLABLE, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_pipeline_get_run, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, run_id, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_telemetry_start_span, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, operation_name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, attributes, IS_ARRAY, 1, "null") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, parent_span_id, IS_STRING, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_telemetry_end_span, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, span_id, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, final_attributes, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_telemetry_record_metric, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, metric_name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, value, IS_DOUBLE, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, labels, IS_ARRAY, 1, "null") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, metric_type, IS_STRING, 0, "\"counter\"") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_telemetry_log, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, level, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, message, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, attributes, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_optional_instances, 0, 0, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, instances, IS_LONG, 0, "1") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_autoscaling_register_node, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, server_id, IS_LONG, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, name, IS_STRING, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_system_process_request, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, request_data, IS_ARRAY, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_xslt_transform_file, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, source_path, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, stylesheet_path, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_king_xslt_transform_to_file, 0, 0, 3) - ZEND_ARG_TYPE_INFO(0, source_path, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, stylesheet_path, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, output_path, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - -const zend_function_entry king_object_store_class_methods[] = { - ZEND_ME_MAPPING(init, king_object_store_init, arginfo_king_config_array, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(put, king_object_store_put, arginfo_king_object_store_put, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(putFromStream, king_object_store_put_from_stream, arginfo_king_object_store_put_from_stream, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(beginResumableUpload, king_object_store_begin_resumable_upload, arginfo_king_object_store_begin_resumable_upload, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(appendResumableUploadChunk, king_object_store_append_resumable_upload_chunk, arginfo_king_object_store_append_resumable_upload_chunk, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(completeResumableUpload, king_object_store_complete_resumable_upload, arginfo_king_object_id, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(abortResumableUpload, king_object_store_abort_resumable_upload, arginfo_king_object_id, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(getResumableUploadStatus, king_object_store_get_resumable_upload_status, arginfo_king_object_id, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(get, king_object_store_get, arginfo_king_object_lookup, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(getToStream, king_object_store_get_to_stream, arginfo_king_object_store_get_to_stream, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(delete, king_object_store_delete, arginfo_king_object_id, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(backupObject, king_object_store_backup_object, arginfo_king_object_store_backup, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(restoreObject, king_object_store_restore_object, arginfo_king_object_store_restore, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(backupAllObjects, king_object_store_backup_all_objects, arginfo_king_object_store_backup_all_objects, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(restoreAllObjects, king_object_store_restore_all_objects, arginfo_king_object_store_all_objects_path, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(listObjects, king_object_store_list, arginfo_king_no_args, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(getStats, king_object_store_get_stats, arginfo_king_no_args, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(optimize, king_object_store_optimize, arginfo_king_no_args, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(cleanupExpiredObjects, king_object_store_cleanup_expired_objects, arginfo_king_no_args, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(getMetadata, king_object_store_get_metadata, arginfo_king_object_id, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - PHP_FE_END -}; - -const zend_function_entry king_autoscaling_class_methods[] = { - ZEND_ME_MAPPING(init, king_autoscaling_init, arginfo_king_config_array, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(startMonitoring, king_autoscaling_start_monitoring, arginfo_king_no_args, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(stopMonitoring, king_autoscaling_stop_monitoring, arginfo_king_no_args, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(getMetrics, king_autoscaling_get_metrics, arginfo_king_no_args, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(getStatus, king_autoscaling_get_status, arginfo_king_no_args, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(getNodes, king_autoscaling_get_nodes, arginfo_king_no_args, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(scaleUp, king_autoscaling_scale_up, arginfo_king_optional_instances, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(scaleDown, king_autoscaling_scale_down, arginfo_king_optional_instances, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(registerNode, king_autoscaling_register_node, arginfo_king_autoscaling_register_node, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(markNodeReady, king_autoscaling_mark_node_ready, arginfo_king_one_long, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(drainNode, king_autoscaling_drain_node, arginfo_king_one_long, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - PHP_FE_END -}; - -const zend_function_entry king_pipeline_orchestrator_class_methods[] = { - ZEND_ME_MAPPING(run, king_pipeline_orchestrator_run, arginfo_king_pipeline_run, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(runAsync, king_pipeline_orchestrator_run_async, arginfo_king_pipeline_run_async, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(dispatch, king_pipeline_orchestrator_dispatch, arginfo_king_pipeline_run, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(dispatchAsync, king_pipeline_orchestrator_dispatch_async, arginfo_king_pipeline_run_async, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(registerTool, king_pipeline_orchestrator_register_tool, arginfo_king_pipeline_register_tool, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(registerHandler, king_pipeline_orchestrator_register_handler, arginfo_king_pipeline_register_handler, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(configureLogging, king_pipeline_orchestrator_configure_logging, arginfo_king_config_array, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(workerRunNext, king_pipeline_orchestrator_worker_run_next, arginfo_king_no_args, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(resumeRun, king_pipeline_orchestrator_resume_run, arginfo_king_pipeline_get_run, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(getRun, king_pipeline_orchestrator_get_run, arginfo_king_pipeline_get_run, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_ME_MAPPING(cancelRun, king_pipeline_orchestrator_cancel_run, arginfo_king_pipeline_get_run, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - PHP_FE_END -}; diff --git a/extension/src/php_king/class_registration.inc b/extension/src/php_king/class_registration.inc new file mode 100644 index 000000000..9e74391e5 --- /dev/null +++ b/extension/src/php_king/class_registration.inc @@ -0,0 +1,94 @@ +/* + * Class and object-handler registration for the King extension. The ordering + * mirrors the historical MINIT sequence; this file only keeps that sequence + * out of the lifecycle hook body. + */ + +static void king_register_exception_classes(void) +{ + king_ce_exception = king_register_exception( + "King\\Exception", zend_ce_exception); + + king_ce_stream_exception = king_register_exception( + "King\\StreamException", king_ce_exception); + king_ce_invalid_state = king_register_exception( + "King\\InvalidStateException", king_ce_stream_exception); + king_ce_unknown_stream = king_register_exception( + "King\\UnknownStreamException", king_ce_stream_exception); + king_ce_stream_blocked = king_register_exception( + "King\\StreamBlockedException", king_ce_stream_exception); + king_ce_stream_limit = king_register_exception( + "King\\StreamLimitException", king_ce_stream_exception); + king_ce_final_size = king_register_exception( + "King\\FinalSizeException", king_ce_stream_exception); + king_ce_stream_stopped = king_register_exception( + "King\\StreamStoppedException", king_ce_stream_exception); + king_ce_fin_expected = king_register_exception( + "King\\FinExpectedException", king_ce_stream_exception); + king_ce_invalid_fin_state = king_register_exception( + "King\\InvalidFinStateException", king_ce_stream_exception); + king_ce_done = king_register_exception( + "King\\DoneException", king_ce_stream_exception); + king_ce_too_many_streams = king_register_exception( + "King\\TooManyStreamsException", king_ce_stream_exception); + + king_ce_quic_exception = king_register_exception( + "King\\QuicException", king_ce_exception); + king_ce_congestion_control = king_register_exception( + "King\\CongestionControlException", king_ce_quic_exception); + + king_mcp_register_exception_classes(); + king_client_register_websocket_exception_classes(); + + king_ce_runtime_exception = king_register_exception( + "King\\RuntimeException", king_ce_exception); + king_ce_system_exception = king_register_exception( + "King\\SystemException", king_ce_exception); + king_ce_validation_exception = king_register_exception( + "King\\ValidationException", king_ce_exception); + king_ce_timeout_exception = king_register_exception( + "King\\TimeoutException", king_ce_exception); + king_ce_network_exception = king_register_exception( + "King\\NetworkException", king_ce_exception); + king_ce_tls_exception = king_register_exception( + "King\\TlsException", king_ce_exception); + king_ce_protocol_exception = king_register_exception( + "King\\ProtocolException", king_ce_exception); +} + +static void king_register_userland_classes(void) +{ + king_awaitable_register_classes(); + king_config_register_classes(); + king_client_register_session_classes(); + king_mcp_register_classes(); + king_pipeline_orchestrator_register_classes(); + king_object_store_register_classes(); + king_autoscaling_register_classes(); + king_media_register_classes(); + king_xslt_register_classes(); + king_inference_register_classes(); + king_client_register_http_classes(); + king_server_register_websocket_classes(); + king_client_register_websocket_classes(); +} + +static void king_init_object_handlers(void) +{ + king_awaitable_init_object_handlers(); + king_config_init_object_handlers(); + king_mcp_init_object_handlers(); + king_media_init_object_handlers(); + king_xslt_init_object_handlers(); + king_inference_init_object_handlers(); + king_client_init_object_handlers(); + king_server_init_object_handlers(); + king_client_init_session_object_handlers(); +} + +void king_register_php_classes_and_handlers(void) +{ + king_register_exception_classes(); + king_register_userland_classes(); + king_init_object_handlers(); +} diff --git a/extension/src/php_king/classes.inc b/extension/src/php_king/classes.inc index 1de4b5d1e..c82c10de1 100644 --- a/extension/src/php_king/classes.inc +++ b/extension/src/php_king/classes.inc @@ -3,7 +3,7 @@ * flag-aware wrappers used while registering core classes and facades. */ -static zend_class_entry *king_register_class_with_flags( +zend_class_entry *king_register_class_with_flags( const char *name, zend_class_entry *parent, const zend_function_entry *functions, diff --git a/extension/src/php_king/exceptions.inc b/extension/src/php_king/exceptions.inc index 3d44d5f0c..a05b9ceeb 100644 --- a/extension/src/php_king/exceptions.inc +++ b/extension/src/php_king/exceptions.inc @@ -3,7 +3,7 @@ * small wrappers that build the extension's exception hierarchy. */ -static zend_class_entry *king_register_exception( +zend_class_entry *king_register_exception( const char *name, zend_class_entry *parent) { diff --git a/extension/src/php_king/externals.inc b/extension/src/php_king/externals.inc deleted file mode 100644 index 5ddc154a7..000000000 --- a/extension/src/php_king/externals.inc +++ /dev/null @@ -1,205 +0,0 @@ -/* - * External lifecycle declarations shared across php_king bootstrap slices. - * Lists the module-init and shutdown hooks imported from neighboring runtime - * components. - */ - -int king_proto_registry_minit(void); -void king_proto_registry_mshutdown(void); -int king_iibin_minit(void); -int king_semantic_dns_registry_minit(void); -void king_semantic_dns_registry_mshutdown(void); -void king_semantic_dns_shutdown_system(void); -void king_semantic_dns_request_shutdown(void); - -int king_cdn_cache_registry_minit(void); -void king_cdn_cache_registry_mshutdown(void); -void king_object_store_request_shutdown(void); -void king_object_store_shutdown_system(void); -void king_xslt_shutdown_system(void); - -PHP_FUNCTION(king_version); -PHP_FUNCTION(king_health); -PHP_FUNCTION(king_await); -PHP_FUNCTION(king_awaitable_poll); -PHP_FUNCTION(king_awaitable_cancel); -PHP_FUNCTION(king_awaitable_status); -PHP_FUNCTION(king_get_last_error); -PHP_FUNCTION(king_db_ingest); -PHP_FUNCTION(king_client_websocket_get_last_error); -PHP_FUNCTION(king_mcp_get_error); -PHP_FUNCTION(king_telemetry_get_metrics); -PHP_FUNCTION(king_telemetry_get_trace_context); -PHP_FUNCTION(king_telemetry_inject_context); -PHP_FUNCTION(king_telemetry_extract_context); -PHP_FUNCTION(king_telemetry_flush); -PHP_FUNCTION(king_object_store_get_stats); -PHP_FUNCTION(king_object_store_put); -PHP_FUNCTION(king_object_store_put_from_stream); -PHP_FUNCTION(king_object_store_begin_resumable_upload); -PHP_FUNCTION(king_object_store_append_resumable_upload_chunk); -PHP_FUNCTION(king_object_store_complete_resumable_upload); -PHP_FUNCTION(king_object_store_abort_resumable_upload); -PHP_FUNCTION(king_object_store_get_resumable_upload_status); -PHP_FUNCTION(king_object_store_get); -PHP_FUNCTION(king_object_store_get_to_stream); -PHP_FUNCTION(king_object_store_delete); -PHP_FUNCTION(king_object_store_list); -PHP_FUNCTION(king_object_store_backup_object); -PHP_FUNCTION(king_object_store_restore_object); -PHP_FUNCTION(king_object_store_backup_all_objects); -PHP_FUNCTION(king_object_store_restore_all_objects); -PHP_FUNCTION(king_cdn_get_edge_nodes); -PHP_FUNCTION(king_telemetry_get_status); -PHP_FUNCTION(king_autoscaling_get_status); -PHP_FUNCTION(king_autoscaling_get_metrics); -PHP_FUNCTION(king_autoscaling_get_nodes); -PHP_FUNCTION(king_semantic_dns_get_service_topology); -PHP_FUNCTION(king_semantic_dns_discover_service); -PHP_FUNCTION(king_semantic_dns_get_optimal_route); -PHP_FUNCTION(king_proto_is_defined); -PHP_FUNCTION(king_proto_is_schema_defined); -PHP_FUNCTION(king_proto_is_enum_defined); -PHP_FUNCTION(king_proto_define_schema); -PHP_FUNCTION(king_proto_define_enum); -PHP_FUNCTION(king_proto_encode); -PHP_FUNCTION(king_proto_encode_batch); -PHP_FUNCTION(king_proto_decode); -PHP_FUNCTION(king_proto_decode_batch); -PHP_FUNCTION(king_proto_get_defined_schemas); -PHP_FUNCTION(king_proto_get_defined_enums); -PHP_FUNCTION(king_system_health_check); -PHP_FUNCTION(king_system_get_status); -PHP_FUNCTION(king_system_get_metrics); -PHP_FUNCTION(king_system_get_performance_report); -PHP_FUNCTION(king_system_get_component_info); - -PHP_FUNCTION(king_new_config); - -PHP_FUNCTION(king_connect); -PHP_FUNCTION(king_close); -PHP_FUNCTION(king_send_request); -PHP_FUNCTION(king_send_request_async); -PHP_FUNCTION(king_receive_response); -PHP_FUNCTION(king_poll); -PHP_FUNCTION(king_cancel_stream); -PHP_FUNCTION(king_export_session_ticket); -PHP_FUNCTION(king_import_session_ticket); -PHP_FUNCTION(king_set_ca_file); -PHP_FUNCTION(king_set_client_cert); -PHP_FUNCTION(king_get_stats); - -PHP_FUNCTION(king_client_send_request); -PHP_FUNCTION(king_client_send_request_async); -PHP_FUNCTION(king_client_stream_cancel); -PHP_FUNCTION(king_client_tls_set_ca_file); -PHP_FUNCTION(king_client_tls_set_client_cert); -PHP_FUNCTION(king_client_tls_export_session_ticket); -PHP_FUNCTION(king_client_tls_import_session_ticket); -PHP_FUNCTION(king_client_early_hints_get_pending); -PHP_FUNCTION(king_client_early_hints_process); - -PHP_FUNCTION(king_http1_request_send); -PHP_FUNCTION(king_http1_request_send_async); -PHP_FUNCTION(king_http2_request_send); -PHP_FUNCTION(king_http2_request_send_async); -PHP_FUNCTION(king_http2_request_send_multi); -PHP_FUNCTION(king_http2_request_send_multi_async); -PHP_FUNCTION(king_http3_request_send); -PHP_FUNCTION(king_http3_request_send_async); -PHP_FUNCTION(king_http3_request_send_multi); -PHP_FUNCTION(king_http3_request_send_multi_async); - -PHP_FUNCTION(king_client_websocket_connect); -PHP_FUNCTION(king_client_websocket_connect_async); -PHP_FUNCTION(king_client_websocket_send); -PHP_FUNCTION(king_client_websocket_send_async); -PHP_FUNCTION(king_client_websocket_receive); -PHP_FUNCTION(king_client_websocket_receive_async); -PHP_FUNCTION(king_client_websocket_ping); -PHP_FUNCTION(king_client_websocket_get_status); -PHP_FUNCTION(king_client_websocket_close); -PHP_FUNCTION(king_websocket_send); - -PHP_FUNCTION(king_http1_server_listen); -PHP_FUNCTION(king_http1_server_listen_once); -PHP_FUNCTION(king_http2_server_listen); -PHP_FUNCTION(king_http2_server_listen_once); -PHP_FUNCTION(king_http3_server_listen); -PHP_FUNCTION(king_http3_server_listen_once); -PHP_FUNCTION(king_server_listen); -PHP_FUNCTION(king_server_on_cancel); -PHP_FUNCTION(king_server_send_early_hints); -PHP_FUNCTION(king_server_upgrade_to_websocket); -PHP_FUNCTION(king_rtp_bind); -PHP_FUNCTION(king_rtp_ice_credentials); -PHP_FUNCTION(king_rtp_dtls_fingerprint); -PHP_FUNCTION(king_rtp_dtls_accept); -PHP_FUNCTION(king_rtp_recv); -PHP_FUNCTION(king_rtp_send); -PHP_FUNCTION(king_rtp_close); -PHP_FUNCTION(king_server_reload_tls_config); -PHP_FUNCTION(king_server_init_telemetry); -PHP_FUNCTION(king_session_get_peer_cert_subject); -PHP_FUNCTION(king_session_close_server_initiated); -PHP_FUNCTION(king_admin_api_listen); - -PHP_FUNCTION(king_mcp_connect); -PHP_FUNCTION(king_mcp_request); -PHP_FUNCTION(king_mcp_request_async); -PHP_FUNCTION(king_mcp_upload_from_stream); -PHP_FUNCTION(king_mcp_download_to_stream); -PHP_FUNCTION(king_mcp_close); - -PHP_FUNCTION(king_pipeline_orchestrator_run); -PHP_FUNCTION(king_pipeline_orchestrator_run_async); -PHP_FUNCTION(king_pipeline_orchestrator_dispatch); -PHP_FUNCTION(king_pipeline_orchestrator_dispatch_async); -PHP_FUNCTION(king_pipeline_orchestrator_register_tool); -PHP_FUNCTION(king_pipeline_orchestrator_register_handler); -PHP_FUNCTION(king_pipeline_orchestrator_configure_logging); -PHP_FUNCTION(king_pipeline_orchestrator_worker_run_next); -PHP_FUNCTION(king_pipeline_orchestrator_resume_run); -PHP_FUNCTION(king_pipeline_orchestrator_get_run); -PHP_FUNCTION(king_pipeline_orchestrator_cancel_run); - -PHP_FUNCTION(king_semantic_dns_init); -PHP_FUNCTION(king_semantic_dns_start_server); -PHP_FUNCTION(king_semantic_dns_query); -PHP_FUNCTION(king_semantic_dns_register_service); -PHP_FUNCTION(king_semantic_dns_register_mother_node); -PHP_FUNCTION(king_semantic_dns_update_service_status); - -PHP_FUNCTION(king_object_store_init); -PHP_FUNCTION(king_object_store_optimize); -PHP_FUNCTION(king_object_store_cleanup_expired_objects); -PHP_FUNCTION(king_object_store_get_metadata); -PHP_FUNCTION(king_cdn_cache_object); -PHP_FUNCTION(king_cdn_invalidate_cache); - -PHP_FUNCTION(king_xslt_engine_status); -PHP_FUNCTION(king_xslt_transform_file); -PHP_FUNCTION(king_xslt_transform_to_file); - -PHP_FUNCTION(king_telemetry_init); -PHP_FUNCTION(king_telemetry_start_span); -PHP_FUNCTION(king_telemetry_end_span); -PHP_FUNCTION(king_telemetry_record_metric); -PHP_FUNCTION(king_telemetry_log); - -PHP_FUNCTION(king_autoscaling_init); -PHP_FUNCTION(king_autoscaling_start_monitoring); -PHP_FUNCTION(king_autoscaling_stop_monitoring); -PHP_FUNCTION(king_autoscaling_get_nodes); -PHP_FUNCTION(king_autoscaling_scale_up); -PHP_FUNCTION(king_autoscaling_scale_down); -PHP_FUNCTION(king_autoscaling_register_node); -PHP_FUNCTION(king_autoscaling_mark_node_ready); -PHP_FUNCTION(king_autoscaling_drain_node); - -PHP_FUNCTION(king_system_init); -PHP_FUNCTION(king_system_process_request); -PHP_FUNCTION(king_system_restart_component); -PHP_FUNCTION(king_system_fail_component); -PHP_FUNCTION(king_system_recover); -PHP_FUNCTION(king_system_shutdown); diff --git a/extension/src/php_king/function_table.inc b/extension/src/php_king/function_table.inc index 1a34e1ea3..6ba30e5f1 100644 --- a/extension/src/php_king/function_table.inc +++ b/extension/src/php_king/function_table.inc @@ -1,193 +1,37 @@ /* * Root function table for the King extension. Maps the public procedural - * surface onto the shared arginfo blocks declared in this module. + * surface onto shared and module-local binding fragments. */ static const zend_function_entry king_functions[] = { PHP_FE(king_version, arginfo_king_no_args) PHP_FE(king_health, arginfo_king_no_args) - PHP_FE(king_new_config, arginfo_king_new_config) - PHP_FE(king_await, arginfo_king_await) - PHP_FE(king_awaitable_poll, arginfo_king_awaitable_poll) - PHP_FE(king_awaitable_cancel, arginfo_king_awaitable_cancel) - PHP_FE(king_awaitable_status, arginfo_king_awaitable_status) - - PHP_FE(king_connect, arginfo_king_connect) - PHP_FE(king_close, arginfo_king_session_resource) - PHP_FE(king_send_request, arginfo_king_request_send) - PHP_FE(king_send_request_async, arginfo_king_request_send_async) - PHP_FE(king_receive_response, arginfo_king_receive_response) - PHP_FE(king_poll, arginfo_king_poll) - PHP_FE(king_cancel_stream, arginfo_king_cancel_stream) - PHP_FE(king_export_session_ticket, arginfo_king_session_ticket_export) - PHP_FE(king_import_session_ticket, arginfo_king_session_ticket_import) - PHP_FE(king_set_ca_file, arginfo_king_set_ca_file) - PHP_FE(king_set_client_cert, arginfo_king_set_client_cert) PHP_FE(king_get_last_error, arginfo_king_no_args) - PHP_FE(king_get_stats, arginfo_king_session_resource) - PHP_FE(king_db_ingest, arginfo_king_db_ingest) - - PHP_FE(king_client_send_request, arginfo_king_request_send) - PHP_FE(king_client_send_request_async, arginfo_king_request_send_async) - PHP_FE(king_client_stream_cancel, arginfo_king_cancel_stream) - PHP_FE(king_client_tls_set_ca_file, arginfo_king_set_ca_file) - PHP_FE(king_client_tls_set_client_cert, arginfo_king_set_client_cert) - PHP_FE(king_client_tls_export_session_ticket, arginfo_king_session_ticket_export) - PHP_FE(king_client_tls_import_session_ticket, arginfo_king_session_ticket_import) - PHP_FE(king_client_early_hints_get_pending, arginfo_king_request_context_resource) - PHP_FE(king_client_early_hints_process, arginfo_king_early_hints_process) - - PHP_FE(king_http1_request_send, arginfo_king_request_send) - PHP_FE(king_http1_request_send_async, arginfo_king_request_send_async) - PHP_FE(king_http2_request_send, arginfo_king_request_send) - PHP_FE(king_http2_request_send_async, arginfo_king_request_send_async) - PHP_FE(king_http2_request_send_multi, arginfo_king_http2_request_send_multi) - PHP_FE(king_http2_request_send_multi_async, arginfo_king_http2_request_send_multi_async) - PHP_FE(king_http3_request_send, arginfo_king_request_send) - PHP_FE(king_http3_request_send_async, arginfo_king_request_send_async) - PHP_FE(king_http3_request_send_multi, arginfo_king_http3_request_send_multi) - PHP_FE(king_http3_request_send_multi_async, arginfo_king_http3_request_send_multi_async) - - PHP_FE(king_client_websocket_connect, arginfo_king_websocket_connect) - PHP_FE(king_client_websocket_connect_async, arginfo_king_websocket_connect_async) - PHP_FE(king_client_websocket_send, arginfo_king_websocket_send) - PHP_FE(king_client_websocket_send_async, arginfo_king_websocket_send_async) - PHP_FE(king_client_websocket_receive, arginfo_king_websocket_receive) - PHP_FE(king_client_websocket_receive_async, arginfo_king_websocket_receive_async) - PHP_FE(king_client_websocket_ping, arginfo_king_websocket_ping) - PHP_FE(king_client_websocket_get_status, arginfo_king_websocket_status) - PHP_FE(king_client_websocket_get_last_error, arginfo_king_no_args) - PHP_FE(king_client_websocket_close, arginfo_king_websocket_close) - PHP_FE(king_websocket_send, arginfo_king_websocket_send) - - PHP_FE(king_http1_server_listen, arginfo_king_server_listen) - PHP_FE(king_http1_server_listen_once, arginfo_king_server_listen) - PHP_FE(king_http2_server_listen, arginfo_king_server_listen) - PHP_FE(king_http2_server_listen_once, arginfo_king_server_listen) - PHP_FE(king_http3_server_listen, arginfo_king_server_listen) - PHP_FE(king_http3_server_listen_once, arginfo_king_server_listen) - PHP_FE(king_server_listen, arginfo_king_server_listen) - PHP_FE(king_server_on_cancel, arginfo_king_server_on_cancel) - PHP_FE(king_server_send_early_hints, arginfo_king_server_send_early_hints) - PHP_FE(king_server_upgrade_to_websocket, arginfo_king_server_upgrade_to_websocket) - PHP_FE(king_rtp_bind, arginfo_king_rtp_bind) - PHP_FE(king_rtp_ice_credentials, arginfo_king_rtp_ice_credentials) - PHP_FE(king_rtp_dtls_fingerprint, arginfo_king_rtp_dtls_fingerprint) - PHP_FE(king_rtp_dtls_accept, arginfo_king_rtp_dtls_accept) - PHP_FE(king_rtp_recv, arginfo_king_rtp_recv) - PHP_FE(king_rtp_send, arginfo_king_rtp_send) - PHP_FE(king_rtp_close, arginfo_king_rtp_close) - PHP_FE(king_server_reload_tls_config, arginfo_king_server_reload_tls_config) - PHP_FE(king_server_init_telemetry, arginfo_king_server_init_telemetry) - PHP_FE(king_session_get_peer_cert_subject, arginfo_king_session_capability) - PHP_FE(king_session_close_server_initiated, arginfo_king_session_close_server_initiated) - PHP_FE(king_admin_api_listen, arginfo_king_admin_api_listen) + PHP_FE(king_new_config, arginfo_king_new_config) +#include "awaitable/function_entries.h" +#include "client/function_entries.h" +#include "db_ingest/function_entries.h" +#include "server/function_entries.h" +#include "media/function_entries.h" - PHP_FE(king_proto_define_schema, arginfo_king_proto_define_schema) - PHP_FE(king_proto_define_enum, arginfo_king_proto_define_enum) - PHP_FE(king_proto_encode, arginfo_king_proto_encode) - PHP_FE(king_proto_encode_batch, arginfo_king_proto_encode_batch) - PHP_FE(king_proto_decode, arginfo_king_proto_decode) - PHP_FE(king_proto_decode_batch, arginfo_king_proto_decode_batch) - PHP_FE(king_proto_is_schema_defined, arginfo_king_one_string) - PHP_FE(king_proto_is_enum_defined, arginfo_king_one_string) - PHP_FE(king_proto_is_defined, arginfo_king_one_string) - PHP_FE(king_proto_get_defined_schemas, arginfo_king_no_args) - PHP_FE(king_proto_get_defined_enums, arginfo_king_no_args) +#include "iibin/function_entries.h" - PHP_FE(king_mcp_connect, arginfo_king_mcp_connect) - PHP_FE(king_mcp_request, arginfo_king_mcp_request) - PHP_FE(king_mcp_request_async, arginfo_king_mcp_request_async) - PHP_FE(king_mcp_upload_from_stream, arginfo_king_mcp_upload_from_stream) - PHP_FE(king_mcp_download_to_stream, arginfo_king_mcp_download_to_stream) - PHP_FE(king_mcp_close, arginfo_king_mcp_close) - PHP_FE(king_mcp_get_error, arginfo_king_no_args) +#include "mcp/function_entries.h" - PHP_FE(king_pipeline_orchestrator_run, arginfo_king_pipeline_run) - PHP_FE(king_pipeline_orchestrator_run_async, arginfo_king_pipeline_run_async) - PHP_FE(king_pipeline_orchestrator_dispatch, arginfo_king_pipeline_run) - PHP_FE(king_pipeline_orchestrator_dispatch_async, arginfo_king_pipeline_run_async) - PHP_FE(king_pipeline_orchestrator_register_tool, arginfo_king_pipeline_register_tool) - PHP_FE(king_pipeline_orchestrator_register_handler, arginfo_king_pipeline_register_handler) - PHP_FE(king_pipeline_orchestrator_configure_logging, arginfo_king_config_array) - PHP_FE(king_pipeline_orchestrator_worker_run_next, arginfo_king_no_args) - PHP_FE(king_pipeline_orchestrator_resume_run, arginfo_king_pipeline_get_run) - PHP_FE(king_pipeline_orchestrator_get_run, arginfo_king_pipeline_get_run) - PHP_FE(king_pipeline_orchestrator_cancel_run, arginfo_king_pipeline_get_run) +#include "pipeline_orchestrator/function_entries.h" - PHP_FE(king_semantic_dns_init, arginfo_king_config_array) - PHP_FE(king_semantic_dns_start_server, arginfo_king_no_args) - PHP_FE(king_semantic_dns_query, arginfo_king_semantic_dns_query) - PHP_FE(king_semantic_dns_register_service, arginfo_king_register_service) - PHP_FE(king_semantic_dns_discover_service, arginfo_king_service_discovery) - PHP_FE(king_semantic_dns_register_mother_node, arginfo_king_register_mother_node) - PHP_FE(king_semantic_dns_get_optimal_route, arginfo_king_service_lookup) - PHP_FE(king_semantic_dns_update_service_status, arginfo_king_update_service_status) - PHP_FE(king_semantic_dns_get_service_topology, arginfo_king_no_args) +#include "semantic_dns/function_entries.h" - PHP_FE(king_object_store_init, arginfo_king_config_array) - PHP_FE(king_object_store_put, arginfo_king_object_store_put) - PHP_FE(king_object_store_put_from_stream, arginfo_king_object_store_put_from_stream) - PHP_FE(king_object_store_begin_resumable_upload, arginfo_king_object_store_begin_resumable_upload) - PHP_FE(king_object_store_append_resumable_upload_chunk, arginfo_king_object_store_append_resumable_upload_chunk) - PHP_FE(king_object_store_complete_resumable_upload, arginfo_king_object_id) - PHP_FE(king_object_store_abort_resumable_upload, arginfo_king_object_id) - PHP_FE(king_object_store_get_resumable_upload_status, arginfo_king_object_id) - PHP_FE(king_object_store_get, arginfo_king_object_lookup) - PHP_FE(king_object_store_get_to_stream, arginfo_king_object_store_get_to_stream) - PHP_FE(king_object_store_delete, arginfo_king_object_id) - PHP_FE(king_object_store_backup_object, arginfo_king_object_store_backup) - PHP_FE(king_object_store_restore_object, arginfo_king_object_store_restore) - PHP_FE(king_object_store_backup_all_objects, arginfo_king_object_store_backup_all_objects) - PHP_FE(king_object_store_restore_all_objects, arginfo_king_object_store_all_objects_path) - PHP_FE(king_object_store_list, arginfo_king_no_args) - PHP_FE(king_object_store_get_stats, arginfo_king_no_args) - PHP_FE(king_object_store_optimize, arginfo_king_no_args) - PHP_FE(king_object_store_cleanup_expired_objects, arginfo_king_no_args) - PHP_FE(king_object_store_get_metadata, arginfo_king_object_id) - PHP_FE(king_cdn_cache_object, arginfo_king_object_lookup) - PHP_FE(king_cdn_invalidate_cache, arginfo_king_optional_object_id) - PHP_FE(king_cdn_get_edge_nodes, arginfo_king_no_args) +#include "object_store/function_entries.h" - PHP_FE(king_xslt_engine_status, arginfo_king_no_args) - PHP_FE(king_xslt_transform_file, arginfo_king_xslt_transform_file) - PHP_FE(king_xslt_transform_to_file, arginfo_king_xslt_transform_to_file) +#include "xslt/function_entries.h" +#include "inference/functions/function_entries.h" - PHP_FE(king_telemetry_init, arginfo_king_config_array) - PHP_FE(king_telemetry_start_span, arginfo_king_telemetry_start_span) - PHP_FE(king_telemetry_end_span, arginfo_king_telemetry_end_span) - PHP_FE(king_telemetry_record_metric, arginfo_king_telemetry_record_metric) - PHP_FE(king_telemetry_log, arginfo_king_telemetry_log) - PHP_FE(king_telemetry_flush, arginfo_king_no_args) - PHP_FE(king_telemetry_get_metrics, arginfo_king_no_args) - PHP_FE(king_telemetry_get_status, arginfo_king_no_args) - PHP_FE(king_telemetry_get_trace_context, arginfo_king_no_args) - PHP_FE(king_telemetry_inject_context, arginfo_king_optional_headers) - PHP_FE(king_telemetry_extract_context, arginfo_king_headers) +#include "telemetry/function_entries.h" - PHP_FE(king_autoscaling_init, arginfo_king_config_array) - PHP_FE(king_autoscaling_start_monitoring, arginfo_king_no_args) - PHP_FE(king_autoscaling_stop_monitoring, arginfo_king_no_args) - PHP_FE(king_autoscaling_get_metrics, arginfo_king_no_args) - PHP_FE(king_autoscaling_get_status, arginfo_king_no_args) - PHP_FE(king_autoscaling_get_nodes, arginfo_king_no_args) - PHP_FE(king_autoscaling_scale_up, arginfo_king_optional_instances) - PHP_FE(king_autoscaling_scale_down, arginfo_king_optional_instances) - PHP_FE(king_autoscaling_register_node, arginfo_king_autoscaling_register_node) - PHP_FE(king_autoscaling_mark_node_ready, arginfo_king_one_long) - PHP_FE(king_autoscaling_drain_node, arginfo_king_one_long) +#include "autoscaling/function_entries.h" - PHP_FE(king_system_init, arginfo_king_config_array) - PHP_FE(king_system_health_check, arginfo_king_no_args) - PHP_FE(king_system_get_status, arginfo_king_no_args) - PHP_FE(king_system_get_metrics, arginfo_king_no_args) - PHP_FE(king_system_get_performance_report, arginfo_king_no_args) - PHP_FE(king_system_get_component_info, arginfo_king_one_string) - PHP_FE(king_system_process_request, arginfo_king_system_process_request) - PHP_FE(king_system_restart_component, arginfo_king_one_string) - PHP_FE(king_system_fail_component, arginfo_king_one_string) - PHP_FE(king_system_recover, arginfo_king_no_args) - PHP_FE(king_system_shutdown, arginfo_king_no_args) +#include "integration/function_entries.h" PHP_FE_END }; diff --git a/extension/src/php_king/lifecycle.inc b/extension/src/php_king/lifecycle.inc index 4e0f72bae..d73712f0f 100644 --- a/extension/src/php_king/lifecycle.inc +++ b/extension/src/php_king/lifecycle.inc @@ -35,191 +35,10 @@ PHP_MINIT_FUNCTION(king) return FAILURE; } - le_king_session = zend_register_list_destructors_ex( - king_session_dtor, NULL, "King\\Session", module_number); - le_king_cfg = zend_register_list_destructors_ex( - king_cfg_dtor, NULL, "King\\Config", module_number); - le_king_mcp = zend_register_list_destructors_ex( - king_mcp_dtor, NULL, "King\\MCP", module_number); - le_king_ws = zend_register_list_destructors_ex( - king_ws_dtor, NULL, "King\\WebSocket", module_number); - le_king_request_context = zend_register_list_destructors_ex( - king_http1_request_context_dtor, NULL, "King\\HttpRequestContext", module_number); + king_register_resource_types(module_number); + king_rtp_minit(module_number); - king_ce_exception = king_register_exception( - "King\\Exception", zend_ce_exception); - - king_ce_stream_exception = king_register_exception( - "King\\StreamException", king_ce_exception); - king_ce_invalid_state = king_register_exception( - "King\\InvalidStateException", king_ce_stream_exception); - king_ce_unknown_stream = king_register_exception( - "King\\UnknownStreamException", king_ce_stream_exception); - king_ce_stream_blocked = king_register_exception( - "King\\StreamBlockedException", king_ce_stream_exception); - king_ce_stream_limit = king_register_exception( - "King\\StreamLimitException", king_ce_stream_exception); - king_ce_final_size = king_register_exception( - "King\\FinalSizeException", king_ce_stream_exception); - king_ce_stream_stopped = king_register_exception( - "King\\StreamStoppedException", king_ce_stream_exception); - king_ce_fin_expected = king_register_exception( - "King\\FinExpectedException", king_ce_stream_exception); - king_ce_invalid_fin_state = king_register_exception( - "King\\InvalidFinStateException", king_ce_stream_exception); - king_ce_done = king_register_exception( - "King\\DoneException", king_ce_stream_exception); - king_ce_too_many_streams = king_register_exception( - "King\\TooManyStreamsException", king_ce_stream_exception); - - king_ce_quic_exception = king_register_exception( - "King\\QuicException", king_ce_exception); - king_ce_congestion_control = king_register_exception( - "King\\CongestionControlException", king_ce_quic_exception); - - king_ce_mcp_exception = king_register_exception( - "King\\MCPException", king_ce_exception); - king_ce_mcp_connection_error = king_register_exception( - "King\\MCPConnectionException", king_ce_mcp_exception); - king_ce_mcp_protocol_error = king_register_exception( - "King\\MCPProtocolException", king_ce_mcp_exception); - king_ce_mcp_timeout = king_register_exception( - "King\\MCPTimeoutException", king_ce_mcp_exception); - king_ce_mcp_data_error = king_register_exception( - "King\\MCPDataException", king_ce_mcp_exception); - - king_ce_ws_exception = king_register_exception( - "King\\WebSocketException", king_ce_exception); - king_ce_ws_connection_error = king_register_exception( - "King\\WebSocketConnectionException", king_ce_ws_exception); - king_ce_ws_protocol_error = king_register_exception( - "King\\WebSocketProtocolException", king_ce_ws_exception); - king_ce_ws_timeout = king_register_exception( - "King\\WebSocketTimeoutException", king_ce_ws_exception); - king_ce_ws_closed = king_register_exception( - "King\\WebSocketClosedException", king_ce_ws_exception); - - king_ce_runtime_exception = king_register_exception( - "King\\RuntimeException", king_ce_exception); - king_ce_system_exception = king_register_exception( - "King\\SystemException", king_ce_exception); - king_ce_validation_exception = king_register_exception( - "King\\ValidationException", king_ce_exception); - king_ce_timeout_exception = king_register_exception( - "King\\TimeoutException", king_ce_exception); - king_ce_network_exception = king_register_exception( - "King\\NetworkException", king_ce_exception); - king_ce_tls_exception = king_register_exception( - "King\\TlsException", king_ce_exception); - king_ce_protocol_exception = king_register_exception( - "King\\ProtocolException", king_ce_exception); - - king_ce_cancel_token = king_register_class_with_flags( - "King\\CancelToken", - NULL, - king_cancel_token_class_methods, - ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES - ); - king_ce_awaitable = king_register_class_with_flags( - "King\\Awaitable", - NULL, - king_awaitable_class_methods, - ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES - ); - king_ce_config = king_register_class_with_flags( - "King\\Config", - NULL, - king_config_class_methods, - ZEND_ACC_NO_DYNAMIC_PROPERTIES - ); - king_ce_session = king_register_class_with_flags( - "King\\Session", - NULL, - king_session_class_methods, - ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES - ); - king_ce_stream = king_register_class_with_flags( - "King\\Stream", - NULL, - king_stream_class_methods, - ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES - ); - king_ce_response = king_register_class_with_flags( - "King\\Response", - NULL, - king_response_class_methods, - ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES - ); - king_ce_mcp = king_register_class_with_flags( - "King\\MCP", - NULL, - king_mcp_class_methods, - ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES - ); - king_ce_pipeline_orchestrator = king_register_class_with_flags( - "King\\PipelineOrchestrator", - NULL, - king_pipeline_orchestrator_class_methods, - ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES - ); - king_ce_object_store = king_register_class_with_flags( - "King\\ObjectStore", - NULL, - king_object_store_class_methods, - ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES - ); - king_ce_autoscaling = king_register_class_with_flags( - "King\\Autoscaling", - NULL, - king_autoscaling_class_methods, - ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES - ); - king_ce_client_http = king_register_class_with_flags( - "King\\Client\\HttpClient", - NULL, - king_http_client_class_methods, - ZEND_ACC_NO_DYNAMIC_PROPERTIES - ); - king_ce_client_http1 = king_register_class_with_flags( - "King\\Client\\Http1Client", - king_ce_client_http, - NULL, - ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES - ); - king_ce_client_http2 = king_register_class_with_flags( - "King\\Client\\Http2Client", - king_ce_client_http, - NULL, - ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES - ); - king_ce_client_http3 = king_register_class_with_flags( - "King\\Client\\Http3Client", - king_ce_client_http, - NULL, - ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES - ); - king_ce_ws_server = king_register_class_with_flags( - "King\\WebSocket\\Server", - NULL, - king_ws_server_class_methods, - ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES - ); - king_ce_ws_connection = king_register_class_with_flags( - "King\\WebSocket\\Connection", - NULL, - king_ws_connection_class_methods, - ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES - ); - king_cancel_token_object_handlers_init(); - king_awaitable_object_handlers_init(); - king_config_object_handlers_init(); - king_mcp_object_handlers_init(); - king_stream_object_handlers_init(); - king_response_object_handlers_init(); - king_http_client_object_handlers_init(); - king_ws_object_handlers_init(); - king_ws_server_object_handlers_init(); - king_session_object_handlers_init(); + king_register_php_classes_and_handlers(); return SUCCESS; } diff --git a/extension/src/php_king/mcp.inc b/extension/src/php_king/mcp.inc deleted file mode 100644 index 37479af15..000000000 --- a/extension/src/php_king/mcp.inc +++ /dev/null @@ -1,17 +0,0 @@ -/* - * King\MCP userland surface. Carries the MCP class arginfo, object methods and - * helper glue that bridge the native MCP runtime into userland. - */ - -#include "main/php_streams.h" -#include "ext/standard/base64.h" -#include "include/mcp/mcp.h" -#include "include/config/mcp_and_orchestrator/base_layer.h" -#include "include/config/mcp_and_orchestrator/default.h" -#include "include/king_hrtime.h" -#include - -#include "mcp/arginfo_and_runtime_controls.inc" -#include "mcp/transfer_helpers.inc" -#include "mcp/procedural_api.inc" -#include "mcp/object_api.inc" diff --git a/extension/src/php_king/objects.inc b/extension/src/php_king/objects.inc deleted file mode 100644 index cf5af97cb..000000000 --- a/extension/src/php_king/objects.inc +++ /dev/null @@ -1,425 +0,0 @@ -/* - * Zend object handlers for php_king classes. Owns the create/free logic for - * the internal CancelToken and other object-backed surfaces. - */ - -#include - -static void king_cancel_token_object_free(zend_object *object) -{ - king_cancel_token_object *intern = php_king_cancel_token_obj_from_zend(object); - - intern->cancelled = false; - zend_object_std_dtor(&intern->std); -} - -static zend_object *king_cancel_token_object_create(zend_class_entry *ce) -{ - king_cancel_token_object *intern = zend_object_alloc(sizeof(*intern), ce); - - intern->cancelled = false; - zend_object_std_init(&intern->std, ce); - object_properties_init(&intern->std, ce); - intern->std.handlers = &king_cancel_token_object_handlers; - - return &intern->std; -} - -static void king_config_object_free(zend_object *object) -{ - king_config_object *intern = php_king_config_obj_from_zend(object); - - if (!Z_ISUNDEF(intern->resource)) { - zval_ptr_dtor(&intern->resource); - ZVAL_UNDEF(&intern->resource); - } - if (!Z_ISUNDEF(intern->overrides)) { - zval_ptr_dtor(&intern->overrides); - ZVAL_UNDEF(&intern->overrides); - } - - zend_object_std_dtor(&intern->std); -} - -static zend_object *king_config_object_create(zend_class_entry *ce) -{ - king_config_object *intern = zend_object_alloc(sizeof(*intern), ce); - - ZVAL_UNDEF(&intern->resource); - ZVAL_UNDEF(&intern->overrides); - zend_object_std_init(&intern->std, ce); - object_properties_init(&intern->std, ce); - intern->std.handlers = &king_config_object_handlers; - - return &intern->std; -} - -static void king_stream_object_free(zend_object *object) -{ - king_stream_object *intern = php_king_stream_obj_from_zend(object); - - if (!Z_ISUNDEF(intern->session)) { - zval_ptr_dtor(&intern->session); - ZVAL_UNDEF(&intern->session); - } - if (!Z_ISUNDEF(intern->cancel_token)) { - zval_ptr_dtor(&intern->cancel_token); - ZVAL_UNDEF(&intern->cancel_token); - } - if (!Z_ISUNDEF(intern->connection_config)) { - zval_ptr_dtor(&intern->connection_config); - ZVAL_UNDEF(&intern->connection_config); - } - if (!Z_ISUNDEF(intern->request_headers)) { - zval_ptr_dtor(&intern->request_headers); - ZVAL_UNDEF(&intern->request_headers); - } - if (intern->request_method != NULL) { - zend_string_release(intern->request_method); - intern->request_method = NULL; - } - if (intern->request_path != NULL) { - zend_string_release(intern->request_path); - intern->request_path = NULL; - } - if (intern->request_body != NULL) { - zend_string_release(intern->request_body); - intern->request_body = NULL; - } - - zend_object_std_dtor(&intern->std); -} - -static zend_object *king_stream_object_create(zend_class_entry *ce) -{ - king_stream_object *intern = zend_object_alloc(sizeof(*intern), ce); - - ZVAL_UNDEF(&intern->session); - ZVAL_UNDEF(&intern->cancel_token); - ZVAL_UNDEF(&intern->connection_config); - ZVAL_UNDEF(&intern->request_headers); - intern->request_method = NULL; - intern->request_path = NULL; - intern->request_body = zend_string_init("", 0, 0); - intern->stream_id = -1; - intern->buffered_bytes = 0; - intern->request_body_was_supplied = false; - intern->finished = false; - intern->closed = false; - intern->response_started = false; - zend_object_std_init(&intern->std, ce); - object_properties_init(&intern->std, ce); - intern->std.handlers = &king_stream_object_handlers; - - return &intern->std; -} - -static void king_mcp_object_free(zend_object *object) -{ - king_mcp_object *intern = php_king_mcp_obj_from_zend(object); - - if (!Z_ISUNDEF(intern->resource)) { - zval_ptr_dtor(&intern->resource); - ZVAL_UNDEF(&intern->resource); - } - - zend_object_std_dtor(&intern->std); -} - -static zend_object *king_mcp_object_create(zend_class_entry *ce) -{ - king_mcp_object *intern = zend_object_alloc(sizeof(*intern), ce); - - ZVAL_UNDEF(&intern->resource); - zend_object_std_init(&intern->std, ce); - object_properties_init(&intern->std, ce); - intern->std.handlers = &king_mcp_object_handlers; - - return &intern->std; -} - -static void king_ws_object_free(zend_object *object) -{ - king_ws_object *intern = php_king_ws_obj_from_zend(object); - - if (!Z_ISUNDEF(intern->resource)) { - zval_ptr_dtor(&intern->resource); - ZVAL_UNDEF(&intern->resource); - } - - zend_object_std_dtor(&intern->std); -} - -static zend_object *king_ws_object_create(zend_class_entry *ce) -{ - king_ws_object *intern = zend_object_alloc(sizeof(*intern), ce); - - ZVAL_UNDEF(&intern->resource); - zend_object_std_init(&intern->std, ce); - object_properties_init(&intern->std, ce); - intern->std.handlers = &king_ws_object_handlers; - - return &intern->std; -} - -static void king_ws_server_object_free(zend_object *object) -{ - king_ws_server_object *intern = php_king_ws_server_obj_from_zend(object); - - if (!Z_ISUNDEF(intern->config)) { - zval_ptr_dtor(&intern->config); - ZVAL_UNDEF(&intern->config); - } - if (intern->host != NULL) { - zend_string_release(intern->host); - intern->host = NULL; - } - if (intern->listener_fd >= 0) { - close(intern->listener_fd); - intern->listener_fd = -1; - } - if (intern->registry_initialized) { - zval *entry; - - ZEND_HASH_FOREACH_VAL(&intern->connections, entry) - { - king_ws_state *state = Z_TYPE_P(entry) == IS_PTR - ? (king_ws_state *) Z_PTR_P(entry) - : NULL; - - if (state != NULL && state->server_owner == intern) { - state->server_owner = NULL; - } - } - ZEND_HASH_FOREACH_END(); - - zend_hash_destroy(&intern->connections); - intern->registry_initialized = false; - } - - zend_object_std_dtor(&intern->std); -} - -static zend_object *king_ws_server_object_create(zend_class_entry *ce) -{ - king_ws_server_object *intern = zend_object_alloc(sizeof(*intern), ce); - - ZVAL_UNDEF(&intern->config); - intern->host = NULL; - intern->port = 0; - intern->listener_fd = -1; - zend_hash_init(&intern->connections, 8, NULL, NULL, 0); - intern->next_connection_sequence = 1; - intern->registry_initialized = true; - intern->closed = false; - zend_object_std_init(&intern->std, ce); - object_properties_init(&intern->std, ce); - intern->std.handlers = &king_ws_server_object_handlers; - - return &intern->std; -} - -static void king_response_object_free(zend_object *object) -{ - king_response_object *intern = php_king_response_obj_from_zend(object); - - if (!Z_ISUNDEF(intern->payload)) { - zval_ptr_dtor(&intern->payload); - ZVAL_UNDEF(&intern->payload); - } - if (!Z_ISUNDEF(intern->request_context)) { - zval_ptr_dtor(&intern->request_context); - ZVAL_UNDEF(&intern->request_context); - } - - zend_object_std_dtor(&intern->std); -} - -static zend_object *king_response_object_create(zend_class_entry *ce) -{ - king_response_object *intern = zend_object_alloc(sizeof(*intern), ce); - - ZVAL_UNDEF(&intern->payload); - ZVAL_UNDEF(&intern->request_context); - intern->read_offset = 0; - zend_object_std_init(&intern->std, ce); - object_properties_init(&intern->std, ce); - intern->std.handlers = &king_response_object_handlers; - - return &intern->std; -} - -static void king_http_client_object_free(zend_object *object) -{ - king_http_client_object *intern = php_king_http_client_obj_from_zend(object); - - if (!Z_ISUNDEF(intern->config)) { - zval_ptr_dtor(&intern->config); - ZVAL_UNDEF(&intern->config); - } - - zend_object_std_dtor(&intern->std); -} - -static zend_object *king_http_client_object_create(zend_class_entry *ce) -{ - king_http_client_object *intern = zend_object_alloc(sizeof(*intern), ce); - - ZVAL_UNDEF(&intern->config); - intern->preferred_protocol = KING_CLIENT_PROTOCOL_AUTO; - intern->closed = false; - zend_object_std_init(&intern->std, ce); - object_properties_init(&intern->std, ce); - intern->std.handlers = &king_http_client_object_handlers; - - return &intern->std; -} - -static void king_session_object_free(zend_object *object) -{ - king_session_object *intern = php_king_obj_from_zend(object); - - if (!Z_ISUNDEF(intern->resource)) { - zval_ptr_dtor(&intern->resource); - ZVAL_UNDEF(&intern->resource); - } - if (!Z_ISUNDEF(intern->config)) { - zval_ptr_dtor(&intern->config); - ZVAL_UNDEF(&intern->config); - } - - zend_object_std_dtor(&intern->std); -} - -static zend_object *king_session_object_create(zend_class_entry *ce) -{ - king_session_object *intern = zend_object_alloc(sizeof(*intern), ce); - - ZVAL_UNDEF(&intern->resource); - ZVAL_UNDEF(&intern->config); - zend_object_std_init(&intern->std, ce); - object_properties_init(&intern->std, ce); - intern->std.handlers = &king_session_object_handlers; - - return &intern->std; -} - -static void king_config_object_handlers_init(void) -{ - memcpy( - &king_config_object_handlers, - &std_object_handlers, - sizeof(zend_object_handlers) - ); - king_config_object_handlers.offset = XtOffsetOf(king_config_object, std); - king_config_object_handlers.free_obj = king_config_object_free; - king_config_object_handlers.clone_obj = NULL; - king_ce_config->create_object = king_config_object_create; -} - -static void king_cancel_token_object_handlers_init(void) -{ - memcpy( - &king_cancel_token_object_handlers, - &std_object_handlers, - sizeof(zend_object_handlers) - ); - king_cancel_token_object_handlers.offset = XtOffsetOf(king_cancel_token_object, std); - king_cancel_token_object_handlers.free_obj = king_cancel_token_object_free; - king_cancel_token_object_handlers.clone_obj = NULL; - king_ce_cancel_token->create_object = king_cancel_token_object_create; -} - -static void king_stream_object_handlers_init(void) -{ - memcpy( - &king_stream_object_handlers, - &std_object_handlers, - sizeof(zend_object_handlers) - ); - king_stream_object_handlers.offset = XtOffsetOf(king_stream_object, std); - king_stream_object_handlers.free_obj = king_stream_object_free; - king_stream_object_handlers.clone_obj = NULL; - king_ce_stream->create_object = king_stream_object_create; -} - -static void king_response_object_handlers_init(void) -{ - memcpy( - &king_response_object_handlers, - &std_object_handlers, - sizeof(zend_object_handlers) - ); - king_response_object_handlers.offset = XtOffsetOf(king_response_object, std); - king_response_object_handlers.free_obj = king_response_object_free; - king_response_object_handlers.clone_obj = NULL; - king_ce_response->create_object = king_response_object_create; -} - -static void king_mcp_object_handlers_init(void) -{ - memcpy( - &king_mcp_object_handlers, - &std_object_handlers, - sizeof(zend_object_handlers) - ); - king_mcp_object_handlers.offset = XtOffsetOf(king_mcp_object, std); - king_mcp_object_handlers.free_obj = king_mcp_object_free; - king_mcp_object_handlers.clone_obj = NULL; - king_ce_mcp->create_object = king_mcp_object_create; -} - -static void king_http_client_object_handlers_init(void) -{ - memcpy( - &king_http_client_object_handlers, - &std_object_handlers, - sizeof(zend_object_handlers) - ); - king_http_client_object_handlers.offset = XtOffsetOf(king_http_client_object, std); - king_http_client_object_handlers.free_obj = king_http_client_object_free; - king_http_client_object_handlers.clone_obj = NULL; - king_ce_client_http->create_object = king_http_client_object_create; - king_ce_client_http1->create_object = king_http_client_object_create; - king_ce_client_http2->create_object = king_http_client_object_create; - king_ce_client_http3->create_object = king_http_client_object_create; -} - -static void king_ws_object_handlers_init(void) -{ - memcpy( - &king_ws_object_handlers, - &std_object_handlers, - sizeof(zend_object_handlers) - ); - king_ws_object_handlers.offset = XtOffsetOf(king_ws_object, std); - king_ws_object_handlers.free_obj = king_ws_object_free; - king_ws_object_handlers.clone_obj = NULL; - king_ce_ws_connection->create_object = king_ws_object_create; -} - -static void king_ws_server_object_handlers_init(void) -{ - memcpy( - &king_ws_server_object_handlers, - &std_object_handlers, - sizeof(zend_object_handlers) - ); - king_ws_server_object_handlers.offset = XtOffsetOf(king_ws_server_object, std); - king_ws_server_object_handlers.free_obj = king_ws_server_object_free; - king_ws_server_object_handlers.clone_obj = NULL; - king_ce_ws_server->create_object = king_ws_server_object_create; -} - -static void king_session_object_handlers_init(void) -{ - memcpy( - &king_session_object_handlers, - &std_object_handlers, - sizeof(zend_object_handlers) - ); - king_session_object_handlers.offset = XtOffsetOf(king_session_object, std); - king_session_object_handlers.free_obj = king_session_object_free; - king_session_object_handlers.clone_obj = NULL; - king_ce_session->create_object = king_session_object_create; -} diff --git a/extension/src/php_king/resources.inc b/extension/src/php_king/resources.inc index e970c77e3..a8a46d640 100644 --- a/extension/src/php_king/resources.inc +++ b/extension/src/php_king/resources.inc @@ -1,41 +1,11 @@ /* - * Zend resource destructors for php_king. Releases session, config, MCP and - * related native resource handles when their userland wrappers are freed. + * Core Zend resource-id registration orchestration. Concrete resource type + * names and destructors stay with the subsystem that owns the native state. */ -#include "include/mcp/mcp.h" - -static void king_session_dtor(zend_resource *rsrc) -{ - if (rsrc->ptr) { - king_client_session_free(rsrc->ptr); - } -} - -static void king_cfg_dtor(zend_resource *rsrc) -{ - if (rsrc->ptr) { - king_config_free((king_cfg_t *)rsrc->ptr); - } -} - -static void king_mcp_dtor(zend_resource *rsrc) -{ - if (rsrc->ptr) { - king_mcp_state_free((king_mcp_state *) rsrc->ptr); - } -} - -static void king_ws_dtor(zend_resource *rsrc) -{ - if (rsrc->ptr) { - king_ws_state_free((king_ws_state *) rsrc->ptr); - } -} - -static void king_http1_request_context_dtor(zend_resource *rsrc) +void king_register_resource_types(int module_number) { - if (rsrc->ptr) { - king_http1_request_context_free((king_http1_request_context *) rsrc->ptr); - } + king_client_register_resource_types(module_number); + king_config_register_resource_types(module_number); + king_mcp_register_resource_types(module_number); } diff --git a/extension/src/php_king/state.inc b/extension/src/php_king/state.inc index b0ba95eae..040f003f4 100644 --- a/extension/src/php_king/state.inc +++ b/extension/src/php_king/state.inc @@ -1,18 +1,8 @@ /* - * Shared php_king module state. Declares the resource ids, exception class - * pointers and forward declarations reused across the bootstrap slices. + * Shared php_king module state. Declares core exception class pointers, + * and owns the shared error buffer reused across bootstrap slices. */ -typedef struct king_cfg_s king_cfg_t; -void king_config_free(king_cfg_t *cfg); - -int le_king_session = -1; -int le_king_cfg = -1; -int le_king_perf = -1; -int le_king_mcp = -1; -int le_king_ws = -1; -int le_king_request_context = -1; - zend_class_entry *king_ce_exception = NULL; zend_class_entry *king_ce_stream_exception = NULL; zend_class_entry *king_ce_invalid_state = NULL; @@ -34,42 +24,6 @@ zend_class_entry *king_ce_timeout_exception = NULL; zend_class_entry *king_ce_network_exception = NULL; zend_class_entry *king_ce_tls_exception = NULL; zend_class_entry *king_ce_protocol_exception = NULL; -zend_class_entry *king_ce_mcp_exception = NULL; -zend_class_entry *king_ce_mcp_connection_error = NULL; -zend_class_entry *king_ce_mcp_protocol_error = NULL; -zend_class_entry *king_ce_mcp_timeout = NULL; -zend_class_entry *king_ce_mcp_data_error = NULL; -zend_class_entry *king_ce_ws_exception = NULL; -zend_class_entry *king_ce_ws_connection_error = NULL; -zend_class_entry *king_ce_ws_protocol_error = NULL; -zend_class_entry *king_ce_ws_timeout = NULL; -zend_class_entry *king_ce_ws_closed = NULL; -zend_class_entry *king_ce_cancel_token = NULL; -zend_class_entry *king_ce_awaitable = NULL; -zend_class_entry *king_ce_config = NULL; -zend_class_entry *king_ce_session = NULL; -zend_class_entry *king_ce_stream = NULL; -zend_class_entry *king_ce_response = NULL; -zend_class_entry *king_ce_mcp = NULL; -zend_class_entry *king_ce_pipeline_orchestrator = NULL; -zend_class_entry *king_ce_object_store = NULL; -zend_class_entry *king_ce_autoscaling = NULL; -zend_class_entry *king_ce_client_http = NULL; -zend_class_entry *king_ce_client_http1 = NULL; -zend_class_entry *king_ce_client_http2 = NULL; -zend_class_entry *king_ce_client_http3 = NULL; -zend_class_entry *king_ce_ws_server = NULL; -zend_class_entry *king_ce_ws_connection = NULL; -static zend_object_handlers king_cancel_token_object_handlers; -static zend_object_handlers king_awaitable_object_handlers; -static zend_object_handlers king_config_object_handlers; -static zend_object_handlers king_stream_object_handlers; -static zend_object_handlers king_response_object_handlers; -static zend_object_handlers king_mcp_object_handlers; -static zend_object_handlers king_http_client_object_handlers; -static zend_object_handlers king_session_object_handlers; -static zend_object_handlers king_ws_object_handlers; -static zend_object_handlers king_ws_server_object_handlers; char king_last_error[KING_ERR_LEN] = {0}; @@ -85,24 +39,3 @@ const char *king_get_error(void) { return king_last_error; } - -void *king_fetch_config(zval *zcfg) -{ - if (Z_TYPE_P(zcfg) == IS_OBJECT - && king_ce_config != NULL - && instanceof_function(Z_OBJCE_P(zcfg), king_ce_config)) { - king_config_object *intern = php_king_config_obj_from_zend(Z_OBJ_P(zcfg)); - - if (Z_ISUNDEF(intern->resource) || Z_TYPE(intern->resource) != IS_RESOURCE) { - return NULL; - } - - zcfg = &intern->resource; - } - - if (Z_TYPE_P(zcfg) != IS_RESOURCE) { - return NULL; - } - - return zend_fetch_resource(Z_RES_P(zcfg), "King\\Config", le_king_cfg); -} diff --git a/extension/src/pipeline_orchestrator/class_entries.inc b/extension/src/pipeline_orchestrator/class_entries.inc new file mode 100644 index 000000000..f791cc1f6 --- /dev/null +++ b/extension/src/pipeline_orchestrator/class_entries.inc @@ -0,0 +1,5 @@ +/* + * Pipeline orchestrator PHP class-entry storage. + */ + +zend_class_entry *king_ce_pipeline_orchestrator = NULL; diff --git a/extension/src/pipeline_orchestrator/orchestrator.c b/extension/src/pipeline_orchestrator/orchestrator.c index 7b1fee98a..a54c730a2 100644 --- a/extension/src/pipeline_orchestrator/orchestrator.c +++ b/extension/src/pipeline_orchestrator/orchestrator.c @@ -4,10 +4,12 @@ * classification and the persisted step snapshots used by resume/recovery. */ #include "php_king.h" -#include "include/config/mcp_and_orchestrator/base_layer.h" -#include "include/king_hrtime.h" -#include "include/pipeline_orchestrator/orchestrator.h" -#include "include/telemetry/telemetry.h" +#include "php_king/core_arginfo.h" +#include "config/mcp_and_orchestrator/base_layer.h" +#include "king_hrtime.h" +#include "pipeline_orchestrator/arginfo/index.h" +#include "pipeline_orchestrator/orchestrator.h" +#include "telemetry/telemetry.h" #include "ext/standard/base64.h" #include "ext/standard/php_var.h" #include "zend_smart_str.h" @@ -36,3 +38,5 @@ typedef struct _king_orchestrator_error_meta { #include "orchestrator/remote_transport.inc" #include "orchestrator/pipeline_execution.inc" #include "orchestrator/runtime_api.inc" +#include "state.inc" +#include "registration.inc" diff --git a/extension/src/pipeline_orchestrator/orchestrator/runtime_api.inc b/extension/src/pipeline_orchestrator/orchestrator/runtime_api.inc index 00df06b55..4591f65d8 100644 --- a/extension/src/pipeline_orchestrator/orchestrator/runtime_api.inc +++ b/extension/src/pipeline_orchestrator/orchestrator/runtime_api.inc @@ -1,1130 +1,226 @@ -static zend_bool king_orchestrator_distributed_tracing_enabled(void) -{ - return king_mcp_orchestrator_config.orchestrator_enable_distributed_tracing ? 1 : 0; -} - -static const char *king_orchestrator_boundary_backend_label(void) -{ - if ( - king_mcp_orchestrator_config.orchestrator_execution_backend == NULL - || king_mcp_orchestrator_config.orchestrator_execution_backend[0] == '\0' - ) { - return "local"; - } - - return king_mcp_orchestrator_config.orchestrator_execution_backend; -} - -static void king_orchestrator_capture_distributed_parent_context(zval *destination) -{ - if (destination == NULL) { - return; - } - - ZVAL_NULL(destination); - if (!king_orchestrator_distributed_tracing_enabled()) { - return; - } - - if (!king_telemetry_build_distributed_parent_context(destination)) { - ZVAL_NULL(destination); - } -} - -static const char *king_orchestrator_boundary_origin_label(const char *function_name) -{ - if ( - function_name != NULL - && strcmp(function_name, "king_pipeline_orchestrator_worker_run_next") == 0 - ) { - return "file_worker"; - } +#include "runtime_api/pipeline_observability.inc" - return "process_resume"; -} +#include "runtime_api/handler_invocation.inc" -static zend_result king_orchestrator_begin_boundary_span( +static int king_orchestrator_execute_existing_run( zend_string *run_id, + zval *initial_data, + zval *pipeline_array, + zval *return_value, + king_orchestrator_exec_control_t *control, const char *function_name, - king_trace_context_t **span_out -) -{ - zval attributes; - zend_result rc; - - if (span_out == NULL) { - return FAILURE; - } - - *span_out = NULL; - if (!king_telemetry_has_incoming_parent_context()) { - return SUCCESS; - } - - array_init(&attributes); - add_assoc_string(&attributes, "component", "pipeline_orchestrator"); - add_assoc_string( - &attributes, - "boundary_origin", - (char *) king_orchestrator_boundary_origin_label(function_name) - ); - add_assoc_string( - &attributes, - "execution_backend", - (char *) king_orchestrator_boundary_backend_label() - ); - add_assoc_string(&attributes, "hierarchy_source", "distributed_parent_context"); - if (run_id != NULL) { - add_assoc_stringl(&attributes, "run_id", ZSTR_VAL(run_id), ZSTR_LEN(run_id)); - } - - rc = king_telemetry_start_internal_span( - span_out, - "pipeline-orchestrator-boundary", - KING_SPAN_KIND_CONSUMER, - &attributes, - NULL - ); - zval_ptr_dtor(&attributes); - - return rc; -} - -static void king_orchestrator_finish_boundary_span(king_trace_context_t *span, int rc) -{ - zval final_attributes; - - if (span == NULL) { - return; - } - - array_init(&final_attributes); - add_assoc_string(&final_attributes, "outcome", rc == SUCCESS ? "completed" : "failed"); - (void) king_telemetry_end_internal_span(span, &final_attributes); - zval_ptr_dtor(&final_attributes); -} - -static const char *king_orchestrator_runtime_topology_scope(void) -{ - if (king_orchestrator_backend_is_file_worker()) { - return "same_host_file_worker"; - } - - if (king_orchestrator_backend_is_remote_peer()) { - return "tcp_host_port_execution_peer"; - } - - return "local_in_process"; -} - -static zval *king_orchestrator_find_step_telemetry_id( - zval *step, - const char *field_name, - size_t field_name_len -) -{ - zval *value; - - if (step == NULL || Z_TYPE_P(step) != IS_ARRAY) { - return NULL; - } - - value = zend_hash_str_find(Z_ARRVAL_P(step), field_name, field_name_len); - if (value == NULL || Z_TYPE_P(value) != IS_STRING || Z_STRLEN_P(value) == 0) { - return NULL; - } - - return value; -} - -static zend_long king_orchestrator_run_attempt_number(zend_string *run_id) + zend_bool throw_on_error) { - zval snapshot; - zval *observability; - zval *recovery_count; - zend_long attempt_number = 1; - - if (run_id == NULL) { - return 1; - } - - ZVAL_NULL(&snapshot); - if (king_orchestrator_get_run_snapshot(run_id, &snapshot) != SUCCESS) { - return 1; - } + HashTable *ht; + zval *step; + uint32_t index = 0; + zend_bool cancelled = 0; + zend_long failed_step_index = -1; + const char *execution_backend = king_orchestrator_boundary_backend_label(); + const char *topology_scope = king_orchestrator_runtime_topology_scope(); + zend_long attempt_number; + zend_long persisted_completed_step_count = 0; + zend_string *attempt_identity = NULL; + king_trace_context_t *run_span = NULL; + zval handler_boundary; + zval current_payload; + zend_bool use_registered_tool_handlers = king_orchestrator_executes_userland_handlers_locally(); + zend_bool use_handler_boundary = 0; - observability = zend_hash_str_find( - Z_ARRVAL(snapshot), - "distributed_observability", - sizeof("distributed_observability") - 1 - ); - if (observability != NULL && Z_TYPE_P(observability) == IS_ARRAY) { - recovery_count = zend_hash_str_find( - Z_ARRVAL_P(observability), - "recovery_count", - sizeof("recovery_count") - 1 + if (initial_data == NULL || pipeline_array == NULL || Z_TYPE_P(pipeline_array) != IS_ARRAY) { + return king_orchestrator_raise_error( + "king_pipeline_orchestrator_run() requires an array pipeline definition.", + king_ce_validation_exception, + throw_on_error ); - if (recovery_count != NULL && Z_TYPE_P(recovery_count) == IS_LONG && Z_LVAL_P(recovery_count) >= 0) { - attempt_number = Z_LVAL_P(recovery_count) + 1; - } - } - - zval_ptr_dtor(&snapshot); - return attempt_number > 0 ? attempt_number : 1; -} - -static zend_string *king_orchestrator_build_attempt_identity(zend_string *run_id) -{ - return strpprintf( - 0, - "%s:attempt-" ZEND_LONG_FMT, - run_id != NULL ? ZSTR_VAL(run_id) : "run-unknown", - king_orchestrator_run_attempt_number(run_id) - ); -} - -static zend_string *king_orchestrator_build_failure_identity( - zend_string *run_id, - zend_string *attempt_identity, - const char *category, - zend_long step_index, - const char *final_status -) -{ - return strpprintf( - 0, - "%s:%s:%s:" ZEND_LONG_FMT, - attempt_identity != NULL - ? ZSTR_VAL(attempt_identity) - : (run_id != NULL ? ZSTR_VAL(run_id) : "run-unknown"), - final_status != NULL && final_status[0] != '\0' ? final_status : "failed", - category != NULL && category[0] != '\0' ? category : "backend", - step_index - ); -} - -static void king_orchestrator_record_pipeline_counter( - const char *metric_name, - const char *execution_backend, - const char *topology_scope, - const char *outcome, - const char *category, - const char *retry_disposition -) -{ - zval labels; - - if (!king_telemetry_is_capture_enabled() || metric_name == NULL || metric_name[0] == '\0') { - return; } - array_init(&labels); - add_assoc_string(&labels, "component", "pipeline_orchestrator"); - add_assoc_string(&labels, "execution_backend", (char *) execution_backend); - add_assoc_string(&labels, "topology_scope", (char *) topology_scope); - if (outcome != NULL && outcome[0] != '\0') { - add_assoc_string(&labels, "outcome", (char *) outcome); - } - if (category != NULL && category[0] != '\0') { - add_assoc_string(&labels, "category", (char *) category); - } - if (retry_disposition != NULL && retry_disposition[0] != '\0') { - add_assoc_string(&labels, "retry_disposition", (char *) retry_disposition); + if (control != NULL) { + control->run_id = run_id; } - (void) king_telemetry_record_metric_internal(metric_name, KING_METRIC_TYPE_COUNTER, 1.0, &labels); - zval_ptr_dtor(&labels); -} -static void king_orchestrator_emit_pipeline_failure_log( - zend_string *run_id, - zend_string *attempt_identity, - zval *step, - zend_long step_index, - const char *execution_backend, - const char *category, - const char *retry_disposition, - const char *message, - const char *final_status -) -{ - zval attributes; - zval *partition_id; - zval *batch_id; - zend_string *failure_identity; + ZVAL_NULL(&handler_boundary); + ZVAL_UNDEF(¤t_payload); + attempt_number = king_orchestrator_run_attempt_number(run_id); + attempt_identity = king_orchestrator_build_attempt_identity(run_id); - if (!king_telemetry_is_capture_enabled()) { - return; + if ( + king_orchestrator_prepare_registered_handler_execution( + run_id, + initial_data, + &handler_boundary, + ¤t_payload, + &persisted_completed_step_count, + &use_registered_tool_handlers, + &use_handler_boundary, + throw_on_error + ) != SUCCESS + ) { + if (attempt_identity != NULL) { + zend_string_release(attempt_identity); + } + zval_ptr_dtor(&handler_boundary); + return FAILURE; } - partition_id = king_orchestrator_find_step_telemetry_id(step, "partition_id", sizeof("partition_id") - 1); - batch_id = king_orchestrator_find_step_telemetry_id(step, "batch_id", sizeof("batch_id") - 1); - failure_identity = king_orchestrator_build_failure_identity( + king_orchestrator_begin_pipeline_run_span( + &run_span, run_id, attempt_identity, - category, - step_index, - final_status + attempt_number, + execution_backend, + topology_scope ); - array_init(&attributes); - add_assoc_string(&attributes, "component", "pipeline_orchestrator"); - add_assoc_stringl(&attributes, "run_id", ZSTR_VAL(run_id), ZSTR_LEN(run_id)); - if (attempt_identity != NULL) { - add_assoc_stringl( - &attributes, - "attempt_identity", - ZSTR_VAL(attempt_identity), - ZSTR_LEN(attempt_identity) - ); - } else { - add_assoc_null(&attributes, "attempt_identity"); - } - add_assoc_long(&attributes, "step_index", step_index); - add_assoc_string(&attributes, "execution_backend", (char *) execution_backend); - add_assoc_string(&attributes, "topology_scope", (char *) king_orchestrator_runtime_topology_scope()); - add_assoc_string(&attributes, "failure_status", (char *) final_status); - add_assoc_string(&attributes, "failure_category", (char *) category); - add_assoc_string(&attributes, "retry_disposition", (char *) retry_disposition); - add_assoc_str(&attributes, "failure_identity", failure_identity); - if (partition_id != NULL) { - add_assoc_stringl(&attributes, "partition_id", Z_STRVAL_P(partition_id), Z_STRLEN_P(partition_id)); - } - if (batch_id != NULL) { - add_assoc_stringl(&attributes, "batch_id", Z_STRVAL_P(batch_id), Z_STRLEN_P(batch_id)); - } - - (void) king_telemetry_log_internal( - strcmp(final_status, "cancelled") == 0 ? KING_TELEMETRY_LEVEL_WARN : KING_TELEMETRY_LEVEL_ERROR, - "pipeline_orchestrator", - message != NULL && message[0] != '\0' ? message : "pipeline run failed", - &attributes - ); - zval_ptr_dtor(&attributes); -} - -static void king_orchestrator_append_pipeline_span_attributes( - zval *attributes, - zend_string *run_id, - zend_string *attempt_identity, - zval *step, - zend_long step_index -) -{ - zval *partition_id; - zval *batch_id; - zval *tool; - - if (attributes == NULL) { - return; - } - - add_assoc_string(attributes, "component", "pipeline_orchestrator"); - add_assoc_string(attributes, "telemetry_adapter_contract", "run_partition_batch_retry_failure_identity"); - add_assoc_stringl(attributes, "run_id", ZSTR_VAL(run_id), ZSTR_LEN(run_id)); - if (attempt_identity != NULL) { - add_assoc_stringl( - attributes, - "attempt_identity", - ZSTR_VAL(attempt_identity), - ZSTR_LEN(attempt_identity) - ); - } else { - add_assoc_null(attributes, "attempt_identity"); - } - add_assoc_string(attributes, "execution_backend", (char *) king_orchestrator_boundary_backend_label()); - add_assoc_string(attributes, "topology_scope", (char *) king_orchestrator_runtime_topology_scope()); - - if (step_index >= 0) { - add_assoc_long(attributes, "step_index", step_index); - } + if (king_orchestrator_exec_control_check(control, function_name, throw_on_error, &cancelled) != SUCCESS) { + zend_string *failure_identity = NULL; + const char *category = cancelled ? "cancelled" : "backend"; + const char *retry_disposition = cancelled ? "not_applicable" : "caller_managed_retry"; - if (step != NULL && Z_TYPE_P(step) == IS_ARRAY) { - tool = zend_hash_str_find(Z_ARRVAL_P(step), "tool", sizeof("tool") - 1); - if (tool != NULL && Z_TYPE_P(tool) == IS_STRING && Z_STRLEN_P(tool) > 0) { - add_assoc_stringl(attributes, "step_tool", Z_STRVAL_P(tool), Z_STRLEN_P(tool)); + if (!cancelled + && EG(exception) != NULL + && instanceof_function(EG(exception)->ce, king_ce_timeout_exception)) { + category = "timeout"; } - } - - partition_id = king_orchestrator_find_step_telemetry_id(step, "partition_id", sizeof("partition_id") - 1); - if (partition_id != NULL) { - add_assoc_stringl(attributes, "partition_id", Z_STRVAL_P(partition_id), Z_STRLEN_P(partition_id)); - } - - batch_id = king_orchestrator_find_step_telemetry_id(step, "batch_id", sizeof("batch_id") - 1); - if (batch_id != NULL) { - add_assoc_stringl(attributes, "batch_id", Z_STRVAL_P(batch_id), Z_STRLEN_P(batch_id)); - } -} - -static void king_orchestrator_finish_pipeline_span( - king_trace_context_t *span, - const char *outcome, - const char *category, - const char *retry_disposition, - zend_string *failure_identity -) -{ - zval final_attributes; - - if (span == NULL) { - return; - } - - array_init(&final_attributes); - add_assoc_string( - &final_attributes, - "outcome", - (char *) ( - outcome != NULL && outcome[0] != '\0' - ? outcome - : "completed" - ) - ); - if (category != NULL && category[0] != '\0') { - add_assoc_string(&final_attributes, "failure_category", (char *) category); - } - if (retry_disposition != NULL && retry_disposition[0] != '\0') { - add_assoc_string(&final_attributes, "retry_disposition", (char *) retry_disposition); - } - if (failure_identity != NULL) { - add_assoc_stringl( - &final_attributes, - "failure_identity", - ZSTR_VAL(failure_identity), - ZSTR_LEN(failure_identity) + if (attempt_identity != NULL) { + failure_identity = king_orchestrator_build_failure_identity( + run_id, + attempt_identity, + category, + -1, + cancelled ? "cancelled" : "failed" + ); + } + king_orchestrator_finish_pipeline_span( + run_span, + cancelled ? "cancelled" : "failed", + category, + retry_disposition, + failure_identity ); + if (failure_identity != NULL) { + zend_string_release(failure_identity); + } + king_orchestrator_mark_interrupted_run(run_id, cancelled, -1, NULL); + if (use_registered_tool_handlers) { + zval_ptr_dtor(¤t_payload); + } + zval_ptr_dtor(&handler_boundary); + ZVAL_FALSE(return_value); + if (attempt_identity != NULL) { + zend_string_release(attempt_identity); + } + return FAILURE; } - (void) king_telemetry_end_internal_span(span, &final_attributes); - zval_ptr_dtor(&final_attributes); -} - -static zend_bool king_orchestrator_executes_userland_handlers_locally(void) -{ - return !king_orchestrator_backend_is_file_worker() && !king_orchestrator_backend_is_remote_peer(); -} - -static zend_bool king_orchestrator_handler_boundary_requires_process_registration( - zval *handler_boundary -) -{ - zval *requires_process_registration; - - if (handler_boundary == NULL || Z_TYPE_P(handler_boundary) != IS_ARRAY) { - return 0; - } - - requires_process_registration = zend_hash_str_find( - Z_ARRVAL_P(handler_boundary), - "requires_process_registration", - sizeof("requires_process_registration") - 1 - ); - - return requires_process_registration != NULL && zend_is_true(requires_process_registration); -} - -static zend_bool king_orchestrator_handler_boundary_step_uses_registered_handler( - zval *handler_boundary, - zval *step, - zend_long step_index -) -{ - zval *required_step_refs; - zval *tool; - zval *entry; - - if ( - handler_boundary == NULL - || Z_TYPE_P(handler_boundary) != IS_ARRAY - || step == NULL - || Z_TYPE_P(step) != IS_ARRAY - ) { - return 0; - } - - tool = zend_hash_str_find(Z_ARRVAL_P(step), "tool", sizeof("tool") - 1); - if (tool == NULL || Z_TYPE_P(tool) != IS_STRING || Z_STRLEN_P(tool) == 0) { - return 0; - } - - required_step_refs = zend_hash_str_find( - Z_ARRVAL_P(handler_boundary), - "required_step_refs", - sizeof("required_step_refs") - 1 - ); - if (required_step_refs == NULL || Z_TYPE_P(required_step_refs) != IS_ARRAY) { - return 0; + if (!use_registered_tool_handlers) { + /* Runs without a registered-handler execution boundary pass initial data through. */ + ZVAL_COPY(return_value, initial_data); } - ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(required_step_refs), entry) { - zval *entry_index; - zval *tool_name; + ht = Z_ARRVAL_P(pipeline_array); + ZEND_HASH_FOREACH_VAL(ht, step) { + king_trace_context_t *step_span = NULL; + zend_string *failure_identity = NULL; + zend_bool step_uses_registered_handler = use_registered_tool_handlers && !use_handler_boundary; + zval step_output; - if (entry == NULL || Z_TYPE_P(entry) != IS_ARRAY) { + if (use_registered_tool_handlers && (zend_long) index < persisted_completed_step_count) { + index++; continue; } - entry_index = zend_hash_str_find(Z_ARRVAL_P(entry), "index", sizeof("index") - 1); - tool_name = zend_hash_str_find( - Z_ARRVAL_P(entry), - "tool_name", - sizeof("tool_name") - 1 - ); - if ( - entry_index == NULL - || Z_TYPE_P(entry_index) != IS_LONG - || Z_LVAL_P(entry_index) != step_index - || tool_name == NULL - || Z_TYPE_P(tool_name) != IS_STRING - || !zend_string_equals(Z_STR_P(tool_name), Z_STR_P(tool)) - ) { - continue; + if (use_handler_boundary) { + step_uses_registered_handler = king_orchestrator_handler_boundary_step_uses_registered_handler( + &handler_boundary, + step, + (zend_long) index + ); } - return 1; - } ZEND_HASH_FOREACH_END(); - - return 0; -} - -static const char *king_orchestrator_failure_retry_disposition_for_category( - const char *category -) -{ - if (category == NULL || category[0] == '\0') { - return "caller_managed_retry"; - } - - if (strcmp(category, "validation") == 0) { - return "non_retryable"; - } - - if (strcmp(category, "cancelled") == 0) { - return "not_applicable"; - } - - return "caller_managed_retry"; -} - -static const char *king_orchestrator_exception_message_or_last_error(void) -{ - zval rv; - zval *message; - const char *last_error = king_get_error(); - - if (EG(exception) == NULL) { - return last_error; - } + if (king_telemetry_is_capture_enabled() && attempt_identity != NULL) { + zval step_attributes; - message = zend_read_property( - zend_ce_exception, - EG(exception), - "message", - sizeof("message") - 1, - 1, - &rv - ); - if (message == NULL || Z_TYPE_P(message) != IS_STRING || Z_STRLEN_P(message) == 0) { - return last_error; - } - - king_set_error(Z_STRVAL_P(message)); - return king_get_error(); -} - -static void king_orchestrator_classify_handler_throwable_failure( - const char **category_out, - const char **retry_disposition_out -) -{ - const char *category = "runtime"; - - if ( - EG(exception) != NULL - && instanceof_function(EG(exception)->ce, king_ce_validation_exception) - ) { - category = "validation"; - } else if ( - EG(exception) != NULL - && instanceof_function(EG(exception)->ce, king_ce_timeout_exception) - ) { - category = "timeout"; - } - - if (category_out != NULL) { - *category_out = category; - } - if (retry_disposition_out != NULL) { - *retry_disposition_out = king_orchestrator_failure_retry_disposition_for_category(category); - } -} - -static zend_result king_orchestrator_invoke_registered_tool_handler( - zend_string *run_id, - zval *step, - uint32_t step_index, - zval *current_payload, - zval *step_output, - const char *execution_backend, - const char *topology_scope, - zend_long attempt_number, - king_orchestrator_exec_control_t *control, - const char *function_name, - zend_bool throw_on_error, - const char **failure_category_out, - const char **retry_disposition_out -) -{ - zval *tool; - zval *handler; - zval context; - zval context_input; - zval tool_metadata; - zval tool_config; - zval run_metadata; - zval step_metadata; - zval step_definition; - zval cancel; - zval handler_result; - zval *result_output; - zend_fcall_info fci; - zend_fcall_info_cache fcc; - char message[KING_ERR_LEN]; - - if (failure_category_out != NULL) { - *failure_category_out = "backend"; - } - if (retry_disposition_out != NULL) { - *retry_disposition_out = "caller_managed_retry"; - } - - if ( - step == NULL - || Z_TYPE_P(step) != IS_ARRAY - || current_payload == NULL - || step_output == NULL - ) { - if (failure_category_out != NULL) { - *failure_category_out = "backend"; - } - return king_orchestrator_raise_error( - "king_pipeline_orchestrator_run() could not prepare the registered userland handler invocation.", - king_ce_runtime_exception, - throw_on_error - ); - } - - ZVAL_NULL(step_output); - tool = zend_hash_str_find(Z_ARRVAL_P(step), "tool", sizeof("tool") - 1); - if (tool == NULL || Z_TYPE_P(tool) != IS_STRING || Z_STRLEN_P(tool) == 0) { - if (failure_category_out != NULL) { - *failure_category_out = "validation"; - } - if (retry_disposition_out != NULL) { - *retry_disposition_out = "non_retryable"; - } - return king_orchestrator_raise_error( - "king_pipeline_orchestrator_run() step requires a non-empty tool name before registered handler execution.", - king_ce_validation_exception, - throw_on_error - ); - } - - handler = king_orchestrator_lookup_tool_handler(Z_STRVAL_P(tool), Z_STRLEN_P(tool)); - if (handler == NULL) { - snprintf( - message, - sizeof(message), - "%s() has no registered handler for tool '%s'.", - function_name != NULL ? function_name : "king_pipeline_orchestrator_run", - Z_STRVAL_P(tool) - ); - if (failure_category_out != NULL) { - *failure_category_out = "missing_handler"; - } - return king_orchestrator_raise_error( - message, - king_ce_runtime_exception, - throw_on_error - ); - } - - ZVAL_NULL(&tool_config); - if ( - king_orchestrator_load_tool_config( - Z_STRVAL_P(tool), - Z_STRLEN_P(tool), - &tool_config - ) != SUCCESS - ) { - snprintf( - message, - sizeof(message), - "%s() could not load the persisted tool config for registered handler '%s'.", - function_name != NULL ? function_name : "king_pipeline_orchestrator_run", - Z_STRVAL_P(tool) - ); - if (failure_category_out != NULL) { - *failure_category_out = "backend"; - } - return king_orchestrator_raise_error( - message, - king_ce_runtime_exception, - throw_on_error - ); - } - - if (zend_fcall_info_init(handler, 0, &fci, &fcc, NULL, NULL) != SUCCESS) { - zval_ptr_dtor(&tool_config); - snprintf( - message, - sizeof(message), - "%s() found an invalid registered handler for tool '%s'.", - function_name != NULL ? function_name : "king_pipeline_orchestrator_run", - Z_STRVAL_P(tool) - ); - if (failure_category_out != NULL) { - *failure_category_out = "missing_handler"; - } - return king_orchestrator_raise_error( - message, - king_ce_runtime_exception, - throw_on_error - ); - } - - array_init(&context); - ZVAL_COPY(&context_input, current_payload); - add_assoc_zval(&context, "input", &context_input); - - array_init(&tool_metadata); - add_assoc_stringl(&tool_metadata, "name", Z_STRVAL_P(tool), Z_STRLEN_P(tool)); - add_assoc_zval(&tool_metadata, "config", &tool_config); - add_assoc_zval(&context, "tool", &tool_metadata); - - array_init(&run_metadata); - if (run_id != NULL) { - add_assoc_stringl(&context, "run_id", ZSTR_VAL(run_id), ZSTR_LEN(run_id)); - add_assoc_stringl(&run_metadata, "run_id", ZSTR_VAL(run_id), ZSTR_LEN(run_id)); - } else { - add_assoc_null(&context, "run_id"); - add_assoc_null(&run_metadata, "run_id"); - } - add_assoc_long(&run_metadata, "attempt_number", attempt_number > 0 ? attempt_number : 1); - add_assoc_string( - &run_metadata, - "execution_backend", - (char *) (execution_backend != NULL ? execution_backend : "local") - ); - add_assoc_string( - &run_metadata, - "topology_scope", - (char *) (topology_scope != NULL ? topology_scope : "local_in_process") - ); - add_assoc_zval(&context, "run", &run_metadata); - - array_init(&step_metadata); - add_assoc_long(&step_metadata, "index", (zend_long) step_index); - add_assoc_stringl(&step_metadata, "tool_name", Z_STRVAL_P(tool), Z_STRLEN_P(tool)); - ZVAL_COPY(&step_definition, step); - add_assoc_zval(&step_metadata, "definition", &step_definition); - add_assoc_zval(&context, "step", &step_metadata); - - if (control != NULL && !Z_ISUNDEF(control->cancel_token)) { - ZVAL_COPY(&cancel, &control->cancel_token); - } else { - ZVAL_NULL(&cancel); - } - add_assoc_zval(&context, "cancel", &cancel); - - add_assoc_long( - &context, - "timeout_budget_ms", - king_orchestrator_control_timeout_budget_ms(control) - ); - add_assoc_long( - &context, - "deadline_budget_ms", - king_orchestrator_control_deadline_budget_ms(control) - ); - - ZVAL_NULL(&handler_result); - fci.retval = &handler_result; - fci.param_count = 1; - fci.params = &context; - - if (zend_call_function(&fci, &fcc) != SUCCESS) { - zval_ptr_dtor(&context); - if (EG(exception) == NULL) { - snprintf( - message, - sizeof(message), - "%s() failed while invoking the registered handler for tool '%s'.", - function_name != NULL ? function_name : "king_pipeline_orchestrator_run", - Z_STRVAL_P(tool) + array_init(&step_attributes); + king_orchestrator_append_pipeline_span_attributes( + &step_attributes, + run_id, + attempt_identity, + step, + (zend_long) index + ); + add_assoc_string(&step_attributes, "identity_scope", "step"); + if (attempt_number > 1) { + add_assoc_stringl( + &step_attributes, + "retry_identity", + ZSTR_VAL(attempt_identity), + ZSTR_LEN(attempt_identity) ); - if (failure_category_out != NULL) { - *failure_category_out = "runtime"; } - return king_orchestrator_raise_error( - message, - king_ce_runtime_exception, - throw_on_error + (void) king_telemetry_start_internal_span( + &step_span, + "pipeline-orchestrator-step", + KING_SPAN_KIND_INTERNAL, + &step_attributes, + NULL ); - } - king_orchestrator_classify_handler_throwable_failure( - failure_category_out, - retry_disposition_out - ); - (void) king_orchestrator_exception_message_or_last_error(); - return FAILURE; - } - - zval_ptr_dtor(&context); - if (EG(exception) != NULL) { - zval_ptr_dtor(&handler_result); - king_orchestrator_classify_handler_throwable_failure( - failure_category_out, - retry_disposition_out - ); - (void) king_orchestrator_exception_message_or_last_error(); - return FAILURE; - } - - if (Z_TYPE(handler_result) != IS_ARRAY) { - zval_ptr_dtor(&handler_result); - snprintf( - message, - sizeof(message), - "%s() registered handler for tool '%s' must return an array result contract.", - function_name != NULL ? function_name : "king_pipeline_orchestrator_run", - Z_STRVAL_P(tool) - ); - if (failure_category_out != NULL) { - *failure_category_out = "runtime"; - } - return king_orchestrator_raise_error( - message, - king_ce_runtime_exception, - throw_on_error - ); - } - - result_output = zend_hash_str_find( - Z_ARRVAL(handler_result), - "output", - sizeof("output") - 1 - ); - if (result_output == NULL || Z_TYPE_P(result_output) != IS_ARRAY) { - zval_ptr_dtor(&handler_result); - snprintf( - message, - sizeof(message), - "%s() registered handler for tool '%s' must return an array result contract with key 'output'.", - function_name != NULL ? function_name : "king_pipeline_orchestrator_run", - Z_STRVAL_P(tool) - ); - if (failure_category_out != NULL) { - *failure_category_out = "runtime"; - } - return king_orchestrator_raise_error( - message, - king_ce_runtime_exception, - throw_on_error - ); - } - - ZVAL_COPY(step_output, result_output); - zval_ptr_dtor(&handler_result); - - return SUCCESS; -} - -static int king_orchestrator_execute_existing_run( - zend_string *run_id, - zval *initial_data, - zval *pipeline_array, - zval *return_value, - king_orchestrator_exec_control_t *control, - const char *function_name, - zend_bool throw_on_error) -{ - HashTable *ht; - zval *step; - uint32_t index = 0; - zend_bool cancelled = 0; - zend_long failed_step_index = -1; - const char *execution_backend = king_orchestrator_boundary_backend_label(); - const char *topology_scope = king_orchestrator_runtime_topology_scope(); - zend_long attempt_number; - zend_long persisted_completed_step_count = 0; - zend_string *attempt_identity = NULL; - king_trace_context_t *run_span = NULL; - zval handler_boundary; - zval current_payload; - zend_bool use_registered_tool_handlers = king_orchestrator_executes_userland_handlers_locally(); - zend_bool use_handler_boundary = 0; - - if (initial_data == NULL || pipeline_array == NULL || Z_TYPE_P(pipeline_array) != IS_ARRAY) { - return king_orchestrator_raise_error( - "king_pipeline_orchestrator_run() requires an array pipeline definition.", - king_ce_validation_exception, - throw_on_error - ); - } - - if (control != NULL) { - control->run_id = run_id; - } - - ZVAL_NULL(&handler_boundary); - ZVAL_UNDEF(¤t_payload); - attempt_number = king_orchestrator_run_attempt_number(run_id); - attempt_identity = king_orchestrator_build_attempt_identity(run_id); + zval_ptr_dtor(&step_attributes); - if (king_orchestrator_backend_is_file_worker()) { - if (run_id == NULL || king_orchestrator_load_run_handler_boundary(run_id, &handler_boundary) != SUCCESS) { - if (attempt_identity != NULL) { - zend_string_release(attempt_identity); + if (king_orchestrator_find_step_telemetry_id(step, "partition_id", sizeof("partition_id") - 1) != NULL) { + king_orchestrator_record_pipeline_counter( + "pipeline.partition.count", + execution_backend, + topology_scope, + "observed", + NULL, + NULL + ); + } + if (king_orchestrator_find_step_telemetry_id(step, "batch_id", sizeof("batch_id") - 1) != NULL) { + king_orchestrator_record_pipeline_counter( + "pipeline.batch.count", + execution_backend, + topology_scope, + "observed", + NULL, + NULL + ); } - zval_ptr_dtor(&handler_boundary); - return king_orchestrator_raise_error( - "king_pipeline_orchestrator_run() could not load the persisted file-worker handler boundary.", - king_ce_runtime_exception, - throw_on_error - ); } - if (king_orchestrator_handler_boundary_requires_process_registration(&handler_boundary)) { - use_registered_tool_handlers = 1; - use_handler_boundary = 1; - } - } + cancelled = 0; + if (king_orchestrator_exec_control_check(control, function_name, throw_on_error, &cancelled) != SUCCESS) { + const char *category = cancelled ? "cancelled" : "backend"; + const char *retry_disposition = cancelled ? "not_applicable" : "caller_managed_retry"; - if (use_registered_tool_handlers) { - if ( - run_id == NULL - || king_orchestrator_load_run_progress( - run_id, - ¤t_payload, - &persisted_completed_step_count - ) != SUCCESS - ) { + if (!cancelled + && EG(exception) != NULL + && instanceof_function(EG(exception)->ce, king_ce_timeout_exception)) { + category = "timeout"; + } if (attempt_identity != NULL) { - zend_string_release(attempt_identity); + failure_identity = king_orchestrator_build_failure_identity( + run_id, + attempt_identity, + category, + (zend_long) index, + cancelled ? "cancelled" : "failed" + ); } - zval_ptr_dtor(&handler_boundary); - return king_orchestrator_raise_error( - "king_pipeline_orchestrator_run() could not load persisted registered-handler progress.", - king_ce_runtime_exception, - throw_on_error + king_orchestrator_finish_pipeline_span( + step_span, + cancelled ? "cancelled" : "failed", + category, + retry_disposition, + failure_identity ); - } - - if (persisted_completed_step_count <= 0) { - zval_ptr_dtor(¤t_payload); - ZVAL_COPY(¤t_payload, initial_data); - } - } - - if (king_telemetry_is_capture_enabled() && attempt_identity != NULL) { - zval run_attributes; - - array_init(&run_attributes); - king_orchestrator_append_pipeline_span_attributes( - &run_attributes, - run_id, - attempt_identity, - NULL, - -1 - ); - add_assoc_string(&run_attributes, "identity_scope", "run"); - if (attempt_number > 1) { - add_assoc_stringl( - &run_attributes, - "retry_identity", - ZSTR_VAL(attempt_identity), - ZSTR_LEN(attempt_identity) - ); - } - (void) king_telemetry_start_internal_span( - &run_span, - "pipeline-orchestrator-run", - KING_SPAN_KIND_CONSUMER, - &run_attributes, - NULL - ); - zval_ptr_dtor(&run_attributes); - - king_orchestrator_record_pipeline_counter( - "pipeline.run.count", - execution_backend, - topology_scope, - "attempt", - NULL, - NULL - ); - if (attempt_number > 1) { - king_orchestrator_record_pipeline_counter( - "pipeline.retry.count", - execution_backend, - topology_scope, - "recovered_attempt", - NULL, - "caller_managed_retry" - ); - } - } - - if (king_orchestrator_exec_control_check(control, function_name, throw_on_error, &cancelled) != SUCCESS) { - zend_string *failure_identity = NULL; - const char *category = cancelled ? "cancelled" : "backend"; - const char *retry_disposition = cancelled ? "not_applicable" : "caller_managed_retry"; - - if (!cancelled - && EG(exception) != NULL - && instanceof_function(EG(exception)->ce, king_ce_timeout_exception)) { - category = "timeout"; - } - if (attempt_identity != NULL) { - failure_identity = king_orchestrator_build_failure_identity( - run_id, - attempt_identity, - category, - -1, - cancelled ? "cancelled" : "failed" - ); - } - king_orchestrator_finish_pipeline_span( - run_span, - cancelled ? "cancelled" : "failed", - category, - retry_disposition, - failure_identity - ); - if (failure_identity != NULL) { - zend_string_release(failure_identity); - } - king_orchestrator_mark_interrupted_run(run_id, cancelled, -1, NULL); - if (use_registered_tool_handlers) { - zval_ptr_dtor(¤t_payload); - } - zval_ptr_dtor(&handler_boundary); - ZVAL_FALSE(return_value); - if (attempt_identity != NULL) { - zend_string_release(attempt_identity); - } - return FAILURE; - } - - if (!use_registered_tool_handlers) { - /* Runs without a registered-handler execution boundary still use the placeholder path. */ - ZVAL_COPY(return_value, initial_data); - } - - ht = Z_ARRVAL_P(pipeline_array); - ZEND_HASH_FOREACH_VAL(ht, step) { - king_trace_context_t *step_span = NULL; - zend_string *failure_identity = NULL; - zend_bool step_uses_registered_handler = use_registered_tool_handlers && !use_handler_boundary; - zval step_output; - - if (use_registered_tool_handlers && (zend_long) index < persisted_completed_step_count) { - index++; - continue; - } - - if (use_handler_boundary) { - step_uses_registered_handler = king_orchestrator_handler_boundary_step_uses_registered_handler( - &handler_boundary, - step, - (zend_long) index - ); - } - - if (king_telemetry_is_capture_enabled() && attempt_identity != NULL) { - zval step_attributes; - - array_init(&step_attributes); - king_orchestrator_append_pipeline_span_attributes( - &step_attributes, - run_id, - attempt_identity, - step, - (zend_long) index - ); - add_assoc_string(&step_attributes, "identity_scope", "step"); - if (attempt_number > 1) { - add_assoc_stringl( - &step_attributes, - "retry_identity", - ZSTR_VAL(attempt_identity), - ZSTR_LEN(attempt_identity) - ); - } - (void) king_telemetry_start_internal_span( - &step_span, - "pipeline-orchestrator-step", - KING_SPAN_KIND_INTERNAL, - &step_attributes, - NULL - ); - zval_ptr_dtor(&step_attributes); - - if (king_orchestrator_find_step_telemetry_id(step, "partition_id", sizeof("partition_id") - 1) != NULL) { - king_orchestrator_record_pipeline_counter( - "pipeline.partition.count", - execution_backend, - topology_scope, - "observed", - NULL, - NULL - ); - } - if (king_orchestrator_find_step_telemetry_id(step, "batch_id", sizeof("batch_id") - 1) != NULL) { - king_orchestrator_record_pipeline_counter( - "pipeline.batch.count", - execution_backend, - topology_scope, - "observed", - NULL, - NULL - ); - } - } - - cancelled = 0; - if (king_orchestrator_exec_control_check(control, function_name, throw_on_error, &cancelled) != SUCCESS) { - const char *category = cancelled ? "cancelled" : "backend"; - const char *retry_disposition = cancelled ? "not_applicable" : "caller_managed_retry"; - - if (!cancelled - && EG(exception) != NULL - && instanceof_function(EG(exception)->ce, king_ce_timeout_exception)) { - category = "timeout"; - } - if (attempt_identity != NULL) { - failure_identity = king_orchestrator_build_failure_identity( - run_id, - attempt_identity, - category, - (zend_long) index, - cancelled ? "cancelled" : "failed" - ); - } - king_orchestrator_finish_pipeline_span( - step_span, - cancelled ? "cancelled" : "failed", - category, - retry_disposition, - failure_identity - ); - king_orchestrator_finish_pipeline_span( - run_span, - cancelled ? "cancelled" : "failed", - category, - retry_disposition, - failure_identity + king_orchestrator_finish_pipeline_span( + run_span, + cancelled ? "cancelled" : "failed", + category, + retry_disposition, + failure_identity ); king_orchestrator_emit_pipeline_failure_log( run_id, @@ -1513,1052 +609,173 @@ static int king_orchestrator_execute_existing_run( "failed", "backend", "caller_managed_retry" - ); - if (failure_identity != NULL) { - zend_string_release(failure_identity); - } - zval_ptr_dtor(return_value); - ZVAL_FALSE(return_value); - king_set_error("king_pipeline_orchestrator_run() failed to persist completed step progress."); - (void) king_orchestrator_pipeline_run_fail_classified( - run_id, - king_get_error(), - "backend", - "caller_managed_retry", - -1, - "controller" - ); - if (attempt_identity != NULL) { - zend_string_release(attempt_identity); - } - zval_ptr_dtor(&handler_boundary); - return king_orchestrator_raise_error( - king_get_error(), - king_ce_runtime_exception, - throw_on_error - ); - } - king_orchestrator_finish_pipeline_span( - step_span, - "completed", - NULL, - NULL, - NULL - ); - index++; - } ZEND_HASH_FOREACH_END(); - - cancelled = 0; - if (king_orchestrator_exec_control_check(control, function_name, throw_on_error, &cancelled) != SUCCESS) { - zend_string *failure_identity = NULL; - const char *category = cancelled ? "cancelled" : "backend"; - const char *retry_disposition = cancelled ? "not_applicable" : "caller_managed_retry"; - - if (!cancelled - && EG(exception) != NULL - && instanceof_function(EG(exception)->ce, king_ce_timeout_exception)) { - category = "timeout"; - } - if (attempt_identity != NULL) { - failure_identity = king_orchestrator_build_failure_identity( - run_id, - attempt_identity, - category, - -1, - cancelled ? "cancelled" : "failed" - ); - } - king_orchestrator_finish_pipeline_span( - run_span, - cancelled ? "cancelled" : "failed", - category, - retry_disposition, - failure_identity - ); - king_orchestrator_emit_pipeline_failure_log( - run_id, - attempt_identity, - NULL, - -1, - execution_backend, - category, - retry_disposition, - king_get_error(), - cancelled ? "cancelled" : "failed" - ); - king_orchestrator_record_pipeline_counter( - "pipeline.failure.count", - execution_backend, - topology_scope, - cancelled ? "cancelled" : "failed", - category, - retry_disposition - ); - if (failure_identity != NULL) { - zend_string_release(failure_identity); - } - if (use_registered_tool_handlers) { - zval_ptr_dtor(¤t_payload); - } else { - zval_ptr_dtor(return_value); - ZVAL_FALSE(return_value); - } - king_orchestrator_mark_interrupted_run(run_id, cancelled, -1, NULL); - if (attempt_identity != NULL) { - zend_string_release(attempt_identity); - } - zval_ptr_dtor(&handler_boundary); - return FAILURE; - } - - if (use_registered_tool_handlers) { - ZVAL_COPY(return_value, ¤t_payload); - zval_ptr_dtor(¤t_payload); - } - - if (king_orchestrator_pipeline_run_complete(run_id, return_value) != SUCCESS) { - zend_string *failure_identity = NULL; - - if (attempt_identity != NULL) { - failure_identity = king_orchestrator_build_failure_identity( - run_id, - attempt_identity, - "backend", - -1, - "failed" - ); - } - king_orchestrator_finish_pipeline_span( - run_span, - "failed", - "backend", - "caller_managed_retry", - failure_identity - ); - king_orchestrator_emit_pipeline_failure_log( - run_id, - attempt_identity, - NULL, - -1, - execution_backend, - "backend", - "caller_managed_retry", - "king_pipeline_orchestrator_run() failed to persist the completed run snapshot.", - "failed" - ); - king_orchestrator_record_pipeline_counter( - "pipeline.failure.count", - execution_backend, - topology_scope, - "failed", - "backend", - "caller_managed_retry" - ); - if (failure_identity != NULL) { - zend_string_release(failure_identity); - } - zval_ptr_dtor(return_value); - ZVAL_FALSE(return_value); - if (attempt_identity != NULL) { - zend_string_release(attempt_identity); - } - zval_ptr_dtor(&handler_boundary); - return king_orchestrator_raise_error( - "king_pipeline_orchestrator_run() failed to persist the completed run snapshot.", - king_ce_runtime_exception, - throw_on_error - ); - } - - king_orchestrator_finish_pipeline_span(run_span, "completed", NULL, NULL, NULL); - if (attempt_identity != NULL) { - zend_string_release(attempt_identity); - } - zval_ptr_dtor(&handler_boundary); - return SUCCESS; -} - -int king_orchestrator_resume_run( - zend_string *run_id, - zval *return_value, - const char *function_name, - zend_bool throw_on_error -) -{ - zval initial_data; - zval pipeline; - zval options; - zval telemetry_parent_context; - king_orchestrator_exec_control_t control; - king_trace_context_t *boundary_span = NULL; - zend_bool boundary_seeded = 0; - int rc; - - if (run_id == NULL) { - return FAILURE; - } - - /* Each resumed run must start without stale pre-flush span/log residue. */ - king_telemetry_cleanup_scope_state(); - - ZVAL_NULL(&initial_data); - ZVAL_NULL(&pipeline); - ZVAL_NULL(&options); - ZVAL_NULL(&telemetry_parent_context); - ZVAL_UNDEF(&control.cancel_token); - control.run_id = NULL; - - if ( - king_orchestrator_load_run_payload( - run_id, - &initial_data, - &pipeline, - &options, - &telemetry_parent_context - ) != SUCCESS - ) { - char message[KING_ERR_LEN]; - - snprintf( - message, - sizeof(message), - "%s() could not load the persisted run payload.", - function_name != NULL ? function_name : "king_pipeline_orchestrator_resume_run" - ); - return king_orchestrator_raise_error( - message, - king_ce_runtime_exception, - throw_on_error - ); - } - - if (king_orchestrator_exec_control_parse( - &options, - function_name, - throw_on_error, - &control - ) != SUCCESS) { - zval_ptr_dtor(&initial_data); - zval_ptr_dtor(&pipeline); - zval_ptr_dtor(&options); - zval_ptr_dtor(&telemetry_parent_context); - king_orchestrator_exec_control_cleanup(&control); - return FAILURE; - } - - if ( - king_orchestrator_distributed_tracing_enabled() - && Z_TYPE(telemetry_parent_context) == IS_ARRAY - && king_telemetry_set_incoming_parent_context_from_array(&telemetry_parent_context) == SUCCESS - ) { - boundary_seeded = 1; - (void) king_orchestrator_begin_boundary_span(run_id, function_name, &boundary_span); - } - - if (king_orchestrator_backend_is_remote_peer()) { - rc = king_orchestrator_execute_remote_run( - run_id, - &initial_data, - &pipeline, - &options, - return_value, - &control, - function_name, - throw_on_error - ); - } else { - rc = king_orchestrator_execute_existing_run( - run_id, - &initial_data, - &pipeline, - return_value, - &control, - function_name, - throw_on_error - ); - } - - king_orchestrator_finish_boundary_span(boundary_span, rc); - if (boundary_seeded) { - king_telemetry_clear_incoming_parent_context(); - } - - zval_ptr_dtor(&initial_data); - zval_ptr_dtor(&pipeline); - zval_ptr_dtor(&options); - zval_ptr_dtor(&telemetry_parent_context); - king_orchestrator_exec_control_cleanup(&control); - - return rc; -} - -int king_orchestrator_run(zval *initial_data, zval *pipeline_array, zval *options, zval *return_value) -{ - zend_string *run_id; - zval sanitized_options; - zval telemetry_parent_context; - zval *persisted_options; - king_orchestrator_exec_control_t control; - zend_bool boundary_seeded = 0; - int rc = FAILURE; - - ZVAL_UNDEF(&sanitized_options); - ZVAL_NULL(&telemetry_parent_context); - ZVAL_UNDEF(&control.cancel_token); - control.run_id = NULL; - - /* - * Capture the current caller span before the fresh-run cleanup so resumed - * work can rehydrate the original parent trace relationship. - */ - king_orchestrator_capture_distributed_parent_context(&telemetry_parent_context); - - /* A new orchestrator execution starts a fresh telemetry scope. */ - king_telemetry_cleanup_scope_state(); - - if (king_orchestrator_backend_is_file_worker()) { - return king_orchestrator_raise_error( - "king_pipeline_orchestrator_run() is unavailable when orchestrator_execution_backend=file_worker; use king_pipeline_orchestrator_dispatch().", - king_ce_runtime_exception, - 1 - ); - } - - if ( - king_orchestrator_backend_is_remote_peer() - && king_system_require_admission( - "king_pipeline_orchestrator_run", - "remote_peer_dispatches" - ) != SUCCESS - ) { - return king_orchestrator_raise_error( - king_get_error(), - king_ce_runtime_exception, - 1 - ); - } - - if ( - king_orchestrator_executes_userland_handlers_locally() - && king_system_require_admission( - "king_pipeline_orchestrator_run", - "orchestrator_submissions" - ) != SUCCESS - ) { - return king_orchestrator_raise_error( - king_get_error(), - king_ce_runtime_exception, - 1 - ); - } - - if (king_orchestrator_exec_control_parse( - options, - "king_pipeline_orchestrator_run", - 1, - &control - ) != SUCCESS) { - goto cleanup; - } - - if (king_orchestrator_exec_control_check(&control, "king_pipeline_orchestrator_run", 1, NULL) != SUCCESS) { - goto cleanup; - } - - if (king_orchestrator_enforce_max_concurrency(&control, "king_pipeline_orchestrator_run", 1) != SUCCESS) { - goto cleanup; - } - - persisted_options = king_orchestrator_prepare_persisted_options(options, &sanitized_options); - run_id = king_orchestrator_pipeline_run_begin( - initial_data, - pipeline_array, - persisted_options, - &telemetry_parent_context, - "running" - ); - if (run_id == NULL) { - rc = king_orchestrator_raise_error( - "king_pipeline_orchestrator_run() failed to persist the initial run snapshot.", - king_ce_runtime_exception, - 1 - ); - goto cleanup; - } - - if ( - king_orchestrator_distributed_tracing_enabled() - && Z_TYPE(telemetry_parent_context) == IS_ARRAY - && king_telemetry_set_incoming_parent_context_from_array(&telemetry_parent_context) == SUCCESS - ) { - boundary_seeded = 1; - } - - if (king_orchestrator_backend_is_remote_peer()) { - rc = king_orchestrator_execute_remote_run( - run_id, - initial_data, - pipeline_array, - persisted_options, - return_value, - &control, - "king_pipeline_orchestrator_run", - 1 - ); - } else { - rc = king_orchestrator_execute_existing_run( - run_id, - initial_data, - pipeline_array, - return_value, - &control, - "king_pipeline_orchestrator_run", - 1 - ); - } - if (boundary_seeded) { - king_telemetry_clear_incoming_parent_context(); - } - zend_string_release(run_id); -cleanup: - if (!Z_ISUNDEF(sanitized_options)) { - zval_ptr_dtor(&sanitized_options); - } - zval_ptr_dtor(&telemetry_parent_context); - king_orchestrator_exec_control_cleanup(&control); - return rc; -} - -int king_orchestrator_dispatch(zval *initial_data, zval *pipeline_array, zval *options, zval *return_value) -{ - zend_string *run_id; - zval sanitized_options; - zval telemetry_parent_context; - zval *persisted_options; - king_orchestrator_exec_control_t control; - int rc = FAILURE; - - ZVAL_UNDEF(&sanitized_options); - ZVAL_NULL(&telemetry_parent_context); - ZVAL_UNDEF(&control.cancel_token); - control.run_id = NULL; - - if (!king_orchestrator_backend_is_file_worker()) { - return king_orchestrator_raise_error( - "king_pipeline_orchestrator_dispatch() requires orchestrator_execution_backend=file_worker.", - king_ce_runtime_exception, - 1 - ); - } - - if ( - king_mcp_orchestrator_config.orchestrator_worker_queue_path == NULL - || king_mcp_orchestrator_config.orchestrator_worker_queue_path[0] == '\0' - ) { - return king_orchestrator_raise_error( - "king_pipeline_orchestrator_dispatch() requires a non-empty orchestrator_worker_queue_path.", - king_ce_runtime_exception, - 1 - ); - } - - if (king_orchestrator_exec_control_parse( - options, - "king_pipeline_orchestrator_dispatch", - 1, - &control - ) != SUCCESS) { - goto cleanup; - } - - if (!Z_ISUNDEF(control.cancel_token)) { - rc = king_orchestrator_raise_error( - "king_pipeline_orchestrator_dispatch() does not support live CancelToken propagation on the file_worker backend.", - king_ce_runtime_exception, - 1 - ); - goto cleanup; - } - - if (king_orchestrator_exec_control_check(&control, "king_pipeline_orchestrator_dispatch", 1, NULL) != SUCCESS) { - goto cleanup; - } - - if (king_orchestrator_enforce_max_concurrency(&control, "king_pipeline_orchestrator_dispatch", 1) != SUCCESS) { - goto cleanup; - } - - king_orchestrator_capture_distributed_parent_context(&telemetry_parent_context); - persisted_options = king_orchestrator_prepare_persisted_options(options, &sanitized_options); - run_id = king_orchestrator_pipeline_run_begin( - initial_data, - pipeline_array, - persisted_options, - &telemetry_parent_context, - "queued" - ); - if (run_id == NULL) { - rc = king_orchestrator_raise_error( - "king_pipeline_orchestrator_dispatch() failed to persist the initial run snapshot.", - king_ce_runtime_exception, - 1 - ); - goto cleanup; - } - - rc = king_orchestrator_enqueue_run(run_id, return_value); - if (rc != SUCCESS) { - (void) king_orchestrator_pipeline_run_fail_classified( - run_id, - "king_pipeline_orchestrator_dispatch() failed to enqueue the run for the file-worker backend.", - "backend", - "caller_managed_retry", - -1, - "file_worker_queue" - ); - zend_string_release(run_id); - rc = king_orchestrator_raise_error( - "king_pipeline_orchestrator_dispatch() failed to enqueue the run for the file-worker backend.", - king_ce_runtime_exception, - 1 - ); - goto cleanup; - } - - zend_string_release(run_id); - rc = SUCCESS; -cleanup: - if (!Z_ISUNDEF(sanitized_options)) { - zval_ptr_dtor(&sanitized_options); - } - zval_ptr_dtor(&telemetry_parent_context); - king_orchestrator_exec_control_cleanup(&control); - return rc; -} - -static int king_orchestrator_worker_release_unadmitted_claim( - zend_string *run_id, - char *claimed_path, - int *claimed_fd, - zend_bool recovered_claim -) -{ - char queued_path[1024]; - int rc = SUCCESS; - - if (!recovered_claim) { - if ( - run_id == NULL - || !king_orchestrator_run_id_is_valid(run_id) - || claimed_path == NULL - || claimed_path[0] == '\0' - || king_mcp_orchestrator_config.orchestrator_worker_queue_path == NULL - || king_mcp_orchestrator_config.orchestrator_worker_queue_path[0] == '\0' - || snprintf( - queued_path, - sizeof(queued_path), - "%s/queued-%s.job", - king_mcp_orchestrator_config.orchestrator_worker_queue_path, - ZSTR_VAL(run_id) - ) >= (int) sizeof(queued_path) - || rename(claimed_path, queued_path) != 0 - ) { - rc = FAILURE; - } else { - claimed_path[0] = '\0'; - } - } - - if (claimed_fd != NULL && *claimed_fd >= 0) { - close(*claimed_fd); - *claimed_fd = -1; - } - - return rc; -} - -int king_orchestrator_worker_run_next(zval *return_value) -{ - zend_string *run_id = NULL; - char claimed_path[1024]; - int claimed_fd = -1; - zend_bool recovered_claim = 0; - int rc; - - if (!king_orchestrator_backend_is_file_worker()) { - return king_orchestrator_raise_error( - "king_pipeline_orchestrator_worker_run_next() requires orchestrator_execution_backend=file_worker.", - king_ce_runtime_exception, - 1 - ); - } - - if ( - king_mcp_orchestrator_config.orchestrator_worker_queue_path == NULL - || king_mcp_orchestrator_config.orchestrator_worker_queue_path[0] == '\0' - ) { - return king_orchestrator_raise_error( - "king_pipeline_orchestrator_worker_run_next() requires a non-empty orchestrator_worker_queue_path.", - king_ce_runtime_exception, - 1 - ); - } - - if ( - king_orchestrator_claim_next_run( - &run_id, - claimed_path, - sizeof(claimed_path), - &claimed_fd, - &recovered_claim - ) != SUCCESS - ) { - return king_orchestrator_raise_error( - "king_pipeline_orchestrator_worker_run_next() could not claim a queued run.", - king_ce_runtime_exception, - 1 - ); - } - - if (run_id == NULL) { - if (claimed_fd >= 0) { - close(claimed_fd); - } - ZVAL_FALSE(return_value); - return SUCCESS; - } - - if (king_orchestrator_pipeline_run_is_terminal(run_id)) { - if (claimed_path[0] != '\0') { - unlink(claimed_path); - } - if (claimed_fd >= 0) { - close(claimed_fd); - } - if (king_orchestrator_get_run_snapshot(run_id, return_value) != SUCCESS) { - zend_string_release(run_id); - return king_orchestrator_raise_error( - "king_pipeline_orchestrator_worker_run_next() could not read back the persisted terminal run snapshot.", - king_ce_runtime_exception, - 1 - ); - } - zend_string_release(run_id); - return SUCCESS; - } - - if ( - king_system_require_admission( - "king_pipeline_orchestrator_worker_run_next", - recovered_claim ? "file_worker_resumes" : "file_worker_claims" - ) != SUCCESS - ) { - if ( - king_orchestrator_worker_release_unadmitted_claim( + ); + if (failure_identity != NULL) { + zend_string_release(failure_identity); + } + zval_ptr_dtor(return_value); + ZVAL_FALSE(return_value); + king_set_error("king_pipeline_orchestrator_run() failed to persist completed step progress."); + (void) king_orchestrator_pipeline_run_fail_classified( run_id, - claimed_path, - &claimed_fd, - recovered_claim - ) != SUCCESS - ) { - zend_string_release(run_id); + king_get_error(), + "backend", + "caller_managed_retry", + -1, + "controller" + ); + if (attempt_identity != NULL) { + zend_string_release(attempt_identity); + } + zval_ptr_dtor(&handler_boundary); return king_orchestrator_raise_error( - "king_pipeline_orchestrator_worker_run_next() could not restore the unadmitted file-worker claim after readiness gating.", + king_get_error(), king_ce_runtime_exception, - 1 + throw_on_error ); } - zend_string_release(run_id); - return king_orchestrator_raise_error( - king_get_error(), - king_ce_runtime_exception, - 1 + king_orchestrator_finish_pipeline_span( + step_span, + "completed", + NULL, + NULL, + NULL ); - } + index++; + } ZEND_HASH_FOREACH_END(); - if (king_orchestrator_pipeline_run_mark_running(run_id, recovered_claim, (zend_long) getpid()) != SUCCESS) { - if (claimed_path[0] != '\0') { - unlink(claimed_path); + cancelled = 0; + if (king_orchestrator_exec_control_check(control, function_name, throw_on_error, &cancelled) != SUCCESS) { + zend_string *failure_identity = NULL; + const char *category = cancelled ? "cancelled" : "backend"; + const char *retry_disposition = cancelled ? "not_applicable" : "caller_managed_retry"; + + if (!cancelled + && EG(exception) != NULL + && instanceof_function(EG(exception)->ce, king_ce_timeout_exception)) { + category = "timeout"; } - if (claimed_fd >= 0) { - close(claimed_fd); + if (attempt_identity != NULL) { + failure_identity = king_orchestrator_build_failure_identity( + run_id, + attempt_identity, + category, + -1, + cancelled ? "cancelled" : "failed" + ); } - zend_string_release(run_id); - return king_orchestrator_raise_error( - "king_pipeline_orchestrator_worker_run_next() could not persist the claimed running snapshot.", - king_ce_runtime_exception, - 1 + king_orchestrator_finish_pipeline_span( + run_span, + cancelled ? "cancelled" : "failed", + category, + retry_disposition, + failure_identity ); - } - - ZVAL_NULL(return_value); - rc = king_orchestrator_resume_run( - run_id, - return_value, - "king_pipeline_orchestrator_worker_run_next", - 1 - ); - zval_ptr_dtor(return_value); - ZVAL_NULL(return_value); - - if (claimed_path[0] != '\0') { - unlink(claimed_path); - } - if (claimed_fd >= 0) { - close(claimed_fd); - claimed_fd = -1; - } - - if (rc != SUCCESS) { - if (EG(exception) != NULL) { - zval cancelled_snapshot; - zval *status; - const char *error_message = king_get_error(); - - if ( - king_orchestrator_run_cancel_requested(run_id) - || ( - error_message != NULL - && strstr(error_message, "persisted file-worker cancel channel") != NULL - ) - || king_orchestrator_exception_message_contains( - "persisted file-worker cancel channel" - ) - ) { - (void) king_orchestrator_pipeline_run_cancelled_classified( - run_id, - error_message, - "cancelled", - "not_applicable", - -1, - "file_worker" - ); - } - - ZVAL_NULL(&cancelled_snapshot); - if (king_orchestrator_get_run_snapshot(run_id, &cancelled_snapshot) == SUCCESS) { - status = zend_hash_str_find( - Z_ARRVAL(cancelled_snapshot), - "status", - sizeof("status") - 1 - ); - if ( - status != NULL - && Z_TYPE_P(status) == IS_STRING - && zend_string_equals_literal(Z_STR_P(status), "cancelled") - ) { - zend_clear_exception(); - zend_string_release(run_id); - ZVAL_COPY_VALUE(return_value, &cancelled_snapshot); - return SUCCESS; - } - zval_ptr_dtor(&cancelled_snapshot); - } - if (!king_orchestrator_pipeline_run_is_terminal(run_id)) { - (void) king_orchestrator_pipeline_run_fail_classified( - run_id, - error_message, - "backend", - "caller_managed_retry", - -1, - "file_worker" - ); - } - zend_string_release(run_id); - return FAILURE; - } - zend_string_release(run_id); - return king_orchestrator_raise_error( - "king_pipeline_orchestrator_worker_run_next() failed while executing the claimed run.", - king_ce_runtime_exception, - 1 + king_orchestrator_emit_pipeline_failure_log( + run_id, + attempt_identity, + NULL, + -1, + execution_backend, + category, + retry_disposition, + king_get_error(), + cancelled ? "cancelled" : "failed" ); - } - - if (king_orchestrator_get_run_snapshot(run_id, return_value) != SUCCESS) { - zend_string_release(run_id); - return king_orchestrator_raise_error( - "king_pipeline_orchestrator_worker_run_next() could not read back the persisted run snapshot.", - king_ce_runtime_exception, - 1 + king_orchestrator_record_pipeline_counter( + "pipeline.failure.count", + execution_backend, + topology_scope, + cancelled ? "cancelled" : "failed", + category, + retry_disposition ); - } - - zend_string_release(run_id); - return SUCCESS; -} - -PHP_FUNCTION(king_pipeline_orchestrator_run) -{ - zval *initial_data; - zval *pipeline; - zval *exec_options = NULL; - - ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_ZVAL(initial_data) - Z_PARAM_ARRAY(pipeline) - Z_PARAM_OPTIONAL - Z_PARAM_ARRAY_OR_NULL(exec_options) - ZEND_PARSE_PARAMETERS_END(); - - if (king_orchestrator_run(initial_data, pipeline, exec_options, return_value) == SUCCESS) { - return; - } - - RETURN_THROWS(); -} - -PHP_FUNCTION(king_pipeline_orchestrator_run_async) -{ - zval *initial_data; - zval *pipeline; - zval *exec_options = NULL; - zval params[3]; - uint32_t index; - - ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_ZVAL(initial_data) - Z_PARAM_ARRAY(pipeline) - Z_PARAM_OPTIONAL - Z_PARAM_ARRAY_OR_NULL(exec_options) - ZEND_PARSE_PARAMETERS_END(); - - ZVAL_COPY(¶ms[0], initial_data); - ZVAL_COPY(¶ms[1], pipeline); - if (exec_options != NULL) { - ZVAL_COPY(¶ms[2], exec_options); - } else { - ZVAL_NULL(¶ms[2]); - } - - if (king_awaitable_create_function_call( - return_value, - "king_pipeline_orchestrator_run_async", - sizeof("king_pipeline_orchestrator_run_async") - 1, - "king_pipeline_orchestrator_run", - sizeof("king_pipeline_orchestrator_run") - 1, - params, - 3, - NULL - ) != SUCCESS) { - for (index = 0; index < 3; index++) { - zval_ptr_dtor(¶ms[index]); + if (failure_identity != NULL) { + zend_string_release(failure_identity); } - RETURN_FALSE; - } - - for (index = 0; index < 3; index++) { - zval_ptr_dtor(¶ms[index]); - } -} - -PHP_FUNCTION(king_pipeline_orchestrator_dispatch) -{ - zval *initial_data; - zval *pipeline; - zval *exec_options = NULL; - - ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_ZVAL(initial_data) - Z_PARAM_ARRAY(pipeline) - Z_PARAM_OPTIONAL - Z_PARAM_ARRAY_OR_NULL(exec_options) - ZEND_PARSE_PARAMETERS_END(); - - if (king_orchestrator_dispatch(initial_data, pipeline, exec_options, return_value) == SUCCESS) { - return; - } - - RETURN_THROWS(); -} - -PHP_FUNCTION(king_pipeline_orchestrator_dispatch_async) -{ - zval *initial_data; - zval *pipeline; - zval *exec_options = NULL; - zval params[3]; - uint32_t index; - - ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_ZVAL(initial_data) - Z_PARAM_ARRAY(pipeline) - Z_PARAM_OPTIONAL - Z_PARAM_ARRAY_OR_NULL(exec_options) - ZEND_PARSE_PARAMETERS_END(); - - ZVAL_COPY(¶ms[0], initial_data); - ZVAL_COPY(¶ms[1], pipeline); - if (exec_options != NULL) { - ZVAL_COPY(¶ms[2], exec_options); - } else { - ZVAL_NULL(¶ms[2]); - } - - if (king_awaitable_create_function_call( - return_value, - "king_pipeline_orchestrator_dispatch_async", - sizeof("king_pipeline_orchestrator_dispatch_async") - 1, - "king_pipeline_orchestrator_dispatch", - sizeof("king_pipeline_orchestrator_dispatch") - 1, - params, - 3, - NULL - ) != SUCCESS) { - for (index = 0; index < 3; index++) { - zval_ptr_dtor(¶ms[index]); + if (use_registered_tool_handlers) { + zval_ptr_dtor(¤t_payload); + } else { + zval_ptr_dtor(return_value); + ZVAL_FALSE(return_value); } - RETURN_FALSE; - } - - for (index = 0; index < 3; index++) { - zval_ptr_dtor(¶ms[index]); - } -} - -PHP_FUNCTION(king_pipeline_orchestrator_worker_run_next) -{ - ZEND_PARSE_PARAMETERS_NONE(); - - if (king_orchestrator_worker_run_next(return_value) == SUCCESS) { - return; - } - - RETURN_THROWS(); -} - -PHP_FUNCTION(king_pipeline_orchestrator_resume_run) -{ - zend_string *run_id; - zval run_snapshot; - zval *status; - char message[KING_ERR_LEN]; - - ZEND_PARSE_PARAMETERS_START(1, 1) - Z_PARAM_STR(run_id) - ZEND_PARSE_PARAMETERS_END(); - - if (ZSTR_LEN(run_id) == 0) { - king_set_error("king_pipeline_orchestrator_resume_run() requires a non-empty run id."); - zend_throw_exception_ex( - king_ce_validation_exception, - 0, - "king_pipeline_orchestrator_resume_run() requires a non-empty run id." - ); - RETURN_THROWS(); + king_orchestrator_mark_interrupted_run(run_id, cancelled, -1, NULL); + if (attempt_identity != NULL) { + zend_string_release(attempt_identity); + } + zval_ptr_dtor(&handler_boundary); + return FAILURE; } - if (king_orchestrator_backend_is_file_worker()) { - king_set_error( - "king_pipeline_orchestrator_resume_run() is unavailable when orchestrator_execution_backend=file_worker; use king_pipeline_orchestrator_worker_run_next()." - ); - zend_throw_exception_ex( - king_ce_runtime_exception, - 0, - "king_pipeline_orchestrator_resume_run() is unavailable when orchestrator_execution_backend=file_worker; use king_pipeline_orchestrator_worker_run_next()." - ); - RETURN_THROWS(); + if (use_registered_tool_handlers) { + ZVAL_COPY(return_value, ¤t_payload); + zval_ptr_dtor(¤t_payload); } - ZVAL_NULL(&run_snapshot); - if (king_orchestrator_get_run_snapshot(run_id, &run_snapshot) != SUCCESS) { - king_set_error("king_pipeline_orchestrator_resume_run() could not read the persisted run snapshot."); - zend_throw_exception_ex( - king_ce_runtime_exception, - 0, - "king_pipeline_orchestrator_resume_run() could not read the persisted run snapshot." - ); - RETURN_THROWS(); - } + if (king_orchestrator_pipeline_run_complete(run_id, return_value) != SUCCESS) { + zend_string *failure_identity = NULL; - status = zend_hash_str_find( - Z_ARRVAL(run_snapshot), - "status", - sizeof("status") - 1 - ); - if (status == NULL || Z_TYPE_P(status) != IS_STRING) { - zval_ptr_dtor(&run_snapshot); - king_set_error("king_pipeline_orchestrator_resume_run() requires a persisted run snapshot with a valid status."); - zend_throw_exception_ex( - king_ce_runtime_exception, - 0, - "king_pipeline_orchestrator_resume_run() requires a persisted run snapshot with a valid status." + if (attempt_identity != NULL) { + failure_identity = king_orchestrator_build_failure_identity( + run_id, + attempt_identity, + "backend", + -1, + "failed" + ); + } + king_orchestrator_finish_pipeline_span( + run_span, + "failed", + "backend", + "caller_managed_retry", + failure_identity ); - RETURN_THROWS(); - } - - if (!zend_string_equals_literal(Z_STR_P(status), "running")) { - snprintf( - message, - sizeof(message), - "king_pipeline_orchestrator_resume_run() can only continue runs in 'running' state; '%s' is terminal or not resumable.", - Z_STRVAL_P(status) + king_orchestrator_emit_pipeline_failure_log( + run_id, + attempt_identity, + NULL, + -1, + execution_backend, + "backend", + "caller_managed_retry", + "king_pipeline_orchestrator_run() failed to persist the completed run snapshot.", + "failed" ); - zval_ptr_dtor(&run_snapshot); - king_set_error(message); - zend_throw_exception_ex(king_ce_runtime_exception, 0, "%s", message); - RETURN_THROWS(); - } - - zval_ptr_dtor(&run_snapshot); - - if ( - king_orchestrator_backend_is_remote_peer() - && king_system_require_admission( - "king_pipeline_orchestrator_resume_run", - "remote_peer_resumes" - ) != SUCCESS - ) { - zend_throw_exception_ex( - king_ce_runtime_exception, - 0, - "%s", - king_get_error() + king_orchestrator_record_pipeline_counter( + "pipeline.failure.count", + execution_backend, + topology_scope, + "failed", + "backend", + "caller_managed_retry" ); - RETURN_THROWS(); - } - - if (king_orchestrator_pipeline_run_note_recovery(run_id, "resume_run") != SUCCESS) { - king_set_error("king_pipeline_orchestrator_resume_run() failed to persist recovery observability."); - zend_throw_exception_ex( + if (failure_identity != NULL) { + zend_string_release(failure_identity); + } + zval_ptr_dtor(return_value); + ZVAL_FALSE(return_value); + if (attempt_identity != NULL) { + zend_string_release(attempt_identity); + } + zval_ptr_dtor(&handler_boundary); + return king_orchestrator_raise_error( + "king_pipeline_orchestrator_run() failed to persist the completed run snapshot.", king_ce_runtime_exception, - 0, - "king_pipeline_orchestrator_resume_run() failed to persist recovery observability." + throw_on_error ); - RETURN_THROWS(); } - if ( - king_orchestrator_resume_run( - run_id, - return_value, - "king_pipeline_orchestrator_resume_run", - 1 - ) == SUCCESS - ) { - return; + king_orchestrator_finish_pipeline_span(run_span, "completed", NULL, NULL, NULL); + if (attempt_identity != NULL) { + zend_string_release(attempt_identity); } - - RETURN_THROWS(); + zval_ptr_dtor(&handler_boundary); + return SUCCESS; } -PHP_FUNCTION(king_pipeline_orchestrator_configure_logging) -{ - zval *config; - - ZEND_PARSE_PARAMETERS_START(1, 1) - Z_PARAM_ARRAY(config) - ZEND_PARSE_PARAMETERS_END(); +#include "runtime_api/submission_api.inc" - if (king_orchestrator_configure_logging(config) == SUCCESS) { - RETURN_TRUE; - } +#include "runtime_api/worker_runtime.inc" - king_set_error("king_pipeline_orchestrator_configure_logging() failed to persist the logging snapshot."); - zend_throw_exception_ex( - king_ce_runtime_exception, - 0, - "king_pipeline_orchestrator_configure_logging() failed to persist the logging snapshot." - ); - RETURN_THROWS(); -} +#include "runtime_api/procedural_api.inc" diff --git a/extension/src/pipeline_orchestrator/orchestrator/runtime_api/handler_invocation.inc b/extension/src/pipeline_orchestrator/orchestrator/runtime_api/handler_invocation.inc new file mode 100644 index 000000000..c8f01d898 --- /dev/null +++ b/extension/src/pipeline_orchestrator/orchestrator/runtime_api/handler_invocation.inc @@ -0,0 +1,505 @@ +static zend_bool king_orchestrator_executes_userland_handlers_locally(void) +{ + return !king_orchestrator_backend_is_file_worker() && !king_orchestrator_backend_is_remote_peer(); +} + +static zend_bool king_orchestrator_handler_boundary_requires_process_registration( + zval *handler_boundary +) +{ + zval *requires_process_registration; + + if (handler_boundary == NULL || Z_TYPE_P(handler_boundary) != IS_ARRAY) { + return 0; + } + + requires_process_registration = zend_hash_str_find( + Z_ARRVAL_P(handler_boundary), + "requires_process_registration", + sizeof("requires_process_registration") - 1 + ); + + return requires_process_registration != NULL && zend_is_true(requires_process_registration); +} + +static zend_bool king_orchestrator_handler_boundary_step_uses_registered_handler( + zval *handler_boundary, + zval *step, + zend_long step_index +) +{ + zval *required_step_refs; + zval *tool; + zval *entry; + + if ( + handler_boundary == NULL + || Z_TYPE_P(handler_boundary) != IS_ARRAY + || step == NULL + || Z_TYPE_P(step) != IS_ARRAY + ) { + return 0; + } + + tool = zend_hash_str_find(Z_ARRVAL_P(step), "tool", sizeof("tool") - 1); + if (tool == NULL || Z_TYPE_P(tool) != IS_STRING || Z_STRLEN_P(tool) == 0) { + return 0; + } + + required_step_refs = zend_hash_str_find( + Z_ARRVAL_P(handler_boundary), + "required_step_refs", + sizeof("required_step_refs") - 1 + ); + if (required_step_refs == NULL || Z_TYPE_P(required_step_refs) != IS_ARRAY) { + return 0; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(required_step_refs), entry) { + zval *entry_index; + zval *tool_name; + + if (entry == NULL || Z_TYPE_P(entry) != IS_ARRAY) { + continue; + } + + entry_index = zend_hash_str_find(Z_ARRVAL_P(entry), "index", sizeof("index") - 1); + tool_name = zend_hash_str_find( + Z_ARRVAL_P(entry), + "tool_name", + sizeof("tool_name") - 1 + ); + if ( + entry_index == NULL + || Z_TYPE_P(entry_index) != IS_LONG + || Z_LVAL_P(entry_index) != step_index + || tool_name == NULL + || Z_TYPE_P(tool_name) != IS_STRING + || !zend_string_equals(Z_STR_P(tool_name), Z_STR_P(tool)) + ) { + continue; + } + + return 1; + } ZEND_HASH_FOREACH_END(); + + return 0; +} + +static const char *king_orchestrator_failure_retry_disposition_for_category( + const char *category +) +{ + if (category == NULL || category[0] == '\0') { + return "caller_managed_retry"; + } + + if (strcmp(category, "validation") == 0) { + return "non_retryable"; + } + + if (strcmp(category, "cancelled") == 0) { + return "not_applicable"; + } + + return "caller_managed_retry"; +} + +static const char *king_orchestrator_exception_message_or_last_error(void) +{ + zval rv; + zval *message; + const char *last_error = king_get_error(); + + if (EG(exception) == NULL) { + return last_error; + } + + message = zend_read_property( + zend_ce_exception, + EG(exception), + "message", + sizeof("message") - 1, + 1, + &rv + ); + if (message == NULL || Z_TYPE_P(message) != IS_STRING || Z_STRLEN_P(message) == 0) { + return last_error; + } + + king_set_error(Z_STRVAL_P(message)); + return king_get_error(); +} + +static void king_orchestrator_classify_handler_throwable_failure( + const char **category_out, + const char **retry_disposition_out +) +{ + const char *category = "runtime"; + + if ( + EG(exception) != NULL + && instanceof_function(EG(exception)->ce, king_ce_validation_exception) + ) { + category = "validation"; + } else if ( + EG(exception) != NULL + && instanceof_function(EG(exception)->ce, king_ce_timeout_exception) + ) { + category = "timeout"; + } + + if (category_out != NULL) { + *category_out = category; + } + if (retry_disposition_out != NULL) { + *retry_disposition_out = king_orchestrator_failure_retry_disposition_for_category(category); + } +} + +static zend_result king_orchestrator_prepare_registered_handler_execution( + zend_string *run_id, + zval *initial_data, + zval *handler_boundary, + zval *current_payload, + zend_long *persisted_completed_step_count, + zend_bool *use_registered_tool_handlers, + zend_bool *use_handler_boundary, + zend_bool throw_on_error +) +{ + if ( + handler_boundary == NULL + || current_payload == NULL + || persisted_completed_step_count == NULL + || use_registered_tool_handlers == NULL + || use_handler_boundary == NULL + ) { + return king_orchestrator_raise_error( + "king_pipeline_orchestrator_run() could not prepare registered-handler execution state.", + king_ce_runtime_exception, + throw_on_error + ); + } + + if (king_orchestrator_backend_is_file_worker()) { + if (run_id == NULL || king_orchestrator_load_run_handler_boundary(run_id, handler_boundary) != SUCCESS) { + return king_orchestrator_raise_error( + "king_pipeline_orchestrator_run() could not load the persisted file-worker handler boundary.", + king_ce_runtime_exception, + throw_on_error + ); + } + + if (king_orchestrator_handler_boundary_requires_process_registration(handler_boundary)) { + *use_registered_tool_handlers = 1; + *use_handler_boundary = 1; + } + } + + if (*use_registered_tool_handlers) { + if ( + run_id == NULL + || king_orchestrator_load_run_progress( + run_id, + current_payload, + persisted_completed_step_count + ) != SUCCESS + ) { + return king_orchestrator_raise_error( + "king_pipeline_orchestrator_run() could not load persisted registered-handler progress.", + king_ce_runtime_exception, + throw_on_error + ); + } + + if (*persisted_completed_step_count <= 0) { + zval_ptr_dtor(current_payload); + ZVAL_COPY(current_payload, initial_data); + } + } + + return SUCCESS; +} + +static zend_result king_orchestrator_invoke_registered_tool_handler( + zend_string *run_id, + zval *step, + uint32_t step_index, + zval *current_payload, + zval *step_output, + const char *execution_backend, + const char *topology_scope, + zend_long attempt_number, + king_orchestrator_exec_control_t *control, + const char *function_name, + zend_bool throw_on_error, + const char **failure_category_out, + const char **retry_disposition_out +) +{ + zval *tool; + zval *handler; + zval context; + zval context_input; + zval tool_metadata; + zval tool_config; + zval run_metadata; + zval step_metadata; + zval step_definition; + zval cancel; + zval handler_result; + zval *result_output; + zend_fcall_info fci; + zend_fcall_info_cache fcc; + char message[KING_ERR_LEN]; + + if (failure_category_out != NULL) { + *failure_category_out = "backend"; + } + if (retry_disposition_out != NULL) { + *retry_disposition_out = "caller_managed_retry"; + } + + if ( + step == NULL + || Z_TYPE_P(step) != IS_ARRAY + || current_payload == NULL + || step_output == NULL + ) { + if (failure_category_out != NULL) { + *failure_category_out = "backend"; + } + return king_orchestrator_raise_error( + "king_pipeline_orchestrator_run() could not prepare the registered userland handler invocation.", + king_ce_runtime_exception, + throw_on_error + ); + } + + ZVAL_NULL(step_output); + tool = zend_hash_str_find(Z_ARRVAL_P(step), "tool", sizeof("tool") - 1); + if (tool == NULL || Z_TYPE_P(tool) != IS_STRING || Z_STRLEN_P(tool) == 0) { + if (failure_category_out != NULL) { + *failure_category_out = "validation"; + } + if (retry_disposition_out != NULL) { + *retry_disposition_out = "non_retryable"; + } + return king_orchestrator_raise_error( + "king_pipeline_orchestrator_run() step requires a non-empty tool name before registered handler execution.", + king_ce_validation_exception, + throw_on_error + ); + } + + handler = king_orchestrator_lookup_tool_handler(Z_STRVAL_P(tool), Z_STRLEN_P(tool)); + if (handler == NULL) { + snprintf( + message, + sizeof(message), + "%s() has no registered handler for tool '%s'.", + function_name != NULL ? function_name : "king_pipeline_orchestrator_run", + Z_STRVAL_P(tool) + ); + if (failure_category_out != NULL) { + *failure_category_out = "missing_handler"; + } + return king_orchestrator_raise_error( + message, + king_ce_runtime_exception, + throw_on_error + ); + } + + ZVAL_NULL(&tool_config); + if ( + king_orchestrator_load_tool_config( + Z_STRVAL_P(tool), + Z_STRLEN_P(tool), + &tool_config + ) != SUCCESS + ) { + snprintf( + message, + sizeof(message), + "%s() could not load the persisted tool config for registered handler '%s'.", + function_name != NULL ? function_name : "king_pipeline_orchestrator_run", + Z_STRVAL_P(tool) + ); + if (failure_category_out != NULL) { + *failure_category_out = "backend"; + } + return king_orchestrator_raise_error( + message, + king_ce_runtime_exception, + throw_on_error + ); + } + + if (zend_fcall_info_init(handler, 0, &fci, &fcc, NULL, NULL) != SUCCESS) { + zval_ptr_dtor(&tool_config); + snprintf( + message, + sizeof(message), + "%s() found an invalid registered handler for tool '%s'.", + function_name != NULL ? function_name : "king_pipeline_orchestrator_run", + Z_STRVAL_P(tool) + ); + if (failure_category_out != NULL) { + *failure_category_out = "missing_handler"; + } + return king_orchestrator_raise_error( + message, + king_ce_runtime_exception, + throw_on_error + ); + } + + array_init(&context); + ZVAL_COPY(&context_input, current_payload); + add_assoc_zval(&context, "input", &context_input); + + array_init(&tool_metadata); + add_assoc_stringl(&tool_metadata, "name", Z_STRVAL_P(tool), Z_STRLEN_P(tool)); + add_assoc_zval(&tool_metadata, "config", &tool_config); + add_assoc_zval(&context, "tool", &tool_metadata); + + array_init(&run_metadata); + if (run_id != NULL) { + add_assoc_stringl(&context, "run_id", ZSTR_VAL(run_id), ZSTR_LEN(run_id)); + add_assoc_stringl(&run_metadata, "run_id", ZSTR_VAL(run_id), ZSTR_LEN(run_id)); + } else { + add_assoc_null(&context, "run_id"); + add_assoc_null(&run_metadata, "run_id"); + } + add_assoc_long(&run_metadata, "attempt_number", attempt_number > 0 ? attempt_number : 1); + add_assoc_string( + &run_metadata, + "execution_backend", + (char *) (execution_backend != NULL ? execution_backend : "local") + ); + add_assoc_string( + &run_metadata, + "topology_scope", + (char *) (topology_scope != NULL ? topology_scope : "local_in_process") + ); + add_assoc_zval(&context, "run", &run_metadata); + + array_init(&step_metadata); + add_assoc_long(&step_metadata, "index", (zend_long) step_index); + add_assoc_stringl(&step_metadata, "tool_name", Z_STRVAL_P(tool), Z_STRLEN_P(tool)); + ZVAL_COPY(&step_definition, step); + add_assoc_zval(&step_metadata, "definition", &step_definition); + add_assoc_zval(&context, "step", &step_metadata); + + if (control != NULL && !Z_ISUNDEF(control->cancel_token)) { + ZVAL_COPY(&cancel, &control->cancel_token); + } else { + ZVAL_NULL(&cancel); + } + add_assoc_zval(&context, "cancel", &cancel); + + add_assoc_long( + &context, + "timeout_budget_ms", + king_orchestrator_control_timeout_budget_ms(control) + ); + add_assoc_long( + &context, + "deadline_budget_ms", + king_orchestrator_control_deadline_budget_ms(control) + ); + + ZVAL_NULL(&handler_result); + fci.retval = &handler_result; + fci.param_count = 1; + fci.params = &context; + + if (zend_call_function(&fci, &fcc) != SUCCESS) { + zval_ptr_dtor(&context); + if (EG(exception) == NULL) { + snprintf( + message, + sizeof(message), + "%s() failed while invoking the registered handler for tool '%s'.", + function_name != NULL ? function_name : "king_pipeline_orchestrator_run", + Z_STRVAL_P(tool) + ); + if (failure_category_out != NULL) { + *failure_category_out = "runtime"; + } + return king_orchestrator_raise_error( + message, + king_ce_runtime_exception, + throw_on_error + ); + } + king_orchestrator_classify_handler_throwable_failure( + failure_category_out, + retry_disposition_out + ); + (void) king_orchestrator_exception_message_or_last_error(); + return FAILURE; + } + + zval_ptr_dtor(&context); + if (EG(exception) != NULL) { + zval_ptr_dtor(&handler_result); + king_orchestrator_classify_handler_throwable_failure( + failure_category_out, + retry_disposition_out + ); + (void) king_orchestrator_exception_message_or_last_error(); + return FAILURE; + } + + if (Z_TYPE(handler_result) != IS_ARRAY) { + zval_ptr_dtor(&handler_result); + snprintf( + message, + sizeof(message), + "%s() registered handler for tool '%s' must return an array result contract.", + function_name != NULL ? function_name : "king_pipeline_orchestrator_run", + Z_STRVAL_P(tool) + ); + if (failure_category_out != NULL) { + *failure_category_out = "runtime"; + } + return king_orchestrator_raise_error( + message, + king_ce_runtime_exception, + throw_on_error + ); + } + + result_output = zend_hash_str_find( + Z_ARRVAL(handler_result), + "output", + sizeof("output") - 1 + ); + if (result_output == NULL || Z_TYPE_P(result_output) != IS_ARRAY) { + zval_ptr_dtor(&handler_result); + snprintf( + message, + sizeof(message), + "%s() registered handler for tool '%s' must return an array result contract with key 'output'.", + function_name != NULL ? function_name : "king_pipeline_orchestrator_run", + Z_STRVAL_P(tool) + ); + if (failure_category_out != NULL) { + *failure_category_out = "runtime"; + } + return king_orchestrator_raise_error( + message, + king_ce_runtime_exception, + throw_on_error + ); + } + + ZVAL_COPY(step_output, result_output); + zval_ptr_dtor(&handler_result); + + return SUCCESS; +} diff --git a/extension/src/pipeline_orchestrator/orchestrator/runtime_api/pipeline_observability.inc b/extension/src/pipeline_orchestrator/orchestrator/runtime_api/pipeline_observability.inc new file mode 100644 index 000000000..db1987559 --- /dev/null +++ b/extension/src/pipeline_orchestrator/orchestrator/runtime_api/pipeline_observability.inc @@ -0,0 +1,461 @@ +static zend_bool king_orchestrator_distributed_tracing_enabled(void) +{ + return king_mcp_orchestrator_config.orchestrator_enable_distributed_tracing ? 1 : 0; +} + +static const char *king_orchestrator_boundary_backend_label(void) +{ + if ( + king_mcp_orchestrator_config.orchestrator_execution_backend == NULL + || king_mcp_orchestrator_config.orchestrator_execution_backend[0] == '\0' + ) { + return "local"; + } + + return king_mcp_orchestrator_config.orchestrator_execution_backend; +} + +static void king_orchestrator_capture_distributed_parent_context(zval *destination) +{ + if (destination == NULL) { + return; + } + + ZVAL_NULL(destination); + if (!king_orchestrator_distributed_tracing_enabled()) { + return; + } + + if (!king_telemetry_build_distributed_parent_context(destination)) { + ZVAL_NULL(destination); + } +} + +static const char *king_orchestrator_boundary_origin_label(const char *function_name) +{ + if ( + function_name != NULL + && strcmp(function_name, "king_pipeline_orchestrator_worker_run_next") == 0 + ) { + return "file_worker"; + } + + return "process_resume"; +} + +static zend_result king_orchestrator_begin_boundary_span( + zend_string *run_id, + const char *function_name, + king_trace_context_t **span_out +) +{ + zval attributes; + zend_result rc; + + if (span_out == NULL) { + return FAILURE; + } + + *span_out = NULL; + if (!king_telemetry_has_incoming_parent_context()) { + return SUCCESS; + } + + array_init(&attributes); + add_assoc_string(&attributes, "component", "pipeline_orchestrator"); + add_assoc_string( + &attributes, + "boundary_origin", + (char *) king_orchestrator_boundary_origin_label(function_name) + ); + add_assoc_string( + &attributes, + "execution_backend", + (char *) king_orchestrator_boundary_backend_label() + ); + add_assoc_string(&attributes, "hierarchy_source", "distributed_parent_context"); + if (run_id != NULL) { + add_assoc_stringl(&attributes, "run_id", ZSTR_VAL(run_id), ZSTR_LEN(run_id)); + } + + rc = king_telemetry_start_internal_span( + span_out, + "pipeline-orchestrator-boundary", + KING_SPAN_KIND_CONSUMER, + &attributes, + NULL + ); + zval_ptr_dtor(&attributes); + + return rc; +} + +static void king_orchestrator_finish_boundary_span(king_trace_context_t *span, int rc) +{ + zval final_attributes; + + if (span == NULL) { + return; + } + + array_init(&final_attributes); + add_assoc_string(&final_attributes, "outcome", rc == SUCCESS ? "completed" : "failed"); + (void) king_telemetry_end_internal_span(span, &final_attributes); + zval_ptr_dtor(&final_attributes); +} + +static const char *king_orchestrator_runtime_topology_scope(void) +{ + if (king_orchestrator_backend_is_file_worker()) { + return "same_host_file_worker"; + } + + if (king_orchestrator_backend_is_remote_peer()) { + return "tcp_host_port_execution_peer"; + } + + return "local_in_process"; +} + +static zval *king_orchestrator_find_step_telemetry_id( + zval *step, + const char *field_name, + size_t field_name_len +) +{ + zval *value; + + if (step == NULL || Z_TYPE_P(step) != IS_ARRAY) { + return NULL; + } + + value = zend_hash_str_find(Z_ARRVAL_P(step), field_name, field_name_len); + if (value == NULL || Z_TYPE_P(value) != IS_STRING || Z_STRLEN_P(value) == 0) { + return NULL; + } + + return value; +} + +static zend_long king_orchestrator_run_attempt_number(zend_string *run_id) +{ + zval snapshot; + zval *observability; + zval *recovery_count; + zend_long attempt_number = 1; + + if (run_id == NULL) { + return 1; + } + + ZVAL_NULL(&snapshot); + if (king_orchestrator_get_run_snapshot(run_id, &snapshot) != SUCCESS) { + return 1; + } + + observability = zend_hash_str_find( + Z_ARRVAL(snapshot), + "distributed_observability", + sizeof("distributed_observability") - 1 + ); + if (observability != NULL && Z_TYPE_P(observability) == IS_ARRAY) { + recovery_count = zend_hash_str_find( + Z_ARRVAL_P(observability), + "recovery_count", + sizeof("recovery_count") - 1 + ); + if (recovery_count != NULL && Z_TYPE_P(recovery_count) == IS_LONG && Z_LVAL_P(recovery_count) >= 0) { + attempt_number = Z_LVAL_P(recovery_count) + 1; + } + } + + zval_ptr_dtor(&snapshot); + return attempt_number > 0 ? attempt_number : 1; +} + +static zend_string *king_orchestrator_build_attempt_identity(zend_string *run_id) +{ + return strpprintf( + 0, + "%s:attempt-" ZEND_LONG_FMT, + run_id != NULL ? ZSTR_VAL(run_id) : "run-unknown", + king_orchestrator_run_attempt_number(run_id) + ); +} + +static zend_string *king_orchestrator_build_failure_identity( + zend_string *run_id, + zend_string *attempt_identity, + const char *category, + zend_long step_index, + const char *final_status +) +{ + return strpprintf( + 0, + "%s:%s:%s:" ZEND_LONG_FMT, + attempt_identity != NULL + ? ZSTR_VAL(attempt_identity) + : (run_id != NULL ? ZSTR_VAL(run_id) : "run-unknown"), + final_status != NULL && final_status[0] != '\0' ? final_status : "failed", + category != NULL && category[0] != '\0' ? category : "backend", + step_index + ); +} + +static void king_orchestrator_record_pipeline_counter( + const char *metric_name, + const char *execution_backend, + const char *topology_scope, + const char *outcome, + const char *category, + const char *retry_disposition +) +{ + zval labels; + + if (!king_telemetry_is_capture_enabled() || metric_name == NULL || metric_name[0] == '\0') { + return; + } + + array_init(&labels); + add_assoc_string(&labels, "component", "pipeline_orchestrator"); + add_assoc_string(&labels, "execution_backend", (char *) execution_backend); + add_assoc_string(&labels, "topology_scope", (char *) topology_scope); + if (outcome != NULL && outcome[0] != '\0') { + add_assoc_string(&labels, "outcome", (char *) outcome); + } + if (category != NULL && category[0] != '\0') { + add_assoc_string(&labels, "category", (char *) category); + } + if (retry_disposition != NULL && retry_disposition[0] != '\0') { + add_assoc_string(&labels, "retry_disposition", (char *) retry_disposition); + } + (void) king_telemetry_record_metric_internal(metric_name, KING_METRIC_TYPE_COUNTER, 1.0, &labels); + zval_ptr_dtor(&labels); +} + +static void king_orchestrator_emit_pipeline_failure_log( + zend_string *run_id, + zend_string *attempt_identity, + zval *step, + zend_long step_index, + const char *execution_backend, + const char *category, + const char *retry_disposition, + const char *message, + const char *final_status +) +{ + zval attributes; + zval *partition_id; + zval *batch_id; + zend_string *failure_identity; + + if (!king_telemetry_is_capture_enabled()) { + return; + } + + partition_id = king_orchestrator_find_step_telemetry_id(step, "partition_id", sizeof("partition_id") - 1); + batch_id = king_orchestrator_find_step_telemetry_id(step, "batch_id", sizeof("batch_id") - 1); + failure_identity = king_orchestrator_build_failure_identity( + run_id, + attempt_identity, + category, + step_index, + final_status + ); + + array_init(&attributes); + add_assoc_string(&attributes, "component", "pipeline_orchestrator"); + add_assoc_stringl(&attributes, "run_id", ZSTR_VAL(run_id), ZSTR_LEN(run_id)); + if (attempt_identity != NULL) { + add_assoc_stringl( + &attributes, + "attempt_identity", + ZSTR_VAL(attempt_identity), + ZSTR_LEN(attempt_identity) + ); + } else { + add_assoc_null(&attributes, "attempt_identity"); + } + add_assoc_long(&attributes, "step_index", step_index); + add_assoc_string(&attributes, "execution_backend", (char *) execution_backend); + add_assoc_string(&attributes, "topology_scope", (char *) king_orchestrator_runtime_topology_scope()); + add_assoc_string(&attributes, "failure_status", (char *) final_status); + add_assoc_string(&attributes, "failure_category", (char *) category); + add_assoc_string(&attributes, "retry_disposition", (char *) retry_disposition); + add_assoc_str(&attributes, "failure_identity", failure_identity); + if (partition_id != NULL) { + add_assoc_stringl(&attributes, "partition_id", Z_STRVAL_P(partition_id), Z_STRLEN_P(partition_id)); + } + if (batch_id != NULL) { + add_assoc_stringl(&attributes, "batch_id", Z_STRVAL_P(batch_id), Z_STRLEN_P(batch_id)); + } + + (void) king_telemetry_log_internal( + strcmp(final_status, "cancelled") == 0 ? KING_TELEMETRY_LEVEL_WARN : KING_TELEMETRY_LEVEL_ERROR, + "pipeline_orchestrator", + message != NULL && message[0] != '\0' ? message : "pipeline run failed", + &attributes + ); + zval_ptr_dtor(&attributes); +} + +static void king_orchestrator_append_pipeline_span_attributes( + zval *attributes, + zend_string *run_id, + zend_string *attempt_identity, + zval *step, + zend_long step_index +) +{ + zval *partition_id; + zval *batch_id; + zval *tool; + + if (attributes == NULL) { + return; + } + + add_assoc_string(attributes, "component", "pipeline_orchestrator"); + add_assoc_string(attributes, "telemetry_adapter_contract", "run_partition_batch_retry_failure_identity"); + add_assoc_stringl(attributes, "run_id", ZSTR_VAL(run_id), ZSTR_LEN(run_id)); + if (attempt_identity != NULL) { + add_assoc_stringl( + attributes, + "attempt_identity", + ZSTR_VAL(attempt_identity), + ZSTR_LEN(attempt_identity) + ); + } else { + add_assoc_null(attributes, "attempt_identity"); + } + add_assoc_string(attributes, "execution_backend", (char *) king_orchestrator_boundary_backend_label()); + add_assoc_string(attributes, "topology_scope", (char *) king_orchestrator_runtime_topology_scope()); + + if (step_index >= 0) { + add_assoc_long(attributes, "step_index", step_index); + } + + if (step != NULL && Z_TYPE_P(step) == IS_ARRAY) { + tool = zend_hash_str_find(Z_ARRVAL_P(step), "tool", sizeof("tool") - 1); + if (tool != NULL && Z_TYPE_P(tool) == IS_STRING && Z_STRLEN_P(tool) > 0) { + add_assoc_stringl(attributes, "step_tool", Z_STRVAL_P(tool), Z_STRLEN_P(tool)); + } + } + + partition_id = king_orchestrator_find_step_telemetry_id(step, "partition_id", sizeof("partition_id") - 1); + if (partition_id != NULL) { + add_assoc_stringl(attributes, "partition_id", Z_STRVAL_P(partition_id), Z_STRLEN_P(partition_id)); + } + + batch_id = king_orchestrator_find_step_telemetry_id(step, "batch_id", sizeof("batch_id") - 1); + if (batch_id != NULL) { + add_assoc_stringl(attributes, "batch_id", Z_STRVAL_P(batch_id), Z_STRLEN_P(batch_id)); + } +} + +static void king_orchestrator_finish_pipeline_span( + king_trace_context_t *span, + const char *outcome, + const char *category, + const char *retry_disposition, + zend_string *failure_identity +) +{ + zval final_attributes; + + if (span == NULL) { + return; + } + + array_init(&final_attributes); + add_assoc_string( + &final_attributes, + "outcome", + (char *) ( + outcome != NULL && outcome[0] != '\0' + ? outcome + : "completed" + ) + ); + if (category != NULL && category[0] != '\0') { + add_assoc_string(&final_attributes, "failure_category", (char *) category); + } + if (retry_disposition != NULL && retry_disposition[0] != '\0') { + add_assoc_string(&final_attributes, "retry_disposition", (char *) retry_disposition); + } + if (failure_identity != NULL) { + add_assoc_stringl( + &final_attributes, + "failure_identity", + ZSTR_VAL(failure_identity), + ZSTR_LEN(failure_identity) + ); + } + + (void) king_telemetry_end_internal_span(span, &final_attributes); + zval_ptr_dtor(&final_attributes); +} + +static void king_orchestrator_begin_pipeline_run_span( + king_trace_context_t **run_span, + zend_string *run_id, + zend_string *attempt_identity, + zend_long attempt_number, + const char *execution_backend, + const char *topology_scope +) +{ + zval run_attributes; + + if (run_span == NULL || !king_telemetry_is_capture_enabled() || attempt_identity == NULL) { + return; + } + + array_init(&run_attributes); + king_orchestrator_append_pipeline_span_attributes( + &run_attributes, + run_id, + attempt_identity, + NULL, + -1 + ); + add_assoc_string(&run_attributes, "identity_scope", "run"); + if (attempt_number > 1) { + add_assoc_stringl( + &run_attributes, + "retry_identity", + ZSTR_VAL(attempt_identity), + ZSTR_LEN(attempt_identity) + ); + } + (void) king_telemetry_start_internal_span( + run_span, + "pipeline-orchestrator-run", + KING_SPAN_KIND_CONSUMER, + &run_attributes, + NULL + ); + zval_ptr_dtor(&run_attributes); + + king_orchestrator_record_pipeline_counter( + "pipeline.run.count", + execution_backend, + topology_scope, + "attempt", + NULL, + NULL + ); + if (attempt_number > 1) { + king_orchestrator_record_pipeline_counter( + "pipeline.retry.count", + execution_backend, + topology_scope, + "recovered_attempt", + NULL, + "caller_managed_retry" + ); + } +} diff --git a/extension/src/pipeline_orchestrator/orchestrator/runtime_api/procedural_api.inc b/extension/src/pipeline_orchestrator/orchestrator/runtime_api/procedural_api.inc new file mode 100644 index 000000000..46a3ffe2f --- /dev/null +++ b/extension/src/pipeline_orchestrator/orchestrator/runtime_api/procedural_api.inc @@ -0,0 +1,274 @@ +PHP_FUNCTION(king_pipeline_orchestrator_run) +{ + zval *initial_data; + zval *pipeline; + zval *exec_options = NULL; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_ZVAL(initial_data) + Z_PARAM_ARRAY(pipeline) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(exec_options) + ZEND_PARSE_PARAMETERS_END(); + + if (king_orchestrator_run(initial_data, pipeline, exec_options, return_value) == SUCCESS) { + return; + } + + RETURN_THROWS(); +} + +PHP_FUNCTION(king_pipeline_orchestrator_run_async) +{ + zval *initial_data; + zval *pipeline; + zval *exec_options = NULL; + zval params[3]; + uint32_t index; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_ZVAL(initial_data) + Z_PARAM_ARRAY(pipeline) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(exec_options) + ZEND_PARSE_PARAMETERS_END(); + + ZVAL_COPY(¶ms[0], initial_data); + ZVAL_COPY(¶ms[1], pipeline); + if (exec_options != NULL) { + ZVAL_COPY(¶ms[2], exec_options); + } else { + ZVAL_NULL(¶ms[2]); + } + + if (king_awaitable_create_function_call( + return_value, + "king_pipeline_orchestrator_run_async", + sizeof("king_pipeline_orchestrator_run_async") - 1, + "king_pipeline_orchestrator_run", + sizeof("king_pipeline_orchestrator_run") - 1, + params, + 3, + NULL + ) != SUCCESS) { + for (index = 0; index < 3; index++) { + zval_ptr_dtor(¶ms[index]); + } + RETURN_FALSE; + } + + for (index = 0; index < 3; index++) { + zval_ptr_dtor(¶ms[index]); + } +} + +PHP_FUNCTION(king_pipeline_orchestrator_dispatch) +{ + zval *initial_data; + zval *pipeline; + zval *exec_options = NULL; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_ZVAL(initial_data) + Z_PARAM_ARRAY(pipeline) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(exec_options) + ZEND_PARSE_PARAMETERS_END(); + + if (king_orchestrator_dispatch(initial_data, pipeline, exec_options, return_value) == SUCCESS) { + return; + } + + RETURN_THROWS(); +} + +PHP_FUNCTION(king_pipeline_orchestrator_dispatch_async) +{ + zval *initial_data; + zval *pipeline; + zval *exec_options = NULL; + zval params[3]; + uint32_t index; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_ZVAL(initial_data) + Z_PARAM_ARRAY(pipeline) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(exec_options) + ZEND_PARSE_PARAMETERS_END(); + + ZVAL_COPY(¶ms[0], initial_data); + ZVAL_COPY(¶ms[1], pipeline); + if (exec_options != NULL) { + ZVAL_COPY(¶ms[2], exec_options); + } else { + ZVAL_NULL(¶ms[2]); + } + + if (king_awaitable_create_function_call( + return_value, + "king_pipeline_orchestrator_dispatch_async", + sizeof("king_pipeline_orchestrator_dispatch_async") - 1, + "king_pipeline_orchestrator_dispatch", + sizeof("king_pipeline_orchestrator_dispatch") - 1, + params, + 3, + NULL + ) != SUCCESS) { + for (index = 0; index < 3; index++) { + zval_ptr_dtor(¶ms[index]); + } + RETURN_FALSE; + } + + for (index = 0; index < 3; index++) { + zval_ptr_dtor(¶ms[index]); + } +} + +PHP_FUNCTION(king_pipeline_orchestrator_worker_run_next) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + if (king_orchestrator_worker_run_next(return_value) == SUCCESS) { + return; + } + + RETURN_THROWS(); +} + +PHP_FUNCTION(king_pipeline_orchestrator_resume_run) +{ + zend_string *run_id; + zval run_snapshot; + zval *status; + char message[KING_ERR_LEN]; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(run_id) + ZEND_PARSE_PARAMETERS_END(); + + if (ZSTR_LEN(run_id) == 0) { + king_set_error("king_pipeline_orchestrator_resume_run() requires a non-empty run id."); + zend_throw_exception_ex( + king_ce_validation_exception, + 0, + "king_pipeline_orchestrator_resume_run() requires a non-empty run id." + ); + RETURN_THROWS(); + } + + if (king_orchestrator_backend_is_file_worker()) { + king_set_error( + "king_pipeline_orchestrator_resume_run() is unavailable when orchestrator_execution_backend=file_worker; use king_pipeline_orchestrator_worker_run_next()." + ); + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "king_pipeline_orchestrator_resume_run() is unavailable when orchestrator_execution_backend=file_worker; use king_pipeline_orchestrator_worker_run_next()." + ); + RETURN_THROWS(); + } + + ZVAL_NULL(&run_snapshot); + if (king_orchestrator_get_run_snapshot(run_id, &run_snapshot) != SUCCESS) { + king_set_error("king_pipeline_orchestrator_resume_run() could not read the persisted run snapshot."); + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "king_pipeline_orchestrator_resume_run() could not read the persisted run snapshot." + ); + RETURN_THROWS(); + } + + status = zend_hash_str_find( + Z_ARRVAL(run_snapshot), + "status", + sizeof("status") - 1 + ); + if (status == NULL || Z_TYPE_P(status) != IS_STRING) { + zval_ptr_dtor(&run_snapshot); + king_set_error("king_pipeline_orchestrator_resume_run() requires a persisted run snapshot with a valid status."); + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "king_pipeline_orchestrator_resume_run() requires a persisted run snapshot with a valid status." + ); + RETURN_THROWS(); + } + + if (!zend_string_equals_literal(Z_STR_P(status), "running")) { + snprintf( + message, + sizeof(message), + "king_pipeline_orchestrator_resume_run() can only continue runs in 'running' state; '%s' is terminal or not resumable.", + Z_STRVAL_P(status) + ); + zval_ptr_dtor(&run_snapshot); + king_set_error(message); + zend_throw_exception_ex(king_ce_runtime_exception, 0, "%s", message); + RETURN_THROWS(); + } + + zval_ptr_dtor(&run_snapshot); + + if ( + king_orchestrator_backend_is_remote_peer() + && king_system_require_admission( + "king_pipeline_orchestrator_resume_run", + "remote_peer_resumes" + ) != SUCCESS + ) { + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "%s", + king_get_error() + ); + RETURN_THROWS(); + } + + if (king_orchestrator_pipeline_run_note_recovery(run_id, "resume_run") != SUCCESS) { + king_set_error("king_pipeline_orchestrator_resume_run() failed to persist recovery observability."); + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "king_pipeline_orchestrator_resume_run() failed to persist recovery observability." + ); + RETURN_THROWS(); + } + + if ( + king_orchestrator_resume_run( + run_id, + return_value, + "king_pipeline_orchestrator_resume_run", + 1 + ) == SUCCESS + ) { + return; + } + + RETURN_THROWS(); +} + +PHP_FUNCTION(king_pipeline_orchestrator_configure_logging) +{ + zval *config; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY(config) + ZEND_PARSE_PARAMETERS_END(); + + if (king_orchestrator_configure_logging(config) == SUCCESS) { + RETURN_TRUE; + } + + king_set_error("king_pipeline_orchestrator_configure_logging() failed to persist the logging snapshot."); + zend_throw_exception_ex( + king_ce_runtime_exception, + 0, + "king_pipeline_orchestrator_configure_logging() failed to persist the logging snapshot." + ); + RETURN_THROWS(); +} diff --git a/extension/src/pipeline_orchestrator/orchestrator/runtime_api/submission_api.inc b/extension/src/pipeline_orchestrator/orchestrator/runtime_api/submission_api.inc new file mode 100644 index 000000000..fc656935d --- /dev/null +++ b/extension/src/pipeline_orchestrator/orchestrator/runtime_api/submission_api.inc @@ -0,0 +1,357 @@ +int king_orchestrator_resume_run( + zend_string *run_id, + zval *return_value, + const char *function_name, + zend_bool throw_on_error +) +{ + zval initial_data; + zval pipeline; + zval options; + zval telemetry_parent_context; + king_orchestrator_exec_control_t control; + king_trace_context_t *boundary_span = NULL; + zend_bool boundary_seeded = 0; + int rc; + + if (run_id == NULL) { + return FAILURE; + } + + /* Each resumed run must start without stale pre-flush span/log residue. */ + king_telemetry_cleanup_scope_state(); + + ZVAL_NULL(&initial_data); + ZVAL_NULL(&pipeline); + ZVAL_NULL(&options); + ZVAL_NULL(&telemetry_parent_context); + ZVAL_UNDEF(&control.cancel_token); + control.run_id = NULL; + + if ( + king_orchestrator_load_run_payload( + run_id, + &initial_data, + &pipeline, + &options, + &telemetry_parent_context + ) != SUCCESS + ) { + char message[KING_ERR_LEN]; + + snprintf( + message, + sizeof(message), + "%s() could not load the persisted run payload.", + function_name != NULL ? function_name : "king_pipeline_orchestrator_resume_run" + ); + return king_orchestrator_raise_error( + message, + king_ce_runtime_exception, + throw_on_error + ); + } + + if (king_orchestrator_exec_control_parse( + &options, + function_name, + throw_on_error, + &control + ) != SUCCESS) { + zval_ptr_dtor(&initial_data); + zval_ptr_dtor(&pipeline); + zval_ptr_dtor(&options); + zval_ptr_dtor(&telemetry_parent_context); + king_orchestrator_exec_control_cleanup(&control); + return FAILURE; + } + + if ( + king_orchestrator_distributed_tracing_enabled() + && Z_TYPE(telemetry_parent_context) == IS_ARRAY + && king_telemetry_set_incoming_parent_context_from_array(&telemetry_parent_context) == SUCCESS + ) { + boundary_seeded = 1; + (void) king_orchestrator_begin_boundary_span(run_id, function_name, &boundary_span); + } + + if (king_orchestrator_backend_is_remote_peer()) { + rc = king_orchestrator_execute_remote_run( + run_id, + &initial_data, + &pipeline, + &options, + return_value, + &control, + function_name, + throw_on_error + ); + } else { + rc = king_orchestrator_execute_existing_run( + run_id, + &initial_data, + &pipeline, + return_value, + &control, + function_name, + throw_on_error + ); + } + + king_orchestrator_finish_boundary_span(boundary_span, rc); + if (boundary_seeded) { + king_telemetry_clear_incoming_parent_context(); + } + + zval_ptr_dtor(&initial_data); + zval_ptr_dtor(&pipeline); + zval_ptr_dtor(&options); + zval_ptr_dtor(&telemetry_parent_context); + king_orchestrator_exec_control_cleanup(&control); + + return rc; +} + +int king_orchestrator_run(zval *initial_data, zval *pipeline_array, zval *options, zval *return_value) +{ + zend_string *run_id; + zval sanitized_options; + zval telemetry_parent_context; + zval *persisted_options; + king_orchestrator_exec_control_t control; + zend_bool boundary_seeded = 0; + int rc = FAILURE; + + ZVAL_UNDEF(&sanitized_options); + ZVAL_NULL(&telemetry_parent_context); + ZVAL_UNDEF(&control.cancel_token); + control.run_id = NULL; + + /* + * Capture the current caller span before the fresh-run cleanup so resumed + * work can rehydrate the original parent trace relationship. + */ + king_orchestrator_capture_distributed_parent_context(&telemetry_parent_context); + + /* A new orchestrator execution starts a fresh telemetry scope. */ + king_telemetry_cleanup_scope_state(); + + if (king_orchestrator_backend_is_file_worker()) { + return king_orchestrator_raise_error( + "king_pipeline_orchestrator_run() is unavailable when orchestrator_execution_backend=file_worker; use king_pipeline_orchestrator_dispatch().", + king_ce_runtime_exception, + 1 + ); + } + + if ( + king_orchestrator_backend_is_remote_peer() + && king_system_require_admission( + "king_pipeline_orchestrator_run", + "remote_peer_dispatches" + ) != SUCCESS + ) { + return king_orchestrator_raise_error( + king_get_error(), + king_ce_runtime_exception, + 1 + ); + } + + if ( + king_orchestrator_executes_userland_handlers_locally() + && king_system_require_admission( + "king_pipeline_orchestrator_run", + "orchestrator_submissions" + ) != SUCCESS + ) { + return king_orchestrator_raise_error( + king_get_error(), + king_ce_runtime_exception, + 1 + ); + } + + if (king_orchestrator_exec_control_parse( + options, + "king_pipeline_orchestrator_run", + 1, + &control + ) != SUCCESS) { + goto cleanup; + } + + if (king_orchestrator_exec_control_check(&control, "king_pipeline_orchestrator_run", 1, NULL) != SUCCESS) { + goto cleanup; + } + + if (king_orchestrator_enforce_max_concurrency(&control, "king_pipeline_orchestrator_run", 1) != SUCCESS) { + goto cleanup; + } + + persisted_options = king_orchestrator_prepare_persisted_options(options, &sanitized_options); + run_id = king_orchestrator_pipeline_run_begin( + initial_data, + pipeline_array, + persisted_options, + &telemetry_parent_context, + "running" + ); + if (run_id == NULL) { + rc = king_orchestrator_raise_error( + "king_pipeline_orchestrator_run() failed to persist the initial run snapshot.", + king_ce_runtime_exception, + 1 + ); + goto cleanup; + } + + if ( + king_orchestrator_distributed_tracing_enabled() + && Z_TYPE(telemetry_parent_context) == IS_ARRAY + && king_telemetry_set_incoming_parent_context_from_array(&telemetry_parent_context) == SUCCESS + ) { + boundary_seeded = 1; + } + + if (king_orchestrator_backend_is_remote_peer()) { + rc = king_orchestrator_execute_remote_run( + run_id, + initial_data, + pipeline_array, + persisted_options, + return_value, + &control, + "king_pipeline_orchestrator_run", + 1 + ); + } else { + rc = king_orchestrator_execute_existing_run( + run_id, + initial_data, + pipeline_array, + return_value, + &control, + "king_pipeline_orchestrator_run", + 1 + ); + } + if (boundary_seeded) { + king_telemetry_clear_incoming_parent_context(); + } + zend_string_release(run_id); +cleanup: + if (!Z_ISUNDEF(sanitized_options)) { + zval_ptr_dtor(&sanitized_options); + } + zval_ptr_dtor(&telemetry_parent_context); + king_orchestrator_exec_control_cleanup(&control); + return rc; +} + +int king_orchestrator_dispatch(zval *initial_data, zval *pipeline_array, zval *options, zval *return_value) +{ + zend_string *run_id; + zval sanitized_options; + zval telemetry_parent_context; + zval *persisted_options; + king_orchestrator_exec_control_t control; + int rc = FAILURE; + + ZVAL_UNDEF(&sanitized_options); + ZVAL_NULL(&telemetry_parent_context); + ZVAL_UNDEF(&control.cancel_token); + control.run_id = NULL; + + if (!king_orchestrator_backend_is_file_worker()) { + return king_orchestrator_raise_error( + "king_pipeline_orchestrator_dispatch() requires orchestrator_execution_backend=file_worker.", + king_ce_runtime_exception, + 1 + ); + } + + if ( + king_mcp_orchestrator_config.orchestrator_worker_queue_path == NULL + || king_mcp_orchestrator_config.orchestrator_worker_queue_path[0] == '\0' + ) { + return king_orchestrator_raise_error( + "king_pipeline_orchestrator_dispatch() requires a non-empty orchestrator_worker_queue_path.", + king_ce_runtime_exception, + 1 + ); + } + + if (king_orchestrator_exec_control_parse( + options, + "king_pipeline_orchestrator_dispatch", + 1, + &control + ) != SUCCESS) { + goto cleanup; + } + + if (!Z_ISUNDEF(control.cancel_token)) { + rc = king_orchestrator_raise_error( + "king_pipeline_orchestrator_dispatch() does not support live CancelToken propagation on the file_worker backend.", + king_ce_runtime_exception, + 1 + ); + goto cleanup; + } + + if (king_orchestrator_exec_control_check(&control, "king_pipeline_orchestrator_dispatch", 1, NULL) != SUCCESS) { + goto cleanup; + } + + if (king_orchestrator_enforce_max_concurrency(&control, "king_pipeline_orchestrator_dispatch", 1) != SUCCESS) { + goto cleanup; + } + + king_orchestrator_capture_distributed_parent_context(&telemetry_parent_context); + persisted_options = king_orchestrator_prepare_persisted_options(options, &sanitized_options); + run_id = king_orchestrator_pipeline_run_begin( + initial_data, + pipeline_array, + persisted_options, + &telemetry_parent_context, + "queued" + ); + if (run_id == NULL) { + rc = king_orchestrator_raise_error( + "king_pipeline_orchestrator_dispatch() failed to persist the initial run snapshot.", + king_ce_runtime_exception, + 1 + ); + goto cleanup; + } + + rc = king_orchestrator_enqueue_run(run_id, return_value); + if (rc != SUCCESS) { + (void) king_orchestrator_pipeline_run_fail_classified( + run_id, + "king_pipeline_orchestrator_dispatch() failed to enqueue the run for the file-worker backend.", + "backend", + "caller_managed_retry", + -1, + "file_worker_queue" + ); + zend_string_release(run_id); + rc = king_orchestrator_raise_error( + "king_pipeline_orchestrator_dispatch() failed to enqueue the run for the file-worker backend.", + king_ce_runtime_exception, + 1 + ); + goto cleanup; + } + + zend_string_release(run_id); + rc = SUCCESS; +cleanup: + if (!Z_ISUNDEF(sanitized_options)) { + zval_ptr_dtor(&sanitized_options); + } + zval_ptr_dtor(&telemetry_parent_context); + king_orchestrator_exec_control_cleanup(&control); + return rc; +} diff --git a/extension/src/pipeline_orchestrator/orchestrator/runtime_api/worker_runtime.inc b/extension/src/pipeline_orchestrator/orchestrator/runtime_api/worker_runtime.inc new file mode 100644 index 000000000..d4cf61697 --- /dev/null +++ b/extension/src/pipeline_orchestrator/orchestrator/runtime_api/worker_runtime.inc @@ -0,0 +1,251 @@ +static int king_orchestrator_worker_release_unadmitted_claim( + zend_string *run_id, + char *claimed_path, + int *claimed_fd, + zend_bool recovered_claim +) +{ + char queued_path[1024]; + int rc = SUCCESS; + + if (!recovered_claim) { + if ( + run_id == NULL + || !king_orchestrator_run_id_is_valid(run_id) + || claimed_path == NULL + || claimed_path[0] == '\0' + || king_mcp_orchestrator_config.orchestrator_worker_queue_path == NULL + || king_mcp_orchestrator_config.orchestrator_worker_queue_path[0] == '\0' + || snprintf( + queued_path, + sizeof(queued_path), + "%s/queued-%s.job", + king_mcp_orchestrator_config.orchestrator_worker_queue_path, + ZSTR_VAL(run_id) + ) >= (int) sizeof(queued_path) + || rename(claimed_path, queued_path) != 0 + ) { + rc = FAILURE; + } else { + claimed_path[0] = '\0'; + } + } + + if (claimed_fd != NULL && *claimed_fd >= 0) { + close(*claimed_fd); + *claimed_fd = -1; + } + + return rc; +} + +int king_orchestrator_worker_run_next(zval *return_value) +{ + zend_string *run_id = NULL; + char claimed_path[1024]; + int claimed_fd = -1; + zend_bool recovered_claim = 0; + int rc; + + if (!king_orchestrator_backend_is_file_worker()) { + return king_orchestrator_raise_error( + "king_pipeline_orchestrator_worker_run_next() requires orchestrator_execution_backend=file_worker.", + king_ce_runtime_exception, + 1 + ); + } + + if ( + king_mcp_orchestrator_config.orchestrator_worker_queue_path == NULL + || king_mcp_orchestrator_config.orchestrator_worker_queue_path[0] == '\0' + ) { + return king_orchestrator_raise_error( + "king_pipeline_orchestrator_worker_run_next() requires a non-empty orchestrator_worker_queue_path.", + king_ce_runtime_exception, + 1 + ); + } + + if ( + king_orchestrator_claim_next_run( + &run_id, + claimed_path, + sizeof(claimed_path), + &claimed_fd, + &recovered_claim + ) != SUCCESS + ) { + return king_orchestrator_raise_error( + "king_pipeline_orchestrator_worker_run_next() could not claim a queued run.", + king_ce_runtime_exception, + 1 + ); + } + + if (run_id == NULL) { + if (claimed_fd >= 0) { + close(claimed_fd); + } + ZVAL_FALSE(return_value); + return SUCCESS; + } + + if (king_orchestrator_pipeline_run_is_terminal(run_id)) { + if (claimed_path[0] != '\0') { + unlink(claimed_path); + } + if (claimed_fd >= 0) { + close(claimed_fd); + } + if (king_orchestrator_get_run_snapshot(run_id, return_value) != SUCCESS) { + zend_string_release(run_id); + return king_orchestrator_raise_error( + "king_pipeline_orchestrator_worker_run_next() could not read back the persisted terminal run snapshot.", + king_ce_runtime_exception, + 1 + ); + } + zend_string_release(run_id); + return SUCCESS; + } + + if ( + king_system_require_admission( + "king_pipeline_orchestrator_worker_run_next", + recovered_claim ? "file_worker_resumes" : "file_worker_claims" + ) != SUCCESS + ) { + if ( + king_orchestrator_worker_release_unadmitted_claim( + run_id, + claimed_path, + &claimed_fd, + recovered_claim + ) != SUCCESS + ) { + zend_string_release(run_id); + return king_orchestrator_raise_error( + "king_pipeline_orchestrator_worker_run_next() could not restore the unadmitted file-worker claim after readiness gating.", + king_ce_runtime_exception, + 1 + ); + } + zend_string_release(run_id); + return king_orchestrator_raise_error( + king_get_error(), + king_ce_runtime_exception, + 1 + ); + } + + if (king_orchestrator_pipeline_run_mark_running(run_id, recovered_claim, (zend_long) getpid()) != SUCCESS) { + if (claimed_path[0] != '\0') { + unlink(claimed_path); + } + if (claimed_fd >= 0) { + close(claimed_fd); + } + zend_string_release(run_id); + return king_orchestrator_raise_error( + "king_pipeline_orchestrator_worker_run_next() could not persist the claimed running snapshot.", + king_ce_runtime_exception, + 1 + ); + } + + ZVAL_NULL(return_value); + rc = king_orchestrator_resume_run( + run_id, + return_value, + "king_pipeline_orchestrator_worker_run_next", + 1 + ); + zval_ptr_dtor(return_value); + ZVAL_NULL(return_value); + + if (claimed_path[0] != '\0') { + unlink(claimed_path); + } + if (claimed_fd >= 0) { + close(claimed_fd); + claimed_fd = -1; + } + + if (rc != SUCCESS) { + if (EG(exception) != NULL) { + zval cancelled_snapshot; + zval *status; + const char *error_message = king_get_error(); + + if ( + king_orchestrator_run_cancel_requested(run_id) + || ( + error_message != NULL + && strstr(error_message, "persisted file-worker cancel channel") != NULL + ) + || king_orchestrator_exception_message_contains( + "persisted file-worker cancel channel" + ) + ) { + (void) king_orchestrator_pipeline_run_cancelled_classified( + run_id, + error_message, + "cancelled", + "not_applicable", + -1, + "file_worker" + ); + } + + ZVAL_NULL(&cancelled_snapshot); + if (king_orchestrator_get_run_snapshot(run_id, &cancelled_snapshot) == SUCCESS) { + status = zend_hash_str_find( + Z_ARRVAL(cancelled_snapshot), + "status", + sizeof("status") - 1 + ); + if ( + status != NULL + && Z_TYPE_P(status) == IS_STRING + && zend_string_equals_literal(Z_STR_P(status), "cancelled") + ) { + zend_clear_exception(); + zend_string_release(run_id); + ZVAL_COPY_VALUE(return_value, &cancelled_snapshot); + return SUCCESS; + } + zval_ptr_dtor(&cancelled_snapshot); + } + if (!king_orchestrator_pipeline_run_is_terminal(run_id)) { + (void) king_orchestrator_pipeline_run_fail_classified( + run_id, + error_message, + "backend", + "caller_managed_retry", + -1, + "file_worker" + ); + } + zend_string_release(run_id); + return FAILURE; + } + zend_string_release(run_id); + return king_orchestrator_raise_error( + "king_pipeline_orchestrator_worker_run_next() failed while executing the claimed run.", + king_ce_runtime_exception, + 1 + ); + } + + if (king_orchestrator_get_run_snapshot(run_id, return_value) != SUCCESS) { + zend_string_release(run_id); + return king_orchestrator_raise_error( + "king_pipeline_orchestrator_worker_run_next() could not read back the persisted run snapshot.", + king_ce_runtime_exception, + 1 + ); + } + + zend_string_release(run_id); + return SUCCESS; +} diff --git a/extension/src/pipeline_orchestrator/registration.inc b/extension/src/pipeline_orchestrator/registration.inc new file mode 100644 index 000000000..6ae4db7cb --- /dev/null +++ b/extension/src/pipeline_orchestrator/registration.inc @@ -0,0 +1,16 @@ +/* + * Pipeline-orchestrator registration aggregation. The orchestrator runtime + * translation unit includes this module boundary beside the runner runtime. + */ + +#include "pipeline_orchestrator/class_method_entries.h" + +void king_pipeline_orchestrator_register_classes(void) +{ + king_ce_pipeline_orchestrator = king_register_class_with_flags( + "King\\PipelineOrchestrator", + NULL, + king_pipeline_orchestrator_class_methods, + ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES + ); +} diff --git a/extension/src/pipeline_orchestrator/state.inc b/extension/src/pipeline_orchestrator/state.inc new file mode 100644 index 000000000..74bf8c7ff --- /dev/null +++ b/extension/src/pipeline_orchestrator/state.inc @@ -0,0 +1,6 @@ +/* + * Pipeline-orchestrator module state storage aggregation. The orchestrator + * runtime translation unit owns this class state beside the runner runtime. + */ + +#include "class_entries.inc" diff --git a/extension/src/pipeline_orchestrator/tool_registry.c b/extension/src/pipeline_orchestrator/tool_registry.c index 76aa9ea91..fecfcfbeb 100644 --- a/extension/src/pipeline_orchestrator/tool_registry.c +++ b/extension/src/pipeline_orchestrator/tool_registry.c @@ -4,8 +4,8 @@ * that reload orchestrator state after restart. */ #include "php_king.h" -#include "include/config/mcp_and_orchestrator/base_layer.h" -#include "include/pipeline_orchestrator/orchestrator.h" +#include "config/mcp_and_orchestrator/base_layer.h" +#include "pipeline_orchestrator/orchestrator.h" #include "ext/standard/base64.h" #include "ext/standard/php_var.h" diff --git a/extension/src/pipeline_orchestrator/tool_registry/queue_control.inc b/extension/src/pipeline_orchestrator/tool_registry/queue_control.inc index 4a869df56..a644ed841 100644 --- a/extension/src/pipeline_orchestrator/tool_registry/queue_control.inc +++ b/extension/src/pipeline_orchestrator/tool_registry/queue_control.inc @@ -1,243 +1,4 @@ -static void king_orchestrator_append_step_snapshots( - zval *target, - zval *pipeline, - const king_orchestrator_run_state_t *run_state -) -{ - zval steps; - zval *step; - zval *tool; - zval entry; - uint32_t index = 0; - const char *execution_backend = king_orchestrator_run_execution_backend(run_state); - const char *topology_scope = king_orchestrator_topology_scope_for_backend(execution_backend); - - if (target == NULL) { - return; - } - - array_init(&steps); - if (pipeline == NULL || Z_TYPE_P(pipeline) != IS_ARRAY) { - add_assoc_zval(target, "steps", &steps); - return; - } - - ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(pipeline), step) { - array_init(&entry); - add_assoc_long(&entry, "index", (zend_long) index); - - tool = NULL; - if (step != NULL && Z_TYPE_P(step) == IS_ARRAY) { - tool = zend_hash_str_find(Z_ARRVAL_P(step), "tool", sizeof("tool") - 1); - } - - if (tool != NULL && Z_TYPE_P(tool) == IS_STRING) { - add_assoc_stringl(&entry, "tool", Z_STRVAL_P(tool), Z_STRLEN_P(tool)); - } else { - add_assoc_null(&entry, "tool"); - } - - add_assoc_string( - &entry, - "status", - king_orchestrator_step_status_for_snapshot(run_state, (zend_long) index) - ); - add_assoc_string( - &entry, - "compensation_status", - king_orchestrator_step_compensation_status_for_snapshot(run_state, (zend_long) index) - ); - add_assoc_string(&entry, "execution_backend", (char *) execution_backend); - add_assoc_string(&entry, "topology_scope", (char *) topology_scope); - - if ( - run_state != NULL - && run_state->error_step_index == (zend_long) index - && ( - run_state->error_category != NULL - || run_state->retry_disposition != NULL - || run_state->error_backend != NULL - ) - ) { - king_orchestrator_append_error_classification(&entry, run_state, pipeline); - } else { - add_assoc_null(&entry, "error_classification"); - } - - if (king_orchestrator_find_pipeline_step_string( - pipeline, - (zend_long) index, - "partition_id", - sizeof("partition_id") - 1 - ) != NULL - || king_orchestrator_find_pipeline_step_string( - pipeline, - (zend_long) index, - "batch_id", - sizeof("batch_id") - 1 - ) != NULL) { - zval step_adapter; - zend_string *attempt_identity; - zend_string *failure_identity; - zval *partition_id; - zval *batch_id; - - array_init(&step_adapter); - attempt_identity = king_orchestrator_build_attempt_identity_for_snapshot(run_state); - add_assoc_stringl(&step_adapter, "attempt_identity", ZSTR_VAL(attempt_identity), ZSTR_LEN(attempt_identity)); - if (run_state != NULL && run_state->recovery_count > 0) { - add_assoc_stringl(&step_adapter, "retry_identity", ZSTR_VAL(attempt_identity), ZSTR_LEN(attempt_identity)); - } else { - add_assoc_null(&step_adapter, "retry_identity"); - } - zend_string_release(attempt_identity); - - partition_id = king_orchestrator_find_pipeline_step_string( - pipeline, - (zend_long) index, - "partition_id", - sizeof("partition_id") - 1 - ); - if (partition_id != NULL) { - add_assoc_stringl(&step_adapter, "partition_id", Z_STRVAL_P(partition_id), Z_STRLEN_P(partition_id)); - } else { - add_assoc_null(&step_adapter, "partition_id"); - } - - batch_id = king_orchestrator_find_pipeline_step_string( - pipeline, - (zend_long) index, - "batch_id", - sizeof("batch_id") - 1 - ); - if (batch_id != NULL) { - add_assoc_stringl(&step_adapter, "batch_id", Z_STRVAL_P(batch_id), Z_STRLEN_P(batch_id)); - } else { - add_assoc_null(&step_adapter, "batch_id"); - } - - if ( - run_state != NULL - && run_state->error_step_index == (zend_long) index - && run_state->status != NULL - && ( - zend_string_equals_literal(run_state->status, "failed") - || zend_string_equals_literal(run_state->status, "cancelled") - ) - ) { - failure_identity = king_orchestrator_build_failure_identity_for_snapshot(run_state); - if (failure_identity != NULL) { - add_assoc_stringl(&step_adapter, "failure_identity", ZSTR_VAL(failure_identity), ZSTR_LEN(failure_identity)); - zend_string_release(failure_identity); - } else { - add_assoc_null(&step_adapter, "failure_identity"); - } - } else { - add_assoc_null(&step_adapter, "failure_identity"); - } - - add_assoc_zval(&entry, "telemetry_adapter", &step_adapter); - } else { - add_assoc_null(&entry, "telemetry_adapter"); - } - - add_next_index_zval(&steps, &entry); - index++; - } ZEND_HASH_FOREACH_END(); - - add_assoc_zval(target, "steps", &steps); -} - -int king_orchestrator_get_run_snapshot(zend_string *run_id, zval *return_value) -{ - king_orchestrator_run_state_t *run_state; - zval initial_data; - zval pipeline; - zval options; - zval handler_boundary; - zval result; - zval error; - zval handler_readiness; - int lock_fd = -1; - - ZVAL_NULL(&initial_data); - ZVAL_NULL(&pipeline); - ZVAL_NULL(&options); - ZVAL_NULL(&handler_boundary); - ZVAL_NULL(&result); - ZVAL_NULL(&error); - ZVAL_NULL(&handler_readiness); - - if (king_orchestrator_state_transaction_begin(&lock_fd) != SUCCESS) { - return FAILURE; - } - - run_state = king_orchestrator_find_run(run_id); - if (run_state == NULL) { - king_orchestrator_state_lock_release(lock_fd); - return FAILURE; - } - - if ( - king_orchestrator_decode_base64_zval(run_state->initial_data_b64, &initial_data) != SUCCESS - || king_orchestrator_decode_base64_zval(run_state->pipeline_b64, &pipeline) != SUCCESS - || king_orchestrator_decode_base64_zval(run_state->options_b64, &options) != SUCCESS - || ( - run_state->handler_boundary_b64 != NULL - && king_orchestrator_decode_base64_zval(run_state->handler_boundary_b64, &handler_boundary) != SUCCESS - ) - || king_orchestrator_decode_base64_zval(run_state->result_b64, &result) != SUCCESS - || king_orchestrator_decode_base64_zval(run_state->error_b64, &error) != SUCCESS - ) { - zval_ptr_dtor(&initial_data); - zval_ptr_dtor(&pipeline); - zval_ptr_dtor(&options); - zval_ptr_dtor(&handler_boundary); - zval_ptr_dtor(&result); - zval_ptr_dtor(&error); - king_orchestrator_state_lock_release(lock_fd); - return FAILURE; - } - - array_init(return_value); - add_assoc_stringl(return_value, "run_id", ZSTR_VAL(run_state->run_id), ZSTR_LEN(run_state->run_id)); - add_assoc_stringl(return_value, "status", ZSTR_VAL(run_state->status), ZSTR_LEN(run_state->status)); - add_assoc_string(return_value, "execution_backend", (char *) king_orchestrator_run_execution_backend(run_state)); - add_assoc_string( - return_value, - "topology_scope", - (char *) king_orchestrator_topology_scope_for_backend(king_orchestrator_run_execution_backend(run_state)) - ); - add_assoc_string(return_value, "retry_policy", "single_attempt"); - add_assoc_string(return_value, "idempotency_policy", "caller_managed"); - add_assoc_string(return_value, "compensation_policy", "caller_managed"); - add_assoc_long(return_value, "started_at", (zend_long) run_state->started_at); - add_assoc_long(return_value, "finished_at", (zend_long) run_state->finished_at); - add_assoc_bool(return_value, "cancel_requested", run_state->cancel_requested ? 1 : 0); - add_assoc_long(return_value, "step_count", king_orchestrator_pipeline_step_count(&pipeline)); - add_assoc_long(return_value, "completed_step_count", run_state->completed_step_count); - add_assoc_zval(return_value, "initial_data", &initial_data); - add_assoc_zval(return_value, "pipeline", &pipeline); - add_assoc_zval(return_value, "options", &options); - add_assoc_zval(return_value, "handler_boundary", &handler_boundary); - king_orchestrator_append_handler_readiness( - Z_TYPE(handler_boundary) == IS_ARRAY - ? &handler_boundary - : NULL, - &handler_readiness - ); - add_assoc_zval(return_value, "handler_readiness", &handler_readiness); - add_assoc_zval(return_value, "result", &result); - add_assoc_zval(return_value, "error", &error); - king_orchestrator_append_error_classification(return_value, run_state, &pipeline); - king_orchestrator_append_run_telemetry_adapter_snapshot(return_value, &pipeline, run_state); - king_orchestrator_append_compensation_snapshot(return_value, &pipeline, run_state); - king_orchestrator_append_run_observability(return_value, run_state, &pipeline); - king_orchestrator_append_step_snapshots(return_value, &pipeline, run_state); - - king_orchestrator_state_lock_release(lock_fd); - return SUCCESS; -} +#include "queue_control/snapshot_api.inc" int king_orchestrator_enqueue_run(zend_string *run_id, zval *return_value) { diff --git a/extension/src/pipeline_orchestrator/tool_registry/queue_control/snapshot_api.inc b/extension/src/pipeline_orchestrator/tool_registry/queue_control/snapshot_api.inc new file mode 100644 index 000000000..e74d76a21 --- /dev/null +++ b/extension/src/pipeline_orchestrator/tool_registry/queue_control/snapshot_api.inc @@ -0,0 +1,240 @@ +static void king_orchestrator_append_step_snapshots( + zval *target, + zval *pipeline, + const king_orchestrator_run_state_t *run_state +) +{ + zval steps; + zval *step; + zval *tool; + zval entry; + uint32_t index = 0; + const char *execution_backend = king_orchestrator_run_execution_backend(run_state); + const char *topology_scope = king_orchestrator_topology_scope_for_backend(execution_backend); + + if (target == NULL) { + return; + } + + array_init(&steps); + if (pipeline == NULL || Z_TYPE_P(pipeline) != IS_ARRAY) { + add_assoc_zval(target, "steps", &steps); + return; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(pipeline), step) { + array_init(&entry); + add_assoc_long(&entry, "index", (zend_long) index); + + tool = NULL; + if (step != NULL && Z_TYPE_P(step) == IS_ARRAY) { + tool = zend_hash_str_find(Z_ARRVAL_P(step), "tool", sizeof("tool") - 1); + } + + if (tool != NULL && Z_TYPE_P(tool) == IS_STRING) { + add_assoc_stringl(&entry, "tool", Z_STRVAL_P(tool), Z_STRLEN_P(tool)); + } else { + add_assoc_null(&entry, "tool"); + } + + add_assoc_string( + &entry, + "status", + king_orchestrator_step_status_for_snapshot(run_state, (zend_long) index) + ); + add_assoc_string( + &entry, + "compensation_status", + king_orchestrator_step_compensation_status_for_snapshot(run_state, (zend_long) index) + ); + add_assoc_string(&entry, "execution_backend", (char *) execution_backend); + add_assoc_string(&entry, "topology_scope", (char *) topology_scope); + + if ( + run_state != NULL + && run_state->error_step_index == (zend_long) index + && ( + run_state->error_category != NULL + || run_state->retry_disposition != NULL + || run_state->error_backend != NULL + ) + ) { + king_orchestrator_append_error_classification(&entry, run_state, pipeline); + } else { + add_assoc_null(&entry, "error_classification"); + } + + if (king_orchestrator_find_pipeline_step_string( + pipeline, + (zend_long) index, + "partition_id", + sizeof("partition_id") - 1 + ) != NULL + || king_orchestrator_find_pipeline_step_string( + pipeline, + (zend_long) index, + "batch_id", + sizeof("batch_id") - 1 + ) != NULL) { + zval step_adapter; + zend_string *attempt_identity; + zend_string *failure_identity; + zval *partition_id; + zval *batch_id; + + array_init(&step_adapter); + attempt_identity = king_orchestrator_build_attempt_identity_for_snapshot(run_state); + add_assoc_stringl(&step_adapter, "attempt_identity", ZSTR_VAL(attempt_identity), ZSTR_LEN(attempt_identity)); + if (run_state != NULL && run_state->recovery_count > 0) { + add_assoc_stringl(&step_adapter, "retry_identity", ZSTR_VAL(attempt_identity), ZSTR_LEN(attempt_identity)); + } else { + add_assoc_null(&step_adapter, "retry_identity"); + } + zend_string_release(attempt_identity); + + partition_id = king_orchestrator_find_pipeline_step_string( + pipeline, + (zend_long) index, + "partition_id", + sizeof("partition_id") - 1 + ); + if (partition_id != NULL) { + add_assoc_stringl(&step_adapter, "partition_id", Z_STRVAL_P(partition_id), Z_STRLEN_P(partition_id)); + } else { + add_assoc_null(&step_adapter, "partition_id"); + } + + batch_id = king_orchestrator_find_pipeline_step_string( + pipeline, + (zend_long) index, + "batch_id", + sizeof("batch_id") - 1 + ); + if (batch_id != NULL) { + add_assoc_stringl(&step_adapter, "batch_id", Z_STRVAL_P(batch_id), Z_STRLEN_P(batch_id)); + } else { + add_assoc_null(&step_adapter, "batch_id"); + } + + if ( + run_state != NULL + && run_state->error_step_index == (zend_long) index + && run_state->status != NULL + && ( + zend_string_equals_literal(run_state->status, "failed") + || zend_string_equals_literal(run_state->status, "cancelled") + ) + ) { + failure_identity = king_orchestrator_build_failure_identity_for_snapshot(run_state); + if (failure_identity != NULL) { + add_assoc_stringl(&step_adapter, "failure_identity", ZSTR_VAL(failure_identity), ZSTR_LEN(failure_identity)); + zend_string_release(failure_identity); + } else { + add_assoc_null(&step_adapter, "failure_identity"); + } + } else { + add_assoc_null(&step_adapter, "failure_identity"); + } + + add_assoc_zval(&entry, "telemetry_adapter", &step_adapter); + } else { + add_assoc_null(&entry, "telemetry_adapter"); + } + + add_next_index_zval(&steps, &entry); + index++; + } ZEND_HASH_FOREACH_END(); + + add_assoc_zval(target, "steps", &steps); +} + +int king_orchestrator_get_run_snapshot(zend_string *run_id, zval *return_value) +{ + king_orchestrator_run_state_t *run_state; + zval initial_data; + zval pipeline; + zval options; + zval handler_boundary; + zval result; + zval error; + zval handler_readiness; + int lock_fd = -1; + + ZVAL_NULL(&initial_data); + ZVAL_NULL(&pipeline); + ZVAL_NULL(&options); + ZVAL_NULL(&handler_boundary); + ZVAL_NULL(&result); + ZVAL_NULL(&error); + ZVAL_NULL(&handler_readiness); + + if (king_orchestrator_state_transaction_begin(&lock_fd) != SUCCESS) { + return FAILURE; + } + + run_state = king_orchestrator_find_run(run_id); + if (run_state == NULL) { + king_orchestrator_state_lock_release(lock_fd); + return FAILURE; + } + + if ( + king_orchestrator_decode_base64_zval(run_state->initial_data_b64, &initial_data) != SUCCESS + || king_orchestrator_decode_base64_zval(run_state->pipeline_b64, &pipeline) != SUCCESS + || king_orchestrator_decode_base64_zval(run_state->options_b64, &options) != SUCCESS + || ( + run_state->handler_boundary_b64 != NULL + && king_orchestrator_decode_base64_zval(run_state->handler_boundary_b64, &handler_boundary) != SUCCESS + ) + || king_orchestrator_decode_base64_zval(run_state->result_b64, &result) != SUCCESS + || king_orchestrator_decode_base64_zval(run_state->error_b64, &error) != SUCCESS + ) { + zval_ptr_dtor(&initial_data); + zval_ptr_dtor(&pipeline); + zval_ptr_dtor(&options); + zval_ptr_dtor(&handler_boundary); + zval_ptr_dtor(&result); + zval_ptr_dtor(&error); + king_orchestrator_state_lock_release(lock_fd); + return FAILURE; + } + + array_init(return_value); + add_assoc_stringl(return_value, "run_id", ZSTR_VAL(run_state->run_id), ZSTR_LEN(run_state->run_id)); + add_assoc_stringl(return_value, "status", ZSTR_VAL(run_state->status), ZSTR_LEN(run_state->status)); + add_assoc_string(return_value, "execution_backend", (char *) king_orchestrator_run_execution_backend(run_state)); + add_assoc_string( + return_value, + "topology_scope", + (char *) king_orchestrator_topology_scope_for_backend(king_orchestrator_run_execution_backend(run_state)) + ); + add_assoc_string(return_value, "retry_policy", "single_attempt"); + add_assoc_string(return_value, "idempotency_policy", "caller_managed"); + add_assoc_string(return_value, "compensation_policy", "caller_managed"); + add_assoc_long(return_value, "started_at", (zend_long) run_state->started_at); + add_assoc_long(return_value, "finished_at", (zend_long) run_state->finished_at); + add_assoc_bool(return_value, "cancel_requested", run_state->cancel_requested ? 1 : 0); + add_assoc_long(return_value, "step_count", king_orchestrator_pipeline_step_count(&pipeline)); + add_assoc_long(return_value, "completed_step_count", run_state->completed_step_count); + add_assoc_zval(return_value, "initial_data", &initial_data); + add_assoc_zval(return_value, "pipeline", &pipeline); + add_assoc_zval(return_value, "options", &options); + add_assoc_zval(return_value, "handler_boundary", &handler_boundary); + king_orchestrator_append_handler_readiness( + Z_TYPE(handler_boundary) == IS_ARRAY + ? &handler_boundary + : NULL, + &handler_readiness + ); + add_assoc_zval(return_value, "handler_readiness", &handler_readiness); + add_assoc_zval(return_value, "result", &result); + add_assoc_zval(return_value, "error", &error); + king_orchestrator_append_error_classification(return_value, run_state, &pipeline); + king_orchestrator_append_run_telemetry_adapter_snapshot(return_value, &pipeline, run_state); + king_orchestrator_append_compensation_snapshot(return_value, &pipeline, run_state); + king_orchestrator_append_run_observability(return_value, run_state, &pipeline); + king_orchestrator_append_step_snapshots(return_value, &pipeline, run_state); + + king_orchestrator_state_lock_release(lock_fd); + return SUCCESS; +} diff --git a/extension/src/pipeline_orchestrator/tool_registry/state_persistence.inc b/extension/src/pipeline_orchestrator/tool_registry/state_persistence.inc index 9a569d993..796bc903d 100644 --- a/extension/src/pipeline_orchestrator/tool_registry/state_persistence.inc +++ b/extension/src/pipeline_orchestrator/tool_registry/state_persistence.inc @@ -650,155 +650,4 @@ static king_orchestrator_run_state_t *king_orchestrator_find_run(zend_string *ru return (king_orchestrator_run_state_t *) Z_PTR_P(entry); } -int king_orchestrator_load_run_payload( - zend_string *run_id, - zval *initial_data, - zval *pipeline, - zval *options, - zval *telemetry_parent_context) -{ - king_orchestrator_run_state_t *run_state; - int lock_fd = -1; - - if ( - initial_data == NULL - || pipeline == NULL - || options == NULL - || telemetry_parent_context == NULL - ) { - return FAILURE; - } - - ZVAL_NULL(initial_data); - ZVAL_NULL(pipeline); - ZVAL_NULL(options); - ZVAL_NULL(telemetry_parent_context); - - if (king_orchestrator_state_transaction_begin(&lock_fd) != SUCCESS) { - return FAILURE; - } - - run_state = king_orchestrator_find_run(run_id); - if (run_state == NULL) { - king_orchestrator_state_lock_release(lock_fd); - return FAILURE; - } - - if ( - king_orchestrator_decode_base64_zval(run_state->initial_data_b64, initial_data) != SUCCESS - || king_orchestrator_decode_base64_zval(run_state->pipeline_b64, pipeline) != SUCCESS - || king_orchestrator_decode_base64_zval(run_state->options_b64, options) != SUCCESS - || ( - run_state->telemetry_parent_context_b64 != NULL - && king_orchestrator_decode_base64_zval( - run_state->telemetry_parent_context_b64, - telemetry_parent_context - ) != SUCCESS - ) - ) { - zval_ptr_dtor(initial_data); - zval_ptr_dtor(pipeline); - zval_ptr_dtor(options); - zval_ptr_dtor(telemetry_parent_context); - ZVAL_NULL(initial_data); - ZVAL_NULL(pipeline); - ZVAL_NULL(options); - ZVAL_NULL(telemetry_parent_context); - king_orchestrator_state_lock_release(lock_fd); - return FAILURE; - } - - king_orchestrator_state_lock_release(lock_fd); - return SUCCESS; -} - -int king_orchestrator_load_run_progress( - zend_string *run_id, - zval *result, - zend_long *completed_step_count_out -) -{ - king_orchestrator_run_state_t *run_state; - int lock_fd = -1; - - if (result == NULL) { - return FAILURE; - } - - ZVAL_NULL(result); - if (completed_step_count_out != NULL) { - *completed_step_count_out = 0; - } - - if (king_orchestrator_state_transaction_begin(&lock_fd) != SUCCESS) { - return FAILURE; - } - - run_state = king_orchestrator_find_run(run_id); - if (run_state == NULL) { - king_orchestrator_state_lock_release(lock_fd); - return FAILURE; - } - - if (completed_step_count_out != NULL) { - *completed_step_count_out = run_state->completed_step_count; - } - - if ( - run_state->result_b64 != NULL - && king_orchestrator_decode_base64_zval(run_state->result_b64, result) != SUCCESS - ) { - zval_ptr_dtor(result); - ZVAL_NULL(result); - if (completed_step_count_out != NULL) { - *completed_step_count_out = 0; - } - king_orchestrator_state_lock_release(lock_fd); - return FAILURE; - } - - king_orchestrator_state_lock_release(lock_fd); - return SUCCESS; -} - -int king_orchestrator_load_run_handler_boundary(zend_string *run_id, zval *return_value) -{ - king_orchestrator_run_state_t *run_state; - int lock_fd = -1; - - if (return_value == NULL) { - return FAILURE; - } - - ZVAL_NULL(return_value); - - if (king_orchestrator_state_transaction_begin(&lock_fd) != SUCCESS) { - return FAILURE; - } - - run_state = king_orchestrator_find_run(run_id); - if (run_state == NULL) { - king_orchestrator_state_lock_release(lock_fd); - return FAILURE; - } - - if (run_state->handler_boundary_b64 == NULL) { - king_orchestrator_state_lock_release(lock_fd); - return SUCCESS; - } - - if ( - king_orchestrator_decode_base64_zval( - run_state->handler_boundary_b64, - return_value - ) != SUCCESS - ) { - zval_ptr_dtor(return_value); - ZVAL_NULL(return_value); - king_orchestrator_state_lock_release(lock_fd); - return FAILURE; - } - - king_orchestrator_state_lock_release(lock_fd); - return SUCCESS; -} +#include "state_persistence/run_read_api.inc" diff --git a/extension/src/pipeline_orchestrator/tool_registry/state_persistence/run_read_api.inc b/extension/src/pipeline_orchestrator/tool_registry/state_persistence/run_read_api.inc new file mode 100644 index 000000000..46bbeb780 --- /dev/null +++ b/extension/src/pipeline_orchestrator/tool_registry/state_persistence/run_read_api.inc @@ -0,0 +1,152 @@ +int king_orchestrator_load_run_payload( + zend_string *run_id, + zval *initial_data, + zval *pipeline, + zval *options, + zval *telemetry_parent_context) +{ + king_orchestrator_run_state_t *run_state; + int lock_fd = -1; + + if ( + initial_data == NULL + || pipeline == NULL + || options == NULL + || telemetry_parent_context == NULL + ) { + return FAILURE; + } + + ZVAL_NULL(initial_data); + ZVAL_NULL(pipeline); + ZVAL_NULL(options); + ZVAL_NULL(telemetry_parent_context); + + if (king_orchestrator_state_transaction_begin(&lock_fd) != SUCCESS) { + return FAILURE; + } + + run_state = king_orchestrator_find_run(run_id); + if (run_state == NULL) { + king_orchestrator_state_lock_release(lock_fd); + return FAILURE; + } + + if ( + king_orchestrator_decode_base64_zval(run_state->initial_data_b64, initial_data) != SUCCESS + || king_orchestrator_decode_base64_zval(run_state->pipeline_b64, pipeline) != SUCCESS + || king_orchestrator_decode_base64_zval(run_state->options_b64, options) != SUCCESS + || ( + run_state->telemetry_parent_context_b64 != NULL + && king_orchestrator_decode_base64_zval( + run_state->telemetry_parent_context_b64, + telemetry_parent_context + ) != SUCCESS + ) + ) { + zval_ptr_dtor(initial_data); + zval_ptr_dtor(pipeline); + zval_ptr_dtor(options); + zval_ptr_dtor(telemetry_parent_context); + ZVAL_NULL(initial_data); + ZVAL_NULL(pipeline); + ZVAL_NULL(options); + ZVAL_NULL(telemetry_parent_context); + king_orchestrator_state_lock_release(lock_fd); + return FAILURE; + } + + king_orchestrator_state_lock_release(lock_fd); + return SUCCESS; +} + +int king_orchestrator_load_run_progress( + zend_string *run_id, + zval *result, + zend_long *completed_step_count_out +) +{ + king_orchestrator_run_state_t *run_state; + int lock_fd = -1; + + if (result == NULL) { + return FAILURE; + } + + ZVAL_NULL(result); + if (completed_step_count_out != NULL) { + *completed_step_count_out = 0; + } + + if (king_orchestrator_state_transaction_begin(&lock_fd) != SUCCESS) { + return FAILURE; + } + + run_state = king_orchestrator_find_run(run_id); + if (run_state == NULL) { + king_orchestrator_state_lock_release(lock_fd); + return FAILURE; + } + + if (completed_step_count_out != NULL) { + *completed_step_count_out = run_state->completed_step_count; + } + + if ( + run_state->result_b64 != NULL + && king_orchestrator_decode_base64_zval(run_state->result_b64, result) != SUCCESS + ) { + zval_ptr_dtor(result); + ZVAL_NULL(result); + if (completed_step_count_out != NULL) { + *completed_step_count_out = 0; + } + king_orchestrator_state_lock_release(lock_fd); + return FAILURE; + } + + king_orchestrator_state_lock_release(lock_fd); + return SUCCESS; +} + +int king_orchestrator_load_run_handler_boundary(zend_string *run_id, zval *return_value) +{ + king_orchestrator_run_state_t *run_state; + int lock_fd = -1; + + if (return_value == NULL) { + return FAILURE; + } + + ZVAL_NULL(return_value); + + if (king_orchestrator_state_transaction_begin(&lock_fd) != SUCCESS) { + return FAILURE; + } + + run_state = king_orchestrator_find_run(run_id); + if (run_state == NULL) { + king_orchestrator_state_lock_release(lock_fd); + return FAILURE; + } + + if (run_state->handler_boundary_b64 == NULL) { + king_orchestrator_state_lock_release(lock_fd); + return SUCCESS; + } + + if ( + king_orchestrator_decode_base64_zval( + run_state->handler_boundary_b64, + return_value + ) != SUCCESS + ) { + zval_ptr_dtor(return_value); + ZVAL_NULL(return_value); + king_orchestrator_state_lock_release(lock_fd); + return FAILURE; + } + + king_orchestrator_state_lock_release(lock_fd); + return SUCCESS; +} diff --git a/extension/src/semantic_dns/semantic_dns.c b/extension/src/semantic_dns/semantic_dns.c index a6a8c0eb3..1847d2ecd 100644 --- a/extension/src/semantic_dns/semantic_dns.c +++ b/extension/src/semantic_dns/semantic_dns.c @@ -6,8 +6,8 @@ */ #include "php_king.h" -#include "include/config/smart_dns/base_layer.h" -#include "include/semantic_dns/semantic_dns.h" +#include "config/smart_dns/base_layer.h" +#include "semantic_dns/semantic_dns.h" #include #include diff --git a/extension/src/semantic_dns/semantic_dns/init_config.inc b/extension/src/semantic_dns/semantic_dns/init_config.inc index 3e7fc391f..fcbd2e9f1 100644 --- a/extension/src/semantic_dns/semantic_dns/init_config.inc +++ b/extension/src/semantic_dns/semantic_dns/init_config.inc @@ -291,6 +291,15 @@ static bool king_semantic_dns_parse_init_config( 0, parsed->mothernode_uri, sizeof(parsed->mothernode_uri) + ) + || !king_semantic_dns_require_non_empty_string_option( + config, + "state_path", + sizeof("state_path") - 1, + "dns_state_path", + sizeof("dns_state_path") - 1, + parsed->state_path, + sizeof(parsed->state_path) )) { king_semantic_dns_config_clear(parsed); return false; @@ -336,5 +345,4 @@ static bool king_semantic_dns_runtime_require_initialized(const char *function_n return false; } -#include "include/king_globals.h" - +#include "php_king/globals.h" diff --git a/extension/src/semantic_dns/semantic_dns/runtime_config.inc b/extension/src/semantic_dns/semantic_dns/runtime_config.inc index a21de0ef8..5adb6cca6 100644 --- a/extension/src/semantic_dns/semantic_dns/runtime_config.inc +++ b/extension/src/semantic_dns/semantic_dns/runtime_config.inc @@ -59,6 +59,23 @@ static void king_semantic_dns_config_assign_defaults(king_semantic_dns_config_t ); config->mothernode_uri[sizeof(config->mothernode_uri) - 1] = '\0'; } + + if (king_smart_dns_config.state_path != NULL + && king_smart_dns_config.state_path[0] != '\0') { + strncpy( + config->state_path, + king_smart_dns_config.state_path, + sizeof(config->state_path) - 1 + ); + config->state_path[sizeof(config->state_path) - 1] = '\0'; + } else { + strncpy( + config->state_path, + "/tmp/king_semantic_dns_state/durable_state.bin", + sizeof(config->state_path) - 1 + ); + config->state_path[sizeof(config->state_path) - 1] = '\0'; + } } static void king_semantic_dns_config_copy( @@ -85,6 +102,8 @@ static void king_semantic_dns_config_copy( target->bind_address[sizeof(target->bind_address) - 1] = '\0'; strncpy(target->mothernode_uri, source->mothernode_uri, sizeof(target->mothernode_uri) - 1); target->mothernode_uri[sizeof(target->mothernode_uri) - 1] = '\0'; + strncpy(target->state_path, source->state_path, sizeof(target->state_path) - 1); + target->state_path[sizeof(target->state_path) - 1] = '\0'; } @@ -97,14 +116,21 @@ static void king_semantic_dns_runtime_reset(void) static bool king_semantic_dns_listener_snapshot_path_init(void) { + char state_dir[PATH_MAX]; + if (king_semantic_dns_runtime.listener_state_path[0] != '\0') { return true; } + if (king_semantic_dns_state_build_directory_path(state_dir, sizeof(state_dir)) != SUCCESS) { + return false; + } + if (snprintf( king_semantic_dns_runtime.listener_state_path, sizeof(king_semantic_dns_runtime.listener_state_path), - "/tmp/king_semantic_dns_state/listener_state." ZEND_LONG_FMT ".bin", + "%s/listener_state." ZEND_LONG_FMT ".bin", + state_dir, (zend_long) getpid() ) >= (int) sizeof(king_semantic_dns_runtime.listener_state_path)) { king_semantic_dns_runtime.listener_state_path[0] = '\0'; @@ -191,4 +217,3 @@ static void king_semantic_dns_listener_stop(void) king_semantic_dns_listener_unlink_snapshot(); } - diff --git a/extension/src/semantic_dns/state.c b/extension/src/semantic_dns/state.c index 0a0dd216c..5cfed6a55 100644 --- a/extension/src/semantic_dns/state.c +++ b/extension/src/semantic_dns/state.c @@ -18,9 +18,7 @@ #include #include -#define KING_SEMANTIC_DNS_STATE_DIR "/tmp/king_semantic_dns_state" -#define KING_SEMANTIC_DNS_STATE_FILE KING_SEMANTIC_DNS_STATE_DIR "/durable_state.bin" -#define KING_SEMANTIC_DNS_STATE_LOCK_FILE KING_SEMANTIC_DNS_STATE_DIR "/durable_state.bin.lock" +#define KING_SEMANTIC_DNS_DEFAULT_STATE_FILE "/tmp/king_semantic_dns_state/durable_state.bin" #define KING_SEMANTIC_DNS_STATE_MAGIC 0x53444e53 /* 'SDNS' */ #define KING_SEMANTIC_DNS_STATE_VERSION 1 #define KING_SEMANTIC_DNS_STATE_MAX_MOTHER_NODES 1024U diff --git a/extension/src/semantic_dns/state/load.inc b/extension/src/semantic_dns/state/load.inc index 32f22e09c..efe486f9d 100644 --- a/extension/src/semantic_dns/state/load.inc +++ b/extension/src/semantic_dns/state/load.inc @@ -10,6 +10,7 @@ int king_semantic_dns_state_load(void) uint32_t magic, version, node_count; uint32_t payload_len = 0; unsigned char *payload_buffer = NULL; + const char *state_path; zval state_payload; zend_bool has_state_payload = 0; @@ -23,8 +24,13 @@ int king_semantic_dns_state_load(void) return FAILURE; } + state_path = king_semantic_dns_state_file_path(); + if (php_check_open_basedir(state_path) != 0) { + return FAILURE; + } + fd = open( - KING_SEMANTIC_DNS_STATE_FILE, + state_path, O_RDONLY #ifdef O_NOFOLLOW | O_NOFOLLOW @@ -154,4 +160,3 @@ int king_semantic_dns_state_load(void) return SUCCESS; } - diff --git a/extension/src/semantic_dns/state/save.inc b/extension/src/semantic_dns/state/save.inc index e85a5d76b..b9bba22a0 100644 --- a/extension/src/semantic_dns/state/save.inc +++ b/extension/src/semantic_dns/state/save.inc @@ -3,6 +3,7 @@ int king_semantic_dns_state_save(void) FILE *fp = NULL; int fd; char tmp_template[PATH_MAX]; + const char *state_path; uint32_t magic = KING_SEMANTIC_DNS_STATE_MAGIC; uint32_t version = KING_SEMANTIC_DNS_STATE_VERSION; uint32_t payload_len = 0; @@ -19,11 +20,12 @@ int king_semantic_dns_state_save(void) return FAILURE; } - if (php_check_open_basedir(KING_SEMANTIC_DNS_STATE_FILE) != 0) { + state_path = king_semantic_dns_state_file_path(); + if (php_check_open_basedir(state_path) != 0) { return FAILURE; } - if (snprintf(tmp_template, sizeof(tmp_template), "%s/.state.XXXXXX", KING_SEMANTIC_DNS_STATE_DIR) >= (int) sizeof(tmp_template)) { + if (king_semantic_dns_state_build_tmp_template(tmp_template, sizeof(tmp_template)) != SUCCESS) { return FAILURE; } @@ -123,11 +125,10 @@ int king_semantic_dns_state_save(void) return FAILURE; } - if (rename(tmp_template, KING_SEMANTIC_DNS_STATE_FILE) != 0) { + if (rename(tmp_template, state_path) != 0) { unlink(tmp_template); return FAILURE; } return SUCCESS; } - diff --git a/extension/src/semantic_dns/state/serialization_and_locking.inc b/extension/src/semantic_dns/state/serialization_and_locking.inc index 5a1c3a4c9..5a6f6b229 100644 --- a/extension/src/semantic_dns/state/serialization_and_locking.inc +++ b/extension/src/semantic_dns/state/serialization_and_locking.inc @@ -126,19 +126,97 @@ static int king_semantic_dns_state_dir_is_secure(const struct stat *st) return SUCCESS; } +static const char *king_semantic_dns_state_file_path(void) +{ + if (king_semantic_dns_runtime.initialized + && king_semantic_dns_runtime.config.state_path[0] != '\0') { + return king_semantic_dns_runtime.config.state_path; + } + + return KING_SEMANTIC_DNS_DEFAULT_STATE_FILE; +} + +int king_semantic_dns_state_build_directory_path(char *dest, size_t dest_len) +{ + const char *state_path = king_semantic_dns_state_file_path(); + const char *last_separator; + size_t dir_len; + + if (dest == NULL || dest_len == 0 || state_path == NULL || state_path[0] != '/') { + return FAILURE; + } + + last_separator = strrchr(state_path, '/'); + if (last_separator == NULL || last_separator[1] == '\0') { + return FAILURE; + } + + dir_len = (last_separator == state_path) + ? 1 + : (size_t) (last_separator - state_path); + if (dir_len == 0 || dir_len >= dest_len) { + return FAILURE; + } + + memcpy(dest, state_path, dir_len); + dest[dir_len] = '\0'; + return SUCCESS; +} + +static int king_semantic_dns_state_build_lock_path(char *dest, size_t dest_len) +{ + const char *state_path = king_semantic_dns_state_file_path(); + + if (dest == NULL || dest_len == 0 || state_path == NULL || state_path[0] == '\0') { + return FAILURE; + } + + if (snprintf(dest, dest_len, "%s.lock", state_path) >= (int) dest_len) { + dest[0] = '\0'; + return FAILURE; + } + + return SUCCESS; +} + +static int king_semantic_dns_state_build_tmp_template(char *dest, size_t dest_len) +{ + char state_dir[PATH_MAX]; + + if (dest == NULL || dest_len == 0) { + return FAILURE; + } + + if (king_semantic_dns_state_build_directory_path(state_dir, sizeof(state_dir)) != SUCCESS) { + return FAILURE; + } + + if (snprintf(dest, dest_len, "%s/.state.XXXXXX", state_dir) >= (int) dest_len) { + dest[0] = '\0'; + return FAILURE; + } + + return SUCCESS; +} + static int king_semantic_dns_state_ensure_directory(void) { struct stat st; + char state_dir[PATH_MAX]; - if (php_check_open_basedir(KING_SEMANTIC_DNS_STATE_DIR) != 0) { + if (king_semantic_dns_state_build_directory_path(state_dir, sizeof(state_dir)) != SUCCESS) { return FAILURE; } - if (mkdir(KING_SEMANTIC_DNS_STATE_DIR, 0700) != 0 && errno != EEXIST) { + if (php_check_open_basedir(state_dir) != 0) { return FAILURE; } - if (lstat(KING_SEMANTIC_DNS_STATE_DIR, &st) != 0) { + if (mkdir(state_dir, 0700) != 0 && errno != EEXIST) { + return FAILURE; + } + + if (lstat(state_dir, &st) != 0) { return FAILURE; } @@ -148,8 +226,13 @@ static int king_semantic_dns_state_ensure_directory(void) static int king_semantic_dns_state_path_is_regular_file(void) { struct stat state_stat; + const char *state_path = king_semantic_dns_state_file_path(); - if (lstat(KING_SEMANTIC_DNS_STATE_FILE, &state_stat) != 0) { + if (php_check_open_basedir(state_path) != 0) { + return 0; + } + + if (lstat(state_path, &state_stat) != 0) { return 0; } @@ -161,6 +244,7 @@ static int king_semantic_dns_state_lock_acquire(int *lock_fd_out) int flags = O_RDWR | O_CREAT; int fd; struct stat lock_stat; + char lock_path[PATH_MAX]; if (lock_fd_out == NULL) { return FAILURE; @@ -172,7 +256,11 @@ static int king_semantic_dns_state_lock_acquire(int *lock_fd_out) return FAILURE; } - if (php_check_open_basedir(KING_SEMANTIC_DNS_STATE_LOCK_FILE) != 0) { + if (king_semantic_dns_state_build_lock_path(lock_path, sizeof(lock_path)) != SUCCESS) { + return FAILURE; + } + + if (php_check_open_basedir(lock_path) != 0) { return FAILURE; } @@ -183,7 +271,7 @@ static int king_semantic_dns_state_lock_acquire(int *lock_fd_out) flags |= O_NOFOLLOW; #endif - fd = open(KING_SEMANTIC_DNS_STATE_LOCK_FILE, flags, 0600); + fd = open(lock_path, flags, 0600); if (fd < 0) { return FAILURE; } @@ -220,4 +308,3 @@ static int king_semantic_dns_state_refresh_locked(void) return king_semantic_dns_state_load(); } - diff --git a/extension/src/semantic_dns/state/snapshot_files.inc b/extension/src/semantic_dns/state/snapshot_files.inc index c756f86ac..1ac0ff223 100644 --- a/extension/src/semantic_dns/state/snapshot_files.inc +++ b/extension/src/semantic_dns/state/snapshot_files.inc @@ -3,6 +3,7 @@ int king_semantic_dns_state_write_snapshot_file(const char *path, zval *payload) FILE *fp = NULL; int fd = -1; char tmp_template[PATH_MAX]; + char state_dir[PATH_MAX]; zend_string *serialized_payload = NULL; if (path == NULL || path[0] == '\0' || payload == NULL) { @@ -17,7 +18,11 @@ int king_semantic_dns_state_write_snapshot_file(const char *path, zval *payload) return FAILURE; } - if (snprintf(tmp_template, sizeof(tmp_template), "%s/.listener.XXXXXX", KING_SEMANTIC_DNS_STATE_DIR) >= (int) sizeof(tmp_template)) { + if (king_semantic_dns_state_build_directory_path(state_dir, sizeof(state_dir)) != SUCCESS) { + return FAILURE; + } + + if (snprintf(tmp_template, sizeof(tmp_template), "%s/.listener.XXXXXX", state_dir) >= (int) sizeof(tmp_template)) { return FAILURE; } diff --git a/extension/src/server/admin_api.c b/extension/src/server/admin_api.c index e1c7d4cf0..ffefec859 100644 --- a/extension/src/server/admin_api.c +++ b/extension/src/server/admin_api.c @@ -14,13 +14,13 @@ #include "php.h" #include "php_king.h" -#include "include/client/session.h" -#include "include/config/config.h" -#include "include/config/dynamic_admin_api/base_layer.h" -#include "include/config/security_and_traffic/base_layer.h" -#include "include/server/admin_api.h" -#include "include/server/session.h" -#include "include/server/tls.h" +#include "client/session.h" +#include "config/config.h" +#include "config/dynamic_admin_api/base_layer.h" +#include "config/security_and_traffic/base_layer.h" +#include "server/admin_api.h" +#include "server/session.h" +#include "server/tls.h" #include "main/php_network.h" #include "main/php_streams.h" diff --git a/extension/src/server/cancel.c b/extension/src/server/cancel.c index e8f3255d5..3aab9a5f4 100644 --- a/extension/src/server/cancel.c +++ b/extension/src/server/cancel.c @@ -12,7 +12,7 @@ #include "php.h" #include "php_king.h" -#include "include/server/cancel.h" +#include "server/cancel.h" #include #include diff --git a/extension/src/server/class_entries.inc b/extension/src/server/class_entries.inc new file mode 100644 index 000000000..04a41a451 --- /dev/null +++ b/extension/src/server/class_entries.inc @@ -0,0 +1,5 @@ +/* + * Server PHP class-entry storage. + */ + +zend_class_entry *king_ce_ws_server = NULL; diff --git a/extension/src/server/cors.c b/extension/src/server/cors.c index 70fb8a8ea..70cba8c3e 100644 --- a/extension/src/server/cors.c +++ b/extension/src/server/cors.c @@ -15,8 +15,8 @@ #include "php.h" #include "php_king.h" -#include "include/client/session.h" -#include "include/server/cors.h" +#include "client/session.h" +#include "server/cors.h" #include #include diff --git a/extension/src/server/early_hints.c b/extension/src/server/early_hints.c index 817f33738..f1059bc58 100644 --- a/extension/src/server/early_hints.c +++ b/extension/src/server/early_hints.c @@ -14,7 +14,7 @@ #include "php.h" #include "php_king.h" -#include "include/server/early_hints.h" +#include "server/early_hints.h" #include #include diff --git a/extension/src/server/http1.c b/extension/src/server/http1.c index c235f294c..852d349f4 100644 --- a/extension/src/server/http1.c +++ b/extension/src/server/http1.c @@ -13,12 +13,12 @@ #include "php.h" #include "php_king.h" -#include "include/client/session.h" -#include "include/client/websocket.h" -#include "include/config/config.h" -#include "include/server/http1.h" -#include "include/server/session.h" -#include "include/server/websocket.h" +#include "client/session.h" +#include "client/websocket.h" +#include "config/config.h" +#include "server/http1.h" +#include "server/session.h" +#include "server/websocket.h" #include "Zend/zend_smart_str.h" @@ -35,6 +35,10 @@ #include #include +#ifndef MSG_NOSIGNAL +# define MSG_NOSIGNAL 0 +#endif + #include "local_listener.inc" #define KING_SERVER_HTTP1_MAX_REQUEST_HEAD_BYTES 32768 diff --git a/extension/src/server/http1/public_api.inc b/extension/src/server/http1/public_api.inc index 08f1ec7b7..e974877dd 100644 --- a/extension/src/server/http1/public_api.inc +++ b/extension/src/server/http1/public_api.inc @@ -85,6 +85,303 @@ static zend_result king_server_http1_send_early_hints( return rc; } +static zval *king_server_http1_response_body_stream(zval *retval) +{ + zval *body_stream = zend_hash_str_find( + Z_ARRVAL_P(retval), + "body_stream", + sizeof("body_stream") - 1 + ); + + if (body_stream == NULL || Z_TYPE_P(body_stream) == IS_NULL) { + return NULL; + } + + return body_stream; +} + +static int king_server_http1_persistent_listener_fd = -1; +static zend_string *king_server_http1_persistent_listener_host = NULL; +static zend_long king_server_http1_persistent_listener_port = 0; + +static bool king_server_http1_config_bool( + zval *config, + const char *key, + size_t key_len +) +{ + zval *value; + + if (config == NULL || Z_TYPE_P(config) != IS_ARRAY) { + return false; + } + + value = zend_hash_str_find(Z_ARRVAL_P(config), key, key_len); + if (value == NULL || Z_TYPE_P(value) == IS_NULL) { + return false; + } + + return zend_is_true(value); +} + +static void king_server_http1_close_persistent_listener(void) +{ + if (king_server_http1_persistent_listener_fd >= 0) { + close(king_server_http1_persistent_listener_fd); + king_server_http1_persistent_listener_fd = -1; + } + + if (king_server_http1_persistent_listener_host != NULL) { + zend_string_release(king_server_http1_persistent_listener_host); + king_server_http1_persistent_listener_host = NULL; + } + + king_server_http1_persistent_listener_port = 0; +} + +static zend_result king_server_http1_resolve_listener_socket( + char *host, + size_t host_len, + zend_long port, + bool persistent, + int *listener_fd_out, + const char *function_name +) +{ + int listener_fd = -1; + + if (!persistent) { + return king_server_http1_open_listener_socket( + host, + port, + listener_fd_out, + function_name + ); + } + + if ( + king_server_http1_persistent_listener_fd >= 0 + && king_server_http1_persistent_listener_host != NULL + && king_server_http1_persistent_listener_port == port + && ZSTR_LEN(king_server_http1_persistent_listener_host) == host_len + && memcmp(ZSTR_VAL(king_server_http1_persistent_listener_host), host, host_len) == 0 + ) { + *listener_fd_out = king_server_http1_persistent_listener_fd; + return SUCCESS; + } + + king_server_http1_close_persistent_listener(); + + if ( + king_server_http1_open_listener_socket( + host, + port, + &listener_fd, + function_name + ) != SUCCESS + ) { + return FAILURE; + } + + king_server_http1_persistent_listener_fd = listener_fd; + king_server_http1_persistent_listener_host = zend_string_init(host, host_len, 0); + king_server_http1_persistent_listener_port = port; + *listener_fd_out = listener_fd; + + return SUCCESS; +} + +static zend_long king_server_http1_write_deadline(king_client_session_t *session) +{ + return king_server_http1_now_ms() + king_server_http1_resolve_timeout_ms(session); +} + +static zend_result king_server_http1_call_body_stream( + zval *body_stream, + zval *chunk, + const char *function_name +) +{ + zend_fcall_info fci; + zend_fcall_info_cache fcc; + + if (zend_fcall_info_init(body_stream, 0, &fci, &fcc, NULL, NULL) != SUCCESS) { + king_server_local_set_errorf( + "%s() failed to initialize the HTTP body stream callback.", + function_name + ); + return FAILURE; + } + + fci.retval = chunk; + fci.param_count = 0; + fci.params = NULL; + + if (zend_call_function(&fci, &fcc) != SUCCESS) { + if (EG(exception) == NULL) { + king_server_local_set_errorf( + "%s() failed to invoke the HTTP body stream callback.", + function_name + ); + } + return FAILURE; + } + + return SUCCESS; +} + +static zend_result king_server_http1_write_chunk( + king_client_session_t *session, + const char *bytes, + size_t bytes_len, + const char *function_name +) +{ + char prefix[32]; + int prefix_len; + + if (bytes_len == 0) { + return SUCCESS; + } + + prefix_len = snprintf(prefix, sizeof(prefix), "%zx\r\n", bytes_len); + if (prefix_len <= 0 || (size_t) prefix_len >= sizeof(prefix)) { + king_server_local_set_errorf( + "%s() failed to materialize an HTTP chunk header.", + function_name + ); + return FAILURE; + } + + if (king_server_http1_write_all_fd( + session->transport_socket_fd, + prefix, + (size_t) prefix_len, + king_server_http1_write_deadline(session), + function_name + ) != SUCCESS) { + return FAILURE; + } + if (king_server_http1_write_all_fd( + session->transport_socket_fd, + bytes, + bytes_len, + king_server_http1_write_deadline(session), + function_name + ) != SUCCESS) { + return FAILURE; + } + return king_server_http1_write_all_fd( + session->transport_socket_fd, + "\r\n", + sizeof("\r\n") - 1, + king_server_http1_write_deadline(session), + function_name + ); +} + +static zend_result king_server_http1_finish_chunked_response( + king_client_session_t *session, + const char *function_name +) +{ + return king_server_http1_write_all_fd( + session->transport_socket_fd, + "0\r\n\r\n", + sizeof("0\r\n\r\n") - 1, + king_server_http1_write_deadline(session), + function_name + ); +} + +static zend_result king_server_http1_send_chunked_response( + king_client_session_t *session, + zval *body_stream, + zend_long status, + zval *headers_zv, + zend_long stream_id, + zend_long deadline_ms, + const char *function_name +) +{ + smart_str response = {0}; + zend_result rc; + + if ( + king_server_http1_send_early_hints( + session, + stream_id, + deadline_ms, + function_name + ) != SUCCESS + ) { + return FAILURE; + } + + smart_str_appends(&response, "HTTP/1.1 "); + smart_str_append_long(&response, status); + smart_str_appendc(&response, ' '); + smart_str_appends(&response, king_server_http1_status_text(status)); + smart_str_appends(&response, "\r\n"); + + if (king_server_http1_append_response_headers(&response, headers_zv) != SUCCESS) { + smart_str_free(&response); + return FAILURE; + } + + smart_str_appends(&response, "Transfer-Encoding: chunked\r\nConnection: close\r\n\r\n"); + smart_str_0(&response); + + rc = king_server_http1_write_all_fd( + session->transport_socket_fd, + ZSTR_VAL(response.s), + ZSTR_LEN(response.s), + deadline_ms, + function_name + ); + smart_str_free(&response); + if (rc != SUCCESS) { + return FAILURE; + } + + while (true) { + zval chunk; + + ZVAL_UNDEF(&chunk); + if (king_server_http1_call_body_stream(body_stream, &chunk, function_name) != SUCCESS) { + if (!Z_ISUNDEF(chunk)) { + zval_ptr_dtor(&chunk); + } + return FAILURE; + } + + if (Z_TYPE(chunk) == IS_NULL || Z_TYPE(chunk) == IS_FALSE) { + zval_ptr_dtor(&chunk); + return king_server_http1_finish_chunked_response(session, function_name); + } + + if (Z_TYPE(chunk) != IS_STRING) { + zval_ptr_dtor(&chunk); + king_server_local_set_errorf( + "%s() HTTP body stream callback must return a string chunk, null, or false.", + function_name + ); + return FAILURE; + } + + rc = king_server_http1_write_chunk( + session, + Z_STRVAL(chunk), + Z_STRLEN(chunk), + function_name + ); + zval_ptr_dtor(&chunk); + if (rc != SUCCESS) { + return FAILURE; + } + } +} + static zend_result king_server_http1_send_response( king_client_session_t *session, zval *retval, @@ -96,6 +393,7 @@ static zend_result king_server_http1_send_response( zval *status_zv; zval *headers_zv; zval *body_zv; + zval *body_stream; zend_long status = 200; zend_string *body = NULL; smart_str response = {0}; @@ -113,6 +411,19 @@ static zend_result king_server_http1_send_response( status = zval_get_long(status_zv); } + body_stream = king_server_http1_response_body_stream(retval); + if (body_stream != NULL) { + return king_server_http1_send_chunked_response( + session, + body_stream, + status, + headers_zv, + stream_id, + deadline_ms, + function_name + ); + } + if (body_zv != NULL && Z_TYPE_P(body_zv) != IS_NULL) { body = zval_get_string(body_zv); } else { @@ -263,6 +574,7 @@ PHP_FUNCTION(king_http1_server_listen_once) zend_long stream_id = 0; int listener_fd = -1; int accepted_fd = -1; + bool persistent_listener = false; zend_bool rc = 0; ZEND_PARSE_PARAMETERS_START(4, 4) @@ -283,6 +595,19 @@ PHP_FUNCTION(king_http1_server_listen_once) ZVAL_UNDEF(&request); ZVAL_UNDEF(&retval); + persistent_listener = king_server_http1_config_bool( + config, + "persistent_listener", + sizeof("persistent_listener") - 1 + ) || king_server_http1_config_bool( + config, + "tcp.persistent_listener", + sizeof("tcp.persistent_listener") - 1 + ) || king_server_http1_config_bool( + config, + "tcp_persistent_listener", + sizeof("tcp_persistent_listener") - 1 + ); session = king_server_local_open_session( "king_http1_server_listen_once", @@ -303,9 +628,11 @@ PHP_FUNCTION(king_http1_server_listen_once) } if ( - king_server_http1_open_listener_socket( + king_server_http1_resolve_listener_socket( host, + host_len, port, + persistent_listener, &listener_fd, "king_http1_server_listen_once" ) != SUCCESS @@ -326,8 +653,10 @@ PHP_FUNCTION(king_http1_server_listen_once) goto cleanup; } - close(listener_fd); - listener_fd = -1; + if (!persistent_listener) { + close(listener_fd); + listener_fd = -1; + } if ( king_server_http1_apply_transport_snapshot_from_socket( @@ -439,7 +768,7 @@ PHP_FUNCTION(king_http1_server_listen_once) rc = 1; cleanup: - if (listener_fd >= 0) { + if (listener_fd >= 0 && !persistent_listener) { close(listener_fd); } diff --git a/extension/src/server/http1/request_parsing.inc b/extension/src/server/http1/request_parsing.inc index 6ebf0b58c..f871e95d2 100644 --- a/extension/src/server/http1/request_parsing.inc +++ b/extension/src/server/http1/request_parsing.inc @@ -438,6 +438,7 @@ static zend_result king_server_http1_append_response_headers( if ( zend_string_equals_literal_ci(header_name, "Content-Length") || zend_string_equals_literal_ci(header_name, "Connection") + || zend_string_equals_literal_ci(header_name, "Transfer-Encoding") ) { continue; } diff --git a/extension/src/server/http1/socket_io.inc b/extension/src/server/http1/socket_io.inc index a3913d924..bc3537295 100644 --- a/extension/src/server/http1/socket_io.inc +++ b/extension/src/server/http1/socket_io.inc @@ -148,7 +148,7 @@ static zend_result king_server_http1_write_all_fd( return FAILURE; } - ssize_t chunk = send(fd, buffer + written, buffer_len - written, 0); + ssize_t chunk = send(fd, buffer + written, buffer_len - written, MSG_NOSIGNAL); if (chunk < 0 && errno == EINTR) { continue; diff --git a/extension/src/server/http1/websocket_server_object.inc b/extension/src/server/http1/websocket_server_object.inc index 742d89593..515350141 100644 --- a/extension/src/server/http1/websocket_server_object.inc +++ b/extension/src/server/http1/websocket_server_object.inc @@ -1,11 +1,4 @@ -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_King_WebSocket_Server___construct, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, host, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, port, IS_LONG, 0) - ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, config, King\\Config, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_King_WebSocket_Server_accept, 0, 0, King\\WebSocket\\Connection, 0) -ZEND_END_ARG_INFO() +#include "server/websocket_server_arginfo.h" static void king_websocket_server_set_errorf( const char *format, diff --git a/extension/src/server/http1/websocket_server_object/dispatch.inc b/extension/src/server/http1/websocket_server_object/dispatch.inc index 2cf875124..c5830eb38 100644 --- a/extension/src/server/http1/websocket_server_object/dispatch.inc +++ b/extension/src/server/http1/websocket_server_object/dispatch.inc @@ -1,27 +1,3 @@ -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_WebSocket_Server_getConnections, 0, 0, IS_ARRAY, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_WebSocket_Server_send, 0, 2, IS_VOID, 0) - ZEND_ARG_TYPE_INFO(0, connectionId, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, message, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_WebSocket_Server_sendBinary, 0, 2, IS_VOID, 0) - ZEND_ARG_TYPE_INFO(0, connectionId, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, payload, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_WebSocket_Server_broadcast, 0, 1, IS_VOID, 0) - ZEND_ARG_TYPE_INFO(0, message, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_WebSocket_Server_broadcastBinary, 0, 1, IS_VOID, 0) - ZEND_ARG_TYPE_INFO(0, payload, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_King_WebSocket_Server_stop, 0, 0, IS_VOID, 0) -ZEND_END_ARG_INFO() - PHP_METHOD(King_WebSocket_Server, getConnections) { king_ws_server_object *intern; @@ -172,14 +148,4 @@ PHP_METHOD(King_WebSocket_Server, stop) king_set_error(""); } -const zend_function_entry king_ws_server_class_methods[] = { - PHP_ME(King_WebSocket_Server, __construct, arginfo_class_King_WebSocket_Server___construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) - PHP_ME(King_WebSocket_Server, accept, arginfo_class_King_WebSocket_Server_accept, ZEND_ACC_PUBLIC) - PHP_ME(King_WebSocket_Server, getConnections, arginfo_class_King_WebSocket_Server_getConnections, ZEND_ACC_PUBLIC) - PHP_ME(King_WebSocket_Server, send, arginfo_class_King_WebSocket_Server_send, ZEND_ACC_PUBLIC) - PHP_ME(King_WebSocket_Server, sendBinary, arginfo_class_King_WebSocket_Server_sendBinary, ZEND_ACC_PUBLIC) - PHP_ME(King_WebSocket_Server, broadcast, arginfo_class_King_WebSocket_Server_broadcast, ZEND_ACC_PUBLIC) - PHP_ME(King_WebSocket_Server, broadcastBinary, arginfo_class_King_WebSocket_Server_broadcastBinary, ZEND_ACC_PUBLIC) - PHP_ME(King_WebSocket_Server, stop, arginfo_class_King_WebSocket_Server_stop, ZEND_ACC_PUBLIC) - PHP_FE_END -}; +#include "server/websocket_server_class_method_entries.h" diff --git a/extension/src/server/http1/websocket_server_object_handlers.inc b/extension/src/server/http1/websocket_server_object_handlers.inc new file mode 100644 index 000000000..b1e5ff0c4 --- /dev/null +++ b/extension/src/server/http1/websocket_server_object_handlers.inc @@ -0,0 +1,77 @@ +/* + * King WebSocket server PHP object handlers. + */ + +#include + +static zend_object_handlers king_ws_server_object_handlers; + +static void king_ws_server_object_free(zend_object *object) +{ + king_ws_server_object *intern = php_king_ws_server_obj_from_zend(object); + + if (!Z_ISUNDEF(intern->config)) { + zval_ptr_dtor(&intern->config); + ZVAL_UNDEF(&intern->config); + } + if (intern->host != NULL) { + zend_string_release(intern->host); + intern->host = NULL; + } + if (intern->listener_fd >= 0) { + close(intern->listener_fd); + intern->listener_fd = -1; + } + if (intern->registry_initialized) { + zval *entry; + + ZEND_HASH_FOREACH_VAL(&intern->connections, entry) + { + king_ws_state *state = Z_TYPE_P(entry) == IS_PTR + ? (king_ws_state *) Z_PTR_P(entry) + : NULL; + + if (state != NULL && state->server_owner == intern) { + state->server_owner = NULL; + } + } + ZEND_HASH_FOREACH_END(); + + zend_hash_destroy(&intern->connections); + intern->registry_initialized = false; + } + + zend_object_std_dtor(&intern->std); +} + +static zend_object *king_ws_server_object_create(zend_class_entry *ce) +{ + king_ws_server_object *intern = zend_object_alloc(sizeof(*intern), ce); + + ZVAL_UNDEF(&intern->config); + intern->host = NULL; + intern->port = 0; + intern->listener_fd = -1; + zend_hash_init(&intern->connections, 8, NULL, NULL, 0); + intern->next_connection_sequence = 1; + intern->registry_initialized = true; + intern->closed = false; + zend_object_std_init(&intern->std, ce); + object_properties_init(&intern->std, ce); + intern->std.handlers = &king_ws_server_object_handlers; + + return &intern->std; +} + +static void king_ws_server_object_handlers_init(void) +{ + memcpy( + &king_ws_server_object_handlers, + &std_object_handlers, + sizeof(zend_object_handlers) + ); + king_ws_server_object_handlers.offset = XtOffsetOf(king_ws_server_object, std); + king_ws_server_object_handlers.free_obj = king_ws_server_object_free; + king_ws_server_object_handlers.clone_obj = NULL; + king_ce_ws_server->create_object = king_ws_server_object_create; +} diff --git a/extension/src/server/http2.c b/extension/src/server/http2.c index 04f9ecf65..f0b282fda 100644 --- a/extension/src/server/http2.c +++ b/extension/src/server/http2.c @@ -13,10 +13,10 @@ #include "php.h" #include "php_king.h" -#include "include/client/session.h" -#include "include/config/config.h" -#include "include/server/http2.h" -#include "include/server/session.h" +#include "client/session.h" +#include "config/config.h" +#include "server/http2.h" +#include "server/session.h" #include "Zend/zend_smart_str.h" diff --git a/extension/src/server/http3.c b/extension/src/server/http3.c index 01f4389d3..a6788e019 100644 --- a/extension/src/server/http3.c +++ b/extension/src/server/http3.c @@ -13,13 +13,13 @@ #include "php.h" #include "php_king.h" -#include "include/client/session.h" -#include "include/config/config.h" -#include "include/config/quic_transport/base_layer.h" -#include "include/config/tcp_transport/base_layer.h" -#include "include/config/tls_and_crypto/base_layer.h" -#include "include/server/http3.h" -#include "include/server/session.h" +#include "client/session.h" +#include "config/config.h" +#include "config/quic_transport/base_layer.h" +#include "config/tcp_transport/base_layer.h" +#include "config/tls_and_crypto/base_layer.h" +#include "server/http3.h" +#include "server/session.h" #include "Zend/zend_smart_str.h" diff --git a/extension/src/server/index.c b/extension/src/server/index.c index 0ecbf3ab9..eed39d5ea 100644 --- a/extension/src/server/index.c +++ b/extension/src/server/index.c @@ -13,15 +13,13 @@ #include "php.h" #include "php_king.h" -#include "include/config/config.h" -#include "include/server/http1.h" -#include "include/server/http2.h" -#include "include/server/http3.h" -#include "include/server/index.h" +#include "config/config.h" +#include "server/http1.h" +#include "server/http2.h" +#include "server/http3.h" +#include "server/index.h" #include -extern int le_king_cfg; - static const char *king_server_select_listener_name(const king_cfg_t *cfg) { if (cfg != NULL && !cfg->tcp.enable) { @@ -213,3 +211,7 @@ PHP_FUNCTION(king_server_listen) zval_ptr_dtor(&listener_result); RETURN_FALSE; } + +#include "state.inc" +#include "php_binding.inc" +#include "registration.inc" diff --git a/extension/src/server/local_listener.inc b/extension/src/server/local_listener.inc index 812329eee..0709569db 100644 --- a/extension/src/server/local_listener.inc +++ b/extension/src/server/local_listener.inc @@ -4,9 +4,9 @@ * and builds the normalized request path used by the local protocol leaves. */ -#include "include/server/cors.h" -#include "include/server/open_telemetry.h" -#include "include/telemetry/telemetry.h" +#include "server/cors.h" +#include "server/open_telemetry.h" +#include "telemetry/telemetry.h" static void king_server_local_set_errorf(const char *format, ...) { @@ -358,6 +358,7 @@ static zend_result king_server_local_validate_response( zval *status; zval *headers; zval *body; + zval *body_stream; zend_long code; if (Z_TYPE_P(retval) != IS_ARRAY) { @@ -390,6 +391,21 @@ static zend_result king_server_local_validate_response( return FAILURE; } + body_stream = zend_hash_str_find( + Z_ARRVAL_P(retval), + "body_stream", + sizeof("body_stream") - 1 + ); + if (body_stream != NULL + && Z_TYPE_P(body_stream) != IS_NULL + && !zend_is_callable(body_stream, 0, NULL)) { + king_server_local_set_errorf( + "%s() handler response 'body_stream' must be a callable when present.", + function_name + ); + return FAILURE; + } + status = zend_hash_str_find(Z_ARRVAL_P(retval), "status", sizeof("status") - 1); if (status == NULL || Z_TYPE_P(status) == IS_NULL) { return SUCCESS; diff --git a/extension/src/server/open_telemetry.c b/extension/src/server/open_telemetry.c index 3d839a19f..a03270f9e 100644 --- a/extension/src/server/open_telemetry.c +++ b/extension/src/server/open_telemetry.c @@ -15,10 +15,10 @@ #include "php.h" #include "php_king.h" -#include "include/client/session.h" -#include "include/config/config.h" -#include "include/server/open_telemetry.h" -#include "include/telemetry/telemetry.h" +#include "client/session.h" +#include "config/config.h" +#include "server/open_telemetry.h" +#include "telemetry/telemetry.h" #include #include @@ -337,268 +337,7 @@ static void king_server_telemetry_apply_snapshot( ); } -static zend_bool king_server_telemetry_header_name_equals_ci( - zend_string *header_name, - const char *expected, - size_t expected_len -) -{ - size_t index; - - if (header_name == NULL || ZSTR_LEN(header_name) != expected_len) { - return 0; - } - - for (index = 0; index < expected_len; index++) { - if (tolower((unsigned char) ZSTR_VAL(header_name)[index]) - != tolower((unsigned char) expected[index])) { - return 0; - } - } - - return 1; -} - -static zval *king_server_telemetry_find_request_header( - zval *request, - const char *header_name, - size_t header_name_len -) -{ - zval *headers; - zval *entry; - zend_string *key; - zval *value; - - if (request == NULL || Z_TYPE_P(request) != IS_ARRAY) { - return NULL; - } - - headers = zend_hash_str_find( - Z_ARRVAL_P(request), - "headers", - sizeof("headers") - 1 - ); - if (headers == NULL || Z_TYPE_P(headers) != IS_ARRAY) { - return NULL; - } - - entry = zend_hash_str_find( - Z_ARRVAL_P(headers), - header_name, - header_name_len - ); - if (entry != NULL) { - return entry; - } - - ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(headers), key, value) { - if (king_server_telemetry_header_name_equals_ci( - key, - header_name, - header_name_len - )) { - return value; - } - } ZEND_HASH_FOREACH_END(); - - return NULL; -} - -static zend_string *king_server_telemetry_copy_first_header_string(zval *header_value) -{ - zval *entry; - - if (header_value == NULL) { - return NULL; - } - - if (Z_TYPE_P(header_value) == IS_STRING && Z_STRLEN_P(header_value) > 0) { - return zend_string_copy(Z_STR_P(header_value)); - } - - if (Z_TYPE_P(header_value) != IS_ARRAY) { - return NULL; - } - - ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(header_value), entry) { - if (Z_TYPE_P(entry) == IS_STRING && Z_STRLEN_P(entry) > 0) { - return zend_string_copy(Z_STR_P(entry)); - } - } ZEND_HASH_FOREACH_END(); - - return NULL; -} - -static zend_string *king_server_telemetry_dup_lowercase_slice( - const char *value, - size_t value_len -) -{ - zend_string *normalized; - size_t index; - - normalized = zend_string_alloc(value_len, 0); - for (index = 0; index < value_len; index++) { - ZSTR_VAL(normalized)[index] = (char) tolower((unsigned char) value[index]); - } - ZSTR_VAL(normalized)[value_len] = '\0'; - - return normalized; -} - -static zend_bool king_server_telemetry_slice_is_hex( - const char *value, - size_t value_len -) -{ - size_t index; - - for (index = 0; index < value_len; index++) { - if (!isxdigit((unsigned char) value[index])) { - return 0; - } - } - - return 1; -} - -static zend_bool king_server_telemetry_slice_is_all_zeroes( - const char *value, - size_t value_len -) -{ - size_t index; - - for (index = 0; index < value_len; index++) { - if (value[index] != '0') { - return 0; - } - } - - return 1; -} - -static zend_result king_server_telemetry_extract_incoming_trace_context( - zval *request, - zval *destination -) -{ - zval *traceparent_value; - zval *tracestate_value; - zend_string *traceparent = NULL; - zend_string *trace_state = NULL; - const char *value; - - traceparent_value = king_server_telemetry_find_request_header( - request, - "traceparent", - sizeof("traceparent") - 1 - ); - if (traceparent_value == NULL) { - return FAILURE; - } - - traceparent = king_server_telemetry_copy_first_header_string(traceparent_value); - if (traceparent == NULL) { - return FAILURE; - } - - value = ZSTR_VAL(traceparent); - if (ZSTR_LEN(traceparent) != 55 - || value[2] != '-' - || value[35] != '-' - || value[52] != '-' - || !king_server_telemetry_slice_is_hex(value, 2) - || !king_server_telemetry_slice_is_hex(value + 3, 32) - || !king_server_telemetry_slice_is_hex(value + 36, 16) - || !king_server_telemetry_slice_is_hex(value + 53, 2) - || (tolower((unsigned char) value[0]) == 'f' - && tolower((unsigned char) value[1]) == 'f') - || king_server_telemetry_slice_is_all_zeroes(value + 3, 32) - || king_server_telemetry_slice_is_all_zeroes(value + 36, 16)) { - zend_string_release(traceparent); - return FAILURE; - } - - array_init(destination); - add_assoc_str( - destination, - "trace_id", - king_server_telemetry_dup_lowercase_slice(value + 3, 32) - ); - add_assoc_str( - destination, - "parent_span_id", - king_server_telemetry_dup_lowercase_slice(value + 36, 16) - ); - add_assoc_str( - destination, - "trace_flags", - king_server_telemetry_dup_lowercase_slice(value + 53, 2) - ); - - tracestate_value = king_server_telemetry_find_request_header( - request, - "tracestate", - sizeof("tracestate") - 1 - ); - trace_state = king_server_telemetry_copy_first_header_string(tracestate_value); - if (trace_state != NULL) { - add_assoc_str(destination, "trace_state", trace_state); - } - - zend_string_release(traceparent); - return SUCCESS; -} - -static zval *king_server_telemetry_find_request_incoming_trace_context(zval *request) -{ - zval *telemetry; - - if (request == NULL || Z_TYPE_P(request) != IS_ARRAY) { - return NULL; - } - - telemetry = zend_hash_str_find( - Z_ARRVAL_P(request), - "telemetry", - sizeof("telemetry") - 1 - ); - if (telemetry == NULL || Z_TYPE_P(telemetry) != IS_ARRAY) { - return NULL; - } - - return zend_hash_str_find( - Z_ARRVAL_P(telemetry), - "incoming_trace_context", - sizeof("incoming_trace_context") - 1 - ); -} - -static zend_result king_server_telemetry_parse_trace_flags( - zval *trace_flags, - uint8_t *parsed_flags -) -{ - char *endptr = NULL; - unsigned long parsed; - - if (trace_flags == NULL - || parsed_flags == NULL - || Z_TYPE_P(trace_flags) != IS_STRING - || Z_STRLEN_P(trace_flags) != 2) { - return FAILURE; - } - - parsed = strtoul(Z_STRVAL_P(trace_flags), &endptr, 16); - if (endptr == NULL || *endptr != '\0' || parsed > 0xffUL) { - return FAILURE; - } - - *parsed_flags = (uint8_t) parsed; - return SUCCESS; -} +#include "open_telemetry/trace_context.inc" PHP_FUNCTION(king_server_init_telemetry) { diff --git a/extension/src/server/open_telemetry/trace_context.inc b/extension/src/server/open_telemetry/trace_context.inc new file mode 100644 index 000000000..fd049aba2 --- /dev/null +++ b/extension/src/server/open_telemetry/trace_context.inc @@ -0,0 +1,262 @@ +static zend_bool king_server_telemetry_header_name_equals_ci( + zend_string *header_name, + const char *expected, + size_t expected_len +) +{ + size_t index; + + if (header_name == NULL || ZSTR_LEN(header_name) != expected_len) { + return 0; + } + + for (index = 0; index < expected_len; index++) { + if (tolower((unsigned char) ZSTR_VAL(header_name)[index]) + != tolower((unsigned char) expected[index])) { + return 0; + } + } + + return 1; +} + +static zval *king_server_telemetry_find_request_header( + zval *request, + const char *header_name, + size_t header_name_len +) +{ + zval *headers; + zval *entry; + zend_string *key; + zval *value; + + if (request == NULL || Z_TYPE_P(request) != IS_ARRAY) { + return NULL; + } + + headers = zend_hash_str_find( + Z_ARRVAL_P(request), + "headers", + sizeof("headers") - 1 + ); + if (headers == NULL || Z_TYPE_P(headers) != IS_ARRAY) { + return NULL; + } + + entry = zend_hash_str_find( + Z_ARRVAL_P(headers), + header_name, + header_name_len + ); + if (entry != NULL) { + return entry; + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(headers), key, value) { + if (king_server_telemetry_header_name_equals_ci( + key, + header_name, + header_name_len + )) { + return value; + } + } ZEND_HASH_FOREACH_END(); + + return NULL; +} + +static zend_string *king_server_telemetry_copy_first_header_string(zval *header_value) +{ + zval *entry; + + if (header_value == NULL) { + return NULL; + } + + if (Z_TYPE_P(header_value) == IS_STRING && Z_STRLEN_P(header_value) > 0) { + return zend_string_copy(Z_STR_P(header_value)); + } + + if (Z_TYPE_P(header_value) != IS_ARRAY) { + return NULL; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(header_value), entry) { + if (Z_TYPE_P(entry) == IS_STRING && Z_STRLEN_P(entry) > 0) { + return zend_string_copy(Z_STR_P(entry)); + } + } ZEND_HASH_FOREACH_END(); + + return NULL; +} + +static zend_string *king_server_telemetry_dup_lowercase_slice( + const char *value, + size_t value_len +) +{ + zend_string *normalized; + size_t index; + + normalized = zend_string_alloc(value_len, 0); + for (index = 0; index < value_len; index++) { + ZSTR_VAL(normalized)[index] = (char) tolower((unsigned char) value[index]); + } + ZSTR_VAL(normalized)[value_len] = '\0'; + + return normalized; +} + +static zend_bool king_server_telemetry_slice_is_hex( + const char *value, + size_t value_len +) +{ + size_t index; + + for (index = 0; index < value_len; index++) { + if (!isxdigit((unsigned char) value[index])) { + return 0; + } + } + + return 1; +} + +static zend_bool king_server_telemetry_slice_is_all_zeroes( + const char *value, + size_t value_len +) +{ + size_t index; + + for (index = 0; index < value_len; index++) { + if (value[index] != '0') { + return 0; + } + } + + return 1; +} + +static zend_result king_server_telemetry_extract_incoming_trace_context( + zval *request, + zval *destination +) +{ + zval *traceparent_value; + zval *tracestate_value; + zend_string *traceparent = NULL; + zend_string *trace_state = NULL; + const char *value; + + traceparent_value = king_server_telemetry_find_request_header( + request, + "traceparent", + sizeof("traceparent") - 1 + ); + if (traceparent_value == NULL) { + return FAILURE; + } + + traceparent = king_server_telemetry_copy_first_header_string(traceparent_value); + if (traceparent == NULL) { + return FAILURE; + } + + value = ZSTR_VAL(traceparent); + if (ZSTR_LEN(traceparent) != 55 + || value[2] != '-' + || value[35] != '-' + || value[52] != '-' + || !king_server_telemetry_slice_is_hex(value, 2) + || !king_server_telemetry_slice_is_hex(value + 3, 32) + || !king_server_telemetry_slice_is_hex(value + 36, 16) + || !king_server_telemetry_slice_is_hex(value + 53, 2) + || (tolower((unsigned char) value[0]) == 'f' + && tolower((unsigned char) value[1]) == 'f') + || king_server_telemetry_slice_is_all_zeroes(value + 3, 32) + || king_server_telemetry_slice_is_all_zeroes(value + 36, 16)) { + zend_string_release(traceparent); + return FAILURE; + } + + array_init(destination); + add_assoc_str( + destination, + "trace_id", + king_server_telemetry_dup_lowercase_slice(value + 3, 32) + ); + add_assoc_str( + destination, + "parent_span_id", + king_server_telemetry_dup_lowercase_slice(value + 36, 16) + ); + add_assoc_str( + destination, + "trace_flags", + king_server_telemetry_dup_lowercase_slice(value + 53, 2) + ); + + tracestate_value = king_server_telemetry_find_request_header( + request, + "tracestate", + sizeof("tracestate") - 1 + ); + trace_state = king_server_telemetry_copy_first_header_string(tracestate_value); + if (trace_state != NULL) { + add_assoc_str(destination, "trace_state", trace_state); + } + + zend_string_release(traceparent); + return SUCCESS; +} + +static zval *king_server_telemetry_find_request_incoming_trace_context(zval *request) +{ + zval *telemetry; + + if (request == NULL || Z_TYPE_P(request) != IS_ARRAY) { + return NULL; + } + + telemetry = zend_hash_str_find( + Z_ARRVAL_P(request), + "telemetry", + sizeof("telemetry") - 1 + ); + if (telemetry == NULL || Z_TYPE_P(telemetry) != IS_ARRAY) { + return NULL; + } + + return zend_hash_str_find( + Z_ARRVAL_P(telemetry), + "incoming_trace_context", + sizeof("incoming_trace_context") - 1 + ); +} + +static zend_result king_server_telemetry_parse_trace_flags( + zval *trace_flags, + uint8_t *parsed_flags +) +{ + char *endptr = NULL; + unsigned long parsed; + + if (trace_flags == NULL + || parsed_flags == NULL + || Z_TYPE_P(trace_flags) != IS_STRING + || Z_STRLEN_P(trace_flags) != 2) { + return FAILURE; + } + + parsed = strtoul(Z_STRVAL_P(trace_flags), &endptr, 16); + if (endptr == NULL || *endptr != '\0' || parsed > 0xffUL) { + return FAILURE; + } + + *parsed_flags = (uint8_t) parsed; + return SUCCESS; +} diff --git a/extension/src/server/php_binding.inc b/extension/src/server/php_binding.inc new file mode 100644 index 000000000..9846a2320 --- /dev/null +++ b/extension/src/server/php_binding.inc @@ -0,0 +1,6 @@ +/* + * Server PHP binding aggregation. The server runtime translation unit includes + * this module boundary instead of websocket server object-handler leaves. + */ + +#include "http1/websocket_server_object_handlers.inc" diff --git a/extension/src/server/registration.inc b/extension/src/server/registration.inc new file mode 100644 index 000000000..641deb009 --- /dev/null +++ b/extension/src/server/registration.inc @@ -0,0 +1,14 @@ +void king_server_register_websocket_classes(void) +{ + king_ce_ws_server = king_register_class_with_flags( + "King\\WebSocket\\Server", + NULL, + king_ws_server_class_methods, + ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES + ); +} + +void king_server_init_object_handlers(void) +{ + king_ws_server_object_handlers_init(); +} diff --git a/extension/src/server/session.c b/extension/src/server/session.c index 1587e033f..950776811 100644 --- a/extension/src/server/session.c +++ b/extension/src/server/session.c @@ -13,7 +13,7 @@ #include "php.h" #include "php_king.h" -#include "include/server/session.h" +#include "server/session.h" #include "zend_exceptions.h" #include diff --git a/extension/src/server/state.inc b/extension/src/server/state.inc new file mode 100644 index 000000000..bafe33406 --- /dev/null +++ b/extension/src/server/state.inc @@ -0,0 +1,5 @@ +/* + * Server module state storage aggregation. + */ + +#include "class_entries.inc" diff --git a/extension/src/server/tls.c b/extension/src/server/tls.c index 2e72fae0b..32e312ff6 100644 --- a/extension/src/server/tls.c +++ b/extension/src/server/tls.c @@ -16,8 +16,8 @@ #include "php.h" #include "php_king.h" -#include "include/client/session.h" -#include "include/server/tls.h" +#include "client/session.h" +#include "server/tls.h" #include "main/php_streams.h" #include diff --git a/extension/src/server/websocket.c b/extension/src/server/websocket.c index 689c04f06..cec702b0e 100644 --- a/extension/src/server/websocket.c +++ b/extension/src/server/websocket.c @@ -14,7 +14,7 @@ #include "php.h" #include "php_king.h" -#include "include/server/websocket.h" +#include "server/websocket.h" #include "main/php_network.h" #include "ext/standard/base64.h" diff --git a/extension/src/telemetry/metrics.c b/extension/src/telemetry/metrics.c index 9e1d8efec..d83346d81 100644 --- a/extension/src/telemetry/metrics.c +++ b/extension/src/telemetry/metrics.c @@ -4,7 +4,7 @@ * metrics together with pending telemetry signals for export. */ #include "php_king.h" -#include "include/telemetry/telemetry.h" +#include "telemetry/telemetry.h" static HashTable king_metrics_registry; static bool king_metrics_initialized = false; diff --git a/extension/src/telemetry/telemetry.c b/extension/src/telemetry/telemetry.c index 10fab623b..55fa9f11f 100644 --- a/extension/src/telemetry/telemetry.c +++ b/extension/src/telemetry/telemetry.c @@ -5,8 +5,8 @@ * span to userland. */ #include "php_king.h" -#include "include/telemetry/telemetry.h" -#include "include/runtime/libcurl_candidates.h" +#include "telemetry/telemetry.h" +#include "runtime/libcurl_candidates.h" #include #include #include @@ -20,7 +20,7 @@ #include #include #include "zend_smart_str.h" -#include "include/config/open_telemetry/base_layer.h" +#include "config/open_telemetry/base_layer.h" static king_telemetry_config_t king_telemetry_runtime_config; static bool king_telemetry_system_initialized = false; diff --git a/extension/src/telemetry/telemetry/pending_buffers_and_encoding.inc b/extension/src/telemetry/telemetry/pending_buffers_and_encoding.inc index b4cf9ef01..3f1d9f4f7 100644 --- a/extension/src/telemetry/telemetry/pending_buffers_and_encoding.inc +++ b/extension/src/telemetry/telemetry/pending_buffers_and_encoding.inc @@ -145,119 +145,7 @@ static void king_telemetry_reset_active_span(void) king_current_span = NULL; } -void king_telemetry_clear_incoming_parent_context(void) -{ - memset( - &king_telemetry_incoming_parent_context, - 0, - sizeof(king_telemetry_incoming_parent_context) - ); -} - -zend_result king_telemetry_set_incoming_parent_context( - const char *trace_id, - const char *parent_span_id, - uint8_t trace_flags, - const char *trace_state -) -{ - if (trace_id == NULL - || parent_span_id == NULL - || strlen(trace_id) != 32 - || strlen(parent_span_id) != 16) { - return FAILURE; - } - - king_telemetry_clear_incoming_parent_context(); - strncpy( - king_telemetry_incoming_parent_context.trace_id, - trace_id, - sizeof(king_telemetry_incoming_parent_context.trace_id) - 1 - ); - strncpy( - king_telemetry_incoming_parent_context.parent_span_id, - parent_span_id, - sizeof(king_telemetry_incoming_parent_context.parent_span_id) - 1 - ); - king_telemetry_incoming_parent_context.trace_flags = trace_flags; - - if (trace_state != NULL && trace_state[0] != '\0') { - if (strlen(trace_state) - >= sizeof(king_telemetry_incoming_parent_context.trace_state)) { - king_telemetry_clear_incoming_parent_context(); - return FAILURE; - } - - strncpy( - king_telemetry_incoming_parent_context.trace_state, - trace_state, - sizeof(king_telemetry_incoming_parent_context.trace_state) - 1 - ); - } - - king_telemetry_incoming_parent_context.active = 1; - return SUCCESS; -} - -zend_result king_telemetry_set_incoming_parent_context_from_array(zval *context) -{ - zval *trace_id; - zval *parent_span_id; - zval *trace_flags; - zval *trace_state; - zend_long parsed_trace_flags = 1; - - if (context == NULL || Z_TYPE_P(context) != IS_ARRAY) { - return FAILURE; - } - - trace_id = zend_hash_str_find( - Z_ARRVAL_P(context), - "trace_id", - sizeof("trace_id") - 1 - ); - parent_span_id = zend_hash_str_find( - Z_ARRVAL_P(context), - "parent_span_id", - sizeof("parent_span_id") - 1 - ); - trace_flags = zend_hash_str_find( - Z_ARRVAL_P(context), - "trace_flags", - sizeof("trace_flags") - 1 - ); - trace_state = zend_hash_str_find( - Z_ARRVAL_P(context), - "trace_state", - sizeof("trace_state") - 1 - ); - - if (trace_flags != NULL) { - if (Z_TYPE_P(trace_flags) == IS_LONG) { - parsed_trace_flags = Z_LVAL_P(trace_flags); - } else if (Z_TYPE_P(trace_flags) == IS_STRING && Z_STRLEN_P(trace_flags) > 0) { - parsed_trace_flags = ZEND_STRTOL(Z_STRVAL_P(trace_flags), NULL, 10); - } - } - - if ( - trace_id == NULL - || Z_TYPE_P(trace_id) != IS_STRING - || parent_span_id == NULL - || Z_TYPE_P(parent_span_id) != IS_STRING - ) { - return FAILURE; - } - - return king_telemetry_set_incoming_parent_context( - Z_STRVAL_P(trace_id), - Z_STRVAL_P(parent_span_id), - (uint8_t) (parsed_trace_flags & 0xff), - (trace_state != NULL && Z_TYPE_P(trace_state) == IS_STRING) - ? Z_STRVAL_P(trace_state) - : NULL - ); -} +#include "pending_buffers_and_encoding/parent_context.inc" void king_telemetry_cleanup_scope_state(void) { @@ -786,180 +674,7 @@ static void king_telemetry_json_append_attributes( smart_str_appends(json_payload, "]"); } -static zend_result king_telemetry_config_apply_inline_array( - king_telemetry_config_t *config, - zval *config_arr) -{ - zval *value; - const char *validation_error; - - if (config == NULL || config_arr == NULL || Z_TYPE_P(config_arr) != IS_ARRAY) { - return SUCCESS; - } - - value = zend_hash_str_find(Z_ARRVAL_P(config_arr), "enabled", sizeof("enabled") - 1); - if (value != NULL) { - config->enabled = zend_is_true(value) ? 1 : 0; - } - - value = zend_hash_str_find( - Z_ARRVAL_P(config_arr), - "otel_exporter_endpoint", - sizeof("otel_exporter_endpoint") - 1 - ); - if (value != NULL && Z_TYPE_P(value) == IS_STRING) { - validation_error = king_open_telemetry_validate_exporter_endpoint_value( - Z_STRVAL_P(value), - Z_STRLEN_P(value) - ); - if (validation_error != NULL) { - zend_throw_exception_ex( - spl_ce_InvalidArgumentException, - 0, - "%s", - validation_error - ); - return FAILURE; - } - - snprintf( - config->otel_exporter_endpoint, - sizeof(config->otel_exporter_endpoint), - "%s", - Z_STRVAL_P(value) - ); - } - - value = zend_hash_str_find( - Z_ARRVAL_P(config_arr), - "otel_exporter_headers", - sizeof("otel_exporter_headers") - 1 - ); - if (value != NULL && Z_TYPE_P(value) == IS_STRING) { - validation_error = king_open_telemetry_validate_exporter_headers_value( - Z_STRVAL_P(value), - Z_STRLEN_P(value) - ); - if (validation_error != NULL) { - zend_throw_exception_ex( - spl_ce_InvalidArgumentException, - 0, - "%s", - validation_error - ); - return FAILURE; - } - } - - value = zend_hash_str_find( - Z_ARRVAL_P(config_arr), - "otel_exporter_protocol", - sizeof("otel_exporter_protocol") - 1 - ); - if (value != NULL && Z_TYPE_P(value) == IS_STRING) { - snprintf( - config->otel_exporter_protocol, - sizeof(config->otel_exporter_protocol), - "%s", - Z_STRVAL_P(value) - ); - } - - value = zend_hash_str_find( - Z_ARRVAL_P(config_arr), - "service_name", - sizeof("service_name") - 1 - ); - if (value != NULL && Z_TYPE_P(value) == IS_STRING) { - snprintf( - config->service_name, - sizeof(config->service_name), - "%s", - Z_STRVAL_P(value) - ); - } - - value = zend_hash_str_find( - Z_ARRVAL_P(config_arr), - "traces_sampler_type", - sizeof("traces_sampler_type") - 1 - ); - if (value != NULL - && Z_TYPE_P(value) == IS_STRING - && ( - strcmp(Z_STRVAL_P(value), "parent_based_probability") == 0 - || strcmp(Z_STRVAL_P(value), "always_on") == 0 - || strcmp(Z_STRVAL_P(value), "always_off") == 0 - )) { - snprintf( - config->traces_sampler_type, - sizeof(config->traces_sampler_type), - "%s", - Z_STRVAL_P(value) - ); - } - - value = zend_hash_str_find( - Z_ARRVAL_P(config_arr), - "traces_sampler_ratio", - sizeof("traces_sampler_ratio") - 1 - ); - if (value != NULL) { - double sampler_ratio = zval_get_double(value); - - if (sampler_ratio >= 0.0 && sampler_ratio <= 1.0) { - config->traces_sampler_ratio = sampler_ratio; - config->traces_sampler_ratio_configured = 1; - } - } - - value = zend_hash_str_find( - Z_ARRVAL_P(config_arr), - "batch_processor_max_queue_size", - sizeof("batch_processor_max_queue_size") - 1 - ); - if (value != NULL) { - zend_long queue_limit = zval_get_long(value); - - if (queue_limit > 0) { - config->max_queue_size = queue_limit > UINT32_MAX - ? UINT32_MAX - : (uint32_t) queue_limit; - } - } - - value = zend_hash_str_find( - Z_ARRVAL_P(config_arr), - "logs_exporter_batch_size", - sizeof("logs_exporter_batch_size") - 1 - ); - if (value != NULL) { - zend_long batch_size = zval_get_long(value); - - if (batch_size > 0) { - config->max_export_batch_size = batch_size > UINT32_MAX - ? UINT32_MAX - : (uint32_t) batch_size; - } - } - - value = zend_hash_str_find( - Z_ARRVAL_P(config_arr), - "exporter_timeout_ms", - sizeof("exporter_timeout_ms") - 1 - ); - if (value != NULL) { - zend_long timeout_ms = zval_get_long(value); - - if (timeout_ms > 0) { - config->batch_timeout_ms = timeout_ms > UINT32_MAX - ? UINT32_MAX - : (uint32_t) timeout_ms; - } - } - - return SUCCESS; -} +#include "pending_buffers_and_encoding/inline_config.inc" static const char *king_telemetry_metric_type_from_zval(zval *type_zval) { diff --git a/extension/src/telemetry/telemetry/pending_buffers_and_encoding/inline_config.inc b/extension/src/telemetry/telemetry/pending_buffers_and_encoding/inline_config.inc new file mode 100644 index 000000000..8608c4dad --- /dev/null +++ b/extension/src/telemetry/telemetry/pending_buffers_and_encoding/inline_config.inc @@ -0,0 +1,174 @@ +static zend_result king_telemetry_config_apply_inline_array( + king_telemetry_config_t *config, + zval *config_arr) +{ + zval *value; + const char *validation_error; + + if (config == NULL || config_arr == NULL || Z_TYPE_P(config_arr) != IS_ARRAY) { + return SUCCESS; + } + + value = zend_hash_str_find(Z_ARRVAL_P(config_arr), "enabled", sizeof("enabled") - 1); + if (value != NULL) { + config->enabled = zend_is_true(value) ? 1 : 0; + } + + value = zend_hash_str_find( + Z_ARRVAL_P(config_arr), + "otel_exporter_endpoint", + sizeof("otel_exporter_endpoint") - 1 + ); + if (value != NULL && Z_TYPE_P(value) == IS_STRING) { + validation_error = king_open_telemetry_validate_exporter_endpoint_value( + Z_STRVAL_P(value), + Z_STRLEN_P(value) + ); + if (validation_error != NULL) { + zend_throw_exception_ex( + spl_ce_InvalidArgumentException, + 0, + "%s", + validation_error + ); + return FAILURE; + } + + snprintf( + config->otel_exporter_endpoint, + sizeof(config->otel_exporter_endpoint), + "%s", + Z_STRVAL_P(value) + ); + } + + value = zend_hash_str_find( + Z_ARRVAL_P(config_arr), + "otel_exporter_headers", + sizeof("otel_exporter_headers") - 1 + ); + if (value != NULL && Z_TYPE_P(value) == IS_STRING) { + validation_error = king_open_telemetry_validate_exporter_headers_value( + Z_STRVAL_P(value), + Z_STRLEN_P(value) + ); + if (validation_error != NULL) { + zend_throw_exception_ex( + spl_ce_InvalidArgumentException, + 0, + "%s", + validation_error + ); + return FAILURE; + } + } + + value = zend_hash_str_find( + Z_ARRVAL_P(config_arr), + "otel_exporter_protocol", + sizeof("otel_exporter_protocol") - 1 + ); + if (value != NULL && Z_TYPE_P(value) == IS_STRING) { + snprintf( + config->otel_exporter_protocol, + sizeof(config->otel_exporter_protocol), + "%s", + Z_STRVAL_P(value) + ); + } + + value = zend_hash_str_find( + Z_ARRVAL_P(config_arr), + "service_name", + sizeof("service_name") - 1 + ); + if (value != NULL && Z_TYPE_P(value) == IS_STRING) { + snprintf( + config->service_name, + sizeof(config->service_name), + "%s", + Z_STRVAL_P(value) + ); + } + + value = zend_hash_str_find( + Z_ARRVAL_P(config_arr), + "traces_sampler_type", + sizeof("traces_sampler_type") - 1 + ); + if (value != NULL + && Z_TYPE_P(value) == IS_STRING + && ( + strcmp(Z_STRVAL_P(value), "parent_based_probability") == 0 + || strcmp(Z_STRVAL_P(value), "always_on") == 0 + || strcmp(Z_STRVAL_P(value), "always_off") == 0 + )) { + snprintf( + config->traces_sampler_type, + sizeof(config->traces_sampler_type), + "%s", + Z_STRVAL_P(value) + ); + } + + value = zend_hash_str_find( + Z_ARRVAL_P(config_arr), + "traces_sampler_ratio", + sizeof("traces_sampler_ratio") - 1 + ); + if (value != NULL) { + double sampler_ratio = zval_get_double(value); + + if (sampler_ratio >= 0.0 && sampler_ratio <= 1.0) { + config->traces_sampler_ratio = sampler_ratio; + config->traces_sampler_ratio_configured = 1; + } + } + + value = zend_hash_str_find( + Z_ARRVAL_P(config_arr), + "batch_processor_max_queue_size", + sizeof("batch_processor_max_queue_size") - 1 + ); + if (value != NULL) { + zend_long queue_limit = zval_get_long(value); + + if (queue_limit > 0) { + config->max_queue_size = queue_limit > UINT32_MAX + ? UINT32_MAX + : (uint32_t) queue_limit; + } + } + + value = zend_hash_str_find( + Z_ARRVAL_P(config_arr), + "logs_exporter_batch_size", + sizeof("logs_exporter_batch_size") - 1 + ); + if (value != NULL) { + zend_long batch_size = zval_get_long(value); + + if (batch_size > 0) { + config->max_export_batch_size = batch_size > UINT32_MAX + ? UINT32_MAX + : (uint32_t) batch_size; + } + } + + value = zend_hash_str_find( + Z_ARRVAL_P(config_arr), + "exporter_timeout_ms", + sizeof("exporter_timeout_ms") - 1 + ); + if (value != NULL) { + zend_long timeout_ms = zval_get_long(value); + + if (timeout_ms > 0) { + config->batch_timeout_ms = timeout_ms > UINT32_MAX + ? UINT32_MAX + : (uint32_t) timeout_ms; + } + } + + return SUCCESS; +} diff --git a/extension/src/telemetry/telemetry/pending_buffers_and_encoding/parent_context.inc b/extension/src/telemetry/telemetry/pending_buffers_and_encoding/parent_context.inc new file mode 100644 index 000000000..8485792ff --- /dev/null +++ b/extension/src/telemetry/telemetry/pending_buffers_and_encoding/parent_context.inc @@ -0,0 +1,113 @@ +void king_telemetry_clear_incoming_parent_context(void) +{ + memset( + &king_telemetry_incoming_parent_context, + 0, + sizeof(king_telemetry_incoming_parent_context) + ); +} + +zend_result king_telemetry_set_incoming_parent_context( + const char *trace_id, + const char *parent_span_id, + uint8_t trace_flags, + const char *trace_state +) +{ + if (trace_id == NULL + || parent_span_id == NULL + || strlen(trace_id) != 32 + || strlen(parent_span_id) != 16) { + return FAILURE; + } + + king_telemetry_clear_incoming_parent_context(); + strncpy( + king_telemetry_incoming_parent_context.trace_id, + trace_id, + sizeof(king_telemetry_incoming_parent_context.trace_id) - 1 + ); + strncpy( + king_telemetry_incoming_parent_context.parent_span_id, + parent_span_id, + sizeof(king_telemetry_incoming_parent_context.parent_span_id) - 1 + ); + king_telemetry_incoming_parent_context.trace_flags = trace_flags; + + if (trace_state != NULL && trace_state[0] != '\0') { + if (strlen(trace_state) + >= sizeof(king_telemetry_incoming_parent_context.trace_state)) { + king_telemetry_clear_incoming_parent_context(); + return FAILURE; + } + + strncpy( + king_telemetry_incoming_parent_context.trace_state, + trace_state, + sizeof(king_telemetry_incoming_parent_context.trace_state) - 1 + ); + } + + king_telemetry_incoming_parent_context.active = 1; + return SUCCESS; +} + +zend_result king_telemetry_set_incoming_parent_context_from_array(zval *context) +{ + zval *trace_id; + zval *parent_span_id; + zval *trace_flags; + zval *trace_state; + zend_long parsed_trace_flags = 1; + + if (context == NULL || Z_TYPE_P(context) != IS_ARRAY) { + return FAILURE; + } + + trace_id = zend_hash_str_find( + Z_ARRVAL_P(context), + "trace_id", + sizeof("trace_id") - 1 + ); + parent_span_id = zend_hash_str_find( + Z_ARRVAL_P(context), + "parent_span_id", + sizeof("parent_span_id") - 1 + ); + trace_flags = zend_hash_str_find( + Z_ARRVAL_P(context), + "trace_flags", + sizeof("trace_flags") - 1 + ); + trace_state = zend_hash_str_find( + Z_ARRVAL_P(context), + "trace_state", + sizeof("trace_state") - 1 + ); + + if (trace_flags != NULL) { + if (Z_TYPE_P(trace_flags) == IS_LONG) { + parsed_trace_flags = Z_LVAL_P(trace_flags); + } else if (Z_TYPE_P(trace_flags) == IS_STRING && Z_STRLEN_P(trace_flags) > 0) { + parsed_trace_flags = ZEND_STRTOL(Z_STRVAL_P(trace_flags), NULL, 10); + } + } + + if ( + trace_id == NULL + || Z_TYPE_P(trace_id) != IS_STRING + || parent_span_id == NULL + || Z_TYPE_P(parent_span_id) != IS_STRING + ) { + return FAILURE; + } + + return king_telemetry_set_incoming_parent_context( + Z_STRVAL_P(trace_id), + Z_STRVAL_P(parent_span_id), + (uint8_t) (parsed_trace_flags & 0xff), + (trace_state != NULL && Z_TYPE_P(trace_state) == IS_STRING) + ? Z_STRVAL_P(trace_state) + : NULL + ); +} diff --git a/extension/src/telemetry/telemetry/queue_and_span_runtime.inc b/extension/src/telemetry/telemetry/queue_and_span_runtime.inc index 8498ec0b2..2359a6db9 100644 --- a/extension/src/telemetry/telemetry/queue_and_span_runtime.inc +++ b/extension/src/telemetry/telemetry/queue_and_span_runtime.inc @@ -442,4 +442,4 @@ int king_telemetry_log_internal(king_telemetry_level_t level, const char *logger /* --- PHP Entry Points --- */ -#include "include/king_globals.h" +#include "php_king/globals.h" diff --git a/extension/src/validation/config_param/validate_bool.c b/extension/src/validation/config_param/validate_bool.c old mode 100755 new mode 100644 index 298799a27..590778f62 --- a/extension/src/validation/config_param/validate_bool.c +++ b/extension/src/validation/config_param/validate_bool.c @@ -3,7 +3,7 @@ * checks and raises the shared validation error when the value is not bool. */ -#include "include/validation/config_param/validate_bool.h" +#include "validation/config_param/validate_bool.h" #include "php.h" #include #include diff --git a/extension/src/validation/config_param/validate_colon_separated_string_from_allowlist.c b/extension/src/validation/config_param/validate_colon_separated_string_from_allowlist.c old mode 100755 new mode 100644 index 5efea1f63..4229e4da2 --- a/extension/src/validation/config_param/validate_colon_separated_string_from_allowlist.c +++ b/extension/src/validation/config_param/validate_colon_separated_string_from_allowlist.c @@ -2,7 +2,7 @@ * Validation helper for colon-separated string lists. Enforces string input * and verifies that every token belongs to the provided allowlist. */ -#include "include/validation/config_param/validate_colon_separated_string_from_allowlist.h" +#include "validation/config_param/validate_colon_separated_string_from_allowlist.h" #include #include /* spl_ce_InvalidArgumentException */ diff --git a/extension/src/validation/config_param/validate_comma_separated_numeric_string.c b/extension/src/validation/config_param/validate_comma_separated_numeric_string.c old mode 100755 new mode 100644 index d9dbaf8e8..140b9799e --- a/extension/src/validation/config_param/validate_comma_separated_numeric_string.c +++ b/extension/src/validation/config_param/validate_comma_separated_numeric_string.c @@ -3,7 +3,7 @@ * and keeps the bounded numeric-token validation used by config parsing. */ -#include "include/validation/config_param/validate_comma_separated_numeric_string.h" +#include "validation/config_param/validate_comma_separated_numeric_string.h" #include "ext/standard/php_string.h" #include "zend_exceptions.h" #include diff --git a/extension/src/validation/config_param/validate_comma_separated_string_from_allowlist.c b/extension/src/validation/config_param/validate_comma_separated_string_from_allowlist.c old mode 100755 new mode 100644 index c6076be79..6c266774f --- a/extension/src/validation/config_param/validate_comma_separated_string_from_allowlist.c +++ b/extension/src/validation/config_param/validate_comma_separated_string_from_allowlist.c @@ -3,7 +3,7 @@ * and verifies that every token belongs to the provided allowlist. */ -#include "include/validation/config_param/validate_comma_separated_string_from_allowlist.h" +#include "validation/config_param/validate_comma_separated_string_from_allowlist.h" #include "ext/standard/php_string.h" #include "zend_exceptions.h" #include diff --git a/extension/src/validation/config_param/validate_cors_origin_string.c b/extension/src/validation/config_param/validate_cors_origin_string.c old mode 100755 new mode 100644 index 936e9797e..78616f8a9 --- a/extension/src/validation/config_param/validate_cors_origin_string.c +++ b/extension/src/validation/config_param/validate_cors_origin_string.c @@ -3,7 +3,7 @@ * bounded origin-shape checks used by the current server config surface. */ -#include "include/validation/config_param/validate_cors_origin_string.h" +#include "validation/config_param/validate_cors_origin_string.h" #include "ext/standard/php_string.h" /* For php_trim */ #include "ext/standard/url.h" /* For php_url_parse_ex */ diff --git a/extension/src/validation/config_param/validate_cpu_affinity_map_string.c b/extension/src/validation/config_param/validate_cpu_affinity_map_string.c old mode 100755 new mode 100644 index 92e828dd1..4cc9d287c --- a/extension/src/validation/config_param/validate_cpu_affinity_map_string.c +++ b/extension/src/validation/config_param/validate_cpu_affinity_map_string.c @@ -3,7 +3,7 @@ * the bounded CPU-set grammar accepted by the cluster/process config surface. */ -#include "include/validation/config_param/validate_cpu_affinity_map_string.h" +#include "validation/config_param/validate_cpu_affinity_map_string.h" #include "ext/standard/php_string.h" #include "zend_exceptions.h" #include diff --git a/extension/src/validation/config_param/validate_double_range.c b/extension/src/validation/config_param/validate_double_range.c old mode 100755 new mode 100644 index 1e54481e7..a6ccbc1ce --- a/extension/src/validation/config_param/validate_double_range.c +++ b/extension/src/validation/config_param/validate_double_range.c @@ -3,7 +3,7 @@ * and the inclusive min/max bounds supplied by the caller. */ -#include "include/validation/config_param/validate_double_range.h" +#include "validation/config_param/validate_double_range.h" #include #include diff --git a/extension/src/validation/config_param/validate_erasure_coding_shards_string.c b/extension/src/validation/config_param/validate_erasure_coding_shards_string.c old mode 100755 new mode 100644 index 7ac18f060..9167bfb6c --- a/extension/src/validation/config_param/validate_erasure_coding_shards_string.c +++ b/extension/src/validation/config_param/validate_erasure_coding_shards_string.c @@ -3,7 +3,7 @@ * and the shard-layout grammar accepted by the object-store config surface. */ -#include "include/validation/config_param/validate_erasure_coding_shards_string.h" +#include "validation/config_param/validate_erasure_coding_shards_string.h" #include #include #include diff --git a/extension/src/validation/config_param/validate_generic_string.c b/extension/src/validation/config_param/validate_generic_string.c old mode 100755 new mode 100644 index 9dfc0def6..8a3b49b7a --- a/extension/src/validation/config_param/validate_generic_string.c +++ b/extension/src/validation/config_param/validate_generic_string.c @@ -3,7 +3,7 @@ * returns a duplicated value suitable for longer-lived config state. */ -#include "include/validation/config_param/validate_generic_string.h" +#include "validation/config_param/validate_generic_string.h" #include #include /* For spl_ce_InvalidArgumentException */ diff --git a/extension/src/validation/config_param/validate_host_string.c b/extension/src/validation/config_param/validate_host_string.c old mode 100755 new mode 100644 index cd8b4c6d8..df8714663 --- a/extension/src/validation/config_param/validate_host_string.c +++ b/extension/src/validation/config_param/validate_host_string.c @@ -3,7 +3,7 @@ * host-literal checks used by the current transport and control-plane config. */ -#include "include/validation/config_param/validate_host_string.h" +#include "validation/config_param/validate_host_string.h" #include #include #include diff --git a/extension/src/validation/config_param/validate_long_range.c b/extension/src/validation/config_param/validate_long_range.c old mode 100755 new mode 100644 index cf4cfd2de..80e574b52 --- a/extension/src/validation/config_param/validate_long_range.c +++ b/extension/src/validation/config_param/validate_long_range.c @@ -3,7 +3,7 @@ * inclusive min/max bounds supplied by the caller. */ -#include "include/validation/config_param/validate_long_range.h" +#include "validation/config_param/validate_long_range.h" #include "php.h" #include #include diff --git a/extension/src/validation/config_param/validate_niceness_value.c b/extension/src/validation/config_param/validate_niceness_value.c old mode 100755 new mode 100644 index 2a54429af..58035dc28 --- a/extension/src/validation/config_param/validate_niceness_value.c +++ b/extension/src/validation/config_param/validate_niceness_value.c @@ -3,7 +3,7 @@ * bounded priority range accepted by the cluster/process config surface. */ -#include "include/validation/config_param/validate_niceness_value.h" +#include "validation/config_param/validate_niceness_value.h" #include #include /* For spl_ce_InvalidArgumentException */ diff --git a/extension/src/validation/config_param/validate_non_negative_long.c b/extension/src/validation/config_param/validate_non_negative_long.c old mode 100755 new mode 100644 index 3305b9e1d..b5a8d326a --- a/extension/src/validation/config_param/validate_non_negative_long.c +++ b/extension/src/validation/config_param/validate_non_negative_long.c @@ -3,7 +3,7 @@ * `>= 0` contract used by multiple config families. */ -#include "include/validation/config_param/validate_non_negative_long.h" +#include "validation/config_param/validate_non_negative_long.h" #include #include /* For spl_ce_InvalidArgumentException */ diff --git a/extension/src/validation/config_param/validate_positive_long.c b/extension/src/validation/config_param/validate_positive_long.c old mode 100755 new mode 100644 index d8b17a02a..7d5e8978f --- a/extension/src/validation/config_param/validate_positive_long.c +++ b/extension/src/validation/config_param/validate_positive_long.c @@ -3,7 +3,7 @@ * the `> 0` contract used by multiple config families. */ -#include "include/validation/config_param/validate_positive_long.h" +#include "validation/config_param/validate_positive_long.h" #include "php.h" #include #include diff --git a/extension/src/validation/config_param/validate_readable_file_path.c b/extension/src/validation/config_param/validate_readable_file_path.c old mode 100755 new mode 100644 index e10fadfb2..00c9e83b6 --- a/extension/src/validation/config_param/validate_readable_file_path.c +++ b/extension/src/validation/config_param/validate_readable_file_path.c @@ -3,7 +3,7 @@ * the current `VCWD_ACCESS(..., R_OK)` readability check used by config paths. */ -#include "include/validation/config_param/validate_readable_file_path.h" +#include "validation/config_param/validate_readable_file_path.h" #include "main/php_streams.h" #include #include diff --git a/extension/src/validation/config_param/validate_scale_up_policy_string.c b/extension/src/validation/config_param/validate_scale_up_policy_string.c old mode 100755 new mode 100644 index a8e77ba51..2951a30c4 --- a/extension/src/validation/config_param/validate_scale_up_policy_string.c +++ b/extension/src/validation/config_param/validate_scale_up_policy_string.c @@ -3,7 +3,7 @@ * input and the bounded policy names accepted by the autoscaling config. */ -#include "include/validation/config_param/validate_scale_up_policy_string.h" +#include "validation/config_param/validate_scale_up_policy_string.h" #include "ext/standard/php_string.h" #include "zend_exceptions.h" #include diff --git a/extension/src/validation/config_param/validate_scheduler_policy.c b/extension/src/validation/config_param/validate_scheduler_policy.c old mode 100755 new mode 100644 index de1ddbbdc..73e9f868b --- a/extension/src/validation/config_param/validate_scheduler_policy.c +++ b/extension/src/validation/config_param/validate_scheduler_policy.c @@ -3,7 +3,7 @@ * and the bounded scheduler names accepted by the cluster/process config. */ -#include "include/validation/config_param/validate_scheduler_policy.h" +#include "validation/config_param/validate_scheduler_policy.h" #include #include /* For spl_ce_InvalidArgumentException */ diff --git a/extension/src/validation/config_param/validate_string.c b/extension/src/validation/config_param/validate_string.c old mode 100755 new mode 100644 index f447e16c8..f0bfb3843 --- a/extension/src/validation/config_param/validate_string.c +++ b/extension/src/validation/config_param/validate_string.c @@ -3,7 +3,7 @@ * a duplicated value suitable for persistent module-config storage. */ -#include "include/validation/config_param/validate_string.h" +#include "validation/config_param/validate_string.h" #include #include /* spl_ce_InvalidArgumentException */ diff --git a/extension/src/validation/config_param/validate_string_from_allowlist.c b/extension/src/validation/config_param/validate_string_from_allowlist.c old mode 100755 new mode 100644 index 75282e456..d1d13d080 --- a/extension/src/validation/config_param/validate_string_from_allowlist.c +++ b/extension/src/validation/config_param/validate_string_from_allowlist.c @@ -3,7 +3,7 @@ * Enforces string input and bounded membership checks against allowed values. */ -#include "include/validation/config_param/validate_string_from_allowlist.h" +#include "validation/config_param/validate_string_from_allowlist.h" #include #include diff --git a/extension/src/xslt/class_entries.inc b/extension/src/xslt/class_entries.inc new file mode 100644 index 000000000..6b7278f5a --- /dev/null +++ b/extension/src/xslt/class_entries.inc @@ -0,0 +1,5 @@ +/* + * XSLT PHP class-entry storage. + */ + +zend_class_entry *king_ce_xslt_processor = NULL; diff --git a/extension/src/xslt/php_binding.inc b/extension/src/xslt/php_binding.inc new file mode 100644 index 000000000..b9e6c2415 --- /dev/null +++ b/extension/src/xslt/php_binding.inc @@ -0,0 +1,213 @@ +/* + * King\XSLT\Processor PHP object surface. The XSLT translation unit includes + * this object boundary beside the native SaxonC runtime used by king_xslt_*. + */ + +#include "xslt/xslt.h" + +static zend_object_handlers king_xslt_processor_object_handlers; + +static void king_xslt_processor_object_free(zend_object *object) +{ + king_xslt_processor_object *intern = php_king_xslt_processor_obj_from_zend(object); + + if (!Z_ISUNDEF(intern->options)) { + zval_ptr_dtor(&intern->options); + ZVAL_UNDEF(&intern->options); + } + + zend_object_std_dtor(&intern->std); +} + +static zend_object *king_xslt_processor_object_create(zend_class_entry *ce) +{ + king_xslt_processor_object *intern = zend_object_alloc(sizeof(*intern), ce); + + ZVAL_UNDEF(&intern->options); + array_init(&intern->options); + zend_object_std_init(&intern->std, ce); + object_properties_init(&intern->std, ce); + intern->std.handlers = &king_xslt_processor_object_handlers; + + return &intern->std; +} + +static void king_xslt_processor_object_handlers_init(void) +{ + memcpy( + &king_xslt_processor_object_handlers, + &std_object_handlers, + sizeof(zend_object_handlers) + ); + king_xslt_processor_object_handlers.offset = XtOffsetOf(king_xslt_processor_object, std); + king_xslt_processor_object_handlers.free_obj = king_xslt_processor_object_free; + king_xslt_processor_object_handlers.clone_obj = NULL; + king_ce_xslt_processor->create_object = king_xslt_processor_object_create; +} + +static void king_xslt_processor_merge_nested_properties( + zval *merged, + zval *base_options, + zval *call_options +) +{ + zval *base_properties; + zval *call_properties; + zval properties; + + base_properties = zend_hash_str_find( + Z_ARRVAL_P(base_options), + "properties", + sizeof("properties") - 1 + ); + call_properties = zend_hash_str_find( + Z_ARRVAL_P(call_options), + "properties", + sizeof("properties") - 1 + ); + + if (base_properties == NULL + || call_properties == NULL + || Z_TYPE_P(base_properties) != IS_ARRAY + || Z_TYPE_P(call_properties) != IS_ARRAY) { + return; + } + + array_init(&properties); + zend_hash_copy(Z_ARRVAL(properties), Z_ARRVAL_P(base_properties), zval_add_ref); + zend_hash_merge(Z_ARRVAL(properties), Z_ARRVAL_P(call_properties), zval_add_ref, 1); + zend_hash_str_update(Z_ARRVAL_P(merged), "properties", sizeof("properties") - 1, &properties); +} + +static zval *king_xslt_processor_effective_options( + king_xslt_processor_object *intern, + zval *options, + zval *merged +) +{ + if (Z_ISUNDEF(intern->options) + || Z_TYPE(intern->options) != IS_ARRAY + || zend_hash_num_elements(Z_ARRVAL(intern->options)) == 0) { + return options; + } + + if (options == NULL || Z_TYPE_P(options) == IS_NULL) { + return &intern->options; + } + + array_init(merged); + zend_hash_copy(Z_ARRVAL_P(merged), Z_ARRVAL(intern->options), zval_add_ref); + zend_hash_merge(Z_ARRVAL_P(merged), Z_ARRVAL_P(options), zval_add_ref, 1); + king_xslt_processor_merge_nested_properties(merged, &intern->options, options); + + return merged; +} + +PHP_METHOD(King_XSLT_Processor, __construct) +{ + zval *options = NULL; + king_xslt_processor_object *intern; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + if (options == NULL || Z_TYPE_P(options) == IS_NULL) { + return; + } + if (king_xslt_validate_options(options) != SUCCESS) { + RETURN_THROWS(); + } + + intern = php_king_xslt_processor_obj_from_zend(Z_OBJ_P(ZEND_THIS)); + zval_ptr_dtor(&intern->options); + ZVAL_COPY(&intern->options, options); +} + +PHP_METHOD(King_XSLT_Processor, getOptions) +{ + king_xslt_processor_object *intern; + + ZEND_PARSE_PARAMETERS_NONE(); + + intern = php_king_xslt_processor_obj_from_zend(Z_OBJ_P(ZEND_THIS)); + RETURN_ZVAL(&intern->options, 1, 0); +} + +PHP_METHOD(King_XSLT_Processor, engineStatus) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + king_xslt_engine_status_array(return_value); +} + +PHP_METHOD(King_XSLT_Processor, transformFile) +{ + zend_string *source_path; + zend_string *stylesheet_path; + zval *options = NULL; + zval merged; + zval *effective_options; + king_xslt_processor_object *intern; + + ZVAL_UNDEF(&merged); + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STR(source_path) + Z_PARAM_STR(stylesheet_path) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + intern = php_king_xslt_processor_obj_from_zend(Z_OBJ_P(ZEND_THIS)); + effective_options = king_xslt_processor_effective_options(intern, options, &merged); + + if (king_xslt_transform_file_result(source_path, stylesheet_path, effective_options, return_value) != SUCCESS) { + if (effective_options == &merged) { + zval_ptr_dtor(&merged); + } + RETURN_THROWS(); + } + + if (effective_options == &merged) { + zval_ptr_dtor(&merged); + } +} + +PHP_METHOD(King_XSLT_Processor, transformToFile) +{ + zend_string *source_path; + zend_string *stylesheet_path; + zend_string *output_path; + zval *options = NULL; + zval merged; + zval *effective_options; + king_xslt_processor_object *intern; + + ZVAL_UNDEF(&merged); + + ZEND_PARSE_PARAMETERS_START(3, 4) + Z_PARAM_STR(source_path) + Z_PARAM_STR(stylesheet_path) + Z_PARAM_STR(output_path) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + intern = php_king_xslt_processor_obj_from_zend(Z_OBJ_P(ZEND_THIS)); + effective_options = king_xslt_processor_effective_options(intern, options, &merged); + + if (king_xslt_transform_to_file_result(source_path, stylesheet_path, output_path, effective_options, return_value) != SUCCESS) { + if (effective_options == &merged) { + zval_ptr_dtor(&merged); + } + RETURN_THROWS(); + } + + if (effective_options == &merged) { + zval_ptr_dtor(&merged); + } +} + +#include "xslt/class_method_entries.h" diff --git a/extension/src/xslt/registration.inc b/extension/src/xslt/registration.inc new file mode 100644 index 000000000..fd620a80e --- /dev/null +++ b/extension/src/xslt/registration.inc @@ -0,0 +1,19 @@ +/* + * XSLT registration aggregation. The XSLT translation unit includes this + * module boundary instead of leaving registration in the core bootstrap. + */ + +void king_xslt_register_classes(void) +{ + king_ce_xslt_processor = king_register_class_with_flags( + "King\\XSLT\\Processor", + NULL, + king_xslt_processor_class_methods, + ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES + ); +} + +void king_xslt_init_object_handlers(void) +{ + king_xslt_processor_object_handlers_init(); +} diff --git a/extension/src/xslt/state.inc b/extension/src/xslt/state.inc new file mode 100644 index 000000000..360b2a2db --- /dev/null +++ b/extension/src/xslt/state.inc @@ -0,0 +1,6 @@ +/* + * XSLT module state storage aggregation. The XSLT translation unit owns this + * state beside the native SaxonC runtime. + */ + +#include "class_entries.inc" diff --git a/extension/src/xslt/xslt.c b/extension/src/xslt/xslt.c index 92d27b85d..415200785 100644 --- a/extension/src/xslt/xslt.c +++ b/extension/src/xslt/xslt.c @@ -10,13 +10,16 @@ #include "php.h" #include "php_king.h" -#include "include/runtime/saxonc_candidates.h" +#include "runtime/saxonc_candidates.h" +#include "xslt/arginfo/index.h" #include "xslt/xslt.h" #include "Zend/zend_exceptions.h" #include #include +#include #include #include +#include #include #ifndef PATH_MAX @@ -24,26 +27,70 @@ #endif #include "saxonc_loader.inc" - -static zend_string *king_xslt_realpath_or_throw(zend_string *path, const char *label) +#include "state.inc" +#include "php_binding.inc" +#include "registration.inc" + +static zend_string *king_xslt_realpath_checked_or_throw( + zend_string *path, + const char *label, + bool require_directory +) { char resolved[PATH_MAX]; + struct stat path_stat; if (ZSTR_LEN(path) == 0) { - zend_throw_exception(king_ce_validation_exception, label, 0); + char message[256]; + snprintf(message, sizeof(message), "%s must not be empty.", label); + zend_throw_exception(king_ce_validation_exception, message, 0); return NULL; } if (realpath(ZSTR_VAL(path), resolved) == NULL) { char message[512]; - snprintf(message, sizeof(message), "%s is not a readable local file: %s", label, ZSTR_VAL(path)); + snprintf( + message, + sizeof(message), + "%s is not a readable local %s: %s", + label, + require_directory ? "directory" : "file", + ZSTR_VAL(path) + ); + zend_throw_exception(king_ce_validation_exception, message, 0); + return NULL; + } + + if (stat(resolved, &path_stat) != 0) { + char message[512]; + snprintf(message, sizeof(message), "%s could not be inspected: %s", label, resolved); zend_throw_exception(king_ce_validation_exception, message, 0); return NULL; } - if (access(resolved, R_OK) != 0) { + if (require_directory && !S_ISDIR(path_stat.st_mode)) { + char message[512]; + snprintf(message, sizeof(message), "%s must be a local directory: %s", label, resolved); + zend_throw_exception(king_ce_validation_exception, message, 0); + return NULL; + } + if (!require_directory && !S_ISREG(path_stat.st_mode)) { char message[512]; - snprintf(message, sizeof(message), "%s is not readable: %s", label, resolved); + snprintf(message, sizeof(message), "%s must be a local file: %s", label, resolved); + zend_throw_exception(king_ce_validation_exception, message, 0); + return NULL; + } + + if (access(resolved, require_directory ? (R_OK | X_OK) : R_OK) != 0) { + char message[512]; + snprintf( + message, + sizeof(message), + "%s is not %s: %s", + label, + require_directory ? "accessible" : "readable", + resolved + ); zend_throw_exception(king_ce_validation_exception, message, 0); return NULL; } @@ -51,6 +98,16 @@ static zend_string *king_xslt_realpath_or_throw(zend_string *path, const char *l return zend_string_init(resolved, strlen(resolved), 0); } +static zend_string *king_xslt_realpath_file_or_throw(zend_string *path, const char *label) +{ + return king_xslt_realpath_checked_or_throw(path, label, false); +} + +static zend_string *king_xslt_realpath_directory_or_throw(zend_string *path, const char *label) +{ + return king_xslt_realpath_checked_or_throw(path, label, true); +} + static zend_string *king_xslt_dirname_from_path(zend_string *path) { const char *value = ZSTR_VAL(path); @@ -107,7 +164,7 @@ static zend_string *king_xslt_cwd_from_options(zval *options, zend_string *style && (cwd_value = zend_hash_str_find(Z_ARRVAL_P(options), "cwd", sizeof("cwd") - 1)) != NULL && Z_TYPE_P(cwd_value) != IS_NULL) { cwd_string = zval_get_string(cwd_value); - resolved = king_xslt_realpath_or_throw(cwd_string, "XSLT cwd"); + resolved = king_xslt_realpath_directory_or_throw(cwd_string, "XSLT cwd"); zend_string_release(cwd_string); return resolved; } @@ -115,6 +172,100 @@ static zend_string *king_xslt_cwd_from_options(zval *options, zend_string *style return king_xslt_dirname_from_path(stylesheet_path); } +static bool king_xslt_option_value_is_stringable(zval *value) +{ + if (value == NULL) { + return false; + } + + switch (Z_TYPE_P(value)) { + case IS_NULL: + case IS_FALSE: + case IS_TRUE: + case IS_LONG: + case IS_DOUBLE: + case IS_STRING: + return true; + default: + return false; + } +} + +static zend_result king_xslt_validate_properties_option(zval *properties_value) +{ + zend_string *key; + zval *entry; + + if (properties_value == NULL || Z_TYPE_P(properties_value) == IS_NULL) { + return SUCCESS; + } + if (Z_TYPE_P(properties_value) != IS_ARRAY) { + zend_throw_exception(king_ce_validation_exception, "XSLT properties option must be an associative array.", 0); + return FAILURE; + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(properties_value), key, entry) { + if (key == NULL) { + zend_throw_exception(king_ce_validation_exception, "XSLT property names must be strings.", 0); + return FAILURE; + } + if (!king_xslt_option_value_is_stringable(entry)) { + zend_throw_exception(king_ce_validation_exception, "XSLT property values must be scalar or null.", 0); + return FAILURE; + } + } ZEND_HASH_FOREACH_END(); + + return SUCCESS; +} + +zend_result king_xslt_validate_options(zval *options) +{ + zend_string *key; + zval *entry; + char message[256]; + + if (options == NULL || Z_TYPE_P(options) == IS_NULL) { + return SUCCESS; + } + if (Z_TYPE_P(options) != IS_ARRAY) { + zend_throw_exception(king_ce_validation_exception, "XSLT options must be an array.", 0); + return FAILURE; + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(options), key, entry) { + if (key == NULL) { + zend_throw_exception(king_ce_validation_exception, "XSLT option names must be strings.", 0); + return FAILURE; + } + + if (zend_string_equals_literal(key, "cwd")) { + if (entry != NULL && Z_TYPE_P(entry) != IS_NULL && Z_TYPE_P(entry) != IS_STRING) { + zend_throw_exception(king_ce_validation_exception, "XSLT cwd option must be a string or null.", 0); + return FAILURE; + } + continue; + } + + if (zend_string_equals_literal(key, "properties")) { + if (king_xslt_validate_properties_option(entry) != SUCCESS) { + return FAILURE; + } + continue; + } + + snprintf( + message, + sizeof(message), + "XSLT option '%s' is not supported. Supported options are 'cwd' and 'properties'.", + ZSTR_VAL(key) + ); + zend_throw_exception(king_ce_validation_exception, message, 0); + return FAILURE; + } ZEND_HASH_FOREACH_END(); + + return SUCCESS; +} + static zend_result king_xslt_apply_properties_from_options( sxnc_property **properties, int *property_len, @@ -135,16 +286,8 @@ static zend_result king_xslt_apply_properties_from_options( if (properties_value == NULL || Z_TYPE_P(properties_value) == IS_NULL) { return SUCCESS; } - if (Z_TYPE_P(properties_value) != IS_ARRAY) { - zend_throw_exception(king_ce_validation_exception, "XSLT properties option must be an associative array.", 0); - return FAILURE; - } ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(properties_value), key, entry) { - if (key == NULL) { - zend_throw_exception(king_ce_validation_exception, "XSLT property names must be strings.", 0); - return FAILURE; - } value = zval_get_string(entry); king_saxonc.setProperty_fn( properties, @@ -168,12 +311,12 @@ static zend_result king_xslt_prepare_context( zend_string **cwd ) { - *source_abs = king_xslt_realpath_or_throw(source_path, "XSLT source XML"); + *source_abs = king_xslt_realpath_file_or_throw(source_path, "XSLT source XML"); if (*source_abs == NULL) { return FAILURE; } - *stylesheet_abs = king_xslt_realpath_or_throw(stylesheet_path, "XSLT stylesheet"); + *stylesheet_abs = king_xslt_realpath_file_or_throw(stylesheet_path, "XSLT stylesheet"); if (*stylesheet_abs == NULL) { zend_string_release(*source_abs); *source_abs = NULL; @@ -205,6 +348,43 @@ static const char *king_xslt_saxon_error(sxnc_environment *environment) : "SaxonC XSLT transformation failed."; } +static bool king_xslt_saxon_has_error(sxnc_environment *environment, const char **message) +{ + *message = NULL; + + if (environment != NULL && king_saxonc.c_getErrorMessage_fn != NULL) { + *message = king_saxonc.c_getErrorMessage_fn(environment); + } + + return *message != NULL && (*message)[0] != '\0'; +} + +static zend_string *king_xslt_temporary_output_path(zend_string *output_abs) +{ + char path_template[PATH_MAX]; + int fd; + + if (ZSTR_LEN(output_abs) + sizeof(".kingtmpXXXXXX") >= PATH_MAX) { + zend_throw_exception(king_ce_validation_exception, "XSLT output temporary path is too long.", 0); + return NULL; + } + + snprintf(path_template, sizeof(path_template), "%s.kingtmpXXXXXX", ZSTR_VAL(output_abs)); + fd = mkstemp(path_template); + if (fd < 0) { + zend_throw_exception(king_ce_runtime_exception, "XSLT output temporary file could not be created.", 0); + return NULL; + } + + close(fd); + if (unlink(path_template) != 0) { + zend_throw_exception(king_ce_runtime_exception, "XSLT output temporary file could not be prepared.", 0); + return NULL; + } + + return zend_string_init(path_template, strlen(path_template), 0); +} + void king_xslt_shutdown_system(void) { king_saxonc_close_runtime_handle(); @@ -219,7 +399,7 @@ void king_xslt_add_component_info(zval *configuration) add_assoc_string(configuration, "supported_use", "XSLT 2.0/3.0 transformation for Schematron/SVRL pipelines"); } -PHP_FUNCTION(king_xslt_engine_status) +void king_xslt_engine_status_array(zval *return_value) { sxnc_environment *environment = NULL; sxnc_processor *processor = NULL; @@ -228,8 +408,6 @@ PHP_FUNCTION(king_xslt_engine_status) const char *version = NULL; const char *variant = NULL; - ZEND_PARSE_PARAMETERS_NONE(); - array_init(return_value); add_assoc_string(return_value, "engine", "saxonc"); add_assoc_string(return_value, "library_env", "KING_SAXONC_LIBRARY"); @@ -261,11 +439,13 @@ PHP_FUNCTION(king_xslt_engine_status) king_saxonc.freeSaxonc_fn(&environment, &processor, ¶meters, &properties); } -PHP_FUNCTION(king_xslt_transform_file) +zend_result king_xslt_transform_file_result( + zend_string *source_path, + zend_string *stylesheet_path, + zval *options, + zval *return_value +) { - zend_string *source_path; - zend_string *stylesheet_path; - zval *options = NULL; zend_string *source_abs = NULL; zend_string *stylesheet_abs = NULL; zend_string *cwd = NULL; @@ -277,20 +457,17 @@ PHP_FUNCTION(king_xslt_transform_file) int property_cap = 4; const char *result; - ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_STR(source_path) - Z_PARAM_STR(stylesheet_path) - Z_PARAM_OPTIONAL - Z_PARAM_ARRAY_OR_NULL(options) - ZEND_PARSE_PARAMETERS_END(); - if (king_saxonc_ensure_ready() != SUCCESS) { zend_throw_exception(king_ce_runtime_exception, king_saxonc.load_error, 0); - RETURN_THROWS(); + return FAILURE; + } + + if (king_xslt_validate_options(options) != SUCCESS) { + return FAILURE; } if (king_xslt_prepare_context(source_path, stylesheet_path, options, &source_abs, &stylesheet_abs, &cwd) != SUCCESS) { - RETURN_THROWS(); + return FAILURE; } king_saxonc.initSaxonc_fn(&environment, &processor, ¶meters, &properties, 0, property_cap); @@ -299,7 +476,7 @@ PHP_FUNCTION(king_xslt_transform_file) zend_string_release(stylesheet_abs); zend_string_release(cwd); zend_throw_exception(king_ce_runtime_exception, "SaxonC processor could not be initialized.", 0); - RETURN_THROWS(); + return FAILURE; } if (king_xslt_apply_properties_from_options(&properties, &property_len, &property_cap, options) != SUCCESS) { @@ -307,7 +484,7 @@ PHP_FUNCTION(king_xslt_transform_file) zend_string_release(source_abs); zend_string_release(stylesheet_abs); zend_string_release(cwd); - RETURN_THROWS(); + return FAILURE; } result = king_saxonc.xsltApplyStylesheet_fn( @@ -324,12 +501,14 @@ PHP_FUNCTION(king_xslt_transform_file) if (result == NULL) { const char *message = king_xslt_saxon_error(environment); + zend_string *saxon_message = zend_string_init(message, strlen(message), 0); king_saxonc.freeSaxonc_fn(&environment, &processor, ¶meters, &properties); zend_string_release(source_abs); zend_string_release(stylesheet_abs); zend_string_release(cwd); - zend_throw_exception(king_ce_validation_exception, message, 0); - RETURN_THROWS(); + zend_throw_exception(king_ce_validation_exception, ZSTR_VAL(saxon_message), 0); + zend_string_release(saxon_message); + return FAILURE; } array_init(return_value); @@ -342,18 +521,24 @@ PHP_FUNCTION(king_xslt_transform_file) zend_string_release(source_abs); zend_string_release(stylesheet_abs); zend_string_release(cwd); + + return SUCCESS; } -PHP_FUNCTION(king_xslt_transform_to_file) +zend_result king_xslt_transform_to_file_result( + zend_string *source_path, + zend_string *stylesheet_path, + zend_string *output_path, + zval *options, + zval *return_value +) { - zend_string *source_path; - zend_string *stylesheet_path; - zend_string *output_path; - zval *options = NULL; zend_string *source_abs = NULL; zend_string *stylesheet_abs = NULL; zend_string *output_abs = NULL; + zend_string *temp_output_abs = NULL; zend_string *cwd = NULL; + struct stat output_stat; sxnc_environment *environment = NULL; sxnc_processor *processor = NULL; sxnc_parameter *parameters = NULL; @@ -363,33 +548,45 @@ PHP_FUNCTION(king_xslt_transform_to_file) const char *message; zend_string *saxon_message = NULL; - ZEND_PARSE_PARAMETERS_START(3, 4) - Z_PARAM_STR(source_path) - Z_PARAM_STR(stylesheet_path) - Z_PARAM_STR(output_path) - Z_PARAM_OPTIONAL - Z_PARAM_ARRAY_OR_NULL(options) - ZEND_PARSE_PARAMETERS_END(); - if (ZSTR_LEN(output_path) == 0) { zend_throw_exception(king_ce_validation_exception, "XSLT output path must not be empty.", 0); - RETURN_THROWS(); + return FAILURE; } if (king_saxonc_ensure_ready() != SUCCESS) { zend_throw_exception(king_ce_runtime_exception, king_saxonc.load_error, 0); - RETURN_THROWS(); + return FAILURE; + } + + if (king_xslt_validate_options(options) != SUCCESS) { + return FAILURE; } if (king_xslt_prepare_context(source_path, stylesheet_path, options, &source_abs, &stylesheet_abs, &cwd) != SUCCESS) { - RETURN_THROWS(); + return FAILURE; } output_abs = king_xslt_absolute_output_path(output_path); if (output_abs == NULL) { zend_string_release(source_abs); zend_string_release(stylesheet_abs); zend_string_release(cwd); - RETURN_THROWS(); + return FAILURE; + } + if (stat(ZSTR_VAL(output_abs), &output_stat) == 0 && !S_ISREG(output_stat.st_mode)) { + zend_throw_exception(king_ce_validation_exception, "XSLT output path must be a local file path.", 0); + zend_string_release(source_abs); + zend_string_release(stylesheet_abs); + zend_string_release(output_abs); + zend_string_release(cwd); + return FAILURE; + } + temp_output_abs = king_xslt_temporary_output_path(output_abs); + if (temp_output_abs == NULL) { + zend_string_release(source_abs); + zend_string_release(stylesheet_abs); + zend_string_release(output_abs); + zend_string_release(cwd); + return FAILURE; } king_saxonc.initSaxonc_fn(&environment, &processor, ¶meters, &properties, 0, property_cap); @@ -397,9 +594,10 @@ PHP_FUNCTION(king_xslt_transform_to_file) zend_string_release(source_abs); zend_string_release(stylesheet_abs); zend_string_release(output_abs); + zend_string_release(temp_output_abs); zend_string_release(cwd); zend_throw_exception(king_ce_runtime_exception, "SaxonC processor could not be initialized.", 0); - RETURN_THROWS(); + return FAILURE; } if (king_xslt_apply_properties_from_options(&properties, &property_len, &property_cap, options) != SUCCESS) { @@ -407,8 +605,9 @@ PHP_FUNCTION(king_xslt_transform_to_file) zend_string_release(source_abs); zend_string_release(stylesheet_abs); zend_string_release(output_abs); + zend_string_release(temp_output_abs); zend_string_release(cwd); - RETURN_THROWS(); + return FAILURE; } king_saxonc.xsltSaveResultToFile_fn( @@ -417,26 +616,53 @@ PHP_FUNCTION(king_xslt_transform_to_file) ZSTR_VAL(cwd), ZSTR_VAL(source_abs), ZSTR_VAL(stylesheet_abs), - ZSTR_VAL(output_abs), + ZSTR_VAL(temp_output_abs), parameters, properties, 0, property_len ); - message = king_xslt_saxon_error(environment); - saxon_message = zend_string_init(message, strlen(message), 0); + if (king_xslt_saxon_has_error(environment, &message)) { + saxon_message = zend_string_init(message, strlen(message), 0); + } king_saxonc.freeSaxonc_fn(&environment, &processor, ¶meters, &properties); - if (access(ZSTR_VAL(output_abs), R_OK) != 0) { - zend_throw_exception(king_ce_runtime_exception, ZSTR_VAL(saxon_message), 0); + if (saxon_message != NULL) { + unlink(ZSTR_VAL(temp_output_abs)); + zend_throw_exception(king_ce_validation_exception, ZSTR_VAL(saxon_message), 0); zend_string_release(source_abs); zend_string_release(stylesheet_abs); zend_string_release(output_abs); + zend_string_release(temp_output_abs); zend_string_release(cwd); zend_string_release(saxon_message); - RETURN_THROWS(); + return FAILURE; + } + + if (stat(ZSTR_VAL(temp_output_abs), &output_stat) != 0 + || !S_ISREG(output_stat.st_mode) + || access(ZSTR_VAL(temp_output_abs), R_OK) != 0) { + unlink(ZSTR_VAL(temp_output_abs)); + zend_throw_exception(king_ce_runtime_exception, "SaxonC XSLT transformation did not produce a readable output file.", 0); + zend_string_release(source_abs); + zend_string_release(stylesheet_abs); + zend_string_release(output_abs); + zend_string_release(temp_output_abs); + zend_string_release(cwd); + return FAILURE; + } + + if (rename(ZSTR_VAL(temp_output_abs), ZSTR_VAL(output_abs)) != 0) { + unlink(ZSTR_VAL(temp_output_abs)); + zend_throw_exception(king_ce_runtime_exception, "XSLT output file could not be moved into place.", 0); + zend_string_release(source_abs); + zend_string_release(stylesheet_abs); + zend_string_release(output_abs); + zend_string_release(temp_output_abs); + zend_string_release(cwd); + return FAILURE; } array_init(return_value); @@ -448,6 +674,53 @@ PHP_FUNCTION(king_xslt_transform_to_file) zend_string_release(source_abs); zend_string_release(stylesheet_abs); zend_string_release(output_abs); + zend_string_release(temp_output_abs); zend_string_release(cwd); - zend_string_release(saxon_message); + + return SUCCESS; +} + +PHP_FUNCTION(king_xslt_engine_status) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + king_xslt_engine_status_array(return_value); +} + +PHP_FUNCTION(king_xslt_transform_file) +{ + zend_string *source_path; + zend_string *stylesheet_path; + zval *options = NULL; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STR(source_path) + Z_PARAM_STR(stylesheet_path) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + if (king_xslt_transform_file_result(source_path, stylesheet_path, options, return_value) != SUCCESS) { + RETURN_THROWS(); + } +} + +PHP_FUNCTION(king_xslt_transform_to_file) +{ + zend_string *source_path; + zend_string *stylesheet_path; + zend_string *output_path; + zval *options = NULL; + + ZEND_PARSE_PARAMETERS_START(3, 4) + Z_PARAM_STR(source_path) + Z_PARAM_STR(stylesheet_path) + Z_PARAM_STR(output_path) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + if (king_xslt_transform_to_file_result(source_path, stylesheet_path, output_path, options, return_value) != SUCCESS) { + RETURN_THROWS(); + } } diff --git a/extension/tests/001-load-and-core.phpt b/extension/tests/001-load-and-core.phpt index c6ce3c0fb..4af2985db 100644 --- a/extension/tests/001-load-and-core.phpt +++ b/extension/tests/001-load-and-core.phpt @@ -26,9 +26,9 @@ array(8) { ["config_override_allowed"]=> bool(false) ["active_runtime_count"]=> - int(31) + int(32) ["active_runtimes"]=> - array(31) { + array(32) { [0]=> string(6) "config" [1]=> @@ -70,26 +70,28 @@ array(8) { [19]=> string(29) "server_open_telemetry_runtime" [20]=> - string(11) "iibin_proto" + string(11) "rtp_runtime" [21]=> - string(21) "semantic_dns_registry" + string(11) "iibin_proto" [22]=> - string(27) "semantic_dns_server_runtime" + string(21) "semantic_dns_registry" [23]=> - string(21) "object_store_registry" + string(27) "semantic_dns_server_runtime" [24]=> - string(18) "cdn_cache_registry" + string(21) "object_store_registry" [25]=> - string(19) "xslt_saxonc_runtime" + string(18) "cdn_cache_registry" [26]=> - string(11) "mcp_runtime" + string(19) "xslt_saxonc_runtime" [27]=> - string(29) "pipeline_orchestrator_runtime" + string(11) "mcp_runtime" [28]=> - string(17) "telemetry_runtime" + string(29) "pipeline_orchestrator_runtime" [29]=> - string(19) "autoscaling_runtime" + string(17) "telemetry_runtime" [30]=> + string(19) "autoscaling_runtime" + [31]=> string(26) "system_integration_runtime" } ["stubbed_api_group_count"]=> diff --git a/extension/tests/006-health-shape.phpt b/extension/tests/006-health-shape.phpt index 9eff189ef..9d8075cb1 100644 --- a/extension/tests/006-health-shape.phpt +++ b/extension/tests/006-health-shape.phpt @@ -47,7 +47,7 @@ bool(true) bool(true) bool(true) bool(false) -int(31) +int(32) bool(true) int(0) bool(true) diff --git a/extension/tests/030-system-component-info-config-backed-matrix.phpt b/extension/tests/030-system-component-info-config-backed-matrix.phpt index 61dc32aec..e4a38b166 100644 --- a/extension/tests/030-system-component-info-config-backed-matrix.phpt +++ b/extension/tests/030-system-component-info-config-backed-matrix.phpt @@ -191,7 +191,7 @@ array(6) { } string(12) "semantic_dns" string(13) "config_backed" -array(9) { +array(10) { [0]=> string(13) "server_enable" [1]=> @@ -210,6 +210,8 @@ array(9) { string(24) "live_probe_allowed_hosts" [8]=> string(14) "mothernode_uri" + [9]=> + string(10) "state_path" } bool(true) array(6) { diff --git a/extension/tests/115-health-runtime-surface.phpt b/extension/tests/115-health-runtime-surface.phpt index 341ba2e55..1e77d6301 100644 --- a/extension/tests/115-health-runtime-surface.phpt +++ b/extension/tests/115-health-runtime-surface.phpt @@ -10,8 +10,8 @@ var_dump($health['stubbed_api_group_count']); var_dump($health['stubbed_api_groups']); ?> --EXPECT-- -int(31) -array(31) { +int(32) +array(32) { [0]=> string(6) "config" [1]=> @@ -53,26 +53,28 @@ array(31) { [19]=> string(29) "server_open_telemetry_runtime" [20]=> - string(11) "iibin_proto" + string(11) "rtp_runtime" [21]=> - string(21) "semantic_dns_registry" + string(11) "iibin_proto" [22]=> - string(27) "semantic_dns_server_runtime" + string(21) "semantic_dns_registry" [23]=> - string(21) "object_store_registry" + string(27) "semantic_dns_server_runtime" [24]=> - string(18) "cdn_cache_registry" + string(21) "object_store_registry" [25]=> - string(19) "xslt_saxonc_runtime" + string(18) "cdn_cache_registry" [26]=> - string(11) "mcp_runtime" + string(19) "xslt_saxonc_runtime" [27]=> - string(29) "pipeline_orchestrator_runtime" + string(11) "mcp_runtime" [28]=> - string(17) "telemetry_runtime" + string(29) "pipeline_orchestrator_runtime" [29]=> - string(19) "autoscaling_runtime" + string(17) "telemetry_runtime" [30]=> + string(19) "autoscaling_runtime" + [31]=> string(26) "system_integration_runtime" } int(0) diff --git a/extension/tests/303-semantic-dns-state-hardening.phpt b/extension/tests/303-semantic-dns-state-hardening.phpt index 133eb8181..57d3462a5 100644 --- a/extension/tests/303-semantic-dns-state-hardening.phpt +++ b/extension/tests/303-semantic-dns-state-hardening.phpt @@ -33,7 +33,7 @@ file_put_contents( ); $cmd = sprintf( - '%s -d king.security_allow_config_override=1 -d %s %s', + '%s -n -d king.security_allow_config_override=1 -d %s %s', escapeshellarg(PHP_BINARY), escapeshellarg('extension=' . $extension_path), escapeshellarg($child_script) diff --git a/extension/tests/325-semantic-dns-state-node-count-hardening.phpt b/extension/tests/325-semantic-dns-state-node-count-hardening.phpt index 70be74a80..a9217e97d 100644 --- a/extension/tests/325-semantic-dns-state-node-count-hardening.phpt +++ b/extension/tests/325-semantic-dns-state-node-count-hardening.phpt @@ -41,7 +41,7 @@ PHP ); $cmd = sprintf( - '%s -d king.security_allow_config_override=1 -d %s %s', + '%s -n -d king.security_allow_config_override=1 -d %s %s', escapeshellarg(PHP_BINARY), escapeshellarg('extension=' . $extension_path), escapeshellarg($child_script) diff --git a/extension/tests/331-smart-dns-config-surface-honesty.phpt b/extension/tests/331-smart-dns-config-surface-honesty.phpt index 9fcbc98c4..f97f29f64 100644 --- a/extension/tests/331-smart-dns-config-surface-honesty.phpt +++ b/extension/tests/331-smart-dns-config-surface-honesty.phpt @@ -82,7 +82,7 @@ Semantic-DNS v1 does not support init option 'health_check_interval_ms'. King\ValidationException Semantic-DNS v1 does not support init option 'mothernode_sync_interval_sec'. bool(true) -array(9) { +array(10) { [0]=> string(13) "server_enable" [1]=> @@ -101,4 +101,6 @@ array(9) { string(24) "live_probe_allowed_hosts" [8]=> string(14) "mothernode_uri" + [9]=> + string(10) "state_path" } diff --git a/extension/tests/356-semantic-dns-restart-topology-recovery.phpt b/extension/tests/356-semantic-dns-restart-topology-recovery.phpt index c16e41309..7532565fa 100644 --- a/extension/tests/356-semantic-dns-restart-topology-recovery.phpt +++ b/extension/tests/356-semantic-dns-restart-topology-recovery.phpt @@ -126,13 +126,13 @@ PHP ); $writer_cmd = sprintf( - '%s -d king.security_allow_config_override=1 -d %s %s', + '%s -n -d king.security_allow_config_override=1 -d %s %s', escapeshellarg(PHP_BINARY), escapeshellarg('extension=' . $extension_path), escapeshellarg($writer_script) ); $reader_cmd = sprintf( - '%s -d king.security_allow_config_override=1 -d %s %s', + '%s -n -d king.security_allow_config_override=1 -d %s %s', escapeshellarg(PHP_BINARY), escapeshellarg('extension=' . $extension_path), escapeshellarg($reader_script) diff --git a/extension/tests/395-semantic-dns-state-object-payload-hardening.phpt b/extension/tests/395-semantic-dns-state-object-payload-hardening.phpt index e1e03334f..ed00b9a99 100644 --- a/extension/tests/395-semantic-dns-state-object-payload-hardening.phpt +++ b/extension/tests/395-semantic-dns-state-object-payload-hardening.phpt @@ -66,7 +66,7 @@ PHP ); $cmd = sprintf( - '%s -d king.security_allow_config_override=1 -d %s %s', + '%s -n -d king.security_allow_config_override=1 -d %s %s', escapeshellarg(PHP_BINARY), escapeshellarg('extension=' . $extension_path), escapeshellarg($child_script) diff --git a/extension/tests/500-semantic-dns-query-failure-recovery-contract.phpt b/extension/tests/500-semantic-dns-query-failure-recovery-contract.phpt index 10cd1709c..aa39be41f 100644 --- a/extension/tests/500-semantic-dns-query-failure-recovery-contract.phpt +++ b/extension/tests/500-semantic-dns-query-failure-recovery-contract.phpt @@ -117,13 +117,13 @@ PHP ); $writerCmd = sprintf( - '%s -d king.security_allow_config_override=1 -d %s %s', + '%s -n -d king.security_allow_config_override=1 -d %s %s', escapeshellarg(PHP_BINARY), escapeshellarg('extension=' . $extensionPath), escapeshellarg($writerScript) ); $readerCmd = sprintf( - '%s -d king.security_allow_config_override=1 -d %s %s', + '%s -n -d king.security_allow_config_override=1 -d %s %s', escapeshellarg(PHP_BINARY), escapeshellarg('extension=' . $extensionPath), escapeshellarg($readerScript) diff --git a/extension/tests/711-proto-batch-public-api-surface-contract.phpt b/extension/tests/711-proto-batch-public-api-surface-contract.phpt index 2f70a957d..32065466a 100644 --- a/extension/tests/711-proto-batch-public-api-surface-contract.phpt +++ b/extension/tests/711-proto-batch-public-api-surface-contract.phpt @@ -29,25 +29,21 @@ $expectations = [ 'public static function decodeBatch(string $schema, array $records, bool|string|array $decodeAsObject = false): array {}', 'whole batch fails', ], - 'extension/src/php_king/arginfo.inc' => [ + 'extension/include/iibin/arginfo/arginfo.h' => [ 'arginfo_king_proto_encode_batch', 'arginfo_king_proto_decode_batch', 'ZEND_ARG_TYPE_INFO(0, records, IS_ARRAY, 0)', 'ZEND_ARG_TYPE_INFO(0, binary_records, IS_ARRAY, 0)', ], - 'extension/src/php_king/externals.inc' => [ + 'extension/include/iibin/iibin.h' => [ 'PHP_FUNCTION(king_proto_encode_batch);', 'PHP_FUNCTION(king_proto_decode_batch);', ], - 'extension/src/php_king/function_table.inc' => [ + 'extension/include/iibin/function_entries.h' => [ 'PHP_FE(king_proto_encode_batch, arginfo_king_proto_encode_batch)', 'PHP_FE(king_proto_decode_batch, arginfo_king_proto_decode_batch)', ], - 'extension/include/iibin/iibin.h' => [ - 'PHP_FUNCTION(king_proto_encode_batch);', - 'PHP_FUNCTION(king_proto_decode_batch);', - ], - 'extension/src/iibin/iibin_api.c' => [ + 'extension/include/iibin/class_method_entries.h' => [ 'ZEND_ME_MAPPING(encodeBatch, king_proto_encode_batch, arginfo_class_King_IIBIN_encodeBatch, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC)', 'ZEND_ME_MAPPING(decodeBatch, king_proto_decode_batch, arginfo_class_King_IIBIN_decodeBatch, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC)', ], diff --git a/extension/tests/748-native-toolchain-linker-selector-contract.phpt b/extension/tests/748-native-toolchain-linker-selector-contract.phpt index 87fa801a1..b59854966 100644 --- a/extension/tests/748-native-toolchain-linker-selector-contract.phpt +++ b/extension/tests/748-native-toolchain-linker-selector-contract.phpt @@ -40,15 +40,10 @@ var_dump(str_contains($selector, '-install_name')); var_dump(str_contains($selector, '-undefined dynamic_lookup')); var_dump(str_contains($selector, 'no_undefined_flag="-undefined dynamic_lookup"')); -$configureDarwin = ''; -if (preg_match('/darwin\* \| rhapsody\*\).*?archive_cmds_need_lc=no/ms', $configure, $match)) { - $configureDarwin = $match[0]; -} - -var_dump($configure === '' || $configureDarwin !== ''); -var_dump($configure === '' || str_contains($configureDarwin, '${wl}-undefined ${wl}dynamic_lookup')); -var_dump($configure === '' || !str_contains($configureDarwin, '-undefined ${wl}suppress')); -var_dump($configure === '' || !str_contains($configureDarwin, '-flat_namespace')); +var_dump($configure === '' || str_contains($configure, "_lt_dar_allow_undefined='\$wl-undefined \${wl}dynamic_lookup'")); +var_dump($configure === '' || !str_contains($configure, "_lt_dar_allow_undefined='\$wl-undefined \${wl}suppress'")); +var_dump($configure === '' || !str_contains($configure, "_lt_dar_allow_undefined='\$wl-flat_namespace \$wl-undefined \${wl}suppress'")); +var_dump($configure === '' || str_contains($configure, 'allow_undefined_flag=$_lt_dar_allow_undefined')); var_dump($configure === '' || str_contains($configure, "_lt_dar_single_mod=''")); $templateDarwin = ''; diff --git a/extension/tests/751-hrtime-header-php81-php82-compat-contract.phpt b/extension/tests/751-hrtime-header-php81-php82-compat-contract.phpt index df712c63c..e062a9577 100644 --- a/extension/tests/751-hrtime-header-php81-php82-compat-contract.phpt +++ b/extension/tests/751-hrtime-header-php81-php82-compat-contract.phpt @@ -6,12 +6,12 @@ $root = dirname(__DIR__, 2); $paths = [ 'extension/src/pipeline_orchestrator/orchestrator.c', 'extension/src/mcp/mcp.c', - 'extension/src/php_king/mcp.inc', + 'extension/src/mcp/php_binding.inc', ]; foreach ($paths as $path) { $source = (string) file_get_contents($root . '/' . $path); - var_dump(str_contains($source, 'include/king_hrtime.h')); + var_dump(str_contains($source, '#include "king_hrtime.h"')); var_dump(!str_contains($source, 'Zend/zend_hrtime.h')); var_dump(!str_contains($source, 'zend_hrtime()')); } diff --git a/extension/tests/757-awaitable-aggregate-contract.phpt b/extension/tests/757-awaitable-aggregate-contract.phpt new file mode 100644 index 000000000..f0463b738 --- /dev/null +++ b/extension/tests/757-awaitable-aggregate-contract.phpt @@ -0,0 +1,137 @@ +--TEST-- +King Awaitable aggregate APIs expose keyed any/all envelopes +--FILE-- + $input]; +} + +foreach ([ + 'king_awaitable_any', + 'king_awaitable_all', +] as $function) { + var_dump(function_exists($function)); +} + +foreach ([ + [King\Awaitable::class, 'any'], + [King\Awaitable::class, 'all'], +] as [$class, $method]) { + $methodReflection = new ReflectionMethod($class, $method); + var_dump($methodReflection->isPublic()); + var_dump($methodReflection->isStatic()); +} + +var_dump(king_pipeline_orchestrator_register_tool('aggregate-tool', [ + 'model' => 'gpt-sim', +])); +var_dump(king_pipeline_orchestrator_register_handler('aggregate-tool', 'aggregate_handler')); + +$first = king_pipeline_orchestrator_run_async( + ['history' => ['first']], + [['tool' => 'aggregate-tool']] +); +$second = king_pipeline_orchestrator_run_async( + ['history' => ['second']], + [['tool' => 'aggregate-tool']] +); +$any = king_awaitable_any([ + 'first' => $first, + 'second' => $second, +]); +var_dump($any instanceof King\Awaitable); +var_dump($any->poll(0)); +$ready = king_await($any); +var_dump($ready['key']); +var_dump($ready['status']); +var_dump($ready['operation']); +var_dump($ready['value']['history']); + +$left = King\PipelineOrchestrator::runAsync( + ['history' => ['left']], + [['tool' => 'aggregate-tool']] +); +$right = King\PipelineOrchestrator::runAsync( + ['history' => ['right']], + [['tool' => 'aggregate-tool']] +); +$all = King\Awaitable::all([ + 'left' => $left, + 'right' => $right, +]); +var_dump($all instanceof King\Awaitable); +var_dump($all->poll(0)); +$allReady = $all->await(); +var_dump(array_keys($allReady)); +var_dump($allReady['left']['status']); +var_dump($allReady['left']['operation']); +var_dump($allReady['left']['value']['history']); +var_dump($allReady['right']['status']); +var_dump($allReady['right']['value']['history']); + +try { + king_awaitable_any([]); + echo "no-empty-exception\n"; +} catch (ValueError $e) { + var_dump(str_contains($e->getMessage(), 'at least one')); +} + +try { + King\Awaitable::all([new stdClass()]); + echo "no-type-exception\n"; +} catch (TypeError $e) { + var_dump(str_contains($e->getMessage(), 'King\\Awaitable')); +} +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +string(5) "first" +string(8) "resolved" +string(36) "king_pipeline_orchestrator_run_async" +array(2) { + [0]=> + string(5) "first" + [1]=> + string(17) "aggregate-handler" +} +bool(true) +bool(true) +array(2) { + [0]=> + string(4) "left" + [1]=> + string(5) "right" +} +string(8) "resolved" +string(36) "king_pipeline_orchestrator_run_async" +array(2) { + [0]=> + string(4) "left" + [1]=> + string(17) "aggregate-handler" +} +string(8) "resolved" +array(2) { + [0]=> + string(5) "right" + [1]=> + string(17) "aggregate-handler" +} +bool(true) +bool(true) diff --git a/extension/tests/758-inference-awaitable-contract.phpt b/extension/tests/758-inference-awaitable-contract.phpt new file mode 100644 index 000000000..da3673676 --- /dev/null +++ b/extension/tests/758-inference-awaitable-contract.phpt @@ -0,0 +1,112 @@ +--TEST-- +King inference async stream reader resolves through Awaitable +--SKIPIF-- + +--FILE-- +isPublic()); + + $model = king_inference_model_load([ + 'name' => 'test-model', + 'artifact' => $modelPath, + 'quantization' => 'q4', + 'backend' => [ + 'type' => 'local', + 'runner' => [ + 'path' => $runnerPath, + ], + ], + ]); + var_dump($model instanceof King\Inference\Model); + + $info = king_inference_model_info($model); + var_dump($info['name']); + var_dump($info['format']); + var_dump($info['backend']); + var_dump($info['backend_capabilities']['streaming']); + + $stream = king_inference_stream($model, [ + 'prompt' => 'hello', + 'max_tokens' => 1, + 'temperature' => 0, + ]); + var_dump($stream instanceof King\Inference\Stream); + + $startRead = king_inference_next_async($stream, 0); + var_dump($startRead instanceof King\Awaitable); + var_dump($startRead->poll(0)); + $start = $startRead->await(); + var_dump($start['type']); + + $token = null; + for ($i = 0; $i < 20; $i++) { + $read = $stream->nextAsync(50); + if (!$read->poll(50)) { + usleep(10000); + continue; + } + + $event = $read->await(); + if (is_array($event) && ($event['type'] ?? null) === 'token') { + $token = $event['text']; + break; + } + if (is_array($event) && (($event['type'] ?? null) === 'done' || ($event['type'] ?? null) === 'cancelled')) { + break; + } + } + + var_dump($token); + var_dump(king_inference_cancel($stream)); +} finally { + @unlink($runnerPath); +} +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +string(10) "test-model" +string(4) "gguf" +string(5) "local" +bool(true) +bool(true) +bool(true) +bool(true) +string(5) "start" +string(17) "token-from-runner" +bool(true) diff --git a/extension/tests/759-inference-native-loader-contract.phpt b/extension/tests/759-inference-native-loader-contract.phpt new file mode 100644 index 000000000..82ede6589 --- /dev/null +++ b/extension/tests/759-inference-native-loader-contract.phpt @@ -0,0 +1,96 @@ +--TEST-- +King native inference loader exposes GGUF metadata without external runtime +--SKIPIF-- + +--FILE-- + 'native-loader-test', + 'artifact' => [ + 'path' => getenv('KING_INFERENCE_TEST_MODEL_PATH'), + ], + 'backend' => 'king_native_cpu', +]); + +$info = king_inference_model_info($model); +var_dump($info['name']); +var_dump($info['backend']); +var_dump($info['engine']); +var_dump($info['external_runtime']); +var_dump($info['token_generation_ready']); +var_dump($info['backend_capabilities']['native_model_loader']); +var_dump($info['backend_capabilities']['token_generation']); +var_dump($info['gguf']['header_loaded']); +var_dump($info['gguf']['version'] > 0); +var_dump($info['gguf']['tensor_count'] > 0); +var_dump($info['artifact_bytes'] > 24); + +$encoded = king_inference_tokenize($model, 'Hello world'); +var_dump($encoded['token_count'] > 0); +var_dump($encoded['unknown_count'] === 0); + +$graphs = []; +foreach ($encoded['tokens'] as $tokenId) { + $graphs[] = [ + 'inputs' => [ + 'token' => [(int) $tokenId], + ], + 'ops' => [ + [ + 'id' => 'next_token', + 'op' => 'scale', + 'input' => 'token', + 'factor' => 1.0, + ], + ], + 'output' => 'next_token', + ]; +} + +$stream = king_inference_stream($model, [ + 'graphs' => $graphs, +], [ + 'with_memory' => false, + 'max_native_stream_tokens' => 16, +]); + +$text = ''; +$events = 0; +while (($event = king_inference_next($stream, 0)) !== null) { + if (($event['type'] ?? '') === 'token') { + $events++; + $text .= $event['text']; + } + if (($event['type'] ?? '') === 'done') { + var_dump($event['exit_code']); + var_dump($event['chunks'] === $events); + break; + } +} + +var_dump($events > 0); +var_dump(trim($text) === 'Hello world'); +?> +--EXPECT-- +string(18) "native-loader-test" +string(15) "king_native_cpu" +string(15) "king_native_cpu" +bool(false) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +int(0) +bool(true) +bool(true) +bool(true) diff --git a/extension/tests/760-inference-memory-config-contract.phpt b/extension/tests/760-inference-memory-config-contract.phpt new file mode 100644 index 000000000..2bd042067 --- /dev/null +++ b/extension/tests/760-inference-memory-config-contract.phpt @@ -0,0 +1,44 @@ +--TEST-- +King inference memory config is explicit without requiring a model artifact +--INI-- +king.inference_with_memory=1 +--FILE-- + __FILE__, + 'with_memory' => 'yes', + ]); + echo "invalid-type-accepted\n"; +} catch (Throwable $e) { + var_dump(str_contains($e->getMessage(), 'with_memory must be a boolean')); +} + +try { + king_inference_model_load([ + 'artifact' => __FILE__, + 'with_memory' => true, + 'with-memory' => false, + ]); + echo "duplicate-alias-accepted\n"; +} catch (Throwable $e) { + var_dump(str_contains($e->getMessage(), 'memory config must use either with_memory or with-memory')); +} + +try { + king_inference_model_load([ + 'artifact' => __FILE__, + 'with-memory' => [], + ]); + echo "invalid-alias-type-accepted\n"; +} catch (Throwable $e) { + var_dump(str_contains($e->getMessage(), 'with-memory must be a boolean')); +} +?> +--EXPECT-- +string(1) "1" +bool(true) +bool(true) +bool(true) diff --git a/extension/tests/761-inference-gpu-runtime-status-config-contract.phpt b/extension/tests/761-inference-gpu-runtime-status-config-contract.phpt new file mode 100644 index 000000000..8cca28782 --- /dev/null +++ b/extension/tests/761-inference-gpu-runtime-status-config-contract.phpt @@ -0,0 +1,105 @@ +--TEST-- +King GPU inference runtime status follows local model config +--INI-- +king.security_allow_config_override=1 +king.gpu_bindings_enable=1 +king.gpu_default_backend=cuda +--SKIPIF-- + +--FILE-- + 'gpu', + 'inference.gpu_model_name' => 'gpu-runtime-status-test', + 'inference.gpu_model_artifact' => $modelPath, + 'inference.gpu_max_gpu_layers' => 64, + 'inference.gpu_vram_reserve_mb' => 0, + 'inference.gpu_min_free_vram_mb' => 0, + 'inference.gpu_thermal_max_temperature_c' => 90.5, + 'inference.gpu_allow_unmonitored' => true, +]); +$snapshot = $config->toArray(); +$status = king_inference_gpu_runtime_status($config); + +var_dump($config->get('inference.preferred_model_profile')); +var_dump($config->get('inference.gpu_model_name')); +var_dump($snapshot['inference.gpu_model_artifact'] === $modelPath); +var_dump($snapshot['inference.gpu_max_gpu_layers']); +var_dump($snapshot['inference.gpu_vram_reserve_mb']); +var_dump($snapshot['inference.gpu_min_free_vram_mb']); +var_dump($snapshot['inference.gpu_allow_unmonitored']); + +var_dump($status['gpu_enabled']); +var_dump($status['process_gpu_bindings_enable']); +var_dump($status['config_gpu_bindings_enable']); +var_dump($status['backend']); +var_dump($status['backend_supported']); +var_dump($status['artifact_path'] === $modelPath); +var_dump($status['artifact_configured']); +var_dump($status['artifact_readable']); +var_dump($status['artifact_size_available']); +var_dump($status['artifact_bytes'] > 0); +var_dump($status['max_gpu_layers']); +var_dump($status['vram_reserve_mb']); +var_dump($status['min_free_vram_mb']); +var_dump($status['thermal']['allow_unmonitored_gpu']); +var_dump($status['thermal']['monitored']); +var_dump($status['thermal']['max_temperature_c']); +var_dump($status['vram_admission_checked']); +var_dump($status['runtime_vram_compared_to_free']); +var_dump(is_bool($status['runtime_vram_fits_free'])); +var_dump(is_bool($status['config_ready'])); +var_dump(is_string($status['reason']) && $status['reason'] !== ''); +var_dump(is_array($status['refusal_reasons'])); +var_dump(array_key_exists('decoder_kernel_ready', $status)); +var_dump(array_key_exists('generation_ready', $status)); +var_dump(is_array($status['cuda_driver'])); +var_dump($status['cuda_driver']['initialized'] || $status['driver_visible']); +?> +--EXPECT-- +string(3) "gpu" +string(23) "gpu-runtime-status-test" +bool(true) +int(64) +int(0) +int(0) +bool(true) +bool(true) +bool(true) +bool(true) +string(4) "cuda" +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +int(64) +int(0) +int(0) +bool(true) +bool(false) +float(90.5) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/extension/tests/762-inference-gpu-model-metadata-contract.phpt b/extension/tests/762-inference-gpu-model-metadata-contract.phpt new file mode 100644 index 000000000..dfc16018d --- /dev/null +++ b/extension/tests/762-inference-gpu-model-metadata-contract.phpt @@ -0,0 +1,146 @@ +--TEST-- +King GPU inference model metadata is exposed through model info and OpenAI models +--INI-- +king.security_allow_config_override=1 +king.gpu_bindings_enable=1 +king.gpu_default_backend=cuda +--SKIPIF-- + +--FILE-- + 'gpu', + 'inference.gpu_model_name' => 'gpu-model-metadata-test', + 'inference.gpu_model_artifact' => $modelPath, + 'inference.gpu_max_gpu_layers' => 64, + 'inference.gpu_vram_reserve_mb' => 0, + 'inference.gpu_min_free_vram_mb' => 0, + 'inference.gpu_thermal_max_temperature_c' => 90.5, + 'inference.gpu_allow_unmonitored' => true, +]); + +$model = king_inference_runtime_model_load($config); +$info = king_inference_model_info($model); + +$response = king_inference_openai_http_response( + ['gpu-model-metadata-test' => $model], + ['method' => 'GET', 'path' => '/v1/models'] +); +$payload = json_decode($response['body'], true, 512, JSON_THROW_ON_ERROR); +$listed = $payload['data'][0]; +$king = $listed['x_king']; + +var_dump($info['name']); +var_dump($info['backend']); +var_dump($info['engine']); +var_dump($info['external_runtime']); +var_dump($info['artifact_path'] === $modelPath); +var_dump($info['artifact_bytes'] > 0); +var_dump($info['gpu_enabled']); +var_dump($info['decoder_stream_contract_ready']); +var_dump(is_bool($info['decoder_kernel_ready'])); +var_dump(is_bool($info['plain_text_chat_ready'])); +var_dump(is_bool($info['generation_ready'])); +var_dump(is_bool($info['openai_generation'])); +var_dump($info['generation_ready'] === $info['decoder_kernel_ready']); +var_dump($info['openai_generation'] === $info['generation_ready']); +var_dump($info['silent_cpu_fallback']); +var_dump($info['backend_capabilities']['gpu_backend']); +var_dump($info['backend_capabilities']['gpu_runtime_status']); +var_dump($info['backend_capabilities']['native_stream_contract']); +var_dump($info['backend_capabilities']['silent_cpu_fallback']); +var_dump(is_array($info['decoder_blockers'])); +var_dump(is_array($info['gpu_runtime'])); +var_dump($info['gpu_runtime']['backend']); +var_dump($info['gpu_runtime']['artifact_path'] === $modelPath); +var_dump($info['gpu_runtime']['generation_ready'] === $info['generation_ready']); +var_dump($info['gpu_runtime']['decoder_kernel_ready'] === $info['decoder_kernel_ready']); +var_dump(is_array($info['gpu_runtime']['cuda_driver'])); + +var_dump($response['status']); +var_dump($payload['object']); +var_dump($listed['id']); +var_dump($listed['object']); +var_dump($listed['owned_by']); +var_dump($king['backend']); +var_dump($king['backend_config_valid']); +var_dump($king['gpu_enabled']); +var_dump($king['native_stream_contract']); +var_dump($king['openai_generation'] === $info['gpu_runtime']['generation_ready']); +var_dump($king['openai_chat_completions_stream'] === $king['openai_generation']); +var_dump($king['gpu_runtime']['artifact_path'] === $modelPath); +var_dump($king['gpu_runtime']['generation_ready'] === $info['generation_ready']); +var_dump($king['gpu_runtime']['decoder_kernel_ready'] === $info['decoder_kernel_ready']); +var_dump($king['capabilities']['gpu_backend']); +var_dump($king['capabilities']['gpu_runtime_status']); +var_dump($king['capabilities']['native_stream_contract']); +var_dump($king['client_capabilities']['version']); +var_dump($king['client_capabilities']['model_selectable']); +var_dump($king['client_capabilities']['requires_gpu']); +var_dump($king['client_capabilities']['gpu_runtime_required']); +var_dump($king['client_capabilities']['gpu_generation_ready'] === $info['generation_ready']); +var_dump($king['client_capabilities']['openai_tool_calls']); +?> +--EXPECT-- +string(23) "gpu-model-metadata-test" +string(15) "king_native_gpu" +string(15) "king_native_gpu" +bool(false) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(false) +bool(true) +bool(true) +bool(true) +bool(false) +bool(true) +bool(true) +string(4) "cuda" +bool(true) +bool(true) +bool(true) +bool(true) +int(200) +string(4) "list" +string(23) "gpu-model-metadata-test" +string(5) "model" +string(12) "king-runtime" +string(15) "king_native_gpu" +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +int(1) +bool(true) +bool(true) +bool(true) +bool(true) +bool(false) diff --git a/extension/tests/763-inference-cpu-decoder-logits-contract.phpt b/extension/tests/763-inference-cpu-decoder-logits-contract.phpt new file mode 100644 index 000000000..130c83cf0 --- /dev/null +++ b/extension/tests/763-inference-cpu-decoder-logits-contract.phpt @@ -0,0 +1,91 @@ +--TEST-- +King CPU reference decoder produces logits from a local GGUF model +--SKIPIF-- + +--FILE-- + 'cpu-decoder-logits-test', + 'artifact' => [ + 'path' => $modelPath, + ], + 'backend' => 'king_native_cpu', +]); +$info = king_inference_model_info($model); +$encoded = king_inference_tokenize($model, 'Hello'); +$graph = king_inference_token_decode_graph($model, $encoded, 0, [ + 'emit_token' => false, + 'emit_logits' => true, +]); +$result = king_inference_graph_run($model, $graph, [ + 'max_vector_values' => 300000, + 'max_operations' => 500000000, + 'return_outputs' => false, +]); + +$logits = $result['final']['logits']; +$values = $logits['values']; +$sampleIndexes = [0, 100, $logits['length'] - 1]; +$finiteSamples = true; +foreach ($sampleIndexes as $index) { + $finiteSamples = $finiteSamples && isset($values[$index]) && is_finite($values[$index]); +} + +var_dump($info['name']); +var_dump($info['backend']); +var_dump($info['engine']); +var_dump($info['external_runtime']); +var_dump($info['token_generation_ready']); +var_dump($info['gguf']['decoder_ready']); +var_dump($info['backend_capabilities']['native_model_loader']); +var_dump($info['backend_capabilities']['native_token_selection']); +var_dump($info['backend_capabilities']['token_generation']); +var_dump($encoded['token_count'] > 0); +var_dump($encoded['unknown_count'] === 0); +var_dump($graph['output']); +var_dump($graph['terminal']['emits_logits']); +var_dump($graph['terminal']['emits_token']); +var_dump($graph['terminal']['token_selection']); +var_dump($graph['terminal']['output_projection_status'] !== ''); +var_dump(count($graph['ops']) > 100); +var_dump($result['output']); +var_dump($result['op_count'] === count($graph['ops'])); +var_dump($logits['length'] > 0); +var_dump($logits['length'] === count($values)); +var_dump($logits['length'] === $info['native_tokenizer_token_count']); +var_dump($finiteSamples); +var_dump(is_array($result['state']['kv_cache'])); +var_dump(count($result['state']['kv_cache']) > 0); +?> +--EXPECT-- +string(23) "cpu-decoder-logits-test" +string(15) "king_native_cpu" +string(15) "king_native_cpu" +bool(false) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +string(6) "logits" +bool(true) +bool(false) +string(4) "none" +bool(true) +bool(true) +string(6) "logits" +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/extension/tests/764-inference-king-only-hello-world-contract.phpt b/extension/tests/764-inference-king-only-hello-world-contract.phpt new file mode 100644 index 000000000..c359fa570 --- /dev/null +++ b/extension/tests/764-inference-king-only-hello-world-contract.phpt @@ -0,0 +1,67 @@ +--TEST-- +King-only hello-world command performs native prompt-to-token generation +--SKIPIF-- + +--FILE-- + getenv('PATH') ?: '/usr/bin:/bin', + 'PHP_BIN' => $php, + 'KING_EXTENSION' => $extension, + 'KING_INFERENCE_HELLO_MODEL_PATH' => $modelPath, + 'KING_INFERENCE_HELLO_PROMPT' => 'Hello world', + 'KING_INFERENCE_HELLO_TOKENS' => '1', +]; +$ldLibraryPath = getenv('LD_LIBRARY_PATH'); +if (is_string($ldLibraryPath) && $ldLibraryPath !== '') { + $env['LD_LIBRARY_PATH'] = $ldLibraryPath; +} + +$process = proc_open( + escapeshellarg($script), + [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ], + $pipes, + $root, + $env +); +if (!is_resource($process)) { + echo "proc-open-failed\n"; + exit; +} + +fclose($pipes[0]); +$stdout = (string) stream_get_contents($pipes[1]); +fclose($pipes[1]); +$stderr = (string) stream_get_contents($pipes[2]); +fclose($pipes[2]); +$exit = proc_close($process); +$combined = strtolower($stdout . $stderr); + +var_dump($exit); +var_dump($stderr === ''); +var_dump($stdout !== ''); +var_dump(!str_contains($combined, 'ollama')); +var_dump(!str_contains($combined, 'vllm')); +var_dump(!str_contains($combined, 'external model server')); +?> +--EXPECT-- +int(0) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/extension/tests/765-inference-openai-native-streaming-e2e-contract.phpt b/extension/tests/765-inference-openai-native-streaming-e2e-contract.phpt new file mode 100644 index 000000000..c1286d2af --- /dev/null +++ b/extension/tests/765-inference-openai-native-streaming-e2e-contract.phpt @@ -0,0 +1,128 @@ +--TEST-- +King OpenAI-compatible chat completions run end-to-end on native CPU streaming +--SKIPIF-- + +--FILE-- + 'openai-native-e2e-test', + 'artifact' => [ + 'path' => $modelPath, + ], + 'backend' => 'king_native_cpu', +]); +$models = ['openai-native-e2e-test' => $model]; +$options = [ + 'read_timeout_ms' => 250, + 'max_events' => 64, + 'max_idle_events' => 16, +]; + +$basePayload = [ + 'model' => 'openai-native-e2e-test', + 'messages' => [ + ['role' => 'system', 'content' => 'Reply briefly.'], + ['role' => 'user', 'content' => 'Hello world'], + ], + 'max_tokens' => 1, + 'temperature' => 0.0, + 'top_k' => 1, + 'top_p' => 1.0, + 'seed' => 1, +]; + +$jsonResponse = king_inference_openai_http_response($models, [ + 'method' => 'POST', + 'path' => '/v1/chat/completions', + 'body' => json_encode($basePayload, JSON_UNESCAPED_SLASHES), +], $options); +$jsonPayload = json_decode($jsonResponse['body'], true, 512, JSON_THROW_ON_ERROR); + +$streamPayload = $basePayload; +$streamPayload['stream'] = true; +$streamPayload['stream_options'] = ['include_usage' => true]; +$streamResponse = King\Inference::openaiHttpResponse($models, [ + 'method' => 'POST', + 'path' => '/v1/chat/completions', + 'body' => json_encode($streamPayload, JSON_UNESCAPED_SLASHES), +], $options); + +$streamLines = preg_split('/\R/', trim($streamResponse['body'])); +$dataLines = array_values(array_filter($streamLines, static fn ($line) => str_starts_with($line, 'data: '))); +$jsonChunks = []; +$doneSeen = false; +foreach ($dataLines as $line) { + $payload = substr($line, 6); + if ($payload === '[DONE]') { + $doneSeen = true; + continue; + } + $jsonChunks[] = json_decode($payload, true, 512, JSON_THROW_ON_ERROR); +} +$contentChunks = 0; +$finishChunks = 0; +$usageChunks = 0; +foreach ($jsonChunks as $chunk) { + $choices = $chunk['choices'] ?? []; + if ($choices === []) { + $usageChunks++; + continue; + } + $choice = $choices[0]; + if (isset($choice['delta']['content']) && $choice['delta']['content'] !== '') { + $contentChunks++; + } + if (($choice['finish_reason'] ?? null) === 'stop') { + $finishChunks++; + } +} + +var_dump($jsonResponse['status']); +var_dump($jsonResponse['headers']['content-type']); +var_dump($jsonPayload['object']); +var_dump($jsonPayload['model']); +var_dump($jsonPayload['choices'][0]['message']['role']); +var_dump(is_string($jsonPayload['choices'][0]['message']['content'])); +var_dump($jsonPayload['choices'][0]['message']['content'] !== ''); +var_dump($jsonPayload['choices'][0]['finish_reason']); +var_dump(is_array($jsonPayload['usage'])); + +var_dump($streamResponse['status']); +var_dump($streamResponse['headers']['content-type']); +var_dump($streamResponse['headers']['cache-control']); +var_dump($streamResponse['headers']['x-accel-buffering']); +var_dump($doneSeen); +var_dump(count($jsonChunks) >= 3); +var_dump($contentChunks >= 1); +var_dump($finishChunks === 1); +var_dump($usageChunks === 1); +var_dump($jsonChunks[0]['object']); +var_dump($jsonChunks[0]['model']); +?> +--EXPECT-- +int(200) +string(16) "application/json" +string(15) "chat.completion" +string(22) "openai-native-e2e-test" +string(9) "assistant" +bool(true) +bool(true) +string(4) "stop" +bool(true) +int(200) +string(17) "text/event-stream" +string(8) "no-cache" +string(2) "no" +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +string(21) "chat.completion.chunk" +string(22) "openai-native-e2e-test" diff --git a/extension/tests/766-inference-gemma4-12b-gpu-vram-guardrail-contract.phpt b/extension/tests/766-inference-gemma4-12b-gpu-vram-guardrail-contract.phpt new file mode 100644 index 000000000..a8cc7dd80 --- /dev/null +++ b/extension/tests/766-inference-gemma4-12b-gpu-vram-guardrail-contract.phpt @@ -0,0 +1,139 @@ +--TEST-- +King Gemma4 12B GPU profile enforces VRAM guardrails and loads admitted weights +--INI-- +king.security_allow_config_override=1 +king.gpu_bindings_enable=1 +king.gpu_default_backend=cuda +--SKIPIF-- +/dev/null'); +if (is_string($free) && preg_match('/^\s*(\d+)/', $free, $match) && (int) $match[1] < 8500) { + echo "skip Gemma4 12B GPU check needs at least 8500 MiB free VRAM before loading\n"; +} +?> +--FILE-- + 'gemma4-12b-full-context-guardrail', + 'artifact' => ['path' => $modelPath], + 'backend' => 'king_native_gpu', + 'gpu' => [ + 'enabled' => true, + 'max_gpu_layers' => 99, + 'vram_reserve_mb' => 0, + 'min_free_vram_mb' => 0, + 'thermal' => [ + 'allow_unmonitored_gpu' => true, + 'max_temperature_c' => 78.0, + ], + ], +]); +$fullInfo = king_inference_model_info($fullContext); +$fullGpu = $fullInfo['gpu_runtime']; +$fullRequiredBytes = $fullGpu['runtime_vram_required_bytes']; +unset($fullContext); +gc_collect_cycles(); + +$smallContext = king_inference_model_load([ + 'name' => 'gemma4-12b-small-context-gpu', + 'artifact' => ['path' => $modelPath], + 'backend' => 'king_native_gpu', + 'paged_attention' => [ + 'max_context_tokens' => 64, + 'page_tokens' => 16, + 'element_bytes' => 2, + ], + 'gpu' => [ + 'enabled' => true, + 'max_gpu_layers' => 99, + 'vram_reserve_mb' => 0, + 'min_free_vram_mb' => 0, + 'thermal' => [ + 'allow_unmonitored_gpu' => true, + 'max_temperature_c' => 78.0, + ], + ], +]); +$smallInfo = king_inference_model_info($smallContext); +$smallGpu = $smallInfo['gpu_runtime']; +$upload = $smallGpu['required_weight_upload']; +$paged = $smallInfo['paged_kv_cache']; + +var_dump($fullInfo['backend']); +var_dump($fullInfo['gguf']['architecture']); +var_dump($fullInfo['artifact_bytes'] > 6000000000); +var_dump($fullGpu['runtime_vram_required_source']); +var_dump($fullGpu['kv_cache_estimate_available']); +var_dump($fullGpu['runtime_vram_fits_free']); +var_dump($fullGpu['runtime_vram_required_bytes'] > $fullInfo['artifact_bytes']); +var_dump($fullGpu['reason']); +var_dump($fullInfo['silent_cpu_fallback']); + +var_dump($smallInfo['backend']); +var_dump($smallGpu['backend']); +var_dump($smallGpu['runtime_vram_required_source']); +var_dump($smallGpu['kv_cache_context_tokens']); +var_dump($paged['ready']); +var_dump($paged['max_context_tokens']); +var_dump($smallGpu['runtime_vram_fits_free']); +var_dump($smallGpu['runtime_vram_required_bytes'] < $fullRequiredBytes); +var_dump($smallGpu['device_memory_allocator']['available']); +var_dump($upload['attempted']); +var_dump($upload['complete']); +var_dump($upload['required_tensors']); +var_dump($upload['resolved_tensors'] === $upload['required_tensors']); +var_dump($upload['uploaded_tensors'] + $upload['duplicate_tensors'] === $upload['required_tensors']); +var_dump($upload['duplicate_tensors'] >= 1); +var_dump($upload['failed_tensors']); +var_dump($upload['error']); +var_dump($smallInfo['silent_cpu_fallback']); +var_dump($smallGpu['decoder_blockers']); +?> +--EXPECT-- +string(15) "king_native_gpu" +string(6) "gemma4" +bool(true) +string(22) "artifact_plus_kv_cache" +bool(true) +bool(false) +bool(true) +string(35) "gpu_required_vram_exceeds_free_vram" +bool(false) +string(15) "king_native_gpu" +string(4) "cuda" +string(22) "artifact_plus_kv_cache" +int(64) +bool(true) +int(64) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +int(435) +bool(true) +bool(true) +bool(true) +int(0) +string(0) "" +bool(false) +array(0) { +} diff --git a/extension/tests/767-inference-gpu-thermal-refusal-contract.phpt b/extension/tests/767-inference-gpu-thermal-refusal-contract.phpt new file mode 100644 index 000000000..0ff9a027f --- /dev/null +++ b/extension/tests/767-inference-gpu-thermal-refusal-contract.phpt @@ -0,0 +1,242 @@ +--TEST-- +King inference refuses overheated GPU runs before and during streaming +--INI-- +king.security_allow_config_override=1 +king.gpu_bindings_enable=1 +king.gpu_default_backend=cuda +--SKIPIF-- + +--FILE-- + ['token' => [1]], + 'ops' => [[ + 'id' => 'next_token', + 'op' => 'scale', + 'input' => 'token', + 'factor' => 1.0, + ]], + 'output' => 'next_token', + ]; +} + +function thermalModelConfig(string $modelPath, string $sensor): array +{ + return [ + 'name' => 'thermal-refusal-contract', + 'artifact' => ['path' => $modelPath], + 'backend' => 'king_native_cpu', + 'gpu' => [ + 'enabled' => true, + 'thermal' => [ + 'sensor_path' => $sensor, + 'max_temperature_c' => 80.0, + 'check_interval_seconds' => 0, + 'allow_unmonitored_gpu' => false, + ], + ], + ]; +} + +function runPreflightRefusalProcess(string $php, string $extension, string $modelPath): array +{ + $script = tempnam(sys_get_temp_dir(), 'king-thermal-refusal-child-'); + $sensor = tempnam(sys_get_temp_dir(), 'king-thermal-refusal-sensor-'); + if ($script === false || $sensor === false) { + return ['exit' => -1, 'stdout' => '', 'stderr' => 'tempnam failed']; + } + + file_put_contents($sensor, "45000\n"); + $code = <<<'PHP' + ['token' => [1]], + 'ops' => [[ + 'id' => 'next_token', + 'op' => 'scale', + 'input' => 'token', + 'factor' => 1.0, + ]], + 'output' => 'next_token', + ]; +} +function thermalModelConfig(string $modelPath, string $sensor): array +{ + return [ + 'name' => 'thermal-refusal-preflight-child', + 'artifact' => ['path' => $modelPath], + 'backend' => 'king_native_cpu', + 'gpu' => [ + 'enabled' => true, + 'thermal' => [ + 'sensor_path' => $sensor, + 'max_temperature_c' => 80.0, + 'check_interval_seconds' => 0, + 'allow_unmonitored_gpu' => false, + ], + ], + ]; +} +$model = king_inference_model_load(thermalModelConfig($modelPath, $sensor)); +file_put_contents($sensor, "91000\n"); +try { + king_inference_stream($model, ['graphs' => [thermalGraph()]], [ + 'with_memory' => false, + 'max_native_stream_tokens' => 4, + ]); + var_dump('no-exception'); +} catch (Throwable $exception) { + var_dump(get_class($exception)); + var_dump(str_contains($exception->getMessage(), 'refused GPU inference because sensor')); + var_dump(str_contains($exception->getMessage(), '91.0 C')); + var_dump(str_contains($exception->getMessage(), '80.0 C')); +} +PHP; + file_put_contents($script, $code); + + $env = [ + 'PATH' => getenv('PATH') ?: '/usr/bin:/bin', + 'KING_THERMAL_MODEL_PATH' => $modelPath, + 'KING_THERMAL_SENSOR_PATH' => $sensor, + ]; + $ldLibraryPath = getenv('LD_LIBRARY_PATH'); + if (is_string($ldLibraryPath) && $ldLibraryPath !== '') { + $env['LD_LIBRARY_PATH'] = $ldLibraryPath; + } + $command = escapeshellarg($php) + . ' -n -d extension=posix -d extension=sockets' + . ' -d extension=' . escapeshellarg($extension) + . ' -d king.security_allow_config_override=1' + . ' -d king.gpu_bindings_enable=1' + . ' -d king.gpu_default_backend=cuda ' + . escapeshellarg($script); + $process = proc_open( + $command, + [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ], + $pipes, + $GLOBALS['root'], + $env + ); + if (!is_resource($process)) { + @unlink($script); + @unlink($sensor); + return ['exit' => -1, 'stdout' => '', 'stderr' => 'proc_open failed']; + } + + fclose($pipes[0]); + $stdout = (string) stream_get_contents($pipes[1]); + fclose($pipes[1]); + $stderr = (string) stream_get_contents($pipes[2]); + fclose($pipes[2]); + $exit = proc_close($process); + @unlink($script); + @unlink($sensor); + return ['exit' => $exit, 'stdout' => $stdout, 'stderr' => $stderr]; +} + +$preflight = runPreflightRefusalProcess($php, $extension, $modelPath); +var_dump($preflight['exit']); +var_dump($preflight['stderr'] === ''); +var_dump(str_contains($preflight['stdout'], 'string(21) "King\\RuntimeException"')); +var_dump(substr_count($preflight['stdout'], 'bool(true)') === 3); + +$sensor = tempnam(sys_get_temp_dir(), 'king-thermal-refusal-sensor-'); +if ($sensor === false) { + echo "sensor-tempnam-failed\n"; + exit; +} + +try { + file_put_contents($sensor, "45000\n"); + $model = king_inference_model_load(thermalModelConfig($modelPath, $sensor)); + $stream = new King\Inference\Stream($model, ['graphs' => [thermalGraph()]], [ + 'with_memory' => false, + 'max_native_stream_tokens' => 4, + ]); + $start = $stream->next(0); + + var_dump($start['type']); + var_dump($start['backend']); + var_dump($start['native_stream']); + var_dump($start['gpu_thermal_preflight_checked']); + var_dump($start['gpu_thermal_preflight_temperature_c']); + + file_put_contents($sensor, "91000\n"); + try { + $stream->next(0); + var_dump('no-abort'); + } catch (Throwable $exception) { + var_dump(get_class($exception)); + var_dump(str_contains($exception->getMessage(), 'refused GPU inference because sensor')); + var_dump(str_contains($exception->getMessage(), '91.0 C')); + var_dump(str_contains($exception->getMessage(), '80.0 C')); + } + + $metrics = $stream->getMetrics(); + var_dump($metrics['done']); + var_dump($metrics['cancelled']); + var_dump($metrics['gpu_thermal_aborted']); + var_dump($metrics['gpu_thermal_abort_temperature_c']); + var_dump($metrics['gpu_thermal_abort_ceiling_c']); + var_dump($metrics['gpu_thermal_preflight_checked']); + var_dump($metrics['gpu_thermal_preflight_temperature_available']); + var_dump($metrics['gpu_thermal_preflight_temperature_c']); + var_dump($metrics['native_stream']); +} finally { + @unlink($sensor); +} +?> +--EXPECT-- +int(0) +bool(true) +bool(true) +bool(true) +string(5) "start" +string(15) "king_native_cpu" +bool(true) +bool(true) +float(45) +string(21) "King\RuntimeException" +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +float(91) +float(80) +bool(true) +bool(true) +float(45) +bool(true) diff --git a/extension/tests/768-inference-gpu-profile-no-cpu-fallback-contract.phpt b/extension/tests/768-inference-gpu-profile-no-cpu-fallback-contract.phpt new file mode 100644 index 000000000..4b6d571d1 --- /dev/null +++ b/extension/tests/768-inference-gpu-profile-no-cpu-fallback-contract.phpt @@ -0,0 +1,137 @@ +--TEST-- +King GPU runtime profile never falls back to CPU generation silently +--INI-- +king.security_allow_config_override=1 +king.gpu_bindings_enable=1 +king.gpu_default_backend=cuda +--SKIPIF-- + +--FILE-- + 'gpu', + 'inference.cpu_model_name' => 'cpu-fallback-must-not-run', + 'inference.cpu_model_artifact' => $modelPath, + 'inference.gpu_model_name' => 'gpu-profile-no-fallback-test', + 'inference.gpu_model_artifact' => $modelPath, + 'inference.gpu_max_gpu_layers' => 64, + 'inference.gpu_vram_reserve_mb' => 0, + 'inference.gpu_min_free_vram_mb' => 999999, + 'inference.gpu_thermal_max_temperature_c' => 95.0, + 'inference.gpu_allow_unmonitored' => true, +]); + +$modelConfig = king_inference_runtime_model_config($config); +$model = king_inference_runtime_model_load($config); +$info = king_inference_model_info($model); +$modelsResponse = king_inference_openai_http_response( + ['gpu-profile-no-fallback-test' => $model], + ['method' => 'GET', 'path' => '/v1/models'] +); +$modelsPayload = json_decode($modelsResponse['body'], true, 512, JSON_THROW_ON_ERROR); +$listed = $modelsPayload['data'][0]['x_king']; + +$chatResponse = king_inference_openai_http_response( + ['gpu-profile-no-fallback-test' => $model], + [ + 'method' => 'POST', + 'path' => '/v1/chat/completions', + 'body' => json_encode([ + 'model' => 'gpu-profile-no-fallback-test', + 'messages' => [ + ['role' => 'user', 'content' => 'Hello world'], + ], + 'max_tokens' => 1, + 'temperature' => 0.0, + ], JSON_UNESCAPED_SLASHES), + ], + ['read_timeout_ms' => 100, 'max_events' => 8] +); +$chatPayload = json_decode($chatResponse['body'], true, 512, JSON_THROW_ON_ERROR); +$message = $chatPayload['error']['message'] ?? ''; + +var_dump($modelConfig['runtime_requested_profile']); +var_dump($modelConfig['runtime_profile']); +var_dump($modelConfig['backend']); +var_dump($modelConfig['name']); +var_dump($modelConfig['runtime_cpu_model_name']); +var_dump($modelConfig['runtime_gpu_profile_available']); +var_dump($modelConfig['gpu']['enabled']); +var_dump($modelConfig['gpu']['min_free_vram_mb']); + +var_dump($info['backend']); +var_dump($info['name']); +var_dump($info['gpu_enabled']); +var_dump($info['silent_cpu_fallback']); +var_dump($info['backend_capabilities']['silent_cpu_fallback']); +var_dump($info['gpu_runtime']['min_free_vram_mb']); +var_dump($info['gpu_runtime']['generation_ready']); +var_dump(in_array('gpu_free_vram_below_configured_floor', $info['gpu_runtime']['refusal_reasons'], true)); + +var_dump($modelsResponse['status']); +var_dump($listed['backend']); +var_dump($listed['client_capabilities']['requires_gpu']); +var_dump($listed['client_capabilities']['gpu_runtime_required']); +var_dump($listed['client_capabilities']['gpu_generation_ready']); +var_dump($listed['openai_generation']); +var_dump($listed['gpu_runtime']['generation_ready']); + +var_dump($chatResponse['status']); +var_dump($chatResponse['headers']['content-type']); +var_dump($chatPayload['error']['type']); +var_dump(str_contains($message, 'selected King GPU model')); +var_dump(str_contains($message, 'will not silently fall back to CPU')); +var_dump(str_contains($chatResponse['body'], 'chat.completion')); +var_dump(str_contains($chatResponse['body'], 'cpu-fallback-must-not-run')); +?> +--EXPECT-- +string(3) "gpu" +string(3) "gpu" +string(15) "king_native_gpu" +string(28) "gpu-profile-no-fallback-test" +string(25) "cpu-fallback-must-not-run" +bool(true) +bool(true) +int(999999) +string(15) "king_native_gpu" +string(28) "gpu-profile-no-fallback-test" +bool(true) +bool(false) +bool(false) +int(999999) +bool(false) +bool(true) +int(200) +string(15) "king_native_gpu" +bool(true) +bool(true) +bool(false) +bool(false) +bool(false) +int(400) +string(16) "application/json" +string(21) "invalid_request_error" +bool(true) +bool(true) +bool(false) +bool(false) diff --git a/extension/tests/769-inference-status-cli-contract.phpt b/extension/tests/769-inference-status-cli-contract.phpt new file mode 100644 index 000000000..4b3249731 --- /dev/null +++ b/extension/tests/769-inference-status-cli-contract.phpt @@ -0,0 +1,144 @@ +--TEST-- +King inference status CLI reports local readiness and hard GPU requirements +--SKIPIF-- + +--FILE-- + getenv('PATH') ?: '/usr/bin:/bin', + 'PHP_BIN' => $php, + 'KING_EXTENSION' => $extension, + 'KING_INFERENCE_PHP_INI' => $ini, + ]; + $ldLibraryPath = getenv('LD_LIBRARY_PATH'); + if (is_string($ldLibraryPath) && $ldLibraryPath !== '') { + $env['LD_LIBRARY_PATH'] = $ldLibraryPath; + } + $command = escapeshellarg($script); + foreach ($args as $arg) { + $command .= ' ' . escapeshellarg($arg); + } + $process = proc_open( + $command, + [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ], + $pipes, + dirname($script, 2), + $env + ); + if (!is_resource($process)) { + return ['exit' => -1, 'stdout' => '', 'stderr' => 'proc_open failed']; + } + + fclose($pipes[0]); + $stdout = (string) stream_get_contents($pipes[1]); + fclose($pipes[1]); + $stderr = (string) stream_get_contents($pipes[2]); + fclose($pipes[2]); + return ['exit' => proc_close($process), 'stdout' => $stdout, 'stderr' => $stderr]; +} + +try { + $ready = runStatusCli($script, $php, $extension, $ini, ['--json']); + $readyPayload = json_decode($ready['stdout'], true, 512, JSON_THROW_ON_ERROR); + + var_dump($ready['exit']); + var_dump($ready['stderr'] === ''); + var_dump($readyPayload['ready']); + var_dump($readyPayload['exit_code']); + var_dump($readyPayload['runtime']['requested_profile']); + var_dump($readyPayload['runtime']['selected_profile']); + var_dump($readyPayload['runtime']['backend']); + var_dump($readyPayload['runtime']['model']); + var_dump($readyPayload['model']['loaded']); + var_dump($readyPayload['model']['generation_ready']); + var_dump($readyPayload['router']['model_listing_ready']); + var_dump($readyPayload['router']['openai_chat_ready']); + var_dump($readyPayload['require_gpu']); + + $requireGpu = runStatusCli($script, $php, $extension, $ini, ['--require-gpu', '--json']); + $requireGpuPayload = json_decode($requireGpu['stdout'], true, 512, JSON_THROW_ON_ERROR); + + var_dump($requireGpu['exit']); + var_dump($requireGpu['stderr'] === ''); + var_dump($requireGpuPayload['ready']); + var_dump($requireGpuPayload['exit_code']); + var_dump($requireGpuPayload['reason']); + var_dump($requireGpuPayload['refusal_reasons']); + var_dump($requireGpuPayload['runtime']['backend']); + var_dump($requireGpuPayload['require_gpu']); +} finally { + @unlink($ini); +} +?> +--EXPECT-- +int(0) +bool(true) +bool(true) +int(0) +string(3) "cpu" +string(3) "cpu" +string(15) "king_native_cpu" +string(14) "status-cli-cpu" +bool(true) +bool(true) +bool(true) +bool(true) +bool(false) +int(2) +bool(true) +bool(false) +int(2) +string(24) "gpu_profile_not_selected" +array(1) { + [0]=> + string(24) "gpu_profile_not_selected" +} +string(15) "king_native_cpu" +bool(true) diff --git a/extension/tests/770-inference-router-runtime-log-lines-contract.phpt b/extension/tests/770-inference-router-runtime-log-lines-contract.phpt new file mode 100644 index 000000000..adbce5201 --- /dev/null +++ b/extension/tests/770-inference-router-runtime-log-lines-contract.phpt @@ -0,0 +1,90 @@ +--TEST-- +King local inference router log helpers distinguish configured, admitted, and executing states +--SKIPIF-- + +--FILE-- + 'router-log-test', + 'artifact' => ['path' => $modelPath], + 'backend' => 'king_native_cpu', +]); + +$log = fopen('php://temp', 'w+'); +if (!is_resource($log)) { + echo "log-open-failed\n"; + exit; +} + +king_inference_runtime_log_configured([ + 'host' => '127.0.0.1', + 'port' => 8080, + 'profile' => 'cpu', + 'backend' => 'king_native_cpu', + 'cpu_model' => 'router-log-test', + 'cpu_artifact_readable' => true, + 'gpu_enabled' => false, +], $log); +king_inference_runtime_log_model_admitted('router-log-test', $model, $log); +king_inference_runtime_log_request_executing([ + 'method' => 'POST', + 'path' => '/v1/chat/completions', + 'body' => json_encode([ + 'model' => 'router-log-test', + 'messages' => [ + ['role' => 'user', 'content' => 'do not log this prompt'], + ], + ], JSON_UNESCAPED_SLASHES), +], ['router-log-test' => $model], $log); + +rewind($log); +$output = (string) stream_get_contents($log); +$lines = array_values(array_filter(explode("\n", trim($output)))); + +var_dump(count($lines)); +var_dump(str_contains($lines[0], 'king-inference state=configured')); +var_dump(str_contains($lines[0], 'profile=cpu')); +var_dump(str_contains($lines[0], 'cpu_artifact_readable=yes')); +var_dump(str_contains($lines[1], 'king-inference state=admitted')); +var_dump(str_contains($lines[1], 'model=router-log-test')); +var_dump(str_contains($lines[1], 'backend=king_native_cpu')); +var_dump(str_contains($lines[1], 'admitted=yes')); +var_dump(str_contains($lines[1], 'generation_ready=yes')); +var_dump(str_contains($lines[2], 'king-inference state=executing')); +var_dump(str_contains($lines[2], 'method=POST')); +var_dump(str_contains($lines[2], 'path=/v1/chat/completions')); +var_dump(str_contains($lines[2], 'requested_model=router-log-test')); +var_dump(!str_contains($output, 'do not log this prompt')); +?> +--EXPECT-- +int(3) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/extension/tests/771-inference-llm-cache-disk-floor-contract.phpt b/extension/tests/771-inference-llm-cache-disk-floor-contract.phpt new file mode 100644 index 000000000..02eb6ba32 --- /dev/null +++ b/extension/tests/771-inference-llm-cache-disk-floor-contract.phpt @@ -0,0 +1,148 @@ +--TEST-- +King memory-enabled inference checks LLM cache disk floor before streaming +--INI-- +king.security_allow_config_override=1 +--SKIPIF-- + +--FILE-- + ['token' => [1]], + 'ops' => [[ + 'id' => 'next_token', + 'op' => 'scale', + 'input' => 'token', + 'factor' => 1.0, + ]], + 'output' => 'next_token', + ]; +} + +function llmCacheModel(string $modelPath, string $cachePath, int $minFreeMb, bool $failClosed): King\Inference\Model +{ + return king_inference_model_load([ + 'name' => $failClosed ? 'llm-cache-fail-closed' : 'llm-cache-fail-open', + 'artifact' => ['path' => $modelPath], + 'backend' => 'king_native_cpu', + 'llm_cache' => [ + 'enabled' => true, + 'path' => $cachePath, + 'min_free_mb' => $minFreeMb, + 'fail_closed' => $failClosed, + 'disk_alert_webhook' => 'https://ops.example/cache-alert', + 'disk_alert_mcp_service' => 'ops.cache', + 'disk_alert_mcp_method' => 'diskFloorWarning', + ], + ]); +} + +try { + $config = King\Config::new([ + 'inference.with_memory' => true, + 'inference.llm_cache_enable' => true, + 'inference.llm_cache_path' => $cachePath, + 'inference.llm_cache_min_free_mb' => $minFreeMb, + 'inference.llm_cache_fail_closed' => true, + 'inference.llm_cache_disk_alert_webhook' => 'https://ops.example/cache-alert', + 'inference.llm_cache_disk_alert_mcp_service' => 'ops.cache', + 'inference.llm_cache_disk_alert_mcp_method' => 'diskFloorWarning', + ]); + $status = king_inference_llm_cache_status($config, ['with_memory' => true]); + + var_dump($status['type']); + var_dump($status['enabled']); + var_dump($status['active']); + var_dump($status['with_memory']); + var_dump($status['path'] === $cachePath); + var_dump($status['min_free_mb']); + var_dump($status['ok']); + var_dump($status['degraded']); + var_dump($status['action']); + var_dump($status['free_bytes'] > 0); + var_dump($status['free_mb'] > 0); + var_dump($status['alert']['requested']); + var_dump($status['alert']['webhook_configured']); + var_dump($status['alert']['mcp_configured']); + + $closedModel = llmCacheModel($modelPath, $cachePath, $minFreeMb, true); + try { + new King\Inference\Stream($closedModel, ['graphs' => [llmCacheGraph()]], [ + 'with_memory' => true, + 'max_native_stream_tokens' => 2, + ]); + var_dump('fail-closed-accepted'); + } catch (Throwable $exception) { + var_dump(get_class($exception)); + var_dump(str_contains($exception->getMessage(), 'LLM cache disk floor')); + } + + $openModel = llmCacheModel($modelPath, $cachePath, $minFreeMb, false); + $stream = new King\Inference\Stream($openModel, ['graphs' => [llmCacheGraph()]], [ + 'with_memory' => true, + 'max_native_stream_tokens' => 2, + ]); + $start = $stream->next(0); + $cacheEvent = $stream->next(0); + + var_dump($start['type']); + var_dump($cacheEvent['type']); + var_dump($cacheEvent['active']); + var_dump($cacheEvent['ok']); + var_dump($cacheEvent['degraded']); + var_dump($cacheEvent['action']); + var_dump($cacheEvent['alert']['requested']); + var_dump($cacheEvent['alert']['webhook']); + var_dump($cacheEvent['alert']['mcp_service']); + var_dump($cacheEvent['alert']['mcp_method']); +} finally { + if (is_dir($cachePath)) { + @rmdir($cachePath); + } +} +?> +--EXPECT-- +string(16) "llm_cache_status" +bool(true) +bool(true) +bool(true) +bool(true) +int(999999999) +bool(false) +bool(true) +string(17) "disk_floor_failed" +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +string(21) "King\RuntimeException" +bool(true) +string(5) "start" +string(16) "llm_cache_status" +bool(true) +bool(false) +bool(true) +string(17) "disk_floor_failed" +bool(true) +string(31) "https://ops.example/cache-alert" +string(9) "ops.cache" +string(16) "diskFloorWarning" diff --git a/extension/tests/772-inference-llm-cache-default-off-contract.phpt b/extension/tests/772-inference-llm-cache-default-off-contract.phpt new file mode 100644 index 000000000..e254c6c6a --- /dev/null +++ b/extension/tests/772-inference-llm-cache-default-off-contract.phpt @@ -0,0 +1,77 @@ +--TEST-- +King inference LLM cache is disabled by default until explicitly configured +--INI-- +king.security_allow_config_override=1 +--FILE-- +toArray(); +var_dump($defaultConfig->get('inference.with_memory')); +var_dump($defaultConfig->get('inference.llm_cache_enable')); +var_dump($defaultSnapshot['inference.with_memory']); +var_dump($defaultSnapshot['inference.llm_cache_enable']); + +$statusWithoutMemory = king_inference_llm_cache_status(); +var_dump($statusWithoutMemory['enabled']); +var_dump($statusWithoutMemory['active']); +var_dump($statusWithoutMemory['with_memory']); +var_dump($statusWithoutMemory['ok']); +var_dump($statusWithoutMemory['degraded']); +var_dump($statusWithoutMemory['action']); + +$statusWithMemoryRequest = king_inference_llm_cache_status(null, ['with_memory' => true]); +var_dump($statusWithMemoryRequest['enabled']); +var_dump($statusWithMemoryRequest['active']); +var_dump($statusWithMemoryRequest['with_memory']); +var_dump($statusWithMemoryRequest['ok']); +var_dump($statusWithMemoryRequest['degraded']); +var_dump($statusWithMemoryRequest['action']); +var_dump($statusWithMemoryRequest['path']); +var_dump($statusWithMemoryRequest['min_free_mb']); +var_dump($statusWithMemoryRequest['fail_closed']); +var_dump($statusWithMemoryRequest['alert']['requested']); + +$enabledConfig = King\Config::new([ + 'inference.with_memory' => true, + 'inference.llm_cache_enable' => true, +]); +$enabledStatus = king_inference_llm_cache_status($enabledConfig, ['with_memory' => true]); +var_dump($enabledConfig->get('inference.llm_cache_enable')); +var_dump($enabledStatus['enabled']); +var_dump($enabledStatus['active']); +var_dump($enabledStatus['with_memory']); +var_dump(is_bool($enabledStatus['ok'])); +var_dump(is_string($enabledStatus['action'])); +?> +--EXPECT-- +string(1) "0" +string(1) "0" +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(true) +bool(false) +string(23) "disabled_without_memory" +bool(false) +bool(false) +bool(true) +bool(true) +bool(false) +string(18) "disabled_by_config" +string(19) "/tmp/king-llm-cache" +int(5120) +bool(true) +bool(false) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/infra/inference/bench-openai-route.php b/infra/inference/bench-openai-route.php new file mode 100644 index 000000000..382e8887f --- /dev/null +++ b/infra/inference/bench-openai-route.php @@ -0,0 +1,370 @@ += N. + --allow-hot Do not refuse a hot GPU. + --json Print only JSON metrics. + +The router must already be running. This script does not start King or the GPU. +Stream usage is requested by default so prompt/completion tokens are tokenizer-backed +when the selected King model can count them. +TXT); +} + +function king_bench_option(array $argv, string $name, ?string $default = null): ?string +{ + $prefix = $name . '='; + $count = count($argv); + for ($i = 1; $i < $count; $i++) { + $arg = $argv[$i]; + if ($arg === $name && isset($argv[$i + 1])) { + return $argv[$i + 1]; + } + if (str_starts_with($arg, $prefix)) { + return substr($arg, strlen($prefix)); + } + } + return $default; +} + +function king_bench_flag(array $argv, string $name): bool +{ + return in_array($name, $argv, true); +} + +function king_bench_int_option(array $argv, string $name, int $default): int +{ + $value = king_bench_option($argv, $name); + if ($value === null || !preg_match('/^\d+$/', $value)) { + return $default; + } + return (int) $value; +} + +function king_bench_float_option(array $argv, string $name, float $default): float +{ + $value = king_bench_option($argv, $name); + if ($value === null || !is_numeric($value)) { + return $default; + } + return (float) $value; +} + +function king_bench_gpu_snapshot(): array +{ + $command = 'nvidia-smi --query-gpu=temperature.gpu,utilization.gpu,memory.used,memory.total,power.draw --format=csv,noheader,nounits 2>/dev/null'; + $lines = []; + $exitCode = 0; + exec($command, $lines, $exitCode); + if ($exitCode !== 0 || $lines === []) { + return ['available' => false]; + } + + $parts = array_map('trim', explode(',', $lines[0])); + return [ + 'available' => true, + 'temperature_c' => isset($parts[0]) && is_numeric($parts[0]) ? (float) $parts[0] : null, + 'utilization_percent' => isset($parts[1]) && is_numeric($parts[1]) ? (float) $parts[1] : null, + 'vram_used_mb' => isset($parts[2]) && is_numeric($parts[2]) ? (float) $parts[2] : null, + 'vram_total_mb' => isset($parts[3]) && is_numeric($parts[3]) ? (float) $parts[3] : null, + 'power_w' => isset($parts[4]) && is_numeric($parts[4]) ? (float) $parts[4] : null, + ]; +} + +function king_bench_prompt(array $argv): string +{ + $promptFile = king_bench_option($argv, '--prompt-file'); + if ($promptFile !== null) { + $content = @file_get_contents($promptFile); + if (!is_string($content)) { + fwrite(STDERR, "king-openai-bench: cannot read prompt file: {$promptFile}\n"); + exit(2); + } + return $content; + } + + $prompt = king_bench_option($argv, '--prompt'); + if ($prompt !== null) { + return $prompt; + } + + $promptTokens = king_bench_int_option($argv, '--prompt-tokens', 0); + if ($promptTokens > 0) { + $words = []; + for ($i = 0; $i < $promptTokens; $i++) { + $words[] = 'context' . ($i % 97); + } + return implode(' ', $words) . "\nAnswer with one concise sentence."; + } + + return 'Say hello in one short sentence.'; +} + +function king_bench_estimated_tokens(string $text): int +{ + $words = preg_split('/\s+/', trim($text)); + $wordCount = is_array($words) && $words !== [''] ? count($words) : 0; + return max(1, $wordCount); +} + +function king_bench_models_url(string $chatUrl): string +{ + return preg_replace('#/chat/completions$#', '/models', $chatUrl) ?? $chatUrl; +} + +function king_bench_model_state(string $chatUrl, string $model): array +{ + $state = [ + 'listed' => false, + 'resident' => null, + 'openai_generation' => null, + 'backend' => null, + 'gpu_generation_ready' => null, + 'gpu_reason' => null, + ]; + $context = stream_context_create([ + 'http' => [ + 'method' => 'GET', + 'timeout' => 1.5, + 'ignore_errors' => true, + ], + ]); + $json = @file_get_contents(king_bench_models_url($chatUrl), false, $context); + if (!is_string($json) || $json === '') { + return $state; + } + $decoded = json_decode($json, true); + if (!is_array($decoded) || !is_array($decoded['data'] ?? null)) { + return $state; + } + foreach ($decoded['data'] as $item) { + if (is_array($item) && (($item['id'] ?? null) === $model)) { + $king = is_array($item['x_king'] ?? null) ? $item['x_king'] : []; + $truth = is_array($king['runtime_truth'] ?? null) ? $king['runtime_truth'] : []; + $gpuRuntime = is_array($king['gpu_runtime'] ?? null) ? $king['gpu_runtime'] : []; + + $state['listed'] = true; + $state['resident'] = is_bool($truth['model_resident'] ?? null) ? $truth['model_resident'] : null; + $state['openai_generation'] = is_bool($king['openai_generation'] ?? null) ? $king['openai_generation'] : null; + $state['backend'] = is_string($king['backend'] ?? null) ? $king['backend'] : null; + $state['gpu_generation_ready'] = is_bool($gpuRuntime['generation_ready'] ?? null) + ? $gpuRuntime['generation_ready'] + : null; + $state['gpu_reason'] = is_string($gpuRuntime['reason'] ?? null) ? $gpuRuntime['reason'] : null; + return $state; + } + } + return $state; +} + +function king_bench_gpu_delta(array $before, array $after): array +{ + $fields = ['temperature_c', 'utilization_percent', 'vram_used_mb', 'vram_total_mb', 'power_w']; + $delta = []; + + foreach ($fields as $field) { + $beforeValue = $before[$field] ?? null; + $afterValue = $after[$field] ?? null; + $delta[$field] = is_float($beforeValue) && is_float($afterValue) + ? $afterValue - $beforeValue + : null; + } + + return $delta; +} + +function king_bench_sse_content(array $event): string +{ + $choice = $event['choices'][0] ?? null; + if (!is_array($choice)) { + return ''; + } + $delta = $choice['delta'] ?? null; + if (is_array($delta) && is_string($delta['content'] ?? null)) { + return $delta['content']; + } + $message = $choice['message'] ?? null; + if (is_array($message) && is_string($message['content'] ?? null)) { + return $message['content']; + } + return ''; +} + +if (king_bench_flag($argv, '--help') || king_bench_flag($argv, '-h')) { + king_bench_usage(); + exit(0); +} + +$url = king_bench_option($argv, '--url', getenv('KING_OPENAI_BENCH_URL') ?: 'http://127.0.0.1:8080/v1/chat/completions'); +$model = king_bench_option($argv, '--model', getenv('KING_OPENAI_BENCH_MODEL') ?: getenv('KING_INFERENCE_GPU_MODEL_NAME') ?: 'gemma4:12b'); +$maxTokens = king_bench_int_option($argv, '--max-tokens', 128); +$thermalCeiling = king_bench_float_option($argv, '--thermal-ceiling', (float) (getenv('KING_INFERENCE_GPU_THERMAL_MAX_TEMPERATURE_C') ?: 78)); +$allowHot = king_bench_flag($argv, '--allow-hot'); +$jsonOnly = king_bench_flag($argv, '--json'); +$prompt = king_bench_prompt($argv); +$gpuBefore = king_bench_gpu_snapshot(); + +if (!$allowHot + && ($gpuBefore['available'] ?? false) + && is_float($gpuBefore['temperature_c'] ?? null) + && $gpuBefore['temperature_c'] >= $thermalCeiling +) { + fwrite(STDERR, "king-openai-bench: refusing request, GPU is {$gpuBefore['temperature_c']}C and ceiling is {$thermalCeiling}C\n"); + exit(3); +} + +$payload = [ + 'model' => $model, + 'stream' => true, + 'messages' => [ + ['role' => 'user', 'content' => $prompt], + ], + 'max_tokens' => $maxTokens, + 'temperature' => 0, + 'stream_options' => [ + 'include_usage' => true, + ], +]; +$body = (string) json_encode($payload, JSON_UNESCAPED_SLASHES); +$modelStateBefore = king_bench_model_state($url, $model); + +$context = stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => "content-type: application/json\r\naccept: text/event-stream\r\nconnection: close\r\n", + 'content' => $body, + 'ignore_errors' => true, + 'timeout' => 180, + ], +]); + +$startNs = hrtime(true); +$firstByteNs = null; +$firstContentNs = null; +$contentChunks = 0; +$contentBytes = 0; +$usage = null; +$errorEvents = []; +$handle = @fopen($url, 'rb', false, $context); +if ($handle === false) { + fwrite(STDERR, "king-openai-bench: cannot connect to {$url}\n"); + exit(4); +} + +while (!feof($handle)) { + $line = fgets($handle); + if ($line === false) { + usleep(10_000); + continue; + } + if ($firstByteNs === null && $line !== '') { + $firstByteNs = hrtime(true); + } + $line = trim($line); + if ($line === '' || str_starts_with($line, ':')) { + continue; + } + if (str_starts_with($line, 'event: error')) { + $errorEvents[] = 'error'; + continue; + } + if (!str_starts_with($line, 'data: ')) { + continue; + } + $data = substr($line, 6); + if ($data === '[DONE]') { + break; + } + $event = json_decode($data, true); + if (!is_array($event)) { + continue; + } + if (is_array($event['usage'] ?? null)) { + $usage = $event['usage']; + } + $content = king_bench_sse_content($event); + if ($content === '') { + continue; + } + if ($firstContentNs === null) { + $firstContentNs = hrtime(true); + } + $contentChunks++; + $contentBytes += strlen($content); +} +fclose($handle); +$endNs = hrtime(true); +$gpuAfter = king_bench_gpu_snapshot(); +$modelStateAfter = king_bench_model_state($url, $model); + +$totalMs = ($endNs - $startNs) / 1_000_000; +$ttfbMs = $firstContentNs !== null ? ($firstContentNs - $startNs) / 1_000_000 : null; +$firstByteMs = $firstByteNs !== null ? ($firstByteNs - $startNs) / 1_000_000 : null; +$decodeSeconds = $firstContentNs !== null ? max(0.001, ($endNs - $firstContentNs) / 1_000_000_000) : null; +$promptTokens = is_array($usage) && is_int($usage['prompt_tokens'] ?? null) + ? $usage['prompt_tokens'] + : null; +$generatedTokens = is_array($usage) && is_int($usage['completion_tokens'] ?? null) + ? $usage['completion_tokens'] + : $contentChunks; +$tokensPerSecond = $decodeSeconds !== null ? $generatedTokens / $decodeSeconds : null; + +$metrics = [ + 'url' => $url, + 'model' => $model, + 'model_state_before_request' => $modelStateBefore, + 'model_state_after_request' => $modelStateAfter, + 'resident_before_request' => $modelStateBefore['resident'], + 'resident_after_request' => $modelStateAfter['resident'], + 'request_body_bytes' => strlen($body), + 'tool_count' => 0, + 'prompt_tokens' => $promptTokens, + 'prompt_tokens_estimate' => king_bench_estimated_tokens($prompt), + 'requested_max_tokens' => $maxTokens, + 'generated_tokens' => $generatedTokens, + 'generated_tokens_stream_chunks' => $contentChunks, + 'generated_content_bytes' => $contentBytes, + 'first_byte_ms' => $firstByteMs, + 'ttfb_ms' => $ttfbMs, + 'total_ms' => $totalMs, + 'tokens_per_second' => $tokensPerSecond, + 'usage' => $usage, + 'thermal_ceiling_c' => $thermalCeiling, + 'gpu_before' => $gpuBefore, + 'gpu_after' => $gpuAfter, + 'gpu_delta' => king_bench_gpu_delta($gpuBefore, $gpuAfter), + 'error_events' => $errorEvents, +]; + +if ($jsonOnly) { + echo json_encode($metrics, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; + exit(0); +} + +foreach ($metrics as $key => $value) { + if (is_array($value)) { + echo $key . '=' . json_encode($value, JSON_UNESCAPED_SLASHES) . "\n"; + continue; + } + if ($value === null) { + echo $key . "=null\n"; + continue; + } + if (is_bool($value)) { + echo $key . '=' . ($value ? 'yes' : 'no') . "\n"; + continue; + } + echo $key . '=' . $value . "\n"; +} diff --git a/infra/inference/fine-tune/coder-dataset.php b/infra/inference/fine-tune/coder-dataset.php new file mode 100644 index 000000000..fdce06874 --- /dev/null +++ b/infra/inference/fine-tune/coder-dataset.php @@ -0,0 +1,440 @@ + 'prepare', + 'model' => getenv('KING_FINE_TUNE_MODEL_PATH') ?: $root . '/var/inference-models/gemma3-1b.gguf', + 'out' => getenv('KING_FINE_TUNE_OUTPUT_DIR') ?: $root . '/var/fine-tuning/gemma3-1b-coder', + 'sources' => ['docs'], + 'max_tokens' => getenv('KING_FINE_TUNE_MAX_TOKENS') ?: '2048', + 'limit' => getenv('KING_FINE_TUNE_LIMIT') ?: '0', + 'trainable_base' => getenv('KING_FINE_TUNE_TRAINABLE_BASE') ?: '', + ]; + + if (count($argv) > 1 && !str_starts_with((string) $argv[1], '-')) { + $options['command'] = (string) $argv[1]; + $start = 2; + } else { + $start = 1; + } + + $options['sources'] = []; + for ($i = $start; $i < count($argv); $i++) { + $arg = (string) $argv[$i]; + $next = static function () use (&$i, $argv, $arg): string { + if (!array_key_exists($i + 1, $argv)) { + king_ft_fail("missing value for {$arg}", 64); + } + return (string) $argv[++$i]; + }; + + if ($arg === '--help' || $arg === '-h') { + king_ft_usage(); + } elseif (str_starts_with($arg, '--model=')) { + $options['model'] = substr($arg, strlen('--model=')); + } elseif ($arg === '--model') { + $options['model'] = $next(); + } elseif (str_starts_with($arg, '--out=')) { + $options['out'] = substr($arg, strlen('--out=')); + } elseif ($arg === '--out') { + $options['out'] = $next(); + } elseif (str_starts_with($arg, '--source=')) { + $options['sources'][] = substr($arg, strlen('--source=')); + } elseif ($arg === '--source') { + $options['sources'][] = $next(); + } elseif (str_starts_with($arg, '--max-tokens=')) { + $options['max_tokens'] = substr($arg, strlen('--max-tokens=')); + } elseif ($arg === '--max-tokens') { + $options['max_tokens'] = $next(); + } elseif (str_starts_with($arg, '--limit=')) { + $options['limit'] = substr($arg, strlen('--limit=')); + } elseif ($arg === '--limit') { + $options['limit'] = $next(); + } elseif (str_starts_with($arg, '--trainable-base=')) { + $options['trainable_base'] = substr($arg, strlen('--trainable-base=')); + } elseif ($arg === '--trainable-base') { + $options['trainable_base'] = $next(); + } else { + king_ft_fail("unsupported argument {$arg}", 64); + } + } + + if ($options['command'] !== 'prepare') { + king_ft_fail('only the prepare command is implemented; in-King training kernels are not implemented yet', 64); + } + if ($options['sources'] === []) { + $options['sources'] = ['docs']; + } + if (!preg_match('/^[1-9][0-9]*$/', (string) $options['max_tokens'])) { + king_ft_fail('--max-tokens must be a positive integer', 64); + } + $options['max_tokens'] = (int) $options['max_tokens']; + if (!preg_match('/^[0-9]+$/', (string) $options['limit'])) { + king_ft_fail('--limit must be a non-negative integer', 64); + } + $options['limit'] = (int) $options['limit']; + + return $options; +} + +function king_ft_absolute_path(string $root, string $path): string +{ + if ($path === '') { + king_ft_fail('path must not be empty', 64); + } + if (str_starts_with($path, '/')) { + return $path; + } + return $root . '/' . $path; +} + +function king_ft_json_encode(array $value): string +{ + return json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE | JSON_THROW_ON_ERROR); +} + +function king_ft_split_sources(array $sources): array +{ + $docSources = []; + $includeLocalExamples = false; + foreach ($sources as $source) { + $source = (string) $source; + if (in_array($source, ['local', 'local-examples', 'examples'], true)) { + $includeLocalExamples = true; + continue; + } + $docSources[] = $source === 'docs' ? 'docs' : $source; + } + return [$docSources, $includeLocalExamples]; +} + +function king_ft_docs_files(string $root, array $sources): array +{ + $files = []; + foreach ($sources as $source) { + $path = king_ft_absolute_path($root, $source); + if (is_dir($path)) { + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS) + ); + foreach ($iterator as $item) { + if (!$item instanceof SplFileInfo || !$item->isFile()) { + continue; + } + if (strtolower($item->getExtension()) !== 'md') { + continue; + } + $files[] = $item->getPathname(); + } + } elseif (is_file($path) && strtolower(pathinfo($path, PATHINFO_EXTENSION)) === 'md') { + $files[] = $path; + } else { + king_ft_fail("source {$source} is not a markdown file or directory", 64); + } + } + + $files = array_values(array_unique($files)); + sort($files, SORT_STRING); + return $files; +} + +function king_ft_extract_code_blocks(string $root, string $file): array +{ + $relative = str_starts_with($file, $root . '/') ? substr($file, strlen($root) + 1) : $file; + $lines = file($file, FILE_IGNORE_NEW_LINES); + if ($lines === false) { + king_ft_fail("cannot read {$relative}"); + } + + $allowed = [ + 'bash' => true, + 'c' => true, + 'ini' => true, + 'json' => true, + 'php' => true, + 'sh' => true, + 'sql' => true, + 'xml' => true, + 'yaml' => true, + 'yml' => true, + ]; + $blocks = []; + $heading = basename($relative); + $paragraphs = []; + $inFence = false; + $language = ''; + $code = []; + $startLine = 0; + + foreach ($lines as $index => $line) { + if (preg_match('/^(#{1,6})\s+(.+)$/', $line, $matches) === 1 && !$inFence) { + $heading = trim($matches[2]); + $paragraphs = []; + continue; + } + + if (preg_match('/^```([A-Za-z0-9_+-]*)\s*$/', $line, $matches) === 1) { + if (!$inFence) { + $inFence = true; + $language = strtolower((string) $matches[1]); + $code = []; + $startLine = $index + 1; + } else { + $inFence = false; + $body = rtrim(implode("\n", $code)); + $normalizedLanguage = $language === '' ? 'text' : $language; + if (isset($allowed[$normalizedLanguage]) && strlen($body) >= 80 && strlen($body) <= 8000) { + $context = trim(implode("\n\n", array_slice($paragraphs, -2))); + $blocks[] = [ + 'source_file' => $relative, + 'start_line' => $startLine, + 'heading' => $heading, + 'language' => $normalizedLanguage, + 'context' => $context, + 'code' => $body, + ]; + } + $language = ''; + $code = []; + } + continue; + } + + if ($inFence) { + $code[] = $line; + continue; + } + + $trimmed = trim($line); + if ($trimmed !== '' && !str_starts_with($trimmed, '<') && !str_starts_with($trimmed, '- [')) { + $paragraphs[] = $trimmed; + if (count($paragraphs) > 8) { + array_shift($paragraphs); + } + } + } + + return $blocks; +} + +function king_ft_training_example(array $block): array +{ + $language = (string) $block['language']; + $context = (string) $block['context']; + $user = "Generate a production-grade {$language} example for the King primitive \"{$block['heading']}\"."; + if ($context !== '') { + $user .= "\n\nSource context:\n{$context}"; + } + $user .= "\n\nReturn the implementation only, with no marketing text."; + + return [ + 'messages' => [ + [ + 'role' => 'system', + 'content' => 'You are King Coder. Produce precise, production-grade code for King PHP runtime work. Prefer direct PHP and king_* APIs. Do not invent missing APIs.', + ], + [ + 'role' => 'user', + 'content' => $user, + ], + [ + 'role' => 'assistant', + 'content' => "```{$language}\n{$block['code']}\n```", + ], + ], + 'metadata' => [ + 'source_file' => $block['source_file'], + 'start_line' => $block['start_line'], + 'heading' => $block['heading'], + 'language' => $language, + 'example_type' => 'positive', + 'topic' => 'docs_code_block', + 'source_sha256' => hash('sha256', (string) $block['code']), + ], + ]; +} + +function king_ft_count_metadata(array $examples, string $key): array +{ + $counts = []; + foreach ($examples as $example) { + $value = (string) ($example['metadata'][$key] ?? 'unknown'); + $counts[$value] = ($counts[$value] ?? 0) + 1; + } + ksort($counts); + return $counts; +} + +function king_ft_example_text(array $example): string +{ + $parts = []; + foreach ($example['messages'] as $message) { + $parts[] = strtoupper((string) $message['role']) . ":\n" . (string) $message['content']; + } + return implode("\n\n", $parts); +} + +function king_ft_token_count(object $model, array $example): int +{ + $encoded = king_inference_tokenize($model, king_ft_example_text($example)); + $tokens = $encoded['tokens'] ?? null; + if (!is_array($tokens)) { + king_ft_fail('king_inference_tokenize returned no token list'); + } + return count($tokens); +} + +function king_ft_write_jsonl(string $path, array $examples): void +{ + $handle = fopen($path, 'wb'); + if ($handle === false) { + king_ft_fail("cannot write {$path}"); + } + foreach ($examples as $example) { + fwrite($handle, king_ft_json_encode($example) . "\n"); + } + fclose($handle); +} + +function king_ft_run(array $argv): void +{ + $root = dirname(__DIR__, 3); + $options = king_ft_parse_cli($argv); + $modelPath = king_ft_absolute_path($root, (string) $options['model']); + $outDir = king_ft_absolute_path($root, (string) $options['out']); + + if (!is_readable($modelPath)) { + king_ft_fail("model artifact is not readable: {$modelPath}"); + } + + if (!function_exists('king_inference_model_load')) { + king_ft_fail('King extension is not loaded'); + } + + $model = king_inference_model_load([ + 'name' => 'king-coder-fine-tune-tokenizer', + 'artifact' => ['path' => $modelPath], + 'backend' => 'king_native_cpu', + 'with_memory' => false, + ]); + + [$docSources, $includeLocalExamples] = king_ft_split_sources($options['sources']); + $candidateExamples = []; + if ($includeLocalExamples) { + array_push($candidateExamples, ...king_ft_local_training_examples()); + } + if ($docSources !== []) { + foreach (king_ft_docs_files($root, $docSources) as $file) { + foreach (king_ft_extract_code_blocks($root, $file) as $block) { + $candidateExamples[] = king_ft_training_example($block); + } + } + } + + $examples = []; + $skippedTooLarge = 0; + foreach ($candidateExamples as $example) { + $tokenCount = king_ft_token_count($model, $example); + if ($tokenCount > $options['max_tokens']) { + $skippedTooLarge++; + continue; + } + $example['metadata']['token_count'] = $tokenCount; + $examples[] = $example; + if ($options['limit'] > 0 && count($examples) >= $options['limit']) { + break; + } + } + + if ($examples === []) { + king_ft_fail('no tokenizer-valid examples were produced'); + } + + $train = []; + $validation = []; + foreach ($examples as $example) { + $hash = hexdec(substr((string) $example['metadata']['source_sha256'], 0, 8)); + if ($hash % 10 === 0) { + $validation[] = $example; + } else { + $train[] = $example; + } + } + if ($validation === [] && count($train) > 1) { + $validation[] = array_pop($train); + } + + if (!is_dir($outDir) && !mkdir($outDir, 0775, true)) { + king_ft_fail("cannot create output directory {$outDir}"); + } + + king_ft_write_jsonl($outDir . '/train.jsonl', $train); + king_ft_write_jsonl($outDir . '/validation.jsonl', $validation); + + $manifest = [ + 'kind' => 'king.coder_fine_tune.dataset.v1', + 'created_at' => gmdate(DATE_ATOM), + 'model_tokenizer_artifact' => $modelPath, + 'trainable_base_checkpoint' => (string) $options['trainable_base'], + 'output_dir' => $outDir, + 'sources' => array_values($options['sources']), + 'source_breakdown' => [ + 'docs' => $docSources, + 'local_examples' => $includeLocalExamples, + ], + 'max_tokens' => $options['max_tokens'], + 'examples_total' => count($examples), + 'train_examples' => count($train), + 'validation_examples' => count($validation), + 'example_types' => king_ft_count_metadata($examples, 'example_type'), + 'topics' => king_ft_count_metadata($examples, 'topic'), + 'skipped_too_large' => $skippedTooLarge, + 'status' => (string) $options['trainable_base'] === '' + ? 'dataset_prepared_trainable_base_missing' + : 'dataset_prepared_ready_for_lora_training', + 'notes' => [ + 'GGUF is used here only as the King tokenizer and runtime reference artifact.', + 'Actual adapter training needs a trainable base checkpoint; this script does not fake that step.', + 'The produced dataset is source-grounded from repository documentation examples.', + ], + ]; + file_put_contents($outDir . '/manifest.json', king_ft_json_encode($manifest) . "\n"); + + $runBook = "# King Coder Fine-Tune Run\n\n" + . "Dataset status: `{$manifest['status']}`\n\n" + . "Files:\n\n" + . "- `train.jsonl`: supervised chat examples for adapter training\n" + . "- `validation.jsonl`: held-out examples for adapter evaluation\n" + . "- `manifest.json`: reproducibility metadata\n\n" + . "Important: the configured GGUF file is not the trainable checkpoint. It is used for King tokenizer validation and later runtime export compatibility.\n"; + file_put_contents($outDir . '/run.md', $runBook); + + fwrite(STDOUT, king_ft_json_encode([ + 'status' => 'ok', + 'out' => $outDir, + 'train_examples' => count($train), + 'validation_examples' => count($validation), + 'skipped_too_large' => $skippedTooLarge, + ]) . "\n"); +} + +king_ft_run($argv); diff --git a/infra/inference/fine-tune/coder-local-examples.php b/infra/inference/fine-tune/coder-local-examples.php new file mode 100644 index 000000000..95474408a --- /dev/null +++ b/infra/inference/fine-tune/coder-local-examples.php @@ -0,0 +1,186 @@ + $messages, 'metadata' => $metadata]; +} + +function king_ft_local_system(): string +{ + return 'You are King Coder. Produce precise implementation artifacts for King PHP runtime work. Prefer direct PHP and king_* APIs. Call out unsupported APIs instead of inventing them.'; +} + +function king_ft_local_training_examples(): array +{ + $system = king_ft_local_system(); + + return [ + king_ft_local_example( + [ + ['role' => 'system', 'content' => $system], + ['role' => 'user', 'content' => 'Create a PHP King HTTP endpoint that exposes /health and returns JSON without using Node or a framework.'], + ['role' => 'assistant', 'content' => <<<'PHP' +```php + 404, + 'headers' => ['content-type' => 'application/json'], + 'body' => json_encode(['error' => 'not_found'], JSON_THROW_ON_ERROR), + ]; + } + + return [ + 'status' => 200, + 'headers' => ['content-type' => 'application/json'], + 'body' => json_encode(['status' => 'ok'], JSON_THROW_ON_ERROR), + ]; +}; + +while (true) { + if (king_http1_server_listen_once('127.0.0.1', 18080, null, $handler) === false) { + usleep(50_000); + } +} +``` +PHP], + ], + ['example_type' => 'positive', 'topic' => 'king_http_php', 'language' => 'php'] + ), + king_ft_local_example( + [ + ['role' => 'system', 'content' => $system], + ['role' => 'user', 'content' => 'Fix this bad King answer: "Use express.listen() because King has no HTTP primitive."'], + ['role' => 'assistant', 'content' => 'That answer is wrong. King HTTP endpoints should use King runtime APIs such as `king_http1_server_listen_once()` in PHP. Do not introduce Express or Node for a King runtime path.'], + ], + ['example_type' => 'negative', 'topic' => 'no_node_fallback', 'language' => 'text'] + ), + king_ft_local_example( + [ + ['role' => 'system', 'content' => $system], + ['role' => 'user', 'content' => 'Convert the request "How many letters a are in banana?" into a safe deterministic Mini-Op.'], + ['role' => 'assistant', 'content' => <<<'JSON' +```json +{ + "operation": "count_occurrences", + "input": { + "subject": "banana", + "needle": "a", + "case_sensitive": true + }, + "result": 3, + "side_effects": false, + "allowed_capabilities": ["string.count"] +} +``` +JSON], + ], + ['example_type' => 'tool_mini_op', 'topic' => 'deterministic_count', 'language' => 'json'] + ), + king_ft_local_example( + [ + ['role' => 'system', 'content' => $system], + ['role' => 'user', 'content' => 'Show how a configured MCP tool candidate should be represented before execution.'], + ['role' => 'assistant', 'content' => <<<'JSON' +```json +{ + "tool_candidate": { + "name": "invoice.lookup_status", + "configured": true, + "selected_by_model": false, + "arguments_schema": { + "invoice_id": "string" + } + }, + "execution": { + "status": "not_executed", + "reason": "tool execution is outside this inference step" + } +} +``` +JSON], + ], + ['example_type' => 'tool_mini_op', 'topic' => 'mcp_tool_candidate', 'language' => 'json'] + ), + king_ft_local_example( + [ + ['role' => 'system', 'content' => $system], + ['role' => 'user', 'content' => 'Create a PHP function that writes JSONL records safely for King fine-tuning data.'], + ['role' => 'assistant', 'content' => <<<'PHP' +```php + 'positive', 'topic' => 'jsonl_writer', 'language' => 'php'] + ), + king_ft_local_example( + [ + ['role' => 'system', 'content' => $system], + ['role' => 'user', 'content' => 'Reject this unsafe plan: "Run arbitrary shell commands from a generated counting tool."'], + ['role' => 'assistant', 'content' => 'Do not execute shell commands for deterministic counting. Use a bounded Mini-Op such as `count_occurrences` that accepts strings, has no file or CLI capability, and returns a typed result.'], + ], + ['example_type' => 'negative', 'topic' => 'no_shell_for_mini_ops', 'language' => 'text'] + ), + king_ft_local_example( + [ + ['role' => 'system', 'content' => $system], + ['role' => 'user', 'content' => 'Provide a compact PHP King model-load call for tokenizer validation.'], + ['role' => 'assistant', 'content' => <<<'PHP' +```php + 'king-coder-tokenizer', + 'artifact' => ['path' => $modelPath], + 'backend' => 'king_native_cpu', + 'with_memory' => false, +]); + +$encoded = king_inference_tokenize($model, $text); +if (!is_array($encoded['tokens'] ?? null)) { + throw new RuntimeException('Tokenization returned no token list'); +} +``` +PHP], + ], + ['example_type' => 'king_api_php', 'topic' => 'tokenizer_validation', 'language' => 'php'] + ), + king_ft_local_example( + [ + ['role' => 'system', 'content' => $system], + ['role' => 'user', 'content' => 'Explain what to do if an API is not exposed by King yet.'], + ['role' => 'assistant', 'content' => 'State that the API is not exposed yet, keep the strongest intended contract visible, and avoid inventing a fake function name or silently replacing the King runtime path with unrelated fallback code.'], + ], + ['example_type' => 'negative', 'topic' => 'no_invented_api', 'language' => 'text'] + ), + ]; +} diff --git a/infra/inference/fine-tune/result-recorder.php b/infra/inference/fine-tune/result-recorder.php new file mode 100644 index 000000000..73df2553e --- /dev/null +++ b/infra/inference/fine-tune/result-recorder.php @@ -0,0 +1,124 @@ + $enabled, + 'path' => $path, + 'max_field_chars' => $maxChars, + 'run_id' => getenv('KING_INFERENCE_RESULT_RECORD_RUN_ID') ?: gmdate('Ymd\THis\Z'), + ]; +} + +function king_ir_redact_text(string $value, int $maxChars): string +{ + $redacted = preg_replace('/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu', '[redacted-email]', $value) ?? $value; + $redacted = preg_replace('/https?:\/\/[^\s<>"\']+/iu', '[redacted-url]', $redacted) ?? $redacted; + $redacted = preg_replace('/-----BEGIN [^-]+ PRIVATE KEY-----.*?-----END [^-]+ PRIVATE KEY-----/is', '[redacted-private-key]', $redacted) ?? $redacted; + $redacted = preg_replace('/\b(?:bearer|api[_ -]?key|token|secret|password)\s*[:=]\s*[^\s,"\']+/iu', '[redacted-secret]', $redacted) ?? $redacted; + $redacted = preg_replace('/\b(?:sk-[A-Za-z0-9_-]{20,}|[A-Za-z0-9_-]{32,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,})\b/u', '[redacted-token]', $redacted) ?? $redacted; + + if (strlen($redacted) <= $maxChars) { + return $redacted; + } + return substr($redacted, 0, $maxChars) . "\n[truncated sha256=" . hash('sha256', $redacted) . ']'; +} + +function king_ir_redact_value(mixed $value, int $maxChars): mixed +{ + if (is_string($value)) { + return king_ir_redact_text($value, $maxChars); + } + if (!is_array($value)) { + return $value; + } + + $result = []; + foreach ($value as $key => $item) { + $normalizedKey = is_string($key) ? strtolower($key) : $key; + if (is_string($normalizedKey) && preg_match('/password|secret|token|api[_-]?key|authorization/i', $normalizedKey) === 1) { + $result[$key] = '[redacted-secret]'; + continue; + } + $result[$key] = king_ir_redact_value($item, $maxChars); + } + return $result; +} + +function king_ir_write_jsonl(string $path, array $record): void +{ + $dir = dirname($path); + if (!is_dir($dir) && !mkdir($dir, 0775, true)) { + throw new RuntimeException('Cannot create inference result record directory: ' . $dir); + } + $line = json_encode($record, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE) . "\n"; + if (file_put_contents($path, $line, FILE_APPEND | LOCK_EX) === false) { + throw new RuntimeException('Cannot append inference result record: ' . $path); + } +} + +function king_ir_record_case(array $config, array $case, array $result, string $model, array $artifact, array $modelSummary): ?array +{ + if (empty($config['enabled'])) { + return null; + } + + $maxChars = (int) $config['max_field_chars']; + $record = [ + 'kind' => 'king.inference.result_record.v1', + 'created_at' => gmdate(DATE_ATOM), + 'run_id' => (string) $config['run_id'], + 'case' => [ + 'name' => $case['name'] ?? null, + 'category' => $case['category'] ?? $case['coverage'] ?? null, + 'scope' => $case['scope'] ?? null, + 'mode' => $case['mode'] ?? 'deterministic', + ], + 'model' => [ + 'id' => $model, + 'backend' => $modelSummary['backend'] ?? null, + 'artifact_basename' => isset($artifact['path']) ? basename((string) $artifact['path']) : null, + 'artifact_bytes' => $artifact['bytes'] ?? null, + 'artifact_sha256' => $artifact['sha256'] ?? null, + ], + 'request' => [ + 'messages' => king_ir_redact_value($case['messages'] ?? [], $maxChars), + 'max_tokens' => $case['max_tokens'] ?? null, + 'sampler' => king_ir_redact_value($case['sampler'] ?? [], $maxChars), + 'stop' => king_ir_redact_value($case['stop'] ?? null, $maxChars), + ], + 'expected' => king_ir_redact_value($case['expected'] ?? null, $maxChars), + 'actual' => [ + 'content' => king_ir_redact_value($result['content'] ?? '', $maxChars), + 'sha256' => hash('sha256', (string) ($result['content'] ?? '')), + ], + 'evaluation' => [ + 'ok' => (bool) ($result['ok'] ?? false), + 'reason' => $result['reason'] ?? null, + 'details' => king_ir_redact_value($result['evaluation'] ?? [], $maxChars), + ], + 'transport' => [ + 'http_status' => $result['http_status'] ?? null, + 'duration_ms' => $result['duration_ms'] ?? null, + ], + ]; + + king_ir_write_jsonl((string) $config['path'], $record); + return ['path' => (string) $config['path'], 'kind' => $record['kind']]; +} diff --git a/infra/inference/golden-prompt-pack.php b/infra/inference/golden-prompt-pack.php new file mode 100644 index 000000000..c1601da2b --- /dev/null +++ b/infra/inference/golden-prompt-pack.php @@ -0,0 +1,211 @@ + 'pack_' . $name, + 'category' => $category, + 'coverage' => $category, + 'scope' => $scope, + 'mode' => 'deterministic', + 'sampler' => ['temperature' => 0.0, 'top_p' => 1.0, 'seed' => $seed++], + 'max_tokens' => $maxTokens, + 'messages' => [ + ['role' => 'system', 'content' => $system], + ['role' => 'user', 'content' => $prompt], + ], + 'expected' => $expected, + ]; + if ($stop !== null) { + $case['stop'] = $stop; + } + $cases[] = $case; + }; + $addQualitative = static function ( + string $category, + string $name, + string $prompt, + int $maxTokens = 160 + ) use (&$cases, &$seed, $system): void { + $cases[] = [ + 'name' => 'pack_' . $name, + 'category' => $category, + 'coverage' => $category, + 'scope' => 'model', + 'mode' => 'qualitative', + 'sampler' => ['temperature' => 0.2, 'top_p' => 0.9, 'seed' => $seed++], + 'max_tokens' => $maxTokens, + 'messages' => [ + ['role' => 'system', 'content' => $system], + ['role' => 'user', 'content' => $prompt], + ], + ]; + }; + + for ($i = 1; $i <= 12; $i++) { + $marker = sprintf('KING_EXACT_%02d', $i); + $addCase( + 'exact output', + strtolower($marker), + 'Return exactly this marker and nothing else: ' . $marker, + ['type' => 'exact', 'tokens' => [$marker]], + 12, + 'fast' + ); + } + + foreach ([ + ['banana', 'a', '3'], ['letter', 'e', '2'], ['mississippi', 's', '4'], + ['committee', 'm', '2'], ['bookkeeper', 'e', '3'], ['parallel', 'l', '2'], + ['sassafras', 's', '4'], ['alloy', 'l', '2'], ['queue', 'u', '2'], + ['abracadabra', 'a', '5'], ['programming', 'm', '2'], ['cranberry', 'r', '2'], + ['encyclopedia', 'e', '2'], ['Tennessee', 'e', '4'], ['assessment', 's', '4'], + ['development', 'e', '3'], ['controller', 'l', '2'], ['minimum', 'm', '3'], + ['referer', 'r', '3'], ['configuration', 'i', '2'], + ] as $index => [$word, $needle, $count]) { + $addCase( + 'counting', + sprintf('count_%02d_%s_%s', $index + 1, strtolower($word), $needle), + sprintf('How many letters %s are in the word %s? Answer with only the number.', $needle, $word), + ['type' => 'exact', 'tokens' => [$count]], + 8, + 'fast' + ); + } + + foreach ([ + ['7 + 5', '12'], ['13 + 29', '42'], ['100 - 37', '63'], ['9 * 8', '72'], + ['144 / 12', '12'], ['6 * 7 + 1', '43'], ['90 - 45', '45'], ['18 + 24', '42'], + ['11 * 11', '121'], ['81 / 9', '9'], ['14 + 15', '29'], ['64 - 19', '45'], + ['12 * 12', '144'], ['125 / 5', '25'], ['33 + 44', '77'], ['70 - 28', '42'], + ['8 * 13', '104'], ['96 / 8', '12'], ['17 + 19', '36'], ['55 - 13', '42'], + ['15 * 6', '90'], ['72 / 6', '12'], ['101 - 59', '42'], ['23 + 19', '42'], + ] as $index => [$expression, $answer]) { + $addCase( + 'arithmetic', + 'arithmetic_' . sprintf('%02d', $index + 1), + 'Calculate ' . $expression . '. Answer only with the number.', + ['type' => 'exact', 'tokens' => [$answer]], + 10, + 'fast' + ); + } + + foreach ([ + ['status', 'ok'], ['route', 'openai'], ['backend', 'king'], ['format', 'json'], + ['runtime', 'native'], ['stream', 'ready'], ['cache', 'off'], ['profile', 'cpu'], + ['profile', 'gpu'], ['mode', 'strict'], ['scope', 'fast'], ['scope', 'model'], + ] as $index => [$key, $value]) { + $addCase( + 'JSON contract', + 'json_' . sprintf('%02d', $index + 1), + sprintf('Return only this JSON object: {"%s":"%s"}', $key, $value), + ['type' => 'json_object', 'object' => [$key => $value]], + 32, + 'model' + ); + } + + foreach ([ + 'King Notes', 'Runtime Contract', 'Inference Smoke', 'GPU Profile', + 'Prompt Pack', 'Router Check', 'Tokenizer Step', 'Sampler Result', + ] as $index => $heading) { + $addCase( + 'Markdown source contract', + 'markdown_' . sprintf('%02d', $index + 1), + 'Return Markdown source with one H1 heading named ' . $heading . '. Wrap it in a triple-tilde markdown fence.', + ['type' => 'contains_all', 'texts' => ['~~~markdown', '# ' . $heading, '~~~']], + 64, + 'model' + ); + } + + foreach ([ + ['king_alpha', 'alpha'], ['king_beta', 'beta'], ['king_gamma', 'gamma'], + ['king_delta', 'delta'], ['king_epsilon', 'epsilon'], ['king_zeta', 'zeta'], + ['king_eta', 'eta'], ['king_theta', 'theta'], ['king_iota', 'iota'], ['king_kappa', 'kappa'], + ] as $index => [$function, $value]) { + $addCase( + 'PHP code contract', + 'php_' . sprintf('%02d', $index + 1), + sprintf('Return only a PHP code fence containing a function named %s that returns the string %s.', $function, $value), + ['type' => 'contains_all', 'texts' => ['```php', 'function ' . $function, 'return', $value]], + 96, + 'model' + ); + } + + foreach ([ + ['Gib exakt JA aus.', 'JA'], ['Gib exakt NEIN aus.', 'NEIN'], ['Gib exakt OK aus.', 'OK'], + ['Gib exakt 42 aus.', '42'], ['Gib exakt King aus.', 'King'], ['Gib exakt Token aus.', 'Token'], + ['Gib exakt Modell aus.', 'Modell'], ['Gib exakt GPU aus.', 'GPU'], ['Gib exakt CPU aus.', 'CPU'], + ['Gib exakt Router aus.', 'Router'], + ] as $index => [$prompt, $answer]) { + $addCase( + 'German exact instruction', + 'german_exact_' . sprintf('%02d', $index + 1), + $prompt, + ['type' => 'exact', 'tokens' => [$answer]], + 12, + 'fast' + ); + } + + for ($i = 1; $i <= 8; $i++) { + $stop = sprintf('STOP_PACK_%02d', $i); + $addCase( + 'stop token behavior', + 'stop_' . sprintf('%02d', $i), + sprintf('Respond with exactly: before %s after', $stop), + ['type' => 'not_contains', 'text' => $stop], + 32, + 'fast', + [$stop] + ); + } + + foreach ([ + ['classification', 'Classify whether this text asks for code, explanation, or data: Write a PHP function for parsing JSON.'], + ['classification', 'Classify whether this text asks for code, explanation, or data: Explain what a token is.'], + ['classification', 'Classify whether this text asks for code, explanation, or data: Return a JSON object with status ok.'], + ['summarization', 'Summarize in one sentence why deterministic prompt probes are useful.'], + ['summarization', 'Summarize in one sentence why silent CPU fallback is dangerous.'], + ['reasoning sample', 'Explain briefly how a stop sequence should affect streamed output.'], + ['reasoning sample', 'Explain briefly why exact JSON output must not include prose.'], + ['reasoning sample', 'Explain briefly what a tokenizer does before inference.'], + ] as $index => [$category, $prompt]) { + $addQualitative($category, 'qualitative_' . sprintf('%02d', $index + 1), $prompt); + } + + return [ + 'id' => 'king-core-prompt-pack-v1', + 'version' => 1, + 'categories' => [ + 'exact output' => 'Strict string contracts for format drift.', + 'counting' => 'Small deterministic character counting checks.', + 'arithmetic' => 'Small deterministic arithmetic checks.', + 'JSON contract' => 'Machine-readable JSON-only responses.', + 'Markdown source contract' => 'Markdown source fences and headings.', + 'PHP code contract' => 'Simple PHP code-fence generation contracts.', + 'German exact instruction' => 'German instruction following with exact outputs.', + 'stop token behavior' => 'Stop-sequence boundary checks.', + 'classification' => 'Qualitative intent classification samples.', + 'summarization' => 'Qualitative short-answer samples.', + 'reasoning sample' => 'Qualitative explanation samples.', + ], + 'cases' => $cases, + ]; +} diff --git a/infra/inference/golden-prompts.php b/infra/inference/golden-prompts.php new file mode 100644 index 000000000..3aec1ac28 --- /dev/null +++ b/infra/inference/golden-prompts.php @@ -0,0 +1,784 @@ + getenv('KING_INFERENCE_GOLDEN_BASE_URL') ?: 'http://127.0.0.1:8080/v1', + 'model' => getenv('KING_INFERENCE_GOLDEN_MODEL') ?: '', + 'artifact' => getenv('KING_INFERENCE_GOLDEN_MODEL_PATH') ?: '', + 'case' => getenv('KING_INFERENCE_GOLDEN_CASE') ?: '', + 'scope' => getenv('KING_INFERENCE_GOLDEN_SCOPE') ?: 'all', + 'pack' => getenv('KING_INFERENCE_GOLDEN_PACK') ?: 'none', + 'pack_summary' => king_golden_env_bool('KING_INFERENCE_GOLDEN_PACK_SUMMARY', false), + 'strict' => king_golden_env_bool('KING_INFERENCE_GOLDEN_STRICT', false), + 'json' => king_golden_env_bool('KING_INFERENCE_GOLDEN_JSON', false), + 'timeout' => max(1, (int) (getenv('KING_INFERENCE_GOLDEN_TIMEOUT_SEC') ?: 45)), + ]; + + foreach (array_slice($argv, 1) as $arg) { + if ($arg === '--strict' || $arg === '--fail-on-mismatch') { + $options['strict'] = true; + continue; + } + if ($arg === '--json') { + $options['json'] = true; + continue; + } + if ($arg === '--pack-summary') { + $options['pack_summary'] = true; + continue; + } + if ($arg === '-h' || $arg === '--help') { + king_golden_usage(); + exit(0); + } + foreach (['url', 'model', 'artifact', 'case', 'scope', 'pack', 'timeout'] as $key) { + $prefix = '--' . $key . '='; + if (str_starts_with($arg, $prefix)) { + $options[$key] = substr($arg, strlen($prefix)); + continue 2; + } + } + fwrite(STDERR, "golden-prompts: unknown argument: {$arg}\n"); + king_golden_usage(); + exit(2); + } + + $options['url'] = rtrim((string) $options['url'], '/'); + $options['timeout'] = max(1, (int) $options['timeout']); + $options['scope'] = in_array((string) $options['scope'], ['all', 'fast', 'model'], true) + ? (string) $options['scope'] + : 'all'; + if ($options['pack_summary'] && in_array((string) $options['pack'], ['', 'none'], true)) { + $options['pack'] = 'core'; + } + return $options; +} + +function king_golden_cases(): array +{ + $system = 'You are a deterministic King inference contract runner. Follow the requested output format exactly.'; + + return [ + [ + 'name' => 'simple_exact_output', + 'coverage' => 'simple language', + 'scope' => 'fast', + 'sampler' => ['temperature' => 0.0, 'top_p' => 1.0, 'seed' => 1001], + 'max_tokens' => 8, + 'messages' => [ + ['role' => 'system', 'content' => $system], + ['role' => 'user', 'content' => 'Return exactly this two-letter answer and nothing else: OK'], + ], + 'expected' => [ + 'type' => 'exact', + 'tokens' => ['OK'], + ], + ], + [ + 'name' => 'count_banana', + 'coverage' => 'counting', + 'scope' => 'fast', + 'sampler' => ['temperature' => 0.0, 'top_p' => 1.0, 'seed' => 1002], + 'max_tokens' => 8, + 'messages' => [ + ['role' => 'system', 'content' => $system], + ['role' => 'user', 'content' => 'How many letters a are in the word banana? Answer with only the number.'], + ], + 'expected' => [ + 'type' => 'exact', + 'tokens' => ['3'], + ], + ], + [ + 'name' => 'count_hallo_welt', + 'coverage' => 'counting', + 'scope' => 'fast', + 'sampler' => ['temperature' => 0.0, 'top_p' => 1.0, 'seed' => 1003], + 'max_tokens' => 8, + 'messages' => [ + ['role' => 'system', 'content' => $system], + ['role' => 'user', 'content' => 'Wie viele Buchstaben l hat Hallo Welt? Antworte nur mit der Zahl.'], + ], + 'expected' => [ + 'type' => 'exact', + 'tokens' => ['3'], + ], + ], + [ + 'name' => 'json_status_contract', + 'coverage' => 'JSON-only response', + 'scope' => 'model', + 'sampler' => ['temperature' => 0.0, 'top_p' => 1.0, 'seed' => 1004], + 'max_tokens' => 48, + 'messages' => [ + ['role' => 'system', 'content' => $system], + ['role' => 'user', 'content' => 'Return only a JSON object with exactly one key named status and the value ok.'], + ], + 'expected' => [ + 'type' => 'json_object', + 'object' => ['status' => 'ok'], + ], + ], + [ + 'name' => 'german_token_explanation', + 'coverage' => 'German instruction following', + 'scope' => 'model', + 'sampler' => ['temperature' => 0.0, 'top_p' => 1.0, 'seed' => 1005], + 'max_tokens' => 48, + 'messages' => [ + ['role' => 'system', 'content' => $system], + ['role' => 'user', 'content' => 'Antworte auf Deutsch in einem kurzen Satz: Was ist ein Token?'], + ], + 'expected' => [ + 'type' => 'all', + 'expectations' => [ + [ + 'type' => 'contains_any', + 'texts' => ['Token', 'Zeichen', 'Worteinheit', 'Texteinheit'], + ], + [ + 'type' => 'max_chars', + 'max' => 220, + ], + ], + ], + ], + [ + 'name' => 'stop_boundary', + 'coverage' => 'stop token behavior', + 'scope' => 'fast', + 'sampler' => ['temperature' => 0.0, 'top_p' => 1.0, 'seed' => 1006], + 'max_tokens' => 32, + 'stop' => ['STOP_HERE'], + 'messages' => [ + ['role' => 'system', 'content' => $system], + ['role' => 'user', 'content' => 'Respond with exactly: alpha STOP_HERE beta'], + ], + 'expected' => [ + 'type' => 'not_contains', + 'text' => 'STOP_HERE', + ], + ], + [ + 'name' => 'php_generation', + 'coverage' => 'simple PHP generation', + 'scope' => 'model', + 'sampler' => ['temperature' => 0.0, 'top_p' => 1.0, 'seed' => 1007], + 'max_tokens' => 96, + 'messages' => [ + ['role' => 'system', 'content' => $system], + ['role' => 'user', 'content' => 'Return only a PHP code fence containing a function named king_example that returns the string ok.'], + ], + 'expected' => [ + 'type' => 'contains_all', + 'texts' => ['```php', 'function king_example', 'return', 'ok'], + ], + ], + [ + 'name' => 'concise_logits_explanation', + 'coverage' => 'short explanation', + 'scope' => 'model', + 'sampler' => ['temperature' => 0.0, 'top_p' => 1.0, 'seed' => 1008], + 'max_tokens' => 64, + 'messages' => [ + ['role' => 'system', 'content' => $system], + ['role' => 'user', 'content' => 'Explain logits in one concise sentence.'], + ], + 'expected' => [ + 'type' => 'all', + 'expectations' => [ + ['type' => 'regex', 'pattern' => '/\\blogits?\\b/i', 'description' => 'mentions logit/logits'], + ['type' => 'max_chars', 'max' => 240], + ], + ], + ], + [ + 'name' => 'markdown_source_contract', + 'coverage' => 'Markdown source contract', + 'scope' => 'model', + 'sampler' => ['temperature' => 0.0, 'top_p' => 1.0, 'seed' => 1009], + 'max_tokens' => 96, + 'messages' => [ + ['role' => 'system', 'content' => $system], + ['role' => 'user', 'content' => 'Return Markdown source for a heading named King Notes. Wrap the Markdown source in a triple-tilde markdown fence.'], + ], + 'expected' => [ + 'type' => 'all', + 'expectations' => [ + ['type' => 'regex', 'pattern' => '/^~~~markdown\\R/i', 'description' => 'starts with markdown source fence'], + ['type' => 'contains_all', 'texts' => ['# King Notes', '~~~']], + ], + ], + ], + ]; +} + +function king_golden_pack_path(string $pack): string +{ + if ($pack === '' || $pack === 'none') { + return ''; + } + if ($pack === 'core') { + return __DIR__ . '/golden-prompt-pack.php'; + } + return $pack; +} + +function king_golden_load_prompt_pack(string $pack): array +{ + $path = king_golden_pack_path($pack); + if ($path === '') { + return ['id' => 'none', 'version' => 0, 'categories' => [], 'cases' => []]; + } + if (!is_file($path) || !is_readable($path)) { + throw new RuntimeException('Prompt pack is not readable: ' . $path); + } + + require_once $path; + if (!function_exists('king_inference_golden_prompt_pack')) { + throw new RuntimeException('Prompt pack must define king_inference_golden_prompt_pack().'); + } + + $loaded = king_inference_golden_prompt_pack(); + if (!is_array($loaded)) { + throw new RuntimeException('Prompt pack did not return an array.'); + } + $loaded['_path'] = $path; + return $loaded; +} + +function king_golden_prompt_pack_summary(array $pack): array +{ + $cases = is_array($pack['cases'] ?? null) ? $pack['cases'] : []; + $categories = is_array($pack['categories'] ?? null) ? $pack['categories'] : []; + $categoryCounts = []; + $modeCounts = []; + $invalid = []; + + foreach ($cases as $index => $case) { + if (!is_array($case)) { + $invalid[] = 'case_' . $index . ':not_array'; + continue; + } + $category = (string) ($case['category'] ?? $case['coverage'] ?? 'uncategorized'); + $mode = (string) ($case['mode'] ?? 'deterministic'); + $categoryCounts[$category] = ($categoryCounts[$category] ?? 0) + 1; + $modeCounts[$mode] = ($modeCounts[$mode] ?? 0) + 1; + + foreach (['name', 'messages', 'max_tokens', 'sampler'] as $required) { + if (!array_key_exists($required, $case)) { + $invalid[] = ($case['name'] ?? 'case_' . $index) . ':missing_' . $required; + } + } + if ($mode === 'deterministic' && !is_array($case['expected'] ?? null)) { + $invalid[] = ($case['name'] ?? 'case_' . $index) . ':missing_expected'; + } + if ($mode !== 'deterministic' && array_key_exists('expected', $case)) { + $invalid[] = ($case['name'] ?? 'case_' . $index) . ':qualitative_has_expected'; + } + } + + ksort($categoryCounts); + ksort($modeCounts); + $deterministic = (int) ($modeCounts['deterministic'] ?? 0); + $qualitative = array_sum($modeCounts) - $deterministic; + $isNoPack = ($pack['id'] ?? null) === 'none'; + + return [ + 'ok' => $isNoPack || ($deterministic >= 100 && $invalid === [] && count($categoryCounts) >= 8), + 'id' => $pack['id'] ?? null, + 'version' => $pack['version'] ?? null, + 'path' => $pack['_path'] ?? null, + 'category_definitions' => count($categories), + 'category_count' => count($categoryCounts), + 'categories' => $categoryCounts, + 'modes' => $modeCounts, + 'case_count' => count($cases), + 'deterministic_count' => $deterministic, + 'qualitative_count' => $qualitative, + 'invalid_count' => count($invalid), + 'invalid' => $invalid, + ]; +} + +function king_golden_prompt_pack_runtime_cases(array $pack): array +{ + $runtimeCases = []; + foreach (($pack['cases'] ?? []) as $case) { + if (!is_array($case) || ($case['mode'] ?? 'deterministic') !== 'deterministic') { + continue; + } + if (!is_array($case['expected'] ?? null)) { + continue; + } + $runtimeCases[] = $case; + } + return $runtimeCases; +} + +function king_golden_http_json(string $method, string $url, ?array $payload, int $timeout): array +{ + if (!function_exists('curl_init')) { + throw new RuntimeException('golden-prompts requires the PHP curl extension.'); + } + + $ch = curl_init($url); + $headers = ['accept: application/json']; + $body = null; + if ($payload !== null) { + $body = json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $headers[] = 'content-type: application/json'; + } + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_TIMEOUT => $timeout, + ]); + if ($body !== null) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + + $started = hrtime(true); + $raw = curl_exec($ch); + $durationMs = (hrtime(true) - $started) / 1_000_000.0; + $code = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); + if ($raw === false) { + $error = curl_error($ch); + curl_close($ch); + throw new RuntimeException($error); + } + curl_close($ch); + + $decoded = json_decode((string) $raw, true); + return [ + 'status' => $code, + 'duration_ms' => round($durationMs, 3), + 'raw' => (string) $raw, + 'json' => is_array($decoded) ? $decoded : null, + ]; +} + +function king_golden_validate_artifact(string $path): array +{ + if ($path === '') { + throw new RuntimeException('KING_INFERENCE_GOLDEN_MODEL_PATH or --artifact must point to the fixed local GGUF artifact for this contract run.'); + } + if (!is_file($path) || !is_readable($path)) { + throw new RuntimeException("Golden model artifact is not readable: {$path}"); + } + + $real = realpath($path); + return [ + 'path' => $path, + 'realpath' => $real !== false ? $real : $path, + 'bytes' => filesize($path), + 'mtime' => filemtime($path), + 'sha256' => getenv('KING_INFERENCE_GOLDEN_MODEL_SHA256') ?: null, + ]; +} + +function king_golden_model_id(array $options, array $models): string +{ + if ((string) $options['model'] !== '') { + return (string) $options['model']; + } + $first = $models['data'][0]['id'] ?? null; + if (!is_string($first) || $first === '') { + throw new RuntimeException('/v1/models did not expose a selectable model id.'); + } + return $first; +} + +function king_golden_model_summary(array $entry): array +{ + $king = is_array($entry['x_king'] ?? null) ? $entry['x_king'] : []; + $gpuRuntime = is_array($king['gpu_runtime'] ?? null) ? $king['gpu_runtime'] : []; + $clientCapabilities = is_array($king['client_capabilities'] ?? null) ? $king['client_capabilities'] : []; + $capabilities = is_array($king['capabilities'] ?? null) ? $king['capabilities'] : []; + $route = is_array($king['openai_route'] ?? null) ? $king['openai_route'] : []; + + return [ + 'id' => $entry['id'] ?? null, + 'owned_by' => $entry['owned_by'] ?? null, + 'backend' => $king['backend'] ?? null, + 'openai_generation' => $king['openai_generation'] ?? null, + 'gpu_enabled' => $king['gpu_enabled'] ?? null, + 'gpu_generation_ready' => $gpuRuntime['generation_ready'] ?? null, + 'gpu_admission_reason' => $gpuRuntime['reason'] ?? null, + 'client_generation_ready' => $clientCapabilities['generation_ready'] ?? null, + 'prompt_to_logits_generation' => $clientCapabilities['prompt_to_logits_generation'] ?? null, + 'synthetic_token_vector_graph' => $clientCapabilities['synthetic_token_vector_graph'] ?? null, + 'capability_prompt_to_logits_generation' => $capabilities['prompt_to_logits_generation'] ?? null, + 'capability_synthetic_token_vector_graph' => $capabilities['synthetic_token_vector_graph'] ?? null, + 'active_hot_path' => $route['active_hot_path'] ?? null, + 'batch_prefill_admitted' => $route['batch_prefill_admitted'] ?? null, + ]; +} + +function king_golden_expected_text(array $expected): ?string +{ + if (($expected['type'] ?? '') !== 'exact') { + return null; + } + $tokens = $expected['tokens'] ?? null; + if (!is_array($tokens)) { + return null; + } + return implode('', array_map(static fn($value): string => (string) $value, $tokens)); +} + +function king_golden_string_length(string $value): int +{ + return function_exists('mb_strlen') ? mb_strlen($value, 'UTF-8') : strlen($value); +} + +function king_golden_evaluate(string $content, array $expected): array +{ + $type = (string) ($expected['type'] ?? ''); + $trimmed = trim($content); + + if ($type === 'all') { + $checks = []; + $ok = true; + foreach (($expected['expectations'] ?? []) as $nested) { + if (!is_array($nested)) { + $checks[] = [ + 'ok' => false, + 'reason' => 'invalid_nested_expectation', + 'expected' => $nested, + 'actual' => $trimmed, + ]; + $ok = false; + continue; + } + $check = king_golden_evaluate($content, $nested); + $checks[] = $check; + $ok = $ok && !empty($check['ok']); + } + $failed = array_values(array_filter($checks, static fn(array $check): bool => empty($check['ok']))); + return [ + 'ok' => $ok, + 'expected' => $expected['expectations'] ?? [], + 'actual' => $trimmed, + 'reason' => $ok ? 'all_expectations_match' : 'nested_failure: ' . implode('; ', array_column($failed, 'reason')), + 'checks' => $checks, + ]; + } + + if ($type === 'exact') { + $expectedText = king_golden_expected_text($expected); + $ok = $trimmed === $expectedText; + return [ + 'ok' => $ok, + 'expected' => $expectedText, + 'actual' => $trimmed, + 'reason' => $ok ? 'exact_match' : 'exact_mismatch', + ]; + } + + if ($type === 'regex') { + $pattern = (string) ($expected['pattern'] ?? '//'); + $ok = preg_match($pattern, $trimmed) === 1; + return [ + 'ok' => $ok, + 'expected' => $expected['description'] ?? $pattern, + 'actual' => $trimmed, + 'reason' => $ok ? 'regex_match' : 'regex_mismatch', + ]; + } + + if ($type === 'json_object') { + try { + $decoded = json_decode($trimmed, true, 64, JSON_THROW_ON_ERROR); + $expectedObject = $expected['object'] ?? []; + $ok = is_array($decoded) && $decoded === $expectedObject; + return [ + 'ok' => $ok, + 'expected' => $expectedObject, + 'actual' => $decoded, + 'reason' => $ok ? 'json_object_match' : 'json_object_mismatch', + ]; + } catch (JsonException $exception) { + return [ + 'ok' => false, + 'expected' => $expected['object'] ?? [], + 'actual' => $trimmed, + 'reason' => 'json_parse_failed: ' . $exception->getMessage(), + ]; + } + } + + if ($type === 'not_contains') { + $needle = (string) ($expected['text'] ?? ''); + $ok = $needle !== '' && !str_contains($content, $needle); + return [ + 'ok' => $ok, + 'expected' => 'content does not contain ' . $needle, + 'actual' => $trimmed, + 'reason' => $ok ? 'needle_absent' : 'needle_present', + ]; + } + + if ($type === 'contains_any') { + $needles = is_array($expected['texts'] ?? null) ? $expected['texts'] : []; + foreach ($needles as $needle) { + if (is_string($needle) && $needle !== '' && str_contains($content, $needle)) { + return [ + 'ok' => true, + 'expected' => $needles, + 'actual' => $trimmed, + 'reason' => 'one_needle_present: ' . $needle, + ]; + } + } + return [ + 'ok' => false, + 'expected' => $needles, + 'actual' => $trimmed, + 'reason' => 'no_expected_needle_present', + ]; + } + + if ($type === 'contains_all') { + $missing = []; + foreach (($expected['texts'] ?? []) as $needle) { + if (!is_string($needle) || $needle === '' || !str_contains($content, $needle)) { + $missing[] = $needle; + } + } + return [ + 'ok' => $missing === [], + 'expected' => $expected['texts'] ?? [], + 'actual' => $trimmed, + 'reason' => $missing === [] ? 'all_needles_present' : 'missing: ' . implode(', ', $missing), + ]; + } + + if ($type === 'max_chars') { + $max = max(0, (int) ($expected['max'] ?? 0)); + $actual = king_golden_string_length($trimmed); + $ok = $max > 0 && $actual <= $max; + return [ + 'ok' => $ok, + 'expected' => 'at most ' . $max . ' chars', + 'actual' => $actual, + 'reason' => $ok ? 'within_char_limit' : 'char_limit_exceeded', + ]; + } + + return [ + 'ok' => false, + 'expected' => $expected, + 'actual' => $trimmed, + 'reason' => 'unknown_expectation_type', + ]; +} + +function king_golden_request_payload(string $model, array $case): array +{ + $sampler = is_array($case['sampler'] ?? null) ? $case['sampler'] : []; + $payload = [ + 'model' => $model, + 'stream' => false, + 'messages' => $case['messages'], + 'max_tokens' => (int) $case['max_tokens'], + 'temperature' => (float) ($sampler['temperature'] ?? 0.0), + 'top_p' => (float) ($sampler['top_p'] ?? 1.0), + 'seed' => (int) ($sampler['seed'] ?? 0), + ]; + if (isset($case['stop'])) { + $payload['stop'] = $case['stop']; + } + return $payload; +} + +function king_golden_filter_cases(array $cases, string $caseName, string $scope): array +{ + if ($scope !== 'all') { + $cases = array_values(array_filter( + $cases, + static fn(array $case): bool => (($case['scope'] ?? 'model') === $scope) + )); + } + + if ($caseName !== '') { + $cases = array_values(array_filter( + $cases, + static fn(array $case): bool => $case['name'] === $caseName + )); + } + + return $cases; +} + +function king_golden_run_case(string $url, string $model, array $case, int $timeout): array +{ + $payload = king_golden_request_payload($model, $case); + $response = king_golden_http_json('POST', $url . '/chat/completions', $payload, $timeout); + $content = ''; + if (is_array($response['json'])) { + $content = (string) ($response['json']['choices'][0]['message']['content'] ?? ''); + } + $evaluation = $response['status'] >= 200 && $response['status'] < 300 + ? king_golden_evaluate($content, $case['expected']) + : [ + 'ok' => false, + 'expected' => $case['expected'], + 'actual' => $response['raw'], + 'reason' => 'http_' . $response['status'], + ]; + + return [ + 'name' => $case['name'], + 'coverage' => $case['coverage'], + 'scope' => $case['scope'] ?? 'model', + 'model' => $model, + 'sampler' => $case['sampler'], + 'max_tokens' => $case['max_tokens'], + 'stop' => $case['stop'] ?? null, + 'expected' => $case['expected'], + 'http_status' => $response['status'], + 'duration_ms' => $response['duration_ms'], + 'content' => trim($content), + 'ok' => $evaluation['ok'], + 'reason' => $evaluation['reason'], + 'evaluation' => $evaluation, + ]; +} + +$options = king_golden_parse_args($argv); +$recordConfig = king_ir_config_from_env(); + +try { + if ($options['pack_summary']) { + $pack = king_golden_load_prompt_pack((string) $options['pack']); + $summary = king_golden_prompt_pack_summary($pack); + if ($options['json']) { + echo json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"; + } else { + echo "King golden prompt pack\n"; + echo "id={$summary['id']}\n"; + echo "version={$summary['version']}\n"; + echo "path={$summary['path']}\n"; + echo "cases={$summary['case_count']}\n"; + echo "deterministic={$summary['deterministic_count']}\n"; + echo "qualitative={$summary['qualitative_count']}\n"; + echo "categories={$summary['category_count']}\n"; + foreach ($summary['categories'] as $category => $count) { + echo " {$category}: {$count}\n"; + } + if ($summary['invalid_count'] > 0) { + echo "invalid={$summary['invalid_count']}\n"; + } + echo "status=" . ($summary['ok'] ? 'ok' : 'invalid') . "\n"; + } + exit($summary['ok'] ? 0 : 1); + } + + $artifact = king_golden_validate_artifact((string) $options['artifact']); + $modelsResponse = king_golden_http_json('GET', (string) $options['url'] . '/models', null, (int) $options['timeout']); + if ($modelsResponse['status'] < 200 || $modelsResponse['status'] >= 300 || !is_array($modelsResponse['json'])) { + throw new RuntimeException('/v1/models is not reachable or returned invalid JSON.'); + } + $model = king_golden_model_id($options, $modelsResponse['json']); + $selected = []; + foreach (($modelsResponse['json']['data'] ?? []) as $entry) { + if (is_array($entry) && ($entry['id'] ?? null) === $model) { + $selected = $entry; + break; + } + } + $modelSummary = king_golden_model_summary($selected); + + $pack = king_golden_load_prompt_pack((string) $options['pack']); + $packCases = king_golden_prompt_pack_runtime_cases($pack); + $allCases = array_merge(king_golden_cases(), $packCases); + $cases = king_golden_filter_cases($allCases, (string) $options['case'], (string) $options['scope']); + if ($cases === []) { + throw new RuntimeException( + ((string) $options['case'] !== '' ? 'Unknown golden case or scope mismatch: ' . $options['case'] : 'No golden cases match scope: ' . $options['scope']) + ); + } + + $results = []; + $recorded = 0; + foreach ($cases as $case) { + $result = king_golden_run_case((string) $options['url'], $model, $case, (int) $options['timeout']); + $results[] = $result; + if (king_ir_record_case($recordConfig, $case, $result, $model, $artifact, $modelSummary) !== null) { + $recorded++; + } + } + + $failed = array_values(array_filter($results, static fn(array $result): bool => !$result['ok'])); + $report = [ + 'ok' => $failed === [], + 'strict' => (bool) $options['strict'], + 'url' => $options['url'], + 'model' => $model, + 'scope' => $options['scope'], + 'artifact' => $artifact, + 'model_entry' => $modelSummary, + 'prompt_pack' => king_golden_prompt_pack_summary($pack), + 'result_recording' => ['enabled' => (bool) $recordConfig['enabled'], 'path' => $recordConfig['path'], 'records' => $recorded], + 'available_case_count' => count($allCases), + 'case_count' => count($results), + 'passed' => count($results) - count($failed), + 'failed' => count($failed), + 'coverage' => array_values(array_unique(array_map(static fn(array $result): string => (string) $result['coverage'], $results))), + 'results' => $results, + ]; + + if ($options['json']) { + echo json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"; + } else { + echo "King golden prompt contract\n"; + echo "url={$report['url']}\n"; + echo "model={$report['model']}\n"; + echo "artifact={$artifact['path']}\n"; + echo "scope={$report['scope']}\n"; + echo "mode=" . ($report['strict'] ? 'strict' : 'report') . "\n"; + if ($recorded > 0) { + echo "result_records={$recorded}\n"; + } + foreach ($results as $result) { + echo sprintf( + "[%s] %s (%s/%s, %.1fms): %s\n", + $result['ok'] ? 'PASS' : 'FAIL', + $result['name'], + $result['coverage'], + $result['scope'], + (float) $result['duration_ms'], + $result['reason'] + ); + if (!$result['ok']) { + echo " actual: " . str_replace("\n", "\\n", $result['content']) . "\n"; + } + } + echo "summary={$report['passed']}/{$report['case_count']} passed\n"; + } + + exit($failed !== [] && $options['strict'] ? 1 : 0); +} catch (Throwable $exception) { + fwrite(STDERR, 'golden-prompts: ' . $exception->getMessage() . "\n"); + exit(2); +} diff --git a/infra/inference/king-local-infer.php b/infra/inference/king-local-infer.php new file mode 100644 index 000000000..6b4b9f65d --- /dev/null +++ b/infra/inference/king-local-infer.php @@ -0,0 +1,406 @@ + null, + 'prompt' => '', + 'tokens' => 128, + 'temperature' => 0.8, + 'top_k' => 40, + 'top_p' => 0.95, + 'seed' => 0, + 'context' => 0, + 'gpu_layers' => 0, + 'sampler' => null, + 'stops' => [], + ]; + + $float = static function (string $value, string $arg): float { + if (!is_numeric($value)) { + fail("{$arg} must be a finite number"); + } + $number = (float) $value; + if (!is_finite($number)) { + fail("{$arg} must be a finite number"); + } + return $number; + }; + $int = static function (string $value, string $arg): int { + if (!preg_match('/^-?[0-9]+$/', $value)) { + fail("{$arg} must be an integer"); + } + return (int) $value; + }; + + for ($i = 1; $i < count($argv); $i++) { + $arg = $argv[$i]; + $next = static function () use ($argv, &$i, $arg): string { + if (!array_key_exists($i + 1, $argv)) { + fail("missing value for {$arg}"); + } + return (string) $argv[++$i]; + }; + + switch ($arg) { + case '-m': + case '--model': + $options['model'] = $next(); + break; + case '-p': + case '--prompt': + $options['prompt'] = $next(); + break; + case '-n': + case '--tokens': + $options['tokens'] = max(1, (int) $next()); + break; + case '--temp': + case '--temperature': + $options['temperature'] = $float($next(), $arg); + if ($options['temperature'] < 0.0) { + fail("{$arg} must be non-negative"); + } + break; + case '--top-k': + $options['top_k'] = $int($next(), $arg); + if ($options['top_k'] < 0) { + fail("{$arg} must be non-negative"); + } + break; + case '--top-p': + $options['top_p'] = $float($next(), $arg); + if ($options['top_p'] <= 0.0 || $options['top_p'] > 1.0) { + fail("{$arg} must be greater than zero and at most one"); + } + break; + case '--sampler': + $options['sampler'] = $next(); + break; + case '--seed': + $options['seed'] = $int($next(), $arg); + break; + case '-c': + case '--ctx-size': + $options['context'] = max(0, (int) $next()); + break; + case '--reverse-prompt': + case '--stop': + $options['stops'][] = $next(); + break; + case '-ngl': + case '--gpu-layers': + $options['gpu_layers'] = max(0, (int) $next()); + break; + case '--no-display-prompt': + case '--log-disable': + break; + default: + fail("unsupported argument {$arg}"); + } + } + + if (!is_string($options['model']) || $options['model'] === '') { + fail('model path is required via -m'); + } + if (!is_file($options['model'])) { + fail("model path does not exist: {$options['model']}"); + } + + return $options; +} + +function require_supported_execution(array $args): void +{ + if ((int) $args['gpu_layers'] > 0) { + fail( + 'GPU offload was requested, but this King local PHP graph runner has no native GPU decoder kernel yet. Refusing CPU fallback.' + ); + } +} + +function model_load(string $path): King\Inference\Model +{ + return king_inference_model_load([ + 'name' => basename($path), + 'artifact' => $path, + 'backend' => 'king_native_cpu', + 'with_memory' => false, + ]); +} + +function graph_options(array $gguf): array +{ + $vocab = max(1, (int) ($gguf['tokenizer_token_count'] ?? 262144)); + $hidden = max(1, (int) ($gguf['embedding_length'] ?? 1152)); + + return [ + 'max_vector_values' => max($vocab, 262144), + 'max_operations' => max($vocab * $hidden + 1024, 400000000), + 'return_outputs' => false, + ]; +} + +function require_kv_state(array $result): array +{ + $state = $result['state'] ?? null; + if (!is_array($state)) { + fail('native graph produced no state for KV-cache continuity'); + } + + $kvCache = $state['kv_cache'] ?? null; + if (!is_array($kvCache) || $kvCache === []) { + fail('native graph produced no kv_cache entries for KV-cache continuity'); + } + + return $state; +} + +function metadata_token_id(array $gguf, string $key): ?int +{ + if (!array_key_exists($key, $gguf)) { + return null; + } + + $id = (int) $gguf[$key]; + return $id >= 0 ? $id : null; +} + +function prepare_prompt_tokens(array $tokens, ?int $bosTokenId, int $context): array +{ + $tokens = array_values(array_map('intval', $tokens)); + if ($bosTokenId !== null && ($tokens === [] || $tokens[0] !== $bosTokenId)) { + array_unshift($tokens, $bosTokenId); + } + if ($tokens === []) { + fail('prompt produced no tokens'); + } + if ($context > 0 && count($tokens) > $context) { + if ($bosTokenId !== null && $context > 1 && $tokens[0] === $bosTokenId) { + $tokens = array_merge([$bosTokenId], array_slice($tokens, -($context - 1))); + } else { + $tokens = array_slice($tokens, -$context); + } + } + + return array_values($tokens); +} + +function normalize_stop_sequences(array $stops): array +{ + if (count($stops) > 4) { + fail('at most four stop sequences are supported'); + } + + $unique = []; + foreach ($stops as $stop) { + $stop = (string) $stop; + if ($stop === '') { + fail('stop sequence must not be empty'); + } + $unique[$stop] = $stop; + } + + return array_values($unique); +} + +function run_step( + King\Inference\Model $model, + int|array $token, + int $position, + ?array $state, + array $sample, + bool $emitToken, + array $options +): array { + $decodeOptions = $sample; + $decodeOptions['emit_token'] = $emitToken; + if ($state !== null) { + $decodeOptions['state'] = $state; + } + + $graph = king_inference_token_decode_graph($model, $token, $position, $decodeOptions); + $result = king_inference_graph_run($model, $graph, $options); + $nextState = require_kv_state($result); + + if (!$emitToken) { + return [null, $nextState]; + } + + $payload = $result['final']['next_token']['values'] ?? null; + if (!is_array($payload) || !isset($payload[0])) { + fail('native graph produced no next token'); + } + + return [(int) $payload[0], $nextState]; +} + +function should_stop_on_token(int $token, ?int $bosTokenId, ?int $eosTokenId): bool +{ + if ($eosTokenId !== null && $token === $eosTokenId) { + return true; + } + if ($bosTokenId !== null && $token === $bosTokenId) { + return true; + } + + return false; +} + +function find_stop_offset(string $text, array $stops): ?int +{ + $offset = null; + foreach ($stops as $stop) { + $position = strpos($text, $stop); + if ($position !== false && ($offset === null || $position < $offset)) { + $offset = $position; + } + } + + return $offset; +} + +function pending_stop_prefix_length(string $text, array $stops): int +{ + $keep = 0; + foreach ($stops as $stop) { + $max = min(strlen($stop) - 1, strlen($text)); + for ($length = $max; $length > 0; $length--) { + if (substr($text, -$length) === substr($stop, 0, $length)) { + $keep = max($keep, $length); + break; + } + } + } + + return $keep; +} + +function write_output(string $text): void +{ + if ($text === '') { + return; + } + + echo $text; + flush(); +} + +function emit_generated_text(string &$pending, string $piece, array $stops): bool +{ + if ($stops === []) { + write_output($piece); + return false; + } + + $pending .= $piece; + $stopOffset = find_stop_offset($pending, $stops); + if ($stopOffset !== null) { + write_output(substr($pending, 0, $stopOffset)); + $pending = ''; + return true; + } + + $keep = pending_stop_prefix_length($pending, $stops); + $emitLength = strlen($pending) - $keep; + if ($emitLength > 0) { + write_output(substr($pending, 0, $emitLength)); + $pending = $keep > 0 ? substr($pending, -$keep) : ''; + } + + return false; +} + +$args = parse_args($argv); +$stopSequences = normalize_stop_sequences($args['stops']); +require_supported_execution($args); +$model = model_load($args['model']); +$info = king_inference_model_info($model); +$gguf = $info['gguf'] ?? []; +if (!is_array($gguf)) { + fail('model does not expose GGUF metadata'); +} + +$encoded = king_inference_tokenize($model, (string) $args['prompt']); +$tokens = $encoded['tokens'] ?? []; +if (!is_array($tokens)) { + fail('tokenizer did not return token ids'); +} +$bosTokenId = metadata_token_id($gguf, 'tokenizer_bos_id'); +$eosTokenId = metadata_token_id($gguf, 'tokenizer_eos_id'); +$tokens = prepare_prompt_tokens($tokens, $bosTokenId, (int) $args['context']); + +$graphOptions = graph_options($gguf); +$state = null; +$nextToken = null; +$tokenizedPrompt = ['tokens' => $tokens]; +$sample = [ + 'temperature' => (float) $args['temperature'], + 'top_k' => (int) $args['top_k'], + 'top_p' => (float) $args['top_p'], + 'seed' => (int) $args['seed'], +]; +if (is_string($args['sampler']) && $args['sampler'] !== '') { + $sample['sampler'] = $args['sampler']; +} + +foreach (array_keys($tokens) as $index) { + [$nextToken, $state] = run_step( + $model, + $tokenizedPrompt, + $index, + $state, + $sample, + $index === count($tokens) - 1, + $graphOptions + ); +} + +$pendingText = ''; +$stoppedByText = false; +$generated = 0; +$position = count($tokens); +while ($nextToken !== null && $generated < (int) $args['tokens']) { + if (should_stop_on_token($nextToken, $bosTokenId, $eosTokenId)) { + break; + } + + $piece = king_inference_token_decode($model, $nextToken); + $generated++; + if (emit_generated_text($pendingText, $piece, $stopSequences)) { + $stoppedByText = true; + break; + } + if ($generated >= (int) $args['tokens']) { + break; + } + + [$nextToken, $state] = run_step( + $model, + $nextToken, + $position, + $state, + $sample, + true, + $graphOptions + ); + $position++; +} + +if (!$stoppedByText) { + write_output($pendingText); +} diff --git a/infra/inference/king-native-hello-world.php b/infra/inference/king-native-hello-world.php new file mode 100644 index 000000000..4a882abcf --- /dev/null +++ b/infra/inference/king-native-hello-world.php @@ -0,0 +1,599 @@ + getenv('KING_INFERENCE_HELLO_BACKEND') ?: 'both', + 'mode' => getenv('KING_INFERENCE_HELLO_MODE') ?: 'roundtrip', + 'text' => getenv('KING_INFERENCE_HELLO_TEXT') ?: 'Hello world', + 'prompt' => getenv('KING_INFERENCE_HELLO_PROMPT') ?: 'Say hello.', + 'expect_prompt' => getenv('KING_INFERENCE_HELLO_EXPECT_PROMPT') ?: '', + 'prompt_template' => getenv('KING_INFERENCE_HELLO_PROMPT_TEMPLATE') ?: 'auto', + 'tokens' => getenv('KING_INFERENCE_HELLO_TOKENS') ?: '1', + 'cpu_model' => getenv('KING_INFERENCE_HELLO_CPU_MODEL_PATH') ?: '', + 'gpu_model' => getenv('KING_INFERENCE_HELLO_GPU_MODEL_PATH') ?: '', + 'model' => getenv('KING_INFERENCE_HELLO_MODEL_PATH') ?: '', + 'json' => false, + ]; + + for ($i = 1; $i < count($argv); $i++) { + $arg = (string) $argv[$i]; + $next = static function () use (&$i, $argv, $arg): string { + if (!array_key_exists($i + 1, $argv)) { + fail("missing value for {$arg}", 64); + } + return (string) $argv[++$i]; + }; + + if ($arg === '--help' || $arg === '-h') { + usage(); + } elseif ($arg === '--json') { + $options['json'] = true; + } elseif ($arg === '--cpu') { + $options['backend'] = 'cpu'; + } elseif ($arg === '--gpu') { + $options['backend'] = 'gpu'; + } elseif ($arg === '--both') { + $options['backend'] = 'both'; + } elseif (str_starts_with($arg, '--backend=')) { + $options['backend'] = substr($arg, strlen('--backend=')); + } elseif ($arg === '--backend') { + $options['backend'] = $next(); + } elseif (str_starts_with($arg, '--mode=')) { + $options['mode'] = substr($arg, strlen('--mode=')); + } elseif ($arg === '--mode') { + $options['mode'] = $next(); + } elseif (str_starts_with($arg, '--text=')) { + $options['text'] = substr($arg, strlen('--text=')); + } elseif ($arg === '--text') { + $options['text'] = $next(); + } elseif (str_starts_with($arg, '--prompt=')) { + $options['prompt'] = substr($arg, strlen('--prompt=')); + } elseif ($arg === '--prompt') { + $options['prompt'] = $next(); + } elseif (str_starts_with($arg, '--expect-prompt=')) { + $options['expect_prompt'] = substr($arg, strlen('--expect-prompt=')); + } elseif ($arg === '--expect-prompt') { + $options['expect_prompt'] = $next(); + } elseif (str_starts_with($arg, '--prompt-template=')) { + $options['prompt_template'] = substr($arg, strlen('--prompt-template=')); + } elseif ($arg === '--prompt-template') { + $options['prompt_template'] = $next(); + } elseif (str_starts_with($arg, '--tokens=')) { + $options['tokens'] = substr($arg, strlen('--tokens=')); + } elseif ($arg === '--tokens') { + $options['tokens'] = $next(); + } elseif (str_starts_with($arg, '--model=')) { + $options['model'] = substr($arg, strlen('--model=')); + } elseif ($arg === '--model') { + $options['model'] = $next(); + } elseif (str_starts_with($arg, '--cpu-model=')) { + $options['cpu_model'] = substr($arg, strlen('--cpu-model=')); + } elseif ($arg === '--cpu-model') { + $options['cpu_model'] = $next(); + } elseif (str_starts_with($arg, '--gpu-model=')) { + $options['gpu_model'] = substr($arg, strlen('--gpu-model=')); + } elseif ($arg === '--gpu-model') { + $options['gpu_model'] = $next(); + } else { + fail("unsupported argument {$arg}", 64); + } + } + + $backend = strtolower((string) $options['backend']); + if (!in_array($backend, ['cpu', 'gpu', 'both'], true)) { + fail('backend must be cpu, gpu, or both', 64); + } + $options['backend'] = $backend; + $mode = strtolower((string) $options['mode']); + if (!in_array($mode, ['roundtrip', 'prompt', 'both'], true)) { + fail('mode must be roundtrip, prompt, or both', 64); + } + $options['mode'] = $mode; + if ((string) $options['text'] === '') { + fail('text must not be empty', 64); + } + if ((string) $options['prompt'] === '') { + fail('prompt must not be empty', 64); + } + $promptTemplate = strtolower((string) $options['prompt_template']); + if (!in_array($promptTemplate, ['auto', 'none'], true)) { + fail('prompt-template must be auto or none', 64); + } + $options['prompt_template'] = $promptTemplate; + if (!preg_match('/^[1-9][0-9]*$/', (string) $options['tokens'])) { + fail('tokens must be a positive integer', 64); + } + $options['tokens'] = (int) $options['tokens']; + + return $options; +} + +function env_int(string $name, int $default): int +{ + $value = getenv($name); + if ($value === false || $value === '') { + return $default; + } + if (!preg_match('/^-?[0-9]+$/', $value)) { + fail("{$name} must be an integer"); + } + return (int) $value; +} + +function env_float(string $name, float $default): float +{ + $value = getenv($name); + if ($value === false || $value === '') { + return $default; + } + if (!is_numeric($value)) { + fail("{$name} must be numeric"); + } + return (float) $value; +} + +function env_bool(string $name, bool $default): bool +{ + $value = getenv($name); + if ($value === false || $value === '') { + return $default; + } + $normalized = strtolower($value); + if (in_array($normalized, ['1', 'true', 'yes', 'on'], true)) { + return true; + } + if (in_array($normalized, ['0', 'false', 'no', 'off'], true)) { + return false; + } + fail("{$name} must be boolean"); +} + +function resolve_model_path(array $options, string $backend): string +{ + $root = dirname(__DIR__, 2); + $fallback = $backend === 'gpu' + ? $root . '/var/inference-models/gemma4-12b.gguf' + : $root . '/var/inference-models/gemma3-1b.gguf'; + $candidates = []; + + if ($backend === 'cpu') { + $candidates[] = (string) $options['cpu_model']; + $candidates[] = getenv('KING_INFERENCE_CPU_MODEL_PATH') ?: ''; + } else { + $candidates[] = (string) $options['gpu_model']; + $candidates[] = getenv('KING_INFERENCE_GPU_MODEL_PATH') ?: ''; + } + + $candidates[] = (string) $options['model']; + $candidates[] = getenv('KING_INFERENCE_MODEL_PATH') ?: ''; + $candidates[] = getenv('KING_INFERENCE_TEST_MODEL_PATH') ?: ''; + $candidates[] = $fallback; + + foreach ($candidates as $candidate) { + if ($candidate !== '') { + if (!is_readable($candidate)) { + continue; + } + return $candidate; + } + } + + fail("no readable {$backend} model found; set KING_INFERENCE_HELLO_" . strtoupper($backend) . "_MODEL_PATH"); +} + +function model_config(string $backend, string $path): array +{ + $nativeBackend = $backend === 'gpu' ? 'king_native_gpu' : 'king_native_cpu'; + $contextTokens = env_int('KING_INFERENCE_CONTEXT_TOKENS', 2048); + $config = [ + 'name' => 'hello-world-' . $backend, + 'artifact' => ['path' => $path], + 'backend' => $nativeBackend, + 'context_tokens' => $contextTokens, + 'kv_cache' => [ + 'max_context_tokens' => $contextTokens, + 'page_tokens' => env_int('KING_INFERENCE_KV_PAGE_TOKENS', 16), + 'element_bytes' => env_int('KING_INFERENCE_KV_ELEMENT_BYTES', 2), + ], + 'with_memory' => false, + ]; + + if ($backend === 'gpu') { + $thermal = [ + 'max_temperature_c' => env_float('KING_INFERENCE_GPU_THERMAL_MAX_C', 78.0), + 'check_interval_seconds' => env_int('KING_INFERENCE_GPU_THERMAL_CHECK_INTERVAL_SEC', 15), + 'allow_unmonitored_gpu' => env_bool('KING_INFERENCE_GPU_ALLOW_UNMONITORED', true), + ]; + $sensorCommand = getenv('KING_INFERENCE_GPU_THERMAL_SENSOR_COMMAND'); + if ($sensorCommand === false) { + $sensorCommand = 'nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader,nounits'; + } + if ($sensorCommand !== '') { + $thermal['sensor_command'] = $sensorCommand; + } + + $power = [ + 'max_watts' => env_float('KING_INFERENCE_GPU_POWER_MAX_WATTS', 0.0), + 'check_interval_seconds' => env_int('KING_INFERENCE_GPU_POWER_CHECK_INTERVAL_SEC', 15), + ]; + $powerSensorCommand = getenv('KING_INFERENCE_GPU_POWER_SENSOR_COMMAND'); + if ($powerSensorCommand === false) { + $powerSensorCommand = 'nvidia-smi --query-gpu=power.draw --format=csv,noheader,nounits'; + } + if ($powerSensorCommand !== '') { + $power['sensor_command'] = $powerSensorCommand; + } + + $config['gpu'] = [ + 'enabled' => true, + 'max_gpu_layers' => env_int('KING_INFERENCE_GPU_MAX_GPU_LAYERS', 99), + 'vram_reserve_mb' => env_int('KING_INFERENCE_GPU_VRAM_RESERVE_MB', 1024), + 'min_free_vram_mb' => env_int('KING_INFERENCE_GPU_MIN_FREE_VRAM_MB', 1024), + 'thermal' => $thermal, + 'power' => $power, + ]; + } + + return $config; +} + +function model_uses_gemma_start_turn_template(King\Inference\Model $model): bool +{ + $info = $model->info(); + $architecture = (string) ($info['gguf']['architecture'] ?? $info['architecture'] ?? ''); + if (!str_starts_with($architecture, 'gemma')) { + return false; + } + + $start = king_inference_tokenize($model, ''); + $end = king_inference_tokenize($model, ''); + return ($start['tokens'] ?? []) !== [] && ($end['tokens'] ?? []) !== []; +} + +function prompt_already_formatted(string $prompt): bool +{ + return str_contains($prompt, '') + || str_contains($prompt, '') + || str_contains($prompt, '<|turn>') + || str_contains($prompt, ''); +} + +function format_prompt(King\Inference\Model $model, string $prompt, string $templateMode): string +{ + if ($templateMode === 'none' || prompt_already_formatted($prompt)) { + return $prompt; + } + if (!model_uses_gemma_start_turn_template($model)) { + return $prompt; + } + + return "user\n" . $prompt . "\nmodel\n"; +} + +function hello_graphs(King\Inference\Model $model, string $text): array +{ + $encoded = king_inference_tokenize($model, $text); + $tokens = $encoded['tokens'] ?? null; + if (!is_array($tokens) || $tokens === []) { + fail('tokenizer produced no token ids'); + } + + $graphs = []; + foreach ($tokens as $tokenId) { + $graphs[] = [ + 'inputs' => ['token' => [(int) $tokenId]], + 'ops' => [[ + 'id' => 'next_token', + 'op' => 'scale', + 'input' => 'token', + 'factor' => 1.0, + ]], + 'output' => 'next_token', + ]; + } + + return $graphs; +} + +function gpu_snapshot(): array +{ + $line = @shell_exec('nvidia-smi --query-gpu=temperature.gpu,utilization.gpu,memory.used,memory.total,power.draw --format=csv,noheader,nounits 2>/dev/null | head -n 1'); + if (!is_string($line) || trim($line) === '') { + return ['available' => false]; + } + + $parts = array_map('trim', explode(',', trim($line))); + return [ + 'available' => true, + 'temperature_c' => isset($parts[0]) && is_numeric($parts[0]) ? (float) $parts[0] : null, + 'utilization_percent' => isset($parts[1]) && is_numeric($parts[1]) ? (float) $parts[1] : null, + 'vram_used_mb' => isset($parts[2]) && is_numeric($parts[2]) ? (float) $parts[2] : null, + 'vram_total_mb' => isset($parts[3]) && is_numeric($parts[3]) ? (float) $parts[3] : null, + 'power_w' => isset($parts[4]) && is_numeric($parts[4]) ? (float) $parts[4] : null, + ]; +} + +function gpu_delta(array $before, array $after): array +{ + $delta = []; + foreach (['temperature_c', 'utilization_percent', 'vram_used_mb', 'vram_total_mb', 'power_w'] as $field) { + $beforeValue = $before[$field] ?? null; + $afterValue = $after[$field] ?? null; + $delta[$field] = is_float($beforeValue) && is_float($afterValue) + ? $afterValue - $beforeValue + : null; + } + + return $delta; +} + +function monotonic_elapsed_ms(int $startNs, int $endNs): float +{ + return ($endNs - $startNs) / 1_000_000; +} + +function text_token_count(King\Inference\Model $model, string $text): int +{ + $encoded = king_inference_tokenize($model, $text); + $tokens = $encoded['tokens'] ?? null; + return is_array($tokens) ? count($tokens) : 0; +} + +function stream_text(King\Inference\Stream $stream, int $streamStartNs): array +{ + $raw = ''; + $types = []; + $firstTokenNs = null; + $tokenEvents = 0; + + while (($event = king_inference_next($stream, 1000)) !== null) { + if (!is_array($event)) { + continue; + } + $type = (string) ($event['type'] ?? 'array'); + $types[] = $type; + if ($type === 'token' && isset($event['text']) && is_string($event['text'])) { + if ($firstTokenNs === null) { + $firstTokenNs = hrtime(true); + } + $tokenEvents++; + $raw .= $event['text']; + } + if ($type === 'done') { + break; + } + } + + $finishedNs = hrtime(true); + $streamTotalMs = monotonic_elapsed_ms($streamStartNs, $finishedNs); + $generatedTokens = $tokenEvents; + + return [ + $raw, + $types, + $stream->getMetrics(), + [ + 'stream_ttfb_ms' => $firstTokenNs !== null ? monotonic_elapsed_ms($streamStartNs, $firstTokenNs) : null, + 'stream_total_ms' => $streamTotalMs, + 'token_event_count' => $tokenEvents, + 'tokens_per_second' => $generatedTokens > 0 && $streamTotalMs > 0.0 + ? $generatedTokens / ($streamTotalMs / 1000.0) + : null, + ], + ]; +} + +function graph_options(King\Inference\Model $model): array +{ + $info = $model->info(); + $gguf = is_array($info['gguf'] ?? null) ? $info['gguf'] : []; + $vocab = max(1, (int) ($gguf['tokenizer_token_count'] ?? 262144)); + $hidden = max(1, (int) ($gguf['embedding_length'] ?? 1152)); + + return [ + 'max_vector_values' => max($vocab, 262144), + 'max_operations' => max($vocab * $hidden + 1024, 400000000), + 'return_outputs' => false, + ]; +} + +function result_payload( + string $backend, + string $mode, + string $modelPath, + string $text, + string $raw, + array $types, + array $metrics, + array $measurement, + array $runtimeTruth, + ?array $gpuBefore, + ?array $gpuAfter, + int $promptTokens, + float $modelLoadMs, + float $endToEndMs +): array { + $generatedTokens = is_int($metrics['native_decoder_tokens'] ?? null) + ? $metrics['native_decoder_tokens'] + : ($measurement['token_event_count'] ?? 0); + + return [ + 'backend' => $backend, + 'mode' => $mode, + 'engine' => $backend === 'gpu' ? 'king_native_gpu' : 'king_native_cpu', + 'model' => $modelPath, + 'text' => $text, + 'raw_text' => $raw, + 'event_types' => $types, + 'prompt_tokens' => $promptTokens, + 'generated_tokens' => $generatedTokens, + 'ttfb_ms' => $measurement['stream_ttfb_ms'] ?? null, + 'stream_total_ms' => $measurement['stream_total_ms'] ?? null, + 'end_to_end_ms' => $endToEndMs, + 'model_load_ms' => $modelLoadMs, + 'tokens_per_second' => $measurement['tokens_per_second'] ?? null, + 'model_resident' => $runtimeTruth['model_resident'] ?? null, + 'resident_state' => $runtimeTruth['resident_state'] ?? null, + 'gpu_before' => $gpuBefore, + 'gpu_after' => $gpuAfter, + 'gpu_delta' => $gpuBefore !== null && $gpuAfter !== null ? gpu_delta($gpuBefore, $gpuAfter) : null, + 'native_decoder_tokens' => $metrics['native_decoder_tokens'] ?? null, + 'native_decoder_last_token_id' => $metrics['native_decoder_last_token_id'] ?? null, + 'native_decoder_last_probability' => $metrics['native_decoder_last_probability'] ?? null, + 'native_decoder_last_logit' => $metrics['native_decoder_last_logit'] ?? null, + 'native_decoder_last_rank' => $metrics['native_decoder_last_rank'] ?? null, + 'gpu_thermal_preflight_checked' => $metrics['gpu_thermal_preflight_checked'] ?? false, + 'gpu_thermal_preflight_temperature_c' => $metrics['gpu_thermal_preflight_temperature_c'] ?? null, + 'gpu_power_preflight_checked' => $metrics['gpu_power_preflight_checked'] ?? false, + 'gpu_power_preflight_watts' => $metrics['gpu_power_preflight_watts'] ?? null, + 'gpu_power_aborted' => $metrics['gpu_power_aborted'] ?? false, + 'gpu_power_abort_watts' => $metrics['gpu_power_abort_watts'] ?? null, + 'gpu_power_abort_ceiling_watts' => $metrics['gpu_power_abort_ceiling_watts'] ?? null, + ]; +} + +function run_roundtrip_backend(string $backend, string $text, array $options): array +{ + $overallStartNs = hrtime(true); + $modelPath = resolve_model_path($options, $backend); + $gpuBefore = $backend === 'gpu' ? gpu_snapshot() : null; + $loadStartNs = hrtime(true); + $model = king_inference_model_load(model_config($backend, $modelPath)); + $modelLoadMs = monotonic_elapsed_ms($loadStartNs, hrtime(true)); + $graphs = hello_graphs($model, $text); + $streamStartNs = hrtime(true); + $stream = king_inference_stream( + $model, + ['graphs' => $graphs], + ['with_memory' => false, 'max_native_stream_tokens' => max(16, count($graphs))] + ); + [$raw, $types, $metrics, $measurement] = stream_text($stream, $streamStartNs); + $gpuAfter = $backend === 'gpu' ? gpu_snapshot() : null; + $info = $model->info(); + $runtimeTruth = is_array($info['runtime_truth'] ?? null) ? $info['runtime_truth'] : []; + + $actual = trim($raw); + $expected = trim($text); + if ($actual !== $expected) { + fail("{$backend} roundtrip produced " . json_encode($actual) . ", expected " . json_encode($expected), 2); + } + + return result_payload( + $backend, + 'roundtrip', + $modelPath, + $actual, + $raw, + $types, + $metrics, + $measurement, + $runtimeTruth, + $gpuBefore, + $gpuAfter, + 0, + $modelLoadMs, + monotonic_elapsed_ms($overallStartNs, hrtime(true)) + ); +} + +function run_prompt_backend(string $backend, string $prompt, int $tokens, array $options): array +{ + $overallStartNs = hrtime(true); + $modelPath = resolve_model_path($options, $backend); + $gpuBefore = $backend === 'gpu' ? gpu_snapshot() : null; + $loadStartNs = hrtime(true); + $model = king_inference_model_load(model_config($backend, $modelPath)); + $modelLoadMs = monotonic_elapsed_ms($loadStartNs, hrtime(true)); + $formattedPrompt = format_prompt($model, $prompt, (string) $options['prompt_template']); + $promptTokens = text_token_count($model, $formattedPrompt); + $streamStartNs = hrtime(true); + $stream = king_inference_stream( + $model, + [ + 'native_prompt_text' => $formattedPrompt, + 'max_tokens' => $tokens, + 'graph_options' => graph_options($model), + 'temperature' => 0, + ], + ['with_memory' => false, 'max_native_stream_tokens' => $tokens] + ); + [$raw, $types, $metrics, $measurement] = stream_text($stream, $streamStartNs); + $gpuAfter = $backend === 'gpu' ? gpu_snapshot() : null; + $info = $model->info(); + $runtimeTruth = is_array($info['runtime_truth'] ?? null) ? $info['runtime_truth'] : []; + $actual = trim($raw); + + if ($actual === '' || !in_array('token', $types, true)) { + fail("{$backend} prompt mode produced no token text", 2); + } + if ((string) $options['expect_prompt'] !== '' && $actual !== trim((string) $options['expect_prompt'])) { + fail( + "{$backend} prompt mode produced " . json_encode($actual) . ", expected " . json_encode(trim((string) $options['expect_prompt'])), + 2 + ); + } + + return result_payload( + $backend, + 'prompt', + $modelPath, + $actual, + $raw, + $types, + $metrics, + $measurement, + $runtimeTruth, + $gpuBefore, + $gpuAfter, + $promptTokens, + $modelLoadMs, + monotonic_elapsed_ms($overallStartNs, hrtime(true)) + ); +} + +if (!extension_loaded('king')) { + fail('King extension is not loaded'); +} + +$options = parse_cli($argv); +$backends = $options['backend'] === 'both' ? ['cpu', 'gpu'] : [$options['backend']]; +$modes = $options['mode'] === 'both' ? ['roundtrip', 'prompt'] : [$options['mode']]; +$results = []; + +foreach ($backends as $backend) { + foreach ($modes as $mode) { + if ($mode === 'roundtrip') { + $results[] = run_roundtrip_backend($backend, (string) $options['text'], $options); + } else { + $results[] = run_prompt_backend($backend, (string) $options['prompt'], (int) $options['tokens'], $options); + } + } +} + +if ($options['json']) { + echo json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"; + exit(0); +} + +foreach ($results as $result) { + echo $result['backend'] . '/' . $result['mode'] . ' [' . $result['engine'] . ']: ' . $result['text'] . "\n"; +} diff --git a/infra/inference/layer0-debug.php b/infra/inference/layer0-debug.php new file mode 100644 index 000000000..e65f64155 --- /dev/null +++ b/infra/inference/layer0-debug.php @@ -0,0 +1,367 @@ + getenv('KING_INFERENCE_LAYER0_PROMPT') ?: 'Hello world', + 'position' => getenv('KING_INFERENCE_LAYER0_POSITION') ?: '0', + 'timeout_ms' => getenv('KING_INFERENCE_LAYER0_TIMEOUT_MS') ?: '30000', + 'max_values' => getenv('KING_INFERENCE_LAYER0_NUMERIC_MAX_VALUES') ?: '8', + 'include_raw' => false, + 'json' => false, + ]; + + for ($i = 1; $i < count($argv); $i++) { + $arg = (string) $argv[$i]; + $next = static function () use (&$i, $argv, $arg): string { + if (!array_key_exists($i + 1, $argv)) { + fail("missing value for {$arg}", 64); + } + return (string) $argv[++$i]; + }; + + if ($arg === '--help' || $arg === '-h') { + usage(); + } elseif ($arg === '--json') { + $options['json'] = true; + } elseif ($arg === '--include-raw') { + $options['include_raw'] = true; + } elseif (str_starts_with($arg, '--prompt=')) { + $options['prompt'] = substr($arg, strlen('--prompt=')); + } elseif ($arg === '--prompt') { + $options['prompt'] = $next(); + } elseif (str_starts_with($arg, '--position=')) { + $options['position'] = substr($arg, strlen('--position=')); + } elseif ($arg === '--position') { + $options['position'] = $next(); + } elseif (str_starts_with($arg, '--timeout-ms=')) { + $options['timeout_ms'] = substr($arg, strlen('--timeout-ms=')); + } elseif ($arg === '--timeout-ms') { + $options['timeout_ms'] = $next(); + } elseif (str_starts_with($arg, '--max-values=')) { + $options['max_values'] = substr($arg, strlen('--max-values=')); + } elseif ($arg === '--max-values') { + $options['max_values'] = $next(); + } else { + fail("unsupported argument {$arg}", 64); + } + } + + foreach (['position', 'timeout_ms', 'max_values'] as $key) { + if (!preg_match('/^[0-9]+$/', (string) $options[$key])) { + fail("{$key} must be a non-negative integer", 64); + } + $options[$key] = (int) $options[$key]; + } + if ((string) $options['prompt'] === '') { + fail('prompt must not be empty', 64); + } + if ($options['timeout_ms'] <= 0 || $options['max_values'] <= 0) { + fail('timeout-ms and max-values must be positive', 64); + } + + return $options; +} + +function layer0_model_config(int $maxValues): array +{ + $config = king_inference_runtime_model_config(); + if (($config['backend'] ?? null) !== 'king_native_gpu') { + fail('layer-0 debug requires the runtime GPU profile; set the effective King inference profile to gpu'); + } + + $config['with_memory'] = false; + $config['gpu'] = is_array($config['gpu'] ?? null) ? $config['gpu'] : []; + $config['gpu']['debug'] = is_array($config['gpu']['debug'] ?? null) ? $config['gpu']['debug'] : []; + $config['gpu']['debug']['numeric_compare_enabled'] = true; + $config['gpu']['debug']['numeric_compare_max_values'] = $maxValues; + + return $config; +} + +function graph_options(King\Inference\Model $model): array +{ + $info = $model->info(); + $gguf = is_array($info['gguf'] ?? null) ? $info['gguf'] : []; + $vocab = max(1, (int) ($gguf['tokenizer_token_count'] ?? 262144)); + $hidden = max(1, (int) ($gguf['embedding_length'] ?? 1152)); + + return [ + 'max_vector_values' => max($vocab, 262144), + 'max_operations' => max($vocab * $hidden + 1024, 400000000), + 'return_outputs' => false, + ]; +} + +function reference_contract_report(King\Inference\Model $model): array +{ + $info = $model->info(); + $capabilities = is_array($info['backend_capabilities'] ?? null) ? $info['backend_capabilities'] : []; + $contract = is_array($capabilities['reference_backend'] ?? null) ? $capabilities['reference_backend'] : []; + $operations = is_array($contract['compared_operations'] ?? null) ? $contract['compared_operations'] : []; + $required = [ + 'embedding', + 'rms_norm', + 'qkv_projection', + 'rope', + 'attention_score', + 'attention_softmax', + 'attention_value', + 'ffn_gate_up', + 'ffn_swiglu', + 'ffn_down', + 'final_norm', + 'logits_projection', + ]; + $missing = array_values(array_diff($required, $operations)); + if (($contract['available'] ?? false) !== true + || ($contract['selected_reference'] ?? null) !== 'king_internal_cpu_reference' + || ($contract['production_execution_path'] ?? true) !== false + || $missing !== []) { + fail('GPU reference backend contract is not wired for layer-0 numeric debug'); + } + + $gpuRuntime = is_array($info['gpu_runtime'] ?? null) ? $info['gpu_runtime'] : []; + $ops = is_array($info['device_vector_ops'] ?? null) + ? $info['device_vector_ops'] + : (is_array($gpuRuntime['device_vector_ops'] ?? null) ? $gpuRuntime['device_vector_ops'] : []); + $hook = is_array($ops['numeric_compare_hook'] ?? null) ? $ops['numeric_compare_hook'] : []; + + return [ + 'schema_version' => $contract['schema_version'] ?? null, + 'selected_reference' => $contract['selected_reference'] ?? null, + 'comparison_path' => $contract['comparison_path'] ?? null, + 'activation' => $contract['activation'] ?? null, + 'production_execution_path' => $contract['production_execution_path'] ?? null, + 'required_operations_present' => true, + 'numeric_compare_hook_status' => $hook['status'] ?? null, + 'numeric_compare_hook_reference' => $hook['reference_backend'] ?? null, + ]; +} + +function first_graph_result(King\Inference\Stream $stream, int $timeoutMs): array +{ + while (($event = king_inference_next($stream, $timeoutMs)) !== null) { + if (!is_array($event)) { + continue; + } + if (($event['type'] ?? null) === 'gpu_decoder_graph_execution_result') { + return $event; + } + if (($event['type'] ?? null) === 'error') { + fail((string) ($event['message'] ?? 'native stream returned an error')); + } + if (($event['type'] ?? null) === 'done') { + break; + } + } + + fail('native stream ended without gpu_decoder_graph_execution_result'); +} + +function collect_compares(mixed $value, array &$compares, array &$failed, string $path = ''): void +{ + if (!is_array($value)) { + return; + } + if ((isset($value['type']) && is_string($value['type']) && str_contains($value['type'], 'numeric_compare')) + || (array_key_exists('matched', $value) && array_key_exists('status', $value))) { + $status = (string) ($value['status'] ?? ''); + $matched = $value['matched'] ?? null; + $ok = ($matched === true) || $status === 'matched'; + $compares[] = [ + 'path' => $path, + 'type' => $value['type'] ?? basename(str_replace('.', '/', $path)), + 'status' => $status !== '' ? $status : ($ok ? 'matched' : 'unknown'), + 'compared_values' => $value['compared_values'] ?? null, + 'matched_values' => $value['matched_values'] ?? null, + 'max_abs_diff' => $value['max_abs_diff'] ?? null, + 'tolerance' => $value['tolerance'] ?? null, + ]; + if (!$ok) { + $failed[] = $path; + } + return; + } + + foreach ($value as $key => $child) { + $childPath = $path === '' ? (string) $key : $path . '.' . (string) $key; + if (is_string($key) && str_ends_with($key, '_numeric_compare') && is_array($child)) { + $status = (string) ($child['status'] ?? ''); + $matched = $child['matched'] ?? null; + $ok = ($matched === true) || $status === 'matched'; + $compares[] = [ + 'path' => $childPath, + 'type' => $child['type'] ?? $key, + 'status' => $status !== '' ? $status : ($ok ? 'matched' : 'unknown'), + 'compared_values' => $child['compared_values'] ?? null, + 'matched_values' => $child['matched_values'] ?? null, + 'max_abs_diff' => $child['max_abs_diff'] ?? null, + 'tolerance' => $child['tolerance'] ?? null, + ]; + if (!$ok) { + $failed[] = $childPath; + } + continue; + } + if (is_string($key) && str_ends_with($key, '_numeric_compare_failed') && $child === true) { + $failed[] = $childPath; + continue; + } + collect_compares($child, $compares, $failed, $childPath); + } +} + +function stage_report(string $name, array $result, array $keys, ?string $compareTypeContains = null): array +{ + $present = []; + $compares = []; + $failed = []; + + foreach ($keys as $key) { + if (!array_key_exists($key, $result)) { + continue; + } + $present[] = $key; + collect_compares($result[$key], $compares, $failed, $key); + } + if ($compareTypeContains !== null) { + $compares = array_values(array_filter( + $compares, + static fn (array $compare): bool => str_contains((string) ($compare['type'] ?? ''), $compareTypeContains) + || str_contains((string) ($compare['path'] ?? ''), $compareTypeContains) + )); + $failed = array_values(array_filter( + $failed, + static fn (string $path): bool => str_contains($path, $compareTypeContains) + )); + } + + return [ + 'name' => $name, + 'ready' => $present !== [] && $failed === [], + 'present_keys' => $present, + 'compare_count' => count($compares), + 'compare_status' => $compares === [] ? 'missing' : ($failed === [] ? 'matched' : 'failed'), + 'failed_compares' => array_values(array_unique($failed)), + 'compares' => $compares, + ]; +} + +function summarize_layer0(array $result): array +{ + $stages = [ + stage_report('embedding', $result, ['embedding_device_execution', 'embedding_numeric_compare']), + stage_report('norm', $result, ['rms_norm_device_execution', 'first_rms_norm_numeric_compare']), + stage_report('qkv', $result, ['linear_device_execution', 'block0_qkv_projection_numeric_compares']), + stage_report('rope', $result, ['rope_device_execution', 'kv_head_prepare_device_execution'], 'rope'), + stage_report('attention', $result, [ + 'kv_head_prepare_device_execution', + 'block0_attention_score_numeric_compare', + 'block0_attention_softmax_numeric_compare', + 'block0_attention_value_numeric_compare', + ]), + stage_report('residual', $result, [ + 'attention_output_projection_device_execution', + 'attention_residual_device_execution', + ]), + stage_report('ffn', $result, [ + 'ffn_norm_device_execution', + 'ffn_gate_up_projection_device_execution', + 'ffn_swiglu_device_execution', + 'ffn_down_projection_device_execution', + 'ffn_output_residual_device_execution', + ]), + stage_report('logits', $result, ['final_norm_device_execution', 'logits_projection_device_execution']), + ]; + + $missing = []; + $failed = []; + foreach ($stages as $stage) { + if ($stage['present_keys'] === []) { + $missing[] = $stage['name']; + } + if ($stage['compare_status'] !== 'matched') { + $failed[] = $stage['name']; + } + } + + return [ + 'stages' => $stages, + 'all_required_stages_present' => $missing === [], + 'all_stage_compares_matched' => $failed === [], + 'missing_stages' => $missing, + 'stages_without_matched_compares' => $failed, + ]; +} + +$options = parse_cli($argv); +$model = king_inference_model_load(layer0_model_config((int) $options['max_values'])); +$referenceContract = reference_contract_report($model); +$encoded = king_inference_tokenize($model, (string) $options['prompt']); +$tokens = $encoded['tokens'] ?? null; +if (!is_array($tokens) || $tokens === []) { + fail('tokenizer produced no token ids'); +} +if (!array_key_exists((int) $options['position'], $tokens)) { + fail('position exceeds tokenized prompt length', 64); +} + +$graph = king_inference_token_decode_graph($model, $encoded, (int) $options['position'], [ + 'debug_layer_limit' => 1, + 'emit_token' => true, + 'temperature' => 0.0, + 'sampler' => 'argmax', +]); +$stream = king_inference_stream( + $model, + ['graphs' => [$graph], 'graph_options' => graph_options($model)], + ['with_memory' => false, 'max_native_stream_tokens' => 1] +); +$result = first_graph_result($stream, (int) $options['timeout_ms']); +$summary = summarize_layer0($result); +$payload = [ + 'ok' => $summary['all_required_stages_present'] && $summary['all_stage_compares_matched'], + 'prompt' => $options['prompt'], + 'position' => $options['position'], + 'token_id' => $result['token_id'] ?? null, + 'block_count' => $result['block_count'] ?? null, + 'model_block_count' => $result['model_block_count'] ?? null, + 'debug_layer_limit' => $result['debug_layer_limit'] ?? null, + 'device_execution_result_ready' => $result['device_execution_result_ready'] ?? false, + 'executed_device_ops' => $result['executed_device_ops'] ?? null, + 'reference_backend_contract' => $referenceContract, +] + $summary; + +if ($options['include_raw']) { + $payload['raw_result'] = $result; +} + +if ($options['json']) { + echo json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), "\n"; +} else { + echo "Layer-0 debug: " . ($payload['ok'] ? 'OK' : 'FAILED') . "\n"; + echo "Token id: " . (string) $payload['token_id'] . "\n"; + echo "Blocks: " . (string) $payload['block_count'] . " / model " . (string) $payload['model_block_count'] . "\n"; + foreach ($payload['stages'] as $stage) { + echo "- {$stage['name']}: {$stage['compare_status']} ({$stage['compare_count']} compares)\n"; + } +} + +if (!$payload['ok']) { + exit(2); +} diff --git a/infra/inference/local-gpu.php.ini b/infra/inference/local-gpu.php.ini new file mode 100644 index 000000000..89064df5b --- /dev/null +++ b/infra/inference/local-gpu.php.ini @@ -0,0 +1,35 @@ +; King local inference profile for the OpenAI-compatible router. +; Loaded by bin/king-openai-router. + +king.gpu_bindings_enable=1 +king.gpu_default_backend=cuda + +king.inference_preferred_model_profile=gpu +; Optional multi-model registry for /v1/models: +; id,backend,artifact,alias1+alias2@id2,backend,artifact,alias +; king.inference_models=gemma4:12b,gpu,${KING_ROOT}/var/inference-models/gemma4-12b.gguf,gemma4 +king.inference_cpu_model_name=gemma3:1b +king.inference_cpu_model_artifact=${KING_ROOT}/var/inference-models/gemma3-1b.gguf +king.inference_gpu_model_name=gemma3:1b +king.inference_gpu_model_artifact=${KING_ROOT}/var/inference-models/gemma3-1b.gguf +king.inference_context_tokens=1024 +king.inference_kv_page_tokens=16 +king.inference_kv_element_bytes=2 +king.inference_gpu_max_gpu_layers=99 +king.inference_gpu_vram_reserve_mb=512 +king.inference_gpu_min_free_vram_mb=256 +king.inference_gpu_allow_system_ram_offload=1 +king.inference_gpu_system_ram_offload_max_mb=32768 +king.inference_gpu_system_ram_offload_min_free_mb=8192 +king.inference_with_memory=0 +king.inference_llm_cache_enable=0 + +king.inference_gpu_thermal_sensor_path= +king.inference_gpu_thermal_sensor_command="nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader,nounits" +king.inference_gpu_thermal_max_temperature_c=78 +king.inference_gpu_thermal_check_interval_sec=15 +king.inference_gpu_allow_unmonitored=0 +king.inference_gpu_power_sensor_command="nvidia-smi --query-gpu=power.draw --format=csv,noheader,nounits" +king.inference_gpu_power_max_watts=450 +king.inference_gpu_power_check_interval_sec=15 +king.inference_gpu_batch_prefill_experimental_enable=0 diff --git a/infra/inference/openai-engine-deterministic.php b/infra/inference/openai-engine-deterministic.php new file mode 100644 index 000000000..7f271e001 --- /dev/null +++ b/infra/inference/openai-engine-deterministic.php @@ -0,0 +1,71 @@ + 'not_applicable']; + + if (($toolResult['status'] ?? 'not_applicable') !== 'not_applicable') { + king_openai_engine_log('tool_bridge', [ + 'tool_name' => $toolResult['tool_name'] ?? '', + 'status' => $toolResult['status'] ?? '', + 'error_category' => $toolResult['error_category'] ?? '', + 'duration_ms' => $toolResult['duration_ms'] ?? 0, + 'registered_now' => $toolResult['registered_now'] ?? [], + 'registry_names' => $toolResult['registry_names'] ?? [], + 'registry_errors' => $toolResult['registry_errors'] ?? [], + 'executed' => !empty($toolResult['executed']), + ]); + } + if (!empty($toolResult['executed']) && is_string($toolResult['content'] ?? null)) { + return [ + 'content' => $toolResult['content'], + 'source' => 'mcp_internal_tool', + 'tool' => $toolResult, + ]; + } + + $miniLanguageResult = function_exists('king_openai_router_mini_language_result') + ? king_openai_router_mini_language_result($payload) + : null; + if (is_array($miniLanguageResult) && is_string($miniLanguageResult['content'] ?? null) && $miniLanguageResult['content'] !== '') { + king_openai_engine_log('mini_language', [ + 'operation' => $miniLanguageResult['program']['operation'] ?? '', + 'typed_result' => $miniLanguageResult['result'] ?? null, + 'source' => 'structured_mini_language', + ]); + return [ + 'content' => $miniLanguageResult['content'], + 'source' => 'structured_mini_language', + 'tool' => $miniLanguageResult, + ]; + } + + if (!function_exists('king_inference_runtime_mini_op_content')) { + return null; + } + + $content = king_inference_runtime_mini_op_content($payload); + if (!is_string($content) || $content === '') { + return null; + } + return [ + 'content' => $content, + 'source' => 'runtime_mini_op', + 'tool' => null, + ]; +} + +function king_openai_router_deterministic_result(array $payload, array $routerOptions = []): ?array +{ + return king_openai_engine_deterministic_result($payload, $routerOptions); +} diff --git a/infra/inference/openai-router-coder.php b/infra/inference/openai-router-coder.php new file mode 100644 index 000000000..2675ef565 --- /dev/null +++ b/infra/inference/openai-router-coder.php @@ -0,0 +1,102 @@ + 'system', + 'content' => king_openai_router_coder_instruction_text(), + ]); + $payload['messages'] = $messages; + + return $payload; +} diff --git a/infra/inference/openai-router-memory.php b/infra/inference/openai-router-memory.php new file mode 100644 index 000000000..938461003 --- /dev/null +++ b/infra/inference/openai-router-memory.php @@ -0,0 +1,106 @@ + $withMemory]); + } catch (Throwable $e) { + $status = [ + 'ok' => false, + 'active' => false, + 'reason' => 'status_error:' . $e::class, + ]; + } + } + $statusOk = ($status === []) || !array_key_exists('ok', $status) || $status['ok'] === true; + $statusAction = is_string($status['action'] ?? null) ? $status['action'] : ''; + $llmCacheActive = $withMemory && $llmCacheConfigured && $statusOk; + $disabledReason = ''; + if (!$withMemory) { + $disabledReason = 'memory_disabled'; + } else if (!$llmCacheConfigured) { + $disabledReason = 'llm_cache_not_configured'; + } else if (!$statusOk) { + $disabledReason = $statusAction !== '' ? $statusAction : 'disk_floor_failed'; + } + + return [ + 'with_memory' => $withMemory, + 'mode' => $withMemory ? 'stateful_graph_memory' : 'stateless', + 'llm_cache_configured' => $llmCacheConfigured, + 'llm_cache_active' => $llmCacheActive, + 'llm_cache' => [ + ...$llmCache, + 'enabled' => $llmCacheActive, + 'configured_enabled' => $llmCacheConfigured, + 'status_ok' => $statusOk, + 'status_action' => $statusAction, + 'disabled_reason' => $llmCacheActive ? '' : $disabledReason, + ], + 'llm_cache_status' => is_array($status) ? $status : [], + ]; +} + +function king_openai_router_memory_status_log_fields(array $policy): array +{ + $status = is_array($policy['llm_cache_status'] ?? null) ? $policy['llm_cache_status'] : []; + return [ + 'llm_cache_status_action' => is_string($status['action'] ?? null) ? $status['action'] : '', + 'llm_cache_status_degraded' => !empty($status['degraded']), + 'llm_cache_min_free_mb' => is_int($status['min_free_mb'] ?? null) ? $status['min_free_mb'] : '', + 'llm_cache_free_mb' => is_int($status['free_mb'] ?? null) ? $status['free_mb'] : '', + 'llm_cache_alert_requested' => !empty($status['alert']['requested']), + 'llm_cache_webhook_configured' => !empty($status['alert']['webhook_configured']), + 'llm_cache_mcp_configured' => !empty($status['alert']['mcp_configured']), + ]; +} + +function king_openai_router_payload_bool(array $payload, string $key): ?bool +{ + return is_bool($payload[$key] ?? null) ? $payload[$key] : null; +} + +function king_openai_router_payload_llm_cache_enabled(array $payload): ?bool +{ + $cache = $payload['llm_cache'] ?? null; + if (!is_array($cache)) { + return null; + } + return king_openai_router_payload_bool($cache, 'enabled'); +} + +function king_openai_router_apply_memory_policy_to_payload(array $payload, array $policy): array +{ + $withMemory = !empty($policy['with_memory']); + $llmCache = is_array($policy['llm_cache'] ?? null) ? $policy['llm_cache'] : ['enabled' => false]; + + $payload['with_memory'] = $withMemory; + $payload['llm_cache'] = $llmCache; + + $graphOptions = is_array($payload['graph_options'] ?? null) ? $payload['graph_options'] : []; + $graphOptions['with_memory'] = $withMemory; + $graphOptions['llm_cache'] = $llmCache; + $payload['graph_options'] = $graphOptions; + + return $payload; +} + +function king_openai_router_memory_log_fields(array $payload): array +{ + $graphOptions = is_array($payload['graph_options'] ?? null) ? $payload['graph_options'] : []; + $graphCache = is_array($graphOptions['llm_cache'] ?? null) ? $graphOptions['llm_cache'] : []; + + return [ + 'prepared_with_memory' => king_openai_router_payload_bool($payload, 'with_memory') === true, + 'prepared_llm_cache_enabled' => king_openai_router_payload_llm_cache_enabled($payload) === true, + 'prepared_graph_with_memory' => king_openai_router_payload_bool($graphOptions, 'with_memory') === true, + 'prepared_graph_llm_cache_enabled' => king_openai_router_payload_llm_cache_enabled(['llm_cache' => $graphCache]) === true, + ]; +} diff --git a/infra/inference/openai-router-mini-language.php b/infra/inference/openai-router-mini-language.php new file mode 100644 index 000000000..e86d27d8b --- /dev/null +++ b/infra/inference/openai-router-mini-language.php @@ -0,0 +1,400 @@ + [ + 'input' => [ + 'subject' => 'string', + 'needle' => 'string', + 'case_sensitive' => 'bool', + ], + 'output' => [ + 'count' => 'int', + ], + 'pure' => true, + 'io' => 'none', + ], + 'string.literal_contains' => [ + 'input' => [ + 'subject' => 'string', + 'needle' => 'string', + 'case_sensitive' => 'bool', + ], + 'output' => [ + 'contains' => 'bool', + ], + 'pure' => true, + 'io' => 'none', + ], + 'math.evaluate_arithmetic' => [ + 'input' => [ + 'expression' => 'string', + ], + 'output' => [ + 'value' => 'int|float', + ], + 'pure' => true, + 'io' => 'none', + ], + ]; +} + +function king_openai_router_mini_language_last_user_text(array $payload): string +{ + $messages = is_array($payload['messages'] ?? null) ? $payload['messages'] : []; + for ($index = count($messages) - 1; $index >= 0; $index--) { + $message = $messages[$index] ?? null; + if (!is_array($message) || (($message['role'] ?? null) !== 'user')) { + continue; + } + $content = king_openai_router_message_content_text($message['content'] ?? ''); + if ($content !== '') { + return $content; + } + } + return ''; +} + +function king_openai_router_mini_language_program_from_payload(array $payload): ?array +{ + $text = king_openai_router_mini_language_last_user_text($payload); + if ($text === '') { + return null; + } + + return king_openai_router_mini_language_count_program($text) + ?? king_openai_router_mini_language_contains_program($text) + ?? king_openai_router_mini_language_arithmetic_program($text); +} + +function king_openai_router_mini_language_program_base(string $operation, array $input): array +{ + return [ + 'language' => 'king-mini-language', + 'version' => 1, + 'operation' => $operation, + 'input' => $input, + 'safety' => [ + 'pure' => true, + 'filesystem' => false, + 'cli' => false, + 'network' => false, + ], + ]; +} + +function king_openai_router_mini_language_count_program(string $text): ?array +{ + $normalized = trim(preg_replace('/\s+/u', ' ', $text) ?? $text); + if ($normalized === '') { + return null; + } + + $patterns = [ + '/\bhow many (?:letters?|characters?) ["\']?(\p{L}|\p{N})["\']? (?:are |are there )?in (?:the )?(?:word|string )?["\']?([\p{L}\p{N}_ -]{1,80})["\']?\??$/iu', + '/\bhow often (?:does|is) ["\']?(\p{L}|\p{N})["\']? (?:appear|occurs?|contained) in (?:the )?(?:word|string )?["\']?([\p{L}\p{N}_ -]{1,80})["\']?\??$/iu', + '/\bwie ?viele (?:buchstaben|zeichen) ["\']?(\p{L}|\p{N})["\']? (?:hat|sind in|kommen in) (?:dem |der |das |wort |string )?["\']?([\p{L}\p{N}_ -]{1,80})["\']?\??$/iu', + '/\bwie oft (?:kommt|ist) ["\']?(\p{L}|\p{N})["\']? in (?:dem |der |das |wort |string )?["\']?([\p{L}\p{N}_ -]{1,80})["\']? (?:vor|enthalten)\??$/iu', + '/\bho man ltter r ["\']?(\p{L}|\p{N})["\']? in da word ["\']?([\p{L}\p{N}_ -]{1,80})["\']?\??$/iu', + ]; + + foreach ($patterns as $pattern) { + if (preg_match($pattern, $normalized, $match) !== 1) { + continue; + } + + $needle = trim($match[1]); + $subject = trim($match[2], " \t\n\r\0\x0B\"'"); + if ($needle === '' || $subject === '') { + continue; + } + + return king_openai_router_mini_language_program_base('string.count_occurrences', [ + 'subject' => $subject, + 'needle' => $needle, + 'case_sensitive' => true, + ]); + } + + return null; +} + +function king_openai_router_mini_language_contains_program(string $text): ?array +{ + $normalized = trim(preg_replace('/\s+/u', ' ', $text) ?? $text); + if ($normalized === '') { + return null; + } + + $patterns = [ + '/\bdoes (?:the )?(?:word|string )?["\']?([\p{L}\p{N}_ -]{1,80})["\']? contain ["\']?([\p{L}\p{N}_ -]{1,32})["\']?\??$/iu', + '/\bis ["\']?([\p{L}\p{N}_ -]{1,32})["\']? contained in (?:the )?(?:word|string )?["\']?([\p{L}\p{N}_ -]{1,80})["\']?\??$/iu', + '/\benthaelt (?:das )?(?:wort|string )?["\']?([\p{L}\p{N}_ -]{1,80})["\']? ["\']?([\p{L}\p{N}_ -]{1,32})["\']?\??$/iu', + ]; + + foreach ($patterns as $index => $pattern) { + if (preg_match($pattern, $normalized, $match) !== 1) { + continue; + } + + $subject = trim($index === 1 ? $match[2] : $match[1], " \t\n\r\0\x0B\"'"); + $needle = trim($index === 1 ? $match[1] : $match[2], " \t\n\r\0\x0B\"'"); + if ($subject === '' || $needle === '') { + continue; + } + + return king_openai_router_mini_language_program_base('string.literal_contains', [ + 'subject' => $subject, + 'needle' => $needle, + 'case_sensitive' => true, + ]); + } + + return null; +} + +function king_openai_router_mini_language_arithmetic_program(string $text): ?array +{ + $normalized = trim(preg_replace('/\s+/u', ' ', $text) ?? $text); + if ($normalized === '') { + return null; + } + + $expression = null; + if (preg_match('/\b(?:calculate|compute|what is|was ist|rechne)\s+([0-9][0-9+\-*\/().\s]{0,79})\??$/iu', $normalized, $match) === 1) { + $expression = trim($match[1]); + } elseif (preg_match('/^[0-9+\-*\/().\s]{1,80}\??$/', $normalized) === 1 && preg_match('/[+\-*\/]/', $normalized) === 1) { + $expression = rtrim($normalized, " ?\t\n\r\0\x0B"); + } + + if (!is_string($expression) || $expression === '') { + return null; + } + if (king_openai_router_mini_math_evaluate($expression)['ok'] !== true) { + return null; + } + + return king_openai_router_mini_language_program_base('math.evaluate_arithmetic', [ + 'expression' => $expression, + ]); +} + +function king_openai_router_mini_language_execute(array $program): array +{ + if (($program['language'] ?? null) !== 'king-mini-language' || ($program['version'] ?? null) !== 1) { + return ['ok' => false, 'error' => 'invalid_program_header']; + } + + $operation = $program['operation'] ?? null; + $input = $program['input'] ?? null; + if (!is_string($operation) || !is_array($input)) { + return ['ok' => false, 'error' => 'invalid_program_shape']; + } + + return match ($operation) { + 'string.count_occurrences' => king_openai_router_mini_language_execute_count($program, $input), + 'string.literal_contains' => king_openai_router_mini_language_execute_contains($program, $input), + 'math.evaluate_arithmetic' => king_openai_router_mini_language_execute_arithmetic($program, $input), + default => ['ok' => false, 'error' => 'unsupported_operation'], + }; +} + +function king_openai_router_mini_language_execute_count(array $program, array $input): array +{ + if (!is_string($input['subject'] ?? null) || !is_string($input['needle'] ?? null) || !is_bool($input['case_sensitive'] ?? null)) { + return ['ok' => false, 'error' => 'invalid_count_input']; + } + + $count = king_openai_router_mini_count_literal($input['subject'], $input['needle'], $input['case_sensitive']); + return [ + 'ok' => true, + 'content' => (string) $count, + 'program' => $program, + 'result' => [ + 'type' => 'integer', + 'value' => $count, + ], + ]; +} + +function king_openai_router_mini_language_execute_contains(array $program, array $input): array +{ + if (!is_string($input['subject'] ?? null) || !is_string($input['needle'] ?? null) || !is_bool($input['case_sensitive'] ?? null)) { + return ['ok' => false, 'error' => 'invalid_contains_input']; + } + + $subject = $input['case_sensitive'] ? $input['subject'] : strtolower($input['subject']); + $needle = $input['case_sensitive'] ? $input['needle'] : strtolower($input['needle']); + $contains = $needle !== '' && str_contains($subject, $needle); + return [ + 'ok' => true, + 'content' => $contains ? 'true' : 'false', + 'program' => $program, + 'result' => [ + 'type' => 'boolean', + 'value' => $contains, + ], + ]; +} + +function king_openai_router_mini_language_execute_arithmetic(array $program, array $input): array +{ + if (!is_string($input['expression'] ?? null)) { + return ['ok' => false, 'error' => 'invalid_arithmetic_input']; + } + + $value = king_openai_router_mini_math_evaluate($input['expression']); + if ($value['ok'] !== true) { + return ['ok' => false, 'error' => $value['error'] ?? 'invalid_arithmetic']; + } + + $number = $value['value']; + $content = is_int($number) || floor((float) $number) === (float) $number + ? (string) (int) $number + : rtrim(rtrim(sprintf('%.12F', (float) $number), '0'), '.'); + return [ + 'ok' => true, + 'content' => $content, + 'program' => $program, + 'result' => [ + 'type' => is_int($number) || floor((float) $number) === (float) $number ? 'integer' : 'float', + 'value' => $number, + ], + ]; +} + +function king_openai_router_mini_language_result(array $payload): ?array +{ + $program = king_openai_router_mini_language_program_from_payload($payload); + if (!is_array($program)) { + return null; + } + + $result = king_openai_router_mini_language_execute($program); + return !empty($result['ok']) ? $result : null; +} + +function king_openai_router_mini_count_literal(string $subject, string $needle, bool $caseSensitive): int +{ + if ($needle === '') { + return 0; + } + + $pattern = '/' . preg_quote($needle, '/') . '/u'; + if (!$caseSensitive) { + $pattern .= 'i'; + } + $matched = preg_match_all($pattern, $subject); + return is_int($matched) ? $matched : 0; +} + +function king_openai_router_mini_math_evaluate(string $expression): array +{ + $expression = trim($expression); + if ($expression === '' || strlen($expression) > 80 || preg_match('/^[0-9+\-*\/().\s]+$/', $expression) !== 1) { + return ['ok' => false, 'error' => 'invalid_arithmetic_expression']; + } + + preg_match_all('/\d+(?:\.\d+)?|[()+\-*\/]/', $expression, $matches); + $tokens = $matches[0] ?? []; + if ($tokens === []) { + return ['ok' => false, 'error' => 'empty_arithmetic_expression']; + } + if (implode('', $tokens) !== preg_replace('/\s+/', '', $expression)) { + return ['ok' => false, 'error' => 'invalid_arithmetic_tokens']; + } + + $position = 0; + $result = king_openai_router_mini_math_parse_expression($tokens, $position); + if ($result['ok'] !== true) { + return $result; + } + if ($position !== count($tokens)) { + return ['ok' => false, 'error' => 'trailing_arithmetic_tokens']; + } + + return $result; +} + +function king_openai_router_mini_math_parse_expression(array $tokens, int &$position): array +{ + $left = king_openai_router_mini_math_parse_term($tokens, $position); + if ($left['ok'] !== true) { + return $left; + } + + while ($position < count($tokens) && in_array($tokens[$position], ['+', '-'], true)) { + $operator = $tokens[$position++]; + $right = king_openai_router_mini_math_parse_term($tokens, $position); + if ($right['ok'] !== true) { + return $right; + } + $left['value'] = $operator === '+' + ? $left['value'] + $right['value'] + : $left['value'] - $right['value']; + } + + return $left; +} + +function king_openai_router_mini_math_parse_term(array $tokens, int &$position): array +{ + $left = king_openai_router_mini_math_parse_factor($tokens, $position); + if ($left['ok'] !== true) { + return $left; + } + + while ($position < count($tokens) && in_array($tokens[$position], ['*', '/'], true)) { + $operator = $tokens[$position++]; + $right = king_openai_router_mini_math_parse_factor($tokens, $position); + if ($right['ok'] !== true) { + return $right; + } + if ($operator === '/' && (float) $right['value'] === 0.0) { + return ['ok' => false, 'error' => 'division_by_zero']; + } + $left['value'] = $operator === '*' + ? $left['value'] * $right['value'] + : $left['value'] / $right['value']; + } + + return $left; +} + +function king_openai_router_mini_math_parse_factor(array $tokens, int &$position): array +{ + if ($position >= count($tokens)) { + return ['ok' => false, 'error' => 'expected_arithmetic_factor']; + } + + $token = $tokens[$position++]; + if ($token === '+') { + return king_openai_router_mini_math_parse_factor($tokens, $position); + } + if ($token === '-') { + $value = king_openai_router_mini_math_parse_factor($tokens, $position); + if ($value['ok'] === true) { + $value['value'] = -$value['value']; + } + return $value; + } + if ($token === '(') { + $value = king_openai_router_mini_math_parse_expression($tokens, $position); + if ($value['ok'] !== true) { + return $value; + } + if (($tokens[$position] ?? null) !== ')') { + return ['ok' => false, 'error' => 'unclosed_arithmetic_group']; + } + $position++; + return $value; + } + if (is_numeric($token)) { + $value = str_contains($token, '.') ? (float) $token : (int) $token; + return ['ok' => true, 'value' => $value]; + } + + return ['ok' => false, 'error' => 'unexpected_arithmetic_token']; +} diff --git a/infra/inference/openai-router-models.php b/infra/inference/openai-router-models.php new file mode 100644 index 000000000..3e326d51d --- /dev/null +++ b/infra/inference/openai-router-models.php @@ -0,0 +1,75 @@ + $config) { + if (!is_string($id) && !is_int($id)) { + throw new RuntimeException('King runtime model registry returned an invalid model id.'); + } + if (!is_array($config)) { + throw new RuntimeException('King runtime model registry entry is not a model config array.'); + } + } + + return $configs; +} + +function king_openai_router_model_config_name(array $config, string $fallback): string +{ + foreach (['id', 'name'] as $key) { + if (is_string($config[$key] ?? null) && $config[$key] !== '') { + return $config[$key]; + } + } + return $fallback !== '' ? $fallback : 'king-runtime'; +} + +function king_openai_router_model_config_aliases(array $config): array +{ + $aliases = $config['aliases'] ?? []; + if (!is_array($aliases)) { + return []; + } + + $normalized = []; + foreach ($aliases as $alias) { + if (is_string($alias) && $alias !== '') { + $normalized[$alias] = true; + } + } + + return array_keys($normalized); +} + +function king_openai_router_load_model_registry(array $configs): array +{ + $models = []; + $aliases = []; + + foreach ($configs as $id => $config) { + $registryName = king_openai_router_model_config_name($config, (string) $id); + if (isset($models[$registryName])) { + throw new RuntimeException('Duplicate King model registry id: ' . $registryName); + } + + $models[$registryName] = king_inference_model_load($config); + foreach (king_openai_router_model_config_aliases($config) as $alias) { + if (isset($models[$alias]) || isset($aliases[$alias])) { + throw new RuntimeException('Duplicate King model registry alias: ' . $alias); + } + $aliases[$alias] = $registryName; + } + } + + if ($models === []) { + throw new RuntimeException('No King inference models were loaded from the runtime registry.'); + } + + return [$models, $aliases]; +} diff --git a/infra/inference/openai-router-prompt.php b/infra/inference/openai-router-prompt.php new file mode 100644 index 000000000..dfd1d6dfb --- /dev/null +++ b/infra/inference/openai-router-prompt.php @@ -0,0 +1,380 @@ + !isset($available[$name]) + )); + + $present = []; + foreach (king_openai_router_tool_field_keys() as $key) { + if (array_key_exists($key, $payload) && $payload[$key] !== null && $payload[$key] !== []) { + $present[] = $key; + } + } + + return [ + 'present_fields' => $present, + 'available_tool_names' => $availableNames, + 'forced_tool_names' => $forcedNames, + 'assistant_tool_call_names' => $assistantNames, + 'unknown_tool_names' => $unknown, + 'tool_schema_count' => count($tools), + 'legacy_function_count' => count($functions), + 'invalid_schema_count' => $invalidSchemaCount, + 'parallel_tool_calls_present' => array_key_exists('parallel_tool_calls', $payload), + 'context_only' => $present !== [] || $assistantNames !== [], + ]; +} + +function king_openai_router_tool_result_text(array $message): string +{ + $content = king_openai_router_message_content_text($message['content'] ?? ''); + $name = is_string($message['name'] ?? null) && $message['name'] !== '' + ? $message['name'] + : ''; + $toolCallId = is_string($message['tool_call_id'] ?? null) && $message['tool_call_id'] !== '' + ? $message['tool_call_id'] + : ''; + $header = 'Tool result context only. King has not executed a tool in this route.'; + if ($name !== '') { + $header .= "\nTool name: " . $name; + } + if ($toolCallId !== '') { + $header .= "\nTool call id: " . $toolCallId; + } + + return $content !== '' ? $header . "\n" . $content : $header; +} + +function king_openai_router_instruction_message(array $message): ?array +{ + $role = $message['role'] ?? null; + $content = king_openai_router_message_content_text($message['content'] ?? ''); + if ($content === '') { + return null; + } + if ($role === 'developer') { + $content = "Developer instruction:\n" . $content; + } + + return [ + 'role' => 'system', + 'content' => $content, + ]; +} + +function king_openai_router_normalize_prompt_message(mixed $message): ?array +{ + if (!is_array($message)) { + return null; + } + + $role = $message['role'] ?? null; + $role = is_string($role) ? strtolower(trim($role)) : ''; + if ($role === 'system' || $role === 'developer') { + return king_openai_router_instruction_message(['role' => $role, 'content' => $message['content'] ?? '']); + } + if ($role === 'assistant') { + $content = king_openai_router_message_content_text($message['content'] ?? ''); + if ($content === '') { + $content = king_openai_router_message_tool_calls_text($message); + } + return $content !== '' ? ['role' => 'assistant', 'content' => $content] : null; + } + if ($role === 'tool') { + return ['role' => 'user', 'content' => king_openai_router_tool_result_text($message)]; + } + + $content = king_openai_router_message_content_text($message['content'] ?? ''); + if ($role !== 'user') { + $content = $content !== '' + ? "Message with unsupported role `" . ($role !== '' ? $role : 'unknown') . "`:\n" . $content + : ''; + } + + return $content !== '' ? ['role' => 'user', 'content' => $content] : null; +} + +function king_openai_router_normalize_prompt_messages(array $payload): array +{ + $messages = $payload['messages'] ?? null; + if (!is_array($messages) || $messages === []) { + return $payload; + } + + $normalized = []; + foreach ($messages as $message) { + $entry = king_openai_router_normalize_prompt_message($message); + if ($entry !== null) { + $normalized[] = $entry; + } + } + if ($normalized !== []) { + $payload['messages'] = $normalized; + } + + return $payload; +} + +function king_openai_router_tool_context_text(array $toolStatus): string +{ + if (empty($toolStatus['context_only'])) { + return ''; + } + + $parts = [ + 'OpenAI tool/function fields are present as context only.', + 'King has not executed any tool call in this route.', + ]; + $available = $toolStatus['available_tool_names'] ?? []; + if (is_array($available) && $available !== []) { + $parts[] = 'Configured tool names: ' . implode(', ', $available) . '.'; + } + $forced = $toolStatus['forced_tool_names'] ?? []; + if (is_array($forced) && $forced !== []) { + $parts[] = 'Requested tool names: ' . implode(', ', $forced) . '.'; + } + $unknown = $toolStatus['unknown_tool_names'] ?? []; + if (is_array($unknown) && $unknown !== []) { + $parts[] = 'Unknown requested tool names: ' . implode(', ', $unknown) . '.'; + } + + return implode("\n", $parts); +} + +function king_openai_router_insert_instruction_message(array $payload, array $message): array +{ + $messages = $payload['messages'] ?? null; + if (!is_array($messages) || $messages === []) { + return $payload; + } + + $insertAt = 0; + foreach ($messages as $index => $entry) { + if (!king_openai_router_is_instruction_message($entry)) { + break; + } + $insertAt = $index + 1; + } + array_splice($messages, $insertAt, 0, [$message]); + $payload['messages'] = $messages; + return $payload; +} + +function king_openai_router_apply_tool_context(array $payload, array $toolStatus): array +{ + $text = king_openai_router_tool_context_text($toolStatus); + if ($text === '') { + return $payload; + } + + return king_openai_router_insert_instruction_message($payload, [ + 'role' => 'system', + 'content' => $text, + ]); +} + +function king_openai_router_strip_tool_execution_fields(array $payload): array +{ + foreach (king_openai_router_tool_field_keys() as $key) { + unset($payload[$key]); + } + return $payload; +} + +function king_openai_router_apply_context_policy(array $payload, string $contextPolicy): array +{ + if ($contextPolicy !== 'last_user') { + return $payload; + } + + $messages = $payload['messages'] ?? null; + if (!is_array($messages) || $messages === []) { + return $payload; + } + + $instructionMessages = []; + foreach ($messages as $message) { + if (king_openai_router_is_instruction_message($message) + && king_openai_router_message_content_text($message['content'] ?? '') !== '' + ) { + $instructionMessages[] = $message; + } + } + + for ($index = count($messages) - 1; $index >= 0; $index--) { + $message = $messages[$index] ?? null; + if (!is_array($message) || (($message['role'] ?? null) !== 'user')) { + continue; + } + $content = king_openai_router_message_content_text($message['content'] ?? ''); + if ($content === '') { + continue; + } + $payload['messages'] = [ + ...$instructionMessages, + [ + 'role' => 'user', + 'content' => $content, + ], + ]; + return $payload; + } + + return $payload; +} + +function king_openai_router_ensure_default_system_instruction(array $payload, string $defaultSystemPrompt): array +{ + if (trim($defaultSystemPrompt) === '') { + return $payload; + } + + $messages = $payload['messages'] ?? null; + if (!is_array($messages) || $messages === []) { + return $payload; + } + + foreach ($messages as $message) { + if (king_openai_router_is_instruction_message($message) + && king_openai_router_message_content_text($message['content'] ?? '') !== '' + ) { + return $payload; + } + } + + array_unshift($messages, [ + 'role' => 'system', + 'content' => $defaultSystemPrompt, + ]); + $payload['messages'] = $messages; + return $payload; +} + +function king_openai_router_assemble_chat_messages( + array $payload, + string $contextPolicy, + string $defaultSystemPrompt, + bool $coderInstructionWrapper +): array { + $toolStatus = king_openai_router_tool_status($payload); + $payload = king_openai_router_normalize_prompt_messages($payload); + $payload = king_openai_router_apply_tool_context($payload, $toolStatus); + $payload = king_openai_router_strip_tool_execution_fields($payload); + $payload = king_openai_router_apply_context_policy($payload, $contextPolicy); + $payload = king_openai_router_ensure_default_system_instruction($payload, $defaultSystemPrompt); + return king_openai_router_apply_coder_instruction_wrapper($payload, $coderInstructionWrapper); +} diff --git a/infra/inference/openai-router-stream.php b/infra/inference/openai-router-stream.php new file mode 100644 index 000000000..1b3bcafcc --- /dev/null +++ b/infra/inference/openai-router-stream.php @@ -0,0 +1,420 @@ +getMetrics(); + } catch (Throwable) { + return []; + } + return is_array($metrics) ? $metrics : []; +} + +function king_openai_router_timing_payload( + int $startedNs, + int $nowNs, + int $eventIndex, + int $generatedTokenChunks, + ?int $firstTokenNs, + ?int $previousTokenNs, + ?int $previousEventNs, + bool $buffered, + bool $terminal, + ?string $finishReason, + array $streamMetrics +): array { + $payload = [ + 'schema_version' => 1, + 'transport' => 'http1_chunked_body_stream', + 'flush_contract' => 'one_sse_event_per_body_stream_callback', + 'body_stream_callback' => true, + 'buffered' => $buffered, + 'event_index' => $eventIndex, + 'generated_token_chunks' => $generatedTokenChunks, + 'request_elapsed_ms' => king_openai_router_elapsed_ms($startedNs, $nowNs), + 'event_delta_ms' => $previousEventNs !== null ? round(($nowNs - $previousEventNs) / 1_000_000, 3) : null, + 'first_token_elapsed_ms' => $firstTokenNs !== null ? king_openai_router_elapsed_ms($startedNs, $firstTokenNs) : null, + 'token_delta_ms' => $previousTokenNs !== null ? round(($nowNs - $previousTokenNs) / 1_000_000, 3) : null, + 'terminal' => $terminal, + 'finish_reason' => $finishReason, + ]; + + foreach ([ + 'chunks', + 'bytes', + 'native_decoder_tokens', + 'native_decoder_last_token_id', + 'gpu_thermal_aborted', + 'gpu_power_aborted', + ] as $key) { + if (array_key_exists($key, $streamMetrics)) { + $payload[$key] = $streamMetrics[$key]; + } + } + + return $payload; +} + +function king_openai_router_attach_stream_timing(array $event, array $timing): array +{ + $king = $event['x_king'] ?? []; + if (!is_array($king)) { + $king = []; + } + $king['streaming'] = $timing; + $event['x_king'] = $king; + return $event; +} + +function king_openai_router_model_stream_status(mixed $model, bool $allowBufferedNativeStream): array +{ + $info = []; + if (is_object($model) && function_exists('king_inference_model_info')) { + try { + $info = king_inference_model_info($model); + } catch (Throwable) { + $info = []; + } + } + $backend = is_string($info['backend'] ?? null) ? $info['backend'] : 'unknown'; + $capabilities = is_array($info['backend_capabilities'] ?? null) ? $info['backend_capabilities'] : []; + $immediateGpuStream = $backend === 'king_native_gpu' && !empty($capabilities['gpu_prompt_decoder_loop']); + $nativeBuffered = in_array($backend, ['king_native_cpu', 'king_native_gpu'], true) && !$immediateGpuStream; + + return [ + 'accepted' => !$nativeBuffered || $allowBufferedNativeStream, + 'backend' => $backend, + 'native_buffered' => $nativeBuffered, + 'immediate_token_streaming' => $immediateGpuStream, + 'reason' => $nativeBuffered + ? 'buffered_native_stream_requires_explicit_opt_in' + : 'stream_backend_accepted', + ]; +} + +function king_openai_router_attach_stream_backend_status(array $event, array $status): array +{ + $king = $event['x_king'] ?? []; + if (!is_array($king)) { + $king = []; + } + $king['stream_backend'] = $status; + $event['x_king'] = $king; + return $event; +} + +function king_openai_router_typed_stream_end_response( + string $responseId, + int $created, + string $responseModel, + int $startedNs, + array $status +): array { + $nowNs = hrtime(true); + $message = 'King accepted this OpenAI-compatible streaming request, but the selected model exposes a buffered native generation path instead of an immediate token stream. The router ended the stream without blocking the client.'; + $initial = king_openai_router_attach_stream_timing( + king_openai_router_initial_chunk($responseId, $created, $responseModel), + king_openai_router_timing_payload($startedNs, $nowNs, 1, 0, null, null, null, false, false, null, []) + ); + $content = king_openai_router_attach_stream_timing( + king_openai_router_content_chunk($responseId, $created, $responseModel, $message), + king_openai_router_timing_payload($startedNs, $nowNs, 2, 1, $nowNs, null, $nowNs, false, false, null, []) + ); + $terminal = king_openai_router_attach_stream_timing( + king_openai_router_terminal_chunk($responseId, $created, $responseModel), + king_openai_router_timing_payload($startedNs, $nowNs, 3, 1, $nowNs, $nowNs, $nowNs, false, true, 'stop', []) + ); + $initial = king_openai_router_attach_stream_backend_status($initial, $status); + $content = king_openai_router_attach_stream_backend_status($content, $status); + $terminal = king_openai_router_attach_stream_backend_status($terminal, $status); + + return [ + 'status' => 200, + 'headers' => [ + 'content-type' => 'text/event-stream', + 'cache-control' => 'no-cache', + 'x-accel-buffering' => 'no', + 'x-king-openai-router-path' => 'typed_stream_end', + 'x-king-openai-stream-api' => 'capability_preflight', + 'x-king-openai-stream-backend' => (string) ($status['reason'] ?? 'stream_backend_rejected'), + 'x-king-openai-tool-fields' => 'accepted_context_only', + ], + 'body' => king_openai_router_sse($initial) + . king_openai_router_sse($content) + . king_openai_router_sse($terminal) + . "data: [DONE]\n\n", + ]; +} + +function king_openai_router_stream_response( + array $models, + array $payload, + array $options, + array $request, + int $startedNs +): ?array { + $requestedModel = $payload['model'] ?? null; + if (is_string($requestedModel) && $requestedModel !== '') { + if (!array_key_exists($requestedModel, $models)) { + return null; + } + $model = $models[$requestedModel]; + } else { + $model = reset($models); + if ($model === false) { + return null; + } + } + + $streamPayload = $payload; + $responseModel = is_string($streamPayload['model'] ?? null) && $streamPayload['model'] !== '' + ? $streamPayload['model'] + : (is_string($requestedModel) && $requestedModel !== '' ? $requestedModel : 'king-local'); + $responseId = king_openai_router_stream_id(); + $created = time(); + $allowBufferedNativeStream = isset($options['allow_buffered_native_stream']) && is_bool($options['allow_buffered_native_stream']) + ? $options['allow_buffered_native_stream'] + : false; + $streamStatus = king_openai_router_model_stream_status($model, $allowBufferedNativeStream); + if (empty($streamStatus['accepted'])) { + return king_openai_router_typed_stream_end_response( + $responseId, + $created, + $responseModel, + $startedNs, + $streamStatus + ); + } + $streamOptions = [ + 'openai_compatible' => true, + 'format' => 'openai_chat_completions', + ]; + $readTimeoutMs = isset($options['read_timeout_ms']) && is_int($options['read_timeout_ms']) + ? max(0, $options['read_timeout_ms']) + : 250; + $maxEvents = isset($options['max_events']) && is_int($options['max_events']) + ? max(1, $options['max_events']) + : 4096; + + $stream = null; + $done = false; + $sentInitial = false; + $logged = false; + $events = 0; + $generatedTokenChunks = 0; + $firstTokenNs = null; + $previousTokenNs = null; + $previousEventNs = null; + $plainArtifactMode = king_openai_router_plain_artifact_requested($payload); + $plainArtifactContent = ''; + + return [ + 'status' => 200, + 'headers' => [ + 'content-type' => 'text/event-stream', + 'cache-control' => 'no-cache', + 'x-accel-buffering' => 'no', + 'x-king-openai-router-path' => 'php_body_stream', + 'x-king-openai-stream-api' => 'king_inference_openai_chat_stream', + 'x-king-openai-compat-drain' => 'false', + 'x-king-openai-stream-buffered' => $plainArtifactMode ? 'artifact-only' : 'false', + 'x-king-openai-stream-flush' => 'http1_chunked_body_stream', + 'x-king-openai-tool-fields' => 'accepted_context_only', + 'x-king-openai-plain-artifact' => $plainArtifactMode ? 'true' : 'false', + ], + 'body_stream' => static function () use ($model, $streamPayload, $streamOptions, $readTimeoutMs, $maxEvents, $responseId, $created, $responseModel, $request, $models, $startedNs, $plainArtifactMode, &$plainArtifactContent, &$stream, &$done, &$sentInitial, &$logged, &$events, &$generatedTokenChunks, &$firstTokenNs, &$previousTokenNs, &$previousEventNs): ?string { + if ($done) { + return null; + } + if (!$sentInitial) { + $sentInitial = true; + $events++; + $nowNs = hrtime(true); + $event = king_openai_router_initial_chunk($responseId, $created, $responseModel); + $event = king_openai_router_attach_stream_timing( + $event, + king_openai_router_timing_payload( + $startedNs, + $nowNs, + $events, + $generatedTokenChunks, + $firstTokenNs, + $previousTokenNs, + $previousEventNs, + false, + false, + null, + [] + ) + ); + $previousEventNs = $nowNs; + return king_openai_router_sse($event); + } + if ($events >= $maxEvents) { + $done = true; + if (!$logged) { + $logged = true; + king_inference_runtime_log_request_completed($request, $models, $startedNs, ['status' => 200]); + } + return "event: error\ndata: {\"message\":\"King inference stream exceeded the configured router event limit.\"}\n\n" + . "data: [DONE]\n\n"; + } + + try { + if ($stream === null) { + $stream = king_inference_openai_chat_stream($model, $streamPayload, $streamOptions); + } + + $event = king_inference_next($stream, $readTimeoutMs); + } catch (Throwable $e) { + $done = true; + if (!$logged) { + $logged = true; + king_inference_runtime_log_request_completed($request, $models, $startedNs, ['status' => 200]); + } + return "event: error\ndata: " . json_encode(['message' => $e->getMessage()], JSON_UNESCAPED_SLASHES) . "\n\n" + . "data: [DONE]\n\n"; + } + if ($event === null) { + return ": king-keepalive\n\n"; + } + if (!is_array($event)) { + $done = true; + return "event: error\ndata: {\"message\":\"King inference stream produced an invalid event.\"}\n\n" + . "data: [DONE]\n\n"; + } + + $event = king_openai_router_normalize_chunk($event, $responseId, $created, $responseModel); + if (king_openai_router_role_only_chunk($event)) { + return ": king-role-ack\n\n"; + } + + $delta = king_openai_router_event_delta_content($event); + $terminal = king_openai_router_stream_terminal($event); + $finishReason = king_openai_router_stream_finish_reason($event); + $nowNs = hrtime(true); + $metrics = king_openai_router_stream_metrics($stream); + if ($delta !== '' && !$plainArtifactMode) { + if ($firstTokenNs === null) { + $firstTokenNs = $nowNs; + } + $generatedTokenChunks++; + } + + if ($plainArtifactMode) { + $plainArtifactContent .= $delta; + if (!$terminal) { + $previousEventNs = $nowNs; + return ": king-artifact-buffer\n\n"; + } + + $done = true; + if (!$logged) { + $logged = true; + king_inference_runtime_log_request_completed($request, $models, $startedNs, ['status' => 200]); + } + + $cleaned = king_openai_router_strip_artifact_markdown_fence($plainArtifactContent); + $event = king_openai_router_clear_event_delta_content($event); + $chunk = ''; + if ($cleaned !== '') { + $events++; + $generatedTokenChunks++; + if ($firstTokenNs === null) { + $firstTokenNs = $nowNs; + } + $contentEvent = king_openai_router_content_chunk($responseId, $created, $responseModel, $cleaned); + $contentEvent = king_openai_router_attach_stream_timing( + $contentEvent, + king_openai_router_timing_payload( + $startedNs, + $nowNs, + $events, + $generatedTokenChunks, + $firstTokenNs, + $previousTokenNs, + $previousEventNs, + true, + false, + null, + $metrics + ) + ); + $previousTokenNs = $nowNs; + $chunk .= king_openai_router_sse($contentEvent); + } + $events++; + $event = king_openai_router_attach_stream_timing( + $event, + king_openai_router_timing_payload( + $startedNs, + $nowNs, + $events, + $generatedTokenChunks, + $firstTokenNs, + $previousTokenNs, + $previousEventNs, + true, + true, + $finishReason ?? 'stop', + $metrics + ) + ); + $chunk .= king_openai_router_sse($event); + $chunk .= "data: [DONE]\n\n"; + $previousEventNs = $nowNs; + return $chunk; + } + + $events++; + $event = king_openai_router_attach_stream_timing( + $event, + king_openai_router_timing_payload( + $startedNs, + $nowNs, + $events, + $generatedTokenChunks, + $firstTokenNs, + $delta !== '' ? $previousTokenNs : null, + $previousEventNs, + false, + $terminal, + $finishReason, + $metrics + ) + ); + $chunk = king_openai_router_sse($event); + if ($delta !== '') { + $previousTokenNs = $nowNs; + } + $previousEventNs = $nowNs; + if ($terminal) { + $done = true; + if (!$logged) { + $logged = true; + king_inference_runtime_log_request_completed($request, $models, $startedNs, ['status' => 200]); + } + $chunk .= "data: [DONE]\n\n"; + } + return $chunk; + }, + ]; +} diff --git a/infra/inference/openai-router-tools.php b/infra/inference/openai-router-tools.php new file mode 100644 index 000000000..53b1c052e --- /dev/null +++ b/infra/inference/openai-router-tools.php @@ -0,0 +1,166 @@ + [ + 'kind' => 'king_inference_mini_language', + 'operations' => array_keys(king_openai_router_mini_language_specs()), + 'io' => 'none', + 'safe' => true, + 'pure' => true, + 'filesystem' => false, + 'cli' => false, + 'network' => false, + 'timeout_category' => 'timeout', + 'error_category' => 'runtime', + ], + 'king.inference.count_occurrences' => [ + 'kind' => 'king_inference_mini_op', + 'operation' => 'count_occurrences', + 'io' => 'none', + 'safe' => true, + 'timeout_category' => 'timeout', + 'error_category' => 'runtime', + ], + ]; + return $tools; +} + +function king_openai_router_mcp_tools_enabled(array $routerOptions): bool +{ + return !array_key_exists('internal_mcp_tools_enabled', $routerOptions) + || $routerOptions['internal_mcp_tools_enabled'] === true; +} + +function king_openai_router_mcp_tool_timeout_ms(array $routerOptions): int +{ + $timeout = $routerOptions['internal_mcp_tool_timeout_ms'] ?? 100; + return is_int($timeout) ? max(1, min($timeout, 5000)) : 100; +} + +function king_openai_router_mcp_registered_tool_names(): array +{ + if (!function_exists('king_system_get_component_info')) { + return []; + } + + try { + $info = king_system_get_component_info('pipeline_orchestrator'); + } catch (Throwable) { + return []; + } + $registered = $info['configuration']['registered_tools'] ?? []; + if (!is_array($registered)) { + return []; + } + + $names = []; + foreach ($registered as $name) { + if (is_string($name) && $name !== '') { + $names[$name] = true; + } + } + $result = array_keys($names); + sort($result); + return $result; +} + +function king_openai_router_mcp_register_internal_tools(): array +{ + static $registration = null; + if (is_array($registration)) { + return $registration; + } + + $registered = []; + $errors = []; + if (!function_exists('king_pipeline_orchestrator_register_tool')) { + return $registration = [ + 'registered' => [], + 'registry_names' => king_openai_router_mcp_registered_tool_names(), + 'errors' => ['king_pipeline_orchestrator_register_tool unavailable'], + ]; + } + + foreach (king_openai_router_internal_tool_specs() as $name => $config) { + try { + $ok = king_pipeline_orchestrator_register_tool($name, $config); + if ($ok === true) { + $registered[] = $name; + } + } catch (Throwable $e) { + $errors[] = $name . ':' . $e::class; + } + } + + return $registration = [ + 'registered' => $registered, + 'registry_names' => king_openai_router_mcp_registered_tool_names(), + 'errors' => $errors, + ]; +} + +function king_openai_router_mcp_tool_bridge_result(array $payload, array $routerOptions): array +{ + $startedNs = hrtime(true); + $toolName = 'king.inference.structured_mini_language'; + $miniLanguageResult = king_openai_router_mini_language_result($payload); + $result = [ + 'executed' => false, + 'content' => null, + 'tool_name' => $toolName, + 'status' => 'not_applicable', + 'error_category' => 'none', + 'duration_ms' => 0.0, + 'registry_names' => [], + 'registered_now' => [], + 'registry_errors' => [], + ]; + + if (!is_array($miniLanguageResult)) { + return $result; + } + if (!king_openai_router_mcp_tools_enabled($routerOptions)) { + $result['status'] = 'disabled'; + $result['error_category'] = 'policy'; + return $result; + } + + $registry = king_openai_router_mcp_register_internal_tools(); + $result['registry_names'] = is_array($registry['registry_names'] ?? null) ? $registry['registry_names'] : []; + $result['registered_now'] = is_array($registry['registered'] ?? null) ? $registry['registered'] : []; + $result['registry_errors'] = is_array($registry['errors'] ?? null) ? $registry['errors'] : []; + if (!in_array($toolName, $result['registry_names'], true)) { + $result['status'] = 'not_registered'; + $result['error_category'] = 'registry'; + return $result; + } + + $content = is_array($miniLanguageResult) && is_string($miniLanguageResult['content'] ?? null) + ? $miniLanguageResult['content'] + : null; + + $durationMs = (hrtime(true) - $startedNs) / 1_000_000; + $result['duration_ms'] = round($durationMs, 3); + if ($durationMs > king_openai_router_mcp_tool_timeout_ms($routerOptions)) { + $result['status'] = 'timeout'; + $result['error_category'] = 'timeout'; + return $result; + } + if (!is_string($content) || $content === '') { + $result['status'] = 'no_result'; + $result['error_category'] = 'validation'; + return $result; + } + + $result['executed'] = true; + $result['content'] = $content; + $result['program'] = $miniLanguageResult['program'] ?? null; + $result['typed_result'] = $miniLanguageResult['result'] ?? null; + $result['status'] = 'executed'; + return $result; +} diff --git a/infra/inference/openai-router.php b/infra/inference/openai-router.php new file mode 100644 index 000000000..6e3a6b691 --- /dev/null +++ b/infra/inference/openai-router.php @@ -0,0 +1,733 @@ + 65535) { + fwrite(STDERR, "KING_OPENAI_PORT must be between 1 and 65535.\n"); + exit(1); +} +$contextPolicy = getenv('KING_OPENAI_CONTEXT_POLICY') ?: 'full'; +if (!in_array($contextPolicy, ['full', 'last_user'], true)) { + $contextPolicy = 'full'; +} +$defaultSystemPrompt = getenv('KING_OPENAI_DEFAULT_SYSTEM_PROMPT'); +if (!is_string($defaultSystemPrompt) || trim($defaultSystemPrompt) === '') { + $defaultSystemPrompt = ''; +} +$defaultMaxTokens = max(1, (int) (getenv('KING_OPENAI_DEFAULT_MAX_TOKENS') ?: 128)); +$maxCompletionTokens = max(1, (int) (getenv('KING_OPENAI_MAX_COMPLETION_TOKENS') ?: 512)); +$coderInstructionWrapper = king_openai_router_env_bool('KING_OPENAI_CODER_INSTRUCTION_WRAPPER', true); +$internalMcpToolsEnabled = king_openai_router_env_bool('KING_OPENAI_INTERNAL_MCP_TOOLS', true); +$internalMcpToolTimeoutMs = max(1, min((int) (getenv('KING_OPENAI_INTERNAL_MCP_TOOL_TIMEOUT_MS') ?: 100), 5000)); +$allowBufferedNativeStream = king_openai_router_env_bool('KING_OPENAI_ALLOW_BUFFERED_NATIVE_STREAM', false); + +function king_openai_router_string(array $source, string $key, string $fallback = ''): string +{ + return is_string($source[$key] ?? null) && $source[$key] !== '' ? $source[$key] : $fallback; +} +function king_openai_router_int(array $source, string $key, int $fallback = 0): int +{ + return is_int($source[$key] ?? null) ? $source[$key] : $fallback; +} +function king_openai_router_bool(array $source, string $key, bool $fallback = false): bool +{ + return is_bool($source[$key] ?? null) ? $source[$key] : $fallback; +} +function king_openai_router_ini_bool(string $key, bool $fallback = false): bool +{ + $value = ini_get($key); + if ($value === false || trim((string) $value) === '') { + return $fallback; + } + return in_array(strtolower(trim((string) $value)), ['1', 'on', 'yes', 'true'], true); +} +function king_openai_router_array(array $source, string $key): array +{ + return is_array($source[$key] ?? null) ? $source[$key] : []; +} +function king_openai_router_artifact_path(array $config): string +{ + $artifact = king_openai_router_array($config, 'artifact'); + return king_openai_router_string($artifact, 'path', king_openai_router_string($config, 'artifact_path')); +} + +$runtimeModelConfigs = king_openai_router_model_configs(); +$runtimeModelConfig = reset($runtimeModelConfigs); +if (!is_array($runtimeModelConfig)) { + fwrite(STDERR, "King runtime model registry did not provide a primary model config.\n"); + exit(1); +} +$modelName = king_openai_router_string($runtimeModelConfig, 'name', 'king-runtime'); +$backend = king_openai_router_string($runtimeModelConfig, 'backend', 'unknown'); +$runtimeProfile = king_openai_router_string($runtimeModelConfig, 'runtime_profile', 'unknown'); +$requestedProfile = king_openai_router_string($runtimeModelConfig, 'runtime_requested_profile', $runtimeProfile); +$artifactPath = king_openai_router_artifact_path($runtimeModelConfig); +$memoryPolicy = king_openai_router_memory_policy($runtimeModelConfig); +$withMemory = !empty($memoryPolicy['with_memory']); +$contextTokens = king_openai_router_int($runtimeModelConfig, 'context_tokens', 2048); +$kvCache = king_openai_router_array($runtimeModelConfig, 'kv_cache'); +$kvPageTokens = king_openai_router_int($kvCache, 'page_tokens', 16); +$kvElementBytes = king_openai_router_int($kvCache, 'element_bytes', 2); +$llmCacheEnabled = !empty($memoryPolicy['llm_cache_active']); +$gpuConfig = king_openai_router_array($runtimeModelConfig, 'gpu'); +$gpuThermal = king_openai_router_array($gpuConfig, 'thermal'); +$gpuRuntime = king_openai_router_array($runtimeModelConfig, 'gpu_runtime'); +$gpuEnabled = $backend === 'king_native_gpu' && king_openai_router_bool($gpuConfig, 'enabled'); +$gpuLayers = king_openai_router_int($gpuConfig, 'max_gpu_layers'); +$gpuVramReserveMb = king_openai_router_int($gpuConfig, 'vram_reserve_mb'); +$gpuMinFreeVramMb = king_openai_router_int($gpuConfig, 'min_free_vram_mb'); +$gpuSensorPath = king_openai_router_string($gpuThermal, 'sensor_path'); +$gpuSensorCommand = king_openai_router_string($gpuThermal, 'sensor_command'); +$listenerOverrideAllowed = king_openai_router_ini_bool('king.security_allow_config_override'); +$listenerConfig = $listenerOverrideAllowed ? [ + 'tcp_connect_timeout_ms' => 1000, + 'tcp.persistent_listener' => true, +] : []; + +king_inference_runtime_log_configured([ + 'host' => $host, + 'port' => $port, + 'listener_config_override_allowed' => $listenerOverrideAllowed, + 'listener_persistent' => $listenerOverrideAllowed, + 'requested_profile' => $requestedProfile, + 'selected_profile' => $runtimeProfile, + 'backend' => $backend, + 'model' => $modelName, + 'artifact' => $artifactPath, + 'artifact_readable' => is_file($artifactPath) && is_readable($artifactPath), + 'gpu_profile_available' => king_openai_router_bool($runtimeModelConfig, 'runtime_gpu_profile_available'), + 'gpu_enabled' => $gpuEnabled, + 'gpu_layers' => $gpuLayers, + 'gpu_vram_reserve_mb' => $gpuVramReserveMb, + 'gpu_min_free_vram_mb' => $gpuMinFreeVramMb, + 'context_tokens' => $contextTokens, + 'kv_page_tokens' => $kvPageTokens, + 'kv_element_bytes' => $kvElementBytes, + 'with_memory' => $withMemory, + 'llm_cache_enable' => $llmCacheEnabled, + 'memory_mode' => $memoryPolicy['mode'], + 'llm_cache_configured' => !empty($memoryPolicy['llm_cache_configured']), + 'llm_cache_status_active' => !empty($memoryPolicy['llm_cache_status']['active']), + 'llm_cache_status_ok' => !empty($memoryPolicy['llm_cache_status']['ok']), + 'llm_cache_status_reason' => is_string($memoryPolicy['llm_cache_status']['reason'] ?? null) ? $memoryPolicy['llm_cache_status']['reason'] : '', + ...king_openai_router_memory_status_log_fields($memoryPolicy), + 'context_policy' => $contextPolicy, + 'default_max_tokens' => $defaultMaxTokens, + 'max_completion_tokens' => $maxCompletionTokens, + 'coder_instruction_wrapper' => $coderInstructionWrapper, + 'internal_mcp_tools_enabled' => $internalMcpToolsEnabled, + 'internal_mcp_tool_timeout_ms' => $internalMcpToolTimeoutMs, + 'allow_buffered_native_stream' => $allowBufferedNativeStream, + 'model_registry_count' => count($runtimeModelConfigs), + 'model_registry_ids' => array_keys($runtimeModelConfigs), + 'gpu_thermal_source' => $gpuSensorPath !== '' ? $gpuSensorPath : $gpuSensorCommand, + 'gpu_thermal_check_interval_sec' => king_openai_router_int($gpuThermal, 'check_interval_seconds'), + 'gpu_generation_ready' => king_openai_router_bool($gpuRuntime, 'generation_ready'), + 'gpu_admission_reason' => king_openai_router_string($gpuRuntime, 'reason'), +]); + +foreach ($runtimeModelConfigs as $registryId => $registryConfig) { + if (!is_array($registryConfig)) { + fwrite(STDERR, "Configured model registry entry is invalid: {$registryId}\n"); + exit(1); + } + $registryArtifactPath = king_openai_router_artifact_path($registryConfig); + if ($registryArtifactPath === '' || !is_file($registryArtifactPath) || !is_readable($registryArtifactPath)) { + fwrite(STDERR, "Configured model artifact is not readable for {$registryId}: {$registryArtifactPath}\n"); + exit(1); + } +} + +[$models, $modelAliases] = king_openai_router_load_model_registry($runtimeModelConfigs); +foreach ($models as $registryName => $model) { + king_inference_runtime_log_model_admitted($registryName, $model); +} + +fwrite(STDERR, "King OpenAI router listening on http://{$host}:{$port}/v1\n"); +fwrite(STDERR, "Configured models: " . implode(',', array_keys($models)) . "\n"); +fwrite(STDERR, "Configured backend: {$backend}\n"); +fwrite(STDERR, "Configured artifact: {$artifactPath}\n"); +if ($gpuEnabled) { + fwrite(STDERR, "GPU layers: {$gpuLayers}\n"); + fwrite(STDERR, "GPU VRAM reserve: {$gpuVramReserveMb} MB\n"); + fwrite(STDERR, "GPU minimum free VRAM: {$gpuMinFreeVramMb} MB\n"); + fwrite(STDERR, 'GPU thermal source: ' . ($gpuSensorPath !== '' ? $gpuSensorPath : $gpuSensorCommand) . "\n"); +} + +function king_openai_router_path_is(array $request, string $path): bool +{ + $requestPath = $request['path'] ?? $request['uri'] ?? ''; + if (!is_string($requestPath) || $requestPath === '') { + return false; + } + $queryOffset = strpos($requestPath, '?'); + if ($queryOffset !== false) { + $requestPath = substr($requestPath, 0, $queryOffset); + } + return $requestPath === $path; +} + +function king_openai_router_decode_chat_payload(array $request): ?array +{ + if (strcasecmp((string) ($request['method'] ?? ''), 'POST') !== 0) { + return null; + } + if (!king_openai_router_path_is($request, '/v1/chat/completions')) { + return null; + } + $body = $request['body'] ?? null; + if (!is_string($body) || $body === '') { + return null; + } + try { + $payload = json_decode($body, true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException) { + return null; + } + if (!is_array($payload)) { + return null; + } + return $payload; +} + +function king_openai_router_int_payload_value(array $payload, string $key): ?int +{ + $value = $payload[$key] ?? null; + if (is_int($value)) { + return $value; + } + if (is_float($value)) { + return (int) $value; + } + if (is_string($value) && preg_match('/^\d+$/', $value) === 1) { + return (int) $value; + } + return null; +} + +function king_openai_router_prepare_chat_payload( + array $payload, + string $contextPolicy, + string $defaultSystemPrompt, + bool $coderInstructionWrapper, + array $modelAliases, + int $defaultMaxTokens, + int $maxCompletionTokens, + array $memoryPolicy +): array +{ + $prepared = king_openai_router_assemble_chat_messages( + $payload, + $contextPolicy, + $defaultSystemPrompt, + $coderInstructionWrapper + ); + $prepared = king_openai_router_apply_memory_policy_to_payload($prepared, $memoryPolicy); + $requestedMaxTokens = king_openai_router_int_payload_value($prepared, 'max_tokens') + ?? king_openai_router_int_payload_value($prepared, 'max_completion_tokens') + ?? $defaultMaxTokens; + $prepared['max_tokens'] = max(1, min($requestedMaxTokens, $maxCompletionTokens)); + unset($prepared['max_completion_tokens']); + if (!array_key_exists('temperature', $prepared)) { + $prepared['temperature'] = 0.0; + } + if (!array_key_exists('top_p', $prepared)) { + $prepared['top_p'] = 1.0; + } + $requestedModel = $prepared['model'] ?? null; + if (is_string($requestedModel) + && $requestedModel !== '' + && isset($modelAliases[$requestedModel]) + && is_string($modelAliases[$requestedModel]) + && $modelAliases[$requestedModel] !== '' + ) { + $prepared['model'] = $modelAliases[$requestedModel]; + } + return $prepared; +} + +function king_openai_router_log_prepared_payload(array $original, array $prepared): void +{ + $encoded = json_encode($prepared, JSON_UNESCAPED_SLASHES); + $originalTools = $original['tools'] ?? null; + $originalFunctions = $original['functions'] ?? null; + $preparedTools = $prepared['tools'] ?? null; + $preparedFunctions = $prepared['functions'] ?? null; + $toolStatus = king_openai_router_tool_status($original); + [$originalMessageCount, $originalTextChars, $originalLastUserChars] = + king_inference_runtime_payload_message_stats($original); + [$preparedMessageCount, $preparedTextChars, $preparedLastUserChars] = + king_inference_runtime_payload_message_stats($prepared); + + king_inference_runtime_log_line('prepared', [ + 'normalized' => $original !== $prepared, + 'requested_model' => king_inference_runtime_request_model($original), + 'prepared_model' => king_inference_runtime_request_model($prepared), + 'prepared_body_bytes' => is_string($encoded) ? strlen($encoded) : 0, + 'original_message_count' => $originalMessageCount, + 'prepared_message_count' => $preparedMessageCount, + 'original_message_text_chars' => $originalTextChars, + 'prepared_message_text_chars' => $preparedTextChars, + 'original_last_user_chars' => $originalLastUserChars, + 'prepared_last_user_chars' => $preparedLastUserChars, + 'prepared_stream' => ($prepared['stream'] ?? null) === true, + 'prepared_max_tokens' => king_openai_router_int_payload_value($prepared, 'max_tokens') ?? '', + 'original_tool_schema_count' => is_array($originalTools) ? count($originalTools) : 0, + 'prepared_tool_schema_count' => is_array($preparedTools) ? count($preparedTools) : 0, + 'original_legacy_function_count' => is_array($originalFunctions) ? count($originalFunctions) : 0, + 'prepared_legacy_function_count' => is_array($preparedFunctions) ? count($preparedFunctions) : 0, + ...king_openai_router_memory_log_fields($prepared), + 'tool_choice_removed' => array_key_exists('tool_choice', $original) && !array_key_exists('tool_choice', $prepared), + 'parallel_tool_calls_removed' => array_key_exists('parallel_tool_calls', $original) && !array_key_exists('parallel_tool_calls', $prepared), + 'tool_execution' => !empty($toolStatus['context_only']) ? 'context_only' : 'none', + 'tool_fields_sanitized' => !empty($toolStatus['context_only']) + && !array_key_exists('tools', $prepared) + && !array_key_exists('functions', $prepared) + && !array_key_exists('tool_choice', $prepared) + && !array_key_exists('function_call', $prepared) + && !array_key_exists('parallel_tool_calls', $prepared), + 'tool_field_names' => is_array($toolStatus['present_fields'] ?? null) ? $toolStatus['present_fields'] : [], + 'available_tool_names' => is_array($toolStatus['available_tool_names'] ?? null) ? $toolStatus['available_tool_names'] : [], + 'forced_tool_names' => is_array($toolStatus['forced_tool_names'] ?? null) ? $toolStatus['forced_tool_names'] : [], + 'assistant_tool_call_names' => is_array($toolStatus['assistant_tool_call_names'] ?? null) ? $toolStatus['assistant_tool_call_names'] : [], + 'unknown_tool_names' => is_array($toolStatus['unknown_tool_names'] ?? null) ? $toolStatus['unknown_tool_names'] : [], + 'invalid_tool_schema_count' => is_int($toolStatus['invalid_schema_count'] ?? null) + ? $toolStatus['invalid_schema_count'] + : 0, + ]); +} + +function king_openai_router_stream_terminal(array $event): bool +{ + $choice = $event['choices'][0] ?? null; + return is_array($choice) + && array_key_exists('finish_reason', $choice) + && $choice['finish_reason'] !== null; +} + +function king_openai_router_sse(array $event): string +{ + return 'data: ' . json_encode($event, JSON_UNESCAPED_SLASHES) . "\n\n"; +} + +function king_openai_router_chat_completion_response(string $id, int $created, string $model, string $content): array +{ + return [ + 'id' => $id, + 'object' => 'chat.completion', + 'created' => $created, + 'model' => $model, + 'choices' => [ + [ + 'index' => 0, + 'message' => [ + 'role' => 'assistant', + 'content' => $content, + ], + 'finish_reason' => 'stop', + ], + ], + ]; +} + +function king_openai_router_stream_id(): string +{ + try { + return 'chatcmpl-king-' . bin2hex(random_bytes(8)); + } catch (Throwable) { + return 'chatcmpl-king-' . (string) getmypid() . '-' . (string) time(); + } +} + +function king_openai_router_content_chunk(string $id, int $created, string $model, string $content): array +{ + return [ + 'id' => $id, + 'object' => 'chat.completion.chunk', + 'created' => $created, + 'model' => $model, + 'choices' => [ + [ + 'index' => 0, + 'delta' => ['content' => $content], + 'finish_reason' => null, + ], + ], + ]; +} + +function king_openai_router_deterministic_response( + array $payload, + string $content, + string $model, + int $startedNs +): array { + $id = king_openai_router_stream_id(); + $created = time(); + + if (($payload['stream'] ?? false) === true) { + $nowNs = hrtime(true); + $initial = king_openai_router_attach_stream_timing( + king_openai_router_initial_chunk($id, $created, $model), + king_openai_router_timing_payload($startedNs, $nowNs, 1, 0, null, null, null, false, false, null, []) + ); + $contentEvent = king_openai_router_attach_stream_timing( + king_openai_router_content_chunk($id, $created, $model, $content), + king_openai_router_timing_payload($startedNs, $nowNs, 2, 1, $nowNs, null, $nowNs, false, false, null, []) + ); + $terminal = king_openai_router_attach_stream_timing( + king_openai_router_terminal_chunk($id, $created, $model), + king_openai_router_timing_payload($startedNs, $nowNs, 3, 1, $nowNs, $nowNs, $nowNs, false, true, 'stop', []) + ); + + return [ + 'status' => 200, + 'headers' => [ + 'content-type' => 'text/event-stream', + 'cache-control' => 'no-cache', + 'x-accel-buffering' => 'no', + 'x-king-openai-router-path' => 'deterministic_task', + ], + 'body' => king_openai_router_sse($initial) + . king_openai_router_sse($contentEvent) + . king_openai_router_sse($terminal) + . "data: [DONE]\n\n", + ]; + } + + return [ + 'status' => 200, + 'headers' => ['content-type' => 'application/json'], + 'body' => (string) json_encode( + king_openai_router_chat_completion_response($id, $created, $model, $content), + JSON_UNESCAPED_SLASHES + ), + ]; +} + +function king_openai_router_terminal_chunk(string $id, int $created, string $model): array +{ + return [ + 'id' => $id, + 'object' => 'chat.completion.chunk', + 'created' => $created, + 'model' => $model, + 'choices' => [ + [ + 'index' => 0, + 'delta' => ['content' => ''], + 'finish_reason' => 'stop', + ], + ], + ]; +} + +function king_openai_router_initial_chunk(string $id, int $created, string $model): array +{ + return [ + 'id' => $id, + 'object' => 'chat.completion.chunk', + 'created' => $created, + 'model' => $model, + 'choices' => [ + [ + 'index' => 0, + 'delta' => ['role' => 'assistant'], + 'finish_reason' => null, + ], + ], + ]; +} + +function king_openai_router_normalize_chunk(array $event, string $id, int $created, string $model): array +{ + if (($event['object'] ?? null) === 'chat.completion.chunk') { + $event['id'] = $id; + $event['created'] = $created; + $event['model'] = $model; + } + return $event; +} + +function king_openai_router_role_only_chunk(array $event): bool +{ + $choice = $event['choices'][0] ?? null; + $delta = is_array($choice) ? ($choice['delta'] ?? null) : null; + + return is_array($delta) + && ($delta['role'] ?? null) === 'assistant' + && count($delta) === 1 + && (($choice['finish_reason'] ?? null) === null); +} + +function king_openai_router_plain_artifact_requested(array $payload): bool +{ + $responseFormat = $payload['response_format'] ?? null; + if (is_array($responseFormat)) { + $type = $responseFormat['type'] ?? null; + if ($type === 'json_object' || $type === 'json_schema') { + return true; + } + } + + $texts = []; + $messages = $payload['messages'] ?? null; + if (is_array($messages)) { + foreach ($messages as $message) { + if (!is_array($message)) { + continue; + } + if (($message['role'] ?? null) !== 'user') { + continue; + } + $content = king_openai_router_message_content_text($message['content'] ?? ''); + if ($content !== '') { + $texts[] = $content; + } + } + } + foreach (['prompt', 'native_prompt_text'] as $key) { + if (is_string($payload[$key] ?? null) && trim($payload[$key]) !== '') { + $texts[] = $payload[$key]; + } + } + + $text = strtolower(implode("\n", $texts)); + if ($text === '') { + return false; + } + + foreach ([ + 'no markdown', + 'without markdown', + 'do not use markdown', + 'return only json', + 'output only json', + 'respond only json', + 'valid json', + 'return only php', + 'output only php', + 'return only code', + 'output only code', + 'only file contents', + 'exact text', + ] as $needle) { + if (str_contains($text, $needle)) { + return true; + } + } + + return false; +} + +function king_openai_router_strip_artifact_markdown_fence(string $content): string +{ + $trimmed = trim($content); + if (preg_match('/^```[A-Za-z0-9_.+-]*[ \t]*\R(.*)\R```[ \t]*$/s', $trimmed, $match) === 1) { + return trim($match[1]); + } + if (preg_match('/^```[A-Za-z0-9_.+-]*[ \t]*(.*?)```[ \t]*$/s', $trimmed, $match) === 1) { + return trim($match[1]); + } + return $content; +} + +function king_openai_router_event_delta_content(array $event): string +{ + $choice = $event['choices'][0] ?? null; + if (!is_array($choice)) { + return ''; + } + $delta = $choice['delta'] ?? null; + if (!is_array($delta) || !is_string($delta['content'] ?? null)) { + return ''; + } + return $delta['content']; +} + +function king_openai_router_clear_event_delta_content(array $event): array +{ + if (isset($event['choices'][0]['delta']['content'])) { + $event['choices'][0]['delta']['content'] = ''; + } + return $event; +} + +function king_openai_router_normalize_plain_artifact_response(array $response, array $payload): array +{ + if (!king_openai_router_plain_artifact_requested($payload)) { + return $response; + } + if (!is_string($response['body'] ?? null) || trim($response['body']) === '') { + return $response; + } + + try { + $body = json_decode($response['body'], true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException) { + return $response; + } + if (!is_array($body)) { + return $response; + } + + $changed = false; + if (isset($body['choices'][0]['message']['content']) + && is_string($body['choices'][0]['message']['content'])) { + $before = $body['choices'][0]['message']['content']; + $after = king_openai_router_strip_artifact_markdown_fence($before); + if ($after !== $before) { + $body['choices'][0]['message']['content'] = $after; + $changed = true; + } + } + if (isset($body['choices'][0]['text']) && is_string($body['choices'][0]['text'])) { + $before = $body['choices'][0]['text']; + $after = king_openai_router_strip_artifact_markdown_fence($before); + if ($after !== $before) { + $body['choices'][0]['text'] = $after; + $changed = true; + } + } + if (!$changed) { + return $response; + } + + $encoded = json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + if (!is_string($encoded)) { + return $response; + } + $response['body'] = $encoded; + if (isset($response['headers']) && is_array($response['headers'])) { + $response['headers']['content-length'] = (string) strlen($encoded); + } + return $response; +} + +$routerOptions = [ + 'owned_by' => 'local-king', + 'with_memory' => $withMemory, + 'memory_policy' => $memoryPolicy, + 'context_policy' => $contextPolicy, + 'default_system_prompt' => $defaultSystemPrompt, + 'coder_instruction_wrapper' => $coderInstructionWrapper, + 'model_aliases' => $modelAliases, + 'default_max_tokens' => $defaultMaxTokens, + 'max_completion_tokens' => $maxCompletionTokens, + 'internal_mcp_tools_enabled' => $internalMcpToolsEnabled, + 'internal_mcp_tool_timeout_ms' => $internalMcpToolTimeoutMs, + 'allow_buffered_native_stream' => $allowBufferedNativeStream, + 'read_timeout_ms' => 250, + 'max_events' => 4096, + 'max_idle_events' => 4800, + 'max_chat_messages' => 256, + 'max_response_input_items' => 256, + 'max_completion_prompts' => 128, + 'max_embedding_inputs' => 512, + 'max_embedding_tokens' => 2048, + 'max_embedding_dimensions' => 8192, +]; + +while (true) { + try { + king_http1_server_listen_once( + $host, + $port, + $listenerConfig, + static function (array $request) use ($models, $routerOptions): array { + $startedNs = hrtime(true); + king_inference_runtime_log_request_executing($request, $models); + $chatPayload = king_openai_router_decode_chat_payload($request); + if ($chatPayload !== null) { + $contextPolicy = isset($routerOptions['context_policy']) && is_string($routerOptions['context_policy']) + ? $routerOptions['context_policy'] + : 'full'; + $defaultSystemPrompt = isset($routerOptions['default_system_prompt']) && is_string($routerOptions['default_system_prompt']) + ? $routerOptions['default_system_prompt'] + : ''; + $coderInstructionWrapper = isset($routerOptions['coder_instruction_wrapper']) && is_bool($routerOptions['coder_instruction_wrapper']) + ? $routerOptions['coder_instruction_wrapper'] + : true; + $modelAliases = isset($routerOptions['model_aliases']) && is_array($routerOptions['model_aliases']) + ? $routerOptions['model_aliases'] + : []; + $defaultMaxTokens = isset($routerOptions['default_max_tokens']) && is_int($routerOptions['default_max_tokens']) + ? max(1, $routerOptions['default_max_tokens']) + : 32; + $maxCompletionTokens = isset($routerOptions['max_completion_tokens']) && is_int($routerOptions['max_completion_tokens']) + ? max(1, $routerOptions['max_completion_tokens']) + : 64; + $memoryPolicy = isset($routerOptions['memory_policy']) && is_array($routerOptions['memory_policy']) + ? $routerOptions['memory_policy'] + : ['with_memory' => false, 'llm_cache' => ['enabled' => false]]; + $preparedPayload = king_openai_router_prepare_chat_payload( + $chatPayload, + $contextPolicy, + $defaultSystemPrompt, + $coderInstructionWrapper, + $modelAliases, + $defaultMaxTokens, + $maxCompletionTokens, + $memoryPolicy + ); + king_openai_router_log_prepared_payload($chatPayload, $preparedPayload); + $preparedRequest = $request; + $preparedRequest['body'] = (string) json_encode($preparedPayload, JSON_UNESCAPED_SLASHES); + if (!isset($preparedRequest['headers']) || !is_array($preparedRequest['headers'])) { + $preparedRequest['headers'] = []; + } + $preparedRequest['headers']['content-length'] = (string) strlen($preparedRequest['body']); + $deterministicResult = king_openai_engine_deterministic_result($preparedPayload, $routerOptions); + if ($deterministicResult !== null && is_string($deterministicResult['content'] ?? null)) { + $responseModel = is_string($preparedPayload['model'] ?? null) && $preparedPayload['model'] !== '' + ? $preparedPayload['model'] + : array_key_first($models); + $response = king_openai_router_deterministic_response( + $preparedPayload, + $deterministicResult['content'], + is_string($responseModel) && $responseModel !== '' ? $responseModel : 'king-local', + $startedNs + ); + king_inference_runtime_log_request_completed($preparedRequest, $models, $startedNs, $response); + return $response; + } + if (($preparedPayload['stream'] ?? false) === true) { + $streamResponse = king_openai_router_stream_response( + $models, + $preparedPayload, + $routerOptions, + $preparedRequest, + $startedNs + ); + if ($streamResponse !== null) { + return $streamResponse; + } + } + + $request = $preparedRequest; + } + + $response = king_inference_openai_http_response($models, $request, $routerOptions); + if ($chatPayload !== null && isset($preparedPayload) && is_array($preparedPayload)) { + $response = king_openai_router_normalize_plain_artifact_response($response, $preparedPayload); + } + king_inference_runtime_log_request_completed($request, $models, $startedNs, $response); + return $response; + } + ); + } catch (Throwable $e) { + fwrite(STDERR, '[' . date('c') . '] ' . $e::class . ': ' . $e->getMessage() . "\n"); + usleep(250000); + } +} diff --git a/infra/inference/performance-baseline.php b/infra/inference/performance-baseline.php new file mode 100644 index 000000000..e04988911 --- /dev/null +++ b/infra/inference/performance-baseline.php @@ -0,0 +1,651 @@ + is_int($v) || is_float($v))); + if ($numbers === []) { + return null; + } + sort($numbers, SORT_NUMERIC); + $index = (int) ceil(($percentile / 100.0) * count($numbers)) - 1; + $index = max(0, min(count($numbers) - 1, $index)); + return round((float) $numbers[$index], 3); +} + +function king_baseline_shell_line(string $command): string +{ + $lines = []; + $code = 0; + exec($command, $lines, $code); + return $code === 0 && isset($lines[0]) ? trim($lines[0]) : ''; +} + +function king_baseline_gpu_snapshot(): array +{ + $line = king_baseline_shell_line( + 'nvidia-smi --query-gpu=name,driver_version,temperature.gpu,utilization.gpu,memory.used,memory.total,power.draw --format=csv,noheader,nounits 2>/dev/null' + ); + if ($line === '') { + return ['available' => false]; + } + $parts = array_map('trim', explode(',', $line)); + return [ + 'available' => true, + 'name' => $parts[0] ?? '', + 'driver_version' => $parts[1] ?? '', + 'temperature_c' => isset($parts[2]) && is_numeric($parts[2]) ? (float) $parts[2] : null, + 'utilization_percent' => isset($parts[3]) && is_numeric($parts[3]) ? (float) $parts[3] : null, + 'vram_used_mb' => isset($parts[4]) && is_numeric($parts[4]) ? (float) $parts[4] : null, + 'vram_total_mb' => isset($parts[5]) && is_numeric($parts[5]) ? (float) $parts[5] : null, + 'power_w' => isset($parts[6]) && is_numeric($parts[6]) ? (float) $parts[6] : null, + ]; +} + +function king_baseline_cuda_runtime_version(): ?string +{ + $version = king_baseline_shell_line('cat /usr/local/cuda/version.txt 2>/dev/null'); + if ($version !== '') { + return $version; + } + + $version = king_baseline_shell_line('nvcc --version 2>/dev/null'); + return $version !== '' ? $version : null; +} + +function king_baseline_gpu_delta(array $before, array $after): array +{ + $delta = []; + foreach (['temperature_c', 'utilization_percent', 'vram_used_mb', 'vram_total_mb', 'power_w'] as $key) { + $delta[$key] = is_numeric($before[$key] ?? null) && is_numeric($after[$key] ?? null) + ? round((float) $after[$key] - (float) $before[$key], 3) + : null; + } + return $delta; +} + +function king_baseline_cpu_model(): string +{ + $lines = @file('/proc/cpuinfo', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + if (!is_array($lines)) { + return php_uname('m'); + } + foreach ($lines as $line) { + if (str_starts_with($line, 'model name')) { + $parts = explode(':', $line, 2); + return trim($parts[1] ?? php_uname('m')); + } + } + return php_uname('m'); +} + +function king_baseline_memory_total_mb(): ?int +{ + $lines = @file('/proc/meminfo', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + if (!is_array($lines)) { + return null; + } + foreach ($lines as $line) { + if (preg_match('/^MemTotal:\s+(\d+)\s+kB$/', $line, $m) === 1) { + return (int) floor(((int) $m[1]) / 1024); + } + } + return null; +} + +function king_baseline_artifact(string $path): array +{ + $real = realpath($path) ?: $path; + $readable = is_file($path) && is_readable($path); + return [ + 'path' => $path, + 'real_path' => $real, + 'readable' => $readable, + 'bytes' => $readable ? filesize($path) : null, + 'sha256' => $readable ? hash_file('sha256', $path) : null, + ]; +} + +function king_baseline_http_json(string $method, string $url, ?array $payload = null, float $timeout = 10.0): array +{ + $options = ['http' => [ + 'method' => $method, + 'header' => "content-type: application/json\r\nconnection: close\r\n", + 'ignore_errors' => true, + 'timeout' => $timeout, + ]]; + if ($payload !== null) { + $options['http']['content'] = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } + $body = @file_get_contents($url, false, stream_context_create($options)); + $headers = $http_response_header ?? []; + $statusLine = $headers[0] ?? ''; + preg_match('/\s(\d{3})\s/', $statusLine, $match); + $status = isset($match[1]) ? (int) $match[1] : 0; + $json = is_string($body) && $body !== '' ? json_decode($body, true) : null; + return [$status, is_array($json) ? $json : null, is_string($body) ? $body : '']; +} + +function king_baseline_wait_for_router(string $modelsUrl, int $timeoutSeconds): bool +{ + $deadline = time() + $timeoutSeconds; + while (time() <= $deadline) { + [$status] = king_baseline_http_json('GET', $modelsUrl, null, 1.0); + if ($status === 200) { + return true; + } + usleep(250_000); + } + return false; +} + +function king_baseline_model_meta(string $modelsUrl, string $model): array +{ + [$status, $json, $raw] = king_baseline_http_json('GET', $modelsUrl, null, 5.0); + $meta = ['status' => $status, 'listed' => false, 'raw_error' => $status === 200 ? '' : substr($raw, 0, 500)]; + if ($status !== 200 || !is_array($json['data'] ?? null)) { + return $meta; + } + foreach ($json['data'] as $item) { + if (!is_array($item) || ($item['id'] ?? null) !== $model) { + continue; + } + $king = is_array($item['x_king'] ?? null) ? $item['x_king'] : []; + $truth = is_array($king['runtime_truth'] ?? null) ? $king['runtime_truth'] : []; + $readiness = is_array($king['native_engine_readiness'] ?? null) ? $king['native_engine_readiness'] : []; + $gpuRuntime = is_array($king['gpu_runtime'] ?? null) ? $king['gpu_runtime'] : []; + $route = is_array($king['openai_route'] ?? null) ? $king['openai_route'] : []; + return $meta + [ + 'listed' => true, + 'id' => $item['id'], + 'backend' => $king['backend'] ?? null, + 'active_device' => $truth['active_device'] ?? null, + 'silent_cpu_fallback' => $truth['silent_cpu_fallback'] ?? null, + 'model_resident' => $truth['model_resident'] ?? null, + 'context_tokens' => $truth['context_tokens'] ?? null, + 'quantization' => $king['quantization'] ?? null, + 'openai_generation' => $king['openai_generation'] ?? null, + 'native_engine_state' => $readiness['state'] ?? null, + 'plain_text_generation_admitted' => $readiness['plain_text_generation_admitted'] ?? null, + 'gpu_generation_ready' => $gpuRuntime['generation_ready'] ?? null, + 'gpu_reason' => $gpuRuntime['reason'] ?? null, + 'cuda_driver_version' => $gpuRuntime['cuda_driver']['driver_version'] ?? null, + 'cuda_device_name' => $gpuRuntime['cuda_driver']['first_device_name'] ?? null, + 'batch_prefill_status' => $route['batch_prefill_status'] ?? null, + 'batch_prefill_admitted' => $route['batch_prefill_admitted'] ?? null, + ]; + } + return $meta; +} + +function king_baseline_stream_run(string $url, string $model, string $prompt, int $maxTokens): array +{ + $payload = [ + 'model' => $model, + 'messages' => [['role' => 'user', 'content' => $prompt]], + 'stream' => true, + 'max_completion_tokens' => $maxTokens, + 'temperature' => 0.0, + ]; + $context = stream_context_create(['http' => [ + 'method' => 'POST', + 'header' => "content-type: application/json\r\naccept: text/event-stream\r\nconnection: close\r\n", + 'content' => json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), + 'ignore_errors' => true, + 'timeout' => 180, + ]]); + $start = hrtime(true); + $handle = @fopen($url, 'rb', false, $context); + if (!is_resource($handle)) { + return ['ok' => false, 'error' => 'connect_failed']; + } + $headers = $http_response_header ?? []; + $statusLine = $headers[0] ?? ''; + preg_match('/\s(\d{3})\s/', $statusLine, $match); + $status = isset($match[1]) ? (int) $match[1] : 0; + $firstByte = null; + $firstContent = null; + $lastContent = null; + $events = 0; + $contentEvents = 0; + $contentBytes = 0; + $done = false; + $finishReason = null; + $serverFirstTokenMs = null; + $serverLastEventMs = null; + $serverGeneratedChunks = null; + $error = ''; + + while (!feof($handle)) { + $line = fgets($handle); + $now = hrtime(true); + if ($line === false) { + usleep(10_000); + continue; + } + if ($firstByte === null && $line !== '') { + $firstByte = $now; + } + $line = rtrim($line, "\r\n"); + if (!str_starts_with($line, 'data: ')) { + continue; + } + $data = substr($line, 6); + if ($data === '[DONE]') { + $done = true; + break; + } + $event = json_decode($data, true); + if (!is_array($event)) { + continue; + } + $events++; + $choice = $event['choices'][0] ?? null; + $delta = is_array($choice) && is_array($choice['delta'] ?? null) && is_string($choice['delta']['content'] ?? null) + ? $choice['delta']['content'] + : ''; + $finish = is_array($choice) ? ($choice['finish_reason'] ?? null) : null; + if (is_string($finish)) { + $finishReason = $finish; + } + $streaming = is_array($event['x_king']['streaming'] ?? null) ? $event['x_king']['streaming'] : []; + if (is_numeric($streaming['first_token_elapsed_ms'] ?? null)) { + $serverFirstTokenMs = (float) $streaming['first_token_elapsed_ms']; + } + if (is_numeric($streaming['request_elapsed_ms'] ?? null)) { + $serverLastEventMs = (float) $streaming['request_elapsed_ms']; + } + if (is_int($streaming['generated_token_chunks'] ?? null)) { + $serverGeneratedChunks = $streaming['generated_token_chunks']; + } + if ($delta !== '') { + if ($firstContent === null) { + $firstContent = $now; + } + $lastContent = $now; + $contentEvents++; + $contentBytes += strlen($delta); + } + } + fclose($handle); + $end = hrtime(true); + + if ($status !== 200) { + $error = 'http_status_' . $status; + } else if (!$done) { + $error = 'missing_done'; + } else if ($contentEvents === 0) { + $error = 'no_content_events'; + } + + $totalMs = ($end - $start) / 1_000_000; + $ttfbMs = $firstContent !== null ? ($firstContent - $start) / 1_000_000 : null; + $firstByteMs = $firstByte !== null ? ($firstByte - $start) / 1_000_000 : null; + $clientDecodeMs = $firstContent !== null ? max(0.001, ($end - $firstContent) / 1_000_000) : null; + $serverDecodeMs = is_numeric($serverFirstTokenMs) && is_numeric($serverLastEventMs) + ? max(0.001, $serverLastEventMs - $serverFirstTokenMs) + : null; + $generated = $serverGeneratedChunks ?? $contentEvents; + $clientTokensPerSecond = $clientDecodeMs !== null ? ($generated * 1000.0) / $clientDecodeMs : null; + $serverTokensPerSecond = $serverDecodeMs !== null ? ($generated * 1000.0) / $serverDecodeMs : null; + $tokensPerSecond = $serverTokensPerSecond ?? $clientTokensPerSecond; + $promptEstimate = max(1, count(preg_split('/\s+/', trim($prompt)) ?: [])); + $prefillPerSecond = $ttfbMs !== null && $ttfbMs > 0.0 ? ($promptEstimate * 1000.0) / $ttfbMs : null; + + return [ + 'ok' => $error === '', + 'error' => $error, + 'status' => $status, + 'events' => $events, + 'content_events' => $contentEvents, + 'content_bytes' => $contentBytes, + 'first_byte_ms' => $firstByteMs !== null ? round($firstByteMs, 3) : null, + 'ttfb_ms' => $ttfbMs !== null ? round($ttfbMs, 3) : null, + 'total_ms' => round($totalMs, 3), + 'client_decode_ms' => $clientDecodeMs !== null ? round($clientDecodeMs, 3) : null, + 'server_decode_ms' => $serverDecodeMs !== null ? round($serverDecodeMs, 3) : null, + 'server_first_token_ms' => $serverFirstTokenMs, + 'server_last_event_ms' => $serverLastEventMs, + 'generated_tokens_estimate' => $generated, + 'tokens_per_second' => $tokensPerSecond !== null ? round($tokensPerSecond, 3) : null, + 'client_tokens_per_second' => $clientTokensPerSecond !== null ? round($clientTokensPerSecond, 3) : null, + 'server_tokens_per_second' => $serverTokensPerSecond !== null ? round($serverTokensPerSecond, 3) : null, + 'prefill_tokens_per_second_estimate' => $prefillPerSecond !== null ? round($prefillPerSecond, 3) : null, + 'finish_reason' => $finishReason, + ]; +} + +function king_baseline_error_counts(array $runs): array +{ + $errors = []; + foreach ($runs as $run) { + $error = is_string($run['error'] ?? null) ? $run['error'] : ''; + if ($error === '') { + continue; + } + $errors[$error] = ($errors[$error] ?? 0) + 1; + } + ksort($errors); + return $errors; +} + +function king_baseline_first_error(array $runs): string +{ + foreach ($runs as $run) { + $error = is_string($run['error'] ?? null) ? $run['error'] : ''; + if ($error !== '') { + return $error; + } + } + return 'unknown'; +} + +function king_baseline_aggregate(array $runs): array +{ + $okRuns = array_values(array_filter($runs, static fn (array $run): bool => !empty($run['ok']))); + $field = static fn (string $key): array => array_map(static fn (array $run): mixed => $run[$key] ?? null, $okRuns); + return [ + 'runs' => count($runs), + 'successful_runs' => count($okRuns), + 'failed_runs' => count($runs) - count($okRuns), + 'errors' => king_baseline_error_counts($runs), + 'ttfb_ms' => [ + 'p50' => king_baseline_percentile($field('ttfb_ms'), 50), + 'p95' => king_baseline_percentile($field('ttfb_ms'), 95), + ], + 'total_ms' => [ + 'p50' => king_baseline_percentile($field('total_ms'), 50), + 'p95' => king_baseline_percentile($field('total_ms'), 95), + ], + 'tokens_per_second' => [ + 'p50' => king_baseline_percentile($field('tokens_per_second'), 50), + 'p95' => king_baseline_percentile($field('tokens_per_second'), 95), + ], + 'client_tokens_per_second' => [ + 'p50' => king_baseline_percentile($field('client_tokens_per_second'), 50), + 'p95' => king_baseline_percentile($field('client_tokens_per_second'), 95), + ], + 'server_tokens_per_second' => [ + 'p50' => king_baseline_percentile($field('server_tokens_per_second'), 50), + 'p95' => king_baseline_percentile($field('server_tokens_per_second'), 95), + ], + 'prefill_tokens_per_second_estimate' => [ + 'p50' => king_baseline_percentile($field('prefill_tokens_per_second_estimate'), 50), + 'p95' => king_baseline_percentile($field('prefill_tokens_per_second_estimate'), 95), + ], + 'generated_tokens_estimate_total' => array_sum(array_map(static fn (array $run): int => (int) ($run['generated_tokens_estimate'] ?? 0), $okRuns)), + 'finish_reasons' => array_values(array_unique(array_filter(array_map(static fn (array $run): mixed => $run['finish_reason'] ?? null, $okRuns), 'is_string'))), + ]; +} + +function king_baseline_start_router(array $profile, int $port, string $root, string $php, string $ini, string $extension): array +{ + $log = sys_get_temp_dir() . '/king-baseline-' . $profile['label'] . '-' . $port . '.log'; + $cmd = [ + $php, + '-n', + '-c', $ini, + '-d', 'extension=' . $extension, + '-d', 'king.security_allow_config_override=1', + '-d', 'memory_limit=4G', + '-d', 'king.inference_preferred_model_profile=' . $profile['runtime_profile'], + '-d', 'king.inference_cpu_model_name=' . $profile['cpu_model'], + '-d', 'king.inference_cpu_model_artifact=' . $profile['cpu_artifact'], + '-d', 'king.inference_gpu_model_name=' . $profile['gpu_model'], + '-d', 'king.inference_gpu_model_artifact=' . $profile['gpu_artifact'], + $root . '/infra/inference/openai-router.php', + ]; + $env = array_merge($_ENV, [ + 'PATH' => getenv('PATH') ?: '/usr/local/bin:/usr/bin:/bin', + 'KING_ROOT' => $root, + 'KING_OPENAI_HOST' => '127.0.0.1', + 'KING_OPENAI_PORT' => (string) $port, + 'KING_OPENAI_CONTEXT_POLICY' => 'full', + 'KING_OPENAI_DEFAULT_MAX_TOKENS' => '32', + 'KING_OPENAI_MAX_COMPLETION_TOKENS' => '32', + 'PHP_INI_SCAN_DIR' => '', + 'LD_LIBRARY_PATH' => trim('/usr/local/cuda/targets/x86_64-linux/lib:' . (getenv('LD_LIBRARY_PATH') ?: ''), ':'), + ]); + $process = proc_open( + $cmd, + [ + 0 => ['file', '/dev/null', 'r'], + 1 => ['file', $log, 'a'], + 2 => ['file', $log, 'a'], + ], + $pipes, + $root, + $env + ); + if (!is_resource($process)) { + throw new RuntimeException('failed to start router'); + } + return ['process' => $process, 'log' => $log, 'cmd' => $cmd]; +} + +function king_baseline_stop_router(mixed $process): void +{ + if (!is_resource($process)) { + return; + } + proc_terminate($process); + usleep(500_000); + $status = proc_get_status($process); + if (($status['running'] ?? false) === true) { + proc_terminate($process, 9); + usleep(250_000); + } + proc_close($process); +} + +if (king_baseline_flag($argv, '--help') || king_baseline_flag($argv, '-h')) { + king_baseline_usage(); + exit(0); +} + +$root = dirname(__DIR__, 2); +$php = getenv('PHP_BIN') ?: PHP_BINARY; +$ini = getenv('KING_INFERENCE_PHP_INI') ?: $root . '/infra/inference/local-gpu.php.ini'; +$extension = getenv('KING_EXTENSION') ?: $root . '/extension/modules/king.so'; +$runs = king_baseline_int($argv, '--runs', 3); +$maxTokens = king_baseline_int($argv, '--max-tokens', 8); +$portStart = king_baseline_int($argv, '--port-start', 19080, 1024); +$jsonOnly = king_baseline_flag($argv, '--json'); +$selectedProfiles = array_filter(array_map('trim', explode(',', king_baseline_option($argv, '--profiles', 'cpu-gemma3,gpu-gemma3,gpu-large') ?? ''))); +if (king_baseline_flag($argv, '--skip-large')) { + $selectedProfiles = array_values(array_filter($selectedProfiles, static fn (string $p): bool => $p !== 'gpu-large')); +} + +$cpuArtifact = $root . '/var/inference-models/gemma3-1b.gguf'; +$largeArtifact = $root . '/var/inference-models/gemma4-12b.gguf'; +$allProfiles = [ + 'cpu-gemma3' => [ + 'label' => 'cpu-gemma3', + 'runtime_profile' => 'cpu', + 'model' => 'gemma3:1b', + 'cpu_model' => 'gemma3:1b', + 'cpu_artifact' => $cpuArtifact, + 'gpu_model' => 'gemma3:1b', + 'gpu_artifact' => $cpuArtifact, + ], + 'gpu-gemma3' => [ + 'label' => 'gpu-gemma3', + 'runtime_profile' => 'gpu', + 'model' => 'gemma3:1b', + 'cpu_model' => 'gemma3:1b', + 'cpu_artifact' => $cpuArtifact, + 'gpu_model' => 'gemma3:1b', + 'gpu_artifact' => $cpuArtifact, + ], + 'gpu-large' => [ + 'label' => 'gpu-large', + 'runtime_profile' => 'gpu', + 'model' => getenv('KING_INFERENCE_LARGE_GPU_MODEL_NAME') ?: 'gemma4:12b', + 'cpu_model' => 'gemma3:1b', + 'cpu_artifact' => $cpuArtifact, + 'gpu_model' => getenv('KING_INFERENCE_LARGE_GPU_MODEL_NAME') ?: 'gemma4:12b', + 'gpu_artifact' => getenv('KING_INFERENCE_LARGE_GPU_MODEL_PATH') ?: $largeArtifact, + ], +]; + +$report = [ + 'schema_version' => 1, + 'generated_at' => date(DATE_ATOM), + 'host' => [ + 'os' => php_uname(), + 'php_version' => PHP_VERSION, + 'cpu_model' => king_baseline_cpu_model(), + 'cpu_cores_visible' => (int) trim(king_baseline_shell_line('nproc 2>/dev/null') ?: '0'), + 'memory_total_mb' => king_baseline_memory_total_mb(), + 'cuda_runtime_version' => king_baseline_cuda_runtime_version(), + ], + 'gpu_before_all' => king_baseline_gpu_snapshot(), + 'runs_per_profile' => $runs, + 'max_tokens' => $maxTokens, + 'profiles' => [], +]; + +$prompt = 'Answer in one short sentence: what is a tokenizer?'; +$profileIndex = 0; +foreach ($selectedProfiles as $profileName) { + $profile = $allProfiles[$profileName] ?? null; + if ($profile === null) { + $report['profiles'][] = ['label' => $profileName, 'status' => 'skipped', 'reason' => 'unknown_profile']; + continue; + } + + $artifact = king_baseline_artifact($profile['runtime_profile'] === 'gpu' ? $profile['gpu_artifact'] : $profile['cpu_artifact']); + $entry = [ + 'label' => $profile['label'], + 'status' => 'not_started', + 'runtime_profile' => $profile['runtime_profile'], + 'model' => $profile['model'], + 'artifact' => $artifact, + 'port' => $portStart + $profileIndex, + 'runs' => [], + ]; + $profileIndex++; + + if (!$artifact['readable']) { + $entry['status'] = 'skipped'; + $entry['reason'] = 'artifact_not_readable'; + $report['profiles'][] = $entry; + continue; + } + + $router = null; + try { + $router = king_baseline_start_router($profile, $entry['port'], $root, $php, $ini, $extension); + $modelsUrl = 'http://127.0.0.1:' . $entry['port'] . '/v1/models'; + $chatUrl = 'http://127.0.0.1:' . $entry['port'] . '/v1/chat/completions'; + if (!king_baseline_wait_for_router($modelsUrl, 90)) { + $entry['status'] = 'failed'; + $entry['reason'] = 'router_not_ready'; + $entry['router_log'] = $router['log']; + $report['profiles'][] = $entry; + king_baseline_stop_router($router['process']); + continue; + } + + $entry['model_before'] = king_baseline_model_meta($modelsUrl, $profile['model']); + $entry['gpu_before'] = king_baseline_gpu_snapshot(); + for ($i = 0; $i < $runs; $i++) { + $entry['runs'][] = king_baseline_stream_run($chatUrl, $profile['model'], $prompt, $maxTokens); + } + $entry['gpu_after'] = king_baseline_gpu_snapshot(); + $entry['gpu_delta'] = king_baseline_gpu_delta($entry['gpu_before'], $entry['gpu_after']); + $entry['model_after'] = king_baseline_model_meta($modelsUrl, $profile['model']); + $entry['aggregate'] = king_baseline_aggregate($entry['runs']); + $entry['fallback_status'] = [ + 'backend' => $entry['model_after']['backend'] ?? null, + 'active_device' => $entry['model_after']['active_device'] ?? null, + 'silent_cpu_fallback' => $entry['model_after']['silent_cpu_fallback'] ?? null, + ]; + $entry['status'] = ($entry['aggregate']['successful_runs'] ?? 0) > 0 ? 'measured' : 'failed'; + $entry['reason'] = $entry['status'] === 'measured' + ? 'ok' + : 'no_successful_runs:' . king_baseline_first_error($entry['runs']); + king_baseline_stop_router($router['process']); + } catch (Throwable $e) { + if (is_array($router ?? null) && isset($router['process'])) { + king_baseline_stop_router($router['process']); + } + $entry['status'] = 'failed'; + $entry['reason'] = 'exception'; + $entry['error'] = $e::class . ': ' . $e->getMessage(); + } + + $report['profiles'][] = $entry; +} + +$report['gpu_after_all'] = king_baseline_gpu_snapshot(); + +if ($jsonOnly) { + echo json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; + exit(0); +} + +foreach ($report['profiles'] as $profile) { + echo 'profile=' . ($profile['label'] ?? '') . ' status=' . ($profile['status'] ?? '') . ' model=' . ($profile['model'] ?? '') . "\n"; + if (isset($profile['aggregate']) && is_array($profile['aggregate'])) { + echo ' ttfb_p50_ms=' . json_encode($profile['aggregate']['ttfb_ms']['p50'] ?? null) + . ' total_p50_ms=' . json_encode($profile['aggregate']['total_ms']['p50'] ?? null) + . ' tps_p50=' . json_encode($profile['aggregate']['tokens_per_second']['p50'] ?? null) + . ' server_tps_p50=' . json_encode($profile['aggregate']['server_tokens_per_second']['p50'] ?? null) + . ' prefill_est_p50=' . json_encode($profile['aggregate']['prefill_tokens_per_second_estimate']['p50'] ?? null) + . ' generated_total=' . json_encode($profile['aggregate']['generated_tokens_estimate_total'] ?? null) + . "\n"; + echo ' fallback=' . json_encode($profile['fallback_status'] ?? null, JSON_UNESCAPED_SLASHES) + . ' errors=' . json_encode($profile['aggregate']['errors'] ?? [], JSON_UNESCAPED_SLASHES) + . ' reason=' . ($profile['reason'] ?? '') + . "\n"; + } else { + echo ' reason=' . ($profile['reason'] ?? '') . "\n"; + } +} diff --git a/infra/inference/release-gate.php b/infra/inference/release-gate.php new file mode 100644 index 000000000..6203c0510 --- /dev/null +++ b/infra/inference/release-gate.php @@ -0,0 +1,398 @@ + $label, 'path' => '', 'present' => false, 'json' => null, 'reason' => 'path_not_configured']; + } + if (!is_file($path) || !is_readable($path)) { + return ['label' => $label, 'path' => $path, 'present' => false, 'json' => null, 'reason' => 'file_not_readable']; + } + + try { + $raw = file_get_contents($path); + if (!is_string($raw) || trim($raw) === '') { + return ['label' => $label, 'path' => $path, 'present' => false, 'json' => null, 'reason' => 'file_empty']; + } + $json = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); + if (!is_array($json)) { + return ['label' => $label, 'path' => $path, 'present' => false, 'json' => null, 'reason' => 'json_not_object']; + } + return ['label' => $label, 'path' => $path, 'present' => true, 'json' => $json, 'reason' => 'ok']; + } catch (Throwable $e) { + return ['label' => $label, 'path' => $path, 'present' => false, 'json' => null, 'reason' => $e::class . ': ' . $e->getMessage()]; + } +} + +function king_release_gate_requirement(string $name, string $status, string $reason, array $evidence = []): array +{ + return ['name' => $name, 'status' => $status, 'reason' => $reason, 'evidence' => $evidence]; +} + +function king_release_gate_matrix_map(array $matrix): array +{ + $map = []; + foreach (($matrix['gates'] ?? []) as $gate) { + if (!is_array($gate) || !is_string($gate['name'] ?? null)) { + continue; + } + $map[$gate['name']] = $gate; + } + return $map; +} + +function king_release_gate_matrix_requirement(array $matrixReport, string $gate): array +{ + if (!$matrixReport['present']) { + return king_release_gate_requirement('matrix:' . $gate, 'missing', 'matrix_report_missing', [ + 'report' => $matrixReport['path'], + 'reason' => $matrixReport['reason'], + ]); + } + + $map = king_release_gate_matrix_map($matrixReport['json']); + if (!isset($map[$gate])) { + return king_release_gate_requirement('matrix:' . $gate, 'missing', 'matrix_gate_missing', ['gate' => $gate]); + } + + $entry = $map[$gate]; + $status = (string) ($entry['status'] ?? 'failed'); + if ($status === 'passed') { + return king_release_gate_requirement('matrix:' . $gate, 'passed', 'ok', ['gate' => $entry]); + } + + return king_release_gate_requirement( + 'matrix:' . $gate, + $status === 'skipped' ? 'missing' : 'failed', + 'matrix_gate_' . $status . ':' . (string) ($entry['reason'] ?? 'unknown'), + ['gate' => $entry] + ); +} + +function king_release_gate_deterministic_prompts(array $goldenReport, int $minimumCases): array +{ + if (!$goldenReport['present']) { + return king_release_gate_requirement('deterministic_prompt_pack', 'missing', 'golden_report_missing', [ + 'report' => $goldenReport['path'], + 'reason' => $goldenReport['reason'], + 'minimum_cases' => $minimumCases, + ]); + } + + $golden = $goldenReport['json']; + $caseCount = (int) ($golden['case_count'] ?? 0); + $failed = (int) ($golden['failed'] ?? 0); + $ok = ($golden['ok'] ?? false) === true; + if ($caseCount < $minimumCases) { + return king_release_gate_requirement('deterministic_prompt_pack', 'failed', 'not_enough_golden_cases', [ + 'case_count' => $caseCount, + 'minimum_cases' => $minimumCases, + 'failed' => $failed, + 'ok' => $ok, + ]); + } + if (!$ok || $failed !== 0) { + return king_release_gate_requirement('deterministic_prompt_pack', 'failed', 'golden_prompt_failures', [ + 'case_count' => $caseCount, + 'failed' => $failed, + 'ok' => $ok, + ]); + } + + return king_release_gate_requirement('deterministic_prompt_pack', 'passed', 'ok', [ + 'case_count' => $caseCount, + 'minimum_cases' => $minimumCases, + ]); +} + +function king_release_gate_no_silent_fallback(array $baselineReport): array +{ + if (!$baselineReport['present']) { + return king_release_gate_requirement('no_silent_fallback', 'missing', 'baseline_report_missing', [ + 'report' => $baselineReport['path'], + 'reason' => $baselineReport['reason'], + ]); + } + + $checked = []; + $failures = []; + foreach (($baselineReport['json']['profiles'] ?? []) as $profile) { + if (!is_array($profile) || ($profile['runtime_profile'] ?? null) !== 'gpu') { + continue; + } + if (($profile['status'] ?? null) !== 'measured') { + continue; + } + $fallback = is_array($profile['fallback_status'] ?? null) ? $profile['fallback_status'] : []; + $checked[] = [ + 'label' => $profile['label'] ?? null, + 'active_device' => $fallback['active_device'] ?? null, + 'silent_cpu_fallback' => $fallback['silent_cpu_fallback'] ?? null, + ]; + if (($fallback['silent_cpu_fallback'] ?? null) !== false) { + $failures[] = end($checked); + } + } + + if ($checked === []) { + return king_release_gate_requirement('no_silent_fallback', 'missing', 'no_measured_gpu_profile', [ + 'profiles' => array_map(static fn(array $profile): array => [ + 'label' => $profile['label'] ?? null, + 'status' => $profile['status'] ?? null, + 'reason' => $profile['reason'] ?? null, + ], array_filter($baselineReport['json']['profiles'] ?? [], 'is_array')), + ]); + } + if ($failures !== []) { + return king_release_gate_requirement('no_silent_fallback', 'failed', 'silent_cpu_fallback_detected', ['failures' => $failures]); + } + + return king_release_gate_requirement('no_silent_fallback', 'passed', 'ok', ['checked' => $checked]); +} + +function king_release_gate_real_streaming(array $baselineReport): array +{ + if (!$baselineReport['present']) { + return king_release_gate_requirement('real_streaming', 'missing', 'baseline_report_missing', [ + 'report' => $baselineReport['path'], + 'reason' => $baselineReport['reason'], + ]); + } + + $measured = []; + foreach (($baselineReport['json']['profiles'] ?? []) as $profile) { + if (!is_array($profile) || ($profile['status'] ?? null) !== 'measured') { + continue; + } + $aggregate = is_array($profile['aggregate'] ?? null) ? $profile['aggregate'] : []; + $successful = (int) ($aggregate['successful_runs'] ?? 0); + $tokens = (int) ($aggregate['generated_tokens_estimate_total'] ?? 0); + if ($successful > 0 && $tokens > 0) { + $measured[] = [ + 'label' => $profile['label'] ?? null, + 'successful_runs' => $successful, + 'generated_tokens_estimate_total' => $tokens, + 'ttfb_ms_p50' => $aggregate['ttfb_ms']['p50'] ?? null, + 'tokens_per_second_p50' => $aggregate['tokens_per_second']['p50'] ?? null, + ]; + } + } + + if ($measured === []) { + return king_release_gate_requirement('real_streaming', 'missing', 'no_measured_streaming_profile', []); + } + + return king_release_gate_requirement('real_streaming', 'passed', 'ok', ['profiles' => $measured]); +} + +function king_release_gate_batch_prefill(array $baselineReport, array $goldenReport): array +{ + $entries = []; + if ($baselineReport['present']) { + foreach (($baselineReport['json']['profiles'] ?? []) as $profile) { + if (!is_array($profile)) { + continue; + } + $modelAfter = is_array($profile['model_after'] ?? null) ? $profile['model_after'] : []; + if (array_key_exists('batch_prefill_admitted', $modelAfter) || array_key_exists('batch_prefill_status', $modelAfter)) { + $entries[] = [ + 'source' => 'baseline', + 'label' => $profile['label'] ?? null, + 'admitted' => $modelAfter['batch_prefill_admitted'] ?? null, + 'status' => $modelAfter['batch_prefill_status'] ?? null, + ]; + } + } + } + if ($goldenReport['present']) { + $modelEntry = is_array($goldenReport['json']['model_entry'] ?? null) ? $goldenReport['json']['model_entry'] : []; + if (array_key_exists('batch_prefill_admitted', $modelEntry)) { + $entries[] = [ + 'source' => 'golden', + 'label' => $goldenReport['json']['model'] ?? null, + 'admitted' => $modelEntry['batch_prefill_admitted'] ?? null, + 'status' => null, + ]; + } + } + + if ($entries === []) { + return king_release_gate_requirement('batch_prefill_state', 'missing', 'no_batch_prefill_metadata', []); + } + + $bad = []; + foreach ($entries as $entry) { + $admitted = $entry['admitted'] ?? null; + $status = strtolower((string) ($entry['status'] ?? '')); + if ($admitted === true && !in_array($status, ['ready', 'green', 'passed', 'stable'], true)) { + $bad[] = $entry; + } + } + if ($bad !== []) { + return king_release_gate_requirement('batch_prefill_state', 'failed', 'batch_prefill_admitted_without_green_status', ['entries' => $bad]); + } + + return king_release_gate_requirement('batch_prefill_state', 'passed', 'ok', ['entries' => $entries]); +} + +function king_release_gate_claim_is_ready(array $claimsReport): bool +{ + if (!$claimsReport['present']) { + return false; + } + $claims = $claimsReport['json']; + if (($claims['native_inference_ready'] ?? null) === true) { + return true; + } + if (is_array($claims['claims'] ?? null) && ($claims['claims']['native_inference_ready'] ?? null) === true) { + return true; + } + return false; +} + +if (king_release_gate_flag($argv, '--help') || king_release_gate_flag($argv, '-h')) { + king_release_gate_usage(); +} + +$options = [ + 'json' => king_release_gate_flag($argv, '--json'), + 'matrix' => king_release_gate_option($argv, '--matrix', getenv('KING_INFERENCE_RELEASE_MATRIX_REPORT') ?: ''), + 'golden' => king_release_gate_option($argv, '--golden', getenv('KING_INFERENCE_RELEASE_GOLDEN_REPORT') ?: ''), + 'baseline' => king_release_gate_option($argv, '--baseline', getenv('KING_INFERENCE_RELEASE_BASELINE_REPORT') ?: ''), + 'claims' => king_release_gate_option($argv, '--claims', getenv('KING_INFERENCE_RELEASE_CLAIMS_REPORT') ?: ''), + 'min_golden_cases' => king_release_gate_int($argv, '--min-golden-cases', 100, 1), +]; + +$matrixReport = king_release_gate_read_json('matrix', (string) $options['matrix']); +$goldenReport = king_release_gate_read_json('golden', (string) $options['golden']); +$baselineReport = king_release_gate_read_json('baseline', (string) $options['baseline']); +$claimsReport = king_release_gate_read_json('claims', (string) $options['claims']); + +$requirements = []; +$requirements[] = king_release_gate_deterministic_prompts($goldenReport, (int) $options['min_golden_cases']); +foreach ([ + 'tokenizer', + 'gguf_load', + 'cpu_reference', + 'gpu_smoke', + 'cpu_gpu_match', + 'openai_route_smoke', + 'long_prompt', + 'stop_tokens', + 'cancellation', + 'error_taxonomy', +] as $matrixGate) { + $requirements[] = king_release_gate_matrix_requirement($matrixReport, $matrixGate); +} +$requirements[] = king_release_gate_no_silent_fallback($baselineReport); +$requirements[] = king_release_gate_real_streaming($baselineReport); +$requirements[] = king_release_gate_batch_prefill($baselineReport, $goldenReport); + +$failedOrMissing = array_values(array_filter( + $requirements, + static fn(array $requirement): bool => ($requirement['status'] ?? '') !== 'passed' +)); + +$claimReady = king_release_gate_claim_is_ready($claimsReport); +if ($claimReady && $failedOrMissing !== []) { + $requirements[] = king_release_gate_requirement('release_claim_consistency', 'failed', 'ready_claim_without_passing_gate', [ + 'claims_report' => $claimsReport['path'], + ]); + $failedOrMissing[] = end($requirements); +} else { + $requirements[] = king_release_gate_requirement('release_claim_consistency', 'passed', $claimReady ? 'ready_claim_allowed_by_gate' : 'no_ready_claim_requested', [ + 'claims_report' => $claimsReport['present'] ? $claimsReport['path'] : null, + ]); +} + +$counts = ['passed' => 0, 'failed' => 0, 'missing' => 0]; +foreach ($requirements as $requirement) { + $status = (string) ($requirement['status'] ?? 'failed'); + $counts[$status] = ($counts[$status] ?? 0) + 1; +} + +$ok = $counts['failed'] === 0 && $counts['missing'] === 0; +$report = [ + 'schema_version' => 1, + 'generated_at' => date(DATE_ATOM), + 'ok' => $ok, + 'minimum_golden_cases' => (int) $options['min_golden_cases'], + 'release_claim' => $ok ? 'native_inference_ready_for_reported_matrix' : 'native_inference_experimental_hardening', + 'allowed_release_note_claim' => $ok + ? 'Native inference passed the release gate for the supplied evidence reports. Scope must match the matrix and baseline reports.' + : 'Native inference remains experimental/hardening; do not claim release-ready native inference beyond the supplied passing gates.', + 'reports' => [ + 'matrix' => ['path' => $matrixReport['path'], 'present' => $matrixReport['present'], 'reason' => $matrixReport['reason']], + 'golden' => ['path' => $goldenReport['path'], 'present' => $goldenReport['present'], 'reason' => $goldenReport['reason']], + 'baseline' => ['path' => $baselineReport['path'], 'present' => $baselineReport['present'], 'reason' => $baselineReport['reason']], + 'claims' => ['path' => $claimsReport['path'], 'present' => $claimsReport['present'], 'reason' => $claimsReport['reason']], + ], + 'summary' => $counts, + 'requirements' => $requirements, +]; + +if ($options['json']) { + echo json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; + exit($ok ? 0 : 1); +} + +echo "King native inference release gate\n"; +echo 'status=' . ($ok ? 'passed' : 'blocked') . "\n"; +echo 'minimum_golden_cases=' . $report['minimum_golden_cases'] . "\n"; +foreach ($requirements as $requirement) { + echo '[' . strtoupper((string) $requirement['status']) . '] ' + . $requirement['name'] . ': ' + . $requirement['reason'] . "\n"; +} +echo 'summary passed=' . $counts['passed'] . ' missing=' . $counts['missing'] . ' failed=' . $counts['failed'] . "\n"; +echo $report['allowed_release_note_claim'] . "\n"; + +exit($ok ? 0 : 1); diff --git a/infra/inference/runtime-logging.php b/infra/inference/runtime-logging.php new file mode 100644 index 000000000..445f4f066 --- /dev/null +++ b/infra/inference/runtime-logging.php @@ -0,0 +1,291 @@ + king_inference_runtime_log_value($item), $value); + return $items === [] ? '[]' : implode(',', $items); + } + + $string = (string) $value; + if ($string === '') { + return '""'; + } + if (preg_match('/^[A-Za-z0-9_.:\\/@+=,-]+$/', $string) === 1) { + return $string; + } + return (string) json_encode($string, JSON_UNESCAPED_SLASHES); +} + +function king_inference_runtime_log_line(string $state, array $fields, mixed $stream = null): void +{ + $line = '[' . date('c') . '] king-inference state=' . king_inference_runtime_log_value($state); + foreach ($fields as $key => $value) { + $line .= ' ' . $key . '=' . king_inference_runtime_log_value($value); + } + fwrite($stream ?? STDERR, $line . "\n"); +} + +function king_inference_runtime_backend_label(mixed $backend): string +{ + if (is_array($backend)) { + $name = isset($backend['name']) && is_string($backend['name']) ? $backend['name'] : 'array'; + $runner = isset($backend['runner_path']) && is_string($backend['runner_path']) ? $backend['runner_path'] : ''; + return $runner === '' ? $name : $name . ':' . $runner; + } + return is_string($backend) && $backend !== '' ? $backend : 'unknown'; +} + +function king_inference_runtime_log_configured(array $config, mixed $stream = null): void +{ + king_inference_runtime_log_line('configured', $config, $stream); +} + +function king_inference_runtime_decoder_truth_fields(array $info): array +{ + $truth = is_array($info['decoder_truth'] ?? null) ? $info['decoder_truth'] : []; + + return [ + 'decoder_backend' => is_string($truth['backend'] ?? null) ? $truth['backend'] : '', + 'decoder_active_device' => is_string($truth['active_device'] ?? null) ? $truth['active_device'] : '', + 'prompt_graph_path' => is_string($truth['prompt_graph_path'] ?? null) ? $truth['prompt_graph_path'] : '', + 'synthetic_graph_path' => is_string($truth['synthetic_graph_path'] ?? null) ? $truth['synthetic_graph_path'] : '', + 'sampler_path' => is_string($truth['sampler_path'] ?? null) ? $truth['sampler_path'] : '', + 'kv_cache_path' => is_string($truth['kv_cache_path'] ?? null) ? $truth['kv_cache_path'] : '', + 'prompt_to_logits_inference' => !empty($truth['prompt_to_logits_inference']), + 'synthetic_token_vector_graph' => !empty($truth['synthetic_token_vector_graph']), + 'decoder_silent_cpu_fallback' => !empty($truth['silent_cpu_fallback']), + 'fallback_policy' => is_string($truth['fallback_policy'] ?? null) ? $truth['fallback_policy'] : '', + 'fallback_error' => is_string($truth['fallback_error'] ?? null) ? $truth['fallback_error'] : '', + ]; +} + +function king_inference_runtime_log_model_admitted(string $registryName, object $model, mixed $stream = null): void +{ + $info = king_inference_model_info($model); + $backend = is_string($info['backend'] ?? null) ? $info['backend'] : 'unknown'; + $truth = is_array($info['runtime_truth'] ?? null) ? $info['runtime_truth'] : []; + $timing = is_array($truth['measured_token_timing'] ?? null) ? $truth['measured_token_timing'] : []; + $fields = [ + 'registry' => $registryName, + 'model' => is_string($info['name'] ?? null) ? $info['name'] : $registryName, + 'backend' => $backend, + 'admitted' => true, + 'artifact' => is_string($info['artifact_path'] ?? null) ? $info['artifact_path'] : '', + 'active_device' => is_string($truth['active_device'] ?? null) ? $truth['active_device'] : '', + 'model_resident' => !empty($truth['model_resident']), + 'fallback_mode' => is_string($truth['fallback_mode'] ?? null) ? $truth['fallback_mode'] : '', + 'gpu_admission_reason' => is_string($truth['gpu_admission_reason'] ?? null) ? $truth['gpu_admission_reason'] : '', + 'silent_cpu_fallback' => !empty($info['silent_cpu_fallback']), + 'last_timing_available' => !empty($timing['available']), + 'last_generated_tokens' => is_int($timing['generated_tokens'] ?? null) ? $timing['generated_tokens'] : 0, + 'last_ttfb_ms' => is_int($timing['time_to_first_token_ms'] ?? null) ? $timing['time_to_first_token_ms'] : '', + 'last_tokens_per_second' => is_float($timing['tokens_per_second'] ?? null) || is_int($timing['tokens_per_second'] ?? null) + ? $timing['tokens_per_second'] + : '', + ]; + $fields += king_inference_runtime_decoder_truth_fields($info); + + if ($backend === 'king_native_gpu') { + $runtime = is_array($info['gpu_runtime'] ?? null) ? $info['gpu_runtime'] : []; + $fields['config_ready'] = !empty($runtime['config_ready']); + $fields['generation_ready'] = !empty($runtime['generation_ready']); + $fields['reason'] = is_string($runtime['reason'] ?? null) ? $runtime['reason'] : ''; + $fields['refusal_reasons'] = is_array($runtime['refusal_reasons'] ?? null) + ? array_values($runtime['refusal_reasons']) + : []; + } else { + $fields['generation_ready'] = !empty($info['openai_generation'] ?? ($info['token_generation_ready'] ?? false)); + } + + king_inference_runtime_log_line('admitted', $fields, $stream); +} + +function king_inference_runtime_selected_model(array $payload, array $models): ?object +{ + $requested = king_inference_runtime_request_model($payload); + if ($requested !== '' && isset($models[$requested]) && is_object($models[$requested])) { + return $models[$requested]; + } + if ($requested !== '') { + foreach ($models as $model) { + if (!is_object($model)) { + continue; + } + $info = king_inference_model_info($model); + if (($info['name'] ?? null) === $requested) { + return $model; + } + } + return null; + } + $first = reset($models); + return is_object($first) ? $first : null; +} + +function king_inference_runtime_log_request_completed( + array $request, + array $models, + int $startedNs, + array $response, + mixed $stream = null +): void { + $payload = king_inference_runtime_request_payload($request); + $model = king_inference_runtime_selected_model($payload, $models); + $status = is_int($response['status'] ?? null) ? $response['status'] : 0; + $durationMs = (hrtime(true) - $startedNs) / 1_000_000; + $fields = [ + 'method' => is_string($request['method'] ?? null) ? $request['method'] : '', + 'path' => is_string($request['path'] ?? null) ? $request['path'] : ($request['uri'] ?? ''), + 'requested_model' => king_inference_runtime_request_model($payload), + 'status' => $status, + 'duration_ms' => round($durationMs, 3), + ]; + + if ($model !== null) { + $info = king_inference_model_info($model); + $truth = is_array($info['runtime_truth'] ?? null) ? $info['runtime_truth'] : []; + $timing = is_array($truth['measured_token_timing'] ?? null) ? $truth['measured_token_timing'] : []; + $fields += [ + 'model' => is_string($info['name'] ?? null) ? $info['name'] : '', + 'backend' => is_string($info['backend'] ?? null) ? $info['backend'] : '', + 'active_device' => is_string($truth['active_device'] ?? null) ? $truth['active_device'] : '', + 'model_resident' => !empty($truth['model_resident']), + 'fallback_mode' => is_string($truth['fallback_mode'] ?? null) ? $truth['fallback_mode'] : '', + 'gpu_admission_reason' => is_string($truth['gpu_admission_reason'] ?? null) ? $truth['gpu_admission_reason'] : '', + 'silent_cpu_fallback' => !empty($truth['silent_cpu_fallback']), + 'timing_available' => !empty($timing['available']), + 'generated_tokens' => is_int($timing['generated_tokens'] ?? null) ? $timing['generated_tokens'] : 0, + 'ttfb_ms' => is_int($timing['time_to_first_token_ms'] ?? null) ? $timing['time_to_first_token_ms'] : '', + 'tokens_per_second' => is_float($timing['tokens_per_second'] ?? null) || is_int($timing['tokens_per_second'] ?? null) + ? $timing['tokens_per_second'] + : '', + ]; + $fields += king_inference_runtime_decoder_truth_fields($info); + } + + king_inference_runtime_log_line('completed', $fields, $stream); +} + +function king_inference_runtime_request_payload(array $request): array +{ + $body = $request['body'] ?? null; + if (!is_string($body) || $body === '') { + return []; + } + try { + $payload = json_decode($body, true, 512, JSON_THROW_ON_ERROR); + } catch (Throwable) { + return []; + } + return is_array($payload) ? $payload : []; +} + +function king_inference_runtime_request_model(array $payload): string +{ + return is_string($payload['model'] ?? null) ? $payload['model'] : ''; +} + +function king_inference_runtime_request_has_assistant_tool_calls(array $payload): bool +{ + $messages = $payload['messages'] ?? null; + if (!is_array($messages)) { + return false; + } + foreach ($messages as $message) { + if (!is_array($message)) { + continue; + } + if (($message['role'] ?? null) === 'assistant' + && array_key_exists('tool_calls', $message) + && $message['tool_calls'] !== null + && $message['tool_calls'] !== [] + ) { + return true; + } + } + return false; +} + +function king_inference_runtime_message_content_chars(mixed $content): int +{ + if (is_string($content)) { + return strlen($content); + } + if (!is_array($content)) { + return 0; + } + + $chars = 0; + foreach ($content as $part) { + if (is_string($part)) { + $chars += strlen($part); + } else if (is_array($part) && is_string($part['text'] ?? null)) { + $chars += strlen($part['text']); + } + } + return $chars; +} + +function king_inference_runtime_payload_message_stats(array $payload): array +{ + $messages = $payload['messages'] ?? null; + $count = is_array($messages) ? count($messages) : 0; + $textChars = 0; + $lastUserChars = 0; + + if (is_array($messages)) { + foreach ($messages as $message) { + if (!is_array($message)) { + continue; + } + $chars = king_inference_runtime_message_content_chars($message['content'] ?? null); + $textChars += $chars; + if (($message['role'] ?? null) === 'user') { + $lastUserChars = $chars; + } + } + } + + return [$count, $textChars, $lastUserChars]; +} + +function king_inference_runtime_log_request_executing(array $request, array $models, mixed $stream = null): void +{ + $payload = king_inference_runtime_request_payload($request); + $tools = $payload['tools'] ?? null; + $functions = $payload['functions'] ?? null; + $body = $request['body'] ?? ''; + [$messageCount, $messageTextChars, $lastUserChars] = king_inference_runtime_payload_message_stats($payload); + + king_inference_runtime_log_line('executing', [ + 'method' => is_string($request['method'] ?? null) ? $request['method'] : '', + 'path' => is_string($request['path'] ?? null) ? $request['path'] : ($request['uri'] ?? ''), + 'requested_model' => king_inference_runtime_request_model($payload), + 'registered_models' => count($models), + 'body_bytes' => is_string($body) ? strlen($body) : 0, + 'message_count' => $messageCount, + 'message_text_chars' => $messageTextChars, + 'last_user_chars' => $lastUserChars, + 'stream' => ($payload['stream'] ?? null) === true, + 'max_tokens' => is_int($payload['max_tokens'] ?? null) ? $payload['max_tokens'] : '', + 'max_completion_tokens' => is_int($payload['max_completion_tokens'] ?? null) ? $payload['max_completion_tokens'] : '', + 'tool_schema_count' => is_array($tools) ? count($tools) : 0, + 'legacy_function_count' => is_array($functions) ? count($functions) : 0, + 'tool_choice_present' => array_key_exists('tool_choice', $payload), + 'function_call_present' => array_key_exists('function_call', $payload), + 'parallel_tool_calls_present' => array_key_exists('parallel_tool_calls', $payload), + 'assistant_tool_calls_present' => king_inference_runtime_request_has_assistant_tool_calls($payload), + 'tool_execution' => 'not_dispatched', + ], $stream); +} diff --git a/infra/inference/status.php b/infra/inference/status.php new file mode 100644 index 000000000..c89eb78da --- /dev/null +++ b/infra/inference/status.php @@ -0,0 +1,298 @@ + false, + 'exit_code' => 1, + 'reason' => 'not_evaluated', + 'refusal_reasons' => [], + 'require_gpu' => $requireGpu, + 'runtime' => [ + 'requested_profile' => '', + 'selected_profile' => '', + 'backend' => '', + 'model' => '', + 'gpu_profile_available' => false, + ], + 'model' => [ + 'loaded' => false, + 'name' => '', + 'backend' => '', + 'artifact_path' => '', + 'artifact_bytes' => 0, + 'silent_cpu_fallback' => false, + 'generation_ready' => false, + ], + 'gpu' => [ + 'present' => false, + 'backend' => '', + 'config_ready' => false, + 'generation_ready' => false, + 'thermal_monitored' => false, + 'runtime_vram_fits_free' => false, + 'system_ram_offload_allowed' => false, + 'system_ram_offload_required' => false, + 'system_ram_offload_required_mib' => null, + 'system_ram_offload_max_mb' => 0, + 'system_ram_offload_min_free_mb' => 0, + 'system_ram_offload_status' => 'unknown', + 'system_ram_offload_error' => 'unknown', + 'device_name' => '', + 'free_vram_after_reserve_gib' => null, + 'reason' => '', + 'refusal_reasons' => [], + ], + 'router' => [ + 'model_listing_ready' => false, + 'openai_chat_ready' => false, + 'gpu_generation_ready' => false, + ], + 'error' => '', +]; + +try { + $gpuStatus = king_inference_gpu_runtime_status(); + $modelConfig = king_inference_runtime_model_config(); + + $report['runtime']['requested_profile'] = king_status_string($modelConfig['runtime_requested_profile'] ?? null); + $report['runtime']['selected_profile'] = king_status_string($modelConfig['runtime_profile'] ?? null); + $report['runtime']['backend'] = king_status_string($modelConfig['backend'] ?? null); + $report['runtime']['model'] = king_status_string($modelConfig['name'] ?? null); + $report['runtime']['gpu_profile_available'] = king_status_bool($modelConfig['runtime_gpu_profile_available'] ?? false); + + $report['gpu']['present'] = true; + $report['gpu']['backend'] = king_status_string($gpuStatus['backend'] ?? null); + $report['gpu']['config_ready'] = king_status_bool($gpuStatus['config_ready'] ?? false); + $report['gpu']['generation_ready'] = king_status_bool($gpuStatus['generation_ready'] ?? false); + $report['gpu']['thermal_monitored'] = king_status_bool($gpuStatus['thermal']['monitored'] ?? false); + $report['gpu']['runtime_vram_fits_free'] = king_status_bool($gpuStatus['runtime_vram_fits_free'] ?? false); + $report['gpu']['system_ram_offload_allowed'] = king_status_bool($gpuStatus['system_ram_offload_allowed'] ?? false); + $report['gpu']['system_ram_offload_required'] = king_status_bool($gpuStatus['system_ram_offload_required'] ?? false); + $report['gpu']['system_ram_offload_required_mib'] = king_status_bytes_to_mib($gpuStatus['system_ram_offload_required_bytes'] ?? null); + $report['gpu']['system_ram_offload_max_mb'] = is_int($gpuStatus['system_ram_offload_max_mb'] ?? null) ? $gpuStatus['system_ram_offload_max_mb'] : 0; + $report['gpu']['system_ram_offload_min_free_mb'] = is_int($gpuStatus['system_ram_offload_min_free_mb'] ?? null) ? $gpuStatus['system_ram_offload_min_free_mb'] : 0; + $report['gpu']['system_ram_offload_status'] = king_status_string($gpuStatus['system_ram_offload_status'] ?? null, 'unknown'); + $report['gpu']['system_ram_offload_error'] = king_status_string($gpuStatus['system_ram_offload_error'] ?? null, 'unknown'); + $report['gpu']['device_name'] = king_status_string($gpuStatus['cuda_driver']['device_name'] ?? null); + $report['gpu']['free_vram_after_reserve_gib'] = king_status_bytes_to_gib( + $gpuStatus['free_vram_after_reserve_bytes'] ?? null + ); + $report['gpu']['reason'] = king_status_string($gpuStatus['reason'] ?? null); + $report['gpu']['refusal_reasons'] = is_array($gpuStatus['refusal_reasons'] ?? null) + ? array_values($gpuStatus['refusal_reasons']) + : []; + + if ($requireGpu && $report['runtime']['backend'] !== 'king_native_gpu') { + $report['exit_code'] = 2; + $report['reason'] = 'gpu_profile_not_selected'; + $report['refusal_reasons'] = ['gpu_profile_not_selected']; + } else { + $model = king_inference_runtime_model_load(); + $info = king_inference_model_info($model); + + $report['model']['loaded'] = true; + $report['model']['name'] = king_status_string($info['name'] ?? null); + $report['model']['backend'] = king_status_string($info['backend'] ?? null); + $report['model']['artifact_path'] = king_status_string($info['artifact_path'] ?? null); + $report['model']['artifact_bytes'] = is_int($info['artifact_bytes'] ?? null) ? $info['artifact_bytes'] : 0; + $report['model']['silent_cpu_fallback'] = king_status_bool($info['silent_cpu_fallback'] ?? false); + + if ($report['model']['backend'] === 'king_native_gpu') { + $runtime = is_array($info['gpu_runtime'] ?? null) ? $info['gpu_runtime'] : []; + $report['gpu']['present'] = true; + $report['gpu']['backend'] = king_status_string($runtime['backend'] ?? null, $report['gpu']['backend']); + $report['gpu']['config_ready'] = king_status_bool($runtime['config_ready'] ?? false); + $report['gpu']['generation_ready'] = king_status_bool($runtime['generation_ready'] ?? false); + $report['gpu']['thermal_monitored'] = king_status_bool($runtime['thermal']['monitored'] ?? false); + $report['gpu']['runtime_vram_fits_free'] = king_status_bool($runtime['runtime_vram_fits_free'] ?? false); + $report['gpu']['system_ram_offload_allowed'] = king_status_bool($runtime['system_ram_offload_allowed'] ?? false); + $report['gpu']['system_ram_offload_required'] = king_status_bool($runtime['system_ram_offload_required'] ?? false); + $report['gpu']['system_ram_offload_required_mib'] = king_status_bytes_to_mib($runtime['system_ram_offload_required_bytes'] ?? null); + $report['gpu']['system_ram_offload_max_mb'] = is_int($runtime['system_ram_offload_max_mb'] ?? null) ? $runtime['system_ram_offload_max_mb'] : 0; + $report['gpu']['system_ram_offload_min_free_mb'] = is_int($runtime['system_ram_offload_min_free_mb'] ?? null) ? $runtime['system_ram_offload_min_free_mb'] : 0; + $report['gpu']['system_ram_offload_status'] = king_status_string($runtime['system_ram_offload_status'] ?? null, 'unknown'); + $report['gpu']['system_ram_offload_error'] = king_status_string($runtime['system_ram_offload_error'] ?? null, 'unknown'); + $report['gpu']['device_name'] = king_status_string( + $runtime['cuda_driver']['device_name'] ?? null, + $report['gpu']['device_name'] + ); + $report['gpu']['free_vram_after_reserve_gib'] = king_status_bytes_to_gib( + $runtime['free_vram_after_reserve_bytes'] ?? null + ); + $report['gpu']['reason'] = king_status_string($runtime['reason'] ?? null); + $report['gpu']['refusal_reasons'] = is_array($runtime['refusal_reasons'] ?? null) + ? array_values($runtime['refusal_reasons']) + : []; + $report['model']['generation_ready'] = $report['gpu']['generation_ready']; + } else { + $report['model']['generation_ready'] = king_status_bool( + $info['openai_generation'] ?? ($info['token_generation_ready'] ?? false) + ); + } + + $response = king_inference_openai_http_response( + [$report['model']['name'] => $model], + ['method' => 'GET', 'path' => '/v1/models'] + ); + $payload = json_decode($response['body'], true, flags: JSON_THROW_ON_ERROR); + $listed = $payload['data'][0]['x_king'] ?? []; + $client = is_array($listed['client_capabilities'] ?? null) ? $listed['client_capabilities'] : []; + + $report['router']['model_listing_ready'] = ($response['status'] ?? 0) === 200; + $report['router']['openai_chat_ready'] = king_status_bool($client['openai_chat_completions'] ?? false); + $report['router']['gpu_generation_ready'] = king_status_bool($client['gpu_generation_ready'] ?? false); + + if ($report['model']['backend'] === 'king_native_gpu') { + $ready = $report['model']['generation_ready'] + && $report['router']['gpu_generation_ready'] + && !$report['model']['silent_cpu_fallback']; + } else { + $ready = $report['model']['generation_ready'] + && $report['router']['openai_chat_ready'] + && !$report['model']['silent_cpu_fallback']; + } + + $report['ready'] = $ready; + $report['exit_code'] = $ready ? 0 : 3; + $report['reason'] = $ready ? 'ready' : ($report['gpu']['reason'] !== '' ? $report['gpu']['reason'] : 'generation_not_ready'); + $report['refusal_reasons'] = $ready ? [] : ( + $report['gpu']['refusal_reasons'] !== [] ? $report['gpu']['refusal_reasons'] : [$report['reason']] + ); + } +} catch (Throwable $exception) { + $report['ready'] = false; + $report['exit_code'] = 1; + $report['reason'] = 'runtime_error'; + $report['refusal_reasons'] = ['runtime_error']; + $report['error'] = $exception::class . ': ' . $exception->getMessage(); +} + +if ($jsonOutput) { + fwrite(STDOUT, json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"); +} else { + king_status_report_human($report); +} + +exit($report['exit_code']); diff --git a/infra/inference/verification-matrix.php b/infra/inference/verification-matrix.php new file mode 100644 index 000000000..95b135a09 --- /dev/null +++ b/infra/inference/verification-matrix.php @@ -0,0 +1,621 @@ + '--gpu-model', 'path' => (string) ($options['gpu_model'] ?? '')]; + } else { + $candidates[] = ['source' => '--cpu-model', 'path' => (string) ($options['cpu_model'] ?? '')]; + } + $candidates[] = ['source' => '--model', 'path' => (string) ($options['model'] ?? '')]; + + foreach ($names as $name) { + $value = getenv($name); + $candidates[] = ['source' => $name, 'path' => is_string($value) ? $value : '']; + } + + $default = $root . '/var/inference-models/gemma3-1b.gguf'; + if (!king_verify_bool_env('KING_INFERENCE_VERIFY_DISABLE_DEFAULT_MODEL')) { + $candidates[] = ['source' => 'default:' . $default, 'path' => $default]; + } + + foreach ($candidates as $candidate) { + $path = $candidate['path']; + if ($path !== '' && is_file($path) && is_readable($path)) { + $real = realpath($path); + return [ + 'available' => true, + 'path' => $path, + 'real_path' => $real !== false ? $real : $path, + 'source' => $candidate['source'], + 'bytes' => filesize($path), + 'sha256' => hash_file('sha256', $path), + ]; + } + } + + return [ + 'available' => false, + 'path' => '', + 'source' => '', + 'needed' => implode(' or ', array_merge( + $kind === 'gpu' ? ['--gpu-model'] : ['--cpu-model'], + ['--model'], + $names, + ['readable ' . $default . ' unless KING_INFERENCE_VERIFY_DISABLE_DEFAULT_MODEL=1'] + )), + ]; +} + +function king_verify_skip(string $name, string $reason, array $meta = []): array +{ + return ['name' => $name, 'status' => 'skipped', 'reason' => $reason] + $meta; +} + +function king_verify_pass(string $name, array $meta = []): array +{ + return ['name' => $name, 'status' => 'passed', 'reason' => 'ok'] + $meta; +} + +function king_verify_fail(string $name, Throwable $e, array $meta = []): array +{ + return [ + 'name' => $name, + 'status' => 'failed', + 'reason' => $e::class . ': ' . $e->getMessage(), + ] + $meta; +} + +function king_verify_cpu_model(array $path): King\Inference\Model +{ + return king_inference_model_load([ + 'name' => 'verify-cpu', + 'artifact' => ['path' => $path['path']], + 'backend' => 'king_native_cpu', + 'context_tokens' => 2048, + 'with_memory' => false, + ]); +} + +function king_verify_gpu_model(array $path): King\Inference\Model +{ + return king_inference_model_load([ + 'name' => 'verify-gpu', + 'artifact' => ['path' => $path['path']], + 'backend' => 'king_native_gpu', + 'context_tokens' => 2048, + 'with_memory' => false, + 'gpu' => [ + 'enabled' => true, + 'max_gpu_layers' => 99, + 'vram_reserve_mb' => 1024, + 'min_free_vram_mb' => 1024, + 'thermal' => [ + 'sensor_command' => 'nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader,nounits', + 'max_temperature_c' => 78.0, + 'check_interval_seconds' => 15, + 'allow_unmonitored_gpu' => true, + ], + ], + ]); +} + +function king_verify_graph_options(King\Inference\Model $model): array +{ + $info = $model->info(); + $gguf = is_array($info['gguf'] ?? null) ? $info['gguf'] : []; + $vocab = max(1, (int) ($gguf['tokenizer_token_count'] ?? 262144)); + $hidden = max(1, (int) ($gguf['embedding_length'] ?? 1152)); + return [ + 'max_vector_values' => max($vocab, 262144), + 'max_operations' => max($vocab * $hidden + 1024, 400000000), + 'return_outputs' => false, + ]; +} + +function king_verify_drain_text(King\Inference\Stream $stream, int $timeoutMs = 1000): array +{ + $text = ''; + $types = []; + $events = 0; + while (($event = king_inference_next($stream, $timeoutMs)) !== null) { + if (!is_array($event)) { + continue; + } + $events++; + $type = (string) ($event['type'] ?? ''); + $types[] = $type; + if ($type === 'token' && is_string($event['text'] ?? null)) { + $text .= $event['text']; + } + if ($type === 'done' || $type === 'cancelled') { + break; + } + } + return ['text' => $text, 'types' => $types, 'events' => $events, 'metrics' => $stream->getMetrics()]; +} + +function king_verify_roundtrip_graphs(King\Inference\Model $model, string $text): array +{ + $encoded = king_inference_tokenize($model, $text); + $tokens = is_array($encoded['tokens'] ?? null) ? $encoded['tokens'] : []; + if ($tokens === []) { + throw new RuntimeException('tokenizer produced no tokens'); + } + $graphs = []; + foreach ($tokens as $tokenId) { + $graphs[] = [ + 'inputs' => ['token' => [(int) $tokenId]], + 'ops' => [[ + 'id' => 'next_token', + 'op' => 'scale', + 'input' => 'token', + 'factor' => 1.0, + ]], + 'output' => 'next_token', + ]; + } + return $graphs; +} + +function king_verify_openai_content(array $response): string +{ + if (($response['status'] ?? 0) !== 200) { + throw new RuntimeException('OpenAI response status ' . (string) ($response['status'] ?? 0)); + } + $payload = json_decode((string) ($response['body'] ?? ''), true); + if (!is_array($payload)) { + throw new RuntimeException('OpenAI response body is not JSON'); + } + $content = $payload['choices'][0]['message']['content'] ?? null; + if (!is_string($content)) { + throw new RuntimeException('OpenAI response has no assistant content'); + } + return $content; +} + +function king_verify_gate_gguf(array $cpuPath): array +{ + if (!$cpuPath['available']) { + return king_verify_skip('gguf_load', 'missing_model_path', ['needed' => $cpuPath['needed']]); + } + try { + $model = king_verify_cpu_model($cpuPath); + $info = $model->info(); + if (($info['gguf']['header_loaded'] ?? false) !== true || (int) ($info['gguf']['tensor_count'] ?? 0) <= 0) { + throw new RuntimeException('GGUF header or tensor directory was not loaded'); + } + return king_verify_pass('gguf_load', [ + 'model_path' => $cpuPath, + 'tensor_count' => $info['gguf']['tensor_count'] ?? null, + 'architecture' => $info['gguf']['architecture'] ?? null, + ]); + } catch (Throwable $e) { + return king_verify_fail('gguf_load', $e, ['model_path' => $cpuPath]); + } +} + +function king_verify_gate_tokenizer(array $cpuPath): array +{ + if (!$cpuPath['available']) { + return king_verify_skip('tokenizer', 'missing_model_path', ['needed' => $cpuPath['needed']]); + } + try { + $model = king_verify_cpu_model($cpuPath); + $encoded = king_inference_tokenize($model, 'Hello world'); + if ((int) ($encoded['token_count'] ?? 0) <= 0 || (int) ($encoded['unknown_count'] ?? 0) !== 0) { + throw new RuntimeException('tokenizer did not produce clean token ids'); + } + $stream = king_inference_stream($model, ['graphs' => king_verify_roundtrip_graphs($model, 'Hello world')], [ + 'with_memory' => false, + 'max_native_stream_tokens' => 16, + ]); + $result = king_verify_drain_text($stream); + if (trim($result['text']) !== 'Hello world') { + throw new RuntimeException('token roundtrip mismatch'); + } + return king_verify_pass('tokenizer', [ + 'model_path' => $cpuPath, + 'token_count' => $encoded['token_count'] ?? null, + 'events' => $result['events'], + ]); + } catch (Throwable $e) { + return king_verify_fail('tokenizer', $e, ['model_path' => $cpuPath]); + } +} + +function king_verify_gate_cpu_reference(array $cpuPath): array +{ + if (!$cpuPath['available']) { + return king_verify_skip('cpu_reference', 'missing_model_path', ['needed' => $cpuPath['needed']]); + } + try { + $model = king_verify_cpu_model($cpuPath); + $encoded = king_inference_tokenize($model, 'Hello'); + $graph = king_inference_token_decode_graph($model, $encoded, 0, [ + 'emit_token' => false, + 'emit_logits' => true, + ]); + $result = king_inference_graph_run($model, $graph, king_verify_graph_options($model)); + $logits = $result['final']['logits'] ?? null; + if (!is_array($logits) || (int) ($logits['length'] ?? 0) <= 0) { + throw new RuntimeException('CPU reference produced no logits'); + } + if (!is_array($result['state']['kv_cache'] ?? null) || count($result['state']['kv_cache']) === 0) { + throw new RuntimeException('CPU reference produced no KV state'); + } + return king_verify_pass('cpu_reference', [ + 'model_path' => $cpuPath, + 'op_count' => $result['op_count'] ?? null, + 'logit_count' => $logits['length'] ?? null, + ]); + } catch (Throwable $e) { + return king_verify_fail('cpu_reference', $e, ['model_path' => $cpuPath]); + } +} + +function king_verify_gate_gpu_smoke(array $gpuPath): array +{ + if (!$gpuPath['available']) { + return king_verify_skip('gpu_smoke', 'missing_model_path', ['needed' => $gpuPath['needed']]); + } + if (trim((string) shell_exec('command -v nvidia-smi 2>/dev/null')) === '') { + return king_verify_skip('gpu_smoke', 'missing_gpu_runtime', ['needed' => 'nvidia-smi and CUDA runtime']); + } + try { + $model = king_verify_gpu_model($gpuPath); + $info = $model->info(); + if (($info['runtime_truth']['silent_cpu_fallback'] ?? true) !== false) { + throw new RuntimeException('GPU smoke detected silent CPU fallback'); + } + $stream = king_inference_stream($model, ['graphs' => king_verify_roundtrip_graphs($model, 'Hello world')], [ + 'with_memory' => false, + 'max_native_stream_tokens' => 16, + ]); + $result = king_verify_drain_text($stream); + if (trim($result['text']) !== 'Hello world') { + throw new RuntimeException('GPU token-vector roundtrip mismatch'); + } + return king_verify_pass('gpu_smoke', [ + 'model_path' => $gpuPath, + 'backend' => $info['backend'] ?? null, + 'active_device' => $info['runtime_truth']['active_device'] ?? null, + 'events' => $result['events'], + ]); + } catch (Throwable $e) { + return king_verify_fail('gpu_smoke', $e, ['model_path' => $gpuPath]); + } +} + +function king_verify_gate_cpu_gpu_match(array $cpuPath, array $gpuPath): array +{ + if (!$cpuPath['available']) { + return king_verify_skip('cpu_gpu_match', 'missing_cpu_model_path', ['needed' => $cpuPath['needed']]); + } + if (!$gpuPath['available']) { + return king_verify_skip('cpu_gpu_match', 'missing_gpu_model_path', ['needed' => $gpuPath['needed']]); + } + if (trim((string) shell_exec('command -v nvidia-smi 2>/dev/null')) === '') { + return king_verify_skip('cpu_gpu_match', 'missing_gpu_runtime', ['needed' => 'nvidia-smi and CUDA runtime']); + } + + try { + $text = 'Hello world'; + $cpuModel = king_verify_cpu_model($cpuPath); + $cpuStream = king_inference_stream($cpuModel, ['graphs' => king_verify_roundtrip_graphs($cpuModel, $text)], [ + 'with_memory' => false, + 'max_native_stream_tokens' => 16, + ]); + $cpu = king_verify_drain_text($cpuStream); + + $gpuModel = king_verify_gpu_model($gpuPath); + $gpuInfo = $gpuModel->info(); + if (($gpuInfo['runtime_truth']['silent_cpu_fallback'] ?? true) !== false) { + throw new RuntimeException('GPU side used silent CPU fallback'); + } + $gpuStream = king_inference_stream($gpuModel, ['graphs' => king_verify_roundtrip_graphs($gpuModel, $text)], [ + 'with_memory' => false, + 'max_native_stream_tokens' => 16, + ]); + $gpu = king_verify_drain_text($gpuStream); + + if (trim($cpu['text']) !== $text || trim($gpu['text']) !== $text || trim($cpu['text']) !== trim($gpu['text'])) { + throw new RuntimeException('CPU/GPU token-vector roundtrip output mismatch'); + } + + return king_verify_pass('cpu_gpu_match', [ + 'cpu_model_path' => $cpuPath, + 'gpu_model_path' => $gpuPath, + 'active_device' => $gpuInfo['runtime_truth']['active_device'] ?? null, + 'cpu_events' => $cpu['events'], + 'gpu_events' => $gpu['events'], + ]); + } catch (Throwable $e) { + return king_verify_fail('cpu_gpu_match', $e, ['cpu_model_path' => $cpuPath, 'gpu_model_path' => $gpuPath]); + } +} + +function king_verify_gate_openai(array $cpuPath): array +{ + if (!$cpuPath['available']) { + return king_verify_skip('openai_route_smoke', 'missing_model_path', ['needed' => $cpuPath['needed']]); + } + try { + $model = king_verify_cpu_model($cpuPath); + $response = king_inference_openai_http_response(['verify-cpu' => $model], [ + 'method' => 'POST', + 'path' => '/v1/chat/completions', + 'body' => json_encode([ + 'model' => 'verify-cpu', + 'messages' => [['role' => 'user', 'content' => 'Say hello.']], + 'max_tokens' => 1, + 'temperature' => 0, + ], JSON_UNESCAPED_SLASHES), + ], ['read_timeout_ms' => 250, 'max_events' => 64, 'max_idle_events' => 16]); + $content = king_verify_openai_content($response); + if (trim($content) === '') { + throw new RuntimeException('OpenAI route emitted empty content'); + } + return king_verify_pass('openai_route_smoke', ['model_path' => $cpuPath, 'content_bytes' => strlen($content)]); + } catch (Throwable $e) { + return king_verify_fail('openai_route_smoke', $e, ['model_path' => $cpuPath]); + } +} + +function king_verify_gate_long_prompt(array $cpuPath): array +{ + if (!$cpuPath['available']) { + return king_verify_skip('long_prompt', 'missing_model_path', ['needed' => $cpuPath['needed']]); + } + try { + $model = king_verify_cpu_model($cpuPath); + $words = []; + for ($i = 0; $i < 384; $i++) { + $words[] = 'context' . ($i % 31); + } + $prompt = implode(' ', $words) . '. Answer with one token.'; + $response = king_inference_openai_http_response(['verify-cpu' => $model], [ + 'method' => 'POST', + 'path' => '/v1/chat/completions', + 'body' => json_encode([ + 'model' => 'verify-cpu', + 'messages' => [['role' => 'user', 'content' => $prompt]], + 'max_tokens' => 1, + 'temperature' => 0, + ], JSON_UNESCAPED_SLASHES), + ], ['read_timeout_ms' => 500, 'max_events' => 128, 'max_idle_events' => 32]); + $content = king_verify_openai_content($response); + if (trim($content) === '') { + throw new RuntimeException('long prompt emitted empty content'); + } + return king_verify_pass('long_prompt', ['model_path' => $cpuPath, 'prompt_words' => count($words)]); + } catch (Throwable $e) { + return king_verify_fail('long_prompt', $e, ['model_path' => $cpuPath]); + } +} + +function king_verify_gate_stop(array $cpuPath): array +{ + if (!$cpuPath['available']) { + return king_verify_skip('stop_tokens', 'missing_model_path', ['needed' => $cpuPath['needed']]); + } + try { + $model = king_verify_cpu_model($cpuPath); + $response = king_inference_openai_http_response(['verify-cpu' => $model], [ + 'method' => 'POST', + 'path' => '/v1/chat/completions', + 'body' => json_encode([ + 'model' => 'verify-cpu', + 'messages' => [['role' => 'user', 'content' => 'Write alpha STOP_HERE beta.']], + 'max_tokens' => 16, + 'temperature' => 0, + 'stop' => ['STOP_HERE'], + ], JSON_UNESCAPED_SLASHES), + ], ['read_timeout_ms' => 250, 'max_events' => 64, 'max_idle_events' => 16]); + $content = king_verify_openai_content($response); + if (str_contains($content, 'STOP_HERE')) { + throw new RuntimeException('stop sequence leaked into assistant content'); + } + return king_verify_pass('stop_tokens', ['model_path' => $cpuPath, 'content_bytes' => strlen($content)]); + } catch (Throwable $e) { + return king_verify_fail('stop_tokens', $e, ['model_path' => $cpuPath]); + } +} + +function king_verify_gate_cancellation(array $cpuPath): array +{ + if (!$cpuPath['available']) { + return king_verify_skip('cancellation', 'missing_model_path', ['needed' => $cpuPath['needed']]); + } + try { + $model = king_verify_cpu_model($cpuPath); + $stream = king_inference_stream($model, ['graphs' => king_verify_roundtrip_graphs($model, 'Hello world')], [ + 'with_memory' => false, + 'max_native_stream_tokens' => 16, + ]); + $first = king_inference_next($stream, 1000); + if (!is_array($first) || ($first['type'] ?? null) !== 'start') { + throw new RuntimeException('stream did not start before cancellation'); + } + if (!king_inference_cancel($stream)) { + throw new RuntimeException('king_inference_cancel returned false'); + } + $next = king_inference_next($stream, 1000); + $metrics = $stream->getMetrics(); + if (($metrics['cancelled'] ?? false) !== true && (!is_array($next) || ($next['type'] ?? null) !== 'cancelled')) { + throw new RuntimeException('stream did not report cancelled state'); + } + return king_verify_pass('cancellation', [ + 'model_path' => $cpuPath, + 'next_type' => is_array($next) ? ($next['type'] ?? null) : null, + ]); + } catch (Throwable $e) { + return king_verify_fail('cancellation', $e, ['model_path' => $cpuPath]); + } +} + +function king_verify_gate_error_taxonomy(): array +{ + try { + $response = king_inference_openai_http_response([], [ + 'method' => 'POST', + 'path' => '/v1/chat/completions', + 'body' => '{', + ], []); + if (($response['status'] ?? 0) < 400) { + throw new RuntimeException('invalid request did not return an error status'); + } + $payload = json_decode((string) ($response['body'] ?? ''), true); + if (!is_array($payload)) { + throw new RuntimeException('error response body is not JSON'); + } + $error = is_array($payload['error'] ?? null) ? $payload['error'] : []; + $code = $error['code'] ?? null; + $king = is_array($error['x_king'] ?? null) ? $error['x_king'] : []; + if (!is_string($code) || !str_starts_with($code, 'king.')) { + throw new RuntimeException('error response did not expose a stable King error code'); + } + if (($king['prompt_included'] ?? true) !== false || ($king['request_body_included'] ?? true) !== false) { + throw new RuntimeException('error taxonomy leaked prompt or request body'); + } + + return king_verify_pass('error_taxonomy', [ + 'status' => $response['status'] ?? null, + 'code' => $code, + 'category' => $king['category'] ?? null, + ]); + } catch (Throwable $e) { + return king_verify_fail('error_taxonomy', $e); + } +} + +if (king_verify_flag($argv, '--help') || king_verify_flag($argv, '-h')) { + king_verify_usage(); +} + +$options = [ + 'json' => king_verify_flag($argv, '--json'), + 'strict_skips' => king_verify_flag($argv, '--strict-skips') || king_verify_bool_env('KING_INFERENCE_VERIFY_STRICT_SKIPS'), + 'model' => king_verify_option($argv, '--model', ''), + 'cpu_model' => king_verify_option($argv, '--cpu-model', ''), + 'gpu_model' => king_verify_option($argv, '--gpu-model', ''), +]; +$gateCsv = king_verify_option($argv, '--gates', getenv('KING_INFERENCE_VERIFY_GATES') ?: ''); +$selected = $gateCsv !== '' ? array_flip(array_filter(array_map('trim', explode(',', $gateCsv)))) : null; +$includeExpensive = king_verify_bool_env('KING_INFERENCE_VERIFY_INCLUDE_EXPENSIVE'); + +$cpuPath = king_verify_model_path($options, 'cpu'); +$gpuPath = king_verify_model_path($options, 'gpu'); +$expensiveGates = array_fill_keys(['cpu_reference', 'long_prompt'], true); +$gates = [ + 'tokenizer' => static fn (): array => king_verify_gate_tokenizer($cpuPath), + 'gguf_load' => static fn (): array => king_verify_gate_gguf($cpuPath), + 'cpu_reference' => static fn (): array => king_verify_gate_cpu_reference($cpuPath), + 'gpu_smoke' => static fn (): array => king_verify_gate_gpu_smoke($gpuPath), + 'cpu_gpu_match' => static fn (): array => king_verify_gate_cpu_gpu_match($cpuPath, $gpuPath), + 'openai_route_smoke' => static fn (): array => king_verify_gate_openai($cpuPath), + 'long_prompt' => static fn (): array => king_verify_gate_long_prompt($cpuPath), + 'stop_tokens' => static fn (): array => king_verify_gate_stop($cpuPath), + 'cancellation' => static fn (): array => king_verify_gate_cancellation($cpuPath), + 'error_taxonomy' => static fn (): array => king_verify_gate_error_taxonomy(), +]; + +$results = []; +foreach ($gates as $name => $runner) { + if ($selected !== null && !array_key_exists($name, $selected)) { + continue; + } + if ($selected === null && !$includeExpensive && isset($expensiveGates[$name]) && $cpuPath['available']) { + $results[] = king_verify_skip($name, 'expensive_gate_not_requested', [ + 'needed' => '--gates=' . $name . ' or KING_INFERENCE_VERIFY_INCLUDE_EXPENSIVE=1', + 'model_path' => $cpuPath, + ]); + continue; + } + $results[] = $runner(); +} + +$counts = ['passed' => 0, 'failed' => 0, 'skipped' => 0]; +foreach ($results as $result) { + $status = (string) ($result['status'] ?? 'failed'); + $counts[$status] = ($counts[$status] ?? 0) + 1; +} + +$report = [ + 'schema_version' => 1, + 'generated_at' => date(DATE_ATOM), + 'default_ci_downloads_models' => false, + 'cpu_model_path' => $cpuPath, + 'gpu_model_path' => $gpuPath, + 'counts' => $counts, + 'gates' => $results, +]; + +$exit = $counts['failed'] > 0 || ($options['strict_skips'] && $counts['skipped'] > 0) ? 1 : 0; +if ($options['json']) { + echo json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; + exit($exit); +} + +foreach ($results as $result) { + echo ($result['status'] ?? 'failed') . ' ' . ($result['name'] ?? '?') . ': ' . ($result['reason'] ?? '') . "\n"; + if (($result['status'] ?? '') === 'skipped' && isset($result['needed'])) { + echo ' needed: ' . $result['needed'] . "\n"; + } +} +echo 'summary passed=' . $counts['passed'] . ' skipped=' . $counts['skipped'] . ' failed=' . $counts['failed'] . "\n"; +exit($exit); diff --git a/infra/scripts/build-profile.sh b/infra/scripts/build-profile.sh index 62562f14d..5828de332 100755 --- a/infra/scripts/build-profile.sh +++ b/infra/scripts/build-profile.sh @@ -32,6 +32,10 @@ Environment variables: Explicit OpenSSL compile flags. Appended before configure. KING_OPENSSL_LIBS Explicit OpenSSL linker flags. Appended before configure. + PHP_BIN PHP binary used by matching smoke scripts. Also used to infer + phpizeX.Y/php-configX.Y when PHPIZE/PHP_CONFIG are unset. + PHPIZE phpize binary to use for this profile build. + PHP_CONFIG php-config binary to pass to configure. EOF } @@ -142,6 +146,7 @@ BASE_CPPFLAGS="${CPPFLAGS:-}" BASE_LDFLAGS="${LDFLAGS:-}" BASE_CC="${CC:-}" BASE_CXX="${CXX:-}" +PHP_BIN="${PHP_BIN:-php}" profile_cc="" profile_cxx="" @@ -150,6 +155,8 @@ profile_cppflags="${BASE_CPPFLAGS}" profile_ldflags="${BASE_LDFLAGS}" sanitizer_kind="" lsquic_runtime_prefix="${KING_LSQUIC_RUNTIME_PREFIX:-}" +phpize_bin="" +php_config_bin="" declare -a PHPIZE_GENERATED_RELATIVE_PATHS=() declare -a CONFIGURE_ENV=() PHPIZE_SNAPSHOT_DIR="" @@ -162,6 +169,82 @@ trim_ascii_whitespace() { printf '%s\n' "${value}" } +php_binary_suffix() { + local binary_name="" + + binary_name="$(basename "${PHP_BIN}")" + case "${binary_name}" in + php[0-9].[0-9]) + printf '%s\n' "${binary_name#php}" + return 0 + ;; + esac + + return 1 +} + +php_runtime_api() { + "${PHP_BIN}" -n -i 2>/dev/null | awk -F'=> ' '/^PHP API/ { print $2; exit }' +} + +phpize_api() { + "${phpize_bin}" -v 2>/dev/null | awk -F': *' '/^PHP Api Version/ { print $2; exit }' +} + +resolve_php_toolchain() { + local suffix="" + local runtime_api="" + local config_api="" + local phpize_reported_api="" + + phpize_bin="${PHPIZE:-}" + php_config_bin="${PHP_CONFIG:-}" + + if [[ -z "${phpize_bin}" || -z "${php_config_bin}" ]]; then + suffix="$(php_binary_suffix || true)" + if [[ -n "${suffix}" ]]; then + if [[ -z "${phpize_bin}" ]] && command -v "phpize${suffix}" >/dev/null 2>&1; then + phpize_bin="phpize${suffix}" + fi + if [[ -z "${php_config_bin}" ]] && command -v "php-config${suffix}" >/dev/null 2>&1; then + php_config_bin="php-config${suffix}" + fi + fi + fi + + phpize_bin="${phpize_bin:-phpize}" + php_config_bin="${php_config_bin:-php-config}" + + if ! command -v "${phpize_bin}" >/dev/null 2>&1; then + echo "Missing phpize binary: ${phpize_bin}" >&2 + exit 1 + fi + + if ! command -v "${php_config_bin}" >/dev/null 2>&1; then + echo "Missing php-config binary: ${php_config_bin}" >&2 + exit 1 + fi + + if ! command -v "${PHP_BIN}" >/dev/null 2>&1; then + echo "Missing PHP binary: ${PHP_BIN}" >&2 + exit 1 + fi + + runtime_api="$(php_runtime_api || true)" + config_api="$("${php_config_bin}" --phpapi 2>/dev/null || true)" + phpize_reported_api="$(phpize_api || true)" + + if [[ -n "${runtime_api}" && -n "${config_api}" && -n "${phpize_reported_api}" + && ( "${runtime_api}" != "${config_api}" || "${runtime_api}" != "${phpize_reported_api}" ) ]]; then + { + echo "Warning: PHP toolchain API mismatch:" + echo " ${PHP_BIN}: ${runtime_api}" + echo " ${php_config_bin}: ${config_api}" + echo " ${phpize_bin}: ${phpize_reported_api}" + } >&2 + fi +} + load_phpize_generated_relative_paths() { local raw_line="" local normalized_line="" @@ -482,6 +565,11 @@ echo "Building King profile: ${PROFILE}" echo "Compiler: ${profile_cc}" echo "Jobs: ${JOBS}" +resolve_php_toolchain +echo "PHP binary: ${PHP_BIN}" +echo "phpize: ${phpize_bin}" +echo "php-config: ${php_config_bin}" + load_phpize_generated_relative_paths snapshot_phpize_generated_files trap restore_phpize_generated_files EXIT @@ -492,8 +580,8 @@ if [[ -f Makefile ]]; then make clean >/dev/null 2>&1 || true fi -phpize --clean >/dev/null 2>&1 || true -phpize +"${phpize_bin}" --clean >/dev/null 2>&1 || true +"${phpize_bin}" CONFIGURE_ENV=( CC="${profile_cc}" @@ -501,6 +589,7 @@ CONFIGURE_ENV=( CFLAGS="${profile_cflags}" CPPFLAGS="${profile_cppflags}" LDFLAGS="${profile_ldflags}" + PHP_CONFIG="${php_config_bin}" ) if [[ "$(host_os)" == "darwin" ]]; then @@ -519,7 +608,7 @@ if [[ -n "${lsquic_runtime_prefix}" ]]; then ) fi -env "${CONFIGURE_ENV[@]}" ./configure --enable-king +env "${CONFIGURE_ENV[@]}" ./configure --enable-king --with-php-config="${php_config_bin}" patch_generated_libtool_for_host make -j"${JOBS}" diff --git a/infra/scripts/check-inference-port-exposure.sh b/infra/scripts/check-inference-port-exposure.sh new file mode 100755 index 000000000..cc4a66a4d --- /dev/null +++ b/infra/scripts/check-inference-port-exposure.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: ./infra/scripts/check-inference-port-exposure.sh [--ports CSV] + +Fails when a configured King inference/OpenAI-compatible port is listening on +a public interface. Loopback-only listeners are accepted; no listener is also +accepted. + +Environment variables: + KING_OPENAI_PORT Added to the checked port list when set. +USAGE +} + +if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then + usage + exit 0 +fi + +PORTS="${KING_OPENAI_PORT:-8080}" +while [[ "$#" -gt 0 ]]; do + case "$1" in + --ports) + PORTS="${2:-}" + shift 2 + ;; + *) + echo "Unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +if [[ -n "${KING_OPENAI_PORT:-}" && ",${PORTS}," != *",${KING_OPENAI_PORT},"* ]]; then + PORTS="${PORTS},${KING_OPENAI_PORT}" +fi + +socket_table() { + if command -v ss >/dev/null 2>&1; then + ss -H -ltn + return + fi + if command -v netstat >/dev/null 2>&1; then + netstat -ltn | awk 'NR > 2 {print}' + return + fi + + echo "ss or netstat is required to inspect listening ports." >&2 + exit 1 +} + +normalize_ports() { + tr ',' '\n' <<<"${PORTS}" \ + | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' \ + | awk '/^[0-9]+$/ && $1 > 0 && $1 <= 65535 {seen[$1]=1} END {for (port in seen) print port}' \ + | LC_ALL=C sort -n +} + +is_loopback_address() { + local address="$1" + + address="${address#[}" + address="${address%]}" + case "${address}" in + 127.*|::1|localhost) + return 0 + ;; + esac + return 1 +} + +local_address_from_line() { + awk '{print $4}' <<<"$1" +} + +address_without_port() { + local endpoint="$1" + + endpoint="${endpoint%:*}" + endpoint="${endpoint#[}" + endpoint="${endpoint%]}" + printf '%s\n' "${endpoint}" +} + +port_matches_line() { + local line="$1" + local port="$2" + local endpoint="" + + endpoint="$(local_address_from_line "${line}")" + [[ "${endpoint}" == *":${port}" ]] +} + +SOCKETS="$(socket_table)" +FAIL=0 + +while IFS= read -r port; do + [[ -z "${port}" ]] && continue + MATCHED=0 + while IFS= read -r line; do + [[ -z "${line}" ]] && continue + if ! port_matches_line "${line}" "${port}"; then + continue + fi + + MATCHED=1 + endpoint="$(local_address_from_line "${line}")" + address="$(address_without_port "${endpoint}")" + case "${address}" in + 0.0.0.0|::|'*') + echo "Public inference port exposure detected on ${endpoint}: ${line}" >&2 + FAIL=1 + ;; + *) + if is_loopback_address "${address}"; then + echo "Loopback-only inference listener accepted on ${endpoint}." + else + echo "Non-loopback inference listener detected on ${endpoint}: ${line}" >&2 + FAIL=1 + fi + ;; + esac + done <<<"${SOCKETS}" + + if [[ "${MATCHED}" == "0" ]]; then + echo "No listener on inference port ${port}." + fi +done < <(normalize_ports) + +exit "${FAIL}" diff --git a/infra/scripts/check-persistence-migration.sh b/infra/scripts/check-persistence-migration.sh index fa81c8205..14c9254e7 100755 --- a/infra/scripts/check-persistence-migration.sh +++ b/infra/scripts/check-persistence-migration.sh @@ -23,11 +23,13 @@ PHP_BIN="${PHP_BIN:-php}" ARTIFACTS_DIR="" SCRATCH_DIR="" PREVIOUS_WORKTREE="" +BASELINE_REF="" SEMANTIC_DNS_STATE_DIR="/tmp/king_semantic_dns_state" SEMANTIC_DNS_STATE_FILE="${SEMANTIC_DNS_STATE_DIR}/durable_state.bin" SEMANTIC_DNS_STATE_DIR_EXISTED=0 SEMANTIC_DNS_BACKUP_FILE="" +SEMANTIC_DNS_COMPAT_LOCK="/tmp/king_semantic_dns_release_compat.lock" resolve_existing_path() { local candidate="$1" @@ -58,6 +60,10 @@ resolve_path_for_output() { } restore_semantic_dns_state() { + if [[ -z "${SEMANTIC_DNS_STATE_DIR}" || -z "${SEMANTIC_DNS_STATE_FILE}" ]]; then + return 0 + fi + if [[ -n "${SEMANTIC_DNS_BACKUP_FILE}" && -f "${SEMANTIC_DNS_BACKUP_FILE}" ]]; then mkdir -p "${SEMANTIC_DNS_STATE_DIR}" chmod 0700 "${SEMANTIC_DNS_STATE_DIR}" @@ -174,6 +180,8 @@ OBJECT_STORE_ROOT="${ARTIFACTS_DIR}/persisted-object-store" ORCHESTRATOR_STATE_PATH="${ARTIFACTS_DIR}/orchestrator-state.bin" mkdir -p "${PREVIOUS_BUILD_DIR}" "${CURRENT_BUILD_DIR}" +exec 9>"${SEMANTIC_DNS_COMPAT_LOCK}" +flock 9 if [[ -d "${SEMANTIC_DNS_STATE_DIR}" ]]; then SEMANTIC_DNS_STATE_DIR_EXISTED=1 fi @@ -185,6 +193,20 @@ mkdir -p "${SEMANTIC_DNS_STATE_DIR}" chmod 0700 "${SEMANTIC_DNS_STATE_DIR}" rm -f "${SEMANTIC_DNS_STATE_FILE}" +prepare_legacy_packaging_tree() { + local tree_root="$1" + local package_script="${tree_root}/infra/scripts/package-release.sh" + local constants_header="${tree_root}/extension/include/php_king/constants.h" + + if [[ ! -f "${package_script}" || ! -f "${constants_header}" ]]; then + return 0 + fi + + sed -i \ + -e 's#${EXT_DIR}/include/php_king.h#${EXT_DIR}/include/php_king/constants.h#g' \ + "${package_script}" +} + package_tree() { local tree_root="$1" local output_dir="$2" @@ -212,7 +234,7 @@ package_tree() { echo "Last 40 log lines from ${log_path}:" >&2 tail -n 40 "${log_path}" >&2 fi - exit "${package_status}" + return "${package_status}" fi archive_path="$( @@ -236,12 +258,12 @@ package_tree() { if [[ -z "${archive_path}" ]]; then echo "Failed to resolve package archive from ${tree_root}." >&2 - exit 1 + return 1 fi resolve_existing_path "${archive_path}" || { echo "Resolved package archive path does not exist: ${archive_path}" >&2 - exit 1 + return 1 } } @@ -258,10 +280,61 @@ verify_archive() { ( cd "${ROOT_DIR}" - PHP_BIN="${PHP_BIN}" "${verify_args[@]}" + KING_SEMANTIC_DNS_STATE_PATH="${SEMANTIC_DNS_STATE_FILE}" \ + PHP_BIN="${PHP_BIN}" "${verify_args[@]}" ) 2>&1 | tee "${log_path}" } +prepare_previous_archive() { + local candidate_ref="$1" + local package_log="" + local verify_log="" + local next_candidate="" + local attempt=0 + local short_ref="" + + while [[ -n "${candidate_ref}" ]]; do + short_ref="$(git -C "${ROOT_DIR}" rev-parse --short "${candidate_ref}")" + package_log="${PREVIOUS_BUILD_DIR}/package-${short_ref}.log" + verify_log="${PREVIOUS_BUILD_DIR}/verify-${short_ref}.log" + + printf 'Preparing previous release archive from %s\n' "${candidate_ref}" + PREVIOUS_WORKTREE="$(mktemp -d)" + rm -rf "${PREVIOUS_WORKTREE}" + git -C "${ROOT_DIR}" worktree add --detach "${PREVIOUS_WORKTREE}" "${candidate_ref}" >/dev/null + git -C "${PREVIOUS_WORKTREE}" submodule update --init --recursive >/dev/null + prepare_legacy_packaging_tree "${PREVIOUS_WORKTREE}" + + if PREVIOUS_ARCHIVE="$(package_tree "${PREVIOUS_WORKTREE}" "${PREVIOUS_BUILD_DIR}/dist" "${package_log}")"; then + printf 'Verifying previous archive candidate from %s\n' "${candidate_ref}" + if verify_archive "${PREVIOUS_ARCHIVE}" "${verify_log}" "1"; then + BASELINE_REF="${candidate_ref}" + return 0 + fi + + echo "Packaged baseline ${candidate_ref} failed release-package verification. Last 40 log lines from ${verify_log}:" >&2 + if [[ -f "${verify_log}" ]]; then + tail -n 40 "${verify_log}" >&2 + fi + fi + + next_candidate="$(git -C "${ROOT_DIR}" rev-parse "${candidate_ref}^" 2>/dev/null || true)" + if [[ -z "${next_candidate}" ]]; then + break + fi + + echo "Persistence baseline ${candidate_ref} is not package-smokeable. Trying parent ${next_candidate} (attempt $((attempt + 1)))." >&2 + attempt=$((attempt + 1)) + + git -C "${ROOT_DIR}" worktree remove --force "${PREVIOUS_WORKTREE}" >/dev/null 2>&1 || rm -rf "${PREVIOUS_WORKTREE}" + PREVIOUS_WORKTREE="" + candidate_ref="${next_candidate}" + done + + echo "Failed to build and verify a package from ${FROM_REF} or any first-parent ancestor." >&2 + exit 1 +} + extract_archive_to_prefix() { local archive_path="$1" local prefix="$2" @@ -288,22 +361,13 @@ run_package_fixture() { ) 2>&1 | tee "${log_path}" } -printf 'Preparing previous release archive from %s\n' "${FROM_REF}" -PREVIOUS_WORKTREE="$(mktemp -d)" -rm -rf "${PREVIOUS_WORKTREE}" -git -C "${ROOT_DIR}" worktree add --detach "${PREVIOUS_WORKTREE}" "${FROM_REF}" >/dev/null -git -C "${PREVIOUS_WORKTREE}" submodule update --init --recursive >/dev/null - -PREVIOUS_ARCHIVE="$(package_tree "${PREVIOUS_WORKTREE}" "${PREVIOUS_BUILD_DIR}/dist" "${PREVIOUS_BUILD_DIR}/package.log")" +prepare_previous_archive "${FROM_REF}" if [[ -z "${CURRENT_ARCHIVE}" ]]; then printf 'Preparing current release archive from working tree\n' CURRENT_ARCHIVE="$(package_tree "${ROOT_DIR}" "${CURRENT_BUILD_DIR}/dist" "${CURRENT_BUILD_DIR}/package.log")" fi -printf 'Verifying previous archive\n' -verify_archive "${PREVIOUS_ARCHIVE}" "${PREVIOUS_BUILD_DIR}/verify.log" "1" - printf 'Verifying current archive\n' verify_archive "${CURRENT_ARCHIVE}" "${CURRENT_BUILD_DIR}/verify.log" "0" @@ -323,7 +387,7 @@ run_package_fixture "${CURRENT_PREFIX}" "read" "${CURRENT_BUILD_DIR}/migration-r cat > "${ARTIFACTS_DIR}/summary.txt" </dev/null 2>&1 || true fi @@ -61,10 +68,28 @@ cleanup() { if [[ -n "${SCRATCH_DIR}" && -d "${SCRATCH_DIR}" ]]; then rm -rf "${SCRATCH_DIR}" fi + + if [[ -n "${SEMANTIC_DNS_LEGACY_BACKUP_FILE}" ]]; then + rm -f "${SEMANTIC_DNS_LEGACY_BACKUP_FILE}" >/dev/null 2>&1 || true + fi } trap cleanup EXIT +restore_semantic_dns_legacy_state() { + if [[ -n "${SEMANTIC_DNS_LEGACY_BACKUP_FILE}" && -f "${SEMANTIC_DNS_LEGACY_BACKUP_FILE}" ]]; then + mkdir -p "${SEMANTIC_DNS_LEGACY_STATE_DIR}" + chmod 0700 "${SEMANTIC_DNS_LEGACY_STATE_DIR}" + cp "${SEMANTIC_DNS_LEGACY_BACKUP_FILE}" "${SEMANTIC_DNS_LEGACY_STATE_FILE}" + else + rm -f "${SEMANTIC_DNS_LEGACY_STATE_FILE}" + fi + + if [[ "${SEMANTIC_DNS_LEGACY_STATE_DIR_EXISTED}" == "0" ]]; then + rmdir "${SEMANTIC_DNS_LEGACY_STATE_DIR}" >/dev/null 2>&1 || true + fi +} + while [[ $# -gt 0 ]]; do case "$1" in --from-ref) @@ -162,7 +187,37 @@ fi PREVIOUS_BUILD_DIR="${ARTIFACTS_DIR}/previous" CURRENT_BUILD_DIR="${ARTIFACTS_DIR}/current" INSTALL_ROOT="${ARTIFACTS_DIR}/upgrade-prefix" -mkdir -p "${PREVIOUS_BUILD_DIR}" "${CURRENT_BUILD_DIR}" "${INSTALL_ROOT}" +SEMANTIC_DNS_STATE_DIR="${ARTIFACTS_DIR}/semantic-dns-state" +SEMANTIC_DNS_STATE_FILE="${SEMANTIC_DNS_STATE_DIR}/durable_state.bin" +mkdir -p "${PREVIOUS_BUILD_DIR}" "${CURRENT_BUILD_DIR}" "${INSTALL_ROOT}" "${SEMANTIC_DNS_STATE_DIR}" +chmod 0700 "${SEMANTIC_DNS_STATE_DIR}" + +exec 9>"${SEMANTIC_DNS_COMPAT_LOCK}" +flock 9 +if [[ -d "${SEMANTIC_DNS_LEGACY_STATE_DIR}" ]]; then + SEMANTIC_DNS_LEGACY_STATE_DIR_EXISTED=1 +fi +if [[ -f "${SEMANTIC_DNS_LEGACY_STATE_FILE}" && ! -L "${SEMANTIC_DNS_LEGACY_STATE_FILE}" ]]; then + SEMANTIC_DNS_LEGACY_BACKUP_FILE="$(mktemp)" + cp "${SEMANTIC_DNS_LEGACY_STATE_FILE}" "${SEMANTIC_DNS_LEGACY_BACKUP_FILE}" +fi +mkdir -p "${SEMANTIC_DNS_LEGACY_STATE_DIR}" +chmod 0700 "${SEMANTIC_DNS_LEGACY_STATE_DIR}" +rm -f "${SEMANTIC_DNS_LEGACY_STATE_FILE}" + +prepare_legacy_packaging_tree() { + local tree_root="$1" + local package_script="${tree_root}/infra/scripts/package-release.sh" + local constants_header="${tree_root}/extension/include/php_king/constants.h" + + if [[ ! -f "${package_script}" || ! -f "${constants_header}" ]]; then + return 0 + fi + + sed -i \ + -e 's#${EXT_DIR}/include/php_king.h#${EXT_DIR}/include/php_king/constants.h#g' \ + "${package_script}" +} package_tree() { local tree_root="$1" @@ -228,7 +283,8 @@ verify_archive() { ( cd "${ROOT_DIR}" - PHP_BIN="${PHP_BIN}" "${verify_args[@]}" + KING_SEMANTIC_DNS_STATE_PATH="${SEMANTIC_DNS_STATE_FILE}" \ + PHP_BIN="${PHP_BIN}" "${verify_args[@]}" ) 2>&1 | tee "${log_path}" } @@ -242,33 +298,47 @@ install_archive_to_prefix() { ( tar -xzf "${archive_path}" -C "${prefix}" --strip-components=1 - PHP_BIN="${PHP_BIN}" "${prefix}/bin/smoke.sh" + KING_SEMANTIC_DNS_STATE_PATH="${SEMANTIC_DNS_STATE_FILE}" \ + PHP_BIN="${PHP_BIN}" "${prefix}/bin/smoke.sh" ) 2>&1 | tee "${log_path}" } prepare_previous_archive() { local candidate_ref="$1" local package_log="" + local verify_log="" local next_candidate="" local attempt=0 + local short_ref="" while [[ -n "${candidate_ref}" ]]; do - package_log="${PREVIOUS_BUILD_DIR}/package-$(git -C "${ROOT_DIR}" rev-parse --short "${candidate_ref}").log" + short_ref="$(git -C "${ROOT_DIR}" rev-parse --short "${candidate_ref}")" + package_log="${PREVIOUS_BUILD_DIR}/package-${short_ref}.log" + verify_log="${PREVIOUS_BUILD_DIR}/verify-${short_ref}.log" printf 'Preparing previous release archive from %s\n' "${candidate_ref}" PREVIOUS_WORKTREE="$(mktemp -d)" rm -rf "${PREVIOUS_WORKTREE}" git -C "${ROOT_DIR}" worktree add --detach "${PREVIOUS_WORKTREE}" "${candidate_ref}" >/dev/null git -C "${PREVIOUS_WORKTREE}" submodule update --init --recursive >/dev/null + prepare_legacy_packaging_tree "${PREVIOUS_WORKTREE}" if PREVIOUS_ARCHIVE="$(package_tree "${PREVIOUS_WORKTREE}" "${PREVIOUS_BUILD_DIR}/dist" "${package_log}")"; then - BASELINE_REF="${candidate_ref}" - return 0 - fi + printf 'Verifying previous archive candidate from %s\n' "${candidate_ref}" + if verify_archive "${PREVIOUS_ARCHIVE}" "${verify_log}" "1"; then + BASELINE_REF="${candidate_ref}" + return 0 + fi - echo "Failed to package from ${candidate_ref}. Last 40 log lines from ${package_log}:" >&2 - if [[ -f "${package_log}" ]]; then - tail -n 40 "${package_log}" >&2 + echo "Packaged baseline ${candidate_ref} failed release-package verification. Last 40 log lines from ${verify_log}:" >&2 + if [[ -f "${verify_log}" ]]; then + tail -n 40 "${verify_log}" >&2 + fi + else + echo "Failed to package from ${candidate_ref}. Last 40 log lines from ${package_log}:" >&2 + if [[ -f "${package_log}" ]]; then + tail -n 40 "${package_log}" >&2 + fi fi next_candidate="$(git -C "${ROOT_DIR}" rev-parse "${candidate_ref}^" 2>/dev/null || true)" @@ -276,15 +346,15 @@ prepare_previous_archive() { break fi - echo "Packaging baseline ${candidate_ref} failed. Trying parent ${next_candidate} (attempt $((attempt + 1)))." >&2 + echo "Compatibility baseline ${candidate_ref} is not package-smokeable. Trying parent ${next_candidate} (attempt $((attempt + 1)))." >&2 attempt=$((attempt + 1)) - rm -rf "${PREVIOUS_WORKTREE}" + git -C "${ROOT_DIR}" worktree remove --force "${PREVIOUS_WORKTREE}" >/dev/null 2>&1 || rm -rf "${PREVIOUS_WORKTREE}" PREVIOUS_WORKTREE="" candidate_ref="${next_candidate}" done - echo "Failed to build a package from ${FROM_REF} or any first-parent ancestor." >&2 + echo "Failed to build and verify a package from ${FROM_REF} or any first-parent ancestor." >&2 exit 1 } @@ -295,9 +365,6 @@ if [[ -z "${CURRENT_ARCHIVE}" ]]; then CURRENT_ARCHIVE="$(package_tree "${ROOT_DIR}" "${CURRENT_BUILD_DIR}/dist" "${CURRENT_BUILD_DIR}/package.log")" fi -printf 'Verifying previous archive\n' -verify_archive "${PREVIOUS_ARCHIVE}" "${PREVIOUS_BUILD_DIR}/verify.log" "1" - printf 'Verifying current archive\n' verify_archive "${CURRENT_ARCHIVE}" "${CURRENT_BUILD_DIR}/verify.log" "0" diff --git a/infra/scripts/commit-docker-buildx-cache.sh b/infra/scripts/commit-docker-buildx-cache.sh new file mode 100755 index 000000000..fd6670ad4 --- /dev/null +++ b/infra/scripts/commit-docker-buildx-cache.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: ./infra/scripts/commit-docker-buildx-cache.sh --cache-dir DIR --next-dir DIR + +Promotes a Docker Buildx local cache export directory after a successful build. +USAGE +} + +CACHE_DIR="" +NEXT_DIR="" + +while [[ "$#" -gt 0 ]]; do + case "$1" in + --cache-dir) + CACHE_DIR="${2:-}" + shift 2 + ;; + --next-dir) + NEXT_DIR="${2:-}" + shift 2 + ;; + --help|-h) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +if [[ -z "${CACHE_DIR}" || -z "${NEXT_DIR}" ]]; then + echo "--cache-dir and --next-dir are required." >&2 + exit 1 +fi + +if [[ ! -d "${NEXT_DIR}" ]]; then + echo "Next Docker Buildx cache directory does not exist: ${NEXT_DIR}" >&2 + exit 1 +fi + +parent_dir="$(dirname "${CACHE_DIR}")" +tmp_old="${CACHE_DIR}.old.$$" + +mkdir -p "${parent_dir}" +rm -rf "${tmp_old}" + +if [[ -e "${CACHE_DIR}" ]]; then + mv "${CACHE_DIR}" "${tmp_old}" +fi + +mv "${NEXT_DIR}" "${CACHE_DIR}" +rm -rf "${tmp_old}" + +echo "Committed local Docker Buildx cache: ${CACHE_DIR}" diff --git a/infra/scripts/install-ci-system-dependencies.sh b/infra/scripts/install-ci-system-dependencies.sh index 57b9742b2..1524df033 100755 --- a/infra/scripts/install-ci-system-dependencies.sh +++ b/infra/scripts/install-ci-system-dependencies.sh @@ -24,6 +24,37 @@ packages=( zlib1g-dev ) +dependencies_present() { + local missing=0 + local tool + + for tool in autoconf automake bison cc clang cmake ninja pkg-config re2c php phpize nm; do + if ! command -v "${tool}" >/dev/null 2>&1; then + echo "[ci-deps] missing tool: ${tool}" >&2 + missing=1 + fi + done + + for pkg in libcurl openssl zlib; do + if ! pkg-config --exists "${pkg}" >/dev/null 2>&1; then + echo "[ci-deps] missing pkg-config dependency: ${pkg}" >&2 + missing=1 + fi + done + + return "${missing}" +} + +if ! command -v sudo >/dev/null 2>&1 || ! sudo -n true >/dev/null 2>&1; then + if dependencies_present; then + echo "[ci-deps] sudo is unavailable, but required CI dependencies are already installed." + exit 0 + fi + + echo "[ci-deps] sudo is unavailable and required CI dependencies are missing." >&2 + exit 1 +fi + run_with_retry() { local label="$1" shift diff --git a/infra/scripts/local-inference-model-cache.sh b/infra/scripts/local-inference-model-cache.sh new file mode 100755 index 000000000..c1e7d61a5 --- /dev/null +++ b/infra/scripts/local-inference-model-cache.sh @@ -0,0 +1,211 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: ./infra/scripts/local-inference-model-cache.sh resolve [--require] [--github-output PATH] + +Resolves a local GGUF inference model artifact from the self-hosted runner +cache without downloading model weights or allowing model files to live in git. + +Environment variables: + KING_INFERENCE_TEST_MODEL_PATH Explicit model artifact path. + KING_CI_LOCAL_INFERENCE_MODEL_PATH Explicit runner-cache model path. + KING_CI_LOCAL_INFERENCE_MODEL_DIR Runner model cache dir. + Default: /opt/king/cache/inference-models + KING_CI_LOCAL_INFERENCE_MODEL_NAME Preferred file inside the cache dir. + Default: gemma3-1b.gguf + KING_CI_ALLOW_REPO_MODEL_ARTIFACT Set to 1 only for local experiments. +USAGE +} + +if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then + usage + exit 0 +fi + +MODE="${1:-}" +shift || true + +REQUIRE=0 +GITHUB_OUTPUT_PATH="" +while [[ "$#" -gt 0 ]]; do + case "$1" in + --require) + REQUIRE=1 + shift + ;; + --github-output) + GITHUB_OUTPUT_PATH="${2:-}" + shift 2 + ;; + *) + echo "Unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +if [[ "${MODE}" != "resolve" ]]; then + echo "Mode must be resolve." >&2 + usage >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)" +CACHE_DIR="${KING_CI_LOCAL_INFERENCE_MODEL_DIR:-/opt/king/cache/inference-models}" +MODEL_NAME="${KING_CI_LOCAL_INFERENCE_MODEL_NAME:-gemma3-1b.gguf}" +ALLOW_REPO_ARTIFACT="${KING_CI_ALLOW_REPO_MODEL_ARTIFACT:-0}" + +write_output() { + local key="$1" + local value="$2" + + if [[ -n "${GITHUB_OUTPUT_PATH}" ]]; then + printf '%s=%s\n' "${key}" "${value}" >> "${GITHUB_OUTPUT_PATH}" + fi +} + +assert_no_tracked_model_weights() { + local tracked="" + + if ! command -v git >/dev/null 2>&1 || ! git -C "${ROOT_DIR}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + return 0 + fi + + tracked="$( + git -C "${ROOT_DIR}" ls-files \ + '*.gguf' '*.safetensors' '*.pt' '*.pth' '*.ckpt' '*.onnx' '*.tflite' 2>/dev/null || true + )" + if [[ -n "${tracked}" ]]; then + echo "Model artifact files are tracked by git, which is forbidden:" >&2 + printf '%s\n' "${tracked}" >&2 + exit 1 + fi +} + +resolve_path() { + local path="$1" + + if [[ -z "${path}" || "${path}" == *"://"* ]]; then + return 1 + fi + if [[ -f "${path}" && -r "${path}" ]]; then + printf '%s\n' "${path}" + return 0 + fi + return 1 +} + +gguf_magic_ok() { + local path="$1" + local magic="" + + magic="$(LC_ALL=C head -c 4 "${path}" 2>/dev/null || true)" + [[ "${magic}" == "GGUF" ]] +} + +repo_relative_path() { + local real_path="$1" + + case "${real_path}" in + "${ROOT_DIR}"/*) + printf '%s\n' "${real_path#"${ROOT_DIR}/"}" + return 0 + ;; + esac + return 1 +} + +reject_git_artifact() { + local source_path="$1" + local real_path="$2" + local rel="" + + if [[ "${ALLOW_REPO_ARTIFACT}" == "1" ]]; then + return 0 + fi + + if rel="$(repo_relative_path "${real_path}")"; then + echo "Refusing inference model inside repository checkout: ${rel}" >&2 + echo "Model weights must live in a runner cache or external object store, not git." >&2 + exit 1 + fi + + if [[ "${source_path}" == "${ROOT_DIR}"/* ]] && [[ ! -L "${source_path}" ]]; then + rel="${source_path#"${ROOT_DIR}/"}" + echo "Refusing non-symlink inference model inside repository checkout: ${rel}" >&2 + exit 1 + fi +} + +candidate_paths() { + local explicit="" + + explicit="${KING_INFERENCE_TEST_MODEL_PATH:-}" + [[ -n "${explicit}" ]] && printf '%s\t%s\n' "KING_INFERENCE_TEST_MODEL_PATH" "${explicit}" + + explicit="${KING_CI_LOCAL_INFERENCE_MODEL_PATH:-}" + [[ -n "${explicit}" ]] && printf '%s\t%s\n' "KING_CI_LOCAL_INFERENCE_MODEL_PATH" "${explicit}" + + printf '%s\t%s\n' "KING_CI_LOCAL_INFERENCE_MODEL_DIR/${MODEL_NAME}" "${CACHE_DIR%/}/${MODEL_NAME}" + + if [[ -d "${CACHE_DIR}" ]]; then + find "${CACHE_DIR}" -maxdepth 2 -type f -name '*.gguf' -print \ + | LC_ALL=C sort \ + | awk '{print "KING_CI_LOCAL_INFERENCE_MODEL_DIR/*.gguf\t" $0}' + fi + + printf '%s\t%s\n' "repo-local symlink var/inference-models/gemma3-1b.gguf" "${ROOT_DIR}/var/inference-models/gemma3-1b.gguf" +} + +assert_no_tracked_model_weights + +FOUND_PATH="" +FOUND_SOURCE="" +while IFS=$'\t' read -r source path; do + if resolved="$(resolve_path "${path}")"; then + FOUND_PATH="${resolved}" + FOUND_SOURCE="${source}" + break + fi +done < <(candidate_paths) + +if [[ -z "${FOUND_PATH}" ]]; then + write_output "model-present" "false" + write_output "model-path" "" + write_output "model-realpath" "" + write_output "model-source" "" + write_output "model-cache-dir" "${CACHE_DIR}" + if [[ "${REQUIRE}" == "1" ]]; then + echo "No readable local GGUF inference model found." >&2 + echo "Checked explicit env paths, ${CACHE_DIR}, and repo-local symlink fallback." >&2 + exit 1 + fi + echo "No readable local GGUF inference model found." + exit 0 +fi + +REAL_PATH="$(realpath "${FOUND_PATH}")" +reject_git_artifact "${FOUND_PATH}" "${REAL_PATH}" + +if ! gguf_magic_ok "${FOUND_PATH}"; then + echo "Resolved inference artifact is not a GGUF file: ${FOUND_PATH}" >&2 + exit 1 +fi + +BYTES="$(wc -c < "${FOUND_PATH}" | tr -d '[:space:]')" +write_output "model-present" "true" +write_output "model-path" "${FOUND_PATH}" +write_output "model-realpath" "${REAL_PATH}" +write_output "model-source" "${FOUND_SOURCE}" +write_output "model-cache-dir" "${CACHE_DIR}" +write_output "model-bytes" "${BYTES}" + +echo "Resolved local GGUF inference model: ${FOUND_PATH}" +echo "Real path: ${REAL_PATH}" +echo "Source: ${FOUND_SOURCE}" +echo "Bytes: ${BYTES}" diff --git a/infra/scripts/local-lsquic-runtime-cache.sh b/infra/scripts/local-lsquic-runtime-cache.sh new file mode 100755 index 000000000..7da193c3f --- /dev/null +++ b/infra/scripts/local-lsquic-runtime-cache.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: ./infra/scripts/local-lsquic-runtime-cache.sh restore|save [--github-output PATH] + +Restores or saves the pinned LSQUIC runtime from a persistent local runner cache. +The cache key is derived from host architecture and infra/scripts/lsquic-bootstrap.lock. + +Environment variables: + KING_CI_LOCAL_LSQUIC_CACHE_DIR Persistent cache root. + KING_LSQUIC_RUNTIME_PREFIX Runtime install prefix. +USAGE +} + +if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then + usage + exit 0 +fi + +MODE="${1:-}" +shift || true + +GITHUB_OUTPUT_PATH="" +while [[ "$#" -gt 0 ]]; do + case "$1" in + --github-output) + GITHUB_OUTPUT_PATH="${2:-}" + shift 2 + ;; + *) + echo "Unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +case "${MODE}" in + restore|save) + ;; + *) + echo "Mode must be restore or save." >&2 + usage >&2 + exit 1 + ;; +esac + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)" +LOCK_FILE="${SCRIPT_DIR}/lsquic-bootstrap.lock" +BUILD_SCRIPT="${SCRIPT_DIR}/build-lsquic-runtime.sh" +CACHE_ROOT="${KING_CI_LOCAL_LSQUIC_CACHE_DIR:-}" +PREFIX_DIR="${KING_LSQUIC_RUNTIME_PREFIX:-${ROOT_DIR}/.cache/king/lsquic/runtime/prefix}" + +if [[ -z "${CACHE_ROOT}" ]]; then + echo "KING_CI_LOCAL_LSQUIC_CACHE_DIR is required." >&2 + exit 1 +fi + +if [[ ! -f "${LOCK_FILE}" ]]; then + echo "Missing LSQUIC bootstrap lock file: ${LOCK_FILE}" >&2 + exit 1 +fi + +if [[ ! -x "${BUILD_SCRIPT}" ]]; then + echo "Missing executable LSQUIC build script: ${BUILD_SCRIPT}" >&2 + exit 1 +fi + +file_sha256() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + return + fi + + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + return + fi + + echo "sha256sum or shasum is required." >&2 + exit 1 +} + +host_os() { + case "$(uname -s)" in + Darwin) + printf '%s\n' "darwin" + ;; + Linux) + printf '%s\n' "linux" + ;; + CYGWIN*|MINGW*|MSYS*) + printf '%s\n' "windows" + ;; + *) + uname -s | tr '[:upper:]' '[:lower:]' + ;; + esac +} + +runtime_arch() { + local os="" + + os="$(host_os)" + case "$(uname -m)" in + x86_64|amd64) + printf '%s\n' "${os}-amd64" + ;; + aarch64|arm64) + printf '%s\n' "${os}-arm64" + ;; + *) + printf '%s-%s\n' "${os}" "$(uname -m)" + ;; + esac +} + +write_output() { + local key="$1" + local value="$2" + + if [[ -n "${GITHUB_OUTPUT_PATH}" ]]; then + printf '%s=%s\n' "${key}" "${value}" >> "${GITHUB_OUTPUT_PATH}" + fi +} + +LOCK_SHA="$(file_sha256 "${LOCK_FILE}")" +CACHE_KEY="$(runtime_arch)-${LOCK_SHA}" +CACHE_DIR="${CACHE_ROOT%/}/${CACHE_KEY}" +CACHE_PREFIX="${CACHE_DIR}/prefix" + +write_output "cache-key" "${CACHE_KEY}" + +case "${MODE}" in + restore) + if [[ ! -d "${CACHE_PREFIX}" ]]; then + echo "No local LSQUIC runtime cache entry: ${CACHE_DIR}" + write_output "cache-hit" "false" + exit 0 + fi + + rm -rf "${PREFIX_DIR}" + mkdir -p "$(dirname "${PREFIX_DIR}")" + cp -a "${CACHE_PREFIX}" "${PREFIX_DIR}" + + if KING_LSQUIC_RUNTIME_PREFIX="${PREFIX_DIR}" "${BUILD_SCRIPT}" --verify-current; then + echo "Restored local LSQUIC runtime cache: ${CACHE_DIR}" + write_output "cache-hit" "true" + exit 0 + fi + + echo "Local LSQUIC runtime cache is stale: ${CACHE_DIR}" >&2 + rm -rf "${PREFIX_DIR}" + write_output "cache-hit" "false" + ;; + save) + KING_LSQUIC_RUNTIME_PREFIX="${PREFIX_DIR}" "${BUILD_SCRIPT}" --verify-current >/dev/null + + mkdir -p "${CACHE_ROOT}" + TMP_DIR="${CACHE_DIR}.tmp.$$" + rm -rf "${TMP_DIR}" + mkdir -p "${TMP_DIR}" + cp -a "${PREFIX_DIR}" "${TMP_DIR}/prefix" + rm -rf "${CACHE_DIR}" + mv "${TMP_DIR}" "${CACHE_DIR}" + echo "Saved local LSQUIC runtime cache: ${CACHE_DIR}" + write_output "cache-hit" "true" + ;; +esac diff --git a/infra/scripts/package-pie-source.sh b/infra/scripts/package-pie-source.sh index 9581c8f07..e3901d0f3 100755 --- a/infra/scripts/package-pie-source.sh +++ b/infra/scripts/package-pie-source.sh @@ -52,7 +52,7 @@ done resolve_version() { sed -n 's/^# define PHP_KING_VERSION[[:space:]]*"\(.*\)"/\1/p' \ - "${ROOT_DIR}/extension/include/php_king.h" | head -n 1 + "${ROOT_DIR}/extension/include/php_king/constants.h" | head -n 1 } resolve_source_epoch() { diff --git a/infra/scripts/package-release.sh b/infra/scripts/package-release.sh index 246f9aeb7..d956fc26a 100755 --- a/infra/scripts/package-release.sh +++ b/infra/scripts/package-release.sh @@ -155,7 +155,7 @@ release_env_has_pinned_lsquic_runtime() { } resolve_version() { - sed -n 's/^# define PHP_KING_VERSION[[:space:]]*"\(.*\)"/\1/p' "${EXT_DIR}/include/php_king.h" | head -n 1 + sed -n 's/^# define PHP_KING_VERSION[[:space:]]*"\(.*\)"/\1/p' "${EXT_DIR}/include/php_king/constants.h" | head -n 1 } resolve_git_short() { @@ -272,7 +272,7 @@ ensure_release_git_lock_state() { VERSION="$(resolve_version)" if [[ -z "${VERSION}" ]]; then - echo "Failed to resolve PHP_KING_VERSION from ${EXT_DIR}/include/php_king.h." >&2 + echo "Failed to resolve PHP_KING_VERSION from ${EXT_DIR}/include/php_king/constants.h." >&2 exit 1 fi diff --git a/infra/scripts/resolve-docker-buildx-cache.sh b/infra/scripts/resolve-docker-buildx-cache.sh new file mode 100755 index 000000000..208b883aa --- /dev/null +++ b/infra/scripts/resolve-docker-buildx-cache.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: ./infra/scripts/resolve-docker-buildx-cache.sh --scope NAME --github-output PATH + +Resolves Docker Buildx cache inputs for GitHub Actions. GitHub-hosted runners +use the GitHub Actions cache. Self-hosted runners can additionally use a +persistent local BuildKit cache when KING_CI_LOCAL_DOCKER_BUILDX_CACHE_DIR is a +writable directory. + +Environment variables: + KING_CI_LOCAL_DOCKER_BUILDX_CACHE_DIR Persistent local Buildx cache root. +USAGE +} + +SCOPE="" +GITHUB_OUTPUT_PATH="" + +while [[ "$#" -gt 0 ]]; do + case "$1" in + --scope) + SCOPE="${2:-}" + shift 2 + ;; + --github-output) + GITHUB_OUTPUT_PATH="${2:-}" + shift 2 + ;; + --help|-h) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +if [[ -z "${SCOPE}" ]]; then + echo "--scope is required." >&2 + exit 1 +fi + +if [[ -z "${GITHUB_OUTPUT_PATH}" ]]; then + echo "--github-output is required." >&2 + exit 1 +fi + +if [[ ! "${SCOPE}" =~ ^[A-Za-z0-9._-]+$ ]]; then + echo "--scope must contain only letters, numbers, dot, underscore, and dash." >&2 + exit 1 +fi + +write_output() { + local key="$1" + local value="$2" + + { + printf '%s<> "${GITHUB_OUTPUT_PATH}" +} + +write_scalar_output() { + local key="$1" + local value="$2" + + printf '%s=%s\n' "${key}" "${value}" >> "${GITHUB_OUTPUT_PATH}" +} + +cache_from="type=gha,scope=${SCOPE}" +cache_to="type=gha,mode=max,scope=${SCOPE}" +local_root="${KING_CI_LOCAL_DOCKER_BUILDX_CACHE_DIR:-}" +use_local="false" +local_dir="" +local_next_dir="" + +if [[ -n "${local_root}" && -d "${local_root}" && -w "${local_root}" ]]; then + local_dir="${local_root%/}/${SCOPE}" + local_next_dir="${local_root%/}/${SCOPE}.next" + mkdir -p "${local_dir}" + rm -rf "${local_next_dir}" + mkdir -p "${local_next_dir}" + + cache_from=$(printf '%s\n%s' "type=local,src=${local_dir}" "${cache_from}") + cache_to=$(printf '%s\n%s' "type=local,dest=${local_next_dir},mode=max" "${cache_to}") + use_local="true" +fi + +write_output "cache-from" "${cache_from}" +write_output "cache-to" "${cache_to}" +write_scalar_output "use-local-cache" "${use_local}" +write_scalar_output "local-cache-dir" "${local_dir}" +write_scalar_output "local-cache-next-dir" "${local_next_dir}" + +if [[ "${use_local}" == "true" ]]; then + echo "Using local Docker Buildx cache scope ${SCOPE}: ${local_dir}" +else + echo "Using GitHub Docker Buildx cache scope ${SCOPE}" +fi diff --git a/infra/scripts/runtime-install-smoke.php b/infra/scripts/runtime-install-smoke.php index 05ff2f4c1..ada7f4433 100644 --- a/infra/scripts/runtime-install-smoke.php +++ b/infra/scripts/runtime-install-smoke.php @@ -3,7 +3,91 @@ declare(strict_types=1); $schema = 'InstallSmoke_' . getmypid(); -$dnsPort = 10000 + (getmypid() % 40000); +$semanticDnsStatePath = getenv('KING_SEMANTIC_DNS_STATE_PATH'); + +function install_smoke_allocate_udp_port(): int +{ + $errno = 0; + $errstr = ''; + $probe = @stream_socket_server( + 'udp://127.0.0.1:0', + $errno, + $errstr, + STREAM_SERVER_BIND + ); + if ($probe === false) { + throw new RuntimeException("Failed to allocate UDP smoke port: {$errstr} ({$errno})"); + } + + $name = stream_socket_get_name($probe, false); + fclose($probe); + if (!is_string($name) || $name === '') { + throw new RuntimeException('Failed to read allocated UDP smoke port.'); + } + + $separator = strrpos($name, ':'); + if ($separator === false) { + throw new RuntimeException("Unexpected UDP socket name: {$name}"); + } + + $port = (int) substr($name, $separator + 1); + if ($port < 1 || $port > 65535) { + throw new RuntimeException("Invalid allocated UDP smoke port: {$name}"); + } + + return $port; +} + +function install_smoke_default_semantic_dns_state_path(): string +{ + return sys_get_temp_dir() + . '/king_install_smoke_semantic_dns_' . getmypid() + . '/semantic_dns_state.bin'; +} + +function install_smoke_semantic_dns_config(int $dnsPort, ?string $statePath): array +{ + return [ + 'enabled' => true, + 'bind_address' => '127.0.0.1', + 'dns_port' => $dnsPort, + 'default_record_ttl_sec' => 120, + 'service_discovery_max_ips_per_response' => 5, + 'semantic_mode_enable' => true, + 'state_path' => is_string($statePath) && $statePath !== '' + ? $statePath + : install_smoke_default_semantic_dns_state_path(), + 'mothernode_uri' => 'mother://install-smoke', + 'routing_policies' => ['mode' => 'local'], + ]; +} + +function install_smoke_start_semantic_dns(?string $statePath): int +{ + $lastError = null; + for ($attempt = 0; $attempt < 8; $attempt++) { + $dnsPort = install_smoke_allocate_udp_port(); + if (!king_semantic_dns_init(install_smoke_semantic_dns_config($dnsPort, $statePath))) { + fwrite(STDERR, "Semantic-DNS init smoke failed.\n"); + exit(1); + } + + try { + if (king_semantic_dns_start_server()) { + return $dnsPort; + } + $lastError = 'king_semantic_dns_start_server returned false'; + } catch (Throwable $error) { + $lastError = $error->getMessage(); + if (!str_contains($lastError, 'could not bind the UDP DNS listener')) { + throw $error; + } + } + } + + fwrite(STDERR, "Semantic-DNS start smoke failed after retries: {$lastError}\n"); + exit(1); +} if (!function_exists('king_connect')) { fwrite(STDERR, "King extension functions are unavailable.\n"); @@ -64,23 +148,7 @@ @rmdir($root); } -if (!king_semantic_dns_init([ - 'enabled' => true, - 'bind_address' => '127.0.0.1', - 'dns_port' => $dnsPort, - 'default_record_ttl_sec' => 120, - 'service_discovery_max_ips_per_response' => 5, - 'semantic_mode_enable' => true, - 'mothernode_uri' => 'mother://install-smoke', - 'routing_policies' => ['mode' => 'local'], -])) { - fwrite(STDERR, "Semantic-DNS init smoke failed.\n"); - exit(1); -} -if (!king_semantic_dns_start_server()) { - fwrite(STDERR, "Semantic-DNS start smoke failed.\n"); - exit(1); -} +install_smoke_start_semantic_dns(is_string($semanticDnsStatePath) ? $semanticDnsStatePath : null); if (!king_semantic_dns_register_service([ 'service_id' => 'install-smoke', 'service_name' => 'install-smoke', diff --git a/infra/scripts/smoke-profile.sh b/infra/scripts/smoke-profile.sh index 5317a8d16..02e9f7fe5 100755 --- a/infra/scripts/smoke-profile.sh +++ b/infra/scripts/smoke-profile.sh @@ -128,6 +128,7 @@ case "${PROFILE}" in esac "${PHP_BIN}" \ + -n \ -d "extension=${EXT_SO}" \ -d "king.security_allow_config_override=1" \ "${SCRIPT_DIR}/runtime-install-smoke.php" diff --git a/stubs/king.php b/stubs/king.php index 48b0d3a09..b98fa4ccd 100755 --- a/stubs/king.php +++ b/stubs/king.php @@ -15,6 +15,10 @@ function king_await(\King\Awaitable $awaitable, ?int $timeout_ms = null): mixed function king_awaitable_poll(\King\Awaitable $awaitable, int $timeout_ms = 0): bool {} function king_awaitable_cancel(\King\Awaitable $awaitable): bool {} function king_awaitable_status(\King\Awaitable $awaitable): string {} + /** @param array $awaitables */ + function king_awaitable_any(array $awaitables, ?\King\CancelToken $cancel = null): \King\Awaitable {} + /** @param array $awaitables */ + function king_awaitable_all(array $awaitables, ?\King\CancelToken $cancel = null): \King\Awaitable {} /** * Establish a local runtime QUIC session handle backed by a real @@ -304,13 +308,13 @@ function king_rtp_dtls_fingerprint($socket) {} * Perform DTLS handshake with a remote peer. * @return bool|false */ - function king_rtp_dtls_accept($socket, string $ip, int $port, int $timeout_ms) {} + function king_rtp_dtls_accept($socket, string $ip, int $port, int $timeout_ms = 5000) {} /** * Receive RTP data from a peer. * @return array|false */ - function king_rtp_recv($socket, int $timeout_ms) {} + function king_rtp_recv($socket, int $timeout_ms = 0) {} /** * Send RTP data to a peer. @@ -327,6 +331,10 @@ function king_rtp_close($socket) {} * Create an MCP connection-state resource for the active runtime. * This stores host, port, optional `King\Config`, and the explicit * open/closed lifecycle for the active remote peer socket. + * `options['default_timeout_ms']` sets the per-connection timeout default + * for later request, IIBIN, upload, and download operations. `timeout_ms` + * is accepted as an alias at connection creation time. Per-call + * `timeout_ms` overrides this default. * @param mixed $config * @param array|null $options * @return resource|false @@ -347,6 +355,65 @@ function king_mcp_request(mixed $connection, string $service_name, string $metho function king_mcp_request_async(mixed $connection, string $service_name, string $method_name, string $request_payload, ?array $options = null): \King\Awaitable {} + /** + * Encode request data with the `request_schema` configured under + * `mcp.iibin_routes` for this connection's `(service, method)` route, + * exchange it over King's internal line-framed MCP peer transport, and + * decode the response when that route defines `response_schema`. + * + * Schemas are fixed by the `King\Config` snapshot used at connect time; + * they cannot be overridden per request. + * + * @param mixed $connection + * @param mixed $data + * @param array|null $options + * @return array|object|string|false + */ + function king_mcp_request_iibin(mixed $connection, string $service_name, string $method_name, mixed $data, ?array $options = null): array|object|string|false {} + + function king_mcp_request_iibin_async(mixed $connection, string $service_name, string $method_name, mixed $data, ?array $options = null): \King\Awaitable {} + + /** + * Normalize a public MCP server definition for JSON-RPC stdio and + * Streamable HTTP transports. Creation options are applied once to the + * normalized definition; supported keys are `streamable_http.prefer_sse` + * and `streamable_http.allowed_origins`, with top-level shortcuts + * `prefer_sse` and `allowed_origins`. + * + * @param array $definition + * @param array|null $options + * @return array|false + */ + function king_mcp_server_create(array $definition, ?array $options = null): array|false {} + + /** + * Handle one public MCP JSON-RPC message. Requests return a JSON-encoded + * JSON-RPC response; notifications return null. + * + * @param array $server + * @param string|array $message + * @param array|null $context + */ + function king_mcp_server_handle_jsonrpc(array $server, mixed $message, ?array $context = null): ?string {} + + /** + * Adapt a King HTTP request array to the MCP Streamable HTTP transport. + * + * @param array $server + * @param array $request + * @param array|null $context + * @return array{status:int,headers:array,body:string} + */ + function king_mcp_server_handle_http(array $server, array $request, ?array $context = null): array {} + + /** + * Run the public MCP stdio transport until EOF. + * + * @param array $server + * @param array|null $options + */ + function king_mcp_server_stdio(array $server, ?array $options = null): bool {} + /** * Drain a PHP stream and upload the bytes to the active remote MCP peer * under the `(service, method, stream_identifier)` tuple. The tuple is @@ -1130,6 +1197,9 @@ function king_xslt_engine_status(): array {} /** * Transform one source XML file with one stylesheet and return the * materialized result string plus runtime metadata. + * Supported options are `cwd` and `properties`; unknown options are + * rejected. Source and stylesheet paths must resolve to readable local + * files; `cwd` must resolve to a readable and searchable local directory. * @param array|null $options * @return array * @throws \King\RuntimeException|\King\ValidationException @@ -1139,12 +1209,358 @@ function king_xslt_transform_file(string $source_path, string $stylesheet_path, /** * Transform one source XML file with one stylesheet and write the result * to `output_path`. + * Supported options are `cwd` and `properties`; unknown options are + * rejected. Source and stylesheet paths must resolve to readable local + * files; `cwd` must resolve to a readable and searchable local directory. + * Output is written through a temporary sibling file and moved into place + * only after SaxonC produced a readable result. * @param array|null $options * @return array * @throws \King\RuntimeException|\King\ValidationException */ function king_xslt_transform_to_file(string $source_path, string $stylesheet_path, string $output_path, ?array $options = null): array {} + /** + * Resolve the effective King runtime inference model config. + * The default profile is `auto`: when GPU use is enabled and + * `inference.gpu_model_artifact` is configured, King selects the + * `gemma4:12b` profile; otherwise it falls back to `gemma3:1b` through + * `inference.cpu_model_artifact`. The returned array can be passed to + * `king_inference_model_load()`. + * @param mixed $config null, King\Config, or native King\Config resource + * @return array + * @throws \King\ValidationException + */ + function king_inference_runtime_model_config(mixed $config = null): array {} + + /** + * Return all effective OpenAI-compatible runtime model registry entries + * derived from the current King inference config. + * @param mixed $config null, King\Config, or native King\Config resource + * @return array + * @throws \King\ValidationException + */ + function king_inference_runtime_model_registry_config(mixed $config = null): array {} + + /** + * Load the model selected by the effective King runtime inference profile. + * This is the direct primitive for applications that want "use the + * configured runtime model" instead of manually composing the model array. + * @param mixed $config null, King\Config, or native King\Config resource + * @throws \King\ValidationException|\King\RuntimeException + */ + function king_inference_runtime_model_load(mixed $config = null): \King\Inference\Model {} + + /** + * Return deterministic local mini-op content for OpenAI-style payloads + * when the runtime can prove the answer without model inference. + * @param array $payload + */ + function king_inference_runtime_mini_op_content(array $payload): ?string {} + + /** + * Return the effective GPU inference readiness for the current process or + * a provided King\Config snapshot. This reports configuration, artifact, + * driver/thermal visibility, the primary refusal reason, all active + * refusal reasons, and the current decoder-kernel readiness. + * @param mixed $config null, King\Config, or native King\Config resource + * @return array + * @throws \King\ValidationException + */ + function king_inference_gpu_runtime_status(mixed $config = null): array {} + + /** + * Return the effective LLM cache admission status for King inference. + * The cache is active only for memory-enabled native graph inference. When + * active, King checks the configured cache path and minimum free disk + * floor before admitting graph memory cache work. The default cache policy + * is disabled until the operator opts in through config. Options may + * override `with_memory` or `with-memory` for a preflight status check. + * @param mixed $config null, King\Config, or native King\Config resource + * @param array{with_memory?:bool,with-memory?:bool}|null $options + * @return array + * @throws \King\ValidationException|\King\RuntimeException + */ + function king_inference_llm_cache_status(mixed $config = null, ?array $options = null): array {} + + /** + * Register a local quantized GGUF model artifact for King inference. + * Artifacts are materialized filesystem paths supplied through `artifact`, + * `artifact.path`, or `artifact_path`; the selected path must be a + * non-empty string. Backend selection is config-driven. + * Optional `name`, `quantization`, `owned_by`, `embedding_tensor`, and + * `token_embedding_tensor` must be non-empty strings when provided; + * `context_tokens` must be a positive integer when provided. + * Omitting `backend` selects `king_native_cpu`. The `local` backend + * streams through the King runner contract; the + * `king_native_cpu` backend exposes native GGUF metadata, an internal + * tensor index, native tokenizer lookup, paged KV-cache planning, native + * graph-backed token streaming, and read-only model mapping without an + * external inference runtime. The `king_native_gpu` backend may register + * a GGUF artifact and expose metadata before GPU token generation is ready; + * its backend capabilities keep streaming and token generation disabled + * until the decoder kernel exists. Local runner streams preflight the + * runner executable before fork/exec. GPU use is disabled by default and + * requires explicit config plus thermal policy when enabled. GPU config is strict: + * `gpu.enabled` and `gpu.thermal.allow_unmonitored_gpu` must be booleans, + * `gpu.max_gpu_layers`, `gpu.vram_reserve_mb`, and `gpu.min_free_vram_mb` + * must be non-negative integers, + * `gpu.thermal.sensor_path` and `gpu.thermal.sensor_command` must be + * non-empty strings when provided, and + * `gpu.thermal.max_temperature_c` must be a positive finite number. + * `gpu.thermal.check_interval_seconds` must be a non-negative integer. + * Optional `gpu.power.max_watts` must be a non-negative finite number; + * values greater than zero require `gpu.power.sensor_command`. + * `gpu.power.check_interval_seconds` must be a non-negative integer. + * `gpu.debug.numeric_compare_enabled` and + * `gpu.debug.numeric_compare_max_values` are internal local diagnostic + * controls for the CUDA device-vector compare hook; they do not expose a + * public inference API and are disabled by default. + * @param array $config + * @throws \King\ValidationException|\King\RuntimeException + */ + function king_inference_model_load(array $config): \King\Inference\Model {} + + /** + * Return the normalized metadata for a loaded King inference model, + * including backend name, configured backend, active backend/device, + * fallback state, memory mode, backend capabilities, GPU readiness + * surfaces, explicit GPU decoder and generation readiness, and local + * runner executable status when the local backend is selected. + * @return array + */ + function king_inference_model_info(\King\Inference\Model $model): array {} + + /** + * Encode text with the native tokenizer loaded from the model artifact. + * @return array + * @throws \King\RuntimeException + */ + function king_inference_tokenize(\King\Inference\Model $model, string $text): array {} + + /** + * Decode one native token id with the tokenizer loaded from the model + * artifact. + * @throws \King\ValidationException|\King\RuntimeException + */ + function king_inference_token_decode(\King\Inference\Model $model, int $token_id): string {} + + /** + * Build the complete native CPU token decode graph for one token position. + * `$token` may be a non-negative token id or the array returned by + * `king_inference_tokenize()`, in which case `tokens[$position]` feeds the + * embedding step. The return payload includes a `terminal` descriptor for + * final norm and output projection wiring. + * @param int|array{tokens:array} $token + * @param array{emit_token?:bool,emit_logits?:bool,epsilon?:float,rope_base?:float,sampler?:string,temperature?:float,top_k?:int,top_p?:float,seed?:int,state?:array}|null $options + * @return array + * @throws \King\ValidationException|\King\RuntimeException + */ + function king_inference_token_decode_graph(\King\Inference\Model $model, int|array $token, int $position, ?array $options = null): array {} + + /** + * Return a read-only native tensor view descriptor for one tensor from the + * loaded GGUF artifact. The descriptor includes shape, quantized block + * format, byte range, bounds status, and native map readiness, but never + * exposes raw process addresses to PHP. + * @return array + * @throws \King\RuntimeException + */ + function king_inference_tensor_view(\King\Inference\Model $model, string $name): array {} + + /** + * Return tensor view descriptors from the loaded GGUF artifact. + * Options: `prefix` filters tensor names and `limit` limits returned + * descriptors. Use `limit <= 0` when the complete index is required. + * @param array{prefix?:string,limit?:int}|null $options + * @return array + */ + function king_inference_tensor_index(\King\Inference\Model $model, ?array $options = null): array {} + + /** + * Dequantize a bounded element range from one native tensor into PHP + * floats. Supported tensor formats are scalar F32/F16/BF16/I* types plus + * Q4_0, Q4_1, Q5_0, Q8_0, Q4_K, Q5_K, and Q6_K block formats. + * @param array{offset?:int,count?:int,max_values?:int}|null $options + * @return array + * @throws \King\ValidationException|\King\RuntimeException + */ + function king_inference_tensor_dequantize(\King\Inference\Model $model, string $name, ?array $options = null): array {} + + /** + * Multiply a rank-1 or rank-2 native tensor by a PHP vector on CPU. + * Quantized rows use blockwise dot decoding where supported. Rank-2 + * tensors use GGUF order: dimension 0 is columns/input width and dimension + * 1 is rows/output width. Limits are explicit options so large matrices + * fail safely instead of accidentally burning the host. + * @param list $input + * @param array{row_offset?:int,row_limit?:int,max_output_values?:int,max_input_values?:int,max_operations?:int}|null $options + * @return array + * @throws \King\ValidationException|\King\RuntimeException + */ + function king_inference_tensor_matmul(\King\Inference\Model $model, string $name, array $input, ?array $options = null): array {} + + /** + * Execute a bounded native tensor mini-graph on a loaded model. Supported + * ops are `embedding`, `rms_norm`, `linear`, `rope`, `dot`, `scale`, `mul`, + * `silu`, `slice`, and `add`, plus `stack`, `softmax`, and `weighted_sum` for + * attention-score and context-vector assembly. `kv_read` and `kv_write` read and update + * the serializable `state.kv_cache` payload returned by the graph runner; + * `kv_attention` consumes a strict slot range from that state and returns + * the scaled QK-softmax context vector for token decoding. `argmax_token` + * and `sample_token` select the next token from logits; `sample_token` + * accepts temperature, top-k, top-p, seed, sample_index, and token_offset, + * then returns `[token_id, probability, logit, rank]`. + * @param array $graph + * @param array{max_vector_values?:int,max_operations?:int,return_outputs?:bool}|null $options + * @return array + * @throws \King\ValidationException|\King\RuntimeException + */ + function king_inference_graph_run(\King\Inference\Model $model, array $graph, ?array $options = null): array {} + + /** + * Return the native paged KV-cache layout plan for a loaded model. + * @param array|null $request + * @return array + */ + function king_inference_kv_cache_plan(\King\Inference\Model $model, ?array $request = null): array {} + + /** + * Start token streaming through the configured King inference backend for + * a loaded model. Set `openai_compatible` or + * `format=openai_chat_completions` in request/options to receive + * Chat-Completions-style streaming chunks from `king_inference_next()`. + * `openai_compatible` must be a boolean and `format` must be one of + * `openai`, `openai_chat`, or `openai_chat_completions` when provided. + * Native graph streams require `graph` as an object array, `graphs` as a + * list array, and `graph_options` as an object array when provided. GPU + * streams perform fresh thermal and optional power preflight immediately + * before backend run admission and expose those readings through stream + * start events and `King\Inference\Stream::getMetrics()`. Active GPU + * streams are aborted when the configured thermal or power ceiling is + * reached, and native CUDA prompt decoding checks those guardrails from the + * token loop. Internal GPU stream + * contract diagnostics are emitted only when server-side stream options set + * `include_stream_diagnostics=true` or + * `diagnostics.stream_contracts=true`. + * @param array $request + * @param array|null $options + * @throws \King\ValidationException|\King\RuntimeException + */ + function king_inference_stream(\King\Inference\Model $model, array $request, ?array $options = null): \King\Inference\Stream {} + + /** + * Start a live OpenAI-compatible Chat Completions stream for a loaded King + * model. Unlike `king_inference_openai_chat_http_response()` this returns + * the stream object immediately so an HTTP router can flush each + * `king_inference_next()` chunk as SSE instead of buffering the full body. + * The payload is the decoded Chat Completions JSON object. Native OpenAI + * streams return token-content chunks immediately and do not emit a + * leading role-only chunk before the first native token. + * @param array $payload + * @param array|null $options + * @throws \King\ValidationException|\King\RuntimeException + */ + function king_inference_openai_chat_stream(\King\Inference\Model $model, array $payload, ?array $options = null): \King\Inference\Stream {} + + /** + * Handle one OpenAI-compatible `POST /v1/chat/completions` HTTP request + * and return a King HTTP server response array. The request body must be a + * JSON string containing a Chat Completions object with `messages`; + * `stream=true` returns a bounded `text/event-stream` body, otherwise an + * OpenAI-shaped `chat.completion` JSON body. `n` may be omitted or set to + * `1`; higher choice counts are rejected instead of being silently + * collapsed. Tool/function request fields are accepted as model context + * and logged, but not executed until a real King MCP tool path is bound to + * the route. Multimodal/audio output, prediction hints, and logprob output + * are rejected by the local generation route until those features have a + * real King execution path. + * GPU models return an explicit readiness refusal while generation is not + * ready instead of silently falling back to CPU. + * Options include + * `read_timeout_ms`, `max_events`, and `max_idle_events` for the bounded + * drain window, defaulting to `250`, `4096`, and `240`, plus + * `max_chat_messages`, defaulting to `256`. Router drain controls and + * limits must be positive integers. + * @param array $request + * @param array|null $options + * @return array{status:int,headers:array,body:string} + * @throws \King\ValidationException|\King\RuntimeException + */ + function king_inference_openai_chat_http_response(\King\Inference\Model $model, array $request, ?array $options = null): array {} + + /** + * Route one OpenAI-compatible HTTP request against a registry of loaded + * King inference models. Supported routes are `GET /v1/models`, + * `GET /v1/models/{model}`, `POST /v1/chat/completions`, + * `POST /v1/responses`, legacy `POST /v1/completions`, and + * `POST /v1/embeddings`. POST routes require `body` to be a JSON string + * containing a JSON object; missing, empty, array, or scalar request bodies + * are rejected before model selection. + * For generation requests, the JSON `model` field selects a string key + * from `$models`, the model name stored in the loaded King model, or the + * listed OpenAI model id. If exactly one model is registered, `model` may + * be omitted. Registered models must resolve to unique listed OpenAI model + * ids. Embeddings use the configured `embedding_tensor` or a known + * GGUF token embedding tensor and return float vectors or base64-encoded + * Float32LE vectors. Embedding requests may set `dimensions` when a + * bounded prefix of the native embedding width is required. Embedding + * inputs, tokens, and dimensions are bounded by `max_embedding_inputs`, + * `max_embedding_tokens`, and `max_embedding_dimensions`, defaulting to + * `2048`, `8192`, and `8192`. + * Chat Completions and legacy Completions accept `n` only when it is `1`. + * Active tool, multimodal, prediction, reasoning, best-of, echo, suffix, + * continuation, include-filter, and logprob feature requests are rejected + * by local generation routes instead of being silently ignored. + * Chat Completions message arrays are bounded by `max_chat_messages`, + * defaulting to `256`. Responses input item lists are bounded by + * `max_response_input_items`, defaulting to `256`; Responses + * `instructions` must be a non-empty string when provided. + * Legacy Completions prompt arrays are bounded by `max_completion_prompts` + * in `$options`, defaulting to `128`. `read_timeout_ms`, `max_events`, and + * `max_idle_events` default to `250`, `4096`, and `240` and must be + * positive integers like the router limits. `stream_options`, + * `response_format`, and native `graph_options` must be JSON objects when + * provided; native `graphs` remains the explicit graph sequence array. + * Model-listing option + * `owned_by` overrides model config owners and must be a non-empty string + * when provided. Model listing responses always include integer + * `created`; `x_king.created_config_valid` reports whether model config + * provided a usable non-negative integer timestamp. GPU generation + * readiness must be read from `x_king.openai_generation`, + * `x_king.readiness.openai_generation_ready`, and + * `x_king.client_capabilities`, not from the static backend capability + * map alone. + * @param array $models + * @param array $request + * @param array|null $options + * @return array{status:int,headers:array,body:string} + * @throws \King\ValidationException|\King\RuntimeException + */ + function king_inference_openai_http_response(array $models, array $request, ?array $options = null): array {} + + /** + * Read the next inference stream event. Native events use `type=start`, + * `token`, `stderr`, `done`, or `cancelled`; OpenAI-compatible streams + * return `chat.completion.chunk`-style arrays. The start event carries + * `backend`, `native_stream`, `pid`, and GPU thermal/power-preflight + * fields where applicable. Thermal or power ceiling aborts terminate the + * stream and are exposed through `King\Inference\Stream::getMetrics()`. + * @return array|null + */ + function king_inference_next(\King\Inference\Stream $stream, ?int $timeout_ms = null): ?array {} + + /** + * Start one asynchronous read against an inference stream. The awaitable + * resolves with the next event or terminal null when the stream is done. + */ + function king_inference_next_async(\King\Inference\Stream $stream, ?int $timeout_ms = null): \King\Awaitable {} + + /** + * Cancel an active inference stream and release backend resources. + */ + function king_inference_cancel(\King\Inference\Stream $stream): bool {} + /** * Telemetry runtime status, queue pressure, and self-observation counters. * @return array{ @@ -1476,7 +1892,7 @@ function king_autoscaling_drain_node(int $server_id): bool {} /** * Initializes the local semantic-DNS core/runtime config snapshot. * Supported keys include bind/port, TTL/discovery limits, semantic mode, - * mother-node URI, and optional `routing_policies`. + * mother-node URI, durable `state_path`, and optional `routing_policies`. * @param array $config * @throws \King\ValidationException|\King\SystemException */ @@ -2002,6 +2418,18 @@ public function isDone(): bool {} public function isCancelled(): bool {} public function getStatus(): string {} public function getOperation(): string {} + + /** + * Resolve when the first child awaitable reaches a terminal state. + * @param array $awaitables + */ + public static function any(array $awaitables, ?CancelToken $cancel = null): Awaitable {} + + /** + * Resolve when all child awaitables reach a terminal state. + * @param array $awaitables + */ + public static function all(array $awaitables, ?CancelToken $cancel = null): Awaitable {} } /* =========================== @@ -2021,7 +2449,7 @@ public function __construct(?array $options = null) {} public static function new(array $options = []): static {} /** - * Read a key from the active OO config parity slice. + * Read a key from the active config snapshot or explicit OO overrides. */ public function get(string $key): mixed {} @@ -2031,7 +2459,7 @@ public function get(string $key): mixed {} public function set(string $key, mixed $value): void {} /** - * Export the active OO config parity slice plus explicit OO overrides. + * Export the active config snapshot plus explicit OO overrides. * @return array */ public function toArray(): array {} @@ -2118,17 +2546,19 @@ public function read(int $length = 8192): string {} public function isEndOfBody(): bool {} } - /* =========================== - * MCP (Model Context Protocol) - * =========================== */ + /* ================================== + * MCP (King internal peer transport) + * ================================== */ final class MCP { /** * Materialize the MCP connection-state wrapper for host, port, * optional `King\Config`, and the explicit open/closed lifecycle over - * the active remote peer socket. + * the active remote peer socket. `options['default_timeout_ms']` sets + * the per-connection timeout default; per-call `timeout_ms` overrides + * it. * @throws ValidationException */ - public function __construct(string $host, int $port, ?Config $config = null) {} + public function __construct(string $host, int $port, ?Config $config = null, ?array $options = null) {} /** * Unary RPC call over the MCP connection-state wrapper. @@ -2141,6 +2571,19 @@ public function request(string $service, string $method, string $payload, ?Cance public function requestAsync(string $service, string $method, string $payload, ?CancelToken $cancel = null, ?array $options = null): Awaitable {} + /** + * IIBIN-aware unary call over King's internal line-framed MCP peer + * transport. The route contract is read from the connection's + * `King\Config` snapshot under `mcp.iibin_routes`; callers pass only + * the structured data for the configured request schema. + * + * @return array|object|string + * @throws RuntimeException|ValidationException|MCPConnectionException|MCPProtocolException|MCPTimeoutException + */ + public function requestIibin(string $service, string $method, mixed $data, ?CancelToken $cancel = null, ?array $options = null): array|object|string {} + + public function requestIibinAsync(string $service, string $method, mixed $data, ?CancelToken $cancel = null, ?array $options = null): Awaitable {} + /** * Drain a source stream into the remote MCP transfer store keyed by * `(service, method, streamIdentifier)`. The tuple is encoded @@ -2163,6 +2606,38 @@ public function downloadToStream(string $service, string $method, string $payloa public function close(): void {} } + /** + * Public MCP server surface for JSON-RPC stdio and Streamable HTTP. + */ + final class MCPServer { + /** + * @param array $definition + * Supported creation options are `streamable_http.prefer_sse` and + * `streamable_http.allowed_origins`, with top-level shortcuts + * `prefer_sse` and `allowed_origins`. + * @param array|null $options + */ + public function __construct(array $definition, ?array $options = null) {} + + /** + * @param string|array $message + * @param array|null $context + */ + public function handleJsonRpc(mixed $message, ?array $context = null): ?string {} + + /** + * @param array $request + * @param array|null $context + * @return array{status:int,headers:array,body:string} + */ + public function handleHttp(array $request, ?array $context = null): array {} + + /** + * @param array|null $options + */ + public function runStdio(?array $options = null): bool {} + } + /* =========================== * Pipeline Orchestrator * =========================== */ @@ -2337,6 +2812,136 @@ public static function markNodeReady(int $server_id): bool {} public static function drainNode(int $server_id): bool {} } + /* =========================== + * Inference + * =========================== */ + final class Inference { + /** + * @param mixed $config null, King\Config, or native King\Config resource + * @return array + */ + public static function runtimeModelConfig(mixed $config = null): array {} + + /** + * @param mixed $config null, King\Config, or native King\Config resource + * @return array + */ + public static function runtimeModelRegistryConfig(mixed $config = null): array {} + + /** + * @param mixed $config null, King\Config, or native King\Config resource + */ + public static function runtimeModelLoad(mixed $config = null): Inference\Model {} + + /** + * @param array $payload + */ + public static function runtimeMiniOpContent(array $payload): ?string {} + + /** + * @param mixed $config null, King\Config, or native King\Config resource + * @return array + */ + public static function gpuRuntimeStatus(mixed $config = null): array {} + + /** + * @param mixed $config null, King\Config, or native King\Config resource + * @param array{with_memory?:bool,with-memory?:bool}|null $options + * @return array + */ + public static function llmCacheStatus(mixed $config = null, ?array $options = null): array {} + + /** @param array $config */ + public static function loadModel(array $config): Inference\Model {} + + /** @return array */ + public static function modelInfo(Inference\Model $model): array {} + + /** @return array */ + public static function tokenize(Inference\Model $model, string $text): array {} + + public static function tokenDecode(Inference\Model $model, int $token_id): string {} + + /** + * The return payload includes a `terminal` descriptor for final norm + * and output projection wiring. + * @param int|array{tokens:array} $token + * @param array{emit_token?:bool,emit_logits?:bool,epsilon?:float,rope_base?:float,sampler?:string,temperature?:float,top_k?:int,top_p?:float,seed?:int,state?:array}|null $options + * @return array + */ + public static function tokenDecodeGraph(Inference\Model $model, int|array $token, int $position, ?array $options = null): array {} + + /** @return array */ + public static function tensorView(Inference\Model $model, string $name): array {} + + /** + * @param array{prefix?:string,limit?:int}|null $options + * @return array + */ + public static function tensorIndex(Inference\Model $model, ?array $options = null): array {} + + /** + * @param array{offset?:int,count?:int,max_values?:int}|null $options + * @return array + */ + public static function tensorDequantize(Inference\Model $model, string $name, ?array $options = null): array {} + + /** + * @param list $input + * @param array{row_offset?:int,row_limit?:int,max_output_values?:int,max_input_values?:int,max_operations?:int}|null $options + * @return array + */ + public static function tensorMatmul(Inference\Model $model, string $name, array $input, ?array $options = null): array {} + + /** + * @param array $graph + * @param array{max_vector_values?:int,max_operations?:int}|null $options + * @return array + */ + public static function graphRun(Inference\Model $model, array $graph, ?array $options = null): array {} + + /** + * @param array|null $request + * @return array + */ + public static function kvCachePlan(Inference\Model $model, ?array $request = null): array {} + + /** + * @param array $request + * @param array|null $options + * `openai_compatible` must be a boolean and `format` must be one of + * `openai`, `openai_chat`, or `openai_chat_completions` when provided. + * Native graph streams require `graph` as an object array, `graphs` as + * a list array, and `graph_options` as an object array when provided. + * Internal GPU stream contract diagnostics are emitted only when + * server-side stream options set `include_stream_diagnostics=true` or + * `diagnostics.stream_contracts=true`. + */ + public static function stream(Inference\Model $model, array $request, ?array $options = null): Inference\Stream {} + + /** + * @param array $request + * @param array|null $options + * @return array{status:int,headers:array,body:string} + */ + public static function openaiChatHttpResponse(Inference\Model $model, array $request, ?array $options = null): array {} + + /** + * @param array $models + * @param array $request + * @param array|null $options + * @return array{status:int,headers:array,body:string} + */ + public static function openaiHttpResponse(array $models, array $request, ?array $options = null): array {} + + /** @return array|null Native event or OpenAI-compatible chunk. */ + public static function next(Inference\Stream $stream, ?int $timeout_ms = null): ?array {} + + public static function nextAsync(Inference\Stream $stream, ?int $timeout_ms = null): \King\Awaitable {} + + public static function cancel(Inference\Stream $stream): bool {} + } + } /* =========================== @@ -2413,3 +3018,179 @@ public function close(int $code = 1000, ?string $reason = null): void {} public function getInfo(): array {} } } + +/* =========================== + * XSLT 2.0/3.0 + * =========================== */ +namespace King\XSLT { + final class Processor { + /** + * Supported options are `cwd` and `properties`; unknown options are + * rejected. Source and stylesheet paths must resolve to readable local + * files; `cwd` must resolve to a readable and searchable local + * directory. + * @param array|null $options + */ + public function __construct(?array $options = null) {} + + /** + * @return array + */ + public function getOptions(): array {} + + /** + * @return array + */ + public function engineStatus(): array {} + + /** + * Supported options are `cwd` and `properties`; unknown options are + * rejected. Source and stylesheet paths must resolve to readable local + * files; `cwd` must resolve to a readable and searchable local + * directory. + * @param array|null $options + * @return array + */ + public function transformFile(string $source_path, string $stylesheet_path, ?array $options = null): array {} + + /** + * Supported options are `cwd` and `properties`; unknown options are + * rejected. Source and stylesheet paths must resolve to readable local + * files; `cwd` must resolve to a readable and searchable local + * directory. + * Output is written through a temporary sibling file and moved into + * place only after SaxonC produced a readable result. + * @param array|null $options + * @return array + */ + public function transformToFile(string $source_path, string $stylesheet_path, string $output_path, ?array $options = null): array {} + } +} + +/* =========================== + * Local Quantized Inference + * =========================== */ +namespace King\Inference { + final class Model { + /** @param array $config */ + public function __construct(array $config) {} + + /** @return array */ + public function info(): array {} + + /** @return array */ + public function tokenize(string $text): array {} + + public function tokenDecode(int $token_id): string {} + + /** + * The return payload includes a `terminal` descriptor for final norm + * and output projection wiring. + * @param int|array{tokens:array} $token + * @param array{emit_token?:bool,emit_logits?:bool,epsilon?:float,rope_base?:float,sampler?:string,temperature?:float,top_k?:int,top_p?:float,seed?:int,state?:array}|null $options + * @return array + */ + public function tokenDecodeGraph(int|array $token, int $position, ?array $options = null): array {} + + /** @return array */ + public function tensorView(string $name): array {} + + /** + * @param array{prefix?:string,limit?:int}|null $options + * @return array + */ + public function tensorIndex(?array $options = null): array {} + + /** + * @param array{offset?:int,count?:int,max_values?:int}|null $options + * @return array + */ + public function tensorDequantize(string $name, ?array $options = null): array {} + + /** + * @param list $input + * @param array{row_offset?:int,row_limit?:int,max_output_values?:int,max_input_values?:int,max_operations?:int}|null $options + * @return array + */ + public function tensorMatmul(string $name, array $input, ?array $options = null): array {} + + /** + * @param array $graph + * @param array{max_vector_values?:int,max_operations?:int}|null $options + * @return array + */ + public function graphRun(array $graph, ?array $options = null): array {} + + /** + * @param array|null $request + * @return array + */ + public function kvCachePlan(?array $request = null): array {} + } + + final class Stream { + /** + * @param array $request + * @param array|null $options + * Set `openai_compatible` or `format=openai_chat_completions` to emit + * Chat-Completions-style chunks from next(). `openai_compatible` must + * be a boolean and native graph request shapes are validated before the + * backend starts. + */ + public function __construct(Model $model, array $request, ?array $options = null) {} + + /** @return array|null Native event or OpenAI-compatible chunk. */ + public function next(?int $timeout_ms = null): ?array {} + + public function nextAsync(?int $timeout_ms = null): \King\Awaitable {} + + public function cancel(): bool {} + + public function isDone(): bool {} + + /** + * @return array Includes chunk/error counters plus + * `native_stream`, `native_event_count`, and `native_event_index` for + * graph-backed native CPU streams. + */ + public function getMetrics(): array {} + } +} + +/* =========================== + * RTP / ICE-lite / DTLS-SRTP + * =========================== */ +namespace King\RTP { + final class Socket { + public function __construct(string $host, int $port) {} + + /** @return array{ufrag:string,pwd:string} */ + public function iceCredentials(): array {} + + public function dtlsFingerprint(): string {} + + public function acceptDtls(string $ip, int $port, int $timeout_ms = 5000): bool {} + + /** + * @return array{ + * ssrc:int, + * payload_type:int, + * seq:int, + * timestamp:int, + * marker:bool, + * data:string, + * from_ip:string, + * from_port:int, + * ice_consented:bool, + * srtp:bool + * }|false + */ + public function receive(int $timeout_ms = 0): array|false {} + + public function send(string $host, int $port, string $data): bool {} + + public function close(): void {} + + public function isClosed(): bool {} + } +}