hugpy-wrapper 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1298 @@
1
+ """A minimal llama.cpp adapter STUB — the concrete seam, not a working engine.
2
+
3
+ It shows how `EngineHost.footprint` would price a GGUF from its typed structure
4
+ following llama.cpp's placement rules (clean-room; no hugpy import):
5
+
6
+ * `--n-gpu-layers L` puts the top L of `block_count` blocks on the GPU; the
7
+ output tensor rides the GPU only when all layers do.
8
+ * `--n-cpu-moe N` keeps the expert tensors of the first N blocks on the CPU;
9
+ every other tensor of a GPU block stays on the GPU.
10
+ * KV rides its block (a GPU layer holds its KV slice on the GPU).
11
+
12
+ `gguf_footprint` is pure math over a structure dict; a real adapter would read
13
+ that structure from the file and implement `measure` / `list_residents` /
14
+ `evict` / `load` against the running engine. Only the pricing seam is made
15
+ concrete here, deliberately — geometry belongs in the adapter, never the core.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from typing import Optional
20
+
21
+ from ..types import ModelFootprint
22
+
23
+ _GPU_MARGIN = 1.15
24
+ _RAM_MARGIN = 1.15
25
+ _DTYPE_BYTES = 2.0
26
+ _COMPUTE_CUSHION_BYTES = 512 * 2 ** 20
27
+
28
+
29
+ def gguf_footprint(struct: dict, *, ctx_tokens: int, n_gpu_layers: int = -1,
30
+ n_cpu_moe: int = 0, ctx_pct: Optional[int] = None,
31
+ ctx_deviation_pct: Optional[float] = None) -> ModelFootprint:
32
+ """Price a GGUF load into a `ModelFootprint`.
33
+
34
+ `struct` carries the typed per-layer bytes the way a GGUF inspector would
35
+ report them::
36
+
37
+ {"block_count": int,
38
+ "token_embd_bytes": int, "output_bytes": int, "global_bytes": int,
39
+ "is_moe": bool, "output_tied": bool,
40
+ "kv_elems_per_token": int, # full-attention KV elems / token / layer
41
+ "layers": {i: {"always_bytes": int, "expert_bytes": int}}}
42
+
43
+ The returned footprint's `weights_bytes`/`kv_bytes` are the GPU-side figures
44
+ (what admission gates on); `expert_bytes`/`non_expert_bytes` carry the MoE
45
+ split so the planner can reason about an expert spill."""
46
+ layers = {int(k): v for k, v in (struct.get("layers") or {}).items()}
47
+ n_block = int(struct.get("block_count") or len(layers))
48
+ ngl = int(n_gpu_layers if n_gpu_layers is not None else -1)
49
+ if ngl < 0 or ngl > n_block:
50
+ start, out_on_gpu = 0, True
51
+ else:
52
+ start, out_on_gpu = n_block - ngl, False
53
+ gpu_layers = {i for i in layers if i >= start} if ngl != 0 else set()
54
+
55
+ is_moe = bool(struct.get("is_moe"))
56
+ ncm = max(0, int(n_cpu_moe or 0)) if is_moe else 0
57
+
58
+ gw = rw = experts_gpu = experts_cpu = non_expert = 0
59
+ for i, L in layers.items():
60
+ a = int(L.get("always_bytes") or 0)
61
+ e = int(L.get("expert_bytes") or 0)
62
+ non_expert += a
63
+ if i in gpu_layers:
64
+ gw += a
65
+ if i < ncm:
66
+ rw += e
67
+ experts_cpu += e
68
+ else:
69
+ gw += e
70
+ experts_gpu += e
71
+ else:
72
+ rw += a + e
73
+ experts_cpu += e
74
+
75
+ embd = int(struct.get("token_embd_bytes") or 0)
76
+ outb = int(struct.get("output_bytes") or 0)
77
+ glob = int(struct.get("global_bytes") or 0)
78
+ non_expert += embd + outb + glob
79
+ rw += embd # input layer is always on the CPU
80
+ if out_on_gpu:
81
+ gw += outb + glob + (embd if struct.get("output_tied") else 0)
82
+ else:
83
+ rw += outb + glob
84
+
85
+ # KV: full-attention per-token elems * ctx * dtype, shared by GPU layers.
86
+ per_tok = int(struct.get("kv_elems_per_token") or 0)
87
+ kv_total = int(ctx_tokens * per_tok * _DTYPE_BYTES)
88
+ n_gpu = len(gpu_layers)
89
+ kv_gpu = int(kv_total * (n_gpu / float(n_block))) if n_block else 0
90
+ cushion = _COMPUTE_CUSHION_BYTES if (gpu_layers and is_moe) else 0
91
+
92
+ gpu_weights = int(gw * _GPU_MARGIN)
93
+ gpu_total = gpu_weights + kv_gpu + cushion
94
+ return ModelFootprint(
95
+ weights_bytes=gpu_weights,
96
+ kv_bytes=kv_gpu,
97
+ total_bytes=gpu_total,
98
+ layers=n_block,
99
+ moe=is_moe,
100
+ expert_bytes=int(experts_gpu + experts_cpu) or None,
101
+ non_expert_bytes=int(non_expert) or None,
102
+ ctx_pct=ctx_pct,
103
+ ctx_deviation_pct=ctx_deviation_pct,
104
+ )
105
+
106
+
107
+ # ─────────────────────────────────────────────────────────────────────────────
108
+ # LlamaCppHost — the concrete EngineHost that RUNS llama-server children.
109
+ #
110
+ # GLUE (new code, the only kind the task allows here): it maps the LIFTED results
111
+ # onto fitevict's types and executes drive()'s plan. Every fact comes from a
112
+ # lifted function or a live measurement — nothing is hardcoded:
113
+ # * footprint <- lifted gguf_inspect.gguf_structure + fit.gguf_need.gguf_need
114
+ # * measure <- lifted hardware.detect_gpus + spill reserve/free_* closure
115
+ # * resolution <- lifted model_resolve (is_model_dir / get_gguf_file / marker)
116
+ # * binary <- lifted native_resolve.server_bin
117
+ # * launch <- lifted procutil.popen_detached, argv per slot_agent._build_cmd
118
+ # * stop <- lifted procutil.terminate_tree
119
+ # * timings <- lifted timings.tok_s_from_timings / generation_split
120
+ # ─────────────────────────────────────────────────────────────────────────────
121
+ import json as _json
122
+ import os as _os
123
+ import socket as _socket
124
+ import subprocess as _subprocess
125
+ import time as _time
126
+ import urllib.request as _urllib
127
+ import uuid as _uuid
128
+ from dataclasses import dataclass, field
129
+ from typing import Dict, List, Optional
130
+
131
+ from ..types import (
132
+ DeviceBudget, FitPlan, HostBudget, ModelFootprint, Resident, VRAM,
133
+ )
134
+ from ..lifted import (
135
+ hardware as _hw,
136
+ model_resolve as _mr,
137
+ native_resolve as _nr,
138
+ spill_reserve as _spill,
139
+ timings as _tm,
140
+ )
141
+ from ..lifted.gguf_need import gguf_need as _gguf_need, structure_for as _structure_for
142
+
143
+
144
+ def discover_models(models_home: Optional[str] = None) -> "Dict[str, str]":
145
+ """{model_key: gguf_path} for every GGUF model dir on disk, via the lifted
146
+ legacy resolution (walk MODELS_HOME, keep dirs is_model_dir() accepts with a
147
+ model GGUF get_gguf_file() elects). model_key = the repo dir basename, or
148
+ ``owner~repo`` when two owners hold a repo of the same name (hugpy's key
149
+ convention). A vision projector is never elected as the model. This box's
150
+ discovery is THE catalog: the front door publishes it to the DB."""
151
+ from ..lifted.constants import MODELS_HOME
152
+ root = _os.path.join(models_home or MODELS_HOME, "gguf")
153
+ found: "list[tuple[str, str, str]]" = []
154
+ if not _os.path.isdir(root):
155
+ return {}
156
+ for owner in sorted(_os.listdir(root)):
157
+ od = _os.path.join(root, owner)
158
+ if not _os.path.isdir(od):
159
+ continue
160
+ for repo in sorted(_os.listdir(od)):
161
+ rd = _os.path.join(od, repo)
162
+ if not _os.path.isdir(rd) or not _mr.is_model_dir(rd):
163
+ continue
164
+ g = _mr.get_gguf_file(rd)
165
+ if g and ("/mmproj/" in g or _os.path.basename(g).lower().startswith("mmproj")):
166
+ g = None
167
+ if g:
168
+ found.append((owner, repo, g))
169
+ # transformers chat dirs (the other engine hugpy serves chat with)
170
+ from . import transformers as _tf
171
+ for owner, repo, d in _tf.discover(models_home or MODELS_HOME):
172
+ found.append((owner, repo, d))
173
+ counts: Dict[str, int] = {}
174
+ for _o, repo, _g in found:
175
+ counts[repo] = counts.get(repo, 0) + 1
176
+ out: Dict[str, str] = {}
177
+ for owner, repo, g in found:
178
+ out[f"{owner}~{repo}" if counts[repo] > 1 else repo] = g
179
+ return out
180
+
181
+
182
+ def engine_of_path(path: Optional[str]) -> str:
183
+ """'gguf' for a GGUF file, 'transformers' for a model dir."""
184
+ return "gguf" if path and str(path).lower().endswith(".gguf") else "transformers"
185
+
186
+
187
+ def model_weights(key: str, gguf_path: str) -> dict:
188
+ """Discovery detail for one model: its directory, the GGUF the loader elects,
189
+ and EVERY weight file in the directory (quant + bytes; the mmproj projector
190
+ flagged). Pure filesystem facts — nothing is opened or priced."""
191
+ from ..lifted.gguf_election import quant_token
192
+ if engine_of_path(gguf_path) == "transformers":
193
+ from ..lifted import tf_facts as _tff
194
+ d = gguf_path
195
+ weights = []
196
+ for root, _dn, files in _os.walk(d):
197
+ for f in sorted(files):
198
+ if f.endswith(_tff.WEIGHT_EXT):
199
+ p = _os.path.join(root, f)
200
+ try:
201
+ size = _os.path.getsize(p)
202
+ except OSError:
203
+ size = None
204
+ weights.append({"file": _os.path.relpath(p, d), "path": p, "quant": None,
205
+ "bytes": size, "mmproj": False, "elected": True})
206
+ mp = _os.path.join(d, "hugpy.json")
207
+ hj, hje = None, None
208
+ try:
209
+ with open(mp, "r", encoding="utf-8") as fh:
210
+ hj = _json.load(fh)
211
+ except (OSError, ValueError) as exc:
212
+ hje = f"{type(exc).__name__}: {exc}"
213
+ return {"dir": d, "path": d, "hugpy_json": hj, "hugpy_json_error": hje,
214
+ "file": "dir", "quant": "dir", "engine": "transformers",
215
+ "bytes": _tff.dir_bytes(d), "mmproj": None, "weights": weights}
216
+ d = _os.path.dirname(gguf_path)
217
+ while d and _os.path.basename(d) != key and _os.path.dirname(d) != d:
218
+ d = _os.path.dirname(d)
219
+ if _os.path.basename(d) != key:
220
+ d = _os.path.dirname(gguf_path)
221
+ weights = []
222
+ for g in _mr.get_guffs_in_dir(d):
223
+ try:
224
+ size = _os.path.getsize(g)
225
+ except OSError:
226
+ size = None
227
+ weights.append({"file": _os.path.relpath(g, d), "path": g,
228
+ "quant": quant_token(g) or None, "bytes": size,
229
+ "mmproj": _mr.is_mmproj_file(g),
230
+ "elected": g == gguf_path})
231
+ elected = next((w for w in weights if w["elected"]), None)
232
+ hugpy_json, hugpy_json_error = None, None
233
+ mp = _os.path.join(d, "hugpy.json")
234
+ if _os.path.isfile(mp):
235
+ try:
236
+ with open(mp, "r", encoding="utf-8") as fh:
237
+ hugpy_json = _json.load(fh)
238
+ except (OSError, ValueError) as exc:
239
+ hugpy_json_error = f"{type(exc).__name__}: {exc}"
240
+ else:
241
+ hugpy_json_error = "no hugpy.json in the model dir"
242
+ return {"dir": d, "path": gguf_path, "hugpy_json": hugpy_json,
243
+ "hugpy_json_error": hugpy_json_error,
244
+ "file": _os.path.basename(gguf_path),
245
+ "quant": (elected or {}).get("quant"),
246
+ "bytes": (elected or {}).get("bytes"),
247
+ "mmproj": _mr.find_mmproj(gguf_path),
248
+ "weights": weights}
249
+
250
+
251
+ def _compute_apps_vram() -> "Dict[int, int]":
252
+ """{pid: used_vram_bytes} — EXACT bytes from NVML (lifted.nvml); nvidia-smi's
253
+ whole-MiB rows only when NVML is unavailable."""
254
+ try:
255
+ from ..lifted import nvml as _nvml
256
+ procs = _nvml.processes()
257
+ if procs is not None:
258
+ return {int(pid): int(r["bytes"]) for pid, r in procs.items()}
259
+ except Exception: # noqa: BLE001
260
+ pass
261
+ smi = _hw.resolve_bin("nvidia-smi")
262
+ if not smi:
263
+ return {}
264
+ try:
265
+ out = _subprocess.check_output(
266
+ [smi, "--query-compute-apps=pid,used_memory",
267
+ "--format=csv,noheader,nounits"],
268
+ stderr=_subprocess.DEVNULL, timeout=10).decode("utf-8", "replace")
269
+ except Exception: # noqa: BLE001
270
+ return {}
271
+ res: Dict[int, int] = {}
272
+ for line in out.strip().splitlines():
273
+ parts = [p.strip() for p in line.split(",")]
274
+ if len(parts) >= 2 and parts[0].isdigit() and parts[1].isdigit():
275
+ res[int(parts[0])] = int(parts[1]) * 1024 * 1024
276
+ return res
277
+
278
+
279
+ _SERVER_FLAG_CACHE: dict = {}
280
+
281
+
282
+ def _server_supports_flag(server_bin, flag) -> bool:
283
+ """Lifted from slot_agent._server_supports_flag: probe `llama-server --help`
284
+ once and cache whether `flag` appears. Can't probe -> assume unsupported."""
285
+ key = (str(server_bin), str(flag))
286
+ cached = _SERVER_FLAG_CACHE.get(key)
287
+ if cached is not None:
288
+ return cached
289
+ ok = False
290
+ try:
291
+ out = _subprocess.run([server_bin, "--help"], capture_output=True,
292
+ text=True, timeout=15)
293
+ ok = flag in ((out.stdout or "") + (out.stderr or ""))
294
+ except Exception: # noqa: BLE001
295
+ ok = False
296
+ _SERVER_FLAG_CACHE[key] = ok
297
+ return ok
298
+
299
+
300
+ def _proc_rss_bytes(pid: int) -> Optional[int]:
301
+ try:
302
+ with open(f"/proc/{pid}/status") as fh:
303
+ for line in fh:
304
+ if line.startswith("VmRSS:"):
305
+ return int(line.split()[1]) * 1024
306
+ except Exception: # noqa: BLE001
307
+ return None
308
+ return None
309
+
310
+
311
+ def _gpu_changed(reason: str) -> None:
312
+ """Tell the live GPU-state publisher the card just changed (fitevict.gpu_state)."""
313
+ try:
314
+ from .. import gpu_state as _gs
315
+ _gs.changed(reason)
316
+ except Exception: # noqa: BLE001 — telemetry never stops a load/evict
317
+ pass
318
+
319
+
320
+ # llama-server parallel slots per model (operator 2026-10-03: one): a hybrid
321
+ # model's recurrent state is allocated PER SLOT (4 slots = 4x the state).
322
+ _PARALLEL = int(_os.environ.get("FITEVICT_PARALLEL", "1"))
323
+
324
+
325
+ def _db_kv_structure(worker, file):
326
+ from . import db as _db
327
+ return _db.get_kv_structure(worker, file)
328
+
329
+
330
+ def _db_get_driver(worker, device):
331
+ from . import db as _db
332
+ return int(_db.get_cuda_context(worker, device + ":driver") or 0)
333
+
334
+
335
+ # the request whose placement is evicting (set by the front door under its placement lock)
336
+ _CURRENT_REQUEST: dict = {"id": None}
337
+
338
+
339
+ @dataclass
340
+ class _Child:
341
+ key: str
342
+ proc: object
343
+ port: int
344
+ model_file: str
345
+ struct: dict
346
+ need: dict
347
+ n_gpu_layers: int
348
+ n_cpu_moe: Optional[int]
349
+ ctx: int
350
+ total_layers: Optional[int]
351
+ is_moe: bool
352
+ quant: str
353
+ mmproj: Optional[str]
354
+ resident_since: float
355
+ weights_staged_at: Optional[float] = None
356
+ weights_loaded_at: Optional[float] = None
357
+ load_error: Optional[str] = None
358
+ last_used: Optional[float] = None
359
+ calls: int = 0
360
+ vram_at_load: Optional[int] = None
361
+ argv: Optional[list] = None
362
+ contract: Optional[dict] = None
363
+
364
+ @property
365
+ def base_url(self) -> str:
366
+ return f"http://127.0.0.1:{self.port}"
367
+
368
+
369
+ _HOSTROWS = __import__("re").compile(r"\|\s*-\s*([A-Za-z0-9_]+)\s*\|\s*(\d+)\s*=\s*(\d+)\s*\+\s*(\d+)\s*\+\s*(\d+)")
370
+
371
+
372
+ def host_rows_first_block(text):
373
+ """Every host-side row (Host, CPU_REPACK, ...) of llama.cpp's FIRST memory
374
+ breakdown (the initial full config), summed: [total, model, context, compute]
375
+ in MiB, or None. CUDA rows have the '= free + (self = ...)' shape and never match.
376
+ CPU_REPACK (weights repacked for the CPU kernels, anonymous memory) is its own
377
+ row — reading only '- Host' missed 35,450 MiB on Qwen3-Coder-Next (2026-10-03)."""
378
+ i = text.find("memory breakdown")
379
+ j = text.find("projected to use", i if i >= 0 else 0)
380
+ if i < 0 or j < 0:
381
+ return None
382
+ rows = _HOSTROWS.findall(text[i:j])
383
+ if not rows:
384
+ return None
385
+ out = [sum(int(r[k]) for r in rows) for k in (1, 2, 3, 4)]
386
+ out.append(sum(int(r[2]) for r in rows if r[0] != "Host")) # [4] repacked weights (CPU_REPACK, ...)
387
+ return out
388
+
389
+
390
+ class LlamaCppHost:
391
+ """Concrete `EngineHost`: measures the box, prices GGUFs, launches/stops
392
+ llama-server children, and lists the children IT started as residents."""
393
+
394
+ def __init__(self, ctx: int = 4096, main_gpu: int = 0,
395
+ log_dir: Optional[str] = None):
396
+ self.ctx = int(ctx)
397
+ self.main_gpu = int(main_gpu)
398
+ self.children: "Dict[str, _Child]" = {}
399
+ self.models: Dict[str, str] = {}
400
+ self._models_published = False
401
+ self.refresh_models()
402
+ self.contracts: Dict[str, dict] = {}
403
+ # Host constraints handed down by the harness (never knobs): a ceiling
404
+ # on what this front door may hold, and seats it must never evict.
405
+ self.gpu_limit: Optional[int] = None
406
+ self.ram_limit: Optional[int] = None
407
+ self.protected: set = set()
408
+ # 🔒static models (DB: the worker's config.residency) — never evicted
409
+ # once loaded; refreshed before every placement.
410
+ self.static_keys: set = set()
411
+ self.server_bin = _nr.server_bin()
412
+ self.log_dir = log_dir or _os.path.join(
413
+ _os.path.expanduser("~"), ".fitevict", "run-logs")
414
+ _os.makedirs(self.log_dir, exist_ok=True)
415
+ self._port_next = int(_os.environ.get("FITEVICT_PORT_BASE", "8190"))
416
+
417
+ # ── measurement ─────────────────────────────────────────────────────────
418
+ def measure(self) -> HostBudget:
419
+ gpus = _hw.detect_gpus()
420
+ g0 = gpus[self.main_gpu] if len(gpus) > self.main_gpu else {}
421
+ total = _hw.total_vram_bytes(self.main_gpu) or g0.get("memory_total")
422
+ free = _spill.free_vram_bytes() # budgetable (reserve removed)
423
+ if free is None:
424
+ free = g0.get("memory_free")
425
+ # a dry-run projection probe (HUGPY_PROBE) is transient — its bytes and
426
+ # its G7 driver bytes are not taken from the room a placement sees
427
+ if free is not None:
428
+ try:
429
+ from .. import gpu_state as _gs
430
+ procs = _compute_apps_vram()
431
+ drv = int(_db_get_driver(self._worker(), self._device_key()) or 0)
432
+ free = int(free) + sum(int(b) + drv for pid, b in procs.items() if b and _gs._is_probe(int(pid)))
433
+ except Exception: # noqa: BLE001
434
+ pass
435
+ ram_free = _spill.free_ram_bytes()
436
+ if self.gpu_limit is not None or self.ram_limit is not None:
437
+ apps = _compute_apps_vram()
438
+ mine_vram = mine_ram = 0
439
+ for ch in self.children.values():
440
+ if self._alive(ch):
441
+ pid = getattr(ch.proc, "pid", None)
442
+ mine_vram += int(apps.get(pid) or 0)
443
+ mine_ram += int(_proc_rss_bytes(pid) or 0) if pid else 0
444
+ if self.gpu_limit is not None:
445
+ total = min(int(total or self.gpu_limit), int(self.gpu_limit))
446
+ room = max(0, int(self.gpu_limit) - mine_vram)
447
+ free = room if free is None else min(int(free), room)
448
+ if self.ram_limit is not None:
449
+ room = max(0, int(self.ram_limit) - mine_ram)
450
+ ram_free = room if ram_free is None else min(int(ram_free), room)
451
+ dev = DeviceBudget(id=self.main_gpu, total_bytes=total, free_bytes=free,
452
+ external_floor_bytes=_spill.vram_reserve_bytes())
453
+ ram_total = None
454
+ try:
455
+ import psutil
456
+ ram_total = int(psutil.virtual_memory().total)
457
+ except Exception: # noqa: BLE001
458
+ ram_total = None
459
+ return HostBudget(devices=(dev,), ram_free_bytes=ram_free,
460
+ ram_total_bytes=ram_total,
461
+ ram_reserve_bytes=_spill.ram_reserve_bytes(),
462
+ now=_time.time())
463
+
464
+ def refresh_static(self) -> None:
465
+ from . import db as _db
466
+ got = _db.static_keys(_os.environ.get("FITEVICT_WORKER", "ae"))
467
+ if got is not None:
468
+ self.static_keys = got
469
+
470
+ def refresh_models(self) -> int:
471
+ """Re-walk this box's disk (the catalog's source) and publish it to the
472
+ DB (inference_engine.models) whenever it changed."""
473
+ found = discover_models()
474
+ if found != self.models or not self._models_published:
475
+ self.models = found
476
+ from . import db as _db
477
+ rows = []
478
+ for key, path in sorted(found.items()):
479
+ w = model_weights(key, path)
480
+ files = w.get("files") or w.get("weights") or []
481
+ rows.append({"key": key, "path": path, "model_dir": w.get("dir"),
482
+ "quant": w.get("quant"), "engine": engine_of_path(path),
483
+ "bytes": (_os.path.getsize(path) if _os.path.exists(path) else None),
484
+ "files": files})
485
+ worker = _os.environ.get("FITEVICT_WORKER", "ae")
486
+ self._models_published = _db.write_models(worker, rows)
487
+ return len(self.models)
488
+
489
+ def list_residents(self) -> "List[Resident]":
490
+ apps = _compute_apps_vram()
491
+ res: List[Resident] = []
492
+ for key, ch in list(self.children.items()):
493
+ if not self._alive(ch):
494
+ continue
495
+ pid = getattr(ch.proc, "pid", None)
496
+ vram = apps.get(pid)
497
+ if vram is not None:
498
+ ch.vram_at_load = vram
499
+ res.append(Resident(
500
+ key=key, device_id=self.main_gpu, vram_bytes=vram,
501
+ ram_bytes=_proc_rss_bytes(pid) if pid else None,
502
+ last_used=ch.last_used, calls=ch.calls, pref=VRAM,
503
+ resident_since=ch.resident_since,
504
+ protected=key in self.protected or key in self.static_keys))
505
+ return res
506
+
507
+ # ── pricing ──────────────────────────────────────────────────────────────
508
+ # ── per-call load contract (the caller's knobs; never stored past the call) ─
509
+ def set_contract(self, key: str, knobs: Optional[dict]) -> dict:
510
+ """Resolve the caller's knobs for ``key`` into a load contract:
511
+ ``path`` (gguf_file inside the model's dir, else the elected file) and
512
+ ``moe`` (True = experts in RAM, False = no expert split, None = auto).
513
+ Unknown/unsafe knob values fall back to the default, never fail."""
514
+ knobs = knobs if isinstance(knobs, dict) else {}
515
+ path = self.models.get(key)
516
+ want = knobs.get("gguf_file")
517
+ if path and engine_of_path(path) == "gguf" and isinstance(want, str) and want.strip():
518
+ d = _os.path.dirname(path)
519
+ while d and _os.path.basename(d) != key and _os.path.dirname(d) != d:
520
+ d = _os.path.dirname(d)
521
+ if _os.path.basename(d) != key:
522
+ d = _os.path.dirname(path)
523
+ cand = _os.path.realpath(_os.path.join(d, want.strip()))
524
+ if cand.startswith(_os.path.realpath(d) + _os.sep) and _os.path.isfile(cand) \
525
+ and cand.lower().endswith(".gguf"):
526
+ path = cand
527
+ moe = knobs.get("moe")
528
+ mode = knobs.get("alloc_mode")
529
+ mode = mode if mode in ("gpu-only", "ram-only", "max-gpu", "max-ram", "explicit") else None
530
+
531
+ def _layers(v):
532
+ try:
533
+ return int(v) if v is not None and int(v) >= -1 else None
534
+ except (TypeError, ValueError):
535
+ return None
536
+ # ctx pin (ctx_pct of the trained window) -> the launch context; absent
537
+ # = the host default. 4-bit (bitsandbytes) applies to transformers only.
538
+ ctx = None
539
+ try:
540
+ pct = knobs.get("ctx_pct")
541
+ if pct is not None and path:
542
+ if engine_of_path(path) == "transformers":
543
+ from . import transformers as _tf
544
+ trained = int(_tf.geometry(path).get("ctx_train") or 0)
545
+ else:
546
+ trained = int((_structure_for(path) or {}).get("ctx_train") or 0)
547
+ if trained:
548
+ ctx = max(1024, (int(trained * float(pct) / 100) // 1024) * 1024)
549
+ except (TypeError, ValueError):
550
+ ctx = None
551
+ kvt = str(knobs.get("kv_cache_type") or "f16").lower()
552
+ kvt = kvt if kvt in ("f16", "q8_0", "q4_0") else "f16"
553
+ c = {"path": path, "moe": moe if isinstance(moe, bool) else None,
554
+ "alloc_mode": mode, "ctx": ctx, "kv_type": kvt,
555
+ "flash_attn": knobs.get("flash_attn") is True or kvt != "f16",
556
+ "bnb4": knobs.get("bnb_4bit") is True and bool(path) and engine_of_path(path) == "transformers",
557
+ # explicit placement in llama.cpp terms (legacy 0.2.6 explicit panel)
558
+ "n_gpu_layers": _layers(knobs.get("attention_gpu_layers", knobs.get("n_gpu_layers"))),
559
+ "n_cpu_moe": _layers(knobs.get("experts_cpu_layers", knobs.get("n_cpu_moe")))}
560
+ self.contracts[key] = c
561
+ return c
562
+
563
+ def apply_ctx(self, key: str, info: dict, knobs: Optional[dict]) -> Optional[int]:
564
+ """The launch context from the DB (operator 2026-10-03): a pinned ctx_pct
565
+ is that tick's own ctx (kv_cost.ticks), no pin = the history default
566
+ (model_default_ctx). Sets contract ctx / ctx_start; returns the ctx."""
567
+ c = self.contracts.get(key)
568
+ if c is None or not info:
569
+ return None
570
+ ticks = info.get("ticks") or {}
571
+ pct = (knobs or {}).get("ctx_pct")
572
+ ctx = None
573
+ if pct is not None:
574
+ t = ticks.get(str(max(1, min(100, int(pct))))) or {}
575
+ ctx = int(t["ctx"]) if t.get("ctx") else c.get("ctx")
576
+ c["ctx_source"] = "pinned"
577
+ elif info.get("default_ctx"):
578
+ ctx = int(info["default_ctx"])
579
+ c["ctx_source"] = f"history ({info.get('basis')}, largest used {info.get('need_tokens')} tokens)"
580
+ if ctx:
581
+ c["ctx"] = ctx
582
+ c["ctx_start"] = ctx
583
+ c["ctx_ladder"] = sorted({int(v["ctx"]) for v in ticks.values() if isinstance(v, dict) and v.get("ctx")})
584
+ c["ctx_need"] = info.get("need_tokens")
585
+ return ctx
586
+
587
+ def contract_of(self, key: str) -> dict:
588
+ return self.contracts.get(key) or {"path": self.models.get(key), "moe": None,
589
+ "alloc_mode": None, "n_gpu_layers": None,
590
+ "n_cpu_moe": None, "ctx": None, "bnb4": False,
591
+ "kv_type": "f16", "flash_attn": False}
592
+
593
+ def footprint(self, key: str, quant: Optional[str] = None) -> ModelFootprint:
594
+ c = self.contract_of(key)
595
+ path = c["path"]
596
+ if not path:
597
+ return ModelFootprint() # unknown -> planner fails open
598
+ if engine_of_path(path) == "transformers":
599
+ from . import transformers as _tf
600
+ f = _tf.footprint(path, c.get("ctx") or self.ctx, bnb4=bool(c.get("bnb4")),
601
+ worker=self._worker(), key=key)
602
+ return ModelFootprint(
603
+ weights_bytes=f["weights_bytes"] + f["compute_bytes"],
604
+ kv_bytes=f["kv_bytes"], total_bytes=f["total_bytes"],
605
+ layers=f["layers"], moe=False)
606
+ struct = _structure_for(path)
607
+ mmproj = _mr.find_mmproj(path)
608
+ mmproj_bytes = _os.path.getsize(mmproj) if mmproj and _os.path.isfile(mmproj) else 0
609
+ ngl, ncm = self._placement(c, struct)
610
+ ctx = c.get("ctx") or self.ctx
611
+ p = self.project(path, ctx, ngl, ncm, c.get("kv_type") or "f16", bool(c.get("flash_attn")))
612
+ cuda = 0 if ((key in self.children and self._alive(self.children[key])) or ngl == 0) \
613
+ else self.cuda_context_bytes() # RAM-only (ngl 0, device none) has no CUDA process
614
+ need = _gguf_need(struct, ctx=ctx, n_gpu_layers=ngl, n_cpu_moe=ncm, mmproj_bytes=mmproj_bytes,
615
+ gpu_margin=1.0, ram_margin=1.0, cushion_bytes=0)
616
+ if p: # formula sheet G2/G5 (fitevict.sizing): measured if this config ran, else page-rounded projection + context
617
+ from .. import sizing as _S
618
+ new_proc = bool(cuda)
619
+ drv = int(_db_get_driver(self._worker(), self._device_key())) if new_proc else 0
620
+ # KV: llama.cpp's own projected context buffers (KV + recurrent state).
621
+ # K1 is NOT used over a projection: on hybrid models it omits the
622
+ # recurrent-state buffers (measured 2026-10-03, Qwen3.6-35B-A3B ctx
623
+ # 169,984: K1 3,547,136,000 vs llama.cpp 3,744,464,896 -> OOM at load)
624
+ st = _db_kv_structure(self._worker(), _os.path.basename(path))
625
+ kvt_ = c.get("kv_type") or "f16"
626
+ kv_k2 = None
627
+ if st and ngl < 0 and not ncm: # K2 (measured structure) for whole-GPU loads
628
+ cell = int(st["k_cell_bytes"]) + int(st["v_cell_bytes"])
629
+ cell = cell if kvt_ == "f16" else cell * (17 if kvt_ == "q8_0" else 9) // 32
630
+ kv_k2 = _S.kv_device_bytes(ctx, cell, 0, int(st.get("rs_bytes") or 0))
631
+ pred = _S.gguf_predict(p, cuda, measured_process=p.get("measured_process") if new_proc else None,
632
+ kv_exact=kv_k2, driver=drv)
633
+ kv = int(p["dev_context"])
634
+ total = int(pred["device_bytes"]) + (0 if pred["exact"] else _S.page(mmproj_bytes))
635
+ # G8: what the process grows by once it serves (cuBLAS handle +
636
+ # workspace, pool growth) — must be free after the load
637
+ if ngl != 0:
638
+ from . import db as _db8
639
+ g8, g8_src = _db8.serving_growth(self._worker(), _os.path.basename(path), ctx, ngl, ncm,
640
+ c.get("kv_type") or "f16")
641
+ total += int(g8)
642
+ self.last_g8 = {"bytes": int(g8), "source": g8_src}
643
+ else: # llama.cpp could not project: the structure's exact tensor + KV bytes
644
+ kv = int(need["kv_gpu_bytes"])
645
+ total = int(need["gpu_bytes"]) + cuda
646
+ # R1: what a RAM-only load of this config holds in host memory
647
+ ram_only = None
648
+ try:
649
+ from .. import sizing as _S1
650
+ from . import db as _db1
651
+ kvt1 = c.get("kv_type") or "f16"
652
+ mh = _db1.get_measured_host(self._worker(), _os.path.basename(path), ctx, 0, 0, kvt1) or {}
653
+ p0 = None if (mh.get("anon") is not None) else self.project(path, ctx, 0, 0, kvt1, bool(c.get("flash_attn")))
654
+ ram_only = int(_S1.ram_only_bytes(p0, _S1.gguf_file_bytes(path) + mmproj_bytes,
655
+ mh.get("anon"), mh.get("file"))["bytes"])
656
+ except Exception as exc: # noqa: BLE001
657
+ print(f"[ram_only] {key}: {exc}")
658
+ return ModelFootprint(
659
+ weights_bytes=max(0, total - kv),
660
+ kv_bytes=kv,
661
+ total_bytes=total,
662
+ ram_only_bytes=ram_only,
663
+ layers=int(need["block_count"]) or None,
664
+ moe=bool(need["is_moe"]),
665
+ expert_bytes=int(need["experts_gpu_bytes"] + need["experts_cpu_bytes"]) or None,
666
+ non_expert_bytes=int(need["gpu_weights_bytes"]) or None,
667
+ )
668
+
669
+ def _placement(self, c: dict, struct: dict) -> "tuple[int, int]":
670
+ """(n_gpu_layers, n_cpu_moe) the contract asks for (before any partial plan)."""
671
+ ncm = int(struct.get("block_count") or 0) if (c.get("moe") and struct.get("is_moe")) else 0
672
+ ngl = -1
673
+ if c.get("alloc_mode") == "explicit":
674
+ if c.get("n_gpu_layers") is not None:
675
+ ngl = int(c["n_gpu_layers"])
676
+ if c.get("n_cpu_moe") is not None and struct.get("is_moe"):
677
+ ncm = int(c["n_cpu_moe"])
678
+ elif c.get("alloc_mode") == "ram-only":
679
+ ngl, ncm = 0, 0
680
+ return ngl, ncm
681
+
682
+ # ── exact sizing ──────────────────────────────────────────────────────────
683
+ def _worker(self) -> str:
684
+ return _os.environ.get("FITEVICT_WORKER", "ae")
685
+
686
+ def _device_key(self) -> str:
687
+ return f"cuda{self.main_gpu}"
688
+
689
+ def cuda_context_bytes(self) -> int:
690
+ """The MEASURED per-process CUDA context on this card (a new process pays
691
+ it); 0 until the first load on this box has measured it."""
692
+ from . import db as _db
693
+ return int(_db.get_cuda_context(self._worker(), self._device_key()) or 0)
694
+
695
+ def project(self, path: str, ctx: int, ngl: int, ncm: int, kv: str = "f16",
696
+ flash: bool = False) -> "Optional[dict]":
697
+ """llama.cpp's OWN projected device memory for this file at these params
698
+ (common_params_fit: model / context / compute buffers, no weights
699
+ loaded), cached in the DB. None when llama.cpp cannot project."""
700
+ from . import db as _db
701
+ file = _os.path.basename(path)
702
+ hit = _db.get_projection(self._worker(), file, ctx, ngl, ncm, kv)
703
+ if hit and hit.get("dev_self") is not None:
704
+ return hit
705
+ import re as _re, select as _select
706
+ port = self._free_port()
707
+ argv = [self.server_bin, "-m", path, "--host", "127.0.0.1", "--port", str(port),
708
+ "-c", str(int(ctx)), "--n-gpu-layers", "all" if int(ngl) < 0 else str(int(ngl)),
709
+ "-t", "6", "--fit", "on", "-lv", "4", "-np", str(_PARALLEL)]
710
+ if ncm:
711
+ argv += ["--n-cpu-moe", str(int(ncm))]
712
+ if kv != "f16":
713
+ argv += ["--cache-type-k", kv, "--cache-type-v", kv]
714
+ if flash or kv != "f16":
715
+ argv += ["-fa", "on"]
716
+ env = dict(_os.environ)
717
+ env["CUDA_VISIBLE_DEVICES"] = str(self.main_gpu)
718
+ env["HUGPY_PROBE"] = "projection" # gpu_state: a transient probe, never external use
719
+ if int(ngl) == 0: # RAM-only: no GPU at all
720
+ argv += ["-dev", "none"]
721
+ env["CUDA_VISIBLE_DEVICES"] = ""
722
+ from ..lifted.procutil import popen_detached, terminate_tree
723
+ proc = popen_detached(argv, stdout=_subprocess.PIPE, stderr=_subprocess.STDOUT, env=env)
724
+ dev = host = None
725
+ deadline = _time.time() + 120
726
+ row = _re.compile(r"CUDA\d+[^|]*\|.*?\(\s*(\d+)\s*=\s*(\d+)\s*\+\s*(\d+)\s*\+\s*(\d+)\s*\)")
727
+ hrow = _re.compile(r"-\s*Host\s*\|\s*(\d+)\s*=\s*(\d+)\s*\+\s*(\d+)\s*\+\s*(\d+)")
728
+ try:
729
+ buf = b""
730
+ while _time.time() < deadline and proc.poll() is None:
731
+ r, _, _ = _select.select([proc.stdout], [], [], 1.0)
732
+ if not r:
733
+ continue
734
+ chunk = _os.read(proc.stdout.fileno(), 65536)
735
+ if not chunk:
736
+ break
737
+ buf += chunk
738
+ text = buf.decode("utf-8", "replace")
739
+ if dev is None:
740
+ m = row.search(text)
741
+ if m:
742
+ dev = [int(x) for x in m.groups()]
743
+ if "projected to use" in text: # the first breakdown block is complete
744
+ host = host_rows_first_block(text)
745
+ break
746
+ finally:
747
+ try:
748
+ terminate_tree(proc)
749
+ proc.wait(timeout=20)
750
+ except Exception: # noqa: BLE001
751
+ pass
752
+ # wait until the driver has RELEASED the probe's device memory (NVML
753
+ # no longer lists it) — a placement must never price around a dying probe
754
+ _deadline = _time.time() + 30.0
755
+ while proc.pid in _compute_apps_vram() and _time.time() < _deadline:
756
+ _time.sleep(0.02)
757
+ if dev is None and int(ngl) == 0 and host is not None:
758
+ dev = [0, 0, 0, 0] # device none: nothing on the GPU
759
+ if dev is None:
760
+ return None
761
+ M = 2 ** 20
762
+ out = {"dev_self": dev[0] * M, "dev_model": dev[1] * M, "dev_context": dev[2] * M,
763
+ "dev_compute": dev[3] * M,
764
+ "host_model": host[1] * M if host else None, "host_context": host[2] * M if host else None,
765
+ "host_compute": host[3] * M if host else None,
766
+ "host_repack": host[4] * M if host else None}
767
+ _db.put_projection(self._worker(), file, ctx, ngl, ncm, kv, out)
768
+ return out
769
+
770
+ # ── execution ─────────────────────────────────────────────────────────────
771
+ def _alive(self, ch: _Child) -> bool:
772
+ p = ch.proc
773
+ return bool(p) and p.poll() is None
774
+
775
+ def evict(self, key: str, reason: str = "evict-to-fit") -> int:
776
+ """Stop ``key``'s process and MEASURE the unload: NVML process bytes and
777
+ device free before, the exit time, and the time the card's free memory
778
+ rose by those bytes (polled every 20 ms, up to 30 s). Recorded in
779
+ inference_engine.evictions; returns the bytes actually freed."""
780
+ ch = self.children.get(key)
781
+ if ch is None:
782
+ return 0
783
+ from ..lifted.procutil import terminate_tree
784
+ from ..lifted import nvml as _nvml
785
+ from . import db as _db
786
+
787
+ def _free():
788
+ for d in (_nvml.devices() or []):
789
+ if d["index"] == int(self.main_gpu):
790
+ return d.get("free")
791
+ return None
792
+ pid = getattr(ch.proc, "pid", None)
793
+ held = _compute_apps_vram().get(pid)
794
+ free_before = _free()
795
+ started = _time.time()
796
+ terminate_tree(ch.proc)
797
+ try:
798
+ ch.proc.wait(timeout=30)
799
+ except Exception: # noqa: BLE001
800
+ pass
801
+ exited = _time.time()
802
+ released = None
803
+ free_after = _free()
804
+ if held and free_before is not None:
805
+ deadline = exited + 30.0
806
+ while free_after is not None and free_after - free_before < int(held) and _time.time() < deadline:
807
+ _time.sleep(0.02)
808
+ free_after = _free()
809
+ if free_after is not None and free_after - free_before >= int(held):
810
+ released = _time.time()
811
+ elif not held:
812
+ released = exited # nothing on the GPU (RAM-only) — released at exit
813
+ self.children.pop(key, None)
814
+ _gpu_changed(f"evict {key}")
815
+ try: # an eviction frees room: missing sizing facts may be measurable now
816
+ from .. import gpu_state as _gsw
817
+ _gsw.FACTS_WAKE.set()
818
+ except Exception: # noqa: BLE001
819
+ pass
820
+ try:
821
+ _db.put_eviction({"worker": self._worker(), "key": key,
822
+ "engine": engine_of_path(ch.model_file), "request_id": _CURRENT_REQUEST.get("id"),
823
+ "reason": reason, "pid": pid, "process_bytes": held,
824
+ "free_before": free_before, "free_after": free_after,
825
+ "started_at": started, "exited_at": exited, "released_at": released})
826
+ except Exception as exc: # noqa: BLE001
827
+ print(f"[eviction] record failed: {exc}")
828
+ if free_before is not None and free_after is not None:
829
+ freed = max(0, int(free_after) - int(free_before))
830
+ if held and released and freed > int(held): # G7: driver bytes per process
831
+ try:
832
+ _db.put_cuda_context(self._worker(), self._device_key() + ":driver", freed - int(held))
833
+ except Exception: # noqa: BLE001
834
+ pass
835
+ return freed
836
+ return int(held or 0)
837
+
838
+ def load(self, key: str, plan: FitPlan) -> None:
839
+ """Launch a llama-server child per the committed plan. argv flag mapping
840
+ follows the lifted slot_agent._build_cmd reference."""
841
+ contract = dict(self.contract_of(key))
842
+ path = contract["path"] or self.models[key]
843
+ if engine_of_path(path) == "transformers":
844
+ return self._load_transformers(key, path, plan, contract)
845
+ struct = _structure_for(path)
846
+ mmproj = _mr.find_mmproj(path)
847
+ mmproj_bytes = _os.path.getsize(mmproj) if mmproj and _os.path.isfile(mmproj) else 0
848
+
849
+ # Placement from the plan: partial -> the offload's layer/expert split;
850
+ # otherwise the whole model on the GPU (-ngl -1), no forced expert split.
851
+ n_gpu_layers = -1
852
+ n_cpu_moe: Optional[int] = None
853
+ if getattr(plan, "placement", None) == "ram":
854
+ n_gpu_layers = 0 # the plan put 0 B on the card
855
+ elif plan.action == "partial" and plan.offload is not None:
856
+ n_gpu_layers = int(plan.offload.n_gpu_layers)
857
+ n_cpu_moe = plan.offload.n_cpu_moe
858
+ # Caller's MoE knob: True = every layer on the card with ALL experts in
859
+ # RAM; False = never split experts (a partial plan keeps its layer split).
860
+ if contract.get("moe") is True and struct.get("is_moe") and n_gpu_layers != 0:
861
+ n_gpu_layers, n_cpu_moe = -1, int(struct.get("block_count") or 0) or None
862
+ elif contract.get("moe") is False:
863
+ n_cpu_moe = None
864
+ # explicit allocation: the operator's exact layer counts (llama.cpp terms)
865
+ if contract.get("alloc_mode") == "explicit":
866
+ if contract.get("n_gpu_layers") is not None:
867
+ n_gpu_layers = int(contract["n_gpu_layers"])
868
+ if contract.get("n_cpu_moe") is not None and struct.get("is_moe"):
869
+ n_cpu_moe = int(contract["n_cpu_moe"]) or None
870
+ elif contract.get("alloc_mode") == "ram-only" or contract.get("decided_mode") == "ram-only":
871
+ n_gpu_layers, n_cpu_moe = 0, None # pinned, or sizing.decide() chose RAM-only
872
+ ctx = contract.get("ctx") or self.ctx
873
+ need = _gguf_need(struct, ctx=ctx, n_gpu_layers=n_gpu_layers,
874
+ n_cpu_moe=n_cpu_moe or 0, mmproj_bytes=mmproj_bytes,
875
+ gpu_margin=1.0, ram_margin=1.0, cushion_bytes=0)
876
+
877
+ kvt0 = contract.get("kv_type") or "f16"
878
+ port = self._free_port()
879
+ # Flag mapping follows the lifted slot_agent._build_cmd reference: when
880
+ # the binary is --fit capable, turn its own auto-fit OFF (fitevict is the
881
+ # fit authority now) and pass -ngl "all" for the whole-model case.
882
+ fit_capable = _server_supports_flag(self.server_bin, "--fit")
883
+ ngl_arg = "all" if (fit_capable and n_gpu_layers == -1) else str(n_gpu_layers)
884
+ # max-gpu (pinned): llama.cpp's own --fit picks the layer split that fits
885
+ # the card, keeping a DERIVED margin = G8 (serving growth) + G7 (driver
886
+ # bytes per process) + one PAGE per device buffer (model, KV, RS,
887
+ # compute: the page rounding llama.cpp's raw sizes do not include)
888
+ fit_solver = fit_capable and contract.get("alloc_mode") == "max-gpu" and n_gpu_layers != 0 \
889
+ and not n_cpu_moe
890
+ argv = [self.server_bin, "-m", path,
891
+ "--host", "127.0.0.1", "--port", str(port)]
892
+ if not fit_solver:
893
+ argv += ["--n-gpu-layers", ngl_arg]
894
+ argv += ["-c", str(ctx), "-t", "6", "-np", str(_PARALLEL)]
895
+ if fit_solver:
896
+ from . import db as _dbf
897
+ from .. import sizing as _Sf
898
+ g8 = _dbf.serving_growth(self._worker(), _os.path.basename(path), ctx, -1, 0, kvt0)[0]
899
+ g7 = int(_db_get_driver(self._worker(), self._device_key()) or 0)
900
+ margin = int(g8) + g7 + 4 * _Sf.PAGE
901
+ argv += ["--fit", "on", "-fitt", str(-(-margin // _Sf.MiB))]
902
+ elif fit_capable:
903
+ argv += ["--fit", "off"]
904
+ argv += ["-lv", "4"] # formula sheet G1 source B: per-buffer sizes in the load log
905
+ if n_cpu_moe and _server_supports_flag(self.server_bin, "--n-cpu-moe"):
906
+ argv += ["--n-cpu-moe", str(int(n_cpu_moe))]
907
+ if mmproj:
908
+ argv += ["--mmproj", mmproj]
909
+ kvt = contract.get("kv_type") or "f16"
910
+ if kvt != "f16":
911
+ argv += ["--cache-type-k", kvt, "--cache-type-v", kvt]
912
+ if contract.get("flash_attn") or kvt != "f16":
913
+ argv += ["-fa", "on"]
914
+
915
+ if n_gpu_layers == 0: # RAM-only: no GPU at all (no CUDA process, no device buffers)
916
+ argv += ["-dev", "none"]
917
+ logf = open(_os.path.join(self.log_dir, f"{key}.{port}.log"), "ab")
918
+ from ..lifted.procutil import popen_detached
919
+ env = dict(_os.environ)
920
+ if self.main_gpu is not None:
921
+ env["CUDA_VISIBLE_DEVICES"] = str(self.main_gpu)
922
+ if n_gpu_layers == 0:
923
+ env["CUDA_VISIBLE_DEVICES"] = ""
924
+ staged = _time.time()
925
+ proc = popen_detached(argv, stdout=logf, stderr=logf, env=env)
926
+ ch = _Child(key=key, proc=proc, port=port, model_file=path, struct=struct,
927
+ need=need, n_gpu_layers=n_gpu_layers, n_cpu_moe=n_cpu_moe,
928
+ ctx=ctx, total_layers=int(need["block_count"]) or None,
929
+ is_moe=bool(need["is_moe"]), quant=_os.path.basename(path),
930
+ mmproj=mmproj, resident_since=staged)
931
+ ch.weights_staged_at = staged
932
+ ch.argv = list(argv)
933
+ ch.contract = contract
934
+ self.children[key] = ch
935
+ ok, err = self._wait_healthy(ch)
936
+ if ok and fit_solver: # the split llama.cpp chose: "offloaded X/Y layers to GPU"
937
+ try:
938
+ import re as _ref
939
+ with open(_os.path.join(self.log_dir, f"{key}.{port}.log"), "r", encoding="utf-8", errors="replace") as fh:
940
+ m = list(_ref.finditer(r"offloaded (\d+)/(\d+) layers to GPU", fh.read()))
941
+ if m:
942
+ ch.n_gpu_layers = n_gpu_layers = int(m[-1].group(1))
943
+ except Exception: # noqa: BLE001
944
+ pass
945
+ if ok:
946
+ ch.weights_loaded_at = _time.time()
947
+ self._measure_cuda_context(ch, path, ctx, n_gpu_layers, n_cpu_moe or 0, kvt,
948
+ bool(contract.get("flash_attn")), mmproj_bytes)
949
+ if n_gpu_layers == 0: # R1: RAM-only host bytes, measured
950
+ try:
951
+ from . import db as _dbh
952
+ rss = {}
953
+ with open(f"/proc/{proc.pid}/status") as fh:
954
+ for line in fh:
955
+ if line.startswith(("RssAnon:", "RssFile:")):
956
+ rss[line.split(":")[0]] = int(line.split()[1]) * 1024
957
+ if rss:
958
+ _dbh.put_measured_host(self._worker(), _os.path.basename(path), ctx, 0, 0, kvt,
959
+ rss.get("RssAnon", 0), rss.get("RssFile", 0))
960
+ except Exception as exc: # noqa: BLE001
961
+ print(f"[host] measure failed: {exc}")
962
+ else:
963
+ ch.load_error = err
964
+ _gpu_changed(f"load {key}")
965
+
966
+ def _load_transformers(self, key: str, path: str, plan: FitPlan, contract: dict) -> None:
967
+ """Launch the lifted tf_child for a transformers model per the plan:
968
+ ram -> CPU; partial -> the plan's GPU share, the rest in RAM; else the
969
+ whole model on the card (device_map=auto capped to it, no CPU spill)."""
970
+ from . import transformers as _tf
971
+ ctx = contract.get("ctx") or self.ctx
972
+ bnb4 = bool(contract.get("bnb4"))
973
+ f = _tf.footprint(path, ctx, bnb4=bnb4, worker=self._worker(), key=key)
974
+ mode = contract.get("alloc_mode")
975
+ if getattr(plan, "placement", None) == "ram" or mode == "ram-only" or contract.get("decided_mode") == "ram-only":
976
+ placement, gpu_b = "ram", None
977
+ elif plan.action == "partial" and plan.offload is not None and f["layers"]:
978
+ placement = "split"
979
+ frac = max(0, int(plan.offload.n_gpu_layers)) / float(f["layers"])
980
+ gpu_b = int(f["weights_bytes"] * frac)
981
+ else:
982
+ placement = "gpu"
983
+ gpu_b = f["total_bytes"]
984
+ port = self._free_port()
985
+ argv = _tf.argv(key, path, port, ctx, placement=placement, gpu_bytes=gpu_b, bnb4=bnb4)
986
+ logf = open(_os.path.join(self.log_dir, f"{key}.{port}.log"), "ab")
987
+ from ..lifted.procutil import popen_detached
988
+ env = dict(_os.environ)
989
+ if self.main_gpu is not None and placement != "ram":
990
+ env["CUDA_VISIBLE_DEVICES"] = str(self.main_gpu)
991
+ if placement == "ram":
992
+ env["CUDA_VISIBLE_DEVICES"] = ""
993
+ staged = _time.time()
994
+ proc = popen_detached(argv, stdout=logf, stderr=logf, env=env)
995
+ layers = f["layers"]
996
+ ngl = 0 if placement == "ram" else (-1 if placement == "gpu"
997
+ else int(plan.offload.n_gpu_layers))
998
+ ch = _Child(key=key, proc=proc, port=port, model_file=path, struct={},
999
+ need={"gpu_bytes": gpu_b or 0, "kv_gpu_bytes": f["kv_bytes"] if placement != "ram" else 0,
1000
+ "gpu_weights_bytes": f["weights_bytes"] if placement == "gpu" else (gpu_b or 0)},
1001
+ n_gpu_layers=ngl, n_cpu_moe=None, ctx=ctx, total_layers=layers,
1002
+ is_moe=False, quant="dir-bnb4" if bnb4 else "dir", mmproj=None, resident_since=staged)
1003
+ ch.weights_staged_at = staged
1004
+ ch.argv = list(argv)
1005
+ ch.contract = contract
1006
+ self.children[key] = ch
1007
+ ok, err = self._wait_healthy(ch, hard_cap=1800.0, stall=180.0)
1008
+ if ok:
1009
+ ch.weights_loaded_at = _time.time()
1010
+ if placement != "ram":
1011
+ try:
1012
+ from . import db as _db
1013
+ used = _compute_apps_vram().get(getattr(proc, "pid", None))
1014
+ if used:
1015
+ _db.put_tf_measure(self._worker(), key, bnb4, ctx,
1016
+ tensor_bytes=f["weights_bytes"], load_bytes=int(used))
1017
+ if int(used) > f["weights_bytes"]:
1018
+ _db.put_cuda_context(self._worker(), "torch", int(used) - f["weights_bytes"])
1019
+ except Exception as exc: # noqa: BLE001
1020
+ print(f"[tf_measure] {key}: {exc}")
1021
+ else:
1022
+ ch.load_error = err
1023
+ _gpu_changed(f"load {key}")
1024
+
1025
+ def record_host(self, key: str) -> None:
1026
+ """R1 after a call: a RAM-only resident's RssAnon / RssFile grow as mmap'd
1027
+ pages fault in — keep the configuration's MAX measured host bytes."""
1028
+ ch = self.children.get(key)
1029
+ cfg = self.config_of(key)
1030
+ if ch is None or cfg is None or cfg[2] != 0:
1031
+ return
1032
+ from . import db as _db
1033
+ rss = {}
1034
+ try:
1035
+ with open(f"/proc/{ch.proc.pid}/status") as fh:
1036
+ for line in fh:
1037
+ if line.startswith(("RssAnon:", "RssFile:")):
1038
+ rss[line.split(":")[0]] = int(line.split()[1]) * 1024
1039
+ except OSError:
1040
+ return
1041
+ old = _db.get_measured_host(self._worker(), *cfg) or {}
1042
+ anon = max(rss.get("RssAnon", 0), int(old.get("anon") or 0))
1043
+ filb = max(rss.get("RssFile", 0), int(old.get("file") or 0))
1044
+ if (anon, filb) != (old.get("anon"), old.get("file")):
1045
+ _db.put_measured_host(self._worker(), *cfg, anon, filb)
1046
+
1047
+ def live_room(self) -> "tuple[Optional[int], Optional[int]]":
1048
+ """(room_gpu, room_ram) NOW by the shared formulas (sizing.room_gpu /
1049
+ room_ram — central computes the console's room with the same ones)."""
1050
+ from .. import sizing as _S
1051
+ from .. import gpu_state as _gs
1052
+ from ..lifted import nvml as _nvml
1053
+ rg = rr = None
1054
+ try:
1055
+ d = next((x for x in (_nvml.devices() or []) if x["index"] == int(self.main_gpu)), None)
1056
+ procs = _compute_apps_vram()
1057
+ ours = {getattr(c.proc, "pid", None) for c in self.children.values() if self._alive(c)}
1058
+ static = {getattr(c.proc, "pid", None) for k, c in self.children.items()
1059
+ if self._alive(c) and k in (getattr(self, "static_keys", None) or set())}
1060
+ probes = {p for p in procs if _gs._is_probe(int(p))}
1061
+ if d and d.get("total") is not None:
1062
+ rg = _S.room_gpu({"usable": int(d["total"]) - int(d.get("reserved") or 0), "used": int(d["used"]),
1063
+ "processes": procs}, ours - {None}, static - {None}, probes,
1064
+ int(_db_get_driver(self._worker(), self._device_key()) or 0))
1065
+ mem = {}
1066
+ with open("/proc/meminfo") as fh:
1067
+ for line in fh:
1068
+ k, _, v = line.partition(":")
1069
+ if v.split():
1070
+ mem[k] = int(v.split()[0]) * 1024
1071
+ anon = {}
1072
+ for pid in ours - {None}:
1073
+ try:
1074
+ with open(f"/proc/{pid}/status") as fh:
1075
+ for line in fh:
1076
+ if line.startswith("RssAnon:"):
1077
+ anon[str(pid)] = int(line.split()[1]) * 1024
1078
+ except OSError:
1079
+ pass
1080
+ rr = _S.room_ram(mem.get("MemAvailable", 0), anon, static - {None})
1081
+ except Exception as exc: # noqa: BLE001
1082
+ print(f"[room] {exc}")
1083
+ return rg, rr
1084
+
1085
+ def ensure_facts(self, key: str, facts: dict, kv: str = "f16") -> bool:
1086
+ """Measure the projection points decide() is missing: llama.cpp dry runs
1087
+ at the ladder's two ends for the GPU (ngl -1) and RAM-only (ngl 0) —
1088
+ every tick in between is linear from them. One-off per quant (facts are
1089
+ keyed to the exact configuration and never go stale). Returns True when
1090
+ anything was measured."""
1091
+ c = self.contract_of(key)
1092
+ path = c.get("path") or self.models.get(key)
1093
+ ladder = sorted(int(x) for x in (facts.get("ladder") or []))
1094
+ if not path or not ladder or engine_of_path(path) == "transformers":
1095
+ return False
1096
+ have_g = {int(p["ctx"]) for p in facts.get("gpu_points") or [] if p.get("model") is not None}
1097
+ have_r = {int(p["ctx"]) for p in facts.get("ram_points") or [] if p.get("host_repack") is not None}
1098
+ did = False
1099
+ for ctx in {ladder[0], ladder[-1]}:
1100
+ if ctx not in have_g:
1101
+ did |= self.project(path, ctx, -1, 0, kv) is not None
1102
+ if ctx not in have_r:
1103
+ did |= self.project(path, ctx, 0, 0, kv) is not None
1104
+ return did
1105
+
1106
+ def config_of(self, key: str) -> "Optional[tuple]":
1107
+ """(file, ctx, n_gpu_layers, n_cpu_moe, kv) of a resident — the key every
1108
+ measured term (G3/G5/G8) is stored under."""
1109
+ ch = self.children.get(key)
1110
+ if ch is None or engine_of_path(ch.model_file) == "transformers":
1111
+ return None
1112
+ kvt = (getattr(ch, "contract", None) or {}).get("kv_type") or "f16"
1113
+ return (_os.path.basename(ch.model_file), int(ch.ctx), int(ch.n_gpu_layers), int(ch.n_cpu_moe or 0), kvt)
1114
+
1115
+ def measure_full_ctx(self, key: str) -> "Optional[dict]":
1116
+ """Formula sheet G8/A: send the resident ONE request that fills its whole
1117
+ context, through the SAME path and parameters real traffic uses
1118
+ (self.chat -> /v1/chat/completions; measured 2026-10-03: the raw
1119
+ /completion path grows the process 2,097,152 B less than chat does).
1120
+ Prompt = the chat template + k copies of one token, k = n_ctx − template
1121
+ tokens − 1 (the single generated token takes the last position). Then
1122
+ read the NVML process bytes; stored on the configuration's row."""
1123
+ import json as _json, urllib.request as _ur
1124
+ from . import db as _db
1125
+ ch = self.children.get(key)
1126
+ cfg = self.config_of(key)
1127
+ if ch is None or cfg is None or cfg[2] == 0:
1128
+ return None
1129
+ pid = getattr(ch.proc, "pid", None)
1130
+ base = ch.base_url
1131
+
1132
+ def _post(path, body, timeout):
1133
+ req = _ur.Request(base + path, _json.dumps(body).encode(), {"Content-Type": "application/json"})
1134
+ return _json.loads(_ur.urlopen(req, timeout=timeout).read())
1135
+ props = _json.loads(_ur.urlopen(base + "/props", timeout=10).read())
1136
+ n_ctx = int((props.get("default_generation_settings") or {}).get("n_ctx") or ch.ctx)
1137
+ empty = _post("/apply-template", {"messages": [{"role": "user", "content": ""}]}, 10).get("prompt") or ""
1138
+ overhead = len(_post("/tokenize", {"content": empty, "parse_special": True}, 10).get("tokens") or [])
1139
+ one = _post("/tokenize", {"content": " word word"}, 10).get("tokens") or []
1140
+ if len(one) != 2 or one[0] != one[1]:
1141
+ return None # " word" is not one token here: cannot size exactly
1142
+ k = n_ctx - overhead - 1
1143
+ # repeat the fill until the process stops growing: measured 2026-10-03,
1144
+ # the SECOND full-context request adds one more 2 MiB allocation, then
1145
+ # it plateaus — the max is reached when two readings agree
1146
+ before = _compute_apps_vram().get(pid)
1147
+ t0 = _time.time()
1148
+ readings = []
1149
+ while True:
1150
+ res = self.chat(key, [{"role": "user", "content": " word" * k}], max_tokens=1)
1151
+ if not res.get("ok"):
1152
+ return None
1153
+ readings.append(_compute_apps_vram().get(pid))
1154
+ if len(readings) >= 2 and readings[-1] == readings[-2]:
1155
+ break
1156
+ secs = _time.time() - t0
1157
+ after = readings[-1]
1158
+ if not after:
1159
+ return None
1160
+ used = res.get("prompt_tokens")
1161
+ rid = "g8-" + __import__("uuid").uuid4().hex[:16]
1162
+ _db.put_full_ctx(self._worker(), *cfg, int(after), int(used or k + overhead), secs, rid)
1163
+ return {"request_id": rid, "prompt_tokens": used, "n_ctx": n_ctx, "template_tokens": overhead,
1164
+ "readings": readings, "fills": len(readings),
1165
+ "prefill_s": secs, "process_before": before, "process_after": after, "config": cfg}
1166
+
1167
+ def _measure_cuda_context(self, ch, path, ctx, ngl, ncm, kv, flash, mmproj_bytes) -> None:
1168
+ """Formula sheet G3 + G5: process = NVML bytes of the new process;
1169
+ context = process − Σ page(device buffers from its load log)."""
1170
+ try:
1171
+ from .. import sizing as _S
1172
+ from . import db as _db
1173
+ pid = getattr(ch.proc, "pid", None)
1174
+ used = _compute_apps_vram().get(pid)
1175
+ logp = _os.path.join(self.log_dir, f"{ch.key}.{ch.port}.log")
1176
+ with open(logp, "r", encoding="utf-8", errors="replace") as fh:
1177
+ bufs = _S.load_log_buffers(fh.read())
1178
+ dev = [b for (d, _k, b) in bufs if d.startswith("CUDA") and d != "CUDA_Host"]
1179
+ if not used or not dev:
1180
+ return
1181
+ context = _S.gguf_context(int(used), dev)
1182
+ _db.put_measured(self._worker(), _os.path.basename(path), ctx, ngl, ncm, kv, int(used), context)
1183
+ # K2: the exact KV / recurrent-state structure + the engine's slot count
1184
+ try:
1185
+ with open(logp, "r", encoding="utf-8", errors="replace") as fh2:
1186
+ st = _S.kv_structure_from_log(fh2.read())
1187
+ if st:
1188
+ import json as _j, urllib.request as _u
1189
+ slots = _j.loads(_u.urlopen(ch.base_url + "/props", timeout=10).read()).get("total_slots")
1190
+ _db.put_kv_structure(self._worker(), _os.path.basename(path), st, slots, ctx, logp)
1191
+ if st["kv_type"] == "f16":
1192
+ _db.rewrite_ticks(_os.path.basename(path), st)
1193
+ except Exception as exc: # noqa: BLE001
1194
+ print(f"[kv_structure] {exc}")
1195
+ if context > 0:
1196
+ _db.put_cuda_context(self._worker(), self._device_key(), context)
1197
+ except Exception as exc: # noqa: BLE001
1198
+ print(f"[measure] failed: {exc}")
1199
+
1200
+ def _free_port(self) -> int:
1201
+ """Next port from the base that nothing is bound to. A port held by a
1202
+ stray llama-server would answer our health check for a child that
1203
+ never bound it."""
1204
+ while True:
1205
+ port = self._port_next
1206
+ self._port_next += 1
1207
+ with _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM) as s:
1208
+ try:
1209
+ s.bind(("127.0.0.1", port))
1210
+ except OSError:
1211
+ continue
1212
+ return port
1213
+
1214
+ # health wait (lifted slot_agent._wait_healthy logic: progress/stall aware)
1215
+ def _wait_healthy(self, ch: _Child, hard_cap: float = 600.0,
1216
+ stall: float = 90.0) -> "tuple[bool, Optional[str]]":
1217
+ start = _time.time()
1218
+ last_progress = 0
1219
+ last_progress_ts = start
1220
+ while True:
1221
+ if not self._alive(ch):
1222
+ rc = getattr(ch.proc, "returncode", None)
1223
+ return False, f"child exited rc={rc}"
1224
+ if self._health(ch) and self._alive(ch):
1225
+ return True, None
1226
+ now = _time.time()
1227
+ if now - start >= hard_cap:
1228
+ return False, f"hard cap {hard_cap:.0f}s"
1229
+ pid = getattr(ch.proc, "pid", None)
1230
+ cur = (_proc_rss_bytes(pid) or 0) + (_compute_apps_vram().get(pid, 0))
1231
+ if cur - last_progress >= 8 * 2 ** 20:
1232
+ last_progress, last_progress_ts = cur, now
1233
+ elif now - last_progress_ts >= stall:
1234
+ return False, f"stalled (no RSS/VRAM progress {stall:.0f}s)"
1235
+ _time.sleep(1.0)
1236
+
1237
+ def _health(self, ch: _Child) -> bool:
1238
+ try:
1239
+ req = _urllib.Request(ch.base_url + "/health")
1240
+ with _urllib.urlopen(req, timeout=2.0) as r:
1241
+ return r.status == 200
1242
+ except Exception: # noqa: BLE001
1243
+ return False
1244
+
1245
+ # ── a real chat call through the running seat ─────────────────────────────
1246
+ def chat(self, key: str, messages: list, max_tokens: int = 64) -> dict:
1247
+ ch = self.children[key]
1248
+ forwarded = {"messages": messages, "max_tokens": max_tokens,
1249
+ "temperature": 0.1, "cache_prompt": False}
1250
+ body = _json.dumps(forwarded).encode()
1251
+ req = _urllib.Request(ch.base_url + "/v1/chat/completions", data=body,
1252
+ headers={"Content-Type": "application/json"})
1253
+ call_start = _time.time()
1254
+ try:
1255
+ with _urllib.urlopen(req, timeout=600) as r:
1256
+ payload = _json.loads(r.read().decode("utf-8", "replace"))
1257
+ ok, error = True, None
1258
+ except Exception as exc: # noqa: BLE001
1259
+ payload, ok, error = {}, False, repr(exc)
1260
+ responded = _time.time()
1261
+ ch.last_used = responded
1262
+ ch.calls += 1
1263
+
1264
+ timings = payload.get("timings") if isinstance(payload, dict) else {}
1265
+ usage = payload.get("usage") if isinstance(payload, dict) else {}
1266
+ usage = usage if isinstance(usage, dict) else {}
1267
+ comp = (usage.get("completion_tokens")
1268
+ or (timings.get("predicted_n") if isinstance(timings, dict) else None))
1269
+ split = _tm.generation_split(timings, elapsed_s=responded - call_start,
1270
+ completion_tokens=comp)
1271
+ tok_s = _tm.tok_s_from_timings(payload)
1272
+ text = ""
1273
+ try:
1274
+ text = payload["choices"][0]["message"]["content"]
1275
+ except Exception: # noqa: BLE001
1276
+ pass
1277
+ prompt_s = split.get("prompt_s")
1278
+ gen_s = split.get("generation_s")
1279
+ first_token_at = call_start + (prompt_s or 0.0)
1280
+ ended_at = first_token_at + (gen_s or 0.0)
1281
+ return {
1282
+ "ok": ok, "error": error, "payload": payload, "text": text,
1283
+ "forwarded_body": forwarded,
1284
+ "timings": timings, "usage": usage, "tok_s": tok_s, "split": split,
1285
+ "call_start": call_start, "first_token_at": first_token_at,
1286
+ "ended_at": ended_at, "responded_at": responded,
1287
+ "prompt_tokens": usage.get("prompt_tokens")
1288
+ or (timings.get("prompt_n") if isinstance(timings, dict) else None),
1289
+ "completion_tokens": comp,
1290
+ "decode_tokens": split.get("gen_tokens"),
1291
+ }
1292
+
1293
+ def shutdown(self) -> None:
1294
+ for key in list(self.children):
1295
+ self.evict(key, reason="front door shutdown")
1296
+
1297
+
1298
+ __all__ = ["gguf_footprint", "LlamaCppHost", "discover_models", "model_weights", "engine_of_path"]