Research: kimodo.cpp probe (Q8 GGML encoder, MNTP adapter missing from Vili's vectors)

Measured on Yggdrasil CPU; Raven GPU timing deferred (household using the GPU).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-23 02:20:44 +02:00
commit 4a4c8ea5dd

View file

@ -0,0 +1,161 @@
# kimodo.cpp probe, 2026-10-23
Directed by `motion-pipeline:ruling:remap-against-community-tooling`. Shaped by
`motion-pipeline:ruling:own-implementation-borrow-best`: kimodo.cpp is weighed as a
source of parts for Vili, not as a replacement worker. Measured by
imagination-kimodo-cpp, session sp-reentry-2026-10-21.
## Headline
1. **Vili's text vectors lack the LLM2Vec MNTP adapter.** This also holds for
upstream Kimodo at 6bb5848 and at origin/main e1c219c. Under transformers 5.1.0,
Kimodo's `LLM2Vec.from_pretrained("McGill-NLP/...-mntp")` reports the MNTP LoRA
keys as UNEXPECTED (`base_model.model.layers.*`) and MISSING (`layers.*`, newly
initialized). The merge that follows therefore merges a freshly initialized
adapter. Two torch assemblies were run on the same machine with the same
versions (torch 2.5.1 CPU, transformers 5.1.0, peft 0.18.1, bf16):
- **A**, Kimodo's own `LLM2VecEncoder` (the path Vili's own loader mirrors),
reproduces Vili's stored vectors at cosine **0.999972** (fist) and **0.999968**
(wave).
- **B** loads the Llama base, merges the MNTP adapter explicitly with
`PeftModel` and then adds the supervised adapter. This is LLM2Vec's design and
kimodo.cpp's reference assembly. It reproduces kimodo.cpp BF16 at **0.999946**
and **0.999960**.
- A and B agree only at cosine **0.906 to 0.936**.
This answers the probe asked for by `motion-pipeline:follow_up:encoder-adapter-load-report`.
Upstream has not changed `kimodo/model/llm2vec`, `load_model.py` or the
transformers pin since 6bb5848. The probe cannot tell which encoder NVIDIA's
denoiser was trained on: A, or a correctly loaded B from an older transformers
version.
2. **kimodo.cpp's encoder is numerically B.** Over Vili's 8 cached prompts, its Q8_0
output has cosine **0.9075 to 0.9354** with Vili's bf16 GPU vectors, while its BF16
output has **0.9059 to 0.9361**. The whole gap is the
MNTP difference above, not quantization: Q8_0 against kimodo.cpp BF16 is
**0.99967 minimum, 0.99978 mean**, and Q8_0 against torch assembly B is **0.99972**
(fist) and **0.99984** (wave). Upstream's own sweep reports 0.999824.
3. **Clips cannot be compared against Vili's reference clips for the same seed.** This is a
finding, not a gap in the run. kimodo.cpp draws its initial noise with
`std::mt19937_64` and `std::normal_distribution`. Kimodo uses torch's generator
after `torch.manual_seed` and `torch.cuda.manual_seed_all`, on CUDA. Beyond that,
kimodo.cpp does not run Kimodo's post-processing (Vili runs it by default), it
returns 30 SOMA joints rather than the 77-joint posed skeleton Vili stores, and
its CLI fixes both CFG weights at 2.0. Its fixture tools
(`kmd-sample-embedding`, `kmd-sample-fixture`) accept injected embedding and
noise. A like-for-like clip comparison would need Kimodo's CUDA noise captured
on Raven's GPU and post-processing disabled on both sides. That was not done,
because Raven's GPU was in use (below).
## Clip deltas inside kimodo.cpp (same noise bytes, embedding swapped)
Setup: SOMA-RP v1.1 F32 denoiser, 60 frames (2 s at 30 fps), 20 DDIM steps, CFG
2/2. The noise came from numpy `default_rng(seed)`. Positions come from forward
kinematics over kimodo.cpp's `soma30` offsets. Bone-length spread was at most
3e-8 m, and a rerun with the same embedding was 0.0000 mm from the first run,
so the sampler is deterministic.
| prompt | seed | Q8_0 vs kimodo.cpp BF16, mean / max mm | Vili bf16 vector vs kimodo.cpp Q8_0, mean / max mm |
|---|---|---|---|
| fist | 7 | 2.87 / 62.30 | 84.72 / 927.40 |
| fist | 42 | 2.45 / 16.46 | 43.94 / 986.76 |
| shrug | 7 | 2.06 / 17.39 | 28.42 / 145.01 |
| shrug | 42 | 3.00 / 17.83 | 16.46 / 157.28 |
| wave | 7 | 0.54 / 10.87 | 14.06 / 391.16 |
| wave | 42 | 0.94 / 20.84 | 27.70 / 553.69 |
How to read the columns:
- **Q8 against BF16** is about the size of Vili's own GPU-bf16 against CPU drift
(fist: mean 4.63, max 54.28 mm, `motion-pipeline:verdict:cut-vili-encoder-placement.s1`).
- **MNTP against no MNTP** produces a different motion. The fist clip's root ends up
close to a metre away.
## Resources
None of these runs touched Raven's GPU. At the time of the probe Raven's GPU showed
35 % utilization and 11.6 GiB in use, so **GPU (Vulkan) timing is undone**. It is
deferred to a run when the household tray toggle allows GPU use.
All runs were on Yggdrasil (AMD EPYC-Genoa VM, 16 vCPU, 62 GiB) in Docker
containers capped at 8 CPUs and 24 GiB (torch: 40 GiB), with `KIMODO_THREADS=8`.
The host was shared with other agents' verification containers, so the timings are
noisy.
| what | wall | peak RSS |
|---|---|---|
| kimodo.cpp Q8_0 encode, cold process (load and 1st encode) | 6.8 to 14 s (27 s on the very first, cold page cache) | 1.97 to 2.12 GiB |
| kimodo.cpp Q8_0 encode, warm 2nd encode in-process | 3.2 to 7.8 s | the same |
| kimodo.cpp BF16 encode, cold process (warm in-process encode 8.2 to 12.5 s) | 16.6 to 38 s | 3.45 to 3.60 GiB |
| kimodo.cpp 2 s clip from embedding (`kmd-sample-embedding`, 20 steps, includes the 1.1 GB denoiser load) | 26 to 75 s, typically about 30 s | 1.13 GiB |
| kimodo.cpp text to 2 s clip end to end (`kmd-generate`, Q8_0) | 42.9 s | 2.05 GiB |
| torch assembly A or B, bf16 CPU, model load / encode | 41 s or 38 s / 0.9 to 2.1 s | about 16 GiB of weights (peak not sampled) |
For comparison, today on Raven: a cache hit costs about 2 s for a 2 s clip, and a
miss costs a 61 s encode (`cut-vili-encoder-placement.s1`). Raven has 16 GiB of host
RAM, which is why its CPU bf16 encode took 428 s; Yggdrasil's 1 to 2 s CPU bf16
encode is a RAM fact, not a CPU fact. kimodo.cpp's Q8 encoder reads its weights
layer by layer and needs about 2 GiB resident, so it would fit Raven on CPU.
## Feature coverage (kimodo.cpp ed8a931, 2026-10-19)
- Checkpoints: SOMA-RP and SOMA-SEED **v1.1** only, plus G1 and a locally converted
SMPL-X. There is **no v1.2** (`motion-pipeline:follow_up:kimodo-v1-2`).
- Skeleton: the 30 joints SOMA predicts. 77-joint expansion is "not implemented yet".
- Output: raw local rotations and root positions as f32, and skeleton-only
`animation.glb` from the Go demo. There is **no BVH or FBX from the binaries**
(`scripts/export_bvh.py` is a Python helper), and no skinned GLB.
- Constraints: "General constraint input ... not implemented". The C API
carries a `constraint_cfg_weight` but no constraint data. Vili's
`load_constraints_lst` path has no equivalent. Multi-prompt sequences with
transitions are supported.
- Post-processing: Kimodo's motion correction and foot handling do not run in the
inference path.
- Backends: CPU and Vulkan. Windows MSVC builds via the fork TheLocalLab/kimodo.cpp-windows,
merged upstream 2026-08-31.
## Maintenance signal and licence
- **Port.** github.com/localai-org/kimodo.cpp, Apache-2.0, 34 commits from
2026-08-22 to 2026-10-19. Richard Palethorpe wrote 30 of them, with 3 from
Deepesh70 and 1 from mudler-agent. It is published by the LocalAI team, and the
README carries their attribution. It has 929 stars. The last functional commits
were 2026-09-17 (performance work and the monolithic quantizations); since then
there has been one README commit. GGML is a pinned submodule. The code is built
for parity work: reference capture harness, layer, stage and fixture parity
tests, fuzzers and sanitizer presets.
- **Installer defect seen.** `scripts/download_gguf_weights.sh` exits with
"missing or wrong-sized file: generated/llm2vec-text-bundle/tokenizer.gguf",
because MANIFEST.json still lists the legacy split bundle. The monolith files
were verified by hand against the manifest's sha256 values:
- Q8_0 `b26d0e74...4c5c` (8,142,058,752 bytes)
- BF16 `d9a60017...b20f` (15,177,675,936 bytes)
- tokenizer `81614aca...607e`
- **Quantized weights.**
- HF `LocalAI-io/Llama-3-Kimodo-GGML` was created 2026-08-23, last modified
2026-09-17, has 161,096 downloads, and is licensed "other". It was quantized by
LocalAI-io with their own `kmd-quantize-text` (Q8_0 for the large matrices;
RMSNorm in BF16 and the supervised LoRA in F32, unquantized).
- It was converted from exactly the revisions in Vili's encoder identity:
`meta-llama/Meta-Llama-3-8B-Instruct@8afb486c`, `McGill-NLP/...-mntp@31474e39`
and `...-mntp-supervised@baa8ebf0`. Its manifest records them.
- It carries the Meta Llama 3 Community License, and the LLM2Vec adapters are MIT.
- The SOMA-RP v1.1 GGUF (`kimodo-soma-rp-v1.1-f32.gguf`, sha256 `3bf1229f...8ee4`)
is a one-to-one conversion under the NVIDIA Open Model License.
## Method and custody
- **Reference vectors.** The 8 reference vectors came from a read-only copy of
Raven's `E:\Projects\Vili\.vili\embeddings\text-embeddings.cc`, decoded with
cultcache-py and Vili's `gamecult.vili.text_embedding.v0` definition. Every
vectorSha256 checked. kimodo.cpp was fed each record's stored, already
normalized text.
- **Torch assemblies.** These used the McGill adapters and the Llama config,
copied from Raven's WSL Hugging Face cache, and the four Llama safetensors
shards from the ungated mirror `NousResearch/Meta-Llama-3-8B-Instruct`. Those
shards are byte-identical to Raven's blobs: the sha256 values `d8cf9c4d`,
`8d4782b4`, `3acdd690` and `67e9ad31` match. No Hugging Face token left Raven.
- **Nothing on Raven was touched** beyond those reads: not Vili's daemon, its
worker container, or the GPU.
- **Yggdrasil scratch** is `~/scratch/kimodo-cpp-probe` (39 GiB: weights,
checkouts, outputs). The probe's containers are all removed. Delete the scratch
directory once nobody needs to rerun the probe.