pino-source-code / src /pino /inference_cloud.py
mattbitzesty's picture
perf(optimizer): cache PIMT model locally instead of hitting Space every candidate
96f3ac5
Raw
History Blame Contribute Delete
5.01 kB
from __future__ import annotations
import json
import logging
from pathlib import Path
from typing import Any
import numpy as np
import requests
from huggingface_hub import InferenceClient
from .pimt_model import FragranceTrajectoryDataset
from .pimt_model_hf import PIMTConfig, PhysicsInformedMixtureTransformer
logger = logging.getLogger("pino.inference_cloud")
def _resolve_space_url(token: str | None = None) -> str:
"""Return the canonical predict URL for the private HF training Space."""
return "https://mattbitzesty-pino-pimt-training.hf.space/predict"
def encode_formula(formula: dict[str, Any]) -> dict[str, Any]:
"""
Convert a raw formula record into token/physics tensors ready for the cloud endpoint.
For now this mirrors FragranceTrajectoryDataset's preprocessing for a single record.
"""
from .embeddings import OlfactoryEmbeddingEngine
from .pimt_model import FragranceTrajectoryDataset
from .semantics import get_objective_targets
engine = OlfactoryEmbeddingEngine(use_fallback=True)
tokens = []
for comp in formula["formula"]:
smiles = comp.get("smiles", "")
if not smiles:
smiles = _smiles_for_cas(comp.get("cas", ""))
tokens.append(engine.get_embedding(smiles).tolist())
t_steps = len(formula["trajectory"])
n_mol = len(tokens)
states = np.zeros((t_steps, n_mol, 2), dtype=np.float32)
for t, step in enumerate(formula["trajectory"]):
for j, comp in enumerate(formula["formula"]):
if j >= n_mol:
break
name = comp.get("cas", f"ing_{j}")
states[t, j, 0] = step["x_liquid"].get(name, 0.0)
states[t, j, 1] = np.log10(max(step["OAV"].get(name, 0.0), 1e-10))
src_key_padding_mask = np.zeros(n_mol, dtype=np.bool_)
return {
"tokens": tokens,
"physics": states.tolist(),
"src_key_padding_mask": src_key_padding_mask.tolist(),
}
def _smiles_for_cas(cas: str) -> str:
"""Best-effort registry lookup."""
try:
from .registry import AromaRegistry
reg = AromaRegistry()
rec = reg.get(cas)
reg.close()
return rec.get("smiles", "") if rec else ""
except Exception:
return ""
def predict_cloud(
formula: dict[str, Any],
token: str | None = None,
*,
_cached_model: Any = None,
) -> dict[str, Any]:
"""
POST a processed formula to the HF Space inference endpoint and return predictions.
Falls back to a local model if the Space endpoint is not yet available. A
cached model can be supplied to avoid repeated Hub downloads.
"""
url = _resolve_space_url(token)
payload = encode_formula(formula)
headers = {}
if token:
headers["Authorization"] = f"Bearer {token}"
try:
logger.info("Sending prediction request to %s", url)
response = requests.post(url, json=payload, headers=headers, timeout=120)
response.raise_for_status()
result = response.json()
if result.get("status") == "error":
raise RuntimeError(result.get("message", "Unknown cloud error"))
return result
except Exception as e:
logger.warning("Cloud inference failed (%s); falling back to local model", e)
return _predict_local(formula, cached_model=_cached_model)
def _predict_local(formula: dict[str, Any], *, cached_model: Any = None) -> dict[str, Any]:
"""Local CPU fallback for development/testing, with optional cached model."""
import torch
model = cached_model
if model is None:
model = PhysicsInformedMixtureTransformer.from_pretrained("mattbitzesty/pino-pimt")
model.eval()
payload = encode_formula(formula)
tokens = torch.tensor(payload["tokens"], dtype=torch.float32).unsqueeze(0)
physics = torch.tensor(payload["physics"], dtype=torch.float32).unsqueeze(0)
mask = torch.tensor(payload["src_key_padding_mask"], dtype=torch.bool).unsqueeze(0)
with torch.no_grad():
output = model(tokens, physics, src_key_padding_mask=mask)
return {
"status": "success (local fallback)",
"objective": output.logits.cpu().numpy().tolist(),
"subjective": output.subjective.cpu().numpy().tolist(),
}
def predict_with_inference_client(
formula: dict[str, Any],
model_id: str = "mattbitzesty/pino-pimt",
token: str | None = None,
) -> dict[str, Any]:
"""
Placeholder route using huggingface_hub.InferenceClient.
Note: InferenceClient is designed for standard HF tasks (text, image, etc.).
For custom tensor payloads, the Space /predict endpoint via requests is preferred.
This function demonstrates the managed client signature.
"""
client = InferenceClient(model=model_id, token=token)
# Standard InferenceClient doesn't support arbitrary tensor payloads; this route
# documents the intended API surface and falls back to the Space endpoint.
return predict_cloud(formula, token=token)