Spaces:
Running on Zero
Running on Zero
Upload app_deepseek.py
Browse files- app_deepseek.py +222 -439
app_deepseek.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
"""
|
| 2 |
gijl/agentic-thinker-v1 -- multi-loop dataset generator (single-file app.py)
|
| 3 |
-
v8, worker "deepseek" (DeepSeek-V4-Flash-0731): llama.cpp
|
| 4 |
output, + genuine agent-task trajectories + five specialized workers (glm/minimax/deepseek/kimi/
|
| 5 |
qwen) concurrent collection, now with real per-model output-compatibility handling. Sibling files
|
| 6 |
run the identical pipeline -- see WHAT CHANGED IN v7 and WHAT CHANGED IN v8.
|
|
@@ -310,17 +310,27 @@ architecture to mature (GIJL_LLAMA_SERVER is unset by default, so a later run au
|
|
| 310 |
whatever "latest" is by then -- no file change needed), or point GIJL_LLAMA_SERVER (already supported,
|
| 311 |
see ensure_llama_server()) at a self-built binary from an experimental third-party CPU-capable fork --
|
| 312 |
unofficial, so verify it yourself before trusting it with real data.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
FOUR sibling files (glm/minimax/deepseek/kimi/qwen minus this one) run the same pipeline, each specialized
|
| 314 |
differently -- see WHAT CHANGED IN v7 and WHAT CHANGED IN v8 at the top of this file for the full table and the
|
| 315 |
-
evidence behind each choice. all five write into the SAME dataset concurrently -- see MODEL_TAG and the v6 note above for how that stays race-free.
|
| 316 |
-
MODEL_ID repo (rendered locally), the weights come from the GGUF repo. ONE llama-server process, ONE generation
|
| 317 |
stream (parallel slots = 1), context 12288 tokens by default (GIJL_CTX). The tool-call XML convention below
|
| 318 |
(<atem:...>) is taught IN THE PROMPT (see build_scout_prompt/build_agent_task_prompt), not a trained-in format of
|
| 319 |
any one model, so it is unchanged across workers -- but it is still worth a GIJL_SMOKE=1 run per worker before a
|
| 320 |
long session, to confirm this model actually follows it turn after turn.
|
| 321 |
|
| 322 |
-
HARDWARE (
|
| 323 |
-
|
| 324 |
(The old transformers backend's TPU notes: device_map="auto" did NOT place the model on the TPU -- the log showed
|
| 325 |
"no hf_device_map" -- and generation stalled in XLA compilation.)
|
| 326 |
|
|
@@ -366,8 +376,8 @@ Before running:
|
|
| 366 |
Stack Exchange key (label must match SECRET_NAMES below).
|
| 367 |
3. Optional env: GIJL_ONLY_LOOPS=judgment_dpo,sft_helpful (restrict loops),
|
| 368 |
GIJL_TARGET_REPO=user/repo, GIJL_WORK_DIR=/some/dir, GIJL_MODEL_ID=..., GIJL_CTX=12288,
|
| 369 |
-
GIJL_GGUF_QUANT=
|
| 370 |
-
|
| 371 |
"""
|
| 372 |
|
| 373 |
import os
|
|
@@ -539,21 +549,13 @@ TARGET_DATASET_REPO = os.environ.get("GIJL_TARGET_REPO", "gijl/agentic-thinker-v
|
|
| 539 |
SEARCH_LANGUAGE_CYCLE = ["arabic", "english"]
|
| 540 |
|
| 541 |
# --- inference backend -------------------------------------------------------
|
| 542 |
-
# "
|
|
|
|
| 543 |
# "hf": the old transformers path (only kept for reference; on TPU it produced ~3 chars/s and hung).
|
| 544 |
-
BACKEND = os.environ.get("GIJL_BACKEND", "
|
| 545 |
-
GGUF_REPO = os.environ.get("GIJL_GGUF_REPO", "
|
| 546 |
-
GGUF_QUANT = os.environ.get("GIJL_GGUF_QUANT", "
|
| 547 |
-
|
| 548 |
-
# safety net (step down further on a smaller box); GIJL_GGUF_QUANT
|
| 549 |
-
# overrides this default at launch time.
|
| 550 |
-
LLAMA_CTX = int(os.environ.get("GIJL_CTX", "12288")) # tokens; KV cache is ~2-4GB at this size
|
| 551 |
-
LLAMA_LOAD_TIMEOUT_S = 1800
|
| 552 |
-
LLAMA_BIN_DIR = os.path.join(WORK_DIR, "bin") # CUDA build cache (synced to the hub, see CACHE_LLAMA_BINARY)
|
| 553 |
-
LLAMA_CPU_DIR = os.path.join(WORK_DIR, "llama_cpu") # prebuilt CPU release (not synced: quick to re-download)
|
| 554 |
-
LLAMA_BUILD_ROOT = os.environ.get("GIJL_LLAMA_BUILD_DIR", "/tmp/gijl_llama_build")
|
| 555 |
-
CACHE_LLAMA_BINARY = os.environ.get("GIJL_CACHE_LLAMA_BINARY", "1") != "0"
|
| 556 |
-
LLAMA_BUILD_TIMEOUT_S = 45 * 60
|
| 557 |
# Meta's recommended sampling is temperature 1.0 / top_p 0.95 / top_k 64. A slightly lower temperature
|
| 558 |
# suits structured-JSON tasks; top_p/top_k stay as recommended.
|
| 559 |
TOP_P, TOP_K = 0.95, 64
|
|
@@ -1326,12 +1328,9 @@ class ModelClient:
|
|
| 1326 |
|
| 1327 |
|
| 1328 |
# ---------------------------------------------------------------------------
|
| 1329 |
-
# 5b.
|
| 1330 |
-
# llama.cpp
|
| 1331 |
-
#
|
| 1332 |
-
# /tokenize double-BOS, HTTP 400 on context overflow, cancel-on-disconnect.
|
| 1333 |
-
# There are NO official Linux CUDA binaries (only Windows CUDA), so the GPU
|
| 1334 |
-
# path builds from source (cached in the hub repo under bin/).
|
| 1335 |
# ---------------------------------------------------------------------------
|
| 1336 |
def _run(cmd, timeout=60, env=None):
|
| 1337 |
try:
|
|
@@ -1399,279 +1398,94 @@ def _memory_budget_gb(acc: dict) -> float:
|
|
| 1399 |
return (acc["ram_gb"] or 24.0) * 0.75 - 2.0
|
| 1400 |
|
| 1401 |
|
| 1402 |
-
# ---
|
| 1403 |
-
|
| 1404 |
-
|
| 1405 |
-
|
| 1406 |
-
|
| 1407 |
-
|
| 1408 |
-
|
| 1409 |
-
|
| 1410 |
-
|
| 1411 |
-
|
| 1412 |
-
|
| 1413 |
-
|
| 1414 |
-
|
| 1415 |
-
|
| 1416 |
-
|
| 1417 |
-
|
| 1418 |
-
|
| 1419 |
-
|
| 1420 |
-
|
| 1421 |
-
|
| 1422 |
-
|
| 1423 |
-
|
| 1424 |
-
|
| 1425 |
-
|
| 1426 |
-
|
| 1427 |
-
|
| 1428 |
-
"""-> (tag, reason). An explicitly requested quant is always honored; the default one steps down if it
|
| 1429 |
-
does not fit the memory budget."""
|
| 1430 |
-
if not groups:
|
| 1431 |
-
raise RuntimeError(f"no usable .gguf files found in {GGUF_REPO}")
|
| 1432 |
-
want_u = (want or "").upper()
|
| 1433 |
-
if want_u in groups and (explicit or groups[want_u]["size_gb"] <= budget_gb):
|
| 1434 |
-
return want_u, "requested"
|
| 1435 |
-
fitting = [t for t, g in groups.items() if g["size_gb"] <= budget_gb]
|
| 1436 |
-
if want_u in groups:
|
| 1437 |
-
smaller = [t for t in fitting if groups[t]["size_gb"] < groups[want_u]["size_gb"]]
|
| 1438 |
-
if smaller:
|
| 1439 |
-
return max(smaller, key=lambda t: groups[t]["size_gb"]), f"{want_u} ({groups[want_u]['size_gb']:.1f}GB) exceeds the {budget_gb:.1f}GB budget"
|
| 1440 |
-
elif fitting:
|
| 1441 |
-
return max(fitting, key=lambda t: groups[t]["size_gb"]), f"{want_u or 'requested quant'} not in the repo"
|
| 1442 |
-
return min(groups, key=lambda t: groups[t]["size_gb"]), "nothing fits the budget: smallest quant, expect partial offload"
|
| 1443 |
|
| 1444 |
|
| 1445 |
def download_gguf(acc: dict):
|
| 1446 |
-
"""-> (
|
| 1447 |
-
|
| 1448 |
-
from huggingface_hub import HfApi, hf_hub_download
|
| 1449 |
token = os.environ.get("HF_TOKEN")
|
| 1450 |
explicit_file = os.environ.get("GIJL_GGUF_FILE")
|
| 1451 |
if explicit_file:
|
| 1452 |
path = hf_hub_download(GGUF_REPO, explicit_file, token=token)
|
| 1453 |
return path, {"quant": explicit_file, "size_gb": os.path.getsize(path) / 1e9}
|
| 1454 |
-
|
| 1455 |
-
|
| 1456 |
-
|
| 1457 |
-
|
| 1458 |
-
|
| 1459 |
-
|
| 1460 |
-
|
| 1461 |
-
|
| 1462 |
-
|
| 1463 |
-
|
| 1464 |
-
budget = _memory_budget_gb(acc)
|
| 1465 |
-
tag, why = choose_quant(groups, budget, GGUF_QUANT, "GIJL_GGUF_QUANT" in os.environ)
|
| 1466 |
-
files = groups[tag]["files"]
|
| 1467 |
-
print(f"[llama] model: {GGUF_REPO} quant={tag} ({groups[tag]['size_gb']:.1f}GB, budget {budget:.1f}GB) -- {why}", flush=True)
|
| 1468 |
t0 = time.time()
|
| 1469 |
-
|
| 1470 |
-
print(f"[
|
| 1471 |
-
return
|
| 1472 |
-
|
| 1473 |
-
|
| 1474 |
-
# --- getting a llama-server binary ---------------------------------------------------
|
| 1475 |
-
def _llama_env(lib_dir):
|
| 1476 |
-
env = os.environ.copy()
|
| 1477 |
-
if lib_dir:
|
| 1478 |
-
env["LD_LIBRARY_PATH"] = lib_dir + (":" + env["LD_LIBRARY_PATH"] if env.get("LD_LIBRARY_PATH") else "")
|
| 1479 |
-
return env
|
| 1480 |
|
| 1481 |
|
| 1482 |
-
|
| 1483 |
-
|
| 1484 |
-
rc, out, err = _run([binary, "--
|
| 1485 |
-
|
| 1486 |
-
return False
|
| 1487 |
-
if need_cuda:
|
| 1488 |
-
rc, out, err = _run([binary, "--list-devices"], 90, env)
|
| 1489 |
-
return rc == 0 and "cuda" in (out + err).lower()
|
| 1490 |
-
return True
|
| 1491 |
-
|
| 1492 |
-
|
| 1493 |
-
def _cuda_archs(acc: dict) -> list:
|
| 1494 |
-
archs = []
|
| 1495 |
-
for g in acc["gpus"]:
|
| 1496 |
-
m = re.fullmatch(r"(\d+)\.(\d+)", (g.get("cc") or "").strip())
|
| 1497 |
-
if m and (m.group(1) + m.group(2)) not in archs:
|
| 1498 |
-
archs.append(m.group(1) + m.group(2))
|
| 1499 |
-
return archs or ["native"]
|
| 1500 |
-
|
| 1501 |
-
|
| 1502 |
-
def _find_nvcc():
|
| 1503 |
-
found = shutil.which("nvcc")
|
| 1504 |
-
if found:
|
| 1505 |
-
return found
|
| 1506 |
-
for cand in sorted(glob.glob("/usr/local/cuda*/bin/nvcc"), reverse=True):
|
| 1507 |
-
return cand
|
| 1508 |
-
return None
|
| 1509 |
|
| 1510 |
|
| 1511 |
-
def
|
| 1512 |
-
"""
|
| 1513 |
-
|
| 1514 |
-
|
| 1515 |
-
|
| 1516 |
-
|
| 1517 |
-
|
| 1518 |
-
|
| 1519 |
-
|
| 1520 |
-
|
| 1521 |
-
|
| 1522 |
-
|
| 1523 |
-
|
| 1524 |
-
|
| 1525 |
-
|
| 1526 |
-
|
| 1527 |
-
|
| 1528 |
-
if not cmake or not shutil.which("git"):
|
| 1529 |
-
print("[llama][build] cmake/git missing -- cannot build.", flush=True)
|
| 1530 |
-
return None
|
| 1531 |
-
src, build = os.path.join(LLAMA_BUILD_ROOT, "llama.cpp"), os.path.join(LLAMA_BUILD_ROOT, "llama.cpp", "build")
|
| 1532 |
-
log = os.path.join(LLAMA_BUILD_ROOT, "build.log")
|
| 1533 |
-
env = os.environ.copy()
|
| 1534 |
-
env["CUDACXX"] = nvcc
|
| 1535 |
-
env["PATH"] = os.path.dirname(nvcc) + os.pathsep + env.get("PATH", "")
|
| 1536 |
t0 = time.time()
|
| 1537 |
-
if
|
| 1538 |
-
|
| 1539 |
-
|
| 1540 |
-
print(f"[llama][build] git clone failed:\n{_tail_file(log, 15)}", flush=True)
|
| 1541 |
-
return None
|
| 1542 |
-
toolkit_root = os.path.dirname(os.path.dirname(nvcc))
|
| 1543 |
-
base = [cmake, "-S", src, "-B", build, "-DGGML_CUDA=ON", f"-DCMAKE_CUDA_ARCHITECTURES={';'.join(archs)}",
|
| 1544 |
-
"-DCMAKE_BUILD_TYPE=Release", "-DBUILD_SHARED_LIBS=OFF", "-DLLAMA_CURL=OFF", "-DLLAMA_BUILD_TESTS=OFF",
|
| 1545 |
-
"-DGGML_CUDA_NO_VMM=ON"]
|
| 1546 |
-
if os.path.isfile(os.path.join(toolkit_root, "include", "cuda_runtime.h")):
|
| 1547 |
-
base.append(f"-DCUDAToolkit_ROOT={toolkit_root}")
|
| 1548 |
jobs = str(max(2, os.cpu_count() or 4))
|
| 1549 |
-
|
| 1550 |
-
|
| 1551 |
-
|
| 1552 |
-
|
| 1553 |
-
|
| 1554 |
-
|
| 1555 |
-
|
| 1556 |
-
|
| 1557 |
-
print(f"[llama][build] build failed ({label}):\n{_tail_file(log, 25)}", flush=True)
|
| 1558 |
-
continue
|
| 1559 |
-
binary = os.path.join(build, "bin", "llama-server")
|
| 1560 |
-
if os.path.isfile(binary):
|
| 1561 |
-
print(f"[llama][build] done in {time.time() - t0:.0f}s ({label})", flush=True)
|
| 1562 |
-
return binary
|
| 1563 |
-
return None
|
| 1564 |
-
|
| 1565 |
-
|
| 1566 |
-
def download_llama_cpu_release():
|
| 1567 |
-
"""Prebuilt CPU llama-server from GitHub releases. The 'latest' release only carries nightly-tag.txt, which
|
| 1568 |
-
names the tag holding the real binaries (llama-<tag>-bin-ubuntu-x64.tar.gz)."""
|
| 1569 |
-
import requests
|
| 1570 |
-
arch = "arm64" if platform.machine().lower() in ("aarch64", "arm64") else "x64"
|
| 1571 |
-
base = "https://github.com/ggml-org/llama.cpp/releases"
|
| 1572 |
-
hdr = {"User-Agent": _CRAWLER_UA}
|
| 1573 |
-
pinned_tag = os.environ.get("GIJL_LLAMA_CPU_TAG", "").strip()
|
| 1574 |
-
if pinned_tag:
|
| 1575 |
-
# v8: bisecting knob, opt-in only (default behavior below is unchanged) -- added because DeepSeek-V4-Flash
|
| 1576 |
-
# crashed llama-server on CPU with GGML_ASSERT(*cur_backend_id != -1) in ggml_backend_sched_split_graph
|
| 1577 |
-
# during context/graph setup, a signature consistent with "this architecture's CPU backend coverage in
|
| 1578 |
-
# llama.cpp is still incomplete" rather than anything wrong in this file -- DeepSeek-V4 support was still
|
| 1579 |
-
# WIP/fork-based in llama.cpp as of its own GitHub support thread. "latest" always means whatever today's
|
| 1580 |
-
# CI build is, so there is no way to test a different one without this. EXCLUSIVE on purpose (no silent
|
| 1581 |
-
# fallback to latest): the point is to know for certain which build actually ran.
|
| 1582 |
-
print(f"[llama] GIJL_LLAMA_CPU_TAG={pinned_tag} set -- using this exact release instead of latest", flush=True)
|
| 1583 |
-
tags = [pinned_tag]
|
| 1584 |
-
else:
|
| 1585 |
-
tags = []
|
| 1586 |
-
try:
|
| 1587 |
-
latest = requests.get(f"{base}/latest", timeout=30, headers=hdr).url.rstrip("/").rsplit("/", 1)[-1]
|
| 1588 |
-
nt = requests.get(f"{base}/download/{latest}/nightly-tag.txt", timeout=30, headers=hdr)
|
| 1589 |
-
if nt.status_code == 200 and nt.text.strip():
|
| 1590 |
-
tags.append(nt.text.strip().split()[0])
|
| 1591 |
-
tags.append(latest)
|
| 1592 |
-
except requests.RequestException as e:
|
| 1593 |
-
raise RuntimeError(f"cannot reach GitHub releases: {_redact(str(e))[:200]}")
|
| 1594 |
-
last_err = "no candidate tag"
|
| 1595 |
-
for tag in tags:
|
| 1596 |
-
dest = os.path.join(LLAMA_CPU_DIR, tag)
|
| 1597 |
-
found = glob.glob(os.path.join(dest, "**", "llama-server"), recursive=True)
|
| 1598 |
-
if found:
|
| 1599 |
-
return found[0]
|
| 1600 |
-
url = f"{base}/download/{tag}/llama-{tag}-bin-ubuntu-{arch}.tar.gz"
|
| 1601 |
-
os.makedirs(dest, exist_ok=True)
|
| 1602 |
-
archive = os.path.join(dest, "release.tar.gz")
|
| 1603 |
-
try:
|
| 1604 |
-
print(f"[llama] downloading CPU build {tag} ...", flush=True)
|
| 1605 |
-
with requests.get(url, stream=True, timeout=(20, 120), headers=hdr) as r:
|
| 1606 |
-
r.raise_for_status()
|
| 1607 |
-
with open(archive, "wb") as f:
|
| 1608 |
-
for part in r.iter_content(chunk_size=1 << 20):
|
| 1609 |
-
f.write(part)
|
| 1610 |
-
with tarfile.open(archive) as tf:
|
| 1611 |
-
if hasattr(tarfile, "data_filter"):
|
| 1612 |
-
tf.extractall(dest, filter="data")
|
| 1613 |
-
else:
|
| 1614 |
-
tf.extractall(dest)
|
| 1615 |
-
os.remove(archive)
|
| 1616 |
-
except Exception as e: # noqa: BLE001
|
| 1617 |
-
last_err = f"{tag}: {type(e).__name__}: {_redact(str(e))[:160]}"
|
| 1618 |
-
continue
|
| 1619 |
-
found = glob.glob(os.path.join(dest, "**", "llama-server"), recursive=True)
|
| 1620 |
-
if found:
|
| 1621 |
-
os.chmod(found[0], 0o755)
|
| 1622 |
-
return found[0]
|
| 1623 |
-
raise RuntimeError(f"could not obtain a prebuilt llama-server ({last_err})")
|
| 1624 |
-
|
| 1625 |
-
|
| 1626 |
-
def ensure_llama_server(acc: dict):
|
| 1627 |
-
"""-> (binary, lib_dir, kind). Order: GIJL_LLAMA_SERVER override, cached CUDA build, fresh CUDA build,
|
| 1628 |
-
prebuilt CPU release (with a loud warning when a GPU exists but a CUDA build is impossible)."""
|
| 1629 |
-
override = os.environ.get("GIJL_LLAMA_SERVER")
|
| 1630 |
-
if override:
|
| 1631 |
-
return override, os.path.dirname(override) or None, ("gpu" if acc["kind"] == "gpu" else "cpu")
|
| 1632 |
-
if acc["kind"] == "gpu":
|
| 1633 |
-
archs = _cuda_archs(acc)
|
| 1634 |
-
cdir = os.path.join(LLAMA_BIN_DIR, "cuda-sm" + "-".join(archs))
|
| 1635 |
-
cbin = os.path.join(cdir, "llama-server")
|
| 1636 |
-
if os.path.isfile(cbin) and os.environ.get("GIJL_REBUILD_LLAMA") != "1":
|
| 1637 |
-
for name in os.listdir(cdir):
|
| 1638 |
-
try:
|
| 1639 |
-
os.chmod(os.path.join(cdir, name), 0o755) # the hub does not preserve the executable bit
|
| 1640 |
-
except OSError:
|
| 1641 |
-
pass
|
| 1642 |
-
if _binary_ok(cbin, cdir, True):
|
| 1643 |
-
print(f"[llama] using cached CUDA build {cbin}", flush=True)
|
| 1644 |
-
return cbin, cdir, "gpu"
|
| 1645 |
-
print("[llama] cached CUDA build is unusable here (different image/driver?) -- rebuilding", flush=True)
|
| 1646 |
-
built = build_llama_cuda(archs)
|
| 1647 |
-
if built:
|
| 1648 |
-
shutil.rmtree(cdir, ignore_errors=True)
|
| 1649 |
-
os.makedirs(cdir, exist_ok=True)
|
| 1650 |
-
for name in os.listdir(os.path.dirname(built)):
|
| 1651 |
-
fp = os.path.join(os.path.dirname(built), name)
|
| 1652 |
-
if os.path.isfile(fp):
|
| 1653 |
-
shutil.copy2(fp, os.path.join(cdir, name))
|
| 1654 |
-
if _binary_ok(cbin, cdir, True):
|
| 1655 |
-
return cbin, cdir, "gpu"
|
| 1656 |
-
print("[llama][build] the fresh build does not see a CUDA device", flush=True)
|
| 1657 |
-
names = ", ".join(g["name"] for g in acc["gpus"])
|
| 1658 |
-
if os.environ.get("GIJL_ALLOW_CPU_FALLBACK") != "1":
|
| 1659 |
-
# This used to fall back silently: a T4x2 machine then ran a 30B model on 4 CPU cores (~1 token/s,
|
| 1660 |
-
# 3 tok/s prefill) and nothing looked wrong for the first 15 minutes.
|
| 1661 |
-
raise RuntimeError(
|
| 1662 |
-
f"A GPU is present ({names}) but no CUDA llama-server could be built or loaded, so this would run on the CPU "
|
| 1663 |
-
f"(~1 token/s). See the [llama][build] errors above and {os.path.join(LLAMA_BUILD_ROOT, 'build.log')}. "
|
| 1664 |
-
f"Options: GIJL_LLAMA_SERVER=/path/to/your/cuda/llama-server, or GIJL_ALLOW_CPU_FALLBACK=1 to run on CPU anyway.")
|
| 1665 |
-
print("[llama][WARNING] GIJL_ALLOW_CPU_FALLBACK=1 -> using the CPU build although a GPU is present (MUCH slower).", flush=True)
|
| 1666 |
-
binary = download_llama_cpu_release()
|
| 1667 |
-
if not _binary_ok(binary, os.path.dirname(binary), False):
|
| 1668 |
-
raise RuntimeError(f"the downloaded llama-server does not run here: {binary}")
|
| 1669 |
-
return binary, os.path.dirname(binary), "cpu"
|
| 1670 |
-
|
| 1671 |
-
|
| 1672 |
-
def load_tokenizer():
|
| 1673 |
-
from transformers import AutoTokenizer
|
| 1674 |
-
return AutoTokenizer.from_pretrained(MODEL_ID, token=os.environ.get("HF_TOKEN"))
|
| 1675 |
|
| 1676 |
|
| 1677 |
def _free_port() -> int:
|
|
@@ -1680,14 +1494,14 @@ def _free_port() -> int:
|
|
| 1680 |
return s.getsockname()[1]
|
| 1681 |
|
| 1682 |
|
| 1683 |
-
class
|
| 1684 |
-
"""Owns the
|
| 1685 |
|
| 1686 |
-
def __init__(self, binary,
|
| 1687 |
-
self.binary, self.
|
| 1688 |
-
self.
|
| 1689 |
self.port, self.proc, self.restarts, self._help_text = _free_port(), None, 0, None
|
| 1690 |
-
self.log_path = os.path.join(WORK_DIR, "
|
| 1691 |
atexit.register(self.stop)
|
| 1692 |
|
| 1693 |
@property
|
|
@@ -1696,30 +1510,14 @@ class LlamaServer:
|
|
| 1696 |
|
| 1697 |
def _has(self, flag: str) -> bool:
|
| 1698 |
if self._help_text is None:
|
| 1699 |
-
rc, out, err = _run([self.binary, "--help"], 90,
|
| 1700 |
self._help_text = out + err
|
| 1701 |
return flag in self._help_text
|
| 1702 |
|
| 1703 |
def command(self) -> list:
|
| 1704 |
-
cmd = [self.binary, "-m", self.model_path, "--host", "127.0.0.1", "--port", str(self.port), "-
|
| 1705 |
-
if self._has("--
|
| 1706 |
-
cmd += ["--
|
| 1707 |
-
if self._has("--special"):
|
| 1708 |
-
cmd += ["--special"] # emit special tokens so <atem:...> tool markup survives
|
| 1709 |
-
if self._has("--no-context-shift"):
|
| 1710 |
-
cmd += ["--no-context-shift"] # error out instead of silently dropping the prompt's start
|
| 1711 |
-
if self._has("--no-jinja"):
|
| 1712 |
-
cmd += ["--no-jinja"] # we render the template ourselves: /completion stays raw pass-through
|
| 1713 |
-
if self._has("--reasoning-format"):
|
| 1714 |
-
cmd += ["--reasoning-format", "none"] # never move 'thinking' out of the text we parse
|
| 1715 |
-
threads = "16"
|
| 1716 |
-
cmd += ["--threads", threads]
|
| 1717 |
-
if self.kind == "gpu":
|
| 1718 |
-
if self.model_gb and self.model_gb + 2.5 <= self.acc.get("vram_gb", 0) * 0.92:
|
| 1719 |
-
cmd += ["-ngl", "999"] # fits entirely: offload every layer
|
| 1720 |
-
# else: leave -ngl unset so llama.cpp's default --fit adjusts layers to the free VRAM
|
| 1721 |
-
else:
|
| 1722 |
-
cmd += ["-ngl", "0"]
|
| 1723 |
return cmd + self.extra_args
|
| 1724 |
|
| 1725 |
def tail(self, n: int = 25) -> str:
|
|
@@ -1729,40 +1527,34 @@ class LlamaServer:
|
|
| 1729 |
import requests
|
| 1730 |
os.makedirs(os.path.dirname(self.log_path), exist_ok=True)
|
| 1731 |
cmd = self.command()
|
| 1732 |
-
print("[
|
| 1733 |
self._log = open(self.log_path, "ab")
|
| 1734 |
-
self.proc = subprocess.Popen(cmd, stdout=self._log, stderr=subprocess.STDOUT,
|
| 1735 |
start_new_session=True)
|
| 1736 |
t0, last = time.time(), time.time()
|
| 1737 |
-
while time.time() - t0 <
|
| 1738 |
if self.proc.poll() is not None:
|
| 1739 |
-
raise RuntimeError(f"
|
| 1740 |
try:
|
| 1741 |
-
r = requests.get(self.base_url + "/
|
| 1742 |
-
if r.status_code == 200
|
| 1743 |
-
print(f"[
|
| 1744 |
-
self._print_offload_summary()
|
| 1745 |
return
|
| 1746 |
except (requests.RequestException, ValueError):
|
| 1747 |
pass
|
| 1748 |
if time.time() - last > 30:
|
| 1749 |
-
print(f"[
|
| 1750 |
last = time.time()
|
| 1751 |
time.sleep(2)
|
| 1752 |
-
raise TimeoutError(f"
|
| 1753 |
-
|
| 1754 |
-
def _print_offload_summary(self):
|
| 1755 |
-
lines = [l for l in self.tail(400).splitlines() if re.search(r"offload|CUDA|fit|n_ctx|KV", l)]
|
| 1756 |
-
for l in lines[-6:]:
|
| 1757 |
-
print("[llama] " + l[-200:], flush=True)
|
| 1758 |
|
| 1759 |
def ensure_running(self):
|
| 1760 |
if self.proc is not None and self.proc.poll() is None:
|
| 1761 |
return
|
| 1762 |
self.restarts += 1
|
| 1763 |
-
print(f"[
|
| 1764 |
if self.restarts > 3:
|
| 1765 |
-
raise RuntimeError("
|
| 1766 |
self.port = _free_port()
|
| 1767 |
self.start()
|
| 1768 |
|
|
@@ -1780,78 +1572,75 @@ class LlamaServer:
|
|
| 1780 |
pass
|
| 1781 |
|
| 1782 |
|
| 1783 |
-
class
|
| 1784 |
-
"""Same interface as ModelClient.chat():
|
| 1785 |
-
|
| 1786 |
-
|
| 1787 |
-
|
| 1788 |
-
-
|
| 1789 |
-
|
| 1790 |
-
-
|
| 1791 |
-
|
| 1792 |
-
|
| 1793 |
-
|
| 1794 |
-
|
| 1795 |
-
self.
|
| 1796 |
-
self.
|
| 1797 |
-
|
| 1798 |
-
|
| 1799 |
-
|
| 1800 |
-
|
| 1801 |
-
|
| 1802 |
-
|
| 1803 |
-
|
| 1804 |
-
|
| 1805 |
-
|
| 1806 |
-
|
| 1807 |
-
|
| 1808 |
-
|
| 1809 |
-
|
| 1810 |
-
|
| 1811 |
-
|
| 1812 |
-
|
| 1813 |
-
def _probe_add_bos(self) -> bool:
|
| 1814 |
-
try:
|
| 1815 |
-
return len(self._tokenize("a", True)) == len(self._tokenize("a", False)) + 1
|
| 1816 |
-
except Exception: # noqa: BLE001
|
| 1817 |
-
return False
|
| 1818 |
-
|
| 1819 |
-
def _prepare_prompt(self, prompt: str) -> str:
|
| 1820 |
-
bos = getattr(self.tok, "bos_token", None)
|
| 1821 |
-
if self.server_adds_bos and bos and prompt.startswith(bos):
|
| 1822 |
-
prompt = prompt[len(bos):]
|
| 1823 |
-
return prompt
|
| 1824 |
|
| 1825 |
def chat(self, messages, max_new_tokens=GEN_MAX_NEW_TOKENS, temperature=0.4, tools=None):
|
| 1826 |
import requests
|
| 1827 |
-
tool_mode = bool(tools)
|
| 1828 |
self.server.ensure_running()
|
| 1829 |
-
|
| 1830 |
-
|
| 1831 |
-
|
| 1832 |
-
|
| 1833 |
-
n_prompt = -1
|
| 1834 |
-
if n_prompt >= 0 and n_prompt + 256 > self.ctx:
|
| 1835 |
-
print(f"[llama] prompt is {n_prompt} tokens with ctx={self.ctx} -- refusing to generate", flush=True)
|
| 1836 |
-
return {"content": "", "error": "context_too_long", "timed_out": False, "prompt_tokens": n_prompt}
|
| 1837 |
-
n_predict = max_new_tokens if n_prompt < 0 else max(64, min(max_new_tokens, self.ctx - n_prompt - 32))
|
| 1838 |
-
payload = {"prompt": prompt, "n_predict": n_predict, "temperature": temperature, "top_p": TOP_P, "top_k": TOP_K,
|
| 1839 |
-
"stream": True, "cache_prompt": True}
|
| 1840 |
payload.update(self.extra_payload)
|
|
|
|
|
|
|
| 1841 |
|
| 1842 |
gap = GEN_PER_CHUNK_GAP_TIMEOUT_S.get(self.kind, 300)
|
| 1843 |
cap = GEN_MAX_TOTAL_S.get(self.kind, 900)
|
| 1844 |
-
chunks,
|
| 1845 |
start = last_data = last_hb = time.time()
|
| 1846 |
try:
|
| 1847 |
-
|
| 1848 |
-
|
|
|
|
|
|
|
|
|
|
| 1849 |
body = resp.text[:400]
|
| 1850 |
-
|
| 1851 |
-
|
| 1852 |
-
|
| 1853 |
-
|
| 1854 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1855 |
for raw in resp.iter_lines(chunk_size=1):
|
| 1856 |
now = time.time()
|
| 1857 |
if now - start > cap:
|
|
@@ -1864,9 +1653,12 @@ class LlamaCppClient:
|
|
| 1864 |
break
|
| 1865 |
line = raw.decode("utf-8", errors="replace") if isinstance(raw, bytes) else raw
|
| 1866 |
if not line.startswith("data:"):
|
| 1867 |
-
continue # SSE keep-alive pings / blank lines
|
|
|
|
|
|
|
|
|
|
| 1868 |
try:
|
| 1869 |
-
d = json.loads(
|
| 1870 |
except ValueError:
|
| 1871 |
continue
|
| 1872 |
if "error" in d:
|
|
@@ -1874,19 +1666,20 @@ class LlamaCppClient:
|
|
| 1874 |
err = _redact(str(e.get("message", e)) if isinstance(e, dict) else str(e))
|
| 1875 |
break
|
| 1876 |
last_data = now
|
| 1877 |
-
if d.get("
|
| 1878 |
-
|
| 1879 |
-
|
| 1880 |
-
|
| 1881 |
-
|
|
|
|
|
|
|
| 1882 |
if now - last_hb > 30:
|
| 1883 |
print(f"[gen] {sum(len(c) for c in chunks)} chars, {now - start:.0f}s elapsed ({self.kind})", flush=True)
|
| 1884 |
last_hb = now
|
| 1885 |
except requests.RequestException as e:
|
| 1886 |
-
# While streaming, requests surfaces a socket read timeout as ConnectionError("... Read timed out")
|
| 1887 |
-
# NOT as ReadTimeout -- so match on the message too.
|
| 1888 |
if isinstance(e, requests.exceptions.Timeout) or "timed out" in str(e).lower():
|
| 1889 |
-
print(f"[gen][TIMEOUT]
|
| 1890 |
timed_out = True
|
| 1891 |
else:
|
| 1892 |
err = f"{type(e).__name__}: {_redact(str(e))[:200]}"
|
|
@@ -1894,52 +1687,43 @@ class LlamaCppClient:
|
|
| 1894 |
if resp is not None:
|
| 1895 |
resp.close() # closing the connection cancels generation on the server
|
| 1896 |
|
| 1897 |
-
|
| 1898 |
-
|
| 1899 |
-
|
| 1900 |
-
|
| 1901 |
-
|
| 1902 |
-
f"{t.get('prompt_per_second') or 0:.0f} tok/s | generated {gen} tok @ "
|
| 1903 |
-
f"{t.get('predicted_per_second') or 0:.1f} tok/s | stop={final.get('stop_type')}", flush=True)
|
| 1904 |
if err and err != "context_too_long":
|
| 1905 |
-
print(f"[
|
| 1906 |
-
|
| 1907 |
-
|
| 1908 |
-
"cached_tokens": cached, "gen_tokens": gen}
|
| 1909 |
|
| 1910 |
def warmup(self):
|
| 1911 |
"""One tiny generation at startup: proves the whole path works and prints the real speed."""
|
| 1912 |
r = self.chat([{"role": "user", "content": "Reply with the single word: ready"}], max_new_tokens=24, temperature=0.0)
|
| 1913 |
ok = bool(r["content"].strip()) and not r["error"]
|
| 1914 |
-
print(f"[
|
| 1915 |
return ok
|
| 1916 |
|
| 1917 |
|
| 1918 |
-
def
|
| 1919 |
-
"""-> (client, server). Model download
|
| 1920 |
from concurrent.futures import ThreadPoolExecutor
|
| 1921 |
acc = detect_accelerators()
|
| 1922 |
-
|
| 1923 |
-
print(f"[hardware]
|
| 1924 |
-
if acc["kind"] == "
|
| 1925 |
-
print("[hardware][WARNING]
|
| 1926 |
-
"
|
| 1927 |
-
|
| 1928 |
-
|
| 1929 |
-
|
| 1930 |
-
|
| 1931 |
-
|
| 1932 |
-
(model_path, meta), (binary, lib_dir, kind), tok = f_model.result(), f_bin.result(), f_tok.result()
|
| 1933 |
-
server = LlamaServer(binary, lib_dir, model_path, kind, LLAMA_CTX, acc, meta["size_gb"])
|
| 1934 |
-
print(f"[llama] inference device: {'GPU (CUDA)' if kind == 'gpu' else 'CPU'}", flush=True)
|
| 1935 |
server.start()
|
| 1936 |
-
|
| 1937 |
-
|
| 1938 |
-
f"Check {server.log_path}", flush=True)
|
| 1939 |
-
client = LlamaCppClient(server, tok, kind)
|
| 1940 |
-
print(f"[llama] context per request: {client.ctx} tokens | server adds BOS: {client.server_adds_bos}", flush=True)
|
| 1941 |
if not client.warmup():
|
| 1942 |
-
print("[
|
| 1943 |
return client, server
|
| 1944 |
|
| 1945 |
|
|
@@ -4384,7 +4168,7 @@ def _sync_patterns() -> list:
|
|
| 4384 |
# filenames (MANIFEST.json/README.md) needed a MODEL_TAG suffix -- done where they are defined above.
|
| 4385 |
pats = ([f"{m['folder']}/*" for m in DATA_TYPES.values()]
|
| 4386 |
+ [f"state/{MODEL_TAG}/*", os.path.basename(MANIFEST_PATH), os.path.basename(README_PATH)])
|
| 4387 |
-
return pats
|
| 4388 |
|
| 4389 |
|
| 4390 |
def _with_retry(fn, what: str, tries: int = 3, base_delay: float = 2.0):
|
|
@@ -4418,8 +4202,7 @@ def restore_from_hf():
|
|
| 4418 |
# other two workers' data down into every container unnecessarily.
|
| 4419 |
_with_retry(lambda: snapshot_download(
|
| 4420 |
repo_id=TARGET_DATASET_REPO, repo_type="dataset", token=token, local_dir=WORK_DIR,
|
| 4421 |
-
allow_patterns=[f"{m['folder']}/{MODEL_TAG}/*" for m in DATA_TYPES.values()] + [f"state/{MODEL_TAG}/*"]
|
| 4422 |
-
+ (["bin/*"] if CACHE_LLAMA_BINARY and BACKEND == "llamacpp" else [])), "restore")
|
| 4423 |
print(f"[restore] pulled prior state from {TARGET_DATASET_REPO} (worker={MODEL_TAG})", flush=True)
|
| 4424 |
except Exception as e:
|
| 4425 |
print(f"[restore] nothing to restore yet ({_redact(str(e))})", flush=True)
|
|
@@ -4448,16 +4231,16 @@ def sync_to_hf():
|
|
| 4448 |
# 14. Main loop -- single process, runs until the container stops (or GIJL_SESSION_BUDGET_S, if set)
|
| 4449 |
# ---------------------------------------------------------------------------
|
| 4450 |
def _start_backend():
|
| 4451 |
-
"""-> (client, close). close() stops
|
| 4452 |
if BACKEND == "hf":
|
| 4453 |
hardware = detect_hardware()
|
| 4454 |
print(f"[hardware] transformers backend, selected: {hardware}", flush=True)
|
| 4455 |
if hardware == "tpu":
|
| 4456 |
print("[hardware][WARNING] the transformers path on TPU was ~3 chars/s and hung in XLA compilation (see your TPU "
|
| 4457 |
-
"log). Use the default
|
| 4458 |
model, tok = load_model(hardware)
|
| 4459 |
return ModelClient(model, tok, hardware), (lambda: None)
|
| 4460 |
-
client, server =
|
| 4461 |
return client, server.stop
|
| 4462 |
|
| 4463 |
|
|
@@ -4537,7 +4320,7 @@ def main():
|
|
| 4537 |
print(f"[cycle] took {time.time() - t_cycle:.0f}s. " + _status_line(cycle, state), flush=True)
|
| 4538 |
maybe_sync(state)
|
| 4539 |
if smoke:
|
| 4540 |
-
print("[smoke] one cycle finished -- see the [
|
| 4541 |
"Unset GIJL_SMOKE for the full run.", flush=True)
|
| 4542 |
finally:
|
| 4543 |
try:
|
|
|
|
| 1 |
"""
|
| 2 |
gijl/agentic-thinker-v1 -- multi-loop dataset generator (single-file app.py)
|
| 3 |
+
v8, worker "deepseek" (DeepSeek-V4-Flash-0731): ds4 backend (v8.2; was llama.cpp) + multi-loop + verified, source-grouped, auditable
|
| 4 |
output, + genuine agent-task trajectories + five specialized workers (glm/minimax/deepseek/kimi/
|
| 5 |
qwen) concurrent collection, now with real per-model output-compatibility handling. Sibling files
|
| 6 |
run the identical pipeline -- see WHAT CHANGED IN v7 and WHAT CHANGED IN v8.
|
|
|
|
| 310 |
whatever "latest" is by then -- no file change needed), or point GIJL_LLAMA_SERVER (already supported,
|
| 311 |
see ensure_llama_server()) at a self-built binary from an experimental third-party CPU-capable fork --
|
| 312 |
unofficial, so verify it yourself before trusting it with real data.
|
| 313 |
+
v8.2 (backend swap, this worker only): llama.cpp is REMOVED from this file and replaced by antirez/ds4 (DwarfStar),
|
| 314 |
+
the inference engine written specifically for DeepSeek-V4-Flash -- section 5b. `make cpu` builds a CPU-only ds4-server
|
| 315 |
+
from source on first start (plain C, a few minutes, built in GIJL_DS4_BUILD_DIR, not synced to the hub because it is
|
| 316 |
+
-march=native). ds4 loads ONLY its own GGUFs: GGUF_REPO is now antirez/deepseek-v4-gguf and GGUF_QUANT is one of
|
| 317 |
+
q2 (default, ~81GiB) | q2-q4 (~98GB) | q4 -- the unsloth/llama.cpp GGUFs no longer apply. ds4 renders the DeepSeek chat
|
| 318 |
+
template itself, so the client sends plain `messages` to /v1/chat/completions (tool role folded into user turns, `tools`
|
| 319 |
+
not forwarded, thinking off via `think:false` from _CHAT_TEMPLATE_EXTRA_KWARGS) instead of a locally-rendered raw
|
| 320 |
+
prompt; the tokenizer is no longer loaded for inference. NOTHING outside section 5b, the backend constants, the two
|
| 321 |
+
`bin/*` sync/restore patterns and _start_backend() was touched. UNVERIFIED in this file (no 90GB box available when it
|
| 322 |
+
was written): real CPU speed, and whether ds4-server cancels generation when the client disconnects -- run
|
| 323 |
+
GIJL_SMOKE=1 first and read the [ds4] lines.
|
| 324 |
FOUR sibling files (glm/minimax/deepseek/kimi/qwen minus this one) run the same pipeline, each specialized
|
| 325 |
differently -- see WHAT CHANGED IN v7 and WHAT CHANGED IN v8 at the top of this file for the full table and the
|
| 326 |
+
evidence behind each choice. all five write into the SAME dataset concurrently -- see MODEL_TAG and the v6 note above for how that stays race-free. (v8.2: ds4 renders the chat template itself; the weights come from the GGUF repo.) ONE ds4-server process, ONE generation
|
|
|
|
| 327 |
stream (parallel slots = 1), context 12288 tokens by default (GIJL_CTX). The tool-call XML convention below
|
| 328 |
(<atem:...>) is taught IN THE PROMPT (see build_scout_prompt/build_agent_task_prompt), not a trained-in format of
|
| 329 |
any one model, so it is unchanged across workers -- but it is still worth a GIJL_SMOKE=1 run per worker before a
|
| 330 |
long session, to confirm this model actually follows it turn after turn.
|
| 331 |
|
| 332 |
+
HARDWARE (ds4 backend, v8.2): CPU by default (`make cpu`, 16 vCPUs / ~103GB file ceiling as in v7); GIJL_DS4_BUILD=cuda-generic
|
| 333 |
+
builds ds4's CUDA path instead. TPU is not supported.
|
| 334 |
(The old transformers backend's TPU notes: device_map="auto" did NOT place the model on the TPU -- the log showed
|
| 335 |
"no hf_device_map" -- and generation stalled in XLA compilation.)
|
| 336 |
|
|
|
|
| 376 |
Stack Exchange key (label must match SECRET_NAMES below).
|
| 377 |
3. Optional env: GIJL_ONLY_LOOPS=judgment_dpo,sft_helpful (restrict loops),
|
| 378 |
GIJL_TARGET_REPO=user/repo, GIJL_WORK_DIR=/some/dir, GIJL_MODEL_ID=..., GIJL_CTX=12288,
|
| 379 |
+
GIJL_GGUF_QUANT=q2|q2-q4|q4, GIJL_DS4_SERVER=/path/to/your/ds4-server, GIJL_REBUILD_DS4=1,
|
| 380 |
+
GIJL_DS4_REF=<tag/branch to pin>, GIJL_DS4_BUILD=cpu|cuda-generic, GIJL_REASONING_GEN=low, GIJL_REASONING_SCOUT=medium
|
| 381 |
"""
|
| 382 |
|
| 383 |
import os
|
|
|
|
| 549 |
SEARCH_LANGUAGE_CYCLE = ["arabic", "english"]
|
| 550 |
|
| 551 |
# --- inference backend -------------------------------------------------------
|
| 552 |
+
# "ds4" (default): antirez/ds4 (DwarfStar) ds4-server + its own GGUF, built from source with `make cpu` (CPU) -- the
|
| 553 |
+
# engine written specifically for DeepSeek-V4-Flash; llama.cpp is no longer used. See section 5b.
|
| 554 |
# "hf": the old transformers path (only kept for reference; on TPU it produced ~3 chars/s and hung).
|
| 555 |
+
BACKEND = os.environ.get("GIJL_BACKEND", "ds4").strip().lower()
|
| 556 |
+
GGUF_REPO = os.environ.get("GIJL_GGUF_REPO", "antirez/deepseek-v4-gguf") # ds4's own GGUF repo (the unsloth/llama.cpp GGUFs do NOT load in ds4)
|
| 557 |
+
GGUF_QUANT = os.environ.get("GIJL_GGUF_QUANT", "q2") # q2 (~81GiB, ds4f-q2, default) | q2-q4 (~98GB) | q4 (~153GiB) -- see DS4_GGUF_FILES; q2-q4 also fits the 103GB ceiling but needs more RAM
|
| 558 |
+
DS4_CTX = int(os.environ.get("GIJL_CTX", "12288")) # tokens passed to ds4-server --ctx
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 559 |
# Meta's recommended sampling is temperature 1.0 / top_p 0.95 / top_k 64. A slightly lower temperature
|
| 560 |
# suits structured-JSON tasks; top_p/top_k stay as recommended.
|
| 561 |
TOP_P, TOP_K = 0.95, 64
|
|
|
|
| 1328 |
|
| 1329 |
|
| 1330 |
# ---------------------------------------------------------------------------
|
| 1331 |
+
# 5b. ds4 (DwarfStar) backend (DEFAULT): a DeepSeek-V4-Flash-specific engine, CPU build (`make cpu`) or GPU build.
|
| 1332 |
+
# Replaces the llama.cpp backend (llama-server crashed on this model, see the v8.1 field report above).
|
| 1333 |
+
# ds4 only runs its own GGUFs (antirez/deepseek-v4-gguf) and renders the chat template itself.
|
|
|
|
|
|
|
|
|
|
| 1334 |
# ---------------------------------------------------------------------------
|
| 1335 |
def _run(cmd, timeout=60, env=None):
|
| 1336 |
try:
|
|
|
|
| 1398 |
return (acc["ram_gb"] or 24.0) * 0.75 - 2.0
|
| 1399 |
|
| 1400 |
|
| 1401 |
+
# --- ds4 (DwarfStar) backend: the DeepSeek-V4-Flash-specific engine, replaces llama.cpp ------------------------
|
| 1402 |
+
# WHY (field report in the module docstring, v8.1): llama-server crashed on CPU at startup with this model
|
| 1403 |
+
# (GGML_ASSERT(*cur_backend_id != -1) in ggml_backend_sched_split_graph) -- llama.cpp's DeepSeek-V4 support is
|
| 1404 |
+
# still the weak spot, not this script. antirez/ds4 is an inference engine written ONLY for DeepSeek V4 Flash:
|
| 1405 |
+
# * `make cpu` builds a CPU-only ds4-server (Linux, plain C, no CUDA/Metal needed) -- confirmed in its Makefile.
|
| 1406 |
+
# * it runs ONLY its own GGUF files (repo antirez/deepseek-v4-gguf, "-0731" files = Flash 0731), NOT the
|
| 1407 |
+
# unsloth/llama.cpp GGUFs. GIJL_GGUF_REPO/GIJL_GGUF_QUANT therefore now name THAT repo and one of the
|
| 1408 |
+
# DS4_GGUF_FILES keys below (q2 ~81GiB | q2-q4 ~98GB | q4 ~153GiB).
|
| 1409 |
+
# * it renders the DeepSeek chat template itself (incl. thinking on/off), so this client sends plain chat
|
| 1410 |
+
# `messages` to POST /v1/chat/completions instead of a locally-rendered raw prompt to /completion. The tokenizer
|
| 1411 |
+
# is therefore not needed for inference any more. `tools` is deliberately NOT forwarded: the atem: tool protocol
|
| 1412 |
+
# is taught in the system prompt (see section 6), and a native `tools` field would make the server parse
|
| 1413 |
+
# DeepSeek's own DSML tool calls into a structured field this pipeline never reads.
|
| 1414 |
+
DS4_REPO_URL = os.environ.get("GIJL_DS4_REPO_URL", "https://github.com/antirez/ds4")
|
| 1415 |
+
DS4_REF = os.environ.get("GIJL_DS4_REF", "").strip() # optional tag/branch/commit to pin (main moves fast)
|
| 1416 |
+
DS4_BUILD_TARGET = os.environ.get("GIJL_DS4_BUILD", "cpu").strip() # `make <target>`; anything but "cpu" is a GPU build
|
| 1417 |
+
DS4_BUILD_ROOT = os.environ.get("GIJL_DS4_BUILD_DIR", "/tmp/gijl_ds4_build")
|
| 1418 |
+
DS4_BUILD_TIMEOUT_S = 30 * 60
|
| 1419 |
+
DS4_LOAD_TIMEOUT_S = 3600 # ~80-100GB GGUF has to be read from disk first
|
| 1420 |
+
DS4_MODEL_ALIAS = "deepseek-v4-flash" # the server ignores it (the GGUF picks the model)
|
| 1421 |
+
DS4_GGUF_FILES = { # exact names from ds4's own download_model.sh (ds4f-q2 / ds4f-q2-q4 / ds4f-q4)
|
| 1422 |
+
"q2": "DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-chat-v2-imatrix-0731.gguf",
|
| 1423 |
+
"q2-q4": "DeepSeek-V4-Flash-Layers37-42Q4KExperts-OtherExpertLayersIQ2XXSGateUp-Q2KDown-AProjQ8-SExpQ8-OutQ8-chat-v2-imatrix-fixed-0731.gguf",
|
| 1424 |
+
"q4": "DeepSeek-V4-Flash-Q4KExperts-F16HC-F16Compressor-F16Indexer-Q8Attn-Q8Shared-Q8Out-chat-v2-imatrix-0731.gguf",
|
| 1425 |
+
}
|
| 1426 |
+
DS4_GGUF_SIZE_GB = {"q2": 87.0, "q2-q4": 98.0, "q4": 164.0} # approximate, from ds4's own download notes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1427 |
|
| 1428 |
|
| 1429 |
def download_gguf(acc: dict):
|
| 1430 |
+
"""-> (path_to_gguf, {"quant","size_gb"}). Stored in the default HF cache (NOT /kaggle/working)."""
|
| 1431 |
+
from huggingface_hub import hf_hub_download
|
|
|
|
| 1432 |
token = os.environ.get("HF_TOKEN")
|
| 1433 |
explicit_file = os.environ.get("GIJL_GGUF_FILE")
|
| 1434 |
if explicit_file:
|
| 1435 |
path = hf_hub_download(GGUF_REPO, explicit_file, token=token)
|
| 1436 |
return path, {"quant": explicit_file, "size_gb": os.path.getsize(path) / 1e9}
|
| 1437 |
+
tag = re.sub(r"^ds4f-", "", (GGUF_QUANT or "q2").strip().lower())
|
| 1438 |
+
if tag not in DS4_GGUF_FILES:
|
| 1439 |
+
raise RuntimeError(f"GIJL_GGUF_QUANT={GGUF_QUANT!r} is not a ds4 quant; use one of {sorted(DS4_GGUF_FILES)} "
|
| 1440 |
+
f"(or GIJL_GGUF_FILE=<exact file name inside {GGUF_REPO}>)")
|
| 1441 |
+
size_gb = DS4_GGUF_SIZE_GB[tag]
|
| 1442 |
+
ram = acc.get("ram_gb") or 0.0
|
| 1443 |
+
if ram and size_gb > ram * 0.92:
|
| 1444 |
+
print(f"[ds4][WARNING] quant={tag} (~{size_gb:.0f}GB) is close to / above this machine's RAM ({ram:.0f}GB): "
|
| 1445 |
+
f"expect swapping or a failed load. Try GIJL_GGUF_QUANT=q2 (~87GB).", flush=True)
|
| 1446 |
+
print(f"[ds4] model: {GGUF_REPO}/{DS4_GGUF_FILES[tag]} quant={tag} (~{size_gb:.0f}GB, RAM {ram:.0f}GB)", flush=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1447 |
t0 = time.time()
|
| 1448 |
+
path = hf_hub_download(GGUF_REPO, DS4_GGUF_FILES[tag], token=token)
|
| 1449 |
+
print(f"[ds4] GGUF ready in {time.time() - t0:.0f}s: {path}", flush=True)
|
| 1450 |
+
return path, {"quant": tag, "size_gb": os.path.getsize(path) / 1e9}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1451 |
|
| 1452 |
|
| 1453 |
+
# --- getting a ds4-server binary ---------------------------------------------------
|
| 1454 |
+
def _ds4_binary_ok(binary: str) -> bool:
|
| 1455 |
+
rc, out, err = _run([binary, "--help"], 90, None)
|
| 1456 |
+
return "--ctx" in (out + err)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1457 |
|
| 1458 |
|
| 1459 |
+
def ensure_ds4_server():
|
| 1460 |
+
"""-> (binary, workdir). Order: GIJL_DS4_SERVER override, cached build, fresh `make <target>` of antirez/ds4."""
|
| 1461 |
+
override = os.environ.get("GIJL_DS4_SERVER")
|
| 1462 |
+
if override:
|
| 1463 |
+
return override, os.path.dirname(override) or None
|
| 1464 |
+
src = os.path.join(DS4_BUILD_ROOT, "ds4")
|
| 1465 |
+
binary = os.path.join(src, "ds4-server")
|
| 1466 |
+
if os.path.isfile(binary) and os.environ.get("GIJL_REBUILD_DS4") != "1" and _ds4_binary_ok(binary):
|
| 1467 |
+
print(f"[ds4] using cached build {binary}", flush=True)
|
| 1468 |
+
return binary, src
|
| 1469 |
+
for tool in ("git", "make", "cc"):
|
| 1470 |
+
if not shutil.which(tool):
|
| 1471 |
+
raise RuntimeError(f"cannot build ds4: '{tool}' is not installed in this container "
|
| 1472 |
+
f"(or point GIJL_DS4_SERVER at a ds4-server binary you built elsewhere)")
|
| 1473 |
+
log = os.path.join(DS4_BUILD_ROOT, "build.log")
|
| 1474 |
+
os.makedirs(DS4_BUILD_ROOT, exist_ok=True)
|
| 1475 |
+
shutil.rmtree(src, ignore_errors=True) # a half-finished clone/build is worse than a fresh one
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1476 |
t0 = time.time()
|
| 1477 |
+
if _run_logged(["git", "clone", "--depth", "1"] + (["--branch", DS4_REF] if DS4_REF else []) + [DS4_REPO_URL, src],
|
| 1478 |
+
os.environ.copy(), log, "ds4-git", 600) != 0:
|
| 1479 |
+
raise RuntimeError(f"git clone of {DS4_REPO_URL} failed:\n{_tail_file(log, 15)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1480 |
jobs = str(max(2, os.cpu_count() or 4))
|
| 1481 |
+
print(f"[ds4][build] make {DS4_BUILD_TARGET} -j{jobs} (a few minutes, plain C) ...", flush=True)
|
| 1482 |
+
if _run_logged(["make", "-C", src, DS4_BUILD_TARGET, "-j", jobs], os.environ.copy(), log, "ds4-build",
|
| 1483 |
+
DS4_BUILD_TIMEOUT_S) != 0 or not os.path.isfile(binary):
|
| 1484 |
+
raise RuntimeError(f"ds4 build failed (make {DS4_BUILD_TARGET}):\n{_tail_file(log, 30)}")
|
| 1485 |
+
if not _ds4_binary_ok(binary):
|
| 1486 |
+
raise RuntimeError(f"the freshly built ds4-server does not run here: {binary}")
|
| 1487 |
+
print(f"[ds4][build] done in {time.time() - t0:.0f}s: {binary}", flush=True)
|
| 1488 |
+
return binary, src
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1489 |
|
| 1490 |
|
| 1491 |
def _free_port() -> int:
|
|
|
|
| 1494 |
return s.getsockname()[1]
|
| 1495 |
|
| 1496 |
|
| 1497 |
+
class Ds4Server:
|
| 1498 |
+
"""Owns the ds4-server subprocess: starts it, waits for /v1/models, restarts it if it dies, stops it at exit."""
|
| 1499 |
|
| 1500 |
+
def __init__(self, binary, workdir, model_path, kind, ctx, model_gb=0.0, extra_args=None):
|
| 1501 |
+
self.binary, self.workdir, self.model_path, self.kind, self.ctx = binary, workdir, model_path, kind, ctx
|
| 1502 |
+
self.model_gb, self.extra_args = model_gb, list(extra_args or [])
|
| 1503 |
self.port, self.proc, self.restarts, self._help_text = _free_port(), None, 0, None
|
| 1504 |
+
self.log_path = os.path.join(WORK_DIR, "ds4-server.log")
|
| 1505 |
atexit.register(self.stop)
|
| 1506 |
|
| 1507 |
@property
|
|
|
|
| 1510 |
|
| 1511 |
def _has(self, flag: str) -> bool:
|
| 1512 |
if self._help_text is None:
|
| 1513 |
+
rc, out, err = _run([self.binary, "--help"], 90, None)
|
| 1514 |
self._help_text = out + err
|
| 1515 |
return flag in self._help_text
|
| 1516 |
|
| 1517 |
def command(self) -> list:
|
| 1518 |
+
cmd = [self.binary, "-m", self.model_path, "--host", "127.0.0.1", "--port", str(self.port), "--ctx", str(self.ctx)]
|
| 1519 |
+
if self._has("--threads"):
|
| 1520 |
+
cmd += ["--threads", "16"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1521 |
return cmd + self.extra_args
|
| 1522 |
|
| 1523 |
def tail(self, n: int = 25) -> str:
|
|
|
|
| 1527 |
import requests
|
| 1528 |
os.makedirs(os.path.dirname(self.log_path), exist_ok=True)
|
| 1529 |
cmd = self.command()
|
| 1530 |
+
print("[ds4] starting: " + " ".join(cmd), flush=True)
|
| 1531 |
self._log = open(self.log_path, "ab")
|
| 1532 |
+
self.proc = subprocess.Popen(cmd, stdout=self._log, stderr=subprocess.STDOUT, cwd=self.workdir,
|
| 1533 |
start_new_session=True)
|
| 1534 |
t0, last = time.time(), time.time()
|
| 1535 |
+
while time.time() - t0 < DS4_LOAD_TIMEOUT_S:
|
| 1536 |
if self.proc.poll() is not None:
|
| 1537 |
+
raise RuntimeError(f"ds4-server exited during startup (code {self.proc.returncode}). Log tail:\n{self.tail(40)}")
|
| 1538 |
try:
|
| 1539 |
+
r = requests.get(self.base_url + "/v1/models", timeout=5)
|
| 1540 |
+
if r.status_code == 200:
|
| 1541 |
+
print(f"[ds4] server ready after {time.time() - t0:.0f}s on port {self.port}", flush=True)
|
|
|
|
| 1542 |
return
|
| 1543 |
except (requests.RequestException, ValueError):
|
| 1544 |
pass
|
| 1545 |
if time.time() - last > 30:
|
| 1546 |
+
print(f"[ds4] loading model ... {time.time() - t0:.0f}s", flush=True)
|
| 1547 |
last = time.time()
|
| 1548 |
time.sleep(2)
|
| 1549 |
+
raise TimeoutError(f"ds4-server not ready after {DS4_LOAD_TIMEOUT_S}s. Log tail:\n{self.tail(40)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1550 |
|
| 1551 |
def ensure_running(self):
|
| 1552 |
if self.proc is not None and self.proc.poll() is None:
|
| 1553 |
return
|
| 1554 |
self.restarts += 1
|
| 1555 |
+
print(f"[ds4][WARNING] server is not running (restart #{self.restarts}). Log tail:\n{self.tail(15)}", flush=True)
|
| 1556 |
if self.restarts > 3:
|
| 1557 |
+
raise RuntimeError("ds4-server keeps dying; giving up")
|
| 1558 |
self.port = _free_port()
|
| 1559 |
self.start()
|
| 1560 |
|
|
|
|
| 1572 |
pass
|
| 1573 |
|
| 1574 |
|
| 1575 |
+
class Ds4Client:
|
| 1576 |
+
"""Same interface as ModelClient.chat(): streams POST /v1/chat/completions from ds4-server (OpenAI-style SSE).
|
| 1577 |
+
- ds4 renders the DeepSeek chat template itself, so `messages` go over as plain chat. Messages with role='tool'
|
| 1578 |
+
are folded into user messages (the same fallback _render_chat uses) and consecutive same-role messages are
|
| 1579 |
+
merged, so the server only ever sees a clean system/user/assistant alternation.
|
| 1580 |
+
- thinking follows _CHAT_TEMPLATE_EXTRA_KWARGS["enable_thinking"] (False for this worker) via the request's
|
| 1581 |
+
`think` field, which is ds4's documented switch.
|
| 1582 |
+
- a separately streamed reasoning_content is NOT part of the returned text (only visible `content` is).
|
| 1583 |
+
- closing the HTTP connection is how a timeout cancels generation (assumed to be honored by ds4-server like any
|
| 1584 |
+
SSE server -- NOT verified here; if the log shows generation continuing after a [gen][TIMEOUT], that is why)."""
|
| 1585 |
+
|
| 1586 |
+
def __init__(self, server: Ds4Server, kind: str):
|
| 1587 |
+
self.server, self.kind, self.hardware = server, kind, kind
|
| 1588 |
+
self.tok = None # no local tokenizer: ds4 tokenizes and renders the template itself
|
| 1589 |
+
self.ctx = server.ctx
|
| 1590 |
+
self.extra_payload = {} # (tests / advanced use) merged into every request
|
| 1591 |
+
self._dropped = set() # optional request fields this server build rejected with HTTP 400
|
| 1592 |
+
|
| 1593 |
+
@staticmethod
|
| 1594 |
+
def _wire_messages(messages) -> list:
|
| 1595 |
+
out = []
|
| 1596 |
+
for m in messages:
|
| 1597 |
+
role, content = m.get("role", "user"), str(m.get("content", "") or "")
|
| 1598 |
+
if role == "tool":
|
| 1599 |
+
role, content = "user", f"[tool result from {m.get('name', 'tool')}]\n{content}"
|
| 1600 |
+
if out and out[-1]["role"] == role and role != "system":
|
| 1601 |
+
out[-1]["content"] += "\n\n" + content
|
| 1602 |
+
else:
|
| 1603 |
+
out.append({"role": role, "content": content})
|
| 1604 |
+
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1605 |
|
| 1606 |
def chat(self, messages, max_new_tokens=GEN_MAX_NEW_TOKENS, temperature=0.4, tools=None):
|
| 1607 |
import requests
|
|
|
|
| 1608 |
self.server.ensure_running()
|
| 1609 |
+
payload = {"model": DS4_MODEL_ALIAS, "messages": self._wire_messages(messages), "stream": True,
|
| 1610 |
+
"temperature": temperature, "top_p": TOP_P, "top_k": TOP_K, "max_tokens": max_new_tokens,
|
| 1611 |
+
"think": bool(_CHAT_TEMPLATE_EXTRA_KWARGS.get("enable_thinking", True)),
|
| 1612 |
+
"stream_options": {"include_usage": True}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1613 |
payload.update(self.extra_payload)
|
| 1614 |
+
for k in self._dropped:
|
| 1615 |
+
payload.pop(k, None)
|
| 1616 |
|
| 1617 |
gap = GEN_PER_CHUNK_GAP_TIMEOUT_S.get(self.kind, 300)
|
| 1618 |
cap = GEN_MAX_TOTAL_S.get(self.kind, 900)
|
| 1619 |
+
chunks, usage, err, timed_out, resp, finish = [], {}, None, False, None, None
|
| 1620 |
start = last_data = last_hb = time.time()
|
| 1621 |
try:
|
| 1622 |
+
for _attempt in range(2):
|
| 1623 |
+
resp = requests.post(self.server.base_url + "/v1/chat/completions", json=payload, stream=True,
|
| 1624 |
+
timeout=(15, gap))
|
| 1625 |
+
if resp.status_code == 200:
|
| 1626 |
+
break
|
| 1627 |
body = resp.text[:400]
|
| 1628 |
+
resp.close()
|
| 1629 |
+
optional = [k for k in ("top_k", "stream_options") if k in payload and k in body]
|
| 1630 |
+
if resp.status_code == 400 and optional and _attempt == 0:
|
| 1631 |
+
for k in optional: # this build does not know the field -> drop it for good and retry
|
| 1632 |
+
self._dropped.add(k)
|
| 1633 |
+
payload.pop(k, None)
|
| 1634 |
+
print(f"[ds4] server rejected {optional}; retrying without", flush=True)
|
| 1635 |
+
continue
|
| 1636 |
+
low = body.lower()
|
| 1637 |
+
err = ("context_too_long" if resp.status_code == 400 and
|
| 1638 |
+
("context" in low or "too long" in low or "exceed" in low) else
|
| 1639 |
+
f"HTTP {resp.status_code}: {_redact(body)}")
|
| 1640 |
+
resp = None
|
| 1641 |
+
break
|
| 1642 |
+
if resp is not None:
|
| 1643 |
+
# bytes -> utf-8 ourselves (Arabic!); chunk_size=1 so keep-alive pings can't hide a stalled server.
|
| 1644 |
for raw in resp.iter_lines(chunk_size=1):
|
| 1645 |
now = time.time()
|
| 1646 |
if now - start > cap:
|
|
|
|
| 1653 |
break
|
| 1654 |
line = raw.decode("utf-8", errors="replace") if isinstance(raw, bytes) else raw
|
| 1655 |
if not line.startswith("data:"):
|
| 1656 |
+
continue # SSE keep-alive pings / blank lines / comments
|
| 1657 |
+
data = line[5:].strip()
|
| 1658 |
+
if data == "[DONE]":
|
| 1659 |
+
break
|
| 1660 |
try:
|
| 1661 |
+
d = json.loads(data)
|
| 1662 |
except ValueError:
|
| 1663 |
continue
|
| 1664 |
if "error" in d:
|
|
|
|
| 1666 |
err = _redact(str(e.get("message", e)) if isinstance(e, dict) else str(e))
|
| 1667 |
break
|
| 1668 |
last_data = now
|
| 1669 |
+
if isinstance(d.get("usage"), dict):
|
| 1670 |
+
usage = d["usage"]
|
| 1671 |
+
for ch in d.get("choices") or []:
|
| 1672 |
+
delta = ch.get("delta") or {}
|
| 1673 |
+
if delta.get("content"):
|
| 1674 |
+
chunks.append(delta["content"])
|
| 1675 |
+
finish = ch.get("finish_reason") or finish
|
| 1676 |
if now - last_hb > 30:
|
| 1677 |
print(f"[gen] {sum(len(c) for c in chunks)} chars, {now - start:.0f}s elapsed ({self.kind})", flush=True)
|
| 1678 |
last_hb = now
|
| 1679 |
except requests.RequestException as e:
|
| 1680 |
+
# While streaming, requests surfaces a socket read timeout as ConnectionError("... Read timed out").
|
|
|
|
| 1681 |
if isinstance(e, requests.exceptions.Timeout) or "timed out" in str(e).lower():
|
| 1682 |
+
print(f"[gen][TIMEOUT] ds4-server sent nothing for {gap}s -- treating as stuck.", flush=True)
|
| 1683 |
timed_out = True
|
| 1684 |
else:
|
| 1685 |
err = f"{type(e).__name__}: {_redact(str(e))[:200]}"
|
|
|
|
| 1687 |
if resp is not None:
|
| 1688 |
resp.close() # closing the connection cancels generation on the server
|
| 1689 |
|
| 1690 |
+
n_prompt, gen = int(usage.get("prompt_tokens") or -1), int(usage.get("completion_tokens") or 0)
|
| 1691 |
+
el = max(time.time() - start, 1e-6)
|
| 1692 |
+
print(f"[ds4] prompt {n_prompt if n_prompt >= 0 else '?'} tok | generated "
|
| 1693 |
+
f"{gen if gen else '?'} tok in {el:.0f}s{f' (~{gen / el:.1f} tok/s incl. prefill)' if gen else ''} "
|
| 1694 |
+
f"| finish={finish}", flush=True)
|
|
|
|
|
|
|
| 1695 |
if err and err != "context_too_long":
|
| 1696 |
+
print(f"[ds4] request failed: {err}", flush=True)
|
| 1697 |
+
return {"content": "".join(chunks), "error": err, "timed_out": timed_out, "prompt_tokens": n_prompt,
|
| 1698 |
+
"cached_tokens": 0, "gen_tokens": gen}
|
|
|
|
| 1699 |
|
| 1700 |
def warmup(self):
|
| 1701 |
"""One tiny generation at startup: proves the whole path works and prints the real speed."""
|
| 1702 |
r = self.chat([{"role": "user", "content": "Reply with the single word: ready"}], max_new_tokens=24, temperature=0.0)
|
| 1703 |
ok = bool(r["content"].strip()) and not r["error"]
|
| 1704 |
+
print(f"[ds4] warmup {'ok' if ok else 'FAILED'}: {r['content'].strip()[:60]!r} {r['error'] or ''}", flush=True)
|
| 1705 |
return ok
|
| 1706 |
|
| 1707 |
|
| 1708 |
+
def start_ds4_backend():
|
| 1709 |
+
"""-> (client, server). Model download and ds4 build run in parallel."""
|
| 1710 |
from concurrent.futures import ThreadPoolExecutor
|
| 1711 |
acc = detect_accelerators()
|
| 1712 |
+
kind = "cpu" if DS4_BUILD_TARGET == "cpu" else "gpu"
|
| 1713 |
+
print(f"[hardware] ds4 backend (make {DS4_BUILD_TARGET}) | RAM {acc['ram_gb']:.1f}GB | CPUs {acc['cpus']}", flush=True)
|
| 1714 |
+
if kind == "cpu" and acc["kind"] == "gpu":
|
| 1715 |
+
print("[hardware][WARNING] an NVIDIA GPU is present but GIJL_DS4_BUILD=cpu (default) -> CPU inference. "
|
| 1716 |
+
"For GPU set GIJL_DS4_BUILD=cuda-generic (builds ds4's CUDA path).", flush=True)
|
| 1717 |
+
with ThreadPoolExecutor(max_workers=2) as ex:
|
| 1718 |
+
f_model, f_bin = ex.submit(download_gguf, acc), ex.submit(ensure_ds4_server)
|
| 1719 |
+
(model_path, meta), (binary, workdir) = f_model.result(), f_bin.result()
|
| 1720 |
+
server = Ds4Server(binary, workdir, model_path, kind, DS4_CTX, meta["size_gb"])
|
| 1721 |
+
print(f"[ds4] inference device: {'GPU' if kind == 'gpu' else 'CPU'}", flush=True)
|
|
|
|
|
|
|
|
|
|
| 1722 |
server.start()
|
| 1723 |
+
client = Ds4Client(server, kind)
|
| 1724 |
+
print(f"[ds4] context per request: {client.ctx} tokens", flush=True)
|
|
|
|
|
|
|
|
|
|
| 1725 |
if not client.warmup():
|
| 1726 |
+
print("[ds4][WARNING] warmup produced no text; continuing, but check ds4-server.log", flush=True)
|
| 1727 |
return client, server
|
| 1728 |
|
| 1729 |
|
|
|
|
| 4168 |
# filenames (MANIFEST.json/README.md) needed a MODEL_TAG suffix -- done where they are defined above.
|
| 4169 |
pats = ([f"{m['folder']}/*" for m in DATA_TYPES.values()]
|
| 4170 |
+ [f"state/{MODEL_TAG}/*", os.path.basename(MANIFEST_PATH), os.path.basename(README_PATH)])
|
| 4171 |
+
return pats
|
| 4172 |
|
| 4173 |
|
| 4174 |
def _with_retry(fn, what: str, tries: int = 3, base_delay: float = 2.0):
|
|
|
|
| 4202 |
# other two workers' data down into every container unnecessarily.
|
| 4203 |
_with_retry(lambda: snapshot_download(
|
| 4204 |
repo_id=TARGET_DATASET_REPO, repo_type="dataset", token=token, local_dir=WORK_DIR,
|
| 4205 |
+
allow_patterns=[f"{m['folder']}/{MODEL_TAG}/*" for m in DATA_TYPES.values()] + [f"state/{MODEL_TAG}/*"]), "restore")
|
|
|
|
| 4206 |
print(f"[restore] pulled prior state from {TARGET_DATASET_REPO} (worker={MODEL_TAG})", flush=True)
|
| 4207 |
except Exception as e:
|
| 4208 |
print(f"[restore] nothing to restore yet ({_redact(str(e))})", flush=True)
|
|
|
|
| 4231 |
# 14. Main loop -- single process, runs until the container stops (or GIJL_SESSION_BUDGET_S, if set)
|
| 4232 |
# ---------------------------------------------------------------------------
|
| 4233 |
def _start_backend():
|
| 4234 |
+
"""-> (client, close). close() stops ds4-server (no-op for the transformers backend)."""
|
| 4235 |
if BACKEND == "hf":
|
| 4236 |
hardware = detect_hardware()
|
| 4237 |
print(f"[hardware] transformers backend, selected: {hardware}", flush=True)
|
| 4238 |
if hardware == "tpu":
|
| 4239 |
print("[hardware][WARNING] the transformers path on TPU was ~3 chars/s and hung in XLA compilation (see your TPU "
|
| 4240 |
+
"log). Use the default ds4 backend instead.", flush=True)
|
| 4241 |
model, tok = load_model(hardware)
|
| 4242 |
return ModelClient(model, tok, hardware), (lambda: None)
|
| 4243 |
+
client, server = start_ds4_backend()
|
| 4244 |
return client, server.stop
|
| 4245 |
|
| 4246 |
|
|
|
|
| 4320 |
print(f"[cycle] took {time.time() - t_cycle:.0f}s. " + _status_line(cycle, state), flush=True)
|
| 4321 |
maybe_sync(state)
|
| 4322 |
if smoke:
|
| 4323 |
+
print("[smoke] one cycle finished -- see the [ds4] tok/s lines and [gen]/[write] lines above. "
|
| 4324 |
"Unset GIJL_SMOKE for the full run.", flush=True)
|
| 4325 |
finally:
|
| 4326 |
try:
|