Spaces:
Build error
Build error
File size: 6,737 Bytes
5c2e981 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | """
IBM Granite 4.1 3B Instruct model loader and generation utilities.
"""
from __future__ import annotations
import os
from typing import Optional
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline, Pipeline
from config import cfg
from logging_config import get_logger, setup_logging
from utils import Timer
setup_logging(log_dir=cfg.app.log_dir)
logger = get_logger(__name__)
class GraniteModelLoader:
"""
Loads IBM Granite 4.1 3B Instruct and exposes a text-generation pipeline.
"""
def __init__(self) -> None:
self.pipe: Optional[Pipeline] = None
self.tokenizer = None
self.model_id = cfg.model.model_id
self._is_loaded = False
# ββ Loading βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def load(self, token: Optional[str] = None) -> None:
if self._is_loaded:
logger.info("Model already loaded; skipping.")
return
hf_token = token or cfg.hf_token
if not hf_token:
raise EnvironmentError(
"Hugging Face token is required to load gated models. "
"Set the HF_TOKEN environment variable."
)
logger.info("Loading tokenizer for '%s'β¦", self.model_id)
with Timer() as t:
self.tokenizer = AutoTokenizer.from_pretrained(
self.model_id,
token=hf_token,
)
logger.info("Tokenizer loaded in %s.", t)
dtype = self._resolve_dtype()
logger.info("Loading model with dtype=%sβ¦", dtype)
with Timer() as t:
model = AutoModelForCausalLM.from_pretrained(
self.model_id,
token=hf_token,
torch_dtype=dtype,
device_map=cfg.model.device_map,
low_cpu_mem_usage=True,
)
logger.info("Model loaded in %s.", t)
self.pipe = pipeline(
"text-generation",
model=model,
tokenizer=self.tokenizer,
return_full_text=False,
)
self._is_loaded = True
logger.info("Generation pipeline ready.")
# ββ Generation ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def generate(
self,
prompt: str,
max_new_tokens: Optional[int] = None,
temperature: Optional[float] = None,
top_p: Optional[float] = None,
repetition_penalty: Optional[float] = None,
) -> str:
if not self._is_loaded or self.pipe is None:
raise RuntimeError("Model is not loaded. Call load() first.")
params = {
"max_new_tokens": max_new_tokens or cfg.model.max_new_tokens,
"temperature": temperature or cfg.model.temperature,
"top_p": top_p or cfg.model.top_p,
"repetition_penalty": repetition_penalty or cfg.model.repetition_penalty,
"do_sample": cfg.model.do_sample,
}
logger.info(
"Generating response (max_new_tokens=%d, temperature=%.2f)β¦",
params["max_new_tokens"],
params["temperature"],
)
with Timer() as t:
output = self.pipe(prompt, **params)
text = output[0]["generated_text"].strip()
logger.info("Response generated in %s (%d chars).", t, len(text))
return text
# ββ Prompt construction βββββββββββββββββββββββββββββββββββββββββββββββββββ
@staticmethod
def build_prompt(
query: str,
retrieved_context: str,
conversation_history: str = "",
) -> str:
"""
Builds a retrieval-first prompt for IBM Granite chat models.
Uses the model's expected <|...|> chat tokens.
"""
system_msg = (
"You are a professional university admissions assistant. "
"Your role is to help prospective students with accurate information "
"about admissions requirements, programs, fees, scholarships, deadlines, "
"and related topics.\n\n"
"STRICT RULES:\n"
"1. Answer ONLY using the information provided in the CONTEXT section below.\n"
"2. If the answer is not present in the context, respond: "
"'I'm sorry, I couldn't find that information in the university knowledge base. "
"Please contact the admissions office directly for assistance.'\n"
"3. NEVER invent, guess, or extrapolate admissions policies, fees, or deadlines.\n"
"4. Cite the source document name when possible.\n"
"5. Keep answers concise, accurate, and professionally toned.\n"
"6. If the query is off-topic (not related to university admissions), "
"politely redirect the user."
)
context_block = (
f"CONTEXT (retrieved from university knowledge base):\n"
f"{'=' * 60}\n"
f"{retrieved_context}\n"
f"{'=' * 60}"
)
history_block = ""
if conversation_history:
history_block = (
f"\nCONVERSATION HISTORY:\n{conversation_history}\n"
)
user_content = (
f"{context_block}\n"
f"{history_block}\n"
f"QUESTION: {query}"
)
# IBM Granite 4.1 uses the standard HuggingFace chat template;
# we format manually for pipeline compatibility.
prompt = (
f"<|start_of_role|>system<|end_of_role|>{system_msg}<|end_of_text|>\n"
f"<|start_of_role|>user<|end_of_role|>{user_content}<|end_of_text|>\n"
f"<|start_of_role|>assistant<|end_of_role|>"
)
return prompt
# ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@staticmethod
def _resolve_dtype() -> torch.dtype:
if torch.cuda.is_available():
return torch.bfloat16
if torch.backends.mps.is_available():
return torch.float16
return torch.float32
@property
def is_loaded(self) -> bool:
return self._is_loaded
@property
def model_name(self) -> str:
return self.model_id
|