ProCreations commited on
Commit
85d767b
·
verified ·
1 Parent(s): f95b39a

Upload scripts/inference_server.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. scripts/inference_server.py +254 -0
scripts/inference_server.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Persistent JSONL sidecar for the local BetterWright 2.0 alpha."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import bisect
8
+ import json
9
+ import sys
10
+ import time
11
+ from pathlib import Path
12
+
13
+ import torch
14
+ from transformers import AutoModel, AutoTokenizer
15
+
16
+ from tree_utils import references, structural_windows
17
+
18
+ MUST_KEEP = ("[active]", "[selected]", "[checked]", "dialog", "alert", "error", "textbox")
19
+
20
+
21
+ def ancestors(lines: list[str], index: int) -> set[int]:
22
+ keep = set()
23
+ indent = len(lines[index]) - len(lines[index].lstrip())
24
+ for i in range(index - 1, -1, -1):
25
+ candidate = len(lines[i]) - len(lines[i].lstrip())
26
+ if candidate < indent:
27
+ keep.add(i)
28
+ indent = candidate
29
+ if indent == 0:
30
+ break
31
+ return keep
32
+
33
+
34
+ def render_indices(tree: str, indices: set[int]) -> str:
35
+ lines = [line.rstrip() for line in tree.splitlines() if line.strip()]
36
+ indices = {index for index in indices if 0 <= index < len(lines)}
37
+ for i in list(indices):
38
+ indices.update(ancestors(lines, i))
39
+ out = []
40
+ previous = -1
41
+ for i in sorted(indices):
42
+ if i > previous + 1:
43
+ out.append("- text: … irrelevant subtree pruned …")
44
+ out.append(lines[i])
45
+ previous = i
46
+ return "\n".join(out)
47
+
48
+
49
+ def coarse_ref_indices(
50
+ tree: str,
51
+ windows,
52
+ scores: list[float],
53
+ max_ranked_windows: int,
54
+ ref_context_lines: int,
55
+ ) -> set[int]:
56
+ """Mirror the validated coarse-ref-context policy exactly."""
57
+ lines = [line.rstrip() for line in tree.splitlines() if line.strip()]
58
+ ranked_windows = sorted(
59
+ range(len(windows)), key=lambda index: scores[index], reverse=True
60
+ )[:max_ranked_windows]
61
+ candidate_lines = set()
62
+ for window_index in ranked_windows:
63
+ window = windows[window_index]
64
+ candidate_lines.update(
65
+ range(window.start_line, min(window.end_line, len(lines)))
66
+ )
67
+ chosen = {
68
+ index
69
+ for index in candidate_lines
70
+ if references(lines[index])
71
+ or any(term in lines[index].casefold() for term in MUST_KEEP)
72
+ }
73
+ for index in list(chosen):
74
+ base_indent = len(lines[index]) - len(lines[index].lstrip())
75
+ kept = 0
76
+ for child in range(index + 1, len(lines)):
77
+ child_indent = len(lines[child]) - len(lines[child].lstrip())
78
+ if child_indent <= base_indent:
79
+ break
80
+ if child in candidate_lines:
81
+ chosen.add(child)
82
+ kept += 1
83
+ if kept >= ref_context_lines:
84
+ break
85
+ return chosen
86
+
87
+
88
+ class Server:
89
+ def __init__(self, model_id: str, config_path: Path):
90
+ self.config = json.loads(config_path.read_text())
91
+ device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
92
+ self.device = device
93
+ self.tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
94
+ self.model = AutoModel.from_pretrained(
95
+ model_id, trust_remote_code=True, torch_dtype=torch.bfloat16
96
+ ).to(device).eval()
97
+
98
+ @torch.inference_mode()
99
+ def prune(self, query: str, tree: str, max_chars: int | None = None) -> dict:
100
+ started = time.perf_counter()
101
+ if not query.strip() or len(tree) < 1800:
102
+ return {"tree": tree, "fallback": True, "reason": "missing-query-or-small-tree", "savings": 0}
103
+ validated_max_chars = int(self.config.get("validated_max_chars", 10_000))
104
+ if max_chars is not None and max_chars < validated_max_chars:
105
+ return {
106
+ "tree": tree,
107
+ "fallback": True,
108
+ "reason": "requested-budget-below-validated-budget",
109
+ "savings": 0,
110
+ }
111
+ window_chars = int(self.config.get("window_chars", 3600))
112
+ strategy = self.config.get("strategy", "token-lines")
113
+ windows = structural_windows(tree, max_chars=window_chars, overlap_lines=4)
114
+ scores = []
115
+ confidences = []
116
+ total_lines = max((window.end_line for window in windows), default=0)
117
+ line_scores = [float("-inf")] * total_lines
118
+ for start in range(0, len(windows), 16):
119
+ part = windows[start : start + 16]
120
+ prefixes = [
121
+ f"[BETTERWRIGHT_TASK]\n{query}\n[ACCESSIBILITY_SUBTREE]\n"
122
+ for _ in part
123
+ ]
124
+ texts = [prefix + window.text for prefix, window in zip(prefixes, part)]
125
+ tokenizer_args = {
126
+ "padding": True,
127
+ "truncation": True,
128
+ "max_length": 2048,
129
+ "return_tensors": "pt",
130
+ }
131
+ if strategy != "coarse-ref-context":
132
+ tokenizer_args["return_offsets_mapping"] = True
133
+ batch = self.tokenizer(texts, **tokenizer_args)
134
+ offsets = batch.pop("offset_mapping", None)
135
+ output = self.model(**batch.to(self.device))
136
+ scores.extend(torch.sigmoid(output.logits).float().cpu().tolist())
137
+ confidences.extend(torch.sigmoid(output.uncertainty_logits).float().cpu().tolist())
138
+ if strategy == "coarse-ref-context":
139
+ continue
140
+ token_scores = torch.sigmoid(output.token_logits).float().cpu()
141
+ for batch_index, window in enumerate(part):
142
+ prefix_chars = len(prefixes[batch_index])
143
+ line_starts = []
144
+ offset = 0
145
+ for line in window.text.splitlines():
146
+ line_starts.append(offset)
147
+ offset += len(line) + 1
148
+ for token_index, (token_start, token_end) in enumerate(
149
+ offsets[batch_index].tolist()
150
+ ):
151
+ if token_end <= token_start or token_end <= prefix_chars:
152
+ continue
153
+ relative = max(token_start, prefix_chars) - prefix_chars
154
+ if relative >= len(window.text):
155
+ continue
156
+ local_line = bisect.bisect_right(line_starts, relative) - 1
157
+ if local_line < 0:
158
+ continue
159
+ global_line = window.start_line + local_line
160
+ if global_line < len(line_scores):
161
+ line_scores[global_line] = max(
162
+ line_scores[global_line],
163
+ float(token_scores[batch_index, token_index]),
164
+ )
165
+ if not scores:
166
+ return {"tree": tree, "fallback": True, "reason": "no-windows", "savings": 0}
167
+ threshold = self.config["relevance_threshold"]
168
+ if strategy == "coarse-ref-context":
169
+ chosen = coarse_ref_indices(
170
+ tree,
171
+ windows,
172
+ scores,
173
+ int(self.config["max_ranked_windows"]),
174
+ int(self.config["ref_context_lines"]),
175
+ )
176
+ else:
177
+ lines = [line.rstrip() for line in tree.splitlines() if line.strip()]
178
+ chosen = {
179
+ index
180
+ for index, line in enumerate(lines)
181
+ if any(term in line.casefold() for term in MUST_KEEP)
182
+ }
183
+ ranked = sorted(
184
+ (
185
+ index
186
+ for index, score in enumerate(line_scores)
187
+ if score != float("-inf")
188
+ ),
189
+ key=lambda index: line_scores[index],
190
+ reverse=True,
191
+ )
192
+ chosen.update(ranked[: int(self.config["max_ranked_lines"])])
193
+ peak_i = max(range(len(scores)), key=lambda i: scores[i])
194
+ if (
195
+ not chosen
196
+ or scores[peak_i] < threshold
197
+ or confidences[peak_i] < self.config["confidence_threshold"]
198
+ ):
199
+ return {"tree": tree, "fallback": True, "reason": "low-confidence", "savings": 0}
200
+ pruned = render_indices(tree, chosen)
201
+ savings = 1 - len(pruned) / max(1, len(tree))
202
+ if len(pruned) > validated_max_chars:
203
+ return {
204
+ "tree": tree,
205
+ "fallback": True,
206
+ "reason": "candidate-over-limit",
207
+ "savings": 0,
208
+ "candidate_chars": len(pruned),
209
+ "candidate_savings": savings,
210
+ }
211
+ if savings < 0.08:
212
+ return {
213
+ "tree": tree,
214
+ "fallback": True,
215
+ "reason": "insufficient-benefit",
216
+ "savings": 0,
217
+ "candidate_chars": len(pruned),
218
+ "candidate_savings": savings,
219
+ }
220
+ return {
221
+ "tree": pruned, "fallback": False, "reason": "model", "savings": savings,
222
+ "latency_ms": round((time.perf_counter() - started) * 1000, 2),
223
+ "peak_score": scores[peak_i], "peak_confidence": confidences[peak_i],
224
+ }
225
+
226
+
227
+ def main() -> None:
228
+ parser = argparse.ArgumentParser()
229
+ parser.add_argument("--model", default="ProCreations/betterwright-encoder-350m")
230
+ parser.add_argument("--config", type=Path, required=True)
231
+ args = parser.parse_args()
232
+ server = Server(args.model, args.config)
233
+ print(json.dumps({"ready": True, "model": args.model}), flush=True)
234
+ for line in sys.stdin:
235
+ request = {}
236
+ try:
237
+ request = json.loads(line)
238
+ raw_max_chars = request.get("max_chars")
239
+ max_chars = int(raw_max_chars) if raw_max_chars is not None else None
240
+ response = {
241
+ "id": request.get("id"),
242
+ **server.prune(
243
+ str(request.get("query", "")),
244
+ str(request.get("tree", "")),
245
+ max_chars=max_chars,
246
+ ),
247
+ }
248
+ except Exception as error:
249
+ response = {"id": request.get("id"), "fallback": True, "error": str(error)}
250
+ print(json.dumps(response, ensure_ascii=False), flush=True)
251
+
252
+
253
+ if __name__ == "__main__":
254
+ main()