Corin1998 commited on
Commit
ccb2915
·
verified ·
1 Parent(s): 448f25c

Update irpr/deps.py

Browse files
Files changed (1) hide show
  1. irpr/deps.py +37 -51
irpr/deps.py CHANGED
@@ -1,9 +1,8 @@
1
  import os
2
  import pickle
3
  import numpy as np
4
- import torch
5
 
6
- # ---- キャッシュ/データディレクトリを先に用意(import 時でも安全) -----------------
7
  CACHE_DIR = os.environ.get("HF_HOME") or "/tmp/hf-cache"
8
  for k in ["HF_HOME", "TRANSFORMERS_CACHE", "SENTENCE_TRANSFORMERS_HOME", "TORCH_HOME"]:
9
  os.environ.setdefault(k, CACHE_DIR)
@@ -14,38 +13,34 @@ from irpr.config import settings
14
  DATA_DIR = settings.DATA_DIR or "data"
15
  os.makedirs(DATA_DIR, exist_ok=True)
16
 
17
- # 永続化パス(NumPyベースの簡易インデックス)
18
  VEC_PATH = os.path.join(DATA_DIR, "vectors.npy")
19
  STORE_PATH = os.path.join(DATA_DIR, "store.pkl")
20
 
21
- # ---- 遅延ロード用のシングルトン ---------------------------------------------------
22
  from typing import Optional, List, Dict
23
- from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
24
- from sentence_transformers import SentenceTransformer
25
 
26
- _EMB: Optional[SentenceTransformer] = None
27
  _EMB_DIM: Optional[int] = None
 
 
 
28
 
29
- _TOK = None
30
- _MODEL = None
31
- _GEN = None
32
 
33
- _VECTORS: Optional[np.ndarray] = None # shape [N, D], float32, L2正規化済み
34
- _STORE: Optional[List[Dict]] = None # メタ情報の配列
35
-
36
- # ---- 低レベルユーティリティ -------------------------------------------------------
37
  def _load_index():
38
- """ベクトルとメタ情報を遅延ロード"""
39
- global _VECTORS, _STORE, _EMB_DIM
40
  if _VECTORS is None:
41
  if os.path.exists(VEC_PATH):
42
  try:
43
- vec = np.load(VEC_PATH)
44
- _VECTORS = vec.astype(np.float32, copy=False)
45
  except Exception:
46
- _VECTORS = np.empty((0, _EMB_DIM or 0), dtype=np.float32)
47
  else:
48
- _VECTORS = np.empty((0, _EMB_DIM or 0), dtype=np.float32)
49
  if _STORE is None:
50
  if os.path.exists(STORE_PATH):
51
  try:
@@ -61,31 +56,35 @@ def _save_index():
61
  global _VECTORS, _STORE
62
  if _VECTORS is None or _STORE is None:
63
  return
64
- # np.save はディレクトリが必要
65
  os.makedirs(os.path.dirname(VEC_PATH), exist_ok=True)
66
  np.save(VEC_PATH, _VECTORS)
67
  with open(STORE_PATH, "wb") as f:
68
  pickle.dump(_STORE, f)
69
 
70
- # ---- モデル遅延ロード -------------------------------------------------------------
71
- def _get_emb_model() -> SentenceTransformer:
72
- """SentenceTransformer を初回呼び出し時にロード"""
73
  global _EMB, _EMB_DIM
74
  if _EMB is None:
 
75
  model_name = settings.EMB_MODEL or "intfloat/multilingual-e5-base"
76
  _EMB = SentenceTransformer(model_name, cache_folder=CACHE_DIR)
77
  _EMB_DIM = _EMB.get_sentence_embedding_dimension()
78
- # 既存ベクトル配列のD次元を確定させる
 
79
  _load_index()
80
- if _VECTORS is not None and _VECTORS.size == 0:
81
- # 空なら次元を確定
82
- _set_vectors(np.empty((0, _EMB_DIM), dtype=np.float32))
83
  return _EMB
84
 
85
  def _get_gen_pipeline():
86
- """text-generation pipeline を初回呼び出し時にロード"""
87
  global _TOK, _MODEL, _GEN
88
  if _GEN is None:
 
 
 
89
  gen_name = settings.GEN_MODEL or "Qwen/Qwen2.5-3B-Instruct"
90
  _TOK = AutoTokenizer.from_pretrained(gen_name, cache_dir=CACHE_DIR)
91
  _MODEL = AutoModelForCausalLM.from_pretrained(
@@ -93,12 +92,12 @@ def _get_gen_pipeline():
93
  cache_dir=CACHE_DIR,
94
  torch_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,
95
  device_map="auto",
96
- low_cpu_mem_usage=True
97
  )
98
  _GEN = pipeline("text-generation", model=_MODEL, tokenizer=_TOK)
99
  return _GEN, _TOK
100
 
101
- # ---- ベクトル化と検索 -------------------------------------------------------------
102
  def embed_texts(texts: List[str]) -> np.ndarray:
103
  emb = _get_emb_model()
104
  v = emb.encode(
@@ -109,11 +108,6 @@ def embed_texts(texts: List[str]) -> np.ndarray:
109
  )
110
  return v.astype(np.float32, copy=False)
111
 
112
- def _set_vectors(arr: np.ndarray):
113
- """内部ベクトル配列を安全に差し替え"""
114
- global _VECTORS
115
- _VECTORS = arr.astype(np.float32, copy=False)
116
-
117
  def add_to_index(records: List[Dict]):
118
  """
119
  records: [{"text":..., "source_url":..., "title":..., "doc_id":..., "chunk_id":...}]
@@ -123,27 +117,22 @@ def add_to_index(records: List[Dict]):
123
  _load_index()
124
  vecs = embed_texts([r["text"] for r in records]) # [M, D]
125
  global _VECTORS, _STORE
126
- if _VECTORS is None or _VECTORS.size == 0:
127
- _set_vectors(vecs)
128
  else:
129
- _set_vectors(np.vstack([_VECTORS, vecs]))
130
  _STORE.extend(records)
131
  _save_index()
132
 
133
  def search(query: str, top_k=8):
134
- """
135
- 内積ベース(両ベクトル正規化済み→cos 類似)で上位K件を返す
136
- """
137
  _load_index()
138
- if _VECTORS is None or _VECTORS.size == 0 or not _STORE:
139
  return []
140
-
141
- qv = embed_texts([query])[0] # [D]
142
- sims = _VECTORS @ qv # [N]
143
  top_k = min(top_k, sims.shape[0])
144
  idx = np.argpartition(-sims, top_k - 1)[:top_k]
145
  idx = idx[np.argsort(-sims[idx])]
146
-
147
  hits = []
148
  for i in idx.tolist():
149
  rec = _STORE[i].copy()
@@ -151,11 +140,8 @@ def search(query: str, top_k=8):
151
  hits.append(rec)
152
  return hits
153
 
154
- # ---- 生成ユーティリティ ----------------------------------------------------------
155
  def generate_chat(messages: list[dict], max_new_tokens=800, temperature=0.2) -> str:
156
- """
157
- messages: [{"role":"system"/"user"/"assistant", "content":"..."}]
158
- """
159
  gen, tok = _get_gen_pipeline()
160
  prompt = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
161
  out = gen(
 
1
  import os
2
  import pickle
3
  import numpy as np
 
4
 
5
+ # ========= キャッシュ/データディレクトリ(import時は作成と権限確認だけ) =========
6
  CACHE_DIR = os.environ.get("HF_HOME") or "/tmp/hf-cache"
7
  for k in ["HF_HOME", "TRANSFORMERS_CACHE", "SENTENCE_TRANSFORMERS_HOME", "TORCH_HOME"]:
8
  os.environ.setdefault(k, CACHE_DIR)
 
13
  DATA_DIR = settings.DATA_DIR or "data"
14
  os.makedirs(DATA_DIR, exist_ok=True)
15
 
 
16
  VEC_PATH = os.path.join(DATA_DIR, "vectors.npy")
17
  STORE_PATH = os.path.join(DATA_DIR, "store.pkl")
18
 
19
+ # ========= グローバル状態(import時にモデルは作らない!) =========
20
  from typing import Optional, List, Dict
 
 
21
 
22
+ _EMB = None # SentenceTransformer
23
  _EMB_DIM: Optional[int] = None
24
+ _TOK = None # AutoTokenizer
25
+ _MODEL = None # AutoModelForCausalLM
26
+ _GEN = None # pipeline("text-generation")
27
 
28
+ _VECTORS: Optional[np.ndarray] = None # shape [N, D] float32
29
+ _STORE: Optional[List[Dict]] = None
 
30
 
31
+ # ========= 永続インデックスの入出力 =========
 
 
 
32
  def _load_index():
33
+ """ベクトル/メタの遅延ロード(モデルは触らない)"""
34
+ global _VECTORS, _STORE
35
  if _VECTORS is None:
36
  if os.path.exists(VEC_PATH):
37
  try:
38
+ arr = np.load(VEC_PATH)
39
+ _VECTORS = arr.astype(np.float32, copy=False)
40
  except Exception:
41
+ _VECTORS = np.empty((0, 0), dtype=np.float32)
42
  else:
43
+ _VECTORS = np.empty((0, 0), dtype=np.float32)
44
  if _STORE is None:
45
  if os.path.exists(STORE_PATH):
46
  try:
 
56
  global _VECTORS, _STORE
57
  if _VECTORS is None or _STORE is None:
58
  return
 
59
  os.makedirs(os.path.dirname(VEC_PATH), exist_ok=True)
60
  np.save(VEC_PATH, _VECTORS)
61
  with open(STORE_PATH, "wb") as f:
62
  pickle.dump(_STORE, f)
63
 
64
+ # ========= モデル(初回呼び出し時にだけロード) =========
65
+ def _get_emb_model():
66
+ """SentenceTransformer を初めて必要になったときだけロード"""
67
  global _EMB, _EMB_DIM
68
  if _EMB is None:
69
+ from sentence_transformers import SentenceTransformer # ← ここで初めてimport
70
  model_name = settings.EMB_MODEL or "intfloat/multilingual-e5-base"
71
  _EMB = SentenceTransformer(model_name, cache_folder=CACHE_DIR)
72
  _EMB_DIM = _EMB.get_sentence_embedding_dimension()
73
+
74
+ # 既存ベクトル配列の次元整合
75
  _load_index()
76
+ global _VECTORS
77
+ if _VECTORS.size == 0 or (_VECTORS.ndim == 2 and _VECTORS.shape[1] != _EMB_DIM):
78
+ _VECTORS = np.empty((0, _EMB_DIM), dtype=np.float32)
79
  return _EMB
80
 
81
  def _get_gen_pipeline():
82
+ """text-generation pipeline を初回だけロード"""
83
  global _TOK, _MODEL, _GEN
84
  if _GEN is None:
85
+ from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline # ← 遅延import
86
+ import torch # ← 遅延import
87
+
88
  gen_name = settings.GEN_MODEL or "Qwen/Qwen2.5-3B-Instruct"
89
  _TOK = AutoTokenizer.from_pretrained(gen_name, cache_dir=CACHE_DIR)
90
  _MODEL = AutoModelForCausalLM.from_pretrained(
 
92
  cache_dir=CACHE_DIR,
93
  torch_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,
94
  device_map="auto",
95
+ low_cpu_mem_usage=True,
96
  )
97
  _GEN = pipeline("text-generation", model=_MODEL, tokenizer=_TOK)
98
  return _GEN, _TOK
99
 
100
+ # ========= 埋め込みと検索 =========
101
  def embed_texts(texts: List[str]) -> np.ndarray:
102
  emb = _get_emb_model()
103
  v = emb.encode(
 
108
  )
109
  return v.astype(np.float32, copy=False)
110
 
 
 
 
 
 
111
  def add_to_index(records: List[Dict]):
112
  """
113
  records: [{"text":..., "source_url":..., "title":..., "doc_id":..., "chunk_id":...}]
 
117
  _load_index()
118
  vecs = embed_texts([r["text"] for r in records]) # [M, D]
119
  global _VECTORS, _STORE
120
+ if _VECTORS.size == 0:
121
+ _VECTORS = vecs
122
  else:
123
+ _VECTORS = np.vstack([_VECTORS, vecs])
124
  _STORE.extend(records)
125
  _save_index()
126
 
127
  def search(query: str, top_k=8):
 
 
 
128
  _load_index()
129
+ if _VECTORS.size == 0 or not _STORE:
130
  return []
131
+ qv = embed_texts([query])[0]
132
+ sims = _VECTORS @ qv
 
133
  top_k = min(top_k, sims.shape[0])
134
  idx = np.argpartition(-sims, top_k - 1)[:top_k]
135
  idx = idx[np.argsort(-sims[idx])]
 
136
  hits = []
137
  for i in idx.tolist():
138
  rec = _STORE[i].copy()
 
140
  hits.append(rec)
141
  return hits
142
 
143
+ # ========= 生成ユーティリティ =========
144
  def generate_chat(messages: list[dict], max_new_tokens=800, temperature=0.2) -> str:
 
 
 
145
  gen, tok = _get_gen_pipeline()
146
  prompt = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
147
  out = gen(