Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e5a37a21f | ||
|
|
1e4fa14772 | ||
|
|
e6fba5d97e | ||
|
|
ca1475543d | ||
|
|
f83c241668 | ||
|
|
b4d7f6e7fe |
No files matched your search
+79
-18
@@ -22,6 +22,9 @@ Mechanics notes:
|
|||||||
prompts do not bias the Hessians (the corpus is drawn round-robin over
|
prompts do not bias the Hessians (the corpus is drawn round-robin over
|
||||||
source classes, so loss is class-neutral as long as failures are rare;
|
source classes, so loss is class-neutral as long as failures are rare;
|
||||||
a systematic 4xx is loud in the log and must be fixed, not ignored).
|
a systematic 4xx is loud in the log and must be fixed, not ignored).
|
||||||
|
- a round that seals no tap bytes is a FAILURE, not a retry: the engine
|
||||||
|
answering 200 while the tap directory is wrong, full or read-only is
|
||||||
|
how a corpus gets pushed four times for nothing.
|
||||||
|
|
||||||
Usage: calib-push.py [--port 7770] [--parallel 64] [--container arbench]
|
Usage: calib-push.py [--port 7770] [--parallel 64] [--container arbench]
|
||||||
[--corpus /home/dank/corpora/out/calib.jsonl] [--max-rounds 4]
|
[--corpus /home/dank/corpora/out/calib.jsonl] [--max-rounds 4]
|
||||||
@@ -29,11 +32,18 @@ Usage: calib-push.py [--port 7770] [--parallel 64] [--container arbench]
|
|||||||
import argparse
|
import argparse
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
|
_HERE = os.path.dirname(os.path.abspath(__file__)) if "__file__" in globals() else os.getcwd()
|
||||||
|
if _HERE not in sys.path:
|
||||||
|
sys.path.insert(0, _HERE)
|
||||||
|
|
||||||
|
import qwen38_common as qc # noqa: E402 - sibling module, same directory
|
||||||
|
|
||||||
DONE_MARK = "calibration wrote"
|
DONE_MARK = "calibration wrote"
|
||||||
|
|
||||||
|
|
||||||
@@ -49,9 +59,9 @@ def tap_done(container):
|
|||||||
return DONE_MARK in (out.stdout + out.stderr)
|
return DONE_MARK in (out.stdout + out.stderr)
|
||||||
|
|
||||||
|
|
||||||
def push_one(url, prompt):
|
def push_one(url, prompt, model):
|
||||||
body = json.dumps({
|
body = json.dumps({
|
||||||
"model": "frank-archon",
|
"model": model,
|
||||||
"prompt": prompt,
|
"prompt": prompt,
|
||||||
"max_tokens": 1,
|
"max_tokens": 1,
|
||||||
"temperature": 0,
|
"temperature": 0,
|
||||||
@@ -68,38 +78,89 @@ def push_one(url, prompt):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def push_round(url, prompts, model, parallel):
|
||||||
|
"""Push the whole corpus once; returns how many requests were accepted."""
|
||||||
|
ok = 0
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(parallel) as ex:
|
||||||
|
for good in ex.map(lambda p: push_one(url, p, model), prompts):
|
||||||
|
ok += 1 if good else 0
|
||||||
|
return ok
|
||||||
|
|
||||||
|
|
||||||
|
def tap_bytes(path):
|
||||||
|
"""Bytes the tap has sealed so far; 0 when the directory is absent.
|
||||||
|
|
||||||
|
A tap whose directory is wrong, full or read-only stops writing while the
|
||||||
|
engine keeps answering 200 — the driver then re-pushes the whole corpus for
|
||||||
|
every round it is allowed and the loss is invisible until the hours are gone.
|
||||||
|
"""
|
||||||
|
total = 0
|
||||||
|
try:
|
||||||
|
for root, _, files in os.walk(path):
|
||||||
|
for f in files:
|
||||||
|
try:
|
||||||
|
total += os.path.getsize(os.path.join(root, f))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
except OSError:
|
||||||
|
return 0
|
||||||
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
ap = argparse.ArgumentParser()
|
ap = argparse.ArgumentParser()
|
||||||
ap.add_argument("--port", type=int, default=7770)
|
ap.add_argument("--port", type=int, default=7770)
|
||||||
ap.add_argument("--parallel", type=int, default=64)
|
ap.add_argument("--parallel", type=int, default=64)
|
||||||
ap.add_argument("--container", default="arbench")
|
ap.add_argument("--container", default="arbench")
|
||||||
|
ap.add_argument("--model", default="frank-archon")
|
||||||
|
ap.add_argument("--tap-dir", default="/home/dank/campaign/qwen38-w5nl64/calib")
|
||||||
ap.add_argument("--corpus", default="/home/dank/corpora/out/calib.jsonl")
|
ap.add_argument("--corpus", default="/home/dank/corpora/out/calib.jsonl")
|
||||||
ap.add_argument("--max-rounds", type=int, default=4)
|
ap.add_argument("--max-rounds", type=int, default=4)
|
||||||
a = ap.parse_args()
|
return ap.parse_args()
|
||||||
url = f"http://127.0.0.1:{a.port}/v1/completions"
|
|
||||||
|
|
||||||
|
|
||||||
|
def load_prompts(a) -> list[str]:
|
||||||
|
if not os.path.isfile(a.corpus):
|
||||||
|
qc.refuse(f"no corpus at {a.corpus}")
|
||||||
prompts = []
|
prompts = []
|
||||||
with open(a.corpus) as f:
|
for lineno, rec in qc.iter_jsonl(a.corpus):
|
||||||
for line in f:
|
if not isinstance(rec.get("prompt"), str):
|
||||||
line = line.strip()
|
qc.refuse(f"{a.corpus}:{lineno} has no string 'prompt' field")
|
||||||
if line:
|
prompts.append(rec["prompt"])
|
||||||
prompts.append(json.loads(line)["prompt"])
|
if not prompts:
|
||||||
print(f"corpus: {len(prompts)} prompts", flush=True)
|
qc.refuse(f"the corpus at {a.corpus} holds no prompts")
|
||||||
|
return prompts
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
a = parse_args()
|
||||||
|
url = f"http://127.0.0.1:{a.port}/v1/completions"
|
||||||
|
prompts = load_prompts(a)
|
||||||
|
chars = sum(len(p) for p in prompts)
|
||||||
|
print(f"corpus: {len(prompts)} prompts, ~{chars // 4} tokens (chars/4)",
|
||||||
|
flush=True)
|
||||||
|
|
||||||
for rnd in range(1, a.max_rounds + 1):
|
for rnd in range(1, a.max_rounds + 1):
|
||||||
if tap_done(a.container):
|
if tap_done(a.container):
|
||||||
break
|
break
|
||||||
|
before = tap_bytes(a.tap_dir)
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
ok = 0
|
ok = push_round(url, prompts, a.model, a.parallel)
|
||||||
with concurrent.futures.ThreadPoolExecutor(a.parallel) as ex:
|
dt = max(time.time() - t0, 1e-9)
|
||||||
for good in ex.map(lambda p: push_one(url, p), prompts):
|
|
||||||
ok += 1 if good else 0
|
|
||||||
dt = time.time() - t0
|
|
||||||
print(f"round {rnd}: {ok}/{len(prompts)} ok in {dt:.0f}s "
|
print(f"round {rnd}: {ok}/{len(prompts)} ok in {dt:.0f}s "
|
||||||
f"({sum(len(p) for p in prompts)/4/dt:.0f} chars/s est)",
|
f"({chars / 4 / dt:.0f} tokens/s est)", flush=True)
|
||||||
flush=True)
|
if ok == 0:
|
||||||
|
print("FAIL: not one prompt was accepted; fix the engine or the argv "
|
||||||
|
"before spending another round", flush=True)
|
||||||
|
sys.exit(1)
|
||||||
if tap_done(a.container):
|
if tap_done(a.container):
|
||||||
break
|
break
|
||||||
|
after = tap_bytes(a.tap_dir)
|
||||||
|
if after <= before:
|
||||||
|
print(f"FAIL: the tap at {a.tap_dir} sealed nothing this round "
|
||||||
|
f"({before} B before, {after} B after) — the engine is serving "
|
||||||
|
"without recording (wrong dir, full disk, read-only mount)", flush=True)
|
||||||
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
print("max rounds exhausted, tap not done", flush=True)
|
print("max rounds exhausted, tap not done", flush=True)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -71,10 +71,12 @@ PLAN_ONLY=0
|
|||||||
KEEP_RECIPE=0
|
KEEP_RECIPE=0
|
||||||
while [ "$#" -gt 0 ]; do
|
while [ "$#" -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--arm) ARMS+=("$2"); shift 2 ;;
|
--arm) [ "$#" -ge 2 ] || { echo "--arm needs a value (cd3|cd6|ao1)" >&2; exit 2; }
|
||||||
|
ARMS+=("$2"); shift 2 ;;
|
||||||
--dry-run) DRY=1; shift ;;
|
--dry-run) DRY=1; shift ;;
|
||||||
--plan-only) PLAN_ONLY=1; shift ;;
|
--plan-only) PLAN_ONLY=1; shift ;;
|
||||||
--target-count) TARGET_COUNT="$2"; shift 2 ;;
|
--target-count) [ "$#" -ge 2 ] || { echo "--target-count needs a value" >&2; exit 2; }
|
||||||
|
TARGET_COUNT="$2"; shift 2 ;;
|
||||||
--keep-recipe) KEEP_RECIPE=1; shift ;;
|
--keep-recipe) KEEP_RECIPE=1; shift ;;
|
||||||
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
@@ -124,6 +126,13 @@ fi
|
|||||||
for a in "${ARMS[@]}"; do
|
for a in "${ARMS[@]}"; do
|
||||||
[ -e "$OUTDIR/qwen38-w5nl64-$a.rad" ] && {
|
[ -e "$OUTDIR/qwen38-w5nl64-$a.rad" ] && {
|
||||||
echo "$OUTDIR/qwen38-w5nl64-$a.rad already exists; move it to trash first" >&2; exit 2; }
|
echo "$OUTDIR/qwen38-w5nl64-$a.rad already exists; move it to trash first" >&2; exit 2; }
|
||||||
|
# A killed run leaves its container behind (--rm only fires when podman itself
|
||||||
|
# exits), and the next arm then dies on "name already in use" AFTER the plan
|
||||||
|
# phase; refuse here, where the fix is one `podman rm -f`.
|
||||||
|
podman container exists "qwen38-convert-$a" && {
|
||||||
|
echo "container qwen38-convert-$a still exists (a killed run?);" \
|
||||||
|
"'podman rm -f qwen38-convert-$a' and re-run" >&2
|
||||||
|
exit 2; }
|
||||||
done
|
done
|
||||||
|
|
||||||
# --- the protected plane, spliced into a per-run copy of the template --------------------------
|
# --- the protected plane, spliced into a per-run copy of the template --------------------------
|
||||||
@@ -203,6 +212,7 @@ for arm in "${ARMS[@]}"; do
|
|||||||
rc=0; run_convert "$arm" || rc=$?
|
rc=0; run_convert "$arm" || rc=$?
|
||||||
[ "$rc" -eq 0 ] || { echo "arm $arm: rad-convert rc=$rc; log: $STATE/convert-$arm-$TS.log" >&2; overall=1; continue; }
|
[ "$rc" -eq 0 ] || { echo "arm $arm: rad-convert rc=$rc; log: $STATE/convert-$arm-$TS.log" >&2; overall=1; continue; }
|
||||||
out="$OUTDIR/qwen38-w5nl64-$arm.rad"
|
out="$OUTDIR/qwen38-w5nl64-$arm.rad"
|
||||||
|
[ -f "$out" ] || { echo "arm $arm: rad-convert exited 0 but wrote no $out; log: $STATE/convert-$arm-$TS.log" >&2; overall=1; continue; }
|
||||||
got_mib=$(( $(stat -c %s "$out") / 1048576 ))
|
got_mib=$(( $(stat -c %s "$out") / 1048576 ))
|
||||||
# The receipt: the artifact is what the plan predicted, within the metadata the plan does not
|
# The receipt: the artifact is what the plan predicted, within the metadata the plan does not
|
||||||
# count. An arm that came out materially smaller lost a plane somewhere.
|
# count. An arm that came out materially smaller lost a plane somewhere.
|
||||||
|
|||||||
+12
-10
@@ -74,12 +74,14 @@ FRANK_ARCHON_PLACEMENT=expert_tiered
|
|||||||
# residency -- ~3.2 GiB a card of experts now lives in host RAM and is read over the
|
# residency -- ~3.2 GiB a card of experts now lives in host RAM and is read over the
|
||||||
# x8 link when the router picks it. The ledger's coverage line is the referee, not
|
# x8 link when the router picks it. The ledger's coverage line is the referee, not
|
||||||
# this file: if vram_kv comes back under 700,000 tokens, raise this 1024 and redeploy.
|
# this file: if vram_kv comes back under 700,000 tokens, raise this 1024 and redeploy.
|
||||||
# 2026-10-29: 49,152 -> 66,560 (tp x 65 GiB, inside the 246 GiB the box has). The
|
# 2026-10-29 A/B, measured on both payloads and folded back the same day: at 66,560 the
|
||||||
# campaign rig's bf16 container at the shared step reported "kv cache 269.99 MiB a
|
# endpoint printed the identical `elastic 26.73 GiB / experts 20.72 / kv cache 5.64 /
|
||||||
# card: 7436 tokens", so 682 of the 1,294 calibration documents were refused at that
|
# vram_weights 24.30` it had printed at 49,152, opening the bigger pool with `0 B used`;
|
||||||
# pool; the pool, not the step, is what widens it (scripts/run-qwen38-bf16.sh). The
|
# the campaign rig (bf16, same step) moved `vram_kv 269.99 -> 271.25 MiB`, a rounding
|
||||||
# new cache line belongs to the next boot ledger, not to this comment.
|
# step — its window is fixed by its own n-gram table (95.37 GiB) and expert plane, not
|
||||||
FRANK_ARCHON_HOST_POOL_MIB=66560
|
# by this pool, so the refused calibration documents were fixed by cutting them to the
|
||||||
|
# window (scripts/rechunk-kld-corpus.py), not by widening the pool.
|
||||||
|
FRANK_ARCHON_HOST_POOL_MIB=49152
|
||||||
# VRAM left unclaimed per card (publisher default 96 MiB); the engine's settle
|
# VRAM left unclaimed per card (publisher default 96 MiB); the engine's settle
|
||||||
# floor is built from it.
|
# floor is built from it.
|
||||||
FRANK_ARCHON_GPU_HEADROOM_MIB=96
|
FRANK_ARCHON_GPU_HEADROOM_MIB=96
|
||||||
@@ -142,10 +144,10 @@ FRANK_ARCHON_NGRAM_PLACEMENT=ram
|
|||||||
# itself before refusing by name, so recovery is a boot, not a visit.
|
# itself before refusing by name, so recovery is a boot, not a visit.
|
||||||
# Floor = 2 x HOST_POOL + 48,824 (static weights + arena) + PREFIX_CACHE + MNBT + the
|
# Floor = 2 x HOST_POOL + 48,824 (static weights + arena) + PREFIX_CACHE + MNBT + the
|
||||||
# auto checkpoint store (MAX_SEQS x 10 slots of 111.98 MiB), per the bats ledger test;
|
# auto checkpoint store (MAX_SEQS x 10 slots of 111.98 MiB), per the bats ledger test;
|
||||||
# at the 66,560 pool with 4 slots that computes to 196,664 MiB (161,848 at the old
|
# at the 49,152 pool with 4 slots that computes to 161,848 MiB (196,664 at the
|
||||||
# 49,152 pool).
|
# 66,560 pool the 2026-10-29 A/B rejected).
|
||||||
FRANK_ARCHON_TTM_MIN_MIB=197000
|
FRANK_ARCHON_TTM_MIN_MIB=162000
|
||||||
FRANK_ARCHON_TTM_TARGET_MIB=224000
|
FRANK_ARCHON_TTM_TARGET_MIB=204800
|
||||||
# Escape hatch for experiments. Unset -> no extra argv; the serve script expands
|
# Escape hatch for experiments. Unset -> no extra argv; the serve script expands
|
||||||
# this unquoted on purpose, so keep multi-flag values as one string.
|
# this unquoted on purpose, so keep multi-flag values as one string.
|
||||||
FRANK_ARCHON_EXTRA_ARGS=
|
FRANK_ARCHON_EXTRA_ARGS=
|
||||||
|
|||||||
+175
-112
@@ -34,14 +34,25 @@ import json
|
|||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
from tokenizers import Tokenizer
|
|
||||||
|
_HERE = os.path.dirname(os.path.abspath(__file__)) if "__file__" in globals() else os.getcwd()
|
||||||
|
if _HERE not in sys.path:
|
||||||
|
sys.path.insert(0, _HERE)
|
||||||
|
|
||||||
|
import qwen38_common as qc # noqa: E402 - sibling module, same directory
|
||||||
|
|
||||||
CODE_EXT = (".sh", ".py", ".jinja", ".recipe", ".env", ".example",
|
CODE_EXT = (".sh", ".py", ".jinja", ".recipe", ".env", ".example",
|
||||||
".yaml", ".yml", ".bats", ".service", ".container", ".toml")
|
".yaml", ".yml", ".bats", ".service", ".container", ".toml")
|
||||||
JSONL_EXTS = (".jsonl", ".json")
|
JSONL_EXTS = (".jsonl", ".json")
|
||||||
DOC_DIR = "serverAppConfigs-docs"
|
DOC_DIR = "serverAppConfigs-docs"
|
||||||
|
FRAGMENT_TOKENS = 2400 # what one fragment may contribute
|
||||||
|
CLASS_MIX = {"code": .45, "prose": .30, "tools": .25}
|
||||||
|
HF_CAP = {"code": 40000, "prose": 60000, "tools": 0}
|
||||||
|
POOL_CYCLES = 40 # how many times a class pool may repeat before giving up
|
||||||
|
KLD_GUARD = 5000 # rendered conversations tried before declaring the pool short
|
||||||
|
|
||||||
FLEET_CMDS = [
|
FLEET_CMDS = [
|
||||||
"systemctl status frank-archon --no-pager",
|
"systemctl status frank-archon --no-pager",
|
||||||
@@ -142,7 +153,7 @@ def load_parquet_texts(path: str, kind: str, rng: random.Random,
|
|||||||
return out[:cap]
|
return out[:cap]
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def parse_args() -> argparse.Namespace:
|
||||||
ap = argparse.ArgumentParser(description=__doc__)
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
ap.add_argument("--repo", required=True)
|
ap.add_argument("--repo", required=True)
|
||||||
ap.add_argument("--tokenizer-json", required=True)
|
ap.add_argument("--tokenizer-json", required=True)
|
||||||
@@ -156,22 +167,26 @@ def main() -> None:
|
|||||||
ap.add_argument("--hf-code-parquet", default="")
|
ap.add_argument("--hf-code-parquet", default="")
|
||||||
ap.add_argument("--hf-prose-parquet", default="")
|
ap.add_argument("--hf-prose-parquet", default="")
|
||||||
ap.add_argument("--seed", type=int, default=20261006)
|
ap.add_argument("--seed", type=int, default=20261006)
|
||||||
a = ap.parse_args()
|
return ap.parse_args()
|
||||||
|
|
||||||
rng = random.Random(a.seed)
|
|
||||||
tokenizer = Tokenizer.from_file(a.tokenizer_json)
|
def load_template(template: str) -> tuple[Environment, str]:
|
||||||
env = Environment(loader=FileSystemLoader(os.path.dirname(a.template)),
|
"""The chat template as the engine will render it, plus its short name."""
|
||||||
|
env = Environment(loader=FileSystemLoader(os.path.dirname(template)),
|
||||||
trim_blocks=True, lstrip_blocks=True)
|
trim_blocks=True, lstrip_blocks=True)
|
||||||
env.filters["tojson"] = lambda v: json.dumps(v, ensure_ascii=False)
|
env.filters["tojson"] = lambda v: json.dumps(v, ensure_ascii=False)
|
||||||
env.filters["raise_exception"] = lambda m: (_ for _ in ()).throw(
|
env.filters["raise_exception"] = lambda m: (_ for _ in ()).throw(
|
||||||
ValueError(m))
|
ValueError(m))
|
||||||
tname = os.path.basename(a.template)
|
return env, os.path.basename(template)
|
||||||
|
|
||||||
|
|
||||||
|
def classify(repo: str) -> dict[str, list[str]]:
|
||||||
|
"""Walk the checkout into the three source classes the mix draws from."""
|
||||||
code, prose, jsonl = [], [], []
|
code, prose, jsonl = [], [], []
|
||||||
for root, dirs, files in os.walk(a.repo):
|
for root, dirs, files in os.walk(repo):
|
||||||
dirs[:] = [d for d in dirs if not d.startswith(".")]
|
dirs[:] = [d for d in dirs if not d.startswith(".")]
|
||||||
for f in sorted(files):
|
for f in sorted(files):
|
||||||
p = os.path.relpath(os.path.join(root, f), a.repo)
|
p = os.path.relpath(os.path.join(root, f), repo)
|
||||||
if DOC_DIR in p and f.endswith(".md"):
|
if DOC_DIR in p and f.endswith(".md"):
|
||||||
prose.append(p)
|
prose.append(p)
|
||||||
elif p.startswith(DOC_DIR):
|
elif p.startswith(DOC_DIR):
|
||||||
@@ -180,137 +195,185 @@ def main() -> None:
|
|||||||
code.append(p)
|
code.append(p)
|
||||||
elif f.endswith(JSONL_EXTS) and f != "package-lock.json":
|
elif f.endswith(JSONL_EXTS) and f != "package-lock.json":
|
||||||
jsonl.append(p)
|
jsonl.append(p)
|
||||||
# file-level split: bucket 0 -> KLD pool, 1..4 -> calibration pool
|
return {"code": code, "prose": prose, "tools": jsonl}
|
||||||
pools: dict[str, tuple[list, list]] = {}
|
|
||||||
for cls, lst in (("code", code), ("prose", prose), ("tools", jsonl)):
|
|
||||||
pools[cls] = ([p for p in lst if fnv(p) % 5 == 0],
|
|
||||||
[p for p in lst if fnv(p) % 5 != 0])
|
|
||||||
|
|
||||||
def text_of(p: str) -> str:
|
|
||||||
path = os.path.join(a.repo, p)
|
|
||||||
raw = open(path, encoding="utf-8", errors="replace").read()
|
|
||||||
if p.endswith(JSONL_EXTS):
|
|
||||||
lines = [json.loads(l)["content"] for l in raw.splitlines()
|
|
||||||
if l.strip() and "content" in l]
|
|
||||||
raw = "\n".join(lines)
|
|
||||||
toks = tokenizer.encode(raw, add_special_tokens=False).ids
|
|
||||||
return tokenizer.decode(toks[:2400], skip_special_tokens=True)
|
|
||||||
|
|
||||||
|
def split_pools(classes: dict[str, list[str]]) -> dict[str, tuple[list, list]]:
|
||||||
|
"""File-level split: bucket 0 -> KLD pool, everything else -> calibration."""
|
||||||
|
return {cls: ([p for p in lst if fnv(p) % 5 == 0],
|
||||||
|
[p for p in lst if fnv(p) % 5 != 0])
|
||||||
|
for cls, lst in classes.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def jsonl_content(raw: str) -> str:
|
||||||
|
"""The content fields of a content-keyed JSONL, or "" for anything else.
|
||||||
|
|
||||||
|
A .json in this repo is not necessarily a content-keyed JSONL: the substring
|
||||||
|
test passes on any line that merely mentions "content", and a list-shaped
|
||||||
|
document raises TypeError — both used to crash the whole build. Skip what
|
||||||
|
does not fit instead.
|
||||||
|
"""
|
||||||
|
lines = []
|
||||||
|
for line in raw.splitlines():
|
||||||
|
if not line.strip() or "content" not in line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
rec = json.loads(line)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
if isinstance(rec, dict) and isinstance(rec.get("content"), str):
|
||||||
|
lines.append(rec["content"])
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def fragment_text(repo: str, rel: str, tok) -> str:
|
||||||
|
"""One file as a fragment: its first FRAGMENT_TOKENS tokens, decoded."""
|
||||||
|
raw = open(os.path.join(repo, rel), encoding="utf-8",
|
||||||
|
errors="replace").read()
|
||||||
|
if rel.endswith(JSONL_EXTS):
|
||||||
|
raw = jsonl_content(raw)
|
||||||
|
ids = tok.encode(raw, add_special_tokens=False).ids
|
||||||
|
return tok.decode(ids[:FRAGMENT_TOKENS], skip_special_tokens=True)
|
||||||
|
|
||||||
|
|
||||||
|
def build_frags(a: argparse.Namespace, tok,
|
||||||
|
rng: random.Random) -> dict[str, dict[str, list[str]]]:
|
||||||
|
"""Both fragment pools per class, seeded so the same seed rebuilds the corpus."""
|
||||||
|
pools = split_pools(classify(a.repo))
|
||||||
cmds = harvest_cmds(a.repo, rng)
|
cmds = harvest_cmds(a.repo, rng)
|
||||||
# fragment pools per class, corpus-split first
|
|
||||||
frags: dict[str, dict[str, list[str]]] = {}
|
frags: dict[str, dict[str, list[str]]] = {}
|
||||||
for cls in pools:
|
for cls, (kld_files, cal_files) in pools.items():
|
||||||
kld_files, cal_files = pools[cls]
|
|
||||||
fr = {}
|
|
||||||
src = {"kld": kld_files, "cal": cal_files}
|
|
||||||
if cls == "tools":
|
if cls == "tools":
|
||||||
fr["kld"] = [tool_call(rng, cmds) for _ in range(600)]
|
frags[cls] = {"kld": [tool_call(rng, cmds) for _ in range(600)],
|
||||||
fr["cal"] = [tool_call(rng, cmds) for _ in range(40000)]
|
"cal": [tool_call(rng, cmds) for _ in range(40000)]}
|
||||||
else:
|
else:
|
||||||
fr["kld"] = [text_of(p) for p in src["kld"]]
|
frags[cls] = {"kld": [fragment_text(a.repo, p, tok) for p in kld_files],
|
||||||
fr["cal"] = [text_of(p) for p in src["cal"]]
|
"cal": [fragment_text(a.repo, p, tok) for p in cal_files]}
|
||||||
frags[cls] = fr
|
|
||||||
# per-class fragment caps: prose rows are short (~66 tok) so need many;
|
# per-class fragment caps: prose rows are short (~66 tok) so need many;
|
||||||
# codeparrot files are long (~2.6k tok) so a small cap suffices
|
# codeparrot files are long (~2.6k tok) so a small cap suffices
|
||||||
hfcap = {"code": 40000, "prose": 60000, "tools": 0}
|
|
||||||
for cls, spec in (("code", a.hf_code_parquet),
|
for cls, spec in (("code", a.hf_code_parquet),
|
||||||
("prose", a.hf_prose_parquet)):
|
("prose", a.hf_prose_parquet)):
|
||||||
for path in [p for p in spec.split(",") if p]:
|
for path in [p for p in spec.split(",") if p]:
|
||||||
frags[cls]["cal"].extend(
|
frags[cls]["cal"].extend(
|
||||||
load_parquet_texts(path, cls, rng, cap=hfcap[cls]))
|
load_parquet_texts(path, cls, rng, cap=HF_CAP[cls]))
|
||||||
for cls in frags:
|
for cls in frags:
|
||||||
rng.shuffle(frags[cls]["cal"])
|
rng.shuffle(frags[cls]["cal"])
|
||||||
|
return frags
|
||||||
|
|
||||||
def toks(s: str) -> int:
|
|
||||||
return len(tokenizer.encode(s, add_special_tokens=False).ids)
|
|
||||||
|
|
||||||
def emit_calib() -> tuple[int, list[dict]]:
|
def pack_class(cls: str, pool: list[str], want: int, tok,
|
||||||
targets = {"code": .45, "prose": .30, "tools": .25}
|
chunk_tokens: int) -> list[dict]:
|
||||||
rows: list[dict] = []
|
"""Greedily pack `pool` into chunk_tokens-sized calibration prompts."""
|
||||||
for cls, frac in targets.items():
|
if not pool:
|
||||||
want = int(a.calib_tokens * frac)
|
return []
|
||||||
pool = frags[cls]["cal"]
|
rows: list[dict] = []
|
||||||
|
# Count every fragment once: the loop cycles the pool up to POOL_CYCLES
|
||||||
|
# times, and re-tokenizing a 2.4k-token fragment on each pass dominated the
|
||||||
|
# build's wall time.
|
||||||
|
counts = [qc.n_tokens(tok, f) for f in pool]
|
||||||
|
got = 0
|
||||||
|
i = 0
|
||||||
|
buf: list[str] = []
|
||||||
|
nbuf = 0
|
||||||
|
while got < want:
|
||||||
|
j = i % len(pool)
|
||||||
|
frag = pool[j]
|
||||||
|
i += 1
|
||||||
|
n = counts[j]
|
||||||
|
if nbuf + n > chunk_tokens and buf:
|
||||||
|
rows.append({"prompt": "\n".join(buf), "cls": cls})
|
||||||
|
got += nbuf
|
||||||
|
buf, nbuf = [], 0
|
||||||
|
if got >= want:
|
||||||
|
break
|
||||||
|
buf.append(frag)
|
||||||
|
nbuf += n
|
||||||
|
if i > len(pool) * POOL_CYCLES:
|
||||||
|
break # pool exhausted with repeats; stop
|
||||||
|
if buf and got < want:
|
||||||
|
rows.append({"prompt": "\n".join(buf), "cls": cls})
|
||||||
|
got += nbuf
|
||||||
|
print(f"calib {cls}: {got} tokens, {i} fragments")
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def emit_calib(a: argparse.Namespace, frags, tok,
|
||||||
|
rng: random.Random) -> tuple[int, list[dict]]:
|
||||||
|
"""calib.jsonl rows: the class mix, shuffled, prompts only."""
|
||||||
|
rows: list[dict] = []
|
||||||
|
for cls, frac in CLASS_MIX.items():
|
||||||
|
rows.extend(pack_class(cls, frags[cls]["cal"], int(a.calib_tokens * frac),
|
||||||
|
tok, a.chunk_tokens))
|
||||||
|
rng.shuffle(rows)
|
||||||
|
return len(rows), [{"prompt": r["prompt"]} for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def emit_kld(a: argparse.Namespace, frags, tok, rng: random.Random,
|
||||||
|
env: Environment, tname: str) -> tuple[int, list[dict], int]:
|
||||||
|
"""kld.jsonl rows: multi-turn conversations, each with a scored tail."""
|
||||||
|
template = env.get_template(tname)
|
||||||
|
rows: list[dict] = []
|
||||||
|
total = scored_total = 0
|
||||||
|
classes = ["code", "prose", "tools"]
|
||||||
|
cursor = {c: 0 for c in classes}
|
||||||
|
guard = 0
|
||||||
|
while total < a.kld_tokens and guard < KLD_GUARD:
|
||||||
|
guard += 1
|
||||||
|
msgs, srcs = [], []
|
||||||
|
for t in range(rng.choice([2, 3, 4])):
|
||||||
|
cls = classes[(len(rows) + t) % 3]
|
||||||
|
pool = frags[cls]["kld"]
|
||||||
if not pool:
|
if not pool:
|
||||||
continue
|
continue
|
||||||
got, i, buf, nbuf = 0, 0, [], 0
|
frag = pool[cursor[cls] % len(pool)]
|
||||||
while got < want:
|
cursor[cls] += 1
|
||||||
frag = pool[i % len(pool)]
|
msgs.append({"role": "user",
|
||||||
i += 1
|
"content": "Summarize and explain this "
|
||||||
n = toks(frag)
|
f"engineering material:\n{frag}"})
|
||||||
if nbuf + n > a.chunk_tokens and buf:
|
msgs.append({"role": "assistant",
|
||||||
rows.append({"prompt": "\n".join(buf), "cls": cls})
|
"content": frag[:4000]})
|
||||||
got += nbuf
|
srcs.append(cls)
|
||||||
buf, nbuf = [], 0
|
rendered = template.render(messages=msgs, add_generation_prompt=False)
|
||||||
if got >= want:
|
n = qc.n_tokens(tok, rendered)
|
||||||
break
|
if n < a.kld_min_prompt:
|
||||||
buf.append(frag)
|
continue
|
||||||
nbuf += n
|
rows.append({"prompt": rendered,
|
||||||
if i > len(pool) * 40:
|
"score_from": n - a.score_tail,
|
||||||
break # pool exhausted with repeats; stop
|
"source": "+".join(sorted(set(srcs)))})
|
||||||
if buf and got < want:
|
total += n
|
||||||
rows.append({"prompt": "\n".join(buf), "cls": cls})
|
scored_total += a.score_tail
|
||||||
got += nbuf
|
return len(rows), rows, scored_total
|
||||||
print(f"calib {cls}: {got} tokens, {i} fragments")
|
|
||||||
rng.shuffle(rows)
|
|
||||||
return len(rows), [{k: v for k, v in r.items() if k == "prompt"}
|
|
||||||
for r in rows]
|
|
||||||
|
|
||||||
def emit_kld() -> tuple[int, list[dict]]:
|
|
||||||
rows, total = [], 0
|
|
||||||
classes = ["code", "prose", "tools"]
|
|
||||||
i = {c: 0 for c in classes}
|
|
||||||
guard = 0
|
|
||||||
while total < a.kld_tokens and guard < 5000:
|
|
||||||
guard += 1
|
|
||||||
msgs, srcs = [], []
|
|
||||||
for t in range(rng.choice([2, 3, 4])):
|
|
||||||
cls = classes[(len(rows) + t) % 3]
|
|
||||||
pool = frags[cls]["kld"]
|
|
||||||
if not pool:
|
|
||||||
continue
|
|
||||||
frag = pool[i[cls] % len(pool)]
|
|
||||||
i[cls] += 2
|
|
||||||
msgs.append({"role": "user",
|
|
||||||
"content": "Summarize and explain this "
|
|
||||||
f"engineering material:\n{frag}"})
|
|
||||||
msgs.append({"role": "assistant",
|
|
||||||
"content": frag[:4000]})
|
|
||||||
srcs.append(cls)
|
|
||||||
rendered = env.get_template(tname).render(
|
|
||||||
messages=msgs, add_generation_prompt=False)
|
|
||||||
n = toks(rendered)
|
|
||||||
if n < a.kld_min_prompt:
|
|
||||||
continue
|
|
||||||
rows.append({"prompt": rendered,
|
|
||||||
"score_from": n - a.score_tail,
|
|
||||||
"source": "+".join(sorted(set(srcs)))})
|
|
||||||
total += n
|
|
||||||
return len(rows), rows
|
|
||||||
|
|
||||||
os.makedirs(a.out_dir, exist_ok=True)
|
def manifest(a: argparse.Namespace, tname: str, nc: int, nk: int,
|
||||||
nc, rows = emit_calib()
|
scored: int) -> dict:
|
||||||
with open(os.path.join(a.out_dir, "calib.jsonl"), "w") as f:
|
return {
|
||||||
for r in rows:
|
|
||||||
f.write(json.dumps(r) + "\n")
|
|
||||||
nk, krows = emit_kld()
|
|
||||||
with open(os.path.join(a.out_dir, "kld.jsonl"), "w") as f:
|
|
||||||
for r in krows:
|
|
||||||
f.write(json.dumps(r) + "\n")
|
|
||||||
scored = sum(len(tokenizer.encode(r["prompt"], add_special_tokens=False).ids)
|
|
||||||
- r["score_from"] for r in krows)
|
|
||||||
manifest = {
|
|
||||||
"seed": a.seed, "template": tname,
|
"seed": a.seed, "template": tname,
|
||||||
"tokenizer": os.path.basename(a.tokenizer_json),
|
"tokenizer": os.path.basename(a.tokenizer_json),
|
||||||
"calib_prompts": nc, "kld_prompts": nk,
|
"calib_prompts": nc, "kld_prompts": nk,
|
||||||
"kld_scored_positions": scored,
|
"kld_scored_positions": scored,
|
||||||
"kld_score_tail": a.score_tail,
|
"kld_score_tail": a.score_tail,
|
||||||
"disjoint_file_pools": "local KLD material only; HF code feeds calib",
|
"disjoint_file_pools": "local KLD material only; HF code feeds calib",
|
||||||
"calib_mix": {"code": .45, "prose": .30, "tools": .25},
|
"calib_mix": dict(CLASS_MIX),
|
||||||
"hf_code": a.hf_code_parquet, "hf_prose": a.hf_prose_parquet,
|
"hf_code": a.hf_code_parquet, "hf_prose": a.hf_prose_parquet,
|
||||||
}
|
}
|
||||||
with open(os.path.join(a.out_dir, "manifest.json"), "w") as f:
|
|
||||||
json.dump(manifest, f, indent=1)
|
|
||||||
print(json.dumps(manifest, indent=1))
|
def main() -> None:
|
||||||
|
a = parse_args()
|
||||||
|
rng = random.Random(a.seed)
|
||||||
|
tok = qc.load_tokenizer(a.tokenizer_json)
|
||||||
|
env, tname = load_template(a.template)
|
||||||
|
|
||||||
|
frags = build_frags(a, tok, rng)
|
||||||
|
os.makedirs(a.out_dir, exist_ok=True)
|
||||||
|
nc, calib_rows = emit_calib(a, frags, tok, rng)
|
||||||
|
qc.write_jsonl(os.path.join(a.out_dir, "calib.jsonl"), calib_rows)
|
||||||
|
nk, kld_rows, scored = emit_kld(a, frags, tok, rng, env, tname)
|
||||||
|
qc.write_jsonl(os.path.join(a.out_dir, "kld.jsonl"), kld_rows)
|
||||||
|
man = manifest(a, tname, nc, nk, scored)
|
||||||
|
qc.write_json(os.path.join(a.out_dir, "manifest.json"), man)
|
||||||
|
print(json.dumps(man, indent=1))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Executable
+94
@@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Shared plumbing for the qwen38 campaign corpus tools.
|
||||||
|
|
||||||
|
The three tools move the same two shapes — a JSONL of prompt records and a
|
||||||
|
one-artifact JSON manifest — and they all tokenize with the SAME tokenizer. That
|
||||||
|
last part is not a detail: a KLD reference is only meaningful against the exact
|
||||||
|
token sequence the corpus recorded, so `n_tokens` exists once, here, and every
|
||||||
|
tool counts the same way. A window that decode/encode does not round-trip is a
|
||||||
|
refusal, never a warning.
|
||||||
|
|
||||||
|
Loaded as a sibling module. Each tool puts its own directory on `sys.path`
|
||||||
|
first, so `python3 scripts/<tool>.py` and the driver's container mount
|
||||||
|
(`runpy.run_path('/work/<tool>.py')`, `-v scripts:/work:ro`) both resolve it —
|
||||||
|
run the tools by path, not piped through stdin.
|
||||||
|
"""
|
||||||
|
import contextlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def refuse(msg: str) -> None:
|
||||||
|
"""Exit 2 with a REFUSED line — the code argparse uses for unusable input."""
|
||||||
|
print(f"REFUSED: {msg}", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
|
||||||
|
def load_tokenizer(path: str):
|
||||||
|
try:
|
||||||
|
from tokenizers import Tokenizer
|
||||||
|
except ImportError as exc: # the corpus tools carry their own venv
|
||||||
|
refuse(f"tokenizers is not importable by {sys.executable} — run the "
|
||||||
|
f"corpus venv's python (on dankserver: "
|
||||||
|
f"/home/dank/venvs/radcorpus/bin/python) or set "
|
||||||
|
f"FRANK_QWEN38_TOKENIZER: {exc}")
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
refuse(f"no tokenizer at {path}")
|
||||||
|
return Tokenizer.from_file(path)
|
||||||
|
|
||||||
|
|
||||||
|
def n_tokens(tok, text: str) -> int:
|
||||||
|
"""The corpus's own count: no special tokens added, prompt side only."""
|
||||||
|
return len(tok.encode(text, add_special_tokens=False).ids)
|
||||||
|
|
||||||
|
|
||||||
|
def iter_jsonl(path: str):
|
||||||
|
"""Yield `(lineno, record)` for every non-blank line; refuse on a bad line.
|
||||||
|
|
||||||
|
Field validation stays with the caller: the three corpora carry different
|
||||||
|
fields (`prompt` alone, `prompt`+`cls`, `prompt`+`score_from`+`source`), and
|
||||||
|
a record missing the one the tool needs is that tool's refusal to make.
|
||||||
|
"""
|
||||||
|
with open(path, encoding="utf-8", errors="replace") as fh:
|
||||||
|
for lineno, line in enumerate(fh, 1):
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
rec = json.loads(line)
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
refuse(f"{path}:{lineno} is not JSON: {exc}")
|
||||||
|
yield lineno, rec
|
||||||
|
|
||||||
|
|
||||||
|
def write_jsonl(path: str, rows) -> None:
|
||||||
|
with open(path, "w", encoding="utf-8") as fh:
|
||||||
|
for row in rows:
|
||||||
|
fh.write(json.dumps(row, ensure_ascii=False) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def atomic_writer(path: str):
|
||||||
|
"""Write `path` through a temporary sibling and rename on success.
|
||||||
|
|
||||||
|
An interrupted run then leaves no half corpus behind, and the temporary name
|
||||||
|
keeps the one-artifact-one-name law the driver applies to a score report.
|
||||||
|
"""
|
||||||
|
d = os.path.dirname(os.path.abspath(path)) or "."
|
||||||
|
fd, tmp = tempfile.mkstemp(dir=d, prefix="." + os.path.basename(path) + "-")
|
||||||
|
try:
|
||||||
|
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
||||||
|
yield fh
|
||||||
|
os.replace(tmp, path)
|
||||||
|
except BaseException:
|
||||||
|
if os.path.exists(tmp):
|
||||||
|
os.unlink(tmp)
|
||||||
|
raise
|
||||||
|
os.chmod(path, 0o644)
|
||||||
|
|
||||||
|
|
||||||
|
def write_json(path: str, obj) -> None:
|
||||||
|
with atomic_writer(path) as fh:
|
||||||
|
json.dump(obj, fh, indent=1)
|
||||||
|
fh.write("\n")
|
||||||
@@ -38,12 +38,19 @@ one-artifact-one-name law run-qwen38-bf16.sh applies to a score report. The file
|
|||||||
is written through a temporary name and renamed, so an interrupted run leaves no
|
is written through a temporary name and renamed, so an interrupted run leaves no
|
||||||
half corpus behind. A JSON summary of what was written goes to stdout.
|
half corpus behind. A JSON summary of what was written goes to stdout.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import dataclasses
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
|
_HERE = os.path.dirname(os.path.abspath(__file__)) if "__file__" in globals() else os.getcwd()
|
||||||
|
if _HERE not in sys.path:
|
||||||
|
sys.path.insert(0, _HERE)
|
||||||
|
|
||||||
|
import qwen38_common as qc
|
||||||
|
|
||||||
|
MISSING = object() # "the parent record carried no source field"
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
def parse_args() -> argparse.Namespace:
|
||||||
@@ -69,95 +76,96 @@ def parse_args() -> argparse.Namespace:
|
|||||||
return ap.parse_args()
|
return ap.parse_args()
|
||||||
|
|
||||||
|
|
||||||
def load_tokenizer(path: str):
|
@dataclasses.dataclass(frozen=True)
|
||||||
try:
|
class Window:
|
||||||
from tokenizers import Tokenizer
|
"""One scoring document cut out of a parent record."""
|
||||||
except ImportError as exc: # the corpus scripts carry their own venv
|
prompt: str
|
||||||
raise SystemExit(
|
score_from: int
|
||||||
f"REFUSED: tokenizers is not importable by {sys.executable} — run the "
|
n_tokens: int
|
||||||
f"corpus venv's python (on dankserver: /home/dank/venvs/radcorpus/bin/python) "
|
clamped: bool
|
||||||
f"or set FRANK_QWEN38_TOKENIZER: {exc}") from None
|
source: object = MISSING
|
||||||
if not os.path.isfile(path):
|
|
||||||
raise SystemExit(f"REFUSED: no tokenizer at {path}")
|
def record(self) -> dict:
|
||||||
return Tokenizer.from_file(path)
|
rec = {"prompt": self.prompt, "score_from": self.score_from}
|
||||||
|
if self.source is not MISSING:
|
||||||
|
rec["source"] = self.source
|
||||||
|
return rec
|
||||||
|
|
||||||
|
|
||||||
def refuse(msg: str) -> None:
|
def validate(a: argparse.Namespace) -> None:
|
||||||
print(f"REFUSED: {msg}", file=sys.stderr)
|
"""Everything that makes this run unusable, named before a token is cut."""
|
||||||
sys.exit(2) # 2 is the code argparse uses for unusable arguments
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
a = parse_args()
|
|
||||||
if a.max_doc_tokens <= 0:
|
if a.max_doc_tokens <= 0:
|
||||||
refuse(f"--max-doc-tokens must be positive (got {a.max_doc_tokens})")
|
qc.refuse(f"--max-doc-tokens must be positive (got {a.max_doc_tokens})")
|
||||||
if a.score_tail <= 0:
|
if a.score_tail <= 0:
|
||||||
refuse(f"--score-tail must be positive (got {a.score_tail})")
|
qc.refuse(f"--score-tail must be positive (got {a.score_tail})")
|
||||||
if a.max_doc_tokens <= a.score_tail:
|
if a.max_doc_tokens <= a.score_tail:
|
||||||
refuse(f"--max-doc-tokens {a.max_doc_tokens} cannot hold a {a.score_tail}-position "
|
qc.refuse(f"--max-doc-tokens {a.max_doc_tokens} cannot hold a "
|
||||||
"score tail: every window would be all tail and no context")
|
f"{a.score_tail}-position score tail: every window would be all "
|
||||||
|
"tail and no context")
|
||||||
if not os.path.isfile(a.corpus):
|
if not os.path.isfile(a.corpus):
|
||||||
refuse(f"no corpus at {a.corpus}")
|
qc.refuse(f"no corpus at {a.corpus}")
|
||||||
if os.path.realpath(a.out) == os.path.realpath(a.corpus):
|
if os.path.realpath(a.out) == os.path.realpath(a.corpus):
|
||||||
refuse(f"--out {a.out} is the corpus itself; move it to trash first — the trash law "
|
qc.refuse(f"--out {a.out} is the corpus itself; move it to trash first — the "
|
||||||
"means the old segmentation stays recoverable")
|
"trash law means the old segmentation stays recoverable")
|
||||||
if os.path.exists(a.out) and os.path.getsize(a.out) > 0 and not a.force:
|
if os.path.exists(a.out) and os.path.getsize(a.out) > 0 and not a.force:
|
||||||
refuse(f"{a.out} exists and is not empty; pass --force after moving it to trash")
|
qc.refuse(f"{a.out} exists and is not empty; pass --force after moving it to trash")
|
||||||
|
|
||||||
tok = load_tokenizer(a.tokenizer_json)
|
|
||||||
|
|
||||||
|
def cut(rec: dict, tok, a: argparse.Namespace,
|
||||||
|
where: str) -> tuple[list[Window], int]:
|
||||||
|
"""Cut one record's token ids into windows, proving each one round-trips.
|
||||||
|
|
||||||
|
The round-trip encode is the whole reason this tool may claim the reference
|
||||||
|
scores the corpus's own token sequence: decoding a window to text and
|
||||||
|
encoding it back must return the identical ids, or the score is of a
|
||||||
|
different document than the one on disk.
|
||||||
|
"""
|
||||||
|
ids = tok.encode(rec["prompt"], add_special_tokens=False).ids
|
||||||
|
windows: list[Window] = []
|
||||||
|
for start in range(0, max(len(ids), 1), a.max_doc_tokens):
|
||||||
|
window = ids[start:start + a.max_doc_tokens]
|
||||||
|
if not window:
|
||||||
|
continue
|
||||||
|
text = tok.decode(window)
|
||||||
|
if tok.encode(text, add_special_tokens=False).ids != window:
|
||||||
|
qc.refuse(f"{where} window at token {start} does not round-trip through "
|
||||||
|
"text — the reference would score a different token sequence "
|
||||||
|
"than the corpus holds")
|
||||||
|
windows.append(Window(prompt=text,
|
||||||
|
score_from=max(len(window) - a.score_tail, 0),
|
||||||
|
n_tokens=len(window),
|
||||||
|
clamped=len(window) < a.score_tail,
|
||||||
|
source=rec["source"] if "source" in rec else MISSING))
|
||||||
|
return windows, len(ids)
|
||||||
|
|
||||||
|
|
||||||
|
def rechunk(a: argparse.Namespace, tok) -> dict:
|
||||||
|
"""Write the re-chunked corpus and return the summary it printed."""
|
||||||
docs_in = docs_out = tokens_in = tokens_out = 0
|
docs_in = docs_out = tokens_in = tokens_out = 0
|
||||||
longest_child = 0
|
longest_child = clamped = scored = 0
|
||||||
clamped = scored = 0
|
|
||||||
sources: dict[str, int] = {}
|
sources: dict[str, int] = {}
|
||||||
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(os.path.abspath(a.out)) or ".",
|
with qc.atomic_writer(a.out) as out:
|
||||||
prefix=".rechunk-")
|
for lineno, rec in qc.iter_jsonl(a.corpus):
|
||||||
try:
|
where = f"{a.corpus}:{lineno}"
|
||||||
with os.fdopen(fd, "w", encoding="utf-8") as out:
|
if not isinstance(rec.get("prompt"), str):
|
||||||
with open(a.corpus, encoding="utf-8") as src:
|
qc.refuse(f"{where} has no string 'prompt' field")
|
||||||
for lineno, line in enumerate(src, 1):
|
windows, n_parent = cut(rec, tok, a, where)
|
||||||
if not line.strip():
|
docs_in += 1
|
||||||
continue
|
tokens_in += n_parent
|
||||||
try:
|
for w in windows:
|
||||||
rec = json.loads(line)
|
out.write(json.dumps(w.record(), ensure_ascii=False) + "\n")
|
||||||
except json.JSONDecodeError as exc:
|
docs_out += 1
|
||||||
refuse(f"{a.corpus}:{lineno} is not JSON: {exc}")
|
tokens_out += w.n_tokens
|
||||||
prompt = rec.get("prompt")
|
longest_child = max(longest_child, w.n_tokens)
|
||||||
if not isinstance(prompt, str):
|
clamped += 1 if w.clamped else 0
|
||||||
refuse(f"{a.corpus}:{lineno} has no string 'prompt' field")
|
scored += w.n_tokens - w.score_from
|
||||||
ids = tok.encode(prompt, add_special_tokens=False).ids
|
if "source" in rec:
|
||||||
docs_in += 1
|
for cls in str(rec["source"]).split("+"):
|
||||||
tokens_in += len(ids)
|
sources[cls] = sources.get(cls, 0) + 1
|
||||||
for start in range(0, max(len(ids), 1), a.max_doc_tokens):
|
if tokens_out != tokens_in:
|
||||||
window = ids[start:start + a.max_doc_tokens]
|
raise SystemExit(f"REFUSED: the windows hold {tokens_out} tokens and the corpus "
|
||||||
if not window:
|
f"has {tokens_in} — re-chunking lost or invented tokens")
|
||||||
continue
|
return {
|
||||||
score_from = max(len(window) - a.score_tail, 0)
|
|
||||||
if len(window) < a.score_tail:
|
|
||||||
clamped += 1
|
|
||||||
scored += len(window) - score_from
|
|
||||||
child = {"prompt": tok.decode(window),
|
|
||||||
"score_from": score_from}
|
|
||||||
if "source" in rec:
|
|
||||||
child["source"] = rec["source"]
|
|
||||||
out.write(json.dumps(child, ensure_ascii=False) + "\n")
|
|
||||||
docs_out += 1
|
|
||||||
tokens_out += len(window)
|
|
||||||
longest_child = max(longest_child, len(window))
|
|
||||||
if "source" in rec:
|
|
||||||
for cls in str(rec["source"]).split("+"):
|
|
||||||
sources[cls] = sources.get(cls, 0) + 1
|
|
||||||
if tokens_out != tokens_in:
|
|
||||||
raise SystemExit(f"REFUSED: the windows hold {tokens_out} tokens and the corpus "
|
|
||||||
f"has {tokens_in} — re-chunking lost or invented tokens")
|
|
||||||
os.replace(tmp, a.out)
|
|
||||||
except BaseException:
|
|
||||||
if os.path.exists(tmp):
|
|
||||||
os.unlink(tmp)
|
|
||||||
raise
|
|
||||||
os.chmod(a.out, 0o644)
|
|
||||||
|
|
||||||
print(json.dumps({
|
|
||||||
"corpus": a.corpus,
|
"corpus": a.corpus,
|
||||||
"out": a.out,
|
"out": a.out,
|
||||||
"tokenizer": os.path.basename(a.tokenizer_json),
|
"tokenizer": os.path.basename(a.tokenizer_json),
|
||||||
@@ -171,7 +179,13 @@ def main() -> None:
|
|||||||
"scored_positions": scored,
|
"scored_positions": scored,
|
||||||
"clamped_score_from": clamped,
|
"clamped_score_from": clamped,
|
||||||
"source_documents": sources,
|
"source_documents": sources,
|
||||||
}, indent=1))
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
a = parse_args()
|
||||||
|
validate(a)
|
||||||
|
print(json.dumps(rechunk(a, qc.load_tokenizer(a.tokenizer_json)), indent=1))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
+29
-11
@@ -120,7 +120,8 @@ while [ $# -gt 0 ]; do
|
|||||||
case "$1" in
|
case "$1" in
|
||||||
kldref|calib|score) MODE="$1" ;;
|
kldref|calib|score) MODE="$1" ;;
|
||||||
--dry-run) DRY=1 ;;
|
--dry-run) DRY=1 ;;
|
||||||
--image) IMAGE="$2"; shift ;;
|
--image) [ "$#" -ge 2 ] || { echo "--image needs a reference" >&2; exit 2; }
|
||||||
|
IMAGE="$2"; shift ;;
|
||||||
*)
|
*)
|
||||||
# score's two positional arguments, and nothing else may be bare.
|
# score's two positional arguments, and nothing else may be bare.
|
||||||
if [ "$MODE" = score ] && [ "${#POS[@]}" -lt 2 ]; then
|
if [ "$MODE" = score ] && [ "${#POS[@]}" -lt 2 ]; then
|
||||||
@@ -170,13 +171,14 @@ fi
|
|||||||
|
|
||||||
# ONE step size for every mode, quoted from the file that owns it — the audit
|
# ONE step size for every mode, quoted from the file that owns it — the audit
|
||||||
# law for a radiance rig is to inherit the proven number, not to carry a
|
# law for a radiance rig is to inherit the proven number, not to carry a
|
||||||
# campaign-private value: production's FRANK_ARCHON_MAX_BATCHED_TOKENS is 2,048,
|
# campaign-private value: production's FRANK_ARCHON_MAX_BATCHED_TOKENS. The
|
||||||
# the widest step the quantised expert FILE tiers can stream (above it the
|
# 1,024 default this script carried was PR #80's answer to a starved cache;
|
||||||
# engine charges a 10.65 GiB arena to the card and the load refuses), and the
|
# production moved to its widest legal step (2,048) on 2026-10-29 and the rig
|
||||||
# arena it charges comes out of the cache — so the corpora are cut to the window
|
# follows. The pool raise that shipped with it (66,560 a rank) bought this
|
||||||
# the boot ledger reports, never the other way round. The 1,024 default this
|
# container 1.26 MiB of cache (`vram_kv 269.99 -> 271.25 MiB`, window still
|
||||||
# script carried was PR #80's answer to a starved cache; the pool raise
|
# 7,436 tokens — the 95.37 GiB n-gram table's claim decides the rig's cache,
|
||||||
# (frank-archon.env: 66,560 MiB a rank) is the fix, not a narrower step.
|
# not the pool) and was folded back the same day. The corpora stay cut to the
|
||||||
|
# window the boot ledger reports, never the other way round.
|
||||||
MNBT="${FRANK_ARCHON_MAX_BATCHED_TOKENS:?FRANK_ARCHON_MAX_BATCHED_TOKENS must be set}"
|
MNBT="${FRANK_ARCHON_MAX_BATCHED_TOKENS:?FRANK_ARCHON_MAX_BATCHED_TOKENS must be set}"
|
||||||
if ! [[ "$MNBT" =~ ^[0-9]+$ ]]; then
|
if ! [[ "$MNBT" =~ ^[0-9]+$ ]]; then
|
||||||
echo "REFUSED: FRANK_ARCHON_MAX_BATCHED_TOKENS must be a token count (got: $MNBT)" >&2
|
echo "REFUSED: FRANK_ARCHON_MAX_BATCHED_TOKENS must be a token count (got: $MNBT)" >&2
|
||||||
@@ -249,7 +251,8 @@ case "$MODE" in
|
|||||||
# time the engine reads a recording and writes a report instead of the
|
# time the engine reads a recording and writes a report instead of the
|
||||||
# reverse. The reference dir mounts READ-ONLY — a scoring pass must not be
|
# reverse. The reference dir mounts READ-ONLY — a scoring pass must not be
|
||||||
# able to touch the zero point — and the report gets its own writable bind.
|
# able to touch the zero point — and the report gets its own writable bind.
|
||||||
if [ "$OUT_DIR" = "$KLD_REF_DIR" ] || same_dir "$OUT_DIR" "$KLD_REF_DIR"; then
|
if [ "$OUT_DIR" = "$KLD_REF_DIR" ] || same_dir "$OUT_DIR" "$KLD_REF_DIR" \
|
||||||
|
|| [ "$(realpath -m "$OUT_DIR")" = "$(realpath -m "$KLD_REF_DIR")" ]; then
|
||||||
echo "REFUSED: score output $SCORE_OUT is inside the reference dir $KLD_REF_DIR, which this mode mounts read-only" >&2
|
echo "REFUSED: score output $SCORE_OUT is inside the reference dir $KLD_REF_DIR, which this mode mounts read-only" >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
@@ -275,9 +278,17 @@ case "$MODE" in
|
|||||||
# through MAX_SEQS, exactly as kldref and score do.
|
# through MAX_SEQS, exactly as kldref and score do.
|
||||||
ARGS+=(--model "$MODEL_IN" --port 7770
|
ARGS+=(--model "$MODEL_IN" --port 7770
|
||||||
--max-num-seqs "$MAX_SEQS" --max-num-batched-tokens "$MNBT")
|
--max-num-seqs "$MAX_SEQS" --max-num-batched-tokens "$MNBT")
|
||||||
|
# The tap is only real if this directory exists and the engine can write it:
|
||||||
|
# without the check a typo'd CALIB_DIR auto-creates a root-owned empty dir in
|
||||||
|
# whatever path was given, and the run burns hours serving prompts into a tap
|
||||||
|
# nobody reads. RADIANCE_CALIB_TOKENS is the whole corpus' floor, not a round's.
|
||||||
|
[ "$DRY" = 1 ] || [ -w "$CALIB_DIR" ] || {
|
||||||
|
echo "REFUSED: calib tap dir $CALIB_DIR is not writable (mkdir it, chown it, then re-run)" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
MOUNTS+=(-v "$CALIB_DIR:/data/calib")
|
MOUNTS+=(-v "$CALIB_DIR:/data/calib")
|
||||||
TAPENV=(-e RADIANCE_CALIB_DIR=/data/calib
|
TAPENV=(-e RADIANCE_CALIB_DIR=/data/calib
|
||||||
-e RADIANCE_CALIB_TOKENS=12000000)
|
-e "RADIANCE_CALIB_TOKENS=${FRANK_QWEN38_CALIB_TOKENS:-12000000}")
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@@ -351,7 +362,14 @@ cp -f "$TEMPLATE" "$TPL" || {
|
|||||||
[ "$MODE" = kldref ] && mkdir -p "$KLD_REF_DIR"
|
[ "$MODE" = kldref ] && mkdir -p "$KLD_REF_DIR"
|
||||||
[ "$MODE" = calib ] && mkdir -p "$CALIB_DIR"
|
[ "$MODE" = calib ] && mkdir -p "$CALIB_DIR"
|
||||||
|
|
||||||
restore() { sudo systemctl start "$SERVICE" >/dev/null 2>&1 || true; }
|
# Stop the campaign container BEFORE handing the cards back: a killed run leaves
|
||||||
|
# its conmon alive (--rm only fires when podman itself exits), and production
|
||||||
|
# then boots beside a container still holding the GPUs — measured 2026-10-29,
|
||||||
|
# where the service reported `active` while its engine starved on the rig's VRAM.
|
||||||
|
restore() {
|
||||||
|
podman rm -f "$C" >/dev/null 2>&1 || true
|
||||||
|
sudo systemctl start "$SERVICE" >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
trap restore EXIT
|
trap restore EXIT
|
||||||
echo "run-qwen38-bf16 $MODE: stopping $SERVICE ($(date -Is)); log: $LOG"
|
echo "run-qwen38-bf16 $MODE: stopping $SERVICE ($(date -Is)); log: $LOG"
|
||||||
sudo systemctl stop "$SERVICE" >/dev/null 2>&1 || true
|
sudo systemctl stop "$SERVICE" >/dev/null 2>&1 || true
|
||||||
|
|||||||
@@ -337,8 +337,12 @@ def main():
|
|||||||
if any(meta[shard_ids[i]][4][1] != hd for i in range(n_shards)):
|
if any(meta[shard_ids[i]][4][1] != hd for i in range(n_shards)):
|
||||||
log("ERROR: PLE shards disagree on HD")
|
log("ERROR: PLE shards disagree on HD")
|
||||||
return 2
|
return 2
|
||||||
if hd % 128 != 0 and hd != 160:
|
if hd != 160 and hd % 128 != 0:
|
||||||
log(f"WARNING: PLE HD={hd} is neither 160 nor a multiple of 128")
|
# The pack below writes lanes of 128 codes; any other HD would produce a
|
||||||
|
# table that decodes to the wrong rows. The old warning let it continue.
|
||||||
|
log(f"ERROR: PLE HD={hd} is neither 160 nor a multiple of 128, and the "
|
||||||
|
"128-lane pack cannot express it")
|
||||||
|
return 2
|
||||||
|
|
||||||
# ---- idempotence gate -------------------------------------------------
|
# ---- idempotence gate -------------------------------------------------
|
||||||
stacked_names = [f"{p}.mlp.experts.gate_up_proj" for p in prefixes]
|
stacked_names = [f"{p}.mlp.experts.gate_up_proj" for p in prefixes]
|
||||||
@@ -521,12 +525,16 @@ def verify(stage, prefixes, n_e, shapes, meta, shard_ids, rows, hd,
|
|||||||
}
|
}
|
||||||
for name, (dt, shp) in want.items():
|
for name, (dt, shp) in want.items():
|
||||||
e = hdr[name]
|
e = hdr[name]
|
||||||
assert e["dtype"] == dt and list(e["shape"]) == shp, name
|
if e["dtype"] != dt or list(e["shape"]) != shp:
|
||||||
assert e["data_offsets"][1] - e["data_offsets"][0] == \
|
raise RuntimeError(f"{name}: header is {e['dtype']}{e['shape']}, expected {dt}{shp}")
|
||||||
int(np.prod(shp)) * (1 if dt == F8 else 2), name
|
if e["data_offsets"][1] - e["data_offsets"][0] != int(np.prod(shp)) * (1 if dt == F8 else 2):
|
||||||
|
raise RuntimeError(f"{name}: data span is {e['data_offsets']}, shape {shp} needs "
|
||||||
|
f"{int(np.prod(shp)) * (1 if dt == F8 else 2)} B")
|
||||||
_, phdr = parse_header(out_path)
|
_, phdr = parse_header(out_path)
|
||||||
e = phdr[next(iter(phdr))]
|
e = phdr[next(iter(phdr))]
|
||||||
assert e["dtype"] == BF16 and list(e["shape"]) == [sum(rows), hd]
|
if e["dtype"] != BF16 or list(e["shape"]) != [sum(rows), hd]:
|
||||||
|
raise RuntimeError(f"PLE table header is {e['dtype']}{e['shape']}, "
|
||||||
|
f"expected {BF16}[{sum(rows)}, {hd}]")
|
||||||
|
|
||||||
# sampled PLE rows: recompute and compare bytes exactly
|
# sampled PLE rows: recompute and compare bytes exactly
|
||||||
lut = e4m3_lut()
|
lut = e4m3_lut()
|
||||||
@@ -546,7 +554,8 @@ def verify(stage, prefixes, n_e, shapes, meta, shard_ids, rows, hd,
|
|||||||
with open(out_path, "rb") as ofh:
|
with open(out_path, "rb") as ofh:
|
||||||
ofh.seek(out_off + got_row * hd * 2)
|
ofh.seek(out_off + got_row * hd * 2)
|
||||||
got = ofh.read(hd * 2)
|
got = ofh.read(hd * 2)
|
||||||
assert got == want, f"PLE row {got_row} mismatch"
|
if got != want:
|
||||||
|
raise RuntimeError(f"PLE row {got_row} does not match the decoded source")
|
||||||
mm.close()
|
mm.close()
|
||||||
log("verify: new-file headers, sizes and sampled PLE rows all exact; "
|
log("verify: new-file headers, sizes and sampled PLE rows all exact; "
|
||||||
"expert slabs were byte-checked in-stack")
|
"expert slabs were byte-checked in-stack")
|
||||||
|
|||||||
@@ -91,27 +91,36 @@ on stop. Boot budget 1800 s (the publisher's compose allows a 15-minute `start_p
|
|||||||
| Slots / window | `MAX_SEQS=4`, `MAX_MODEL_LEN=200000` (native 262,144; longer is refused, never truncated) — this pair is the **cache bound**: the resolver sizes the KV carve against `slots × window`, and no slot count funds itself (Laws) |
|
| Slots / window | `MAX_SEQS=4`, `MAX_MODEL_LEN=200000` (native 262,144; longer is refused, never truncated) — this pair is the **cache bound**: the resolver sizes the KV carve against `slots × window`, and no slot count funds itself (Laws) |
|
||||||
| KV dtype | `KV_DTYPE=fp8` — the only dtype that funds the slot count (Laws) |
|
| KV dtype | `KV_DTYPE=fp8` — the only dtype that funds the slot count (Laws) |
|
||||||
| VRAM split | **derived at boot** from `GPU_HEADROOM_MIB=96` + the cache bound + `HOST_POOL_MIB`; naming `VRAM_WEIGHTS_MIB`/`VRAM_KV_MIB` is a boot refusal (removed at 1.3.0) |
|
| VRAM split | **derived at boot** from `GPU_HEADROOM_MIB=96` + the cache bound + `HOST_POOL_MIB`; naming `VRAM_WEIGHTS_MIB`/`VRAM_KV_MIB` is a boot refusal (removed at 1.3.0) |
|
||||||
| Expert tier | `PLACEMENT=expert_tiered`, `HOST_POOL_MIB=66560` **per rank** (raised 2026-10-29 from 49152 — see *Eras*) — the pinned host pool is also the **KV lever**: the expert floor is `plane + 2 units − host room`, so every MiB pinned there lowers what the experts claim on the card one-for-one and the cache takes it (Laws) |
|
| Expert tier | `PLACEMENT=expert_tiered`, `HOST_POOL_MIB=49152` **per rank** (raised to 66560 on 2026-10-29, folded back the same day — the A/B under *Laws* shows the raise bought no cache) — the pinned host pool is also the **KV lever**: the expert floor is `plane + 2 units − host room`, so every MiB pinned there lowers what the experts claim on the card one-for-one and the cache takes it (Laws) |
|
||||||
| PLE | `NGRAM_PLACEMENT=ram` (47.7 GiB pinned; refuses boot on a short memlock) |
|
| PLE | `NGRAM_PLACEMENT=ram` (47.7 GiB pinned; refuses boot on a short memlock) |
|
||||||
| Spec / batch | `SPEC_TOKENS=2` — MTP depth 2, the `.rad` carries the model's own `mtp.*` head (`mtp.fc_hidden`/`fc_embedding`/`draft_head`); `MAX_BATCHED_TOKENS=2048` (MNBT — 2560 collapses decode, Laws) |
|
| Spec / batch | `SPEC_TOKENS=2` — MTP depth 2, the `.rad` carries the model's own `mtp.*` head (`mtp.fc_hidden`/`fc_embedding`/`draft_head`); `MAX_BATCHED_TOKENS=2048` (MNBT — 2560 collapses decode, Laws) |
|
||||||
| Prefix cache | `PREFIX_CACHE_MIB=8192`, pinned **host tier only** — the 64 GiB disk tier is gone, so an evicted prefix is re-prefilled; `STATE_DIR=/home/dank/radiance-state` now exists only to hold the rad-tune cache |
|
| Prefix cache | `PREFIX_CACHE_MIB=8192`, pinned **host tier only** — the 64 GiB disk tier is gone, so an evicted prefix is re-prefilled; `STATE_DIR=/home/dank/radiance-state` now exists only to hold the rad-tune cache |
|
||||||
| Template | `TEMPLATE=scripts/qwen38-froggeric-v22.5.jinja` (28,234 B) via `--override-chat-template` — froggeric's fixed Qwen3.8 template (the repo root's `chat_template.jinja`, vendored); the checkpoint's own template is baked into the `.rad` by rad-convert and this overrides it, and radiance derives the reasoning and tool-call reply format from it |
|
| Template | `TEMPLATE=scripts/qwen38-froggeric-v22.5.jinja` (28,234 B) via `--override-chat-template` — froggeric's fixed Qwen3.8 template (the repo root's `chat_template.jinja`, vendored); the checkpoint's own template is baked into the `.rad` by rad-convert and this overrides it, and radiance derives the reasoning and tool-call reply format from it |
|
||||||
| Step sharing | `BUSY_PREFILL_CHUNK=auto` — prefill-speed first: it reserves 3 tokens per waiting decoder and ⅛ of the budget per waiting request, and does **not** shrink the chunk for decoders alone · `PREFILL_INTERLEAVE=1` — the decoder dial, **armed 2026-10-20**: one decode-only step after each chunk step taken beside decoders; meter `engine_steps_held_total`, price is the prefilling prompt's TTFT |
|
| Step sharing | `BUSY_PREFILL_CHUNK=auto` — prefill-speed first: it reserves 3 tokens per waiting decoder and ⅛ of the budget per waiting request, and does **not** shrink the chunk for decoders alone · `PREFILL_INTERLEAVE=1` — the decoder dial, **armed 2026-10-20**: one decode-only step after each chunk step taken beside decoders; meter `engine_steps_held_total`, price is the prefilling prompt's TTFT |
|
||||||
| TTM gate | `TTM_MIN_MIB=197000`, `TTM_TARGET_MIB=224000` |
|
| TTM gate | `TTM_MIN_MIB=162000`, `TTM_TARGET_MIB=204800` |
|
||||||
| Boot | `RAD_MIN_MIB=110000` deployer floor; the listener binds only after the full load (health ~100 s, budget 1800 s) |
|
| Boot | `RAD_MIN_MIB=110000` deployer floor; the listener binds only after the full load (health ~100 s, budget 1800 s) |
|
||||||
|
|
||||||
Boot ledger for the live shape (2026-10-20, pin 1.8.1, `MAX_SEQS=4`, `HOST_POOL_MIB=49152`):
|
Boot ledger for the live payload. Last boot: 2026-10-29 at `MAX_SEQS=4`,
|
||||||
the resolver prints `elastic 26.73 GiB` → **experts 20.72 GiB (78 %) / kv cache 5.64 GiB (21 %)**,
|
`HOST_POOL_MIB=66560`, `MAX_BATCHED_TOKENS=2048`; the live pool is `49152` again, and the
|
||||||
`prefill staging 373.65 MiB` out of the cache, `vram_weights 24.30 GiB`, `host_pool 48.59 GiB`.
|
two boots are **identical above `host_pool`**, so one ledger serves both shapes:
|
||||||
The referee line: `kv: the cache can grow to 5.64 GiB a card — 747,492 tokens of context, 3.7
|
`elastic 26.73 GiB` → **experts 20.72 GiB (78 %) / kv cache 5.64 GiB (21 %)**,
|
||||||
sessions at 200,000 — with every block it lends the experts taken back; 440.04 MiB of that is
|
`activation arena 990.36 MiB` (the price of the 2,048-token step: `256 tokens use
|
||||||
per-sequence state, and up to 5.20 GiB is lent to the experts while no request holds it`.
|
155.60 MiB, 1024 tokens use 472.86 MiB of 896.20 MiB`), `prefill staging 373.65 MiB`,
|
||||||
`--checkpoint-slots 40` is auto (`max_num_seqs × 10` slots of 111.98 MiB = 4,479 MiB of pinned
|
`vram_weights 24.30 GiB`, `host_pool 65.59 GiB` (opened with `0 B used`; `48.59 GiB` at
|
||||||
host), `/health` reports `queue_cap 32` (`max(8, seqs × 8)`), sampler **12 rows**
|
the 49,152 boot), `unclaimed 96.89 MiB`; referee line `kv: the pool backs 93% of the worst
|
||||||
(= `seqs × (1 + speculative depth)`), 4 hardware queues a card, template 28,234 B, vision live
|
case (4 sequences all at 200000 tokens), 3.7 full-length sessions`; `device: the 2 cards
|
||||||
(≤ 4,194,304 px, 16,384 patches/pass), P2P confirmed (`the 2 cards reach each other's memory
|
reach each other's memory directly (peer to peer)`; sampler **12 rows**
|
||||||
directly`). Experts living in the pinned host pool: **8.99 GiB a rank** (was 5.99) — the price of
|
(= `seqs × (1 + speculative depth)`), 4 hardware queues a card (133.45 MiB), `already held
|
||||||
the bigger cache, paid over PCIe when those experts are hit.
|
256.67 MiB`, mover `17519 slab slots, 7624 pinned pool slots, 3.58 GiB static vram`,
|
||||||
|
`load: rank 0, mover took 24.30 GiB`.
|
||||||
|
|
||||||
|
The A/B that ended the raise: the 2026-10-20 boot at `HOST_POOL_MIB=49152` printed the
|
||||||
|
**same** `elastic 26.73 / experts 20.72 / kv cache 5.64 / vram_weights 24.30` (Laws: the
|
||||||
|
lever saturates at the expert floor), and the campaign rig — the other payload that
|
||||||
|
inherits this env — moved `vram_kv 269.99 → 271.25 MiB`, a rounding step, because its
|
||||||
|
window is set by its own n-gram table (95.37 GiB) and expert plane rather than by the
|
||||||
|
pool. The refused calibration documents are fixed by cutting them to the window, not by
|
||||||
|
widening the pool.
|
||||||
|
|
||||||
The boot **ledger**, not this arithmetic, is the referee: radiance states every budget before
|
The boot **ledger**, not this arithmetic, is the referee: radiance states every budget before
|
||||||
anything is allocated (weights vs KV, the host pool's coverage of the worst case, the PLE pin,
|
anything is allocated (weights vs KV, the host pool's coverage of the worst case, the PLE pin,
|
||||||
@@ -150,7 +159,7 @@ marked "re-fetch" is gone from `/home/dank/models/`, which today holds only `Sti
|
|||||||
|
|
||||||
| Window | Payload | Runtime | Shape | Replaced because | Rollback |
|
| Window | Payload | Runtime | Shape | Replaced because | Rollback |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| 2026-10-20 → 10-29 | same `qwen3.8-next-flash-fp8-iq4r-moe.rad` (payload unchanged) | radiance **1.8.1**, `sha256:d590e300…` (same digest) | 4 × 200,000 fp8 KV — resolver cache 5.64 GiB = 747,492 tokens, `prefill staging 373.65 MiB`, `vram_weights 24.30 GiB`, `host_pool 48.59 GiB`; host pool 49,152 MiB a rank, `TTM_MIN_MIB=162000` / `TTM_TARGET_MIB=204800`, `queue_cap 32`, sampler 12 rows, `--prefill-interleave 1` | the `qwen38-w5nl64` campaign rig booted this pool at the shared 1,024 step and reported `kv cache 269.99 MiB a card: 7436 tokens`, so 682 of its 1,294 calibration documents were refused; Frank ordered the pool **and** the step raised to production's widest legal step (2,048) as one shape | revert the `feat/frank-archon-pool-step` commit and redeploy `scripts/deploy.sh dankserver --service frank-archon` — trio back to `HOST_POOL_MIB=49152`, `TTM_MIN_MIB=162000`, `TTM_TARGET_MIB=204800` |
|
| 2026-10-20 → 10-29 | same `qwen3.8-next-flash-fp8-iq4r-moe.rad` (payload unchanged) | radiance **1.8.1**, `sha256:d590e300…` (same digest) | 4 × 200,000 fp8 KV — resolver cache 5.64 GiB = 747,492 tokens, `prefill staging 373.65 MiB`, `vram_weights 24.30 GiB`, `host_pool 48.59 GiB`; host pool 49,152 MiB a rank, `TTM_MIN_MIB=162000` / `TTM_TARGET_MIB=204800`, `queue_cap 32`, sampler 12 rows, `--prefill-interleave 1` | Frank ordered the campaign's step (2,048) onto production; the pool half of that order (66,560) booted, came back byte-identical to this row's cache, and was folded back the same day — the calibration documents it was meant to save were cut to the window instead (`scripts/rechunk-kld-corpus.py`) | the live shape is this row's pool with `MAX_BATCHED_TOKENS=2048`: the pair is the rollback unit, not the merge commit (`scripts/frank-archon.env`) |
|
||||||
| 2026-10-19 → 10-20 | same `qwen3.8-next-flash-fp8-iq4r-moe.rad` (payload unchanged) | radiance **1.8.1**, `sha256:d590e300…` (same digest) | 2 × 200,000 fp8 KV — resolver cache 2.76 GiB = 365,204 tokens (91 % backing); host pool 44,928 MiB a rank, `TTM_MIN_MIB=150000`, `queue_cap 16`, sampler 9 rows; `--prefill-interleave 1` armed on 10-20 | Frank ordered **four** concurrent conversations over a ≥ 700,000-token pool; the resolver funds that only by lowering the expert floor with more pinned host room (Laws) | revert `1bb6399` and redeploy `scripts/deploy.sh dankserver --service frank-archon` — trio back to `MAX_SEQS=2` / `HOST_POOL_MIB=44928` / `TTM_MIN_MIB=150000` |
|
| 2026-10-19 → 10-20 | same `qwen3.8-next-flash-fp8-iq4r-moe.rad` (payload unchanged) | radiance **1.8.1**, `sha256:d590e300…` (same digest) | 2 × 200,000 fp8 KV — resolver cache 2.76 GiB = 365,204 tokens (91 % backing); host pool 44,928 MiB a rank, `TTM_MIN_MIB=150000`, `queue_cap 16`, sampler 9 rows; `--prefill-interleave 1` armed on 10-20 | Frank ordered **four** concurrent conversations over a ≥ 700,000-token pool; the resolver funds that only by lowering the expert floor with more pinned host room (Laws) | revert `1bb6399` and redeploy `scripts/deploy.sh dankserver --service frank-archon` — trio back to `MAX_SEQS=2` / `HOST_POOL_MIB=44928` / `TTM_MIN_MIB=150000` |
|
||||||
| 2026-10-15 → 10-19 | same `qwen3.8-next-flash-fp8-iq4r-moe.rad` (payload unchanged) | radiance **1.0.13**, `sha256:4c3f4845…` | 3 × 200,000 fp8 KV bought with the **explicit** pair `--vram-weights-mib 25600 --vram-kv-mib 5120`; host pool 44,928 → 45,568 MiB a rank; prefix cache 8 GiB host + 64 GiB disk | decode turned to sludge beside any long prompt — step contention, 18.7 ms decode-only steps against 317 ms steps carrying a prefill chunk (Laws) — and the only two knobs that fix it, `--busy-prefill-chunk` and `--prefill-interleave`, exist only after this pin | `FRANK_ARCHON_IMAGE` back to `sha256:4c3f4845…` **plus a trio rewrite**: restore the pair and the host+disk prefix knobs, delete `BUSY_PREFILL_CHUNK`/`PREFILL_INTERLEAVE`. A digest swap alone is a boot refusal (Runtime) |
|
| 2026-10-15 → 10-19 | same `qwen3.8-next-flash-fp8-iq4r-moe.rad` (payload unchanged) | radiance **1.0.13**, `sha256:4c3f4845…` | 3 × 200,000 fp8 KV bought with the **explicit** pair `--vram-weights-mib 25600 --vram-kv-mib 5120`; host pool 44,928 → 45,568 MiB a rank; prefix cache 8 GiB host + 64 GiB disk | decode turned to sludge beside any long prompt — step contention, 18.7 ms decode-only steps against 317 ms steps carrying a prefill chunk (Laws) — and the only two knobs that fix it, `--busy-prefill-chunk` and `--prefill-interleave`, exist only after this pin | `FRANK_ARCHON_IMAGE` back to `sha256:4c3f4845…` **plus a trio rewrite**: restore the pair and the host+disk prefix knobs, delete `BUSY_PREFILL_CHUNK`/`PREFILL_INTERLEAVE`. A digest swap alone is a boot refusal (Runtime) |
|
||||||
| 2026-10-08 → 10-15 | in-house `qwen4exp-swift15-w4dn8-i8-hc8m.rad`, 116,416 MiB (Swift1.5 abliterated BF16, GPTQ w4 experts + 24 bf16 protected, KLD 0.0131 / top-1 97.92 %) | radiance 1.0.13, same digest | 3 × 200,000 fp8 KV, slab 25,600 / pool 5,120 — the shape the live file still carries | the engine author's own container is the supported path and his quality ceiling; the finetune's refusal-bending left the endpoint | `.rad` purged 2026-10-17 (trash-staged then emptied, Frank's approval). Re-download `frank8465/Swift-1.5-Qwen3.8-Flash-Next-GPTQ-W4A8-Radiance-AMD-R9700` → `qwen4exp-w4nl64-i8-hc8m.rad`, 122,071,973,888 B, LFS sha256 `09dd9306…`, via the iq4r fetch script's hfget/verify pattern retargeted at that repo; point `MODEL_FILE` at it, redeploy. Re-**convert** is dead: the BF16 source was `rm`'d 2026-10-15 (`kld-ref`, grams, corpora and `/home/dank/logs/swift15_*.log` survive) |
|
| 2026-10-08 → 10-15 | in-house `qwen4exp-swift15-w4dn8-i8-hc8m.rad`, 116,416 MiB (Swift1.5 abliterated BF16, GPTQ w4 experts + 24 bf16 protected, KLD 0.0131 / top-1 97.92 %) | radiance 1.0.13, same digest | 3 × 200,000 fp8 KV, slab 25,600 / pool 5,120 — the shape the live file still carries | the engine author's own container is the supported path and his quality ceiling; the finetune's refusal-bending left the endpoint | `.rad` purged 2026-10-17 (trash-staged then emptied, Frank's approval). Re-download `frank8465/Swift-1.5-Qwen3.8-Flash-Next-GPTQ-W4A8-Radiance-AMD-R9700` → `qwen4exp-w4nl64-i8-hc8m.rad`, 122,071,973,888 B, LFS sha256 `09dd9306…`, via the iq4r fetch script's hfget/verify pattern retargeted at that repo; point `MODEL_FILE` at it, redeploy. Re-**convert** is dead: the BF16 source was `rm`'d 2026-10-15 (`kld-ref`, grams, corpora and `/home/dank/logs/swift15_*.log` survive) |
|
||||||
@@ -244,7 +253,8 @@ vllm-radiance era (this table opens on 09-26; the era page claims 10-10).
|
|||||||
| 3 × 200,000, host pool 44,928 | 600,000 tok | 4.20 GiB | 93 % |
|
| 3 × 200,000, host pool 44,928 | 600,000 tok | 4.20 GiB | 93 % |
|
||||||
| 3 × 200,000, host pool 45,568 | 600,000 tok | **4.20 GiB — byte-identical** | 93 % |
|
| 3 × 200,000, host pool 45,568 | 600,000 tok | **4.20 GiB — byte-identical** | 93 % |
|
||||||
| 2 × 200,000, host pool 44,928 | 400,000 tok | 2.76 GiB | 91 % |
|
| 2 × 200,000, host pool 44,928 | 400,000 tok | 2.76 GiB | 91 % |
|
||||||
| 4 × 200,000, host pool 49,152 (live) | 800,000 tok | **5.64 GiB = 747,492 tok** | 93 % |
|
| 4 × 200,000, host pool 49,152 (live) | 800,000 tok | 5.64 GiB = 747,492 tok | 93 % |
|
||||||
|
| 4 × 200,000, host pool 66,560 (2026-10-29 A/B, folded back) | 800,000 tok | **5.64 GiB — byte-identical to 49,152** | 93 % |
|
||||||
|
|
||||||
The 45,568 row is the ceiling case, not a mystery: at 3 slots the cache already held its whole
|
The 45,568 row is the ceiling case, not a mystery: at 3 slots the cache already held its whole
|
||||||
`ceiling`, so extra host room could not grow it and shows up as `unclaimed`. **The referee is the
|
`ceiling`, so extra host room could not grow it and shows up as `unclaimed`. **The referee is the
|
||||||
@@ -631,7 +641,10 @@ vllm-radiance era (this table opens on 09-26; the era page claims 10-10).
|
|||||||
- Convert campaign (dormant, kept for a future quant): `scripts/convert-swift15-w4dn8.sh`
|
- Convert campaign (dormant, kept for a future quant): `scripts/convert-swift15-w4dn8.sh`
|
||||||
`scripts/recipes/qwen4exp-swift15-w4dn8-i8-hc8m.recipe`, `scripts/run-swift15-bf16.sh`
|
`scripts/recipes/qwen4exp-swift15-w4dn8-i8-hc8m.recipe`, `scripts/run-swift15-bf16.sh`
|
||||||
`scripts/kld-gate-swift15.sh`, `scripts/kld-then-calib.sh`, `scripts/calib-push.py`
|
`scripts/kld-gate-swift15.sh`, `scripts/kld-then-calib.sh`, `scripts/calib-push.py`
|
||||||
`scripts/make-calib-corpus.py`, `scripts/derive-protected-experts.py`
|
`scripts/make-calib-corpus.py`, `scripts/rechunk-kld-corpus.py`,
|
||||||
|
`scripts/qwen38_common.py` (the tokenizer, JSONL and refusal plumbing those three
|
||||||
|
corpus tools share — one `n_tokens`, so every tool counts the same way),
|
||||||
|
`scripts/derive-protected-experts.py`
|
||||||
`scripts/apply-protected-cast.sh`, `scripts/verify-swift15-ple.py`.
|
`scripts/apply-protected-cast.sh`, `scripts/verify-swift15-ple.py`.
|
||||||
- Probes: `scripts/probe-frank-archon-kv.py` (reads slots/window/dtype from the deployed env so
|
- Probes: `scripts/probe-frank-archon-kv.py` (reads slots/window/dtype from the deployed env so
|
||||||
it cannot drift; leg D = decode), `scripts/frank-archon-expert-activity.py`
|
it cannot drift; leg D = decode), `scripts/frank-archon-expert-activity.py`
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ nosudo_bin() {
|
|||||||
# so the pool's token count is a boot-ledger line — verified live by the
|
# so the pool's token count is a boot-ledger line — verified live by the
|
||||||
# capacity leg of scripts/probe-frank-archon-kv.py at every cutover.
|
# capacity leg of scripts/probe-frank-archon-kv.py at every cutover.
|
||||||
grep -q '^FRANK_ARCHON_PLACEMENT=expert_tiered$' "$ENV_FILE"
|
grep -q '^FRANK_ARCHON_PLACEMENT=expert_tiered$' "$ENV_FILE"
|
||||||
grep -q '^FRANK_ARCHON_HOST_POOL_MIB=66560$' "$ENV_FILE"
|
grep -q '^FRANK_ARCHON_HOST_POOL_MIB=49152$' "$ENV_FILE"
|
||||||
grep -q '^FRANK_ARCHON_KV_DTYPE=fp8$' "$ENV_FILE"
|
grep -q '^FRANK_ARCHON_KV_DTYPE=fp8$' "$ENV_FILE"
|
||||||
grep -q '^FRANK_ARCHON_NGRAM_PLACEMENT=ram$' "$ENV_FILE"
|
grep -q '^FRANK_ARCHON_NGRAM_PLACEMENT=ram$' "$ENV_FILE"
|
||||||
# Prefix cache: pinned host RAM only (the disk tier was removed), and the
|
# Prefix cache: pinned host RAM only (the disk tier was removed), and the
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ RUNNER="$REPO_ROOT/scripts/run-qwen38-bf16.sh"
|
|||||||
# The reference shape, spelled exactly as run-qwen38-bf16.sh must pass it: this
|
# The reference shape, spelled exactly as run-qwen38-bf16.sh must pass it: this
|
||||||
# is the contract the recorded reference is measured at, so a silent edit here
|
# is the contract the recorded reference is measured at, so a silent edit here
|
||||||
# is a silent edit to every KLD number in the campaign.
|
# is a silent edit to every KLD number in the campaign.
|
||||||
SHAPE="--tp 2 --placement expert_tiered --weights-disk-tier --host-pool-mib 66560 --gpu-headroom-mib 96 --deterministic --num-speculative-tokens 0 --kv-cache-dtype fp8 --max-model-len 32768 --ngram-placement ram --override-chat-template /models/tpl.jinja"
|
SHAPE="--tp 2 --placement expert_tiered --weights-disk-tier --host-pool-mib 49152 --gpu-headroom-mib 96 --deterministic --num-speculative-tokens 0 --kv-cache-dtype fp8 --max-model-len 32768 --ngram-placement ram --override-chat-template /models/tpl.jinja"
|
||||||
|
|
||||||
# oneline — both dry runs print one shell-quoted argument a line; joining them
|
# oneline — both dry runs print one shell-quoted argument a line; joining them
|
||||||
# lets an option and its value be asserted as the pair they are.
|
# lets an option and its value be asserted as the pair they are.
|
||||||
|
|||||||
Reference in new issue
Block a user