Arnic commited on
Commit
c1272c5
·
1 Parent(s): 0e6a725

refactor: ZeroGPU-compatible — spaces first import, @spaces.GPU on generate(), deferred torch/transformers

Browse files
Files changed (3) hide show
  1. app.py +41 -51
  2. requirements.txt +10 -10
  3. src/generation.py +16 -5
app.py CHANGED
@@ -1,25 +1,21 @@
1
- """Gradio Chat UI for Aethron Portfolio Agent — CPU mode on ZeroGPU."""
 
 
 
2
 
3
  import os
4
  import sys
 
5
 
6
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
7
 
8
- import gradio as gr
9
-
10
- try:
11
- import spaces
12
- import spaces.zero
13
- from spaces.zero import torch as spaces_torch
14
- spaces_torch.patch()
15
- except ImportError:
16
- spaces = None
17
 
18
  print("=" * 55)
19
- print("AETHRON PORTFOLIO AGENT — CPU Mode")
20
  print("=" * 55)
21
 
22
- # Lazy-loaded inside @spaces.GPU context to avoid CUDA init in main process
23
  _pipeline = None
24
 
25
 
@@ -27,52 +23,45 @@ def get_pipeline():
27
  global _pipeline
28
  if _pipeline is None:
29
  from rag_pipeline import AethronPipeline
30
- print("Loading pipeline on CPU (first query only)...")
31
  _pipeline = AethronPipeline(
32
  build_index=not os.path.exists("data/index/faiss.index")
33
  )
34
  return _pipeline
35
 
36
 
37
- if spaces is not None:
38
- @spaces.GPU(duration=120)
39
- def respond(message, history):
40
- if not message or not message.strip():
41
- return history
42
- pipeline = get_pipeline()
43
- result = pipeline.query(message.strip())
44
- response = result["answer"]
45
- if result["sources"]:
46
- source_names = []
47
- for s in result["sources"][:3]:
48
- name = s["section"].replace("_", " ").title()
49
- if "Summary" in name:
50
- name = name.replace("Summary", "(Summary)")
51
- source_names.append(name)
52
- response += "\n\n**Sources:** " + " | ".join(source_names)
53
- history.append({"role": "user", "content": message})
54
- history.append({"role": "assistant", "content": response})
55
- return history
56
- else:
57
  from rag_pipeline import AethronPipeline
 
 
 
 
 
 
 
 
 
58
 
59
- def respond(message, history):
60
- if not message or not message.strip():
61
- return history
62
- pipeline = get_pipeline()
63
- result = pipeline.query(message.strip())
64
- response = result["answer"]
65
- if result["sources"]:
66
- source_names = []
67
- for s in result["sources"][:3]:
68
- name = s["section"].replace("_", " ").title()
69
- if "Summary" in name:
70
- name = name.replace("Summary", "(Summary)")
71
- source_names.append(name)
72
- response += "\n\n**Sources:** " + " | ".join(source_names)
73
- history.append({"role": "user", "content": message})
74
- history.append({"role": "assistant", "content": response})
75
  return history
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
 
78
  CUSTOM_CSS = """
@@ -82,7 +71,7 @@ CUSTOM_CSS = """
82
  .header p { color: #4a4a6a; }
83
  """
84
 
85
- with gr.Blocks(css=CUSTOM_CSS, title="Aethron | Chat with Arash's Portfolio") as demo:
86
 
87
  gr.HTML("""
88
  <div class="header">
@@ -98,7 +87,7 @@ with gr.Blocks(css=CUSTOM_CSS, title="Aethron | Chat with Arash's Portfolio") as
98
  </div>
99
  """)
100
 
101
- chatbot = gr.Chatbot(type="messages", height=500)
102
 
103
  with gr.Row():
104
  msg_input = gr.Textbox(
@@ -151,4 +140,5 @@ if __name__ == "__main__":
151
  share=False,
152
  show_error=True,
153
  ssr_mode=False,
 
154
  )
 
1
+ """Gradio Chat UI for Aethron Portfolio Agent — ZeroGPU-compatible."""
2
+
3
+ # ── 1. spaces MUST be imported first, before torch/transformers/faiss ──
4
+ import spaces # noqa: E402 — intentionally first
5
 
6
  import os
7
  import sys
8
+ from threading import Thread
9
 
10
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
11
 
12
+ import gradio as gr # noqa: E402 — after spaces
 
 
 
 
 
 
 
 
13
 
14
  print("=" * 55)
15
+ print("AETHRON PORTFOLIO AGENT — GPU Mode (ZeroGPU)")
16
  print("=" * 55)
17
 
18
+ # Pipeline loaded lazily inside @spaces.GPU context avoids CUDA init in main process
19
  _pipeline = None
20
 
21
 
 
23
  global _pipeline
24
  if _pipeline is None:
25
  from rag_pipeline import AethronPipeline
26
+ print("Loading pipeline (first query)...")
27
  _pipeline = AethronPipeline(
28
  build_index=not os.path.exists("data/index/faiss.index")
29
  )
30
  return _pipeline
31
 
32
 
33
+ def _preload():
34
+ global _pipeline
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  from rag_pipeline import AethronPipeline
36
+ print("Preloading pipeline in background...")
37
+ _pipeline = AethronPipeline(
38
+ build_index=not os.path.exists("data/index/faiss.index")
39
+ )
40
+ print("Pipeline preloaded and ready.")
41
+
42
+
43
+ Thread(target=_preload, daemon=True).start()
44
+
45
 
46
+ @spaces.GPU(duration=60)
47
+ def respond(message, history):
48
+ """Handle chat — runs inside GPU context so torch.cuda patches apply."""
49
+ if not message or not message.strip():
 
 
 
 
 
 
 
 
 
 
 
 
50
  return history
51
+ pipeline = get_pipeline()
52
+ result = pipeline.query(message.strip())
53
+ response = result["answer"]
54
+ if result["sources"]:
55
+ source_names = []
56
+ for s in result["sources"][:3]:
57
+ name = s["section"].replace("_", " ").title()
58
+ if "Summary" in name:
59
+ name = name.replace("Summary", "(Summary)")
60
+ source_names.append(name)
61
+ response += "\n\n**Sources:** " + " | ".join(source_names)
62
+ history.append({"role": "user", "content": message})
63
+ history.append({"role": "assistant", "content": response})
64
+ return history
65
 
66
 
67
  CUSTOM_CSS = """
 
71
  .header p { color: #4a4a6a; }
72
  """
73
 
74
+ with gr.Blocks(title="Aethron | Chat with Arash's Portfolio") as demo:
75
 
76
  gr.HTML("""
77
  <div class="header">
 
87
  </div>
88
  """)
89
 
90
+ chatbot = gr.Chatbot(height=500)
91
 
92
  with gr.Row():
93
  msg_input = gr.Textbox(
 
140
  share=False,
141
  show_error=True,
142
  ssr_mode=False,
143
+ css=CUSTOM_CSS,
144
  )
requirements.txt CHANGED
@@ -1,10 +1,10 @@
1
- gradio>=4.44.0
2
- transformers>=4.40.0
3
- sentence-transformers>=3.0.0
4
- faiss-cpu>=1.8.0
5
- PyMuPDF>=1.24.0
6
- numpy>=1.24.0
7
- torch>=2.3.0
8
- huggingface-hub>=0.24.0
9
- accelerate>=0.30.0
10
- spaces>=0.30.0
 
1
+ spaces
2
+ torch
3
+ transformers
4
+ sentence-transformers
5
+ faiss-cpu
6
+ gradio
7
+ numpy
8
+ huggingface-hub
9
+ accelerate
10
+ PyMuPDF
src/generation.py CHANGED
@@ -1,11 +1,14 @@
1
- """SLM generation with transformers."""
 
 
 
 
 
 
2
 
3
  import os
4
  from typing import List, Dict
5
 
6
- import torch
7
- from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
8
-
9
  from config import LLM_MODEL_ID, LLM_MAX_NEW_TOKENS, LLM_TEMPERATURE, LLM_DO_SAMPLE
10
 
11
 
@@ -27,10 +30,15 @@ RULES:
27
  TECHNICAL_PROMPT = "You are speaking with a technical partner or peer. Emphasize: architecture decisions, implementation details, repository names, and design philosophy. Be precise and technically deep."
28
 
29
  def __init__(self):
 
 
 
 
30
  print(f"Loading SLM: {LLM_MODEL_ID}...")
31
 
32
  self.tokenizer = AutoTokenizer.from_pretrained(LLM_MODEL_ID)
33
 
 
34
  self.model = AutoModelForCausalLM.from_pretrained(
35
  LLM_MODEL_ID, dtype="auto", device_map="cpu"
36
  )
@@ -48,8 +56,11 @@ RULES:
48
 
49
  print("SLM loaded successfully")
50
 
 
51
  def generate(self, query: str, chunks: List[Dict], persona: str = "general") -> Dict:
52
- """Generate response from retrieved context."""
 
 
53
  context_text = "\n\n".join([
54
  f"[Source: {c['section_type']} - {c['title']}]\n{c['text']}"
55
  for c in chunks
 
1
+ """SLM generation with transformers — ZeroGPU-compatible."""
2
+
3
+ # ── 1. spaces MUST be imported first, before torch/transformers ──
4
+ try:
5
+ import spaces # noqa: E402 — intentionally first
6
+ except ImportError:
7
+ spaces = None
8
 
9
  import os
10
  from typing import List, Dict
11
 
 
 
 
12
  from config import LLM_MODEL_ID, LLM_MAX_NEW_TOKENS, LLM_TEMPERATURE, LLM_DO_SAMPLE
13
 
14
 
 
30
  TECHNICAL_PROMPT = "You are speaking with a technical partner or peer. Emphasize: architecture decisions, implementation details, repository names, and design philosophy. Be precise and technically deep."
31
 
32
  def __init__(self):
33
+ # Deferred imports — torch/transformers loaded inside @spaces.GPU context
34
+ import torch
35
+ from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
36
+
37
  print(f"Loading SLM: {LLM_MODEL_ID}...")
38
 
39
  self.tokenizer = AutoTokenizer.from_pretrained(LLM_MODEL_ID)
40
 
41
+ # Model stays on CPU at init; @spaces.GPU moves it to GPU at inference time
42
  self.model = AutoModelForCausalLM.from_pretrained(
43
  LLM_MODEL_ID, dtype="auto", device_map="cpu"
44
  )
 
56
 
57
  print("SLM loaded successfully")
58
 
59
+ @spaces.GPU(duration=60)
60
  def generate(self, query: str, chunks: List[Dict], persona: str = "general") -> Dict:
61
+ """Generate response tokenization, forward pass, and generation inside GPU context."""
62
+ import torch # noqa: F811 — re-import safe inside GPU context
63
+
64
  context_text = "\n\n".join([
65
  f"[Source: {c['section_type']} - {c['title']}]\n{c['text']}"
66
  for c in chunks