@gobing-ai/ts-laya-mlx 0.5.1
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.
- package/LICENSE +176 -0
- package/NOTICE +31 -0
- package/README.md +145 -0
- package/dist/driver.d.ts +25 -0
- package/dist/driver.d.ts.map +1 -0
- package/dist/driver.js +150 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/worker-client.d.ts +106 -0
- package/dist/worker-client.d.ts.map +1 -0
- package/dist/worker-client.js +446 -0
- package/package.json +64 -0
- package/src/driver.ts +184 -0
- package/src/index.ts +14 -0
- package/src/worker-client.ts +592 -0
- package/worker/README.md +45 -0
- package/worker/laya_worker.py +208 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Long-lived JSON-lines worker around the installed ``laya_mlx`` runtime (ADR-027).
|
|
3
|
+
|
|
4
|
+
Lifecycle: resolve options (CLI args, else the first stdin line) -> import the runtime
|
|
5
|
+
module -> construct one Agent -> emit the handshake -> serve one JSON object per line
|
|
6
|
+
until EOF. The worker owns lifecycle and framing only: tokenization, batching,
|
|
7
|
+
calibration, and answer construction are the runtime's (``Agent.predict``). A request
|
|
8
|
+
carrying more questions than the configured batch size is chunked inside the runtime;
|
|
9
|
+
every answer is returned keyed by its question name.
|
|
10
|
+
|
|
11
|
+
Line protocol (docs/design/laya-local-decision-backend.md):
|
|
12
|
+
|
|
13
|
+
handshake {"ready": true, "model": "...", "revision": "...|null", "maxLen": N}
|
|
14
|
+
request {"id": "...", "state": "...", "questions": {"<qid>": {...}}}
|
|
15
|
+
success {"id": "...", "ok": true, "result": <runtime predict payload>}
|
|
16
|
+
failure {"id": "...", "ok": false, "error": {"kind": "config|request|backend", "message": "..."}}
|
|
17
|
+
startup {"ready": false, "error": {"kind": "config", "message": "..."}} then exit 1
|
|
18
|
+
|
|
19
|
+
Error kinds: ``config`` — construction/host problems (model resolution, dtype, batch
|
|
20
|
+
size); ``request`` — caller problems (malformed line, malformed question); ``backend``
|
|
21
|
+
— model problems (non-finite outputs, unexpected runtime failure).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import importlib
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import sys
|
|
31
|
+
|
|
32
|
+
DEFAULT_MODULE = "laya_mlx"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse_args(argv):
|
|
36
|
+
parser = argparse.ArgumentParser(
|
|
37
|
+
prog="laya_worker.py",
|
|
38
|
+
description="JSON-lines worker executing one laya_mlx.Agent for its lifetime.",
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument("--model", default=None, help="Hugging Face model id")
|
|
41
|
+
parser.add_argument("--model-path", default=None, help="local checkpoint directory (wins over --model)")
|
|
42
|
+
parser.add_argument("--revision", default=None, help="checkpoint revision")
|
|
43
|
+
# dtype/batch-size are validated by the runtime's Agent constructor so a bad
|
|
44
|
+
# value surfaces as the structured config handshake, not an argparse exit.
|
|
45
|
+
parser.add_argument("--dtype", default="float16", help="weight precision")
|
|
46
|
+
parser.add_argument("--batch-size", type=int, default=16, help="questions per forward pass")
|
|
47
|
+
parser.add_argument("--cache-dir", default=None, help="artifact cache root (exported as HF_HUB_CACHE)")
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"--module",
|
|
50
|
+
default=DEFAULT_MODULE,
|
|
51
|
+
help="runtime module to import (default: laya_mlx); a non-default module is "
|
|
52
|
+
"resolved from the working directory so tests can inject a stub",
|
|
53
|
+
)
|
|
54
|
+
return parser.parse_args(argv)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def merge_first_line_options(args, line):
|
|
58
|
+
"""Apply construction options from a first line of ``{"options": {...}}``.
|
|
59
|
+
|
|
60
|
+
Returns ``(args, applied)``. ``applied`` is False when the line is absent,
|
|
61
|
+
unparsable, or an ordinary request — in every case the caller must still
|
|
62
|
+
serve the line so no request is ever dropped.
|
|
63
|
+
"""
|
|
64
|
+
if line is None or not line.strip():
|
|
65
|
+
return args, False
|
|
66
|
+
try:
|
|
67
|
+
parsed = json.loads(line)
|
|
68
|
+
except json.JSONDecodeError:
|
|
69
|
+
return args, False
|
|
70
|
+
if not isinstance(parsed, dict) or not isinstance(parsed.get("options"), dict):
|
|
71
|
+
return args, False
|
|
72
|
+
supplied = parsed["options"]
|
|
73
|
+
if "model" in supplied:
|
|
74
|
+
args.model = supplied["model"]
|
|
75
|
+
if "modelPath" in supplied:
|
|
76
|
+
args.model_path = supplied["modelPath"]
|
|
77
|
+
if "revision" in supplied:
|
|
78
|
+
args.revision = supplied["revision"]
|
|
79
|
+
if "dtype" in supplied:
|
|
80
|
+
args.dtype = supplied["dtype"]
|
|
81
|
+
if "batchSize" in supplied:
|
|
82
|
+
args.batch_size = supplied["batchSize"]
|
|
83
|
+
if "cacheDir" in supplied:
|
|
84
|
+
args.cache_dir = supplied["cacheDir"]
|
|
85
|
+
return args, True
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def emit(payload):
|
|
89
|
+
sys.stdout.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
|
90
|
+
sys.stdout.flush()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def startup_failure(message):
|
|
94
|
+
emit({"ready": False, "error": {"kind": "config", "message": message}})
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def build_agent(args):
|
|
98
|
+
module_name = args.module
|
|
99
|
+
if module_name != DEFAULT_MODULE:
|
|
100
|
+
# Test seam: resolve an injected module from the working directory.
|
|
101
|
+
sys.path.insert(0, os.getcwd())
|
|
102
|
+
|
|
103
|
+
# huggingface_hub reads cache locations at import time, so the cache root must
|
|
104
|
+
# be exported before the import below; it is config plumbing, not model logic.
|
|
105
|
+
if args.cache_dir:
|
|
106
|
+
os.environ["HF_HUB_CACHE"] = args.cache_dir
|
|
107
|
+
|
|
108
|
+
module = importlib.import_module(module_name)
|
|
109
|
+
agent_cls = getattr(module, "Agent")
|
|
110
|
+
|
|
111
|
+
model = args.model_path or args.model
|
|
112
|
+
kwargs = {}
|
|
113
|
+
if model is not None:
|
|
114
|
+
kwargs["model_id_or_path"] = model
|
|
115
|
+
if args.revision is not None:
|
|
116
|
+
kwargs["revision"] = args.revision
|
|
117
|
+
kwargs["dtype"] = args.dtype
|
|
118
|
+
kwargs["batch_size"] = args.batch_size
|
|
119
|
+
kwargs["token"] = os.environ.get("HF_TOKEN")
|
|
120
|
+
return agent_cls(**kwargs)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def request_error(request_id, message):
|
|
124
|
+
return {"id": request_id, "ok": False, "error": {"kind": "request", "message": message}}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def classify(exc):
|
|
128
|
+
"""Map a runtime exception onto the protocol's error taxonomy."""
|
|
129
|
+
if isinstance(exc, FloatingPointError):
|
|
130
|
+
return "backend"
|
|
131
|
+
if isinstance(exc, ValueError):
|
|
132
|
+
return "request"
|
|
133
|
+
return "backend"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def handle_request(agent, line):
|
|
137
|
+
try:
|
|
138
|
+
request = json.loads(line)
|
|
139
|
+
except json.JSONDecodeError as exc:
|
|
140
|
+
return request_error("", f"malformed JSON line: {exc}")
|
|
141
|
+
if not isinstance(request, dict):
|
|
142
|
+
return request_error("", "request line must be a JSON object")
|
|
143
|
+
if "id" not in request:
|
|
144
|
+
return request_error("", "request is missing id")
|
|
145
|
+
request_id = request["id"]
|
|
146
|
+
if isinstance(request_id, bool) or not isinstance(request_id, (str, int)):
|
|
147
|
+
return request_error("", "request id must be a string or integer")
|
|
148
|
+
|
|
149
|
+
state = request.get("state")
|
|
150
|
+
if not isinstance(state, str):
|
|
151
|
+
return request_error(request_id, "request state must be a string")
|
|
152
|
+
questions = request.get("questions")
|
|
153
|
+
if not isinstance(questions, dict) or not questions:
|
|
154
|
+
return request_error(request_id, "request questions must be a nonempty object")
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
result = agent.predict(state, questions)
|
|
158
|
+
except Exception as exc: # classified and reported, never fatal to the worker
|
|
159
|
+
return {"id": request_id, "ok": False, "error": {"kind": classify(exc), "message": str(exc)}}
|
|
160
|
+
return {"id": request_id, "ok": True, "result": result}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def main(argv=None):
|
|
164
|
+
argv = sys.argv[1:] if argv is None else list(argv)
|
|
165
|
+
args = parse_args(argv)
|
|
166
|
+
|
|
167
|
+
# Without CLI args the construction options may arrive on the first stdin line;
|
|
168
|
+
# a first line that is not an options line is the first request and must be served.
|
|
169
|
+
pending_line = None
|
|
170
|
+
if not argv:
|
|
171
|
+
first_line = sys.stdin.readline().rstrip("\n")
|
|
172
|
+
args, applied = merge_first_line_options(args, first_line)
|
|
173
|
+
if not applied:
|
|
174
|
+
pending_line = first_line
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
agent = build_agent(args)
|
|
178
|
+
except Exception as exc: # every construction failure is config
|
|
179
|
+
startup_failure(str(exc))
|
|
180
|
+
return 1
|
|
181
|
+
|
|
182
|
+
emit(
|
|
183
|
+
{
|
|
184
|
+
"ready": True,
|
|
185
|
+
"model": str(agent.model_id),
|
|
186
|
+
"revision": agent.revision,
|
|
187
|
+
"maxLen": int(agent.cfg.get("max_len", 512)),
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def input_lines():
|
|
192
|
+
if pending_line is not None:
|
|
193
|
+
yield pending_line
|
|
194
|
+
for line in sys.stdin:
|
|
195
|
+
yield line.rstrip("\n")
|
|
196
|
+
|
|
197
|
+
for line in input_lines():
|
|
198
|
+
if not line.strip():
|
|
199
|
+
continue
|
|
200
|
+
try:
|
|
201
|
+
emit(handle_request(agent, line))
|
|
202
|
+
except BrokenPipeError:
|
|
203
|
+
return 1
|
|
204
|
+
return 0
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
if __name__ == "__main__":
|
|
208
|
+
sys.exit(main())
|