ds5110-image-captioning / app_integration.py
natalieseah's picture
Update app_integration.py
33aa9ef verified
Raw
History Blame Contribute Delete
4.05 kB
import time
import hashlib
import spaces
import torch
from PIL import Image
from transformers import BlipProcessor, BlipForConditionalGeneration
MODEL_VARIANTS = {
"base": "Salesforce/blip-image-captioning-base",
"large": "Salesforce/blip-image-captioning-large",
}
_loaded_models = {} # model_name -> (processor, model), kept on CPU
caption_cache = {} # cache_key -> result dict
def _load_model(model_variant):
"""Load a BLIP variant once (on CPU) and reuse it on later requests."""
if model_variant not in MODEL_VARIANTS:
raise ValueError(f"model_variant must be one of {list(MODEL_VARIANTS)}")
model_name = MODEL_VARIANTS[model_variant]
start_load = time.time()
if model_name not in _loaded_models:
processor = BlipProcessor.from_pretrained(model_name)
model = BlipForConditionalGeneration.from_pretrained(model_name)
model.eval()
_loaded_models[model_name] = (processor, model)
load_time = time.time() - start_load # ~0 on every request after the first
processor, model = _loaded_models[model_name]
return processor, model, load_time, model_name
@spaces.GPU
def run_inference(model, inputs):
device = "cuda" if torch.cuda.is_available() else "cpu"
model = model.to(device)
inputs = {k: v.to(device) for k, v in inputs.items()}
with torch.no_grad():
output = model.generate(**inputs)
return output.to("cpu")
def _cache_key(model_variant, image):
# Stable across process restarts (unlike the built-in hash(), which is
# salted per process) and includes the variant so Base/Large cache apart.
digest = hashlib.sha256(image.tobytes()).hexdigest()
return f"{model_variant}:{digest}"
def generate_caption(image, model_variant="base", use_cache=True):
start_total = time.time()
# ---- CPU: preprocessing ----
start_preprocess = time.time()
if not isinstance(image, Image.Image):
image = Image.fromarray(image)
image = image.convert("RGB")
key = _cache_key(model_variant, image)
preprocess_time = time.time() - start_preprocess
# ---- CPU: cache check. On a hit, the GPU is NEVER allocated. ----
if use_cache and key in caption_cache:
cached = caption_cache[key].copy()
cached["cache_hit"] = True
cached["total_time"] = round(time.time() - start_total, 3)
cached["preprocess_time"] = round(preprocess_time, 3)
cached["model_load_time"] = 0.0
cached["inference_time"] = 0.0
cached["postprocess_time"] = 0.0
cached["approx_gpu_time"] = 0.0
return cached
# ---- CPU: load (or reuse) the model and tokenize the image ----
processor, model, load_time, model_name = _load_model(model_variant)
inputs = processor(image, return_tensors="pt")
start_inference = time.time()
output = run_inference(model, inputs)
inference_time = time.time() - start_inference
# ---- CPU: postprocessing ----
start_postprocess = time.time()
caption = processor.decode(output[0], skip_special_tokens=True)
postprocess_time = time.time() - start_postprocess
total_time = time.time() - start_total
result = {
"caption": caption,
"total_time": round(total_time, 3),
"preprocess_time": round(preprocess_time, 3),
"model_load_time": round(load_time, 3),
"inference_time": round(inference_time, 3),
"postprocess_time": round(postprocess_time, 3),
# NOTE: inference_time is measured around run_inference from the main
# process, so it also includes the CPU<->GPU transfer and the ZeroGPU
# scheduling/fork overhead. Treat approx_gpu_time as an UPPER BOUND on
# true GPU time, not a precise GPU measurement.
"approx_gpu_time": round(inference_time, 3),
"cache_hit": False,
"model_variant": model_variant,
"model": model_name,
"use_cache": use_cache,
}
if use_cache:
caption_cache[key] = result.copy()
return result
_load_model("base")