diff --git a/README.md b/README.md index 69ffdb7..ae5adeb 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ translations and local XYZW rotations. ## Demo -After building the debug preset and converting the text bundle: +After building the debug preset and downloading the native GGUF bundle: ```sh go run ./demo -addr 0.0.0.0:8094 @@ -61,11 +61,28 @@ Open `http://localhost:8094`. The left sidebar contains the prompt and a persistent history; choosing a previous animation restores its prompt for a new generation. -## Licensed weights +## Weights -The SMPL-X checkpoint and Llama base model are gated. After accepting their -Hugging Face licences and authenticating, download the exact revisions and -hash manifests with: +The ready-to-run native GGUF bundle is published under the Hugging Face +`LocalAI-io` organisation (not GitHub's `localai-org`). Download it directly; +this avoids recreating the conversion locally: + +```sh +nix develop path:. --command scripts/download_gguf_weights.sh --output "$PWD" +``` + +The installer verifies the published manifest and SHA-256 hashes. Use +`--motion-only` when supplying a precomputed 4096-float LLM2Vec embedding. + +The GGUF bundle includes converted Meta Llama 3 material and Kimodo is +non-commercial research-only. Review the published model card and upstream +licences before downloading or redistributing. + +### Regenerating the bundle + +This is only needed to reproduce a conversion. The SMPL-X checkpoint and Llama +base model are gated. After accepting their Hugging Face licences and +authenticating, download the exact revisions and hash manifests with: ```sh nix develop path:. --command hf auth login @@ -79,3 +96,12 @@ Convert the local LLM2Vec model to the native component bundle with: nix develop path:. --command scripts/convert_llm2vec_bundle.sh \ "$PWD/models/llama3-8b-instruct-base" "$PWD/generated/llm2vec-text-bundle" ``` + +Validate a prospective release without network access, then explicitly upload +it from an account allowed to publish to `LocalAI-io`: + +```sh +nix develop path:. --command python scripts/publish_gguf.py +nix develop path:. --command python scripts/publish_gguf.py \ + --upload --confirm-upstream-licences +``` diff --git a/scripts/download_gguf_weights.sh b/scripts/download_gguf_weights.sh new file mode 100755 index 0000000..e7a432b --- /dev/null +++ b/scripts/download_gguf_weights.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Download the published native GGUF bundle into Kimodo's standard paths. +set -euo pipefail +export HF_HUB_DISABLE_PROGRESS_BARS=1 + +ORG="${GGUF_ORG:-LocalAI-io}" +REPO_DEFAULT="$ORG/Llama-3-Kimodo-SMPLX-RP-v1-GGUF" + +usage() { + printf '%s\n' "usage: $0 --output DIR [--repo HF_REPO] [--revision REVISION] [--motion-only]" >&2 + exit 2 +} + +output='' repo="$REPO_DEFAULT" revision='main' motion_only=0 +while [ "$#" -gt 0 ]; do + case "$1" in + --output) [ "$#" -ge 2 ] || usage; output=$2; shift 2 ;; + --repo) [ "$#" -ge 2 ] || usage; repo=$2; shift 2 ;; + --revision) [ "$#" -ge 2 ] || usage; revision=$2; shift 2 ;; + --motion-only) motion_only=1; shift ;; + *) usage ;; + esac +done +[ -n "$output" ] || usage +command -v hf >/dev/null || { echo "hf not found; enter the Nix shell first" >&2; exit 1; } + +mkdir -p "$output" +patterns=("MANIFEST.json" "SHA256SUMS" "models/kimodo-smplx-rp-v1-f32.gguf") +if [ "$motion_only" -eq 0 ]; then + patterns+=("generated/llm2vec-text-bundle/*") +fi +echo "Downloading $repo at $revision into $output" +args=(download "$repo" --revision "$revision" --local-dir "$output") +for pattern in "${patterns[@]}"; do args+=(--include "$pattern"); done +hf "${args[@]}" >/dev/null + +manifest="$output/MANIFEST.json" +[ -f "$manifest" ] || { echo "missing MANIFEST.json from $repo" >&2; exit 1; } +python - "$manifest" "$output" "$motion_only" <<'PY' +import hashlib +import json +import sys +from pathlib import Path + +manifest_path = Path(sys.argv[1]) +output = Path(sys.argv[2]) +motion_only = sys.argv[3] == "1" +root = output.resolve() +manifest = json.loads(manifest_path.read_text(encoding="utf-8")) +if manifest.get("format") != "kimodo-gguf-manifest-v1": + raise SystemExit("unsupported or malformed GGUF manifest") +for entry in manifest.get("files", []): + relative = Path(entry.get("path", "")) + if relative.is_absolute() or ".." in relative.parts or relative.suffix != ".gguf": + raise SystemExit(f"unsafe manifest path: {relative}") + if motion_only and str(relative).startswith("generated/"): + continue + path = root / relative + if not path.is_file() or path.stat().st_size != entry.get("bytes"): + raise SystemExit(f"missing or wrong-sized file: {relative}") + h = hashlib.sha256() + with path.open("rb") as f: + for chunk in iter(lambda: f.read(1 << 20), b""): + h.update(chunk) + if h.hexdigest() != entry.get("sha256"): + raise SystemExit(f"checksum mismatch: {relative}") +print("verified native Kimodo GGUF bundle") +PY diff --git a/scripts/hf/Llama-3-Kimodo-SMPLX-RP-v1-GGUF/NOTICE b/scripts/hf/Llama-3-Kimodo-SMPLX-RP-v1-GGUF/NOTICE new file mode 100644 index 0000000..ea0dfe6 --- /dev/null +++ b/scripts/hf/Llama-3-Kimodo-SMPLX-RP-v1-GGUF/NOTICE @@ -0,0 +1,7 @@ +Meta Llama 3 is licensed under the Meta Llama 3 Community License, Copyright © Meta Platforms, Inc. All Rights Reserved. + +Built with Meta Llama 3. + +Kimodo-SMPLX-RP-v1 source model: NVIDIA. This converted distribution remains +subject to the NVIDIA Internal Scientific Research and Development Model License +and is for non-commercial research use only. diff --git a/scripts/hf/Llama-3-Kimodo-SMPLX-RP-v1-GGUF/README.md b/scripts/hf/Llama-3-Kimodo-SMPLX-RP-v1-GGUF/README.md new file mode 100644 index 0000000..38a9c34 --- /dev/null +++ b/scripts/hf/Llama-3-Kimodo-SMPLX-RP-v1-GGUF/README.md @@ -0,0 +1,66 @@ +--- +license: other +library_name: ggml +tags: + - gguf + - ggml + - text-to-motion + - smplx + - llama-3 +--- + +# Llama-3-Kimodo-SMPLX-RP-v1-GGUF + +Native F32 GGUF conversion of NVIDIA's Kimodo-SMPLX-RP-v1 motion model and +its LLM2Vec text encoder. It is for the `kimodo.cpp` GGML runtime; it is not a +llama.cpp language-model conversion. + +`Kimodo-SMPLX-RP-v1` is restricted to non-commercial research use. Before use +or redistribution, review and comply with the upstream NVIDIA model terms and +the Meta Llama 3 Community License below. This repository does not grant rights +beyond those upstream licences. + +## Install + +From the kimodo.cpp checkout: + +```sh +nix develop path:. --command scripts/download_gguf_weights.sh --output "$PWD" +``` + +This places the motion model at `models/kimodo-smplx-rp-v1-f32.gguf` and text +components under `generated/llm2vec-text-bundle/`, which are the default paths +used by the library and demo. Add `--motion-only` for embedding-only inference. +`SHA256SUMS` and `MANIFEST.json` record every published artifact and its source +revision; the installer verifies the selected files after download. + +## Contents + +- `models/kimodo-smplx-rp-v1-f32.gguf` — Kimodo motion diffusion model (F32) +- `generated/llm2vec-text-bundle/` — tokenizer, embeddings, final norm, and 32 + F32 transformer layers for native LLM2Vec inference + +The text components are split deliberately: kimodo.cpp loads a bounded number +of layers at once for GPU memory control. + +## Provenance + +The conversion is generated by `kimodo.cpp` from these exact upstream commits: + +- NVIDIA Kimodo-SMPLX-RP-v1: `1419ba56b734c48bbafb41fefa84088ca94583b5` +- Meta Llama-3-8B-Instruct: `8afb486c1db24fe5011ec46dfbe5b5dccdb575c2` +- McGill LLM2Vec MNTP adapter: `31474e395ada192e8ed1586db6be79fb3b70c9c0` +- McGill LLM2Vec supervised adapter: `baa8ebf04a1c2500e61288e7dad65e8ae42601a7` + +The two McGill adapters are MIT-licensed. The merged text encoder includes +Meta Llama 3 material. **Built with Meta Llama 3.** + +## Licences and notices + +- Kimodo: [NVIDIA Internal Scientific Research and Development Model License](https://huggingface.co/nvidia/Kimodo-SMPLX-RP-v1) + (non-commercial research only). +- Text base: [Meta Llama 3 Community License](https://huggingface.co/meta-llama/Meta-Llama-3-8B-Instruct). +- LLM2Vec adapters: [MIT](https://huggingface.co/McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp). + +`LICENSE-META-LLAMA-3.txt` and `NOTICE` accompany every published copy with +the required Meta licence and attribution. diff --git a/scripts/publish_gguf.py b/scripts/publish_gguf.py new file mode 100755 index 0000000..4a27955 --- /dev/null +++ b/scripts/publish_gguf.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +"""Publish the reproducible Kimodo GGUF distribution to Hugging Face. + +The default is deliberately a dry run: it validates the exact converter +outputs, prints every path, size and SHA-256, and performs no network I/O. +Use --upload only after reviewing the upstream licence obligations. + +The text bundle contains merged Meta Llama 3 weights. It is therefore kept in +the same repository as the Kimodo motion GGUF and is published under the model +name required by the Meta Llama 3 Community License: + + LocalAI-io/Llama-3-Kimodo-SMPLX-RP-v1-GGUF +""" +from __future__ import annotations + +import argparse +import hashlib +import io +import json +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent.parent +HF_ORG = "LocalAI-io" # Hugging Face organisation; GitHub is localai-org. +DEFAULT_REPO = f"{HF_ORG}/Llama-3-Kimodo-SMPLX-RP-v1-GGUF" +MOTION_NAME = "kimodo-smplx-rp-v1-f32.gguf" +TEXT_NAMES = ( + "tokenizer.gguf", "embedding.gguf", "final-norm.gguf", + *(f"layer-{index:02d}.gguf" for index in range(32)), +) +SOURCE_REVISIONS = { + "nvidia/Kimodo-SMPLX-RP-v1": "1419ba56b734c48bbafb41fefa84088ca94583b5", + "meta-llama/Meta-Llama-3-8B-Instruct": "8afb486c1db24fe5011ec46dfbe5b5dccdb575c2", + "McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp": "31474e395ada192e8ed1586db6be79fb3b70c9c0", + "McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp-supervised": "baa8ebf04a1c2500e61288e7dad65e8ae42601a7", +} +CARD = ROOT / "scripts/hf/Llama-3-Kimodo-SMPLX-RP-v1-GGUF/README.md" +NOTICE = ROOT / "scripts/hf/Llama-3-Kimodo-SMPLX-RP-v1-GGUF/NOTICE" +LLAMA_LICENSE = ROOT / "models/llama3-8b-instruct-base/LICENSE" + + +def digest(path: Path) -> str: + value = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + value.update(chunk) + return value.hexdigest() + + +def require_revision(repo: str) -> None: + revision = ROOT / "models" / { + "nvidia/Kimodo-SMPLX-RP-v1": "Kimodo-SMPLX-RP-v1", + "meta-llama/Meta-Llama-3-8B-Instruct": "llama3-8b-instruct-base", + "McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp": "llm2vec-mntp-adapter", + "McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp-supervised": "llm2vec-adapter", + }[repo] / "REVISION" + if not revision.is_file(): + raise ValueError(f"missing provenance file: {revision}") + actual = revision.read_text(encoding="utf-8").split()[0] + if actual != SOURCE_REVISIONS[repo]: + raise ValueError(f"unexpected {repo} revision: {actual} (expected {SOURCE_REVISIONS[repo]})") + + +def artifacts(motion: Path, bundle: Path) -> list[tuple[Path, str]]: + result = [(motion, f"models/{MOTION_NAME}")] + result.extend((bundle / name, f"generated/llm2vec-text-bundle/{name}") for name in TEXT_NAMES) + for source, destination in result: + if not source.is_file() or source.stat().st_size == 0: + raise ValueError(f"missing or empty GGUF: {source}") + if source.suffix != ".gguf": + raise ValueError(f"not a GGUF: {source}") + with source.open("rb") as handle: + if handle.read(4) != b"GGUF": + raise ValueError(f"invalid GGUF magic: {source}") + if ".." in Path(destination).parts: + raise ValueError(f"unsafe destination: {destination}") + return result + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--motion", type=Path, default=ROOT / "models" / MOTION_NAME) + parser.add_argument("--text-bundle", type=Path, + default=ROOT / "generated/llm2vec-text-bundle") + parser.add_argument("--repo", default=DEFAULT_REPO) + parser.add_argument("--upload", action="store_true", + help="actually create/update the Hugging Face model repo") + parser.add_argument("--confirm-upstream-licences", action="store_true", + help="required with --upload; confirms authority to redistribute all inputs") + args = parser.parse_args() + + try: + if not CARD.is_file() or not NOTICE.is_file() or not LLAMA_LICENSE.is_file(): + raise ValueError("model card, NOTICE, or Meta Llama 3 licence is missing") + for repo in SOURCE_REVISIONS: + require_revision(repo) + files = artifacts(args.motion, args.text_bundle) + except ValueError as error: + print(f"error: {error}", file=sys.stderr) + return 1 + + entries = [] + for source, destination in files: + entries.append({"path": destination, "bytes": source.stat().st_size, "sha256": digest(source)}) + manifest = { + "format": "kimodo-gguf-manifest-v1", + "repository": args.repo, + "source_revisions": SOURCE_REVISIONS, + "files": entries, + } + sums = "".join(f"{entry['sha256']} {entry['path']}\n" for entry in entries) + total = sum(entry["bytes"] for entry in entries) + + print(f"repo: https://huggingface.co/{args.repo}") + print(f"files: {len(entries)} GGUFs, {total / 1e9:.2f} GB") + for entry in entries: + print(f" {entry['sha256']} {entry['bytes']:>12} {entry['path']}") + if not args.upload: + print("\n[dry-run] nothing uploaded. Re-run with --upload --confirm-upstream-licences to publish.") + return 0 + if not args.confirm_upstream_licences: + print("error: --upload requires --confirm-upstream-licences", file=sys.stderr) + return 2 + + from huggingface_hub import HfApi + api = HfApi() + api.create_repo(args.repo, repo_type="model", exist_ok=True) + uploads = [(CARD, "README.md"), (NOTICE, "NOTICE"), + (LLAMA_LICENSE, "LICENSE-META-LLAMA-3.txt")] + for source, destination in uploads + files: + print(f"uploading {destination} ...", flush=True) + api.upload_file(path_or_fileobj=str(source), path_in_repo=destination, + repo_id=args.repo, repo_type="model", + commit_message=f"Add {destination}") + for payload, destination in ((json.dumps(manifest, indent=2, sort_keys=True).encode() + b"\n", "MANIFEST.json"), + (sums.encode(), "SHA256SUMS")): + print(f"uploading {destination} ...", flush=True) + api.upload_file(path_or_fileobj=io.BytesIO(payload), path_in_repo=destination, + repo_id=args.repo, repo_type="model", + commit_message=f"Add {destination}") + print(f"done -> https://huggingface.co/{args.repo}") + return 0 + + +if __name__ == "__main__": + sys.exit(main())