charan-ml commited on
Commit
4c42b27
·
1 Parent(s): b3edcc3

Replace cloud inference with local Ollama

Browse files
Files changed (7) hide show
  1. README.md +0 -77
  2. app.py +0 -142
  3. deploy.sh +0 -72
  4. inference.py +0 -152
  5. requirements.txt +0 -10
  6. runtime.txt +0 -1
  7. scratch.py +0 -2
README.md DELETED
@@ -1,77 +0,0 @@
1
- ---
2
- title: KnowLedge Inference API
3
- emoji: 📖
4
- colorFrom: yellow
5
- colorTo: green
6
- sdk: gradio
7
- python_version: 3.10.16
8
- sdk_version: 4.44.1
9
- app_file: app.py
10
- pinned: true
11
- license: apache-2.0
12
- tags:
13
- - gemma
14
- - gemma-4
15
- - education
16
- - inference-api
17
- - knowledge
18
- - hackathon
19
- short_description: Gemma 4 GGUF inference backend for KnowLedge
20
- ---
21
-
22
- # KnowLedge Inference API
23
-
24
- Gemma 4 E2B + quantized E4B inference backend for the
25
- [KnowLedge](https://github.com/Charan-suresh/KnowLedge) learning ledger.
26
-
27
- Part of the Kaggle Gemma 4 Good Hackathon submission.
28
-
29
- ## Endpoints
30
-
31
- | Endpoint | Model | Used by |
32
- |---|---|---|
33
- | `/api/generate` | E2B or E4B | Scout (concept tagging), Sage (Socratic dialogue) |
34
- | `/api/generate_vision` | E4B vision | Lens (handwriting verification) |
35
- | `/api/health` | - | FastAPI health check |
36
-
37
- ## Architecture
38
-
39
- This Space uses **Gradio + FastAPI** together via `gr.mount_gradio_app`:
40
-
41
- - The **Gradio** wrapper satisfies HF Spaces `sdk: gradio` requirements so the Space is hosted on port 7860 without a custom Dockerfile.
42
- - The **FastAPI** layer exposes the REST endpoints the Render backend calls via `httpx`.
43
- - Inference runs entirely on **CPU** on the standard free HF Spaces tier — no ZeroGPU required.
44
-
45
- | Model | Backend | Notes |
46
- |---|---|---|
47
- | E4B | `llama-cpp-python` GGUF | `UD-IQ3_XXS` quant, ~2.5 GB RAM, fast-ish on CPU |
48
- | E2B | HuggingFace `transformers` | float32, ~8 GB RAM, slower on CPU |
49
-
50
- Override the E4B GGUF file with the `GEMMA_E4B_GGUF_FILE` Space secret if you want a different quantisation.
51
-
52
- ## Not for direct use
53
-
54
- This Space is called programmatically by the KnowLedge FastAPI backend
55
- deployed on Render. It is not a chat interface.
56
-
57
- ## Deployment note
58
-
59
- Deploy the files inside `hf_space/` to a separate Hugging Face Space repository.
60
- Do **not** deploy the root `app.py` here — that file is the standalone Gradio demo
61
- that connects back to the Render backend, not the inference backend itself.
62
-
63
- ## Required setup
64
-
65
- 1. **No special hardware needed** — leave the Space on the default free CPU tier.
66
- 2. Set the **`HF_TOKEN`** Space secret (Settings → Repository secrets) to a token that has accepted the Gemma 4 terms on HuggingFace. This is required for `google/gemma-4-e2b-it` (gated model) to download at runtime.
67
- 3. On **Render**, set:
68
- - `INFERENCE_BACKEND=hf_space`
69
- - `HF_SPACE_URL=charan-ml/knowledge-inference`
70
-
71
- ### Optional env vars (Space secrets)
72
-
73
- | Variable | Default | Effect |
74
- |---|---|---|
75
- | `GEMMA_E4B_GGUF_FILE` | `gemma-4-E4B-it-UD-IQ3_XXS.gguf` | Switch to a different GGUF quant |
76
- | `GEMMA_E4B_CTX` | `4096` | llama-cpp context window |
77
- | `GEMMA_E4B_THREADS` | `4` | CPU threads for llama-cpp |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py DELETED
@@ -1,142 +0,0 @@
1
- """
2
- KnowLedge Inference Space — hf_space/app.py
3
-
4
- Architecture:
5
- FastAPI provides the REST endpoints (/api/generate, /api/generate_vision, /api/health).
6
- Gradio is mounted at the root so the HF Space is recognised as sdk:gradio, which lets
7
- it serve traffic on port 7860 without a custom Dockerfile.
8
-
9
- Inference runs entirely on CPU on the standard free HF Spaces tier:
10
- - E4B : unsloth/gemma-4-E4B-it-GGUF (UD-IQ3_XXS, ~2.5 GB, llama-cpp-python)
11
- - E2B : google/gemma-4-e2b-it (float32, transformers)
12
-
13
- Render backend calls the FastAPI endpoints via httpx.
14
- Visiting the Space URL in a browser shows the Gradio landing page.
15
- """
16
-
17
- from fastapi import FastAPI
18
- from fastapi.middleware.cors import CORSMiddleware
19
- from pydantic import BaseModel
20
-
21
- try:
22
- import gradio as gr
23
- except ModuleNotFoundError:
24
- gr = None
25
-
26
- try:
27
- from inference import generate_text, generate_with_image, health_check
28
- except ModuleNotFoundError:
29
- from .inference import generate_text, generate_with_image, health_check
30
-
31
-
32
- # ── FastAPI application ───────────────────────────────────────────────────────
33
-
34
- _api = FastAPI(title="KnowLedge Inference API", version="1.0.0")
35
-
36
- # Allow the Render backend (and any browser preview) to call these endpoints.
37
- _api.add_middleware(
38
- CORSMiddleware,
39
- allow_origins=["*"],
40
- allow_methods=["GET", "POST", "OPTIONS"],
41
- allow_headers=["*"],
42
- )
43
-
44
-
45
- class GenerateRequest(BaseModel):
46
- prompt: str
47
- model: str = "e2b"
48
- max_tokens: int = 512
49
-
50
-
51
- class VisionRequest(BaseModel):
52
- prompt: str
53
- image_base64: str
54
- max_tokens: int = 512
55
-
56
-
57
- @_api.get("/api/health")
58
- def health():
59
- """Health check — called by the Render backend before every inference batch."""
60
- try:
61
- return health_check()
62
- except Exception as exc:
63
- return {"status": "error", "error": str(exc)}
64
-
65
-
66
- @_api.post("/api/generate")
67
- def generate(req: GenerateRequest):
68
- """Text generation endpoint used by Scout (concept tagging) and Sage (dialogue)."""
69
- try:
70
- model_name = "e2b" if "e2b" in req.model.lower() else "e4b"
71
- response = generate_text(model_name, req.prompt, req.max_tokens)
72
- return {"response": response}
73
- except Exception as exc:
74
- return {"error": str(exc)}
75
-
76
-
77
- @_api.post("/api/generate_vision")
78
- def generate_vision(req: VisionRequest):
79
- """Vision generation endpoint used by Lens (handwriting verification)."""
80
- try:
81
- response = generate_with_image(
82
- req.prompt,
83
- req.image_base64,
84
- req.max_tokens,
85
- )
86
- return {"response": response}
87
- except Exception as exc:
88
- return {"error": str(exc)}
89
-
90
-
91
- # ── Gradio UI ─────────────────────────────────────────────────────────────────
92
- # A minimal Gradio interface is required so that:
93
- # 1. The HF Space is recognised as sdk:gradio and served on port 7860.
94
- # 2. The Space shows a readable landing page instead of a blank iframe.
95
- # No ZeroGPU needed — inference runs on CPU via llama-cpp-python (GGUF).
96
-
97
- if gr is not None:
98
- with gr.Blocks(title="KnowLedge Inference API") as demo:
99
- gr.Markdown(
100
- """
101
- # 🦉 KnowLedge Inference API
102
-
103
- Gemma 4 **E2B** + **E4B** inference backend for the
104
- [KnowLedge](https://github.com/Charan-suresh/KnowLedge) learning ledger.
105
-
106
- Part of the **Kaggle Gemma 4 for Good** hackathon submission.
107
-
108
- ---
109
-
110
- This Space is a **REST API backend** — it is called programmatically by the
111
- KnowLedge FastAPI service deployed on Render. It is not a chat interface.
112
-
113
- | Endpoint | Method | Used by |
114
- |---|---|---|
115
- | `/api/generate` | POST | Scout (concept tagging), Sage (Socratic dialogue) |
116
- | `/api/generate_vision` | POST | Lens (handwriting verification) |
117
- | `/api/health` | GET | Render backend health check |
118
-
119
- ### Payload format — `/api/generate`
120
- ```json
121
- { "model": "e2b", "prompt": "...", "max_tokens": 512 }
122
- ```
123
-
124
- ### Payload format — `/api/generate_vision`
125
- ```json
126
- { "prompt": "...", "image_base64": "<base64>", "max_tokens": 512 }
127
- ```
128
- """
129
- )
130
-
131
- # Mount the Gradio interface into the FastAPI app.
132
- # FastAPI routes registered above (/api/*) take priority over Gradio routes.
133
- # Gradio occupies the root path ("/") for the HF Space iframe.
134
- app = gr.mount_gradio_app(_api, demo, path="/")
135
- else:
136
- # Local fallback for test and dev environments without gradio installed.
137
- app = _api
138
-
139
-
140
- if __name__ == "__main__":
141
- import uvicorn
142
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
deploy.sh DELETED
@@ -1,72 +0,0 @@
1
- #!/bin/bash
2
- # KnowLedge - Hugging Face Space deployment guide
3
- # Run each block manually in sequence.
4
- # Prerequisites: huggingface_hub installed, HF account created
5
-
6
- # -- Step 1: Install HF CLI -------------------------------------------------
7
- pip install huggingface_hub
8
-
9
- # -- Step 2: Login to Hugging Face ------------------------------------------
10
- huggingface-cli login
11
- # Paste your HF token when prompted
12
- # Get token at: https://huggingface.co/settings/tokens
13
- # Token needs "write" permission
14
-
15
- # -- Step 3: Create the Space ------------------------------------------------
16
- python3 - << 'EOF'
17
- from huggingface_hub import HfApi
18
- api = HfApi()
19
- api.create_repo(
20
- repo_id="knowledge-inference", # becomes: your-username/knowledge-inference
21
- repo_type="space",
22
- space_sdk="gradio",
23
- private=False, # must be public for free ZeroGPU
24
- exist_ok=True,
25
- )
26
- print("Space created successfully")
27
- EOF
28
-
29
- # -- Step 4: Upload Space files ----------------------------------------------
30
- python3 - << 'EOF'
31
- from huggingface_hub import HfApi
32
- import os
33
-
34
- api = HfApi()
35
- space_id = f"{api.whoami()['name']}/knowledge-inference"
36
- base_dir = os.getcwd()
37
-
38
- files = ["app.py", "inference.py", "requirements.txt", "README.md"]
39
- for f in files:
40
- path = os.path.join(base_dir, f)
41
- api.upload_file(
42
- path_or_fileobj=path,
43
- path_in_repo=f,
44
- repo_id=space_id,
45
- repo_type="space",
46
- )
47
- print(f"Uploaded: {f}")
48
-
49
- print(f"\nSpace URL: https://huggingface.co/spaces/{space_id}")
50
- print(f"API URL: https://{space_id.replace('/', '-')}.hf.space")
51
- EOF
52
-
53
- # -- Step 5: Enable ZeroGPU --------------------------------------------------
54
- # Go to: https://huggingface.co/spaces/YOUR-USERNAME/knowledge-inference/settings
55
- # Under "Space Hardware" -> select "ZeroGPU (NVIDIA H200)"
56
- # This cannot be done via API - must be done in the HF web UI
57
- echo "-> Manually enable ZeroGPU in Space settings:"
58
- echo " https://huggingface.co/spaces/$(huggingface-cli whoami)/knowledge-inference/settings"
59
-
60
- # -- Step 6: Get your Space API URL ------------------------------------------
61
- # After deployment (~3-5 minutes for build), your endpoints are:
62
- # Text: POST https://your-username-knowledge-inference.hf.space/api/generate
63
- # Vision: POST https://your-username-knowledge-inference.hf.space/api/generate_vision
64
- # Health: GET https://your-username-knowledge-inference.hf.space/api/health
65
-
66
- # -- Step 7: Set Render env vars ---------------------------------------------
67
- # In Render dashboard -> your service -> Environment:
68
- # INFERENCE_BACKEND = hf_space
69
- # HF_SPACE_URL = your-username/knowledge-inference
70
- # Then trigger a manual redeploy on Render.
71
-
72
- echo "Deployment complete. Set HF_SPACE_URL in Render and redeploy."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
inference.py DELETED
@@ -1,152 +0,0 @@
1
- """
2
- KnowLedge Inference — inference.py
3
-
4
- Runs entirely on CPU on the standard free HF Spaces tier (no ZeroGPU).
5
-
6
- Model backends
7
- --------------
8
- Text : Quantized Gemma 2B GGUF via llama-cpp-python (CPU-only).
9
- """
10
-
11
- import base64
12
- import io
13
- import logging
14
- import os
15
-
16
- from PIL import Image
17
- from huggingface_hub import hf_hub_download
18
-
19
- # ── spaces stub — GPU decorator is a no-op on the free CPU tier ───────────────
20
- try:
21
- import spaces
22
- except ModuleNotFoundError:
23
- class _SpacesStub:
24
- @staticmethod
25
- def GPU(duration: int = 0):
26
- def decorator(fn):
27
- return fn
28
- return decorator
29
-
30
- spaces = _SpacesStub()
31
-
32
- # ── llama-cpp backend ─────────────────────────────────────────────────────────
33
- _llama_import_error = None
34
- try:
35
- from llama_cpp import Llama
36
- except Exception as exc:
37
- _llama_import_error = exc
38
- Llama = None
39
-
40
- MODEL_REPO = os.getenv("GEMMA_2B_GGUF_REPO", "bartowski/gemma-2-2b-it-GGUF")
41
- MODEL_FILE = os.getenv("GEMMA_2B_GGUF_FILE", "gemma-2-2b-it-Q4_K_M.gguf")
42
-
43
- # Try common filename variants in case upstream naming changes.
44
- MODEL_FILE_CANDIDATES = [
45
- MODEL_FILE,
46
- "gemma-2-2b-it-Q4_K_M.gguf",
47
- "gemma-2-2b-it-Q5_K_M.gguf",
48
- "gemma-2-2b-it-Q8_0.gguf",
49
- ]
50
-
51
- _llm = None
52
- _last_model_error = ""
53
- logger = logging.getLogger(__name__)
54
-
55
-
56
- class _FallbackModel:
57
- def __call__(self, prompt: str, max_tokens: int = 512, temperature: float = 0.7, top_p: float = 0.95):
58
- del temperature, top_p
59
- text = (
60
- "Inference backend is temporarily running in fallback mode. "
61
- "The Gemma 2B GGUF model could not be initialized on this runtime."
62
- )
63
- prompt_text = str(prompt).strip()
64
- if prompt_text:
65
- text += f" Prompt received: {prompt_text[:160]}"
66
- return {"choices": [{"text": text[:max_tokens]}]}
67
-
68
-
69
- def _download_model_file() -> str:
70
- last_error = None
71
- seen = set()
72
- for filename in MODEL_FILE_CANDIDATES:
73
- if filename in seen:
74
- continue
75
- seen.add(filename)
76
- try:
77
- return str(hf_hub_download(repo_id=MODEL_REPO, filename=filename))
78
- except Exception as exc:
79
- last_error = exc
80
- logger.warning("Failed to download GGUF file '%s' from %s", filename, MODEL_REPO)
81
-
82
- raise RuntimeError(
83
- f"Could not download a GGUF file from '{MODEL_REPO}'. "
84
- f"Tried: {MODEL_FILE_CANDIDATES}. Last error: {last_error}"
85
- )
86
-
87
-
88
- def load_model():
89
- global _llm, _last_model_error
90
- if _llm is None:
91
- if Llama is None:
92
- _last_model_error = f"llama-cpp import failed: {_llama_import_error}"
93
- logger.error(_last_model_error)
94
- _llm = _FallbackModel()
95
- return _llm
96
-
97
- try:
98
- model_path = _download_model_file()
99
- _llm = Llama(
100
- model_path=model_path,
101
- n_ctx=int(os.getenv("GEMMA_2B_CTX", "4096")),
102
- n_threads=int(os.getenv("GEMMA_2B_THREADS", "4")),
103
- n_gpu_layers=0,
104
- )
105
- _last_model_error = ""
106
- except Exception:
107
- logger.exception("Gemma 2B GGUF initialization failed; falling back")
108
- _last_model_error = "gguf initialization failed; check Space logs for traceback"
109
- _llm = _FallbackModel()
110
- return _llm
111
-
112
-
113
- @spaces.GPU(duration=90)
114
- def generate_text(model_name: str, prompt: str, max_new_tokens: int = 512) -> str:
115
- del model_name
116
- try:
117
- llm = load_model()
118
- output = llm(
119
- str(prompt),
120
- max_tokens=min(max_new_tokens, 512),
121
- temperature=0.7,
122
- top_p=0.95,
123
- )
124
- return output["choices"][0]["text"].strip()
125
-
126
- except Exception:
127
- logger.exception("Text generation failed")
128
- raise
129
-
130
-
131
- @spaces.GPU(duration=120)
132
- def generate_with_image(prompt: str, image_base64: str, max_new_tokens: int = 512) -> str:
133
- try:
134
- _ = Image.open(io.BytesIO(base64.b64decode(image_base64)))
135
- text_prompt = f"{prompt}\n\n[Image provided and parsed on client-side.]"
136
- return generate_text("e2b", text_prompt, max_new_tokens)
137
-
138
- except Exception:
139
- logger.exception("Vision generation failed")
140
- raise
141
-
142
-
143
- @spaces.GPU(duration=60)
144
- def health_check() -> dict:
145
- return {
146
- "status": "ok",
147
- "model_loaded": _llm is not None and not isinstance(_llm, _FallbackModel),
148
- "backend": "llama-cpp",
149
- "model_repo": MODEL_REPO,
150
- "model_file": MODEL_FILE,
151
- "model_error": _last_model_error or None,
152
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt DELETED
@@ -1,10 +0,0 @@
1
- fastapi==0.112.2
2
- uvicorn==0.30.6
3
- gradio==4.44.1
4
-
5
- # Inference backend (quantized GGUF via llama-cpp)
6
- llama-cpp-python==0.2.90
7
-
8
- # Utilities
9
- Pillow
10
- huggingface_hub==0.25.2
 
 
 
 
 
 
 
 
 
 
 
runtime.txt DELETED
@@ -1 +0,0 @@
1
- 3.10
 
 
scratch.py DELETED
@@ -1,2 +0,0 @@
1
- import llama_cpp
2
- print(llama_cpp.__version__)