Vili/scripts/kimodo-text-encode.py
Meta Vens c3684b733b Encoder: assembly B on CPU with a complete identity and a thread cap
The encoder child is CPU only (GPU placement, GpuBusy and plan_placement
are deleted) and assembles LLM2Vec B: Llama base, MNTP merged, then the
supervised adapter. The cache identity names assembly, device, dtype,
torch/transformers/peft/accelerate versions, Kimodo commit and the child's
own sha256 beside the three revisions. VILI_THREADS (1..16) caps torch in
the worker and the child. The worker binds port 0 and reports it in its
ready line; errors name a class and place, never the request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:14:09 +02:00

79 lines
3.2 KiB
Python

#!/usr/bin/env python3
"""One-shot Kimodo text encoder: texts on stdin, vectors in --out, then exit so the OS reclaims its RAM.
Reads {"texts": [...]} as JSON on stdin. Loads LLM2Vec assembly B on CPU in bfloat16, encodes each text alone as
LLM2VecEncoder does, and writes a MessagePack answer. The torch thread budget is VILI_THREADS.
"""
import json
import os
import resource
import sys
import time
from pathlib import Path
import msgpack
import torch
sys.path.insert(0, str(Path(__file__).resolve().parent))
from text_encoding import ASSEMBLY, EMBEDDING_DIM, LLAMA_BASE, apply_thread_cap # noqa: E402
from peft import PeftModel # noqa: E402
from transformers import AutoConfig, AutoTokenizer # noqa: E402
from kimodo.model.llm2vec.llm2vec import LLM2Vec # noqa: E402
from kimodo.model.load_model import TEXT_ENCODER_PRESETS # noqa: E402
def load_encoder(mntp_id, supervised_id, dtype):
"""Assembly B: the Llama base with the MNTP adapter merged in, then the supervised adapter on top.
Kimodo's own LLM2VecEncoder loads the MNTP repo as the base and never applies the MNTP weights under the
pinned transformers (assembly A); this assembly is ours (motion-pipeline:ruling:encoder-assembly-b). The
identity carries the Kimodo commit and this file's sha256, so a changed loader misses the cache.
"""
tokenizer = AutoTokenizer.from_pretrained(LLAMA_BASE)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "left"
model_class = LLM2Vec._get_model_class(AutoConfig.from_pretrained(LLAMA_BASE).__class__.__name__, enable_bidirectional=True)
model = model_class.from_pretrained(LLAMA_BASE, torch_dtype=dtype)
model = PeftModel.from_pretrained(model, mntp_id).merge_and_unload()
model = PeftModel.from_pretrained(model, supervised_id)
return LLM2Vec(model=model, tokenizer=tokenizer)
def main() -> int:
apply_thread_cap(os.environ)
texts = json.load(sys.stdin)["texts"]
out = Path(sys.argv[sys.argv.index("--out") + 1])
preset = TEXT_ENCODER_PRESETS["llm2vec"]["kwargs"]
started = time.perf_counter()
model = load_encoder(preset["base_model_name_or_path"], preset["peft_model_name_or_path"], getattr(torch, preset["dtype"]))
model.eval()
for p in model.parameters():
p.requires_grad = False
load_seconds = time.perf_counter() - started
vectors, encode_seconds = [], []
for text in texts:
began = time.perf_counter()
features = model.tokenize([model.prepare_for_tokenization(model._convert_to_str("", text))])
with torch.no_grad():
vector = model.forward(features).detach().cpu().to(torch.float32)[0]
assert vector.shape == (EMBEDDING_DIM,)
vectors.append(vector.numpy().tobytes())
encode_seconds.append(time.perf_counter() - began)
out.write_bytes(msgpack.packb({
"vectors": vectors,
"encodeSeconds": encode_seconds,
"loadSeconds": load_seconds,
"placement": {"cpuBytes": sum(p.numel() * p.element_size() for p in model.parameters())},
"peakRssBytes": resource.getrusage(resource.RUSAGE_SELF).ru_maxrss * 1024,
"device": "cpu",
"assembly": ASSEMBLY,
}, use_bin_type=True))
return 0
sys.exit(main())