Initial Kimodo GGML implementation

This commit is contained in:
Richard Palethorpe 2026-08-22 09:40:35 +01:00
commit 9a15084388
64 changed files with 4925 additions and 0 deletions

41
.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# CMake and generated build products
/build/
/build-*/
/cmake-build-*/
/compile_commands.json
*.o
*.a
*.so
*.so.*
*.dylib
*.dll
*.exe
# Nix and local development state
/result
/result-*
/.direnv/
/.cache/
/.agents/
/.codex/
# Downloaded/derived model and reference assets
/models/*
!/models/MANIFEST.md
/generated/
/demo-output/
*.gguf
*.safetensors
*.pth
*.pt
/dumps/
/fixtures/
# Tooling output
*.profraw
*.profdata
*.gcda
*.gcno
__pycache__/
*.py[cod]
.pytest_cache/

4
.gitmodules vendored Normal file
View File

@ -0,0 +1,4 @@
[submodule "ggml"]
path = ggml
url = https://github.com/ggml-org/ggml.git
branch = master

148
CMakeLists.txt Normal file
View File

@ -0,0 +1,148 @@
cmake_minimum_required(VERSION 3.25)
project(kimodo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
option(KIMODO_BUILD_TESTS "Build Kimodo tests" ON)
option(KIMODO_ENABLE_VULKAN "Enable GGML Vulkan backend when GGML is available" ON)
option(KIMODO_ENABLE_FUZZERS "Build libFuzzer parser targets" OFF)
set(KIMODO_GGML_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ggml" CACHE PATH "Pinned GGML submodule source directory")
set(KIMODO_ROOT_PARITY_MODEL "${CMAKE_CURRENT_SOURCE_DIR}/models/kimodo-smplx-rp-v1-f32.gguf" CACHE FILEPATH "F32 motion GGUF required by parity tests")
set(KIMODO_ROOT_PARITY_FIXTURE "${CMAKE_CURRENT_SOURCE_DIR}/fixtures/smplx-zero-embedding" CACHE PATH "Fixture directory required by motion parity tests")
set(KIMODO_LLM_TEXT_BUNDLE "${CMAKE_CURRENT_SOURCE_DIR}/generated/llm2vec-text-bundle" CACHE PATH "Native LLM2Vec GGUF component bundle required by text parity tests")
set(KIMODO_LLM_TEXT_FIXTURE "${CMAKE_CURRENT_SOURCE_DIR}/fixtures/llm2vec-real-prompt" CACHE PATH "Captured LLM2Vec fixture required by text parity tests")
add_library(kimodo src/capi.cpp src/model.cpp src/gguf.cpp src/diffusion.cpp src/motion_rep.cpp src/motion_decode.cpp)
add_library(kimodo::kimodo ALIAS kimodo)
target_include_directories(kimodo PUBLIC "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>")
target_compile_definitions(kimodo PRIVATE KIMODO_BUILD KIMODO_SHARED)
set_target_properties(kimodo PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN YES)
add_executable(kmd-inspect src/inspect.cpp)
target_link_libraries(kmd-inspect PRIVATE kimodo)
if(MSVC)
target_compile_options(kimodo PRIVATE /W4 /permissive-)
else()
target_compile_options(kimodo PRIVATE -Wall -Wextra -Wpedantic -Wconversion -Wshadow)
endif()
if(EXISTS "${KIMODO_GGML_SOURCE_DIR}/CMakeLists.txt")
# GGML is a pinned submodule: portable outside Nix, patchable, and kept
# current independently of a distribution package.
set(GGML_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(GGML_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(GGML_NATIVE OFF CACHE BOOL "" FORCE)
set(GGML_VULKAN ${KIMODO_ENABLE_VULKAN} CACHE BOOL "" FORCE)
set(GGML_BACKEND_DL OFF CACHE BOOL "" FORCE)
set(BUILD_SHARED_LIBS ON CACHE BOOL "" FORCE)
add_subdirectory("${KIMODO_GGML_SOURCE_DIR}" "${CMAKE_BINARY_DIR}/ggml" EXCLUDE_FROM_ALL)
set(KIMODO_HAVE_GGML TRUE)
target_link_libraries(kimodo PRIVATE ggml)
target_compile_definitions(kimodo PRIVATE KIMODO_HAVE_GGML=1)
if(KIMODO_ENABLE_VULKAN AND TARGET ggml-vulkan)
target_link_libraries(kimodo PRIVATE ggml-vulkan)
target_compile_definitions(kimodo PRIVATE KIMODO_HAVE_GGML_VULKAN=1)
endif()
target_link_libraries(kmd-inspect PRIVATE ggml)
target_compile_definitions(kmd-inspect PRIVATE KIMODO_HAVE_GGML=1)
target_sources(kimodo PRIVATE src/ggml_weights.cpp src/denoiser.cpp)
target_sources(kimodo PRIVATE src/llm_tokenizer.cpp src/llm_text_encoder.cpp)
add_executable(kmd-sample-fixture src/sample_fixture.cpp)
target_sources(kmd-sample-fixture PRIVATE
src/gguf.cpp src/ggml_weights.cpp src/denoiser.cpp src/diffusion.cpp
src/motion_rep.cpp src/motion_decode.cpp)
target_include_directories(kmd-sample-fixture PRIVATE src)
target_link_libraries(kmd-sample-fixture PRIVATE ggml ggml-vulkan)
target_compile_definitions(kmd-sample-fixture PRIVATE KIMODO_HAVE_GGML=1 KIMODO_HAVE_GGML_VULKAN=1)
add_executable(kmd-generate src/generate.cpp)
target_link_libraries(kmd-generate PRIVATE kimodo)
add_executable(kimodo-llm-layer-parity tests/llm_layer_parity.cpp)
target_link_libraries(kimodo-llm-layer-parity PRIVATE ggml ggml-vulkan)
target_include_directories(kimodo-llm-layer-parity PRIVATE src)
target_compile_definitions(kimodo-llm-layer-parity PRIVATE KIMODO_HAVE_GGML_VULKAN=1)
add_executable(kimodo-llm-final-norm-parity tests/llm_final_norm_parity.cpp)
target_link_libraries(kimodo-llm-final-norm-parity PRIVATE ggml ggml-vulkan)
target_compile_definitions(kimodo-llm-final-norm-parity PRIVATE KIMODO_HAVE_GGML_VULKAN=1)
add_executable(kimodo-llm-tokenizer-test tests/llm_tokenizer_test.cpp src/llm_tokenizer.cpp)
target_include_directories(kimodo-llm-tokenizer-test PRIVATE src)
target_link_libraries(kimodo-llm-tokenizer-test PRIVATE ggml)
add_executable(kimodo-llm-embedding-parity tests/llm_embedding_parity.cpp)
target_link_libraries(kimodo-llm-embedding-parity PRIVATE ggml ggml-vulkan)
add_executable(kimodo-llm-text-session-parity
tests/llm_text_session_parity.cpp src/llm_text_encoder.cpp src/llm_tokenizer.cpp)
target_include_directories(kimodo-llm-text-session-parity PRIVATE src)
target_link_libraries(kimodo-llm-text-session-parity PRIVATE ggml ggml-vulkan)
target_compile_definitions(kimodo-llm-text-session-parity PRIVATE KIMODO_HAVE_GGML=1 KIMODO_HAVE_GGML_VULKAN=1)
endif()
include(CTest)
if(BUILD_TESTING AND KIMODO_BUILD_TESTS)
add_executable(kimodo-capi-test tests/capi_test.cpp)
target_link_libraries(kimodo-capi-test PRIVATE kimodo)
add_test(NAME kimodo-capi-test COMMAND kimodo-capi-test "${KIMODO_ROOT_PARITY_MODEL}" "${KIMODO_LLM_TEXT_BUNDLE}")
add_executable(kimodo-diffusion-test tests/diffusion_test.cpp)
target_sources(kimodo-diffusion-test PRIVATE src/diffusion.cpp)
target_include_directories(kimodo-diffusion-test PRIVATE src)
target_link_libraries(kimodo-diffusion-test PRIVATE kimodo)
add_test(NAME kimodo-diffusion-test COMMAND kimodo-diffusion-test)
find_package(Python3 COMPONENTS Interpreter QUIET)
if(Python3_Interpreter_FOUND)
add_test(NAME kimodo-converter-test
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/converter_test.py")
endif()
if(KIMODO_HAVE_GGML)
add_test(NAME kimodo-llm-text-session-cpu
COMMAND "${CMAKE_COMMAND}" -E env KIMODO_BACKEND=cpu "$<TARGET_FILE:kimodo-llm-text-session-parity>" "${KIMODO_LLM_TEXT_BUNDLE}" "${KIMODO_LLM_TEXT_FIXTURE}")
add_test(NAME kimodo-llm-text-session-vulkan
COMMAND kimodo-llm-text-session-parity "${KIMODO_LLM_TEXT_BUNDLE}" "${KIMODO_LLM_TEXT_FIXTURE}")
add_executable(kimodo-root-parity tests/root_parity.cpp src/motion_rep.cpp)
target_include_directories(kimodo-root-parity PRIVATE src)
target_link_libraries(kimodo-root-parity PRIVATE kimodo ggml ggml-vulkan)
add_test(NAME kimodo-root-parity
COMMAND kimodo-root-parity "${KIMODO_ROOT_PARITY_MODEL}" "${KIMODO_ROOT_PARITY_FIXTURE}" root)
add_test(NAME kimodo-body-parity
COMMAND kimodo-root-parity "${KIMODO_ROOT_PARITY_MODEL}" "${KIMODO_ROOT_PARITY_FIXTURE}" body)
add_executable(kimodo-fixture-sampler-parity tests/fixture_sampler_parity.cpp)
target_include_directories(kimodo-fixture-sampler-parity PRIVATE src)
target_link_libraries(kimodo-fixture-sampler-parity PRIVATE kimodo)
add_test(NAME kimodo-fixture-sampler-parity
COMMAND kimodo-fixture-sampler-parity "${KIMODO_ROOT_PARITY_FIXTURE}")
add_executable(kimodo-ggml-weights-test tests/ggml_weights_test.cpp)
target_sources(kimodo-ggml-weights-test PRIVATE src/gguf.cpp src/ggml_weights.cpp)
target_include_directories(kimodo-ggml-weights-test PRIVATE src)
target_link_libraries(kimodo-ggml-weights-test PRIVATE kimodo ggml ggml-vulkan)
target_compile_definitions(kimodo-ggml-weights-test PRIVATE KIMODO_HAVE_GGML=1 KIMODO_HAVE_GGML_VULKAN=1)
add_test(NAME kimodo-ggml-weights-test
COMMAND kimodo-ggml-weights-test "${KIMODO_ROOT_PARITY_MODEL}")
add_executable(kimodo-denoiser-runtime-test tests/denoiser_runtime_test.cpp)
target_sources(kimodo-denoiser-runtime-test PRIVATE src/gguf.cpp src/ggml_weights.cpp src/denoiser.cpp src/motion_rep.cpp src/diffusion.cpp)
target_include_directories(kimodo-denoiser-runtime-test PRIVATE src)
target_link_libraries(kimodo-denoiser-runtime-test PRIVATE kimodo ggml ggml-vulkan)
target_compile_definitions(kimodo-denoiser-runtime-test PRIVATE KIMODO_HAVE_GGML=1 KIMODO_HAVE_GGML_VULKAN=1)
add_test(NAME kimodo-denoiser-runtime-root
COMMAND kimodo-denoiser-runtime-test "${KIMODO_ROOT_PARITY_MODEL}" "${KIMODO_ROOT_PARITY_FIXTURE}" root)
add_test(NAME kimodo-denoiser-runtime-body
COMMAND kimodo-denoiser-runtime-test "${KIMODO_ROOT_PARITY_MODEL}" "${KIMODO_ROOT_PARITY_FIXTURE}" body)
add_test(NAME kimodo-denoiser-runtime-full
COMMAND kimodo-denoiser-runtime-test "${KIMODO_ROOT_PARITY_MODEL}" "${KIMODO_ROOT_PARITY_FIXTURE}" full)
add_executable(kimodo-decode-test tests/decode_test.cpp)
target_sources(kimodo-decode-test PRIVATE src/gguf.cpp src/ggml_weights.cpp src/motion_decode.cpp)
target_include_directories(kimodo-decode-test PRIVATE src)
target_link_libraries(kimodo-decode-test PRIVATE kimodo ggml ggml-vulkan)
target_compile_definitions(kimodo-decode-test PRIVATE KIMODO_HAVE_GGML=1 KIMODO_HAVE_GGML_VULKAN=1)
add_test(NAME kimodo-decode-test COMMAND kimodo-decode-test "${KIMODO_ROOT_PARITY_MODEL}" "${KIMODO_ROOT_PARITY_FIXTURE}")
add_executable(kimodo-generate-smoke tests/generate_smoke.cpp)
target_link_libraries(kimodo-generate-smoke PRIVATE kimodo)
add_test(NAME kimodo-generate-smoke COMMAND kimodo-generate-smoke "${KIMODO_ROOT_PARITY_MODEL}")
add_executable(kimodo-sampler2-test tests/sampler2_test.cpp)
target_sources(kimodo-sampler2-test PRIVATE src/gguf.cpp src/ggml_weights.cpp src/denoiser.cpp src/motion_rep.cpp src/diffusion.cpp)
target_include_directories(kimodo-sampler2-test PRIVATE src)
target_link_libraries(kimodo-sampler2-test PRIVATE kimodo ggml ggml-vulkan)
target_compile_definitions(kimodo-sampler2-test PRIVATE KIMODO_HAVE_GGML=1 KIMODO_HAVE_GGML_VULKAN=1)
endif()
endif()
if(KIMODO_ENABLE_FUZZERS)
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")
message(FATAL_ERROR "KIMODO_ENABLE_FUZZERS requires Clang/libFuzzer")
endif()
add_executable(kimodo-gguf-fuzz fuzz/gguf_fuzz.cpp src/gguf.cpp)
target_include_directories(kimodo-gguf-fuzz PRIVATE include src)
target_compile_options(kimodo-gguf-fuzz PRIVATE -fsanitize=fuzzer,address,undefined)
target_link_options(kimodo-gguf-fuzz PRIVATE -fsanitize=fuzzer,address,undefined)
endif()

11
CMakePresets.json Normal file
View File

@ -0,0 +1,11 @@
{
"version": 6,
"configurePresets": [
{"name":"debug","generator":"Ninja","binaryDir":"${sourceDir}/build/debug","cacheVariables":{"CMAKE_BUILD_TYPE":"Debug"}},
{"name":"release","generator":"Ninja","binaryDir":"${sourceDir}/build/release","cacheVariables":{"CMAKE_BUILD_TYPE":"Release"}},
{"name":"asan-ubsan","inherits":"debug","binaryDir":"${sourceDir}/build/asan-ubsan","cacheVariables":{"CMAKE_CXX_FLAGS":"-O0 -fsanitize=address,undefined -fno-omit-frame-pointer","CMAKE_EXE_LINKER_FLAGS":"-fsanitize=address,undefined"}},
{"name":"fuzz","inherits":"debug","binaryDir":"${sourceDir}/build/fuzz","cacheVariables":{"CMAKE_CXX_COMPILER":"clang++","KIMODO_BUILD_TESTS":"OFF","KIMODO_ENABLE_FUZZERS":"ON"}}
],
"buildPresets":[{"name":"debug","configurePreset":"debug"},{"name":"release","configurePreset":"release"},{"name":"asan-ubsan","configurePreset":"asan-ubsan"}],
"testPresets":[{"name":"debug","configurePreset":"debug","output":{"outputOnFailure":true}},{"name":"asan-ubsan","configurePreset":"asan-ubsan","output":{"outputOnFailure":true}}]
}

137
PORTING.md Normal file
View File

@ -0,0 +1,137 @@
# Kimodo porting plan
## Scope and first checkpoint
Start with `Kimodo-SMPLX-RP-v1`: its 22-joint output is the most direct bridge
to a SkinTokens rig built on an SMPL-X hierarchy. It is an R&D-licensed
checkpoint, so distribution and test-download automation must preserve the
upstream licence gate. The later default-quality target is
`Kimodo-SOMA-RP-v1.1`, which has a different 77-joint motion representation.
The port boundary is deliberately the motion generator. It is not a robot
control policy: Kimodo produces kinematic motion. ProtoMotions or another
tracker is a downstream, optional physics-control stage.
## Upstream graph inventory
For one prompt and one sample, the PyTorch graph is:
```text
prompt
-> LLM2Vec: Llama-3-8B + MNTP/PEFT adapter, modified bidirectional attention
-> [1, 1, 4096] text embedding
-> diffusion loop (100 steps by default)
-> classifier-free guidance calls
-> two-stage denoiser
root TransformerEncoder
global-root -> local-root representation conversion
body TransformerEncoder
-> DDIM update
-> motion representation inverse + optional C++ motion correction
-> local rotations, global rotations, joints, contacts and root motion
```
Each transformer is PyTorch `TransformerEncoder`: pre/post-norm is checkpoint
configured; exact configuration must come from the downloaded `config.yaml`,
not be inferred from the source defaults. The denoiser has ordinary linear
projections, sinusoidal positional/timestep embeddings, multi-head attention,
MLPs, LayerNorm and GELU/ReLU as configured. The global-root conversion,
normalisation statistics, diffusion schedule, CFG batching/order and DDIM
arithmetic are all part of the parity surface.
## Text encoder: reuse, but not unmodified llama.cpp
Kimodo loads `McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp` and the
`...-supervised` PEFT adapter in bfloat16. Upstream modifies Llama attention
to be bidirectional, performs LLM2Vec instruction/token processing and pools
the result into one 4096-wide vector. A normal causal llama.cpp embedding
call will therefore not reproduce Kimodo embeddings, even if tokenizer and
base weights match.
The intended implementation investigation is:
1. Verify whether the current GGML Llama graph can expose a non-causal mask
and exact mean pooling for this architecture.
2. Merge or apply the PEFT adapter during conversion, and prove the merged
tensors against PyTorch.
3. Capture and test tokens, attention mask, final hidden states, pooled
embedding, then the denoiser text projection separately.
Do not convert the 8B encoder until this test proves that an exact
bidirectional GGML graph is available. A temporary reference service may
produce cached `[1,1,4096]` embeddings while the motion denoiser is ported.
## VRAM decision
The upstream README's approximately 17 GB all-GPU figure is primarily the
8B bfloat16 LLM2Vec text encoder. It states that putting that encoder on CPU
reduces GPU use below 3 GB, which bounds the denoiser/runtime allocation in
their tested configuration. The current upstream process loads the encoder
and denoiser independently and does not unload the encoder after encoding.
`kimodo.cpp` should support serial GPU use:
```text
load text encoder -> encode prompt -> copy/cache 4096 floats -> unload encoder
load denoiser -> diffusion sample -> export motion
```
This exchanges latency and model reloads for low peak VRAM. It must be
benchmarked after parity work; it is not an assumption that both weight sets
fit alongside the allocator workspace on every GPU.
## Reference fixtures and validation order
Use upstream's bundled demo folders as immutable input cases. Initially use
the unconstrained cases:
- `kimodo-soma-rp/01_single_text_prompt` — seed 42, 5 seconds, 100 steps.
- `kimodo-g1-rp/01_single_text_prompt` — seed 43, 5 seconds, 100 steps.
The matching `meta.json` and generated `motion.npz` are already included in
the upstream checkout. Once the model checkpoints are locally available,
capture a fresh PyTorch result using the same metadata, record package/model
revisions and compare the supplied output separately (it may originate from a
different release).
Fixtures must contain self-describing tensors and metadata, using a safe
format such as NPZ plus JSON or GGUF. They should be generated in this order:
1. tokenizer IDs, text attention mask, LLM final states and pooled 4096-vector;
2. denoiser root-model input/output at one fixed diffusion timestep;
3. global-root-to-local-root conversion;
4. denoiser body-model input/output;
5. CFG combined clean prediction;
6. one DDIM update;
7. all sampling steps and motion-representation inverse;
8. optional motion correction, tested independently of neural inference.
Use fixed CPU reference tensors for primitive layer tests and CUDA tensors for
end-to-end fixtures. Randomness needs an explicit generator and captured
initial noise; matching a seed alone is not sufficient across frameworks.
## Project requirements
The eventual implementation follows the established sibling-project pattern:
- GGUF conversion reads only safetensors; reject pickle checkpoints by
default. If upstream ships a legacy `.pt`, require an isolated trusted
reference conversion that writes safe tensors atomically.
- A pinned Docker reference environment mounts the upstream checkout and
checkpoints read-only and writes fixtures only to the project workspace.
- A Nix development flake pins GGML, provides CPU and Vulkan backends, tests,
Clang ASan/UBSan and libFuzzer builds.
- Every public C entry point catches exceptions and validates all pointer,
length, dimension and finite-float inputs before allocation or graph build.
- Fuzz GLB/NPZ import, GGUF metadata/tensor layouts, prompt UTF-8, constraints
JSON, and motion-export/retarget data; do not fuzz model compute with
arbitrary unbounded dimensions.
## Milestones
1. Reference container, model downloader with hashes/licence notes, and a
capture script.
2. Motion-only GGUF converter and root/body transformer layer parity.
3. Diffusion/CFG/motion-representation parity and skeletal GLB export.
4. LLM2Vec bidirectional encoder port or a separately versioned GGML extension.
5. Complete C API, sanitizer/fuzzer suite, benchmark and local web demo.

81
README.md Normal file
View File

@ -0,0 +1,81 @@
# kimodo.cpp
GGML/C++ implementation of NVIDIA's Kimodo text-to-motion model.
## Status
`Kimodo-SMPLX-RP-v1` accepts either a UTF-8 prompt or a precomputed LLM2Vec
embedding and generates unconstrained SMPL-X22 local rotations and root
translations on CPU or Vulkan. The text encoder uses eight-layer Vulkan chunks
by default; set `KIMODO_TEXT_LAYER_CHUNK=1..32` to tune VRAM use.
Included: checked GGUF loading, safetensors conversion, DDIM sampling, C/C++
APIs, CPU/Vulkan parity tests, and a local text-to-motion demo. Constraints,
SOMA, G1, GLB export, and quantised models are not implemented yet.
## Build and test
GGML is a pinned Git submodule:
```sh
git submodule update --init --recursive
nix develop path:. --command cmake --preset debug
nix develop path:. --command cmake --build --preset debug
nix develop path:. --command ctest --preset debug
```
The standard test suite requires the local motion GGUF, text bundle, and
fixtures. It never downloads weights by itself. `release`, `asan-ubsan`, and
`fuzz` presets are also available.
For sanitizer work:
```sh
nix develop path:. --command cmake --preset asan-ubsan
nix develop path:. --command cmake --build --preset asan-ubsan
nix develop path:. --command env \
LD_LIBRARY_PATH="$PWD/build/asan-ubsan/ggml/src:$PWD/build/asan-ubsan/ggml/src/ggml-vulkan:$LD_LIBRARY_PATH" \
ASAN_OPTIONS=detect_leaks=0:abort_on_error=1 UBSAN_OPTIONS=print_stacktrace=1 \
ctest --preset asan-ubsan --output-on-failure
```
Leak detection is disabled because Vulkan loader/driver allocations are global
to the process. The GGUF parser fuzzer requires Clang.
## API
`include/kimodo/kimodo_capi.h` is the C API. Model loading checks the motion
GGUF and text bundle before inference. Use `kimodo_generate_embedding` for
4096 F32 values or `kimodo_generate` for text. Both return SMPL-X22 root
translations and local XYZW rotations.
## Demo
After building the debug preset and converting the text bundle:
```sh
go run ./demo -addr 0.0.0.0:8094
```
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
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
nix develop path:. --command scripts/download_weights.sh \
--output "$PWD/models" --with-text
```
Convert the local LLM2Vec model to the native component bundle with:
```sh
nix develop path:. --command scripts/convert_llm2vec_bundle.sh \
"$PWD/models/llama3-8b-instruct-base" "$PWD/generated/llm2vec-text-bundle"
```

17
demo/index.html Normal file
View File

@ -0,0 +1,17 @@
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Kimodo text to motion</title>
<style>
:root{color-scheme:dark;font-family:Inter,system-ui,sans-serif;background:#10131b;color:#f2f5fb}body{margin:0;overflow:hidden;background:#10131b}main{width:100vw;height:100vh;display:grid;grid-template-columns:360px minmax(0,1fr)}.sidebar{padding:24px 18px;display:flex;flex-direction:column;gap:18px;overflow:auto;background:radial-gradient(circle at 0 0,#293b62,transparent 32rem),#141a27;border-right:1px solid #2b354b}h1{font-size:2rem;letter-spacing:-.05em;margin:0}.eyebrow{font-size:.7rem;text-transform:uppercase;letter-spacing:.14em;color:#9bb8ff;margin-bottom:8px}.card{background:#171c28dd;border:1px solid #2b354b;border-radius:14px;overflow:hidden}.stage{min-width:0;min-height:0;display:flex;flex-direction:column;background:#0c1019}canvas{display:block;width:100%;height:100%;flex:1;min-height:0;background:linear-gradient(#111b31,#0c1019);cursor:grab;touch-action:none}canvas.dragging{cursor:grabbing}.controls{padding:14px 18px;display:flex;align-items:center;gap:10px;flex-wrap:wrap;border-top:1px solid #2b354b}.promptbox{padding:16px;display:grid;gap:10px}.promptbox textarea{min-height:110px;resize:vertical}textarea,input{box-sizing:border-box;width:100%;border:1px solid #35415b;border-radius:10px;background:#0d121d;color:#f2f5fb;padding:10px;font:inherit}button{border:0;border-radius:999px;padding:10px 16px;background:#9bb8ff;color:#10131b;font:inherit;font-weight:700;cursor:pointer}button:disabled{opacity:.5;cursor:wait}.readout,.hint{color:#a9b3c8;font-size:.88rem}.gallery{padding:4px;display:grid;gap:8px;align-content:start}.history{flex:1;min-height:150px;overflow:auto}.gallery h2{font-size:1rem;margin:8px}.item{width:100%;background:#101622;border:1px solid #2c3750;border-radius:12px;padding:11px;text-align:left;color:#e8eefc}.item:hover,.item.active{border-color:#9bb8ff;background:#19233a}.item p{margin:0 0 6px;font-size:.88rem;line-height:1.35}.status{font-size:.75rem;color:#a9b3c8}.error{color:#ffacac;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.camera{flex:1 1 100%;color:#a9b3c8;font-size:.82rem}.progress{display:inline-flex;align-items:center;gap:7px;color:#d9e5ff;font-weight:600}.progress::before{content:'';width:8px;height:8px;border-radius:50%;background:#9bb8ff;box-shadow:0 0 0 0 #9bb8ff;animation:pulse 1.25s infinite}@keyframes pulse{70%{box-shadow:0 0 0 8px #9bb8ff00}}.item.running{border-color:#739cf7}@media(max-width:850px){body{overflow:auto}main{height:auto;min-height:100vh;grid-template-columns:1fr}.sidebar{overflow:visible}.stage{height:min(70vh,700px)}}
</style><main><aside class="sidebar"><header><div class="eyebrow">Kimodo-SMPLX-RP-v1 · Vulkan</div><h1>Text to motion</h1></header><section class="card promptbox"><label for="prompt">Describe a motion</label><textarea id="prompt">A person runs forward and then leaps over an obstacle in front of them.</textarea><button id="generate">Generate motion</button><span id="status" class="readout">Ready</span><div class="hint">One generation runs at a time. Select an animation below to restore and edit its prompt.</div></section><section class="card history"><div class="gallery"><h2>Past animations</h2><div id="items" class="readout">Loading…</div></div></section></aside><section class="stage"><canvas id="view" width="1280" height="720" aria-label="Animated SMPL-X skeleton"></canvas><div class="controls"><button id="play">Pause</button><button id="reset">Reset view</button><input id="frame" type="range" min="0" value="0" step="1"><span id="frameText" class="readout"></span><span class="camera">Drag: rotate · wheel: zoom · Shift/right-drag: pan · double-click: reset</span></div></section></main>
<script>
const parents=[-1,0,0,0,1,2,3,4,5,6,7,8,9,9,9,12,13,14,16,17,18,19];
// SMPL-X22 rest offsets calibrated from the captured upstream posed-joint /
// global-rotation fixture. The generator persists local rotations and root
// translations; these fixed parent-local bone vectors make them viewable here.
const offsets=[[0,0,0],[.052299,-.093936,-.027607],[-.057193,-.106548,-.022218],[-.001496,.11293,-.024981],[.058867,-.416442,-.006557],[-.048074,-.39756,-.014061],[.0069,.145636,-.006859],[-.041738,-.437584,-.029512],[.014489,-.446853,-.01803],[-.010334,.056082,.021116],[.049294,-.065279,.126259],[-.040575,-.065287,.127076],[-.011026,.171365,-.028827],[.047725,.087643,-.008375],[-.046636,.086612,-.014864],[.024654,.175391,.024463],[.126285,.05768,-.013885],[-.109342,.053674,-.009118],[.272907,-.069853,-.039094],[-.292029,-.03544,-.024565],[.276174,.021254,-.002478],[-.271878,-.004835,-.016445]];
const canvas=document.querySelector('#view'),ctx=canvas.getContext('2d'),slider=document.querySelector('#frame'),promptBox=document.querySelector('#prompt'),generate=document.querySelector('#generate'),status=document.querySelector('#status'),items=document.querySelector('#items');let selected,root,rotations,frame=0,playing=true,last=0,animations=[],activeRequest,activeStarted;const view={yaw:-.56,pitch:.28,zoom:2700,panX:0,panY:0};
function reset(){Object.assign(view,{yaw:-.56,pitch:.28,zoom:2700,panX:0,panY:0});draw()}function rotate(q,v){const[x,y,z,w]=q,[vx,vy,vz]=v,tx=2*(y*vz-z*vy),ty=2*(z*vx-x*vz),tz=2*(x*vy-y*vx);return[vx+w*tx+y*tz-z*ty,vy+w*ty+z*tx-x*tz,vz+w*tz+x*ty-y*tx]};function add(a,b){return[a[0]+b[0],a[1]+b[1],a[2]+b[2]]}function multiply(a,b){const[x,y,z,w]=a,[X,Y,Z,W]=b;return[x*W+w*X+y*Z-z*Y,y*W+w*Y+z*X-x*Z,z*W+w*Z+x*Y-y*X,w*W-x*X-y*Y-z*Z]}
function pose(){if(!selected||!root||!rotations)return[];const positions=[],global=[];for(let j=0;j<22;j++){const q=Array.from(rotations.subarray((frame*22+j)*4,(frame*22+j+1)*4)),p=parents[j];if(p<0){global[j]=q;positions[j]=Array.from(root.subarray(frame*3,frame*3+3))}else{global[j]=multiply(global[p],q);positions[j]=add(positions[p],rotate(global[p],offsets[j]))}}return positions}function project([x,y,z]){const rx=x*Math.cos(view.yaw)-z*Math.sin(view.yaw),rz=x*Math.sin(view.yaw)+z*Math.cos(view.yaw),ry=y*Math.cos(view.pitch)-rz*Math.sin(view.pitch),dz=y*Math.sin(view.pitch)+rz*Math.cos(view.pitch)+8;return[canvas.width/2+view.panX+view.zoom*rx/dz,canvas.height*.78+view.panY-view.zoom*ry/dz]}
function draw(){ctx.clearRect(0,0,canvas.width,canvas.height);ctx.strokeStyle='#27334d';ctx.lineWidth=2;for(let i=-7;i<=7;i++){ctx.beginPath();ctx.moveTo(0,canvas.height*.75+i*16);ctx.lineTo(canvas.width,canvas.height*.75+i*16);ctx.stroke()}const p=pose();if(p.length){ctx.strokeStyle='#91afff';ctx.lineWidth=8;ctx.lineCap='round';for(let i=1;i<22;i++){const a=project(p[i]),b=project(p[parents[i]]);ctx.beginPath();ctx.moveTo(a[0],a[1]);ctx.lineTo(b[0],b[1]);ctx.stroke()}ctx.fillStyle='#f0f5ff';for(const x of p){const a=project(x);ctx.beginPath();ctx.arc(a[0],a[1],5,0,Math.PI*2);ctx.fill()}}slider.value=frame;document.querySelector('#frameText').textContent=selected?`frame ${frame+1} / ${selected.frames}`:'No animation selected'}function tick(t){if(playing&&selected&&t-last>1000/30){frame=(frame+1)%selected.frames;last=t;draw()}requestAnimationFrame(tick)}
async function select(a){if(a.status!=='ready')return;selected=a;promptBox.value=a.prompt;status.className='readout';status.textContent=`Selected ${a.id.slice(0,8)} · prompt restored`;[root,rotations]=await Promise.all([fetch(`/api/animations/${a.id}/root.f32`).then(r=>r.arrayBuffer()).then(b=>new Float32Array(b)),fetch(`/api/animations/${a.id}/rotations.f32`).then(r=>r.arrayBuffer()).then(b=>new Float32Array(b))]);frame=0;slider.max=a.frames-1;renderGallery();draw()}function renderGallery(){if(!animations.length){items.textContent='No animations yet.';return}items.replaceChildren(...animations.map(a=>{const b=document.createElement('button');b.className='item '+a.status+(selected?.id===a.id?' active':'');b.disabled=a.status!=='ready';b.innerHTML=`<p>${a.prompt}</p><span class="status">${a.status} · ${a.frames} frames · ${a.diffusion_steps} steps</span>${a.error?`<div class="error">${a.error}</div>`:''}`;b.onclick=()=>select(a);return b}))}function showProgress(){const a=animations.find(a=>a.id===activeRequest);if(!a)return;if(a.status==='ready'){status.className='readout';status.textContent='Generation complete — select it from the gallery to play it.';activeRequest=undefined;generate.disabled=false;return}if(a.status==='failed'){status.className='error';status.textContent=`Generation failed: ${a.error}`;activeRequest=undefined;generate.disabled=false;return}const seconds=Math.max(0,Math.floor((Date.now()-activeStarted)/1000));status.className='readout progress';status.textContent=a.status==='running'?`Generating motion… ${seconds}s elapsed`:`Queued for generation… ${seconds}s elapsed`;generate.disabled=true}async function refresh(){animations=await fetch('/api/animations').then(r=>r.json());renderGallery();showProgress()}generate.onclick=async()=>{const prompt=promptBox.value.trim();if(!prompt)return;generate.disabled=true;status.className='readout progress';status.textContent='Submitting generation…';try{const r=await fetch('/api/generate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({prompt,frames:150,steps:100,seed:0})});if(!r.ok)throw new Error(await r.text());const a=await r.json();activeRequest=a.id;activeStarted=Date.now();await refresh()}catch(e){status.className='error';status.textContent=e.message;generate.disabled=false}};
document.querySelector('#play').onclick=e=>{playing=!playing;e.target.textContent=playing?'Pause':'Play'};slider.oninput=()=>{frame=Number(slider.value);draw()};document.querySelector('#reset').onclick=reset;canvas.addEventListener('dblclick',reset);canvas.addEventListener('contextmenu',e=>e.preventDefault());let drag;canvas.addEventListener('pointerdown',e=>{canvas.setPointerCapture(e.pointerId);drag={x:e.clientX,y:e.clientY,pan:e.button===2||e.shiftKey};canvas.classList.add('dragging')});canvas.addEventListener('pointermove',e=>{if(!drag)return;const dx=e.clientX-drag.x,dy=e.clientY-drag.y;drag.x=e.clientX;drag.y=e.clientY;if(drag.pan){view.panX+=dx;view.panY+=dy}else{view.yaw+=dx*.008;view.pitch=Math.max(-1.25,Math.min(1.25,view.pitch+dy*.008))}draw()});function end(){drag=undefined;canvas.classList.remove('dragging')}canvas.addEventListener('pointerup',end);canvas.addEventListener('pointercancel',end);canvas.addEventListener('wheel',e=>{e.preventDefault();view.zoom=Math.max(350,Math.min(3600,view.zoom*Math.exp(-e.deltaY*.001)));draw()},{passive:false});refresh().then(()=>{const a=animations.find(a=>a.status==='ready');if(a)return select(a);draw()});setInterval(refresh,2500);requestAnimationFrame(tick);
</script></html>

212
demo/main.go Normal file
View File

@ -0,0 +1,212 @@
// Local Kimodo text-to-motion demo. Generation is serialized so one native
// process owns Vulkan at a time, while the persistent gallery stays readable.
package main
import (
"crypto/rand"
"embed"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"sync"
"time"
)
//go:embed index.html
var files embed.FS
type animation struct {
ID string `json:"id"`
Prompt string `json:"prompt"`
Frames int `json:"frames"`
DiffusionSteps int `json:"diffusion_steps"`
Seed uint64 `json:"seed"`
CreatedAt string `json:"created_at"`
Status string `json:"status"`
Error string `json:"error,omitempty"`
Kind string `json:"kind"`
}
type gallery struct {
mu sync.RWMutex
items map[string]*animation
output string
queue chan string
generator, motion, text string
}
func token() string {
b := make([]byte, 8)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return hex.EncodeToString(b)
}
func (g *gallery) save(a *animation) error {
b, err := json.MarshalIndent(a, "", " ")
if err != nil {
return err
}
return os.WriteFile(filepath.Join(g.output, a.ID+".json"), b, 0644)
}
func (g *gallery) list() []*animation {
g.mu.RLock()
defer g.mu.RUnlock()
result := make([]*animation, 0, len(g.items))
for _, item := range g.items {
copy := *item
result = append(result, &copy)
}
sort.Slice(result, func(i, j int) bool { return result[i].CreatedAt > result[j].CreatedAt })
return result
}
func (g *gallery) worker() {
for id := range g.queue {
g.mu.Lock()
item := g.items[id]
item.Status = "running"
_ = g.save(item)
g.mu.Unlock()
dir := filepath.Join(g.output, id)
err := os.MkdirAll(dir, 0755)
if err == nil {
err = os.WriteFile(filepath.Join(dir, "prompt.txt"), []byte(item.Prompt), 0600)
}
if err == nil {
cmd := exec.Command(g.generator, g.motion, g.text, filepath.Join(dir, "prompt.txt"), fmt.Sprint(item.Frames), fmt.Sprint(item.DiffusionSteps), fmt.Sprint(item.Seed), dir)
cmd.Env = append(os.Environ(), "KIMODO_BACKEND=vulkan")
output, runErr := cmd.CombinedOutput()
if runErr != nil {
err = fmt.Errorf("%w: %s", runErr, strings.TrimSpace(string(output)))
}
}
g.mu.Lock()
if err != nil {
item.Status = "failed"
item.Error = err.Error()
} else {
item.Status = "ready"
}
if saveErr := g.save(item); saveErr != nil {
log.Printf("save %s: %v", item.ID, saveErr)
}
g.mu.Unlock()
}
}
func main() {
addr := flag.String("addr", "127.0.0.1:8090", "listen address")
motion := flag.String("motion-model", "models/kimodo-smplx-rp-v1-f32.gguf", "motion GGUF")
text := flag.String("text-bundle", "generated/llm2vec-text-bundle", "native LLM2Vec component directory")
generator := flag.String("generator", "build/debug/kmd-generate", "native text-to-motion command")
output := flag.String("output", "demo-output", "persistent gallery directory")
flag.Parse()
if err := os.MkdirAll(*output, 0755); err != nil {
log.Fatal(err)
}
g := &gallery{items: map[string]*animation{}, output: *output, queue: make(chan string, 32), generator: *generator, motion: *motion, text: *text}
entries, _ := filepath.Glob(filepath.Join(*output, "*.json"))
for _, path := range entries {
b, err := os.ReadFile(path)
if err != nil {
continue
}
var a animation
if json.Unmarshal(b, &a) == nil {
g.items[a.ID] = &a
}
}
go g.worker()
index, err := files.ReadFile("index.html")
if err != nil {
log.Fatal(err)
}
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write(index)
})
mux.HandleFunc("/api/animations", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(g.list())
})
mux.HandleFunc("/api/generate", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.Header().Set("Allow", http.MethodPost)
http.Error(w, "POST required", http.StatusMethodNotAllowed)
return
}
var request struct {
Prompt string `json:"prompt"`
Frames int `json:"frames"`
Steps int `json:"steps"`
Seed uint64 `json:"seed"`
}
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10)).Decode(&request); err != nil {
http.Error(w, "invalid JSON", 400)
return
}
request.Prompt = strings.TrimSpace(request.Prompt)
if request.Prompt == "" || len(request.Prompt) > 4096 {
http.Error(w, "prompt must be 1..4096 bytes", 400)
return
}
if request.Frames == 0 {
request.Frames = 150
}
if request.Steps == 0 {
request.Steps = 100
}
if request.Frames < 1 || request.Frames > 1000 || request.Steps < 1 || request.Steps > 1000 {
http.Error(w, "frames and steps must be 1..1000", 400)
return
}
a := &animation{ID: token(), Prompt: request.Prompt, Frames: request.Frames, DiffusionSteps: request.Steps, Seed: request.Seed, CreatedAt: time.Now().UTC().Format(time.RFC3339), Status: "queued", Kind: "generated"}
g.mu.Lock()
g.items[a.ID] = a
err := g.save(a)
g.mu.Unlock()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
g.queue <- a.ID
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
_ = json.NewEncoder(w).Encode(a)
})
mux.HandleFunc("/api/animations/", func(w http.ResponseWriter, r *http.Request) {
parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/animations/"), "/")
if len(parts) != 2 || (parts[1] != "root.f32" && parts[1] != "rotations.f32") {
http.NotFound(w, r)
return
}
g.mu.RLock()
a := g.items[parts[0]]
g.mu.RUnlock()
if a == nil || a.Status != "ready" {
http.NotFound(w, r)
return
}
name := "root_positions.f32"
if parts[1] == "rotations.f32" {
name = "local_rotations_xyzw.f32"
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Cache-Control", "no-store")
http.ServeFile(w, r, filepath.Join(g.output, a.ID, name))
})
log.Printf("Kimodo text-to-motion demo listening at http://%s", *addr)
log.Fatal(http.ListenAndServe(*addr, mux))
}

301
docs/IMPLEMENTATION.md Normal file
View File

@ -0,0 +1,301 @@
# Implementation sketch
This is the engineering design for a reference-faithful Kimodo inference port.
It intentionally separates the motion denoiser from the LLM2Vec text encoder:
that is both the natural validation boundary and the way to avoid retaining an
8B text model in GPU memory while sampling motion.
## First supported slice
The first shippable slice is `Kimodo-SMPLX-RP-v1`, one prompt, one sample,
unconstrained motion, no post-processing. It produces a 30-FPS sequence of
SMPL-X22 local joint rotations and root translations. It is not dependent on
SkinTokens, GLB, a robotics policy, or a browser demo.
```text
UTF-8 prompt
-> LLM2Vec embedding [1, 1, 4096]
-> two-stage Kimodo denoiser, 100 DDIM iterations
-> normalized motion representation
-> inverse motion representation
-> rotations [T, 22, 3, 3], root translations [T, 3], contacts
```
`Kimodo-SOMA-RP-v1.1` becomes the primary quality/demo checkpoint after that
slice passes; it reuses the denoiser/runtime but supplies SOMA77-specific
metadata and motion-representation data. G1 is a third checkpoint/skeleton
variant, not a control policy build.
## Source layout
```text
include/kimodo/kimodo_capi.h stable flat C ABI
include/kimodo/kimodo.hpp optional safe C++ wrapper
src/
model.{hpp,cpp} GGUF metadata/tensors, lazy model sessions
gguf.{hpp,cpp} checked metadata and tensor lookup helpers
text_encoder.{hpp,cpp} LLM2Vec tokenizer/encoder abstraction
llama_bi.{hpp,cpp} small bidirectional Llama graph and mean pooling
llama3_tokenizer.{hpp,cpp} Llama-3 byte-BPE tokenizer only
denoiser.{hpp,cpp} root/body transformer graphs
transformer.{hpp,cpp} LayerNorm, MHA, MLP, positional/timestep ops
diffusion.{hpp,cpp} schedule, CFG and DDIM update
motion_rep.{hpp,cpp} normalise/inverse/root-local conversion
skeleton.{hpp,cpp} immutable SMPL-X/SOMA/G1 metadata and FK
export.{hpp,cpp} NPZ/JSON initially; GLB later
capi.cpp exception firewall and C ownership rules
cli.cpp kmd-cli
scripts/
convert_motion_to_gguf.py
convert_llm2vec_to_gguf.py
reference/
dump_kimodo_reference.py
dump_text_reference.py
dump_motion_rep_reference.py
tests/ fixtures are optional through environment vars
fuzz/ parsers and public API boundaries only
demo/ local Go server and WebGL motion viewer
```
No source file is shared by the PyTorch reference and the implementation. The
only bridge is versioned, checked test data.
## Model files
Use separate memory-mappable GGUF files.
```text
kimodo-smplx-rp-v1-f32.gguf denoiser + schedule + representation metadata
llm2vec-llama3-8b-bidir-f16.gguf base model, merged adapter and tokenizer
```
The motion GGUF stores:
- architecture: skeleton key, parent array, FPS, input/output dimensions,
root/body dimensions, heads, layers, feed-forward width, activation,
norm order, text-token count and base diffusion-step count;
- normalisation means/stds, rest-pose transforms, joint names and contact-joint
indices;
- diffusion schedule constants or enough configuration to generate and test
them exactly;
- root and body input/output/text/time projections, positional encoding
parameters, each transformer LayerNorm, Q/K/V/O projection and MLP weight;
- any learned motion-representation tensor consumed during inverse conversion.
The converter must record source repository, revision, file hashes, dtype and
conversion program revision in GGUF metadata. F32 is required for initial
parity. F16 and quantised denoisers come only after an F32 end-to-end fixture
passes.
PyTorch `TransformerEncoderLayer` stores fused `in_proj_weight`/bias. The
converter may store that fused layout and slice it at graph construction, or
store named Q/K/V tensors; the latter is easier to validate. Its orientation
must be explicitly transposed for GGML's matrix multiplication convention and
unit-tested per projection.
## Runtime and VRAM lifecycle
The public model handle owns two immutable GGUF descriptions, but it does not
keep both backend-resident at once.
```text
model handle: mmap motion GGUF + mmap text GGUF + CPU metadata
generate(prompt):
1. create/load text session on requested backend
2. tokenize, bidirectional Llama inference, mean-pool -> 4096 floats
3. copy the embedding to host cache; destroy text session/backend buffers
4. create/load motion session on requested backend
5. upload [1,1,4096] embedding; run the complete DDIM loop
6. inverse representation and copy output to caller-owned motion result
```
This is intentionally more conservative than upstream Python, which keeps its
text encoder and denoiser objects alive. It avoids their combined peak GPU
allocation. A later `--keep-text-loaded` option may trade VRAM for latency.
The cache key is SHA-256 of text-model identity, adapter identity, tokenizer
identity and exact UTF-8 prompt. Cached embeddings are only valid for that
identity and are stored as F32. A `--embedding-npz`/C-API embedding input is
also supported for denoiser-only validation and batch production.
## Text encoder design
`LLM2VecEncoder` is an adapter behind this interface:
```cpp
struct text_encoder {
result<encoded_text> encode(std::string_view utf8_prompt) const;
};
struct encoded_text {
std::vector<std::int32_t> token_ids;
std::vector<std::uint8_t> attention_mask;
std::vector<float> pooled; // exactly 4096 values
};
```
The only third-party inference dependency is a pinned GGML/gguf revision, added
as a git submodule (ordinary builds) or a Nix flake input (reproducible builds).
`kimodo.cpp` links directly to `ggml` and `gguf`; it does **not** vendor or link
all of llama.cpp.
Implement the small Llama-3 byte-BPE tokenizer in `llama3_tokenizer.cpp` from
the tokenizer JSON/GGUF metadata: special tokens, Unicode pre-tokenisation,
byte encoding and merge ranks. Its test fixtures are token IDs from upstream
LLM2Vec. We may initially use llama.cpp only as a read-only implementation
reference for edge-case tests, not as a build dependency.
Likewise, `llama_bi.cpp` implements only the Llama components LLM2Vec actually
uses—embedding, RMSNorm, RoPE, Q/K/V/O projections, gated MLP, residual stack,
non-causal attention mask and mean pooling—using raw GGML operations. It does
not include generation, KV-cache, sampling, server, grammar, multimodal or
other llama.cpp subsystems. LLM2Vec modifies ordinary Llama attention to be
bidirectional, applies the MNTP/supervised PEFT adapter and performs pooling,
so wrapping a stock causal llama.cpp runtime would not be exact anyway.
Choose the exact non-causal mask only after text fixtures prove upstream token
preparation. Merge the adapter during conversion, and compare base-plus-
adapter and merged output in PyTorch first. This removes LoRA arithmetic from
production inference.
The motion-port milestone may use externally captured text embeddings. That
is a supported test mode, not a silent Python dependency in the final CLI.
## Denoiser implementation
For one DDIM step, construct two GGML graphs (or one graph with a scheduled
intermediate host conversion):
```text
root graph:
noisy motion -> input projection
text [B,L,4096] -> text projection
sinusoidal timestep -> timestep projection
concatenate [text, time, motion] + positional encoding
TransformerEncoder layers -> root prediction [B,T,global_root_dim]
host/graph conversion:
root global representation -> local-root representation
body graph:
[local root, original body] -> input projection
same text/time prefix + TransformerEncoder layers
-> body prediction [B,T,body_dim]
combine root/body -> CFG result -> DDIM x(t-1)
```
For separated CFG, concatenate the text-conditioned, constraint-conditioned
and unconditional rows exactly as upstream does, run each stage once batched,
then combine the three output chunks. The first slice has no constraints but
must still reproduce the upstream separated-CFG ordering; do not substitute a
regular-CFG shortcut.
Start with F32 model tensors and F32 graph activations. Treat finite-value
checks, dimensions and all mask lengths as untrusted input at the C boundary.
Use a deterministic local PRNG for the initial normal noise; reference tests
consume a stored initial-noise tensor rather than relying on seed agreement.
## Motion representation and export
The motion-representation code is ordinary deterministic math and belongs in
C++, not in the web app. Implement and test it in this order:
1. normalisation/de-normalisation;
2. diffusion/global-root-to-local-root conversion;
3. local rotations and root path to global FK;
4. contact/headings output;
5. optional upstream C++ motion correction as a separately tested library.
The first CLI export is an NPZ-compatible result plus JSON metadata. The
production asset export is a standards-compliant animated GLB with a skin,
joint hierarchy and local quaternion rotation tracks. It must not use morph
targets for skeletal motion.
## Reference and conversion pipeline
All checkpoints are handled in an isolated trusted Python container. The
normal converter consumes safetensors only. If an upstream model uses a
legacy PyTorch pickle checkpoint, the reference container reads it once and
writes a hash-checked safetensors intermediate; C++ and normal conversion
never deserialize pickle.
Capture exact fixtures in increasing order:
| Fixture | C++ test |
|---|---|
| Llama IDs, masks, final states, pooled embedding | tokenizer/text parity |
| root model input/output | root transformer parity |
| global-root to local-root output | motion-representation parity |
| body model input/output | body transformer parity |
| CFG combined clean prediction | CFG parity |
| DDIM `x(t-1)` | sampler parity |
| all diffusion states and decoded motion | full parity |
| postprocessed motion | C++ correction parity |
Fixtures record upstream Git commit, checkpoint tensor hash, model config,
device, PyTorch/CUDA version, prompt, CFG settings, frame count and initial
noise. Tests reject mismatched metadata before comparing arrays. Thresholds
are explicit: start with F32 maximum absolute error and relative L2 limits for
each boundary, then make separate expectations for F16/quantised models.
## C API
The public API remains flat and exception-safe. `kimodo_model_load` accepts
motion/text/adapter GGUF paths and validates their mutually compatible model
identities. `kimodo_generate` accepts UTF-8, `frames`, steps, seed and CFG
weights, returning opaque `kimodo_motion` storage. Borrowed output pointers
remain valid only until `kimodo_motion_free`.
Add before first release:
- `kimodo_generate_embedding(...)` for validation/cache-backed generation;
- versioned `kimodo_generation_options.size` compatibility checks;
- fixed-size caller-provided error buffers plus per-context `last_error`;
- progress callback `(stage, step, total)` for text encoding and diffusion;
- no exceptions across C and no global mutable model state.
The safe C++ API wraps it with `std::expected`; neither API exposes GGML
objects, raw file mappings or backend internals.
## Build, tests and hardening
Use the animate-any-mesh.cpp pattern: a pinned **GGML-only** flake, dynamically
loaded CPU variants plus Vulkan, release hardening, separate Docker reference
image, and a Clang ASan/UBSan/fuzzer preset. The ordinary tests do not download
models; `KMD_REFERENCE_DIR` and `KMD_TEST_GGUF` opt into locally supplied
fixtures/models.
Fuzz targets:
- GGUF header/metadata/tensor dimension validation;
- NPZ fixture and output import bounds;
- UTF-8 prompt/tokenisation boundaries;
- constraints JSON and skeleton/animation export;
- C API null pointers, overflow dimensions, error-buffer sizes and invalid
option-struct versions.
Fuzzing does not call arbitrary model tensors or unbounded diffusion loops.
ASan/UBSan runs fixed tiny fixtures and parser fuzzers; full GPU parity remains
a separate, opt-in integration test.
## Delivery sequence
1. Download the official SMPL-X RP checkpoint, LLM2Vec base and adapter; record
exact revisions/hashes; build upstream reference Docker image.
2. Capture the supplied single-prompt demo fixture and write the safe weight
extraction/converter manifest.
3. Implement GGUF loading plus diffusion/math tests with no neural graph.
4. Convert and implement F32 root/body transformer parity using cached text
embeddings.
5. Implement CFG, full DDIM sampling, inverse representation and NPZ output.
6. Add bidirectional LLM2Vec port, serial GPU residency and text parity.
7. Add SMPL-X skeletal GLB export and SkinTokens retarget test asset.
8. Add SOMA v1.1, C API, sanitizers/fuzzing, benchmark and local web demo.
The demo is deliberately last. It will mirror prior local-first projects:
a localhost-only Go server queues one inference job, stores prompt/options and
motion outputs durably, and a dependency-free WebGL viewer plays the skeleton
and animated rigged GLB side by side. It is a QA surface for exact bundled
examples, not a substitute for layer-level parity tests.

27
flake.lock generated Normal file
View File

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1776169885,
"narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

27
flake.nix Normal file
View File

@ -0,0 +1,27 @@
{
description = "Kimodo GGML C++23 development environment";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9";
outputs = { self, nixpkgs }:
let
systems = [ "x86_64-linux" "aarch64-linux" ];
eachSystem = nixpkgs.lib.genAttrs systems;
in {
devShells = eachSystem (system:
let
pkgs = import nixpkgs { inherit system; };
in {
default = pkgs.mkShell {
# `hf` is supplied by huggingface-hub. It is deliberately a dev
# shell tool, never a build input: model downloads remain explicit
# and licence-gated.
packages = [ pkgs.cmake pkgs.ninja pkgs.clang pkgs.pkg-config pkgs.vulkan-loader pkgs.vulkan-headers pkgs.shaderc pkgs.vulkan-tools pkgs.python3Packages.huggingface-hub pkgs.python3Packages.numpy ];
shellHook = ''
export LD_LIBRARY_PATH="${pkgs.vulkan-loader}/lib:/run/opengl-driver/lib''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
'';
};
fuzz = pkgs.mkShell {
packages = [ pkgs.cmake pkgs.ninja pkgs.clang pkgs.llvm pkgs.pkg-config pkgs.python3Packages.huggingface-hub ];
};
});
};
}

16
fuzz/gguf_fuzz.cpp Normal file
View File

@ -0,0 +1,16 @@
#include "gguf.hpp"
#include <cstddef>
#include <cstdint>
#include <fstream>
#include <string>
extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t *data, std::size_t size) {
// File APIs are intentionally fuzzed through a bounded temporary name in
// the fuzzer harness, not by passing attacker-controlled paths to C API.
if (size > 4 * 1024 * 1024) return 0;
const std::string path = "/tmp/kimodo-gguf-fuzz-input";
{ std::ofstream out(path, std::ios::binary); out.write(reinterpret_cast<const char *>(data), static_cast<std::streamsize>(size)); }
(void) kimodo::detail::read_gguf_header(path);
return 0;
}

1
ggml Submodule

@ -0,0 +1 @@
Subproject commit 8c63e70982c95ceb862e3a1073a2c1beef75d60a

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module kimodo.local/demo
go 1.26

44
include/kimodo/kimodo.hpp Normal file
View File

@ -0,0 +1,44 @@
#pragma once
#include <kimodo/kimodo_capi.h>
#include <array>
#include <cstdint>
#include <expected>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
namespace kimodo {
inline constexpr unsigned embedding_width = 4096;
struct motion_data {
unsigned frames = 0;
unsigned joints = 0;
std::vector<float> local_rotations_xyzw;
std::vector<float> root_positions;
};
class KIMODO_API model {
public:
static std::expected<std::unique_ptr<model>, std::string> load(
std::string_view motion_gguf, std::string_view text_bundle = {});
std::expected<motion_data, std::string> generate_embedding(
const std::array<float, embedding_width> &embedding,
unsigned frames, unsigned steps, std::uint64_t seed,
float text_cfg, float constraint_cfg) const;
std::expected<motion_data, std::string> generate_text(
std::string_view utf8_prompt, unsigned frames, unsigned steps, std::uint64_t seed,
float text_cfg, float constraint_cfg) const;
~model();
model(const model &) = delete;
model &operator=(const model &) = delete;
private:
struct impl;
explicit model(std::unique_ptr<impl> impl);
std::unique_ptr<impl> impl_;
};
} // namespace kimodo

View File

@ -0,0 +1,110 @@
/*
* kimodo_capi.h -- stable C ABI for kimodo.cpp.
*
* All entry points are implemented as an exception firewall. Neural graph
* execution is enabled only after its converted tensors pass reference tests.
*/
#pragma once
#include <stdint.h>
#if defined(KIMODO_SHARED)
# if defined(_WIN32) && !defined(__MINGW32__)
# if defined(KIMODO_BUILD)
# define KIMODO_API __declspec(dllexport)
# else
# define KIMODO_API __declspec(dllimport)
# endif
# else
# define KIMODO_API __attribute__((visibility("default")))
# endif
#else
# define KIMODO_API
#endif
#ifdef __cplusplus
extern "C" {
#endif
#define KIMODO_CAPI_ABI_VERSION 1
typedef struct kimodo_model kimodo_model;
typedef struct kimodo_motion kimodo_motion;
typedef enum kimodo_device {
KIMODO_DEVICE_AUTO = 0,
KIMODO_DEVICE_CPU = 1,
KIMODO_DEVICE_VULKAN = 2,
} kimodo_device;
typedef struct kimodo_runtime_options {
uint32_t size; /* caller sets sizeof(kimodo_runtime_options) */
uint32_t threads; /* 0 selects the runtime default */
kimodo_device device;
const char *backend_dir; /* NULL selects the executable/library directory */
} kimodo_runtime_options;
typedef struct kimodo_generation_options {
uint32_t size; /* caller sets sizeof(kimodo_generation_options) */
uint64_t seed;
uint32_t frames;
uint32_t diffusion_steps;
float text_cfg_weight;
float constraint_cfg_weight;
} kimodo_generation_options;
/* A borrowed, row-major [1, 1, 4096] F32 LLM2Vec embedding. */
typedef struct kimodo_embedding {
const float *data;
uint32_t values; /* must be exactly 4096 */
} kimodo_embedding;
/* Returns KIMODO_CAPI_ABI_VERSION. */
KIMODO_API int kimodo_abi_version(void);
/*
* Load a converted motion model. `text_gguf` is optional only for a
* precomputed-embedding workflow; ordinary prompt generation requires it.
* `text_adapter_gguf` is the merged or separately converted LLM2Vec adapter.
* On failure returns NULL and writes a NUL-terminated reason if `err` permits.
*/
KIMODO_API kimodo_model *kimodo_model_load(
const char *motion_gguf,
const char *text_gguf,
const char *text_adapter_gguf,
const kimodo_runtime_options *options,
char *err,
int err_len);
KIMODO_API void kimodo_model_free(kimodo_model *model);
KIMODO_API const char *kimodo_model_last_error(const kimodo_model *model);
/* Prompt is UTF-8. Returns an owning motion or NULL on failure. */
KIMODO_API kimodo_motion *kimodo_generate(
kimodo_model *model,
const char *prompt,
const kimodo_generation_options *options,
char *err,
int err_len);
/*
* Denoiser-only entry point. This is the first supported integration
* boundary and deliberately does not start Python or load a text runtime.
*/
KIMODO_API kimodo_motion *kimodo_generate_embedding(
kimodo_model *model,
const kimodo_embedding *embedding,
const kimodo_generation_options *options,
char *err,
int err_len);
KIMODO_API void kimodo_motion_free(kimodo_motion *motion);
KIMODO_API int kimodo_motion_frames(const kimodo_motion *motion);
KIMODO_API int kimodo_motion_joints(const kimodo_motion *motion);
/* Borrowed row-major buffers, valid until kimodo_motion_free: [T,J,4], [T,3]. */
KIMODO_API const float *kimodo_motion_local_rotations_xyzw(const kimodo_motion *motion);
KIMODO_API const float *kimodo_motion_root_positions(const kimodo_motion *motion);
#ifdef __cplusplus
}
#endif

55
reference/README.md Normal file
View File

@ -0,0 +1,55 @@
# PyTorch reference harness
This directory is intentionally separate from the C++ build. It captures
trusted upstream tensors before conversion, so a future GGML graph is compared
at each boundary rather than only by subjective motion quality.
Set these paths for your machine. The upstream checkout and Hugging Face cache
remain outside this repository:
```sh
export KIMODO_UPSTREAM_DIR=/path/to/kimodo
export KIMODO_HF_CACHE=/path/to/huggingface-cache
export KIMODO_PROJECT_DIR="$PWD"
docker build -t kimodo-reference:upstream "$KIMODO_UPSTREAM_DIR"
```
Run a bundled upstream demo case, mounting source/checkpoints read-only and
this project writable. The checkpoint downloader must already have populated
the supplied cache and the relevant model licence must have been accepted:
```sh
docker run --rm --gpus all \
-v "$KIMODO_UPSTREAM_DIR:/opt/kimodo:ro" \
-v "$KIMODO_HF_CACHE:/cache:ro" \
-v "$KIMODO_PROJECT_DIR:/work" \
-e HUGGINGFACE_CACHE_DIR=/cache \
-e LOCAL_CACHE=True \
kimodo-reference:upstream \
python /work/reference/dump_kimodo_reference.py \
--upstream /opt/kimodo \
--model kimodo-smplx-rp \
--prompt "A person runs forward and then leaps over an obstacle in front of them." \
--frames 150 --steps 100 --seed 42 \
--output /work/dumps/smplx-single-prompt
```
The first fixture deliberately disables motion post-processing. The C++
MotionCorrection source is a separate algorithm with its own tests; mixing it
into the neural fixture would hide an inference mismatch.
For motion-only graph bring-up, a deterministic zero `[1,1,4096]` embedding
can be used without loading the 8B text model. It is a layer fixture, not a
text-quality result:
```sh
CHECKPOINT_DIR=/models python /work/reference/dump_kimodo_reference.py \
--upstream /opt/kimodo --checkpoint-dir /models --zero-embedding \
--model kimodo-smplx-rp --prompt fixture --frames 8 --steps 1 --seed 42 \
--device cpu --output /work/dumps/smplx-zero-embedding
```
`dump_kimodo_reference.py` writes only NPZ/JSON. It records the first root
and body transformer invocation (including all masks) plus the final sampled
motion. More granular operations should be added one at a time as the GGML
implementation reaches them.

View File

@ -0,0 +1,307 @@
#!/usr/bin/env python3
"""Capture safe Kimodo PyTorch fixtures for GGML layer-parity tests.
The script is run inside the official upstream environment. It keeps upstream
code read-only and writes only an NPZ/JSON pair. The generated fixture covers
unconstrained single-prompt inference; constraints and post-processing are
captured separately so their behaviour cannot mask a denoiser error.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import platform
import sys
import gc
from pathlib import Path
from typing import Any
import numpy as np
import torch
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--upstream", required=True, type=Path, help="Kimodo checkout")
parser.add_argument("--model", default="kimodo-smplx-rp")
parser.add_argument("--prompt", required=True)
parser.add_argument("--frames", required=True, type=int)
parser.add_argument("--steps", default=100, type=int)
parser.add_argument("--seed", required=True, type=int)
parser.add_argument("--device", default="cuda")
parser.add_argument("--checkpoint-dir", type=Path,
help="local directory containing Kimodo-SMPLX-RP-v1; never download at capture time")
parser.add_argument("--zero-embedding", action="store_true",
help="use a deterministic [1,1,4096] zero embedding; enables motion-only fixtures")
parser.add_argument("--text-base", type=Path,
help="local Llama-3 base model; together with both adapters, encodes before loading diffusion")
parser.add_argument("--text-mntp-adapter", type=Path,
help="local LLM2Vec MNTP adapter")
parser.add_argument("--text-supervised-adapter", type=Path,
help="local LLM2Vec supervised adapter")
parser.add_argument("--output", required=True, type=Path)
return parser.parse_args()
def as_numpy(value: Any) -> np.ndarray:
if not isinstance(value, torch.Tensor):
raise TypeError(f"expected tensor, got {type(value)!r}")
return value.detach().to(device="cpu", dtype=torch.float32).contiguous().numpy()
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
class CachedEmbedding:
"""A one-prompt text encoder that keeps the diffusion capture GPU-only.
The real LLM2Vec model is deliberately released before Kimodo is loaded.
This avoids an otherwise unnecessary combined 8B-LLM + diffusion VRAM peak
while retaining the exact F32 embedding used by the upstream denoiser.
"""
def __init__(self, embedding: torch.Tensor, prompt: str):
self.embedding = embedding.detach().to(device="cpu", dtype=torch.float32).contiguous()
self.prompt = prompt
def __call__(self, texts: list[str] | str):
values = [texts] if isinstance(texts, str) else texts
if values != [self.prompt]:
raise RuntimeError("capture cache only supports its recorded single prompt")
return self.embedding.clone(), [1]
def capture_real_embedding(args: argparse.Namespace) -> CachedEmbedding:
paths = [args.text_base, args.text_mntp_adapter, args.text_supervised_adapter]
if any(path is None for path in paths):
raise SystemExit("real text capture requires --text-base, --text-mntp-adapter, and --text-supervised-adapter")
if any(not path.is_dir() for path in paths):
raise SystemExit("each local text model path must be a directory")
from kimodo.model.llm2vec import LLM2Vec # pylint: disable=import-outside-toplevel
from peft import PeftModel # pylint: disable=import-outside-toplevel
# LLM2Vec's upstream preset is base Llama + MNTP LoRA + supervised LoRA.
# Merge only the MNTP stage; the second adapter remains active exactly as
# it is in the upstream Kimodo LLM2VecEncoder.
encoder = LLM2Vec.from_pretrained(
str(args.text_base), peft_model_name_or_path=str(args.text_mntp_adapter),
merge_peft=True, torch_dtype=torch.bfloat16,
)
encoder.model = PeftModel.from_pretrained(encoder.model, str(args.text_supervised_adapter))
embedding = encoder.encode([args.prompt], batch_size=1, show_progress_bar=False, device=args.device)
if tuple(embedding.shape) != (1, 4096):
raise RuntimeError(f"LLM2Vec returned {tuple(embedding.shape)}, expected (1, 4096)")
return CachedEmbedding(embedding[:, None, :], args.prompt)
def main() -> None:
args = parse_args()
upstream = args.upstream.resolve()
if not (upstream / "kimodo").is_dir():
raise SystemExit(f"not a Kimodo checkout: {upstream}")
if args.frames <= 0 or args.steps <= 0:
raise SystemExit("--frames and --steps must be positive")
if args.zero_embedding and any((args.text_base, args.text_mntp_adapter, args.text_supervised_adapter)):
raise SystemExit("--zero-embedding cannot be combined with real text model paths")
if args.checkpoint_dir:
checkpoint = args.checkpoint_dir.resolve()
if not (checkpoint / "Kimodo-SMPLX-RP-v1" / "config.yaml").is_file():
raise SystemExit("--checkpoint-dir must contain Kimodo-SMPLX-RP-v1/config.yaml")
# This is deliberately set only for the reference subprocess. It
# prevents a missing local model from silently falling back to HF.
import os
os.environ["CHECKPOINT_DIR"] = str(checkpoint)
sys.path.insert(0, str(upstream))
from kimodo import load_model # pylint: disable=import-outside-toplevel
torch.manual_seed(args.seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(args.seed)
text_encoder = None
if args.zero_embedding:
class ZeroEmbedding:
def __call__(self, text: list[str] | str):
batch = len(text) if isinstance(text, list) else 1
return torch.zeros((batch, 1, 4096), dtype=torch.float32), [1] * batch
text_encoder = ZeroEmbedding()
elif any((args.text_base, args.text_mntp_adapter, args.text_supervised_adapter)):
text_encoder = capture_real_embedding(args)
# The 8B encoder owns the GPU while encoding. The cached output is
# CPU F32, so release all allocator-owned memory before diffusion.
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
model, resolved_name = load_model(
args.model, device=args.device, return_resolved_name=True, text_encoder=text_encoder
)
root_calls: list[tuple[tuple[torch.Tensor, ...], torch.Tensor]] = []
body_calls: list[tuple[tuple[torch.Tensor, ...], torch.Tensor]] = []
root_layer0: list[torch.Tensor] = []
root_layers: list[list[torch.Tensor]] = [[] for _ in range(16)]
root_layer0_inputs: list[torch.Tensor] = []
root_attention0: list[torch.Tensor] = []
root_norm10: list[torch.Tensor] = []
root_motion_projection: list[torch.Tensor] = []
root_text_projection: list[torch.Tensor] = []
root_timestep_projection: list[torch.Tensor] = []
root_heading_projection: list[torch.Tensor] = []
sampling_inputs: list[torch.Tensor] = []
sampling_outputs: list[torch.Tensor] = []
def capture(calls: list[tuple[tuple[torch.Tensor, ...], torch.Tensor]]):
def hook(_module: torch.nn.Module, inputs: tuple[Any, ...], output: torch.Tensor) -> None:
tensors = tuple(item for item in inputs if isinstance(item, torch.Tensor))
calls.append((tensors, output))
return hook
root_hook = model.denoiser.model.root_model.register_forward_hook(capture(root_calls))
body_hook = model.denoiser.model.body_model.register_forward_hook(capture(body_calls))
root_layer_hook = model.denoiser.model.root_model.seqTransEncoder.layers[0].register_forward_hook(
lambda _module, _inputs, output: root_layer0.append(output)
)
root_layer_hooks = [layer.register_forward_hook(
lambda _module, _inputs, output, index=index: root_layers[index].append(output)
) for index, layer in enumerate(model.denoiser.model.root_model.seqTransEncoder.layers)]
root_layer_input_hook = model.denoiser.model.root_model.seqTransEncoder.layers[0].register_forward_pre_hook(
lambda _module, inputs: root_layer0_inputs.append(inputs[0])
)
root_attention_hook = model.denoiser.model.root_model.seqTransEncoder.layers[0].self_attn.register_forward_hook(
lambda _module, _inputs, output: root_attention0.append(output[0])
)
root_norm1_hook = model.denoiser.model.root_model.seqTransEncoder.layers[0].norm1.register_forward_hook(
lambda _module, _inputs, output: root_norm10.append(output)
)
root_motion_hook = model.denoiser.model.root_model.input_linear.register_forward_hook(
lambda _module, _inputs, output: root_motion_projection.append(output)
)
root_text_hook = model.denoiser.model.root_model.embed_text.register_forward_hook(
lambda _module, _inputs, output: root_text_projection.append(output)
)
root_timestep_hook = model.denoiser.model.root_model.embed_timestep.register_forward_hook(
lambda _module, _inputs, output: root_timestep_projection.append(output)
)
root_heading_hook = model.denoiser.model.root_model.linear_first_heading_angle.register_forward_hook(
lambda _module, _inputs, output: root_heading_projection.append(output)
)
original_denoising_step = model.denoising_step
def capture_denoising_step(*args: Any, **kwargs: Any) -> torch.Tensor:
sampling_inputs.append(args[0].detach().clone())
value = original_denoising_step(*args, **kwargs)
sampling_outputs.append(value.detach().clone())
return value
model.denoising_step = capture_denoising_step
try:
text_features, text_lengths = model.text_encoder([args.prompt])
output = model(
args.prompt,
num_frames=args.frames,
num_denoising_steps=args.steps,
num_samples=1,
cfg_weight=[2.0, 2.0],
cfg_type="separated",
post_processing=False,
return_numpy=True,
progress_bar=lambda values: values,
)
finally:
root_hook.remove()
body_hook.remove()
root_layer_hook.remove()
for hook in root_layer_hooks:
hook.remove()
root_layer_input_hook.remove()
root_attention_hook.remove()
root_norm1_hook.remove()
root_motion_hook.remove()
root_text_hook.remove()
root_timestep_hook.remove()
root_heading_hook.remove()
model.denoising_step = original_denoising_step
if not root_calls or not body_calls or not root_layer0 or any(not values for values in root_layers) or not root_layer0_inputs or not root_attention0 or not root_norm10 or not root_motion_projection or not root_text_projection or not root_timestep_projection or not root_heading_projection:
raise RuntimeError("the denoiser hooks did not observe inference")
# CFG may invoke a transformer more than once per diffusion step. Capturing
# call zero is a stable, fully specified first boundary; later calls remain
# reproducible from the final output and are added as C++ reaches CFG.
root_inputs, root_output = root_calls[0]
body_inputs, body_output = body_calls[0]
# This is the exact stage boundary used to construct body_input_0: it is
# deliberately captured separately so C++ can validate the representation
# conversion independently of either Transformer graph.
root_lengths = root_inputs[1].sum(-1)
root_local = model.denoiser.model.motion_rep.global_root_to_local_root(
root_output, normalized=True, lengths=root_lengths
)
if not sampling_inputs or len(sampling_inputs) != len(sampling_outputs):
raise RuntimeError("the sampler trajectory hook did not observe inference")
tensors: dict[str, np.ndarray] = {
"text_features": as_numpy(text_features),
"text_lengths": np.asarray(text_lengths, dtype=np.int64),
"root_output": as_numpy(root_output),
"root_local": as_numpy(root_local),
"body_output": as_numpy(body_output),
"root_layer0_output": as_numpy(root_layer0[0]),
"root_layer0_input": as_numpy(root_layer0_inputs[0]),
"root_attention0_output": as_numpy(root_attention0[0]),
"root_norm10_output": as_numpy(root_norm10[0]),
"root_motion_projection": as_numpy(root_motion_projection[0]),
"root_text_projection": as_numpy(root_text_projection[0]),
"root_timestep_projection": as_numpy(root_timestep_projection[0]),
"root_heading_projection": as_numpy(root_heading_projection[0]),
"sampling_initial_noise": as_numpy(sampling_inputs[0]),
"sampling_final_state": as_numpy(sampling_outputs[-1]),
}
for index, values in enumerate(root_layers):
tensors[f"root_layer{index}_output"] = as_numpy(values[0])
for prefix, inputs in (("root", root_inputs), ("body", body_inputs)):
for index, value in enumerate(inputs):
tensors[f"{prefix}_input_{index}"] = as_numpy(value)
for key, value in output.items():
if isinstance(value, np.ndarray):
tensors[f"motion_{key}"] = value
for index, (sample_in, sample_out) in enumerate(zip(sampling_inputs, sampling_outputs)):
tensors[f"sampling_input_{index}"] = as_numpy(sample_in)
tensors[f"sampling_output_{index}"] = as_numpy(sample_out)
args.output.parent.mkdir(parents=True, exist_ok=True)
np.savez_compressed(args.output.with_suffix(".npz"), **tensors)
metadata = {
"fixture_format": 1,
"upstream": str(upstream),
"resolved_model": resolved_name,
"prompt": args.prompt,
"frames": args.frames,
"diffusion_steps": args.steps,
"seed": args.seed,
"cfg_type": "separated",
"cfg_weight": [2.0, 2.0],
"post_processing": False,
"device": args.device,
"checkpoint_dir": str(args.checkpoint_dir) if args.checkpoint_dir else None,
"zero_embedding": args.zero_embedding,
"torch": torch.__version__,
"cuda": torch.version.cuda,
"python": platform.python_version(),
"root_call_count": len(root_calls),
"body_call_count": len(body_calls),
"sampling_step_count": len(sampling_inputs),
"npz_sha256": sha256(args.output.with_suffix(".npz")),
}
args.output.with_suffix(".json").write_text(json.dumps(metadata, indent=2) + "\n", encoding="utf-8")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""Capture a real LLM2Vec prompt fixture from the upstream Kimodo encoder."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
import sys
import numpy as np
import torch
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--upstream", type=Path, required=True)
parser.add_argument("--base", type=Path, required=True)
parser.add_argument("--mntp-adapter", type=Path, required=True)
parser.add_argument("--supervised-adapter", type=Path, required=True)
parser.add_argument("--prompt", required=True)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--device", default="cuda")
parser.add_argument("--debug-layer", type=int, default=0, choices=range(32), help="capture module checkpoints for this transformer layer")
return parser.parse_args()
def as_f32(value: torch.Tensor) -> np.ndarray:
return value.detach().to(device="cpu", dtype=torch.float32).contiguous().numpy()
def main() -> None:
opt = args()
if not (opt.upstream / "kimodo").is_dir():
raise SystemExit("--upstream is not a Kimodo checkout")
if any(not path.is_dir() for path in (opt.base, opt.mntp_adapter, opt.supervised_adapter)):
raise SystemExit("all model paths must be existing directories")
sys.path.insert(0, str(opt.upstream.resolve()))
from kimodo.model.llm2vec import LLM2Vec # pylint: disable=import-outside-toplevel
from kimodo.model.llm2vec.llm2vec import batch_to_device # pylint: disable=import-outside-toplevel
from peft import PeftModel # pylint: disable=import-outside-toplevel
torch.manual_seed(42)
encoder = LLM2Vec.from_pretrained(
str(opt.base), peft_model_name_or_path=str(opt.mntp_adapter), merge_peft=True, torch_dtype=torch.bfloat16,
)
encoder.model = PeftModel.from_pretrained(encoder.model, str(opt.supervised_adapter))
encoder.to(opt.device).eval()
for parameter in encoder.parameters():
parameter.requires_grad = False
prepared = encoder.prepare_for_tokenization(encoder._convert_to_str("", opt.prompt))
features = encoder.tokenize([prepared])
recorded_features = {name: value.detach().cpu().numpy() for name, value in features.items()}
features = batch_to_device(features, opt.device)
layer_values: list[torch.Tensor | None] = [None] * 32
embedding_values: list[torch.Tensor] = []
layer0_values: dict[str, torch.Tensor] = {}
layer_hooks = []
debug_modules = {
"debug_input_norm": f"layers.{opt.debug_layer}.input_layernorm",
"debug_q": f"layers.{opt.debug_layer}.self_attn.q_proj",
"debug_k": f"layers.{opt.debug_layer}.self_attn.k_proj",
"debug_v": f"layers.{opt.debug_layer}.self_attn.v_proj",
"debug_o": f"layers.{opt.debug_layer}.self_attn.o_proj",
"debug_post_norm": f"layers.{opt.debug_layer}.post_attention_layernorm",
"debug_gate": f"layers.{opt.debug_layer}.mlp.gate_proj",
"debug_up": f"layers.{opt.debug_layer}.mlp.up_proj",
"debug_down": f"layers.{opt.debug_layer}.mlp.down_proj",
}
for name, module in encoder.model.named_modules():
if name.endswith("embed_tokens"):
layer_hooks.append(module.register_forward_hook(lambda _m, _i, o: embedding_values.append(o)))
for output_name, suffix in debug_modules.items():
if name.endswith(suffix):
layer_hooks.append(module.register_forward_hook(
lambda _m, _i, o, output_name=output_name: layer0_values.__setitem__(output_name, o[0] if isinstance(o, tuple) else o)
))
for index in range(32):
if name.endswith(f"layers.{index}"):
layer_hooks.append(module.register_forward_hook(
lambda _m, _i, o, index=index: layer_values.__setitem__(index, o[0] if isinstance(o, tuple) else o)
))
try:
with torch.inference_mode():
reps = encoder.model(**features)
pooled = encoder.get_pooling(features, reps.last_hidden_state)
finally:
for hook in layer_hooks:
hook.remove()
if len(embedding_values) != 1 or any(value is None for value in layer_values) or set(layer0_values) != set(debug_modules):
raise RuntimeError("did not observe exactly one embedding and every Llama layer")
arrays: dict[str, np.ndarray] = {
"input_ids": recorded_features["input_ids"].astype(np.int64),
"attention_mask": recorded_features["attention_mask"].astype(np.int64),
"embed_mask": recorded_features["embed_mask"].astype(np.int64),
"token_embeddings": as_f32(embedding_values[0]),
"final_hidden_state": as_f32(reps.last_hidden_state),
"pooled_embedding": as_f32(pooled),
}
for index, value in enumerate(layer_values):
arrays[f"layer_{index:02d}_output"] = as_f32(value) # type: ignore[arg-type]
for name, value in layer0_values.items():
arrays[name] = as_f32(value)
opt.output.parent.mkdir(parents=True, exist_ok=True)
np.savez_compressed(opt.output.with_suffix(".npz"), **arrays)
metadata = {
"fixture_format": 1,
"prompt": opt.prompt,
"prepared_text": prepared,
"device": opt.device,
"torch": torch.__version__,
"cuda": torch.version.cuda,
"base_sha256": {path.name: sha256(path) for path in opt.base.glob("*.safetensors")},
"mntp_adapter_sha256": sha256(opt.mntp_adapter / "adapter_model.safetensors"),
"supervised_adapter_sha256": sha256(opt.supervised_adapter / "adapter_model.safetensors"),
"npz_sha256": sha256(opt.output.with_suffix(".npz")),
"debug_layer": opt.debug_layer,
}
opt.output.with_suffix(".json").write_text(json.dumps(metadata, indent=2) + "\n", encoding="utf-8")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Check the layer-0 RMSNorm fixture against its safetensors F32 calculation.
This imports only the project's safe safetensors reader; it deliberately does
not import torch or deserialize a PyTorch checkpoint.
"""
from __future__ import annotations
import argparse
import importlib.util
import sys
from pathlib import Path
import numpy as np
def load_converter():
path = Path(__file__).with_name("convert_llm2vec_layer_to_gguf.py")
spec = importlib.util.spec_from_file_location("llm2vec_converter", path)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def max_abs(a: np.ndarray, b: np.ndarray) -> float:
return float(np.max(np.abs(a - b)))
def bf16(value: np.ndarray) -> np.ndarray:
bits = value.view(np.uint32)
rounded_bits = (bits + np.uint32(0x7FFF) + ((bits >> 16) & 1)) & np.uint32(0xFFFF0000)
return rounded_bits.view(np.float32)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base", type=Path, required=True)
parser.add_argument("--fixture", type=Path, required=True)
opt = parser.parse_args()
converter = load_converter()
shards = [converter.safe_file(path) for path in sorted(opt.base.glob("model-*.safetensors"))]
weight = converter.f32(converter.base_tensor(shards, "model.layers.0.input_layernorm.weight"))
x = np.fromfile(opt.fixture / "token_embeddings.f32", dtype="<f4").reshape(16, 4096)
expected = np.fromfile(opt.fixture / "layer0_input_norm.f32", dtype="<f4").reshape(16, 4096)
actual = (x * np.reciprocal(np.sqrt(np.mean(x * x, axis=-1, keepdims=True) + 1e-5))) * weight
print(f"f32 max_abs={max_abs(actual, expected):g}")
print(f"bf16-final max_abs={max_abs(bf16(actual), expected):g}")
normalized = x * np.reciprocal(np.sqrt(np.mean(x * x, axis=-1, keepdims=True) + 1e-5))
print(f"transformers-rmsnorm max_abs={max_abs(bf16(bf16(normalized) * weight), expected):g}")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,38 @@
#!/usr/bin/env bash
# Build the serial native text bundle. Run this once inside `nix develop`;
# the Python converter itself has no network or framework dependency.
set -euo pipefail
if [[ $# -ne 2 && $# -ne 4 ]]; then
echo "usage: $0 BASE_MODEL_DIR OUTPUT_DIR [FIRST_LAYER LAST_LAYER]" >&2
exit 2
fi
base=$1
output=$2
root=$(cd "$(dirname "$0")/.." && pwd)
mkdir -p "$output"
first=0
last=31
if [[ $# -eq 2 ]]; then
python3 "$root/scripts/convert_llm2vec_tokenizer_to_gguf.py" \
--tokenizer "$base/tokenizer.json" --output "$output/tokenizer.gguf"
python3 "$root/scripts/convert_llm2vec_layer_to_gguf.py" \
--base "$base" --mntp-adapter "$root/models/llm2vec-mntp-adapter" \
--supervised-adapter "$root/models/llm2vec-adapter" --embedding \
--output "$output/embedding.gguf"
python3 "$root/scripts/convert_llm2vec_layer_to_gguf.py" \
--base "$base" --mntp-adapter "$root/models/llm2vec-mntp-adapter" \
--supervised-adapter "$root/models/llm2vec-adapter" --final-norm \
--output "$output/final-norm.gguf"
else
first=$3
last=$4
fi
for layer in $(seq "$first" "$last"); do
printf -v name 'layer-%02d.gguf' "$layer"
python3 "$root/scripts/convert_llm2vec_layer_to_gguf.py" \
--base "$base" --mntp-adapter "$root/models/llm2vec-mntp-adapter" \
--supervised-adapter "$root/models/llm2vec-adapter" --layer "$layer" \
--output "$output/$name"
done

View File

@ -0,0 +1,235 @@
#!/usr/bin/env python3
"""Safely convert one LLM2Vec Llama layer into a GGUF parity artifact.
This uses safetensors' documented raw layout only. It does not import torch or
deserialize pickle. It preserves the upstream execution path: the MNTP
adapter is merged into BF16 base weights, while the supervised adapter remains
an F32 LoRA branch.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import struct
from dataclasses import dataclass
from pathlib import Path
import numpy as np
ALIGN, MAGIC, VERSION, F32, BF16 = 32, 0x46554747, 3, 0, 30
UINT32, UINT64, STRING = 4, 10, 8
TARGETS = ("self_attn.q_proj", "self_attn.k_proj", "self_attn.v_proj", "self_attn.o_proj", "mlp.gate_proj", "mlp.up_proj", "mlp.down_proj")
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for part in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(part)
return digest.hexdigest()
def pairs(items: list[tuple[str, object]]) -> dict[str, object]:
result: dict[str, object] = {}
for key, value in items:
if key in result:
raise ValueError(f"duplicate safetensors key: {key}")
result[key] = value
return result
@dataclass(frozen=True)
class Tensor:
path: Path
dtype: str
shape: tuple[int, ...]
offset: int
def array(self) -> np.ndarray:
dtype = {"BF16": "<u2", "F32": "<f4"}.get(self.dtype)
if dtype is None:
raise ValueError(f"unsupported tensor dtype {self.dtype}")
return np.memmap(self.path, mode="r", dtype=dtype, offset=self.offset, shape=self.shape, order="C")
def safe_file(path: Path) -> dict[str, Tensor]:
size = path.stat().st_size
with path.open("rb") as stream:
raw = stream.read(8)
if len(raw) != 8:
raise ValueError(f"{path}: truncated safetensors header")
length = struct.unpack("<Q", raw)[0]
if length > 128 * 1024 * 1024 or length > size - 8:
raise ValueError(f"{path}: invalid safetensors header length")
header = json.loads(stream.read(length), object_pairs_hook=pairs)
if not isinstance(header, dict):
raise ValueError(f"{path}: safetensors header is not an object")
data = 8 + length
result: dict[str, Tensor] = {}
ranges: list[tuple[int, int]] = []
for name, desc in header.items():
if name == "__metadata__":
continue
if not isinstance(name, str) or not isinstance(desc, dict):
raise ValueError(f"{path}: invalid tensor entry")
dtype = desc.get("dtype")
shape, offsets = desc.get("shape"), desc.get("data_offsets")
if dtype not in ("BF16", "F32") or not isinstance(shape, list) or not isinstance(offsets, list) or len(offsets) != 2:
raise ValueError(f"{path}: invalid tensor {name}")
if not shape or any(not isinstance(dim, int) or dim <= 0 for dim in shape):
raise ValueError(f"{path}: invalid shape for {name}")
begin, end = offsets
width = 2 if dtype == "BF16" else 4
elements = int(np.prod(shape, dtype=np.int64))
if not isinstance(begin, int) or not isinstance(end, int) or begin < 0 or end < begin or end > size - data or end - begin != elements * width:
raise ValueError(f"{path}: invalid payload range for {name}")
result[name] = Tensor(path, dtype, tuple(shape), data + begin)
ranges.append((begin, end))
for (_, previous), (begin, _) in zip(sorted(ranges), sorted(ranges)[1:]):
if begin < previous:
raise ValueError(f"{path}: overlapping payload ranges")
return result
def f32(tensor: Tensor) -> np.ndarray:
raw = tensor.array()
if tensor.dtype == "F32":
return np.asarray(raw, dtype=np.float32)
# BF16 has the high 16 bits of IEEE-754 F32. This is an independent,
# direct format conversion; no upstream framework code is used.
return (np.asarray(raw, dtype=np.uint32) << 16).view(np.float32)
def bf16(value: np.ndarray) -> np.ndarray:
"""Round F32 to IEEE BF16 using round-to-nearest-even."""
bits = np.asarray(value, dtype=np.float32).view(np.uint32)
return ((bits + np.uint32(0x7FFF) + ((bits >> 16) & 1)) >> 16).astype("<u2")
def text(value: str) -> bytes:
encoded = value.encode("utf-8")
return struct.pack("<Q", len(encoded)) + encoded
def meta_string(key: str, value: str) -> bytes:
return text(key) + struct.pack("<I", STRING) + text(value)
def meta_uint(key: str, value: int, kind: int = UINT64) -> bytes:
return text(key) + struct.pack("<I", kind) + (struct.pack("<I", value) if kind == UINT32 else struct.pack("<Q", value))
def tensor_info(name: str, shape: tuple[int, ...], kind: int, offset: int) -> bytes:
dims = tuple(reversed(shape))
return text(name) + struct.pack("<I", len(dims)) + b"".join(struct.pack("<Q", dim) for dim in dims) + struct.pack("<I", kind) + struct.pack("<Q", offset)
def base_tensor(base_files: list[dict[str, Tensor]], name: str) -> Tensor:
matches = [file[name] for file in base_files if name in file]
if len(matches) != 1:
raise ValueError(f"expected exactly one base tensor {name}, got {len(matches)}")
return matches[0]
def merged_mntp(base: Tensor, adapter: dict[str, Tensor], name: str) -> np.ndarray:
weight = f32(base).copy()
prefix = "base_model." + name
a = adapter.get(prefix + ".lora_A.weight")
b = adapter.get(prefix + ".lora_B.weight")
if not a or not b:
raise ValueError(f"missing MNTP LoRA pair for {name}")
av, bv = f32(a), f32(b)
if av.shape[0] != 16 or bv.shape[1] != 16 or bv.shape[0] != weight.shape[0] or av.shape[1] != weight.shape[1]:
raise ValueError(f"invalid MNTP LoRA shapes for {name}")
return bf16(weight + (2.0 * (bv @ av)).astype(np.float32, copy=False))
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base", type=Path, required=True)
parser.add_argument("--mntp-adapter", type=Path, required=True)
parser.add_argument("--supervised-adapter", type=Path, required=True)
target = parser.add_mutually_exclusive_group(required=True)
target.add_argument("--layer", type=int, choices=range(32))
target.add_argument("--final-norm", action="store_true", help="convert model.norm only")
target.add_argument("--embedding", action="store_true", help="convert model.embed_tokens only")
parser.add_argument("--output", type=Path, required=True)
opt = parser.parse_args()
index = opt.layer
shards = sorted(opt.base.glob("model-*.safetensors"))
if len(shards) != 4:
raise SystemExit("--base must contain the four Llama safetensors shards")
base = [safe_file(path) for path in shards]
mntp_adapter = safe_file(opt.mntp_adapter / "adapter_model.safetensors")
supervised_adapter = safe_file(opt.supervised_adapter / "adapter_model.safetensors")
prefix = f"model.layers.{index}." if index is not None else ""
if opt.final_norm:
names: list[tuple[str, Tensor | None, int, str | None]] = [
("final_norm.weight", base_tensor(base, "model.norm.weight"), BF16, None),
]
elif opt.embedding:
names = [("token_embedding.weight", base_tensor(base, "model.embed_tokens.weight"), BF16, None)]
else:
names = [
("attn_norm.weight", base_tensor(base, prefix + "input_layernorm.weight"), BF16, None),
("ffn_norm.weight", base_tensor(base, prefix + "post_attention_layernorm.weight"), BF16, None),
]
for target in TARGETS:
short = target.replace("self_attn.", "attn_").replace("mlp.", "ffn_")
names.append((short + "_base.weight", None, BF16, target))
adapter_prefix = "base_model." + prefix + target
names.append((short + "_lora_a.weight", supervised_adapter[adapter_prefix + ".lora_A.weight"], F32, None))
names.append((short + "_lora_b.weight", supervised_adapter[adapter_prefix + ".lora_B.weight"], F32, None))
offsets: list[int] = []
cursor = 0
shapes: list[tuple[int, ...]] = []
for _, tensor, _, target in names:
shape = tensor.shape if tensor else base_tensor(base, prefix + target + ".weight").shape
shapes.append(shape)
cursor = (cursor + ALIGN - 1) // ALIGN * ALIGN
offsets.append(cursor)
cursor += int(np.prod(shape, dtype=np.int64)) * (4 if names[len(shapes) - 1][2] == F32 else 2)
metadata = [
meta_string("general.architecture", "kimodo-llm2vec-layer"),
meta_uint("general.alignment", ALIGN, UINT32),
meta_uint("kimodo.format_version", 1),
meta_string("kimodo.component", "final_norm" if opt.final_norm else "token_embedding" if opt.embedding else "transformer_layer"),
meta_uint("kimodo.hidden_size", 4096),
meta_uint("kimodo.heads", 32),
meta_uint("kimodo.key_value_heads", 8),
meta_uint("kimodo.rope_theta", 500000),
meta_string("kimodo.lora_merge", "MNTP W + 2*B@A rounded to BF16; supervised W + 2*B@A evaluated as F32 LoRA branch"),
meta_string("kimodo.base_sha256", ",".join(sha256(path) for path in shards)),
meta_string("kimodo.mntp_adapter_sha256", sha256(opt.mntp_adapter / "adapter_model.safetensors")),
meta_string("kimodo.supervised_adapter_sha256", sha256(opt.supervised_adapter / "adapter_model.safetensors")),
]
header = struct.pack("<IIQQ", MAGIC, VERSION, len(names), len(metadata)) + b"".join(metadata)
header += b"".join(tensor_info(name, shape, kind, offset) for (name, _, kind, _), shape, offset in zip(names, shapes, offsets))
output = opt.output.resolve()
output.parent.mkdir(parents=True, exist_ok=True)
temporary = output.with_name(output.name + ".tmp")
try:
with temporary.open("wb") as stream:
stream.write(header)
stream.write(b"\0" * ((-len(header)) % ALIGN))
written = 0
for (name, tensor, kind, target), offset in zip(names, offsets):
stream.write(b"\0" * (offset - written))
value = (np.asarray(tensor.array(), dtype="<u2") if kind == BF16 and tensor else
f32(tensor) if tensor else
merged_mntp(base_tensor(base, prefix + target + ".weight"), mntp_adapter, prefix + target))
stream.write(np.asarray(value, order="C").tobytes())
written = offset + value.size * (2 if kind == BF16 else 4)
stream.write(b"\0" * ((-written) % ALIGN))
os.replace(temporary, output)
finally:
if temporary.exists():
temporary.unlink()
component = "final norm" if opt.final_norm else "token embedding" if opt.embedding else f"BF16 MNTP + F32 supervised LoRA layer {index}"
print(f"wrote {output} ({output.stat().st_size} bytes, {component})")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""Convert a trusted Llama-3 tokenizer.json to a tokenizer-only GGUF.
The input is JSON, not a Python checkpoint. The output deliberately keeps
the tokenizer separate from streamed weight shards so it is small and can be
validated/loaded without transformers.
"""
from __future__ import annotations
import argparse
import json
import struct
from pathlib import Path
MAGIC, VERSION, STRING, ARRAY, UINT32, UINT64 = 0x46554747, 3, 8, 9, 4, 10
def string(value: str) -> bytes:
data = value.encode("utf-8")
return struct.pack("<Q", len(data)) + data
def meta_string(key: str, value: str) -> bytes:
return string(key) + struct.pack("<I", STRING) + string(value)
def meta_uint(key: str, value: int, kind: int = UINT64) -> bytes:
return string(key) + struct.pack("<I", kind) + (struct.pack("<I", value) if kind == UINT32 else struct.pack("<Q", value))
def meta_strings(key: str, values: list[str]) -> bytes:
return string(key) + struct.pack("<I", ARRAY) + struct.pack("<I", STRING) + struct.pack("<Q", len(values)) + b"".join(string(value) for value in values)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--tokenizer", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
opt = parser.parse_args()
source = json.loads(opt.tokenizer.read_text(encoding="utf-8"))
model = source.get("model")
if not isinstance(model, dict) or model.get("type") != "BPE":
raise SystemExit("tokenizer must contain a BPE model")
vocab, merges = model.get("vocab"), model.get("merges")
if not isinstance(vocab, dict) or not isinstance(merges, list) or len(vocab) != 128000 or len(merges) != 280147:
raise SystemExit("unexpected Llama-3 tokenizer vocabulary or merge count")
tokens = [""] * len(vocab)
for token, index in vocab.items():
if not isinstance(token, str) or not isinstance(index, int) or index < 0 or index >= len(tokens) or tokens[index]:
raise SystemExit("invalid or non-contiguous tokenizer vocabulary")
tokens[index] = token
if any(not token for token in tokens):
raise SystemExit("tokenizer vocabulary has an empty entry")
if not all(isinstance(merge, str) and " " in merge for merge in merges):
raise SystemExit("invalid BPE merge list")
metadata = [
meta_string("general.architecture", "kimodo-llm2vec-tokenizer"),
meta_uint("kimodo.format_version", 1),
meta_string("kimodo.tokenizer", "llama3-byte-bpe"),
meta_uint("kimodo.vocab_size", len(tokens), UINT32),
meta_uint("kimodo.bos_token_id", 128000, UINT32),
meta_strings("kimodo.tokenizer.tokens", tokens),
meta_strings("kimodo.tokenizer.merges", merges),
]
payload = struct.pack("<IIQQ", MAGIC, VERSION, 0, len(metadata)) + b"".join(metadata)
opt.output.parent.mkdir(parents=True, exist_ok=True)
opt.output.write_bytes(payload)
print(f"wrote {opt.output} ({opt.output.stat().st_size} bytes)")
if __name__ == "__main__":
main()

242
scripts/convert_motion_to_gguf.py Executable file
View File

@ -0,0 +1,242 @@
#!/usr/bin/env python3
"""Convert the Kimodo SMPL-X safetensors checkpoint to a self-describing GGUF.
This converter deliberately implements only the safe safetensors and NPY
formats. It never imports torch, never deserializes pickle, and writes to a
temporary sibling before atomically publishing the GGUF.
"""
from __future__ import annotations
import argparse
import ast
import hashlib
import json
import os
import re
import struct
from dataclasses import dataclass
from pathlib import Path
ALIGNMENT = 32
GGUF_MAGIC, GGUF_VERSION, GGML_TYPE_F32 = 0x46554747, 3, 0
TYPE_UINT64, TYPE_STRING = 10, 8
TYPE_UINT32 = 4
@dataclass(frozen=True)
class Tensor:
name: str
shape: tuple[int, ...]
start: int
size: int
source: Path
source_size: int | None = None
def checked_file_size(path: Path) -> int:
size = path.stat().st_size
if size < 0:
raise ValueError(f"{path}: invalid file size")
return size
def reject_duplicate_keys(pairs: list[tuple[str, object]]) -> dict[str, object]:
result: dict[str, object] = {}
for key, value in pairs:
if key in result:
raise ValueError(f"duplicate JSON key: {key}")
result[key] = value
return result
def sha256(path: Path) -> str:
h = hashlib.sha256()
with path.open("rb") as f:
for part in iter(lambda: f.read(1024 * 1024), b""): h.update(part)
return h.hexdigest()
def string(value: str) -> bytes:
encoded = value.encode("utf-8")
return struct.pack("<Q", len(encoded)) + encoded
def read_safetensors(path: Path) -> list[Tensor]:
file_size = checked_file_size(path)
if file_size < 8:
raise ValueError(f"{path}: truncated safetensors header")
with path.open("rb") as f:
raw_header_len = f.read(8)
if len(raw_header_len) != 8:
raise ValueError(f"{path}: truncated safetensors header")
header_len = struct.unpack("<Q", raw_header_len)[0]
if header_len > 128 * 1024 * 1024 or header_len > file_size - 8:
raise ValueError(f"{path}: safetensors header too large or truncated")
raw_header = f.read(header_len)
if len(raw_header) != header_len:
raise ValueError(f"{path}: truncated safetensors header")
try:
header = json.loads(raw_header, object_pairs_hook=reject_duplicate_keys)
except (UnicodeDecodeError, json.JSONDecodeError, ValueError) as error:
raise ValueError(f"{path}: invalid safetensors JSON header") from error
if not isinstance(header, dict):
raise ValueError(f"{path}: safetensors header is not an object")
data_start = 8 + header_len
payload_size = file_size - data_start
tensors: list[Tensor] = []
ranges: list[tuple[int, int]] = []
for name, desc in header.items():
if name == "__metadata__": continue
if not isinstance(name, str) or not name or len(name) > 4096 or not isinstance(desc, dict):
raise ValueError(f"{path}: invalid safetensors tensor descriptor")
if desc.get("dtype") != "F32": raise ValueError(f"{name}: expected F32, got {desc.get('dtype')}")
raw_shape = desc.get("shape")
raw_offsets = desc.get("data_offsets")
if not isinstance(raw_shape, list) or not raw_shape or len(raw_shape) > 8:
raise ValueError(f"{name}: malformed tensor shape")
if not isinstance(raw_offsets, list) or len(raw_offsets) != 2:
raise ValueError(f"{name}: malformed tensor byte range")
if any(not isinstance(n, int) or isinstance(n, bool) or n <= 0 for n in raw_shape):
raise ValueError(f"{name}: malformed tensor shape")
if any(not isinstance(n, int) or isinstance(n, bool) for n in raw_offsets):
raise ValueError(f"{name}: malformed tensor byte range")
shape = tuple(raw_shape)
start, end = raw_offsets
if start < 0 or end < start or end > payload_size:
raise ValueError(f"{name}: tensor byte range is outside safetensors payload")
size = end - start
expected = 4
for n in shape: expected *= n
if size != expected:
raise ValueError(f"{name}: malformed tensor shape or byte range")
ranges.append((start, end))
tensors.append(Tensor(name.removeprefix("denoiser.backbone."), shape, data_start + start, size, path))
for (_, previous_end), (start, _) in zip(sorted(ranges), sorted(ranges)[1:]):
if start < previous_end:
raise ValueError(f"{path}: overlapping safetensors tensor byte ranges")
return sorted(tensors, key=lambda t: t.name)
def read_npy(path: Path, name: str) -> Tensor:
file_size = checked_file_size(path)
with path.open("rb") as f:
if f.read(6) != b"\x93NUMPY": raise ValueError(f"{path}: not an NPY file")
major, _ = struct.unpack("BB", f.read(2))
if major not in (1, 2, 3): raise ValueError(f"{path}: unsupported NPY version")
width = 2 if major == 1 else 4
raw_header_len = f.read(width)
if len(raw_header_len) != width: raise ValueError(f"{path}: truncated NPY header")
header_len = struct.unpack("<H" if major == 1 else "<I", raw_header_len)[0]
if header_len > 1024 * 1024 or header_len > file_size - (6 + 2 + width):
raise ValueError(f"{path}: invalid NPY header length")
header = f.read(header_len).decode("latin1")
try:
descriptor = ast.literal_eval(header)
except (SyntaxError, ValueError) as error:
raise ValueError(f"{path}: invalid NPY header") from error
if not isinstance(descriptor, dict) or descriptor.get("fortran_order") is not False:
raise ValueError(f"{path}: expected C-order NPY")
is_f64 = descriptor.get("descr") == "<f8"
if not is_f64 and descriptor.get("descr") != "<f4":
raise ValueError(f"{path}: expected little-endian F32 or F64 NPY")
raw_shape = descriptor.get("shape")
if not isinstance(raw_shape, tuple) or not raw_shape or len(raw_shape) > 8 or any(not isinstance(n, int) or isinstance(n, bool) or n <= 0 for n in raw_shape):
raise ValueError(f"{path}: invalid NPY shape")
shape = raw_shape
elements = 1
for n in shape: elements *= n
source_size = (8 if is_f64 else 4) * elements
start = 6 + 2 + width + header_len
if source_size > file_size - start:
raise ValueError(f"{path}: truncated NPY payload")
return Tensor(name, shape, start, 4 * elements, path, source_size)
def metadata_string(key: str, value: str) -> bytes:
return string(key) + struct.pack("<I", TYPE_STRING) + string(value)
def metadata_uint(key: str, value: int) -> bytes:
return string(key) + struct.pack("<I", TYPE_UINT64) + struct.pack("<Q", value)
def metadata_uint32(key: str, value: int) -> bytes:
return string(key) + struct.pack("<I", TYPE_UINT32) + struct.pack("<I", value)
def tensor_info(tensor: Tensor, offset: int) -> bytes:
# GGML stores dim 0 as the contiguous dimension. PyTorch F32 storage is
# row-major, so reverse dimensions without changing the underlying bytes.
dims = tuple(reversed(tensor.shape))
return string(tensor.name) + struct.pack("<I", len(dims)) + b"".join(struct.pack("<Q", n) for n in dims) + struct.pack("<I", GGML_TYPE_F32) + struct.pack("<Q", offset)
def copy_range(dst, tensor: Tensor) -> None:
with tensor.source.open("rb") as src:
src.seek(tensor.start)
remaining = tensor.source_size or tensor.size
if remaining != tensor.size:
# Upstream normalization statistics are F64. Convert them once at
# the safe conversion boundary so all runtime tensors are F32.
while remaining:
raw = src.read(8)
if len(raw) != 8: raise RuntimeError(f"unexpected EOF in {tensor.source}")
dst.write(struct.pack("<f", struct.unpack("<d", raw)[0])); remaining -= 8
return
while remaining:
block = src.read(min(8 * 1024 * 1024, remaining))
if not block: raise RuntimeError(f"unexpected EOF in {tensor.source}")
dst.write(block); remaining -= len(block)
def main() -> None:
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--input", required=True, type=Path, help="downloaded Kimodo-SMPLX-RP-v1 directory")
p.add_argument("--output", required=True, type=Path)
args = p.parse_args()
root = args.input.resolve()
ckpt = root / "model.safetensors"
if not ckpt.is_file(): raise SystemExit("missing model.safetensors")
tensors = read_safetensors(ckpt)
expected = 408
if len(tensors) != expected: raise SystemExit(f"expected {expected} checkpoint tensors, got {len(tensors)}")
for part in ("global_root", "local_root", "body"):
for stat in ("mean", "std"):
tensors.append(read_npy(root / "stats" / "motion" / part / f"{stat}.npy", f"stats.{part}.{stat}"))
revision = (root / "REVISION").read_text(encoding="utf-8").split()[0]
meta = [
metadata_string("general.architecture", "kimodo-motion"),
metadata_string("general.name", "Kimodo-SMPLX-RP-v1"),
# GGML's own loader requires general.alignment to be UINT32.
metadata_uint32("general.alignment", ALIGNMENT),
metadata_uint("kimodo.format_version", 1),
metadata_string("kimodo.skeleton", "smplx22"),
metadata_string("kimodo.model_identity", f"nvidia/Kimodo-SMPLX-RP-v1@{revision}"),
metadata_string("kimodo.source_revision", revision),
metadata_string("kimodo.source_sha256", sha256(ckpt)),
metadata_uint("kimodo.text_embedding_width", 4096),
metadata_uint("kimodo.motion_dim", 273),
metadata_uint("kimodo.global_root_dim", 5),
metadata_uint("kimodo.local_root_dim", 4),
metadata_uint("kimodo.body_dim", 268),
metadata_uint("kimodo.hidden_size", 1024),
metadata_uint("kimodo.layers", 16),
metadata_uint("kimodo.heads", 8),
metadata_uint("kimodo.feed_forward_size", 2048),
metadata_uint("kimodo.num_text_tokens", 50),
metadata_uint("kimodo.base_diffusion_steps", 1000),
metadata_uint("kimodo.fps", 30),
]
offsets, cursor = [], 0
for tensor in tensors:
cursor = (cursor + ALIGNMENT - 1) // ALIGNMENT * ALIGNMENT
offsets.append(cursor); cursor += tensor.size
header = struct.pack("<IIQQ", GGUF_MAGIC, GGUF_VERSION, len(tensors), len(meta)) + b"".join(meta)
header += b"".join(tensor_info(t, off) for t, off in zip(tensors, offsets))
padding = (-len(header)) % ALIGNMENT
output = args.output.resolve(); output.parent.mkdir(parents=True, exist_ok=True)
temporary = output.with_name(output.name + ".tmp")
try:
with temporary.open("wb") as out:
out.write(header); out.write(b"\0" * padding)
written = 0
for tensor, offset in zip(tensors, offsets):
out.write(b"\0" * (offset - written)); copy_range(out, tensor); written = offset + tensor.size
# GGML validates the complete aligned tensor blob, including the
# final tensor's padding (not merely its logical data bytes).
out.write(b"\0" * ((-written) % ALIGNMENT))
os.replace(temporary, output)
finally:
if temporary.exists(): temporary.unlink()
print(f"wrote {output} ({output.stat().st_size} bytes, {len(tensors)} F32 tensors)")
if __name__ == "__main__": main()

62
scripts/download_weights.sh Executable file
View File

@ -0,0 +1,62 @@
#!/usr/bin/env bash
# Download Kimodo inputs only after the caller has accepted each HF licence.
# This script resolves `main` once to a commit SHA, then downloads that exact
# revision and writes a content manifest. It never receives a token argument.
set -euo pipefail
export HF_HUB_DISABLE_PROGRESS_BARS=1
usage() {
printf '%s\n' "usage: $0 --output DIR [--revision REVISION] [--with-text]" >&2
exit 2
}
output='' revision='main' with_text=0
while [ "$#" -gt 0 ]; do
case "$1" in
--output) [ "$#" -ge 2 ] || usage; output=$2; shift 2 ;;
--revision) [ "$#" -ge 2 ] || usage; revision=$2; shift 2 ;;
--with-text) with_text=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; }
# The model is gated. `hf auth login` stores the token in the caller's normal
# HF config; an HF_TOKEN environment variable is also honoured by the client.
if [ -z "${HF_TOKEN:-}" ] && ! hf auth whoami >/dev/null 2>&1; then
echo "No Hugging Face login found. Accept the model licences, then run: hf auth login" >&2
exit 1
fi
resolve_revision() {
local repo=$1
python - "$repo" "$revision" <<'PY'
from huggingface_hub import HfApi
import sys
info = HfApi().model_info(sys.argv[1], revision=sys.argv[2])
print(info.sha)
PY
}
download() {
local repo=$1 name=$2 sha
sha=$(resolve_revision "$repo")
local target="$output/$name"
mkdir -p "$target"
echo "Downloading $repo at $sha"
# Do not retain legacy pickle checkpoints. The converter accepts only
# safetensors and the reference container owns any one-time trusted import.
hf download "$repo" --revision "$sha" --exclude '*.pth' --local-dir "$target"
(cd "$target" && find . -type f ! -name SHA256SUMS -print0 | sort -z | xargs -0 sha256sum) > "$target/SHA256SUMS"
printf '%s %s\n' "$sha" "$repo" > "$target/REVISION"
}
download nvidia/Kimodo-SMPLX-RP-v1 Kimodo-SMPLX-RP-v1
if [ "$with_text" -eq 1 ]; then
# The MNTP repo is a LoRA adapter, not the Llama base checkpoint. Keep all
# three identities separately so converter provenance cannot confuse them.
download meta-llama/Meta-Llama-3-8B-Instruct llama3-8b-instruct-base
download McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp llm2vec-mntp-adapter
download McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp-supervised llm2vec-adapter
fi

View File

@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Extract an upstream Kimodo NPZ capture into checked raw F32 tensors.
The raw files are intentionally dependency-free inputs for the C++ parity
tests and demo tooling. ``shapes.json`` preserves every original array shape
and dtype; integral arrays stay integral while floating arrays are canonical
little-endian F32.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import numpy as np
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input", type=Path, help="upstream .npz capture")
parser.add_argument("output", type=Path, help="output fixture directory")
args = parser.parse_args()
if args.input.suffix != ".npz" or not args.input.is_file():
raise SystemExit("input must be an existing .npz file")
args.output.mkdir(parents=True, exist_ok=True)
shapes: dict[str, dict[str, object]] = {}
with np.load(args.input, allow_pickle=False) as archive:
for name in sorted(archive.files):
value = np.ascontiguousarray(archive[name])
if value.dtype.kind == "f":
value = np.asarray(value, dtype="<f4")
suffix = ".f32"
elif value.dtype.kind in "iu":
value = np.asarray(value, dtype="<i8")
suffix = ".i64"
elif value.dtype.kind == "b":
value = np.asarray(value, dtype="u1")
suffix = ".u8"
else:
raise SystemExit(f"unsupported capture tensor {name}: {value.dtype}")
(args.output / f"{name}{suffix}").write_bytes(value.tobytes())
shapes[name] = {"shape": list(value.shape), "dtype": str(value.dtype), "file": f"{name}{suffix}"}
(args.output / "shapes.json").write_text(json.dumps(shapes, indent=2, sort_keys=True) + "\n", encoding="utf-8")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""Build comparable 22-joint positions from a GGML decoded-motion replay.
The current C++ motion decoder emits root translation and local rotations. For
fixture visualisation, this tool calibrates the fixed SMPL-X22 bone offsets
from upstream's posed-joint/global-rotation capture, applies C++ local
rotations with forward kinematics, and writes a raw [T,22,3] F32 trajectory.
It is a test/demo bridge, not part of the runtime inference dependency set.
"""
from __future__ import annotations
import argparse
from pathlib import Path
import numpy as np
PARENTS = np.array([-1, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 9, 12, 13, 14, 16, 17, 18, 19])
def read(path: Path, shape: tuple[int, ...]) -> np.ndarray:
value = np.fromfile(path, dtype="<f4")
if value.size != int(np.prod(shape)):
raise SystemExit(f"{path}: expected {shape}, got {value.size} values")
return value.reshape(shape)
def quaternions_to_matrix(q: np.ndarray) -> np.ndarray:
x, y, z, w = np.moveaxis(q, -1, 0)
return np.stack((
1 - 2 * (y*y + z*z), 2 * (x*y - z*w), 2 * (x*z + y*w),
2 * (x*y + z*w), 1 - 2 * (x*x + z*z), 2 * (y*z - x*w),
2 * (x*z - y*w), 2 * (y*z + x*w), 1 - 2 * (x*x + y*y),
), axis=-1).reshape(q.shape[:-1] + (3, 3))
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("fixture", type=Path)
parser.add_argument("ggml", type=Path)
parser.add_argument("output", type=Path)
args = parser.parse_args()
shapes = __import__("json").loads((args.fixture / "shapes.json").read_text())
frames = int(shapes["motion_posed_joints"]["shape"][1])
upstream_pos = read(args.fixture / "motion_posed_joints.f32", (frames, 22, 3))
upstream_global = read(args.fixture / "motion_global_rot_mats.f32", (frames, 22, 3, 3))
root = read(args.ggml / "motion_root_positions.f32", (frames, 3))
local = read(args.ggml / "motion_local_rotations_xyzw.f32", (frames, 22, 4))
# Bone vectors are constant in the parent local space; average all frames
# to remove the small numerical error in the recorded pose transforms.
offsets = np.zeros((22, 3), dtype=np.float32)
for joint in range(1, 22):
parent = PARENTS[joint]
world = upstream_pos[:, joint] - upstream_pos[:, parent]
offsets[joint] = np.einsum("tji,tj->ti", upstream_global[:, parent], world).mean(axis=0)
local_matrix = quaternions_to_matrix(local)
global_matrix = np.empty_like(local_matrix)
positions = np.empty((frames, 22, 3), dtype=np.float32)
global_matrix[:, 0] = local_matrix[:, 0]
positions[:, 0] = root
for joint in range(1, 22):
parent = PARENTS[joint]
global_matrix[:, joint] = global_matrix[:, parent] @ local_matrix[:, joint]
positions[:, joint] = positions[:, parent] + np.einsum("tij,j->ti", global_matrix[:, parent], offsets[joint])
args.output.parent.mkdir(parents=True, exist_ok=True)
positions.astype("<f4").tofile(args.output)
difference = positions - upstream_pos
print(f"joint max_abs={np.abs(difference).max():.7g} rms={np.sqrt(np.mean(difference*difference)):.7g}")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Convert and execute one layer at a time. This is intentionally a parity
# harness, not inference: it keeps disk and model residency bounded while the
# native streaming session is being implemented.
set -euo pipefail
if [[ $# -ne 6 ]]; then
echo "usage: $0 BUILD_DIR FIXTURE_DIR cpu|vulkan FIRST_LAYER LAST_LAYER STATE.f32" >&2
exit 2
fi
build_dir=$1
fixture_dir=$2
backend=$3
first=$4
last=$5
state_file=$6
root=$(cd "$(dirname "$0")/.." && pwd)
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
if (( first == 0 )); then
cp "$fixture_dir/token_embeddings.f32" "$state_file"
fi
state=$state_file
for layer in $(seq "$first" "$last"); do
gguf="$scratch/layer.gguf"
next="$scratch/state-$layer.f32"
python3 "$root/scripts/convert_llm2vec_layer_to_gguf.py" \
--base "$root/models/llama3-8b-instruct-base" \
--mntp-adapter "$root/models/llm2vec-mntp-adapter" \
--supervised-adapter "$root/models/llm2vec-adapter" \
--layer "$layer" --output "$gguf"
"$build_dir/kimodo-llm-layer-parity" "$gguf" "$fixture_dir" "$layer" "$backend" "$state" "$next" || true
mv "$next" "$state_file"
done

70
src/capi.cpp Normal file
View File

@ -0,0 +1,70 @@
#include <kimodo/kimodo_capi.h>
#include <kimodo/kimodo.hpp>
#include <algorithm>
#include <array>
#include <cstring>
#include <memory>
#include <string>
struct kimodo_model { std::unique_ptr<kimodo::model> value; std::string last_error; };
struct kimodo_motion { kimodo::motion_data value; };
namespace {
void set_error(kimodo_model *model, char *buffer, int length, const std::string &message) noexcept {
if (model) model->last_error = message;
if (!buffer || length <= 0) return;
const size_t n = std::min(message.size(), static_cast<size_t>(length - 1));
std::memcpy(buffer, message.data(), n); buffer[n] = '\0';
}
bool valid_options(const kimodo_generation_options *o, std::string &error) {
if (!o || o->size != sizeof(*o)) { error = "invalid kimodo_generation_options"; return false; }
return true;
}
}
extern "C" {
int kimodo_abi_version(void) { return KIMODO_CAPI_ABI_VERSION; }
kimodo_model *kimodo_model_load(const char *motion, const char *text, const char *adapter, const kimodo_runtime_options *options, char *err, int err_len) {
try {
if (!motion || !*motion) { set_error(nullptr, err, err_len, "motion_gguf is required"); return nullptr; }
if (options && options->size != sizeof(*options)) { set_error(nullptr, err, err_len, "invalid kimodo_runtime_options"); return nullptr; }
if (adapter && *adapter) { set_error(nullptr, err, err_len, "separate text adapters are unsupported; convert a merged native text bundle"); return nullptr; }
auto loaded = kimodo::model::load(motion, text ? text : "");
if (!loaded) { set_error(nullptr, err, err_len, loaded.error()); return nullptr; }
return new kimodo_model{std::move(*loaded), {}};
} catch (const std::exception &e) { set_error(nullptr, err, err_len, e.what()); return nullptr; }
catch (...) { set_error(nullptr, err, err_len, "unknown C++ exception"); return nullptr; }
}
void kimodo_model_free(kimodo_model *m) { delete m; }
const char *kimodo_model_last_error(const kimodo_model *m) { return m ? m->last_error.c_str() : "invalid model"; }
kimodo_motion *kimodo_generate(kimodo_model *m, const char *prompt, const kimodo_generation_options *o, char *err, int len) {
try {
std::string error;
if (!m || !m->value) { set_error(m, err, len, "invalid model"); return nullptr; }
if (!prompt) { set_error(m, err, len, "UTF-8 prompt is required"); return nullptr; }
if (!valid_options(o, error)) { set_error(m, err, len, error); return nullptr; }
auto generated = m->value->generate_text(prompt, o->frames, o->diffusion_steps, o->seed, o->text_cfg_weight, o->constraint_cfg_weight);
if (!generated) { set_error(m, err, len, generated.error()); return nullptr; }
m->last_error.clear(); return new kimodo_motion{std::move(*generated)};
} catch (const std::exception &x) { set_error(m, err, len, x.what()); return nullptr; }
catch (...) { set_error(m, err, len, "unknown C++ exception"); return nullptr; }
}
kimodo_motion *kimodo_generate_embedding(kimodo_model *m, const kimodo_embedding *e, const kimodo_generation_options *o, char *err, int len) {
try {
std::string error;
if (!m || !m->value) { set_error(m, err, len, "invalid model"); return nullptr; }
if (!e || !e->data || e->values != kimodo::embedding_width) { set_error(m, err, len, "embedding must contain exactly 4096 values"); return nullptr; }
if (!valid_options(o, error)) { set_error(m, err, len, error); return nullptr; }
std::array<float, kimodo::embedding_width> values;
std::copy_n(e->data, values.size(), values.data());
auto generated = m->value->generate_embedding(values, o->frames, o->diffusion_steps, o->seed, o->text_cfg_weight, o->constraint_cfg_weight);
if (!generated) { set_error(m, err, len, generated.error()); return nullptr; }
m->last_error.clear(); return new kimodo_motion{std::move(*generated)};
} catch (const std::exception &x) { set_error(m, err, len, x.what()); return nullptr; }
catch (...) { set_error(m, err, len, "unknown C++ exception"); return nullptr; }
}
void kimodo_motion_free(kimodo_motion *m) { delete m; }
int kimodo_motion_frames(const kimodo_motion *m) { return m ? static_cast<int>(m->value.frames) : 0; }
int kimodo_motion_joints(const kimodo_motion *m) { return m ? static_cast<int>(m->value.joints) : 0; }
const float *kimodo_motion_local_rotations_xyzw(const kimodo_motion *m) { return m && !m->value.local_rotations_xyzw.empty() ? m->value.local_rotations_xyzw.data() : nullptr; }
const float *kimodo_motion_root_positions(const kimodo_motion *m) { return m && !m->value.root_positions.empty() ? m->value.root_positions.data() : nullptr; }
}

148
src/denoiser.cpp Normal file
View File

@ -0,0 +1,148 @@
#include "denoiser.hpp"
#include "ggml_weights.hpp"
#include "motion_rep.hpp"
#include "diffusion.hpp"
#include <cmath>
#include <cstring>
#include <ggml.h>
#include <ggml-alloc.h>
#include <ggml-backend.h>
namespace kimodo::detail {
namespace {
constexpr int width=1024, heads=8, head_width=128, text_tokens=50, prefix_tokens=52;
thread_local std::vector<std::pair<ggml_tensor *, std::vector<float>>> inputs;
ggml_tensor *input(ggml_context *ctx, std::span<const float> values, int a, int b, int c) {
auto *r=ggml_new_tensor_3d(ctx,GGML_TYPE_F32,a,b,c); inputs.emplace_back(r, std::vector<float>(values.begin(),values.end())); return r;
}
ggml_tensor *linear(ggml_context *ctx, ggml_tensor *x, ggml_tensor *w, ggml_tensor *bias) {
auto *y=ggml_mul_mat(ctx,w,x);
// F32 parity takes precedence over Tensor Core throughput. In
// particular, do not let a Vulkan backend lower the accumulation
// precision for the reference model.
ggml_mul_mat_set_prec(y, GGML_PREC_F32);
return ggml_add(ctx,y,ggml_repeat(ctx,bias,y));
}
ggml_tensor *norm(ggml_context *ctx, ggml_tensor *x, ggml_tensor *scale, ggml_tensor *bias) {
auto *n=ggml_norm(ctx,x,1.e-5f); return ggml_add(ctx,ggml_mul(ctx,n,ggml_repeat(ctx,scale,n)),ggml_repeat(ctx,bias,n));
}
std::expected<std::vector<float>,std::string> execute(ggml_context *ctx, ggml_tensor *out, size_t values, ggml_backend_t backend) {
auto *graph=ggml_new_graph(ctx); ggml_build_forward_expand(graph,out);
auto alloc=ggml_gallocr_new(ggml_backend_get_default_buffer_type(backend));
if(!alloc || !ggml_gallocr_reserve(alloc,graph) || !ggml_gallocr_alloc_graph(alloc,graph)) return std::unexpected("GGML graph allocation failed");
for(const auto &[t,data]:inputs) ggml_backend_tensor_set(t,data.data(),0,data.size()*sizeof(float));
inputs.clear();
if(ggml_backend_graph_compute(backend,graph)!=GGML_STATUS_SUCCESS) { ggml_gallocr_free(alloc); return std::unexpected("GGML graph execution failed"); }
std::vector<float> r(values); ggml_backend_tensor_get(out,r.data(),0,r.size()*sizeof(float)); ggml_gallocr_free(alloc); return r;
}
ggml_tensor *weight(const ggml_motion_weights&w,std::string_view n) { auto*t=w.tensor(n); if(!t) throw std::runtime_error("missing GGML tensor: "+std::string(n)); return t; }
ggml_tensor *layer(ggml_context *ctx,ggml_tensor*x,const ggml_motion_weights&w,std::string_view p,int seq,int batch) {
const std::string s(p); auto*qkv=linear(ctx,x,weight(w,s+"self_attn.in_proj_weight"),weight(w,s+"self_attn.in_proj_bias"));
// Use explicit [head, batch] branches for the F32 reference graph. The
// packed 4-D variant is faster, but differs slightly across Vulkan
// backends; this layout exactly matches the PyTorch tensor boundaries.
auto head = [&](int block, int h, int b) {
return ggml_view_2d(ctx, qkv, head_width, seq, qkv->nb[1],
static_cast<size_t>(block*width)*sizeof(float) +
static_cast<size_t>(b)*qkv->nb[2] +
static_cast<size_t>(h*head_width)*sizeof(float));
};
std::vector<ggml_tensor *> batches;
batches.reserve(static_cast<size_t>(batch));
for (int b=0; b<batch; ++b) {
std::vector<ggml_tensor *> joined_heads;
joined_heads.reserve(heads);
for (int h=0; h<heads; ++h) {
auto *q=ggml_cont(ctx,head(0,h,b)), *k=ggml_cont(ctx,head(1,h,b)), *v=ggml_cont(ctx,head(2,h,b));
auto *scores=ggml_mul_mat(ctx,k,q);
ggml_mul_mat_set_prec(scores, GGML_PREC_F32);
auto *prob=ggml_soft_max(ctx,ggml_scale(ctx,scores,1.f/std::sqrt(float(head_width))));
auto *value_product=ggml_mul_mat(ctx,prob,ggml_cont(ctx,ggml_transpose(ctx,v)));
ggml_mul_mat_set_prec(value_product, GGML_PREC_F32);
joined_heads.push_back(ggml_transpose(ctx,value_product));
}
auto *joined=joined_heads.front();
for (int h=1; h<heads; ++h) joined=ggml_concat(ctx,joined,joined_heads[static_cast<size_t>(h)],0);
batches.push_back(ggml_reshape_3d(ctx,joined,width,seq,1));
}
auto *a=batches.front();
for (int b=1; b<batch; ++b) a=ggml_concat(ctx,a,batches[static_cast<size_t>(b)],2);
a=linear(ctx,a,weight(w,s+"self_attn.out_proj.weight"),weight(w,s+"self_attn.out_proj.bias")); x=norm(ctx,ggml_add(ctx,x,a),weight(w,s+"norm1.weight"),weight(w,s+"norm1.bias")); auto*ff=linear(ctx,x,weight(w,s+"linear1.weight"),weight(w,s+"linear1.bias")); ff=ggml_gelu_erf(ctx,ff); ff=linear(ctx,ff,weight(w,s+"linear2.weight"),weight(w,s+"linear2.bias")); return norm(ctx,ggml_add(ctx,x,ff),weight(w,s+"norm2.weight"),weight(w,s+"norm2.bias"));
}
}
std::expected<std::vector<float>, std::string> run_motion_transformer(const ggml_motion_weights&w,std::string_view prefix,std::span<const float> motion,size_t motion_dim,std::span<const float> embedding,std::span<const float> timesteps,std::span<const float> headings,size_t batch,size_t frames) try {
if(!batch||!frames||motion.size()!=batch*frames*motion_dim||embedding.size()!=batch*4096||timesteps.size()!=batch||headings.size()!=batch) return std::unexpected("invalid Transformer input dimensions");
const int seq=prefix_tokens+static_cast<int>(frames); std::vector<float> text(batch*text_tokens*4096),time(batch*width),angle(batch*2),position(size_t(seq)*width);
for(size_t b=0;b<batch;++b) { std::memcpy(text.data()+b*text_tokens*4096,embedding.data()+b*4096,4096*sizeof(float)); for(int d=0;d<width;d+=2){float z=timesteps[b]*std::pow(10000.f,-float(d)/width);time[b*width+d]=std::sin(z);time[b*width+d+1]=std::cos(z);} angle[2*b]=std::cos(headings[b]);angle[2*b+1]=std::sin(headings[b]); }
for(int s=0;s<seq;++s)for(int d=0;d<width;d+=2){float z=float(s)*std::pow(10000.f,-float(d)/width);position[size_t(s)*width+d]=std::sin(z);position[size_t(s)*width+d+1]=std::cos(z);}
const std::string p(prefix); std::vector<float> state;
{ auto*ctx=ggml_init({128ULL*1024*1024,nullptr,true}); if(!ctx)return std::unexpected("GGML context allocation failed"); auto*m=linear(ctx,input(ctx,motion,int(motion_dim),int(frames),int(batch)),weight(w,p+"input_linear.weight"),weight(w,p+"input_linear.bias")); auto*te=linear(ctx,input(ctx,text,4096,text_tokens,int(batch)),weight(w,p+"embed_text.weight"),weight(w,p+"embed_text.bias")); auto*ti=linear(ctx,input(ctx,time,width,1,int(batch)),weight(w,p+"embed_timestep.time_embed.0.weight"),weight(w,p+"embed_timestep.time_embed.0.bias"));ti=linear(ctx,ggml_silu(ctx,ti),weight(w,p+"embed_timestep.time_embed.2.weight"),weight(w,p+"embed_timestep.time_embed.2.bias"));auto*he=linear(ctx,input(ctx,angle,2,1,int(batch)),weight(w,p+"linear_first_heading_angle.weight"),weight(w,p+"linear_first_heading_angle.bias"));auto*x=ggml_concat(ctx,ggml_concat(ctx,ggml_concat(ctx,te,ti,1),he,1),m,1);auto*pos=input(ctx,position,width,seq,1);x=ggml_add(ctx,x,ggml_repeat(ctx,pos,x));auto r=execute(ctx,x,size_t(width)*seq*batch,w.backend());ggml_free(ctx);if(!r)return std::unexpected(r.error());state=std::move(*r); }
for(int i=0;i<16;++i){auto*ctx=ggml_init({128ULL*1024*1024,nullptr,true});if(!ctx)return std::unexpected("GGML context allocation failed");auto*x=layer(ctx,input(ctx,state,width,seq,int(batch)),w,p+"seqTransEncoder.layers."+std::to_string(i)+".",seq,int(batch));auto r=execute(ctx,x,size_t(width)*seq*batch,w.backend());ggml_free(ctx);if(!r)return std::unexpected(r.error());state=std::move(*r);}
auto*ctx=ggml_init({32ULL*1024*1024,nullptr,true});if(!ctx)return std::unexpected("GGML context allocation failed");auto*all=input(ctx,state,width,seq,int(batch));auto*part=ggml_view_3d(ctx,all,width,frames,batch,all->nb[1],all->nb[2],size_t(prefix_tokens)*width*sizeof(float));part=ggml_cont(ctx,part);auto*y=linear(ctx,part,weight(w,p+"output_linear.weight"),weight(w,p+"output_linear.bias"));const size_t outdim=size_t(weight(w,p+"output_linear.bias")->ne[0]);auto r=execute(ctx,y,outdim*frames*batch,w.backend());ggml_free(ctx);return r;
} catch(const std::exception&e){inputs.clear();return std::unexpected(e.what());}
std::expected<std::vector<float>, std::string> run_two_stage_denoiser(
const ggml_motion_weights &weights, std::span<const float> x,
std::span<const float> embedding, std::span<const float> timesteps,
std::span<const float> headings, std::span<const float> mask,
std::size_t batch, std::size_t frames) {
if (!batch || !frames || x.size()!=batch*frames*546 || mask.size()!=batch*frames)
return std::unexpected("invalid two-stage denoiser input dimensions");
auto root=run_motion_transformer(weights,"root_model.",x,546,embedding,timesteps,headings,batch,frames);
if(!root)return std::unexpected(root.error());
auto gm=weights.f32_values("stats.global_root.mean"), gs=weights.f32_values("stats.global_root.std"), lm=weights.f32_values("stats.local_root.mean"), ls=weights.f32_values("stats.local_root.std");
if(!gm)return std::unexpected(gm.error());
if(!gs)return std::unexpected(gs.error());
if(!lm)return std::unexpected(lm.error());
if(!ls)return std::unexpected(ls.error());
auto local=global_root_to_local_root(*root,mask,batch,frames,*gm,*gs,*lm,*ls);
if(!local)return std::unexpected(local.error());
std::vector<float> body_input(batch*frames*545);
for(std::size_t b=0;b<batch;++b) for(std::size_t t=0;t<frames;++t) {
const auto src=(b*frames+t)*546, dst=(b*frames+t)*545;
std::memcpy(body_input.data()+dst,local->data()+(b*frames+t)*4,4*sizeof(float));
std::memcpy(body_input.data()+dst+4,x.data()+src+5,541*sizeof(float));
}
auto body=run_motion_transformer(weights,"body_model.",body_input,545,embedding,timesteps,headings,batch,frames);
if(!body)return std::unexpected(body.error());
std::vector<float> output(batch*frames*273);
for(std::size_t b=0;b<batch;++b)for(std::size_t t=0;t<frames;++t){const auto r=(b*frames+t)*5, q=(b*frames+t)*268, o=(b*frames+t)*273;std::memcpy(output.data()+o,root->data()+r,5*sizeof(float));std::memcpy(output.data()+o+5,body->data()+q,268*sizeof(float));}
return output;
}
std::expected<std::vector<float>, std::string> run_separated_cfg_denoiser(
const ggml_motion_weights &weights, std::span<const float> motion,
std::span<const float> embedding, float timestep, float text_weight,
float constraint_weight, std::size_t frames) {
if (motion.size()!=frames*273 || embedding.size()!=4096 || !std::isfinite(timestep) || !std::isfinite(text_weight) || !std::isfinite(constraint_weight))
return std::unexpected("invalid separated CFG denoiser input");
constexpr size_t cfg_batch=3; std::vector<float> extended(cfg_batch*frames*546), text(cfg_batch*4096), times(cfg_batch,timestep), headings(cfg_batch), mask(cfg_batch*frames,1.f);
for(size_t b=0;b<cfg_batch;++b) for(size_t t=0;t<frames;++t) std::memcpy(extended.data()+(b*frames+t)*546,motion.data()+t*273,273*sizeof(float));
// Upstream order is text, constraint, unconditional. No constraints
// means all motion-mask channels are zero; only batch zero has text.
std::memcpy(text.data(),embedding.data(),4096*sizeof(float));
auto all=run_two_stage_denoiser(weights,extended,text,times,headings,mask,cfg_batch,frames);
if(!all)return std::unexpected(all.error());
std::vector<float> result(frames*273);
for(size_t i=0;i<result.size();++i) result[i]=(*all)[2*result.size()+i]+text_weight*((*all)[i]-(*all)[2*result.size()+i])+constraint_weight*((*all)[result.size()+i]-(*all)[2*result.size()+i]);
return result;
}
std::expected<std::vector<float>, std::string> sample_motion_from_noise(
const ggml_motion_weights &weights, std::span<const float> initial,
std::span<const float> embedding, std::size_t frames, unsigned steps,
float text_weight, float constraint_weight) {
if(initial.size()!=frames*273) return std::unexpected("invalid initial motion noise dimensions");
auto schedule=make_cosine_schedule(1000,steps); if(!schedule)return std::unexpected(schedule.error());
std::vector<float> state(initial.begin(),initial.end()), next(state.size());
for(unsigned i=steps;i-->0;) {
auto clean=run_separated_cfg_denoiser(weights,state,embedding,float(schedule->use_timesteps[i]),text_weight,constraint_weight,frames);
if(!clean)return std::unexpected(clean.error());
auto stepped=ddim_step(*schedule,i,state.data(),clean->data(),next.data(),state.size());
if(!stepped)return std::unexpected(stepped.error());
state.swap(next);
}
return state;
}
}

40
src/denoiser.hpp Normal file
View File

@ -0,0 +1,40 @@
#pragma once
#include <cstddef>
#include <expected>
#include <span>
#include <string>
#include <vector>
namespace kimodo::detail {
class ggml_motion_weights;
// F32 TransformerEncoderBlock. Inputs/outputs are row-major [B,T,D], while
// the implementation creates GGML [D,T,B] views over the same byte order.
std::expected<std::vector<float>, std::string> run_motion_transformer(
const ggml_motion_weights &weights, std::string_view prefix,
std::span<const float> motion, std::size_t motion_dim,
std::span<const float> text_embedding, std::span<const float> timesteps,
std::span<const float> headings, std::size_t batch, std::size_t frames);
// Exact two-stage Kimodo denoiser for concatenated motion/mask inputs
// [B,T,546]. Returned clean prediction is [B,T,273].
std::expected<std::vector<float>, std::string> run_two_stage_denoiser(
const ggml_motion_weights &weights, std::span<const float> motion_and_mask,
std::span<const float> text_embedding, std::span<const float> timesteps,
std::span<const float> headings, std::span<const float> motion_mask,
std::size_t batch, std::size_t frames);
// Unconstrained separated CFG wrapper. `motion` is [T,273], embedding is
// [4096], and the result is one clean [T,273] prediction.
std::expected<std::vector<float>, std::string> run_separated_cfg_denoiser(
const ggml_motion_weights &weights, std::span<const float> motion,
std::span<const float> embedding, float timestep, float text_weight,
float constraint_weight, std::size_t frames);
// Deterministic eta=0 DDIM sampling from caller-supplied F32 initial noise.
std::expected<std::vector<float>, std::string> sample_motion_from_noise(
const ggml_motion_weights &weights, std::span<const float> initial_noise,
std::span<const float> embedding, std::size_t frames, unsigned steps,
float text_weight, float constraint_weight);
}

63
src/diffusion.cpp Normal file
View File

@ -0,0 +1,63 @@
#include "diffusion.hpp"
#include <cmath>
#include <limits>
namespace kimodo::detail {
std::expected<diffusion_schedule, std::string> make_cosine_schedule(
std::uint32_t base_steps, std::uint32_t sample_steps) {
if (base_steps < 2 || base_steps > 100000 || sample_steps < 1 || sample_steps > base_steps)
return std::unexpected("invalid diffusion schedule step count");
std::vector<float> base_alpha(base_steps);
auto alpha_bar = [](double t) { return std::pow(std::cos((t + .008) / 1.008 * std::acos(-1.) / 2.), 2.); };
double cumulative = 1.;
for (std::uint32_t i = 0; i < base_steps; ++i) {
const double beta = std::min(1. - alpha_bar(static_cast<double>(i + 1) / base_steps) /
alpha_bar(static_cast<double>(i) / base_steps), .999);
cumulative *= 1. - beta;
base_alpha[i] = static_cast<float>(cumulative);
}
diffusion_schedule result;
result.use_timesteps.reserve(sample_steps); result.alpha_cumprod.reserve(sample_steps); result.alpha_cumprod_prev.reserve(sample_steps);
const double stride = static_cast<double>(base_steps - 1) / std::max<std::uint32_t>(1, sample_steps - 1);
float previous = 1.f;
for (std::uint32_t i = 0; i < sample_steps; ++i) {
const auto t = std::min<std::uint32_t>(static_cast<std::uint32_t>(std::floor(i * stride + .5)), base_steps - 1);
// PyTorch calculates a new beta sequence from selected alpha-bars,
// then cumulative-products it; algebraically this is the same selected
// value, while retaining the explicit predecessor for DDIM.
const float alpha = std::max(base_alpha[t], 1.e-9f);
result.use_timesteps.push_back(t); result.alpha_cumprod.push_back(alpha); result.alpha_cumprod_prev.push_back(previous); previous = alpha;
}
return result;
}
std::expected<void, std::string> ddim_step(const diffusion_schedule &s, std::uint32_t index,
const float *x_t, const float *pred, float *out, std::size_t values) {
if (!x_t || !pred || !out || index >= s.alpha_cumprod.size()) return std::unexpected("invalid DDIM input");
const float alpha = s.alpha_cumprod[index], previous = s.alpha_cumprod_prev[index];
if (!(alpha > 0.f && alpha <= 1.f && previous > 0.f && previous <= 1.f)) return std::unexpected("invalid DDIM alpha");
const float reciprocal = 1.f / std::sqrt(alpha);
// PyTorch's sqrt_recipm1_alphas_cumprod is
// rsqrt(alpha / (1-alpha)) == sqrt((1-alpha) / alpha).
const float reciprocal_m1 = std::sqrt((1.f - alpha) / alpha);
for (std::size_t i = 0; i < values; ++i) {
if (!std::isfinite(x_t[i]) || !std::isfinite(pred[i])) return std::unexpected("non-finite DDIM input");
const float epsilon = (reciprocal * x_t[i] - pred[i]) / reciprocal_m1;
out[i] = pred[i] * std::sqrt(previous) + std::sqrt(1.f - previous) * epsilon;
}
return {};
}
std::expected<void, std::string> separated_cfg(const float *text, const float *constraint, const float *uncond,
float text_weight, float constraint_weight, float *out, std::size_t values) {
if (!text || !constraint || !uncond || !out || !std::isfinite(text_weight) || !std::isfinite(constraint_weight))
return std::unexpected("invalid separated CFG input");
for (std::size_t i = 0; i < values; ++i) {
if (!std::isfinite(text[i]) || !std::isfinite(constraint[i]) || !std::isfinite(uncond[i]))
return std::unexpected("non-finite separated CFG input");
out[i] = uncond[i] + text_weight * (text[i] - uncond[i]) + constraint_weight * (constraint[i] - uncond[i]);
}
return {};
}
} // namespace kimodo::detail

31
src/diffusion.hpp Normal file
View File

@ -0,0 +1,31 @@
#pragma once
#include <cstdint>
#include <expected>
#include <string>
#include <vector>
namespace kimodo::detail {
struct diffusion_schedule {
std::vector<std::uint32_t> use_timesteps;
std::vector<float> alpha_cumprod;
std::vector<float> alpha_cumprod_prev;
};
std::expected<diffusion_schedule, std::string> make_cosine_schedule(
std::uint32_t base_steps, std::uint32_t sample_steps);
// In-place DDIM eta=0 update for a contiguous [B,T,D] F32 tensor.
std::expected<void, std::string> ddim_step(
const diffusion_schedule &schedule, std::uint32_t index,
const float *x_t, const float *pred_xstart, float *output,
std::size_t values);
// Exact separated CFG chunk order from upstream: text, constraint, uncond.
std::expected<void, std::string> separated_cfg(
const float *text, const float *constraint, const float *uncond,
float text_weight, float constraint_weight, float *output,
std::size_t values);
} // namespace kimodo::detail

46
src/generate.cpp Normal file
View File

@ -0,0 +1,46 @@
// Command-line bridge for the localhost demo. It deliberately uses only the
// public C++ model API, so the demo exercises the same text route as embedders.
#include <kimodo/kimodo.hpp>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <iterator>
#include <string>
namespace {
void write_f32(const std::filesystem::path &path, const std::vector<float> &values) {
std::ofstream out(path, std::ios::binary | std::ios::trunc);
if (!out) throw std::runtime_error("cannot open " + path.string());
out.write(reinterpret_cast<const char *>(values.data()),
static_cast<std::streamsize>(values.size() * sizeof(float)));
if (!out) throw std::runtime_error("cannot write " + path.string());
}
}
int main(int argc, char **argv) try {
if (argc != 8) {
std::cerr << "usage: " << argv[0] << " MOTION.gguf TEXT_BUNDLE PROMPT.txt FRAMES STEPS SEED OUTPUT_DIR\n";
return 2;
}
std::ifstream prompt_file(argv[3]);
const std::string prompt{std::istreambuf_iterator<char>(prompt_file), {}};
if (!prompt_file && prompt.empty()) throw std::runtime_error("cannot read prompt");
const auto frames = static_cast<unsigned>(std::stoul(argv[4]));
const auto steps = static_cast<unsigned>(std::stoul(argv[5]));
const auto seed = static_cast<std::uint64_t>(std::stoull(argv[6]));
auto model = kimodo::model::load(argv[1], argv[2]);
if (!model) throw std::runtime_error(model.error());
auto motion = (*model)->generate_text(prompt, frames, steps, seed, 2.F, 2.F);
if (!motion) throw std::runtime_error(motion.error());
const std::filesystem::path output(argv[7]);
std::filesystem::create_directories(output);
write_f32(output / "root_positions.f32", motion->root_positions);
write_f32(output / "local_rotations_xyzw.f32", motion->local_rotations_xyzw);
std::cout << "generated " << motion->frames << " SMPL-X22 frames\n";
return 0;
} catch (const std::exception &error) {
std::cerr << error.what() << '\n';
return 1;
}

123
src/ggml_weights.cpp Normal file
View File

@ -0,0 +1,123 @@
#include "ggml_weights.hpp"
#include "gguf.hpp"
#include <algorithm>
#include <cerrno>
#include <cstdlib>
#include <fstream>
#include <limits>
#include <thread>
#include <vector>
#include <ggml.h>
#include <ggml-alloc.h>
#include <ggml-backend.h>
#include <ggml-cpu.h>
#include <gguf.h>
#if defined(KIMODO_HAVE_GGML_VULKAN)
#include <ggml-vulkan.h>
#endif
namespace kimodo::detail {
namespace {
void configure_vulkan_f32_parity() noexcept {
#if defined(__unix__)
// Kimodo's reference model is F32. Current Vulkan cooperative-matrix
// paths convert F32 inputs to FP16 on this GPU, which breaks parity.
// Keep callers free to supply their own stricter environment, but make
// the correct reference-first path the default.
setenv("GGML_VK_DISABLE_COOPMAT", "1", 0);
setenv("GGML_VK_DISABLE_COOPMAT2", "1", 0);
setenv("GGML_VK_DISABLE_F16", "1", 0);
#endif
}
// GGML's CPU backend defaults to four threads. That is a sensible library
// default, but makes a full diffusion sample use only a small fraction of a
// typical workstation. Honour an explicit cap for predictable deployment
// and otherwise use the machine's advertised concurrency.
int cpu_thread_count() noexcept {
constexpr unsigned fallback = 4;
unsigned threads = std::thread::hardware_concurrency();
if (threads == 0) threads = fallback;
if (const char *value = std::getenv("KIMODO_THREADS")) {
char *end = nullptr;
errno = 0;
const long requested = std::strtol(value, &end, 10);
if (errno == 0 && end != value && *end == '\0' && requested > 0 &&
requested <= std::numeric_limits<int>::max()) {
threads = static_cast<unsigned>(requested);
}
}
return static_cast<int>(std::min<unsigned>(threads, std::numeric_limits<int>::max()));
}
} // namespace
std::expected<std::unique_ptr<ggml_motion_weights>, std::string> ggml_motion_weights::load(std::string_view path) {
auto checked = read_gguf_header(path);
if (!checked) return std::unexpected(checked.error());
if (auto valid = validate_motion_gguf(*checked); !valid) return std::unexpected(valid.error());
auto result = std::unique_ptr<ggml_motion_weights>(new ggml_motion_weights);
gguf_init_params params{true, &result->context_};
result->gguf_ = gguf_init_from_file(std::string(path).c_str(), params);
if (!result->gguf_ || !result->context_) return std::unexpected("GGML could not load checked motion GGUF");
// Vulkan is the normal inference path. Keep the CPU backend as a
// portability fallback, including for CI systems without a Vulkan ICD.
#if defined(KIMODO_HAVE_GGML_VULKAN)
// Retain a deterministic CPU escape hatch for parity triage. It is not
// a performance mode; a captured fixture can establish whether a drift
// belongs to the GGML graph or specifically to Vulkan.
const bool force_cpu = [] {
const char *value = std::getenv("KIMODO_BACKEND");
return value && std::string_view(value) == "cpu";
}();
if (!force_cpu) {
configure_vulkan_f32_parity();
if (ggml_backend_vk_get_device_count() > 0) result->backend_ = ggml_backend_vk_init(0);
}
#endif
if (!result->backend_) {
result->backend_ = ggml_backend_cpu_init();
if (!result->backend_) return std::unexpected("GGML CPU backend initialization failed");
ggml_backend_cpu_set_n_threads(result->backend_, cpu_thread_count());
}
result->buffer_ = ggml_backend_alloc_ctx_tensors(result->context_, result->backend_);
if (!result->buffer_) return std::unexpected("GGML motion weight allocation failed");
std::ifstream input(std::string(path), std::ios::binary);
if (!input) return std::unexpected("cannot reopen motion GGUF");
const size_t data_start = gguf_get_data_offset(result->gguf_);
std::vector<char> scratch(8U*1024U*1024U);
for (int64_t i=0;i<gguf_get_n_tensors(result->gguf_);++i) {
auto *tensor = ggml_get_tensor(result->context_, gguf_get_tensor_name(result->gguf_, i));
if (!tensor || tensor->type != GGML_TYPE_F32) return std::unexpected("motion GGUF contains an invalid non-F32 tensor");
const size_t bytes=ggml_nbytes(tensor), offset=gguf_get_tensor_offset(result->gguf_, i);
input.seekg(static_cast<std::streamoff>(data_start+offset));
for(size_t done=0;done<bytes;) {
const size_t chunk=std::min(scratch.size(), bytes-done);
input.read(scratch.data(), static_cast<std::streamsize>(chunk));
if (!input) return std::unexpected("short tensor data in motion GGUF");
ggml_backend_tensor_set(tensor, scratch.data(), done, chunk); done+=chunk;
}
}
return result;
}
ggml_motion_weights::~ggml_motion_weights() {
if (buffer_) ggml_backend_buffer_free(buffer_);
if (gguf_) gguf_free(gguf_);
if (context_) ggml_free(context_);
if (backend_) ggml_backend_free(backend_);
}
ggml_tensor *ggml_motion_weights::tensor(std::string_view name) const {
return context_ ? ggml_get_tensor(context_, std::string(name).c_str()) : nullptr;
}
std::expected<std::vector<float>, std::string> ggml_motion_weights::f32_values(std::string_view name) const {
auto *value = tensor(name);
if (!value || value->type != GGML_TYPE_F32) return std::unexpected("missing F32 GGML tensor: " + std::string(name));
std::vector<float> result(static_cast<size_t>(ggml_nelements(value)));
ggml_backend_tensor_get(value, result.data(), 0, result.size()*sizeof(float));
return result;
}
} // namespace kimodo::detail

39
src/ggml_weights.hpp Normal file
View File

@ -0,0 +1,39 @@
#pragma once
#include <expected>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
struct ggml_context;
struct gguf_context;
struct ggml_backend;
struct ggml_backend_buffer;
struct ggml_tensor;
namespace kimodo::detail {
// Owns a CPU-resident, F32 GGUF tensor set. Loading is deliberately separate
// from model-header validation so hostile files never reach a backend before
// the checked parser has accepted their Kimodo metadata and tensor directory.
class ggml_motion_weights {
public:
static std::expected<std::unique_ptr<ggml_motion_weights>, std::string> load(std::string_view path);
~ggml_motion_weights();
ggml_motion_weights(const ggml_motion_weights &) = delete;
ggml_motion_weights &operator=(const ggml_motion_weights &) = delete;
ggml_tensor *tensor(std::string_view name) const;
std::expected<std::vector<float>, std::string> f32_values(std::string_view name) const;
ggml_backend *backend() const noexcept { return backend_; }
private:
ggml_motion_weights() = default;
ggml_context *context_ = nullptr;
gguf_context *gguf_ = nullptr;
ggml_backend *backend_ = nullptr;
ggml_backend_buffer *buffer_ = nullptr;
};
} // namespace kimodo::detail

151
src/gguf.cpp Normal file
View File

@ -0,0 +1,151 @@
#include "gguf.hpp"
#include <algorithm>
#include <array>
#include <fstream>
#include <limits>
#include <string>
namespace kimodo::detail {
namespace {
constexpr std::uint32_t gguf_magic = 0x46554747; // "GGUF", little endian
constexpr std::uint32_t max_metadata = 100000;
constexpr std::uint64_t max_string_bytes = 16 * 1024 * 1024;
template <class T> bool read(std::istream &in, T &value) {
return static_cast<bool>(in.read(reinterpret_cast<char *>(&value), sizeof(value)));
}
bool read_string(std::istream &in, std::string &out) {
std::uint64_t size = 0;
if (!read(in, size) || size > max_string_bytes) return false;
out.resize(static_cast<size_t>(size));
return size == 0 || static_cast<bool>(in.read(out.data(), static_cast<std::streamsize>(size)));
}
bool skip(std::istream &in, std::uint32_t type) {
// GGUF metadata scalar types. Arrays are rejected below: Kimodo metadata
// never needs them in this small, hostile-input loader.
static constexpr std::array<unsigned, 12> widths{1, 1, 2, 2, 4, 4, 4, 1, 1, 1, 8, 8};
if (type >= widths.size()) return false;
in.seekg(widths[type], std::ios::cur);
return static_cast<bool>(in);
}
}
std::expected<gguf_file, std::string> read_gguf_header(std::string_view path) {
std::ifstream in(std::string(path), std::ios::binary);
if (!in) return std::unexpected("cannot open GGUF file");
std::uint32_t magic = 0, version = 0;
std::uint64_t tensor_count = 0, metadata_count = 0;
if (!read(in, magic) || !read(in, version) || !read(in, tensor_count) || !read(in, metadata_count))
return std::unexpected("truncated GGUF header");
if (magic != gguf_magic) return std::unexpected("not a GGUF file");
if (version < 2 || version > 3) return std::unexpected("unsupported GGUF version");
if (metadata_count > max_metadata || tensor_count > 10000000)
return std::unexpected("GGUF count exceeds safety limit");
gguf_file result;
for (std::uint64_t i = 0; i < metadata_count; ++i) {
std::string key;
std::uint32_t type = 0;
if (!read_string(in, key) || !read(in, type)) return std::unexpected("truncated GGUF metadata");
if (key.empty() || key.size() > 4096) return std::unexpected("invalid GGUF metadata key");
if (type == 8) {
std::string value;
if (!read_string(in, value)) return std::unexpected("invalid GGUF string metadata");
result.strings.emplace(std::move(key), std::move(value));
} else if (type == 10 || type == 11) {
std::uint64_t value = 0;
if (!read(in, value)) return std::unexpected("truncated GGUF integer metadata");
result.uints.emplace(std::move(key), value);
} else if (type == 9) {
return std::unexpected("GGUF array metadata is not accepted by the minimal loader");
} else if (!skip(in, type)) {
return std::unexpected("unsupported or truncated GGUF metadata");
}
}
// Tensor directory is also untrusted. Read every descriptor even though
// tensor mapping/graph construction is deferred, preventing a truncated
// or malformed file from being accepted merely because its metadata is
// well formed.
std::uint64_t max_tensor_end = 0;
for (std::uint64_t i = 0; i < tensor_count; ++i) {
std::string name;
std::uint32_t dimensions = 0, type = 0;
if (!read_string(in, name) || !read(in, dimensions) || name.empty() || dimensions == 0 || dimensions > 4)
return std::unexpected("invalid GGUF tensor descriptor");
if (!result.tensor_names.emplace(name).second) return std::unexpected("duplicate GGUF tensor name");
std::uint64_t elements = 1;
for (std::uint32_t dim = 0; dim < dimensions; ++dim) {
std::uint64_t extent = 0;
if (!read(in, extent) || extent == 0 || extent > 100000000 || elements > std::numeric_limits<std::uint64_t>::max() / extent)
return std::unexpected("invalid GGUF tensor dimension");
elements *= extent;
}
std::uint64_t offset = 0;
if (!read(in, type) || !read(in, offset) || type != 0 || offset % 32 != 0 || elements > std::numeric_limits<std::uint64_t>::max() / 4)
return std::unexpected("unsupported GGUF tensor type or offset");
if (offset > std::numeric_limits<std::uint64_t>::max() - elements * 4)
return std::unexpected("GGUF tensor offset overflows");
max_tensor_end = std::max(max_tensor_end, offset + elements * 4);
}
const auto directory_end = static_cast<std::uint64_t>(in.tellg());
if (directory_end == std::numeric_limits<std::uint64_t>::max()) return std::unexpected("invalid GGUF tensor directory");
const auto data_start = (directory_end + 31U) & ~std::uint64_t{31U};
in.seekg(0, std::ios::end);
const auto file_end = static_cast<std::uint64_t>(in.tellg());
if (file_end < data_start || max_tensor_end > file_end - data_start)
return std::unexpected("GGUF tensor data is truncated");
result.tensor_count = tensor_count;
return result;
}
std::expected<void, std::string> validate_motion_gguf(const gguf_file &file) {
const auto architecture = file.strings.find("general.architecture");
if (architecture == file.strings.end() || architecture->second != "kimodo-motion")
return std::unexpected("GGUF is not a Kimodo motion model");
const auto format = file.uints.find("kimodo.format_version");
if (format == file.uints.end() || format->second != 1)
return std::unexpected("unsupported Kimodo motion GGUF format");
const auto skeleton = file.strings.find("kimodo.skeleton");
if (skeleton == file.strings.end() || skeleton->second != "smplx22")
return std::unexpected("first runtime supports only smplx22 skeletons");
const auto width = file.uints.find("kimodo.text_embedding_width");
if (width == file.uints.end() || width->second != 4096)
return std::unexpected("motion GGUF has incompatible text embedding width");
if (file.tensor_count != 414)
return std::unexpected("motion GGUF has an unexpected tensor count");
for (const char *stage : {"root_model", "body_model"}) {
for (const char *name : {"embed_text.weight", "embed_text.bias", "input_linear.weight", "input_linear.bias",
"output_linear.weight", "output_linear.bias", "linear_first_heading_angle.weight",
"linear_first_heading_angle.bias", "embed_timestep.time_embed.0.weight",
"embed_timestep.time_embed.0.bias", "embed_timestep.time_embed.2.weight",
"embed_timestep.time_embed.2.bias"}) {
if (!file.tensor_names.contains(std::string(stage) + "." + name))
return std::unexpected("motion GGUF is missing a transformer projection tensor");
}
for (unsigned layer = 0; layer < 16; ++layer) {
const std::string prefix = std::string(stage) + ".seqTransEncoder.layers." + std::to_string(layer) + ".";
for (const char *name : {"linear1.weight", "linear1.bias", "linear2.weight", "linear2.bias", "norm1.weight",
"norm1.bias", "norm2.weight", "norm2.bias", "self_attn.in_proj_weight",
"self_attn.in_proj_bias", "self_attn.out_proj.weight", "self_attn.out_proj.bias"}) {
if (!file.tensor_names.contains(prefix + name)) return std::unexpected("motion GGUF is missing a transformer layer tensor");
}
}
}
for (const char *group : {"global_root", "local_root", "body"})
for (const char *stat : {"mean", "std"})
if (!file.tensor_names.contains(std::string("stats.") + group + "." + stat))
return std::unexpected("motion GGUF is missing normalization statistics");
return {};
}
std::expected<void, std::string> validate_text_gguf(const gguf_file &file) {
const auto architecture = file.strings.find("general.architecture");
if (architecture == file.strings.end() || architecture->second != "kimodo-llm2vec")
return std::unexpected("GGUF is not a Kimodo LLM2Vec model");
const auto width = file.uints.find("kimodo.text_embedding_width");
if (width == file.uints.end() || width->second != 4096)
return std::unexpected("text GGUF has incompatible embedding width");
return {};
}
} // namespace kimodo::detail

23
src/gguf.hpp Normal file
View File

@ -0,0 +1,23 @@
#pragma once
#include <cstdint>
#include <expected>
#include <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
namespace kimodo::detail {
struct gguf_file {
std::unordered_map<std::string, std::string> strings;
std::unordered_map<std::string, std::uint64_t> uints;
std::uint64_t tensor_count = 0;
std::unordered_set<std::string> tensor_names;
};
std::expected<gguf_file, std::string> read_gguf_header(std::string_view path);
std::expected<void, std::string> validate_motion_gguf(const gguf_file &file);
std::expected<void, std::string> validate_text_gguf(const gguf_file &file);
} // namespace kimodo::detail

26
src/inspect.cpp Normal file
View File

@ -0,0 +1,26 @@
#include <kimodo/kimodo.hpp>
#include <cstdio>
#if defined(KIMODO_HAVE_GGML)
#include <gguf.h>
#endif
int main(int argc, char **argv) {
if (argc != 2) { std::fprintf(stderr, "usage: kmd-inspect MOTION.gguf\n"); return 2; }
auto model = kimodo::model::load(argv[1]);
if (!model) { std::fprintf(stderr, "invalid Kimodo motion GGUF: %s\n", model.error().c_str()); return 1; }
#if defined(KIMODO_HAVE_GGML)
ggml_context *tensor_context = nullptr;
gguf_init_params parameters{true, &tensor_context};
gguf_context *file = gguf_init_from_file(argv[1], parameters);
if (!file || !tensor_context || gguf_get_n_tensors(file) != 414) {
if (file) gguf_free(file);
if (tensor_context) ggml_free(tensor_context);
std::fputs("GGML rejected the otherwise valid GGUF\n", stderr);
return 1;
}
gguf_free(file); ggml_free(tensor_context);
#endif
std::puts("Kimodo SMPL-X motion GGUF: valid (414 F32 tensors)");
}

287
src/llm_text_encoder.cpp Normal file
View File

@ -0,0 +1,287 @@
// Attention layout is independently implemented with llama.cpp
// src/llama-graph.cpp at 78ec4c378031811671d1c76a067acbee4f4c56ce as a
// reference. No llama.cpp source is copied.
#include "llm_text_encoder.hpp"
#include "llm_tokenizer.hpp"
#include <ggml.h>
#include <ggml-alloc.h>
#include <ggml-backend.h>
#include <ggml-cpu.h>
#include <ggml-vulkan.h>
#include <gguf.h>
#include <algorithm>
#include <array>
#include <charconv>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <memory>
#include <stdexcept>
#include <thread>
#include <vector>
namespace kimodo::detail {
namespace {
constexpr int64_t hidden = 4096, heads = 32, kv_heads = 8, head_dim = 128;
bool use_vulkan() {
const char *choice = std::getenv("KIMODO_BACKEND");
return !choice || std::string_view(choice) != "cpu";
}
int layer_chunk_size() {
constexpr int fallback = 8;
const char *value = std::getenv("KIMODO_TEXT_LAYER_CHUNK");
if (!value) return fallback;
int parsed = 0;
const auto [end, error] = std::from_chars(value, value + std::strlen(value), parsed);
if (error != std::errc{} || *end != '\0' || parsed < 1 || parsed > 32)
throw std::runtime_error("KIMODO_TEXT_LAYER_CHUNK must be in 1..32");
return parsed;
}
struct component {
ggml_context *ctx = nullptr;
gguf_context *file = nullptr;
ggml_backend_buffer_t weights = nullptr;
~component() {
if (weights) ggml_backend_buffer_free(weights);
if (file) gguf_free(file);
if (ctx) ggml_free(ctx);
}
ggml_tensor *tensor(const char *name) const { return ggml_get_tensor(ctx, name); }
};
std::unique_ptr<component> open_component(const std::filesystem::path &path, ggml_backend_t backend) {
auto result = std::make_unique<component>();
gguf_init_params params{true, &result->ctx};
result->file = gguf_init_from_file(path.c_str(), params);
if (!result->file || !result->ctx)
throw std::runtime_error("cannot load text component " + path.string());
result->weights = ggml_backend_alloc_ctx_tensors(result->ctx, backend);
if (!result->weights) throw std::runtime_error("cannot allocate text component " + path.string());
std::ifstream in(path, std::ios::binary);
if (!in) throw std::runtime_error("cannot reopen text component " + path.string());
const auto data_offset = gguf_get_data_offset(result->file);
std::vector<char> scratch(8U * 1024U * 1024U);
for (int64_t i = 0; i < gguf_get_n_tensors(result->file); ++i) {
auto *tensor = ggml_get_tensor(result->ctx, gguf_get_tensor_name(result->file, i));
if (!tensor || (tensor->type != GGML_TYPE_BF16 && tensor->type != GGML_TYPE_F32))
throw std::runtime_error("invalid text tensor");
const size_t bytes = ggml_nbytes(tensor);
const size_t offset = gguf_get_tensor_offset(result->file, i);
in.seekg(static_cast<std::streamoff>(data_offset + offset));
for (size_t done = 0; done < bytes;) {
const size_t n = std::min(scratch.size(), bytes - done);
in.read(scratch.data(), static_cast<std::streamsize>(n));
if (!in) throw std::runtime_error("truncated text tensor");
ggml_backend_tensor_set(tensor, scratch.data(), done, n);
done += n;
}
}
return result;
}
std::vector<float> read_output(ggml_tensor *tensor) {
std::vector<float> result(ggml_nelements(tensor));
if (tensor->type == GGML_TYPE_F32) {
ggml_backend_tensor_get(tensor, result.data(), 0, result.size() * sizeof(float));
return result;
}
if (tensor->type == GGML_TYPE_BF16) {
std::vector<uint16_t> raw(result.size());
ggml_backend_tensor_get(tensor, raw.data(), 0, raw.size() * sizeof(uint16_t));
for (size_t i = 0; i < result.size(); ++i) {
const uint32_t bits = uint32_t(raw[i]) << 16;
std::memcpy(&result[i], &bits, sizeof(float));
}
return result;
}
throw std::runtime_error("unsupported text output type");
}
ggml_tensor *norm(ggml_context *ctx, ggml_tensor *x, ggml_tensor *weight) {
auto *normalized = ggml_rms_norm(ctx, x->type == GGML_TYPE_F32 ? x : ggml_cast(ctx, x, GGML_TYPE_F32), 1e-5F);
if (x->type == GGML_TYPE_BF16) {
normalized = ggml_cast(ctx, normalized, GGML_TYPE_BF16);
auto *repeated = ggml_repeat(ctx, weight, normalized);
return ggml_cast(ctx, ggml_mul(ctx, ggml_cast(ctx, normalized, GGML_TYPE_F32),
ggml_cast(ctx, repeated, GGML_TYPE_F32)), GGML_TYPE_BF16);
}
return ggml_mul(ctx, normalized, ggml_repeat(ctx, ggml_cast(ctx, weight, GGML_TYPE_F32), normalized));
}
ggml_tensor *repeat_kv(ggml_context *ctx, ggml_tensor *x, int64_t seq) {
auto *value = ggml_reshape_4d(ctx, x, head_dim, kv_heads, 1, seq);
auto *shape = ggml_new_tensor_4d(ctx, x->type, head_dim, kv_heads, heads / kv_heads, seq);
value = ggml_repeat(ctx, value, shape);
value = ggml_cont(ctx, ggml_permute(ctx, value, 0, 2, 1, 3));
return ggml_reshape_3d(ctx, value, head_dim, heads, seq);
}
ggml_tensor *layer_graph(ggml_context *ctx, ggml_tensor *x, ggml_tensor *positions,
const component &model, int64_t seq) {
auto base = [&](const char *name, ggml_tensor *value) {
const std::string prefix(name);
auto *weight = model.tensor((prefix + "_base.weight").c_str());
if (!weight || weight->type != GGML_TYPE_BF16) throw std::runtime_error("missing base projection");
// Vulkan's BF16 matrix-vector kernel rejects BF16 right operands. A
// F32 cast preserves the BF16 values while taking its supported path.
return ggml_mul_mat(ctx, weight, value->type == GGML_TYPE_F32 ? value : ggml_cast(ctx, value, GGML_TYPE_F32));
};
auto linear = [&](const char *name, ggml_tensor *value) {
const std::string prefix(name);
auto *a = model.tensor((prefix + "_lora_a.weight").c_str());
auto *b = model.tensor((prefix + "_lora_b.weight").c_str());
if (!a || !b) throw std::runtime_error("missing LoRA projection");
auto *lora = ggml_mul_mat(ctx, b, ggml_mul_mat(ctx, a,
value->type == GGML_TYPE_F32 ? value : ggml_cast(ctx, value, GGML_TYPE_F32)));
return ggml_add(ctx, ggml_cast(ctx, base(name, value), GGML_TYPE_F32), ggml_scale(ctx, lora, 2.F));
};
auto *attn_norm = model.tensor("attn_norm.weight");
auto *ffn_norm = model.tensor("ffn_norm.weight");
if (!attn_norm || !ffn_norm) throw std::runtime_error("missing layer norm");
auto *residual = ggml_cast(ctx, x, GGML_TYPE_BF16);
auto *q = linear("attn_q_proj", norm(ctx, residual, attn_norm));
auto *k = linear("attn_k_proj", norm(ctx, residual, attn_norm));
auto *v = linear("attn_v_proj", norm(ctx, residual, attn_norm));
q = ggml_reshape_3d(ctx, q, head_dim, heads, seq);
k = ggml_reshape_3d(ctx, k, head_dim, kv_heads, seq);
v = ggml_reshape_3d(ctx, v, head_dim, kv_heads, seq);
q = ggml_rope_ext(ctx, q, positions, nullptr, head_dim, GGML_ROPE_TYPE_NEOX, 8192, 500000.F, 1, 0, 1, 0, 0);
k = ggml_rope_ext(ctx, k, positions, nullptr, head_dim, GGML_ROPE_TYPE_NEOX, 8192, 500000.F, 1, 0, 1, 0, 0);
k = repeat_kv(ctx, k, seq);
v = repeat_kv(ctx, v, seq);
q = ggml_permute(ctx, q, 0, 2, 1, 3);
k = ggml_permute(ctx, k, 0, 2, 1, 3);
v = ggml_permute(ctx, v, 0, 2, 1, 3);
auto *probability = ggml_soft_max(ctx, ggml_scale(ctx, ggml_mul_mat(ctx, k, q), 1.F / std::sqrt(float(head_dim))));
v = ggml_cont(ctx, ggml_transpose(ctx, v));
auto *attention = ggml_cont(ctx, ggml_permute(ctx, ggml_mul_mat(ctx, v, probability), 0, 2, 1, 3));
auto *output = ggml_add(ctx, ggml_cast(ctx, residual, GGML_TYPE_F32),
linear("attn_o_proj", ggml_reshape_2d(ctx, attention, hidden, seq)));
auto *hidden_norm = norm(ctx, output, ffn_norm);
auto *gate = ggml_silu(ctx, linear("ffn_gate_proj", hidden_norm));
output = ggml_add(ctx, output, linear("ffn_down_proj", ggml_mul(ctx, gate, linear("ffn_up_proj", hidden_norm))));
return output;
}
std::vector<float> run_layer_chunk(const std::vector<std::unique_ptr<component>> &layers,
const std::vector<float> &input, ggml_backend_t backend) {
const int64_t seq = static_cast<int64_t>(input.size() / hidden);
auto *ctx = ggml_init({128ULL * 1024ULL * 1024ULL, nullptr, true});
if (!ctx) throw std::runtime_error("layer chunk graph allocation failed");
auto cleanup = std::unique_ptr<ggml_context, decltype(&ggml_free)>(ctx, ggml_free);
auto *state = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, hidden, seq);
auto *input_state = state;
auto *positions = ggml_new_tensor_1d(ctx, GGML_TYPE_I32, seq);
ggml_set_input(state);
ggml_set_input(positions);
for (const auto &layer : layers) state = layer_graph(ctx, state, positions, *layer, seq);
auto *graph = ggml_new_graph(ctx);
ggml_build_forward_expand(graph, state);
auto *buffer = ggml_backend_alloc_ctx_tensors(ctx, backend);
if (!buffer) throw std::runtime_error("layer chunk backend allocation failed");
auto release = std::unique_ptr<ggml_backend_buffer, decltype(&ggml_backend_buffer_free)>(buffer, ggml_backend_buffer_free);
std::vector<int32_t> position_values(static_cast<size_t>(seq));
for (int32_t i = 0; i < seq; ++i) position_values[static_cast<size_t>(i)] = i;
ggml_backend_tensor_set(input_state, input.data(), 0, input.size() * sizeof(float));
ggml_backend_tensor_set(positions, position_values.data(), 0, position_values.size() * sizeof(int32_t));
if (ggml_backend_graph_compute(backend, graph) != GGML_STATUS_SUCCESS)
throw std::runtime_error("layer chunk graph failed");
return read_output(state);
}
} // namespace
struct llm_text_encoder::impl {
std::filesystem::path directory;
std::unique_ptr<llm_tokenizer> tokenizer;
ggml_backend_t backend = nullptr;
~impl() { if (backend) ggml_backend_free(backend); }
};
llm_text_encoder::~llm_text_encoder() = default;
std::expected<std::unique_ptr<llm_text_encoder>, std::string> llm_text_encoder::load(std::string_view directory) try {
const auto path = std::filesystem::path(directory);
if (!std::filesystem::is_directory(path)) return std::unexpected("text model must be a component directory");
for (const auto &name : {"tokenizer.gguf", "embedding.gguf", "final-norm.gguf"})
if (!std::filesystem::is_regular_file(path / name)) return std::unexpected("text bundle missing " + std::string(name));
for (int i = 0; i < 32; ++i) { char name[32]; std::snprintf(name, sizeof(name), "layer-%02d.gguf", i); if (!std::filesystem::is_regular_file(path / name)) return std::unexpected("text bundle missing " + std::string(name)); }
auto result = std::unique_ptr<llm_text_encoder>(new llm_text_encoder);
result->impl_ = std::make_unique<impl>();
if (use_vulkan() && ggml_backend_vk_get_device_count()) result->impl_->backend = ggml_backend_vk_init(0);
if (!result->impl_->backend) {
result->impl_->backend = ggml_backend_cpu_init();
if (!result->impl_->backend) return std::unexpected("cannot initialize text backend");
ggml_backend_cpu_set_n_threads(result->impl_->backend, static_cast<int>(std::max(1U, std::thread::hardware_concurrency())));
}
auto tokenizer = llm_tokenizer::load((path / "tokenizer.gguf").string());
if (!tokenizer) return std::unexpected(tokenizer.error());
result->impl_->directory = path;
result->impl_->tokenizer = std::move(*tokenizer);
return result;
} catch (const std::exception &error) { return std::unexpected(error.what()); }
std::expected<std::array<float, 4096>, std::string> llm_text_encoder::encode(std::string_view prompt) const try {
auto ids = impl_->tokenizer->encode(prompt);
if (!ids) return std::unexpected(ids.error());
if (ids->size() < 2 || ids->size() > 512) return std::unexpected("prompt token count must be in 1..511 excluding BOS");
std::vector<float> state;
{
auto embedding = open_component(impl_->directory / "embedding.gguf", impl_->backend);
auto *weight = embedding->tensor("token_embedding.weight");
if (!weight) throw std::runtime_error("text bundle missing token_embedding.weight");
auto *ctx = ggml_init({2ULL * 1024ULL * 1024ULL, nullptr, true});
if (!ctx) throw std::runtime_error("embedding graph allocation failed");
auto cleanup = std::unique_ptr<ggml_context, decltype(&ggml_free)>(ctx, ggml_free);
auto *indices = ggml_new_tensor_1d(ctx, GGML_TYPE_I32, ids->size());
ggml_set_input(indices);
auto *rows = ggml_get_rows(ctx, weight, indices);
auto *graph = ggml_new_graph(ctx); ggml_build_forward_expand(graph, rows);
auto *buffer = ggml_backend_alloc_ctx_tensors(ctx, impl_->backend);
if (!buffer) throw std::runtime_error("embedding graph backend allocation failed");
auto release = std::unique_ptr<ggml_backend_buffer, decltype(&ggml_backend_buffer_free)>(buffer, ggml_backend_buffer_free);
std::vector<int32_t> values(ids->begin(), ids->end());
ggml_backend_tensor_set(indices, values.data(), 0, values.size() * sizeof(int32_t));
if (ggml_backend_graph_compute(impl_->backend, graph) != GGML_STATUS_SUCCESS) throw std::runtime_error("embedding graph failed");
state = read_output(rows);
}
const int chunk = layer_chunk_size();
for (int first = 0; first < 32; first += chunk) {
std::vector<std::unique_ptr<component>> layers;
for (int i = first; i < std::min(first + chunk, 32); ++i) {
char name[32]; std::snprintf(name, sizeof(name), "layer-%02d.gguf", i);
layers.push_back(open_component(impl_->directory / name, impl_->backend));
}
state = run_layer_chunk(layers, state, impl_->backend);
}
auto final = open_component(impl_->directory / "final-norm.gguf", impl_->backend);
auto *weight = final->tensor("final_norm.weight");
if (!weight) throw std::runtime_error("text bundle missing final_norm.weight");
auto *ctx = ggml_init({8ULL * 1024ULL * 1024ULL, nullptr, true});
if (!ctx) throw std::runtime_error("final norm graph allocation failed");
auto cleanup = std::unique_ptr<ggml_context, decltype(&ggml_free)>(ctx, ggml_free);
auto *input = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, hidden, ids->size()); ggml_set_input(input);
auto *normalized = ggml_rms_norm(ctx, input, 1e-5F);
auto *output = ggml_mul(ctx, normalized, ggml_repeat(ctx, ggml_cast(ctx, weight, GGML_TYPE_F32), normalized));
auto *graph = ggml_new_graph(ctx); ggml_build_forward_expand(graph, output);
auto *buffer = ggml_backend_alloc_ctx_tensors(ctx, impl_->backend);
if (!buffer) throw std::runtime_error("final norm graph backend allocation failed");
auto release = std::unique_ptr<ggml_backend_buffer, decltype(&ggml_backend_buffer_free)>(buffer, ggml_backend_buffer_free);
ggml_backend_tensor_set(input, state.data(), 0, state.size() * sizeof(float));
if (ggml_backend_graph_compute(impl_->backend, graph) != GGML_STATUS_SUCCESS) throw std::runtime_error("final norm graph failed");
state = read_output(output);
std::array<float, 4096> pooled{};
for (size_t token = 1; token < ids->size(); ++token)
for (size_t dim = 0; dim < pooled.size(); ++dim) pooled[dim] += state[token * pooled.size() + dim];
for (float &value : pooled) value /= float(ids->size() - 1);
return pooled;
} catch (const std::exception &error) { return std::unexpected(error.what()); }
} // namespace kimodo::detail

27
src/llm_text_encoder.hpp Normal file
View File

@ -0,0 +1,27 @@
#pragma once
#include <array>
#include <expected>
#include <memory>
#include <string>
#include <string_view>
namespace kimodo::detail {
class llm_text_encoder {
public:
// A text bundle is a directory containing tokenizer.gguf, embedding.gguf,
// final-norm.gguf, and layer-00.gguf through layer-31.gguf. Components
// are loaded serially so only one transformer layer is GPU-resident.
static std::expected<std::unique_ptr<llm_text_encoder>, std::string> load(std::string_view bundle_directory);
std::expected<std::array<float, 4096>, std::string> encode(std::string_view utf8_prompt) const;
~llm_text_encoder();
llm_text_encoder(const llm_text_encoder &) = delete;
llm_text_encoder &operator=(const llm_text_encoder &) = delete;
private:
llm_text_encoder() = default;
struct impl;
std::unique_ptr<impl> impl_;
};
} // namespace kimodo::detail

148
src/llm_tokenizer.cpp Normal file
View File

@ -0,0 +1,148 @@
// The byte-level BPE merge priority and Llama-3 pre-tokenization ordering
// were independently implemented with llama.cpp src/llama-vocab.cpp at
// 78ec4c378031811671d1c76a067acbee4f4c56ce as a reference. No llama.cpp
// source is copied here.
#include "llm_tokenizer.hpp"
#include <gguf.h>
#include <algorithm>
#include <array>
#include <cctype>
#include <cstdint>
#include <limits>
#include <unordered_map>
namespace kimodo::detail {
namespace {
constexpr char separator = '\x1f';
std::string utf8(std::uint32_t codepoint) {
std::string result;
if (codepoint < 0x80) result.push_back(static_cast<char>(codepoint));
else if (codepoint < 0x800) { result.push_back(static_cast<char>(0xc0 | (codepoint >> 6))); result.push_back(static_cast<char>(0x80 | (codepoint & 0x3f))); }
else { result.push_back(static_cast<char>(0xe0 | (codepoint >> 12))); result.push_back(static_cast<char>(0x80 | ((codepoint >> 6) & 0x3f))); result.push_back(static_cast<char>(0x80 | (codepoint & 0x3f))); }
return result;
}
bool ascii_alpha(unsigned char c) { return std::isalpha(c) != 0 || c >= 0x80; }
bool ascii_digit(unsigned char c) { return c >= '0' && c <= '9'; }
bool ascii_space(unsigned char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; }
bool contraction(std::string_view text, size_t at, size_t &length) {
if (at >= text.size() || text[at] != '\'') return false;
const auto lower = [](char c) { return static_cast<char>(std::tolower(static_cast<unsigned char>(c))); };
const std::string_view rest = text.substr(at);
for (const std::string_view option : {"'re", "'ve", "'ll", "'s", "'t", "'m", "'d"}) {
if (rest.size() >= option.size() && std::equal(option.begin(), option.end(), rest.begin(), [&](char a, char b) { return a == lower(b); })) { length = option.size(); return true; }
}
return false;
}
bool valid_utf8(std::string_view text) {
for (size_t i = 0; i < text.size();) {
const unsigned char first = static_cast<unsigned char>(text[i]);
if (first < 0x80) { ++i; continue; }
unsigned continuation = 0;
if (first >= 0xc2 && first <= 0xdf) continuation = 1;
else if (first >= 0xe0 && first <= 0xef) continuation = 2;
else if (first >= 0xf0 && first <= 0xf4) continuation = 3;
else return false;
if (i + continuation >= text.size()) return false;
for (unsigned offset = 1; offset <= continuation; ++offset)
if ((static_cast<unsigned char>(text[i + offset]) & 0xc0) != 0x80) return false;
// Reject overlong encodings, surrogate scalars, and code points above
// U+10FFFF. Byte-BPE itself operates on bytes, but the public API's
// contract is UTF-8 and must reject malformed caller input.
if ((first == 0xe0 && static_cast<unsigned char>(text[i + 1]) < 0xa0) ||
(first == 0xed && static_cast<unsigned char>(text[i + 1]) >= 0xa0) ||
(first == 0xf0 && static_cast<unsigned char>(text[i + 1]) < 0x90) ||
(first == 0xf4 && static_cast<unsigned char>(text[i + 1]) >= 0x90)) return false;
i += continuation + 1;
}
return true;
}
}
struct llm_tokenizer::impl {
std::unordered_map<std::string, int> token_ids;
std::unordered_map<std::string, int> merge_ranks;
std::array<std::string, 256> byte_encode;
int bos = 128000;
};
llm_tokenizer::~llm_tokenizer() = default;
std::expected<std::unique_ptr<llm_tokenizer>, std::string> llm_tokenizer::load(std::string_view path) {
gguf_init_params params{false, nullptr};
gguf_context *file = gguf_init_from_file(std::string(path).c_str(), params);
if (!file) return std::unexpected("cannot load tokenizer GGUF");
const auto release = std::unique_ptr<gguf_context, decltype(&gguf_free)>(file, gguf_free);
auto key = [&](const char *name) { const auto value = gguf_find_key(file, name); if (value < 0) throw std::runtime_error(std::string("missing tokenizer key: ") + name); return value; };
try {
const auto architecture = key("general.architecture");
if (std::string_view(gguf_get_val_str(file, architecture)) != "kimodo-llm2vec-tokenizer") return std::unexpected("not a Kimodo LLM2Vec tokenizer GGUF");
const auto tokens_key = key("kimodo.tokenizer.tokens"), merges_key = key("kimodo.tokenizer.merges");
if (gguf_get_arr_type(file, tokens_key) != GGUF_TYPE_STRING || gguf_get_arr_type(file, merges_key) != GGUF_TYPE_STRING || gguf_get_arr_n(file, tokens_key) != 128000 || gguf_get_arr_n(file, merges_key) != 280147) return std::unexpected("invalid Llama-3 tokenizer GGUF arrays");
auto result = std::unique_ptr<llm_tokenizer>(new llm_tokenizer);
result->impl_ = std::make_unique<impl>();
result->impl_->token_ids.reserve(128000);
result->impl_->merge_ranks.reserve(280147);
for (size_t i = 0; i < 128000; ++i) {
const char *value = gguf_get_arr_str(file, tokens_key, i);
if (!value || !result->impl_->token_ids.emplace(value, static_cast<int>(i)).second) return std::unexpected("invalid duplicate tokenizer token");
}
for (size_t i = 0; i < 280147; ++i) {
std::string merge = gguf_get_arr_str(file, merges_key, i);
const auto split = merge.find(' ');
if (split == std::string::npos || split == 0 || split + 1 == merge.size()) return std::unexpected("invalid BPE merge");
merge[split] = separator;
if (!result->impl_->merge_ranks.emplace(std::move(merge), static_cast<int>(i)).second) return std::unexpected("duplicate BPE merge");
}
std::array<bool, 256> direct{};
for (unsigned i = 33; i <= 126; ++i) direct[i] = true;
for (unsigned i = 161; i <= 172; ++i) direct[i] = true;
for (unsigned i = 174; i <= 255; ++i) direct[i] = true;
std::uint32_t extra = 256;
for (unsigned i = 0; i < 256; ++i) result->impl_->byte_encode[i] = utf8(direct[i] ? i : extra++);
return result;
} catch (const std::exception &error) { return std::unexpected(error.what()); }
}
std::expected<std::vector<int>, std::string> llm_tokenizer::encode(std::string_view text) const {
if (!impl_) return std::unexpected("invalid tokenizer");
if (!valid_utf8(text)) return std::unexpected("prompt is not valid UTF-8");
std::vector<std::string> words;
for (size_t pos = 0; pos < text.size();) {
size_t size = 0;
if (contraction(text, pos, size)) { words.emplace_back(text.substr(pos, size)); pos += size; continue; }
const unsigned char first = static_cast<unsigned char>(text[pos]);
const bool prefixed_letter = !ascii_digit(first) && first != '\r' && first != '\n' && !ascii_alpha(first) && pos + 1 < text.size() && ascii_alpha(static_cast<unsigned char>(text[pos + 1]));
if (ascii_alpha(first) || prefixed_letter) { size = prefixed_letter ? 1 : 0; while (pos + size < text.size() && ascii_alpha(static_cast<unsigned char>(text[pos + size]))) ++size; words.emplace_back(text.substr(pos, size)); pos += size; continue; }
if (ascii_digit(first)) { while (size < 3 && pos + size < text.size() && ascii_digit(static_cast<unsigned char>(text[pos + size]))) ++size; words.emplace_back(text.substr(pos, size)); pos += size; continue; }
if (!ascii_space(first) || (pos + 1 < text.size() && !ascii_space(static_cast<unsigned char>(text[pos + 1])) && !ascii_alpha(static_cast<unsigned char>(text[pos + 1])) && !ascii_digit(static_cast<unsigned char>(text[pos + 1])))) {
size = first == ' ' ? 1 : 0; while (pos + size < text.size() && !ascii_space(static_cast<unsigned char>(text[pos + size])) && !ascii_alpha(static_cast<unsigned char>(text[pos + size])) && !ascii_digit(static_cast<unsigned char>(text[pos + size]))) ++size; words.emplace_back(text.substr(pos, size)); pos += size; continue;
}
while (pos + size < text.size() && ascii_space(static_cast<unsigned char>(text[pos + size]))) ++size;
words.emplace_back(text.substr(pos, size)); pos += size;
}
std::vector<int> result{impl_->bos};
for (const auto &word : words) {
std::vector<std::string> symbols;
for (unsigned char byte : word) symbols.push_back(impl_->byte_encode[byte]);
while (symbols.size() > 1) {
int best_rank = std::numeric_limits<int>::max(); size_t best = symbols.size();
for (size_t i = 0; i + 1 < symbols.size(); ++i) {
const auto found = impl_->merge_ranks.find(symbols[i] + separator + symbols[i + 1]);
if (found != impl_->merge_ranks.end() && found->second < best_rank) { best_rank = found->second; best = i; }
}
if (best == symbols.size()) break;
symbols[best] += symbols[best + 1]; symbols.erase(symbols.begin() + static_cast<std::ptrdiff_t>(best + 1));
}
for (const auto &symbol : symbols) {
const auto found = impl_->token_ids.find(symbol);
if (found == impl_->token_ids.end()) return std::unexpected("BPE symbol is absent from vocabulary");
result.push_back(found->second);
}
}
return result;
}
} // namespace kimodo::detail

26
src/llm_tokenizer.hpp Normal file
View File

@ -0,0 +1,26 @@
#pragma once
#include <expected>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
namespace kimodo::detail {
// Llama-3 byte-level BPE. Its split/merge ordering is independently
// implemented from tokenizer.json; see the attribution in the .cpp file.
class llm_tokenizer {
public:
static std::expected<std::unique_ptr<llm_tokenizer>, std::string> load(std::string_view tokenizer_gguf);
std::expected<std::vector<int>, std::string> encode(std::string_view prepared_text) const;
~llm_tokenizer();
llm_tokenizer(const llm_tokenizer &) = delete;
llm_tokenizer &operator=(const llm_tokenizer &) = delete;
private:
llm_tokenizer() = default;
struct impl;
std::unique_ptr<impl> impl_;
};
} // namespace kimodo::detail

97
src/model.cpp Normal file
View File

@ -0,0 +1,97 @@
#include <kimodo/kimodo.hpp>
#include "gguf.hpp"
#ifdef KIMODO_HAVE_GGML
#include "ggml_weights.hpp"
#include "denoiser.hpp"
#include "motion_decode.hpp"
#include "llm_text_encoder.hpp"
#endif
#include <cmath>
#include <random>
namespace kimodo {
struct model::impl {
detail::gguf_file motion;
std::string motion_path;
#ifdef KIMODO_HAVE_GGML
mutable std::unique_ptr<detail::ggml_motion_weights> weights;
std::unique_ptr<detail::llm_text_encoder> text;
#endif
};
model::model(std::unique_ptr<impl> state) : impl_(std::move(state)) {}
model::~model() = default;
std::expected<std::unique_ptr<model>, std::string> model::load(std::string_view motion_path, std::string_view text_path) {
auto file = detail::read_gguf_header(motion_path);
if (!file) return std::unexpected(file.error());
if (auto valid = detail::validate_motion_gguf(*file); !valid) return std::unexpected(valid.error());
auto state = std::make_unique<impl>();
state->motion = std::move(*file);
state->motion_path = std::string(motion_path);
#ifdef KIMODO_HAVE_GGML
if (!text_path.empty()) {
auto text = detail::llm_text_encoder::load(text_path);
if (!text) return std::unexpected(text.error());
state->text = std::move(*text);
}
#else
if (!text_path.empty()) return std::unexpected("Kimodo was built without GGML support");
#endif
return std::unique_ptr<model>(new model(std::move(state)));
}
std::expected<motion_data, std::string> model::generate_text(
std::string_view utf8_prompt, unsigned frames, unsigned steps, std::uint64_t seed,
float text_cfg, float constraint_cfg) const {
#ifdef KIMODO_HAVE_GGML
if (!impl_->text) return std::unexpected("model was loaded without a native text bundle");
auto embedding = impl_->text->encode(utf8_prompt);
if (!embedding) return std::unexpected(embedding.error());
return generate_embedding(*embedding, frames, steps, seed, text_cfg, constraint_cfg);
#else
(void) utf8_prompt; (void) frames; (void) steps; (void) seed; (void) text_cfg; (void) constraint_cfg;
return std::unexpected("Kimodo was built without GGML support");
#endif
}
std::expected<motion_data, std::string> model::generate_embedding(
const std::array<float, embedding_width> &embedding, unsigned frames, unsigned steps,
std::uint64_t seed, float text_cfg, float constraint_cfg) const {
if (frames == 0 || frames > 10000) return std::unexpected("frames must be in 1..10000");
if (steps == 0 || steps > 1000) return std::unexpected("diffusion_steps must be in 1..1000");
if (!std::isfinite(text_cfg) || !std::isfinite(constraint_cfg)) return std::unexpected("CFG weights must be finite");
for (float value : embedding) if (!std::isfinite(value)) return std::unexpected("embedding contains a non-finite value");
#ifdef KIMODO_HAVE_GGML
// Weight residency is deferred until inference so model-load stays a
// bounded metadata operation. The graph integration consumes this exact
// session; no separate unchecked tensor loader exists in the runtime.
if (!impl_->weights) {
auto loaded = detail::ggml_motion_weights::load(impl_->motion_path);
if (!loaded) return std::unexpected(loaded.error());
impl_->weights = std::move(*loaded);
}
std::mt19937_64 rng(seed);
std::normal_distribution<float> normal(0.f, 1.f);
std::vector<float> noise(static_cast<size_t>(frames)*273);
for (float &value : noise) value = normal(rng);
auto sampled = detail::sample_motion_from_noise(*impl_->weights, noise, embedding, frames, steps, text_cfg, constraint_cfg);
if (!sampled) return std::unexpected(sampled.error());
auto global_mean=impl_->weights->f32_values("stats.global_root.mean"), global_std=impl_->weights->f32_values("stats.global_root.std");
auto body_mean=impl_->weights->f32_values("stats.body.mean"), body_std=impl_->weights->f32_values("stats.body.std");
if (!global_mean) return std::unexpected(global_mean.error());
if (!global_std) return std::unexpected(global_std.error());
if (!body_mean) return std::unexpected(body_mean.error());
if (!body_std) return std::unexpected(body_std.error());
auto decoded=detail::decode_smplx22(*sampled,frames,*global_mean,*global_std,*body_mean,*body_std);
if (!decoded) return std::unexpected(decoded.error());
motion_data result;
result.frames=frames; result.joints=22;
result.local_rotations_xyzw=std::move(decoded->local_xyzw);
result.root_positions=std::move(decoded->root_positions);
return result;
#else
return std::unexpected("Kimodo was built without GGML support");
#endif
}
} // namespace kimodo

13
src/motion_decode.cpp Normal file
View File

@ -0,0 +1,13 @@
#include "motion_decode.hpp"
#include <array>
#include <cmath>
namespace kimodo::detail { namespace {
constexpr int parent[22]={-1,0,0,0,1,2,3,4,5,6,7,8,9,9,9,12,13,14,16,17,18,19};
struct M{float v[9];};
M mul(const M&a,const M&b){M r{};for(int i=0;i<3;++i)for(int j=0;j<3;++j)for(int k=0;k<3;++k)r.v[i*3+j]+=a.v[i*3+k]*b.v[k*3+j];return r;}
M tr(const M&a){M r{};for(int i=0;i<3;++i)for(int j=0;j<3;++j)r.v[i*3+j]=a.v[j*3+i];return r;}
M six(const float*x){float n=std::sqrt(x[0]*x[0]+x[1]*x[1]+x[2]*x[2]);float a[3]={x[0]/n,x[1]/n,x[2]/n};float z[3]={a[1]*x[5]-a[2]*x[4],a[2]*x[3]-a[0]*x[5],a[0]*x[4]-a[1]*x[3]};n=std::sqrt(z[0]*z[0]+z[1]*z[1]+z[2]*z[2]);for(float&v:z)v/=n;float b[3]={z[1]*a[2]-z[2]*a[1],z[2]*a[0]-z[0]*a[2],z[0]*a[1]-z[1]*a[0]};return M{{a[0],b[0],z[0],a[1],b[1],z[1],a[2],b[2],z[2]}};}
void quat(const M&m,float*q){float w,x,y,z,t=m.v[0]+m.v[4]+m.v[8];if(t>0){float s=2*std::sqrt(t+1);w=.25f*s;x=(m.v[7]-m.v[5])/s;y=(m.v[2]-m.v[6])/s;z=(m.v[3]-m.v[1])/s;}else if(m.v[0]>m.v[4]&&m.v[0]>m.v[8]){float s=2*std::sqrt(1+m.v[0]-m.v[4]-m.v[8]);w=(m.v[7]-m.v[5])/s;x=.25f*s;y=(m.v[1]+m.v[3])/s;z=(m.v[2]+m.v[6])/s;}else if(m.v[4]>m.v[8]){float s=2*std::sqrt(1+m.v[4]-m.v[0]-m.v[8]);w=(m.v[2]-m.v[6])/s;x=(m.v[1]+m.v[3])/s;y=.25f*s;z=(m.v[5]+m.v[7])/s;}else{float s=2*std::sqrt(1+m.v[8]-m.v[0]-m.v[4]);w=(m.v[3]-m.v[1])/s;x=(m.v[2]+m.v[6])/s;y=(m.v[5]+m.v[7])/s;z=.25f*s;}q[0]=x;q[1]=y;q[2]=z;q[3]=w;}
}
std::expected<decoded_motion,std::string> decode_smplx22(std::span<const float>x,size_t T,std::span<const float>gm,std::span<const float>gs,std::span<const float>bm,std::span<const float>bs){if(x.size()!=T*273||gm.size()!=5||gs.size()!=5||bm.size()!=268||bs.size()!=268)return std::unexpected("invalid SMPL-X decode inputs");decoded_motion o;o.local_xyzw.resize(T*22*4);o.root_positions.resize(T*3);for(size_t t=0;t<T;++t){const float*in=x.data()+t*273;std::array<float,273> f{};for(int i=0;i<5;++i)f[i]=in[i]*gs[i]+gm[i];for(int i=0;i<268;++i)f[5+i]=in[5+i]*bs[i]+bm[i];o.root_positions[t*3]=f[0]+f[5];o.root_positions[t*3+1]=f[6];o.root_positions[t*3+2]=f[2]+f[7];M g[22],l[22];for(int j=0;j<22;++j)g[j]=six(f.data()+71+j*6);for(int j=0;j<22;++j)l[j]=parent[j]<0?g[j]:mul(tr(g[parent[j]]),g[j]);for(int j=0;j<22;++j)quat(l[j],o.local_xyzw.data()+(t*22+j)*4);}return o;}
}

9
src/motion_decode.hpp Normal file
View File

@ -0,0 +1,9 @@
#pragma once
#include <expected>
#include <span>
#include <string>
#include <vector>
namespace kimodo::detail {
struct decoded_motion { std::vector<float> local_xyzw, root_positions; };
std::expected<decoded_motion,std::string> decode_smplx22(std::span<const float> normalized, std::size_t frames, std::span<const float> global_mean, std::span<const float> global_std, std::span<const float> body_mean, std::span<const float> body_std);
}

36
src/motion_rep.cpp Normal file
View File

@ -0,0 +1,36 @@
#include "motion_rep.hpp"
#include <cmath>
namespace kimodo::detail {
std::expected<std::vector<float>, std::string> global_root_to_local_root(
std::span<const float> root, std::span<const float> mask,
std::size_t batch, std::size_t frames,
std::span<const float> global_mean, std::span<const float> global_std,
std::span<const float> local_mean, std::span<const float> local_std, float fps) {
if (batch == 0 || frames < 2 || root.size() != batch*frames*5 || mask.size() != batch*frames ||
global_mean.size() != 5 || global_std.size() != 5 || local_mean.size() != 4 || local_std.size() != 4 ||
!std::isfinite(fps) || fps <= 0.f) return std::unexpected("invalid global-root conversion input");
for (float x : global_std) if (!std::isfinite(x) || x == 0.f) return std::unexpected("invalid global root standard deviation");
for (float x : local_std) if (!std::isfinite(x) || x == 0.f) return std::unexpected("invalid local root standard deviation");
std::vector<float> result(batch*frames*4);
for (std::size_t b=0;b<batch;++b) {
std::size_t length=0; for(std::size_t t=0;t<frames;++t) length += mask[b*frames+t] > .5f;
if (length < 2 || length > frames) return std::unexpected("invalid root motion mask length");
std::vector<float> angle(frames), x(frames), y(frames), z(frames);
for (std::size_t t=0;t<frames;++t) {
const auto p=(b*frames+t)*5;
x[t]=root[p]*global_std[0]+global_mean[0]; y[t]=root[p+1]*global_std[1]+global_mean[1]; z[t]=root[p+2]*global_std[2]+global_mean[2];
angle[t]=std::atan2(root[p+4]*global_std[4]+global_mean[4], root[p+3]*global_std[3]+global_mean[3]);
}
for(std::size_t t=0;t<frames;++t) {
const std::size_t next=t+1<length?t+1:length-1, previous=t+1<length?t:length-2;
const float cos_diff=std::cos(angle[next])*std::cos(angle[previous])+std::sin(angle[next])*std::sin(angle[previous]);
const float sin_diff=std::sin(angle[next])*std::cos(angle[previous])-std::cos(angle[next])*std::sin(angle[previous]);
const float raw[]{fps*std::atan2(sin_diff,cos_diff), fps*(x[next]-x[previous]), fps*(z[next]-z[previous]), y[t]};
for(std::size_t d=0;d<4;++d) result[(b*frames+t)*4+d]=(raw[d]-local_mean[d])/local_std[d];
}
}
return result;
}
} // namespace kimodo::detail

21
src/motion_rep.hpp Normal file
View File

@ -0,0 +1,21 @@
#pragma once
#include <cstddef>
#include <expected>
#include <span>
#include <string>
#include <vector>
namespace kimodo::detail {
// Exact SMPL-X RP global-root -> local-root conditioning boundary. Inputs and
// output are row-major [batch, frames, feature], with feature widths 5 and 4.
std::expected<std::vector<float>, std::string> global_root_to_local_root(
std::span<const float> normalized_global_root,
std::span<const float> motion_mask,
std::size_t batch, std::size_t frames,
std::span<const float> global_mean, std::span<const float> global_std,
std::span<const float> local_mean, std::span<const float> local_std,
float fps = 30.f);
} // namespace kimodo::detail

80
src/sample_fixture.cpp Normal file
View File

@ -0,0 +1,80 @@
// Developer utility: replay an upstream capture with the exact F32 embedding
// and initial diffusion noise, then emit GGML's comparable raw/decoded output.
#include "denoiser.hpp"
#include "ggml_weights.hpp"
#include "motion_decode.hpp"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <string>
#include <vector>
namespace {
std::vector<float> read_f32(const std::filesystem::path &path) {
std::ifstream input(path, std::ios::binary | std::ios::ate);
if (!input) throw std::runtime_error("cannot read " + path.string());
const auto bytes = input.tellg();
if (bytes < 0 || bytes % static_cast<std::streamoff>(sizeof(float)) != 0)
throw std::runtime_error("invalid F32 file " + path.string());
std::vector<float> values(static_cast<size_t>(bytes) / sizeof(float));
input.seekg(0);
input.read(reinterpret_cast<char *>(values.data()), bytes);
if (!input) throw std::runtime_error("short F32 file " + path.string());
return values;
}
void write_f32(const std::filesystem::path &path, const std::vector<float> &values) {
std::ofstream output(path, std::ios::binary | std::ios::trunc);
if (!output) throw std::runtime_error("cannot write " + path.string());
output.write(reinterpret_cast<const char *>(values.data()), static_cast<std::streamsize>(values.size() * sizeof(float)));
if (!output) throw std::runtime_error("short write " + path.string());
}
float max_abs(const std::vector<float> &a, const std::vector<float> &b) {
if (a.size() != b.size()) throw std::runtime_error("comparison size mismatch");
float result = 0.f;
for (size_t i = 0; i < a.size(); ++i) result = std::max(result, std::abs(a[i] - b[i]));
return result;
}
} // namespace
int main(int argc, char **argv) try {
if (argc != 6) {
std::fprintf(stderr, "usage: %s MODEL.gguf FIXTURE_DIR FRAMES STEPS OUTPUT_DIR\n", argv[0]);
return 2;
}
const auto frames = static_cast<size_t>(std::stoul(argv[3]));
const auto steps = static_cast<unsigned>(std::stoul(argv[4]));
if (frames == 0 || steps == 0) throw std::runtime_error("frames and steps must be positive");
const std::filesystem::path fixture(argv[2]), output(argv[5]);
const auto embedding = read_f32(fixture / "text_features.f32");
const auto noise = read_f32(fixture / "sampling_initial_noise.f32");
if (embedding.size() != 4096 || noise.size() != frames * 273)
throw std::runtime_error("fixture does not match requested [1,1,4096] embedding and [1,T,273] noise");
auto weights = kimodo::detail::ggml_motion_weights::load(argv[1]);
if (!weights) throw std::runtime_error(weights.error());
auto sampled = kimodo::detail::sample_motion_from_noise(**weights, noise, embedding, frames, steps, 2.f, 2.f);
if (!sampled) throw std::runtime_error(sampled.error());
auto gm = (**weights).f32_values("stats.global_root.mean");
auto gs = (**weights).f32_values("stats.global_root.std");
auto bm = (**weights).f32_values("stats.body.mean");
auto bs = (**weights).f32_values("stats.body.std");
if (!gm || !gs || !bm || !bs) throw std::runtime_error("missing motion normalisation tensors");
auto decoded = kimodo::detail::decode_smplx22(*sampled, frames, *gm, *gs, *bm, *bs);
if (!decoded) throw std::runtime_error(decoded.error());
std::filesystem::create_directories(output);
write_f32(output / "sampling_final_state.f32", *sampled);
write_f32(output / "motion_root_positions.f32", decoded->root_positions);
write_f32(output / "motion_local_rotations_xyzw.f32", decoded->local_xyzw);
const auto upstream = read_f32(fixture / "sampling_final_state.f32");
std::printf("sampling_final_state max_abs=%g\n", max_abs(*sampled, upstream));
return 0;
} catch (const std::exception &error) {
std::fprintf(stderr, "%s\n", error.what());
return 1;
}

83
tests/capi_test.cpp Normal file
View File

@ -0,0 +1,83 @@
#include <kimodo/kimodo_capi.h>
#include <cassert>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <string>
#include <vector>
template <class T> static void put(std::ofstream &out, T value) {
out.write(reinterpret_cast<const char *>(&value), sizeof(value));
}
static void put_string(std::ofstream &out, const char *value) {
const auto n = static_cast<std::uint64_t>(std::strlen(value)); put(out, n); out.write(value, static_cast<std::streamsize>(n));
}
static void put_key_string(std::ofstream &out, const char *key, const char *value) {
put_string(out, key); put(out, std::uint32_t{8}); put_string(out, value);
}
static void put_key_uint(std::ofstream &out, const char *key, std::uint64_t value) {
put_string(out, key); put(out, std::uint32_t{10}); put(out, value);
}
static void motion_gguf(const char *path) {
std::ofstream out(path, std::ios::binary);
put(out, std::uint32_t{0x46554747}); put(out, std::uint32_t{3}); put(out, std::uint64_t{0}); put(out, std::uint64_t{5});
put_key_string(out, "general.architecture", "kimodo-motion");
put_key_uint(out, "kimodo.format_version", 1);
put_key_string(out, "kimodo.skeleton", "smplx22");
put_key_uint(out, "kimodo.text_embedding_width", 4096);
put_key_string(out, "kimodo.model_identity", "fixture-v1");
}
int main(int argc, char **argv) {
assert(kimodo_abi_version() == 1);
char error[64];
auto *model = kimodo_model_load("does-not-exist.gguf", nullptr, nullptr, nullptr, error, sizeof(error));
assert(model == nullptr && std::strlen(error) > 0);
const char *path = "kimodo-test-motion.gguf";
motion_gguf(path);
model = kimodo_model_load(path, nullptr, nullptr, nullptr, error, sizeof(error));
assert(model == nullptr);
assert(std::strlen(error) > 0);
// A tensorless metadata-only fixture must never be treated as a model.
std::remove(path);
assert(kimodo_generate_embedding(nullptr, nullptr, nullptr, error, sizeof(error)) == nullptr);
if (argc == 2 || argc == 3) {
kimodo_runtime_options runtime{};
runtime.size = sizeof(runtime);
auto *loaded = kimodo_model_load(argv[1], argc == 3 ? argv[2] : nullptr, nullptr, &runtime, error, sizeof(error));
assert(loaded != nullptr);
std::vector<float> embedding(4096);
kimodo_embedding input{embedding.data(), static_cast<uint32_t>(embedding.size())};
kimodo_generation_options options{};
options.size = sizeof(options);
options.seed = 42;
options.frames = 2;
options.diffusion_steps = 1;
options.text_cfg_weight = 2.f;
options.constraint_cfg_weight = 2.f;
auto *motion = kimodo_generate_embedding(loaded, &input, &options, error, sizeof(error));
assert(motion != nullptr);
assert(kimodo_motion_frames(motion) == 2);
assert(kimodo_motion_joints(motion) == 22);
const float *root = kimodo_motion_root_positions(motion);
const float *rotations = kimodo_motion_local_rotations_xyzw(motion);
assert(root != nullptr && rotations != nullptr);
for (int i = 0; i < 6; ++i) assert(std::isfinite(root[i]));
for (int i = 0; i < 2 * 22 * 4; ++i) assert(std::isfinite(rotations[i]));
kimodo_motion_free(motion);
if (argc == 3) {
motion = kimodo_generate(loaded,
"A person runs forward and then leaps over an obstacle in front of them.",
&options, error, sizeof(error));
assert(motion != nullptr);
assert(kimodo_motion_frames(motion) == 2);
assert(kimodo_motion_joints(motion) == 22);
kimodo_motion_free(motion);
}
kimodo_model_free(loaded);
}
}

78
tests/converter_test.py Normal file
View File

@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""Focused hostile-input tests for the safe motion GGUF converter."""
from __future__ import annotations
import importlib.util
import json
import struct
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location(
"convert_motion_to_gguf", ROOT / "scripts" / "convert_motion_to_gguf.py"
)
assert SPEC and SPEC.loader
CONVERTER = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = CONVERTER
SPEC.loader.exec_module(CONVERTER)
class ConverterInputTest(unittest.TestCase):
def setUp(self) -> None:
self.temp = tempfile.TemporaryDirectory()
self.path = Path(self.temp.name) / "input"
def tearDown(self) -> None:
self.temp.cleanup()
def write_safe(self, header: str | dict, payload: bytes = b"\0" * 8) -> Path:
encoded = header.encode() if isinstance(header, str) else json.dumps(header).encode()
output = self.path.with_suffix(".safetensors")
output.write_bytes(struct.pack("<Q", len(encoded)) + encoded + payload)
return output
def test_valid_f32_safetensors(self) -> None:
path = self.write_safe({"weight": {"dtype": "F32", "shape": [2], "data_offsets": [0, 8]}})
tensors = CONVERTER.read_safetensors(path)
self.assertEqual([(item.name, item.shape, item.size) for item in tensors], [("weight", (2,), 8)])
def test_safetensors_duplicate_json_key_is_rejected(self) -> None:
path = self.write_safe('{"weight":{"dtype":"F32","shape":[1],"data_offsets":[0,4]},"weight":{"dtype":"F32","shape":[1],"data_offsets":[4,8]}}')
with self.assertRaises(ValueError):
CONVERTER.read_safetensors(path)
def test_safetensors_out_of_range_and_overlap_are_rejected(self) -> None:
out_of_range = self.write_safe({"weight": {"dtype": "F32", "shape": [2], "data_offsets": [0, 8]}}, b"\0" * 4)
with self.assertRaises(ValueError):
CONVERTER.read_safetensors(out_of_range)
overlap = self.write_safe({
"left": {"dtype": "F32", "shape": [1], "data_offsets": [0, 4]},
"right": {"dtype": "F32", "shape": [1], "data_offsets": [2, 6]},
})
with self.assertRaises(ValueError):
CONVERTER.read_safetensors(overlap)
def write_npy(self, descriptor: str, payload: bytes) -> Path:
output = self.path.with_suffix(".npy")
header = descriptor.encode("latin1")
output.write_bytes(b"\x93NUMPY" + bytes((1, 0)) + struct.pack("<H", len(header)) + header + payload)
return output
def test_npy_payload_and_layout_are_checked(self) -> None:
valid = self.write_npy("{'descr': '<f4', 'fortran_order': False, 'shape': (2,), }", b"\0" * 8)
self.assertEqual(CONVERTER.read_npy(valid, "stats.test").size, 8)
truncated = self.write_npy("{'descr': '<f4', 'fortran_order': False, 'shape': (2,), }", b"\0" * 4)
with self.assertRaises(ValueError):
CONVERTER.read_npy(truncated, "stats.test")
fortran = self.write_npy("{'descr': '<f4', 'fortran_order': True, 'shape': (1,), }", b"\0" * 4)
with self.assertRaises(ValueError):
CONVERTER.read_npy(fortran, "stats.test")
if __name__ == "__main__":
unittest.main()

11
tests/decode_test.cpp Normal file
View File

@ -0,0 +1,11 @@
#include "ggml_weights.hpp"
#include "motion_decode.hpp"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <fstream>
#include <stdexcept>
#include <string>
#include <vector>
static std::vector<float> r(const std::string&p){std::ifstream f(p,std::ios::binary|std::ios::ate);std::vector<float>x(size_t(f.tellg())/4);f.seekg(0);f.read(reinterpret_cast<char*>(x.data()),std::streamsize(x.size()*4));return x;}
int main(int c,char**v)try{if(c!=3)return 2;auto w=kimodo::detail::ggml_motion_weights::load(v[1]);if(!w)throw std::runtime_error(w.error());auto x=r(std::string(v[2])+"/sampling_final_state.f32");auto gm=(**w).f32_values("stats.global_root.mean");auto gs=(**w).f32_values("stats.global_root.std");auto bm=(**w).f32_values("stats.body.mean");auto bs=(**w).f32_values("stats.body.std");auto d=kimodo::detail::decode_smplx22(x,8,*gm,*gs,*bm,*bs);if(!d)throw std::runtime_error(d.error());auto root=r(std::string(v[2])+"/motion_root_positions.f32"),rot=r(std::string(v[2])+"/motion_local_rot_mats.f32");float mr=0,mm=0;for(size_t i=0;i<root.size();++i)mr=std::max(mr,std::abs(root[i]-d->root_positions[i]));for(size_t i=0;i<8*22;++i){const float*q=d->local_xyzw.data()+i*4;float X=q[0],Y=q[1],Z=q[2],W=q[3];float a[]={1-2*(Y*Y+Z*Z),2*(X*Y-Z*W),2*(X*Z+Y*W),2*(X*Y+Z*W),1-2*(X*X+Z*Z),2*(Y*Z-X*W),2*(X*Z-Y*W),2*(Y*Z+X*W),1-2*(X*X+Y*Y)};for(int j=0;j<9;++j)mm=std::max(mm,std::abs(a[j]-rot[i*9+j]));}std::printf("decode root max_abs=%g rot_matrix=%g\n",mr,mm);return mr<3.e-4f&&mm<3.e-4f?0:1;}catch(const std::exception&e){std::fprintf(stderr,"%s\n",e.what());return 1;}

View File

@ -0,0 +1,12 @@
#include "denoiser.hpp"
#include "ggml_weights.hpp"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <cstdio>
#include <fstream>
#include <stdexcept>
#include <string>
#include <vector>
static std::vector<float> read(const std::string&p){std::ifstream f(p,std::ios::binary|std::ios::ate);if(!f||f.tellg()<0)throw std::runtime_error("bad fixture");std::vector<float>x(size_t(f.tellg())/4);f.seekg(0);f.read(reinterpret_cast<char*>(x.data()),std::streamsize(x.size()*4));return x;}
int main(int argc,char**argv)try{if(argc!=4)return 2;const std::string dir=std::string(argv[2])+"/",stage=argv[3];auto w=kimodo::detail::ggml_motion_weights::load(argv[1]);if(!w)throw std::runtime_error(w.error());std::expected<std::vector<float>,std::string> out=std::unexpected("unset");std::vector<float> ref;if(stage=="full"){out=kimodo::detail::run_two_stage_denoiser(**w,read(dir+"root_input_0.f32"),read(dir+"root_input_2.f32"),read(dir+"root_input_4.f32"),read(dir+"root_input_5.f32"),read(dir+"root_input_1.f32"),3,8);auto r=read(dir+"root_output.f32"),b=read(dir+"body_output.f32");ref.resize(3*8*273);for(int i=0;i<24;++i){std::memcpy(ref.data()+i*273,r.data()+i*5,5*sizeof(float));std::memcpy(ref.data()+i*273+5,b.data()+i*268,268*sizeof(float));}}else if(stage=="cfg"){auto t=read(dir+"root_input_4.f32");out=kimodo::detail::run_separated_cfg_denoiser(**w,read(dir+"sampling_input_0.f32"),read(dir+"text_features.f32"),t[0],2.f,2.f,8);ref=read(dir+"sampling_output_0.f32");}else if(stage=="sample"){out=kimodo::detail::sample_motion_from_noise(**w,read(dir+"sampling_initial_noise.f32"),read(dir+"text_features.f32"),8,1,2.f,2.f);ref=read(dir+"sampling_final_state.f32");}else{const size_t dim=stage=="root"?546:545;out=kimodo::detail::run_motion_transformer(**w,stage+"_model.",read(dir+stage+"_input_0.f32"),dim,read(dir+stage+"_input_2.f32"),read(dir+stage+"_input_4.f32"),read(dir+stage+"_input_5.f32"),3,8);ref=read(dir+stage+"_output.f32");}if(!out)throw std::runtime_error(out.error());float m=0;for(size_t i=0;i<ref.size();++i)m=std::max(m,std::abs((*out)[i]-ref[i]));std::printf("runtime %s max_abs=%g\n",stage.c_str(),m);return m<2.e-3f?0:1;}catch(const std::exception&e){std::fprintf(stderr,"%s\n",e.what());return 1;}

14
tests/diffusion_test.cpp Normal file
View File

@ -0,0 +1,14 @@
#include "diffusion.hpp"
#include <cassert>
#include <cmath>
int main() {
auto schedule = kimodo::detail::make_cosine_schedule(1000, 100);
assert(schedule && schedule->use_timesteps.front() == 0 && schedule->use_timesteps.back() == 999);
float text[]{3.f, 5.f}, constraint[]{2.f, 9.f}, uncond[]{1.f, 1.f}, result[2]{};
assert(kimodo::detail::separated_cfg(text, constraint, uncond, 2.f, .5f, result, 2));
assert(result[0] == 5.5f && result[1] == 13.f);
float x[]{.4f}, pred[]{.1f}, next[1]{};
assert(kimodo::detail::ddim_step(*schedule, 1, x, pred, next, 1));
assert(std::isfinite(next[0]));
}

View File

@ -0,0 +1,36 @@
#include "diffusion.hpp"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <fstream>
#include <stdexcept>
#include <string>
#include <vector>
namespace {
std::vector<float> read_f32(const std::string &path) {
std::ifstream in(path, std::ios::binary | std::ios::ate);
if (!in || in.tellg() < 0 || static_cast<std::size_t>(in.tellg()) % sizeof(float)) throw std::runtime_error("invalid fixture: " + path);
std::vector<float> values(static_cast<std::size_t>(in.tellg())/sizeof(float));
in.seekg(0); in.read(reinterpret_cast<char *>(values.data()), static_cast<std::streamsize>(values.size()*sizeof(float)));
if (!in) throw std::runtime_error("short fixture: " + path); return values;
}
}
int main(int argc, char **argv) try {
if (argc != 2) { std::fprintf(stderr, "usage: kimodo-fixture-sampler-parity FIXTURE_DIR\n"); return 2; }
const std::string dir = std::string(argv[1]) + "/";
const auto root=read_f32(dir+"root_output.f32"), body=read_f32(dir+"body_output.f32"), expected=read_f32(dir+"sampling_output_0.f32");
if (root.size() % 15 || body.size() % (3*268)) throw std::runtime_error("unexpected CFG fixture dimensions");
const size_t frames=root.size()/(3*5); std::vector<float> predicted(expected.size());
if (expected.size() != frames*273 || body.size()/(3*268) != frames) throw std::runtime_error("unexpected fixture dimensions");
for(size_t b=0;b<1;++b) for(size_t t=0;t<frames;++t) {
const size_t out=(b*frames+t)*273;
for(size_t d=0;d<5;++d) predicted[out+d]=root[(2*frames+t)*5+d] + 2.f*(root[(0*frames+t)*5+d]-root[(2*frames+t)*5+d]) + 2.f*(root[(1*frames+t)*5+d]-root[(2*frames+t)*5+d]);
for(size_t d=0;d<268;++d) predicted[out+5+d]=body[(2*frames+t)*268+d] + 2.f*(body[(0*frames+t)*268+d]-body[(2*frames+t)*268+d]) + 2.f*(body[(1*frames+t)*268+d]-body[(2*frames+t)*268+d]);
}
float max_abs=0.f; for(size_t i=0;i<predicted.size();++i) max_abs=std::max(max_abs,std::abs(predicted[i]-expected[i]));
std::printf("one-step CFG/DDIM fixture max_abs=%g\n",max_abs);
return max_abs < 2.e-4f ? 0 : 1;
} catch(const std::exception &e) { std::fprintf(stderr,"fixture sampler parity error: %s\n",e.what()); return 1; }

4
tests/generate_smoke.cpp Normal file
View File

@ -0,0 +1,4 @@
#include <kimodo/kimodo.hpp>
#include <array>
#include <cstdio>
int main(int argc,char**argv){if(argc!=2)return 2;auto m=kimodo::model::load(argv[1]);if(!m){std::fprintf(stderr,"%s\n",m.error().c_str());return 1;}std::array<float,kimodo::embedding_width> e{};auto r=(*m)->generate_embedding(e,2,1,42,2.f,2.f);if(!r){std::fprintf(stderr,"%s\n",r.error().c_str());return 1;}if(r->frames!=2||r->joints!=22||r->root_positions.size()!=6||r->local_rotations_xyzw.size()!=176)return 1;return 0;}

View File

@ -0,0 +1,13 @@
#include "ggml_weights.hpp"
#include <cstdio>
int main(int argc, char **argv) {
if (argc != 2) { std::fprintf(stderr, "usage: kimodo-ggml-weights-test MODEL.gguf\n"); return 2; }
auto weights = kimodo::detail::ggml_motion_weights::load(argv[1]);
if (!weights) { std::fprintf(stderr, "%s\n", weights.error().c_str()); return 1; }
if (!(*weights)->tensor("root_model.input_linear.weight") || !(*weights)->tensor("body_model.output_linear.bias")) {
std::fprintf(stderr, "expected motion tensors missing after GGML load\n"); return 1;
}
return 0;
}

View File

@ -0,0 +1,25 @@
#include <ggml.h>
#include <ggml-alloc.h>
#include <ggml-backend.h>
#include <ggml-cpu.h>
#include <ggml-vulkan.h>
#include <gguf.h>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <memory>
#include <stdexcept>
#include <string_view>
#include <vector>
namespace {
template<class T> std::vector<T> read(const std::filesystem::path &path) { std::ifstream in(path,std::ios::binary|std::ios::ate);if(!in)throw std::runtime_error("cannot read");const auto n=in.tellg();if(n<0||n%static_cast<std::streamoff>(sizeof(T)))throw std::runtime_error("bad fixture");std::vector<T>v(static_cast<size_t>(n)/sizeof(T));in.seekg(0);in.read(reinterpret_cast<char*>(v.data()),n);return v; }
float bf16(uint16_t x) { uint32_t b=uint32_t(x)<<16;float f;std::memcpy(&f,&b,4);return f; }
struct loaded { ggml_context*c=nullptr;gguf_context*f=nullptr;ggml_backend_t b=nullptr;ggml_backend_buffer_t w=nullptr;~loaded(){if(w)ggml_backend_buffer_free(w);if(f)gguf_free(f);if(c)ggml_free(c);if(b)ggml_backend_free(b);} };
std::unique_ptr<loaded> load(const char *path,bool vk){auto r=std::make_unique<loaded>();gguf_init_params p{true,&r->c};r->f=gguf_init_from_file(path,p);if(!r->f||!r->c)throw std::runtime_error("load GGUF");if(vk&&ggml_backend_vk_get_device_count())r->b=ggml_backend_vk_init(0);if(!r->b){r->b=ggml_backend_cpu_init();ggml_backend_cpu_set_n_threads(r->b,24);}r->w=ggml_backend_alloc_ctx_tensors(r->c,r->b);auto*t=ggml_get_tensor(r->c,"token_embedding.weight");if(!r->w||!t||t->type!=GGML_TYPE_BF16)throw std::runtime_error("missing BF16 embedding");std::ifstream in(path,std::ios::binary);const auto base=gguf_get_data_offset(r->f),off=gguf_get_tensor_offset(r->f,0);std::vector<char>v(ggml_nbytes(t));in.seekg(static_cast<std::streamoff>(base+off));in.read(v.data(),static_cast<std::streamsize>(v.size()));if(!in)throw std::runtime_error("short GGUF");ggml_backend_tensor_set(t,v.data(),0,v.size());return r;}
}
int main(int argc,char**argv)try{if(argc!=4){std::fprintf(stderr,"usage: %s EMBEDDING.gguf FIXTURE cpu|vulkan\n",argv[0]);return 2;}const bool vk=std::string_view(argv[3])=="vulkan";if(!vk&&std::string_view(argv[3])!="cpu")throw std::runtime_error("backend");auto model=load(argv[1],vk);const auto fixture=std::filesystem::path(argv[2]);const auto ids64=read<int64_t>(fixture/"input_ids.i64");std::vector<int32_t>ids(ids64.begin(),ids64.end());auto*ctx=ggml_init({2ULL*1024*1024,nullptr,true});auto cleanup=std::unique_ptr<ggml_context,decltype(&ggml_free)>(ctx,ggml_free);auto*indices=ggml_new_tensor_1d(ctx,GGML_TYPE_I32,ids.size());ggml_set_input(indices);auto*output=ggml_get_rows(ctx,ggml_get_tensor(model->c,"token_embedding.weight"),indices);auto*graph=ggml_new_graph(ctx);ggml_build_forward_expand(graph,output);auto*buffer=ggml_backend_alloc_ctx_tensors(ctx,model->b);auto release=std::unique_ptr<ggml_backend_buffer,decltype(&ggml_backend_buffer_free)>(buffer,ggml_backend_buffer_free);if(!buffer)throw std::runtime_error("allocation");ggml_backend_tensor_set(indices,ids.data(),0,ids.size()*sizeof(int32_t));if(ggml_backend_graph_compute(model->b,graph)!=GGML_STATUS_SUCCESS)throw std::runtime_error("compute");const auto expected=read<float>(fixture/"token_embeddings.f32");std::vector<float>actual(expected.size());if(output->type==GGML_TYPE_F32)ggml_backend_tensor_get(output,actual.data(),0,actual.size()*sizeof(float));else if(output->type==GGML_TYPE_BF16){std::vector<uint16_t>raw(actual.size());ggml_backend_tensor_get(output,raw.data(),0,raw.size()*sizeof(uint16_t));for(size_t i=0;i<raw.size();++i)actual[i]=bf16(raw[i]);}else throw std::runtime_error("unexpected embedding output type");float max=0;double e=0,r=0;for(size_t i=0;i<actual.size();++i){float d=actual[i]-expected[i];max=std::max(max,std::abs(d));e+=double(d)*d;r+=double(expected[i])*expected[i];}std::printf("embedding %s max_abs=%g rel_l2=%g type=%d\n",argv[3],max,std::sqrt(e/r),output->type);return max==0?0:1;}catch(const std::exception&e){std::fprintf(stderr,"%s\n",e.what());return 1;}

View File

@ -0,0 +1,60 @@
// GGML graph structure follows the operation conventions in llama.cpp
// src/llama-graph.cpp at 78ec4c378031811671d1c76a067acbee4f4c56ce.
// This is an independent implementation; no llama.cpp source is copied.
#include <ggml.h>
#include <ggml-alloc.h>
#include <ggml-backend.h>
#include <ggml-cpu.h>
#include <ggml-vulkan.h>
#include <gguf.h>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <memory>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
namespace {
struct loaded {
ggml_context *ctx = nullptr; gguf_context *file = nullptr; ggml_backend_t backend = nullptr; ggml_backend_buffer_t buffer = nullptr;
~loaded() { if (buffer) ggml_backend_buffer_free(buffer); if (file) gguf_free(file); if (ctx) ggml_free(ctx); if (backend) ggml_backend_free(backend); }
};
template<class T> std::vector<T> read(const std::filesystem::path &path) {
std::ifstream in(path, std::ios::binary | std::ios::ate); if (!in) throw std::runtime_error("cannot read " + path.string());
const auto bytes=in.tellg(); if (bytes < 0 || bytes % static_cast<std::streamoff>(sizeof(T))) throw std::runtime_error("invalid fixture");
std::vector<T> out(static_cast<size_t>(bytes)/sizeof(T)); in.seekg(0); in.read(reinterpret_cast<char *>(out.data()),bytes); if(!in) throw std::runtime_error("short fixture"); return out;
}
std::unique_ptr<loaded> load(const char *path, bool vulkan) {
auto out=std::make_unique<loaded>(); gguf_init_params p{true,&out->ctx}; out->file=gguf_init_from_file(path,p);
if(!out->file||!out->ctx) throw std::runtime_error("cannot load GGUF");
if(vulkan && ggml_backend_vk_get_device_count()) out->backend=ggml_backend_vk_init(0);
if(!out->backend) { out->backend=ggml_backend_cpu_init(); ggml_backend_cpu_set_n_threads(out->backend,24); }
out->buffer=ggml_backend_alloc_ctx_tensors(out->ctx,out->backend); if(!out->buffer) throw std::runtime_error("cannot allocate weights");
std::ifstream in(path,std::ios::binary); const auto start=gguf_get_data_offset(out->file); auto *weight=ggml_get_tensor(out->ctx,"final_norm.weight");
if(!weight||weight->type!=GGML_TYPE_BF16) throw std::runtime_error("missing BF16 final norm");
std::vector<char> data(ggml_nbytes(weight)); in.seekg(static_cast<std::streamoff>(start+gguf_get_tensor_offset(out->file,0))); in.read(data.data(),static_cast<std::streamsize>(data.size()));
if(!in) throw std::runtime_error("short GGUF"); ggml_backend_tensor_set(weight,data.data(),0,data.size()); return out;
}
void report(std::string_view name,const std::vector<float>& actual,const std::vector<float>& expected) {
if(actual.size()!=expected.size()) throw std::runtime_error("size mismatch"); float maximum=0.f; double err=0,ref=0;
for(size_t i=0;i<actual.size();++i){const float d=actual[i]-expected[i];maximum=std::max(maximum,std::abs(d));err+=double(d)*d;ref+=double(expected[i])*expected[i];}
std::printf("%.*s max_abs=%g rel_l2=%g\n",int(name.size()),name.data(),maximum,std::sqrt(err/ref));
}
}
int main(int argc,char **argv) try {
if(argc!=5){std::fprintf(stderr,"usage: %s FINAL_NORM.gguf HIDDEN.f32 FIXTURE_DIR cpu|vulkan\n",argv[0]);return 2;}
const bool vulkan=std::string_view(argv[4])=="vulkan"; if(!vulkan&&std::string_view(argv[4])!="cpu") throw std::runtime_error("backend must be cpu or vulkan");
const auto input=read<float>(argv[2]); if(input.size()!=16*4096) throw std::runtime_error("expected [1,16,4096] hidden state");
auto model=load(argv[1],vulkan); auto *weight=ggml_get_tensor(model->ctx,"final_norm.weight");
auto *ctx=ggml_init({8ULL*1024*1024,nullptr,true}); if(!ctx) throw std::runtime_error("cannot allocate graph"); auto guard=std::unique_ptr<ggml_context,decltype(&ggml_free)>(ctx,ggml_free);
auto *x=ggml_new_tensor_2d(ctx,GGML_TYPE_F32,4096,16);ggml_set_input(x);auto *normal=ggml_rms_norm(ctx,x,1e-5f);auto *out=ggml_mul(ctx,normal,ggml_repeat(ctx,ggml_cast(ctx,weight,GGML_TYPE_F32),normal));
auto *graph=ggml_new_graph(ctx);ggml_build_forward_expand(graph,out);auto buffer=ggml_backend_alloc_ctx_tensors(ctx,model->backend);if(!buffer)throw std::runtime_error("cannot allocate graph");auto release=std::unique_ptr<ggml_backend_buffer,decltype(&ggml_backend_buffer_free)>(buffer,ggml_backend_buffer_free);
ggml_backend_tensor_set(x,input.data(),0,input.size()*sizeof(float));if(ggml_backend_graph_compute(model->backend,graph)!=GGML_STATUS_SUCCESS)throw std::runtime_error("GGML graph failed");std::vector<float> actual(input.size());ggml_backend_tensor_get(out,actual.data(),0,actual.size()*sizeof(float));
const auto fixture=std::filesystem::path(argv[3]);report("final_hidden",actual,read<float>(fixture/"final_hidden_state.f32"));
const auto mask=read<int64_t>(fixture/"embed_mask.i64");if(mask.size()!=16)throw std::runtime_error("expected 16 embed-mask values");std::vector<float> pooled(4096);int count=0;for(int t=0;t<16;++t)if(mask[t]){++count;for(int d=0;d<4096;++d)pooled[d]+=actual[size_t(t)*4096+d];}for(float &v:pooled)v/=count;report("pooled_embedding",pooled,read<float>(fixture/"pooled_embedding.f32"));return 0;
}catch(const std::exception&e){std::fprintf(stderr,"%s\n",e.what());return 1;}

260
tests/llm_layer_parity.cpp Normal file
View File

@ -0,0 +1,260 @@
// The operation ordering and attention tensor views follow llama.cpp
// src/llama-graph.cpp at 78ec4c378031811671d1c76a067acbee4f4c56ce.
// This is an independent implementation; no llama.cpp source is copied.
#include <ggml.h>
#include <ggml-alloc.h>
#include <ggml-backend.h>
#include <ggml-cpu.h>
#include <ggml-vulkan.h>
#include <gguf.h>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <expected>
#include <filesystem>
#include <fstream>
#include <memory>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
namespace {
struct weights {
ggml_context *context = nullptr;
gguf_context *file = nullptr;
ggml_backend_t backend = nullptr;
ggml_backend_buffer_t buffer = nullptr;
~weights() { if (buffer) ggml_backend_buffer_free(buffer); if (file) gguf_free(file); if (context) ggml_free(context); if (backend) ggml_backend_free(backend); }
ggml_tensor *get(std::string_view name) const { return ggml_get_tensor(context, std::string(name).c_str()); }
};
std::vector<float> read_f32(const std::filesystem::path &path) {
std::ifstream input(path, std::ios::binary | std::ios::ate);
if (!input) throw std::runtime_error("cannot read " + path.string());
const auto bytes = input.tellg();
if (bytes < 0 || bytes % static_cast<std::streamoff>(sizeof(float))) throw std::runtime_error("invalid F32 fixture " + path.string());
std::vector<float> values(static_cast<size_t>(bytes) / sizeof(float));
input.seekg(0); input.read(reinterpret_cast<char *>(values.data()), bytes);
if (!input) throw std::runtime_error("short F32 fixture " + path.string());
return values;
}
void write_f32(const std::filesystem::path &path, const std::vector<float> &values) {
std::ofstream output(path, std::ios::binary | std::ios::trunc);
if (!output) throw std::runtime_error("cannot write " + path.string());
output.write(reinterpret_cast<const char *>(values.data()), static_cast<std::streamsize>(values.size() * sizeof(float)));
if (!output) throw std::runtime_error("short write " + path.string());
}
std::string layer_fixture_name(int layer) {
char name[32];
std::snprintf(name, sizeof(name), "layer_%02d_output.f32", layer);
return name;
}
std::unique_ptr<weights> load(const char *path, bool use_vulkan) {
auto value = std::make_unique<weights>();
gguf_init_params params{true, &value->context};
value->file = gguf_init_from_file(path, params);
if (!value->file || !value->context) throw std::runtime_error("cannot load layer GGUF");
if (use_vulkan && ggml_backend_vk_get_device_count() > 0) value->backend = ggml_backend_vk_init(0);
if (!value->backend) { value->backend = ggml_backend_cpu_init(); ggml_backend_cpu_set_n_threads(value->backend, 24); }
value->buffer = ggml_backend_alloc_ctx_tensors(value->context, value->backend);
if (!value->buffer) throw std::runtime_error("cannot allocate layer weights");
std::ifstream input(path, std::ios::binary);
if (!input) throw std::runtime_error("cannot reopen layer GGUF");
const auto start = gguf_get_data_offset(value->file);
std::vector<char> scratch(8 * 1024 * 1024);
for (int64_t index = 0; index < gguf_get_n_tensors(value->file); ++index) {
auto *tensor = ggml_get_tensor(value->context, gguf_get_tensor_name(value->file, index));
if (!tensor || (tensor->type != GGML_TYPE_F32 && tensor->type != GGML_TYPE_BF16)) throw std::runtime_error("layer GGUF must contain F32 or BF16 tensors");
const size_t bytes = ggml_nbytes(tensor), offset = gguf_get_tensor_offset(value->file, index);
input.seekg(static_cast<std::streamoff>(start + offset));
for (size_t done = 0; done < bytes;) {
const size_t chunk = std::min(scratch.size(), bytes - done);
input.read(scratch.data(), static_cast<std::streamsize>(chunk));
if (!input) throw std::runtime_error("short GGUF tensor payload");
ggml_backend_tensor_set(tensor, scratch.data(), done, chunk); done += chunk;
}
}
return value;
}
ggml_tensor *norm(ggml_context *ctx, ggml_tensor *x, ggml_tensor *weight) {
if (!weight || weight->type != GGML_TYPE_BF16) throw std::runtime_error("expected BF16 RMSNorm weight");
auto *normalized = ggml_rms_norm(ctx, x->type == GGML_TYPE_F32 ? x : ggml_cast(ctx, x, GGML_TYPE_F32), 1e-5f);
if (x->type == GGML_TYPE_BF16) {
// Transformers LlamaRMSNorm normalizes in F32, then casts to its
// input dtype before multiplying its BF16 scale. GGML Vulkan has no
// BF16 elementwise MUL shader, so perform the multiply in F32 and
// explicitly round its result back to BF16; this is the same tensor
// precision boundary as the upstream operation.
normalized = ggml_cast(ctx, normalized, GGML_TYPE_BF16);
auto *scale = ggml_repeat(ctx, weight, normalized);
auto *product = ggml_mul(ctx, ggml_cast(ctx, normalized, GGML_TYPE_F32), ggml_cast(ctx, scale, GGML_TYPE_F32));
return ggml_cast(ctx, product, GGML_TYPE_BF16);
}
return ggml_mul(ctx, normalized, ggml_repeat(ctx, ggml_cast(ctx, weight, GGML_TYPE_F32), normalized));
}
float bf16_to_f32(uint16_t value) {
const uint32_t bits = static_cast<uint32_t>(value) << 16;
float result;
std::memcpy(&result, &bits, sizeof(result));
return result;
}
std::vector<float> read_tensor(ggml_tensor *tensor) {
if (tensor->type == GGML_TYPE_F32) {
std::vector<float> values(ggml_nelements(tensor));
ggml_backend_tensor_get(tensor, values.data(), 0, values.size() * sizeof(float));
return values;
}
if (tensor->type == GGML_TYPE_BF16) {
std::vector<uint16_t> raw(ggml_nelements(tensor));
ggml_backend_tensor_get(tensor, raw.data(), 0, raw.size() * sizeof(uint16_t));
std::vector<float> values(raw.size());
std::transform(raw.begin(), raw.end(), values.begin(), bf16_to_f32);
return values;
}
throw std::runtime_error("unexpected snapshot tensor type");
}
// Grouped-query KV expansion: repeat each KV head consecutively. The tensor
// ordering is independently expressed, following the MHA layout conventions
// documented in llama.cpp's build_attn_mha (commit noted in this file header).
ggml_tensor *repeat_kv(ggml_context *ctx, ggml_tensor *value, int64_t head_dim, int64_t kv_heads, int64_t groups, int64_t seq) {
auto *grouped = ggml_reshape_4d(ctx, value, head_dim, kv_heads, 1, seq);
auto *shape = ggml_new_tensor_4d(ctx, value->type, head_dim, kv_heads, groups, seq);
auto *repeated = ggml_repeat(ctx, grouped, shape);
repeated = ggml_cont(ctx, ggml_permute(ctx, repeated, 0, 2, 1, 3));
return ggml_reshape_3d(ctx, repeated, head_dim, kv_heads * groups, seq);
}
struct layer_result {
std::vector<float> output;
std::vector<std::pair<std::string, std::vector<float>>> snapshots;
};
layer_result run(const weights &w, const std::vector<float> &input, bool use_vulkan) {
constexpr int64_t dim = 4096, seq = 16, heads = 32, kv_heads = 8, head_dim = 128, ff = 14336;
if (input.size() != static_cast<size_t>(dim * seq)) throw std::runtime_error("unexpected layer input size");
ggml_init_params params{96ULL * 1024 * 1024, nullptr, true};
ggml_context *ctx = ggml_init(params);
if (!ctx) throw std::runtime_error("cannot allocate GGML graph context");
auto cleanup = std::unique_ptr<ggml_context, decltype(&ggml_free)>(ctx, ggml_free);
auto *x = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, dim, seq);
auto *pos = ggml_new_tensor_1d(ctx, GGML_TYPE_I32, seq);
ggml_set_input(x); ggml_set_input(pos);
auto base_linear = [&](const char *name, ggml_tensor *value) {
const std::string stem(name);
auto *base = w.get(stem + "_base.weight");
auto *a = w.get(stem + "_lora_a.weight");
auto *b = w.get(stem + "_lora_b.weight");
if (!base || !a || !b || base->type != GGML_TYPE_BF16 || a->type != GGML_TYPE_F32 || b->type != GGML_TYPE_F32) {
throw std::runtime_error("invalid linear tensors for " + stem);
}
auto *base_input = value->type == GGML_TYPE_BF16 ? value : ggml_cast(ctx, value, GGML_TYPE_BF16);
return ggml_mul_mat(ctx, base, base_input);
};
auto linear = [&](const char *name, ggml_tensor *value) {
const std::string stem(name);
auto *a = w.get(stem + "_lora_a.weight");
auto *b = w.get(stem + "_lora_b.weight");
auto *base_result = base_linear(name, value);
if (!a || !b) throw std::runtime_error("missing LoRA tensors for " + stem);
auto *lora_input = value->type == GGML_TYPE_F32 ? value : ggml_cast(ctx, value, GGML_TYPE_F32);
auto *lora_result = ggml_mul_mat(ctx, b, ggml_mul_mat(ctx, a, lora_input));
return ggml_add(ctx, ggml_cast(ctx, base_result, GGML_TYPE_F32), ggml_scale(ctx, lora_result, 2.f));
};
auto *residual = ggml_cast(ctx, x, GGML_TYPE_BF16);
auto *attn_norm = norm(ctx, residual, w.get("attn_norm.weight"));
auto *h = attn_norm;
auto *q_linear = linear("attn_q_proj", h);
auto *k_linear = linear("attn_k_proj", h);
auto *v_linear = linear("attn_v_proj", h);
auto *q = ggml_reshape_3d(ctx, q_linear, head_dim, heads, seq);
auto *k = ggml_reshape_3d(ctx, k_linear, head_dim, kv_heads, seq);
auto *v = ggml_reshape_3d(ctx, v_linear, head_dim, kv_heads, seq);
// Hugging Face Llama's rotate_half uses the NeoX half-split layout.
q = ggml_rope_ext(ctx, q, pos, nullptr, head_dim, GGML_ROPE_TYPE_NEOX, 8192, 500000.f, 1.f, 0.f, 1.f, 0.f, 0.f);
k = ggml_rope_ext(ctx, k, pos, nullptr, head_dim, GGML_ROPE_TYPE_NEOX, 8192, 500000.f, 1.f, 0.f, 1.f, 0.f, 0.f);
k = repeat_kv(ctx, k, head_dim, kv_heads, heads / kv_heads, seq);
v = repeat_kv(ctx, v, head_dim, kv_heads, heads / kv_heads, seq);
q = ggml_permute(ctx, q, 0, 2, 1, 3);
k = ggml_permute(ctx, k, 0, 2, 1, 3);
v = ggml_permute(ctx, v, 0, 2, 1, 3);
auto *scores = ggml_scale(ctx, ggml_mul_mat(ctx, k, q), 1.f / std::sqrt(static_cast<float>(head_dim)));
auto *probabilities = ggml_soft_max(ctx, scores);
v = ggml_cont(ctx, ggml_transpose(ctx, v));
auto *attended = ggml_cont(ctx, ggml_permute(ctx, ggml_mul_mat(ctx, v, probabilities), 0, 2, 1, 3));
auto *o_linear = linear("attn_o_proj", ggml_reshape_2d(ctx, attended, dim, seq));
h = ggml_add(ctx, ggml_cast(ctx, residual, GGML_TYPE_F32), o_linear);
residual = h;
auto *ffn_norm = norm(ctx, h, w.get("ffn_norm.weight"));
h = ffn_norm;
auto *gate_linear = linear("ffn_gate_proj", h);
auto *gate = ggml_silu(ctx, gate_linear);
auto *up = linear("ffn_up_proj", h);
auto *down = linear("ffn_down_proj", ggml_mul(ctx, gate, up));
h = ggml_add(ctx, residual, down);
const std::pair<const char *, ggml_tensor *> snapshot_tensors[] = {
{"debug_input_norm", attn_norm}, {"debug_q", q_linear}, {"debug_k", k_linear},
{"debug_v", v_linear}, {"debug_o", o_linear}, {"debug_post_norm", ffn_norm},
{"debug_gate", gate_linear}, {"debug_up", up}, {"debug_down", down},
};
// Keep diagnostics live through execution. Without this, the backend
// allocator may reuse an intermediate's buffer after its final consumer.
for (const auto &[_, tensor] : snapshot_tensors) ggml_set_output(tensor);
ggml_cgraph *graph = ggml_new_graph(ctx);
ggml_build_forward_expand(graph, h);
ggml_backend_buffer_t buffer = ggml_backend_alloc_ctx_tensors(ctx, w.backend);
if (!buffer) throw std::runtime_error("cannot allocate layer graph");
auto release = std::unique_ptr<ggml_backend_buffer, decltype(&ggml_backend_buffer_free)>(buffer, ggml_backend_buffer_free);
std::vector<int32_t> positions(seq); for (int32_t index = 0; index < seq; ++index) positions[static_cast<size_t>(index)] = index;
ggml_backend_tensor_set(x, input.data(), 0, input.size() * sizeof(float));
ggml_backend_tensor_set(pos, positions.data(), 0, positions.size() * sizeof(int32_t));
if (ggml_backend_graph_compute(w.backend, graph) != GGML_STATUS_SUCCESS) throw std::runtime_error("GGML layer graph failed");
layer_result result;
result.output = read_tensor(h);
for (const auto &[name, tensor] : snapshot_tensors) {
result.snapshots.emplace_back(name, read_tensor(tensor));
}
return result;
}
} // namespace
int main(int argc, char **argv) try {
if (argc != 5 && argc != 7) { std::fprintf(stderr, "usage: %s LAYER.gguf FIXTURE_DIR LAYER_INDEX cpu|vulkan [INPUT.f32 OUTPUT.f32]\n", argv[0]); return 2; }
const int layer = std::atoi(argv[3]);
if (layer < 0 || layer >= 32) throw std::runtime_error("layer index must be in [0, 31]");
const bool use_vulkan = std::string_view(argv[4]) == "vulkan";
if (!use_vulkan && std::string_view(argv[4]) != "cpu") throw std::runtime_error("backend must be cpu or vulkan");
const auto fixture = std::filesystem::path(argv[2]);
const auto input = read_f32(argc == 7 ? std::filesystem::path(argv[5]) : fixture / (layer == 0 ? "token_embeddings.f32" : layer_fixture_name(layer - 1)));
const auto expected = read_f32(fixture / layer_fixture_name(layer));
const auto loaded = load(argv[1], use_vulkan);
const auto actual = run(*loaded, input, use_vulkan);
if (argc == 7) write_f32(argv[6], actual.output);
float error = 0.f; for (size_t index = 0; index < actual.output.size(); ++index) error = std::max(error, std::abs(actual.output[index] - expected[index]));
for (const auto &[name, values] : actual.snapshots) {
if (!std::getenv("KIMODO_LLM_DEBUG")) continue;
const auto reference = read_f32(fixture / (name + ".f32"));
if (reference.size() != values.size()) throw std::runtime_error("unexpected snapshot size for " + name);
float snapshot_error = 0.f;
double squared_error = 0.0, squared_reference = 0.0;
for (size_t index = 0; index < values.size(); ++index) {
const float difference = values[index] - reference[index];
snapshot_error = std::max(snapshot_error, std::abs(difference));
squared_error += static_cast<double>(difference) * difference;
squared_reference += static_cast<double>(reference[index]) * reference[index];
}
std::printf("%s max_abs=%g rel_l2=%g\n", name.c_str(), snapshot_error, std::sqrt(squared_error / squared_reference));
}
std::printf("layer%d %s max_abs=%g\n", layer, argv[4], error);
return error < 2e-2f ? 0 : 1;
} catch (const std::exception &error) { std::fprintf(stderr, "%s\n", error.what()); return 1; }

View File

@ -0,0 +1,55 @@
#include "llm_text_encoder.hpp"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <string>
namespace {
bool read_f32(const std::string &path, float *out, size_t count) {
std::ifstream in(path, std::ios::binary);
in.read(reinterpret_cast<char *>(out), static_cast<std::streamsize>(count * sizeof(float)));
return in && in.peek() == std::ifstream::traits_type::eof();
}
} // namespace
int main(int argc, char **argv) {
if (argc != 3) {
std::cerr << "usage: " << argv[0] << " TEXT_BUNDLE FIXTURE_DIR\n";
return 2;
}
// The versioned upstream fixture deliberately contains only tensors. Its
// captured prompt is kept here to make the complete native route explicit.
constexpr std::string_view prompt =
"A person runs forward and then leaps over an obstacle in front of them.";
auto encoder = kimodo::detail::llm_text_encoder::load(argv[1]);
if (!encoder) {
std::cerr << "load failed: " << encoder.error() << '\n';
return 1;
}
auto actual = (*encoder)->encode(prompt);
if (!actual) {
std::cerr << "encode failed: " << actual.error() << '\n';
return 1;
}
std::array<float, 4096> expected{};
if (!read_f32(std::string(argv[2]) + "/pooled_embedding.f32", expected.data(), expected.size())) {
std::cerr << "cannot read fixture pooled_embedding.f32\n";
return 2;
}
float maximum = 0.0F;
double squared_error = 0.0, squared_reference = 0.0;
for (size_t i = 0; i < expected.size(); ++i) {
const double diff = double((*actual)[i]) - expected[i];
maximum = std::max(maximum, std::abs(static_cast<float>(diff)));
squared_error += diff * diff;
squared_reference += double(expected[i]) * expected[i];
}
const double rel_l2 = std::sqrt(squared_error / squared_reference);
const char *backend = std::getenv("KIMODO_BACKEND");
std::cout << "native text session backend=" << (backend ? backend : "vulkan")
<< " max_abs=" << maximum << " rel_l2=" << rel_l2 << '\n';
return rel_l2 < 0.01 ? 0 : 1;
}

View File

@ -0,0 +1,32 @@
#include "llm_tokenizer.hpp"
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <vector>
int main(int argc, char **argv) {
if (argc != 3) { std::fprintf(stderr, "usage: %s TOKENIZER.gguf FIXTURE_DIR\n", argv[0]); return 2; }
auto tokenizer = kimodo::detail::llm_tokenizer::load(argv[1]);
if (!tokenizer) { std::fprintf(stderr, "%s\n", tokenizer.error().c_str()); return 1; }
const std::string prompt = "A person runs forward and then leaps over an obstacle in front of them.";
auto tokens = (*tokenizer)->encode(prompt);
if (!tokens) { std::fprintf(stderr, "%s\n", tokens.error().c_str()); return 1; }
std::ifstream input(std::filesystem::path(argv[2]) / "input_ids.i64", std::ios::binary | std::ios::ate);
if (!input) { std::fprintf(stderr, "cannot read input fixture\n"); return 1; }
if (input.tellg() != static_cast<std::streamoff>(tokens->size() * sizeof(std::int64_t))) { std::fprintf(stderr, "token count: got %zu fixture bytes=%lld; IDs:", tokens->size(), static_cast<long long>(input.tellg())); for (int id : *tokens) std::fprintf(stderr, " %d", id); std::fprintf(stderr, "\n"); return 1; }
std::vector<std::int64_t> expected(tokens->size()); input.seekg(0); input.read(reinterpret_cast<char *>(expected.data()), static_cast<std::streamsize>(expected.size() * sizeof(std::int64_t)));
for (size_t i = 0; i < tokens->size(); ++i) if ((*tokens)[i] != expected[i]) { std::fprintf(stderr, "token %zu: got %d expected %lld\n", i, (*tokens)[i], static_cast<long long>(expected[i])); return 1; }
const std::pair<const char *, std::vector<int>> cases[] = {
{"hello, world!", {128000, 15339, 11, 1917, 0}},
{"caf\xc3\xa9 d\xc3\xa9j\xc3\xa0 vu", {128000, 936, 59958, 46939, 33614}},
{"\xe4\xbd\xa0\xe5\xa5\xbd\xef\xbc\x8c\xe4\xb8\x96\xe7\x95\x8c", {128000, 57668, 53901, 3922, 102616}},
{"\xf0\x9f\x99\x82 running\nfast", {128000, 9468, 19044, 4401, 198, 9533}},
};
for (const auto &[text, reference] : cases) {
auto actual = (*tokenizer)->encode(text);
if (!actual || *actual != reference) { std::fprintf(stderr, "UTF-8 tokenizer mismatch for %s\n", text); return 1; }
}
if ((*tokenizer)->encode("\xc3").has_value()) { std::fprintf(stderr, "malformed UTF-8 was accepted\n"); return 1; }
std::printf("tokenizer matched prompt and %zu UTF-8 upstream cases\n", std::size(cases)); return 0;
}

189
tests/root_parity.cpp Normal file
View File

@ -0,0 +1,189 @@
#include <ggml.h>
#include <ggml-alloc.h>
#include <ggml-backend.h>
#include <ggml-cpu.h>
#include <ggml-vulkan.h>
#include <gguf.h>
#include "motion_rep.hpp"
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <string>
#include <unordered_map>
#include <vector>
namespace {
constexpr int D = 1024, LLM = 4096, H = 8, HD = 128, TEXT = 50, PREFIX = 52, T = 8, B = 3, S = PREFIX + T;
thread_local std::vector<std::pair<ggml_tensor *, std::vector<float>>> graph_inputs;
std::vector<float> read_f32(const std::string &path) {
std::ifstream in(path, std::ios::binary | std::ios::ate);
if (!in || in.tellg() < 0 || static_cast<std::size_t>(in.tellg()) % sizeof(float)) throw std::runtime_error("invalid F32 fixture: " + path);
std::vector<float> result(static_cast<std::size_t>(in.tellg()) / sizeof(float));
in.seekg(0); in.read(reinterpret_cast<char *>(result.data()), static_cast<std::streamsize>(result.size() * sizeof(float)));
if (!in) throw std::runtime_error("short F32 fixture: " + path); return result;
}
struct weights {
ggml_context *ctx = nullptr; gguf_context *file = nullptr; ggml_backend_t backend = nullptr; ggml_backend_buffer_t buffer = nullptr; std::unordered_map<std::string, ggml_tensor *> tensors;
explicit weights(const char *path) {
gguf_init_params p{true, &ctx}; file = gguf_init_from_file(path, p);
if (!file || !ctx) throw std::runtime_error("GGML could not load GGUF");
for (int64_t i = 0; i < gguf_get_n_tensors(file); ++i) {
const char *n = gguf_get_tensor_name(file, i); tensors.emplace(n, ggml_get_tensor(ctx, n));
}
setenv("GGML_VK_DISABLE_COOPMAT", "1", 0);
setenv("GGML_VK_DISABLE_COOPMAT2", "1", 0);
setenv("GGML_VK_DISABLE_F16", "1", 0);
if (ggml_backend_vk_get_device_count() > 0) backend = ggml_backend_vk_init(0);
if (!backend) backend = ggml_backend_cpu_init();
if (!backend) throw std::runtime_error("backend init failed");
buffer = ggml_backend_alloc_ctx_tensors(ctx, backend); if (!buffer) throw std::runtime_error("weight buffer allocation failed");
std::ifstream input(path, std::ios::binary); if (!input) throw std::runtime_error("cannot re-open GGUF");
std::vector<char> scratch(8*1024*1024);
const size_t data_start = gguf_get_data_offset(file);
for (int64_t i=0;i<gguf_get_n_tensors(file);++i) {
auto *tensor=ggml_get_tensor(ctx,gguf_get_tensor_name(file,i)); const size_t bytes=ggml_nbytes(tensor), offset=gguf_get_tensor_offset(file,i);
input.seekg(static_cast<std::streamoff>(data_start+offset));
for(size_t done=0;done<bytes;) { const size_t n=std::min(scratch.size(),bytes-done); input.read(scratch.data(),static_cast<std::streamsize>(n)); if(!input) throw std::runtime_error("short GGUF tensor read"); ggml_backend_tensor_set(tensor,scratch.data(),done,n); done+=n; }
}
}
~weights() { if (buffer) ggml_backend_buffer_free(buffer); if (file) gguf_free(file); if (ctx) ggml_free(ctx); if (backend) ggml_backend_free(backend); }
ggml_tensor *get(const std::string &name) const { auto it = tensors.find(name); if (it == tensors.end()) throw std::runtime_error("missing tensor " + name); return it->second; }
};
std::vector<float> tensor_values(const weights &w, const std::string &name) {
auto *tensor = w.get(name);
std::vector<float> values(static_cast<size_t>(ggml_nelements(tensor)));
ggml_backend_tensor_get(tensor, values.data(), 0, values.size()*sizeof(float));
return values;
}
std::vector<float> root_local_reference(const weights &w, const std::vector<float> &root, const std::vector<float> &mask) {
const auto global_mean = tensor_values(w, "stats.global_root.mean");
const auto global_std = tensor_values(w, "stats.global_root.std");
const auto local_mean = tensor_values(w, "stats.local_root.mean");
const auto local_std = tensor_values(w, "stats.local_root.std");
if (global_mean.size()!=5 || global_std.size()!=5 || local_mean.size()!=4 || local_std.size()!=4) throw std::runtime_error("unexpected root statistics size");
auto result = kimodo::detail::global_root_to_local_root(root, mask, B, T, global_mean, global_std, local_mean, local_std);
if (!result) throw std::runtime_error(result.error());
return std::move(*result);
}
ggml_tensor *input3(ggml_context *ctx, const float *data, int a, int b, int c) {
auto *r = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, a, b, c); graph_inputs.emplace_back(r, std::vector<float>(data, data + static_cast<std::size_t>(a)*b*c)); return r;
}
ggml_tensor *linear(ggml_context *ctx, ggml_tensor *x, ggml_tensor *w, ggml_tensor *bias) {
if (w->ne[0] != x->ne[0]) throw std::runtime_error("linear dimension mismatch: weight=" + std::to_string(w->ne[0]) + " input=" + std::to_string(x->ne[0]));
auto *y = ggml_mul_mat(ctx, w, x); ggml_mul_mat_set_prec(y, GGML_PREC_F32); return ggml_add(ctx, y, ggml_repeat(ctx, bias, y));
}
ggml_tensor *layer_norm(ggml_context *ctx, ggml_tensor *x, ggml_tensor *scale, ggml_tensor *bias) {
auto *n = ggml_norm(ctx, x, 1.e-5f); n = ggml_mul(ctx, n, ggml_repeat(ctx, scale, n)); return ggml_add(ctx, n, ggml_repeat(ctx, bias, n));
}
using capture = std::pair<ggml_tensor *, std::vector<float> *>;
std::vector<float> execute(ggml_context *ctx, ggml_tensor *out, std::size_t values, ggml_backend_t backend, const std::vector<capture> &captures = {}) {
ggml_cgraph *graph = ggml_new_graph(ctx); ggml_build_forward_expand(graph, out);
// Expand captured intermediates as graph outputs too. Otherwise gallocr
// is free to reuse their storage after the final output has consumed them.
for (const auto &[tensor, _] : captures) ggml_build_forward_expand(graph, tensor);
ggml_gallocr_t alloc = ggml_gallocr_new(ggml_backend_get_default_buffer_type(backend));
if (!alloc || !ggml_gallocr_reserve(alloc, graph) || !ggml_gallocr_alloc_graph(alloc, graph)) throw std::runtime_error("graph allocation failed");
for (const auto &[tensor, data] : graph_inputs) ggml_backend_tensor_set(tensor, data.data(), 0, data.size()*sizeof(float));
graph_inputs.clear();
if (ggml_backend_graph_compute(backend, graph) != GGML_STATUS_SUCCESS) throw std::runtime_error("GGML graph failed");
for (const auto &[tensor, values_out] : captures) { values_out->resize(static_cast<size_t>(ggml_nelements(tensor))); ggml_backend_tensor_get(tensor, values_out->data(), 0, values_out->size()*sizeof(float)); }
std::vector<float> result(values); ggml_backend_tensor_get(out, result.data(), 0, values*sizeof(float));
ggml_gallocr_free(alloc); return result;
}
ggml_tensor *transformer_layer(ggml_context *ctx, ggml_tensor *x, const weights &w, const std::string &p) {
auto *qkv = linear(ctx, x, w.get(p+"self_attn.in_proj_weight"), w.get(p+"self_attn.in_proj_bias"));
// Keep the reference layout explicit. qkv is [3D, S, B], with the
// PyTorch head dimension contiguous inside D. The generic operations
// below avoid relying on flash-attention's different head/sequence
// conventions while we establish F32 parity.
auto head = [&](size_t block, int h, int b) {
return ggml_view_2d(ctx, qkv, HD, S, qkv->nb[1],
block*static_cast<size_t>(D)*sizeof(float) + static_cast<size_t>(b)*qkv->nb[2] + static_cast<size_t>(h)*HD*sizeof(float));
};
std::vector<ggml_tensor *> batches;
for (int b = 0; b < B; ++b) {
std::vector<ggml_tensor *> heads;
for (int h = 0; h < H; ++h) {
auto *q = ggml_cont(ctx, head(0, h, b));
auto *k = ggml_cont(ctx, head(1, h, b));
auto *v = ggml_cont(ctx, head(2, h, b));
// scores are [key, query]; softmax's first dimension is exactly
// the key axis required by PyTorch's attention implementation.
auto *scores = ggml_mul_mat(ctx, k, q);
ggml_mul_mat_set_prec(scores, GGML_PREC_F32);
scores = ggml_scale(ctx, scores, 1.f/std::sqrt(static_cast<float>(HD)));
auto *prob = ggml_soft_max(ctx, scores);
auto *value_product = ggml_mul_mat(ctx, prob, ggml_cont(ctx, ggml_transpose(ctx, v)));
ggml_mul_mat_set_prec(value_product, GGML_PREC_F32);
heads.push_back(ggml_transpose(ctx, value_product));
}
auto *joined = heads.front();
for (int h = 1; h < H; ++h) joined = ggml_concat(ctx, joined, heads[h], 0);
batches.push_back(ggml_reshape_3d(ctx, joined, D, S, 1));
}
auto *a = batches.front();
for (int b = 1; b < B; ++b) a = ggml_concat(ctx, a, batches[b], 2);
a = linear(ctx, a, w.get(p+"self_attn.out_proj.weight"), w.get(p+"self_attn.out_proj.bias"));
x = layer_norm(ctx, ggml_add(ctx, x, a), w.get(p+"norm1.weight"), w.get(p+"norm1.bias"));
auto *ff = linear(ctx, x, w.get(p+"linear1.weight"), w.get(p+"linear1.bias"));
ff = ggml_gelu_erf(ctx, ff);
ff = linear(ctx, ff, w.get(p+"linear2.weight"), w.get(p+"linear2.bias"));
return layer_norm(ctx, ggml_add(ctx, x, ff), w.get(p+"norm2.weight"), w.get(p+"norm2.bias"));
}
}
int main(int argc, char **argv) try {
if (argc != 4 || (std::string_view(argv[3]) != "root" && std::string_view(argv[3]) != "body")) {
std::fprintf(stderr, "usage: kimodo-root-parity MODEL.gguf FIXTURE_DIR {root|body}\n"); return 2;
}
const std::string stage(argv[3]), prefix = stage + "_model.", f = std::string(argv[2]) + "/";
const int input_dim = stage == "root" ? 546 : 545;
const int output_dim = stage == "root" ? 5 : 268;
weights w(argv[1]);
const auto motion = read_f32(f+stage+"_input_0.f32"), mask = read_f32(f+stage+"_input_1.f32"), text = read_f32(f+stage+"_input_2.f32"), time = read_f32(f+stage+"_input_4.f32"), heading = read_f32(f+stage+"_input_5.f32"), expected = read_f32(f+stage+"_output.f32");
const auto layer0_expected = stage == "root" ? read_f32(f+"root_layer0_output.f32") : std::vector<float>{};
const auto layer0_input = stage == "root" ? read_f32(f+"root_layer0_input.f32") : std::vector<float>{};
std::vector<float> padded_text(static_cast<size_t>(LLM)*TEXT*B), time_pe(static_cast<size_t>(D)*B), angle(static_cast<size_t>(2)*B), pos(static_cast<size_t>(D)*S);
for (int b=0;b<B;++b) { std::memcpy(padded_text.data()+static_cast<size_t>(b)*TEXT*LLM, text.data()+static_cast<size_t>(b)*LLM, LLM*sizeof(float));
const int t = static_cast<int>(time[b]); for (int d=0;d<D;d+=2) { const float z=t*std::pow(10000.f,-static_cast<float>(d)/D); time_pe[static_cast<size_t>(b)*D+d]=std::sin(z); time_pe[static_cast<size_t>(b)*D+d+1]=std::cos(z); }
angle[2*b]=std::cos(heading[b]); angle[2*b+1]=std::sin(heading[b]); }
for (int s=0;s<S;++s) for (int d=0;d<D;d+=2) { const float z=s*std::pow(10000.f,-static_cast<float>(d)/D); pos[static_cast<size_t>(s)*D+d]=std::sin(z); pos[static_cast<size_t>(s)*D+d+1]=std::cos(z); }
std::vector<float> state;
{ ggml_init_params ip{128ULL*1024*1024,nullptr,true}; ggml_context *ctx=ggml_init(ip); if(!ctx) throw std::runtime_error("graph allocation failed");
auto *motion_input=input3(ctx,motion.data(),input_dim,T,B); auto *m=linear(ctx,motion_input,w.get(prefix+"input_linear.weight"),w.get(prefix+"input_linear.bias"));
auto *te=linear(ctx,input3(ctx,padded_text.data(),LLM,TEXT,B),w.get(prefix+"embed_text.weight"),w.get(prefix+"embed_text.bias"));
auto *ti=linear(ctx,input3(ctx,time_pe.data(),D,1,B),w.get(prefix+"embed_timestep.time_embed.0.weight"),w.get(prefix+"embed_timestep.time_embed.0.bias")); ti=linear(ctx,ggml_silu(ctx,ti),w.get(prefix+"embed_timestep.time_embed.2.weight"),w.get(prefix+"embed_timestep.time_embed.2.bias"));
auto *heading_input=input3(ctx,angle.data(),2,1,B); auto *he=linear(ctx,heading_input,w.get(prefix+"linear_first_heading_angle.weight"),w.get(prefix+"linear_first_heading_angle.bias"));
auto *x=ggml_concat(ctx,ggml_concat(ctx,ggml_concat(ctx,te,ti,1),he,1),m,1); auto *position=ggml_new_tensor_2d(ctx,GGML_TYPE_F32,D,S); graph_inputs.emplace_back(position,pos); x=ggml_add(ctx,x,ggml_repeat(ctx,position,x));
state=execute(ctx,x,static_cast<size_t>(D)*S*B,w.backend); if(stage == "root") { float input_max=0.f; for(size_t j=0;j<state.size();++j)input_max=std::max(input_max,std::abs(state[j]-layer0_input[j])); std::printf("root layer0 input max_abs=%g\n",input_max); } ggml_free(ctx); }
for(int i=0;i<16;++i) { ggml_init_params ip{128ULL*1024*1024,nullptr,true}; ggml_context *ctx=ggml_init(ip); auto *x=input3(ctx,state.data(),D,S,B); x=transformer_layer(ctx,x,w,prefix+"seqTransEncoder.layers."+std::to_string(i)+"."); state=execute(ctx,x,static_cast<size_t>(D)*S*B,w.backend); ggml_free(ctx);
if(stage == "root") { const auto layer_expected=read_f32(f+"root_layer"+std::to_string(i)+"_output.f32"); float m=0.f; for(size_t j=0;j<state.size();++j)m=std::max(m,std::abs(state[j]-layer_expected[j])); std::printf("root layer%d output max_abs=%g\n",i,m); } }
std::vector<float> output;
{ ggml_init_params ip{32ULL*1024*1024,nullptr,true}; ggml_context *ctx=ggml_init(ip); auto *all=input3(ctx,state.data(),D,S,B); auto *motion_out=ggml_view_3d(ctx,all,D,T,B,all->nb[1],all->nb[2],static_cast<size_t>(PREFIX)*D*sizeof(float)); motion_out=ggml_cont(ctx,motion_out); auto *y=linear(ctx,motion_out,w.get(prefix+"output_linear.weight"),w.get(prefix+"output_linear.bias")); output=execute(ctx,y,static_cast<size_t>(output_dim)*T*B,w.backend); ggml_free(ctx); }
float max_abs=0.f, sq=0.f, ref=0.f; for(size_t i=0;i<output.size();++i){ float d=output[i]-expected[i]; max_abs=std::max(max_abs,std::abs(d)); sq+=d*d; ref+=expected[i]*expected[i]; }
const float rel=std::sqrt(sq/std::max(ref,1.e-20f)); std::printf("%s parity: max_abs=%g rel_l2=%g\n", stage.c_str(), max_abs,rel);
if (stage == "root") {
const auto expected_local = read_f32(f+"root_local.f32"), local = root_local_reference(w, output, mask);
float local_max=0.f, local_sq=0.f, local_ref=0.f;
for(size_t i=0;i<local.size();++i) { const float d=local[i]-expected_local[i]; local_max=std::max(local_max,std::abs(d)); local_sq+=d*d; local_ref+=expected_local[i]*expected_local[i]; }
const float local_rel=std::sqrt(local_sq/std::max(local_ref,1.e-20f));
std::printf("root local conversion: max_abs=%g rel_l2=%g\n",local_max,local_rel);
if (local_max >= 2.e-3f || local_rel >= 2.e-4f) return 1;
const auto expected_body_input = read_f32(f+"body_input_0.f32");
float body_input_max=0.f;
for(int b=0;b<B;++b) for(int t=0;t<T;++t) {
const size_t root_base=(static_cast<size_t>(b)*T+t)*546, body_base=(static_cast<size_t>(b)*T+t)*545;
for(int d=0;d<4;++d) body_input_max=std::max(body_input_max,std::abs(local[(static_cast<size_t>(b)*T+t)*4+d]-expected_body_input[body_base+d]));
for(int d=0;d<541;++d) body_input_max=std::max(body_input_max,std::abs(motion[root_base+5+d]-expected_body_input[body_base+4+d]));
}
std::printf("root-to-body input max_abs=%g\n",body_input_max);
if (body_input_max >= 2.e-3f) return 1;
}
return (max_abs < 2.e-3f && rel < 2.e-4f) ? 0 : 1;
} catch(const std::exception &e) { std::fprintf(stderr,"root parity error: %s\n",e.what()); return 1; }

11
tests/sampler2_test.cpp Normal file
View File

@ -0,0 +1,11 @@
#include "denoiser.hpp"
#include "ggml_weights.hpp"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <fstream>
#include <stdexcept>
#include <string>
#include <vector>
static std::vector<float> readf(const std::string&p){std::ifstream f(p,std::ios::binary|std::ios::ate);if(!f||f.tellg()<0)throw std::runtime_error("missing fixture: "+p);std::vector<float>x(size_t(f.tellg())/4);f.seekg(0);f.read(reinterpret_cast<char*>(x.data()),std::streamsize(x.size()*4));return x;}
int main(int c,char**v)try{if(c!=5)return 2;auto w=kimodo::detail::ggml_motion_weights::load(v[1]);if(!w)throw std::runtime_error(w.error());std::string d=std::string(v[2])+"/";auto o=kimodo::detail::sample_motion_from_noise(**w,readf(d+"sampling_initial_noise.f32"),readf(d+"text_features.f32"),std::stoul(v[3]),unsigned(std::stoul(v[4])),2.f,2.f);if(!o)throw std::runtime_error(o.error());auto e=readf(d+"sampling_final_state.f32");float m=0;for(size_t i=0;i<e.size();++i)m=std::max(m,std::abs(e[i]-(*o)[i]));std::printf("sampler max_abs=%g\n",m);return m<3.e-3f?0:1;}catch(const std::exception&e){std::fprintf(stderr,"%s\n",e.what());return 1;}