@tensor-cad/mcp 0.1.0
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/README.md +217 -0
- package/bridge/protocol.d.ts +99 -0
- package/bridge/server.d.ts +106 -0
- package/bridge/session.d.ts +24 -0
- package/design-schema.d.ts +198 -0
- package/index.d.ts +16 -0
- package/index.js +2874 -0
- package/mcp.example.json +12 -0
- package/ops.d.ts +69 -0
- package/package.json +37 -0
- package/prompts.d.ts +12 -0
- package/resources.d.ts +11 -0
- package/schemas.d.ts +291 -0
- package/serve.d.ts +11 -0
- package/serve.js +2867 -0
- package/server.d.ts +31 -0
- package/server.json +32 -0
- package/stdio.d.ts +10 -0
- package/stdio.js +2869 -0
- package/store/file-store.d.ts +58 -0
- package/store/types.d.ts +119 -0
- package/summarize.d.ts +128 -0
- package/tools.d.ts +12 -0
package/serve.js
ADDED
|
@@ -0,0 +1,2867 @@
|
|
|
1
|
+
// packages/mcp/src/serve.ts
|
|
2
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
3
|
+
import { loadEngine } from "@tensor-cad/engine/node";
|
|
4
|
+
|
|
5
|
+
// packages/mcp/src/bridge/server.ts
|
|
6
|
+
import { createServer } from "node:http";
|
|
7
|
+
import { randomBytes } from "node:crypto";
|
|
8
|
+
import { WebSocketServer } from "ws";
|
|
9
|
+
|
|
10
|
+
// packages/mcp/src/schemas.ts
|
|
11
|
+
import * as z from "zod";
|
|
12
|
+
var DESIGN_ID = z.string().describe('Handle returned by tensorcad_new_design or tensorcad_open_design, e.g. "dsn_1".');
|
|
13
|
+
var Severity = z.enum(["error", "warning", "info"]);
|
|
14
|
+
var Finding = z.object({
|
|
15
|
+
rule: z.string().describe('Stable rule id, e.g. "flash-head-dim".'),
|
|
16
|
+
severity: Severity,
|
|
17
|
+
path: z.string().optional().describe("Block path the finding is about."),
|
|
18
|
+
port: z.string().optional(),
|
|
19
|
+
message: z.string(),
|
|
20
|
+
hint: z.string().optional().describe("What to change to clear the finding.")
|
|
21
|
+
});
|
|
22
|
+
var Counts = z.object({
|
|
23
|
+
error: z.number().int(),
|
|
24
|
+
warning: z.number().int(),
|
|
25
|
+
info: z.number().int()
|
|
26
|
+
});
|
|
27
|
+
var ValidationSummary = z.object({
|
|
28
|
+
ok: z.boolean().describe("True when nothing blocks building this design."),
|
|
29
|
+
counts: Counts,
|
|
30
|
+
top_findings: z.array(Finding).describe("The worst few findings. Call tensorcad_validate for all of them.")
|
|
31
|
+
});
|
|
32
|
+
var DesignSummary = z.object({
|
|
33
|
+
design_id: z.string(),
|
|
34
|
+
name: z.string(),
|
|
35
|
+
revision: z.number().int(),
|
|
36
|
+
path: z.string().optional(),
|
|
37
|
+
source: z.enum(["preset", "file", "empty"]),
|
|
38
|
+
dirty: z.boolean(),
|
|
39
|
+
created_at: z.string(),
|
|
40
|
+
updated_at: z.string()
|
|
41
|
+
});
|
|
42
|
+
var OutlineSymbol = z.object({
|
|
43
|
+
name: z.string(),
|
|
44
|
+
kind: z.enum(["design", "runtime"]),
|
|
45
|
+
value: z.string().describe("Literal or expression as written in the document."),
|
|
46
|
+
resolved: z.number().optional(),
|
|
47
|
+
doc: z.string().optional()
|
|
48
|
+
});
|
|
49
|
+
var OutlineBlock = z.object({
|
|
50
|
+
path: z.string().describe('Slash-separated path, e.g. "layers/block".'),
|
|
51
|
+
type: z.string(),
|
|
52
|
+
kind: z.string().describe("primitive, composite, container or unknown."),
|
|
53
|
+
depth: z.number().int(),
|
|
54
|
+
label: z.string().optional(),
|
|
55
|
+
params: z.number().describe("Trainable parameters under this path, repeats included."),
|
|
56
|
+
repeat: z.number().optional().describe("Instance count, for repeat containers.")
|
|
57
|
+
});
|
|
58
|
+
var OutlineEdge = z.object({
|
|
59
|
+
graph: z.string().describe('Container path holding the edge; "" is the root graph.'),
|
|
60
|
+
from: z.string(),
|
|
61
|
+
to: z.string(),
|
|
62
|
+
shape: z.string().optional().describe('Inferred shape on the wire, e.g. "B T D".')
|
|
63
|
+
});
|
|
64
|
+
var Outline = z.object({
|
|
65
|
+
name: z.string(),
|
|
66
|
+
family: z.string().optional(),
|
|
67
|
+
notes: z.string().optional(),
|
|
68
|
+
symbols: z.array(OutlineSymbol),
|
|
69
|
+
blocks: z.array(OutlineBlock),
|
|
70
|
+
edges: z.array(OutlineEdge),
|
|
71
|
+
params_total: z.number(),
|
|
72
|
+
params_active: z.number(),
|
|
73
|
+
issues: z.number().int()
|
|
74
|
+
});
|
|
75
|
+
var BlockPort = z.object({
|
|
76
|
+
name: z.string(),
|
|
77
|
+
pattern: z.string().describe('Declared shape pattern, e.g. "B T D".'),
|
|
78
|
+
shape: z.string().optional().describe("Shape actually inferred for this port."),
|
|
79
|
+
dtype: z.string().optional().describe("What the tensor carries, when the port declares it rather than inheriting it."),
|
|
80
|
+
optional: z.boolean().optional().describe("True when this port may legitimately dangle."),
|
|
81
|
+
connected_to: z.array(z.string()).optional()
|
|
82
|
+
});
|
|
83
|
+
var analysisOptionsShape = {
|
|
84
|
+
T: z.number().int().positive().optional().describe("Sequence length. Defaults to the document's own T."),
|
|
85
|
+
B: z.number().int().positive().optional().describe("Micro-batch size."),
|
|
86
|
+
dtype: z.enum(["fp32", "bf16", "fp16", "fp8"]).optional().describe("Training dtype. Default bf16."),
|
|
87
|
+
hardware: z.string().optional().describe("Hardware id: h100-sxm, h200-sxm, b200, a100-80, rtx5080 or rtx4090. Default h100-sxm."),
|
|
88
|
+
gpus: z.number().int().positive().optional(),
|
|
89
|
+
tokens: z.number().positive().optional().describe("Training token budget. Defaults to Chinchilla-optimal."),
|
|
90
|
+
optimizer: z.enum(["adamw", "adamw8bit", "muon", "sgd_momentum", "sgd", "bf16_adam"]).optional(),
|
|
91
|
+
recompute: z.enum(["none", "selective", "full"]).optional(),
|
|
92
|
+
zero: z.number().int().min(0).max(3).optional().describe("ZeRO/FSDP sharding stage."),
|
|
93
|
+
tp: z.number().int().positive().optional().describe("Tensor parallel degree."),
|
|
94
|
+
dp: z.number().int().positive().optional().describe("Data parallel degree."),
|
|
95
|
+
pp: z.number().int().positive().optional().describe("Pipeline parallel degree."),
|
|
96
|
+
ep: z.number().int().positive().optional().describe("Expert parallel degree."),
|
|
97
|
+
concurrency: z.number().int().positive().optional().describe("Concurrent sequences when serving."),
|
|
98
|
+
mfu: z.number().positive().max(1).optional().describe("Model FLOPs utilization, 0..1.")
|
|
99
|
+
};
|
|
100
|
+
var ParamValue = z.any().describe("A number, an expression string over the design symbols, a boolean, null, or an object.");
|
|
101
|
+
var Op = z.discriminatedUnion("op", [
|
|
102
|
+
z.object({
|
|
103
|
+
op: z.literal("add_node"),
|
|
104
|
+
parent: z.string().optional().describe("Container path to add into. Omit for the root graph."),
|
|
105
|
+
id: z.string().describe("New block id, unique within its graph."),
|
|
106
|
+
type: z.string().describe("Catalog block type; see tensorcad_search_catalog."),
|
|
107
|
+
params: z.record(z.string(), ParamValue).optional(),
|
|
108
|
+
label: z.string().optional()
|
|
109
|
+
}),
|
|
110
|
+
z.object({ op: z.literal("remove_node"), path: z.string().describe("Block path. Its edges go with it.") }),
|
|
111
|
+
z.object({
|
|
112
|
+
op: z.literal("set_param"),
|
|
113
|
+
path: z.string(),
|
|
114
|
+
key: z.string(),
|
|
115
|
+
value: ParamValue
|
|
116
|
+
}),
|
|
117
|
+
z.object({
|
|
118
|
+
op: z.literal("connect"),
|
|
119
|
+
graph: z.string().optional().describe("Container path holding the edge. Omit for the root graph."),
|
|
120
|
+
from: z.string().describe('Producer endpoint, "blockId:port", local to that graph.'),
|
|
121
|
+
to: z.string().describe('Consumer endpoint, "blockId:port", local to that graph.')
|
|
122
|
+
}),
|
|
123
|
+
z.object({
|
|
124
|
+
op: z.literal("disconnect"),
|
|
125
|
+
graph: z.string().optional(),
|
|
126
|
+
from: z.string(),
|
|
127
|
+
to: z.string()
|
|
128
|
+
}),
|
|
129
|
+
z.object({
|
|
130
|
+
op: z.literal("set_symbol"),
|
|
131
|
+
name: z.string(),
|
|
132
|
+
value: z.union([z.number(), z.string(), z.null()]).describe("Number, expression over earlier symbols, or null to delete the symbol."),
|
|
133
|
+
doc: z.string().optional(),
|
|
134
|
+
runtime: z.boolean().optional().describe("Keep the symbol indeterminate (B, T). Defaults to the current kind.")
|
|
135
|
+
}),
|
|
136
|
+
z.object({ op: z.literal("rename"), path: z.string(), id: z.string().describe("New id; edges are rewritten.") }),
|
|
137
|
+
z.object({
|
|
138
|
+
op: z.literal("set_label"),
|
|
139
|
+
path: z.string(),
|
|
140
|
+
label: z.string().nullable().optional().describe("Null or empty clears the label.")
|
|
141
|
+
})
|
|
142
|
+
]);
|
|
143
|
+
var num = () => z.number().nullable();
|
|
144
|
+
var AnalysisOutput = z.object({
|
|
145
|
+
design_id: z.string(),
|
|
146
|
+
revision: z.number().int(),
|
|
147
|
+
name: z.string(),
|
|
148
|
+
options: z.object({
|
|
149
|
+
T: z.number(),
|
|
150
|
+
B: z.number(),
|
|
151
|
+
dtype: z.string(),
|
|
152
|
+
hardware: z.string(),
|
|
153
|
+
gpus: z.number(),
|
|
154
|
+
parallel: z.object({
|
|
155
|
+
dp: z.number(),
|
|
156
|
+
tp: z.number(),
|
|
157
|
+
pp: z.number(),
|
|
158
|
+
ep: z.number(),
|
|
159
|
+
zero: z.number(),
|
|
160
|
+
sequenceParallel: z.boolean()
|
|
161
|
+
}),
|
|
162
|
+
optimizer: z.string(),
|
|
163
|
+
recompute: z.string(),
|
|
164
|
+
tokens: z.number(),
|
|
165
|
+
tokens_were_defaulted: z.boolean(),
|
|
166
|
+
mfu: z.number(),
|
|
167
|
+
concurrency: z.number()
|
|
168
|
+
}),
|
|
169
|
+
params: z.object({
|
|
170
|
+
total: z.number(),
|
|
171
|
+
active: z.number(),
|
|
172
|
+
embedding: z.number(),
|
|
173
|
+
head: z.number(),
|
|
174
|
+
non_embedding: z.number(),
|
|
175
|
+
non_embedding_active: z.number(),
|
|
176
|
+
by_category: z.record(z.string(), z.number()),
|
|
177
|
+
by_type: z.record(z.string(), z.number())
|
|
178
|
+
}),
|
|
179
|
+
flops: z.object({
|
|
180
|
+
fwd_dense: num(),
|
|
181
|
+
fwd_attention: num(),
|
|
182
|
+
fwd_total: num(),
|
|
183
|
+
elementwise: num(),
|
|
184
|
+
train_per_token: num(),
|
|
185
|
+
attention_share: num()
|
|
186
|
+
}),
|
|
187
|
+
kv: z.object({
|
|
188
|
+
bytes_per_token: num(),
|
|
189
|
+
bytes_per_sequence: num(),
|
|
190
|
+
bytes_per_token_decompressed: num().describe("What an engine that does not absorb latent attention's up-projections caches instead; equal to bytes_per_token otherwise.")
|
|
191
|
+
}),
|
|
192
|
+
memory: z.object({
|
|
193
|
+
optimizer_label: z.string(),
|
|
194
|
+
train_weights: num(),
|
|
195
|
+
train_grads: num(),
|
|
196
|
+
train_optimizer: num(),
|
|
197
|
+
train_activations: num(),
|
|
198
|
+
train_per_gpu: num(),
|
|
199
|
+
train_total: num(),
|
|
200
|
+
infer_weights: num(),
|
|
201
|
+
infer_kv: num(),
|
|
202
|
+
infer_total: num(),
|
|
203
|
+
device_memory: num(),
|
|
204
|
+
notes: z.array(z.string())
|
|
205
|
+
}),
|
|
206
|
+
throughput: z.object({
|
|
207
|
+
decode_tokens_per_second: num(),
|
|
208
|
+
decode_weight_bytes: num().describe("What a step at this batch reads: for a mixture of experts, the union of what its tokens routed to."),
|
|
209
|
+
resident_weight_bytes: num().describe("Every weight the device holds, read or not."),
|
|
210
|
+
prefill_seconds: num(),
|
|
211
|
+
memory_bound: z.boolean(),
|
|
212
|
+
notes: z.array(z.string())
|
|
213
|
+
}),
|
|
214
|
+
cost: z.object({
|
|
215
|
+
total_flops: num(),
|
|
216
|
+
gpu_hours: num(),
|
|
217
|
+
wall_clock_hours: num(),
|
|
218
|
+
dollars: num(),
|
|
219
|
+
tokens: z.number()
|
|
220
|
+
}),
|
|
221
|
+
chinchilla: z.object({
|
|
222
|
+
optimal_tokens: num(),
|
|
223
|
+
tokens_per_param: num(),
|
|
224
|
+
over_training_ratio: num(),
|
|
225
|
+
verdict: z.string()
|
|
226
|
+
}),
|
|
227
|
+
errors: z.array(z.string())
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// packages/mcp/src/store/types.ts
|
|
231
|
+
class RevisionConflictError extends Error {
|
|
232
|
+
designId;
|
|
233
|
+
expected;
|
|
234
|
+
actual;
|
|
235
|
+
constructor(designId, expected, actual) {
|
|
236
|
+
super(`Design ${designId} is at revision ${actual}, not the expected ${expected}. ` + `Re-read it with tensorcad_get_design and rebuild the edit on the current document.`);
|
|
237
|
+
this.designId = designId;
|
|
238
|
+
this.expected = expected;
|
|
239
|
+
this.actual = actual;
|
|
240
|
+
this.name = "RevisionConflictError";
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
class UnknownDesignError extends Error {
|
|
245
|
+
constructor(id, known) {
|
|
246
|
+
super(`Unknown design_id "${id}". Open designs: ${known.length > 0 ? known.join(", ") : "(none)"}. ` + `Use tensorcad_new_design or tensorcad_open_design first.`);
|
|
247
|
+
this.name = "UnknownDesignError";
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// packages/mcp/src/bridge/protocol.ts
|
|
252
|
+
var BRIDGE_PROTOCOL = 1;
|
|
253
|
+
|
|
254
|
+
// packages/mcp/src/bridge/session.ts
|
|
255
|
+
import { rmSync } from "node:fs";
|
|
256
|
+
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
257
|
+
import { homedir } from "node:os";
|
|
258
|
+
import { dirname, join } from "node:path";
|
|
259
|
+
var SESSION_VERSION = 1;
|
|
260
|
+
function sessionPath() {
|
|
261
|
+
return process.env.TENSORCAD_SESSION_FILE ?? join(homedir(), ".tensorcad", "session.json");
|
|
262
|
+
}
|
|
263
|
+
async function writeSession(session, path = sessionPath()) {
|
|
264
|
+
const full = { version: SESSION_VERSION, protocol: BRIDGE_PROTOCOL, ...session };
|
|
265
|
+
await mkdir(dirname(path), { recursive: true });
|
|
266
|
+
await writeFile(path, `${JSON.stringify(full, null, 2)}
|
|
267
|
+
`, "utf8");
|
|
268
|
+
try {
|
|
269
|
+
await chmod(path, 384);
|
|
270
|
+
} catch {}
|
|
271
|
+
}
|
|
272
|
+
async function clearSession(path = sessionPath()) {
|
|
273
|
+
await rm(path, { force: true });
|
|
274
|
+
}
|
|
275
|
+
function clearSessionSync(path = sessionPath()) {
|
|
276
|
+
try {
|
|
277
|
+
rmSync(path, { force: true });
|
|
278
|
+
} catch {}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// packages/mcp/src/bridge/server.ts
|
|
282
|
+
var DEFAULT_BRIDGE_PORT = 7357;
|
|
283
|
+
var PORT_ATTEMPTS = 4;
|
|
284
|
+
|
|
285
|
+
class BridgeServer {
|
|
286
|
+
token;
|
|
287
|
+
http;
|
|
288
|
+
wss;
|
|
289
|
+
options;
|
|
290
|
+
log;
|
|
291
|
+
unsubscribe;
|
|
292
|
+
watchers = new Set;
|
|
293
|
+
port_ = 0;
|
|
294
|
+
acting;
|
|
295
|
+
constructor(options) {
|
|
296
|
+
this.options = options;
|
|
297
|
+
this.token = options.token ?? randomBytes(32).toString("hex");
|
|
298
|
+
this.log = options.log ?? ((line) => process.stderr.write(`${line}
|
|
299
|
+
`));
|
|
300
|
+
this.http = createServer((req, res) => this.serveHttp(req, res));
|
|
301
|
+
this.wss = new WebSocketServer({ noServer: true });
|
|
302
|
+
this.http.on("upgrade", (req, socket, head) => {
|
|
303
|
+
const refusal = this.refuse(req, req.url ?? "/");
|
|
304
|
+
if (refusal) {
|
|
305
|
+
socket.end(`HTTP/1.1 ${refusal}\r
|
|
306
|
+
Connection: close\r
|
|
307
|
+
Content-Length: 0\r
|
|
308
|
+
\r
|
|
309
|
+
`);
|
|
310
|
+
setTimeout(() => socket.destroy(), 50).unref();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
this.wss.handleUpgrade(req, socket, head, (ws) => this.attach(ws));
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
get port() {
|
|
317
|
+
return this.port_;
|
|
318
|
+
}
|
|
319
|
+
get url() {
|
|
320
|
+
return `ws://127.0.0.1:${this.port_}/bridge?token=${this.token}`;
|
|
321
|
+
}
|
|
322
|
+
get connections() {
|
|
323
|
+
return this.wss.clients.size;
|
|
324
|
+
}
|
|
325
|
+
watch(watcher) {
|
|
326
|
+
this.watchers.add(watcher);
|
|
327
|
+
return () => this.watchers.delete(watcher);
|
|
328
|
+
}
|
|
329
|
+
async start() {
|
|
330
|
+
const first = this.options.port ?? DEFAULT_BRIDGE_PORT;
|
|
331
|
+
this.port_ = await listenSomewhere(this.http, first, PORT_ATTEMPTS);
|
|
332
|
+
this.unsubscribe = this.options.store.subscribe((change) => this.mirror(change));
|
|
333
|
+
await writeSession({
|
|
334
|
+
port: this.port_,
|
|
335
|
+
token: this.token,
|
|
336
|
+
pid: process.pid,
|
|
337
|
+
root: this.options.root,
|
|
338
|
+
started_at: new Date().toISOString()
|
|
339
|
+
}, this.options.sessionFile);
|
|
340
|
+
this.log(`tensorcad bridge on 127.0.0.1:${this.port_}`);
|
|
341
|
+
}
|
|
342
|
+
async stop() {
|
|
343
|
+
this.unsubscribe?.();
|
|
344
|
+
this.unsubscribe = undefined;
|
|
345
|
+
for (const client of this.wss.clients)
|
|
346
|
+
client.close(1001, "server stopping");
|
|
347
|
+
await new Promise((done) => this.wss.close(() => done()));
|
|
348
|
+
this.http.closeAllConnections();
|
|
349
|
+
await new Promise((done) => this.http.close(() => done()));
|
|
350
|
+
await clearSession(this.options.sessionFile);
|
|
351
|
+
}
|
|
352
|
+
serveHttp(req, res) {
|
|
353
|
+
const path = (req.url ?? "/").split("?")[0];
|
|
354
|
+
if (path !== "/session") {
|
|
355
|
+
res.writeHead(404, { "content-type": "text/plain" });
|
|
356
|
+
res.end(`tensorcad bridge
|
|
357
|
+
`);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const refusal = this.refuse(req, req.url ?? "/", { token: false });
|
|
361
|
+
if (refusal) {
|
|
362
|
+
res.writeHead(Number(refusal.split(" ")[0]), { "content-type": "text/plain" });
|
|
363
|
+
res.end(`${refusal}
|
|
364
|
+
`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
res.writeHead(200, {
|
|
368
|
+
"content-type": "application/json",
|
|
369
|
+
"access-control-allow-origin": req.headers.origin ?? "*"
|
|
370
|
+
});
|
|
371
|
+
res.end(JSON.stringify({ protocol: BRIDGE_PROTOCOL, port: this.port_, token: this.token }));
|
|
372
|
+
}
|
|
373
|
+
refuse(req, url, checks = { token: true }) {
|
|
374
|
+
const remote = req.socket.remoteAddress ?? "";
|
|
375
|
+
if (!isLoopback(remote))
|
|
376
|
+
return "403 Forbidden";
|
|
377
|
+
const origin = req.headers.origin;
|
|
378
|
+
if (origin !== undefined && !isLocalOrigin(origin))
|
|
379
|
+
return "403 Forbidden";
|
|
380
|
+
if (checks.token) {
|
|
381
|
+
const supplied = new URL(url, "http://127.0.0.1").searchParams.get("token");
|
|
382
|
+
if (supplied !== this.token)
|
|
383
|
+
return "401 Unauthorized";
|
|
384
|
+
}
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
attach(ws) {
|
|
388
|
+
this.send(ws, {
|
|
389
|
+
type: "hello",
|
|
390
|
+
protocol: BRIDGE_PROTOCOL,
|
|
391
|
+
server: this.options.name,
|
|
392
|
+
version: this.options.version,
|
|
393
|
+
root: this.options.root,
|
|
394
|
+
designs: this.options.store.list()
|
|
395
|
+
});
|
|
396
|
+
ws.on("message", (raw) => {
|
|
397
|
+
let message;
|
|
398
|
+
try {
|
|
399
|
+
message = JSON.parse(String(raw));
|
|
400
|
+
} catch (e) {
|
|
401
|
+
this.send(ws, { type: "error", message: `not JSON: ${e.message}` });
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
this.handle(ws, message);
|
|
406
|
+
} catch (e) {
|
|
407
|
+
this.send(ws, { type: "error", message: e.message, about: message?.type });
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
handle(ws, message) {
|
|
412
|
+
switch (message?.type) {
|
|
413
|
+
case "publish": {
|
|
414
|
+
const doc = asDocument(message.doc);
|
|
415
|
+
const record = this.during(ws, () => this.options.store.adopt(doc));
|
|
416
|
+
this.send(ws, designMessage(record, "published"));
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
case "attach": {
|
|
420
|
+
const record = this.options.store.get(message.design_id);
|
|
421
|
+
this.send(ws, designMessage(record, "requested"));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
case "ops":
|
|
425
|
+
case "replace": {
|
|
426
|
+
const write = message.type === "ops" ? () => this.options.store.apply(message.design_id, parseOps(message.ops), message.revision) : () => this.options.store.replace(message.design_id, asDocument(message.doc), message.revision);
|
|
427
|
+
try {
|
|
428
|
+
const { record } = this.during(ws, write);
|
|
429
|
+
this.send(ws, designMessage(record, message.type === "ops" ? "applied" : "replaced"));
|
|
430
|
+
} catch (e) {
|
|
431
|
+
if (e instanceof RevisionConflictError) {
|
|
432
|
+
this.send(ws, { type: "error", message: e.message, about: message.type });
|
|
433
|
+
this.send(ws, designMessage(this.options.store.get(message.design_id), "requested"));
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
throw e;
|
|
437
|
+
}
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
default:
|
|
441
|
+
this.send(ws, {
|
|
442
|
+
type: "error",
|
|
443
|
+
message: `unknown message type ${JSON.stringify(message?.type)}`
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
mirror(change) {
|
|
448
|
+
const from = this.acting ? "editor" : "agent";
|
|
449
|
+
const message = designMessage(change.record, change.kind, change.ops);
|
|
450
|
+
for (const client of this.wss.clients) {
|
|
451
|
+
if (client === this.acting)
|
|
452
|
+
continue;
|
|
453
|
+
this.send(client, message);
|
|
454
|
+
}
|
|
455
|
+
for (const watcher of this.watchers) {
|
|
456
|
+
try {
|
|
457
|
+
watcher(change, from);
|
|
458
|
+
} catch (e) {
|
|
459
|
+
this.log(`tensorcad bridge: watcher failed: ${e.message}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
during(ws, work) {
|
|
464
|
+
this.acting = ws;
|
|
465
|
+
try {
|
|
466
|
+
return work();
|
|
467
|
+
} finally {
|
|
468
|
+
this.acting = undefined;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
send(ws, message) {
|
|
472
|
+
if (ws.readyState !== ws.OPEN)
|
|
473
|
+
return;
|
|
474
|
+
ws.send(JSON.stringify(message));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function designMessage(record, reason, ops) {
|
|
478
|
+
const { doc, ...design } = record;
|
|
479
|
+
const message = { type: "design", reason, design, doc };
|
|
480
|
+
if (ops)
|
|
481
|
+
message.ops = ops;
|
|
482
|
+
return message;
|
|
483
|
+
}
|
|
484
|
+
async function listenSomewhere(server, first, attempts) {
|
|
485
|
+
let lastError;
|
|
486
|
+
for (let port = first;port < first + attempts; port++) {
|
|
487
|
+
try {
|
|
488
|
+
await new Promise((resolve, reject) => {
|
|
489
|
+
const onError = (e) => {
|
|
490
|
+
server.removeListener("listening", onListening);
|
|
491
|
+
reject(e);
|
|
492
|
+
};
|
|
493
|
+
const onListening = () => {
|
|
494
|
+
server.removeListener("error", onError);
|
|
495
|
+
resolve();
|
|
496
|
+
};
|
|
497
|
+
server.once("error", onError);
|
|
498
|
+
server.once("listening", onListening);
|
|
499
|
+
server.listen(port, "127.0.0.1");
|
|
500
|
+
});
|
|
501
|
+
return port;
|
|
502
|
+
} catch (e) {
|
|
503
|
+
lastError = e;
|
|
504
|
+
if (e.code !== "EADDRINUSE")
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
throw new Error(`tensorcad bridge could not listen on 127.0.0.1:${first}..${first + attempts - 1}: ${lastError?.message}`);
|
|
509
|
+
}
|
|
510
|
+
function isLoopback(address) {
|
|
511
|
+
const bare = address.startsWith("::ffff:") ? address.slice("::ffff:".length) : address;
|
|
512
|
+
return bare === "127.0.0.1" || bare === "::1" || bare.startsWith("127.");
|
|
513
|
+
}
|
|
514
|
+
function isLocalOrigin(origin) {
|
|
515
|
+
if (origin === "null" || origin === "file://")
|
|
516
|
+
return true;
|
|
517
|
+
let url;
|
|
518
|
+
try {
|
|
519
|
+
url = new URL(origin);
|
|
520
|
+
} catch {
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
if (url.protocol === "wails:" || url.protocol === "file:")
|
|
524
|
+
return true;
|
|
525
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
526
|
+
return false;
|
|
527
|
+
const host = url.hostname;
|
|
528
|
+
return host === "localhost" || host.endsWith(".localhost") || host === "127.0.0.1" || host === "[::1]" || host === "::1";
|
|
529
|
+
}
|
|
530
|
+
function asDocument(value) {
|
|
531
|
+
const doc = value;
|
|
532
|
+
if (!doc || typeof doc !== "object" || !doc.graph || !doc.meta) {
|
|
533
|
+
throw new Error(`published value is not a design document: it has no "meta" and "graph".`);
|
|
534
|
+
}
|
|
535
|
+
return doc;
|
|
536
|
+
}
|
|
537
|
+
function parseOps(value) {
|
|
538
|
+
if (!Array.isArray(value))
|
|
539
|
+
throw new Error("ops must be an array");
|
|
540
|
+
const parsed = Op.array().safeParse(value);
|
|
541
|
+
if (!parsed.success) {
|
|
542
|
+
throw new Error(`ops rejected: ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`);
|
|
543
|
+
}
|
|
544
|
+
return parsed.data;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// packages/mcp/src/server.ts
|
|
548
|
+
import { McpServer as McpServer2 } from "@modelcontextprotocol/server";
|
|
549
|
+
// packages/mcp/package.json
|
|
550
|
+
var package_default = {
|
|
551
|
+
name: "@tensor-cad/mcp",
|
|
552
|
+
version: "0.1.0",
|
|
553
|
+
description: "Model Context Protocol server for TensorCAD: design, validate, analyze and generate LLM architectures from an agent",
|
|
554
|
+
mcpName: "io.github.filip-pajalic/tensorcad",
|
|
555
|
+
type: "module",
|
|
556
|
+
bin: {
|
|
557
|
+
"tensorcad-mcp": "./src/stdio.ts"
|
|
558
|
+
},
|
|
559
|
+
main: "./src/index.ts",
|
|
560
|
+
exports: {
|
|
561
|
+
".": "./src/index.ts",
|
|
562
|
+
"./server": "./src/server.ts"
|
|
563
|
+
},
|
|
564
|
+
files: [
|
|
565
|
+
"src",
|
|
566
|
+
"README.md",
|
|
567
|
+
"mcp.example.json"
|
|
568
|
+
],
|
|
569
|
+
scripts: {
|
|
570
|
+
start: "bun run src/stdio.ts",
|
|
571
|
+
test: "bun test ../../packages/mcp/test",
|
|
572
|
+
typecheck: "tsc --noEmit"
|
|
573
|
+
},
|
|
574
|
+
dependencies: {
|
|
575
|
+
"@modelcontextprotocol/server": "^2.0.0",
|
|
576
|
+
"@tensor-cad/engine": "workspace:*",
|
|
577
|
+
ws: "^8.21.3",
|
|
578
|
+
zod: "^4.2.0"
|
|
579
|
+
},
|
|
580
|
+
devDependencies: {
|
|
581
|
+
"@modelcontextprotocol/client": "^2.0.0",
|
|
582
|
+
"@types/ws": "^8.18.1"
|
|
583
|
+
},
|
|
584
|
+
license: "MIT"
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// packages/mcp/src/store/file-store.ts
|
|
588
|
+
import { readdir, readFile as readFile2, stat, writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
|
|
589
|
+
import { dirname as dirname2, extname, isAbsolute, join as join2, resolve } from "node:path";
|
|
590
|
+
|
|
591
|
+
// packages/mcp/src/ops.ts
|
|
592
|
+
import { joinPath, splitEndpoint } from "@tensor-cad/engine";
|
|
593
|
+
import { getBlock } from "@tensor-cad/engine/node";
|
|
594
|
+
|
|
595
|
+
class OpError extends Error {
|
|
596
|
+
index;
|
|
597
|
+
op;
|
|
598
|
+
constructor(index, op, message) {
|
|
599
|
+
super(`op ${index} (${op.op}): ${message}`);
|
|
600
|
+
this.index = index;
|
|
601
|
+
this.op = op;
|
|
602
|
+
this.name = "OpError";
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
function locate(doc, path) {
|
|
606
|
+
const segments = path.split("/").filter(Boolean);
|
|
607
|
+
if (segments.length === 0)
|
|
608
|
+
throw new Error(`"${path}" is not a node path`);
|
|
609
|
+
let graph = doc.graph;
|
|
610
|
+
for (let i = 0;i < segments.length - 1; i++) {
|
|
611
|
+
const id = segments[i];
|
|
612
|
+
const node = graph.nodes.find((n) => n.id === id);
|
|
613
|
+
if (!node)
|
|
614
|
+
throw new Error(`no block "${segments.slice(0, i + 1).join("/")}"`);
|
|
615
|
+
if (!node.graph)
|
|
616
|
+
throw new Error(`block "${segments.slice(0, i + 1).join("/")}" has no subgraph`);
|
|
617
|
+
graph = node.graph;
|
|
618
|
+
}
|
|
619
|
+
const id = segments[segments.length - 1];
|
|
620
|
+
const node = graph.nodes.find((n) => n.id === id);
|
|
621
|
+
if (!node)
|
|
622
|
+
throw new Error(`no block "${path}"`);
|
|
623
|
+
return { graph, node, parent: segments.slice(0, -1).join("/") };
|
|
624
|
+
}
|
|
625
|
+
function graphAt(doc, path) {
|
|
626
|
+
if (!path)
|
|
627
|
+
return doc.graph;
|
|
628
|
+
const { node } = locate(doc, path);
|
|
629
|
+
if (!node.graph)
|
|
630
|
+
throw new Error(`block "${path}" is not a container and has no subgraph`);
|
|
631
|
+
return node.graph;
|
|
632
|
+
}
|
|
633
|
+
var edgeEq = (a, from, to) => a[0] === from && a[1] === to;
|
|
634
|
+
function applyOne(doc, op) {
|
|
635
|
+
switch (op.op) {
|
|
636
|
+
case "add_node": {
|
|
637
|
+
if (!op.id)
|
|
638
|
+
throw new Error("id is required");
|
|
639
|
+
const def = getBlock(op.type);
|
|
640
|
+
if (!def)
|
|
641
|
+
throw new Error(`unknown block type "${op.type}"`);
|
|
642
|
+
const graph = graphAt(doc, op.parent);
|
|
643
|
+
if (graph.nodes.some((n) => n.id === op.id)) {
|
|
644
|
+
throw new Error(`"${joinPath(op.parent ?? "", op.id)}" already exists`);
|
|
645
|
+
}
|
|
646
|
+
const node = { id: op.id, type: op.type };
|
|
647
|
+
if (op.params)
|
|
648
|
+
node.params = { ...op.params };
|
|
649
|
+
if (op.label)
|
|
650
|
+
node.label = op.label;
|
|
651
|
+
if (def.kind === "container")
|
|
652
|
+
node.graph = { nodes: [], edges: [] };
|
|
653
|
+
graph.nodes.push(node);
|
|
654
|
+
return `added ${joinPath(op.parent ?? "", op.id)} (${op.type})`;
|
|
655
|
+
}
|
|
656
|
+
case "remove_node": {
|
|
657
|
+
const { graph, node } = locate(doc, op.path);
|
|
658
|
+
const index = graph.nodes.indexOf(node);
|
|
659
|
+
graph.nodes.splice(index, 1);
|
|
660
|
+
const before = graph.edges.length;
|
|
661
|
+
graph.edges = graph.edges.filter(([from, to]) => splitEndpoint(from).node !== node.id && splitEndpoint(to).node !== node.id);
|
|
662
|
+
const dropped = before - graph.edges.length;
|
|
663
|
+
return `removed ${op.path}${dropped > 0 ? ` and ${dropped} edge${dropped === 1 ? "" : "s"}` : ""}`;
|
|
664
|
+
}
|
|
665
|
+
case "set_param": {
|
|
666
|
+
const { node } = locate(doc, op.path);
|
|
667
|
+
const def = getBlock(node.type);
|
|
668
|
+
if (def && !(op.key in (def.params ?? {}))) {
|
|
669
|
+
const known = Object.keys(def.params ?? {}).join(", ");
|
|
670
|
+
throw new Error(`"${node.type}" has no parameter "${op.key}". Known: ${known || "(none)"}`);
|
|
671
|
+
}
|
|
672
|
+
node.params ??= {};
|
|
673
|
+
const previous = node.params[op.key];
|
|
674
|
+
if (op.value === undefined)
|
|
675
|
+
delete node.params[op.key];
|
|
676
|
+
else
|
|
677
|
+
node.params[op.key] = op.value;
|
|
678
|
+
return `${op.path}.${op.key}: ${JSON.stringify(previous ?? null)} -> ${JSON.stringify(op.value ?? null)}`;
|
|
679
|
+
}
|
|
680
|
+
case "connect": {
|
|
681
|
+
const graph = graphAt(doc, op.graph);
|
|
682
|
+
assertEndpoint(graph, op.from, "from");
|
|
683
|
+
assertEndpoint(graph, op.to, "to");
|
|
684
|
+
if (graph.edges.some((e) => edgeEq(e, op.from, op.to))) {
|
|
685
|
+
throw new Error(`${op.from} -> ${op.to} already exists`);
|
|
686
|
+
}
|
|
687
|
+
const occupied = graph.edges.find((e) => e[1] === op.to);
|
|
688
|
+
if (occupied)
|
|
689
|
+
throw new Error(`${op.to} already receives ${occupied[0]}; disconnect it first`);
|
|
690
|
+
graph.edges.push([op.from, op.to]);
|
|
691
|
+
return `connected ${op.from} -> ${op.to}`;
|
|
692
|
+
}
|
|
693
|
+
case "disconnect": {
|
|
694
|
+
const graph = graphAt(doc, op.graph);
|
|
695
|
+
const index = graph.edges.findIndex((e) => edgeEq(e, op.from, op.to));
|
|
696
|
+
if (index < 0)
|
|
697
|
+
throw new Error(`no edge ${op.from} -> ${op.to}`);
|
|
698
|
+
graph.edges.splice(index, 1);
|
|
699
|
+
return `disconnected ${op.from} -> ${op.to}`;
|
|
700
|
+
}
|
|
701
|
+
case "set_symbol": {
|
|
702
|
+
if (!op.name)
|
|
703
|
+
throw new Error("name is required");
|
|
704
|
+
const existing = doc.symbols[op.name];
|
|
705
|
+
if (op.value === null) {
|
|
706
|
+
if (existing === undefined)
|
|
707
|
+
throw new Error(`no symbol "${op.name}"`);
|
|
708
|
+
delete doc.symbols[op.name];
|
|
709
|
+
return `removed symbol ${op.name}`;
|
|
710
|
+
}
|
|
711
|
+
const wasRuntime = op.runtime ?? (typeof existing === "object" && existing !== null && existing.kind === "runtime");
|
|
712
|
+
let next;
|
|
713
|
+
if (wasRuntime) {
|
|
714
|
+
if (typeof op.value !== "number")
|
|
715
|
+
throw new Error(`runtime symbol "${op.name}" needs a number default`);
|
|
716
|
+
next = { kind: "runtime", default: op.value };
|
|
717
|
+
} else {
|
|
718
|
+
next = { kind: "design", value: op.value };
|
|
719
|
+
}
|
|
720
|
+
const docString = op.doc ?? (typeof existing === "object" && existing !== null ? existing.doc : undefined);
|
|
721
|
+
if (docString)
|
|
722
|
+
next.doc = docString;
|
|
723
|
+
doc.symbols[op.name] = next;
|
|
724
|
+
return `${op.name}: ${describeSymbol(existing)} -> ${describeSymbol(next)}`;
|
|
725
|
+
}
|
|
726
|
+
case "rename": {
|
|
727
|
+
const { graph, node, parent } = locate(doc, op.path);
|
|
728
|
+
if (!op.id)
|
|
729
|
+
throw new Error("id is required");
|
|
730
|
+
if (op.id === node.id)
|
|
731
|
+
return `${op.path} unchanged`;
|
|
732
|
+
if (graph.nodes.some((n) => n.id === op.id)) {
|
|
733
|
+
throw new Error(`"${joinPath(parent, op.id)}" already exists`);
|
|
734
|
+
}
|
|
735
|
+
const old = node.id;
|
|
736
|
+
node.id = op.id;
|
|
737
|
+
graph.edges = graph.edges.map(([from, to]) => [rewrite(from, old, op.id), rewrite(to, old, op.id)]);
|
|
738
|
+
return `renamed ${op.path} -> ${joinPath(parent, op.id)}`;
|
|
739
|
+
}
|
|
740
|
+
case "set_label": {
|
|
741
|
+
const { node } = locate(doc, op.path);
|
|
742
|
+
if (op.label === null || op.label === undefined || op.label === "")
|
|
743
|
+
delete node.label;
|
|
744
|
+
else
|
|
745
|
+
node.label = op.label;
|
|
746
|
+
return `${op.path} label -> ${op.label ?? "(none)"}`;
|
|
747
|
+
}
|
|
748
|
+
default: {
|
|
749
|
+
const bad = op;
|
|
750
|
+
throw new Error(`unknown operation "${bad.op}"`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
function describeSymbol(s) {
|
|
755
|
+
if (s === undefined)
|
|
756
|
+
return "(unset)";
|
|
757
|
+
if (typeof s === "number" || typeof s === "string")
|
|
758
|
+
return String(s);
|
|
759
|
+
return s.kind === "runtime" ? `runtime(${s.default})` : String(s.value);
|
|
760
|
+
}
|
|
761
|
+
function rewrite(endpoint, from, to) {
|
|
762
|
+
const { node, port } = splitEndpoint(endpoint);
|
|
763
|
+
return node === from ? `${to}:${port}` : endpoint;
|
|
764
|
+
}
|
|
765
|
+
function assertEndpoint(graph, endpoint, which) {
|
|
766
|
+
let split;
|
|
767
|
+
try {
|
|
768
|
+
split = splitEndpoint(endpoint);
|
|
769
|
+
} catch {
|
|
770
|
+
throw new Error(`${which} endpoint "${endpoint}" is not "blockId:port"`);
|
|
771
|
+
}
|
|
772
|
+
if (!graph.nodes.some((n) => n.id === split.node)) {
|
|
773
|
+
const known = graph.nodes.map((n) => n.id).join(", ");
|
|
774
|
+
throw new Error(`${which} endpoint "${endpoint}" names no block in this graph. Blocks: ${known}`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
function applyOps(doc, ops) {
|
|
778
|
+
const next = structuredClone(doc);
|
|
779
|
+
const applied = [];
|
|
780
|
+
for (let i = 0;i < ops.length; i++) {
|
|
781
|
+
try {
|
|
782
|
+
applied.push(applyOne(next, ops[i]));
|
|
783
|
+
} catch (e) {
|
|
784
|
+
throw new OpError(i, ops[i], e.message);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return { doc: next, applied };
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// packages/mcp/src/store/file-store.ts
|
|
791
|
+
import { PRESET_NAMES, getPreset } from "@tensor-cad/engine/node";
|
|
792
|
+
var EMPTY_DOC = (name) => ({
|
|
793
|
+
version: 1,
|
|
794
|
+
meta: { name },
|
|
795
|
+
symbols: {
|
|
796
|
+
B: { kind: "runtime", default: 1, doc: "Batch size" },
|
|
797
|
+
T: { kind: "runtime", default: 2048, doc: "Sequence length in tokens" }
|
|
798
|
+
},
|
|
799
|
+
graph: { nodes: [], edges: [] }
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
class FileStore {
|
|
803
|
+
entries = new Map;
|
|
804
|
+
listeners = new Set;
|
|
805
|
+
nextDesign = 1;
|
|
806
|
+
nextCheckpoint = 1;
|
|
807
|
+
root;
|
|
808
|
+
depth;
|
|
809
|
+
constructor(options = {}) {
|
|
810
|
+
this.root = resolve(options.root ?? process.cwd());
|
|
811
|
+
this.depth = options.depth ?? 3;
|
|
812
|
+
}
|
|
813
|
+
subscribe(listener) {
|
|
814
|
+
this.listeners.add(listener);
|
|
815
|
+
return () => this.listeners.delete(listener);
|
|
816
|
+
}
|
|
817
|
+
emit(change) {
|
|
818
|
+
for (const listener of this.listeners) {
|
|
819
|
+
try {
|
|
820
|
+
listener(change);
|
|
821
|
+
} catch (e) {
|
|
822
|
+
process.stderr.write(`tensorcad: store listener failed: ${e.message}
|
|
823
|
+
`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
list() {
|
|
828
|
+
return [...this.entries.values()].map((e) => summaryOf(e.record)).sort((a, b) => b.updated_at.localeCompare(a.updated_at));
|
|
829
|
+
}
|
|
830
|
+
async listFiles() {
|
|
831
|
+
const out = [];
|
|
832
|
+
const skip = new Set(["node_modules", ".git", "dist", "build", ".venv", "__pycache__"]);
|
|
833
|
+
const walk = async (dir, left) => {
|
|
834
|
+
let items;
|
|
835
|
+
try {
|
|
836
|
+
items = await readdir(dir, { withFileTypes: true });
|
|
837
|
+
} catch {
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
for (const item of items) {
|
|
841
|
+
const full = join2(dir, item.name);
|
|
842
|
+
if (item.isDirectory()) {
|
|
843
|
+
if (left > 0 && !skip.has(item.name) && !item.name.startsWith("."))
|
|
844
|
+
await walk(full, left - 1);
|
|
845
|
+
} else if (item.name.endsWith(".tensorcad.json")) {
|
|
846
|
+
out.push(full);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
await walk(this.root, this.depth);
|
|
851
|
+
return out.sort();
|
|
852
|
+
}
|
|
853
|
+
get(id) {
|
|
854
|
+
const entry = this.entries.get(id);
|
|
855
|
+
if (!entry)
|
|
856
|
+
throw new UnknownDesignError(id, [...this.entries.keys()]);
|
|
857
|
+
return entry.record;
|
|
858
|
+
}
|
|
859
|
+
create(options) {
|
|
860
|
+
let doc;
|
|
861
|
+
let source;
|
|
862
|
+
if (options.preset) {
|
|
863
|
+
if (!PRESET_NAMES.includes(options.preset)) {
|
|
864
|
+
throw new Error(`Unknown preset "${options.preset}". Available: ${PRESET_NAMES.join(", ")}`);
|
|
865
|
+
}
|
|
866
|
+
doc = getPreset(options.preset);
|
|
867
|
+
source = "preset";
|
|
868
|
+
} else {
|
|
869
|
+
doc = EMPTY_DOC(options.name ?? "untitled");
|
|
870
|
+
source = "empty";
|
|
871
|
+
}
|
|
872
|
+
if (options.name)
|
|
873
|
+
doc.meta.name = options.name;
|
|
874
|
+
return this.register(doc, source, undefined, true);
|
|
875
|
+
}
|
|
876
|
+
adopt(doc) {
|
|
877
|
+
return this.register(doc, "derived", undefined, true);
|
|
878
|
+
}
|
|
879
|
+
async open(path) {
|
|
880
|
+
const full = this.resolvePath(path);
|
|
881
|
+
for (const entry of this.entries.values()) {
|
|
882
|
+
if (entry.record.path === full)
|
|
883
|
+
return entry.record;
|
|
884
|
+
}
|
|
885
|
+
const text = await readFile2(full, "utf8");
|
|
886
|
+
let parsed;
|
|
887
|
+
try {
|
|
888
|
+
parsed = JSON.parse(text);
|
|
889
|
+
} catch (e) {
|
|
890
|
+
throw new Error(`${full} is not valid JSON: ${e.message}`);
|
|
891
|
+
}
|
|
892
|
+
const doc = parsed;
|
|
893
|
+
if (!doc || typeof doc !== "object" || !doc.graph || !doc.meta) {
|
|
894
|
+
throw new Error(`${full} does not look like a design document: it has no "meta" and "graph".`);
|
|
895
|
+
}
|
|
896
|
+
doc.symbols ??= {};
|
|
897
|
+
return this.register(doc, "file", full, false);
|
|
898
|
+
}
|
|
899
|
+
register(doc, source, path, dirty) {
|
|
900
|
+
const now = new Date().toISOString();
|
|
901
|
+
const id = `dsn_${this.nextDesign++}`;
|
|
902
|
+
const record = {
|
|
903
|
+
design_id: id,
|
|
904
|
+
name: doc.meta.name,
|
|
905
|
+
revision: 1,
|
|
906
|
+
source,
|
|
907
|
+
dirty,
|
|
908
|
+
created_at: now,
|
|
909
|
+
updated_at: now,
|
|
910
|
+
doc
|
|
911
|
+
};
|
|
912
|
+
if (path)
|
|
913
|
+
record.path = path;
|
|
914
|
+
this.entries.set(id, { record, log: [], checkpoints: new Map });
|
|
915
|
+
this.emit({ kind: "registered", record });
|
|
916
|
+
return record;
|
|
917
|
+
}
|
|
918
|
+
apply(id, ops, expectedRevision) {
|
|
919
|
+
const entry = this.entry(id);
|
|
920
|
+
const { record } = entry;
|
|
921
|
+
if (expectedRevision !== undefined && expectedRevision !== record.revision) {
|
|
922
|
+
throw new RevisionConflictError(id, expectedRevision, record.revision);
|
|
923
|
+
}
|
|
924
|
+
const before = structuredClone(record.doc);
|
|
925
|
+
const { doc, applied } = applyOps(record.doc, ops);
|
|
926
|
+
const previousRevision = record.revision;
|
|
927
|
+
entry.log.push({ revision: previousRevision, at: new Date().toISOString(), ops, before });
|
|
928
|
+
record.doc = doc;
|
|
929
|
+
record.name = doc.meta.name;
|
|
930
|
+
record.revision = previousRevision + 1;
|
|
931
|
+
record.dirty = true;
|
|
932
|
+
record.updated_at = new Date().toISOString();
|
|
933
|
+
this.emit({ kind: "applied", record, ops });
|
|
934
|
+
return { record, applied, previousRevision };
|
|
935
|
+
}
|
|
936
|
+
replace(id, doc, expectedRevision) {
|
|
937
|
+
const entry = this.entry(id);
|
|
938
|
+
const { record } = entry;
|
|
939
|
+
if (expectedRevision !== undefined && expectedRevision !== record.revision) {
|
|
940
|
+
throw new RevisionConflictError(id, expectedRevision, record.revision);
|
|
941
|
+
}
|
|
942
|
+
const previousRevision = record.revision;
|
|
943
|
+
entry.log.push({ revision: previousRevision, at: new Date().toISOString(), ops: [], before: record.doc });
|
|
944
|
+
record.doc = doc;
|
|
945
|
+
record.name = doc.meta.name;
|
|
946
|
+
record.revision = previousRevision + 1;
|
|
947
|
+
record.dirty = true;
|
|
948
|
+
record.updated_at = new Date().toISOString();
|
|
949
|
+
this.emit({ kind: "replaced", record });
|
|
950
|
+
return { record, applied: ["replaced the document"], previousRevision };
|
|
951
|
+
}
|
|
952
|
+
async save(id, path) {
|
|
953
|
+
const record = this.get(id);
|
|
954
|
+
const target = path ? this.resolvePath(path) : record.path ?? join2(this.root, `${slug(record.name)}.tensorcad.json`);
|
|
955
|
+
const text = `${JSON.stringify(record.doc, null, 2)}
|
|
956
|
+
`;
|
|
957
|
+
await mkdir2(dirname2(target), { recursive: true });
|
|
958
|
+
await writeFile2(target, text, "utf8");
|
|
959
|
+
record.path = target;
|
|
960
|
+
record.dirty = false;
|
|
961
|
+
record.updated_at = new Date().toISOString();
|
|
962
|
+
this.emit({ kind: "saved", record });
|
|
963
|
+
return { record, path: target, bytes: Buffer.byteLength(text, "utf8") };
|
|
964
|
+
}
|
|
965
|
+
checkpoint(id, label) {
|
|
966
|
+
const entry = this.entry(id);
|
|
967
|
+
const info = {
|
|
968
|
+
checkpoint_id: `ckpt_${this.nextCheckpoint++}`,
|
|
969
|
+
label: label ?? `revision ${entry.record.revision}`,
|
|
970
|
+
revision: entry.record.revision,
|
|
971
|
+
created_at: new Date().toISOString()
|
|
972
|
+
};
|
|
973
|
+
entry.checkpoints.set(info.checkpoint_id, { ...info, doc: structuredClone(entry.record.doc) });
|
|
974
|
+
return info;
|
|
975
|
+
}
|
|
976
|
+
checkpoints(id) {
|
|
977
|
+
const entry = this.entry(id);
|
|
978
|
+
return [...entry.checkpoints.values()].map(({ doc: _doc, ...info }) => info);
|
|
979
|
+
}
|
|
980
|
+
restore(id, checkpointId) {
|
|
981
|
+
const entry = this.entry(id);
|
|
982
|
+
const { record } = entry;
|
|
983
|
+
let doc;
|
|
984
|
+
let restoredFrom;
|
|
985
|
+
if (checkpointId) {
|
|
986
|
+
const saved = entry.checkpoints.get(checkpointId);
|
|
987
|
+
if (!saved) {
|
|
988
|
+
const known = [...entry.checkpoints.keys()];
|
|
989
|
+
throw new Error(`Unknown checkpoint "${checkpointId}" for ${id}. ` + `Checkpoints: ${known.length > 0 ? known.join(", ") : "(none)"}.`);
|
|
990
|
+
}
|
|
991
|
+
doc = structuredClone(saved.doc);
|
|
992
|
+
restoredFrom = `checkpoint ${checkpointId} (${saved.label})`;
|
|
993
|
+
} else {
|
|
994
|
+
const last = entry.log.pop();
|
|
995
|
+
if (!last)
|
|
996
|
+
throw new Error(`Design ${id} has no edits to undo and no checkpoint was named.`);
|
|
997
|
+
doc = last.before;
|
|
998
|
+
restoredFrom = `undo of ${last.ops.length} op${last.ops.length === 1 ? "" : "s"} at revision ${last.revision}`;
|
|
999
|
+
}
|
|
1000
|
+
record.doc = doc;
|
|
1001
|
+
record.name = doc.meta.name;
|
|
1002
|
+
record.revision += 1;
|
|
1003
|
+
record.dirty = true;
|
|
1004
|
+
record.updated_at = new Date().toISOString();
|
|
1005
|
+
this.emit({ kind: "restored", record });
|
|
1006
|
+
return { record, restoredFrom };
|
|
1007
|
+
}
|
|
1008
|
+
entry(id) {
|
|
1009
|
+
const entry = this.entries.get(id);
|
|
1010
|
+
if (!entry)
|
|
1011
|
+
throw new UnknownDesignError(id, [...this.entries.keys()]);
|
|
1012
|
+
return entry;
|
|
1013
|
+
}
|
|
1014
|
+
resolvePath(path) {
|
|
1015
|
+
const full = isAbsolute(path) ? path : resolve(this.root, path);
|
|
1016
|
+
if (extname(full) === "")
|
|
1017
|
+
return `${full}.tensorcad.json`;
|
|
1018
|
+
return full;
|
|
1019
|
+
}
|
|
1020
|
+
static async exists(path) {
|
|
1021
|
+
try {
|
|
1022
|
+
await stat(path);
|
|
1023
|
+
return true;
|
|
1024
|
+
} catch {
|
|
1025
|
+
return false;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
function summaryOf(record) {
|
|
1030
|
+
const { doc: _doc, ...summary } = record;
|
|
1031
|
+
return summary;
|
|
1032
|
+
}
|
|
1033
|
+
function slug(name) {
|
|
1034
|
+
return name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "design";
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// packages/mcp/src/prompts.ts
|
|
1038
|
+
import * as z2 from "zod";
|
|
1039
|
+
import { HARDWARE, PRESET_NAMES as PRESET_NAMES2 } from "@tensor-cad/engine/node";
|
|
1040
|
+
var user = (text) => ({
|
|
1041
|
+
messages: [{ role: "user", content: { type: "text", text } }]
|
|
1042
|
+
});
|
|
1043
|
+
var HARDWARE_IDS = HARDWARE.map((h) => h.id).join(", ");
|
|
1044
|
+
function registerPrompts(server) {
|
|
1045
|
+
server.registerPrompt("design_model", {
|
|
1046
|
+
title: "Design a model",
|
|
1047
|
+
description: "Design a decoder-only LLM to a parameter budget and context length, then check and cost it.",
|
|
1048
|
+
argsSchema: z2.object({
|
|
1049
|
+
target_params: z2.string().describe('Parameter budget, e.g. "3B", "8B", "70B".'),
|
|
1050
|
+
context_len: z2.string().optional().describe('Context length in tokens, e.g. "8192".'),
|
|
1051
|
+
family: z2.string().optional().describe(`Architecture to start from: ${PRESET_NAMES2.join(", ")}, or "empty".`)
|
|
1052
|
+
})
|
|
1053
|
+
}, ({ target_params, context_len, family }) => user([
|
|
1054
|
+
`Design a decoder-only language model of about ${target_params} parameters` + `${context_len ? ` with a ${context_len}-token context` : ""}` + `${family ? `, starting from the ${family} architecture` : ""}.`,
|
|
1055
|
+
"",
|
|
1056
|
+
"Work like this:",
|
|
1057
|
+
`1. ${family && family !== "empty" ? `tensorcad_new_design with preset "${family}"` : "tensorcad_new_design, choosing the closest preset with tensorcad_list_designs first"}.`,
|
|
1058
|
+
"2. tensorcad_get_design (outline) to see what you have.",
|
|
1059
|
+
"3. tensorcad_apply_ops with set_symbol operations to move L, D, H, Hkv, dh, F and V toward the budget.",
|
|
1060
|
+
" Keep D divisible by H, keep the head dimension in {64, 96, 128, 160, 192, 256} so fused attention",
|
|
1061
|
+
" kernels apply, keep D and F multiples of 128, and keep H divisible by Hkv.",
|
|
1062
|
+
"4. tensorcad_validate and fix every error and any warning you can.",
|
|
1063
|
+
`5. tensorcad_analyze${context_len ? ` with T=${context_len}` : ""} and report parameters, KV cache per token,`,
|
|
1064
|
+
" training memory per GPU and the Chinchilla-optimal token budget.",
|
|
1065
|
+
"",
|
|
1066
|
+
"Report the final symbol table, the parameter count against the target, and anything you traded off."
|
|
1067
|
+
].join(`
|
|
1068
|
+
`)));
|
|
1069
|
+
server.registerPrompt("review_design", {
|
|
1070
|
+
title: "Review a design",
|
|
1071
|
+
description: "Read a design, run the rules, and report what is wrong and what to do about it.",
|
|
1072
|
+
argsSchema: z2.object({
|
|
1073
|
+
design_id: z2.string().describe("Design handle. Use tensorcad_list_designs if you do not have one."),
|
|
1074
|
+
hardware: z2.string().optional().describe(`Device to judge memory against: ${HARDWARE_IDS}.`)
|
|
1075
|
+
})
|
|
1076
|
+
}, ({ design_id, hardware }) => user([
|
|
1077
|
+
`Review design ${design_id}.`,
|
|
1078
|
+
"",
|
|
1079
|
+
`1. tensorcad_get_design with format "outline".`,
|
|
1080
|
+
`2. tensorcad_validate${hardware ? ` with hardware "${hardware}"` : ""}.`,
|
|
1081
|
+
"3. tensorcad_get_block on anything a finding points at, to see the shapes and parameters for yourself.",
|
|
1082
|
+
"4. tensorcad_analyze for the numbers behind the memory and cost findings.",
|
|
1083
|
+
"",
|
|
1084
|
+
"Report: every error with the exact edit that fixes it, then warnings by how much they cost,",
|
|
1085
|
+
"then anything the rules do not catch (unusual width ratios, a vocabulary that dominates the",
|
|
1086
|
+
"parameter count, attention that dominates FLOPs at this context length).",
|
|
1087
|
+
"Do not change the design unless I ask."
|
|
1088
|
+
].join(`
|
|
1089
|
+
`)));
|
|
1090
|
+
server.registerPrompt("scale_design", {
|
|
1091
|
+
title: "Scale a design",
|
|
1092
|
+
description: "Scale a design up or down by a factor while keeping its proportions sane.",
|
|
1093
|
+
argsSchema: z2.object({
|
|
1094
|
+
design_id: z2.string().describe("Design handle."),
|
|
1095
|
+
factor: z2.string().describe('Parameter multiplier, e.g. "2", "0.5", or a target such as "70B".')
|
|
1096
|
+
})
|
|
1097
|
+
}, ({ design_id, factor }) => user([
|
|
1098
|
+
`Scale design ${design_id} by ${factor}.`,
|
|
1099
|
+
"",
|
|
1100
|
+
"1. tensorcad_checkpoint first, labelled with what you are about to try, so tensorcad_restore can undo it.",
|
|
1101
|
+
"2. tensorcad_analyze to record the starting numbers.",
|
|
1102
|
+
"3. Decide how to spend the factor. Parameters go roughly as L x D^2, so depth is linear and width is",
|
|
1103
|
+
" quadratic; published families widen and deepen together rather than stretching one axis.",
|
|
1104
|
+
"4. tensorcad_apply_ops with set_symbol operations. Keep D divisible by H, keep the head dimension in",
|
|
1105
|
+
" {64, 96, 128, 160, 192, 256}, keep D and F multiples of 128, and keep H divisible by Hkv.",
|
|
1106
|
+
"5. tensorcad_validate, then tensorcad_analyze again.",
|
|
1107
|
+
"",
|
|
1108
|
+
"Report the before and after symbol tables, the parameter count against the target, and how KV cache,",
|
|
1109
|
+
"training memory and training cost moved. If you cannot hit the target without breaking a constraint,",
|
|
1110
|
+
"say which constraint and what the nearest good design is."
|
|
1111
|
+
].join(`
|
|
1112
|
+
`)));
|
|
1113
|
+
server.registerPrompt("explain_costs", {
|
|
1114
|
+
title: "Explain the costs",
|
|
1115
|
+
description: "Explain what a design costs to train and to serve, and where the money goes.",
|
|
1116
|
+
argsSchema: z2.object({
|
|
1117
|
+
design_id: z2.string().describe("Design handle."),
|
|
1118
|
+
batch: z2.string().optional().describe("Micro-batch size for training, and concurrency for serving."),
|
|
1119
|
+
seq: z2.string().optional().describe("Sequence length in tokens."),
|
|
1120
|
+
hardware: z2.string().optional().describe(`Device: ${HARDWARE_IDS}.`)
|
|
1121
|
+
})
|
|
1122
|
+
}, ({ design_id, batch, seq, hardware }) => user([
|
|
1123
|
+
`Explain what design ${design_id} costs.`,
|
|
1124
|
+
"",
|
|
1125
|
+
`1. tensorcad_analyze with${seq ? ` T=${seq}` : " the document's own T"}${batch ? `, B=${batch} and concurrency=${batch}` : ""}` + `${hardware ? `, hardware "${hardware}"` : ""}.`,
|
|
1126
|
+
"2. Run it again with a different GPU count or ZeRO stage if training does not fit the device.",
|
|
1127
|
+
"",
|
|
1128
|
+
"Cover, in plain language:",
|
|
1129
|
+
"- training memory: weights, gradients, optimizer state and activations, which one dominates, and what",
|
|
1130
|
+
" ZeRO stage or tensor parallelism would make it fit.",
|
|
1131
|
+
"- training compute: FLOPs per token, the Chinchilla-optimal token budget, GPU-hours and dollars, and",
|
|
1132
|
+
" what the MFU assumption is doing to that number.",
|
|
1133
|
+
"- serving: weights plus KV cache per concurrent sequence, how many sequences fit in device memory,",
|
|
1134
|
+
" decode tokens per second, and whether decode is memory-bound or compute-bound.",
|
|
1135
|
+
"- the one change that would most reduce each of those."
|
|
1136
|
+
].join(`
|
|
1137
|
+
`)));
|
|
1138
|
+
}
|
|
1139
|
+
var PROMPT_NAMES = ["design_model", "review_design", "scale_design", "explain_costs"];
|
|
1140
|
+
|
|
1141
|
+
// packages/mcp/src/resources.ts
|
|
1142
|
+
import { ResourceTemplate } from "@modelcontextprotocol/server";
|
|
1143
|
+
|
|
1144
|
+
// packages/mcp/src/design-schema.ts
|
|
1145
|
+
var DESIGN_JSON_SCHEMA = {
|
|
1146
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
1147
|
+
$id: "tensorcad://schema/design",
|
|
1148
|
+
title: "TensorCAD design document",
|
|
1149
|
+
description: "The `.tensorcad.json` format. The graph is the source of truth; the editor's node positions are a view of it.",
|
|
1150
|
+
type: "object",
|
|
1151
|
+
required: ["version", "meta", "symbols", "graph"],
|
|
1152
|
+
additionalProperties: false,
|
|
1153
|
+
properties: {
|
|
1154
|
+
version: { const: 1, description: "Document format version." },
|
|
1155
|
+
meta: {
|
|
1156
|
+
type: "object",
|
|
1157
|
+
required: ["name"],
|
|
1158
|
+
additionalProperties: false,
|
|
1159
|
+
properties: {
|
|
1160
|
+
name: { type: "string" },
|
|
1161
|
+
family: { type: "string", description: "Architecture family, e.g. llama, qwen, gpt2." },
|
|
1162
|
+
notes: { type: "string" },
|
|
1163
|
+
published: {
|
|
1164
|
+
type: "object",
|
|
1165
|
+
description: "Reference numbers from the model card or paper, asserted by the regression suite.",
|
|
1166
|
+
additionalProperties: false,
|
|
1167
|
+
properties: {
|
|
1168
|
+
params: { type: "number" },
|
|
1169
|
+
activeParams: { type: "number" },
|
|
1170
|
+
kvBytesPerToken: { type: "number" },
|
|
1171
|
+
source: { type: "string" },
|
|
1172
|
+
tolerance: { type: "number", description: "Allowed relative difference. Defaults to 0.5%." }
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
},
|
|
1177
|
+
symbols: {
|
|
1178
|
+
type: "object",
|
|
1179
|
+
description: "The design's named dimensions. A symbol is a number, an expression over earlier symbols, or a runtime " + "dimension (B, T) that stays indeterminate through analysis.",
|
|
1180
|
+
additionalProperties: {
|
|
1181
|
+
anyOf: [
|
|
1182
|
+
{ type: "number" },
|
|
1183
|
+
{ type: "string", description: 'Expression over earlier symbols, e.g. "ceil_mult(1.3*8/3*D, 1024)".' },
|
|
1184
|
+
{
|
|
1185
|
+
type: "object",
|
|
1186
|
+
required: ["kind", "default"],
|
|
1187
|
+
additionalProperties: false,
|
|
1188
|
+
properties: {
|
|
1189
|
+
kind: { const: "runtime" },
|
|
1190
|
+
default: { type: "number" },
|
|
1191
|
+
doc: { type: "string" }
|
|
1192
|
+
}
|
|
1193
|
+
},
|
|
1194
|
+
{
|
|
1195
|
+
type: "object",
|
|
1196
|
+
required: ["kind", "value"],
|
|
1197
|
+
additionalProperties: false,
|
|
1198
|
+
properties: {
|
|
1199
|
+
kind: { const: "design" },
|
|
1200
|
+
value: { anyOf: [{ type: "number" }, { type: "string" }] },
|
|
1201
|
+
doc: { type: "string" }
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
]
|
|
1205
|
+
}
|
|
1206
|
+
},
|
|
1207
|
+
graph: { $ref: "#/$defs/graph" },
|
|
1208
|
+
ui: {
|
|
1209
|
+
type: "object",
|
|
1210
|
+
description: "Editor state. Ignored by analysis and codegen.",
|
|
1211
|
+
additionalProperties: false,
|
|
1212
|
+
properties: {
|
|
1213
|
+
positions: {
|
|
1214
|
+
type: "object",
|
|
1215
|
+
additionalProperties: { type: "array", items: { type: "number" }, minItems: 2, maxItems: 2 }
|
|
1216
|
+
},
|
|
1217
|
+
collapsed: { type: "array", items: { type: "string" } }
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
},
|
|
1221
|
+
$defs: {
|
|
1222
|
+
graph: {
|
|
1223
|
+
type: "object",
|
|
1224
|
+
required: ["nodes", "edges"],
|
|
1225
|
+
additionalProperties: false,
|
|
1226
|
+
properties: {
|
|
1227
|
+
nodes: { type: "array", items: { $ref: "#/$defs/node" } },
|
|
1228
|
+
edges: {
|
|
1229
|
+
type: "array",
|
|
1230
|
+
description: 'Each edge is ["fromBlock:port", "toBlock:port"], with ids local to this graph.',
|
|
1231
|
+
items: {
|
|
1232
|
+
type: "array",
|
|
1233
|
+
items: { type: "string" },
|
|
1234
|
+
minItems: 2,
|
|
1235
|
+
maxItems: 2
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
},
|
|
1240
|
+
node: {
|
|
1241
|
+
type: "object",
|
|
1242
|
+
required: ["id", "type"],
|
|
1243
|
+
additionalProperties: false,
|
|
1244
|
+
properties: {
|
|
1245
|
+
id: { type: "string", description: "Unique within its graph." },
|
|
1246
|
+
type: { type: "string", description: "A catalog block type; see tensorcad://catalog." },
|
|
1247
|
+
params: {
|
|
1248
|
+
type: "object",
|
|
1249
|
+
description: "Block parameters. Numeric parameters accept an expression string over the design symbols. " + "The per-type schema is in tensorcad://catalog/{type}.",
|
|
1250
|
+
additionalProperties: true
|
|
1251
|
+
},
|
|
1252
|
+
graph: { $ref: "#/$defs/graph", description: "Subgraph, for container blocks such as repeat." },
|
|
1253
|
+
variants: {
|
|
1254
|
+
type: "object",
|
|
1255
|
+
description: "Named subgraph variants, for hybrid repeat patterns.",
|
|
1256
|
+
additionalProperties: { $ref: "#/$defs/graph" }
|
|
1257
|
+
},
|
|
1258
|
+
label: { type: "string", description: "Shown on the canvas instead of the id." }
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
|
|
1264
|
+
// packages/mcp/src/summarize.ts
|
|
1265
|
+
import { formatBytes, formatCount, formatFlops, isComposite, isContainer, isPrimitive, joinPath as joinPath2, splitEndpoint as splitEndpoint2 } from "@tensor-cad/engine";
|
|
1266
|
+
import { catalogByCategory, countParams, getBlock as getBlock2, inferShapes, resolveSymbols } from "@tensor-cad/engine/node";
|
|
1267
|
+
function outlineOf(doc) {
|
|
1268
|
+
const symbols = resolveSymbols(doc);
|
|
1269
|
+
const infer = inferShapes(doc);
|
|
1270
|
+
const params = countParams(doc);
|
|
1271
|
+
const paramsAt = (path) => {
|
|
1272
|
+
let sum = 0;
|
|
1273
|
+
for (const [p, v] of Object.entries(params.byPath)) {
|
|
1274
|
+
if (p === path || p.startsWith(`${path}/`))
|
|
1275
|
+
sum += v;
|
|
1276
|
+
}
|
|
1277
|
+
return sum;
|
|
1278
|
+
};
|
|
1279
|
+
const blocks = [];
|
|
1280
|
+
const edges = [];
|
|
1281
|
+
const walk = (graph, prefix, depth) => {
|
|
1282
|
+
for (const [from, to] of graph.edges) {
|
|
1283
|
+
const edge = { graph: prefix, from, to };
|
|
1284
|
+
const source = splitEndpoint2(from);
|
|
1285
|
+
const shape = infer.outputs[`${joinPath2(prefix, source.node)}:${source.port}`];
|
|
1286
|
+
if (shape)
|
|
1287
|
+
edge.shape = shape.symbolic;
|
|
1288
|
+
edges.push(edge);
|
|
1289
|
+
}
|
|
1290
|
+
for (const node of graph.nodes) {
|
|
1291
|
+
const path = joinPath2(prefix, node.id);
|
|
1292
|
+
const def = getBlock2(node.type);
|
|
1293
|
+
const block = {
|
|
1294
|
+
path,
|
|
1295
|
+
type: node.type,
|
|
1296
|
+
kind: def?.kind ?? "unknown",
|
|
1297
|
+
depth,
|
|
1298
|
+
params: paramsAt(path)
|
|
1299
|
+
};
|
|
1300
|
+
if (node.label)
|
|
1301
|
+
block.label = node.label;
|
|
1302
|
+
if (node.graph) {
|
|
1303
|
+
const count = infer.resolved[path]?.p?.count;
|
|
1304
|
+
if (typeof count === "number")
|
|
1305
|
+
block.repeat = count;
|
|
1306
|
+
}
|
|
1307
|
+
blocks.push(block);
|
|
1308
|
+
if (node.graph)
|
|
1309
|
+
walk(node.graph, path, depth + 1);
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
walk(doc.graph, "", 0);
|
|
1313
|
+
const out = {
|
|
1314
|
+
name: doc.meta.name,
|
|
1315
|
+
symbols: outlineSymbols(doc, symbols),
|
|
1316
|
+
blocks,
|
|
1317
|
+
edges,
|
|
1318
|
+
params_total: params.total,
|
|
1319
|
+
params_active: params.active,
|
|
1320
|
+
issues: infer.issues.length
|
|
1321
|
+
};
|
|
1322
|
+
if (doc.meta.family)
|
|
1323
|
+
out.family = doc.meta.family;
|
|
1324
|
+
if (doc.meta.notes)
|
|
1325
|
+
out.notes = doc.meta.notes;
|
|
1326
|
+
return out;
|
|
1327
|
+
}
|
|
1328
|
+
function outlineSymbols(doc, symbols) {
|
|
1329
|
+
return Object.entries(doc.symbols ?? {}).map(([name, def]) => {
|
|
1330
|
+
const runtime = typeof def === "object" && def !== null && def.kind === "runtime";
|
|
1331
|
+
const raw = typeof def === "object" && def !== null ? runtime ? String(def.default) : String(def.value) : String(def);
|
|
1332
|
+
const s = { name, kind: runtime ? "runtime" : "design", value: raw };
|
|
1333
|
+
const resolved = symbols.values[name];
|
|
1334
|
+
if (typeof resolved === "number")
|
|
1335
|
+
s.resolved = resolved;
|
|
1336
|
+
const docString = typeof def === "object" && def !== null ? def.doc : undefined;
|
|
1337
|
+
if (docString)
|
|
1338
|
+
s.doc = docString;
|
|
1339
|
+
return s;
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
function outlineText(o) {
|
|
1343
|
+
const lines = [
|
|
1344
|
+
`${o.name}${o.family ? ` (${o.family})` : ""} ${formatCount(o.params_total)} parameters` + (o.params_active !== o.params_total ? `, ${formatCount(o.params_active)} active` : "")
|
|
1345
|
+
];
|
|
1346
|
+
if (o.notes)
|
|
1347
|
+
lines.push(o.notes);
|
|
1348
|
+
lines.push("", "symbols");
|
|
1349
|
+
for (const s of o.symbols) {
|
|
1350
|
+
lines.push(` ${s.name} = ${s.value}${s.kind === "runtime" ? " (runtime)" : ""}${s.doc ? ` ${s.doc}` : ""}`);
|
|
1351
|
+
}
|
|
1352
|
+
lines.push("", "blocks");
|
|
1353
|
+
for (const b of o.blocks) {
|
|
1354
|
+
const indent = " ".repeat(b.depth + 1);
|
|
1355
|
+
const repeat = b.repeat !== undefined ? ` x${b.repeat}` : "";
|
|
1356
|
+
const size = b.params > 0 ? ` ${formatCount(b.params)}` : "";
|
|
1357
|
+
lines.push(`${indent}${b.path.split("/").pop()} ${b.type}${repeat}${size}`);
|
|
1358
|
+
}
|
|
1359
|
+
lines.push("", "edges");
|
|
1360
|
+
for (const e of o.edges) {
|
|
1361
|
+
const where = e.graph ? `${e.graph}/` : "";
|
|
1362
|
+
lines.push(` ${where}${e.from} -> ${where}${e.to}${e.shape ? ` ${e.shape}` : ""}`);
|
|
1363
|
+
}
|
|
1364
|
+
if (o.issues > 0)
|
|
1365
|
+
lines.push("", `${o.issues} shape issue(s); run tensorcad_validate`);
|
|
1366
|
+
return lines.join(`
|
|
1367
|
+
`);
|
|
1368
|
+
}
|
|
1369
|
+
function blockDetail(doc, path) {
|
|
1370
|
+
const infer = inferShapes(doc);
|
|
1371
|
+
const params = countParams(doc);
|
|
1372
|
+
const segments = path.split("/").filter(Boolean);
|
|
1373
|
+
if (segments.length === 0)
|
|
1374
|
+
throw new Error(`"${path}" is not a block path.`);
|
|
1375
|
+
let graph = doc.graph;
|
|
1376
|
+
let node = undefined;
|
|
1377
|
+
for (let i = 0;i < segments.length; i++) {
|
|
1378
|
+
node = findNode(graph, segments[i]);
|
|
1379
|
+
if (!node) {
|
|
1380
|
+
const known = graph.nodes.map((n) => n.id).join(", ");
|
|
1381
|
+
throw new Error(`No block "${segments.slice(0, i + 1).join("/")}". Blocks here: ${known || "(none)"}.`);
|
|
1382
|
+
}
|
|
1383
|
+
if (i < segments.length - 1) {
|
|
1384
|
+
if (!node.graph)
|
|
1385
|
+
throw new Error(`Block "${segments.slice(0, i + 1).join("/")}" has no subgraph.`);
|
|
1386
|
+
graph = node.graph;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
if (!node)
|
|
1390
|
+
throw new Error(`No block "${path}".`);
|
|
1391
|
+
const def = getBlock2(node.type);
|
|
1392
|
+
const ports = infer.ports[path] ?? { in: {}, out: {} };
|
|
1393
|
+
const consumers = new Map;
|
|
1394
|
+
for (const [consumer, producer] of Object.entries(infer.producerOf)) {
|
|
1395
|
+
const list = consumers.get(producer);
|
|
1396
|
+
if (list)
|
|
1397
|
+
list.push(consumer);
|
|
1398
|
+
else
|
|
1399
|
+
consumers.set(producer, [consumer]);
|
|
1400
|
+
}
|
|
1401
|
+
const inputs = Object.entries(ports.in).map(([name, spec]) => {
|
|
1402
|
+
const port = { name, pattern: spec.shape };
|
|
1403
|
+
if (spec.dtype !== "inherit")
|
|
1404
|
+
port.dtype = spec.dtype;
|
|
1405
|
+
if (spec.optional)
|
|
1406
|
+
port.optional = true;
|
|
1407
|
+
const shape = infer.inputs[`${path}:${name}`];
|
|
1408
|
+
if (shape)
|
|
1409
|
+
port.shape = shape.symbolic;
|
|
1410
|
+
const producer = infer.producerOf[`${path}:${name}`];
|
|
1411
|
+
if (producer)
|
|
1412
|
+
port.connected_to = [producer];
|
|
1413
|
+
return port;
|
|
1414
|
+
});
|
|
1415
|
+
const outputs = Object.entries(ports.out).map(([name, spec]) => {
|
|
1416
|
+
const port = { name, pattern: spec.shape };
|
|
1417
|
+
if (spec.dtype !== "inherit")
|
|
1418
|
+
port.dtype = spec.dtype;
|
|
1419
|
+
const shape = infer.outputs[`${path}:${name}`];
|
|
1420
|
+
if (shape)
|
|
1421
|
+
port.shape = shape.symbolic;
|
|
1422
|
+
const to = consumers.get(`${path}:${name}`);
|
|
1423
|
+
if (to)
|
|
1424
|
+
port.connected_to = to;
|
|
1425
|
+
return port;
|
|
1426
|
+
});
|
|
1427
|
+
let paramsCount = 0;
|
|
1428
|
+
for (const [p, v] of Object.entries(params.byPath)) {
|
|
1429
|
+
if (p === path || p.startsWith(`${path}/`))
|
|
1430
|
+
paramsCount += v;
|
|
1431
|
+
}
|
|
1432
|
+
const resolved = infer.resolved[path];
|
|
1433
|
+
const detail = {
|
|
1434
|
+
path,
|
|
1435
|
+
id: node.id,
|
|
1436
|
+
type: node.type,
|
|
1437
|
+
kind: def?.kind ?? "unknown",
|
|
1438
|
+
category: def?.category ?? "unknown",
|
|
1439
|
+
summary: def?.docs.summary ?? "Unknown block type.",
|
|
1440
|
+
params: { ...node.params ?? {} },
|
|
1441
|
+
resolved_params: resolved ? { ...resolved.p } : {},
|
|
1442
|
+
param_errors: resolved ? [] : [`Unknown block type "${node.type}"`],
|
|
1443
|
+
inputs,
|
|
1444
|
+
outputs,
|
|
1445
|
+
params_count: paramsCount,
|
|
1446
|
+
instances: instancesOf(infer, segments),
|
|
1447
|
+
children: (node.graph?.nodes ?? []).map((n) => joinPath2(path, n.id))
|
|
1448
|
+
};
|
|
1449
|
+
if (node.label)
|
|
1450
|
+
detail.label = node.label;
|
|
1451
|
+
if (def?.docs.formula)
|
|
1452
|
+
detail.formula = def.docs.formula;
|
|
1453
|
+
return detail;
|
|
1454
|
+
}
|
|
1455
|
+
function findNode(graph, id) {
|
|
1456
|
+
return graph.nodes.find((n) => n.id === id);
|
|
1457
|
+
}
|
|
1458
|
+
function instancesOf(infer, segments) {
|
|
1459
|
+
let multiplier = 1;
|
|
1460
|
+
for (let i = 0;i < segments.length - 1; i++) {
|
|
1461
|
+
const prefix = segments.slice(0, i + 1).join("/");
|
|
1462
|
+
const count = infer.resolved[prefix]?.p?.count;
|
|
1463
|
+
if (typeof count === "number")
|
|
1464
|
+
multiplier *= count;
|
|
1465
|
+
}
|
|
1466
|
+
return multiplier;
|
|
1467
|
+
}
|
|
1468
|
+
function blockText(b) {
|
|
1469
|
+
const lines = [
|
|
1470
|
+
`${b.path} ${b.type} (${b.kind}/${b.category})${b.instances > 1 ? ` x${b.instances}` : ""}`,
|
|
1471
|
+
b.summary
|
|
1472
|
+
];
|
|
1473
|
+
if (b.formula)
|
|
1474
|
+
lines.push(`formula: ${b.formula}`);
|
|
1475
|
+
lines.push(`parameters: ${formatCount(b.params_count)}`);
|
|
1476
|
+
if (Object.keys(b.params).length > 0) {
|
|
1477
|
+
lines.push("", "params");
|
|
1478
|
+
for (const [k, v] of Object.entries(b.params))
|
|
1479
|
+
lines.push(` ${k} = ${JSON.stringify(v)}`);
|
|
1480
|
+
}
|
|
1481
|
+
if (b.inputs.length > 0) {
|
|
1482
|
+
lines.push("", "in");
|
|
1483
|
+
for (const p of b.inputs) {
|
|
1484
|
+
lines.push(` ${p.name}: ${p.shape ?? p.pattern}${p.connected_to ? ` <- ${p.connected_to.join(", ")}` : ""}`);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
if (b.outputs.length > 0) {
|
|
1488
|
+
lines.push("", "out");
|
|
1489
|
+
for (const p of b.outputs) {
|
|
1490
|
+
lines.push(` ${p.name}: ${p.shape ?? p.pattern}${p.connected_to ? ` -> ${p.connected_to.join(", ")}` : ""}`);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
if (b.param_errors.length > 0)
|
|
1494
|
+
lines.push("", ...b.param_errors.map((e) => `! ${e}`));
|
|
1495
|
+
return lines.join(`
|
|
1496
|
+
`);
|
|
1497
|
+
}
|
|
1498
|
+
function catalogEntry(def) {
|
|
1499
|
+
const entry = {
|
|
1500
|
+
type: def.type,
|
|
1501
|
+
kind: def.kind,
|
|
1502
|
+
category: def.category,
|
|
1503
|
+
summary: def.docs.summary ?? "",
|
|
1504
|
+
refs: def.docs.refs ?? [],
|
|
1505
|
+
params: def.paramOrder.map((name) => catalogParam(name, def.params[name])),
|
|
1506
|
+
inputs: [],
|
|
1507
|
+
outputs: [],
|
|
1508
|
+
dynamic_ports: false
|
|
1509
|
+
};
|
|
1510
|
+
if (def.docs.formula)
|
|
1511
|
+
entry.formula = def.docs.formula;
|
|
1512
|
+
if (isPrimitive(def) || isComposite(def)) {
|
|
1513
|
+
const declared = Object.keys(def.ports.in).length + Object.keys(def.ports.out).length;
|
|
1514
|
+
entry.dynamic_ports = declared === 0;
|
|
1515
|
+
entry.inputs = Object.entries(def.ports.in).map(([n, shape]) => `${n}: ${shape}`);
|
|
1516
|
+
entry.outputs = Object.entries(def.ports.out).map(([n, shape]) => `${n}: ${shape}`);
|
|
1517
|
+
} else if (isContainer(def)) {
|
|
1518
|
+
entry.inputs = ["(from the container's boundary_in block)"];
|
|
1519
|
+
entry.outputs = ["(from the container's boundary_out block)"];
|
|
1520
|
+
entry.dynamic_ports = true;
|
|
1521
|
+
}
|
|
1522
|
+
return entry;
|
|
1523
|
+
}
|
|
1524
|
+
function catalogParam(name, spec) {
|
|
1525
|
+
const p = { name, type: spec.type };
|
|
1526
|
+
const dflt = spec.default;
|
|
1527
|
+
if (dflt !== undefined)
|
|
1528
|
+
p.default = JSON.stringify(dflt);
|
|
1529
|
+
if (spec.doc)
|
|
1530
|
+
p.doc = spec.doc;
|
|
1531
|
+
if (spec.type === "enum" && spec.values)
|
|
1532
|
+
p.values = [...spec.values];
|
|
1533
|
+
return p;
|
|
1534
|
+
}
|
|
1535
|
+
function allCatalogEntries() {
|
|
1536
|
+
const byCategory = catalogByCategory();
|
|
1537
|
+
return Object.keys(byCategory).sort().flatMap((category) => byCategory[category].map(catalogEntry));
|
|
1538
|
+
}
|
|
1539
|
+
function catalogText(entries) {
|
|
1540
|
+
return entries.map((e) => {
|
|
1541
|
+
const lines = [`${e.type} (${e.kind}/${e.category})`, ` ${e.summary}`];
|
|
1542
|
+
if (e.formula)
|
|
1543
|
+
lines.push(` formula: ${e.formula}`);
|
|
1544
|
+
if (e.params.length > 0) {
|
|
1545
|
+
lines.push(` params: ${e.params.map((p) => `${p.name}:${p.type}${p.default !== undefined ? `=${p.default}` : ""}`).join(", ")}`);
|
|
1546
|
+
}
|
|
1547
|
+
if (e.inputs.length > 0)
|
|
1548
|
+
lines.push(` in: ${e.inputs.join(", ")}`);
|
|
1549
|
+
if (e.outputs.length > 0)
|
|
1550
|
+
lines.push(` out: ${e.outputs.join(", ")}`);
|
|
1551
|
+
return lines.join(`
|
|
1552
|
+
`);
|
|
1553
|
+
}).join(`
|
|
1554
|
+
|
|
1555
|
+
`);
|
|
1556
|
+
}
|
|
1557
|
+
function findingsJson(report) {
|
|
1558
|
+
return report.findings.map((f) => {
|
|
1559
|
+
const out = { rule: f.rule, severity: f.severity, message: f.message };
|
|
1560
|
+
if (f.path)
|
|
1561
|
+
out.path = f.path;
|
|
1562
|
+
if (f.port)
|
|
1563
|
+
out.port = f.port;
|
|
1564
|
+
if (f.hint)
|
|
1565
|
+
out.hint = f.hint;
|
|
1566
|
+
return out;
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
function validationSummary(report, limit = 5) {
|
|
1570
|
+
return {
|
|
1571
|
+
ok: report.ok,
|
|
1572
|
+
counts: report.counts,
|
|
1573
|
+
top_findings: findingsJson(report).slice(0, limit)
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
function findingsText(findings) {
|
|
1577
|
+
if (findings.length === 0)
|
|
1578
|
+
return "No findings.";
|
|
1579
|
+
return findings.map((f) => {
|
|
1580
|
+
const where = f.path ? ` [${f.path}${f.port ? `:${f.port}` : ""}]` : "";
|
|
1581
|
+
return `${f.severity}: ${f.message}${where}${f.hint ? `
|
|
1582
|
+
fix: ${f.hint}` : ""} (${f.rule})`;
|
|
1583
|
+
}).join(`
|
|
1584
|
+
`);
|
|
1585
|
+
}
|
|
1586
|
+
function n(v) {
|
|
1587
|
+
return Number.isFinite(v) ? v : null;
|
|
1588
|
+
}
|
|
1589
|
+
function analysisJson(a) {
|
|
1590
|
+
const o = a.options;
|
|
1591
|
+
return {
|
|
1592
|
+
name: a.name,
|
|
1593
|
+
options: {
|
|
1594
|
+
T: o.T,
|
|
1595
|
+
B: o.B,
|
|
1596
|
+
dtype: o.dtype,
|
|
1597
|
+
hardware: o.hardware.id,
|
|
1598
|
+
gpus: o.gpus,
|
|
1599
|
+
parallel: { ...o.parallel },
|
|
1600
|
+
optimizer: o.optimizer,
|
|
1601
|
+
recompute: o.recompute,
|
|
1602
|
+
tokens: o.tokens,
|
|
1603
|
+
tokens_were_defaulted: o.tokensWereDefaulted,
|
|
1604
|
+
mfu: o.mfu,
|
|
1605
|
+
concurrency: o.concurrency
|
|
1606
|
+
},
|
|
1607
|
+
params: {
|
|
1608
|
+
total: a.params.total,
|
|
1609
|
+
active: a.params.active,
|
|
1610
|
+
embedding: a.params.embedding,
|
|
1611
|
+
head: a.params.head,
|
|
1612
|
+
non_embedding: a.params.nonEmbedding,
|
|
1613
|
+
non_embedding_active: a.params.nonEmbeddingActive,
|
|
1614
|
+
by_category: a.params.byCategory,
|
|
1615
|
+
by_type: a.params.byType
|
|
1616
|
+
},
|
|
1617
|
+
flops: {
|
|
1618
|
+
fwd_dense: n(a.flops.fwdDense),
|
|
1619
|
+
fwd_attention: n(a.flops.fwdAttention),
|
|
1620
|
+
fwd_total: n(a.flops.fwdTotal),
|
|
1621
|
+
elementwise: n(a.flops.elementwise),
|
|
1622
|
+
train_per_token: n(a.flops.trainPerToken),
|
|
1623
|
+
attention_share: n(a.flops.attentionShare)
|
|
1624
|
+
},
|
|
1625
|
+
kv: {
|
|
1626
|
+
bytes_per_token: n(a.kv.bytesPerToken),
|
|
1627
|
+
bytes_per_sequence: n(a.kv.bytesPerToken * o.T + a.kv.bytesPerSequenceFixed),
|
|
1628
|
+
bytes_per_token_decompressed: n(a.kv.bytesPerTokenDecompressed)
|
|
1629
|
+
},
|
|
1630
|
+
memory: {
|
|
1631
|
+
optimizer_label: a.memory.optimizerLabel,
|
|
1632
|
+
train_weights: n(a.memory.train.perGpu.weights),
|
|
1633
|
+
train_grads: n(a.memory.train.perGpu.grads),
|
|
1634
|
+
train_optimizer: n(a.memory.train.perGpu.optimizer),
|
|
1635
|
+
train_activations: n(a.memory.train.perGpu.activations),
|
|
1636
|
+
train_per_gpu: n(a.memory.train.perGpu.total),
|
|
1637
|
+
train_total: n(a.memory.train.total),
|
|
1638
|
+
infer_weights: n(a.memory.infer.weights),
|
|
1639
|
+
infer_kv: n(a.memory.infer.kv),
|
|
1640
|
+
infer_total: n(a.memory.infer.total),
|
|
1641
|
+
device_memory: n(o.hardware.memory),
|
|
1642
|
+
notes: a.memory.notes
|
|
1643
|
+
},
|
|
1644
|
+
throughput: {
|
|
1645
|
+
decode_tokens_per_second: n(a.throughput.decodeTokensPerSecond),
|
|
1646
|
+
decode_weight_bytes: n(a.throughput.decodeWeightBytes),
|
|
1647
|
+
resident_weight_bytes: n(a.throughput.residentWeightBytes),
|
|
1648
|
+
prefill_seconds: n(a.throughput.prefillSeconds),
|
|
1649
|
+
memory_bound: a.throughput.memoryBound,
|
|
1650
|
+
notes: a.throughput.notes
|
|
1651
|
+
},
|
|
1652
|
+
cost: {
|
|
1653
|
+
total_flops: n(a.cost.totalFlops),
|
|
1654
|
+
gpu_hours: n(a.cost.gpuHours),
|
|
1655
|
+
wall_clock_hours: n(a.cost.wallClockHours),
|
|
1656
|
+
dollars: n(a.cost.dollars),
|
|
1657
|
+
tokens: a.cost.tokens
|
|
1658
|
+
},
|
|
1659
|
+
chinchilla: {
|
|
1660
|
+
optimal_tokens: n(a.chinchilla.optimalTokens),
|
|
1661
|
+
tokens_per_param: n(a.chinchilla.tokensPerParam),
|
|
1662
|
+
over_training_ratio: n(a.chinchilla.overTrainingRatio),
|
|
1663
|
+
verdict: a.chinchilla.verdict
|
|
1664
|
+
},
|
|
1665
|
+
errors: a.errors
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
function analysisText(a) {
|
|
1669
|
+
const o = a.options;
|
|
1670
|
+
const fits = a.memory.train.perGpu.total <= o.hardware.memory;
|
|
1671
|
+
return [
|
|
1672
|
+
`${a.name} at T=${o.T} B=${o.B} ${o.dtype} on ${o.gpus} x ${o.hardware.id}`,
|
|
1673
|
+
``,
|
|
1674
|
+
`parameters ${formatCount(a.params.total)} total, ${formatCount(a.params.active)} active, ` + `${formatCount(a.params.nonEmbedding)} non-embedding`,
|
|
1675
|
+
`flops/token ${formatFlops(a.flops.fwdTotal)} forward, ${formatFlops(a.flops.trainPerToken)} training; ` + `attention ${(a.flops.attentionShare * 100).toFixed(1)}%`,
|
|
1676
|
+
`kv cache ${formatBytes(a.kv.bytesPerToken)}/token, ` + `${formatBytes(a.kv.bytesPerToken * o.T + a.kv.bytesPerSequenceFixed)} at T=${o.T}`,
|
|
1677
|
+
`train memory ${formatBytes(a.memory.train.perGpu.total)} per GPU ` + `(weights ${formatBytes(a.memory.train.perGpu.weights)}, optimizer ${formatBytes(a.memory.train.perGpu.optimizer)}, ` + `activations ${formatBytes(a.memory.train.perGpu.activations)}) - ` + `${fits ? "fits" : "does NOT fit"} ${formatBytes(o.hardware.memory)}`,
|
|
1678
|
+
`serve memory ${formatBytes(a.memory.infer.total)} for ${o.concurrency} concurrent sequence(s)`,
|
|
1679
|
+
`throughput ${a.throughput.decodeTokensPerSecond.toFixed(1)} tok/s decode ` + `(${a.throughput.memoryBound ? "memory" : "compute"} bound), prefill ${a.throughput.prefillSeconds.toFixed(2)} s`,
|
|
1680
|
+
`training cost ${formatCount(a.cost.tokens)} tokens, ${a.cost.gpuHours.toFixed(0)} GPU-hours, ` + `$${a.cost.dollars.toFixed(0)}`,
|
|
1681
|
+
`chinchilla ${a.chinchilla.tokensPerParam.toFixed(1)} tokens/param - ${a.chinchilla.verdict}`,
|
|
1682
|
+
...a.errors.length > 0 ? ["", ...a.errors.map((e) => `! ${e}`)] : []
|
|
1683
|
+
].join(`
|
|
1684
|
+
`);
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// packages/mcp/src/resources.ts
|
|
1688
|
+
import { CATALOG, analyze, validate } from "@tensor-cad/engine/node";
|
|
1689
|
+
var JSON_MIME = "application/json";
|
|
1690
|
+
var json = (uri, value) => ({
|
|
1691
|
+
contents: [{ uri: uri.href, mimeType: JSON_MIME, text: `${JSON.stringify(value, null, 2)}
|
|
1692
|
+
` }]
|
|
1693
|
+
});
|
|
1694
|
+
function registerResources(server, store) {
|
|
1695
|
+
server.registerResource("design-validation", new ResourceTemplate("tensorcad://designs/{id}/validation", { list: undefined, complete: { id: completeDesignId(store) } }), {
|
|
1696
|
+
title: "Design rule report",
|
|
1697
|
+
description: "Every finding for a design: shape errors, memory fit, kernel constraints, Chinchilla sanity.",
|
|
1698
|
+
mimeType: JSON_MIME
|
|
1699
|
+
}, (uri, { id }) => {
|
|
1700
|
+
const record = store.get(String(id));
|
|
1701
|
+
const report = validate(record.doc);
|
|
1702
|
+
return json(uri, {
|
|
1703
|
+
design_id: record.design_id,
|
|
1704
|
+
revision: record.revision,
|
|
1705
|
+
name: record.name,
|
|
1706
|
+
ok: report.ok,
|
|
1707
|
+
counts: report.counts,
|
|
1708
|
+
findings: findingsJson(report)
|
|
1709
|
+
});
|
|
1710
|
+
});
|
|
1711
|
+
server.registerResource("design-analysis", new ResourceTemplate("tensorcad://designs/{id}/analysis", { list: undefined, complete: { id: completeDesignId(store) } }), {
|
|
1712
|
+
title: "Design analysis",
|
|
1713
|
+
description: "Parameters, FLOPs, KV cache, memory, throughput and cost at the document's own defaults.",
|
|
1714
|
+
mimeType: JSON_MIME
|
|
1715
|
+
}, (uri, { id }) => {
|
|
1716
|
+
const record = store.get(String(id));
|
|
1717
|
+
const result = analyze(record.doc);
|
|
1718
|
+
return json(uri, { design_id: record.design_id, revision: record.revision, ...analysisJson(result) });
|
|
1719
|
+
});
|
|
1720
|
+
server.registerResource("design", new ResourceTemplate("tensorcad://designs/{id}", {
|
|
1721
|
+
list: () => ({
|
|
1722
|
+
resources: store.list().map((d) => ({
|
|
1723
|
+
uri: `tensorcad://designs/${d.design_id}`,
|
|
1724
|
+
name: d.name,
|
|
1725
|
+
title: `${d.name} (revision ${d.revision})`,
|
|
1726
|
+
description: `${d.source} design${d.dirty ? ", unsaved changes" : ""}`,
|
|
1727
|
+
mimeType: JSON_MIME
|
|
1728
|
+
}))
|
|
1729
|
+
}),
|
|
1730
|
+
complete: { id: completeDesignId(store) }
|
|
1731
|
+
}), {
|
|
1732
|
+
title: "Design document",
|
|
1733
|
+
description: "The literal .tensorcad.json document, with a compact outline beside it.",
|
|
1734
|
+
mimeType: JSON_MIME
|
|
1735
|
+
}, (uri, { id }) => {
|
|
1736
|
+
const record = store.get(String(id));
|
|
1737
|
+
return json(uri, {
|
|
1738
|
+
design_id: record.design_id,
|
|
1739
|
+
revision: record.revision,
|
|
1740
|
+
dirty: record.dirty,
|
|
1741
|
+
...record.path ? { path: record.path } : {},
|
|
1742
|
+
outline: outlineOf(record.doc),
|
|
1743
|
+
document: record.doc
|
|
1744
|
+
});
|
|
1745
|
+
});
|
|
1746
|
+
server.registerResource("catalog", "tensorcad://catalog", {
|
|
1747
|
+
title: "Block catalog",
|
|
1748
|
+
description: "Every block type with its parameter schema, port patterns and documentation. " + "Primitives carry the formulas; composites expand into primitives; repeat is the only container.",
|
|
1749
|
+
mimeType: JSON_MIME
|
|
1750
|
+
}, (uri) => {
|
|
1751
|
+
const blocks = allCatalogEntries();
|
|
1752
|
+
return json(uri, {
|
|
1753
|
+
count: blocks.length,
|
|
1754
|
+
categories: [...new Set(blocks.map((b) => b.category))].sort(),
|
|
1755
|
+
blocks
|
|
1756
|
+
});
|
|
1757
|
+
});
|
|
1758
|
+
server.registerResource("catalog-block", new ResourceTemplate("tensorcad://catalog/{type}", {
|
|
1759
|
+
list: () => ({
|
|
1760
|
+
resources: Object.keys(CATALOG).sort().map((type) => ({
|
|
1761
|
+
uri: `tensorcad://catalog/${type}`,
|
|
1762
|
+
name: type,
|
|
1763
|
+
title: type,
|
|
1764
|
+
description: CATALOG[type].docs.summary,
|
|
1765
|
+
mimeType: JSON_MIME
|
|
1766
|
+
}))
|
|
1767
|
+
}),
|
|
1768
|
+
complete: {
|
|
1769
|
+
type: (value) => Object.keys(CATALOG).filter((t) => t.startsWith(value)).sort().slice(0, 50)
|
|
1770
|
+
}
|
|
1771
|
+
}), { title: "Catalog block", description: "One block type in full.", mimeType: JSON_MIME }, (uri, { type }) => {
|
|
1772
|
+
const def = CATALOG[String(type)];
|
|
1773
|
+
if (!def) {
|
|
1774
|
+
throw new Error(`Unknown block type "${String(type)}". Known: ${Object.keys(CATALOG).sort().join(", ")}`);
|
|
1775
|
+
}
|
|
1776
|
+
return json(uri, catalogEntry(def));
|
|
1777
|
+
});
|
|
1778
|
+
server.registerResource("design-schema", "tensorcad://schema/design", {
|
|
1779
|
+
title: "Design document schema",
|
|
1780
|
+
description: "JSON Schema for the .tensorcad.json format.",
|
|
1781
|
+
mimeType: "application/schema+json"
|
|
1782
|
+
}, (uri) => ({
|
|
1783
|
+
contents: [
|
|
1784
|
+
{
|
|
1785
|
+
uri: uri.href,
|
|
1786
|
+
mimeType: "application/schema+json",
|
|
1787
|
+
text: `${JSON.stringify(DESIGN_JSON_SCHEMA, null, 2)}
|
|
1788
|
+
`
|
|
1789
|
+
}
|
|
1790
|
+
]
|
|
1791
|
+
}));
|
|
1792
|
+
}
|
|
1793
|
+
function completeDesignId(store) {
|
|
1794
|
+
return (value) => store.list().map((d) => d.design_id).filter((id) => id.startsWith(value));
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// packages/mcp/src/tools.ts
|
|
1798
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
|
|
1799
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join3, resolve as resolve2 } from "node:path";
|
|
1800
|
+
import * as z3 from "zod";
|
|
1801
|
+
import { formatBytes as formatBytes2, formatCount as formatCount2 } from "@tensor-cad/engine";
|
|
1802
|
+
import {
|
|
1803
|
+
HARDWARE as HARDWARE2,
|
|
1804
|
+
PRESET_NAMES as PRESET_NAMES3,
|
|
1805
|
+
analyze as analyze2,
|
|
1806
|
+
diffDesigns,
|
|
1807
|
+
explain,
|
|
1808
|
+
generateTorch,
|
|
1809
|
+
getPreset as getPreset2,
|
|
1810
|
+
importHfConfig,
|
|
1811
|
+
mupLadder,
|
|
1812
|
+
planCluster,
|
|
1813
|
+
scaleDesign,
|
|
1814
|
+
validate as validate2
|
|
1815
|
+
} from "@tensor-cad/engine/node";
|
|
1816
|
+
var ok = (text, structured) => ({
|
|
1817
|
+
content: [{ type: "text", text }],
|
|
1818
|
+
structuredContent: structured
|
|
1819
|
+
});
|
|
1820
|
+
async function guard(run) {
|
|
1821
|
+
try {
|
|
1822
|
+
return await run();
|
|
1823
|
+
} catch (e) {
|
|
1824
|
+
return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
function toAnalysisOptions(input) {
|
|
1828
|
+
const out = {};
|
|
1829
|
+
if (input.T !== undefined)
|
|
1830
|
+
out.T = input.T;
|
|
1831
|
+
if (input.B !== undefined)
|
|
1832
|
+
out.B = input.B;
|
|
1833
|
+
if (input.dtype)
|
|
1834
|
+
out.dtype = input.dtype;
|
|
1835
|
+
if (input.tokens !== undefined)
|
|
1836
|
+
out.tokens = input.tokens;
|
|
1837
|
+
if (input.optimizer)
|
|
1838
|
+
out.optimizer = input.optimizer;
|
|
1839
|
+
if (input.recompute)
|
|
1840
|
+
out.recompute = input.recompute;
|
|
1841
|
+
if (input.concurrency !== undefined)
|
|
1842
|
+
out.concurrency = input.concurrency;
|
|
1843
|
+
if (input.mfu !== undefined)
|
|
1844
|
+
out.mfu = input.mfu;
|
|
1845
|
+
if (input.hardware) {
|
|
1846
|
+
if (!HARDWARE2.some((h) => h.id === input.hardware)) {
|
|
1847
|
+
throw new Error(`Unknown hardware "${input.hardware}". Known ids: ${HARDWARE2.map((h) => h.id).join(", ")}.`);
|
|
1848
|
+
}
|
|
1849
|
+
out.hardware = input.hardware;
|
|
1850
|
+
}
|
|
1851
|
+
const parallel = {};
|
|
1852
|
+
if (input.zero !== undefined)
|
|
1853
|
+
parallel.zero = input.zero;
|
|
1854
|
+
if (input.tp !== undefined)
|
|
1855
|
+
parallel.tp = input.tp;
|
|
1856
|
+
if (input.dp !== undefined)
|
|
1857
|
+
parallel.dp = input.dp;
|
|
1858
|
+
if (input.pp !== undefined)
|
|
1859
|
+
parallel.pp = input.pp;
|
|
1860
|
+
if (input.ep !== undefined)
|
|
1861
|
+
parallel.ep = input.ep;
|
|
1862
|
+
if (Object.keys(parallel).length > 0)
|
|
1863
|
+
out.parallel = parallel;
|
|
1864
|
+
if (input.gpus !== undefined)
|
|
1865
|
+
out.gpus = input.gpus;
|
|
1866
|
+
else {
|
|
1867
|
+
const implied = (parallel.dp ?? 1) * (parallel.tp ?? 1) * (parallel.pp ?? 1);
|
|
1868
|
+
if (implied > 1)
|
|
1869
|
+
out.gpus = implied;
|
|
1870
|
+
}
|
|
1871
|
+
return out;
|
|
1872
|
+
}
|
|
1873
|
+
var READ = { readOnlyHint: true, idempotentHint: true, openWorldHint: false };
|
|
1874
|
+
var WRITE = { readOnlyHint: false, idempotentHint: false, openWorldHint: false };
|
|
1875
|
+
var DESTRUCTIVE = { readOnlyHint: false, idempotentHint: false, destructiveHint: true, openWorldHint: false };
|
|
1876
|
+
function registerTools(server, store) {
|
|
1877
|
+
server.registerTool("tensorcad_list_designs", {
|
|
1878
|
+
title: "List designs",
|
|
1879
|
+
description: "List the designs this server has open, the built-in reference architectures you can start from, " + "and the .tensorcad.json files it can see on disk. Start here when you do not already hold a design_id.",
|
|
1880
|
+
inputSchema: z3.object({
|
|
1881
|
+
include_files: z3.boolean().optional().describe("Also scan the working directory for .tensorcad.json files.")
|
|
1882
|
+
}),
|
|
1883
|
+
outputSchema: z3.object({
|
|
1884
|
+
designs: z3.array(DesignSummary),
|
|
1885
|
+
presets: z3.array(z3.object({
|
|
1886
|
+
name: z3.string(),
|
|
1887
|
+
family: z3.string().optional(),
|
|
1888
|
+
published_params: z3.number().optional(),
|
|
1889
|
+
notes: z3.string().optional()
|
|
1890
|
+
})),
|
|
1891
|
+
files: z3.array(z3.string())
|
|
1892
|
+
}),
|
|
1893
|
+
annotations: { ...READ, title: "List designs" }
|
|
1894
|
+
}, async ({ include_files }) => guard(async () => {
|
|
1895
|
+
const designs = store.list();
|
|
1896
|
+
const presets = PRESET_NAMES3.map((name) => {
|
|
1897
|
+
const doc = getPreset2(name);
|
|
1898
|
+
const p = { name };
|
|
1899
|
+
if (doc.meta.family)
|
|
1900
|
+
p.family = doc.meta.family;
|
|
1901
|
+
if (doc.meta.published?.params)
|
|
1902
|
+
p.published_params = doc.meta.published.params;
|
|
1903
|
+
if (doc.meta.notes)
|
|
1904
|
+
p.notes = doc.meta.notes;
|
|
1905
|
+
return p;
|
|
1906
|
+
});
|
|
1907
|
+
const files = include_files ? await store.listFiles() : [];
|
|
1908
|
+
const text = [
|
|
1909
|
+
designs.length > 0 ? `open designs:
|
|
1910
|
+
${designs.map((d) => ` ${d.design_id} ${d.name} rev ${d.revision}${d.dirty ? " (unsaved)" : ""}`).join(`
|
|
1911
|
+
`)}` : "open designs: none. Use tensorcad_new_design or tensorcad_open_design.",
|
|
1912
|
+
"",
|
|
1913
|
+
`presets (${presets.length}):`,
|
|
1914
|
+
...presets.map((p) => ` ${p.name}${p.published_params ? ` ${formatCount2(p.published_params)}` : ""}`),
|
|
1915
|
+
...include_files ? ["", `files (${files.length}):`, ...files.map((f) => ` ${f}`)] : []
|
|
1916
|
+
].join(`
|
|
1917
|
+
`);
|
|
1918
|
+
return ok(text, { designs, presets, files });
|
|
1919
|
+
}));
|
|
1920
|
+
server.registerTool("tensorcad_new_design", {
|
|
1921
|
+
title: "New design",
|
|
1922
|
+
description: "Create a design from a reference architecture, or an empty one with just the B and T runtime symbols. " + "Returns the design_id every other tool needs.",
|
|
1923
|
+
inputSchema: z3.object({
|
|
1924
|
+
preset: z3.string().optional().describe(`One of: ${PRESET_NAMES3.join(", ")}. Omit for an empty design.`),
|
|
1925
|
+
name: z3.string().optional().describe("Name for the new design. Defaults to the preset's name.")
|
|
1926
|
+
}),
|
|
1927
|
+
outputSchema: z3.object({
|
|
1928
|
+
design_id: z3.string(),
|
|
1929
|
+
revision: z3.number().int(),
|
|
1930
|
+
name: z3.string(),
|
|
1931
|
+
source: z3.enum(["preset", "file", "empty"]),
|
|
1932
|
+
params_total: z3.number(),
|
|
1933
|
+
outline: Outline
|
|
1934
|
+
}),
|
|
1935
|
+
annotations: { ...WRITE, title: "New design" }
|
|
1936
|
+
}, async (args) => guard(() => {
|
|
1937
|
+
const record = store.create(args);
|
|
1938
|
+
const outline = outlineOf(record.doc);
|
|
1939
|
+
return ok(`${record.design_id} (revision ${record.revision})
|
|
1940
|
+
|
|
1941
|
+
${outlineText(outline)}`, {
|
|
1942
|
+
design_id: record.design_id,
|
|
1943
|
+
revision: record.revision,
|
|
1944
|
+
name: record.name,
|
|
1945
|
+
source: record.source,
|
|
1946
|
+
params_total: outline.params_total,
|
|
1947
|
+
outline
|
|
1948
|
+
});
|
|
1949
|
+
}));
|
|
1950
|
+
server.registerTool("tensorcad_open_design", {
|
|
1951
|
+
title: "Open design",
|
|
1952
|
+
description: "Load a .tensorcad.json document from disk and return a design_id for it. " + "Opening the same path twice returns the same handle.",
|
|
1953
|
+
inputSchema: z3.object({
|
|
1954
|
+
path: z3.string().describe("Path to a .tensorcad.json file, absolute or relative to the server's directory.")
|
|
1955
|
+
}),
|
|
1956
|
+
outputSchema: z3.object({
|
|
1957
|
+
design_id: z3.string(),
|
|
1958
|
+
revision: z3.number().int(),
|
|
1959
|
+
name: z3.string(),
|
|
1960
|
+
path: z3.string(),
|
|
1961
|
+
params_total: z3.number(),
|
|
1962
|
+
validation: ValidationSummary
|
|
1963
|
+
}),
|
|
1964
|
+
annotations: { ...WRITE, title: "Open design" }
|
|
1965
|
+
}, async ({ path }) => guard(async () => {
|
|
1966
|
+
const record = await store.open(path);
|
|
1967
|
+
const report = validate2(record.doc);
|
|
1968
|
+
return ok(`${record.design_id} ${record.name} revision ${record.revision}
|
|
1969
|
+
` + `${record.path}
|
|
1970
|
+
${formatCount2(report.analysis.params.total)} parameters, ` + `${report.counts.error} error(s), ${report.counts.warning} warning(s)`, {
|
|
1971
|
+
design_id: record.design_id,
|
|
1972
|
+
revision: record.revision,
|
|
1973
|
+
name: record.name,
|
|
1974
|
+
path: record.path ?? path,
|
|
1975
|
+
params_total: report.analysis.params.total,
|
|
1976
|
+
validation: validationSummary(report)
|
|
1977
|
+
});
|
|
1978
|
+
}));
|
|
1979
|
+
server.registerTool("tensorcad_save_design", {
|
|
1980
|
+
title: "Save design",
|
|
1981
|
+
description: "Write a design to disk as .tensorcad.json. Overwrites the file it was opened from unless a path is given.",
|
|
1982
|
+
inputSchema: z3.object({
|
|
1983
|
+
design_id: DESIGN_ID,
|
|
1984
|
+
path: z3.string().optional().describe("Where to write. Defaults to the path it was opened from.")
|
|
1985
|
+
}),
|
|
1986
|
+
outputSchema: z3.object({
|
|
1987
|
+
design_id: z3.string(),
|
|
1988
|
+
revision: z3.number().int(),
|
|
1989
|
+
path: z3.string(),
|
|
1990
|
+
bytes: z3.number().int()
|
|
1991
|
+
}),
|
|
1992
|
+
annotations: { ...DESTRUCTIVE, title: "Save design" }
|
|
1993
|
+
}, async ({ design_id, path }) => guard(async () => {
|
|
1994
|
+
const { record, path: written, bytes } = await store.save(design_id, path);
|
|
1995
|
+
return ok(`wrote ${written} (${bytes} bytes, revision ${record.revision})`, {
|
|
1996
|
+
design_id: record.design_id,
|
|
1997
|
+
revision: record.revision,
|
|
1998
|
+
path: written,
|
|
1999
|
+
bytes
|
|
2000
|
+
});
|
|
2001
|
+
}));
|
|
2002
|
+
server.registerTool("tensorcad_get_design", {
|
|
2003
|
+
title: "Get design",
|
|
2004
|
+
description: 'Read a design. Use format "outline" (the default) first: it is the whole structure, symbol table and ' + 'edge shapes in a fraction of the tokens. Use format "full" only when you need the literal JSON document.',
|
|
2005
|
+
inputSchema: z3.object({
|
|
2006
|
+
design_id: DESIGN_ID,
|
|
2007
|
+
format: z3.enum(["full", "outline"]).optional().describe('"outline" is a compact block/edge summary; "full" is the whole document.')
|
|
2008
|
+
}),
|
|
2009
|
+
outputSchema: z3.object({
|
|
2010
|
+
design_id: z3.string(),
|
|
2011
|
+
revision: z3.number().int(),
|
|
2012
|
+
name: z3.string(),
|
|
2013
|
+
format: z3.enum(["full", "outline"]),
|
|
2014
|
+
dirty: z3.boolean(),
|
|
2015
|
+
path: z3.string().optional(),
|
|
2016
|
+
params_total: z3.number(),
|
|
2017
|
+
params_active: z3.number(),
|
|
2018
|
+
outline: Outline.optional(),
|
|
2019
|
+
document: z3.record(z3.string(), z3.unknown()).optional().describe("The literal design document.")
|
|
2020
|
+
}),
|
|
2021
|
+
annotations: { ...READ, title: "Get design" }
|
|
2022
|
+
}, async ({ design_id, format }) => guard(() => {
|
|
2023
|
+
const record = store.get(design_id);
|
|
2024
|
+
const outline = outlineOf(record.doc);
|
|
2025
|
+
const mode = format ?? "outline";
|
|
2026
|
+
const base = {
|
|
2027
|
+
design_id: record.design_id,
|
|
2028
|
+
revision: record.revision,
|
|
2029
|
+
name: record.name,
|
|
2030
|
+
format: mode,
|
|
2031
|
+
dirty: record.dirty,
|
|
2032
|
+
params_total: outline.params_total,
|
|
2033
|
+
params_active: outline.params_active,
|
|
2034
|
+
...record.path ? { path: record.path } : {}
|
|
2035
|
+
};
|
|
2036
|
+
if (mode === "full") {
|
|
2037
|
+
return ok(JSON.stringify(record.doc, null, 2), {
|
|
2038
|
+
...base,
|
|
2039
|
+
document: record.doc
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
return ok(`${record.design_id} revision ${record.revision}
|
|
2043
|
+
|
|
2044
|
+
${outlineText(outline)}`, {
|
|
2045
|
+
...base,
|
|
2046
|
+
outline
|
|
2047
|
+
});
|
|
2048
|
+
}));
|
|
2049
|
+
server.registerTool("tensorcad_get_block", {
|
|
2050
|
+
title: "Get block",
|
|
2051
|
+
description: "One block of a design: its parameters as written and as resolved, the inferred shape on every port, " + "what each port is wired to, and how many trainable parameters it contributes.",
|
|
2052
|
+
inputSchema: z3.object({
|
|
2053
|
+
design_id: DESIGN_ID,
|
|
2054
|
+
path: z3.string().describe('Block path from the outline, e.g. "layers/block" or "embed".')
|
|
2055
|
+
}),
|
|
2056
|
+
outputSchema: z3.object({
|
|
2057
|
+
design_id: z3.string(),
|
|
2058
|
+
revision: z3.number().int(),
|
|
2059
|
+
path: z3.string(),
|
|
2060
|
+
id: z3.string(),
|
|
2061
|
+
type: z3.string(),
|
|
2062
|
+
kind: z3.string(),
|
|
2063
|
+
category: z3.string(),
|
|
2064
|
+
label: z3.string().optional(),
|
|
2065
|
+
summary: z3.string(),
|
|
2066
|
+
formula: z3.string().optional(),
|
|
2067
|
+
params: z3.record(z3.string(), z3.unknown()),
|
|
2068
|
+
resolved_params: z3.record(z3.string(), z3.unknown()),
|
|
2069
|
+
param_errors: z3.array(z3.string()),
|
|
2070
|
+
inputs: z3.array(BlockPort),
|
|
2071
|
+
outputs: z3.array(BlockPort),
|
|
2072
|
+
params_count: z3.number(),
|
|
2073
|
+
instances: z3.number(),
|
|
2074
|
+
children: z3.array(z3.string())
|
|
2075
|
+
}),
|
|
2076
|
+
annotations: { ...READ, title: "Get block" }
|
|
2077
|
+
}, async ({ design_id, path }) => guard(() => {
|
|
2078
|
+
const record = store.get(design_id);
|
|
2079
|
+
const detail = blockDetail(record.doc, path);
|
|
2080
|
+
return ok(blockText(detail), {
|
|
2081
|
+
design_id: record.design_id,
|
|
2082
|
+
revision: record.revision,
|
|
2083
|
+
...detail
|
|
2084
|
+
});
|
|
2085
|
+
}));
|
|
2086
|
+
server.registerTool("tensorcad_search_catalog", {
|
|
2087
|
+
title: "Search catalog",
|
|
2088
|
+
description: "Search the block catalog. Returns each block's parameter schema, port patterns and documentation, " + "which is what you need before adding a block with tensorcad_apply_ops.",
|
|
2089
|
+
inputSchema: z3.object({
|
|
2090
|
+
query: z3.string().optional().describe("Substring matched against type, category, summary and formula."),
|
|
2091
|
+
category: z3.string().optional().describe("attention, mlp, norm, embedding, container, io, ..."),
|
|
2092
|
+
kind: z3.enum(["primitive", "composite", "container"]).optional(),
|
|
2093
|
+
limit: z3.number().int().positive().max(100).optional().describe("Default 20.")
|
|
2094
|
+
}),
|
|
2095
|
+
outputSchema: z3.object({
|
|
2096
|
+
total: z3.number().int().describe("Matches before the limit was applied."),
|
|
2097
|
+
categories: z3.array(z3.string()),
|
|
2098
|
+
blocks: z3.array(z3.object({
|
|
2099
|
+
type: z3.string(),
|
|
2100
|
+
kind: z3.string(),
|
|
2101
|
+
category: z3.string(),
|
|
2102
|
+
summary: z3.string(),
|
|
2103
|
+
formula: z3.string().optional(),
|
|
2104
|
+
refs: z3.array(z3.string()),
|
|
2105
|
+
params: z3.array(z3.object({
|
|
2106
|
+
name: z3.string(),
|
|
2107
|
+
type: z3.string(),
|
|
2108
|
+
default: z3.string().optional(),
|
|
2109
|
+
doc: z3.string().optional(),
|
|
2110
|
+
values: z3.array(z3.string()).optional()
|
|
2111
|
+
})),
|
|
2112
|
+
inputs: z3.array(z3.string()),
|
|
2113
|
+
outputs: z3.array(z3.string()),
|
|
2114
|
+
dynamic_ports: z3.boolean()
|
|
2115
|
+
}))
|
|
2116
|
+
}),
|
|
2117
|
+
annotations: { ...READ, title: "Search catalog" }
|
|
2118
|
+
}, async ({ query, category, kind, limit }) => guard(() => {
|
|
2119
|
+
const all = allCatalogEntries();
|
|
2120
|
+
const q = query?.toLowerCase();
|
|
2121
|
+
const matched = all.filter((e) => {
|
|
2122
|
+
if (category && e.category !== category)
|
|
2123
|
+
return false;
|
|
2124
|
+
if (kind && e.kind !== kind)
|
|
2125
|
+
return false;
|
|
2126
|
+
if (!q)
|
|
2127
|
+
return true;
|
|
2128
|
+
const hay = `${e.type} ${e.category} ${e.summary} ${e.formula ?? ""}`.toLowerCase();
|
|
2129
|
+
return hay.includes(q);
|
|
2130
|
+
});
|
|
2131
|
+
const blocks = matched.slice(0, limit ?? 20);
|
|
2132
|
+
const categories = [...new Set(all.map((e) => e.category))].sort();
|
|
2133
|
+
const header = `${matched.length} match(es)${matched.length > blocks.length ? `, showing ${blocks.length}` : ""}`;
|
|
2134
|
+
return ok(`${header}
|
|
2135
|
+
|
|
2136
|
+
${catalogText(blocks)}`, {
|
|
2137
|
+
total: matched.length,
|
|
2138
|
+
categories,
|
|
2139
|
+
blocks
|
|
2140
|
+
});
|
|
2141
|
+
}));
|
|
2142
|
+
server.registerTool("tensorcad_apply_ops", {
|
|
2143
|
+
title: "Apply edits",
|
|
2144
|
+
description: "Apply a batch of edits to a design. The batch is all-or-nothing: the first rejected operation aborts it " + "and the design is left untouched. Pass expected_revision to be told about a concurrent edit instead of " + "silently overwriting it. Returns the new revision, what changed, and a fresh validation summary.",
|
|
2145
|
+
inputSchema: z3.object({
|
|
2146
|
+
design_id: DESIGN_ID,
|
|
2147
|
+
expected_revision: z3.number().int().optional().describe("Revision you last read. The call is rejected if the design has moved on."),
|
|
2148
|
+
ops: z3.array(Op).min(1).describe("Edits applied in order.")
|
|
2149
|
+
}),
|
|
2150
|
+
outputSchema: z3.object({
|
|
2151
|
+
design_id: z3.string(),
|
|
2152
|
+
revision: z3.number().int(),
|
|
2153
|
+
previous_revision: z3.number().int(),
|
|
2154
|
+
name: z3.string(),
|
|
2155
|
+
applied: z3.array(z3.string()).describe("One line per operation, in order."),
|
|
2156
|
+
params_total: z3.number(),
|
|
2157
|
+
params_active: z3.number(),
|
|
2158
|
+
params_delta: z3.number().describe("Change in total parameters caused by this batch."),
|
|
2159
|
+
validation: ValidationSummary
|
|
2160
|
+
}),
|
|
2161
|
+
annotations: { ...WRITE, title: "Apply edits" }
|
|
2162
|
+
}, async ({ design_id, expected_revision, ops }) => guard(() => {
|
|
2163
|
+
const before = store.get(design_id);
|
|
2164
|
+
const paramsBefore = outlineOf(before.doc).params_total;
|
|
2165
|
+
const outcome = store.apply(design_id, ops, expected_revision);
|
|
2166
|
+
const report = validate2(outcome.record.doc);
|
|
2167
|
+
const total = report.analysis.params.total;
|
|
2168
|
+
const delta = total - paramsBefore;
|
|
2169
|
+
const text = [
|
|
2170
|
+
`${outcome.record.design_id} revision ${outcome.previousRevision} -> ${outcome.record.revision}`,
|
|
2171
|
+
...outcome.applied.map((a) => ` ${a}`),
|
|
2172
|
+
"",
|
|
2173
|
+
`parameters ${formatCount2(total)}` + (delta === 0 ? " (unchanged)" : ` (${delta > 0 ? "+" : ""}${formatCount2(delta)})`),
|
|
2174
|
+
`${report.counts.error} error(s), ${report.counts.warning} warning(s)`,
|
|
2175
|
+
...report.findings.length > 0 ? ["", findingsText(findingsJson(report).slice(0, 5))] : []
|
|
2176
|
+
].join(`
|
|
2177
|
+
`);
|
|
2178
|
+
return ok(text, {
|
|
2179
|
+
design_id: outcome.record.design_id,
|
|
2180
|
+
revision: outcome.record.revision,
|
|
2181
|
+
previous_revision: outcome.previousRevision,
|
|
2182
|
+
name: outcome.record.name,
|
|
2183
|
+
applied: outcome.applied,
|
|
2184
|
+
params_total: total,
|
|
2185
|
+
params_active: report.analysis.params.active,
|
|
2186
|
+
params_delta: delta,
|
|
2187
|
+
validation: validationSummary(report)
|
|
2188
|
+
});
|
|
2189
|
+
}));
|
|
2190
|
+
server.registerTool("tensorcad_validate", {
|
|
2191
|
+
title: "Validate design",
|
|
2192
|
+
description: "Run every design rule: shape and symbol errors, kernel-friendly head dimensions, tensor-core multiples, " + "whether training and serving fit the chosen device, Chinchilla sanity and drift from published numbers. " + "Each finding carries a fix hint.",
|
|
2193
|
+
inputSchema: z3.object({
|
|
2194
|
+
design_id: DESIGN_ID,
|
|
2195
|
+
severity: z3.enum(["error", "warning", "info"]).optional().describe("Only return findings at least this bad."),
|
|
2196
|
+
...analysisOptionsShape
|
|
2197
|
+
}),
|
|
2198
|
+
outputSchema: z3.object({
|
|
2199
|
+
design_id: z3.string(),
|
|
2200
|
+
revision: z3.number().int(),
|
|
2201
|
+
name: z3.string(),
|
|
2202
|
+
ok: z3.boolean(),
|
|
2203
|
+
counts: Counts,
|
|
2204
|
+
findings: z3.array(Finding),
|
|
2205
|
+
params_total: z3.number()
|
|
2206
|
+
}),
|
|
2207
|
+
annotations: { ...READ, title: "Validate design" }
|
|
2208
|
+
}, async ({ design_id, severity, ...rest }) => guard(() => {
|
|
2209
|
+
const record = store.get(design_id);
|
|
2210
|
+
const report = validate2(record.doc, toAnalysisOptions(rest));
|
|
2211
|
+
const rank = { error: 0, warning: 1, info: 2 };
|
|
2212
|
+
const findings = findingsJson(report).filter((f) => severity === undefined || rank[f.severity] <= rank[severity]);
|
|
2213
|
+
const text = [
|
|
2214
|
+
`${record.name} (${record.design_id} revision ${record.revision})`,
|
|
2215
|
+
`${report.ok ? "ok" : "FAILED"}: ${report.counts.error} error(s), ` + `${report.counts.warning} warning(s), ${report.counts.info} info`,
|
|
2216
|
+
"",
|
|
2217
|
+
findingsText(findings)
|
|
2218
|
+
].join(`
|
|
2219
|
+
`);
|
|
2220
|
+
return ok(text, {
|
|
2221
|
+
design_id: record.design_id,
|
|
2222
|
+
revision: record.revision,
|
|
2223
|
+
name: record.name,
|
|
2224
|
+
ok: report.ok,
|
|
2225
|
+
counts: report.counts,
|
|
2226
|
+
findings,
|
|
2227
|
+
params_total: report.analysis.params.total
|
|
2228
|
+
});
|
|
2229
|
+
}));
|
|
2230
|
+
server.registerTool("tensorcad_analyze", {
|
|
2231
|
+
title: "Analyze design",
|
|
2232
|
+
description: "Parameters, FLOPs per token, KV cache, training and serving memory, decode throughput, training cost and " + "Chinchilla position, for a given sequence length, batch, dtype, device, GPU count and parallel plan.",
|
|
2233
|
+
inputSchema: z3.object({ design_id: DESIGN_ID, ...analysisOptionsShape }),
|
|
2234
|
+
outputSchema: AnalysisOutput,
|
|
2235
|
+
annotations: { ...READ, title: "Analyze design" }
|
|
2236
|
+
}, async ({ design_id, ...rest }) => guard(() => {
|
|
2237
|
+
const record = store.get(design_id);
|
|
2238
|
+
const result = analyze2(record.doc, toAnalysisOptions(rest));
|
|
2239
|
+
return ok(analysisText(result), {
|
|
2240
|
+
design_id: record.design_id,
|
|
2241
|
+
revision: record.revision,
|
|
2242
|
+
...analysisJson(result)
|
|
2243
|
+
});
|
|
2244
|
+
}));
|
|
2245
|
+
server.registerTool("tensorcad_generate_code", {
|
|
2246
|
+
title: "Generate code",
|
|
2247
|
+
description: "Emit a runnable PyTorch module plus the design document. Without out_dir the file contents come back in " + "the result; with out_dir they are written to disk and only a manifest comes back.",
|
|
2248
|
+
inputSchema: z3.object({
|
|
2249
|
+
design_id: DESIGN_ID,
|
|
2250
|
+
class_name: z3.string().optional().describe("Class name for the top-level module."),
|
|
2251
|
+
include_smoke_test: z3.boolean().optional().describe("Emit a __main__ block that checks the size."),
|
|
2252
|
+
out_dir: z3.string().optional().describe("Directory to write into. Omit to get the contents inline.")
|
|
2253
|
+
}),
|
|
2254
|
+
outputSchema: z3.object({
|
|
2255
|
+
design_id: z3.string(),
|
|
2256
|
+
revision: z3.number().int(),
|
|
2257
|
+
wrote: z3.boolean(),
|
|
2258
|
+
out_dir: z3.string().optional(),
|
|
2259
|
+
files: z3.array(z3.object({
|
|
2260
|
+
path: z3.string(),
|
|
2261
|
+
bytes: z3.number().int(),
|
|
2262
|
+
lines: z3.number().int(),
|
|
2263
|
+
contents: z3.string().optional().describe("Present only when out_dir was not given."),
|
|
2264
|
+
written_to: z3.string().optional()
|
|
2265
|
+
})),
|
|
2266
|
+
warnings: z3.array(z3.string())
|
|
2267
|
+
}),
|
|
2268
|
+
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false, title: "Generate code" }
|
|
2269
|
+
}, async ({ design_id, class_name, include_smoke_test, out_dir }) => guard(async () => {
|
|
2270
|
+
const record = store.get(design_id);
|
|
2271
|
+
const generated = generateTorch(record.doc, {
|
|
2272
|
+
...class_name ? { className: class_name } : {},
|
|
2273
|
+
includeSmokeTest: include_smoke_test ?? false
|
|
2274
|
+
});
|
|
2275
|
+
const root = out_dir ? isAbsolute2(out_dir) ? out_dir : resolve2(process.cwd(), out_dir) : undefined;
|
|
2276
|
+
const files = [];
|
|
2277
|
+
for (const file of generated.files) {
|
|
2278
|
+
const bytes = Buffer.byteLength(file.contents, "utf8");
|
|
2279
|
+
const lines = file.contents.split(`
|
|
2280
|
+
`).length;
|
|
2281
|
+
if (root) {
|
|
2282
|
+
const target = join3(root, file.path);
|
|
2283
|
+
await mkdir3(dirname3(target), { recursive: true });
|
|
2284
|
+
await writeFile3(target, file.contents, "utf8");
|
|
2285
|
+
files.push({ path: file.path, bytes, lines, written_to: target });
|
|
2286
|
+
} else {
|
|
2287
|
+
files.push({ path: file.path, bytes, lines, contents: file.contents });
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
const text = root ? [`wrote ${files.length} file(s) to ${root}`, ...files.map((f) => ` ${f.path} ${f.bytes} bytes`)].join(`
|
|
2291
|
+
`) : generated.files.map((f) => `# ${f.path}
|
|
2292
|
+
${f.contents}`).join(`
|
|
2293
|
+
|
|
2294
|
+
`);
|
|
2295
|
+
return ok(generated.warnings.length > 0 ? `${text}
|
|
2296
|
+
|
|
2297
|
+
warnings:
|
|
2298
|
+
${generated.warnings.map((w) => ` ${w}`).join(`
|
|
2299
|
+
`)}` : text, {
|
|
2300
|
+
design_id: record.design_id,
|
|
2301
|
+
revision: record.revision,
|
|
2302
|
+
wrote: Boolean(root),
|
|
2303
|
+
...root ? { out_dir: root } : {},
|
|
2304
|
+
files,
|
|
2305
|
+
warnings: generated.warnings
|
|
2306
|
+
});
|
|
2307
|
+
}));
|
|
2308
|
+
server.registerTool("tensorcad_checkpoint", {
|
|
2309
|
+
title: "Checkpoint design",
|
|
2310
|
+
description: "Snapshot a design under a name you can come back to. Take one before an experiment so tensorcad_restore " + "can put it back exactly.",
|
|
2311
|
+
inputSchema: z3.object({
|
|
2312
|
+
design_id: DESIGN_ID,
|
|
2313
|
+
label: z3.string().optional().describe('What this snapshot is, e.g. "before widening the FFN".')
|
|
2314
|
+
}),
|
|
2315
|
+
outputSchema: z3.object({
|
|
2316
|
+
design_id: z3.string(),
|
|
2317
|
+
checkpoint_id: z3.string(),
|
|
2318
|
+
label: z3.string(),
|
|
2319
|
+
revision: z3.number().int(),
|
|
2320
|
+
created_at: z3.string(),
|
|
2321
|
+
checkpoints: z3.array(z3.object({
|
|
2322
|
+
checkpoint_id: z3.string(),
|
|
2323
|
+
label: z3.string(),
|
|
2324
|
+
revision: z3.number().int(),
|
|
2325
|
+
created_at: z3.string()
|
|
2326
|
+
}))
|
|
2327
|
+
}),
|
|
2328
|
+
annotations: { ...WRITE, title: "Checkpoint design" }
|
|
2329
|
+
}, async ({ design_id, label }) => guard(() => {
|
|
2330
|
+
const info = store.checkpoint(design_id, label);
|
|
2331
|
+
return ok(`${info.checkpoint_id} at revision ${info.revision}: ${info.label}`, {
|
|
2332
|
+
design_id,
|
|
2333
|
+
...info,
|
|
2334
|
+
checkpoints: store.checkpoints(design_id)
|
|
2335
|
+
});
|
|
2336
|
+
}));
|
|
2337
|
+
server.registerTool("tensorcad_restore", {
|
|
2338
|
+
title: "Restore design",
|
|
2339
|
+
description: "Put a design back. With a checkpoint_id it restores that snapshot; without one it undoes the most recent " + "tensorcad_apply_ops batch. Either way the revision moves forward, so a stale expected_revision still fails.",
|
|
2340
|
+
inputSchema: z3.object({
|
|
2341
|
+
design_id: DESIGN_ID,
|
|
2342
|
+
checkpoint_id: z3.string().optional().describe("Omit to undo the last batch of edits.")
|
|
2343
|
+
}),
|
|
2344
|
+
outputSchema: z3.object({
|
|
2345
|
+
design_id: z3.string(),
|
|
2346
|
+
revision: z3.number().int(),
|
|
2347
|
+
name: z3.string(),
|
|
2348
|
+
restored_from: z3.string(),
|
|
2349
|
+
params_total: z3.number(),
|
|
2350
|
+
validation: ValidationSummary
|
|
2351
|
+
}),
|
|
2352
|
+
annotations: { ...DESTRUCTIVE, title: "Restore design" }
|
|
2353
|
+
}, async ({ design_id, checkpoint_id }) => guard(() => {
|
|
2354
|
+
const { record, restoredFrom } = store.restore(design_id, checkpoint_id);
|
|
2355
|
+
const report = validate2(record.doc);
|
|
2356
|
+
return ok(`${record.design_id} restored from ${restoredFrom}; now revision ${record.revision}, ` + `${formatCount2(report.analysis.params.total)} parameters`, {
|
|
2357
|
+
design_id: record.design_id,
|
|
2358
|
+
revision: record.revision,
|
|
2359
|
+
name: record.name,
|
|
2360
|
+
restored_from: restoredFrom,
|
|
2361
|
+
params_total: report.analysis.params.total,
|
|
2362
|
+
validation: validationSummary(report)
|
|
2363
|
+
});
|
|
2364
|
+
}));
|
|
2365
|
+
server.registerTool("tensorcad_explain", {
|
|
2366
|
+
title: "Explain a block",
|
|
2367
|
+
description: "What one block is and what it contributes: its parameters as written and as evaluated, the shape on " + "every port, its share of the model's weights and compute, and its documentation. Use it to answer " + '"why is this block this size" without reading the whole design.',
|
|
2368
|
+
inputSchema: z3.object({
|
|
2369
|
+
design_id: DESIGN_ID,
|
|
2370
|
+
path: z3.string().describe('Full path of the block, e.g. "layers/block/attn".'),
|
|
2371
|
+
...analysisOptionsShape
|
|
2372
|
+
}),
|
|
2373
|
+
outputSchema: z3.object({
|
|
2374
|
+
design_id: z3.string(),
|
|
2375
|
+
revision: z3.number().int(),
|
|
2376
|
+
path: z3.string(),
|
|
2377
|
+
type: z3.string(),
|
|
2378
|
+
kind: z3.string(),
|
|
2379
|
+
summary: z3.string().optional(),
|
|
2380
|
+
params: z3.number(),
|
|
2381
|
+
share_of_params: z3.number(),
|
|
2382
|
+
flops_per_token: z3.number(),
|
|
2383
|
+
share_of_flops: z3.number(),
|
|
2384
|
+
copies: z3.object({ total: z3.number(), active: z3.number() }),
|
|
2385
|
+
parameters: z3.array(z3.object({
|
|
2386
|
+
name: z3.string(),
|
|
2387
|
+
expression: z3.string().optional(),
|
|
2388
|
+
value: z3.number().optional(),
|
|
2389
|
+
doc: z3.string().optional()
|
|
2390
|
+
})),
|
|
2391
|
+
ports: z3.object({
|
|
2392
|
+
in: z3.record(z3.string(), z3.string()),
|
|
2393
|
+
out: z3.record(z3.string(), z3.string())
|
|
2394
|
+
})
|
|
2395
|
+
}),
|
|
2396
|
+
annotations: { ...READ, title: "Explain a block" }
|
|
2397
|
+
}, async ({ design_id, path, ...rest }) => guard(() => {
|
|
2398
|
+
const record = store.get(design_id);
|
|
2399
|
+
const e = explain(record.doc, path, toAnalysisOptions(rest));
|
|
2400
|
+
const lines = [
|
|
2401
|
+
`${path} ${e.type} (${e.kind})`,
|
|
2402
|
+
e.docs.summary ?? "",
|
|
2403
|
+
`parameters ${formatCount2(e.contributes.params)} ${(e.contributes.shareOfParams * 100).toFixed(1)}% of the model`,
|
|
2404
|
+
`FLOPs/token ${formatCount2(e.contributes.flopsPerToken)} ${(e.contributes.shareOfFlops * 100).toFixed(1)}%`,
|
|
2405
|
+
`copies ${e.copies.total} total, ${e.copies.active} active per token`,
|
|
2406
|
+
"",
|
|
2407
|
+
...e.paramOrder.map((name) => {
|
|
2408
|
+
const p = e.params[name];
|
|
2409
|
+
const written = p.expression !== undefined && String(p.expression) !== String(p.value);
|
|
2410
|
+
return ` ${name} = ${p.value ?? "—"}${written ? ` (${p.expression})` : ""}`;
|
|
2411
|
+
})
|
|
2412
|
+
];
|
|
2413
|
+
return ok(lines.filter((l) => l !== "").join(`
|
|
2414
|
+
`), {
|
|
2415
|
+
design_id: record.design_id,
|
|
2416
|
+
revision: record.revision,
|
|
2417
|
+
path,
|
|
2418
|
+
type: e.type,
|
|
2419
|
+
kind: e.kind,
|
|
2420
|
+
...e.docs.summary ? { summary: e.docs.summary } : {},
|
|
2421
|
+
params: e.contributes.params,
|
|
2422
|
+
share_of_params: e.contributes.shareOfParams,
|
|
2423
|
+
flops_per_token: e.contributes.flopsPerToken,
|
|
2424
|
+
share_of_flops: e.contributes.shareOfFlops,
|
|
2425
|
+
copies: e.copies,
|
|
2426
|
+
parameters: e.paramOrder.map((name) => ({
|
|
2427
|
+
name,
|
|
2428
|
+
...e.params[name].expression !== undefined ? { expression: String(e.params[name].expression) } : {},
|
|
2429
|
+
...typeof e.params[name].value === "number" ? { value: e.params[name].value } : {},
|
|
2430
|
+
...e.params[name].doc ? { doc: e.params[name].doc } : {}
|
|
2431
|
+
})),
|
|
2432
|
+
ports: {
|
|
2433
|
+
in: Object.fromEntries(Object.entries(e.shapes.in).map(([k, v]) => [k, String(v)])),
|
|
2434
|
+
out: Object.fromEntries(Object.entries(e.shapes.out).map(([k, v]) => [k, String(v)]))
|
|
2435
|
+
}
|
|
2436
|
+
});
|
|
2437
|
+
}));
|
|
2438
|
+
server.registerTool("tensorcad_scale", {
|
|
2439
|
+
title: "Scale a design",
|
|
2440
|
+
description: "Shrink a design towards a parameter budget while keeping its proportions, and save the result as a new " + "design. Use it to get a bench-sized proxy of a large architecture: the widths and depth move together, " + "the head dimension stays sane, and the result is reported with how close it landed.",
|
|
2441
|
+
inputSchema: z3.object({
|
|
2442
|
+
design_id: DESIGN_ID,
|
|
2443
|
+
target_params: z3.number().positive().describe("The parameter count to aim for."),
|
|
2444
|
+
target_basis: z3.enum(["total", "non-embedding"]).optional().describe('Whether target_params counts the embedding tables. At bench sizes "non-embedding" is usually meant.'),
|
|
2445
|
+
vocab: z3.number().int().positive().optional().describe("Replace the vocabulary, for a smaller tokenizer."),
|
|
2446
|
+
tie_head: z3.boolean().optional().describe("Share the output projection with the embedding."),
|
|
2447
|
+
keep_depth: z3.boolean().optional().describe("Hold the layer count fixed and move only the width.")
|
|
2448
|
+
}),
|
|
2449
|
+
outputSchema: z3.object({
|
|
2450
|
+
design_id: z3.string().describe("The new design, saved in this session."),
|
|
2451
|
+
from: z3.string(),
|
|
2452
|
+
name: z3.string(),
|
|
2453
|
+
achieved: z3.number(),
|
|
2454
|
+
target: z3.number(),
|
|
2455
|
+
changes: z3.array(z3.object({ symbol: z3.string(), from: z3.number(), to: z3.number() })),
|
|
2456
|
+
notes: z3.array(z3.string())
|
|
2457
|
+
}),
|
|
2458
|
+
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false, title: "Scale a design" }
|
|
2459
|
+
}, async ({ design_id, target_params, target_basis, vocab, tie_head, keep_depth }) => guard(() => {
|
|
2460
|
+
const record = store.get(design_id);
|
|
2461
|
+
const result = scaleDesign(record.doc, {
|
|
2462
|
+
targetParams: target_params,
|
|
2463
|
+
...target_basis ? { targetBasis: target_basis } : {},
|
|
2464
|
+
...vocab !== undefined ? { vocab } : {},
|
|
2465
|
+
...tie_head !== undefined ? { tieHead: tie_head } : {},
|
|
2466
|
+
...keep_depth !== undefined ? { keepDepth: keep_depth } : {}
|
|
2467
|
+
});
|
|
2468
|
+
const saved = store.adopt(result.doc);
|
|
2469
|
+
const changes = Object.entries(result.changes).map(([symbol, c]) => ({
|
|
2470
|
+
symbol,
|
|
2471
|
+
from: c.from,
|
|
2472
|
+
to: c.to
|
|
2473
|
+
}));
|
|
2474
|
+
const text = [
|
|
2475
|
+
`${result.doc.meta.name}: ${formatCount2(result.achieved)} against a target of ${formatCount2(result.target)}`,
|
|
2476
|
+
...changes.map((c) => ` ${c.symbol} ${c.from} -> ${c.to}`),
|
|
2477
|
+
...result.notes.map((n) => ` note: ${n}`)
|
|
2478
|
+
].join(`
|
|
2479
|
+
`);
|
|
2480
|
+
return ok(text, {
|
|
2481
|
+
design_id: saved.design_id,
|
|
2482
|
+
from: record.design_id,
|
|
2483
|
+
name: result.doc.meta.name,
|
|
2484
|
+
achieved: result.achieved,
|
|
2485
|
+
target: result.target,
|
|
2486
|
+
changes,
|
|
2487
|
+
notes: result.notes
|
|
2488
|
+
});
|
|
2489
|
+
}));
|
|
2490
|
+
server.registerTool("tensorcad_mup", {
|
|
2491
|
+
title: "Build a width ladder",
|
|
2492
|
+
description: "The same design at several widths, with what to multiply the initialization and the learning rate by at " + "each, following Tensor Programs V. Sweep hyperparameters at the narrow end and carry the answer up: a " + "learning rate tuned at the base rung is the right one at every rung, scaled per class. Every rung is " + "saved as a design of its own, ready to analyze or generate. It does not choose a learning rate; that is " + "what the sweep is for.",
|
|
2493
|
+
inputSchema: z3.object({
|
|
2494
|
+
design_id: DESIGN_ID,
|
|
2495
|
+
widths: z3.array(z3.number().positive()).optional().describe("The widths to build. Omit to halve the design's own width down to a width worth sweeping at."),
|
|
2496
|
+
base_width: z3.number().positive().optional().describe("The width the sweep happens at. Omit for the narrowest.")
|
|
2497
|
+
}),
|
|
2498
|
+
outputSchema: z3.object({
|
|
2499
|
+
width_symbol: z3.string(),
|
|
2500
|
+
base_width: z3.number(),
|
|
2501
|
+
head_dim: z3.number().describe("Held fixed while the width moves: the heads get more numerous, not wider."),
|
|
2502
|
+
rungs: z3.array(z3.object({
|
|
2503
|
+
design_id: z3.string().describe("The rung, saved in this session."),
|
|
2504
|
+
width: z3.number(),
|
|
2505
|
+
multiplier: z3.number().describe("Width over the base width: the m every rule is written in."),
|
|
2506
|
+
heads: z3.number(),
|
|
2507
|
+
params: z3.number(),
|
|
2508
|
+
base: z3.boolean(),
|
|
2509
|
+
scaling: z3.array(z3.object({
|
|
2510
|
+
class: z3.enum(["input", "hidden", "output"]),
|
|
2511
|
+
init_std: z3.number().describe("Multiplies the base model's initialization standard deviation."),
|
|
2512
|
+
adam_lr: z3.number().describe("Multiplies the base model's learning rate."),
|
|
2513
|
+
paths: z3.array(z3.string()),
|
|
2514
|
+
why: z3.string()
|
|
2515
|
+
})),
|
|
2516
|
+
notes: z3.array(z3.string())
|
|
2517
|
+
})),
|
|
2518
|
+
notes: z3.array(z3.string())
|
|
2519
|
+
}),
|
|
2520
|
+
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false, title: "Build a width ladder" }
|
|
2521
|
+
}, async ({ design_id, widths, base_width }) => guard(() => {
|
|
2522
|
+
const record = store.get(design_id);
|
|
2523
|
+
const ladder = mupLadder(record.doc, {
|
|
2524
|
+
...widths ? { widths } : {},
|
|
2525
|
+
...base_width !== undefined ? { baseWidth: base_width } : {}
|
|
2526
|
+
});
|
|
2527
|
+
const rungs = ladder.rungs.map((rung) => ({
|
|
2528
|
+
design_id: store.adopt(rung.doc).design_id,
|
|
2529
|
+
width: rung.width,
|
|
2530
|
+
multiplier: rung.multiplier,
|
|
2531
|
+
heads: rung.heads,
|
|
2532
|
+
params: rung.params,
|
|
2533
|
+
base: rung.base,
|
|
2534
|
+
scaling: rung.scaling.map((s) => ({
|
|
2535
|
+
class: s.class,
|
|
2536
|
+
init_std: s.initStd,
|
|
2537
|
+
adam_lr: s.adamLr,
|
|
2538
|
+
paths: s.paths,
|
|
2539
|
+
why: s.why
|
|
2540
|
+
})),
|
|
2541
|
+
notes: rung.notes
|
|
2542
|
+
}));
|
|
2543
|
+
const text = [
|
|
2544
|
+
`${record.doc.meta.name} laddered by ${ladder.widthSymbol}, tuned at ${ladder.baseWidth}, ` + `heads of ${ladder.headDim} throughout`,
|
|
2545
|
+
...rungs.map((r) => ` ${r.base ? "base " : " "}${r.width} wide, ${r.heads} heads, ${formatCount2(r.params)}` + ` ${r.scaling.filter((s) => s.class !== "input").map((s) => `${s.class} init x${s.init_std.toPrecision(4)} rate x${s.adam_lr.toPrecision(4)}`).join(", ")}`),
|
|
2546
|
+
...ladder.notes.map((n) => ` note: ${n}`)
|
|
2547
|
+
].join(`
|
|
2548
|
+
`);
|
|
2549
|
+
return ok(text, {
|
|
2550
|
+
width_symbol: ladder.widthSymbol,
|
|
2551
|
+
base_width: ladder.baseWidth,
|
|
2552
|
+
head_dim: ladder.headDim,
|
|
2553
|
+
rungs,
|
|
2554
|
+
notes: ladder.notes
|
|
2555
|
+
});
|
|
2556
|
+
}));
|
|
2557
|
+
server.registerTool("tensorcad_plan", {
|
|
2558
|
+
title: "Plan a cluster",
|
|
2559
|
+
description: "Every way of splitting the training across a cluster that fits, least demanding first. Prices data, " + "tensor, pipeline and expert parallelism, the four ZeRO stages, sequence parallelism and the three " + "recompute settings. Memory is the claim and it is arithmetic; which plan is fastest is not claimed, so " + "each one carries a note about what it costs to run.",
|
|
2560
|
+
inputSchema: z3.object({
|
|
2561
|
+
design_id: DESIGN_ID,
|
|
2562
|
+
...analysisOptionsShape,
|
|
2563
|
+
gpus: z3.number().int().positive().describe("How many devices there are."),
|
|
2564
|
+
gpus_per_node: z3.number().int().positive().optional().describe("Bounds the tensor-parallel degree. Default 8."),
|
|
2565
|
+
headroom: z3.number().positive().max(0.9).optional().describe("Fraction of device memory left free. Default 0.1."),
|
|
2566
|
+
limit: z3.number().int().positive().optional().describe("How many plans to return. Default 8.")
|
|
2567
|
+
}),
|
|
2568
|
+
outputSchema: z3.object({
|
|
2569
|
+
design_id: z3.string(),
|
|
2570
|
+
revision: z3.number().int(),
|
|
2571
|
+
hardware: z3.string(),
|
|
2572
|
+
budget_bytes: z3.number(),
|
|
2573
|
+
considered: z3.number().int(),
|
|
2574
|
+
fits: z3.array(z3.object({
|
|
2575
|
+
summary: z3.string(),
|
|
2576
|
+
dp: z3.number(),
|
|
2577
|
+
tp: z3.number(),
|
|
2578
|
+
pp: z3.number(),
|
|
2579
|
+
ep: z3.number(),
|
|
2580
|
+
zero: z3.number().int(),
|
|
2581
|
+
sequence_parallel: z3.boolean(),
|
|
2582
|
+
recompute: z3.string(),
|
|
2583
|
+
per_gpu_bytes: z3.number(),
|
|
2584
|
+
used: z3.number(),
|
|
2585
|
+
notes: z3.array(z3.string())
|
|
2586
|
+
})),
|
|
2587
|
+
closest: z3.object({ summary: z3.string(), per_gpu_bytes: z3.number() }).optional(),
|
|
2588
|
+
notes: z3.array(z3.string())
|
|
2589
|
+
}),
|
|
2590
|
+
annotations: { ...READ, title: "Plan a cluster" }
|
|
2591
|
+
}, async ({ design_id, gpus, gpus_per_node, headroom, limit, ...rest }) => guard(() => {
|
|
2592
|
+
const record = store.get(design_id);
|
|
2593
|
+
const result = planCluster(record.doc, toAnalysisOptions(rest), {
|
|
2594
|
+
gpus,
|
|
2595
|
+
...gpus_per_node !== undefined ? { gpusPerNode: gpus_per_node } : {},
|
|
2596
|
+
...headroom !== undefined ? { headroom } : {},
|
|
2597
|
+
...limit !== undefined ? { limit } : {}
|
|
2598
|
+
});
|
|
2599
|
+
const text = result.fits.length === 0 ? [
|
|
2600
|
+
`nothing fits on ${gpus} x ${result.hardware}`,
|
|
2601
|
+
...result.closest ? [` closest: ${result.closest.summary} at ${formatBytes2(result.closest.perGpu.total)}`] : [],
|
|
2602
|
+
...result.notes.map((n) => ` ${n}`)
|
|
2603
|
+
].join(`
|
|
2604
|
+
`) : [
|
|
2605
|
+
`${gpus} x ${result.hardware}, ${formatBytes2(result.budget)} usable each, ${result.considered} plans priced`,
|
|
2606
|
+
...result.fits.map((p) => ` ${p.summary} ${formatBytes2(p.perGpu.total)} ${Math.round(p.used * 100)}% of budget`)
|
|
2607
|
+
].join(`
|
|
2608
|
+
`);
|
|
2609
|
+
return ok(text, {
|
|
2610
|
+
design_id: record.design_id,
|
|
2611
|
+
revision: record.revision,
|
|
2612
|
+
hardware: result.hardware,
|
|
2613
|
+
budget_bytes: result.budget,
|
|
2614
|
+
considered: result.considered,
|
|
2615
|
+
fits: result.fits.map((p) => ({
|
|
2616
|
+
summary: p.summary,
|
|
2617
|
+
dp: p.parallel.dp,
|
|
2618
|
+
tp: p.parallel.tp,
|
|
2619
|
+
pp: p.parallel.pp,
|
|
2620
|
+
ep: p.parallel.ep,
|
|
2621
|
+
zero: p.parallel.zero,
|
|
2622
|
+
sequence_parallel: p.parallel.sequenceParallel,
|
|
2623
|
+
recompute: p.recompute,
|
|
2624
|
+
per_gpu_bytes: p.perGpu.total,
|
|
2625
|
+
used: p.used,
|
|
2626
|
+
notes: p.notes
|
|
2627
|
+
})),
|
|
2628
|
+
...result.closest ? { closest: { summary: result.closest.summary, per_gpu_bytes: result.closest.perGpu.total } } : {},
|
|
2629
|
+
notes: result.notes
|
|
2630
|
+
});
|
|
2631
|
+
}));
|
|
2632
|
+
server.registerTool("tensorcad_diff", {
|
|
2633
|
+
title: "Compare two designs",
|
|
2634
|
+
description: "What changed between two designs and what it cost: the symbols, blocks and wires that moved, then the " + "parameters, FLOPs, cache and memory. Both sides are measured at one operating point, so the attention " + "terms are comparable. Use it after an edit, or against a preset, to check the change did what was meant.",
|
|
2635
|
+
inputSchema: z3.object({
|
|
2636
|
+
a: DESIGN_ID.describe("The design to compare from."),
|
|
2637
|
+
b: DESIGN_ID.describe("The design to compare to."),
|
|
2638
|
+
...analysisOptionsShape
|
|
2639
|
+
}),
|
|
2640
|
+
outputSchema: z3.object({
|
|
2641
|
+
a: z3.string(),
|
|
2642
|
+
b: z3.string(),
|
|
2643
|
+
identical: z3.boolean().describe("True when nothing structural moved; the numbers may still differ."),
|
|
2644
|
+
at: z3.object({ T: z3.number(), B: z3.number(), hardware: z3.string() }),
|
|
2645
|
+
symbols: z3.object({
|
|
2646
|
+
added: z3.array(z3.string()),
|
|
2647
|
+
removed: z3.array(z3.string()),
|
|
2648
|
+
changed: z3.array(z3.object({ name: z3.string(), from: z3.string(), to: z3.string() }))
|
|
2649
|
+
}),
|
|
2650
|
+
blocks: z3.object({
|
|
2651
|
+
added: z3.array(z3.string()),
|
|
2652
|
+
removed: z3.array(z3.string()),
|
|
2653
|
+
changed: z3.array(z3.object({
|
|
2654
|
+
path: z3.string(),
|
|
2655
|
+
params: z3.array(z3.object({ key: z3.string(), from: z3.string(), to: z3.string() }))
|
|
2656
|
+
}))
|
|
2657
|
+
}),
|
|
2658
|
+
edges: z3.object({ added: z3.number().int(), removed: z3.number().int() }),
|
|
2659
|
+
metrics: z3.array(z3.object({
|
|
2660
|
+
metric: z3.string(),
|
|
2661
|
+
a: z3.number(),
|
|
2662
|
+
b: z3.number(),
|
|
2663
|
+
delta: z3.number(),
|
|
2664
|
+
ratio: z3.number().nullable()
|
|
2665
|
+
}))
|
|
2666
|
+
}),
|
|
2667
|
+
annotations: { ...READ, title: "Compare two designs" }
|
|
2668
|
+
}, async ({ a, b, ...rest }) => guard(() => {
|
|
2669
|
+
const left = store.get(a);
|
|
2670
|
+
const right = store.get(b);
|
|
2671
|
+
const d = diffDesigns(left.doc, right.doc, toAnalysisOptions(rest));
|
|
2672
|
+
const brief = (v) => {
|
|
2673
|
+
if (v === undefined || v === null)
|
|
2674
|
+
return "—";
|
|
2675
|
+
if (typeof v === "object") {
|
|
2676
|
+
const o = v;
|
|
2677
|
+
const n = o.value ?? o.expr ?? o.default;
|
|
2678
|
+
if (n !== undefined)
|
|
2679
|
+
return String(n);
|
|
2680
|
+
}
|
|
2681
|
+
return String(v);
|
|
2682
|
+
};
|
|
2683
|
+
const text = [
|
|
2684
|
+
`${d.a} -> ${d.b} at T=${d.at.T}, B=${d.at.B}`,
|
|
2685
|
+
...d.identical ? ["structurally identical"] : [],
|
|
2686
|
+
...d.symbols.changed.map((s) => ` ~ ${s.name} ${brief(s.from)} -> ${brief(s.to)}`),
|
|
2687
|
+
...d.symbols.added.map((s) => ` + ${s.name} = ${brief(s.to)}`),
|
|
2688
|
+
...d.symbols.removed.map((s) => ` - ${s.name}`),
|
|
2689
|
+
...d.blocks.added.map((x) => ` + ${x.path} ${x.type}`),
|
|
2690
|
+
...d.blocks.removed.map((x) => ` - ${x.path} ${x.type}`),
|
|
2691
|
+
...d.blocks.changed.map((c) => ` ~ ${c.path} ${c.params.map((p) => p.key).join(", ")}`),
|
|
2692
|
+
"",
|
|
2693
|
+
...d.metrics.filter((m) => m.delta !== 0).map((m) => ` ${m.metric} ${formatCount2(m.a)} -> ${formatCount2(m.b)}`)
|
|
2694
|
+
].filter((l) => l !== "").join(`
|
|
2695
|
+
`);
|
|
2696
|
+
return ok(text, {
|
|
2697
|
+
a: d.a,
|
|
2698
|
+
b: d.b,
|
|
2699
|
+
identical: d.identical,
|
|
2700
|
+
at: d.at,
|
|
2701
|
+
symbols: {
|
|
2702
|
+
added: d.symbols.added.map((x) => x.name),
|
|
2703
|
+
removed: d.symbols.removed.map((x) => x.name),
|
|
2704
|
+
changed: d.symbols.changed.map((x) => ({
|
|
2705
|
+
name: x.name,
|
|
2706
|
+
from: brief(x.from),
|
|
2707
|
+
to: brief(x.to)
|
|
2708
|
+
}))
|
|
2709
|
+
},
|
|
2710
|
+
blocks: {
|
|
2711
|
+
added: d.blocks.added.map((x) => x.path),
|
|
2712
|
+
removed: d.blocks.removed.map((x) => x.path),
|
|
2713
|
+
changed: d.blocks.changed.map((c) => ({
|
|
2714
|
+
path: c.path,
|
|
2715
|
+
params: c.params.map((p) => ({ key: p.key, from: brief(p.from), to: brief(p.to) }))
|
|
2716
|
+
}))
|
|
2717
|
+
},
|
|
2718
|
+
edges: { added: d.edges.added.length, removed: d.edges.removed.length },
|
|
2719
|
+
metrics: d.metrics
|
|
2720
|
+
});
|
|
2721
|
+
}));
|
|
2722
|
+
server.registerTool("tensorcad_import_hf", {
|
|
2723
|
+
title: "Import a Hugging Face config",
|
|
2724
|
+
description: "Read a Hugging Face `config.json` into a design and save it in this session. Covers the Llama, Mistral, " + "Qwen, Gemma, Mixtral, DeepSeek and GPT-2 families. Anything the importer cannot model faithfully comes " + "back as a warning rather than being approximated silently.",
|
|
2725
|
+
inputSchema: z3.object({
|
|
2726
|
+
config: z3.string().describe("The contents of config.json."),
|
|
2727
|
+
name: z3.string().optional().describe("A name for the design; the config's own is used otherwise.")
|
|
2728
|
+
}),
|
|
2729
|
+
outputSchema: z3.object({
|
|
2730
|
+
design_id: z3.string(),
|
|
2731
|
+
name: z3.string(),
|
|
2732
|
+
params_total: z3.number(),
|
|
2733
|
+
warnings: z3.array(z3.string())
|
|
2734
|
+
}),
|
|
2735
|
+
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false, title: "Import a config" }
|
|
2736
|
+
}, async ({ config, name }) => guard(() => {
|
|
2737
|
+
const result = importHfConfig(config, name);
|
|
2738
|
+
const record = store.adopt(result.doc);
|
|
2739
|
+
const total = analyze2(result.doc).params.total;
|
|
2740
|
+
const text = [
|
|
2741
|
+
`${result.doc.meta.name}: ${formatCount2(total)} parameters`,
|
|
2742
|
+
...result.warnings.map((w) => ` warning: ${w}`)
|
|
2743
|
+
].join(`
|
|
2744
|
+
`);
|
|
2745
|
+
return ok(text, {
|
|
2746
|
+
design_id: record.design_id,
|
|
2747
|
+
name: result.doc.meta.name,
|
|
2748
|
+
params_total: total,
|
|
2749
|
+
warnings: result.warnings
|
|
2750
|
+
});
|
|
2751
|
+
}));
|
|
2752
|
+
}
|
|
2753
|
+
var TOOL_NAMES = [
|
|
2754
|
+
"tensorcad_list_designs",
|
|
2755
|
+
"tensorcad_new_design",
|
|
2756
|
+
"tensorcad_open_design",
|
|
2757
|
+
"tensorcad_save_design",
|
|
2758
|
+
"tensorcad_get_design",
|
|
2759
|
+
"tensorcad_get_block",
|
|
2760
|
+
"tensorcad_search_catalog",
|
|
2761
|
+
"tensorcad_apply_ops",
|
|
2762
|
+
"tensorcad_validate",
|
|
2763
|
+
"tensorcad_analyze",
|
|
2764
|
+
"tensorcad_generate_code",
|
|
2765
|
+
"tensorcad_checkpoint",
|
|
2766
|
+
"tensorcad_restore",
|
|
2767
|
+
"tensorcad_explain",
|
|
2768
|
+
"tensorcad_scale",
|
|
2769
|
+
"tensorcad_mup",
|
|
2770
|
+
"tensorcad_plan",
|
|
2771
|
+
"tensorcad_diff",
|
|
2772
|
+
"tensorcad_import_hf"
|
|
2773
|
+
];
|
|
2774
|
+
|
|
2775
|
+
// packages/mcp/src/server.ts
|
|
2776
|
+
var SERVER_NAME = "tensorcad";
|
|
2777
|
+
var SERVER_VERSION = package_default.version;
|
|
2778
|
+
var INSTRUCTIONS = [
|
|
2779
|
+
"TensorCAD designs transformer language models as a graph of typed blocks and reports what they would cost.",
|
|
2780
|
+
"",
|
|
2781
|
+
"Hold a design_id from tensorcad_new_design or tensorcad_open_design and pass it to everything else.",
|
|
2782
|
+
'Read with tensorcad_get_design format "outline" before reaching for the full document: it carries the whole',
|
|
2783
|
+
"structure, the symbol table and the shape on every edge for a fraction of the tokens.",
|
|
2784
|
+
"",
|
|
2785
|
+
"Edit through tensorcad_apply_ops. A batch is all-or-nothing, and passing expected_revision turns a concurrent",
|
|
2786
|
+
"edit into a clear error instead of a silent overwrite. Take an tensorcad_checkpoint before an experiment;",
|
|
2787
|
+
"tensorcad_restore puts it back, or undoes the last batch when you name no checkpoint.",
|
|
2788
|
+
"",
|
|
2789
|
+
"Most designs are parameterised by symbols (L layers, D width, H heads, Hkv key/value heads, dh head dim,",
|
|
2790
|
+
"F feed-forward width, V vocabulary), so a set_symbol operation is usually the right edit rather than",
|
|
2791
|
+
"touching individual blocks.",
|
|
2792
|
+
"",
|
|
2793
|
+
"After an edit, tensorcad_diff against the design you started from says what moved and what it cost;",
|
|
2794
|
+
"tensorcad_explain answers why one block is the size it is without reading the whole document.",
|
|
2795
|
+
"tensorcad_plan answers whether the thing would train on a given number of GPUs and how it would have to",
|
|
2796
|
+
"be split. tensorcad_scale shrinks a design to a bench budget, and tensorcad_import_hf reads a Hugging Face",
|
|
2797
|
+
"config.json into one. Both save their result as a new design, analysable and diffable like any other."
|
|
2798
|
+
].join(`
|
|
2799
|
+
`);
|
|
2800
|
+
function createServer2(options = {}) {
|
|
2801
|
+
const { store: given, bridge, ...storeOptions } = options;
|
|
2802
|
+
const store = given ?? new FileStore(storeOptions);
|
|
2803
|
+
const server = new McpServer2({ name: SERVER_NAME, version: SERVER_VERSION }, {
|
|
2804
|
+
capabilities: { tools: {}, resources: {}, prompts: {}, completions: {} },
|
|
2805
|
+
instructions: INSTRUCTIONS
|
|
2806
|
+
});
|
|
2807
|
+
registerTools(server, store);
|
|
2808
|
+
registerResources(server, store);
|
|
2809
|
+
registerPrompts(server);
|
|
2810
|
+
if (bridge)
|
|
2811
|
+
followEditor(server, bridge);
|
|
2812
|
+
return server;
|
|
2813
|
+
}
|
|
2814
|
+
function followEditor(server, bridge) {
|
|
2815
|
+
const stop = bridge.watch((change, from) => {
|
|
2816
|
+
if (from !== "editor")
|
|
2817
|
+
return;
|
|
2818
|
+
const base = `tensorcad://designs/${change.record.design_id}`;
|
|
2819
|
+
for (const uri of [base, `${base}/analysis`, `${base}/validation`]) {
|
|
2820
|
+
server.server.sendResourceUpdated({ uri }).catch(() => {});
|
|
2821
|
+
}
|
|
2822
|
+
});
|
|
2823
|
+
const closed = server.server.onclose;
|
|
2824
|
+
server.server.onclose = () => {
|
|
2825
|
+
stop();
|
|
2826
|
+
closed?.();
|
|
2827
|
+
};
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
// packages/mcp/src/serve.ts
|
|
2831
|
+
async function serve(root = process.env.TENSORCAD_ROOT ?? process.cwd()) {
|
|
2832
|
+
await loadEngine();
|
|
2833
|
+
const store = new FileStore({ root });
|
|
2834
|
+
const bridge = await startBridge(store, root);
|
|
2835
|
+
serveStdio(() => createServer2({ root, store, bridge }));
|
|
2836
|
+
process.stderr.write(`tensorcad mcp server on stdio, root ${root}
|
|
2837
|
+
`);
|
|
2838
|
+
}
|
|
2839
|
+
async function startBridge(store, root) {
|
|
2840
|
+
if (process.env.TENSORCAD_BRIDGE !== "1")
|
|
2841
|
+
return;
|
|
2842
|
+
const bridge = new BridgeServer({
|
|
2843
|
+
store,
|
|
2844
|
+
root,
|
|
2845
|
+
name: SERVER_NAME,
|
|
2846
|
+
version: SERVER_VERSION,
|
|
2847
|
+
port: Number(process.env.TENSORCAD_BRIDGE_PORT) || DEFAULT_BRIDGE_PORT
|
|
2848
|
+
});
|
|
2849
|
+
try {
|
|
2850
|
+
await bridge.start();
|
|
2851
|
+
} catch (e) {
|
|
2852
|
+
process.stderr.write(`tensorcad: the editor bridge did not start: ${e.message}
|
|
2853
|
+
`);
|
|
2854
|
+
return;
|
|
2855
|
+
}
|
|
2856
|
+
process.once("exit", () => clearSessionSync());
|
|
2857
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
2858
|
+
process.once(signal, () => {
|
|
2859
|
+
clearSessionSync();
|
|
2860
|
+
process.exit(0);
|
|
2861
|
+
});
|
|
2862
|
+
}
|
|
2863
|
+
return bridge;
|
|
2864
|
+
}
|
|
2865
|
+
export {
|
|
2866
|
+
serve
|
|
2867
|
+
};
|