sota-model-router / code /inference.py
m-newhauser's picture
Upload trained model to m-newhauser/sota-model-router
bf99391 verified
Raw
History Blame Contribute Delete
5.6 kB
"""
SageMaker MME inference script for GLiNER2.
Pure computation only — all schema normalization and construction happens
in Brain. SageMaker receives a pre-built, serialized schema (schema_dict +
schema_metadata) and reconstructs it for batch_extract.
Wire format (from Brain):
{
"text": str | list[str],
"schema_dict": dict, # Schema.build() output
"schema_metadata": dict, # _field_metadata, _entity_metadata, orders, etc.
"threshold": float,
"batch_size": int,
"include_confidence": bool,
"include_spans": bool,
"format_results": bool,
}
"""
import json
import logging
import os
import subprocess
import sys
from typing import Any
try:
import gliner2 as _check # noqa: F401
except ImportError:
_req_path = os.path.join(os.path.dirname(__file__), "requirements.txt")
if os.path.exists(_req_path):
subprocess.check_call(
[sys.executable, "-m", "pip", "install", "-r", _req_path, "-q"],
stdout=subprocess.DEVNULL,
)
else:
subprocess.check_call(
[sys.executable, "-m", "pip", "install", "gliner2", "-q"],
stdout=subprocess.DEVNULL,
)
import torch # noqa: E402
from gliner2 import GLiNER2 # noqa: E402
logger = logging.getLogger(__name__)
CPU_THREADS_PER_MODEL = 4
class DeserializedSchema:
"""
Reconstructs a Schema-like object from Brain's serialized wire format.
batch_extract accepts either a Schema object or a raw dict. When given
a Schema-like object, it reads internal metadata (_field_metadata,
_entity_metadata, etc.) for correct per-field processing. This class
exposes that interface without requiring the full Schema builder.
Args:
schema_dict: Output of Schema.build() (entities, classifications, etc.)
metadata: Serialized metadata (field_metadata, entity_metadata, orders)
"""
def __init__(self, schema_dict: dict, metadata: dict) -> None:
self._schema_dict = schema_dict
self._entity_metadata = metadata.get("entity_metadata", {})
self._field_metadata = metadata.get("field_metadata", {})
self._field_orders = metadata.get("field_orders", {})
self._entity_order = metadata.get("entity_order", [])
self._relation_order = metadata.get("relation_order", [])
self._relation_metadata = metadata.get("relation_metadata", {})
def build(self) -> dict:
"""Return the schema dict for batch_extract."""
return self._schema_dict
class ContainerPlaceholder:
"""SageMaker MME requires an initial model — this is the no-op stand-in."""
pass
def model_fn(model_dir: str) -> GLiNER2 | ContainerPlaceholder:
"""Load a GLiNER2 model from extracted .tar.gz artifacts."""
logger.info("Loading model from: %s", model_dir)
if os.path.exists(os.path.join(model_dir, "mme_container.txt")):
return ContainerPlaceholder()
torch.set_num_threads(CPU_THREADS_PER_MODEL)
hf_token = os.environ.get("HF_TOKEN")
if os.path.exists(os.path.join(model_dir, "config.json")):
model = GLiNER2.from_pretrained(model_dir, token=hf_token)
else:
name = os.environ.get("GLINER_MODEL", "fastino/gliner2-base-v1")
logger.warning("No config.json — downloading: %s", name)
model = GLiNER2.from_pretrained(name, token=hf_token)
model.eval()
return model
def input_fn(request_body: str, content_type: str) -> dict[str, Any]:
"""Parse JSON request body."""
if content_type != "application/json":
raise ValueError(f"Unsupported content type: {content_type}")
return json.loads(request_body)
def predict_fn(input_data: dict[str, Any], model: GLiNER2 | ContainerPlaceholder) -> Any:
"""
Execute inference with a pre-built schema from Brain.
Brain handles all schema normalization and construction. SageMaker
receives schema_dict + schema_metadata and reconstructs a Schema-like
object for batch_extract.
Args:
input_data: Request dict with text, schema_dict, schema_metadata, etc.
model: GLiNER2 model instance or placeholder
Returns:
Inference results (single dict for single text, list for batch)
Raises:
ValueError: If required fields missing or model is placeholder
"""
if isinstance(model, ContainerPlaceholder):
raise ValueError("Set the TargetModel header to a valid model archive.")
text = input_data.get("text")
schema_dict = input_data.get("schema_dict")
schema_metadata = input_data.get("schema_metadata", {})
threshold = input_data.get("threshold", 0.5)
if not text:
raise ValueError("'text' field is required")
if schema_dict is None:
raise ValueError("'schema_dict' field is required")
is_batch = isinstance(text, list)
texts = text if is_batch else [text]
schema = DeserializedSchema(schema_dict, schema_metadata)
results = model.batch_extract(
texts,
schema,
batch_size=input_data.get("batch_size", 8),
threshold=threshold,
format_results=input_data.get("format_results", True),
include_confidence=input_data.get("include_confidence", False),
include_spans=input_data.get("include_spans", False),
)
return results if is_batch else results[0]
def output_fn(prediction: Any, accept: str) -> str:
"""Serialize prediction to JSON."""
if accept != "application/json":
raise ValueError(f"Unsupported accept type: {accept}")
return json.dumps(prediction)