Spaces:
Sleeping
Sleeping
File size: 8,370 Bytes
12ab90a | 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 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | # -*- coding: utf-8 -*-
"""
swarm_llm.py β Local CPU LLM Swarm (Qwen2.5-1.5B-Instruct, Q4_K_M)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Why a local model?
The NIM API is rate-limited (tokens/minute). Every small sub-task
(JSON formatting, log summarization, brainstorm question generation,
Bell Curve trimming) that goes to NIM wastes quota needed for coding.
This module runs Qwen2.5-1.5B-Instruct at Q4_K_M quantization:
- RAM: ~1.1 GB (leaves 14 GB free for FastAPI + data)
- Speed: ~45 tok/s on 2 vCPUs (good enough for short tasks)
- Model: downloaded from HF Hub on first run β cached in /tmp/models/
NIM is used ONLY for heavy coding tasks (forge_execute).
SwarmLLM handles everything else.
If llama-cpp-python is not installed (e.g. first boot before pip):
the module silently degrades and returns a FALLBACK_STUB response,
so the rest of the system still works.
"""
import os
import logging
import asyncio
import time
from typing import Optional
logger = logging.getLogger("swarm_llm")
MODEL_CACHE_DIR = os.environ.get("MODEL_CACHE_DIR", "/tmp/models")
MODEL_REPO = "Qwen/Qwen2.5-1.5B-Instruct-GGUF"
MODEL_FILENAME = "qwen2.5-1.5b-instruct-q4_k_m.gguf"
N_CTX = 2048 # context window β matches our Bell Curve budget
N_THREADS = int(os.environ.get("SWARM_THREADS", "2"))
MAX_TOKENS = int(os.environ.get("SWARM_MAX_TOKENS", "256"))
ENABLE_SWARM = os.environ.get("ENABLE_SWARM_LLM", "true").lower() == "true"
_llm = None # loaded lazily on first call
_llm_lock = None # asyncio.Lock initialised at first call
def _get_lock():
global _llm_lock
if _llm_lock is None:
_llm_lock = asyncio.Lock()
return _llm_lock
def _load_model() -> Optional[object]:
"""Download (if needed) and load the GGUF model. Blocking β call in executor."""
global _llm
if _llm is not None:
return _llm
if not ENABLE_SWARM:
logger.info("[SwarmLLM] Disabled via ENABLE_SWARM_LLM=false")
return None
try:
from llama_cpp import Llama
except ImportError:
logger.warning("[SwarmLLM] llama-cpp-python not installed. Running in stub mode.")
return None
model_path = os.path.join(MODEL_CACHE_DIR, MODEL_FILENAME)
if not os.path.exists(model_path):
logger.info(f"[SwarmLLM] Downloading {MODEL_FILENAME} from HF Hubβ¦")
try:
from huggingface_hub import hf_hub_download
os.makedirs(MODEL_CACHE_DIR, exist_ok=True)
model_path = hf_hub_download(
repo_id=MODEL_REPO,
filename=MODEL_FILENAME,
local_dir=MODEL_CACHE_DIR,
local_dir_use_symlinks=False,
)
logger.info(f"[SwarmLLM] Downloaded β {model_path}")
except Exception as e:
logger.error(f"[SwarmLLM] Download failed: {e}")
return None
logger.info(f"[SwarmLLM] Loading model (n_ctx={N_CTX}, threads={N_THREADS}, type_k=8 (Q8_0), type_v=8 (Q8_0))β¦")
t0 = time.time()
try:
_llm = Llama(
model_path=model_path,
n_ctx=N_CTX,
n_threads=N_THREADS,
n_gpu_layers=0, # CPU only β HF free spaces have no GPU
verbose=False,
chat_format="chatml",
type_k=8, # 8-bit quantization for Key Cache (Turbo Quant)
type_v=8, # 8-bit quantization for Value Cache (Turbo Quant)
)
logger.info(f"[SwarmLLM] Model loaded in {time.time()-t0:.1f}s")
return _llm
except Exception as e:
logger.error(f"[SwarmLLM] Failed to load model: {e}")
return None
class SwarmLLM:
"""
Async wrapper around the local Qwen model.
All inference runs in a thread executor so the FastAPI event loop
is never blocked.
"""
def __init__(self):
self._ready = False
async def warm_up(self):
"""Pre-load the model at startup so first inference is instant."""
loop = asyncio.get_event_loop()
model = await loop.run_in_executor(None, _load_model)
self._ready = model is not None
if self._ready:
logger.info("[SwarmLLM] Warm-up complete. Ready for inference.")
else:
logger.warning("[SwarmLLM] Running in stub mode (model not available).")
async def infer(self, prompt: str, system: str = "", max_tokens: int = MAX_TOKENS) -> str:
"""
Run inference on the local model. Returns the generated text.
Falls back to a stub if model is not loaded.
"""
if not self._ready:
return self._stub(prompt)
lock = _get_lock()
async with lock:
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(
None,
lambda: self._sync_infer(prompt, system, max_tokens),
)
return result
def _sync_infer(self, prompt: str, system: str, max_tokens: int) -> str:
global _llm
if _llm is None:
return self._stub(prompt)
try:
messages = []
if system:
messages.append({"role": "system", "content": system})
messages.append({"role": "user", "content": prompt})
response = _llm.create_chat_completion(
messages=messages,
max_tokens=max_tokens,
temperature=0.3,
stop=["<|im_end|>", "</s>"],
)
text = response["choices"][0]["message"]["content"].strip()
logger.debug("[SwarmLLM] Infer complete: %d chars", len(text))
return text
except Exception as e:
logger.error(f"[SwarmLLM] Inference error: {e}")
return self._stub(prompt)
@staticmethod
def _stub(prompt: str) -> str:
"""Fallback when model is unavailable β returns a safe placeholder."""
return "[SwarmLLM unavailable β NIM will handle this task]"
# ββ High-Level Task Shortcuts ββββββββββββββββββββββββββββββββββββββββββββββ
async def summarize(self, text: str, max_words: int = 80) -> str:
"""Summarise a long text into β€ max_words words. Used to enforce Bell Curve budget."""
prompt = (
f"Summarise the following in β€ {max_words} words. "
f"Be dense with information. No filler sentences.\n\n{text[:3000]}"
)
return await self.infer(prompt, system="You are a precise technical summariser.")
async def format_json(self, raw: str) -> str:
"""Extract and clean a JSON object from a messy LLM response."""
prompt = (
"Extract the JSON object from the following text. "
"Return ONLY the JSON, no markdown fences, no explanation.\n\n" + raw[:2000]
)
return await self.infer(prompt, system="You are a JSON extractor. Output only valid JSON.")
async def generate_brainstorm_questions(self, goal: str, n: int = 5) -> list:
"""Generate n 'What ifβ¦' brainstorm questions for the hourly swarm cycle."""
prompt = (
f"Generate exactly {n} creative 'What ifβ¦' questions to improve: '{goal}'. "
f"Each question should be a novel technical idea. "
f"Format: one question per line, no numbering."
)
raw = await self.infer(prompt, system="You are a creative technical brainstormer.", max_tokens=400)
questions = [q.strip() for q in raw.strip().splitlines() if q.strip()]
return questions[:n]
async def smart_trim(self, text: str, char_limit: int) -> str:
"""
If text exceeds char_limit, ask the local model to summarise it
to fit. Better than hard-cutting. Used in Bell Curve budget enforcement.
"""
if len(text) <= char_limit:
return text
target_words = char_limit // 6 # rough chars-to-words ratio
summary = await self.summarize(text, max_words=target_words)
return summary
# Singleton β import and use directly
swarm = SwarmLLM()
|