annoscript 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.
- annoscript/__init__.py +1 -0
- annoscript/_backend_profiles.py +17 -0
- annoscript/_client_common.py +287 -0
- annoscript/_data.py +37 -0
- annoscript/_dispatch_pool.py +47 -0
- annoscript/_env.py +19 -0
- annoscript/_examples.py +34 -0
- annoscript/_expressions.py +93 -0
- annoscript/_plan_viewer.html +953 -0
- annoscript/_remote_jobs.py +211 -0
- annoscript/_request_adapters.py +71 -0
- annoscript/_speculation.py +321 -0
- annoscript/_test_artifacts.py +33 -0
- annoscript/_test_evaluation_http.py +117 -0
- annoscript/_test_ir_runtime.py +138 -0
- annoscript/_test_job_process.py +166 -0
- annoscript/_test_plan_runtime.py +331 -0
- annoscript/_test_remote_client.py +46 -0
- annoscript/_test_remote_server.py +75 -0
- annoscript/_test_synthetic.py +67 -0
- annoscript/anno.py +270 -0
- annoscript/backends/__init__.py +13 -0
- annoscript/backends/_batch_wire.py +26 -0
- annoscript/backends/_bounded_grammar.py +97 -0
- annoscript/backends/_example_layout.py +23 -0
- annoscript/backends/_generation_policy.py +116 -0
- annoscript/backends/_grammar.py +83 -0
- annoscript/backends/_preview.py +13 -0
- annoscript/backends/_prompt.py +115 -0
- annoscript/backends/_shared_rules.py +38 -0
- annoscript/backends/adapters/__init__.py +17 -0
- annoscript/backends/adapters/chat.py +39 -0
- annoscript/backends/adapters/decision.py +119 -0
- annoscript/backends/base.py +427 -0
- annoscript/backends/config.py +67 -0
- annoscript/backends/decision.py +150 -0
- annoscript/backends/llm.py +338 -0
- annoscript/client.py +247 -0
- annoscript/client_tests.py +17 -0
- annoscript/compiler.py +708 -0
- annoscript/cross_version_tests.py +68 -0
- annoscript/fixtures/request-adapters-v1.json +4741 -0
- annoscript/formatter.py +171 -0
- annoscript/grammar_tests.py +176 -0
- annoscript/inspect.py +77 -0
- annoscript/ir.py +458 -0
- annoscript/job_cross_version_tests.py +118 -0
- annoscript/job_protocol.py +180 -0
- annoscript/jobs.py +255 -0
- annoscript/metrics.py +153 -0
- annoscript/p0_grammar_tests.py +147 -0
- annoscript/passes/__init__.py +40 -0
- annoscript/passes/_analysis.py +178 -0
- annoscript/passes/control_flow.py +68 -0
- annoscript/passes/dead_assignments.py +44 -0
- annoscript/passes/folding.py +104 -0
- annoscript/passes/propagation.py +53 -0
- annoscript/passes/temporaries.py +45 -0
- annoscript/plan.py +542 -0
- annoscript/plan_formatter.py +452 -0
- annoscript/plan_lowering.py +571 -0
- annoscript/preview.py +115 -0
- annoscript/remote.py +304 -0
- annoscript/runtime.py +861 -0
- annoscript/server/__init__.py +1 -0
- annoscript/server/__main__.py +49 -0
- annoscript/server/_plan_cache.py +79 -0
- annoscript/server/api.py +146 -0
- annoscript/server/metrics.py +140 -0
- annoscript/server/migrate.py +61 -0
- annoscript/server/releases.py +143 -0
- annoscript/server/store.py +429 -0
- annoscript/server/worker.py +290 -0
- annoscript/substring_tests.py +79 -0
- annoscript/test_advisory_lengths.py +115 -0
- annoscript/test_backends.py +181 -0
- annoscript/test_client.py +349 -0
- annoscript/test_compact_batch.py +95 -0
- annoscript/test_compiler.py +444 -0
- annoscript/test_decision_adapters.py +410 -0
- annoscript/test_decision_wire.py +190 -0
- annoscript/test_diagnostics.py +97 -0
- annoscript/test_engineering_p0.py +191 -0
- annoscript/test_environment.py +65 -0
- annoscript/test_example_backends.py +251 -0
- annoscript/test_examples.py +149 -0
- annoscript/test_field_names.py +221 -0
- annoscript/test_formatter.py +153 -0
- annoscript/test_fusion_scheduling.py +109 -0
- annoscript/test_generation_policy.py +180 -0
- annoscript/test_ir.py +148 -0
- annoscript/test_jobs.py +371 -0
- annoscript/test_jobs_server.py +511 -0
- annoscript/test_llm_batch.py +428 -0
- annoscript/test_metrics.py +59 -0
- annoscript/test_metrics_server.py +261 -0
- annoscript/test_minimal_fusion.py +59 -0
- annoscript/test_nullable_generate.py +169 -0
- annoscript/test_passes.py +408 -0
- annoscript/test_plan.py +522 -0
- annoscript/test_plan_constraints.py +305 -0
- annoscript/test_plan_formatter.py +272 -0
- annoscript/test_prompt_layout.py +140 -0
- annoscript/test_qwen_backend.py +181 -0
- annoscript/test_qwen_pilot_regression.py +180 -0
- annoscript/test_release_client.py +20 -0
- annoscript/test_remote.py +308 -0
- annoscript/test_request_adapters.py +295 -0
- annoscript/test_requirements.py +228 -0
- annoscript/test_rule_dedup.py +67 -0
- annoscript/test_runtime.py +314 -0
- annoscript/test_runtime_client.py +133 -0
- annoscript/test_source_fields.py +111 -0
- annoscript/test_speculation.py +458 -0
- annoscript/test_substring_backend.py +81 -0
- annoscript/test_synthetic_example.py +61 -0
- annoscript-0.1.0.dist-info/METADATA +145 -0
- annoscript-0.1.0.dist-info/RECORD +128 -0
- annoscript-0.1.0.dist-info/WHEEL +4 -0
- annoscript-0.1.0.dist-info/licenses/LICENSE +21 -0
- annoscript_examples/__init__.py +1 -0
- annoscript_examples/fixtures.py +3 -0
- annoscript_examples/request.execution_plan.json +3017 -0
- annoscript_examples/request.json +858 -0
- annoscript_examples/request.py +37 -0
- annoscript_examples/request_preview.decision.json +23 -0
- annoscript_examples/request_preview.generation.json +20 -0
- annoscript_examples/run.py +24 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""Read-only Infer dependency projection and inspection formats.
|
|
2
|
+
|
|
3
|
+
The projection includes implicit region completion and iteration activation;
|
|
4
|
+
Loop.enter must not be confused with Loop.done. Edges are a union of possible
|
|
5
|
+
paths, not a new executable schedule. Guards and the selected mode's prerequisites
|
|
6
|
+
govern actual readiness; success always awaits checks. Carried edges retain
|
|
7
|
+
witnesses rather than inventing a single iteration distance for a transitive
|
|
8
|
+
path with multiple recurrences.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections import defaultdict, deque
|
|
14
|
+
from dataclasses import dataclass, fields, replace
|
|
15
|
+
import html
|
|
16
|
+
import json
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
import re
|
|
19
|
+
|
|
20
|
+
from annoscript import ir
|
|
21
|
+
from annoscript import plan as p
|
|
22
|
+
from annoscript.formatter import _expression, _location
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class _Arc:
|
|
27
|
+
source: tuple[str, str]
|
|
28
|
+
target: tuple[str, str]
|
|
29
|
+
kind: str = ""
|
|
30
|
+
carried: str | None = None
|
|
31
|
+
distance: int = 0
|
|
32
|
+
guard: p.Gate | None = None
|
|
33
|
+
hazard: str | None = None
|
|
34
|
+
conservative: bool = False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_NOTES = (
|
|
38
|
+
"节点表示静态 Infer 调用位置;循环可产生多个实例,共享请求可能只发起一次。",
|
|
39
|
+
"依赖图是所有可能路径的并集;调用须满足各自的数据、控制和内存依赖,不执行未选分支。",
|
|
40
|
+
"eager 允许独立任务越过尚未完成的约束检查;strict 保留约束顺序屏障。两者成功返回前都须通过当前路径上的全部检查。",
|
|
41
|
+
"依赖层用于阅读,不是固定调度波次。并行候选是保守子集,不是对实际并发量的预测。",
|
|
42
|
+
"同算子只构成批处理候选:还需同时就绪、模型/系统提示词/后端选项兼容。不同参数可能要求拆批。",
|
|
43
|
+
"严格模式保留单条 request 的失败顺序;跨 request 的调度与批处理不在此图中。",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _prompt_label(expr):
|
|
48
|
+
"""Human-readable template, with dynamic parts visibly left unresolved."""
|
|
49
|
+
if isinstance(expr, ir.Literal) and isinstance(expr.value, str):
|
|
50
|
+
return expr.value
|
|
51
|
+
if isinstance(expr, ir.FormatString):
|
|
52
|
+
return "".join(_prompt_label(part) for part in expr.parts)
|
|
53
|
+
if isinstance(expr, ir.FormatValue):
|
|
54
|
+
return "{" + _expression(expr.value) + "}"
|
|
55
|
+
if isinstance(expr, ir.Binary) and expr.op == "add":
|
|
56
|
+
return _prompt_label(expr.left) + _prompt_label(expr.right)
|
|
57
|
+
return "{" + _expression(expr) + "}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class _View:
|
|
61
|
+
def __init__(self, plan):
|
|
62
|
+
self.plan = plan
|
|
63
|
+
self.tasks = {t.id: t for t in plan.tasks}
|
|
64
|
+
self.values = {v.id: v for v in plan.values}
|
|
65
|
+
self.regions = {r.id: r for r in plan.regions}
|
|
66
|
+
self.requests = {q.id: q for q in plan.requests}
|
|
67
|
+
self.sites = [t for t in plan.tasks if isinstance(t, p.Infer)]
|
|
68
|
+
self.alias = {t.id: f"I{i:02d}" for i, t in enumerate(self.sites, 1)}
|
|
69
|
+
self.loops = {t.id: f"L{i:02d}" for i, t in enumerate((t for t in plan.tasks if isinstance(t, p.Loop)), 1)}
|
|
70
|
+
self.branches = {t.id: f"B{i:02d}" for i, t in enumerate((t for t in plan.tasks if isinstance(t, p.Branch)), 1)}
|
|
71
|
+
self.checks = {t.id: f"C{i:02d}" for i, t in enumerate((t for t in plan.tasks if isinstance(t, p.Check)), 1)}
|
|
72
|
+
self.request_alias = {q.id: f"Q{i:02d}" for i, q in enumerate(plan.requests, 1)}
|
|
73
|
+
|
|
74
|
+
def path(self, task):
|
|
75
|
+
return p.loop_path(self.tasks[task].region, self.regions)
|
|
76
|
+
|
|
77
|
+
def expand(self, bound):
|
|
78
|
+
"""Bounded symbolic substitution for display only; never evaluate code."""
|
|
79
|
+
budget = [160]
|
|
80
|
+
|
|
81
|
+
def value(v, seen):
|
|
82
|
+
t = self.tasks[self.values[v].producer]
|
|
83
|
+
if isinstance(t, p.Infer):
|
|
84
|
+
return ir.Var(self.alias[t.id])
|
|
85
|
+
if isinstance(t, p.Eval) and v not in seen and budget[0] > 0:
|
|
86
|
+
return expr(t.expression.expression, {b.name: b.value for b in t.expression.bindings}, seen | {v})
|
|
87
|
+
return ir.Var(self.values[v].name or v)
|
|
88
|
+
|
|
89
|
+
def expr(e, bindings, seen):
|
|
90
|
+
budget[0] -= 1
|
|
91
|
+
if isinstance(e, ir.Var):
|
|
92
|
+
return value(bindings[e.name], seen)
|
|
93
|
+
if budget[0] <= 0:
|
|
94
|
+
return e
|
|
95
|
+
def walk(x):
|
|
96
|
+
if isinstance(x, ir.Expr):
|
|
97
|
+
return expr(x, bindings, seen)
|
|
98
|
+
if isinstance(x, tuple):
|
|
99
|
+
return tuple(walk(v) for v in x)
|
|
100
|
+
return x
|
|
101
|
+
return replace(e, **{f.name: walk(getattr(e, f.name)) for f in fields(e)})
|
|
102
|
+
|
|
103
|
+
return expr(bound.expression, {b.name: b.value for b in bound.bindings}, set())
|
|
104
|
+
|
|
105
|
+
def display(self, bound):
|
|
106
|
+
return _expression(self.expand(bound))
|
|
107
|
+
|
|
108
|
+
def choices(self, region):
|
|
109
|
+
choices = {}
|
|
110
|
+
while region is not None:
|
|
111
|
+
r = self.regions[region]
|
|
112
|
+
if r.kind in ("then", "else"):
|
|
113
|
+
choices[r.owner] = r.kind == "then"
|
|
114
|
+
region = r.parent
|
|
115
|
+
return choices
|
|
116
|
+
|
|
117
|
+
def compatible(self, a, b):
|
|
118
|
+
left, right = self.tasks[a], self.tasks[b]
|
|
119
|
+
if left.request == right.request:
|
|
120
|
+
return False
|
|
121
|
+
x, y = self.choices(left.region), self.choices(right.region)
|
|
122
|
+
return not any(k in y and v != y[k] for k, v in x.items())
|
|
123
|
+
|
|
124
|
+
def arcs(self, mode):
|
|
125
|
+
execution_mode = "eager" if mode == "speculative" else mode
|
|
126
|
+
arcs = []
|
|
127
|
+
|
|
128
|
+
def add(a, b, kind="", **kwargs):
|
|
129
|
+
arcs.append(_Arc(a, b, kind, **kwargs))
|
|
130
|
+
|
|
131
|
+
for d in self.plan.dependencies_for(mode):
|
|
132
|
+
add((d.source, d.port), (d.target, "start"), d.kind,
|
|
133
|
+
carried=d.mapping.carried_loop, distance=d.mapping.distance,
|
|
134
|
+
guard=d.condition, hazard=d.hazard, conservative=d.certainty == "conservative")
|
|
135
|
+
for t in self.plan.tasks:
|
|
136
|
+
add((t.region, "active"), (t.id, "start"), "control")
|
|
137
|
+
if isinstance(t, p.Loop):
|
|
138
|
+
add((t.id, "start"), (t.id, "enter"))
|
|
139
|
+
add((t.id, "enter"), (t.id, "done"), "completion") # zero iterations
|
|
140
|
+
add((t.id, "enter"), (t.body, "active"), "control")
|
|
141
|
+
for child in self.regions[t.body].tasks:
|
|
142
|
+
add((child, "done"), (t.id, "done"), "completion")
|
|
143
|
+
if execution_mode in t.serial_modes:
|
|
144
|
+
add((child, "done"), (t.body, "active"), "iteration_order", carried=t.id, distance=1)
|
|
145
|
+
elif isinstance(t, p.Join):
|
|
146
|
+
add((t.id, "start"), (t.id, "done"))
|
|
147
|
+
branch = self.tasks[t.branch]
|
|
148
|
+
for region, port in ((branch.then_region, "true"), (branch.else_region, "false")):
|
|
149
|
+
for child in self.regions[region].tasks:
|
|
150
|
+
add((child, "done"), (t.id, "done"), "completion", guard=p.Gate(branch.id, port))
|
|
151
|
+
add((t.id, "done"), (t.id, "continue"), "control")
|
|
152
|
+
else:
|
|
153
|
+
add((t.id, "start"), (t.id, "done"))
|
|
154
|
+
if isinstance(t, p.Branch):
|
|
155
|
+
for region, port in ((t.then_region, "true"), (t.else_region, "false")):
|
|
156
|
+
add((t.id, "done"), (t.id, port), "control", guard=p.Gate(t.id, port))
|
|
157
|
+
add((t.id, port), (region, "active"), "control", guard=p.Gate(t.id, port))
|
|
158
|
+
return arcs
|
|
159
|
+
|
|
160
|
+
def project(self, mode):
|
|
161
|
+
incoming = defaultdict(list)
|
|
162
|
+
for arc in self.arcs(mode):
|
|
163
|
+
incoming[arc.target].append(arc)
|
|
164
|
+
result = []
|
|
165
|
+
for target in self.sites:
|
|
166
|
+
# Recurrences outside the endpoints' common loop coordinates are
|
|
167
|
+
# internal work (gather/broadcast), not Infer-to-Infer recurrences.
|
|
168
|
+
groups = defaultdict(set)
|
|
169
|
+
for source in self.sites:
|
|
170
|
+
common = []
|
|
171
|
+
for a, b in zip(self.path(source.id), self.path(target.id)):
|
|
172
|
+
if a != b:
|
|
173
|
+
break
|
|
174
|
+
common.append(a)
|
|
175
|
+
groups[tuple(common)].add(source.id)
|
|
176
|
+
for common, sources in groups.items():
|
|
177
|
+
result.extend(self._trace(target, common, sources, incoming))
|
|
178
|
+
return sorted(result, key=lambda e: (e["source"], e["target"], e["carried"]))
|
|
179
|
+
|
|
180
|
+
def _trace(self, target, common, sources, incoming):
|
|
181
|
+
# Two states per signal suffice for each shared coordinate prefix.
|
|
182
|
+
# Distances remain on witness arcs, not on a fabricated static DAG.
|
|
183
|
+
result = []
|
|
184
|
+
start = ((target.id, "start"), False)
|
|
185
|
+
seen, queue = {start}, deque([start])
|
|
186
|
+
parents, first, terminals = defaultdict(list), {}, []
|
|
187
|
+
while queue:
|
|
188
|
+
state = queue.popleft()
|
|
189
|
+
signal, carried = state
|
|
190
|
+
if signal[1] == "done" and signal[0] in self.alias:
|
|
191
|
+
if signal[0] in sources:
|
|
192
|
+
terminals.append(state)
|
|
193
|
+
continue
|
|
194
|
+
for arc in incoming[signal]:
|
|
195
|
+
previous = (arc.source, carried or arc.carried in common)
|
|
196
|
+
parents[previous].append((state, arc))
|
|
197
|
+
if previous not in seen:
|
|
198
|
+
seen.add(previous)
|
|
199
|
+
first[previous] = (state, arc)
|
|
200
|
+
queue.append(previous)
|
|
201
|
+
for terminal in terminals:
|
|
202
|
+
used, todo, visited = set(), [terminal], set()
|
|
203
|
+
while todo:
|
|
204
|
+
state = todo.pop()
|
|
205
|
+
if state == start or state in visited:
|
|
206
|
+
continue
|
|
207
|
+
visited.add(state)
|
|
208
|
+
for parent, arc in parents[state]:
|
|
209
|
+
used.add(arc)
|
|
210
|
+
todo.append(parent)
|
|
211
|
+
witness, state = [], terminal
|
|
212
|
+
while state != start:
|
|
213
|
+
parent, arc = first[state]
|
|
214
|
+
witness.append({"source": list(arc.source), "target": list(arc.target), "kind": arc.kind,
|
|
215
|
+
"loop": self.loops.get(arc.carried), "distance": arc.distance})
|
|
216
|
+
state = parent
|
|
217
|
+
guards = sorted({f"{self.branches.get(a.guard.task, a.guard.task)}={a.guard.port}" for a in used if a.guard})
|
|
218
|
+
recurrence = sorted({(self.loops[a.carried], a.distance) for a in used if a.carried in common})
|
|
219
|
+
result.append({
|
|
220
|
+
"source": self.alias[terminal[0][0]], "target": self.alias[target.id],
|
|
221
|
+
"carried": terminal[1], "kinds": sorted({a.kind for a in used if a.kind}),
|
|
222
|
+
"hazards": sorted({a.hazard for a in used if a.hazard}),
|
|
223
|
+
"guards": guards, "recurrences": [{"loop": loop, "distance": distance} for loop, distance in recurrence],
|
|
224
|
+
"via_loops": sorted({self.loops[a.carried] for a in used if a.carried and a.carried not in common}),
|
|
225
|
+
"gather": [self.loops[x] for x in self.path(terminal[0][0])[len(common):]],
|
|
226
|
+
"broadcast": [self.loops[x] for x in self.path(target.id)[len(common):]],
|
|
227
|
+
"conservative": any(a.conservative for a in used), "witness": witness,
|
|
228
|
+
})
|
|
229
|
+
return result
|
|
230
|
+
|
|
231
|
+
def nodes(self):
|
|
232
|
+
result = []
|
|
233
|
+
for site in self.sites:
|
|
234
|
+
args_task = self.tasks[self.values[site.arguments].producer]
|
|
235
|
+
expression = self.expand(args_task.expression) if isinstance(args_task, p.Eval) else ir.Var(site.arguments)
|
|
236
|
+
arguments = _expression(expression)
|
|
237
|
+
prompt, label = arguments, site.id
|
|
238
|
+
if isinstance(expression, ir.TupleExpr) and len(expression.items) == 2:
|
|
239
|
+
positional, keywords = expression.items
|
|
240
|
+
if isinstance(positional, ir.TupleExpr) and positional.items:
|
|
241
|
+
instruction = positional.items[0]
|
|
242
|
+
elif "instructions" in site.keywords and isinstance(keywords, ir.TupleExpr):
|
|
243
|
+
instruction = keywords.items[site.keywords.index("instructions")]
|
|
244
|
+
else:
|
|
245
|
+
instruction = expression
|
|
246
|
+
prompt = instruction.value if isinstance(instruction, ir.Literal) and isinstance(instruction.value, str) else _expression(instruction)
|
|
247
|
+
label = _prompt_label(instruction)
|
|
248
|
+
request = self.requests[site.request]
|
|
249
|
+
gates = []
|
|
250
|
+
for g in site.gates:
|
|
251
|
+
task = self.tasks[g.task]
|
|
252
|
+
if isinstance(task, p.Branch):
|
|
253
|
+
gates.append(f"{self.branches[task.id]}={g.port}: {self.display(task.condition)}")
|
|
254
|
+
elif isinstance(task, p.Loop):
|
|
255
|
+
gates.append(f"{self.loops[task.id]}: iteration active")
|
|
256
|
+
else:
|
|
257
|
+
gates.append(f"{g.task}.{g.port}")
|
|
258
|
+
result.append({
|
|
259
|
+
"id": self.alias[site.id], "task": site.id, "origin": site.origin,
|
|
260
|
+
"operation": request.operation, "label": " ".join(label.split())[:78],
|
|
261
|
+
"prompt": prompt, "arguments": arguments, "keywords": list(site.keywords),
|
|
262
|
+
"request": self.request_alias[site.request], "reusable": request.reusable,
|
|
263
|
+
"reuse_scope": [self.loops[x] for x in request.scope],
|
|
264
|
+
"loops": [self.loops[x] for x in self.path(site.id)], "gates": gates,
|
|
265
|
+
"source": _location(site.source.span), "calls": [_location(s) for s in site.source.calls],
|
|
266
|
+
})
|
|
267
|
+
return result
|
|
268
|
+
|
|
269
|
+
def analyze(self, mode):
|
|
270
|
+
execution_mode = "eager" if mode == "speculative" else mode
|
|
271
|
+
edges = self.project(mode)
|
|
272
|
+
nodes = self.nodes()
|
|
273
|
+
ids = [n["id"] for n in nodes]
|
|
274
|
+
original = {self.alias[s.id]: s.id for s in self.sites}
|
|
275
|
+
adjacency = {n: set() for n in ids}
|
|
276
|
+
for e in edges:
|
|
277
|
+
if not e["carried"]:
|
|
278
|
+
adjacency[e["source"]].add(e["target"])
|
|
279
|
+
reach = {}
|
|
280
|
+
for n in ids:
|
|
281
|
+
seen, todo = set(), list(adjacency[n])
|
|
282
|
+
while todo:
|
|
283
|
+
x = todo.pop()
|
|
284
|
+
if x not in seen:
|
|
285
|
+
seen.add(x)
|
|
286
|
+
todo.extend(adjacency[x])
|
|
287
|
+
reach[n] = seen
|
|
288
|
+
for e in edges:
|
|
289
|
+
e["redundant"] = not e["carried"] and any(e["target"] in reach[v] for v in adjacency[e["source"]] if v != e["target"])
|
|
290
|
+
remaining, layers = set(ids), []
|
|
291
|
+
while remaining:
|
|
292
|
+
ready = sorted(n for n in remaining if not any(n in adjacency[k] for k in remaining))
|
|
293
|
+
if not ready:
|
|
294
|
+
break
|
|
295
|
+
layers.append(ready)
|
|
296
|
+
remaining.difference_update(ready)
|
|
297
|
+
rank = {n: i for i, layer in enumerate(layers) for n in layer}
|
|
298
|
+
for node in nodes:
|
|
299
|
+
node["layer"] = rank.get(node["id"], len(layers))
|
|
300
|
+
|
|
301
|
+
def groups(members):
|
|
302
|
+
# Deterministic disjoint cliques, deliberately not exponential
|
|
303
|
+
# maximum-antichain enumeration. Exclusive sites never share one.
|
|
304
|
+
buckets = []
|
|
305
|
+
for n in members:
|
|
306
|
+
for bucket in buckets:
|
|
307
|
+
if all(self.compatible(original[n], original[x]) for x in bucket):
|
|
308
|
+
bucket.append(n)
|
|
309
|
+
break
|
|
310
|
+
else:
|
|
311
|
+
buckets.append([n])
|
|
312
|
+
return [b for b in buckets if len(b) > 1]
|
|
313
|
+
|
|
314
|
+
parallel, batches = [], []
|
|
315
|
+
operations = {n["id"]: n["operation"] for n in nodes}
|
|
316
|
+
for i, layer in enumerate(layers):
|
|
317
|
+
parallel.extend({"layer": i, "sites": g} for g in groups(layer))
|
|
318
|
+
for op in sorted(set(operations[n] for n in layer)):
|
|
319
|
+
batches.extend({"layer": i, "operation": op, "sites": g}
|
|
320
|
+
for g in groups([n for n in layer if operations[n] == op]))
|
|
321
|
+
loops = []
|
|
322
|
+
for tid, alias in self.loops.items():
|
|
323
|
+
loop = self.tasks[tid]
|
|
324
|
+
members = [n["id"] for n in nodes if alias in n["loops"]]
|
|
325
|
+
carried = [e for e in edges if e["carried"] and e["source"] in members and e["target"] in members
|
|
326
|
+
and any(r["loop"] == alias for r in e["recurrences"])]
|
|
327
|
+
shared = [n["id"] for n in nodes if n["id"] in members and alias not in n["reuse_scope"] and n["reusable"]]
|
|
328
|
+
loops.append({"id": alias, "task": tid, "iterable": self.display(loop.iterable), "sites": members,
|
|
329
|
+
"serial": execution_mode in loop.serial_modes, "carried_infer_edges": len(carried),
|
|
330
|
+
"parallel_iterations": bool(members) and not carried and execution_mode not in loop.serial_modes,
|
|
331
|
+
"shared_sites": shared, "batch_sites": [n for n in members if n not in shared] if not carried and execution_mode not in loop.serial_modes else [],
|
|
332
|
+
"carried_values": [self.values[t.outputs[0]].name or t.outputs[0] for t in self.plan.tasks
|
|
333
|
+
if isinstance(t, p.Carry) and t.loop == tid]})
|
|
334
|
+
checks = [{"id": alias, "task": tid, "condition": self.display(self.tasks[tid].condition),
|
|
335
|
+
"message": self.display(self.tasks[tid].message) if self.tasks[tid].message else None}
|
|
336
|
+
for tid, alias in self.checks.items()]
|
|
337
|
+
notes = list(_NOTES)
|
|
338
|
+
speculation = {}
|
|
339
|
+
if mode == "speculative":
|
|
340
|
+
notes[1] = "依赖图保留正式执行的数据、控制和内存依赖;每条输入先完成本轮 Jev 决策,再合批已确认路径的 Qwen 任务。"
|
|
341
|
+
notes.append("Jev 可跨未决分支推测;Qwen 须确认路径。候选仍受参数、内存及循环窗口限制,交替依赖按轮执行。")
|
|
342
|
+
speculation["speculative_candidates"] = [n["id"] for n in nodes if n["reusable"]]
|
|
343
|
+
return {"entry": self.plan.entry, "mode": mode, "nodes": nodes, "edges": edges, "layers": layers,
|
|
344
|
+
"parallel": parallel, "batches": batches, "loops": loops, "checks": checks,
|
|
345
|
+
"unresolved_cycles": sorted(remaining), "notes": notes, **speculation,
|
|
346
|
+
"stats": {"tasks": len(self.plan.tasks), "infer_sites": len(nodes), "requests": len(self.plan.requests),
|
|
347
|
+
"dependencies": len(self.plan.dependencies_for(mode)), "infer_edges": len(edges)}}
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def analyze_plan(plan: p.ExecutionPlan, *, mode: p.FailureMode = "eager") -> dict:
|
|
351
|
+
"""Return detached, JSON-compatible inspection data, not a scheduling IR."""
|
|
352
|
+
if not isinstance(plan, p.ExecutionPlan):
|
|
353
|
+
raise TypeError("analyze_plan expects an ExecutionPlan")
|
|
354
|
+
plan.validate()
|
|
355
|
+
plan.dependencies_for(mode)
|
|
356
|
+
return _View(plan).analyze(mode)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _edge_label(edge):
|
|
360
|
+
label = "/".join(edge["kinds"])
|
|
361
|
+
if edge["hazards"]:
|
|
362
|
+
label += " " + "/".join(edge["hazards"])
|
|
363
|
+
if edge["carried"]:
|
|
364
|
+
label += " carried " + ",".join(f"{r['loop']}:+{r['distance']}" for r in edge["recurrences"])
|
|
365
|
+
if edge["conservative"]:
|
|
366
|
+
label += " (may)"
|
|
367
|
+
return label
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def format_plan(plan: p.ExecutionPlan, *, mode: p.FailureMode = "eager", show_source: bool = False) -> str:
|
|
371
|
+
data = analyze_plan(plan, mode=mode)
|
|
372
|
+
lines = [f"Execution Plan v{plan.version} · {plan.entry!r} · mode={mode}",
|
|
373
|
+
f"{len(data['nodes'])} Infer sites / {len(plan.requests)} request definitions / {len(plan.tasks)} tasks", "", "Infer 调用:"]
|
|
374
|
+
for n in data["nodes"]:
|
|
375
|
+
lines.append(f" {n['id']} {n['operation']} [{n['request']}] {n['label']}")
|
|
376
|
+
if n["loops"]:
|
|
377
|
+
lines.append(" loops: " + ", ".join(n["loops"]))
|
|
378
|
+
for gate in n["gates"]:
|
|
379
|
+
lines.append(" gate: " + gate)
|
|
380
|
+
if show_source:
|
|
381
|
+
lines.append(f" [{n['task']}] {n['source']}")
|
|
382
|
+
lines.extend(" inlined from " + call for call in n["calls"])
|
|
383
|
+
if mode == "speculative":
|
|
384
|
+
lines.extend(("", "两阶段推测候选(先 Jev 跨未决分支决策,再 Qwen 按已确认路径合批):",
|
|
385
|
+
" " + ", ".join(data["speculative_candidates"])))
|
|
386
|
+
lines.extend(("", "Infer 依赖(隐藏同轮传递边;完整路径可在 HTML 中查看):"))
|
|
387
|
+
for e in data["edges"]:
|
|
388
|
+
if not e["redundant"] or e["carried"]:
|
|
389
|
+
lines.append(f" {e['source']} -> {e['target']} [{_edge_label(e)}]")
|
|
390
|
+
if not data["edges"]:
|
|
391
|
+
lines.append(" 无 Infer 间依赖。仍需满足各自的激活条件和非 Infer 前置任务。")
|
|
392
|
+
lines.extend(("", "并行候选(按依赖层;不是执行波次):"))
|
|
393
|
+
lines.extend(f" 层 {g['layer']}: {', '.join(g['sites'])}" for g in data["parallel"])
|
|
394
|
+
if not data["parallel"]:
|
|
395
|
+
lines.append(" 未发现同轮静态候选组。")
|
|
396
|
+
lines.extend(("", "批处理候选(需要后端兼容且同时就绪):"))
|
|
397
|
+
lines.extend(f" {g['operation']} · 层 {g['layer']}: {', '.join(g['sites'])}" for g in data["batches"])
|
|
398
|
+
if not data["batches"]:
|
|
399
|
+
lines.append(" 未发现同轮静态候选组。")
|
|
400
|
+
lines.extend(("", "循环:"))
|
|
401
|
+
for loop in data["loops"]:
|
|
402
|
+
if not loop["sites"]:
|
|
403
|
+
continue
|
|
404
|
+
status = "可跨迭代并行" if loop["parallel_iterations"] else "存在迭代顺序限制"
|
|
405
|
+
lines.append(f" {loop['id']} {', '.join(loop['sites'])}: {status}; {loop['iterable']}")
|
|
406
|
+
if loop["batch_sites"]:
|
|
407
|
+
lines.append(" 跨迭代批处理候选: " + ", ".join(loop["batch_sites"]))
|
|
408
|
+
if loop["shared_sites"]:
|
|
409
|
+
lines.append(" 跨迭代共享请求(不是多次调用待批处理): " + ", ".join(loop["shared_sites"]))
|
|
410
|
+
if loop["carried_values"]:
|
|
411
|
+
lines.append(" 携带值: " + ", ".join(loop["carried_values"]))
|
|
412
|
+
lines.extend(("", "约束检查(strict 顺序屏障):" if mode == "strict" else "约束检查(成功返回前必须通过):"))
|
|
413
|
+
lines.extend(f" {c['id']}: {c['condition']}" for c in data["checks"])
|
|
414
|
+
if data["unresolved_cycles"]:
|
|
415
|
+
lines.append("未解析的同轮环(不据此推断并行): " + ", ".join(data["unresolved_cycles"]))
|
|
416
|
+
lines.extend(("", *data["notes"], "HTML: python3 -m annoscript.inspect PLAN.json --format html -o plan.html"))
|
|
417
|
+
return "\n".join(lines) + "\n"
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def format_plan_mermaid(plan: p.ExecutionPlan, *, mode: p.FailureMode = "eager", show_source: bool = False) -> str:
|
|
421
|
+
data = analyze_plan(plan, mode=mode)
|
|
422
|
+
def quote(text):
|
|
423
|
+
# Mermaid quoted labels use decimal entities for delimiters. No source
|
|
424
|
+
# text is allowed to inject a Mermaid statement, URL or HTML element.
|
|
425
|
+
return "".join(c if c.isalnum() or c in " ,.:_-/+" else f"#{ord(c)};" for c in text)
|
|
426
|
+
lines = ["flowchart LR", f" %% Infer dependency projection, mode={mode}; conditional paths are a union."]
|
|
427
|
+
for n in data["nodes"]:
|
|
428
|
+
text = f"{n['id']} {n['operation']} {n['label']}"
|
|
429
|
+
if n["loops"]:
|
|
430
|
+
text += " · " + ",".join(n["loops"])
|
|
431
|
+
if show_source:
|
|
432
|
+
text += " · " + n["source"]
|
|
433
|
+
lines.append(f' {n["id"]}["{quote(text)}"]')
|
|
434
|
+
for e in data["edges"]:
|
|
435
|
+
if not e["redundant"] or e["carried"]:
|
|
436
|
+
arrow = "-.->" if e["carried"] else "-->"
|
|
437
|
+
lines.append(f' {e["source"]} {arrow}|"{quote(_edge_label(e))}"| {e["target"]}')
|
|
438
|
+
return "\n".join(lines) + "\n"
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def format_plan_html(plan: p.ExecutionPlan, *, mode: p.FailureMode = "eager", show_source: bool = False) -> str:
|
|
442
|
+
plan.validate()
|
|
443
|
+
plan.dependencies_for(mode)
|
|
444
|
+
view = _View(plan)
|
|
445
|
+
data = {"eager": view.analyze("eager"), "strict": view.analyze("strict"),
|
|
446
|
+
"speculative": view.analyze("speculative"), "initial_mode": mode,
|
|
447
|
+
"show_source": show_source}
|
|
448
|
+
# JSON in an inert script still needs escaping: a prompt can contain </script>.
|
|
449
|
+
payload = json.dumps(data, ensure_ascii=False, allow_nan=False).replace("&", "\\u0026").replace("<", "\\u003c").replace(">", "\\u003e")
|
|
450
|
+
template = Path(__file__).with_name("_plan_viewer.html").read_text(encoding="utf-8")
|
|
451
|
+
replacements = {"__PLAN_TITLE__": html.escape(plan.entry, quote=True), "__PLAN_DATA__": payload}
|
|
452
|
+
return re.sub(r"__PLAN_(?:TITLE|DATA)__", lambda match: replacements[match[0]], template)
|