From 048e752294df37d11ccf9c330bdc16f5a9797d7b Mon Sep 17 00:00:00 2001 From: wellwei <96378453+wellwei@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:59:28 +0800 Subject: [PATCH 1/2] feat(pkg): add ggml-org.llamacpp b10069 --- .github/workflows/validate.yml | 71 +- .gitignore | 1 + mcpp.toml | 2 + pkgs/g/ggml-org.llamacpp.lua | 946 ++++++++++++++++++ tests/check_llamacpp_snapshot.py | 700 +++++++++++++ .../examples/llamacpp-internal-cpu/mcpp.toml | 9 + .../llamacpp-internal-cpu/tests/decode.cpp | 101 ++ .../llamacpp-internal-metal/mcpp.toml | 12 + .../llamacpp-internal-metal/tests/decode.cpp | 198 ++++ tests/fetch_llamacpp_model.py | 135 +++ tests/test_check_llamacpp_snapshot.py | 275 +++++ tests/test_fetch_llamacpp_model.py | 142 +++ tests/test_llamacpp_package_contract.py | 94 ++ tools/llamacpp/audit_snapshot.py | 559 +++++++++++ tools/llamacpp/gen_exports.py | 280 ++++++ tools/llamacpp/module/gen_exports/llama.inc | 352 +++++++ .../module/gen_exports/llama.skipped.txt | 51 + .../module/gen_exports/required_ggml.inc | 97 ++ tools/llamacpp/module/llama.cppm | 42 + tools/llamacpp/snapshots/b10069.json | 275 +++++ tools/llamacpp/test_audit_snapshot.py | 402 ++++++++ tools/llamacpp/test_gen_exports.py | 275 +++++ 22 files changed, 5015 insertions(+), 4 deletions(-) create mode 100644 pkgs/g/ggml-org.llamacpp.lua create mode 100644 tests/check_llamacpp_snapshot.py create mode 100644 tests/examples/llamacpp-internal-cpu/mcpp.toml create mode 100644 tests/examples/llamacpp-internal-cpu/tests/decode.cpp create mode 100644 tests/examples/llamacpp-internal-metal/mcpp.toml create mode 100644 tests/examples/llamacpp-internal-metal/tests/decode.cpp create mode 100644 tests/fetch_llamacpp_model.py create mode 100644 tests/test_check_llamacpp_snapshot.py create mode 100644 tests/test_fetch_llamacpp_model.py create mode 100644 tests/test_llamacpp_package_contract.py create mode 100644 tools/llamacpp/audit_snapshot.py create mode 100644 tools/llamacpp/gen_exports.py create mode 100644 tools/llamacpp/module/gen_exports/llama.inc create mode 100644 tools/llamacpp/module/gen_exports/llama.skipped.txt create mode 100644 tools/llamacpp/module/gen_exports/required_ggml.inc create mode 100644 tools/llamacpp/module/llama.cppm create mode 100644 tools/llamacpp/snapshots/b10069.json create mode 100644 tools/llamacpp/test_audit_snapshot.py create mode 100644 tools/llamacpp/test_gen_exports.py diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 4a7d33f..ba2b3e9 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -5,7 +5,7 @@ on: # mcpp.toml and index.toml carry the workspace member list, the inherited # [indices] redirect and the client version floor — a change to any of them # can break every member, so they gate the run like the descriptors do. - paths: ["pkgs/**/*.lua", "tests/**", "README.md", "mcpp.toml", "index.toml", ".github/workflows/validate.yml"] + paths: ["pkgs/**/*.lua", "tests/**", "tools/**", "README.md", "mcpp.toml", "index.toml", ".github/workflows/validate.yml"] push: branches: [main] schedule: @@ -82,6 +82,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" - name: Install lua run: sudo apt-get install -y --no-install-recommends lua5.4 - name: Lint package descriptors @@ -148,6 +151,21 @@ jobs: # because it needs the full set of declared identities. - name: Lint cross-package references run: lua5.4 tests/check_cross_package_refs.lua pkgs/*/*.lua + - name: Check ggml-org.llamacpp snapshot + run: | + python3 -m unittest tools/llamacpp/test_audit_snapshot.py -v + python3 -m unittest tools/llamacpp/test_gen_exports.py -v + python3 -m unittest tests/test_fetch_llamacpp_model.py -v + python3 -m unittest tests/test_llamacpp_package_contract.py -v + python3 -m unittest tests/test_check_llamacpp_snapshot.py -v + python3 tools/llamacpp/audit_snapshot.py \ + --check tools/llamacpp/snapshots/b10069.json \ + --check-exports tools/llamacpp/module/gen_exports \ + --tag b10069 \ + --commit 178a6c44937154dc4c4eff0d166f4a044c4fceba \ + --url https://github.com/ggml-org/llama.cpp/archive/refs/tags/b10069.tar.gz \ + --sha256 293a7c65a11e2203c5468a06d0d0e8d21dfff16ad08712b16c61efbe0d93e097 + python3 tests/check_llamacpp_snapshot.py # ── Single-source-of-truth grammar check ───────────────────────── # `mcpp xpkg parse` uses EXACTLY the resolver's parser, so what # passes here is what builds for users of the pinned MCPP_VERSION. @@ -263,6 +281,9 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" - name: Restore mcpp registry cache uses: actions/cache@v4 with: @@ -349,12 +370,14 @@ jobs: # which already forces a full run above. [ -d "tests/examples/$m" ] && add "$m" ;; pkgs/*.lua|pkgs/*/*.lua) - lib=$(basename "$f" .lua); lib=${lib#compat.} + lib=$(basename "$f" .lua); lib=${lib##*.} hit=0 for mt in tests/examples/*/mcpp.toml; do - if grep -q "$lib" "$mt"; then add "$(basename "$(dirname "$mt")")"; hit=1; fi + if grep -Eq "^[[:space:]]*([A-Za-z0-9_-]+\.)*${lib}[[:space:]]*=" "$mt"; then + add "$(basename "$(dirname "$mt")")"; hit=1 + fi done - [ "$hit" = 1 ] || echo "note: no workspace member exercises $f" ;; + [ "$hit" = 1 ] || full "no workspace member exercises $f" ;; # tools/ holds OFFLINE descriptor-generation and publishing # helpers (tools/compat-*/, tools/gtc/, publish_mcpp_index.sh). # Nothing under it is consumed by a package build: when one of @@ -372,6 +395,46 @@ jobs: echo "MEMBERS=$sel" >> "$GITHUB_ENV" echo "selected members: ${sel:-}" + - name: Cold-start llama.cpp package + shell: bash + run: | + package_store="ggml-org-x-ggml-org.llamacpp" + rm -rf "$HOME/.mcpp/registry/data/xpkgs/$package_store" + rm -rf ".mcpp/.xlings/data/xpkgs/$package_store" + members="$MEMBERS" + if [ "$members" = "__ALL__" ]; then + members="llamacpp-internal-cpu llamacpp-internal-metal" + fi + for member in $members; do + case "$member" in + *llama*) + rm -rf "tests/examples/$member/.mcpp/.xlings/data/xpkgs/$package_store" + rm -rf "tests/examples/$member/target/.build-mcpp/deps/ggml-org.llamacpp@b10069" + ;; + esac + done + + - name: Provision pinned llama.cpp model + shell: bash + run: | + need_model=0 + if [ "$MEMBERS" = "__ALL__" ]; then + need_model=1 + else + for member in $MEMBERS; do + case "$member" in + *llama*) need_model=1 ;; + esac + done + fi + if [ "$need_model" = 1 ]; then + model="$RUNNER_TEMP/stories15M-q4_0.gguf" + python3 tests/fetch_llamacpp_model.py --output "$model" + echo "LLAMACPP_TEST_MODEL=$model" >> "$GITHUB_ENV" + else + echo "No llama.cpp member selected; model provisioning is not required." + fi + - name: mcpp test (workspace or affected members) shell: bash env: diff --git a/.gitignore b/.gitignore index 8bf7308..ae1075e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ tests/examples/*/mcpp.lock tests/examples/build-mcpp/src/generated.cpp imgui.ini tests/examples/*/imgui.ini +__pycache__/ diff --git a/mcpp.toml b/mcpp.toml index d5a3ad0..74a832a 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -25,6 +25,8 @@ members = [ "tests/examples/imgui", "tests/examples/imgui-module", "tests/examples/imgui-window", + "tests/examples/llamacpp-internal-cpu", + "tests/examples/llamacpp-internal-metal", "tests/examples/marzer.tomlplusplus", "tests/examples/nlohmann.json", "tests/examples/openblas", diff --git a/pkgs/g/ggml-org.llamacpp.lua b/pkgs/g/ggml-org.llamacpp.lua new file mode 100644 index 0000000..956e831 --- /dev/null +++ b/pkgs/g/ggml-org.llamacpp.lua @@ -0,0 +1,946 @@ +-- ggml-org.llamacpp - llama.cpp b10069 registry + inference implementation. +-- +-- Consumer syntax: +-- mcpp add ggml-org.llamacpp@b10069 +-- +-- The default/backend-cpu path uses CPU. backend-metal is supported only on +-- macOS AArch64. build.mcpp rejects every other feature before compilation. + +package = { + spec = "1", + namespace = "ggml-org", + name = "llamacpp", + description = "llama.cpp b10069 registry and inference implementation", + licenses = {"MIT"}, + repo = "https://github.com/ggml-org/llama.cpp", + type = "package", + + xpm = { + linux = { + ["b10069"] = { + url = "https://github.com/ggml-org/llama.cpp/archive/refs/tags/b10069.tar.gz", + sha256 = "293a7c65a11e2203c5468a06d0d0e8d21dfff16ad08712b16c61efbe0d93e097", + }, + }, + macosx = { + ["b10069"] = { + url = "https://github.com/ggml-org/llama.cpp/archive/refs/tags/b10069.tar.gz", + sha256 = "293a7c65a11e2203c5468a06d0d0e8d21dfff16ad08712b16c61efbe0d93e097", + }, + }, + windows = { + ["b10069"] = { + url = "https://github.com/ggml-org/llama.cpp/archive/refs/tags/b10069.tar.gz", + sha256 = "293a7c65a11e2203c5468a06d0d0e8d21dfff16ad08712b16c61efbe0d93e097", + }, + }, + }, + + mcpp = { + c_standard = "c11", + language = "c++23", + import_std = false, + include_dirs = { + "*/include", + "*/ggml/include", + "*/ggml/src", + "*/ggml/src/ggml-cpu", + "*/ggml/src/ggml-metal", + "*/src", + "mcpp_generated", + }, + modules = { "llama" }, + generated_files = { + ["mcpp_generated/ggml_cpp.cpp"] = "#include \"ggml.cpp\"\n", + ["mcpp_generated/ggml-cpu_cpp.cpp"] = "#include \"ggml-cpu.cpp\"\n", + ["mcpp_generated/ggml_metal_device_m.m"] = "#include \"ggml-metal-device.m\"\n", + ["mcpp_generated/ggml_build_info.h"] = [=[ +#pragma once +#define GGML_VERSION "b10069" +#define GGML_COMMIT "178a6c44937154dc4c4eff0d166f4a044c4fceba" +]=], + ["mcpp_generated/llama.cppm"] = [==[ +module; + +#include +#include +#include + +#undef LLAMA_DEFAULT_SEED +#undef LLAMA_TOKEN_NULL +#undef LLAMA_FILE_MAGIC_GGLA +#undef LLAMA_FILE_MAGIC_GGSN +#undef LLAMA_FILE_MAGIC_GGSQ +#undef LLAMA_SESSION_MAGIC +#undef LLAMA_SESSION_VERSION +#undef LLAMA_STATE_SEQ_MAGIC +#undef LLAMA_STATE_SEQ_VERSION +#undef LLAMA_STATE_SEQ_FLAGS_NONE +#undef LLAMA_STATE_SEQ_FLAGS_SWA_ONLY +#undef LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY +#undef LLAMA_STATE_SEQ_FLAGS_ON_DEVICE + +export module llama; + +#include "gen_exports/required_ggml.inc" +#include "gen_exports/llama.inc" + +// Typed constant replacements for public preprocessor macros. +// uint32_t and llama_token are provided by (global module fragment). +export inline constexpr uint32_t LLAMA_DEFAULT_SEED = 0xFFFFFFFFu; +export inline constexpr llama_token LLAMA_TOKEN_NULL = -1; + +export inline constexpr uint32_t LLAMA_FILE_MAGIC_GGLA = 0x67676c61u; +export inline constexpr uint32_t LLAMA_FILE_MAGIC_GGSN = 0x6767736eu; +export inline constexpr uint32_t LLAMA_FILE_MAGIC_GGSQ = 0x67677371u; +export inline constexpr uint32_t LLAMA_SESSION_MAGIC = 0x6767736eu; +export inline constexpr uint32_t LLAMA_SESSION_VERSION = 9u; +export inline constexpr uint32_t LLAMA_STATE_SEQ_MAGIC = 0x67677371u; +export inline constexpr uint32_t LLAMA_STATE_SEQ_VERSION = 2u; + +export inline constexpr llama_state_seq_flags LLAMA_STATE_SEQ_FLAGS_NONE = 0u; +export inline constexpr llama_state_seq_flags LLAMA_STATE_SEQ_FLAGS_SWA_ONLY = 1u; +export inline constexpr llama_state_seq_flags LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY = 1u; +export inline constexpr llama_state_seq_flags LLAMA_STATE_SEQ_FLAGS_ON_DEVICE = 2u; +]==], + ["mcpp_generated/gen_exports/required_ggml.inc"] = [==[ +export using ::GGML_BACKEND_DEVICE_TYPE_ACCEL; +export using ::GGML_BACKEND_DEVICE_TYPE_CPU; +export using ::GGML_BACKEND_DEVICE_TYPE_GPU; +export using ::GGML_LOG_LEVEL_CONT; +export using ::GGML_LOG_LEVEL_DEBUG; +export using ::GGML_LOG_LEVEL_ERROR; +export using ::GGML_LOG_LEVEL_INFO; +export using ::GGML_LOG_LEVEL_NONE; +export using ::GGML_LOG_LEVEL_WARN; +export using ::GGML_NUMA_STRATEGY_COUNT; +export using ::GGML_NUMA_STRATEGY_DISABLED; +export using ::GGML_NUMA_STRATEGY_DISTRIBUTE; +export using ::GGML_NUMA_STRATEGY_ISOLATE; +export using ::GGML_NUMA_STRATEGY_MIRROR; +export using ::GGML_NUMA_STRATEGY_NUMACTL; +export using ::GGML_OPT_OPTIMIZER_TYPE_ADAMW; +export using ::GGML_OPT_OPTIMIZER_TYPE_COUNT; +export using ::GGML_OPT_OPTIMIZER_TYPE_SGD; +export using ::GGML_STATUS_SUCCESS; +export using ::GGML_TYPE_BF16; +export using ::GGML_TYPE_COUNT; +export using ::GGML_TYPE_F16; +export using ::GGML_TYPE_F32; +export using ::GGML_TYPE_F64; +export using ::GGML_TYPE_I16; +export using ::GGML_TYPE_I32; +export using ::GGML_TYPE_I64; +export using ::GGML_TYPE_I8; +export using ::GGML_TYPE_IQ1_M; +export using ::GGML_TYPE_IQ1_S; +export using ::GGML_TYPE_IQ2_S; +export using ::GGML_TYPE_IQ2_XS; +export using ::GGML_TYPE_IQ2_XXS; +export using ::GGML_TYPE_IQ3_S; +export using ::GGML_TYPE_IQ3_XXS; +export using ::GGML_TYPE_IQ4_NL; +export using ::GGML_TYPE_IQ4_XS; +export using ::GGML_TYPE_MXFP4; +export using ::GGML_TYPE_NVFP4; +export using ::GGML_TYPE_Q1_0; +export using ::GGML_TYPE_Q2_0; +export using ::GGML_TYPE_Q2_K; +export using ::GGML_TYPE_Q3_K; +export using ::GGML_TYPE_Q4_0; +export using ::GGML_TYPE_Q4_1; +export using ::GGML_TYPE_Q4_K; +export using ::GGML_TYPE_Q5_0; +export using ::GGML_TYPE_Q5_1; +export using ::GGML_TYPE_Q5_K; +export using ::GGML_TYPE_Q6_K; +export using ::GGML_TYPE_Q8_0; +export using ::GGML_TYPE_Q8_1; +export using ::GGML_TYPE_Q8_K; +export using ::GGML_TYPE_TQ1_0; +export using ::GGML_TYPE_TQ2_0; +export using ::ggml_abort_callback; +export using ::ggml_add; +export using ::ggml_backend_alloc_ctx_tensors; +export using ::ggml_backend_buffer_free; +export using ::ggml_backend_buffer_t; +export using ::ggml_backend_buffer_type_t; +export using ::ggml_backend_dev_init; +export using ::ggml_backend_dev_t; +export using ::ggml_backend_dev_type; +export using ::ggml_backend_free; +export using ::ggml_backend_get_features_t; +export using ::ggml_backend_graph_compute; +export using ::ggml_backend_reg_by_name; +export using ::ggml_backend_reg_dev_count; +export using ::ggml_backend_reg_dev_get; +export using ::ggml_backend_reg_get_proc_address; +export using ::ggml_backend_reg_t; +export using ::ggml_backend_sched_eval_callback; +export using ::ggml_backend_synchronize; +export using ::ggml_backend_t; +export using ::ggml_backend_tensor_get; +export using ::ggml_backend_tensor_set; +export using ::ggml_build_forward_expand; +export using ::ggml_cgraph; +export using ::ggml_context; +export using ::ggml_free; +export using ::ggml_init; +export using ::ggml_init_params; +export using ::ggml_log_callback; +export using ::ggml_log_level; +export using ::ggml_new_graph; +export using ::ggml_new_tensor_1d; +export using ::ggml_numa_strategy; +export using ::ggml_opt_dataset_t; +export using ::ggml_opt_epoch_callback; +export using ::ggml_opt_get_optimizer_params; +export using ::ggml_opt_optimizer_type; +export using ::ggml_opt_result_t; +export using ::ggml_status; +export using ::ggml_tensor; +export using ::ggml_threadpool_t; +export using ::ggml_type; +]==], + ["mcpp_generated/gen_exports/llama.inc"] = [==[ +export using ::LLAMA_ATTENTION_TYPE_CAUSAL; +export using ::LLAMA_ATTENTION_TYPE_NON_CAUSAL; +export using ::LLAMA_ATTENTION_TYPE_UNSPECIFIED; +export using ::LLAMA_CONTEXT_TYPE_DEFAULT; +export using ::LLAMA_CONTEXT_TYPE_MTP; +export using ::LLAMA_FLASH_ATTN_TYPE_AUTO; +export using ::LLAMA_FLASH_ATTN_TYPE_DISABLED; +export using ::LLAMA_FLASH_ATTN_TYPE_ENABLED; +export using ::LLAMA_FTYPE_ALL_F32; +export using ::LLAMA_FTYPE_GUESSED; +export using ::LLAMA_FTYPE_MOSTLY_BF16; +export using ::LLAMA_FTYPE_MOSTLY_F16; +export using ::LLAMA_FTYPE_MOSTLY_IQ1_M; +export using ::LLAMA_FTYPE_MOSTLY_IQ1_S; +export using ::LLAMA_FTYPE_MOSTLY_IQ2_M; +export using ::LLAMA_FTYPE_MOSTLY_IQ2_S; +export using ::LLAMA_FTYPE_MOSTLY_IQ2_XS; +export using ::LLAMA_FTYPE_MOSTLY_IQ2_XXS; +export using ::LLAMA_FTYPE_MOSTLY_IQ3_M; +export using ::LLAMA_FTYPE_MOSTLY_IQ3_S; +export using ::LLAMA_FTYPE_MOSTLY_IQ3_XS; +export using ::LLAMA_FTYPE_MOSTLY_IQ3_XXS; +export using ::LLAMA_FTYPE_MOSTLY_IQ4_NL; +export using ::LLAMA_FTYPE_MOSTLY_IQ4_XS; +export using ::LLAMA_FTYPE_MOSTLY_MXFP4_MOE; +export using ::LLAMA_FTYPE_MOSTLY_NVFP4; +export using ::LLAMA_FTYPE_MOSTLY_Q1_0; +export using ::LLAMA_FTYPE_MOSTLY_Q2_0; +export using ::LLAMA_FTYPE_MOSTLY_Q2_K; +export using ::LLAMA_FTYPE_MOSTLY_Q2_K_S; +export using ::LLAMA_FTYPE_MOSTLY_Q3_K_L; +export using ::LLAMA_FTYPE_MOSTLY_Q3_K_M; +export using ::LLAMA_FTYPE_MOSTLY_Q3_K_S; +export using ::LLAMA_FTYPE_MOSTLY_Q4_0; +export using ::LLAMA_FTYPE_MOSTLY_Q4_1; +export using ::LLAMA_FTYPE_MOSTLY_Q4_K_M; +export using ::LLAMA_FTYPE_MOSTLY_Q4_K_S; +export using ::LLAMA_FTYPE_MOSTLY_Q5_0; +export using ::LLAMA_FTYPE_MOSTLY_Q5_1; +export using ::LLAMA_FTYPE_MOSTLY_Q5_K_M; +export using ::LLAMA_FTYPE_MOSTLY_Q5_K_S; +export using ::LLAMA_FTYPE_MOSTLY_Q6_K; +export using ::LLAMA_FTYPE_MOSTLY_Q8_0; +export using ::LLAMA_FTYPE_MOSTLY_TQ1_0; +export using ::LLAMA_FTYPE_MOSTLY_TQ2_0; +export using ::LLAMA_KV_OVERRIDE_TYPE_BOOL; +export using ::LLAMA_KV_OVERRIDE_TYPE_FLOAT; +export using ::LLAMA_KV_OVERRIDE_TYPE_INT; +export using ::LLAMA_KV_OVERRIDE_TYPE_STR; +export using ::LLAMA_MODEL_META_KEY_SAMPLING_MIN_P; +export using ::LLAMA_MODEL_META_KEY_SAMPLING_MIROSTAT; +export using ::LLAMA_MODEL_META_KEY_SAMPLING_MIROSTAT_ETA; +export using ::LLAMA_MODEL_META_KEY_SAMPLING_MIROSTAT_TAU; +export using ::LLAMA_MODEL_META_KEY_SAMPLING_PENALTY_LAST_N; +export using ::LLAMA_MODEL_META_KEY_SAMPLING_PENALTY_REPEAT; +export using ::LLAMA_MODEL_META_KEY_SAMPLING_SEQUENCE; +export using ::LLAMA_MODEL_META_KEY_SAMPLING_TEMP; +export using ::LLAMA_MODEL_META_KEY_SAMPLING_TOP_K; +export using ::LLAMA_MODEL_META_KEY_SAMPLING_TOP_P; +export using ::LLAMA_MODEL_META_KEY_SAMPLING_XTC_PROBABILITY; +export using ::LLAMA_MODEL_META_KEY_SAMPLING_XTC_THRESHOLD; +export using ::LLAMA_POOLING_TYPE_CLS; +export using ::LLAMA_POOLING_TYPE_LAST; +export using ::LLAMA_POOLING_TYPE_MEAN; +export using ::LLAMA_POOLING_TYPE_NONE; +export using ::LLAMA_POOLING_TYPE_RANK; +export using ::LLAMA_POOLING_TYPE_UNSPECIFIED; +export using ::LLAMA_ROPE_SCALING_TYPE_LINEAR; +export using ::LLAMA_ROPE_SCALING_TYPE_LONGROPE; +export using ::LLAMA_ROPE_SCALING_TYPE_MAX_VALUE; +export using ::LLAMA_ROPE_SCALING_TYPE_NONE; +export using ::LLAMA_ROPE_SCALING_TYPE_UNSPECIFIED; +export using ::LLAMA_ROPE_SCALING_TYPE_YARN; +export using ::LLAMA_ROPE_TYPE_IMROPE; +export using ::LLAMA_ROPE_TYPE_MROPE; +export using ::LLAMA_ROPE_TYPE_NEOX; +export using ::LLAMA_ROPE_TYPE_NONE; +export using ::LLAMA_ROPE_TYPE_NORM; +export using ::LLAMA_ROPE_TYPE_VISION; +export using ::LLAMA_SPLIT_MODE_LAYER; +export using ::LLAMA_SPLIT_MODE_NONE; +export using ::LLAMA_SPLIT_MODE_ROW; +export using ::LLAMA_SPLIT_MODE_TENSOR; +export using ::LLAMA_TOKEN_ATTR_BYTE; +export using ::LLAMA_TOKEN_ATTR_CONTROL; +export using ::LLAMA_TOKEN_ATTR_LSTRIP; +export using ::LLAMA_TOKEN_ATTR_NORMAL; +export using ::LLAMA_TOKEN_ATTR_NORMALIZED; +export using ::LLAMA_TOKEN_ATTR_RSTRIP; +export using ::LLAMA_TOKEN_ATTR_SINGLE_WORD; +export using ::LLAMA_TOKEN_ATTR_UNDEFINED; +export using ::LLAMA_TOKEN_ATTR_UNKNOWN; +export using ::LLAMA_TOKEN_ATTR_UNUSED; +export using ::LLAMA_TOKEN_ATTR_USER_DEFINED; +export using ::LLAMA_TOKEN_TYPE_BYTE; +export using ::LLAMA_TOKEN_TYPE_CONTROL; +export using ::LLAMA_TOKEN_TYPE_NORMAL; +export using ::LLAMA_TOKEN_TYPE_UNDEFINED; +export using ::LLAMA_TOKEN_TYPE_UNKNOWN; +export using ::LLAMA_TOKEN_TYPE_UNUSED; +export using ::LLAMA_TOKEN_TYPE_USER_DEFINED; +export using ::LLAMA_VOCAB_TYPE_BPE; +export using ::LLAMA_VOCAB_TYPE_NONE; +export using ::LLAMA_VOCAB_TYPE_PLAMO2; +export using ::LLAMA_VOCAB_TYPE_RWKV; +export using ::LLAMA_VOCAB_TYPE_SPM; +export using ::LLAMA_VOCAB_TYPE_UGM; +export using ::LLAMA_VOCAB_TYPE_WPM; +export using ::llama_adapter_get_alora_invocation_tokens; +export using ::llama_adapter_get_alora_n_invocation_tokens; +export using ::llama_adapter_lora; +export using ::llama_adapter_lora_free; +export using ::llama_adapter_lora_init; +export using ::llama_adapter_meta_count; +export using ::llama_adapter_meta_key_by_index; +export using ::llama_adapter_meta_val_str; +export using ::llama_adapter_meta_val_str_by_index; +export using ::llama_attach_threadpool; +export using ::llama_attention_type; +export using ::llama_backend_free; +export using ::llama_backend_init; +export using ::llama_batch; +export using ::llama_batch_free; +export using ::llama_batch_get_one; +export using ::llama_batch_init; +export using ::llama_chat_apply_template; +export using ::llama_chat_builtin_templates; +export using ::llama_chat_message; +export using ::llama_context; +export using ::llama_context_default_params; +export using ::llama_context_params; +export using ::llama_context_type; +export using ::llama_decode; +export using ::llama_detach_threadpool; +export using ::llama_detokenize; +export using ::llama_encode; +export using ::llama_flash_attn_type; +export using ::llama_flash_attn_type_name; +export using ::llama_free; +export using ::llama_ftype; +export using ::llama_ftype_name; +export using ::llama_get_embeddings; +export using ::llama_get_embeddings_ith; +export using ::llama_get_embeddings_seq; +export using ::llama_get_logits; +export using ::llama_get_logits_ith; +export using ::llama_get_memory; +export using ::llama_get_model; +export using ::llama_get_sampled_candidates_count_ith; +export using ::llama_get_sampled_candidates_ith; +export using ::llama_get_sampled_logits_count_ith; +export using ::llama_get_sampled_logits_ith; +export using ::llama_get_sampled_probs_count_ith; +export using ::llama_get_sampled_probs_ith; +export using ::llama_get_sampled_token_ith; +export using ::llama_init_from_model; +export using ::llama_log_get; +export using ::llama_log_set; +export using ::llama_logit_bias; +export using ::llama_max_devices; +export using ::llama_max_parallel_sequences; +export using ::llama_max_tensor_buft_overrides; +export using ::llama_memory_can_shift; +export using ::llama_memory_clear; +export using ::llama_memory_i; +export using ::llama_memory_seq_add; +export using ::llama_memory_seq_cp; +export using ::llama_memory_seq_div; +export using ::llama_memory_seq_keep; +export using ::llama_memory_seq_pos_max; +export using ::llama_memory_seq_pos_min; +export using ::llama_memory_seq_rm; +export using ::llama_memory_t; +export using ::llama_model; +export using ::llama_model_chat_template; +export using ::llama_model_cls_label; +export using ::llama_model_decoder_start_token; +export using ::llama_model_default_params; +export using ::llama_model_desc; +export using ::llama_model_free; +export using ::llama_model_ftype; +export using ::llama_model_get_vocab; +export using ::llama_model_has_decoder; +export using ::llama_model_has_encoder; +export using ::llama_model_imatrix_data; +export using ::llama_model_init_from_user; +export using ::llama_model_is_diffusion; +export using ::llama_model_is_hybrid; +export using ::llama_model_is_recurrent; +export using ::llama_model_kv_override; +export using ::llama_model_kv_override_type; +export using ::llama_model_load_from_file; +export using ::llama_model_load_from_file_ptr; +export using ::llama_model_load_from_splits; +export using ::llama_model_meta_count; +export using ::llama_model_meta_key; +export using ::llama_model_meta_key_by_index; +export using ::llama_model_meta_key_str; +export using ::llama_model_meta_val_str; +export using ::llama_model_meta_val_str_by_index; +export using ::llama_model_n_cls_out; +export using ::llama_model_n_ctx_train; +export using ::llama_model_n_embd; +export using ::llama_model_n_embd_inp; +export using ::llama_model_n_embd_out; +export using ::llama_model_n_head; +export using ::llama_model_n_head_kv; +export using ::llama_model_n_layer; +export using ::llama_model_n_layer_nextn; +export using ::llama_model_n_params; +export using ::llama_model_n_swa; +export using ::llama_model_params; +export using ::llama_model_quantize; +export using ::llama_model_quantize_default_params; +export using ::llama_model_quantize_params; +export using ::llama_model_rope_freq_scale_train; +export using ::llama_model_rope_type; +export using ::llama_model_save_to_file; +export using ::llama_model_set_tensor_data_t; +export using ::llama_model_size; +export using ::llama_model_tensor_buft_override; +export using ::llama_model_tensor_override; +export using ::llama_n_batch; +export using ::llama_n_ctx; +export using ::llama_n_ctx_seq; +export using ::llama_n_rs_seq; +export using ::llama_n_seq_max; +export using ::llama_n_threads; +export using ::llama_n_threads_batch; +export using ::llama_n_ubatch; +export using ::llama_numa_init; +export using ::llama_opt_epoch; +export using ::llama_opt_init; +export using ::llama_opt_param_filter; +export using ::llama_opt_param_filter_all; +export using ::llama_opt_params; +export using ::llama_perf_context; +export using ::llama_perf_context_data; +export using ::llama_perf_context_print; +export using ::llama_perf_context_reset; +export using ::llama_perf_sampler; +export using ::llama_perf_sampler_data; +export using ::llama_perf_sampler_print; +export using ::llama_perf_sampler_reset; +export using ::llama_pooling_type; +export using ::llama_pos; +export using ::llama_print_system_info; +export using ::llama_progress_callback; +export using ::llama_rope_scaling_type; +export using ::llama_rope_type; +export using ::llama_sampler; +export using ::llama_sampler_accept; +export using ::llama_sampler_apply; +export using ::llama_sampler_chain_add; +export using ::llama_sampler_chain_default_params; +export using ::llama_sampler_chain_get; +export using ::llama_sampler_chain_init; +export using ::llama_sampler_chain_n; +export using ::llama_sampler_chain_params; +export using ::llama_sampler_chain_remove; +export using ::llama_sampler_clone; +export using ::llama_sampler_context_t; +export using ::llama_sampler_data; +export using ::llama_sampler_free; +export using ::llama_sampler_get_seed; +export using ::llama_sampler_i; +export using ::llama_sampler_init; +export using ::llama_sampler_init_adaptive_p; +export using ::llama_sampler_init_dist; +export using ::llama_sampler_init_dry; +export using ::llama_sampler_init_grammar; +export using ::llama_sampler_init_grammar_lazy_patterns; +export using ::llama_sampler_init_greedy; +export using ::llama_sampler_init_infill; +export using ::llama_sampler_init_logit_bias; +export using ::llama_sampler_init_min_p; +export using ::llama_sampler_init_mirostat; +export using ::llama_sampler_init_mirostat_v2; +export using ::llama_sampler_init_penalties; +export using ::llama_sampler_init_temp; +export using ::llama_sampler_init_temp_ext; +export using ::llama_sampler_init_top_k; +export using ::llama_sampler_init_top_n_sigma; +export using ::llama_sampler_init_top_p; +export using ::llama_sampler_init_typical; +export using ::llama_sampler_init_xtc; +export using ::llama_sampler_name; +export using ::llama_sampler_reset; +export using ::llama_sampler_sample; +export using ::llama_sampler_seq_config; +export using ::llama_seq_id; +export using ::llama_set_abort_callback; +export using ::llama_set_adapter_cvec; +export using ::llama_set_adapters_lora; +export using ::llama_set_causal_attn; +export using ::llama_set_embeddings; +export using ::llama_set_n_threads; +export using ::llama_set_sampler; +export using ::llama_split_mode; +export using ::llama_split_path; +export using ::llama_split_prefix; +export using ::llama_state_get_data; +export using ::llama_state_get_size; +export using ::llama_state_load_file; +export using ::llama_state_save_file; +export using ::llama_state_seq_flags; +export using ::llama_state_seq_get_data; +export using ::llama_state_seq_get_data_ext; +export using ::llama_state_seq_get_size; +export using ::llama_state_seq_get_size_ext; +export using ::llama_state_seq_load_file; +export using ::llama_state_seq_save_file; +export using ::llama_state_seq_set_data; +export using ::llama_state_seq_set_data_ext; +export using ::llama_state_set_data; +export using ::llama_supports_gpu_offload; +export using ::llama_supports_mlock; +export using ::llama_supports_mmap; +export using ::llama_supports_rpc; +export using ::llama_synchronize; +export using ::llama_time_us; +export using ::llama_token; +export using ::llama_token_attr; +export using ::llama_token_data; +export using ::llama_token_data_array; +export using ::llama_token_to_piece; +export using ::llama_token_type; +export using ::llama_tokenize; +export using ::llama_vocab; +export using ::llama_vocab_bos; +export using ::llama_vocab_eos; +export using ::llama_vocab_eot; +export using ::llama_vocab_fim_mid; +export using ::llama_vocab_fim_pad; +export using ::llama_vocab_fim_pre; +export using ::llama_vocab_fim_rep; +export using ::llama_vocab_fim_sep; +export using ::llama_vocab_fim_suf; +export using ::llama_vocab_get_add_bos; +export using ::llama_vocab_get_add_eos; +export using ::llama_vocab_get_add_sep; +export using ::llama_vocab_get_attr; +export using ::llama_vocab_get_score; +export using ::llama_vocab_get_text; +export using ::llama_vocab_is_control; +export using ::llama_vocab_is_eog; +export using ::llama_vocab_mask; +export using ::llama_vocab_n_tokens; +export using ::llama_vocab_nl; +export using ::llama_vocab_pad; +export using ::llama_vocab_sep; +export using ::llama_vocab_type; +]==], + ["build.mcpp"] = [=[ +#include +#include +#include +#include +#include +#include +#include +#include +namespace fs = std::filesystem; +static std::string read_all(const fs::path & path) { + std::ifstream in(path, std::ios::binary); + if (!in) throw std::runtime_error("cannot read " + path.string()); + return {std::istreambuf_iterator(in), std::istreambuf_iterator()}; +} +static void write_all(const fs::path & path, const std::string & value) { + fs::create_directories(path.parent_path()); + std::ofstream out(path, std::ios::binary | std::ios::trunc); + out.write(value.data(), static_cast(value.size())); + if (!out) throw std::runtime_error("cannot write " + path.string()); +} +static void replace_once(std::string & value, const std::string & marker, + const std::string & replacement) { + auto first = value.find(marker); + if (first == std::string::npos + || value.find(marker, first + marker.size()) != std::string::npos) { + throw std::runtime_error("expected exactly one marker: " + marker); + } + value.replace(first, marker.size(), replacement); +} +static std::string asm_quote(std::string value) { + std::string out; + for (char c : value) { + if (c == '\\' || c == '"') out.push_back('\\'); + out.push_back(c); + } + return out; +} +static int validate_features() { + const char * raw = std::getenv("MCPP_FEATURES"); + if (!raw || *raw == '\0') return 0; + std::istringstream input(raw); + std::string feature; + while (std::getline(input, feature, ',')) { + if (feature.empty() || feature == "backend-cpu" || feature == "backend-metal") { + continue; + } + std::fprintf(stderr, + "ggml-org.llamacpp: unsupported feature '%s' " + "(supported features: backend-cpu, backend-metal)\n", + feature.c_str()); + return 2; + } + return 0; +} +int main() try { + if (int result = validate_features(); result != 0) return result; + const char * enabled = std::getenv("MCPP_FEATURE_BACKEND_METAL"); + if (!enabled || std::string(enabled) != "1") return 0; + const char * os = std::getenv("MCPP_TARGET_OS"); + const char * arch = std::getenv("MCPP_TARGET_ARCH"); + const char * manifest = std::getenv("MCPP_MANIFEST_DIR"); + const char * out_env = std::getenv("MCPP_OUT_DIR"); + if (!os || std::string(os) != "macos") { + std::fprintf(stderr, "ggml-org.llamacpp requires target_os=macos\n"); + return 2; + } + if (!arch || std::string(arch) != "aarch64") { + std::fprintf(stderr, "ggml-org.llamacpp requires target_arch=aarch64\n"); + return 2; + } + if (!manifest || !out_env) { + std::fprintf(stderr, "ggml-org.llamacpp requires MCPP_MANIFEST_DIR and MCPP_OUT_DIR\n"); + return 2; + } + fs::path root; + for (const auto & entry : fs::directory_iterator(manifest)) { + fs::path candidate = entry.path(); + if (entry.is_directory() && fs::exists(candidate / "ggml/src/ggml-common.h") + && fs::exists(candidate / "ggml/src/ggml-metal/ggml-metal.metal")) { + if (!root.empty()) throw std::runtime_error("multiple llama.cpp source roots"); + root = candidate; + } + } + if (root.empty()) throw std::runtime_error("llama.cpp source root not found"); + const fs::path common = root / "ggml/src/ggml-common.h"; + const fs::path metal = root / "ggml/src/ggml-metal/ggml-metal.metal"; + const fs::path impl = root / "ggml/src/ggml-metal/ggml-metal-impl.h"; + const fs::path out = out_env; + const fs::path merged = out / "ggml-metal-embed.metal"; + const fs::path assembly = out / "ggml-metal-embed.s"; + std::string source = read_all(metal); + replace_once(source, "__embed_ggml-common.h__", read_all(common)); + replace_once(source, "#include \"ggml-metal-impl.h\"", read_all(impl)); + write_all(merged, source); + std::ostringstream body; + body << ".section __DATA,__ggml_metallib\n" + << ".globl _ggml_metallib_start\n" + << "_ggml_metallib_start:\n" + << ".incbin \"" << asm_quote(merged.string()) << "\"\n" + << ".globl _ggml_metallib_end\n" + << "_ggml_metallib_end:\n"; + write_all(assembly, body.str()); + std::printf("mcpp:generated=%s\n", assembly.string().c_str()); + std::printf("mcpp:cfg=GGML_METAL_EMBED_LIBRARY\n"); + for (const fs::path & input : {common, metal, impl}) { + std::printf("mcpp:rerun-if-changed=%s\n", input.string().c_str()); + } + std::fflush(stdout); + return 0; +} catch (const std::exception & error) { + std::fprintf(stderr, "ggml-org.llamacpp build.mcpp: %s\n", error.what()); + return 1; +} +]=], + }, + sources = { + "*/ggml/src/ggml.c", + "mcpp_generated/ggml_cpp.cpp", + "*/ggml/src/ggml-alloc.c", + "*/ggml/src/ggml-backend.cpp", + "*/ggml/src/ggml-backend-meta.cpp", + "*/ggml/src/ggml-opt.cpp", + "*/ggml/src/ggml-threading.cpp", + "*/ggml/src/ggml-quants.c", + "*/ggml/src/gguf.cpp", + "*/ggml/src/ggml-backend-dl.cpp", + "*/ggml/src/ggml-backend-reg.cpp", + "*/ggml/src/ggml-cpu/ggml-cpu.c", + "mcpp_generated/ggml-cpu_cpp.cpp", + "*/ggml/src/ggml-cpu/binary-ops.cpp", + "*/ggml/src/ggml-cpu/hbm.cpp", + "*/ggml/src/ggml-cpu/ops.cpp", + "*/ggml/src/ggml-cpu/quants.c", + "*/ggml/src/ggml-cpu/repack.cpp", + "*/ggml/src/ggml-cpu/traits.cpp", + "*/ggml/src/ggml-cpu/unary-ops.cpp", + "*/ggml/src/ggml-cpu/vec.cpp", + "*/ggml/src/ggml-cpu/amx/amx.cpp", + "*/ggml/src/ggml-cpu/amx/mmq.cpp", + "*/ggml/src/ggml-cpu/llamafile/sgemm.cpp", + "mcpp_generated/llama.cppm", + "*/src/llama.cpp", + "*/src/llama-adapter.cpp", + "*/src/llama-arch.cpp", + "*/src/llama-batch.cpp", + "*/src/llama-chat.cpp", + "*/src/llama-context.cpp", + "*/src/llama-cparams.cpp", + "*/src/llama-grammar.cpp", + "*/src/llama-graph.cpp", + "*/src/llama-hparams.cpp", + "*/src/llama-impl.cpp", + "*/src/llama-io.cpp", + "*/src/llama-kv-cache.cpp", + "*/src/llama-kv-cache-iswa.cpp", + "*/src/llama-kv-cache-dsa.cpp", + "*/src/llama-kv-cache-dsv4.cpp", + "*/src/llama-memory.cpp", + "*/src/llama-memory-hybrid.cpp", + "*/src/llama-memory-hybrid-iswa.cpp", + "*/src/llama-memory-recurrent.cpp", + "*/src/llama-mmap.cpp", + "*/src/llama-model-loader.cpp", + "*/src/llama-model-saver.cpp", + "*/src/llama-model.cpp", + "*/src/llama-quant.cpp", + "*/src/llama-sampler.cpp", + "*/src/llama-vocab.cpp", + "*/src/unicode-data.cpp", + "*/src/unicode.cpp", + "*/src/models/afmoe.cpp", + "*/src/models/apertus.cpp", + "*/src/models/arcee.cpp", + "*/src/models/arctic.cpp", + "*/src/models/arwkv7.cpp", + "*/src/models/baichuan.cpp", + "*/src/models/bailingmoe.cpp", + "*/src/models/bailingmoe2.cpp", + "*/src/models/bert.cpp", + "*/src/models/bitnet.cpp", + "*/src/models/bloom.cpp", + "*/src/models/chameleon.cpp", + "*/src/models/chatglm.cpp", + "*/src/models/codeshell.cpp", + "*/src/models/cogvlm.cpp", + "*/src/models/cohere2.cpp", + "*/src/models/cohere2moe.cpp", + "*/src/models/command-r.cpp", + "*/src/models/dbrx.cpp", + "*/src/models/deci.cpp", + "*/src/models/deepseek.cpp", + "*/src/models/deepseek2.cpp", + "*/src/models/deepseek2ocr.cpp", + "*/src/models/deepseek32.cpp", + "*/src/models/deepseek4.cpp", + "*/src/models/delta-net-base.cpp", + "*/src/models/dflash.cpp", + "*/src/models/dots1.cpp", + "*/src/models/dream.cpp", + "*/src/models/eagle3.cpp", + "*/src/models/ernie4-5-moe.cpp", + "*/src/models/ernie4-5.cpp", + "*/src/models/eurobert.cpp", + "*/src/models/exaone-moe.cpp", + "*/src/models/exaone.cpp", + "*/src/models/exaone4.cpp", + "*/src/models/falcon-h1.cpp", + "*/src/models/falcon.cpp", + "*/src/models/gemma-embedding.cpp", + "*/src/models/gemma.cpp", + "*/src/models/gemma2.cpp", + "*/src/models/gemma3.cpp", + "*/src/models/gemma3n.cpp", + "*/src/models/gemma4-assistant.cpp", + "*/src/models/gemma4.cpp", + "*/src/models/glm-dsa.cpp", + "*/src/models/glm4-moe.cpp", + "*/src/models/glm4.cpp", + "*/src/models/gpt2.cpp", + "*/src/models/gptneox.cpp", + "*/src/models/granite-hybrid.cpp", + "*/src/models/granite-moe.cpp", + "*/src/models/granite.cpp", + "*/src/models/grok.cpp", + "*/src/models/grovemoe.cpp", + "*/src/models/hunyuan-dense.cpp", + "*/src/models/hunyuan-moe.cpp", + "*/src/models/hunyuan-vl.cpp", + "*/src/models/hy-v3.cpp", + "*/src/models/internlm2.cpp", + "*/src/models/jais.cpp", + "*/src/models/jais2.cpp", + "*/src/models/jamba.cpp", + "*/src/models/jina-bert-v2.cpp", + "*/src/models/jina-bert-v3.cpp", + "*/src/models/kimi-linear.cpp", + "*/src/models/lfm2.cpp", + "*/src/models/lfm2moe.cpp", + "*/src/models/llada-moe.cpp", + "*/src/models/llada.cpp", + "*/src/models/llama-embed.cpp", + "*/src/models/llama.cpp", + "*/src/models/llama4.cpp", + "*/src/models/maincoder.cpp", + "*/src/models/mamba-base.cpp", + "*/src/models/mamba.cpp", + "*/src/models/mamba2.cpp", + "*/src/models/mellum.cpp", + "*/src/models/mimo2.cpp", + "*/src/models/minicpm.cpp", + "*/src/models/minicpm3.cpp", + "*/src/models/minimax-m2.cpp", + "*/src/models/mistral3.cpp", + "*/src/models/mistral4.cpp", + "*/src/models/modern-bert.cpp", + "*/src/models/mpt.cpp", + "*/src/models/nemotron-h-moe.cpp", + "*/src/models/nemotron-h.cpp", + "*/src/models/nemotron.cpp", + "*/src/models/neo-bert.cpp", + "*/src/models/nomic-bert-moe.cpp", + "*/src/models/nomic-bert.cpp", + "*/src/models/olmo.cpp", + "*/src/models/olmo2.cpp", + "*/src/models/olmoe.cpp", + "*/src/models/openai-moe.cpp", + "*/src/models/openelm.cpp", + "*/src/models/orion.cpp", + "*/src/models/paddleocr.cpp", + "*/src/models/pangu-embed.cpp", + "*/src/models/phi2.cpp", + "*/src/models/phi3.cpp", + "*/src/models/phimoe.cpp", + "*/src/models/plamo.cpp", + "*/src/models/plamo2.cpp", + "*/src/models/plamo3.cpp", + "*/src/models/plm.cpp", + "*/src/models/qwen.cpp", + "*/src/models/qwen2.cpp", + "*/src/models/qwen2moe.cpp", + "*/src/models/qwen2vl.cpp", + "*/src/models/qwen3.cpp", + "*/src/models/qwen35.cpp", + "*/src/models/qwen35moe.cpp", + "*/src/models/qwen3moe.cpp", + "*/src/models/qwen3next.cpp", + "*/src/models/qwen3vl.cpp", + "*/src/models/qwen3vlmoe.cpp", + "*/src/models/refact.cpp", + "*/src/models/rnd1.cpp", + "*/src/models/rwkv6-base.cpp", + "*/src/models/rwkv6.cpp", + "*/src/models/rwkv6qwen2.cpp", + "*/src/models/rwkv7-base.cpp", + "*/src/models/rwkv7.cpp", + "*/src/models/seed-oss.cpp", + "*/src/models/smallthinker.cpp", + "*/src/models/smollm3.cpp", + "*/src/models/stablelm.cpp", + "*/src/models/starcoder.cpp", + "*/src/models/starcoder2.cpp", + "*/src/models/step35.cpp", + "*/src/models/t5.cpp", + "*/src/models/t5encoder.cpp", + "*/src/models/talkie.cpp", + "*/src/models/wavtokenizer-dec.cpp", + "*/src/models/xverse.cpp", + }, + targets = { + ["llama"] = { kind = "lib" }, + }, + cflags = { "-w", "-include", "ggml_build_info.h", "-DGGML_USE_CPU_REPACK" }, + cxxflags = { "-w", "-include", "ggml_build_info.h", "-DGGML_USE_CPU_REPACK" }, + flags = { + { glob = "*/ggml/src/ggml-backend-reg.cpp", defines = { "GGML_USE_CPU" } }, + { + glob = "*/ggml/src/ggml-cpu/**", + defines = { "GGML_USE_LLAMAFILE" }, + }, + { + glob = "mcpp_generated/ggml-cpu_cpp.cpp", + defines = { "GGML_USE_LLAMAFILE" }, + }, + { glob = "*/src/models/t5.cpp", cxxflags = { "-std=c++20" } }, + { glob = "*/src/models/eagle3.cpp", cxxflags = { "-std=c++20" } }, + { glob = "*/src/models/dflash.cpp", cxxflags = { "-std=c++20" } }, + { glob = "*/src/models/hunyuan-dense.cpp", cxxflags = { "-std=c++20" } }, + { glob = "*/src/models/llama-embed.cpp", cxxflags = { "-std=c++20" } }, + { glob = "*/src/models/minimax-m2.cpp", cxxflags = { "-std=c++20" } }, + }, + linux = { + sources = { + "*/ggml/src/ggml-cpu/arch/x86/quants.c", + "*/ggml/src/ggml-cpu/arch/x86/repack.cpp", + }, + cflags = { "-D_GNU_SOURCE" }, + cxxflags = { "-D_GNU_SOURCE" }, + ldflags = { "-ldl", "-lpthread", "-lm" }, + }, + macosx = { + sources = { + "*/ggml/src/ggml-cpu/arch/arm/quants.c", + "*/ggml/src/ggml-cpu/arch/arm/repack.cpp", + }, + cflags = { "-D_DARWIN_C_SOURCE" }, + cxxflags = { "-D_DARWIN_C_SOURCE" }, + ldflags = { + "-lpthread", "-lm", + "-framework", "Foundation", + "-framework", "Metal", + "-framework", "MetalKit", + }, + }, + windows = { + sources = { + "*/ggml/src/ggml-cpu/arch/x86/quants.c", + "*/ggml/src/ggml-cpu/arch/x86/repack.cpp", + }, + cflags = { "-D_CRT_SECURE_NO_WARNINGS", "-DWIN32_LEAN_AND_MEAN" }, + cxxflags = { "-D_CRT_SECURE_NO_WARNINGS", "-DWIN32_LEAN_AND_MEAN" }, + ldflags = { "-ladvapi32" }, + }, + features = { + default = { implies = { "backend-cpu" } }, + ["backend-cpu"] = {}, + ["backend-metal"] = { + sources = { + "*/ggml/src/ggml-metal/ggml-metal.cpp", + "mcpp_generated/ggml_metal_device_m.m", + "*/ggml/src/ggml-metal/ggml-metal-device.cpp", + "*/ggml/src/ggml-metal/ggml-metal-common.cpp", + "*/ggml/src/ggml-metal/ggml-metal-context.m", + "*/ggml/src/ggml-metal/ggml-metal-ops.cpp", + }, + flags = { + { glob = "mcpp_generated/ggml_metal_device_m.m", cflags = { "-fno-objc-arc" } }, + { glob = "*/ggml/src/ggml-metal/ggml-metal-context.m", cflags = { "-fno-objc-arc" } }, + { glob = "*/ggml/src/ggml-metal/ggml-metal.cpp", cxxflags = { "-include", "memory" } }, + { + glob = "*/ggml/src/ggml-backend-reg.cpp", + defines = { "GGML_USE_METAL" }, + }, + }, + }, + }, + }, +} diff --git a/tests/check_llamacpp_snapshot.py b/tests/check_llamacpp_snapshot.py new file mode 100644 index 0000000..2535d0d --- /dev/null +++ b/tests/check_llamacpp_snapshot.py @@ -0,0 +1,700 @@ +#!/usr/bin/env python3 +"""Hard gate for the ggml-org.llamacpp llama.cpp b10069 descriptor.""" +from __future__ import annotations + +import json +import os +import re +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +REPORT_PATH = ROOT / "tools/llamacpp/snapshots/b10069.json" +DESCRIPTORS = { + "ggml-org.llamacpp": ROOT / "pkgs/g/ggml-org.llamacpp.lua", +} + +LUA_DUMP = r''' +local function hex(value) + return (value:gsub('.', function(c) return string.format('%02x', string.byte(c)) end)) +end +local function key(value) + if type(value) == 'number' then return 'n' .. tostring(value) end + return 's' .. hex(tostring(value)) +end +local function path_string(path) + local parts = {} + for i, value in ipairs(path) do parts[i] = key(value) end + return table.concat(parts, ',') +end +local function sorted_keys(value) + local keys = {} + for k, _ in pairs(value) do table.insert(keys, k) end + table.sort(keys, function(a, b) + if type(a) ~= type(b) then return type(a) < type(b) end + if type(a) == 'number' then return a < b end + return tostring(a) < tostring(b) + end) + return keys +end +local function walk(value, path) + if type(value) == 'table' then + print('T\t' .. path_string(path)) + for _, k in ipairs(sorted_keys(value)) do + table.insert(path, k) + walk(value[k], path) + table.remove(path) + end + return + end + print('V\t' .. path_string(path) .. '\t' .. type(value) .. '\t' .. hex(tostring(value))) +end +local ok, err = pcall(dofile, arg[1]) +if not ok then io.stderr:write(err .. '\n'); os.exit(2) end +if type(package) ~= 'table' then io.stderr:write('descriptor did not define package table\n'); os.exit(2) end +walk(package, {}) +''' + + +class CheckError(RuntimeError): + pass + + +def require(condition: bool, message: str) -> None: + if not condition: + raise CheckError(message) + + +def find_lua() -> str: + override = os.environ.get("LUA") + if override: + require(not any(ch.isspace() for ch in override), + "LUA must name one interpreter executable") + resolved = shutil.which(override) + require(resolved is not None, f"LUA interpreter not found: {override}") + return resolved + for candidate in ("lua5.4", "lua"): + resolved = shutil.which(candidate) + if resolved: + return resolved + raise CheckError("Lua interpreter not found (tried lua5.4, then lua; set LUA to override)") + + +def _decode_path(value: str) -> list[str | int]: + if not value: + return [] + result: list[str | int] = [] + for item in value.split(","): + if item.startswith("n"): + result.append(int(item[1:])) + elif item.startswith("s"): + result.append(bytes.fromhex(item[1:]).decode()) + else: + raise CheckError(f"invalid Lua path token: {item}") + return result + + +def descriptor_identity(descriptor: dict) -> str: + namespace = descriptor.get("namespace", "") + name = descriptor.get("name", "") + require(isinstance(namespace, str) and isinstance(name, str), + "descriptor namespace and name must be strings") + if namespace and not name.startswith(f"{namespace}."): + return f"{namespace}.{name}" + return name + + +def load_descriptor(lua: str, path: Path) -> dict: + result = subprocess.run( + [lua, "-", str(path)], input=LUA_DUMP, text=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + ) + require(result.returncode == 0, + f"cannot load {path.relative_to(ROOT)} with {lua}: {result.stderr.strip()}") + root: dict = {} + for raw in result.stdout.splitlines(): + columns = raw.split("\t") + require(columns[0] in {"T", "V"}, f"invalid Lua row: {raw!r}") + path_parts = _decode_path(columns[1]) + if not path_parts: + continue + current = root + for key in path_parts[:-1]: + current = current.setdefault(key, {}) + key = path_parts[-1] + if columns[0] == "T": + current.setdefault(key, {}) + continue + require(len(columns) == 4, f"invalid Lua scalar row: {raw!r}") + scalar_type = columns[2] + value = bytes.fromhex(columns[3]).decode() + if scalar_type == "boolean": + scalar: object = value == "true" + elif scalar_type == "number": + scalar = float(value) if "." in value else int(value) + else: + scalar = value + current[key] = scalar + require(descriptor_identity(root) in DESCRIPTORS, + f"unexpected descriptor identity in {path.relative_to(ROOT)}") + return root + + +def array(value: object, context: str) -> list: + require(isinstance(value, dict), f"{context} must be a Lua array") + keys = sorted(value) + require(keys == list(range(1, len(keys) + 1)), f"{context} is not a dense Lua array") + return [value[key] for key in keys] + + +def get_path(value: dict, *path: str) -> object: + current: object = value + for key in path: + require(isinstance(current, dict) and key in current, + f"missing descriptor field: {'.'.join(path)}") + current = current[key] + return current + + +def iter_tables(value: object, path: tuple = ()): + if not isinstance(value, dict): + return + yield path, value + for key, child in value.items(): + if isinstance(child, dict): + yield from iter_tables(child, path + (key,)) + + +def normalized_source(source: str, package_name: str) -> str | None: + if source == "mcpp_generated/ggml_cpp.cpp": + return "ggml/src/ggml.cpp" + if source == "mcpp_generated/ggml-cpu_cpp.cpp": + return "ggml/src/ggml-cpu/ggml-cpu.cpp" + if source == "mcpp_generated/ggml_metal_device_m.m": + return "ggml/src/ggml-metal/ggml-metal-device.m" + if source.startswith("mcpp_generated/") or "*" not in source: + return None + normalized = source.replace("*/", "", 1) + require("*" not in normalized, f"wildcard source is not auditable: {package_name}: {source}") + return normalized + + +def descriptor_sources(descriptor: dict) -> list[str]: + sources: list[str] = [] + mcpp = get_path(descriptor, "mcpp") + for path, table in iter_tables(mcpp): + if path and path[-1] == "sources": + sources.extend(array(table, f"{descriptor['name']}.mcpp.{'.'.join(map(str, path))}")) + return sources + + +def _default_feature_closure(table: dict, context: str) -> set[str]: + features = table.get("features", {}) + require(isinstance(features, dict), f"{context}.features must be a table") + if "default" not in features: + return set() + + default = features["default"] + require(isinstance(default, dict), + f"{context}.features.default must be a table") + enabled: set[str] = set() + pending = array( + default.get("implies", {}), + f"{context}.features.default.implies") + require(all(isinstance(name, str) for name in pending), + f"{context}.features.default.implies must contain feature names") + while pending: + feature_name = pending.pop() + if feature_name == "default": + continue + if feature_name in enabled: + continue + require(feature_name in features, + f"{context} implies unknown feature: {feature_name}") + feature = features[feature_name] + require(isinstance(feature, dict), + f"{context}.features.{feature_name} must be a table") + enabled.add(feature_name) + + implied_features = array( + feature.get("implies", {}), + f"{context}.features.{feature_name}.implies") + require(all(isinstance(name, str) for name in implied_features), + f"{context}.features.{feature_name}.implies must contain feature names") + pending.extend(implied_features) + return enabled + + +def descriptor_sources_for_platform(descriptor: dict, platform: str) -> list[str]: + mcpp = get_path(descriptor, "mcpp") + context = f"{descriptor['name']}.mcpp" + platform_table = mcpp.get(platform, {}) + require(isinstance(platform_table, dict), + f"{context}.{platform} must be a table") + + result: list[str] = [] + merged_fields: dict[str, dict[str, list]] = {} + for table, scope_context in ( + (mcpp, context), + (platform_table, f"{context}.{platform}")): + if "sources" in table: + result.extend(array(table["sources"], f"{scope_context}.sources")) + features = table.get("features", {}) + require(isinstance(features, dict), + f"{scope_context}.features must be a table") + for feature_name, feature in features.items(): + require(isinstance(feature_name, str), + f"{scope_context}.features keys must be feature names") + require(isinstance(feature, dict), + f"{scope_context}.features.{feature_name} must be a table") + merged = merged_fields.setdefault(feature_name, {}) + for field in ("implies", "sources"): + if field in feature: + merged.setdefault(field, []).extend(array( + feature[field], + f"{scope_context}.features.{feature_name}.{field}")) + + effective_features = { + feature_name: { + field: dict(enumerate(values, start=1)) + for field, values in fields.items() + } + for feature_name, fields in merged_fields.items() + } + effective = {"features": effective_features} + for feature_name in _default_feature_closure(effective, context): + feature = effective_features[feature_name] + if "sources" in feature: + result.extend(array( + feature["sources"], + f"{context}.features.{feature_name}.sources")) + return result + + +def check_cohort(descriptors: dict[str, dict], report: dict) -> None: + require(set(descriptors) == {"ggml-org.llamacpp"}, + f"descriptor cohort drift: {sorted(descriptors)}") + descriptor = descriptors["ggml-org.llamacpp"] + identity = report["upstream"] + require(descriptor.get("namespace") == "ggml-org", "descriptor namespace drift") + require(descriptor.get("name") == "llamacpp", "descriptor name drift") + xpm = get_path(descriptor, "xpm") + require(set(xpm) == {"linux", "macosx", "windows"}, + f"ggml-org.llamacpp platform cohort drift: {sorted(xpm)}") + for platform, versions in xpm.items(): + require(set(versions) == {identity["tag"]}, + f"ggml-org.llamacpp/{platform} must expose only {identity['tag']}") + release = versions[identity["tag"]] + require(release.get("sha256") == identity["sha256"], + f"ggml-org.llamacpp/{platform}/{identity['tag']} SHA drift") + require(release.get("url") == identity["url"], + f"ggml-org.llamacpp/{platform}/{identity['tag']} URL drift") + + +def check_dependencies(descriptors: dict[str, dict], tag: str) -> None: + descriptor = descriptors["ggml-org.llamacpp"] + mcpp = get_path(descriptor, "mcpp") + require(mcpp.get("deps", {}) == {}, + "ggml-org.llamacpp must not depend on component packages") + require(mcpp.get("provides", {}) in ({}, []), + "ggml-org.llamacpp must not publish provider capabilities") + for scope in ("linux", "macosx", "windows"): + surface = mcpp.get(scope, {}) + require(isinstance(surface, dict), f"ggml-org.llamacpp {scope} scope must be a table") + require(surface.get("deps", {}) == {}, + f"ggml-org.llamacpp/{scope} must not depend on component packages") + feature = get_path(mcpp, "features", "backend-metal") + require(feature.get("deps", {}) == {}, + "backend-metal must be an in-package feature") + require(feature.get("requires", {}) in ({}, []), + "backend-metal must not require a provider capability") + + +def _report_tus(report: dict) -> set[str]: + result = set() + for group, sources in report["sources"].items(): + for source in sources: + if Path(source).suffix not in {".c", ".cc", ".cpp", ".m", ".mm"}: + continue + result.add("src/" + source if group == "models" else source) + return result + + +def _group_tus(report: dict, groups: tuple[str, ...]) -> set[str]: + return { + ("src/" + source if group == "models" else source) + for group in groups + for source in report["sources"][group] + if Path(source).suffix in {".c", ".cc", ".cpp", ".m", ".mm"} + } + + +def check_sources(descriptors: dict[str, dict], report: dict) -> None: + descriptor = descriptors["ggml-org.llamacpp"] + report_tus = _report_tus(report) + default_groups = ("ggml_base", "ggml_registry", "ggml_cpu_common", + "llama_core", "models") + arch_groups = { + "linux": ("ggml_cpu_x86",), + "macosx": ("ggml_cpu_arm",), + "windows": ("ggml_cpu_x86",), + } + for platform in ("linux", "macosx", "windows"): + actual: set[str] = set() + for source in descriptor_sources_for_platform(descriptor, platform): + normalized = normalized_source(source, "ggml-org.llamacpp") + if normalized is None: + continue + require(normalized in report_tus, + f"ggml-org.llamacpp source is absent from snapshot report: {normalized}") + require(normalized not in actual, + f"duplicate upstream TU ownership on {platform}: {normalized}") + actual.add(normalized) + expected = _group_tus(report, default_groups + arch_groups[platform]) + require(actual == expected, + f"default TU set drift on {platform}: missing={sorted(expected - actual)}, " + f"extra={sorted(actual - expected)}") + + core_sources = descriptor_sources(descriptor) + require("*/src/models/*.cpp" not in core_sources, + "ggml-org.llamacpp must not use a model wildcard") + actual_models = sorted( + source.removeprefix("*/src/") for source in core_sources + if source.startswith("*/src/models/") + ) + require(actual_models == report["sources"]["models"], + f"ggml-org.llamacpp model TU drift: expected {len(report['sources']['models'])}, " + f"got {len(actual_models)}") + + metal_source_list = [ + normalized_source(source, "ggml-org.llamacpp") + for source in array( + get_path(descriptor, "mcpp", "features", "backend-metal", "sources"), + "ggml-org.llamacpp.features.backend-metal.sources", + ) + ] + metal_sources = set(metal_source_list) + metal_sources.discard(None) + require(len(metal_sources) == len(metal_source_list), + "duplicate Metal TU ownership") + expected_metal = _group_tus(report, ("ggml_metal",)) + require(metal_sources == expected_metal, + f"Metal TU set drift: missing={sorted(expected_metal - metal_sources)}, " + f"extra={sorted(metal_sources - expected_metal)}") + + +def all_define_sites(descriptors: dict[str, dict]) -> dict[str, list[tuple[str, tuple, dict]]]: + sites: dict[str, list[tuple[str, tuple, dict]]] = {} + for name, descriptor in descriptors.items(): + for path, table in iter_tables(get_path(descriptor, "mcpp")): + if "defines" not in table: + continue + for define in array(table["defines"], f"{name}.{path}.defines"): + sites.setdefault(define, []).append((name, path, table)) + return sites + + +def all_macro_sites(descriptors: dict[str, dict]) -> dict[str, list[tuple[str, tuple, str, dict]]]: + tracked = { + "GGML_USE_CPU", "GGML_USE_METAL", + "GGML_USE_CPU_REPACK", "GGML_USE_LLAMAFILE", + } + sites: dict[str, list[tuple[str, tuple, str, dict]]] = {} + for name, descriptor in descriptors.items(): + for path, table in iter_tables(get_path(descriptor, "mcpp")): + for field in ("cflags", "cxxflags", "defines", + "interface_defines", "public_defines"): + if field not in table: + continue + values = array(table[field], f"{name}.{path}.{field}") + index = 0 + while index < len(values): + value = values[index] + require(isinstance(value, str), f"{name}.{path}.{field} must contain strings") + macro = value + if field in {"cflags", "cxxflags"}: + if value in {"-D", "/D"}: + index += 1 + if index >= len(values): + break + macro = values[index] + require(isinstance(macro, str), + f"{name}.{path}.{field} split define must be a string") + elif value.startswith("-D") or value.startswith("/D"): + macro = value[2:].strip() + else: + index += 1 + continue + macro = macro.split("=", 1)[0] + if macro in tracked: + sites.setdefault(macro, []).append((name, path, field, table)) + index += 1 + return sites + + +def check_registry_and_features(descriptors: dict[str, dict], report: dict) -> None: + descriptor = descriptors["ggml-org.llamacpp"] + registry = set(report["sources"]["ggml_registry"]) + owned = {normalized_source(source, "ggml-org.llamacpp") + for source in descriptor_sources(descriptor)} + require(registry <= owned, "ggml-org.llamacpp must own all registry TUs") + + sites = all_define_sites(descriptors) + for macro in ("GGML_USE_CPU", "GGML_USE_METAL"): + macro_sites = sites.get(macro, []) + require(len(macro_sites) == 1, f"{macro} must have one registry-only owner") + name, _path, table = macro_sites[0] + require(name == "ggml-org.llamacpp", f"{macro} must be owned by ggml-org.llamacpp") + require(table.get("glob") == "*/ggml/src/ggml-backend-reg.cpp", + f"{macro} must be scoped to ggml-backend-reg.cpp") + + metal_ldflags = array(descriptor["mcpp"]["macosx"].get("ldflags"), + "ggml-org.llamacpp.macosx.ldflags") + expected_ldflags = [item for framework in report["metal"]["frameworks"] + for item in ("-framework", framework)] + require(metal_ldflags == ["-lpthread", "-lm"] + expected_ldflags, + "Metal framework linkage drift") + + feature_sites = [] + mcpp = descriptor["mcpp"] + features = get_path(mcpp, "features") + require(set(features) == {"default", "backend-cpu", "backend-metal"}, + "ggml-org.llamacpp must declare exactly CPU and Metal backend features") + default = get_path(features, "default") + require(array(default.get("implies"), "features.default.implies") == ["backend-cpu"], + "ggml-org.llamacpp default feature must select backend-cpu") + require(get_path(features, "backend-cpu") == {}, + "backend-cpu must remain an explicit no-op feature") + if "backend-metal" in mcpp.get("features", {}): + feature_sites.append(("ggml-org.llamacpp", "common")) + for platform in ("linux", "macosx", "windows"): + if "backend-metal" in mcpp.get(platform, {}).get("features", {}): + feature_sites.append(("ggml-org.llamacpp", platform)) + require(feature_sites == [("ggml-org.llamacpp", "common")], + f"backend-metal feature placement drift: {feature_sites}") + + feature = get_path(mcpp, "features", "backend-metal") + flags = array(feature.get("flags"), "backend-metal.flags") + metal_flags = [flag for flag in flags if flag.get("defines")] + require(len(metal_flags) == 1 + and metal_flags[0].get("glob") == "*/ggml/src/ggml-backend-reg.cpp" + and array(metal_flags[0].get("defines"), "backend-metal.flags.defines") + == ["GGML_USE_METAL"], + "backend-metal registry flag drift") + + +def check_cpu_private_macros(descriptors: dict[str, dict]) -> None: + cpu = descriptors["ggml-org.llamacpp"]["mcpp"] + for field in ("cflags", "cxxflags"): + require("-DGGML_USE_CPU_REPACK" in array(cpu[field], f"ggml-org.llamacpp.{field}"), + "CPU repack macro must remain a private compile flag") + llamafile_sites = all_macro_sites(descriptors).get("GGML_USE_LLAMAFILE", []) + require(len(llamafile_sites) == 2 and all( + name == "ggml-org.llamacpp" and path[:1] == ("flags",) and field == "defines" + for name, path, field, _table in llamafile_sites + ), + "llamafile macro must remain CPU-source-scoped") + require({table.get("glob") for _name, _path, _field, table in llamafile_sites} == { + "*/ggml/src/ggml-cpu/**", "mcpp_generated/ggml-cpu_cpp.cpp", + }, "GGML_USE_LLAMAFILE private glob drift") + + sites = all_macro_sites(descriptors) + cpu_registry = sites.get("GGML_USE_CPU", []) + require(len(cpu_registry) == 1, "GGML_USE_CPU must have one private registry site") + name, path, field, table = cpu_registry[0] + require(name == "ggml-org.llamacpp" and path[:1] == ("flags",) + and field == "defines" + and table.get("glob") == "*/ggml/src/ggml-backend-reg.cpp", + "GGML_USE_CPU must remain on the core common registry flag") + + metal_registry = sites.get("GGML_USE_METAL", []) + require(len(metal_registry) == 1, "GGML_USE_METAL must have one private registry site") + name, path, field, table = metal_registry[0] + require(name == "ggml-org.llamacpp" + and path[:3] == ("features", "backend-metal", "flags") + and field == "defines" + and table.get("glob") == "*/ggml/src/ggml-backend-reg.cpp", + "GGML_USE_METAL must remain on the backend-metal registry flag") + + +def check_report_metadata_contracts(descriptors: dict[str, dict], report: dict) -> None: + build_info = get_path( + descriptors["ggml-org.llamacpp"], "mcpp", "generated_files", + "mcpp_generated/ggml_build_info.h", + ) + require(f'#define GGML_VERSION "{report["upstream"]["tag"]}"' in build_info, + "generated GGML version drift") + require(f'#define GGML_COMMIT "{report["upstream"]["commit"]}"' in build_info, + "generated GGML commit drift") + + windows_ldflags = array( + get_path(descriptors["ggml-org.llamacpp"], "mcpp", "windows", "ldflags"), + "ggml-org.llamacpp.windows.ldflags", + ) + expected_windows = [f"-l{library}" for library in report["platform_links"]["windows_cpu"]] + require(windows_ldflags == expected_windows, "Windows CPU linkage drift") + + cpp20_sources = [] + for flag in array(descriptors["ggml-org.llamacpp"]["mcpp"]["flags"], + "ggml-org.llamacpp.flags"): + if flag.get("cxxflags") and "-std=c++20" in array( + flag["cxxflags"], "ggml-org.llamacpp.flags.cxxflags"): + source = normalized_source(flag.get("glob", ""), "ggml-org.llamacpp") + require(source is not None, "C++20 exception must target one upstream TU") + cpp20_sources.append(source) + require(sorted(cpp20_sources) == report["dialect_exceptions"]["c++20"], + "C++20 dialect exception drift") + + +def check_metal_build_contract(descriptors: dict[str, dict], report: dict) -> None: + generated = get_path(descriptors["ggml-org.llamacpp"], "mcpp", "generated_files") + source = generated.get("build.mcpp") + require(isinstance(source, str) and source, "ggml-org.llamacpp build.mcpp is missing") + for tool in ("/usr/bin/xcrun", "/usr/bin/metal", "/usr/bin/metallib"): + require(tool not in source, + f"forbidden absolute tool: {tool} (absolute Xcode Metal tool path)") + process_launch = re.search( + r"\b(?:(?:[A-Za-z_]\w*)::)*(?:system|popen|exec\w*|posix_spawn\w*)\s*\(", + source, + ) + require(process_launch is None, + "ggml-org.llamacpp build.mcpp contains a forbidden process launch API") + compiler = shutil.which(os.environ.get("CXX", "c++")) + require(compiler is not None, "host C++ compiler not found for build.mcpp contract") + + with tempfile.TemporaryDirectory(prefix="llamacpp-build-contract-") as td: + root = Path(td) + program = root / "build.cpp" + executable = root / "build" + program.write_text(source) + compiled = subprocess.run( + [compiler, "-std=c++17", str(program), "-o", str(executable)], + text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + ) + require(compiled.returncode == 0, + f"cannot compile ggml-org.llamacpp build.mcpp: {compiled.stderr}") + + def run(target_os: str, target_arch: str, manifest: Path, output: Path, + features: tuple[str, ...]): + env = os.environ.copy() + for name in tuple(env): + if name == "MCPP_FEATURES" or name.startswith("MCPP_FEATURE_"): + env.pop(name) + env.update({ + "MCPP_TARGET_OS": target_os, + "MCPP_TARGET_ARCH": target_arch, + "MCPP_MANIFEST_DIR": str(manifest), + "MCPP_OUT_DIR": str(output), + "MCPP_FEATURES": ",".join(features), + }) + for feature in features: + feature_env = re.sub(r"[^A-Za-z0-9]", "_", feature.upper()) + env[f"MCPP_FEATURE_{feature_env}"] = "1" + return subprocess.run( + [str(executable)], env=env, text=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + ) + + missing = root / "manifest" + missing.mkdir() + output = root / "out" + disabled = run("linux", "x86_64", missing, output, ()) + require(disabled.returncode == 0 and not (output / "ggml-metal-embed.s").exists(), + "build.mcpp must no-op when backend-metal is disabled") + cpu = run("linux", "x86_64", missing, output, ("backend-cpu",)) + require(cpu.returncode == 0 and not (output / "ggml-metal-embed.s").exists(), + "build.mcpp must accept explicit backend-cpu") + for feature_name in ("backend-cuda", "backend-xxx", "undeclared"): + invalid = run("linux", "x86_64", missing, output, (feature_name,)) + require(invalid.returncode != 0 + and f"unsupported feature '{feature_name}'" in invalid.stderr, + f"build.mcpp must reject undeclared feature {feature_name}") + metal_features = ("backend-cpu", "backend-metal") + linux = run("linux", "x86_64", missing, output, metal_features) + require(linux.returncode != 0 and "requires target_os=macos" in linux.stderr, + "build.mcpp must reject non-macOS targets") + mac_x86 = run("macos", "x86_64", missing, output, metal_features) + require(mac_x86.returncode != 0 and "requires target_arch=aarch64" in mac_x86.stderr, + "build.mcpp must reject non-aarch64 macOS targets") + + upstream = missing / "llama.cpp-b10069" + metal_dir = upstream / "ggml/src/ggml-metal" + metal_dir.mkdir(parents=True) + common = upstream / "ggml/src/ggml-common.h" + metal = metal_dir / "ggml-metal.metal" + impl = metal_dir / "ggml-metal-impl.h" + common.write_text("COMMON_PAYLOAD\n") + impl.write_text("IMPL_PAYLOAD\n") + metal.write_text("A __embed_ggml-common.h__ B #include \"ggml-metal-impl.h\" C\n") + success = run("macos", "aarch64", missing, output, metal_features) + require(success.returncode == 0, + f"build.mcpp macOS/aarch64 contract failed: {success.stderr}") + assembly = output / "ggml-metal-embed.s" + expected_lines = { + f"mcpp:generated={assembly}", + "mcpp:cfg=GGML_METAL_EMBED_LIBRARY", + } + for relative in report["metal"]["shader_inputs"]: + expected_lines.add(f"mcpp:rerun-if-changed={upstream / relative}") + actual_lines = success.stdout.splitlines() + require(len(actual_lines) == len(expected_lines) + and set(actual_lines) == expected_lines, + f"build.mcpp stdout contract drift: {success.stdout!r}") + merged_text = (output / "ggml-metal-embed.metal").read_text() + require(merged_text.count("COMMON_PAYLOAD") == 1 + and merged_text.count("IMPL_PAYLOAD") == 1, + "build.mcpp must embed each shader input exactly once") + assembly_text = assembly.read_text() + for marker in ("_ggml_metallib_start", ".incbin", "_ggml_metallib_end"): + require(marker in assembly_text, f"build.mcpp assembly missing {marker}") + + +def check_module_contract(descriptors: dict[str, dict]) -> None: + mcpp = get_path(descriptors["ggml-org.llamacpp"], "mcpp") + require(mcpp.get("targets") == {"llama": {"kind": "lib"}}, + "ggml-org.llamacpp must expose exactly one llama target") + require(array(mcpp.get("modules"), "ggml-org.llamacpp.modules") == ["llama"], + "ggml-org.llamacpp must expose exactly the llama module") + generated = get_path(mcpp, "generated_files") + for generated_name, source_name in ( + ("mcpp_generated/llama.cppm", "llama.cppm"), + ("mcpp_generated/gen_exports/required_ggml.inc", "gen_exports/required_ggml.inc"), + ("mcpp_generated/gen_exports/llama.inc", "gen_exports/llama.inc"), + ): + expected = (ROOT / "tools/llamacpp/module" / source_name).read_text() + require(generated.get(generated_name) == expected, + f"generated module input drift: {generated_name}") + module = generated["mcpp_generated/llama.cppm"] + require("export module llama;" in module + and "#include " in module + and "#include \"gen_exports/llama.inc\"" in module, + "llama module wrapper contract drift") + + +def main() -> int: + try: + report = json.loads(REPORT_PATH.read_text()) + lua = find_lua() + descriptors = {name: load_descriptor(lua, path) + for name, path in DESCRIPTORS.items()} + check_cohort(descriptors, report) + check_dependencies(descriptors, report["upstream"]["tag"]) + check_sources(descriptors, report) + check_registry_and_features(descriptors, report) + check_cpu_private_macros(descriptors) + check_report_metadata_contracts(descriptors, report) + check_metal_build_contract(descriptors, report) + check_module_contract(descriptors) + except (CheckError, OSError, json.JSONDecodeError) as error: + print(f"llama.cpp snapshot check failed: {error}", file=sys.stderr) + return 1 + print("ggml-org.llamacpp llama.cpp descriptor matches b10069 report") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/examples/llamacpp-internal-cpu/mcpp.toml b/tests/examples/llamacpp-internal-cpu/mcpp.toml new file mode 100644 index 0000000..32e3a3f --- /dev/null +++ b/tests/examples/llamacpp-internal-cpu/mcpp.toml @@ -0,0 +1,9 @@ +[package] +name = "llamacpp-internal-cpu" +version = "0.1.0" + +[indices] +ggml-org = { path = "../../.." } + +[dependencies.ggml-org] +llamacpp = "b10069" diff --git a/tests/examples/llamacpp-internal-cpu/tests/decode.cpp b/tests/examples/llamacpp-internal-cpu/tests/decode.cpp new file mode 100644 index 0000000..b15b70c --- /dev/null +++ b/tests/examples/llamacpp-internal-cpu/tests/decode.cpp @@ -0,0 +1,101 @@ +/// Internal CPU smoke test: loads pinned GGUF, decodes one batch, and samples. +/// Requires LLAMACPP_TEST_MODEL env var pointing to a valid GGUF file. + +import std; + +import llama; + +#ifdef LLAMA_H +#error "import llama leaked LLAMA_H" +#endif + +#ifdef LLAMA_API +#error "import llama leaked LLAMA_API" +#endif + + +int main() { + static_assert(LLAMA_DEFAULT_SEED == 0xFFFFFFFFu); + static_assert(LLAMA_TOKEN_NULL == llama_token{-1}); + + const char *model_path = std::getenv("LLAMACPP_TEST_MODEL"); + if (!model_path || !*model_path) { + std::cerr << "LLAMACPP_TEST_MODEL not set or empty\n"; + return 1; + } + + // Verify model file exists and is readable + { + std::ifstream f(model_path, std::ios::binary | std::ios::ate); + if (!f) { + std::cerr << "cannot open model file: " << model_path << "\n"; + return 2; + } + auto sz = f.tellg(); + std::cerr << "model size: " << sz << " bytes\n"; + } + + llama_backend_init(); + + // Model params: CPU only. + llama_model_params mparams = llama_model_default_params(); + mparams.n_gpu_layers = 0; + + llama_model *model = llama_model_load_from_file(model_path, mparams); + if (!model) { + std::cerr << "failed to load model\n"; + llama_backend_free(); + return 3; + } + std::cout << "model loaded: " << llama_model_n_params(model) << " params\n"; + + // Context params + llama_context_params cparams = llama_context_default_params(); + cparams.n_ctx = 64; + + llama_context *ctx = llama_init_from_model(model, cparams); + if (!ctx) { + std::cerr << "failed to create context\n"; + llama_model_free(model); + llama_backend_free(); + return 4; + } + + // Decode a tiny batch + llama_token tokens[] = {1, 2, 3}; + int n_tokens = sizeof(tokens) / sizeof(tokens[0]); + int ret = llama_decode(ctx, llama_batch_get_one(tokens, n_tokens)); + if (ret != 0) { + std::cerr << "decode returned " << ret << "\n"; + llama_free(ctx); + llama_model_free(model); + llama_backend_free(); + return 5; + } + std::cout << "decode OK\n"; + + // Sample one token + llama_sampler *smpl = llama_sampler_chain_init( + llama_sampler_chain_default_params()); + llama_sampler_chain_add(smpl, llama_sampler_init_greedy()); + llama_token sampled = llama_sampler_sample(smpl, ctx, -1); + std::cout << "sampled token: " << sampled << "\n"; + + // Validate token range + if (sampled < 0 || sampled >= llama_vocab_n_tokens(llama_model_get_vocab(model))) { + std::cerr << "sampled token " << sampled << " out of vocab range [0, " << llama_vocab_n_tokens(llama_model_get_vocab(model)) << ")\n"; + llama_sampler_free(smpl); + llama_free(ctx); + llama_model_free(model); + llama_backend_free(); + return 6; + } + + llama_sampler_free(smpl); + llama_free(ctx); + llama_model_free(model); + llama_backend_free(); + + std::cout << "CPU smoke test PASSED\n"; + return 0; +} diff --git a/tests/examples/llamacpp-internal-metal/mcpp.toml b/tests/examples/llamacpp-internal-metal/mcpp.toml new file mode 100644 index 0000000..e769deb --- /dev/null +++ b/tests/examples/llamacpp-internal-metal/mcpp.toml @@ -0,0 +1,12 @@ +[package] +name = "llamacpp-internal-metal" +version = "0.1.0" + +[indices] +ggml-org = { path = "../../.." } + +[target.'cfg(all(macos, arch = "aarch64"))'.dependencies.ggml-org] +llamacpp = { version = "b10069", features = ["backend-metal"] } + +[target.'cfg(all(macos, arch = "aarch64"))'.build] +cxxflags = ["-DLLAMACPP_METAL_TEST=1"] diff --git a/tests/examples/llamacpp-internal-metal/tests/decode.cpp b/tests/examples/llamacpp-internal-metal/tests/decode.cpp new file mode 100644 index 0000000..f9b19b2 --- /dev/null +++ b/tests/examples/llamacpp-internal-metal/tests/decode.cpp @@ -0,0 +1,198 @@ +#if defined(__APPLE__) \ + && (defined(__aarch64__) || defined(__arm64__)) \ + && !defined(LLAMACPP_METAL_TEST) +#error "LLAMACPP_METAL_TEST must be enabled on macOS ARM64" +#endif + +#ifdef LLAMACPP_METAL_TEST + +import std; + +import llama; + +#ifdef LLAMA_H +#error "import llama leaked LLAMA_H" +#endif + +#ifdef LLAMA_API +#error "import llama leaked LLAMA_API" +#endif + + +namespace { + +std::string logs; + +void capture_log(enum ggml_log_level, const char * text, void *) { + if (text) { + logs += text; + std::cerr << text; + } +} + +int fail(const char * message) { + std::cerr << "Metal smoke test failed: " << message << "\n"; + return 1; +} + +bool has_embedded_library(ggml_backend_reg_t reg) { + auto get_features = reinterpret_cast( + ggml_backend_reg_get_proc_address(reg, "ggml_backend_get_features")); + if (!get_features) { + return false; + } + for (auto * feature = get_features(reg); feature && feature->name; ++feature) { + if (std::strcmp(feature->name, "EMBED_LIBRARY") == 0 + && std::strcmp(feature->value, "1") == 0) { + return true; + } + } + return false; +} + +bool run_metal_add_probe(ggml_backend_dev_t device) { + ggml_backend_t backend = ggml_backend_dev_init(device, nullptr); + if (!backend) { + return false; + } + + ggml_init_params params = {}; + params.mem_size = 1024 * 1024; + params.no_alloc = true; + ggml_context * context = ggml_init(params); + if (!context) { + ggml_backend_free(backend); + return false; + } + + ggml_cgraph * graph = ggml_new_graph(context); + ggml_tensor * lhs = ggml_new_tensor_1d(context, GGML_TYPE_F32, 4); + ggml_tensor * rhs = ggml_new_tensor_1d(context, GGML_TYPE_F32, 4); + ggml_tensor * sum = ggml_add(context, lhs, rhs); + ggml_backend_buffer_t buffer = ggml_backend_alloc_ctx_tensors(context, backend); + if (!graph || !lhs || !rhs || !sum || !buffer) { + if (buffer) { + ggml_backend_buffer_free(buffer); + } + ggml_free(context); + ggml_backend_free(backend); + return false; + } + ggml_build_forward_expand(graph, sum); + + const float lhs_values[] = {1.0F, -2.0F, 3.5F, 10.0F}; + const float rhs_values[] = {4.0F, 5.0F, -1.5F, -3.0F}; + ggml_backend_tensor_set(lhs, lhs_values, 0, sizeof(lhs_values)); + ggml_backend_tensor_set(rhs, rhs_values, 0, sizeof(rhs_values)); + + bool passed = ggml_backend_graph_compute(backend, graph) == GGML_STATUS_SUCCESS; + float actual[4] = {}; + if (passed) { + ggml_backend_synchronize(backend); + ggml_backend_tensor_get(sum, actual, 0, sizeof(actual)); + const float expected[] = {5.0F, 3.0F, 2.0F, 7.0F}; + for (std::size_t i = 0; i < 4; ++i) { + if (actual[i] != expected[i]) { + passed = false; + break; + } + } + } + + ggml_backend_buffer_free(buffer); + ggml_free(context); + ggml_backend_free(backend); + return passed; +} + +} // namespace + +int main() { + const char * model_path = std::getenv("LLAMACPP_TEST_MODEL"); + if (!model_path || !*model_path) { + return fail("LLAMACPP_TEST_MODEL is not set"); + } + + llama_log_set(capture_log, nullptr); + llama_backend_init(); + + ggml_backend_reg_t metal = ggml_backend_reg_by_name("MTL"); + if (!metal || ggml_backend_reg_dev_count(metal) == 0) { + llama_backend_free(); + return fail("MTL registry or device is missing"); + } + ggml_backend_dev_t device = ggml_backend_reg_dev_get(metal, 0); + if (!device || ggml_backend_dev_type(device) != GGML_BACKEND_DEVICE_TYPE_GPU) { + llama_backend_free(); + return fail("MTL device is not a GPU"); + } + if (!has_embedded_library(metal)) { + llama_backend_free(); + return fail("MTL registry does not report EMBED_LIBRARY=1"); + } + if (!llama_supports_gpu_offload()) { + llama_backend_free(); + return fail("llama does not report GPU offload support"); + } + if (!run_metal_add_probe(device)) { + llama_backend_free(); + return fail("MTL F32 ADD graph did not execute correctly"); + } + + llama_model_params model_params = llama_model_default_params(); + model_params.n_gpu_layers = 1; + llama_model * model = llama_model_load_from_file(model_path, model_params); + if (!model) { + llama_backend_free(); + return fail("model load failed"); + } + + const std::regex offload_pattern("offloaded ([1-9][0-9]*)/([1-9][0-9]*) layers to GPU"); + if (!std::regex_search(logs, offload_pattern)) { + llama_model_free(model); + llama_backend_free(); + return fail("positive GPU layer offload was not logged"); + } + llama_context_params context_params = llama_context_default_params(); + context_params.n_ctx = 64; + llama_context * context = llama_init_from_model(model, context_params); + if (!context) { + llama_model_free(model); + llama_backend_free(); + return fail("context creation failed"); + } + + llama_token tokens[] = {1, 2, 3}; + const int decode_result = llama_decode( + context, llama_batch_get_one(tokens, sizeof(tokens) / sizeof(tokens[0]))); + const bool used_embedded_library = + logs.find("using embedded metal library") != std::string::npos; + llama_free(context); + llama_model_free(model); + llama_backend_free(); + if (decode_result != 0) { + return fail("decode failed"); + } + if (!used_embedded_library) { + return fail("embedded Metal source path was not used"); + } + + std::cout << "Metal smoke test PASSED\n"; + return 0; +} + +#else + +#if defined(__APPLE__) \ + && (defined(__aarch64__) || defined(__arm64__)) +#error "Metal smoke test cannot skip on macOS ARM64" +#endif + +import std; + +int main() { + std::cout << "Metal smoke test skipped on unsupported target\n"; + return 0; +} + +#endif diff --git a/tests/fetch_llamacpp_model.py b/tests/fetch_llamacpp_model.py new file mode 100644 index 0000000..ce534e3 --- /dev/null +++ b/tests/fetch_llamacpp_model.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""Fetch the pinned GGUF test model with cryptographic verification.""" +import argparse +import hashlib +import os +import sys +import urllib.request +from pathlib import Path + +_MODEL_URL = ( + "https://huggingface.co/ggml-org/models-moved/resolve/" + "499bc8821c6b12b4e53c5bffcb21ec206f212d81/tinyllamas/" + "stories15M-q4_0.gguf" +) +_MODEL_SIZE = 19077344 +_MODEL_SHA256 = "66967fbece6dbe97886593fdbb73589584927e29119ec31f08090732d1861739" +_DOWNLOAD_TIMEOUT = 60 + + +def sha256_file(path: str) -> str: + h = hashlib.sha256() + with open(path, "rb") as f: + while chunk := f.read(1 << 20): + h.update(chunk) + return h.hexdigest() + + +def fetch(output: str, *, url: str = _MODEL_URL, + expected_size: int = _MODEL_SIZE, + expected_sha256: str = _MODEL_SHA256) -> str: + output = os.path.abspath(output) + os.makedirs(os.path.dirname(output), exist_ok=True) + + if os.path.isfile(output): + size = os.path.getsize(output) + digest = sha256_file(output) + if size == expected_size and digest == expected_sha256: + print(f"Model already present and verified: {output}", file=sys.stderr) + return output + print(f"Existing file {output} (size={size}, sha256={digest}) " + f"does not match expected (size={expected_size}, " + f"sha256={expected_sha256}); re-downloading", + file=sys.stderr) + + tmp = output + ".tmp" + try: + if os.path.exists(tmp): + os.unlink(tmp) + print(f"Downloading {url} ...", file=sys.stderr) + downloaded = 0 + with urllib.request.urlopen( + url, timeout=_DOWNLOAD_TIMEOUT) as response, \ + open(tmp, "wb") as target: + while chunk := response.read( + min(1 << 20, expected_size - downloaded + 1)): + downloaded += len(chunk) + if downloaded > expected_size: + raise RuntimeError( + f"Downloaded model exceeds expected size {expected_size}") + target.write(chunk) + actual_size = os.path.getsize(tmp) + if actual_size != expected_size: + raise RuntimeError( + f"Downloaded model size {actual_size} != expected {expected_size}") + actual_sha = sha256_file(tmp) + if actual_sha != expected_sha256: + raise RuntimeError( + f"Downloaded model SHA-256 {actual_sha} != expected {expected_sha256}") + os.replace(tmp, output) + print(f"Model verified and saved to {output}", file=sys.stderr) + except BaseException: + if os.path.isfile(tmp): + os.unlink(tmp) + raise + return output + + +def self_test() -> None: + import tempfile + with tempfile.TemporaryDirectory() as td: + payload = b"local pinned model payload\0" * 64 + source = os.path.join(td, "source.gguf") + with open(source, "wb") as f: + f.write(payload) + url = Path(source).as_uri() + size = len(payload) + digest = hashlib.sha256(payload).hexdigest() + + output = os.path.join(td, "model.gguf") + fetch(output, url=url, expected_size=size, + expected_sha256=digest) + fetch(output, url="https://invalid.example/reuse.gguf", + expected_size=size, expected_sha256=digest) + + for label, rejected_size, rejected_digest in ( + ("size", size + 1, digest), + ("digest", size, "0" * 64)): + rejected = os.path.join(td, f"reject-{label}.gguf") + try: + fetch(rejected, url=url, expected_size=rejected_size, + expected_sha256=rejected_digest) + except RuntimeError: + if os.path.exists(rejected) or os.path.exists(rejected + ".tmp"): + raise AssertionError(f"{label} rejection left output bytes") + else: + raise AssertionError(f"{label} mismatch was accepted") + + with open(output, "wb") as f: + f.write(b"invalid existing output") + fetch(output, url=url, expected_size=size, + expected_sha256=digest) + if sha256_file(output) != digest or os.path.exists(output + ".tmp"): + raise AssertionError("atomic replacement did not produce verified output") + + print("Self-test passed", file=sys.stderr) + + +def main() -> int: + ap = argparse.ArgumentParser(description="Fetch pinned GGUF test model") + ap.add_argument("--output", help="Output path for the model file") + ap.add_argument("--self-test", action="store_true", + help="Run local self-test only") + args = ap.parse_args() + if args.self_test: + self_test() + return 0 + if not args.output: + ap.error("--output is required (or use --self-test)") + path = fetch(args.output) + print(os.path.abspath(path)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_check_llamacpp_snapshot.py b/tests/test_check_llamacpp_snapshot.py new file mode 100644 index 0000000..dde6fc5 --- /dev/null +++ b/tests/test_check_llamacpp_snapshot.py @@ -0,0 +1,275 @@ +"""Mutation tests for the ggml-org.llamacpp llama.cpp snapshot gate.""" +from __future__ import annotations + +import copy +import importlib.util +import json +import subprocess +import unittest +from pathlib import Path + + +SCRIPT = Path(__file__).with_name("check_llamacpp_snapshot.py") +SPEC = importlib.util.spec_from_file_location("check_llamacpp_snapshot", SCRIPT) +assert SPEC and SPEC.loader +checker = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(checker) + + +class TestLlamacppSnapshotMutations(unittest.TestCase): + @classmethod + def setUpClass(cls): + lua = checker.find_lua() + cls.report = json.loads(checker.REPORT_PATH.read_text()) + cls.original = { + name: checker.load_descriptor(lua, path) + for name, path in checker.DESCRIPTORS.items() + } + + def setUp(self): + self.descriptors = copy.deepcopy(self.original) + self.tag = self.report["upstream"]["tag"] + + def assert_rejected(self, function, pattern): + with self.assertRaisesRegex(checker.CheckError, pattern): + if function is checker.check_dependencies: + function(self.descriptors, self.tag) + elif function in (checker.check_cpu_private_macros, + checker.check_module_contract): + function(self.descriptors) + else: + function(self.descriptors, self.report) + + def test_uses_one_llamacpp_descriptor(self): + self.assertEqual( + checker.DESCRIPTORS, + {"ggml-org.llamacpp": checker.ROOT / "pkgs/g/ggml-org.llamacpp.lua"}, + ) + for removed in ( + "compat.ggml-base.lua", + "compat.ggml-cpu.lua", + "compat.ggml-metal.lua", + ): + self.assertFalse((checker.ROOT / "pkgs/c" / removed).exists()) + + def test_rejects_cn_url_before_mirror_is_published(self): + release = self.descriptors["ggml-org.llamacpp"]["xpm"]["linux"][self.tag] + release["url"] = { + "GLOBAL": release["url"], + "CN": "https://gitcode.com/mcpp-res/llamacpp/releases/download/" + f"{self.tag}/llama.cpp-{self.tag}.tar.gz", + } + self.assert_rejected(checker.check_cohort, "URL") + + def test_rejects_legacy_descriptor_name_for_new_package(self): + self.descriptors["ggml-org.llamacpp"]["name"] = "ggml-org.llamacpp" + self.assert_rejected(checker.check_cohort, "descriptor name drift") + + def test_rejects_component_dependency(self): + self.descriptors["ggml-org.llamacpp"]["mcpp"]["deps"] = { + "compat.ggml-base": self.tag, + } + self.assert_rejected(checker.check_dependencies, "component packages") + + def test_rejects_feature_provider_dependency(self): + feature = self.descriptors["ggml-org.llamacpp"]["mcpp"]["features"]["backend-metal"] + feature["deps"] = {"compat.ggml-metal": self.tag} + self.assert_rejected(checker.check_dependencies, "in-package feature") + + def test_rejects_provider_capability(self): + self.descriptors["ggml-org.llamacpp"]["mcpp"]["provides"] = {1: "ggml.accelerator"} + self.assert_rejected(checker.check_dependencies, "provider capabilities") + + def test_rejects_missing_default_translation_unit(self): + sources = self.descriptors["ggml-org.llamacpp"]["mcpp"]["sources"] + source = "*/ggml/src/ggml-cpu/ops.cpp" + key = next(key for key, value in sources.items() if value == source) + del sources[key] + remaining = [sources[key] for key in sorted(sources)] + sources.clear() + sources.update(enumerate(remaining, start=1)) + self.assert_rejected(checker.check_sources, r"default TU set drift.*ops\.cpp") + + def test_rejects_missing_cpu_architecture_translation_unit(self): + sources = self.descriptors["ggml-org.llamacpp"]["mcpp"]["linux"]["sources"] + del sources[max(sources)] + self.assert_rejected(checker.check_sources, "default TU set drift") + + def test_rejects_cpu_translation_unit_in_disabled_feature(self): + mcpp = self.descriptors["ggml-org.llamacpp"]["mcpp"] + source = "*/ggml/src/ggml-cpu/ops.cpp" + sources = mcpp["sources"] + key = next(key for key, value in sources.items() if value == source) + del sources[key] + remaining = [sources[key] for key in sorted(sources)] + sources.clear() + sources.update(enumerate(remaining, start=1)) + mcpp["features"]["parked"] = {"sources": {1: source}} + self.assert_rejected(checker.check_sources, r"default TU set drift.*ops\.cpp") + + def test_rejects_default_metal_source(self): + sources = self.descriptors["ggml-org.llamacpp"]["mcpp"]["sources"] + sources[max(sources) + 1] = "*/ggml/src/ggml-metal/ggml-metal.cpp" + self.assert_rejected(checker.check_sources, "default TU set drift") + + def test_rejects_missing_metal_translation_unit(self): + feature = self.descriptors["ggml-org.llamacpp"]["mcpp"]["features"]["backend-metal"] + del feature["sources"][max(feature["sources"])] + self.assert_rejected(checker.check_sources, "Metal TU set drift") + + def test_rejects_duplicate_metal_translation_unit(self): + sources = self.descriptors["ggml-org.llamacpp"]["mcpp"]["features"]["backend-metal"]["sources"] + sources[max(sources) + 1] = "*/ggml/src/ggml-metal/ggml-metal.cpp" + self.assert_rejected(checker.check_sources, "duplicate Metal TU") + + def test_rejects_duplicate_translation_unit(self): + sources = self.descriptors["ggml-org.llamacpp"]["mcpp"]["sources"] + sources[max(sources) + 1] = "*/ggml/src/ggml-backend-reg.cpp" + self.assert_rejected(checker.check_sources, "duplicate") + + def test_rejects_model_wildcard(self): + sources = self.descriptors["ggml-org.llamacpp"]["mcpp"]["sources"] + key = next(key for key, value in sources.items() if value == "*/src/models/afmoe.cpp") + sources[key] = "*/src/models/*.cpp" + self.assert_rejected(checker.check_sources, "wildcard source") + + def test_rejects_backend_metal_on_platform_scope(self): + mcpp = self.descriptors["ggml-org.llamacpp"]["mcpp"] + mcpp.setdefault("linux", {})["features"] = {"backend-metal": {}} + self.assert_rejected(checker.check_registry_and_features, "feature placement") + + def test_rejects_backend_feature_contract_drift(self): + mutations = ( + ("missing CPU", lambda features: features.pop("backend-cpu")), + ("extra feature", lambda features: features.update({"backend-cuda": {}})), + ("non-CPU default", lambda features: features["default"].update( + {"implies": {1: "backend-metal"}})), + ) + for label, mutate in mutations: + mutated = copy.deepcopy(self.descriptors) + mutate(mutated["ggml-org.llamacpp"]["mcpp"]["features"]) + with self.subTest(label=label): + with self.assertRaisesRegex(checker.CheckError, "CPU and Metal|select backend-cpu"): + checker.check_registry_and_features(mutated, self.report) + + def test_rejects_registry_macro_outside_registry_source(self): + flags = self.descriptors["ggml-org.llamacpp"]["mcpp"]["flags"] + flags[max(flags) + 1] = {"glob": "*/src/llama.cpp", "defines": {1: "GGML_USE_CPU"}} + self.assert_rejected(checker.check_registry_and_features, "one registry-only owner") + + def test_rejects_cpu_repack_macro_removal(self): + flags = self.descriptors["ggml-org.llamacpp"]["mcpp"]["cflags"] + remaining = [value for _, value in sorted(flags.items()) + if value != "-DGGML_USE_CPU_REPACK"] + flags.clear() + flags.update(enumerate(remaining, start=1)) + self.assert_rejected(checker.check_cpu_private_macros, "CPU repack") + + def test_rejects_llamafile_macro_removal(self): + flags = self.descriptors["ggml-org.llamacpp"]["mcpp"]["flags"] + key = next(key for key, value in flags.items() + if value.get("glob") == "*/ggml/src/ggml-cpu/**") + del flags[key] + remaining = [flags[key] for key in sorted(flags)] + flags.clear() + flags.update(enumerate(remaining, start=1)) + self.assert_rejected(checker.check_cpu_private_macros, "llamafile") + + def test_rejects_generated_module_drift(self): + generated = self.descriptors["ggml-org.llamacpp"]["mcpp"]["generated_files"] + generated["mcpp_generated/llama.cppm"] += "\n// drift\n" + self.assert_rejected(checker.check_module_contract, "generated module input drift") + + def test_rejects_module_name_drift(self): + self.descriptors["ggml-org.llamacpp"]["mcpp"]["modules"] = {1: "wrong"} + self.assert_rejected(checker.check_module_contract, "exactly the llama module") + + def test_rejects_extra_target(self): + self.descriptors["ggml-org.llamacpp"]["mcpp"]["targets"]["extra"] = { + "kind": "lib", + } + self.assert_rejected(checker.check_module_contract, "exactly one llama target") + + def test_rejects_absolute_xcode_metal_tool_paths(self): + generated = self.descriptors["ggml-org.llamacpp"]["mcpp"]["generated_files"] + for tool in ("/usr/bin/xcrun", "/usr/bin/metal", "/usr/bin/metallib"): + mutated = copy.deepcopy(self.descriptors) + mutated["ggml-org.llamacpp"]["mcpp"]["generated_files"]["build.mcpp"] += ( + f"\n// {tool}\n" + ) + with self.subTest(tool=tool): + with self.assertRaisesRegex(checker.CheckError, "absolute Xcode Metal tool path"): + checker.check_metal_build_contract(mutated, self.report) + self.assertTrue(generated["build.mcpp"]) + + def test_rejects_process_launch_apis_in_metal_generator(self): + calls = ( + 'std::system("metal")', + 'popen("metal", "r")', + 'execl("xcrun", "xcrun", "metal", nullptr)', + 'posix_spawn(...)', + ) + for call in calls: + with self.subTest(call=call): + mutated = copy.deepcopy(self.descriptors) + mutated["ggml-org.llamacpp"]["mcpp"]["generated_files"]["build.mcpp"] += ( + f"\n{call};\n" + ) + with self.assertRaisesRegex(checker.CheckError, "process launch API"): + checker.check_metal_build_contract(mutated, self.report) + + def test_rejects_missing_metal_feature_gate(self): + mutated = copy.deepcopy(self.descriptors) + source = mutated["ggml-org.llamacpp"]["mcpp"]["generated_files"]["build.mcpp"] + mutated["ggml-org.llamacpp"]["mcpp"]["generated_files"]["build.mcpp"] = source.replace( + ' const char * enabled = std::getenv("MCPP_FEATURE_BACKEND_METAL");\n' + ' if (!enabled || std::string(enabled) != "1") return 0;\n', + "", + ) + with self.assertRaisesRegex(checker.CheckError, "must no-op"): + checker.check_metal_build_contract(mutated, self.report) + + def test_rejects_missing_feature_whitelist(self): + mutated = copy.deepcopy(self.descriptors) + source = mutated["ggml-org.llamacpp"]["mcpp"]["generated_files"]["build.mcpp"] + mutated["ggml-org.llamacpp"]["mcpp"]["generated_files"]["build.mcpp"] = source.replace( + " if (int result = validate_features(); result != 0) return result;\n", + "", + ) + with self.assertRaisesRegex(checker.CheckError, "must reject undeclared feature"): + checker.check_metal_build_contract(mutated, self.report) + + def test_workflow_runs_snapshot_and_selection_suites(self): + workflow = checker.ROOT / ".github/workflows/validate.yml" + ruby = r''' +doc = YAML.load_file(ARGV[0]) +step = doc.fetch('jobs').fetch('lint').fetch('steps').find do |item| + item['name'] == 'Check ggml-org.llamacpp snapshot' +end +abort 'snapshot check step missing' unless step +puts step.fetch('run') +''' + result = subprocess.run( + ["ruby", "-ryaml", "-e", ruby, str(workflow)], + text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("python3 -m unittest tools/llamacpp/test_gen_exports.py -v", result.stdout) + self.assertIn("python3 -m unittest tests/test_check_llamacpp_snapshot.py -v", result.stdout) + self.assertIn("python3 tools/llamacpp/audit_snapshot.py", result.stdout) + self.assertIn("--check-exports tools/llamacpp/module/gen_exports", result.stdout) + self.assertIn("--tag b10069", result.stdout) + self.assertIn("--commit 178a6c44937154dc4c4eff0d166f4a044c4fceba", result.stdout) + self.assertIn("python3 tests/check_llamacpp_snapshot.py", result.stdout) + + def test_metal_member_consumes_merged_package(self): + root_manifest = (checker.ROOT / "mcpp.toml").read_text() + metal_manifest = checker.ROOT / "tests/examples/llamacpp-internal-metal/mcpp.toml" + self.assertIn('"tests/examples/llamacpp-internal-metal"', root_manifest) + self.assertIn('features = ["backend-metal"]', metal_manifest.read_text()) + self.assertNotIn("compat.ggml-metal", metal_manifest.read_text()) + self.assertIn("import llama;", (checker.ROOT / "tests/examples/llamacpp-internal-metal/tests/decode.cpp").read_text()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_fetch_llamacpp_model.py b/tests/test_fetch_llamacpp_model.py new file mode 100644 index 0000000..271eda8 --- /dev/null +++ b/tests/test_fetch_llamacpp_model.py @@ -0,0 +1,142 @@ +"""Tests for immutable, atomic llama.cpp GGUF provisioning.""" +from __future__ import annotations + +import contextlib +import hashlib +import importlib.util +import io +import tempfile +import unittest +from pathlib import Path +from unittest import mock + + +SCRIPT = Path(__file__).with_name("fetch_llamacpp_model.py") +SPEC = importlib.util.spec_from_file_location("fetch_llamacpp_model", SCRIPT) +assert SPEC and SPEC.loader +fetch_model = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(fetch_model) + + +class TestFetchLlamacppModel(unittest.TestCase): + def setUp(self): + self.tempdir = tempfile.TemporaryDirectory() + self.addCleanup(self.tempdir.cleanup) + self.root = Path(self.tempdir.name) + self.payload = b"pinned gguf fixture\0" * 32 + self.source = self.root / "source.gguf" + self.source.write_bytes(self.payload) + self.digest = hashlib.sha256(self.payload).hexdigest() + + def fetch(self, output: Path, *, size=None, digest=None, url=None): + return fetch_model.fetch( + str(output), + url=url or self.source.as_uri(), + expected_size=len(self.payload) if size is None else size, + expected_sha256=self.digest if digest is None else digest, + ) + + def test_pins_immutable_model_identity(self): + self.assertEqual( + fetch_model._MODEL_URL, + "https://huggingface.co/ggml-org/models-moved/resolve/" + "499bc8821c6b12b4e53c5bffcb21ec206f212d81/" + "tinyllamas/stories15M-q4_0.gguf", + ) + self.assertEqual(fetch_model._MODEL_SIZE, 19077344) + self.assertEqual( + fetch_model._MODEL_SHA256, + "66967fbece6dbe97886593fdbb73589584927e29119ec31f08090732d1861739", + ) + + def test_accepts_local_source_through_fetch_path(self): + output = self.root / "models" / "model.gguf" + result = self.fetch(output) + self.assertTrue(Path(result).is_absolute()) + self.assertTrue(Path(result).samefile(output)) + self.assertEqual(output.read_bytes(), self.payload) + self.assertFalse(Path(f"{output}.tmp").exists()) + + def test_rejects_wrong_size_without_replacing_output(self): + output = self.root / "model.gguf" + output.write_bytes(b"existing") + with self.assertRaisesRegex(RuntimeError, "size"): + self.fetch(output, size=len(self.payload) + 1) + self.assertEqual(output.read_bytes(), b"existing") + self.assertFalse(Path(f"{output}.tmp").exists()) + + def test_rejects_wrong_digest_without_replacing_output(self): + output = self.root / "model.gguf" + output.write_bytes(b"existing") + with self.assertRaisesRegex(RuntimeError, "SHA-256"): + self.fetch(output, digest="0" * 64) + self.assertEqual(output.read_bytes(), b"existing") + self.assertFalse(Path(f"{output}.tmp").exists()) + + def test_reuses_only_valid_existing_output(self): + output = self.root / "model.gguf" + output.write_bytes(self.payload) + with mock.patch.object(fetch_model.urllib.request, "urlopen") as urlopen: + result = self.fetch(output, url="https://invalid.example/model.gguf") + urlopen.assert_not_called() + self.assertTrue(Path(result).is_absolute()) + self.assertTrue(Path(result).samefile(output)) + + def test_invalid_output_is_atomically_replaced(self): + output = self.root / "model.gguf" + output.write_bytes(b"existing") + self.fetch(output) + self.assertEqual(output.read_bytes(), self.payload) + self.assertFalse(Path(f"{output}.tmp").exists()) + + def test_streams_and_cleans_partial_temp_on_network_failure(self): + timeout = getattr(fetch_model, "_DOWNLOAD_TIMEOUT", None) + self.assertIsNotNone(timeout) + output = self.root / "model.gguf" + tmp = Path(f"{output}.tmp") + tmp.write_bytes(b"stale partial") + response = io.BytesIO(self.payload) + with mock.patch.object( + fetch_model.urllib.request, "urlopen", return_value=response) as urlopen: + self.fetch(output, url="https://example.invalid/model.gguf") + urlopen.assert_called_once_with( + "https://example.invalid/model.gguf", + timeout=timeout, + ) + self.assertEqual(output.read_bytes(), self.payload) + self.assertFalse(tmp.exists()) + + tmp.write_bytes(b"another partial") + with mock.patch.object( + fetch_model.urllib.request, "urlopen", + side_effect=OSError("network down")): + with self.assertRaisesRegex(OSError, "network down"): + self.fetch(output, url="https://example.invalid/model.gguf", + digest="1" * 64) + self.assertFalse(tmp.exists()) + + def test_rejects_response_larger_than_pinned_size(self): + output = self.root / "model.gguf" + oversized = io.BytesIO(self.payload + b"unexpected trailing bytes") + with mock.patch.object( + fetch_model.urllib.request, "urlopen", return_value=oversized): + with self.assertRaisesRegex(RuntimeError, "exceeds expected size"): + self.fetch( + output, + url="https://example.invalid/model.gguf", + size=len(self.payload), + ) + self.assertFalse(output.exists()) + self.assertFalse(Path(f"{output}.tmp").exists()) + + def test_self_test_exercises_fetch(self): + stderr = io.StringIO() + with mock.patch.object(fetch_model, "fetch", wraps=fetch_model.fetch) as fetch, \ + contextlib.redirect_stderr(stderr): + fetch_model.self_test() + self.assertGreaterEqual(fetch.call_count, 4) + self.assertIn("Self-test passed", stderr.getvalue()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_llamacpp_package_contract.py b/tests/test_llamacpp_package_contract.py new file mode 100644 index 0000000..d4ea163 --- /dev/null +++ b/tests/test_llamacpp_package_contract.py @@ -0,0 +1,94 @@ +import pathlib +import re +import subprocess +import tempfile +import tomllib +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +DESCRIPTOR = ROOT / "pkgs/g/ggml-org.llamacpp.lua" +WORKFLOW = ROOT / ".github/workflows/validate.yml" +CONSUMERS = ( + ROOT / "tests/examples/llamacpp-internal-cpu/mcpp.toml", + ROOT / "tests/examples/llamacpp-internal-metal/mcpp.toml", +) + + +class LlamaCppPackageContractTests(unittest.TestCase): + def test_descriptor_publishes_only_the_verified_version(self): + self.assertTrue(DESCRIPTOR.is_file(), "llama.cpp descriptor is missing") + text = DESCRIPTOR.read_text() + self.assertIn('namespace = "ggml-org"', text) + self.assertIn('name = "llamacpp"', text) + self.assertNotIn('name = "ggml-org.llamacpp"', text) + self.assertIn('["b10069"]', text) + self.assertNotIn('["b10107"]', text) + + def test_consumers_resolve_b10069_from_this_checkout(self): + for manifest in CONSUMERS: + with self.subTest(manifest=manifest): + self.assertTrue(manifest.is_file(), f"missing consumer: {manifest}") + data = tomllib.loads(manifest.read_text()) + self.assertEqual(data["indices"]["ggml-org"]["path"], "../../..") + + versions = [] + + def collect(value): + if isinstance(value, dict): + for key, child in value.items(): + if key == "llamacpp": + versions.append( + child if isinstance(child, str) else child["version"] + ) + else: + collect(child) + + collect(data) + self.assertEqual(versions, ["b10069"]) + + def test_workflow_selects_namespaced_descriptor_consumers(self): + workflow = WORKFLOW.read_text() + self.assertIn('lib=$(basename "$f" .lua); lib=${lib##*.}', workflow) + self.assertIn( + '[ "$hit" = 1 ] || full "no workspace member exercises $f"', + workflow, + ) + self.assertNotIn("tools/select_affected_members.py", workflow) + + def test_workflow_dependency_match_is_exact(self): + workflow = WORKFLOW.read_text() + line = next( + (line for line in workflow.splitlines() if 'grep -Eq' in line and '"$mt"' in line), + None, + ) + self.assertIsNotNone(line, "exact dependency-key matcher is missing") + match = re.search(r'grep -Eq "(.+)" "\$mt"', line) + self.assertIsNotNone(match, f"cannot parse dependency matcher: {line}") + pattern = match.group(1).replace("${lib}", "llamacpp") + + for manifest, expected in ( + ('ggml-org.llamacpp = "b10069"\n', True), + ('llamacpp = "b10069"\n', True), + ('# llamacpp = "b10069"\n', False), + ('llamacpp-helper = "1.0.0"\n', False), + ('name = "llamacpp-internal-cpu"\n', False), + ): + with self.subTest(manifest=manifest), tempfile.NamedTemporaryFile( + mode="w", encoding="utf-8" + ) as fixture: + fixture.write(manifest) + fixture.flush() + result = subprocess.run( + ["grep", "-Eq", pattern, fixture.name], check=False + ) + self.assertEqual(result.returncode == 0, expected) + + def test_workflow_forces_cold_llamacpp_materialization(self): + workflow = WORKFLOW.read_text() + self.assertIn("ggml-org-x-ggml-org.llamacpp", workflow) + self.assertIn("Cold-start llama.cpp package", workflow) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/llamacpp/audit_snapshot.py b/tools/llamacpp/audit_snapshot.py new file mode 100644 index 0000000..c68f141 --- /dev/null +++ b/tools/llamacpp/audit_snapshot.py @@ -0,0 +1,559 @@ +#!/usr/bin/env python3 +"""Read-only audit of a llama.cpp upstream snapshot.""" +from __future__ import annotations + +import argparse, hashlib, json, os, re, sys, urllib.request +from collections import OrderedDict +from pathlib import Path + + +_DOWNLOAD_TIMEOUT = 60 +_MAX_ARCHIVE_BYTES = 1 << 30 + + +def sha256_file(path: str | Path) -> str: + h = hashlib.sha256() + with open(path, 'rb') as f: + for chunk in iter(lambda: f.read(1 << 20), b''): + h.update(chunk) + return h.hexdigest() + + +def download_file(url: str, destination: str | Path, *, + timeout: int = _DOWNLOAD_TIMEOUT, + max_bytes: int = _MAX_ARCHIVE_BYTES) -> None: + destination = os.fspath(destination) + os.makedirs(os.path.dirname(os.path.abspath(destination)), exist_ok=True) + temporary = destination + '.tmp' + downloaded = 0 + try: + if os.path.exists(temporary): + os.unlink(temporary) + with urllib.request.urlopen(url, timeout=timeout) as response, \ + open(temporary, 'wb') as target: + while True: + chunk = response.read(min(1 << 20, max_bytes - downloaded + 1)) + if not chunk: + break + downloaded += len(chunk) + if downloaded > max_bytes: + raise RuntimeError( + f'download exceeds {max_bytes}-byte limit: {url}') + target.write(chunk) + os.replace(temporary, destination) + except BaseException: + if os.path.isfile(temporary): + os.unlink(temporary) + raise + + +def safe_extract_tar(archive, destination: str): + import tarfile + dest = os.path.realpath(destination) + os.makedirs(dest, exist_ok=True) + with tarfile.open(fileobj=archive, mode='r:*') as tf: + for member in tf.getmembers(): + resolved = os.path.realpath(os.path.join(dest, member.name)) + if os.path.commonpath([dest, resolved]) != dest: + raise ValueError(f"archive member escapes extraction directory: {member.name}") + archive.seek(0) + with tarfile.open(fileobj=archive, mode='r:*') as tf2: + tf2.extractall(dest, filter='data') + + +def _tokenize_cmake(text: str): + lines = [] + for ln in text.splitlines(): + idx = ln.find('#') + lines.append(ln[:idx] if idx >= 0 else ln) + text = ' '.join(lines) + i, n = 0, len(text) + while i < n: + c = text[i] + if c.isspace(): + i += 1 + continue + if c == '"': + j = i + 1 + while j < n: + if text[j] == '\\': + j += 2 + elif text[j] == '"': + j += 1 + break + else: + j += 1 + yield ('STR', text[i+1:j-1]) + i = j + continue + if c in '()': + yield (c, c) + i += 1 + continue + j = i + while j < n and not text[j].isspace() and text[j] not in '()"': + j += 1 + yield ('WORD', text[i:j]) + i = j + + +def extract_cmake_call(text: str, callee: str, + first_arg: str | None = None + ) -> tuple[str | None, list[str]]: + tokens = list(_tokenize_cmake(text)) + for i, (typ, val) in enumerate(tokens): + if typ != 'WORD' or val != callee: + continue + if i + 1 >= len(tokens) or tokens[i + 1] != ('(', '('): + continue + depth = 1 + j = i + 2 + args: list[str] = [] + while j < len(tokens) and depth > 0: + typ2, val2 = tokens[j] + if typ2 == '(': + depth += 1 + if depth == 1: + j += 1 + continue + elif typ2 == ')': + depth -= 1 + if depth == 0: + break + if depth == 1 and typ2 in ('WORD', 'STR'): + args.append(val2) + j += 1 + if not args: + continue + if first_arg is not None and args[0] != first_arg: + continue + return (args[0], args[1:]) + return (None, []) + + +def extract_cmake_list_appends(text: str, variable: str) -> list[str]: + """Return literal sources appended to a CMake list variable.""" + tokens = list(_tokenize_cmake(text)) + result: list[str] = [] + for i, (typ, val) in enumerate(tokens): + if typ != 'WORD' or val.lower() != 'list': + continue + if i + 1 >= len(tokens) or tokens[i + 1] != ('(', '('): + continue + depth = 1 + j = i + 2 + args: list[str] = [] + while j < len(tokens) and depth > 0: + typ2, val2 = tokens[j] + if typ2 == '(': + depth += 1 + elif typ2 == ')': + depth -= 1 + if depth == 0: + break + elif depth == 1 and typ2 in ('WORD', 'STR'): + args.append(val2) + j += 1 + if len(args) >= 2 and args[0].upper() == 'APPEND' and args[1] == variable: + result.extend(args[2:]) + return result + + +def _expand_glob(base: str, pattern: str) -> list[str]: + directory = os.path.dirname(pattern) + glob_pat = os.path.basename(pattern) + full_dir = os.path.join(base, directory) + if not os.path.isdir(full_dir): + return [] + import fnmatch + result = [] + for fn in sorted(os.listdir(full_dir)): + if fnmatch.fnmatch(fn, glob_pat): + result.append(os.path.join(directory, fn)) + return sorted(result) + + +def _read_cmake_forest(root: str) -> tuple[str, dict[str, str]]: + top = os.path.join(root, 'CMakeLists.txt') + with open(top, 'r') as f: + top_text = f.read() if os.path.isfile(top) else '' + subs: dict[str, str] = {} + for dirpath, _dirnames, filenames in os.walk(root): + if 'CMakeLists.txt' in filenames: + sub = os.path.relpath(dirpath, root) + if sub == '.': + continue + fp = os.path.join(dirpath, 'CMakeLists.txt') + with open(fp, 'r') as f: + subs[sub] = f.read() + return top_text, subs + + +def _find_gpu_backend_calls(text: str, name: str) -> list[str]: + """Find ggml_add_backend_library(NAME ...) calls, skipping function defs.""" + results = [] + for m in re.finditer( + r'^\s*ggml_add_backend_library\s*\(\s*(' + re.escape(name) + r')\b', + text, re.MULTILINE, + ): + before = text[:m.start()] + opens = before.count('function(') + closes = before.count('endfunction()') + if opens == closes: # not inside a function definition + _, args = extract_cmake_call( + text[m.start():], 'ggml_add_backend_library', name) + if args: + results.extend(args) + return results + + +def _find_files(root: str, ext: str) -> list[str]: + found = [] + for dirpath, _dirnames, filenames in os.walk(root): + for fn in filenames: + if fn.endswith(ext): + rel = os.path.relpath(os.path.join(dirpath, fn), root) + found.append(rel) + return sorted(found) + + +IGNORED_CPU_SOURCE_VARIABLES = { + '${GGML_KLEIDIAI_SME_SOURCES}', + '${GGML_KLEIDIAI_SME2_SOURCES}', + '${GGML_KLEIDIAI_SOURCES}', +} + + +def _collect_cpu_sources(root: str, top_text: str, + sub_cmakes: dict[str, str]) -> tuple[list[str], list[str], list[str]]: + common: set[str] = set() + x86: set[str] = set() + arm: set[str] = set() + cmakes = [('.', top_text), *sub_cmakes.items()] + for subdir, text in cmakes: + if 'GGML_CPU_SOURCES' not in text: + continue + for source in extract_cmake_list_appends(text, 'GGML_CPU_SOURCES'): + if '$' in source: + if source in IGNORED_CPU_SOURCE_VARIABLES: + continue + raise ValueError(f'unexpected CPU source variable: {source}') + # Literal conditional sources are conservatively included. This audit is + # not a full CMake evaluator, so unknown variable expansions fail closed. + if Path(source).suffix not in {'.c', '.cc', '.cpp', '.m', '.mm'}: + continue + candidates = ( + Path(root) / source, + Path(root) / subdir / source, + Path(root) / subdir / '..' / source, + ) + resolved = next((path.resolve() for path in candidates if path.is_file()), None) + if resolved is None: + raise ValueError(f'unresolved CPU source: {source}') + relative = resolved.relative_to(Path(root).resolve()).as_posix() + prefix = 'ggml/src/ggml-cpu/' + if not relative.startswith(prefix): + continue + cpu_relative = relative.removeprefix(prefix) + if cpu_relative.startswith('arch/x86/'): + x86.add(relative) + elif cpu_relative.startswith('arch/arm/'): + arm.add(relative) + elif (cpu_relative.startswith('arch/') + or cpu_relative.startswith('kleidiai/') + or cpu_relative.startswith('spacemit/')): + continue + else: + common.add(relative) + if not common or not x86 or not arm: + raise ValueError('cannot derive common/x86/arm GGML CPU sources from CMake') + return sorted(common), sorted(x86), sorted(arm) + + +def collect_snapshot(root, tag, commit, url, archive_sha256): + top_text, sub_cmakes = _read_cmake_forest(root) + if not top_text and not sub_cmakes: + raise FileNotFoundError(f"No CMakeLists.txt found under {root}") + + def _extract_from_forest(callee, first_arg, cmake_dir='.'): + for subdir, text in sub_cmakes.items(): + name, args = extract_cmake_call(text, callee, first_arg) + if name is not None: + return name, [os.path.join(subdir, a) for a in args] + name, args = extract_cmake_call(top_text, callee, first_arg) + if name is not None: + return name, args + return None, [] + + _, ggml_base = _extract_from_forest('add_library', 'ggml-base') + _, ggml_registry = _extract_from_forest('add_library', 'ggml') + _, ggml_metal = _extract_from_forest('ggml_add_backend_library', 'ggml-metal') + _, llama = _extract_from_forest('add_library', 'llama') + + # Expand GLOB in llama sources + llama_expanded: list[str] = [] + model_files: list[str] = [] + for src in llama: + if '${LLAMA_MODELS_SOURCES}' in src: + for subdir, text in sub_cmakes.items(): + _, glob_args = extract_cmake_call(text, 'file', 'GLOB') + if (glob_args and len(glob_args) >= 2 and + glob_args[0] == 'LLAMA_MODELS_SOURCES'): + pattern = glob_args[1].strip('"') + model_dir = os.path.join(root, subdir) + model_files = _expand_glob(model_dir, pattern) + break + if not model_files: + _, glob_args = extract_cmake_call(top_text, 'file', 'GLOB') + if glob_args and len(glob_args) >= 2: + model_files = _expand_glob(root, glob_args[1]) + elif src.endswith('.cpp') or src.endswith('.c'): + llama_expanded.append(src) + + cpu_common, cpu_arch_x86, cpu_arch_arm = _collect_cpu_sources( + root, top_text, sub_cmakes) + + # Registry macros + registry: dict[str, str] = {} + for candidate in ['ggml/src/ggml-backend-reg.cpp', 'ggml-backend-reg.cpp']: + reg_path = os.path.join(root, candidate) + if os.path.isfile(reg_path): + with open(reg_path, 'r') as f: + reg_text = f.read() + for macro in ['GGML_USE_CPU', 'GGML_USE_METAL', 'GGML_USE_CUDA', + 'GGML_USE_VULKAN', 'GGML_USE_SYCL', 'GGML_USE_CANN']: + # Match within a single #ifdef ... #endif block only + block_pat = re.compile( + r'#ifdef\s+' + macro + r'\b[^\n]*\n(.*?)#endif', + re.DOTALL) + for block_m in block_pat.finditer(reg_text): + inner = block_m.group(1) + rm = re.search(r'register_backend\((\w+)\(\)\)', inner) + if rm: + registry[macro] = rm.group(1) + break + break + + # Metal shader inputs + metal_inputs: list[str] = [] + for candidate_dir in [os.path.join(root, 'ggml', 'src', 'ggml-metal'), + os.path.join(root, 'ggml-metal')]: + if os.path.isdir(candidate_dir): + for pat in ['ggml-common.h', 'ggml-metal.metal', 'ggml-metal-impl.h']: + for base in [candidate_dir, os.path.dirname(candidate_dir)]: + full = os.path.join(base, pat) + if os.path.isfile(full): + rel = os.path.relpath(full, root) + if rel not in metal_inputs: + metal_inputs.append(rel) + break + if metal_inputs: + break + metal_inputs.sort() + + # C++20 exceptions + cpp20_exceptions = [] + for p in ['src/models/dflash.cpp', 'src/models/eagle3.cpp', + 'src/models/hunyuan-dense.cpp', 'src/models/llama-embed.cpp', + 'src/models/minimax-m2.cpp', + 'src/models/t5.cpp']: + if os.path.isfile(os.path.join(root, p)): + cpp20_exceptions.append(p) + + # Public header hashes + header_hashes = {} + for hdr in [ + 'include/llama.h', + 'ggml/include/ggml.h', + 'ggml/include/ggml-cpu.h']: + p = os.path.join(root, hdr) + if os.path.isfile(p): + header_hashes[hdr] = sha256_file(p) + + return OrderedDict([ + ('schema', 1), + ('upstream', OrderedDict([ + ('tag', tag), ('commit', commit), + ('url', url), ('sha256', archive_sha256), + ])), + ('sources', OrderedDict([ + ('ggml_base', sorted(set(ggml_base))), + ('ggml_registry', sorted(set(ggml_registry))), + ('ggml_cpu_common', cpu_common), + ('ggml_cpu_x86', cpu_arch_x86), + ('ggml_cpu_arm', cpu_arch_arm), + ('llama_core', sorted(set(llama_expanded))), + ('models', sorted(set(model_files))), + ('ggml_metal', sorted(set(ggml_metal))), + ])), + ('registry', registry), + ('metal', OrderedDict([ + ('frameworks', ['Foundation', 'Metal', 'MetalKit']), + ('shader_inputs', metal_inputs), + ])), + ('dialect_exceptions', OrderedDict([ + ('c++20', cpp20_exceptions), + ])), + ('platform_links', {'windows_cpu': ['advapi32']}), + ('public_header_sha256', header_hashes), + ]) + + +def compare_reports(old: dict, new: dict) -> list[str]: + diffs = [] + for key in ['upstream', 'sources', 'registry', 'metal', 'dialect_exceptions', + 'platform_links', 'public_header_sha256']: + if old.get(key) != new.get(key): + diffs.append(f"{key} changed") + return diffs + + +def check_exports(root: str, output_dir: Path) -> int: + from gen_exports import generate_exports, sync_outputs + + llama_inc, ggml_inc, skipped_txt = generate_exports(root) + return sync_outputs( + output_dir, + { + 'llama.inc': llama_inc, + 'required_ggml.inc': ggml_inc, + 'llama.skipped.txt': skipped_txt, + }, + True, + ) + + +def main() -> int: + ap = argparse.ArgumentParser(description='Audit a llama.cpp snapshot') + src = ap.add_mutually_exclusive_group() + src.add_argument('--upstream', help='Local llama.cpp checkout directory') + src.add_argument('--url', help='URL of the source tarball') + ap.add_argument('--sha256', help='Expected SHA-256 of the tarball') + ap.add_argument('--tag', help='Upstream tag') + ap.add_argument('--commit', help='Upstream commit SHA') + out = ap.add_mutually_exclusive_group() + out.add_argument('--output', help='Write JSON report to this file') + out.add_argument('--check', help='Regenerate and compare with this report file') + out.add_argument('--compare', help='Compare OLD_REPORT with a new generation') + ap.add_argument( + '--check-exports', type=Path, + help='Compare generated module exports using the same verified source tree') + args = ap.parse_args() + + if args.check_exports and not args.check: + ap.error('--check-exports requires --check') + if args.url and not args.sha256: + ap.error('--sha256 is required when using --url') + if args.sha256 and not args.url: + ap.error('--sha256 requires --url') + + expected = None + identity = None + if args.check: + try: + with open(args.check, 'r') as f: + expected = json.load(f, object_pairs_hook=OrderedDict) + except (OSError, json.JSONDecodeError) as error: + ap.error(f'cannot load check report: {error}') + identity = expected.get('upstream') + if not isinstance(identity, dict): + ap.error('check report is missing upstream identity') + required = ('tag', 'commit', 'url', 'sha256') + missing = [key for key in required + if not isinstance(identity.get(key), str) + or not identity[key]] + if missing: + ap.error('check report is missing upstream identity fields: ' + + ', '.join(missing)) + for option in required: + explicit = getattr(args, option) + if explicit is not None and explicit != identity[option]: + ap.error(f'--{option} conflicts with report identity ' + f'({explicit!r} != {identity[option]!r})') + + if identity is not None: + tag = identity['tag'] + commit = identity['commit'] + report_url = identity['url'] + report_sha256 = identity['sha256'] + else: + if not args.upstream and not args.url: + ap.error('one of --upstream or --url is required without --check') + if not args.tag or not args.commit: + ap.error('--tag and --commit are required without --check') + tag = args.tag + commit = args.commit + report_url = args.url + report_sha256 = args.sha256 + + root: str + temporary = None + if args.upstream: + root = os.path.abspath(args.upstream) + else: + import tempfile + temporary = tempfile.TemporaryDirectory(prefix='llamacpp-audit-') + tmpdir = temporary.name + tarball = os.path.join(tmpdir, 'source.tar.gz') + source_url = args.url or report_url + source_sha256 = args.sha256 or report_sha256 + download_file(source_url, tarball) + actual = sha256_file(tarball) + if actual != source_sha256: + os.unlink(tarball) + sys.exit(f"SHA-256 mismatch: expected {source_sha256}, got {actual}") + extract_root = os.path.join(tmpdir, 'extracted') + os.makedirs(extract_root, exist_ok=True) + with open(tarball, 'rb') as f: + safe_extract_tar(f, extract_root) + entries = os.listdir(extract_root) + dirs = [e for e in entries + if os.path.isdir(os.path.join(extract_root, e))] + root = os.path.join(extract_root, dirs[0]) if dirs else extract_root + + exports_result = 0 + try: + report = collect_snapshot( + root=root, tag=tag, commit=commit, + url=report_url or 'file://' + root, + archive_sha256=report_sha256 or 'unknown', + ) + if args.check_exports: + exports_result = check_exports(root, args.check_exports) + finally: + if temporary is not None: + temporary.cleanup() + + if args.output: + with open(args.output, 'w') as f: + json.dump(report, f, indent=2) + f.write('\n') + print(f"Snapshot written to {args.output}", file=sys.stderr) + elif args.check: + assert expected is not None + if expected == report: + print("Snapshot matches.", file=sys.stderr) + else: + print("Snapshot differs:", file=sys.stderr) + for diff in compare_reports(expected, report): + print(f" - {diff}", file=sys.stderr) + return 1 + if exports_result != 0: + return exports_result + elif args.compare: + with open(args.compare, 'r') as f: + old = json.load(f, object_pairs_hook=OrderedDict) + diffs = compare_reports(old, report) + if diffs: + for d in diffs: + print(d) + else: + print("No differences.") + else: + json.dump(report, sys.stdout, indent=2) + print() + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/llamacpp/gen_exports.py b/tools/llamacpp/gen_exports.py new file mode 100644 index 0000000..0e8615b --- /dev/null +++ b/tools/llamacpp/gen_exports.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python3 +"""Deterministic API export generator using Clang JSON AST dump.""" +from __future__ import annotations + +import argparse, json, os, re, subprocess, sys, tempfile +from pathlib import Path + +REQUIRED_GGML_TYPES = { + "ggml_abort_callback", "ggml_backend_buffer_t", + "ggml_backend_buffer_type_t", "ggml_backend_get_features_t", + "ggml_backend_reg_t", "ggml_backend_t", + "ggml_backend_dev_t", "ggml_backend_sched_eval_callback", + "ggml_cgraph", "ggml_context", "ggml_log_callback", + "ggml_init_params", "ggml_log_level", "ggml_numa_strategy", + "ggml_opt_dataset_t", + "ggml_opt_epoch_callback", "ggml_opt_get_optimizer_params", + "ggml_opt_optimizer_type", "ggml_opt_result_t", "ggml_status", + "ggml_tensor", "ggml_threadpool_t", "ggml_type", +} + +REQUIRED_GGML_ENUM_MEMBERS = { + "GGML_BACKEND_DEVICE_TYPE_ACCEL", + "GGML_BACKEND_DEVICE_TYPE_CPU", + "GGML_BACKEND_DEVICE_TYPE_GPU", + "GGML_STATUS_SUCCESS", +} + +REQUIRED_GGML_FUNCTIONS = { + "ggml_add", "ggml_backend_alloc_ctx_tensors", + "ggml_backend_buffer_free", "ggml_backend_dev_init", + "ggml_backend_dev_type", "ggml_backend_free", + "ggml_backend_graph_compute", "ggml_backend_reg_by_name", + "ggml_backend_reg_dev_count", "ggml_backend_reg_dev_get", + "ggml_backend_reg_get_proc_address", "ggml_backend_synchronize", + "ggml_backend_tensor_get", "ggml_backend_tensor_set", + "ggml_build_forward_expand", "ggml_free", "ggml_init", + "ggml_new_graph", "ggml_new_tensor_1d", +} + +GGML_ENUM_TYPES = {"ggml_log_level", "ggml_numa_strategy", + "ggml_opt_optimizer_type", "ggml_type"} +DEFAULT_OUTPUT_DIR = Path(__file__).resolve().parent / "module" / "gen_exports" + + +def _find_clang(): + for candidate in [os.environ.get("CLANG", ""), "clang++-22", + "clang++-20", "clang++-19", "clang++"]: + if not candidate: + continue + r = subprocess.run(["which", candidate], capture_output=True, text=True) + if r.returncode == 0: + return r.stdout.strip() + raise RuntimeError("clang++ not found") + + +def generate_exports(upstream_dir, include_dirs=None): + if include_dirs is None: + include_dirs = [os.path.join(upstream_dir, "include"), + os.path.join(upstream_dir, "ggml", "include")] + inc_flags = " ".join(f"-I{p}" for p in include_dirs) + clang = _find_clang() + + # Find header paths + llama_h = None + ggml_h = None + for d in include_dirs: + for name, var in [("llama.h", "llama_h"), ("ggml.h", "ggml_h")]: + p = os.path.join(d, name) + if os.path.isfile(p) and not locals()[var]: + if var == "llama_h": + llama_h = p + else: + ggml_h = p + assert llama_h, "llama.h not found" + + # Read source lines + with open(llama_h) as f: + llama_lines = f.readlines() + + # Create probe and run Clang AST dump + probe = tempfile.NamedTemporaryFile(suffix=".cpp", mode="w", delete=False) + probe.write( + '#include "llama.h"\n' + '#include "ggml.h"\n' + '#include "ggml-backend.h"\n' + '#include "ggml-alloc.h"\n' + ) + probe.close() + try: + cmd = [clang, "-std=c++20", "-fsyntax-only", + "-Xclang", "-ast-dump=json"] + inc_flags.split() + [probe.name] + r = subprocess.run(cmd, capture_output=True, text=True) + if r.returncode != 0: + raise RuntimeError(f"Clang AST dump failed:\n{r.stderr[:2000]}") + ast_data = json.loads(r.stdout) + finally: + os.unlink(probe.name) + + # Macro dump + macro_r = subprocess.run( + [clang, "-dM", "-E"] + inc_flags.split() + ["-"], + input='#include "llama.h"\n', capture_output=True, text=True) + if macro_r.returncode != 0: + raise RuntimeError(f"Clang macro dump failed:\n{macro_r.stderr[:2000]}") + + # Collect declarations. + llama_exports = [] + ggml_exports = [] + ggml_enum_members = set() + skipped = [] + + def _walk(node): + if not isinstance(node, dict): + return + kind = node.get("kind", "") + name = node.get("name", "") + loc = node.get("loc", {}) + fpath = "" + if isinstance(loc, dict): + fpath = loc.get("file", loc.get("spellingLoc", {}).get("file", "")) + + # Clang's JSON -ast-dump does not reliably include the source file name + # via `loc.file` or `loc.includedFrom.file`; it may point to the probe + # .cpp rather than the actual header. Use naming heuristics instead: + # FunctionDecl / RecordDecl / EnumDecl / EnumConstantDecl names that + # start with `llama_` (or `LLAMA_`) are from llama.h. + # Types prefixed `ggml_` (or `GGML_`) are from ggml.h. + in_llama = name.startswith("llama_") or name.startswith("LLAMA_") + # namespace-like prefixes that belong to ggml + in_ggml = name.startswith("ggml_") or name.startswith("GGML_") + + if kind in ("FunctionDecl", "CXXMethodDecl") and name in REQUIRED_GGML_FUNCTIONS: + ggml_exports.append(f"export using ::{name};") + + elif kind in ("FunctionDecl", "CXXMethodDecl") and in_llama and name.startswith("llama_"): + line_no = 0 + if isinstance(loc, dict): + line_no = loc.get("line", loc.get("spellingLoc", {}).get("line", 1)) + if 1 <= line_no <= len(llama_lines): + src_line = llama_lines[line_no - 1] + else: + src_line = "" + # Check for DeprecatedAttr + has_deprecated = any( + c.get("kind") == "DeprecatedAttr" + for c in node.get("inner", [])) + if has_deprecated or "DEPRECATED" in src_line: + skipped.append(f"deprecated function '{name}'") + elif "LLAMA_API" in src_line: + llama_exports.append(f"export using ::{name};") + # else: static/inline helpers, skip + + elif kind in ("RecordDecl", "CXXRecordDecl", "TypedefDecl", "ClassTemplateDecl"): + if in_llama and name.startswith("llama_"): + llama_exports.append(f"export using ::{name};") + elif in_ggml and name in REQUIRED_GGML_TYPES: + ggml_exports.append(f"export using ::{name};") + + elif kind == "EnumDecl": + if in_llama and name.startswith("llama_"): + llama_exports.append(f"export using ::{name};") + elif in_ggml and name in REQUIRED_GGML_TYPES: + ggml_exports.append(f"export using ::{name};") + if name in GGML_ENUM_TYPES: + for child in node.get("inner", []): + if child.get("kind") == "EnumConstantDecl": + cname = child.get("name", "") + if cname: + ggml_enum_members.add(cname) + + elif kind == "EnumConstantDecl": + if name in REQUIRED_GGML_ENUM_MEMBERS: + ggml_exports.append(f"export using ::{name};") + elif in_llama and name.startswith("LLAMA_"): + llama_exports.append(f"export using ::{name};") + + for child in node.get("inner", []): + _walk(child) + + if isinstance(ast_data, dict): + _walk(ast_data) + elif isinstance(ast_data, list): + for n in ast_data: + _walk(n) + + # Add GGML enumerator exports + for ename in sorted(ggml_enum_members): + ggml_exports.append(f"export using ::{ename};") + + # Collect LLAMA_ macros + for line in macro_r.stdout.splitlines(): + parts = line.split() + if len(parts) >= 2 and parts[0] == "#define" and parts[1].startswith("LLAMA_"): + skipped.append(f"macro {parts[1]}") + + # Deduplicate and sort + llama_exports = sorted(set(llama_exports), key=lambda x: x.rsplit("::", 1)[-1]) + ggml_exports = sorted(set(ggml_exports), key=lambda x: x.rsplit("::", 1)[-1]) + skipped = sorted(set(skipped)) + + exported_ggml = { + line.rsplit("::", 1)[-1].removesuffix(";") + for line in ggml_exports + } + missing_ggml = sorted( + (REQUIRED_GGML_TYPES + | REQUIRED_GGML_ENUM_MEMBERS + | REQUIRED_GGML_FUNCTIONS) + - exported_ggml + ) + if missing_ggml: + raise RuntimeError( + "required GGML API missing from Clang AST: " + + ", ".join(missing_ggml) + ) + + return ("\n".join(llama_exports) + "\n", + "\n".join(ggml_exports) + "\n", + "\n".join(skipped) + "\n") + + +def sync_outputs(output_dir, outputs, check): + output_dir = Path(output_dir) + if check: + for name, content in outputs.items(): + existing = output_dir / name + if not existing.exists(): + print(f"{name} does not exist", file=sys.stderr) + return 1 + if existing.read_text(encoding="utf-8") != content: + print(f"{name} differs", file=sys.stderr) + return 1 + print("All exports match.", file=sys.stderr) + return 0 + + output_dir.mkdir(parents=True, exist_ok=True) + for name, content in outputs.items(): + (output_dir / name).write_text(content, encoding="utf-8") + print(f"Wrote {name} ({len(content)} bytes)", file=sys.stderr) + return 0 + + +def main(argv=None): + ap = argparse.ArgumentParser() + ap.add_argument("--upstream", help="llama.cpp checkout dir") + ap.add_argument("--check", action="store_true") + ap.add_argument( + "--output-dir", + type=Path, + help="generated export directory (default: src/gen_exports)", + ) + args = ap.parse_args(argv) + + upstream = args.upstream + if not upstream: + repo_root = Path(__file__).resolve().parent.parent + fetch = repo_root / "tools" / "fetch_upstream.sh" + if fetch.exists(): + upstream = subprocess.run( + ["bash", str(fetch)], capture_output=True, text=True + ).stdout.strip() + if not upstream: + print("fetch_upstream.sh failed", file=sys.stderr) + return 1 + else: + print("No --upstream and no fetch_upstream.sh", file=sys.stderr) + return 1 + + llama_inc, ggml_inc, skipped_txt = generate_exports(upstream) + + outputs = { + "llama.inc": llama_inc, + "required_ggml.inc": ggml_inc, + "llama.skipped.txt": skipped_txt, + } + return sync_outputs(args.output_dir or DEFAULT_OUTPUT_DIR, outputs, args.check) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/llamacpp/module/gen_exports/llama.inc b/tools/llamacpp/module/gen_exports/llama.inc new file mode 100644 index 0000000..23fafd6 --- /dev/null +++ b/tools/llamacpp/module/gen_exports/llama.inc @@ -0,0 +1,352 @@ +export using ::LLAMA_ATTENTION_TYPE_CAUSAL; +export using ::LLAMA_ATTENTION_TYPE_NON_CAUSAL; +export using ::LLAMA_ATTENTION_TYPE_UNSPECIFIED; +export using ::LLAMA_CONTEXT_TYPE_DEFAULT; +export using ::LLAMA_CONTEXT_TYPE_MTP; +export using ::LLAMA_FLASH_ATTN_TYPE_AUTO; +export using ::LLAMA_FLASH_ATTN_TYPE_DISABLED; +export using ::LLAMA_FLASH_ATTN_TYPE_ENABLED; +export using ::LLAMA_FTYPE_ALL_F32; +export using ::LLAMA_FTYPE_GUESSED; +export using ::LLAMA_FTYPE_MOSTLY_BF16; +export using ::LLAMA_FTYPE_MOSTLY_F16; +export using ::LLAMA_FTYPE_MOSTLY_IQ1_M; +export using ::LLAMA_FTYPE_MOSTLY_IQ1_S; +export using ::LLAMA_FTYPE_MOSTLY_IQ2_M; +export using ::LLAMA_FTYPE_MOSTLY_IQ2_S; +export using ::LLAMA_FTYPE_MOSTLY_IQ2_XS; +export using ::LLAMA_FTYPE_MOSTLY_IQ2_XXS; +export using ::LLAMA_FTYPE_MOSTLY_IQ3_M; +export using ::LLAMA_FTYPE_MOSTLY_IQ3_S; +export using ::LLAMA_FTYPE_MOSTLY_IQ3_XS; +export using ::LLAMA_FTYPE_MOSTLY_IQ3_XXS; +export using ::LLAMA_FTYPE_MOSTLY_IQ4_NL; +export using ::LLAMA_FTYPE_MOSTLY_IQ4_XS; +export using ::LLAMA_FTYPE_MOSTLY_MXFP4_MOE; +export using ::LLAMA_FTYPE_MOSTLY_NVFP4; +export using ::LLAMA_FTYPE_MOSTLY_Q1_0; +export using ::LLAMA_FTYPE_MOSTLY_Q2_0; +export using ::LLAMA_FTYPE_MOSTLY_Q2_K; +export using ::LLAMA_FTYPE_MOSTLY_Q2_K_S; +export using ::LLAMA_FTYPE_MOSTLY_Q3_K_L; +export using ::LLAMA_FTYPE_MOSTLY_Q3_K_M; +export using ::LLAMA_FTYPE_MOSTLY_Q3_K_S; +export using ::LLAMA_FTYPE_MOSTLY_Q4_0; +export using ::LLAMA_FTYPE_MOSTLY_Q4_1; +export using ::LLAMA_FTYPE_MOSTLY_Q4_K_M; +export using ::LLAMA_FTYPE_MOSTLY_Q4_K_S; +export using ::LLAMA_FTYPE_MOSTLY_Q5_0; +export using ::LLAMA_FTYPE_MOSTLY_Q5_1; +export using ::LLAMA_FTYPE_MOSTLY_Q5_K_M; +export using ::LLAMA_FTYPE_MOSTLY_Q5_K_S; +export using ::LLAMA_FTYPE_MOSTLY_Q6_K; +export using ::LLAMA_FTYPE_MOSTLY_Q8_0; +export using ::LLAMA_FTYPE_MOSTLY_TQ1_0; +export using ::LLAMA_FTYPE_MOSTLY_TQ2_0; +export using ::LLAMA_KV_OVERRIDE_TYPE_BOOL; +export using ::LLAMA_KV_OVERRIDE_TYPE_FLOAT; +export using ::LLAMA_KV_OVERRIDE_TYPE_INT; +export using ::LLAMA_KV_OVERRIDE_TYPE_STR; +export using ::LLAMA_MODEL_META_KEY_SAMPLING_MIN_P; +export using ::LLAMA_MODEL_META_KEY_SAMPLING_MIROSTAT; +export using ::LLAMA_MODEL_META_KEY_SAMPLING_MIROSTAT_ETA; +export using ::LLAMA_MODEL_META_KEY_SAMPLING_MIROSTAT_TAU; +export using ::LLAMA_MODEL_META_KEY_SAMPLING_PENALTY_LAST_N; +export using ::LLAMA_MODEL_META_KEY_SAMPLING_PENALTY_REPEAT; +export using ::LLAMA_MODEL_META_KEY_SAMPLING_SEQUENCE; +export using ::LLAMA_MODEL_META_KEY_SAMPLING_TEMP; +export using ::LLAMA_MODEL_META_KEY_SAMPLING_TOP_K; +export using ::LLAMA_MODEL_META_KEY_SAMPLING_TOP_P; +export using ::LLAMA_MODEL_META_KEY_SAMPLING_XTC_PROBABILITY; +export using ::LLAMA_MODEL_META_KEY_SAMPLING_XTC_THRESHOLD; +export using ::LLAMA_POOLING_TYPE_CLS; +export using ::LLAMA_POOLING_TYPE_LAST; +export using ::LLAMA_POOLING_TYPE_MEAN; +export using ::LLAMA_POOLING_TYPE_NONE; +export using ::LLAMA_POOLING_TYPE_RANK; +export using ::LLAMA_POOLING_TYPE_UNSPECIFIED; +export using ::LLAMA_ROPE_SCALING_TYPE_LINEAR; +export using ::LLAMA_ROPE_SCALING_TYPE_LONGROPE; +export using ::LLAMA_ROPE_SCALING_TYPE_MAX_VALUE; +export using ::LLAMA_ROPE_SCALING_TYPE_NONE; +export using ::LLAMA_ROPE_SCALING_TYPE_UNSPECIFIED; +export using ::LLAMA_ROPE_SCALING_TYPE_YARN; +export using ::LLAMA_ROPE_TYPE_IMROPE; +export using ::LLAMA_ROPE_TYPE_MROPE; +export using ::LLAMA_ROPE_TYPE_NEOX; +export using ::LLAMA_ROPE_TYPE_NONE; +export using ::LLAMA_ROPE_TYPE_NORM; +export using ::LLAMA_ROPE_TYPE_VISION; +export using ::LLAMA_SPLIT_MODE_LAYER; +export using ::LLAMA_SPLIT_MODE_NONE; +export using ::LLAMA_SPLIT_MODE_ROW; +export using ::LLAMA_SPLIT_MODE_TENSOR; +export using ::LLAMA_TOKEN_ATTR_BYTE; +export using ::LLAMA_TOKEN_ATTR_CONTROL; +export using ::LLAMA_TOKEN_ATTR_LSTRIP; +export using ::LLAMA_TOKEN_ATTR_NORMAL; +export using ::LLAMA_TOKEN_ATTR_NORMALIZED; +export using ::LLAMA_TOKEN_ATTR_RSTRIP; +export using ::LLAMA_TOKEN_ATTR_SINGLE_WORD; +export using ::LLAMA_TOKEN_ATTR_UNDEFINED; +export using ::LLAMA_TOKEN_ATTR_UNKNOWN; +export using ::LLAMA_TOKEN_ATTR_UNUSED; +export using ::LLAMA_TOKEN_ATTR_USER_DEFINED; +export using ::LLAMA_TOKEN_TYPE_BYTE; +export using ::LLAMA_TOKEN_TYPE_CONTROL; +export using ::LLAMA_TOKEN_TYPE_NORMAL; +export using ::LLAMA_TOKEN_TYPE_UNDEFINED; +export using ::LLAMA_TOKEN_TYPE_UNKNOWN; +export using ::LLAMA_TOKEN_TYPE_UNUSED; +export using ::LLAMA_TOKEN_TYPE_USER_DEFINED; +export using ::LLAMA_VOCAB_TYPE_BPE; +export using ::LLAMA_VOCAB_TYPE_NONE; +export using ::LLAMA_VOCAB_TYPE_PLAMO2; +export using ::LLAMA_VOCAB_TYPE_RWKV; +export using ::LLAMA_VOCAB_TYPE_SPM; +export using ::LLAMA_VOCAB_TYPE_UGM; +export using ::LLAMA_VOCAB_TYPE_WPM; +export using ::llama_adapter_get_alora_invocation_tokens; +export using ::llama_adapter_get_alora_n_invocation_tokens; +export using ::llama_adapter_lora; +export using ::llama_adapter_lora_free; +export using ::llama_adapter_lora_init; +export using ::llama_adapter_meta_count; +export using ::llama_adapter_meta_key_by_index; +export using ::llama_adapter_meta_val_str; +export using ::llama_adapter_meta_val_str_by_index; +export using ::llama_attach_threadpool; +export using ::llama_attention_type; +export using ::llama_backend_free; +export using ::llama_backend_init; +export using ::llama_batch; +export using ::llama_batch_free; +export using ::llama_batch_get_one; +export using ::llama_batch_init; +export using ::llama_chat_apply_template; +export using ::llama_chat_builtin_templates; +export using ::llama_chat_message; +export using ::llama_context; +export using ::llama_context_default_params; +export using ::llama_context_params; +export using ::llama_context_type; +export using ::llama_decode; +export using ::llama_detach_threadpool; +export using ::llama_detokenize; +export using ::llama_encode; +export using ::llama_flash_attn_type; +export using ::llama_flash_attn_type_name; +export using ::llama_free; +export using ::llama_ftype; +export using ::llama_ftype_name; +export using ::llama_get_embeddings; +export using ::llama_get_embeddings_ith; +export using ::llama_get_embeddings_seq; +export using ::llama_get_logits; +export using ::llama_get_logits_ith; +export using ::llama_get_memory; +export using ::llama_get_model; +export using ::llama_get_sampled_candidates_count_ith; +export using ::llama_get_sampled_candidates_ith; +export using ::llama_get_sampled_logits_count_ith; +export using ::llama_get_sampled_logits_ith; +export using ::llama_get_sampled_probs_count_ith; +export using ::llama_get_sampled_probs_ith; +export using ::llama_get_sampled_token_ith; +export using ::llama_init_from_model; +export using ::llama_log_get; +export using ::llama_log_set; +export using ::llama_logit_bias; +export using ::llama_max_devices; +export using ::llama_max_parallel_sequences; +export using ::llama_max_tensor_buft_overrides; +export using ::llama_memory_can_shift; +export using ::llama_memory_clear; +export using ::llama_memory_i; +export using ::llama_memory_seq_add; +export using ::llama_memory_seq_cp; +export using ::llama_memory_seq_div; +export using ::llama_memory_seq_keep; +export using ::llama_memory_seq_pos_max; +export using ::llama_memory_seq_pos_min; +export using ::llama_memory_seq_rm; +export using ::llama_memory_t; +export using ::llama_model; +export using ::llama_model_chat_template; +export using ::llama_model_cls_label; +export using ::llama_model_decoder_start_token; +export using ::llama_model_default_params; +export using ::llama_model_desc; +export using ::llama_model_free; +export using ::llama_model_ftype; +export using ::llama_model_get_vocab; +export using ::llama_model_has_decoder; +export using ::llama_model_has_encoder; +export using ::llama_model_imatrix_data; +export using ::llama_model_init_from_user; +export using ::llama_model_is_diffusion; +export using ::llama_model_is_hybrid; +export using ::llama_model_is_recurrent; +export using ::llama_model_kv_override; +export using ::llama_model_kv_override_type; +export using ::llama_model_load_from_file; +export using ::llama_model_load_from_file_ptr; +export using ::llama_model_load_from_splits; +export using ::llama_model_meta_count; +export using ::llama_model_meta_key; +export using ::llama_model_meta_key_by_index; +export using ::llama_model_meta_key_str; +export using ::llama_model_meta_val_str; +export using ::llama_model_meta_val_str_by_index; +export using ::llama_model_n_cls_out; +export using ::llama_model_n_ctx_train; +export using ::llama_model_n_embd; +export using ::llama_model_n_embd_inp; +export using ::llama_model_n_embd_out; +export using ::llama_model_n_head; +export using ::llama_model_n_head_kv; +export using ::llama_model_n_layer; +export using ::llama_model_n_layer_nextn; +export using ::llama_model_n_params; +export using ::llama_model_n_swa; +export using ::llama_model_params; +export using ::llama_model_quantize; +export using ::llama_model_quantize_default_params; +export using ::llama_model_quantize_params; +export using ::llama_model_rope_freq_scale_train; +export using ::llama_model_rope_type; +export using ::llama_model_save_to_file; +export using ::llama_model_set_tensor_data_t; +export using ::llama_model_size; +export using ::llama_model_tensor_buft_override; +export using ::llama_model_tensor_override; +export using ::llama_n_batch; +export using ::llama_n_ctx; +export using ::llama_n_ctx_seq; +export using ::llama_n_rs_seq; +export using ::llama_n_seq_max; +export using ::llama_n_threads; +export using ::llama_n_threads_batch; +export using ::llama_n_ubatch; +export using ::llama_numa_init; +export using ::llama_opt_epoch; +export using ::llama_opt_init; +export using ::llama_opt_param_filter; +export using ::llama_opt_param_filter_all; +export using ::llama_opt_params; +export using ::llama_perf_context; +export using ::llama_perf_context_data; +export using ::llama_perf_context_print; +export using ::llama_perf_context_reset; +export using ::llama_perf_sampler; +export using ::llama_perf_sampler_data; +export using ::llama_perf_sampler_print; +export using ::llama_perf_sampler_reset; +export using ::llama_pooling_type; +export using ::llama_pos; +export using ::llama_print_system_info; +export using ::llama_progress_callback; +export using ::llama_rope_scaling_type; +export using ::llama_rope_type; +export using ::llama_sampler; +export using ::llama_sampler_accept; +export using ::llama_sampler_apply; +export using ::llama_sampler_chain_add; +export using ::llama_sampler_chain_default_params; +export using ::llama_sampler_chain_get; +export using ::llama_sampler_chain_init; +export using ::llama_sampler_chain_n; +export using ::llama_sampler_chain_params; +export using ::llama_sampler_chain_remove; +export using ::llama_sampler_clone; +export using ::llama_sampler_context_t; +export using ::llama_sampler_data; +export using ::llama_sampler_free; +export using ::llama_sampler_get_seed; +export using ::llama_sampler_i; +export using ::llama_sampler_init; +export using ::llama_sampler_init_adaptive_p; +export using ::llama_sampler_init_dist; +export using ::llama_sampler_init_dry; +export using ::llama_sampler_init_grammar; +export using ::llama_sampler_init_grammar_lazy_patterns; +export using ::llama_sampler_init_greedy; +export using ::llama_sampler_init_infill; +export using ::llama_sampler_init_logit_bias; +export using ::llama_sampler_init_min_p; +export using ::llama_sampler_init_mirostat; +export using ::llama_sampler_init_mirostat_v2; +export using ::llama_sampler_init_penalties; +export using ::llama_sampler_init_temp; +export using ::llama_sampler_init_temp_ext; +export using ::llama_sampler_init_top_k; +export using ::llama_sampler_init_top_n_sigma; +export using ::llama_sampler_init_top_p; +export using ::llama_sampler_init_typical; +export using ::llama_sampler_init_xtc; +export using ::llama_sampler_name; +export using ::llama_sampler_reset; +export using ::llama_sampler_sample; +export using ::llama_sampler_seq_config; +export using ::llama_seq_id; +export using ::llama_set_abort_callback; +export using ::llama_set_adapter_cvec; +export using ::llama_set_adapters_lora; +export using ::llama_set_causal_attn; +export using ::llama_set_embeddings; +export using ::llama_set_n_threads; +export using ::llama_set_sampler; +export using ::llama_split_mode; +export using ::llama_split_path; +export using ::llama_split_prefix; +export using ::llama_state_get_data; +export using ::llama_state_get_size; +export using ::llama_state_load_file; +export using ::llama_state_save_file; +export using ::llama_state_seq_flags; +export using ::llama_state_seq_get_data; +export using ::llama_state_seq_get_data_ext; +export using ::llama_state_seq_get_size; +export using ::llama_state_seq_get_size_ext; +export using ::llama_state_seq_load_file; +export using ::llama_state_seq_save_file; +export using ::llama_state_seq_set_data; +export using ::llama_state_seq_set_data_ext; +export using ::llama_state_set_data; +export using ::llama_supports_gpu_offload; +export using ::llama_supports_mlock; +export using ::llama_supports_mmap; +export using ::llama_supports_rpc; +export using ::llama_synchronize; +export using ::llama_time_us; +export using ::llama_token; +export using ::llama_token_attr; +export using ::llama_token_data; +export using ::llama_token_data_array; +export using ::llama_token_to_piece; +export using ::llama_token_type; +export using ::llama_tokenize; +export using ::llama_vocab; +export using ::llama_vocab_bos; +export using ::llama_vocab_eos; +export using ::llama_vocab_eot; +export using ::llama_vocab_fim_mid; +export using ::llama_vocab_fim_pad; +export using ::llama_vocab_fim_pre; +export using ::llama_vocab_fim_rep; +export using ::llama_vocab_fim_sep; +export using ::llama_vocab_fim_suf; +export using ::llama_vocab_get_add_bos; +export using ::llama_vocab_get_add_eos; +export using ::llama_vocab_get_add_sep; +export using ::llama_vocab_get_attr; +export using ::llama_vocab_get_score; +export using ::llama_vocab_get_text; +export using ::llama_vocab_is_control; +export using ::llama_vocab_is_eog; +export using ::llama_vocab_mask; +export using ::llama_vocab_n_tokens; +export using ::llama_vocab_nl; +export using ::llama_vocab_pad; +export using ::llama_vocab_sep; +export using ::llama_vocab_type; diff --git a/tools/llamacpp/module/gen_exports/llama.skipped.txt b/tools/llamacpp/module/gen_exports/llama.skipped.txt new file mode 100644 index 0000000..7e725d2 --- /dev/null +++ b/tools/llamacpp/module/gen_exports/llama.skipped.txt @@ -0,0 +1,51 @@ +deprecated function 'llama_add_bos_token' +deprecated function 'llama_add_eos_token' +deprecated function 'llama_copy_state_data' +deprecated function 'llama_free_model' +deprecated function 'llama_get_state_size' +deprecated function 'llama_load_model_from_file' +deprecated function 'llama_load_session_file' +deprecated function 'llama_n_ctx_train' +deprecated function 'llama_n_embd' +deprecated function 'llama_n_head' +deprecated function 'llama_n_layer' +deprecated function 'llama_n_vocab' +deprecated function 'llama_new_context_with_model' +deprecated function 'llama_sampler_init_grammar_lazy' +deprecated function 'llama_save_session_file' +deprecated function 'llama_set_state_data' +deprecated function 'llama_set_warmup' +deprecated function 'llama_token_bos' +deprecated function 'llama_token_cls' +deprecated function 'llama_token_eos' +deprecated function 'llama_token_eot' +deprecated function 'llama_token_fim_mid' +deprecated function 'llama_token_fim_pad' +deprecated function 'llama_token_fim_pre' +deprecated function 'llama_token_fim_rep' +deprecated function 'llama_token_fim_sep' +deprecated function 'llama_token_fim_suf' +deprecated function 'llama_token_get_attr' +deprecated function 'llama_token_get_score' +deprecated function 'llama_token_get_text' +deprecated function 'llama_token_is_control' +deprecated function 'llama_token_is_eog' +deprecated function 'llama_token_nl' +deprecated function 'llama_token_pad' +deprecated function 'llama_token_sep' +deprecated function 'llama_vocab_cls' +macro LLAMA_API +macro LLAMA_DEFAULT_SEED +macro LLAMA_FILE_MAGIC_GGLA +macro LLAMA_FILE_MAGIC_GGSN +macro LLAMA_FILE_MAGIC_GGSQ +macro LLAMA_H +macro LLAMA_SESSION_MAGIC +macro LLAMA_SESSION_VERSION +macro LLAMA_STATE_SEQ_FLAGS_NONE +macro LLAMA_STATE_SEQ_FLAGS_ON_DEVICE +macro LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY +macro LLAMA_STATE_SEQ_FLAGS_SWA_ONLY +macro LLAMA_STATE_SEQ_MAGIC +macro LLAMA_STATE_SEQ_VERSION +macro LLAMA_TOKEN_NULL diff --git a/tools/llamacpp/module/gen_exports/required_ggml.inc b/tools/llamacpp/module/gen_exports/required_ggml.inc new file mode 100644 index 0000000..1d66f2e --- /dev/null +++ b/tools/llamacpp/module/gen_exports/required_ggml.inc @@ -0,0 +1,97 @@ +export using ::GGML_BACKEND_DEVICE_TYPE_ACCEL; +export using ::GGML_BACKEND_DEVICE_TYPE_CPU; +export using ::GGML_BACKEND_DEVICE_TYPE_GPU; +export using ::GGML_LOG_LEVEL_CONT; +export using ::GGML_LOG_LEVEL_DEBUG; +export using ::GGML_LOG_LEVEL_ERROR; +export using ::GGML_LOG_LEVEL_INFO; +export using ::GGML_LOG_LEVEL_NONE; +export using ::GGML_LOG_LEVEL_WARN; +export using ::GGML_NUMA_STRATEGY_COUNT; +export using ::GGML_NUMA_STRATEGY_DISABLED; +export using ::GGML_NUMA_STRATEGY_DISTRIBUTE; +export using ::GGML_NUMA_STRATEGY_ISOLATE; +export using ::GGML_NUMA_STRATEGY_MIRROR; +export using ::GGML_NUMA_STRATEGY_NUMACTL; +export using ::GGML_OPT_OPTIMIZER_TYPE_ADAMW; +export using ::GGML_OPT_OPTIMIZER_TYPE_COUNT; +export using ::GGML_OPT_OPTIMIZER_TYPE_SGD; +export using ::GGML_STATUS_SUCCESS; +export using ::GGML_TYPE_BF16; +export using ::GGML_TYPE_COUNT; +export using ::GGML_TYPE_F16; +export using ::GGML_TYPE_F32; +export using ::GGML_TYPE_F64; +export using ::GGML_TYPE_I16; +export using ::GGML_TYPE_I32; +export using ::GGML_TYPE_I64; +export using ::GGML_TYPE_I8; +export using ::GGML_TYPE_IQ1_M; +export using ::GGML_TYPE_IQ1_S; +export using ::GGML_TYPE_IQ2_S; +export using ::GGML_TYPE_IQ2_XS; +export using ::GGML_TYPE_IQ2_XXS; +export using ::GGML_TYPE_IQ3_S; +export using ::GGML_TYPE_IQ3_XXS; +export using ::GGML_TYPE_IQ4_NL; +export using ::GGML_TYPE_IQ4_XS; +export using ::GGML_TYPE_MXFP4; +export using ::GGML_TYPE_NVFP4; +export using ::GGML_TYPE_Q1_0; +export using ::GGML_TYPE_Q2_0; +export using ::GGML_TYPE_Q2_K; +export using ::GGML_TYPE_Q3_K; +export using ::GGML_TYPE_Q4_0; +export using ::GGML_TYPE_Q4_1; +export using ::GGML_TYPE_Q4_K; +export using ::GGML_TYPE_Q5_0; +export using ::GGML_TYPE_Q5_1; +export using ::GGML_TYPE_Q5_K; +export using ::GGML_TYPE_Q6_K; +export using ::GGML_TYPE_Q8_0; +export using ::GGML_TYPE_Q8_1; +export using ::GGML_TYPE_Q8_K; +export using ::GGML_TYPE_TQ1_0; +export using ::GGML_TYPE_TQ2_0; +export using ::ggml_abort_callback; +export using ::ggml_add; +export using ::ggml_backend_alloc_ctx_tensors; +export using ::ggml_backend_buffer_free; +export using ::ggml_backend_buffer_t; +export using ::ggml_backend_buffer_type_t; +export using ::ggml_backend_dev_init; +export using ::ggml_backend_dev_t; +export using ::ggml_backend_dev_type; +export using ::ggml_backend_free; +export using ::ggml_backend_get_features_t; +export using ::ggml_backend_graph_compute; +export using ::ggml_backend_reg_by_name; +export using ::ggml_backend_reg_dev_count; +export using ::ggml_backend_reg_dev_get; +export using ::ggml_backend_reg_get_proc_address; +export using ::ggml_backend_reg_t; +export using ::ggml_backend_sched_eval_callback; +export using ::ggml_backend_synchronize; +export using ::ggml_backend_t; +export using ::ggml_backend_tensor_get; +export using ::ggml_backend_tensor_set; +export using ::ggml_build_forward_expand; +export using ::ggml_cgraph; +export using ::ggml_context; +export using ::ggml_free; +export using ::ggml_init; +export using ::ggml_init_params; +export using ::ggml_log_callback; +export using ::ggml_log_level; +export using ::ggml_new_graph; +export using ::ggml_new_tensor_1d; +export using ::ggml_numa_strategy; +export using ::ggml_opt_dataset_t; +export using ::ggml_opt_epoch_callback; +export using ::ggml_opt_get_optimizer_params; +export using ::ggml_opt_optimizer_type; +export using ::ggml_opt_result_t; +export using ::ggml_status; +export using ::ggml_tensor; +export using ::ggml_threadpool_t; +export using ::ggml_type; diff --git a/tools/llamacpp/module/llama.cppm b/tools/llamacpp/module/llama.cppm new file mode 100644 index 0000000..1960745 --- /dev/null +++ b/tools/llamacpp/module/llama.cppm @@ -0,0 +1,42 @@ +module; + +#include +#include +#include + +#undef LLAMA_DEFAULT_SEED +#undef LLAMA_TOKEN_NULL +#undef LLAMA_FILE_MAGIC_GGLA +#undef LLAMA_FILE_MAGIC_GGSN +#undef LLAMA_FILE_MAGIC_GGSQ +#undef LLAMA_SESSION_MAGIC +#undef LLAMA_SESSION_VERSION +#undef LLAMA_STATE_SEQ_MAGIC +#undef LLAMA_STATE_SEQ_VERSION +#undef LLAMA_STATE_SEQ_FLAGS_NONE +#undef LLAMA_STATE_SEQ_FLAGS_SWA_ONLY +#undef LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY +#undef LLAMA_STATE_SEQ_FLAGS_ON_DEVICE + +export module llama; + +#include "gen_exports/required_ggml.inc" +#include "gen_exports/llama.inc" + +// Typed constant replacements for public preprocessor macros. +// uint32_t and llama_token are provided by (global module fragment). +export inline constexpr uint32_t LLAMA_DEFAULT_SEED = 0xFFFFFFFFu; +export inline constexpr llama_token LLAMA_TOKEN_NULL = -1; + +export inline constexpr uint32_t LLAMA_FILE_MAGIC_GGLA = 0x67676c61u; +export inline constexpr uint32_t LLAMA_FILE_MAGIC_GGSN = 0x6767736eu; +export inline constexpr uint32_t LLAMA_FILE_MAGIC_GGSQ = 0x67677371u; +export inline constexpr uint32_t LLAMA_SESSION_MAGIC = 0x6767736eu; +export inline constexpr uint32_t LLAMA_SESSION_VERSION = 9u; +export inline constexpr uint32_t LLAMA_STATE_SEQ_MAGIC = 0x67677371u; +export inline constexpr uint32_t LLAMA_STATE_SEQ_VERSION = 2u; + +export inline constexpr llama_state_seq_flags LLAMA_STATE_SEQ_FLAGS_NONE = 0u; +export inline constexpr llama_state_seq_flags LLAMA_STATE_SEQ_FLAGS_SWA_ONLY = 1u; +export inline constexpr llama_state_seq_flags LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY = 1u; +export inline constexpr llama_state_seq_flags LLAMA_STATE_SEQ_FLAGS_ON_DEVICE = 2u; diff --git a/tools/llamacpp/snapshots/b10069.json b/tools/llamacpp/snapshots/b10069.json new file mode 100644 index 0000000..8df8c68 --- /dev/null +++ b/tools/llamacpp/snapshots/b10069.json @@ -0,0 +1,275 @@ +{ + "schema": 1, + "upstream": { + "tag": "b10069", + "commit": "178a6c44937154dc4c4eff0d166f4a044c4fceba", + "url": "https://github.com/ggml-org/llama.cpp/archive/refs/tags/b10069.tar.gz", + "sha256": "293a7c65a11e2203c5468a06d0d0e8d21dfff16ad08712b16c61efbe0d93e097" + }, + "sources": { + "ggml_base": [ + "ggml/src/../include/ggml-alloc.h", + "ggml/src/../include/ggml-backend.h", + "ggml/src/../include/ggml-cpp.h", + "ggml/src/../include/ggml-opt.h", + "ggml/src/../include/ggml.h", + "ggml/src/../include/gguf.h", + "ggml/src/ggml-alloc.c", + "ggml/src/ggml-backend-meta.cpp", + "ggml/src/ggml-backend.cpp", + "ggml/src/ggml-opt.cpp", + "ggml/src/ggml-quants.c", + "ggml/src/ggml-quants.h", + "ggml/src/ggml-threading.cpp", + "ggml/src/ggml-threading.h", + "ggml/src/ggml.c", + "ggml/src/ggml.cpp", + "ggml/src/gguf.cpp" + ], + "ggml_registry": [ + "ggml/src/ggml-backend-dl.cpp", + "ggml/src/ggml-backend-reg.cpp" + ], + "ggml_cpu_common": [ + "ggml/src/ggml-cpu/amx/amx.cpp", + "ggml/src/ggml-cpu/amx/mmq.cpp", + "ggml/src/ggml-cpu/binary-ops.cpp", + "ggml/src/ggml-cpu/ggml-cpu.c", + "ggml/src/ggml-cpu/ggml-cpu.cpp", + "ggml/src/ggml-cpu/hbm.cpp", + "ggml/src/ggml-cpu/llamafile/sgemm.cpp", + "ggml/src/ggml-cpu/ops.cpp", + "ggml/src/ggml-cpu/quants.c", + "ggml/src/ggml-cpu/repack.cpp", + "ggml/src/ggml-cpu/traits.cpp", + "ggml/src/ggml-cpu/unary-ops.cpp", + "ggml/src/ggml-cpu/vec.cpp" + ], + "ggml_cpu_x86": [ + "ggml/src/ggml-cpu/arch/x86/quants.c", + "ggml/src/ggml-cpu/arch/x86/repack.cpp" + ], + "ggml_cpu_arm": [ + "ggml/src/ggml-cpu/arch/arm/quants.c", + "ggml/src/ggml-cpu/arch/arm/repack.cpp" + ], + "llama_core": [ + "src/llama-adapter.cpp", + "src/llama-arch.cpp", + "src/llama-batch.cpp", + "src/llama-chat.cpp", + "src/llama-context.cpp", + "src/llama-cparams.cpp", + "src/llama-grammar.cpp", + "src/llama-graph.cpp", + "src/llama-hparams.cpp", + "src/llama-impl.cpp", + "src/llama-io.cpp", + "src/llama-kv-cache-dsa.cpp", + "src/llama-kv-cache-dsv4.cpp", + "src/llama-kv-cache-iswa.cpp", + "src/llama-kv-cache.cpp", + "src/llama-memory-hybrid-iswa.cpp", + "src/llama-memory-hybrid.cpp", + "src/llama-memory-recurrent.cpp", + "src/llama-memory.cpp", + "src/llama-mmap.cpp", + "src/llama-model-loader.cpp", + "src/llama-model-saver.cpp", + "src/llama-model.cpp", + "src/llama-quant.cpp", + "src/llama-sampler.cpp", + "src/llama-vocab.cpp", + "src/llama.cpp", + "src/unicode-data.cpp", + "src/unicode.cpp" + ], + "models": [ + "models/afmoe.cpp", + "models/apertus.cpp", + "models/arcee.cpp", + "models/arctic.cpp", + "models/arwkv7.cpp", + "models/baichuan.cpp", + "models/bailingmoe.cpp", + "models/bailingmoe2.cpp", + "models/bert.cpp", + "models/bitnet.cpp", + "models/bloom.cpp", + "models/chameleon.cpp", + "models/chatglm.cpp", + "models/codeshell.cpp", + "models/cogvlm.cpp", + "models/cohere2.cpp", + "models/cohere2moe.cpp", + "models/command-r.cpp", + "models/dbrx.cpp", + "models/deci.cpp", + "models/deepseek.cpp", + "models/deepseek2.cpp", + "models/deepseek2ocr.cpp", + "models/deepseek32.cpp", + "models/deepseek4.cpp", + "models/delta-net-base.cpp", + "models/dflash.cpp", + "models/dots1.cpp", + "models/dream.cpp", + "models/eagle3.cpp", + "models/ernie4-5-moe.cpp", + "models/ernie4-5.cpp", + "models/eurobert.cpp", + "models/exaone-moe.cpp", + "models/exaone.cpp", + "models/exaone4.cpp", + "models/falcon-h1.cpp", + "models/falcon.cpp", + "models/gemma-embedding.cpp", + "models/gemma.cpp", + "models/gemma2.cpp", + "models/gemma3.cpp", + "models/gemma3n.cpp", + "models/gemma4-assistant.cpp", + "models/gemma4.cpp", + "models/glm-dsa.cpp", + "models/glm4-moe.cpp", + "models/glm4.cpp", + "models/gpt2.cpp", + "models/gptneox.cpp", + "models/granite-hybrid.cpp", + "models/granite-moe.cpp", + "models/granite.cpp", + "models/grok.cpp", + "models/grovemoe.cpp", + "models/hunyuan-dense.cpp", + "models/hunyuan-moe.cpp", + "models/hunyuan-vl.cpp", + "models/hy-v3.cpp", + "models/internlm2.cpp", + "models/jais.cpp", + "models/jais2.cpp", + "models/jamba.cpp", + "models/jina-bert-v2.cpp", + "models/jina-bert-v3.cpp", + "models/kimi-linear.cpp", + "models/lfm2.cpp", + "models/lfm2moe.cpp", + "models/llada-moe.cpp", + "models/llada.cpp", + "models/llama-embed.cpp", + "models/llama.cpp", + "models/llama4.cpp", + "models/maincoder.cpp", + "models/mamba-base.cpp", + "models/mamba.cpp", + "models/mamba2.cpp", + "models/mellum.cpp", + "models/mimo2.cpp", + "models/minicpm.cpp", + "models/minicpm3.cpp", + "models/minimax-m2.cpp", + "models/mistral3.cpp", + "models/mistral4.cpp", + "models/modern-bert.cpp", + "models/mpt.cpp", + "models/nemotron-h-moe.cpp", + "models/nemotron-h.cpp", + "models/nemotron.cpp", + "models/neo-bert.cpp", + "models/nomic-bert-moe.cpp", + "models/nomic-bert.cpp", + "models/olmo.cpp", + "models/olmo2.cpp", + "models/olmoe.cpp", + "models/openai-moe.cpp", + "models/openelm.cpp", + "models/orion.cpp", + "models/paddleocr.cpp", + "models/pangu-embed.cpp", + "models/phi2.cpp", + "models/phi3.cpp", + "models/phimoe.cpp", + "models/plamo.cpp", + "models/plamo2.cpp", + "models/plamo3.cpp", + "models/plm.cpp", + "models/qwen.cpp", + "models/qwen2.cpp", + "models/qwen2moe.cpp", + "models/qwen2vl.cpp", + "models/qwen3.cpp", + "models/qwen35.cpp", + "models/qwen35moe.cpp", + "models/qwen3moe.cpp", + "models/qwen3next.cpp", + "models/qwen3vl.cpp", + "models/qwen3vlmoe.cpp", + "models/refact.cpp", + "models/rnd1.cpp", + "models/rwkv6-base.cpp", + "models/rwkv6.cpp", + "models/rwkv6qwen2.cpp", + "models/rwkv7-base.cpp", + "models/rwkv7.cpp", + "models/seed-oss.cpp", + "models/smallthinker.cpp", + "models/smollm3.cpp", + "models/stablelm.cpp", + "models/starcoder.cpp", + "models/starcoder2.cpp", + "models/step35.cpp", + "models/t5.cpp", + "models/t5encoder.cpp", + "models/talkie.cpp", + "models/wavtokenizer-dec.cpp", + "models/xverse.cpp" + ], + "ggml_metal": [ + "ggml/src/ggml-metal/ggml-metal-common.cpp", + "ggml/src/ggml-metal/ggml-metal-context.m", + "ggml/src/ggml-metal/ggml-metal-device.cpp", + "ggml/src/ggml-metal/ggml-metal-device.m", + "ggml/src/ggml-metal/ggml-metal-ops.cpp", + "ggml/src/ggml-metal/ggml-metal.cpp" + ] + }, + "registry": { + "GGML_USE_CPU": "ggml_backend_cpu_reg", + "GGML_USE_METAL": "ggml_backend_metal_reg", + "GGML_USE_CUDA": "ggml_backend_cuda_reg", + "GGML_USE_VULKAN": "ggml_backend_vk_reg", + "GGML_USE_SYCL": "ggml_backend_sycl_reg", + "GGML_USE_CANN": "ggml_backend_cann_reg" + }, + "metal": { + "frameworks": [ + "Foundation", + "Metal", + "MetalKit" + ], + "shader_inputs": [ + "ggml/src/ggml-common.h", + "ggml/src/ggml-metal/ggml-metal-impl.h", + "ggml/src/ggml-metal/ggml-metal.metal" + ] + }, + "dialect_exceptions": { + "c++20": [ + "src/models/dflash.cpp", + "src/models/eagle3.cpp", + "src/models/hunyuan-dense.cpp", + "src/models/llama-embed.cpp", + "src/models/minimax-m2.cpp", + "src/models/t5.cpp" + ] + }, + "platform_links": { + "windows_cpu": [ + "advapi32" + ] + }, + "public_header_sha256": { + "include/llama.h": "2331631b6a3567311abc0402c55aa9a867ee99759f2550bdfa261ec3693a21f6", + "ggml/include/ggml.h": "c65c30fdb4dce95eac71c26bb38ae8423fbc80d79db91d2b2ffaea8c4e46276a", + "ggml/include/ggml-cpu.h": "316279e004cdeb8e6ef78599acb602bf79a8abdf897fed9fd1914808c1518c6e" + } +} diff --git a/tools/llamacpp/test_audit_snapshot.py b/tools/llamacpp/test_audit_snapshot.py new file mode 100644 index 0000000..9df9aa8 --- /dev/null +++ b/tools/llamacpp/test_audit_snapshot.py @@ -0,0 +1,402 @@ +"""Unit tests for audit_snapshot.py against a miniature CMake tree.""" +import contextlib +import io +import json +import os +import sys +import tarfile +import tempfile +import unittest +import urllib.request +from pathlib import Path +from unittest import mock + +# Ensure the tools/llamacpp directory is on the path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '.')) +import audit_snapshot + + +class TestExtractCmakeCall(unittest.TestCase): + """Balanced-paren CMake extraction.""" + + def test_simple_add_library(self): + text = 'add_library(ggml-base ggml.c ggml.cpp ggml-backend.cpp)' + name, sources = audit_snapshot.extract_cmake_call(text, 'add_library', 'ggml-base') + self.assertEqual(name, 'ggml-base') + self.assertEqual(sources, ['ggml.c', 'ggml.cpp', 'ggml-backend.cpp']) + + def test_ggml_add_backend_library(self): + text = '''ggml_add_backend_library(ggml-metal + ggml-metal.cpp + ggml-metal-device.m + ggml-metal-device.cpp)''' + name, sources = audit_snapshot.extract_cmake_call(text, 'ggml_add_backend_library', 'ggml-metal') + self.assertEqual(name, 'ggml-metal') + self.assertEqual(sources, ['ggml-metal.cpp', 'ggml-metal-device.m', 'ggml-metal-device.cpp']) + + def test_multiline_file_glob(self): + text = '''file(GLOB LLAMA_MODELS_SOURCES "models/*.cpp") +add_library(llama llama.cpp ${LLAMA_MODELS_SOURCES})''' + # Should handle the GLOB but not expand it here + name, sources = audit_snapshot.extract_cmake_call(text, 'add_library', 'llama') + self.assertEqual(name, 'llama') + # GLOB variable reference is preserved as-is for later expansion + self.assertIn('${LLAMA_MODELS_SOURCES}', ' '.join(sources)) + + def test_balanced_nested_parens(self): + text = 'target_compile_definitions(ggml PRIVATE GGML_BUILD=1 GGML_SHARED=0 $<$:GGML_DEBUG>)' + name, args = audit_snapshot.extract_cmake_call(text, 'target_compile_definitions', 'ggml') + self.assertIn('GGML_BUILD=1', args) + self.assertIn('$<$:GGML_DEBUG>', ' '.join(args)) + + +class TestSafeExtract(unittest.TestCase): + """Archive extraction safety.""" + + def test_rejects_escape_attempt(self): + import tarfile, io + with tempfile.TemporaryDirectory() as dest: + # Create an in-memory tarball with a path-escape entry + buf = io.BytesIO() + with tarfile.open(fileobj=buf, mode='w') as tf: + info = tarfile.TarInfo(name='../evil.txt') + info.size = 5 + tf.addfile(info, io.BytesIO(b'EVIL\n')) + buf.seek(0) + with self.assertRaises(ValueError): + audit_snapshot.safe_extract_tar(buf, dest) + + +class TestDownload(unittest.TestCase): + def test_download_uses_timeout(self): + downloader = getattr(audit_snapshot, 'download_file', None) + self.assertIsNotNone(downloader) + payload = b"archive" + with tempfile.TemporaryDirectory() as td, mock.patch.object( + urllib.request, + "urlopen", + return_value=io.BytesIO(payload), + ) as urlopen: + destination = Path(td) / "source.tar.gz" + downloader( + "https://example.invalid/source.tar.gz", + destination, + timeout=17, + max_bytes=len(payload), + ) + urlopen.assert_called_once_with( + "https://example.invalid/source.tar.gz", timeout=17 + ) + + def test_download_rejects_oversized_response(self): + downloader = getattr(audit_snapshot, 'download_file', None) + self.assertIsNotNone(downloader) + payload = b"too large" + with tempfile.TemporaryDirectory() as td, mock.patch.object( + urllib.request, + "urlopen", + return_value=io.BytesIO(payload), + ): + destination = Path(td) / "source.tar.gz" + with self.assertRaisesRegex(RuntimeError, "exceeds.*limit"): + downloader( + "https://example.invalid/source.tar.gz", + destination, + timeout=17, + max_bytes=len(payload) - 1, + ) + self.assertFalse(destination.exists()) + + +class TestCollectSnapshotMiniTree(unittest.TestCase): + """Full snapshot from a synthetic tree.""" + + maxDiff = None + + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.root = self.tmp.name + os.makedirs(os.path.join(self.root, 'ggml', 'src'), exist_ok=True) + os.makedirs(os.path.join(self.root, 'ggml', 'src', 'ggml-cpu', 'arch', 'x86'), exist_ok=True) + os.makedirs(os.path.join(self.root, 'ggml', 'src', 'ggml-cpu', 'arch', 'arm'), exist_ok=True) + os.makedirs(os.path.join(self.root, 'ggml', 'src', 'ggml-metal'), exist_ok=True) + os.makedirs(os.path.join(self.root, 'ggml', 'include'), exist_ok=True) + os.makedirs(os.path.join(self.root, 'src', 'models'), exist_ok=True) + os.makedirs(os.path.join(self.root, 'include'), exist_ok=True) + + # Write CMake snippet + with open(os.path.join(self.root, 'CMakeLists.txt'), 'w') as f: + f.write('''add_library(ggml-base ggml.c ggml.cpp ggml-backend.cpp) +add_library(ggml ggml-backend-dl.cpp ggml-backend-reg.cpp) +ggml_add_backend_library(ggml-metal + ggml-metal.cpp + ggml-metal-device.m + ggml-metal-device.cpp) +file(GLOB LLAMA_MODELS_SOURCES "src/models/*.cpp") +add_library(llama llama.cpp ${LLAMA_MODELS_SOURCES}) +''') + cpu_cmake = os.path.join( + self.root, 'ggml', 'src', 'ggml-cpu', 'CMakeLists.txt') + with open(cpu_cmake, 'w') as f: + f.write('''function(ggml_add_cpu_backend_variant_impl tag_name) + list(APPEND GGML_CPU_SOURCES + ggml-cpu/ggml-cpu.c + ggml-cpu/ggml-cpu.cpp + ggml-cpu/ops.cpp) + if (GGML_SYSTEM_ARCH STREQUAL "ARM") + list(APPEND GGML_CPU_SOURCES ggml-cpu/arch/arm/quants.c) + elseif (GGML_SYSTEM_ARCH STREQUAL "x86") + list(APPEND GGML_CPU_SOURCES ggml-cpu/arch/x86/quants.c) + endif() +endfunction() +''') + # Create source files + for src in ['ggml.c', 'ggml.cpp', 'ggml-backend.cpp', + 'ggml-backend-dl.cpp', 'ggml-backend-reg.cpp', + 'ggml-metal.cpp', 'ggml-metal-device.m', 'ggml-metal-device.cpp', + 'llama.cpp']: + open(os.path.join(self.root, src), 'w').close() + for src in [ + 'ggml/src/ggml-cpu/ggml-cpu.c', + 'ggml/src/ggml-cpu/ggml-cpu.cpp', + 'ggml/src/ggml-cpu/ops.cpp', + 'ggml/src/ggml-cpu/arch/x86/quants.c', + 'ggml/src/ggml-cpu/arch/arm/quants.c']: + open(os.path.join(self.root, src), 'w').close() + # Create model files (sorted names for deterministic order) + for name in ['a.cpp', 'z.cpp']: + open(os.path.join(self.root, 'src', 'models', name), 'w').close() + # Create registry fixture + reg_path = os.path.join(self.root, 'ggml-backend-reg.cpp') + with open(reg_path, 'w') as f: + f.write(''' +#ifdef GGML_USE_CPU + register_backend(ggml_backend_cpu_reg()); +#endif +#ifdef GGML_USE_METAL + register_backend(ggml_backend_metal_reg()); +#endif +''') + # Create metal shader input files + for name in ['ggml-common.h', 'ggml-metal.metal', 'ggml-metal-impl.h']: + sub = 'ggml-metal' if name != 'ggml-common.h' else '' + p = os.path.join(self.root, 'ggml', 'src', sub, name) + os.makedirs(os.path.dirname(p), exist_ok=True) + with open(p, 'w') as f: + f.write('/* placeholder */\n') + # Create public headers + for name, directory in [ + ('llama.h', 'include'), + ('ggml.h', 'ggml/include'), + ('ggml-cpu.h', 'ggml/include')]: + with open(os.path.join(self.root, directory, name), 'w') as f: + f.write(f'/* {name} placeholder */\n') + + # Create the metal shader with markers + metal_path = os.path.join(self.root, 'ggml', 'src', 'ggml-metal', 'ggml-metal.metal') + with open(metal_path, 'w') as f: + f.write(''' +#include "ggml-common.h" +// ... metal shader code ... +// replacement marker for build system +''') + + def tearDown(self): + self.tmp.cleanup() + + def test_collect_snapshot_sources(self): + report = audit_snapshot.collect_snapshot( + self.root, + tag='test', + commit='deadbeef', + url='https://example.com/test.tar.gz', + archive_sha256='abc123') + # sorted() gives ASCII order: '-' < '.', so ggml-backend*.cpp comes first + self.assertEqual(report['sources']['ggml_base'], + ['ggml-backend.cpp', 'ggml.c', 'ggml.cpp']) + self.assertEqual(report['sources']['ggml_registry'], + ['ggml-backend-dl.cpp', 'ggml-backend-reg.cpp']) + # Models: glob expanded and sorted + self.assertEqual(report['sources']['models'], + ['src/models/a.cpp', 'src/models/z.cpp']) + # Metal: 3 source files + # sorted order for ggml_metal sources + self.assertEqual(report['sources']['ggml_metal'], + ['ggml-metal-device.cpp', 'ggml-metal-device.m', 'ggml-metal.cpp']) + self.assertEqual(report['sources']['ggml_cpu_common'], [ + 'ggml/src/ggml-cpu/ggml-cpu.c', + 'ggml/src/ggml-cpu/ggml-cpu.cpp', + 'ggml/src/ggml-cpu/ops.cpp', + ]) + self.assertEqual(report['sources']['ggml_cpu_x86'], [ + 'ggml/src/ggml-cpu/arch/x86/quants.c', + ]) + self.assertEqual(report['sources']['ggml_cpu_arm'], [ + 'ggml/src/ggml-cpu/arch/arm/quants.c', + ]) + # Registry markers + self.assertEqual(report['registry']['GGML_USE_CPU'], 'ggml_backend_cpu_reg') + self.assertEqual(report['registry']['GGML_USE_METAL'], 'ggml_backend_metal_reg') + # Shader inputs (sorted, deduplicated) + self.assertIn('ggml/src/ggml-common.h', report['metal']['shader_inputs']) + self.assertIn('ggml/src/ggml-metal/ggml-metal.metal', report['metal']['shader_inputs']) + self.assertIn('ggml/src/ggml-metal/ggml-metal-impl.h', report['metal']['shader_inputs']) + self.assertEqual( + set(report['public_header_sha256']), + {'include/llama.h', 'ggml/include/ggml.h', 'ggml/include/ggml-cpu.h'}, + ) + + def test_check_exports_uses_verified_upstream_tree(self): + import gen_exports + + checker = getattr(audit_snapshot, 'check_exports', None) + self.assertIsNotNone(checker) + generated = ('llama exports\n', 'ggml exports\n', 'skipped\n') + output_dir = Path(self.root) / 'exports' + with mock.patch.object( + gen_exports, 'generate_exports', return_value=generated) as generate, \ + mock.patch.object( + gen_exports, 'sync_outputs', return_value=0) as sync: + self.assertEqual(checker(self.root, output_dir), 0) + generate.assert_called_once_with(self.root) + sync.assert_called_once_with( + output_dir, + { + 'llama.inc': generated[0], + 'required_ggml.inc': generated[1], + 'llama.skipped.txt': generated[2], + }, + True, + ) + + def test_rejects_unknown_cpu_source_variable(self): + cpu_cmake = Path(self.root) / 'ggml/src/ggml-cpu/CMakeLists.txt' + with cpu_cmake.open('a') as f: + f.write('\nlist(APPEND GGML_CPU_SOURCES ${NEW_DEFAULT_CPU_SOURCES})\n') + + with self.assertRaisesRegex( + ValueError, 'unexpected CPU source variable.*NEW_DEFAULT_CPU_SOURCES'): + audit_snapshot.collect_snapshot( + self.root, + tag='test', + commit='deadbeef', + url='https://example.com/test.tar.gz', + archive_sha256='abc123') + + def test_rejects_unresolved_cpu_translation_unit(self): + cpu_cmake = Path(self.root) / 'ggml/src/ggml-cpu/CMakeLists.txt' + with cpu_cmake.open('a') as f: + f.write( + '\nlist(APPEND GGML_CPU_SOURCES ggml-cpu/generated-default.cpp)\n') + + with self.assertRaisesRegex( + ValueError, 'unresolved CPU source.*generated-default.cpp'): + audit_snapshot.collect_snapshot( + self.root, + tag='test', + commit='deadbeef', + url='https://example.com/test.tar.gz', + archive_sha256='abc123') + + def _write_archive_and_report(self): + fixture_dir = tempfile.TemporaryDirectory() + archive = Path(fixture_dir.name) / 'llama.cpp-test.tar.gz' + with tarfile.open(archive, 'w:gz') as tf: + tf.add(self.root, arcname='llama.cpp-test') + digest = audit_snapshot.sha256_file(archive) + url = archive.as_uri() + report = audit_snapshot.collect_snapshot( + self.root, + tag='test', + commit='deadbeef', + url=url, + archive_sha256=digest, + ) + report_path = Path(fixture_dir.name) / 'snapshot.json' + report_path.write_text(json.dumps(report, indent=2) + '\n') + return fixture_dir, report_path + + def test_check_only_uses_identity_from_report(self): + fixture_dir, report_path = self._write_archive_and_report() + self.addCleanup(fixture_dir.cleanup) + with mock.patch.object( + sys, 'argv', ['audit_snapshot.py', '--check', str(report_path)]): + self.assertEqual(audit_snapshot.main(), 0) + + def test_check_supports_local_upstream_override(self): + fixture_dir, report_path = self._write_archive_and_report() + self.addCleanup(fixture_dir.cleanup) + with mock.patch.object( + sys, 'argv', ['audit_snapshot.py', '--check', str(report_path), + '--upstream', self.root]): + self.assertEqual(audit_snapshot.main(), 0) + + def test_check_rejects_conflicting_source_identity(self): + fixture_dir, report_path = self._write_archive_and_report() + self.addCleanup(fixture_dir.cleanup) + report = json.loads(report_path.read_text()) + original = Path(report['upstream']['url'].removeprefix('file://')) + controlled = original.with_name('controlled-source.tar.gz') + controlled.write_bytes(original.read_bytes()) + stderr = io.StringIO() + with mock.patch.object( + sys, 'argv', ['audit_snapshot.py', '--check', str(report_path), + '--url', controlled.as_uri(), '--sha256', + report['upstream']['sha256']]), \ + contextlib.redirect_stderr(stderr), \ + self.assertRaises(SystemExit): + audit_snapshot.main() + self.assertIn('--url conflicts with report identity', stderr.getvalue()) + + def test_check_accepts_matching_explicit_identity(self): + fixture_dir, report_path = self._write_archive_and_report() + self.addCleanup(fixture_dir.cleanup) + identity = json.loads(report_path.read_text())['upstream'] + argv = ['audit_snapshot.py', '--check', str(report_path)] + for option in ('tag', 'commit', 'url', 'sha256'): + argv.extend((f'--{option}', identity[option])) + with mock.patch.object(sys, 'argv', argv): + self.assertEqual(audit_snapshot.main(), 0) + + def test_check_rejects_incomplete_report_identity(self): + fixture_dir, report_path = self._write_archive_and_report() + self.addCleanup(fixture_dir.cleanup) + report = json.loads(report_path.read_text()) + del report['upstream']['sha256'] + report_path.write_text(json.dumps(report)) + stderr = io.StringIO() + with mock.patch.object( + sys, 'argv', ['audit_snapshot.py', '--check', str(report_path), + '--upstream', self.root]), \ + contextlib.redirect_stderr(stderr), \ + self.assertRaises(SystemExit): + audit_snapshot.main() + self.assertIn('missing upstream identity fields: sha256', stderr.getvalue()) + + def test_check_rejects_conflicting_explicit_identity(self): + fixture_dir, report_path = self._write_archive_and_report() + self.addCleanup(fixture_dir.cleanup) + stderr = io.StringIO() + with mock.patch.object( + sys, 'argv', ['audit_snapshot.py', '--check', str(report_path), + '--upstream', self.root, '--tag', 'other']), \ + contextlib.redirect_stderr(stderr), \ + self.assertRaises(SystemExit): + audit_snapshot.main() + self.assertIn('--tag conflicts with report identity', stderr.getvalue()) + + def test_check_requires_complete_url_override(self): + fixture_dir, report_path = self._write_archive_and_report() + self.addCleanup(fixture_dir.cleanup) + stderr = io.StringIO() + with mock.patch.object( + sys, 'argv', ['audit_snapshot.py', '--check', str(report_path), + '--url', 'file:///controlled.tar.gz']), \ + contextlib.redirect_stderr(stderr), \ + self.assertRaises(SystemExit): + audit_snapshot.main() + self.assertIn('--sha256 is required when using --url', stderr.getvalue()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/llamacpp/test_gen_exports.py b/tools/llamacpp/test_gen_exports.py new file mode 100644 index 0000000..af2e822 --- /dev/null +++ b/tools/llamacpp/test_gen_exports.py @@ -0,0 +1,275 @@ +from __future__ import annotations + +import contextlib +import io +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path +from unittest import mock + +TOOL_DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(TOOL_DIR)) + +import gen_exports + + +class TestGenerateExports(unittest.TestCase): + def setUp(self): + for name, value in { + "REQUIRED_GGML_TYPES": { + "ggml_backend_dev_t", + "ggml_backend_reg_t", + "ggml_context", + "ggml_log_callback", + "ggml_log_level", + }, + "REQUIRED_GGML_ENUM_MEMBERS": { + "GGML_BACKEND_DEVICE_TYPE_ACCEL", + "GGML_BACKEND_DEVICE_TYPE_CPU", + "GGML_BACKEND_DEVICE_TYPE_GPU", + }, + "REQUIRED_GGML_FUNCTIONS": { + "ggml_backend_alloc_ctx_tensors", + "ggml_backend_dev_type", + "ggml_backend_reg_by_name", + }, + }.items(): + patcher = mock.patch.object(gen_exports, name, value) + patcher.start() + self.addCleanup(patcher.stop) + self.tmp = tempfile.TemporaryDirectory() + self.root = self.tmp.name + os.makedirs(os.path.join(self.root, "include")) + with open( + os.path.join(self.root, "include", "llama.h"), + "w", + encoding="utf-8", + ) as fixture: + fixture.write( + """\ +#define LLAMA_API +struct llama_model; +enum llama_mode { LLAMA_MODE_A, LLAMA_MODE_B }; +LLAMA_API void llama_live(struct llama_model *); +#define LLAMA_NUMBER 7 +""" + ) + os.makedirs(os.path.join(self.root, "ggml", "include")) + with open( + os.path.join(self.root, "ggml", "include", "ggml.h"), + "w", + encoding="utf-8", + ) as fixture: + fixture.write( + """\ +enum ggml_log_level { GGML_LOG_LEVEL_NONE, GGML_LOG_LEVEL_INFO }; +typedef void (*ggml_log_callback)(enum ggml_log_level, const char *, void *); + """ + ) + with open( + os.path.join(self.root, "ggml", "include", "ggml-backend.h"), + "w", + encoding="utf-8", + ) as fixture: + fixture.write( + """\ +typedef struct ggml_backend_reg * ggml_backend_reg_t; +typedef struct ggml_backend_device * ggml_backend_dev_t; +enum ggml_backend_dev_type { + GGML_BACKEND_DEVICE_TYPE_CPU, + GGML_BACKEND_DEVICE_TYPE_GPU, + GGML_BACKEND_DEVICE_TYPE_ACCEL, +}; +ggml_backend_reg_t ggml_backend_reg_by_name(const char *); +enum ggml_backend_dev_type ggml_backend_dev_type(ggml_backend_dev_t); +""" + ) + with open( + os.path.join(self.root, "ggml", "include", "ggml-alloc.h"), + "w", + encoding="utf-8", + ) as fixture: + fixture.write( + """\ +struct ggml_context; +struct ggml_backend; +struct ggml_backend_buffer; +typedef struct ggml_backend * ggml_backend_t; +struct ggml_backend_buffer * ggml_backend_alloc_ctx_tensors( + struct ggml_context *, ggml_backend_t); +""" + ) + + def tearDown(self): + self.tmp.cleanup() + + def test_generate_exports_finds_llama_types(self): + llama, _, _ = gen_exports.generate_exports( + upstream_dir=self.root, + include_dirs=[ + os.path.join(self.root, "include"), + os.path.join(self.root, "ggml", "include"), + ], + ) + self.assertIn("export using ::llama_model;", llama) + self.assertIn("export using ::llama_live;", llama) + self.assertIn("export using ::LLAMA_MODE_A;", llama) + + def test_generate_exports_skips_macros(self): + _, _, skipped = gen_exports.generate_exports( + upstream_dir=self.root, + include_dirs=[ + os.path.join(self.root, "include"), + os.path.join(self.root, "ggml", "include"), + ], + ) + self.assertIn("LLAMA_NUMBER", skipped) + + def test_generate_exports_includes_required_ggml_types(self): + _, ggml, _ = gen_exports.generate_exports( + upstream_dir=self.root, + include_dirs=[ + os.path.join(self.root, "include"), + os.path.join(self.root, "ggml", "include"), + ], + ) + self.assertIn("export using ::ggml_log_level;", ggml) + self.assertIn("export using ::GGML_LOG_LEVEL_INFO;", ggml) + self.assertIn("export using ::ggml_log_callback;", ggml) + + def test_generate_exports_includes_required_backend_api(self): + _, ggml, _ = gen_exports.generate_exports( + upstream_dir=self.root, + include_dirs=[ + os.path.join(self.root, "include"), + os.path.join(self.root, "ggml", "include"), + ], + ) + self.assertIn("export using ::ggml_backend_reg_t;", ggml) + self.assertIn("export using ::GGML_BACKEND_DEVICE_TYPE_GPU;", ggml) + self.assertIn("export using ::ggml_backend_reg_by_name;", ggml) + self.assertIn("export using ::ggml_backend_alloc_ctx_tensors;", ggml) + + def test_generate_exports_rejects_missing_required_api(self): + with mock.patch.object( + gen_exports, + "REQUIRED_GGML_FUNCTIONS", + gen_exports.REQUIRED_GGML_FUNCTIONS | {"ggml_missing"}, + ), self.assertRaisesRegex(RuntimeError, "ggml_missing"): + gen_exports.generate_exports( + upstream_dir=self.root, + include_dirs=[ + os.path.join(self.root, "include"), + os.path.join(self.root, "ggml", "include"), + ], + ) + + def test_generate_exports_rejects_failed_macro_dump(self): + real_run = subprocess.run + + def run(command, *args, **kwargs): + if "-dM" in command: + return subprocess.CompletedProcess( + command, 1, stdout="", stderr="macro failure" + ) + return real_run(command, *args, **kwargs) + + with mock.patch.object(gen_exports.subprocess, "run", side_effect=run), \ + self.assertRaisesRegex(RuntimeError, "macro failure"): + gen_exports.generate_exports( + upstream_dir=self.root, + include_dirs=[ + os.path.join(self.root, "include"), + os.path.join(self.root, "ggml", "include"), + ], + ) + + +class TestCheckMode(unittest.TestCase): + GENERATED = { + "llama.inc": "export using ::llama_model;\n", + "required_ggml.inc": "export using ::ggml_context;\n", + "llama.skipped.txt": "macro LLAMA_API\n", + } + + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.output_dir = Path(self.tmp.name) / "generated" + self.output_dir.mkdir() + + def tearDown(self): + self.tmp.cleanup() + + def test_default_output_is_the_checked_in_module_snapshot(self): + self.assertEqual( + gen_exports.DEFAULT_OUTPUT_DIR, + TOOL_DIR / "module/gen_exports", + ) + + def write_outputs(self, names=None): + selected = names if names is not None else self.GENERATED.keys() + for name in selected: + (self.output_dir / name).write_text( + self.GENERATED[name], encoding="utf-8" + ) + + def run_check(self): + generated_tuple = ( + self.GENERATED["llama.inc"], + self.GENERATED["required_ggml.inc"], + self.GENERATED["llama.skipped.txt"], + ) + stderr = io.StringIO() + with mock.patch.object( + gen_exports, "generate_exports", return_value=generated_tuple + ), contextlib.redirect_stderr(stderr): + result = gen_exports.main( + [ + "--upstream", + self.tmp.name, + "--output-dir", + str(self.output_dir), + "--check", + ] + ) + return result, stderr.getvalue() + + def snapshot(self): + return { + path.name: path.read_bytes() + for path in sorted(self.output_dir.iterdir()) + if path.is_file() + } + + def test_check_accepts_matching_outputs_without_writing(self): + self.write_outputs() + before = self.snapshot() + result, stderr = self.run_check() + self.assertEqual(result, 0) + self.assertIn("All exports match.", stderr) + self.assertEqual(self.snapshot(), before) + + def test_check_rejects_stale_output_without_writing(self): + self.write_outputs() + stale = self.output_dir / "llama.inc" + stale.write_text("stale\n", encoding="utf-8") + before = self.snapshot() + result, stderr = self.run_check() + self.assertEqual(result, 1) + self.assertIn("llama.inc differs", stderr) + self.assertEqual(self.snapshot(), before) + + def test_check_rejects_missing_output_without_writing(self): + self.write_outputs(["llama.inc", "required_ggml.inc"]) + before = self.snapshot() + result, stderr = self.run_check() + self.assertEqual(result, 1) + self.assertIn("llama.skipped.txt does not exist", stderr) + self.assertEqual(self.snapshot(), before) + + +if __name__ == "__main__": + unittest.main() From b58d4d0ca4bc02bbf14bcbbdfaf036998f66b8ca Mon Sep 17 00:00:00 2001 From: wellwei <96378453+wellwei@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:37:55 +0800 Subject: [PATCH 2/2] feat(pkg): expose complete llamacpp module API --- pkgs/g/ggml-org.llamacpp.lua | 44 +++++++++++++++++-- tests/check_llamacpp_snapshot.py | 12 ++--- .../llamacpp-internal-cpu/tests/decode.cpp | 6 +-- .../llamacpp-internal-metal/tests/decode.cpp | 6 +-- tests/test_check_llamacpp_snapshot.py | 12 +++-- tools/llamacpp/gen_exports.py | 8 +--- tools/llamacpp/module/gen_exports/llama.inc | 36 +++++++++++++++ .../module/gen_exports/llama.skipped.txt | 36 --------------- .../module/{llama.cppm => llamacpp.cppm} | 2 +- tools/llamacpp/test_gen_exports.py | 13 ++++++ 10 files changed, 112 insertions(+), 63 deletions(-) rename tools/llamacpp/module/{llama.cppm => llamacpp.cppm} (98%) diff --git a/pkgs/g/ggml-org.llamacpp.lua b/pkgs/g/ggml-org.llamacpp.lua index 956e831..7a636fd 100644 --- a/pkgs/g/ggml-org.llamacpp.lua +++ b/pkgs/g/ggml-org.llamacpp.lua @@ -49,7 +49,7 @@ package = { "*/src", "mcpp_generated", }, - modules = { "llama" }, + modules = { "llamacpp" }, generated_files = { ["mcpp_generated/ggml_cpp.cpp"] = "#include \"ggml.cpp\"\n", ["mcpp_generated/ggml-cpu_cpp.cpp"] = "#include \"ggml-cpu.cpp\"\n", @@ -59,7 +59,7 @@ package = { #define GGML_VERSION "b10069" #define GGML_COMMIT "178a6c44937154dc4c4eff0d166f4a044c4fceba" ]=], - ["mcpp_generated/llama.cppm"] = [==[ + ["mcpp_generated/llamacpp.cppm"] = [==[ module; #include @@ -80,7 +80,7 @@ module; #undef LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY #undef LLAMA_STATE_SEQ_FLAGS_ON_DEVICE -export module llama; +export module llamacpp; #include "gen_exports/required_ggml.inc" #include "gen_exports/llama.inc" @@ -320,6 +320,8 @@ export using ::llama_adapter_meta_count; export using ::llama_adapter_meta_key_by_index; export using ::llama_adapter_meta_val_str; export using ::llama_adapter_meta_val_str_by_index; +export using ::llama_add_bos_token; +export using ::llama_add_eos_token; export using ::llama_attach_threadpool; export using ::llama_attention_type; export using ::llama_backend_free; @@ -335,6 +337,7 @@ export using ::llama_context; export using ::llama_context_default_params; export using ::llama_context_params; export using ::llama_context_type; +export using ::llama_copy_state_data; export using ::llama_decode; export using ::llama_detach_threadpool; export using ::llama_detokenize; @@ -342,6 +345,7 @@ export using ::llama_encode; export using ::llama_flash_attn_type; export using ::llama_flash_attn_type_name; export using ::llama_free; +export using ::llama_free_model; export using ::llama_ftype; export using ::llama_ftype_name; export using ::llama_get_embeddings; @@ -358,7 +362,10 @@ export using ::llama_get_sampled_logits_ith; export using ::llama_get_sampled_probs_count_ith; export using ::llama_get_sampled_probs_ith; export using ::llama_get_sampled_token_ith; +export using ::llama_get_state_size; export using ::llama_init_from_model; +export using ::llama_load_model_from_file; +export using ::llama_load_session_file; export using ::llama_log_get; export using ::llama_log_set; export using ::llama_logit_bias; @@ -428,11 +435,17 @@ export using ::llama_model_tensor_override; export using ::llama_n_batch; export using ::llama_n_ctx; export using ::llama_n_ctx_seq; +export using ::llama_n_ctx_train; +export using ::llama_n_embd; +export using ::llama_n_head; +export using ::llama_n_layer; export using ::llama_n_rs_seq; export using ::llama_n_seq_max; export using ::llama_n_threads; export using ::llama_n_threads_batch; export using ::llama_n_ubatch; +export using ::llama_n_vocab; +export using ::llama_new_context_with_model; export using ::llama_numa_init; export using ::llama_opt_epoch; export using ::llama_opt_init; @@ -474,6 +487,7 @@ export using ::llama_sampler_init_adaptive_p; export using ::llama_sampler_init_dist; export using ::llama_sampler_init_dry; export using ::llama_sampler_init_grammar; +export using ::llama_sampler_init_grammar_lazy; export using ::llama_sampler_init_grammar_lazy_patterns; export using ::llama_sampler_init_greedy; export using ::llama_sampler_init_infill; @@ -493,6 +507,7 @@ export using ::llama_sampler_name; export using ::llama_sampler_reset; export using ::llama_sampler_sample; export using ::llama_sampler_seq_config; +export using ::llama_save_session_file; export using ::llama_seq_id; export using ::llama_set_abort_callback; export using ::llama_set_adapter_cvec; @@ -501,6 +516,8 @@ export using ::llama_set_causal_attn; export using ::llama_set_embeddings; export using ::llama_set_n_threads; export using ::llama_set_sampler; +export using ::llama_set_state_data; +export using ::llama_set_warmup; export using ::llama_split_mode; export using ::llama_split_path; export using ::llama_split_prefix; @@ -526,13 +543,32 @@ export using ::llama_synchronize; export using ::llama_time_us; export using ::llama_token; export using ::llama_token_attr; +export using ::llama_token_bos; +export using ::llama_token_cls; export using ::llama_token_data; export using ::llama_token_data_array; +export using ::llama_token_eos; +export using ::llama_token_eot; +export using ::llama_token_fim_mid; +export using ::llama_token_fim_pad; +export using ::llama_token_fim_pre; +export using ::llama_token_fim_rep; +export using ::llama_token_fim_sep; +export using ::llama_token_fim_suf; +export using ::llama_token_get_attr; +export using ::llama_token_get_score; +export using ::llama_token_get_text; +export using ::llama_token_is_control; +export using ::llama_token_is_eog; +export using ::llama_token_nl; +export using ::llama_token_pad; +export using ::llama_token_sep; export using ::llama_token_to_piece; export using ::llama_token_type; export using ::llama_tokenize; export using ::llama_vocab; export using ::llama_vocab_bos; +export using ::llama_vocab_cls; export using ::llama_vocab_eos; export using ::llama_vocab_eot; export using ::llama_vocab_fim_mid; @@ -697,7 +733,7 @@ int main() try { "*/ggml/src/ggml-cpu/amx/amx.cpp", "*/ggml/src/ggml-cpu/amx/mmq.cpp", "*/ggml/src/ggml-cpu/llamafile/sgemm.cpp", - "mcpp_generated/llama.cppm", + "mcpp_generated/llamacpp.cppm", "*/src/llama.cpp", "*/src/llama-adapter.cpp", "*/src/llama-arch.cpp", diff --git a/tests/check_llamacpp_snapshot.py b/tests/check_llamacpp_snapshot.py index 2535d0d..47f2ff6 100644 --- a/tests/check_llamacpp_snapshot.py +++ b/tests/check_llamacpp_snapshot.py @@ -657,22 +657,22 @@ def check_module_contract(descriptors: dict[str, dict]) -> None: mcpp = get_path(descriptors["ggml-org.llamacpp"], "mcpp") require(mcpp.get("targets") == {"llama": {"kind": "lib"}}, "ggml-org.llamacpp must expose exactly one llama target") - require(array(mcpp.get("modules"), "ggml-org.llamacpp.modules") == ["llama"], - "ggml-org.llamacpp must expose exactly the llama module") + require(array(mcpp.get("modules"), "ggml-org.llamacpp.modules") == ["llamacpp"], + "ggml-org.llamacpp must expose exactly the llamacpp module") generated = get_path(mcpp, "generated_files") for generated_name, source_name in ( - ("mcpp_generated/llama.cppm", "llama.cppm"), + ("mcpp_generated/llamacpp.cppm", "llamacpp.cppm"), ("mcpp_generated/gen_exports/required_ggml.inc", "gen_exports/required_ggml.inc"), ("mcpp_generated/gen_exports/llama.inc", "gen_exports/llama.inc"), ): expected = (ROOT / "tools/llamacpp/module" / source_name).read_text() require(generated.get(generated_name) == expected, f"generated module input drift: {generated_name}") - module = generated["mcpp_generated/llama.cppm"] - require("export module llama;" in module + module = generated["mcpp_generated/llamacpp.cppm"] + require("export module llamacpp;" in module and "#include " in module and "#include \"gen_exports/llama.inc\"" in module, - "llama module wrapper contract drift") + "llamacpp module wrapper contract drift") def main() -> int: diff --git a/tests/examples/llamacpp-internal-cpu/tests/decode.cpp b/tests/examples/llamacpp-internal-cpu/tests/decode.cpp index b15b70c..2373cbc 100644 --- a/tests/examples/llamacpp-internal-cpu/tests/decode.cpp +++ b/tests/examples/llamacpp-internal-cpu/tests/decode.cpp @@ -3,14 +3,14 @@ import std; -import llama; +import llamacpp; #ifdef LLAMA_H -#error "import llama leaked LLAMA_H" +#error "import llamacpp leaked LLAMA_H" #endif #ifdef LLAMA_API -#error "import llama leaked LLAMA_API" +#error "import llamacpp leaked LLAMA_API" #endif diff --git a/tests/examples/llamacpp-internal-metal/tests/decode.cpp b/tests/examples/llamacpp-internal-metal/tests/decode.cpp index f9b19b2..80f2176 100644 --- a/tests/examples/llamacpp-internal-metal/tests/decode.cpp +++ b/tests/examples/llamacpp-internal-metal/tests/decode.cpp @@ -8,14 +8,14 @@ import std; -import llama; +import llamacpp; #ifdef LLAMA_H -#error "import llama leaked LLAMA_H" +#error "import llamacpp leaked LLAMA_H" #endif #ifdef LLAMA_API -#error "import llama leaked LLAMA_API" +#error "import llamacpp leaked LLAMA_API" #endif diff --git a/tests/test_check_llamacpp_snapshot.py b/tests/test_check_llamacpp_snapshot.py index dde6fc5..2748fc7 100644 --- a/tests/test_check_llamacpp_snapshot.py +++ b/tests/test_check_llamacpp_snapshot.py @@ -177,12 +177,18 @@ def test_rejects_llamafile_macro_removal(self): def test_rejects_generated_module_drift(self): generated = self.descriptors["ggml-org.llamacpp"]["mcpp"]["generated_files"] - generated["mcpp_generated/llama.cppm"] += "\n// drift\n" + generated["mcpp_generated/llamacpp.cppm"] += "\n// drift\n" self.assert_rejected(checker.check_module_contract, "generated module input drift") + def test_module_name_and_file_are_llamacpp(self): + mcpp = self.descriptors["ggml-org.llamacpp"]["mcpp"] + self.assertEqual(mcpp["modules"], {1: "llamacpp"}) + module = mcpp["generated_files"]["mcpp_generated/llamacpp.cppm"] + self.assertIn("export module llamacpp;", module) + def test_rejects_module_name_drift(self): self.descriptors["ggml-org.llamacpp"]["mcpp"]["modules"] = {1: "wrong"} - self.assert_rejected(checker.check_module_contract, "exactly the llama module") + self.assert_rejected(checker.check_module_contract, "exactly the llamacpp module") def test_rejects_extra_target(self): self.descriptors["ggml-org.llamacpp"]["mcpp"]["targets"]["extra"] = { @@ -268,7 +274,7 @@ def test_metal_member_consumes_merged_package(self): self.assertIn('"tests/examples/llamacpp-internal-metal"', root_manifest) self.assertIn('features = ["backend-metal"]', metal_manifest.read_text()) self.assertNotIn("compat.ggml-metal", metal_manifest.read_text()) - self.assertIn("import llama;", (checker.ROOT / "tests/examples/llamacpp-internal-metal/tests/decode.cpp").read_text()) + self.assertIn("import llamacpp;", (checker.ROOT / "tests/examples/llamacpp-internal-metal/tests/decode.cpp").read_text()) if __name__ == "__main__": diff --git a/tools/llamacpp/gen_exports.py b/tools/llamacpp/gen_exports.py index 0e8615b..0901649 100644 --- a/tools/llamacpp/gen_exports.py +++ b/tools/llamacpp/gen_exports.py @@ -140,13 +140,7 @@ def _walk(node): src_line = llama_lines[line_no - 1] else: src_line = "" - # Check for DeprecatedAttr - has_deprecated = any( - c.get("kind") == "DeprecatedAttr" - for c in node.get("inner", [])) - if has_deprecated or "DEPRECATED" in src_line: - skipped.append(f"deprecated function '{name}'") - elif "LLAMA_API" in src_line: + if "LLAMA_API" in src_line: llama_exports.append(f"export using ::{name};") # else: static/inline helpers, skip diff --git a/tools/llamacpp/module/gen_exports/llama.inc b/tools/llamacpp/module/gen_exports/llama.inc index 23fafd6..c501df9 100644 --- a/tools/llamacpp/module/gen_exports/llama.inc +++ b/tools/llamacpp/module/gen_exports/llama.inc @@ -115,6 +115,8 @@ export using ::llama_adapter_meta_count; export using ::llama_adapter_meta_key_by_index; export using ::llama_adapter_meta_val_str; export using ::llama_adapter_meta_val_str_by_index; +export using ::llama_add_bos_token; +export using ::llama_add_eos_token; export using ::llama_attach_threadpool; export using ::llama_attention_type; export using ::llama_backend_free; @@ -130,6 +132,7 @@ export using ::llama_context; export using ::llama_context_default_params; export using ::llama_context_params; export using ::llama_context_type; +export using ::llama_copy_state_data; export using ::llama_decode; export using ::llama_detach_threadpool; export using ::llama_detokenize; @@ -137,6 +140,7 @@ export using ::llama_encode; export using ::llama_flash_attn_type; export using ::llama_flash_attn_type_name; export using ::llama_free; +export using ::llama_free_model; export using ::llama_ftype; export using ::llama_ftype_name; export using ::llama_get_embeddings; @@ -153,7 +157,10 @@ export using ::llama_get_sampled_logits_ith; export using ::llama_get_sampled_probs_count_ith; export using ::llama_get_sampled_probs_ith; export using ::llama_get_sampled_token_ith; +export using ::llama_get_state_size; export using ::llama_init_from_model; +export using ::llama_load_model_from_file; +export using ::llama_load_session_file; export using ::llama_log_get; export using ::llama_log_set; export using ::llama_logit_bias; @@ -223,11 +230,17 @@ export using ::llama_model_tensor_override; export using ::llama_n_batch; export using ::llama_n_ctx; export using ::llama_n_ctx_seq; +export using ::llama_n_ctx_train; +export using ::llama_n_embd; +export using ::llama_n_head; +export using ::llama_n_layer; export using ::llama_n_rs_seq; export using ::llama_n_seq_max; export using ::llama_n_threads; export using ::llama_n_threads_batch; export using ::llama_n_ubatch; +export using ::llama_n_vocab; +export using ::llama_new_context_with_model; export using ::llama_numa_init; export using ::llama_opt_epoch; export using ::llama_opt_init; @@ -269,6 +282,7 @@ export using ::llama_sampler_init_adaptive_p; export using ::llama_sampler_init_dist; export using ::llama_sampler_init_dry; export using ::llama_sampler_init_grammar; +export using ::llama_sampler_init_grammar_lazy; export using ::llama_sampler_init_grammar_lazy_patterns; export using ::llama_sampler_init_greedy; export using ::llama_sampler_init_infill; @@ -288,6 +302,7 @@ export using ::llama_sampler_name; export using ::llama_sampler_reset; export using ::llama_sampler_sample; export using ::llama_sampler_seq_config; +export using ::llama_save_session_file; export using ::llama_seq_id; export using ::llama_set_abort_callback; export using ::llama_set_adapter_cvec; @@ -296,6 +311,8 @@ export using ::llama_set_causal_attn; export using ::llama_set_embeddings; export using ::llama_set_n_threads; export using ::llama_set_sampler; +export using ::llama_set_state_data; +export using ::llama_set_warmup; export using ::llama_split_mode; export using ::llama_split_path; export using ::llama_split_prefix; @@ -321,13 +338,32 @@ export using ::llama_synchronize; export using ::llama_time_us; export using ::llama_token; export using ::llama_token_attr; +export using ::llama_token_bos; +export using ::llama_token_cls; export using ::llama_token_data; export using ::llama_token_data_array; +export using ::llama_token_eos; +export using ::llama_token_eot; +export using ::llama_token_fim_mid; +export using ::llama_token_fim_pad; +export using ::llama_token_fim_pre; +export using ::llama_token_fim_rep; +export using ::llama_token_fim_sep; +export using ::llama_token_fim_suf; +export using ::llama_token_get_attr; +export using ::llama_token_get_score; +export using ::llama_token_get_text; +export using ::llama_token_is_control; +export using ::llama_token_is_eog; +export using ::llama_token_nl; +export using ::llama_token_pad; +export using ::llama_token_sep; export using ::llama_token_to_piece; export using ::llama_token_type; export using ::llama_tokenize; export using ::llama_vocab; export using ::llama_vocab_bos; +export using ::llama_vocab_cls; export using ::llama_vocab_eos; export using ::llama_vocab_eot; export using ::llama_vocab_fim_mid; diff --git a/tools/llamacpp/module/gen_exports/llama.skipped.txt b/tools/llamacpp/module/gen_exports/llama.skipped.txt index 7e725d2..71123df 100644 --- a/tools/llamacpp/module/gen_exports/llama.skipped.txt +++ b/tools/llamacpp/module/gen_exports/llama.skipped.txt @@ -1,39 +1,3 @@ -deprecated function 'llama_add_bos_token' -deprecated function 'llama_add_eos_token' -deprecated function 'llama_copy_state_data' -deprecated function 'llama_free_model' -deprecated function 'llama_get_state_size' -deprecated function 'llama_load_model_from_file' -deprecated function 'llama_load_session_file' -deprecated function 'llama_n_ctx_train' -deprecated function 'llama_n_embd' -deprecated function 'llama_n_head' -deprecated function 'llama_n_layer' -deprecated function 'llama_n_vocab' -deprecated function 'llama_new_context_with_model' -deprecated function 'llama_sampler_init_grammar_lazy' -deprecated function 'llama_save_session_file' -deprecated function 'llama_set_state_data' -deprecated function 'llama_set_warmup' -deprecated function 'llama_token_bos' -deprecated function 'llama_token_cls' -deprecated function 'llama_token_eos' -deprecated function 'llama_token_eot' -deprecated function 'llama_token_fim_mid' -deprecated function 'llama_token_fim_pad' -deprecated function 'llama_token_fim_pre' -deprecated function 'llama_token_fim_rep' -deprecated function 'llama_token_fim_sep' -deprecated function 'llama_token_fim_suf' -deprecated function 'llama_token_get_attr' -deprecated function 'llama_token_get_score' -deprecated function 'llama_token_get_text' -deprecated function 'llama_token_is_control' -deprecated function 'llama_token_is_eog' -deprecated function 'llama_token_nl' -deprecated function 'llama_token_pad' -deprecated function 'llama_token_sep' -deprecated function 'llama_vocab_cls' macro LLAMA_API macro LLAMA_DEFAULT_SEED macro LLAMA_FILE_MAGIC_GGLA diff --git a/tools/llamacpp/module/llama.cppm b/tools/llamacpp/module/llamacpp.cppm similarity index 98% rename from tools/llamacpp/module/llama.cppm rename to tools/llamacpp/module/llamacpp.cppm index 1960745..e8b3e83 100644 --- a/tools/llamacpp/module/llama.cppm +++ b/tools/llamacpp/module/llamacpp.cppm @@ -18,7 +18,7 @@ module; #undef LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY #undef LLAMA_STATE_SEQ_FLAGS_ON_DEVICE -export module llama; +export module llamacpp; #include "gen_exports/required_ggml.inc" #include "gen_exports/llama.inc" diff --git a/tools/llamacpp/test_gen_exports.py b/tools/llamacpp/test_gen_exports.py index af2e822..c29b35a 100644 --- a/tools/llamacpp/test_gen_exports.py +++ b/tools/llamacpp/test_gen_exports.py @@ -51,9 +51,11 @@ def setUp(self): fixture.write( """\ #define LLAMA_API +#define DEPRECATED(func, hint) func __attribute__((deprecated(hint))) struct llama_model; enum llama_mode { LLAMA_MODE_A, LLAMA_MODE_B }; LLAMA_API void llama_live(struct llama_model *); +DEPRECATED(LLAMA_API void llama_legacy(struct llama_model *), "use llama_live"); #define LLAMA_NUMBER 7 """ ) @@ -118,6 +120,17 @@ def test_generate_exports_finds_llama_types(self): self.assertIn("export using ::llama_live;", llama) self.assertIn("export using ::LLAMA_MODE_A;", llama) + def test_generate_exports_includes_deprecated_llama_api(self): + llama, _, skipped = gen_exports.generate_exports( + upstream_dir=self.root, + include_dirs=[ + os.path.join(self.root, "include"), + os.path.join(self.root, "ggml", "include"), + ], + ) + self.assertIn("export using ::llama_legacy;", llama) + self.assertNotIn("deprecated function 'llama_legacy'", skipped) + def test_generate_exports_skips_macros(self): _, _, skipped = gen_exports.generate_exports( upstream_dir=self.root,