semantic-harmonization-api / src /llm /jpt_runtime.py
Ruurd's picture
Deploy OpenAI classification and protected terminology streaming
c2b7494 verified
Raw History Blame Contribute Delete
5.76 kB
"""JPT-4B through its published llm2jev scoring interface on a local GPU."""
from functools import lru_cache
import platform
class AppleJPT:
"""Published llm2jev prompts and probabilities with MLX model execution."""
def __init__(self, model_name):
from mlx_vlm import load
from src.llm.model_paths import prepared_model_path
print(f'[JPT] Loading {model_name} with MLX (original BF16 weights)',flush=True)
self.model,self.processor=load(prepared_model_path(model_name))
self.prefix_tokens=[];self.prefix_cache=None
def warm(self,prefix,images):
if images:raise ValueError('The study uses text-only questions.')
import mlx.core as mx
# Leave a boundary margin so tokenization cannot merge the final prefix
# token with the question. Every scoring request verifies this prefix.
tokens=self.processor.tokenizer.encode(prefix,add_special_tokens=False)[:-2]
if tokens==self.prefix_tokens:return
self.prefix_tokens=tokens;lm=self.model.language_model
self.prefix_cache=lm.make_cache()
if tokens:
lm(mx.array([tokens]),cache=self.prefix_cache,skip_logits=True)
mx.eval([c.state for c in self.prefix_cache])
def score(self,text,images,ids):
if images:raise ValueError('The study uses text-only questions.')
rows,count=self._score_texts([text],ids)
return rows[0],count
def _score_texts(self,texts,ids):
import mlx.core as mx
tokens=[self.processor.tokenizer.encode(text,add_special_tokens=False) for text in texts]
lm=self.model.language_model
cache=lm.make_cache();suffixes=tokens
if self.prefix_tokens and all(t[:len(self.prefix_tokens)]==self.prefix_tokens for t in tokens):
from mlx.utils import tree_map
for fresh,saved in zip(cache,self.prefix_cache):
snapshot=tree_map(lambda x:mx.repeat(x,len(texts),axis=0) if isinstance(x,mx.array) else x,
saved.prefix_cache_snapshot())
fresh.prefix_cache_restore(snapshot)
suffixes=[t[len(self.prefix_tokens):] for t in tokens]
lengths=[len(s) for s in suffixes];width=max(lengths)
# Future padding cannot affect earlier positions in this causal model.
# Select each row's final real token before projecting to vocabulary.
padded=[s+[0]*(width-len(s)) for s in suffixes]
out=lm(mx.array(padded),cache=cache,skip_logits=True,return_hidden=True)
hidden=out.hidden_states[-1][mx.arange(len(texts)),mx.array(lengths)-1][:,None,:]
logits=lm.speculative_logits_from_hidden(hidden)[:,0].astype(mx.float32)
values=(logits-mx.logsumexp(logits,axis=-1,keepdims=True))[:,mx.array(ids)].tolist()
return values,sum(map(len,tokens))
def score_many(self,texts,ids):
rows=[];count=0
for i in range(0,len(texts),4):
batch,n=self._score_texts(texts[i:i+4],ids);rows.extend(batch);count+=n
print(f'[JPT] {min(i+4,len(texts))}/{len(texts)} questions scored',flush=True)
return rows,count
@lru_cache(maxsize=1)
def load_jpt(model_name):
from llm2jev.engine import LLM2Jev
if platform.system()=='Darwin' and platform.machine()=='arm64':
backend=AppleJPT(model_name)
return LLM2Jev(backend.processor,backend,temperature=1.036,workers=1)
import torch
from llm2jev.backends import HF
class LastTokenHF(HF):
def __init__(self, model, device=None, **kwargs):
# Loading safetensors directly onto MPS starts concurrent transfer
# threads. Load on CPU and transfer serially to avoid a native
# allocator/Metal crash in that loading path on this Mac.
super().__init__(model, device='cpu' if device=='mps' else device, **kwargs)
if device=='mps':self.model.to(device)
# Restrict the LM projection to the last position. The upstream HF
# reference projects every EDF token, wasting substantial GPU memory.
def score(self, text, images, ids):
if images:
raise ValueError('This experiment uses text-only JPT inputs.')
rows, count = self.score_many([text], ids)
return rows[0], count
def score_many(self, texts, ids):
self.tok.padding_side = 'left'
rows = []; count = 0
for start in range(0, len(texts), 4):
inputs = self.tok(texts[start:start+4],padding=True,return_tensors='pt',
add_special_tokens=False)
count += int(inputs['attention_mask'].sum())
inputs = {k:v.to(self.model.device) for k,v in inputs.items()}
with self.lock, torch.inference_mode():
logits = self.model(**inputs,logits_to_keep=1).logits[:,-1].float().log_softmax(-1)
rows.extend(logits[:,ids].cpu().tolist())
return rows,count
device = 'mps' if torch.backends.mps.is_available() else 'cuda' if torch.cuda.is_available() else 'cpu'
print(f'[JPT] Loading {model_name} on {device}',flush=True)
from src.llm.model_paths import prepared_model_path
backend = LastTokenHF(prepared_model_path(model_name),device=device,dtype='bfloat16')
return LLM2Jev(backend.processor or backend.tok,backend,temperature=1.036,workers=1)
def predict_jpt(state, questions, model_name):
answers, usage = load_jpt(model_name).run(state, questions)
return {'answers':answers,'model':model_name,'usage':usage,
'runtime':'llm2jev/mlx' if platform.system()=='Darwin' and platform.machine()=='arm64' else 'llm2jev/hf',
'temperature':1.036}