cookienter's picture
Load complete private model snapshots locally
95d0236
Raw History Blame Contribute Delete
4.02 kB
"""Private phrase classification; inputs are not persisted by this application."""
import json
import os
from functools import lru_cache
from pathlib import Path
import gradio as gr
from huggingface_hub import snapshot_download
import numpy as np
import torch
import spaces
from transformers import AutoModelForSequenceClassification, AutoTokenizer
from inference import calibrate, rank_average
CONFIG = json.loads((Path(__file__).parent / 'deployment_config.json').read_text())
LABELS = CONFIG['label2id']
ID2LABEL = {int(index): label for label, index in LABELS.items()}
ZERO_GPU = os.getenv('MS_ZERO_GPU') == '1'
DEVICE = 'cuda' if ZERO_GPU or torch.cuda.is_available() else 'cpu'
RECORDS = {record['name']: record for record in CONFIG['models']}
CHOICES = CONFIG['choices']
@lru_cache(maxsize=len(RECORDS) if ZERO_GPU else 1)
def load_model(name):
record = RECORDS[name]
token = os.environ.get('HF_TOKEN')
print('Loading classifier:', name, flush=True)
model_path = snapshot_download(
record['repo_id'], revision=record.get('revision'), token=token, max_workers=2,
allow_patterns=['*.safetensors', 'config.json', 'tokenizer*.json',
'special_tokens_map.json', 'added_tokens.json', 'vocab.*',
'merges.txt', '*.model'],
)
model = AutoModelForSequenceClassification.from_pretrained(
model_path, local_files_only=True, trust_remote_code=False,
use_safetensors=True,
).to(DEVICE).eval()
if {label: int(index) for label, index in model.config.label2id.items()} != LABELS:
raise RuntimeError('Model label mapping differs from the deployment configuration')
tokenizer = AutoTokenizer.from_pretrained(model_path, local_files_only=True, trust_remote_code=False)
print('Classifier ready:', name, flush=True)
return model, tokenizer
def probabilities(name, text):
model, tokenizer = load_model(name)
inputs = tokenizer(text, return_tensors='pt', truncation=True,
max_length=int(RECORDS[name]['max_length'])).to(DEVICE)
with torch.inference_mode():
return torch.softmax(model(**inputs).logits, dim=-1).cpu().numpy()
@spaces.GPU(duration=60)
def classify(text, choice):
if not isinstance(text, str) or not text.strip():
raise gr.Error('Enter a phrase first.')
if len(text) > 20000:
raise gr.Error('Enter a shorter phrase (at most 20,000 characters).')
if choice not in CHOICES:
raise gr.Error('Select one of the available classifiers.')
selected = CHOICES[choice]
if selected == '__ensemble__':
values = rank_average([probabilities(name, text) for name in CONFIG['model_order']])
values = calibrate(values, CONFIG['calibration']['temperature'])
else:
values = probabilities(selected, text)
row = values[0]
index = int(np.argmax(row))
scores = {ID2LABEL[i]: float(value) for i, value in enumerate(row)}
return ID2LABEL[index], scores
if ZERO_GPU:
# ZeroGPU requires CUDA placement at startup, outside the GPU function.
for model_name in RECORDS:
load_model(model_name)
with gr.Blocks(title='MS life-event classifier', analytics_enabled=False) as demo:
gr.Markdown('# MS life-event classifier\nPaste one phrase and choose a classifier. Each prediction returns one event category.')
phrase = gr.Textbox(label='Phrase', lines=4)
choice = gr.Dropdown(list(CHOICES), value=list(CHOICES)[0], label='Classifier')
button = gr.Button('Classify', variant='primary')
label = gr.Textbox(label='Assigned category', interactive=False)
scores = gr.Label(label='Top category scores', num_top_classes=5)
gr.Markdown('Scores are diagnostic; they do not guarantee correctness. Long inputs are truncated to each model’s training token limit.')
button.click(classify, [phrase, choice], [label, scores], api_name='predict')
demo.queue(default_concurrency_limit=1, max_size=16)
if __name__ == '__main__':
demo.launch()