File size: 6,145 Bytes
e1b76ea
 
 
ab04b85
 
 
 
 
 
 
 
 
 
e1b76ea
ab04b85
e1b76ea
ab04b85
e1b76ea
 
 
 
 
 
ab04b85
 
e1b76ea
 
 
ab04b85
 
 
 
 
 
e1b76ea
ab04b85
dee67dc
ab04b85
 
 
e1b76ea
ab04b85
 
 
 
 
e1b76ea
ab04b85
 
dee67dc
e1b76ea
 
 
ab04b85
 
6180f4c
ab04b85
 
 
 
 
 
 
 
 
 
 
 
 
6180f4c
 
e1b76ea
 
ab04b85
 
 
 
 
 
 
 
 
6180f4c
ab04b85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e1b76ea
ab04b85
 
 
 
 
 
 
 
 
 
 
 
 
e1b76ea
 
 
 
ab04b85
 
 
 
e1b76ea
6180f4c
ab04b85
6180f4c
ab04b85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
KnowLedge Inference β€” inference.py

Requires transformers>=5.0.0.  Gemma 4 (E4B) is a multimodal model whose
architecture is Gemma4ForConditionalGeneration (added in transformers 5.x).

ZeroGPU pattern
---------------
* Model loaded on CPU at startup with torch_dtype=torch.bfloat16 (~8 GB RAM).
* Each @spaces.GPU() call gets a real CUDA device; we move model β†’ cuda there,
  run generation, then move back β†’ cpu and empty the cache so the next request
  can reuse the shared GPU slot.
* health_check() is lightweight β€” it never triggers model download.
"""
import base64, io, logging, sys, re

logger = logging.getLogger(__name__)

try:
    import spaces
except ModuleNotFoundError:
    class _SpacesStub:
        @staticmethod
        def GPU(duration: int = 60):
            def decorator(fn): return fn
            return decorator
    spaces = _SpacesStub()

_import_error = None
HAS_TRANSFORMERS = False
Gemma4Processor = None
AutoModelForCausalLM = None
torch = None

try:
    from transformers import Gemma4Processor, AutoModelForCausalLM
    import torch
    HAS_TRANSFORMERS = True
except Exception as _e:
    _import_error = f"{type(_e).__name__}: {_e}"

try:
    from PIL import Image
    HAS_PIL = True
except ImportError:
    HAS_PIL = False

MODEL_REPO = "unsloth/gemma-4-E4B-it"
_processor = None
_model = None


def load_model():
    """Load model on CPU with bfloat16 (lazy, once only)."""
    global _processor, _model
    if _model is not None:
        return _processor, _model
    if not HAS_TRANSFORMERS:
        raise RuntimeError(f"transformers import failed: {_import_error}")
    logger.info("Loading %s on CPU (bfloat16)…", MODEL_REPO)
    _processor = Gemma4Processor.from_pretrained(MODEL_REPO)
    _model = AutoModelForCausalLM.from_pretrained(
        MODEL_REPO,
        torch_dtype=torch.bfloat16,   # ~8 GB β€” NOT dtype= which is silently ignored
        low_cpu_mem_usage=True,        # stream weights, lower peak RAM
    )
    _model.eval()
    logger.info("Model loaded. Device: %s", next(_model.parameters()).device)
    return _processor, _model


@spaces.GPU(duration=90)
def generate_text(model_name: str, prompt: str, max_new_tokens: int = 512) -> str:
    processor, model = load_model()
    device = "cuda" if torch.cuda.is_available() else "cpu"
    logger.info("generate_text: device=%s cuda_available=%s", device, torch.cuda.is_available())
    model.to(device)
    try:
        messages = [
            {"role": "system", "content": "You are a helpful educational assistant."},
            {"role": "user",   "content": prompt},
        ]
        try:
            text = processor.apply_chat_template(
                messages, tokenize=False, add_generation_prompt=True, enable_thinking=False
            )
            inputs = processor(text=text, return_tensors="pt").to(device)
        except TypeError:
            text = processor.apply_chat_template(
                messages, tokenize=False, add_generation_prompt=True
            )
            inputs = processor(text, return_tensors="pt").to(device)

        input_len = inputs["input_ids"].shape[-1]
        eos = (
            processor.tokenizer.eos_token_id
            if hasattr(processor, "tokenizer")
            else processor.eos_token_id
        )
        with torch.no_grad():
            outputs = model.generate(
                **inputs,
                max_new_tokens=max_new_tokens,
                temperature=1.0, top_p=0.95, top_k=64, do_sample=True,
                pad_token_id=eos,
            )
        raw = processor.decode(outputs[0][input_len:], skip_special_tokens=True)
        return re.sub(r"<think>.*?</think>", "", raw, flags=re.DOTALL).strip()
    finally:
        model.to("cpu")
        if device == "cuda" and torch is not None:
            torch.cuda.empty_cache()


@spaces.GPU(duration=120)
def generate_with_image(prompt: str, image_base64: str, max_new_tokens: int = 512) -> str:
    processor, model = load_model()
    device = "cuda" if torch.cuda.is_available() else "cpu"
    logger.info("generate_with_image: device=%s cuda_available=%s", device, torch.cuda.is_available())
    model.to(device)
    try:
        try:
            image = Image.open(io.BytesIO(base64.b64decode(image_base64))).convert("RGB")
        except Exception:
            return generate_text("e4b", prompt, max_new_tokens)
        try:
            messages = [{"role": "user", "content": [
                {"type": "image", "image": image},
                {"type": "text",  "text": prompt},
            ]}]
            inputs = processor.apply_chat_template(
                messages, tokenize=True, return_dict=True,
                return_tensors="pt", add_generation_prompt=True,
            )
            inputs = {k: v.to(device) for k, v in inputs.items()}
        except Exception:
            return generate_text("e4b", prompt, max_new_tokens)
        input_len = inputs["input_ids"].shape[-1]
        with torch.no_grad():
            outputs = model.generate(
                **inputs, max_new_tokens=max_new_tokens,
                temperature=1.0, top_p=0.95, top_k=64, do_sample=True,
            )
        raw = processor.decode(outputs[0][input_len:], skip_special_tokens=True)
        return re.sub(r"<think>.*?</think>", "", raw, flags=re.DOTALL).strip()
    finally:
        model.to("cpu")
        if device == "cuda" and torch is not None:
            torch.cuda.empty_cache()


def health_check() -> dict:
    """Lightweight health check β€” does NOT trigger model download."""
    import transformers
    has_cuda = torch.cuda.is_available() if torch is not None else False
    return {
        "backend": "transformers",
        "transformers_version": transformers.__version__,
        "model_repo": MODEL_REPO,
        "has_transformers": HAS_TRANSFORMERS,
        "import_error": _import_error,
        "python": sys.version,
        "cuda_available": has_cuda,
        "model_loaded": _model is not None,
        "status": "ok" if HAS_TRANSFORMERS else "error",
        "device": str(next(_model.parameters()).device) if _model is not None else "not_loaded_yet",
    }