File size: 3,536 Bytes
b1aba72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51e9502
b1aba72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
Model loading for HuggingFace Spaces ZeroGPU deployment.
"""
import torch
from transformers import (
    AutoTokenizer,
    AutoModelForCausalLM,
    BitsAndBytesConfig,
    pipeline,
)
from langchain_huggingface import HuggingFaceEmbeddings

from config import MODEL_ID, EMBEDDING_MODEL_ID, MAX_NEW_TOKENS
from logging_config import get_logger

logger = get_logger(__name__)

# --- Tokenizer -------------------------------
logger.info(f"Loading tokenizer: {MODEL_ID}")
tokenizer = AutoTokenizer.from_pretrained(
    MODEL_ID,
    trust_remote_code=True, # Qwen has a custom tokenizer class
    clean_up_tokenization_spaces=False, # BPE tokenizers must NOT strip spaces before punctuation — it
                                        # corrupts output. This flag is meant for WordPiece (BERT) only.
)

# --- Quantization config -----------------------------------
# 4-bit NF4 (Normal Float 4-bit) is the most accurate 4-bit format for LLM weights.
# double_quant saves an additional ~0.4 bits per parameter on the quantization constants themselves.
quantization_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
)

# --- Model --------------------------------------------------
# device_map={"": 0} pins the ENTIRE model onto GPU slot 0 at load time.
# This is the module-scope CUDA placement ZeroGPU requires — the
# `spaces` package (imported first in app.py) intercepts this and
# handles it correctly even though no physical GPU is attached yet
# during this import.
logger.info(f"Loading model (4-bit NF4 quantized) — device_map={{'':0}}…")
model = AutoModelForCausalLM.from_pretrained(
    MODEL_ID,
    quantization_config=quantization_config,
    device_map={"": 0}, # Module-scope GPU placement (see docstring)
    low_cpu_mem_usage=True, # Streams weights from disk, avoids RAM spike
    trust_remote_code=True,
)
model.eval() # Disable dropout — we're doing inference only

# NOTE: No torch.compile() here.
# ZeroGPU does not support torch.compile. Calling it would either
# silently fail to provide speedup or raise an error when the first
# @spaces.GPU-decorated call tries to actually run on a real GPU worker.

# --- Text generation pipeline --------------------------------
# The pipeline object itself does not run any CUDA kernels at
# construction time — it only wraps tokenizer + model + generation
# config. Building it here at module scope is safe. The ACTUAL
# inference call (invoking this pipeline) happens inside the
# @spaces.GPU-decorated function in generation.py — never here.
logger.info("Building text generation pipeline…")
text_gen_pipeline = pipeline(
    task="text-generation",
    model=model,
    tokenizer=tokenizer,
    max_new_tokens=MAX_NEW_TOKENS,
    max_length=None, # Override model's default max_length (often 20) to silence the HF conflict warning
    do_sample=False, # Greedy decoding: deterministic, faithful to context
    return_full_text=False, # Return only newly generated tokens, not the prompt
)

# --- Embedding model ---------------------------------------------
# MiniLM stays on CPU — it's fast enough there and doesn't need the
# GPU slot, which should be reserved for the LLM's generation calls.
logger.info("Loading embedding model…")
embedding_model = HuggingFaceEmbeddings(
    model_name=EMBEDDING_MODEL_ID,
    model_kwargs={"device": "cpu"},
    encode_kwargs={"normalize_embeddings": True},
)

logger.info("All models ready.")