teszenofficial commited on
Commit
0eeeaa1
·
verified ·
1 Parent(s): dc89c5a

Upload 6 files

Browse files
Files changed (4) hide show
  1. Dockerfile +6 -1
  2. README.md +1 -1
  3. app.py +37 -3
  4. milox_model.py +81 -19
Dockerfile CHANGED
@@ -1,9 +1,14 @@
1
  FROM python:3.11-slim
2
 
3
  ENV PYTHONUNBUFFERED=1 \
 
4
  PIP_NO_CACHE_DIR=1 \
 
5
  HF_HOME=/home/user/.cache/huggingface \
6
- OMP_NUM_THREADS=2
 
 
 
7
 
8
  RUN useradd -m -u 1000 user
9
  WORKDIR /home/user/app
 
1
  FROM python:3.11-slim
2
 
3
  ENV PYTHONUNBUFFERED=1 \
4
+ PYTHONDONTWRITEBYTECODE=1 \
5
  PIP_NO_CACHE_DIR=1 \
6
+ PIP_DISABLE_PIP_VERSION_CHECK=1 \
7
  HF_HOME=/home/user/.cache/huggingface \
8
+ TOKENIZERS_PARALLELISM=false \
9
+ MALLOC_ARENA_MAX=2
10
+
11
+ # Los hilos de PyTorch los fija app.py según las vCPU reales del contenedor (variable TORCH_THREADS para forzarlos).
12
 
13
  RUN useradd -m -u 1000 user
14
  WORKDIR /home/user/app
README.md CHANGED
@@ -96,7 +96,7 @@ Los errores usan el formato de OpenAI: `{"error": {"message", "type", "param", "
96
  | `MAX_TOKENS_CAP` | `1024` | Tope de `max_tokens` |
97
  | `DEFAULT_MAX_TOKENS` | `512` | Valor si no envías `max_tokens` |
98
  | `ALLOWED_ORIGINS` | `*` | Orígenes CORS (coma). Pon el dominio de tu app en producción |
99
- | `TORCH_THREADS` | núm. de CPUs | Hilos de PyTorch |
100
 
101
  ## Cosas que debes saber
102
 
 
96
  | `MAX_TOKENS_CAP` | `1024` | Tope de `max_tokens` |
97
  | `DEFAULT_MAX_TOKENS` | `512` | Valor si no envías `max_tokens` |
98
  | `ALLOWED_ORIGINS` | `*` | Orígenes CORS (coma). Pon el dominio de tu app en producción |
99
+ | `TORCH_THREADS` | vCPU reales del contenedor | Hilos de PyTorch (se detectan por cgroup, no por los núcleos del servidor) |
100
 
101
  ## Cosas que debes saber
102
 
app.py CHANGED
@@ -10,6 +10,7 @@ Endpoints:
10
  Docs interactivas: /docs
11
  """
12
  import asyncio
 
13
  import json
14
  import os
15
  import sys
@@ -57,15 +58,46 @@ class State:
57
  STATE = State()
58
 
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  def load_model():
61
  for fn in ("model.safetensors", "milox_config.json", "tokenizer.json"):
62
  path = hf_hub_download(REPO_ID, fn, token=HF_TOKEN)
63
  model_dir = os.path.dirname(path)
64
- torch.set_num_threads(int(os.getenv("TORCH_THREADS", os.cpu_count() or 2)))
 
 
 
 
 
 
65
  device = "cuda" if torch.cuda.is_available() else "cpu"
66
  dtype = torch.float16 if device == "cuda" else torch.float32
67
  model = mm.load_milox(model_dir, device=device, dtype=dtype, int8=(USE_INT8 and device == "cpu"))
68
  tok = mm.ChatTokenizer(os.path.join(model_dir, "tokenizer.json"))
 
69
  return model, tok, device
70
 
71
 
@@ -271,7 +303,7 @@ class StopHit(Exception):
271
  pass
272
 
273
 
274
- def run_generation(ids, p, stops, stop_ids, on_text, cancel):
275
  """Se ejecuta en un hilo. Llama on_text(delta) con texto nuevo. Devuelve dict(text, completion_tokens, finish)."""
276
  tok, model = STATE.tok, STATE.model
277
  if p["seed"] is not None:
@@ -287,6 +319,8 @@ def run_generation(ids, p, stops, stop_ids, on_text, cancel):
287
  if cancel.is_set():
288
  raise Abort()
289
  out.append(t)
 
 
290
  text = tok.decode(out)
291
  if text.endswith("\ufffd"): # carácter UTF-8 incompleto: espera al siguiente token
292
  return
@@ -336,7 +370,7 @@ async def respond(kind, ids, p, stops, stream, include_usage):
336
  parts = []
337
  try:
338
  async with STATE.lock:
339
- res = await asyncio.to_thread(run_generation, ids, p, stops, stop_ids, parts.append, cancel)
340
  except asyncio.CancelledError:
341
  cancel.set()
342
  raise
 
10
  Docs interactivas: /docs
11
  """
12
  import asyncio
13
+ import gc
14
  import json
15
  import os
16
  import sys
 
58
  STATE = State()
59
 
60
 
61
+ def effective_cpus():
62
+ """vCPUs realmente disponibles para este contenedor. os.cpu_count() devuelve los nucleos del servidor
63
+ anfitrion (p. ej. 8-48) aunque el Space solo tenga 2 vCPU; usar ese numero de hilos satura la CPU
64
+ y hace la generacion MUCHO mas lenta."""
65
+ n = os.cpu_count() or 2
66
+ try:
67
+ n = min(n, len(os.sched_getaffinity(0)))
68
+ except Exception: # noqa: BLE001
69
+ pass
70
+ try: # cgroup v2
71
+ quota, period = open("/sys/fs/cgroup/cpu.max").read().split()[:2]
72
+ if quota != "max":
73
+ n = min(n, max(1, int(float(quota) / float(period) + 0.5)))
74
+ except Exception: # noqa: BLE001
75
+ try: # cgroup v1
76
+ quota = int(open("/sys/fs/cgroup/cpu/cpu.cfs_quota_us").read())
77
+ period = int(open("/sys/fs/cgroup/cpu/cpu.cfs_period_us").read())
78
+ if quota > 0:
79
+ n = min(n, max(1, int(quota / period + 0.5)))
80
+ except Exception: # noqa: BLE001
81
+ pass
82
+ return max(1, n)
83
+
84
+
85
  def load_model():
86
  for fn in ("model.safetensors", "milox_config.json", "tokenizer.json"):
87
  path = hf_hub_download(REPO_ID, fn, token=HF_TOKEN)
88
  model_dir = os.path.dirname(path)
89
+ threads = int(os.getenv("TORCH_THREADS", effective_cpus()))
90
+ torch.set_num_threads(threads)
91
+ try:
92
+ torch.set_num_interop_threads(1) # no hace falta paralelismo entre operaciones
93
+ except Exception: # noqa: BLE001
94
+ pass
95
+ print(f"Hilos de PyTorch: {threads}", flush=True)
96
  device = "cuda" if torch.cuda.is_available() else "cpu"
97
  dtype = torch.float16 if device == "cuda" else torch.float32
98
  model = mm.load_milox(model_dir, device=device, dtype=dtype, int8=(USE_INT8 and device == "cpu"))
99
  tok = mm.ChatTokenizer(os.path.join(model_dir, "tokenizer.json"))
100
+ gc.collect() # libera temporales de la carga
101
  return model, tok, device
102
 
103
 
 
303
  pass
304
 
305
 
306
+ def run_generation(ids, p, stops, stop_ids, on_text, cancel, live=True):
307
  """Se ejecuta en un hilo. Llama on_text(delta) con texto nuevo. Devuelve dict(text, completion_tokens, finish)."""
308
  tok, model = STATE.tok, STATE.model
309
  if p["seed"] is not None:
 
319
  if cancel.is_set():
320
  raise Abort()
321
  out.append(t)
322
+ if not live and not stops: # respuesta no-stream sin stop: se decodifica una sola vez al final
323
+ return
324
  text = tok.decode(out)
325
  if text.endswith("\ufffd"): # carácter UTF-8 incompleto: espera al siguiente token
326
  return
 
370
  parts = []
371
  try:
372
  async with STATE.lock:
373
+ res = await asyncio.to_thread(run_generation, ids, p, stops, stop_ids, parts.append, cancel, False)
374
  except asyncio.CancelledError:
375
  cancel.set()
376
  raise
milox_model.py CHANGED
@@ -133,6 +133,22 @@ def apply_rope(q, k, cos, sin):
133
  return q * cos + rotate_half(q) * sin, k * cos + rotate_half(k) * sin
134
 
135
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  class Attention(nn.Module):
137
  def __init__(self, c: MiloXConfig):
138
  super().__init__()
@@ -152,6 +168,31 @@ class Attention(nn.Module):
152
  k = self.k_proj(x).view(B, T, self.nkv, self.hd).transpose(1, 2)
153
  v = self.v_proj(x).view(B, T, self.nkv, self.hd).transpose(1, 2)
154
  q, k = apply_rope(q, k, cos, sin)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  if past is not None:
156
  k = torch.cat([past[0], k], dim=2)
157
  v = torch.cat([past[1], v], dim=2)
@@ -213,7 +254,7 @@ def _blk_fwd(blk, x, cos, sin):
213
 
214
 
215
  class MiloX(nn.Module):
216
- def __init__(self, cfg: MiloXConfig):
217
  super().__init__()
218
  self.cfg = cfg
219
  self.model = Backbone(cfg)
@@ -224,11 +265,12 @@ class MiloX(nn.Module):
224
  self.register_buffer("rope_cos", cos, persistent=False)
225
  self.register_buffer("rope_sin", sin, persistent=False)
226
  self.grad_ckpt = False
227
- self.apply(self._init_weights)
228
- std = 0.02 / math.sqrt(2 * cfg.n_layers)
229
- for n, p in self.named_parameters():
230
- if n.endswith("o_proj.weight") or n.endswith("down_proj.weight"):
231
- nn.init.normal_(p, mean=0.0, std=std)
 
232
 
233
  @staticmethod
234
  def _init_weights(m):
@@ -245,7 +287,7 @@ class MiloX(nn.Module):
245
 
246
  def forward(self, idx, targets=None, past=None, use_cache=False, full_logits=False):
247
  B, T = idx.shape
248
- off = 0 if past is None else past[0][0].shape[2]
249
  assert off + T <= self.cfg.max_seq_len, f"Secuencia demasiado larga ({off + T} > {self.cfg.max_seq_len})"
250
  cos = self.rope_cos[off:off + T]
251
  sin = self.rope_sin[off:off + T]
@@ -268,30 +310,37 @@ class MiloX(nn.Module):
268
  loss = F.cross_entropy(logits.float().view(-1, logits.size(-1)), targets.reshape(-1), ignore_index=-100)
269
  return logits, loss, presents
270
 
271
- @torch.no_grad()
272
  def generate(self, ids, max_new_tokens=256, temperature=0.7, top_k=40, top_p=0.9,
273
  repetition_penalty=1.1, stop_ids=(), on_token=None):
274
  """Genera tokens con KV-cache. `ids` es una lista de ints. Devuelve la lista de tokens nuevos."""
275
  self.eval()
276
- device = self.model.embed_tokens.weight.device
 
277
  max_len = self.cfg.max_seq_len
278
  if len(ids) >= max_len:
279
  ids = ids[-(max_len - 1):]
 
 
 
280
  cur = torch.tensor([ids], dtype=torch.long, device=device)
281
- past, out, seen = None, [], list(ids[-256:])
282
  stop_ids = set(stop_ids)
283
- block = [i for i in range(N_SPECIAL) if i not in stop_ids]
 
 
 
 
284
  for _ in range(max_new_tokens):
285
- total = cur.shape[1] + (0 if past is None else past[0][0].shape[2])
286
  if total > max_len:
287
  break
288
  logits, _, past = self(cur, past=past, use_cache=True)
289
  logits = logits[0, -1].float()
290
  logits[block] = float("-inf")
291
- if repetition_penalty != 1.0 and seen:
292
- sidx = torch.tensor(sorted(set(seen)), device=device)
293
- sc = logits[sidx]
294
- logits[sidx] = torch.where(sc > 0, sc / repetition_penalty, sc * repetition_penalty)
295
  if temperature <= 0:
296
  nxt = int(torch.argmax(logits))
297
  else:
@@ -310,7 +359,7 @@ class MiloX(nn.Module):
310
  if nxt in stop_ids:
311
  break
312
  out.append(nxt)
313
- seen.append(nxt)
314
  if on_token is not None:
315
  on_token(nxt)
316
  cur = torch.tensor([[nxt]], dtype=torch.long, device=device)
@@ -410,11 +459,24 @@ class ChatSession:
410
  def load_milox(path, device="cpu", dtype=torch.float32, int8=False):
411
  from safetensors.torch import load_file
412
  cfg = MiloXConfig.from_json(os.path.join(path, "milox_config.json"))
413
- model = MiloX(cfg)
414
  sd = load_file(os.path.join(path, "model.safetensors"))
415
  if cfg.tie_embeddings and "lm_head.weight" not in sd:
416
  sd["lm_head.weight"] = sd["model.embed_tokens.weight"]
417
- model.load_state_dict(sd, strict=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  model.eval()
419
  if int8:
420
  try:
 
133
  return q * cos + rotate_half(q) * sin, k * cos + rotate_half(k) * sin
134
 
135
 
136
+ class LayerCache:
137
+ """KV-cache preasignado por capa: escribe en un buffer fijo en vez de hacer torch.cat en cada token
138
+ (menos copias de memoria, menos fragmentacion de RAM). Solo se usa al generar (batch = 1)."""
139
+ __slots__ = ("k", "v", "n")
140
+
141
+ def __init__(self, n_kv, cap, hd, dtype, device):
142
+ self.k = torch.empty(1, n_kv, cap, hd, dtype=dtype, device=device)
143
+ self.v = torch.empty(1, n_kv, cap, hd, dtype=dtype, device=device)
144
+ self.n = 0
145
+
146
+
147
+ def _past_len(past):
148
+ p0 = past[0]
149
+ return p0.n if isinstance(p0, LayerCache) else p0[0].shape[2]
150
+
151
+
152
  class Attention(nn.Module):
153
  def __init__(self, c: MiloXConfig):
154
  super().__init__()
 
168
  k = self.k_proj(x).view(B, T, self.nkv, self.hd).transpose(1, 2)
169
  v = self.v_proj(x).view(B, T, self.nkv, self.hd).transpose(1, 2)
170
  q, k = apply_rope(q, k, cos, sin)
171
+ if isinstance(past, LayerCache):
172
+ n0 = past.n
173
+ past.k[:, :, n0:n0 + T] = k
174
+ past.v[:, :, n0:n0 + T] = v
175
+ past.n = n0 + T
176
+ k = past.k[:, :, :past.n]
177
+ v = past.v[:, :, :past.n]
178
+ if T == 1 and n0 > 0:
179
+ # Decodificacion token a token: GQA sin repetir K/V. Las cabezas de q de un mismo grupo
180
+ # (h = j*rep + r) se apilan en la dimension de consulta; es el mismo calculo, sin copiar
181
+ # rep veces toda la cache en cada token y en cada capa.
182
+ qg = q.reshape(B, self.nkv, self.rep, self.hd)
183
+ y = F.scaled_dot_product_attention(qg, k, v)
184
+ return self.o_proj(y.reshape(B, 1, self.nh * self.hd)), past
185
+ if self.rep > 1:
186
+ k = k.repeat_interleave(self.rep, dim=1)
187
+ v = v.repeat_interleave(self.rep, dim=1)
188
+ if n0 == 0:
189
+ y = F.scaled_dot_product_attention(q, k, v, is_causal=True)
190
+ else:
191
+ S = k.shape[2]
192
+ mask = torch.ones(T, S, dtype=torch.bool, device=x.device).tril(diagonal=S - T)
193
+ y = F.scaled_dot_product_attention(q, k, v, attn_mask=mask)
194
+ y = y.transpose(1, 2).contiguous().view(B, T, self.nh * self.hd)
195
+ return self.o_proj(y), past
196
  if past is not None:
197
  k = torch.cat([past[0], k], dim=2)
198
  v = torch.cat([past[1], v], dim=2)
 
254
 
255
 
256
  class MiloX(nn.Module):
257
+ def __init__(self, cfg: MiloXConfig, init_weights=True):
258
  super().__init__()
259
  self.cfg = cfg
260
  self.model = Backbone(cfg)
 
265
  self.register_buffer("rope_cos", cos, persistent=False)
266
  self.register_buffer("rope_sin", sin, persistent=False)
267
  self.grad_ckpt = False
268
+ if init_weights: # al cargar un modelo exportado no hace falta (los pesos se sobrescriben)
269
+ self.apply(self._init_weights)
270
+ std = 0.02 / math.sqrt(2 * cfg.n_layers)
271
+ for n, p in self.named_parameters():
272
+ if n.endswith("o_proj.weight") or n.endswith("down_proj.weight"):
273
+ nn.init.normal_(p, mean=0.0, std=std)
274
 
275
  @staticmethod
276
  def _init_weights(m):
 
287
 
288
  def forward(self, idx, targets=None, past=None, use_cache=False, full_logits=False):
289
  B, T = idx.shape
290
+ off = 0 if past is None else _past_len(past)
291
  assert off + T <= self.cfg.max_seq_len, f"Secuencia demasiado larga ({off + T} > {self.cfg.max_seq_len})"
292
  cos = self.rope_cos[off:off + T]
293
  sin = self.rope_sin[off:off + T]
 
310
  loss = F.cross_entropy(logits.float().view(-1, logits.size(-1)), targets.reshape(-1), ignore_index=-100)
311
  return logits, loss, presents
312
 
313
+ @torch.inference_mode()
314
  def generate(self, ids, max_new_tokens=256, temperature=0.7, top_k=40, top_p=0.9,
315
  repetition_penalty=1.1, stop_ids=(), on_token=None):
316
  """Genera tokens con KV-cache. `ids` es una lista de ints. Devuelve la lista de tokens nuevos."""
317
  self.eval()
318
+ emb = self.model.embed_tokens.weight
319
+ device, dtype = emb.device, emb.dtype
320
  max_len = self.cfg.max_seq_len
321
  if len(ids) >= max_len:
322
  ids = ids[-(max_len - 1):]
323
+ cap = min(max_len, len(ids) + max_new_tokens) # cache del tamano justo, no del maximo
324
+ hd = self.cfg.dim // self.cfg.n_heads
325
+ past = [LayerCache(self.cfg.n_kv_heads, cap, hd, dtype, device) for _ in range(self.cfg.n_layers)]
326
  cur = torch.tensor([ids], dtype=torch.long, device=device)
327
+ out = []
328
  stop_ids = set(stop_ids)
329
+ block = torch.tensor([i for i in range(N_SPECIAL) if i not in stop_ids], dtype=torch.long, device=device)
330
+ # tokens "vistos" para la penalizacion (ultimos 256 del prompt + todo lo generado), como mascara fija
331
+ seen = torch.zeros(self.cfg.vocab_size, dtype=torch.bool, device=device)
332
+ if ids:
333
+ seen[torch.tensor(ids[-256:], dtype=torch.long, device=device)] = True
334
  for _ in range(max_new_tokens):
335
+ total = cur.shape[1] + _past_len(past)
336
  if total > max_len:
337
  break
338
  logits, _, past = self(cur, past=past, use_cache=True)
339
  logits = logits[0, -1].float()
340
  logits[block] = float("-inf")
341
+ if repetition_penalty != 1.0:
342
+ logits = torch.where(seen, torch.where(logits > 0, logits / repetition_penalty,
343
+ logits * repetition_penalty), logits)
 
344
  if temperature <= 0:
345
  nxt = int(torch.argmax(logits))
346
  else:
 
359
  if nxt in stop_ids:
360
  break
361
  out.append(nxt)
362
+ seen[nxt] = True
363
  if on_token is not None:
364
  on_token(nxt)
365
  cur = torch.tensor([[nxt]], dtype=torch.long, device=device)
 
459
  def load_milox(path, device="cpu", dtype=torch.float32, int8=False):
460
  from safetensors.torch import load_file
461
  cfg = MiloXConfig.from_json(os.path.join(path, "milox_config.json"))
 
462
  sd = load_file(os.path.join(path, "model.safetensors"))
463
  if cfg.tie_embeddings and "lm_head.weight" not in sd:
464
  sd["lm_head.weight"] = sd["model.embed_tokens.weight"]
465
+ try:
466
+ # Carga eficiente en RAM: el modelo se crea "vacio" (meta) y adopta directamente los tensores del
467
+ # archivo; evita inicializar pesos al azar y tener 2-3 copias del modelo a la vez durante el arranque.
468
+ with torch.device("meta"):
469
+ model = MiloX(cfg, init_weights=False)
470
+ model.load_state_dict(sd, strict=True, assign=True)
471
+ cos, sin = build_rope(cfg.dim // cfg.n_heads, cfg.max_seq_len, cfg.rope_theta)
472
+ model.rope_cos, model.rope_sin = cos, sin
473
+ if cfg.tie_embeddings:
474
+ model.lm_head.weight = model.model.embed_tokens.weight
475
+ except Exception as e: # torch antiguo u otro problema: camino clasico
476
+ print("Carga eficiente no disponible, se usa la carga clasica:", e)
477
+ model = MiloX(cfg, init_weights=False)
478
+ model.load_state_dict(sd, strict=True)
479
+ del sd
480
  model.eval()
481
  if int8:
482
  try: