simit 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.
- simit/__init__.py +39 -0
- simit/api.py +258 -0
- simit/backends/__init__.py +112 -0
- simit/backends/bagel.py +359 -0
- simit/backends/base.py +114 -0
- simit/backends/hf.py +336 -0
- simit/backends/imagegen.py +136 -0
- simit/backends/lance.py +338 -0
- simit/backends/mot/engine.py +844 -0
- simit/backends/mot/kernels.py +130 -0
- simit/backends/mot/modeling.py +514 -0
- simit/backends/mot/wan_vae.py +872 -0
- simit/backends/vllm.py +204 -0
- simit/config.py +112 -0
- simit/metrics.py +113 -0
- simit/pipeline/core.py +586 -0
- simit/pipeline/prompts.py +167 -0
- simit/skills/__init__.py +6 -0
- simit/skills/assets/MERMAID_LICENSE +21 -0
- simit/skills/assets/mermaid.min.js +3587 -0
- simit/skills/base.py +172 -0
- simit/skills/builtin.py +614 -0
- simit/skills/helpers.py +197 -0
- simit/skills/pool.py +262 -0
- simit/skills/prompts.py +2374 -0
- simit/skills/renderers.py +1292 -0
- simit/skills/routing.py +89 -0
- simit/skills/sandbox.py +188 -0
- simit/skills/web.py +253 -0
- simit/tune.py +323 -0
- simit/types.py +96 -0
- simit/utils.py +67 -0
- simit-0.1.0.dist-info/METADATA +388 -0
- simit-0.1.0.dist-info/RECORD +38 -0
- simit-0.1.0.dist-info/WHEEL +5 -0
- simit-0.1.0.dist-info/licenses/LICENSE +202 -0
- simit-0.1.0.dist-info/licenses/src/simit/skills/assets/MERMAID_LICENSE +21 -0
- simit-0.1.0.dist-info/top_level.txt +1 -0
simit/skills/helpers.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Deterministic helpers shared across renderers: geometry notation helpers
|
|
2
|
+
(injected into the model's sandbox) and automatic text-overlap avoidance
|
|
3
|
+
(used both in-process by the deterministic renderers and injected into the
|
|
4
|
+
model's sandbox)."""
|
|
5
|
+
|
|
6
|
+
# Pre-defined geometry notation helpers, injected ahead of every model-written
|
|
7
|
+
# script so the model calls them by name instead of constructing this
|
|
8
|
+
# notation by hand -- it's unreliable at hand-rolling right-angle squares,
|
|
9
|
+
# equal-length tick marks, and angle arcs from scratch (wrong vertex, wrong
|
|
10
|
+
# offset direction, text overlapping the shape), but reliable at calling a
|
|
11
|
+
# documented function with the right arguments (see MASTER_PROMPT Example 26).
|
|
12
|
+
GEOMETRY_HELPERS_SRC = '''
|
|
13
|
+
import numpy as np
|
|
14
|
+
from matplotlib.patches import Polygon as _HelperPolygon, Arc as _HelperArc
|
|
15
|
+
|
|
16
|
+
def right_angle_marker(ax, vertex, p1, p2, size=0.3, **kwargs):
|
|
17
|
+
vertex, p1, p2 = np.array(vertex, dtype=float), np.array(p1, dtype=float), np.array(p2, dtype=float)
|
|
18
|
+
u1 = (p1 - vertex) / np.linalg.norm(p1 - vertex)
|
|
19
|
+
u2 = (p2 - vertex) / np.linalg.norm(p2 - vertex)
|
|
20
|
+
pts = [vertex, vertex + size * u1, vertex + size * u1 + size * u2, vertex + size * u2]
|
|
21
|
+
kwargs.setdefault("fill", False)
|
|
22
|
+
kwargs.setdefault("edgecolor", "black")
|
|
23
|
+
kwargs.setdefault("linewidth", 1.2)
|
|
24
|
+
ax.add_patch(_HelperPolygon(pts, closed=True, **kwargs))
|
|
25
|
+
|
|
26
|
+
def tick_marks(ax, p1, p2, count=1, size=0.15, gap=0.08, **kwargs):
|
|
27
|
+
p1, p2 = np.array(p1, dtype=float), np.array(p2, dtype=float)
|
|
28
|
+
mid = (p1 + p2) / 2
|
|
29
|
+
along = (p2 - p1) / np.linalg.norm(p2 - p1)
|
|
30
|
+
perp = np.array([-along[1], along[0]])
|
|
31
|
+
kwargs.setdefault("color", "black")
|
|
32
|
+
kwargs.setdefault("linewidth", 1.5)
|
|
33
|
+
for off in (np.arange(count) - (count - 1) / 2) * gap:
|
|
34
|
+
c = mid + along * off
|
|
35
|
+
ax.plot([c[0] - perp[0] * size, c[0] + perp[0] * size],
|
|
36
|
+
[c[1] - perp[1] * size, c[1] + perp[1] * size], **kwargs)
|
|
37
|
+
|
|
38
|
+
def angle_arc(ax, vertex, theta1, theta2, radius=0.5, label=None, label_radius_mult=1.4, **kwargs):
|
|
39
|
+
kwargs.setdefault("color", "black")
|
|
40
|
+
kwargs.setdefault("linewidth", 1.2)
|
|
41
|
+
ax.add_patch(_HelperArc(vertex, 2 * radius, 2 * radius, angle=0, theta1=theta1, theta2=theta2, **kwargs))
|
|
42
|
+
if label:
|
|
43
|
+
mid = np.radians((theta1 + theta2) / 2)
|
|
44
|
+
lx = vertex[0] + radius * label_radius_mult * np.cos(mid)
|
|
45
|
+
ly = vertex[1] + radius * label_radius_mult * np.sin(mid)
|
|
46
|
+
ax.text(lx, ly, label, ha="center", va="center", fontsize=10)
|
|
47
|
+
|
|
48
|
+
def side_label(ax, p1, p2, text=None, offset=None, fmt="{:.2f}", **kwargs):
|
|
49
|
+
p1, p2 = np.array(p1, dtype=float), np.array(p2, dtype=float)
|
|
50
|
+
length = np.linalg.norm(p2 - p1)
|
|
51
|
+
if text is None:
|
|
52
|
+
text = fmt.format(length)
|
|
53
|
+
if offset is None:
|
|
54
|
+
offset = 0.35
|
|
55
|
+
mid = (p1 + p2) / 2
|
|
56
|
+
along = (p2 - p1) / length
|
|
57
|
+
perp = np.array([-along[1], along[0]])
|
|
58
|
+
kwargs.setdefault("ha", "center")
|
|
59
|
+
kwargs.setdefault("va", "center")
|
|
60
|
+
kwargs.setdefault("fontsize", 10)
|
|
61
|
+
ax.text(*(mid + perp * offset), text, **kwargs)
|
|
62
|
+
'''
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def autofix_text_overlaps(fig, max_iter: int = 40, min_font_size: float = 6.0) -> None:
|
|
66
|
+
"""Detects overlapping ax.text() bounding boxes across every Axes in
|
|
67
|
+
`fig` and nudges colliding labels apart along the line between their
|
|
68
|
+
centers, iterating until none overlap. Each text has a displacement
|
|
69
|
+
budget (proportional to its own size) -- once a text has moved further
|
|
70
|
+
than that without resolving its overlap, repositioning stops for it and
|
|
71
|
+
its font shrinks instead, so a tight cluster that can't be cleanly
|
|
72
|
+
separated ends up smaller rather than flung arbitrarily far from its
|
|
73
|
+
meaningful anchor point. Used directly (in-process) by every
|
|
74
|
+
deterministic renderer below; the model-facing FIGURE sandbox gets the
|
|
75
|
+
equivalent logic via the separately-injected TEXT_OVERLAP_FIX_SRC string
|
|
76
|
+
instead, since that runs in an isolated subprocess that can't import
|
|
77
|
+
this module."""
|
|
78
|
+
fig.canvas.draw()
|
|
79
|
+
for ax in fig.get_axes():
|
|
80
|
+
texts = [t for t in ax.texts if t.get_text().strip()]
|
|
81
|
+
if len(texts) < 2:
|
|
82
|
+
continue
|
|
83
|
+
renderer = fig.canvas.get_renderer()
|
|
84
|
+
budget = [1.3 * max(t.get_window_extent(renderer=renderer).width,
|
|
85
|
+
t.get_window_extent(renderer=renderer).height) for t in texts]
|
|
86
|
+
traveled = [0.0] * len(texts)
|
|
87
|
+
for _ in range(max_iter):
|
|
88
|
+
fig.canvas.draw()
|
|
89
|
+
renderer = fig.canvas.get_renderer()
|
|
90
|
+
bboxes = [t.get_window_extent(renderer=renderer) for t in texts]
|
|
91
|
+
moved = False
|
|
92
|
+
for i in range(len(texts)):
|
|
93
|
+
for j in range(i + 1, len(texts)):
|
|
94
|
+
bi, bj = bboxes[i], bboxes[j]
|
|
95
|
+
if not bi.overlaps(bj):
|
|
96
|
+
continue
|
|
97
|
+
cxi, cyi = bi.x0 + bi.width / 2, bi.y0 + bi.height / 2
|
|
98
|
+
cxj, cyj = bj.x0 + bj.width / 2, bj.y0 + bj.height / 2
|
|
99
|
+
dx, dy = cxj - cxi, cyj - cyi
|
|
100
|
+
dist = (dx ** 2 + dy ** 2) ** 0.5
|
|
101
|
+
if dist < 1e-6:
|
|
102
|
+
dx, dy, dist = float(j - i), 0.0, float(j - i)
|
|
103
|
+
ux, uy = dx / dist, dy / dist
|
|
104
|
+
overlap_x = (bi.width + bj.width) / 2 - abs(dx)
|
|
105
|
+
overlap_y = (bi.height + bj.height) / 2 - abs(dy)
|
|
106
|
+
push = max(overlap_x, overlap_y, 2.0) * 0.5 + 1.5
|
|
107
|
+
for idx, t, sign in ((i, texts[i], -1), (j, texts[j], 1)):
|
|
108
|
+
if traveled[idx] >= budget[idx]:
|
|
109
|
+
if t.get_fontsize() > min_font_size:
|
|
110
|
+
t.set_fontsize(max(min_font_size, t.get_fontsize() * 0.85))
|
|
111
|
+
moved = True
|
|
112
|
+
continue
|
|
113
|
+
trans = t.get_transform()
|
|
114
|
+
x, y = t.get_position()
|
|
115
|
+
px, py = trans.transform((x, y))
|
|
116
|
+
nx, ny = trans.inverted().transform((px + sign * ux * push, py + sign * uy * push))
|
|
117
|
+
t.set_position((nx, ny))
|
|
118
|
+
traveled[idx] += push
|
|
119
|
+
moved = True
|
|
120
|
+
if not moved:
|
|
121
|
+
break
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# The model can't be relied on to predict where its own labels will end up
|
|
125
|
+
# relative to each other -- it places each ax.text() independently with no
|
|
126
|
+
# view of the others. This patches plt.savefig (transparently, the model's
|
|
127
|
+
# own `import matplotlib.pyplot as plt` binds the same module object) to
|
|
128
|
+
# detect overlapping text bounding boxes across every Axes right before the
|
|
129
|
+
# real save, and nudge colliding labels apart along the line between their
|
|
130
|
+
# centers, iterating until none overlap. A deterministic, geometric fix --
|
|
131
|
+
# exactly the kind of thing that's calculable rather than left to chance.
|
|
132
|
+
# (Duplicated as source text rather than shared with autofix_text_overlaps()
|
|
133
|
+
# above because this runs inside an isolated model-code subprocess that
|
|
134
|
+
# can't import this module.)
|
|
135
|
+
TEXT_OVERLAP_FIX_SRC = '''
|
|
136
|
+
import matplotlib.pyplot as _overlap_plt
|
|
137
|
+
|
|
138
|
+
def _shift_text_px(text, dx_px, dy_px):
|
|
139
|
+
trans = text.get_transform()
|
|
140
|
+
x, y = text.get_position()
|
|
141
|
+
px, py = trans.transform((x, y))
|
|
142
|
+
nx, ny = trans.inverted().transform((px + dx_px, py + dy_px))
|
|
143
|
+
text.set_position((nx, ny))
|
|
144
|
+
|
|
145
|
+
def _autofix_text_overlaps(fig, max_iter=40, min_font_size=6.0):
|
|
146
|
+
fig.canvas.draw()
|
|
147
|
+
for ax in fig.get_axes():
|
|
148
|
+
texts = [t for t in ax.texts if t.get_text().strip()]
|
|
149
|
+
if len(texts) < 2:
|
|
150
|
+
continue
|
|
151
|
+
renderer = fig.canvas.get_renderer()
|
|
152
|
+
budget = [1.3 * max(t.get_window_extent(renderer=renderer).width,
|
|
153
|
+
t.get_window_extent(renderer=renderer).height) for t in texts]
|
|
154
|
+
traveled = [0.0] * len(texts)
|
|
155
|
+
for _ in range(max_iter):
|
|
156
|
+
fig.canvas.draw()
|
|
157
|
+
renderer = fig.canvas.get_renderer()
|
|
158
|
+
bboxes = [t.get_window_extent(renderer=renderer) for t in texts]
|
|
159
|
+
moved = False
|
|
160
|
+
for i in range(len(texts)):
|
|
161
|
+
for j in range(i + 1, len(texts)):
|
|
162
|
+
bi, bj = bboxes[i], bboxes[j]
|
|
163
|
+
if not bi.overlaps(bj):
|
|
164
|
+
continue
|
|
165
|
+
cxi, cyi = bi.x0 + bi.width / 2, bi.y0 + bi.height / 2
|
|
166
|
+
cxj, cyj = bj.x0 + bj.width / 2, bj.y0 + bj.height / 2
|
|
167
|
+
dx, dy = cxj - cxi, cyj - cyi
|
|
168
|
+
dist = (dx ** 2 + dy ** 2) ** 0.5
|
|
169
|
+
if dist < 1e-6:
|
|
170
|
+
dx, dy, dist = float(j - i), 0.0, float(j - i)
|
|
171
|
+
ux, uy = dx / dist, dy / dist
|
|
172
|
+
overlap_x = (bi.width + bj.width) / 2 - abs(dx)
|
|
173
|
+
overlap_y = (bi.height + bj.height) / 2 - abs(dy)
|
|
174
|
+
push = max(overlap_x, overlap_y, 2.0) * 0.5 + 1.5
|
|
175
|
+
for idx, t, sign in ((i, texts[i], -1), (j, texts[j], 1)):
|
|
176
|
+
if traveled[idx] >= budget[idx]:
|
|
177
|
+
if t.get_fontsize() > min_font_size:
|
|
178
|
+
t.set_fontsize(max(min_font_size, t.get_fontsize() * 0.85))
|
|
179
|
+
moved = True
|
|
180
|
+
continue
|
|
181
|
+
_shift_text_px(t, sign * ux * push, sign * uy * push)
|
|
182
|
+
traveled[idx] += push
|
|
183
|
+
moved = True
|
|
184
|
+
if not moved:
|
|
185
|
+
break
|
|
186
|
+
|
|
187
|
+
_original_savefig = _overlap_plt.savefig
|
|
188
|
+
|
|
189
|
+
def _savefig_with_overlap_fix(*args, **kwargs):
|
|
190
|
+
try:
|
|
191
|
+
_autofix_text_overlaps(_overlap_plt.gcf())
|
|
192
|
+
except Exception:
|
|
193
|
+
pass
|
|
194
|
+
return _original_savefig(*args, **kwargs)
|
|
195
|
+
|
|
196
|
+
_overlap_plt.savefig = _savefig_with_overlap_fix
|
|
197
|
+
'''
|
simit/skills/pool.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""Parallel rendering in warm worker processes.
|
|
2
|
+
|
|
3
|
+
Rendering (matplotlib, RDKit, Chromium, executing model-written code) is CPU
|
|
4
|
+
work that would otherwise stall the GPU-driving main process. Each worker is a
|
|
5
|
+
``python -m simit.skills.pool`` subprocess -- deliberately not a
|
|
6
|
+
multiprocessing child, which would re-import the user's ``__main__`` script --
|
|
7
|
+
that keeps its imports and browser warm. A hung or crashed worker is killed
|
|
8
|
+
and replaced. Skills that cannot be pickled by reference (e.g. defined in a
|
|
9
|
+
notebook or ``__main__``) are rendered in a thread of the main process.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import io
|
|
15
|
+
import os
|
|
16
|
+
import pickle
|
|
17
|
+
import queue
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
import traceback
|
|
23
|
+
from concurrent.futures import Future, ThreadPoolExecutor
|
|
24
|
+
from multiprocessing.connection import Connection
|
|
25
|
+
|
|
26
|
+
from PIL import Image
|
|
27
|
+
|
|
28
|
+
from .base import SpecError
|
|
29
|
+
|
|
30
|
+
RENDER_TIMEOUT_S = 90.0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _serve(conn_in: Connection, conn_out: Connection):
|
|
34
|
+
os.environ.setdefault("MPLBACKEND", "Agg")
|
|
35
|
+
while True:
|
|
36
|
+
try:
|
|
37
|
+
msg = conn_in.recv()
|
|
38
|
+
except (EOFError, OSError, KeyboardInterrupt):
|
|
39
|
+
return
|
|
40
|
+
if msg is None:
|
|
41
|
+
return
|
|
42
|
+
task_id, payload = msg
|
|
43
|
+
try:
|
|
44
|
+
skill, spec = pickle.loads(payload)
|
|
45
|
+
except Exception as e: # this process cannot import the skill: the parent renders it instead
|
|
46
|
+
conn_out.send((task_id, None, f"{type(e).__name__}: {e}"))
|
|
47
|
+
continue
|
|
48
|
+
try:
|
|
49
|
+
image = skill.render(spec)
|
|
50
|
+
buf = io.BytesIO()
|
|
51
|
+
image.save(buf, format="PNG")
|
|
52
|
+
conn_out.send((task_id, True, buf.getvalue()))
|
|
53
|
+
except SpecError as e:
|
|
54
|
+
conn_out.send((task_id, False, str(e)))
|
|
55
|
+
except Exception as e: # unexpected renderer failure: still reported back for repair
|
|
56
|
+
conn_out.send((task_id, False, f"{type(e).__name__}: {e}"))
|
|
57
|
+
except BaseException:
|
|
58
|
+
conn_out.send((task_id, False, traceback.format_exc()[-500:]))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _Worker:
|
|
62
|
+
def __init__(self):
|
|
63
|
+
p2c_r, p2c_w = os.pipe()
|
|
64
|
+
c2p_r, c2p_w = os.pipe()
|
|
65
|
+
env = dict(os.environ, MPLBACKEND="Agg", PYTHONUNBUFFERED="1")
|
|
66
|
+
# The parent's import path, so skills from the user's own modules unpickle here too.
|
|
67
|
+
paths = [p for p in sys.path if p and os.path.isdir(p)] + [env.get("PYTHONPATH", "")]
|
|
68
|
+
env["PYTHONPATH"] = os.pathsep.join(p for p in paths if p)
|
|
69
|
+
self.proc = subprocess.Popen(
|
|
70
|
+
[sys.executable, "-m", "simit.skills.pool", str(p2c_r), str(c2p_w)],
|
|
71
|
+
pass_fds=(p2c_r, c2p_w), env=env, stdin=subprocess.DEVNULL,
|
|
72
|
+
)
|
|
73
|
+
os.close(p2c_r)
|
|
74
|
+
os.close(c2p_w)
|
|
75
|
+
self.send_conn = Connection(p2c_w, readable=False)
|
|
76
|
+
self.recv_conn = Connection(c2p_r, writable=False)
|
|
77
|
+
self.task = None # (task_id, future, deadline)
|
|
78
|
+
|
|
79
|
+
def alive(self) -> bool:
|
|
80
|
+
return self.proc.poll() is None
|
|
81
|
+
|
|
82
|
+
def kill(self):
|
|
83
|
+
for conn in (self.send_conn, self.recv_conn):
|
|
84
|
+
try:
|
|
85
|
+
conn.close()
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
try:
|
|
89
|
+
self.proc.kill()
|
|
90
|
+
self.proc.wait(timeout=5)
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class RenderPool:
|
|
96
|
+
def __init__(self, num_workers: int | None = None, timeout: float = RENDER_TIMEOUT_S):
|
|
97
|
+
self.num_workers = default_num_workers() if num_workers is None else num_workers
|
|
98
|
+
self.timeout = timeout
|
|
99
|
+
self._queue: "queue.Queue[tuple[int, bytes, Future]]" = queue.Queue()
|
|
100
|
+
self._workers: list[_Worker] = []
|
|
101
|
+
self._lock = threading.Lock()
|
|
102
|
+
self._thread = None
|
|
103
|
+
self._next_id = 0
|
|
104
|
+
self._closed = False
|
|
105
|
+
self._local = ThreadPoolExecutor(max_workers=1, thread_name_prefix="simit-render")
|
|
106
|
+
self._local_types: set = set() # skill classes the workers could not import
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------- public
|
|
109
|
+
def submit(self, skill, spec) -> Future:
|
|
110
|
+
"""Render ``spec`` with ``skill``; the future yields a PIL image or
|
|
111
|
+
raises SpecError."""
|
|
112
|
+
if (self.num_workers <= 0 or not getattr(skill, "render_in_subprocess", True)
|
|
113
|
+
or type(skill).__module__ in ("__main__", "__mp_main__") or _key(skill) in self._local_types):
|
|
114
|
+
return self._local.submit(_render_local, skill, spec)
|
|
115
|
+
try:
|
|
116
|
+
payload = pickle.dumps((skill, spec))
|
|
117
|
+
except Exception:
|
|
118
|
+
return self._local.submit(_render_local, skill, spec)
|
|
119
|
+
fut: Future = Future()
|
|
120
|
+
with self._lock:
|
|
121
|
+
self._next_id += 1
|
|
122
|
+
task_id = self._next_id
|
|
123
|
+
if self._thread is None:
|
|
124
|
+
self._thread = threading.Thread(target=self._loop, daemon=True, name="simit-render-pool")
|
|
125
|
+
self._thread.start()
|
|
126
|
+
self._queue.put((task_id, payload, fut))
|
|
127
|
+
return fut
|
|
128
|
+
|
|
129
|
+
def warmup(self):
|
|
130
|
+
"""Start the worker processes now instead of on the first render."""
|
|
131
|
+
with self._lock:
|
|
132
|
+
while len(self._workers) < self.num_workers:
|
|
133
|
+
self._workers.append(_Worker())
|
|
134
|
+
|
|
135
|
+
def close(self):
|
|
136
|
+
self._closed = True
|
|
137
|
+
with self._lock:
|
|
138
|
+
for w in self._workers:
|
|
139
|
+
try:
|
|
140
|
+
w.send_conn.send(None)
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
w.kill()
|
|
144
|
+
self._workers = []
|
|
145
|
+
self._local.shutdown(wait=False)
|
|
146
|
+
|
|
147
|
+
def __del__(self):
|
|
148
|
+
try:
|
|
149
|
+
self.close()
|
|
150
|
+
except Exception:
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
# -------------------------------------------------------------- internals
|
|
154
|
+
def _loop(self):
|
|
155
|
+
pending: list[tuple[int, bytes, Future]] = []
|
|
156
|
+
while not self._closed:
|
|
157
|
+
busy = any(w.task is not None for w in self._workers)
|
|
158
|
+
try:
|
|
159
|
+
pending.append(self._queue.get(timeout=0.005 if (pending or busy) else 0.5))
|
|
160
|
+
while True:
|
|
161
|
+
pending.append(self._queue.get_nowait())
|
|
162
|
+
except queue.Empty:
|
|
163
|
+
pass
|
|
164
|
+
with self._lock:
|
|
165
|
+
if self._closed:
|
|
166
|
+
break
|
|
167
|
+
while len(self._workers) < self.num_workers and pending:
|
|
168
|
+
self._workers.append(_Worker())
|
|
169
|
+
workers = list(self._workers)
|
|
170
|
+
for w in workers:
|
|
171
|
+
if w.task is None and pending:
|
|
172
|
+
task_id, payload, fut = pending.pop(0)
|
|
173
|
+
try:
|
|
174
|
+
w.send_conn.send((task_id, payload))
|
|
175
|
+
w.task = (task_id, fut, time.monotonic() + self.timeout, payload)
|
|
176
|
+
except Exception:
|
|
177
|
+
pending.insert(0, (task_id, payload, fut))
|
|
178
|
+
self._replace(w)
|
|
179
|
+
for w in workers:
|
|
180
|
+
if w.task is None:
|
|
181
|
+
continue
|
|
182
|
+
_, fut, deadline, payload = w.task
|
|
183
|
+
try:
|
|
184
|
+
ready = w.recv_conn.poll()
|
|
185
|
+
except Exception:
|
|
186
|
+
ready = False
|
|
187
|
+
if ready:
|
|
188
|
+
try:
|
|
189
|
+
_, ok, data = w.recv_conn.recv()
|
|
190
|
+
except Exception:
|
|
191
|
+
w.task = None
|
|
192
|
+
fut.set_exception(SpecError("renderer process crashed"))
|
|
193
|
+
self._replace(w)
|
|
194
|
+
continue
|
|
195
|
+
w.task = None
|
|
196
|
+
if ok is None:
|
|
197
|
+
skill, spec = pickle.loads(payload)
|
|
198
|
+
self._local_types.add(_key(skill))
|
|
199
|
+
_chain(self._local.submit(_render_local, skill, spec), fut)
|
|
200
|
+
elif ok:
|
|
201
|
+
image = Image.open(io.BytesIO(data))
|
|
202
|
+
image.load()
|
|
203
|
+
fut.set_result(image)
|
|
204
|
+
else:
|
|
205
|
+
fut.set_exception(SpecError(data))
|
|
206
|
+
elif not w.alive():
|
|
207
|
+
w.task = None
|
|
208
|
+
fut.set_exception(SpecError("renderer process crashed"))
|
|
209
|
+
self._replace(w)
|
|
210
|
+
elif time.monotonic() > deadline:
|
|
211
|
+
w.task = None
|
|
212
|
+
fut.set_exception(SpecError(f"rendering timed out after {self.timeout:.0f}s"))
|
|
213
|
+
self._replace(w)
|
|
214
|
+
|
|
215
|
+
def _replace(self, worker: _Worker):
|
|
216
|
+
worker.kill()
|
|
217
|
+
with self._lock:
|
|
218
|
+
if worker in self._workers:
|
|
219
|
+
self._workers[self._workers.index(worker)] = _Worker()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _key(skill) -> tuple:
|
|
223
|
+
return type(skill).__module__, type(skill).__qualname__
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _chain(src: Future, dst: Future):
|
|
227
|
+
def done(f):
|
|
228
|
+
if f.exception() is not None:
|
|
229
|
+
dst.set_exception(f.exception())
|
|
230
|
+
else:
|
|
231
|
+
dst.set_result(f.result())
|
|
232
|
+
src.add_done_callback(done)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _render_local(skill, spec):
|
|
236
|
+
try:
|
|
237
|
+
return skill.render(spec)
|
|
238
|
+
except SpecError:
|
|
239
|
+
raise
|
|
240
|
+
except Exception as e:
|
|
241
|
+
raise SpecError(f"{type(e).__name__}: {e}") from None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def default_num_workers() -> int:
|
|
245
|
+
"""Worker count from the CPUs actually granted to this job."""
|
|
246
|
+
if os.environ.get("SIMIT_RENDER_WORKERS"):
|
|
247
|
+
return int(os.environ["SIMIT_RENDER_WORKERS"])
|
|
248
|
+
n = None
|
|
249
|
+
for var in ("LSB_DJOB_NUMPROC", "SLURM_CPUS_PER_TASK"):
|
|
250
|
+
if os.environ.get(var, "").isdigit():
|
|
251
|
+
n = int(os.environ[var])
|
|
252
|
+
break
|
|
253
|
+
if n is None:
|
|
254
|
+
try:
|
|
255
|
+
n = len(os.sched_getaffinity(0))
|
|
256
|
+
except AttributeError:
|
|
257
|
+
n = os.cpu_count() or 2
|
|
258
|
+
return max(1, min(8, n - 1 if n > 2 else n))
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
if __name__ == "__main__":
|
|
262
|
+
_serve(Connection(int(sys.argv[1]), writable=False), Connection(int(sys.argv[2]), readable=False))
|