diff --git a/.agents/docs/2026-10-24-project-template-checkpoint.md b/.agents/docs/2026-10-24-project-template-checkpoint.md new file mode 100644 index 0000000..88c7717 --- /dev/null +++ b/.agents/docs/2026-10-24-project-template-checkpoint.md @@ -0,0 +1,17 @@ +# Runnable project template checkpoint + +The existing ggml-org.llamacpp identity is retained. Package b10069.3 follows the existing upstream b10069 plus one packaging revision; no existing release or index hash is rewritten. + +The default cpu template uses the fully qualified self.name and self.version dependency. It initializes the actual CPU backend with no model download. With a local GGUF path it loads the model, creates a context, decodes and samples. Resource cleanup is explicit and scoped. + +The template test uses the actual mcpp package renderer and verifies the emitted dependency before substituting the staged candidate source. It then compiles and runs the generated program. Both the no-argument flow and a pinned 19 MB GGUF inference flow passed locally on LLVM 23.1.3 (sampled token 471). + +A fresh path consumer revealed missing generated build inputs that earlier install-time materialization had hidden. The four stable generated inputs are now checked in verbatim from generated_files, with tools/generate_build_files.py --check guarding drift. Feature-specific Metal/Vulkan generation still uses the build output. Current libc++ also exposed missing standard includes in upstream source; two scoped wrapper compile-flag rules provide cstdlib/cerrno without patching upstream. + +mcpp is pinned to 2026.10.21.1. The root package built under LLVM 23.1.3 and all 116 existing unit/repository/build-helper checks passed with LLAMACPP_REQUIRE_BUILD_HELPER=1. Native CPU CI now additionally executes the real template with the existing checksummed model. No merge or release is performed by this checkpoint. + +Native CI exposed an existing exact-version conflict: compat.vulkan@1.4.357.0 requires compat.vulkan-runtime@2026.09.11 while this wrapper directly requested 2026.09.07. The direct pin is aligned to 2026.09.11 rather than linking or mangling two copies of a runtime with no module. Evidence: job 120654792434. + +The native ARM64 job decoded correctly, but its old static-file-label check rejected the current engine-managed glibc loader. The check now inspects actual PT_INTERP and the loader-reported complete dependency closure: static remains accepted, dynamic is accepted only when loader and every resolved library belong to the declared isolated registry. Any system-library escape fails. This retains the no-host-runtime invariant and architecture check without changing the engine or skipping inference. + +The next Vulkan CI passed compilation but strict mode exposed static adapter/header anchors shared between program and multiple DSOs. The actual provider defaults are fixed in index commit a4130c75a49c6e9df03690743467696ef81d3a19, and only the Vulkan CI job checks out that exact candidate index and routes compat through it. This tests the paired change without merging the index or weakening --strict. diff --git a/.agents/docs/2026-10-25-unified-mwrap-checkpoint.md b/.agents/docs/2026-10-25-unified-mwrap-checkpoint.md new file mode 100644 index 0000000..e913b76 --- /dev/null +++ b/.agents/docs/2026-10-25-unified-mwrap-checkpoint.md @@ -0,0 +1,8 @@ +# Unified mwrap first release + +Package/module: `mcpplibs.mwrap.llamacpp`; upstream version: `b10069`. + +This checkpoint migrates wrapper identity while retaining upstream API names and SDK sources. +Default README is English with a mandatory Chinese counterpart; original section order is preserved. +Validation and release evidence are recorded in the PR and the index ecosystem ledger. +No old release tag or source resource is rewritten. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec83093..b98441e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,14 +10,18 @@ permissions: contents: read env: - MCPP_VERSION: "2026.9.6.3" + MCPP_VERSION: "2026.10.24.1" MCPP_RELEASE_BASE: https://github.com/mcpp-community/mcpp/releases/download + VULKAN_INDEX_COMMIT: "a4130c75a49c6e9df03690743467696ef81d3a19" jobs: unit-and-import: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 + - name: Mwrap identity and bilingual README contract + run: python3 tools/check_mwrap_release.py + - uses: actions/setup-python@v5 with: python-version: "3.12" @@ -45,12 +49,15 @@ jobs: - name: Cold CPU package build run: | rm -rf target + python3 tools/generate_build_files.py --check mcpp --version | grep -F "${MCPP_VERSION}" mcpp build --strict --no-cache - name: Unit and repository checks env: LLAMACPP_REQUIRE_BUILD_HELPER: "1" run: python3 -m unittest discover -s tests/unit -p 'test_*.py' -v + - name: Actual default project template + run: python3 tools/test_template.py --toolchain llvm@23.1.3 - name: Verify vendored upstream and generated API env: GITHUB_TOKEN: ${{ github.token }} @@ -146,6 +153,10 @@ jobs: throw "CPU pass marker missing" } + - name: Actual template GGUF decode + shell: bash + run: python3 tools/test_template.py --run-arg "$RUNNER_TEMP/stories15M.gguf" + linux-arm64: runs-on: ubuntu-24.04-arm steps: @@ -191,16 +202,11 @@ jobs: test_bin=$(find target -type f -path "*/bin/cpu_decode" -print -quit) test -x "${helper}" test -n "${test_bin}" - for binary in "${helper}" "${test_bin}"; do - file "${binary}" | tee /tmp/file.log - grep -F "ARM aarch64" /tmp/file.log - grep -F "statically linked" /tmp/file.log - if readelf -l "${binary}" | grep -q INTERP; then - echo "PT_INTERP present in ${binary}" - exit 1 - fi - echo "PT_INTERP absent in ${binary}" - done + # Current mcpp may bind a dynamic managed glibc runtime. + # Validate PT_INTERP and every loaded path, rather than assuming + # the host helper and target must both have a static file label. + python3 tools/check_native_runtime.py --home "${MCPP_HOME}" \ + --arch "ARM aarch64" "${helper}" "${test_bin}" test -f compile_commands.json grep -F "/arch/arm/quants.c" compile_commands.json @@ -273,6 +279,20 @@ jobs: MCPP_HOME: /tmp/llamacpp-vulkan-mcpp-home steps: - uses: actions/checkout@v4 + - name: Checkout the candidate runtime adapter metadata + uses: actions/checkout@v4 + with: + repository: mcpplibs/mcpp-index + ref: ${{ env.VULKAN_INDEX_COMMIT }} + path: .candidate-index + - name: Route Vulkan dependencies through the candidate index + run: | + python3 - <<'PY' + from pathlib import Path + import json + with Path("mcpp.toml").open("a", encoding="utf-8") as manifest: + manifest.write('\n[indices]\ncompat = { path = ' + json.dumps(Path(".candidate-index").resolve().as_posix()) + ' }\n') + PY - uses: actions/setup-python@v5 with: python-version: "3.12" @@ -423,4 +443,4 @@ jobs: # moved to upstream's checkpoint (mcpp-index#163). tag=$(sed -n 's/^version *= *"\(.*\)"/\1/p' mcpp.toml | head -1) echo "release contract for tag: $tag" - python3 tools/check_release.py --tag "$tag" + python3 tools/check_release.py --tag "mwrap-$tag" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 457e93b..237dac0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,24 +1,16 @@ -name: release - +name: Release on: push: - # The tag IS the package version, which is upstream's build number - # (`b10069`), optionally with a wrapper revision (`b10069.1`) — see - # docs/upstream-update-policy.md. `v*` was the third place the old - # semantic-version scheme was written down, and it silently did not fire. - tags: ["b[0-9]*"] - + tags: ["mwrap-*"] permissions: contents: read - jobs: - verify: - runs-on: ubuntu-24.04 + publish: + runs-on: ubuntu-latest + timeout-minutes: 30 permissions: + contents: write actions: read - contents: read - outputs: - mapping: ${{ steps.contract.outputs.mapping }} steps: - uses: actions/checkout@v4 with: @@ -26,62 +18,36 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.12" + - name: Check tag, upstream version and bilingual entry pages + run: python3 tools/check_mwrap_release.py --tag "$GITHUB_REF_NAME" - name: Run complete unit suite run: python3 -m unittest discover -s tests/unit -p 'test_*.py' -v - - name: Verify immutable release contract - id: contract - env: - GITHUB_TOKEN: ${{ github.token }} - shell: bash - run: | - set -euo pipefail - mapping=$(python3 tools/check_release.py --tag "${GITHUB_REF_NAME}" | tail -n 1) - echo "${mapping}" - echo "mapping=${mapping}" >> "${GITHUB_OUTPUT}" - - name: Require latest successful CI run for the tag commit + - name: Verify immutable upstream release mapping + run: python3 tools/check_release.py --tag "$GITHUB_REF_NAME" + - name: Require latest successful source CI env: GH_TOKEN: ${{ github.token }} shell: bash run: | set -euo pipefail - output="${RUNNER_TEMP}/ci-runs.json" - downloaded=0 - for attempt in 1 2 3 4 5; do - if gh api \ - "repos/${GITHUB_REPOSITORY}/actions/workflows/ci.yml/runs?head_sha=${GITHUB_SHA}&per_page=100" \ - > "${output}"; then - downloaded=1 - break - fi - if [[ "${attempt}" -lt 5 ]]; then sleep $((attempt * 2)); fi - done - if [[ "${downloaded}" -ne 1 ]]; then - echo "failed to query CI workflow runs after 5 attempts" - exit 1 - fi - python3 tools/check_ci_run.py --runs "${output}" --sha "${GITHUB_SHA}" - - publish: - needs: verify - runs-on: ubuntu-24.04 - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Create release notes for the immutable tag archive + gh api "repos/${GITHUB_REPOSITORY}/actions/workflows/ci.yml/runs?head_sha=${GITHUB_SHA}&per_page=100" > "$RUNNER_TEMP/ci-runs.json" + python3 tools/check_ci_run.py --runs "$RUNNER_TEMP/ci-runs.json" --sha "$GITHUB_SHA" + - name: Build immutable source asset and publish env: GH_TOKEN: ${{ github.token }} - RELEASE_MAPPING: ${{ needs.verify.outputs.mapping }} shell: bash run: | set -euo pipefail - if gh release view "${GITHUB_REF_NAME}" >/dev/null 2>&1; then - echo "release ${GITHUB_REF_NAME} already exists; refusing to mutate it" - exit 1 + version="${GITHUB_REF_NAME#mwrap-}" + repo="${GITHUB_REPOSITORY#*/}" + asset="${repo}-${version}.tar.gz" + git archive --format=tar.gz --prefix="${repo}-${version}/" "$GITHUB_SHA" > "$RUNNER_TEMP/$asset" + (cd "$RUNNER_TEMP" && sha256sum "$asset" > "$asset.sha256") + if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + echo 'Release already exists; refusing to change immutable assets'; exit 1 fi - gh release create "${GITHUB_REF_NAME}" \ - --verify-tag \ - --title "llama.cpp-m ${GITHUB_REF_NAME#v}" \ - --notes "${RELEASE_MAPPING}" + extra=() + if [[ "$version" == *-rc* ]]; then extra+=(--prerelease); fi + gh release create "$GITHUB_REF_NAME" "$RUNNER_TEMP/$asset" "$RUNNER_TEMP/$asset.sha256" \ + --repo "$GITHUB_REPOSITORY" --verify-tag --title "$repo $version" \ + --notes "Module wrapper for upstream $version. New mwrap package identities become installable after mcpp-index publication; see the bilingual README for the tested support scope." "${extra[@]}" diff --git a/.gitignore b/.gitignore index 5aec130..dfa5df0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,12 @@ .mcpp/ +.xlings/ +.candidate-index/ target/ -generated/ +generated/* +!generated/ggml_cpp.cpp +!generated/ggml-cpu_cpp.cpp +!generated/ggml_metal_device_m.m +!generated/ggml_build_info.h compile_commands.json __pycache__/ *.pyc diff --git a/.xlings.json b/.xlings.json new file mode 100644 index 0000000..b914806 --- /dev/null +++ b/.xlings.json @@ -0,0 +1 @@ +{"workspace":{"xim:mcpp":"2026.10.24.1"}} diff --git a/README.md b/README.md index a3fb04d..8c3fbec 100644 --- a/README.md +++ b/README.md @@ -1,237 +1,62 @@ # llama.cpp-m -`llama.cpp-m` packages a pinned, audited llama.cpp checkpoint as a C++23 -module for mcpp. The public module is: +> Pinned llama.cpp b10069 through C++23 modules; upstream API names are preserved. -```cpp -import llamacpp; -``` - -The package version IS llama.cpp's build number, plus this packaging's own -revision of it: `b10069.2`. CPU is the default backend; Metal is an additive -feature for macOS ARM64 and Vulkan an additive feature for Linux. Consumers pin -exactly -- `b10069` is not semver, so no range expresses "b10069 or later" -- -which means a consumer wanting a feature names the revision that has it. -`backend-vulkan` arrived in `b10069.1`. +**English** · [简体中文](README.zh.md) · [Upstream](https://github.com/ggml-org/llama.cpp) · [Releases](https://github.com/mcpplibs/llama.cpp-m/releases) · [mcpp](https://github.com/mcpp-community/mcpp) · [Index](https://github.com/mcpplibs/mcpp-index) -## Add The Package - -After `b10069.2` is published in mcpp-index: +[![C++23](https://img.shields.io/badge/C%2B%2B-23-blue.svg)](https://en.cppreference.com/w/cpp/23) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +[![CI](https://github.com/mcpplibs/llama.cpp-m/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/mcpplibs/llama.cpp-m/actions/workflows/ci.yml) -```bash -mcpp add ggml-org:llamacpp@b10069.2 -``` +## Modules -The equivalent manifest entry is: +- `import mcpplibs.mwrap.llamacpp;`: llama.cpp model, context and inference APIs +- `import mcpplibs.mwrap.llamacpp.macros;`: Typed macro constants -```toml -[dependencies.ggml-org] -llamacpp = "b10069.2" -``` +> More Modules API -> [Usage and API](docs/usage.md) -For Metal on macOS ARM64: +## Add The Package -```toml -[dependencies.ggml-org] -llamacpp = { version = "b10069.2", features = ["backend-metal"] } +```sh +mcpp new llama_app --template mcpplibs.mwrap.llamacpp@b10069 +cd llama_app +mcpp run ``` -For Vulkan on Linux: - ```toml -[dependencies.ggml-org] -llamacpp = { version = "b10069.2", features = ["backend-vulkan"] } +[dependencies] +mcpplibs.mwrap.llamacpp = "b10069" ``` -That line is the whole diff. The shader compiler, the Khronos loader, the -SPIR-V headers and the adapter that lets a built binary reach the machine's own -driver are declared by this package under the feature, so a consumer names none -of them -- and a consumer that does not name the feature acquires none of them -either. - ## Minimal CPU Program ```cpp -import std; -import llamacpp; - -int main() { - llama_backend_init(); - - auto params = llama_model_default_params(); - params.n_gpu_layers = 0; - llama_model * model = llama_model_load_from_file("model.gguf", params); - if (!model) { - llama_backend_free(); - return 1; - } - - llama_model_free(model); - llama_backend_free(); -} -``` - -Build and run it with the normal mcpp commands: - -```bash -mcpp build -mcpp run +import mcpplibs.mwrap.llamacpp; +int main() { llama_backend_init(); llama_backend_free(); } ``` -Model loading alone does not perform inference. See `tests/cpu_decode.cpp` for -a compact load/decode/sample path and `examples/chat-cpu` for token generation. +The default template checks CPU initialization without downloading a model. ## CPU Chat Example -The checked-in example uses a path dependency on this repository: - -```bash +```sh cd examples/chat-cpu -mcpp run -- /path/to/model.gguf \ - 'User: Introduce yourself in one sentence.\nAssistant:' +mcpp run -- /path/to/model.gguf "Hello" ``` -It sets `n_gpu_layers = 0`, imports `llamacpp`, and performs real -tokenization, decode, and sampling. +A compatible local GGUF model is required for tokenization, decode and sampling. ## Metal Chat Example -Metal requires macOS ARM64 and the `backend-metal` feature: - -```bash -cd examples/chat-metal -mcpp run -- /path/to/model.gguf metal \ - 'User: Introduce yourself in one sentence.\nAssistant:' -``` - -The example requests GPU offload and exits nonzero if Metal cannot be used. A -valid run should report all of the following: - -```text -using embedded metal library -GPU name: MTL0 (...) -offloaded N/M layers to GPU -MTL0_Mapped model buffer size = ... MiB -MTL0 compute buffer size = ... MiB -backend=metal ... generated_tokens=32 -``` - -The package test is stricter than successful compilation: it checks Metal -registration, graph execution, positive layer offload, positive Metal-owned -model and compute buffers, decode, and sampling. The Metal example exits -nonzero before reporting success if either Metal buffer is absent. +Use `backend-metal` on macOS ARM64; [the example](examples/chat-metal) requires actual offload and Metal buffers. ## Vulkan Chat Example -Vulkan requires Linux and the `backend-vulkan` feature: - -```bash -cd examples/chat-vulkan -mcpp run -- /path/to/model.gguf vulkan \ - 'User: Introduce yourself in one sentence.\nAssistant:' -``` - -The example refuses to report a device run that did not happen: it exits -nonzero when Vulkan is requested and no layer was offloaded, because a build -that quietly fell back to the CPU produces output indistinguishable from -success. - -### How the shaders are built - -The backend needs 134 shader sets, and upstream generates them with a tool it -keeps in its own tree. `build.mcpp` does not run that tool 134 times; it -DECLARES 136 build-graph edges -- one per shader, one to compile the generator -from the vendored source, one for the header they share. Each shader is then -incremental, parallel, and attributable to the edge that failed rather than to -`build.mcpp exited 1`. - -The build program does exactly one thing itself: it asks `glslc` which -extensions it accepts. That answer has two readers -- the generator, which -decides which variants to emit, and `ggml-vulkan.cpp`, which decides which to -look for -- and asking twice would make one truth into two. - -### A libstdc++ toolchain, on this checkpoint - -`backend-vulkan` builds with a libstdc++ toolchain. Under libc++ the compile of -upstream's `ggml-vulkan.cpp` stops at - -``` -error: invalid application of 'sizeof' to an incomplete type 'vk_memory_logger' - in instantiation of member function 'std::unique_ptr::~unique_ptr' -``` - -`vk_device_struct` holds a `std::unique_ptr` and its -destructor is instantiated at line 1015, while the class is defined at line -1920. Destroying a `unique_ptr` to an incomplete type is undefined behaviour; -libc++ has a static assertion for it and libstdc++ does not. It is upstream's -source rather than this packaging, and nothing in a build program can repair -it without patching the vendored tree, which this repository does not do. - -`build.mcpp` therefore refuses the combination and says so, instead of letting -the compiler produce a page of template diagnostics about a header the reader -did not write. The refusal became possible with mcpp 2026.9.6.3, which added -`mcpp::cxx_stdlib()`; the only signal available before that was the compiler's -NAME, and refusing on it would also have refused clang configured against -libstdc++, which builds this backend correctly. - -Everything else is unaffected: the CPU backend builds under both standard -libraries, measured. - -### Running without a GPU - -ggml keeps only Vulkan devices whose type is not `eCpu`, so a software -implementation is excluded for its type alone. Measured: Mesa's lavapipe -advertises `storageBuffer16BitAccess` and every feature the backend requires, -and is still dropped. Upstream ships the selector for this case, and a machine -with no GPU needs both halves of it: - -```bash -VK_DRIVER_FILES=/share/vulkan/icd.d/lvp_icd.x86_64.json \ -GGML_VK_VISIBLE_DEVICES=0 \ - mcpp test vulkan_decode --features backend-vulkan -``` - -### What the artifact depends on - -Measured on the built test binary, everything outside the mcpp registry: - -``` -$ ldd target/*/*/bin/vulkan_decode | grep -v mcpp/registry - linux-vdso.so.1 - libvulkan.so.1 => .../bin/libvulkan.so.1 -``` - -The Khronos loader is built from source by `compat:vulkan` and travels beside -the binary; the kernel's vDSO is the only thing left. The shader generator is -statically linked for the same reason -- it runs from inside the build, and a -build tool that needs the host's libstdc++ is a host dependency this ecosystem -does not accept. - -The package test is stricter than a token in range: it decodes the same prompt -twice from one model, once with every layer on the host and once with every -layer offloaded, and requires greedy sampling to produce the SAME token. A -token inside the vocabulary is produced by a backend that computed nonsense -just as readily as by one that worked. The equality is only evidence about the -device if the device ran, so the offload is asserted from llama.cpp's own log -before the tokens are compared. +Use `backend-vulkan` on Linux. [The example](examples/chat-vulkan) rejects silent CPU fallback. +Shader generation, libstdc++ constraints and driver boundaries are in [usage](docs/usage.md). ## Supported Boundary -`b10069.2` includes: - -- the public llama.cpp C API exposed through `import llamacpp;`; -- the CPU backend on Linux x86_64, Linux ARM64, Windows x86_64, and macOS ARM64; -- the Metal backend on macOS ARM64; -- the Vulkan backend on Linux, under `backend-vulkan`, with a libstdc++ - toolchain (a libc++ one is refused by name, for the upstream reason recorded - above); -- architecture-specific x86_64 and ARM64 CPU source selection. - -It does not include `mtmd`, CUDA, RPC, or other upstream backends. It also does -not claim that every upstream model architecture has been tested. -Deprecated upstream C APIs remain exported for API completeness; code that -calls them may receive the deprecation warnings defined by upstream. - -See [the update policy](docs/upstream-update-policy.md) for versioning and -checkpoint selection, and [the b10069 validation record](docs/validation/b10069.md) -for exact evidence and remaining release gates. +Pin `b10069` exactly; it is not a semver range. `.macros` exposes mapped constants. +Model quality and unsupported GPU configurations are not implied by the CPU template. +[Upstream policy](docs/upstream-update-policy.md) · [Validation](docs/validation/b10069.md) · [MIT](LICENSE) diff --git a/README.zh.md b/README.zh.md new file mode 100644 index 0000000..4a30c81 --- /dev/null +++ b/README.zh.md @@ -0,0 +1,62 @@ +# llama.cpp-m + +> 通过 C++23 模块使用固定的 llama.cpp b10069,保留上游 API 名称。 + +[English](README.md) · **简体中文** · [Upstream](https://github.com/ggml-org/llama.cpp) · [Releases](https://github.com/mcpplibs/llama.cpp-m/releases) · [mcpp](https://github.com/mcpp-community/mcpp) · [Index](https://github.com/mcpplibs/mcpp-index) + +[![C++23](https://img.shields.io/badge/C%2B%2B-23-blue.svg)](https://en.cppreference.com/w/cpp/23) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +[![CI](https://github.com/mcpplibs/llama.cpp-m/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/mcpplibs/llama.cpp-m/actions/workflows/ci.yml) + +## 模块 + +- `import mcpplibs.mwrap.llamacpp;`: llama.cpp 模型、上下文与推理 API +- `import mcpplibs.mwrap.llamacpp.macros;`: 类型化宏常量 + +> More Modules API -> [模块与 API](docs/usage.md) + +## 添加依赖 + +```sh +mcpp new llama_app --template mcpplibs.mwrap.llamacpp@b10069 +cd llama_app +mcpp run +``` + +```toml +[dependencies] +mcpplibs.mwrap.llamacpp = "b10069" +``` + +## 最小 CPU 程序 + +```cpp +import mcpplibs.mwrap.llamacpp; +int main() { llama_backend_init(); llama_backend_free(); } +``` + +默认模板验证 CPU 初始化,无需下载模型。 + +## CPU 对话示例 + +```sh +cd examples/chat-cpu +mcpp run -- /path/to/model.gguf "Hello" +``` + +实际分词、解码与采样需要兼容的本地 GGUF 模型。 + +## Metal 对话示例 + +macOS ARM64 使用 `backend-metal`;[示例](examples/chat-metal) 要求真实 offload 和 Metal buffer。 + +## Vulkan 对话示例 + +Linux 使用 `backend-vulkan`;[示例](examples/chat-vulkan) 拒绝静默 CPU 回退。 +shader 生成、libstdc++ 限制及驱动边界见[使用说明](docs/usage.md)。 + +## 支持边界 + +精确固定 `b10069`,不使用 semver 范围;`.macros` 导出映射常量。 +CPU 模板通过不代表模型质量或未支持 GPU 配置通过。 +[上游策略](docs/upstream-update-policy.md) · [验证](docs/validation/b10069.md) · [MIT](LICENSE) diff --git a/build.mcpp b/build.mcpp index 449e897..3f0a39d 100644 --- a/build.mcpp +++ b/build.mcpp @@ -93,7 +93,7 @@ int validate_features() { if (!supported.empty()) supported += ", "; supported += name; } - std::cerr << "ggml-org:llamacpp: unsupported feature '" << feature + std::cerr << "mcpplibs.mwrap.llamacpp: unsupported feature '" << feature << "' (supported features: " << supported << ")\n"; return 2; } @@ -108,15 +108,15 @@ int emit_metal() { const std::string manifest = mcpp::manifest_dir(); const std::string out_env = mcpp::out_dir(); if (std::string(os) != "macos") { - std::cerr << "ggml-org:llamacpp backend-metal requires target_os=macos\n"; + std::cerr << "mcpplibs.mwrap.llamacpp backend-metal requires target_os=macos\n"; return 2; } if (std::string(arch) != "aarch64") { - std::cerr << "ggml-org:llamacpp backend-metal requires target_arch=aarch64\n"; + std::cerr << "mcpplibs.mwrap.llamacpp backend-metal requires target_arch=aarch64\n"; return 2; } if (manifest.empty() || out_env.empty()) { - std::cerr << "ggml-org:llamacpp requires MCPP_MANIFEST_DIR and MCPP_OUT_DIR\n"; + std::cerr << "mcpplibs.mwrap.llamacpp requires MCPP_MANIFEST_DIR and MCPP_OUT_DIR\n"; return 2; } @@ -269,7 +269,7 @@ int emit_vulkan() { // that tried anyway would fail somewhere further in, with a message about // whatever broke first rather than about the platform. if (std::string(mcpp::target_os()) != "linux") { - std::cerr << "ggml-org:llamacpp backend-vulkan is Linux only; this " + std::cerr << "mcpplibs.mwrap.llamacpp backend-vulkan is Linux only; this " "build targets " << mcpp::target_os() << ". On macOS use backend-metal.\n"; return 2; @@ -291,7 +291,7 @@ int emit_vulkan() { // libstdc++, which builds this backend correctly. `mcpp::cxx_stdlib()` // answers the question that is actually being asked. if (std::string(mcpp::cxx_stdlib()) == "libc++") { - std::cerr << "ggml-org:llamacpp backend-vulkan needs a libstdc++ " + std::cerr << "mcpplibs.mwrap.llamacpp backend-vulkan needs a libstdc++ " "toolchain; this build resolved libc++.\n" " Upstream's ggml-vulkan.cpp destroys a std::unique_ptr to " "an incomplete type, which libc++ rejects by static " @@ -308,7 +308,7 @@ int emit_vulkan() { // failure worth naming here rather than a confusing "Exec format error" // from the middle of the graph. if (std::string(mcpp::host()) != std::string(mcpp::target())) { - std::cerr << "ggml-org:llamacpp backend-vulkan does not cross-compile: " + std::cerr << "mcpplibs.mwrap.llamacpp backend-vulkan does not cross-compile: " << "the shader generator is built from the vendored source and " << "run during the build, and this build targets " << mcpp::target() << " from " << mcpp::host() << "\n"; @@ -317,21 +317,21 @@ int emit_vulkan() { const std::string shaderc = mcpp::xpkg_dir("xim", "shaderc"); if (shaderc.empty()) { - std::cerr << "ggml-org:llamacpp backend-vulkan needs glslc. Declare it:\n" + std::cerr << "mcpplibs.mwrap.llamacpp backend-vulkan needs glslc. Declare it:\n" << " [feature-xlings.backend-vulkan]\n" << " \"xim:shaderc\" = \"2026.3\"\n"; return 2; } const fs::path glslc = fs::path(shaderc) / "bin/glslc"; if (!fs::exists(glslc)) { - std::cerr << "ggml-org:llamacpp backend-vulkan: xim:shaderc is installed at " + std::cerr << "mcpplibs.mwrap.llamacpp backend-vulkan: xim:shaderc is installed at " << shaderc << " but carries no bin/glslc\n"; return 2; } const host_compiler compiler = find_host_compiler(); if (!compiler) { - std::cerr << "ggml-org:llamacpp backend-vulkan: no C++ compiler found under " + std::cerr << "mcpplibs.mwrap.llamacpp backend-vulkan: no C++ compiler found under " << mcpp::toolchain_dir() << "/bin (looked for g++, clang++, c++)\n"; return 2; } @@ -534,6 +534,6 @@ int main() try { } return 0; } catch (const std::exception & error) { - std::cerr << "ggml-org:llamacpp build.mcpp: " << error.what() << "\n"; + std::cerr << "mcpplibs.mwrap.llamacpp build.mcpp: " << error.what() << "\n"; return 1; } diff --git a/docs/upstream-update-policy.md b/docs/upstream-update-policy.md index 8b526fa..b7b6c4f 100644 --- a/docs/upstream-update-policy.md +++ b/docs/upstream-update-policy.md @@ -12,9 +12,8 @@ The wrapper and upstream project have separate identities: own. See "Version Model" below for why that replaced a separate semantic version. - Every wrapper release maps to exactly one upstream tag and commit. -- The package coordinate is `ggml-org:llamacpp`; consumers use - `import llamacpp;`. The namespace names whose library this is — llama.cpp is - ggml-org's — rather than who packaged it (see mcpp-index#163). +- The package coordinate is `mcpplibs.mwrap.llamacpp`; consumers use + `import mcpplibs.mwrap.llamacpp;`. The wrapper belongs to the mwrap category; the vendored upstream remains ggml-org/llama.cpp. - mcpp-index points only to immutable `llama.cpp-m` releases, never a moving upstream branch. @@ -190,7 +189,7 @@ No release claims all upstream models merely because the API compiled. 5. Verify the tag, version, lock, vendored source, exports, and snapshot: ```bash - python3 tools/check_release.py --tag v + python3 tools/check_release.py --tag mwrap- ``` 6. Create an immutable wrapper tag and release only after all gates pass. diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..3d541bf --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,255 @@ +Detailed usage and background. See the [README](../README.md) for the current candidate quickstart and support boundary. + +# llama.cpp-m + +`llama.cpp-m` packages a pinned, audited llama.cpp checkpoint as a C++23 +module for mcpp. The public module is: + +```cpp +import mcpplibs.mwrap.llamacpp; +``` + +The package version IS llama.cpp's build number, plus this packaging's own +revision of it: `b10069`. CPU is the default backend; Metal is an additive +feature for macOS ARM64 and Vulkan an additive feature for Linux. Consumers pin +exactly -- `b10069` is not semver, so no range expresses "b10069 or later" -- +which means a consumer wanting a feature names the revision that has it. +`backend-vulkan` arrived in `b10069.1`. + +## Add The Package + +After `b10069` is published in mcpp-index: + +```bash +mcpp add mcpplibs.mwrap.llamacpp@b10069 +``` + +The equivalent manifest entry is: + +```toml +[dependencies.mcpplibs.mwrap] +llamacpp = "b10069" +``` + +For Metal on macOS ARM64: + +```toml +[dependencies.mcpplibs.mwrap] +llamacpp = { version = "b10069", features = ["backend-metal"] } +``` + +For Vulkan on Linux: + +```toml +[dependencies.mcpplibs.mwrap] +llamacpp = { version = "b10069", features = ["backend-vulkan"] } +``` + +That line is the whole diff. The shader compiler, the Khronos loader, the +SPIR-V headers and the adapter that lets a built binary reach the machine's own +driver are declared by this package under the feature, so a consumer names none +of them -- and a consumer that does not name the feature acquires none of them +either. + +## Minimal CPU Program + +```cpp +import std; +import mcpplibs.mwrap.llamacpp; + +int main() { + llama_backend_init(); + + auto params = llama_model_default_params(); + params.n_gpu_layers = 0; + llama_model * model = llama_model_load_from_file("model.gguf", params); + if (!model) { + llama_backend_free(); + return 1; + } + + llama_model_free(model); + llama_backend_free(); +} +``` + +Build and run it with the normal mcpp commands: + +```bash +mcpp build +mcpp run +``` + +Model loading alone does not perform inference. See `tests/cpu_decode.cpp` for +a compact load/decode/sample path and `examples/chat-cpu` for token generation. + +## CPU Chat Example + +The checked-in example uses a path dependency on this repository: + +```bash +cd examples/chat-cpu +mcpp run -- /path/to/model.gguf \ + 'User: Introduce yourself in one sentence.\nAssistant:' +``` + +It sets `n_gpu_layers = 0`, imports `mcpplibs.mwrap.llamacpp`, and performs real +tokenization, decode, and sampling. + +## Metal Chat Example + +Metal requires macOS ARM64 and the `backend-metal` feature: + +```bash +cd examples/chat-metal +mcpp run -- /path/to/model.gguf metal \ + 'User: Introduce yourself in one sentence.\nAssistant:' +``` + +The example requests GPU offload and exits nonzero if Metal cannot be used. A +valid run should report all of the following: + +```text +using embedded metal library +GPU name: MTL0 (...) +offloaded N/M layers to GPU +MTL0_Mapped model buffer size = ... MiB +MTL0 compute buffer size = ... MiB +backend=metal ... generated_tokens=32 +``` + +The package test is stricter than successful compilation: it checks Metal +registration, graph execution, positive layer offload, positive Metal-owned +model and compute buffers, decode, and sampling. The Metal example exits +nonzero before reporting success if either Metal buffer is absent. + +## Vulkan Chat Example + +Vulkan requires Linux and the `backend-vulkan` feature: + +```bash +cd examples/chat-vulkan +mcpp run -- /path/to/model.gguf vulkan \ + 'User: Introduce yourself in one sentence.\nAssistant:' +``` + +The example refuses to report a device run that did not happen: it exits +nonzero when Vulkan is requested and no layer was offloaded, because a build +that quietly fell back to the CPU produces output indistinguishable from +success. + +### How the shaders are built + +The backend needs 134 shader sets, and upstream generates them with a tool it +keeps in its own tree. `build.mcpp` does not run that tool 134 times; it +DECLARES 136 build-graph edges -- one per shader, one to compile the generator +from the vendored source, one for the header they share. Each shader is then +incremental, parallel, and attributable to the edge that failed rather than to +`build.mcpp exited 1`. + +The build program does exactly one thing itself: it asks `glslc` which +extensions it accepts. That answer has two readers -- the generator, which +decides which variants to emit, and `ggml-vulkan.cpp`, which decides which to +look for -- and asking twice would make one truth into two. + +### A libstdc++ toolchain, on this checkpoint + +`backend-vulkan` builds with a libstdc++ toolchain. Under libc++ the compile of +upstream's `ggml-vulkan.cpp` stops at + +``` +error: invalid application of 'sizeof' to an incomplete type 'vk_memory_logger' + in instantiation of member function 'std::unique_ptr::~unique_ptr' +``` + +`vk_device_struct` holds a `std::unique_ptr` and its +destructor is instantiated at line 1015, while the class is defined at line +1920. Destroying a `unique_ptr` to an incomplete type is undefined behaviour; +libc++ has a static assertion for it and libstdc++ does not. It is upstream's +source rather than this packaging, and nothing in a build program can repair +it without patching the vendored tree, which this repository does not do. + +`build.mcpp` therefore refuses the combination and says so, instead of letting +the compiler produce a page of template diagnostics about a header the reader +did not write. The refusal became possible with mcpp 2026.9.6.3, which added +`mcpp::cxx_stdlib()`; the only signal available before that was the compiler's +NAME, and refusing on it would also have refused clang configured against +libstdc++, which builds this backend correctly. + +Everything else is unaffected: the CPU backend builds under both standard +libraries, measured. + +### Running without a GPU + +ggml keeps only Vulkan devices whose type is not `eCpu`, so a software +implementation is excluded for its type alone. Measured: Mesa's lavapipe +advertises `storageBuffer16BitAccess` and every feature the backend requires, +and is still dropped. Upstream ships the selector for this case, and a machine +with no GPU needs both halves of it: + +```bash +VK_DRIVER_FILES=/share/vulkan/icd.d/lvp_icd.x86_64.json \ +GGML_VK_VISIBLE_DEVICES=0 \ + mcpp test vulkan_decode --features backend-vulkan +``` + +### What the artifact depends on + +Measured on the built test binary, everything outside the mcpp registry: + +``` +$ ldd target/*/*/bin/vulkan_decode | grep -v mcpp/registry + linux-vdso.so.1 + libvulkan.so.1 => .../bin/libvulkan.so.1 +``` + +The Khronos loader is built from source by `compat:vulkan` and travels beside +the binary; the kernel's vDSO is the only thing left. The shader generator is +statically linked for the same reason -- it runs from inside the build, and a +build tool that needs the host's libstdc++ is a host dependency this ecosystem +does not accept. + +The package test is stricter than a token in range: it decodes the same prompt +twice from one model, once with every layer on the host and once with every +layer offloaded, and requires greedy sampling to produce the SAME token. A +token inside the vocabulary is produced by a backend that computed nonsense +just as readily as by one that worked. The equality is only evidence about the +device if the device ran, so the offload is asserted from llama.cpp's own log +before the tokens are compared. + +## Supported Boundary + +`b10069` includes: + +- the public llama.cpp C API exposed through `import mcpplibs.mwrap.llamacpp;`; +- the CPU backend on Linux x86_64, Linux ARM64, Windows x86_64, and macOS ARM64; +- the Metal backend on macOS ARM64; +- the Vulkan backend on Linux, under `backend-vulkan`, with a libstdc++ + toolchain (a libc++ one is refused by name, for the upstream reason recorded + above); +- architecture-specific x86_64 and ARM64 CPU source selection. + +It does not include `mtmd`, CUDA, RPC, or other upstream backends. It also does +not claim that every upstream model architecture has been tested. +Deprecated upstream C APIs remain exported for API completeness; code that +calls them may receive the deprecation warnings defined by upstream. + +See [the update policy](../docs/upstream-update-policy.md) for versioning and +checkpoint selection, and [the b10069 validation record](../docs/validation/b10069.md) +for exact evidence and remaining release gates. + +## Quickstart project template + +The b10069 package adds a runnable default template. After that version is indexed: + +```sh +mcpp new my-llama --template mcpplibs.mwrap.llamacpp@b10069 +cd my-llama +mcpp run +``` + +The default run initializes the CPU backend and prints its capabilities without +downloading a model. `mcpp run -- /path/to/model.gguf` loads that local model, +decodes a small batch and samples a token. This smoke example is a starting +point; the existing chat-cpu example demonstrates prompt tokenization and chat. +`python3 tools/test_template.py` exercises the real renderer and generated project. diff --git a/docs/validation/b10069.md b/docs/validation/b10069.md index 8a25845..bb18168 100644 --- a/docs/validation/b10069.md +++ b/docs/validation/b10069.md @@ -1,5 +1,10 @@ # llama.cpp-m b10069 Validation +This is historical validation of the legacy package identity. New releases use +`mcpplibs.mwrap.llamacpp`; current exact-head CI and installed-consumer results +are tracked in the PR and the mcpp-index execution ledger. + + This record separates completed local evidence from GitHub Actions evidence that requires an authorized push. Version `b10069` is not published by this branch. @@ -10,7 +15,7 @@ branch. |---|---| | Wrapper | `llama.cpp-m b10069` | | Package | `ggml-org:llamacpp@b10069` | -| Module | `import llamacpp;` | +| Module | `import mcpplibs.mwrap.llamacpp;` | | Upstream tag | `b10069` | | Upstream commit | `178a6c44937154dc4c4eff0d166f4a044c4fceba` | | Official archive SHA-256 | `293a7c65a11e2203c5468a06d0d0e8d21dfff16ad08712b16c61efbe0d93e097` | diff --git a/examples/chat-cpu/mcpp.toml b/examples/chat-cpu/mcpp.toml index ec9f875..935f32f 100644 --- a/examples/chat-cpu/mcpp.toml +++ b/examples/chat-cpu/mcpp.toml @@ -8,4 +8,4 @@ kind = "bin" main = "src/main.cpp" [dependencies] -llamacpp = { path = "../.." } +"mcpplibs.mwrap.llamacpp" = { path = "../.." } diff --git a/examples/chat-cpu/src/main.cpp b/examples/chat-cpu/src/main.cpp index 73a278c..c724161 100644 --- a/examples/chat-cpu/src/main.cpp +++ b/examples/chat-cpu/src/main.cpp @@ -1,5 +1,5 @@ import std; -import llamacpp; +import mcpplibs.mwrap.llamacpp; namespace { @@ -88,7 +88,7 @@ int main(int argc, char ** argv) { int generated = 0; int exit_code = 0; - llama_token sampled = LLAMA_TOKEN_NULL; + llama_token sampled = mcpplibs::mwrap::llamacpp::macros::LLAMA_TOKEN_NULL; for (; generated < max_generated_tokens; ++generated) { const int decode_result = llama_decode(context, batch); if (decode_result != 0) { diff --git a/examples/chat-metal/mcpp.toml b/examples/chat-metal/mcpp.toml index 7cc7ed2..2e34391 100644 --- a/examples/chat-metal/mcpp.toml +++ b/examples/chat-metal/mcpp.toml @@ -8,4 +8,4 @@ kind = "bin" main = "src/main.cpp" [dependencies] -llamacpp = { path = "../..", features = ["backend-metal"] } +"mcpplibs.mwrap.llamacpp" = { path = "../..", features = ["backend-metal"] } diff --git a/examples/chat-metal/src/main.cpp b/examples/chat-metal/src/main.cpp index 75c73a0..10824dc 100644 --- a/examples/chat-metal/src/main.cpp +++ b/examples/chat-metal/src/main.cpp @@ -1,5 +1,5 @@ import std; -import llamacpp; +import mcpplibs.mwrap.llamacpp; namespace { @@ -146,7 +146,7 @@ int main(int argc, char ** argv) { int generated = 0; int exit_code = 0; - llama_token sampled = LLAMA_TOKEN_NULL; + llama_token sampled = mcpplibs::mwrap::llamacpp::macros::LLAMA_TOKEN_NULL; for (; generated < max_generated_tokens; ++generated) { const int decode_result = llama_decode(context, batch); if (decode_result != 0) { diff --git a/examples/chat-vulkan/mcpp.toml b/examples/chat-vulkan/mcpp.toml index 8ec2ef0..3b84b19 100644 --- a/examples/chat-vulkan/mcpp.toml +++ b/examples/chat-vulkan/mcpp.toml @@ -1,7 +1,7 @@ # The consumer's whole diff for a Vulkan build is the feature name below. # Every payload the backend needs -- glslc for the shaders, the Khronos loader, # the SPIR-V headers, the adapter that lets a built artifact reach the machine's -# own driver -- is declared by `ggml-org:llamacpp` under that feature, and a +# own driver -- is declared by `mcpplibs.mwrap.llamacpp` under that feature, and a # consumer that does not name it acquires none of them. [package] name = "chat-vulkan" @@ -13,4 +13,4 @@ kind = "bin" main = "src/main.cpp" [dependencies] -llamacpp = { path = "../..", features = ["backend-vulkan"] } +"mcpplibs.mwrap.llamacpp" = { path = "../..", features = ["backend-vulkan"] } diff --git a/examples/chat-vulkan/src/main.cpp b/examples/chat-vulkan/src/main.cpp index 7d2a594..1e890c9 100644 --- a/examples/chat-vulkan/src/main.cpp +++ b/examples/chat-vulkan/src/main.cpp @@ -5,7 +5,7 @@ // reports which backend ran and how many layers were offloaded, and refuses to // claim the device when it did not get one. import std; -import llamacpp; +import mcpplibs.mwrap.llamacpp; namespace { @@ -146,7 +146,7 @@ int main(int argc, char ** argv) { int generated = 0; int exit_code = 0; - llama_token sampled = LLAMA_TOKEN_NULL; + llama_token sampled = mcpplibs::mwrap::llamacpp::macros::LLAMA_TOKEN_NULL; for (; generated < max_generated_tokens; ++generated) { const int decode_result = llama_decode(context, batch); if (decode_result != 0) { diff --git a/generated/ggml-cpu_cpp.cpp b/generated/ggml-cpu_cpp.cpp new file mode 100644 index 0000000..bf147c1 --- /dev/null +++ b/generated/ggml-cpu_cpp.cpp @@ -0,0 +1 @@ +#include "ggml-cpu.cpp" diff --git a/generated/ggml_build_info.h b/generated/ggml_build_info.h new file mode 100644 index 0000000..6513d5f --- /dev/null +++ b/generated/ggml_build_info.h @@ -0,0 +1,3 @@ +#pragma once +#define GGML_VERSION "b10069" +#define GGML_COMMIT "178a6c44937154dc4c4eff0d166f4a044c4fceba" diff --git a/generated/ggml_cpp.cpp b/generated/ggml_cpp.cpp new file mode 100644 index 0000000..4f5c280 --- /dev/null +++ b/generated/ggml_cpp.cpp @@ -0,0 +1 @@ +#include "ggml.cpp" diff --git a/generated/ggml_metal_device_m.m b/generated/ggml_metal_device_m.m new file mode 100644 index 0000000..359fbf2 --- /dev/null +++ b/generated/ggml_metal_device_m.m @@ -0,0 +1 @@ +#include "ggml-metal-device.m" diff --git a/mcpp.toml b/mcpp.toml index d1c737e..8e88d53 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,27 +1,8 @@ -# The namespace names whose library this is, not who packaged it; the version -# says which upstream you are getting. `mcpplibs` is mcpp's DEFAULT namespace -# (kDefaultNamespace), not an answer to "whose library" — llama.cpp is -# ggml-org's, and this repository only adds the C++23 module layer. -# -# The version is upstream's build number VERBATIM. `0.1.0` needed a lookup -# table to read, and the README carried it: "Version 0.1.0 maps to llama.cpp -# b10069". A version that needs a decoder ring is not a version. -# -# `b10069` is not semver, and mcpp handles it — verified: resolution, version-key -# matching and wire addressing all work on a literal non-semver key. What it -# cannot do is RANGES over it (`^b10069` is not expressible), so consumers pin -# exactly. See mcpp#363; rule and migration table in mcpp-index#163. -# -# The fourth segment is this PACKAGING's revision of that checkpoint, and it -# moves when the packaging changes without the upstream tree changing: -# `b10069.1` added `backend-vulkan`, `b10069.2` refuses a libc++ toolchain for -# it by name. Consumers pin exactly, so a consumer wanting a feature must name -# the revision that has it -- which is why every version in the README moves -# with this line rather than staying at the bare checkpoint. +# Module wrapper; upstream API preserved. Policy: mcpp-index/docs/mwrap.md. [package] -namespace = "ggml-org" +namespace = "mcpplibs.mwrap" name = "llamacpp" -version = "b10069.2" +version = "b10069" description = "C++23 module package for llama.cpp b10069" license = "MIT" standard = "c++23" @@ -63,6 +44,7 @@ sources = [ "third_party/llama.cpp/ggml/src/ggml-cpu/amx/mmq.cpp", "third_party/llama.cpp/ggml/src/ggml-cpu/llamafile/sgemm.cpp", "src/llamacpp.cppm", + "src/macros.cppm", "third_party/llama.cpp/src/llama.cpp", "third_party/llama.cpp/src/llama-adapter.cpp", "third_party/llama.cpp/src/llama-arch.cpp", @@ -233,6 +215,8 @@ sources = [ cflags = ["-w", "-include", "ggml_build_info.h", "-DGGML_USE_CPU_REPACK"] cxxflags = ["-w", "-include", "ggml_build_info.h", "-DGGML_USE_CPU_REPACK"] flags = [ + { glob = "third_party/llama.cpp/src/**", cxxflags = ["-include", "cstdlib"] }, + { glob = "third_party/llama.cpp/ggml/src/gguf.cpp", cxxflags = ["-include", "cerrno"] }, { glob = "third_party/llama.cpp/ggml/src/ggml-backend-reg.cpp", defines = ["GGML_USE_CPU"] }, { glob = "third_party/llama.cpp/ggml/src/ggml-cpu/**", defines = ["GGML_USE_LLAMAFILE"] }, { glob = "generated/ggml-cpu_cpp.cpp", defines = ["GGML_USE_LLAMAFILE"] }, @@ -336,7 +320,7 @@ flags = [ [feature-deps.backend-vulkan] compat.vulkan = "1.4.357.0" compat.spirv-headers = "1.4.357.0" -compat.vulkan-runtime = "2026.09.07" +compat.vulkan-runtime = "2026.09.11" # shaderc carries glslc, which upstream's generator drives by name and whose # flags are not glslang's. mesa-lavapipe is a Vulkan device that needs no GPU; @@ -362,3 +346,6 @@ flags = [ { glob = "third_party/llama.cpp/ggml/src/ggml-metal/ggml-metal.cpp", cxxflags = ["-include", "memory"] }, { glob = "third_party/llama.cpp/ggml/src/ggml-backend-reg.cpp", defines = ["GGML_USE_METAL"] }, ] + +[package.metadata.mwrap] +upstream-version = "b10069" diff --git a/src/llamacpp.cppm b/src/llamacpp.cppm index 09aca34..49a3d82 100644 --- a/src/llamacpp.cppm +++ b/src/llamacpp.cppm @@ -18,8 +18,8 @@ module; #undef LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY #undef LLAMA_STATE_SEQ_FLAGS_ON_DEVICE -export module llamacpp; +export module mcpplibs.mwrap.llamacpp; +export import mcpplibs.mwrap.llamacpp.macros; #include "gen_exports/required_ggml.inc" #include "gen_exports/llama.inc" -#include "gen_exports/typed_constants.inc" diff --git a/src/macros.cppm b/src/macros.cppm new file mode 100644 index 0000000..67406ac --- /dev/null +++ b/src/macros.cppm @@ -0,0 +1,25 @@ +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 mcpplibs.mwrap.llamacpp.macros; + +namespace mcpplibs::mwrap::llamacpp::macros { +#include "gen_exports/typed_constants.inc" +} diff --git a/templates/cpu/mcpp.toml.in b/templates/cpu/mcpp.toml.in new file mode 100644 index 0000000..9077430 --- /dev/null +++ b/templates/cpu/mcpp.toml.in @@ -0,0 +1,7 @@ +[package] +name = "{{project.name}}" +version = "0.1.0" +description = "llama.cpp CPU quickstart" + +[dependencies] +"{{self.name}}" = "{{self.version}}" diff --git a/templates/cpu/src/main.cpp.in b/templates/cpu/src/main.cpp.in new file mode 100644 index 0000000..5ee848c --- /dev/null +++ b/templates/cpu/src/main.cpp.in @@ -0,0 +1,36 @@ +import std; +import mcpplibs.mwrap.llamacpp; + +struct Backend { + Backend() { llama_backend_init(); } + ~Backend() { llama_backend_free(); } +}; + +int main(int argc, char** argv) { + // Destroy every model/context before releasing the backend. + const Backend backend; + std::println("llama.cpp CPU ready: {}", llama_print_system_info()); + if (argc < 2) { + std::println("Pass a local GGUF path to decode and sample: mcpp run -- model.gguf"); + return 0; + } + auto params = llama_model_default_params(); + params.n_gpu_layers = 0; + const auto model = std::unique_ptr( + llama_model_load_from_file(argv[1], params), llama_model_free); + if (!model) return 1; + auto context_params = llama_context_default_params(); + context_params.n_ctx = 64; + context_params.n_threads = 2; + const auto context = std::unique_ptr( + llama_init_from_model(model.get(), context_params), llama_free); + if (!context) return 2; + if (llama_vocab_n_tokens(llama_model_get_vocab(model.get())) < 4) return 3; + llama_token tokens[]{1, 2, 3}; + if (llama_decode(context.get(), llama_batch_get_one(tokens, 3))) return 4; + const auto sampler = std::unique_ptr( + llama_sampler_init_greedy(), llama_sampler_free); + const auto sampled = llama_sampler_sample(sampler.get(), context.get(), -1); + if (sampled < 0 || sampled >= llama_vocab_n_tokens(llama_model_get_vocab(model.get()))) return 5; + std::println("CPU decode passed; sampled token {}", sampled); +} diff --git a/templates/cpu/template.toml b/templates/cpu/template.toml new file mode 100644 index 0000000..3336299 --- /dev/null +++ b/templates/cpu/template.toml @@ -0,0 +1,4 @@ +[template] +description = "Initialize llama.cpp on the CPU and optionally decode a local GGUF model" +default = true +post_message = "Run `mcpp run` for CPU initialization, or `mcpp run -- /path/to/model.gguf` for a decode and sample." diff --git a/tests/cpu_decode.cpp b/tests/cpu_decode.cpp index 08fbe44..cbef08a 100644 --- a/tests/cpu_decode.cpp +++ b/tests/cpu_decode.cpp @@ -1,7 +1,7 @@ /// Portable CPU smoke: load a pinned GGUF, decode one batch, and sample. import std; -import llamacpp; +import mcpplibs.mwrap.llamacpp; #ifdef LLAMA_H #error "import llamacpp leaked LLAMA_H" @@ -13,8 +13,8 @@ import llamacpp; int main() { - static_assert(LLAMA_DEFAULT_SEED == 0xFFFFFFFFu); - static_assert(LLAMA_TOKEN_NULL == llama_token{-1}); + static_assert(mcpplibs::mwrap::llamacpp::macros::LLAMA_DEFAULT_SEED == 0xFFFFFFFFu); + static_assert(mcpplibs::mwrap::llamacpp::macros::LLAMA_TOKEN_NULL == llama_token{-1}); const char * model_path = std::getenv("LLAMACPP_TEST_MODEL"); if (!model_path || !*model_path) { diff --git a/tests/macros.cpp b/tests/macros.cpp new file mode 100644 index 0000000..1ed3439 --- /dev/null +++ b/tests/macros.cpp @@ -0,0 +1,3 @@ +import mcpplibs.mwrap.llamacpp; +namespace macros = mcpplibs::mwrap::llamacpp::macros; +int main() { static_assert(macros::LLAMA_DEFAULT_SEED == 0xFFFFFFFFu); static_assert(macros::LLAMA_TOKEN_NULL == -1); return 0; } diff --git a/tests/metal_decode.cpp b/tests/metal_decode.cpp index d41c25c..325db35 100644 --- a/tests/metal_decode.cpp +++ b/tests/metal_decode.cpp @@ -8,7 +8,7 @@ import std; -import llamacpp; +import mcpplibs.mwrap.llamacpp; #ifdef LLAMA_H #error "import llamacpp leaked LLAMA_H" diff --git a/tests/unit/test_build_contract.py b/tests/unit/test_build_contract.py index 3fe20a4..38cb6fb 100644 --- a/tests/unit/test_build_contract.py +++ b/tests/unit/test_build_contract.py @@ -78,7 +78,7 @@ def test_identity_and_public_module_are_cxx23(self): # The namespace names whose library this is (mcpp-index#163); the version # IS the upstream checkpoint, so it must agree with the build info the # manifest generates for ggml. - self.assertEqual(package["namespace"], "ggml-org") + self.assertEqual(package["namespace"], "mcpplibs.mwrap") # THE VERSION AND GGML_VERSION ARE TWO FACTS, AND A WRAPPER REVISION # SEPARATES THEM. The package version may carry a dotted revision for a diff --git a/tests/unit/test_check_release.py b/tests/unit/test_check_release.py index 66e6d27..8fac1cc 100644 --- a/tests/unit/test_check_release.py +++ b/tests/unit/test_check_release.py @@ -48,6 +48,14 @@ def test_tag_matches_package_version_and_lock_mapping(self): "178a6c44937154dc4c4eff0d166f4a044c4fceba", ) + def test_mwrap_first_release_has_independent_tag_namespace(self): + manifest = self.root / "mcpp.toml" + manifest.write_text(manifest.read_text().replace('[package]', '[package]\nnamespace = "mcpplibs.mwrap"')) + identity = check_release.load_identity(self.root, "mwrap-b10069") + self.assertEqual(str(identity.version), "b10069") + with self.assertRaisesRegex(ValueError, "tag.*version"): + check_release.load_identity(self.root, "b10069") + def test_rejects_tag_version_mismatch(self): with self.assertRaisesRegex(ValueError, "tag.*version"): check_release.load_identity(self.root, "b10069.1") diff --git a/tests/unit/test_repository_contract.py b/tests/unit/test_repository_contract.py index b9665fa..1f0efee 100644 --- a/tests/unit/test_repository_contract.py +++ b/tests/unit/test_repository_contract.py @@ -33,7 +33,7 @@ def test_no_submodule_or_consumer_time_upstream_fetch(self): def test_package_and_module_identity(self): manifest = (ROOT / "mcpp.toml").read_text(encoding="utf-8") module = (ROOT / "src/llamacpp.cppm").read_text(encoding="utf-8") - self.assertRegex(manifest, r'(?m)^namespace\s*=\s*"ggml-org"$') + self.assertRegex(manifest, r'(?m)^namespace\s*=\s*"mcpplibs.mwrap"$') self.assertRegex(manifest, r'(?m)^name\s*=\s*"llamacpp"$') # The wrapper revision on an unchanged upstream checkpoint. The # checkpoint the version NAMES is asserted separately below, so a @@ -41,7 +41,7 @@ def test_package_and_module_identity(self): self.assertRegex(manifest, r'(?m)^version\s*=\s*"b10069(\.[1-9]\d*)?"$') lock = (ROOT / "upstream.lock").read_text(encoding="utf-8") self.assertIn('"b10069"', lock) - self.assertIn("export module llamacpp;", module) + self.assertIn("export module mcpplibs.mwrap.llamacpp;", module) def test_vendored_tree_is_not_locally_patched(self): patches = ROOT / "patches" @@ -117,7 +117,7 @@ def test_chat_examples_keep_next_token_alive_and_report_runtime_errors(self): ).read_text(encoding="utf-8") self.assertRegex( source, - r"llama_token\s+sampled\s*=\s*LLAMA_TOKEN_NULL;" + r"llama_token\s+sampled\s*=\s*mcpplibs::mwrap::llamacpp::macros::LLAMA_TOKEN_NULL;" r"[\s\S]*for\s*\([^)]*\)\s*\{" r"[\s\S]*sampled\s*=\s*llama_sampler_sample", ) @@ -184,7 +184,7 @@ def test_policy_workflows_are_report_only_and_tag_gated(self): # why the scheme change did not ship a release workflow that never # fires: it was the FOURTH place `v*` was written down, and the only # one that would have failed loudly on its own. - 'tags: ["b[0-9]*"]', + 'tags: ["mwrap-*"]', "fetch-depth: 0", "python3 -m unittest discover", "tools/check_release.py", diff --git a/tests/vulkan_decode.cpp b/tests/vulkan_decode.cpp index 3edce87..84032d0 100644 --- a/tests/vulkan_decode.cpp +++ b/tests/vulkan_decode.cpp @@ -19,7 +19,7 @@ import std; -import llamacpp; +import mcpplibs.mwrap.llamacpp; #ifdef LLAMA_H #error "import llamacpp leaked LLAMA_H" diff --git a/tools/audit_snapshot.py b/tools/audit_snapshot.py index da47e58..266c60f 100644 --- a/tools/audit_snapshot.py +++ b/tools/audit_snapshot.py @@ -542,7 +542,7 @@ def main() -> int: root = args.source.resolve() if not (root / 'include/llama.h').is_file(): - ap.error(f'ggml-org:llamacpp source tree not found: {root}') + ap.error(f'mcpplibs.mwrap.llamacpp source tree not found: {root}') expected = None if args.check: diff --git a/tools/check_mwrap_release.py b/tools/check_mwrap_release.py new file mode 100644 index 0000000..227fd8d --- /dev/null +++ b/tools/check_mwrap_release.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Check the immutable mwrap release coordinate and bilingual entry pages.""" +import argparse +from pathlib import Path +import re +import tomllib + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--tag', help='release tag; omitted during PR checks') + args = parser.parse_args() + root = Path(__file__).resolve().parents[1] + package = tomllib.loads((root / 'mcpp.toml').read_text(encoding="utf-8"))['package'] + assert package['namespace'] == 'mcpplibs.mwrap', package + assert re.fullmatch(r'[A-Za-z_][A-Za-z_0-9]*', package['name']), package['name'] + upstream = package['metadata']['mwrap']['upstream-version'] + version = package['version'] + assert version == upstream or re.fullmatch(re.escape(upstream) + r'\.[1-9][0-9]*', version), (version, upstream) + if args.tag: + assert args.tag == 'mwrap-' + version, (args.tag, version) + identity = package['namespace'] + '.' + package['name'] + for name, other in [('README.md', 'README.zh.md'), ('README.zh.md', 'README.md')]: + text = (root / name).read_text(encoding="utf-8") + assert len(text.splitlines()) <= 150, name + assert '](' + other + ')' in '\n'.join(text.splitlines()[:12]), name + assert identity in text and version in text, name + assert text.count('```') % 2 == 0, name + print(identity + '@' + version) + + +if __name__ == '__main__': + main() diff --git a/tools/check_native_runtime.py b/tools/check_native_runtime.py new file mode 100644 index 0000000..c648ab7 --- /dev/null +++ b/tools/check_native_runtime.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Verify native ELF artifacts use static or explicitly managed runtimes.""" +import argparse +from pathlib import Path +import re +import subprocess + + +def output(command): + return subprocess.check_output(command, text=True, stderr=subprocess.STDOUT) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--home", required=True, type=Path) + parser.add_argument("--arch", required=True) + parser.add_argument("binaries", nargs="+", type=Path) + args = parser.parse_args() + registry = (args.home / "registry").resolve() + for binary in args.binaries: + binary = binary.resolve() + description = output(["file", str(binary)]) + print(description, end="") + if args.arch not in description: + raise SystemExit(f"Wrong artifact architecture: {binary}") + headers = output(["readelf", "-l", str(binary)]) + interpreter = re.search(r"Requesting program interpreter: ([^\]]+)", headers) + if not interpreter: + if "statically linked" not in description: + raise SystemExit(f"Artifact has neither a managed PT_INTERP nor static linkage: {binary}") + print(f"PT_INTERP absent: {binary}") + continue + loader = Path(interpreter[1]).resolve() + if not loader.is_relative_to(registry) or "xim-x-glibc" not in loader.parts: + raise SystemExit(f"Unmanaged ELF interpreter: {loader}") + dependencies = output([str(loader), "--list", str(binary)]) + print(dependencies, end="") + for path in re.findall(r"(/[^\s()]+)", dependencies): + if not Path(path).resolve().is_relative_to(registry): + raise SystemExit(f"Host library escaped the managed runtime: {path}") + print(f"PT_INTERP and dependency closure are managed: {binary}") + + +if __name__ == "__main__": + main() diff --git a/tools/check_release.py b/tools/check_release.py index 83dd99c..f6a7b56 100644 --- a/tools/check_release.py +++ b/tools/check_release.py @@ -92,7 +92,8 @@ def _identity_from_text(manifest_text: str, lock_text: str, tag: str) -> Release # The tag is the version verbatim, which is also upstream's own tag form # (ggml-org/llama.cpp tags releases `b10069`, not `vb10069`). - if tag != str(identity.version): + prefix = "mwrap-" if manifest["package"].get("namespace") == "mcpplibs.mwrap" else "" + if tag != prefix + str(identity.version): raise ValueError( f"release tag {tag} does not match package version {identity.version}" ) @@ -247,14 +248,15 @@ def _tag_identity(root: Path, tag: str) -> ReleaseIdentity | None: def _previous_identity( root: Path, current: ReleaseIdentity, current_tag: str ) -> ReleaseIdentity | None: - tags = _git(root, "tag", "--list", "v[0-9]*", "--sort=-version:refname") + pattern = "mwrap-b[0-9]*" if current_tag.startswith("mwrap-") else "b[0-9]*" + tags = _git(root, "tag", "--list", pattern, "--sort=-version:refname") if tags.returncode != 0: raise ValueError(f"cannot list release tags: {tags.stderr.strip()}") for tag in tags.stdout.splitlines(): if tag == current_tag: continue try: - version = Version.parse(tag) + version = Version.parse(tag.removeprefix("mwrap-")) except ValueError: continue if version > current.version: diff --git a/tools/gen_exports.py b/tools/gen_exports.py index 42b6f47..0276c2b 100644 --- a/tools/gen_exports.py +++ b/tools/gen_exports.py @@ -565,7 +565,7 @@ def main(argv=None): upstream = Path(args.upstream) if args.upstream else DEFAULT_UPSTREAM_DIR if not args.upstream and not (upstream / "include/llama.h").is_file(): print( - f"ggml-org:llamacpp upstream tree not found: {upstream}", + f"mcpplibs.mwrap.llamacpp upstream tree not found: {upstream}", file=sys.stderr, ) return 1 diff --git a/tools/generate_build_files.py b/tools/generate_build_files.py new file mode 100644 index 0000000..28677cb --- /dev/null +++ b/tools/generate_build_files.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +"""Materialize stable manifest-generated build inputs for path consumers too.""" +import argparse +from pathlib import Path +import tomllib + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + root = Path(__file__).resolve().parents[1] + manifest = tomllib.loads((root / "mcpp.toml").read_text()) + for relative, content in manifest["generated_files"].items(): + path = root / relative + if args.check: + if not path.is_file() or path.read_text() != content: + raise SystemExit(f"Stale or absent build input: {relative}") + else: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content) + print("Stable generated inputs match the manifest") + + +if __name__ == "__main__": + main() diff --git a/tools/test_template.py b/tools/test_template.py new file mode 100644 index 0000000..0a9955c --- /dev/null +++ b/tools/test_template.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Render an installed candidate's default mcpp template, then build and run it.""" +import argparse +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import tomllib + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--mcpp", default="mcpp") + parser.add_argument("--toolchain", default="llvm@23.1.3") + parser.add_argument("--run-arg", action="append", default=[], help="argument passed to the generated application") + args = parser.parse_args() + root = Path(__file__).resolve().parents[1] + metadata = tomllib.loads((root / "mcpp.toml").read_text())["package"] + namespace, name, version = (metadata[key] for key in ("namespace", "name", "version")) + identity = namespace + "." + name + with tempfile.TemporaryDirectory(prefix=name + "-template-") as directory: + temporary = Path(directory) + home = temporary / "home" + package = home / "registry/data/xpkgs" / (namespace + "-x-" + name) / version / (name + "-" + version) + shutil.copytree(root, package, ignore=shutil.ignore_patterns( + ".git", "target", ".mcpp", ".xlings", ".candidate-index", "__pycache__", "mcpp.lock", "compile_commands.json", "mcpp-2026.*")) + index = home / "indices/mcpplibs/pkgs/mwrap" + index.mkdir(parents=True) + entry = '{url="https://example.invalid/candidate.tar.gz",sha256="fixture"}' + descriptor = ('package={spec="1",namespace=' + json.dumps(namespace) + ',name=' + json.dumps(name) + + ',description="Installed template fixture",type="package",mcpp="mcpp.toml",xpm={') + descriptor += ",".join(platform + "={[" + json.dumps(version) + "]=" + entry + "}" + for platform in ("linux", "macosx", "windows")) + "}}\n" + (index / (identity + ".lua")).write_text(descriptor) + env = dict(os.environ, MCPP_HOME=str(home)) + selector = identity + "@" + version + for command in ([args.mcpp, "new", "--list-templates", selector], + [args.mcpp, "new", "quickstart", "--template", selector]): + print("+", " ".join(command), flush=True) + subprocess.run(command, cwd=temporary, env=env, check=True) + project = temporary / "quickstart" + manifest = project / "mcpp.toml" + text = manifest.read_text() + dependency = tomllib.loads(text)["dependencies"][identity] + options = {"version": dependency} if isinstance(dependency, str) else dependency + assert options["version"] == version, options + replacement = '{ path = ' + json.dumps(package.as_posix()) + if options.get("features"): + replacement += ', features = ' + json.dumps(options["features"]) + replacement += ' }' + lines = text.splitlines() + matches = [i for i, line in enumerate(lines) if line.startswith(json.dumps(identity) + " =")] + assert len(matches) == 1, "Expected exactly one qualified dependency from self.name" + lines[matches[0]] = json.dumps(identity) + " = " + replacement + manifest.write_text("\n".join(lines) + "\n") + command = [args.mcpp, "run", "--toolchain", args.toolchain] + if args.run_arg: + command += ["--", *args.run_arg] + print("+", " ".join(command), flush=True) + subprocess.run(command, cwd=project, check=True) + + +if __name__ == "__main__": + main()