tinyjev 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tinyjev/__init__.py +13 -0
- tinyjev/agent.py +172 -0
- tinyjev/backends/__init__.py +46 -0
- tinyjev/backends/mlx_backend.py +74 -0
- tinyjev/backends/torch_backend.py +71 -0
- tinyjev/cli.py +64 -0
- tinyjev/convert.py +243 -0
- tinyjev/families/__init__.py +55 -0
- tinyjev/families/marker.py +152 -0
- tinyjev/families/pointer.py +131 -0
- tinyjev/registry.py +13 -0
- tinyjev/serve.py +82 -0
- tinyjev-0.1.0.dist-info/METADATA +237 -0
- tinyjev-0.1.0.dist-info/RECORD +19 -0
- tinyjev-0.1.0.dist-info/WHEEL +5 -0
- tinyjev-0.1.0.dist-info/entry_points.txt +2 -0
- tinyjev-0.1.0.dist-info/licenses/LICENSE +21 -0
- tinyjev-0.1.0.dist-info/licenses/NOTICE +20 -0
- tinyjev-0.1.0.dist-info/top_level.txt +1 -0
tinyjev/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""tinyjev: run tiny Jev-style decision models locally, on any machine.
|
|
2
|
+
|
|
3
|
+
import tinyjev
|
|
4
|
+
agent = tinyjev.load("AnkitAI/tinyjev-kev-0.6b") # MLX on Apple Silicon, torch elsewhere
|
|
5
|
+
agent.predict({"state": "...", "questions": {...}}) # System One request shape
|
|
6
|
+
agent.predict({"states": [...]}) # NanoJev's native shape
|
|
7
|
+
"""
|
|
8
|
+
from .agent import Agent, load, normalize_request
|
|
9
|
+
from .convert import convert
|
|
10
|
+
from .registry import MODELS
|
|
11
|
+
|
|
12
|
+
__all__ = ["Agent", "load", "convert", "normalize_request", "MODELS", "__version__"]
|
|
13
|
+
__version__ = "0.1.0"
|
tinyjev/agent.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""One loaded model: a family (prompt + head) on a backend (MLX or torch)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from . import backends, families
|
|
10
|
+
from .families import softmax
|
|
11
|
+
|
|
12
|
+
SCHEMA_VERSION = "tinyjev-v1"
|
|
13
|
+
TYPE_ALIASES = {"noul": "boolean", "boolean": "boolean", "choice": "choice", "score": "score"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _resolve(model_path, subfolder: Optional[str] = None) -> Path:
|
|
17
|
+
from .registry import resolve
|
|
18
|
+
repo, sub = resolve(str(model_path))
|
|
19
|
+
sub = subfolder or sub
|
|
20
|
+
p = Path(repo).expanduser()
|
|
21
|
+
if p.exists():
|
|
22
|
+
root = p.resolve()
|
|
23
|
+
else:
|
|
24
|
+
from huggingface_hub import snapshot_download
|
|
25
|
+
patterns = [f"{sub}/*"] if sub else ["*"]
|
|
26
|
+
root = Path(snapshot_download(repo, allow_patterns=patterns))
|
|
27
|
+
return root / sub if sub else root
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def normalize_request(payload: Dict[str, Any]) -> List[dict]:
|
|
31
|
+
"""Accept NanoJev's {"states": [...]} or a System One {"state", "questions"} body.
|
|
32
|
+
|
|
33
|
+
Returns records: {"id", "state", "questions": [{"id","type","instructions","criteria"?}]}
|
|
34
|
+
with `noul` folded into `boolean`."""
|
|
35
|
+
if not isinstance(payload, dict):
|
|
36
|
+
raise ValueError("request must be a JSON object")
|
|
37
|
+
if "states" in payload:
|
|
38
|
+
if set(payload) != {"states"} or not isinstance(payload["states"], list) or not payload["states"]:
|
|
39
|
+
raise ValueError('native request must be exactly {"states": [...]} with at least one state')
|
|
40
|
+
raw = payload["states"]
|
|
41
|
+
elif "state" in payload and "questions" in payload:
|
|
42
|
+
raw = [{"id": payload.get("id") or "request", "state": payload["state"],
|
|
43
|
+
"questions": payload["questions"]}]
|
|
44
|
+
else:
|
|
45
|
+
raise ValueError('request needs either {"states": [...]} or {"state", "questions"}')
|
|
46
|
+
|
|
47
|
+
records, seen = [], set()
|
|
48
|
+
for s in raw:
|
|
49
|
+
if not isinstance(s, dict) or not {"id", "state", "questions"} <= set(s):
|
|
50
|
+
raise ValueError("each state needs id, state and questions")
|
|
51
|
+
sid = s["id"]
|
|
52
|
+
if not isinstance(sid, str) or not sid.strip() or sid in seen:
|
|
53
|
+
raise ValueError("state id must be a unique non-empty string")
|
|
54
|
+
seen.add(sid)
|
|
55
|
+
qs = s["questions"]
|
|
56
|
+
if not isinstance(qs, dict) or not qs:
|
|
57
|
+
raise ValueError(f"{sid}: questions must be a non-empty object")
|
|
58
|
+
out = []
|
|
59
|
+
for qid, q in qs.items():
|
|
60
|
+
if not isinstance(qid, str) or not qid.strip() or not isinstance(q, dict):
|
|
61
|
+
raise ValueError(f"{sid}: question ids must be non-empty strings mapping to objects")
|
|
62
|
+
typ = TYPE_ALIASES.get(q.get("type"))
|
|
63
|
+
if typ is None:
|
|
64
|
+
raise ValueError(f"{sid}:{qid}: unsupported question type {q.get('type')!r}")
|
|
65
|
+
item = {"id": qid, "type": typ, "instructions": q.get("instructions")}
|
|
66
|
+
if "criteria" in q and q["criteria"] is not None:
|
|
67
|
+
item["criteria"] = q["criteria"]
|
|
68
|
+
out.append(item)
|
|
69
|
+
records.append({"id": sid, "state": s["state"], "questions": out})
|
|
70
|
+
return records
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Agent:
|
|
74
|
+
def __init__(self, model_path, backend: Optional[str] = None, device: Optional[str] = None,
|
|
75
|
+
subfolder: Optional[str] = None, quantize: int = 0):
|
|
76
|
+
root = _resolve(model_path, subfolder)
|
|
77
|
+
manifest_path = root / "tinyjev.json"
|
|
78
|
+
if not manifest_path.exists():
|
|
79
|
+
raise FileNotFoundError(f"{root} is not a tinyjev checkpoint (no tinyjev.json); "
|
|
80
|
+
f"run `tinyjev convert` first")
|
|
81
|
+
self.manifest = json.loads(manifest_path.read_text())
|
|
82
|
+
if self.manifest.get("format") != "tinyjev-v2":
|
|
83
|
+
raise ValueError(f"{root}: layout {self.manifest.get('format')!r} is not tinyjev-v2; re-run `tinyjev convert`")
|
|
84
|
+
self.manifest["backbone_config"] = json.loads((root / "config.json").read_text())
|
|
85
|
+
self.root = root
|
|
86
|
+
self.family = families.make(self.manifest["family"], root, self.manifest)
|
|
87
|
+
name = backend or backends.default_backend()
|
|
88
|
+
kw = {"device": device} if (device and name == "torch") else {}
|
|
89
|
+
if name == "torch":
|
|
90
|
+
if quantize:
|
|
91
|
+
raise ValueError("quantize is only implemented on the mlx backend for now")
|
|
92
|
+
from .backends.torch_backend import Qwen3Backbone
|
|
93
|
+
self.backbone = Qwen3Backbone(self.manifest["backbone_config"], str(root / "model.safetensors"), **kw)
|
|
94
|
+
else:
|
|
95
|
+
self.backbone = backends.make(name, self.manifest["backbone_config"], str(root / "model.safetensors"),
|
|
96
|
+
quantize=quantize)
|
|
97
|
+
self.backend = self.backbone.name
|
|
98
|
+
self.quantize = int(quantize)
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def name(self) -> str:
|
|
102
|
+
return self.manifest.get("name", self.manifest["family"])
|
|
103
|
+
|
|
104
|
+
def _run(self, records: List[dict], temperature: float):
|
|
105
|
+
results, paths, elapsed = {}, 0, 0.0
|
|
106
|
+
for rec in records:
|
|
107
|
+
enc = self.family.encode(rec)
|
|
108
|
+
t = time.perf_counter()
|
|
109
|
+
hs = self.backbone.hidden_rows(enc.prefix, enc.rows, self.family.pad_token_id)
|
|
110
|
+
logits = self.family.logits(hs, enc)
|
|
111
|
+
elapsed += time.perf_counter() - t
|
|
112
|
+
paths += len(enc.rows)
|
|
113
|
+
for q in enc.questions:
|
|
114
|
+
z = logits[q["id"]]
|
|
115
|
+
probs = softmax(z / temperature).tolist()
|
|
116
|
+
results[f"{rec['id']}:{q['id']}"] = {
|
|
117
|
+
"state_id": rec["id"], "qid": q["id"], "type": q["type"], "keys": q["keys"],
|
|
118
|
+
"logits": [float(v) for v in z], "probabilities": probs,
|
|
119
|
+
"answer": self.family.answer(q, probs),
|
|
120
|
+
"path_token_counts": [len(enc.prefix) + len(enc.rows[i]) for i in
|
|
121
|
+
(q["rows"] if "rows" in q else [q["row"]])]}
|
|
122
|
+
return results, paths, elapsed * 1000.0
|
|
123
|
+
|
|
124
|
+
def logits(self, payload: Dict[str, Any]) -> Dict[str, dict]:
|
|
125
|
+
"""Raw per-candidate logits/probabilities keyed by `<state id>:<question id>`."""
|
|
126
|
+
results, _, _ = self._run(normalize_request(payload), 1.0)
|
|
127
|
+
return {k: {kk: v[kk] for kk in ("type", "logits", "probabilities", "answer", "path_token_counts")}
|
|
128
|
+
| {"candidate_ids": v["keys"]} for k, v in results.items()}
|
|
129
|
+
|
|
130
|
+
def predict(self, payload: Dict[str, Any], temperature: float = 1.0) -> Dict[str, Any]:
|
|
131
|
+
if not isinstance(temperature, (int, float)) or isinstance(temperature, bool) \
|
|
132
|
+
or temperature <= 0 or temperature != temperature:
|
|
133
|
+
raise ValueError("temperature must be a finite positive number")
|
|
134
|
+
records = normalize_request(payload)
|
|
135
|
+
results, paths, ms = self._run(records, float(temperature))
|
|
136
|
+
states = {r["id"]: {"id": r["id"], "answers": {}} for r in records}
|
|
137
|
+
for v in results.values():
|
|
138
|
+
states[v["state_id"]]["answers"][v["qid"]] = v["answer"]
|
|
139
|
+
return {
|
|
140
|
+
"schema_version": SCHEMA_VERSION,
|
|
141
|
+
"model": {"name": self.name, "family": self.manifest["family"], "backend": self.backend,
|
|
142
|
+
"quantize_bits": self.quantize or None,
|
|
143
|
+
"directory": str(self.root), "upstream": self.manifest.get("upstream", {})},
|
|
144
|
+
"temperature": {"value": float(temperature)},
|
|
145
|
+
"execution": {"states": len(records), "questions": len(results), "candidate_paths": paths,
|
|
146
|
+
"autoregressive_decode_steps": 0, "model_ms": round(ms, 2)},
|
|
147
|
+
"states": list(states.values()),
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
def systemone(self, body: Dict[str, Any]) -> Dict[str, Any]:
|
|
151
|
+
"""TypeSafe System One response for a System One request."""
|
|
152
|
+
res = self.predict(body)
|
|
153
|
+
answers = {}
|
|
154
|
+
for qid, a in res["states"][0]["answers"].items():
|
|
155
|
+
if a["type"] in ("boolean", "noul"):
|
|
156
|
+
answers[qid] = {"type": "noul", "noul": round(float(a["p_true"]), 4)}
|
|
157
|
+
elif a["type"] == "choice":
|
|
158
|
+
answers[qid] = {"type": "choice", "choice": a["choice"],
|
|
159
|
+
"confidence": a.get("confidence"), "probabilities": a["probabilities"]}
|
|
160
|
+
else:
|
|
161
|
+
answers[qid] = {"type": "score", "score": round(float(a["score"]), 4),
|
|
162
|
+
"legend": a.get("legend"), "probabilities": a["probabilities"],
|
|
163
|
+
"confidence": a.get("confidence")}
|
|
164
|
+
return {"model": body.get("model") or self.name, "answers": answers,
|
|
165
|
+
"latency_ms": res["execution"]["model_ms"]}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def load(model_path, backend: Optional[str] = None, device: Optional[str] = None,
|
|
169
|
+
subfolder: Optional[str] = None, quantize: int = 0) -> Agent:
|
|
170
|
+
"""`load("kev-0.6b")` (alias), `load("/path/to/dir")`, or `load("org/repo", subfolder="name")`.
|
|
171
|
+
quantize=8 or 4 quantizes the backbone's Linear layers at load time (mlx backend)."""
|
|
172
|
+
return Agent(model_path, backend=backend, device=device, subfolder=subfolder, quantize=quantize)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Backbone backends. Each exposes `Qwen3Backbone(config, weights_path)` with
|
|
2
|
+
|
|
3
|
+
hidden_rows(prefix: list[int], suffixes: list[list[int]], pad_token: int) -> list[np.ndarray]
|
|
4
|
+
|
|
5
|
+
returning the fp32 hidden states [len(prefix)+len(suffix_i), d] of every row prefix+suffix_i.
|
|
6
|
+
Attention is causal, so a shared prefix can be run once and its KV broadcast to every suffix.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import platform
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def available() -> list[str]:
|
|
14
|
+
out = []
|
|
15
|
+
try:
|
|
16
|
+
import mlx.core # noqa: F401
|
|
17
|
+
out.append("mlx")
|
|
18
|
+
except Exception:
|
|
19
|
+
pass
|
|
20
|
+
try:
|
|
21
|
+
import torch # noqa: F401
|
|
22
|
+
out.append("torch")
|
|
23
|
+
except Exception:
|
|
24
|
+
pass
|
|
25
|
+
return out
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def default_backend() -> str:
|
|
29
|
+
have = available()
|
|
30
|
+
if "mlx" in have and platform.machine() == "arm64" and platform.system() == "Darwin":
|
|
31
|
+
return "mlx"
|
|
32
|
+
if have:
|
|
33
|
+
return have[0]
|
|
34
|
+
raise RuntimeError("install either mlx (Apple Silicon) or torch")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def make(name: str, config: dict, weights_path: str, prefix_min_tokens: int = 96, quantize: int = 0):
|
|
38
|
+
if name == "mlx":
|
|
39
|
+
from .mlx_backend import Qwen3Backbone
|
|
40
|
+
return Qwen3Backbone(config, weights_path, prefix_min_tokens=prefix_min_tokens, quantize=quantize)
|
|
41
|
+
if name == "torch":
|
|
42
|
+
if quantize:
|
|
43
|
+
raise ValueError("quantize is only implemented on the mlx backend for now")
|
|
44
|
+
from .torch_backend import Qwen3Backbone
|
|
45
|
+
return Qwen3Backbone(config, weights_path, prefix_min_tokens=prefix_min_tokens)
|
|
46
|
+
raise ValueError(f"unknown backend {name!r}; choose mlx or torch")
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Qwen3 backbone on MLX with shared-prefix KV reuse."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import List, Sequence
|
|
5
|
+
|
|
6
|
+
import mlx.core as mx
|
|
7
|
+
import mlx.nn as nn
|
|
8
|
+
import numpy as np
|
|
9
|
+
from mlx_lm.models.qwen3 import ModelArgs, Qwen3Model
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def qwen3_args(cfg: dict) -> ModelArgs:
|
|
13
|
+
rope = cfg.get("rope_parameters") or {}
|
|
14
|
+
return ModelArgs(
|
|
15
|
+
model_type="qwen3", hidden_size=cfg["hidden_size"],
|
|
16
|
+
num_hidden_layers=cfg["num_hidden_layers"], intermediate_size=cfg["intermediate_size"],
|
|
17
|
+
num_attention_heads=cfg["num_attention_heads"], rms_norm_eps=cfg["rms_norm_eps"],
|
|
18
|
+
vocab_size=cfg["vocab_size"], num_key_value_heads=cfg["num_key_value_heads"],
|
|
19
|
+
head_dim=cfg["head_dim"], max_position_embeddings=cfg.get("max_position_embeddings", 40960),
|
|
20
|
+
rope_theta=rope.get("rope_theta", cfg.get("rope_theta", 1000000)),
|
|
21
|
+
tie_word_embeddings=cfg.get("tie_word_embeddings", True))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Qwen3Backbone:
|
|
25
|
+
name = "mlx"
|
|
26
|
+
|
|
27
|
+
def __init__(self, config: dict, weights_path: str, prefix_min_tokens: int = 96,
|
|
28
|
+
quantize: int = 0, group_size: int = 64):
|
|
29
|
+
self.model = Qwen3Model(qwen3_args(config))
|
|
30
|
+
weights = mx.load(weights_path) # model.safetensors: standard Qwen3Model keys
|
|
31
|
+
self.model.load_weights(list(weights.items()))
|
|
32
|
+
if quantize:
|
|
33
|
+
# Backbone Linear layers only (the decision head runs fp32 in numpy). Embeddings stay
|
|
34
|
+
# unquantized: they are gathered, not multiplied, and small models lose most at 4-bit.
|
|
35
|
+
nn.quantize(self.model, group_size=group_size, bits=int(quantize),
|
|
36
|
+
class_predicate=lambda _p, m: isinstance(m, nn.Linear))
|
|
37
|
+
self.quantize = int(quantize)
|
|
38
|
+
self.model.eval()
|
|
39
|
+
mx.eval(self.model.parameters())
|
|
40
|
+
self.prefix_min_tokens = prefix_min_tokens
|
|
41
|
+
|
|
42
|
+
def _rows_plain(self, rows: Sequence[Sequence[int]], pad: int) -> List[np.ndarray]:
|
|
43
|
+
lengths = [len(r) for r in rows]
|
|
44
|
+
width = max(lengths)
|
|
45
|
+
tokens = mx.array([list(r) + [pad] * (width - len(r)) for r in rows])
|
|
46
|
+
h = self.model(tokens).astype(mx.float32)
|
|
47
|
+
mx.eval(h)
|
|
48
|
+
arr = np.array(h)
|
|
49
|
+
return [arr[i, :n] for i, n in enumerate(lengths)]
|
|
50
|
+
|
|
51
|
+
def _rows_shared(self, prefix: Sequence[int], suffixes: Sequence[Sequence[int]], pad: int):
|
|
52
|
+
from mlx_lm.models.cache import KVCache
|
|
53
|
+
cache = [KVCache() for _ in self.model.layers]
|
|
54
|
+
h_prefix = self.model(mx.array([list(prefix)]), cache=cache).astype(mx.float32)
|
|
55
|
+
k = len(suffixes)
|
|
56
|
+
shared = []
|
|
57
|
+
for c in cache:
|
|
58
|
+
keys, values = c.state
|
|
59
|
+
bc = KVCache()
|
|
60
|
+
bc.keys, bc.values = mx.repeat(keys, k, axis=0), mx.repeat(values, k, axis=0)
|
|
61
|
+
bc.offset = keys.shape[2]
|
|
62
|
+
shared.append(bc)
|
|
63
|
+
lengths = [len(s) for s in suffixes]
|
|
64
|
+
width = max(lengths)
|
|
65
|
+
tokens = mx.array([list(s) + [pad] * (width - len(s)) for s in suffixes])
|
|
66
|
+
h = self.model(tokens, cache=shared).astype(mx.float32)
|
|
67
|
+
mx.eval(h_prefix, h)
|
|
68
|
+
hp, hs = np.array(h_prefix)[0], np.array(h)
|
|
69
|
+
return [np.concatenate([hp, hs[i, :n]], axis=0) for i, n in enumerate(lengths)]
|
|
70
|
+
|
|
71
|
+
def hidden_rows(self, prefix: Sequence[int], suffixes: Sequence[Sequence[int]], pad: int):
|
|
72
|
+
if len(suffixes) >= 2 and len(prefix) >= self.prefix_min_tokens:
|
|
73
|
+
return self._rows_shared(prefix, suffixes, pad)
|
|
74
|
+
return self._rows_plain([list(prefix) + list(s) for s in suffixes], pad)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Qwen3 backbone on PyTorch (CPU, CUDA or MPS) with shared-prefix KV reuse."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import List, Sequence
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Qwen3Backbone:
|
|
10
|
+
name = "torch"
|
|
11
|
+
|
|
12
|
+
def __init__(self, config: dict, weights_path: str, prefix_min_tokens: int = 96, device=None):
|
|
13
|
+
"""`weights_path` is <root>/model.safetensors; the root is a standard transformers model dir."""
|
|
14
|
+
import torch
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from transformers import AutoConfig, AutoModel
|
|
17
|
+
|
|
18
|
+
self.torch = torch
|
|
19
|
+
self.device = torch.device(device or ("cuda" if torch.cuda.is_available() else
|
|
20
|
+
"mps" if torch.backends.mps.is_available() else "cpu"))
|
|
21
|
+
root = str(Path(weights_path).parent)
|
|
22
|
+
cfg = AutoConfig.from_pretrained(root)
|
|
23
|
+
# transformers 4.x ignores `rope_parameters`; the converter also writes rope_theta at the top
|
|
24
|
+
# level, but force it here too in case the config came from elsewhere
|
|
25
|
+
rope = config.get("rope_parameters") or {}
|
|
26
|
+
cfg.rope_theta = rope.get("rope_theta", config.get("rope_theta", getattr(cfg, "rope_theta", None)))
|
|
27
|
+
cfg.use_cache = True
|
|
28
|
+
dtype = torch.float16 if self.device.type != "cpu" else torch.float32
|
|
29
|
+
self.model = AutoModel.from_pretrained(root, config=cfg, dtype=dtype, attn_implementation="sdpa")
|
|
30
|
+
self.model.to(self.device).eval()
|
|
31
|
+
self.prefix_min_tokens = prefix_min_tokens
|
|
32
|
+
|
|
33
|
+
def _pad(self, rows, pad):
|
|
34
|
+
torch = self.torch
|
|
35
|
+
lengths = [len(r) for r in rows]
|
|
36
|
+
width = max(lengths)
|
|
37
|
+
ids = torch.full((len(rows), width), pad, dtype=torch.long)
|
|
38
|
+
att = torch.zeros((len(rows), width), dtype=torch.long)
|
|
39
|
+
for i, r in enumerate(rows):
|
|
40
|
+
ids[i, :len(r)] = torch.tensor(r)
|
|
41
|
+
att[i, :len(r)] = 1
|
|
42
|
+
return ids.to(self.device), att.to(self.device), lengths
|
|
43
|
+
|
|
44
|
+
def _rows_plain(self, rows, pad) -> List[np.ndarray]:
|
|
45
|
+
torch = self.torch
|
|
46
|
+
ids, att, lengths = self._pad(rows, pad)
|
|
47
|
+
with torch.inference_mode():
|
|
48
|
+
h = self.model(input_ids=ids, attention_mask=att, use_cache=False).last_hidden_state.float().cpu().numpy()
|
|
49
|
+
return [h[i, :n] for i, n in enumerate(lengths)]
|
|
50
|
+
|
|
51
|
+
def _rows_shared(self, prefix, suffixes, pad):
|
|
52
|
+
torch = self.torch
|
|
53
|
+
from transformers import DynamicCache
|
|
54
|
+
k = len(suffixes)
|
|
55
|
+
with torch.inference_mode():
|
|
56
|
+
p = torch.tensor([list(prefix)], device=self.device)
|
|
57
|
+
out = self.model(input_ids=p, past_key_values=DynamicCache(), use_cache=True)
|
|
58
|
+
hp = out.last_hidden_state[0].float().cpu().numpy()
|
|
59
|
+
cache = out.past_key_values
|
|
60
|
+
cache.reorder_cache(torch.zeros(k, dtype=torch.long, device=self.device))
|
|
61
|
+
ids, att, lengths = self._pad(suffixes, pad)
|
|
62
|
+
full_att = torch.cat([torch.ones((k, len(prefix)), dtype=torch.long, device=self.device), att], 1)
|
|
63
|
+
pos = torch.arange(len(prefix), len(prefix) + ids.shape[1], device=self.device)[None].expand(k, -1)
|
|
64
|
+
h = self.model(input_ids=ids, attention_mask=full_att, position_ids=pos,
|
|
65
|
+
past_key_values=cache, use_cache=True).last_hidden_state.float().cpu().numpy()
|
|
66
|
+
return [np.concatenate([hp, h[i, :n]], axis=0) for i, n in enumerate(lengths)]
|
|
67
|
+
|
|
68
|
+
def hidden_rows(self, prefix, suffixes, pad):
|
|
69
|
+
if len(suffixes) >= 2 and len(prefix) >= self.prefix_min_tokens:
|
|
70
|
+
return self._rows_shared(prefix, suffixes, pad)
|
|
71
|
+
return self._rows_plain([list(prefix) + list(s) for s in suffixes], pad)
|
tinyjev/cli.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""tinyjev command line."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main(argv=None) -> int:
|
|
10
|
+
parser = argparse.ArgumentParser(prog="tinyjev", description="tiny Jev-style decision models, locally")
|
|
11
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
12
|
+
|
|
13
|
+
c = sub.add_parser("convert", help="convert an upstream checkpoint into the tinyjev layout")
|
|
14
|
+
c.add_argument("family", choices=["nanojev", "kev"])
|
|
15
|
+
c.add_argument("dest")
|
|
16
|
+
c.add_argument("--source", help="nanojev: upstream checkpoint dir")
|
|
17
|
+
c.add_argument("--adapter", help="kev: adapter dir (adapter_model.safetensors + head.pt)")
|
|
18
|
+
c.add_argument("--base", help="kev: Qwen3 base model dir")
|
|
19
|
+
c.add_argument("--name", default=None)
|
|
20
|
+
c.add_argument("--dtype", default="float16", choices=["float16", "float32"])
|
|
21
|
+
|
|
22
|
+
def model_args(p):
|
|
23
|
+
p.add_argument("--backend", choices=["mlx", "torch"], default=None)
|
|
24
|
+
p.add_argument("--device", default=None, help="torch backend: cpu, mps or cuda")
|
|
25
|
+
p.add_argument("--quantize", type=int, default=0, choices=[0, 4, 8], help="mlx: quantize backbone Linear layers to 4 or 8 bits at load")
|
|
26
|
+
|
|
27
|
+
s = sub.add_parser("serve", help="serve a checkpoint over HTTP (/predict, /v1/systemone)")
|
|
28
|
+
s.add_argument("model"); s.add_argument("--host", default="127.0.0.1")
|
|
29
|
+
s.add_argument("--port", type=int, default=8077); model_args(s)
|
|
30
|
+
|
|
31
|
+
a = sub.add_parser("ask", help="answer one request (System One or native shape) from a file or stdin")
|
|
32
|
+
a.add_argument("model"); a.add_argument("request", nargs="?", default="-"); model_args(a)
|
|
33
|
+
|
|
34
|
+
ls = sub.add_parser("models", help="list known checkpoints")
|
|
35
|
+
|
|
36
|
+
args = parser.parse_args(argv)
|
|
37
|
+
|
|
38
|
+
if args.command == "models":
|
|
39
|
+
from .registry import MODELS
|
|
40
|
+
for k, v in MODELS.items():
|
|
41
|
+
print(f"{k:<13} {v['params']:<5} {v['family']:<8} {v['repo']:<22} {v['what']}")
|
|
42
|
+
print("\nOther projects' models: convert them locally, e.g.\n"
|
|
43
|
+
" tinyjev convert nanojev <dest> --source <C-Tianyu/NanoJev checkout>\n"
|
|
44
|
+
" tinyjev convert kev <dest> --adapter <kev run or hub dir> --base <Qwen3 base dir>")
|
|
45
|
+
return 0
|
|
46
|
+
if args.command == "convert":
|
|
47
|
+
from .convert import convert
|
|
48
|
+
kw = {"source": args.source, "adapter": args.adapter, "base": args.base, "name": args.name or args.family}
|
|
49
|
+
convert(args.family, args.dest, dtype=args.dtype, **{k: v for k, v in kw.items() if v})
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
from . import load
|
|
53
|
+
if args.command == "serve":
|
|
54
|
+
from .serve import serve
|
|
55
|
+
serve(load(args.model, backend=args.backend, device=args.device, quantize=args.quantize), host=args.host, port=args.port)
|
|
56
|
+
return 0
|
|
57
|
+
payload = json.load(sys.stdin) if args.request == "-" else json.load(open(args.request))
|
|
58
|
+
agent = load(args.model, backend=args.backend, device=args.device, quantize=args.quantize)
|
|
59
|
+
print(json.dumps(agent.predict(payload), indent=2, ensure_ascii=False))
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
raise SystemExit(main())
|