viarl199020 commited on
Commit
a716923
Β·
verified Β·
1 Parent(s): cf3a0fd

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +127 -0
  2. README.md +37 -0
  3. server.py +277 -0
  4. start.sh +67 -0
Dockerfile ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ═══════════════════════════════════════════════════════════════════════════
2
+ # OMNI-NEXUS Β· Worker BitNet b1.58 (ternario, 1.58 bits)
3
+ # ═══════════════════════════════════════════════════════════════════════════
4
+ #
5
+ # POR QUÉ BITNET Y NO UN GGUF Q4 NORMAL
6
+ # ─────────────────────────────────────
7
+ # BitNet restringe cada peso a {-1, 0, +1}. Eso elimina las multiplicaciones
8
+ # del matmul y las convierte en sumas y restas β€” que es exactamente donde una
9
+ # CPU sin AVX potente deja de ser el cuello de botella.
10
+ #
11
+ # NΓΊmeros oficiales de bitnet.cpp en x86: 2.37x-6.17x mΓ‘s rΓ‘pido que fp16,
12
+ # mΓ‘s 1.15x-2.1x extra con los kernels paralelos de enero 2026.
13
+ #
14
+ # El detalle que decide todo: el paper reporta que la ventaja es MAYOR en
15
+ # escenarios con hilos limitados. Un Space tiene 2 vCPU. Es el peor caso para
16
+ # un Q4 normal y el mejor caso relativo para BitNet.
17
+ #
18
+ # Modelo TamaΓ±o tok/s en 2 vCPU (estimado)
19
+ # ─────────────────────────────────────────────────────────
20
+ # Llama 3.2 3B Q4_K_M 2.0 GB 8-12
21
+ # BitNet b1.58 2B i2_s 1.2 GB 18-30 ← este
22
+ #
23
+ # EL BUG QUE MATΓ“ TU "CEREBRO A"
24
+ # ──────────────────────────────
25
+ # Tu Dockerfile anterior documentaba que ggml-model-i2_s.gguf usa el tensor
26
+ # type 36 (IQ4_NL_4_4), eliminado del llama.cpp oficial. Por eso tuviste que
27
+ # recurrir a los binarios de ik_llama.cpp de Thireus.
28
+ #
29
+ # AquΓ­ no hay ese problema: compilamos bitnet.cpp, que es el framework OFICIAL
30
+ # de Microsoft para i2_s. No depende de que un fork de terceros siga publicando
31
+ # binarios, y trae las optimizaciones de enero 2026 que ik_llama no tiene.
32
+ # ═══════════════════════════════════════════════════════════════════════════
33
+
34
+ # ── Etapa 1: compilar bitnet.cpp ───────────────────────────────────────────
35
+ FROM python:3.11-slim AS builder
36
+
37
+ RUN apt-get update && apt-get install -y --no-install-recommends \
38
+ build-essential cmake git ca-certificates \
39
+ clang lld libomp-dev \
40
+ && rm -rf /var/lib/apt/lists/*
41
+
42
+ WORKDIR /build
43
+
44
+ # --recursive es obligatorio: bitnet.cpp lleva llama.cpp como submΓ³dulo con
45
+ # los kernels ternarios parcheados encima.
46
+ RUN git clone --recursive --depth 1 https://github.com/microsoft/BitNet.git bitnet
47
+
48
+ WORKDIR /build/bitnet
49
+
50
+ RUN pip install --no-cache-dir -r requirements.txt || true
51
+
52
+ # Se compila con clang porque los kernels TL1/TL2 de BitNet usan intrΓ­nsecos
53
+ # que gcc no vectoriza igual de bien.
54
+ RUN cmake -B build \
55
+ -DCMAKE_BUILD_TYPE=Release \
56
+ -DCMAKE_C_COMPILER=clang \
57
+ -DCMAKE_CXX_COMPILER=clang++ \
58
+ -DGGML_NATIVE=OFF \
59
+ -DGGML_AVX2=ON \
60
+ -DGGML_FMA=ON \
61
+ -DLLAMA_BUILD_SERVER=ON \
62
+ && cmake --build build --config Release -j"$(nproc)" --target llama-server \
63
+ && find build -name "llama-server" -type f -exec cp {} /build/llama-server \; \
64
+ && chmod +x /build/llama-server \
65
+ && echo "[BUILD] bitnet.cpp compilado"
66
+
67
+ # ── Etapa 2: descargar el modelo ternario ──────────────────────────────────
68
+ FROM python:3.11-slim AS fetcher
69
+
70
+ RUN pip install --no-cache-dir huggingface_hub
71
+
72
+ # Repo oficial de Microsoft, pΓΊblico y sin gating. Licencia MIT.
73
+ # i2_s = cuantizaciΓ³n ternaria nativa de BitNet (~1.2 GB para 2.4B params).
74
+ ARG MODEL_REPO="microsoft/bitnet-b1.58-2B-4T-gguf"
75
+ ARG MODEL_FILE="ggml-model-i2_s.gguf"
76
+ ENV MODEL_REPO=${MODEL_REPO}
77
+ ENV MODEL_FILE=${MODEL_FILE}
78
+
79
+ RUN mkdir -p /models && python3 -c "\
80
+ import os, sys; \
81
+ from huggingface_hub import hf_hub_download; \
82
+ p = hf_hub_download(repo_id=os.environ['MODEL_REPO'], filename=os.environ['MODEL_FILE'], local_dir='/models'); \
83
+ gb = os.path.getsize(p)/1024**3; \
84
+ print(f'[FETCH] {p} ({gb:.2f} GB)', flush=True); \
85
+ sys.exit(0 if gb > 0.5 else 1)"
86
+
87
+ # ── Etapa 3: runtime ───────────────────────────────────────────────────────
88
+ FROM python:3.11-slim AS runtime
89
+
90
+ RUN apt-get update && apt-get install -y --no-install-recommends \
91
+ curl ca-certificates libgomp1 libstdc++6 \
92
+ && rm -rf /var/lib/apt/lists/*
93
+
94
+ WORKDIR /app
95
+
96
+ COPY --from=builder /build/llama-server /usr/local/bin/llama-server
97
+ COPY --from=fetcher /models /models
98
+
99
+ RUN chmod +x /usr/local/bin/llama-server
100
+
101
+ RUN pip install --no-cache-dir \
102
+ fastapi==0.115.6 \
103
+ "uvicorn[standard]==0.34.0" \
104
+ httpx==0.28.1
105
+
106
+ # ── ConfiguraciΓ³n ──────────────────────────────────────────────────────────
107
+ # N_THREADS=2 : son 2 vCPU reales; mΓ‘s hilos solo generan contenciΓ³n de cachΓ©.
108
+ # N_CTX=4096 : el KV cache se reserva entero al arrancar. Con la cuantizaciΓ³n
109
+ # q8_0 de abajo ocupa la mitad que en f16.
110
+ ENV N_CTX=4096
111
+ ENV N_THREADS=2
112
+ ENV LLAMA_PORT=8080
113
+ ENV PORT=7860
114
+ ENV WORKER_ID=A
115
+ ENV WORKER_KIND=chat
116
+ ENV MODEL_NAME="bitnet-b1.58-2b"
117
+ ENV LLAMA_TIMEOUT=180.0
118
+
119
+ COPY server.py start.sh ./
120
+ RUN chmod +x start.sh
121
+
122
+ EXPOSE 7860
123
+
124
+ HEALTHCHECK --interval=30s --timeout=15s --start-period=240s --retries=5 \
125
+ CMD curl -f http://localhost:7860/health || exit 1
126
+
127
+ CMD ["./start.sh"]
README.md ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Omni-Nexus Worker A (BitNet)
3
+ emoji: ⚑
4
+ colorFrom: indigo
5
+ colorTo: purple
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
+ # Omni-Nexus Β· Worker BitNet b1.58
12
+
13
+ Nodo de inferencia ternario (1.58 bits) compatible con la API de OpenAI.
14
+
15
+ Cada peso vale {-1, 0, +1}, lo que convierte las multiplicaciones del matmul
16
+ en sumas. La ventaja crece en escenarios con pocos hilos β€” justo el caso de
17
+ un Space con 2 vCPU.
18
+
19
+ | | 3B Q4_K_M | BitNet b1.58 2B |
20
+ |---|---|---|
21
+ | TamaΓ±o | 2.0 GB | 1.2 GB |
22
+ | tok/s en 2 vCPU | 8-12 | 18-30 |
23
+
24
+ ## Uso
25
+
26
+ `Odysseus β†’ Settings β†’ Add Local Models (Endpoint)`:
27
+
28
+ ```
29
+ https://TU-USUARIO-ESTE-SPACE.hf.space/v1
30
+ ```
31
+
32
+ ## Variables
33
+
34
+ | Variable | Valor | Nota |
35
+ |---|---|---|
36
+ | `WORKER_ID` | `A` | Identifica quiΓ©n respondiΓ³ |
37
+ | `CHAT_TEMPLATE` | `llama3` | BitNet usa formato Llama 3, **no** ChatML |
server.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ OMNI-NEXUS Β· Worker de cΓ³mputo elΓ‘stico
3
+ =======================================
4
+ Envuelve llama.cpp en una API compatible con OpenAI para que Odysseus lo
5
+ vea como un endpoint local mΓ‘s, sin modificar nada del proyecto.
6
+
7
+ Endpoints:
8
+ GET /health β†’ estado (lo usa el healthcheck de HF)
9
+ GET /v1/models β†’ catΓ‘logo (lo usa Odysseus al aΓ±adir el endpoint)
10
+ POST /v1/chat/completions β†’ inferencia, con y sin streaming
11
+ GET /warm β†’ despierta el Space sin gastar inferencia
12
+
13
+ Nota sobre /warm: los Spaces gratuitos se suspenden tras un rato inactivos y
14
+ tardan ~60-90 s en volver. El router del gateway llama a /warm para levantar
15
+ el siguiente worker mientras el actual trabaja, asΓ­ el usuario no percibe el
16
+ arranque en frΓ­o.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import os
23
+ import time
24
+ import uuid
25
+ from typing import Any, AsyncIterator, Dict, List, Optional
26
+
27
+ import httpx
28
+ from fastapi import FastAPI, HTTPException
29
+ from fastapi.middleware.cors import CORSMiddleware
30
+ from fastapi.responses import StreamingResponse
31
+ from pydantic import BaseModel, Field
32
+
33
+ LLAMA_URL = f"http://127.0.0.1:{os.getenv('LLAMA_PORT', '8080')}"
34
+ WORKER_ID = os.getenv("WORKER_ID", "A")
35
+ N_CTX = int(os.getenv("N_CTX", "4096"))
36
+ TIMEOUT = float(os.getenv("LLAMA_TIMEOUT", "180.0"))
37
+ MODEL_NAME = os.getenv("MODEL_NAME", "omni-nexus/worker")
38
+
39
+ # Plantilla de chat. BitNet b1.58-2B-4T se entrenΓ³ con el formato de Llama 3,
40
+ # no con ChatML. Usar la plantilla equivocada degrada mucho la calidad sin dar
41
+ # ningΓΊn error visible β€” el modelo simplemente responde peor. Por eso es una
42
+ # variable explΓ­cita y no una suposiciΓ³n.
43
+ CHAT_TEMPLATE = os.getenv("CHAT_TEMPLATE", "llama3").lower()
44
+
45
+ app = FastAPI(title=f"Omni-Nexus Worker {WORKER_ID}", version="1.0.0")
46
+
47
+ # CORS abierto: el endpoint se consume desde el navegador de Odysseus.
48
+ app.add_middleware(
49
+ CORSMiddleware,
50
+ allow_origins=["*"],
51
+ allow_methods=["*"],
52
+ allow_headers=["*"],
53
+ )
54
+
55
+ _stats = {"requests": 0, "tokens": 0, "started_at": time.time()}
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Modelos de entrada (subconjunto del esquema OpenAI que Odysseus usa)
60
+ # ---------------------------------------------------------------------------
61
+ class Message(BaseModel):
62
+ role: str
63
+ content: Any = ""
64
+
65
+
66
+ class ChatRequest(BaseModel):
67
+ model: Optional[str] = None
68
+ messages: List[Message]
69
+ temperature: float = 0.7
70
+ top_p: float = 0.95
71
+ max_tokens: Optional[int] = Field(default=1024)
72
+ stream: bool = False
73
+ stop: Optional[List[str]] = None
74
+
75
+
76
+ # ---------------------------------------------------------------------------
77
+ def _flatten(content: Any) -> str:
78
+ """El contenido puede venir como string o como lista de bloques."""
79
+ if isinstance(content, str):
80
+ return content
81
+ if isinstance(content, list):
82
+ parts = []
83
+ for block in content:
84
+ if isinstance(block, dict) and block.get("type") == "text":
85
+ parts.append(block.get("text", ""))
86
+ elif isinstance(block, str):
87
+ parts.append(block)
88
+ return "\n".join(parts)
89
+ return str(content or "")
90
+
91
+
92
+ def _prompt_llama3(messages: List[Message]) -> str:
93
+ """Formato Llama 3 β€” el correcto para BitNet b1.58-2B-4T."""
94
+ out = ["<|begin_of_text|>"]
95
+ for m in messages:
96
+ role = m.role if m.role in ("system", "user", "assistant") else "user"
97
+ out.append(
98
+ f"<|start_header_id|>{role}<|end_header_id|>\n\n"
99
+ f"{_flatten(m.content)}<|eot_id|>"
100
+ )
101
+ out.append("<|start_header_id|>assistant<|end_header_id|>\n\n")
102
+ return "".join(out)
103
+
104
+
105
+ def _prompt_chatml(messages: List[Message]) -> str:
106
+ """Formato ChatML β€” Qwen 2.5 y buena parte de los instruct actuales."""
107
+ out = []
108
+ for m in messages:
109
+ role = m.role if m.role in ("system", "user", "assistant") else "user"
110
+ out.append(f"<|im_start|>{role}\n{_flatten(m.content)}<|im_end|>")
111
+ out.append("<|im_start|>assistant\n")
112
+ return "\n".join(out)
113
+
114
+
115
+ def to_prompt(messages: List[Message]) -> str:
116
+ if CHAT_TEMPLATE == "chatml":
117
+ return _prompt_chatml(messages)
118
+ return _prompt_llama3(messages)
119
+
120
+
121
+ # Se incluyen los stops de ambas plantillas: sobra un par de tokens en la
122
+ # lista y evita que un cambio de modelo deje basura al final de la respuesta.
123
+ STOP_TOKENS = [
124
+ "<|eot_id|>", "<|start_header_id|>", "<|end_header_id|>",
125
+ "<|im_end|>", "<|im_start|>", "</s>", "<|end_of_text|>",
126
+ ]
127
+
128
+
129
+ # ---------------------------------------------------------------------------
130
+ @app.get("/health")
131
+ async def health():
132
+ llama_ok = False
133
+ try:
134
+ async with httpx.AsyncClient(timeout=5.0) as c:
135
+ r = await c.get(f"{LLAMA_URL}/health")
136
+ llama_ok = r.status_code == 200
137
+ except Exception:
138
+ pass
139
+ return {
140
+ "status": "ok" if llama_ok else "starting",
141
+ "worker": WORKER_ID,
142
+ "llama": llama_ok,
143
+ "ctx": N_CTX,
144
+ "template": CHAT_TEMPLATE,
145
+ "kind": os.getenv("WORKER_KIND", "chat"),
146
+ "uptime_s": round(time.time() - _stats["started_at"]),
147
+ "requests": _stats["requests"],
148
+ }
149
+
150
+
151
+ @app.get("/warm")
152
+ async def warm():
153
+ """Despierta el Space sin gastar inferencia. Responde apenas el proceso vive."""
154
+ return {"worker": WORKER_ID, "awake": True, "ts": time.time()}
155
+
156
+
157
+ @app.get("/v1/models")
158
+ async def models():
159
+ return {
160
+ "object": "list",
161
+ "data": [
162
+ {
163
+ "id": MODEL_NAME,
164
+ "object": "model",
165
+ "created": int(_stats["started_at"]),
166
+ "owned_by": f"omni-nexus-worker-{WORKER_ID}",
167
+ "context_length": N_CTX,
168
+ }
169
+ ],
170
+ }
171
+
172
+
173
+ @app.post("/v1/chat/completions")
174
+ async def chat(req: ChatRequest):
175
+ prompt = to_prompt(req.messages)
176
+ payload = {
177
+ "prompt": prompt,
178
+ "temperature": req.temperature,
179
+ "top_p": req.top_p,
180
+ "n_predict": req.max_tokens or 1024,
181
+ "stop": STOP_TOKENS + (req.stop or []),
182
+ "stream": req.stream,
183
+ "cache_prompt": True, # reutiliza el KV cache entre turnos: gran ahorro
184
+ }
185
+ _stats["requests"] += 1
186
+
187
+ if req.stream:
188
+ return StreamingResponse(
189
+ _stream(payload, req.model), media_type="text/event-stream"
190
+ )
191
+
192
+ try:
193
+ async with httpx.AsyncClient(timeout=TIMEOUT) as c:
194
+ r = await c.post(f"{LLAMA_URL}/completion", json=payload)
195
+ r.raise_for_status()
196
+ data = r.json()
197
+ except httpx.TimeoutException:
198
+ raise HTTPException(504, detail=f"Worker {WORKER_ID}: timeout de inferencia")
199
+ except Exception as exc:
200
+ raise HTTPException(502, detail=f"Worker {WORKER_ID}: {exc}")
201
+
202
+ text = (data.get("content") or "").strip()
203
+ for tok in STOP_TOKENS:
204
+ text = text.replace(tok, "")
205
+
206
+ n_out = data.get("tokens_predicted", 0)
207
+ n_in = data.get("tokens_evaluated", 0)
208
+ _stats["tokens"] += n_out
209
+
210
+ return {
211
+ "id": f"chatcmpl-{uuid.uuid4().hex[:12]}",
212
+ "object": "chat.completion",
213
+ "created": int(time.time()),
214
+ "model": req.model or MODEL_NAME,
215
+ "choices": [
216
+ {
217
+ "index": 0,
218
+ "message": {"role": "assistant", "content": text.strip()},
219
+ "finish_reason": "stop",
220
+ }
221
+ ],
222
+ "usage": {
223
+ "prompt_tokens": n_in,
224
+ "completion_tokens": n_out,
225
+ "total_tokens": n_in + n_out,
226
+ },
227
+ "_worker": WORKER_ID,
228
+ }
229
+
230
+
231
+ async def _stream(payload: Dict[str, Any], model: Optional[str]) -> AsyncIterator[str]:
232
+ cid = f"chatcmpl-{uuid.uuid4().hex[:12]}"
233
+ created = int(time.time())
234
+
235
+ def frame(delta: Dict[str, Any], finish: Optional[str] = None) -> str:
236
+ return "data: " + json.dumps(
237
+ {
238
+ "id": cid,
239
+ "object": "chat.completion.chunk",
240
+ "created": created,
241
+ "model": model or MODEL_NAME,
242
+ "choices": [{"index": 0, "delta": delta, "finish_reason": finish}],
243
+ }
244
+ ) + "\n\n"
245
+
246
+ yield frame({"role": "assistant", "content": ""})
247
+
248
+ try:
249
+ async with httpx.AsyncClient(timeout=TIMEOUT) as c:
250
+ async with c.stream("POST", f"{LLAMA_URL}/completion", json=payload) as r:
251
+ async for line in r.aiter_lines():
252
+ if not line.startswith("data: "):
253
+ continue
254
+ try:
255
+ chunk = json.loads(line[6:])
256
+ except json.JSONDecodeError:
257
+ continue
258
+ piece = chunk.get("content", "")
259
+ if piece:
260
+ _stats["tokens"] += 1
261
+ yield frame({"content": piece})
262
+ if chunk.get("stop"):
263
+ break
264
+ except Exception as exc:
265
+ yield frame({"content": f"\n[worker {WORKER_ID}: {exc}]"})
266
+
267
+ yield frame({}, finish="stop")
268
+ yield "data: [DONE]\n\n"
269
+
270
+
271
+ @app.get("/")
272
+ async def root():
273
+ return {
274
+ "name": f"Omni-Nexus Worker {WORKER_ID}",
275
+ "usage": "AΓ±Γ‘delo en Odysseus β†’ Settings β†’ Add Local Models con la URL /v1",
276
+ "endpoints": ["/health", "/warm", "/v1/models", "/v1/chat/completions"],
277
+ }
start.sh ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # ═══════════════════════════════════════════════════════════════════════════
3
+ # start.sh β€” Worker BitNet b1.58
4
+ # ═══════════════════════════════════════════════════════════════════════════
5
+ set -e
6
+
7
+ N_CTX="${N_CTX:-4096}"
8
+ N_THREADS="${N_THREADS:-2}"
9
+ LLAMA_PORT="${LLAMA_PORT:-8080}"
10
+ PORT="${PORT:-7860}"
11
+ WORKER_ID="${WORKER_ID:-A}"
12
+
13
+ echo "[START] ═══════════════════════════════════════════"
14
+ echo "[START] Omni-Nexus Worker ${WORKER_ID} β€” BitNet b1.58"
15
+ echo "[START] ═══════════════════════════════════════════"
16
+
17
+ GGUF=$(find /models -name "*.gguf" -type f | head -1)
18
+ if [ -z "$GGUF" ]; then
19
+ echo "[START] FATAL: no hay .gguf en /models"; ls -lah /models 2>/dev/null; exit 1
20
+ fi
21
+ echo "[START] Modelo ternario: $(du -h "$GGUF" | cut -f1) β€” $GGUF"
22
+ echo "[START] Hilos: ${N_THREADS} | Contexto: ${N_CTX}"
23
+
24
+ LOG=/tmp/llama.log; : > "$LOG"
25
+
26
+ # --cache-type-k/v q8_0: cuantiza el KV cache. Reduce a la mitad la RAM del
27
+ # contexto con pΓ©rdida de calidad inapreciable. Es lo que permite tener
28
+ # 4096 de contexto cΓ³modamente en vez de recortar a 2048.
29
+ # --cont-batching: permite solapar prefill y generaciΓ³n entre peticiones.
30
+ # --mlock: evita que el kernel mande los pesos a swap. Con 16 GB y un modelo
31
+ # de 1.2 GB sobra memoria, asΓ­ que fijarlo en RAM es gratis y elimina
32
+ # pausas aleatorias a mitad de generaciΓ³n.
33
+ llama-server \
34
+ --model "$GGUF" \
35
+ --host 127.0.0.1 \
36
+ --port "$LLAMA_PORT" \
37
+ --ctx-size "$N_CTX" \
38
+ --threads "$N_THREADS" \
39
+ --n-gpu-layers 0 \
40
+ --cache-type-k q8_0 \
41
+ --cache-type-v q8_0 \
42
+ --cont-batching \
43
+ --parallel 1 \
44
+ --mlock \
45
+ 2>&1 | tee "$LOG" | sed 's/^/[BITNET] /' &
46
+
47
+ PID=$!
48
+ echo "[START] PID=$PID β€” cargando (BitNet carga rΓ‘pido: ~20-40s)"
49
+
50
+ MAX=300; T=0
51
+ while [ $T -lt $MAX ]; do
52
+ if ! kill -0 $PID 2>/dev/null; then
53
+ echo "[START] llama-server muriΓ³:"; tail -30 "$LOG" | sed 's/^/[CRASH] /'; exit 1
54
+ fi
55
+ if curl -sf "http://127.0.0.1:${LLAMA_PORT}/health" >/dev/null 2>&1; then
56
+ echo "[START] Worker ${WORKER_ID} ONLINE en ${T}s"; break
57
+ fi
58
+ sleep 5; T=$((T+5))
59
+ [ $((T % 30)) -eq 0 ] && echo "[START] ${T}/${MAX}s..."
60
+ done
61
+
62
+ if [ $T -ge $MAX ]; then
63
+ echo "[START] FATAL: timeout"; tail -20 "$LOG" | sed 's/^/[TIMEOUT] /'; exit 1
64
+ fi
65
+
66
+ echo "[START] FastAPI en :${PORT}"
67
+ exec uvicorn server:app --host 0.0.0.0 --port "$PORT" --workers 1 --log-level info