@gridlock-compute/native-worker 0.1.1
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 +222 -0
- package/bin/gridlock-native-worker.cjs +4 -0
- package/dist/attestation-quote.d.ts +11 -0
- package/dist/attestation-quote.js +56 -0
- package/dist/attestation.d.ts +2 -0
- package/dist/attestation.js +11 -0
- package/dist/banner.d.ts +2 -0
- package/dist/banner.js +41 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.js +34 -0
- package/dist/console-io.d.ts +2 -0
- package/dist/console-io.js +10 -0
- package/dist/dashboard.d.ts +54 -0
- package/dist/dashboard.js +247 -0
- package/dist/gpu.d.ts +4 -0
- package/dist/gpu.js +168 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +53 -0
- package/dist/inference-ollama-worker.d.ts +9 -0
- package/dist/inference-ollama-worker.js +94 -0
- package/dist/inference.d.ts +27 -0
- package/dist/inference.js +223 -0
- package/dist/live-stats.d.ts +14 -0
- package/dist/live-stats.js +66 -0
- package/dist/lock.d.ts +2 -0
- package/dist/lock.js +46 -0
- package/dist/model-select.d.ts +3 -0
- package/dist/model-select.js +39 -0
- package/dist/ollama.d.ts +22 -0
- package/dist/ollama.js +195 -0
- package/dist/wallet.d.ts +2 -0
- package/dist/wallet.js +14 -0
- package/dist/worker.d.ts +10 -0
- package/dist/worker.js +437 -0
- package/package.json +58 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
const LIME = "\x1b[38;2;182;255;60m";
|
|
2
|
+
const DIM = "\x1b[2m";
|
|
3
|
+
const RESET = "\x1b[0m";
|
|
4
|
+
const GREEN = "\x1b[32m";
|
|
5
|
+
const YELLOW = "\x1b[33m";
|
|
6
|
+
const RED = "\x1b[31m";
|
|
7
|
+
/** Fixed height — keep in sync with buildLines(). */
|
|
8
|
+
const FIXED_LINES = 15;
|
|
9
|
+
function shouldColorize() {
|
|
10
|
+
if (process.env.GRIDLOCK_PLAIN_LOGS === "true")
|
|
11
|
+
return false;
|
|
12
|
+
const { FORCE_COLOR, NO_COLOR } = process.env;
|
|
13
|
+
if (FORCE_COLOR !== undefined) {
|
|
14
|
+
return FORCE_COLOR !== "0" && FORCE_COLOR.toLowerCase() !== "false";
|
|
15
|
+
}
|
|
16
|
+
if (NO_COLOR !== undefined)
|
|
17
|
+
return false;
|
|
18
|
+
return process.stdout.isTTY === true;
|
|
19
|
+
}
|
|
20
|
+
/** Interactive TTY dashboard — suppresses startup log lines. */
|
|
21
|
+
export function isDashboardMode() {
|
|
22
|
+
return shouldColorize() && process.stdout.isTTY === true;
|
|
23
|
+
}
|
|
24
|
+
function c(text, color) {
|
|
25
|
+
if (!shouldColorize())
|
|
26
|
+
return text;
|
|
27
|
+
return `${color}${text}${RESET}`;
|
|
28
|
+
}
|
|
29
|
+
function formatDuration(ms) {
|
|
30
|
+
const totalSec = Math.floor(ms / 1000);
|
|
31
|
+
const h = Math.floor(totalSec / 3600);
|
|
32
|
+
const m = Math.floor((totalSec % 3600) / 60);
|
|
33
|
+
const s = totalSec % 60;
|
|
34
|
+
if (h > 0)
|
|
35
|
+
return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
|
36
|
+
return `${m}:${String(s).padStart(2, "0")}`;
|
|
37
|
+
}
|
|
38
|
+
function activityBar(now, width = 18) {
|
|
39
|
+
const pulseWidth = 5;
|
|
40
|
+
const travel = width - pulseWidth;
|
|
41
|
+
const offset = Math.floor(now / 120) % (travel + 1);
|
|
42
|
+
const chars = Array.from({ length: width }, (_, i) => i >= offset && i < offset + pulseWidth ? "█" : "░");
|
|
43
|
+
return c(`[${chars.join("")}]`, LIME);
|
|
44
|
+
}
|
|
45
|
+
function connectionLabel(status) {
|
|
46
|
+
switch (status) {
|
|
47
|
+
case "connected":
|
|
48
|
+
return c("● Connected", GREEN);
|
|
49
|
+
case "disconnected":
|
|
50
|
+
return c("○ Disconnected", RED);
|
|
51
|
+
case "reconnecting":
|
|
52
|
+
return c("↻ Reconnecting", YELLOW);
|
|
53
|
+
default:
|
|
54
|
+
return c("… Starting", YELLOW);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function row(label, value, width = 76) {
|
|
58
|
+
const left = ` ${c(label, DIM)}`;
|
|
59
|
+
const gap = Math.max(1, 22 - label.length);
|
|
60
|
+
const content = left + " ".repeat(gap) + value;
|
|
61
|
+
return content.length > width ? content.slice(0, width) : content;
|
|
62
|
+
}
|
|
63
|
+
function topBorder(width = 76) {
|
|
64
|
+
const title = " Gridlock Worker Dashboard ";
|
|
65
|
+
const dashes = Math.max(1, width - title.length - 2);
|
|
66
|
+
return c(`┌${title}${"─".repeat(dashes)}┐`, LIME);
|
|
67
|
+
}
|
|
68
|
+
function bottomBorder(width = 76) {
|
|
69
|
+
return c(`└${"─".repeat(width - 2)}┘`, LIME);
|
|
70
|
+
}
|
|
71
|
+
export function createDashboard(initial, options = {}) {
|
|
72
|
+
const state = { ...initial };
|
|
73
|
+
const anchorRow = Math.max(1, options.anchorRow ?? 1);
|
|
74
|
+
const startedAt = Date.now();
|
|
75
|
+
let timer = null;
|
|
76
|
+
let jobTimer = null;
|
|
77
|
+
let lastNote = c("—", DIM);
|
|
78
|
+
let started = false;
|
|
79
|
+
let renderPending = false;
|
|
80
|
+
const enabled = shouldColorize() && process.stdout.isTTY;
|
|
81
|
+
function buildLines() {
|
|
82
|
+
const uptime = formatDuration(Date.now() - startedAt);
|
|
83
|
+
const jobs = `${state.jobsCompleted} session · ${state.jobsToday} today · ${state.jobsFailed} failed`;
|
|
84
|
+
const bench = `${state.benchmarkTokPerSec} tok/s`;
|
|
85
|
+
const live = state.liveTokPerSec !== null ? `${state.liveTokPerSec} tok/s` : c("—", DIM);
|
|
86
|
+
const queue = state.queueJobs !== null
|
|
87
|
+
? `${state.queueJobs} queued · ${state.inflightJobs ?? 0} net · ${state.workerInflightJobs ?? 0} worker`
|
|
88
|
+
: c("—", DIM);
|
|
89
|
+
const tokens = state.tokensTodayBase + (state.currentJob?.tokens ?? 0);
|
|
90
|
+
const lines = [
|
|
91
|
+
topBorder(),
|
|
92
|
+
row("Wallet", state.wallet),
|
|
93
|
+
row("API", state.api),
|
|
94
|
+
row("Hardware", state.hardware),
|
|
95
|
+
row("Model", `${state.model} (${state.inference})`),
|
|
96
|
+
row("Status", `${connectionLabel(state.connection)} · ${state.workerStatus}`),
|
|
97
|
+
row("Uptime", uptime),
|
|
98
|
+
row("Jobs", jobs),
|
|
99
|
+
row("Tokens", String(tokens)),
|
|
100
|
+
row("Throughput", `bench ${bench} · live ${live}`),
|
|
101
|
+
row("Router queue", queue),
|
|
102
|
+
];
|
|
103
|
+
if (state.currentJob) {
|
|
104
|
+
const job = state.currentJob;
|
|
105
|
+
const elapsed = formatDuration(Date.now() - job.startedAt);
|
|
106
|
+
const liveRate = state.liveTokPerSec !== null ? `${state.liveTokPerSec} tok/s` : "warming up";
|
|
107
|
+
lines.push(row("Current job", `${job.id.slice(0, 12)}… · ${elapsed}`));
|
|
108
|
+
lines.push(row("Generation", `${activityBar(Date.now())} ${job.tokens} tok · ${liveRate}`));
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
lines.push(row("Current job", c("idle — waiting for work", DIM)));
|
|
112
|
+
lines.push(row("Generation", c("idle", DIM)));
|
|
113
|
+
}
|
|
114
|
+
lines.push(row("Last event", lastNote));
|
|
115
|
+
lines.push(bottomBorder());
|
|
116
|
+
return lines;
|
|
117
|
+
}
|
|
118
|
+
function render() {
|
|
119
|
+
if (!enabled || !started)
|
|
120
|
+
return;
|
|
121
|
+
const lines = buildLines();
|
|
122
|
+
while (lines.length < FIXED_LINES)
|
|
123
|
+
lines.push("");
|
|
124
|
+
if (lines.length > FIXED_LINES)
|
|
125
|
+
lines.length = FIXED_LINES;
|
|
126
|
+
for (let i = 0; i < FIXED_LINES; i += 1) {
|
|
127
|
+
const rowNum = anchorRow + i;
|
|
128
|
+
process.stdout.write(`\x1b[${rowNum};1H\x1b[2K${lines[i] ?? ""}`);
|
|
129
|
+
}
|
|
130
|
+
process.stdout.write(`\x1b[${anchorRow + FIXED_LINES};1H`);
|
|
131
|
+
}
|
|
132
|
+
function scheduleRender() {
|
|
133
|
+
if (!started)
|
|
134
|
+
return;
|
|
135
|
+
if (renderPending)
|
|
136
|
+
return;
|
|
137
|
+
renderPending = true;
|
|
138
|
+
setImmediate(() => {
|
|
139
|
+
renderPending = false;
|
|
140
|
+
render();
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function startJobTicker() {
|
|
144
|
+
stopJobTicker();
|
|
145
|
+
jobTimer = setInterval(() => {
|
|
146
|
+
if (!state.currentJob)
|
|
147
|
+
return;
|
|
148
|
+
scheduleRender();
|
|
149
|
+
}, 100);
|
|
150
|
+
}
|
|
151
|
+
function stopJobTicker() {
|
|
152
|
+
if (!jobTimer)
|
|
153
|
+
return;
|
|
154
|
+
clearInterval(jobTimer);
|
|
155
|
+
jobTimer = null;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
update(patch) {
|
|
159
|
+
Object.assign(state, patch);
|
|
160
|
+
scheduleRender();
|
|
161
|
+
},
|
|
162
|
+
applyLiveStats(stats) {
|
|
163
|
+
state.jobsToday = Math.max(state.jobsToday, stats.jobsToday);
|
|
164
|
+
state.workerStatus = stats.workerStatus;
|
|
165
|
+
state.queueJobs = stats.queueJobs;
|
|
166
|
+
state.inflightJobs = stats.inflightJobs;
|
|
167
|
+
state.workerInflightJobs = stats.workerInflightJobs;
|
|
168
|
+
state.jobsFailed = Math.max(state.jobsFailed, stats.jobsFailedToday);
|
|
169
|
+
state.tokensTodayBase = Math.max(state.tokensTodayBase, stats.tokensToday);
|
|
170
|
+
if (!state.currentJob) {
|
|
171
|
+
state.liveTokPerSec = stats.wsTokPerSec > 0 ? stats.wsTokPerSec : null;
|
|
172
|
+
}
|
|
173
|
+
scheduleRender();
|
|
174
|
+
},
|
|
175
|
+
setJob(jobId, maxTokens) {
|
|
176
|
+
state.currentJob = {
|
|
177
|
+
id: jobId,
|
|
178
|
+
tokens: 0,
|
|
179
|
+
maxTokens: Math.max(1, maxTokens),
|
|
180
|
+
startedAt: Date.now(),
|
|
181
|
+
};
|
|
182
|
+
state.liveTokPerSec = 0;
|
|
183
|
+
startJobTicker();
|
|
184
|
+
scheduleRender();
|
|
185
|
+
},
|
|
186
|
+
setJobProgress(tokens, maxTokens) {
|
|
187
|
+
if (!state.currentJob)
|
|
188
|
+
return;
|
|
189
|
+
state.currentJob.tokens = Math.max(0, tokens);
|
|
190
|
+
if (maxTokens !== undefined)
|
|
191
|
+
state.currentJob.maxTokens = Math.max(1, maxTokens);
|
|
192
|
+
const elapsedSec = Math.max((Date.now() - state.currentJob.startedAt) / 1000, 0.001);
|
|
193
|
+
state.liveTokPerSec = Math.round((state.currentJob.tokens / elapsedSec) * 10) / 10;
|
|
194
|
+
scheduleRender();
|
|
195
|
+
},
|
|
196
|
+
clearJob(jobId, success, tokens, elapsedMs) {
|
|
197
|
+
if (success) {
|
|
198
|
+
state.jobsCompleted += 1;
|
|
199
|
+
state.tokensTodayBase += tokens;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
state.jobsFailed += 1;
|
|
203
|
+
}
|
|
204
|
+
if (state.currentJob?.id !== jobId) {
|
|
205
|
+
scheduleRender();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (elapsedMs > 0 && tokens > 0) {
|
|
209
|
+
state.liveTokPerSec = Math.round((tokens / (elapsedMs / 1000)) * 10) / 10;
|
|
210
|
+
}
|
|
211
|
+
state.currentJob = null;
|
|
212
|
+
stopJobTicker();
|
|
213
|
+
scheduleRender();
|
|
214
|
+
},
|
|
215
|
+
setConnection(status) {
|
|
216
|
+
state.connection = status;
|
|
217
|
+
scheduleRender();
|
|
218
|
+
},
|
|
219
|
+
note(message) {
|
|
220
|
+
lastNote = message.length > 52 ? `${message.slice(0, 49)}…` : message;
|
|
221
|
+
scheduleRender();
|
|
222
|
+
},
|
|
223
|
+
start() {
|
|
224
|
+
if (!enabled)
|
|
225
|
+
return;
|
|
226
|
+
process.stdout.write("\x1b[?25l");
|
|
227
|
+
started = true;
|
|
228
|
+
scheduleRender();
|
|
229
|
+
timer = setInterval(scheduleRender, 200);
|
|
230
|
+
},
|
|
231
|
+
stop() {
|
|
232
|
+
if (timer)
|
|
233
|
+
clearInterval(timer);
|
|
234
|
+
timer = null;
|
|
235
|
+
stopJobTicker();
|
|
236
|
+
if (!enabled || !started)
|
|
237
|
+
return;
|
|
238
|
+
started = false;
|
|
239
|
+
process.stdout.write("\x1b[?25h");
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
export function plainLog(msg) {
|
|
244
|
+
if (process.env.GRIDLOCK_PLAIN_LOGS === "true" || !process.stdout.isTTY) {
|
|
245
|
+
console.log(`[${new Date().toISOString()}] ${msg}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
package/dist/gpu.d.ts
ADDED
package/dist/gpu.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { cpus } from "node:os";
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
async function runCmd(command, args) {
|
|
9
|
+
try {
|
|
10
|
+
const { stdout } = await execFileAsync(command, args, {
|
|
11
|
+
timeout: 8_000,
|
|
12
|
+
maxBuffer: 1024 * 1024,
|
|
13
|
+
windowsHide: true,
|
|
14
|
+
});
|
|
15
|
+
const text = stdout.trim();
|
|
16
|
+
return text || null;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function nvidiaSmiBin() {
|
|
23
|
+
const override = process.env.GRIDLOCK_NVIDIA_SMI?.trim();
|
|
24
|
+
if (override)
|
|
25
|
+
return override;
|
|
26
|
+
if (process.platform === "win32") {
|
|
27
|
+
for (const path of [
|
|
28
|
+
join(process.env.ProgramFiles ?? "", "NVIDIA Corporation", "NVSMI", "nvidia-smi.exe"),
|
|
29
|
+
join(process.env.SystemRoot ?? "C:\\Windows", "System32", "nvidia-smi.exe"),
|
|
30
|
+
]) {
|
|
31
|
+
if (path && existsSync(path))
|
|
32
|
+
return path;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return "nvidia-smi";
|
|
36
|
+
}
|
|
37
|
+
async function rocmSmiBin() {
|
|
38
|
+
const override = process.env.GRIDLOCK_ROCM_SMI?.trim();
|
|
39
|
+
if (override && existsSync(override))
|
|
40
|
+
return override;
|
|
41
|
+
for (const candidate of ["rocm-smi", "amd-smi"]) {
|
|
42
|
+
try {
|
|
43
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
44
|
+
const { stdout } = await execFileAsync(cmd, [candidate], { encoding: "utf8", windowsHide: true });
|
|
45
|
+
const path = stdout.trim().split(/\r?\n/)[0]?.trim();
|
|
46
|
+
if (path)
|
|
47
|
+
return path;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
/* try next */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
async function detectCpuName() {
|
|
56
|
+
if (process.platform === "linux") {
|
|
57
|
+
try {
|
|
58
|
+
const cpuinfo = await readFile("/proc/cpuinfo", "utf8");
|
|
59
|
+
for (const line of cpuinfo.split("\n")) {
|
|
60
|
+
if (line.toLowerCase().includes("model name")) {
|
|
61
|
+
const name = line.split(":")[1]?.trim();
|
|
62
|
+
if (name)
|
|
63
|
+
return name;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
/* fall through */
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (process.platform === "darwin") {
|
|
72
|
+
const out = await runCmd("sysctl", ["-n", "machdep.cpu.brand_string"]);
|
|
73
|
+
if (out)
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
if (process.platform === "win32") {
|
|
77
|
+
const out = await runCmd("wmic", ["cpu", "get", "Name", "/format:list"]);
|
|
78
|
+
if (out) {
|
|
79
|
+
for (const line of out.split("\n")) {
|
|
80
|
+
if (line.toLowerCase().startsWith("name=")) {
|
|
81
|
+
const name = line.split("=")[1]?.trim();
|
|
82
|
+
if (name)
|
|
83
|
+
return name;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const model = cpus()[0]?.model?.trim();
|
|
89
|
+
return model || `${process.platform} CPU`;
|
|
90
|
+
}
|
|
91
|
+
async function detectNvidiaGpus() {
|
|
92
|
+
const out = await runCmd(nvidiaSmiBin(), [
|
|
93
|
+
"--query-gpu=index,name",
|
|
94
|
+
"--format=csv,noheader",
|
|
95
|
+
]);
|
|
96
|
+
if (!out)
|
|
97
|
+
return [];
|
|
98
|
+
const gpus = [];
|
|
99
|
+
for (const line of out.split("\n")) {
|
|
100
|
+
const parts = line.split(",").map((p) => p.trim());
|
|
101
|
+
if (parts.length < 2)
|
|
102
|
+
continue;
|
|
103
|
+
const index = Number.parseInt(parts[0] ?? "", 10);
|
|
104
|
+
const name = parts[1] ?? "";
|
|
105
|
+
if (!name)
|
|
106
|
+
continue;
|
|
107
|
+
gpus.push({
|
|
108
|
+
vendor: "nvidia",
|
|
109
|
+
name,
|
|
110
|
+
index: Number.isFinite(index) ? index : gpus.length,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return gpus;
|
|
114
|
+
}
|
|
115
|
+
async function detectAmdGpus() {
|
|
116
|
+
const bin = await rocmSmiBin();
|
|
117
|
+
if (!bin)
|
|
118
|
+
return [];
|
|
119
|
+
const out = await runCmd(bin, ["--showproductname"]);
|
|
120
|
+
if (!out)
|
|
121
|
+
return [];
|
|
122
|
+
const gpus = [];
|
|
123
|
+
for (const line of out.split("\n")) {
|
|
124
|
+
const trimmed = line.trim();
|
|
125
|
+
if (!trimmed || trimmed.startsWith("=") || !/gpu/i.test(trimmed))
|
|
126
|
+
continue;
|
|
127
|
+
const name = trimmed.includes(":") ? trimmed.split(":").slice(1).join(":").trim() : trimmed;
|
|
128
|
+
if (!name)
|
|
129
|
+
continue;
|
|
130
|
+
gpus.push({ vendor: "amd", name, index: gpus.length });
|
|
131
|
+
}
|
|
132
|
+
return gpus;
|
|
133
|
+
}
|
|
134
|
+
async function detectAllGpus() {
|
|
135
|
+
const nvidia = await detectNvidiaGpus();
|
|
136
|
+
if (nvidia.length > 0)
|
|
137
|
+
return nvidia;
|
|
138
|
+
return detectAmdGpus();
|
|
139
|
+
}
|
|
140
|
+
function selectGpu(gpus, gpuIndex) {
|
|
141
|
+
if (gpus.length === 0)
|
|
142
|
+
return null;
|
|
143
|
+
return gpus.find((g) => g.index === gpuIndex) ?? gpus[0] ?? null;
|
|
144
|
+
}
|
|
145
|
+
function formatGpuLabel(gpu) {
|
|
146
|
+
const prefix = gpu.vendor === "amd" ? "AMD" : gpu.vendor === "nvidia" ? "NVIDIA" : "";
|
|
147
|
+
if (prefix && !gpu.name.toLowerCase().includes(prefix.toLowerCase())) {
|
|
148
|
+
return `${prefix} ${gpu.name}`;
|
|
149
|
+
}
|
|
150
|
+
return gpu.name;
|
|
151
|
+
}
|
|
152
|
+
/** Detect compute hardware for router registration and startup logs. */
|
|
153
|
+
export async function detectHardwareTier() {
|
|
154
|
+
const override = process.env.GRIDLOCK_HW_TIER?.trim();
|
|
155
|
+
if (override)
|
|
156
|
+
return override;
|
|
157
|
+
const gpuIndex = Number.parseInt(process.env.GRIDLOCK_GPU_INDEX ?? "0", 10);
|
|
158
|
+
const gpus = await detectAllGpus();
|
|
159
|
+
const gpu = selectGpu(gpus, Number.isFinite(gpuIndex) ? gpuIndex : 0);
|
|
160
|
+
if (gpu)
|
|
161
|
+
return formatGpuLabel(gpu);
|
|
162
|
+
const cpuName = await detectCpuName();
|
|
163
|
+
return `CPU · ${cpuName}`;
|
|
164
|
+
}
|
|
165
|
+
/** @deprecated Use detectHardwareTier() */
|
|
166
|
+
export async function detectGpuName() {
|
|
167
|
+
return detectHardwareTier();
|
|
168
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { getBackendUrl } from "./config.js";
|
|
7
|
+
import { parseWorkerWallet } from "./wallet.js";
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
10
|
+
const program = new Command();
|
|
11
|
+
program
|
|
12
|
+
.name("gridlock-native-worker")
|
|
13
|
+
.description("Native headless worker for the Gridlock inference network")
|
|
14
|
+
.version(pkg.version)
|
|
15
|
+
.option("--wallet <address>", "EVM wallet address (worker identity)")
|
|
16
|
+
.option("--inference <backend>", "Inference backend: auto, ollama, or vllm", "auto")
|
|
17
|
+
.option("--model <tag>", "Ollama model tag (e.g. llama3.2:3b); prompts if multiple are installed")
|
|
18
|
+
.option("--benchmark", "Run benchmark only, then exit")
|
|
19
|
+
.action(async (opts) => {
|
|
20
|
+
const walletRaw = opts.wallet ?? process.env.GRIDLOCK_WALLET;
|
|
21
|
+
if (!walletRaw) {
|
|
22
|
+
console.error("Error: --wallet is required (or set GRIDLOCK_WALLET to your 0x EVM address).");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
let wallet;
|
|
26
|
+
try {
|
|
27
|
+
wallet = parseWorkerWallet(walletRaw);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
const inference = opts.inference;
|
|
34
|
+
if (!["auto", "ollama", "vllm"].includes(inference)) {
|
|
35
|
+
console.error('Error: --inference must be "auto", "ollama", or "vllm".');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const { startWorker } = await import("./worker.js");
|
|
40
|
+
await startWorker({
|
|
41
|
+
wallet,
|
|
42
|
+
backendUrl: getBackendUrl(),
|
|
43
|
+
inference,
|
|
44
|
+
model: opts.model,
|
|
45
|
+
benchmarkOnly: opts.benchmark ?? false,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
console.error(`Fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
program.parse();
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { parentPort } from "node:worker_threads";
|
|
2
|
+
parentPort?.on("message", async (req) => {
|
|
3
|
+
try {
|
|
4
|
+
const start = performance.now();
|
|
5
|
+
let firstAt = null;
|
|
6
|
+
let content = "";
|
|
7
|
+
let tokens = 0;
|
|
8
|
+
const res = await fetch(`${req.url.replace(/\/$/, "")}/api/chat`, {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers: { "Content-Type": "application/json", Connection: "close" },
|
|
11
|
+
body: JSON.stringify({
|
|
12
|
+
model: req.model,
|
|
13
|
+
messages: req.messages,
|
|
14
|
+
stream: true,
|
|
15
|
+
options: { num_predict: req.maxTokens },
|
|
16
|
+
}),
|
|
17
|
+
});
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
const text = await res.text();
|
|
20
|
+
parentPort?.postMessage({
|
|
21
|
+
type: "error",
|
|
22
|
+
error: `Ollama error ${res.status}: ${text.slice(0, 200)}`,
|
|
23
|
+
});
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (!res.body) {
|
|
27
|
+
parentPort?.postMessage({ type: "error", error: "Ollama returned empty body" });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const reader = res.body.getReader();
|
|
31
|
+
const decoder = new TextDecoder();
|
|
32
|
+
let buffer = "";
|
|
33
|
+
while (true) {
|
|
34
|
+
const { done, value } = await reader.read();
|
|
35
|
+
if (done)
|
|
36
|
+
break;
|
|
37
|
+
buffer += decoder.decode(value, { stream: true });
|
|
38
|
+
const lines = buffer.split("\n");
|
|
39
|
+
buffer = lines.pop() ?? "";
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
const trimmed = line.trim();
|
|
42
|
+
if (!trimmed)
|
|
43
|
+
continue;
|
|
44
|
+
try {
|
|
45
|
+
const chunk = JSON.parse(trimmed);
|
|
46
|
+
const piece = chunk.message?.content ?? "";
|
|
47
|
+
if (piece) {
|
|
48
|
+
if (firstAt === null)
|
|
49
|
+
firstAt = performance.now();
|
|
50
|
+
content += piece;
|
|
51
|
+
}
|
|
52
|
+
const evalCount = Number(chunk.eval_count ?? 0);
|
|
53
|
+
if (evalCount > 0) {
|
|
54
|
+
tokens = evalCount;
|
|
55
|
+
}
|
|
56
|
+
else if (piece) {
|
|
57
|
+
tokens = Math.max(tokens, Math.ceil(content.length / 4));
|
|
58
|
+
}
|
|
59
|
+
if (piece || evalCount > 0 || chunk.done) {
|
|
60
|
+
parentPort?.postMessage({
|
|
61
|
+
type: "progress",
|
|
62
|
+
tokens: Math.max(tokens, piece ? 1 : 0),
|
|
63
|
+
maxTokens: req.maxTokens,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
/* skip malformed chunk */
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const end = performance.now();
|
|
73
|
+
const ttftMs = Math.floor((firstAt ?? end) - start);
|
|
74
|
+
const outputTokens = Math.max(tokens, 1);
|
|
75
|
+
const tpotMs = outputTokens > 1 && firstAt
|
|
76
|
+
? Math.floor((end - firstAt) / (outputTokens - 1))
|
|
77
|
+
: 0;
|
|
78
|
+
parentPort?.postMessage({
|
|
79
|
+
type: "done",
|
|
80
|
+
result: {
|
|
81
|
+
content: content.trim() || "(empty)",
|
|
82
|
+
tokens: outputTokens,
|
|
83
|
+
ttftMs,
|
|
84
|
+
tpotMs,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
parentPort?.postMessage({
|
|
90
|
+
type: "error",
|
|
91
|
+
error: e instanceof Error ? e.message : String(e),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type InferenceBackend } from "./config.js";
|
|
2
|
+
export interface InferenceProgress {
|
|
3
|
+
tokens: number;
|
|
4
|
+
maxTokens: number;
|
|
5
|
+
}
|
|
6
|
+
export interface InferenceResult {
|
|
7
|
+
content: string;
|
|
8
|
+
tokens: number;
|
|
9
|
+
ttftMs: number;
|
|
10
|
+
tpotMs: number;
|
|
11
|
+
}
|
|
12
|
+
export interface InferenceOptions {
|
|
13
|
+
maxTokens?: number;
|
|
14
|
+
onProgress?: (progress: InferenceProgress) => void;
|
|
15
|
+
}
|
|
16
|
+
export interface ChatMessage {
|
|
17
|
+
role: string;
|
|
18
|
+
content: string;
|
|
19
|
+
}
|
|
20
|
+
export type ActiveBackend = "ollama" | "vllm";
|
|
21
|
+
export declare function getActiveBackend(): ActiveBackend;
|
|
22
|
+
export declare function getActiveModel(): string;
|
|
23
|
+
export declare function checkOllama(): Promise<boolean>;
|
|
24
|
+
export declare function checkVllm(): Promise<boolean>;
|
|
25
|
+
export declare function resolveInferenceBackend(preferred?: InferenceBackend, modelPreference?: string): Promise<ActiveBackend>;
|
|
26
|
+
export declare function runInference(messages: ChatMessage[], maxTokensOrOptions?: number | InferenceOptions, maybeOptions?: InferenceOptions): Promise<InferenceResult>;
|
|
27
|
+
export declare function runBenchmark(): Promise<number>;
|