crit 0.10.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crit/__init__.py +2 -0
- crit/cli.py +55 -0
- crit/inference.py +153 -0
- crit/process/__init__.py +0 -0
- crit/process/guardian.py +63 -0
- crit/process/ownership.py +270 -0
- crit/process/tree.py +283 -0
- crit/run/__init__.py +0 -0
- crit/run/cli.py +225 -0
- crit/run/cost.py +347 -0
- crit/run/evaluation.py +174 -0
- crit/run/hub.py +144 -0
- crit/run/measure.py +365 -0
- crit/run/plans.py +1206 -0
- crit/run/recon.py +566 -0
- crit/run/records.py +1099 -0
- crit/run/served.py +184 -0
- crit/run/store.py +1106 -0
- crit-0.10.0.dist-info/METADATA +508 -0
- crit-0.10.0.dist-info/RECORD +53 -0
- crit-0.10.0.dist-info/WHEEL +5 -0
- crit-0.10.0.dist-info/entry_points.txt +2 -0
- crit-0.10.0.dist-info/licenses/LICENSE +208 -0
- crit-0.10.0.dist-info/top_level.txt +4 -0
- crit_api/__init__.py +1 -0
- crit_api/admission.py +153 -0
- crit_api/anthropic.py +468 -0
- crit_api/catalog.py +87 -0
- crit_api/channel.py +214 -0
- crit_api/cli.py +220 -0
- crit_api/content.py +51 -0
- crit_api/delivery.py +114 -0
- crit_api/disconnect.py +66 -0
- crit_api/engine.py +988 -0
- crit_api/host.py +450 -0
- crit_api/logprobs.py +140 -0
- crit_api/metrics.py +169 -0
- crit_api/models.py +320 -0
- crit_api/ollama.py +357 -0
- crit_api/options.py +344 -0
- crit_api/residency.py +159 -0
- crit_api/responses.py +410 -0
- crit_api/server.py +1866 -0
- crit_api/stream.py +100 -0
- crit_api/tool_stream.py +97 -0
- crit_kernels/__init__.py +351 -0
- crit_models/__init__.py +114 -0
- crit_models/calibration.py +116 -0
- crit_models/codecs.py +156 -0
- crit_models/drafter.py +314 -0
- crit_models/evaluate.py +71 -0
- crit_models/ptq.py +375 -0
- crit_models/recipe.py +177 -0
crit/__init__.py
ADDED
crit/cli.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Small product CLI; implementation code is never imported by navigation."""
|
|
2
|
+
import argparse
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main(argv=None):
|
|
10
|
+
# Route subcommands with their own parsers before the maintenance parser.
|
|
11
|
+
arguments = list(sys.argv[1:] if argv is None else argv)
|
|
12
|
+
if arguments and arguments[0] == "run":
|
|
13
|
+
from .run.cli import main as run_main
|
|
14
|
+
return run_main(arguments[1:])
|
|
15
|
+
if arguments and arguments[0] == "serve":
|
|
16
|
+
from crit_api.cli import main as serve_main
|
|
17
|
+
return serve_main(arguments[1:])
|
|
18
|
+
if arguments and arguments[0] == "kernels":
|
|
19
|
+
from crit_kernels import main as kernels_main
|
|
20
|
+
return kernels_main(arguments[1:])
|
|
21
|
+
if arguments and arguments[0] == "models":
|
|
22
|
+
from crit_models import main as models_main
|
|
23
|
+
return models_main(arguments[1:])
|
|
24
|
+
if arguments and arguments[0] == "eval":
|
|
25
|
+
try:
|
|
26
|
+
from crit_eval.cli import main as eval_main
|
|
27
|
+
except ImportError:
|
|
28
|
+
raise SystemExit("crit eval needs crit Eval (https://github.com/crit-labs/crit-eval): install it with "
|
|
29
|
+
"python -m pip install -e ../crit-eval beside this checkout") from None
|
|
30
|
+
return eval_main(arguments[1:])
|
|
31
|
+
parser = argparse.ArgumentParser(description="crit Core, Inference, Kernels, Models, Eval, run records and the serving API")
|
|
32
|
+
from . import __version__
|
|
33
|
+
parser.add_argument("--version", action="version", version="crit " + __version__)
|
|
34
|
+
subs = parser.add_subparsers(dest="command", required=True)
|
|
35
|
+
subs.add_parser("contracts", help="Print native core contract locations")
|
|
36
|
+
subs.add_parser("kernels", help="Inspect kernel applicability and retain verified dependency bytes")
|
|
37
|
+
subs.add_parser("models", help="Transform model weights (PTQ) and record how; use crit models --help")
|
|
38
|
+
subs.add_parser("serve", help="Serve a sealed candidate as an OpenAI-compatible API; use crit serve --help")
|
|
39
|
+
subs.add_parser("run", help="Publish retained run/candidate records; use crit run --help")
|
|
40
|
+
subs.add_parser("eval", help="Run the common speed, quality and exactness suite (crit Eval); use crit eval --help")
|
|
41
|
+
sub = subs.add_parser("inference", help="Query or check the inference techniques")
|
|
42
|
+
sub.add_argument("arguments", nargs=argparse.REMAINDER)
|
|
43
|
+
args = parser.parse_args(arguments)
|
|
44
|
+
repository = ROOT if (ROOT / "core/include/crit/core.h").is_file() else Path.cwd()
|
|
45
|
+
if not (repository / "core/include/crit/core.h").is_file():
|
|
46
|
+
parser.error("Run repository maintenance commands from the crit repository")
|
|
47
|
+
if args.command == "inference":
|
|
48
|
+
from . import inference
|
|
49
|
+
return inference.main(args.arguments, repository)
|
|
50
|
+
print("core/include/crit/core.h\ncore/include/crit/cuda.h\ncore/contracts/README.md")
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
raise SystemExit(main())
|
crit/inference.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Query/check the crit inference techniques; never import implementation code."""
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path, PurePosixPath
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
8
|
+
GUIDANCE_TEXT = ("use_when", "avoid_when")
|
|
9
|
+
GUIDANCE_LISTS = ("provides", "engine_supplies", "preserve", "adapt")
|
|
10
|
+
# Navigation metadata, not folders: every entry names one category; `list` groups by it in this order.
|
|
11
|
+
CATEGORIES = ("attention", "projections", "experts", "speculation", "sampling-and-constraints",
|
|
12
|
+
"execution", "memory-and-loading")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def under(root, relative):
|
|
16
|
+
if not isinstance(relative, str) or not relative or "\\" in relative or ":" in relative:
|
|
17
|
+
raise ValueError("canonical repository-relative path required")
|
|
18
|
+
path = PurePosixPath(relative)
|
|
19
|
+
if path.is_absolute() or any(p in {"", ".", ".."} for p in relative.split("/")):
|
|
20
|
+
raise ValueError("canonical repository-relative path required")
|
|
21
|
+
target = (root / relative).resolve()
|
|
22
|
+
if not target.is_relative_to(root.resolve()):
|
|
23
|
+
raise ValueError("path escapes the repository")
|
|
24
|
+
return target
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load(root=ROOT):
|
|
28
|
+
"""Read authored entries directly, without generated files or code imports."""
|
|
29
|
+
entries = [json.loads(p.read_text(encoding="utf-8")) for p in sorted((root / "inference").glob("*/entry.json"))]
|
|
30
|
+
return {"schema_version": 3, "collection_name": "crit Inference", "entries": entries}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def check(root=ROOT):
|
|
34
|
+
errors = []
|
|
35
|
+
store = load(root)
|
|
36
|
+
ids = set()
|
|
37
|
+
if not store["entries"]:
|
|
38
|
+
errors.append("Inference contains no techniques")
|
|
39
|
+
for entry in store["entries"]:
|
|
40
|
+
ident = entry["id"]
|
|
41
|
+
under(root, "inference/" + ident)
|
|
42
|
+
if ident in ids:
|
|
43
|
+
errors.append("duplicate technique ID")
|
|
44
|
+
ids.add(ident)
|
|
45
|
+
if any(field in entry for field in ("donors", "origin_path", "source_bundle_path", "code_locations")):
|
|
46
|
+
errors.append(f"{ident}: external source-bundle metadata is not a crit technique")
|
|
47
|
+
if entry.get("category") not in CATEGORIES:
|
|
48
|
+
errors.append(f"{ident}: category must be one of " + ", ".join(CATEGORIES))
|
|
49
|
+
if not entry["summary"] or not entry["implementation_scope"] or not entry["code_files"]:
|
|
50
|
+
errors.append(f"{ident}: description and actual implementation required")
|
|
51
|
+
if entry.get("implementation_status") not in {"reference", "native_source"}:
|
|
52
|
+
errors.append(f"{ident}: explicit reference/native-source scope required")
|
|
53
|
+
if any(not entry.get("validation", {}).get(field) for field in ("host", "native", "performance")):
|
|
54
|
+
errors.append(f"{ident}: host/native/performance status must be explicit")
|
|
55
|
+
guidance = entry.get("agent_guidance")
|
|
56
|
+
if not isinstance(guidance, dict):
|
|
57
|
+
errors.append(f"{ident}: agent selection, implementation and adaptation guidance required")
|
|
58
|
+
else:
|
|
59
|
+
for field in GUIDANCE_TEXT:
|
|
60
|
+
value = guidance.get(field)
|
|
61
|
+
if not isinstance(value, str) or not value.strip():
|
|
62
|
+
errors.append(f"{ident}: agent_guidance.{field} must be nonempty text")
|
|
63
|
+
for field in GUIDANCE_LISTS:
|
|
64
|
+
value = guidance.get(field)
|
|
65
|
+
if not isinstance(value, list) or not value or any(not isinstance(item, str) or not item.strip() for item in value):
|
|
66
|
+
errors.append(f"{ident}: agent_guidance.{field} must list concrete responsibilities or choices")
|
|
67
|
+
if entry["code_path"] != f"inference/{ident}/code" or entry["path"] != f"inference/{ident}/README.md":
|
|
68
|
+
errors.append(f"{ident}: technique-local description/code required")
|
|
69
|
+
if not under(root, entry["path"]).is_file():
|
|
70
|
+
errors.append(f"{ident}: description missing")
|
|
71
|
+
for field in ("input", "output", "ownership", "failure", "core_boundary"):
|
|
72
|
+
if not entry["contract"].get(field):
|
|
73
|
+
errors.append(f"{ident}: missing {field} contract")
|
|
74
|
+
for field in ("hardware", "model", "workload"):
|
|
75
|
+
if not entry["requires"].get(field):
|
|
76
|
+
errors.append(f"{ident}: missing {field} requirement")
|
|
77
|
+
actual = False
|
|
78
|
+
listed = set()
|
|
79
|
+
for code in entry["code_files"]:
|
|
80
|
+
path = under(root, code["path"])
|
|
81
|
+
if not path.is_relative_to(under(root, entry["code_path"])) or not path.is_file():
|
|
82
|
+
errors.append(f"{ident}: actual code must be inside the feature")
|
|
83
|
+
elif path.suffix in {".py", ".cpp", ".cu", ".h"} and path.stat().st_size:
|
|
84
|
+
actual = True
|
|
85
|
+
listed.add(path)
|
|
86
|
+
if not actual:
|
|
87
|
+
errors.append(f"{ident}: code folder contains no actual implementation")
|
|
88
|
+
for path in under(root, entry["code_path"]).rglob("*"):
|
|
89
|
+
if path.is_file() and path.suffix in {".py", ".cpp", ".cu", ".h"} and path.resolve() not in listed:
|
|
90
|
+
errors.append(f"{ident}: implementation file missing from metadata")
|
|
91
|
+
return errors
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def main(argv=None, root=ROOT):
|
|
95
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
96
|
+
subs = parser.add_subparsers(dest="command", required=True)
|
|
97
|
+
for name in ("list", "search", "show"):
|
|
98
|
+
sub = subs.add_parser(name)
|
|
99
|
+
if name != "list":
|
|
100
|
+
sub.add_argument("query")
|
|
101
|
+
sub.add_argument("--json", action="store_true")
|
|
102
|
+
subs.add_parser("check")
|
|
103
|
+
args = parser.parse_args(argv)
|
|
104
|
+
try:
|
|
105
|
+
if args.command == "check":
|
|
106
|
+
errors = check(root)
|
|
107
|
+
if errors:
|
|
108
|
+
raise ValueError("\n".join(errors))
|
|
109
|
+
print("crit Inference passed: actual technique code and authored metadata.")
|
|
110
|
+
else:
|
|
111
|
+
entries = load(root)["entries"]
|
|
112
|
+
if args.command == "show":
|
|
113
|
+
entries = [e for e in entries if e["id"] == args.query]
|
|
114
|
+
if not entries:
|
|
115
|
+
raise ValueError("unknown technique")
|
|
116
|
+
elif args.command == "search":
|
|
117
|
+
words = args.query.casefold().split()
|
|
118
|
+
entries = [e for e in entries if all(w in json.dumps(e).casefold() for w in words)]
|
|
119
|
+
if args.json:
|
|
120
|
+
print(json.dumps(entries[0] if args.command == "show" else entries, indent=2))
|
|
121
|
+
elif args.command == "list":
|
|
122
|
+
for category in CATEGORIES + tuple(sorted({e.get("category") or "" for e in entries} - set(CATEGORIES))):
|
|
123
|
+
group = [e for e in entries if (e.get("category") or "") == category]
|
|
124
|
+
if group:
|
|
125
|
+
print((category or "uncategorized") + ":")
|
|
126
|
+
for entry in group:
|
|
127
|
+
print(" " + entry["id"] + ": " + entry["summary"])
|
|
128
|
+
else:
|
|
129
|
+
for entry in entries:
|
|
130
|
+
print(entry["id"] + ": " + entry["summary"])
|
|
131
|
+
if args.command == "show":
|
|
132
|
+
print("Category: " + str(entry.get("category")))
|
|
133
|
+
print("Scope: " + entry["implementation_scope"])
|
|
134
|
+
guidance = entry["agent_guidance"]
|
|
135
|
+
print("Use when: " + guidance["use_when"])
|
|
136
|
+
print("Consider another approach when: " + guidance["avoid_when"])
|
|
137
|
+
for title, field in (("Supplied here", "provides"), ("Engine supplies", "engine_supplies"),
|
|
138
|
+
("Preserve", "preserve"), ("Adapt", "adapt")):
|
|
139
|
+
print(title + ":")
|
|
140
|
+
for item in guidance[field]:
|
|
141
|
+
print(" - " + item)
|
|
142
|
+
print("Description and worked example: " + entry["path"])
|
|
143
|
+
print("Code:")
|
|
144
|
+
for code in entry["code_files"]:
|
|
145
|
+
print(" " + code["path"])
|
|
146
|
+
return 0
|
|
147
|
+
except (ValueError, OSError, KeyError, TypeError) as exc:
|
|
148
|
+
print("ERROR: " + str(exc), file=sys.stderr)
|
|
149
|
+
return 1
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
raise SystemExit(main())
|
crit/process/__init__.py
ADDED
|
File without changes
|
crit/process/guardian.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Launch gate and parent-death monitor, inside the supervisor's process tree."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import os
|
|
4
|
+
import signal
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
if __package__ in {None, ""}:
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
11
|
+
from crit.process.ownership import process_identity, read_ownership
|
|
12
|
+
from crit.run.records import read_json, write_json
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
launch = read_json(Path(sys.argv[1]))
|
|
17
|
+
owner_path = Path(launch["owner_path"])
|
|
18
|
+
owner = read_ownership(owner_path)
|
|
19
|
+
identity = process_identity(os.getpid())
|
|
20
|
+
if (identity in {None, "unknown"} or owner["token"] != launch["token"] or
|
|
21
|
+
process_identity(owner["owner_pid"]) != owner["owner_identity"]):
|
|
22
|
+
return 125
|
|
23
|
+
# A Windows virtual-environment launcher is a different process from the
|
|
24
|
+
# interpreter running this file. Report the actual guardian before the
|
|
25
|
+
# supervisor verifies containment and grants permission to start the engine.
|
|
26
|
+
write_json(Path(sys.argv[1]).with_suffix(".ready.json"),
|
|
27
|
+
{"token": launch["token"], "pid": os.getpid(), "identity": identity}, immutable=True)
|
|
28
|
+
end = time.monotonic() + 10
|
|
29
|
+
while True:
|
|
30
|
+
owner = read_ownership(owner_path)
|
|
31
|
+
if owner["token"] != launch["token"] or process_identity(owner["owner_pid"]) != owner["owner_identity"]:
|
|
32
|
+
return 125
|
|
33
|
+
child = owner.get("process")
|
|
34
|
+
if child and child["pid"] == os.getpid() and child["identity"] == process_identity(os.getpid()):
|
|
35
|
+
break
|
|
36
|
+
if time.monotonic() >= end:
|
|
37
|
+
return 125
|
|
38
|
+
time.sleep(.02)
|
|
39
|
+
# Inherit the guardian's POSIX process group / Windows Job Object.
|
|
40
|
+
try:
|
|
41
|
+
engine = subprocess.Popen(launch["command"], cwd=launch["cwd"], stdin=subprocess.DEVNULL)
|
|
42
|
+
except OSError as exc:
|
|
43
|
+
write_json(Path(sys.argv[1]).with_suffix(".error.json"), {"error": str(exc)}, immutable=True)
|
|
44
|
+
return 125
|
|
45
|
+
while engine.poll() is None:
|
|
46
|
+
try:
|
|
47
|
+
owner = read_ownership(owner_path)
|
|
48
|
+
alive = owner["token"] == launch["token"] and process_identity(owner["owner_pid"]) == owner["owner_identity"]
|
|
49
|
+
except (OSError, ValueError, KeyError) as exc:
|
|
50
|
+
print("Launch guardian could not read ownership: " + str(exc), file=sys.stderr, flush=True)
|
|
51
|
+
alive = False
|
|
52
|
+
if not alive:
|
|
53
|
+
if os.name != "nt":
|
|
54
|
+
os.killpg(os.getpgrp(), signal.SIGKILL)
|
|
55
|
+
engine.kill() # Windows job also closes when its owner exits.
|
|
56
|
+
engine.wait(timeout=5)
|
|
57
|
+
return 125
|
|
58
|
+
time.sleep(.05)
|
|
59
|
+
return engine.returncode
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Durable per-user runtime ownership; stale/failed cleanup never grants admission."""
|
|
2
|
+
import getpass
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import signal
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
import tempfile
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
from crit.run.records import keys, now, number, read_json, safe_path, write_json
|
|
14
|
+
from crit.run.store import lease
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def process_identity(pid):
|
|
18
|
+
if not isinstance(pid, int) or pid <= 0:
|
|
19
|
+
return None
|
|
20
|
+
if os.name == "nt":
|
|
21
|
+
import ctypes as c
|
|
22
|
+
from ctypes import wintypes as w
|
|
23
|
+
api = c.WinDLL("kernel32", use_last_error=True)
|
|
24
|
+
api.OpenProcess.argtypes, api.OpenProcess.restype = [w.DWORD, w.BOOL, w.DWORD], w.HANDLE
|
|
25
|
+
api.GetProcessTimes.argtypes = [w.HANDLE] + [c.POINTER(w.FILETIME)] * 4
|
|
26
|
+
api.CloseHandle.argtypes = [w.HANDLE]
|
|
27
|
+
api.GetExitCodeProcess.argtypes = [w.HANDLE, c.POINTER(w.DWORD)]
|
|
28
|
+
handle = api.OpenProcess(0x1000, False, pid)
|
|
29
|
+
if not handle:
|
|
30
|
+
return None if c.get_last_error() in {87, 1168} else "unknown"
|
|
31
|
+
try:
|
|
32
|
+
code = w.DWORD()
|
|
33
|
+
if not api.GetExitCodeProcess(handle, c.byref(code)):
|
|
34
|
+
return "unknown"
|
|
35
|
+
if code.value != 259:
|
|
36
|
+
return None
|
|
37
|
+
values = [w.FILETIME() for _ in range(4)]
|
|
38
|
+
if not api.GetProcessTimes(handle, *(c.byref(v) for v in values)):
|
|
39
|
+
return "unknown"
|
|
40
|
+
return str((values[0].dwHighDateTime << 32) | values[0].dwLowDateTime)
|
|
41
|
+
finally:
|
|
42
|
+
api.CloseHandle(handle)
|
|
43
|
+
try:
|
|
44
|
+
fields = Path(f"/proc/{pid}/stat").read_text().rsplit(")", 1)[1].split()
|
|
45
|
+
if fields[0] in {"Z", "X"}:
|
|
46
|
+
return None
|
|
47
|
+
boot = Path("/proc/sys/kernel/random/boot_id").read_text().strip()
|
|
48
|
+
return boot + ":" + fields[19]
|
|
49
|
+
except FileNotFoundError:
|
|
50
|
+
if Path("/proc").is_dir():
|
|
51
|
+
return None
|
|
52
|
+
except (OSError, IndexError):
|
|
53
|
+
return "unknown"
|
|
54
|
+
try:
|
|
55
|
+
os.kill(pid, 0)
|
|
56
|
+
return "unknown"
|
|
57
|
+
except ProcessLookupError:
|
|
58
|
+
return None
|
|
59
|
+
except PermissionError:
|
|
60
|
+
return "unknown"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def group_alive(pgid):
|
|
64
|
+
if Path("/proc/self/stat").exists():
|
|
65
|
+
for path in Path("/proc").glob("[0-9]*/stat"):
|
|
66
|
+
try:
|
|
67
|
+
fields = path.read_text().rsplit(")", 1)[1].split()
|
|
68
|
+
if int(fields[2]) == pgid and fields[0] not in {"Z", "X"}:
|
|
69
|
+
return True
|
|
70
|
+
except FileNotFoundError:
|
|
71
|
+
continue
|
|
72
|
+
except (OSError, ValueError, IndexError):
|
|
73
|
+
return True # An unobservable member cannot prove settlement.
|
|
74
|
+
return False
|
|
75
|
+
try:
|
|
76
|
+
os.killpg(pgid, 0)
|
|
77
|
+
return True
|
|
78
|
+
except ProcessLookupError:
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def windows_job_settled(name):
|
|
83
|
+
import ctypes as c
|
|
84
|
+
from ctypes import wintypes as w
|
|
85
|
+
if not name:
|
|
86
|
+
raise ValueError("No retained Windows job identity; cleanup cannot be proven")
|
|
87
|
+
api = c.WinDLL("kernel32", use_last_error=True)
|
|
88
|
+
api.OpenJobObjectW.argtypes, api.OpenJobObjectW.restype = [w.DWORD, w.BOOL, w.LPCWSTR], w.HANDLE
|
|
89
|
+
api.QueryInformationJobObject.argtypes = [w.HANDLE, c.c_int, c.c_void_p, w.DWORD, c.c_void_p]
|
|
90
|
+
api.CloseHandle.argtypes = [w.HANDLE]
|
|
91
|
+
handle = api.OpenJobObjectW(4, False, name) # JOB_OBJECT_QUERY
|
|
92
|
+
if not handle:
|
|
93
|
+
if c.get_last_error() == 2: # Destroyed only after the owned processes exit.
|
|
94
|
+
return True
|
|
95
|
+
raise c.WinError(c.get_last_error())
|
|
96
|
+
try:
|
|
97
|
+
class Accounting(c.Structure):
|
|
98
|
+
_fields_ = [("times", c.c_int64 * 4), ("faults", w.DWORD), ("total", w.DWORD),
|
|
99
|
+
("active", w.DWORD), ("terminated", w.DWORD)]
|
|
100
|
+
value = Accounting()
|
|
101
|
+
if not api.QueryInformationJobObject(handle, 1, c.byref(value), c.sizeof(value), None):
|
|
102
|
+
raise c.WinError(c.get_last_error())
|
|
103
|
+
return value.active == 0
|
|
104
|
+
finally:
|
|
105
|
+
api.CloseHandle(handle)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# How long a killed process tree may take to exit before cleanup counts as failed. Exit is not immediate: a process
|
|
109
|
+
# that pinned tens of GB of host memory or holds a GPU context releases it after SIGKILL (several seconds here).
|
|
110
|
+
RETIREMENT_SECONDS = 60
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def read_ownership(path):
|
|
114
|
+
"""Read fresh ownership after bounded sharing-denial retries; never infer ownership from cached data.
|
|
115
|
+
|
|
116
|
+
Both supervisor and guardian need this: Windows atomic replacement can briefly deny an open even while
|
|
117
|
+
the owner remains alive. Missing/malformed records and an expired retry still fail closed.
|
|
118
|
+
"""
|
|
119
|
+
deadline = time.monotonic() + .25
|
|
120
|
+
while True:
|
|
121
|
+
try:
|
|
122
|
+
return read_json(path)
|
|
123
|
+
except PermissionError:
|
|
124
|
+
if time.monotonic() >= deadline:
|
|
125
|
+
raise
|
|
126
|
+
time.sleep(.005)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def default_directory():
|
|
130
|
+
user = str(os.getuid()) if hasattr(os, "getuid") else hashlib.sha256(getpass.getuser().encode()).hexdigest()[:16]
|
|
131
|
+
return Path(os.environ.get("CRIT_RUNTIME_DIRECTORY", str(Path(tempfile.gettempdir()) / ("crit-runtime-" + user))))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def clean_snapshots(directory, runtime_id):
|
|
135
|
+
"""Remove a settled owner's private model snapshots."""
|
|
136
|
+
if runtime_id is None:
|
|
137
|
+
return
|
|
138
|
+
if not isinstance(runtime_id, str) or not re.fullmatch(r"[0-9a-f]{32}", runtime_id):
|
|
139
|
+
raise ValueError("Invalid private runtime identity")
|
|
140
|
+
root = safe_path(directory, "runtimes/" + runtime_id + "/models")
|
|
141
|
+
if root.exists():
|
|
142
|
+
for path in root.rglob("*"):
|
|
143
|
+
safe_path(root, path.relative_to(root).as_posix())
|
|
144
|
+
if path.is_file():
|
|
145
|
+
path.chmod(0o600)
|
|
146
|
+
# Job retirement can precede release of a mapped file's Windows handle.
|
|
147
|
+
# Retry only transient sharing/permission errors on this checked private
|
|
148
|
+
# directory; persistent failure must retain runtime ownership.
|
|
149
|
+
deadline = time.monotonic() + .25
|
|
150
|
+
while True:
|
|
151
|
+
try:
|
|
152
|
+
shutil.rmtree(root)
|
|
153
|
+
break
|
|
154
|
+
except PermissionError as exc:
|
|
155
|
+
if getattr(exc, "winerror", None) not in {5, 32, 33} or time.monotonic() >= deadline:
|
|
156
|
+
raise
|
|
157
|
+
time.sleep(.005)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class RuntimeOwnership:
|
|
161
|
+
def __init__(self, directory=None):
|
|
162
|
+
self.directory = (Path(directory) if directory is not None else default_directory()).expanduser().absolute()
|
|
163
|
+
self.path = safe_path(self.directory, "owner.json")
|
|
164
|
+
self.token = None
|
|
165
|
+
self.failed_tree = None
|
|
166
|
+
self.launch_settled = True # In-process proof for failed guarded launches.
|
|
167
|
+
self.blocked = ""
|
|
168
|
+
|
|
169
|
+
def acquire(self):
|
|
170
|
+
if self.token:
|
|
171
|
+
value = self.read_owned()
|
|
172
|
+
if self.blocked or value["state"] == "blocked":
|
|
173
|
+
raise ValueError("Runtime cleanup is unresolved. Unload/recover it before another request.")
|
|
174
|
+
return
|
|
175
|
+
self.directory.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
176
|
+
token = uuid.uuid4().hex
|
|
177
|
+
value = {"schema_version": 1, "token": token, "owner_pid": os.getpid(),
|
|
178
|
+
"owner_identity": process_identity(os.getpid()), "process": None,
|
|
179
|
+
"state": "reserved", "updated_at": now(), "error": None}
|
|
180
|
+
try:
|
|
181
|
+
write_json(self.path, value, immutable=True)
|
|
182
|
+
except FileExistsError:
|
|
183
|
+
raise ValueError("Another CRIT runtime owns execution or left unresolved cleanup. Unload it or run crit serve --recover-runtime.") from None
|
|
184
|
+
self.token = token
|
|
185
|
+
|
|
186
|
+
def read_owned(self):
|
|
187
|
+
value = read_ownership(self.path)
|
|
188
|
+
if not self.token or value.get("token") != self.token:
|
|
189
|
+
raise ValueError("Runtime ownership changed")
|
|
190
|
+
return value
|
|
191
|
+
|
|
192
|
+
def update(self, **changes):
|
|
193
|
+
value = self.read_owned()
|
|
194
|
+
write_json(self.path, value | changes | {"updated_at": now()})
|
|
195
|
+
|
|
196
|
+
def bind(self, tree):
|
|
197
|
+
if not tree.guardian or process_identity(tree.guardian["pid"]) != tree.guardian["identity"]:
|
|
198
|
+
raise ValueError("Launch guardian identity must be verified before binding")
|
|
199
|
+
self.update(process=tree.guardian | {"job_name": tree.job_name}, state="running")
|
|
200
|
+
|
|
201
|
+
def block(self, error):
|
|
202
|
+
if self.token:
|
|
203
|
+
self.blocked = str(error) or "Unresolved runtime cleanup"
|
|
204
|
+
try:
|
|
205
|
+
self.update(state="blocked", error=self.blocked)
|
|
206
|
+
except (ValueError, OSError) as exc:
|
|
207
|
+
# The existing exclusive guard still blocks other clients. Keep
|
|
208
|
+
# local quarantine even if the diagnostic update cannot persist.
|
|
209
|
+
self.blocked += "; could not record cleanup failure: " + str(exc)
|
|
210
|
+
|
|
211
|
+
def release(self, settled=False):
|
|
212
|
+
if self.token:
|
|
213
|
+
value = self.read_owned()
|
|
214
|
+
if not settled and (self.blocked or value["state"] == "blocked"):
|
|
215
|
+
raise ValueError("Prove runtime cleanup before releasing ownership")
|
|
216
|
+
self.path.unlink()
|
|
217
|
+
self.token = None
|
|
218
|
+
self.blocked = ""
|
|
219
|
+
|
|
220
|
+
def recover(self):
|
|
221
|
+
"""Explicit dead-owner recovery; live owners and PID reuse block it."""
|
|
222
|
+
self.directory.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
223
|
+
with lease(self.directory):
|
|
224
|
+
if not self.path.exists():
|
|
225
|
+
return {"status": "idle"}
|
|
226
|
+
value = read_json(self.path)
|
|
227
|
+
keys(value, {"schema_version", "token", "owner_pid", "owner_identity", "process", "state", "updated_at", "error"}, {"runtime_id"})
|
|
228
|
+
if value["schema_version"] != 1 or value["state"] not in {"reserved", "running", "blocked"}:
|
|
229
|
+
raise ValueError("Invalid runtime ownership record")
|
|
230
|
+
number(value["owner_pid"], "runtime owner PID", 1, 2 ** 32, integer=True)
|
|
231
|
+
if not isinstance(value["token"], str) or not re.fullmatch(r"[0-9a-f]{32}", value["token"]):
|
|
232
|
+
raise ValueError("Invalid runtime ownership token")
|
|
233
|
+
if value["process"] is not None:
|
|
234
|
+
keys(value["process"], {"pid", "identity"}, {"job_name"})
|
|
235
|
+
number(value["process"]["pid"], "runtime process PID", 1, 2 ** 32, integer=True)
|
|
236
|
+
owner = process_identity(value["owner_pid"])
|
|
237
|
+
if owner is not None and (owner == "unknown" or owner == value["owner_identity"]):
|
|
238
|
+
raise ValueError("The runtime owner is still alive; use its Unload action")
|
|
239
|
+
child = value.get("process")
|
|
240
|
+
if child:
|
|
241
|
+
current = process_identity(child["pid"])
|
|
242
|
+
if current is not None and current != child["identity"]:
|
|
243
|
+
raise ValueError("Process identity changed; refusing ambiguous cleanup")
|
|
244
|
+
if os.name == "nt":
|
|
245
|
+
# Windows job ownership closes on owner death. Do not kill an
|
|
246
|
+
# arbitrary process outside that job or infer that it settled.
|
|
247
|
+
if current is not None or not windows_job_settled(child.get("job_name")):
|
|
248
|
+
raise ValueError("Owned Windows job has not finished closing; retry recovery after settlement")
|
|
249
|
+
else:
|
|
250
|
+
if child["identity"] in {None, "unknown"}:
|
|
251
|
+
raise ValueError("Process birth identity is unavailable; refusing ambiguous recovery")
|
|
252
|
+
boot_path = Path("/proc/sys/kernel/random/boot_id")
|
|
253
|
+
if boot_path.exists() and not child["identity"].startswith(boot_path.read_text().strip() + ":"):
|
|
254
|
+
raise ValueError("Runtime belongs to a different boot; verify and archive its stale guard explicitly")
|
|
255
|
+
if current is None and group_alive(child["pid"]):
|
|
256
|
+
raise ValueError("Live group has no matching birth-identified guardian; refusing ambiguous recovery")
|
|
257
|
+
if group_alive(child["pid"]):
|
|
258
|
+
os.killpg(child["pid"], signal.SIGKILL)
|
|
259
|
+
end = time.monotonic() + RETIREMENT_SECONDS
|
|
260
|
+
while group_alive(child["pid"]) and time.monotonic() < end:
|
|
261
|
+
time.sleep(.02)
|
|
262
|
+
if group_alive(child["pid"]):
|
|
263
|
+
raise ValueError("Owned process group has not settled")
|
|
264
|
+
clean_snapshots(self.directory, value.get("runtime_id"))
|
|
265
|
+
# Before bind, the guardian cannot launch engine code. After bind,
|
|
266
|
+
# recovery has checked the recorded private process group/job.
|
|
267
|
+
recovery = value | {"recovered_at": now(), "recovery": "dead owner; owned process settlement verified"}
|
|
268
|
+
write_json(self.directory / ("recovery-" + uuid.uuid4().hex + ".json"), recovery, immutable=True)
|
|
269
|
+
self.path.unlink()
|
|
270
|
+
return {"status": "recovered"}
|